免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

波士顿房价预测作业高分指南:数据清洗、特征工程与模型评估全解析

波士顿房价预测作业高分指南:数据清洗、特征工程与模型评估全解析 简介这份资源面向机器学习初学者与需要完成课程大作业的学生围绕经典的波士顿房价数据集展开回归分析实战。项目覆盖完整流程数据预处理、模型训练、结果评估与可视化分析并实现批处理梯度下降、随机梯度下降、岭回归与LASSO回归四种算法帮助读者理解线性回归的优化思路与正则化差异。压缩包共12个文件约944KB包含3个csv数据与结果文件、3个py源码脚本、3个png可视化图表、2个txt说明及1个md文档结构清晰便于对照代码与输出结果学习。目前已有120人学习下载。读者可获得可直接运行的完整源码、模型对比与系数分析结果、训练曲线与预测图以及配套文档说明适合作为期末大作业参考或回归算法入门练习。1. 波士顿房价预测这个作业真正拉开差距的不是模型波士顿房价预测几乎是每个机器学习入门者绕不开的第一个回归任务。很多人拿到数据、调个sklearn的LinearRegression、跑出一个均方误差就以为作业结束了。但真正拿高分的项目差距从来不在模型选得多花哨而在于数据清洗是否干净、特征工程是否讲得清、评估指标是否选得对、文档说明是否能让别人照着跑通。这篇笔记就围绕「机器学习作业-波士顿房价预测作业源代码文档说明」这个典型场景把从数据加载到结果解释的完整链路拆开讲。适合正在做机器学习线性回归实验的学生也适合想补一遍回归建模基本功的从业者。下面所有代码都可以直接复制运行参数我会逐个说明为什么这么设。2. 先把数据和任务边界搞清楚波士顿房价预测到底在预测什么2.1 数据集字段含义与回归目标波士顿房价数据集Boston Housing是一个经典的回归数据集目标是预测波士顿不同区域的房价中位数单位千美元。数据集包含 506 条样本每条样本有 13 个特征涵盖犯罪率、住宅用地比例、是否临河、空气质量、平均房间数、到市中心距离、税率、师生比等。目标变量是MEDV即自有住房价格中位数。这里有个容易被忽略的点MEDV在原始数据里被截断在 50 千美元也就是说所有超过 50 的样本都被记成了 50。这意味着如果你不做处理模型在高价区间的预测会系统性偏低。常见做法是要么接受这个偏差并在文档里说明要么把MEDV50的样本单独标记出来做敏感性分析。我一般会在文档说明里写清楚这一点因为这是体现你是否真正理解数据的关键细节。另一个字段是CHAS表示是否临查尔斯河取值 0 或 1。它是唯一的类别型特征不需要做独热编码直接当数值用即可。RAD和TAX有较强的相关性后续做特征选择时可以留意。2.2 为什么线性回归仍然是这个作业的首选基线很多同学一上来就想用 XGBoost 或神经网络觉得模型越复杂分数越高。但在波士顿房价这个任务上线性回归及其正则化变体Ridge、Lasso往往能给出非常接近甚至更好的结果原因有三第一样本量只有 506复杂模型极易过拟合第二特征与目标之间存在较强的线性关系第三线性模型的系数可以直接解释文档说明里能写清楚每个特征对房价的影响方向和大小这是作业评分的重要加分项。所以我的建议是把线性回归作为主模型Ridge 和 Lasso 作为对比最多再加一个决策树或随机森林作为非线性对照。这样既体现了你对模型选型的思考又不会因为模型太复杂而讲不清楚。2.3 最小可运行的数据加载与划分代码import numpy as np import pandas as pd from sklearn.datasets import load_boston # 注意新版sklearn已移除见下方说明 from sklearn.model_selection import train_test_split # 加载数据 boston load_boston() X pd.DataFrame(boston.data, columnsboston.feature_names) y pd.Series(boston.target, nameMEDV) # 查看基本统计信息 print(X.describe()) print(目标变量描述) print(y.describe()) # 划分训练集和测试集固定随机种子保证可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) print(f训练集大小{X_train.shape}测试集大小{X_test.shape})这段代码做了三件事加载数据、查看分布、按 8:2 划分。random_state42是为了保证每次运行结果一致这在作业提交里非常重要否则助教跑出来的结果和你文档里写的不一样直接扣分。test_size0.2是回归任务里常用的比例506 条样本下测试集约 101 条足够评估又不至于让训练数据太少。提示新版 scikit-learn 已经移除了load_boston如果报错可以用sklearn.datasets.fetch_openml(nameboston)替代或者直接从本地 CSV 读取。文档说明里一定要写清楚你用的数据来源和版本这是很多人翻车的地方。3. 特征工程与数据清洗把 13 个特征逐个过一遍3.1 缺失值与异常值处理波士顿房价数据集本身没有缺失值但这不代表你可以跳过这一步。作业文档里如果写「数据无缺失值故未处理」显得很敷衍。正确做法是先检查缺失值确认没有之后再检查异常值。比如CRIM犯罪率的分布极度右偏最大值远超均值ZN住宅用地比例有大量 0 值B字段的计算公式存在争议。这些都需要在文档里说明你的处理策略。我一般会做两步第一用 IQR 方法标记异常值但不直接删除而是生成一个「是否异常」的标记特征让模型自己决定第二对右偏严重的特征做对数变换比如CRIM、DIS、LSTAT。对数变换后线性模型的残差会更接近正态分布回归效果通常有提升。import numpy as np # 检查缺失值 print(缺失值统计) print(X.isnull().sum()) # 对右偏特征做对数变换加1避免log(0) skewed_features [CRIM, ZN, DIS, LSTAT] for feat in skewed_features: X_train[feat] np.log1p(X_train[feat]) X_test[feat] np.log1p(X_test[feat]) # 用IQR标记异常值仅作分析不删除 def mark_outliers(df, columns): outlier_flag pd.Series(0, indexdf.index) for col in columns: Q1 df[col].quantile(0.25) Q3 df[col].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR outlier_flag | ((df[col] lower) | (df[col] upper)).astype(int) return outlier_flag X_train[is_outlier] mark_outliers(X_train, skewed_features) X_test[is_outlier] mark_outliers(X_test, skewed_features) print(f训练集中异常值比例{X_train[is_outlier].mean():.2%})np.log1p等价于log(1x)专门处理含 0 的特征。注意变换要在训练集和测试集上分别做但参数比如均值、标准差只能从训练集学否则就是数据泄露。这里对数变换不涉及跨样本统计量所以直接分别变换没问题。is_outlier这个特征在树模型里有用在线性模型里可能引入噪声你可以根据验证集表现决定是否保留。3.2 特征缩放与多项式特征线性回归对特征尺度敏感尤其是用了 Ridge 或 Lasso 之后。标准化StandardScaler是标配把每个特征变成均值 0、方差 1。注意scaler 只能在训练集上 fit然后 transform 测试集。这个顺序搞反了作业直接判数据泄露。另外波士顿房价任务里特征之间的交互作用不可忽略。比如RM房间数和LSTAT低收入人群比例的交互项往往能提升模型表现。我一般会加二项式特征但只加平方项和两两交互项不做三次以上避免维度爆炸。from sklearn.preprocessing import StandardScaler, PolynomialFeatures from sklearn.pipeline import Pipeline from sklearn.linear_model import Ridge # 构建管道多项式特征 - 标准化 - Ridge回归 pipe Pipeline([ (poly, PolynomialFeatures(degree2, include_biasFalse)), (scaler, StandardScaler()), (ridge, Ridge(alpha1.0)) ]) # 在训练集上拟合 pipe.fit(X_train.drop(columns[is_outlier]), y_train) # 评估 train_score pipe.score(X_train.drop(columns[is_outlier]), y_train) test_score pipe.score(X_test.drop(columns[is_outlier]), y_test) print(f训练集R²{train_score:.4f}测试集R²{test_score:.4f})PolynomialFeatures(degree2)会把 13 个特征扩展到约 104 个含平方和交互include_biasFalse是因为后面有标准化和 Ridge 自带截距。Ridge(alpha1.0)是默认正则化强度实际作业里应该用交叉验证选 alpha。管道的好处是 fit 时自动按顺序执行predict 时也按同样顺序变换不会出现训练和预测流程不一致的低级错误。3.3 用交叉验证选正则化参数Ridge 和 Lasso 的alpha直接决定模型复杂度。alpha 太大模型欠拟合alpha 太小过拟合。我一般用 5 折交叉验证在训练集上选选完之后再在测试集上报告最终结果。注意测试集只能用一次不能反复调参。from sklearn.linear_model import RidgeCV, LassoCV from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state42) # Ridge交叉验证 ridge_cv RidgeCV(alphasnp.logspace(-3, 3, 50), cvkf) ridge_cv.fit(X_train_scaled, y_train) print(fRidge最优alpha{ridge_cv.alpha_:.4f}) print(fRidge测试集R²{ridge_cv.score(X_test_scaled, y_test):.4f}) # Lasso交叉验证 lasso_cv LassoCV(alphasnp.logspace(-3, 3, 50), cvkf, max_iter10000) lasso_cv.fit(X_train_scaled, y_train) print(fLasso最优alpha{lasso_cv.alpha_:.4f}) print(fLasso测试集R²{lasso_cv.score(X_test_scaled, y_test):.4f}) print(fLasso非零系数个数{np.sum(lasso_cv.coef_ ! 0)})np.logspace(-3, 3, 50)生成从 0.001 到 1000 的 50 个对数等距值覆盖了常见的正则化强度范围。LassoCV的max_iter10000是为了防止迭代不收敛默认 1000 在特征多的时候经常不够。Lasso 的好处是会把不重要的特征系数压到 0你可以据此做特征选择在文档里写清楚哪些特征被剔除了这是很好的分析素材。4. 模型训练与评估R²、MSE、残差图一个都不能少4.1 评估指标的选择与解释回归任务最常用的指标是 MSE、RMSE、MAE 和 R²。MSE 对大误差惩罚重RMSE 量纲和原变量一致MAE 对异常值更鲁棒R² 表示模型解释了多少方差。作业里我建议四个都报但重点解释 RMSE 和 R²。RMSE 的单位是千美元比如 RMSE4.5 意味着预测平均偏离真实房价 4500 美元这样写文档读者才有直观感受。R² 有个坑它随着特征增多只会上升或不变所以不能用来比较特征数不同的模型。如果你加了多项式特征后 R² 涨了不代表模型真的更好要看调整后的 R² 或者直接用交叉验证的 RMSE。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate_model(model, X_train, y_train, X_test, y_test, nameModel): y_train_pred model.predict(X_train) y_test_pred model.predict(X_test) metrics { 模型: name, 训练集RMSE: np.sqrt(mean_squared_error(y_train, y_train_pred)), 测试集RMSE: np.sqrt(mean_squared_error(y_test, y_test_pred)), 测试集MAE: mean_absolute_error(y_test, y_test_pred), 测试集R²: r2_score(y_test, y_test_pred) } return metrics # 假设已有训练好的模型 results [] results.append(evaluate_model(ridge_cv, X_train_scaled, y_train, X_test_scaled, y_test, Ridge)) results.append(evaluate_model(lasso_cv, X_train_scaled, y_train, X_test_scaled, y_test, Lasso)) results_df pd.DataFrame(results) print(results_df.to_string(indexFalse))这段代码把四个指标打包成一个函数方便对比不同模型。注意训练集和测试集的 RMSE 都要报如果训练集 RMSE 远小于测试集说明过拟合如果两者都很大说明欠拟合。文档里最好画一张训练集和测试集 RMSE 随 alpha 变化的曲线直观展示正则化的作用。4.2 残差分析与模型诊断残差图是回归作业里最能体现你功力的部分。理想情况下残差应该随机分布在 0 附近不呈现任何规律。如果残差随预测值增大而增大说明存在异方差如果残差呈 U 型说明模型欠拟合需要加非线性项。import matplotlib.pyplot as plt y_pred ridge_cv.predict(X_test_scaled) residuals y_test - y_pred fig, axes plt.subplots(1, 2, figsize(12, 5)) # 残差 vs 预测值 axes[0].scatter(y_pred, residuals, alpha0.6) axes[0].axhline(y0, colorr, linestyle--) axes[0].set_xlabel(预测值) axes[0].set_ylabel(残差) axes[0].set_title(残差 vs 预测值) # 残差分布 axes[1].hist(residuals, bins20, edgecolorblack) axes[1].set_xlabel(残差) axes[1].set_ylabel(频数) axes[1].set_title(残差分布) plt.tight_layout() plt.savefig(residual_analysis.png, dpi150) plt.show()左图看残差是否均匀右图看残差是否近似正态。如果右图明显偏斜可以考虑对目标变量做变换比如log(MEDV)。不过要注意变换后预测值要反变换回来才能算 RMSE否则量纲不对。我一般会在文档里同时报告原始尺度和变换尺度的结果让读者自己判断。4.3 特征重要性排序与业务解释线性模型的系数就是特征重要性但要注意标准化之后的系数才能直接比较大小。正系数表示该特征增加会推高房价负系数相反。比如RM房间数系数为正LSTAT低收入比例系数为负CRIM犯罪率系数为负这些和直觉一致文档里可以展开解释。# 获取标准化后的系数 feature_names X_train.drop(columns[is_outlier]).columns coefs pd.Series(ridge_cv.coef_, indexfeature_names) coefs_sorted coefs.sort_values(keyabs, ascendingFalse) print(Ridge回归系数按绝对值排序) print(coefs_sorted.head(10)) # 可视化 plt.figure(figsize(10, 6)) coefs_sorted.head(10).plot(kindbarh) plt.xlabel(系数值) plt.title(Ridge回归前10重要特征) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(feature_importance.png, dpi150) plt.show()这里有个细节如果你用了多项式特征系数对应的特征名会变成RM^2、RM LSTAT这种解释起来更复杂。我一般会在文档里先解释原始特征的系数再单独说明几个重要的交互项。比如RM和LSTAT的交互项如果系数为负说明低收入区域里房间数对房价的正面影响被削弱了这种洞察是作业拿高分的关键。5. 避坑与排查波士顿房价预测作业里最容易翻车的 5 个地方5.1 数据泄露标准化在划分之前做了现象测试集 R² 高得离谱接近 0.95 以上但助教跑出来只有 0.7。原因先对整个数据集做了标准化或填充缺失值再划分训练测试集导致测试集的统计信息泄露到了训练过程。解决所有涉及跨样本统计量的操作标准化、归一化、缺失值填充、PCA必须在训练集上 fit然后 transform 测试集。用Pipeline是最稳妥的做法。5.2 目标变量截断未处理现象模型对高价房预测普遍偏低残差图在高价区呈现明显负偏差。原因MEDV在 50 处截断模型学到的上限就是 50。解决在文档里明确说明这个数据特性可以额外训练一个分类模型判断房价是否超过 50或者直接接受这个偏差并在结论里讨论。不要假装没看见。5.3 随机种子不固定导致结果不可复现现象文档里写 R²0.82助教跑出来 0.79要求你解释。原因train_test_split和模型初始化没有固定random_state。解决所有涉及随机性的地方都设random_state42包括数据划分、交叉验证折数、模型初始化。在文档里写明「固定随机种子 42结果可复现」。5.4 用测试集调参现象测试集 R² 很高但换一组测试数据就崩了。原因反复在测试集上调整 alpha 或特征组合测试集实际上变成了验证集。解决训练集再切一部分出来做验证集或者直接用交叉验证在训练集上选参。测试集只在最终评估时用一次用完就封存。5.5 文档说明只贴代码不写结论现象源代码能跑但文档说明只有代码和截图没有分析。原因把作业当成了编程练习忽略了机器学习任务的核心是「从数据中提取洞察」。解决文档里至少包含数据探索结论、特征工程理由、模型对比表格、残差分析解读、业务含义解释。代码是附录分析才是正文。6. 让作业脱颖而出的两个进阶技巧系数稳定性检验与预测区间6.1 用 Bootstrap 检验系数稳定性线性回归的系数是点估计但作业里如果能给出系数的置信区间档次立刻不一样。Bootstrap 是一种非参数方法通过有放回重采样估计系数的分布。具体做法从训练集里重复抽样 1000 次每次拟合 Ridge 回归记录系数最后算均值和 2.5%/97.5% 分位数。def bootstrap_coefs(X, y, n_boot1000, alpha1.0): coefs [] n len(X) for _ in range(n_boot): idx np.random.choice(n, n, replaceTrue) X_boot X.iloc[idx] y_boot y.iloc[idx] model Ridge(alphaalpha) model.fit(X_boot, y_boot) coefs.append(model.coef_) coefs np.array(coefs) return np.mean(coefs, axis0), np.percentile(coefs, [2.5, 97.5], axis0) mean_coefs, ci bootstrap_coefs(X_train_scaled, y_train) for i, name in enumerate(feature_names): print(f{name}: 均值{mean_coefs[i]:.4f}, 95%CI[{ci[0][i]:.4f}, {ci[1][i]:.4f}])如果某个特征的置信区间跨过 0说明它的影响不显著可以在文档里讨论是否剔除。这个方法不需要额外假设适合小样本场景。注意 Bootstrap 只用于分析不要用它来调参。6.2 用分位数回归给出预测区间普通最小二乘只给点预测但实际房价预测里给出一个区间更有价值。分位数回归可以预测比如 10% 和 90% 分位数形成 80% 预测区间。sklearn的QuantileRegressor可以直接用。from sklearn.linear_model import QuantileRegressor # 训练10%和90%分位数回归 qr_low QuantileRegressor(quantile0.1, alpha0.01, solverhighs) qr_high QuantileRegressor(quantile0.9, alpha0.01, solverhighs) qr_low.fit(X_train_scaled, y_train) qr_high.fit(X_train_scaled, y_train) # 在测试集上生成预测区间 y_low qr_low.predict(X_test_scaled) y_high qr_high.predict(X_test_scaled) coverage np.mean((y_test y_low) (y_test y_high)) print(f80%预测区间的实际覆盖率{coverage:.2%})alpha0.01是 L1 正则化强度防止分位数回归过拟合。solverhighs是线性规划求解器比默认的更快更稳。覆盖率如果接近 80%说明区间校准得不错如果远低于 80%说明区间太窄需要增大 alpha 或检查数据分布。这个技巧在作业里属于加分项能体现你对回归任务不确定性的理解。最后说个我自己的习惯每次提交作业前我会把文档从头到尾读一遍假装自己是助教看能不能在不看代码的情况下理解整个流程。如果有一句话需要回头翻代码才能懂那就说明文档没写清楚。这个习惯帮我避开了很多次「代码跑通但分数不高」的翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表