简介:面向机器学习课程期末大作业的房价预测项目包,基于Python实现二手房数据采集、清洗、建模与可视化,适合计算机相关专业学生完成大作业或项目实战。项目采用爬虫抓取链家、安居客等平台数据,配套CSV数据集与特征工程、模型训练脚本,内容经导师审核且本地运行通过,评审成绩98分,可放心参考。压缩包为zip格式,共26个文件、约1.28MB,主要包括Python源码、Jupyter分析笔记本、CSV数据集、可视化图片与使用说明。py脚本覆盖数据爬取、预处理与模型评估,ipynb提供完整分析流程,read/md文档帮助快速上手,目录结构清晰便于按模块取用。目前已有111人在线学习下载,可直接复用其中爬虫框架和建模代码来节省调试时间;同时可学习房价特征选择、回归模型训练与误差验证的完整思路,对课程设计、毕业设计或机器学习入门练习都有较高价值。
1. 机器学习大作业选房价预测:为什么这个题目最容易拿高分
房价预测几乎是每所高校机器学习课程期末大作业里出现频率最高的题目,原因很现实:数据公开、任务清晰、模型对比空间大。但多数人把这题做成“导入波士顿房价→跑个线性回归→输出RMSE”就交差了,结果分数平平。实际上,房价预测大作业的评分点从来不在于模型多深,而在于你有没有把数据处理、特征工程、模型对比、评估分析这一整条流水线走完整。这份源码包对应的就是一条完整流水线:从二手房源数据清洗开始,到特征构造、多种模型训练与对比、交叉验证评估,最后输出预测结果和可视化图表,是一份可以直接提交的课程设计作业,也适合想完整跑一遍机器学习流程的入门者。
它解决的核心问题是:你在几周内可能没时间从零写全套代码,而这份源码把繁琐的数据处理细节全部封装好,你需要的只是读懂每一段在做什么、为什么这么做,然后替换成自己的数据集跑通。适合三类人:正在赶机器学习或人工智能期末大作业的学生、想用房价数据练手 Kaggle 式流程的初学者、以及需要一份代码基线来快速验证自己想法的从业者。接下来我按实际拆项目时的顺序,把这份资源从头到尾捋一遍。
2. 数据预处理:拿到二手房数据先处理这四个脏点
2.1 缺失值不是无脑删,先分类型再决定策略
绝大多数房价数据集里的缺失值分三类:数值型字段缺失、类别型字段缺失、目标变量缺失。很多初学者一看到缺失值就dropna()清场,这在数据量大时勉强能用,但大作业答辩时老师一定会问“为什么删?删了多少?影响分布吗?”——答不上来就是扣分点。
这份源码里的处理逻辑是分层处理的。数值型字段如面积、卧室数,优先用中位数填充,而不是均值。原因很简单:房价数据里面积、总价这类字段往往右偏,个别豪宅会把均值拉得很高,用均值填充会让普通房源的预测整体偏移。中位数对偏态分布更稳健。类别型字段如朝向、装修情况,则用众数填充,缺失太多才考虑单独标记一列“是否缺失”作为特征。
import pandas as pd import numpy as np df = pd.read_csv('house_data.csv') # 数值列:中位数填充 num_cols = ['area', 'bedrooms', 'bathrooms', 'floor'] for col in num_cols: df[col] = df[col].fillna(df[col].median()) # 类别列:众数填充,并记录缺失标记 cat_cols = ['orientation', 'decoration'] for col in cat_cols: df[col + '_is_missing'] = df[col].isna().astype(int) df[col] = df[col].fillna(df[col].mode()[0])逻辑说明:先分离数值列和类别列,分别用不同策略填充,同时为类别列保留缺失标记,让模型自己学习“缺失”这个信息是否有预测力。参数说明:fillna(df[col].median())里如果你面对的数据分布接近正态,可以换成mean();mode()[0]取第一个众数,防止类别列有多个众数时 pandas 返回数组导致赋值失败。
2.2 离群点处理:房价数据里“豪宅”不是噪声
这是最容易翻车的一步。很多教程教人用 3σ 准则删离群点,直接套在房价上会把真正的豪宅删掉。比如某城市二手房源里,一套独栋别墅总价 5000 万,在普通住宅数据集里按 3σ 判断就是妥妥的离群点,但它在现实中是真实存在的样本,删了反而让模型学不到高端市场的规律。
正确做法是先看分布再做判断。源码里用的是 IQR(四分位距)检测,但只在目标变量 y 上做,而且不是删除,是截断处理——把超出边界的值压缩到边界值,而不是直接丢弃。
Q1 = df['total_price'].quantile(0.25) Q3 = df['total_price'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df['total_price'] = df['total_price'].clip(lower_bound, upper_bound)逻辑说明:quantile(0.25)和quantile(0.75)算出下四分位和上四分位,IQR衡量数据中间 50% 的散布范围。clip()把超出上下界的值强制截断到边界值,保留样本行数不变。参数说明:1.5是 IQR 法的标准系数,但对房价这种尾部极重的分布,如果你发现截断比例超过 5%,可以放宽到3.0。我一般会在截断后打印截断比例确认一下,超过 5% 就说明边界值得商榷。
3. 特征工程:让模型看到数字背后的含义
3.1 单价特征与总价特征必须分开构造
原始数据里往往同时存在总价和面积,但直接丢给模型,模型学到的可能是“面积大→总价高”这个显然的线性关系,而忽略了“单价”这个更细腻的信号。同一套房子,单价高可能意味着地段好、楼层佳、朝向优。源码里专门构造了unit_price = total_price / area作为独立特征,同时保留总价作为目标变量或辅助特征。
另外,楼层信息如果是“高/中/低”这类文本,不能直接编码成 1、2、3——模型会误认为“高楼层=3倍低楼层”。源码里做了独热编码,或者降维成“是否高层”“是否中层”两个布尔特征。朝向也是同理,“南北通透”这种优质朝向单独拆一列,而不是笼统地塞进多分类里。
df['unit_price'] = df['total_price'] / df['area'] # 楼层文本转有序数值 + 独热编码双向并行走 floor_map = {'低': 1, '中': 2, '高': 3} df['floor_num'] = df['floor'].map(floor_map) df = pd.get_dummies(df, columns=['floor'], prefix='floor')逻辑说明:unit_price是计算得到的派生特征,把总价归一化到单位面积维度,消除面积差异带来的混淆。floor_num保留楼层的顺序信息,get_dummies同时做独热编码保留类别独立性,两者并行输入模型。参数说明:prefix参数控制生成的列名前缀,避免和原列名冲突。如果你的特征列数膨胀太多,可以只保留floor_num而不用独热,树模型对有序编码容忍度较高。
3.2 时间特征从日期字符串里榨出周期信号
如果数据里有挂牌时间或成交时间,别只当成字符串丢掉。房价数据里“月份”“星期几”“是否节假日”往往暗含周期性规律——年初挂牌的房子可能因为学位房政策集中,年底挂牌可能因为房主急售。源码里把日期拆成年、月、日、星期几,以及一个“距年初天数”的周期特征。
df['deal_date'] = pd.to_datetime(df['deal_date']) df['deal_year'] = df['deal_date'].dt.year df['deal_month'] = df['deal_date'].dt.month df['deal_weekday'] = df['deal_date'].dt.weekday df['deal_dayofyear'] = df['deal_date'].dt.dayofyear逻辑说明:pd.to_datetime先把字符串统一转成 datetime 类型,避免混合格式报错。然后用.dt访问器逐层拆出时间分量。weekday返回 0-6 的整数(周一到周日),dayofyear返回 1-365 的累计天数,这两个特征能让模型捕捉“金九银十”这类季节效应。参数说明:如果你的原始数据里时间列不是标准格式,需要在to_datetime里加format='%Y%m%d'这类参数指定解析格式,否则 pandas 会按默认格式猜,碰到 2024/1/5 和 2024-01-05 混用时会报错或解析错误。
3.3 特征相关性检查:删掉冗余特征防止多重共线性
构造了这么多特征之后,必须做一次相关性检查。比如total_price和unit_price之间必然强相关,如果两个都喂给线性回归,会放大共线性问题,导致系数解释失真。源码里用皮尔逊相关系数矩阵筛查,阈值定在 0.9,超过就把其中一个特征丢弃或合并。
corr_matrix = df.corr() high_corr_pairs = [] for i in range(len(corr_matrix.columns)): for j in range(i): if abs(corr_matrix.iloc[i, j]) > 0.9: col_i = corr_matrix.columns[i] col_j = corr_matrix.columns[j] high_corr_pairs.append((col_i, col_j, corr_matrix.iloc[i, j]))逻辑说明:双重循环遍历相关系数矩阵的下三角部分(range(i)避免重复计算),找出所有相关系数绝对值超过 0.9 的特征对。corr_matrix.iloc[i, j]的值越接近 1 说明两个特征几乎携带相同信息。参数说明:0.9 这个阈值不是死的——线性模型用 0.7 就该警惕了,树模型对共线性不敏感,可以放宽到 0.95。遇到高相关对时,我一般保留和业务强相关更强的那一个,比如总价和面积强相关时保留总价,因为它是模型要预测的目标附近的信息。
4. 模型训练与对比:线性回归、岭回归、随机森林、XGBoost 怎么选
4.1 先跑一个线性基线,再逐级增加复杂度
很多初学者直接上 XGBoost,结果调参调到怀疑人生。正确的做法是先跑一个最简单的线性回归,拿到一个 RMSE 基线,然后逐步加复杂度:岭回归 → 随机森林 → XGBoost。每一步的收益都能清晰看到,答辩时你也能说清楚“我为每一步模型选择付出的理由”。
源码里训练测试集划分用的是 8:2,同时用random_state=42固定随机种子。分割时注意一点:房价数据如果按地区聚类明显,直接随机划分会让训练集和测试集包含同一小区的样本,模型“记忆”了小区特征导致评估虚高。更严谨的做法是按小区分组划分,但大作业里 8:2 随机划分通常够用,答辩时提到这个局限反而加分。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import xgboost as xgb X = df.drop('total_price', axis=1) y = df['total_price'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) models = { 'linear': LinearRegression(), 'ridge': Ridge(alpha=1.0), 'rf': RandomForestRegressor(n_estimators=200, max_depth=12, random_state=42), 'xgb': xgb.XGBRegressor(n_estimators=300, max_depth=6, learning_rate=0.05, random_state=42) } for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = mean_squared_error(y_test, y_pred, squared=False) r2 = r2_score(y_test, y_pred) print(f'{name}: RMSE={rmse:.2f}, R2={r2:.4f}')逻辑说明:把四个模型放进同一个字典里循环训练,保证所有模型使用完全相同的训练集和测试集,这样对比才公平。mean_squared_error的squared=False参数直接返回 RMSE(均方根误差),单位和房价一致,方便业务解读——比如 RMSE 是 35.6 万,说明平均预测偏差约 35.6 万元。r2_score是决定系数,越接近 1 越好。参数说明:Ridge(alpha=1.0)里的 alpha 是正则化强度,越大系数越被压缩;RandomForestRegressor的n_estimators=200表示 200 棵树,max_depth=12限制树深,防止过拟合;XGBoost 的learning_rate=0.05是收缩步长,越小越稳但要更多树来补偿。
4.2 网格搜索调参:注意验证策略,别用测试集调参
这是大作业里最容易被抓的学术规范问题。有些同学直接拿测试集的 RMSE 来调参,调完再用同一个测试集报分数,这相当于考试时先看答案再做题,分数严重虚高。正确做法是把训练集再切一份验证集出来,或者用交叉验证。源码里用的是GridSearchCV配合 5 折交叉验证,调完参数后再用完全没见过的测试集做最终评估。
from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [8, 12, 16], 'min_samples_split': [2, 5, 10] } rf = RandomForestRegressor(random_state=42) grid = GridSearchCV( rf, param_grid, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) best_rf = grid.best_estimator_ print(f'best params: {grid.best_params_}') print(f'best CV RMSE: {-grid.best_score_:.2f}')逻辑说明:GridSearchCV对参数组合做 5 折交叉验证——每次用其中 4 折训练、1 折验证,轮换 5 次取平均分。scoring参数用neg_root_mean_squared_error,注意 sklearn 的惯例是“负的 RMSE”,所以要取负号才是真实 RMSE。n_jobs=-1让所有 CPU 核心并行跑,参数组合越多加速越明显。参数说明:min_samples_split是节点继续分裂所需的最小样本数,调大它能让树更保守,抑制过拟合。网格搜索完不要直接结束,用best_rf在测试集上跑一次predict,那个分数才是作业里该报的分数。
4.3 特征重要性分析:给模型找“能说出口”的依据
大作业答辩时老师必问的问题是“你的模型为什么选这些特征?”随机森林和 XGBoost 都自带特征重要性属性,直接打印排名,但要注意:这个重要性是基于“分裂收益”算的,不是因果解释。你要做的就是把排名最高的几个特征和业务逻辑对齐——比如“面积”“单价”“所在商圈”排在前三,就能自圆其说;如果某个莫名其妙的特征排第一,就要回去查数据是不是出了问题。
import matplotlib.pyplot as plt importance = best_rf.feature_importances_ feat_names = X_train.columns.tolist() feat_imp = sorted(zip(feat_names, importance), key=lambda x: x[1], reverse=True) for name, imp in feat_imp[:10]: print(f'{name}: {imp:.4f}') # 可视化前10特征 top_n = feat_imp[:10] plt.figure(figsize=(10, 6)) plt.barh([x[0] for x in top_n][::-1], [x[1] for x in top_n][::-1]) plt.xlabel('Importance') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)逻辑说明:feature_importances_返回每个特征的归一化重要性分数,总和为 1。sorted降序排列后取前 10 名,用barh画水平条形图,逆向切片让最重要的特征显示在最上方。参数说明:dpi=150控制导出图清晰度,论文或报告里插图 150 够了,PPT 演示可以调 200。如果你用的是 XGBoost,重要性可用xgb.plot_importance(model)一步画图,但列名需要提前设置feature_names。
5. 常见问题排查:跑源码时最常踩的五个坑
5.1 文件路径报错:相对路径和绝对路径的玄学
现象:代码报FileNotFoundError: [Errno 2] No such file or directory: 'house_data.csv',但你确认文件就在代码同目录下。
原因:多数脚本用相对路径读取文件,但 vscode 或 PyCharm 的工作目录(working directory)不一定是脚本所在目录,而是你打开项目时的根目录。所以相对路径找不到文件。
解决:统一改成绝对路径,或者用os.path动态拼接脚本所在目录,源码包里就是后者。
import os base_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(base_dir, 'data', 'house_data.csv') df = pd.read_csv(file_path)参数说明:__file__是当前脚本的完整路径,os.path.dirname取所在目录,os.path.join用操作系统的路径分隔符拼接子目录。这样不管从哪个目录启动项目都不会报错。
5.2 pandas 版本差异导致 get_dummies 后列数不一致
现象:自己跑源码时,训练集和测试集经过get_dummies后列数不同,模型预测时报ValueError: feature shape mismatch。
原因:训练集和测试集里类别字段的取值集合不一样。比如训练集所有房子都有“朝向-南”这个类别,测试集里恰好没有,独热编码后测试集少一列。
解决:先用pd.get_dummies同时处理两份数据,或者用reindex对齐列。源码里的做法是合并处理后再拆分。
X_all = pd.concat([X_train, X_test]) X_all = pd.get_dummies(X_all, columns=cat_cols) X_train = X_all.iloc[:len(X_train)] X_test = X_all.iloc[len(X_train):]逻辑说明:先把训练集和测试集纵向拼接,统一做独热编码,再按原始行数切回去。这样保证两边列完全一致。参数说明:注意pd.concat后索引会重置,iloc是按位置切分的,不受索引影响。
5.3 中文路径或中文列名的编码问题
现象:读取含中文列名的 CSV 时出现乱码,或者pyplot画图时中文标签变成方块。
原因:Windows 下 pandas 默认编码可能是gbk,而 CSV 文件是utf-8保存的;matplotlib 默认字体不支持中文显示。
解决:读取时显式指定编码,画图前设置中文字体。
df = pd.read_csv(file_path, encoding='utf-8-sig') plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False参数说明:utf-8-sig会去除文件头部的 BOM 标记,防止第一列列名出现乱码。SimHei是 Windows 自带黑体,Microsoft YaHei是微软雅黑,Linux 环境改成WenQuanYi Zen Hei。axes.unicode_minus=False是防止负号显示成方块。
5.4 目标变量泄露:不小心把未来信息喂给模型
现象:模型评估分数特别高,R2 达到 0.99,但实际预测新数据时完全崩塌。
原因:特征里混入了目标变量的“未来对应值”或强派生字段。比如直接用了“成交总价/面积”生成的单价,但测试时根本没有成交总价可用;或者数据里同时存在“挂牌价”和“成交价”,把挂牌价当成特征输入了。
解决:检查所有特征列,凡是计算逻辑依赖目标变量的直接删掉。单价特征只能在训练阶段构造,预测时无法获得真实总价去算单价——除非你用预测出来的总价去回代,这就循环引用了,属于逻辑硬伤。
5.5 网格搜索跑太久:参数组合数爆炸
现象:GridSearchCV跑了半小时还没出结果,机器风扇狂转。
原因:n_estimators取了 5 个值,max_depth取 5 个值,min_samples_split取 4 个值,5折交叉验证下共 5×5×4×5=500 次完整模型训练,每次训练 200 棵树,总计算量巨大。
解决:先用小规模参数粗调,锁定好区域再细调;或者用RandomizedSearchCV做随机搜索,用更少的组合覆盖更大的参数空间。
from sklearn.model_selection import RandomizedSearchCV import scipy.stats as stats param_dist = { 'n_estimators': stats.randint(100, 500), 'max_depth': stats.randint(5, 20), 'min_samples_split': stats.randint(2, 20) } random_search = RandomizedSearchCV( rf, param_dist, n_iter=20, cv=5, scoring='neg_root_mean_squared_error', random_state=42 )逻辑说明:stats.randint生成连续整数分布,n_iter=20只采样 20 组参数组合,计算量是网格搜索的零头。random_state=42保证结果可复现。参数说明:如果你的数据量超过 5 万行,建议先用 1 万行子样本粗调,锁定超参范围后再全量训练,这是实际项目里省时间的常规操作。
6. 验证不止看 RMSE:用残差图发现模型盲区
6.1 残差分布比单一指标更有说服力
RMSE 和 R2 只能告诉你模型整体“平均表现”,但平均值会掩盖局部系统性偏差。比如模型对低价房预测偏保守、对高价房预测偏激进,两者的误差互相抵消,RMSE 看着不错,实际业务场景里高价房的预测误差会让你亏大钱。
残差分析就是看y_true - y_pred的分布。源码里画了两张图:残差 vs 预测值的散点图,以及残差的直方图。前者如果呈现喇叭形(左窄右宽),说明数据存在异方差性,预测区间在高价区间越来越大。后者如果明显偏离正态分布且均值不为 0,说明模型存在系统性偏差。
import numpy as np y_pred = best_rf.predict(X_test) residuals = y_test.values - y_pred print(f'Mean residual: {np.mean(residuals):.2f}') print(f'Std residual: {np.std(residuals):.2f}') # 分价位段看误差表现 bins = [0, 200, 500, 1000, np.inf] labels = ['0-200万', '200-500万', '500-1000万', '1000万+'] segments = pd.cut(y_test.values, bins=bins, labels=labels) for seg in labels: mask = segments == seg seg_rmse = np.sqrt(np.mean(residuals[mask] ** 2)) print(f'{seg}: RMSE={seg_rmse:.2f}万')逻辑说明:residuals是真实值减预测值,正值代表低估、负值代表高估。np.mean如果明显偏离 0,比如均值是 -8 万,说明模型整体高估了 8 万。pd.cut把测试集的真实房价切成四个价格段,分别计算每段的 RMSE——你会发现 1000 万以上价位的 RMSE 可能是低价段的几十倍,这就是模型的盲区。
6.2 分价位段误差是答辩杀手锏
把上面的分价位 RMSE 做成柱状图,答辩时直接展示“模型在 200 万以下的预测误差控制在 15 万左右,但 1000 万以上误差放大到 80 万,原因是高端房源样本量少、特征区分度不足”。这句话的价值比任何调参技巧都高,因为它表明你不仅知道模型表现好,还知道它在哪里表现不好、为什么不好。
从那以后我每次跑完模型不会先去看 R2,而是强制自己先做这一遍残差和分段误差分析。R2 好看只能说明整体趋势拟合到位,但交作业或交付项目时,“哪里不行”往往比“哪里行”更能扛住追问。这份源码包里已经把这段分析和可视化都写好了,你拿到后第一件事应该是把自己的数据跑进去,先看残差图,再决定要不要调参——这比盲目优化模型参数有用得多。希望帮到你。
本文还有配套的精品资源,点击获取