Chapter 04

数据清洗

缺失值处理策略、重复数据识别与删除、类型转换、字符串向量化操作

为什么数据清洗至关重要

真实世界的数据几乎从不是"干净"的:缺失值、格式不一致、重复记录、异常值——这些问题如果不处理,后续的分析结论将失去可信度。数据科学界有一句话:"Garbage in, garbage out"——输入垃圾,输出垃圾。数据清洗通常占据整个数据分析工作量的 60-80%。

缺失值处理

Pandas 中缺失值有两种表示:NaN(float 类型)和 pd.NA(通用 NA,2.0 引入)。

import pandas as pd
import numpy as np

# 示例数据
df = pd.DataFrame({
    'age': [25, np.nan, 30, np.nan, 35],
    'salary': [50000, 60000, np.nan, 45000, np.nan],
    'city': ['北京', '上海', '北京', None, '深圳']
})

# 检测缺失值
df.isnull()            # 布尔 DataFrame,True 表示缺失
df.isnull().sum()      # 每列缺失值数量
df.isnull().mean()     # 每列缺失值比例

# 策略一:删除含缺失值的行
df.dropna()                    # 删除任意列有缺失的行
df.dropna(subset=['age'])      # 只有 age 缺失时才删除
df.dropna(thresh=2)           # 保留至少有 2 个非空值的行

# 策略二:填充缺失值
df['age'].fillna(df['age'].mean())     # 用均值填充(连续变量常用)
df['age'].fillna(df['age'].median())   # 用中位数(对离群值鲁棒)
df['city'].fillna('未知')              # 分类变量填充固定值
df['salary'].fillna(method='ffill')  # 向前填充(时序数据常用)

# 策略三:插值(时间序列数据)
df['salary'].interpolate(method='linear')   # 线性插值
df['salary'].interpolate(method='time')     # 按时间插值(需 DatetimeIndex)
缺失值处理的常见误区

不要无脑用均值填充!如果数据存在明显的分组特征(如不同城市的薪资差异很大),应该用分组均值填充:df['salary'].fillna(df.groupby('city')['salary'].transform('mean'))。全局均值填充会引入系统性偏差。

重复数据处理

# 检测重复行
df.duplicated()                    # 布尔 Series,True 表示重复
df.duplicated(subset=['id'])      # 只按 id 列判断重复
df.duplicated().sum()              # 重复行数量

# 删除重复行
df.drop_duplicates()               # 保留第一次出现的行
df.drop_duplicates(keep='last')  # 保留最后一次出现的行
df.drop_duplicates(keep=False)   # 删除所有重复行(包括第一次出现的)
df.drop_duplicates(subset=['email'])  # 按 email 去重

类型转换

# astype() 是显式类型转换的标准方法
df['age'] = df['age'].astype('Int64')   # 大写 Int64 = 支持 NA 的整数类型
df['price'] = df['price'].astype('float32')  # 降精度节省内存
df['category'] = df['category'].astype('category')  # 分类类型,节省内存

# pd.to_numeric:处理"脏"数字字符串
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# errors='coerce':无法转换的值变为 NaN,而非抛出异常

# pd.to_datetime:解析日期字符串
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
df['date'] = pd.to_datetime(df['date'], errors='coerce')  # 容错模式

字符串向量化操作(.str 访问器)

# .str 访问器让字符串操作向量化,避免 apply() 循环
df['name'].str.strip()           # 去除首尾空格
df['name'].str.lower()           # 转小写
df['email'].str.contains('@')   # 判断是否包含 @ 符号
df['phone'].str.replace(r'[^0-9]', '', regex=True)  # 只保留数字
df['address'].str.extract(r'(\d{6})')  # 提取邮政编码(6位数字)
df['name'].str.split(' ', expand=True)  # 按空格拆分,返回 DataFrame

# 完整的数据清洗流水线示例
df_clean = (df
    .drop_duplicates(subset=['id'])
    .dropna(subset=['name', 'email'])
    .assign(
        name=df['name'].str.strip().str.title(),
        email=df['email'].str.strip().str.lower(),
        age=pd.to_numeric(df['age'], errors='coerce'),
        created_at=pd.to_datetime(df['created_at'])
    )
    .query('age >= 18 and age <= 120')  # 过滤年龄异常值
    .reset_index(drop=True)
)

本章小结