查看原文
其他

【翻译】《利用Python进行数据分析·第2版》第10章(下) 数据聚合与分组运算

SeanCheney Python爱好者社区 2019-04-07

作者:SeanCheney   Python爱好者社区专栏作者

简书专栏:https://www.jianshu.com/u/130f76596b02


前文传送门:

【翻译】《利用Python进行数据分析·第2版》第1章 准备工作

【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(中)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(下)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第5章(上)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(中)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(下)pandas入门

【翻译】《利用Python进行数据分析·第2版》第6章(上) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(中) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(下) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第7章(上)数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(中) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(下) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第8章(上) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(中) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(下) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第9章(上) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(中) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(下) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第10章(上) 数据聚合与分组运算

【翻译】《利用Python进行数据分析·第2版》第10章(中) 数据聚合与分组运算


分位数和桶分析


我曾在第8章中讲过,pandas有一些能根据指定面元或样本分位数将数据拆分成多块的工具(比如cut和qcut)。将这些函数跟groupby结合起来,就能非常轻松地实现对数据集的桶(bucket)或分位数(quantile)分析了。以下面这个简单的随机数据集为例,我们利用cut将其装入长度相等的桶中:

In [82]: frame = pd.DataFrame({'data1': np.random.randn(1000),   ....:                       'data2': np.random.randn(1000)}) In [83]: quartiles = pd.cut(frame.data1, 4) In [84]: quartiles[:10] Out[84]: 0     (-1.23, 0.489] 1    (-2.956, -1.23] 2     (-1.23, 0.489] 3     (0.489, 2.208] 4     (-1.23, 0.489] 5     (0.489, 2.208] 6     (-1.23, 0.489] 7     (-1.23, 0.489] 8     (0.489, 2.208] 9     (0.489, 2.208] Name: data1, dtype: category Categories (4, interval[float64]): [(-2.956, -1.23] < (-1.23, 0.489] < (0.489, 2. 208] < (2.208, 3.928]]

由cut返回的Categorical对象可直接传递到groupby。因此,我们可以像下面这样对data2列做一些统计计算:

In [85]: def get_stats(group):   ....:     return {'min': group.min(), 'max': group.max(),   ....:             'count': group.count(), 'mean': group.mean()} In [86]: grouped = frame.data2.groupby(quartiles) In [87]: grouped.apply(get_stats).unstack() Out[87]:                 count       max      mean       min data1                                               (-2.956, -1.23]   95.0  1.670835 -0.039521 -3.399312 (-1.23, 0.489]   598.0  3.260383 -0.002051 -2.989741 (0.489, 2.208]   297.0  2.954439  0.081822 -3.745356 (2.208, 3.928]    10.0  1.765640  0.024750 -1.929776

这些都是长度相等的桶。要根据样本分位数得到大小相等的桶,使用qcut即可。传入labels=False即可只获取分位数的编号:

# Return quantile numbers In [88]: grouping = pd.qcut(frame.data1, 10, labels=False) In [89]: grouped = frame.data2.groupby(grouping) In [90]: grouped.apply(get_stats).unstack() Out[90]:       count       max      mean       min data1                                     0      100.0  1.670835 -0.049902 -3.399312 1      100.0  2.628441  0.030989 -1.950098 2      100.0  2.527939 -0.067179 -2.925113 3      100.0  3.260383  0.065713 -2.315555 4      100.0  2.074345 -0.111653 -2.047939 5      100.0  2.184810  0.052130 -2.989741 6      100.0  2.458842 -0.021489 -2.223506 7      100.0  2.954439 -0.026459 -3.056990 8      100.0  2.735527  0.103406 -3.745356 9      100.0  2.377020  0.220122 -2.064111

我们会在第12章详细讲解pandas的Categorical类型。


示例:用特定于分组的值填充缺失值


对于缺失数据的清理工作,有时你会用dropna将其替换掉,而有时则可能会希望用一个固定值或由数据集本身所衍生出来的值去填充NA值。这时就得使用fillna这个工具了。在下面这个例子中,我用平均值去填充NA值:

In [91]: s = pd.Series(np.random.randn(6)) In [92]: s[::2] = np.nan In [93]: s Out[93]: 0         NaN 1   -0.125921 2         NaN 3   -0.884475 4         NaN 5    0.227290 dtype: float64 In [94]: s.fillna(s.mean()) Out[94]: 0   -0.261035 1   -0.125921 2   -0.261035 3   -0.884475 4   -0.261035 5    0.227290 dtype: float64

假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。下面是一些有关美国几个州的示例数据,这些州又被分为东部和西部:

In [95]: states = ['Ohio', 'New York', 'Vermont', 'Florida',   ....:           'Oregon', 'Nevada', 'California', 'Idaho'] In [96]: group_key = ['East'] * 4 + ['West'] * 4 In [97]: data = pd.Series(np.random.randn(8), index=states) In [98]: data Out[98]: Ohio          0.922264 New York     -2.153545 Vermont      -0.365757 Florida      -0.375842 Oregon        0.329939 Nevada        0.981994 California    1.105913 Idaho        -1.613716 dtype: float64

['East'] * 4产生了一个列表,包括了['East']中元素的四个拷贝。将这些列表串联起来。


将一些值设为缺失:

In [99]: data[['Vermont', 'Nevada', 'Idaho']] = np.nan In [100]: data Out[100]: Ohio          0.922264 New York     -2.153545 Vermont            NaN Florida      -0.375842 Oregon        0.329939 Nevada             NaN California    1.105913 Idaho              NaN dtype: float64 In [101]: data.groupby(group_key).mean() Out[101]: East   -0.535707 West    0.717926 dtype: float64

我们可以用分组平均值去填充NA值:

In [102]: fill_mean = lambda g: g.fillna(g.mean()) In [103]: data.groupby(group_key).apply(fill_mean) Out[103]: Ohio          0.922264 New York     -2.153545 Vermont      -0.535707 Florida      -0.375842 Oregon        0.329939 Nevada        0.717926 California    1.105913 Idaho         0.717926 dtype: float64

也可以在代码中预定义各组的填充值。由于分组具有一个name属性,所以我们可以拿来用一下:

In [104]: fill_values = {'East': 0.5, 'West': -1} In [105]: fill_func = lambda g: g.fillna(fill_values[g.name]) In [106]: data.groupby(group_key).apply(fill_func) Out[106]: Ohio          0.922264 New York     -2.153545 Vermont       0.500000 Florida      -0.375842 Oregon        0.329939 Nevada       -1.000000 California    1.105913 Idaho        -1.000000 dtype: float64

示例:随机采样和排列


假设你想要从一个大数据集中随机抽取(进行替换或不替换)样本以进行蒙特卡罗模拟(Monte Carlo simulation)或其他分析工作。“抽取”的方式有很多,这里使用的方法是对Series使用sample方法:

# Hearts, Spades, Clubs, Diamonds suits = ['H', 'S', 'C', 'D'] card_val = (list(range(1, 11)) + [10] * 3) * 4 base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q'] cards = [] for suit in ['H', 'S', 'C', 'D']:    cards.extend(str(num) + suit for num in base_names) deck = pd.Series(card_val, index=cards)

现在我有了一个长度为52的Series,其索引包括牌名,值则是21点或其他游戏中用于计分的点数(为了简单起见,我当A的点数为1):

In [108]: deck[:13] Out[108]: AH      1 2H      2 3H      3 4H      4 5H      5 6H      6 7H      7 8H      8 9H      9 10H    10 JH     10 KH     10 QH     10 dtype: int64

现在,根据我上面所讲的,从整副牌中抽出5张,代码如下:

In [109]: def draw(deck, n=5):   .....:     return deck.sample(n) In [110]: draw(deck) Out[110]: AD     1 8C     8 5H     5 KC    10 2C     2 dtype: int64

假设你想要从每种花色中随机抽取两张牌。由于花色是牌名的最后一个字符,所以我们可以据此进行分组,并使用apply:

In [111]: get_suit = lambda card: card[-1] # last letter is suit In [112]: deck.groupby(get_suit).apply(draw, n=2) Out[112]: C  2C     2   3C     3 D  KD    10   8D     8 H  KH    10   3H     3 S  2S     2   4S     4 dtype: int64

或者,也可以这样写:

In [113]: deck.groupby(get_suit, group_keys=False).apply(draw, n=2) Out[113]: KC    10 JC    10 AD     1 5D     5 5H     5 6H     6 7S     7 KS    10 dtype: int64

示例:分组加权平均数和相关系数


根据groupby的“拆分-应用-合并”范式,可以进行DataFrame的列与列之间或两个Series之间的运算(比如分组加权平均)。以下面这个数据集为例,它含有分组键、值以及一些权重值:

In [114]: df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',   .....:                                 'b', 'b', 'b', 'b'],   .....:                    'data': np.random.randn(8),   .....:                    'weights': np.random.rand(8)}) In [115]: df Out[115]:  category      data   weights 0        a  1.561587  0.957515 1        a  1.219984  0.347267 2        a -0.482239  0.581362 3        a  0.315667  0.217091 4        b -0.047852  0.894406 5        b -0.454145  0.918564 6        b -0.556774  0.277825 7        b  0.253321  0.955905

然后可以利用category计算分组加权平均数:

In [116]: grouped = df.groupby('category') In [117]: get_wavg = lambda g: np.average(g['data'], weights=g['weights']) In [118]: grouped.apply(get_wavg) Out[118]: category a    0.811643 b   -0.122262 dtype: float64

另一个例子,考虑一个来自Yahoo!Finance的数据集,其中含有几只股票和标准普尔500指数(符号SPX)的收盘价:

In [119]: close_px = pd.read_csv('examples/stock_px_2.csv', parse_dates=True,   .....:                        index_col=0) In [120]: close_px.info() <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 In [121]: close_px[-4:] Out[121]:              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之间的年度相关系数组成的DataFrame。下面是一个实现办法,我们先创建一个函数,用它计算每列和SPX列的成对相关系数:

In [122]: spx_corr = lambda x: x.corrwith(x['SPX'])

接下来,我们使用pct_change计算close_px的百分比变化:

In [123]: rets = close_px.pct_change().dropna()


最后,我们用年对百分比变化进行分组,可以用一个一行的函数,从每行的标签返回每个datetime标签的year属性:

In [124]: get_year = lambda x: x.year In [125]: by_year = rets.groupby(get_year) In [126]: by_year.apply(spx_corr) Out[126]:          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

当然,你还可以计算列与列之间的相关系数。这里,我们计算Apple和Microsoft的年相关系数:

In [127]: by_year.apply(lambda g: g['AAPL'].corr(g['MSFT'])) Out[127]: 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

示例:组级别的线性回归


顺着上一个例子继续,你可以用groupby执行更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,我可以定义下面这个regress函数(利用statsmodels计量经济学库)对各数据块执行普通最小二乘法(Ordinary Least Squares,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

现在,为了按年计算AAPL对SPX收益率的线性回归,执行:

In [129]: by_year.apply(regress, 'AAPL', ['SPX']) Out[129]:           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

10.4 透视表和交叉表


透视表(pivot table)是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。在Python和pandas中,可以通过本章所介绍的groupby功能以及(能够利用层次化索引的)重塑运算制作透视表。DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项小计,也叫做margins。


回到小费数据集,假设我想要根据day和smoker计算分组平均数(pivot_table的默认聚合类型),并将day和smoker放到行上:

In [130]: tips.pivot_table(index=['day', 'smoker']) Out[130]:                 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

可以用groupby直接来做。现在,假设我们只想聚合tip_pct和size,而且想根据time进行分组。我将smoker放到列上,把day放到行上:

In [131]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],   .....:                  columns='smoker') Out[131]:                 size             tip_pct           smoker             No       Yes        No       Yes time   day                                         Dinner Fri   2.000000  2.222222  0.139622  0.165347       Sat   2.555556  2.476190  0.158048  0.147906       Sun   2.929825  2.578947  0.160113  0.187250       Thur  2.000000       NaN  0.159744       NaN Lunch  Fri   3.000000  1.833333  0.187735  0.188937       Thur  2.500000  2.352941  0.160311  0.163863

还可以对这个表作进一步的处理,传入margins=True添加分项小计。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计:

In [132]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],   .....:                  columns='smoker', margins=True) Out[132]:                 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

这里,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行)。


要使用其他的聚合函数,将其传给aggfunc即可。例如,使用count或len可以得到有关分组大小的交叉表(计数或频率):

In [133]: tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',   .....:                  aggfunc=len, margins=True) Out[133]: 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

如果存在空的组合(也就是NA),你可能会希望设置一个fill_value:

In [134]: tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],   .....:                  columns='day', aggfunc='mean', fill_value=0) Out[134]: 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            Yes     0.000000  0.144995  0.152660  0.000000       4    No      0.000000  0.150096  0.148143  0.000000            Yes     0.117750  0.124515  0.193370  0.000000       5    No      0.000000  0.000000  0.206928  0.000000 Yes     0.000000  0.106572  0.065660  0.000000 ...                      ...       ...       ...       ... Lunch  1    No      0.000000  0.000000  0.000000  0.181728            Yes     0.223776  0.000000  0.000000  0.000000       2    No      0.000000  0.000000  0.000000  0.166005            Yes     0.181969  0.000000  0.000000  0.158843       3    No      0.187735  0.000000  0.000000  0.084246            Yes     0.000000  0.000000  0.000000  0.204952       4    No      0.000000  0.000000  0.000000  0.138919            Yes     0.000000  0.000000  0.000000  0.155410       5    No      0.000000  0.000000  0.000000  0.121389       6    No      0.000000  0.000000  0.000000  0.173706 [21 rows x 4 columns]

pivot_table的参数说明请参见表10-2。


表10-2 pivot_table的选项


交叉表:crosstab


交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频率的特殊透视表。看下面的例子:

In [138]: data Out[138]:   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

作为调查分析的一部分,我们可能想要根据国籍和用手习惯对这段数据进行统计汇总。虽然可以用pivot_table实现该功能,但是pandas.crosstab函数会更方便:

In [139]: pd.crosstab(data.Nationality, data.Handedness, margins=True) Out[139]: Handedness   Left-handed  Right-handed  All Nationality Japan                  2             3    5 USA                    1             4    5 All                    3             7   10

crosstab的前两个参数可以是数组或Series,或是数组列表。就像小费数据:

In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margins=True) Out[140]: 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

10.5 总结


掌握pandas数据分组工具既有助于数据清理,也有助于建模或统计分析工作。在第14章,我们会看几个例子,对真实数据使用groupby。

在下一章,我们将关注时间序列数据。


赞赏作者

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存