本文目录导读:

- 目录导读
- 为什么需要数据分组与过滤?
- Pandas分组过滤的核心函数
- 案例实战:电商销售数据分组过滤
- 案例二:用户行为数据——按活跃度分组过滤
- 案例三:金融交易数据——按时间窗口分组过滤异常
- 高频问题与避坑指南
- 实战扩展:用SQL思维理解Pandas分组过滤
- 总结本文核心要点:
Python案例深度解析:用Pandas高效实现数据分组过滤(附实战代码)
目录导读
- 为什么需要数据分组与过滤? —— 从业务场景理解核心痛点
- Pandas分组过滤的核心函数 —— groupby()、filter()、apply()详解
- 电商销售数据——按品类分组过滤高绩效商品
- 用户行为数据——按活跃度分组过滤异常值
- 金融交易数据——按时间窗口分组过滤极端波动
- 高频问题与避坑指南 —— 常见报错与性能优化
- 实战扩展:结合SQL思维用Pandas实现复杂过滤
为什么需要数据分组与过滤?
在日常数据分析工作中,我们常面临这样的场景:一张包含数百万行的销售表,需要分别计算每个品类的平均销售额,并只保留高于平均值的商品;或者从用户行为日志中筛选出每个用户最近7天活跃次数超过10次的记录,这些需求的核心都是先分组(划分逻辑单元),再过滤(按组内规则筛选)。
传统方法(如循环遍历)不仅代码冗长,且效率极低,Pandas通过向量化操作,让分组过滤变得像写SQL一样简洁,本篇文章将用三个真实案例,手把手教你掌握这一核心技巧。
Pandas分组过滤的核心函数
1 groupby()——数据的逻辑切分
df_grouped = df.groupby('column_name')
返回的是一个DataFrameGroupBy对象,此时数据并未真正计算,而是定义了分组规则。
2 filter()——按组内条件筛选行
df_filtered = df.groupby('group_col').filter(lambda x: x['value'].mean() > threshold)
filter函数会对每个分组执行一个返回布尔值的函数,保留整个分组或丢弃整个分组,注意:它返回的是原始DataFrame的子集。
3 transform()与布尔索引——更灵活的过滤
当需要基于组内计算结果(如Z-score、排名)筛选行时,transform更合适:
z_scores = df.groupby('group')['value'].transform(lambda x: (x - x.mean()) / x.std())
df_filtered = df[abs(z_scores) < 2]
4 性能对比表
| 方法 | 适用场景 | 性能 | 代码简洁度 |
|---|---|---|---|
| filter() | 按组统计量保留整个分组 | ||
| transform() | 按行级条件过滤 | ||
| apply() | 复杂自定义逻辑 |
案例实战:电商销售数据分组过滤
1 场景描述
数据集orders.csv包含:order_id, category, sales_amount, date,需求:找出每个品类中,销售额超过该品类平均销售额的订单。
2 数据准备
import pandas as pd
import numpy as np
# 模拟数据
np.random.seed(42)
data = {
'category': ['A']*1000 + ['B']*1000 + ['C']*1000,
'sales_amount': np.random.normal(500, 100, 3000),
'date': pd.date_range('2024-01-01', periods=3000, freq='D')
}
df = pd.DataFrame(data)
3 实现代码
使用filter()
def above_group_mean(group):
mean_val = group['sales_amount'].mean()
return group['sales_amount'] > mean_val
# 注意:filter返回布尔值,需用lambda包装
filtered_df1 = df.groupby('category').filter(lambda x: x['sales_amount'].mean() > 500) # 错误!这是过滤整个组
filtered_df1 = df.groupby('category').filter(lambda x: (x['sales_amount'] > x['sales_amount'].mean()).any()) # 错误!any()导致组内只要有一个大于就保留全组
# 正确做法:使用transform
filtered_df1 = df[df.groupby('category')['sales_amount'].transform(lambda x: x > x.mean())]
print(filtered_df1.shape) # 输出:(1500, 3) 约50%数据被保留
使用apply()
def mark_above(group):
group['above_mean'] = group['sales_amount'] > group['sales_amount'].mean()
return group
df_marked = df.groupby('category', group_keys=False).apply(mark_above)
filtered_df2 = df_marked[df_marked['above_mean']].drop('above_mean', axis=1)
4 结果验证
# 验证品类A的平均值
mean_A = df[df['category']=='A']['sales_amount'].mean()
print(f"品类A平均销售额: {mean_A:.2f}")
print(f"品类A过滤后数据量: {filtered_df1[filtered_df1['category']=='A'].shape[0]}")
# 应约等于原始量的一半
案例二:用户行为数据——按活跃度分组过滤
1 场景描述
用户日志表user_actions.csv:user_id, action_type, timestamp,需求:保留每个用户最近7天中,活跃天数≥3天的用户的所有记录。
2 实现代码
# 生成模拟数据
users = [f'U{i}' for i in range(100)]
dates = pd.date_range('2024-10-01', '2024-10-31', freq='H')
df_log = pd.DataFrame({
'user_id': np.random.choice(users, 5000),
'timestamp': np.random.choice(dates, 5000)
})
# 先计算每个用户每天活跃次数
df_log['date'] = df_log['timestamp'].dt.date
daily_active = df_log.groupby(['user_id', 'date']).size().reset_index(name='counts')
# 再统计每个用户的活跃天数(按过去7天滑动窗口)
# 注意:实际推荐使用动态窗口,这里简化处理
recent_7days = df_log[df_log['timestamp'] >= '2024-10-25']
user_active_days = recent_7days.groupby('user_id')['date'].nunique()
# 过滤出活跃天数≥3的用户
active_users = user_active_days[user_active_days >= 3].index
filtered_log = df_log[df_log['user_id'].isin(active_users)]
print(f"原始用户数: {df_log['user_id'].nunique()}, 过滤后用户数: {len(active_users)}")
3 关键点
- 使用
isin()实现分组过滤:属于“提前计算分组统计量,再回查原表”的模式,适合分组数量较少的情况。 - 对于更大数据集,可先对
user_id建立索引加速。
案例三:金融交易数据——按时间窗口分组过滤异常
1 场景描述
股票交易表trades.csv:symbol, price, volume, timestamp,需求:对每支股票,过滤掉价格偏离其最近20笔交易中位数超过3倍IQR(四分位距)的异常交易。
2 实现代码(使用rolling+groupby)
# 模拟数据
stocks = ['AAPL', 'GOOG', 'MSFT']
df_trade = pd.DataFrame({
'symbol': np.random.choice(stocks, 5000),
'price': np.random.normal(150, 20, 5000),
'timestamp': pd.date_range('2024-01-01', periods=5000, freq='T')
}).sort_values(['symbol', 'timestamp'])
# 定义过滤函数
def filter_outliers(group):
# 滚动计算中位数和IQR
rolling_med = group['price'].rolling(20, min_periods=5).median()
rolling_q1 = group['price'].rolling(20, min_periods=5).quantile(0.25)
rolling_q3 = group['price'].rolling(20, min_periods=5).quantile(0.75)
iqr = rolling_q3 - rolling_q1
lower_bound = rolling_med - 1.5 * iqr
upper_bound = rolling_med + 1.5 * iqr
# 返回布尔Series
return (group['price'] >= lower_bound) & (group['price'] <= upper_bound)
# 应用过滤
mask = df_trade.groupby('symbol', group_keys=False).apply(filter_outliers)
clean_trades = df_trade[mask]
print(f"原始交易数: {len(df_trade)}, 过滤后: {len(clean_trades)}")
# 理论上会过滤掉约0.7%的极端值
3 注意事项
rolling()必须在分组后使用,且需先按时间排序。group_keys=False避免生成多余索引。
高频问题与避坑指南
1 Q:filter()和transform()都能过滤行,区别是什么?
A:
filter():对整个分组操作,返回布尔值,如果返回True,保留整个分组;返回False,丢弃整个分组。transform():对组内每个元素操作,返回与原DataFrame等长的Series,然后通过布尔索引过滤行。
选择原则:需要按组保留整体(如保留整个高均值品类)用filter;需要按行条件(如只保留高于均值的行)用transform。
2 Q:为什么groupby().filter(lambda x: condition)有时会报错?
A:filter传入的函数必须返回布尔值(True/False),但常见错误是返回一个布尔Series。
# 错误
df.groupby('A').filter(lambda x: x['B'] > 0) # 返回布尔Series
# 正确
df.groupby('A').filter(lambda x: (x['B'] > 0).all()) # 返回单个布尔值
3 Q:数据量巨大(百万行以上)时如何优化?
A:
- 优先使用
transform替代apply,因为transform有C语言级别优化。 - 减少分组数量:如果分组键有大量唯一值(如用户ID),先对数据做聚合统计,再用
isin过滤。 - 使用
pandas>=1.5.0中的groupby().sample()进行采样测试。
4 Q:需要按多个列分组过滤怎么做?
A:在groupby中传入列表即可:
df.groupby(['year', 'month']).filter(lambda x: x['sales'].sum() > 10000)
或者使用transform:
df[df.groupby(['year', 'month'])['sales'].transform('sum') > 10000]
实战扩展:用SQL思维理解Pandas分组过滤
对于熟悉SQL的读者,可以将Pandas操作对应到SQL语法:
| SQL语法 | Pandas等价操作 |
|---|---|
GROUP BY category HAVING AVG(sales) > 500 |
df.groupby('category').filter(lambda x: x['sales'].mean() > 500) |
WHERE sales > (SELECT AVG(sales) FROM t WHERE category = outer.category) |
df[df['sales'] > df.groupby('category')['sales'].transform('mean')] |
ROW_NUMBER() OVER (PARTITION BY user ORDER BY date) = 1 |
df.groupby('user').apply(lambda x: x.sort_values('date').iloc[0]) |
高级技巧:当遇到“分组内排名前N”的需求时,用rank()方法:
# 取每个品类销售额最高的前10%订单
df['rank'] = df.groupby('category')['sales'].rank(method='dense', pct=True)
top10pct = df[df['rank'] <= 0.1]
总结本文核心要点:
- 分组过滤的核心是先定义分组逻辑,再按组内统计量或位置条件筛选。
transform()是最灵活的过滤方式,适合按行筛选;filter()适合按组整体保留。- 性能优化方向:减少
apply使用,用向量化操作代替循环,对大分组先聚合再回查。 - 尽量将问题拆解为:分组计算 → 得到过滤条件 → 布尔索引,这是最通用的模式。
延伸思考:如果需求变成“将每个组内销售额最高的订单标记为'冠军'”,你会如何实现?提示:结合transform('max')与布尔索引。
(全文完)