第九章:Python ETL进阶:数据清洗与转换实战

各位同学,欢迎来到第九章。说实话,到了这个阶段,你已经不是那个只会用Excel拖拽的初级工程师了。今天我们要聊的,是ETL流程中最磨人、也最体现功力的部分——数据清洗与转换。

我在项目中遇到过太多「脏数据」翻车的案例。有一次,一个客户的数据集里,年龄字段居然出现了「-999」和「两百岁」这种值。你想想看,如果直接丢进模型,结果会多离谱?所以,这一章的内容,说白了就是帮你练就一双火眼金睛,把数据收拾得服服帖帖。

9.1 数据清洗:去重、缺失值、异常值

数据清洗是ETL的第一步,也是最耗时的一步。我个人习惯,拿到数据后先做三件事:去重、补缺失、揪异常。顺序不能乱,否则后面会越洗越乱。

9.1.1 重复数据检测与去重

重复数据是最常见的「隐形杀手」。你以为数据量很大,其实一半都是重复的。

import pandas as pd

# 模拟一份有重复的数据
df = pd.DataFrame({
    'user_id': [101, 102, 103, 101, 104, 102],
    'name': ['张三', '李四', '王五', '张三', '赵六', '李四'],
    'score': [85, 92, 78, 85, 88, 92]
})

# 检测重复行
print(df.duplicated())  # 返回布尔序列

# 基于所有列去重
df_clean = df.drop_duplicates()

# 基于指定列去重(保留第一次出现)
df_clean_subset = df.drop_duplicates(subset=['user_id'], keep='first')
我的经验: 去重时一定要先确认业务逻辑。比如订单表,同一个订单号出现两次可能是系统重试导致的,但用户表里user_id重复就是数据错误。我曾经因为没搞清楚业务规则,把正常的多笔交易当成重复删掉了……嗯,那天的复盘会开得特别长。

9.1.2 缺失值处理

缺失值处理,说白了就是「填坑」。但怎么填,很有讲究。

# 检查缺失值
print(df.isnull().sum())

# 方法一:直接删除(适合缺失比例极低)
df_dropna = df.dropna()

# 方法二:填充固定值(适合分类字段)
df['gender'].fillna('未知', inplace=True)

# 方法三:填充均值/中位数(适合数值字段)
df['age'].fillna(df['age'].median(), inplace=True)

# 方法四:前向填充(适合时间序列)
df['price'].fillna(method='ffill', inplace=True)
避坑指南: 我曾经用均值填充了一个严重偏态的字段(比如收入分布),结果模型训练出来全是偏差。后来改用中位数,效果好了很多。记住:偏态分布用中位数,正态分布用均值。

9.1.3 异常值检测

异常值检测,我常用的方法就两种:3σ原则和箱线图法。

import numpy as np

# 3σ原则:超出均值±3倍标准差视为异常
mean = df['score'].mean()
std = df['score'].std()
df_outlier_3sigma = df[(df['score'] >= mean - 3*std) & 
                       (df['score'] <= mean + 3*std)]

# 箱线图法:超出Q1-1.5IQR或Q3+1.5IQR视为异常
Q1 = df['score'].quantile(0.25)
Q3 = df['score'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df_outlier_iqr = df[(df['score'] >= lower_bound) & 
                    (df['score'] <= upper_bound)]

你可能会问:两种方法选哪个?我的建议是:数据量小、分布未知时用箱线图;数据量大、近似正态分布时用3σ。我在金融风控项目中,经常两种方法一起用,取交集作为最终异常值。

9.2 数据转换:apply、map、groupby

数据清洗完了,接下来就是「变形」。这三个函数,是Pandas里我最常用的三把刀。

9.2.1 apply:逐行/逐列应用函数

apply的灵活性最高,几乎能处理任何自定义逻辑。

# 对单列应用函数
df['score_level'] = df['score'].apply(lambda x: '优秀' if x >= 90 else '良好' if x >= 80 else '一般')

# 对多列应用函数(axis=1表示按行)
def classify_row(row):
    if row['score'] > 85 and row['age'] < 30:
        return '潜力股'
    else:
        return '普通'
df['category'] = df.apply(classify_row, axis=1)
性能提示: 如果数据量超过10万行,apply会变慢。这时候我一般改用向量化操作或numpy。记住:能不用apply就不用,除非逻辑真的复杂到没法向量化。

9.2.2 map:一对一映射

map适合做字典映射,简单粗暴。

# 字典映射
gender_map = {1: '男', 0: '女', -1: '未知'}
df['gender_label'] = df['gender'].map(gender_map)

# 也可以直接传函数
df['score_sqrt'] = df['score'].map(np.sqrt)

map和apply的区别?map只能处理Series(单列),apply可以处理DataFrame(多列)。我一般用map做简单的值替换,用apply做复杂的行级计算。

9.2.3 groupby:分组聚合

groupby是ETL里最强大的工具之一。没有它,很多汇总统计根本没法做。

# 单字段分组,计算均值
df_grouped = df.groupby('department')['salary'].mean()

# 多字段分组,多种聚合
df_grouped_multi = df.groupby(['department', 'gender']).agg({
    'salary': ['mean', 'std', 'count'],
    'age': 'median'
})

# 分组后应用自定义函数
df_grouped_apply = df.groupby('department')['salary'].apply(lambda x: x.max() - x.min())
核心要点: groupby之后一定要跟聚合操作(agg、transform、filter),否则返回的是GroupBy对象,不是DataFrame。我见过太多新手在这里卡住,以为代码写错了。

9.3 数据合并:merge与concat

ETL中,数据合并是家常便饭。你从不同系统拉来的数据,最终要拼成一张大表。

9.3.1 merge:类似SQL的JOIN

merge是Pandas里最像SQL JOIN的操作。我个人习惯,能不用merge就不用,因为性能开销大。但有些场景,非它不可。

# 内连接(inner join)
df_merged_inner = pd.merge(df_left, df_right, on='key', how='inner')

# 左连接(left join)
df_merged_left = pd.merge(df_left, df_right, on='key', how='left')

# 基于多个键连接
df_merged_multi = pd.merge(df_left, df_right, on=['key1', 'key2'], how='outer')

# 处理键名不同的情况
df_merged_diff = pd.merge(df_left, df_right, left_on='id_left', right_on='id_right')
避坑指南: 我曾经在merge时没注意索引重复,结果数据量膨胀了3倍。后来养成了习惯:merge之前先检查键的唯一性,用duplicated()看一眼。另外,merge后一定要检查行数是否合理,别等到下游报错才回头找。

9.3.2 concat:纵向或横向拼接

concat适合做「堆叠」操作。比如把上个月和这个月的数据拼在一起。

# 纵向拼接(行增加)
df_concat_rows = pd.concat([df_jan, df_feb, df_mar], axis=0, ignore_index=True)

# 横向拼接(列增加)
df_concat_cols = pd.concat([df_basic, df_extra], axis=1)

# 处理索引重复
df_concat_reset = pd.concat([df_a, df_b], axis=0, ignore_index=True)

concat和merge的区别?concat是「物理拼接」,不关心键值;merge是「逻辑关联」,基于键值匹配。我一般用concat做数据归档,用merge做数据关联。

9.4 本章知识体系

下面这张图,是我自己总结的本章核心逻辑。你可以把它当成一张「数据清洗与转换地图」。

Python ETL进阶:数据清洗与转换核心流程 原始数据 数据清洗 去重 (drop_duplicates) 缺失值处理 (fillna) 异常值检测 (3σ/IQR) 数据转换 apply (逐行/逐列) map (一对一映射) groupby (分组聚合) 数据合并 (merge / concat)

这张图把本章的三个核心模块串起来了:先清洗(去重→补缺失→揪异常),再转换(apply/map/groupby),最后合并(merge/concat)。顺序不能乱,否则你会在清洗阶段就遇到索引错乱的问题。

9.5 实战建议

最后,给你几条我在项目中沉淀下来的建议:

  • 先备份原始数据:清洗前一定留一份原始副本。我吃过这个亏,清洗错了只能从头再来。
  • 每一步都检查:每做完一个操作,用shape、info、describe看一眼数据。别等到最后才发现问题。
  • 写注释:清洗逻辑往往很复杂,一个月后你自己都可能看不懂。我习惯在每段清洗代码前写一行注释,说明「为什么这么做」。
  • 用函数封装:把清洗、转换、合并的逻辑写成函数,方便复用和调试。

一句话总结: 数据清洗是ETL的「地基」,地基不稳,后面全白搭。别嫌麻烦,把每一步做扎实了,后面的分析工作才能顺风顺水。


公众号:蓝海资料掘金营,微信deep3321