ML · 章节 4
机器学习 4 — 数据准备
引言:为什么数据准备如此关键
在前面三章中,我们重点介绍了模型本身——线性回归、k-近邻、决策树、随机森林、梯度提升等。然而,在现实的机器学习项目中,数据准备所花费的时间往往远超模型训练。一个广为流传的经验法则是:数据科学家约 80% 的时间用于清理、探索和转换数据,只有约 20% 用于建模与调优。
这并非一个偶然的比例,而是源于一个根本事实:算法只能学习它所看到的数据。如果数据中包含缺失值、错误编码的类别变量、量纲差异巨大的特征,或者目标变量与某个解释变量存在隐性泄漏,那么再先进的模型也无法挽救结果。反过来,经过精心准备的数据往往能让一个简单的模型取得令人惊讶的好表现。
本章将系统介绍机器学习中数据准备的核心环节:缺失值处理、类别变量编码、ColumnTransformer 与 Pipeline、不平衡数据应对(SMOTE、class_weight)、特征工程,以及与之配套的网格搜索调参和数据泄漏防范。我们将以企鹅、泰坦尼克、蘑菇、学生成绩、信用卡欺诈、电信流失、人口普查收入等数据集为例,从一个个真实场景中归纳出方法论。
核心立场:模型很重要,但数据更重要。数据准备不是建模的"附属步骤",而是机器学习项目成败的决定性环节。
一、缺失值处理
1.1 识别缺失值
真实世界的数据集几乎不可避免地存在缺失值。以帕尔默企鹅(Palmer Penguins)数据集为例,这是一份现代版的鸢尾花数据集,记录了南极帕尔默群岛上三种企鹅(Adelie、Chinstrap、Gentoo)的形态测量(喙长、喙深、鳍长、体重)以及岛屿、性别等分类变量。该数据集有意保留了缺失值,因此非常适合教学。
识别缺失值的第一步是逐列统计:
df.isna().sum()
如果想知道有多少行至少包含一个缺失值:
df.isna().any(axis=1).sum()
在 Penguins 数据集中,缺失值常出现在 bill_length_mm、bill_depth_mm、flipper_length_mm、body_mass_g 和 sex 列。它们来自现场观察时无法测量或记录失误的样本。
1.2 策略一:删除行(dropna)
最简单的策略是直接删除含缺失值的行:
df_clean = df.dropna()
优点:实现简单,不引入任何关于数据分布的假设。
缺点:丢失信息;如果缺失并非完全随机(MCAR),可能引入选择偏差。
适用条件:受影响的行数较少,删除后数据集仍足够大。
1.3 策略二:简单插补(fillna)
另一种常见的策略是用合理值替换缺失值:
# 数值列用中位数 df["body_mass_g"] = df["body_mass_g"].fillna(df["body_mass_g"].median()) # 类别列用众数 df["sex"] = df["sex"].fillna(df["sex"].mode()[0])
更进阶的方法包括 KNNImputer(基于近邻样本的值填补)和 IterativeImputer(基于回归模型迭代估计)。在 scikit-learn 中,这些插补器都可以放进 Pipeline,从而避免在训练集和测试集之间泄漏统计量。
关键点:中位数、众数或均值必须在训练集上计算,然后应用到测试集上。如果直接在整个数据集上
fillna再切分,你已经悄悄向训练过程透露了测试集的信息——这是数据泄漏的一种隐蔽形式。
二、类别变量编码
经典的机器学习算法主要处理数值。Penguins 中的 species、island、sex,泰坦尼克号的 Embarked、Sex,蘑菇数据集中清一色的字符串类别——这些都需要编码为数字才能进入模型。
2.1 简单映射
当变量只有少量明确取值时,可以使用 map:
df["sex_num"] = df["sex"].map({"male": 1, "female": 0})
这种方法适用于二值类别,以及顺序无关紧要的场景。
2.2 独热编码(One-Hot Encoding)
对于有 3 个或更多无序模态的变量,独热编码是最常用的方案。它为每个类别创建一列 0/1 指示变量:
df_encoded = pd.get_dummies(df, columns=["island"], drop_first=False)
如果 island 有 3 个值,就会产生 island_Biscoe、island_Dream、island_Torgersen 三列。在 scikit-learn 的 Pipeline 中,推荐使用 OneHotEncoder(handle_unknown="ignore"),这样测试集中出现训练集未见过的类别时不会报错。
为什么独热编码对树模型(决策树、随机森林、梯度提升)特别合适?因为树在每个节点上做的是"该列 ≤ 阈值"或"该列 == 某值"的二元判断,0/1 编码恰好让树能够捕捉每一个类别。
2.3 序数编码(Ordinal Encoding)
当类别本身存在自然顺序(例如学历:小学 < 初中 < 高中 < 本科 < 硕士),可以使用 OrdinalEncoder 将其映射为有序整数。但要警惕:对无序类别使用序数编码会向模型注入虚假的大小关系(线性模型尤其敏感)。
2.4 目标编码与 CatBoost
当某个类别变量基数非常大(State 有 50 个值,Job 有上百种),独热编码会导致维度爆炸。此时可考虑目标编码(用每个类别下目标的均值替代该类别),但这极易引发目标泄漏,需要严格的交叉折叠机制。
CatBoost 是一种专门处理类别变量的梯度提升库。它接受字符串列作为输入,在内部使用带正则的有序目标统计来编码类别——通过随机化数据顺序来避免观察样本"看到自己的目标值",从而抑制泄漏:
from catboost import CatBoostClassifier cat_features = X.select_dtypes( include=["object", "category", "bool"] ).columns.tolist() model = CatBoostClassifier(verbose=False) model.fit(X_train, y_train, cat_features=cat_features)
在蘑菇(mushrooms)这种几乎全为类别变量的数据集上,CatBoost 通常表现优异且无需手工编码。
三、ColumnTransformer 与 Pipeline
真实数据集往往是数值与类别变量的混合体。ColumnTransformer 让我们能够对不同类型的列应用不同的预处理:
from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from sklearn.pipeline import Pipeline num_features = X.select_dtypes(include=["int64", "float64"]).columns.tolist() cat_features = X.select_dtypes(include=["object", "category"]).columns.tolist() num_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()), ]) cat_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", OneHotEncoder(handle_unknown="ignore")), ]) preprocessor = ColumnTransformer([ ("num", num_pipe, num_features), ("cat", cat_pipe, cat_features), ]) model = Pipeline([ ("prep", preprocessor), ("clf", RandomForestClassifier()), ]) model.fit(X_train, y_train)
通过 select_dtypes,我们实现了一个清晰的分区:每一列要么属于数值组,要么属于类别组,既无遗漏也无重复。
关键点:Pipeline 不仅是代码整洁的工具,更是防止数据泄漏的结构性保障。当我们使用
cross_val_score或GridSearchCV时,每一折的均值、中位数、独热编码字典都会在该折的训练子集上重新拟合,而不会污染验证集。
四、不平衡数据
4.1 问题的本质
信用卡欺诈检测(Credit Card Fraud)数据集中,欺诈交易仅占约 0.17%。如果模型简单地预测"全部不是欺诈",其准确率高达 99.83%——但它捕获的欺诈数为零,毫无业务价值。
不平衡数据的危险在于:
- 准确率(accuracy)被多数类主导,失去判别力;
- 模型决策边界向多数类偏移;
- 预测概率校准失真。
正确的评估应使用 召回率(Recall)、精确率(Precision)、F1 分数、ROC-AUC、PR-AUC 等对类别比例敏感的指标。
4.2 分层切分(stratify)
切分训练/测试集时,务必使用 stratify=y 保持类别比例:
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )
注意:stratify 仅保证比例分配,并不会重新平衡类别。
4.3 重采样:欠采样与过采样
imbalanced-learn(imblearn)库提供了完整的重采样工具:
from imblearn.under_sampling import RandomUnderSampler from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN from imblearn.combine import SMOTEENN, SMOTETomek from imblearn.pipeline import Pipeline as ImbPipeline model = ImbPipeline([ ("smote", SMOTE()), ("clf", DecisionTreeClassifier()), ])
- RandomUnderSampler:随机丢弃多数类样本。简单但损失信息。
- RandomOverSampler:简单复制少数类样本。可能加剧过拟合。
- SMOTE(Synthetic Minority Oversampling Technique):在少数类样本之间插值生成新合成样本。仅适用于数值特征。
- ADASYN:SMOTE 的变体,在分类困难的区域生成更多样本。
- SMOTEENN / SMOTETomek:组合方法,SMOTE + 边界清理。
关键点 — SMOTE 只在训练集上做,绝不在测试集上做。否则,合成样本会"污染"测试集,导致评估结果虚高。正确做法:用
imblearn.pipeline.Pipeline包装重采样器和模型,scikit-learn 在交叉验证时会自动只对训练折应用重采样。
4.4 类别权重(class_weight)
无需修改数据,直接在损失函数中加大少数类错误的代价:
DecisionTreeClassifier(class_weight="balanced") RandomForestClassifier(class_weight="balanced")
"balanced" 模式会按类别频率自动计算权重(频率越低,权重越大)。也可以手工指定:
DecisionTreeClassifier(class_weight={0: 1, 1: 10})
XGBoost 没有 class_weight,使用 scale_pos_weight = n_negative / n_positive;LightGBM 提供 is_unbalance=True 或显式 class_weight。CatBoost 提供 auto_class_weights="Balanced"。
4.5 阈值调整
分类器默认以 0.5 为阈值将概率转为类别。在不平衡场景下,这个阈值往往不是业务最优:
y_proba = model.predict_proba(X_test)[:, 1] threshold = 0.3 y_hat = (y_proba > threshold).astype(int)
阈值的选择应根据 PR 曲线 或业务对召回率的具体要求来确定。降低阈值可以提高召回率(抓更多欺诈),但会牺牲精确率(误报增加)。
五、特征工程
5.1 相关性分析
在建模前观察变量间的相关性,既能发现冗余,也能识别与目标强相关的特征:
corr = df[num_features].corr() # 默认 Pearson corr_spearman = df[num_features].corr(method="spearman") import seaborn as sns sns.heatmap(corr, cmap="coolwarm", center=0) sns.clustermap(corr.abs(), cmap="coolwarm", center=0)
- Pearson 测量线性关系,对异常值敏感;
- Spearman 基于排名,捕捉单调但非线性的关系;
- Kendall 评估配对一致性,对小样本和噪声更稳健。
热图直观展示成对相关,聚类图(clustermap)还会按相似度对变量分层聚类,便于发现"特征簇"。
5.2 派生特征
特征工程的核心是把领域知识注入数据。例子:
- 泰坦尼克号:从
SibSp + Parch + 1构造FamilySize,从Name中提取Title(Mr/Mrs/Miss/Master)。 - 时间序列:将日期拆为年、月、日、星期、是否节假日。
- 比值与组合:将
total_charge / total_minutes作为单价;对数变换右偏分布(log1p(income))。
合理的特征工程往往能让简单模型超越未经雕琢的复杂模型。
六、网格搜索调参(GridSearchCV)
模型本身的超参数(树的深度、学习率、正则化系数)对性能影响巨大。GridSearchCV 系统地搜索参数组合,每个组合都通过交叉验证评估:
from sklearn.model_selection import GridSearchCV param_grid = { "clf__n_estimators": [100, 300, 500], "clf__max_depth": [None, 5, 10, 20], "clf__min_samples_split": [2, 5, 10], } grid = GridSearchCV( model, param_grid, cv=5, scoring="f1", n_jobs=-1, ) grid.fit(X_train, y_train) print(grid.best_params_, grid.best_score_)
关键点:
- 在 Pipeline 中,参数名形如
"<step>__<param>"(双下划线); - 不平衡分类应使用
scoring="f1"、"roc_auc"或"average_precision",而非"accuracy"; - 当组合数量过大时,使用
RandomizedSearchCV抽样搜索更高效; - 现代 AutoML(Optuna、Hyperopt)采用贝叶斯优化,效率更高。
七、数据泄漏(Target Leakage)
7.1 什么是数据泄漏
数据泄漏是指模型在训练过程中利用了未来信息或目标本身的信息,导致训练时性能虚高,但在真实部署时表现急剧下降。这是机器学习项目中最隐蔽、也最致命的陷阱之一。
核心警告 — 数据泄漏会让你的模型"在评估时极佳,在生产时失败"。一个 99% 准确率的训练结果如果与真实业务表现严重不符,第一时间应怀疑数据泄漏,而不是为成功而欢呼。
7.2 典型场景
学生成绩(Student Performance)数据集是经典案例。目标 G3 是学年末的最终成绩,而特征中包含 G1(第一阶段成绩)和 G2(第二阶段成绩)。G2 与 G3 的相关性接近 0.9——保留它们,模型 R² 轻松达到 0.85 以上;一旦移除:
df = df.drop(columns=["G1", "G2"])
R² 跌至 0.2 左右。原因显而易见:G1 和 G2 实际上"几乎就是" G3,但在真实场景下,如果我们想"提前预测期末成绩",我们当然不会有 G2 可用。
其他常见泄漏:
- 在缺失值插补、标准化前没有先切分训练/测试集;
- 时间序列任务用未来值预测过去;
- 特征中包含目标变量经过简单变换得到的列;
- 类别编码使用了全数据集的目标统计。
7.3 防范措施
- 先切分,再做任何处理;
- 所有插补、缩放、编码必须封装在
Pipeline中; - 仔细审视每个特征:它在预测时刻是否真的可用?
- 如果模型表现"好得不像话",怀疑泄漏。
八、学习曲线
学习曲线展示训练集大小或训练轮次与性能之间的关系,是诊断欠拟合和过拟合的有力工具:
from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores = learning_curve( model, X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 10), scoring="accuracy", )
读图要点:
- 训练曲线和验证曲线都低且接近 → 欠拟合(模型容量不足),应增加复杂度或特征;
- 训练曲线高,验证曲线明显低 → 过拟合,应增加正则、增加数据或减小复杂度;
- 两条曲线随数据量增加在收敛 → 增加数据可能仍有收益;
- 曲线已平稳 → 增加数据已无明显帮助,应改进模型或特征。
类似地,验证曲线(validation_curve)展示某个超参数取不同值时的训练/验证表现,有助于直接定位最佳超参数区间。
练习
- 企鹅清洗对比:在 Penguins 数据集上,分别尝试
dropna、中位数插补、KNNImputer,在测试集上比较随机森林的 macro-F1。 - 泰坦尼克 Pipeline:为泰坦尼克号数据集构造一个完整的
ColumnTransformer + Pipeline,包含数值插补与缩放、类别独热编码,以及RandomForestClassifier。报告测试 AUC。 - 蘑菇可食用性:在 Mushrooms 上分别使用
pd.get_dummies + RandomForest与CatBoost(cat_features=...),比较训练时间、预测准确率和泛化表现。讨论:你是否真的会把生命交给一个机器学习模型? - 学生成绩泄漏实验:在 Student Performance 上,先保留
G1/G2训练 XGBoost,再移除它们重新训练。报告 R² 差异,并撰写 100 字以内的批判性分析。 - 欺诈检测组合:在 Credit Card Fraud 上分别测试 (a) 基线决策树、(b)
RandomUnderSampler+ 决策树、(c)SMOTE+ 决策树、(d)class_weight="balanced",以及 (e) 阈值从 0.5 调整到 0.3。绘制每种方案的混淆矩阵和 ROC 曲线。 - 客户流失对比:在 Telco Churn 上对比逻辑回归、随机森林(class_weight)、CatBoost(auto_class_weights)。哪种方法在保持精确率的同时召回率最高?
- GridSearch 调参:对成人收入(Adult Census Income)数据集上的 XGBoost 进行网格搜索,搜索
n_estimators、max_depth、learning_rate,使用 5 折交叉验证和roc_auc作为评估指标。 - 学习曲线诊断:为练习 7 中的最佳模型绘制学习曲线,判断它是否在过拟合,并提出改进方案。
拓展阅读
- Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O'Reilly. 第 2 章关于端到端项目的数据准备,第 8 章关于降维。
- Kuhn, M., & Johnson, K. (2019). Feature Engineering and Selection: A Practical Approach for Predictive Models. CRC Press. 在线版本:http://www.feat.engineering
- scikit-learn 官方文档:Preprocessing data、Pipelines and composite estimators、Imputation of missing values。
- imbalanced-learn 官方文档:https://imbalanced-learn.org。包含 SMOTE、ADASYN、组合方法的详细原理与示例。
- CatBoost 文档:https://catboost.ai/docs/。重点阅读 Ordered Target Statistics 章节,理解 CatBoost 如何避免类别编码中的泄漏。
- Kaggle Learn 提供免费微课程 Data Cleaning 与 Feature Engineering,包含可在线运行的练习。
- Chawla, N. V., et al. (2002). SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research, 16, 321-357. SMOTE 的原始论文。
- Kapoor, S., & Narayanan, A. (2023). Leakage and the reproducibility crisis in machine-learning-based science. Patterns, 4(9). 系统综述科研中的数据泄漏问题。