跳到主要内容

缺失值

余额缺失,可能是没有记录,不能据此判断账户里没有钱。是否填充,要看数据为什么缺失。Pandas 为 pd.NANaNNaT 等缺失表示提供统一的处理 API。

检测

import pandas as pd

df = pd.DataFrame({
"required_key": [1, 2, 3], "country": ["CA", None, "FR"],
"account_id": ["A", "A", "B"], "time": [1, 2, 1],
"balance": pd.Series([10, pd.NA, pd.NA], dtype="Int64"),
}).sort_values(["account_id", "time"])
missing_by_column = df.isna().sum()
complete_rows = df.notna().all(axis="columns")

填充前,missing_by_column 显示两个余额缺失、一个国家缺失,其余列没有缺失;complete_rows 只有第一行为真。

始终使用 isnanotna 进行判断,直接比较是否等于某个缺失哨兵值(sentinel)是不可靠的。 除了查看缺失总数,还要关注缺失比例。如果整体缺失率掩盖了局部规律,需按业务维度(如用户群、时间段)拆分检查,以发现系统性模式。

选择处理策略

操作适用场景
保留缺失本身具有业务含义,或后续逻辑已处理该情况
删除行/列无法使用,且删除带来的偏差在可接受范围内
填充常量存在具有明确业务含义的领域专用哨兵值
前向/后向填充数据有序,且数值在相邻观测间具有持续性
插值存在合理的模型连接相邻数据点
建模/插补能显式控制不确定性及数据泄漏风险
df = df.dropna(subset=["required_key"])
df["country"] = df["country"].fillna("unknown")
df["balance"] = df.groupby("account_id")["balance"].ffill()

处理后的余额如下:

账户时间填充前ffill
A11010
A2<NA>10
B1<NA><NA>

A 的第二行沿用本账户上一条观测的余额 10。B 在自己的分组里没有更早的余额,所以仍然缺失;A 的 10 不能跨账户传给 B。所有必填键都有值,因此 dropna 没有删行;缺失的国家被填成 "unknown"dropnafillna 默认返回结果,上面的赋值才会更新 df

执行 ffill 等方向性填充前,先按实体键和时间键排序,再按实体分组,避免把其他账户的值填进来。

数据类型与聚合

Int64booleanstring 等可空扩展类型(Nullable Extension Dtypes)能在表示缺失的同时保留语义类型。在关键数据边界处应显式指定这些类型,而非依赖 Pandas 的自动推断。

大多数聚合操作(reductions)默认跳过缺失值。当默认行为会改变结果语义时,需显式设置 skipnamin_count(最少有效值数量)。

未知不等于零或假

缺失数据规则也影响逻辑判断:pd.NA == pd.NA 的结果仍未知,bool(pd.NA) 会抛出 TypeError。空字符串和无穷大默认不算缺失。全缺失序列求和默认得到零,除非要求最少有效值数量。可空比较后调用 .all() 可能跳过未知项,因此必填列还需单独检查是否缺失。

empty = pd.Series([pd.NA, pd.NA], dtype="Int64")
assert empty.sum() == 0
assert pd.isna(empty.sum(min_count=1))
assert empty.ge(0).all()
assert not empty.notna().all()

记录决策

数据清洗流程应明确记录:哪些列允许缺失、缺失原因,以及各处理策略对下游分析的影响。 如果插补行为本身可能携带预测或诊断信息,建议保留一个指示列(indicator column)标记原始数据是否缺失。

参考资料

探索关联打开关联网络