十、数据聚合和组操作
原文:
wesmckinney.com/book/data-aggregation
译者:飞龙
此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。
如果您发现本书的在线版本有用,请考虑订购纸质版或无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。
对数据集进行分类并对每个组应用函数,无论是聚合还是转换,都可能是数据分析工作流程的关键组成部分。加载、合并和准备数据集后,您可能需要计算组统计信息或可能需要为报告或可视化目的计算数据透视表。pandas 提供了一个多功能的groupby
接口,使您能够以自然的方式切片、切块和总结数据集。
关系数据库和 SQL(结构化查询语言)的流行原因之一是数据可以很容易地进行连接、过滤、转换和聚合。然而,像 SQL 这样的查询语言对可以执行的组操作类型施加了一定的限制。正如您将看到的,借助 Python 和 pandas 的表达力,我们可以通过将它们表达为自定义 Python 函数来执行相当复杂的组操作,这些函数操作与每个组相关联的数据。在本章中,您将学习如何:
- 使用一个或多个键(以函数、数组或 DataFrame 列名的形式)将 pandas 对象分成片段
- 计算组摘要统计信息,如计数、均值或标准差,或用户定义的函数
- 应用组内转换或其他操作,如归一化、线性回归、排名或子集选择
- 计算数据透视表和交叉制表
- 执行分位数分析和其他统计组分析
注意
对时间序列数据进行基于时间的聚合,是groupby
的一个特殊用例,在本书中被称为重新采样,将在第十一章:时间序列中单独处理。*与其他章节一样,我们首先导入 NumPy 和 pandas:
In [12]: import numpy as np In [13]: import pandas as pd
10.1 如何思考组操作
Hadley Wickham,R 编程语言许多流行包的作者,为描述组操作创造了术语split-apply-combine。在过程的第一阶段中,包含在 pandas 对象中的数据,无论是 Series、DataFrame 还是其他形式,都根据您提供的一个或多个键被分割成组。分割是在对象的特定轴上执行的。例如,DataFrame 可以根据其行(axis="index"
)或列(axis="columns"
)进行分组。完成此操作后,将应用一个函数到每个组,生成一个新值。最后,所有这些函数应用的结果将合并成一个结果对象。结果对象的形式通常取决于对数据的操作。请参见图 10.1 以查看简单组聚合的模拟。
每个分组键可以采用多种形式,键不必是相同类型的:
- 一个与被分组的轴长度相同的值列表或数组
- DataFrame 中表示列名的值
- 一个字典或 Series,给出了被分组的轴上的值与组名之间的对应关系
- 要在轴索引或索引中的个别标签上调用的函数
图 10.1:组聚合的示例
请注意,后三种方法是用于生成用于拆分对象的值数组的快捷方式。如果这一切看起来很抽象,不要担心。在本章中,我将给出所有这些方法的许多示例。为了开始,这里是一个作为 DataFrame 的小表格数据集:
In [14]: df = pd.DataFrame({"key1" : ["a", "a", None, "b", "b", "a", None], ....: "key2" : pd.Series([1, 2, 1, 2, 1, None, 1], ....: dtype="Int64"), ....: "data1" : np.random.standard_normal(7), ....: "data2" : np.random.standard_normal(7)}) In [15]: df Out[15]: key1 key2 data1 data2 0 a 1 -0.204708 0.281746 1 a 2 0.478943 0.769023 2 None 1 -0.519439 1.246435 3 b 2 -0.555730 1.007189 4 b 1 1.965781 -1.296221 5 a <NA> 1.393406 0.274992 6 None 1 0.092908 0.228913
假设你想使用 key1
标签计算 data1
列的均值。有多种方法可以做到这一点。一种方法是访问 data1
并使用 key1
列(一个 Series)调用 groupby
:
In [16]: grouped = df["data1"].groupby(df["key1"]) In [17]: grouped Out[17]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x17b7913f0>
这个 grouped
变量现在是一个特殊的 “GroupBy” 对象。除了一些关于组键 df["key1"]
的中间数据之外,它实际上还没有计算任何东西。这个对象的想法是它包含了对每个组应用某些操作所需的所有信息。例如,要计算组均值,我们可以调用 GroupBy 的 mean
方法:
In [18]: grouped.mean() Out[18]: key1 a 0.555881 b 0.705025 Name: data1, dtype: float64
稍后在 数据聚合 中,我将更详细地解释当你调用 .mean()
时会发生什么。这里重要的是,数据(一个 Series)已经通过在组键上拆分数据进行聚合,产生了一个新的 Series,现在由 key1
列中的唯一值进行索引。结果索引的名称是 "key1"
,因为 DataFrame 列 df["key1"]
是这样的。
如果我们传递了多个数组作为列表,将会得到不同的结果:
In [19]: means = df["data1"].groupby([df["key1"], df["key2"]]).mean() In [20]: means Out[20]: key1 key2 a 1 -0.204708 2 0.478943 b 1 1.965781 2 -0.555730 Name: data1, dtype: float64
在这里,我们使用两个键对数据进行分组,结果 Series 现在具有由观察到的唯一键对组成的分层索引:
In [21]: means.unstack() Out[21]: key2 1 2 key1 a -0.204708 0.478943 b 1.965781 -0.555730
在这个例子中,组键都是 Series,尽管它们可以是任何正确长度的数组:
In [22]: states = np.array(["OH", "CA", "CA", "OH", "OH", "CA", "OH"]) In [23]: years = [2005, 2005, 2006, 2005, 2006, 2005, 2006] In [24]: df["data1"].groupby([states, years]).mean() Out[24]: CA 2005 0.936175 2006 -0.519439 OH 2005 -0.380219 2006 1.029344 Name: data1, dtype: float64
通常,分组信息在与你要处理的数据相同的 DataFrame 中找到。在这种情况下,你可以将列名(无论是字符串、数字还是其他 Python 对象)作为组键传递:
In [25]: df.groupby("key1").mean() Out[25]: key2 data1 data2 key1 a 1.5 0.555881 0.441920 b 1.5 0.705025 -0.144516 In [26]: df.groupby("key2").mean(numeric_only=True) Out[26]: data1 data2 key2 1 0.333636 0.115218 2 -0.038393 0.888106 In [27]: df.groupby(["key1", "key2"]).mean() Out[27]: data1 data2 key1 key2 a 1 -0.204708 0.281746 2 0.478943 0.769023 b 1 1.965781 -1.296221 2 -0.555730 1.007189
你可能会注意到,在第二种情况下,有必要传递 numeric_only=True
,因为 key1
列不是数值列,因此不能使用 mean()
进行聚合。
无论使用 groupby
的目的是什么,一个通常有用的 GroupBy 方法是 size
,它返回一个包含组大小的 Series:
In [28]: df.groupby(["key1", "key2"]).size() Out[28]: key1 key2 a 1 1 2 1 b 1 1 2 1 dtype: int64
请注意,默认情况下,组键中的任何缺失值都会被排除在结果之外。通过将 dropna=False
传递给 groupby
可以禁用此行为:
In [29]: df.groupby("key1", dropna=False).size() Out[29]: key1 a 3 b 2 NaN 2 dtype: int64 In [30]: df.groupby(["key1", "key2"], dropna=False).size() Out[30]: key1 key2 a 1 1 2 1 <NA> 1 b 1 1 2 1 NaN 1 2 dtype: int64
一种类似于 size
的组函数是 count,它计算每个组中的非空值的数量:
In [31]: df.groupby("key1").count() Out[31]: key2 data1 data2 key1 a 2 3 3 b 2 2 2
遍历组
groupby
返回的对象支持迭代,生成一个包含组名和数据块的 2 元组序列。考虑以下内容:
In [32]: for name, group in df.groupby("key1"): ....: print(name) ....: print(group) ....: a key1 key2 data1 data2 0 a 1 -0.204708 0.281746 1 a 2 0.478943 0.769023 5 a <NA> 1.393406 0.274992 b key1 key2 data1 data2 3 b 2 -0.555730 1.007189 4 b 1 1.965781 -1.296221
在多个键的情况下,元组中的第一个元素将是一个键值的元组:
In [33]: for (k1, k2), group in df.groupby(["key1", "key2"]): ....: print((k1, k2)) ....: print(group) ....: ('a', 1) key1 key2 data1 data2 0 a 1 -0.204708 0.281746 ('a', 2) key1 key2 data1 data2 1 a 2 0.478943 0.769023 ('b', 1) key1 key2 data1 data2 4 b 1 1.965781 -1.296221 ('b', 2) key1 key2 data1 data2 3 b 2 -0.55573 1.007189
当然,你可以选择对数据块做任何你想做的事情。一个你可能会发现有用的方法是将数据块计算为一个字典:
In [34]: pieces = {name: group for name, group in df.groupby("key1")} In [35]: pieces["b"] Out[35]: key1 key2 data1 data2 3 b 2 -0.555730 1.007189 4 b 1 1.965781 -1.296221
默认情况下,groupby
在 axis="index"
上进行分组,但你可以在任何其他轴上进行分组。例如,我们可以按照我们的示例 df
的列是否以 "key"
或 "data"
开头进行分组:
In [36]: grouped = df.groupby({"key1": "key", "key2": "key", ....: "data1": "data", "data2": "data"}, axis="columns")
我们可以这样打印出组:
In [37]: for group_key, group_values in grouped: ....: print(group_key) ....: print(group_values) ....: data data1 data2 0 -0.204708 0.281746 1 0.478943 0.769023 2 -0.519439 1.246435 3 -0.555730 1.007189 4 1.965781 -1.296221 5 1.393406 0.274992 6 0.092908 0.228913 key key1 key2 0 a 1 1 a 2 2 None 1 3 b 2 4 b 1 5 a <NA> 6 None 1
选择列或列的子集
从 DataFrame 创建的 GroupBy 对象进行索引,使用列名或列名数组会对聚合进行列子集操作。这意味着:
df.groupby("key1")["data1"] df.groupby("key1")[["data2"]]
是方便的:
df["data1"].groupby(df["key1"]) df[["data2"]].groupby(df["key1"])
特别是对于大型数据集,可能只需要聚合几列。例如,在前面的数据集中,仅计算 data2
列的均值并将结果作为 DataFrame 获取,我们可以这样写:
In [38]: df.groupby(["key1", "key2"])[["data2"]].mean() Out[38]: data2 key1 key2 a 1 0.281746 2 0.769023 b 1 -1.296221 2 1.007189
通过这种索引操作返回的对象是一个分组的 DataFrame(如果传递了列表或数组),或者是一个分组的 Series(如果只传递了一个列名作为标量):
In [39]: s_grouped = df.groupby(["key1", "key2"])["data2"] In [40]: s_grouped Out[40]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x17b8356c0> In [41]: s_grouped.mean() Out[41]: key1 key2 a 1 0.281746 2 0.769023 b 1 -1.296221 2 1.007189 Name: data2, dtype: float64
使用字典和 Series 进行分组
分组信息可能以其他形式存在,而不仅仅是数组。让我们考虑另一个示例 DataFrame:
In [42]: people = pd.DataFrame(np.random.standard_normal((5, 5)), ....: columns=["a", "b", "c", "d", "e"], ....: index=["Joe", "Steve", "Wanda", "Jill", "Trey"]) In [43]: people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values In [44]: people Out[44]: a b c d e Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025 Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228 Wanda -0.577087 NaN NaN 0.523772 0.000940 Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761 Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512
现在,假设我有列的分组对应关系,并且想要按组对列求和:
In [45]: mapping = {"a": "red", "b": "red", "c": "blue", ....: "d": "blue", "e": "red", "f" : "orange"}
现在,您可以从这个字典构造一个数组传递给groupby
,但我们可以直接传递字典(我包含了键"f"
来突出显示未使用的分组键是可以的):
In [46]: by_column = people.groupby(mapping, axis="columns") In [47]: by_column.sum() Out[47]: blue red Joe -2.373480 3.908371 Steve 3.725929 -1.999539 Wanda 0.523772 -0.576147 Jill -3.201385 -1.230495 Trey -1.146107 -1.364125
相同的功能也适用于 Series,它可以被视为一个固定大小的映射:
In [48]: map_series = pd.Series(mapping) In [49]: map_series Out[49]: a red b red c blue d blue e red f orange dtype: object In [50]: people.groupby(map_series, axis="columns").count() Out[50]: blue red Joe 2 3 Steve 2 3 Wanda 1 2 Jill 2 3 Trey 2 3
使用函数分组
使用 Python 函数比使用字典或 Series 定义分组映射更通用。作为分组键传递的任何函数将针对每个索引值(或者如果使用axis="columns"
则是每个列值)调用一次,返回值将用作分组名称。更具体地,考虑前一节中的示例 DataFrame,其中人们的名字作为索引值。假设您想按名称长度分组。虽然您可以计算一个字符串长度的数组,但更简单的方法是只传递len
函数:
In [51]: people.groupby(len).sum() Out[51]: a b c d e 3 1.352917 0.886429 -2.001637 -0.371843 1.669025 4 0.483052 -0.153399 -2.097088 -2.250405 -2.924273 5 -1.015657 -0.539741 0.476985 3.772716 -1.020287
将函数与数组、字典或 Series 混合在一起不是问题,因为所有内容在内部都会转换为数组:
In [52]: key_list = ["one", "one", "one", "two", "two"] In [53]: people.groupby([len, key_list]).min() Out[53]: a b c d e 3 one 1.352917 0.886429 -2.001637 -0.371843 1.669025 4 two -0.860757 -0.713544 -1.265934 -2.370232 -1.860761 5 one -0.577087 -0.539741 0.476985 0.523772 -1.021228
按索引级别分组
对于具有层次索引的数据集,最后一个便利之处是能够使用轴索引的一个级别进行聚合。让我们看一个例子:
In [54]: columns = pd.MultiIndex.from_arrays([["US", "US", "US", "JP", "JP"], ....: [1, 3, 5, 1, 3]], ....: names=["cty", "tenor"]) In [55]: hier_df = pd.DataFrame(np.random.standard_normal((4, 5)), columns=column s) In [56]: hier_df Out[56]: cty US JP tenor 1 3 5 1 3 0 0.332883 -2.359419 -0.199543 -1.541996 -0.970736 1 -1.307030 0.286350 0.377984 -0.753887 0.331286 2 1.349742 0.069877 0.246674 -0.011862 1.004812 3 1.327195 -0.919262 -1.549106 0.022185 0.758363
要按级别分组,请使用level
关键字传递级别编号或名称:
In [57]: hier_df.groupby(level="cty", axis="columns").count() Out[57]: cty JP US 0 2 3 1 2 3 2 2 3 3 2 3
10.2 数据聚合
聚合指的是从数组中产生标量值的任何数据转换。前面的示例中使用了其中几个,包括mean
、count
、min
和sum
。当您在 GroupBy 对象上调用mean()
时,您可能会想知道发生了什么。许多常见的聚合,如表 10.1 中找到的那些,都有优化的实现。但是,您不仅限于这组方法。
表 10.1:优化的groupby
方法
函数名称 | 描述 |
any, all |
如果任何(一个或多个值)或所有非 NA 值为“真值”则返回True |
count |
非 NA 值的数量 |
cummin, cummax |
非 NA 值的累积最小值和最大值 |
cumsum |
非 NA 值的累积和 |
cumprod |
非 NA 值的累积乘积 |
first, last |
首个和最后一个非 NA 值 |
mean |
非 NA 值的均值 |
median |
非 NA 值的算术中位数 |
min, max |
非 NA 值的最小值和最大值 |
nth |
检索在排序顺序中出现在位置n 的值 |
ohlc |
为类似时间序列的数据计算四个“开盘-最高-最低-收盘”统计数据 |
prod |
非 NA 值的乘积 |
quantile |
计算样本分位数 |
rank |
非 NA 值的序数排名,类似于调用Series.rank |
size |
计算组大小,将结果返回为 Series |
sum |
非 NA 值的总和 |
std, var |
样本标准差和方差 |
您可以使用自己设计的聚合,并额外调用任何也在被分组对象上定义的方法。例如,nsmallest
Series 方法从数据中选择请求的最小数量的值。虽然nsmallest
没有明确为 GroupBy 实现,但我们仍然可以使用它与非优化的实现。在内部,GroupBy 将 Series 切片,为每个片段调用piece.nsmallest(n)
,然后将这些结果组装成结果对象:
In [58]: df Out[58]: key1 key2 data1 data2 0 a 1 -0.204708 0.281746 1 a 2 0.478943 0.769023 2 None 1 -0.519439 1.246435 3 b 2 -0.555730 1.007189 4 b 1 1.965781 -1.296221 5 a <NA> 1.393406 0.274992 6 None 1 0.092908 0.228913 In [59]: grouped = df.groupby("key1") In [60]: grouped["data1"].nsmallest(2) Out[60]: key1 a 0 -0.204708 1 0.478943 b 3 -0.555730 4 1.965781 Name: data1, dtype: float64
要使用自己的聚合函数,只需将任何聚合数组的函数传递给aggregate
方法或其简短别名agg
:
In [61]: def peak_to_peak(arr): ....: return arr.max() - arr.min() In [62]: grouped.agg(peak_to_peak) Out[62]: key2 data1 data2 key1 a 1 1.598113 0.494031 b 1 2.521511 2.303410
您可能会注意到一些方法,比如describe
,即使严格来说它们不是聚合也可以工作:
In [63]: grouped.describe() Out[63]: key2 data1 ... count mean std min 25% 50% 75% max count mean ... key1 ... a 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 3.0 0.555881 ... \ b 2.0 1.5 0.707107 1.0 1.25 1.5 1.75 2.0 2.0 0.705025 ... data2 75% max count mean std min 25% key1 a 0.936175 1.393406 3.0 0.441920 0.283299 0.274992 0.278369 \ b 1.335403 1.965781 2.0 -0.144516 1.628757 -1.296221 -0.720368 50% 75% max key1 a 0.281746 0.525384 0.769023 b -0.144516 0.431337 1.007189 [2 rows x 24 columns]
我将在应用:通用的分割-应用-合并中更详细地解释这里发生了什么。
注意
自定义聚合函数通常比在 Table 10.1 中找到的优化函数慢得多。这是因为在构建中间组数据块时存在一些额外开销(函数调用,数据重新排列)*### 按列和多函数应用
让我们回到上一章中使用的小费数据集。在使用pandas.read_csv
加载后,我们添加一个小费百分比列:
In [64]: tips = pd.read_csv("examples/tips.csv") In [65]: tips.head() Out[65]: total_bill tip smoker day time size 0 16.99 1.01 No Sun Dinner 2 1 10.34 1.66 No Sun Dinner 3 2 21.01 3.50 No Sun Dinner 3 3 23.68 3.31 No Sun Dinner 2 4 24.59 3.61 No Sun Dinner 4
现在我将添加一个tip_pct
列,其中包含总账单的小费百分比:
In [66]: tips["tip_pct"] = tips["tip"] / tips["total_bill"] In [67]: tips.head() Out[67]: total_bill tip smoker day time size tip_pct 0 16.99 1.01 No Sun Dinner 2 0.059447 1 10.34 1.66 No Sun Dinner 3 0.160542 2 21.01 3.50 No Sun Dinner 3 0.166587 3 23.68 3.31 No Sun Dinner 2 0.139780 4 24.59 3.61 No Sun Dinner 4 0.146808
正如您已经看到的,聚合 Series 或 DataFrame 的所有列是使用aggregate
(或agg
)与所需函数或调用mean
或std
方法的问题。但是,您可能希望根据列使用不同的函数进行聚合,或者一次使用多个函数。幸运的是,这是可能的,我将通过一些示例来说明。首先,我将按day
和smoker
对tips
进行分组:
In [68]: grouped = tips.groupby(["day", "smoker"])
请注意,对于像 Table 10.1 中的描述性统计数据,您可以将函数的名称作为字符串传递:
In [69]: grouped_pct = grouped["tip_pct"] In [70]: grouped_pct.agg("mean") Out[70]: day smoker Fri No 0.151650 Yes 0.174783 Sat No 0.158048 Yes 0.147906 Sun No 0.160113 Yes 0.187250 Thur No 0.160298 Yes 0.163863 Name: tip_pct, dtype: float64
如果您传递的是函数或函数名称的列表,您将获得一个列名从函数中获取的 DataFrame:
In [71]: grouped_pct.agg(["mean", "std", peak_to_peak]) Out[71]: 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
在这里,我们将一系列聚合函数传递给agg
,以独立评估数据组。
您不需要接受 GroupBy 为列提供的名称;特别是,lambda
函数的名称为""
,这使得它们难以识别(您可以通过查看函数的__name__
属性来自行查看)。因此,如果您传递一个(name, function)
元组的列表,每个元组的第一个元素将被用作 DataFrame 列名(您可以将 2 元组的列表视为有序映射):
In [72]: grouped_pct.agg([("average", "mean"), ("stdev", np.std)]) Out[72]: 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
使用 DataFrame,您有更多的选项,因为您可以指定要应用于所有列或不同列的不同函数的函数列表。首先,假设我们想要计算tip_pct
和total_bill
列的相同三个统计数据:
In [73]: functions = ["count", "mean", "max"] In [74]: result = grouped[["tip_pct", "total_bill"]].agg(functions) In [75]: result Out[75]: 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
如您所见,生成的 DataFrame 具有分层列,与分别聚合每列并使用列名作为keys
参数使用concat
粘合结果时获得的结果相同:
In [76]: result["tip_pct"] Out[76]: count mean max day smoker Fri No 4 0.151650 0.187735 Yes 15 0.174783 0.263480 Sat No 45 0.158048 0.291990 Yes 42 0.147906 0.325733 Sun No 57 0.160113 0.252672 Yes 19 0.187250 0.710345 Thur No 45 0.160298 0.266312 Yes 17 0.163863 0.241255
与以前一样,可以传递具有自定义名称的元组列表:
In [77]: ftuples = [("Average", "mean"), ("Variance", np.var)] In [78]: grouped[["tip_pct", "total_bill"]].agg(ftuples) Out[78]: tip_pct total_bill Average Variance Average Variance day smoker Fri No 0.151650 0.000791 18.420000 25.596333 Yes 0.174783 0.002631 16.813333 82.562438 Sat No 0.158048 0.001581 19.661778 79.908965 Yes 0.147906 0.003767 21.276667 101.387535 Sun No 0.160113 0.001793 20.506667 66.099980 Yes 0.187250 0.023757 24.120000 109.046044 Thur No 0.160298 0.001503 17.113111 59.625081 Yes 0.163863 0.001551 19.190588 69.808518
现在,假设您想要对一个或多个列应用可能不同的函数。为此,请将包含列名到迄今为止列出的任何函数规范的映射的字典传递给agg
:
In [79]: grouped.agg({"tip" : np.max, "size" : "sum"}) Out[79]: 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 In [80]: grouped.agg({"tip_pct" : ["min", "max", "mean", "std"], ....: "size" : "sum"}) Out[80]: 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
只有在至少对一列应用多个函数时,DataFrame 才会具有分层列。
返回不带行索引的聚合数据
到目前为止的所有示例中,聚合数据都带有一个索引,可能是分层的,由唯一的组键组合组成。由于这并不总是理想的,您可以通过在大多数情况下将as_index=False
传递给groupby
来禁用此行为:
In [81]: grouped = tips.groupby(["day", "smoker"], as_index=False) In [82]: grouped.mean(numeric_only=True) Out[82]: 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
当然,通过在结果上调用reset_index
,总是可以以这种格式获得结果。使用as_index=False
参数可以避免一些不必要的计算。*## 10.3 应用:通用的分割-应用-合并
最通用的 GroupBy 方法是apply
,这是本节的主题。apply
将被操作的对象分割成片段,对每个片段调用传递的函数,然后尝试连接这些片段。
回到以前的小费数据集,假设您想要按组选择前五个tip_pct
值。首先,编写一个函数,该函数选择特定列中最大值的行:
In [83]: def top(df, n=5, column="tip_pct"): ....: return df.sort_values(column, ascending=False)[:n] In [84]: top(tips, n=6) Out[84]: total_bill tip smoker day time size tip_pct 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 232 11.61 3.39 No Sat Dinner 2 0.291990 183 23.17 6.50 Yes Sun Dinner 4 0.280535 109 14.31 4.00 Yes Sat Dinner 2 0.279525
现在,如果我们按smoker
分组,并使用此函数调用apply
,我们将得到以下结果:
In [85]: tips.groupby("smoker").apply(top) Out[85]: 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
这里发生了什么?首先,根据smoker
的值将tips
DataFrame 分成组。然后在每个组上调用top
函数,并使用pandas.concat
将每个函数调用的结果粘合在一起,用组名标记各个部分。因此,结果具有一个具有内部级别的分层索引,该级别包含原始 DataFrame 的索引值。
如果您将一个接受其他参数或关键字的函数传递给apply
,则可以在函数之后传递这些参数:
In [86]: tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill") Out[86]: 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
可能需要一些创造力。传递的函数内部发生的事情取决于你;它必须返回一个 pandas 对象或一个标量值。本章的其余部分主要将包含示例,向您展示如何使用groupby
解决各种问题。
例如,你可能还记得我之前在 GroupBy 对象上调用describe
:
In [87]: result = tips.groupby("smoker")["tip_pct"].describe() In [88]: result Out[88]: count mean std min 25% 50% 75% smoker No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014 \ Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059 max smoker No 0.291990 Yes 0.710345 In [89]: result.unstack("smoker") Out[89]: smoker count No 151.000000 Yes 93.000000 mean No 0.159328 Yes 0.163196 std No 0.039910 Yes 0.085119 min No 0.056797 Yes 0.035638 25% No 0.136906 Yes 0.106771 50% No 0.155625 Yes 0.153846 75% No 0.185014 Yes 0.195059 max No 0.291990 Yes 0.710345 dtype: float64
在 GroupBy 中,当你调用像describe
这样的方法时,实际上只是一个快捷方式:
def f(group): return group.describe() grouped.apply(f)
抑制组键
在前面的示例中,您可以看到生成的对象具有从组键形成的分层索引,以及原始对象的每个部分的索引。您可以通过将group_keys=False
传递给groupby
来禁用这一点:
In [90]: tips.groupby("smoker", group_keys=False).apply(top) Out[90]: 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
分位数和桶分析
正如你可能从第八章:数据整理:连接、合并和重塑中记得的那样,pandas 有一些工具,特别是pandas.cut
和pandas.qcut
,可以将数据切分成您选择的桶或样本分位数。将这些函数与groupby
结合起来,可以方便地对数据集进行桶或分位数分析。考虑一个简单的随机数据集和使用pandas.cut
进行等长度桶分类:
In [91]: frame = pd.DataFrame({"data1": np.random.standard_normal(1000), ....: "data2": np.random.standard_normal(1000)}) In [92]: frame.head() Out[92]: data1 data2 0 -0.660524 -0.612905 1 0.862580 0.316447 2 -0.010032 0.838295 3 0.050009 -1.034423 4 0.670216 0.434304 In [93]: quartiles = pd.cut(frame["data1"], 4) In [94]: quartiles.head(10) Out[94]: 0 (-1.23, 0.489] 1 (0.489, 2.208] 2 (-1.23, 0.489] 3 (-1.23, 0.489] 4 (0.489, 2.208] 5 (0.489, 2.208] 6 (-1.23, 0.489] 7 (-1.23, 0.489] 8 (-2.956, -1.23] 9 (-1.23, 0.489] Name: data1, dtype: category Categories (4, interval[float64, right]): [(-2.956, -1.23] < (-1.23, 0.489] < (0. 489, 2.208] < (2.208, 3.928]]
cut
返回的Categorical
对象可以直接传递给groupby
。因此,我们可以计算四分位数的一组组统计信息,如下所示:
In [95]: def get_stats(group): ....: return pd.DataFrame( ....: {"min": group.min(), "max": group.max(), ....: "count": group.count(), "mean": group.mean()} ....: ) In [96]: grouped = frame.groupby(quartiles) In [97]: grouped.apply(get_stats) Out[97]: min max count mean data1 (-2.956, -1.23] data1 -2.949343 -1.230179 94 -1.658818 data2 -3.399312 1.670835 94 -0.033333 (-1.23, 0.489] data1 -1.228918 0.488675 598 -0.329524 data2 -2.989741 3.260383 598 -0.002622 (0.489, 2.208] data1 0.489965 2.200997 298 1.065727 data2 -3.745356 2.954439 298 0.078249 (2.208, 3.928] data1 2.212303 3.927528 10 2.644253 data2 -1.929776 1.765640 10 0.024750
请记住,同样的结果可以更简单地计算为:
In [98]: grouped.agg(["min", "max", "count", "mean"]) Out[98]: data1 data2 min max count mean min max count data1 (-2.956, -1.23] -2.949343 -1.230179 94 -1.658818 -3.399312 1.670835 94 \ (-1.23, 0.489] -1.228918 0.488675 598 -0.329524 -2.989741 3.260383 598 (0.489, 2.208] 0.489965 2.200997 298 1.065727 -3.745356 2.954439 298 (2.208, 3.928] 2.212303 3.927528 10 2.644253 -1.929776 1.765640 10 mean data1 (-2.956, -1.23] -0.033333 (-1.23, 0.489] -0.002622 (0.489, 2.208] 0.078249 (2.208, 3.928] 0.024750
这些是等长度的桶;要基于样本分位数计算等大小的桶,使用pandas.qcut
。我们可以将4
作为桶的数量计算样本四分位数,并传递labels=False
以仅获取四分位数索引而不是间隔:
In [99]: quartiles_samp = pd.qcut(frame["data1"], 4, labels=False) In [100]: quartiles_samp.head() Out[100]: 0 1 1 3 2 2 3 2 4 3 Name: data1, dtype: int64 In [101]: grouped = frame.groupby(quartiles_samp) In [102]: grouped.apply(get_stats) Out[102]: min max count mean data1 0 data1 -2.949343 -0.685484 250 -1.212173 data2 -3.399312 2.628441 250 -0.027045 1 data1 -0.683066 -0.030280 250 -0.368334 data2 -2.630247 3.260383 250 -0.027845 2 data1 -0.027734 0.618965 250 0.295812 data2 -3.056990 2.458842 250 0.014450 3 data1 0.623587 3.927528 250 1.248875 data2 -3.745356 2.954439 250 0.115899
示例:使用组特定值填充缺失值
在清理缺失数据时,有些情况下您将使用dropna
删除数据观察值,但在其他情况下,您可能希望使用固定值或从数据中派生的某个值填充空(NA)值。fillna
是正确的工具;例如,这里我用均值填充了空值:
In [103]: s = pd.Series(np.random.standard_normal(6)) In [104]: s[::2] = np.nan In [105]: s Out[105]: 0 NaN 1 0.227290 2 NaN 3 -2.153545 4 NaN 5 -0.375842 dtype: float64 In [106]: s.fillna(s.mean()) Out[106]: 0 -0.767366 1 0.227290 2 -0.767366 3 -2.153545 4 -0.767366 5 -0.375842 dtype: float64
假设您需要填充值根据组而变化。一种方法是对数据进行分组,并使用调用fillna
的函数在每个数据块上使用apply
。这里是一些关于美国各州的样本数据,分为东部和西部地区:
In [107]: states = ["Ohio", "New York", "Vermont", "Florida", .....: "Oregon", "Nevada", "California", "Idaho"] In [108]: group_key = ["East", "East", "East", "East", .....: "West", "West", "West", "West"] In [109]: data = pd.Series(np.random.standard_normal(8), index=states) In [110]: data Out[110]: Ohio 0.329939 New York 0.981994 Vermont 1.105913 Florida -1.613716 Oregon 1.561587 Nevada 0.406510 California 0.359244 Idaho -0.614436 dtype: float64
让我们将数据中的一些值设置为缺失:
In [111]: data[["Vermont", "Nevada", "Idaho"]] = np.nan In [112]: data Out[112]: Ohio 0.329939 New York 0.981994 Vermont NaN Florida -1.613716 Oregon 1.561587 Nevada NaN California 0.359244 Idaho NaN dtype: float64 In [113]: data.groupby(group_key).size() Out[113]: East 4 West 4 dtype: int64 In [114]: data.groupby(group_key).count() Out[114]: East 3 West 2 dtype: int64 In [115]: data.groupby(group_key).mean() Out[115]: East -0.100594 West 0.960416 dtype: float64
我们可以使用组均值填充 NA 值,如下所示:
In [116]: def fill_mean(group): .....: return group.fillna(group.mean()) In [117]: data.groupby(group_key).apply(fill_mean) Out[117]: East Ohio 0.329939 New York 0.981994 Vermont -0.100594 Florida -1.613716 West Oregon 1.561587 Nevada 0.960416 California 0.359244 Idaho 0.960416 dtype: float64
在另一种情况下,您可能在代码中预定义了根据组变化的填充值。由于组内部设置了name
属性,我们可以使用它:
In [118]: fill_values = {"East": 0.5, "West": -1} In [119]: def fill_func(group): .....: return group.fillna(fill_values[group.name]) In [120]: data.groupby(group_key).apply(fill_func) Out[120]: East Ohio 0.329939 New York 0.981994 Vermont 0.500000 Florida -1.613716 West Oregon 1.561587 Nevada -1.000000 California 0.359244 Idaho -1.000000 dtype: float64
Python 数据分析(PYDA)第三版(五)(2)https://developer.aliyun.com/article/1482390