机器学习房价预测:回归模型与特征工程全指南

发布时间:2026/10/9 12:09:46
机器学习房价预测:回归模型与特征工程全指南
简介面向人工智能、深度学习及Python相关课程设计与毕业设计场景这份基于机器学习的房价与二手房房价预测项目源码涵盖数据采集、预处理、特征分析、模型训练与评估的完整流程适合即将完成期末大作业或希望进行项目实战的计算机专业学生。项目曾获导师指导并认可评审成绩98分源码均在本地编译调试通过可直接运行。压缩包共26个文件类型包含Python脚本建模与可视化、Jupyter Notebook分析与演示、CSV数据集链家/安居客房源数据、爬虫程序、图片及使用文档等合计1.29MB目录清晰便于按模块学习。目前已有144人学习下载。读者可获得完整可复现的房价预测方案、链家与安居客等平台的数据爬取与清洗思路、基于机器学习的回归预测核心代码以及配套使用说明能够帮助理解从原始房源信息到价格预测模型的落地过程。1. 机器学习房价预测为什么这类大作业选回归模型最稳「房价预测」是人工智能大作业里出现频率极高的选题但不少人拿到题目后的第一反应是把它当成分类问题判断一套房子值钱还是不值钱。实际打开数据集看两眼就明白成交总价是连续数值更适合用机器学习回归模型去拟合尤其是当数据里同时混着新房和二手房时面积、房龄、楼层、朝向、区域这些特征和价格之间根本不是一条简单直线传统公式或手工加权根本招架不住。回归模型能自动从数据里学出那套隐式定价逻辑不依赖人为假设还能对每套房输出具体的预测价格。这个方向适合想完整走一遍数据挖掘流程的人拿到 CSV 之后从数据清洗、特征构造、模型训练、误差评估到写配套使用文档链路完整技术点扎实工程量足够撑起一份大作业。2. 数据与特征工程决定房价预测上限的一步我自己的习惯是拿到题目后先不开模型而是把数据集每一列的语义过一遍重点判断它是「看房时能知道的信息」还是「成交后才知道的信息」。前者能做特征后者坚决不能用。这个判断做对了后面模型的分数才有意义判断错了后续所有花哨操作都是在垃圾上雕花。2.1 先弄清楚数据里有哪些特征新房和二手房的差异是什么房价数据集的常见形态有两种。第一种是纯新房数据特征相对规整面积、居室数、所在楼层、总楼层、装修标准、车位数、开发商报价。第二种是典型二手房数据特征里多出房龄、剩余产权年限、抵押状态、唯一住房标识、挂牌时间。还有一种很常见的情况是两类数据混在同一张表里靠「房屋类型」或「所在区域」区分标题里同时提到「房价和二手房房价预测」对应的通常就是这种混合表。混合表最常见的问题出在房龄列。新房数据里建成年份可能是空的或直接写0如果简单地把缺失值填0再喂给模型模型就会把「房龄0年」当成一个普通数值看不出这是特殊状态。我一般会构造一个独立的二值特征「是否有房龄记录」新房标记为1二手房标记为0同时保留建成年份本身的数值。这样模型在同一个特征空间里可以分别学习两类房屋的定价逻辑而不是被迫用一个连续特征去跨越两种完全不同的语义。在特征选型上我习惯按三层来划分必须保留面积、居室数、楼层/总楼层、建筑年代、装修程度、区域、房屋类型、朝向建议保留距地铁距离、物业费、容积率、有无电梯、是否临街、车位数量一般剔除唯一ID、描述文本、带看次数、挂牌天数这类不稳定或隐含未来信息的字段一句话原则人类实地看房时会问的问题都值得做成特征只有成交之后才能拿到答案的信息不要进特征。带看次数和议价空间这类字段虽然和成交价有相关性但测试集里根本拿不到同口径数据放进训练集只会让模型学一个靠不住的捷径。2.2 清洗、补缺失、删异常房价实测的三步处理动手清洗前先跑一段数据概览把数据底细摸清楚。import pandas as pd import numpy as np df pd.read_csv(house_data.csv, encodingutf-8-sig) print(df.shape) print(df.info()) print(df.isnull().sum().sort_values(ascendingFalse))这段代码的作用有三个看数据量够不够、看每列缺失数量、看字段类型有没有被误读。如果某列缺失率超过40%基本可以直接放弃硬补只会给模型塞进大量噪声缺失率在10%以内的列才值得精心处理。print(df.info()) 还能顺带发现「看着是数字其实是字符串」的列这类问题后面 fit 时会直接报错。补缺失和删异常我按三条规则来# 数值列用中位数填充抗极端值能力强于均值 df[area] df[area].fillna(df[area].median()) # 类别列用众数填充不会引入字典之外的新值 df[decoration] df[decoration].fillna(df[decoration].mode()[0]) # 重复行删除防止同一套房在训练集和测试集里出现 df df.drop_duplicates() # 面积异常值过滤5平米和1000平米都超出住宅常规语义 df df[(df[area] 5) (df[area] 1000)] # 单价小于0的行说明价格录入错误直接删除 df df[df[unit_price] 0]为什么面积取5到1000这个区间5平米以下一般是车位或储物间价格语义和住宅完全不同1000平米以上在普通住宅成交记录里属于极少数很可能来自别墅数据或录入错误。unit_price小于0则说明原始表的成交价或面积至少有一个是错的留着会把回归直线拉偏。补缺失时用median还是mean取决于列本身的分布数据接近正态分布用mean没问题分布存在明显右偏时median更稳decoration这类离散列用众数最省心。还有一个很多人忽略的动作低频类别合并。某个区域或小区在数据集里只出现两三条样本时独热编码后模型对它的学习几乎没有样本支撑反而多出一堆特征维度。我会先统计类别频次出现次数低于30的统一合并成「其他」数据总量只有两三百条时这个阈值可以放宽到10。读入时最好顺手把关键数值列做强转防止解析阶段埋雷num_cols [area, total_price, lon, lat] for col in num_cols: df[col] pd.to_numeric(df[col], errorscoerce)errorscoerce 的作用是把无法解析的脏值统一变成 NaN交给后面的缺失值处理流程接管。这是很省心的防御性写法能挡住一大类「表面是数字、实际是字符串」的读入问题。2.3 类别特征编码与训练集切分模型输入端的两件小事字符串列必须转数值这是每个初学者都会亲手撞上一次的坎。区域、朝向、装修程度都是无序类别用独热编码楼层等级、新旧程度这类存在先后顺序的字段用标签编码更合适。拿不准时默认独热不会错只是特征维度会变高。一个关键顺序问题先切分再对训练集拟合编码器然后再变换测试集。很多人为了方便先对全量数据做 get_dummies 再切分这在某些场景下会引入测试集信息泄漏后面避坑章会专门展开。单看 get_dummies 本身是无参变换只是为了展示流程才写在切分前实际交付代码时我会把顺序反过来。from sklearn.model_selection import train_test_split categorical_cols [district, direction, decoration, house_type] df_encoded pd.get_dummies(df, columnscategorical_cols, drop_firstTrue) target_col total_price X df_encoded.drop(columns[target_col, unit_price]) y df_encoded[target_col] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )这里有两个容易踩的点。第一unit_price 必须从特征里删掉因为它等于 total_price 除以 area等于把目标答案泄露了一半。第二drop_firstTrue 会去掉每个类别编码后的第一列避免线性模型里出现完全多重共线性树模型对这个不敏感但统一处理不亏。切分参数上test_size0.2 是常规比例数据量小可以放宽到0.15random_state 固定是为了复现交作业时保证别人跑同一份代码能得到一致结果shuffle 默认开启如果数据是按时间排序的这一点尤其重要否则同一时期的房子可能全被分进训练集或测试集。3. 模型训练与评估从线性回归到集成模型数据准备好以后训练的流程极其固定定义特征矩阵 X 和目标向量 y划分训练测试集实例化模型fitpredict最后在测试集上算误差。难的不是代码而是搞清楚每一步在做什么、每个模型适合什么数据。3.1 基线模型用线性回归跑通最小可运行流程第一次跑通全流程我强烈建议先上线性回归。它简单、可解释、训练极快还能作为后续复杂模型的基准。如果线性回归在数据上 R² 只有0.2你就不该指望一个没调参的随机森林直接飙到0.9反过来如果线性回归已经到了0.7说明数据里线性关系占主导后续用非线性模型提升的空间有限应该把时间花在特征工程上而不是疯狂堆参数。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score lr LinearRegression() lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) mae_lr mean_absolute_error(y_test, y_pred_lr) rmse_lr np.sqrt(mean_squared_error(y_test, y_pred_lr)) r2_lr r2_score(y_test, y_pred_lr) print(fLinearRegression MAE{mae_lr:.1f} RMSE{rmse_lr:.1f} R2{r2_lr:.4f})fit 是训练、predict 是推理这是回归模型统一的接口风格后面换随机森林、梯度提升树都是这套写法。在房价预测里MAE 的直观含义是「平均每套房预测偏差多少万元」RMSE 会放大个别离谱样本的影响R² 表示模型解释了目标值多少比例的方差。如果预测结果里出现大量负价格或某个特征系数大到离谱先别怀疑代码去检查特征之间是否存在强相关比如面积和单位面积价格同时进了矩阵。此时换成带 L2 惩罚的岭回归往往能稳住结果from sklearn.linear_model import Ridge ridge Ridge(alpha1.0) ridge.fit(X_train, y_train) y_pred_ridge ridge.predict(X_test) mae_ridge mean_absolute_error(y_test, y_pred_ridge) print(fRidge MAE{mae_ridge:.1f})alpha 控制惩罚强度常用尝试顺序是0.1、1.0、10。alpha 越大系数被压得越向0靠近找到合适值后岭回归在线性相关严重的房价特征上通常比普通线性回归更稳。3.2 改用树模型随机森林与梯度提升的参数怎么设线性回归不能表达「老小区面积大但单价反而低」这类非线性关系所以第二步我会端出树模型。随机森林适合作为第一选择对异常值和缺失值有天然容忍度解释性也好梯度提升树在数据量足够时精度通常更高但参数更多、更容易过拟合。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators200, max_depth12, min_samples_leaf4, random_state42, n_jobs-1, ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) mae_rf mean_absolute_error(y_test, y_pred_rf) rmse_rf np.sqrt(mean_squared_error(y_test, y_pred_rf)) r2_rf r2_score(y_test, y_pred_rf) print(fRandomForest MAE{mae_rf:.1f} RMSE{rmse_rf:.1f} R2{r2_rf:.4f})这组参数里最值得动的是三个。n_estimators 控制树的数量200 在多数房价数据集上够用太小欠拟合、太大训练变慢且收益递减max_depth12 限制单棵树的深度防止树把训练集细节背下来min_samples_leaf4 要求叶子节点至少4个样本能显著压低预测噪声。n_jobs-1 把多核用满避免训练阶段空转。换成梯度提升树时需要把注意力从「树的数量」挪到「学习率」上from sklearn.ensemble import GradientBoostingRegressor gbr GradientBoostingRegressor( n_estimators300, learning_rate0.05, max_depth4, random_state42, ) gbr.fit(X_train, y_train) y_pred_gbr gbr.predict(X_test)学习率越小每棵树贡献的修正量越保守需要的树就越多常见稳定组合是 n_estimators300 配合 learning_rate0.05。如果追求更快的训练或更高的精度可以考虑用带早停的梯度提升实现但大作业场景下这组默认参数已经足够支撑一份完整报告。3.3 用 MAE、RMSE、MAPE 看模型到底准不准评估环节只看 R² 不够因为 R² 对数据均值敏感一组全局偏移也能得到不错的 R²但具体到每个价格段可能一塌糊涂。房价预测里我至少会同时看三个指标再加一个 MAPE。指标单位含义注意事项MAE万元平均绝对误差直观但高低价样本权重相同RMSE万元均方根误差放大极端误差适合观察坏样本R²无方差解释比例1.0 完美0 等于猜均值MAPE%平均相对误差能看出系统性低估/高估def mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 mape_rf mape(y_test, y_pred_rf) print(fRandomForest MAPE{mape_rf:.2f}%)MAPE 的直观含义是「平均每套房预测偏差百分之几」。常规数据集上 2% 到 5% 算优秀8% 以上说明模型对高价房或低价房存在系统性偏见。注意如果真实价格里有接近0的样本MAPE 会被单条记录直接拉爆这时应该改用加权 MAPE 或回到 MAE/RMSE 上来。除了指标计算交作业时还建议把预测结果和真实值按价格区间分桶打印出每个区间的平均误差。这一步既能验证「贵房子是不是总被低估」也是写使用文档时最有说服力的截图素材。预测结果保存成 CSV 的代码很简单但能让复盘和展示方便很多。4. 房价预测避坑指南五个高频翻车点这一章写成问答式的排查手册每一条都是常见项目里反复出现过的真实问题按「现象、原因、解决」三段来看。4.1 现象训练集 R² 高验证集 R² 却离谱模型上线就废某开发者在复现项目时把标准化写在了切分之前对全量数据先做了归一化再划分训练测试集结果训练集 R² 有0.92测试集直接掉到0.4。原因在于标准化这一步「偷看」了测试集的均值和方差。标准化用的统计量来自全量数据测试集的分布信息已经通过这个变换泄露给了训练过程模型看到的并不是纯净的训练分布。解决方法是把标准化、填充、编码这类数据依赖操作全部放进同一个流程先 train_test_split再在 X_train 上拟合 scaler然后用同一个 scaler 去 transform X_test。独热编码同理先 fit 训练集避免测试集里出现的类别被提前看到。顺序问题看起来只是代码位置的区别实际是数据泄漏最隐蔽的一种形态。4.2 现象二手房预测普遍偏高新房预测普遍偏低误差分成两团当表格同时包含新房和二手房时如果只把「房屋类型」当成普通类别特征丢进随机森林模型学到的是两类房屋的「平均效果」并不理解房龄导致的价格衰减机制。结果就是老房龄样本被拉向均值新房样本也被拉向均值误差在房价上呈现明显的两团分布。原因在于没有把房龄和房屋类型之间的交互显式处理。解决方式是构造「有效房龄」特征新房记为0并单独保留新房标志列二手房按当前年份减建筑年代计算真实房龄让两类样本共享面积、装修等其他特征。这样模型在同一个特征空间里自动学会两套定价逻辑而不是靠一个特征去硬分。4.3 现象便宜的房子预测得很准高档住宅总是被低估这是房价预测最典型的翻车点。原因是房价分布呈明显右偏长尾少数高价房占据了极大的数值范围回归模型在最小化平方误差时会天然牺牲少数高价样本优先压低多数中低价样本的误差。这不是模型不行是优化目标本身导致的偏差。通用做法是在训练前对目标变量取对数y_log np.log1p(y)训练和预测完成后用 np.expm1 还原成原始价格。对数变换能把长尾分布拉成近似正态显著改善高价房的低估问题。这个技巧属于性价比极高的后处理手段比盲目堆模型参数有效得多。4.4 现象对区域加了独热编码后预测指标反而恶化有些城市数据集把区域细分到街道甚至小区级别独热编码后特征维度暴涨某个街道只有三五条样本时模型对它输出的预测基本等于「记住均价」而不是「学习规律」。原因在于低频类别带来的维度扩张噪声贡献超过信息贡献。解决方法是先做类别频次统计把低于30条样本的低频区域合并成「其他」或者改用目标编码用该区域的历史均价替换独热向量。目标编码更省维度但必须配合交叉验证使用否则目标编码本身就会过拟合。4.5 现象R² 有 0.85MAPE 却超过 15%到底该信哪个R² 高说明整体方差解释率高但对高价房样本的绝对误差权重不足MAPE 是相对误差占比任何价格段的百分比偏差都算数两者天然关注不同的价格段。所以不能说哪个指标错了只能说单一指标欺骗性太强。解决方式是在评估表里多放几个指标而不是为了报告好看只挑一个。如果课程要求只写一个指标回归任务首选 RMSE并按价格区间分桶说明误差分布比单看 R² 更有说服力。5. 进阶把预测误差再压低一点的做法5.1 目标变量取对数后再训练把对数值作为目标重新训练一次是提高高价房预测精度最直接的操作。y_train_log np.log1p(y_train) y_test_log np.log1p(y_test) rf_log RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf4, random_state42, n_jobs-1, ) rf_log.fit(X_train, y_train_log) y_pred_log rf_log.predict(X_test) y_pred_restored np.expm1(y_pred_log) mae_log mean_absolute_error(y_test, y_pred_restored) mape_log mape(y_test, y_pred_restored) print(fLogTarget MAE{mae_log:.1f} MAPE{mape_log:.2f}%)log1p 和 expm1 是天然配套的1p 的存在是为了防止价格等于0时对数取不到值。对比第3章的基准结果如果 MAPE 下降幅度超过1个百分点说明长尾偏分布确实在拖后腿如果几乎没有变化说明主要误差来自特征而非目标分布应该把精力放回特征工程。5.2 用特征重要度砍特征再做交叉验证随机森林训练完成后可以直接输出特征重要度这是它比线性模型更好用的地方之一。importance pd.Series(rf.feature_importances_, indexX_train.columns) top_features importance.sort_values(ascendingFalse).head(15) print(top_features)把重要度排名前15的特征保留、其余删掉重新训练一次如果误差没有明显恶化说明被删掉的特征本来就是噪声。这一步对使用文档的价值很大你可以直接回答「哪些因素对房价影响最大」这类问题。最后建议用交叉验证替代单次切分。单次切分的结果太依赖随机种子换一个 random_state 可能就差0.3个百分点5折交叉验证能给出一个稳定得多、也更可信的误差估计。最佳实践是只对最终选定的模型做交叉验证不要在中途每个候选模型上都跑否则训练时间成倍上涨。我自己的收尾习惯是先跑随机森林基线再做对数变换对比两组 MAE 和 MAPE最后用交叉验证确认结论稳定三关都过了才敢把结果写进使用文档。这套流程虽然朴素但能挡住绝大多数「换一个数据切分就翻车」的尴尬。希望帮到你。本文还有配套的精品资源点击获取