⚙️ 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:深度清洗

质检初筛是"看病发报告",深度清洗是"动手治病"。同一个问题(缺失值、异常值),质检初筛阶段只统计不修改,深度清洗阶段才根据决策落地处理。

🔍 质检初筛 vs 深度清洗

💡 一句话:质检初筛是看病发报告——同一个问题(缺失值、异常值)只统计不修改;深度清洗是动手治病——根据报告+业务决策落地处理。
🩺 质检初筛 —— 只扔"真垃圾"
• 两件事:检查(出质检报告)+ 初筛(扔真垃圾)
• 真垃圾 = 不可救药:物理重复(完全一样)、文件损坏(打不开)、全黑全白、分辨率 < 32×32、CSV 完全空行、音频时长 < 0.1 秒、时序全 NaN
• 初筛不扔"可修复"的:缺 8% 不是 100% 空、5 条异常值不是整列都错、图像有噪声不是全黑、音频有底噪不是损坏
• 初筛也绝不修:缺多少、异常多少只统计数量写进报告,原样留给深度清洗
🔧 深度清洗(预处理阶段)
• 修为主,慎删——能修复的不丢,只有确实没用才删
• 决策依据:质检报告 + 业务理解 + 统计分布
• 典型动作:填缺失值、截异常值(盖帽)、格式转换、去噪
• 绝对不能做:用全量数据 fit 清洗器(会数据泄漏)

💡 完整链条:为什么质检了又标注了,还要深度清洗?

这是故意的分层设计——三步各有职责,前两步故意不做修复,把修复留给第三步:
🩺 ① 质检初筛 (数据工程阶段,已完成)
做了:扔真垃圾(物理重复、损坏文件、全黑全白、分辨率过低)+ 出质检报告(缺失率 8%、异常值 12 个、格式错误 23 条)
故意没做:缺失值不填、异常值不盖帽、格式不转、噪声不去——只统计数量原样记录进报告
🏷️ ② 标注 (人工/半自动,已完成)
做了:给每条数据打 label(框对猫、情感正负、这是异常点)
故意没做:标注员眼里只看 label 对不对,不填 NaN、不删 HTML、不转 EXIF——这些是脏东西,但人眼能自动跳过
🔧 ③ 深度清洗 ← 当前就在做这步 (预处理阶段)
现在才做:拿着质检报告(缺失率 8% 建议中位数填充;异常值 12 个建议盖帽)+业务理解(标签列的 NaN 必须 dropna),才开始动手修复
为什么不能提前做:修复需要统计分布(用什么值填?3σ 阈值多少?)和业务决策(删还是修?)——这不是初筛或标注阶段能拍板的

🔑 核心边界:初筛到底扔什么、留什么?(5 模态具体例子)

模态 🗑️ 初筛直接扔(不可救药的真垃圾) 📝 初筛统计但留着(可修复的,交给深度清洗)
📊 表格整行完全空白 / 完全重复的副本 / 文件损坏打不开 / 全部字段只有 NaNvalue 列 8% 缺失 / 12 个异常值 / 日期格式不统一(5种写法)/ 分类值有别名
🖼️ 图像文件损坏 / 全黑 / 全白 / 分辨率 < 32×32 / 完全重复像素EXIF 方向标记导致歪 90° / 有高斯噪声 / CLAHE 才能看清的暗光 / 轻微压缩伪影
📝 文本空字符串 / 完全重复的评论(刷量)/ 无法解码的乱码HTML 标签残留 / URL 和 @用户名 / Emoji 要不要留 / 繁简混杂
🎵 音频文件损坏 / 时长 < 0.1 秒 / 全静音 / 全部段完全相同首尾各 2 秒静音 / 空调底噪 -35dB / 采样率 8kHz 和 16kHz 混杂 / 双声道要转单声道
📈 时序文件损坏 / 全部只有一个值 / 只有 2 个数据点 / 时间戳乱序且无法还原value 列 5% 缺失(但不是全空)/ 秒级分钟级混杂 / 有 3 个尖峰异常 / 方差不平稳

🧐 具体到每个模态——前两步留下了什么,这一步要清什么:

📊 表格
初筛报告缺 8% value 列;标注员照样标 label;现在模型报 Input contains NaN
clean_missing / clean_outlier
🖼️ 图像
初筛只扔全黑图;标注员能框歪图的猫;现在模型学了歪的特征
img_align / img_denoise
📝 文本
初筛不碰格式;标注员忽略 HTML/URL;现在 TF-IDF 把 div 当高频词
txt_regex / txt_jieba
🎵 音频
初筛只扔损坏文件;标注员听到哭声就行;现在 Mel 频谱里静音占 30%
aud_silence / aud_denoise
📈 时序
初筛只报告缺失数;标注员看出异常点标 label;现在 ARIMA 要求等距索引
ts_interpolate / ts_diff
一句话总结:初筛 = 体检(只出报告),标注 = 给病人贴身份标签(谁是谁),深度清洗 = 主治医生(拿着报告 + 标签,动手治病)。跳过前两步直接修 = 瞎修,只做前两步不修 = 病人还是有问题。

🧰 各模态深度清洗方法

点击切换不同数据类型,查看各自的清洗重点和代码示例

📊 表格数据深度清洗 修为主,慎删

🕳️缺失值智能填充
先判断缺失率 + 缺失类型,再选方法——少量删行,中量填值,大量删列
年龄缺失 2 行 → 中位数 30 填充 · 标签缺失 1 行 → 直接删(标签不能猜)
🔢异常值盖帽(Winsorize)
不丢样本,截断到 IQR / 业务合法范围
年龄 -45 → clip 到 0 · 年龄 999 → clip 到 120 · 金额用 1.5×IQR 盖帽
📐格式/类型统一
标签编码 + 正则清洗 + 日期归一
label 是/否 → 1/0 · 手机号非法值 maybe → 删行 · 金额 $1,200 → 1200.0
🆔去重 + 业务规则校验
物理重复必删 + 主键重复保最新 + 业务范围过滤
全表 drop_duplicates → 删 1 行 · user_id 重复 002 → keep='last' 保留

🎯 交互式演练:一份脏数据的深度清洗

点击任意一行查看具体清洗方案和代码:

ID 姓名 年龄 手机号 金额 🎯 是否流失(label) → 清洗后
001张三281380013800150000✓ 正常
002李四321391234567880001⚠️ 重复
002李四321391234567880001⚠️ 重复
003王五NaN137987654326500是⚠️ 缺+格式
004赵六-451360000111192000⚠️ 异常
005钱七24123453800maybe⚠️ 非法
006孙八301355555666671000✓ 正常
010冯十二99913612345678105001⚠️ 异常
013卫十五NaN138888899995100NaN⚠️ 双缺

🛠️ 一键运行:完整表格深度清洗 Pipeline pip install pandas numpy scikit-learn

import pandas as pd
import numpy as np

# ═══════════════════════════════════════════
# 一键运行:完整深度清洗 Pipeline
# 覆盖 4 张卡片:缺失值 · 盖帽 · 格式统一 · 去重
# 依赖:pip install pandas numpy scikit-learn
# ═══════════════════════════════════════════

# ====== Step 0: 读取数据 ======
# 🎯 先明确标签列(监督学习必须)
TARGET = "churn"
df = pd.read_csv("dirty_users.csv")

# ====== Step 1: 加载 + 去重 ======
df.drop_duplicates(inplace=True)

# ====== Step 2: 标签优先清洗(标签绝对不能瞎填!)======
# 2a. 格式统一:是/否/Yes/No → 1/0
df[TARGET] = df[TARGET].replace({"是":1, "否":0,
                                  "Yes":1, "No":0})
# 2b. 非法值 + 缺失 → 直接删行
df = df[df[TARGET].isin([0, 1])]
df.dropna(subset=[TARGET], inplace=True)

# ====== Step 3: 特征列缺失值智能填充(跳过标签列)======
feature_cols = [c for c in df.columns if c != TARGET]
for col in feature_cols:
    rate = df[col].isna().mean()
    if rate < 0.05:
        df.dropna(subset=[col], inplace=True)
    elif rate < 0.3:
        fill = df[col].median() if df[col].dtype != "object" \
               else df[col].mode()[0]
        df[col].fillna(fill, inplace=True)
    else:
        df.drop(columns=[col], inplace=True)

# ====== Step 4: 特征异常值盖帽(跳过标签列)======
for col in df.select_dtypes(include=[np.number]).columns:
    if col == TARGET: continue
    Q1, Q3 = df[col].quantile(0.25), df[col].quantile(0.75)
    IQR = Q3 - Q1
    df[col] = df[col].clip(Q1-1.5*IQR, Q3+1.5*IQR)
df["age"] = df["age"].clip(0, 120)

# ====== Step 5: 输出干净数据 ======
df.to_csv("clean_users.csv", index=False)

✅ 深度清洗 Checklist

⚠️ 清洗阶段的"数据泄漏"红线

清洗时绝对不能用全量数据来计算填充值、异常值阈值——这会把测试集的信息"泄漏"进训练过程。
正确做法:先划分 train/test → 再在 train 上 fit 清洗器 → transform 到 test
# ❌ 错误:全量 fit 会泄漏
imputer = KNNImputer()
df_all = imputer.fit_transform(df)  # test 的信息被用来填充 train

# ✅ 正确:先划分,再在 train 上 fit
X_train, X_test, y_train, y_test = train_test_split(df, label)
imputer = KNNImputer()
X_train_clean = imputer.fit_transform(X_train)  # 只用 train 计算
X_test_clean = imputer.transform(X_test)          # 用 train 的结果转换 test

✂️ 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,有序 → Ordinal,高基数 → Target Encoding
颜色[红/绿/蓝] → 3 列 One-Hot · 尺寸[S/M/L/XL] → Ordinal 0-3 · 城市 100+ 类 → 目标均值替代
📉降维与特征选择
PCA 保留 95% 方差 + 随机森林重要性排序取 Top N
200 特征 → 50 主成分 · RandomForest 输出 feature_importances_ · 只在训练集 fit
🔗交叉特征(业务洞察)
两个特征组合 = 新信号,捕捉"1+1>2"的关系
年龄×消费额 → VIP 识别 · 总价÷数量 → 客单价 · 购买天数差 → 复购周期
⏰时间特征提取
日期/时间本身就是最强的特征来源
date → year/month/day_of_week/is_weekend/hour · 永远先 to_datetime
⚠️特征工程绝对不能犯的错(数据泄漏重灾区)
  • ❌ 用测试集信息做特征(如用全量数据 fit PCA、计算均值)
  • ❌ 把 ID 列(user_id / order_id)当特征 —— 模型会记住 ID 而非真正学到模式
  • ❌ 缺失值用测试集均值 / 中位数填充
  • ❌ 标准化(StandardScaler / MinMaxScaler)时用测试集计算 mean/std / min/max

📏 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 区间,差异很小
← 返回数据工程