跳到主要内容

Pandas 数据清洗实战

数据清洗的本质,是将“实际观测到的数据结构”转换为“业务期望的目标结构”。核心原则只有两条:转换过程必须可复现,被剔除或修正的数据必须可见(可追溯)。

标准流程

  1. 保留原始数据:保留原始输入文件或其不可变存储位置及来源元数据(Provenance)。
  2. 基础规范化:统一列名,清理文本中多余的空白字符。
  3. 类型解析:使用显式格式解析数据类型,并明确定义解析失败时的行为(如转为 NaN 或报错)。
  4. 处理缺失与重复:依据业务规则处理缺失值和重复记录。
  5. 数据验证:检查主键、数值范围、枚举类别、单位一致性以及跨列逻辑约束。
  6. 输出结果:同时产出清洗后的干净数据和质量报告(Quality Report)。
import pandas as pd

raw = pd.DataFrame({
"entity_id": [1, 1, 2],
"email": [" ADA@example.org ", "ada@example.org", "lin@example.org"],
"amount": ["10", "12", "bad"],
"occurred_at": ["2026-01-01", "2026-01-02", "invalid"],
"updated_at": pd.to_datetime(["2026-01-01", "2026-01-02", "2026-01-03"]),
"code": ["AB-01", "AB-02", "CD-03"],
})
clean = (
raw.rename(columns=str.strip)
.assign(
email=lambda x: x["email"].str.strip().str.lower(),
amount=lambda x: pd.to_numeric(x["amount"], errors="coerce"),
occurred_at=lambda x: pd.to_datetime(
x["occurred_at"], format="%Y-%m-%d", errors="coerce", utc=True
),
)
)

注意 errors="coerce" 的作用:它将解析失败的项转换为缺失值(NaN),但这不代表这些数据是合法的。在继续后续步骤前,务必统计并检查这些失败案例。

下面先分离不合格记录,再去重和校验。本例剔除第 2 行;实体 1 保留较新的记录,金额为 12。

invalid = clean["amount"].isna() | clean["occurred_at"].isna()
rejected = raw.loc[invalid].copy()
clean = clean.loc[~invalid].copy()

文本提取

优先使用 Pandas 的向量化字符串方法(.str 访问器),避免使用 Python 原生回调(如 apply),以提升性能。

parts = clean["code"].str.extract(r"^(?P<prefix>[A-Z]+)-(?P<number>[0-9]+)\Z")
clean = clean.join(parts)

正则表达式应当严格描述“被接受的完整格式”,而不仅仅是匹配看起来像目标内容的子串。在验证通过之前,务必保留原始列,以便回溯。

处理重复项

在调用 drop_duplicates 之前,必须先明确“实体键”是什么,以及当存在重复时,保留哪一条记录(排序规则)。

clean = (
clean.sort_values("updated_at", kind="stable")
.drop_duplicates(subset=["entity_id"], keep="last")
)

这不仅是技术操作,更是一条业务规则。你需要记录为什么保留的那一行是“权威”的(例如:以最新更新时间为准)。

显式验证

使用断言(Assertions)或验证库来强制检查数据约束:

assert clean["entity_id"].notna().all()
assert clean["entity_id"].is_unique
assert clean["amount"].notna().all()
assert clean["amount"].ge(0).all()

在生产环境中,建议构建可复用的 Schema 定义或独立的验证层,而不是在代码中散落一堆临时的 assert 语句。

让校验真正有效

要区分解析失败与原本缺失,应在筛选前、索引一致时,将原列的 notna() 与解析后列的 isna() 对照。文本提取返回两列字符串,不会自动把 number 变成整数;需要计算时再显式转换。[0-9] 只接受 ASCII 数字,\Z 要求真正到达字符串末尾。

整个邮箱地址转小写,假设应用不区分地址大小写,这不是普遍适用的邮箱身份规则。这里的 utc=True 把仅含日期的输入解释为 UTC 零点,而非本地零点。去重假设更新时间已解析且不缺失;稳定排序会在时间相同时保留输入中最后一行,只有业务认可这一裁决方式时才适用。Python 的 -O 会禁用断言,必须执行的检查应显式抛出异常。

参考资料

探索关联打开关联网络