Skip to content

Python Pandas - GroupBy

数据分组是数据分析中的一个基本操作。Pandas 的 groupby 操作允许您高效地对数据的子集进行计算。它通常包括三个步骤:

  • 拆分 (Splitting):根据某些标准(例如,某列中的值)将数据分成组。
  • 应用 (Applying):独立地对每个组应用一个函数(例如,计算均值、总和或自定义转换)。
  • 组合 (Combining):将结果组合成新的数据结构(如 DataFrame 或 Series)。

在“应用 (Apply)”步骤中的常见应用包括:

  • 聚合 (Aggregation):为每个组计算汇总统计信息(例如,计数、均值、标准差)。
  • 转换 (Transformation):执行组特定的计算,返回与输入组具有相同形状的对象(例如,在每个组内标准化数据)。
  • 过滤 (Filtration):根据组级别的计算或条件丢弃整个组(例如,只保留包含足够数量数据点的组)。

我们来创建一个示例 DataFrame 以说明这些操作:

# 导入 pandas 库
import pandas as pd
import numpy as np
# 示例数据 (为保持一致性,将 'kings' 更正为 'Kings')
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
print(df)

输出:

Team Rank Year Points
0 Riders 1 2014 876
1 Riders 2 2015 789
2 Devils 2 2014 863
3 Devils 3 2015 673
4 Kings 3 2014 741
5 Kings 4 2015 812
6 Kings 1 2016 756
7 Kings 1 2017 788
8 Riders 2 2016 694
9 Royals 4 2014 701
10 Royals 1 2015 804
11 Riders 2 2017 690

您可以使用 groupby() 方法将 Pandas 对象(如 DataFrame)拆分为组。指定分组标准的常见方式包括:

  • df.groupby('column_name'): 按单个列中的值进行分组。
  • df.groupby(['col1', 'col2']): 按多个列中的值的组合进行分组。
  • df.groupby(level='index_level'): 如果 DataFrame 具有 MultiIndex,则按索引级别进行分组。

单独应用 groupby() 方法并不会立即进行任何计算,而是返回一个 DataFrameGroupBy 对象,该对象包含关于组的信息。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
# 创建一个 GroupBy 对象
grouped_by_team = df.groupby('Team')
print(grouped_by_team)

输出(表示形式可能因 Pandas 版本略有差异):

<pandas.core.groupby.generic.DataFrameGroupBy object at 0x...>

要查看哪些行属于哪个组,您可以检查 GroupBy 对象的 .groups 属性。它返回一个字典,其中键是组名,值是属于该组的索引标签。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_team = df.groupby('Team')
print(grouped_by_team.groups)

输出(索引类型可能因 Pandas 版本而异,例如,Index 而不是 Int64Index):

{'Devils': [2, 3], 'Kings': [4, 5, 6, 7], 'Riders': [0, 1, 8, 11], 'Royals': [9, 10]}

按多个列分组会创建分层组。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
# 按 Team 和 Year 两列进行分组
grouped_by_team_year = df.groupby(['Team', 'Year'])
print(grouped_by_team_year.groups)

输出:

{('Devils', 2014): [2], ('Devils', 2015): [3], ('Kings', 2014): [4], ('Kings', 2015): [5], ('Kings', 2016): [6], ('Kings', 2017): [7], ('Riders', 2014): [0], ('Riders', 2015): [1], ('Riders', 2016): [8], ('Riders', 2017): [11], ('Royals', 2014): [9], ('Royals', 2015): [10]}

您可以遍历 GroupBy 对象。每次迭代都会产生一个元组,其中包含组名(对于多列分组,可能是一个元组)和原始 DataFrame 中相应的子集。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_year = df.groupby('Year')
print("Iterating through groups based on Year:") # 遍历按年份分组的组:
for name, group_df in grouped_by_year:
print(f"\nYear: {name}")
print(group_df)

输出:

Iterating through groups based on Year:
Year: 2014
Team Rank Year Points
0 Riders 1 2014 876
2 Devils 2 2014 863
4 Kings 3 2014 741
9 Royals 4 2014 701
Year: 2015
Team Rank Year Points
1 Riders 2 2015 789
3 Devils 3 2015 673
5 Kings 4 2015 812
10 Royals 1 2015 804
Year: 2016
Team Rank Year Points
6 Kings 1 2016 756
8 Riders 2 2016 694
Year: 2017
Team Rank Year Points
7 Kings 1 2017 788
11 Riders 2 2017 690

遍历对于执行不适合标准聚合或转换模式的复杂操作非常有用。

使用 get_group() 方法检索对应于单个特定组名的 DataFrame。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_year = df.groupby('Year')
# 获取 2014 年的数据
group_2014 = grouped_by_year.get_group(2014)
print(group_2014)

输出:

Team Rank Year Points
0 Riders 1 2014 876
2 Devils 2 2014 863
4 Kings 3 2014 741
9 Royals 4 2014 701

聚合为每个组计算一个(或多个)汇总统计信息,生成一个更小的数据结构,其中索引通常对应于组名。

最常见的聚合方式是使用 aggregate() 方法(或其别名 agg())。您可以对特定列或整个组应用聚合函数。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_year = df.groupby('Year')
# 计算每年的平均分数
# 使用 numpy 函数
mean_points_np = grouped_by_year['Points'].agg(np.mean)
print("Mean Points per Year (using np.mean):") # 每年的平均分数 (使用 np.mean):
print(mean_points_np)
# 使用 pandas 字符串别名 (更常见)
mean_points_str = grouped_by_year['Points'].agg('mean')
print("\nMean Points per Year (using 'mean'):") # 每年的平均分数 (使用 'mean'):
print(mean_points_str)

输出:

Mean Points per Year (using np.mean):
Year
2014 795.25
2015 769.50
2016 725.00
2017 739.00
Name: Points, dtype: float64
Mean Points per Year (using 'mean'):
Year
2014 795.25
2015 769.50
2016 725.00
2017 739.00
Name: Points, dtype: float64

size() 方法是获取每个组中行数的便捷方式:

import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_team = df.groupby('Team')
# 获取每个组的大小
print(grouped_by_team.size())

输出:

Team
Devils 2
Kings 4
Riders 4
Royals 2
dtype: int64

注意:size() 会计算 NaN 值,而 count() 不会。如果您在不指定列的情况下应用 agg,它会应用于所有有效(通常是数值)列。

# 示例: 在 GroupBy 对象上使用 agg('size') (隐式应用于所有列)
print("\nUsing grouped_by_team.agg('size'):") # 使用 grouped_by_team.agg('size'):
# 注意: 这可能不是最直观的用法
# print(grouped_by_team.agg('size')) # 如果聚合多个列,这通常会产生一个 DataFrame
# 更常见的做法可能是这样计算每个组的大小:
print(grouped_by_team.agg(size=('Team', 'size')))

输出:

Using grouped_by_team.agg('size'):
size
Team
Devils 2
Kings 4
Riders 4
Royals 2

您可以将函数列表(或字符串别名)传递给 agg(),以一次计算多个汇总统计信息。结果是一个具有分层列的 DataFrame。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_team = df.groupby('Team')
# 计算每个团队分数的总和、均值和标准差
agg_stats = grouped_by_team['Points'].agg(['sum', 'mean', 'std'])
print(agg_stats)

输出:

sum mean std
Team
Devils 1536 768.00 134.350288
Kings 3107 776.75 31.899321
Riders 3049 762.25 88.567771
Royals 1505 752.50 72.831998

您还可以使用字典为不同列应用不同的聚合:df.groupby('Team').agg({'Points': ['mean', 'sum'], 'Rank': 'mean'})

转换将一个函数应用于每个组,并返回一个对象(如 Series 或 DataFrame),该对象与原始对象具有相同的索引。这对于需要与原始数据对齐的组特定计算非常有用,例如在组内对数据进行中心化或按组填充缺失值。

为此使用 transform() 方法。传递给 transform 的函数必须返回一个标量值或一个与输入组块具有相同形状的 Series/DataFrame。

# 导入 pandas 库
import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
grouped_by_team = df.groupby('Team')
# 示例: 计算每个团队组内 'Points' 的 z-score
# z-score = (值 - 组均值) / 组标准差
zscore = lambda x: (x - x.mean()) / x.std()
# 对 'Points' 列应用转换
# 注意: 如果未指定列,transform 会将函数应用于每个数值列
# 为了清晰起见,我们将在分组后选择特定的列
transformed_scores = grouped_by_team[['Points', 'Rank']].transform(zscore)
print(transformed_scores)
# 注意: 如果一个组只有一个成员,其标准差为 NaN,导致 z-score 为 NaN。
# 如果一个组的标准差为零 (所有值都相同),z-score 将为 NaN 或 Inf。

输出(如果使用了随机数据生成,值可能会有所不同;此处使用固定数据):

Points Rank
0 1.284327 -1.336306
1 0.302029 0.267261
2 0.707107 -0.707107
3 -0.707107 0.707107
4 -1.120330 0.481125
5 1.105721 1.443376
6 -0.650947 -1.082532
7 0.350004 -1.082532
8 -0.770596 0.267261
9 -0.707107 0.707107
10 0.707107 -0.707107
11 -0.815759 0.267261

过滤允许您根据在组级别评估的条件丢弃整个组。filter() 方法接受一个函数,该函数将一个组(作为 DataFrame/Series)作为输入,如果应保留该组,则返回 True,否则返回 False。

常见用例:移除未达到最小大小阈值的组。

import pandas as pd
import numpy as np
ipl_data = {
'Team': ['Riders', 'Riders', 'Devils', 'Devils', 'Kings',
'Kings', 'Kings', 'Kings', 'Riders', 'Royals', 'Royals', 'Riders'],
'Rank': [1, 2, 2, 3, 3, 4, 1, 1, 2, 4, 1, 2],
'Year': [2014, 2015, 2014, 2015, 2014, 2015, 2016, 2017, 2016, 2014, 2015, 2017],
'Points': [876, 789, 863, 673, 741, 812, 756, 788, 694, 701, 804, 690]
}
df = pd.DataFrame(ipl_data)
# 过滤掉在数据集中出现少于 3 次的团队
filtered_df = df.groupby('Team').filter(lambda group: len(group) >= 3)
print(filtered_df)

输出(只有 ‘Kings’ 和 ‘Riders’ 组保留,因为它们各有 4 条记录):

Team Rank Year Points
0 Riders 1 2014 876
1 Riders 2 2015 789
4 Kings 3 2014 741
5 Kings 4 2015 812
6 Kings 1 2016 756
7 Kings 1 2017 788
8 Riders 2 2016 694
11 Riders 2 2017 690

在这个示例中,lambda 函数 lambda group: len(group) >= 3 检查每个团队组的大小。只有满足此条件(大小 >= 3)的组才会包含在最终的 filtered_df 中。