📊 第11章 二手房房价分析与预测系统
📄 一句话概括

21 个特征、1184 条样本,R 方 0.9422、RMSE 15.21 万、MAPE 7.72%。 只用面积一个特征,R 方就有 0.6998——分数高是因为特征好用,不是模型厉害。 估价 161.88 万,区间 146.66 到 177.09 万;随机切而不是按时间切。

📝 本章和前面几章的关系

第 10 章用一列广告费预测销售额,这一篇用二十一列特征预测总价。 模型还是那个线性回归,变的是特征工程的份量——这一篇的重点全在特征上。

一、特征怎么选:先划清谁能进 X

要预测的是总价,所以总价是 y,不能进 X。这一条错了,模型会给出 100% 的准确率,然后毫无用处。 单价也得踢掉。它和总价是一回事,放进 X 等于把答案抄进去。 留下这些:面积、房龄(连续),楼层、装修、区域、户型(分类)。

分类变量不能直接进模型,得先变成 0/1 哑变量,get_dummies 一句搞定。 drop_first=True 去掉每类第一档,避免「几档加起来必然等于 1」造成的完全共线。 结果是 6 列变 21 列。特征列数比原始字段多,是正常现象,别以为是哪里写错了。

二、随机切:和第 10 章正好相反

1184 条样本,训练 947 条、测试 237 条,八成对两成。 这里可以随机切,因为房价数据没有时间先后。第 10 章那是日销售流水,一切就漏未来。 同一个操作,两个项目里的做法正好相反。判断依据不是习惯,是「数据有没有时间序」。

有时间的(销售流水、股价、气象)必须按时间切;没时间的(房源、用户、商品)随便切。 切之前先设 random_state。不设的话每次跑出来的分数都不一样,你会以为模型在抽风。

三、四个指标,外加一个对照

指标 这次的值 怎么读
R 方 0.9422 解释了多少波动
RMSE 15.21 万 均方根误差,罚大错
MAE 10.18 万 平均差多少
MAPE 7.72% 平均差百分之几

R 方 0.9422,比第 10 章那个 0.5005 高得多。但这不是模型变强了。 只用面积一个特征重跑一遍,R 方是 0.6998。一列就解释掉七成的波动。 剩下那 0.24,是区域、装修、房龄、楼层一起挣来的。

这个对照一定要做。不做,你会把「特征好用」记成「模型厉害」,下次换数据就翻车。 MAE 10.18 万,相对一套百来万的房子,误差接近一成;MAPE 7.72% 说的也是这件事。

四、加个岭回归对照

套一个 StandardScaler 加 Ridge(alpha=1.0) 的流水线,重跑一次。 R 方 0.9419、RMSE 15.25 万、MAPE 7.72%——和线性回归几乎一模一样。 这是正常的:特征不多、也不高度共线,岭回归那根缰绳根本用不上。

岭回归不是万灵药。它的用处是「特征多、共线重」的时候稳住系数,不是无条件提分。 加了标准化之后系数能横向比大小,这是岭回归流水线顺带的好处。

五、估一套房:点估计加区间

城东、89.5 平米、房龄 6 年、中楼层、精装、2 室 2 厅,模型估价 161.88 万元。 给区间:上下各留一个 RMSE,146.66 到 177.09 万。一套房差 10 万很正常,报一个数就是在骗人。 换算成单价:161.88 万除 89.5 平米,合 18086 元每平米,落在城东 16577 的均线上方。

高出来的部分能解释:房龄只有 6 年,还是精装,两样都加分。 模型给的不是市场价,是这份数据里的「合理价」。 实际挂牌还要看楼层朝向、小区品质、业主心态,模型一个都不知道。

六、系数怎么读,以及这一章的小结

面积那一项是 1.4740,意思是每多一平米,总价多 1.4740 万元,也就是 14740 元。 最负的三个:区域_郊县 -78.903、区域_经开 -51.504、区域_城北 -36.706,单位都是万元。 最正的三个:装修_精装 6.006、区域_城中 18.552、装修_豪华 25.288。

系数的单位和特征尺度绑在一起,面积每平米加多少万,和「郊县比基准区少多少万」不能直接比大小。 想比重要性得先标准化。不标准化就比系数,是这一章最容易犯的错。 六篇走完:概述定问题、图表成模块、清洗靠规则、分组看结构、模型看特征。

第 10 章的重点是「怎么评分」,这一章的重点是「怎么选特征」。两件事凑齐,一个预测项目才算完整。

七、动手 Demo:二手房房价预测

选特征、转哑变量、随机切、线性回归加岭回归对照,最后估一套房并读系数。 第三节那个「只用面积」的对照是本篇的重点,别看漏。

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("二手房数据_clean.csv")
X = pd.get_dummies(df[ ["面积", "房龄", "楼层", "装修", "区域", "户型"]],
                   drop_first=True)
y = df["总价"].values
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)

lr = LinearRegression().fit(Xtr, ytr)
pred = lr.predict(Xte)
print("特征 %d 列,训练 %d,测试 %d" % (X.shape[1], len(Xtr), len(Xte)))
print("R2 %.4f  RMSE %.2f  MAE %.2f  MAPE %.2f%%"
      % (r2_score(yte, pred), mean_squared_error(yte, pred) ** 0.5,
         mean_absolute_error(yte, pred),
         np.mean(np.abs((yte - pred) / yte)) * 100))

r2a = r2_score(yte, LinearRegression().fit(Xtr[ ["面积"]], ytr)
               .predict(Xte[ ["面积"]]))
print("只用面积,R2 %.4f" % r2a)

rd = make_pipeline(StandardScaler(), Ridge(alpha=1.0)).fit(Xtr, ytr)
print("岭回归 R2 %.4f" % r2_score(yte, rd.predict(Xte)))

实跑输出:

--------------------------------------------------------------------
二手房房价预测
--------------------------------------------------------------------

  ① 特征怎么选
  --------------------------------------------------------------------
  要预测的是「总价」,所以总价是 y,不能进 X。单价也得踢掉——它和总价是一回事。
  留下这些:面积(连续)、房龄(连续)、楼层、装修、区域、户型(分类)。
  分类变量不能直接进模型,得先变成 0/1 的哑变量,get_dummies 一句搞定。
  drop_first=True 是去掉每个分类的第一档,避免「几档加起来必然等于 1」的完全共线。

  ② 随机切,不是按时间切
  --------------------------------------------------------------------
  特征从 6 列变成 21 列,1184 条样本。
  训练 947 条,测试 237 条。
  这里可以随机切,因为房价数据没有时间先后。第 10 章那是日销售流水,一切就漏未来。
  ! 同一种操作,两个项目里的做法正好相反。判断依据不是习惯,是「数据有没有时间序」。

  ③ 线性回归,四个指标
  --------------------------------------------------------------------

| 指标 | 这次的值 | 怎么读 |
|---|---|---|
| R 方 | 0.9422 | 解释了多少波动 |
| RMSE | 15.21 万 | 均方根误差,罚大错 |
| MAE | 10.18 万 | 平均差多少 |
| MAPE | 7.72% | 平均差百分之几 |

  R 方 0.9422,比第 10 章那个 0.5005 高得多。
  只用面积一个特征,R 方就有 0.6998。房价里一大半的差异,这一列就解释掉了。
  所以这次分数高,不是模型变强了,是特征里多了一个几乎决定性的变量。
  平均差 10.18 万,相对一套百来万的房子,这个误差还能接受。

  ④ 加个岭回归对照
  --------------------------------------------------------------------

| 模型 | R 方 | RMSE(万) | MAPE |
|---|---:|---:|---:|
| 线性回归 | 0.9422 | 15.21 | 7.72% |
| 岭回归 alpha=1 | 0.9419 | 15.25 | 7.72% |

  两个几乎一样。这是正常的:特征不多、也不高度共线,岭回归那根缰绳用不上。
  岭回归不是万灵药。它的用处是「特征多、共线重」的时候稳住系数,不是无条件提分。

  ⑤ 估一套房
  --------------------------------------------------------------------
  一套城东、89.5 平米、房龄 6 年、中楼层、精装的 2 室 2 厅。
  模型估价 161.88 万元。
  给区间:上下各留一个 RMSE,146.66 到 177.09 万。
  别忘了单价:161.88 万除 89.5 平米,合 18086 元每平米。
  ! 报估价必须报区间。一套房差 10 万很正常,报一个数就是在骗人。

  ⑥ 系数看谁最重
  --------------------------------------------------------------------
  面积那一项是 1.4740,意思是每多一平米,总价多 1.4740 万元,也就是 14740 元。
  系数最负的三个:区域_郊县 -78.903、区域_经开 -51.504、区域_城北 -36.706。
  系数最正的三个:装修_精装 6.006、区域_城中 18.552、装修_豪华 25.288。
  系数的单位和特征的尺度绑在一起,面积每多一平米加多少万,和「城东比城中差多少万」不能直接比大小。
  想比重要性,得先标准化,或者看第 8 章讲过的那些办法。
  ! 模型能跑出数,不等于结论能直接用。每个系数都要能说清「它到底在比什么」。

y = df["总价"] 之后,总价和单价都不能再出现在 X 里。漏一个,R 方就会跳到 1.0,那是抄答案。 get_dummies 要在切分之前做,这样训练集和测试集的列完全一致;切完再做容易对不上列。 random_state=42 固定切法。不固定的话,同一份代码跑两次分数不同,很难判断改动有没有用。

30 秒实验

  1. 把 drop_first 改成 False,看特征从 21 列变成多少列,R 方有没有变。
  2. 把 test_size 从 0.2 改成 0.3,看 MAPE 往哪边动。
  3. 想一想:如果把单价放回 X,R 方会变成多少?为什么这个结果没有意义?

常见误区

  1. 把总价或单价放进 X:等于把答案抄进去,R 方接近 1,模型其实什么也没学到。
  2. 分类变量不转哑变量:模型会把「区域」当数字大小来算,城东城西变成 2 和 3,毫无意义。
  3. 切完再 get_dummies:训练集和测试集列对不齐,预测时直接报错。
  4. 不设 random_state:分数每次不同,改了半天也不知道是改动生效还是随机波动。
自测三问(点开对答案) 1. 为什么房价数据可以随机切,销售流水不行? —— 房价没有时间先后,随机切不会让训练集混进未来;流水有时间序,随机切就是让模型提前看到未来。 2. R 方 0.9422 说明模型很厉害吗? —— 说明特征好用。只用面积一个特征就有 0.6998,剩下的 0.24 才是其他特征挣来的。 3. 岭回归这次为什么没提分? —— 特征不多也不高度共线,缰绳用不上。它的用处在特征多、共线重的时候。

第 11 章收尾。第 12 章换个题目:用 RFM 模型加聚类,把客户分成能分别对待的几类。

相关笔记