ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

二手房房价预测实战:从Scrapy爬虫到随机森林的完整数据管道

二手房房价预测实战:从Scrapy爬虫到随机森林的完整数据管道 简介一份面向毕业设计、期末大作业与课程设计的机器学习房价预测完整方案覆盖北京二手房房价预测场景含数据集、Python项目源码及文档资料适合需要快速落地项目并获取高分参考的初学者和高校学生。资源压缩包共26个文件约1.29MB以15个Python脚本为核心包含数据爬取、特征处理与模型训练等模块另配CSV房价数据集、Jupyter Notebook分析演示、可视化结果图片以及README说明文档结构清晰便于按需查看。代码中附有详细注释即使新手也能理解关键逻辑配套文档与可复现的分析流程可帮助用户理解房价预测从数据采集到结果展示的完整链路整体可复现性较强。已有161人浏览学习具备一定的参考价值适合作为毕业设计、期末大作业或课程设计的高分项目范本。1. 二手房价预测不是模型题是数据流水线题拿到一份标注“98分”的机器学习大作业源码先别急着看模型用了哪个算法。真正值钱的不是某个神秘参数而是它把数据闭环串起来了链家和安居客两套二手房数据、Scrapy 爬虫、CSV 落盘、特征工程、回归模型、可视化出图最后到 HTML 报告。在人工智能大作业和课程设计里导师看重的往往是“数据从哪来、怎么清洗、为什么选这个模型”而不是 R² 是不是多了小数点后一位。这份项目正好补齐整条链路适合要做房价预测、机器学习大作业又不想从零攒代码的人。下面的章节我会顺着源码里出现的文件把采集、建模、可视化、复现几个环节拆开讲。2. 数据集采集与字段对齐lianjia.csv 和 anjuke.csv 背后的 Scrapy 工程这份资源里最有信息量的文件是 lianjia.csv 和 anjuke.csv。它们不是随手导出的表格而是由 spiders 目录下两套爬虫分别抓取后落盘的。先把数据来源讲清楚后面建模才有据可依。2.1 两个爬虫目录lianjia_scrapy_crawl 与 anjuke_scrapy_crawl项目文件里能看到 lianjia_scrapy_crawl 和 anjuke_scrapy_crawl 两个目录命名虽长其实就是链家和安居客两个爬虫工程的根目录。Scrapy 工程的常见结构是 items.py 定义字段pipelines.py 做清洗和去重spiders/ 放爬虫本体settings.py 控制下载延迟和并发。解压后看到的目录结构接近下面这样lianjia_scrapy_crawl/ ├── items.py ├── pipelines.py ├── settings.py └── spiders/ └── lianjia.pyitems.py 里一般定义一个 HouseItem把每条房源映射成字典结构。爬虫逻辑则写在 lianjia.py 的 Spider 类里。下面这段是常见的链家二手房抓取片段和你项目里 runs 的方式类似直接通过 start_requests 翻页# spiders/lianjia.py import scrapy from ..items import HouseItem class LianjiaSpider(scrapy.Spider): name lianjia def start_requests(self): for page in range(1, 6): yield scrapy.Request( urlfhttps://bj.lianjia.com/ershoufang/pg{page}/, callbackself.parse, ) def parse(self, response): for li in response.css(.sellListContent li.clear): item HouseItem() item[title] li.css(.title a::text).get() item[total_price] li.css(.totalPrice span::text).get() item[unit_price] li.css(.unitPrice span::text).get() yield item这里的start_requests是 Scrapy 的入口方法f...拼出分页 URLcallback指定解析函数。li.css(...)用 CSS 选择器从列表页抽房源卡片::text取标签内文本。yield item会交给 pipelines 继续处理而不是直接 return因为后续可能还要去重、补字段或落库。需要提醒的是平台页面改版后这些选择器很容易失效爬虫报空结果时通常要从这里查起。2.2 链家和安居客字段差异为什么不能直接 concat很多新手拿到两份数据后第一件事就是pd.concat结果列对不上、单位乱套。链家字段里常见“所在区”“小区名称”安居客更偏“区域”“板块”只有先统一命名再纵向拼接才能得到一张可建模的表。字段对齐是这份项目里最琐碎但也最重要的环节。合并前的映射关系大致如下统一字段lianjia.csv 来源字段anjuke.csv 来源字段清洗动作district所在区区域去空格、替换分隔符layout房屋户型户型统一成“室/厅”格式area建筑面积面积去掉“㎡”转 floattotal_price总价(万元)总价转 floatunit_price单价(元/㎡)单价去单位转 int统一字段名后写一个通用的读取函数把字符串和单位问题一次性消化掉。项目里 lianjia.csv 和 anjuke.csv 都能套用同一套逻辑。2.3 CSV 读取与数值化把脏字符串变成可计算数字房源数据常见的坑是“93.4㎡”“暂无数据”这类混合文本。我一般会先把列转成字符串再做替换和强制类型转换import pandas as pd def load_house_csv(path): df pd.read_csv(path) df[area] df[area].astype(str).str.replace(㎡, ).str.strip() df[area] pd.to_numeric(df[area], errorscoerce) df[total_price] pd.to_numeric(df[total_price], errorscoerce) df[unit_price] pd.to_numeric(df[unit_price], errorscoerce) return df.dropna(subset[area, unit_price]) df_lj load_house_csv(lianjia.csv) df_aj load_house_csv(anjuke.csv) df pd.concat([df_lj, df_aj], ignore_indexTrue, joininner).drop_duplicates()这段代码里astype(str)是为了避免某些行是数字或 NaN 时直接调用字符串方法报错errorscoerce把“暂无数据”这类非法值统一变成 NaN方便后续 dropna 过滤。pd.concat的joininner只保留两份数据都有的列drop_duplicates()去掉完全重复的挂牌记录。提示如果只用默认参数直接读取 CSV单价列很可能变成 object 类型模型训练时 sklearn 会直接报“could not convert string to float”。先做类型清洗比后面补洞省事得多。3. 特征工程从地址、户型、楼龄里挤出可学习的信号机器学习项目里特征工程往往比模型选择更影响分数上限。二手房数据看似有十几个字段但可以直接喂给模型的几乎没有户型是“2室1厅”这样的文本朝向是中文面积和价格混着单位。这章按房价预测的常规流程把原始字段变成数值特征并解释每个特征的用途。3.1 目标变量选单价还是总价做房价预测前先回答一个问题预测二手房总价还是预测单价项目里 lianjia.csv 同时给出 total_price 和 unit_price多数课程设计用单价做目标更合理。因为总价由面积和单价共同决定预测总价等价于同时猜测两个变量误差会更大单价剔除了面积因素更能反映地段和房屋本身的价值。可以用一句简单计算验证两份数据的单位是否一致df[unit_price_check] df[total_price] * 10000 / df[area] diff (df[unit_price_check] - df[unit_price]).abs() print(diff.describe())如果diff的中位数接近 0说明总价、面积、单价三个字段是自洽的如果出现大量几十万的偏差说明某些行来自不同口径需要回看 2.3 节的清洗逻辑。后续训练时把unit_price作为 y把 total_price 排除在特征外避免信息泄漏。3.2 文本特征的正则提取与编码户型字段“2室1厅1卫”若直接丢掉很可惜。这里的结构化信息包括居室数和厅数完全可以用正则单独抽出来放进模型里作为数值特征。项目里常见的处理方式是这样df[layout_room] df[layout].str.extract(r(\d)室).astype(float) df[layout_hall] df[layout].str.extract(r(\d)厅).astype(float) df[district_code] df[district].astype(category).cat.codes第一行中的r(\d)室会从左到右找第一个“数字室”的组合并返回括号内的数字例如“2室1厅”抽取成 2.0。astype(float)是为了兼容缺失值 NaN后续模型中可以保留 NaN 或填充为 0。district是行政区用cat.codes转成类别编码这种方法比手动 map 更加省事且能自动处理字典中不存在的类别。3.3 类别变量 one-hot 与特征列表行政区用整数编码还能接受朝向和装修这种无序类别就不适合用 0、1、2 表示因为模型会误以为 321。常见做法是 one-hot 展开也就是 sklearn 里的OneHotEncoder或 pandas 的get_dummiesdf[decoration] df[decoration].fillna(未知) df pd.concat( [df, pd.get_dummies(df[decoration], prefixdecoration, drop_firstTrue)], axis1, )prefix会给生成的列加前缀避免和原始列重名drop_firstTrue去掉第一列防止和截距项产生多重共线性。经过几步处理后特征集合大概长这样特征名类型处理方式area数值float面积layout_room数值正则提取缺失填 0layout_hall数值正则提取缺失填 0district_code类别编码行政区转整数decoration_精装二值one-hotdecoration_简装二值one-hotunit_price目标变量不进入特征矩阵把特征和标签分开时注意 DataFrame 的索引对齐feature_cols [ area, layout_room, layout_hall, district_code, decoration_精装, decoration_简装, ] X df[feature_cols] y df[unit_price]到这里原始 CSV 已经变成 sklearn 可以直接接受的数值矩阵。X是特征y是目标顺序错了模型训练时会报维度不匹配这是最常见的低级错误。4. 房价预测模型从线性回归到随机森林的对照实验特征工程完成后模型部分反而简单。源码里的 Notebook 通常先跑一个线性回归打底再用树模型提升效果。这样做不仅能拿到分数还能在报告里写出“模型对比”的完整理由。4.1 数据集划分与评价基准任何监督学习实验都要先切分训练集和测试集。项目里保留 20% 样本做测试并用固定随机种子保证可复现from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleTrue )test_size0.2是常见比例数据量约几千条时留 20% 既能评价模型又不会让训练样本太少。random_state42固定随机过程方便你和导师复现同一组结果shuffleTrue打乱样本顺序避免按城区排列带来的偶然性。4.2 线性回归基线模型线性回归虽然简单但能告诉你好坏的下限。代码就几行from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model_lr LinearRegression() model_lr.fit(X_train, y_train) y_pred_lr model_lr.predict(X_test) mae_lr mean_absolute_error(y_test, y_pred_lr) rmse_lr mean_squared_error(y_test, y_pred_lr, squaredFalse) r2_lr r2_score(y_test, y_pred_lr) print(fLR MAE{mae_lr:.2f}, RMSE{rmse_lr:.2f}, R2{r2_lr:.4f})线性回归对异常值敏感房价数据里存在极贵房源所以 RMSE 通常会比 MAE 大很多。如果你的数据集里 MAE 只有几万而 RMSE 到几十万说明尾巴长、 outlier 多这时要重点看残差分布而不是只盯 R²。4.3 随机森林回归与超参数树模型处理类别特征和交互项更自然。项目里用的通常是RandomForestRegressor这套参数在二手房数据上比较稳from sklearn.ensemble import RandomForestRegressor model_rf RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf3, n_jobs-1, random_state42, ) model_rf.fit(X_train, y_train) y_pred_rf model_rf.predict(X_test)各参数含义参数作用建议范围n_estimators树的数量越大越稳定200500max_depth单棵树最大深度控制过拟合815min_samples_leaf叶节点最少样本数越大越保守25n_jobs-1使用所有 CPU 核心加速训练保持默认max_depth12会限制单棵树不要无限生长min_samples_leaf3保证每个叶子至少有 3 个样本这两项是抑制房价预测过拟合的主要旋钮。如果发现训练集 R² 接近 1.0 而测试集明显下降优先调大 min_samples_leaf而不是增加树的深度。4.4 评估指标怎么选模型对比报告里只放一个准确率不专业。回归问题通常看这三项指标含义数值偏好MAE平均绝对误差单位与房价一致越小越好RMSE平方误差开根放大极端值影响越小越好R²模型解释方差的比例越接近 1 越好实际运行后随机森林的 R² 通常比线性回归高一截MAE 也会更低。这个提升本身就是课程设计里可以写一页分析的点为什么树模型更擅长处理二手房这种非线性数据。不要只贴代码要把线性回归和随机森林的结果并排放在表格里结论更有说服力。5. 可视化与结果解读visuals.py 和 Notebook 怎么配合项目里的一组图片 ershoufang_0.JPG、ershoufang_1.JPG、ershoufang_2.JPG是 Notebook 运行后由 visuals.py 生成的输出。这个文件是可复用性最高的部分因为绘图逻辑和训练逻辑分开改数据不用动图片代码。下面顺着源码里常见函数分析。5.1 visuals.py 里的绘图函数拆分visuals.py 通常包含三类函数价格分布、分区域对比、特征重要性。以区域对比为例一个较为完整的函数会长这样import matplotlib.pyplot as plt import seaborn as sns def plot_price_by_district(df, save_pathershoufang_0.JPG): order ( df.groupby(district)[unit_price] .median() .sort_values() .index ) plt.figure(figsize(12, 6)) sns.boxplot( datadf, xdistrict, yunit_price, orderorder, paletteviridis, ) plt.xticks(rotation45) plt.xlabel(行政区) plt.ylabel(单价元/㎡) plt.tight_layout() plt.savefig(save_path, dpi150)order参数让箱线图按单价中位数升序排列图片一出来就能看到哪些区贵、哪些区便宜不需要额外读数字。paletteviridis是色盲友好的配色tight_layout()避免 x 轴文字被裁掉。save_path支持传文件名对应项目里的 ershoufang_0.JPG。5.2 在 Notebook 里调用 visuals.py在 Jupyter Notebook 中可视化模块用%run直接加载%run visuals.py df pd.read_csv(lianjia.csv) plot_price_by_district(df, save_pathershoufang_1.JPG)这里没有用 import因为 visuals.py 可能没有封装成包。%run会执行文件内所有顶层代码通常也就定义绘图函数和默认参数所以比 import 更省事。如果你要修改函数再调试记得先重启 kernel 或重新%run否则旧定义会被缓存。5.3 特征重要性图与结论提取随机森林训练之后可以接着用 visuals.py 里的函数把特征重要性画出来def plot_feature_importance(model, feature_names, save_pathershoufang_2.JPG): importance pd.Series( model.feature_importances_, indexfeature_names, ).sort_values() plt.figure(figsize(8, 5)) importance.plot.barh() plt.xlabel(Feature Importance) plt.tight_layout() plt.savefig(save_path, dpi150)这样一张图的价值在于回答导师的必问题“哪个特征对房价影响最大”如果 area 排第一、district_code 排第二说明面积和地段主导价格如果 decoration 系列排最后也正常它只说明在控制其他变量后装修这个维度解释力不足。5.4 导出 HTML 报告项目里的“北京二手房房价预测与分析.html”是 Notebook 的导出产物答辩时可以直接展示。对应命令jupyter nbconvert --to html 北京二手房房价预测与分析.ipynb执行后会在同目录生成同名 HTML 文件。用它展示有几个好处一是图表以 base64 内嵌离线也能打开二是答辩环境中没有 Python 也能浏览全部分析过程。6. 从源码包到本地复现三个最容易翻车的细节很多下载者卡在第一步解压后不知道先打开哪个文件。按源码包结构正确顺序是先看 README.md再启动 Notebook缺包时按报错装依赖。目录里主-master 下应有 ipynb、visuals.py、spiders、两个 CSV 和本说明文件。pip install pandas scikit-learn matplotlib seaborn jupyter jupyter notebook 北京二手房房价预测与分析.ipynb如果用的是 conda 环境建议先建一个新环境再装依赖避免和系统 Python 冲突。6.1 中文乱码问题在 Linux 服务器上运行 visuals.py 时plot 里的中文标题很容易变成方块。这是因为 matplotlib 默认字体没有中文字形。最直接的做法是在配置文件或代码开头指定字体import matplotlib matplotlib.rcParams[font.sans-serif] [ SimHei, WenQuanYi Zen Hei, Noto Sans CJK SC ] matplotlib.rcParams[axes.unicode_minus] False三项按顺序依次尝试系统里有哪个字体就用哪个。axes.unicode_minus False解决坐标轴负号在部分中文字体下显示成方块的问题。6.2 读取 CSV 时的编码报错lianjia.csv 和 anjuke.csv 可能分别用 utf-8 和 gbk 保存直接用 pandas 读会报 UnicodeDecodeError。遇到时改成这样df pd.read_csv(anjuke.csv, encodinggbk)如果 gbk 还报错就用encodinggbk, errorsreplace把无法解析的字符替换成占位符之后再清洗。这个坑在 Windows 导出的 CSV 里非常常见教学环境里问的频率也很高。6.3 特征泄漏为什么 R² 会突然暴涨最后一个容易忽视的问题是误把 total_price 当成特征。因为 total_price 和 unit_price 高度相关把它加进 X 后 R² 可能从 0.8 跳到 0.98看起来模型“变强了”实际上是标签信息提前泄露。复现时检查特征列是否包含价格相关字段assert total_price not in X.columns, 检测到价格特征泄漏 assert unit_price not in X.columns, 目标变量不能进入特征如果这两条 assert 失败说明数据合并时没把价格列剔除干净。特征泄漏会让答辩现场被一眼看穿也是课程设计中最高频的扣分点。把这条守好再结合 5.3 节的特征重要性图解释模型整个项目就能形成“数据采集 → 清洗 → 建模 → 验证”的完整闭环。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进