数据清洗实战:Pandas缺失值处理的代码详解与避坑指南

张开发
2026/4/17 23:45:15 15 分钟阅读

分享文章

数据清洗实战:Pandas缺失值处理的代码详解与避坑指南
废话不多说今天直接上干货。我会用大量的代码示例逐行解释Pandas缺失值处理的每一个细节。看完这篇文章你不仅能看懂代码更能理解为什么这么写以及不同写法之间的区别。一、缺失值的三种面孔NaN、None、NA在开始处理之前我们先创建一个包含三种缺失值的Series看看它们长什么样。import pandas as pd import numpy as np # 创建一个包含三种缺失值的Series s pd.Series([1, np.nan, None, pd.NA]) print(s)输出0 1 1 NaN 2 None 3 NA dtype: object代码解读np.nan 是浮点型的缺失值打印出来显示为 NaNNone 是Python原生的空值打印出来就是 Nonepd.NA 是Pandas 1.0之后引入的统一缺失值标记打印出来显示为 NA虽然它们显示不同但Pandas的缺失值检测方法对它们一视同仁# 用 isnull() 检测缺失值 print(s.isnull())输出0 False 1 True 2 True 3 True dtype: bool重点说明isnull() 和 isna() 是完全等价的你可以选择任何一个使用。notnull() 和 notna() 则是它们的反义词用于筛选非缺失值。# 筛选出非缺失值 print(s[s.notnull()])输出0 1 dtype: object这里有一个容易踩的坑千万不要用 np.nan 来检测缺失值因为 np.nan np.nan 的结果是 False。下面的写法是错的# 错误示范这样写永远找不到缺失值 print(s[s np.nan]) # 返回空结果二、读取数据时处理缺失值实际工作中缺失值往往在数据文件里就以各种形式存在。我们来看一个天气数据的CSV文件它有5列日期、降水量、最高温、最低温、风向天气。1. 默认读取方式# 默认读取Pandas会自动识别常见的缺失值表示 df pd.read_csv(data/weather_withna.csv) print(df.tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 NaN NaN NaN NaN NaN 1457 2015-12-28 NaN NaN NaN NaN NaN 1458 2015-12-29 NaN NaN NaN NaN NaN 1459 2015-12-30 NaN NaN NaN NaN NaN 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明默认情况下Pandas会把空字符串、NA、NaN、null 等常见形式自动转换为缺失值。从输出可以看到前4行除了日期之外全部变成了 NaN。2. 关闭自动转换有时候你不想让Pandas自动转换比如你想保留原始的空字符串# keep_default_naFalse 会关闭所有自动缺失值转换 df pd.read_csv(data/weather_withna.csv, keep_default_naFalse) print(df.tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 1457 2015-12-28 1458 2015-12-29 1459 2015-12-30 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明此时原本是空白的单元格变成了空字符串而不是 NaN。这在某些需要区分“数据缺失”和“数据为空字符串”的场景下很有用。3. 自定义缺失值标记更常见的情况是你的数据文件用特殊值表示缺失比如用 -999、Unknown 或者某个特定日期# 将指定的值视为缺失值 df pd.read_csv(data/weather_withna.csv, na_values[2015-12-31]) print(df.tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 NaN NaN NaN NaN NaN 1457 2015-12-28 NaN NaN NaN NaN NaN 1458 2015-12-29 NaN NaN NaN NaN NaN 1459 2015-12-30 NaN NaN NaN NaN NaN 1460 NaN 20.6 12.2 5.0 3.8 rain说明na_values 参数可以接受一个列表将列表中的值全部转为缺失值。这里我们把日期 2015-12-31 也视为缺失值所以最后一行的日期变成了 NaN。三、查看缺失值的分布1. 统计每列缺失数量df pd.read_csv(data/weather_withna.csv) missing_counts df.isnull().sum() print(missing_counts)输出date 0 precipitation 303 temp_max 303 temp_min 303 wind 303 weather 303 dtype: int64说明isnull() 返回一个布尔型的DataFramesum() 对每列求和True1False0得到每列的缺失值个数。这里可以看到除了日期列其他5列都缺失了303个值。如果想看缺失比例可以这样写# 计算缺失比例 missing_ratio df.isnull().sum() / len(df) * 100 print(missing_ratio.round(2))输出date 0.00 precipitation 20.75 temp_max 20.75 temp_min 20.75 wind 20.75 weather 20.75 dtype: float642. 可视化缺失值需要安装missingnoimport missingno as msno import matplotlib.pyplot as plt # 条形图直观显示每列缺失比例 msno.bar(df) plt.show()说明条形图的高度表示每列非缺失值的数量右侧的刻度表示缺失比例。一眼就能看出哪列缺失最严重。# 热力图显示列之间缺失值的相关性 msno.heatmap(df) plt.show()说明热力图的颜色越接近1深色表示两列的缺失值正相关越强。如果 temp_max 和 temp_min 的相关性接近1说明它们往往是同时缺失的。这个信息非常重要如果两列缺失强相关那么填充时可以考虑使用其中一列的信息来推算另一列。四、剔除缺失值dropna详解1. Series的dropna# 创建一个带缺失值的Series s pd.Series([1, pd.NA, 2, None, 3]) print(原始数据) print(s) print(\n剔除缺失值后) print(s.dropna())输出原始数据 0 1 1 NA 2 2 3 None 4 3 dtype: object 剔除缺失值后 0 1 2 2 4 3 dtype: object说明dropna() 会直接删除所有缺失值返回一个新的Series原始数据不变。2. DataFrame的dropnaDataFrame的删除逻辑和Series不同你不能只删除某个单元格只能删除整行或整列。# 创建一个3行3列的DataFrame包含缺失值 df pd.DataFrame([ [1, pd.NA, 2], [2, 3, 5], [pd.NA, 4, 6] ]) print(原始DataFrame) print(df)输出原始DataFrame 0 1 2 0 1 NA 2 1 2 3 5 2 NA 4 6默认行为删除任何包含缺失值的行print(删除包含缺失值的行) print(df.dropna())输出删除包含缺失值的行 0 1 2 1 2 3 5说明默认情况下 dropna() 会删除任何一行中只要有一个缺失值就整行删除。只有第1行索引1没有缺失值所以只保留了这一行。按列删除axis1print(删除包含缺失值的列) print(df.dropna(axis1))输出删除包含缺失值的列 2 0 2 1 5 2 6说明axis1 表示按列操作。检查每一列只要有一个缺失值整列就被删除。这里第0列和第1列都有缺失值只有第2列没有缺失值所以只保留了第2列。删除全为缺失值的行howall# 创建一个包含全缺失行的DataFrame df2 pd.DataFrame([ [1, pd.NA, 2], [pd.NA, pd.NA, pd.NA], [pd.NA, pd.NA, 5] ]) print(原始DataFrame) print(df2) print(\n删除全为缺失值的行) print(df2.dropna(howall))输出原始DataFrame 0 1 2 0 1 NA 2 1 NA NA NA 2 NA NA 5 删除全为缺失值的行 0 1 2 0 1 NA 2 2 NA NA 5说明howall 表示只有当一行中所有值都是缺失值时才删除这一行。索引1的行全是缺失值被删除了索引0和索引2的行至少有一个非缺失值被保留。保留足够多的非缺失值threshprint(保留至少有2个非缺失值的行) print(df2.dropna(thresh2))输出保留至少有2个非缺失值的行 0 1 2 0 1 NA 2说明thresh2 表示保留那些至少有2个非缺失值的行。索引0有2个非缺失值1和2符合条件索引1有0个非缺失值不符合索引2只有1个非缺失值5不符合。所以只保留了索引0这一行。根据特定列进行删除subsetprint(根据第0列是否有缺失值来删除行) print(df2.dropna(subset[0]))输出根据第0列是否有缺失值来删除行 0 1 2 0 1 NA 2说明subset[0] 表示只看第0列如果第0列的值为缺失值就删除该行。索引1和索引2的第0列都是缺失值被删除只有索引0的第0列是1非缺失被保留。五、填充缺失值fillna详解1. 用固定值填充df pd.read_csv(data/weather_withna.csv) # 用0填充所有缺失值 print(用0填充后) print(df.fillna(0).tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 0.0 0.0 0.0 0.0 0 1457 2015-12-28 0.0 0.0 0.0 0.0 0 1458 2015-12-29 0.0 0.0 0.0 0.0 0 1459 2015-12-30 0.0 0.0 0.0 0.0 0 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明fillna(0) 把所有缺失值都替换成了0。注意 weather 列原本是字符串类型但被填充后也变成了0这可能导致数据类型变化。所以更推荐按列分别填充。2. 用字典按列填充# 为不同列指定不同的填充值 fill_values {temp_max: 60, temp_min: -60} print(按列填充) print(df.fillna(fill_values).tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 NaN 60.0 -60.0 NaN NaN 1457 2015-12-28 NaN 60.0 -60.0 NaN NaN 1458 2015-12-29 NaN 60.0 -60.0 NaN NaN 1459 2015-12-30 NaN 60.0 -60.0 NaN NaN 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明字典的键是列名值是要填充的内容。这样只填充了 temp_max 和 temp_min 两列其他列的缺失值保持不变。3. 用统计值填充实际工作中最常用的是用均值、中位数、众数填充。# 计算各列的均值 mean_values df[[precipitation, temp_max, temp_min, wind]].mean() print(各列均值) print(mean_values) print(\n用均值填充后) print(df.fillna(mean_values).tail(5))输出各列均值 precipitation 3.052332 temp_max 15.851468 temp_min 7.877202 wind 3.820945 dtype: float64 用均值填充后 date precipitation temp_max temp_min wind weather 1456 2015-12-27 3.052332 15.851468 7.877202 3.820945 NaN 1457 2015-12-28 3.052332 15.851468 7.877202 3.820945 NaN 1458 2015-12-29 3.052332 15.851468 7.877202 3.820945 NaN 1459 2015-12-30 3.052332 15.851468 7.877202 3.820945 NaN 1460 2015-12-31 20.600000 12.200000 5.000000 3.800000 rain说明先计算需要填充的列的平均值然后一次性填充。这里有一个细节weather 列是字符串无法计算均值所以单独处理或保持原样。如果想用中位数填充# 用中位数填充 median_values df[[precipitation, temp_max, temp_min, wind]].median() df.fillna(median_values, inplaceTrue)inplaceTrue 表示直接在原DataFrame上修改不返回新对象。4. 用前后值填充适合时间序列# 先用0填充一下制造一个有规律缺失的数据 df pd.read_csv(data/weather_withna.csv) # 向前填充用上一个有效值填充 print(向前填充ffill) print(df.ffill().tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 0.0 11.1 4.4 0.0 NaN 1457 2015-12-28 0.0 11.1 4.4 0.0 NaN 1458 2015-12-29 0.0 11.1 4.4 0.0 NaN 1459 2015-12-30 0.0 11.1 4.4 0.0 NaN 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明ffill() 是 fillna(methodffill) 的简写。它会用前一个有效值来填充缺失值。从输出可以看出1456行到1459行的缺失值都被填充成了1455行未显示的数据。# 向后填充用下一个有效值填充 print(向后填充bfill) print(df.bfill().tail(5))输出date precipitation temp_max temp_min wind weather 1456 2015-12-27 20.6 12.2 5.0 3.8 rain 1457 2015-12-28 20.6 12.2 5.0 3.8 rain 1458 2015-12-29 20.6 12.2 5.0 3.8 rain 1459 2015-12-30 20.6 12.2 5.0 3.8 rain 1460 2015-12-31 20.6 12.2 5.0 3.8 rain说明bfill() 是 fillna(methodbfill) 的简写。它会用后一个有效值来填充。注意最后一行索引1460是有真实数据的所以它之前的所有缺失行都被填充成了这个值。5. 线性插值填充interpolate这是比前后填充更“聪明”的方法它根据数据的变化趋势来估算缺失值。# 创建一个有缺失值的Series s pd.Series([1, np.nan, 3, 4, np.nan, 6]) print(原始Series) print(s) print(\n线性插值后) print(s.interpolate())输出原始Series 0 1.0 1 NaN 2 3.0 3 4.0 4 NaN 5 6.0 dtype: float64 线性插值后 0 1.0 1 2.0 2 3.0 3 4.0 4 5.0 5 6.0 dtype: float64说明线性插值假设缺失值前后的两个点之间是直线关系。索引1的缺失值介于索引0的1和索引2的3之间插值得到2索引4的缺失值介于索引3的4和索引5的6之间插值得到5。对于时间序列数据可以指定 methodtime这样插值时会考虑时间间隔# 创建带时间索引的Series dates pd.date_range(2023-01-01, periods5, freqD) s_time pd.Series([10, np.nan, np.nan, 40, 50], indexdates) print(原始时间序列) print(s_time) print(\n时间插值考虑时间间隔) print(s_time.interpolate(methodtime))输出原始时间序列 2023-01-01 10.0 2023-01-02 NaN 2023-01-03 NaN 2023-01-04 40.0 2023-01-05 50.0 Freq: D, dtype: float64 时间插值考虑时间间隔 2023-01-01 10.0 2023-01-02 20.0 2023-01-03 30.0 2023-01-04 40.0 2023-01-05 50.0 Freq: D, dtype: float64说明由于时间间隔是均匀的每天一次结果和线性插值相同。但如果时间间隔不均匀methodtime 会根据实际的时间差来分配权重使结果更合理。六、实际案例综合处理一个真实数据集假设我们有这么一份数据包含了缺失值我们想要做完整的清洗# 模拟一份数据 data { 日期: [2024-01-01, 2024-01-02, 2024-01-03, 2024-01-04, 2024-01-05], 销售额: [100, None, 150, None, 200], 成本: [60, 70, None, 90, 100], 地区: [北区, 南区, None, 北区, 东区] } df pd.DataFrame(data) print(原始数据) print(df)输出原始数据 日期 销售额 成本 地区 0 2024-01-01 100.0 60.0 北区 1 2024-01-02 NaN 70.0 南区 2 2024-01-03 150.0 NaN None 3 2024-01-04 NaN 90.0 北区 4 2024-01-05 200.0 100.0 东区步骤1查看缺失情况print(缺失值统计) print(df.isnull().sum()) print(\n缺失比例) print((df.isnull().sum() / len(df) * 100).round(2))输出缺失值统计 日期 0 销售额 2 成本 1 地区 1 dtype: int64 缺失比例 日期 0.0 销售额 40.0 成本 20.0 地区 20.0 dtype: float64步骤2根据业务决定处理策略销售额可能受时间趋势影响用线性插值填充成本用均值填充地区用众数出现最多的地区填充# 先处理数值列 df[销售额] df[销售额].interpolate(methodlinear) print(线性插值填充销售额后) print(df) # 用均值填充成本 mean_cost df[成本].mean() df[成本] df[成本].fillna(mean_cost) print(\n用均值填充成本后) print(df) # 用众数填充地区 mode_region df[地区].mode()[0] # mode()返回Series取第一个 df[地区] df[地区].fillna(mode_region) print(\n用众数填充地区后) print(df)输出线性插值填充销售额后 日期 销售额 成本 地区 0 2024-01-01 100.0 60.0 北区 1 2024-01-02 125.0 70.0 南区 2 2024-01-03 150.0 NaN None 3 2024-01-04 175.0 90.0 北区 4 2024-01-05 200.0 100.0 东区 用均值填充成本后 日期 销售额 成本 地区 0 2024-01-01 100.0 60.0 北区 1 2024-01-02 125.0 70.0 南区 2 2024-01-03 150.0 80.0 None 3 2024-01-04 175.0 90.0 北区 4 2024-01-05 200.0 100.0 东区 用众数填充地区后 日期 销售额 成本 地区 0 2024-01-01 100.0 60.0 北区 1 2024-01-02 125.0 70.0 南区 2 2024-01-03 150.0 80.0 北区 3 2024-01-04 175.0 90.0 北区 4 2024-01-05 200.0 100.0 东区说明销售额的插值索引1缺失介于100和150之间插值得125索引3缺失介于150和200之间插值得175。成本的均值(607090100)/4 80填充到索引2。地区的众数北区出现2次南区1次东区1次所以用“北区”填充索引2。总结通过上面的代码示例我们可以看到Pandas缺失值处理的核心就是两件事查找和处理。查找isnull() sum() 快速统计missingno 可视化分析缺失模式处理dropna() 删除适合缺失比例极低的情况fillna() 填充适合缺失比例适中且有合理填充依据的情况interpolate() 插值适合时间序列或有趋势的数据在实际工作中没有放之四海而皆准的方法。你需要先理解业务搞清楚缺失的原因再选择合适的方法。有时候宁可保留缺失值用模型预测也不要胡乱填充引入偏差。希望这些代码示例能帮助你在实际项目中游刃有余地处理缺失值问题。记住数据清洗是数据分析的基石处理得好后面的分析才能站得稳。

更多文章