本文为 Python 60 Days Challenge 课程学习整理,原作者与原 Notebook 链接见「课程资料」。代码按章节顺序运行;语法示意和故意报错的反例已单独标注。

文中保留的截图、性能表和数值属于原讲义的历史示例,不是本次修订后的重新训练结果。实际输出受数据、版本、随机种子和训练预算影响。

打卡任务

  • 阅读本节讲义或 Notebook
  • 跑通示例代码
  • 整理 3 个关键点
  • 完成当日打卡输出

课程资料

学习内容

我们正式进入回归问题 (Regression) 的篇章。和分类问题(猜它是谁)不同,回归问题的核心是 “猜它多少” (预测一个连续的数值,比如房价、气温、销量)。

我们将使用 UCI 的混凝土抗压强度数据集(这里从注明来源的 CSV 镜像读取,并非 sklearn 内置数据集)。

  • 来源:UCI 机器学习库(经典工程数据集)。
  • 背景:根据水泥、水、粗骨料、细骨料、外加剂、时间等 8 个特征,预测混凝土的抗压强度。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error

# --- 1. 全局绘图设置 ---
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False   # 用来正常显示负号
sns.set(style="whitegrid", font='SimHei')    # Seaborn 也要同步设置字体

# ==========================================
# 2. 加载数据 & 转换为 DataFrame (修复版)
# ==========================================
print("正在加载混凝土抗压强度数据集 (Concrete Data)...")

# 【修改点】不再使用 fetch_openml,直接读取稳定的 CSV 地址
# 这是一个托管在 GitHub 上的标准混凝土数据集,内容和 OpenML 一模一样
url = "https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/concrete.csv"

try:
    df = pd.read_csv(url)
except Exception as e:
    print("网络读取失败,请检查网络设置。")
    # 如果网络实在不通,可以使用 sklearn 自带的糖尿病数据集作为备选(虽然它不如混凝土数据直观)
    # from sklearn.datasets import load_diabetes
    # df = load_diabetes(as_frame=True).frame
    raise e

# 为了讲义可读性,我们重命名列为中文对照
# 注意:原始 CSV 的列顺序是固定的,所以直接按顺序重命名是安全的
df.columns = [
    'Cement (水泥)', 'BlastFurnaceSlag (矿渣)', 'FlyAsh (粉煤灰)', 
    'Water (水)', 'Superplasticizer (减水剂)', 'CoarseAggregate (粗骨料)', 
    'FineAggregate (细骨料)', 'Age (养护天数)', 'Strength (抗压强度)'
]

print("="*30 + " 数据集概览 " + "="*30)
print(f"数据形状: {df.shape}")
print(df.head()) # 打印前5行预览

# ==========================================
# 3. 数据切分
# ==========================================
# 最后一列 'Strength' 是我们要预测的目标 (y)
X = df.iloc[:, :-1]
y = df.iloc[:, -1]

# 80% 训练,20% 测试
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# ==========================================
# 4. 定义并训练多个模型
# ==========================================
regressors = {
    "Linear Regression (线性回归)": LinearRegression(),
    "Decision Tree (决策树)": DecisionTreeRegressor(random_state=42),
    "Random Forest (随机森林)": RandomForestRegressor(n_estimators=100, random_state=42),
    "Gradient Boosting (梯度提升)": GradientBoostingRegressor(n_estimators=100, random_state=42)
}

results = []      # 用于存指标
preds_dict = {}   # 用于存预测结果(画图用)

print("\n" + "="*30 + " 开始训练与评估 " + "="*30)

for name, model in regressors.items():
    # A. 训练
    model.fit(X_train, y_train)
    
    # B. 预测
    y_pred = model.predict(X_test)
    preds_dict[name] = y_pred # 存下来画图
    
    # C. 计算核心指标
    r2 = r2_score(y_test, y_pred)
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_test, y_pred)
    
    # D. 记录结果
    results.append({
        "模型名称": name,
        "R2": r2,      # 决定系数
        "RMSE": rmse,  # 均方根误差
        "MAE": mae     # 平均绝对误差
    })
    print(f"模型 {name} 训练完成。R2 = {r2:.4f}")

# ==========================================
# 5. 展示指标对比表
# ==========================================
results_df = pd.DataFrame(results).sort_values(by="R2", ascending=False)
print("\n" + "="*30 + " 最终性能排行榜 " + "="*30)
print(results_df)
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from sklearn.metrics import r2_score, mean_squared_error

# --- 假设前面的数据加载、切分、训练代码已经运行过 ---
# --- 直接从绘图部分开始 ---

# 1. 设置画布:2行2列
fig, axes = plt.subplots(2, 2, figsize=(16, 14))
axes = axes.flatten()  # 把 2x2 的矩阵展平,方便用 [0,1,2,3] 索引遍历

# 定义颜色,每个模型一个色
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]

print("正在绘制带有置信区间的单模型分析图...")

# 2. 确定坐标轴范围 (为了美观,所有子图统一范围)
# 找到所有真实值和预测值中的最大最小值
all_preds = [preds_dict[m] for m in regressors.keys()]
data_min = min(y_test.min(), np.min(all_preds)) - 5 
data_max = max(y_test.max(), np.max(all_preds)) + 5

for i, (name, model) in enumerate(regressors.items()):
    ax = axes[i]
    y_pred = preds_dict[name]
    
    # --- 核心绘图函数: sns.regplot ---
    # ci=95: 自动画出 95% 的置信区间阴影
    # scatter_kws: 控制散点的样式
    # line_kws: 控制拟合线的样式
    sns.regplot(x=y_test, y=y_pred, ax=ax, 
                color=colors[i],
                ci=95,  # regplot 趋势线的 bootstrap 置信带,不是单个预测的预测区间
                scatter_kws={'s': 30, 'alpha': 0.5, 'edgecolor': 'white'},
                line_kws={'color': '#333333', 'linewidth': 2, 'label': '拟合趋势线'})
    
    # --- 画出完美的对角线 (y=x) 作为基准 ---
    ax.plot([data_min, data_max], [data_min, data_max], 
            'r--', linewidth=3, label='完美预测线 (Perfect Fit)')
    
    # --- 计算指标并写在图上 ---
    r2 = r2_score(y_test, y_pred)
    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    
    # 在左上角添加文本框
    text_str = f'$R^2$ = {r2:.3f}\nRMSE = {rmse:.3f}'
    ax.text(0.05, 0.95, text_str, transform=ax.transAxes, fontsize=14,
            verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.9))

    # --- 装饰 ---
    ax.set_title(name, fontsize=16, fontweight='bold')
    ax.set_xlabel('真实强度 (Actual)', fontsize=12)
    ax.set_ylabel('预测强度 (Predicted)', fontsize=12)
    ax.set_xlim(data_min, data_max)
    ax.set_ylim(data_min, data_max)
    ax.legend(loc='lower right')
    ax.grid(True, linestyle='--', alpha=0.5)

plt.tight_layout()
plt.subplots_adjust(top=0.92) # 留出标题空间
plt.suptitle('各模型回归拟合效果与置信区间分析', fontsize=20)
plt.show()

regplot(ci=95) 对散点拟合的趋势线进行 bootstrap 并绘制置信带;它没有读取原随机森林模型,也不是单个新观测的 95% 预测区间。下例对训练样本重采样后重新拟合随机森林,展示均值预测对训练样本变化的敏感性;其分位数区间也未包含全部观测噪声,不保证单个观测的覆盖率。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.utils import resample
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# --- 1. 全局设置 ---
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False   # 用来正常显示负号
sns.set(style="whitegrid", font='SimHei')

# ==========================================
# 2. 加载数据 (使用稳定的 GitHub 源)
# ==========================================
url = "https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/concrete.csv"
try:
    df = pd.read_csv(url)
    print("成功加载混凝土数据集。")
except:
    print("网络错误,无法加载数据。")
    exit()

# 重命名列方便阅读
df.columns = [
    'Cement', 'BlastFurnaceSlag', 'FlyAsh', 'Water', 'Superplasticizer', 
    'CoarseAggregate', 'FineAggregate', 'Age', 'Strength'
]

X = df.iloc[:, :-1]
y = df.iloc[:, -1]

# 切分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ==========================================
# 3. 随机森林 Bootstrap 核心过程 (100次)
# ==========================================
n_bootstraps = 100  # 教学示例;重复次数不是覆盖率保证
bootstrap_preds = []

print(f"\n正在进行 {n_bootstraps} 次随机森林 Bootstrap 重采样...")
print("进度: ", end="")

for i in range(n_bootstraps):
    # 打印进度条 (每10次打印一个点)
    if i % 10 == 0: print(".", end="", flush=True)
    
    # A. 重采样训练集 (有放回)
    # 模拟平行宇宙:每次生成一个新的训练集
    X_boot, y_boot = resample(X_train, y_train, random_state=i)
    
    # B. 训练模型
    # n_estimators=50: 为了速度适当减少树的数量,因为我们要训练100个模型
    rf_boot = RandomForestRegressor(n_estimators=50, random_state=42)
    rf_boot.fit(X_boot, y_boot)
    
    # C. 预测测试集
    # 关键:所有模型都预测同一个 X_test
    y_pred_boot = rf_boot.predict(X_test)
    bootstrap_preds.append(y_pred_boot)

print(" 完成!")

# 转换为矩阵 (行: 100次模拟, 列: 测试集样本数)
bootstrap_preds = np.array(bootstrap_preds)

# ==========================================
# 4. 计算置信区间 (Confidence Interval)
# ==========================================
# 在每一列(每个样本)上,掐头去尾取中间 95%
ci_lower = np.percentile(bootstrap_preds, 2.5, axis=0)
ci_upper = np.percentile(bootstrap_preds, 97.5, axis=0)
mean_pred = np.mean(bootstrap_preds, axis=0)

# ==========================================
# 5. 绘图:带状排序图 (Sorted Area Plot)
# ==========================================
# 为了画出漂亮的带状图,必须按真实值排序
plot_df = pd.DataFrame({
    'y_true': y_test.values,
    'y_pred_mean': mean_pred,
    'ci_lower': ci_lower,
    'ci_upper': ci_upper
})
plot_df = plot_df.sort_values(by='y_true').reset_index(drop=True)

plt.figure(figsize=(14, 7))

# A. 画出 95% 置信区间 (阴影带)
plt.fill_between(plot_df.index, 
                 plot_df['ci_lower'], 
                 plot_df['ci_upper'], 
                 color='#2ca02c', alpha=0.3, label='95% 置信区间 (Bootstrap CI)')

# B. 画出平均预测线
plt.plot(plot_df.index, plot_df['y_pred_mean'], 
         color='#2ca02c', linewidth=1.5, alpha=0.9, linestyle='--', label='随机森林平均预测')

# C. 画出真实值 (基准线)
plt.plot(plot_df.index, plot_df['y_true'], 
         color='black', linewidth=2, label='真实值 (Ground Truth)')

plt.title(f'随机森林回归:Bootstrap 置信区间分析 (n={n_bootstraps})', fontsize=16)
plt.xlabel('测试集样本索引 (按真实强度排序)', fontsize=12)
plt.ylabel('混凝土抗压强度', fontsize=12)
plt.legend(loc='upper left')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

回归问题可以修改为分类问题,但是分类问题一般难以转化为回归模型

多输出回归并不局限于神经网络;线性回归、决策树、随机森林等也支持多输出,或可通过 MultiOutputRegressor 包装。