利用Python进行数据分析-聚合与分组

  |  

摘要: 《利用Python进行数据分析》数据的聚合与分组的笔记

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


对数据集分类形成若干组,并在每一组上应用一个聚合函数或转换函数。是数据分析 workflow 的常用操作。

计算分组统计或数据透视表用于报告或可视化的目的。

Pandas 的 groupby 可以对数据集进行切片,切块和总结。

关系型数据库和 SQL 流行的原因也是对数据的连接、过滤、变换、聚合。但 SQL 在可以执行的组操作种类上有所限制。

  • 使用一个或多个键(函数、数组或DataFrame列名的形式)将 pandas 对象拆分为多块
  • 计算组汇总统计信息: 例如计数、平均值、标准偏差或用户自定义函数
  • 应用组内变换或其它操作: 例如标准化、线性回归、排位、子集选择
  • 计算数据透视表和交叉表
  • 分位数分析,其它统计组分析
  • 时间序列数据的聚合是 groupby 的特殊用法(重采样)

本章内容比其它章明显多,而且细,例子也很多。需要一个目录,如下

  • GroupBy 机制
    • 遍历各分组
    • 选择一列或所有列的子集
    • 使用字典和 Series 分组
    • 使用函数分组
    • 根据索引层级分组
  • 数据聚合
    • 逐列及多函数应用
  • 应用: 通用拆分-应用-联合
    • 压缩与组键
    • 分位数与桶分析
    • 例子:使用指定分组值填充缺失值
    • 例子:随机采样与排列
    • 例子:分组加权平均和相关性
    • 例子:逐组线性回归
  • 数据透视表与交叉表
    • pivot_table
    • 交叉表 crosstab

GroupBy 机制

描述组操作的术语: 拆分-应用-联合

分组键可以是多种形式的:

  • 与需要分组的轴向长度一致的值列表或值数组
  • DataFrame 的列名的值
  • 可以在轴索引或索引中的带个标签上调用的函数

考虑以下数据

1
2
3
4
5
df = pd.DataFrame({"key1": ["a", "a", "b", "b", "a"]
,"key2": ["one", "two", "one", "two", "one"]
,"data1": np.random.randn(5)
,"data2": np.random.randn(5)
})
1
2
3
4
5
6
  key1 key2     data1     data2
0 a one -0.244697 2.553470
1 a two -1.101256 1.676770
2 b one 0.449596 1.338166
3 b two 1.749545 2.252941
4 a one -1.926595 2.016802

根据 key1 标签计算 data1 列的均值

1
grouped = df["data1"].groupby(df["key1"])

grouped 是 GroupBy 对象,除了关于分组键 df["key1"] 的一些中间数据外,还没有进行任何计算

这个对象之后可以在每个分组上应用一些操作,例如求均值: grouped.mean(),结果为

1
2
3
4
key1
a -1.090849
b 1.099570
Name: data1, dtype: float64

grouped 调用 mean() 时发生了什么

数据(一个 Series)根据分组键进行了聚合,并产生一个新的 Series,这个 Series 使用 key1 列的唯一值作为索引

1
means = df["data1"].groupby([df["key1"], df["key2"]]).mean()

用两个分组键得到的结果 Series 包含唯一键对的多层索引

1
2
3
4
5
6
key1  key2
a one -1.085646
two -1.101256
b ont 0.449596
two 1.749545
Name: data1, dtype: float64

分组键也可以是任何长度正确的数组

1
2
3
states = np.array(["Ohio", "California", "California", "Ohio", "Ohio"])
years = np.array([2005, 2005, 2006, 2005, 2006])
df["data1"].groupby([states, years]).mean()
1
2
3
4
5
California  2005   -1.101256
2006 0.449596
Ohio 2005 0.752424
2006 -1.926595
Name: data1, dtype: float64

分组信息一般还要做后续处理,因此通常包含在同一个 DataFrame 中。此时可以传递列名作分组键

1
df.groupby("key1").mean()
1
2
3
4
         data1     data2
key1
a -0.134290 -0.206242
b 0.968495 0.333336
1
df.groupby(["key1", "key2"]).mean()
1
2
3
4
5
6
              data1     data2
key1 key2
a one 0.011570 -0.354296
two -0.426011 0.089865
b ont 1.306989 -1.394461
two 0.630002 2.061132

默认情况下所有的数值列都可以聚合。

GroupBy 的 size 方法返回包含组大小信息的 Series

1
df.groupby("key1", "key2").size()
分组键中的任何缺失值被排除在结果之外

遍历各分组

GroupBy 对象支持迭代,生成一个包含组名和数据块的2维元组序列。

1
2
3
for name, group in df.groupby("key1"):
print(name)
print(group)
1
2
3
4
5
6
7
8
9
a
key1 key2 data1 data2
0 a one 0.531706 -0.152676
1 a two -0.426011 0.089865
4 a one -0.508567 -0.555916
b
key1 key2 data1 data2
2 b ont 1.306989 -1.394461
3 b two 0.630002 2.061132

多个分组键的情况,元组中的第一个元素是键值的元组。

1
2
3
for (k1, k2), group in df.groupby(["key1", "key2"]):
print((k1, k2))
print(group)
1
2
3
4
5
6
7
8
9
10
11
12
13
('a', 'one')
key1 key2 data1 data2
0 a one 0.531706 -0.152676
4 a one -0.508567 -0.555916
('a', 'two')
key1 key2 data1 data2
1 a two -0.426011 0.089865
('b', 'ont')
key1 key2 data1 data2
2 b ont 1.306989 -1.394461
('b', 'two')
key1 key2 data1 data2
3 b two 0.630002 2.061132

先计算出数据块的字典,然后就可以在某一块数据上进行操作而不是所有数据块

1
pieces = dict(list(df.groupby("key1")))
1
2
3
4
5
6
{'a':   key1 key2     data1     data2
0 a one 0.531706 -0.152676
1 a two -0.426011 0.089865
4 a one -0.508567 -0.555916, 'b': key1 key2 data1 data2
2 b ont 1.306989 -1.394461
3 b two 0.630002 2.061132}

默认情况下,groupby 在 axis=0 轴向上分组。可以在任意轴向上分组,例如根据 dtype 分组

1
2
3
4
grouped = df.groupby(df.dtypes, axis=1)
for dtype, group in grouped:
print(dtype)
print(group)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float64
data1 data2
0 0.531706 -0.152676
1 -0.426011 0.089865
2 1.306989 -1.394461
3 0.630002 2.061132
4 -0.508567 -0.555916
object
key1 key2
0 a one
1 a two
2 b ont
3 b two
4 a one

选择一列或所有列的子集

从 DataFrame 创建的 GroupBy 对象,用列名称或列名称数组索引时,会产生用于聚合的列子集的效果

1
2
df.groupby("key1")["data1"]
df.groupby("key1")[["data2"]]

是以下代码的与语法糖

1
2
df["data1"].groupby("key1")
df[["data2"]].groupby("key1")

对于大型数据集,可能只需要聚合少部分列。例如要计算 data2 列的均值并获得 DataFrame 形式的结果

1
df.groupby(["key1", "key2"])[["data2"]].mean()
1
2
3
4
5
6
              data2
key1 key2
a one -0.354296
two 0.089865
b ont -1.394461
two 2.061132

如果 groupby 中传递的是列表或数组,则对 GroupBy 对象索引操作返回的对象是分组的 DataFrame。

如果只有单个列名作为标量传递,则返回的是分组的 Series,如下

1
2
s_grouped = df.groupby(["key1", "key2"])["data2"]
s_grouped.mean()
1
2
3
4
5
6
key1  key2
a one -0.354296
two 0.089865
b ont -1.394461
two 2.061132
Name: data2, dtype: float64

使用字典和 Series 分组

分组信息可能以非数组形式存在。

比如有各列的分组对应关系,想把各列按组累加:

1
2
3
4
5
mapping = {"key1": "red"
,"key2": "red"
,"data1": "green"
,"data2": "green"
}

可以根据字典构造传给 groupby 的数组,也可以直接传入字典:

1
2
by_column = df.groupby(mapping, axis=1)
by_column.sum()

Series 也类似

1
2
map_series = pd.Series(mapping)
df.groupby(map_series, axis=1).count()
1
2
3
4
5
6
   green  red
0 2 2
1 2 2
2 2 2
3 2 2
4 2 2

使用函数分组

与使用字典或者 Series 相比,用 Python 函数定义分组关系是更通用的,返回值会被用作分组名称。

例如考虑以下数据

1
2
3
4
5
people = pd.DataFrame(np.random.randn(5, 5)
,columns=["a", "b", "c", "d", "e"]
,index=["Joe", "Steve", "Wes", "Jim", "Travis"]
)
people.iloc[2:3, [1, 2]] = np.nan
1
2
3
4
5
6
               a         b         c         d         e
Joe 0.017890 -0.174005 -0.405519 -0.329940 0.086393
Steve -0.780601 -0.373768 -1.228587 0.948700 0.407224
Wes 0.143479 NaN NaN 0.595019 0.069059
Jim -1.011969 -1.851514 -0.517057 -0.436948 -0.412682
Travis -1.067871 -0.916260 0.707470 -0.533765 -1.339300
  • (1) 用字典分组
1
2
3
4
mapping = {"a": "red", "b": "red", "c": "blue",
"d": "blue", "e": "red", "f": "orange"}
by_column = people.groupby(mapping, axis=1)
by_column.sum()
1
2
3
4
5
6
            blue       red
Joe -0.735459 -0.069721
Steve -0.279887 -0.747144
Wes 0.595019 0.212538
Jim -0.954005 -3.276165
Travis 0.173705 -3.323431
  • (2) 用 Series 分组

与 (1) 中用字典相同的分组效果

1
2
3
map_series = pd.Series(mapping)
by_column = people.groupby(map_series, axis=1)
by_column.sum()
  • (3) 用函数分组
1
people.groupby(len).sum()
1
2
3
4
          a         b         c         d         e
3 -0.850600 -2.025518 -0.922576 -0.171870 -0.257230
5 -0.780601 -0.373768 -1.228587 0.948700 0.407224
6 -1.067871 -0.916260 0.707470 -0.533765 -1.339300
  • (4) 将函数与数组、字典、Series 混合

所有的对象都会在内部转换为数组

1
2
key_list = ["one", "one", "one", "two", "two"]
people.groupby([len, key_list]).min()
1
2
3
4
5
              a         b         c         d         e
3 one 0.017890 -0.174005 -0.405519 -0.329940 0.069059
two -1.011969 -1.851514 -0.517057 -0.436948 -0.412682
5 one -0.780601 -0.373768 -1.228587 0.948700 0.407224
6 two -1.067871 -0.916260 0.707470 -0.533765 -1.339300

根据索引层级分组

分层索引的数据集可以在轴索引的某个层级进行聚合。

1
2
3
4
5
columns = pd.MultiIndex.from_arrays([["US", "US", "US", "JP", "JP"]
,[1, 3, 5, 1, 3]]
,names=["cty", "tenor"]
)
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
1
2
3
4
5
6
cty          US                            JP
tenor 1 3 5 1 3
0 -1.444190 0.379001 -0.368231 -0.269382 0.589637
1 -0.241827 0.349321 -0.856892 -0.552612 0.713058
2 -0.316878 -1.592876 -1.332315 1.885868 -0.315526
3 0.201294 0.641879 1.693554 -1.755356 -0.150754
1
hier_df.groupby(level="cty", axis=1).count()
1
2
3
4
5
cty  JP  US
0 2 3
1 2 3
2 2 3
3 2 3

数据聚合

聚合:根据数组产生标量值的数据转换过程。常见的聚合操作:

函数名 描述
count 分组中的非 NA 值数量
sum 非 NA 值的和
mean 非 NA 值的均值
median 非 NA 值的算术中位数
std, var 无偏的(n-1分母) 标准差和方差
min, max 非 NA 值的最小值, 最大值
prod 非 NA 值的乘积
first, last 非 NA 值的第一个和最后一个值

可以使用自行定制的聚合,然后再调用已经在分组对象上定义好的方法。

例如,用 quantile 可以算 Series 或 DataFrame 列的样本分位数

1
2
3
4
5
6
# 计算一开始的 df 的各列的上四分位数
df = pd.DataFrame(data={"data1": [2.553470, 1.676770, 1.338166, 2.252941, 2.016802]
,"data2": [-0.244697, -1.101256, 0.449596, 1.749545, -1.926595]
,"key1": ["a", "a", "b", "b", "a"]
,"key2": ["one", "two", "one", "two", "one"]
})
1
2
3
4
5
6
      data1     data2 key1 key2
0 2.553470 -0.244697 a one
1 1.676770 -1.101256 a two
2 1.338166 0.449596 b one
3 2.252941 1.749545 b two
4 2.016802 -1.926595 a one
1
df.quantile(0.9)
1
2
3
data1    2.433258
data2 1.229565
Name: 0.9, dtype: float64
quantile 不是显式地为 GroupBy 对象实现的,但它是 Series 的方法,因此可以用于聚合。

在内部,GroupBy 对 Series 进行切片,为每一块切片 piece 调用 piece.quantile(0.9),然后将这些结果组装起来。

1
2
grouped = df.groupby("key1")
grouped["data1"].quantile(0.9)
1
2
3
4
key1
a 2.446136
b 2.161463
Name: data1, dtype: float64

要使用自己的聚合函数,需要将函数传递给 aggregate 或 agg 方法

1
2
3
4
5
# 自定义聚合函数
def peak_to_peak(arr):
return arr.max() - arr.min()

grouped.agg(peak_to_peak)
1
2
3
4
         data1     data2
key1
a 0.876700 1.681898
b 0.914775 1.299949
  • describe 也是有效的 grouped.describe()
  • 自定义聚合函数通常比内置的慢很多。因为构造中间组数据块的开销,比如函数调用、数据重新排列

逐列及多函数应用

对 Series 或 DataFrame 的所有列进行聚合,有以下两种办法

  • 用 GroupBy 对象内置的 mean, std 等内置的方法
  • 使用 agg 和所需的函数

各列同时使用多个函数进行聚合也是可以的:

考虑以下DataFrame (tips 数据) (这个例子在 利用Python进行数据分析-可视化 中也有使用)

1
2
3
4
5
6
7
8
9
10
11
12
     total_bill   tip smoker   day    time  size   tip_pct
0 16.99 1.01 No Sun Dinner 2 0.063204
1 10.34 1.66 No Sun Dinner 3 0.191244
2 21.01 3.50 No Sun Dinner 3 0.199886
3 23.68 3.31 No Sun Dinner 2 0.162494
4 24.59 3.61 No Sun Dinner 4 0.172069
.. ... ... ... ... ... ... ...
239 29.03 5.92 No Sat Dinner 3 0.256166
240 27.18 2.00 Yes Sat Dinner 2 0.079428
241 22.67 2.00 Yes Sat Dinner 2 0.096759
242 17.82 1.75 No Sat Dinner 2 0.108899
243 18.78 3.00 No Thur Dinner 2 0.190114

根据两列对 DataFrame 用函数进行分组,函数名可以以字符串传递

1
2
3
4
# 根据 day 和 smoker 对 tips 进行分组
grouped = df.groupby(["day", "smoker"])
grouped_pct = grouped["tip_pct"]
grouped_pct.agg("mean") # 函数名可以以字符串传递
1
2
3
4
5
6
7
8
9
10
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
Name: tip_pct, dtype: float64

传递函数或函数名的列表,则会得到一个列名是这些函数名的 DataFrame

1
grouped_pct.agg(["mean", "std", peak_to_peak])
1
2
3
4
5
6
7
8
9
10
                 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
  • 注意 lambda 函数具有名称 "<lambda>"(__name__ 属性),因此难以识别
  • 如果传入的是 (name, function) 元组的列表,元组第一个元素将作为 DataFrame 的列名
1
grouped_pct.agg([("foo", "mean"), ("bar", np.std)])
1
2
3
4
5
6
7
8
9
10
                  foo       bar
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

可以指定应用到所有列上的函数列表,也可以每一列上应用不同的函数

1
2
functions = ["count", "mean", "max"]
result = grouped[["tip_pct", "total_bill"]].agg(functions)

产生的 DataFrame 用分层的列,与分别聚合每一列再以列名作为 key 使用 concat 将结果拼在一起的结果相同

1
2
result = pd.concat([grouped["tip_pct"].agg(functions)
,grouped["total_bill"].agg(functions)], axis=1)
1
2
3
4
5
6
7
8
9
10
11
            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

可以传递自定义名称的元组列表

1
2
ftuples = [("Durchschnitt", "mean"), ("Abweichung", np.var)]
result = grouped[["tip_pct", "total_bill"]].agg(ftuples)
1
2
3
4
5
6
7
8
9
10
11
                 tip_pct              total_bill            
Durchschnitt Abweichung Durchschnitt Abweichung
day smoker
Fri No 0.151650 0.000791 18.420000 25.596333
Yes 0.174783 0.002631 16.813333 82.562438
Sat No 0.158048 0.001581 19.661778 79.908965
Yes 0.147906 0.003767 21.276667 101.387535
Sun No 0.160113 0.001793 20.506667 66.099980
Yes 0.187250 0.023757 24.120000 109.046044
Thur No 0.160298 0.001503 17.113111 59.625081
Yes 0.163863 0.001551 19.190588 69.808518

将不同的函数应用到一个或多个列上

1
grouped.agg({"tip": np.max, "size": "sum"})
1
2
3
4
5
6
7
8
9
10
               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
1
2
grouped.agg({"tip_pct": ["min", "max", "mean", "std"]
,"size": "sum"})
1
2
3
4
5
6
7
8
9
10
11
              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
  • 返回不含行索引的聚合数据
    • 加上 ax_index=False
    • 在结果上调用 reset_index()

应用: 通用拆分-应用-联合

GroupBy 对象最常见的用法是 apply:将 GroupBy 对象拆分成多块,在每一块上调用传递的函数,然后将每一块拼接到一起

例如选出 tip_pct 最高的 5 组,首先定义一个可以再特定列中选出最大值所在行的函数

1
2
3
def top(df, n=5, column="tip_pct"):
return df.sort_values(by=column)[-n:]
top(tips, n=5)
1
2
3
4
5
6
     total_bill   tip smoker  day    time  size   tip_pct
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

1
tips.groupby("smoker").apply(top)

top 函数在 DataFrame 每一行分组上被调用,之后用 pd.concat 将函数结果粘贴在一起,并用分组名作各组的标签。

行上是分层索引,其内部层级包含原 DataFrame 的索引值。

1
2
3
4
5
6
7
8
9
10
11
12
            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

GroupBy 对象上调用 describe 方法 grouped.describe(),在 GroupBy 对象内部相当于以下代码的简写: grouped.apply(lambda x: x.describe())

压缩与组键

apply 所得对象具有分组键所形成的分层索引,可以传递 group_keys=False 禁用

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

分位数与桶分析

cut 与 qcut 与 groupby 一起可以对数据集进行分桶分析和分位数分析。

1
2
3
4
df = pd.DataFrame({"data1": np.random.randn(1000)
,"data2": np.random.randn(1000)
})
quantiles = pd.cut(df.data1, 4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0       (0.0334, 1.598]
1 (0.0334, 1.598]
2 (-3.102, -1.531]
3 (-1.531, 0.0334]
4 (0.0334, 1.598]
...
995 (-1.531, 0.0334]
996 (0.0334, 1.598]
997 (-1.531, 0.0334]
998 (0.0334, 1.598]
999 (0.0334, 1.598]
Name: data1, Length: 1000, dtype: category
Categories (4, interval[float64]): [(-3.102, -1.531] < (-1.531, 0.0334] < (0.0334, 1.598] <
(1.598, 3.163]]

Categorical 对象可以直接传给 groupby

1
2
3
4
5
6
7
8
def get_stats(group):
return {"min": group.min()
,"max": group.max()
,"count": group.count()
,"mean": group.mean()
}
grouped = df.data2.groupby(quantiles)
grouped.apply(get_stats).unstack()
1
2
3
4
5
6
                       min       max  count      mean
data1
(-3.102, -1.531] -1.918526 2.746732 60.0 0.249957
(-1.531, 0.0334] -2.956697 3.211938 424.0 -0.014414
(0.0334, 1.598] -3.334961 3.268233 466.0 0.007832
(1.598, 3.163] -1.837061 2.476310 50.0 -0.012843

这些是等长桶: 如果要根据样本分位数计算出等大小的桶,用 qcut,传入 labels=False 获得分位数数值

1
2
3
grouping = pd.qcut(df.data1, 10, labels=False)
grouped = df.data2.groupby(grouping)
grouped.apply(get_stats).unstack()
1
2
3
4
5
6
7
8
9
10
11
12
            min       max  count      mean
data1
0 -2.357036 2.746732 100.0 0.182744
1 -2.171426 3.211938 100.0 0.151674
2 -2.956697 2.772613 100.0 -0.018299
3 -2.339851 2.649183 100.0 -0.180259
4 -2.358595 2.425139 100.0 -0.052244
5 -3.334961 2.887811 100.0 -0.039381
6 -2.715801 2.217948 100.0 -0.042654
7 -1.723145 1.798921 100.0 0.025377
8 -2.037132 3.268233 100.0 0.087156
9 -2.284983 2.476310 100.0 0.004818

例子:使用指定分组值填充缺失值

fillna 用指定分组值来填充

1
2
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
1
2
3
4
5
6
7
0         NaN
1 0.802539
2 NaN
3 1.733642
4 NaN
5 0.845553
dtype: float64

一般的均值填充:

1
s.fillna(s.mean())

填充值按组变化

  • 对数据分组后用 apply
  • 在每个数据块上都调用 fillna
1
2
3
4
5
states = ["Ohio", "New York", "Vermont", "Florida"
,"Orefon", "Nevada", "California", "Idaho"]
group_key = ["East"] * 4 + ["West"] * 4
data = pd.Series(np.random.randn(8), index=states)
data[["Vermont", "Nevada", "Idano"]] = np.nan
1
2
3
4
5
6
7
8
9
Ohio          1.063109
New York -0.261407
Vermont NaN
Florida 1.844568
Orefon 1.414252
Nevada NaN
California 1.745096
Idaho NaN
dtype: float64
1
data.groupby(group_key).mean()
1
2
3
East    0.882090
West 1.579674
dtype: float64

用分组的平均值填充 NA

法一

1
2
fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
1
2
3
4
5
6
7
8
9
Ohio          1.063109
New York -0.261407
Vermont 0.882090
Florida 1.844568
Orefon 1.414252
Nevada 1.579674
California 1.745096
Idaho 1.579674
dtype: float64

法二: 用到 GroupBy 内部的 name 属性

1
2
3
fill_values = {"East": 0.5, "West": -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
1
2
3
4
5
6
7
8
9
Ohio          1.063109
New York -0.261407
Vermont 0.500000
Florida 1.844568
Orefon 1.414252
Nevada -1.000000
California 1.745096
Idaho -1.000000
dtype: float64

例子:随机采样与排列

从大数据集中抽取随机样本

  • 构造扑克牌
1
2
3
4
5
6
7
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)
  • 抽出 5 张牌
1
2
3
def draw(deck, n=5):
return deck.sample(n)
draw(deck)
1
2
3
4
5
6
KC     10
KD 10
4S 4
7H 7
10D 10
dtype: int64
  • 从每个花色随机抽取两张牌

先分组在用 apply(用函数分组+定制聚合函数)

1
2
get_suit = lambda card: card[-1]
deck.groupby(get_suit).apply(draw, n=2)
1
2
3
4
5
6
7
8
9
C  4C    4
9C 9
D 2D 2
8D 8
H 3H 3
AH 1
S 9S 9
AS 1
dtype: int64

例子:分组加权平均和相关性

数据案例1

1
2
3
4
df = pd.DataFrame({"category": ["a", "a", "a", "a", "b", "b", "b", "b"]
,"data": np.random.randn(8)
,"weights": np.random.rand(8)
})
1
2
3
4
5
6
7
8
9
  category      data   weights
0 a 0.087366 0.485621
1 a -1.500472 0.103132
2 a 0.400191 0.228989
3 a -1.503600 0.304832
4 b -1.370590 0.489394
5 b -0.926318 0.561078
6 b -0.513752 0.259509
7 b 0.548558 0.941081

通过 category 进行分组加权平均

1
2
3
grouped = df.groupby("category")
get_wavg = lambda g: np.average(g["data"], weights=g["weights"])
grouped.apply(get_wavg)
1
2
3
4
category
a -0.426722
b -0.358755
dtype: float64
  • 数据案例2

数据 close_px 如下

1
2
3
4
5
6
              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

计算一个 DataFrame,它包含标普指数 SPX 每日收益的年度相关性(通过百分比变化计算)。

首先定义每列与 SPX 列成对关联的函数

1
spx_corr = lambda x: x.corrwith(x["SPX"])

pct_change() 计算一行对比上一行的百分比的变化。close_px.pct_change() 的结果如下:

1
2
3
4
5
6
                AAPL      MSFT       XOM       SPX
2003-01-02 NaN NaN NaN NaN
2003-01-03 0.006757 0.001421 0.000684 -0.000484
2003-01-06 0.000000 0.017975 0.024624 0.022474
2003-01-07 -0.002685 0.019052 -0.033712 -0.006545
2003-01-08 -0.020188 -0.028272 -0.004145 -0.014086
1
rets = close_px.pct_change().dropna()

按年对百分比变化进行分组,可以用单行函数从每个行标签中提取每个 datetime 标签的 year 属性

1
2
3
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
1
2
3
4
5
6
7
8
9
10
          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

计算内部列的相关性

1
by_year.apply(lambda g: g["AAPL"].corr(g["MSFT"]))
1
2
3
4
5
6
7
8
9
10
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

例子:逐组线性回归

依然用 close_px 数据

用计量经济学库 statsmodels 定义回归函数 regress,对每个数据块执行普通最小二乘

1
2
3
4
5
6
7
8
9
10
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

by_year.apply(regress, "AAPL", ["SPX"])
1
2
3
4
5
6
7
8
9
10
           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

数据透视表与交叉表

pivot_table

数据透视表: 根据一个或多个键聚合一张表的数据,将数据在矩形格式中排列,其中一些分组键是沿着行的,另一些是沿着列的。

Pandas 透视表是通过 groupby + 分层索引的重塑操作实现的。

DataFrame 有一个 pivot_table 方法,还有 pd.pivot_table 函数。可以为 groupby 提供接口,还可以添加部分统计,称为边距

考虑 tips 数据

1
2
3
4
5
6
7
8
9
10
11
12
     total_bill   tip smoker   day    time  size   tip_pct
0 16.99 1.01 No Sun Dinner 2 0.063204
1 10.34 1.66 No Sun Dinner 3 0.191244
2 21.01 3.50 No Sun Dinner 3 0.199886
3 23.68 3.31 No Sun Dinner 2 0.162494
4 24.59 3.61 No Sun Dinner 4 0.172069
.. ... ... ... ... ... ... ...
239 29.03 5.92 No Sat Dinner 3 0.256166
240 27.18 2.00 Yes Sat Dinner 2 0.079428
241 22.67 2.00 Yes Sat Dinner 2 0.096759
242 17.82 1.75 No Sat Dinner 2 0.108899
243 18.78 3.00 No Thur Dinner 2 0.190114
  • 要计算一张在行方向上按 day 和 smoker 排列的分组平均值(pivot_table 的默认聚合类型)的表
1
tips.pivot_table(index=["day", "smoker"])

这个功能可以用 groupby 实现 tips.groupby(["day", "smoker"]).mean()

1
2
3
4
5
6
7
8
9
10
             total_bill       tip      size   tip_pct
day smoker
Fri No 18.420000 2.812500 2.250000 0.179740
Yes 16.813333 2.714000 2.066667 0.216293
Sat No 19.661778 3.102889 2.555556 0.190412
Yes 21.276667 2.875476 2.476190 0.179833
Sun No 20.506667 3.167895 2.929825 0.193617
Yes 24.120000 3.516842 2.578947 0.322021
Thur No 17.113111 2.673778 2.488889 0.193424
Yes 19.190588 3.030000 2.352941 0.198508
  • 只想在 tip_pctsize 上进行聚合,并按 time 分组。将 smoker 放入表的列,将 day 放入表的行。
1
tips.pivot_table(["tip_pct", "size"], index=["time", "day"], columns="smoker")
1
2
3
4
5
6
7
8
9
                 size             tip_pct          
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.162612 0.202545
Sat 2.555556 2.476190 0.190412 0.179833
Sun 2.929825 2.578947 0.193617 0.322021
Thur 2.000000 NaN 0.190114 NaN
Lunch Fri 3.000000 1.833333 0.231125 0.236915
Thur 2.500000 2.352941 0.193499 0.198508
  • 可以通过 margins=True 扩充这个表来包含部分统计。会添加 ALL 行和列标签。相应的值是单层中所有数据的分组统计值
1
2
3
4
5
tips.pivot_table(["tip_pct", "size"]
,index=["time", "day"]
,columns="smoker"
,margins=True
)
1
2
3
4
5
6
7
8
9
10
                 size                       tip_pct                    
smoker No Yes All No Yes All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.162612 0.202545 0.192562
Sat 2.555556 2.476190 2.517241 0.190412 0.179833 0.185305
Sun 2.929825 2.578947 2.842105 0.193617 0.322021 0.225718
Thur 2.000000 NaN 2.000000 0.190114 NaN 0.190114
Lunch Fri 3.000000 1.833333 2.000000 0.231125 0.236915 0.236088
Thur 2.500000 2.352941 2.459016 0.193499 0.198508 0.194895
All 2.668874 2.408602 2.569672 0.192237 0.218176 0.202123
  • 要使用不同的聚合函数时,将函数传给 aggfunc
1
2
3
4
5
6
tips.pivot_table("tip_pct"
,index=["time", "smoker"]
,columns="day"
,aggfunc=len
,margins=True
)
1
2
3
4
5
6
7
day             Fri   Sat   Sun  Thur    All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106.0
Yes 9.0 42.0 19.0 NaN 70.0
Lunch No 1.0 NaN NaN 44.0 45.0
Yes 6.0 NaN NaN 17.0 23.0
All 19.0 87.0 76.0 62.0 244.0
  • 可以传递空值的 fill_value
1
2
3
4
5
6
tips.pivot_table("tip_pct"
,index=["time", "size" ,"smoker"]
,columns="day"
,aggfunc="mean"
,fill_value=0
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
day                      Fri       Sat       Sun      Thur
time size smoker
Dinner 1 No 0.000000 0.160000 0.000000 0.000000
Yes 0.000000 0.483092 0.000000 0.000000
2 No 0.162612 0.198319 0.206535 0.190114
Yes 0.211180 0.178877 0.400522 0.000000
3 No 0.000000 0.183870 0.182962 0.000000
Yes 0.000000 0.176599 0.183278 0.000000
4 No 0.000000 0.177734 0.175289 0.000000
Yes 0.133465 0.147074 0.254373 0.000000
5 No 0.000000 0.000000 0.263344 0.000000
Yes 0.000000 0.119284 0.070274 0.000000
6 No 0.000000 0.000000 0.115821 0.000000
Lunch 1 No 0.000000 0.000000 0.000000 0.222087
Yes 0.288288 0.000000 0.000000 0.000000
2 No 0.000000 0.000000 0.000000 0.201503
Yes 0.226641 0.000000 0.000000 0.191197
3 No 0.231125 0.000000 0.000000 0.092162
Yes 0.000000 0.000000 0.000000 0.257941
4 No 0.000000 0.000000 0.000000 0.161573
Yes 0.000000 0.000000 0.000000 0.186592
5 No 0.000000 0.000000 0.000000 0.138160
6 No 0.000000 0.000000 0.000000 0.211191

交叉表 crosstab

交叉表是数据透视表的特殊情况:计算的是分组中的频率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from io import StringIO
data = """\
Sample 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"""

df = pd.read_table(StringIO(data), sep='\s+')

数据

1
2
3
4
5
6
7
8
9
10
11
   Sample 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

按照国籍和惯用性总结这些数据

1
pd.crosstab(df.Nationality, df.Handedness, margins=True)
1
2
3
4
5
Handedness   Left-handed  Right-handed  All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 10
  • crosstab 前两个参数可以使数组、Series、数组的列表
1
pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
1
2
3
4
5
6
7
8
9
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

Share