前言
真实数据几乎不可能干干净净,缺失值(missing value)是分析前必须处理的一步。 但"缺失"在 pandas 里并不是一个单一的东西,它至少有四种面孔:
NaN(Not a Number):浮点意义上的"非数",是float;None:Python 里的空对象;NaT(Not a Time):时间类型里的缺失;pd.NA:pandas 为可空类型引入的缺失标记。
很多人以为它们可以随便互换,结果在判等、聚合和类型转换上踩坑。 本文先讲清这四者的区别与判定方法,再讲删除与填充的正确姿势。
需要说明:pandas 是第三方库,本机没有 Python 解释器也没有 pandas, 示例无法运行验证,只能逐行人工推演。参数细节以 Pandas 官方文档为准。
一、NaN 与 None 的区别
这是最核心、也最容易错的一点。它们判等行为完全不同:
| 表达式 | 结果 | 说明 |
|---|
None == None | True | 普通对象比较 |
float("nan") == float("nan") | False | NaN 不等于自身 |
numpy.nan == numpy.nan | False | 同上 |
pandas.NaT == pandas.NaT | False | 同 NaN 语义 |
pandas.isna(x) | 是否为缺失 | 统一判定入口 |
正因为NaN != NaN,不能用==或!=来检测缺失, 必须用isna()/notna()(isnull是isna的别名,notnull是notna的别名)。
另一个关键点是类型:NaN是浮点数,所以当整数列出现缺失时, pandas 会把整列提升为float64,3就显示成3.0。 而None放进object列时会原样保留为None;放进浮点列时会被转成NaN。
# pandas 1.0+
import numpy as np
import pandas as pd
s = pd.Series([1, None, 3], dtype="float64")
print(s) # 1.0 NaN 3.0
print(s.isna().tolist()) # [False, True, False]
obj = pd.Series([1, None, 3], dtype=object)
print(obj.tolist()) # [1, None, 3],这里 None 被保留关键结论:isna()把None、NaN、NaT、pd.NA都视为缺失, 所以检测一律走isna(),不要手写比较。
二、检测:先看清缺失的分布
# pandas 1.0+
df = pd.DataFrame({"a": [1, None, 3], "b": [None, None, 6]})
print(df.isna()) # 每个格子 True/False
print(df.isna().sum()) # 每列缺失个数
print(df.isna().mean()) # 每列缺失比例df.isna().sum()给出每列的缺失数,df.isna().sum(axis=1)给出每行的缺失数,df.isna().mean()则把缺失比例直接算出来,判断"这列还能不能用"很直观。isna()返回的全是布尔值,所以它可以直接和布尔索引配合:df[df["a"].isna()]就是"把 a 列缺失的那些行挑出来"。
三、删除:dropna
# pandas 1.0+
df.dropna() # 任一行只要有一个缺失就删
df.dropna(axis="columns") # 一列只要有一个缺失就删该列
df.dropna(how="all") # 只有全为缺失才删
df.dropna(subset=["a"]) # 只看 a 列有没有缺失
df.dropna(thresh=2) # 至少保留有 2 个非缺失的行签名是dropna(*, axis=0, how=<no_default>, thresh=<no_default>, subset=None, inplace=False, ignore_index=False)。 要点:
axis=0删行、axis="columns"(即 1)删列;how="any"(默认)任一是缺失即删,how="all"全部缺失才删;subset限定"只看哪几列";thresh=n表示"非缺失值至少要有 n 个才保留"。
四、填充:fillna
# pandas 3.0 的签名:fillna(value, *, axis=None, inplace=False, limit=None)
df.fillna(0) # 常数填充
df.fillna({"a": 0, "b": "未知"}) # 按列填不同值
df.fillna(method="ffill") # 旧写法,见下方说明版本差异要特别注意:在 pandas 3.0 里,fillna的method关键字 已经被移除,官方推荐改用专门的方法ffill()(前向填充)和bfill()(后向填充):
# pandas 3.0 起推荐
df.ffill() # 用前一个非缺失值填充
df.bfill() # 用后一个非缺失值填充数值列的平滑填充还可以用interpolate()(线性插值等), 它适合有序列特征(如时间序列、有序指标)的数据:
# pandas 1.0+
ser = pd.Series([1.0, float("nan"), 3.0])
print(ser.interpolate()) # 中间缺失被插值成 2.0五、可空类型:Int64/boolean/string
如果既想保留整数外观、又想容纳缺失,用首字母大写的可空类型:
# pandas 1.0+ 可用
s = pd.Series([1, None, 3], dtype="Int64")
print(s) # 1, <NA>, 3;缺失是 pd.NA,不是 NaN注意Int64(可空)与int64(NumPy 的,不能有缺失)是两个不同类型。 在聚合时,pd.NA的传播规则与传统NaN略有差别,处理逻辑要区分对待, 这一点以官方文档的"nullable integer"章节为准。
六、一个完整流程
# pandas 1.0+;fillna 的用法按你实际安装的版本调整
import pandas as pd
def clean(df: pd.DataFrame) -> pd.DataFrame:
# 1) 看缺失比例
ratio = df.isna().mean()
# 2) 缺失超过一半的列直接丢掉
keep = ratio[ratio <= 0.5].index
df = df[keep].copy()
# 3) 数值列用 0 填,类别列用 "未知" 填
num_cols = df.select_dtypes("number").columns
df[num_cols] = df[num_cols].fillna(0)
return df
if __name__ == "__main__":
print(clean(pd.DataFrame({"a": [1.0, None, 3.0]})))常见坑点
- 用
==检测缺失
❌df[df["a"] == np.nan],因为NaN != NaN,结果永远是空 ✅df[df["a"].isna()]
- 以为
None和NaN完全等价
❌ 在object列里写s.fillna("x")前先s == None判断,结果漏掉真正的NaN✅ 统一用s.isna()判定,它同时覆盖两者
- 整数列出现缺失后类型被抬升
❌ 用1, None, 3建列,打印成1.0 NaN 3.0觉得奇怪 ✅ 这是int64容不下NaN的必然结果;要保留整数外观用dtype="Int64"
- 在 pandas 3.0 里继续用
method="ffill"
❌df.fillna(method="ffill"),新版本已移除该关键字,直接报错 ✅ 用df.ffill()/df.bfill()
dropna()一刀切删掉太多
❌ 直接df.dropna(),某列本来就大面积缺失,结果把绝大部分行也删了 ✅ 先用df.isna().sum()看分布,再决定subset、thresh或改填充
- 填充时把类别列也填成
0
❌df.fillna(0)无差别填充,本该是"未知"的文本列里混进数字0✅ 按列指定df.fillna({"num": 0, "cat": "未知"})
dropna/fillna忘了返回新对象
❌df.dropna()之后照旧用df,发现没生效 ✅ 这些方法默认返回新对象,要接住返回值,或显式inplace=True(并理解它在新版语义)
inplace与链式调用的配合
❌df.dropna(inplace=True).reset_index(),inplace=True返回None,链式直接崩 ✅ 二者选一:要么接返回值,要么inplace=True后单独一行
总结
| 主题 | 关键点 |
|---|
| 缺失的四种面孔 | NaN(float)、None、NaT、pd.NA |
| 判定 | 一律isna()/notna(),绝不用== |
| 检测 | isna().sum()数个数,isna().mean()算比例 |
| 删除 | dropna(how=/thresh=/subset=) |
| 填充 | fillna(value=...);3.0 用ffill()/bfill()取代method |
| 类型 | 整数列有缺失会被抬成float;要保整数用Int64 |
缺失值处理没有万能公式:先看清(检测分布),再决定(删还是填), 最后验证(处理后isna().sum()是否归零)。把NaN != NaN这条记住, 很多"查不出缺失"的怪问题都会迎刃而解。