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

1206 行里五类毛病:完全重复 6、装修空 18、面积空 4、房龄负 3、单价离谱 9。 单价乘面积除一万必须等于总价——用这条自洽性规则抓错,比肉眼看可靠得多。 洗完 1184 行,清掉 22 条,原始文件一个字不动。

📝 本章和前面几章的关系

第 3 章讲的是「怎么查」,这一篇是「查到之后怎么处理」,而且多了一条规则:两列互相比。 第 10 章那份数据太干净,三项全 0;这一份才是真实项目的样子。

一、读进来,先把单位和类型对齐

read_csv 一句读完,1206 行 10 列。编号是字符串(FJ0001),不是数字。 单价是元,总价是万元,这一条是整章最容易出错的地方。 单价乘面积得到的是元,除以一万才是万元。少了这一步,总价那一列会大一万倍。

装修那一列是 object,有缺失,读进来是 NaN。房龄是 int64,负数一定是错的。 单位这事在报告里也要写清楚。读者默认「总价」就是万元,你按元写,他能吓一跳。

二、缺:能填的填,不能填的删

装修缺 18 条——填成「未知」,不删。房子还在,只是没写装修,删掉就少一套房源。 面积缺 4 条——删。面积没有,单价和总价都算不出来,留着也没法进任何统计。 同一个「缺失」,两种处理。判断标准只有一个:这一行还能不能参与后面的计算。

能参与就填,不能参与就删。反过来,看着别扭就删,是最容易把有效数据扔掉的坏习惯。

三、重与怪:去重、房龄、单价

毛病 查到 怎么处理
完全重复 6 条 drop_duplicates 去掉
房龄为负 3 条 删(负数不可能)
单价离谱 9 条 先存疑,交给一致性规则

去重一句 drop_duplicates() 就完事,1206 减 6 得 1200。 房龄那一列出现 -2,直接删。负数在业务上不成立,不需要再想。 单价从 3259 到 80347,差二十多倍——同一个城市不可能有这种差距。

但「离谱」这两个字靠不住。这次是差二十倍好认,差两成呢?肉眼就判不出来了。所以要有规则。

四、一致性校验:两列必须自洽

表里同时有单价和总价,这两个数本来就是一对:单价乘面积除一万,必须等于总价。 于是写一条规则:算出偏差率,超过 15% 的判为错,整行删掉。

import pandas as pd

df = pd.read_csv("二手房数据.csv")
df["验算"] = df["单价"] * df["面积"] / 10000.0
df["偏差"] = (df["验算"] - df["总价"]).abs() / df["总价"]
print("偏差超 15%% 的 %d 条" % (df["偏差"] > 0.15).sum())
df = df[df["偏差"] <= 0.15]

这条规则一次抓出 9 条,正好是前面那批单价离谱的记录。 为什么是 15%?因为正常记录只有四舍五入的误差,偏差在千分之一量级;真错了至少差几倍。 阈值取多少要看业务。取 5% 会误删正常数据,取 50% 会漏掉错数据,15% 是这条数据的分界线。

这就是「用数据自己的约束查数据」:不靠眼睛,不靠常识,靠两列之间的关系。

五、洗完另存,原文件不动

洗完 1184 行 10 列,存成 二手房数据_clean.csv。 1206 减 22 得 1184:去重 6、面积空 4、房龄负 3、单价错 9,一笔一笔都对得上。 清掉的全是「没法用」的,没有一条是「看着别扭」的。这个标准要能对别人说清楚。

原始文件一个字不动。清洗规则以后要是改了,还能拿原表重跑一遍。

六、动手 Demo:数据清洗

五步:去重、填装修、删面积空、删房龄负、按一致性删单价错的。 每一步都打印「减几条、剩几条」,账要能对上。

import pandas as pd

df = pd.read_csv("二手房数据.csv")
print("原始 %s" % (df.shape,))

print("重复 %d,装修空 %d,面积空 %d,房龄负 %d"
      % (df.duplicated().sum(), df["装修"].isnull().sum(),
         df["面积"].isnull().sum(), (df["房龄"] < 0).sum()))

df = df.drop_duplicates()
df["装修"] = df["装修"].fillna("未知")
df = df[df["面积"].notnull()]
df = df[(df["房龄"] >= 0) & (df["房龄"] <= 40)]
print("前三步洗完 %d 行" % len(df))

df["验算"] = df["单价"] * df["面积"] / 10000.0
df["偏差"] = (df["验算"] - df["总价"]).abs() / df["总价"]
bad = df["偏差"] > 0.15
print("偏差超 15%% 的有 %d 条" % bad.sum())
df = df[~bad].drop(columns=["验算", "偏差"])
print("洗完 %s" % (df.shape,))
df.to_csv("二手房数据_clean.csv", index=False, encoding="utf-8-sig")

实跑输出:

--------------------------------------------------------------------
数据清洗
--------------------------------------------------------------------

  ① 读进来
  --------------------------------------------------------------------
  读进来 1206 行 10 列。
  read_csv 默认当第一行是表头,这份数据正好是。中文列名直接用。
  编号那一列是字符串(FJ0001),不是数字。用 dtype 指定一下更稳,不指定也不会错。

  ② 类型和单位先对齐
  --------------------------------------------------------------------

| 列 | 类型 | 要留意什么 |
|---|---|---|
| 面积 | float64 | 平方米,带一位小数 |
| 单价 | int64 | 元每平米 |
| 总价 | float64 | 万元,和单价的单位差一万倍 |
| 房龄 | int64 | 年,负数一定是错的 |
| 装修 | object | 有缺失,读进来是 NaN |

  单价是元、总价是万元,这一条是整章最容易出错的地方。
  单价乘面积得到的是元,除以一万才是万元。少了这一步,总价那一列会大一万倍。

  ③ 查:缺、重、怪
  --------------------------------------------------------------------
  原始 1206 行。

| 毛病 | 怎么查 | 这次查到 |
|---|---|---:|
| 完全重复 | duplicated().sum() | 6 条 |
| 装修缺失 | isnull().sum() | 18 条 |
| 面积缺失 | isnull().sum() | 4 条 |
| 房龄为负 | (房龄 < 0).sum() | 3 条 |
| 单价离谱 | 看最小最大 | 3259 到 80347 元 |

  单价从 3259 到 80347,差 20 多倍。同一个城市不可能有这种差距,一眼就知道有错。
  真实数据里这种「差一个数量级」的错,八成是录入时小数点错位或者多打了一个零。

  ④ 按规则洗:五步
  --------------------------------------------------------------------
  第一步去重:1206 减 6 得 1200。
  第二步填装修:空的那 18 条填成「未知」。这是填,不是删——房子还在,只是没写装修。
  第三步删面积缺失:1200 减 4 得 1196。面积没有,单价和总价都算不了,只能删。
  第四步删房龄异常:1196 减 3 得 1193。负数不可能,超过 40 年的房子这个城市基本没有。
  第五步按一致性删:单价乘面积除一万,和表里的总价对一遍,偏差超过 15% 的判为错。
  删掉 9 条,剩 1184 条。
  ! 这一步是这一节的重点:不靠肉眼看,靠「两列必须自洽」这条规则。
  单价和总价本来就是一对,对不上就至少有一个是错的。

  ⑤ 落回干净的一份
  --------------------------------------------------------------------
  洗完 1184 行 10 列,存成 二手房数据_clean.csv。
  清掉 22 条,保住 1184 条。清掉的全是「没法用」的,没有一条是「看着别扭」的。
  原始文件一个字不动。清洗规则以后要是改了,还能拿原表重跑一遍。

df[df["面积"].notnull()] 是筛选,不是删除——它返回一个新表,原表还在。要接住返回值。 df["装修"].fillna("未知") 如果没接住,等于白写一行。这类「写了但没生效」最难查。 drop(columns=[...]) 把两个临时列去掉,不然它们会跟着写进 clean 文件,多出两列。

30 秒实验

  1. 把偏差阈值从 0.15 改成 0.05,看会多删多少条。
  2. 把 fillna("未知") 换成 dropna(),看最后剩多少行。
  3. 想一想:如果表里只有总价、没有单价,这条一致性规则还能用吗?

常见误区

  1. 缺失一律删:装修空着的房源还是有效房源,填「未知」比删掉好。
  2. 靠肉眼判异常:差二十倍好认,差两成根本看不出来,得靠规则。
  3. 筛完不接返回值:df[...] 是返回新表,写成一句不赋值,什么都没发生。
  4. 临时列忘了删:验算和偏差两列跟着进了 clean 文件,后面统计多出两列莫名其妙的东西。
自测三问(点开对答案) 1. 装修缺失为什么填、面积缺失为什么删? —— 装修没有不影响算单价总价,房子还在;面积没有,单价和总价都算不出来,留着没用。 2. 一致性规则的依据是什么? —— 单价乘面积除一万必须等于总价。两列本来是一对,对不上就至少有一个错。 3. 1206 减到 1184,少的 22 条分别是什么? —— 重复 6、面积空 4、房龄负 3、单价错 9,加起来 22。

数据干净了。下一篇上第一组分析:八个区的均价和房源占比。

相关笔记