8.聚合与分组

大数据分析的一个必要部分是高效的概况:计算聚合值sum(),mean(),median(),min()和max(),这些单个数值可以透视出潜在大数据集的特性。在这部分,我们将会探索Pandas中的聚合,从类似于NumPy数据中的那些简单的操作,到基于分组概念的复杂操作。

为了方便,我们将使用之前章节用到的display函数:

import numpy as np
import pandas as pd

class display(object):
    """Display HTML representation of multiple objects"""
    template = """

{0}

{1}
""" 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 package (see Visualization With 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发现的1000多太阳系外行星的详细信息。

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的SeriesDataFrame包括所有在Aggregations: Min, Max, and Everything In Between中提到的聚合手段;另外,还有一个方便的方法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内建的聚合方法:

Aggregation Description
count() Total number of items
first(), last() First and last item
mean(), median() Mean and median
min(), max() Minimum and maximum
std(), var() Standard deviation and variance
mad() Mean absolute deviation
prod() Product of all items
sum() Sum of all items

它们都是DataFrameSeries的对象方法。

但是为了更深的挖掘数据,简单的聚合经常是不够的。下一层次的数据整理是groupby操作,它可以让你快速有效的计算数据子集上的聚合值。

GroupBy:拆分,应用,合并。

简单聚合能给你数据集的一点味道,但通常我们更倾向于有条件的在一些标签或索引上进行聚合:这是通过叫做groupby的操作实现的。“group by”的名称来自于SQL数据库语言的命令,但认为是从Hadley Wickham of Rstats的名言:拆分,应用,合并,或许更具启发性。

拆分,应用,合并

拆分-应用-合并操作的经典例子,这里“应用”是一个求和聚合,可以由这个图片来说明:

[图片上传失败...(image-186b9e-1535446906132)]
figure source in Appendix

它把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')

注意返回值不是DataFrame集合,而是一个DataFrameGroupBy对象。这个对象奇妙的地方是:你可以认为它是一种特殊的DataFrame视图,它已经准备好分组当并不进行实际计算,直到聚合应用时才真正的计算。这种延迟求值方法意味着普通的聚合操作可以以用户透明的方式高效实现。
为了计算结果,我们可以对DataFrameGroupBy对象应用一种聚合方法,它将会执行适当的应用/合并步骤来产生期望的结果:

df.groupby('key').sum()
    data
key 
A   3
B   5
C   7

这里sum()方法只是一个例子你;如你在随后的讨论种可见,几乎可以应用所有Pandas或NumPy聚合函数,以及所有有效的DataFrame操作。

GroupBy对象

GroupBy对象非常灵活抽象。在许多方面,你可以简单的将它看成是DataFrame的集合,并且它在底层做了一些复杂的事情。让我们看些使用行星数据的例子。
也许GroupBy提供的最重要操作是:聚合,过滤,转换和应用。我们将在 "Aggregate, Filter, Transform, Apply"进行更多的讨论,在此之前让我们介绍一些其他可以用于GroupBy基本操作的功能。

列检索

GroupBy对象支持同DataFrame一样的列检索操作,并且返回一个修改过的GroupBy对象。例如:

planets.groupby('method')

planets.groupby('method')['orbital_period']

这里,通过引用它的列名,我们从原始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方法发现的,尽管后者在最近10年变得很普通(由于新的,更精确的望远镜)。最新的方法看起来是Transit Timing Variation和 Orbital Brightness Modulation,它们直到2011年才开始用于发现新行星。
这只是利用派遣方法的一个例子。注意,它们被用在每个单独的组上,结果在GroupBy内部合并,并返回。再次强调,任何合法的DataFrame/Series方法都可以用在对应的GroupBy对象上,它可以允许一些灵活并且强大的操作!

聚合,过滤,转换,应用

前面的讨论集中在聚合合并操作上,但实际上由更多可用的选项。特别的,GroupBy对象有 aggregate(),filter(),transform(), 和apply()方法,它们可以在合并分组数据前,高效的执行许多有用的操作。
为了随后子章节的目的,我们使用这个DATa Frame:

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

过滤函数应该返回一个布尔值来表明组是否通过过滤条件。这里因为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)或者一个标量;组合操作将被调整为输出返回的类型。

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。这只是分组定义了的许多选项中的一个,我们将会浏览分组规范的其他选项。

使用列表,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

当然,这意味着有其他,更多样的方法来提供groupby 的键值:

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

字典或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

任何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代码就可以把所有这些集中在一起,并且统计方法和每10年发现的行星数目:

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

当使用实际数据集时,这显示出集合许多操作的威力。我们可以立即对过去70年间行星被发现的时间和方法有大概的了解!
这里我建议仔细研究这几行代码,并且评估每一步确保你真正的理解它们对结果起了怎样的作用。这当然是较复杂的一个例子,理解这些代码将会给你在自己探索数据是提供一些手段。

你可能感兴趣的:(8.聚合与分组)