1. Pandas数据清洗与合并实战指南
作为一名数据分析师,我每天80%的时间都在和数据"较劲"。现实世界的数据就像刚从菜市场买回来的蔬菜——带着泥土、烂叶和不规则的形状。今天我要分享的是如何用Pandas这把"瑞士军刀"来处理这些"脏数据",让你的分析工作事半功倍。
1.1 为什么数据清洗如此重要?
记得我刚入行时,领导给了一份号称"已经清洗过"的销售数据。当我兴冲冲地跑完模型后,结果却离谱得让人怀疑人生。后来才发现,数据里藏着大量重复记录和异常值。这就是著名的"Garbage In, Garbage Out"原则——垃圾数据进,垃圾结果出。
数据清洗的核心任务包括:
- 处理缺失值(NaN)
- 清除重复记录
- 统一数据格式
- 合并多源数据
2. 缺失值处理实战
2.1 识别缺失值
先来看一个典型的数据集:
python复制import pandas as pd
import numpy as np
df = pd.DataFrame({
"客户ID": [101, 102, 103, 104],
"消费金额": [1500, np.nan, 800, 1200],
"城市": ["北京", "上海", np.nan, "广州"],
"注册日期": ["2023-01-15", "2023-02-20", None, "2023-03-10"]
})
2.1.1 检测缺失值
python复制# 查看整体缺失情况
print(df.isnull().sum())
# 可视化缺失情况(需安装missingno)
import missingno as msno
msno.matrix(df)
提示:在实际项目中,我习惯先用missingno生成缺失值热力图,能直观看到哪些列缺失严重。
2.2 处理缺失值的三种策略
2.2.1 直接删除
python复制# 删除含有任何缺失值的行
df_drop_all = df.dropna()
# 只删除特定列缺失的行
df_drop_partial = df.dropna(subset=['消费金额'])
注意:当缺失比例<5%时可以考虑删除,否则会损失太多数据。
2.2.2 填充缺失值
python复制# 均值填充数值列
mean_value = df['消费金额'].mean()
df['消费金额'].fillna(mean_value, inplace=True)
# 众数填充分类列
mode_city = df['城市'].mode()[0]
df['城市'].fillna(mode_city, inplace=True)
# 向前填充时间序列
df['注册日期'].fillna(method='ffill', inplace=True)
2.2.3 高级填充技巧
python复制# 使用KNN填充
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=2)
df[['消费金额']] = imputer.fit_transform(df[['消费金额']])
# 分组均值填充
df['消费金额'] = df.groupby('城市')['消费金额'].transform(
lambda x: x.fillna(x.mean()))
3. 重复数据处理
3.1 识别重复记录
python复制# 检查完全重复的行
print(df.duplicated().sum())
# 检查关键字段重复
print(df.duplicated(subset=['客户ID']).sum())
3.2 处理重复数据
python复制# 保留第一条记录
df_unique = df.drop_duplicates()
# 保留最后一条记录
df_unique_last = df.drop_duplicates(keep='last')
# 自定义去重规则:保留消费金额最大的记录
df_unique_max = df.sort_values('消费金额', ascending=False).drop_duplicates('客户ID')
经验分享:电商数据中经常会出现同一用户多次点击生成的重复记录,建议结合时间戳和用户ID综合判断。
4. 数据类型转换
4.1 常见类型问题
python复制# 查看数据类型
print(df.dtypes)
# 典型问题案例
df_test = pd.DataFrame({
"价格": ["100", "200", "特价"],
"日期": ["2023-01-01", "2023/02/15", "2023年3月20日"]
})
4.2 类型转换方法
python复制# 字符串转数值(处理异常值)
df_test['价格'] = pd.to_numeric(df_test['价格'], errors='coerce')
# 统一日期格式
df_test['日期'] = pd.to_datetime(df_test['日期'], format='mixed')
# 分类数据转换
df['城市'] = df['城市'].astype('category')
# 布尔值转换
df['是否VIP'] = df['消费金额'] > 1000
5. 数据合并高级技巧
5.1 表连接(merge)实战
假设我们有两个表:
python复制# 用户信息表
users = pd.DataFrame({
"user_id": [101, 102, 103, 104],
"会员等级": ["黄金", "白银", "青铜", "黄金"]
})
# 订单表
orders = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004],
"user_id": [101, 101, 102, 105],
"金额": [500, 300, 200, 1000]
})
5.1.1 连接类型对比
python复制# 内连接
pd.merge(orders, users, on='user_id', how='inner')
# 左连接
pd.merge(orders, users, on='user_id', how='left')
# 右连接
pd.merge(orders, users, on='user_id', how='right')
# 全外连接
pd.merge(orders, users, on='user_id', how='outer')
5.1.2 复杂连接场景
python复制# 多键连接
pd.merge(df1, df2, left_on=['key1', 'key2'], right_on=['keyA', 'keyB'])
# 索引连接
pd.merge(df1, df2, left_index=True, right_on='key')
# 连接指示器
pd.merge(orders, users, on='user_id', how='left', indicator=True)
5.2 数据堆叠(concat)技巧
python复制# 垂直堆叠(相同列名)
pd.concat([df1, df2], axis=0)
# 水平拼接(相同行索引)
pd.concat([df1, df2], axis=1)
# 复杂拼接案例
pd.concat([df1, df2],
axis=0,
join='inner',
keys=['2023', '2024'],
names=['年份', '行号'])
6. 电商数据清洗完整案例
6.1 原始数据问题分析
python复制raw_data = pd.DataFrame({
"订单ID": [1001, 1002, 1003, 1004, 1005],
"用户ID": ["A101", "A102", "A101", "A103", None],
"商品": ["手机", "电脑", "手机", "平板", "耳机"],
"价格": ["5999", "8999", "5999", None, "299"],
"数量": [1, 1, 1, 2, None],
"下单时间": ["2023-01-01", "2023-01-01", "2023-01-01",
"2023-01-02", "2023-01-03"]
})
print("原始数据问题:")
print("1. 重复订单(同一用户同一天的同商品订单)")
print("2. 价格缺失和格式问题(字符串类型)")
print("3. 用户ID缺失")
print("4. 数量缺失")
print("5. 日期格式标准化")
6.2 分步清洗方案
python复制# 步骤1:处理价格字段
raw_data['价格'] = pd.to_numeric(raw_data['价格'], errors='coerce')
# 步骤2:填充缺失数量(用1填充)
raw_data['数量'] = raw_data['数量'].fillna(1)
# 步骤3:处理用户ID缺失(创建特殊标记)
raw_data['用户ID'] = raw_data['用户ID'].fillna('UNKNOWN')
# 步骤4:标准化日期格式
raw_data['下单时间'] = pd.to_datetime(raw_data['下单时间'])
# 步骤5:去除重复订单(保留第一个)
raw_data = raw_data.drop_duplicates(
subset=['用户ID', '商品', '下单时间'],
keep='first')
# 步骤6:计算总金额
raw_data['总金额'] = raw_data['价格'] * raw_data['数量']
# 步骤7:价格异常值处理(3σ原则)
price_mean = raw_data['价格'].mean()
price_std = raw_data['价格'].std()
raw_data = raw_data[
(raw_data['价格'] > price_mean - 3*price_std) &
(raw_data['价格'] < price_mean + 3*price_std)
]
6.3 清洗后数据分析
python复制print("\n清洗后数据统计:")
print(f"有效订单数:{len(raw_data)}")
print(f"总销售额:{raw_data['总金额'].sum():.2f}")
print(f"客单价:{raw_data['总金额'].sum()/len(raw_data):.2f}")
# 保存清洗结果
raw_data.to_csv('cleaned_orders.csv', index=False)
7. 性能优化技巧
7.1 大数据量处理
python复制# 分块读取
chunk_iter = pd.read_csv('large_file.csv', chunksize=100000)
results = []
for chunk in chunk_iter:
# 在每块上执行操作
clean_chunk = chunk.dropna(subset=['关键字段'])
results.append(clean_chunk)
df_clean = pd.concat(results)
# 使用dtype参数优化内存
dtypes = {
'id': 'int32',
'price': 'float32',
'category': 'category'
}
df = pd.read_csv('data.csv', dtype=dtypes)
7.2 加速merge操作
python复制# 设置索引加速连接
users.set_index('user_id', inplace=True)
orders.set_index('user_id', inplace=True)
result = orders.join(users, how='left')
# 使用merge的sort参数
pd.merge(orders, users, on='user_id', sort=False)
8. 常见陷阱与解决方案
8.1 内存爆炸问题
当合并大型DataFrame时,可能会遇到内存不足的情况。解决方案:
- 先过滤再合并
- 使用dask等分布式库
- 考虑数据库join操作
8.2 合并键不匹配
python复制# 处理键的类型不一致
df1['key'] = df1['key'].astype(str)
df2['key'] = df2['key'].astype(str)
# 处理键的空白字符
df1['key'] = df1['key'].str.strip()
8.3 重复列名处理
python复制# 合并时自动添加后缀
pd.merge(df1, df2, on='key', suffixes=('_left', '_right'))
# 合并前重命名列
df2 = df2.rename(columns={'amount': 'order_amount'})
9. 最佳实践总结
-
数据质量检查清单:
- 检查缺失值分布
- 验证数据类型
- 检测异常值范围
- 确认业务逻辑一致性
-
处理流程建议:
mermaid复制graph TD A[原始数据] --> B{质量评估} B -->|缺失值| C[填充/删除] B -->|重复值| D[去重处理] B -->|异常值| E[修正/剔除] C --> F[类型转换] D --> F E --> F F --> G[数据合并] G --> H[验证输出] -
性能优化要点:
- 尽量在源头过滤数据
- 使用合适的数据类型
- 避免循环操作,使用向量化方法
- 考虑分块处理大数据集
10. 扩展应用场景
10.1 时间序列数据处理
python复制# 处理不规则时间序列
df.set_index('timestamp').asfreq('D').fillna(method='ffill')
# 合并多个时间序列
pd.merge_asof(prices, volumes, on='datetime', direction='nearest')
10.2 多层索引合并
python复制# 创建多层索引DataFrame
index = pd.MultiIndex.from_tuples(
[('A', 1), ('A', 2), ('B', 1)],
names=['group', 'id'])
df1 = pd.DataFrame({'value': [10, 20, 30]}, index=index)
# 多层索引合并
df2 = pd.DataFrame({
'group': ['A', 'B'],
'id': [1, 1],
'name': ['测试A', '测试B']
})
result = pd.merge(df1.reset_index(), df2, on=['group', 'id'])
10.3 大数据集合并优化
python复制# 使用临时列加速合并
df1['merge_key'] = df1['col1'].astype(str) + '_' + df1['col2'].astype(str)
df2['merge_key'] = df2['colA'].astype(str) + '_' + df2['colB'].astype(str)
merged = pd.merge(df1, df2, on='merge_key')
在实际项目中,我发现数据清洗最耗时的往往不是技术实现,而是与业务部门确认各种特殊情况的处理规则。比如:
- "用户年龄为0"是表示未知还是真实新生儿?
- "价格为空"是因为商品免费还是数据缺失?
- "同一用户短时间内多次购买相同商品"是刷单还是正常行为?
这些业务逻辑的判断往往比写代码更重要。建议在开始清洗前,先与业务方确认好各种边界情况的处理原则,可以节省大量返工时间。