7.11 聚合和分组
译者:飞龙
本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。
大数据分析的必要部分是有效的总结:计算聚合,如sum()
,mean()
,median()
,min()
和max()
,其中单个数字提供了大数据集的潜在本质的见解。在本节中,我们将探讨 Pandas 中的聚合,从类似于我们在 NumPy 数组中看到的简单操作,到基于groupby
概念的更复杂的操作。
为方便起见,我们将使用display
魔术函数,和我们在前面部分中看到的相同:
import numpy as np
import pandas as pd
class display(object):
"""Display HTML representation of multiple objects"""
template = """<div style="float: left; padding: 10px;">
<p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}
</div>"""
def __init__(self, *args):
self.args = args
def _repr_html_(self):
return '\n'.join(self.template.format(a, eval(a)._repr_html_())
for a in self.args)
def __repr__(self):
return '\n\n'.join(a + '\n' + repr(eval(a))
for a in self.args)
行星数据
在这里,我们将使用行星数据集,可通过 Seaborn 软件包获得(参见“可视化与 Seaborn”)。它提供了天文学家在其他恒星周围发现的行星信息(称为太阳系外行星或简称系外行星)。可以使用简单的 Seaborn 命令下载它:
import seaborn as sns
planets = sns.load_dataset('planets')
planets.shape
# (1035, 6)
planets.head()
method | number | orbital_period | mass | distance | year | |
---|---|---|---|---|---|---|
0 | Radial Velocity | 1 | 269.300 | 7.10 | 77.40 | 2006 |
1 | Radial Velocity | 1 | 874.774 | 2.21 | 56.95 | 2008 |
2 | Radial Velocity | 1 | 763.000 | 2.60 | 19.84 | 2011 |
3 | Radial Velocity | 1 | 326.030 | 19.40 | 110.62 | 2007 |
4 | Radial Velocity | 1 | 516.220 | 10.50 | 119.47 | 2009 |
这包含了截至 2014 年发现的 1,000 多颗系外行星的一些细节。
Pandas 中的简单聚合
之前,我们研究了一些可用于 NumPy 数组的数据聚合(“聚合:最小,最大和之间的任何东西”)。与一维 NumPy 数组一样,对于 Pandas Series
,聚合返回单个值:
rng = np.random.RandomState(42)
ser = pd.Series(rng.rand(5))
ser
'''
0 0.374540
1 0.950714
2 0.731994
3 0.598658
4 0.156019
dtype: float64
'''
ser.sum()
# 2.8119254917081569
ser.mean()
# 0.56238509834163142
对于DataFrame
,默认情况下聚合返回每列的结果:
df = pd.DataFrame({'A': rng.rand(5),
'B': rng.rand(5)})
df
A | B | |
---|---|---|
0 | 0.155995 | 0.020584 |
1 | 0.058084 | 0.969910 |
2 | 0.866176 | 0.832443 |
3 | 0.601115 | 0.212339 |
4 | 0.708073 | 0.181825 |
df.mean()
'''
A 0.477888
B 0.443420
dtype: float64
'''
通过指定axis
参数,你可以按行聚合:
df.mean(axis='columns')
'''
0 0.088290
1 0.513997
2 0.849309
3 0.406727
4 0.444949
dtype: float64
'''
Pandas Series
和DataFrame
包含“聚合:最小,最大和之间的任何东西”中提到的所有常见聚合;另外,还有一个方便的方法describe()
,它为每列计算几个常见聚合并返回结果。
让我们在行星数据上使用它,现在删除带有缺失值的行:
planets.dropna().describe()
number | orbital_period | mass | distance | year | |
---|---|---|---|---|---|
count | 498.00000 | 498.000000 | 498.000000 | 498.000000 | 498.000000 |
mean | 1.73494 | 835.778671 | 2.509320 | 52.068213 | 2007.377510 |
std | 1.17572 | 1469.128259 | 3.636274 | 46.596041 | 4.167284 |
min | 1.00000 | 1.328300 | 0.003600 | 1.350000 | 1989.000000 |
25% | 1.00000 | 38.272250 | 0.212500 | 24.497500 | 2005.000000 |
50% | 1.00000 | 357.000000 | 1.245000 | 39.940000 | 2009.000000 |
75% | 2.00000 | 999.600000 | 2.867500 | 59.332500 | 2011.000000 |
max | 6.00000 | 17337.500000 | 25.000000 | 354.000000 | 2014.000000 |
这可以是开始了解数据集的整体属性的有用方法。
例如,我们在year
列中看到,虽然早在 1989 年就发现了系外行星,但是一半的已知系外行星直到 2010 年或之后才发现了。这主要得益于开普勒任务,这是一种专门设计的太空望远镜,用于寻找其他恒星周围的遮蔽行星。
下表总结了其他一些内置的 Pandas 聚合:
聚合 | 描述 |
---|---|
count() |
项目总数 |
first() , last() |
第一个和最后一个项目 |
mean() , median() |
均值和中值 |
min() , max() |
最小和最大值 |
std() , var() |
标准差和方差 |
mad() |
平均绝对偏差 |
prod() |
所有项目的积 |
sum() |
所有项目的和 |
这些都是DataFrame
和Series
对象的方法。
然而,要深入探索数据,简单的聚合通常是不够的。数据汇总的下一级是groupby
操作,它允许你快速有效地计算数据子集的聚合。
分组:分割,应用和组合
简单的聚合可以为你提供数据集的风格,但我们通常更愿意在某些标签或索引上有条件地聚合:这是在所谓的groupby
操作中实现的。名称group by
来自 SQL 数据库语言中的一个命令,但使用 Rstats 的作者 Hadley Wickham 创造的术语:分割(split),应用(apply)和组合(combine)来思考它,可能更有启发性。
分割,应用和组合
这是分割-应用-组合操作的规则示例,其中“应用”是汇总聚合,如下图所示:
这清楚地表明groupby
完成了什么:
- “分割”步骤涉及根据指定键的值打破和分组
DataFrame
。 - “应用”步骤涉及计算单个组内的某些函数,通常是聚合,转换或过滤。
- “组合”步骤将这些操作的结果合并到输出数组中。
虽然这肯定可以使用前面介绍的掩码,聚合和合并命令的某种组合来手动完成,但一个重要的认识是,中间的分割不需要显式实例化。相反,GroupBy
可以(经常)只遍历单次数据来执行此操作,在此过程中更新每个组的总和,均值,计数,最小值或其他聚合。GroupBy
的强大之处在于,它抽象了这些步骤:用户不需要考虑计算如何在背后完成,而是考虑整个操作。
作为一个具体的例子,让我们看看,将 Pandas 用于此图中所示的计算。我们首先创建输入DataFrame
:
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
'data': range(6)}, columns=['key', 'data'])
df
key | data | |
---|---|---|
0 | A | 0 |
1 | B | 1 |
2 | C | 2 |
3 | A | 3 |
4 | B | 4 |
5 | C | 5 |
最基本的分割-应用-组合操作可以使用DataFrame
的groupby()
方法计算,传递所需键列的名称:
df.groupby('key')
# <pandas.core.groupby.DataFrameGroupBy object at 0x117272160>
请注意,返回值不是一组DataFrame
,而是一个DataFrameGroupBy
对象。这个对象就是神奇之处:你可以把它想象成DataFrame
的特殊视图,它做好了准备来深入挖掘分组,但在应用聚合之前不会进行实际计算。这种“惰性求值”方式意味着,可以以对用户几乎透明的方式,非常有效地实现常见聚合。
为了产生结果,我们可以将聚合应用于这个DataFrameGroupBy
对象,该对象将执行适当的应用/组合步骤来产生所需的结果:
df.groupby('key').sum()
data | |
---|---|
key | |
A | 3 |
B | 5 |
C | 7 |
`sum()方法只是这里的一种可能性; 你可以应用几乎任何常见的 Pandas 或 NumPy 聚合函数,以及几乎任何有效的
DataFrame``操作,我们将在下面的讨论中看到。
GroupBy
对象
GroupBy
对象是一个非常灵活的抽象。在许多方面,你可以简单地将它视为DataFrame
的集合,它可以解决困难的问题。让我们看一些使用行星数据的例子。
也许由GroupBy
提供的最重要的操作是聚合,过滤,转换和应用。
我们将在“聚合,过滤,转换,应用”中,更全面地讨论这些内容,但在此之前,我们将介绍一些其他功能,它们可以与基本的GroupBy
操作配合使用。
列索引
`GroupBy对象支持列索引,方式与
DataFrame相同,并返回修改后的
GroupBy``对象。例如:
planets.groupby('method')
# <pandas.core.groupby.DataFrameGroupBy object at 0x1172727b8>
planets.groupby('method')['orbital_period']
# <pandas.core.groupby.SeriesGroupBy object at 0x117272da0>
在这里,我们通过列名的引用,从原始的DataFrame
组中选择了一个特定的Series
组。与GroupBy
对象一样,在我们调用对象上的聚合之前,不会进行任何计算:
planets.groupby('method')['orbital_period'].median()
'''
method
Astrometry 631.180000
Eclipse Timing Variations 4343.500000
Imaging 27500.000000
Microlensing 3300.000000
Orbital Brightness Modulation 0.342887
Pulsar Timing 66.541900
Pulsation Timing Variations 1170.000000
Radial Velocity 360.200000
Transit 5.714932
Transit Timing Variations 57.011000
Name: orbital_period, dtype: float64
'''
这给出了每种方法对其敏感的,轨道周期(以天为单位)的一般尺度的概念。
分组上的迭代
GroupBy
对象支持分组上的直接迭代,将每个组作为Series
或DataFrame
返回:
for (method, group) in planets.groupby('method'):
print("{0:30s} shape={1}".format(method, group.shape))
'''
Astrometry shape=(2, 6)
Eclipse Timing Variations shape=(9, 6)
Imaging shape=(38, 6)
Microlensing shape=(23, 6)
Orbital Brightness Modulation shape=(3, 6)
Pulsar Timing shape=(5, 6)
Pulsation Timing Variations shape=(1, 6)
Radial Velocity shape=(553, 6)
Transit shape=(397, 6)
Transit Timing Variations shape=(4, 6)
'''
这对于手动执行某些操作非常有用,尽管使用内置的apply
函数通常要快得多,我们之后将讨论这个函数。
分发方法
通过一些 Python 类魔术,任何未由GroupBy
对象显式实现的方法都将被传递给分组,并在它上面调用,无论它们是DataFrame
还是Series
对象。例如,你可以使用DataFrame
的describe()
方法,来执行一组聚合,它们描述数据中的每个分组:
planets.groupby('method')['year'].describe().unstack()
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
method | ||||||||
Astrometry | 2.0 | 2011.500000 | 2.121320 | 2010.0 | 2010.75 | 2011.5 | 2012.25 | 2013.0 |
Eclipse Timing Variations | 9.0 | 2010.000000 | 1.414214 | 2008.0 | 2009.00 | 2010.0 | 2011.00 | 2012.0 |
Imaging | 38.0 | 2009.131579 | 2.781901 | 2004.0 | 2008.00 | 2009.0 | 2011.00 | 2013.0 |
Microlensing | 23.0 | 2009.782609 | 2.859697 | 2004.0 | 2008.00 | 2010.0 | 2012.00 | 2013.0 |
Orbital Brightness Modulation | 3.0 | 2011.666667 | 1.154701 | 2011.0 | 2011.00 | 2011.0 | 2012.00 | 2013.0 |
Pulsar Timing | 5.0 | 1998.400000 | 8.384510 | 1992.0 | 1992.00 | 1994.0 | 2003.00 | 2011.0 |
Pulsation Timing Variations | 1.0 | 2007.000000 | NaN | 2007.0 | 2007.00 | 2007.0 | 2007.00 | 2007.0 |
Radial Velocity | 553.0 | 2007.518987 | 4.249052 | 1989.0 | 2005.00 | 2009.0 | 2011.00 | 2014.0 |
Transit | 397.0 | 2011.236776 | 2.077867 | 2002.0 | 2010.00 | 2012.0 | 2013.00 | 2014.0 |
Transit Timing Variations | 4.0 | 2012.500000 | 1.290994 | 2011.0 | 2011.75 | 2012.5 | 2013.25 | 2014.0 |
查看此表格有助于我们更好地理解数据:例如,绝大多数行星都是通过径向速度(Radial Velocity)和 Transit Method 发现的,尽管后者在过去十年中变得普遍(由于新的,更精确的望远镜)。最新的方法似乎是 Transit Timing Variation 和 Orbital Brightness Modulation,它们直到 2011 年才被用于发现新的行星。
这只是分发方法的一个例子。请注意,它们被应用于每个单独的分组,然后在`GroupBy
中组合并返回结果。同样,任何有效的DataFrame
或Series
方法都可以用在相应的GroupBy
对象上,这允许一些非常灵活和强大的操作!
聚合,过滤,转换,应用
前面的讨论主要关注组合操作的聚合,但还有更多选择。特别是GroupBy
对象有aggregate()
,filter()
,transform()
和apply()
方法,在组合分组数据之前,它们有效实现各种实用操作。
出于以下小节的目的,我们将使用这个DataFrame
:
rng = np.random.RandomState(0)
df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
'data1': range(6),
'data2': rng.randint(0, 10, 6)},
columns = ['key', 'data1', 'data2'])
df
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
聚合
我们现在熟悉GroupBy
聚合与`sum(),
median()等,但
aggregate()``方法允许更多的灵活性。它可以接受字符串,函数或其列表,并一次计算所有聚合。这是一个结合所有这些的快速示例:
df.groupby('key').aggregate(['min', np.median, max])
data1 | data2 | |||||
---|---|---|---|---|---|---|
min | median | max | min | median | max | |
key | ||||||
A | 0 | 1.5 | 3 | 3 | 4.0 | 5 |
B | 1 | 2.5 | 4 | 0 | 3.5 | 7 |
C | 2 | 3.5 | 5 | 3 | 6.0 | 9 |
另一个有用的方案是传递字典,将列名称映射到要应用于该列的操作:
df.groupby('key').aggregate({'data1': 'min',
'data2': 'max'})
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 7 |
C | 2 | 9 |
过滤
过滤操作允许你根据分组的属性来删除数据。 例如,我们可能希望保留标准差大于某个临界值的所有分组:
def filter_func(x):
return x['data2'].std() > 4
display('df', "df.groupby('key').std()", "df.groupby('key').filter(filter_func)")
df
:
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby('key').std()
:
data1 | data2 | |
---|---|---|
key | ||
A | 2.12132 | 1.414214 |
B | 2.12132 | 4.949747 |
C | 2.12132 | 4.242641 |
df.groupby('key').filter(filter_func)
:
key | data1 | data2 | |
---|---|---|---|
1 | B | 1 | 0 |
2 | C | 2 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
filter
函数应返回一个布尔值,指定组是否通过过滤。 这里因为组 A 没有大于 4 的标准差,所以从结果中删除它。
转换
虽然聚合必须返回数据的简化版本,但转换可以返回完整数据的某些重新组合的转换版本。对于这种变换,输出与输入的形状相同。一个常见的例子是通过减去分组均值来使数据居中:
df.groupby('key').transform(lambda x: x - x.mean())
data1 | data2 | |
---|---|---|
0 | -1.5 | 1.0 |
1 | -1.5 | -3.5 |
2 | -1.5 | -3.0 |
3 | 1.5 | -1.0 |
4 | 1.5 | 3.5 |
5 | 1.5 | 3.0 |
apply()
方法
apply()
方法允许你将任意函数应用于分组结果。该函数应该接受DataFrame
,并返回一个 Pandas 对象(例如,DataFrame
,Series
)或一个标量;组合操作将根据返回的输出类型进行调整。
例如,这里是一个apply()
,它按照第二列的总和将第一列标准化:
def norm_by_data2(x):
# x 是分组值的数据帧
x['data1'] /= x['data2'].sum()
return x
display('df', "df.groupby('key').apply(norm_by_data2)")
df
:
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby('key').apply(norm_by_data2)
:
key | data1 | data2 | |
---|---|---|---|
0 | A | 0.000000 | 5 |
1 | B | 0.142857 | 0 |
2 | C | 0.166667 | 3 |
3 | A | 0.375000 | 3 |
4 | B | 0.571429 | 7 |
5 | C | 0.416667 | 9 |
GroupBy
中的apply()
非常灵活:唯一的规则是,函数接受一个DataFrame
并返回一个 Pandas 对象或标量;在中间做什么取决于你!
指定分割键
在之前介绍的简单示例中,我们将DataFrame
拆分为单个列名。这只是定义分组的众多选项之一,我们将在此处介绍分组规则的其他选项。
提供分组键的列表,数组,系列或索引
键可以是任何序列或列表,其长度匹配DataFrame
的长度。例如:
L = [0, 1, 0, 1, 2, 0]
display('df', 'df.groupby(L).sum()')
df
:
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby(L).sum()
:
data1 | data2 | |
---|---|---|
0 | 7 | 17 |
1 | 4 | 3 |
2 | 4 | 7 |
当然,这意味着还有另一种更冗长的方式来完成之前的df.groupby('key')
:
display('df', "df.groupby(df['key']).sum()")
df
:
key | data1 | data2 | |
---|---|---|---|
0 | A | 0 | 5 |
1 | B | 1 | 0 |
2 | C | 2 | 3 |
3 | A | 3 | 3 |
4 | B | 4 | 7 |
5 | C | 5 | 9 |
df.groupby(df['key']).sum()
:
data1 | data2 | |
---|---|---|
key | ||
A | 3 | 8 |
B | 5 | 7 |
C | 7 | 12 |
将索引映射到分组的字典或序列
另一种方法是提供将索引值映射到分组键的字典:
df2 = df.set_index('key')
mapping = {'A': 'vowel', 'B': 'consonant', 'C': 'consonant'}
display('df2', 'df2.groupby(mapping).sum()')
df2
:
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 0 |
C | 2 | 3 |
A | 3 | 3 |
B | 4 | 7 |
C | 5 | 9 |
df2.groupby(mapping).sum()
:
data1 | data2 | |
---|---|---|
consonant | 12 | 19 |
vowel | 3 | 8 |
任何 Python 函数
与映射类似,你可以传递任何接受索引值并输出分组的 Python 函数:
display('df2', 'df2.groupby(str.lower).mean()')
df2
:
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 0 |
C | 2 | 3 |
A | 3 | 3 |
B | 4 | 7 |
C | 5 | 9 |
df2.groupby(str.lower).mean()
:
data1 | data2 | |
---|---|---|
a | 1.5 | 4.0 |
b | 2.5 | 3.5 |
c | 3.5 | 6.0 |
有效键的列表
此外,可以组合任何前面选择的键,来在多重索引上分组:
df2.groupby([str.lower, mapping]).mean()
data1 | data2 | ||
---|---|---|---|
a | vowel | 1.5 | 4.0 |
b | consonant | 2.5 | 3.5 |
c | consonant | 3.5 | 6.0 |
分组示例
作为一个例子,在几行 Python 代码中,我们可以将所有这些放在一起,并通过method
和decade
计算发现的行星:
decade = 10 * (planets['year'] // 10)
decade = decade.astype(str) + 's'
decade.name = 'decade'
planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
decade | 1980s | 1990s | 2000s | 2010s |
---|---|---|---|---|
method | ||||
Astrometry | 0.0 | 0.0 | 0.0 | 2.0 |
Eclipse Timing Variations | 0.0 | 0.0 | 5.0 | 10.0 |
Imaging | 0.0 | 0.0 | 29.0 | 21.0 |
Microlensing | 0.0 | 0.0 | 12.0 | 15.0 |
Orbital Brightness Modulation | 0.0 | 0.0 | 0.0 | 5.0 |
Pulsar Timing | 0.0 | 9.0 | 1.0 | 1.0 |
Pulsation Timing Variations | 0.0 | 0.0 | 1.0 | 0.0 |
Radial Velocity | 1.0 | 52.0 | 475.0 | 424.0 |
Transit | 0.0 | 0.0 | 64.0 | 712.0 |
Transit Timing Variations | 0.0 | 0.0 | 0.0 | 9.0 |
这展示了在查看真实数据集时,结合我们讨论过的许多操作的强大威力。我们立即大致了解,过去几十年内行星何时以及如何被发现!
在这里,我建议深入研究这几行代码,并评估各个步骤,来确保你准确了解它们对结果的作用。 这当然是一个有点复杂的例子,但理解这些部分将为你提供,探索自己的数据的类似方法。