数据清洗全流程指南:从规则设计到质量报告与监控
2026/8/30 2:39:56 网站建设 项目流程

做数据分析的人应该都有这样的经历:一份看似完整的报表,按用户维度汇总后,人数却对不上;同一个用户在一张表里出现两次,手机号格式还不一样;日期字段一部分是2024-01-01,另一部分是20240101。真正决定分析质量的,往往不是最后那个漂亮的折线图,而是进入分析之前的数据清洗。数据清洗做到位,数据本身会变得清晰、一致、可解释。这也是“清者自清万人识”在工程里的含义:数据是否干净,不靠某个人拍脑袋,而靠一套可复现的规则,让业务、开发和下游模型看到同一份质量结论。

这篇文章会围绕一条完整的数据清洗流水线展开。你会看到一份包含重复值、缺失值、异常值、格式不一致和非法手机号的样例数据,然后按“读入、探查、修正、校验、落库”五个阶段完成清洗。文章还会给出规则表、清洗报告、常见问题和发布前检查清单,方便你直接套用到自己的项目里。

1. 数据清洗不是简单删空值,而是一套带规则的认知工程

数据清洗在很多团队里被理解成“把空值删掉、把重复去掉”。这个理解不够完整。空值和重复只是最外层的问题,真正难处理的是同一份数据在不同人眼里有不同含义。比如user_id大小写不一致,SQL 里JOIN就会丢数据;城市字段中文和英文混写,GROUP BY之后会出现两个“北京”。

1.1 “清”指的是数据质量可被所有人识别

数据清洗的目标不是让数据“看起来顺眼”,而是让数据质量可以被量化、被审查、被复现。工程上通常会从这几个维度评估一份数据是否干净:

质量维度要回答的问题常见脏数据表现
完整性关键字段是否缺失手机号为空、订单金额为NULL
唯一性是否存在重复记录同一用户出现两次
准确性字段值是否真实有效手机号长度不足、金额为负数
一致性同一对象在不同表里是否统一user_id大小写混用、城市中英文混写
及时性数据是否在预期时间到达日期字段出现未来时间
有效性值是否满足业务规则状态字段出现未定义的枚举值

“万人识”并不是说所有人都去读原始数据,而是指清洗规则、清洗报告、字段定义都是透明的。业务人员可以说“手机号规则是 11 位且以 1 开头”,开发人员可以在代码里找到对应规则,模型训练时也可以解释为什么某些行被排除。大家看到的是同一套结论,而不是各自凭感觉处理数据。

1.2 什么时候需要清洗,什么时候不要过度清洗

清洗不是越多越好。过度清洗会把有效信息当成噪声删掉,或者用错误规则填出“看起来很合理”的值。常见场景里,这几类数据一般需要清洗:

场景典型脏数据是否清洗
外部 Excel 活动报名表姓名带空格、手机号格式混杂、重复提交需要清洗后入库
老系统数据迁移新旧编码不一致、字典值不统一需要清洗并保留映射关系
埋点日志解析字段缺失、时间格式混杂、值为"null"字符串需要清洗后进入分析层
数据库直接导出的规范表主键唯一、类型固定只需做基础校验
一次性临时查询数据量小,分析完即丢弃优先探查,不要盲目清洗

判断标准很简单:如果数据要进入报表、模型、对外接口或者跨团队使用,就需要清洗。如果只是临时看一眼,清洗规则不明确时,先做探查比直接删除更安全。清者自清的前提,是先把规则定清楚。

2. 环境准备和样例数据集构建

为了把清洗流程讲清楚,这里用 Python 加 pandas 构建一份模拟的“用户报名表”。数据规模不大,适合入门实验,也适合作为复杂项目的原型。数据量更大时,可以换到 Spark 或数据库 SQL 清洗,但核心思路一致。

2.1 使用 Python + pandas 作为清洗底座

pandas 适合处理表格型数据,空值、重复值、格式转换都内置了方法。在这类任务里,我会额外强调读入时的字段类型设置,因为很多脏数据问题是从类型推断开始的。

pip install pandas numpy openpyxl

如果后续需要读写 Parquet 格式,再安装 pyarrow:

pip install pyarrow

如果原始材料没有给出明确版本,落地前要确认项目锁定的 Python 和 pandas 版本。下面示例不依赖高阶 API,Python 3.8 以上的常见版本都可以运行。

2.2 构造一份包含常见脏数据的样例表

先创建一份模拟数据,写入raw_users.csv。样例中包含大小写不一致的用户 ID、重复手机号、空手机号、非法手机号、混合日期格式、带空格文本和异常金额。

import pandas as pd import numpy as np raw = pd.DataFrame({ "user_id": ["U001", "u001", "U002", "U003", "U004", "U005", "U006", "U007", "U008", "U009"], "user_name": [" 张三 ", "张三", "李四", "王五", "赵六", "孙七", "周八", "吴九", "郑十", "钱十一"], "city": ["北京", "北京", "上海", "广州", "深圳", "杭州", "南京", "武汉", "成都", "西安"], "phone": [ "13800138000", "13800138000", "13900139000", "15800158000", "123456", "18800000000", None, "17600176000", "13512345678", "19900199000" ], "register_date": [ "2024-01-01", "2024/01/01", "2024-01-02", "20240103", "2024-01-05", None, "2024-02-01", "2024-02-02", "2024-02-03", "2024-02-04" ], "channel": [" App ", "APP", "Web", "H5", "APP", "小程序", "web", "APP", "H5", "APP"], "amount": [199.0, 199.0, 58.5, 1001.2, 999999.99, 0.0, 88.8, 66.6, 12.0, 5000.0], "status": ["success", "SUCCESS", "failed", "success", "success", "pending", "success", "success", "failed", "success"] }) raw.to_csv("raw_users.csv", index=False, encoding="utf-8-sig")

这份数据刻意融入了几个典型问题。user_idU001u001,规范后是重复用户。phone列存在空值、重复值和123456这种明显非法值。register_date混用了-/和无分隔符格式。channel有前后空格和大小写差异。amount中存在999999.99这种超出常规范围的异常值。

注意:示例中的手机号和用户均为演示数据,不能直接当成线上数据使用。实际项目要把这类表名、字段名和隐私字段脱敏规则替换成自己的。

3. 清洗主流程:按“读入-探查-修正-校验-落库”五步走

清洗最忌讳直接对原始文件动手。正确顺序是:先固定字段类型读入,再系统探查,然后统一修正,接着做校验标记,最后把清洗结果和报告一起落库。这个顺序能避免“改完发现读错了类型,全部白做”。

3.1 读入数据时先固定字段类型,不要全部交给 pandas 猜测

手机号如果被读成整数,会丢失前导零;用户 ID 如果被读成整数,U001这类值会被转成错误内容。所以读入时要明确指定dtype

import pandas as pd import re df = pd.read_csv( "raw_users.csv", dtype={ "user_id": str, "user_name": str, "city": str, "phone": str, "channel": str, "status": str }, encoding="utf-8-sig" )

这里把字符串字段全部显式声明为str,避免 pandas 自动推断。phone即使看起来像数字,也按字符串处理。读取时还要注意编码:用utf-8-sig而不是utf-8,因为保存 CSV 时使用的utf-8-sig会写入 BOM 头,之后用 Excel 打开也不容易乱码。

3.2 先探查再动手:完成空值、重复值、异常值盘点

清洗之前先掌握数据全貌。下面代码查看字段类型、空值数量、重复 ID 和城市分布。

print(df.info()) print("空值数量:") print(df.isna().sum()) print("重复 user_id 数量:") print(df["user_id"].duplicated().sum()) duplicated_users = df[df["user_id"].duplicated(keep=False)] print("重复 user_id 的全部记录:") print(duplicated_users) print("城市分布:") print(df["city"].value_counts(dropna=False))

从样例数据可看到,phone有 1 个空值,register_date有 1 个空值,user_id表面看没有重复,但U001u001会被当成两个值。如果直接按user_id去重,就会漏掉这个重复。这也说明去重前必须先统一格式。

异常值的探查不能只依赖describe()。数值字段可以用describe()看最大值和分位数,但业务上是否异常,要结合业务阈值判断。

print(df["amount"].describe())

amount的最大值会显示为999999.99,明显偏离大多数订单金额。此时不要直接删除,先标记成异常,再交给业务确认。

3.3 用统一规则修正:标识、文本、日期、金额

先做格式统一,再做去重。顺序很重要:先清洗user_id,再drop_duplicates,才能把大小写导致的重复识别出来。

def normalize_id(value): if pd.isna(value): return value return str(value).strip().upper() def normalize_text(value): if pd.isna(value): return value return re.sub(r"\s+", "", str(value)).strip() def normalize_channel(value): if pd.isna(value): return value return str(value).strip().lower() def normalize_status(value): if pd.isna(value): return value return str(value).strip().lower() df["user_id"] = df["user_id"].map(normalize_id) df["user_name"] = df["user_name"].map(normalize_text) df["channel"] = df["channel"].map(normalize_channel) df["status"] = df["status"].map(normalize_status) df["register_date"] = pd.to_datetime(df["register_date"], errors="coerce")

register_date列在转换后可能产生NaT,这不是错误,而是标记异常日期。errors="coerce"的作用是让无法解析的值变成空值而不是抛出异常,方便后续统一处理。

日期统一之后,再处理重复:

df = df.drop_duplicates(subset=["user_id"], keep="first").copy()

为什么保留第一条而不是最后一条?因为原始数据的写入顺序通常代表到达顺序,第一条往往更接近用户首次提交。如果业务规则是“保留最新状态”,则应该改成keep="last"。这个选择必须在清洗报告里说明,否则下游无法判断。

3.4 保留异常标记,而不是“假干净”

清洗的常见误区是把异常行直接删掉。更好的做法是新增标记列,把“数据有异常”这件事保留下来。比如手机号是否合法、金额是否异常,都可以用布尔列标识。

def is_valid_phone(value): if pd.isna(value): return False phone = re.sub(r"\D", "", str(value)) return bool(re.fullmatch(r"1[3-9]\d{9}", phone)) df["phone_valid"] = df["phone"].map(is_valid_phone) amount_lower = 0 amount_upper = 100000 df["amount_outlier"] = ~df["amount"].between(amount_lower, amount_upper) print(df[ ["user_id", "phone", "phone_valid", "amount", "amount_outlier", "register_date"] ].to_string())

手机号校验规则是示例,实际项目要根据业务区域和运营商号段调整。比如有些海外手机号长度不同,就不能用中国手机号规则一刀切。金额阈值也要来自业务,不能只靠统计分位数拍脑袋。

保留标记列之后,下游可以使用“仅合法手机号且非异常金额”的用户做分析,同时还能看到被排除的原因。这样做数据分析仍然是“万人识”:每个人都知道某一行为什么不被采用。

4. 用一张清洗规则表管理规则,而不是散落在代码里

小项目里清洗逻辑可以直接写在脚本里,但多人协作或数据进入生产环境后,散落的if-else很难审查。你不知道哪条规则是谁加的,参数为什么这么设。清洗规则表可以解决这个问题。

4.1 为什么规则表比 if-else 更好维护

规则表本质上是把代码里的判断逻辑抽成配置。业务人员能读规则表,开发人员能按规则实现,测试人员能按规则造数据。规则表必须包含至少四部分:规则编号、字段、规则类型、规则参数和动作。

规则编号字段规则类型规则参数动作
R001user_id标准化strip + upper覆盖原字段
R002phone格式校验正则:^1[3-9]\d{9}$生成 phone_valid
R003amount范围校验0 ~ 100000生成 amount_outlier
R004register_date日期解析errors=coerce覆盖原字段
R005channel标准化strip + lower覆盖原字段
R006user_id去重keep first删除重复行

规则表的好处是清洗流程可审计。如果业务说“手机号规则不能限制为中国手机号”,只需要改规则表,不用翻代码。

4.2 规则表设计与最小执行引擎

规则表可以存在 JSON、YAML 或数据库配置表中。这里用 JSON 示例:

{ "rules": [ { "id": "R001", "field": "user_id", "type": "normalize", "params": { "method": "strip_upper" }, "action": "overwrite" }, { "id": "R002", "field": "phone", "type": "regex_check", "params": { "pattern": "^1[3-9]\\d{9}$" }, "action": "flag", "flag_column": "phone_valid" }, { "id": "R003", "field": "amount", "type": "range_check", "params": { "min": 0, "max": 100000 }, "action": "flag", "flag_column": "amount_outlier" } ] }

对应的最小执行引擎可以按规则类型分发:

def apply_regex_check(df, rule): pattern = rule["params"]["pattern"] df[rule["flag_column"]] = df[rule["field"]].astype(str).str.fullmatch(pattern) return df def apply_range_check(df, rule): min_value = rule["params"]["min"] max_value = rule["params"]["max"] df[rule["flag_column"]] = df[rule["field"]].between(min_value, max_value) return df

这里保留了R002的语义:规则名为regex_check,flag 列phone_valid表示真值。业务上“合法手机号”为真,非法行为假,逻辑更直接。

注意:规则配置化不代表完全去掉代码。规则类型对应的执行函数仍然需要测试,尤其是新增规则类型时,要同时补充单元测试。

5. 用清洗报告验证效果,而不是“看上去对了”

清洗完成后不能只说“清洗好了”。判断清洗是否成功,要靠清洗报告和数据快照。清洗报告要回答三个问题:处理前有多少行,处理后有多少行,每一类问题处理了多少。

5.1 清洗前后必须保留可比较的数据快照

不要在原文件上原地修改。项目目录里至少保留raw/clean/两个目录,按日期归档。

from pathlib import Path Path("archive").mkdir(exist_ok=True) raw.to_csv("archive/raw_users_20250101.csv", index=False, encoding="utf-8-sig") clean_df = df.copy() clean_df.to_csv("archive/clean_users_20250101.csv", index=False, encoding="utf-8-sig")

归档时可以把raw快照放在只读目录,或者加上版本号。这样做的好处是:下游发现问题后,可以回溯源数据,判断是清洗规则问题,还是源数据发生了变化。

5.2 汇总一张清洗报告,让业务和开发看到同一份结论

清洗报告用一张汇总表表达。示例字段可以这样计算:

report_rows = [ {"指标": "原始行数", "数值": len(raw)}, {"指标": "清洗后行数", "数值": len(clean_df)}, {"指标": "重复 user_id 删除行数", "数值": len(raw) - len(clean_df)}, {"指标": "手机号空值数量", "数值": int(raw["phone"].isna().sum())}, {"指标": "清洗后手机号非法数量", "数值": int((~clean_df["phone_valid"]).sum())}, {"指标": "金额异常数量", "数值": int(clean_df["amount_outlier"].sum())}, {"指标": "日期解析失败数量", "数值": int(clean_df["register_date"].isna().sum())}, ] report_df = pd.DataFrame(report_rows) print(report_df.to_string(index=False))

这份报告本身也是一份数据,需要能被导出和归档。只有清洗报告稳定输出后,后续每次调度才能对比规则是否生效、数据质量是变好还是变坏。

在生产环境里,清洗报告建议同时写入监控系统或数据库表。开发看日志,业务看报表,运维看告警。这样数据质量变化就能被及时发现,而不是等下游报表出错后才排查。

6. 常见问题排查

实际清洗中,很多问题不是规则复杂,而是读取、编码、类型和边界条件。以下四类问题出现频率很高。

6.1 中文乱码

现象:读取 CSV 后,中文变成乱码,或者用 Excel 打开导出的 CSV 时中文显示异常。

常见原因:读取时指定的编码和文件保存时的编码不一致。CSV 可能由gbkgb18030utf-8保存,但代码用另一种编码读取。

检查方式:用编辑器打开 CSV,查看文件右下角编码;或者在 Python 中分别尝试encoding="utf-8"encoding="utf-8-sig"encoding="gbk"

处理建议:统一约定写入和读取都使用utf-8-sig。如果源文件是 Excel 另存的 CSV,优先尝试gbk。不要在代码里反复切换编码,建议在数据接入层做一次统一转换。

6.2 手机号明明正常却被正则拦下

现象:肉眼检查手机号是 11 位数字,但正则校验返回False

常见原因:字段被读成浮点数,13800138000被显示成13800138000.0;手机号带有空格、横线或不可见字符;Excel 单元格如果以数字保存,导出后可能丢失前导零。

检查方式:打印repr(value)查看实际字符;查看字段的dtype是否为objectstr

处理建议:读入时将手机号指定为str;校验前先用re.sub(r"\D", "", str(value))去掉非数字字符。如果规则要求严格按原始输入校验,则需要在清洗报告中保留原始值和规范化值两列。

6.3 日期字段全部变成 NaT

现象:pd.to_datetime()之后,整列日期变成NaT

常见原因:列中存在完全无法解析的文本,或者日期格式被 Excel 转成了序列号,比如45292这种表示日期的数值。errors="coerce"会把所有无法解析的值转为NaT,有时候反而把整列“吞掉”。

检查方式:查看to_datetime之前的unique()值,确认格式;打印转换后日期为空的行,观察前后差异。

处理建议:先对日期做探查,判断格式是否统一。如果只有少量格式混杂,可以用format参数指定主格式;如果格式多样,可以按样例正则分组后分别解析。不要把errors="coerce"当作万能选项,它只是避免抛异常,不等于日期是对的。

6.4 空值一删,数据量少了 30%

现象:按“至少有一个空值就删行”或“删除所有含空值的行”之后,有效数据大幅减少。

常见原因:没有区分关键字段和普通字段。比如phone为空不代表user_idamount不能用,直接整行删除会损失大量信息。

检查方式:按列统计空值比例,而不是只看总行数。必要时查看空值行的其他字段是否完整。

处理建议:不要全局dropna()。先给字段分优先级,核心字段为空才需要处理;普通字段为空可以标记或使用默认值。另一个思路是计算行完整度,只删除完整度极低的行。

min_completeness = 0.8 row_null_ratio = df.isna().sum(axis=1) / df.shape[1] df_filtered = df[row_null_ratio <= (1 - min_completeness)].copy()

这个逻辑表示保留至少 80% 字段非空的行。阈值由业务决定,写进清洗规则表之后,要比“删除所有空值行”更容易解释。

7. 最佳实践:清洗过程也要可审查、可回滚、可复用

清洗流程上线后,真正决定成败的不是第一次清洗效果,而是后续维护。源数据一变、业务规则一变、下游口径一变,清洗代码都要跟着变。可审查、可回滚、可复用是生产环境的基本要求。

7.1 原始数据永远保留一份

清洗是对原始数据的加工,不是对原始数据的销毁。任何时候都不要直接覆盖raw文件,也不要让清洗脚本先读原表再写回原表。落库时,清洗结果可以写入新表,例如dwd_user_clean,原始表保持不动。

保留原始数据的另一个作用是对账。当清洗后报表数据与旧系统不一致时,可以从原始数据重新计算,判断是规则变化还是源数据变化。

7.2 清洗规则要有唯一编号和生效时间

规则编号要进入代码注释、清洗报告和数据库表。比如R001表示用户 ID 标准化,R002表示手机号校验。规则上线时要记录生效时间,规则下线时要保留历史记录,不能直接删除。

7.3 清洗代码要写成可测试函数

不要把所有清洗逻辑写在脚本最外层。推荐把一个完整流程封装成函数,输入原始 DataFrame,输出清洗后的 DataFrame 和清洗报告。

def clean_user_data(raw_df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: df = raw_df.copy() df["user_id"] = df["user_id"].map(normalize_id) df["phone_valid"] = df["phone"].map(is_valid_phone) return df, report_df

这样写的好处是可以对函数做单元测试:给定一行正常数据、一行空值数据、一行非法手机号数据,断言输出结果是否符合预期。等到规则越来越多时,测试能够帮助你快速定位是哪条规则误伤了数据。

7.4 发布前检查清单

清洗任务上线前,逐项确认以下内容:

检查项是否完成
原始数据已归档,且不参与原地覆盖待确认
每条清洗规则都有唯一编号和生效时间待确认
清洗报告包含清洗前后行数和关键字段质量指标待确认
非法数据是标记而不是全部删除待确认
日期、金额、手机号字段类型已固定待确认
过滤阈值和业务方确认过待确认
清洗输出表有独立表名或版本号待确认
异常值不会阻断整个清洗任务待确认
任务失败时有日志和告警待确认
有回滚方式,可以重新执行上一版本规则待确认

这张清单也适合作为代码评审的检查项。数据清洗不是“一次性写完就结束”,而是每一次规则变更都走同样的审查流程。

8. 扩展:从一次性清洗走向持续数据质量监控

清洗流程跑通之后,下一步是把“清者自清”变成一种持续状态。不要让数据质量靠人工救火,而是让数据质量指标自动上报。

8.1 把清洗规则固化成质量校验任务

可以把规则表里的规则转成定时任务,每天对增量数据执行同样的校验。校验结果写入质量表,至少记录三个指标:规则编号、校验时间、异常行数。

常见实现有两种。一种是自己写定时任务,把清洗报告写到数据库表;另一种是使用开源数据质量工具,把规则定义为 expectation 套件。无论哪种方式,核心都是把“手机号是否合法”这类判断变成可观测、可告警的指标。

8.2 让下游消费“清洗后版本”而不是各自清洗

数据团队经常遇到的问题是:报表团队清洗一次,算法团队又清洗一次,两边口径不一致。更合理的方式是建立统一的数据清洗层,把清洗后的结果作为公共数据提供给下游。

在数据量不大时,pandas 脚本加归档表就够用。数据量大时,可以切换到 Spark 做批量清洗;实时场景下,可以用 Flink 在流上完成标准化和校验。架构可以变,但规则表、清洗报告、回滚机制这些思想不会变。

数据清洗的终点不是“数据完全正确”,而是“数据质量可以被讨论”。当业务、开发、测试都能指着同一份清洗报告说清楚哪里有问题、规则是什么、为什么这样处理时,这份数据才真正做到了清者自清万人识。

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

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

立即咨询