☰
机器学习房价预测:回归模型与特征工程全指南
2026/10/9 12:09:12 网站建设 项目流程

简介:面向人工智能、深度学习及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", encoding="utf-8-sig") print(df.shape) print(df.info()) print(df.isnull().sum().sort_values(ascending=False))

这段代码的作用有三个:看数据量够不够、看每列缺失数量、看字段类型有没有被误读。如果某列缺失率超过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], errors="coerce")

errors="coerce" 的作用是把无法解析的脏值统一变成 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, columns=categorical_cols, drop_first=True) 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_size=0.2, random_state=42 )

这里有两个容易踩的点。第一,unit_price 必须从特征里删掉,因为它等于 total_price 除以 area,等于把目标答案泄露了一半。第二,drop_first=True 会去掉每个类别编码后的第一列,避免线性模型里出现完全多重共线性;树模型对这个不敏感,但统一处理不亏。

切分参数上,test_size=0.2 是常规比例,数据量小可以放宽到0.15;random_state 固定是为了复现,交作业时保证别人跑同一份代码能得到一致结果;shuffle 默认开启,如果数据是按时间排序的,这一点尤其重要,否则同一时期的房子可能全被分进训练集或测试集。

3. 模型训练与评估:从线性回归到集成模型

数据准备好以后,训练的流程极其固定:定义特征矩阵 X 和目标向量 y,划分训练测试集,实例化模型,fit,predict,最后在测试集上算误差。难的不是代码,而是搞清楚每一步在做什么、每个模型适合什么数据。

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(f"LinearRegression 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(alpha=1.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(f"Ridge MAE={mae_ridge:.1f}")

alpha 控制惩罚强度,常用尝试顺序是0.1、1.0、10。alpha 越大,系数被压得越向0靠近;找到合适值后,岭回归在线性相关严重的房价特征上通常比普通线性回归更稳。

3.2 改用树模型:随机森林与梯度提升的参数怎么设

线性回归不能表达「老小区面积大但单价反而低」这类非线性关系,所以第二步我会端出树模型。随机森林适合作为第一选择,对异常值和缺失值有天然容忍度,解释性也好;梯度提升树在数据量足够时精度通常更高,但参数更多、更容易过拟合。

from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=200, max_depth=12, min_samples_leaf=4, random_state=42, 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(f"RandomForest MAE={mae_rf:.1f} RMSE={rmse_rf:.1f} R2={r2_rf:.4f}")

这组参数里最值得动的是三个。n_estimators 控制树的数量,200 在多数房价数据集上够用,太小欠拟合、太大训练变慢且收益递减;max_depth=12 限制单棵树的深度,防止树把训练集细节背下来;min_samples_leaf=4 要求叶子节点至少4个样本,能显著压低预测噪声。n_jobs=-1 把多核用满,避免训练阶段空转。

换成梯度提升树时,需要把注意力从「树的数量」挪到「学习率」上:

from sklearn.ensemble import GradientBoostingRegressor gbr = GradientBoostingRegressor( n_estimators=300, learning_rate=0.05, max_depth=4, random_state=42, ) gbr.fit(X_train, y_train) y_pred_gbr = gbr.predict(X_test)

学习率越小,每棵树贡献的修正量越保守,需要的树就越多;常见稳定组合是 n_estimators=300 配合 learning_rate=0.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(f"RandomForest 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.85,MAPE 却超过 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_estimators=300, max_depth=12, min_samples_leaf=4, random_state=42, 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(f"LogTarget MAE={mae_log:.1f} MAPE={mape_log:.2f}%")

log1p 和 expm1 是天然配套的,1p 的存在是为了防止价格等于0时对数取不到值。对比第3章的基准结果,如果 MAPE 下降幅度超过1个百分点,说明长尾偏分布确实在拖后腿;如果几乎没有变化,说明主要误差来自特征而非目标分布,应该把精力放回特征工程。

5.2 用特征重要度砍特征,再做交叉验证

随机森林训练完成后可以直接输出特征重要度,这是它比线性模型更好用的地方之一。

importance = pd.Series(rf.feature_importances_, index=X_train.columns) top_features = importance.sort_values(ascending=False).head(15) print(top_features)

把重要度排名前15的特征保留、其余删掉,重新训练一次,如果误差没有明显恶化,说明被删掉的特征本来就是噪声。这一步对使用文档的价值很大,你可以直接回答「哪些因素对房价影响最大」这类问题。

最后建议用交叉验证替代单次切分。单次切分的结果太依赖随机种子,换一个 random_state 可能就差0.3个百分点;5折交叉验证能给出一个稳定得多、也更可信的误差估计。最佳实践是只对最终选定的模型做交叉验证,不要在中途每个候选模型上都跑,否则训练时间成倍上涨。我自己的收尾习惯是:先跑随机森林基线,再做对数变换,对比两组 MAE 和 MAPE,最后用交叉验证确认结论稳定,三关都过了才敢把结果写进使用文档。这套流程虽然朴素,但能挡住绝大多数「换一个数据切分就翻车」的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询