第九章: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')
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)
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())
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')
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 本章知识体系
下面这张图,是我自己总结的本章核心逻辑。你可以把它当成一张「数据清洗与转换地图」。
这张图把本章的三个核心模块串起来了:先清洗(去重→补缺失→揪异常),再转换(apply/map/groupby),最后合并(merge/concat)。顺序不能乱,否则你会在清洗阶段就遇到索引错乱的问题。
9.5 实战建议
最后,给你几条我在项目中沉淀下来的建议:
- 先备份原始数据:清洗前一定留一份原始副本。我吃过这个亏,清洗错了只能从头再来。
- 每一步都检查:每做完一个操作,用shape、info、describe看一眼数据。别等到最后才发现问题。
- 写注释:清洗逻辑往往很复杂,一个月后你自己都可能看不懂。我习惯在每段清洗代码前写一行注释,说明「为什么这么做」。
- 用函数封装:把清洗、转换、合并的逻辑写成函数,方便复用和调试。
一句话总结: 数据清洗是ETL的「地基」,地基不稳,后面全白搭。别嫌麻烦,把每一步做扎实了,后面的分析工作才能顺风顺水。
公众号:蓝海资料掘金营,微信deep3321