第3章 数据准备与探索:缺失值、异常值、分布分析
数据准备,说白了就是给模型喂“干净”的数据。我做了这么多年项目,大部分时间都花在这一步——不是模型不好,是数据没弄对。
今天咱们就聊三件核心事:缺失值怎么补、异常值怎么揪、数据分布怎么调。嗯,这三件事搞定了,分箱特征就稳了一大半。
3.1 缺失值处理
缺失值,每个数据集都逃不掉。你想想看,一个字段空了一大片,模型只能猜,猜错了就带偏。
先分类型
- MCAR(完全随机缺失):比如用户手抖没填,概率和其他字段无关。直接删行或填充都行。
- MAR(随机缺失):缺失概率和其他字段有关。举个例子,高收入人群更不愿意填收入,那收入缺失就和所在行业、职位有关。这时候得用回归或模型填充。
- MNAR(非随机缺失):缺失值和自身有关。比如低分学生故意不填成绩。这最难办,得结合业务逻辑。
我常用的填充方法
| 方法 | 适用场景 | 注意点 |
|---|---|---|
| 直接删除 | 缺失率<5%,且MCAR | 丢失数据量不宜过大 |
| 均值/中位数填充 | 数值型,分布较对称 | 会降低方差 |
| 众数填充 | 分类型 | 可能引入偏误 |
| KNN填充 | 样本量够,缺失字段和邻居相关 | 计算量大,大样本慎用 |
| 回归/模型预测填充 | 缺失字段和其他字段有强相关 | 防止过拟合 |
我记得有一次做信贷评分,收入字段缺失了40%。我偷懒直接填了均值,结果模型把钱借给了一堆低收入的人——坏账率飙升。后来改用回归填充,把年龄、学历、职业放进去预测,效果才正常。
注意:LightGBM原生支持缺失值,默认将缺失值分到增益最大的方向。但你要是不管缺失值的业务含义,模型可能会学到“缺失”是某种信号。我个人习惯把缺失单独设一个占位符(比如-999),再观察模型怎么分箱。
import pandas as pd
from sklearn.impute import SimpleImputer
# 数值型:中位数填充
num_cols = ['age', 'income']
imputer_num = SimpleImputer(strategy='median')
df[num_cols] = imputer_num.fit_transform(df[num_cols])
# 分类型:众数填充
cat_cols = ['education', 'occupation']
imputer_cat = SimpleImputer(strategy='most_frequent')
df[cat_cols] = imputer_cat.fit_transform(df[cat_cols])
3.2 异常值处理
异常值,就是数据里的“刺头”。一个错误录入就可能把模型带沟里。你想想看,一个年薪10亿的人出现在收入字段里,模型还以为人人都这么高。
怎么发现异常?
- IQR法:下界Q1-1.5*IQR,上界Q3+1.5*IQR。简单好用,适合偏态分布。
- Z-score法:超过3倍标准差算异常。适合正态分布。
- 业务阈值:比如年龄超过120岁、收入小于0。这是我数据探索第一步,先设业务规则过滤。
处理策略
| 策略 | 做法 | 什么时候用 |
|---|---|---|
| 截尾(Winsorize) | 把超出上下界的值替换为边界值 | 异常值可能是极端但真实 |
| 删除 | 直接删掉异常样本 | 明显录入错误,且量小 |
| 单独建模 | 把异常值作为一个特殊类别 | 异常值本身有业务意义(比如欺诈) |
我曾经在电商数据里给用户特征做分箱,发现消费金额有个极端高值——是个批发客户一次性采购了几百万。如果直接删掉,模型就不知道这种大客户长什么样。后来我把它单独设成一个箱,让模型自己学。
我的建议:先做业务规则过滤(比如年龄>0且<120),再用IQR法筛选。千万别一上来就删,多看看数据上下文。
# IQR法检测异常值
Q1 = df['income'].quantile(0.25)
Q3 = df['income'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df['income'] < lower) | (df['income'] > upper)]
# 截尾处理
df['income'] = df['income'].clip(lower, upper)
3.3 数据分布分析
数据分布,决定了你分箱是否合理。我习惯先画直方图,看一眼是不是偏态严重。LightGBM对极端偏态比较敏感——你想想看,一堆数据挤在左边,右边只有几个点,分箱效果肯定差。
看什么指标
- 偏度:0为对称,>0右偏(长尾在右),<0左偏。绝对值>1算严重偏态。
- 峰度:>3尖峰,<3平峰。尖峰意味着数据集中在均值附近,分箱的边界容易扎堆。
怎么调整
- 对数变换:适用右偏,数据必须>0。
- Box-Cox变换:自动找最佳lambda,适用正数数据。
- 平方根变换:较温和的右偏处理。
- 分箱本身也是个变换:等频分箱可以处理偏态,但箱内可能全是左偏数据,要小心。
我习惯在做分箱之前,先对偏度>1的字段做对数变换。比如收入、交易金额这种长尾字段,取完log分布就舒服多了。之后再分箱,边界清晰,模型也能稳定。
import numpy as np
from scipy import stats
# 检查偏度
skewness = df['amount'].skew()
print(f'偏度: {skewness:.2f}')
# 若右偏严重,做对数变换
if skewness > 1:
df['amount_log'] = np.log1p(df['amount']) # log(1+x)避免0值问题
# 或用Box-Cox
df['amount_boxcox'], lam = stats.boxcox(df['amount'] + 1)
print(f'最佳lambda: {lam:.2f}')
经验总结:先做缺失值填充(别简单填均值,根据业务选方法),再揪异常值(业务规则+IQR),最后看分布做变换。这三步走完,数据才能进分箱。
这张图是我每次建模前的标准动作。记住,顺序不要乱:先补缺失,再除异常,最后调分布。每一步都有坑,多看看数据本身,别光跑代码。