5.1 理解并处理缺失数据
第 4 章把解析和模式验证视为边界契约。这份契约产生的结果之一就是真实呈现缺失:原本不存在、无效或不适用的值保持可见,而不是伪装成零或空文本。清洗的第一步是追问信息为何缺失,而不是立刻把所有单元格填满。
缺失是一种状态,不是用来比较的数值
由于列可以采用不同 dtype,pandas 中会遇到多种缺失标记:
pd.NA出现在Int64、boolean与可空string等 pandas 可空 dtype 中。
NaN是传统的浮点“非数值”标记。
NaT表示缺失日期时间或时间差。
- Python
None可能出现在对象型输入中,通常也会被识别为缺失。
可以用 isna 和 notna 统一检测这些表示:
missing = deliveries.isna()
known_minutes = deliveries["minutes"].notna()不要使用 value == pd.NA。对于可空 dtype,与未知值比较的结果通常仍然未知:
pd.NA == pd.NA # pd.NA
pd.NA > 40 # pd.NApd.NA 遵循三值逻辑(three-valued logic):真、假和未知。例如,True | pd.NA 为真,因为未知无论是真还是假,结果都为真;False | pd.NA 则仍然未知。这样,不可用事实就不会被悄悄当成假。
mean 与 sum 等归约默认通常跳过缺失值:
mean_minutes = deliveries["minutes"].mean()
strict_mean = deliveries["minutes"].mean(skipna=False)第一个均值可能只使用了表格中的部分行。应始终报告或检查有效分母:
valid_n = deliveries["minutes"].count()
total_n = len(deliveries)同时分析列、行、群体与缺失模式
列缺失表是一个合适起点:
mask = deliveries.isna()
column_profile = pd.DataFrame({
"missing_count": mask.sum(),
"missing_rate": mask.mean(),
}).sort_values("missing_rate", ascending=False)但一个百分比会隐藏结构。还要计算每行缺失负担:
row_missing = mask.sum(axis=1)
review = deliveries.loc[row_missing >= 2]然后检查哪些字段经常一起消失:
patterns = (
mask.astype("int8")
.value_counts(dropna=False)
.rename("rows")
.reset_index()
)如果某天下午 minutes 与 address 同时消失,共同采集故障就比两个独立随机事件更合理。还应按有意义的群体和时间比较缺失比例:
by_zone = deliveries.groupby("zone")["minutes"].apply(
lambda values: values.isna().mean()
)不要把结构性不适用(structurally not applicable)与未知混在一起。自取订单本来就可以没有配送地址。应保存原因码或流程类型,让分析者区分“无需填写”与“本应存在但未采集”。
缺失机制是对数据过程的假设
统计讨论常区分三种机制:
- 完全随机缺失(Missing Completely At Random, MCAR):缺失与分析相关的已观测值和未观测值都无关。
- 随机缺失(Missing At Random, MAR):在给定已观测信息后,缺失不再依赖缺失值本身。
- 非随机缺失(Missing Not At Random, MNAR):缺失仍依赖未观测值,或另一个未观测原因。
这些名称不表示 MAR 就“足够随机”,也不表示一张模式图可以证明机制。它们是关于数据生成过程的假设。例如,耗时很长的配送可能更不容易收到完成扫描。如果即使利用了所有已记录变量,缺失概率仍依赖未观测的配送用时,这个过程就是 MNAR。
应结合系统知识、采集日志、数据源负责人和敏感性分析。缺失检验或仪表板可以挑战某项假设,却很少能单独证明它。
让处理方式匹配分析问题
当方法可以处理缺失,或填补会制造无依据事实时,保持缺失是一项有效选择。其他常见处理包括删除与填补。
完整案例分析(complete-case analysis)只删除当前计算所需字段缺失的行:
complete = deliveries.dropna(subset=["minutes", "zone"])应使用 subset,不要因为任意字段缺失就删除整行。缺少可选备注不应移除一笔原本有效的配送。还要记录删除了多少行、来自哪些群体,因为完整案例可能代表另一个总体。
只有常量具有明确含义时,常量填补(constant filling)才合适:
notes = deliveries["driver_note"].fillna("not_recorded")把缺失分钟数填成零通常是错误的,因为零分钟是真实用时。它会改变均值、阈值分类、相关关系和后续模型。
基于统计量的填补(statistic-based imputation)可以保留行数:
was_missing = deliveries["minutes"].isna()
minutes_numeric = deliveries["minutes"].astype("Float64")
median_minutes = minutes_numeric.median()
analysis = deliveries.assign(
minutes_was_missing=was_missing,
minutes_clean=minutes_numeric.fillna(median_minutes),
)指示列保留了“此值曾经缺失”的证据。中位数填补会压缩变异,也可能扭曲变量关系,因此应与未填补或完整案例方案比较。
分组填补可以利用相关的已观测信息:
zone_median = minutes_numeric.groupby(deliveries["zone"]).transform("median")
analysis["minutes_zone_fill"] = minutes_numeric.fillna(zone_median)小组的中位数可能不稳定,甚至全部缺失。应定义回退层级和最小组规模,而不是意外留下未填补值。
前向填充和插值依赖顺序假设。它们可能适合定期采样的传感器测量,却通常不适合彼此无关的客户行:
sensor = sensor.sort_index()
sensor["temperature_linear"] = sensor["temperature"].interpolate(
method="time",
limit=2,
)limit 防止长时间中断被完全涂抹。不要对标识符或无序类别进行插值。
防止泄漏并保存数据血缘
在预测工作流中,只能使用训练数据估计中位数、类别频率或模型填补规则,再把学到的规则应用到验证集与测试集。如果用完整数据计算填充值,信息就会跨越评估边界泄漏。第 8 章会把这条规则装入机器学习流水线。
每次修复都应保留:
- 原始字段或不可变原始数据集。
- 布尔修复指示列,以及必要时的原因码。
- 方法、参数、分组字段与软件版本。
- 操作前后的数量。
- 在其他合理策略下的敏感性结果。
下一节会把相同的“证据优先”方法应用到重复记录。精确重复可以机械检测,但决定哪条记录代表同一个实体,需要明确身份规则。