
简介这份基于机器学习算法的房价预测项目专注于北京二手房价格分析完整覆盖数据采集、数据清洗、特征工程、模型训练与结果可视化等环节适合人工智能、数据科学或 Python 相关方向的学生用于期末大作业、毕业设计参考也适合希望进行机器学习实战练习的初学者。整个压缩包共包含二十六个文件大小约一点二九兆字节其中以 Python 脚本为主体另有交互式分析文档、CSV 格式的数据集、HTML 格式的分析报告以及若干图片素材Python 脚本主要用于爬虫抓取和模型构建交互式文档可分段演示数据处理与建模思路CSV 文件存放链家、安居客等平台的真实房价数据HTML 文档则直观展示分析结论整体目录结构清晰便于按流程学习和二次开发。压缩包内还配有使用说明文档对项目背景、代码运行方式和结果分析都有介绍能帮助读者快速理解整体实现逻辑。该项目为导师指导认可的高分作业评审得分九十八分所有源码均经过本地编译调试可稳定运行。目前已有一百四十四人学习使用适合需要高质量参考模板并希望在房价预测任务中快速完成作业的读者。1. 人工智能大作业房价预测先想清楚“学什么”再动手大作业要求“基于机器学习的房价和二手房房价预测源码使用文档”大多数人的第一步是从免费源码库里拉一套代码跑通就准备交差。这条路在答辩时很容易翻车老师随机改一个城市、换一批字段你的模型立刻失效。房价预测本质不是“跑通代码”而是建立“特征→价格”映射的过程是否选对特征、是否处理好价格分布的偏斜、模型是否过拟合才是拿分的关键。这篇按一份标准大作业的体量把数据处理、特征构造、模型选型、评估验证到交付文档的完整路径讲清楚。无论你是按人工智能学习路线自学的还是正在补机器学习期末代码都能直接改字段跑也适用于想用真实数据练手的开发者——重点放在工程师视角的可复现性上不写魔改技巧只写经得起追问的方案。2. 数据清洗与特征构造二手房价格预测的胜负手2.1 先定字段二手房源数据的标准特征集拿到一份二手房数据第一件事不是建模而是先确认字段语义。链家、贝壳或其他平台导出的数据通常包含total_price总价万元、unit_price单价元/平米、area面积平米、bedroom卧室数、living_room客厅数、floor所在楼层、total_floor总楼层、build_year建成年份、decoration装修程度、toward朝向、community_name小区名、district行政区。如果只有部分字段我一般会用类似下面的结构统一列名避免后续特征工程里出现字段命名混乱。import pandas as pd df pd.read_csv(house_data.csv) df.rename(columns{ price: total_price, area_sqm: area, bedrooms: bedroom, year: build_year, decorate: decoration }, inplaceTrue) # 最小可用字段集 required_cols [ total_price, area, bedroom, living_room, floor, total_floor, build_year, decoration, toward, district, community_name ] for col in required_cols: if col not in df.columns: print(fmissing: {col})这段代码的价值不在重命名而在于提前暴露缺失字段。很多从“免费python源码大全”里拉来的代码第一个坑就是字段名不统一。作业里推荐用一份检查清单确认总价和单价是否矛盾单价×面积应约等于总价、建成年份是否在合理区间比如大于1950、面积是否为正数。数据里一旦出现总价不为空而面积为空的记录不要直接删除先用小区或行政区的单位面积均价反推反推不了的再删。字段确认后就是缺失值策略。下面的表是二手房项目里常用的做法字段缺失比例处理方式build_year5% 以内用同小区均值无小区的用行政区中位数floor3% 以内填中位数或转为“中间楼层”decoration10% 以上单独标记“未知装修”不丢弃total_price任意行删除因为这是目标变量area10% 以上谨慎优先用单价反推反推不了再删除注意二手房数据里unit_price缺失比total_price缺失常见因为平台展示时单位价格可能未同步。此时可以用total_price / area计算出来而不是丢弃记录。2.2 目标变量为什么要取 log房价数据几乎必然右偏几套豪宅把均价拉高大部分房源集中在低位区间。如果不做处理模型会为了拟合那几个极端的千万级样本牺牲普通房源的价格精度。看一个典型分布中位数 280 万均值 420 万最大 9000 万这是二手房数据的常态。我一般会在进入模型前对total_price做对数变换也就是回归任务里常见的log1p然后在评估时再expm1还原。这样做的直接好处是训练时误差在相对尺度上计算RMSE 对应的是“价格差 10%”而非“差 30 万”更符合人对房价的感知。import numpy as np df[log_price] np.log1p(df[total_price]) # 验证变换效果 before_skew df[total_price].skew() after_skew df[log_price].skew() print(fbefore: {before_skew:.3f}, after: {after_skew:.3f})如果变换后的偏度仍大于 1说明数据里有极端离群点比如某个小区的建筑类型特殊导致价格远超周边。处理方法是看面积和单价是否匹配一间 30 平米的“学区房”总价 500 万单价十几万这种不是离群点而是强信号不能删真正该删的是同一小区、同面积段里总价偏离 5 倍以上的记录。对数变换的目的不是消灭尾部而是让模型在训练时不会把梯度全部花在极值样本上。2.3 构造和房价强相关的派生特征模型能学到的上限由特征决定。二手房项目里我固定构造这四类特征效果稳定且答辩时容易解释。第一类是年代特征house_age 当前年份 - build_year。老房子有两个方向的影响房龄折旧压低价格但历史保护建筑或核心学区的高房龄反而拉高价格所以单放一个房龄让模型自己学非线性关系比手动分段更可靠。第二类是楼层位置floor_ratio floor / total_floor这个比例比绝对楼层更有跨小区可比性。低层、中层、高层的价格差异在不同小区里方向还不一样有的小区顶层带露台反而贵这个特征交给树模型去切分是合理的。第三类是配套密度如果原始数据里有周边学校、地铁、商圈的距离字段可以构造“3 公里内地铁站数量”这类计数特征没有的话就聚合小区内房源数量作为热度特征。同一小区挂牌越多说明换手活跃价格水分相对小这个特征很直观。第四类是交互特征unit_price * area实际上还是总价没有新增信息我不建议这么做更有效的交互是bedroom / area每平米卧室数衡量房间切割密度小户型高密度和大户型低密度对应的目标客群完全不同。df[house_age] 2024 - df[build_year] df[floor_ratio] df[floor] / df[total_floor] df[room_density] df[bedroom] / df[area] df[avg_price_by_district] df.groupby(district)[unit_price].transform(median) # 面积分段小户型、刚需、改善、豪宅 bins [0, 50, 90, 140, 200, np.inf] labels [S, M, L, XL, XXL] df[area_bucket] pd.cut(df[area], binsbins, labelslabels)avg_price_by_district这种行政区分组特征的思路是把区域内市场基准价直接作为特征。注意这会造成一定的数据泄露风险——测试集如果来自同一个行政区模型会依赖这个特征而不是真正学会价格规律。我的处理方案是如果作业是预测同一城市不同区域的房源这个特征保留如果预测目标换成新城市这个特征要删掉否则模型在跨区域场景下直接失效。2.4 离散特征编码与量纲归一化toward、decoration、area_bucket这些离散特征需要编码。朝向通常是南、东南、东西等十几种取值树模型可以直接用标签编码线性模型更适合 one-hot。装修程度有毛坯、简装、精装、豪华四档这个是有序的可以直接映射到 0-3 的整数但我不建议在树模型里把它当普通整数用——LightGBM 会把 3 和 2 的关系当作线性关系来切分而实际上豪华装修和精装的差距远大于精装和简装的差距所以要么 one-hot要么换成分数制100 分制装修评分这样差距更平滑。df[decoration_score] df[decoration].map({ 毛坯: 0, 简装: 40, 精装: 80, 豪华: 100 }) df pd.get_dummies(df, columns[toward, area_bucket], drop_firstTrue)量纲归一化要看模型选什么。决策树和梯度提升树对特征尺度不敏感归一化不影响结果做了反而增加计算负担线性回归、SVM、MLP 需要归一化否则数值范围大的特征主导损失函数。大作业通常会对比多个模型所以我的建议是准备两份数据一份原始特征给树模型一份标准化后的特征给线性模型和 MLP评估时统一用同一套交叉验证划分。标准化时注意只对数值特征做不对 one-hot 后的列做。3. 模型选型与参数调优从线性回归到 LightGBM3.1 用岭回归建立基线很多从“100个python实战项目”里学会的第一件事就是直接上复杂模型但大作业答辩里评委第一个问题通常是“你的基线是什么”。没有基线的实验无法证明复杂模型的价值。我一般先用岭回归做基线它的作用是给出一个“如果特征是线性可加误差能做到多少”的下界。from sklearn.linear_model import Ridge from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score features [c for c in df.columns if c ! total_price and c ! log_price] X df[features].select_dtypes(include[np.number]).fillna(0) y df[log_price] pipeline make_pipeline(StandardScaler(), Ridge(alpha1.0)) scores cross_val_score(pipeline, X, y, cv5, scoringneg_root_mean_squared_error) print(fRidge RMSE(log): {-scores.mean():.4f})这里用neg_root_mean_squared_error是因为 sklearn 的交叉验证默认取最大值所以误差类指标要取负号。如果X里还有字符串列上面的select_dtypes会忽略它们但这样会把district这样的关键信息丢掉。我通常的做法是先对district做 target encoding用该区域的历史平均log_price代替地区名这样既保留区域信息又是数值形式线性模型可用。如果岭回归的 RMSE(log) 在 0.25 以上说明特征和价格的关系存在明显非线性或者缺少关键特征这时候梯度提升树就该登场了。如果已经小于 0.15线性基线已经很好了后面加模型的意义更多是提升几个百分点不值得投入太多时间调参。3.2 为什么梯度提升树是大作业首选李宏毅老师的机器学习课上有个观点很实用模型类别先挑和你数据特性匹配的而不是选最先进的。二手房数据的特性是特征规模几十列、有缺失值、有类别型变量、特征和价格的关系高度非线性且带交互梯度提升树在这些条件下几乎是最省心的选择。XGBoost、LightGBM、CatBoost 三者都可以但大作业我推荐 LightGBM原因很实际直方图算法让训练速度快叶子生长策略在样本量几万条时不容易过拟合对中文环境下的离散特征编码容错高而且 sklearn 接口统一交叉验证代码可以直接复用。from lightgbm import LGBMRegressor model LGBMRegressor( n_estimators500, learning_rate0.05, num_leaves31, max_depth-1, min_child_samples20, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit(X_train, y_train)参数含义在后面展开这里建模型时先把random_state固定大作业要满足“可复现”这个基本要求。第一次跑不用追求最优参数用默认的n_estimators100快速看结果确认训练集和验证集的误差都收敛之后再进入调参环节。如果你发现验证集误差远大于训练集说明模型过拟合下一步不是加数据而是调num_leaves和min_child_samples。3.3 LightGBM 的 6 个必调参数大作业里不需要系统性网格搜索重点调下面这六个参数性价比最高参数作用调整方向典型范围n_estimators树的数量配合早停使用不用手动穷举100-2000learning_rate每一步的步长越低越稳但需要更多树0.01-0.1num_leaves单棵树叶子数控制模型复杂度大则拟合强但容易过拟合15-127min_child_samples叶子节点最少样本数大则约束更强防过拟合20-100subsample行采样比例小则随机性高方差小0.7-0.9colsample_bytree列采样比例小则特征多样性高0.7-0.9调参顺序比参数本身更重要。我固定用一套三段式流程第一步固定learning_rate0.05调num_leaves和min_child_samples观察验证集误差第二步调subsample和colsample_bytree观察误差方差是否下降第三步调learning_rate到 0.02 并增大n_estimators让模型在低学习率下充分拟合。这三步每一步跑一次五折交叉验证就够了不要在同一轮里同时动三个参数否则不知道是哪个参数起作用。from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error kf KFold(n_splits5, shuffleTrue, random_state42) rmse_list [] for train_idx, val_idx in kf.split(X): X_train_fold, X_val_fold X.iloc[train_idx], X.iloc[val_idx] y_train_fold, y_val_fold y.iloc[train_idx], y.iloc[val_idx] model.fit( X_train_fold, y_train_fold, eval_set[(X_val_fold, y_val_fold)], callbacks[lgb.early_stopping(stopping_rounds50, verboseFalse)] ) pred model.predict(X_val_fold) rmse_list.append(mean_squared_error(y_val_fold, pred, squaredFalse)) print(fCV RMSE(log): {np.mean(rmse_list):.4f} ± {np.std(rmse_list):.4f})注意callbacks参数需要import lightgbm as lgb不能直接用LGBMRegressor的字符串写法。early_stopping不是参数是回调函数写错位置会直接报KeyError。这里报错是因为 LightGBM 新版把早停从训练参数移到了回调里如果你用的版本是 3.x 之前才需要写成early_stopping_rounds。环境不统一时这个坑经常出现我建议直接把版本锁定在lightgbm4.0并在文档里写清楚环境要求。3.4 用早停法确定树的数量不靠猜树的数量一旦和学习率联动手动调就很低效。常见做法是设定一个较大的n_estimators比如 2000然后靠早停回调在验证集误差不再下降时终止训练。上面的代码里stopping_rounds50的含义是连续 50 轮验证集误差没有改善就停止训练并回滚到最优迭代次数。注意早停只能用在有验证集的情况交叉验证里每个 fold 都要传入各自的验证集不能把全局的X_val传进去。跑完交叉验证后最终模型的n_estimators取所有 fold 的best_iteration_的平均值再在全量数据上重新训练一次。这一步容易被忽略但“用一半数据训练出的模型直接拿去测试”是典型的不严谨实验答辩时被追问会很尴尬。4. 模型评估与跨区域泛化验证实验的严谨性4.1 不要只看 R²用指标矩阵判断模型大作业报告里最常见的单薄写法是“本题 R² 达到 0.92”这句话信息量很低。R² 对离群点极度敏感房价数据里一个千万级样本就可能让 R² 从 0.7 跳到 0.9。我更建议在报告里给出四个指标各自说明不同的问题指标公式含义关注点RMSE(log)对数空间下的均方根误差整体误差主指标MAE平均绝对误差万元对离群点更鲁棒MAPE平均绝对百分比误差业务可解释性最强R²方差解释度模型相对均值的好坏RMSE(log) 和 MAE 的区别要能讲清楚RMSE 对大型误差惩罚更重所以 RMSE 明显大于 MAE 时说明预测中存在少量误差很大的样本如果这两个指标接近说明误差分布比较均匀。MAPE 是业务上最直观的——“平均偏差 8%”比“RMSE 0.14”更让人有概念。from sklearn.metrics import mean_absolute_error, r2_score pred_log model.predict(X_test) pred_price np.expm1(pred_log) true_price np.expm1(y_test) rmse_log mean_squared_error(y_test, pred_log, squaredFalse) mae_price mean_absolute_error(true_price, pred_price) mape np.mean(np.abs((true_price - pred_price) / true_price)) * 100 r2 r2_score(y_test, pred_log) print(fRMSE(log){rmse_log:.4f}, MAE{mae_price:.2f}万, MAPE{mape:.2f}%, R²{r2:.4f})注意这里y_test是log_price不能用原始total_price直接和pred_log比较必须统一空间。上报这几个指标时我还会在多跑几次不同随机种子的实验后把指标的均值±方差写进报告比如“RMSE(log)0.142±0.008”这比单个数字可信得多。4.2 超出训练区域的二手房模型还准吗这是课程答辩里最有区分度的问题。很多模型的 CV 误差很好看但把训练时没见过的行政区数据扔进去误差翻倍。原因很直接——district的 target encoding 直接泄漏了区域信息模型记住了“这个区的中位数是多少”而没有学会“什么特征的房子在同区内更贵”。验证方法是留出一个行政区完全不参与训练作为模拟新区域的数据。我叫它“留区验证”本质上是在验证“换一个城市/区域这套流程还能不能用”。districts df[district].unique() test_district [districts[0]] # 留出一个区 train_df df[~df[district].isin(test_district)] test_df df[df[district].isin(test_district)] # 注意target encoding 必须只用训练集拟合 train_df[district_encoded] train_df.groupby(district)[log_price].transform(mean) test_df[district_encoded] test_df[district].map( train_df.groupby(district)[log_price].mean() ) test_df[district_encoded].fillna(np.mean(train_df[log_price]), inplaceTrue)这段代码里fillna处理的是测试集里出现了训练集没有的新区域——模拟的就是“模型第一次见到一个城市”的场景。如果留区验证的误差比普通交叉验证高出 30% 以上报告里就诚实说明模型的区域泛化能力有限然后展示你做了哪些尝试比如去掉区域中位数特征、只用房源本身属性来缓解这个问题。即便最终误差没有降下来“你去验证过”这个动作本身就是加分项。4.3 特征重要性答辩时最好用的一页调完模型后我建议在报告里放两张图一张是 LightGBM 的feature_importance一张是 SHAP 依赖图。第一张看排序第二张看方向。import lightgbm as lgb import pandas as pd importance_df pd.DataFrame({ feature: X.columns, gain: model.booster_.feature_importance(importance_typegain) }).sort_values(gain, ascendingFalse) print(importance_df.head(10))gain的含义是“该特征作为分裂点时带来的平均增益”比默认的split被分裂的次数更能反映真实贡献。如果你的报告里有这一页老师问“为什么面积这么重要”你就能回答面积直接决定房屋总价量级而且面积和卧室数的交互在树模型里会被自动探索所以它在分裂时带来的增益最大。X.columns和训练时的X必须完全一致否则importance的索引对不上。我在做特征重要性分析前会先重新运行一次完整的数据处理流程确保X是最终的版本而不是调参中间过程的副本。5. 交付“源码使用文档”并一键复现实验结果5.1 使用文档的最小结构大作业评分通常分三块代码能跑通、答辩能讲清、结果能复现。前两块的载体都落在使用文档里但大部分同学交的 README 只有两句“运行 main.py 即可”。我的做法是文档固定包含四个模块环境准备列出 Python 版本和核心依赖、数据说明字段含义表和数据来源、运行步骤三个命令预处理、训练、评估、预期结果复现时的指标区间。文档里不要写“效果非常好”这种主观描述写“使用提供的house_data.csv在 commit 对应版本下运行RMSE(log) 在 0.14±0.01R² 在 0.91±0.02”就足够。5.2 锁定环境告别“在我电脑上能跑”机器学习环境配置是大作业提交后最常见的失败原因同一个sklearn版本train_test_split的随机切分逻辑都可能变。提交前必须做两件事第一导出requirements.txt并注明 Python 版本第二写清安装命令。pip freeze requirements.txt python -c import sklearn, lightgbm, pandas; print(sklearn.__version__, lightgbm.__version__, pandas.__version__)pip freeze会把所有包都导出来包括和项目无关的包这不影响复现但文档里建议写明“核心依赖版本如下”把上面三个库的版本号剪贴进去。如果老师用 Anaconda 环境还可以补一句conda list -e conda_env.txt方便对方直接重建环境。机器学习环境配置这件事没有技巧只有锁定版本才可控。5.3 固定随机种子用一条命令从原始数据复现最后一步是把整个流程收拢成一个可复现的脚本从读入原始 CSV 到输出评估指标只运行一次。关键是所有随机操作都要固定种子——train_test_split、KFold、LightGBM 里的random_state缺一个结果就漂移。# 复现脚本核心入口 if __name__ __main__: import argparse parser argparse.ArgumentParser(description房价预测复现) parser.add_argument(--data, defaulthouse_data.csv) parser.add_argument(--seed, typeint, default42) args parser.parse_args() df load_data(args.data) # 数据加载 X, y build_features(df) # 特征工程 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_stateargs.seed ) model train_model(X_train, y_train) evaluate(model, X_test, y_test)这份代码不是把所有逻辑塞进一个文件而是把load_data、build_features、train_model、evaluate分成四个函数各自对应一个模块。使用文档里的“运行步骤”就会变成三条命令python preprocess.py、python train.py、python evaluate.py或者一条python run_all.py --seed 42。随手改一个随机种子跑出两组指标对比是答辩时回答“为什么会得到这个数字”的最有效证据。本文还有配套的精品资源点击获取