第2章:数据预处理实战

数据预处理,说白了就是给原始数据「洗澡」。我做了这么多年数据项目,敢说80%的时间都耗在这上面。你想想看,垃圾数据进,垃圾模型出——这是铁律。

这一章,咱们就聊聊数据清洗、缺失值处理、异常值检测,还有特征工程那些事儿。嗯,都是我在项目里踩过坑、流过泪之后总结出来的。

2.1 数据清洗——先把脏东西筛掉

数据清洗,我习惯从三个维度入手:格式统一、重复数据、无效数据。

核心原则:清洗不是删数据,而是让数据变得可用。

2.1.1 格式统一

我在项目中遇到过最头疼的事——同一个日期字段,有人写"2024-01-15",有人写"2024/1/15",还有人写"15-Jan-2024"。你想想看,模型哪分得清这些?

我的做法是:先统一成标准格式,再往下走。

import pandas as pd

# 假设有个日期列
df['date'] = pd.to_datetime(df['date'], errors='coerce')
# errors='coerce' 会把无法解析的变成 NaT

2.1.2 重复数据

重复数据是个隐形杀手。我曾经做过一个用户画像项目,因为没去重,同一个用户被算了两次,结果模型预测偏差了15%。

# 检查重复行
print(df.duplicated().sum())

# 删除重复行
df = df.drop_duplicates()

# 按指定列去重
df = df.drop_duplicates(subset=['user_id', 'timestamp'])

注意:不是所有重复都要删。比如时间序列数据,同一条记录可能是不同时间点的正常采样。先搞清楚业务含义再动手。

2.2 缺失值处理——别让空值毁了模型

缺失值,每个项目都会遇到。我见过有人直接删掉所有含空值的行——结果样本量从10万掉到2万,模型直接废了。

为什么会这样?因为缺失本身可能携带信息。比如用户没填收入字段,可能说明ta对隐私敏感。

2.2.1 先搞清楚缺失模式

缺失类型 含义 处理方式
MCAR(完全随机缺失) 缺失与数据本身无关 直接删除或简单填充
MAR(随机缺失) 缺失与其他变量有关 用其他变量预测填充
MNAR(非随机缺失) 缺失与缺失值本身有关 需要业务知识辅助处理

2.2.2 常用填充方法

我个人习惯按这个优先级来:

  1. 业务填充——比如年龄缺失,用中位数;收入缺失,用行业平均值
  2. 统计填充——均值、中位数、众数
  3. 模型预测填充——用KNN、回归等模型预测缺失值
  4. 标记缺失——单独加一列,标记是否缺失
# 简单填充
df['age'].fillna(df['age'].median(), inplace=True)

# 分组填充(按性别填充年龄中位数)
df['age'] = df.groupby('gender')['age'].transform(
    lambda x: x.fillna(x.median())
)

# 标记缺失
df['age_missing'] = df['age'].isna().astype(int)

小技巧:数值型字段,我常用中位数而不是均值。因为均值容易被异常值拉偏,中位数更稳健。

2.3 异常值检测与处理——别让离群点带偏节奏

异常值,说白了就是「不听话」的数据点。我做过一个电商项目,有个用户的消费金额是其他人的1000倍——后来发现是测试账号。

嗯,这里要注意:异常值不一定是错误,也可能是真正的极端用户。

2.3.1 检测方法

我常用的三种方法:

  • Z-Score法——适合正态分布的数据,Z分数超过3的算异常
  • IQR法——四分位距法,适合非正态分布
  • 孤立森林——适合高维数据
# IQR法检测异常值
Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['amount'] < lower_bound) | (df['amount'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")

2.3.2 处理策略

我曾经犯过一个错——把所有异常值都删了。结果模型在极端场景下完全失效。后来我学乖了:

  • 业务确认——先问业务方,这个值合理吗?
  • 截尾处理——把异常值限制在合理范围内
  • 单独建模——异常值本身可能是一个有价值的群体
# 截尾处理(Winsorization)
def cap_outliers(series, lower=0.01, upper=0.99):
    q_low = series.quantile(lower)
    q_high = series.quantile(upper)
    return series.clip(q_low, q_high)

df['amount_capped'] = cap_outliers(df['amount'])

2.4 特征工程基础——好特征胜过好模型

特征工程,说白了就是把原始数据变成模型能听懂的语言。我见过太多人一上来就调参,结果特征都没处理好——白费功夫。

2.4.1 数值特征处理

数值特征,我习惯做这几步:

  • 标准化——让不同量纲的特征可比
  • 归一化——把值压缩到[0,1]区间
  • 对数变换——处理长尾分布
from sklearn.preprocessing import StandardScaler, MinMaxScaler

# 标准化(均值为0,标准差为1)
scaler = StandardScaler()
df['amount_scaled'] = scaler.fit_transform(df[['amount']])

# 对数变换(处理偏态分布)
import numpy as np
df['amount_log'] = np.log1p(df['amount'])  # log(1+x) 避免0值问题

2.4.2 类别特征处理

类别特征,我踩过最大的坑是「标签编码」用在无序类别上。比如颜色:红=1,蓝=2,绿=3——模型会以为绿色比红色「大」,这完全没道理。

  • 独热编码——适合无序类别,类别数不要太多
  • 标签编码——适合有序类别,比如学历:小学<中学<大学
  • 目标编码——用目标变量的均值编码,适合高基数类别
# 独热编码
df_encoded = pd.get_dummies(df, columns=['color'], drop_first=True)

# 目标编码(用目标变量均值代替类别)
target_mean = df.groupby('city')['target'].mean()
df['city_encoded'] = df['city'].map(target_mean)

2.4.3 时间特征提取

时间数据,我习惯拆成多个维度:

# 从时间戳提取特征
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['month'] = df['timestamp'].dt.month
df['quarter'] = df['timestamp'].dt.quarter

我的经验:时间特征里,「是否周末」「是否节假日」往往比具体的小时更有预测力。你想想看,用户行为模式在周末和工作日差别很大。

2.5 本章知识体系

下面这张图,是我梳理的数据预处理全流程。嗯,每次做项目我都会对照着走一遍,基本不会漏掉关键步骤。

数据预处理全流程 原始数据 数据清洗 格式统一 重复处理 无效数据 缺失值处理 删除 统计填充 模型预测 异常值检测与处理 Z-Score IQR法 孤立森林 特征工程 数值特征 类别特征 时间特征 交互特征 干净可用的数据

2.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要先做特征工程再做数据清洗——顺序搞反了,特征全是脏数据
  • 不要对测试集做任何数据窥探——填充均值、标准化参数都要从训练集学
  • 不要一次性处理所有缺失值——先分析缺失原因,再对症下药
  • 不要忽略业务背景——我曾经把「-1」当成异常值删了,后来发现人家表示「未填写」

血的教训:数据预处理没有银弹。每个数据集都有自己的脾气,多花时间理解数据,比多跑几个模型有用得多。

好了,这一章的内容就到这儿。数据预处理看着琐碎,但做好了,后面的建模工作会顺畅很多。嗯,记住一句话:好数据,好模型;烂数据,烂模型。


公众号:蓝海资料掘金营,微信deep3321