Pandas 处理缺失值

现实世界的数据往往不完整,缺失值(NaN)是数据分析中常见的挑战。Pandas 提供了一套完整的工具来识别、删除、填充或插值缺失值,让你能够根据具体场景选择最佳策略。

📌 准备示例数据

首先创建一个包含多种缺失值情况的 DataFrame:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    '姓名': ['张三', '李四', '王五', '赵六', '陈七'],
    '年龄': [28, np.nan, 42, 29, np.nan],
    '工资': [8000, 12000, np.nan, 9500, 11000],
    '部门': ['技术', '销售', np.nan, '管理', '销售'],
    '入职日期': pd.date_range('2020-01-01', periods=5).insert(2, pd.NaT).delete(3)  # 制造缺失日期
})

print("原始数据:")
print(df)

🔍 识别缺失值

在进行任何处理之前,首先需要了解缺失值的分布情况。

isna() / isnull()

返回布尔型 DataFrame,缺失值位置为 True,否则为 False。

df.isna()
df['年龄'].isna()
notna() / notnull()

isna() 相反,非缺失值为 True。

df.notna()
# 统计每列缺失数量
print(df.isna().sum())

# 统计每行缺失数量
print(df.isna().sum(axis=1))

# 查看有缺失值的行
print(df[df.isna().any(axis=1)])

# 查看全部没有缺失值的行
print(df[df.notna().all(axis=1)])

🗑️ 删除缺失值:dropna()

直接删除包含缺失值的行或列,适用于缺失数据占比很小或随机缺失的情况。

删除含有缺失值的行
df.dropna()                    # 默认删除任何包含NaN的行
df.dropna(how='any')            # 同上(默认)
df.dropna(how='all')            # 仅删除全为NaN的行
df.dropna(thresh=3)             # 保留至少有3个非空值的行
删除含有缺失值的列
df.dropna(axis=1)              # 删除任何包含NaN的列
df.dropna(axis=1, how='all')    # 仅删除全为NaN的列
# 删除年龄缺失的行
df.dropna(subset=['年龄'])

# 原地修改
df.dropna(inplace=True)

🔄 填充缺失值:fillna()

用指定值、统计量或前/后值填充缺失值,是更常用的策略,可以保留数据样本量。

常数填充
df.fillna(0)                    # 所有NaN替换为0
df['年龄'].fillna(df['年龄'].mean())  # 用均值填充年龄
前向/后向填充
df.fillna(method='ffill')       # 用上一行值填充
df.fillna(method='bfill')       # 用下一行值填充
df.fillna(method='ffill', limit=1)  # 最多连续填充1个缺失
# 对不同列使用不同填充值
df.fillna({'年龄': df['年龄'].median(), '部门': '未知'})

# 用每列的均值填充
df.fillna(df.mean(numeric_only=True))

# 对分类列用众数填充
df['部门'].fillna(df['部门'].mode()[0], inplace=True)

# 对时间序列使用插值
df['入职日期'].fillna(method='ffill', inplace=True)  # 或使用 interpolate()

📈 插值:interpolate()

对于有序数据(如时间序列),插值方法可以更合理地估计缺失值。

# 线性插值(默认)
df['年龄'].interpolate()

# 时间插值(适用于日期索引)
df.set_index('入职日期', inplace=True)
df['年龄'].interpolate(method='time')
df.reset_index(inplace=True)  # 恢复索引

# 多项式插值
df['年龄'].interpolate(method='polynomial', order=2)

🔄 替换其他缺失标识

有时数据中用特殊值(如 -999, 'NULL', '?')表示缺失,需要先转换为 NaN。

# 读取时指定
df = pd.read_csv('data.csv', na_values=['NULL', '?', '-999'])

# 读取后替换
df.replace(['NULL', '?', -999], np.nan, inplace=True)

⚙️ 参数详解

axis 0=行,1=列
how 'any'或'all'
thresh 非空值阈值
subset 指定列
inplace 原地修改
method 'ffill'/'bfill'等
limit 填充次数限制

🧪 综合示例

import pandas as pd
import numpy as np

# 创建示例数据
df = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=10),
    '销量': [100, np.nan, 150, np.nan, 200, 180, np.nan, 210, 190, 220],
    '类别': ['A', 'B', np.nan, 'A', 'B', 'A', 'B', 'A', np.nan, 'B']
})

print("原始数据:")
print(df)
print("\n缺失统计:")
print(df.isna().sum())

# 处理策略
# 1. 删除类别缺失的行(因为类别是离散的,无法填充)
df_cleaned = df.dropna(subset=['类别'])
print("\n删除类别缺失后:")
print(df_cleaned)

# 2. 对销量列用前向填充(假设时间序列趋势)
df_cleaned['销量'] = df_cleaned['销量'].fillna(method='ffill')
print("\n销量前向填充后:")
print(df_cleaned)

# 3. 剩余的缺失可能已不存在
print("\n最终缺失统计:")
print(df_cleaned.isna().sum())

⚠️ 常见问题及注意事项

填充前需考虑业务含义:用均值填充年龄可能合理,但用均值填充类别则不妥。分类变量通常用众数或单独标记为“未知”。
链式填充的风险:使用 method='ffill' 时需确保数据已排序,否则会从错误的上一条记录填充。
inplace=True 的副作用:原地修改可能丢失原始数据,建议先创建副本或赋值给新变量。
浮点精度问题:NaN 不等于自身,判断缺失只能用 isna(),不能用 == np.nan
最佳实践:
  • 先使用 df.info()df.isna().sum() 了解缺失情况。
  • 根据数据特点和业务场景选择合适的处理策略:如果缺失极少,可直接删除;如果缺失有规律,可尝试填充或插值。
  • 对分类变量填充众数或“未知”;对数值变量可考虑均值、中位数或插值。
  • 记录缺失处理方式,便于后续分析和复现。