⚙️ Stage 03 · 数据预处理与特征工程

让模型吃干净的数据

数据预处理 = 深度清洗 → 划分 → 特征工程 → 标准化(防数据泄漏)。脏数据喂不出好模型,这一步决定训练效率和上限。

🔄 数据预处理完整流水线(4 步)

从"脏数据集"到"模型可吃的干净数据"——清洗 → 划分 → 特征工程 → 标准化(防数据泄漏)

1
🧹
STEP 1
深度清洗
异常值/去噪/缺失填充
2
✂️
STEP 2
数据集划分
Train/Val/Test/K折
3
STEP 3
特征工程
编码/降维/特征选择
4
📏
STEP 4
标准化归一化
MinMax/Z-Score/Robust
⚠️
为什么先划分再做特征工程/标准化?防止数据泄漏——特征选择、降维(PCA)、StandardScaler 等操作必须只用训练集 fit,再 transform 验证集/测试集。如果对全量数据先做,测试集的统计信息会泄漏到训练过程中,评估分数会虚高。
💡
验证集(val)怎么来?调参需要一个独立的验证集,有两种方式: Hold-out:从训练集里切出 10-20% 做 val,简单快速,适合大数据集。 K 折交叉验证:把训练集 K 等分,轮流用 K-1 份训练、1 份验证,平均 K 次分数选最优参数。数据量小时更稳,不浪费数据。选完最优参数后,用全部训练集重训一遍,再到测试集上做唯一一次最终评估。
📌
数据集划分 · 归纳总结
🔹 什么时候切?清洗之后、特征工程/标准化之前,永远保持 划分 → 特征工程 → 训练 的顺序
🔹 怎么切?普通数据用 train_test_split + stratify;时间序列按先后顺序切;有分组(user_id)用 GroupShuffleSplit
🔹 分几份?一般 train 80% + test 20%;大数据集 test 1-5% 足够;官方给 train/test 时 val 要从 train 里再切
🔹 调参用什么?小数据用 K 折交叉验证(5/10 折,不浪费数据),大数据用 Hold-out 切 val 即可;K 折时特征工程也要放进 Pipeline,每一折内部各自 fit
🔹 黄金规则:train 是 唯一的 fit 源,所有 scaler/encoder/pca 都只在 train 上 fit,transform 到其他集;test 集 只在最后评估一次

🧹 Step 1:深度清洗

深度清洗 ≠ 质检初筛:初筛在数据工程阶段做"及格线检查",深度清洗是"打磨抛光"——处理异常值、降噪、智能填充缺失值

异常值检测与处理

IQR 法(四分位距)
对箱线图的"胡须"外数据点处理
Q1, Q3 = df.quantile(0.25), df.quantile(0.75)
IQR = Q3 - Q1
mask = (df < Q1 - 1.5*IQR) | (df > Q3 + 1.5*IQR)
df[mask] = np.nan  # 标记为 NaN 后续填充
Z-Score 法
适合正态分布数据,|Z| > 3 视为异常
from scipy import stats
z = np.abs(stats.zscore(df))
df_clean = df[(z < 3).all(axis=1)]
Isolation Forest
无监督 ML 方法,适合高维数据
from sklearn.ensemble import IsolationForest
iso = IsolationForest(contamination=0.05)
df['anomaly'] = iso.fit_predict(df)

缺失值智能填充

删除法(简单但有损失)
缺失率 < 5% 时可用
df.dropna(subset=['key_column'], inplace=True)
df.dropna(axis=1, thresh=len(df)*0.8, inplace=True)  # 列删除
统计填充(快速常用)
数值用均值/中位数,分类用众数
df['age'].fillna(df['age'].median(), inplace=True)
df['city'].fillna(df['city'].mode()[0], inplace=True)
KNN 填充(智能但慢)
用相似样本的值填充,适合有相关性的数据
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df_imputed = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)

重复值删除

# 完全重复
df.drop_duplicates(inplace=True)

# 基于特定列的重复
df.drop_duplicates(subset=['user_id', 'timestamp'], keep='first', inplace=True)
⚠️ 注意:业务场景中"重复"可能有意义(如多次购买行为),不要盲目删除

文本/图像去噪

针对特定模态的额外清洗
📝 文本清洗
import re
# 去 HTML 标签、特殊字符、多余空格
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'[^\w\s一-鿿]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
🖼️ 图像去噪
import cv2
# 高斯滤波
denoised = cv2.GaussianBlur(image, (3, 3), 0)
# 去噪点
denoised = cv2.medianBlur(image, 3)

深度清洗 Checklist

✂️ Step 2:数据集划分

关键原则:**测试集必须"没见过"**——调参在验证集上做,测试集只用于最终评估。不能把同一数据同时放进训练和测试集。

📐 常见划分比例

经典分割70:15:15
大数据集90:05:05
小数据集 + K折K=5
■ 训练集 (Train)
■ 验证集 (Val)
■ 测试集 (Test)

📌划分策略 · 归纳总结

核心原则
① 初始只分 train + test,test 锁死不动
② 调参时从 train 里 临时切 val(用完可还回去)或用 K 折交叉验证
③ 最终模型用 全部 train_full 重训,test 只评估 一次
Val vs K折:怎么选?
🔹 数据大(> 10万)→ 临时切 val,简单快速
🔹 数据小(< 10万)→ K 折交叉验证,不浪费数据
🔹 深度学习 → 必须有固定 val 做 early stopping
🔹 Kaggle 竞赛 → K 折选模型,test 是官方隐藏的
避坑指南
❌ 别用 test 集反复调参 → test 变成了 val
❌ 大数据集别给 test 留 30% → 1-5% 够了
❌ 时间序列别用 train_test_split 打乱 → 泄漏未来信息
❌ K 折时特征工程别在全量数据先 fit → 泄漏
分阶段划分流程
Step 1:全量 → train_full + test(test 锁死)
Step 2:train_full → 临时切 val(调参用)
Step 3:调完参 → val 扔回 train_full
Step 4:最优参数 + train_full 重训
Step 5:test 评估 → 最终分数

💻 划分代码

from sklearn.model_selection import train_test_split

# 两次分割:先切出测试集,再切验证集
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.15,
    random_state=42,
    stratify=y           # 分层抽样!保证类别比例一致
)
X_train, X_val, y_train, y_val = train_test_split(
    X_temp, y_temp, test_size=0.1765,
    random_state=42,
    stratify=y_temp
)
# 0.85 * 0.8235 ≈ 0.70 (训练集 70%)
# 0.85 * 0.1765 ≈ 0.15 (验证集 15%)
⚠️ stratify=y 一定要加!分类任务中如果某类只占 5%,不加分层可能切出一个完全不含该类的测试集。

🔁 K折交叉验证(小数据集必备)

当数据不够大时,单次划分结果波动大。K折交叉验证让每一条数据都轮流当训练和验证。
1 2 3 4 5 Fold 1: 4,5 验证 | 1,2,3 训练
1 2 3 4 5 Fold 2: 3,5 验证 | 1,2,4 训练
1 2 3 4 5 Fold 3: 2,5 验证 | 1,3,4 训练
1 2 3 4 5 Fold 4: 1,5 验证 | 2,3,4 训练
1 2 3 4 5 Fold 5: 1,2,3,4 训练 | 5 验证
最终分数 = 5 折的平均分数 ✅ 更稳定可靠

✨ Step 3:特征工程

"数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已" — 特征工程是投入产出比最高的环节

🏷️类别特征编码

One-Hot 编码(无序关系)
颜色: [红, 绿, 蓝] → 3 个 0/1 列
红 → [1, 0, 0]
绿 → [0, 1, 0]
蓝 → [0, 0, 1]
pd.get_dummies(df['color'], prefix='color')
Label Ordinal 编码(有序)
尺寸: [S, M, L, XL] → 保留大小关系
S → 0   M → 1   L → 2   XL → 3
from sklearn.preprocessing import OrdinalEncoder
enc = OrdinalEncoder(categories=[['S','M','L','XL']])
Target Encoding(高基数类别)
城市 100+ 个类别 → 用每类的目标均值替代
df['city_enc'] = df.groupby('city')['target'].transform('mean')

📉降维与特征选择

PCA 主成分分析(线性降维)
200 个特征 → 50 个主成分(保留 95% 方差)
from sklearn.decomposition import PCA
pca = PCA(n_components=0.95)  # 自动选保留 95% 方差的维度
X_pca = pca.fit_transform(X)
✅ 降噪 + 加速训练 + 可降过拟合
特征重要性排序
用随机森林 / XGBoost 输出 feature_importances_
rf = RandomForestClassifier()
rf.fit(X_train, y_train)
# 排序取 Top N
importances = pd.Series(rf.feature_importances_,
                         index=X_train.columns)
top_features = importances.nlargest(20).index
⚠️ 特征工程绝对不能犯的错
  • ❌ 用测试集信息做特征(数据泄露)
  • ❌ 把 ID 列(用户ID、订单ID)当特征
  • ❌ 缺失值用测试集均值填充
  • ❌ 标准化时用测试集计算 mean/std

🔗交叉特征(业务洞察)

把两个特征的组合作为新特征,捕捉"1+1>2"的关系
年龄 × 消费额 → 年轻用户高消费(VIP 识别)
城市 × 产品类别 → 不同区域偏好
购买天数差 → 复购周期
总价 ÷ 数量 → 客单价

时间特征提取

日期/时间本身就是最强的特征来源
import pandas as pd

df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
df['is_weekend'] = df['day_of_week'] >= 5
df['hour'] = df['date'].dt.hour

📏 Step 4:标准化与归一化

为什么要做?不同特征量纲差异大(如年龄 18-80 vs 收入 3k-500k),模型会被大数值特征"绑架"。树形模型(XGBoost/随机森林)对尺度不敏感,但线性模型、神经网络、SVM 必须做。

MinMaxScaler(归一化到 [0,1])

公式: x' = (x - min) / (max - min)
所有数据被压缩到 [0, 1] 区间,原始的分布形态完全保留。
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)  # 用训练集的 min/max
⚠️ 对异常值敏感——一个极端值会压扁整个分布

📊 交互式效果演示

原始数据:[12, 18, 25, 30, 45, 60, 200] — 注意 200 这个异常值的影响
原始值
MinMax 归一化([0,1])
⚠️ 正常数据全部被压到 0~0.26 区间,差异很小
← 返回数据工程