累积与分组¶
分析大数据中一项基本工作是有效数据累计,计算累计指标如 sum(), mean(), median(), min(), 和 max(), 每一个指标都会表现出数据集的一些特征。在此我们应用Pandas的累计功能,并掌握 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 package
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的介绍中,有一些数据累计指标,如np.min(),np.var()等等。在Pandas中,这种累计功能依然存在相应的函数和方法,切用法与在Numpy中非常相似。
#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
对一个 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 参数值为'columns'时,可以对行进行操作。
df.mean(axis='columns')
0 0.088290 1 0.513997 2 0.849309 3 0.406727 4 0.444949 dtype: float64
#上述方法也可以用传统的方式来表示
df.mean(axis=1)
0 0.088290 1 0.513997 2 0.849309 3 0.406727 4 0.444949 dtype: float64
Pandas 中Series 和 DataFrame有Numpy中几乎所有的方法,但在实际中,可能使用 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 |
这是一种理解数据集统计属性的有效方法。从年份Year一列可以看出,从1989年首次发现外行星开始,到目前有一半的已知外行星都是在2010年以后几年里发现的。这得益于天文学界一项通过射电望远镜发现行星的计划——开普勒计划。
Pandas 内置的一些累计方法如下所示:
| 累计方法 | 描述 |
|---|---|
count() |
元素个数 |
first(), last() |
第一项与最后一项 |
mean(), median() |
均值与中位数 |
min(), max() |
最大值和最小值 |
std(), var() |
标准差与方差 |
mad() |
绝对偏差均值 |
prod() |
所有元素乘积 |
sum() |
所有团苏加和 |
Pandas的 DataFrame 和 Series 对象支持以上所有方法。
如果想深入了解数据信息,仅仅使用简单累计函数和方法可能是不够的。在数据累计中另一个利器是使用 groupby 操作,它可以快速、有效计算各个子集的相应累计特征。
GroupBy: 分割、应用和组合¶
虽然简单累计方法可以让我们对数据有一个笼统的认识,但我们仍然需要对某些标签或者索引进行局部分析,这这时就需要使用groupby方法进行操作。Groupby的涵义是分组,这种名称虽然来源于SQL语言,但整个groupby方法的思路却是来源于R社区中最重要的Frame创始人Hadley Wickman,他在分割(split)、应用(apply)和组合(combine)的流程理解更好诠释了groupby的功能逻辑。
分割, 应用, 组合¶
一个演示性的关于分割-应用-组合的例子,这里的应用部分用求和方法来表示。整个逻辑过程见下图:

本例清晰描述了groupby 完成的全过程:
- 分割 步骤将
DataFrame根据指定的键分割成若干组。 - 应用 步骤将每个组应用某种累计函数,通常是累计、转换或者其他过滤函数。
- 组合 步骤将每一组的应用结果汇总并重新输出为相应的数组。
以上面的案例为样本,从创建DataFrame开始,来分析和实践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 |
df.groupby('key')
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001F63E0128A0>
这时候,系统返回的不是一个 DataFrame对象,而是一个 DataFrameGroupBy对象。这个对象的独特之处在于不会显式呈现出结果,而是一种特殊形式的DataFrame,里面掩藏着许多分组数据,但在没有执行应用计算之前,不会产生计算。这种“延迟计算”设计使得大多数累计操作可以通过一种对用户透明的方式高效完成,几乎感觉不到分组过程的存在。
为了得到显式结果,我们对 DataFrameGroupBy 对象进行加总运算,这样可以得到明确和显式的运算结果:
df.groupby('key').sum()
| data | |
|---|---|
| key | |
| A | 3 |
| B | 5 |
| C | 7 |
本例中应用的 sum()方法仅仅是一种示例方法,你可以使用 Pandas 或者 NumPy 累计函数方法中的任何一个。
按照列进行取值索引¶
GroupBy 对象与 DataFrame对象一样,支持按照列进行索引,而后返回一个调整后的 GroupBy 对象。例如:
planets.groupby('method')
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001F63F8F23C0>
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类的魔力(@classmethod),可以让任何不由groupby直接实现的方法,在每一组中得到应用。对于 DataFrame 和 Series 对象都适用。例如,在应用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 |
这些表帮助我们对数据有更深入的认识,例如,绝大多数行星都是通过Radio Velocity方法发现的,其次时Transit方法。后者在最近十年非常普遍。还有有些方法直到最近才开始发挥作用。
这只是Pandas调用方法示例之一。在这种情况下,方法会应用在每组数据之上,结果由groupby组合以后返回。另外,任意DataFrame和Series方法都可以由groupby方法调用,从而实现灵活强大操作。
累计,过滤,转换与应用¶
之前我们主要讨论在组合操作之前的累计加总过程,在实际中还有其他选项,如在groupby对象还有 aggregate(), filter(), transform(), 和apply() 方法,可以有效地在组合分组数据之前,进行其他计算。
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 |
Aggregation¶
我们现在对 GroupBy 累计过程中使用sum(), median()等方法比较熟悉,然而 aggregate() 方法可以是我们的操作更加具有灵活性。它可以在字符串、函数或者一个列表中应用,并立刻完成累计运算。
df.groupby('key').aggregate(['min', '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)或者是一个标量。 组合方法会适应返回结果类型
下面例子是使用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,include_groups=False)")
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,include_groups=False)
| data1 | data2 | ||
|---|---|---|---|
| key | |||
| A | 0 | 0.000000 | 5 |
| 3 | 0.375000 | 3 | |
| B | 1 | 0.142857 | 0 |
| 4 | 0.571429 | 7 | |
| C | 2 | 0.166667 | 3 |
| 5 | 0.416667 | 9 |
display('df', "df.groupby('key').apply(norm_by_data2,include_groups=False)")#pandas更新前版本
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,include_groups=False)
| data1 | data2 | ||
|---|---|---|---|
| key | |||
| A | 0 | 0.000000 | 5 |
| 3 | 0.375000 | 3 | |
| B | 1 | 0.142857 | 0 |
| 4 | 0.571429 | 7 | |
| C | 2 | 0.166667 | 3 |
| 5 | 0.416667 | 9 |
apply() 方法在 GroupBy调用过程中非常灵活,唯一需要注意的是它总是输入分组数据的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()
| key | data1 | data2 | |
|---|---|---|---|
| 0 | ACC | 7 | 17 |
| 1 | BA | 4 | 3 |
| 2 | B | 4 | 7 |
因此,如果使用key作为对象,可以用 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 |
用字典或者Series将索引映射到分组名称上¶
另一种方法是对一个字典对象进行groupby操作,可以用键的名称分组,用值的内容作为分组名称:
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 | |
|---|---|---|
| key | ||
| 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 | |
|---|---|---|
| key | ||
| a | 1.5 | 4.0 |
| b | 2.5 | 3.5 |
| c | 3.5 | 6.0 |
有效的键值组成的列表¶
另外,任意之前有效的键值都可以组合起来进行分组,返回一个多级索引分组结果。
df2.groupby([str.lower, mapping]).mean()
| data1 | data2 | ||
|---|---|---|---|
| key | key | ||
| a | vowel | 1.5 | 4.0 |
| b | consonant | 2.5 | 3.5 |
| c | consonant | 3.5 | 6.0 |
分组案例¶
通过下面几行代码,我们可以运用分组的知识,在行星数据库中获取不同发现行星的方法和不同年份发现的行星数量。
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 |
这些都反应了在探索数据集内部关联性方面,分组技术所具有的快速组合多种操作的能力——只需要寥寥几行代码,然我们对过去几十年里,不同时代的行星发现情况有了一个大概了解。
请大家花点时间认真分析这几行代码,确保自己真正理解每一行代码对结果所产生怎样的影响。本例虽然有些专业,但是理解这几行代码含义可以帮助你掌握分析类似数据的方法。