Apply:General split-apply-combine 通常的分割-应用-合并-灵析社区

清晨我上码

10.3 Apply:General split-apply-combine(应用:通用的分割-应用-合并)

general-purpose: 可以理解为通用,泛用。

例子:在计算机软件中,通用编程语言(General-purpose programming language )指被设计为各种应用领域服务的编程语言。通常通用编程语言不含有为特定应用领域设计的结构。

相对而言,特定域编程语言就是为某一个特定的领域或应用软件设计的编程语言。比如说,LaTeX就是专门为排版文献而设计的语言。

最通用的GroupBy(分组)方法是apply,这也是本节的主题。如下图所示,apply会把对象分为多个部分,然后将函数应用到每一个部分上,然后把所有的部分都合并起来:

返回之前提到的tipping数据集,假设我们想要根据不同组(group),选择前5个tip_pct值最大的。首先,写一个函数,函数的功能为在特定的列,选出有最大值的行:

import numpy as np
import pandas as pd
tips = pd.read_csv('../examples/tips.csv')
# Add tip percentage of total bill
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips.head()

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

现在,如果我们按smoker分组,然后用apply来使用这个函数,我们能得到下面的结果:

tips.groupby('smoker').apply(top)

我们来解释下上面这一行代码发生了什么。这里的top函数,在每一个DataFrame中的行组(row group)都被调用了一次,然后各自的结果通过pandas.concat合并了,最后用组名(group names)来标记每一部分。(译者:可以理解为,我们先按smoker这一列对整个DataFrame进行了分组,一共有No和Yes两组,然后对每一组上调用了top函数,所以每一组会返还5行作为结果,最后把两组的结果整合起来,一共是10行)。

最后的结果是有多层级索引(hierarchical index)的,而且这个多层级索引的内部层级(inner level)含有来自于原来DataFrame中的索引值(index values)

如果传递一个函数给apply,可以在函数之后,设定其他一些参数:

tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')

除了上面这些基本用法,要想用好apply可能需要一点创新能力。毕竟传给这个函数的内容取决于我们自己,而最终的结果只需要返回一个pandas对象或一个标量。这一章的剩余部分主要介绍如何解决在使用groupby时遇到的一些问题。

可以试一试在GroupBy对象上调用describe:

result = tips.groupby('smoker')['tip_pct'].describe()
result


smoker       
No      count    151.000000
        mean       0.159328
        std        0.039910
        min        0.056797
        25%        0.136906
        50%        0.155625
        75%        0.185014
        max        0.291990
Yes     count     93.000000
        mean       0.163196
        std        0.085119
        min        0.035638
        25%        0.106771
        50%        0.153846
        75%        0.195059
        max        0.710345
Name: tip_pct, dtype: float64
result.unstack('smoker')

在GroupBy内部,当我们想要调用一个像describe这样的函数的时候,其实相当于下面的写法:

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

1 Suppressing the Group Keys(抑制组键)

在接下来的例子,我们会看到作为结果的对象有一个多层级索引(hierarchical index),这个多层级索引是由原来的对象中,组键(group key)在每一部分的索引上得到的。我们可以在groupby函数中设置group_keys=False来关闭这个功能:

tips.groupby('smoker', group_keys=False).apply(top)

2 Quantile and Bucket Analysis(分位数与桶分析)

在第八章中,我们介绍了pandas的一些工具,比如cut和qcut,通过设置中位数,切割数据为buckets with bins(有很多箱子的桶)。

把函数通过groupby整合起来,可以在做桶分析或分位数分析的时候更方便。假设一个简单的随机数据集和一个等长的桶类型(bucket categorization),使用cut:

frame = pd.DataFrame({'data1': np.random.randn(1000),
                       'data2': np.random.randn(1000)})
frame.head()

quartiles = pd.cut(frame.data1, 4)
quartiles[:10]
0     (0.194, 1.795]
1     (1.795, 3.395]
2    (-1.407, 0.194]
3    (-1.407, 0.194]
4     (0.194, 1.795]
5     (0.194, 1.795]
6     (0.194, 1.795]
7    (-1.407, 0.194]
8    (-1.407, 0.194]
9    (-1.407, 0.194]
Name: data1, dtype: category
Categories (4, object): [(-3.0139, -1.407] < (-1.407, 0.194] < (0.194, 1.795] < (1.795, 3.395]]

cut返回的Categorical object(类别对象)能直接传入groupby。所以我们可以在data2列上计算很多统计值:

def get_stats(group):
    return {'min': group.min(), 'max': group.max(),
            'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles)
grouped.apply(get_stats).unstack()

也有相同长度的桶(equal-length buckets);想要按照样本的分位数得到相同长度的桶,用qcut。这里设定labels=False来得到分位数的数量:

# Return quantile numbers
grouping = pd.qcut(frame.data1, 10, labels=False)

译者:上面的代码是把frame的data1列分为10个bin,每个bin都有相同的数量。因为一共有1000个样本,所以每个bin里有100个样本。grouping保存的是每个样本的index以及其对应的bin的编号。

grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()

对于pandas的Categorical类型,会在第十二章做详细介绍。

3 Example: Filling Missing Values with Group-Specific Values(例子:用组特异性值来填充缺失值)

在处理缺失值的时候,一些情况下我们会直接用dropna来把缺失值删除,但另一些情况下,我们希望用一些固定的值来代替缺失值,而fillna就是用来做这个的,例如,这里我们用平均值mean来代替缺失值NA:

s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s
0         NaN
1    0.878562
2         NaN
3   -0.264051
4         NaN
5    0.760488
dtype: float64
s.fillna(s.mean())
0    0.458333
1    0.878562
2    0.458333
3   -0.264051
4    0.458333
5    0.760488
dtype: float64

假设我们想要给每一组填充不同的值。一个方法就是对数据分组后,用apply来调用fillna,在每一个组上执行一次。这里有一些样本是把美国各州分为西部和东部:

states = ['Ohio', 'New York', 'Vermont', 'Florida',
          'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
group_key
['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']
data = pd.Series(np.random.randn(8), index=states)
data
Ohio          0.683283
New York     -1.059896
Vermont       0.105837
Florida      -0.328586
Oregon        1.973413
Nevada        0.656673
California    0.001700
Idaho        -0.713295
dtype: float64

我们令data中某些值为缺失值:

data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
Ohio          0.683283
New York     -1.059896
Vermont            NaN
Florida      -0.328586
Oregon        1.973413
Nevada             NaN
California    0.001700
Idaho              NaN
dtype: float64
data.groupby(group_key).mean()
East   -0.235066
West    0.987556
dtype: float64

然后我们可以用每个组的平均值来填充NA:

fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio          0.683283
New York     -1.059896
Vermont      -0.235066
Florida      -0.328586
Oregon        1.973413
Nevada        0.987556
California    0.001700
Idaho         0.987556
dtype: float64

在另外一些情况下,我们可能希望提前设定好用于不同组的填充值。因为group有一个name属性,我们可以利用这个:

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.683283
New York     -1.059896
Vermont       0.500000
Florida      -0.328586
Oregon        1.973413
Nevada       -1.000000
California    0.001700
Idaho        -1.000000
dtype: float64

4 Example: Random Sampling and Permutation(例子:随机抽样和排列)

假设我们想要从一个很大的数据集里随机抽出一些样本,这里我们可以在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,索引(index)部分是牌的名字,对应的值为牌的点数,这里的点数是按Blackjack(二十一点)的游戏规则来设定的。

Blackjack(二十一点): 2点至10点的牌以牌面的点数计算,J、Q、K 每张为10点,A可记为1点或为11点。这里为了方便,我们只把A记为1点。

deck[:13]
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张牌:

def draw(deck, n=5):
    return deck.sample(n)
draw(deck)
7H     7
6D     6
AC     1
JH    10
JS    10
dtype: int64

假设我们想要从每副花色中随机抽取两张,花色是每张牌名字的最后一个字符(即H, S, C, D),我们可以根据花色分组,然后使用apply:

get_suit = lambda card: card[-1] # last letter is suit
deck.groupby(get_suit).apply(draw, n=2)
C  QC    10
   9C     9
D  3D     3
   JD    10
H  KH    10
   6H     6
S  3S     3
   7S     7
dtype: int64

另外一种写法:

deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
7C     7
KC    10
AD     1
4D     4
AH     1
8H     8
7S     7
9S     9
dtype: int64

5 Example: Group Weighted Average and Correlation(例子:组加权平均和相关性)

在groupby的split-apply-combine机制下,DataFrame的两列或两个Series,计算组加权平均(Group Weighted Average)是可能的。这里举个例子,下面的数据集包含组键,值,以及权重:

df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
                                'b', 'b', 'b', 'b'],
                   'data': np.random.randn(8),
                   'weights': np.random.rand(8)})
df

按category分组来计算组加权平均:

grouped = df.groupby('category')
get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
grouped.apply(get_wavg)
category
a    0.695189
b   -0.399497
dtype: float64

另一个例子,考虑一个从Yahoo!财经上得到的经济数据集,包含一些股票交易日结束时的股价,以及S&P 500指数(即SPX符号):

标准普尔500指数英文简写为S&P 500 Index,是记录美国500家上市公司的一个股票指数。这个股票指数由标准普尔公司创建并维护。

标准普尔500指数覆盖的所有公司,都是在美国主要交易所,如纽约证券交易所、Nasdaq交易的上市公司。与道琼斯指数相比,标准普尔500指数包含的公司更多,因此风险更为分散,能够反映更广泛的市场变化。

close_px = pd.read_csv('../examples/stock_px_2.csv', parse_dates=True,
                       index_col=0)
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
close_px[-4:]

一个比较有意思的尝试是计算一个DataFrame,包括与SPX这一列逐年日收益的相关性(计算百分比变化)。一个可能的方法是,我们先创建一个能计算不同列相关性的函数,然后拿每一列与SPX这一列求相关性:

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

然后我们通过pct_change在close_px上计算百分比的变化:

rets = close_px.pct_change().dropna()

最后,我们按年来给这些百分比变化分组,年份可以从每行的标签中通过一个一行函数提取,然后返回的结果中,用datetime标签来表示年份:

get_year = lambda x: x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)

我们也可以计算列内的相关性。这里我们计算苹果和微软每年的相关性:

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

6 Example: Group-Wise Linear Regression(例子:组对组的线性回归)

就像上面介绍的例子,使用groupby可以用于更复杂的组对组统计分析,只要函数能返回一个pandas对象或标量。例如,我们可以定义regress函数(利用statsmodels库),在每一个数据块(each chunk of data)上进行普通最小平方回归(ordinary least squares (OLS) regression)计算:

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上做线性回归:

by_year.apply(regress, 'AAPL', ['SPX'])

阅读量:292

点赞量:0

收藏量:0