机器学习 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 编码
对于具有 种取值的变量,创建 个二进制列。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_score 和 GridSearchCV 兼容。
目标泄漏:陷阱
设想一个 Student Performance 数据集,包含 G1 和 G2(学期成绩)作为特征,G3(最终成绩)作为目标。
第一个反应:把所有列都给模型。得到 R² = 0.95。
太好了,不可能是真的。G1 和 G2 几乎就是目标——知道前两个学期的成绩可以平凡地预测最终成绩。
这就是目标泄漏:我们将一个在实际预测时不应可用的变量纳入了特征。没有 G1 和 G2,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=5,GridSearchCV 系统地尝试网格中的所有组合,并通过交叉验证返回最佳:
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,而是为每个类别使用正则化的目标统计,并在随机顺序上计算以防止信息泄漏。