本文为 Python 60 Days Challenge 课程学习整理,原作者与原 Notebook 链接见「课程资料」。代码按章节顺序运行;语法示意和故意报错的反例已单独标注。
文中保留的截图、性能表和数值属于原讲义的历史示例,不是本次修订后的重新训练结果。实际输出受数据、版本、随机种子和训练预算影响。
打卡任务
- 阅读本节讲义或 Notebook
- 跑通示例代码
- 整理 3 个关键点
- 完成当日打卡输出
课程资料
- 训练营讲义:https://docs.qq.com/doc/DTFhndExSRGNHS3B6?nlc=1
- GitHub 仓库:https://github.com/cheer932041235/Python60DaysChallenge
- 本日 Notebook:Day18 回归问题与置信区间.ipynb
学习内容
我们正式进入回归问题 (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 包装。
原创
Day18 回归问题与置信区间
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法