ML · 章节 5
第 5 章 综合练习
经过前四章的学习,我们已经依次接触到机器学习的三大支柱:回归(第 2 章)、分类(第 3 章)以及模型评估与正则化(第 4 章)。每一章我们都在精心挑选的小型数据集上反复打磨同一个工作流程:加载数据、探索分布、清洗缺失值、编码分类变量、划分训练/测试集、拟合模型、评估表现。
本章不再引入新的算法,而是把这套工作流程整体地搬到四个真实数据集上。这四个数据集来自 Kaggle 的经典竞赛或公开仓库,规模与噪声水平远高于教学用的小例子。它们的共同点是:都需要使用前面章节中介绍过的工具,但又都各自暴露出某种"学习上的痛点"——高维稀疏特征、严重不平衡的标签、几十种异质的列、像素级输入。读者将在解决这些问题的过程中,把零散的知识串联成完整的方法论。
学习目标
- 在不同体量、不同结构的数据集上独立完成一个完整的机器学习项目。
- 学会根据数据集的特性选择合适的模型与评估指标,而不是机械地套用同一个流程。
- 体会预处理(缺失值、编码、归一化)在真实场景中的关键作用。
- 巩固
train_test_split、fit/predict、交叉验证、正则化等核心 API。
本章共包含四个数据集的练习,难度递进。每一节先介绍数据集的来源与商业/科学背景,然后说明练习的目标,最后给出关键提示与代码片段。读者应当先合上参考代码,独立尝试,遇到困难时再回头查看提示。
5.1 梅赛德斯-奔驰:绿色制造(mercedes_test.csv)
5.1.1 数据集背景
本数据集来自 Kaggle 上的 Mercedes-Benz Greener Manufacturing 竞赛。梅赛德斯-奔驰在量产前会对每一种车辆配置进行测试台测试,以验证传动、电子、机械等子系统是否满足规范。这些测试耗时且耗能,因此厂商希望事先预测某种配置在测试台上花费的时间,从而提前调度并削减能耗。
- 链接:https://www.kaggle.com/c/mercedes-benz-greener-manufacturing
- 任务类型:回归
- 目标变量
y:连续值,代表测试时间或工业性能得分。
5.1.2 数据结构
每一行对应一种车辆配置。特征数高达数百,结构相当特殊:
- 8 个分类变量
X0, X1, X2, X3, X4, X5, X6, X8:发动机、传动、机械/电子组件、设计变更等代码,由字母a, b, c, ...编码。 - 约 370 个二元变量
X10至X385:取值为 0 或 1,表示某项技术选项、兼容性标志或子组件的存在与否。
这种"小样本、高维度、几乎全是哑变量"的结构正是正则化(Lasso、Ridge)大显身手的场景。普通最小二乘回归在这里几乎注定过拟合。
5.1.3 练习目标
- 加载
mercedes_test.csv并完成初步探索:行数、列数、缺失值、y的分布。 - 对 8 个字母编码的分类变量进行编码(推荐
OrdinalEncoder或OneHotEncoder)。 - 划分训练集与测试集,依次拟合:线性回归、Ridge、Lasso,并比较 R² 与均方根误差。
- 利用 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)。它的现实意义在于辅助医生进行筛查,把有限的医疗资源优先分配给高风险人群。
- 链接:https://www.kaggle.com/datasets/fedesoriano/stroke-prediction-dataset
- 任务类型:二分类。
- 关键挑战:标签严重不平衡——发生中风的样本不到 5%。
5.2.2 列与含义
| 类别 | 变量 | 含义 |
|---|---|---|
| 标识符 | id | 唯一编号,建模前应丢弃 |
| 目标 | stroke | 1 = 中风,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 练习目标
- 缺失值处理:
bmi中的 NaN 用中位数填补;smoking_status中的"Unknown"应单独处理(保留为一个类别,或按比例插补)。 - 编码分类变量:
gender,ever_married,work_type,Residence_type,smoking_status。 - 划分训练/测试集后,先训练一个逻辑回归基线。
- 重点:评估指标。准确率(accuracy)在不平衡数据上具有误导性——一个永远预测 "0" 的模型也能达到 95% 的准确率。请改用:
- 混淆矩阵
- 召回率(recall),即真正中风者中被正确识别的比例
- F1 分数
- ROC 曲线下面积(AUC)
- 进阶:使用
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 练习目标
- 诊断缺失值:用
df.isna().sum()列出每列缺失数。区分结构性缺失(如GarageType缺失代表没有车库,应填"None")与真实缺失(如LotFrontage,应用中位数填补)。 - 目标变量分布:
SalePrice通常严重右偏,建议对其取log1p后再建模,预测后再expm1还原。 - 特征工程(可选但推荐):
TotalSF = TotalBsmtSF + 1stFlrSF + 2ndFlrSFHouseAge = YrSold - YearBuilt
- 建模:先用 Ridge/Lasso 做线性基线,再尝试随机森林(如果已学过)。比较 R² 与 RMSE。
- 解释性:列出对
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 像素的灰度图。
- 链接:https://www.kaggle.com/datasets/oddrationale/mnist-in-csv
- 任务类型:多分类(10 个类别)。
- 规模:约 60 000 张训练图 + 10 000 张测试图。
在我们使用的 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 练习目标
- 加载数据并显示一个 2 × 5 的网格,确认数据无误。
- 划分训练/测试集(如果 CSV 已经分好,则跳过;否则用
train_test_split)。 - 训练一个多分类逻辑回归 作为基线,注意设置
max_iter=1000、solver="lbfgs"。 - 在测试集上计算准确率,并打印混淆矩阵。哪两个数字最容易被混淆?(经验上是 4/9 与 3/8。)
- 进阶:尝试 K 近邻(KNN,
n_neighbors=3),比较精度与训练/预测速度。 - 可视化错误:找出几张被错误分类的图像,把真实标签与预测标签都打印出来——这是发现模型偏差的最佳方式。
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、归一化、混淆矩阵 |
请记住一条贯穿始终的原则——先理解数据,再选择模型。任何强大的算法在错误的预处理面前都会失效;反过来,扎实的数据清洗与合理的评估指标,往往比模型本身更能决定项目的成败。