📊 第3章 Pandas 入门
📄 一句话概括

脏数据不会自己举手。要先数缺失、再数重复、最后框异常值,三步都得主动做。 缺失值先看清比例再决定删还是填。填 0 是最危险的一种,0 是个真实的值。 顺序错了结论就错:先剔重复再插补,否则均值会被重复行带高。

📝 本章和前面几章的关系

这一篇是整章最该记住的一篇。前面所有操作的前提都是「数据是干净的」,而它默认不干净。 第 4 章的 groupby 和 merge 一样会栽在脏数据上,把这一篇的顺序记住能省很多事。

一、脏数据长什么样

手头这份 11 行的成绩表里埋了三处问题:两处缺失、一处重复。 李娜的数学空着、赵敏的英语空着,张伟那条整行重复了一遍。 从肉眼上看,这表和干净的表一模一样——head() 打出来照样整齐。

脏数据不会自己举手。要主动去查:先数缺失,再数重复,最后看异常值。

二、缺失值:先数,再决定

df.isna() 把整张表变成 True/False,.sum() 数出每列几个 True。

列 缺失个数
姓名 0
班级 0
语文 0
数学 1
英语 1
总分 0

数学缺 1 个、英语缺 1 个,其余列不缺。到这里才知道问题有多小。 处理只有三条路:删掉那行(dropna)、填一个值(fillna)、留着不管。 填 0 是最危险的一种:0 是个真实的值,会把均值拉低。除非 0 有业务含义。

三、填还是删:看缺失比例

整张表的缺失比例 3.03%——2 个空格除以 66 个格子(11 行 6 列)。 百分之三很低,删掉那两行损失不大,也不会让样本偏。 真要高到两三成,就只能插补:删掉的话样本不够,剩下的样本还会有系统性偏差。

按列填:df.fillna({'数学': 均值}),只填数学那一列,别的列不动。 比例这个数一定要现算。凭感觉说「缺得不多」,是数据分析里最常见的狡辩。

四、插补均值的陷阱:85.40 不是 83.00

这份脏表算出来的数学均值是 85.40,而干净的表只有 83.00。 差在哪?那行重复的张伟被算了两次,他的 92 分把均值抬高了。 所以顺序必须是:先剔重复,再算均值,最后插补。反过来均值就是错的。

插补还有一个坑:填进去的值会压缩方差。填得越多,标准差越小,看起来越「稳」。

五、重复值:先判断再删

df.duplicated() 标出「和前面某行完全一样」的行,这次共 1 行。 注意它标的是后出现的那一行,第一行不算重复。 drop_duplicates() 之后剩 10 行,正好回到干净数据的行数。

判断重复默认看全部列。只按姓名判重要写 subset=['姓名']。 subset=['姓名'] 会把同名的不同人当成重复删掉。判重之前先想清楚主键是什么。

六、异常值:用分位数框出来

英语的四分位:Q1=79.25、Q3=87.75,四分位距 IQR 是 8.50。 按 1.5 倍 IQR 算,正常范围是 66.50 到 100.50,落在外面的是异常值。 这次有 1 人落在范围外。找出来之后要回去看原始记录,不是直接删。

异常值不等于错误值。它可能是录错了,也可能是真实但极端的情况。 把真实的高分当错误删掉,等于给自己编了一个更好看的结论。

七、清洗的顺序

步骤 函数 为什么在这个位置
数缺失 isna().sum() 先知道缺多少
剔重复 drop_duplicates() 重复行会污染统计
填或删缺失 fillna / dropna 剔完重复再补,均值才对
查异常 quantile 加布尔索引 前面脏值会带歪分位数

顺序反了会怎样:先填缺失再去重,填进去的值和原来不一样,重复的行就判不出来了。 这一张表是整章最该背下来的一段。它管的不只是这一份数据。 清完之后一定要再打一次 shape 和 describe(),确认数字变了、变得合理。

八、动手 Demo:数据清洗:缺失、重复、异常

先造一份带三处毛病的表,再按「数缺失、剔重复、填缺失、查异常」走一遍。 重点看插补前后均值差多少,以及 IQR 框出来的范围。

import numpy as np
import pandas as pd

DF = pd.DataFrame({
    "姓名": ["张伟", "王芳", "李娜", "刘洋", "陈静",
             "杨帆", "赵敏", "黄磊", "周涛", "吴霞"],
    "班级": ["一班"] * 5 + ["二班"] * 5,
    "语文": [88, 92, 75, 85, 90, 68, 95, 72, 83, 79],
    "数学": [92, 95, 68, 88, 73, 82, 91, 65, 86, 90],
    "英语": [85, 90, 80, 79, 95, 71, 88, 60, 84, 87],
})
DF["总分"] = DF["语文"] + DF["数学"] + DF["英语"]

D = DF.copy()
D.loc[2, "数学"] = np.nan
D.loc[6, "英语"] = np.nan
D = pd.concat([D, D.iloc[ [0]]], ignore_index=True)

print("行数 %d,格子 %d" % (len(D), D.size))
print(D.isna().sum().to_string())
print("缺失比例 %.2f%%" % (D.isna().sum().sum() / D.size * 100))
print("脏表数学均值 %.2f" % D["数学"].mean())
print("重复行 %d" % D.duplicated().sum())

CLEAN = D.drop_duplicates()
print("去重后 %d 行,数学均值 %.2f" % (len(CLEAN), CLEAN["数学"].mean()))

q1, q3 = DF["英语"].quantile([0.25, 0.75])
iqr = q3 - q1
print("Q1 %.2f  Q3 %.2f  IQR %.2f" % (q1, q3, iqr))
print("正常范围 %.2f ~ %.2f" % (q1 - 1.5 * iqr, q3 + 1.5 * iqr))

实跑输出:

--------------------------------------------------------------------
数据清洗:缺失、重复、异常
--------------------------------------------------------------------

  ① 先造一份脏数据
  --------------------------------------------------------------------
  造了两处缺失(李娜的数学、赵敏的英语),又复制了一行(张伟那条重复了)。
  现在一共 11 行。
  ! 脏数据不会自己举手。要主动去查:先数缺失,再数重复,最后看异常值。

  ② 缺失值:先数再决定
  --------------------------------------------------------------------
  isna() 把整张表变成 True/False,sum() 数出每列几个 True。

| 行 | 缺失个数 |
|---|---:|
| 姓名 | 0 |
| 班级 | 0 |
| 语文 | 0 |
| 数学 | 1 |
| 英语 | 1 |
| 总分 | 0 |

  数学缺 1 个,英语缺 1 个,其余列不缺。
  处理只有三条路:删掉那行(dropna)、填一个值(fillna)、留着不管。
  ! 填 0 是最危险的一种:0 是一个真实的值,会把均值拉低。除非 0 有业务含义。

  ③ 填还是删:看缺失比例
  --------------------------------------------------------------------
  整张表的缺失比例:3.03%——2 个空格除以 66 个格子。
  百分之三很低,删掉那两行损失不大。真要高到两三成,就只能插补,否则样本不够。
  按列填:数学用均值 85.40 填,英语那格还空着。

| 行 | 姓名 | 数学 | 英语 |
|---|---:|---:|---:|
| 0 | 张伟 | 92.0 | 85.0 |
| 1 | 王芳 | 95.0 | 90.0 |
| 2 | 李娜 | 85.4 | 80.0 |
| 3 | 刘洋 | 88.0 | 79.0 |

  ! 这个均值 85.40 已经含了那行重复数据,比干净的 83.00 高。先清洗再插补,顺序反了均值就是错的。

  ④ 重复值:先判断再删
  --------------------------------------------------------------------
  duplicated() 标出「和前面某行完全一样」的行,共 1 行。
  注意它标的是后出现的那一行,第一行不算重复。
  drop_duplicates() 之后剩 10 行,去掉了 1 行。
  ! 判断重复默认看全部列。只按姓名判重的写法是 subset=['姓名'],那会把同名的不同人都删掉。

  ⑤ 异常值:用分位数框出来
  --------------------------------------------------------------------
  英语的四分位:Q1=79.25,Q3=87.75,IQR=8.50。
  按 1.5 倍 IQR 算,正常范围是 66.50 到 100.50。
  落在这个范围外的有 1 人。
  ! 异常值不等于错误值。它可能真是错误,也可能是真实但极端的情况,得回去看原始记录。

  ⑥ 清洗的顺序
  --------------------------------------------------------------------

| 步骤 | 函数 | 为什么这个顺序 |
|---|---|---|
| 数缺失 | isna().sum() | 先知道缺多少,才决定删还是填 |
| 剔重复 | drop_duplicates() | 重复行会污染后面的统计 |
| 填或删缺失 | fillna / dropna | 重复处理完再补,指数才对得上 |
| 查异常 | quantile + 布尔索引 | 最后查,因为前面的脏值会带歪分位数 |

  顺序反了会怎样:先填缺失再去重,填进去的值不一样,重复的行就判不出来了。
  ! 这一节是整章最该记住的一段。第 4 章的 groupby 和 merge,脏数据进来一样算错。

D.size 是行数乘列数,拿它当分母才是「占全部格子的比例」,比按列算更直观。 去重前后均值会变:这次去掉的那行是张伟,92 分偏高,均值从 85.40 掉到 84.67。

30 秒实验

  1. 把缺失比例的分母换成 len(DF),看数字变成多少,想一想哪个才是「缺失比例」。
  2. 用 fillna(0) 填数学那一列,比较均值和用 85.00 填的差别。
  3. 想一想:如果 11 行里缺了 5 行,还该不该 dropna?

常见误区

  1. 缺失一律填 0:把均值拉低,还看不出来错在哪。
  2. 先插补再去重:均值含了重复行,插进去的值本身就是错的。
  3. 用 subset=['姓名'] 判重:同名的不同人被当成重复删掉。
  4. 把异常值直接删:真实的高分被当成错误,结论反而更好看、更假。
自测三问(点开对答案) 1. 为什么缺失比例要用「空格数除以总格数」? —— 因为缺的是格子不是行。按行算会把「缺一格」和「整行全缺」算成一个比例。 2. 这份数据里插补均值 85.40 为什么偏高? —— 因为算均值时那行重复的张伟被算了两次,92 分把均值抬上去了。 3. IQR 框异常值的范围是怎么算的? —— Q1 减 1.5 倍 IQR 到 Q3 加 1.5 倍 IQR。IQR 就是 Q3 减 Q1。

最后收个尾:把行标签设好、把顺序排好、把名次算出来,这一章就齐了。

相关笔记