teach.pascalyim.com
目录

ML · 章节 5

第 5 章 综合练习

在 Kaggle 上打开

经过前四章的学习,我们已经依次接触到机器学习的三大支柱:回归(第 2 章)、分类(第 3 章)以及模型评估与正则化(第 4 章)。每一章我们都在精心挑选的小型数据集上反复打磨同一个工作流程:加载数据、探索分布、清洗缺失值、编码分类变量、划分训练/测试集、拟合模型、评估表现。

本章不再引入新的算法,而是把这套工作流程整体地搬到四个真实数据集上。这四个数据集来自 Kaggle 的经典竞赛或公开仓库,规模与噪声水平远高于教学用的小例子。它们的共同点是:都需要使用前面章节中介绍过的工具,但又都各自暴露出某种"学习上的痛点"——高维稀疏特征、严重不平衡的标签、几十种异质的列、像素级输入。读者将在解决这些问题的过程中,把零散的知识串联成完整的方法论。

学习目标

  • 在不同体量、不同结构的数据集上独立完成一个完整的机器学习项目。
  • 学会根据数据集的特性选择合适的模型与评估指标,而不是机械地套用同一个流程。
  • 体会预处理(缺失值、编码、归一化)在真实场景中的关键作用。
  • 巩固 train_test_splitfit/predict、交叉验证、正则化等核心 API。

本章共包含四个数据集的练习,难度递进。每一节先介绍数据集的来源与商业/科学背景,然后说明练习的目标,最后给出关键提示与代码片段。读者应当先合上参考代码,独立尝试,遇到困难时再回头查看提示。


5.1 梅赛德斯-奔驰:绿色制造(mercedes_test.csv)

5.1.1 数据集背景

本数据集来自 Kaggle 上的 Mercedes-Benz Greener Manufacturing 竞赛。梅赛德斯-奔驰在量产前会对每一种车辆配置进行测试台测试,以验证传动、电子、机械等子系统是否满足规范。这些测试耗时且耗能,因此厂商希望事先预测某种配置在测试台上花费的时间,从而提前调度并削减能耗。

5.1.2 数据结构

每一行对应一种车辆配置。特征数高达数百,结构相当特殊:

  • 8 个分类变量 X0, X1, X2, X3, X4, X5, X6, X8:发动机、传动、机械/电子组件、设计变更等代码,由字母 a, b, c, ... 编码。
  • 约 370 个二元变量 X10X385:取值为 0 或 1,表示某项技术选项、兼容性标志或子组件的存在与否。

这种"小样本、高维度、几乎全是哑变量"的结构正是正则化(Lasso、Ridge)大显身手的场景。普通最小二乘回归在这里几乎注定过拟合。

5.1.3 练习目标

  1. 加载 mercedes_test.csv 并完成初步探索:行数、列数、缺失值、y 的分布。
  2. 对 8 个字母编码的分类变量进行编码(推荐 OrdinalEncoderOneHotEncoder)。
  3. 划分训练集与测试集,依次拟合:线性回归、Ridge、Lasso,并比较 R² 与均方根误差。
  4. 利用 Lasso 的稀疏性筛选出真正有用的特征,观察系数为零的变量数量。

5.1.4 关键提示

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.preprocessing import OrdinalEncoder df = pd.read_csv("mercedes_test.csv") y = df["y"] X = df.drop(columns=["y", "ID"])
# 仅对字母编码的列做 OrdinalEncoder cat_cols = ["X0", "X1", "X2", "X3", "X4", "X5", "X6", "X8"] X[cat_cols] = OrdinalEncoder().fit_transform(X[cat_cols])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = Lasso(alpha=0.1) model.fit(X_train, y_train) print((model.coef_ == 0).sum(), "个特征被剔除")

注意:因为特征量级差异巨大(0/1 与字母编码混杂),建议在 Lasso 之前对数据进行 StandardScaler 标准化,否则惩罚不公平。


5.2 中风预测(stroke.csv)

5.2.1 数据集背景

这是一个医学风险预测数据集,目标是根据人口统计、病史与生活习惯预测某人是否会发生中风stroke = 0/1)。它的现实意义在于辅助医生进行筛查,把有限的医疗资源优先分配给高风险人群。

5.2.2 列与含义

类别变量含义
标识符id唯一编号,建模前应丢弃
目标stroke1 = 中风,0 = 正常
人口统计gender, age, Residence_type性别、年龄、城市/农村
病史hypertension, heart_disease高血压、心脏病(0/1)
社会经济ever_married, work_type婚姻、职业类型
生活方式smoking_status吸烟状态(注意 Unknown 是伪装的缺失值)
生物指标avg_glucose_level, bmi平均血糖、BMI(含真实缺失值)

5.2.3 练习目标

  1. 缺失值处理bmi 中的 NaN 用中位数填补;smoking_status 中的 "Unknown" 应单独处理(保留为一个类别,或按比例插补)。
  2. 编码分类变量gender, ever_married, work_type, Residence_type, smoking_status
  3. 划分训练/测试集后,先训练一个逻辑回归基线
  4. 重点:评估指标。准确率(accuracy)在不平衡数据上具有误导性——一个永远预测 "0" 的模型也能达到 95% 的准确率。请改用:
    • 混淆矩阵
    • 召回率(recall),即真正中风者中被正确识别的比例
    • F1 分数
    • ROC 曲线下面积(AUC)
  5. 进阶:使用 class_weight="balanced" 或对少数类过采样,观察召回率的变化。

5.2.4 关键提示

import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score df = pd.read_csv("stroke.csv").drop(columns="id") df["bmi"] = df["bmi"].fillna(df["bmi"].median())
# 不平衡场景下的逻辑回归 model = LogisticRegression(class_weight="balanced", max_iter=1000) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(classification_report(y_test, y_pred)) print("AUC =", roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]))

思考题:如果医院的目标是"宁可误报,也不能漏掉一个中风患者",应当优先优化哪个指标?阈值(默认 0.5)应该往哪个方向调整?


5.3 房价:Ames 住房(house_prices.csv)

5.3.1 数据集背景

这是 Kaggle 上久负盛名的入门竞赛 House Prices: Advanced Regression Techniques,来自爱荷华州 Ames 市的真实房地产销售记录。每一行对应一次房屋成交,目标是预测最终售价 SalePrice

  • 链接https://www.kaggle.com/c/house-prices-advanced-regression-techniques
  • 任务类型:回归(严格为正的连续目标)。
  • 特点:约 80 列,涵盖面积、位置、质量、设施、销售背景等方方面面,混合了连续值、名义型分类、序数型分类,并且结构性缺失非常多(例如没有车库的房子,所有车库相关字段都是 NaN)。

5.3.2 列分组速览

为方便理解,将变量按语义分组(完整字典见 Kaggle 页面):

  • 位置Neighborhood, MSZoning
  • 土地LotArea, LotFrontage, LotShape, LandContour
  • 房屋类型与风格MSSubClass, BldgType, HouseStyle
  • 质量与状况(序数):OverallQual, OverallCond
  • 建造年份YearBuilt, YearRemodAdd
  • 居住面积GrLivArea, TotalBsmtSF, 1stFlrSF, 2ndFlrSF
  • 房间BedroomAbvGr, TotRmsAbvGrd, KitchenAbvGr
  • 地下室BsmtQual, BsmtCond, BsmtFinSF1, ...(缺失 = 没有地下室)
  • 车库GarageType, GarageYrBlt, GarageCars, GarageArea, ...(缺失 = 没有车库)
  • 外观/材料Exterior1st, RoofStyle, MasVnrType, ...
  • 舒适度Heating, CentralAir, Electrical
  • 销售背景MoSold, YrSold, SaleType, SaleCondition

5.3.3 练习目标

  1. 诊断缺失值:用 df.isna().sum() 列出每列缺失数。区分结构性缺失(如 GarageType 缺失代表没有车库,应填 "None")与真实缺失(如 LotFrontage,应用中位数填补)。
  2. 目标变量分布SalePrice 通常严重右偏,建议对其取 log1p 后再建模,预测后再 expm1 还原。
  3. 特征工程(可选但推荐):
    • TotalSF = TotalBsmtSF + 1stFlrSF + 2ndFlrSF
    • HouseAge = YrSold - YearBuilt
  4. 建模:先用 Ridge/Lasso 做线性基线,再尝试随机森林(如果已学过)。比较 R² 与 RMSE。
  5. 解释性:列出对 SalePrice 影响最大的 10 个特征。

5.3.4 关键提示

import numpy as np, pandas as pd df = pd.read_csv("house_prices.csv") # 目标取对数 y = np.log1p(df["SalePrice"]) X = df.drop(columns=["SalePrice", "Id"])
# 区分两类缺失 struct_cols = ["GarageType", "GarageFinish", "BsmtQual", "BsmtCond", "MasVnrType", ...] for c in struct_cols: X[c] = X[c].fillna("None") X["LotFrontage"] = X["LotFrontage"].fillna(X["LotFrontage"].median())
# 一次性 one-hot 编码所有 object 列 X = pd.get_dummies(X, drop_first=True)

进阶:尝试用 sklearn.compose.ColumnTransformer 把数值列与分类列分开处理,再用 Pipeline 串起来。这是工业实践中的标准做法。


5.4 MNIST 手写数字(mnist.csv)

5.4.1 数据集背景

MNIST(Modified National Institute of Standards and Technology)是机器学习与模式识别领域最经典的基准数据集,由 Yann LeCun、Corinna Cortes 和 Christopher J. C. Burges 整理。它包含 0 到 9 的手写数字图像,每张图为 28 × 28 像素的灰度图。

在我们使用的 CSV 版本里,每张图被展平成 784 个像素列1x1, 1x2, ..., 28x28。每个值是 0–255 的整数(0 = 黑,255 = 白)。目标列 label 是数字本身(0–9)。

5.4.2 数据可视化:从向量到图像

由于像素被展平成一维向量,要"看见"图像必须先 reshape 回 28 × 28:

X = df.drop(columns="label").to_numpy() y = df["label"].to_numpy() image = X[0].reshape(28, 28)
import matplotlib.pyplot as plt plt.imshow(image, cmap="gray") plt.title(y[0]) plt.axis("off")

为了同时观察多张图像,使用 subplots 网格:

fig, axes = plt.subplots(2, 5, figsize=(10, 4)) k = 0 for i in range(2): for j in range(5): axes[i, j].imshow(X[k].reshape(28, 28), cmap="gray") axes[i, j].set_title(y[k]) axes[i, j].axis("off") k += 1 plt.tight_layout() plt.show()

5.4.3 像素归一化

像素值范围 0–255 偏大,几乎所有模型(尤其是基于梯度的)在 [0, 1] 区间上训练得更稳定:

X = X / 255.0

5.4.4 练习目标

  1. 加载数据并显示一个 2 × 5 的网格,确认数据无误。
  2. 划分训练/测试集(如果 CSV 已经分好,则跳过;否则用 train_test_split)。
  3. 训练一个多分类逻辑回归 作为基线,注意设置 max_iter=1000solver="lbfgs"
  4. 在测试集上计算准确率,并打印混淆矩阵。哪两个数字最容易被混淆?(经验上是 4/9 与 3/8。)
  5. 进阶:尝试 K 近邻(KNN,n_neighbors=3),比较精度与训练/预测速度。
  6. 可视化错误:找出几张被错误分类的图像,把真实标签与预测标签都打印出来——这是发现模型偏差的最佳方式。

5.4.5 关键提示

from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, confusion_matrix model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("Accuracy:", accuracy_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))
# 显示分类错误的样本 import numpy as np wrong = np.where(y_pred != y_test)[0] fig, axes = plt.subplots(1, 5, figsize=(10, 2)) for ax, idx in zip(axes, wrong[:5]): ax.imshow(X_test[idx].reshape(28, 28), cmap="gray") ax.set_title(f"真:{y_test[idx]} 预:{y_pred[idx]}") ax.axis("off") plt.show()

延伸:MNIST 是从机器学习走向深度学习的天然桥梁。在第 6 章及之后,我们将看到一个简单的卷积神经网络可以把准确率从逻辑回归的约 92% 推到 99% 以上。


5.5 本章小结

四个练习覆盖了完整的方法学谱系:

数据集任务主要难点推荐工具
Mercedes回归高维稀疏Lasso、标准化
Stroke二分类标签不平衡class_weight、AUC、召回率
Ames回归异质特征 + 大量缺失结构性缺失填补、log 变换、Pipeline
MNIST多分类像素特征、可视化reshape、归一化、混淆矩阵

请记住一条贯穿始终的原则——先理解数据,再选择模型。任何强大的算法在错误的预处理面前都会失效;反过来,扎实的数据清洗与合理的评估指标,往往比模型本身更能决定项目的成败。