按组分组:拆分-应用-合并#
“分组依据”是指包含以下一个或多个步骤的过程:
根据某些标准将数据**拆分**成组。
**应用**一个函数到每个组,独立地。
将结果**合并**到一个数据结构中。
在这些操作中,拆分步骤是最简单的。在应用步骤中,我们可能希望执行以下操作之一:
聚合:为每个组计算一个(或多个)汇总统计量。一些示例:
计算组的总和或平均值。
计算组的大小/计数。
转换:执行一些特定于组的计算,并返回一个具有相似索引的对象。一些示例:
在组内对数据进行标准化(z 分数)。
使用每个组派生的值在组内填充 NA。
过滤:根据一个评估为 True 或 False 的与组相关的计算来丢弃某些组。一些示例:
丢弃属于成员很少的组的数据。
根据组的总和或平均值过滤掉数据。
这些操作中的许多都定义在 GroupBy 对象上。这些操作类似于 aggregating API 、window API 和 resample API 的操作。
有可能给定的操作不属于以上类别之一,或者它是它们的某种组合。在这种情况下,可以使用 GroupBy 的 apply 方法来计算该操作。该方法将检查应用步骤的结果,并在不适合以上三个类别之一的情况下,尝试将它们巧妙地合并成一个单一的结果。
备注
使用内置的 GroupBy 操作将操作拆分成多个步骤,其效率将高于使用 apply 方法和用户定义的 Python 函数。
对于那些熟悉 SQL 工具(或 itertools)的人来说,“GroupBy”这个名字应该很熟悉,在这些工具中,您可以编写如下代码:
SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2
我们的目标是使像这样的操作自然且易于使用 pandas 来表达。我们将讨论 GroupBy 功能的每个领域,然后提供一些非琐碎的示例/用例。
有关一些高级策略,请参阅 cookbook 。
拆分一个对象到组#
分组的抽象定义是提供标签到组名的映射。要创建一个 GroupBy 对象(稍后将详细介绍 GroupBy 对象是什么),您可以执行以下操作:
映射可以通过多种不同的方式指定:
一个 Python 函数,用于调用每个索引标签。
一个与索引长度相同的列表或 NumPy 数组。
一个
dict或Series,提供一个label -> group name映射。对于
DataFrame对象,一个字符串,表示一个列名或一个索引级别名称,用于分组。上面任何一种东西的列表。
我们统称为分组对象为**键**。例如,考虑以下 DataFrame:
备注
传递给 groupby 的字符串可能引用列名或索引级别名称。如果字符串同时匹配列名和索引级别名称,则会引发 ValueError。
在 DataFrame 上,我们通过调用 groupby() 来获取 GroupBy 对象。此方法返回一个 pandas.api.typing.DataFrameGroupBy 实例。我们可以自然地按 A 或 B 列,或两者分组:
备注
df.groupby('A') 只是 df.groupby(df['A']) 的语法糖。
如果我们对列 A 和 B 还有一个 MultiIndex,我们可以按除指定列之外的所有列进行分组:
上面的 GroupBy 将在索引(行)上拆分 DataFrame。要通过列拆分,请先进行转置:
pandas 的 Index 对象支持重复值。如果在 groupby 操作中使用非唯一索引作为分组键,同一个索引值的所有值将被视为一个组,因此聚合函数的输出将只包含唯一的索引值:
请注意,直到需要时才会进行拆分。创建 GroupBy 对象仅仅会验证你传入了一个有效的映射。
备注
许多复杂的复杂数据操作都可以用 GroupBy 操作来表达(尽管不能保证是最有效的实现)。你可以对标签映射函数进行创意性的操作。
GroupBy 排序#
默认情况下,组键会在 groupby 操作期间被排序。但是,你可以传入 sort=False 来潜在地加速操作。使用 sort=False 时,组键的顺序将遵循键在原始 DataFrame 中出现的顺序:
请注意,groupby 会保留*观测值*在每个组内排序的顺序。例如,下面 groupby() 创建的组的顺序与它们在原始 DataFrame 中出现的顺序一致:
GroupBy dropna#
默认情况下,NA 值在 groupby 操作期间被从组键中排除。但是,如果你想在组键中包含 NA 值,可以通过传入 dropna=False 来实现。
dropna 参数的默认设置为 True,这意味着 NA 值不包含在组键中。
GroupBy 对象属性#
groups 属性是一个字典,其键是计算出的唯一组,对应的值是属于每个组的轴标签。在上面的例子中,我们有:
对 GroupBy 对象调用标准的 Python len 函数会返回组的数量,这与 groups 字典的长度相同:
GroupBy 会自动补全列名、GroupBy 操作和其他属性:
MultiIndex 的 GroupBy#
对于 hierarchically-indexed data ,按层级的一个级别进行分组是很自然的。
让我们创建一个具有两级 MultiIndex 的 Series。
然后我们可以按 s 中的一个级别进行分组。
如果 MultiIndex 指定了名称,则可以使用这些名称而不是级别编号:
支持按多个级别进行分组。
可以提供索引级别名称作为键。
更多关于 sum 函数和聚合的知识将在后面介绍。
使用 Index 级别和列对 DataFrame 进行分组#
DataFrame 可以通过列和索引级别的组合进行分组。你可以同时指定列名和索引名,或者使用 Grouper 。
首先,我们创建一个带有 MultiIndex 的 DataFrame:
然后我们按``second``索引级别和``A``列对``df``进行分组。
也可以通过名称指定索引级别。
可以直接将索引级别名称作为键传递给 groupby。
GroupBy 中的 DataFrame 列选择#
一旦你从 DataFrame 创建了 GroupBy 对象,你可能想对每个列执行不同的操作。因此,通过在 GroupBy 对象上使用 [],就像用于从 DataFrame 获取列一样,你可以做到:
这主要是为了简化语法,替代方案要冗长得多:
此外,这种方法避免了重新计算从传递的键派生的内部分组信息。
如果你想对分组列本身进行操作,也可以包含它们。
迭代分组#
有了 GroupBy 对象,遍历分组数据非常自然,并且其功能类似于 itertools.groupby() :
在按多个键分组的情况下,组名将是一个元组:
请参阅 迭代分组 。
选择一个组#
可以使用 DataFrameGroupBy.get_group() 选择单个组:
或者对于按多列分组的对象:
聚合#
聚合是一个 GroupBy 操作,它会降低分组对象的维度。聚合的结果对于组中每列的*标量值*是(或至少被视为)一个标量值。例如,生成分组值中每列的总和。
在结果中,组的键默认出现在索引中。通过传递 as_index=False,它们也可以包含在列中。
内置聚合方法#
许多常见的聚合方法已内置于 GroupBy 对象中。在下面的方法列表中,带有 * 的方法*没有*高效的、针对 GroupBy 的实现。
方法 |
描述 |
|---|---|
计算组中的任何值是否为真 |
|
计算组中的所有值是否为真 |
|
计算组中非 NA 值的数量 |
|
|
计算组的协方差 |
计算每组中第一次出现的值 |
|
计算每组中最大值的索引 |
|
计算每组中最小值的索引 |
|
计算每组中最后一次出现的值 |
|
计算每组中的最大值 |
|
计算每组的平均值 |
|
计算每组的中位数 |
|
计算每组中的最小值 |
|
计算每组中唯一值的数量 |
|
计算每组值的乘积 |
|
计算每组值给定的分位数 |
|
计算每组值的平均值的标准误差 |
|
计算每组中的值数量 |
|
|
计算每组值的偏度 |
计算每组值的标准差 |
|
计算每组值的总和 |
|
计算每组值的方差 |
一些示例:
另一个聚合示例是计算每个组的大小。这包含在 GroupBy 的 size 方法中。它返回一个 Series,其索引由组名组成,值为每个组的大小。
虽然 DataFrameGroupBy.describe() 方法本身不是一个归约器,但它可以用来方便地生成一组关于每个组的摘要统计信息。
另一个聚合示例是计算每组的唯一值数量。这与 DataFrameGroupBy.value_counts() 函数类似,只是它只计算唯一值的数量。
aggregate() 方法#
备注
aggregate() 方法可以接受多种不同类型的输入。本节详细介绍了使用各种 GroupBy 方法的字符串别名;其他输入将在以下各节中详细介绍。
pandas 实现的任何归约方法都可以作为字符串传递给 aggregate() 。鼓励用户使用简写 agg。它的作用与调用相应方法一样。
聚合的结果将以组名为新的索引。在有多个键的情况下,结果默认是 MultiIndex 。如上所述,这可以通过使用 as_index 选项来更改:
请注意,您可以使用 DataFrame.reset_index() DataFrame 函数来实现与列名存储在产生的 MultiIndex 中相同的结果,尽管这会进行额外的复制。
使用用户自定义函数进行聚合#
用户还可以提供自己的用户自定义函数(UDF)来进行自定义聚合。
警告
在使用 UDF 进行聚合时,UDF 不应修改提供的 Series。有关更多信息,请参阅 使用用户定义函数 (UDF) 方法进行变异 。
备注
使用 UDF 进行聚合的性能通常不如使用 pandas 内置的 GroupBy 方法。考虑将复杂操作分解为利用内置方法的操作链。
结果的 dtype 将反映聚合函数的结果。如果不同组的结果具有不同的 dtype,则将以与 DataFrame 构造相同的方式确定公共 dtype。
一次应用多个函数#
在分组的 Series 上,您可以将函数列表或字典传递给 SeriesGroupBy.agg() ,这将输出一个 DataFrame:
在分组的 DataFrame 上,您可以将函数列表传递给 DataFrameGroupBy.agg() 来聚合每一列,这将产生一个具有分层列索引的聚合结果:
结果的聚合名称以函数本身命名。如果需要重命名,则可以像这样在 Series 的链式操作中添加重命名:
对于分组的 DataFrame,您可以按类似的方式重命名:
备注
通常,输出列名应该是唯一的,但 pandas 允许您将同一个函数(或两个同名的函数)应用于同一个列。
pandas 还允许您提供多个 lambda 函数。在这种情况下,pandas 会对(无名的)lambda 函数的名称进行混淆,在每个后续的 lambda 函数后面追加 _<i>。
命名聚合#
为了支持*具有输出列名控制*的列特定聚合,pandas 在 DataFrameGroupBy.agg() 和 SeriesGroupBy.agg() 中接受一种特殊语法,称为“命名聚合”,其中:
关键字是*输出*列名
值是元组,元组的第一个元素是要选择的列,第二个元素是要应用于该列的聚合。pandas 提供了
NamedAgg命名元组,其字段为['column', 'aggfunc'],以使其参数更清晰。与往常一样,聚合可以是可调用对象或字符串别名。
NamedAgg 只是一个 namedtuple。普通元组也是允许的。
如果您想要的列名不是有效的 Python 关键字,则构造一个字典并解包关键字参数
在使用命名聚合时,额外的关键字参数不会传递给聚合函数;只有 (column, aggfunc) 对应的键值对应该作为 **kwargs 传递。如果您的聚合函数需要其他参数,请使用 functools.partial() 将它们部分应用。
命名聚合对于 Series 分组聚合也有效。在这种情况下,没有列选择,因此值仅仅是函数。
对 DataFrame 列应用不同的函数#
通过将字典传递给 aggregate,您可以对 DataFrame 的列应用不同的聚合:
函数名也可以是字符串。为了使字符串有效,它必须在 GroupBy 上实现:
转换#
转换是一种 GroupBy 操作,其结果的索引与被分组的对象相同。常见的示例如 cumsum() 和 diff() 。
与聚合不同,用于拆分原始对象的分组不包含在结果中。
备注
由于转换不包含用于拆分结果的分组,因此 DataFrame.groupby() 和 Series.groupby() 中的 as_index 和 sort 参数无效。
转换的一个常见用途是将结果添加回原始 DataFrame。
内置转换方法#
GroupBy 上的以下方法充当转换。
方法 |
描述 |
|---|---|
向后填充每个组内的 NA 值 |
|
计算每个组内的累积计数 |
|
计算每个组内的累积最大值 |
|
计算每个组内的累积最小值 |
|
计算每个组内的累积乘积 |
|
计算每个组内的累积和 |
|
计算每个组内相邻值之间的差值 |
|
在每个组内前向填充 NA 值 |
|
计算每个组内相邻值之间的百分比变化 |
|
计算每个组内每个值的秩 |
|
在每个组内向上或向下移位值 |
此外,将任何内置聚合方法作为字符串传递给 transform() (请参阅下一节)会将结果广播到整个组,从而生成转换后的结果。如果聚合方法有高效的实现,那么这将具有很高的性能。
transform() 方法#
与 aggregation method 类似,transform() 方法可以接受上节中内置转换方法的字符串别名。它还可以接受内置聚合方法的字符串别名。当提供聚合方法时,结果将被广播到整个组。
除了字符串别名之外,transform() 方法还可以接受用户定义的函数 (UDF)。UDF 必须:
返回一个与组块大小相同或可广播到组块大小(例如,标量、
grouped.transform(lambda x: x.iloc[-1]))的结果。按列对组块进行操作。对于第一个组块,使用 chunk.apply 进行转换。
不要对组块执行就地操作。组块应被视为不可变的,对组块的更改可能会产生意外的结果。有关更多信息,请参阅 使用用户定义函数 (UDF) 方法进行变异 。
(可选)一次对整个组块的所有列进行操作。如果支持此操作,则从第二个块开始将使用快速路径。
备注
通过为 transform 提供 UDF 进行转换通常比使用 GroupBy 上的内置方法性能差。请考虑将复杂操作分解为利用内置方法的链式操作。
此部分中的所有示例都可以通过调用内置方法而不是使用 UDF 来提高性能。请参阅 below for examples 。
在 2.0.0 版本发生变更: 在对分组后的 DataFrame 使用 .transform 并且转换函数返回 DataFrame 时,pandas 现在会将结果的索引与输入的索引对齐。您可以在转换函数中调用 .to_numpy() 来避免对齐。
与 aggregate() 方法 类似,结果的 dtype 将反映转换函数的 dtype。如果不同组的结果具有不同的 dtype,则将以与 DataFrame 构造相同的方式确定公共 dtype。
假设我们希望标准化每个组内的数据:
我们期望结果在每个组内的平均值为 0,标准差为 1(在浮点误差范围内),我们可以轻松地进行检查:
我们还可以直观地比较原始数据集和转换后的数据集。
维度较低的输出的转换函数将被广播以匹配输入数组的形状。
另一个常见的数据转换是用组均值替换缺失数据。
我们可以验证转换后的数据中的组均值没有改变,并且转换后的数据不包含 NA。
如上条注释中所述,此部分中的每个示例都可以使用内置方法更高效地计算。在下面的代码中,使用 UDF 的低效方法被注释掉,而下方的更快速的替代方法出现了。
窗口函数和重采样操作#
可以在 groupby 对象上使用 resample()、expanding() 和 rolling() 作为方法。
下面的示例将 rolling() 方法应用于列 B 的样本,基于列 A 的分组。
expanding() 方法将(在示例中为 sum())累积给定操作对于每个特定组的所有成员。
假设您想使用 resample() 方法获取数据框中每个组的每日频率,并希望使用 ffill() 方法填充缺失值。
过滤#
过滤 (Filtration) 是一个 GroupBy 操作,用于对原始分组对象进行子集拆分。它可以过滤掉整个组、组的一部分,或者两者都过滤。过滤操作返回一个调用对象的过滤版本,当提供分组列时,这些列也会被包含在结果中。在下面的示例中,class 被包含在结果中。
备注
与聚合操作不同,过滤操作不会将组键添加到结果的索引中。因此,传递 as_index=False 或 sort=True 不会影响这些方法。
过滤操作会尊重 GroupBy 对象的列子集拆分。
内置过滤#
GroupBy 对象的以下方法用作过滤操作。所有这些方法都有高效的、特定于 GroupBy 的实现。
用户还可以使用转换操作结合布尔索引,来构建组内的复杂过滤。例如,假设我们有一组产品及其销量,并且我们希望将数据子集化为只包含占据组内总销量不超过 90% 的最大产品。
filter 方法#
备注
通过提供用户定义的函数 (UDF) 来进行过滤,通常比使用 GroupBy 上的内置方法性能更差。考虑将复杂操作分解为一系列利用内置方法的操作。
filter 方法接受一个用户定义的函数 (UDF),当将该函数应用于整个组时,它会返回 True 或 False。filter 方法的结果是 UDF 返回 True 的组的子集。
假设我们只想获取属于组和大于 2 的元素的。
另一个有用的操作是过滤掉属于只有少数成员的组的元素。
或者,我们不必删除不符合条件的组,而是可以返回一个具有相同索引的对象,其中不符合过滤条件的组将被填充为 NaN。
对于具有多列的 DataFrame,过滤器应显式指定一列作为过滤标准。
灵活的 apply#
对分组数据的一些操作可能不属于聚合、转换或过滤类别。对于这些情况,您可以使用 apply 函数。
警告
apply 必须尝试从结果中推断它应该充当归约器 (reducer)、转换器 (transformer) 或者 过滤器 (filter),具体取决于传递给它的内容。因此,分组列可能包含在输出中,也可能不包含。虽然它会尝试智能猜测其行为方式,但有时可能会猜错。
备注
本节中的所有示例都可以使用其他 pandas 功能更可靠、更高效地计算。
返回结果的维度也可以发生变化:
Series 上的 apply 可以对应用函数返回的 Series 本身进行操作,并可能将结果向上转换为 DataFrame:
与 aggregate() 方法 类似,结果的 dtype 将反映应用函数的 dtype。如果不同组的结果具有不同的 dtype,则将以与 DataFrame 构建相同的方式确定一个通用 dtype。
使用 group_keys 控制分组列的放置#
要控制分组列是否包含在索引中,可以使用参数 group_keys,该参数默认为 True。比较
与
Numba 加速例程#
在 1.1 版本加入.
如果 Numba 作为可选依赖项安装,则 transform 和 aggregate 方法支持 engine='numba' 和 engine_kwargs 参数。有关参数的常规用法和性能注意事项,请参阅 enhancing performance with Numba 。
函数签名必须以 values, index 精确 开头,因为每个组的数据将被传递到 values,而组索引将被传递到 index。
警告
当使用 engine='numba' 时,内部将没有“回退”行为。组数据和组索引将作为 NumPy 数组传递给 JIT 编译的用户定义函数,不会尝试其他执行方式。
其他有用功能#
排除非数字列#
再次考虑我们一直在查看的示例 DataFrame:
假设我们希望按 A 列计算标准差。有一个小问题,那就是我们不关心 B 列中的数据,因为它不是数字。通过指定 numeric_only=True,您可以避免非数字列:
请注意,df.groupby('A').colname.std(). 比 df.groupby('A').std().colname 更高效。因此,如果聚合函数的结果仅在一个列(此处为 colname)上需要,则可以在应用聚合函数 之前 对其进行过滤。
处理(未)观测到的分类值#
当使用 Categorical 分组器(作为单个分组器或作为多个分组器的一部分)时,observed 关键字控制是返回所有可能分组器值的笛卡尔积(observed=False)还是仅返回那些已观测的分组器(observed=True)。
显示所有值:
仅显示观测到的值:
分组后返回的 dtype 将 总是 包含所有分组的类别。
NA 组处理#
我们所说的 NA 指的是任何 NA 值,包括 NA 、NaN、NaT 和 None。如果分组键中有任何 NA 值,默认情况下它们将被排除。换句话说,“NA 组”将被删除。通过指定 dropna=False,您可以包含 NA 组。
使用有序因子分组#
表示为 pandas 的 Categorical 类实例的分类变量可以用作分组键。如果是这样,其级别的顺序将被保留。当 observed=False 且 sort=False 时,任何未观测的类别将按顺序出现在结果的末尾。
使用分组规范进行分组#
您可能需要指定更多数据才能正确分组。您可以使用 pd.Grouper 来提供此本地控制。
按特定列以所需的频率分组。这类似于重采样。
当指定 freq 时,pd.Grouper 返回的对象将是 pandas.api.typing.TimeGrouper 的实例。当列名和索引名相同时,您可以使用 key 按列分组,使用 level 按索引分组。
获取每组的第一个行#
Just like for a DataFrame or Series you can call head and tail on a groupby:
这显示了每组的前 n 行或最后 n 行。
获取每组的第 n 行#
要选择每组的第 n 个元素,请使用 DataFrameGroupBy.nth() 或 SeriesGroupBy.nth() 。提供的参数可以是任何整数、整数列表、切片或切片列表;请参阅下面的示例。如果组的第 n 个元素不存在,不会引发错误;而是不返回相应的行。
通常,此操作充当过滤。在某些情况下,它还会返回每组一行,使其也成为一种规约。但是,由于它通常可以返回零行或多行,pandas 在所有情况下都将其视为过滤。
如果组的第 n 个元素不存在,则结果中不包含相应的行。特别是,如果指定的 n 大于任何组的大小,结果将是空的 DataFrame。
如果要选择第 n 个非空项,请使用 dropna 关键字参数. 对于 DataFrame,这应该像传递给 dropna 一样是 'any' 或 'all':
您还可以通过指定多个 nth 值作为整数列表来选择每组的多个行。
您也可以使用切片或切片列表。
枚举组项#
要查看每行在其组内的出现顺序,请使用 cumcount 方法:
枚举组#
要查看组的顺序(与 cumcount 决定的组内行的顺序相反),您可以使用 DataFrameGroupBy.ngroup() 。
请注意,赋予组的数字与其在迭代 groupby 对象时看到的顺序相匹配,而不是它们首次出现的顺序。
绘图#
Groupby 也可用于某些绘图方法。在这种情况下,假设我们怀疑列 1 中的值在 B 组中的平均值高三倍。
我们可以通过箱形图轻松地可视化这一点:
The result of calling boxplot is a dictionary whose keys are the values
of our grouping column g (“A” and “B”). The values of the resulting dictionary
can be controlled by the return_type keyword of boxplot.
See the visualization documentation for more.
警告
For historical reasons, df.groupby("g").boxplot() is not equivalent
to df.boxplot(by="g"). See here for
an explanation.
管道函数调用#
Similar to the functionality provided by DataFrame and Series, functions
that take GroupBy objects can be chained together using a pipe method to
allow for a cleaner, more readable syntax. To read about .pipe in general terms,
see here.
结合使用 .groupby 和 .pipe 在需要重用 GroupBy 对象时通常很有用。
例如,假设有一个 DataFrame,其中包含商店、产品、收入和销售数量等列。我们希望按商店和产品对*价格*(即收入/数量)进行分组计算。我们可以通过多步操作来完成,但使用管道表示法可以使代码更具可读性。首先,我们设置数据:
现在我们计算每个商店/产品的价格。
当您想将分组对象传递给任意函数时,管道也可以很有表达力,例如:
这里 mean 接受一个 GroupBy 对象,并分别计算每个商店-产品组合的 Revenue 和 Quantity 列的平均值。mean 函数可以是任何接受 GroupBy 对象的函数;.pipe 会将 GroupBy 对象作为参数传递给您指定的函数。
示例#
多列因子化#
By using DataFrameGroupBy.ngroup(), we can extract
information about the groups in a way similar to factorize() (as described
further in the reshaping API) but which applies
naturally to multiple columns of mixed type and different
sources. This can be useful as an intermediate categorical-like step
in processing, when the relationships between the group rows are more
important than their content, or as input to an algorithm which only
accepts the integer encoding. (For more information about support in
pandas for full categorical data, see the Categorical
introduction and the
API documentation.)
按索引器分组以“重新采样”数据#
重采样会从已有的观测数据或生成数据的模型中生成新的假设样本(重采样)。这些新样本与预先存在的样本相似。
为了使重采样能应用于非日期时间类型的索引,可以采用以下过程。
在以下示例中,df.index // 5 返回一个整数数组,该数组用于确定在分组操作中选择哪些内容。
备注
下面的示例展示了如何通过合并样本来对样本进行降采样。这里通过使用 df.index // 5,我们将样本聚合到不同的 bin 中。通过应用 std() 函数,我们将许多样本中包含的信息聚合到一小组值中,即它们的标准差,从而减少样本数量。
返回 Series 以传播名称#
对 DataFrame 列进行分组,计算一组度量,并返回一个命名的 Series。Series 名称用作列索引的名称。这在与堆叠(stacking)等重塑操作结合使用时尤其有用,其中列索引名称将用作插入列的名称: