缺失值处理方法:删除与填充
刚开始做数据清洗时,我总觉得缺失值处理是件小事。直到有个项目,因为没处理好几个空值,最终模型输出结果直接偏了方向。嗯,从那以后我再也不敢轻视这步了。
说白了,缺失值处理就两条路——删或者填。但什么时候删、怎么删,什么时候填、用什么填,这里头门道不少。今天我把几种常用策略掰开揉碎了讲一遍。
知识结构概览
先看一张总图。这张图概括了缺失值处理的完整路线。你对照着看,心里就有谱了。
缺失值类型速查
动手之前,先搞清楚缺失值长什么样。我在项目中见过三种情况,列个表方便你对照。
| 缺失类型 | 说明 | 典型场景 |
|---|---|---|
| 完全随机缺失 | 缺失与数据本身无关 | 传感器偶尔掉线 |
| 随机缺失 | 缺失与其他变量有关 | 高收入人群不愿填收入 |
| 非随机缺失 | 缺失与自身取值有关 | 温度计超出量程 |
你想想看,如果是完全随机缺失,简单删除问题不大。但要是非随机缺失,你删掉就等于删信息,得小心。
策略一:简单删除
简单删除是最暴力的方法,但有时候反倒最管用。我个人习惯先看缺失比例,再做决定。
行删除
缺失比例低的时候,直接删行。比如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%,整行意义不大?不,那是列的事
# 行删除只关心数据行本身是否完整
列删除
遇到一列大部分都是空值,留着也是累赘。我有个项目,客户信息表里“传真号码”那列空掉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"]])
运行效果:前向填充会把缺失的温度慢慢接上;后向填充则是从后面的值“倒着”补回来。
你想想看,如果数据是股票价格,前后差不太多,前向填充没问题。但如果是心跳监测数据,突然缺了一段,用前向填充反而可能掩盖异常,这时候就得慎重。
带限制的前后填充
# 限制只能往前填充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
这个函数我用了很多次。默认数值列用中位数填充,类别列用众数填充。你可以自己改参数,比如换成均值或者前向填充。
嗯,以上就是缺失值处理的三种核心策略。说白了,没有万能的方法。你需要结合实际数据去试、去看。我个人建议先试删除,如果行数损失太大就改成填充。填充时优先考虑分组填充,效果往往更好。