Python数据清洗实战:5个Pandas技巧让你少写80%代码

0 阅读4分钟

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个技巧回顾:

  1. pipe() 函数化清洗——代码更清晰,可复用
  2. 读数据时指定类型——省内存,避免后续转换
  3. 缺失值"3不原则"——不盲目删、不随意填、不用均值填分类
  4. 3种去重场景——业务去重、时间窗口去重、多列组合去重
  5. 向量化替代循环——性能提升2000倍不是夸张

数据清洗没有银弹,但好的方法能让你的效率翻几倍。如果你在数据清洗中遇到什么坑,评论区聊聊,一起交流。