Python数据清洗实战:5个Pandas技巧让你少写80%代码
数据清洗是数据分析中最耗时、最无聊,但也是最重要的环节。很多人一上来就开始写循环处理缺失值、转换类型、去重,代码写了一大坨,Bug也一堆。
今天分享5个Pandas数据清洗的实战技巧,每个都有真实业务场景,附带完整可运行代码。学会这5个,你的数据清洗代码量至少减少80%。
1. 用 pipe() 替代链式调用,代码更清晰
大多数人的数据清洗代码是这样的:
df = df.dropna(subset=['user_id'])
df = df[df['amount'] > 0]
df = df.rename(columns={'amt': 'amount'})
df = df.assign(date=pd.to_datetime(df['date']))
5行赋值,中间变量被反复覆盖。用 pipe() 重写:
def clean_data(df):
# 第一步:去掉核心字段为空的行
df = df.dropna(subset=['user_id'])
# 第二步:过滤异常值
df = df[df['amount'] > 0]
# 第三步:统一列名
df = df.rename(columns={'amt': 'amount'})
# 第四步:类型转换
df = df.assign(date=pd.to_datetime(df['date']))
return df
df = raw_df.pipe(clean_data)
优势:函数化清洗,可复用、可测试、可读性高。项目里多个数据源做同样清洗,只需要 df1.pipe(clean_data) 和 df2.pipe(clean_data) 就行。
2. 用 infer_objects() 自动修复数据类型
CSV读进来,数字列变成了object,日期列变成了字符串。手动一个个 astype() 很痛苦:
# 传统方式:逐列转换
df['amount'] = df['amount'].astype(float)
df['count'] = df['count'].astype(int)
df['date'] = pd.to_datetime(df['date'])
更好的方式——读数据时就指定类型:
dtypes = {
'user_id': 'int64',
'amount': 'float64',
'count': 'int32',
'category': 'category' # 分类型字段用category,省内存
}
df = pd.read_csv('data.csv', dtype=dtypes, parse_dates=['date'])
实战技巧:如果你的数据量超过100万行,把字符串列转成 category 类型,内存占用能减少50-80%。实测:一个800万行的用户行为表,内存从2.1GB降到0.6GB。
3. 缺失值处理的"3不原则"
处理缺失值,很多人直接 df.dropna() 一把删。这是最偷懒也最危险的做法。
3不原则:
# 不盲目删除:先看缺失比例
missing_ratio = df.isnull().mean().sort_values(ascending=False)
print(missing_ratio[missing_ratio > 0.3]) # 缺失超30%的列,单独处理
# 不随意填充0:区分"真正为0"和"缺失"
# 销售额为0是有意义的,但缺失可能是没有记录
df['revenue'].fillna(0) # 错误!把"没记录"当成了"免费"
df['revenue'].fillna(method='ffill') # 更合理:用前值填充
# 不用均值填充分类变量
df['city'].fillna(df['city'].mode()[0]) # 分类变量用众数
df['age'].fillna(df['age'].median()) # 数值变量用中位数(抗极端值)
关键原则:缺失值处理之前,先搞清楚"为什么缺失"。是数据采集的问题?是业务逻辑的"空"?还是系统Bug?不同原因,处理方式完全不同。
4. 去重的3种场景
df.drop_duplicates() 大部分人会,但下面3种场景你可能没遇到过:
场景1:基于业务逻辑去重
# 同一用户同一天多条记录,只保留金额最大的
df = df.sort_values('amount', ascending=False) \
.drop_duplicates(subset=['user_id', 'date'], keep='first')
场景2:时间窗口去重
# 同一设备5分钟内多次点击,只保留第一次
df['time_diff'] = df.groupby('device_id')['timestamp'].diff().dt.seconds
df = df[(df['time_diff'] > 300) | (df['time_diff'].isna())]
场景3:多列组合去重
# 同一订单+同一商品+同一天,只保留状态最新的
df = df.sort_values('update_time', ascending=False) \
.drop_duplicates(subset=['order_id', 'product_id', 'date'], keep='first')
5. 用向量化替代循环
数据清洗最常见的性能瓶颈:在DataFrame上用 for 循环。
# 反面教材:100万行要跑3分钟
for idx, row in df.iterrows():
if row['amount'] > 1000:
df.loc[idx, 'level'] = 'VIP'
else:
df.loc[idx, 'level'] = 'Normal'
# 正确做法:向量化,0.1秒搞定
import numpy as np
df['level'] = np.where(df['amount'] > 1000, 'VIP', 'Normal')
# 复杂条件用 np.select
conditions = [
df['amount'] > 10000,
df['amount'] > 1000,
df['amount'] > 100
]
choices = ['SVIP', 'VIP', 'Member']
df['level'] = np.select(conditions, choices, default='Normal')
性能对比(100万行数据):
| 方法 | 耗时 |
|---|---|
| iterrows循环 | 183秒 |
| apply函数 | 12秒 |
| np.where向量化 | 0.08秒 |
差距是2000倍。如果你的数据清洗脚本要跑几分钟甚至更久,大概率是用错了方法。
总结
5个技巧回顾:
pipe()函数化清洗——代码更清晰,可复用- 读数据时指定类型——省内存,避免后续转换
- 缺失值"3不原则"——不盲目删、不随意填、不用均值填分类
- 3种去重场景——业务去重、时间窗口去重、多列组合去重
- 向量化替代循环——性能提升2000倍不是夸张
数据清洗没有银弹,但好的方法能让你的效率翻几倍。如果你在数据清洗中遇到什么坑,评论区聊聊,一起交流。