python数据分组运算
摘要: pandas 的 GroupBy 功能可以方便地对数据进行分组、应用函数、转换和聚合等操作。 # 原作者:lionets
GroupBy
分组运算有时也被称为 “split-apply-combine” 操作。其中的 “split” 便是借由 obj.groupby()
方法来实现的。
.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False)
方法作用于一条轴向上,并接受一个分组键(by)参数来给调用者分组。分组键可以是Series 或列表,要求其长度与待分组的轴一致;也可以是映射函数、字典甚至数组的某条列名(字符串),但这些参数类型都只是快捷方式,其最终仍要用于生成一组用于拆分对象的值。
lang:python
>>> df = DataFrame({'key1':['a','a','b','b','a'],
'key2':['one','two','one','two','one'],
'data1':np.random.randn(5),
'data2':np.random.randn(5)})
>>> df
data1 data2 key1 key2
0 0.922269 0.110285 a one
1 -0.181773 1.022435 a two
2 0.635899 0.279316 b one
3 0.527926 0.482807 b two
4 -1.586040 -1.312042 a one
[5 rows x 4 columns]
>>> grouped = df.groupby(df['key1'])
>>> grouped
<pandas.core.groupby.DataFrameGroupBy object at 0x0000000005BC25F8>
这里使用 df['key1']
做了分组键,即按 a 和 b 进行分组。但实际分组键并不需要与数组对象之间存在联系,只要长度相同即可,使用数组的列只是图方便。上例中如果使用 [1,1,2,2,3]
这样的列表做分组键的话,结果与df['key1']
是相同的。
groupby 方法返回的 DataFrameGroupBy 对象实际并不包含数据内容,它记录的是有关分组键——df['key1']
的中间数据。当你对分组数据应用函数或其他聚合运算时,pandas 再依据 groupby 对象内记录的信息对 df 进行快速分块运算,并返回结果。
上面这段话其实想说是: groupby 方法的调用本身并不涉及运算,因此速度很快。而在操作这个 grouped 对象的时候,还是将其看成一个保存了实际数据的对象比较方便。比如我们可以直接对其应用很多方法,或索引切片:
lang:python
>>> grouped.mean()
data1 data2
key1
a -0.281848 -0.059774
b 0.581912 0.381061
[2 rows x 2 columns]
上例中没有显示 key2
列,是因为其值不是数字类型,被 mean() 方法自动忽视了。当想要只看某一(些)列的时候,可以通过索引来实现,在 groupby 方法调用前后均可(这是一种语法糖):
lang:python
>>> df['data1'].groupby(df['key1']).mean()
key1
a -0.281848
b 0.581912
dtype: float64
>>> df.groupby(df['key2'])['data2'].mean()
key2
one -0.307481
two 0.752621
Name: data2, dtype: float64
如果分组键使用的是多个数组,就会得到一个层次化索引的结果:
lang:python
>>> df.groupby([df['key1'],df['key2']]).mean()
data1 data2
key1 key2
a one -0.331885 -0.600879
two -0.181773 1.022435
b one 0.635899 0.279316
two 0.527926 0.482807
[4 rows x 2 columns]
最后,可以使用 GroupBy 对象(不论是 DataFrameGroupBy 还是 SeriesGroupBy)的 .size()
方法查看分组大小:
lang:python
>>> grouped.size()
key1
a 3
b 2
dtype: int64
<br />
对分组进行迭代
GroupBy 对象是可以通过 for 循环迭代的,可以产生一组二元组,分别为分组名和组内数据。下面是一个多重分组键的情况:
lang:python
>>> for i,j in df.groupby([df['key1'],df['key2']]):
print(i)
print('-----------')
print(j)
('a', 'one')
-----------
data1 data2 key1 key2
0 0.922269 0.110285 a one
4 -1.586040 -1.312042 a one
[2 rows x 4 columns]
('a', 'two')
-----------
data1 data2 key1 key2
1 -0.181773 1.022435 a two
[1 rows x 4 columns]
('b', 'one')
-----------
data1 data2 key1 key2
2 0.635899 0.279316 b one
[1 rows x 4 columns]
('b', 'two')
-----------
data1 data2 key1 key2
3 0.527926 0.482807 b two
[1 rows x 4 columns]
<br />
使用字符串列名作分组键
前面曾提到过可以使用字符串形式的列名作为分组键,但上面例子中都没有用。是因为这种方法虽然方便,却存在隐患——使用这种方法时,调用者必须是 DataFrame 对象自身而不可以是 DataFrame 的索引形式。即df.groupby('key1')['data1']
是 ok 的,但 df['data1'].groupby('key1')
会报错。使用时当注意区分。 <br />
使用 字典或Series作分组键
这两种参数需要提供一种从行(列)名到组名的映射关系。(还记得 Series 就是一种定长有序字典 这种说法嘛)
lang:python
>>> df.groupby({0:'a',1:'a',2:'b',3:'b',4:'a'}).mean()
data1 data2
a -0.281848 -0.059774
b 0.581912 0.381061
[2 rows x 2 columns]
<br />
通过函数进行分组
函数的作用有些类似于字典,或者说这些奇怪的分组键都类似于字典——利用某种映射关系将待分组的轴转化为一个等长的由分组名组成的序列。
如果说行列名是作为索引传递给字典以获取组名的话,那么在函数分组键中,行列名就会作为参数传递给函数。这便是你需要提供的函数类型:
lang:python
>>> df.groupby(lambda x:'even' if x%2==0 else 'odd').mean()
data1 data2
even -0.009290 -0.307481
odd 0.173076 0.752621
[2 rows x 2 columns]
<br />
根据索引级别分组
当根据高级别索引来分组的时候,参数就不再是 by=None
了,而要换成 level=None
,值可以是索引级别的编号或名称:
lang:python
>>> index = pd.MultiIndex.from_arrays([['even','odd','even','odd','even'],
[0,1,2,3,4]],names=['a','b'])
>>> df.index = index
>>> df.groupby(level='a').mean()
data1 data2
a
even -0.009290 -0.307481
odd 0.173076 0.752621
[2 rows x 2 columns]
>>> df.groupby(level=0).mean()
data1 data2
a
even -0.009290 -0.307481
odd 0.173076 0.752621
[2 rows x 2 columns]
<br />
数据聚合(Aggregation)
数据聚合,指的是任何能够从数组产生标量值的数据转换过程。你也可以简单地将其理解为统计计算,如 mean(), sum(), max() 等。
数据聚合本身与分组并没有直接关系,在任何一列(行)或全部列(行)上都可以进行。不过当这种运算被应用在分组数据上的时候,结果可能会变得更有意义。
对于 GroupBy 对象可以应用的聚合运算包括:
- 已经内置的方法,如 sum(), mean() 等
- Series 的方法,如 quantile() 等
- 自定义的聚合函数,通过传入
GroupBy.aggregate()
或GroupBy.agg()
来实现
其中自定义函数的参数应当为一个数组类型,即 GroupBy 对象迭代出的元组的第二个元素。如
lang:python
>>> df.groupby('key1')['data1','data2'].agg(lambda arr:arr.max()-arr.min())
data1 data2
key1
a 2.508309 2.334477
b 0.107973 0.203492
[2 rows x 2 columns]
但其实自定义函数的效率很慢,远不如 GroupBy 对象已经优化过的内建方法,这些方法包括: <br />
<table style="font-size:14px"> <tr> <td>############</td> <td>**</td> </tr> <tr> <td>count</td> <td>分组中非 NA 值得数量</td> </tr> <tr> <td>sum</td> <td>非 NA 值的和</td> </tr> <tr> <td>mean</td> <td>非 NA 值的平均值</td> </tr> <tr> <td>median</td> <td>非 NA 值的算数中位数</td> </tr> <tr> <td>std, var</td> <td>无偏(分母为 n-1)标准差和方差</td> </tr> <tr> <td>min, max</td> <td>非 NA 值的最小值和最大值</td> </tr> <tr> <td>prod</td> <td>非 NA 值的积</td> </tr> <tr> <td>first, last</td> <td>第一个和最后一个非 NA 值</td> </tr> </table> <br />
面向列的多函数应用
前面的例子中,我们每次都只调用一个聚合方法。对于多函数应用,我们可以分两种情况讨论:
第一种是相同列应用多个函数从而得到多个结果的情况,这时只需给 agg() 传入一个函数列表即可:
lang:python
>>> df.groupby('key1')['data1','data2'].agg(['min','max'])
data1 data2
min max min max
key1
a -1.586040 0.922269 -1.312042 1.022435
b 0.527926 0.635899 0.279316 0.482807
[2 rows x 4 columns]
这里一个技巧是,对上节中那些统计方法,可以将方法名以字符串的形式传入 agg()。另外,如果你不喜欢列的命名方式,或你使用的干脆是 lambda 匿名函数,你可以把函数参数替换成(name,function)的元组格式,这样结果集中的列就不再以函数名命名而是以你给出的 name 为准。
第二种是对不同列应用不同函数的情况,这时需要传给 agg() 一个从列名映射到函数名的字典:
lang:python
>>> df.groupby('key1').agg({'data1':'min','data2':'max'})
data1 data2
key1
a -1.586040 1.022435
b 0.527926 0.482807
[2 rows x 2 columns]
这里要注意的是,就不要再在 GroupBy 对象上进行索引操作啦,你的字典参数已经做了响应的列选取工作。 <br />
分组级运算和转换
聚合只是分组运算的一种,更多种类的分组运算可以通过 .transform()
和 apply()
方法实现。 <br />
transform
前面进行聚合运算的时候,得到的结果是一个以分组名为 index 的结果对象。如果我们想使用原数组的 index 的话,就需要进行 merge 转换。transform(func, *args, **kwargs)
方法简化了这个过程,它会把 func 参数应用到所有分组,然后把结果放置到原数组的 index 上(如果结果是一个标量,就进行广播):
lang:python
>>> df
data1 data2 key1 key2
a b
even 0 0.922269 0.110285 a one
odd 1 -0.181773 1.022435 a two
even 2 0.635899 0.279316 b one
odd 3 0.527926 0.482807 b two
even 4 -1.586040 -1.312042 a one
[5 rows x 4 columns]
>>> df.groupby('key1').transform('mean')
data1 data2
a b
even 0 -0.281848 -0.059774
odd 1 -0.281848 -0.059774
even 2 0.581912 0.381061
odd 3 0.581912 0.381061
even 4 -0.281848 -0.059774
[5 rows x 2 columns]
<br />
apply
apply(func, *args, **kwargs)
会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试用pd.concat()
把结果组合起来。func 的返回值可以是 pandas 对象或标量,并且数组对象的大小不限。
lang:python
>>> df
data1 data2 key1 key2
0 0.721150 -0.359337 a one
1 -1.727197 1.539508 a two
2 -0.339751 0.171379 b one
3 -0.291888 -1.000769 b two
4 -0.127029 0.506162 a one
[5 rows x 4 columns]
>>> def foo(df,n=12):
return pd.DataFrame(np.arange(n).reshape(3,4))
>>> df.groupby('key1').apply(foo)
0 1 2 3
key1
a 0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
b 0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
[6 rows x 4 columns]
这是一个毫无意义的例子,因为传给 apply 的 func 参数没有对 df 做任何处理,直接返回了一个(3,4)的数组。而实际上,这样一个毫无意义的例子恰好说明了 apply 方法的通用性——你可以返回任意的结果。多数时候,限制 apply 发挥的其实是用户的脑洞。 <br />
透视表和交叉表
DataFrame 对象有一个 .pivot_table(data, values=None, rows=None, cols=None, aggfunc='mean', fill_value=None, margins=False, dropna=True)
方法可以用来制作透视表,同时 pd.pivot_table()
也是一个顶层函数。
- data 参数相当于 self,这里将其命名为 data 也许是为了与顶级函数版本的 pivot_table 保持一致。
- values 参数可以是一个以列名为元素的列表,用于指定想要聚合的数据,不给出的话默认使用全部数据。
- rows 参数用于指定行分组键
- cols 参数用于指定列分组键
- aggfunc 参数用于指定聚合函数,默认为均值(mean)
- margins 参数是小计(Total)功能的开关,设为 True 后结果集中会出现名为 “ALL” 的行和列
例:
lang:python
>>> df
A B C D
0 foo one small 1
1 foo one large 2
2 foo one large 2
3 foo two small 3
4 foo two small 3
5 bar one large 4
6 bar one small 5
7 bar two small 6
8 bar two large 7
>>> table = pivot_table(df, values='D', rows=['A', 'B'],
... cols=['C'], aggfunc=np.sum)
>>> table
small large
foo one 1 4
two 6 NaN
bar one 5 4
two 6 7
<br /> 交叉表(cross-tabulation,crosstab)是一种用于计算分组频数的特殊透视表。
crosstab(rows, cols, values=None, rownames=None, colnames=None, aggfunc=None, margins=False, dropna=True)
lang:python
>>> a
array([foo, foo, foo, foo, bar, bar,
bar, bar, foo, foo, foo], dtype=object)
>>> b
array([one, one, one, two, one, one,
one, two, two, two, one], dtype=object)
>>> c
array([dull, dull, shiny, dull, dull, shiny,
shiny, dull, shiny, shiny, shiny], dtype=object)
>>> crosstab(a, [b, c], rownames=['a'], colnames=['b', 'c'])
b one two
c dull shiny dull shiny
a
bar 1 2 1 0
foo 2 2 1 2