Python机器学习房价预测全流程:数据预处理、特征工程与模型评估

发布时间:2026/10/11 10:50:08
Python机器学习房价预测全流程:数据预处理、特征工程与模型评估 简介这是一份Python机器学习房价预测实战项目包适合计算机相关专业正在完成课程设计、期末大作业的学生也适合需要项目实战练习的初学者。项目以北京二手房房价预测为完整场景覆盖数据采集、特征处理、模型训练、评估与可视化全流程是经导师指导并认可的98分高分课程设计。压缩包共26个文件大小约1.29MB内容以15个Python脚本为核心涵盖爬虫采集、可视化绘图、模型训练等模块另含1个Jupyter Notebook分析文档、2个CSV数据集、1份HTML分析报告以及使用说明和README不仅便于直接运行也适合对照学习。已有896人学习下载。借助源码与数据读者可以了解如何从链家、安居客抓取二手房数据完成清洗与特征工程并使用机器学习模型预测房价最终输出可视化图表与报告可作为课程设计、答辩展示或进阶练习的实用参考。1. 从一个能跑通的 python 机器学习房价预测项目说起如果你搜“python机器学习房价预测”搜到这份资源大概率是课程设计要交差了或者刚学完 sklearn 想找一份完整代码练手。我拆完这个项目的感受是它没有堆砌花哨算法而是把“数据预处理 → 特征工程 → 模型训练 → 评估可视化 → 结果解释”这条最标准的机器学习流水线完整走了一遍波士顿房价数据集从读入到输出 R²、RMSE 指标和预测对比图一步不缺。适合两类人一是要做 python 机器学习课程设计、需要一份能讲清楚原理的源码做基底的在校生二是刚看完理论想看看真实数据长什么样、有哪些坑的入门者。项目里附带的使用说明把每段代码对应哪个环节标注得很清楚这对照着复现的人来说比单纯源码有用得多。接下来我按自己拆解的顺序从数据预处理讲到模型对比再把最容易翻车的几个坑单独拎出来说。2. 数据先立住房价预测的预处理与特征工程2.1 数据长什么样先摸清字段含义和缺失值分布这个项目用的是经典波士顿房价数据集506 条样本、13 个特征加 1 个目标变量 MEDV自住房屋价值中位数。在 sklearn 老版本里可以直接load_boston()加载新版 sklearn 出于数据伦理原因移除了它但原始 CSV 在项目文件夹里有备份。我建议不管用内置加载还是 CSV 读取第一步都先做两件事看一眼字段说明跑一次缺失值统计。import pandas as pd df pd.read_csv(housing.csv) print(df.shape) # (506, 14) print(df.dtypes) # 确认每列类型避免 object 混入数值列 # 缺失值统计房价预测里最常见的坑是某列有少量 NaN直接训练会报错或静默出问题 missing df.isnull().sum() print(missing[missing 0]) # 目标变量分布偏态严重的房价直接喂给线性模型误差会集中在高端价位 import matplotlib.pyplot as plt df[MEDV].hist(bins50) plt.show()逻辑说明先确认数据形状和列类型df.isnull().sum()会列出每一列的缺失值数量这一步决定后面是直接删行还是做填充。很多入门者拿到数据后直接fit()等报错才回头查 NaN浪费一轮调试。目标变量直方图是为了看分布形态——房价数据常见右偏如果偏态明显要么取对数变换要么在评估时对高价房单独看误差。参数说明bins50控制直方图分箱数数值偏大能看出局部波动偏小看整体形态。这里没有硬性标准能看出单峰还是多峰、是否右偏即可。2.2 数值特征标准化先划分数据集再做顺序不能反波士顿房价特征量纲差异很大RM平均房间数是 3 到 9 之间的个位数TAX房产税率是 200 到 700 的百位数DIS到就业中心加权距离又是小数。如果不做标准化线性模型和基于距离的计算会把量纲大的特征当成主导变量。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X df.drop(MEDV, axis1) y df[MEDV] # 先划分再标准化这是防止数据泄漏的关键顺序 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() # fit 只在训练集上做测试集只 transform X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)逻辑说明train_test_split的参数test_size0.2表示拿 20% 样本做测试集random_state42固定随机种子保证每次运行划分结果一致方便复现对比。标准化的fit_transform和transform分离是关键——fit计算出训练集的均值和标准差transform用这套参数去转换测试集。如果对全量数据先fit_transform再划分测试集的信息就已经渗入训练过程这叫数据泄漏会让评估指标虚高。参数说明StandardScaler()默认用均值 0、标准差 1 的标准化方式对近似正态分布的特征效果最好。如果特征里有大量离群点也可以用RobustScaler它基于中位数和四分位距对离群值不敏感。这个项目里用 StandardScaler 够用但我在自己的数据上一般习惯先跑一次RobustScaler对比看 RMSE 变化大不大。2.3 类别变量编码这个数据集没有但你自己的数据一定有波士顿房价数据集全是数值特征项目里没做类别编码。但很多拿这个项目改造成自己数据的同学会遇到“小区名”“朝向”“是否近地铁”这类字符串列。我在这里补一段通用处理因为这是从这份源码延伸到真实场景最常用到的操作。from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer # 假设 df 里 neighborhood 是类别列RM 和 LSTAT 是数值列 preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), [RM, LSTAT]), (cat, OneHotEncoder(dropfirst), [neighborhood]) ] )逻辑说明ColumnTransformer允许对不同列用不同预处理策略数值列走标准化类别列走独热编码。dropfirst的意思是类别变量有 k 个取值就生成 k-1 个哑变量避免多重共线性——线性模型里如果两个哑变量完全相关系数会不稳定。这个设计在房价预测这种特征相关性强的场景里尤其重要。参数说明OneHotEncoder默认返回稀疏矩阵特征多时省内存dropfirst在需要把结果喂给线性模型时建议加上。如果类别列的取值有顺序关系比如“小、中、大”用OrdinalEncoder更合适但要清楚这是强加了一个等距假设大部分场景下并不成立。3. 模型训练与对比线性回归、Lasso 和随机森林的选型逻辑3.1 先从线性回归做基线房价预测的基准线在哪里我从不用复杂模型直接起步。线性回归在这个项目里扮演的是“下限探测器”角色——如果连线性回归的 R² 都不到 0.7那说明特征工程或数据清洗有问题不是模型的问题。项目源码里也是这个顺序先跑线性回归建立基线再逐步往上加复杂度。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score lr LinearRegression() lr.fit(X_train_scaled, y_train) y_pred_lr lr.predict(X_test_scaled) rmse_lr mean_squared_error(y_test, y_pred_lr, squaredFalse) r2_lr r2_score(y_test, y_pred_lr) print(fLinearRegression RMSE{rmse_lr:.2f} R2{r2_lr:.3f}) # 看特征系数找出模型认为最重要的几个因素 coef_df pd.DataFrame({ feature: X.columns, coef: lr.coef_ }).sort_values(coef, keylambda x: abs(x), ascendingFalse) print(coef_df.head(5))逻辑说明mean_squared_error的squaredFalse参数直接返回 RMSE均方根误差单位是房价本身的单位比如千美元便于直观理解squaredTrue才是 MSE。r2_score返回决定系数代表模型解释了目标变量多少比例的方差。这两项要一起看R² 高但 RMSE 也高说明对个别异常样本预测极差分布图能暴露这个问题。参数说明线性回归没有超参数可调但它给出的coef_是最直接的特征重要性参考——系数绝对值越大该特征对房价的影响越强。注意标准化之后系数才可以这样横向比较未标准化的系数受量纲影响没有可比性。3.2 Lasso 回归用 L1 正则化做特征选择波士顿房价的 13 个特征里有几位和房价的相关性其实很弱比如 B非裔人口比例这个特征在建模时就会引发争议。Lasso 的 L1 惩罚项能把不重要特征的系数压缩到 0达到特征选择的效果。项目里用网格搜索找最佳 alpha我觉得这个思路是对的但网格范围值得调整。from sklearn.linear_model import Lasso from sklearn.model_selection import GridSearchCV param_grid {alpha: [0.001, 0.01, 0.1, 1, 10, 50]} lasso Lasso(max_iter10000, random_state42) grid GridSearchCV( lasso, param_grid, cv5, scoringneg_mean_squared_error ) grid.fit(X_train_scaled, y_train) best_alpha grid.best_params_[alpha] print(fbest alpha: {best_alpha}) y_pred_lasso grid.predict(X_test_scaled) rmse_lasso mean_squared_error(y_test, y_pred_lasso, squaredFalse) r2_lasso r2_score(y_test, y_pred_lasso) print(fLasso RMSE{rmse_lasso:.2f} R2{r2_lasso:.3f})逻辑说明GridSearchCV的param_grid定义了 alpha 的搜索空间cv5表示五折交叉验证——训练集被切成 5 份轮流拿 1 份做验证其余 4 份训练最终取 5 次验证分数的均值。scoringneg_mean_squared_error是因为 sklearn 的网格搜索默认“分数越大越好”而误差是越小越好所以取负值让它兼容。max_iter10000是给 Lasso 的坐标下降算法足够的迭代次数防止收敛警告。参数说明alpha 的搜索范围我一般会先用对数尺度拉一遍从 0.0001 到 100 至少 6 个值找到最优区域后再加密。alpha 过小趋近线性回归失去正则化意义过大则把所有系数压到 0模型变成预测均值。如果最优 alpha 落在搜索边界说明范围没设置对要扩大重搜。3.3 随机森林非线性模型的兜底方案随机森林在这份源码里作为对比模型出现它和线性模型互补线性回归假设特征和房价是线性关系随机森林能捕捉非线性互动比如“房间数多但区位差”的组合效应。但它也有代价——可解释性差、容易过拟合、对特征缩放不敏感。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth10, min_samples_split5, min_samples_leaf2, random_state42 ) rf.fit(X_train_scaled, y_train) y_pred_rf rf.predict(X_test_scaled) rmse_rf mean_squared_error(y_test, y_pred_rf, squaredFalse) r2_rf r2_score(y_test, y_pred_rf) print(fRandomForest RMSE{rmse_rf:.2f} R2{r2_rf:.3f}) # 随机森林自带特征重要性 feat_imp pd.DataFrame({ feature: X.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(feat_imp.head(5))逻辑说明n_estimators300表示 300 棵决策树每棵树用 Bootstrap 采样有放回抽样的训练子集训练最终结果取所有树的平均。max_depth10限制单棵树深度防止每一棵树都长到把训练集背下来。min_samples_split5和min_samples_leaf2是叶节点最小样本数约束这两个参数是控过拟合的主力比max_depth更细粒度。参数说明RandomForestRegressor对特征缩放不敏感数据标准化与否不影响结果但项目里统一走标准化流程代码上更整洁。调参时优先动min_samples_leaf从 1 往上加到 5、10验证集误差通常会先降后升找到拐点就停。n_estimators到 200 以上收益递减不用追求 1000 棵树训练时间翻倍但精度提升可以忽略。3.4 三个模型的结果怎么对比不能只盯 R²项目源码最后把三个模型的指标打印在一起。我建议把对比做成表格这是课程设计答辩时最直观的呈现方式。模型RMSE千美元R²特征重要性来源线性回归约 4.9约 0.71回归系数Lasso约 4.9约 0.72非零系数随机森林约 3.6约 0.85feature_importances_随机森林赢在能建模非线性关系但它的预测不可解释——你没法跟客户解释为什么这个房子值这个价。线性模型虽然精度低一截但每个特征的系数都能翻译成“房间数每多一间房价中位数约高 2.8 千美元”。课程设计里如果只追求分数随机森林是安全牌如果想在答辩时讲出故事线性回归加 Lasso 的组合更有内容可讲。我拆的项目里三个模型都保留最终提交用随机森林但在文档里把线性模型的系数分析作为“可解释性讨论”写了进去。4. 评估与可视化从指标到分布图把模型“看透”4.1 RMSE 和 R² 各自的意义评估房价预测模型的组合视角RMSE 告诉你平均错多少R² 告诉你模型解释了百分之多少的方差。但这两个指标有个共同盲区它们都是全局指标掩盖了局部预测失败。房价预测中中低价位房子的预测误差通常小高端房子的误差可能非常大——因为高端样本数量少模型没学到足够模式。项目里画了预测值 vs 真实值的散点图这个图能直接暴露系统性偏差。import numpy as np plt.figure(figsize(8, 6)) plt.scatter(y_test, y_pred_rf, alpha0.6, edgecolorsw) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, lw2, labelPerfect Prediction) plt.xlabel(Actual Price (1000$)) plt.ylabel(Predicted Price (1000$)) plt.title(RandomForest: Actual vs Predicted) plt.legend() plt.show() # 残差分布预测值和真实值的差 residuals y_test - y_pred_rf plt.hist(residuals, bins30, edgecolorwhite) plt.xlabel(Residual (1000$)) plt.ylabel(Count) plt.show()逻辑说明散点图里点越贴近红色虚线预测越准。如果点在对角线下方聚集说明模型系统性高估在上方聚集则是系统性低估。残差直方图如果大致呈以 0 为中心的钟形分布说明误差是随机的如果明显偏左或偏右说明模型存在偏差比如低估高端房价。参数说明alpha0.6控制散点透明度因为 100 多个点在重叠时很难分辨密度edgecolorsw给点加白色描边让重叠点也能看清边界。残差直方图的bins30控制分组数量样本量小可以降到 20避免直方图出现太多空洞。4.2 学习曲线判断模型到底缺数据还是缺复杂度这份源码在最后一个环节画了学习曲线这是我强烈建议保留的部分。学习曲线能区分两种困境训练集和验证集误差都很高且靠拢说明欠拟合模型太简单训练集误差远低于验证集且差距不收敛说明过拟合。前者加特征或换复杂模型后者加数据或加强正则化。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( rf, X_train_scaled, y_train, cv5, train_sizesnp.linspace(0.1, 1.0, 5), scoringneg_mean_squared_error ) train_rmse np.sqrt(-train_scores.mean(axis1)) val_rmse np.sqrt(-val_scores.mean(axis1)) plt.plot(train_sizes, train_rmse, o-, labelTrain RMSE) plt.plot(train_sizes, val_rmse, o-, labelValidation RMSE) plt.xlabel(Training Set Size) plt.ylabel(RMSE) plt.legend() plt.show()逻辑说明learning_curve的train_sizes参数指定在训练集的 10%、32.5%、55%、77.5%、100% 这五个比例点上分别训练模型并评估。每个点上做五折交叉验证所以train_scores和val_scores的形状是 (5, 5)——5 个训练尺寸乘以 5 折。取每折均值后转成 RMSE 再画两条曲线就能直观看到误差随数据量增加的收敛趋势。参数说明np.sqrt(-train_scores.mean(axis1))里的负号是因为learning_curve的 scoring 参数和 GridSearchCV 一样遵循“越高越好”约定MSE 取负后要变回正值再开方。train_sizes用np.linspace(0.1, 1.0, 5)生成 5 个等间距比例如果想看更细的收敛过程改成 8 个点。4.3 交叉验证分数别被单次划分的随机性骗了单次train_test_split的 RMSE 带有运气成分——如果测试集里恰好多几个高端房价样本RMSE 会飙升反之则偏低。项目里用了五折交叉验证来缓解这个问题我觉得这是判断模型真实水平的底线操作。from sklearn.model_selection import cross_val_score cv_scores cross_val_score( rf, X_train_scaled, y_train, cv5, scoringneg_mean_squared_error ) cv_rmse np.sqrt(-cv_scores) print(fCV RMSE: {cv_rmse}) print(fCV RMSE mean: {cv_rmse.mean():.2f} ± {cv_rmse.std():.2f})逻辑说明cross_val_score会把训练集分成 5 份每次用 4 份训练、1 份验证最终输出 5 个分数。cv_rmse.mean()是模型真实水平的更可靠估计std表示这 5 次评估的波动范围——如果标准差大于 1 个千美元说明模型对数据划分非常敏感此时单独报告某一次test_score没有意义。参数说明cv5是默认值对 506 条样本的数据集够用。如果换到更大的数据集cv10会得到更稳定的估计但训练时间翻倍。交叉验证分数和最终测试集分数之间存在轻微差异是正常的后者通常会略好一点因为测试集不参与任何训练决策。5. 避坑指南房价预测里四个见一次翻一次车的陷阱5.1 数据泄漏标准化顺序搞反指标虚高而不自知现象先对全量数据做fit_transform再划分训练测试集测试集 R² 高达 0.9但换到真实数据预测时效果崩盘。原因StandardScaler().fit_transform(X)在计算均值和标准差时已经“看见”了测试集的数据分布。测试集信息从预处理阶段就渗入模型训练评估指标是在“半开卷考试”里拿到的成绩自然虚高。解决严格遵循“先划分、后预处理”的顺序fit只作用在训练集上测试集只调用transform。这是数据泄漏里最常见的一种我在代码里用注释标红提醒自己。特征选择也要放进交叉验证流程里用全量数据选特征再做交叉验证同样属于泄漏。5.2 索引错位模型预测结果和原始数据对不上号现象把预测结果y_pred和y_test对比画图发现点了两遍但图完全错位或者按索引去查某个样本的预测值查到的是另一条记录。原因train_test_split默认会打乱数据顺序y_test的索引不再是原始的 0 到 505而是随机排列的索引。直接用df.loc去匹配预测结果会把真实值和预测值张冠李戴。解决划分后用reset_index(dropTrue)重建索引或者在分析时统一用位置索引。我一般在划分后立刻执行y_test y_test.reset_index(dropTrue)这样后续所有操作都基于位置对齐不会再错位。项目源码里在可视化前有一行y_test y_test.reset_index(dropTrue)我一开始没注意后来自己复现时才体会到这行的分量。5.3 过拟合信号训练集指标吊打验证集模型把噪声也背下来了现象随机森林在训练集上 R² 高达 0.95验证集只有 0.8差距随n_estimators增大而扩大。原因决策树在不加约束时会把训练集每个样本的细节都记住包括噪声。随机森林集成了几百棵树也没有完全缓解这个问题——树的复杂度太高集成只是把方差压小偏差依旧存在。解决优先调min_samples_leaf和max_depth。min_samples_leaf10强制每个叶节点至少有 10 个样本模型被迫学习更通用的模式。我习惯的做法是把min_samples_leaf从 2 开始按 2、5、10、20 逐个试同时观察交叉验证 RMSE 的变化找到拐点就停。如果加约束后训练集和验证集误差同时上升才是真的欠拟合要反过来减少约束。5.4 数据集内部有结构随机划分把同类样本分进训练和测试现象测试集 RMSE 高得离谱且无论换什么模型都稳定偏高检查特征分布发现测试集和训练集的某个特征分布明显不同。原因如果数据本身包含分组结构比如同一片区的房子被随机分进了训练集和测试集模型相当于“考试时见过同班同学的答案但换了考场就懵”。波士顿房价数据集本身没有明确分组但你自己采集的数据经常有。解决分组场景下用GroupShuffleSplit代替train_test_split按小区名或采集批次分组保证同一组的样本不会同时出现在训练集和测试集。判断依据是数据里是否有类似 ID、日期、小区名这样的字段——只要你觉得“这个字段决定了数据间的关联”就把它当成分组字段处理。项目源码里没有展示这个场景但这是我拿这份代码去处理其他房价数据时踩过的坑。6. 把项目玩深一层用学习曲线诊断出模型真正的瓶颈拿到这份资源最值得做的第一件事不是调参而是跑一遍学习曲线判断你的模型瓶颈到底在哪。训练集和验证集两条曲线的差距是模型改进空间的直接度量。def plot_learning_curve(model, X, y, cv5, titleLearning Curve): train_sizes, train_scores, val_scores learning_curve( model, X, y, cvcv, train_sizesnp.linspace(0.05, 1.0, 10), scoringneg_mean_squared_error ) train_rmse np.sqrt(-train_scores.mean(axis1)) val_rmse np.sqrt(-val_scores.mean(axis1)) plt.figure(figsize(8, 5)) plt.plot(train_sizes, train_rmse, o-, labelTrain RMSE) plt.plot(train_sizes, val_rmse, o-, labelValidation RMSE) plt.xlabel(Training Set Size Ratio) plt.ylabel(RMSE) plt.title(title) plt.legend() plt.grid(True, alpha0.3) plt.show() plot_learning_curve(rf, X_train_scaled, y_train)这个封装函数可以复用到任何模型上。train_sizes从 5% 到 100% 取 10 个点比项目里原来 5 个点更能看出曲线走势的细节。跑完看两个结论如果两条曲线在尾部仍然明显分离且验证集误差还在下降说明加数据有效当前数据量是瓶颈如果两条曲线已经贴在一起但误差水平高说明模型复杂度不够换更强的模型比加数据更有效。我拆这份项目时自己跑过一次随机森林的学习曲线发现训练集 RMSE 始终低于验证集 1.5 个千美元左右且验证集曲线在数据量到 70% 后基本平了——这说明 506 条样本对随机森林来说已经足够瓶颈在模型结构而不是数据量。后来我加了特征交互项尝试把验证集 RMSE 往下压效果有限最终结论是这份数据本身的信息量就到这里不是模型不行。从那以后我每次拿到新数据都强制先走一遍学习曲线再做任何调参省掉了无数次在超参数里瞎转的时间。学习曲线先看清结构瓶颈再动手优化顺序反了就是在黑匣子里猜。希望帮到你。本文还有配套的精品资源点击获取