Aller au contenu principal

机器学习 4 — 数据准备

:::tip Kaggle 笔记本 本章的完整可执行代码在 Kaggle 上:打开 →

法语和英语版本可在首页查看。 :::

在现实生活中,数据集很少是干净的。缺失值、类别变量、不平衡类别、目标泄漏……本章涵盖将原始数据转换为模型可以处理的内容所需的一切。

为什么要学这一章?

您将学到:

  • 处理缺失值(删除 vs 填补);
  • 编码类别变量(One-Hot、序数、CatBoost 原生);
  • 使用 ColumnTransformer + Pipeline:工业级模式;
  • 识别和避免目标泄漏
  • 处理不平衡类别(SMOTE、class_weight、PR 曲线);
  • 使用 GridSearchCV 调整超参数。

缺失值

第一个反应:定位 NaN 在哪里。

df.isna().sum() # 每列的 NaN 数量
df.isna().any(axis=1).sum() # 至少有一个 NaN 的行数

两种主要策略:

1. 删除不完整行dropna)。简单,但会丢失数据,如果缺失值不是随机的可能引入偏差。

2. 填补(用合理的值替换)。对于数值型:中位数(比平均值对离群值更稳健)。对于类别型:众数(最频繁的值)。

from sklearn.impute import SimpleImputer

num_imputer = SimpleImputer(strategy='median')
cat_imputer = SimpleImputer(strategy='most_frequent')

编码类别变量

机器学习算法期望数字。species = "Adelie"island = "Biscoe" 这样的列必须转换。

One-Hot 编码

对于具有 kk 种取值的变量,创建 kk 个二进制列。pd.get_dummies 是最简单的:

df_encoded = pd.get_dummies(df, columns=['island'])

在生产环境中,更倾向于使用 scikit-learn 的 OneHotEncoder 配合 handle_unknown='ignore':如果测试时出现未见过的类别,模型不会崩溃。

:::warning 不要随意使用整数编码 将取值编码为 1, 2, 3 暗示了类别之间的顺序距离。这对岛屿、颜色等是错误的。对名义变量始终使用 One-Hot。 :::

ColumnTransformer + Pipeline

在同一 Pipeline 中处理数值和类别特征的工业级模式:

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer

num_features = X.select_dtypes(include='number').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')),
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])

preprocessor = ColumnTransformer([
('num', num_pipe, num_features),
('cat', cat_pipe, cat_features),
])

model = Pipeline([
('prep', preprocessor),
('classifier', RandomForestClassifier()),
])

优点:无数据泄漏、处理未见过的类别、代码简洁、与 cross_val_scoreGridSearchCV 兼容。

目标泄漏:陷阱

设想一个 Student Performance 数据集,包含 G1G2(学期成绩)作为特征,G3(最终成绩)作为目标。

第一个反应:把所有列都给模型。得到 R² = 0.95

太好了,不可能是真的。G1G2 几乎就是目标——知道前两个学期的成绩可以平凡地预测最终成绩。

这就是目标泄漏:我们将一个在实际预测时不应可用的变量纳入了特征。没有 G1G2,R² 降至 0.2-0.3——这才是真实性能。

:::warning 应养成的反射 对每个变量,问自己:"当我真的要预测时,这个变量真的可用吗?"如果不是,删除它。 :::

不平衡类别

在信用卡欺诈数据集上,仅 0.17% 的交易是欺诈。一个总是说"非欺诈"的模型有 99.83% 的准确率,但毫无用处。

三种策略

1. 欠采样(Undersampling):减少多数类。

from imblearn.under_sampling import RandomUnderSampler
sampler = RandomUnderSampler()
X_res, y_res = sampler.fit_resample(X, y)

2. 过采样(Oversampling)SMOTE 通过邻居之间的插值生成合成点。

from imblearn.over_sampling import SMOTE
sampler = SMOTE()
X_res, y_res = sampler.fit_resample(X, y)

3. 加权:告诉模型对少数类的错误更重的惩罚。

from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(class_weight='balanced')

对于 XGBoost,是 scale_pos_weight = n_negative / n_positive

适应的指标

准确率在不平衡类别上无意义。优先选择:

  • 每类的精确率/召回率/F1(参考 classification_report
  • PR 曲线而非 ROC(FP 被稀释时 ROC 过于乐观)
  • 显式的混淆矩阵

train_test_split 中的 stratify

为了在训练集和测试集中保留类别比例:

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)

GridSearchCV:调整超参数

不要凭直觉设置 max_depth=5GridSearchCV 系统地尝试网格中的所有组合,并通过交叉验证返回最佳:

from sklearn.model_selection import GridSearchCV

param_grid = {
'n_estimators': [100, 200],
'max_depth': [None, 5, 10],
'min_samples_split': [2, 5],
}

grid = GridSearchCV(
RandomForestClassifier(class_weight='balanced'),
param_grid,
scoring='f1', # 不平衡类别上用 F1 而非 accuracy
cv=5,
n_jobs=-1,
)
grid.fit(X_train, y_train)
print(grid.best_params_)

对于非常大的网格,RandomizedSearchCV 进行采样而不是枚举。

CatBoost:原生类别型 boosting

当数据集主要由类别变量主导(Mushrooms、Adult Census)时,CatBoost 原生处理它们,无需手动 One-Hot 编码。

from catboost import CatBoostClassifier

model = CatBoostClassifier(iterations=500, learning_rate=0.05, depth=6, verbose=False)
model.fit(X_train, y_train, cat_features=cat_indices)

CatBoost 不使用 One-Hot,而是为每个类别使用正则化的目标统计,并在随机顺序上计算以防止信息泄漏。


Kaggle 上的完整笔记本(可分叉)→