`groupby` 高级聚合:`agg`, `transform`, `filter` 的深度解析

groupby 高级聚合:agg, transform, filter 的深度解析(一场 Pandas 魔法秀)

各位观众,各位程序员,各位数据科学界的魔法师们!欢迎来到 Pandas 魔法学院,我是今天的首席大法师——你的老朋友(或者即将成为老朋友)程序员小P。🧙‍♂️

今天,我们要解开 Pandas 中 groupby 的高级聚合咒语,让你的数据分析能力瞬间提升一个维度! 别害怕,虽然听起来高深莫测,但只要跟着我的节奏,保证你学会 agg, transform, 和 filter 三大绝技,成为数据分析界的 Gandalf!

1. groupby 的基础回顾:让数据井然有序

在我们深入魔法核心之前,先来回顾一下 groupby 的基础。groupby 就像一个超级整理员,它能根据你指定的列,将 DataFrame 分组,然后你就可以对每个组进行操作了。

想象一下,你有一堆扑克牌,groupby 就是能帮你按照花色(红桃、方块、梅花、黑桃)把它们分开放好的工具。

import pandas as pd

data = {'花色': ['红桃', '方块', '红桃', '梅花', '黑桃', '方块', '黑桃', '梅花'],
        '点数': [2, 3, 4, 5, 6, 7, 8, 9]}
df = pd.DataFrame(data)

grouped = df.groupby('花色')
print(grouped) # <pandas.core.groupby.generic.DataFrameGroupBy object at 0x...> 别慌,这是个分组对象

现在,grouped 对象里就包含了按照花色分组后的数据。你可以使用它来做一些基础的操作,比如求和、平均值等等。

print(grouped.sum())
#    点数
# 花色
# 方块   10
# 梅花   14
# 红桃    6
# 黑桃   14

print(grouped.mean())
#      点数
# 花色
# 方块  5.0
# 梅花  7.0
# 红桃  3.0
# 黑桃  7.0

这些都是小儿科,真正的魔法还在后面!

2. agg:聚合的艺术,化繁为简的利器

agg (aggregate) 函数就像一个多才多艺的厨师,它允许你对每个分组应用一个或多个聚合函数,将复杂的数据精炼成简洁的摘要。

2.1 单个聚合函数:简单直接,一目了然

最简单的用法是传入一个函数名,agg 会将这个函数应用到每个分组的指定列上。

print(grouped['点数'].agg('sum'))
# 花色
# 方块    10
# 梅花    14
# 红桃     6
# 黑桃    14
# Name: 点数, dtype: int64

print(grouped['点数'].agg(['sum', 'mean', 'max']))
#      sum  mean  max
# 花色
# 方块   10   5.0    7
# 梅花   14   7.0    9
# 红桃    6   3.0    4
# 黑桃   14   7.0    8

看到没?一行代码搞定求和、均值和最大值!简直是效率神器!

2.2 多个聚合函数:百花齐放,各显神通

agg 的真正威力在于它可以同时应用多个聚合函数。你可以传入一个列表,每个元素都是一个函数名,agg 会分别应用这些函数,并将结果以 DataFrame 的形式返回。

import numpy as np

print(grouped['点数'].agg([np.sum, np.mean, np.std]))
#        sum  mean       std
# 花色
# 方块   10   5.0  2.828427
# 梅花   14   7.0  2.828427
# 红桃    6   3.0  1.414214
# 黑桃   14   7.0  1.414214

除了内置的函数,你还可以使用自定义函数!这才是真正的魔法!

2.3 自定义聚合函数:量身定制,独一无二

你可以定义自己的聚合函数,然后将它们传递给 agg。这让你可以根据具体的需求,进行高度定制化的聚合操作。

def my_sum_plus_one(x):
    return x.sum() + 1

print(grouped['点数'].agg(my_sum_plus_one))
# 花色
# 方块    11
# 梅花    15
# 红桃     7
# 黑桃    15
# Name: 点数, dtype: int64

这个自定义函数将每个分组的点数求和,然后加 1。虽然这个例子很简单,但你可以根据实际情况,定义更加复杂的函数。

2.4 字典式聚合:指哪打哪,精准打击

更高级的用法是使用字典来指定不同的列应用不同的聚合函数。字典的键是列名,值是函数名或函数名列表。

print(grouped.agg({'点数': ['sum', 'mean'], '花色': 'count'}))
#      点数             花色
#       sum  mean  count
# 花色
# 方块   10   5.0      2
# 梅花   14   7.0      2
# 红桃    6   3.0      2
# 黑桃   14   7.0      2

在这个例子中,我们对 ‘点数’ 列应用了 summean 函数,对 ‘花色’ 列应用了 count 函数。

总结:agg 的优点

  • 简洁高效: 一行代码完成复杂的聚合操作。
  • 灵活定制: 支持内置函数、自定义函数和字典式聚合。
  • 可读性强: 代码逻辑清晰,易于理解。

3. transform:变形的艺术,保持原样的魔法

transform 函数就像一个神奇的镜子,它可以对每个分组应用一个函数,并将结果广播回原始 DataFrame 的大小。也就是说,transform 会返回一个与原始 DataFrame 具有相同索引和形状的新列。

3.1 标准化:让数据站在同一起跑线

一个常见的用法是标准化数据,将每个分组的数据转换为均值为 0,标准差为 1 的分布。

def standardize(x):
    return (x - x.mean()) / x.std()

df['标准化点数'] = grouped['点数'].transform(standardize)
print(df)
#    花色  点数     标准化点数
# 0  红桃   2 -0.707107
# 1  方块   3 -0.707107
# 2  红桃   4  0.707107
# 3  梅花   5 -0.707107
# 4  黑桃   6 -0.707107
# 5  方块   7  0.707107
# 6  黑桃   8  0.707107
# 7  梅花   9  0.707107

在这个例子中,我们定义了一个 standardize 函数,它计算每个分组的均值和标准差,然后将数据标准化。transform 函数将这个函数应用到每个分组,并将结果添加到了原始 DataFrame 中。

3.2 填充缺失值:亡羊补牢,犹未晚矣

transform 还可以用于填充缺失值。你可以使用分组的均值、中位数或其他统计量来填充缺失值。

data = {'花色': ['红桃', '方块', '红桃', '梅花', '黑桃', '方块', '黑桃', '梅花'],
        '点数': [2, 3, 4, 5, 6, None, 8, 9]}
df = pd.DataFrame(data)
grouped = df.groupby('花色')

df['点数'] = grouped['点数'].transform(lambda x: x.fillna(x.mean()))
print(df)
#    花色   点数
# 0  红桃  2.0
# 1  方块  3.0
# 2  红桃  4.0
# 3  梅花  5.0
# 4  黑桃  6.0
# 5  方块  7.0
# 6  黑桃  8.0
# 7  梅花  9.0

在这个例子中,我们使用 transform 函数将每个分组的缺失值填充为该分组的均值。

3.3 计算排名:谁是第一,一目了然

transform 还可以用于计算每个分组的排名。

df['排名'] = grouped['点数'].transform('rank')
print(df)
#    花色   点数   排名
# 0  红桃  2.0  1.0
# 1  方块  3.0  1.0
# 2  红桃  4.0  2.0
# 3  梅花  5.0  1.0
# 4  黑桃  6.0  1.0
# 5  方块  7.0  2.0
# 6  黑桃  8.0  2.0
# 7  梅花  9.0  2.0

在这个例子中,我们使用 transform 函数计算每个分组的点数排名。

总结:transform 的优点

  • 保持形状: 返回与原始 DataFrame 具有相同索引和形状的新列。
  • 灵活应用: 可以应用各种函数,包括标准化、填充缺失值、计算排名等。
  • 易于集成: 可以将 transform 的结果添加到原始 DataFrame 中,方便后续分析。

4. filter:过滤的艺术,沙里淘金的秘诀

filter 函数就像一个严苛的守门员,它根据你指定的条件,过滤掉不符合条件的分组。只有当整个分组满足条件时,该分组的数据才会被保留。

4.1 筛选大小:留下精华,剔除糟粕

一个常见的用法是筛选出分组大小大于某个阈值的分组。

data = {'花色': ['红桃', '方块', '红桃', '梅花', '黑桃', '方块', '黑桃', '梅花', '草花'],
        '点数': [2, 3, 4, 5, 6, 7, 8, 9, 10]}
df = pd.DataFrame(data)
grouped = df.groupby('花色')

filtered_df = grouped.filter(lambda x: len(x) > 1)
print(filtered_df)
#    花色  点数
# 0  红桃   2
# 1  方块   3
# 2  红桃   4
# 3  梅花   5
# 4  黑桃   6
# 5  方块   7
# 6  黑桃   8
# 7  梅花   9

在这个例子中,我们使用 filter 函数筛选出大小大于 1 的分组。只有 ‘红桃’、’方块’、’梅花’ 和 ‘黑桃’ 这几个分组满足条件,所以只有这些分组的数据被保留了下来。 ‘草花’因为只有一个数据,所以被无情地过滤掉了。

4.2 筛选总和:留下高富帅,剔除屌丝

你还可以根据分组的总和、均值或其他统计量来过滤分组。

filtered_df = grouped.filter(lambda x: x['点数'].sum() > 10)
print(filtered_df)
#    花色  点数
# 1  方块   3
# 3  梅花   5
# 4  黑桃   6
# 5  方块   7
# 6  黑桃   8
# 7  梅花   9
# 8  草花  10

在这个例子中,我们使用 filter 函数筛选出点数总和大于 10 的分组。

4.3 筛选均值:留下学霸,剔除学渣

你也可以根据分组的均值来筛选分组。

filtered_df = grouped.filter(lambda x: x['点数'].mean() > 6)
print(filtered_df)
#    花色  点数
# 3  梅花   5
# 6  黑桃   8
# 7  梅花   9
# 8  草花  10

在这个例子中,我们使用 filter 函数筛选出点数均值大于 6 的分组。

总结:filter 的优点

  • 分组过滤: 根据分组的整体特征进行过滤,而不是单个数据点。
  • 条件灵活: 可以使用各种条件,包括分组大小、总和、均值等。
  • 数据清洗: 可以用于清洗数据,去除不符合条件的分组。

5. 实战案例:用魔法解决实际问题

理论讲完了,现在让我们用一个实战案例来巩固一下所学的知识。

假设你是一家电商公司的数据分析师,你有一份用户订单数据,包含用户ID、订单金额和下单时间。

import pandas as pd
import numpy as np

# 生成模拟数据
np.random.seed(42)  # 设置随机种子,保证结果可重复
num_users = 10
num_orders = 50

user_ids = np.random.randint(1, num_users + 1, num_orders)
order_amounts = np.random.randint(50, 500, num_orders)
order_dates = pd.to_datetime('2023-01-01') + pd.to_timedelta(np.random.randint(0, 365, num_orders), unit='D')

data = {'用户ID': user_ids,
        '订单金额': order_amounts,
        '下单时间': order_dates}
df = pd.DataFrame(data)

print(df.head())
#    用户ID  订单金额        下单时间
# 0     3   306 2023-08-02
# 1     8   101 2023-04-25
# 2     6   176 2023-12-15
# 3     4   398 2023-09-14
# 4     6   257 2023-03-20

现在,你需要回答以下几个问题:

  1. 每个用户的总订单金额是多少?(使用 agg
  2. 每个订单金额占该用户总订单金额的比例是多少?(使用 transform
  3. 筛选出总订单金额大于 1000 的用户。(使用 filter

解答:

# 1. 每个用户的总订单金额是多少?
grouped = df.groupby('用户ID')
user_total_amount = grouped['订单金额'].agg('sum')
print("每个用户的总订单金额:n", user_total_amount)

# 2. 每个订单金额占该用户总订单金额的比例是多少?
df['订单金额占比'] = grouped['订单金额'].transform(lambda x: x / x.sum())
print("n每个订单金额占该用户总订单金额的比例:n", df.head())

# 3. 筛选出总订单金额大于 1000 的用户。
filtered_df = grouped.filter(lambda x: x['订单金额'].sum() > 1000)
print("n总订单金额大于 1000 的用户的订单数据:n", filtered_df.head())

通过这个案例,你应该对 agg, transform, 和 filter 的用法有了更深入的理解。

6. 总结:掌握魔法,走向巅峰

恭喜你,完成了今天的 Pandas 魔法课程! 🎉

通过学习 agg, transform, 和 filter,你已经掌握了 groupby 的高级聚合技巧,可以更加灵活地处理和分析数据。

记住,练习是掌握魔法的关键。多尝试不同的案例,多思考不同的用法,你就能将这些魔法咒语运用自如,成为真正的数据分析大师!

下次再见,祝你编程愉快! 🚀

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注