Python案例如何用Pandas做数据分组过滤

wen python案例 1

本文目录导读:

Python案例如何用Pandas做数据分组过滤

  1. 目录导读
  2. 为什么需要数据分组与过滤?
  3. Pandas分组过滤的核心函数
  4. 案例实战:电商销售数据分组过滤
  5. 案例二:用户行为数据——按活跃度分组过滤
  6. 案例三:金融交易数据——按时间窗口分组过滤异常
  7. 高频问题与避坑指南
  8. 实战扩展:用SQL思维理解Pandas分组过滤
  9. 总结本文核心要点:

Python案例深度解析:用Pandas高效实现数据分组过滤(附实战代码)

目录导读

  1. 为什么需要数据分组与过滤? —— 从业务场景理解核心痛点
  2. Pandas分组过滤的核心函数 —— groupby()、filter()、apply()详解
  3. 电商销售数据——按品类分组过滤高绩效商品
  4. 用户行为数据——按活跃度分组过滤异常值
  5. 金融交易数据——按时间窗口分组过滤极端波动
  6. 高频问题与避坑指南 —— 常见报错与性能优化
  7. 实战扩展:结合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.csvuser_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.csvsymbol, 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)有时会报错?

Afilter传入的函数必须返回布尔值(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

  1. 优先使用transform替代apply,因为transform有C语言级别优化。
  2. 减少分组数量:如果分组键有大量唯一值(如用户ID),先对数据做聚合统计,再用isin过滤。
  3. 使用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]

总结本文核心要点:

  1. 分组过滤的核心是先定义分组逻辑,再按组内统计量或位置条件筛选
  2. transform()是最灵活的过滤方式,适合按行筛选;filter()适合按组整体保留。
  3. 性能优化方向:减少apply使用,用向量化操作代替循环,对大分组先聚合再回查。
  4. 尽量将问题拆解为:分组计算 → 得到过滤条件 → 布尔索引,这是最通用的模式。

延伸思考:如果需求变成“将每个组内销售额最高的订单标记为'冠军'”,你会如何实现?提示:结合transform('max')与布尔索引。

(全文完)

抱歉,评论功能暂时关闭!