《利用Python进行数据分析》第9章 分组级运算和转换笔记

分组级运算和转换

聚合是分组运算的其中一种。它是数据转换的一个特例,它接受能够将一维数组简化为标量值的函数。

接下来将介绍transform和apply方法,它们能够执行更多其他的分组运算。

如果要为一个DataFrame添加一个用于存放各索引分组平均值的列。一个办法是先聚合再合并

df=DataFrame({'key1':['a','a','b','b','a'],'key2':['one','two','one','two','one'],
              'data1':np.random.randn(5),'data2':np.random.randn(5)})
df
k1_means=df.groupby('key1').mean().add_prefix('mean_')
k1_means
pd.merge(df,k1_means,left_on='key1',right_index=True)

该过程看做利用np.mean函数对两个数据列进行转换,我们将在GroupBy上使用transform方法

people=DataFrame(np.random.randn(5,5),columns=['a','b','c','d','e'],
                index=['Joe','Steve','Wes','Jim','Travis'])
people.loc[2:3,['b','c']]=np.nan #添加NA值
people
key=['one','two','one','two','one']
people.groupby(key).mean()
people.groupby(key).transform(np.mean)

transform会将一个函数应用到各个分组,然后将结果放置到适当的位置上。如果各分组产生的是一个标量值,则该值就会被广播出去。现在,假设你希望从各组中减去平均值。为此,我们先创建一个距平化函数(demeaning function),然后将其传给transform

def demean(arr):
    return arr-arr.mean()
demeaned=people.groupby(key).transform(demean)
demeaned

检查一下demeaned现在的分组平均值是否为0

demeaned.groupby(key).mean()

apply:一般性的“拆分-应用-合并”

跟aggregate一样,transform也是一个有着严格条件的特殊函数:传入的函数只能产生两种结果,要么产生一个可以广播的标量值(如np.mean),要么产生一个相同大小的结果数组。最一般化的GroupBy方法是apply。

回到之前那个小费数据集,假设你想要根据分组选出最高的5个tip_pct值。首先,编写一个选取指定列具有最大值的行的函数

def top(df, n=5, column='tip_pct'):
    return df.sort_index(by=column)[-n:]
    
top(tips, n=6)
e:\python\lib\site-packages\ipykernel_launcher.py:2: FutureWarning: by argument to sort_index is deprecated, please use .sort_values(by=...)

sort_index is deprecated,现在使用新的sort_values

如果对smoker分组并用该函数调用apply,就会得到

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

top函数在DataFrame的各个片段上调用,然后结果由pandas.concat组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次化索引,其内层索引值来自原DataFrame。

如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入

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

之前在GroupBy对象上调用过describe

result=tips.groupby('smoker')['tip_pct'].describe()
result
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

在GroupBy中,当你调用诸如describe之类的方法时,实际上只是应用了下面两条代码的快捷方式而已

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

禁止分组键

从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果

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

分位数和桶分析

以下面这个简单的随机数据集为例,我们利用cut将其装入长度相等的桶中

frame=DataFrame({'data1':np.random.randn(1000),'data2':np.random.randn(1000)})
factor=pd.cut(frame.data1,4)
factor[:8]
0     (-0.131, 1.515]
1    (-3.432, -1.778]
2      (1.515, 3.162]
3     (-0.131, 1.515]
4     (-0.131, 1.515]
5      (1.515, 3.162]
6    (-1.778, -0.131]
7    (-1.778, -0.131]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-3.432, -1.778] < (-1.778, -0.131] < (-0.131, 1.515] < (1.515, 3.162]]

由cut返回的Factor对象可直接用于groupby。因此,我们可以像下面这样对data2做一些统计计算

def get_stats(group):
    return {'min':group.min(),'max':group.max(),
           'count':group.count(),'min':group.mean()}

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

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

grouping=pd.qcut(frame.data1,10,labels=False)# 返回分位数编号
grouped=frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()

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

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

s=Series(np.random.randn(6))
s[::2]=np.nan #插入NA值
s
0         NaN
1   -1.522965
2         NaN
3    0.500331
4         NaN
5   -0.981807
dtype: float64
s.fillna(s.mean())
0   -0.668147
1   -1.522965
2   -0.668147
3    0.500331
4   -0.668147
5   -0.981807
dtype: float64

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

states=['Ohio','New York','Vermont','Florida','Oregon','Nevada','California','Idaho']
group_key=['East']*4+['West']*4
data=Series(np.random.randn(8),index=states)
data[['Vermont','Nevada','Idaho']]=np.nan
data
Ohio          0.255096
New York      0.509371
Vermont            NaN
Florida       0.658680
Oregon        0.475809
Nevada             NaN
California    1.298450
Idaho              NaN
dtype: float64
data.groupby(group_key).mean()
East    0.474382
West    0.887130
dtype: float64

用分组平均值去填充NA值

fill_mean=lambda g:g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio          0.255096
New York      0.509371
Vermont       0.474382
Florida       0.658680
Oregon        0.475809
Nevada        0.887130
California    1.298450
Idaho         0.887130
dtype: float64

也可以在代码中预定义各组的填充值。由于分组具有一个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.255096
New York      0.509371
Vermont       0.500000
Florida       0.658680
Oregon        0.475809
Nevada       -1.000000
California    1.298450
Idaho        -1.000000
dtype: float64

示例:随机采样和排列

假设你想要从一个大数据集中随机抽取样本以进行蒙特卡罗模拟(Monte Carlo simulation)或其他分析工作。“抽取”的方式有很多,其中一些的效率会比其他的高很多。一个办法是,选取np.random.permutation(N)的前K个元素,其中N为完整数据的大小,K为期望的样本大小。作为一个更有趣的例子,下面是构造一副英语型扑克牌的一个方式

# 红桃(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 = Series(card_val, index=cards)

现在我有了一个长度为52的Series,其索引为牌名,值则是21点或其他游戏中用于计分的点数(为了简单起见,我当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.take(np.random.permutation(len(deck))[:n])
draw(deck)
6S     6
2C     2
6D     6
6C     6
QC    10
dtype: int64

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

get_suit=lambda card: card[-1]# 只要最后一个字母就可以了
deck.groupby(get_suit).apply(draw,n=2)
C  4C      4
   9C      9
D  4D      4
   10D    10
H  9H      9
   4H      4
S  10S    10
   2S      2
dtype: int64

也可以这样写

deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
7C      7
6C      6
7D      7
9D      9
10H    10
6H      6
7S      7
5S      5
dtype: int64

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

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

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.548882
b   -0.748385
dtype: float64

看一个稍微实际点的例子——来自Yahoo!Finance的数据集,其中含有标准普尔500指数(SPX字段)和几只股票的收盘价

close_px=pd.read_csv('pydata_book/ch09/stock_px.csv',parse_dates=True,index_col=0)
close_px[:10]
close_px[-4:]

做一个比较有趣的任务:计算一个由日收益率(通过百分数变化计算)与SPX之间的年度相关系数组成的DataFrame。下面是一个实现办法

rets = close_px.pct_change().dropna()
spx_corr = lambda x: x.corrwith(x['SPX'])
by_year = rets.groupby(lambda x: x.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

示例:面向分组的线性回归

顺着上一个例子继续,你可以用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收益率的线性回归,进行执行下面的

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

透视表和交叉表

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

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

 tips.pivot_table(index=['sex', 'smoker'])

现在,假设我们只想聚合tip_pct和size,而且想根据day进行分组。我将smoker放到列上,把day放到行上

注意:第一版的rows和cols报错警告,需要修改为index和columns
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
                  columns='smoker')

传入margins=True添加分项小计。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计。在下面这个例子中,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行)

tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'],
                   columns='smoker', margins=True)

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

tips.pivot_table('tip_pct',index=['sex','smoker'],
                            columns='day',aggfunc=len,margins=True)

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

tips.pivot_table('size',index=['time','sex','smoker'],
                              columns='day',aggfunc=sum,fill_value=0)

交叉表:crosstab 缺少数据就没有进行练习了,自己另外学习。

总结:本章知识比较多,不能很快的掌握,接下来需要复习再进行下一章的时间序列学习。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容