尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

二手房房价预测全流程实战:从爬虫清洗到随机森林调优

发布时间:2026/9/26 14:41:29

资讯中心
01
ARTICLE

二手房房价预测全流程实战:从爬虫清洗到随机森林调优

二手房房价预测全流程实战:从爬虫清洗到随机森林调优
简介这是一份基于Python的二手房房价数据分析与预测项目源码与说明文档定位于高校毕设/课设、数据科学入门与进阶自学特别适合有基础编程能力、希望走通真实数据处理全流程的学习者。压缩包共17个文件包含12个CSV数据集、3个便于跟随操作的Jupyter Notebook教学文档、1个Python脚本和1份Word版项目说明书整体大小仅6.22MB目录精简便于快速上手。项目以链家二手房为切入点覆盖缺失值处理、异常值检测、Pandas探索性分析、可视化呈现特征关系并基于Scikit-learn构建线性回归、决策树回归和随机森林等模型通过交叉验证调参和特征工程提升预测表现此外还引入时间序列分析尝试对未来房价走势做出判断。当前已有118人学习下载适合需要完整项目参考的毕设课设用户。配套说明文档和脚本完整保留了主要实现思路拿到后可以边读边练也便于替换城市或数据集进行二次扩展。1. 二手房房价预测项目一条值得完整跑通的数据分析链路在数据分析和预测这条路上很多项目要么只讲算法原理要么直接丢给你一份清洗好的数据想自己撑起“拿数据 → 清洗 → 建模 → 出结论”整条链路往往要东拼西凑。这份二手房房价数据分析与预测源码包是少见的全链路项目爬虫、清洗、EDA、线性回归 / 决策树 / 随机森林建模、交叉验证一步不缺。拆完最大的感受是它不追求模型上限而是把流程的完整性放在第一位特别适合想让 Pandas 和 Scikit-learn 落地的人。无论你是刚学完 Python 基础还是正在为课设、毕设找一份 baseline这份资源都能直接拿来跑通一条真实链路。按说明把两个 notebook 跑通你就能看到数据从网页变成预测结果的全过程。下面我按自己拆解的顺序把爬虫、清洗、建模和踩坑点逐段展开中间会贴出可执行的替代代码方便你复现时对照。2. 从链家爬虫到结构化数据把页面信息落成 DataFrame2.1 爬虫脚本里到底做了三件事资源里的“链家网数据爬取.ipynb”职责很清楚把目标城市的二手房列表页抓下来解析出字段再交给后续分析使用。很多新手以为爬虫的核心是“请求页面”其实真正花时间的部分是从 HTML 里稳定地提取字段。按我的拆解这个 notebook 做了三件事请求列表页拿到 HTML从每套房子的卡片里抽总价、单价、面积、户型、朝向、楼层、区域等字段翻页拼接 URL加上延时循环抓取。下面这段是我按同样思路整理的最小版本。注意这里把“请求”“解析”“翻页调度”拆成了三个函数这个习惯很值得保留。import time import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_listing_page(page: int) - str: # 实际使用时把 example 域名替换成项目里配置的目标地址 url fhttps://xx.example.com/loupan/pg{page}/ resp requests.get(url, headersHEADERS, timeout15) resp.raise_for_status() return resp.text def parse_listing(html: str) - list[dict]: soup BeautifulSoup(html, html.parser) items [] # 下面的选择器以实际页面结构为准项目 notebook 里已经调好 for card in soup.select(.sellListContent li): total card.select_one(.totalPrice span) unit card.select_one(.unitPrice) info card.select_one(.houseInfo) if total and unit and info: items.append({ 总价: total.text.strip(), 单价: unit.text.strip(), 房源信息: info.text.strip().split(|), }) return items def crawl(max_page: int 10, sleep_secs: float 1.5): all_rows [] for page in range(1, max_page 1): html fetch_listing_page(page) rows parse_listing(html) print(fpage {page}: {len(rows)} rows) if not rows: break all_rows.extend(rows) time.sleep(sleep_secs) return all_rows if __name__ __main__: data crawl(max_page10) print(len(data))逻辑说明fetch_listing_page 只负责拿页面parse_listing 只负责从页面里抠字段crawl 负责控制翻页和节奏。这样目标站点某个字段的标签变了你只需要改 parse_listing不用动整个链路。参数上max_page 控制抓取深度sleep_secs 控制请求间隔。1.5 秒只是一个保守节奏。实际按目标网站的反爬强度和你自己的网速调整。项目里用的是静态请求没有引入 selenium因为列表页大多不需要复杂渲染静态解析更轻量。2.2 跑通爬虫的最短路径依赖、请求头与延时先把环境配齐。README.docx 和 notebook 的 import 区已经写明了依赖我通常会一次性装完整pip install requests beautifulsoup4 pandas jupyter matplotlib seaborn scikit-learn装完后先在 notebook 里做一次“单页测试”不要一上来就开 100 页。字段对上了再放开翻页。常见做法是单独拉一个 cell 做冒烟测试html fetch_listing_page(1) rows parse_listing(html) print(len(rows), rows[0] if rows else EMPTY)如果 rows 是空说明选择器已经失效加再多延时也抓不回来。解决方式很直接在浏览器开发者工具里重新看目标字段的 CSS 选择器或 xpath回填到 parse_listing 里。我把这一步叫前置检查能避掉 70% 的爬虫问题。请求头里至少要有 User-Agent。有的站点还会校验 Referer 和 Cookie遇到 403 就在 HEADERS 里补上浏览器里复制到的 Cookie。不过要注意Cookie 会过期更好的做法是先正常访问一轮把必要的 Cookie 参数化而不是硬编码一个十天后的临时值。2.3 先落 CSV 再进模型data 目录是中间层爬虫拿到的原始数据是一堆 list[dict]直接交给 pandas 转成 DataFrame再存成 CSV。这一步不经过模型却能让后面的调试效率提高一个量级。项目里专门有 data 目录这就是给 CSV 留的中间层。import pandas as pd rows crawl(max_page10) df pd.DataFrame(rows) df.to_csv(data/raw_house.csv, indexFalse, encodingutf-8-sig) print(df.shape)这里用 utf-8-sig 是为了让 Excel 打开 CSV 时不乱码。如果你在 Jupyter 里继续做分析建议从这里开始单独开一个 cell只做 read_csv不要再重新抓取。数据一旦固化后面每一步都不需要重新访问目标网站也降低被封风险。sol.ipynb 里后面的操作基本就是基于这份 CSV 展开的。3. 清洗与 EDA房价分析里最容易翻车的两个环节3.1 缺失值与异常值先看分布再动手把 CSV 读进来后第一件事不是建模而是看 df.info() 和 df.isnull().sum()。二手房字段里最容易缺的是“小区”“户型”“楼层”这类信息型字段而价格、面积一般比较完整。import pandas as pd df pd.read_csv(data/raw_house.csv, encodingutf-8-sig) print(df.info()) print(df.isnull().sum())逐字段看缺失比例。我的处理原则是缺失率低于 5% 的字段直接删除对应行5% 到 20% 的字段用中位数或众数填充超过 20% 的字段要重新评估它可能不适合进模型。这一点在二手房场景里尤其关键因为“满五”“配套”这类信息缺失并不代表不存在强行填充反而会引入噪音。缺失值处理完就要看异常值。价格、面积这类连续变量最容易出现录入错误比如总价 1 万以下、面积 1 平方米。先转成数值类型import pandas as pd import numpy as np df[总价] pd.to_numeric(df[总价].astype(str).str.replace(,, ), errorscoerce) df[单价] pd.to_numeric(df[单价].astype(str).str.replace(元/平, ), errorscoerce) df[面积] pd.to_numeric(df[面积].astype(str).str.replace(平米, ), errorscoerce) print(df.describe())只看 describe 还不够。我一般先看 1% 和 99% 分位数再过滤掉明显不合理的区间。下面的边界不是固定的不同城市、不同房源类型的差异很大df df[(df[面积] 5) (df[面积] 300)] df df[(df[单价] 2000) (df[单价] 200_000)]如果你用的是链家一线城市数据5000 到 15 万每平是比较常见的区间。建议先跑一下 df[单价].quantile([0.01, 0.99])再决定上下界不要直接套死数字。3.2 EDA 可视化价格和哪些字段同涨同跌数据清洗完进入探索性数据分析。项目里用 pandas 和可视化库把数据特征铺开来看这一步的价值是帮你找到“哪些特征值得进模型”。先看总价的分布import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(8, 4)) sns.histplot(df[总价], bins50) plt.yscale(log) plt.show() numeric_cols df.select_dtypes(include[np.number]).columns plt.figure(figsize(10, 8)) sns.heatmap(df[numeric_cols].corr(), annotTrue, cmapcoolwarm, fmt.2f) plt.show()对数 y 轴是为了让右偏的分布看得更清楚。热力图则能一眼看到“面积”和“总价”高度正相关而“楼层”这类字段与价格的相关性往往弱得多。EDA 不是看图就结束了更有效的做法是每发现一个相关性就把它变成候选特征。比如“总价 / 面积”就是单价“区域 户型”的组合可以放进后续模型。项目里后面用到的特征工程很多思路就是从这几张图里来的。3.3 时间维度上的房价走势简单的时间序列观察摘要有提到时间序列分析这一点在二手房场景里通常落地为“挂牌时间 / 成交时间”的月度均价走势。如果你的数据里有挂牌时间字段直接提取月份并按月份聚合。df[挂牌月份] pd.to_datetime(df[挂牌时间]).dt.to_period(M) monthly df.groupby(挂牌月份)[单价].mean().reset_index() print(monthly.head())数据量够的话可以用 three-period moving average 去掉单月波动monthly[单价_ma3] monthly[单价].rolling(3).mean()这里做的只是趋势观察不是严格意义上的时间序列预测模型。对课设和毕设来说能把月份趋势画出来已经足够体现时间序列分析这部分了。更重要的是在后面建模时你最好意识到时间字段不是普通数值这一点第 5 章会专门说。4. 线性回归 vs 决策树 vs 随机森林用交叉验证选出能用的预测模型4.1 特征编码与训练集构造模型训练之前要把文本类特征变成数值。常见做法是使用 pandas 的 get_dummies 做 one-hot 编码或者对有序字段用 LabelEncoder。Scikit-learn 本身不接受字符串所以这一步躲不掉。from sklearn.model_selection import train_test_split feature_cols [面积, 户型, 朝向, 楼层, 区域, 挂牌月份] df_model df.dropna(subsetfeature_cols [总价]) X df_model[feature_cols] y df_model[总价] X_encoded pd.get_dummies(X, columns[户型, 朝向, 楼层, 区域], drop_firstTrue) X_train, X_test, y_train, y_test train_test_split( X_encoded, y, test_size0.2, random_state42 ) print(X_train.shape, y_train.shape)这里的 drop_firstTrue 是为了去除 one-hot 后的冗余列避免线性回归出现多重共线性。random_state 固定成 42保证你每次跑出来的结果可复现这也是做课设时最容易忽略的细节。严格说“挂牌月份”不应该直接当数值特征因为它是有循环周期的月份编号。稳妥的做法是把它转成“一年中的第几个月”或者单独做 one-hot。第 5 章我会把这个问题当重点踩一遍。4.2 三个回归模型的一次对齐项目里同时使用了线性回归、决策树回归和随机森林回归。先各自用默认参数跑一轮用 RMSE 和 R² 对比这一步叫基线对齐。from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score models { LinearRegression: LinearRegression(), DecisionTree: DecisionTreeRegressor(random_state42), RandomForest: RandomForestRegressor(n_estimators100, random_state42), } results [] for name, model in models.items(): model.fit(X_train, y_train) pred model.predict(X_test) rmse mean_squared_error(y_test, pred) ** 0.5 r2 r2_score(y_test, pred) results.append((name, rmse, r2)) for name, rmse, r2 in results: print(f{name:18s} RMSE{rmse:.2f} R2{r2:.3f})逻辑说明把三个模型塞进同一个循环输出统一格式的评估指标这是项目里很实用的做法。RMSE 反映误差的实际量级R² 反映模型解释了多少房价方差。对二手房这个场景RMSE 比 R² 更有参考价值因为 R² 很高不代表你预测的单价误差小。随机森林默认用 100 棵树稳定性明显优于单棵决策树。决策树如果不限制深度几乎一定会过拟合线性回归又太简单对非线性关系无能为力。三者对比的价值就在这里你能直观看到“复杂度上来了误差降了多少”。4.3 GridSearchCV 调参把随机森林的潜力挤出来项目摘要里提到了交叉验证优化模型参数。最常见的是 GridSearchCV把随机森林的主要参数做一个小范围搜索。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], max_depth: [None, 10, 20], min_samples_split: [2, 5], max_features: [sqrt, log2], } grid GridSearchCV( RandomForestRegressor(random_state42), param_grid, scoringneg_root_mean_squared_error, cv5, n_jobs-1, verbose1, ) grid.fit(X_train, y_train) print(grid.best_params_) print(-grid.best_score_)参数说明n_estimators 从 100 起步就够用1000 会让组合数爆炸max_depth 用一个有限值列表去试None 表示不限制min_samples_split 控制分裂所需的最小样本数越大越不容易过拟合max_features 控制每次分裂看几个特征常用 sqrt 和 log2。不要一上来就跑全网格。以小组合先找到大方向再在最优值附近做第二轮。如果你在 Jupyter 里同时打开了很多 celln_jobs 可以调成 4别把 CPU 全占满。GridSearchCV 的 scoring 参数我习惯用负 RMSE因为在 sklearn 里“分数越高越好”负 RMSE 越大代表误差越小。5. 避坑指南跑二手房预测项目常见的五个翻车点5.1 爬虫抓回来全是空数据现象爬虫跑完DataFrame 是空的或者字段全是 NaN。原因最常见的是 CSS 选择器已经失效其次是没有带 User-Agent被目标网站拒绝。解决先做单页调试打印页面前 1000 个字符确认返回的是 HTML 而不是验证码或错误页。然后重新打开开发者工具核对选择器。选择器没调对增加延时也救不回来。项目里链家爬虫的选择器是基于当时页面结构写的等你下载使用时很可能需要自己修正一次。5.2 中文列名让 sklearn 直接报错现象fit 的时候报ValueError: could not convert string to float: 东城。原因不是中文列名本身的问题而是列里还有未编码的字符串值。你只处理了数字列忘记把“区域”“户型”等文本列做编码。解决把所有非数值特征统一收集起来一次性用 pd.get_dummies 处理。处理完检查dtypes确保全部是数值类型。还有个隐蔽点X_train 和 X_test 的列顺序必须一致如果编码时用了两次 get_dummies列顺序很容易错位。5.3 模型 R² 高得离谱先查数据泄露现象R² 能到 0.98看起来特别完美但换一条新的数据来预测结果一塌糊涂。原因特征里混进了目标变量的直接派生字段。最常见的是预测“总价”却把“单价”也放进特征里或者在 train_test_split 之前用全量数据做标准化把测试集的统计信息提前泄漏给了模型。解决检查特征列里有没有和 y 线性相关的派生列。标准化的正确顺序是先切分训练集和测试集再在训练集上 fit用同一个 transform 处理测试集。Scikit-learn 的 Pipeline 能帮你固定这个顺序建议直接用它。5.4 GridSearchCV 慢到怀疑人生现象一个参数组合跑了十几分钟notebook 卡到没法继续。原因参数网格太大数据量又没控制。比如 n_estimators 给到 [300, 500]max_depth 再给四五个值组合数直接翻了几倍。解决先用 5000 到 10000 条样本做一轮粗筛把 n_estimators 控制在 100 和 200max_depth 控制在两三个值。如果搜索空间还是很大就用 RandomizedSearchCV在参数分布里随机采样效率高得多。项目里用的是完整网格搜索适合小数据集你复现时如果数据量大这一步一定要缩。5.5 把挂牌时间当成普通数值特征现象训练集误差很小但按时间顺序验证时误差突然变大甚至出现跳跃。原因月份是循环周期变量12 和 1 在数值上相差 11但实际只差一个月。直接把“挂牌月份”塞进线性模型模型会学到一段并不存在的线性趋势。解决把月份拆成“一年中的第几个月”的多列 one-hot或者把年份和月份分开处理。如果数据带成交时间还必须有时间顺序意识训练集应该是时间靠前的数据测试集是时间靠后的数据不能用 train_test_split 随机打乱。这个坑最隐蔽也是摘要有提时间序列分析的原因。6. 从预测到结论用特征重要性和模型保存收好整条链路6.1 老规矩先看训练与测试误差的差距调完参不要直接拿测试集 R² 写结论。先把训练集和测试集 RMSE 放在一起对比。如果训练集远低于测试集说明模型过拟合了。pred_train grid.best_estimator_.predict(X_train) pred_test grid.best_estimator_.predict(X_test) train_rmse mean_squared_error(y_train, pred_train) ** 0.5 test_rmse mean_squared_error(y_test, pred_test) ** 0.5 print(ftrain RMSE{train_rmse:.2f}, test RMSE{test_rmse:.2f})经验值两者差距控制在 10% 到 20% 以内。如果超过 30%先回退 max_depth随机森林深度太大是过拟合的主要来源。这个检查我每次都会做比单看某个指标靠谱得多。6.2 把模型存下来给下一次预测留后门模型验证完最容易被忽略的是保存。课设答辩或项目展示时现场重新训练十几分钟不是好体验提前保存模型是更专业的方式。import joblib joblib.dump(grid.best_estimator_, models/house_price_rf.pkl) joblib.dump(X_train.columns.tolist(), models/feature_columns.pkl) loaded_model joblib.load(models/house_price_rf.pkl) loaded_columns joblib.load(models/feature_columns.pkl)这里要连列名一起保存。原因很实际预测新数据时你必须保证输入的特征列顺序和训练时完全一致。保存列名之后遇到缺列可以手动补零遇到多出来的列就删掉。6.3 用特征重要性给业务结果一个解释最后随机森林自带 feature_importances_这一步是给整个项目加分的关键。import pandas as pd importance pd.Series( grid.best_estimator_.feature_importances_, indexX_train.columns ).sort_values(ascendingFalse) print(importance.head(10))对二手房这个场景面积、区域通常会排在前面朝向和装修的重要性一般靠后。如果前几名里出现了不合逻辑的特征回头检查是否清洗不干净。这一步能让你的分析报告从“我调了一个模型”变成“我知道了什么在真正拉动房价”。拆这份项目的时候我把模型重复训练了三次每次换一个 random_state最终误差没有明显波动才算放心。从那以后我每次做预测类项目都会强制走一遍训练/测试误差对比、模型保存、特征重要性检查缺一不可。这份源码包把链路搭得很稳希望你也能顺着它跑通自己的第一个房价预测项目。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。