数据聚合与分组操作

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
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容