导语
上一期我们用鸢尾花数据集讲透了分类问题的评估方法。很多读者留言问:回归问题该怎么评估?只看 R² 够不够?残差分析到底在分析什么? 今天,我们就用加州房价数据集,从零开始走完 数据探索 → 多模型训练 → 8大指标评估 → 9张可视化图表 的完整流程。这一次,我们不仅要预测房价,更要搞懂——一个回归模型,到底好在哪,差在哪。
做机器学习的同学一定听说过波士顿房价数据集(Boston Housing),但由于伦理争议,scikit-learn 已在新版本中将其移除。取而代之的是 加州房价数据集(California Housing),它具备以下特点:
为什么回归问题更值得深入学习?因为现实世界中大量场景都是回归:预测房价、预测销量、预测温度、预测股价、预测用户停留时长……分类是"选择题",回归是"填空题",难度和评估复杂度都上了一个台阶。
📦 加载加州房价数据集 (20640 × 8) ↓🔧 数据预处理(标准化)+ 训练/测试集划分 (80/20) ↓🌲 同时训练 3 个模型 ├── 线性回归 (LR) —— 基线模型 ├── 随机森林 (RF) —— 集成学习 └── 梯度提升 (GBDT) —— 更强的集成学习 ↓📐 8 大回归指标全面评估 ↓🎨 9 张可视化图表深度分析 ↓✅ 输出完整分析报告跟上一期只训练一个模型不同,这次我们同时训练 3 个模型做横向对比——因为实际工作中,没人会只试一个模型就下结论。
分类问题看准确率、F1、AUC;回归问题的指标体系完全不同。我们这次引入了 8 个维度,覆盖了从基础到进阶的所有需求。
| MSE | |||
| RMSE | |||
| MAE | |||
| MedAE | |||
| MAPE | |||
| R² | |||
| Adj-R² | |||
| EVS |
Q:RMSE 和 MAE 都衡量误差大小,选谁?
看你的业务对"大错误"的容忍度。RMSE 因为有平方操作,对偏差大的样本惩罚更重。如果你的场景是"偏一点没关系,但千万别偏太多"(比如医疗剂量预测),用 RMSE 更合适;如果所有误差同等重要,用 MAE。
Q:R² = 0.85 算好吗?
取决于领域。预测工业产品尺寸,R² = 0.85 可能不够用;预测房价或股市,R² = 0.85 已经相当优秀。R² = 1 是完美预测,R² = 0 说明你的模型和"直接猜平均值"一样差,R² < 0 说明你的模型比猜平均值还差——该反思了。
Q:为什么要看 Adj-R²?
普通 R² 有个"漏洞":你往模型里塞越多特征,R² 几乎只升不降,哪怕那些特征是噪声。Adj-R² 通过惩罚特征数量来修正这个问题。如果加了一个特征后 R² 升了但 Adj-R² 降了——说明这个特征是噪声,别要了。
Q:MAPE 的坑在哪?
当真实值接近 0 时,MAPE 会爆炸(分母趋近于零)。所以如果你的数据中有很多接近零的值,MAPE 就不太可靠,这时候优先看 MAE 或 RMSE。
我们同时训练了三个模型,来看看它们的表现:
🏆 最优模型:随机森林
R² 最高,RMSE 最低,综合表现最好。但也要注意:R² = 0.81 意味着还有约 19% 的方差没有被解释,说明房价受很多我们没有捕捉到的因素影响(比如学区、装修、朝向等)。
数字看完了,接下来是重头戏——用图表"审讯"模型。我们一次性画了 9 张子图,每一张都有明确的分析目标。
看什么:点越贴近红色对角线,预测越准
这是回归分析最基本的图。所有散点完美落在 y=x 线上就是完美预测。我们的模型在低价区(1~3)预测较准,但在高价区(>4)出现明显偏离——这很常见,因为高端房产本身数据就少,模型学得不充分。
图上还标注了 R² 和 RMSE 的数值,一眼就能把握整体精度。
看什么:残差是否以 0 为中心、接近正态分布
残差 = 真实值 - 预测值。理想情况下:
红色曲线是正态拟合,如果直方图和红色曲线严重不符,说明模型可能遗漏了某些重要模式。
看什么:残差有没有呈现"漏斗形"或其他规律
这张图非常关键。如果残差随着预测值的增大而"散开"(漏斗形),说明存在异方差性——模型在不同价位段的预测精度不一致。
蓝色 LOWESS 趋势线帮助你捕捉残差中的非线性模式。如果趋势线不是水平的,说明模型存在系统性偏差。
看什么:哪些特征真正驱动房价
随机森林的一大优势就是能输出特征重要性。结果显示:
误差棒展示了 200 棵树之间的"分歧"程度,棒越长说明不同树对这个特征的看法差异越大。
看什么:模型是否过拟合或欠拟合
还可以看出:增加更多训练数据是否还能提升性能。如果曲线已经趋平,说明数据量已经足够,瓶颈在模型本身而非数据量。
看什么:三个模型在 R²/RMSE/MAE 上的直观对比
这张图一目了然:随机森林的蓝色柱子在 R² 上最高,在 RMSE 和 MAE 上最低——全面胜出。但注意,线性回归虽然指标最差,却有个不可替代的优势——可解释性。在某些需要给出明确因果解释的场景(比如向政策制定者汇报),简单模型可能比黑箱模型更有价值。
看什么:散点是否贴合红色拟合线
Q-Q 图是统计学中检验正态性的标准工具。如果残差完美服从正态分布,所有散点会严格落在红色直线上。
实际中我们通常会看到尾部偏离——两端的点翘起或弯曲。这说明模型在极端值(特别贵或特别便宜的房子)上的预测误差比正态分布预期的更大。
看什么:特征之间是否存在多重共线性
红色代表正相关,蓝色代表负相关,颜色越深相关性越强。
重点关注:
多重共线性会让线性回归的系数不稳定,但对随机森林影响较小——这也是为什么树模型在实践中如此受欢迎。
看什么:模型在多大比例的样本上误差可控
这张图回答了一个非常实际的问题:"我的模型有多少样本的误差在 10% 以内?20% 以内?"
橙色直方图展示了绝对百分比误差的分布,蓝色曲线是累计分布。通过查看关键分位线,你可以直接告诉老板:
"我们的模型在 XX% 的房子上,定价误差不超过 10%。"
这比干巴巴地说一个 RMSE 数字有意义多了。
上期讲了分类,这期讲了回归。放一张对比表帮你建立完整的知识框架:
经过这次完整分析,总结三个可以立刻用到你项目里的建议:
R² 高不代表模型好——残差可能严重偏态;RMSE 低也不代表万事大吉——可能只是因为大部分样本很容易预测。多个指标交叉验证,图表辅助分析,才能全面判断。
指标告诉你"模型整体水平如何",残差分析告诉你"模型在哪里犯错、怎么犯错"。看残差分布是否对称、是否有趋势、是否有异方差——这些信息能直接指导你的下一步优化方向。
不要给老板说"R² = 0.81"。要说:
"我们的模型能解释 81% 的房价变动,在 75% 的房子上定价误差不超过 20%。主要失准区间在高端房产(>50万美元),建议后续补充高端房产的特征数据。"
把技术指标翻译成业务语言,是数据从业者最核心的能力之一。
# 回归问题完整分析系统 —— 波士顿房价预测(加州房价替代)import pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport matplotlib.gridspec as gridspecimport seaborn as snsfrom sklearn.datasets import fetch_california_housingfrom sklearn.model_selection import train_test_split, cross_val_score, learning_curvefrom sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressorfrom sklearn.linear_model import LinearRegressionfrom sklearn.preprocessing import StandardScalerfrom sklearn.metrics import ( mean_squared_error, mean_absolute_error, r2_score, mean_absolute_percentage_error, median_absolute_error, explained_variance_score)import warningswarnings.filterwarnings('ignore')# ==========================================# 全局样式配置# ==========================================plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans']plt.rcParams['axes.unicode_minus'] = Falseplt.rcParams['figure.dpi'] = 120plt.rcParams['savefig.dpi'] = 150COLORS = ['#2196F3', '#4CAF50', '#FF5722', '#9C27B0', '#FF9800']# ==========================================# 1. 加载和探索数据# ==========================================print("=" * 65)print(" 加州房价数据集 — 随机森林回归完整分析")print("=" * 65)print("\n【第一步】加载数据集...")housing = fetch_california_housing()X = housing.datay = housing.target # 单位:10万美元feature_names = housing.feature_namesdescription = housing.DESCRdf = pd.DataFrame(X, columns=feature_names)df['Price'] = yprint(f" 数据集维度 : {X.shape[0]} 样本 × {X.shape[1]} 特征")print(f" 目标变量(房价): 均值={y.mean():.3f}, " f"中位数={np.median(y):.3f}, " f"范围=[{y.min():.3f}, {y.max():.3f}] (单位: 10万美元)")print(f"\n 特征列表:")for i, name in enumerate(feature_names): print(f" {i+1}. {name:<12} | " f"均值={X[:, i].mean():>10.3f} | " f"标准差={X[:, i].std():>10.3f}")# ==========================================# 2. 数据预处理与划分# ==========================================print("\n【第二步】数据预处理与划分 (训练集80% / 测试集20%)...")# 标准化特征(对线性回归有帮助,对树模型影响不大,但统一处理)scaler = StandardScaler()X_scaled = scaler.fit_transform(X)X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.2, random_state=42)# 保存原始特征用于特征重要性分析X_train_raw, X_test_raw, _, _ = train_test_split( X, y, test_size=0.2, random_state=42)print(f" 训练集样本数: {X_train.shape[0]}")print(f" 测试集样本数: {X_test.shape[0]}")# ==========================================# 3. 训练多个回归模型(对比分析)# ==========================================print("\n【第三步】训练回归模型...")models = { '线性回归 (LR)': LinearRegression(), '随机森林 (RF)': RandomForestRegressor( n_estimators=200, max_depth=15, min_samples_split=5, random_state=42, n_jobs=-1 ), '梯度提升 (GBDT)': GradientBoostingRegressor( n_estimators=200, max_depth=5, learning_rate=0.1, random_state=42 ),}results = {}for name, model in models.items(): print(f" 正在训练: {name}...") model.fit(X_train, y_train) y_pred = model.predict(X_test) results[name] = { 'model': model, 'y_pred': y_pred, 'MSE': mean_squared_error(y_test, y_pred), 'RMSE': np.sqrt(mean_squared_error(y_test, y_pred)), 'MAE': mean_absolute_error(y_test, y_pred), 'MedAE': median_absolute_error(y_test, y_pred), 'MAPE': mean_absolute_percentage_error(y_test, y_pred), 'R2': r2_score(y_test, y_pred), 'Adj_R2': 1 - (1 - r2_score(y_test, y_pred)) * (len(y_test) - 1) / (len(y_test) - X_test.shape[1] - 1), 'EVS': explained_variance_score(y_test, y_pred), }# 选出最优模型(以R2为准)best_name = max(results, key=lambda k: results[k]['R2'])best_result = results[best_name]best_model = best_result['model']y_pred_best = best_result['y_pred']# ==========================================# 4. 全面评估指标输出# ==========================================print("\n" + "=" * 65)print(" 模型评估指标汇总")print("=" * 65)# 4.1 各模型对比表print("\n【各模型指标对比】")print(f"{'指标':<20}", end="")for name in models: print(f"{name:<20}", end="")print()print("-" * 80)metric_labels = { 'MSE': '均方误差 (MSE)', 'RMSE': '均方根误差 (RMSE)', 'MAE': '平均绝对误差 (MAE)', 'MedAE': '中位绝对误差 (MedAE)', 'MAPE': '平均百分比误差 (MAPE)', 'R2': '决定系数 (R²)', 'Adj_R2': '调整R² (Adj-R²)', 'EVS': '解释方差 (EVS)',}for key, label in metric_labels.items(): print(f" {label:<18}", end="") for name in models: val = results[name][key] if key == 'MAPE': print(f"{val*100:>17.2f}%", end=" ") else: print(f"{val:>19.4f}", end=" ") print()print(f"\n 🏆 最优模型: {best_name} (R² = {best_result['R2']:.4f})")# 4.2 交叉验证print(f"\n【5折交叉验证 — {best_name}】")cv_r2 = cross_val_score(best_model, X_scaled, y, cv=5, scoring='r2')cv_rmse = -cross_val_score(best_model, X_scaled, y, cv=5, scoring='neg_root_mean_squared_error')print(f" R² 各折: {[f'{s:.4f}' for s in cv_r2]}")print(f" R² 均值: {cv_r2.mean():.4f} ± {cv_r2.std():.4f}")print(f" RMSE 各折: {[f'{s:.4f}' for s in cv_rmse]}")print(f" RMSE 均值: {cv_rmse.mean():.4f} ± {cv_rmse.std():.4f}")# 4.3 残差统计residuals = y_test - y_pred_bestprint(f"\n【残差统计 — {best_name}】")print(f" 均值 : {residuals.mean():.6f} (理想为0)")print(f" 标准差 : {residuals.std():.4f}")print(f" 偏度 (Skew) : {pd.Series(residuals).skew():.4f} (理想为0)")print(f" 峰度 (Kurt) : {pd.Series(residuals).kurtosis():.4f} (理想为0)")print(f" |残差|>1 比例 : {(np.abs(residuals)>1).mean()*100:.1f}%")print(f" |残差|>2 比例 : {(np.abs(residuals)>2).mean()*100:.1f}%")# ==========================================# 5. 可视化(共 9 张子图)# ==========================================print("\n【第五步】生成可视化图表...")fig = plt.figure(figsize=(22, 20))fig.suptitle(f'加州房价回归分析 — 完整报告 (最优模型: {best_name})', fontsize=18, fontweight='bold', y=0.98)gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.42, wspace=0.35)# ────────────────────────────────────────# 图1:真实值 vs 预测值散点图# ────────────────────────────────────────ax1 = fig.add_subplot(gs[0, 0])ax1.scatter(y_test, y_pred_best, alpha=0.4, s=12, c='#2196F3', edgecolors='none')# 完美预测线lims = [min(y_test.min(), y_pred_best.min()) - 0.2, max(y_test.max(), y_pred_best.max()) + 0.2]ax1.plot(lims, lims, 'r--', lw=2, label='完美预测线 (y=x)')ax1.set_xlim(lims)ax1.set_ylim(lims)ax1.set_xlabel('真实值 (10万美元)')ax1.set_ylabel('预测值 (10万美元)')ax1.set_title('① 真实值 vs 预测值', fontsize=13, fontweight='bold')ax1.legend(fontsize=9)ax1.set_aspect('equal')ax1.grid(alpha=0.3)# 在图内标注 R²ax1.text(0.05, 0.92, f'R$^2$ = {best_result["R2"]:.4f}\nRMSE = {best_result["RMSE"]:.4f}', transform=ax1.transAxes, fontsize=9, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))# ────────────────────────────────────────# 图2:残差分布直方图 + KDE# ────────────────────────────────────────ax2 = fig.add_subplot(gs[0, 1])ax2.hist(residuals, bins=50, density=True, alpha=0.7, color='#4CAF50', edgecolor='white', label='残差分布')# 拟合正态分布曲线from scipy import statsxmin, xmax = ax2.get_xlim()x_kde = np.linspace(xmin, xmax, 200)mu, sigma = residuals.mean(), residuals.std()pdf = stats.norm.pdf(x_kde, mu, sigma)ax2.plot(x_kde, pdf, 'r-', lw=2, label=f'正态拟合\n(μ={mu:.3f}, σ={sigma:.3f})')ax2.axvline(0, color='navy', linestyle='--', lw=1.5, alpha=0.7)ax2.set_title('② 残差分布', fontsize=13, fontweight='bold')ax2.set_xlabel('残差 (真实值 - 预测值)')ax2.set_ylabel('密度')ax2.legend(fontsize=8)ax2.grid(alpha=0.3)# ────────────────────────────────────────# 图3:残差 vs 预测值(检验同方差性)# ────────────────────────────────────────ax3 = fig.add_subplot(gs[0, 2])ax3.scatter(y_pred_best, residuals, alpha=0.4, s=12, c='#FF5722', edgecolors='none')ax3.axhline(0, color='navy', linestyle='--', lw=1.5)ax3.axhline(residuals.std()*2, color='gray', linestyle=':', alpha=0.5, label='±2σ')ax3.axhline(-residuals.std()*2, color='gray', linestyle=':', alpha=0.5)# LOWESS 平滑趋势线try: from statsmodels.nonparametric.smoothers_lowess import lowess smoothed = lowess(residuals, y_pred_best, frac=0.3) ax3.plot(smoothed[:, 0], smoothed[:, 1], 'b-', lw=2, label='LOWESS趋势线')except ImportError: z = np.polyfit(y_pred_best, residuals, 3) p = np.poly1d(z) x_sort = np.sort(y_pred_best) ax3.plot(x_sort, p(x_sort), 'b-', lw=2, label='趋势线')ax3.set_title('③ 残差 vs 预测值', fontsize=13, fontweight='bold')ax3.set_xlabel('预测值')ax3.set_ylabel('残差')ax3.legend(fontsize=8)ax3.grid(alpha=0.3)# ────────────────────────────────────────# 图4:特征重要性(随机森林)# ────────────────────────────────────────ax4 = fig.add_subplot(gs[1, 0])rf_model = results['随机森林 (RF)']['model']importances = rf_model.feature_importances_std_imp = np.std([tree.feature_importances_ for tree in rf_model.estimators_], axis=0)feat_df = pd.DataFrame({ 'Feature': feature_names, 'Importance': importances, 'Std': std_imp}).sort_values('Importance', ascending=True)colors_bar = plt.cm.viridis(np.linspace(0.2, 0.9, len(feature_names)))bars = ax4.barh(feat_df['Feature'], feat_df['Importance'], xerr=feat_df['Std'], color=colors_bar, edgecolor='white', capsize=3, height=0.6)for bar, val in zip(bars, feat_df['Importance']): ax4.text(val + 0.003, bar.get_y() + bar.get_height() / 2, f'{val:.3f}', va='center', fontsize=8)ax4.axvline(importances.mean(), color='red', linestyle='--', alpha=0.7, label='均值')ax4.set_title('④ 特征重要性 (随机森林)', fontsize=13, fontweight='bold')ax4.set_xlabel('重要性')ax4.legend(fontsize=8)ax4.grid(alpha=0.3, axis='x')# ────────────────────────────────────────# 图5:学习曲线# ────────────────────────────────────────ax5 = fig.add_subplot(gs[1, 1])train_sizes, train_scores, val_scores = learning_curve( best_model, X_scaled, y, cv=5, n_jobs=-1, train_sizes=np.linspace(0.1, 1.0, 10), scoring='r2')train_mean = train_scores.mean(axis=1)train_std = train_scores.std(axis=1)val_mean = val_scores.mean(axis=1)val_std = val_scores.std(axis=1)ax5.plot(train_sizes, train_mean, 'o-', color='#2196F3', lw=2, label='训练集 R²')ax5.plot(train_sizes, val_mean, 's-', color='#FF5722', lw=2, label='验证集 R²')ax5.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.15, color='#2196F3')ax5.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.15, color='#FF5722')ax5.set_title('⑤ 学习曲线', fontsize=13, fontweight='bold')ax5.set_xlabel('训练样本数')ax5.set_ylabel('R² 分数')ax5.legend(fontsize=9)ax5.grid(alpha=0.3)# ────────────────────────────────────────# 图6:多模型对比柱状图# ────────────────────────────────────────ax6 = fig.add_subplot(gs[1, 2])compare_metrics = ['R2', 'RMSE', 'MAE']x_pos = np.arange(len(compare_metrics))width = 0.25for i, (name, res) in enumerate(results.items()): vals = [res[m] for m in compare_metrics] bars = ax6.bar(x_pos + i * width, vals, width, label=name, color=COLORS[i], edgecolor='white', alpha=0.85) for bar, val in zip(bars, vals): ax6.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01, f'{val:.3f}', ha='center', va='bottom', fontsize=7)ax6.set_xticks(x_pos + width)ax6.set_xticklabels(compare_metrics, fontsize=11)ax6.set_title('⑥ 多模型指标对比', fontsize=13, fontweight='bold')ax6.set_ylabel('指标值')ax6.legend(fontsize=8, loc='upper right')ax6.grid(alpha=0.3, axis='y')# ────────────────────────────────────────# 图7:Q-Q 图(检验残差正态性)# ────────────────────────────────────────ax7 = fig.add_subplot(gs[2, 0])(osm, osr), (slope, intercept, r_val) = stats.probplot(residuals, dist='norm')ax7.scatter(osm, osr, alpha=0.5, s=12, c='#9C27B0', edgecolors='none')ax7.plot(osm, slope * np.array(osm) + intercept, 'r-', lw=2, label=f'拟合线 (r={r_val:.4f})')ax7.set_title('⑦ Q-Q 图 (残差正态性检验)', fontsize=13, fontweight='bold')ax7.set_xlabel('理论分位数')ax7.set_ylabel('样本分位数')ax7.legend(fontsize=9)ax7.grid(alpha=0.3)# ────────────────────────────────────────# 图8:特征相关性热力图# ────────────────────────────────────────ax8 = fig.add_subplot(gs[2, 1])corr_matrix = df.corr()mask = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.2f', cmap='RdBu_r', center=0, ax=ax8, square=True, linewidths=0.5, annot_kws={'size': 7}, vmin=-1, vmax=1, cbar_kws={'shrink': 0.8})ax8.set_title('⑧ 特征相关性热力图', fontsize=13, fontweight='bold')ax8.tick_params(axis='both', labelsize=7)# ────────────────────────────────────────# 图9:误差百分比分布 + 累计曲线# ────────────────────────────────────────ax9 = fig.add_subplot(gs[2, 2])abs_pct_error = np.abs(residuals / y_test) * 100abs_pct_error_clipped = np.clip(abs_pct_error, 0, 100) # 限制在100%以内显示ax9_twin = ax9.twinx()# 直方图ax9.hist(abs_pct_error_clipped, bins=50, alpha=0.7, color='#FF9800', edgecolor='white', density=True)ax9.set_xlabel('绝对百分比误差 (%)')ax9.set_ylabel('密度', color='#FF9800')# 累计分布曲线sorted_err = np.sort(abs_pct_error)cumulative = np.arange(1, len(sorted_err) + 1) / len(sorted_err)ax9_twin.plot(sorted_err, cumulative, 'b-', lw=2, label='累计分布')ax9_twin.set_ylabel('累计比例', color='blue')ax9_twin.set_ylim([0, 1.02])# 标注关键分位for pct_thresh in [10, 20, 30]: ratio = (abs_pct_error <= pct_thresh).mean() ax9_twin.axhline(ratio, color='gray', linestyle=':', alpha=0.4) ax9_twin.axvline(pct_thresh, color='gray', linestyle=':', alpha=0.4) ax9.text(pct_thresh + 0.5, ax9.get_ylim()[1] * 0.85, f'{pct_thresh}%线\n({ratio*100:.1f}%样本)', fontsize=7, color='navy')ax9.set_title('⑨ 误差百分比分布与累计曲线', fontsize=13, fontweight='bold')ax9.set_xlim([0, 80])ax9_twin.legend(fontsize=8, loc='center right')ax9.grid(alpha=0.3)# ────────────────────────────────────────# 保存并展示# ────────────────────────────────────────plt.savefig('california_housing_regression_report.png', bbox_inches='tight')plt.show()print("\n✅ 图表已保存至 california_housing_regression_report.png")print("✅ 回归分析完成!").py 文件sklearn / pandas / matplotlib / seaborn / scipy
分类和回归都讲完了,下期我们进入 第三个核心任务——无监督学习(聚类分析)。用 K-Means、DBSCAN、层次聚类三种算法对客户数据做分群,从轮廓系数到 t-SNE 可视化,继续保持"指标 + 图表"双线并行的风格。
关注我,一起从"跑通代码"走向"真正理解模型"。
📌 互动话题:你在做回归任务时,最容易忽略的评估步骤是什么?你遇到过 R² 很高但模型实际不好用的情况吗?欢迎在评论区分享你的经历。
如果觉得有用,点个在看,转发给同样在学机器学习的朋友吧 👇
本文涉及的所有代码和图表均基于 scikit-learn 1.x + Python 3.12环境测试通过。数据来源为 sklearn 内置加州房价数据集。