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()
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:]
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 |
现在,如果我们按smoker
分组,然后用apply
来使用这个函数,我们能得到下面的结果:
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 |
我们来解释下上面这一行代码发生了什么。这里的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')
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 |
除了上面这些基本用法,要想用好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')
smoker | No | Yes |
---|---|---|
count | 151.000000 | 93.000000 |
mean | 0.159328 | 0.163196 |
std | 0.039910 | 0.085119 |
min | 0.056797 | 0.035638 |
25% | 0.136906 | 0.106771 |
50% | 0.155625 | 0.153846 |
75% | 0.185014 | 0.195059 |
max | 0.291990 | 0.710345 |
在GroupBy
内部,当我们想要调用一个像describe
这样的函数的时候,其实相当于下面的写法:
f = lambda x: x.describe()
grouped.apply(f)
在接下来的例子,我们会看到作为结果的对象有一个多层级索引(hierarchical index
),这个多层级索引是由原来的对象中,组键(group key
)在每一部分的索引上得到的。我们可以在groupby
函数中设置group_keys=False
来关闭这个功能:
tips.groupby('smoker', group_keys=False).apply(top)
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 |
149 | 7.51 | 2.00 | No | Thur | Lunch | 2 | 0.266312 |
232 | 11.61 | 3.39 | No | Sat | Dinner | 2 | 0.291990 |
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 |
在第八章中,我们介绍了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()
data1 | data2 | |
---|---|---|
0 | 0.723973 | 0.120216 |
1 | 2.053617 | 0.468000 |
2 | -0.543073 | -1.874073 |
3 | -0.915136 | 0.159179 |
4 | 0.775965 | 0.105447 |
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()
count | max | mean | min | |
---|---|---|---|---|
data1 | ||||
(-3.0139, -1.407] | 70.0 | 2.035166 | 0.113238 | -2.363707 |
(-1.407, 0.194] | 481.0 | 3.284688 | -0.044535 | -2.647341 |
(0.194, 1.795] | 407.0 | 2.402272 | -0.043887 | -2.898145 |
(1.795, 3.395] | 42.0 | 2.051843 | 0.095178 | -2.234979 |
也有相同长度的桶(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()
count | max | mean | min | |
---|---|---|---|---|
data1 | ||||
0 | 100.0 | 2.178653 | 0.078390 | -2.363707 |
1 | 100.0 | 3.284688 | -0.018699 | -2.647341 |
2 | 100.0 | 2.214011 | -0.066341 | -2.262063 |
3 | 100.0 | 2.880188 | -0.014041 | -2.475753 |
4 | 100.0 | 2.741344 | -0.007952 | -2.576095 |
5 | 100.0 | 2.346857 | -0.109602 | -2.898145 |
6 | 100.0 | 2.402272 | 0.004522 | -1.911955 |
7 | 100.0 | 2.351513 | -0.161472 | -2.640625 |
8 | 100.0 | 2.135995 | -0.016079 | -1.986676 |
9 | 100.0 | 2.051843 | 0.037685 | -2.513164 |
对于pandas
的Categorical
类型,会在第十二章做详细介绍。
在处理缺失值的时候,一些情况下我们会直接用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
假设我们想要从一个很大的数据集里随机抽出一些样本,这里我们可以在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
在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 | data | weights | |
---|---|---|---|
0 | a | 0.098020 | 0.008455 |
1 | a | 1.389496 | 0.826219 |
2 | a | 0.202869 | 0.258955 |
3 | a | -0.242403 | 0.470473 |
4 | b | -0.820507 | 0.628758 |
5 | b | 0.866326 | 0.653632 |
6 | b | -1.297375 | 0.639703 |
7 | b | 0.525019 | 0.012664 |
按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()
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:]
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 |
一个比较有意思的尝试是计算一个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)
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
就像上面介绍的例子,使用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'])
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 |