Pandas groupby
学习指南:数据分组聚合的终极武器
在数据分析的世界里,我们经常需要对数据进行分组,然后针对每个分组执行计算(比如求和、平均值、计数等)。想象一下,你有一份销售记录,你想知道每个地区的总销售额、每个产品的平均价格、或者每个销售员的最大销售量。这些任务的核心操作就是“分组”和“聚合”。
在 Python 的数据分析库 Pandas 中,groupby()
方法正是解决这类问题的终极武器。它基于“Split-Apply-Combine”(拆分-应用-组合)的思想,能够高效、灵活地完成复杂的数据分组和聚合任务。
本篇文章将带你深入理解 Pandas groupby
的方方面面,从基础用法到高级技巧,帮助你掌握这一强大的工具。
目录
- 理解 Split-Apply-Combine 思想
groupby()
的基本用法- 创建一个示例 DataFrame
- 按单列分组
- 理解 GroupBy 对象
- 应用聚合函数 (Aggregation)
- 常用聚合函数
- 选择特定列进行聚合
- 按多列分组并聚合
- 使用
agg()
应用多个聚合函数- 应用多个函数到所有聚合列
- 应用不同的函数到不同的列
- 使用命名聚合 (Named Aggregation)
size()
vscount()
- 使用
apply()
应用自定义函数apply()
的灵活性- 示例:计算组内排名、选择组内 Top N
- 使用
transform()
应用转换函数transform()
的特点:保留原始索引- 示例:标准化数据、填充组内平均值
- 迭代 GroupBy 对象
groupby
的其他参数 (as_index
,sort
,dropna
)- 链式操作和效率考虑
- 总结与实践
1. 理解 Split-Apply-Combine 思想
groupby()
的核心是 Split-Apply-Combine 策略,由 Hadley Wickham 在 R 语言中推广。它描述了大多数数据聚合和分组操作的通用过程:
- Split(拆分):根据分组键(一个或多个列/索引)将数据拆分成若干个独立的小块(或称分组)。每个小块包含分组键具有相同值的所有行。
- Apply(应用):对每个独立的小块应用一个函数。这个函数可以是聚合函数(如求和、平均值),转换函数(如标准化),或者过滤函数(如选取 Top N)。
- Combine(组合):将每个小块应用函数后的结果组合成一个统一的、有意义的结构(通常是 Pandas Series 或 DataFrame)。
你可以想象这个过程就像整理一堆不同颜色和大小的积木:
- Split: 你根据颜色将积木分成不同的堆(红色堆、蓝色堆、绿色堆)。
- Apply: 对于每一堆积木,你数一下有多少块(计数),或者称一下这一堆的总重量(求和)。
- Combine: 你把每堆积木的计数或总重量记录下来,形成一个新的表格,表格里列出了每种颜色对应的计数/总重量。
Pandas 的 groupby()
方法就是负责高效地完成这个 Split 和 Combine 的过程,而你需要告诉它在 Apply 阶段要做什么。
2. groupby()
的基本用法
创建一个示例 DataFrame
为了演示 groupby
的用法,我们首先创建一个简单的示例 DataFrame:
“`python
import pandas as pd
import numpy as np
创建一个示例 DataFrame
data = {
‘Category’: [‘A’, ‘B’, ‘A’, ‘C’, ‘B’, ‘C’, ‘A’, ‘B’, ‘C’, ‘A’],
‘Subcategory’: [‘X’, ‘Y’, ‘Z’, ‘X’, ‘Y’, ‘Z’, ‘X’, ‘Y’, ‘Z’, ‘Z’],
‘Value1’: [10, 15, 12, 18, 20, 25, 11, 16, 22, 14],
‘Value2’: [100, 150, 120, 180, 200, 250, 110, 160, 220, 140],
‘TextCol’: [‘foo’, ‘bar’, ‘baz’, ‘foo’, ‘bar’, ‘baz’, ‘foo’, ‘bar’, ‘baz’, ‘foo’]
}
df = pd.DataFrame(data)
print(“原始 DataFrame:”)
print(df)
“`
输出:
原始 DataFrame:
Category Subcategory Value1 Value2 TextCol
0 A X 10 100 foo
1 B Y 15 150 bar
2 A Z 12 120 baz
3 C X 18 180 foo
4 B Y 20 200 bar
5 C Z 25 250 baz
6 A X 11 110 foo
7 B Y 16 160 bar
8 C Z 22 220 baz
9 A Z 14 140 foo
按单列分组
使用 groupby()
方法非常简单,只需要指定作为分组键的列名(或多个列名组成的列表):
“`python
按 ‘Category’ 列分组
grouped_by_category = df.groupby(‘Category’)
print(“\n按 ‘Category’ 分组后的 GroupBy 对象:”)
print(grouped_by_category)
print(type(grouped_by_category))
“`
输出:
按 'Category' 分组后的 GroupBy 对象:
<pandas.core.groupby.generic.DataFrameGroupBy object at ...>
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
可以看到,df.groupby('Category')
返回的并不是一个 DataFrame,而是一个 DataFrameGroupBy
对象。这个对象本身并没有执行任何计算,它只是一个描述了如何对数据进行分组的对象。真正的计算(Apply 和 Combine 步骤)会在你调用聚合、转换或过滤方法时发生。这种设计的好处是效率高,因为它避免了创建中间的临时数据结构。
理解 GroupBy 对象
GroupBy
对象包含了分组信息以及对每个组进行操作所需的方法。你可以通过一些属性来了解分组的情况:
“`python
查看分组键
print(“\n分组键:”)
print(grouped_by_category.keys) # 注意:keys 是属性,不是方法
查看分组数量
print(“\n分组数量:”)
print(grouped_by_category.ngroups)
查看每个分组的索引
print(“\n每个分组的索引:”)
print(grouped_by_category.groups)
查看特定分组的数据(不常用,但有助于理解)
print(“\n’A’ 分组的数据:”)
print(grouped_by_category.get_group(‘A’))
“`
输出:
“`
分组键:
[‘Category’]
分组数量:
3
每个分组的索引:
{‘A’: Index([0, 2, 6, 9], dtype=’int64′), ‘B’: Index([1, 4, 7], dtype=’int64′), ‘C’: Index([3, 5, 8], dtype=’int64′)}
‘A’ 分组的数据:
Category Subcategory Value1 Value2 TextCol
0 A X 10 100 foo
2 A Z 12 120 baz
6 A X 11 110 foo
9 A Z 14 140 foo
“`
groups
属性显示了每个分组名称(这里是 Category 的值 ‘A’, ‘B’, ‘C’)对应的原始 DataFrame 中的行索引。get_group('A')
则直接返回 ‘A’ 组对应的所有原始行数据,结果是一个 DataFrame。
3. 应用聚合函数 (Aggregation)
一旦创建了 GroupBy
对象,就可以在其上调用聚合函数。聚合函数会对每个分组的数据进行计算,并返回一个单一的结果。
常用聚合函数
Pandas GroupBy 对象提供了许多内置的常用聚合函数,例如:
sum()
: 计算总和mean()
: 计算平均值median()
: 计算中位数min()
: 计算最小值max()
: 计算最大值std()
: 计算标准差var()
: 计算方差count()
: 计算非 NaN 值的数量size()
: 计算分组的大小(行数),包括 NaNfirst()
: 返回每个分组的第一个值last()
: 返回每个分组的最后一个值nunique()
: 计算唯一值的数量
对 GroupBy 对象直接调用聚合函数时,它会尝试对所有数值型列进行聚合:
“`python
计算每个 Category 的 Value1 和 Value2 的总和
category_sums = grouped_by_category.sum()
print(“\n按 Category 分组并求和:”)
print(category_sums)
计算每个 Category 的 Value1 和 Value2 的平均值
category_means = grouped_by_category.mean()
print(“\n按 Category 分组并求平均值:”)
print(category_means)
计算每个 Category 的行数 (包括 NaN)
category_sizes = grouped_by_category.size()
print(“\n按 Category 分组并计算大小:”)
print(category_sizes) # size() 返回 Series
“`
输出:
“`
按 Category 分组并求和:
Value1 Value2
Category
A 47 470
B 51 510
C 65 650
按 Category 分组并求平均值:
Value1 Value2
Category
A 11.75 117.5
B 17.00 170.0
C 21.67 216.666667
按 Category 分组并计算大小:
Category
A 4
B 3
C 3
dtype: int64
“`
注意,size()
返回的是一个 Series,其索引是分组键,值是每个组的大小。其他聚合函数通常返回一个 DataFrame,其索引也是分组键,列是原始 DataFrame 中被聚合的列。非数值型的列(如 ‘TextCol’)通常会被忽略,除非聚合函数对它们有意义(如 first()
, last()
, count()
, size()
, nunique()
, apply
with string operations)。
选择特定列进行聚合
如果你只想对特定的列进行聚合,可以在 groupby()
之后,但在调用聚合函数之前,使用方括号 []
选择列:
“`python
只计算每个 Category 的 Value1 的总和
category_value1_sum = grouped_by_category[‘Value1’].sum()
print(“\n按 Category 分组,只对 Value1 求和:”)
print(category_value1_sum) # 返回 Series
只计算每个 Category 的 Value1 和 Value2 的平均值
category_values_mean = grouped_by_category[[‘Value1’, ‘Value2’]].mean()
print(“\n按 Category 分组,只对 Value1 和 Value2 求平均值:”)
print(category_values_mean) # 返回 DataFrame
“`
输出:
“`
按 Category 分组,只对 Value1 求和:
Category
A 47
B 51
C 65
Name: Value1, dtype: int64
按 Category 分组,只对 Value1 和 Value2 求平均值:
Value1 Value2
Category
A 11.75 117.500000
B 17.00 170.000000
C 21.67 216.666667
“`
使用单个方括号 ['ColumnName']
选择列会得到一个 SeriesGroupBy
对象,对其应用聚合函数通常返回一个 Series。使用双层方括号 [['ColumnName1', 'ColumnName2']]
选择列会得到一个 DataFrameGroupBy
对象,对其应用聚合函数返回一个 DataFrame。
按多列分组并聚合
groupby()
也可以接受一个列表,以便按多列进行分组。此时,分组键将由这些列的唯一组合决定:
“`python
按 ‘Category’ 和 ‘Subcategory’ 列分组
grouped_multi = df.groupby([‘Category’, ‘Subcategory’])
计算每个组合的 Value1 的总和
multi_level_sum = grouped_multi[‘Value1’].sum()
print(“\n按 Category 和 Subcategory 分组并求和:”)
print(multi_level_sum)
“`
输出:
按 Category 和 Subcategory 分组并求和:
Category Subcategory
A X 21
Z 26
B Y 51
C X 18
Z 47
Name: Value1, dtype: int64
此时,结果的索引是一个多级索引 (MultiIndex),由分组键的唯一组合构成。
使用 agg()
应用多个聚合函数
agg()
(或 aggregate()
, 它们是同一个方法) 是 groupby
中非常重要的一个方法,它允许你一次性对一个或多个列应用一个或多个聚合函数。这比多次调用单一聚合函数更高效且代码更简洁。
应用多个函数到所有聚合列
你可以给 agg()
传递一个聚合函数名称的字符串列表:
“`python
对 Value1 列应用多个聚合函数
category_value1_agg = grouped_by_category[‘Value1’].agg([‘sum’, ‘mean’, ‘std’, ‘count’])
print(“\n按 Category 分组,对 Value1 应用多个函数:”)
print(category_value1_agg)
“`
输出:
按 Category 分组,对 Value1 应用多个函数:
sum mean std count
Category
A 47 11.750000 1.707825 4
B 51 17.000000 2.645751 3
C 65 21.666667 3.511885 3
结果 DataFrame 的列名是聚合函数的名称。
应用不同的函数到不同的列
更常见和强大的是使用字典传递给 agg()
,字典的键是你想要聚合的输出列名,字典的值可以是:
- 一个函数名字符串。
- 一个函数名字符串列表,对该列应用多个函数。
- 一个元组
('原始列名', '函数名')
,用于指定对哪个原始列应用函数并指定输出列名(更推荐的命名聚合方式)。
方式 1&2: 使用字典映射输出列名到函数/函数列表 (旧风格)
“`python
对 Value1 求和,对 Value2 求平均值和最大值
category_custom_agg_old = grouped_by_category.agg({
‘Value1’: ‘sum’, # 输出列名为 Value1
‘Value2’: [‘mean’, ‘max’] # 输出列名为 MultiIndex (Value2, mean) 和 (Value2, max)
})
print(“\n按 Category 分组,使用 agg 字典 (旧风格):”)
print(category_custom_agg_old)
“`
输出:
按 Category 分组,使用 agg 字典 (旧风格):
Value1 Value2
sum mean max
Category
A 47 117.500000 140
B 51 170.000000 200
C 65 216.666667 250
这种旧风格的字典方式在应用多个函数到一个列时会生成一个 MultiIndex 的列,可能不太方便。
使用命名聚合 (Named Aggregation)
Pandas 0.25.0 引入了命名聚合,这是通过在 agg()
中使用关键字参数来实现的,每个关键字参数的名字就是最终结果中的列名。其值是一个元组 ('原始列名', '函数名')
。
“`python
使用命名聚合,对 Value1 求总和,对 Value2 求平均值和最大值
category_custom_agg_named = grouped_by_category.agg(
Total_Value1=(‘Value1’, ‘sum’),
Average_Value2=(‘Value2’, ‘mean’),
Max_Value2=(‘Value2’, ‘max’)
)
print(“\n按 Category 分组,使用命名聚合:”)
print(category_custom_agg_named)
“`
输出:
按 Category 分组,使用命名聚合:
Total_Value1 Average_Value2 Max_Value2
Category
A 47 117.500000 140
B 51 170.000000 200
C 65 216.666667 250
命名聚合的结果列名清晰明了,强烈推荐使用这种方式进行复杂的聚合。
size()
vs count()
这是一个常见的混淆点:
count()
: 计算分组中非 NaN 值的数量。当应用于整个 GroupBy 对象时,它会按列统计每个分组中非 NaN 的数量。size()
: 计算分组的总行数,包括 NaN 值。它总是返回一个 Series。
示例:
“`python
引入 NaN 值进行演示
df_with_nan = df.copy()
df_with_nan.loc[0, ‘Value1’] = np.nan
df_with_nan.loc[4, ‘Value2’] = np.nan
df_with_nan.loc[8, ‘TextCol’] = np.nan
grouped_with_nan = df_with_nan.groupby(‘Category’)
print(“\n带有 NaN 值的 DataFrame:”)
print(df_with_nan)
print(“\n按 Category 分组,使用 count():”)
print(grouped_with_nan.count()) # 统计非 NaN 数量
print(“\n按 Category 分组,使用 size():”)
print(grouped_with_nan.size()) # 统计总行数
“`
输出:
“`
带有 NaN 值的 DataFrame:
Category Subcategory Value1 Value2 TextCol
0 A X NaN 100.0 foo
1 B Y 15.0 150.0 bar
2 A Z 12.0 120.0 baz
3 C X 18.0 180.0 foo
4 B Y 20.0 NaN bar
5 C Z 25.0 250.0 baz
6 A X 11.0 110.0 foo
7 B Y 16.0 160.0 bar
8 C Z 22.0 NaN NaN
9 A Z 14.0 140.0 foo
按 Category 分组,使用 count():
Subcategory Value1 Value2 TextCol
Category
A 4 3 4 4
B 3 3 2 3
C 3 3 2 2
按 Category 分组,使用 size():
Category
A 4
B 3
C 3
dtype: int64
“`
可以看到,’A’ 组的 Value1
少了一个 NaN
,count()
结果是 3,而 size()
结果是 4。’B’ 组的 Value2
少了一个 NaN
,count()
结果是 2,而 size()
结果是 3。’C’ 组的 Value2
和 TextCol
各少了一个 NaN
,count()
结果相应减少,而 size()
结果依然是 3。
4. 使用 apply()
应用自定义函数
虽然 agg()
及其内置函数能处理很多常见的聚合需求,但有时你需要执行更复杂的操作,而这些操作没有内置函数可以直接完成。这时就可以使用 apply()
方法。
apply()
方法会将分组的每个小块(一个 DataFrame)作为输入,传递给一个函数,然后将函数的返回值组合起来。
“`python
定义一个函数,计算每个分组 Value1 的范围 (max – min)
def range_of_value1(group):
return group[‘Value1’].max() – group[‘Value1’].min()
对每个 Category 应用 range_of_value1 函数
category_value1_range = grouped_by_category.apply(range_of_value1)
print(“\n按 Category 分组,计算 Value1 的范围:”)
print(category_value1_range)
“`
输出:
按 Category 分组,计算 Value1 的范围:
Category
A 4.0
B 5.0
C 7.0
dtype: float64
apply()
函数的灵活性非常高。它可以返回:
- 一个 Series:如上面的例子,返回一个标量结果(范围)。Pandas 会将这些 Series 组合成一个 Series,索引是分组键。
- 一个 DataFrame:如果你的函数对每个分组返回一个 DataFrame,Pandas 会将这些 DataFrame 行绑定 (row-bind) 起来,并保留原始分组的索引作为多级索引的第一层。
示例:找出每个 Category 中 Value1 最大的行:
“`python
定义一个函数,返回分组中 Value1 最大的行
def get_row_with_max_value1(group):
# idxmax() 返回最大值所在行的索引
idx_max = group[‘Value1’].idxmax()
return group.loc[[idx_max]] # 返回该行作为一个 DataFrame
对每个 Category 应用函数
max_value_rows = grouped_by_category.apply(get_row_with_max_value1)
print(“\n按 Category 分组,获取 Value1 最大的行:”)
print(max_value_rows)
“`
输出:
按 Category 分组,获取 Value1 最大的行:
Category Subcategory Value1 Value2 TextCol
Category
A 9 A Z 14 140 foo
B 4 B Y 20 200 bar
C 5 C Z 25 250 baz
这里的结果是一个 DataFrame,其索引是多级的:第一层是 Category (分组键),第二层是原始 DataFrame 的索引。
apply()
非常强大,几乎可以对分组数据执行任何操作。然而,相比于优化的聚合函数 (sum
, mean
等) 或 transform
,apply
可能效率较低,因为它需要为每个分组实例化一个 DataFrame 并调用函数。对于简单的聚合,优先使用内置方法或 agg()
。
5. 使用 transform()
应用转换函数
有时候,你需要在保留原始 DataFrame 结构和索引的情况下,基于分组信息创建一个新的列。例如,你想计算每个销售额与该销售员平均销售额的差值,或者将每个值标准化到其所属分组的平均值和标准差。transform()
方法就是为这种“转换”任务设计的。
transform()
方法必须返回一个与其对应的分组具有相同索引的 Series 或 DataFrame。然后 Pandas 会将这些结果组合起来,形成一个与原始 DataFrame 具有相同索引的 Series 或 DataFrame。
“`python
计算每个 Category 的 Value1 的平均值,并将结果添加到原始 DataFrame 中
df[‘Category_Value1_Mean’] = grouped_by_category[‘Value1’].transform(‘mean’)
计算每个 Category 的 Value1 相对于其分组平均值的差值
df[‘Value1_Diff_From_Mean’] = grouped_by_category[‘Value1’].transform(lambda x: x – x.mean())
print(“\n使用 transform 添加分组平均值和差值列:”)
print(df)
“`
输出:
使用 transform 添加分组平均值和差值列:
Category Subcategory Value1 Value2 TextCol Category_Value1_Mean Value1_Diff_From_Mean
0 A X 10 100 foo 11.75 -1.75
1 B Y 15 150 bar 17.00 -2.00
2 A Z 12 120 baz 11.75 0.25
3 C X 18 180 foo 21.67 -3.67
4 B Y 20 200 bar 17.00 3.00
5 C Z 25 250 baz 21.67 3.33
6 A X 11 110 foo 11.75 -0.75
7 B Y 16 160 bar 17.00 -1.00
8 C Z 22 220 baz 21.67 0.33
9 A Z 14 140 foo 11.75 2.25
正如所见,transform()
的结果可以直接赋值给原始 DataFrame 的新列,因为它们拥有相同的索引。
transform()
可以接受:
- 聚合函数名字符串 (‘sum’, ‘mean’, ‘std’ 等)。
- 一个函数对象,该函数接受一个 Series 或 DataFrame(取决于你是在 GroupBy 对象上调用还是在 GroupBy 后选择了列),并返回一个具有相同索引的 Series 或 DataFrame。
示例:对 Value1 按 Category 进行 Z-score 标准化:
“`python
定义 Z-score 标准化函数
def zscore(x):
return (x – x.mean()) / x.std(ddof=0) # ddof=0 for population std dev
df[‘Value1_Zscore’] = grouped_by_category[‘Value1’].transform(zscore)
print(“\n使用 transform 进行 Z-score 标准化:”)
print(df)
“`
输出:
使用 transform 进行 Z-score 标准化:
Category Subcategory Value1 Value2 TextCol Category_Value1_Mean Value1_Diff_From_Mean Value1_Zscore
0 A X 10 100 foo 11.75 -1.75 -1.028857
1 B Y 15 150 bar 17.00 -2.00 -0.816497
2 A Z 12 120 baz 11.75 0.25 0.146984
3 C X 18 180 foo 21.67 -3.67 -1.042578
4 B Y 20 200 bar 17.00 3.00 1.224745
5 C Z 25 250 baz 21.67 3.33 0.943912
6 A X 11 110 foo 11.75 -0.75 -0.439936
7 B Y 16 160 bar 17.00 -1.00 -0.408248
8 C Z 22 220 baz 21.67 0.33 0.098666
9 A Z 14 140 foo 11.75 2.25 1.321808
transform
是进行组内计算并广播结果回原始数据框的理想选择。
6. 迭代 GroupBy 对象
虽然大多数情况下你会使用 agg
, apply
, 或 transform
来处理所有分组,但有时你可能需要逐个访问每个分组进行特殊处理(例如,保存每个分组到单独的文件,或者可视化每个分组的数据)。你可以像迭代器一样遍历 GroupBy
对象:
“`python
迭代 GroupBy 对象
print(“\n迭代 GroupBy 对象:”)
for name, group_df in grouped_by_category:
print(f”\n— 分组: {name} —“)
print(group_df)
“`
输出:
“`
迭代 GroupBy 对象:
— 分组: A —
Category Subcategory Value1 Value2 TextCol Category_Value1_Mean Value1_Diff_From_Mean Value1_Zscore
0 A X 10 100 foo 11.75 -1.75 -1.028857
2 A Z 12 120 baz 11.75 0.25 0.146984
6 A X 11 110 foo 11.75 -0.75 -0.439936
9 A Z 14 140 foo 11.75 2.25 1.321808
— 分组: B —
Category Subcategory Value1 Value2 TextCol Category_Value1_Mean Value1_Diff_From_Mean Value1_Zscore
1 B Y 15 150 bar 17.00 -2.00 -0.816497
4 B Y 20 200 bar 17.00 3.00 1.224745
7 B Y 16 160 bar 17.00 -1.00 -0.408248
— 分组: C —
Category Subcategory Value1 Value2 TextCol Category_Value1_Mean Value1_Diff_From_Mean Value1_Zscore
3 C X 18 180 foo 21.67 -3.67 -1.042578
5 C Z 25 250 baz 21.67 3.33 0.943912
8 C Z 22 220 baz 21.67 0.33 0.098666
“`
每次迭代返回一个元组 (name, group_df)
,其中 name
是当前分组的值(如果是多级分组则是元组),group_df
是该分组对应的 DataFrame。虽然迭代功能强大,但在处理大型数据集时应谨慎使用,因为它可能比矢量化操作效率低。
7. groupby
的其他参数
groupby()
方法还有一些其他有用的参数:
as_index=True
(默认): 分组键将成为结果 DataFrame 的索引。as_index=False
: 分组键将作为普通列保留在结果 DataFrame 中,结果索引是默认的整数索引。这通常在你不想处理多级索引时很有用。sort=True
(默认): 按分组键对结果进行排序。设置为False
可以稍微提高性能,如果分组键的顺序不重要的话。dropna=True
(默认): 默认情况下,分组键中包含NaN
的行会被排除。设置为False
会将NaN
视为一个特殊的分组。
示例 as_index=False
:
“`python
按 Category 分组并求和,不将 Category 作为索引
category_sums_no_index = df.groupby(‘Category’, as_index=False)[‘Value1’, ‘Value2’].sum()
print(“\n按 Category 分组求和 (as_index=False):”)
print(category_sums_no_index)
“`
输出:
按 Category 分组求和 (as_index=False):
Category Value1 Value2
0 A 47 470
1 B 51 510
2 C 65 650
使用 as_index=False
后,结果更像一个常规的二维表,有时更方便后续处理或写入文件。
示例 dropna=False
(需要分组键中有 NaN):
“`python
df_nan_groupkey = pd.DataFrame({
‘Group’: [‘A’, ‘B’, ‘A’, np.nan, ‘B’, ‘A’, np.nan],
‘Value’: [1, 2, 3, 4, 5, 6, 7]
})
print(“\n包含 NaN 分组键的 DataFrame:”)
print(df_nan_groupkey)
默认行为 (dropna=True)
print(“\nGroupBy 默认行为 (dropna=True):”)
print(df_nan_groupkey.groupby(‘Group’)[‘Value’].sum())
保留 NaN 分组 (dropna=False)
print(“\nGroupBy 保留 NaN 分组 (dropna=False):”)
print(df_nan_groupkey.groupby(‘Group’, dropna=False)[‘Value’].sum())
“`
输出:
“`
包含 NaN 分组键的 DataFrame:
Group Value
0 A 1
1 B 2
2 A 3
3 NaN 4
4 B 5
5 A 6
6 NaN 7
GroupBy 默认行为 (dropna=True):
Group
A 10
B 7
Name: Value, dtype: int64
GroupBy 保留 NaN 分组 (dropna=False):
Group
A 10.0
B 7.0
NaN 11.0
Name: Value, dtype: float64
“`
可以看到,当 dropna=False
时,NaN
值被视为一个独立的分组键,并包含了对应的行数据。
8. 链式操作和效率考虑
Pandas 的方法通常可以进行链式操作,groupby
也不例外。这有助于编写更简洁的代码:
“`python
链式操作:按 Category 分组,选择 Value1 列,计算平均值
chain_example = df.groupby(‘Category’)[‘Value1’].mean()
print(“\n链式操作示例:”)
print(chain_example)
“`
在处理大型数据集时,效率是一个重要的考虑因素:
- 优先使用优化的聚合函数或
agg()
:Pandas 对sum
,mean
,count
等内置函数进行了高度优化。agg()
使用这些内置函数的字符串名称时也是高效的。 - 谨慎使用
apply()
:虽然apply()
最灵活,但如果可以用agg()
或transform()
实现相同功能,通常后者会更高效。 - 避免不必要的迭代:逐个处理分组通常比矢量化操作慢。
- 考虑数据类型:数值型列的聚合通常比字符串或对象列更快。
- 处理大型数据时,考虑内存使用和性能:对于海量数据,可能需要 Dask 或 Spark 等分布式计算框架,它们提供了类似 Pandas
groupby
的接口。
9. 总结与实践
Pandas groupby
是数据分析中最常用和最强大的工具之一。掌握它意味着你可以轻松地对数据进行分组、汇总、计算描述性统计量、执行组内转换等复杂任务。
关键概念回顾:
- Split-Apply-Combine 思想是
groupby
的基础。 groupby()
返回一个GroupBy
对象,它是一个延迟计算的对象。- 聚合函数 (Aggregation):使用
sum()
,mean()
,count()
,agg()
等将每个分组 réduit 成一个或少数几个值。结果通常是索引为分组键的 Series 或 DataFrame。 - 转换函数 (Transformation):使用
transform()
对每个分组应用一个函数,该函数返回与原始分组具有相同索引的结果。结果会组合回原始 DataFrame 的结构。 apply()
:最通用的方法,对每个分组应用一个任意函数。它可以返回 Series, DataFrame 或标量,结果的组合方式取决于返回类型。as_index=False
可以避免将分组键设置为索引。- 理解
size()
和count()
的区别。
实践建议:
- 从简单的分组聚合开始 (
groupby().sum()
)。 - 练习使用
agg()
应用不同的函数到不同的列,特别是使用命名聚合。 - 尝试用
transform()
创建新的特征列,比如组内平均值、组内排名等。 - 当遇到复杂的组内计算时,考虑使用
apply()
,但也要思考是否有更优化的transform
或矢量化方法。 - 阅读 Pandas 官方文档中关于 GroupBy 的部分,那里有更深入的细节和示例。
数据分析的很大一部分工作就是理解和处理数据中的分组结构。精通 groupby
将极大地提升你的数据处理能力。不断练习,将其融入你的数据分析工作流中,你会发现它能解决许多看似复杂的问题。
希望这篇详细指南能帮助你全面理解和掌握 Pandas groupby
的强大功能!祝你数据分析愉快!