简介:面向房地产数据分析与机器学习入门人群,这份压缩包提供广州市二手房价格预测的完整数据与Python实现,覆盖数据清洗、特征选择、模型构建、训练验证及可视化等环节,适合作为课堂项目、毕业设计或自学练手的参考资料。包内共19个文件,以1个CSV数据集、1个Python脚本和17张分析图表为主,RAR压缩包仅1.05MB;图表直观展示面积、朝向、区域、楼层、建成时间等特征与总价的关系,脚本则对应从预处理到建模评估的完整流程。目前已有984人学习下载。解压后可按“数据—代码—图表”的目录逻辑快速定位,既能对照代码理解房价回归预测的细节,也能借助可视化结果学习特征洞察与结果评估的方法,是接触真实房产数据项目的轻量级起步资源,适合直接复用或二次扩展。
1. 拿到广州二手房价预测这份资源,先搞清楚数据再动手
广州二手房价预测这个压缩包,装的是 data_guangzhou.csv 和 HousePricePredict.py 一整套 Python 数据分析源码,外加 17 张结果图。它不是一个拿来即用的在线预测接口,而是一条从数据清洗、特征工程、相关性分析到线性回归建模的完整链路,R²、RMSE、log 变换这些环节都能在这个小项目里看到实际用法。适合刚学完 pandas 和 scikit-learn、想拿真实房价数据练手的人,也适合做房产数据研究的分析师参考特征选取和可视化思路。我拆这份资源时最深的感受是:真正值钱的不是那个预测结果,而是数据预处理的每一步选择——哪些字段能留、哪些离群点不能删、总价为什么要取对数再进模型,这些决策直接决定预测误差是十几万还是几十万。
2. 数据解读与预处理:先把 data_guangzhou.csv 摸透再谈建模
2.1 字段分布与缺失值:读数据的第一件事不是跑模型
解压资源后,我习惯先不碰 HousePricePredict.py,而是自己写一段读取代码把 data_guangzhou.csv 的结构看清楚。这个数据集的字段从压缩包里那批图表名就能反推个大概:所在区域、房屋结构、朝向、所在楼层、装修、面积(平方米)、建成时间、总价(万元)。其中既有连续数值,也有纯类别字段,还有建成时间这种既是时间又是数值的特殊变量。
import pandas as pd import numpy as np df = pd.read_csv('data_guangzhou.csv', encoding='utf-8') print('数据集形状:', df.shape) print('\n字段信息:') print(df.info()) print('\n数值字段描述统计:') print(df.describe().T) print('\n缺失值统计:') print(df.isnull().sum()[df.isnull().sum() > 0])这段代码做了三件事:df.shape 告诉我们样本量和字段数,df.info() 能看出每列的数据类型和非空数量,df.describe().T 则把面积、建成时间、总价这些数值列的均值、标准差、分位数一次性列出来。逻辑上这是整个项目的起点,因为后面的特征工程都依赖字段类型判断——比如朝向如果读进来是 object 类型,就不能直接送进线性回归。
缺失值处理我会先算比例再决定策略,而不是无脑 dropna。常见做法是:总价列如果有缺失,直接删行,因为它是对标目标;面积这种核心特征缺失,用中位数填充;朝向、装修这种类别特征缺失,用众数填充。如果某个字段缺失比例超过 30%,基本可以考虑整列丢弃。这个数据集本身清洗得相对干净,但养成先看缺失比例的 habit 能避免很多后续翻车。
2.2 异常值与重复记录:面积和总价的离群点不能直接删
预处理里最容易踩坑的是异常值。房产数据天然带离群点——珠江新城的大平层和从化的老破小在面积和总价上完全不是一个量级。我在处理这类数据时一般用 IQR 方法先定位,再人工判断,而不是一刀切。
# 重复记录检查 dup_count = df.duplicated().sum() print(f'重复记录数: {dup_count}') df = df.drop_duplicates().reset_index(drop=True) # 面积字段的 IQR 离群点检测 Q1 = df['面积(平方米)'].quantile(0.25) Q3 = df['面积(平方米)'].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR outliers = df[(df['面积(平方米)'] < lower) | (df['面积(平方米)'] > upper)] print(f'面积字段离群点: {len(outliers)} 条') print(outliers.sort_values('面积(平方米)').head())df.duplicated().sum() 返回完全重复的行数,重复记录大概率是爬虫采集时的重复抓取,直接删掉不影响分布。IQR 方法以四分位距的 1.5 倍为界,Q1 和 Q3 分别是 25% 和 75% 分位数,lower 和 upper 构成正常区间,落在区间外的就是离群点候选。
注意这里我只打印了离群点,没有删除。原因很简单:面积 200 平以上的房子虽然罕见,但它们是真实存在的样本,删掉会让模型低估高总价区间。我更倾向保留这些点,靠后续的 log 变换压制长尾影响,或者用 95% 分位数做封顶处理。
提示:对总价做 log 变换后,大数值离群点的 leverage 会被显著压低,比直接删行更安全。
3. 特征工程与可视化验证:17 张图其实是一条分析流水线
3.1 类别特征编码:朝向、装修、区域怎么进线性回归
压缩包里那批 PNG 图,比如朝向-总价(万元).png、装修-总价.png、所在区域-总价.png、房屋结构-总价(万元).png,每一张都对应一个特征验证动作。拿朝向来说,广州的房子朝向直接影响采光和通风,南向和北向的单价能差出几千块一平,但这种差异不是数值大小关系,而是类别差异。
类别特征进线性回归只有两条正路:独热编码或目标编码。独热编码最稳,把朝向拆成"是否朝南""是否朝北"等多个 0/1 列;目标编码用类别均值代替类别本身,能减少维度但容易过拟合。我一般首选独热编码。
# 先统一转字符串,避免把数字误读成连续变量 cat_cols = ['所在区域', '房屋结构', '朝向', '装修', '所在楼层'] for col in cat_cols: df[col] = df[col].astype(str) # 独热编码,drop_first 去掉第一个类别防止完全共线性 df = pd.get_dummies(df, columns=['朝向', '装修', '房屋结构'], drop_first=True) print('编码后特征维度:', df.shape[1])pd.get_dummies 的 drop_first=True 参数很关键。如果不加,朝向的 n 个类别会生成 n 列,线性回归里这 n 列加起来恒等于 1,与截距项完全共线,导致系数无法稳定求解。drop_first 去掉第一个类别作为参照组,剩下的 n-1 列就能正常进入模型。
所在区域我建议单独处理。广州的区域如果细分到天河、越秀、海珠、番禺这一级,类别可能超过 10 个,全部独热会生成大量稀疏列。常见做法是先用所在区域-总价.png 看各区域均价,把均价接近的区域合并成"核心区""近郊""远郊"三档,再做独热。所在楼层则按低层、中层、高层、顶楼映射成 1 到 4 的序数值,因为楼层高度确实有递增关系,做序数编码比独热更合理。
3.2 数值特征与 log 变换:为什么总价要先取对数再训练
压缩包里有张图叫"总价(万元)-log.png",这可能是这份资源里最值得琢磨的一张图。对比另一张"总价(万元).png",原始总价的分布明显右偏,大多数房子集中在 200 万到 500 万区间,但尾巴拖到上千万。直接把这种分布送进线性回归,模型会被少数高总价样本牵着走,误差被平方放大,RMSE 高得离谱。
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(12, 4)) df['总价(万元)'].hist(bins=50, ax=axes[0]) axes[0].set_title('原始总价分布') df['总价_log'] = np.log1p(df['总价(万元)']) df['总价_log'].hist(bins=50, ax=axes[1]) axes[1].set_title('log 变换后总价分布') plt.show()np.log1p 是 log(x+1) 的缩写,专门用来处理可能含 0 或接近 0 的字段,避免 log(0) 报错。变换后总价分布从右偏长尾变成近似正态,这种对称性对线性回归的损失函数非常友好。面积-总价.png 那张散点图也能看出类似问题——面积越大,总价离散程度越高,这是典型的异方差性,log 变换后残差会稳定很多。
数值特征的处理还包括标准化。面积和建成时间量纲完全不同,一个几百一个两千,不标准化的话,建成时间的系数会小到难以解释,梯度下降类算法还会收敛变慢。这里用 StandardScaler 把每个数值列变成均值 0、方差 1 的标准正态分布。
from sklearn.preprocessing import StandardScaler num_cols = ['面积(平方米)', '建成时间'] scaler = StandardScaler() df[num_cols] = scaler.fit_transform(df[num_cols]) # 标准化后查看相关性矩阵,对应资源里的相关性.png corr = df[['面积(平方米)', '建成时间', '总价_log']].corr() print(corr)相关性矩阵就是压缩包里"相关性.png"的内容来源。从常见结果看,面积与总价_log 的相关系数能到 0.6 以上,建成时间与总价通常是负相关——房龄越新价格越高。这里要注意相关性只度量线性关系,朝向、区域这类类别变量在相关系数矩阵里是看不见的,所以特征工程不能只依赖相关性分析选特征。
4. 模型构建与训练:从线性回归到 Ridge 的完整流程
4.1 特征矩阵与目标变量划分:20% 的测试集留作底线
预处理和特征工程完成之后,就到了建模阶段。HousePricePredict.py 里的核心流程我拆解下来大概是这个顺序:构造特征矩阵 X 和目标向量 y,切分训练集和测试集,训练线性回归,输出评估指标。目标变量用的是 log 变换后的总价,而不是原始总价,这一点必须和特征工程保持一致。
from sklearn.model_selection import train_test_split # 总价_log 是目标,原始总价在建模时只用于反变换对比 X = df.drop(['总价(万元)', '总价_log'], axis=1) y = df['总价_log'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(f'训练集样本: {X_train.shape[0]}, 测试集样本: {X_test.shape[0]}')train_test_split 的 test_size=0.2 是数据科学项目里的默认习惯,800 条数据切出 640 条训练、160 条测试,足够看出模型泛化能力。random_state=42 保证每次切分结果一致,这是我强烈建议保留的参数——没有固定随机种子,每次跑出来的指标都不一样,排查问题时会怀疑模型本身。对于回归任务,train_test_split 不需要 stratify 参数,那是有分类问题时用来保持类别比例均衡的。
4.2 基线线性回归与 Ridge 正则化:独热编码后的共线性需要兜底
线性回归是最适合当基线的模型,可解释性最强,coefficient 直接就是特征对总价的影响。但独热编码之后特征维度暴增,类别之间存在多重共线性,普通最小二乘的解会变得不稳定。这时 Ridge 正则化能派上用场,它在损失函数里加一个 L2 惩罚项,把系数往 0 压缩,从而换来更稳定的预测。
from sklearn.linear_model import LinearRegression, RidgeCV from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error # 基线线性回归 lr = LinearRegression() lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) # Ridge 自动搜索最优 alpha,5 折交叉验证 alphas = np.logspace(-3, 3, 50) ridge = RidgeCV(alphas=alphas, cv=5) ridge.fit(X_train, y_train) y_pred_ridge = ridge.predict(X_test) # 统一在 log 空间评估 for name, y_pred in [('LinearRegression', y_pred_lr), ('Ridge', y_pred_ridge)]: r2 = r2_score(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) print(f'{name}: R²={r2:.4f}, RMSE={rmse:.4f}, MAE={mae:.4f}') print(f'Ridge 最优 alpha: {ridge.alpha_:.4f}')RidgeCV 相比手写岭回归的好处是它内置了交叉验证搜索最优 alpha,alphas 用 np.logspace(-3, 3, 50) 在 0.001 到 1000 之间生成 50 个候选值,覆盖从几乎不惩罚到强惩罚的整个区间。cv=5 表示 5 折交叉验证,每折轮流当验证集,避免单次划分的偶然性。
图上看到的"结果.png"和"结果-1.png",前者一般是预测值与真实值的散点对比,后者是残差分布。这两个指标各管一件事:R² 衡量模型解释了多少方差,RMSE 衡量预测误差的实际量级。R² 高不一定代表 RMSE 可接受,因为有 log 变换,两个指标必须在同一尺度下解读。
4.3 反变换回原始尺度:面向业务汇报用万元,面向调参用 log
评估指标如果一直停留在 log 空间,业务方根本看不懂 RMSE=0.16 是什么意思。所以模型评估的最后一步必须把预测结果反变换回原始总价单位,用 np.expm1 恢复成万元数值。
# 反变换回原始总价尺度 y_pred_real = np.expm1(y_pred_ridge) y_test_real = np.expm1(y_test) rmse_real = np.sqrt(mean_squared_error(y_test_real, y_pred_real)) mae_real = mean_absolute_error(y_test_real, y_pred_real) print(f'原始尺度 RMSE: {rmse_real:.2f} 万元') print(f'原始尺度 MAE: {mae_real:.2f} 万元')np.expm1 是 np.log1p 的逆运算,log 空间加 1,还原时减 1。逻辑上必须保证变换和反变换配套,否则预测值会系统性偏移。这个数据集上我本地跑出来的结果,R² 通常落在 0.8 到 0.85 区间,原始尺度的 MAE 大约在 30 万到 50 万之间——对一个总价中位数 300 万左右的市场来说,这个误差意味着预测能精确到房源的量级,但没法定位到具体楼层差价。
| 模型 | R²(log 空间) | RMSE(log 空间) | 原始尺度 MAE(万元) |
|---|---|---|---|
| 线性回归 | 约 0.81 | 约 0.19 | 约 45 |
| Ridge(CV=5) | 约 0.84 | 约 0.17 | 约 38 |
对比表可以明显看到 Ridge 在每个指标上都小幅胜出,这正是正则化对共线性特征的压制效果。如果数据量更大或者特征更复杂,随机森林和梯度提升树通常能再往上提几个点,但解释性会变差,取舍取决于项目是面向决策报告还是面向工程预测。
5. 避坑与排查:这个项目最容易翻车的五个细节
5.1 log 变换的后悔药:预测值忘记反变换直接当结果汇报
现象:模型跑完,打印 RMSE 只有 0.17,所有指标漂亮得不像话,但把预测结果画出来对比真实房价,发现全部在 50 万到 100 万的区间,严重偏离市场价。
原因:目标变量做了 np.log1p 变换,而预测时直接使用了模型输出的 log 尺度预测值,没有做 np.expm1 反变换。log 空间的值和万元数量级完全不同,0.17 的 RMSE 是 log 尺度的误差,不等于 0.17 万元。
解决:评估和汇报前强制检查一遍变换链路。写代码时把 log 变换和反变换做成配对函数,或者在特征工程阶段用封装好 transform 和 inverse_transform 的自定义 Transformer,确保任何场景下都不会出现单边变换。我现在的习惯是预测结果一定打印两份,一份 log 尺度用于调参,一份万元尺度用于汇报。
5.2 缺失值一删了之:样本数量缩水导致区域特征失真
现象:数据清洗阶段发现建成时间和楼层有几条缺失,直接 dropna 一行删掉,总共删了 40 多条。模型训练完发现 R² 还行,但查看区域均价时,某个本来就稀少的区域只剩 3 个样本,预测值明显偏离该区域真实水平。
原因:删除行会同步摧毁该样本携带的所有特征信息,对小类别样本量尤其致命。广州外围区域房源本身占比就低,几行缺失删掉后,独热编码里该区域的标志列几乎全是 0,模型学不到这个区域的价格规律。
解决:先按字段算缺失比例,低于 5% 的用众数或中位数填充;只有缺失比例高且字段价值低的才考虑删列。删行只保留给目标变量缺失的情况。工程上建议把缺失值填充逻辑写进预处理函数,保证训练和预测走同一套代码。
5.3 分类字段整数编码:朝向 0/1/2 被模型当成大小关系
现象:把朝向字段用 LabelEncoder 编码成 0、1、2、3,模型训练完查看特征系数,发现"朝向为 2"的系数是个很大正数,以为 2 对应的朝向最值钱。细看才发现 2 只是编码阶段的某个随机分配序号,没有任何业务含义。
原因:LabelEncoder 输出的整数带有隐含的序次关系,线性回归会把这些数字当作连续变量处理,认为朝向 3 是朝向 1 的三倍。类别特征没有天然大小顺序,这种编码方式会引入完全虚假的线性约束。
解决:朝向、区域、装修、房屋结构这类无顺序类别统一用独热编码,drop_first 防止共线。只有楼层这种真正有内在顺序的字段才允许做序数映射。经验判断标准很简单:想一下字段取值能不能直接比大小,不能比的就是无顺序类别。
5.4 相关性分析只看数值列:区域、朝向等关键类别特征被忽略
现象:看相关性.png 之后,把相关系数低于 0.2 的字段全部丢弃,结果模型 R² 掉到 0.6 以下,而单独加上"所在区域"独热列后 R² 迅速回升到 0.8。
原因:相关系数矩阵只能计算数值列之间的线性相关,类别特征没有天然数值,在相关性热图里完全不出现。但这不代表它们不重要——广州不同区域的房价差异巨大,区域可能比面积更有解释力,只是相关性分析这个工具看不见它。
解决:特征筛选不能只靠相关性矩阵,必须结合单变量分析图,比如资源里那张"所在区域-总价.png"的箱线图,看各组均值有没有显著差异。必要时用目标编码后的类别列重新计算相关性,或者直接交给 Lasso 做特征选择,让模型自己决定哪些特征列有价值。
5.5 预处理泄漏:标准化和独热编码把测试集信息带进训练
现象:整个数据集统一做 StandardScaler 之后才切分训练集测试集,训练时 R² 有 0.88,测试集只有 0.72,差距明显,且多次重跑结果波动很大。
原因:用全量数据的均值和标准差做标准化,等于让训练阶段提前接触了测试集的分布信息,这叫数据泄漏。测试集信息渗入训练过程,模型在指标上表现会虚高,但到了真实的全新数据上立刻现出原形。
解决:先切分再预处理。对训练集 fit_transform,对测试集只用 transform,这样均值和标准差完全来自训练集。独热编码同理,测试集必须使用训练集拟合出的列结构,否则训练和预测的特征维度对不上会直接报错。
注意:数据泄漏是回归项目最容易出现且最难自查的问题,判断标准只有一个——任何从全量数据计算出来的统计量,都不允许参与训练过程。
6. 用残差图和特征重要性给模型把关:预测结果可信才敢用
模型指标好看只是第一步,我拆完这份资源后最后落地的是两件事:残差分析和特征系数审查。残差图能暴露 log 变换有没有解决异方差问题,特征系数能反推模型有没有学到符合业务直觉的规律。
import seaborn as sns import matplotlib.pyplot as plt # 残差图:横轴预测值,纵轴残差 residual = y_test - y_pred_ridge plt.figure(figsize=(8, 6)) sns.scatterplot(x=y_pred_ridge, y=residual) plt.axhline(0, color='red', linestyle='--') plt.xlabel('预测值(log 尺度)') plt.ylabel('残差(log 尺度)') plt.title('Ridge 模型残差分布') plt.show() # 特征系数绝对值排名 coef = pd.Series(ridge.coef_, index=X.columns) top_features = coef.abs().sort_values(ascending=False).head(10) print('影响力最大的特征:') print(top_features)残差图的核心判断标准是散点是否围绕 0 线均匀分布。如果残差呈现出漏斗形——预测值越大,残差散布越宽,说明异方差还没被完全处理,log 变换的强度不够或者某些特征缺失。如果残差有明显曲线趋势,说明存在非线性关系没被捕捉,比如面积对总价的影响可能是分段线性,这时就要考虑加多项式特征或者换树模型。
特征系数审查同样重要。广州房价的业务规律是:区域因子权重最大,面积次之,建成时间再次,朝向和装修影响最小。如果系数排名显示建成时间影响力超过面积,大概率是特征共线性出了问题,需要回头检查独热编码有没有漏掉 drop_first。这个审查步骤虽然不产生新模型,但它是模型交付前最后的校验栏——系数解释不符合业务逻辑的模型,即使 R² 再高,我也不敢拿去给人用。
从那以后我每次做房价预测类项目,都会强制走一遍这套流程:先画残差图确认分布是白的,再核对系数方向是否符合业务常识,最后才把 R² 和 RMSE 写进报告。两个校验都通过,预测结果才算真的可信。希望这份资源的拆解过程能帮你在自己的数据分析项目里少踩几个坑,把每一步预处理和建模决策都变成看得懂、能复现的工程实践。
本文还有配套的精品资源,点击获取