第7章封面

问十个数据分析师「时间花在哪」,九个会说「洗数据」。这一章是全书使用频率最高的章节——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)  # 改轴标签

📏 分箱:连续变离散

把年龄变成年龄段、收入变成档位——cutqcut 两兄弟:

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