为什么数据清洗至关重要
真实世界的数据几乎从不是"干净"的:缺失值、格式不一致、重复记录、异常值——这些问题如果不处理,后续的分析结论将失去可信度。数据科学界有一句话:"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)
)
本章小结
- 缺失值处理三策略:删除(缺失少)、填充(有业务依据)、插值(时序数据)
- 用分组均值而非全局均值填充,避免系统性偏差
pd.to_numeric(errors='coerce')和pd.to_datetime(errors='coerce')是处理"脏"数据的容错利器- .str 访问器让字符串清洗向量化,性能远优于 apply() 循环