通用拆分-应用-联合
本文中使用的数据集来自: 《利用python进行数据分析》数据集
经过了前两节对分组和聚合的学习,现在来进行数据拆分和聚合的小实战。现在以资源文件中的tips.csv数据集为例,首先对文件进行读取,并添加一个tip_pct(小费百分比)列:
import pandas as pd
import numpy as np
tips = pd.read_csv('../pydata-book-2nd-edition/examples/tips.csv')
tips['tip_pct'] = tips['tip']/tips['total_bill']
print(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,colunms = 'tip_cpt'):
"""
返回最大的前若干项
:param df: 数据集DataFrame
:param n: 项数
:param colunms:具体的排列数
:return:排列好的数据,DataFrame形式
"""
return df.sort_values(by = colunms)[-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
如果我们先按照smoker分组,之后给apply方法中传入top函数,则会得到以下结果:
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
该过程可以看做先讲分好的组分别使用top函数,之后使用pandas中的cancat将得到的结果黏在一起,并使用分组名作为分组的标签。
若果想传递其他关键则,则可以把这些放在函数的最后进行传递:
print(tips.groupby(['smoker','day']).apply(top,n=1,colunms='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
压缩分组键
在之前的例子中,得到的结果的行索引都是“分组名+起始的默认索引”,如果我们想要去掉作为第一层索引的分组名索引,可以给groupby函数中传入group_keys=False来实现:
print(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
print(tips.groupby('smoker',as_index=False).apply(top))
# 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
# 149 7.51 2.00 No Thur Lunch 2 0.266312
# 232 11.61 3.39 No Sat Dinner 2 0.291990
# 1 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
分位数与桶分析
我们之前介绍过cut和qcut方法,可以将数据按照箱位或者分位数进行分桶。
cut与qcut函数的使用介绍(分箱)。
与groupby方法一起使用这些函数可以对数据集更方便的进行分箱或者分为分析。考虑下面这个DataFrame:
frame = pd.DataFrame({'data1':np.random.randn(1000),
'data2':np.random.randn(1000)})
我们使用cut函数将DataFrame中的data1按等长桶分为四份,以Categorical对象的形式返回。我们可以给groupby方法传入Categorical对象来进行对data2数据的分组,之后使用设定好的函数来进行数据的聚合:
quartiles = pd.cut(frame['data1'],4)
print(quartiles[:5])
# 0 (-1.466, -0.000143]
# 1 (-0.000143, 1.465]
# 2 (-0.000143, 1.465]
# 3 (-0.000143, 1.465]
# 4 (-0.000143, 1.465]
# Name: data1, dtype: category
# Categories (4, interval[float64]): [(-2.937, -1.466] < (-1.466, -0.000143] < (-0.000143, 1.465] <
# (1.465, 2.931]]
def get_states(group):
return {
'min':group.min(),'max':group.max(),
'count':group.count(),'mean':group.mean()
}
grouped = frame['data2'].groupby(quartiles)
print(grouped.apply(get_states).unstack())
# count max mean min
# data1
# (-2.937, -1.466] 68.0 2.015287 0.204690 -1.832583
# (-1.466, -0.000143] 414.0 3.048387 -0.028398 -3.179754
# (-0.000143, 1.465] 449.0 2.761953 -0.001406 -3.111809
# (1.465, 2.931] 69.0 2.464505 -0.016313 -2.147503
另外如果我们想要将其分为若干个等大小的组,可以使用qcut函数,在此不再赘述。
使用指定分组值填充缺失值
在处理缺失值时,我们可以使用dropna来去除缺失值或者使用fillna来替换缺失值。
常用缺失值处理函数介绍
假设我们有一个含有缺失值的Series:
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
我们可以使用该Series的平均值来替换Series中的缺失值:
s.fillna(s.mean())
如果你想要按组来将缺失值替换为不同的值,比如下面这个例子中,包含4个东部城市和4个西部城市,我们想要将东部城市的缺失值用东部城市的平均值替换,将西部城市中的缺失值使用西部的平均值进行替换:
import pandas as pd
import numpy as np
np.random.seed(1234)
states = ['Ohio','New York','Vermont','Florida',
'Oregon','Nevada','California','Idaho']
group_key = ['East'] * 4 + ['west'] * 4
# ['East', 'East', 'East', 'East', 'west', 'west', 'west', 'west']
data = pd.Series(np.random.randn(8),index=states)
data[["Vermont",'Nevada','Idaho']] = np.nan
# Ohio 0.471435
# New York -1.190976
# Vermont NaN
# Florida -0.312652
# Oregon -0.720589
# Nevada NaN
# California 0.859588
# Idaho NaN
# dtype: float64
想要达到之前的目的,我们可以进行以下的操作来实现:
print(data.groupby(group_key).mean())
# East -0.344064
# west 0.069500
# dtype: float64
fill_mean = lambda g:g.fillna(g.mean())
print(data.groupby(group_key).apply(fill_mean))
# Ohio 0.471435
# New York -1.190976
# Vermont -0.344064
# Florida -0.312652
# Oregon -0.720589
# Nevada 0.069500
# California 0.859588
# Idaho 0.069500
# dtype: float64
这里发生了什么?可以分解为以下步骤:
- 首先goupby函数使用group_key将data分为两组
- g.mean()计算每组的平均值,返回一个Series
- 将第二部的Series传入g.fillna(),两个组分别将缺失值替换为对应的平均值
- 将两个组使用concat进行拼接
随机采样与排列
我们可以使用DataFrame和Series中的sample方法来进行样本的抽取,具体用法见:抽样和置换。
这里说明一个具体的小应用,假设我们有一副52张的扑克牌(Series):
# 花色
suits = ['H','S','C','D']
card_val = (list(range(1,11)) + [10] * 3) * 4
base_names = ['A']+list(range(2,11))+['J','Q','K']
cards = []
for suit in suits:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val,index=cards)
print(deck.head(10))
# AH 1
# 2H 2
# 3H 3
# 4H 4
# 5H 5
# 6H 6
# 7H 7
# 8H 8
# 9H 9
# 10H 10
# dtype: int64
从这副牌中拿出5张牌,可以写为:
print(deck.sample(5))
# 9H 9
# 10C 10
# KC 10
# 3S 3
# QH 10
# dtype: int64
如果你想要从每个花色中各抽出2张牌,可以进行如下操作:
def draw(deck,n = 5):
return deck.sample(n)
get_suit = lambda card: card[-1] #获取花色
print(deck.groupby(get_suit).apply(draw,n = 2))
# C 5C 5
# JC 10
# D 7D 7
# AD 1
# H QH 10
# 4H 4
# S 5S 5
# 10S 10
# dtype: int64
如果我们不想要一级行索引,可以写为:
print(deck.groupby(get_suit,group_keys=False).apply(draw,n = 2))
# 2C 2
# 5C 5
# 10D 10
# AD 1
# 3H 3
# 4H 4
# 4S 4
# 5S 5
# dtype: int64
分组加权平均和相关性
考虑下面这个DataFrame:
np.random.seed(1234)
df = pd.DataFrame({'category':list('aaaabbbb'),
'data':np.random.randn(8),
'weight':np.random.rand(8)})
# category data weight
# 0 a 0.471435 0.958139
# 1 a -1.190976 0.875933
# 2 a 1.432707 0.357817
# 3 a -0.312652 0.500995
# 4 b -0.720589 0.683463
# 5 b 0.887163 0.712702
# 6 b 0.859588 0.370251
# 7 b -0.636524 0.561196
我们现在想要将数据分为a组和b组,并计算出各自的加权平均和,那么我们就可以进行如下的操作:
get_wavg = lambda g:np.average(g['data'],weights=g['weight'])
print(df.groupby(df['category']).apply(get_wavg))
# category
# a -0.087454
# b 0.043322
# dtype: float64
为了进一步说明,这里我们使用另外一个从雅虎财经上获取的数据集(已附在文章开头):
close_px = pd.read_csv('../pydata-book-2nd-edition/examples/stock_px_2.csv',parse_dates=True,index_col=0)
# AAPL MSFT XOM SPX
# 2003-01-02 7.40 21.11 29.22 909.03
# 2003-01-03 7.45 21.14 29.24 908.59
# 2003-01-06 7.45 21.52 29.96 929.01
# 2003-01-07 7.43 21.93 28.95 922.93
# 2003-01-08 7.28 21.31 28.83 909.93
这个数据集中,AAPL、MSFT和XOM是三个公司的某种财经指数(不懂),SPX是当时的标普指数。假设我们想要计算出各数据的变化百分比,之后按年进行分类,计算每年各公司的财经指数和标普指数的相关性,我们可以进行如下的操作:
rets =close_px.pct_change().dropna()
# 计算变化百分比
get_year = lambda x:x.year
#用于得到每组数据的所在年份
spx_corr = lambda x:x.corrwith(x['SPX'])
#用于计算与SPX指数的相关性
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