ARTICLE DETAIL

资讯详情

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

Pandas缺失值处理完全指南:dropna与fillna实战详解

Pandas缺失值处理完全指南:dropna与fillna实战详解 1. 为什么缺失值处理是数据分析的第一个分水岭不管是处理爬虫抓来的原始数据、业务导出的Excel报表还是接数仓里其他人跑出来的表几乎没有人能避开那一行行刺眼的NaN、None或者空白。我见过不少初学者拿到数据后第一件事就是data.dropna()一把梭删完了直接开算结果后面特征工程做到一半发现行数对不上、逻辑乱套、结果怎么都不合理。也有朋友在群里问“填充空值到底用0还是用均值”底下的回答五花八门但很少有人能把背后的逻辑讲清楚。先说个结论空值处理不是简单的“删”或“填”二选一它本质上是你在替数据做一次价值判断。删除意味着“这个样本失真严重不值得保留”填充意味着“这个字段的信息还有修复价值我要用某种策略把它补上”。这个判断本身没有绝对的对错但它会直接影响你后面所有分析结果的可信度。Python里处理空值最核心的两个函数就是dropna()和fillna()都出自pandas库。换句话说你得先有pandas环境然后对DataFrame或Series操作。本文的目标就是通过大量真实示例把这两个函数的每个参数、每种常见玩法、每类容易踩的坑都讲透让初学者能照着写让用过的人能查漏补缺。为了照顾零基础的读者我先快速说两个基础点。第一pandas里常见的空值表示法有numpy.nan即NaN、None还有新版本里的pd.NA。它们在显示上可能长得很像但类型上各有区别判断空值时统一用pd.isna()或isnull()最保险。第二判断一个DataFrame里有没有空值、有多少空值常用的三板斧是df.isnull().sum()、df.info()和df.isnull().mean()分别用来统计空值总数、看每列非空计数、看每列空值占比。这三步做完你才真正知道手里的数据缺到了什么程度也才能决定后面用dropna()还是fillna()。2. 到底该删还是该填先看数据状况再做决定2.1 缺失比例是决策的第一指标我个人的习惯是拿到数据先算两个比例单列缺失比例和整行缺失比例。单列缺失比例很好理解就是某一列有多少比例是空的整行缺失比例则要结合业务去看比如一个用户的某条行为记录里如果三四个关键字段同时为空那这一行大概率是采集异常删掉反而干净。判断删除还是填充我一般参考这几种情况该列空值占比超过70%我几乎不会用填充去补因为大部分填充策略均值、中位数、众数在这种极端稀疏的数据上没有意义。除非这列是待预测标签否则直接删列。空值占比在30%到70%之间这个区间比较尴尬删除会损失大量样本盲目填充又可能引入噪声。我会看字段和业务目标的关系如果是核心特征就想办法用模型或规则补如果是边缘字段倾向于删除。空值占比低于10%大部分场景下可以直接填充用均值/中位数/众数/前后值都行对整体分布影响很小。2.2 数据缺失机制对选择的影响这里有个非常关键但经常被忽略的概念叫缺失机制。统计上大致分三类完全随机缺失MCAR、随机缺失MAR、非随机缺失MNAR。听起来像理论课但我用大白话翻译一下你就懂了。完全随机缺失的意思是说某一行缺不缺失跟任何东西都没关系纯属偶然比如用户填问卷时手滑漏了一道题。这种情况下删掉也不损失什么信息填充任何一种常见策略也不会引入系统性偏差。随机缺失的意思是某列缺失的概率跟数据里其他已观测到的字段有关比如高收入人群更不愿意透露收入水平而“职业”这一列往往是有记录的。这种情况删行会有偏差填充时则可以考虑用分组统计量——比如按职业分组后的收入中位数去补效果远好于全局中位数。非随机缺失则意味着缺失本身就和数值大小有关比如极小或极大的数值更容易缺失。这种最麻烦普通的填充和删除都可能带偏结论通常需要引入业务判断或更高级的方法。别急着背术语你只需要记住被迫删除之前先想想这行数据为什么缺失如果缺失和你关心的业务结论可能有关系删除会放大偏差。这比背一堆公式更有用。3. dropna()函数参数逐个拆解示例说话3.1 dropna()的默认行为与axis参数dropna()最基础的用法是DataFrame.dropna()什么都不传。默认情况下pandas会删掉所有包含至少一个空值的行方向是沿着axis0即行方向。import pandas as pd import numpy as np df pd.DataFrame({ 姓名: [张三, 李四, 王五, 赵六], 年龄: [25, np.nan, 30, np.nan], 城市: [北京, 上海, np.nan, 广州] }) print(df) print(---- 默认 dropna() ----) print(df.dropna())运行结果姓名 年龄 城市 0 张三 25.0 北京 1 李四 NaN 上海 2 王五 30.0 NaN 3 赵六 NaN 广州 ---- 默认 dropna() ---- 姓名 年龄 城市 0 张三 25.0 北京因为李四、王五、赵六三行各自都有至少一个空值默认操作把它们全删了只剩张三一个人。这个例子很极端但能清楚说明默认行为。实际用的时候这种“只要有一列缺就整行删”的冷酷方式往往会把样本量砍掉一截所以要谨慎。axis参数控制删除方向axis0表示删除行axis1表示删除列。很多人容易记混我的记忆方法很粗暴axis0是沿着行的方向往下走所以删的是行axis1是沿着列的方向往右走所以删的是列。print(---- axis1删除含空值的列 ----) print(df.dropna(axis1))运行结果姓名 0 张三 1 李四 2 王五 3 赵六这次只剩“姓名”列了因为“年龄”和“城市”都有空值。老实说按列删除在真实项目中比想象中常用尤其是批量导入的多个特征列里若有几列几乎全空删掉比留着做填充更干净。3.2 howany还是howall决定删除的严格程度how参数控制这个删除动作到底有多严格可取值是any和all默认any。any意思是一行/列里只要有任意一个空值就删all意思是一行/列里所有的值都是空值才删。df2 pd.DataFrame({ A: [1, np.nan, 3, np.nan], B: [4, np.nan, 6, np.nan], C: [7, 8, 9, np.nan] }) print(---- howall整行全空才删除 ----) print(df2.dropna(howall))运行结果A B C 0 1.0 4.0 7.0 1 NaN NaN 8.0 2 3.0 6.0 9.0第3行索引3A、B、C全是NaN只有它符合howall的条件于是被删除。索引1那种A、B都是NaN但C有值的情况不会被删。这个参数在清洗“整条记录都是空”的脏数据时非常实用比如从Excel导入时末尾多出来的空行直接dropna(howall)就能清除。howall配合axis1还有另一个用途删掉整列全空的空列。这个在读取某些结构混乱的表格时特别好用比如表头后面跟了几列合并单元格导致的空列直接在读完之后清洗掉。3.3 thresh参数用“至少保留多少个非空值”来控制删除thresh是很多教程一笔带过但我认为是dropna()里最妙的一个参数。它的含义是这一行/列至少要保有多少个非空值如果达不到这个阈值就删掉。它和how的区别在于它是一种带容忍度的删除策略你允许某些样本缺几个字段但不允许缺得太离谱。df3 pd.DataFrame({ 学号: [001, 002, 003, 004], 语文: [90, np.nan, 85, np.nan], 数学: [88, 92, np.nan, np.nan], 英语: [95, 89, 91, np.nan] }) print(---- thresh3至少保留3个非空值。索引0保留索引1/2/3删除 ----) print(df3.dropna(thresh3))运行结果学号 语文 数学 英语 0 001 90.0 88.0 95.0 1 002 NaN 92.0 89.0 2 003 85.0 NaN 91.0 3 004 NaN NaN NaN ---- 处理后 ---- 学号 语文 数学 英语 0 001 90.0 88.0 95.0 1 002 NaN 92.0 89.0 2 003 85.0 NaN 91.0学号语文数学英语一共4列thresh3要求一行至少3个非空值。索引1缺语文但还有学号、数学、英语凑够3个留下了索引2同理索引3只剩一个学号这行数据对分析来说基本没用删除。这种用法在真实项目里非常常见——处理用户行为日志时有些行只有设备ID其他行为指标全空留着只会拉低统计质量。我踩过的坑是thresh算的是非空值个数不是空值个数。刚接触时我总想“允许每行缺2个”直接把thresh设为列数减2但其实你该设成“最少保留的字段数”想清楚再写。3.4 subset参数只在指定的列范围内判断空值subset接受一个列名列表意思是我只关心这些列里有没有空值只有在指定列中出现空值时才删除该行。它解决的是一个非常典型的业务问题整行数据都完整但某几个关键字段缺失其他次要字段缺失无所谓。df4 pd.DataFrame({ 订单号: [A001, A002, A003, A004], 金额: [100, 200, np.nan, 400], 备注: [正常, np.nan, 已催单, np.nan] }) print(---- subset[金额]只看金额列是否为空 ----) print(df4.dropna(subset[金额]))运行结果订单号 金额 备注 0 A001 100.0 正常 1 A002 200.0 NaN 3 A004 400.0 NaN索引2因为金额是NaN被删掉了索引1的备注虽然是NaN但金额存在所以保留下来。在业务里这就是“备注可填可不填但有订单金额记录才是一笔有效订单”的逻辑。这种删除策略非常精准不会误伤其他字段缺失的行。subset也可以传多个列名比如dropna(subset[金额, 客户ID], howany)表示只要金额或客户ID任一为空就删因为这两列是订单的核心字段。可以说subset让你从“全行缺陷”的判断升级为“关键字段缺陷”的判断更贴合数据业务。3.5 inplace参数为什么我不推荐你用它inplaceTrue会直接修改原DataFrame不返回新对象。听起来很方便但实际上有三大问题。第一它会破坏链式操作的流畅性。数据分析过程中你往往要连续做多个处理dropna().reset_index()这种链式写法很舒服但你在中间加一个dropna(inplaceTrue)就断掉了链子。第二它让排查问题变困难。你处理完数据后过了几小时想确认原数据到底有没有被动过如果没有保留原始副本可能已经忘记了当时的改动过程。第三pandas官方自己也越来越不推荐这个参数未来的版本有废弃计划。我的建议是始终使用df_new df.dropna(...)这种返回新对象的方式。如果担心内存问题直接在读取数据后df df.dropna()重新赋值就行效果一样但代码更清晰。4. fillna()函数从简单填充到分组填充层层递进4.1 最基础的固定值填充value参数如果说dropna()是“减法”那fillna()就是“加法”。它最基础的用法就是传一个固定的值进去把所有空值替换成这个值。传标量时对整张表生效传字典可以对不同列指定不同的填充值。df5 pd.DataFrame({ 售价: [100, np.nan, 300, np.nan], 折扣: [0.8, 0.9, np.nan, 0.85] }) print(---- 全表用0填充 ----) print(df5.fillna(0)) print(---- 用字典分别指定各列填充值 ----) print(df5.fillna({售价: -1, 折扣: 1.0}))运行结果---- 全表用0填充 ---- 售价 折扣 0 100.0 0.80 1 0.0 0.90 2 300.0 1.00 3 0.0 0.85 ---- 用字典分别指定各列填充值 ---- 售价 折扣 0 100.0 0.80 1 -1.0 0.90 2 300.0 1.00 3 -1.0 0.85固定值填充适用于哪些场景呢我举几个实际例子。数值型字段如果字段本身表示计数或金额空值可能意味着“没有发生”填充为0是合理的。类别型字段可以填充未知或other保留“存在但不知道”这个信息。标志位字段填充0或False即可。但请注意用0填充均值类连续特征可不是一个无脑操作如果这个字段之后要进入计算0值会让均值被拉低后面还要配合特征工程调整。4.2 用统计量填充均值、中位数、众数的选择逻辑用固定值太粗糙很多人会想到用统计量填充。常用的三个统计量是均值mean、中位数median、众数mode。选择哪个不是靠抛硬币而是看数据的分布特征。均值适合数据接近正态分布或对称分布、没有极端异常值的情况。比如身高、考试成绩这类分布相对正常的字段用均值填充对整体分布影响较小。中位数适合数据偏态严重或存在明显异常值的情况。比如收入、房价、订单金额这种长尾分布的数据中位数比均值稳健得多——几个高净值用户能瞬间把均值拉上天中位数却不为所动。众数适合类别型字段比如城市、职业、支付方式。众数就是出现次数最多的值用它填充等价于“用最大概率猜测缺失的类别”。df6 pd.DataFrame({ 用户等级: [普通, VIP, 普通, np.nan, 普通, VIP, np.nan], 月消费: [200, 800, np.nan, 3500, 120, 650, 5000] }) level_mode df6[用户等级].mode()[0] spend_median df6[月消费].median() print(用户等级众数:, level_mode) print(月消费中位数:, spend_median) df6[用户等级] df6[用户等级].fillna(level_mode) df6[月消费] df6[月消费].fillna(spend_median) print(df6)运行结果用户等级众数: 普通 月消费中位数: 650.0 用户等级 月消费 0 普通 200.0 1 VIP 800.0 2 普通 650.0 3 普通 3500.0 4 普通 120.0 5 VIP 650.0 6 普通 5000.0这里“月消费”的分布里有3500和5000这种明显偏高的值均值会被拉得很高事实上你可以算一下(20080035001206505000) / 6 1711.67而中位数只有650。如果这个字段用来给用户画像用均值填充会把普通用户误判成高消费人群用中位数则稳妥得多。一个小细节mode()返回的是Series因为数据里可能出现多个并列众数。取第一个用[0]如果你用的是新版本pandas也可以用.iloc[0]。别直接写df6[用户等级].mode()塞进fillna()里类型对不上会报错。4.3 method参数前后向填充的适用场景method参数是fillna()里的一个特殊把手取值ffillforward fill前向填充和bfillbackward fill后向填充默认是None。前向填充就是拿缺失值上面的值去补后向填充则是拿下面的值去补。df7 pd.DataFrame({ 时间: [09:00, 09:10, 09:20, 09:30, 09:40], 温度: [23.5, np.nan, np.nan, 25.1, np.nan] }) print(---- 前向填充 ----) print(df7.fillna(methodffill)) print(---- 后向填充 ----) print(df7.fillna(methodbfill))运行结果---- 前向填充 ---- 时间 温度 0 09:00 23.5 1 09:10 23.5 2 09:20 23.5 3 09:30 25.1 4 09:40 25.1 ---- 后向填充 ---- 时间 温度 0 09:00 23.5 1 09:10 25.1 2 09:20 25.1 3 09:30 25.1 4 09:40 NaN前向填充最典型的场景是时间序列数据。比如传感器每隔10分钟上报一次温度中间有几条记录在上传时丢包了那用前一条记录的值补上比用全表均值合理得多因为温度在短时间内是连续变化的。后向填充也有用武之地比如某个指标的缺失是因为设备晚了几拍才上报用后续的值来回溯补充也有一定逻辑。需要特别注意的是新版本pandas已经把method参数标记为“即将废弃”现在的推荐写法是直接用fillna(methodffill)过渡期还能用但新版里建议使用df.ffill()和df.bfill()这两个独立方法。我自己写代码时已经全面转用ffill()和bfill()了语义更清晰也免得到时候升级库时代码报错。limit参数可以限制填充的连续空值个数比如df.fillna(methodffill, limit1)表示最多只向后补1个空值超过的保持NaN。这个参数在传感器连续采集场景里特别实用——设备断电半小时的连续缺失如果一路用断电前的值硬撑反而会掩盖故障限制填充个数能保留“数据其实不可信”的信号。4.4 分组填充用同类数据的信息去补缺失值分组填充是我认为fillna()使用中价值最高、最容易被忽视的一种玩法。它的思路是全局均值未必能代表每个子群的特征但按某些条件分组后再填充每个组的统计量才更贴近组内真实水平。举个最常见的例子假设我们有一份多个城市的房价数据“价格”列有一些空值。如果直接用全局均价填充北京的空值会被同化成县级市的水平如果按“城市”分组分别计算每个城市的均价然后用对应城市的均价填充信息替换的精度就完全不一样了。df8 pd.DataFrame({ 城市: [北京, 北京, 上海, 上海, 广州, 广州], 房价: [80000, np.nan, 65000, np.nan, 45000, np.nan] }) city_avg df8.groupby(城市)[房价].transform(mean) print(分组均值) print(city_avg) df8[房价] df8[房价].fillna(city_avg) print(---- 分组填充后 ----) print(df8)运行结果分组均值 0 80000.0 1 80000.0 2 65000.0 3 65000.0 4 45000.0 5 45000.0 Name: 房价, dtype: float64 ---- 分组填充后 ---- 城市 房价 0 北京 80000.0 1 北京 80000.0 2 上海 65000.0 3 上海 65000.0 4 广州 45000.0 5 广州 45000.0关键代码是groupby(城市)[房价].transform(mean)。transform会把每组算出来的均值映射回原始的每一行这样得到的Series长度和原DataFrame一样可以直接传给fillna()。如果你用groupby(城市)[房价].mean()得到的是每个城市的均值列表长度不对没法直接填充。更进阶一点分组填充可以和多个条件配合按“城市户型”分组、按“用户等级会员类型”分组分组粒度越细填充值的针对性越强。但也要警惕分组过细导致某些组只有一两行数据统计量太不稳定。我踩过的坑是某组所有房价都是NaNtransform(mean)算出NaN填充进去依然是NaN等于白忙。遇到这种情况要提前检查各组非空数量或者用min_count这类参数兜底。4.5 其他填充方式插值法、apply自定义填充有些场景连分组填充都还不够灵活比如时间序列中温度变化的曲线是平滑的用正上方的值硬填会导致数据出现台阶感。这时可以用interpolate()做线性插值它是pandas里比fillna()更精细的补充手段本质上是利用前后已知点拟合一条直线然后估算中间缺失点的值。s pd.Series([1.0, np.nan, np.nan, 4.0, np.nan, 6.0]) print(---- 线性插值 ----) print(s.interpolate())运行结果0 1.0 1 2.0 2 3.0 3 4.0 4 5.0 5 6.0 dtype: float641和4中间的两个空值依次填充成2、34和6中间的填成5非常自然。interpolate()的method参数还有很多选项比如polynomial可以做多项式插值time适合时间索引数据但初学者先把线性插值用好就足够了。如果所有内置方法都无法满足需求你还可以配合apply或自定义函数做更复杂的填充逻辑。比如用df[列].apply(lambda x: 业务规则填充(x))只要函数能接收一个值、输出一个替代值就行。但注意fillna本身不接受函数作为参数所以自定义逻辑要么先算出完整的填充值Series再传给fillna要么用apply逐元素处理。前者更符合pandas的向量化风格性能和代码可读性都更好。5. 实战场景从“两张表”看空值处理的完整流程5.1 场景设计一份不完美的订单数据为了把前面的知识串起来我模拟一份电商订单数据里面包含了多种常见的缺失情况整行全空的脏数据、部分字段缺失、时间序列缺失、类别字段缺失。df_order pd.DataFrame({ 订单号: [D001, D002, D003, D004, D005, D006, D007], 用户ID: [U1, U2, np.nan, U4, U5, U6, np.nan], 订单金额: [120, np.nan, 350, 80, np.nan, 999, 200], 支付方式: [微信, 支付宝, 微信, np.nan, 微信, 银联, 支付宝], 下单时间: pd.to_datetime([2024-01-01 10:00, 2024-01-01 10:05, 2024-01-01 10:10, 2024-01-01 10:15, 2024-01-01 10:20, np.nan, 2024-01-01 10:30]) }) print(df_order)运行结果订单号 用户ID 订单金额 支付方式 下单时间 0 D001 U1 120.0 微信 2024-01-01 10:00:00 1 D002 U2 NaN 支付宝 2024-01-01 10:05:00 2 D003 NaN 350.0 微信 2024-01-01 10:10:00 3 D004 U4 80.0 NaN 2024-01-01 10:15:00 4 D005 U5 NaN 微信 2024-01-01 10:20:00 5 D006 U6 999.0 银联 NaT 6 D007 NaN 200.0 支付宝 2024-01-01 10:30:005.2 清洗流程的分步决策第一步先统计空值分布print(df_order.isnull().sum())运行结果订单号 0 用户ID 2 订单金额 2 支付方式 1 下单时间 1 dtype: int64第二步逐列做决策。订单号无空值作为主键字段明确保留。用户ID有2个空值这些订单没有绑定用户后续做用户画像时无法归因考虑直接删除这两行。当然如果业务上允许匿名下单填充匿名用户也行但这里我按“需用户维度分析”场景判断删除。订单金额有2个空值这是核心指标。一个办法是用非空订单金额的中位数填充另一个办法是删除。这里我倾向填充因为删除会让这部分订单完全无法参与GMV分析。用中位数而非均值因为999这个明显偏高均值会被拉大。支付方式有1个空值类别字段用众数填充。下单时间有1个空值时间序列上下文中有相邻订单的下单时间可以用前后时间做插值填充或前向/后向填充。这里NaT是时间类型的空值标记fillna同样能处理。第三步按顺序执行# 先删除用户ID为空的订单因为它们是无效样本 df_order_clean df_order.dropna(subset[用户ID]).copy() # 金额用中位数填充 amount_median df_order_clean[订单金额].median() # 支付方式用众数填充 pay_mode df_order_clean[支付方式].mode()[0] df_order_clean[订单金额] df_order_clean[订单金额].fillna(amount_median) df_order_clean[支付方式] df_order_clean[支付方式].fillna(pay_mode) # 下单时间用前向填充尽量贴近相邻时间 df_order_clean[下单时间] df_order_clean[下单时间].fillna(methodffill) print(df_order_clean)运行结果订单号 用户ID 订单金额 支付方式 下单时间 0 D001 U1 120.0 微信 2024-01-01 10:00:00 1 D002 U2 200.0 支付宝 2024-01-01 10:05:00 3 D004 U4 80.0 微信 2024-01-01 10:15:00 4 D005 U5 200.0 微信 2024-01-01 10:20:00 5 D006 U6 999.0 银联 2024-01-01 10:20:00看一下这个过程中有意思的细节金额中位数算出来是200这个值同时填充了D002和D005这两笔缺失订单。支付方式的众数是“微信”因为非空值里微信出现了3次支付宝2次银联1次于是D004被填成“微信”。下单时间用的是前向填充D006的缺失时间被填成了上一行D005的时间10:20严格说它不是真实的支付时间但作为分析近似值还是可以接受的。这里有个非常关键的教训我特别加了.copy()目的是防止后续操作出现SettingWithCopyWarning。如果你写df_order.dropna(subset[用户ID])然后直接在上面改动某些pandas版本可能警告你“正在试图修改一个来自切片的数据副本”运行结果偶尔会出乎意料。养成赋值时加copy()的习惯能省掉很多莫名其妙的调试时间。5.3 顺序不同结果不同空值处理的顺序也会影响最终结果。比如先填充下单时间再用用户ID做去空操作和先删再填最终数据行的效果通常一致但某些填充统计量会变。还是金额的例子如果你先删除用户ID为空的D003和D007再算金额中位数[120, 350, 80, 999, 200]的中位数是200但如果你不对用户ID做任何删除直接算所有订单金额的中位数[(120, 350, 80, 999, 200)]加两个NaNpandas默认会跳过NaN中位数还是200这次恰好一样。但换一组数据就不一定了比如空值集中在大额订单上删除行后计算统计量和删除前计算统计量会有本质差别。实际操作上我个人的顺序习惯是先做行级删除按关键字段去空再做列级删除按全空/高缺失比例最后做填充。因为填充策略里的统计量基于的是“即将进入分析的数据集”如果某些行注定被删那它们包含的字段值就不该参与统计量的计算。反过来如果你先填充再删除填充计算时混入了脏行的其他字段信息污染会传导到统计量里。6. 那些容易踩的坑检查NAN的方式、链式索引和内存隐患6.1 判断空值别用“ np.nan”很多初学者会写出df[df[列名] np.nan]这样的代码然后发现筛选结果为空。原因很简单NaN在Python中的比较行为很特殊它不等于任何值包括它自己。就像你去问一个失忆者“你是张三吗”他回答不是问“你是李四吗”也不是问“你是你自己吗”它还是说不是因为NaN本身就是一个“未知”的标记。正确判断空值必须用pd.isna()或isnull()print(df5[df5[售价].isna()])这条代码才能准确筛出“售价”为空的行。类似的还有df[列].notna()等方法。不要小看这个区别我见过有人排查了半天为什么空值筛不出来最后发现就是错用了等号。6.2 处理完空值之后一定要重置索引dropna()删除行后DataFrame的索引会出现“空洞”索引序列变成0、1、3、5这种跳跃形式。很多人后续做循环遍历或按索引切片时会莫名其妙地报KeyError排查方向却弄了半天。解决办法非常简单df_clean df.dropna().reset_index(dropTrue)reset_index(dropTrue)的作用是让索引从0开始重新连续编号并且不保留原来的索引列。如果忘了写dropTruepandas会把旧索引作为一列新数据加进来这在某些场景下同样令人困惑。我现在已经形成了肌肉记忆凡是删过行的DataFrame下一步十有八九是reset_index(dropTrue)。6.3 大DataFrame的内存问题复制是必要代价有人听我说copy()第一反应是浪费内存。这个担忧可以理解。但实际上pandas的copy()在多数场景是浅拷贝底层数据共享并不会把几千万行数据完全复制一份内存开销通常可控。反而是不写copy()时那些在切片视图上做的隐形赋值逻辑才会在数据处理链路过长时给你埋雷。另一个内存相关的建议是如果数据量巨大先做dropna这类“删”的操作再做fillna这类“填”的操作。删除会尽早缩小DataFrame体积后续填充花费的时间更少。反过来先把所有空值都填了再删除等于白算了一大批填充值。6.4 处理“空字符串”和“真正的空值”的区别原始数据里经常有看起来像空值的空白字符串比如Excel导入后Excel空单元格有时被pandas读成NaN但某些系统导出时会把空单元格写成、 甚至未知。isna()只会识别NaN、None、NaT不会把空白字符串判断为空值。遇到这种情况我一般先用df.replace(, np.nan, inplaceFalse)把空字符串转成真正的NaN然后再用dropna()或fillna()统一处理。如果数据里存在多种形式的伪空值可以串多个replace或者用一个更暴力的方法df df.apply(lambda x: x.str.strip() if x.dtype object else x)把字符串首尾空格去掉再做替换。办法是按数据情况灵活组合关键是意识到“看着空不等于真的空”。7. 几个直接可以拿去用的封装函数7.1 一个简易的缺失值报告函数每次处理数据前我都要看缺什么、缺多少于是我写了一个非常简短的报告函数几行代码就能输出每列的缺失情况。把这套逻辑固化成工具每次拿到新表直接调用可以省去大量重复劳动。def missing_report(df): 返回DataFrame中每列的缺失值数量与比例 missing_count df.isnull().sum() missing_ratio missing_count / len(df) report pd.DataFrame({ 缺失数量: missing_count, 缺失比例: missing_ratio.round(4) }) return report[report[缺失数量] 0].sort_values(缺失比例, ascendingFalse) # 用法示例 print(missing_report(df_order))运行结果缺失数量 缺失比例 用户ID 2 0.2857 订单金额 2 0.2857 支付方式 1 0.1429 下单时间 1 0.1429一个表里如果有多列缺失比例差异很大这个报告能帮你一眼锁定问题字段。我通常会在报告基础上决定超过70%的直接删列低于10%的直接填中间的再做人工判断。7.2 一个“理解业务”的填充函数模板下面的工具函数是我在几个项目里反复用过的模板它结合了groupby分组填充和可选的填充值策略日常处理多字段缺失时非常有效率。它本质上是一个策略配置器哪些列用均值、哪些列用中位数、哪些列要按某个分组键来分组填充。def smart_fillna(df, fill_rules): df: 待处理的DataFrame fill_rules: 字典键是列名值是填充策略。 策略可以是 - 一个固定值 - mean / median / mode - (group, 分组列名, mean / median / mode) df_filled df.copy() for col, rule in fill_rules.items(): if col not in df_filled.columns: continue if isinstance(rule, tuple) and rule[0] group: group_cols rule[1] method rule[2] if method mode: def mode_func(s): return s.mode()[0] if not s.mode().empty else np.nan fill_series df_filled.groupby(group_cols)[col].transform(mode_func) else: fill_series df_filled.groupby(group_cols)[col].transform(method) df_filled[col] df_filled[col].fillna(fill_series) elif rule mode: mode_value df_filled[col].mode()[0] df_filled[col] df_filled[col].fillna(mode_value) elif rule in (mean, median): stat_value getattr(df_filled[col], rule)() df_filled[col] df_filled[col].fillna(stat_value) else: df_filled[col] df_filled[col].fillna(rule) return df_filled用法示例df_final smart_fillna(df_order, { 订单金额: median, 支付方式: mode, 用户ID: 未知用户, 下单时间: (group, [支付方式], median) # 假设可对时间取中位数 })这个模板的精髓是把“哪些列用哪种策略”集中在一个可读性很强的字典里以后数据更新了只要改字典就能跑出一条新流程。数据清洗代码最忌讳的是一堆散落的fillna()散落在各处最后的可维护性会变得很差。8. dropna()和fillna()之外的“邻居”方法8.1 drop_duplicates()重复值往往比空值更隐蔽处理缺失值时我经常会顺手检查一遍重复值因为很多数据里重复记录和缺失记录相互纠缠。比如同一个订单出现两行一行金额有值一行金额为空如果直接dropna()可能会把两行都留下但它们其实是重复数据。drop_duplicates()用法很直观df_dup pd.DataFrame({ 订单号: [D001, D001, D002, D003], 金额: [100, 100, np.nan, 300] }) print(df_dup.drop_duplicates(subset[订单号], keepfirst))运行结果订单号 金额 0 D001 100.0 2 D002 NaN 3 D003 300.0在清洗流程里我习惯先做去重再做空值处理这样可以避免“重复行的一行填了、另一行没填”这类不一致问题。keepfirst表示保留第一次出现的行keeplast保留最后一次如果你想知道哪些行被删了可以用keepFalse配合indicatorTrue先观察。8.2 replace()和where()条件替换的灵活组合fillna()只能针对空值但现实数据里经常需要把特定值也看作“伪空值”或者把某些不合理的值替换成空值再做统一处理。这时replace()就能派上用场。df9 pd.DataFrame({ 评分: [5, 0, 3, 0, 4, -1] }) # 把0和-1这类异常评分统一替换成NaN df9[评分] df9[评分].replace([0, -1], np.nan) print(df9)运行结果评分 0 5.0 1 NaN 2 3.0 3 NaN 4 4.0 5 NaN替换成NaN之后就可以衔接fillna()做后续处理了。where()的用法则是换了一个角度它保留满足条件的值不满足条件的替换成指定值等价于“条件为假时填值”。实际项目中where()用得相对少一点但组合起来解决“特定条件下的空值填充”非常灵活比如“只对当天的数据做填充历史数据不动”。8.3 isna()与notna()掩码取用的实战价值除了统计isna()还经常用来做条件筛选和布尔索引。有人会问df[df[列].isna()]筛选出来的行我想直接查看或者导出人工核对该怎么操作很简单把它赋值给一个新变量或者写入Excel。null_rows df9[df9[评分].isna()] print(null_rows)这类掩码操作在数据质量报告、异常样本核查中非常常见。数据清洗不是一股脑执行完就结束偶尔需要把可疑样本拎出来人眼检查。掌握了isna()的布尔索引就等于掌握了“在数据里圈出问题区域”的能力。9. 为什么我会建议你形成一套自己的空值处理流程在写了大量数据处理脚本之后我最大的体会是dropna()和fillna()并不是彼此替代的关系而是一条清洗流水线上的两个工位。它们的共同目标不是“消灭空值”而是“让剩下用于分析的数据尽量真实地反映业务”。我的个人处理流程也分享出来供参考先drop_duplicates()去重让每条样本只出现一次。输出缺失值报告看每一列isna()的数量和比例。删掉全空列、全空行用dropna(axis1, howall)和dropna(axis0, howall)。对关键字段定义“必须非空”的约束用dropna(subset[...])删除不符合要求的行。对剩余字段分组填充或统计量填充优先考虑业务分组其次中位数再其次均值。时间序列字段的缺失用ffill或interpolate()。最后reset_index(dropTrue)再输出一次缺失值报告确认不再有遗漏。这套流程并不总是最优但它覆盖了绝大多数通用场景。如果你的数据是深度学习训练集填充策略可能还要更慎重甚至要设计专门的缺失值指示变量如果你的数据是线上实时特征流清洗逻辑可能要封装成独立模块每天跑。但无论场景怎么变底层逻辑都是同一个先理解为什么缺失再决定怎么处理最后用报告验证处理结果。我在实际项目里还形成过一个习惯每次做完数据清洗都会把“清洗前后行数变化”“每列填充了什么值/删除了多少行”记录到一个文本文件里方便之后回溯。数据清洗这件事看起来是体力活但一旦出了问题没有记录的话排查成本极高。把这些机制用起来再回头看dropna()和fillna()它俩就已经不是冰冷的函数而是你支撑数据分析结论可信度的两条拐杖了。
返回列表