简介:一套基于机器学习实现的房价与二手房价格预测综合项目,适合计算机相关专业学生用作大作业、课程设计或毕业设计参考。项目已经过导师指导与评审,获得98分的高分,全部源码均在本地编译并调试通过,确保可运行。资源包共26个文件,压缩包大小约1.28MB,包含15个Python脚本(含爬虫采集、特征分析与模型训练模块)、2个CSV数据集(链家与安居客二手房数据)、1个Jupyter Notebook分析文档、4张可视化结果图、HTML报告及说明文档等,能够覆盖数据获取、清洗、特征工程、模型对比与结果展示的完整流程。以北京二手房数据为案例,实验涉及线性回归、决策树、随机森林、支持向量机等常见算法,方便读者直观理解不同模型在房价预测中的效果与适用场景。目前已有58人浏览学习,难度适中,既适合新手入门,也适合有一定基础者深入实践,是集数据、源码和说明于一体的实战型资源。
1. 房价预测不是玄学,是特征工程与模型选择的组合题
只给出一条成交记录,任何人都说不准这套房子值多少钱;但把北京几万条二手房成交记录放在一起,机器学习就能从中找到“面积、地段、房龄、地铁距离”与房价之间的非线性映射。这份人工智能大作业把这件事完整做完:它自带链家与安居客两个来源的二手房数据集、基于 Scrapy 的采集脚本、一份可逐格运行的北京二手房房价预测与分析 Notebook,以及可视化输出。对准备交机器学习课程设计、人工智能大作业,或者想拿真实数据练手的学生来说,这套资源的价值在于它不只给了模型代码,还演示了从爬虫清洗、特征构造到多算法对比的完整链路,且报告梳理由导师认可过。
2. 从爬虫到数据清洗:搞懂 lianjia.csv 与 anjuke.csv 的质量控制
拿到项目压缩包后,先别急着开 Notebook。压包里有一组spiders目录,内含lianjia_scrapy_crawl与anjuke_scrapy_crawl两个 Scrapy 工程。这份大作业的数据不是凭空给的,而是先通过爬虫采集,再落成lianjia.csv与anjuke.csv两份数据源。理解采集脚本中对字段和反爬的取舍,后面做特征工程时才不会把脏数据带进模型。
2.1 采集脚本的定位与合规边界
lianjia_scrapy_crawl与anjuke_scrapy_crawl是两个独立 Scrapy 项目,典型启动方式是:
scrapy crawl lianjia -o ../lianjia.csv scrapy crawl anjuke -o ../anjuke.csv-o参数把 Item 导出为 CSV,文件名对应根目录下的lianjia.csv、anjuke.csv。每个 Scrapy Item 里定义的字段就是后续 DataFrame 的列名,例如小区名称、所在区域、户型、面积、朝向、总价、单价、建成年代。采集脚本一般会设置DOWNLOAD_DELAY = 1.0之类的下载延迟,避免请求频率过高。常见做法还有配置USER_AGENT模拟浏览器标识,以及用RandomUserAgentMiddleware随机切换 UA。需要说明的是,这份资源里的爬虫定位是教学演示与一次性数据集构建,实际使用时应遵守目标网站的 robots 协议,控制请求频率,仅采集用于学术分析的公开信息。
2.2 多源数据字段对齐与去重策略
两个网站的字段命名不完全一致:链家倾向用“建筑面积”“参考首付”,安居客则有“每平米价格”“室厅卫结构”。合并前要先把两份 CSV 统一成同一套列名。常见做法是在 Notebook 里建一个字段映射字典:
import pandas as pd lianjia = pd.read_csv('lianjia.csv') anjuke = pd.read_csv('anjuke.csv') # 字段统一映射,保留可用于回归建模的数值列 col_map_aj = { '小区': 'community', '区域': 'district', '户型': 'house_type', '面积': 'area', '单价': 'unit_price', '总价': 'total_price', '朝向': 'orientation', '楼层': 'floor', '年代': 'build_year' } anjuke = anjuke.rename(columns=col_map_aj) # 按小区+面积+总价去重,保留各网站最新一条记录 df = pd.concat([lianjia, anjuke], ignore_index=True) df = df.drop_duplicates(subset=['community', 'area', 'total_price'], keep='first') print(df.shape)这里drop_duplicates的subset是去重判据,选取“小区+面积+总价”是因为这三者足够唯一,可避免同一套房在链家和安居客被重复抓取。注意keep='first'保留先出现的记录,如果担心先出现的数据有缺失,也可以改成keep='last'再对比。
2.3 脏数据识别与清洗
真实抓下来的数据远比教材里的iris数据集脏。我在这个项目里看到的常见问题有三类:面积缺失、总价单位不统一、楼层字段写成了“低楼层/高楼层”而不是数字化楼层。初步清洗代码可以这样写:
# 过滤明显异常样本 df = df[(df['area'] > 5) & (df['area'] < 500)] df = df[(df['total_price'] > 10) & (df['total_price'] < 10000)] # 计算单价并检查偏离度 df['unit_price_calc'] = df['total_price'] * 10000 / df['area'] df['price_diff'] = abs(df['unit_price_calc'] - df['unit_price']) / df['unit_price'] df = df[df['price_diff'] < 0.3] # 缺失值处理:区域和户型直接删除,数值列用中位数填充 df = df.dropna(subset=['district', 'house_type']) df['build_year'] = df['build_year'].fillna(df['build_year'].median())total_price单位通常是万元,area单位是平方米,所以total_price * 10000 / area得到每平方米单价;price_diff过滤掉计算单价与页面单价偏差超过 30% 的记录,这类偏差往往是录入错误或包含赠送面积导致。中位数填充比均值填充更抗异常值,这是处理房龄这类偏态分布特征的常用做法。
3. 特征工程:把“地段”翻译成模型能学习的数字
房价预测这类回归任务,决定了结果上限的往往是特征工程而非模型选择。同一个区域里,面积、朝向、楼层、装修这些变量组合起来才能形成完整的定价逻辑。这一章重点说明这份项目里最容易被忽略、但对分数影响最大的一步:如何把中文类别文本转成数值特征,并构造出模型真正依赖的衍生变量。
3.1 类别特征编码:区域用 One-Hot,户型标签化
北京二手房数据里,“区域”这个字段是强特征——海淀和延庆的均价差出一倍不止。处理区域时,如果直接做LabelEncoder,模型会误认为“海淀(3) > 朝阳(2) > 昌平(1)”存在序号上的大小关系,这显然不符合语义。安全做法是对区域做 One-Hot 编码:
from sklearn.preprocessing import OneHotEncoder df = df.reset_index(drop=True) district_encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False) district_oh = district_encoder.fit_transform(df[['district']]) district_df = pd.DataFrame( district_oh, columns=district_encoder.get_feature_names_out(['district']) ) # 原项目里此处往往再拼上其他数值列 df_feat = pd.concat([df[['area', 'build_year', 'total_price']], district_df], axis=1)handle_unknown='ignore'这个参数很关键:训练时没有出现过的区域,预测时会被编码成全零向量,而不是报错。模型上线或做交叉验证时,测试集里完全可能冒出一个新区块,ignore模式能让管线稳定运行。户型这类字段则不同,它的取值是“2室1厅”“3室2厅”这种计数型描述,用LabelEncoder把它转成一个递增整数,相当于保留了“房间数量越多数值越大”的有序关系,树模型下表现通常不错。
3.2 连续特征的衍生:房龄、均价与交互项
原始数据里的“建成年代”是一个绝对年份,比如 2005、2012。模型需要学习的是“房子新旧程度”对价格的影响,直接喂年份等于让模型假设 1980 年建成的房子和 2020 年建成的房子只差 40 个单位。把这个信息转换成相对值更合理:
# 以2020年为基准房龄 df['house_age'] = 2020 - df['build_year'] # 均价 = 总价 / 面积,排除录入误差后的稳定化特征 df['unit_price'] = df['total_price'] * 10000 / df['area'] # 面积与总价的交互特征 df['area_zongjia'] = df['area'] * (df['total_price'] * 10000 / df['area'])注意第三行area_zongjia在数学上直接约等于总价,这个特征单独放进去会让模型产生严重的多重共线性。我在实际复现时会去掉这种恒等变形,改成area_square = df['area'] ** 2——面积对总价的影响往往不是线性的,90 平米的房子总价比 45 平米高出不止 2 倍,平方项能给树模型更多切分空间。
3.3 用相关性矩阵确认特征有效性
做特征工程后,先跑一遍相关性检验,能快速发现冗余特征与目标变量关联过弱的问题:
import seaborn as sns import matplotlib.pyplot as plt corr_cols = ['area', 'house_age', 'unit_price', 'total_price', 'area_square'] plt.figure(figsize=(8, 6)) sns.heatmap(df[corr_cols].corr(), annot=True, fmt='.2f', cmap='coolwarm') plt.title('Feature Correlation Matrix') plt.tight_layout() plt.savefig('ershoufang_0.JPG', dpi=150)生成ershoufang_0.JPG这类图片就是压包里那几张 JPG 的来源。如果你自己跑这套代码,注意house_age与build_year的相关性一定接近 -1,说明它们表达的信息重复,保留一个即可;area与total_price的相关系数通常在 0.6 到 0.8 之间,属于强正相关,这正是最终回归模型的主干特征。visuals.py里封装的散点图、直方图函数,本质上就是为这些诊断检查服务的。
4. 多算法对比实验:从线性回归到随机森林再到 SVR
项目说明里明确提到了线性回归、决策树、随机森林和 SVM 四种算法。把它们放在同一份数据上做横向对比,是这份人工智能大作业最有教学密度的地方,也是答辩时导师最爱追问的部分。不同算法的适用前提差异很大,这一步选型和参数配置的合理性,比最终 R² 数字本身更值得写进报告。
4.1 训练集与测试集切分:分层抽样
房价数据天然带有区域结构性,简单train_test_split可能让测试集里某个区域占比失衡。常见做法是按区域做分层切分:
from sklearn.model_selection import train_test_split y = df_feat['total_price'] X = df_feat.drop(columns=['total_price']) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=df['district'] if 'district' in X.columns else None )random_state=42保证结果可复现,stratify让训练集和测试集中各区域的比例与原始数据一致。这里有个细节:如果district已经做了 One-Hot 变成多列,df['district']仍在原框里可以取出,分层切分的分组变量要用原始类别值,不能传编码后的矩阵。
4.2 四种模型训练与超参数基准
from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import r2_score, mean_squared_error import numpy as np models = { 'LinearRegression': LinearRegression(), 'DecisionTree': DecisionTreeRegressor(max_depth=8, min_samples_leaf=5), 'RandomForest': RandomForestRegressor(n_estimators=200, max_depth=12, min_samples_leaf=2, n_jobs=-1), 'SVR': make_pipeline(StandardScaler(), SVR(kernel='rbf', C=100, epsilon=0.1)), } for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) r2 = r2_score(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f'{name:20s} R2={r2:.4f} RMSE={rmse:.4f}')四个模型的参数设定各有讲究。DecisionTreeRegressor(max_depth=8, min_samples_leaf=5)抑制了决策树无限生长导致过拟合;RandomForestRegressor(n_estimators=200, max_depth=12)里n_estimators=200足够让 Bagging 发挥方差削减作用,再增大收益递减;SVR必须包在StandardScaler里,因为 RBF 核依赖样本距离尺度,面积动辄几百、房龄只有个位数,不标准化会让面积特征主导核函数距离,这是李宏毅机器学习课程里反复强调过的预处理原则。最终对比结果往往随机森林表现优于线性回归,这是合理的:房价与面积、位置的关系有显著非线性,随机森林又能通过min_samples_leaf控制叶节点纯度,天然携带正则化。
4.3 结果观察与训练诊断
| 模型 | R² | RMSE | 结论 |
|---|---|---|---|
| 线性回归 | 0.68 左右 | 偏高 | 只能抓住面积与总价的线性趋势,说明特征中存在非线性和交互 |
| 决策树 | 0.79 左右 | 中等 | 能捕捉非线性,但单棵树方差大,噪声敏感 |
| 随机森林 | 0.86 左右 | 最低 | 树集成降低方差,综合效果最好 |
| SVR(RBF) | 0.73 左右 | 中等 | 对参数很敏感,未充分调参时不如树模型 |
注意上表是此类数据集下的典型示意值,具体数字以你本地跑出来的结果为准。判读结果时还要关注一个隐藏信号:如果随机森林在训练集上 R² 高达 0.97 而测试集只有 0.86,说明模型仍有过拟合压榨空间,可以继续降低max_depth或者上调min_samples_leaf;如果线性回归的训练与测试 R² 都低于 0.7,这个数据集的非线性结构太强,需要回到特征工程章节检查是否有重要交互项遗漏。
5. 用特征重要性反推定价逻辑
模型训练完毕不是项目的终点。答辩和报告里,导师基本都会问一句:“哪些特征对房价影响最大?你的模型依据什么做判断?”针对树模型,随机森林自带特征重要性,能给出定量回答。获取方式如下:
rf_model = models['RandomForest'] importances = rf_model.feature_importances_ feat_names = X_train.columns # 按重要性降序输出特征排名 for name, imp in sorted(zip(feat_names, importances), key=lambda x: x[1], reverse=True)[:8]: print(f'{name}: {imp:.4f}') # 保存模型与区域编码器,供后续预测复用 import joblib joblib.dump(rf_model, 'housing_price_rf.pkl') joblib.dump(district_encoder, 'district_encoder.pkl')特征重要性本质是随机森林中每个特征在所有决策树节点上带来的不纯度下降总和,经过归一化后得到。一个值得说明的现象:house_age的重要性在多数区域会排进前三,说明北京二手房市场对房龄的敏感度甚至超过某些朝向因素;小区所在区域的特征重要性合计通常最高,这与现实里“地段决定房价”的直觉一致。
实际预测新数据时,容易踩到与训练管线脱节的坑。比如新样本的district值要先进district_encoder.transform(),One-Hot 编码后的列顺序必须与训练时完全一致;连续特征要做同样的缺失值填充处理。joblib同时保存模型和编码器,就是为规避这类顺序错位问题。另一个需要注意的边界是模型外推能力:训练数据覆盖的是 2020 年之前的北京二手房价格区间,如果拿 2024 年的学区房政策落地后的市场价来做预测,误差会显著放大,这不是模型结构的问题,而是特征空间之外的新变量在起作用。做价格预估演示时,建议限定在训练数据同分布范围内,并用feature_importances_的结果向提问者解释预测依据是单价、面积、区位和房龄的组合,而不是单一变量决定。
本文还有配套的精品资源,点击获取