Python数据处理学习笔记 - pandas数据分组和聚合篇

这是我阅读《用Python进行数据分析》一书的笔记、实验和总结。本篇文章主要讲解pandas包中数据分组和聚合技术,主要涉及groupby、aggregate、apply、transform、(q)cut、透视表。数据分组和聚合是对DataFrame进行分析和处理的关键步骤,尤其是apply(),其提供了一个编写函数进行运算的强大接口,正因如此,pandas的agg技术比MongoDB等数据库的agg技术更加先进、灵活、高效。

约定俗成的,引入以下包:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pprint import pprint

在上一章介绍过map函数可以对行列进行运算,但是,大多数时候我们需要对某些行列进行运算,我们可以使用切片来选取行列之后计算,或者使用bool型函数来筛选符合条件的行列后进行运算。如果我们需要对于多个行列进行合并,比如合并二级level,这个时候使用索引操纵列进行map是通用的做法。但是由于这些需求过于常见但是这些操作过于复杂,所以就应运而生了聚合。这里的聚合和数据库的聚合技术是一种东西,不过却灵活、强大很多,它可以按照一定的规则对于行列进行拆分,然后进行某一种运算或者多种或者你自定义的运算,然后进行各种不同层级的数据合并(得益于Python和ndarray)。

数据分组和聚合本质上是按照索引将数据进行分组、计算,然后再合并(SPLIT.APPLY.COMBINE)的过程。对于分组,我们使用groupBy函数,对于分过的组进行计算,可以使用aggregate或者apply来进一步操作。aggregate本质上都是分组计算的一种,不过其更经常生成标量,比如sum、count等(类似MongoDB的aggregate技术)。而使用apply则可以组合各种自己编写的函数,返回各种矢量并进行concat最后生成表。

1. 数据分组:GroupBy技术

分组的依据选择

groupby可以对字符串(列名称)、numpy类型、等长对等索引的数组、Series、字典(列名称的map关系)、函数、索引层次进行分组。大体来说,只要是和index对应的等长列表(lsit),都可以进行分组。而其余的字段,比如column name、function map,本质上都是转换成为list再进行groupby操作。

分组状况查看

对于分过的组,可以使用grouped.groups进行查看,结果会返回一个字典。对于分过的组,也可以进行迭代,在后面会详细介绍分组对象的迭代。

1.1 通过list进行分组、多层分组

一个简单的例子

df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
                   'key2' : ['one', 'two', 'one', 'two', 'one'],
                   'data1' : np.random.randn(5),
                   'data2' : np.random.randn(5)})
      data1     data2 key1 key2
0  0.089452 -1.467732    a  one
1 -0.296581  0.897673    a  two
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two
4 -0.651637  0.749137    a  one

只要一个等row index length的series、list即可进行分组

grouped = df['data1'].groupby(df['key1']) 
#使用Series对于Series进行分组,它们具有完全等同的row index。
print(grouped)
grouped.mean()
<pandas.core.groupby.SeriesGroupBy object at 0x000001B80461ABE0>

key1
a   -0.286255
b   -0.711685
Name: data1, dtype: float64

可以使用多key分组

类似于MongoDB的$group{a,b}分层分组。

means = df['data1'].groupby([df['key1'], df['key2']]).mean() 
#也可以使用一个[],进行多层的分组,类似于SQL的多Key聚合。
print(means)
print(means.unstack())
key1  key2
a     one    -0.281093
      two    -0.296581
b     one    -0.887990
      two    -0.535381
Name: data1, dtype: float64

key2       one       two
key1                    
a    -0.281093 -0.296581
b    -0.887990 -0.535381

引入外部的list而不是内部的column列也可以

states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states, years]).mean()
California  2005   -0.296581
            2006   -0.887990
Ohio        2005   -0.222965
            2006   -0.651637
Name: data1, dtype: float64

可以指定column列name

其列自动会被删除而不会进行分组。这本质上是一种语法糖,填入一个Str的时候会自动对column name进行查找,类似于:

df.groupby([df["key1"],df["key2"]]).mean()

print(df.groupby('key1').mean())
print(df.groupby(['key1', 'key2']).mean())
print(df.groupby([df["key1"],df["key2"]]).mean())
         data1     data2
key1                    
a    -0.286255  0.059693
b    -0.711685 -0.074938
              data1     data2
key1 key2                    
a    one  -0.281093 -0.359298
     two  -0.296581  0.897673
b    one  -0.887990  1.029688
     two  -0.535381 -1.179564
              data1     data2
key1 key2                    
a    one  -0.281093 -0.359298
     two  -0.296581  0.897673
b    one  -0.887990  1.029688
     two  -0.535381 -1.179564

mean用于将被聚合的数据求平均值,而size则对于groupby对象进行count计数

print(df.groupby(['key1', 'key2']).mean())
df.groupby(['key1', 'key2']).size()
              data1     data2
key1 key2                    
a    one  -0.281093 -0.359298
     two  -0.296581  0.897673
b    one  -0.887990  1.029688
     two  -0.535381 -1.179564

key1  key2
a     one     2
      two     1
b     one     1
      two     1
dtype: int64

1.2 通过字典或者Series进行分组

people = pd.DataFrame(np.random.randn(5, 5),
                      columns=['a', 'b', 'c', 'd', 'e'],
                      index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values
print(people)
               a         b         c         d         e
Joe     0.222491 -0.995911 -1.014604 -0.067544 -0.869915
Steve   0.777247 -1.353332  0.821747 -1.104266  0.156773
Wes    -0.080765       NaN       NaN  0.737536  0.132357
Jim    -0.733980 -0.728372 -1.101054 -0.518711 -0.099602
Travis -0.204770 -1.357502  1.421094  0.523682  1.917264
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
           'd': 'blue', 'f': 'red', 'e' : 'orange'} 
           #mapping中可以存在没有对应的列,程序不会对其进行匹配
by_column = people.groupby(mapping, axis=1)
print(by_column.sum())
            blue    orange       red
Joe    -1.082148 -0.869915 -0.773420
Steve  -0.282519  0.156773 -0.576085
Wes     0.737536  0.132357 -0.080765
Jim    -1.619765 -0.099602 -1.462352
Travis  1.944775  1.917264 -1.562272
map_series = pd.Series(mapping)
map_series
print(people.groupby(map_series, axis=1).count())
        blue  orange  red
Joe        2       1    2
Steve      2       1    2
Wes        1       1    1
Jim        2       1    2
Travis     2       1    2

区别于map和funcmap,这里的对各列进行的map分组本质上还是分组,其不保留各原始列的内容。

1.3 通过函数进行分组

传入一个函数作为groupby对象,函数会自动将index列各个index name值作为变量传入,然后返回一个新的值。并以这个新的值作为分组的名称。

print(people)
print(people.groupby(len).sum())
               a         b         c         d         e
Joe     0.222491 -0.995911 -1.014604 -0.067544 -0.869915
Steve   0.777247 -1.353332  0.821747 -1.104266  0.156773
Wes    -0.080765       NaN       NaN  0.737536  0.132357
Jim    -0.733980 -0.728372 -1.101054 -0.518711 -0.099602
Travis -0.204770 -1.357502  1.421094  0.523682  1.917264
          a         b         c         d         e
3 -0.592254 -1.724283 -2.115659  0.151281 -0.837160
5  0.777247 -1.353332  0.821747 -1.104266  0.156773
6 -0.204770 -1.357502  1.421094  0.523682  1.917264

函数还可以作为分组的多个key之一,因为其最后还是会被转换成为一个数组,非常灵活。

key_list = ['one', 'one', 'one', 'two', 'two']
print(people.groupby([len, key_list]).min())
              a         b         c         d         e
3 one -0.080765 -0.995911 -1.014604 -0.067544 -0.869915
  two -0.733980 -0.728372 -1.101054 -0.518711 -0.099602
5 one  0.777247 -1.353332  0.821747 -1.104266  0.156773
6 two -0.204770 -1.357502  1.421094  0.523682  1.917264

1.4 分组的迭代和内部结构

使用 grouped.groups 可以查看分组状况。如果有更多的需求,可以对分组对象进行迭代。groupby对象迭代,其返回一个元组,其第一个对象为分组的name,如果是单key group,则返回对象是一个str字符串,如果是多key group,则这个name则是一个包含多key name的tuple。

print(df.groupby('key1').mean())
for name, group in df.groupby('key1'):
    print(name)
    print(group)
         data1     data2
key1                    
a    -0.286255  0.059693
b    -0.711685 -0.074938
a
      data1     data2 key1 key2
0  0.089452 -1.467732    a  one
1 -0.296581  0.897673    a  two
4 -0.651637  0.749137    a  one
b
      data1     data2 key1 key2
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two
for (k1, k2), group in df.groupby(['key1', 'key2']):
    print((k1, k2))
    print(group)
for x in df.groupby(['key1', 'key2']):
    print(x)
    break
('a', 'one')
      data1     data2 key1 key2
0  0.089452 -1.467732    a  one
4 -0.651637  0.749137    a  one

('a', 'two')
      data1     data2 key1 key2
1 -0.296581  0.897673    a  two

('b', 'one')
     data1     data2 key1 key2
2 -0.88799  1.029688    b  one

('b', 'two')
      data1     data2 key1 key2
3 -0.535381 -1.179564    b  two

(('a', 'one'),       
data1     data2 key1 key2
0  0.089452 -1.467732    a  one
4 -0.651637  0.749137    a  one)

本质上,迭代返回的对象是:

[((name1,namea),ndarray),((name1,nameb),ndarray),((name2,namea),ndarray),((name2,nameb),ndarray)]

这很容易的利用dict和list函数来讲groupby对象变成一个以key name为key,以其聚合为value的dict。

pieces = dict(list(df.groupby('key1')))
pprint(pieces)
print(pieces['b'])
{'a':       
data1     data2 key1 key2
0  0.089452 -1.467732    a  one
1 -0.296581  0.897673    a  two
4 -0.651637  0.749137    a  one,

 'b':       
 data1     data2 key1 key2
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two}

      data1     data2 key1 key2
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two

1.5 组和列的切片选取

df.groupby(["key1"])["data1"].mean() 本质上是 df["data1"].groupby(["key1"]).mean() 的语法糖

df.groupby(["key1"])[["data1"]].mean() 本质上是 df[["data1"]].groupby(["key1"]).mean() 的语法糖

对于前者,其返回子类Groupby对象,不保留切片索引。对于后者,其仅仅是对整体的结果进行切片,保留了索引。

print(df.groupby(['key1', 'key2'])[['data2']].mean())
              data2
key1 key2          
a    one  -0.359298
     two   0.897673
b    one   1.029688
     two  -1.179564
print(df.groupby(['key1', 'key2'])['data2'].mean())
print(df.groupby(['key1', 'key2'])[['data1',"data2"]].mean())
print(df.groupby(['key1', 'key2'])['data1','data2'].mean())
key1  key2
a     one    -0.359298
      two     0.897673
b     one     1.029688
      two    -1.179564
Name: data2, dtype: float64
              data1     data2
key1 key2                    
a    one  -0.281093 -0.359298
     two  -0.296581  0.897673
b    one  -0.887990  1.029688
     two  -0.535381 -1.179564
              data1     data2
key1 key2                    
a    one  -0.281093 -0.359298
     two  -0.296581  0.897673
b    one  -0.887990  1.029688
     two  -0.535381 -1.179564

区别是,采用【】这种选择方法而不是【【】】生成的对象不是groupby而是serise/dataframegroupby对象(如果选择某一列进行计算的话)。

对于多个列进行选择的话,不论是【】还是【【】】,其生成的对象都是一样的。

1.6 axis=1的groupby

最后,也可以传递df.dtypes来作为分组依据。这里的第二个知识点是,可以指定axis为1来对column进行group,之前所有的实例都是对axis=0的row index进行的分组。

print(df)
print(df.dtypes)
grouped = df.groupby(df.dtypes, axis=1)
      data1     data2 key1 key2
0  0.089452 -1.467732    a  one
1 -0.296581  0.897673    a  two
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two
4 -0.651637  0.749137    a  one

data1    float64
data2    float64
key1      object
key2      object
dtype: object
for dtype, group in grouped:
    print(dtype)
    print(group)
float64
      data1     data2
0  0.089452 -1.467732
1 -0.296581  0.897673
2 -0.887990  1.029688
3 -0.535381 -1.179564
4 -0.651637  0.749137

object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

1.7 level=2的groupby

columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
                                    [1, 3, 5, 1, 3]],
                                    names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
print(hier_df)
cty          US                            JP          
tenor         1         3         5         1         3
0     -1.047668 -0.056759  1.000576  0.213877  0.277200
1      2.460159  0.134850 -0.852783  1.358721  0.783409
2     -0.555137 -0.075603  1.629682 -0.716262 -0.981589
3     -0.135948  0.311016  0.310988 -0.741287 -0.024857
#按照level进行groupby,除了此level的所有子层会进行合并。
print(hier_df.groupby(level='cty', axis=1).count())
cty  JP  US
0     2   3
1     2   3
2     2   3
3     2   3
print(hier_df.groupby(level="tenor",axis=1).count())
tenor  1  3  5
0      2  2  1
1      2  2  1
2      2  2  1
3      2  2  1

2. 数据聚合:agg、transform、apply

groupBy的作用主要在于其为我们提供了一个包含分组信息的可迭代的groupBy对象。这就完成了数据分组的任务,然而,真正关键的部分才刚刚开始,我们需要对这些分组进行运算,不论是内置的运算还是我们自己编写的运算,在这里需要注意,运算传入的对象是group的ndarray,至于传出什么结果,一般而言,像是agg应用count、sum这类内置函数会生成标量,然后被整个成一张表,也就是说,每个group会返回一行数据。对于transform来说,每个group中的每行元素都会返回一个数据。对于apply来说,我们可以返回任意的矢量值,也就是说每个group不一定返回相同的长度数据,反正最后会被concat进行数据合并。

2.1 数据聚合过程

groupby.mean(),这个mean是怎么工作的?这涉及到分组后的数据聚合,pandas定义了很多优化过的方法,比如sum、count、first等,但是我们也可以写自己的方法,使用agg/aggregate()进行调用。

quantile可以计算分位数。其本身是一个对于series作用的函数,在这里发生了什么?

groupby对象将结果进行了切片(piece),每一个piece对应一个series,对各片调用了piece.quantile(),然后将分别生成的结果进行了组装。

print(df)
grouped = df.groupby('key1')
for name,value in grouped["data1"]:
    print("\n",name,"\n",value)
grouped['data1'].quantile(0.9)
      data1     data2 key1 key2
0  0.089452 -1.467732    a  one
1 -0.296581  0.897673    a  two
2 -0.887990  1.029688    b  one
3 -0.535381 -1.179564    b  two
4 -0.651637  0.749137    a  one

 a 
 0    0.089452
1   -0.296581
4   -0.651637
Name: data1, dtype: float64

 b 
 2   -0.887990
3   -0.535381
Name: data1, dtype: float64

key1
a    0.012245
b   -0.570642
Name: data1, dtype: float64

也可以使用自己的agg方法,传入的是经过groupby分片的piece(Series),对series进行max和min的计算后返回结果。

在这里需要注意的是,data1和data2都有根据key1分组的两个不同piece,因此就有了四个piece,所以合并之后会生成2×2的表格

def peak_to_peak(arr):
    #print("\nI am in peak, \nthe att is \n%s"%arr)
    return arr.max() - arr.min()
print(grouped.agg(peak_to_peak))
         data1     data2
key1                    
a     0.741089  2.365405
b     0.352608  2.209252
print(grouped.describe().T[:3])
key1                a         b
data1 count  3.000000  2.000000
      mean  -0.286255 -0.711685
      std    0.370652  0.249332

内置的一些函数比如sum、count、mean、median、std、var、min、max、prod、first、last,这些都是经过优化的方法,因此使用起来非常快速。而自己定义的函数速度则受限,因此尽量采用内置的函数进行聚合计算。

2.2 agg技术: 面向列的函数应用

开始编写自己的过程函数

tips = pd.read_csv('tips.csv')
# Add tip percentage of total bill
tips['tip_pct'] = tips['tip'] / tips['total_bill']
print(tips[:6])
   total_bill   tip smoker  day    time  size   tip_pct
0       16.99  1.01     No  Sun  Dinner     2  0.059447
1       10.34  1.66     No  Sun  Dinner     3  0.160542
2       21.01  3.50     No  Sun  Dinner     3  0.166587
3       23.68  3.31     No  Sun  Dinner     2  0.139780
4       24.59  3.61     No  Sun  Dinner     4  0.146808
5       25.29  4.71     No  Sun  Dinner     4  0.186240
grouped = tips.groupby(['day', 'smoker'])
grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')
day   smoker
Fri   No        0.151650
      Yes       0.174783
Sat   No        0.158048
      Yes       0.147906
Sun   No        0.160113
      Yes       0.187250
Thur  No        0.160298
      Yes       0.163863
Name: tip_pct, dtype: float64

agg可以调用str表示的内置函数,或者直接一个函数。可以传递多个函数生成多个结果,放在一个列表中即可。

需要注意,这里和groupby技术中直接调用函数并call不同,采用agg可以使用我们自己的函数,并且能够一次进行多个函数的计算。

多函数计算

print(grouped_pct.agg(['mean', 'std', peak_to_peak]))
                 mean       std  peak_to_peak
day  smoker                                  
Fri  No      0.151650  0.028123      0.067349
     Yes     0.174783  0.051293      0.159925
Sat  No      0.158048  0.039767      0.235193
     Yes     0.147906  0.061375      0.290095
Sun  No      0.160113  0.042347      0.193226
     Yes     0.187250  0.154134      0.644685
Thur No      0.160298  0.038774      0.193350
     Yes     0.163863  0.039389      0.151240

函数别名设置

agg不仅可以调用多个函数生成结果表,还可以对每个函数进行名称的自定义。

print(grouped_pct.agg([('MEAN', 'mean'), ('STD', np.std)]))
                 MEAN       STD
day  smoker                    
Fri  No      0.151650  0.028123
     Yes     0.174783  0.051293
Sat  No      0.158048  0.039767
     Yes     0.147906  0.061375
Sun  No      0.160113  0.042347
     Yes     0.187250  0.154134
Thur No      0.160298  0.038774
     Yes     0.163863  0.039389

多层分组的多函数计算

再看一个更加复杂的例子,使用多个index进行group,对结果进行多个函数的计算,最后汇聚成表。

functions = ['count', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(functions)
print(type(result))
print(result)
<class 'pandas.core.frame.DataFrame'>
            tip_pct                     total_bill                  
              count      mean       max      count       mean    max
day  smoker                                                         
Fri  No           4  0.151650  0.187735          4  18.420000  22.75
     Yes         15  0.174783  0.263480         15  16.813333  40.17
Sat  No          45  0.158048  0.291990         45  19.661778  48.33
     Yes         42  0.147906  0.325733         42  21.276667  50.81
Sun  No          57  0.160113  0.252672         57  20.506667  48.17
     Yes         19  0.187250  0.710345         19  24.120000  45.35
Thur No          45  0.160298  0.266312         45  17.113111  41.19
     Yes         17  0.163863  0.241255         17  19.190588  43.11
print(result['tip_pct'])
             count      mean       max
day  smoker                           
Fri  No          4  0.151650  0.187735
     Yes        15  0.174783  0.263480
Sat  No         45  0.158048  0.291990
     Yes        42  0.147906  0.325733
Sun  No         57  0.160113  0.252672
     Yes        19  0.187250  0.710345
Thur No         45  0.160298  0.266312
     Yes        17  0.163863  0.241255

带有别名的多层分组多函数计算

ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
print(grouped['tip_pct', 'total_bill'].agg(ftuples))
                 tip_pct              total_bill            
            Durchschnitt Abweichung Durchschnitt  Abweichung
day  smoker                                                 
Fri  No         0.151650   0.000791    18.420000   25.596333
     Yes        0.174783   0.002631    16.813333   82.562438
Sat  No         0.158048   0.001581    19.661778   79.908965
     Yes        0.147906   0.003767    21.276667  101.387535
Sun  No         0.160113   0.001793    20.506667   66.099980
     Yes        0.187250   0.023757    24.120000  109.046044
Thur No         0.160298   0.001503    17.113111   59.625081
     Yes        0.163863   0.001551    19.190588   69.808518

多层分组非对称列索引多函数计算

agg技术不仅能够接受一个列表,还可以像group一样接受一个dict,dict的name为需要聚合的column name,dict的value为需要应用的函数。甚至你可以传递多个函数而不仅仅是一个函数,函数们放在列表中,作为二级index显示。

print(grouped.sum())
print(grouped.agg({'tip' : np.max, 'size' : 'sum'}))
print(grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
             'size' : 'sum'}))
             total_bill     tip  size   tip_pct
day  smoker                                    
Fri  No           73.68   11.25     9  0.606602
     Yes         252.20   40.71    31  2.621746
Sat  No          884.78  139.63   115  7.112145
     Yes         893.62  120.77   104  6.212055
Sun  No         1168.88  180.57   167  9.126438
     Yes         458.28   66.82    49  3.557756
Thur No          770.09  120.32   112  7.213414
     Yes         326.24   51.51    40  2.785676

               tip  size
day  smoker             
Fri  No       3.50     9
     Yes      4.73    31
Sat  No       9.00   115
     Yes     10.00   104
Sun  No       6.00   167
     Yes      6.50    49
Thur No       6.70   112
     Yes      5.00    40

              tip_pct                               size
                  min       max      mean       std  sum
day  smoker                                             
Fri  No      0.120385  0.187735  0.151650  0.028123    9
     Yes     0.103555  0.263480  0.174783  0.051293   31
Sat  No      0.056797  0.291990  0.158048  0.039767  115
     Yes     0.035638  0.325733  0.147906  0.061375  104
Sun  No      0.059447  0.252672  0.160113  0.042347  167
     Yes     0.065660  0.710345  0.187250  0.154134   49
Thur No      0.072961  0.266312  0.160298  0.038774  112
     Yes     0.090014  0.241255  0.163863  0.039389   40

没有索引的聚合形式

print(tips.groupby(['day', 'smoker'], as_index=True).mean())
print(tips.groupby(['day', 'smoker'], as_index=False).mean())
             total_bill       tip      size   tip_pct
day  smoker                                          
Fri  No       18.420000  2.812500  2.250000  0.151650
     Yes      16.813333  2.714000  2.066667  0.174783
Sat  No       19.661778  3.102889  2.555556  0.158048
     Yes      21.276667  2.875476  2.476190  0.147906
Sun  No       20.506667  3.167895  2.929825  0.160113
     Yes      24.120000  3.516842  2.578947  0.187250
Thur No       17.113111  2.673778  2.488889  0.160298
     Yes      19.190588  3.030000  2.352941  0.163863
    day smoker  total_bill       tip      size   tip_pct
0   Fri     No   18.420000  2.812500  2.250000  0.151650
1   Fri    Yes   16.813333  2.714000  2.066667  0.174783
2   Sat     No   19.661778  3.102889  2.555556  0.158048
3   Sat    Yes   21.276667  2.875476  2.476190  0.147906
4   Sun     No   20.506667  3.167895  2.929825  0.160113
5   Sun    Yes   24.120000  3.516842  2.578947  0.187250
6  Thur     No   17.113111  2.673778  2.488889  0.160298
7  Thur    Yes   19.190588  3.030000  2.352941  0.163863

使用内置的函数进行agg

聚合是一种特殊的分组运算,其特点是将多维数据进行运算并且生成一个标量值。

print(people)
print(people.groupby(["one","two","one","one","two"]).mean())
print(people.groupby(["one","two","one","one","two"]).agg("mean"))
               a         b         c         d         e
Joe     0.222491 -0.995911 -1.014604 -0.067544 -0.869915
Steve   0.777247 -1.353332  0.821747 -1.104266  0.156773
Wes    -0.080765       NaN       NaN  0.737536  0.132357
Jim    -0.733980 -0.728372 -1.101054 -0.518711 -0.099602
Travis -0.204770 -1.357502  1.421094  0.523682  1.917264
            a         b         c         d         e
one -0.197418 -0.862142 -1.057829  0.050427 -0.279053
two  0.286238 -1.355417  1.121420 -0.290292  1.037019
            a         b         c         d         e
one -0.197418 -0.862142 -1.057829  0.050427 -0.279053
two  0.286238 -1.355417  1.121420 -0.290292  1.037019

2.3 transform技术:不合并的agg

transform和agg不同的是,其保留了所有分组组内的值,不对聚合整体进行运算。但是其结果填充了对于组内运算的值,这看起来有些奇怪。造成这种情况的原因是,transform运行的函数生成了一个标量值,而这个标量值在组内被广播了。

print(people.groupby(["one","two","one","one","two"]).transform("count"))
print(people.groupby(["one","two","one","one","two"]).agg("count")) #显然下面这个看起来更清晰一些。逻辑上来说。
          a    b    c    d    e
Joe     3.0  2.0  2.0  3.0  3.0
Steve   2.0  2.0  2.0  2.0  2.0
Wes     3.0  2.0  2.0  3.0  3.0
Jim     3.0  2.0  2.0  3.0  3.0
Travis  2.0  2.0  2.0  2.0  2.0
     a  b  c  d  e
one  3  2  2  3  3
two  2  2  2  2  2
def defmean(arr):
    #print("\n","The arr is\n",arr)
    return arr - arr.mean()
res = people.groupby(["one","two","one","one","two"]).transform(defmean) 
#这个函数返回的是标量值,因此比上一个看起来有意义些。
print(res)
               a         b         c         d         e
Joe     0.419909 -0.133770  0.043225 -0.117971 -0.590861
Steve   0.491009  0.002085 -0.299673 -0.813974 -0.880246
Wes     0.116653       NaN       NaN  0.687109  0.411410
Jim    -0.536562  0.133770 -0.043225 -0.569138  0.179451
Travis -0.491009 -0.002085  0.299673  0.813974  0.880246
print(res.groupby(["one","two","one","one","two"]).mean())
                a             b             c             d             e
one -3.700743e-17  5.551115e-17  1.110223e-16 -3.700743e-17 -1.850372e-17
two -2.775558e-17  0.000000e+00  5.551115e-17  0.000000e+00 -1.110223e-16

transform和agg一样,其工作必须产生一个标量或者对应大小的数组,对于前者的标量,其会进行广播。但是,对于apply,就没有这种限制。

2.4 apply技术:动态自定义

apply技术可以传递pieces,然后进行函数运算后返回一个任意东西,经过concat方法被合并成结果,其使用限制取决于你的想象。

print(tips[:5])
   total_bill   tip smoker  day    time  size   tip_pct
0       16.99  1.01     No  Sun  Dinner     2  0.059447
1       10.34  1.66     No  Sun  Dinner     3  0.160542
2       21.01  3.50     No  Sun  Dinner     3  0.166587
3       23.68  3.31     No  Sun  Dinner     2  0.139780
4       24.59  3.61     No  Sun  Dinner     4  0.146808
def top(df, n=5, column='tip_pct'):
    return df.sort_values(by=column)[-n:]
print(top(tips, n=6))
     total_bill   tip smoker  day    time  size   tip_pct
109       14.31  4.00    Yes  Sat  Dinner     2  0.279525
183       23.17  6.50    Yes  Sun  Dinner     4  0.280535
232       11.61  3.39     No  Sat  Dinner     2  0.291990
67         3.07  1.00    Yes  Sat  Dinner     1  0.325733
178        9.60  4.00    Yes  Sun  Dinner     2  0.416667
172        7.25  5.15    Yes  Sun  Dinner     2  0.710345
print(tips.groupby('smoker').apply(top))
            total_bill   tip smoker   day    time  size   tip_pct
smoker                                                           
No     88        24.71  5.85     No  Thur   Lunch     2  0.236746
       185       20.69  5.00     No   Sun  Dinner     5  0.241663
       51        10.29  2.60     No   Sun  Dinner     2  0.252672
       149        7.51  2.00     No  Thur   Lunch     2  0.266312
       232       11.61  3.39     No   Sat  Dinner     2  0.291990
Yes    109       14.31  4.00    Yes   Sat  Dinner     2  0.279525
       183       23.17  6.50    Yes   Sun  Dinner     4  0.280535
       67         3.07  1.00    Yes   Sat  Dinner     1  0.325733
       178        9.60  4.00    Yes   Sun  Dinner     2  0.416667
       172        7.25  5.15    Yes   Sun  Dinner     2  0.710345

禁用group_keys

对于groupby传入参数 group_keys=False 来禁用index列。需要区别于 as_index 参数,后者会生成一个num的index,而前者则完全不生成index

print(tips.groupby('smoker',group_keys=False).apply(top)[:3])
     total_bill   tip smoker   day    time  size   tip_pct
88        24.71  5.85     No  Thur   Lunch     2  0.236746
185       20.69  5.00     No   Sun  Dinner     5  0.241663
51        10.29  2.60     No   Sun  Dinner     2  0.252672
print(tips.groupby('smoker',as_index=False).apply(top)[:3])
       total_bill   tip smoker   day    time  size   tip_pct
0 88        24.71  5.85     No  Thur   Lunch     2  0.236746
  185       20.69  5.00     No   Sun  Dinner     5  0.241663
  51        10.29  2.60     No   Sun  Dinner     2  0.252672

函数的参数传递

函数的参数放在apply的第二个开始的参数中。

def top(df, n=5, column='tip_pct'):
    return df.sort_values(by=column)[-n:]

print(tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill'))
                 total_bill    tip smoker   day    time  size   tip_pct
smoker day                                                             
No     Fri  94        22.75   3.25     No   Fri  Dinner     2  0.142857
       Sat  212       48.33   9.00     No   Sat  Dinner     4  0.186220
       Sun  156       48.17   5.00     No   Sun  Dinner     6  0.103799
       Thur 142       41.19   5.00     No  Thur   Lunch     5  0.121389
Yes    Fri  95        40.17   4.73    Yes   Fri  Dinner     4  0.117750
       Sat  170       50.81  10.00    Yes   Sat  Dinner     3  0.196812
       Sun  182       45.35   3.50    Yes   Sun  Dinner     3  0.077178
       Thur 197       43.11   5.00    Yes  Thur   Lunch     4  0.115982
result = tips.groupby('smoker')['tip_pct'].describe()
result.unstack('smoker')
       smoker
count  No        151.000000
       Yes        93.000000
mean   No          0.159328
       Yes         0.163196
std    No          0.039910
       Yes         0.085119
min    No          0.056797
       Yes         0.035638
25%    No          0.136906
       Yes         0.106771
50%    No          0.155625
       Yes         0.153846
75%    No          0.185014
       Yes         0.195059
max    No          0.291990
       Yes         0.710345
dtype: float64

f = lambda x: x.describe() grouped.apply(f)

2.5 bucket技术:分位数分组计算

在上一章介绍了简单的(q)cut技术,此技术可以将数据放入等间距的桶中。在那个时候,我们仅仅做了划分,并且提到了使用哑变量对桶进行计数,这些都太简单以至于无法满足我们的需求。我们看到,cut后生成的对象可以看作是一个带有索引的list,其可以被group传入作为参数进行分组,这样的话,我们就可以使用上述的分组聚合中强大的函数对每个不同的等间距的桶进行函数操作了。传入labels=False即可只获得分位数的编号。

frame = pd.DataFrame({'data1': np.random.randn(1000),
                      'data2': np.random.randn(1000)})
print(frame[:10])
quartiles = pd.cut(frame.data1, 4)
quartiles[:10]
      data1     data2
0 -0.721918  1.834826
1  0.416299 -1.262044
2  0.889662  0.399180
3  0.145300  1.346413
4  0.568909  0.237202
5  0.443284  0.100064
6 -2.968871 -0.762229
7 -0.423001 -2.982941
8  0.526367  0.502630
9 -0.244421 -0.109955

0     (-1.364, 0.262]
1      (0.262, 1.888]
2      (0.262, 1.888]
3     (-1.364, 0.262]
4      (0.262, 1.888]
5      (0.262, 1.888]
6    (-2.996, -1.364]
7     (-1.364, 0.262]
8      (0.262, 1.888]
9     (-1.364, 0.262]
Name: data1, dtype: category
Categories (4, interval[float64]): 
[(-2.996, -1.364] < (-1.364, 0.262] < (0.262, 1.888] < (1.888, 3.514]]
def get_stats(group):
    # 对每个桶进行处理 
    return {'min': group.min(), 'max': group.max(),
            'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles)
print(grouped.agg(["count","max","min","mean"]))
print(grouped.apply(get_stats).unstack())
                  count       max       min      mean
data1                                                
(-2.996, -1.364]     75  2.304557 -2.458287  0.044986
(-1.364, 0.262]     537  3.270650 -2.982941  0.056324
(0.262, 1.888]      366  2.131040 -2.574885 -0.026318
(1.888, 3.514]       22  2.153574 -2.133735 -0.192175

                  count       max      mean       min
data1                                                
(-2.996, -1.364]   75.0  2.304557  0.044986 -2.458287
(-1.364, 0.262]   537.0  3.270650  0.056324 -2.982941
(0.262, 1.888]    366.0  2.131040 -0.026318 -2.574885
(1.888, 3.514]     22.0  2.153574 -0.192175 -2.133735
# Return quantile numbers 使用labels=False来返回num而不是label。
#grouping = pd.qcut(frame.data1, 10)
grouping = pd.qcut(frame.data1, 10,labels=False)
grouped = frame.data2.groupby(grouping)
print(grouped.apply(get_stats).unstack())
       count       max      mean       min
data1                                     
0      100.0  2.304557 -0.005875 -2.458287
1      100.0  2.480745  0.136586 -2.465328
2      100.0  3.270650  0.045831 -2.692579
3      100.0  2.026401 -0.078111 -2.982941
4      100.0  2.569971  0.038701 -2.771897
5      100.0  3.176304  0.201180 -1.867928
6      100.0  2.003798 -0.142893 -2.574885
7      100.0  1.998057 -0.056489 -1.964135
8      100.0  2.009951  0.096824 -2.542099
9      100.0  2.153574 -0.038157 -2.133735

3. 特殊的分组聚合:透视表和交叉表

透视表和交叉表在上一章节也有涉及,其可以看作是分组和堆叠的特殊形式(group & stark)。

3.1 透视表

print(tips[:5])
   total_bill   tip smoker  day    time  size   tip_pct
0       16.99  1.01     No  Sun  Dinner     2  0.059447
1       10.34  1.66     No  Sun  Dinner     3  0.160542
2       21.01  3.50     No  Sun  Dinner     3  0.166587
3       23.68  3.31     No  Sun  Dinner     2  0.139780
4       24.59  3.61     No  Sun  Dinner     4  0.146808
print(tips.pivot_table(index=['day', 'smoker']))
                 size       tip   tip_pct  total_bill
day  smoker                                          
Fri  No      2.250000  2.812500  0.151650   18.420000
     Yes     2.066667  2.714000  0.174783   16.813333
Sat  No      2.555556  3.102889  0.158048   19.661778
     Yes     2.476190  2.875476  0.147906   21.276667
Sun  No      2.929825  3.167895  0.160113   20.506667
     Yes     2.578947  3.516842  0.187250   24.120000
Thur No      2.488889  2.673778  0.160298   17.113111
     Yes     2.352941  3.030000  0.163863   19.190588
print(tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
                 columns='smoker'))

margins可以为所有的二级column进行合并,即不考虑二级column index。

print(tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
                 columns='smoker', margins=True))
                 size                       tip_pct                    
smoker             No       Yes       All        No       Yes       All
time   day                                                             
Dinner Fri   2.000000  2.222222  2.166667  0.139622  0.165347  0.158916
       Sat   2.555556  2.476190  2.517241  0.158048  0.147906  0.153152
       Sun   2.929825  2.578947  2.842105  0.160113  0.187250  0.166897
       Thur  2.000000       NaN  2.000000  0.159744       NaN  0.159744
Lunch  Fri   3.000000  1.833333  2.000000  0.187735  0.188937  0.188765
       Thur  2.500000  2.352941  2.459016  0.160311  0.163863  0.161301
All          2.668874  2.408602  2.569672  0.159328  0.163196  0.160803
print(tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',
                 aggfunc=len, margins=True))
day             Fri   Sat   Sun  Thur    All
time   smoker                               
Dinner No       3.0  45.0  57.0   1.0  106.0
       Yes      9.0  42.0  19.0   NaN   70.0
Lunch  No       1.0   NaN   NaN  44.0   45.0
       Yes      6.0   NaN   NaN  17.0   23.0
All            19.0  87.0  76.0  62.0  244.0

aggfunc参数可以传递任何能够传递给groupby的函数,比如mean、count,自己写的函数等。透视表本质上就是一种数据聚合。聚合的对象是那些被分在同一个表格(cell)中的数据。(在这个例子中,聚合的是某个时间的某个size的某个人的类型在某一天的所有数据)

print(tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
                 columns='day', aggfunc='mean', fill_value=0)[:5])
day                      Fri       Sat       Sun      Thur
time   size smoker                                        
Dinner 1    No      0.000000  0.137931  0.000000  0.000000
            Yes     0.000000  0.325733  0.000000  0.000000
       2    No      0.139622  0.162705  0.168859  0.159744
            Yes     0.171297  0.148668  0.207893  0.000000
       3    No      0.000000  0.154661  0.152663  0.000000

3.2 交叉表

交叉表是一种用来计算分组频率的特殊透视表。

from io import StringIO
data = """\
Sample  Nationality  Handedness
1   USA  Right-handed
2   Japan    Left-handed
3   USA  Right-handed
4   Japan    Right-handed
5   Japan    Left-handed
6   Japan    Right-handed
7   USA  Right-handed
8   USA  Left-handed
9   Japan    Right-handed
10  USA  Right-handed"""
data = pd.read_table(StringIO(data), sep='\s+')
print(data)
   Sample Nationality    Handedness
0       1         USA  Right-handed
1       2       Japan   Left-handed
2       3         USA  Right-handed
3       4       Japan  Right-handed
4       5       Japan   Left-handed
5       6       Japan  Right-handed
6       7         USA  Right-handed
7       8         USA   Left-handed
8       9       Japan  Right-handed
9      10         USA  Right-handed
print(pd.crosstab(data.Nationality, data.Handedness, margins=True))
Handedness   Left-handed  Right-handed  All
Nationality                                
Japan                  2             3    5
USA                    1             4    5
All                    3             7   10
print(pd.crosstab([tips.time, tips.day], tips.smoker, margins=True))
smoker        No  Yes  All
time   day                
Dinner Fri     3    9   12
       Sat    45   42   87
       Sun    57   19   76
       Thur    1    0    1
Lunch  Fri     1    6    7
       Thur   44   17   61
All          151   93  244

4. 实例分析

4.1 使用分组进行NA值自定义填充

这个需求来自于illna()函数并不能很好的根据某些特殊情况填充不同的数值到行列中,比如根据分组的不同填充不同的数据,这个时候,使用分组和trans/apply就可以很好的解决这个问题。

s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s
s.fillna(s.mean())
0   -0.952054
1   -1.194706
2   -0.952054
3   -2.519394
4   -0.952054
5    0.857938
dtype: float64
states = ['Ohio', 'New York', 'Vermont', 'Florida',
          'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
data = pd.Series(np.random.randn(8), index=states)
data
Ohio          0.519011
New York      1.471375
Vermont       1.015578
Florida       1.821681
Oregon       -1.135199
Nevada        0.079404
California   -0.148342
Idaho         1.829694
dtype: float64
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
data.groupby(group_key).mean()
East    1.270689
West   -0.641770
dtype: float64
fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio          0.519011
New York      1.471375
Vermont       1.270689
Florida       1.821681
Oregon       -1.135199
Nevada       -0.641770
California   -0.148342
Idaho        -0.641770
dtype: float64
fill_values = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
Ohio          0.519011
New York      1.471375
Vermont       0.500000
Florida       1.821681
Oregon       -1.135199
Nevada       -1.000000
California   -0.148342
Idaho        -1.000000
dtype: float64

4.2 分组加权平均数和相关系数的计算

在分析中,对于两列进行共同操作是一件常见的事情,比如下例反映了数据和其权重(第一个例子),想要求列的相关(第二个例子),对于第一个例子,我们需要将其进行合并求得加权数据,使用分组的apply可以很好的在进行运算的同时合并不同的分组。当然,使用map也可以。

df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
                                'b', 'b', 'b', 'b'],
                   'data': np.random.randn(8),
                   'weights': np.random.rand(8)})
print(df)
  category      data   weights
0        a -1.927664  0.676313
1        a -0.577289  0.931676
2        a  0.553938  0.763263
3        a  1.470285  0.332282
4        b  1.900069  0.476411
5        b -0.696027  0.615567
6        b  0.124953  0.799935
7        b -0.702701  0.716592

np.average()可以计算平均数,里面有一个参数加权。

grouped = df.groupby("category")
runc = lambda g:np.average(g["data"],weights=g["weights"])
result = grouped.apply(runc)
result
category
a   -0.344069
b    0.028049
dtype: float64
close_px = pd.read_csv('stock_px_2.csv', parse_dates=True,
                       index_col=0)
close_px.info()
print(close_px[-4:])
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
Data columns (total 4 columns):
AAPL    2214 non-null float64
MSFT    2214 non-null float64
XOM     2214 non-null float64
SPX     2214 non-null float64
dtypes: float64(4)
memory usage: 86.5 KB
              AAPL   MSFT    XOM      SPX
2011-10-11  400.29  27.00  76.27  1195.54
2011-10-12  402.19  26.96  77.16  1207.25
2011-10-13  408.43  27.18  76.37  1203.66
2011-10-14  422.00  27.27  78.11  1224.58
spx_corr = lambda x: x.corrwith(x['SPX'])
rets = close_px.pct_change().dropna()
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
print(by_year.apply(spx_corr))
          AAPL      MSFT       XOM  SPX
2003  0.541124  0.745174  0.661265  1.0
2004  0.374283  0.588531  0.557742  1.0
2005  0.467540  0.562374  0.631010  1.0
2006  0.428267  0.406126  0.518514  1.0
2007  0.508118  0.658770  0.786264  1.0
2008  0.681434  0.804626  0.828303  1.0
2009  0.707103  0.654902  0.797921  1.0
2010  0.710105  0.730118  0.839057  1.0
2011  0.691931  0.800996  0.859975  1.0
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
2003    0.480868
2004    0.259024
2005    0.300093
2006    0.161735
2007    0.417738
2008    0.611901
2009    0.432738
2010    0.571946
2011    0.581987
dtype: float64

4.3 分组线性回归

使用statsmodels中的ols线性回归函数进行分组的相关计算。可以很方便的对基于日期的数据进行合并统计。

import statsmodels.api as sm
def regress(data, yvar, xvars):
    Y = data[yvar]
    X = data[xvars]
    X['intercept'] = 1.
    result = sm.OLS(Y, X).fit()
    return result.params
print(by_year.apply(regress, 'AAPL', ['SPX']))
           SPX  intercept
2003  1.195406   0.000710
2004  1.363463   0.004201
2005  1.766415   0.003246
2006  1.645496   0.000080
2007  1.198761   0.003438
2008  0.968016  -0.001110
2009  0.879103   0.002954
2010  1.052608   0.001261
2011  0.806605   0.001514

5 示例:美国联邦选举数据分析

本数据库包含2008年美国大选的数据信息,数据大约150万条,包含投票人、职业、捐赠金额等。

fec = pd.read_csv("P00000001-ALL.csv",low_memory=False)
print(fec[:5])
     cmte_id    cand_id             cand_nm           contbr_nm  \
0  C00410118  P20002978  Bachmann, Michelle     HARVEY, WILLIAM   
1  C00410118  P20002978  Bachmann, Michelle     HARVEY, WILLIAM   
2  C00410118  P20002978  Bachmann, Michelle       SMITH, LANIER   
3  C00410118  P20002978  Bachmann, Michelle    BLEVINS, DARONDA   
4  C00410118  P20002978  Bachmann, Michelle  WARDENBURG, HAROLD   

          contbr_city contbr_st contbr_zip        contbr_employer  \
0              MOBILE        AL  366010290                RETIRED   
1              MOBILE        AL  366010290                RETIRED   
2              LANETT        AL  368633403  INFORMATION REQUESTED   
3             PIGGOTT        AR  724548253                   NONE   
4  HOT SPRINGS NATION        AR  719016467                   NONE   

       contbr_occupation  contb_receipt_amt contb_receipt_dt receipt_desc  \
0                RETIRED              250.0        20-JUN-11          NaN   
1                RETIRED               50.0        23-JUN-11          NaN   
2  INFORMATION REQUESTED              250.0        05-JUL-11          NaN   
3                RETIRED              250.0        01-AUG-11          NaN   
4                RETIRED              300.0        20-JUN-11          NaN   

  memo_cd memo_text form_tp  file_num  
0     NaN       NaN   SA17A    736166  
1     NaN       NaN   SA17A    736166  
2     NaN       NaN   SA17A    749073  
3     NaN       NaN   SA17A    749073  
4     NaN       NaN   SA17A    736166  

随便找一个数据,可以看到有以下字段:

fec.iloc[12000]
cmte_id                            C00431171
cand_id                            P80003353
cand_nm                         Romney, Mitt
contbr_nm            FELSENTHAL, JUDITH MRS.
contbr_city                    BEVERLY HILLS
contbr_st                                 CA
contbr_zip                         902105514
contbr_employer                    HOMEMAKER
contbr_occupation                  HOMEMAKER
contb_receipt_amt                       1000
contb_receipt_dt                   05-DEC-11
receipt_desc                             NaN
memo_cd                                  NaN
memo_text                                NaN
form_tp                                SA17A
file_num                              771927
Name: 12000, dtype: object

首先我们要对候选人分析,使用unique找到所有的候选人信息。

unique_cands = fec.cand_nm.unique()
unique_cands
array(['Bachmann, Michelle', 'Romney, Mitt', 'Obama, Barack',
       "Roemer, Charles E. 'Buddy' III", 'Pawlenty, Timothy',
       'Johnson, Gary Earl', 'Paul, Ron', 'Santorum, Rick', 'Cain, Herman',
       'Gingrich, Newt', 'McCotter, Thaddeus G', 'Huntsman, Jon',
       'Perry, Rick'], dtype=object)

接着我们发现候选人没有党籍信息,所以这里使用map来映射,之后填充到fec的新列。

cands_partymap = {"Bachmann, Michelle":"Republican",
                 'Romney, Mitt':"Republican", 
                  'Obama, Barack':"Democrat",
       "Roemer, Charles E. 'Buddy' III":"Republican",
                  'Pawlenty, Timothy':"Republican",
       'Johnson, Gary Earl':"Republican",
                  'Paul, Ron':"Republican",
                  'Santorum, Rick':"Republican",
       'Cain, Herman':"Republican",
                  'Gingrich, Newt':"Republican",
                  'McCotter, Thaddeus G':"Republican",
       'Huntsman, Jon':"Republican", 
                  'Perry, Rick':"Republican"}
fec.cand_nm[123456:123480].map(cands_partymap)
fec["party"] = fec.cand_nm.map(cands_partymap)

可以看看分组统计信息:

fec.party.value_counts()
Democrat      593746
Republican    407985
Name: party, dtype: int64

由于Obama和Romney占有很大份额,因此搞一个子集等下单独处理,这里用到了isin,用来过滤数据。可以看到,对于一个新数据集,values_counts、isin和unique这三剑客很有用。接下来就是dropna、fillna、bool-ndarray筛选和填充的时候了,比如这个,筛选出收入,只保留支出。

fec = fec[fec.contb_receipt_amt > 0]
fec_choose = fec[fec.cand_nm.isin(["Obama, Barack","Romney, Mitt"])]
fec_choose.count()
cmte_id              694282
cand_id              694282
cand_nm              694282
contbr_nm            694282
contbr_city          694275
contbr_st            694278
contbr_zip           694234
contbr_employer      693607
contbr_occupation    693524
contb_receipt_amt    694282
contb_receipt_dt     694282
receipt_desc           2345
memo_cd               87387
memo_text             90672
form_tp              694282
file_num             694282
party                694282
dtype: int64

这里我们想要统计不同党派的捐献职业。

常见的职业有:

fec.contb_receipt_amt.dropna(inplace=True)

接下来,我们要处理一下职业问题,因为职业里有很多重复,找出最多的职业,之后使用map进行映射。这里需要注意,如果直接map,那么所有不在dict中的职业信息会丢失,因此这里设置了一个函数,然后使用dict.getr(x,x),这样如果没有的话,会填充默认值:

get(key[, default])
Return the value for key if key is in the dictionary, else default. 
If default is not given, it defaults to None, 
so that this method never raises a KeyError.
occ_mapping = {
    "INFORMATION REQUESTED":"NOT PROVIDED",
    "INFORMATION REQUESTED PER BEST EFFORTS":"NOT PROVIDED",
    "C.E.O.":"CEO",
    "C.F.O.":"CFO"
}
emp_mapping = {
    "INFORMATION REQUESTED":"NOT PROVIDED",
    "INFORMATION REQUESTED PER BEST EFFORTS":"NOT PROVIDED",
    "SELF EMPLOYED":"SELF-EMPLOYED",
    "SELF":"SELF-EMPLOYED"
}
f = lambda x:emp_mapping.get(x,x)
fec.contbr_employer=fec.contbr_employer.map(f)
f2 = lambda y:occ_mapping.get(y,y)
fec.contbr_occupation=fec.contbr_occupation.map(f2)
by_occ = fec.pivot_table("contb_receipt_amt", 
index="contbr_occupation",columns="party",aggfunc="sum")
by_occ_max = by_occ[by_occ.sum(1)>2000000]
print(by_occ_max)
party                 Democrat    Republican
contbr_occupation                           
ATTORNEY           11141982.97  7.477194e+06
CEO                 2074974.79  4.211041e+06
CONSULTANT          2459912.71  2.544725e+06
ENGINEER             951525.55  1.818374e+06
EXECUTIVE           1355161.05  4.138850e+06
HOMEMAKER           4248875.80  1.363428e+07
INVESTOR             884133.00  2.431769e+06
LAWYER              3160478.87  3.912243e+05
MANAGER              762883.22  1.444532e+06
NOT PROVIDED        4866973.96  2.023715e+07
OWNER               1001567.36  2.408287e+06
PHYSICIAN           3735124.94  3.594320e+06
PRESIDENT           1878509.95  4.720924e+06
PROFESSOR           2165071.08  2.967027e+05
REAL ESTATE          528902.09  1.625902e+06
RETIRED            25305116.38  2.356124e+07
SELF-EMPLOYED        672393.40  1.640253e+06
%matplotlib inline
by_occ_max.plot(kind="barh")
<matplotlib.axes._subplots.AxesSubplot at 0x1b8175fb3c8>

png

接下来,我们要统计两个候选人和投资人职业的关系。使用groupby技术,这里定义了一个函数用来将同一职业的捐赠金额排序、合并。最后找出了前7位职业。

def get_group_amont(group,key,n=5):
    totals = group.groupby(key)["contb_receipt_amt"].sum()
    return totals.sort_values(ascending=False).iloc[:n]
grouped = fec_choose.groupby("cand_nm")
grouped.apply(get_group_amont,"contbr_occupation",n=7)
cand_nm        contbr_occupation                     
Obama, Barack  RETIRED                                   25305116.38
               ATTORNEY                                  11141982.97
               INFORMATION REQUESTED                      4866973.96
               HOMEMAKER                                  4248875.80
               PHYSICIAN                                  3735124.94
               LAWYER                                     3160478.87
               CONSULTANT                                 2459912.71
Romney, Mitt   RETIRED                                   11508473.59
               INFORMATION REQUESTED PER BEST EFFORTS    11396894.84
               HOMEMAKER                                  8147446.22
               ATTORNEY                                   5364718.82
               PRESIDENT                                  2491244.89
               EXECUTIVE                                  2300947.03
               C.E.O.                                     1968386.11
Name: contb_receipt_amt, dtype: float64
grouped.apply(get_group_amont,"contbr_employer",n=10)
cand_nm        contbr_employer                       
Obama, Barack  RETIRED                                   22694358.85
               SELF-EMPLOYED                             17080985.96
               NOT EMPLOYED                               8586308.70
               INFORMATION REQUESTED                      5053480.37
               HOMEMAKER                                  2605408.54
               SELF                                       1076531.20
               SELF EMPLOYED                               469290.00
               STUDENT                                     318831.45
               VOLUNTEER                                   257104.00
               MICROSOFT                                   215585.36
Romney, Mitt   INFORMATION REQUESTED PER BEST EFFORTS    12059527.24
               RETIRED                                   11506225.71
               HOMEMAKER                                  8147196.22
               SELF-EMPLOYED                              7409860.98
               STUDENT                                     496490.94
               CREDIT SUISSE                               281150.00
               MORGAN STANLEY                              267266.00
               GOLDMAN SACH & CO.                          238250.00
               BARCLAYS CAPITAL                            162750.00
               H.I.G. CAPITAL                              139500.00
Name: contb_receipt_amt, dtype: float64

最后,我们想要了解捐献金额的区别,看看富人和穷人对这两个候选人怎么看。使用bucket技术进行面元划分。

bins = np.array([0,1,10,100,1000,10000,100000,1000000,10000000])
labels = pd.cut(fec_choose.contb_receipt_amt,bins)
labels[:3]
411      (10, 100]
412    (100, 1000]
413    (100, 1000]
Name: contb_receipt_amt, dtype: category
Categories (8, interval[int64]): 
[(0, 1] < (1, 10] < (10, 100] < (100, 1000] < (1000, 10000] < (10000, 100000] < (100000, 1000000] < (1000000, 10000000]]
grouped2 = fec_choose.groupby(["cand_nm",labels])
print(grouped2["contb_receipt_amt"].sum().unstack(level=0))
cand_nm              Obama, Barack  Romney, Mitt
contb_receipt_amt                               
(0, 1]                      318.24         77.00
(1, 10]                  337267.62      29819.66
(10, 100]              20288981.41    1987783.76
(100, 1000]            54798531.46   22363381.69
(1000, 10000]          51753705.67   63942145.42
(10000, 100000]           59100.00      12700.00
(100000, 1000000]       1490683.08           NaN
(1000000, 10000000]     7148839.76           NaN
bucket_sums = grouped2.size().unstack(level=0)
print(bucket_sums)
cand_nm              Obama, Barack  Romney, Mitt
contb_receipt_amt                               
(0, 1]                       493.0          77.0
(1, 10]                    40070.0        3681.0
(10, 100]                 372280.0       31853.0
(100, 1000]               153991.0       43357.0
(1000, 10000]              22284.0       26186.0
(10000, 100000]                2.0           1.0
(100000, 1000000]              3.0           NaN
(1000000, 10000000]            4.0           NaN
normed_sum = bucket_sums.div(bucket_sums.sum(axis=1),axis=0)
print(normed_sum)
cand_nm              Obama, Barack  Romney, Mitt
contb_receipt_amt                               
(0, 1]                    0.864912      0.135088
(1, 10]                   0.915865      0.084135
(10, 100]                 0.921182      0.078818
(100, 1000]               0.780302      0.219698
(1000, 10000]             0.459748      0.540252
(10000, 100000]           0.666667      0.333333
(100000, 1000000]         1.000000           NaN
(1000000, 10000000]       1.000000           NaN
normed_sum[:-2].plot(kind="barh",stacked=True)
<matplotlib.axes._subplots.AxesSubplot at 0x1b81600c588>

png

——————————————————————————————————

更新日志

2018-03-21 阅读《利用Python进行数据分析》并整理笔记。

2018-03-25 修改了部分目录,阅读《Python数据分析实战》并完善笔记。