ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

房价预测实战:从数据采集到机器学习的完整数据科学流程

房价预测实战:从数据采集到机器学习的完整数据科学流程 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 的组合代码逻辑完全透明出了问题能很快定位到是哪一行。实际采集流程分三步针对列表页构造请求解析出每套房源的详情页链接逐条请求详情页提取目标字段把数据存入 CSV 文件每抓完 50 条打印一次进度。我用的请求头长这样headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.5,en;q0.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(stripTrue) field_value dt.find_next_sibling(dd).get_text(stripTrue) data[field_name] field_value这样写的好处是无论字段顺序怎么变只要结构是“dt 后跟 dd”都能正确对应上。而且我特意给标题、价格、面积这几个核心字段加了 strip 清洗把首尾空格和换行符去掉避免后面做数据处理时还要二次清理。字段设计方面我定义了这样的采集结构字段名含义示例title房源标题阳光花园 3室2厅 南北通透total_price总价万元580unit_price单价元/平米45200area建筑面积平米128.5rooms户型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(valuefill_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 设为 300max_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 的水平在房价场景里算是比较好的。但更直观的做法是直接看图——把真实房价和预测房价画成散点图如果所有点都贴着 yx 的对角线说明预测准如果出现了系统性偏移比如高价格段预测全部偏低说明模型存在结构性的问题。6.2 残差分析找到模型的系统性盲区画完预测值和真实值的散点图之后我又画了一张残差图。残差就是真实值减去预测值它有两个作用一是看是否存在系统性偏差二是看模型在哪个价格区间表现最差。残差图显示总价低于 250 万和高于 800 万的房产预测误差明显更大。低总价的房子很多是保障房、安置房它们的定价逻辑和普通商品房完全不同而高总价区房子多是顶豪、别墅样本量本身就少模型很难从有限数据中学到规律。这两个区间是长尾分布任何模型都容易在这里失效。这带给我的启发是单一的全局模型很难应付所有价格段。实际操作中可以考虑做分层建模——比如先用一个分类模型判断房屋属于刚需、改善还是豪宅再用不同的回归模型分别预测价格。这个思路我当时没有时间实现但如果你是想把这个项目继续扩展成一个实战作品这块是一个很好的切入点。6.3 特征重要性用模型输出反哺业务理解XGBoost 和随机森林都可以直接输出特征重要性它是树模型最宝贵的副产品之一。我排在前五的特征是面积、房龄、所处区域、总层数、卧室数量。这个排名和房地产市场的基本常识出奇一致。“面积”排在第一位是意料之中的毕竟面积决定了房屋的基本价值。“房龄”排进前三也在预期内但重要性和“区域”相近这一点说明在这个城市里地段和房子新旧对房价同样重要。唯一一个让我有点意外的特征排到了后段——朝向之前我一直以为“南北通透”会是一个很有区分度的因素但模型给出的重要性并不高。后来细想也合理在现实市场中楼盘位置和面积已经决定了一个房子的价格锚点朝向更多的是溢价和议价空间而不是决定性的价格因素。7. 项目收尾把这个流程复用到你自己的数据上写到这里整个“从爬取到预测”的流程已经完整走了一遍。最后分享几个我反复强调的经验算是给想动手自己做一遍的读者一点底。第一流程比模型重要。模型选型、调参只占这个项目最后 20% 的时间前面 80% 的时间全耗在数据采集和数据清洗上。数据质量不过关再牛的模型也白搭数据质量过硬线性回归也能打出不错的成绩。第二版本控制一定要尽早建立。我第一次跑清洗脚本时把原始数据直接覆盖了后来想回看“哪些样本被删掉了”才发现已经无法溯源。建议所有清洗步骤都写成独立脚本原始数据那一份永远不动。pandas 的链式操作虽然方便但每一步最好单独保存一旦逻辑改动导致结果变化能快速对比出差异。第三单一模型和集成方案并不是对立的它们各有用武之地。如果你要在面试或项目报告中展示这个案例建议至少同时展示一个线性模型和一个树模型并且能解释清楚为什么树模型在这个场景下更优——面试官在看候选人的时候最看重的往往不是最终分数而是你怎么解释那个分数背后的原因。第四别忽略数据故事。预测结果出来以后我顺手把小区名、区域、价格取出来做了个简单分析发现自己对某些区域的判断和模型给出的权重不太一样这也是一个很好的数据洞察练习。数据科学项目最迷人的地方不在于“模型调到 0.9”而在于你通过数据和模型对自己生活的城市有了更清晰、更有据可依的理解。最后说一句现实的话如果你真的打算用这个项目作为求职作品建议把数据爬取部分好好讲讲——怎么设计字段、怎么处理反爬、怎么控制请求频率、怎么设计异常处理——这些才是面试官会重点追问的点。因为模型是通用的而数据处理过程中的思考和取舍才是这个东西真正属于你自己的部分。
返回列表