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
第十章
本节重点介绍数据聚合与分组操作,这是数据分析的关键部分。我们将学习如何对数据进行分类,并对每个组应用函数——这是许多数据工作流程中的基本步骤。
我们将学习:
transform
和 apply
。分组和聚合数据有助于我们:
Hadley Wickham 的 “拆分-应用-合并” (split-apply-combine) 范式是理解数据处理的强大方式。
键可以是:
这些本质上都是用于拆分对象的数组值的快捷方式。
Split-Apply-Combine
Sum
函数应用于每个组的“Data”。让我们创建一个示例 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
此 DataFrame 有两个键列(key1
、key2
)和两个数据列(data1
、data2
)。
计算 key1
中每个组的 data1
的平均值:
df["data1"]
:选择 data1
列。.groupby(df["key1"])
:按 key1
列分组。grouped
:一个 GroupBy
对象,存储分组信息。.mean()
:计算每个组的平均值。按多列分组(分层索引):
数据按 key1
和 key2
的组合进行分组。
unstack()
重塑结果:
键可以是外部 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())
这里,我们按外部数组 states
和 years
对 data1
进行分组。
如果分组信息在 DataFrame 中,直接使用列名:
非数值列 key1
会被自动排除,因为它是一个干扰列
size()
显示每个组中的数据点数量:
默认情况下,分组键中的缺失值会被排除。使用 dropna=False
包含它们:
GroupBy
支持迭代,生成组名和数据块:
使用多个键时,组名是一个元组:
创建组数据的字典:
使用 axis="columns"
按列分组:
对 GroupBy
对象进行索引以聚合特定列:
这是 df["data2"].groupby([df["key1"], df["key2"]]).mean()
的简写。
这等同于 df[["data2"]].groupby([df["key1"], df["key2"]]).mean()
。
使用字典或 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() # 计算每个列组的总和
mapping
指定列分组。
使用函数定义组映射(每个索引值调用一次):
按分层索引的级别分组:
聚合将数组转换为标量。优化方法:
count
、sum
、mean
、median
、std
、var
min
、max
、prod
、first
、last
any
、all
、cummin
、cummax
、cumsum
、cumprod
nth
、ohlc
、quantile
、rank
、size
使用 agg
定义自定义函数:
describe
方法使用非聚合方法,如 describe
:
加载小费数据集:
应用多个函数:
提供自定义名称:
对不同列应用不同的函数:
使用 as_index=False
阻止组键成为索引:
apply
:通用“拆分-应用-合并”apply
的强大之处 💪apply
是最通用的 GroupBy
方法。拆分、应用函数、连接。
apply
传递参数apply
中抑制组键使用 group_keys=False
:
cut
和 qcut
与 groupby
结合使用将 cut
/qcut
与 groupby
结合使用进行桶/分位数分析:
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 函数
相同的结果可以用更简单的方法计算得到:
qcut
获取相等大小的桶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 函数
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 函数
# 假设 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 函数
transform
:组转换和“展开的” GroupBytransform
方法transform
类似于 apply
,但:
我们可以像使用 GroupBy agg
方法一样传递字符串别名:
与 apply
类似,transform
适用于返回 Series 的函数,但结果必须与输入大小相同。 例如,我们可以使用辅助函数将每个组乘以 2:
作为一个更复杂的示例,我们可以计算每个组的降序排名:
“展开的”操作通常比 apply
快:
按键聚合数据,将其排列成矩形。在电子表格中很常见。
pivot_table
利用 groupby
和分层索引:
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) # 创建交叉表
groupby
、apply
、transform
。groupby
的局限性?apply
、transform
和直接聚合之间的权衡?邱飞 💌 [email protected]