恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Pandas缺失值填充全解析:从缺失机制到实战选型
首页
资讯中心
/
Pandas缺失值填充全解析:从缺失机制到实战选型
Pandas缺失值填充全解析:从缺失机制到实战选型
发布时间:2026/10/5 11:20:58
1. 先别急着 fillna缺失值背后的数据逻辑做数据分析的人几乎天天都要面对空缺值。刚入行的时候我拿到一份带空格的表格第一反应就是df.fillna(0)或者df.dropna()速度快代码一行看起来问题就解决了。但后来在真实业务里栽过几次跟头才发现“填充空缺值”这件事最关键的从来不是怎么填而是先搞清楚这些空缺是怎么来的。先说一个我印象特别深的例子。当时在做一个用户消费行为的分析数据里有将近三成的“收入”字段是空的。我图省事直接用全量均值填充结果跑回归模型的时候收入变量的系数显著性和方向都变得很诡异。后来回头细查才发现那些收入为空的人群恰恰是学生和无固定职业者他们的真实消费结构和上班族完全不同。用上班族的平均收入去填学生的那部分空缺等于硬生生把两群人的行为抹平了。模型看起来跑通了实际上结论全偏了。这就引出一个关键概念缺失值不是一张白纸上的洞它本身可能代表一种信息。业内一般把缺失机制分成三类完全随机缺失MCAR缺失和任何变量都无关纯粹是数据采集时的意外比如录入员漏填了一条。这种情况用简单的均值、中位数填充问题不大。随机缺失MAR缺失与其他已观测变量有关比如高收入人群更不愿意填收入——是否缺失和“收入高低”有关但这个高低在你已有的其他字段比如职业、城市里能看出来。这种情况下分组填充或者建立模型预测填充效果更好。非随机缺失MNAR缺失本身和未观测到的值有关比如收入极高的人故意不填。这种情况是最棘手的无论用什么填充方法都会有偏差有时候“把缺失单独作为一个标记”反而比强行填充更稳妥。所以第一步我拿到数据后会先看一眼缺失分布。怎么做几个很朴素的土办法按列看缺失比例按行看缺失集中度再算一下“缺失的行”和“非缺失的行”在别的列上有没有显著差异。比如用crosstab看分组缺失率或者直接画个热力图观察缺失集中在哪几个字段上。花这几分钟是值得的它能帮你判断走哪条填充路线。2. 常用填充方法的本质均值、中位数、众数和场景边界聊完缺失机制再来看实操层面最常用的几招。网上关于fillna的教程一抓一大把但大部分只讲函数怎么用不讲什么时候用导致很多人一套方法打天下。我把最常用的几种拉出来逐个说清楚它的脾气和适用边界。均值填充可能是被用得最多的方法也是被滥用得最严重的。均值对异常值极端敏感一组数据里混进一个大到离谱的离群点均值会被瞬间拉高用它填空缺等于把离群误差扩散到所有缺失位上。所以均值填充只适合数据分布基本对称、没有明显离群值、且缺失率不高我个人经验是低于5%的情况。如果数据偏态明显中位数几乎是严格优于均值的选择——它对离群值不敏感能更好地代表“大多数人的水平”。比如收入、房价这类长尾分布字段填空缺首选应该是中位数。众数填充则基本只用于分类变量。性别、城市、渠道来源这类取值有限、没有数值大小的字段填均值完全无意义只能用众数。但这里藏着一个细节如果某个类别的占比本来就悬殊比如90%都是A类众数填充会让B类缺失的行全部变成A类这会进一步加剧类别不平衡。后面如果拿这个字段做特征训练模型类别分布会更歪模型对B类的识别能力会更差。所以遇到类别严重不平衡的字段我通常会犹豫要不要填充有时候干脆把缺失单独设为一类“unknown”让模型自己学它的规律。前向/后向填充ffill / bfill是时间序列场景的常客。它的逻辑很直观拿上一个观测值顶上。这在传感器数据、股价序列、连续监测场景里很有用因为相邻时间点的数值天然存在延续性用后面的值填前面的空缺反而不符合时序逻辑。但需要注意要么是单调递增的时间索引要么至少是有序的时间列否则“前向”的“前”没有定义会填出错误结果。而且如果连续缺失的区间太长比如一段传感器掉线了三个小时一味用ffill会把设备停机期的“静默”误当成“保持不变”这种时候可能宁可保留缺失或者结合插值来处理。插值法是比简单填充更精细的方案。df.interpolate()在内部会按索引位置做数值插值默认是线性插值也就是把空缺前后两个点直线连起来取中间值。它比均值填充更能保留数据原有的变化趋势适合连续型数值字段且缺失段的整体趋势比较平缓的情况。不过要小心如果数据本身是周期性波动很强的序列比如电力负荷、客流量的日内周期线性插值在弯折处会产生明显的“削峰填谷”失真。这种情况可以考虑methodtime按时间间隔加权或者methodspline样条插值但 spline 的 order 参数不能乱设阶数高了容易过拟合出奇怪的抖动一般 order3 已经够用。我把这几种方法、它们的本质逻辑和适用场景整理成一个表方便对照填充方法本质逻辑适用场景主要风险均值填充用整体中心水平代表缺失个体分布对称、无离群、缺失率低被离群值带偏扭曲分布中位数填充用稳健中心代表缺失个体偏态分布、有离群值丢失个体波动信息众数填充用最常见类别顶替缺失类别分类变量加剧类别不平衡前向/后向填充用相邻时间点的值顶替时间序列、相邻点强相关长缺失段失真破坏趋势线性插值按缺失段两端连线取中间值连续数值、趋势平缓周期性数据削峰填谷样条插值用平滑曲线拟合缺失段有轻微波动的连续序列阶数过高会过拟合抖动模型预测填充用其他字段建回归/分类模型预测字段间关联强、缺失机制为MAR计算成本高变量间多重共线性别把这张表背下来就完事关键是理解每一行背后的“为什么”。方法没有绝对的好坏只有跟场景匹配不匹配。后面我会拿真实代码演示这些方法并把更细的实操细节一并讲清楚。3. pandas 实操从基础 fillna 到分组填充和插值代码逐行拆解说了一堆理论现在上手写代码。为了演示方便我构造一份模拟数据包含时间列、数值字段和一个分类字段另外人为制造一些缺失值。import numpy as np import pandas as pd # 构造400条数据 np.random.seed(42) df pd.DataFrame({ time: pd.date_range(2024-01-01, periods400, freqH), value: np.random.randn(400) * 10 50, group: np.random.choice([A, B, C], size400, p[0.5, 0.3, 0.2]), }) # 随机挖掉一些空缺 df.loc[df.sample(frac0.15, random_state1).index, value] np.nan df.loc[df.sample(frac0.1, random_state2).index, group] np.nan # 先看一眼缺失概览 print(df.isna().sum()) print(df.isna().mean())isna().sum()看的是每列到底缺了多少个isna().mean()则直接给出缺失占比。这一步是做任何处理之前必须做的侦察工作。3.1 直接填充均值、中位数、众数基础写法很简单# 均值填充 df[value_mean_filled] df[value].fillna(df[value].mean()) # 中位数填充 df[value_median_filled] df[value].fillna(df[value].median()) # 众数填充 df[group_mode_filled] df[group].fillna(df[group].mode()[0])注意三点。第一fillna里我传入的是一个标量但如果想按列填不同值可以传一个字典比如df.fillna({value: df[value].median(), group: unknown})一次性处理多列比写三行赋值干净。第二mode()[0]很多人会漏掉索引。mode()返回的是一个 Series即使只有一个众数也需要用[0]取值。如果众数有多个mode()[0]取的是排序后的第一个按出现次数降序、字典序排实务中够用。第三均值和中位数在填充前就已经被 NaN 污染了吗不会。df[value].mean()计算时默认跳过 NaNpandas 的所有统计函数都带强制的 skipna 语义这个放心。3.2 前向填充与后向填充前向填充尤其要注意排序。我见过太多人直接对乱序的数据做 ffill结果填充值来自一个毫无关系的行。# 先确保时间排序 df df.sort_values(time).reset_index(dropTrue) # 前向填充用前一个有效值填后面连续的空缺 df[value_ffill] df[value].ffill() # 后向填充用后一个有效值填前面连续的空缺常和ffill搭配 df[value_bfill] df[value].bfill()如果一条数据从头到尾都没有有效值ffill 之后仍然是 NaNbfill 同理。此时可以再组合一次先 ffill 让中间段被填充剩下的边角用 bfill 兜底或者反过来。下面这种写法是常见操作df[value_combo] df[value].ffill().bfill()但我要提醒连续缺失很长的一段数据比如一段连续6个小时的断档用 ffill 会把这期间画成一条“完全没有变化”的直线这在很多业务场景里是虚假信息。遇到这种长缺口我一般会看一眼这个字段有没有周期性。如果确实是传感器或业务量这类波动很大的数据宁可先线性插值。3.3 插值从线性到时间加权到样条插值用的是interpolate()但它默认按行索引计算而不是按实际的时间间隔。如果你的时间序列本身不是等间距采样这一步就容易出错。# 线性插值 df[value_linear] df[value].interpolate(methodlinear) # 时间加权插值按真实时间间隔的比重来算中间值 df[value_time] df[value].interpolate(methodtime) # 样条插值 df[value_spline] df[value].interpolate(methodspline, order3)如果你构造的数据是等间隔的linear和time结果一致但如果前后两条有效记录之间隔着不同的时长time会更加合理。它的逻辑是间隔越远对中间缺失点的贡献越小本质上是按时间距离做了加权。interpolate默认的limit_direction是forward意思是只从前往后填充。如果数据开头就有缺失需要加参数limit_directionboth才能让首尾空缺也被处理。这是一个非常容易踩的坑我之前在一个带缺失前缀的数据集上跑插值结果前面几行始终是 NaN一度以为是代码写错了。还有一个容易被忽视的细节interpolate的limit参数可以限制填充的连续缺失数量。超出部分保持 NaN这个非常有用。比如你知道某段缺失超过3个小时就没有填充意义了可以设置limit3避免长段失真。3.4 分组填充多数情况下更靠谱的填充姿势前面提到缺失如果是 MAR随机缺失那么缺失个体的分布往往和某个分组特征相关。比如不同地区的用户收入差异极大你就不能拿全量中位数去填充而应该按地区分组求中位数再分别填充。# 分组填充每个group用自己的中位数填 df[value_group_median] df.groupby(group)[value].transform(lambda x: x.fillna(x.median()))transform在这里会先按 group 分组再对每个组计算中位数并广播回每一行最后填充。注意group字段本身也有缺失如果直接用groupby(group)那些 group 为 NaN 的行可能会被丢弃在填充范围外。稳妥的做法是先把 group 用“未知组”填充掉再做分组填充df[group_temp] df[group].fillna(UNKNOWN) df[value_group_median] df.groupby(group_temp)[value].transform(lambda x: x.fillna(x.median()))分组填充比全局填充更能保留组间差异但要注意如果某个分组本身样本量很少比如只有几条用这一组的中位数填充并不稳定。这种情况我建议对该组回退到全局中位数。这种“小样本回退”逻辑用代码表达就是def safe_fill(s): if s.count() 10: return s.fillna(s.median()) return s.fillna(df[value].median()) df[value_safe] df.groupby(group_temp)[value].transform(safe_fill)这类保护性逻辑在真实项目中非常重要但基本不会出现在教程里。3.5 模型预测填充当字段之间的关联足够强时如果字段之间关系复杂简单的分组填充不够用可以用其他字段训练一个模型来预测缺失值。最经典的是用sklearn.impute.IterativeImputer它会迭代地用其他列轮流预测每一列的空缺本质上是一个链式回归。from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer from sklearn.ensemble import RandomForestRegressor # 先对分类字段做数值编码模型才能消费 df_encoded df.copy() df_encoded[group_code] pd.factorize(df[group])[0] # 只取数值特征参与建模注意剔除没有业务意义的原分类列 features [group_code] X df_encoded[features [value]].copy() imputer IterativeImputer(estimatorRandomForestRegressor(n_estimators100), max_iter10) X_imputed imputer.fit_transform(X) df[value_model] X_imputed[:, -1]这个方法的代价是计算量和复杂度都会上升而且需要谨慎评估建模填充会让“填出来的值”带有其他变量的信息如果后面训练模型时还要用到这些变量容易出现多重共线性问题。所以我的建议是只有在字段之间的相关性确实很高、并且缺失率不低的时候才考虑这条路否则用分组中位数已经够用。4. 实战中最容易踩的坑我拿真实数据集踩过一遍这一节聊一些我实打实踩过的坑。都是网上教程不太会写的细节但每一个都在真实项目里坑过我。4.1 填完以后再验证“填充是否改变分布”很多人填充完就交差了从不回头看填充前后的分布对比。正确的做法是填充完成后对比填充前后的均值、标准差、分位数以及把填充值和真实值混合后的直方图与原始真实值的直方图叠在一起看。如果发现填充后标准差明显变小说明填充把数据“压扁”了——因为大量缺失位填上了同一个常数方差自然被稀释。import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(8, 4)) ax.hist(df[value].dropna(), bins50, alpha0.6, labelobserved) ax.hist(df[value_median_filled], bins50, alpha0.3, labelafter median fill) ax.legend() plt.show()如果两个分布形状差异明显你得重新考虑填充策略。这种可视化验证看似多了一步但它能避免你在错误的填充结果上继续建模。4.2 时间序列数据别用“未来数据”填充时间序列预测任务里填充时有一个铁律不能把缺失时刻之后的数据拿来填缺失时刻。如果你对一条时间序列数据用整个序列的均值填充等于让模型在训练时偷看了“未来”的统计量验证时指标会虚高上线后直接崩盘。举个例子假设你要预测明天某个仓库的出货量历史数据中间缺了三天你用后面几天的真实出货量来插补这三天那么模型训练时学到的是包含未来信息的序列等到真正预测明天时明天还没有发生你拿什么来插补所以时间序列填充要严格按时间切分来做要么只用缺失点之前的数据做 ffill / 插值要么在填充时保证窗口内不出现未来数据。interpolate的limit_directionforward其实已经默认考虑了这一点但如果你手动用全序列均值填充就踩进了这个坑。4.3 索引错位引发的“幽灵填充”fillna默认是按索引对齐的。如果你做过dropna、reset_index或者筛选操作行号早就乱掉了这时候再对两个 DataFrame 做填充或计算容易因为索引错位而产生幽灵结果。# 错误示例筛选完之后索引还是乱的直接填充可能错位 sub df[df[group_temp] A] sub[value_filled] sub[value].fillna(sub[value].median())这个例子问题不大因为填充是逐行处理的。但如果你用df[value].fillna(other_df[value])这种跨 DataFrame 填充索引对不上的时候 pandas 不会报错而是留下 NaN或者把行位置同名的错误数据填进去。所以跨表操作前先reset_index(dropTrue)或者显式确认索引一致再动手。4.4 高缺失率字段的“末日策略”缺失率超过某个阈值时任何填充都可能创造虚假信息。我个人的经验值是如果字段缺失率超过50%填充的意义已经不大了超过70%这个字段在建模任务里基本只有“是否缺失”这个二元信息有价值。这种字段的处理思路不是填充数值而是把缺失本身变成特征df[value_is_missing] df[value].isna().astype(int) df[value_filled] df[value].fillna(-999) # 或者用中位数is_missing作为一个新特征让模型自己去学习“缺失”这个状态和标签的关系。在很多业务场景里“这个人没有填收入”这件事本身可能比“他的收入填了多少”更有信号价值。4.5 整形数据里的“上帝视角”平均在按用户聚合的数据集里每个人的多行数据比如按周记录消费之间也有相关性。如果只对全表做均值填充一个人的所有空缺周都会填成全局均值这个人自身的消费波动信息就没了。遇到这种纵向结构分组填充按“用户”来分往往是更符合直觉的做法df[spend_filled] df.groupby(user_id)[spend].transform(lambda x: x.fillna(x.median()))当然这里同样要注意组内样本量太少时回退到全局统计量。5. 面向不同分析目标的选型决策填充、保留还是删除填充方法选型本质上取决于你拿这份数据干什么。目标不同最优策略完全可能相反。5.1 做统计报表优先保持分布的“稳定”如果是做一个分布描述比如想告诉业务方“本季度客户平均收入是多少”这时候用中位数填充比较安全因为它不会被离群值带偏。报表最忌讳的就是因为填充方法不同导致同一个口径下季度之间出现不可解释的跳变。均值填充在数据存在离群值时会让季度均值一个个季度跳来跳去业务方来问为什么你解释不清楚。中位数在这个口径下稳定得多。5.2 训练机器学习模型核心是避免泄漏和信息污染建模场景下的第一原则是测试集不能用全量数据的信息。所谓数据泄漏最经典的例子就是先对全数据集做均值填充再划分训练集和测试集。因为测试集的均值已经参与了填充相当于测试集的信息提前进入了训练集。正确做法是# 先划分 from sklearn.model_selection import train_test_split df_train, df_test train_test_split(df, test_size0.2, random_state0) # 只用训练集计算中位数 med df_train[value].median() # 训练集和测试集都用同一个med填充 df_train[value_filled] df_train[value].fillna(med) df_test[value_filled] df_test[value].fillna(med)建模任务里填充值的“精不精确”未必是第一位**“填充方式能否避免信息泄漏”**才是第一位。很多时候用最简单的中位数只要能保证严格按训练集口径填充效果会比那些花哨的插值更好。另外一个建模场景的细节如果你最终要训练树模型填充精度其实没那么重要。树模型对输入特征的单调变换不敏感只要缺失不是全列同一常数导致方差为零树模型基本能容忍。这种情况下我甚至建议省事一点用-999或中位数填充即可把精力花在更有价值的特征工程上。但如果你要用线性模型或神经网络填充精度会直接影响模型性能这时候分组填充或模型预测填充才值得考虑。5.3 其他备选不填充行不行有些场景里与其花力气填充不如直接删除缺失行。这个策略适用于数据量很大、缺失率低比如低于1%、且缺失的机制接近完全随机。此时删除缺失行丢失的信息很少也不会引入填充偏差。但删除要讲究策略。如果一行里只有个别列缺失你可以只删除那些“关键列”缺失的行保留其他列而不是一刀切dropna()把整行全扔了。更精细的写法# 只丢弃 value 列为空的行其他列的缺失再单独处理 df_clean df.dropna(subset[value])对于分类变量我通常不删除也不无脑众数填充而是把缺失单独作为“未知”类别这比硬填一个众数更尊重原始信息。很多落地项目里模型的线上效果反而因为这种“未知”标记而变好因为线下众数填充把大量样本推向了同一类模型在真实场景里看到更多变体时就不适应了。5.4 一个完整的选型决策路径我在实践中逐渐形成了一条决策路径分享出来供参考统计每列缺失比例和“缺失行”在其他关键列上的分布差异。如果缺失率低于1%~2%直接删行别折腾。如果字段是分类变量优先把缺失编为“unknown”一类类别不平衡严重时更要保留缺失标记。如果字段是数值型且偏态明显用中位数对称且无离群点可以考虑均值。缺失率较高时优先分组填充。如果是时间序列按时间顺序处理优先 ffill / time 插值严格避免使用未来信息。如果缺失率很高超过30%且字段间关联强考虑模型预测填充同时新增“是否缺失”标志特征。不管选择哪种方法填完后对比填充前后的分布确认没有引入明显的分布变形。这条路径没有用到任何高级算法但在绝大多数业务场景里已经能覆盖掉90%的情况。高级方法比如多重插补在学术研究里很常见但在工业界你需要的是“可解释、可控、可复现”的填充方案而不是一味追求数学上的最优。6. 填充之后这只是数据清洗的一部分默认填充值只是数据预处理里的一环而不是终点。我发现很多初学者把填充当成“搞定缺失了”然后就直接去跑模型结果后面又冒出类型不对、异常值干扰、分布偏移等问题。填充之后至少还要跟两步第一步检查填充后的数据类型和取值范围。比如fillna(-999)之后字段从 float 变成了带自定义哨兵值的数值如果后续要做标准化这些哨兵值会对均值和方差产生剧烈干扰需要先做特殊处理。分类变量填充后检查唯一值数量是否符合预期有时fillna(unknown)会产生一个之前完全没出现过的新类别需要确保下游流程能容忍。第二步再检查一遍其他类型的数据问题。比如重复行、日期格式不一致、异常值。有一个经典场景是数据里存在-9999或者9999这种本来就是“缺失代称”的异常编码第一步没处理第二步建模时它就成了巨型离群点把模型的系数搅得天翻地覆。我的习惯是统一先把这些-9999、NA、NULL、N/A这类符号全部转成np.nan再来走填充流程# 常见缺失代称统一映射 df.replace({-9999: np.nan, 9999: np.nan, N/A: np.nan, NULL: np.nan}, inplaceTrue)这样后续所有填充逻辑都会自动覆盖这些“隐性缺失”不会漏掉。还有一点容易被忽略填充后要把“缺失行”和“非缺失行”的标签分布做个对比。如果填充后整体分布出现明显偏移说明填充方法有偏。这种检查对于分类任务尤其关键我一般会用groupby(target)[value_filled].mean()按标签看看差异如果差异异常放大就要回头审视填充逻辑是否引入了与标签相关的噪声。7. 完整可复用的封装函数最后分享一个我平时封装的填充函数把决策过程固化下来不同项目里直接改参数就能复用。它做的事情很简单先按缺失类型分类再对每列执行合理的填充策略最后返回填充后的数据和填充摘要。def smart_fill(df, num_strategymedian, group_colNone, special_valuesNone, add_missing_flagTrue): 参数说明 - num_strategy: median 或 mean数值列的填充策略 - group_col: 分组列名如果传了则按该列分组填充 - special_values: 需要先统一替换为NaN的特殊值列表如[-9999, N/A] - add_missing_flag: 是否在填充后新增“是否缺失”标记列 返回(填充后的DataFrame, 各列缺失率字典) df df.copy() if special_values: df.replace(special_values, np.nan, inplaceTrue) missing_summary df.isna().mean().to_dict() for col in df.columns: if df[col].dtype object: df[col _missing] df[col].isna().astype(int) if add_missing_flag else None df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else unknown, inplaceTrue) elif df[col].dtype in [float64, int64]: if add_missing_flag: df[col _missing] df[col].isna().astype(int) if group_col and group_col in df.columns: df[col] df.groupby(group_col)[col].transform( lambda s: s.fillna(s.median() if num_strategy median else s.mean()) ) df[col].fillna(df[col].median() if num_strategy median else df[col].mean(), inplaceTrue) else: df[col].fillna(methodffill, inplaceTrue) return df, missing_summary这个函数不可避免会带有我个人的取舍比如数值列默认用中位数、分类列默认用众数你可以按照自己的场景替换。重点是它把前面讲的“缺失标记、特殊值归一、分组填充、兜底填充”都串到了一起新项目拿过去稍作调整就能用不用每次从头想一遍。写到这里关于 python 填充空缺值的核心思路和方法基本都覆盖到了。如果只留一句话我会说缺失值填充没有银弹先搞懂缺失从哪来再决定怎么填填完之后记得验证、记得标记、记得检查泄漏。数据清洗是一场慢工出细活的过程填好一个空只是其中的一块砖但这一块砖放得正不正决定了后面的模型能盖多高。