ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从爬取到预测:Python房价预测全流程实战

从爬取到预测:Python房价预测全流程实战 做数据科学项目我见过太多人一上来就调模型参数折腾几天最后发现是数据本身有问题。也有另一类朋友卡在第一步就动不了——数据不知道去哪拿后面清洗、预测的事更是无从谈起。这篇文章我打算借一个特别大众的场景——房价预测把从数据爬取到模型预测的完整流程走一遍。为什么选房价因为它足够贴近生活字段你看得懂结果你有感知不会被业务概念劝退。更重要的是房价数据“量大、字段杂、脏数据多、有异常值”几乎涵盖了数据科学入门阶段你能遇到的所有经典问题。这篇文章适合正在自学数据科学、刚接触 Python、或者上过一些课但没独立跑通过完整项目的人可以把它当成一份实操路线图来用。1. 项目整体设计先想清楚再动手1.1 为什么用房价练手很多人学数据科学最大的问题不是不会调包而是不知道一个项目从零到一到底要经过哪些环节。房价预测这个选题好在三个方面。第一数据好理解。二手房挂牌价、建筑面积、户型、楼层、朝向、装修情况这些字段即使完全没有数据科学背景的人也能看懂。对比一些金融风控或用户画像项目光理解业务术语就要耗掉一半精力房价预测没有这个门槛。第二任务目标非常明确。我们要做的事情就是拟合“价格”和“房屋属性”之间的关系这是一个典型的监督学习回归问题。评价标准也很直观预测的价格和真实挂牌价差多少偏差越小越好。第三脏数据种类齐全。真实爬下来的房源数据里会有“面积”字段是空白的、总价和单价互相矛盾的、户型写成“4室2厅2卫1厨”这种混合文本的、楼层写着“高楼层共28层”的。这些花式脏数据恰好能逼着你去处理缺失值、异常值、文本抽取和类型转换。把这一套流程走完之后做其他结构化数据项目基本都能直接复用。1.2 技术选型Python生态就够了整个项目我用的是 Python 标准的数据科学全家桶requests和BeautifulSoup负责数据爬取pandas做清洗和转换matplotlib和seaborn做可视化scikit-learn完成建模评估。为什么选这套组合而不是更重的工具原因很简单项目范围不大没必要引入 Spark 或大数据组件但也不是一个纯教学玩具它需要真实应对网页解析、数据清洗、特征工程、模型对比这些完整环节。pandas做表格处理是行业标配scikit-learn里回归模型和预处理工具足够成熟这套组合既能跑通流程代码量也不会失控对新手来说每一行都能看懂。如果你还没装 Python 环境我建议直接装 Anaconda它自带 Jupyter Notebook 和大多数常用库省掉命令行逐个安装的麻烦编辑器用 VS Code 或者 PyCharm 都行。安装完成之后在终端跑一下python --version确认环境没问题再pip install requests beautifulsoup4 pandas matplotlib seaborn scikit-learn把依赖补齐。1.3 整个流程的五个阶段一个完整的数据科学项目我会习惯性地拆成五个阶段数据采集从目标网站爬取原始房源数据保存成结构化表格文件。数据清洗处理缺失值、重复数据、异常值统一字段格式。特征工程从原始字段中提取、组合出模型真正能用的特征。建模评估划分训练集和测试集训练回归模型用评价指标衡量效果。分析总结看特征重要性复盘误差来源决定下一步优化方向。很多人一上来就急着写爬虫或者直接model.fit()但我会建议你先在纸上把这五个阶段列出来给自己定一个清晰的验收标准。比如“爬下来多少条数据”“清洗后还剩多少条”“模型 R2 达到多少”。有了目标再去动手过程中才不会盲目。2. 数据爬取先把数据搞到手2.1 数据源选择与字段设计既然做房价预测第一步自然是选数据源。我用的是某房产平台的二手房频道这类平台的房源数据结构化程度高列表页上有总价、单价、面积、户型、楼层、朝向、装修、小区名称等信息非常适合做爬取练习。选择数据源有一个原则优先选列表页信息完整的网站尽量不要选那种必须点进详情页才能看到核心信息的站点因为详情页请求量大、解析复杂爬取难度会成倍增加。爬取之前先想清楚要存哪些字段。我的建议是宁可多存不要少存。因为后续做特征工程时你可能会从“小区名称”里提取区位从“楼层”里判断高低层从“朝向”里识别南北通透。前期多存几个字段后面就不用回头补爬。我最终保留了以下字段小区名称可能包含商圈信息比如“XX小区”或“XX路XX号”户型例如“3室2厅”用于提取房间数和厅数面积建筑面积单位平方米朝向例如“南”“南北”“东”装修例如“精装”“简装”“毛坯”楼层例如“低楼层共6层”包含所在层和总层数总价单位万元作为预测目标单价单位元/平方米可以用于校验数据一致性所在城区发布日期或者区域信息有时也能提供有价值的特征2.2 爬虫核心代码实现爬虫的思路不复杂请求列表页 HTML解析出房源卡片数据翻页重复操作最后把所有数据汇总到 DataFrame 里并保存为 CSV。核心代码长这样import requests from bs4 import BeautifulSoup import pandas as pd import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } def parse_list_page(html): soup BeautifulSoup(html, html.parser) items soup.select(.sellListContent li .info) data [] for item in items: title item.select_one(.title a) flood item.select_one(.flood) address item.select_one(.address .houseInfo) total_price item.select_one(.totalPrice span) unit_price item.select_one(.unitPrice span) if not title or not total_price: continue data.append({ 小区名称: title.get_text(stripTrue), 户型: flood.get_text(stripTrue).split( | )[0], 面积: flood.get_text(stripTrue).split( | )[1], 朝向: flood.get_text(stripTrue).split( | )[2], 装修: flood.get_text(stripTrue).split( | )[3], 楼层: address.get_text(stripTrue).split( | )[0], 总价: total_price.get_text(stripTrue), 单价: unit_price.get_text(stripTrue).replace(元/平, ) }) return data all_data [] for page in range(1, 6): # 先爬前5页验证流程 url fhttps://example.com/ershoufang/pg{page}/ resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: all_data.extend(parse_list_page(resp.text)) print(f第{page}页完成累计{len(all_data)}条) else: print(f第{page}页请求失败状态码{resp.status_code}) time.sleep(1.5) # 控制访问频率 df pd.DataFrame(all_data) df.to_csv(house_raw.csv, indexFalse, encodingutf-8-sig)这里有几个细节值得展开说。HEADERS里的User-Agent一定要设置。很多网站的反爬策略首先看请求头如果默认是python-requests/2.31.0这种很容易被识别为爬虫并直接拒绝。设置成常见浏览器的 User-Agent 是第一步也是最基本的一步。解析部分我用的是 CSS 选择器。.sellListContent li .info这个选择器把房源列表里每一条记录的外层容器选中然后逐个提取内部的标题、户型、价格等信息。写爬虫有个小技巧先在浏览器开发者工具里看 HTML 结构确认目标元素的选择器能唯一匹配再写代码能少踩很多坑。我这里的选择器是基于通用结构写的实际项目里你需要根据目标网站的具体结构调整。翻页逻辑比较简单直接在 URL 上改页码即可。range(1, 6)表示只爬前 5 页这是刻意控制的——做练习项目没必要把数据量搞到几十万条先拿几百条把流程跑通之后如果要扩大规模把循环范围改大就行。最后保存成 CSV 时我用了encodingutf-8-sig。这个细节非常关键。utf-8-sig会在文件开头写入 BOM 头用 Excel 打开 CSV 时中文才不会乱码。如果直接用utf-8Excel 打开时可能出现中文乱码虽然 pandas 读回来没问题但后续人工检查数据时体验会很差。2.3 爬虫合规与防封经验关于爬虫有些话我必须说在前面。爬取公开数据时要遵守目标网站的robots.txt规则控制请求频率不要对目标服务器造成压力爬下来的数据只能用于个人学习研究不能商用或批量传播。这是数据从业者的基本底线。我建议爬取间隔至少 1 到 2 秒不要开多线程并发爬不是为了别的单纯就是不要给对方服务器添麻烦也降低自己 IP 被封的风险。真正实操时你可能还会遇到几种典型情况。第一种是 IP 被封特征往往是请求返回 403 或者验证码页面。遇到这种第一反应应该是停下来而不是换 IP 继续硬刚。休息一段时间降低请求频率再试如果连续多次被限就换个数据源。第二种是页面结构和爬虫代码不一致比如网站上房源卡片结构改版了CSS 选择器匹配不到内容。解决方法是重新打开网页查看当前结构调整选择器。第三种是解析出来的字段有缺失或者错位比如某个房源没有装修信息flood字段拆分后不够 4 项。这种情况用try...except或显式判断跳过保证程序不中断。3. 数据清洗与特征工程模型能学多好一半看这里3.1 缺失值、重复值和异常值处理爬下来的原始数据 CSV直接用df.head()看前几行往往是灾难现场。字段类型是字符串、面积和房价混着“平米”“万元”这种单位、有些行只有一半字段有值。这个时候不能直接建模必须先做清洗。我的标准流程分四步。第一步是去重。房源数据经常有重复记录同一套房被重复抓取或者业主重复挂牌都很常见。判断是否重复时我用“小区名称 户型 面积 总价”四列作为唯一标识因为这四列完全相同的两行基本可以确定是同一套房源。df df.drop_duplicates(subset[小区名称, 户型, 面积, 总价])第二步是处理缺失值。先看一下每一列的缺失情况print(df.isnull().sum()) print(df.shape)如果某些字段缺失比例超过 30%我会直接删除该列如果只是个别行缺某个字段可以选择删除这些行也可以用众数或中位数填充。房价数据里缺失值处理要特别注意总价这一类目标变量如果缺失直接删行不要填充因为填充出来的价格会污染模型。第三步是剔除异常值。这个环节做得好不好直接决定模型的根基。对于房价数据我一般关注两类异常面积异常比如面积小于 10 平方米或者大于 500 平方米的房子很可能是录入错误或非标准住宅车库、厂房等直接删除。价格异常单价特别低或特别高。单价异常通常是房源性质不同造成的比如有些是“车位”或“商业性质”虽然显示在二手房列表里但和普通住宅价格体系完全不同。这种样本混在训练集里会让模型很困惑。处理可以用分位数法也可以设定业务合理区间。df df[(df[面积] 20) (df[面积] 300)] df df[(df[单价] 5000) (df[单价] 100000)]第四步是类型转换。原始数据里面积、总价、单价都是字符串比如“89.5平米”“450万”要先转成浮点数才能计算。df[面积] df[面积].str.replace(平米, ).astype(float) df[总价] df[总价].str.replace(万, ).astype(float) df[单价] df[单价].astype(float)转换之后我习惯做一次交叉校验用“总价 / 面积 * 10000”算出每平方米价格和“单价”字段比对如果差异超过 5%说明原始数据里有录入错误把这行删掉。这个校验逻辑很多人想不到但它能有效识别数据质量差的样本。3.2 从文本字段里抽取结构化特征原始数据里“户型”“楼层”“朝向”都是混合字符串比如“3室2厅” → 室数3厅数2“低楼层共6层” → 所在层低楼层总层数6“南北” → 朝向南北这些字段不能直接喂给模型需要拆解成结构化特征。拆解的逻辑可以用正则表达式也可以用字符串分割。我的实现大概是df[室数] df[户型].str.extract(r(\d)室).astype(float) df[厅数] df[户型].str.extract(r(\d)厅).astype(float) df[总楼层] df[楼层].str.extract(r共(\d)层).astype(float) df[所在层] df[楼层].str.extract(r(.?)).astype(str)所在层是类别变量取值一般是“低楼层”“中楼层”“高楼层”。实际建模时我发现“总楼层”和“所在层”组合成“所在层占比”所在层/总楼层更有意义。比如同样是 5 楼在 6 层楼房里属于高楼层在 30 层楼房里属于低楼层。这个比例的物理含义比单独的楼层数更清晰。3.3 类别特征编码与数值特征归一化特征工程最后一步是把类别文本变成模型能理解的数字。常用的方式有标签编码和独热编码。对于“朝向”“装修”“所在层”这类没有明显大小顺序的类别变量我一般用独热编码。pandas里一行就能实现df pd.get_dummies(df, columns[朝向, 装修, 所在层, 所在城区])对于有顺序的类别比如“朝向”如果人为规定“南北 南 东南 东 北”可以转成有序数字但我实测下来独热编码在这个项目里更稳因为朝向和房价之间的关系并不是简单的线性增减。数值型特征要不要归一化如果你用的是线性回归、岭回归这类对特征尺度敏感的模型就需要归一化如果用的是树模型随机森林、XGBoost 这类特征尺度不影响分裂结果可以不归一化。我的建议是先做归一化因为后续对比基线模型时线性回归和树模型用的是一套数据省得来回改。from sklearn.preprocessing import StandardScaler num_cols [面积, 室数, 厅数, 总楼层, 所在层占比] scaler StandardScaler() df[num_cols] scaler.fit_transform(df[num_cols])特征工程做完我又加了两个常用特征一是“房龄”如果原始数据里有竣工年限可以用当前年份减掉它二是“商圈均价”把同城区的房源价格取平均作为特征这个特征能有效编码区位效应。不过商圈均价这种特征要小心数据泄露——计算时必须只用训练集后面会详细讲。4. 房价预测模型从基线到调优4.1 数据划分与评估指标建模前先做两件事划分数据集、选定评估指标。数据集划分我用 scikit-learn 的train_test_split测试集占比 20%随机种子固定为 42。固定随机种子的意义在于实验可复现后面换模型、调参数时能保证对比是在同一份数据上进行的。from sklearn.model_selection import train_test_split X df.drop(columns[总价]) y df[总价] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )评估指标我用了两个R2决定系数和 MAE平均绝对误差。R2 回答的问题是“模型能解释目标变量多少比例的方差”MAE 回答的问题是“平均预测偏差多少钱”。R2 负责判断模型拟合质量MAE 负责把误差换算成实际业务含义。对于房价预测MAE 如果能在 30 万以内说明预测误差大约在 6% 到 10% 之间模型就算有实用价值了。4.2 线性回归做基线很多人喜欢一上来就上随机森林或者 XGBoost我的习惯是先跑一个线性回归作为基线。目的有三个验证数据管线通不通、得到一组基础指标、为后续对比确定基准线。from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_absolute_error model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(R2:, r2_score(y_test, y_pred)) print(MAE:, mean_absolute_error(y_test, y_pred))第一次跑通的时候R2 大概在 0.6 到 0.7 之间MAE 可能在 40 万左右。这个成绩说明特征和目标之间确实存在相关性数据管线没问题但模型还有优化空间。线性回归效果不够好是正常的因为房价和特征之间的关系往往不是简单的线性关系比如面积从 50 平到 100 平带来的价格增量和从 150 平到 200 平的增量显然不一样线性模型无法捕捉这种边际递减效应。另外线性回归对所有特征一视同仁没办法自动识别特征之间的复杂交互。4.3 随机森林进阶特征重要性与非线性捕捉线性回归验证完数据管线我换随机森林回归模型。随机森林的本质是构建多棵决策树每棵树基于不同的样本子集和特征子集训练最终把预测结果取平均。它擅长捕捉非线性关系对异常值有更强的鲁棒性还能输出特征重要性。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth15, min_samples_leaf3, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(RandomForest R2:, r2_score(y_test, y_pred_rf)) print(RandomForest MAE:, mean_absolute_error(y_test, y_pred_rf))实测效果通常会有明显提升R2 能到 0.8 以上MAE 降到 30 万以内。这时候我会输出特征重要性importance pd.DataFrame({ feature: X_train.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(10))特征重要性会告诉你哪些字段对价格预测贡献最大。我跑这个项目时“面积”“所在城区_XX区”“总楼层”“装修_精装”这几个特征排在最前面说明这些是最核心的房价决定因素。这个结果反过来也验证了特征工程的方向是对的。如果你想进一步提升精度还可以试梯度提升模型比如GradientBoostingRegressor或者XGBoost它们通常在结构化数据上表现更好。但要注意这类模型树更多、超参数更多训练时间更长调参难度也更大。我的建议是先把手里的随机森林调到稳定再尝试更复杂的模型不要一开始就陷入调参泥潭。4.4 模型调参与交叉验证随机森林的超参数里我优先调整三个n_estimators树的数量、max_depth最大深度、min_samples_leaf叶节点最少样本数。树太少拟合不足树太多训练变慢且收益递减max_depth太大容易过拟合太小拟合不足min_samples_leaf设置大一点能有效抑制过拟合特别是在数据量只有几千条的情况下。调参方法我用网格搜索加交叉验证。GridSearchCV配合 5 折交叉验证比手动调参快得多from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300, 500], max_depth: [10, 15, 20], min_samples_leaf: [2, 3, 5] } grid GridSearchCV( RandomForestRegressor(random_state42), param_grid, cv5, scoringr2, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(grid.best_params_)交叉验证的核心价值是防止过拟合。如果只用一份测试集反复调参你实际上是在“适应”测试集而不是真正提高模型泛化能力。交叉验证把训练集再切成多份轮流当验证集得到的结果更接近模型在未知数据上的真实表现。我调完参之后习惯用最优参数重新训练再用测试集评估一次确认最终效果。5. 实战问题排查与避坑心得5.1 爬虫阶段页数越爬越少解析频繁失败我遇到过最典型的问题是爬前面几页好好的到后面页数返回的数据越来越少或者某页直接解析出 0 条。这种情况大概率不是网站改版而是请求频率太高触发了反爬服务器开始返回验证页面解析器自然找不到房源节点。排查方法是打印每次请求的状态码和响应内容长度。如果状态码变成 302 或 403响应内容长度骤减基本可以断定是反爬拦截。处理方式很直接停止爬取休息 10 到 30 分钟再把请求间隔调大到 2 到 3 秒重新跑。我自己在练习项目里一般保持 1.5 秒到 2 秒的间隔就不会触发拦截。5.2 解析乱码编码不对中文全变问号requests拿到的网页内容如果直接resp.textrequests 会根据响应头猜测编码猜错了中文就会乱码。解决办法是手动指定编码。常见房产网站一般是 UTF-8 或 GBK你可以打印resp.encoding确认也可以用resp.apparent_encoding自动检测然后设置resp.encoding utf-8根据实际情况调整。这个坑很小但遇到一次就能浪费半小时。5.3 特征工程阶段数据泄露比模型差更致命特征工程里最隐蔽的问题就是数据泄露。举个例子我在第 3 章提到加了一个“商圈均价”特征如果我在划分训练集之前就用全量数据计算每个商圈的均价那测试集的商圈均价已经包含了测试集自身的信息模型评估结果会虚高上线后真实效果远不如训练时的指标。我当时是单独在训练集上计算商圈均价再映射到测试集这样才避免泄露。另外一个容易犯的错是在对数值特征做标准化时fit和transform应该只用训练集测试集只做transform。如果你对全量数据一起fit_transform同样属于轻微的数据泄露。很多入门教科书为了简化不会强调这一点但我建议从一开始就养成“只从训练集学参数”的习惯否则后期做时间序列或者正式项目时会非常被动。5.4 建模阶段评估指标虚高模型上线就翻车有时候训练集指标很好测试集指标也还行但一用到真实新数据上就差很多。原因往往是训练集和测试集的分布不够均匀。比如爬取的房源集中在某几个商圈的高端小区模型学到的是这些区域的规律拿到其他区域的房子就失灵。缓解办法有几个方向一是尽量均匀地采集数据覆盖多个商圈、多种价位二是用分层采样划分数据集把“所在城区”作为分层依据保证训练集和测试集中各城区的比例一致三是用交叉验证而不是单次划分来评估模型稳定性。房价预测项目规模不大这些方法成本都很低但能显著提高模型在不同数据上的表现。写在最后两点最实在的体会这套流程走下来我最大的一个体会是数据科学项目不是“爬完数据 → 跑模型”这种两步走而是爬取、清洗、特征、建模、评估之间的反复迭代。第一次跑完线性回归发现 R2 只有 0.6这时候不要急着换模型先回头看特征有没有问题如果发现“面积”分布存在极端值清洗后指标立刻上一个台阶这种收益比换任何模型都明显。另一个体会是关于流程本身的。很多人觉得“从爬虫到预测”最难的是爬虫或者建模但我实际做下来最花时间的永远在数据清洗和特征工程大约占整个项目七成的工作量。这不是坏消息反而说明你已经走上正轨了——因为数据科学真正的硬功夫就体现在那些别人不愿多花时间的细节里。如果你跟着这篇文章把流程跑通了一遍建议你换一个城市、换一个数据源独立做一次。你会发现具体代码可能不一样但流程框架是通用的。能独立完成一次“数据爬取 → 清洗 → 特征工程 → 建模评估”全流程你就已经具备了做更多数据科学项目的地基。
返回列表