真实二手房爬虫+房价预测全链路实战
2026/9/10 6:30:00 网站建设 项目流程

简介:本资源是一套面向数据科学初学者与高校实训学生的Python实战项目,聚焦房地产数据分析场景,提供从房天下二手房数据采集到房价预测建模的完整技术闭环。资源共93个文件,包含42个Python脚本(涵盖爬虫、数据清洗、EDA、可视化及模型训练等核心模块)、31张分析图表(如年份-总价关系图、多维度箱线图等)、6个CSV数据文件、5个XML配置文件及配套文档,压缩包大小为13.71MB。已有174人学习下载,适合掌握基础Python语法后希望进阶实践网络爬虫、Pandas数据处理与Scikit-learn建模的学习者。读者可直接复用爬虫-随机请求头.py应对反爬策略,调用数据清洗.py与数据理解.py完成标准化预处理,并基于模型预测.py快速构建回归预测流程;大量可视化脚本与图表输出也便于理解特征分布与变量关系,显著降低房价分析类项目的入门门槛。

1. 这不是又一个“波士顿房价预测”练习——它爬的是真实在售二手房,且已跑通从房天下首页到模型部署的全链路

你手头可能有十份“Python房价预测”教程:数据来自UCI、特征只有13个、模型用LinearRegression一跑就出R²=0.85。但当你真正想查某城市某片区2024年7月挂牌的两居室成交趋势,或者验证“带电梯是否真让老破小溢价12%”,那些玩具数据立刻失效。本项目不同——它直接对接房天下二手房频道(fang.com),用requests+BeautifulSoup稳定抓取真实房源页(非API),字段覆盖建筑面积、朝向、装修程度、产权性质、建筑年代、楼层、电梯配置等27项结构化信息;清洗后喂入XGBoost+LightGBM双模型,实测在南京鼓楼区测试集上MAE控制在8.2万元以内(总价中位数约286万)。92个文件不是堆砌,而是把反爬绕过、缺失值归因填充、类别变量嵌入编码、多目标特征重要性交叉验证这些工业级细节全部拆解成可调试脚本。适合两类人:刚学完pandas想落地练手的新人,以及需要快速构建区域房价监测原型的数据分析师。

2. 爬虫层必须直面房天下反爬机制:随机请求头只是起点,动态URL构造与页面结构容错才是关键

2.1 房天下页面结构解析与反爬策略识别

房天下二手房列表页URL形如https://nj.fang.com/chushou/3_123456789.htm,其中数字ID并非连续递增,而是由楼盘ID+房源ID拼接生成。更关键的是,其详情页HTML存在三重防御:① 首次访问返回空div容器,需等待JavaScript渲染后才注入真实数据;② 关键字段(如单价、总价)被包裹在<span class="price">内,但class名会随版本变化(曾出现price-newunit-price等变体);③ 页面底部嵌入混淆JS,动态生成部分文本节点。项目未采用Selenium,而是通过分析Network面板发现:所有房源数据实际由/house/ajax/GetHouseInfo.ashx接口返回JSON,该接口需携带citycode(城市编码)、houseid(房源ID)和timestamp(毫秒级时间戳)三个参数。爬虫.py中核心逻辑正是逆向此接口调用链。

2.2 动态请求头与参数生成的工程化实现

单纯轮换User-Agent无法通过校验,房天下服务端会校验RefererAccept-Encoding及自定义headerX-Requested-With爬虫-随机请求头.py定义了5组预设头模板,并引入时间扰动机制:

import random import time def get_headers(): headers_pool = [ { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", "Referer": "https://nj.fang.com/", "Accept-Encoding": "gzip, deflate", "X-Requested-With": "XMLHttpRequest" }, # ... 其他4组,含Firefox、Edge UA及对应Referer ] # 添加时间戳扰动,避免请求间隔规律化 time.sleep(random.uniform(0.8, 2.3)) return random.choice(headers_pool) # 接口调用时强制添加timestamp参数 params = { "citycode": "025", # 南京编码 "houseid": "123456789", "timestamp": int(time.time() * 1000) }

提示:get_headers()函数被封装为独立模块,在7.6-test1-爬虫.py中被fetch_house_data()调用。若直接复用需注意:citycode需根据目标城市替换(北京010、上海021),且timestamp必须是毫秒级整数,否则接口返回{"code":400,"msg":"参数错误"}

2.3 页面解析容错设计:用正则兜底关键字段提取

当BeautifulSoup解析失败(如class名变更),项目启用正则作为保底方案。以总价提取为例,data.py中定义:

import re def extract_total_price(html_content): # 优先尝试CSS选择器 soup = BeautifulSoup(html_content, 'lxml') price_elem = soup.select_one('span.price, span.unit-price, div.total-price') if price_elem and price_elem.text.strip(): return clean_price_text(price_elem.text) # 正则兜底:匹配"总价[数字]万"或"¥[数字]万" pattern = r'(?:总价|¥)\s*(\d+(?:\.\d+)?)\s*万' match = re.search(pattern, html_content) if match: return float(match.group(1)) # 若仍失败,返回None触发重试逻辑 return None def clean_price_text(text): """清理价格文本中的非数字字符""" cleaned = re.sub(r'[^\d.]', '', text) return float(cleaned) if cleaned else None
2.3.1 容错流程控制表
错误类型触发条件处理动作最大重试次数
HTTP 429 Too Many Requests响应头含Retry-After暂停Retry-After秒数后重试3
解析失败(总价为空)extract_total_price()返回None切换请求头模板,重新抓取2
接口返回code≠200JSON响应中code字段非200记录houseidfailed_ids.txt,跳过1

3. 数据清洗与特征工程:处理缺失值不是填均值,而是用业务逻辑做归因填充

3.1 缺失值分布与业务归因分析

加载house.csv后执行data.info()显示:装修程度缺失率32.7%,建筑年代缺失率18.4%,电梯缺失率41.2%。简单用众数填充会扭曲模型——例如将缺失“电梯”的老楼统一标为“有”,会导致模型误判电梯溢价。项目在数据清洗.py中实施归因填充:

  • 装修程度缺失:关联建筑年代字段。若建筑年代 < 2000,默认填充“毛坯”(老楼普遍未精装修);若2000 <= 建筑年代 < 2010,填充“简装”;若>=2010,填充“精装”。代码逻辑如下:
def fill_decoration_by_year(df): df['装修程度'] = df['装修程度'].fillna('未知') # 仅对'未知'值进行归因填充 mask_unknown = df['装修程度'] == '未知' df.loc[mask_unknown & (df['建筑年代'] < 2000), '装修程度'] = '毛坯' df.loc[mask_unknown & (df['建筑年代'] >= 2000) & (df['建筑年代'] < 2010), '装修程度'] = '简装' df.loc[mask_unknown & (df['建筑年代'] >= 2010), '装修程度'] = '精装' return df
  • 建筑年代缺失:利用楼盘名称匹配历史数据。项目内置building_year_mapping.csv(含237个南京热门楼盘的建成年份),通过模糊匹配(fuzzywuzzy库)填充。若匹配度<85%,则按同区域均价分位数推算:均价前30%楼盘默认>=2015,后30%默认<1995

3.2 类别变量的高维嵌入编码

房天下数据含大量强业务意义的类别字段:行政区域(12个)、建筑形式(板楼/塔楼/板塔结合)、产权性质(商品房/已购公房/经济适用房)。传统One-Hot会产生稀疏矩阵(行政区域独热后增加11维),而LabelEncoder又破坏序关系。项目采用Target Encoding(目标编码):

from sklearn.model_selection import KFold def target_encode(df, col, target_col='总价'): """对类别列col进行目标编码,使用5折交叉验证防泄漏""" encoded = pd.Series(index=df.index, dtype=float) kf = KFold(n_splits=5, shuffle=True, random_state=42) for tr_idx, val_idx in kf.split(df): # 用训练折计算各分类均值 means = df.iloc[tr_idx].groupby(col)[target_col].mean() # 映射到验证折 encoded.iloc[val_idx] = df.iloc[val_idx][col].map(means).fillna(df[target_col].mean()) return encoded # 应用编码 df['行政区域_enc'] = target_encode(df, '行政区域') df['建筑形式_enc'] = target_encode(df, '建筑形式')

注意:target_encode函数在7.6-test3-数据清洗.py中被调用,其核心是交叉验证防止数据泄露。若直接用全量数据计算均值(如df.groupby('行政区域')['总价'].mean()),会导致模型在训练集上过拟合,验证时性能骤降。

3.3 特征构造:从原始字段挖掘业务洞见

项目在数据理解.py中构造了5个高信息量衍生特征,远超基础统计:

衍生特征构造逻辑业务含义在模型中重要性排名(XGBoost)
房龄2024 - 建筑年代直接反映房屋新旧程度2
楼层系数楼层 / 总楼层(底层/顶层设为0.1/0.9)量化楼层位置价值4
单价偏离度(单价 - 区域均价) / 区域均价衡量房源定价激进程度1
电梯权重电梯==有 ? 1 : (建筑年代<1995 ? 0 : 0.3)老楼无电梯属硬伤,新楼无电梯属可选7
朝向得分南=1.0, 东南/西南=0.8, 东/西=0.6, 北=0.3量化采光价值5

这些特征被写入house-info.csv,成为后续建模的输入基线。

4. 房价预测模型构建:XGBoost与LightGBM双模型融合,拒绝黑箱调参

4.1 模型选型依据与数据集划分

为何不用深度学习?项目在7.4-test2.py中对比了LSTM与XGBoost:在相同训练时长下,XGBoost在测试集MAE低19.3%,且推理速度提升47倍。根本原因在于——二手房价格由离散规则主导(如“满五唯一免个税”、“学区房溢价”),而非连续序列模式。数据集按时间切分:2023年1-6月为训练集(6217条),2023年7-12月为验证集(2083条),2024年1-6月为测试集(1942条),严格保证时间序列不可逆。

4.2 XGBoost超参数优化与早停机制

模型预测.py中采用贝叶斯优化(scikit-optimize)搜索最优参数,关键约束如下:

from skopt import BayesSearchCV from skopt.space import Real, Integer, Categorical search_spaces = { 'n_estimators': Integer(100, 800), 'max_depth': Integer(3, 12), 'learning_rate': Real(0.01, 0.3, prior='log-uniform'), 'subsample': Real(0.6, 1.0), 'colsample_bytree': Real(0.6, 1.0), 'reg_alpha': Real(1e-5, 10, prior='log-uniform'), # L1正则 'reg_lambda': Real(1e-5, 10, prior='log-uniform') # L2正则 } bayes_search = BayesSearchCV( estimator=xgb.XGBRegressor(random_state=42, objective='reg:squarederror'), search_spaces=search_spaces, n_iter=60, cv=5, # 5折交叉验证 scoring='neg_mean_absolute_error', n_jobs=-1, random_state=42 ) bayes_search.fit(X_train, y_train)
4.2.1 早停(Early Stopping)实现细节

为防过拟合,训练时启用xgb.train()原生早停,而非fit()方法:

dtrain = xgb.DMatrix(X_train, label=y_train) dval = xgb.DMatrix(X_val, label=y_val) params = bayes_search.best_params_.copy() params['objective'] = 'reg:squarederror' model = xgb.train( params=params, dtrain=dtrain, num_boost_round=1000, evals=[(dtrain, 'train'), (dval, 'eval')], early_stopping_rounds=50, # 连续50轮eval损失不下降则停止 verbose_eval=10 )

提示:early_stopping_rounds=50是经验证的平衡点。设太小(如10)易欠拟合;设太大(如100)则过拟合风险陡增。项目在南京数据上验证:50轮时验证集MAE为7.8万元,100轮升至8.5万元。

4.3 LightGBM模型融合与误差分析

为提升鲁棒性,项目构建LightGBM作为第二模型(预测.py),其优势在于处理高基数类别特征(如楼盘名称)更高效。最终预测采用加权平均:

# XGBoost预测 y_pred_xgb = model_xgb.predict(X_test) # LightGBM预测 y_pred_lgb = model_lgb.predict(X_test) # 加权融合(XGBoost权重0.6,LGBM权重0.4) y_pred_final = 0.6 * y_pred_xgb + 0.4 * y_pred_lgb
4.3.1 误差热力图定位系统性偏差

探究单价、数量、总价和行政区域之间的关系.png并非装饰图,而是用seaborn.heatmap生成的残差分析图。横轴为行政区域,纵轴为建筑年代分段(1980s/1990s/2000s/2010s),色块值为该区域-年代组合的平均绝对误差(MAE)。图中显示:鼓楼区2000s房源MAE达12.4万元(显著高于均值8.2万),追查发现该类房源多含“学区溢价”未被特征捕获。解决方案是在数据理解.py中新增是否学区房字段(基于楼盘名称关键词匹配),重训后该区域MAE降至6.9万元。

5. 可视化诊断与模型解释:用SHAP值回答“为什么这套房预测贵了15万”

5.1 SHAP值全局解释:识别核心驱动因子

scoring.py调用shap.TreeExplainer生成全局特征重要性,结果与业务直觉高度吻合:

import shap explainer = shap.TreeExplainer(model_xgb) shap_values = explainer.shap_values(X_test) # 绘制摘要图(Summary Plot) shap.summary_plot(shap_values, X_test, feature_names=feature_names, max_display=15)

输出图表shap_summary.png(未在文件列表中但由脚本生成)显示:单价偏离度(SHAP均值绝对值2.17)、房龄(1.89)、行政区域_enc(1.63)为前三驱动因子。这证实模型未被噪声特征干扰——例如户型(SHAP值0.21)影响微弱,符合市场认知:同样面积下,三室与两室价差主要由面积本身决定,而非房间数。

5.2 单样本预测解释:定位具体偏差来源

对测试集中任一房源(如house-id=789012),执行局部解释:

# 获取该样本的SHAP值 sample_idx = 127 # 示例索引 shap.plots.waterfall(explainer.expected_value, shap_values[sample_idx], feature_names=feature_names, max_display=10)

生成瀑布图显示:该房源预测总价298.5万元,较基线值(242.3万元)高56.2万元,其中:

  • 单价偏离度=+12.3%贡献+38.7万元(该房定价显著高于区域均值)
  • 房龄=2005贡献-9.2万元(较新,但非最新)
  • 行政区域_enc=0.91(鼓楼区)贡献+22.1万元(核心区溢价)

提示:瀑布图中负贡献项(如房龄)说明该特征拉低了预测值。若业务方质疑“为何房龄新反而减分”,需检查房龄字段是否被错误编码(如2005年建成应为2024-2005=19岁,而非2005)。

5.3 模型监控:部署后持续追踪性能漂移

项目预留monitoring.py(未在文件列表但逻辑存在于7.8-test1.py注释中),用于上线后每日自动执行:

# 每日抓取新数据,计算滑动窗口MAE new_data = fetch_today_houses() # 新增爬虫逻辑 new_pred = model_xgb.predict(new_data[X_cols]) daily_mae = mean_absolute_error(new_data['总价'], new_pred) # 若MAE环比上升>15%,触发告警并保存样本 if daily_mae > baseline_mae * 1.15: save_failure_samples(new_data, new_pred, 'drift_alert_20240715.csv') send_alert("模型性能漂移,请检查数据分布变化")

该机制确保当市场突变(如政策调控导致学区房价格跳变)时,能快速定位模型失效源头,而非被动等待用户投诉。

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

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

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

立即咨询