3.9 累计与分组
在对较大的数据进行分析时,一项基本工作就是进行有效的数据积累,计算积累指标,如和、平均值、中值、最值等,其中每个指标都呈现了大数据集的特征。pd有累计功能。
3.9.1 行星数据
通过网上seaborn类提供的行星数据进行各种演示:
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)
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 |
数据包括了1000多个外行星的数据。
3.9.2 Pandas的简单累计功能
与一维np数组相同,pd的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.811925491708157
ser.mean()
0.5623850983416314
DF的累计函数默认对每列进行统计:
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
pd的Series和DF支持所有2.4节中介绍的常用累计函数。另外,还有一个非常方便的describe方法可以计算每列的若干常用统计值。
对于行星数据,先丢弃有缺失值的行,再用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 |
这是一种理解数据集所有统计属性的有效方法。如,从年份看,1989年首先发现外行星,而且一半的一只外行星都是在2010年及以后发现的。
pd内置的一些累计方法如下:
方法 | 描述 |
---|---|
count() | 计数项 |
first() , last() | 第一项与最后一项 |
mean() , median() | 均值与中位数 |
min() , max() | 最小值与最大值 |
std() , var() | 标准差与方差 |
mad() | 均值绝对偏差 |
prod() | 所有项乘积 |
sum() | 所有项求和 |
Series和DF对象支持以上所有方法。但若想深入理解数据,仅仅依靠累计函数是不够的,数据累计下一级别是groupby分组操作,其可快速有效地计算数据各个子集的累计值。
3.9.3 GroupBy:分割、应用和组合
分割、应用和组合
这一系列操作是:将数据分割为小组——每组应用特定的操作进行数据处理——将结果组合为最终输出的值。而内部过程对用户是透明的,只需要一行命令即可。
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 |
groupby方法可以完成绝大多数常见的分割应用组合操作,将需要分组的列名作为参数传入即可:
df.groupby('key')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x09AADAB0>
注意,这里的返回值不是DF对象,而是DFGroupBy对象:其可看成特殊的DF,内部隐藏着若干组数据,但在没有应用累计函数之前不会计算。这种封装便于用户使用。
为得到结果,可对DFGB对象应用累计函数,算出结果:
df.groupby('key').sum()
data | |
---|---|
key | |
A | 3 |
B | 5 |
C | 7 |
df.groupby('key').sum().describe()
data | |
---|---|
count | 3.0 |
mean | 5.0 |
std | 2.0 |
min | 3.0 |
25% | 4.0 |
50% | 5.0 |
75% | 6.0 |
max | 7.0 |
df.groupby('key').describe()
data | ||||||||
---|---|---|---|---|---|---|---|---|
count | mean | std | min | 25% | 50% | 75% | max | |
key | ||||||||
A | 2.0 | 1.5 | 2.12132 | 0.0 | 0.75 | 1.5 | 2.25 | 3.0 |
B | 2.0 | 2.5 | 2.12132 | 1.0 | 1.75 | 2.5 | 3.25 | 4.0 |
C | 2.0 | 3.5 | 2.12132 | 2.0 | 2.75 | 3.5 | 4.25 | 5.0 |
GroupBy对象
是一种非常灵活的抽象类型。在大多数场景中,可将其看成是DF的集合,在底层结果所有难题。
用行星数据作为演示GB对象的基本操作:
按列取值
GB对象与DF对象一样,也支持按列取值,饼返回一个修改过的GB对象,如:
planets.groupby('method')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x09AAD050>
planets.groupby('method')['orbital_period']
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x09ACA9B0>
第一句命令将DF按照method数据列进行分组;第二句命令将分组的DF数据中取出名为orbital_period的一列数据。所以返回的是SGB对象。
这里从原来的DF中取某个列名作为一个Series组。与GB对象一样,直到运行累计函数,才会开始计算:
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
这样就可以获得不同方法method下所有行星公转周期orbital_period(按天计算)的中位数。
按组迭代
GB对象支持直接按组进行迭代,返回的每一组都是Series或DF:
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类的魔力(@classmethod),可以让任何不由GB对象直接实现的方法直接应用到每一组,无论是DF还是Series对象都同样适用。例如可用DF的describe方法进行累计,对每一组数据进行描述性统计:
planets.groupby('method')['year'].describe()
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 方法发现的,而且后者在近十年变得越来越普遍,最新的 Transit Timing Variation 和 Orbital Brightness Modulation 方法在2011年之后才有新发现。
这只是演示pd调用方法的示例之一。方法首先会应用到每组数据上,然后结果由GroupBy组合后返回。另外,任意DF或Series的方法都可由GB方法调用,从而实现非常灵活强大的操作。
累计、过滤、转换和应用
虽然前面的章节只重点介绍了组合操作,但是还有许多操作没有介绍,尤其是 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 |
(1) 累计。我们目前比较熟悉的 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 |
另一种用法就是通过 Python 字典指定不同列需要累计的函数:
df.groupby('key').aggregate({'data1': 'min', 'data2': 'max'})
data1 | data2 | |
---|---|---|
key | ||
A | 0 | 5 |
B | 1 | 7 |
C | 2 | 9 |
(2) 过滤。过滤操作可以让你按照分组的属性丢弃若干数据。例如,我们可能只需要保留标准差超过某个阈值的组:
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_func () 函数会返回一个布尔值,表示每个组是否通过过滤。由于 A 组 'data2' 列的标准差不大于 4,所以被丢弃了。
(3) 转换。累计操作返回的是对组内全量数据缩减过的结果,而转换操作会返回一个新的全量数据。数据经过转换之后,其形状与原来的输入数据是一样的。常见的例子就是将每一组的样本数据减去各组的均值,实现数据标准化:
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 |
(4) apply() 方法。apply() 方法让你可以在每个组上应用任意方法。这个函数输入一个 DataFrame,返回一个 Pandas 对象(DataFrame 或 Series)或一个标量(scalar,单个数值)。组合操作会适应返回结果类型。
下面的例子就是用 apply() 方法将第一列数据以第二列的和为基数进行标准化:
def norm_by_data2(x): # x is a DataFrame of group values 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。这只是众多分组操作中的一种,下面将继续介绍更多的分组方法。
(1) 将列表、数组、Series 或索引作为分组键。
分组键可以是长度与 DataFrame 匹配的任意 Series 或列表,例如:
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 |
(2) 用字典或 Series 将索引映射到分组名称。
另一种方法是提供一个字典,将索引映射到分组键:
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 |
(3) 任意 Python 函数。
与前面的字典映射类似,你可以将任意 Python 函数传入 groupby,函数映射到索引,然后新的分组输出:
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 |
(4) 多个有效键构成的列表。
此外,任意之前有效的键都可以组合起来进行分组,从而返回一个多级索引的分组结果:
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 代码,我们就可以运用上述知识,获取不同方法和不同年份发现的行星数量:
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 |
此例足以展现 GroupBy 在探索真实数据集时快速组合多种操作的能力——只用寥寥几行代码,就可以让我们立即对过去几十年里不同年代的行星发现方法有一个大概的了解。 我建议你花点时间分析这几行代码,确保自己真正理解了每一行代码对结果产生了怎样的影响。虽然这个例子的确有点儿复杂,但是理解这几行代码的含义可以帮你掌握分析类似数据的方法。