缺失值处理方法:删除与填充

刚开始做数据清洗时,我总觉得缺失值处理是件小事。直到有个项目,因为没处理好几个空值,最终模型输出结果直接偏了方向。嗯,从那以后我再也不敢轻视这步了。

说白了,缺失值处理就两条路——删或者填。但什么时候删、怎么删,什么时候填、用什么填,这里头门道不少。今天我把几种常用策略掰开揉碎了讲一遍。

知识结构概览

先看一张总图。这张图概括了缺失值处理的完整路线。你对照着看,心里就有谱了。

缺失值处理策略总览 数据输入 缺失率判断 高缺失率 低缺失率 删除列 填充值 均值/中位数填充 前向/后向填充 模型预测填充 清洗后输出

缺失值类型速查

动手之前,先搞清楚缺失值长什么样。我在项目中见过三种情况,列个表方便你对照。

缺失类型说明典型场景
完全随机缺失缺失与数据本身无关传感器偶尔掉线
随机缺失缺失与其他变量有关高收入人群不愿填收入
非随机缺失缺失与自身取值有关温度计超出量程

你想想看,如果是完全随机缺失,简单删除问题不大。但要是非随机缺失,你删掉就等于删信息,得小心。

策略一:简单删除

简单删除是最暴力的方法,但有时候反倒最管用。我个人习惯先看缺失比例,再做决定。

行删除

缺失比例低的时候,直接删行。比如1000行数据里缺了3行,删了也不心疼。

import pandas as pd

df = pd.read_csv("sales_data.csv")
print("原始行数:", len(df))

# 删除任何包含缺失值的行
df_cleaned = df.dropna()
print("删除后行数:", len(df_cleaned))

# 如果某列缺失超过50%,整行意义不大?不,那是列的事
# 行删除只关心数据行本身是否完整
小技巧:dropna() 默认是 any,只要有一列空就删。你可以用 subset 参数指定关键列。我一般先确认哪些列是必填的,只对着几列删,别误伤好数据。

列删除

遇到一列大部分都是空值,留着也是累赘。我有个项目,客户信息表里“传真号码”那列空掉95%,这种列留着干啥呢?

# 删除整列缺失超过80%的列
threshold = 0.8
df_clean = df.loc[:, df.isnull().mean() < threshold]
print("删除前列数:", len(df.columns))
print("删除后列数:", len(df_clean.columns))

为什么要设80%?我见过有人设60%,也有人设90%。这个阈值得看业务。你想想看,如果你非要保留那20%有值的记录,那也行。但我的经验是,缺失超80%的列,后续分析价值已经不大。

注意:列删除是最后的手段。我曾经删掉一列后发现,那列虽然空得多,但却是关键的业务标签。删之前一定跟业务方确认一下。

策略二:均值/中位数填充

填充比删除温和多了。我建议先用描述统计看一眼分布,再决定用均值还是中位数。

为什么?因为均值容易被极值带偏。比如工资数据,大部分人月薪1万,突然冒出个CEO月薪100万,均值就被拉上去了。这时候用中位数更靠谱。

# 加载数据
df = pd.read_csv("employee_data.csv")

# 检查缺失情况
print(df["salary"].isnull().sum())

# 均值填充
mean_salary = df["salary"].mean()
df["salary_filled_mean"] = df["salary"].fillna(mean_salary)

# 中位数填充
median_salary = df["salary"].median()
df["salary_filled_median"] = df["salary"].fillna(median_salary)

print("均值:", mean_salary)
print("中位数:", median_salary)

实际操作中,我还会加个判断:

# 先看看数据分布
skew = df["salary"].skew()
if abs(skew) > 1:
    print("数据偏态明显,建议用中位数填充")
else:
    print("数据近似对称,用均值填充即可")

核心原则:数据正态或对称 → 均值填充;数据偏态或存在异常值 → 中位数填充。

分组填充

很多时候全局均值不够精细。同一个指标,不同组别差异很大。比如不同城市的房价,你拿全国均值去填,肯定不准。

# 分组填充:按城市分组,填充房价缺失值
df["price_filled"] = df.groupby("city")["price"].transform(
    lambda x: x.fillna(x.median())
)

这个操作很实用。我记得在一个电商项目里,不同品类商品的销量分布差很多。用全局均值填充,结果模型效果一塌糊涂。改成按品类分组填充后,效果直接提升了一个档次。

策略三:前向/后向填充

时间序列数据里,前向和后向填充特别好用。说白了就是拿相邻值来补。比如传感器每隔1秒采集一次温度,中间丢了几个值,用前一个值补上,逻辑上说得通。

我个人习惯用前向填充多一点,因为它假设数据变化是连续的、渐进的。

# 时间序列数据
import pandas as pd

data = {
    "timestamp": pd.date_range("2024-01-01", periods=10, freq="h"),
    "temperature": [22.1, 22.3, None, 22.8, None, None, 23.5, 23.7, 23.9, None]
}
df = pd.DataFrame(data)

# 前向填充:用上一个非空值填充
df["temp_ffill"] = df["temperature"].fillna(method="ffill")
print(df[["timestamp", "temperature", "temp_ffill"]])

# 后向填充:用下一个非空值填充
df["temp_bfill"] = df["temperature"].fillna(method="bfill")
print(df[["timestamp", "temperature", "temp_bfill"]])

运行效果:前向填充会把缺失的温度慢慢接上;后向填充则是从后面的值“倒着”补回来。

你想想看,如果数据是股票价格,前后差不太多,前向填充没问题。但如果是心跳监测数据,突然缺了一段,用前向填充反而可能掩盖异常,这时候就得慎重。

避坑指南:我曾经用前向填充处理连续缺失超过5个时间点的数据,结果填充出来的序列变成了平直线,模型直接崩溃。后来我加了限制——连续缺失超过3个点就别用前向填充了,改用插值或者干脆删掉那段。

带限制的前后填充

# 限制只能往前填充2个值,超出的保持为空
df["temp_ffill_limit"] = df["temperature"].fillna(method="ffill", limit=2)

# 限制往后填充3个值
df["temp_bfill_limit"] = df["temperature"].fillna(method="bfill", limit=3)

limit 参数是个好东西。你给它设个上限,避免填充范围过大。我一般设2到3,具体看数据的时间间隔。

方法适用场景优点缺点
行删除缺失比例低(<5%)简单快速损失样本量
列删除缺失比例极高(>80%)减少噪声丢失特征
均值填充数据对称分布保持均值不变改变分布形状
中位数填充数据偏态分布鲁棒性好可能引入偏差
前向填充时间序列、顺序数据保持趋势连续缺失时效果差
后向填充时间序列、顺序数据利用后续信息实时场景不可用

综合实践:一个完整的清洗流程

最后给一个我常用的流程。你把它当成模板,改改就能用。

import pandas as pd
import numpy as np

def clean_missing_data(df, cat_cols=None, num_cols=None, 
                       num_fill="median", cat_fill="mode",
                       drop_col_thresh=0.8, drop_row_thresh=0.8):
    """
    综合缺失值清洗函数
    """
    df_clean = df.copy()
    
    # 1. 处理高缺失率列
    cols_to_drop = []
    for col in df_clean.columns:
        missing_rate = df_clean[col].isnull().mean()
        if missing_rate > drop_col_thresh:
            cols_to_drop.append(col)
    df_clean.drop(columns=cols_to_drop, inplace=True)
    
    # 2. 处理高缺失率行
    df_clean = df_clean.dropna(thresh=len(df_clean.columns) * (1 - drop_row_thresh))
    
    # 3. 填充数值列
    if num_cols is None:
        num_cols = df_clean.select_dtypes(include=[np.number]).columns.tolist()
    for col in num_cols:
        if df_clean[col].isnull().sum() > 0:
            if num_fill == "median":
                fill_val = df_clean[col].median()
            elif num_fill == "mean":
                fill_val = df_clean[col].mean()
            df_clean[col].fillna(fill_val, inplace=True)
    
    # 4. 填充类别列
    if cat_cols is None:
        cat_cols = df_clean.select_dtypes(include=["object"]).columns.tolist()
    for col in cat_cols:
        if df_clean[col].isnull().sum() > 0:
            fill_val = df_clean[col].mode()[0]
            df_clean[col].fillna(fill_val, inplace=True)
    
    return df_clean

这个函数我用了很多次。默认数值列用中位数填充,类别列用众数填充。你可以自己改参数,比如换成均值或者前向填充。

实用习惯:每次跑完清洗后,我会输出一份清洗报告,记录删了多少行、多少列、填充了多少个缺失值。这样后面出了问题也好追查。

嗯,以上就是缺失值处理的三种核心策略。说白了,没有万能的方法。你需要结合实际数据去试、去看。我个人建议先试删除,如果行数损失太大就改成填充。填充时优先考虑分组填充,效果往往更好。