☰
【FDE系列】阶段2:Day 34:数据清洗 — 把脏数据捋干净
2026/10/2 18:05:25 网站建设 项目流程

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段2·Day 34:数据清洗 — 把脏数据捋干净

FDE 学习系列教程 · 第二阶段 · 第 3 周 · Day 4预计时长:2.5 小时 | 难度:★★★☆☆ | 前置知识:SQL 基础、Python 文件读写、logging 日志

📌一句话目标:识别四类常见脏数据,用 Python + SQL 完成去重、缺失值处理、格式对齐、类型转换和脱敏,把一份脏 CSV 洗成标准结构化数据入库。


🧑‍🤝‍🧑 开场:FDE 最真实的工作场景来了

兄弟,前三天学的查询,前提都是"数据是干净的"。但现实会狠狠给你上一课。

客户发来一个 Excel 导出的 CSV,你满怀期待地打开——

设备名 温度 负责人 手机号 ← 问题 注塑机A1 65 李工 13812345678 注塑机A1 65 李工 13812345678 ← ① 跟上面完全重复 注塑机A2 八十二 王工 ← ② 温度是中文?手机号缺失? 注塑机a3 91 赵工 138-1234-5678 ← ③ 设备名小写、手机带横杠 "注塑机 A3" 91 赵工 13812345678 ← ④ 名字多空格,和上一行其实重复 冲压机B1 75 李工 13812345678 冲压机B1 75 李工 13812345678 ← ⑤ 又一条完全重复 注塑机A4 赵工 13811112222 ← ⑥ 温度为空

这就是江湖人称的脏数据(Dirty Data)。这种东西直接进数据库,统计结果全错、JOIN 关联不上、报表数字打架。客户还会一脸无辜地问你:"我们的数据……挺整齐的呀?"

今天你就学会怎么把它洗"干净"。这活儿不炫技,但数据清洗往往占数据分析 60% 以上的时间,是 FDE 的硬功夫。


📖 一、先认清四类脏数据

别上来就写代码,先建立"脏数据分类学"。对症下药才不乱:

┌──────────────────────────────────────────────────────────────┐ │ 脏数据的四大门派 │ ├──────────────┬───────────────────────────────────────────────┤ │ ① 重复数据 │ 同一条记录出现多次 │ │ (Duplicate) │ 完全重复 / 实质重复(大小写空格差异) │ │ │ 危害:统计数量翻倍、SUM 偏大 │ ├──────────────┼───────────────────────────────────────────────┤ │ ② 缺失值 │ 该有的数据是空的 │ │ (Missing) │ 温度没填、手机号空、NULL │ │ │ 危害:AVG 算错、JOIN 丢数据、程序报错 │ ├──────────────┼───────────────────────────────────────────────┤ │ ③ 格式不一致 │ 同一个意思,写法五花八门 │ │ (Inconsistent)│ "82" / "八十二" / "约82度" │ │ │ "138-1234-5678" / "13812345678" │ │ │ "注塑机a3" / "注塑机 A3" / "注塑机A3" │ │ │ 危害:去重失效、关联不上、无法计算 │ ├──────────────┼───────────────────────────────────────────────┤ │ ④ 类型/异常值│ 数字列混进文字、温度写成 9999(传感器故障) │ │ (Wrong Type)│ 危害:转 float 崩溃、统计被极端值带偏 │ └──────────────┴──────────────────────────────────────────────┘

📌清洗总原则:先标准化(去空格、统一大小写),再去重,最后处理缺失和类型。顺序很重要——如果不先把"注塑机a3"和"注塑机 A3"标准化成同一个样子,去重时根本认不出它俩是重复的。

清洗流水线全景图

脏 CSV ① 标准化 ② 去重 ③ 补缺/转类型 干净数据库 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 重复行 │ │ 去空格 │ │ 完全重复 │ │ 温度→数字 │ │ │ │ 中文数字 │ ───► │ 统一大小写│ ───► │ 去掉 │ ─► │ 缺失→NULL │ ──► │ MySQL │ │ 空手机号 │ │ 去横杠 │ │ 实质重复 │ │ 手机号兜底 │ │ 干净表 │ │ 大小写乱 │ │ 去多余字符│ │ 去掉 │ │ 记录日志 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ 每一步都打日志,改了什么可追溯

🖥️ 二、准备脏数据

在sql_practice文件夹新建dirty_data.csv,逐字粘进去(故意做脏):

device_name,temperature,owner_name,phone,type 注塑机A1,65,李工,13812345678,注塑 注塑机A1,65,李工,13812345678,注塑 注塑机A2,八十二,王工,,注塑 注塑机a3,91,赵工,138-1234-5678,注塑 "注塑机 A3",91,赵工,13812345678,注塑 冲压机B1,75,李工,13812345678,冲压 冲压机B1,75,李工,13812345678,冲压 冲压机B2,88,陈工,13987654321,冲压 注塑机A4,,赵工,13811112222,注塑

对照清单确认你能指出每行的毛病:

行

毛病

门派

1-2

完全重复

① 重复

3

温度"八十二"、手机空

③格式 + ②缺失

4

设备名小写a3、手机带横杠

③格式

5

设备名带空格和空格,与第4行实质重复

③格式 + ①重复

6-7

完全重复

① 重复

9

温度为空

②缺失


🖥️ 三、写清洗脚本(Python 标准库版)

这节也简单,会的同学可以重点看"清洗顺序"和"日志记录"两个思路。

新建06_clean.py:

""" 数据清洗实战:从脏 CSV 到干净数据库 文件:06_clean.py """ import sqlite3 import csv import logging # 配置日志:时间 [级别] 信息 —— 清洗过程全程留痕 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) log = logging.getLogger(__name__) # ① 读入脏 CSV def load_dirty_csv(path): with open(path, encoding="utf-8") as f: return list(csv.DictReader(f)) # ② 逐行清洗(先标准化,再转类型) def clean_records(records): # 中文数字 → 阿拉伯数字 的映射表 cn_map = {"八十二": "82", "九十": "90", "九十一": "91"} cleaned = [] for i, r in enumerate(records): # —— 标准化:去空格 —— r["device_name"] = r["device_name"].strip() r["owner_name"] = r["owner_name"].strip() # —— 标准化:设备名英文统一大写 + 去掉内部空格 —— # "注塑机a3" → "注塑机A3";"注塑机 A3" → "注塑机A3" r["device_name"] = r["device_name"].upper().replace(" ", "") # —— 温度:中文数字转阿拉伯,再转 float —— temp_str = r["temperature"].strip() if temp_str in cn_map: temp_str = cn_map[temp_str] try: r["temperature"] = float(temp_str) except (ValueError, TypeError): # 转不了(空字符串、奇怪文字)就设为 None,进库存 NULL r["temperature"] = None log.warning(f"第 {i+2} 行温度无法转换:'{temp_str}',已设为 NULL") # —— 手机号:去掉横杠和空格;空的设 None —— phone = r["phone"].strip().replace("-", "").replace(" ", "") r["phone"] = phone if phone else None if not phone: log.warning(f"第 {i+2} 行手机号缺失,已设为 NULL") r["type"] = r["type"].strip() cleaned.append(r) return cleaned # ③ 去重:标准化之后,按 (设备名, 负责人) 判重 def deduplicate(records): seen = set() unique = [] for r in records: key = (r["device_name"], r["owner_name"]) if key in seen: log.info(f"去除重复行:{r['device_name']} / {r['owner_name']}") continue seen.add(key) unique.append(r) return unique # ④ 存入干净的 SQLite 表 def save_to_db(records, db_path="fde_clean.db"): conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS devices_clean ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_name TEXT NOT NULL, temperature REAL, owner_name TEXT NOT NULL, phone TEXT, type TEXT, imported_at TEXT DEFAULT (datetime('now','localtime')) ) """) for r in records: cur.execute(""" INSERT INTO devices_clean (device_name,temperature,owner_name,phone,type) VALUES (?,?,?,?,?) """, (r["device_name"], r["temperature"], r["owner_name"], r["phone"], r["type"])) conn.commit() cur.execute("SELECT COUNT(*) FROM devices_clean") count = cur.fetchone()[0] conn.close() return count # ===== 主流程:按顺序串起来 ===== raw = load_dirty_csv("dirty_data.csv") log.info(f"原始数据:{len(raw)} 行") cleaned = clean_records(raw) log.info(f"标准化 + 类型转换完成:{len(cleaned)} 行") unique = deduplicate(cleaned) log.info(f"去重后:{len(unique)} 行(删掉 {len(cleaned)-len(unique)} 条重复)") count = save_to_db(unique) log.info(f"✅ 写入数据库:{count} 条干净数据")

运行并观察

python 06_clean.py

预期日志(注意每一步的行数变化):

[INFO] 原始数据:9 行 [WARNING] 第 4 行温度无法转换:'八十二',已设为 NULL ← 实际已被中文映射转成82,这行不会出现 [WARNING] 第 4 行手机号缺失,已设为 NULL [INFO] 去除重复行:注塑机A1 / 李工 [INFO] 去除重复行:注塑机A3 / 赵工 [INFO] 去除重复行:冲压机B1 / 李工 [INFO] 去重后:6 行(删掉 3 条重复) [WARNING] 第 10 行温度无法转换:'',已设为 NULL [INFO] ✅ 写入数据库:6 条干净数据

⚠️ 小细节:第 3 行温度是"八十二",会被cn_map先转成 "82" 再转 float,所以不会报警告;第 9 行温度是空字符串,映射表没有,float("")报错,才走 None 分支。可以在脚本里打断点或加 print 验证这个分支逻辑。

清洗后的干净数据(在 DBeaver 打开 fde_clean.db 查看)

device_name | temperature | owner_name | phone | type 注塑机A1 | 65.0 | 李工 | 13812345678 | 注塑 注塑机A2 | 82.0 | 王工 | NULL | 注塑 ← "八十二"→82,手机NULL 注塑机A3 | 91.0 | 赵工 | 13812345678 | 注塑 ← a3/A3 两种写法合并 冲压机B1 | 75.0 | 李工 | 13812345678 | 冲压 冲压机B2 | 88.0 | 陈工 | 13987654321 | 冲压 注塑机A4 | NULL | 赵工 | 13811112222 | 注塑 ← 空温度存为 NULL

9 行脏数据 → 6 行干净数据。世界清净了。


📖 四、SQL 清洗函数速查表

清洗不一定要在 Python 里做,很多标准化操作 SQL 也能直接干。这张表两边对照着用:

需求

SQL 函数

例子

对应 Python

去两端空格

TRIM(x)

TRIM(device_name)

.strip()

转大写

UPPER(x)

UPPER(name)

.upper()

转小写

LOWER(x)

LOWER(name)

.lower()

替换字符

REPLACE(x,a,b)

REPLACE(phone,'-','')

.replace('-','')

取子串

SUBSTR(x,起,长)

SUBSTR(phone,1,3)

切片s[:3]

拼接

x || y

'138' || '****'

a + b/ f-string

NULL 兜底

COALESCE(x,默认)

COALESCE(temp,0)

x or 默认

类型转换

CAST(x AS 类型)

CAST('82' AS REAL)

float(x)

去重

DISTINCT

SELECT DISTINCT name...

set()/drop_duplicates

💡COALESCE 是清洗界的暖男:COALESCE(字段, 默认值)意思是"这个字段如果是 NULL,就用默认值顶上",可以串多个:COALESCE(phone, '未登记', '未知')——第一个非 NULL 的胜出。比 MySQL 专属的IFNULL更通用(SQLite/MySQL/PG 都支持)。

用 SQL 直接查干净结果(不落库也能验证)

-- 同一份清洗逻辑用纯 SQL 表达(针对已入库的原始表) SELECT DISTINCT UPPER(REPLACE(TRIM(device_name), ' ', '')) AS 标准设备名, CAST(REPLACE(temperature, '八十二', '82') AS REAL) AS 温度, TRIM(owner_name) AS 负责人, REPLACE(REPLACE(TRIM(phone), '-', ''), ' ', '') AS 手机 FROM devices_raw;

🖥️ 五、数据脱敏:把敏感信息打码

清洗入库后,数据还常常要给别人看(做报表、发给外部、截图汇报)。手机号、身份证号这类 PII(个人身份信息)必须脱敏——入库存全量,展示打码。

-- 手机号脱敏:13812345678 → 138****5678 SELECT device_name AS 设备, owner_name AS 负责人, SUBSTR(phone, 1, 3) || '****' || SUBSTR(phone, 8, 4) AS 脱敏手机, COALESCE(CAST(temperature AS TEXT), '缺失') AS 温度 FROM devices_clean ORDER BY device_name;

结果:

设备 | 负责人 | 脱敏手机 | 温度 注塑机A1 | 李工 | 138****5678 | 65.0 注塑机A2 | 王工 | NULL | 缺失 注塑机A3 | 赵工 | 138****5678 | 91.0 冲压机B1 | 李工 | 138****5678 | 75.0 冲压机B2 | 陈工 | 139****4321 | 88.0 注塑机A4 | 赵工 | 138****2222 | 缺失

拆解手机号脱敏公式:

13812345678 ├─┬─┤├────┤├─┬─┤ 前3位 打码4位 后4位 SUBSTR(...,1,3) '****' SUBSTR(...,8,4) 138 **** 5678

📌脱敏 vs 加密 别混淆:

  • 脱敏:不可逆地隐藏一部分(138****5678),用于展示,看个大概但无法还原

  • 加密:可逆,有密钥能解开(如 AES),用于必须还原的存储场景

  • 报表展示用脱敏就够了,别把完整手机号随手贴到群里或截图里。


📖 六、❌ 常见翻车点

翻车点

后果

正确姿势

先去重后标准化

"a3" 和 "A3" 认不出是同一条,去重失效

先 TRIM/UPPER 标准化,再判重

空字符串当正常值

float("")直接崩,或统计出"零个字符"的怪结果

显式判断空,统一转 None/NULL

用== None判断

Python 里None要用is None

if not phone:或x is None

缺失值一律填 0

温度 0 是真实可能的值,会污染平均值

数值缺失填 NULL(AVG 自动忽略)或业务默认值,别想当然

清洗不留日志

出问题查不到改了啥、删了几条

每步打日志:原始多少、删掉多少、转换多少

直接覆盖原始数据

清洗规则错了,原始数据毁了找不回

脏数据只读,结果写新表/新文件

⚠️务必保留原始数据:清洗永远在"副本"上做,原始 CSV 和原始表绝不动。万一清洗逻辑有 bug,你还能推倒重来。这是数据工作者的底线。


📝 本课小结

知识点

一句话记住

四类脏数据

重复、缺失、格式不一致、类型/异常值

清洗顺序

先标准化 → 再去重 → 后补缺/转类型

去空格

Python.strip()/ SQLTRIM()

统一大小写

.upper()/UPPER()

去字符

.replace('-','')/REPLACE()

中文数字

映射字典替换后再转 float

类型转换

float()配 try/except,失败设 None

去重

Python 用set存 key;SQL 用DISTINCT

NULL 兜底

COALESCE(列, 默认值)

脱敏

SUBSTR取头尾 + 拼接打码,展示用

日志留痕

每步记录行数变化和异常,可追溯

保护原始

在副本上清洗,原始数据只读不覆盖

🧠 核心认知:数据清洗不是"碰运气修修补补",而是一条固定流水线——识别脏的类型、按正确顺序处理、每一步留日志、原始数据不动。把这套流程脚本化,以后客户再来 100 份脏 CSV,你改改配置就能批量洗。


📋 课后练习

练习 1:给脏数据加两种新"脏",升级清洗脚本(约 40 分钟)

在dirty_data.csv里追加两行:

注塑机A5,约82度,孙工,138 0000 5555, 注塑机a5,82,孙工,13800005555,

新问题:

  • 温度写成"约82度"(需要从文字里提取数字,提示用正则re.search(r'\d+', s))

  • type列有空值(提示:空的填"未知")

  • 这两行清洗后应识别为同一台设备而只保留一条

修改06_clean.py处理它们,跑通后确认最终是 7 条干净数据。

练习 2:用 SQL DISTINCT 验证去重(约 15 分钟)

把脏数据先导进一张devices_raw表,用SELECT DISTINCT 标准化列...去重,和 Python 脚本的结果对比,确认条数和内容一致。

练习 3:身份证脱敏(约 15 分钟)

写一条 SQL,把身份证号110101199001011234脱敏成110101********1234(保留前 6 位和后 4 位,中间 8 位打星)。提示:SUBSTR(x,1,6)+'********'+SUBSTR(x,15,4)。


🔭 下节预告

今天我们在轻量的 SQLite 里完成了清洗。但客户现场跑的是真正的MySQL。明天是本周收官,干三件大事:

  1. 安装配置 MySQL,把练习环境升级成企业级数据库

  2. 用SQLAlchemy + pandas在 Python 里优雅地读写数据库(告别手写一堆 cursor)

  3. 把第 2 周的工单 API 从内存列表升级为 MySQL 持久化——服务重启数据再也不丢,亲手体会"分层架构"带来的好处

明天过后,你就拥有一条完整的数据管道:脏 CSV → pandas 清洗 → MySQL → FastAPI 查询。本周的压轴大戏,明天见!


🌍附录:前置课程列表

阶段一:

【FDE系列】阶段1Day 1:AI 层级关系 — 四个嵌套的圈-CSDN博客

【FDE系列】阶段1Day 2:AI 三阶段发展史 — 会认 → 会判断 → 会创造-CSDN博客

【FDE系列】阶段1Day 3:符号 AI vs 机器学习 — 两条路线的本质区别-CSDN博客

【FDE系列】阶段1Day 4:Transformer 的历史意义 — 2017 年的分水岭-CSDN博客

【FDE系列】阶段1Day 5:本周复习与自测 — 检验你的 AI 认知地基-CSDN博客

【FDE系列】阶段1Day 6:Transformer 架构 — 一张图纸盖出千千万万栋楼-CSDN博客

【FDE系列】阶段1Day 7:LLM 本质 — 文字接龙机器-CSDN博客

【FDE系列】阶段1Day 8:Token — 模型眼中的最小单位-CSDN博客

【FDE系列】阶段1Day 9:AI 幻觉 — 为什么会一本正经地胡说八道-CSDN博客

【FDE系列】阶段1Day 10:上下文窗口 — 模型的记忆力上限 + 本周复习-CSDN博客

【FDE系列】阶段1Day 11:Prompt — 给模型立规矩-CSDN博客

【FDE系列】阶段1Day 12:Memory — 让模型记住上下文

【FDE系列】阶段1Day 13:RAG — 给模型配图书管理员-CSDN博客

【FDE系列】阶段1Day 14:Tool Use — 让模型动手操作-CSDN博客

【FDE系列】阶段1Day 15:MCP — 统一的工具接口标准 + 第三周复习-CSDN博客

【FDE系列】阶段1Day 16:什么是 FDE — 把 AI 变成客户结果的人-CSDN博客

【FDE系列】阶段1Day 17:FDE vs 传统实施 — 三大本质区别-CSDN博客

【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客

【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客

【FDE系列】阶段1Day 20:阶段总结与产出物 — 第一阶段收官-CSDN博客


阶段二:

【FDE系列】阶段2:Day 21:Python 环境搭建 — 写出你的第一行代码-CSDN博客

【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 — Python 的“记忆“和“判断“-CSDN博客

【FDE系列】阶段2:Day 23:循环与函数 — 让代码跑 100 遍、把逻辑打包复用-CSDN博客

【FDE系列】阶段2:Day 24:数据结构 — 列表、字典、集合、元组-CSDN博客

【FDE系列】阶段2:Day 25:文件读写与 JSON — 让程序连通外部数据(第一周收官)-CSDN博客

【FDE系列】阶段2:Day 26:模块化编程 — 把代码拆成“抽屉柜“-CSDN博客

【FDE系列】阶段2:Day 27:异常处理与日志 — 让程序“摔不烂、查得到“-CSDN博客

【FDE系列】阶段2:Day 28:FastAPI 入门 — 把你的函数变成 API 服务-CSDN博客

【FDE系列】阶段2:Day 29:FastAPI 进阶 — Pydantic 模型与完整 CRUD 实战-CSDN博客

【FDE系列】阶段2:Day 30:生产代码规范 — 测试、类型注解、配置管理(第二周收官)-CSDN博客

【FDE系列】阶段2:Day 31:SQL 基础 — 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 — JOIN 与聚合-CSDN博客

【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE-CSDN博客

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询