数据清洗与预处理:让脏数据变干净的艺术

说实话,做风控建模这么多年,我见过太多人一上来就急着调模型。结果呢?数据一塌糊涂,模型跑出来全是垃圾。我个人的习惯是——拿到数据后,先花 60% 的时间做清洗和预处理。这一步做好了,后面建模就顺了。

今天咱们聊聊数据清洗与预处理的核心环节。说白了,就是让那些乱七八糟的原始数据,变成模型能吃的干净数据。

数据清洗与预处理核心流程 原始数据 缺失值处理 异常值处理 重复值处理 数据类型转换 删除 / 均值填充 中位数填充 模型预测填充 截尾法 分箱法 检测与删除 数值 / 类别 / 时间 干净数据 → 建模

一、缺失值处理:别让空值毁了你的模型

小微企业数据里,缺失值太常见了。有些企业主不愿意填,有些是系统采集不到。你想想看,一个字段空着,模型怎么学?

我一般把缺失值处理分成三种策略:

1. 直接删除法

什么时候用?缺失比例很小(比如低于 5%),而且缺失是随机的。这时候删掉几行,影响不大。

⚠️ 注意: 如果缺失比例超过 30%,千万别直接删。我见过有人把 40% 的数据删了,结果模型偏差大得离谱。
# 删除缺失值
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)
💡 我的经验: 分箱的边界值怎么定?我一般先看业务含义,再看数据分布。比如小微企业贷款,50万以下算小额,50-200万算中额,200万以上算大额。业务逻辑比统计逻辑更重要。

三、重复值检测与删除:别让同一家企业出现两次

这个看起来简单,但坑不少。重复值分两种:

  • 完全重复:所有字段都一样。直接删掉就行。
  • 部分重复:企业名称一样,但其他字段不同。这时候要小心,可能是同一家企业的不同时间点数据。
# 检测重复值
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。这样你就能发现哪些数据有问题,再回头处理。

嗯,数据清洗这块就聊到这儿。说白了,这一步虽然枯燥,但决定了模型的天花板。数据干净了,模型自然就稳了。


专注资料整理