3.9 累计与分组

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()
 methodnumberorbital_periodmassdistanceyear
0Radial Velocity1269.3007.1077.402006
1Radial Velocity1874.7742.2156.952008
2Radial Velocity1763.0002.6019.842011
3Radial Velocity1326.03019.40110.622007
4Radial Velocity1516.22010.50119.472009

数据包括了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
 AB
00.1559950.020584
10.0580840.969910
20.8661760.832443
30.6011150.212339
40.7080730.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()
 numberorbital_periodmassdistanceyear
count498.00000498.000000498.000000498.000000498.000000
mean1.73494835.7786712.50932052.0682132007.377510
std1.175721469.1282593.63627446.5960414.167284
min1.000001.3283000.0036001.3500001989.000000
25%1.0000038.2722500.21250024.4975002005.000000
50%1.00000357.0000001.24500039.9400002009.000000
75%2.00000999.6000002.86750059.3325002011.000000
max6.0000017337.50000025.000000354.0000002014.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
 keydata
0A0
1B1
2C2
3A3
4B4
5C5

groupby方法可以完成绝大多数常见的分割应用组合操作,将需要分组的列名作为参数传入即可:

df.groupby('key')
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x09AADAB0>

注意,这里的返回值不是DF对象,而是DFGroupBy对象:其可看成特殊的DF,内部隐藏着若干组数据,但在没有应用累计函数之前不会计算。这种封装便于用户使用。

为得到结果,可对DFGB对象应用累计函数,算出结果:

df.groupby('key').sum()
 data
key 
A3
B5
C7
df.groupby('key').sum().describe()
 data
count3.0
mean5.0
std2.0
min3.0
25%4.0
50%5.0
75%6.0
max7.0
df.groupby('key').describe()
 data
 countmeanstdmin25%50%75%max
key        
A2.01.52.121320.00.751.52.253.0
B2.02.52.121321.01.752.53.254.0
C2.03.52.121322.02.753.54.255.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()
 countmeanstdmin25%50%75%max
method        
Astrometry2.02011.5000002.1213202010.02010.752011.52012.252013.0
Eclipse Timing Variations9.02010.0000001.4142142008.02009.002010.02011.002012.0
Imaging38.02009.1315792.7819012004.02008.002009.02011.002013.0
Microlensing23.02009.7826092.8596972004.02008.002010.02012.002013.0
Orbital Brightness Modulation3.02011.6666671.1547012011.02011.002011.02012.002013.0
Pulsar Timing5.01998.4000008.3845101992.01992.001994.02003.002011.0
Pulsation Timing Variations1.02007.000000NaN2007.02007.002007.02007.002007.0
Radial Velocity553.02007.5189874.2490521989.02005.002009.02011.002014.0
Transit397.02011.2367762.0778672002.02010.002012.02013.002014.0
Transit Timing Variations4.02012.5000001.2909942011.02011.752012.52013.252014.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
 keydata1data2
0A05
1B10
2C23
3A33
4B47
5C59

(1) 累计。我们目前比较熟悉的 GroupBy 累计方法只有 sum() 和 median() 之类的简单函数,但是 aggregate() 其实可以支持更复杂的操作,比如字符串、函数或者函数列表,并且能一次性计算所有累计值。下面来快速演示一个例子:

df.groupby('key').aggregate(['min', np.median, max])
 data1data2
 minmedianmaxminmedianmax
key      
A01.5334.05
B12.5403.57
C23.5536.09

另一种用法就是通过 Python 字典指定不同列需要累计的函数:

df.groupby('key').aggregate({'data1': 'min',
                             'data2': 'max'})
 data1data2
key  
A05
B17
C29

(2) 过滤。过滤操作可以让你按照分组的属性丢弃若干数据。例如,我们可能只需要保留标准差超过某个阈值的组:

def filter_func(x):
    return x['data2'].std() > 4

display('df', "df.groupby('key').std()", "df.groupby('key').filter(filter_func)")

df

 keydata1data2
0A05
1B10
2C23
3A33
4B47
5C59

df.groupby('key').std()

 data1data2
key  
A2.121321.414214
B2.121324.949747
C2.121324.242641

df.groupby('key').filter(filter_func)

 keydata1data2
1B10
2C23
4B47
5C59

filter_func () 函数会返回一个布尔值,表示每个组是否通过过滤。由于 A 组 'data2' 列的标准差不大于 4,所以被丢弃了。

(3) 转换。累计操作返回的是对组内全量数据缩减过的结果,而转换操作会返回一个新的全量数据。数据经过转换之后,其形状与原来的输入数据是一样的。常见的例子就是将每一组的样本数据减去各组的均值,实现数据标准化:

df.groupby('key').transform(lambda x: x - x.mean())
 data1data2
0-1.51.0
1-1.5-3.5
2-1.5-3.0
31.5-1.0
41.53.5
51.53.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

 keydata1data2
0A05
1B10
2C23
3A33
4B47
5C59

df.groupby('key').apply(norm_by_data2)

 keydata1data2
0A0.0000005
1B0.1428570
2C0.1666673
3A0.3750003
4B0.5714297
5C0.4166679

GroupBy 里的 apply() 方法非常灵活,唯一需要注意的地方是它总是输入分组数据的 DataFrame,返回 Pandas 对象或标量。具体如何选择需要视情况而定。

设置分割的键

前面的简单例子一直在用列名分割 DataFrame。这只是众多分组操作中的一种,下面将继续介绍更多的分组方法。

(1) 将列表、数组、Series 或索引作为分组键。

分组键可以是长度与 DataFrame 匹配的任意 Series 或列表,例如:

L = [0, 1, 0, 1, 2, 0]
display('df', 'df.groupby(L).sum()')

df

 keydata1data2
0A05
1B10
2C23
3A33
4B47
5C59

df.groupby(L).sum()

 data1data2
0717
143
247

因此,还有一种比前面直接用列名更啰嗦的表示df.groupby('key') 的方法:

display('df', "df.groupby(df['key']).sum()")

df

 keydata1data2
0A05
1B10
2C23
3A33
4B47
5C59

df.groupby(df['key']).sum()

 data1data2
key  
A38
B57
C712

(2) 用字典或 Series 将索引映射到分组名称。

另一种方法是提供一个字典,将索引映射到分组键:

df2 = df.set_index('key')
mapping = {'A': 'vowel', 'B': 'consonant', 'C': 'consonant'}
display('df2', 'df2.groupby(mapping).sum()')

df2

 data1data2
key  
A05
B10
C23
A33
B47
C59

df2.groupby(mapping).sum()

 data1data2
consonant1219
vowel38

(3) 任意 Python 函数。

与前面的字典映射类似,你可以将任意 Python 函数传入 groupby,函数映射到索引,然后新的分组输出:

display('df2', 'df2.groupby(str.lower).mean()')

df2

 data1data2
key  
A05
B10
C23
A33
B47
C59

df2.groupby(str.lower).mean()

 data1data2
a1.54.0
b2.53.5
c3.56.0

(4) 多个有效键构成的列表。

此外,任意之前有效的键都可以组合起来进行分组,从而返回一个多级索引的分组结果:

df2.groupby([str.lower, mapping]).mean()
  data1data2
avowel1.54.0
bconsonant2.53.5
cconsonant3.56.0

分组案例

通过下例中的几行 Python 代码,我们就可以运用上述知识,获取不同方法和不同年份发现的行星数量:

decade = 10 * (planets['year'] // 10)
decade = decade.astype(str) + 's'
decade.name = 'decade'
planets.groupby(['method', decade])['number'].sum().unstack().fillna(0)
decade1980s1990s2000s2010s
method    
Astrometry0.00.00.02.0
Eclipse Timing Variations0.00.05.010.0
Imaging0.00.029.021.0
Microlensing0.00.012.015.0
Orbital Brightness Modulation0.00.00.05.0
Pulsar Timing0.09.01.01.0
Pulsation Timing Variations0.00.01.00.0
Radial Velocity1.052.0475.0424.0
Transit0.00.064.0712.0
Transit Timing Variations0.00.00.09.0

此例足以展现 GroupBy 在探索真实数据集时快速组合多种操作的能力——只用寥寥几行代码,就可以让我们立即对过去几十年里不同年代的行星发现方法有一个大概的了解。 我建议你花点时间分析这几行代码,确保自己真正理解了每一行代码对结果产生了怎样的影响。虽然这个例子的确有点儿复杂,但是理解这几行代码的含义可以帮你掌握分析类似数据的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值