ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

二手房数据分析预测系统实战:从数据清洗到XGBoost模型部署全流程

二手房数据分析预测系统实战:从数据清洗到XGBoost模型部署全流程 简介面向计算机专业正在做课程设计或期末大作业的学生以及需要项目实战练习的学习者这份Python二手房数据分析预测系统资料提供完整可运行的实战项目。内容涵盖数据清洗、房价分析与预测建模的全流程源码同时附带文档报告便于理解数据挖掘项目的整体结构并快速参考复用。压缩包共156个文件以18个Python脚本、18个CSV数据文件、15个HTML页面、11个JavaScript文件为主体另有大量可视化图表图片与配置文件等包体大小约40.03MB各类文件按用途归类清晰。目前已有831人学习使用项目经过严格调试下载解压后即可直接运行。通过源码与报告的对应学习读者能够系统掌握数据预处理、特征工程、模型构建与结果展示的核心思路尤其适合期末答辩、课程设计以及希望获得高分范例的开发者作为实践模板。1. 二手房数据分析预测系统数据从业者绕不开的完整实战项目把一套二手房数据分析预测系统从零搭起来最反直觉的结论是模型挑得再好不如数据洗得干净。我见过太多人花两周调 XGBoost 参数最后发现训练集 R² 高达 0.96、测试集只有 0.55原因只是特征里混进了带价格的字段。这篇文章不说空泛理论只讲一条能直接上手的完整流程房源数据采集清洗、特征工程、模型训练评估再到项目落地才会遇到的坑和部署路径。适合想独立完成数据项目、能对每个环节讲清楚取舍的从业者。文中代码都能直接改着跑。2. 数据从哪来二手房源采集与清洗的完整链路无论模型多先进、特征多精巧采集和清洗决定了数据基本面。这一部分看起来没有模型那么高级但恰恰是项目成败的分水岭。本节先解决数据从哪来的问题再做脏数据清洗跑完这两步你手里就有一份能进模型的干净表格。2.1 抓取房源信息requests BeautifulSoup 的最小采集方案采集是数据系统的源头也是新手最容易卡住的环节。常见做法是用 requests 请求目标站点的列表页再用 BeautifulSoup 解析 HTML把每套房子的标题、小区、户型、面积、楼层、朝向、总价、单价、挂牌时间抽出来。别一上来就上 Scrapy——先跑通单页采集确认字段能取全再谈并发和框架。import requests import time import random from bs4 import BeautifulSoup # 请求头必须带很多站点会根据 UA 特征拦截脚本 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_text): 从列表页 HTML 中提取房源字段返回字典列表 soup BeautifulSoup(html_text, html.parser) items [] for card in soup.select(.listing-card): # 选择器以实际页面 DOM 为准 item { house_id: card.select_one(.house-id).get_text(stripTrue), title: card.select_one(.title).get_text(stripTrue), community: card.select_one(.community).get_text(stripTrue), layout: card.select_one(.layout).get_text(stripTrue), area: card.select_one(.area).get_text(stripTrue), total_price: card.select_one(.price).get_text(stripTrue), unit_price: card.select_one(.unit-price).get_text(stripTrue), } items.append(item) return items def crawl_pages(start_page, end_page): 翻页采集每页之间加随机延时 all_items [] for page in range(start_page, end_page 1): url fhttps://your-target-site.com/ershoufang/pg{page}/ resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page}页返回异常状态码: {resp.status_code}) continue page_items parse_list_page(resp.text) all_items.extend(page_items) time.sleep(random.uniform(2, 5)) # 随机延时是续命关键 return all_items这段代码的逻辑很直白parse_list_page 负责解析单页crawl_pages 负责翻页并汇总。核心有三个值得细说。第一个是 headersUser-Agent 伪装是让请求看起来像真实浏览器的基础操作建议直接用浏览器开发者工具复制自己电脑上的 UA 字符串别用网上抄的通用值。第二个是 select 选择器它依赖目标页面的 DOM 结构不同站点的类名差异很大写之前先在开发者工具里确认卡片节点的实际类名再替换。第三个是 time.sleep(random.uniform(2, 5))固定延时容易被频率检测识别随机延时能显著降低被封风险。爬虫的目标是用最少的代码拿到可用的数据不是把页面全部字段都抓下来。我一般先抓标题、小区、户型、面积、朝向、总价和单价这几个核心字段跑通之后再按需扩展。数据量控制在 5000 到 20000 条就够起步太多的话清洗和训练都会变得迟钝。页面里有房屋 ID 的话务必抓下来这是之后去重的唯一可靠依据比用标题去重稳得多。2.2 数据清洗把爬下来的文本变成能建模的结构化表格采集回来的数据一定是脏的。常见问题包括价格字段带「万」「元/㎡」等混用单位、面积是「89.5㎡」夹着中文符号、户型写成「3室2厅1卫」或「三室两厅」不统一、缺失值以空字符串或「暂无」出现、同一房源被多个页面重复抓取。这些不洗掉后面做特征工程全是错。import pandas as pd import re def clean_price(price_str): 把 488万 转成 488.04880000元 转成 488.0 if not isinstance(price_str, str): return None if 万 in price_str: return float(re.search(r([\d.]), price_str).group(1)) match re.search(r([\d.])元, price_str) if match: return float(match.group(1)) / 10000 # 元 - 万元 return None def clean_area(area_str): 把 89.5㎡ 或 89.5平米 统一转成 89.5 match re.search(r([\d.]), str(area_str).replace( , )) return float(match.group(1)) if match else None def clean_orientation(raw): 朝向文本归一化南 北、南北 统一归入枚举 raw str(raw).replace( , ) if 南北 in raw: return south-north if 东南 in raw: return southeast if 西南 in raw: return southwest if 南 in raw: return south return raw df pd.DataFrame(raw_items) df[house_id] df[house_id].astype(str) df df.drop_duplicates(subset[house_id]) # 按房源 ID 去重 df[total_price] df[total_price].apply(clean_price) df[unit_price] df[unit_price].apply(clean_price) df[area] df[area_text].apply(clean_area) df[orientation] df[orientation_raw].apply(clean_orientation) # 总价或面积解析失败的记录无法参与建模直接删除 df df.dropna(subset[total_price, area]) # 粗筛异常值统一成万元后总价低于 10 万基本是录入错误 df df[(df[total_price] 10) (df[area] 5)]清洗代码的取舍逻辑比代码本身更重要。去重必须用 house_id 而不是房源标题因为同标题可能对应不同挂牌批次。clean_price 的核心是把「万」和「元」统一成「万元」这是二手房数据里最隐蔽的坑——不同板块、不同页面显示的价格单位可能不一致清洗时必须显式统一否则后面算特征相关性全是错的。缺失值处理不建议一刀切删除。总价、面积这种核心字段缺失就删行朝向、装修这种可选字段缺失用 unknown 填充。异常值筛选先靠 total_price 10 且 area 5 这种粗暴规则挡掉明显错误更精细的做法是画箱线图把超出上下边缘 1.5 倍四分位距的记录单独拉出来人工确认。清洗完后打印 df.info() 和 df.describe()看每列的缺失情况、数值范围和均值是判断清洗是否到位的快速手段。3. 特征工程与探索性分析把原始字段变成模型吃得下的数值特征数据清洗完只是第一步原始字段还不能直接进模型。朝向是文本、小区名是高基数 ID、面积和价格之间有复杂关系。这一章先做探索性分析确认数据形态再做特征编码把文本转为模型能处理的数值结构。3.1 先做探索性分析三张图决定模型方向建模前做 EDA 的价值是用三张图快速搞清楚房价分布形态、面积与总价的关系、以及城区样本是否均衡。EDA 不是走形式它直接决定后面选模型和特征的方向如果总价分布严重右偏线性回归的误差会被高价房样本主导更合理的做法是先对目标变量做 log 变换。import math import matplotlib.pyplot as plt import seaborn as sns # 1. 总价分布右偏明显就考虑 log1p 变换 fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(df[total_price], bins60, axaxes[0]) axes[0].set_title(total_price distribution) sns.histplot(df[total_price].apply(math.log1p), bins60, axaxes[1]) axes[1].set_title(log(total_price) distribution) plt.show() # 2. 面积 vs 总价判断变量关系是否线性 plt.figure(figsize(6, 4)) sns.scatterplot(datadf, xarea, ytotal_price, alpha0.3) plt.axhline(df[total_price].median(), colorred, linestyle--) plt.show() # 3. 城区均价验证采集样本是否覆盖主要板块 city_avg df.groupby(district)[unit_price].median().sort_values() city_avg.plot(kindbarh) plt.show()这三张图对应三种判断。第一张分布图告诉你 target 要不要做 log 变换肉眼看到长尾直接选 log1p第二张散点图看面积与总价的关系如果出现明显非线性说明后续可以加面积平方项或者直接交给树模型自行切分第三张城区均价图用来验证采集数据覆盖度如果某个城区样本极少它的统计数字带不动模型泛化要么补数据要么干脆把那部分样本从训练集里拿掉。EDA 过程中要顺手复查缺失值的性质。我之前做过一个模拟项目X清洗完发现朝向字段 35% 缺失但这个字段不影响核心建模后来在特征工程里设了 unknown 分类。最怕的是缺失不是随机的——比如高价房源更不爱填装修信息这种结构性缺失直接删掉等于让模型在高价区间失去重要信号。应对方法是对比缺失记录与正常记录的目标均值差异差异明显就要么补全要么显式生成一个「是否缺失」标志特征。3.2 特征编码高基数类别特征别直接 One-Hot特征工程这一步二手房系统里最常用的有两类低基数类别特征朝向、装修、户型和高基数类别特征小区名、行政区域。前者适合 One-Hot后者按小区做目标编码比 One-Hot 更适合因为小区数量动辄上千One-Hot 会瞬间把维度撑爆而且绝大多数小区样本极少独立成列既稀疏又容易过拟合。特征类型推荐做法原因朝向、装修、户型One-Hot枚举少可解释性强行政区域目标编码类别几十种One-Hot 能做但编码更稳小区名目标编码上千类别One-Hot 维度爆炸from sklearn.preprocessing import OneHotEncoder # 低基数特征One-Hotdrop_first 避免共线性 df pd.get_dummies(df, columns[orientation, decoration], drop_firstTrue) def target_encode(series, target, alpha10): 按类别统计 target 均值并用全局先验做平滑 global_mean target.mean() grp target.groupby(series).agg([sum, count]) encoded (grp[sum] alpha * global_mean) / (grp[count] alpha) return series.map(encoded) df[community_encoded] target_encode(df[community], df[total_price]) df[log_area] df[area].apply(math.log1p)target_encode 里有两个硬核参数。alpha 是平滑系数它解决的是小样本类别统计不可靠的问题某个小区只有 1 条记录时直接用这条记录均价编码会严重过拟合alpha 越大编码值越趋向全局均值。实际项目里 alpha 取 5 到 20做完建议再叠加 K 折外编码即每一折只用训练部分统计编码避免验证集信息泄漏到特征里。get_dummies 里的 drop_firstTrue 去掉一列冗余避免和截距项共线线性模型里这一步很重要。还有几个特征容易被忽略但实际很有用楼龄当前年份减建成年份、单价总价除以面积、楼层档位低/中/高、近地铁步行距离如果有坐标。这些衍生特征计算成本低对线性模型和树模型都有实际增益。做完特征工程后跑一遍 df.shape 确认行数没变、列数增加合理再用 df.isna().sum() 确认没有引入新的缺失值。4. 房价预测模型从线性基线到梯度提升树的调参路径特征就绪之后进入建模环节。这一章先讲为什么必须从线性回归跑起再给 XGBoost 的完整参数配置和特征重要性分析方法。目标不是把参数调到极致而是让你拿到一套可以复制、可以解释、可以排查问题的建模流程。4.1 先跑通线性回归基线模型是排查问题的锚点我一般会先跑线性回归不为多好的精度而是为了验证数据流程整体贯通。一个能跑的简单模型配合常规评估指标能快速暴露数据泄露、异常值、单位不统一等问题。如果线性回归的 MAE 小到离谱那大概率是目标变量被混进了特征别急着高兴。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score feature_cols [c for c in df.columns if c not in (total_price, house_id, title)] X df[feature_cols].select_dtypes(includenumber) y df[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(MAE:, mean_absolute_error(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse)) print(R2:, r2_score(y_test, y_pred))这里有个细节feature_cols 先把明显不该进模型的对象列排除掉再用 select_dtypes(includenumber) 兜底只保留数值列。标题和房屋 ID 不能进模型前者是文本、后者是高基数 ID直接喂给线性模型会导致维度灾难或无意义拟合。基线模型的 MAE 是后续对比的锚点如果后面把特征工程做扎实树模型理应比基线明显下降要是没下降说明改进方向偏了。评估指标的选择也有信息量。MAE 适合向业务方解释平均预测差多少钱RMSE 对异常值更敏感会放大高价房预测偏离的问题。我会额外看一眼分位数误差——只算预测价格最低 10% 和最高 10% 样本的 MAE如果低总价样本误差远小于高总价样本说明模型在高价段位拟合不足光调参救不回来要加特征或者对目标变量做 log 变换。4.2 集成模型XGBoost 参数表与早停策略表格型回归任务的首选不是深度学习而是梯度提升树。随机森林方差小但偏置高XGBoost 通过正则化和梯度提升把偏置打下来是这类项目最稳的主模型。我直接用 XGBoost配合早停控制迭代轮数from xgboost import XGBRegressor xgb XGBRegressor( n_estimators1000, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42, early_stopping_rounds50, eval_metricmae, ) xgb.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb xgb.predict(X_test) print(XGBoost MAE:, mean_absolute_error(y_test, y_pred_xgb))关键参数速查表参数建议值说明max_depth5数据特征不多时 4~7 是甜点位越大越容易过拟合learning_rate0.05越小越稳但需要更多树配合早停使用n_estimators1000不直接决定最终轮数早停会自动截断subsample / colsample_bytree0.8每棵树随机采样比例噪声大时可降到 0.7reg_alpha / reg_lambda0.1 / 1.0正则权重特征多时可加大early_stopping_rounds50验证集连续 50 轮无改善就停机返回最优轮learning_rate 设 0.05 配合 n_estimators1000 靠早停自动收敛比一次性设 100 棵树效果好一个档次。subsample 和 colsample_bytree 都设在 0.8给模型加一点随机性减少方差reg_alpha 对这类噪声不小的数据有实际帮助early_stopping_rounds50 是防止过拟合最省心的招。如果环境里没有 XGBoost用 LightGBM 或随机森林逻辑一样只是参数名略有差异。树模型训练完我会把特征重要性打出来看一遍import pandas as pd importance pd.DataFrame({ feature: X_train.columns, importance: xgb.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(15))看特征重要性的目的不是挑刺而是找反向信号。如果面积、小区目标编码、城区排在前面说明采集和特征工程做对了如果楼层、朝向这类该有效的特征重要性接近 0可能是编码方式有问题。特征重要性排序也能帮你砍掉尾部无效特征让最终模型更稳。5. 常见问题与避坑二手房预测系统的 5 个经典翻车现场这一章是整套流程里最值钱的部分。以下 5 个问题都是我在实际项目里见过、踩过、帮人排查过的按现象、原因、解决三步记录。前两个决定模型评估可信度后三个决定项目能否顺利上线。5.1 数据泄露模型「太好」往往是坏消息现象训练集 R² 高达 0.96测试集却只有 0.5或者两个集合指标都好得夸张导致你误以为模型已经完美。原因特征里有字段直接或间接包含目标价格的信息。最常见的元凶是小区均价列和单价字段——单价等于总价除以面积只要面积误差不大用单价预测总价就是作弊。解决训练之前人工过一遍特征名把所有名字里带 price、均价、挂牌价的列全部剔除。再做一次保险验算用留下的特征训练决策树看特征重要性里有没有哪个特征贡献异常偏高。我做过一个模拟项目X排查到最后发现是「近 30 天小区成交均价」这个特征它几乎就是目标值的加噪声版本删掉之后测试集指标才恢复正常。特征来源审查不需要很复杂把特征列和来源写进一个 md 文件写不出来的特征就该怀疑。5.2 随机切分让评估结果虚高现象模型在随机切分的测试集上表现很好但上线后预测新挂牌房源误差明显变大业务方开始质疑模型的可靠性。原因二手房价格带时间趋势随机切分让训练集和测试集高度同分布模型相当于偷看了未来数据的分布规律评估结果虚高。解决如果数据带挂牌时间按时间做切分——前 80% 月份训练、后 20% 月份测试没有时间字段时用 GroupKFold 按小区分组交叉验证避免同一小区不同房源同时出现在训练和测试里。注意时间切分的测试集指标通常比随机切分低一些这是正常的别因此怀疑模型坏了。5.3 高基数类别字段 One-Hot 维度爆炸现象One-Hot 之后训练数据从 20 列涨到 2000 多列训练变慢且明显过拟合模型在验证集上的表现反而不如简单模型。原因把小区名、开发商这种高基数类别字段也丢进了 get_dummies。解决小区名、区域改目标编码或频次编码只有朝向、装修、户型这种枚举数小于 10 的字段才 One-Hot。如果业务上必须保留小区名先做频次过滤只保留样本数超过一定阈值的小区剩下的归为 other 类。5.4 爬虫采集被识别固定延时是致命伤现象采集到第 50 页后开始持续返回 403 或验证码页面导致数据中断。原因请求头固定、请求间隔固定、访问节奏和真实用户差异明显。站点风控看的是短时间内的访问节奏不是单次请求。解决每次请求之间用 random.uniform(2, 5) 随机延时请求头补成真实浏览器的完整集合UA、Accept、Accept-Language。血的教训别为了赶进度把延时压到 1 秒以内被封一次等解封的时间远超省下的时间。采集代码上线前再加一个字段完整性校验当解析结果为空或字段缺失时自动告警而不是悄悄产出脏数据。5.5 价格单位口径不统一现象整体评估不错但预测结果偶尔出现离谱价格比如面积 80 平方米、总价预测 0.08 万元看不出是哪里出了问题。原因同一列里部分记录是「488 万」另一部分是「4880000 元」清洗时没有统一换算模型被迫学习了不一致的目标口径。解决清洗阶段最后一个步骤加断言检查总价取值范围和中位数是否落在真实量级。把元统一转成万元后跑 df[total_price].describe()看 25%、50%、75% 分位数是否符合常识。做转换函数时优先用正则提取数字不要用字符串切片单位字符长度在不同平台上不可靠。提示这 5 个坑里数据泄露和训练测试切分最容易影响模型评估可信度。建议每次训练前都做一次特征来源审查再对比训练集和测试集的关键字段分布确认没有意外的信息泄漏。6. 进阶把预测系统从笔记本变成可迭代的服务训练完模型只是项目的一半另一半是让它能持续被调用、持续被验证。这一章用一个轻量接口解决模型部署问题再聊定期重训练和项目文档的沉淀方式。6.1 模型持久化与最小推理接口训练结束后下一步是把模型从 Jupyter 里搬出来。用 joblib 保存模型和特征列再用 Flask 写一个最小接口让项目从「跑完结果在 notebook 里」变成「能给别人调用的服务」。import joblib from flask import Flask, request, jsonify # 把模型和特征列一起打包特征列顺序是推理的关键 joblib.dump({model: xgb, feature_cols: feature_cols}, service.joblib) pkg joblib.load(service.joblib) model, feature_cols pkg[model], pkg[feature_cols] app Flask(__name__) app.route(/predict, methods[POST]) def predict(): data request.get_json() features [data.get(col, 0) for col in feature_cols] return jsonify({total_price_pred: float(model.predict([features])[0])}) app.run(host0.0.0.0, port5001)这段代码里最容易被忽略的是 feature_cols 的一致性。推理时传入服务的数据列顺序必须与训练时完全一致我习惯把特征列名单和模型打包存在同一个文件里推理端只从包里读列名不手写特征列表。接口用 POST JSON 比 GET 更稳妥参数多的时候不会被 URL 长度限制卡住也方便调用方传嵌套结构。6.2 定期重训练与项目文档沉淀二手房市场受供需、地段成熟度和基础设施变化影响模型上线一个月后精度就会开始下降。常见做法是每周拉取一次新挂牌数据和旧数据合并重新训练做时间切分验证。指标没有明显下降就继续用旧模型有明显下降就用新模型替换。这个流程里最容易出问题的不是训练而是新数据清洗和特征工程必须完全复用训练时的代码——稍有不一致模型在线上的表现就会静默变差。最后聊项目文档。完整交付通常包含采集脚本、清洗脚本、特征工程脚本、模型训练脚本、评估实验记录和一份数据字典。文档不用写长但必须写清数据来源、字段口径、模型选型理由和调参记录——不是给领导看是给三个月后的自己看的。我早期做过一个跨平台数据项目调完模型后没留文档三周后再去改特征连当时为什么对朝向做四分类都记不清了最后只能重跑一遍实验。从那以后每个项目我都先建一个数据字典文件边做边补踩过的坑也顺手记进去。希望我的这些经验能帮到你少走一段弯路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表