问十个数据分析师「时间花在哪」,九个会说「洗数据」。这一章是全书使用频率最高的章节——pandas 为清洗准备的每一件工具,都值得玩到熟。
🕳️ 认识 NaN:缺失不是错误,是信息
pandas 用 NaN(Not a Number)标记缺失。关键心法:NaN 参与运算不会报错,而是「传染」——含 NaN 的列求和、求均值会自动跳过(除非整列全空)。先自己造一批感受一下:
import numpy as np, pandas as pd
string_data = pd.Series(["aardvark", np.nan, "artichoke", None])
string_data.isna() # 逐个判断是否缺失
string_data.dropna() # 扔掉
string_data.fillna("MISS") # 填上
dropna:扔的三种姿势
df = pd.DataFrame([[1, 6.5, 3], [1, np.nan, np.nan],
[np.nan, np.nan, np.nan], [np.nan, 6.5, 3]])
df.dropna() # 只留「整行无缺」的行(很严格)
df.dropna(how="all") # 只扔「整行全空」的(温和,最常用)
df.dropna(thresh=2) # 至少有 2 个非缺失值才保留
df.dropna(axis=1, how="all") # 同样的逻辑用在列上
fillna:填的三种姿势
df.fillna(0) # 常数
df.fillna({"col1": 0.5, "col2": 0})# 每列不同策略
df.fillna(method="ffill") # 用前一个值(时序数据首选)
df.fillna(method="bfill", limit=2) # 后向填,最多填 2 个
# 高级玩法:填每列的中位数
df.fillna(df.median())
书里的选择题没有标准答案,只有取舍:缺失机制重要(随机缺?系统缺?),样本量重要(多就扔、少就填)。填之前先想 10 秒「为什么缺」。
🧽 去重、映射与替换
data = pd.DataFrame({"k1": ["one"]*3 + ["two"]*3, "v1": range(6)})
data.duplicated() # 布尔:是否与前面的行重复
data.drop_duplicates(["k1"]) # 按 k1 列去重
data.drop_duplicates(["k1"], keep="last")
data["v1"].map(lambda x: x * 2) # 逐元素映射
data["k1"].replace("one", "uno") # 换值(可传列表/字典批量换)
data.rename(index=str.title, columns=str.upper) # 改轴标签
📏 分箱:连续变离散
把年龄变成年龄段、收入变成档位——cut 和 qcut 两兄弟:
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats.codes # 每个值落在哪个箱(0,1,2,3)
cats.categories # (18,25] < (25,35] < (35,60] < (60,100]
pd.value_counts(cats) # 每箱频数
pd.cut(ages, 4, precision=1) # 等宽分 4 箱(自动算边界)
pd.qcut(data["v1"], 4) # 按分位数分箱——每箱人数一样多!
cut 等宽、qcut 等人数——统计分析和分组统计超爱 qcut。
🚨 异常值与哑变量
data = pd.DataFrame(rng.standard_normal((1000, 4)))
data.describe()
col = data[2]
col[col.abs() > 3] # 找出绝对值超 3 的异常点
data[(data.abs() > 3).any(axis=1)] # 任一列超标的行
data[col.abs() > 3] = np.sign(col) * 3 # 「封顶」处理:截断到 ±3
# 哑变量:类别 → 0/1 矩阵(机器学习的前置仪式)
dummies = pd.get_dummies(data["key"], prefix="key")
pd.concat([data, dummies], axis=1) # 拼回原表
🔤 字符串与正则
Python 内置 split/strip/join/find 之外,正则 re 模块是文本清洗的核武器:
import re
pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}"
re.findall(pattern, text, flags=re.IGNORECASE) # 抽邮箱
re.sub(pattern, "REDACTED", text, flags=re.IGNORECASE) # 替换
pandas 把这一切向量化——.str 访问器自动帮你处理 NaN、不用循环:
vals = pd.Series([" bob ", "ALICE", None, "carol "])
vals.str.strip().str.title() # 'Bob','Alice',NaN,'Carol'
vals.str.contains("o", na=False) # 布尔筛选
vals.str.count(r"\d") # 正则计数
vals.str.split(",") # 切分 + .str.get(0) 取段
🏷️ 类别型数据:省内存的秘密武器
「性别、城市、等级」这类取值少、重复多的列,存成 category 能省一个数量级内存,还能定义「逻辑顺序」:
fruits = ["apple", "orange", "apple"] * 2
f = pd.Series(fruits).astype("category")
f.cat.categories # Index(['apple','orange'])
f.cat.set_categories(["apple","orange","banana"]) # 显式全集
# 有序类别:small < large
s = pd.Series(["large","small","large"]).astype("category")
s.cat.set_categories(["small","large"], ordered=True)
s > "small" # True/False 有了含义
s.cat.remove_unused_categories() # 清掉没出现的类别
虚拟统计、one-hot(get_dummies)、groupby 在类别列上都会明显提速——第 13 章的联邦选举数据会看到它的实战威力。
📌 本章小结:
dropna(how="all")温和扔、fillna(method="ffill")时序填;去重用drop_duplicates(keep="last");cut 等宽、qcut 等人数;异常值先describe()再(abs()>3).any(axis=1);文本清洗用.str向量化;高重复低基数列转category。