ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

链家二手房房价预测实战:爬虫、特征工程与模型对比解析

链家二手房房价预测实战:爬虫、特征工程与模型对比解析 简介面向计算机及相关专业毕业设计、课程设计与项目实战练习者提供一份经过导师认可通过的Python二手房房价数据分析与预测项目源码包覆盖链家二手房数据采集、清洗、特征分析与建模预测全流程。压缩包共17个文件包含3个Jupyter Notebook、1个Python脚本、12个CSV数据文件和1份README说明文档整体大小约6.23MB结构清晰、便于按步骤复现。目前已有336人学习下载源码经过严格调试确保可运行适合直接用于毕设参考或课程作业。读者可从爬虫Notebook、主分析Notebook和独立Python脚本中对照学习数据爬取、探索性分析与房价预测建模实现配合CSV数据文件和说明文档能快速理解从原始数据到预测结果的完整流程。整体项目既适合快速搭建可运行毕设项目也适合作为数据分析与机器学习入门实战的参照模板。1. 把链家二手房项目拆开看它其实是一条完整的数据流水线多数人拿到这份 Python 的二手房房价数据分析与预测项目源码第一反应是打开 sol.ipynb 直接从上往下跑一遍看到 matplotlib 画出的房价分布图就以为结束了。但实际上这个项目真正值钱的地方不在那张散点图而在于它把一条完整的数据流水线串在了一起链家网数据爬取、字段清洗、特征构造、模型对比、结果解释。你在毕设答辩或简历项目里能讲清楚这条链路比单纯汇报我用了随机森林R² 是 0.83要扎实得多。这个项目适合两类人一类是正在做计算机相关专业毕设、需要拿现成源码改造成自己课题的学生另一类是已经工作、想快速验证自己在爬虫和 sklearn 上的基本功是否还跟得上的一线开发者。项目里附带链家网数据爬取.ipynb、sol.py 和 data 目录压缩包里的 README.docx 还给了背景说明。下面我按爬取 → 清洗 → 建模 → 落地的顺序拆每一步都会给出可以直接复用的代码和参数心得不是把 notebook 复述一遍。2. 爬虫层链家二手房数据抓取与反爬兜底2.1 解析链家页面结构确认要抓的字段链家二手房列表页的结构很稳定每套房源卡片都在div[classsellListContent]下的li里。卡片里能拿到的字段包括小区名称、位置、房厅、面积、朝向、装修、楼层、总价、单价以及关注人数和发布时间。项目里spider 链家网数据爬取.ipynb用的就是 requests BeautifulSoup 组合没有上 Scrapy原因是单城市几百页的数据量用 requests 足够而且调试起来更直观。抓取前建议先在浏览器里按 F12 确认目标字段所在的 DOM 节点我一般会写一小段探测代码来验证选择器是否命中避免直接开跑后产出一堆空值import requests from bs4 import BeautifulSoup url https://bj.lianjia.com/ershoufang/pg1/ headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} resp requests.get(url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) cards soup.select(div.sellListContent li.clear) print(解析到房源卡片数量:, len(cards)) # 验证第一张卡片里的总价和单价 if cards: first cards[0] total_price first.select_one(.totalPrice span).text unit_price first.select_one(.unitPrice span).text print(总价:, total_price, | 单价:, unit_price)这段代码里.select_one(.totalPrice span)会直接取到总价数字比如 568unitPrice字段是链家页面上的 56789 元/平 这种带备注的格式后面清洗时要把数字部分和单位拆开。跑通这一小段后再写循环翻页页面 URL 里的pg1改成pg2、pg3即可。注意链家网页版每个城市最多只能看到 100 页也就是约 3000 条房源足够做训练集了。2.2 请求频率、Cookie 与异常重试链家对非登录请求的限流比较明显连续高频请求会弹验证码或直接返回空白页。项目里建议把每次请求间隔设置在 2 到 5 秒随机浮动并给 requests.Session 挂上重试机制。下面这段是实际爬虫里常用的兜底写法import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor1, status_forcelist[429, 500, 502, 503]) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) def fetch_page(session, page_url, max_try3): for attempt in range(max_try): try: resp session.get(page_url, headersheaders, timeout15) if resp.status_code 200 and sellListContent in resp.text: return resp.text elif resp.status_code 403: print(触发反爬等待较长时间重试) time.sleep(30) except requests.RequestException as e: print(f请求失败: {e}, 尝试 {attempt 1}/{max_try}) time.sleep(5) return None关键点在于status_forcelist里放进 429 和 5xx配合backoff_factor1会让 requests 在第一次失败后等 1 秒、第二次等 2 秒、第三次等 4 秒形成指数退避。fetch_page里对 403 的特殊处理是给自己留一个手动换 Cookie 的入口如果用了代理或登录后的 Cookie直接在session.headers里更新即可。实际操作时还要做断点续爬把已抓取的页面编号记录到本地文件避免中途断网后从头来。2.3 数据落盘与字段核对爬下来的数据建议先存成 CSV再交给 Pandas 做后续处理。比较稳妥的做法是每抓一页就追加一行而不是全部抓完再一次性写入防止进程崩溃丢数据。字段命名统一用英文字段名后面读取时减少中文字符串比较的麻烦。字段名来源选择器示例原值说明district.positionInfo a第一个朝阳所在行政区bizcircle.positionInfo a第二个望京商圈community.communityName a融科橄榄城小区名称rooms.houseInfo按空格拆分2室1厅户型area.houseInfo拆分89.3平米建筑面积orientation.houseInfo拆分南 北朝向decoration.houseInfo拆分精装装修程度total_price.totalPrice span568万元unit_price.unitPrice span56789元/平单价清洗备用follow_info.followInfo99人关注 / 7天带看热度信息这里最容易犯的错是把.positionInfo里两个a标签直接拼成一个字符串导致行政区和商圈混在一起。正确做法是用select(.positionInfo a)拿到列表再分别取[0]和[1]。链家偶尔会缺follow_info字段写入 CSV 前要做空值判断否则 Pandas 读取时会多出一列 NaN。3. 清洗与特征工程从字符串房价到可训练的数值特征3.1 把中文数字串拆成结构化字段链家原始数据里最典型的问题是houseInfo一个字段塞了户型、面积、朝向、装修、楼层五个维度比如 2室1厅 | 89.3平米 | 南 北 | 精装 | 高楼层。sol.ipynb 里第一步就是对这个字段做拆分。常见做法是按|分隔分隔后每个部分再单独解析import pandas as pd import re def split_house_info(info): parts info.split( | ) result {} if len(parts) 5: result[rooms] parts[0].strip() area_match re.search(r(\d\.?\d*)平米, parts[1]) result[area] float(area_match.group(1)) if area_match else None result[orientation] parts[2].strip() result[decoration] parts[3].strip() result[floor] parts[4].strip() return result df pd.read_csv(lianjia_beijing.csv) info_parsed df[house_info].apply(split_house_info).apply(pd.Series) df pd.concat([df, info_parsed], axis1) df df.dropna(subset[area, total_price]) print(df[[rooms, area, orientation, decoration, floor]].head())split_house_info里用re.search而不是re.findall是考虑到面积字段可能带小数点\d\.?\d*这个正则已经覆盖了 89 和 89.3 两种写法。拆分之后rooms字段是 2室1厅 这种整串后续建模时不能直接喂给回归模型还要转换成 室的数量 和 厅的数量 两个数值特征。3.2 总价、单价统一换算与异常值剔除链家页面上的总价单位是万元单价是元/平两个维度量纲差异巨大。如果不处理树模型会偏向数值大的特征。但更重要的问题是异常值一平米低于 1000 元的房源多半是阁楼单间或数据录入错误总价超过 8000 万的则不属于普通住宅分析范围。sol.py 里对单价做了百分位截断这是很实用的手法unit_price_low df[unit_price].quantile(0.01) unit_price_high df[unit_price].quantile(0.99) df df[(df[unit_price] unit_price_low) (df[unit_price] unit_price_high)] df[unit_price_wanyuan] df[unit_price] / 10000 # 转成万元/平 df[log_total_price] np.log(df[total_price]) # 总价取对数缓解右偏为什么对总价取对数房价分布有明显的长尾绝大多数房源在 300 万到 800 万之间少数豪宅把均值拉高。线性回归对目标变量的正态性有一定容忍度但取对数后 R² 更容易提升且预测结果可以按exp()还原成万元。如果你在毕设里想解释系数含义取对数后系数可以直接解释成面积每增加 1%总价变化 beta 个百分点。清洗完的数据集做个可视化观察画出area分布和log_total_price分布的前后对比你会发现尾部明显收拢。这一步建议存一张对比图放到论文的数据预处理章节作为支撑。3.3 位置、朝向、楼层的编码策略行政区、商圈、朝向、装修、楼层都是类别特征。朝向可以按日照价值排序映射成 0 到 3 的有序数值比如 南 记 3、东南/西南 记 2、东/西 记 1、北 记 0。行政区这类 16 个类别没有天然顺序直接 Label Encoding 会让树模型产生错误的顺序假设两种处理方式都可行一是 category 编码后喂给集成树二是做 one-hot。经验结论是 RandomForest 对高基数类别特征用有序整数编码通常效果不差用时省内存线性回归必须用 one-hot。项目里 sol.py 的做法是给线性模型单独做 get_dummies给树模型保留 LabelEncoderfrom sklearn.preprocessing import LabelEncoder # 线性模型独热编码行政区 linear_features [area, rooms_num, hall_num, orientation_score, decoration] for cat in [district, decoration]: dummies pd.get_dummies(df[cat], prefixcat, drop_firstTrue) df pd.concat([df, dummies], axis1) # 树模型有序整数 le_district LabelEncoder() df[district_code] le_district.fit_transform(df[district]) df[floor_code] LabelEncoder().fit_transform(df[floor])drop_firstTrue会留下 k-1 个哑变量避免线性回归里的多重共线性问题。树模型的楼层编码要注意链家把楼层分成 高楼层 / 中楼层 / 低楼层 / 顶层/底层 几种其中顶层和底层在采光、噪音上差异大如果样本量充足可以单独成一个类别不要和低楼层混在一起。字段编码做完后最终训练集的特征列保持在 20 个左右过多会导致模型在千级样本上过拟合。4. 模型选型与预测线性回归、随机森林与 XGBoost 的实战对比4.1 基准模型多元线性回归与系数可解释性房价预测最常见的切入点是线性回归。它在这里的价值不是拿最高精度而是给你一个可解释的基线。用statsmodels的 OLS 可以同时拿到系数的显著性水平毕设答辩时讲面积系数显著为正p 值小于 0.001比单纯报 R² 更有说服力。sklearn 版本的实现如下from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score feature_cols [area, rooms_num, hall_num, orientation_score, district_code, decoration_code] X df[feature_cols].fillna(0) y df[log_total_price] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) print(R²:, r2_score(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse))这里的random_state42固定切分结果保证每次跑出来的指标可比。squaredFalse让 RMSE 和因变量同一单位对数值更直观。线性回归的 R² 通常落在 0.6 到 0.75 之间不用气馁因为房价里还有很多未捕捉因素比如学区溢价、楼龄、停车位而这些信息链家列表页没有暴露。如果你把district_code换成 one-hot 后的行政区哑变量R² 一般能再涨 2 到 3 个百分点。需要留意的坑是decoration_code这类有序编码在线性回归里会强行让 精装」比「简装」高 1 个单位实际上不是线性关系。更科学的做法是对装修程度做哑变量。如果为了统一编码导致线性回归系数不好解释就在论文里说明这是简化的基线模型。4.2 树模型进阶随机森林与 XGBoost 的参数调整随机森林和 XGBoost 这类集成树模型非常适合表格型数据不需要做特征缩放也能自动捕获面积与总价之间的非线性关系。下面是 sol.py 里实际使用的两段训练代码先看随机森林from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth15, min_samples_leaf5, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(RF R²:, r2_score(y_test, y_pred_rf)) print(RF RMSE:, mean_squared_error(y_test, y_pred_rf, squaredFalse))n_estimators300对于 3000 条样本足够再大收益递减且训练变慢max_depth15防止单棵树学太深配合min_samples_leaf5限制叶子节点最少样本数避免某个 10 平米的地下室单间把分支学偏。如果你发现随机森林 R² 只有 0.7 而线性回归有 0.72先检查是不是把原始total_price而不是log_total_price当作目标树模型对长尾分布更敏感取对数后仍能提升稳定性。XGBoost 部分需要单独安装 xgboost参数习惯和随机森林不同import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators400, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42 ) xgb_model.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) y_pred_xgb xgb_model.predict(X_test) print(XGB R²:, r2_score(y_test, y_pred_xgb))learning_rate0.05调得比较保守让每棵树学一小步后面用n_estimators补足轮数subsample和colsample_bytree都设为 0.8相当于随机抽样行和列降低方差reg_alpha和reg_lambda是 L1/L2 正则对 3000 条样本的作用尤其明显不加正则时 XGBoost 很容易在训练集上跑到 0.95 的 R²测试集却只有 0.75。eval_set传入测试集训练的每个轮次都会记录验证误差配合early_stopping_rounds可防止过拟合。4.3 特征重要性排序与误差诊断三个模型跑完后不要只看 R²还要看误差形态。用 matplotlib 把预测值和真实值的散点图画出来对角线yx附近点越密集说明模型越可靠。常见病态是低房价区域预测偏高、高房价区域预测偏低这是回归模型向均值收缩的典型表现毕设里可以专门写一段分析。特征重要性用随机森林的feature_importances_或 XGBoost 的plot_importance展示importance pd.Series(rf.feature_importances_, indexX_train.columns).sort_values(ascendingFalse) print(importance.head(10))最终你会发现area和district_code通常排前两名。这符合直觉面积直接决定空间价值行政区隐含了地段和学区背景。如果一个模型特征重要性里orientation_score高得离谱多半是编码顺序造成的人为信号需要重新审视特征构造。三种模型的指标汇总建议用一张表放进论文模型R²RMSE(万元)说明线性回归0.710.183系数可解释适合做基线随机森林0.790.152非线性捕获好训练快XGBoost0.820.141精度最高正则参数需调RMSE 的单位是 log(万元)要还原成万元需要看exp(pred)逆变换后的误差。比如 RMSE 0.141 对应总价水平约正负 15% 的相对误差这对挂牌价的预测来说是合理范围因为同一小区相似房源挂牌价差别也很大。5. 毕设与课程设计落地调试经验、结果解释与验证技巧5.1 源码运行失败的三个高频原因这个压缩包里的 notebook 和 sol.py 拿来即用但环境不同还是会踩坑。第一是data目录下的 CSV 可能是从 Windows 笔记本保存的Pandas 读取时要用encodinggbk或enginepython否则报 UnicodeDecodeError。第二是 xgboost 在不同 Python 版本上的 wheel 版本冲突建议用pip install xgboost1.7.6这类稳定版本而不是直接装最新版。第三是 sol.py 里如果用了相对路径读取lianjia_beijing.csv必须在项目根目录下执行不要在上一级目录用python src/sol.py跑会把相对路径解析错。验证项目是否能跑通最快的方式是先在命令行里执行一次python sol.py能跑完且打印出 R² 说明环境没问题。接着再看 sol.ipynbKernel 重启后逐格运行遇到某个 cell 输出空白多半是没有%matplotlib inline或者 pandas 版本 2.0 之后对append用法的兼容问题。5.2 把项目改造成自己的毕设课题拿到源码后不要原样交上去至少要替换两个部分。一是数据覆盖城市把爬虫里的bj.lianjia.com改成你所在城市或目标研究城市的子域名比如上海sh.lianjia.com、成都cd.lianjia.com然后重跑爬虫生成新数据集。这样你的数据来源和结论跟原项目区分开来。二是预测目标扩展除了总价还可以把unit_price单独作为目标变量研究各因素对单价的影响单价与面积的相关性通常比总价与面积的相关性更弱分析起来更有的写。如果时间有限不打算重抓数据就在特征上做加法把follow_info字段里的关注人数和带看次数拆出来作为热度特征分析挂牌超过 90 天对下调挂牌价的暗示。链家列表页的follow_info原始值是 99人关注 / 7天带看用正则拆成两个数值即可这个增量特征在答辩时很容易被提问。5.3 一个值得做的技巧用 SHAP 解释单套房源预测树模型跑完只看图形里的特征重要性还不够前沿一点的毕设做法是用 SHAP 解释单个样本的预测。安装shap后对测试集某一条记录生成决策贡献值能说明为什么这套房子预测 568 万其中面积贡献了 80 万商圈贡献了 26 万朝向贡献了 -5 万。这样的输出在评审老师看来就是真正理解了模型而不是调包侠。import shap explainer shap.TreeExplainer(xgb_model) shap_values explainer.shap_values(X_test) shap.force_plot(explainer.expected_value, shap_values[100, :], X_test.iloc[100, :], matplotlibTrue)如果环境不支持 force_plot 的 HTML 交互改成shap.summary_plot(shap_values, X_test)也一样能出图。这个图可以直接贴进毕设的模型可解释性分析小节。整个项目做到这一步已经从单纯的预测房价升级成了解释房价这也正是导师愿意给 98 分的技术增量所在。本文还有配套的精品资源点击获取
返回列表