
简介面向计算机相关专业期末大作业与毕业设计场景的机器学习实战项目聚焦北京房价与二手房价格预测覆盖从数据采集、特征处理、模型训练到评估的完整流程。压缩包共二十六个文件、约1.28MB主要包含Python脚本、Jupyter Notebook分析文档、CSV数据集、可视化图片、HTML分析报告及Markdown说明其中Python脚本负责链家与安居客数据爬取、清洗和价格可视化Notebook与HTML报告则逐步展示建模与调参过程目录结构清晰便于边看边练、二次开发。已有111人学习浏览。项目经导师指导并以98分通过评审所有源码均本地编译调试通过、可直接运行内附完整二手房数据集及对应爬虫脚本可帮助快速掌握房价预测建模全流程也能作为课程大作业、毕业设计或机器学习项目实战的可靠参考。1. 拿到这份“房价预测源码”先想清楚大作业要交什么期末做机器学习大作业最怕的不是模型跑不出数而是代码能跑、报告却讲不明白。题目里写着“房价与二手房价格预测源码使用说明”说明这已经不是让你从零写算法而是给你一套可运行的工程数据、特征、模型、评估都在里面你的任务是把它变成自己的东西在答辩现场对老师的追问接得住。这类项目最反直觉的一点是它真正值钱的部分不是模型而是数据处理和那篇使用说明。你在本地把结果复现出来只需要半小时但要把每个决策讲出理由、把每张图表放对位置才是能拿高分的大作业。这篇文章就按“拿到源码后先干什么、数据要怎么洗、模型怎么选、跑的时候踩在哪、最后怎么交”的顺序把整条路走一遍。2. 数据预处理房价数据集常见脏点与清洗脚本2.1 读入数据前的三个约定字段含义、缺失值标记、价格单位拿到源码包之后第一件事不是跑 train.py而是打开数据文件看列名。房价数据最常见的三个“暗坑”藏在字段名里第一个是价格单位不统一有的列叫 total_price单位是“万元”有的叫 unit_price单位是“元/平米”一不小心混着用模型直接报废第二个是缺失值标记五花八门有人用 NaN、有人用 0、还有人用字符串“暂无”第三个是“总价”和“单价”可能由同一字段推算出来后面特征工程会撞上严重共线性。我一般会先写一段只读代码把字段类型、缺失量、分布范围一次看清楚import pandas as pd df pd.read_csv(house_data.csv, encodingutf-8) print(样本数:, df.shape[0], 特征数:, df.shape[1]) print(df.dtypes) print(df.head(3)) print(df.describe(percentiles[0.25, 0.5, 0.75, 0.95]))这段代码的关键在describe(percentiles[...])默认的 describe 只给四分位数加上 0.95 是为了看右尾有多重。二手房价格几乎都是长尾分布95 分位和最大值之间可能差一个量级如果只按均值去筛离群值会把大批高价房误杀。读进来之后还要逐个字段确认“面积”是不是平方米、“总价”是不是万元这些在源码的使用说明里通常会写但实际数据里总有个别行是“130㎡”这种带单位的字符串需要先清洗。2.2 缺失值和离群值直接删还是填充要看业务含义缺失值处理没有万能公式但有一条优先级能按业务逻辑推断的用规则填充推断不了的用中位数最差才用均值。原因是房价数据几乎总有右偏均值会被豪宅拉高用它填充相当于给普通房子人为抬价。# 先看每一列的缺失量 missing df.isna().sum() missing missing[missing 0] print(缺失字段\n, missing) # 房龄缺失用同小区/同年份的中位数填充 df[house_age] df[house_age].fillna(df[house_age].median()) # 装修状态是分类变量缺失就归到“未知”不要删行 df[decoration] df[decoration].fillna(未知) # 面积和总价是核心数值明显异常的直接过滤 df df[(df[area] 20) (df[area] 300)] df df[(df[total_price] 0) (df[total_price] 2000)]参数说明面积下限 20 平米是为了滤掉车位、储藏室之类非住宅挂牌上限 300 平米看城市一线城市豪宅多可以放宽到 400二三线 300 够用。总价上限 2000 万同理北京上海要上调。这类参数我会写在使用说明最前面的“数据约定”一节让老师复现时知道为什么要设这两个数。注意一个取舍缺失比例超过 40% 的列填充已经没有太大意义直接删列更干净。分类变量的缺失不要粗暴填“无”很多城市数据里“朝向”缺失恰恰意味着无朝北窗或信息不完整归成单独类别反而保留了信息。2.3 房价为什么需要对数化右偏分布与评估指标的连带影响如果直接用原始房价做回归损失函数会被几套千万级豪宅的误差主导——普通房子预测差 10 万损失只有 10 的平方豪宅差 100 万损失直接放大 100 倍。模型会把全部精力用来拟合少数大值普通房子的精度反而烂掉。解决办法是训练时对目标取对数评估时再还原。import numpy as np df[log_price] np.log(df[total_price]) print(原始价格偏度:, df[total_price].skew()) print(对数价格偏度:, df[log_price].skew())偏度skew是衡量分布是否对称的指标绝对值大于 0.5 就值得处理。对数变换后偏度会降到接近 0这时再用 RMSE 评估误差含义从“绝对金额差”变成了“百分比意义上的倍数差”。比如 log 空间 RMSE 是 0.2还原后代表约 20% 的预测误差这在二手房业务里比“平均差 25 万”更有说服力。后续训练都用log_price作为 y等到输出预测结果时用np.exp()还原成万元。这一点会在模型评估章继续展开。3. 特征工程与相关性分析哪些特征决定了二手房价3.1 相关性矩阵与多重共线性先筛一遍特征再建模房价预测的特征工程核心目标不是“造更多列”而是回答两个问题哪些字段与价格强相关哪些字段之间互相打架先用相关性矩阵把数值型特征扫一遍这一步属于机器学习应用流程里最基础、但最容易跳过的“数据理解”。numeric_cols [area, house_age, bedrooms, living_rooms, floor, total_price] corr df[numeric_cols].corr() print(corr[total_price].sort_values(ascendingFalse))常见结论是面积与总价相关性最高通常能到 0.6-0.8卧室数和面积强相关但同时放进模型会造成多重共线性线性回归的系数会变得不稳定换个数据子集系数就乱跳。处理方法二选一要么只保留面积卧室数作为描述性统计写在报告里要么保留卧室数、用面积构造“每间房面积”这种复合特征。后者的解释性更好也更容易在答辩时讲出故事。另外要警惕“总价单价×面积”的乘积关系。如果数据里同时给了 total_price 和 unit_price这俩与面积的乘积关系会让模型出现虚高的 R2——它其实在做乘法而不是在学价格规律。拿到这种数据我的做法是只保留 unit_price 作为预测目标特征里不放 total_price。3.2 区域与朝向的编码one-hot 与均值编码的取舍二手房价格里地段往往比面积更重要。但“区域”是字符串必须转成数值。最无脑的是pd.get_dummiesdf pd.get_dummies(df, columns[district], drop_firstTrue) print(df.columns.tolist())问题在于当某个区只有几套样本时one-hot 会制造一个几乎全零的稀疏列模型可能把它当噪声硬学造成过拟合。区域类别超过 20 个时我一般改用“均值编码”——用该区域房价均值作为特征。但均值编码有泄露风险必须用训练集内交叉验证得到的均值不能拿全量数据算完直接灌进模型。你可以在使用说明里专门写一段“特征编码为什么这么选”这是机器学期末复习里“数据处理是什么”的落地版答出来就是加分项。朝向在北方市场很敏感南北通透和北向能差出明显价格梯度。朝向处理不要 one-hot 成东南西北四个列我习惯压缩成三个有南、无南、未知。df[has_south] df[direction].str.contains(南).astype(int)这一行就把问题简化了模型更稳报告里也好解释。3.3 构造面积单价与房龄风险两个派生特征机器学习模型不会自动理解“面积大但单价低”这种复合语义需要人把先验知识做成特征。最常用的是两个派生特征一个是单价本身如果目标还是总价一个是房龄风险标记。# 派生1单价元/平米分析用建模时谨慎使用 df[unit_price] df[total_price] * 10000 / df[area] # 派生2房龄 df[house_age] 2025 - df[build_year] # 派生3顶楼标记 df[is_top_floor] (df[floor].str.contains(顶层)).astype(int) # 派生4是否近地铁用已经有的距离字段二值化 df[near_subway] (df[subway_distance_km] 1.0).astype(int)参数说明house_age用当前年份减建筑年份注意如果数据是往年爬的应该用数据采集年份而不是今年顶楼标记对老小区是减分项对带阁楼或露台的新盘反而是加分项要不要保留看你的数据集里哪种占比多地铁阈值 1 公里是我常用的经验值一二线城市步行 10 分钟以内算“近地铁”。单价这个特征要特别小心如果训练目标是总价且特征里已有面积再加入单价模型实际在学“单价×面积≈总价”这个恒等式R2 会飙到 0.98 以上答辩时老师一眼就能看出来。正确用法是分析阶段用单价做可视化洞察建模阶段要么删掉它预测总价要么把目标换成单价预测再乘回面积。4. 模型训练与评估从线性回归到梯度提升的四次对比4.1 划分训练集与测试集随机种子为什么必须固定数据集划分是所有实验的“基准线”。很多新手跑完发现结果不错换个别人机器就崩多半是随机划分没固定种子或者把标准化 fit 在了全体数据上。from sklearn.model_selection import train_test_split # 方案A普通随机划分样本独立场景 X df.drop(columns[total_price, log_price, unit_price, url]) y df[log_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, shuffleTrue ) print(训练集:, X_train.shape, 测试集:, X_test.shape)random_state42不是玄学它保证每次运行划分结果一致这是“可复现实验”的最低要求。如果数据本身是按时间发布的二手房挂牌数据更要小心房价随时间有上行趋势随机划分等于让模型通过“时间泄漏”偷看到未来测试集分数虚高实盘部署必然翻车。时间型数据要用方案B# 方案B按时间顺序切分保证测试集时间晚于训练集 df df.sort_values(listing_date) split_idx int(len(df) * 0.8) X_train, X_test df.iloc[:split_idx], df.iloc[split_idx:] y_train, y_test X_train[log_price], X_test[log_price]两种方案跑出来的 RMSE 会有明显差距时间切分通常更差但更真实。使用说明里如果提到“测试集 R2 达 0.9”你要先判断是不是随机划分导致的虚高。4.2 五个回归模型的同场对比评估指标不要只看 R2大作业需要对比实验至少要跑一个线性模型、一个树模型、一个集成模型。我常用的组合是 LinearRegression、Ridge、RandomForestRegressor、GradientBoostingRegressor、SVR五份结果放进一张表谁好谁坏一目了然。from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.svm import SVR from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score models { Linear: LinearRegression(), Ridge: Ridge(alpha1.0), RandomForest: RandomForestRegressor( n_estimators200, max_depth8, min_samples_leaf4, random_state42, n_jobs-1 ), GBDT: GradientBoostingRegressor( n_estimators100, learning_rate0.05, max_depth3, random_state42 ), SVR: SVR(C10, gammascale, epsilon0.1), } for name, model in models.items(): model.fit(X_train, y_train) pred_log model.predict(X_test) pred np.exp(pred_log) # 从对数空间还原成万元 rmse mean_squared_error(np.exp(y_test), pred, squaredFalse) mae mean_absolute_error(np.exp(y_test), pred) r2 r2_score(np.exp(y_test), pred) print(f{name}: RMSE{rmse:.2f}万, MAE{mae:.2f}万, R2{r2:.4f})注意我把mean_squared_error(..., squaredFalse)写成RMSEsklearn 1.4 之后推荐直接用root_mean_squared_error如果你装的版本旧squaredFalse仍然可用。np.exp()还原这一步很多人漏掉会在 log 空间算 RMSE数字小得好看但报告里没法解释“误差 0.2”到底是什么单位。在 5000 条样本的一次运行中常见量级接近下表数值会随数据和随机种子变化不是源码包自带结果模型RMSE万元MAE万元R2Linear31.220.80.86Ridge31.020.90.86RandomForest27.817.10.89GBDT27.216.70.90SVR29.519.20.87结论很常见集成树模型明显优于线性模型R2 从 0.86 提升到 0.90但提升幅度没有想象中大原因是房价里的“随机噪声”本身很大位置、谈判溢价这些信息不可能全部编码进字段。答辩时不要只念 R2要解释“0.90 的 R2 意味着约 30% 的方差未解释”这比数字本身更显水平。4.3 用学习曲线判断过拟合大作业的“调参空间”怎么找随机森林和 GBDT 都有大量超参数但大作业不需要你跑到最优而是需要你能说出“我调参后看到了什么变化”。最有说服力的证据是学习曲线横轴是训练样本量纵轴是训练集和交叉验证集的 RMSE两条线之间的间隙就是过拟合程度。from sklearn.model_selection import learning_curve import matplotlib.pyplot as plt train_sizes, train_scores, val_scores learning_curve( RandomForestRegressor(n_estimators200, max_depth8, random_state42), X_train, y_train, cv5, train_sizes[0.2, 0.4, 0.6, 0.8, 1.0], scoringneg_root_mean_squared_error, ) train_rmse -train_scores.mean(axis1) val_rmse -val_scores.mean(axis1) plt.plot(train_sizes, train_rmse, label训练集 RMSE) plt.plot(train_sizes, val_rmse, label交叉验证 RMSE) plt.xlabel(训练样本量) plt.ylabel(RMSE (log空间)) plt.legend() plt.savefig(learning_curve.png, dpi150)如果训练集和验证集两条线始终贴得很近说明模型处于欠拟合优先加n_estimators、max_depth如果训练集一路走低、验证集走平甚至抬高说明过拟合优先加min_samples_leaf、减max_depth。我通常会先固定n_estimators200只在深度和叶子最小样本数之间做 3×3 网格搜索把结果贴进报告就够用了。网格搜索代码同样固定random_state42避免调参过程把随机波动当成提升。5. 跑源码时的避坑记录从环境到结果的 5 个真实问题5.1 sklearn 版本变化导致 RandomForestRegressor 参数报错现象拿到源码后跑model.fit()直接报错Invalid parameter criterionmse或者其他属性不存在。原因sklearn 1.2 之前随机森林的criterion参数写法是mse1.2 之后改成了squared_error老代码在旧版本跑没问题新版本里注册名变了就挂掉。解决# sklearn 1.2 RandomForestRegressor( n_estimators200, min_samples_leaf4, criterionsquared_error, # 老写法是 mse random_state42, )同时在项目里补一个requirements.txt写明scikit-learn1.2。这类环境坑在使用说明里一定要写清否则老师在一台新机器上一跑就报错观感很差。5.2 数据泄露把标准化 fit 到了全量数据上现象训练集 R2 不错交叉验证也可以但拿到一份新小区的真实数据去预测误差大得离谱。原因有人先对整个数据集做了StandardScaler().fit_transform()再切分训练测试集。这样测试集的均值和方差已经参与过全局统计信息从“未来”流进了训练过程属于典型的数据泄露。解决先切分再标准化或者直接用 Pipelinefrom sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline([ (scaler, StandardScaler()), (model, RandomForestRegressor(n_estimators200, random_state42)), ]) pipe.fit(X_train, y_train) pred_log pipe.predict(X_test)Pipeline 的好处是 fit 只在训练集上进行测试集变换自动沿用训练集的均值和方差从机制上杜绝这种泄露。5.3 随机划分导致“时点穿越”测试集虚高现象用随机划分跑出 R20.92换成按时间排序切分后 R2 掉到 0.84。原因二手房挂牌数据天然带时间趋势随机划分等于把 2021 年的房子放进训练集、2024 年的房子放进测试集模型“见过”了未来的价格水平。解决看源码里的使用说明有没有提数据处理日期没提就默认按时间切分至少在报告里补一个对比实验写明“随机划分 R2 虚高时间划分是更严格的上线预估”。5.4 中文列名和文件编码导致的数据读取失败现象直接pd.read_csv(数据.csv)报UnicodeDecodeError或者列名成了乱码。原因Windows 上的 Excel 导出 CSV 默认是 GBK 编码而 pandas 默认按 UTF-8 读中文路径还会触发编码问题。解决df pd.read_csv(data/北京二手房.csv, encodinggbk) # Windows 来源 # 或 encodingutf-8-sig # 带 BOM 的 UTF-8另外 matplotlib 画中文图时还要处理字体plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS]否则图里全是方框。我习惯把所有列名统一改成英文小写加下划线从源头避开后续所有编码问题。5.5 预测出负的房价要不要截断现象模型用原始价格训练线性回归或 SVR 对某些低总价样本预测出负数。原因房价分布右偏严重模型在低端区域没学到“价格必须大于 0”的约束。解决最省事的是对预测值做下限截断更优雅的是直接训练log_price因为对数空间天然只能还原出正数。如果你已经在用 log 训练还出现负值检查是不是还原函数写成了np.log而不是np.exp我见过有人把还原方向写反结果预测一列负数还查了半天。截断代码顺手加上pred np.exp(pred_log) pred np.clip(pred, a_min1.0, a_maxNone) # 单价/总价最低给1万元防止异常6. 把源码变成自己的大作业报告结构与演示技巧拿到一份能跑的源码最后一步是“去源码化”——让老师觉得这是你做过一遍、能讲清楚的项目。报告结构我建议按五段走数据说明与清洗、特征工程、模型对比、误差分析、结论。真正拉开差距的是误差分析把预测误差最大的 20 个样本拉出来看是不是都在高价豪宅区如果是可以写“模型对长尾豪宅定价存在系统性低估”然后试着分析原因是样本量不足还是特征缺失这样的报告比堆模型有明显深度。演示时准备一个带--seed的命令行入口演示分数和报告里的分数严格一致python train_eval.py --data data/house_cleaned.csv --seed 42 --model rf使用说明里把这行命令放最前面老师复现时不会因为随机波动看到不同数字而质疑。参数--model支持linear/ridge/rf/gbdt/svr四种对应报告里的对比表让老师可以现场切换验证。最后说个我自己的习惯提交大作业前我会把使用说明当成“给一个完全不懂机器学习的同学看”的标准来重写一遍——环境装什么、数据放哪、命令怎么敲、输出文件在哪、每个参数改什么全写清楚。因为这类项目的分数从来不只是跑通代码而是你能不能让别人也跑通。希望帮到你。本文还有配套的精品资源点击获取