10 章: 数据聚合和分组操作

本章内容 🤔

本节重点介绍数据聚合与分组操作,这是数据分析的关键部分。我们将学习如何对数据进行分类,并对每个组应用函数——这是许多数据工作流程中的基本步骤。

核心操作 ⚙️

我们将学习:

  • 将 pandas 对象拆分为组。
  • 计算组摘要统计信息(计数、平均值等)。
  • 在组内应用转换。
  • 计算数据透视表和交叉表。
  • 执行分位数分析。
  • 使用 transformapply

为什么这很重要?📈

分组和聚合数据有助于我们:

  • 通过汇总大型数据集获得见解
  • 比较不同的组。
  • 准备数据以进行分析或可视化。
  • 发现模式

如何理解分组操作

“拆分-应用-合并”范式 💡

Hadley Wickham 的 “拆分-应用-合并” (split-apply-combine) 范式是理解数据处理的强大方式。

  1. 拆分 (Split): 根据“键”将数据分成组。
  2. 应用 (Apply): 对每个组应用一个函数。
  3. 合并 (Combine): 将结果合并为最终输出。

“拆分-应用-合并”可视化 📊

graph LR
    A[数据] --> B(按键拆分)
    B --> C1[组 1]
    B --> C2[组 2]
    B --> C3[组 3]
    C1 --> D1(应用函数)
    C2 --> D2(应用函数)
    C3 --> D3(应用函数)
    D1 --> E[合并结果]
    D2 --> E
    D3 --> E

graph LR
    A[数据] --> B(按键拆分)
    B --> C1[组 1]
    B --> C2[组 2]
    B --> C3[组 3]
    C1 --> D1(应用函数)
    C2 --> D2(应用函数)
    C3 --> D3(应用函数)
    D1 --> E[合并结果]
    D2 --> E
    D3 --> E

分组键 🔑

键可以是:

  • 列表或数组。
  • DataFrame 列名。
  • 将值映射到组名的字典或 Series。
  • 应用于索引的函数。

这些本质上都是用于拆分对象的数组值的快捷方式。

示例:简单分组聚合

Split-Apply-Combine

  • 键和数据:带有“Key”和“Data”列的表格。
  • 拆分:数据按“Key”拆分为组(A、B、C)。
  • 应用Sum 函数应用于每个组的“Data”。
  • 合并:总和合并到一个新表中。

Pandas 实战:创建 DataFrame

让我们创建一个示例 DataFrame:

import numpy as np  # 导入 NumPy 库,用于数值计算
import pandas as pd  # 导入 pandas 库,用于数据分析

# 创建一个 DataFrame
df = pd.DataFrame({
    "key1": ["a", "a", "b", "b", "a"],  # key1 列,包含字符串
    "key2": [1, 2, 1, 2, 1],  # key2 列,包含整数
    "data1": np.random.randn(5),  # data1 列,包含 5 个随机数
    "data2": np.random.randn(5)  # data2 列,包含 5 个随机数
})
df  # 显示 DataFrame
key1 key2 data1 data2
0 a 1 0.279929 2.540148
1 a 2 1.170181 -1.039183
2 b 1 -2.036848 -0.463813
3 b 2 0.725645 0.443820
4 a 1 -0.327967 1.106586

此 DataFrame 有两个键列(key1key2)和两个数据列(data1data2)。

基本 GroupBy 操作

按单列分组

计算 key1 中每个组的 data1 的平均值:

grouped = df["data1"].groupby(df["key1"])  # 按 key1 分组 data1
grouped.mean()  # 计算每个组的平均值
key1
a    0.374047
b   -0.655602
Name: data1, dtype: float64
  • df["data1"]:选择 data1 列。
  • .groupby(df["key1"]):按 key1 列分组。
  • grouped:一个 GroupBy 对象,存储分组信息。
  • .mean():计算每个组的平均值。

按多列分组

按多列分组(分层索引):

grouped = df.groupby(["key1", "key2"])  # 按 key1 和 key2 分组
grouped.agg({
    "data1": ["mean", "std"],  # 对 data1 计算平均值和标准差
    "data2": ["mean", "std"]   # 对 data2 计算平均值和标准差
})
data1 data2
mean std mean std
key1 key2
a 1 -0.024019 0.429847 1.823367 1.013682
2 1.170181 NaN -1.039183 NaN
b 1 -2.036848 NaN -0.463813 NaN
2 0.725645 NaN 0.443820 NaN

数据按 key1key2 的组合进行分组。

展开 (Unstacking)

unstack() 重塑结果:

means = df.groupby(["key1", "key2"])["data1"].mean()  # 按 key1 和 key2 分组,计算 data1 的平均值
means.unstack()  # 将结果展开,使 key2 成为列
key2 1 2
key1
a -0.024019 1.170181
b -2.036848 0.725645

使用 Series 和数组分组

键可以是外部 Series 或数组:

states = np.array(["OH", "CA", "CA", "OH", "OH", "CA", "OH"])  # 外部数组 states
years = np.array([2005, 2005, 2006, 2005, 2006, 2005, 2006])  # 外部数组 years

# 创建新的 df 以匹配 states 和 years 的长度
df_ext = pd.DataFrame({
    "key1": ["a", "a", "b", "b", "a", "b", "a"],
    "key2": [1, 2, 1, 2, 1, 2, 1],
    "data1": np.random.randn(7),
    "data2": np.random.randn(7)
})

result = df_ext["data1"].groupby([states, years]).mean()  # 使用外部数组分组
print("\n分组结果:")
print(result)

print("\n展开结果:")
print(result.unstack())

分组结果:
CA  2005   -0.660362
    2006   -1.050043
OH  2005   -0.220197
    2006    0.282380
Name: data1, dtype: float64

展开结果:
        2005      2006
CA -0.660362 -1.050043
OH -0.220197  0.282380

这里,我们按外部数组 statesyearsdata1 进行分组。

直接使用列名分组

如果分组信息在 DataFrame 中,直接使用列名:

df.groupby("key1").mean()  # 按 key1 分组,计算所有数值列的平均值
key2 data1 data2
key1
a 1.333333 0.374047 0.869184
b 1.500000 -0.655602 -0.009996

非数值列 key1 会被自动排除,因为它是一个干扰列

df.groupby(["key1", "key2"]).mean()  # 按 key1 和 key2 分组,计算所有数值列的平均值
data1 data2
key1 key2
a 1 -0.024019 1.823367
2 1.170181 -1.039183
b 1 -2.036848 -0.463813
2 0.725645 0.443820

组大小

size() 显示每个组中的数据点数量:

df.groupby(["key1", "key2"]).size()  # 按 key1 和 key2 分组,计算每个组的大小
key1  key2
a     1       2
      2       1
b     1       1
      2       1
dtype: int64

处理分组键中的缺失值

默认情况下,分组键中的缺失值会被排除。使用 dropna=False 包含它们:

df.groupby("key1", dropna=False).size()  # 按 key1 分组,包括缺失值(如果有)
key1
a    3
b    2
dtype: int64

迭代组

使用单键迭代

GroupBy 支持迭代,生成组名和数据块:

for name, group in df.groupby("key1"):  # 按 key1 迭代组
    print(f"组名: {name}")  # 打印组名
    print(group)  # 打印组数据
组名: a
  key1  key2     data1     data2
0    a     1  0.279929  2.540148
1    a     2  1.170181 -1.039183
4    a     1 -0.327967  1.106586
组名: b
  key1  key2     data1     data2
2    b     1 -2.036848 -0.463813
3    b     2  0.725645  0.443820

使用多键迭代

使用多个键时,组名是一个元组:

for (k1, k2), group in df.groupby(["key1", "key2"]):  # 按 key1 和 key2 迭代组
    print(f"组键: {(k1, k2)}")  # 打印组键
    print(group)  # 打印组数据
组键: ('a', np.int64(1))
  key1  key2     data1     data2
0    a     1  0.279929  2.540148
4    a     1 -0.327967  1.106586
组键: ('a', np.int64(2))
  key1  key2     data1     data2
1    a     2  1.170181 -1.039183
组键: ('b', np.int64(1))
  key1  key2     data1     data2
2    b     1 -2.036848 -0.463813
组键: ('b', np.int64(2))
  key1  key2     data1    data2
3    b     2  0.725645  0.44382

创建数据块字典

创建组数据的字典:

pieces = dict(list(df.groupby("key1")))  # 将按 key1 分组的结果转换为字典
pieces["b"]  # 获取 key1 为 "b" 的组
key1 key2 data1 data2
2 b 1 -2.036848 -0.463813
3 b 2 0.725645 0.443820

按列分组 (axis=1)

使用 axis="columns" 按列分组:

# 创建一个映射字典, 将列名映射到新的分组名称
grouped = df.groupby({
    "key1": "key",
    "key2": "key",
    "data1": "data",
    "data2": "data"
}, axis="columns")

for group_key, group_values in grouped:  # 迭代列组
    print(group_key)  # 打印列组键
    print(group_values)  # 打印列组数据
data
      data1     data2
0  0.279929  2.540148
1  1.170181 -1.039183
2 -2.036848 -0.463813
3  0.725645  0.443820
4 -0.327967  1.106586
key
  key1  key2
0    a     1
1    a     2
2    b     1
3    b     2
4    a     1

选择要聚合的列

选择单列 (SeriesGroupBy)

GroupBy 对象进行索引以聚合特定列:

df.groupby(["key1", "key2"])["data2"].mean()  # 按 key1 和 key2 分组,计算 data2 的平均值
key1  key2
a     1       1.823367
      2      -1.039183
b     1      -0.463813
      2       0.443820
Name: data2, dtype: float64

这是 df["data2"].groupby([df["key1"], df["key2"]]).mean() 的简写。

选择多列 (DataFrameGroupBy)

df.groupby(["key1", "key2"])[["data2"]].mean()  # 按 key1 和 key2 分组,计算 data2 的平均值
data2
key1 key2
a 1 1.823367
2 -1.039183
b 1 -0.463813
2 0.443820

这等同于 df[["data2"]].groupby([df["key1"], df["key2"]]).mean()

使用字典和 Series 分组

使用字典或 Series 进行分组:

people = pd.DataFrame(np.random.standard_normal((5, 5)),  # 创建一个 5x5 的随机数 DataFrame
                   columns=["a", "b", "c", "d", "e"],  # 列名
                   index=["Joe", "Steve", "Wanda", "Jill", "Trey"])  # 行名
people.iloc[2:3, [1, 2]] = np.nan  # 将一些值设置为 NaN

mapping = {"a": "red", "b": "red", "c": "blue",  # 创建一个列映射字典
           "d": "blue", "e": "red", "f" : "orange"}

by_column = people.groupby(mapping, axis="columns")  # 使用映射字典按列分组
by_column.sum()  # 计算每个列组的总和
blue red
Joe 0.145500 -1.705661
Steve 1.866356 3.455577
Wanda -1.246866 0.514859
Jill 1.844919 -1.084912
Trey 1.503649 1.171454

mapping 指定列分组。

使用函数分组

使用函数定义组映射(每个索引值调用一次):

people.groupby(len).sum()  # 按行名的长度分组,计算总和
a b c d e
3 -2.341368 1.014384 -0.258578 0.404078 -0.378678
4 -0.114652 -0.437672 1.508799 1.839768 0.638866
5 2.401893 1.772392 1.703262 -1.083772 -0.203849

按索引级别分组

按分层索引的级别分组:

columns = pd.MultiIndex.from_arrays([["US", "US", "US", "JP", "JP"],  # 创建一个多级列索引
                                    [1, 3, 5, 1, 3]],
                                   names=["cty", "tenor"])
hier_df = pd.DataFrame(np.random.standard_normal((4, 5)), columns=columns)  # 创建一个具有多级列索引的 DataFrame
hier_df.groupby(level="cty", axis="columns").count()  # 按列的 "cty" 级别分组,计数
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3

数据聚合

优化的聚合方法

聚合将数组转换为标量。优化方法:

  • countsummeanmedianstdvar
  • minmaxprodfirstlast
  • anyallcummincummaxcumsumcumprod
  • nthohlcquantileranksize

使用自定义聚合函数

使用 agg 定义自定义函数:

def peak_to_peak(arr):  # 定义一个函数,计算数组的极差(最大值 - 最小值)
    return arr.max() - arr.min()

grouped = df.groupby("key1")  # 按 key1 分组
grouped.agg(peak_to_peak)  # 应用自定义函数
key2 data1 data2
key1
a 1 1.498148 3.579331
b 1 2.762494 0.907632

describe 方法

使用非聚合方法,如 describe

grouped.describe()  # 获取每个组的描述性统计信息
key2 data1 data2
count mean std min 25% 50% 75% max count mean ... 75% max count mean std min 25% 50% 75% max
key1
a 3.0 1.333333 0.577350 1.0 1.00 1.0 1.50 2.0 3.0 0.374047 ... 0.725055 1.170181 3.0 0.869184 1.801436 -1.039183 0.033701 1.106586 1.823367 2.540148
b 2.0 1.500000 0.707107 1.0 1.25 1.5 1.75 2.0 2.0 -0.655602 ... 0.035022 0.725645 2.0 -0.009996 0.641793 -0.463813 -0.236905 -0.009996 0.216912 0.443820

2 rows × 24 columns

列式和多函数应用

单列上的多个函数

加载小费数据集:

tips = pd.read_csv("examples/tips.csv")  # 从 CSV 文件加载数据(需确保 examples 文件夹及 tips.csv 文件存在)
tips["tip_pct"] = tips["tip"] / tips["total_bill"]  # 添加一个小费百分比列

应用多个函数:

grouped = tips.groupby(["day", "smoker"])  # 按 day 和 smoker 分组
grouped_pct = grouped["tip_pct"]  # 选择 tip_pct 列
grouped_pct.agg(["mean", "std", peak_to_peak])  # 应用多个聚合函数
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240

自定义列名

提供自定义名称:

grouped_pct.agg([("average", "mean"), ("stdev", np.std)])  # 使用自定义列名
average stdev
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389

对不同列应用不同函数

对不同列应用不同的函数:

functions = ["count", "mean", "max"]  # 定义一个函数列表
result = grouped[["tip_pct", "total_bill"]].agg(functions)  # 对 tip_pct 和 total_bill 应用函数列表
result
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11
grouped.agg({"tip" : np.max, "size" : "sum"})  # 对 tip 列应用 max 函数,对 size 列应用 sum 函数
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40
grouped.agg({
    "tip_pct": ["min", "max", "mean", "std"],  # 对 tip_pct 应用多个函数
    "size": "sum"  # 对 size 应用 sum 函数
})
tip_pct size
min max mean std sum
day smoker
Fri No 0.120385 0.187735 0.151650 0.028123 9
Yes 0.103555 0.263480 0.174783 0.051293 31
Sat No 0.056797 0.291990 0.158048 0.039767 115
Yes 0.035638 0.325733 0.147906 0.061375 104
Sun No 0.059447 0.252672 0.160113 0.042347 167
Yes 0.065660 0.710345 0.187250 0.154134 49
Thur No 0.072961 0.266312 0.160298 0.038774 112
Yes 0.090014 0.241255 0.163863 0.039389 40

返回无行索引的聚合数据

使用 as_index=False 阻止组键成为索引:

numeric_cols = tips.select_dtypes(include=[np.number]).columns  # 获取所有数值列
tips.groupby(["day", "smoker"], as_index=False)[numeric_cols].mean()  # 按 day 和 smoker 分组,计算数值列的平均值,不使用组键作为索引
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863

apply:通用“拆分-应用-合并”

apply 的强大之处 💪

apply 是最通用的 GroupBy 方法。拆分、应用函数、连接。

示例:选择前几行

def top(df, n=5, column="tip_pct"):  # 定义一个函数,按指定列选择前 n 行
    return df.sort_values(column, ascending=False)[:n]

tips.groupby("smoker").apply(top)  # 按 smoker 分组,应用 top 函数
total_bill tip smoker day time size tip_pct
smoker
No 232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
Yes 172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525

apply 传递参数

tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill")  # 向 apply 传递额外的参数
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982

apply 中抑制组键

使用 group_keys=False

tips.groupby("smoker", group_keys=False).apply(top)  # 抑制组键
total_bill tip smoker day time size tip_pct
232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525

分位数和桶分析

cutqcutgroupby 结合使用

cut/qcutgroupby 结合使用进行桶/分位数分析:

frame = pd.DataFrame({
    "data1": np.random.standard_normal(1000),  # 创建一个包含 1000 个随机数的 DataFrame
    "data2": np.random.standard_normal(1000)
})
quartiles = pd.cut(frame["data1"], 4)  # 将 data1 列分为 4 个桶

def get_stats(group):  # 定义一个函数,计算组的统计信息
    return pd.DataFrame(
        {"min": group.min(), "max": group.max(),
        "count": group.count(), "mean": group.mean()}
    )

grouped = frame.groupby(quartiles)  # 按分位数分组
grouped.apply(get_stats)  # 应用 get_stats 函数
min max count mean
data1
(-4.203, -2.377] data1 -4.196123 -2.428642 8 -2.987336
data2 -1.583065 0.970803 8 -0.035568
(-2.377, -0.558] data1 -2.355732 -0.564111 281 -1.121894
data2 -3.167964 2.823358 281 0.014741
(-0.558, 1.26] data1 -0.557554 1.260344 596 0.284630
data2 -2.683438 3.090574 596 -0.093414
(1.26, 3.079] data1 1.265729 3.079192 115 1.743742
data2 -2.791754 2.242056 115 0.012349

相同的结果可以用更简单的方法计算得到:

grouped.agg(["min", "max", "count", "mean"])
data1 data2
min max count mean min max count mean
data1
(-4.203, -2.377] -4.196123 -2.428642 8 -2.987336 -1.583065 0.970803 8 -0.035568
(-2.377, -0.558] -2.355732 -0.564111 281 -1.121894 -3.167964 2.823358 281 0.014741
(-0.558, 1.26] -0.557554 1.260344 596 0.284630 -2.683438 3.090574 596 -0.093414
(1.26, 3.079] 1.265729 3.079192 115 1.743742 -2.791754 2.242056 115 0.012349

使用 qcut 获取相等大小的桶

quartiles_samp = pd.qcut(frame["data1"], 4, labels=False)  # 将 data1 列分为 4 个相等大小的桶,返回标签
grouped = frame.groupby(quartiles_samp)  # 按分位数标签分组
grouped.apply(get_stats)  # 应用 get_stats 函数
min max count mean
data1
0 data1 -4.196123 -0.705362 250 -1.257917
data2 -3.167964 2.823358 250 0.008414
1 data1 -0.702598 0.059811 250 -0.299699
data2 -2.402038 3.090574 250 -0.058953
2 data1 0.060668 0.716297 250 0.371346
data2 -2.683438 2.984681 250 -0.107524
3 data1 0.717199 3.079192 250 1.310345
data2 -2.791754 2.839919 250 -0.043524

示例:使用组特定值填充缺失值

使用平均值填充

s = pd.Series(np.random.standard_normal(6))  # 创建一个包含 6 个随机数的 Series
s[::2] = np.nan  # 将一些值设置为 NaN
s.fillna(s.mean())  # 使用 Series 的平均值填充 NaN
0   -0.495993
1   -0.137229
2   -0.495993
3   -0.581604
4   -0.495993
5   -0.769147
dtype: float64

使用组特定平均值填充

states = ["Ohio", "New York", "Vermont", "Florida",  # 创建一个州列表
          "Oregon", "Nevada", "California", "Idaho"]
group_key = ["East"] * 4 + ["West"] * 4  # 创建一个分组键列表
data = pd.Series(np.random.standard_normal(8), index=states)  # 创建一个 Series,索引为州
data[["Vermont", "Nevada", "Idaho"]] = np.nan  # 将一些值设置为 NaN

def fill_mean(group):  # 定义一个函数,使用组的平均值填充 NaN
    return group.fillna(group.mean())

data.groupby(group_key).apply(fill_mean)  # 按组键分组,应用 fill_mean 函数
East  Ohio         -0.267323
      New York     -1.486514
      Vermont      -0.537915
      Florida       0.140094
West  Oregon       -1.302598
      Nevada       -1.226071
      California   -1.149544
      Idaho        -1.226071
dtype: float64

预定义填充值

fill_values = {"East": 0.5, "West": -1}  # 创建一个填充值字典
def fill_func(group):  # 定义一个函数,使用预定义值填充 NaN
    return group.fillna(fill_values[group.name])

data.groupby(group_key).apply(fill_func)  # 按组键分组,应用 fill_func 函数
East  Ohio         -0.267323
      New York     -1.486514
      Vermont       0.500000
      Florida       0.140094
West  Oregon       -1.302598
      Nevada       -1.000000
      California   -1.149544
      Idaho        -1.000000
dtype: float64

示例:随机抽样和排列

模拟一副牌

suits = ["H", "S", "C", "D"]  # 花色
card_val = (list(range(1, 11)) + [10] * 3) * 4  # 牌值
base_names = ["A"] + list(range(2, 11)) + ["J", "K", "Q"]  # 牌名
cards = []
for suit in suits:
    cards.extend(str(num) + suit for num in base_names)

deck = pd.Series(card_val, index=cards)  # 创建一副牌

抽取随机一手牌

def draw(deck, n=5):  # 定义一个函数,从一副牌中抽取 n 张牌
    return deck.sample(n)

draw(deck)  # 抽取 5 张牌
AH     1
KS    10
7S     7
KD    10
9H     9
dtype: int64

从每个花色中抽取两张随机牌

def get_suit(card):  # 定义一个函数,获取牌的花色
    return card[-1]

deck.groupby(get_suit).apply(draw, n=2)  # 按花色分组,从每个花色中抽取 2 张牌
C  KC    10
   7C     7
D  9D     9
   AD     1
H  3H     3
   QH    10
S  9S     9
   QS    10
dtype: int64

示例:组加权平均和相关性

组加权平均

df = pd.DataFrame({
    "category": ["a", "a", "a", "a",  # 创建一个 DataFrame
                 "b", "b", "b", "b"],
    "data": np.random.standard_normal(8),
    "weights": np.random.uniform(size=8)
})

grouped = df.groupby("category")  # 按 category 分组

def get_wavg(group):  # 定义一个函数,计算组的加权平均值
    return np.average(group["data"], weights=group["weights"])

grouped.apply(get_wavg)  # 应用 get_wavg 函数
category
a    0.087067
b   -0.399983
dtype: float64

与 SPX(标准普尔 500 指数)的相关性

# 假设 examples 文件夹存在且包含 stock_px.csv 文件
close_px = pd.read_csv("examples/stock_px.csv", parse_dates=True, index_col=0)  # 从 CSV 文件加载数据

rets = close_px.pct_change().dropna()  # 计算收益率并删除缺失值

def get_year(x):  # 定义一个函数,获取年份
    return x.year

by_year = rets.groupby(get_year)  # 按年份分组

def spx_corr(group):  # 定义一个函数,计算与 SPX 的相关性
    return group.corrwith(group["SPX"])

by_year.apply(spx_corr)  # 应用 spx_corr 函数
AAPL MSFT XOM SPX
2003 0.541124 0.745174 0.661265 1.0
2004 0.374283 0.588531 0.557742 1.0
2005 0.467540 0.562374 0.631010 1.0
2006 0.428267 0.406126 0.518514 1.0
2007 0.508118 0.658770 0.786264 1.0
2008 0.681434 0.804626 0.828303 1.0
2009 0.707103 0.654902 0.797921 1.0
2010 0.710105 0.730118 0.839057 1.0
2011 0.691931 0.800996 0.859975 1.0

列间相关性(Apple 和 Microsoft)

def corr_aapl_msft(group):  # 定义一个函数,计算 Apple 和 Microsoft 之间的相关性
    return group["AAPL"].corr(group["MSFT"])

by_year.apply(corr_aapl_msft)  # 应用 corr_aapl_msft 函数
2003    0.480868
2004    0.259024
2005    0.300093
2006    0.161735
2007    0.417738
2008    0.611901
2009    0.432738
2010    0.571946
2011    0.581987
dtype: float64

示例:组线性回归

import statsmodels.api as sm  # 导入 statsmodels 库

def regress(data, yvar=None, xvars=None):  # 定义一个函数,执行线性回归
    Y = data[yvar]
    X = data[xvars]
    X["intercept"] = 1.
    result = sm.OLS(Y, X).fit()
    return result.params

by_year.apply(regress, yvar="AAPL", xvars=["SPX"])  # 应用 regress 函数
SPX intercept
2003 1.195406 0.000710
2004 1.363463 0.004201
2005 1.766415 0.003246
2006 1.645496 0.000080
2007 1.198761 0.003438
2008 0.968016 -0.001110
2009 0.879103 0.002954
2010 1.052608 0.001261
2011 0.806605 0.001514

transform:组转换和“展开的” GroupBy

transform 方法

transform 类似于 apply,但:

  • 可以生成要广播的标量。
  • 生成与输入形状相同的对象。
  • 不得改变其输入。
df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,
                   'value': np.arange(12.)})
g = df.groupby('key')['value']

def get_mean(group):  #定义一个函数求平均值
    return group.mean()
g.transform(get_mean)
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64

我们可以像使用 GroupBy agg 方法一样传递字符串别名:

g.transform('mean')
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64

apply 类似,transform 适用于返回 Series 的函数,但结果必须与输入大小相同。 例如,我们可以使用辅助函数将每个组乘以 2:

def times_two(group):
      return group * 2
g.transform(times_two)
0      0.0
1      2.0
2      4.0
3      6.0
4      8.0
5     10.0
6     12.0
7     14.0
8     16.0
9     18.0
10    20.0
11    22.0
Name: value, dtype: float64

作为一个更复杂的示例,我们可以计算每个组的降序排名:

def get_ranks(group):
    return group.rank(ascending=False)
g.transform(get_ranks)
0     4.0
1     4.0
2     4.0
3     3.0
4     3.0
5     3.0
6     2.0
7     2.0
8     2.0
9     1.0
10    1.0
11    1.0
Name: value, dtype: float64

“展开的”分组操作

“展开的”操作通常比 apply 快:

def normalize(x):  # 定义一个函数,进行标准化
    return (x - x.mean()) / x.std()

g.transform(normalize)  # 使用 transform 进行标准化
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64
g.apply(normalize)  # 使用 apply 进行标准化
key    
a    0    -1.161895
     3    -0.387298
     6     0.387298
     9     1.161895
b    1    -1.161895
     4    -0.387298
     7     0.387298
     10    1.161895
c    2    -1.161895
     5    -0.387298
     8     0.387298
     11    1.161895
Name: value, dtype: float64
normalized = (df['value'] - g.transform('mean')) / g.transform('std')  # 展开的分组操作
normalized
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64

数据透视表和交叉表

什么是数据透视表?🤔

按键聚合数据,将其排列成矩形。在电子表格中很常见。

使用 pandas 创建数据透视表

pivot_table 利用 groupby 和分层索引:

tips.pivot_table(
    values=['total_bill', 'tip'],  # 要聚合的列
    index=['day', 'smoker'],  # 行索引
    aggfunc='mean'  # 聚合函数
)
tip total_bill
day smoker
Fri No 2.812500 18.420000
Yes 2.714000 16.813333
Sat No 3.102889 19.661778
Yes 2.875476 21.276667
Sun No 3.167895 20.506667
Yes 3.516842 24.120000
Thur No 2.673778 17.113111
Yes 3.030000 19.190588

按多个变量分组

tips.pivot_table(index=["time", "day"], columns="smoker",  # 行和列
                 values=["tip_pct", "size"])  # 要聚合的列
size tip_pct
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000 NaN 0.159744 NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863

添加边距(部分总计)

tips.pivot_table(index=["time", "day"], columns="smoker",
                 values=["tip_pct", "size"], margins=True)  # 添加边距
size tip_pct
smoker No Yes All No Yes All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0.158916
Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0.153152
Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0.166897
Thur 2.000000 NaN 2.000000 0.159744 NaN 0.159744
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0.188765
Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0.161301
All 2.668874 2.408602 2.569672 0.159328 0.163196 0.160803

使用不同的聚合函数

tips.pivot_table(index=["time", "smoker"], columns="day",
                 values="tip_pct", aggfunc=len, margins=True)  # 使用 len 作为聚合函数
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106
Yes 9.0 42.0 19.0 NaN 70
Lunch No 1.0 NaN NaN 44.0 45
Yes 6.0 NaN NaN 17.0 23
All 19.0 87.0 76.0 62.0 244

交叉表 (Crosstab)

Crosstab 计算组频率:

from io import StringIO

data = pd.read_table(StringIO("""Sample Nationality Handedness
1 USA Right-handed
2 Japan Left-handed
3 USA Right-handed
4 Japan Right-handed
5 Japan Left-handed
6 Japan Right-handed
7 USA Right-handed
8 USA Left-handed
9 Japan Right-handed
10 USA Right-handed"""), sep="\s+")

pd.crosstab(data["Nationality"], data["Handedness"], margins=True)  # 创建交叉表
Handedness Left-handed Right-handed All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 10
pd.crosstab([tips["time"], tips["day"]], tips["smoker"], margins=True)  # 创建交叉表
smoker No Yes All
time day
Dinner Fri 3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0 1
Lunch Fri 1 6 7
Thur 44 17 61
All 151 93 244

总结

  • 我们探索了数据聚合、groupbyapplytransform
  • 我们创建了数据透视表和交叉表。
  • 对于总结、比较和准备数据至关重要。

思考与讨论

  • 分组操作的其他实际场景?
  • 这些技术的创造性组合?
  • groupby 的局限性?
  • applytransform 和直接聚合之间的权衡?
  • “展开的”与性能有何关系?