ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Pandas缺失值处理实战:dropna与fillna用法详解

Pandas缺失值处理实战:dropna与fillna用法详解 拿到一份数据集第一步不是急着跑模型、画图表而是先看有没有空值。这个习惯我踩过不少次坑才养成的。空值处理不好统计结果会偏模型会报错图表会出现难看的缺口甚至你都没发现哪里出了问题结果就已经不对了。在Python里处理空值最常用的就是Pandas提供的dropna()和fillna()这两个函数一个负责删一个负责填。这篇文章就围绕这两个函数把它们的参数、用法、适用场景和常见坑一次讲透。文章适合刚入门Python数据分析的读者也适合写过一段时间但没系统梳理过缺失值处理的朋友照着示例敲一遍基本就能应对日常工作中八成的空值处理需求。1. 整体设计思路先搞清楚“删”和“填”的边界1.1 为什么缺失值不能直接无视很多初学者拿到数据看到NaN就直接忽略继续做后面的计算。这在某些情况下没问题但多数时候会埋雷。比如你统计平均值Pandas的mean()方法默认会跳过NaN这看起来“智能”但如果你要统计的是每个用户的消费总金额某一行消费记录缺失了跳过它就等于把这个用户的消费算少了汇总结果就会失真。再比如做机器学习特征工程绝大多数算法包括sklearn里的线性回归、决策树等根本不能接受NaN输入会直接报错。所以处理缺失值不是“可选项”而是数据清洗里绕不开的一步。问题是面对缺失值到底该删还是该填1.2 dropna与fillna的定位差异我的习惯是先用一句口诀做判断删行容易填坑难能填不删删前先看。dropna()的定位是“删除含有缺失值的行或列”fillna()的定位是“用指定值或逻辑填充缺失值”。两者没有绝对好坏只有合不合适。具体来说遇到以下情况优先考虑dropna()缺失值占比极小比如一万行里只有两三个空值删除后不影响整体分布某一行或某一列缺失比例过高比如超过50%这条记录或这个特征本身可信度就低缺失值所在的列是主键、ID、时间戳这类关键标识缺失意味着整行数据不可用而遇到以下情况更适合fillna()缺失值有一定占比直接删除会损失太多样本缺失列是数值型连续变量可以用均值、中位数等统计量填充缺失列是时间序列需要按前向或后向延续的方式进行填充业务上对缺失值有特殊定义比如缺失代表“无此项”可以用0或指定占位值填充这套逻辑捋顺了后面选函数就是顺理成章的事。1.3 数据清洗的操作顺序建议处理空值前强烈建议先做一次摸底排查。不需要写复杂代码三行就够import pandas as pd df pd.read_csv(your_data.csv) print(df.isnull().sum()) print(df.shape)isnull().sum()会输出每一列的缺失值数量shape告诉你数据规模。先看一眼这两项心里就有谱了哪些列缺失多哪些列缺失少该删还是该填基本能定个大致方案。这个习惯比任何“高级技巧”都重要。2. dropna()函数详解什么时候删、怎么删2.1 基础语法与核心参数dropna()的完整语法是DataFrame.dropna(axis0, howany, threshNone, subsetNone, inplaceFalse)每个参数的作用我先用一句话概括axis决定删除方向0表示删行1表示删列how决定删除条件any表示只要有缺失就删all表示全部缺失才删thresh设定保留条件某行或列非空值数量不少于这个阈值就保留subset限定检查范围只针对指定的列或行判断缺失inplace是否原地修改原DataFrame默认False返回新对象其中最容易理解错的是thresh这个参数。它不是“缺失比例阈值”而是“非空值数量阈值”。比如thresh3表示这行至少有3个非空值才保留换句话说最多允许有总列数-3个缺失值。2.2 默认行为与axis参数的直观对比直接看代码最直观。先构造一个带缺失值的示例数据import pandas as pd import numpy as np df pd.DataFrame({ name: [张三, 李四, 王五, 赵六], age: [25, None, 30, None], city: [北京, 上海, None, 广州], score: [88, 92, None, 75] }) print(df)输出结果name age city score 0 张三 25.0 北京 88.0 1 李四 NaN 上海 92.0 2 王五 30.0 NaN NaN 3 赵六 NaN 广州 75.0这时候直接调用df.dropna()默认参数是axis0, howany也就是“哪一行只要有任何缺失值就整行删除”。结果只剩第一行print(df.dropna())name age city score 0 张三 25.0 北京 88.0如果想按列删把axis改成1print(df.dropna(axis1))name列没有缺失保留age、city、score各有一个或两个缺失整列删除。结果是只留一列这在实际工作中很少用但有一种场景例外——当某一列缺失率超过九成基本没信息量了用dropna(axis1)删除是合理的。2.3 howall与thresh的实战价值howall在真实数据里有个很典型的使用场景数据是从多个系统导出后拼接的某些行可能整行都是空的。比如底部多了几行空白或者某些表头重复出现导致空行。这时候用df_clean df.dropna(howall)只删除那些“全部为空”的行保留有部分有效数据的行这样不会误伤。thresh则适合“允许存在少量缺失”的场景。比如一份问卷有20个问题你对每条记录的完整性要求是“至少回答15题”那就df_valid df.dropna(thresh15)什么意思就是只要某行至少有15个非空值就保留哪怕有5个空值也无所谓。这在处理宽表数据列很多时特别实用比手动数每一列缺失情况高效得多。2.4 subset限定范围只针对关键列判断很多时候有些列缺失无所谓但关键列缺失不能忍。比如用户行为数据里“用户ID”如果缺失这行数据就完全无法定位到人没有分析价值但“备注”缺失就没关系。这时候用subset限定判断范围df_clean df.dropna(subset[user_id, order_id])这个操作的含义是只要user_id或order_id任一列为空就删除该行完全不关心其他列是否缺失。这比全表howany更精准也更符合业务逻辑——删除的标准不应该是“有没有缺失”而应该是“关键字段有没有缺失”。3. fillna()函数详解用什么值填、怎么填更合理3.1 基础语法与参数拆解fillna()的语法DataFrame.fillna(valueNone, methodNone, axisNone, inplaceFalse, limitNone)参数逻辑value填充用的值可以是标量、字典、Series或DataFramemethod填充方法ffill前向填充或bfill后向填充axis填充方向0按行方向填充默认1按列方向填充limit最多连续填充几个缺失值inplace是否原地修改有一点要特别提醒新手method参数在Pandas 2.0版本里引入了变更官方更推荐直接用df.ffill()和df.bfill()这两个独立方法methodffill的写法虽然还能用但会提示弃用警告。这篇文章里的示例我会两种写法都演示你看到ffill()就知道它等价于methodffill。3.2 用固定值填充最直接也最常用最常见的场景是“缺失即无”比如一个电商订单表里“优惠券金额”为空意思就是这笔订单没用优惠券那填充0是完全合理的df[coupon_amount] df[coupon_amount].fillna(0)还有场景是按字典对不同的列填不同的值fill_values { age: 0, city: 未知, score: df[score].mean() } df_filled df.fillna(valuefill_values)注意这里value传的是一个字典键是列名值是要填充的内容。这样做的好处是一次调用处理多列各自使用不同的填充策略不用写一堆循环。固定值填充的“经典坑”是分类变量。有些分类列缺失后直接填未知或other虽然不会报错但会在建模时引入一个绝大多数训练样本里不存在的类别。如果后续要做模型训练建议把填充逻辑放进一个transform流程里确保训练集和预测集使用相同方式处理。3.3 用统计量填充均值、中位数、众数的选择数值型变量缺失时最常见的做法是用统计量填充。选择哪个统计量是有讲究的均值mean数据呈近似正态分布、没有明显异常值时用中位数median数据有偏态分布或存在极端值时用比如收入数据少数高收入者会把均值拉得很高中位数更能代表一般水平众数mode适用于离散型分类变量比如“年龄段”“地区代码”这类示例df[age] df[age].fillna(df[age].median()) df[city] df[city].fillna(df[city].mode()[0])注意mode()返回的是一个Series即使只有一个众数取[0]才稳妥。这个细节很多人容易漏。用统计量填充有一个需要注意的理论问题它会降低变量的方差因为所有缺失值都被替换成了同一个值。如果缺失比例不高比如5%以下影响不大如果缺失比例很高比如30%以上填充后的分布会和真实分布有明显偏差。这种时候就要回到前面的判断框架考虑这个变量是否还有保留价值。3.4 前向填充与后向填充处理时间序列的好手时间序列数据里缺失值往往不是随机出现的而是由于采集故障导致某几个连续时间点没有记录。这种场景下用ffill()把上一个有效观测值往下推或者用bfill()把下一个有效观测值往上推会比填均值更合理因为时间序列有“惯性”相邻时刻的值通常更接近。# 前向填充用上一个非空值填充后面连续的空值 df[temperature] df[temperature].ffill() # 后向填充用下一个非空值填充前面的空值 df[temperature] df[temperature].bfill()看这个例子s pd.Series([1, None, None, 4, None]) print(s.ffill())输出0 1.0 1 1.0 2 1.0 3 4.0 4 4.0limit参数控制连续填充的最大数量防止“跨越太远的大窟窿被错误填充”。比如传感器断线5分钟每分钟一条数据断线期间本来不该有数据结果你却用断线前的值连填5个点制造出“数据正常”的假象——这在后续分析里会带来严重误导。# 只连续填充前2个缺失值剩余保留NaN s_limited s.ffill(limit2)3.5 用插值法填充比统计量更“聪明”一点虽然标题只提了dropna和fillna但用fillna配合interpolate()其实是进阶玩家常用的组合打法。interpolate()是Pandas里的插值方法它和fillna的区别在于fillna是用一个固定的值去替换缺失值而interpolate()会利用缺失值前后已知点的信息计算一个“顾前顾后”的过渡值。对线性变化规律比较明显的序列比如温度、股价、人口增长插值法的效果通常好于填中位数。s_interp s.interpolate() print(s_interp)输出0 1.000000 1 2.000000 2 3.000000 3 4.000000 4 NaN可以看到第1、2个缺失值被自动填充为2.0和3.0正好落在1和4的连线上。第4个值仍然是NaN因为它是末端值没有后续数据可以参考interpolate()默认无法外推。4. 实战案例订单数据清洗过程中的完整处理方案4.1 构造一份贴近业务的示例数据理论讲再多不如跑一遍完整流程。我构造一份模拟的某电商平台订单数据包含以下字段order_id订单号user_id用户IDorder_amount订单金额coupon_amount优惠券金额order_date下单日期payment_method支付方式import pandas as pd import numpy as np df_orders pd.DataFrame({ order_id: [A001, A002, A003, A004, A005, A006, A007, A008], user_id: [U01, U02, None, U04, U05, U06, U07, None], order_amount: [120.5, 89.0, 250.0, None, 66.5, 310.0, None, 99.0], coupon_amount: [10.0, None, 20.0, 0.0, None, None, 5.0, 0.0], order_date: [2024-01-01, 2024-01-01, 2024-01-02, 2024-01-02, 2024-01-03, 2024-01-03, 2024-01-04, 2024-01-04], payment_method: [alipay, wechat, alipay, card, None, wechat, alipay, None] })先用前面的探查命令看一眼缺失值分布print(df_orders.isnull().sum())输出order_id 0 user_id 2 order_amount 2 coupon_amount 3 order_date 0 payment_method 2 dtype: int648行数据6列字段缺失情况一目了然。4.2 分字段制定填充策略现实的业务数据处理很少会“一刀切”地对整个表执行同一个填充逻辑。更专业的做法是分字段评估缺失值的业务含义逐字段决定策略。我对这份订单数据的处理计划如下user_id缺失无法定位用户这类订单不能用于用户维度分析。但订单本身是真实存在的如果直接删行会损失交易记录。我的决定是保留行但单独标记出来。order_amount缺失订单金额是核心指标缺失会对GMV统计造成直接影响。这种字段不建议随便填要结合业务判断。这里我选择用订单金额的中位数填充并打个标记方便后续对比分析。coupon_amount缺失从业务逻辑上理解优惠券金额为空代表这笔订单没有使用优惠券填0。payment_method缺失支付方式是分类变量缺失不能填0填unknown作为单独的类别。4.3 完整代码实现# 先复制一份避免修改原始数据 df_clean df_orders.copy() # 1. user_id缺失保留但单独标记 df_clean[user_id_missing] df_clean[user_id].isnull().astype(int) df_clean[user_id] df_clean[user_id].fillna(unknown_user) # 2. order_amount缺失用中位数填充 amount_median df_clean[order_amount].median() df_clean[order_amount] df_clean[order_amount].fillna(amount_median) df_clean[order_amount_missing] df_clean[order_amount].isnull().astype(int) # 这行需要在填充前标记等一下上面代码里最后两行有顺序问题我重新写一遍把标记逻辑挪到填充之前df_clean df_orders.copy() # 填充前先记录缺失标记推荐做法 df_clean[user_id_missing] df_clean[user_id].isnull().astype(int) df_clean[order_amount_missing] df_clean[order_amount].isnull().astype(int) df_clean[coupon_amount_missing] df_clean[coupon_amount].isnull().astype(int) df_clean[payment_method_missing] df_clean[payment_method].isnull().astype(int) # 分字段填充 df_clean[user_id] df_clean[user_id].fillna(unknown_user) df_clean[order_amount] df_clean[order_amount].fillna(df_clean[order_amount].median()) df_clean[coupon_amount] df_clean[coupon_amount].fillna(0) df_clean[payment_method] df_clean[payment_method].fillna(unknown) print(df_clean)输出效果order_id user_id order_amount coupon_amount order_date payment_method user_id_missing order_amount_missing coupon_amount_missing payment_method_missing 0 A001 U01 120.5 10.0 2024-01-01 alipay 0 0 0 0 1 A002 U02 89.0 0.0 2024-01-01 wechat 0 0 1 0 2 A003 unknown_user 250.0 20.0 2024-01-02 alipay 1 0 0 0 3 A004 U04 89.0 0.0 2024-01-02 card 0 1 0 0 4 A005 U05 66.5 0.0 2024-01-03 unknown 0 0 1 1 5 A006 U06 310.0 0.0 2024-01-03 wechat 0 0 1 0 6 A007 U07 89.0 5.0 2024-01-04 alipay 0 1 0 0 7 A008 unknown_user 99.0 0.0 2024-01-04 unknown 1 0 0 14.4 为什么额外增加缺失标记列这里体现了一个从实战中沉淀的细节如果后续要用填充后的数据做机器学习建模直接告诉模型“这个字段原本是缺失的”往往比用中位数填充更有信息量。模型可以从缺失标记列学到“缺失本身也是一种模式”——比如payment_method缺失的用户可能有更高的退款率。所以我的习惯是任何重要字段填充之前先用isnull().astype(int)生成一列0/1标记。这样原始信息不丢填充后的数据也方便后续回溯。4.5 验证处理结果处理完别急着走做个快速校验print(df_clean.isnull().sum().sum()) # 应该是0验证通过后这份数据就可以放心往下游走了。如果填充前忘了做标记也不要慌可以对比原始数据和清洗后数据找出哪些位置发生过填充。5. 常见问题与排查技巧实录5.1 inplaceTrue到底用不用inplace这个参数真是一言难尽。很多初学者喜欢用df.dropna(inplaceTrue)因为看起来代码更短一步到位。但从Pandas官方近几个版本的动向来看inplace参数在未来会被逐步弃用原因是它在链式操作里容易制造隐蔽的bug。举个例子df pd.read_csv(data.csv) df df[df[age] 18] df.dropna(inplaceTrue)这段代码在不同的调用链上执行时inplaceTrue可能作用于视图view而不是原对象导致数据没改到后面计算时又是旧数据排查起来极其痛苦。我的建议很简单统一用df df.dropna()这种重新赋值的方式代码可读性高也不依赖于Pandas内部的视图/副本机制踩坑概率小一半。5.2 dropna后索引乱了怎么办dropna()删除行后索引不会自动重排会出现1、3、7这种“断号”情况。很多人在后续groupby或merge时遇到莫名其妙的索引对齐问题其实就是这个原因。解决办法df_clean df.dropna().reset_index(dropTrue)reset_index(dropTrue)会重新生成从0开始的连续索引dropTrue表示丢弃原来的索引列。这是处理缺失值后必须养成的习惯。5.3 为什么fillna填不进去这个问题我见过太多次。常见原因有**原因一列类型不是数值型。**比如用df[score].fillna(df[score].mean())但score列在读取时被识别成了object类型通常是因为列里有空字符串或特殊字符。mean()计算出来的浮点数填进object类型的列虽然不会报错但类型不对后面做计算全是坑。排查方式print(df[score].dtype)如果是object先做类型转换df[score] pd.to_numeric(df[score], errorscoerce)errorscoerce的意思是遇到无法转换的内容就置为NaN这一步本身就顺带把异常值也暴露出来了。**原因二填充值本身是NaN。**比如df[col].fillna(df[col].mode())忘记取[0]拿到的Series里有NaN填充后原位置还是NaN看起来就像没填进去。原因三你用的是df.fillna()但没赋值。fillna()默认返回新对象不修改原DataFrame。写了df.fillna(0)却没接住返回值等于白干。5.4 机械地填充时间序列会引入“幽灵数据”时间序列里如果传感器断线时间很长用ffill(limit10)连续填充10个点等于人为制造了10个“看起来正常”的数据点。这些假数据在某些场景会比缺失更危险——缺着至少你知道缺填错了你根本不知道。我的经验是对连续缺失超过一定数量的数据段优先考虑从分析中排除或者只在统计描述中使用不做模型训练数据。这个阈值可以根据业务情况定比如连续缺失超过5个时间点就标记为不可信。5.5 按组填充中位数才是更精细的做法很多人学会了df[col].fillna(df[col].median())但忽略了不同分组的差异。比如分析不同城市的房价北京的中位数和三四线城市的中位数截然不同直接用全量中位数填充“城市均价”列会严重扭曲小城市的数据。更好的做法是按组填充df[price_filled] df.groupby(city)[price].transform(lambda x: x.fillna(x.median()))groupby先按城市分组transform对每个组内的缺失值用该组的中位数填充。这个写法比全局填充精细得多也是实际业务里更常用的方案。同理平均值、众数、ffill都可以配合groupby在组内操作。5.6 填充后别忘了检查分布变化填充完数据建议做一个快速可视化对比填充前后的数值分布有没有发生明显变化。如果填充比例很高中位数填充会把大量数据集中到一个点上分布会出现一个“尖峰”这对后续建模有影响。简单检查df[order_amount].hist(bins50) df_clean[order_amount].hist(bins50)两张图一对比心里就有数了。如果差异太大说明这个字段缺失比例过高填充策略需要重新考虑——可能该字段就不适合继续做特征。6. 一些想说但文章里不太好归类的心得做数据清洗这么多年最大的体会是缺失值处理没有银弹所有方法都是取舍。dropna()简单直接但会丢数据fillna(0)省事但可能扭曲业务含义均值填充很常用但会低估方差。真正专业的做法是每处理一个字段都能说清楚“为什么这么填”而不是复制网上的代码就完事。具体到实操层面我想分享一个小技巧如果你处理的是一次性的分析任务直接在脚本里写清楚每一步的填充逻辑就行但如果你在写一个会重复运行的ETL流程建议把填充规则统一封装成一个data_clean函数用配置文件维护每一列的填充策略这样以后数据更新了只需要改配置不用改代码。def clean_orders(df): df df.copy() df[user_id] df[user_id].fillna(unknown_user) df[order_amount] df[order_amount].fillna(df[order_amount].median()) df[coupon_amount] df[coupon_amount].fillna(0) df[payment_method] df[payment_method].fillna(unknown) return df真实项目里还有一类情况很容易被忽视缺失值的“缺失机制”本身可能反映了系统bug。比如某个时间段内所有payment_method都为空那大概率不是用户没选支付方式而是埋点采集出问题了。这种情况下单纯用fillna(unknown)就是把系统bug当成正常业务处理掩盖了真正的问题。我现在的习惯是清洗数据时如果发现某个字段的缺失值呈“批量出现”的特征会先写邮件或提工单找对应的开发确认而不是直接填一个值匆匆往下走。回到dropna()和fillna()这两个函数本身它们只是工具真正值钱的是使用工具的判断力。什么时候删、什么时候填、填什么值这些决策背后是业务理解、统计学常识和处理经验的结合。把这篇文章里的示例代码动手跑一遍再把每个参数都改改试试比看十篇文章都管用。
返回列表