3.2 数据清洗与转换


3.2 数据清洗与转换

本节摘要:SOURCE 3.2 详述 dropna/fillna(均值/中位数/ffill/bfill)、to_numeric/to_datetime/astype(category),以及 map/replace 与简单衍生列。本节按「缺失 → 类型 → 映射 → 衍生」顺序组织,并提醒 ffill 在时间序列上的因果方向。

读前必看

阅读完本节,你应当能够:

  1. 选择删除、均值/中位数/常数填充缺失的策略
  2. 用 to_numeric(..., errors="coerce") 安全转数值
  3. 用 astype category 压缩字符串列内存
  4. 用 map/replace 统一类别编码

一、缺失值处理

SOURCE 代码族:

df_dropna = df.dropna() df_fillna_mean = df.fillna(df.mean(numeric_only=True)) df_fillna_median = df.fillna(df.median(numeric_only=True)) df_fillna_value = df.fillna(0) df_fillna_ffill = df.ffill() # 等价 fillna(method="ffill") df_fillna_bfill = df.bfill()
策略 适用 风险
dropna 缺失极少 丢样本偏差
mean/median 数值列 扭曲分布
0 / 常数 业务有「未知=0」语义 误导模型
ffill/bfill 时序 未来信息泄漏(bfill 慎用)

Notebook 里 df.isnull().sum() 再决定策略,把选择理由写进 Markdown 格。

二、类型转换

SOURCE:

df["col"] = pd.to_numeric(df["col"], errors="coerce") df["col"] = pd.to_datetime(df["col"], errors="coerce") df["col"] = df["col"].astype(str) df["col"] = df["col"].astype("category")

errors="coerce" 把非法值变 NaN/NaT,便于后续统一 fillna。category 对低基数字符串列可显著降内存(衔接第 5 章 downcast)。

三、映射与替换

SOURCE 3.2 后续常配合 map/replace 统一标签:

df["grade"] = df["grade"].replace({"A+": "A", "A-": "A"}) df["city_code"] = df["city"].map({"北京": 1, "上海": 2})

map 未命中得 NaN——先 df["city"].unique() 检查全集。

四、衍生列与向量化

清洗后常用向量化衍生,避免 Python 循环:

df["total"] = df["price"] * df["qty"] df["log_price"] = np.log1p(df["price"])

用 2.2 节 %timeit 对比 loop append vs 列运算——Notebook 里量化「为什么向量化」比背口号有效。

⚠️ 常见坑:对整表 fillna(0) 把类别列也填成 0——应对列子集操作 df[["age","income"]].fillna(...)

💡 关键直觉:清洗决策要可审计——每步保留 before/after 行数或 describe 快照。

核心回顾

  • dropna / fillna:删 vs 填,时序注意 ffill/bfill
  • to_numeric / to_datetime + coerce:安全转换
  • category:省内存
  • replace / map:统一类别
  • 向量化衍生 + %timeit:快且可证明

下一节 3.3 高效数据可视化 把清洗结果画出来验证假设。

清洗流水线模板

下面模板适合"原始数据 → 可分析 DataFrame"的常见转换:

import pandas as pd def clean_sales(df: pd.DataFrame) -> pd.DataFrame: # 1. 统一列名:去空格、转小写 df.columns = df.columns.str.strip().str.lower() # 2. 类型:数值列安全转换,非法值记 NaN df["price"] = pd.to_numeric(df["price"], errors="coerce") df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") # 3. 缺失:先看比例再决定 missing = df.isnull().sum() print("缺失比例:", missing / len(df)) # 4. 标签统一 df["region"] = df["region"].replace({"beijing": "北京", "BJ": "北京"}) return df

缺失值决策的先后顺序

  1. 先量化df.isnull().mean() 看每列缺失比例。
  2. 判断机制:随机缺失可以删行;系统性缺失(如某天整段没数据)要保留列并标注。
  3. 选策略:数值列用中位数比均值抗离群;时序列用 ffill;业务上有"未知"语义的用常数。
  4. 留痕:把每个选择的理由写进 Markdown 格,交付时可直接引用。

类型转换的三个高频坑

  • errors="coerce" 一定要加:不加会在遇到第一个脏值时报错中断。
  • 时间列先 to_datetime 再分组:字符串分组按字典序,日期按时间序。
  • 转换后立刻 df.dtypes 复查:转换失败时列仍是 object,不会报错但结果错。

清洗是否可逆

尽量在原始 DataFrame 之外创建清洗副本,保留 raw 一份。用 df_clean = df.copy() 开头,后面所有转换作用在副本上。这样探索后期想回头换一种缺失处理策略,不用重新读文件。

特征工程前的数据校验

清洗完成后、建模之前,做一轮轻量校验:列数是否和 schema 一致,行数是否和预期一致,关键列是否还有 NaN。这轮校验写在函数里,每次清洗后重跑一遍,比人工检查可靠。

def validate(df, expect_rows, expect_cols): assert df.shape[0] == expect_rows, f"行数 {df.shape[0]} != {expect_rows}" assert set(df.columns) == expect_cols, "列名不一致" assert df.isnull().sum().sum() == 0, "仍有缺失" print("校验通过")

常见转换的先后顺序

类型转换要早于缺失填充:先 to_numeric 把脏值转 NaN,再统一 fillna,顺序反了会把字符串当成数值处理。映射替换要在类型确定之后做,避免把数值误映射成字符串。整体顺序是"类型 → 缺失 → 映射 → 衍生"。

清洗效果的量化对比

每次清洗前后记录行数、缺失数、唯一值数三个指标,形成一张对比表:删除策略行数会变少、填充策略缺失会归零、映射策略唯一值会收敛。指标说话,比"我大概清洗完了"有说服力,也方便回溯哪一步改变了什么。

清洗代码的归宿

探索阶段的清洗代码可以直接写在 Notebook 里,但一旦发现同样的清洗逻辑要在第二份数据上复用,就该抽进 src/ 下的 .py 函数。抽离的时机以"第二次遇到相同需求"为准,不必过早设计接口。

转换函数要写成可复用的

清洗函数越通用越值得沉淀。例如把"统一列名"和"类型安全转换"写进一个函数,参数是 DataFrame,返回清洗后的副本,不依赖任何具体列名。这样的函数可以在多份数据间复用,也方便单元测试。

清洗与探索的迭代节奏

清洗不是一步到位的。先做最小清洗让数据能分析,画出图、看出问题,再回头补清洗。常见节奏是"读入 → 摸底 → 最小清洗 → 画图 → 发现问题 → 补清洗 → 再画图"。保留每次清洗快照(行数、列数、缺失数),迭代时能清楚知道每一轮改了什么。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U