数据清洗与预处理:让脏数据变干净的艺术
说实话,做风控建模这么多年,我见过太多人一上来就急着调模型。结果呢?数据一塌糊涂,模型跑出来全是垃圾。我个人的习惯是——拿到数据后,先花 60% 的时间做清洗和预处理。这一步做好了,后面建模就顺了。
今天咱们聊聊数据清洗与预处理的核心环节。说白了,就是让那些乱七八糟的原始数据,变成模型能吃的干净数据。
一、缺失值处理:别让空值毁了你的模型
小微企业数据里,缺失值太常见了。有些企业主不愿意填,有些是系统采集不到。你想想看,一个字段空着,模型怎么学?
我一般把缺失值处理分成三种策略:
1. 直接删除法
什么时候用?缺失比例很小(比如低于 5%),而且缺失是随机的。这时候删掉几行,影响不大。
# 删除缺失值
df.dropna(subset=['营业收入', '资产负债率'], inplace=True)
2. 均值/中位数填充
这是最常用的方法。我个人习惯:偏态分布用中位数,正态分布用均值。
举个例子,小微企业「营业收入」这个字段,大部分是几百万,但偶尔有几个上亿的。这时候均值会被拉高,用中位数更靠谱。
# 中位数填充
df['营业收入'].fillna(df['营业收入'].median(), inplace=True)
# 均值填充
df['资产负债率'].fillna(df['资产负债率'].mean(), inplace=True)
3. 模型预测填充
这个方法稍微复杂点,但效果最好。用其他字段做特征,预测缺失的那个字段。
我在项目中遇到过这种情况:某家企业的「净利润」字段缺失了 20%。直接用均值填充?太粗糙了。我用「营业收入」「行业」「员工人数」做特征,训练一个随机森林模型来预测缺失值。效果比均值填充提升了 15% 的模型 AUC。
from sklearn.ensemble import RandomForestRegressor
# 准备数据
train_data = df[df['净利润'].notna()]
test_data = df[df['净利润'].isna()]
# 训练模型
model = RandomForestRegressor()
model.fit(train_data[['营业收入', '员工人数']], train_data['净利润'])
# 预测填充
df.loc[df['净利润'].isna(), '净利润'] = model.predict(test_data[['营业收入', '员工人数']])
二、异常值处理:揪出那些离谱的数据
小微企业数据里,异常值特别多。比如某企业年营业收入填了 100 亿,一看就是错的。或者资产负债率填了 200%,明显不合理。
我常用的两种方法:
1. 截尾法(也叫盖帽法)
把超出某个范围的值,强行拉回到边界值。比如把超过 99% 分位数的值,替换成 99% 分位数的值。
# 截尾法处理
def cap_outliers(series, lower=0.01, upper=0.99):
q_low = series.quantile(lower)
q_high = series.quantile(upper)
return series.clip(lower=q_low, upper=q_high)
df['营业收入'] = cap_outliers(df['营业收入'])
为什么用 1% 和 99%?而不是 5% 和 95%?这取决于你的数据量。数据量大,可以卡得严一点;数据量小,别卡太狠,否则样本不够用。
2. 分箱法
把连续变量变成离散的区间。比如把「营业收入」分成 0-100万、100-500万、500万以上 三个箱。
分箱的好处是:异常值被自动归到某个箱子里,不会单独影响模型。我特别喜欢用分箱处理「贷款金额」这个字段——小微企业贷款金额差异太大,分箱后模型更稳定。
# 分箱处理
bins = [0, 100, 500, float('inf')]
labels = ['小型', '中型', '大型']
df['营收等级'] = pd.cut(df['营业收入'], bins=bins, labels=labels)
三、重复值检测与删除:别让同一家企业出现两次
这个看起来简单,但坑不少。重复值分两种:
- 完全重复:所有字段都一样。直接删掉就行。
- 部分重复:企业名称一样,但其他字段不同。这时候要小心,可能是同一家企业的不同时间点数据。
# 检测重复值
duplicates = df.duplicated(subset=['企业名称'], keep=False)
print(f'重复记录数:{duplicates.sum()}')
# 删除完全重复
df.drop_duplicates(inplace=True)
# 处理部分重复:保留最新的一条记录
df.sort_values('数据采集日期', inplace=True)
df.drop_duplicates(subset=['企业名称'], keep='last', inplace=True)
四、数据类型转换:让计算机读懂你的数据
这一步容易被忽略,但特别重要。你想想看,如果「营业收入」被读成了字符串,模型怎么算?
我一般检查这几个方面:
| 字段类型 | 常见问题 | 处理方法 |
|---|---|---|
| 数值型 | 被读成 object(字符串) | pd.to_numeric() |
| 日期型 | 被读成字符串 | pd.to_datetime() |
| 类别型 | 数值编码但实际是类别 | astype('category') |
# 数据类型转换
df['营业收入'] = pd.to_numeric(df['营业收入'], errors='coerce')
df['注册日期'] = pd.to_datetime(df['注册日期'], errors='coerce')
df['行业代码'] = df['行业代码'].astype('category')
errors='coerce',遇到无法转换的值会自动变成 NaN。这样你就能发现哪些数据有问题,再回头处理。
嗯,数据清洗这块就聊到这儿。说白了,这一步虽然枯燥,但决定了模型的天花板。数据干净了,模型自然就稳了。