GroupBy机制
df = pd.DataFrame({
"key1":["a","a",None,"b","b","a",None],
"key2":pd.Series([1,2,1,2,1,None,1],dtype="Int64"),
"data1":np.random.standard_normal(7),
"data2":np.random.standard_normal(7)
})
# key1 key2 data1 data2
# 0 a 1 -1.842437 -0.016600
# 1 a 2 -0.291012 0.220026
# 2 None 1 -0.125414 0.081025
# 3 b 2 0.649559 -0.508579
# 4 b 1 -0.732733 0.699048
# 5 a <NA> 1.480944 2.187330
# 6 None 1 1.871076 -0.693208
# 按key1标签进行分组,并计算data1列的平均值
grouped = df["data1"].groupby(df["key1"])
grouped.mean()
# key1
# a -1.235914
# b 1.085771
# Name: data1, dtype: float64
# 多个列分组
means = df["data1"].groupby([df["key1"],df["key2"]]).mean()
# key1 key2
# a 1 0.132065
# 2 -1.321177
# b 1 1.480566
# 2 -0.052706
# Name: data1, dtype: float64
# 分组键可以是长度正确的任意数组
states = np.array(["OH","CA","CA","OH","OH","CA","OH"])
years = [2005,2005,2006,2005,2006,2005,2006]
df["data1"].groupby([states,years]).mean()
# CA 2005 -0.290733
# 2006 -0.482917
# OH 2005 -0.090698
# 2006 0.238132
# Name: data1, dtype: float64
# 按column分组
df.groupby("key1").mean()
# key2 data1 data2
# key1
# a 1.5 -0.630558 0.976913
# b 1.5 0.827966 0.228338
df.groupby(["key1","key2"]).mean()
data1 data2
# key1 key2
# a 1 -0.370229 -0.163776
# 2 0.150907 -0.032159
# b 1 -0.154925 -0.202077
# 2 0.123678 -0.650102
# 每个分组中非空值的数量
df.groupby("key1").count()
# key2 data1 data2
# key1
# a 2 3 3
# b 2 2 2
注意:
- 任何分组关键字中的缺失值,默认都会从结果中除去。向groupby中传入dropna=False可以禁用该功能
对分组进行迭代
groupby返回的对象支持迭代,可以产生一个二元组构成的序列,每个元组包含分组名和数据块。
for name,group in df.groupby("key1"):
print(name)
print(group)
# a
# key1 key2 data1 data2
# 0 a 1 -0.590997 -1.408358
# 1 a 2 0.595487 -0.027094
# 5 a <NA> -0.401320 -0.061207
# b
# key1 key2 data1 data2
# 3 b 2 -0.469014 -0.516659
# 4 b 1 -1.637888 1.320523
# 多个分组键的情况,元组的第一个元素是由键值组成的元组
for(k1,k2),group in df.groupby(["key1","key2"]):
print((k1,k2))
print(group)
# 你可以对这些数据块做任何操作
# 用一行代码计算得到一个包含这些数据块的字典
peices = {name:group for name,group in df.groupby("key1")}
print(peices["b"])
# key1 key2 data1 data2
# 3 b 2 -0.757362 -0.051297
# 4 b 1 0.252740 -1.139422
# groupby默认是在axis="index"上进行分组的,通过设置也可以在任意其他轴上进行分组。
# 还是以df为例,我们可以根据其列是以"key"还是"data"开头进行分组:
grouped = df.groupby({
"key1":"key","key2":"key","data1":"data","data2":"data"
},axis="columns")
for group_key,group_values in grouped:
print(group_key)
print(group_values)
# data
# data1 data2
# 0 -0.955631 0.789985
# 1 0.714670 0.010806
# 2 0.828098 -1.038737
# 3 1.491584 -1.024496
# 4 1.554073 0.300483
# 5 -2.235472 0.699514
# 6 0.557364 -0.930734
# key
# key1 key2
# 0 a 1
# 1 a 2
# 2 None 1
# 3 b 2
# 4 b 1
# 5 a <NA>
# 6 None 1
选取一列或多列
使用单个列名或列名数组对由DataFrame创建的GroupBy对象建立索引,能实现选取部分列并进行聚合的效果。这表明:
df.groupby("key1")["data1"]
df.groupby("key1")["data2"]
是以下代码的语法糖:
df["data1"].groupby(df["key1"])
df["data2"].groupby(df["key1"])
尤其是对于大型数据集,可能只想对部分列进行聚合。
df.groupby(["key1","key2"])[["data2"]].mean()
# data2
# key1 key2
# a 1 0.613240
# 2 -0.817076
# b 1 -0.512730
# 2 0.384858
# 如果传入的是列表或数组,则该索引操作所返回的对象是一个经过分组的DataFrame。如果传入的是标量形式的单个列名,则返回的对象为经过分组的Series:
df.groupby(["key1","key2"])["data2"].mean()
# key1 key2
# a 1 1.328521
# 2 0.969998
# b 1 -0.650482
# 2 -0.597830
利用字典和Series进行分组
除了数组,分组信息还能以其他形式存在。
people = pd.DataFrame(np.random.standard_normal((5,5)),
columns=["a","b","c","d","e"],
index=["Joe","Steve","Wanda","Jill","Trey"])
people.iloc[2:3,[1,2]] = np.nan # 加入一些空值
# 现在,假设已知列的分组关系,并希望根据分组计算列的和
mapping = {"a":"red","b":"red","c":"blue","d":"blue","e":"red","f":"orange"}
by_column = people.groupby(mapping,axis="columns")
by_column.sum()
# blue red
# Joe -0.611732 2.840763
# Steve -0.612256 -0.763678
# Wanda -0.453089 1.219856
# Jill -0.853819 0.354937
# Trey -0.479482 -0.053055
# Series也有同样的功能,可以看作固定大小的映射:
map_series = pd.Series(mapping)
people.groupby(map_series,axis="columns").count()
# blue red
# Joe 2 3
# Steve 2 3
# Wanda 1 2
# Jill 2 3
# Trey 2 3
利用函数进行分组
与使用字典或Series相比,使用Python函数是一种更加通用的定义分组映射的方式。任何作为分组键的函数都会在各个索引值上调用一次(如果使用axis="columns",则在各个列上调用一次函数),其返回值会用作分组名称.
# 人名长度分组
people.groupby(len).sum()
# a b c d e
# 3 -0.875556 -0.349531 -0.741007 0.496641 0.158738
# 4 -0.921963 -0.402008 -0.301925 2.230507 1.997662
# 5 -1.963974 0.445536 -0.387958 -0.152275 -0.234205
# 将函数与数组、列表、字典、Series混合使用也不是问题,因为它们在内部都会转换为数组:
key_list = ["one","one","one","two","two"]
people.groupby([len,key_list]).min()
# a b c d e
# 3 one 1.128153 0.881510 0.223396 0.782630 -0.255057
# 4 two -0.178333 -2.135464 -1.777221 0.609309 0.235076
# 5 one -0.557456 0.002784 -1.505727 0.234601 -0.943558
根据索引层级分组
层次化索引数据集最方便的地方,就在于它能够根据轴索引的某个层级进行聚合。
columns = pd.MultiIndex.from_arrays([["US","US","US","JP","JP"],[1,3,5,1,3]],names=["cty","tenor"])
hier_df = pd.DataFrame(np.random.standard_normal((4,5)),columns=columns)
# cty US JP
# tenor 1 3 5 1 3
# 0 0.328601 -0.016273 0.308845 1.986743 0.084815
# 1 -0.854281 -0.364848 0.067584 -0.513686 1.095886
# 2 1.259367 -0.232503 -2.478109 0.044573 0.941515
# 3 1.212351 1.358163 0.461287 -1.198118 -1.183688
# 要根据层级分组,可以将层级数值或名称传递给level关键字
hier_df.groupby(level="cty",axis="columns").count()
# cty JP US
# 0 2 3
# 1 2 3
# 2 2 3
# 3 2 3
数据聚合
聚合指的是能够从数组产生标量值的任何数据转换过程。之前的例子使用过一些聚合运算,包括mean、count、min和sum。
经过优化的groupby方法:
- any、all:如果存在非空值或全部为非NA值,则返回True
- count:非NA值的数量
- cummin、cummax:非NA值的累计最小值和最大值
- cumsun:非NA值的累计和
- cumprod:非NA值的累计乘积
- first、last:第一个非NA值和最后一个非NA值
- mean:非NA值的平均值
- median:非NA值的算术中位数
- min、max:非NA值的最小值和最大值
- nth:若数据为有序排列时,返回位置n处的值
- ohlc:对于时间序列类型数据,计算“开一高一低-收”四个统计值
- prod:非NA值的乘积
- quantile:计算样本分位数
- rank:非NA值的顺序排列,比如调用Series.rank
- size:计算分组大小,返回的结果为Series
- sum:非NA值的和
- std、var:样本标准差和方差
你可以使用自己定制的聚合算法,还可以调用分组对象上已经定义好的任何方法。例如,Series的nsmallest方法可以计算数据中的若干最小值。虽然nsmallest并不是特意为GroupBy实现的,没有经过实现优化,但仍然可以用于GroupBy对象。由表及里,GroupBy实际上是先对Series进行切片,然后对各片调用piece.nsmallest(n),最后将这些结果组装成最终结果:
df = pd.DataFrame({
"key1":["a","a",None,"b","b","a",None],
"key2":pd.Series([1,2,1,2,1,None,1],dtype="Int64"),
"data1":np.random.standard_normal(7),
"data2":np.random.standard_normal(7)
})
grouped = df.groupby("key1")
grouped["data1"].nsmallest(2) # 选取最小的两个
# a 0 -2.196496
# 1 -1.879652
# b 4 -1.525838
# 3 -0.495919
# 使用自己的聚合函数
def peak_to_peak(arr):
return arr.max()-arr.min()
grouped.agg(peak_to_peak)
# key2 data1 data2
# key1
# a 1 2.041888 1.299230
# b 1 1.079285 0.315401
自定义的聚合函数通常要比表10-1中经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排)。
逐列操作和多函数应用
tips = pd.read_csv("tips.csv")
tips.head()
# total_bill tip sex smoker day time size
# 0 16.99 1.01 Female No Sun Dinner 2
# 1 10.34 1.66 Male No Sun Dinner 3
# 2 21.01 3.50 Male No Sun Dinner 3
# 3 23.68 3.31 Male No Sun Dinner 2
# 4 24.59 3.61 Female No Sun Dinner 4
# 用小费占总账单的百分比新增一个列tip_pct:
tips["tip_pct"] = tips["tip"]/tips["total_bill"]
# 一次应用多个函数
grouped = tips.groupby(["day","smoker"])
grouped_pct = grouped["tip_pct"]
grouped_pct.agg("mean")
# day smoker
# Fri No 0.151650
# Yes 0.174783
# Sat No 0.158048
# Yes 0.147906
# Sun No 0.160113
# Yes 0.187250
# Thur No 0.160298
# Yes 0.163863
# 传入的是函数或函数名的列表,得到的DataFrame的列就会以相应的函数命名
grouped_pct.agg(["mean","std",peak_to_peak])
# mean std peak_to_peak
# day smoker
# Fri No 0.151650 0.028123 0.067349
# Yes 0.174783 0.051293 0.159925
# Sat No 0.158048 0.039767 0.235193
# Yes 0.147906 0.061375 0.290095
# Sun No 0.160113 0.042347 0.193226
# Yes 0.187250 0.154134 0.644685
# Thur No 0.160298 0.038774 0.193350
# Yes 0.163863 0.039389 0.151240
# 别名
grouped_pct.agg([("average","mean"),("stdev",np.std)])
# average stdev
# day smoker
# Fri No 0.151650 0.028123
# Yes 0.174783 0.051293
# Sat No 0.158048 0.039767
# Yes 0.147906 0.061375
# Sun No 0.160113 0.042347
# Yes 0.187250 0.154134
# Thur No 0.160298 0.038774
# Yes 0.163863 0.039389
# 对tip_pct和total_bill列计算得到3个相同的统计值
functions=["count","mean","max"]
result = grouped[["tip_pct","total_bill"]].agg(functions)
# tip_pct total_bill
# count mean max count mean max
# day smoker
# Fri No 4 0.151650 0.187735 4 18.420000 22.75
# Yes 15 0.174783 0.263480 15 16.813333 40.17
# Sat No 45 0.158048 0.291990 45 19.661778 48.33
# Yes 42 0.147906 0.325733 42 21.276667 50.81
# Sun No 57 0.160113 0.252672 57 20.506667 48.17
# Yes 19 0.187250 0.710345 19 24.120000 45.35
# Thur No 45 0.160298 0.266312 45 17.113111 41.19
# Yes 17 0.163863 0.241255 17 19.190588 43.11
# 传入带有自定义名称的元组列表:
ftuples = [("Average","mean"),("Variance",np.std)]
grouped[["tip_pct","total_bill"]].agg(ftuples)
# tip_pct total_bill
# Average Variance Average Variance
# day smoker
# Fri No 0.151650 0.028123 18.420000 5.059282
# Yes 0.174783 0.051293 16.813333 9.086388
# Sat No 0.158048 0.039767 19.661778 8.939181
# Yes 0.147906 0.061375 21.276667 10.069138
# Sun No 0.160113 0.042347 20.506667 8.130189
# Yes 0.187250 0.154134 24.120000 10.442511
# Thur No 0.160298 0.038774 17.113111 7.721728
# Yes 0.163863 0.039389 19.190588 8.355149
# 单列或多列应用不同的函数
grouped.agg({"tip":np.max,"size":"sum"})
# tip size
# day smoker
# Fri No 3.50 9
# Yes 4.73 31
# Sat No 9.00 115
# Yes 10.00 104
# Sun No 6.00 167
# Yes 6.50 49
# Thur No 6.70 112
# Yes 5.00 40
grouped.agg({"tip_pct":["min","max","mean","std"],"size":"sum"})
# tip_pct size
# min max mean std sum
# day smoker
# Fri No 0.120385 0.187735 0.151650 0.028123 9
# Yes 0.103555 0.263480 0.174783 0.051293 31
# Sat No 0.056797 0.291990 0.158048 0.039767 115
# Yes 0.035638 0.325733 0.147906 0.061375 104
# Sun No 0.059447 0.252672 0.160113 0.042347 167
# Yes 0.065660 0.710345 0.187250 0.154134 49
# Thur No 0.072961 0.266312 0.160298 0.038774 112
# Yes 0.090014 0.241255 0.163863 0.039389 40
返回不含行索引的聚合数据
向groupby传入as_index=False以禁用索引:
tips.groupby(["day","smoker"],as_index=False).mean()
Apply:通用的“拆分-应用-联合”范式
GroupBy方法中最通用的是apply方法,它是本节的重点。apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段拼接到一起。
def top(df,n=5,column="tip_pct"):
return df.sort_values(column,ascending=False)[:n]
# top(tips,n=6)
# 要根据分组选出最高的5个tip_pct值
tips.groupby("smoker").apply(top)
# total_bill tip sex smoker day time size tip_pct
# smoker
# No 232 11.61 3.39 Male No Sat Dinner 2 0.291990
# 149 7.51 2.00 Male No Thur Lunch 2 0.266312
# 51 10.29 2.60 Female No Sun Dinner 2 0.252672
# 185 20.69 5.00 Male No Sun Dinner 5 0.241663
# 88 24.71 5.85 Male No Thur Lunch 2 0.236746
# Yes 172 7.25 5.15 Male Yes Sun Dinner 2 0.710345
# 178 9.60 4.00 Female Yes Sun Dinner 2 0.416667
# 67 3.07 1.00 Female Yes Sat Dinner 1 0.325733
# 183 23.17 6.50 Male Yes Sun Dinner 4 0.280535
# 109 14.31 4.00 Female Yes Sat Dinner 2 0.279525
# 传递其他参数
tips.groupby(["smoker","day"]).apply(top,n=1,column="total_bill")
# total_bill tip sex smoker day time size tip_pct
# smoker day
# No Fri 94 22.75 3.25 Female No Fri Dinner 2 0.142857
# Sat 212 48.33 9.00 Male No Sat Dinner 4 0.186220
# Sun 156 48.17 5.00 Male No Sun Dinner 6 0.103799
# Thur 142 41.19 5.00 Male No Thur Lunch 5 0.121389
# Yes Fri 95 40.17 4.73 Male Yes Fri Dinner 4 0.117750
# Sat 170 50.81 10.00 Male Yes Sat Dinner 3 0.196812
# Sun 182 45.35 3.50 Male Yes Sun Dinner 3 0.077178
# Thur 197 43.11 5.00 Female Yes Thur Lunch 4 0.115982
禁用分组键
分组键会与原始对象各分块的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:
tips.groupby("smoker",group_keys=False).apply(top)
# total_bill tip sex smoker day time size tip_pct
# 232 11.61 3.39 Male No Sat Dinner 2 0.291990
# 149 7.51 2.00 Male No Thur Lunch 2 0.266312
# 51 10.29 2.60 Female No Sun Dinner 2 0.252672
# 185 20.69 5.00 Male No Sun Dinner 5 0.241663
# 88 24.71 5.85 Male No Thur Lunch 2 0.236746
# 172 7.25 5.15 Male Yes Sun Dinner 2 0.710345
# 178 9.60 4.00 Female Yes Sun Dinner 2 0.416667
# 67 3.07 1.00 Female Yes Sat Dinner 1 0.325733
# 183 23.17 6.50 Male Yes Sun Dinner 4 0.280535
# 109 14.31 4.00 Female Yes Sat Dinner 2 0.279525
分位数和桶分析
使用pandas.cut得到的等长桶分类:
frame= pd.DataFrame({"data1":np.random.standard_normal(1000),"data2":np.random.standard_normal(1000)})
quartiles = pd.cut(frame["data1"],4)
quartiles.head(10)
#0 (-1.387, 0.0764]
#1 (1.54, 3.003]
#3 (-1.387, 0.0764]
#4 (-1.387, 0.0764]
#5 (1.54, 3.003]
#6 (-2.856, -1.387]
#7 (0.0764, 1.54]
#8 (-1.387, 0.0764]
#9 (0.0764, 1.54]
#Name: data1, dtype: category
#Categories (4, interval[float64, right]): [(-2.856, -1.387] < (-1.387, 0.0764] < (0.0764, 1.54] < (1.54, 3.003]]
# 由cut返回的Categorical对象可直接传递给groupby。因此,我们可以如下计算分位数的分组统计集合:
def get_stats(group):
return pd.DataFrame({"min":group.min(),"max":group.max()
,"count":group.count(),"mean":group.mean()})
grouped = frame.groupby(quartiles)
grouped.apply(get_stats)
# min max count mean
# data1
# (-3.456, -1.938] data1 -3.449574 -2.008264 31 -2.369388
# data2 -1.730594 1.857899 31 0.069171
# (-1.938, -0.427] data1 -1.926157 -0.427000 280 -0.943627
# data2 -3.124731 3.179547 280 0.106341
# (-0.427, 1.085] data1 -0.423716 1.082533 546 0.277415
# data2 -3.056183 2.721023 546 0.043042
# (1.085, 2.596] data1 1.085348 2.595941 143 1.539440
# data2 -2.765398 2.886432 143 0.174136
grouped.agg(["min","max","count","mean"]) # 与grouped.apply(get_stats)相同
如果要根据样本分位数得到大小相等的桶,使用pandas.qcut即可。我们可以传入4作为桶的数量来计算样本分位数,传入labels=False以获取分位数的索引(而非区间):
quartiles_samp = pd.qcut(frame["data1"],4,labels=False)
grouped = frame.groupby(quartiles_samp)
grouped.apply(get_stats)
# min max count mean
# data1
# 0 data1 -2.629581 -0.665621 250 -1.258305
# data2 -2.439430 2.729825 250 0.057253
# 1 data1 -0.655866 0.043063 250 -0.286781
# data2 -3.021496 2.550985 250 0.026523
# 2 data1 0.044382 0.738915 250 0.368556
# data2 -2.291772 2.166378 250 -0.044903
# 3 data1 0.744181 3.395871 250 1.306751
# data2 -3.165680 2.635572 250 -0.005494
示例:用指定分组的值填充缺失值
在清洗缺失数据时,有时你会用dropna将其删除,而有时则可能想用固定值或由数据本身衍生出的值来填充空(NA)值。这时就得使用fillna这个工具了。在下面这个例子中,我用平均值填充空值:
s= pd.Series(np.random.standard_normal(6))
s[::2]=np.nan
# 0 NaN
# 1 1.275687
# 2 NaN
# 3 0.807253
# 4 NaN
# 5 -0.080000
# dtype: float64
s.fillna(s.mean)
假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数。
states = ["Ohio","New York","Vermont","Florida","Oregon","Nevada","California","Idaho"]
group_key = ["East","East","East","East","West","West","West","West"]
data = pd.Series(np.random.standard_normal(8),index=states)
# 设置缺失值
data[["Vermont","Nevada","Idaho"]]=np.nan
# 平均分组值填充NA值
def fill_mean(group):
return group.fillna(group.mean)
data.groupby(group_key).apply(fill_mean)
# 存在另外一种情况,你可能已经在代码中预设了针对各组的填充值。
fill_values = {"East":0.5,"West":-1}
def fill_func(group):
return group.fillna(fill_func[group.name])
data.groupby(group_key).apply(fill_func)
示例:随机采样和排列
假设你想要从一个大型数据集中随机抽取(进行替换或不替换)样本,以进行蒙特卡罗模拟(Monte Carlo simulation)或其他工作。“抽取”的方式有很多,这里采用的是Series的sample方法。
# 创建一副英氏扑克牌
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 suits:
cards.extend(str(num)+suit for num in base_names)
deck = pd.Series(card_val,index=cards)
# print(deck.head(13))
# 从整副牌中随机抽出5张
def draw(deck,n=5):
return deck.sample(n)
# print(draw(deck))
# 假设你想从每种花色中随机抽取两张牌。由于花色是牌名的最后一个字符,因此我们可以使用apply据此进行分组:
def get_suit(card):
return card[-1] # 最后一个字母是花色
deck.groupby(get_suit).apply(draw,n=2)
示例:分组加权平均和相关系数
根据groupby的“拆分-应用-联合”范式,可以进行DataFrame的列与列之间或两个Series之间的运算,比如分组加权平均。以下面这个数据集为例,它含有分组键、值以及一些权重值:
df = pd.DataFrame({"category":["a","a","a","a","b","b","b","b"],
"data":np.random.standard_normal(8),
"weights":np.random.uniform(size=8)})
# 利用category计算分组加权平均值:
grouped = df.groupby("category")
def get_wavg(group):
return np.average(group["data"],weights=group["weights"])
grouped.apply(get_wavg)
# category
# a 0.623906
# b -0.131110
书中还有另一个股票例子
示例:分组线性回归
延续上一个例子的主题,只要函数返回的是pandas对象或标量值,就可以用groupby执行更复杂的分组统计分析。例如,我可以定义如下的regress函数(利用statsmodels计量经济学库)对各数据块执行普通最小二乘(Ordinary Least Squares,OLS)回归:
import statsmodels.api as sm
def regress(data,yvar=None,xvars=None):
Y = data[yvar]
X = data[xvars]
X["intercept"]=1.
result = sm.OLS(Y,X).fit()
return result.params
# 为了计算AAPL对SPX收益率的年化线性回归,执行如下代码:
by_year.apply(regress,yvar="AAPL",xvars=["SPX"])
分组转换和“展开式”GroupBy运算
还有另一个内置的transform方法,它类似于apply,但对能使用的函数有更多限制:
- 生成标量值,能传播到分组形状
- 生成与输入分组具有相同形状的对象
- 无法修改输入
df = pd.DataFrame({"key":['a','b','c']*4,'value':np.arange(12.)})
# 按照键计算分组平均值
g = df.groupby('key')['value']
g.mean()
# key
# a 4.5
# b 5.5
# c 6.5
# 假设我们想创建一个Series,它的形状与df['value']相同,但值替换为按照'key'的分组平均值。
# 我们可以向transform传入一个计算单个分组平均值的函数:
def get_mean(group):
return group.mean()
g.transform(get_mean) # 展开式分组运算
# 0 4.5
# 1 5.5
# 2 6.5
# 3 4.5
# 4 5.5
# 5 6.5
# 6 4.5
# 7 5.5
# 8 6.5
# 9 4.5
# 10 5.5
# 11 6.5
# Name: value, dtype: float64
# 对于内置的聚合函数,我们可以像GroupBy的agg方法那样,传入函数的字符串别名:
g.transform('mean')
类似于apply,transform可以使用能返回Series的函数,但结果必须与输入具有相同的大小。
def times_two(group):
return group*2
g.transform(times_two)
# 0 0.0
# 1 2.0
# 2 4.0
# 3 6.0
# 4 8.0
# 5 10.0
# 6 12.0
# 7 14.0
# 8 16.0
# 9 18.0
# 10 20.0
# 11 22.0
# Name: value, dtype: float64
# 计算各个分组的降序排名
def get_ranks(group):
return group.rank(ascending=False)
g.transform(get_ranks)
# 0 4.0
# 1 4.0
# 2 4.0
# 3 3.0
# 4 3.0
# 5 3.0
# 6 2.0
# 7 2.0
# 8 2.0
# 9 1.0
# 10 1.0
# 11 1.0
# Name: value, dtype: float64
# 由简单聚合方法构成的分组转换函数
def normalize(x):
return (x-x.mean())/x.std()
g.transform(normalize)
# 0 -1.161895
# 1 -1.161895
# 2 -1.161895
# 3 -0.387298
# 4 -0.387298
# 5 -0.387298
# 6 0.387298
# 7 0.387298
# 8 0.387298
# 9 1.161895
# 10 1.161895
# 11 1.161895
# Name: value, dtype: float64
# 使用transform或apply都可以获得等价的结果
g.apply(normalize)
'mean'或'sum'等内置聚合函数通常比一般的apply函数快得多。这些函数在与transform配合使用时,也存在“高速运行路径”。我们借此可以实现展开式分组运算:
g.transform('mean')
normalize = (df['value']-g.transform('mean'))/g.transform('std')
透视表和交叉表
透视表是各种电子表格程序和其他数据分析软件中常见的数据汇总工具。它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到矩形区域中。在Python和pandas中,可以通过本章所介绍的groupby功能,结合使用层次化索引的重塑操作来制作透视表。DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除了为groupby提供便利的接口,pivot_table还可以添加分项汇总,也称作差额。
tips = pd.read_csv("tips.csv")
# 用小费占总账单的百分比新增一个列tip_pct:
tips["tip_pct"] = tips["tip"]/tips["total_bill"]
# total_bill tip sex smoker day time size tip_pct
# 0 16.99 1.01 Female No Sun Dinner 2 0.059447
# 1 10.34 1.66 Male No Sun Dinner 3 0.160542
# 2 21.01 3.50 Male No Sun Dinner 3 0.166587
# 3 23.68 3.31 Male No Sun Dinner 2 0.139780
# 4 24.59 3.61 Female No Sun Dinner 4 0.146808
# 假设你想计算分组平均数(pivot_table的默认聚合类型),并在行方向上根据day和smoker排列
tips.pivot_table(index=["day","smoker"]) # 也可以用group实现。 tips.groupby(["day","smoker"]).mean()
# 对tip_pct和size求平均值,并根据time进行分组。我把smoker放到列上,把time和day放到行上
tips.pivot_table(index=["time","day"],columns="smoker",values=["tip_pct","size"])
# 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的行和列,对应的值为单行或单列中所有数据的分组统计值
tips.pivot_table(index=["time","day"],columns="smoker",values=["tip_pct","size"], margins=True)
# 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
要使用mean以外的其他聚合函数,可以将其传给aggfunc关键字参数。例如,使用"count"或len("count"会排除分组数据计数中的空值,而len不会)可以得到有关分组大小的交叉表(计数或频率):
tips.pivot_table(index=["time","smoker"],columns="day",values="tip_pct", aggfunc=len, margins=True)
# day Fri Sat Sun Thur All
# time smoker
# Dinner No 3.0 45.0 57.0 1.0 106
# Yes 9.0 42.0 19.0 NaN 70
# Lunch No 1.0 NaN NaN 44.0 45
# Yes 6.0 NaN NaN 17.0 23
# All 19.0 87.0 76.0 62.0 244
如果某些组合为空(也就是NA),可以传入fill_value以填充值:
tips.pivot_table(index=["time","size","smoker"],columns="day",values="tip_pct",fill_value=0)
pivot_table选项:
- values:待聚合的列的名称。默认聚合所有数值列
- index:在结果透视表的行上进行分组的列名或其他分组键
- columns:在结果透视表的列上进行分组的列名或其他分组键
- aggfunc:聚合函数或函数列表(默认为"mean"):可以是groupby上下文中任意有效函数
- fill_value:用于替换结果表中的缺失值
- dropna:如果为True,则不添加条目都为NA的列
- margins:添加行/列小计和总计(默认为False)
- margins_name:传入margins=True时,差额行/列使用的名称(默认为“All")
- observed:对于Categorical分组键,如果为True,则只展示分组键中观测到的分类值,而非全部分类
交叉表:crosstab
交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频次的特殊透视表。
from io import StringIO
data = """mple Nationality Handedness
1 USA Right-handed
2 Japan Left-handed
3 USA Right-handed
4 Japan Right-handed
5 Japan Left-handed
6 Japan Right-handed
7 USA Right-handed
8 USA Left-handed
9 Japan Right-handed
10 USA Right-handed"""
data = pd.read_table(StringIO(data),sep="\s+")
# mple 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
# 根据国籍和用手习惯对这段数据进行统计汇总
pd.crosstab(data["Nationality"],data["Handedness"],margins=True)
# Handedness Left-handed Right-handed All
# Nationality
# Japan 2 3 5
# USA 1 4 5
# All 3 7 10
# crosstab的前两个参数可以是数组、Series或者数组列表
tips = pd.read_csv("tips.csv")
# 用小费占总账单的百分比新增一个列tip_pct:
tips["tip_pct"] = tips["tip"]/tips["total_bill"]
pd.crosstab([tips["time"],tips["day"]],tips["smoker"],margins=True)
# 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