Pandas缺失值处理全攻略:从定位到填充的完整数据清洗流程
2026/9/18 7:30:32 网站建设 项目流程

简介:这是一份面向Python数据分析初学者的Pandas数据预处理学习笔记PDF,聚焦4.11节缺失值处理。内容从缺失值的产生讲起,先说明Pandas用NaN表示缺失值,再系统讲解dropna、fillna、isnull、notnull四个核心函数的用法与参数细节,包括axis控制删除行或列、how选择删除全空或含空行、thresh设置非空数量阈值、subset指定检查列范围。随后通过具体DataFrame实例演示如何删除缺失行/列、用固定值0或前向/后向填充、使用interpolate线性插值,并配有运行结果,方便对照练习;笔记还补充了创建缺失值数据、制造异常数据并填充、模拟电商商品销售数据集进行综合练习等实操环节,能帮助读者快速上手数据清洗流程。整个文件为单个PDF文档,大小约375KB,内容紧凑,适合已经掌握Pandas基础、想强化数据清洗技能的读者随时翻阅,目前已有1348人浏览学习。

1. 数据清洗第一步:为什么说缺失值比离群值更容易毁掉模型

拿到一份四个仓库的库存表,同一件商品在一个仓库有记录,在另一个仓库完全没有,先用groupby一聚合,结果里全是空值,平均库存直接被算成 0。这是数据预处理里最典型的场景:数据里的问题往往不是某个离群的 9999,而是一个个悄悄出现的NaN。Pandas 用NaN标记缺失值,并围绕它提供isnulldropnafillnainterpolate四类接口,搭配好就是一套完整的数据清洗流程。下面把这条流程拆开讲,从缺失值定位开始,到按参数删除、按策略填充,最后落到“把异常值转成 NaN 再统一处理”这个能直接复用的技巧。适合刚入门数据分析、遇到报错就dropna()的新手,也适合想搞懂threshsubset这些冷门参数到底怎么用的从业者。

2. 缺失值体检:isnull、notnull 与 info() 组合定位 NaN

2.1 NaN 的来历与类型转换

开始之前先构造一份带缺失值的 DataFrame,作为全文的演示数据。这里用典型的员工信息表,包含姓名、年龄、工资、性别四列。

import numpy as np import pandas as pd print(pd.__version__) # 先看版本,不同版本 API 行为有差异 df = pd.DataFrame( [ ["张三", np.nan, 3000, "M"], ["李四", 28, np.nan, np.nan], ["王五", 25, np.nan, "W"], ["赵六", 20, 4000, "M"], [np.nan, np.nan, np.nan, np.nan], ], columns=["姓名", "年龄", "工资", "性别"], )

np.nan是浮点标准里的 Not a Number,它的一个特性是np.nan == np.nan返回False,所以千万不要用df[df["年龄"] == np.nan]去筛缺失值,写了也拿不到结果。所有判断要交给isnull()notnull()

Pandas 在读 CSV、Excel 时,会把空白单元格自动解析成NaN,同时把整列推断为float64。这造成一个隐蔽现象:明明源文件里是整数年龄,导入后 dtype 却变成 float。如果发现原本应该是 int 的列变成 float,大概率就是列里混入了 NaN,这是数据预处理中最常见的类型陷阱。

2.2 用 info() 快速掌握数据规模

df.info()不是专门的缺失值函数,但对清洗流程第一步来说性价比最高,因为它直接输出每列的非空值数量:

df.info()

运行结果会显示每列的 Non-Null Count:姓名4 个非空,年龄3 个,工资3 个,性别3 个,最后一行还有索引范围和内存占用。这意味着四列各缺失一处,第五行是全空行。相比打印整个 DataFrame 用肉眼找洞,info()更适合刚接手一份陌生数据时做快速体检,它帮你确认:哪些列还能救,哪些列已经漏成筛子。

2.3 isnull() 与 sum() 计算缺失分布

df.isnull()返回一个形状和原表完全一致的布尔 DataFrame,每个元素用True标记缺失位置。只看布尔表还不够直观,叠加sum()才能得到统计口径:

df.isnull() # 布尔矩阵,True 代表缺失 df.isnull().sum() # 每列缺失个数 df.isnull().sum().sum() # 全表缺失总数

三个调用逐层递进。df.isnull()定位具体格子,比如第一行第二列是True,表示张三的年龄缺失。sum()对列方向求和,布尔值True当作 1 计算,得到每列缺失数量。第二个sum()再对 Series 求和,就是整张表的 NaN 总数。真实项目里,我很少逐行看布尔矩阵,而是直接看df.isnull().sum(),配合df.isnull().mean()得到缺失率,超过 70% 的列基本就考虑放弃或重构了。

2.4 notnull() 与布尔索引筛选健康样本

df.notnull() df[df["年龄"].notnull()]

notnull()isnull()的严格互补,True表示非缺失。第二行的写法是数据清洗里常用的布尔索引:df[条件]中的条件是一串布尔值,Pandas 保留条件为True的行。这里只以“年龄”为过滤标准,年龄非空的行全部保留,哪怕工资或性别仍然是 NaN。

这个行为比dropna()更可控:它按字段逐个过滤,而不是一竿子打死一整行。例如分析“有年龄的用户中谁缺工资”时,先用df["年龄"].notnull()圈定人群,再对结果做二次缺失检查,比直接删行更符合业务逻辑。

四种检测方法的适用场景对比如下:

方法返回内容典型用途
df.info()文本摘要快速查看每列非空数量与类型
df.isnull()布尔 DataFrame精确到元素级别的缺失定位
df.isnull().sum()Series按列统计缺失数量
df.notnull()布尔 DataFrame配合布尔索引过滤非缺失样本

百万行级数据里,isnull()会生成等大的布尔矩阵,内存压力是原表的好几倍。遇到超大文件时建议先抽样或分块读取,用df.sample(100000)统计缺失分布,再决定整表处理策略,避免一次性把全量布尔结果展开。

3. dropna 删除策略:axis、how、thresh、subset 的取舍逻辑

3.1 删除行还是删除列:先说 axis

dropna的完整函数签名是DataFrame.dropna(axis=0, how='any', thresh=None, subset=None, inplace=False)。第一个要决策的就是axis:删行还是删列。axis=0axis='index'删除含有缺失值的行,axis=1axis='columns'删除含有缺失值的列。

df.dropna() # 默认 axis=0,删除任何含 NaN 的行 df.dropna(axis=1) # 删除任何含 NaN 的列

默认调用df.dropna()后,只剩“赵六”这一行,因为只有他全字段完整。df.dropna(axis=1)则更极端:四列里只有姓名没有任何 NaN,其余三列全被删光,表就剩一列了。

这个示范说明一个边界:列不多、但每列都有少量缺失时,axis=1会造成灾难性的信息损失。删列的真正适用场景是“某一列缺失率过高,比如 80% 以上的字段值是 NaN,该列已经失去分析价值”,此时删列是止损而非浪费。

3.2 how='any' 与 how='all':缺失容忍度

how参数决定删除的触发条件。how='any'是默认值,只要行或列里出现任意一个 NaN 就删除;how='all'则要求整行或整列全部为 NaN 才删除。

df.dropna(how='all') # 删除全为 NaN 的行 df.dropna(how='any') # 默认行为,有 NaN 就删

前面构造的数据里恰好有一行全 NaN,df.dropna(how='all')会精准删掉这一行,其他四行保留,因为它们至少有一个有效字段。实际业务中,how='all'最有用的场景是清理 CSV 文件末尾的空行或 Excel 中误插入的空行,既能清掉垃圾数据,又不会误伤正常记录。how='any'则适合字段都必填的严格表,比如订单表中订单号、金额、时间任何一项缺失,整条订单都不能参与统计。

3.3 thresh:不是缺失个数,是有效值个数

thresh是 dropna 参数里最容易被误读的一个。它的语义是“保留至少有 n 个非缺失值的行或列”。单位是非缺失值的数量,不是缺失值的数量。

df.dropna(thresh=3)

逐行数有效值:张三和非缺失值有 3 个(年龄缺失),李四只有 2 个(工资和性别都缺失),王五有 3 个,赵六 4 个,最后一行 0 个。thresh=3的结果是保留下张三、王五、赵六三行。

这个参数解决的问题是“我不在乎缺哪一个字段,但整条记录至少要有几个关键字段”。比如用户画像表有 10 个维度,业务要求每条样本至少覆盖 6 个维度才有建模价值,直接用thresh=6,一条参数搞定,不用写复杂的多列条件组合。

提示:如果需求是“最多容忍 2 个缺失值”,应该写成thresh=总列数-2,而不是thresh=2。把thresh理解为“及格线”而不是“扣分项”,能少踩一半坑。

3.4 subset:只在业务关键列里查缺失

subset接收一个列名列表,指定 dropna 时只在这些列内评估缺失条件。它解决的是“有些字段缺失无所谓,但核心字段不能缺”这类业务诉求。

df.dropna(subset=['工资', '性别'])

这个调用只检查工资性别两列,两列都非空的行才能保留。逐行看:李四性别缺失被删,王五工资缺失被删,全空行直接不满足条件,剩下的只有张三和赵六。注意它评估的是 subset 范围内的列,姓名即便缺失也不会触发删除。

用户注册表就是一个典型场景:user_idreg_time是必填字段,avatar_url是选填字段,那么dropna(subset=['user_id', 'reg_time'])就合理。如果对全表任意缺失都执行删除,注册数据会损失惨重。

3.5 删除后的索引断裂与重置

删除行之后,索引不会自动重排。如果删掉了中间几行,索引会变成 0、2、4 这样的跳跃状态。后续做groupbymerge时,断裂的索引容易引发“看似对齐、实则错位”的隐性 bug。

df_clean = df.dropna(subset=['工资']).reset_index(drop=True)

.reset_index()将索引恢复为从 0 开始的连续整数序列,drop=True表示丢弃原索引,而不是把它作为一列插进数据里。项目实践中,我习惯在所有dropna之后立即接一个reset_index(drop=True),减少后续代码中的不确定性。

3.6 dropna 参数速查表

参数取值含义
axis0/'index'1/'columns'按行还是按列删除
how'any''all'有 NaN 就删,还是全 NaN 才删
thresh整数 n行/列至少保留 n 个非缺失值
subset列名列表只在指定列内评估缺失
inplaceTrue/False是否原地修改原 DataFrame

inplace默认False,返回新对象,原表不动。很多教程推荐df.dropna(inplace=True),但项目里我建议统一写成df = df.dropna(...)。原因很简单:原地修改会打断链式操作,调试时想对比删除前后的差异也麻烦,而没有inplace的写法天然保留原数据的一个备份。

4. fillna 填充方案:常量填充、ffill / bfill 与 interpolate 线性插值

4.1 常数填充与按字段差异化填充

删除缺失值会损失样本,填充则能把样本保留下来。最直接的填充方式是常数填充:

df.fillna(0)

这个操作把整张表的 NaN 都替换成 0。视觉上干净了,但必须警惕一个坑:0 本身可能是一个合法的业务值。工资列为 0 和工资缺失是完全不同的概念,前者代表“没有工资”或“无偿劳动”,后者代表“没录进来”。如果是库存表,0 还意味着“缺货”,直接fillna(0)可能会让业务误判。

更精细的做法是按列填充,不同列用不同策略:

df['年龄'] = df['年龄'].fillna(df['年龄'].median())

先计算年龄列的中位数,再把它填进缺失位置。选择中位数而非均值,是因为中位数对离群值更稳健。年龄列如果混入一个 200 的异常值,均值会被拉高,中位数依然稳定。注意这里必须写成df['年龄'] = ...的形式,把填充结果重新赋回原列;只写df['年龄'].fillna(median)不会修改原数据。

4.2 ffill 与 bfill:顺序数据的继承式填充

基于顺序的填充方式是缺失值处理的另一条路线。前向填充用上一个有效值补缺口,后向填充用下一个有效值补缺口:

df.fillna(method='ffill') # 前向填充,method 写法在老版本中常见 df.fillna(method='bfill') # 后向填充

新版 pandas 中更推荐直接使用专用方法:

df.ffill() df.bfill()

ffill的执行逻辑是沿着axis=0,即从上到下逐行继承:李四缺失的工资被张三的 3000 填充,缺失的性别被上一行的M填充。bfill则相反,从下往上取下一个有效值。

但填充绝不等于“找到值就行”。ffillbfill都强依赖行顺序,什么时候适合用?时间序列。比如传感器每 5 秒上报一次温度,某次网络抖动断了一个点,用上一秒的温度近似是合理的工程假设。如果数据本身无序,比如多个仓库的库存表,用ffill就是把 A 仓库的库存继承给 B 仓库,没有任何业务依据。

顺便说一句,ffill有一个边界限制:第一行之前的空缺没有值可继承,最后一行的空缺也无法被bfill处理。实际项目里可以组合使用,但组合后那些“推测值”和“真值”在后续分析里最好能被区分出来。

4.3 interpolate 线性插值的计算逻辑

interpolate(method='linear')是比 ffill 更平滑的填充方式。它不直接继承某个值,而是在前后两个有效值之间按线性关系推算中间值。

df.interpolate(method='linear')

工资列为例,第一行是 3000,第四行是 4000,第二、三行的工资缺失就会按照索引位置的线性比例折算,分别得到约 3333 和 3666。相比ffill直接把两行都填成 3000,插值保留了数据的趋势变化。

interpolate适合数值型连续字段,比如房价走势、气温变化、销量序列。但它有两个前提:数据本身有顺序逻辑,且相邻值之间存在近似的线性关系。在类别型字段上它无能为力,性别列缺失时会原样保留,因为字符串无法做数值插值。

4.4 填充方式选择对照

把三种填充方法在同一份数据上跑一遍:

print(df.fillna(0)) print(df.ffill()) print(df.bfill()) print(df.interpolate(method='linear'))

四次输出结果各不相同,而且从代码层面看都“没错”。关键不是哪个函数更高级,而是填充结果是否贴近业务事实。数据分析中,缺失值处理没有标准答案,只有相对更合理的假设。

填充方式填充来源数据前提主要风险
fillna(0)外部常量缺失有明确的业务含义0 被当作有效值参与统计
fillna(median)列统计量数值型,分布偏态降低方差,弱化波动信息
ffill()/bfill()相邻行数据严格有序顺序乱则结果无意义
interpolate()前后值拟合连续数值,线性变化非线性场景失真

5. 异常值转 NaN 再统一处理:一个能直接抄进项目的填充技巧

5.1 用布尔掩码把异常值变成 NaN

缺失值处理和异常值处理经常是同一件事。一个常见做法是:先把不合理的取值转成 NaN,让异常值“归入”缺失体系,再统一走前面的删除或填充流程。

df1 = pd.DataFrame(np.random.randint(0, 10, (4, 5))) df1[df1 < 5] = np.nan # 把小于 5 的取值视为异常,转为缺失 print(df1) df1.fillna(-1, inplace=True) print(df1)

核心是df1[df1 < 5] = np.nan这一行。df1 < 5生成布尔掩码,Pandas 把掩码为True的格子全部赋值为NaN,向量化执行,不需要写循环。之后fillna(-1)把所有异常位替换成-1,这里的-1不是正常数值,而是“这里曾出现过异常”的标记。用不可能出现的值保留异常痕迹,比直接删掉更有审计价值。

5.2 仓库库存表的完整预处理流程

用配套素材里的库存数据走一遍完整流程。原始数据是一个字典列表,四个仓库的商品种类不完全一致,缺失值表示该仓库没有这种商品的库存记录。

items2 = [ {'内存条': 33, '移动硬盘': 35, '鼠标': 15, '主板': 8, '键盘': 45}, {'内存条': 10, '键盘': 50, 'U盘': 15, '鼠标': 5, '主板': 17, '显示器': 5}, {'内存条': 24, '键盘': 14, 'U盘': 18, '鼠标': 19, '主板': 12, '移动硬盘': 14, '显示器': 16}, {'U盘': 20, '内存条': 30, '鼠标': 35, '移动硬盘': 4, '键盘': 10}, ] store_items = pd.DataFrame(items2, index=['仓库 1', '仓库 2', '仓库 3', '仓库 4'])

先判断缺失情况:

print(store_items.isnull().sum())

结果显示移动硬盘显示器等列在部分仓库缺失。这个缺失的业务含义是“库存为 0”,而不是“数据丢失”。此时用dropna()删除任意含 NaN 的行,四个仓库会被全部删光,因为没有一个仓库覆盖所有商品;用ffill()会让仓库 1 的移动硬盘库存继承仓库 3 的值,纯属虚构。正确的做法是用 0 填充,表示该仓库确实没有此商品:

store_items.fillna(0, inplace=True) print(store_items)

填充完成后做一次验证,确保全表没有遗漏的 NaN,并检查每个仓库的库存总量是否合理:

assert store_items.isnull().sum().sum() == 0 print(store_items.sum(axis=1))

assert语句会在条件不满足时抛出异常,相当于给数据清洗加了一道保险。sum(axis=1)按行求和,得到每个仓库的商品总数,如果某个仓库求和后明显偏低,说明填充掩盖了更深层的源数据问题,需要回到采集端排查。

整个处理顺序:检测缺失 → 判断缺失的业务含义 → 选择删除或填充 → 用统计量校验,是最常复用的数据清洗套路。遇到同类项目时,把“所有小于阈值的值转 NaN”这类规则抽象成一个小函数,传入阈值和填充值,后续换数据源只需要改动两处参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询