
简介一份基于Python的二手房房价数据分析与预测源码包面向有Python基础、正在开展毕设或课设的计算机方向学习者。项目覆盖数据清洗、缺失值与异常值处理、Pandas探索性分析、Scikit-learn回归建模线性回归、决策树、随机森林、交叉验证调参与特征工程可作为完整的数据科学实践案例。压缩包共17个文件包含12个CSV数据文件、3个Jupyter Notebook、1个Word说明文档与1个Python脚本整体约6.22MB文件类型覆盖从数据爬取、分析建模到结果说明的完整链路便于对照学习。目前已有118人学习下载。通过该项目可掌握真实房价数据的处理流程与模型评估方法还能参考实现思路拓展到其他城市或特征场景。1. 基于 Python 的二手房房价数据分析与预测这个源码包到底能解决什么问题“二手房房价预测”这个方向最容易劝退人的不是模型选型而是数据本身。你可能拿到过一份挂牌数据面积字段里混着“建面”和“套内”楼层写着“低层/中层/高层”朝向既有“南”又有“南北通透”同一套房子在不同平台价格能差出十万。这个基于 Python 的源码包把数据清洗、特征构造、模型训练和项目说明打包在一起核心价值不是预测精度多高而是给你一条完整、能照做的链路拿到 CSV、洗干净、建模、评估、换城市再跑。适合刚啃完 Python 基础、想完整走一遍数据项目的学习者也适合需要快速搭一个房价分析 Demo 的从业者。需要提前说清楚模型输出反映的是特征与价格的经验关系不是定价答案。2. 拆开这个 zip源码包的文件结构与运行顺序拿到“基于 Python 的二手房房价数据分析与预测源码项目说明.zip”之后第一步不是打开代码文件而是先看整个目录长什么样。一个能跑通的二手房项目绝大多数会按“数据、代码、文档”三层组织和那种单文件脚本的区别就在这数据清洗和建模分开中间结果可以随时检查出了问题能定位到具体是哪一步。2.1 常见文件布局先认清楚每个路径在干什么用一个表格来看这种项目最常遇到的文件布局拿到手之后你可以直接对照着找。路径作用落地建议data/raw/原始挂牌 CSV 存放处不要手工修改源文件所有清洗逻辑都下沉到代码data/processed/清洗与特征工程后的中间结果每跑一步都检查这个目录确认字段口径是否符合预期src/data_clean.py数据清洗入口重点看干净的数据长什么样而不是看代码有多花哨src/feature_engineer.py特征构造脚本朝向、楼层、区域编号等衍生字段在这里生成src/train_model.py建模与评估脚本模型参数、训练集测试集划分、评估指标都在这里project_readme.md运行步骤与依赖说明先读这个再跑代码避免凭感觉猜字段含义这种组织方式最大的好处是“可审计”。data/raw 里放原始数据data/processed 里放清洗后的结果你在特征工程阶段发现某个字段不对可以直接对比 raw 和 processed 两个目录很快定位是清洗逻辑写错还是原始数据本身有问题。我一般会先跑一遍 data_clean.py然后把 processed 目录打开看一眼再决定下一步动哪个文件。2.2 环境与运行顺序三个脚本依次执行这种源码包通常依赖 pandas、numpy、scikit-learn 三件套可视化场景还会带 matplotlib。依赖一般写在 requirements.txt 里装环境时不要直接在全局环境里装我习惯先建一个干净的 Python 3.8 虚拟环境再按 requirements 安装避免和系统里旧版本的包打架。运行顺序固定为清洗、特征、建模三步。# 在项目根目录下依次执行 python src/data_clean.py --input data/raw/house_list.csv --output data/processed/clean.csv python src/feature_engineer.py --input data/processed/clean.csv --output data/processed/feature.csv python src/train_model.py --input data/processed/feature.csv --output results/model.pkl逻辑上每个脚本只做一件事清洗脚本负责把脏数据变成干净表格特征脚本负责在干净表格上添加模型可用的数值字段训练脚本负责建模并输出评估结果。--input 指读入文件的位置--output 指生成文件的位置把输出路径改掉之后下一个脚本的输入路径也要跟着改否则会报文件找不到。如果你拿到手的包把三个脚本合并成了一个 train.py运行方式通常是 python train.py效果一样但中间结果需要自己在代码里保存。2.3 项目说明文档里最值得先看的三件事第一件事是目标变量定义。这个项目到底在预测挂牌总价还是每平米单价决定了后面所有特征和评估口径。预测总价是用户最常见的诉求但总价受面积影响极大单价的分布更集中两者在特征重要性和误差指标上表现完全不同。第二件事是字段定义表。原始数据里 price 什么单位、build_year 是拿证年份还是建成年份、楼层文本有没有统一格式这些信息你不看文档只能靠猜。第三件事是已知限制。大多数爬下来的二手房数据都不包含学区、地铁距离、装修实际状况文档里如果写了这一点模型的误差上限你心里就有数不会被一两个离群样本带偏。3. 二手房数据清洗从 CSV 到能建模的表格房价预测项目的建模部分其实不难难的是数据清洗。原始挂牌数据基本都带着三个毛病单位混乱、口径不一致、文本字段的写法天差地别。这一章的目标很直接把一份随手拿到的 CSV 变成能让 sklearn 直接消费的干净表格。3.1 二手房数据为什么脏三个高频问题第一个高频问题是单位不统一。面积字段可能同时出现“89.5㎡”“89.5平米”“89.5平”三种写法不统一就会让 pandas 把整个列读成字符串。第二个高频问题是字段口径不一致总价和单价同时存在但不同房源的单价有的是“元/平米”有的是“万元/平米”建模前必须定清楚最终保留哪个。第三个高频问题藏在文本里楼层字段常见的是“总33层/位于12层”但也有“中楼层”“低楼层”这种无法直接转数字的写法朝向则可能有“南北通透”“南向”“朝南”等多种描述实际上指同一个方向组合。清洗的核心思路是“所有字段只保留一种口径”而不是逐条对数据做人工修正。把面积统一成数字平米把朝向合并成方向分组把楼层文本里的数字抽出来变成楼层数然后把明显异常的样本剔除。整个过程要沉淀成代码这样换一份数据、换一个城市跑一遍同一个函数就能复用。3.2 清洗函数面积、朝向、楼层的统一化处理下面这个函数是我在这种项目里的常规写法覆盖了上面三个高频问题外加缺失值和异常范围的兜底。你可以把它当成一个模板按自己数据的实际字段名调整。import pandas as pd import numpy as np def clean_second_hand(df: pd.DataFrame) - pd.DataFrame: 把常见的二手房挂牌 CSV 清洗成统一口径的表格 df df.copy() # 1. 面积去掉单位后缀转成 float解析失败的变成 NaN df[area] ( df[area] .astype(str) .str.replace(㎡, , regexFalse) .str.replace(平米, , regexFalse) .str.replace(平, , regexFalse) .str.strip() .pipe(pd.to_numeric, errorscoerce) ) # 2. 朝向合并高频写法减少 one-hot 之后的维度爆炸 direction_map { 南: south, 南向: south, 朝南: south, 南北: south_north, 南北通透: south_north, 东南: southeast, 西南: southwest, 东: east, 西: west, 北: north, } df[direction_group] df[direction].astype(str).map(direction_map).fillna(other) # 3. 楼层从“总33层/位于12层”这类文本里抽出第一个数字 df[floor_num] df[floor_info].astype(str).str.extract(r(\d))[0].astype(float) # 4. 删除明显冗余或可能导致目标泄漏的列 drop_cols [c for c in [unit_price, link, description] if c in df.columns] df.drop(columnsdrop_cols, inplaceTrue) # 5. 面积 15-300 平米之外的样本视为异常价格缺失的直接丢弃 df df[(df[area] 15) (df[area] 300)] df df.dropna(subset[price, area]) return df逻辑上分五步走先把面积从带单位字符串转成数值再用映射表把朝向归成 7 个方向组然后从楼层文本里抽取数字做后续建模特征接下来删掉单价、链接这些会导致目标泄漏或无语义信息的列最后按业务常识裁剪异常范围。参数上需要留意两个地方一是 errorscoerce 表示无法解析的字符串会变成 NaN之后由 dropna 兜底二是面积范围 15 到 300 是按普通住宅挂牌来设的如果项目是公寓数据这个区间要放宽到 10 到 500否则会误删大量正常样本。注意如果你的原始数据里没有“总33层/位于12层”这种文本只有“低楼层/中楼层/高楼层”三档就不要硬抽数字直接保留分类字段当特征否则会抽出一堆 NaN。3.3 清洗到什么程度算合格肉眼抽检与字段统计清洗函数跑完不算完关键要验证输出是不是真的干净。我会先打印方向分组的频数和每列缺失值数量再做一次人工抽检。前两步能快速发现映射表漏了哪些写法抽检能发现像楼层文本里“半地下”这种正则表达式抓错了的数字。clean_df clean_second_hand(pd.read_csv(data/raw/house_list.csv)) print(clean_df[direction_group].value_counts()) print(clean_df.isna().sum()) print(clean_df.head(50).to_string())这段代码的意图不是跑通流程而是建立一条验证基线。value_counts 的输出里如果 other 这一类的占比超过 20%说明方向映射表漏掉了本地的常见写法需要回去补表isna().sum() 的输出里area、price、floor_num 这几列应该是 0 或接近 0head(50) 则是给你自己看的人工快速扫一遍面积、楼层、朝向确认没有出现“面积 999”“楼层抽取成 3 位数”之类的明显错误。数据清洗做到这一步后面建模才有意义否则再好的模型也是在垃圾数据上做文章。4. 特征工程与模型选择先线性回归做基准再用随机森林拿结果清洗完之后进入建模阶段。这个项目的常规做法是先上线性回归用它的稳定表现当基线再上随机森林捕捉非线性关系。两个模型各有各的位置不要上来就直接堆复杂模型。4.1 为什么选这两个模型定位对比模型适合场景主要风险在这个项目里的定位线性回归特征与目标近似线性关系对异常值敏感容易预测出负价格基准线用于判断特征是否合理随机森林特征间存在非线性关系树过多时过拟合预测波动大主力模型输出稳定且可解释线性回归在这里的价值不是拿最终结果而是用来“验特征”。如果线性回归在某个特征组合上的误差低得离谱那大概率是出现了数据泄漏而不是特征真的这么强。随机森林因为能捕捉交互关系通常在房价这种特征非线性明显的任务上表现更好但要注意它的预测结果是训练样本的加权平均极端组合可能被平滑掉。4.2 构造模型直接可用的特征列原始数据里的朝向、区域、楼龄都是文本或原始数值需要转成模型能吃的形式。我一般会把朝向和区域各自编码成类别码楼龄按当前年份减建成年份计算。from datetime import datetime current_year datetime.now().year def build_features(df: pd.DataFrame) - pd.DataFrame: df df.copy() # 楼龄当前年份减建成年份负值直接截成 0 df[age] (current_year - df[build_year]).clip(lower0) # 朝向类别编码把字符串映射成整数 df[direction_code] df[direction_group].astype(category).cat.codes # 区域类别编码商圈或行政区名映射成整数 df[district_code] df[district].astype(category).cat.codes return dfage 直接使用当前年份会让结果受时间影响但这个项目做的是截面数据分析不是时间序列预测所以问题不大。direction_code 和 district_code 是用 pandas 的 category 类型自动映射省去了手工维护字典的麻烦。要留意的是如果后面换了新城市的数据district 名字集合变了旧模型就没有办法直接迁移需要重新训练。4.3 训练脚本线性回归和随机森林的对照实验下面这段代码把两个模型放在同一个数据切分上做对照是这类源码包里最常见的主训练逻辑。评估指标用 MAE 和 RMSE 一起看。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error feature_cols [area, age, floor_num, direction_code, district_code] X df[feature_cols] y df[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr LinearRegression() lr.fit(X_train, y_train) lr_pred lr.predict(X_test) rf RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf3, random_state42, n_jobs-1, ) rf.fit(X_train, y_train) rf_pred rf.predict(X_test) for name, pred in [(linear, lr_pred), (random_forest, rf_pred)]: print(name, MAE:, mean_absolute_error(y_test, pred)) rmse mean_squared_error(y_test, pred, squaredFalse) print(name, RMSE:, rmse)先运行线性回归它能给出一个误差基线再跑随机森林如果随机森林的 MAE 没有明显低于线性回归说明特征里没有足够的非线性信号问题多半出在特征工程上而不是模型选择上。几个关键参数解释一下random_state42 固定了数据切分和模型随机性保证不同人复现的结果一致n_estimators300 是随机森林的树数量太少预测不稳定太多训练时间增加但收益递减max_depth12 限制每棵树的深度防止树记住单个样本min_samples_leaf3 保证叶子节点至少有三个样本减少极端预测值n_jobs-1 让所有 CPU 核心并行训练。如果你的 sklearn 版本较新跑上面的代码会提示 squared 参数弃用把 mean_squared_error(..., squaredFalse) 换成 mean_squared_error(..., rootTrue) 即可结果不受影响。4.4 特征重要性不要只看分数还要看业务逻辑随机森林训练完之后我会顺手打印特征重要性这一步在源码包里通常放在训练脚本末尾。它能快速帮我们发现特征构造是否合理。import pandas as pd importance pd.Series(rf.feature_importances_, indexfeature_cols) print(importance.sort_values(ascendingFalse))特征重要性的业务逻辑一般是面积、楼龄、区域排在前三楼层和朝向的权重依城市而定。如果出现 direction_code 排第一或者 age 排第一而面积排到最后那基本可以断定特征编码出了问题比如 area 列在清洗时被错误转成了字符串或者朝向编码泄露了什么额外信息。特征重要性适合当报警器用不适合当精确解释工具。5. 避坑手册本地复现最容易翻车的 5 个场景看完前面几章直接把代码跑起来大概率会遇到几个经典问题。这一章按“现象、原因、解决”的方式写了 5 个我在类似项目里见过最多的情况不是理论推演都是实际翻过车才总结出来的。5.1 测试集里混进同小区房源MAE 好看换城市就崩现象用 train_test_split 随机切分数据MAE 看起来非常低模型似乎很准但把同样的代码套到另一个城市的数据上误差立刻翻倍。原因同一个小区的挂牌房源之间强相关随机切分会让同一小区的一部分样本进训练集、另一部分进测试集模型相当于“见过”这个小区的大致价格区间测试分数虚高。解决按小区做分组切分把整个小区的房源全部放进同一侧。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsdf[community])) X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y.iloc[train_idx], y.iloc[test_idx]GroupShuffleSplit 的 groups 参数传小区名字列它会保证同一个小区不会同时出现在训练集和测试集里。这样评估出的 MAE 才是模型面对“陌生小区”时的真实水平。group 切分的代价是训练集变小但换来了评价结果可信我一般优先选它。5.2 预测结果显示的是单价却不是总价目标变量口径错了现象模型跑完没有报错输出结果一对比发现数值全是每平米单价和项目说明里的目标变量对不上。原因清洗阶段删列顺序写错把 price 总价列删了却把 unit_price 单价列留到了最后目标变量实际建模时的口径是单价。解决在清洗脚本里明确定义目标变量单独抽出来存成一个 Series再检查最终保留的列。y df[price].copy() assert price in df.columns and unit_price not in df.columns print(y.head())这个断言看起来很基础但它能拦截掉一大批因为列名重复或字段名相似而导致的低级错误。我在第一次做类似项目时就因为字段命名类似花了半天才意识到模型拟合的是单价而不是总价。5.3 朝向的 one-hot 维度爆炸映射表不完整现象方向分组后的 value_counts 显示有 20 多个取值训练速度明显变慢特征重要性也分散得没有规律。原因映射表只覆盖了最主流的写法本地房源常见“西南北”“南北东”“其他朝向”等组合全部落进了 other 以外的零散类别。解决把低频类别统一归并频率低于阈值的全部改成 other。freq clean_df[direction_group].map(clean_df[direction_group].value_counts()) clean_df[direction_group] clean_df[direction_group].where(freq 50, other)阈值 50 按数据量调整我习惯用“样本总量 * 1%”作为阈值低于这个比例的方向组没有统计意义保留只会增加噪声。5.4 模型输出了负价格异常值处理没做好现象预测结果里出现负值比如 -12 万完全不符合业务常识。原因线性回归对极端样本敏感价格分布右偏少部分豪宅样本直接把回归超平面拉歪了。解决对目标变量做 log1p 变换再训练模型评估时用 expm1 还原。import numpy as np y_log np.log1p(y) # 训练与预测全部使用 y_log最后还原 y_pred_original np.expm1(rf.predict(X_test))log1p 会把价格压缩到线性可建模的范围豪宅样本的影响力被大幅削弱。还原之后误差会略放大但模型不会再产出负价格这种荒谬结果。如果在真实项目中看到负价格别去调模型参数先回清洗阶段看是不是有异常值没处理干净。5.5 楼层字段里藏着总层数特征泄漏的经典案例现象验证分数高得异常模型上线后一检查发现预测高度依赖楼层特征但业务上楼层对房价影响没那么大。原因楼层原始文本是“总 33 层/位于 12 层”正则抽出数字时把“33”抽了出来模型等于拿到了总层数这个建筑属性。解决特征只保留所在楼层占总层数的比例或者干脆转成“低、中、高”三档。df[floor_ratio] df[floor_num] / df[total_floor] # 或者使用三档分类 df[floor_level] pd.cut(df[floor_ratio], bins[0, 0.33, 0.66, 1.0], labels[low, mid, high])这样既保留楼层位置的横向对比价值又避免了总层数这一信息的直接泄漏。楼层特征只在高楼层密集的小区里才有明显区分度在多数城市它属于弱特征不值得为它引入泄漏风险。6. 验证与迁移交叉验证、换城复跑与一个收尾习惯项目跑通只是开始真正有价值的是知道模型“到底有多可信”以及换一份数据之后还能不能稳定复现。这一章给三个步骤用交叉验证替代单次切分把项目迁移到新城市以及一个我坚持了很久的收尾习惯。6.1 用交叉验证替代单次切分单次切分的结果太依赖随机种子换一个 random_state 误差可能差出好几万。五折交叉验证的做法是让每一条样本都有机会出现在测试集里最终报告的误差是五轮结果的平均值稳定得多。from sklearn.model_selection import KFold, cross_validate def evaluate_model(model, X, y, cv5): kf KFold(n_splitscv, shuffleTrue, random_state42) scoring { mae: neg_mean_absolute_error, rmse: neg_root_mean_squared_error, } results cross_validate(model, X, y, cvkf, scoringscoring) return { MAE: -results[test_mae].mean(), RMSE: -results[test_rmse].mean(), }scoring 里 sklearn 的负号前缀表示“负值越大代表误差越小”所以返回值要手动取负。如果你的 sklearn 版本低于 0.24不支持 neg_root_mean_squared_error就把它拆成两个单独 scoring 项或者只用 neg_mean_absolute_error。6.2 迁移到新城市的三个改动点复制整个项目到一个新城市的数据上不是换一个 CSV 路径就行。第一区域编码的映射表要换成目标城市的商圈或行政区域列表旧映射表里的 district_code 毫无意义第二朝向映射表大概率能保留但建议重新跑一次清洗后的 value_counts确认 other 占比没有因为城市差异暴涨第三目标变量和评估口径要重新审视不同城市房价基数差异大可以把 MAE 换成 MAPE平均绝对百分比误差来对比模型在不同城市间的相对表现。6.3 收尾习惯人工抽检 50 条再信模型模型评估再全面也不如人工看一眼结果来得安心。我做完清洗和建模之后一定会把测试集里的预测值和真实值并排打印 50 条肉眼对比。这一眼能发现模型系统性低估或者高估的区间比如大面积户型全部预测偏低或者高层房源被随机森林过高估值。有一次我在某城市复跑项目模型各项指标正常唯独某一栋楼的房源全部预测错误最后定位到楼层字段里的“半地下”被正则抓成了数字导致面积和楼层完全不匹配。这类问题靠交叉验证发现不了只有人眼扫过前几十行才看得出异常。看完 50 条再决定要不要调特征、换模型或砍样本别让评估指标成为唯一的判断依据这是我在这类项目里最想强调的一个习惯。希望帮到你。本文还有配套的精品资源点击获取