☰
房价预测实战:从数据采集到机器学习的完整数据科学流程
2026/10/5 9:49:17 网站建设 项目流程

1. 为什么要拿房价预测当数据科学练手项目

很多人学数据科学,最大的困惑不是语法,而是“不知道一个完整项目到底长什么样”。今天就用一个非常经典的场景——房价预测——把整个流程从头到尾串一遍。这个项目的本质不是搞出一套能赚钱的房价评估系统,而是让你真正走一遍“数据采集 → 数据清洗 → 特征工程 → 建模 → 评估优化”的完整闭环,每一个环节都会踩坑,每一个坑都值得记下来。

选择房价作为载体有几个非常实际的原因:第一,房价数据在网上随处可得,无论是房产平台还是公开数据集,都能找到足够量的样本,不需要求着别人给你数据,自己就能动手解决数据来源问题;第二,房价的影响因素足够多,面积、房龄、地段、楼层、周边配套全都对价格有作用,这给了特征工程很大的发挥空间;第三,房价是一个连续数值,天然适合做回归问题,评价标准非常清晰,不像分类问题还要纠结正负样本怎么切。

从技术栈角度,整个项目我用的是 Python 生态:爬虫阶段用 requests 和 BeautifulSoup 抓取页面,解析 HTML;数据清洗用 pandas 做缺失值处理和格式统一;特征工程继续用 pandas 加 numpy 做衍生计算;建模阶段对比了线性回归、Ridge、Lasso 和随机森林、XGBoost;评估用 sklearn 自带指标,核心看 RMSE 和 R²,交叉验证保证结果稳定。整个流程下来,你既能看到一个数据科学项目里“脏活累活”的真实比例,也能体会到一个项目从零到能跑通需要跨过哪些坎。

一句话总结这个项目能给读者带来的价值:它不是一个“demo 式”的教程,而是一个可以照着完整复现、能真正理解每个步骤背后逻辑的实战流程。

2. 数据从哪来:爬虫前置准备与采集策略

2.1 目标网站分析和合规边界

我选的是一个公开的二手房交易信息网站,这类网站的房源详情页结构相对稳定,有明确的标题、户型、面积、朝向、楼层、总价、单价等字段。当然,在动手写代码之前必须先做两件事:第一,查看网站的 robots 协议,明确哪些路径允许爬取;第二,评估目标站点的反爬强度。不夸张地说,很多人爬虫写好了却一篇数据都拿不到,不是代码问题,是连基础的请求头都没设置对。

这里多啰嗦一句合规问题。爬取公开数据做学习和研究是常见做法,但需要注意几个底线:不要对目标网站造成访问压力,控制请求频率;不要爬取需要登录才能访问的非公开数据;不要将采集到的数据用于商业用途。这个项目的数据量控制在几千条规模,请求间隔设置在 3-5 秒,完全在一个合理范围内,不会给目标站点带来任何影响。

每个房源详情页的结构大致是这样的:一个包含小区名和楼栋信息的标题区、一个展示基础属性的表格区、一个价格展示区,以及一段文字介绍。了解页面结构是最重要的一步,因为后面写选择器时,90% 的调试精力都花在“怎么精准定位这些字段”上。

2.2 爬虫技术选型:requests + BeautifulSoup vs Scrapy

很多初学者一上来就纠结要不要上 Scrapy,我的建议是:这个规模的项目,没必要。Scrapy 的优势在于大规模并发采集、自带去重和调度机制,但劣势是学习曲线陡峭,而且一旦出现解析错误,调试起来比原生库麻烦得多。用 requests 加 BeautifulSoup 的组合,代码逻辑完全透明,出了问题能很快定位到是哪一行。

实际采集流程分三步:

  1. 针对列表页构造请求,解析出每套房源的详情页链接;
  2. 逐条请求详情页,提取目标字段;
  3. 把数据存入 CSV 文件,每抓完 50 条打印一次进度。

我用的请求头长这样:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", "Connection": "keep-alive", }

这里有一个非常关键但也非常容易被忽略的细节:一定要设置超时时间,并且要捕获超时异常。网络请求永远不可靠,某个节点可能突然变慢,或者服务器主动断开连接。如果不处理异常,整个爬虫会在第 137 条数据的时候直接崩溃,前面抓的全白费。我当时在处理异常的基础上还加了失败重试机制,重试三次仍失败就记录到日志文件里,不阻塞整体进程。

2.3 解析逻辑与字段结构设计

BeautifulSoup 的解析逻辑核心就一句话:先根据标签结构定位到父节点,再从父节点范围内提取子字段,尽量避免从全局去找每个字段,效率低且容易误匹配。

拿“基础属性”这个区块举例,页面上是一个 dl 标签,里面每个 dt 是属性名,dd 是属性值。我的解析逻辑是:

for dt in soup.select("div.base dl dt"): field_name = dt.get_text(strip=True) field_value = dt.find_next_sibling("dd").get_text(strip=True) data[field_name] = field_value

这样写的好处是,无论字段顺序怎么变,只要结构是“dt 后跟 dd”,都能正确对应上。而且我特意给标题、价格、面积这几个核心字段加了 strip 清洗,把首尾空格和换行符去掉,避免后面做数据处理时还要二次清理。

字段设计方面,我定义了这样的采集结构:

字段名含义示例
title房源标题阳光花园 3室2厅 南北通透
total_price总价(万元)580
unit_price单价(元/平米)45200
area建筑面积(平米)128.5
rooms户型3室2厅
floor楼层信息中楼层/共18层
toward朝向南北
decoration装修情况精装
community小区名称阳光花园
district所在区域朝阳
year建筑年代2015年

3. 数据质量是模型的地基:清洗与预处理实战

3.1 缺失值处理:先统计再决定策略,别上来就填充

爬回来的数据基本不会完美,缺失值、异常值、格式不统一是家常便饭。很多人拿到数据第一反应就是直接 dropna 或者 fillna(0),这是最大的误区。缺失值处理的正确姿势是先搞清楚“为什么缺失”以及“缺失比例多少”。

我用 pandas 做了一轮缺失值统计,说实话结果有点出乎意料。户型字段几乎没缺失,但朝向字段大概缺了 8%,装修情况缺了 4%,楼层信息字段缺了 12%。根据缺失比例和字段重要性,我做了三种差异化处理:

  • 对于朝向、装修这类缺失比例较低的字段,用众数填充;
  • 对于楼层信息这种缺失比例偏高、且和采光、噪音等场景强相关的字段,单独生成一个“楼层信息缺失”的布尔特征,让模型自己学习“这个数据缺失”是不是一个信号;
  • 对于单价这种核心字段,如果缺失就直接删除样本。

具体代码逻辑是这样的:

fill_rules = { "toward": data["toward"].mode()[0], "decoration": data["decoration"].mode()[0], } data = data.fillna(value=fill_rules) data["floor_missing"] = data["floor"].isna().astype(int) data = data.dropna(subset=["unit_price", "area"])

有个反直觉的发现值得分享:填充缺失值这件事,真正重要的不是选择“用什么值去填”,而是“是否把缺失这个事实本身暴露给模型”。很多情况下“该字段有这个特征缺失了”本身就是有信息量的,这也是为什么我倾向于为缺失较多的字段额外建一个标记列。

3.2 异常值与重复数据的识别

异常值处理是这个项目里最“致郁”的环节,因为数据真的会给你惊喜——比如某个房源面积填了 6 平米,单价却显示 8 万每平,明显是把“价格/面积”算错了。我用两种方法识别异常值:

一种是基于业务常识的硬性过滤。比如面积小于 10 平米且价格高于 5 万的,大概率是车位或者录入错误,直接删除;单价低于 5000 或高于 20 万每平的,在普通住宅场景下几乎可以肯定是数据异常,也要处理掉。

另一种是基于统计分布的分位数截断。把面积和单价分别按 99% 分位数截断,超出范围的样本单独拉出来看,而不是一刀切删除——因为有些高价豪宅小区、大平层产品虽然数量少,但是合法数据,直接砍掉反而损失了长尾信息。换句话说,异常的判定必须结合业务场景,纯粹靠箱线图或 3σ 法则很容易把真实数据误杀。

重复数据这块,我做了两层去重:第一层是全字段完全一致的样本直接去重;第二层是“标题+小区名+面积”这三个字段相同的样本,视为同一房源重复发布,保留价格较低的一条。这第二层逻辑在实际房地产平台上非常常见,同一套房源被不同中介重复挂出,价格往往会略有差异。

3.3 特征标准化与编码方案的选择

数据清洗完,还有一个非常容易犯的错:把类别特征一股脑塞进模型。机器学习模型不认识“南北通透”“精装”这种字符串,只有两类办法——标签编码或者独热编码。

对于朝向、装修、所在区域这类无序类别特征,我用的是独热编码,因为它们的取值没有天然的先后顺序;对于楼层、户型这类有内在逻辑的特征,我不直接编码,而是先从原始字符串里手工提取数值信息。比如“中楼层/共18层”这个字符串,我分别提取出“楼层位置”和“总层数”两个数值特征。这一步看似不起眼,实际上是整个项目里信息增益最大的操作之一。

另外,数值特征的标准缩放也非常关键。我用的是 StandardScaler 对面积、单价这些数值特征做归一化,让它们落在同一个量级。对于线性模型,这个操作直接影响特征权重的可解释性;对于树模型,虽然不影响预测能力,但会让训练更快、梯度更稳定。我在这里吃了点亏——一开始没做标准化直接跑 Ridge,结果 R² 只有 0.4 出头,标准化之后提升到 0.7 以上,差距就是这么明显。

4. 特征工程的加减法:让模型“看得懂”数据

4.1 从原始字段里挖新特征

特征工程是整个项目里投入产出比最高的环节,但它也最考验“业务理解力”。我做了这么几个新特征:

第一个是房龄,从“建筑年代”计算得到,即当前年份减去建成年份。这个特征的重要性在于,它比“建筑年代”更直观地刻画了房屋的折旧程度,很多模型的权重分析都显示房龄对房价有显著负向影响。我当时计算完分布后发现,这个城市的房子集中建于 2005-2018 年之间,房龄 5-10 年的二手房源占了大头,这类房子流动性最好,价格也相对坚挺。

第二个是“是否带电梯”。虽然原始数据里没有这个字段,但楼层信息里的“总层数”可以间接推断——如果总层数大于等于 7 层,就认为大概率有电梯。这个特征在后续建模中表现得非常显著,因为对于无电梯的老小区,顶层房源价格会明显偏低,而这一信息在“楼层”原始字符串里并未直接体现。

第三个是“面积单价交互特征”,严格说不是新信息,而是把“房型结构”做了量化。比如把户型拆出“室数”和“厅数”,计算平均每室面积,用来区分那些面积大但房间少的户型(这类房子往往客厅面积被压缩,和面积小但房间多的户型形成了鲜明对比,在价格上也会体现出来)。

4.2 相关性分析:哪些特征在混淆模型

构造完特征之后,我画了一张特征相关性热力图,这一步帮我发现了两个问题。

一个问题是总价和单价的相关性高达 0.93,这是预料之内的,因为总价=单价×面积,两者本来就有天然关系。但在建模时必须想清楚:预测目标是总价还是单价?如果预测总价,那么“面积”就成了最具预测力的特征,模型会把所有精力放在面积上,其他特征的影响就被稀释了;如果预测单价,面积的作用依然存在但不再是决定性因素,其他特征的话语权更大。

我在两个方案之间权衡之后选择了预测总价,因为实际业务场景中,买家或中介在讨论时通常先关心“这套房子多少钱”,而不是“每平米多少钱”。同时我在特征列表里直接排除了“单价”字段,避免它作为输入的泄漏变量,这属于数据泄漏问题,如果在特征里加入单价,模型几乎等于作弊,测试集上的表现会被严重高估。

另一个发现是“区域”类别在独热编码后出现了大量接近零且强相关的列,这在高维稀疏场景下会对线性模型的稳定性造成伤害,但在树模型中影响不大。针对线性模型,我后面用了 L1 正则化来自动做特征选择,实际跑下来确实砍掉了不少不重要的区域特征。

4.3 避免数据泄漏:一份容易忽略但致命的红线

数据泄漏是这个项目里我特别想强调的一个知识点。泄漏的本质是“在训练阶段使用了测试阶段无法获得的信息”。

最典型的一个例子就是:我先用全量数据做了缺失值填充和标准化,然后再划分训练集和测试集——这其实已经构成了数据泄漏。为什么呢?因为用全量数据的均值或众数去填充测试集的缺失值,等于在训练模型时“偷看”了测试集的分布信息。正确的做法是:先把数据分割成训练集和测试集,然后只在训练集上做 fillna、StandardScaler 的 fit,再把这个已经 fit 好的 Transformer 直接 transform 测试集,测试集的数据完全不能参与任何统计量的计算。

另一个容易忽略的泄漏点是刚才提到的“单价”字段,如果把它作为特征输入,模型的 RMSE 会低到不真实的程度,但它没有实际意义,因为你要预测的就是这个值本身。

5. 模型选型与调参:从线性基准到树模型的进阶

5.1 先跑一个简单的线性回归当基准

建模的第一步永远不是直接上 XGBoost,而是先跑一个简单的线性回归当基准。道理很简单:如果复杂模型连线性回归都打不过,说明要么特征工程出了问题,要么数据本身就没有那么强的非线性关系。基准模型的意义不是用来做最后的预测,而是给后续所有模型提供一个“及格线”。

我划分了 80% 训练集、20% 测试集,固定随机种子,之后跑了一个默认参数的 LinearRegression,结果 RMSE 大概是 62 万,R² 在 0.61 左右。作为第一版,这个结果中规中矩,说明线性关系已经能解释大部分价格变动,但还有不少未捕捉的模式,这正是后面用正则化和树模型的动力所在。

5.2 岭回归与 Lasso:线性模型的“进化版”

线性回归的前提是特征之间不存在多重共线性,但房价数据里面积、总层数、卧室数、房龄之间本来就高度关联,普通最小二乘法在这种场景下方差会变大,导致模型在训练集上表现不错、一到测试集就崩盘。解决办法就是加正则项。

Ridge 加了 L2 正则,把特征权重往零的方向压缩但不等于零,适合那些“特征之间相关性比较强”的场景;Lasso 加了 L1 正则,可以让部分特征的权重直接变成 0,等价于自动做特征选择。在两个正则化模型上我都用了 GridSearchCV 来调 alpha 系数,网格范围从 0.01 到 100 之间取了几十个候选值,最后 Ridge 的 RMSE 降到了约 53 万,Lasso 稍差一些——因为它的特征选择能力在这个数据集上有些激进,把一些边际有效的特征直接砍掉了。

我实际预测了几套房源,正则化模型的价格误差在 10%-20% 之间,对于二手房这种定价本身就有很强主观性的场景,这个精度已经可以接受。

5.3 随机森林与 XGBoost:非线性关系的突破

线性模型的瓶颈在于必须预先假定特征和目标之间的关系是线性的,但房价数据里满是非线性关系。比如“房龄”对房价的影响不是单调下降的,而是“次新房溢价、老破小折价、极度老房又有拆迁预期”的复杂形态,这种关系需要树模型来捕捉。

随机森林本质上是很多棵决策树的投票集合,每棵树在训练时都会随机使用一部分特征和一部分样本,这样每棵树学到的都是数据的不同侧面,合在一起能够显著降低方差。我在随机森林上调了两个关键参数:n_estimators 设为 300,max_depth 限制在 15 左右防止过拟合。在这个配置下,R² 升到了 0.77。

XGBoost 则更进一步,它用梯度提升的思路,每一棵新树都学习前面所有树预测结果的残差,不断修正错误。XGBoost 在房价这类中等规模数据上几乎是最稳定的选择——既能处理非线性,又内置了正则化机制,对缺失值也有自动处理策略。我用五折交叉验证对学习率、最大深度、子采样比例做了贝叶斯优化调参,最终 RMSE 降到了 41 万左右,R² 稳定在 0.83,这是整个项目拿到的最好结果。

注意一个细节,树模型不需要对特征做 StandardScaler,因为决策树是基于特征值的大小做切分的,不做标准化也不会影响预测结果。但前面做标准化的步骤不能省,因为当尝试用线性模型或神经网络做对比时,特征量纲的问题就会立刻暴露出来。

5.4 交叉验证的使用逻辑

单独评估一次测试集的结果非常不稳,因为一次切分可能正好切到一批好预测的样本,也可能正好切到一批难预测的样本。我在这套流程里用的方案是:先用五折交叉验证选出最优模型,然后再在固定的测试集上做最终评估。

每个模型我都记录了五折交叉验证的均值和标准差。随机森林的均值不如 XGBoost 高,但胜在标准差小,说明它更稳定;XGBoost 均值高,但不同折之间的方差也略大。最后综合均值、方差、业务解释性,我选择把随机森林作为实际使用的模型——因为作为个人项目,稳定性比那 0.02 的 R² 更值钱。

6. 误差分析与模型评估:预测结果的可信度判断

6.1 不要只盯 RMSE:真实业务看什么

RMSE 是回归问题最常用的指标,但它也有明显的局限——它对大误差样本极端敏感,一个预测偏差 200 万的样本,能直接把整个 RMSE 拉高一大截。在实际看结果时,我会同时关注三个层面的指标。

RMSE 衡量的是绝对误差的平均水平,单位是万元,41 万 RMSE 意味着平均误差在 41 万左右,说实话对于平均总价 480 万的数据集来说,这个误差不算小。MAE 平均绝对误差则更接近业务直觉,它告诉你看走一套房平均会差多少钱。还有一个是 R²,代表模型解释了目标变量多少比例的方差,0.83 的水平在房价场景里算是比较好的。但更直观的做法是直接看图——把真实房价和预测房价画成散点图,如果所有点都贴着 y=x 的对角线,说明预测准;如果出现了系统性偏移,比如高价格段预测全部偏低,说明模型存在结构性的问题。

6.2 残差分析:找到模型的系统性盲区

画完预测值和真实值的散点图之后,我又画了一张残差图。残差就是真实值减去预测值,它有两个作用:一是看是否存在系统性偏差,二是看模型在哪个价格区间表现最差。

残差图显示,总价低于 250 万和高于 800 万的房产预测误差明显更大。低总价的房子很多是保障房、安置房,它们的定价逻辑和普通商品房完全不同;而高总价区房子多是顶豪、别墅,样本量本身就少,模型很难从有限数据中学到规律。这两个区间是长尾分布,任何模型都容易在这里失效。

这带给我的启发是:单一的全局模型很难应付所有价格段。实际操作中可以考虑做分层建模——比如先用一个分类模型判断房屋属于刚需、改善还是豪宅,再用不同的回归模型分别预测价格。这个思路我当时没有时间实现,但如果你是想把这个项目继续扩展成一个实战作品,这块是一个很好的切入点。

6.3 特征重要性:用模型输出反哺业务理解

XGBoost 和随机森林都可以直接输出特征重要性,它是树模型最宝贵的副产品之一。我排在前五的特征是:面积、房龄、所处区域、总层数、卧室数量。这个排名和房地产市场的基本常识出奇一致。

“面积”排在第一位是意料之中的,毕竟面积决定了房屋的基本价值。“房龄”排进前三也在预期内,但重要性和“区域”相近这一点,说明在这个城市里,地段和房子新旧对房价同样重要。唯一一个让我有点意外的特征排到了后段——朝向,之前我一直以为“南北通透”会是一个很有区分度的因素,但模型给出的重要性并不高。后来细想也合理:在现实市场中,楼盘位置和面积已经决定了一个房子的价格锚点,朝向更多的是溢价和议价空间,而不是决定性的价格因素。

7. 项目收尾:把这个流程复用到你自己的数据上

写到这里,整个“从爬取到预测”的流程已经完整走了一遍。最后分享几个我反复强调的经验,算是给想动手自己做一遍的读者一点底。

第一,流程比模型重要。模型选型、调参只占这个项目最后 20% 的时间,前面 80% 的时间全耗在数据采集和数据清洗上。数据质量不过关,再牛的模型也白搭;数据质量过硬,线性回归也能打出不错的成绩。

第二,版本控制一定要尽早建立。我第一次跑清洗脚本时,把原始数据直接覆盖了,后来想回看“哪些样本被删掉了”才发现已经无法溯源。建议所有清洗步骤都写成独立脚本,原始数据那一份永远不动。pandas 的链式操作虽然方便,但每一步最好单独保存,一旦逻辑改动导致结果变化,能快速对比出差异。

第三,单一模型和集成方案并不是对立的,它们各有用武之地。如果你要在面试或项目报告中展示这个案例,建议至少同时展示一个线性模型和一个树模型,并且能解释清楚为什么树模型在这个场景下更优——面试官在看候选人的时候,最看重的往往不是最终分数,而是你怎么解释那个分数背后的原因。

第四,别忽略数据故事。预测结果出来以后,我顺手把小区名、区域、价格取出来做了个简单分析,发现自己对某些区域的判断和模型给出的权重不太一样,这也是一个很好的数据洞察练习。数据科学项目最迷人的地方不在于“模型调到 0.9”,而在于你通过数据和模型,对自己生活的城市有了更清晰、更有据可依的理解。

最后说一句现实的话:如果你真的打算用这个项目作为求职作品,建议把数据爬取部分好好讲讲——怎么设计字段、怎么处理反爬、怎么控制请求频率、怎么设计异常处理——这些才是面试官会重点追问的点。因为模型是通用的,而数据处理过程中的思考和取舍,才是这个东西真正属于你自己的部分。

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

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

立即咨询