处理数据的时候,缺胳膊少腿的字段永远是第一道关卡。我记得刚接手一份电商订单表的时候,一列“收货区域”里有将近40%是空值,旁边同事瞟了一眼就建议“直接删掉不就行了”。我先没急着删,跑了一行缺失统计,结果发现这40%的空值几乎集中在某个特定月份的订单里——如果当时手一抖全删掉,后面做月度区域销量分析时,那个月的数据会直接塌成一块空洞。缺失值删除这件事,看起来只是给dropna传几个参数的手艺活,但每一步取舍,其实都在悄悄影响后续结论的方向。
这篇文章要聊的就是“缺失值删除”这件事:什么时候能删、什么时候不能删、删行还是删列、以什么标准删,以及delete之外还有没有更好的选项。适合刚接触pandas、正在学数据清洗的学生和转行者,也适合日常处理报表、被空值反复折磨的运营和产品同学;如果你在做特征工程,需要决定训练样本里哪些行列能删、哪些必须保,这篇也能给你一套可参考的决策思路。不管用什么工具——Excel筛选、SQL写DELETE还是pandas里跑dropna——背后那套判断逻辑是通用的,别让“删除”成为头脑一热的肌肉记忆。
1. 删除缺失值之前,先给数据做一次“体检”
很多人拿到数据的第一个动作是看行数,第二个动作就是哪个字段有NaN就把它干掉。这个流程我当年也走过,直到有一次把一份关键字段缺失率只有2%的营销表删掉了6000多行,下游同事跑出来的人群覆盖率为0,我才反应过来:没有体检就动刀,跟闭着眼做手术没区别。
1.1 理解缺失模式:MCAR、MAR、MNAR
统计上把缺失分成三种模式,搞明白这些名词不是掉书袋,而是为了判断删完之后结果是否还有代表性。
-
完全随机缺失(MCAR):缺失跟数据本身没有任何关系,纯粹是采集时随机漏掉了。比如调查问卷里某几份答卷的某个选择题没填,这属于运气问题。这种情况下直接删掉缺失行,对剩余的总体分布几乎没影响,是最安全的一种删除场景。
-
随机缺失(MAR):缺失跟某些已观测到的变量有关系,但跟缺失值本身无关。举个例子,一份用户行为日志里,注册年份较早的老用户更容易缺失“首次来源渠道”这个字段,但“首次来源渠道”的具体值跟是否缺失没有关系。这种情况下,直接删缺失行,如果恰好删掉的都是老用户,后面分析新老用户特征时会引入偏差。
-
非随机缺失(MNAR):缺失与否跟缺失值本身有关,这是最棘手的情况。比如高端产品价格字段缺失,很可能是因为报价太高不敢填,或者低端设备上报失败导致某功耗字段缺失。这类缺失如果直接删行,留下的全是“敢填的”或者“设备好”的样本,系统性偏差几乎躲不掉。
对应到“能不能删”的结论上:MCAR可以放心删,MAR要看清关联字段再删,MNAR最好谨慎处理——先想想为什么缺,实在想不明白再考虑删除以外的方案。
1.2 用pandas和missingno快速定位缺失分布
做体检不是靠肉眼一行行看CSV,而是靠统计和可视化。我每次拿到新表格,固定会跑下面这段代码:
python复制import pandas as pd
df = pd.read_csv("order_data.csv")
# 1. 看每列的非空计数和类型,快速发现“看着有值其实是空字符串”的坑
df.info()
# 2. 每列缺失总数,按缺失率倒序
missing_df = pd.DataFrame({
"缺失数量": df.isnull().sum(),
"缺失率": df.isnull().mean()
}).sort_values("缺失率", ascending=False)
print(missing_df[missing_df["缺失率"] > 0].round(4))
df.info()会打印每列的非空数量,如果你的列名下面显示的Non-Null Count低于DataFrame的总行数,这一列就存在缺失。isnull().mean()算的就是每列的缺失比例,用sort_values排一下序,哪些字段是“重度缺失”一目了然。
可视化也很值得做,Python里我常用missingno这个库,几行代码就能看出缺失是随机散布,还是呈带状、成片出现:
python复制import missingno as msno
import matplotlib.pyplot as plt
# matrix图:每行是一条样本,每列是一个字段,白色条纹代表缺失
msno.matrix(df.sample(500))
plt.show()
# heatmap:看哪些字段倾向于一起缺失
msno.heatmap(df)
plt.show()
matrix图里如果白色条纹都是均匀的小点点,大概率是MCAR;如果出现明显的空白横条,说明某一段样本集体缺失,这跟时间批次或者设备分群有关,直接删就会把某个群体整段抽走。heatmap图能帮你发现“字段A缺失时字段B也缺失”的关联,比如“收货区域”和“邮编”经常一起缺,说明它们可能来自同一个采集环节,删除或填充时可以合并处理,不用为每个字段单独纠结一遍。
1.3 体检之后先做一张“删除决策表”
看完统计和图表,我会把每个字段的问题和维护方式写成一页纸。这张表不用多规范,但一定要写清楚三件事:缺多少、为什么缺、字段重不重要。只有回答了这三件事,才能回答“要不要删”。
| 观察结果 | 可能的含义 | 处理倾向 |
|---|---|---|
| 某列缺失率超过80% | 数据基本没被采集,或者采集链路已经断裂 | 倾向整列删除,除非该列有不可替代的业务价值 |
| 缺失集中在某个时间段 | 上线了BUG、换了采集方案或渠道断流 | 先修复和补数,不能直接删;时间切片对比会失真 |
| 缺失率低于5%且分布随机 | 典型的MCAR,信息损失可忽略 | 可以直接删除含缺失的行 |
| 主键或业务唯一标识缺失 | 该行无法关联任何业务实体 | 必须删除该行或尝试补全标识,保留会引入脏数据 |
| 某字段缺,但可用其他字段推断 | 例如“省份”缺失但“收货地址”里能解析出来 | 优先尝试填充,不急着删 |
这页纸做完,你大概就清楚哪些缺失该由删除来“背锅”,哪些得去想办法补,哪些要回头找数仓的同事清理采集链路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. dropna参数逐个拆解:axis、how、thresh、subset和现代用法
体检做完了,确认字段可以删,接下来才是真正操作pandas的dropna。这个API看着简单,但五个核心参数代表五种完全不同的删除逻辑,用错一个,结果就差出去十万八千里。
2.1 axis:决定删除方向是行还是列
axis是第一个要搞清楚的参数,它决定了你的删除动作是“删掉某些行”还是“删掉某些列”。
python复制# 默认axis=0,删除所有含缺失的行
df_clean = df.dropna(axis=0)
# axis=1,删除所有含缺失的列
df_clean_col = df.dropna(axis=1)
实际业务里,删行是主流,删列要极其克制。因为每一列通常代表一个业务含义,删掉一列等于丢掉一个维度的信息,而删掉一行,只是少了一个样本。但如果某列缺失率实在太高,比如超过80%甚至90%,这个字段后续不管做统计还是建模,都很难提供有效信息,留着反而是负担,这时候删列就是合理选择。
提示:pandas 2.x之后的版本里,仍然可以用
axis="index"和axis="columns"这样的别名来写,比记0和1更直观,不容易搞混。
2.2 how:是有一个空就删,还是全空才删
how参数控制删除的触发条件,取值只有"any"和"all"两个。
python复制# 某行只要任意一个字段缺失就删掉——默认值
df_clean = df.dropna(how="any")
# 某行所有字段都缺失才删掉
df_clean_all = df.dropna(how="all")
how="any"是最常见的用法,但我见过不少人在字段特别多的宽表上也这么干,结果行数哗啦啦往下掉,才15个字段的表删完只剩原来的一半。这种情况下并一定不是数据质量差,而是删除条件太苛刻。
how="all"主要用于清理全空行。比如从上游接口拿来的数据偶尔会附加一行空的记录,或者合并DataFrame之后凑出来一行全NaN,这种行没有任何信息,直接删干净。想象一下打扫房间,how="any"是“桌上有一粒灰就扔掉整张桌子”,how="all"是“整间屋全空了才扔掉屋子”,后者显然更温和,适用场景也更窄。
2.3 thresh:保住“有料”的行,而不是一刀切
thresh是我在真实项目里用得最多的参数,它的含义是:保留至少有n个非空值的行(或列)。
举一个我踩过的例子:有一次处理一份客户问卷数据,表格里一共50个题目,每行是一个受访者。如果用how="any",只要有一道题没填,整个人就被删了,最后能留下的样本不到30%,分析结论根本不敢用。改成thresh=20之后,只要能答完20题的受访者都保留下来,样本量立刻恢复到了80%以上。
python复制# 保留每行至少20个非空字段的记录
df_clean = df.dropna(thresh=20)
thresh的精髓在于,它把“全有或全无”变成了“有足够的量就可以”,适合字段特别多、且每个字段“含金量”不低的宽表。再比如传感器数据,某一行有10个指标,只测通了6个,剩下4个NaN,但你分析的核心指标其实就在那6个里,用thresh就能把这条有用信息留下来。
2.4 subset:只在关键字段上做缺失判定
subset可以让你在调用dropna时,只针对某些字段检查缺失,其他字段就算为空也不参与“是否删除”的判定。
python复制# 只有关键字段缺失时才删除该行
df_clean = df.dropna(subset=["order_id", "user_id", "pay_amount"])
这个参数的价值在于控制误杀率。很多表里都会有几个“非关键但经常空”的字段,比如“用户备注”“公司规模”,这些字段本来就允许空,如果因为它们的存在把整条订单删掉,那就亏大了。
写subset的时候,我会把那些“缺失了就没法干活”的字段放进去:业务主键、关联外键、核心指标。其他字段交给填充或后续单独处理,大家各管各的一亩三分地。
2.5 inplace的现代表达:别原地改,接返回值
老代码里经常看到df.dropna(inplace=True),把修改直接作用到原来的DataFrame上。这个写法在pandas新版本里已经开始被警告了,原因是原地操作容易导致代码难调试——你根本不知道哪个环节把原始数据给改了。
我现在统一用接收返回值的新写法:
python复制# 推荐:改完之后复制一份,原表保持不动
df_clean = df.dropna(subset=["user_id"])
# 不推荐:原地修改容易污染源数据,且新版pandas会报警告
df.dropna(subset=["user_id"], inplace=True)
保留原表的好处是,你随时可以把清洗后的数据跟原始数据做对比,验证“我到底删了什么”。数据清洗本身是个可回溯的过程,弄完之后发现自己删错了,但原表已经被改了,没有后悔药可吃。这个习惯在多人协作时尤其重要,你永远不知道下一个接手你代码的人,会不会因为你的inplace=True而对原始数据一顿输出。
3. 实战场景:缺得不多、缺得太多、缺得蹊跷分别怎么删
参数都认识之后,真正难的是把参数组合起来应对现实场景。下面四个是我在工作中反复遇到的典型情况,每个都有自己的处理倾向。
3.1 核心字段缺失:用subset精确删除
场景描述:订单表中order_id有空值、user_id有空值,这些字段是后续关联用户表、订单明细表的唯一凭证。缺了它们,这行记录就算其他字段再完整,也无法对业务做出有效解释。
处理方式:
python复制df_clean = df.dropna(subset=["order_id", "user_id"])
这种删法最不需要犹豫。一个连订单号都没有的记录,留在数仓里只会污染后续所有的join和聚合计算。你要做的不是“尽量保留”,而是“确保留下的每条记录都有可用的业务标识”。
3.2 整列大面积缺失:直接删列
场景描述:数据里有一列“广告点击ID”,可能由于埋点版本原因,上线后一直没有采集到,90%以上都是空的。这种情况下,这一列的信息量几乎为零,硬要保留只会增加后续处理负担。
处理方式:
python复制# 先看出这一列缺失率
missing_rate = df["ad_click_id"].isnull().mean()
print(f"{missing_rate:.2%}")
# 确认超过阈值后删列
if missing_rate > 0.8:
df_clean = df.drop(columns=["ad_click_id"])
我通常会用drop(columns=["列名"])而不是dropna(axis=1),因为后者会把所有缺失率超过0的列都删掉,范围太不可控;前者是我显式指定,清楚自己在删什么。如果你确实想用dropna删列,至少加个thresh限制一下,比如df.dropna(axis=1, thresh=int(len(df) * 0.7)),表示“保留非空值数量不少于总行数70%的列”。
3.3 宽表整体缺一点:用thresh保留有料行
场景描述:还是之前说的问卷数据,50个字段里受访者一般能填80%,但很少有人能填满100%。how="any"会严重误杀,thresh是更好的选择。
处理方式:
python复制# 假设共50列,要求作答比例达到60%,即至少30个字段非空
min_non_null = int(df.shape[1] * 0.6)
df_clean = df.dropna(thresh=min_non_null, axis=0)
一句话总结这三类场景的取舍逻辑:核心字段缺失必须删行;整列信息密度太低就删列;列多且行质量参差不齐时,用阈值的思路取代一刀切。
3.4 “先删列还是先删行”的先后顺序
很多人会忽略的问题:如果同时存在需要删的列和需要删的行,应该先删哪个?我的习惯是先删列,再删行。
原因很简单:删除列会改变每行的非空字段数量,如果你后面还想用thresh来删行,先删行的话,后面删列时又会把原本保留的行重新变成“不够格”的行。反过来,先把没用的列清理干净,再基于剩下的字段来判定行是否值得保留,逻辑更顺畅,结果也更稳定。
python复制# 推荐顺序
df_step1 = df.drop(columns=["low_info_col1", "low_info_col2"])
df_clean = df_step1.dropna(subset=["core_field"], thresh=20)
3.5 时间序列和面板数据:删除需额外谨慎
如果数据带时间戳,是每天的成交记录、股价序列或App埋点,就得额外注意:不要因为缺失值破坏时间序列的连续性。
举个例子,有一份按天聚合的UV数据,其中某几天服务宕机导致记录缺失。如果直接把缺失的那几行删掉,后续做环比计算或者时序预测,模型会以为这几天“前后相接”,直接通过网络跳到后一天,环比波动会被严重扭曲。这时比起删除,ffill()或线性插值往往更合适。
python复制# 时序数据里,缺失值前后向填充比直接删除更利于分析
df.fillna(method="ffill", inplace=True)
当然,这里“删除”也并非完全禁用。如果缺失的单日记录长度很长,比如数据源本身就断了一个月,填充反而会制造虚假数据,这时候我会把这段连续缺失的整体区间切掉,而不是零散删点。
4. 什么时候该停手:缺失率界限与“删除之外”的替代方案
很多人把“缺失值删除”当成清洗的默认动作,但删除其实有代价:删行损失样本量,删列损失特征信息。当你的数据总量本来就少,比如只有几千行,或者某一列跟你的分析目标强相关,直接删就不是最优解了。
4.1 缺失率红线:5%、20%、70%的经验切分
我不太喜欢说“绝对阈值”,但根据多年经验,可以把缺失率划成几个区间来辅助决策:
| 缺失率 | 特征信息保留程度 | 建议 |
|---|---|---|
| < 5% | 信息丢失可忽略 | 如果样本量充足,直接用dropna删行 |
| 5% - 20% | 有一定信息丢失,但还可以挽救 | 优先填充;如果缺失随机,也可用thresh保留有效行 |
| 20% - 70% | 大量信息缺失 | 尝试填充、插值或建模预测;结合字段重要性判断是否删列 |
| > 70% | 信息太少,保留价值不高 | 倾向删列,除非有明确的业务必要性 |
这套判断只是起点,不是铁律。如果某列缺失率有60%,但它恰好是你要预测的目标变量“用户是否流失”,那你反而要想办法去补它,或者分析缺失本身是否有业务意义,而不是随手删掉。
4.2 删除之前,先想一想“能不能补”
我还是那份问卷数据的例子,后来我用中位数填充代替了删除,结论完全不一样。填充的本质是用已有信息对缺失值做估计,在很多场景下比删除更“保真”。常用填充方式有:
- 固定值填充:
df["列名"] = df["列名"].fillna(0),适合缺失本身有业务含义的字段,比如“消费次数缺失”可能代表“没消费过”。 - 集中趋势填充:均值、中位数、众数,适合数值型且分布相对平稳的字段。注意均值容易被极值带偏,所以带偏态分布的数据我更推荐中位数。
- 前后值填充:
ffill()和bfill(),适合时间序列数据,用相邻时刻的值来补。 - 线性插值:
df.interpolate(),对趋势比较平滑的序列效果很好,至少比前值填充更优雅一些。 - 模型预测填充:用其他字段做特征训练回归或分类模型来预测缺失值,适合数据量大、字段间关联强的场景,代价是成本高,容易过拟合。
4.3 缺失指示变量:让“缺失”本身成为信息
这点在机器学习特征工程里很重要。有时候“是否缺失”本身就是一个有效的信号。比如“设备型号缺失”可能意味着用户使用的不是常规手机渠道。直接把缺失删掉相当于把这个信号也抹掉了。
我的做法是,在删除之前先给这些字段加一个“缺失标记”:
python复制for col in ["device_model", "channel"]:
df[col + "_is_missing"] = df[col].isnull().astype(int)
然后你再去考虑删除或填充原列。这样哪怕原字段被删掉,模型或分析里也保留了“这个样本的该字段曾经缺失”这一信息,后续说不定就是XGBoost里信息增益最高的特征。这也是我把删除当成“最后手段”而不是“第一手段”的原因之一——删除不是让信息消失,而是你主动选择丢弃一部分信息,前提是你确定它们真的没有用。
4.4 结构化决策:把“删不删”变成一张公式
在实际工作中,我会画一个很简单的决策流程,用文字写出来就是:
- 这个字段是主键或核心标识吗?是→删行或补全。
- 这个字段的缺失率超过70%吗?是→考虑删列。
- 这个字段跟分析目标强相关吗?是→优先填充,不删。
- 缺失是随机的吗?是→可以删行;不是→先查原因再定。
- 删完会损失多少样本/特征?损失超过可接受范围吗?是→考虑填充或thresh。
没有套适合所有数据集的公式,但按这个顺序过一遍,基本不会犯“无脑全删”的错。
5. 一份订单数据的完整删缺演示:从体检到验证
前面理论说了不少,最后放一个可以复现的小例子,把整个“缺失值删除”的流程串起来。这里用的是一份模拟的订单数据,包含订单号、用户ID、支付金额、地区和会员积分五个字段。
python复制import pandas as pd
orders = pd.DataFrame({
"order_id": ["A001", "A002", "A003", "A004", "A005", "A006", "A007"],
"user_id": ["U001", "U002", np.nan, "U004", "U005", "U006", np.nan],
"pay_amount":[99.0, np.nan, 50.0, np.nan, 30.0, 20.0, 88.0],
"region": ["华东", "华南", np.nan, "华东", np.nan, "华南", "华北"],
"score": [4, 5, 3, 1, 5, 2, 4]
})
print(orders.isnull().sum())
输出:
code复制order_id 0
user_id 2
pay_amount 2
region 2
score 0
dtype: int64
现在做分析决策:
order_id不能空,主键缺失需要处理。user_id有2个缺失。用户ID是关联用户维度的关键外键,没有它无法做用户分层,所以这2条记录需要删除或补全——我选择删除。pay_amount有2个缺失。支付金额是核心分析指标,直接填空会引入错误信息,而且缺失数量不大,删除这两行对整体结构影响很小。region有2个缺失。但如果我们只分析支付金额,地区缺失不是致命问题,可以用固定值“未知”填充,不必删除。
按这个逻辑,先处理user_id和pay_amount这两个核心字段:
python复制orders_clean = orders.dropna(subset=["order_id", "user_id", "pay_amount"])
print(orders_clean)
执行后剩下来的行分别是A001、A005、A006、A007,一共4行。
这个时候再回头看region列,可能仍然有缺失(比如A007的region也缺失),但如果分析目标是“支付金额”,region缺失不影响主链路,可以先用fillna("未知")填充。
整个演示可以看到一个原则:删除的触发条件是对字段做分级,而不是对所有字段一视同仁。先判断哪个字段缺了必须删,哪个字段缺了能容忍,再去下代码。
最后分享一个我自己坚持的收尾操作:把删除结果和原始数据做一次质量对比,记录这次清洗到底删掉了多少行、多少列、多大比例,然后写进数据文档。这样下次别人拿到这份数据,能知道它经历过什么,而不是面对一份莫名其妙的“干净表”各种猜。很多时候,数据清洗的产出不只是那个清洗后的DataFrame,而是你在这个过程里对数据字典、缺失原因和删除口径的梳理——这些文字沉淀,比代码本身更值钱。
