数据聚合与分组操作 ¶
GroupBy机制 ¶
import pandas as pd
import numpy as np
分组机制 ¶
分组操作第一步,数据包含在pandas对象中,可以是Series、DataFrame或其他数据结构。之后根据提供的一个或多个键分离到各个组中。
分组键可是多种形式的,并且键不一定是完全相同的类型(注意后面介绍的三个方法是可以产生用于分隔对象的值数组的快捷方式):
- 与需要分组的轴向长度一致的值列表或值数组。默认情况下,groupby在axis=0的轴向上分组。
- DataFrame的列名的值。
- 可以将分组轴向上的值和分组名称相匹配的字典或Series。
- 可以在轴索引或索引中的单个标签上调用的函数。
请注意,分组键中的任何缺失值将被排除在结果之外。
分离操作是在数据对象的特定轴向上进行的。例如,DataFrame可以在它的行方向(axis=0)或列方向(axis=1)进行分组。
分组操作后,一个函数就可以应用到各个组中,产生新的值。最终,所有函数的应用结果会联合为一个结果对象。
df = pd.DataFrame(
{
'key1': ['a', 'a', 'b', 'b', 'a'],
'key2': ['one', 'two', 'one', 'two', 'one'],
'data1': [1, 3, 5, 7, 9],
'data2': [2, 4, 6, 8, 10]
}
)
根据key1标签计算data1列的均值,方法一,访问data1
并使用key1
列(它是一个Series)调用groupby
方法:
grouped = df['data1'].groupby(df['key1'])
print(grouped) # <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fdd2cb01430>
grouped
变量现在是一个GroupBy
对象,它实际上还没有进行任何计算,拥有一些关于分组键df['key1']的一些中间数据的信息。
下面对grouped
对象做一些操作:
result = grouped.mean() # 计算平均值
print(result)
# key1
# a 4.333333
# b 6.000000
# Name: data1, dtype: float64
grouped_means = df['data1'].groupby([df['key1'], df['key2']]).mean()
print(grouped_means)
# key1 key2
# a one 5.0
# two 3.0
# b one 5.0
# two 7.0
# Name: data1, dtype: float64
上面例子使用了两个键对数据进行分组,并且结果Series现在拥有一个包含唯一键对的多层索引。
下面对计算的平均值(mean)进行重塑(unstack)。
print(grouped_means.unstack())
# key2 one two
# key1
# a 5.0 3.0
# b 5.0 7.0
分组信息通常包含在同一个DataFrame中。在这种情况下,可以传递列名(无论那些列名是字符串、数字或其他Python对象)作为分组键:
下面例子中df.groupby('key1').mean()
的结果里并没有key2
列。这是因为df['key2']
并不是数值数据,即df['key2']
是一个冗余列,因此被排除在结果之外。
result = df.groupby('key1').mean()
print(result)
# data1 data2
# key1
# a 4.333333 5.333333
# b 6.000000 7.000000
result = df.groupby(['key1', 'key2']).mean()
print(result)
# data1 data2
# key1 key2
# a one 5.0 6.0
# two 3.0 4.0
# b one 5.0 6.0
# two 7.0 8.0
result = df.groupby(['key1', 'key2']).size()
print(result)
# key1 key
# a one 2
# two 1
# b one 1
# two 1
# dtype: int64
遍历各分组 ¶
GroupBy
对象支持迭代,会生成一个包含组名和数据块的2维元组序列。
df = pd.DataFrame(
{
'key1': ['a', 'a', 'b', 'b', 'a'],
'key2': ['one', 'two', 'one', 'two', 'one'],
'data1': [1, 3, 5, 7, 9],
'data2': [2, 4, 6, 8, 10]
}
)
单个分组键的情况:
for name, group in df.groupby('key1'):
print(name)
print(group)
# a
# key1 key2 data1 data2
# 0 a one 1 2
# 1 a two 3 4
# 4 a one 9 10
# b
# key1 key2 data1 data2
# 2 b one 5 6
# 3 b two 7 8
多个分组键的情况: 元组中的第一个元素是键值的元组。
for (k1, k2), group in df.groupby(['key1', 'key2']):
print((k1, k2))
print(group)
# ('a', 'one')
# key1 key2 data1 data2
# 0 a one 1 2
# 4 a one 9 10
# ('a', 'two')
# key1 key2 data1 data2
# 1 a two 3 4
# ('b', 'one')
# key1 key2 data1 data2
# 2 b one 5 6
# ('b', 'two')
# key1 key2 data1 data2
# 3 b two 7 8
result = dict(list(df.groupby('key1')))
print(result)
# df.groupby('key1')的结果是一个对象
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f240fe058b0>
# list(df.groupby('key1'))的结果是包含FrameData的结构的列表list:
# [
# ('a', key1 key2 data1 data2
# 0 a one 1 2
# 1 a two 3 4
# 4 a one 9 10),
# ('b', key1 key2 data1 data2
# 2 b one 5 6
# 3 b two 7 8)
# ]
# dict(list(df.groupby('key1')))的结果是包含FrameData的结构的字典dict
# {
# 'a': key1 key2 data1 data2
# 0 a one 1 2
# 1 a two 3 4
# 4 a one 9 10,
# 'b': key1 key2 data1 data2
# 2 b one 5 6
# 3 b two 7 8
# }
print(result['b'])
# key1 key2 data1 data2
# 2 b one 5 6
# 3 b two 7 8
默认情况下,groupby
在axis=0
的轴向上分组,也可以在其他任意轴向上进行分组。
print(df.dtypes)
# key1 object
# key2 object
# data1 int64
# data2 int64
# dtype: object
grouped = df.groupby(df.dtypes, axis=1)
print(grouped) # <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f4f6636df70>
print(list(grouped))
# [
# (dtype('int64'), data1 data2
# 0 1 2
# 1 3 4
# 2 5 6
# 3 7 8
# 4 9 10),
# (dtype('O'), key1 key2
# 0 a one
# 1 a two
# 2 b one
# 3 b two
# 4 a one)
# ]
打印各分组如下:
for dtype, group in grouped:
print(dtype)
print(group)
# int64
# data1 data2
# 0 1 2
# 1 3 4
# 2 5 6
# 3 7 8
# 4 9 10
# object
# key1 key2
# 0 a one
# 1 a two
# 2 b one
# 3 b two
# 4 a one
选择一列或所有列的子集 ¶
对于从DataFrame创建的GroupBy
对象,用列名称或列名称数组进行索引时,会产生用于聚合的列子集的效果。
如果传递的是列表或数组,则此索引操作返回的对象是分组的DataFrame;如果只有单个列名作为标量传递,则为分组的Series;
对比下面4句:
result = df.groupby('key1')['data1'] # 单个列名
print(result) # <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fa988609040>
for key, data in result:
print(key)
print(data)
result = df['data1'].groupby(df['key1']) # 单个列名
print(result) # <pandas.core.groupby.generic.SeriesGroupBy object at 0x7fa988609910>
for key, data in result:
print(key)
print(data)
# a
# 0 1
# 1 3
# 4 9
# Name: data1, dtype: int64
# b
# 2 5
# 3 7
# Name: data1, dtype: int64
result = df.groupby('key1')[['data1']] # 列表或数组
print(result) # <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f32666176a0>
for key, data in result:
print(key)
print(data)
# a
# key1 key2 data1 data2
# 0 a one 1 2
# 1 a two 3 4
# 4 a one 9 10
# b
# key1 key2 data1 data2
# 2 b one 5 6
# 3 b two 7 8
result = df[['data1']].groupby(df['key1']) # 列表或数组
print(result) # <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f32666171f0>
for key, data in result:
print(key)
print(data)
# a
# data1
# 0 1
# 1 3
# 4 9
# b
# data1
# 2 5
# 3 7
使用字典和Series分组 ¶
分组信息可能会以非数组形式存在。
生成一个示例DataFrame。
people = pd.DataFrame(
[[1, 3, 5, 7, 9],
[0, 2, 4, 6, 8],
[0, 2, 4, 6, 8],
[1, 3, 5, 7, 9],
[1, 2, 3, 4, 5]],
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis']
)
添加一些NA值。
people.iloc[2:3, [1, 2]] = np.nan
print(people)
# a b c d e
# Joe 1 3.0 5.0 7 9
# Steve 0 2.0 4.0 6 8
# Wes 0 NaN NaN 6 8
# Jim 1 3.0 5.0 7 9
# Travis 1 2.0 3.0 4 5
假设有如下各列的分组对应关系,并且想把各列按组累加。
mapping = {
'a': 'red',
'b': 'red',
'c': 'blue',
'd': 'blue',
'e': 'red',
'f': 'orange' # 注意:健f虽然没有被用到,但不影响在这里定义。
}
把mapping
这个字典传给groupby()
。
by_column = people.groupby(mapping, axis=1)
print(by_column.sum())
# blue red
# Joe 12.0 13.0
# Steve 10.0 10.0
# Wes 6.0 8.0
# Jim 12.0 13.0
# Travis 7.0 8.0
Series也有相同的功能,可以视为固定大小的映射。
map_services = pd.Series(mapping)
print(map_services)
# a red
# b red
# c blue
# d blue
# e red
# f orange
# dtype: object
result = people.groupby(map_services, axis=1).count()
print(result)
# blue red
# Joe 2 3
# Steve 2 3
# Wes 1 2
# Jim 2 3
# Travis 2 3
使用函数分组 ¶
与使用字典或Series分组相比,使用Python函数是定义分组关系的一种更为通用的方式。
作为分组键传递的函数将会按照每个索引值调用一次,同时返回值会被用作分组名称。注意:函数是作用在索引上。
result = people.groupby(len).sum() # 人的名字是索引值,根据名字的长度来进行分组
print(result)
# a b c d e
# 3 2 6.0 10.0 20 26
# 5 0 2.0 4.0 6 8
# 6 1 2.0 3.0 4 5
可以将函数与数组、字典或Series进行混合,所有的对象都会在内部转换为数组。
key_list = ['one', 'one', 'one', 'two', 'two']
result = people.groupby([len, key_list]).min()
print(result)
# a b c d e
# 3 one 0 3.0 5.0 6 8
# two 1 3.0 5.0 7 9
# 5 one 0 2.0 4.0 6 8
# 6 two 1 2.0 3.0 4 5
根据索引层级分组 ¶
根据层级分组时,将层级数值或层级名称传递给level
关键字。
columns = pd.MultiIndex.from_arrays(
[['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor']
)
hier_df = pd.DataFrame(
[[1, 3, 5, 7, 9],
[0, 2, 4, 6, 8],
[1, 3, 5, 7, 9],
[1, 2, 3, 4, 5]],
columns=columns
)
print(hier_df)
# cty US JP
# tenor 1 3 5 1 3
# 0 1 3 5 7 9
# 1 0 2 4 6 8
# 2 1 3 5 7 9
# 3 1 2 3 4 5
result = hier_df.groupby(level='cty', axis=1).count()
print(result)
# cty JP US
# 0 2 3
# 1 2 3
# 2 2 3
# 3 2 3
数据聚合 ¶
聚合是指所有根据数组产生标量值的数据转换过程,比如:mean
、count
、min
和sum
等一些聚合操作。
import pandas as pd
import numpy as np
预备知识:
分位数(Quantile),也称分位点,是指将一个随机变量的概率分布范围分为几个等份的数值点,分析其数据变量的趋势。 常用的分位数有 中位数、四分位数、百分位数等。
中位数(Medians)是一个统计学的专有名词,代表一个样本、种群或概率分布中的一个数值,可以将数值集合划分为相等的两部分。
利用pandas库计算data = [6, 47, 49, 15, 42, 41, 7, 39, 43, 40, 36]
的分位数。
确定p
分位数位置的两种方法(n
为数据的总个数,p
为0-1
之间的值)。在python中计算分位数位置的方案采用position=1+(n-1)*p
:
position = (n+1)*p
position = 1 + (n-1)*p
案例1
data = pd.Series(np.array([6, 47, 49, 15, 42, 41, 7, 39, 43, 40, 36]))
print("数据格式:")
print(np.sort(data)) # 必须要排序
print('Q1:', data.quantile(.25))
print('Q2:', data.quantile(.5))
print('Q3:', data.quantile(.75))
# 数据格式:
# [ 6 7 15 36 39 40 41 42 43 47 49]
# Q1: 25.5
# Q2: 40.0
# Q3: 42.5
# 手算计算结果:
# Q1的p分位数(0.25)位置position = 1+(11-1)*0.25 = 3.5(取第3位) (p=0.25) Q1=15+(36-15)*0.5=25.5 (第3、4位的差乘以余数0.5)
# Q2的p分位数(0.5)位置position = 1+(11-1)*0.5 = 6 (p=0.5) Q2=40
# Q3的p分位数(0.75)位置position = 1+(11-1)*0.75 = 9 (p=0.75) Q3=42+(43-42)*0.5=42.5
# IQR = Q3 - Q1 = 17
案例2
df = pd.DataFrame(np.array([[1, 1], [2, 10], [3, 100], [4, 100]]), columns=['a', 'b'])
print("数据原始格式:")
print(df)
print("计算p=0.1时,a列和b列的分位数")
print(df.quantile(.1))
# 数据原始格式:
# a b
# 0 1 1
# 1 2 10
# 2 3 100
# 3 4 100
# 计算p=0.1时,a列和b列的分位数
# a 1.3
# b 3.7
# Name: 0.1, dtype: float64
# 手算计算结果:
# 计算a列
# position=1+(4-1)*0.1=1.3 (取第1位)
# Q1=1+(2-1)*0.3=1.3 (第1、2位的差乘以余数0.3)
# 计算b列
# position=1+(4-1)*0.1=1.3 (取第1位)
# Q1=1+(10-1)*0.3=3.7 (第1、2位的差乘以余数0.3)
优化的groupby
方法:
- count: 分组中非NA值的数量
- sum: 非NA值的累加和
- mean: 非NA值的平均值
- median: 非NA值的算术中位数
- std, var: 无偏的(n-1分母)标准差和方差
- min, max: 非NA值的最小值、最大值
- prod: 非NA值的乘积
- first, last: 非NA值的第一个、最后一个值
df = pd.DataFrame(
{
'key1': ['a', 'a', 'b', 'b', 'a'],
'key2': ['one', 'two', 'one', 'two', 'one'],
'data1': [1, 3, 5, 7, 9],
'data2': [2, 4, 6, 8, 10]
}
)
print(df)
# key1 key2 data1 data2
# 0 a one 1 2
# 1 a two 3 4
# 2 b one 5 6
# 3 b two 7 8
# 4 a one 9 10
grouped = df.groupby('key1')
result = grouped['data1']
for i in result:
print(i)
# ('a', 0 1
# 1 3
# 4 9
# Name: data1, dtype: int64)
# ('b', 2 5
# 3 7
# Name: data1, dtype: int64)
result = grouped['data1'].quantile(0.9) # quantile分位数
print(result)
# key1
# a 7.8
# b 6.8
# Name: data1, dtype: float64
# 手算计算结果:
# 计算a列
# position=1+(3-1)*0.9=2.8
# Q1=3+(9-3)*0.8=7.8
# 计算b列
# position=1+(2-1)*0.9=1.9
# Q1=5+(7-5)*0.9=6.8
使用自行制定的聚合,并再调用已经在分组对象上定义好的方法。
def peak_to_peak(arr):
return arr.max() - arr.min()
result = grouped.agg(peak_to_peak)
print(result)
# data1 data2
# key1
# a 8 8
# b 2 2
result = grouped.describe()
print(result)
# data1 ... data2
# count mean std min 25% ... min 25% 50% 75% max
# key1 ...
# a 3.0 4.333333 4.163332 1.0 2.0 ... 2.0 3.0 4.0 7.0 10.0
# b 2.0 6.000000 1.414214 5.0 5.5 ... 6.0 6.5 7.0 7.5 8.0
逐列及多函数应用 ¶
tips = pd.read_csv('../examples/tips.csv')
tips['tip_pct'] = tips['tip'] / (tips['total_bill'] - tips['tip'])
print(tips.head(5))
# 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
根据各列同时使用多个函数进行聚合
grouped = tips.groupby(['day', 'smoker'])
# for i in grouped:
# print(i)
# (('Fri', 'No'), total_bill tip smoker day time size tip_pct
# 91 22.49 3.50 No Fri Dinner 2 0.184308
# ......
# 223 15.98 3.00 No Fri Lunch 3 0.231125)
# (('Fri', 'Yes'), total_bill tip smoker day time size tip_pct
# 90 28.97 3.00 Yes Fri Dinner 2 0.115518
# ......
# 226 10.09 2.00 Yes Fri Lunch 2 0.247219)
# ......
grouped_pct = grouped['tip_pct']
for i in grouped_pct:
print(i)
# (('Fri', 'No'), 91 0.184308
# 94 0.166667
# ......
# Name: tip_pct, dtype: float64)
# (('Fri', 'Yes'), 90 0.115518
# 92 0.210526
# ......
# Name: tip_pct, dtype: float64)
# ......
将函数名以字符串形式传递。
result = grouped_pct.agg('mean')
print(result)
# day smoker
# Fri No 0.179740
# Yes 0.216293
# Sat No 0.190412
# Yes 0.179833
# Sun No 0.193617
# Yes 0.322021
# Thur No 0.193424
# Yes 0.198508
# Name: tip_pct, dtype: float64
如果传递的是函数或者函数名的列表,会得到一个列名是这些函数名的DataFrame。 下面传递了聚合函数的列表给agg方法,这些函数会各自运用于数据分组。
result = grouped_pct.agg(['mean', 'std', peak_to_peak])
print(result)
# mean std peak_to_peak
# day smoker
# Fri No 0.179740 0.039458 0.094263
# Yes 0.216293 0.077530 0.242219
# Sat No 0.190412 0.058626 0.352192
# Yes 0.179833 0.089496 0.446137
# Sun No 0.193617 0.060302 0.274897
# Yes 0.322021 0.538061 2.382107
# Thur No 0.193424 0.056065 0.284273
# Yes 0.198508 0.057170 0.219047
如果传递的是(name, function)
元组的列表,每个元组的第一个元素将作为DataFrame的列名(可以认为二元元组的列表是一种有序的对应关系):
result = grouped_pct.agg([('foo', 'mean'), ('bar', np.std)]) # foo是mean值的列名
print(result)
# foo bar
# day smoker
# Fri No 0.179740 0.039458
# Yes 0.216293 0.077530
# Sat No 0.190412 0.058626
# Yes 0.179833 0.089496
# Sun No 0.193617 0.060302
# Yes 0.322021 0.538061
# Thur No 0.193424 0.056065
# Yes 0.198508 0.057170
可以指定应用到所有列上的函数列表或每一列上要应用的不同函数。
下面产生的DataFrame拥有分层列,与分别聚合每一列,再以列名作为keys
参数使用concat
将结果拼接在一起的结果相同。
functions = ['count', 'mean', 'max']
result = grouped[['tip_pct', 'total_bill']].agg(functions)
print(result)
# tip_pct total_bill
# count mean max count mean max
# day smoker
# Fri No 4 0.179740 0.231125 4 18.420000 22.75
# Yes 15 0.216293 0.357737 15 16.813333 40.17
# Sat No 45 0.190412 0.412409 45 19.661778 48.33
# Yes 42 0.179833 0.483092 42 21.276667 50.81
# Sun No 57 0.193617 0.338101 57 20.506667 48.17
# Yes 19 0.322021 2.452381 19 24.120000 45.35
# Thur No 45 0.193424 0.362976 45 17.113111 41.19
# Yes 17 0.198508 0.317965 17 19.190588 43.11
# 把['tip_pct', 'total_bill']改成[['tip_pct', 'total_bill']],就可以避免报错
# FutureWarning: Indexing with multiple keys (implicitly converted to a tuple of keys) will be deprecated, use a list instead.
# result = grouped['tip_pct', 'total_bill'].agg(functions)
print(result['tip_pct'])
# count mean max
# day smoker
# Fri No 4 0.179740 0.231125
# Yes 15 0.216293 0.357737
# Sat No 45 0.190412 0.412409
# Yes 42 0.179833 0.483092
# Sun No 57 0.193617 0.338101
# Yes 19 0.322021 2.452381
# Thur No 45 0.193424 0.362976
# Yes 17 0.198508 0.317965
也同样可以传递具有自定义名称的元组列表:
ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
result = grouped[['tip_pct', 'total_bill']].agg(ftuples)
print(result)
# tip_pct total_bill
# Durchschnitt Abweichung Durchschnitt Abweichung
# day smoker
# Fri No 0.179740 0.001557 18.420000 25.596333
# Yes 0.216293 0.006011 16.813333 82.562438
# Sat No 0.190412 0.003437 19.661778 79.908965
# Yes 0.179833 0.008010 21.276667 101.387535
# Sun No 0.193617 0.003636 20.506667 66.099980
# Yes 0.322021 0.289509 24.120000 109.046044
# Thur No 0.193424 0.003143 17.113111 59.625081
# Yes 0.198508 0.003268 19.190588 69.808518
要将不同的函数应用到一个或多个列上,需要将含有列名与函数对应关系的字典传递给agg
:
result = grouped.agg({'tip': np.max, 'size': 'sum'})
print(result)
# 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
result = grouped.agg({'tip_pct': ['min', 'max', 'mean', 'std']})
print(result)
# tip_pct
# min max mean std
# day smoker
# Fri No 0.136861 0.231125 0.179740 0.039458
# Yes 0.115518 0.357737 0.216293 0.077530
# Sat No 0.060217 0.412409 0.190412 0.058626
# Yes 0.036955 0.483092 0.179833 0.089496
# Sun No 0.063204 0.338101 0.193617 0.060302
# Yes 0.070274 2.452381 0.322021 0.538061
# Thur No 0.078704 0.362976 0.193424 0.056065
# Yes 0.098918 0.317965 0.198508 0.057170
只有多个函数应用于至少一个列时,DataFrame才具有分层列。
返回不含行索引的聚合数据 ¶
在前面所有的例子中,聚合数据返回时都是带有索引的,有时索引是分层的,由唯一的分组键联合形成。
因为不是所有的情况下都需要索引,所以在大多数情况下可以通过向groupby传递as_index=False来禁用分组键作为索引的行为:
result = tips.groupby(['day', 'smoker'], as_index=False).mean()
print(result)
# day smoker total_bill tip size tip_pct
# 0 Fri No 18.420000 2.812500 2.250000 0.179740
# 1 Fri Yes 16.813333 2.714000 2.066667 0.216293
# 2 Sat No 19.661778 3.102889 2.555556 0.190412
# 3 Sat Yes 21.276667 2.875476 2.476190 0.179833
# 4 Sun No 20.506667 3.167895 2.929825 0.193617
# 5 Sun Yes 24.120000 3.516842 2.578947 0.322021
# 6 Thur No 17.113111 2.673778 2.488889 0.193424
# 7 Thur Yes 19.190588 3.030000 2.352941 0.198508
通过在结果上调用reset_index也可以获得同样的结果。使用as_index=False可以避免一些不必要的计算。
result = tips.groupby(['day', 'smoker']).mean()
print(result.reset_index())
# day smoker total_bill tip size tip_pct
# 0 Fri No 18.420000 2.812500 2.250000 0.179740
# 1 Fri Yes 16.813333 2.714000 2.066667 0.216293
# 2 Sat No 19.661778 3.102889 2.555556 0.190412
# 3 Sat Yes 21.276667 2.875476 2.476190 0.179833
# 4 Sun No 20.506667 3.167895 2.929825 0.193617
# 5 Sun Yes 24.120000 3.516842 2.578947 0.322021
# 6 Thur No 17.113111 2.673778 2.488889 0.193424
# 7 Thur Yes 19.190588 3.030000 2.352941 0.198508
print(result)
# 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
应用:通用拆分-应用-联合 ¶
import pandas as pd
import numpy as np
import statsmodels.api as sm
GroupBy
方法最常见的目的是apply
(应用)。apply
将对象拆分成多块,然后在每一块上调用传递的函数,之后尝试将每一块拼接到一起。
根据下面的小费数据集,按组选出小费百分比(tip-pct)最高的五组。
tips = pd.read_csv('../examples/tips.csv')
tips['tip_pct'] = tips['tip'] / (tips['total_bill'] - tips['tip'])
样本数据
print(tips.head(5))
# 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
首先,写一个可以在特定列中选出最大值所在行的函数:
添加了升序,结果输出最后5行(最后的5行也是最大的5个tip_tcp
记录)。
def top(df, n=5, column='tip_pct'):
return df.sort_values(by=column, ascending=True)[-n:]
result = top(tips, n=6)
print(result)
# 等价方式:
# result = tips.sort_values('tip_pct')[-6:]
# print(result)
# total_bill tip smoker day time size tip_pct
# 109 14.31 4.00 Yes Sat Dinner 2 0.387973
# 183 23.17 6.50 Yes Sun Dinner 4 0.389922
# 232 11.61 3.39 No Sat Dinner 2 0.412409
# 67 3.07 1.00 Yes Sat Dinner 1 0.483092
# 178 9.60 4.00 Yes Sun Dinner 2 0.714286
# 172 7.25 5.15 Yes Sun Dinner 2 2.452381
如果按照smoker
进行分组,之后调用apply
,会得到以下结果:
top
函数在DataFrame的每一行分组上被调用,之后使用pandas.concat
将函数结果粘贴在一起,并使用分组名作为各组的标签。 因此结果包含一个分层索引,该分层索引的内部层级包含原DataFrame的索引值。
result = tips.groupby('smoker').apply(top)
print(result)
# total_bill tip smoker day time size tip_pct
# smoker
# No 88 24.71 5.85 No Thur Lunch 2 0.310180
# 185 20.69 5.00 No Sun Dinner 5 0.318674
# 51 10.29 2.60 No Sun Dinner 2 0.338101
# 149 7.51 2.00 No Thur Lunch 2 0.362976
# 232 11.61 3.39 No Sat Dinner 2 0.412409
# Yes 109 14.31 4.00 Yes Sat Dinner 2 0.387973
# 183 23.17 6.50 Yes Sun Dinner 4 0.389922
# 67 3.07 1.00 Yes Sat Dinner 1 0.483092
# 178 9.60 4.00 Yes Sun Dinner 2 0.714286
# 172 7.25 5.15 Yes Sun Dinner 2 2.452381
如果除了向apply
传递函数,还传递其他参数或关键字的话,你可以把这些放在函数后进行传递。
result = tips.groupby('smoker').apply(top, n=1, column='total_bill')
print(result)
# 这2行都是smoker是yes和no时最大total_bill值所在行。
# total_bill tip smoker day time size tip_pct
# smoker
# No 212 48.33 9.0 No Sat Dinner 4 0.228833
# Yes 170 50.81 10.0 Yes Sat Dinner 3 0.245038
在GroupBy
对象上调用describe
方法。
result = tips.groupby('smoker')['tip_pct'].describe()
print(result)
# count mean std ... 50% 75% max
# smoker ...
# No 151.0 0.192237 0.057665 ... 0.184308 0.227015 0.412409
# Yes 93.0 0.218176 0.254295 ... 0.181818 0.242326 2.452381
# [2 rows x 8 columns]
print(result.unstack('smoker')) # 类似于转置
# smoker
# count No 151.000000
# Yes 93.000000
# mean No 0.192237
# Yes 0.218176
# std No 0.057665
# Yes 0.254295
# min No 0.060217
# Yes 0.036955
# 25% No 0.158622
# Yes 0.119534
# 50% No 0.184308
# Yes 0.181818
# 75% No 0.227015
# Yes 0.242326
# max No 0.412409
# Yes 2.452381
# dtype: float64
在GroupBy
对象的内部,当调用像describe
这样的方法时,实际上是以下代码的简写:
grouped = tips.groupby(['smoker'])
f = lambda x: x.describe()
result = grouped.apply(f)
print(result)
# total_bill tip size tip_pct
# smoker
# No count 151.000000 151.000000 151.000000 151.000000
# mean 19.188278 2.991854 2.668874 0.192237
# std 8.255582 1.377190 1.017984 0.057665
# min 7.250000 1.000000 1.000000 0.060217
# 25% 13.325000 2.000000 2.000000 0.158622
# 50% 17.590000 2.740000 2.000000 0.184308
# 75% 22.755000 3.505000 3.000000 0.227015
# max 48.330000 9.000000 6.000000 0.412409
# Yes count 93.000000 93.000000 93.000000 93.000000
# mean 20.756344 3.008710 2.408602 0.218176
# std 9.832154 1.401468 0.810751 0.254295
# min 3.070000 1.000000 1.000000 0.036955
# 25% 13.420000 2.000000 2.000000 0.119534
# 50% 17.920000 3.000000 2.000000 0.181818
# 75% 26.860000 3.680000 3.000000 0.242326
# max 50.810000 10.000000 5.000000 2.452381
压缩分组键 ¶
在前面的例子中所得到的对象,都具有分组键所形成的分层索引以及每个原始对象的索引。 也可以通过向groupby
传递group_keys=False
来禁用这个功能。
result = tips.groupby('smoker', group_keys=True).apply(top)
print(result)
# total_bill tip smoker day time size tip_pct
# smoker
# No 88 24.71 5.85 No Thur Lunch 2 0.310180
# 185 20.69 5.00 No Sun Dinner 5 0.318674
# 51 10.29 2.60 No Sun Dinner 2 0.338101
# 149 7.51 2.00 No Thur Lunch 2 0.362976
# 232 11.61 3.39 No Sat Dinner 2 0.412409
# Yes 109 14.31 4.00 Yes Sat Dinner 2 0.387973
# 183 23.17 6.50 Yes Sun Dinner 4 0.389922
# 67 3.07 1.00 Yes Sat Dinner 1 0.483092
# 178 9.60 4.00 Yes Sun Dinner 2 0.714286
# 172 7.25 5.15 Yes Sun Dinner 2 2.452381
result = tips.groupby('smoker', group_keys=False).apply(top)
print(result)
# total_bill tip smoker day time size tip_pct
# 88 24.71 5.85 No Thur Lunch 2 0.310180
# 185 20.69 5.00 No Sun Dinner 5 0.318674
# 51 10.29 2.60 No Sun Dinner 2 0.338101
# 149 7.51 2.00 No Thur Lunch 2 0.362976
# 232 11.61 3.39 No Sat Dinner 2 0.412409
# 109 14.31 4.00 Yes Sat Dinner 2 0.387973
# 183 23.17 6.50 Yes Sun Dinner 4 0.389922
# 67 3.07 1.00 Yes Sat Dinner 1 0.483092
# 178 9.60 4.00 Yes Sun Dinner 2 0.714286
# 172 7.25 5.15 Yes Sun Dinner 2 2.452381
分位数与桶分析 ¶
第8章中,pandas有一些工具,尤其是cut
和qcut
,用于将数据按照你选择的箱位或样本分位数进行分桶。 与groupby
方法一起使用这些函数可以对数据集更方便地进行分桶或分位分析。
复习:机械学习中的分箱处理。
在机械学习中经常会对数据进行分箱处理的操作, 也就是把一段连续的值切分成若干段,每一段的值看成一个分类。这个把连续值转换成离散值的过程,我们叫做分箱处理。
比如,把年龄按15岁划分成一组,0-15岁叫做少年,16-30岁叫做青年,31-45岁叫做壮年。在这个过程中,我们把连续的年龄分成了三个类别,“少年”,“青年”和“壮年”就是各个类别的名称,或者叫做标签。
在pandas中,cut
和qcut
函数都可以进行分箱处理操作。
cut()
按照变量的值对变量进行分割,每个分组里数据的个数并不一样。qcut()
是按变量的数量来对变量进行分割,并且尽量保证每个分组里变量的个数相同。
考虑下面一个简单的随机数据集和一个使用cut
的等长桶分类:
df = pd.DataFrame(
{
'data1': np.random.randn(1000),
'data2': np.random.randn(1000)
}
)
quartiles = pd.cut(df.data1, 4) # 按照data1值由小到大的顺序将数据分成4份,并且使每组值的范围大致相等。
print(quartiles[:10])
# 0 (-0.0743, 1.729]
# 1 (-0.0743, 1.729]
# 2 (-0.0743, 1.729]
# 3 (-0.0743, 1.729]
# 4 (-1.877, -0.0743]
# 5 (-0.0743, 1.729]
# 6 (-0.0743, 1.729]
# 7 (-0.0743, 1.729]
# 8 (-1.877, -0.0743]
# 9 (-0.0743, 1.729]
# Name: data1, dtype: category
# Categories (
# 4,
# interval[float64, right]): [
# (-3.687, -1.877] < (-1.877, -0.0743] < (-0.0743, 1.729] < (1.729, 3.531]
# ]
上面cut
返回的Categorical
对象可以直接传递给groupby
。利用它计算出data2
列的一个统计值集合,如下:
def get_stats(group):
return {
'min': group.min(),
'max': group.max(),
'count': group.count(),
'mean': group.mean()
}
grouped = df.data2.groupby(quartiles)
for i in grouped:
print(i)
result = grouped.apply(get_stats).unstack()
print(result)
# min max count mean
# data1
# (-3.145, -1.424] -1.759377 2.484321 77.0 -0.127900
# (-1.424, 0.29] -3.142344 2.830654 524.0 -0.081931
# (0.29, 2.005] -3.557136 3.261635 376.0 0.015715
# (2.005, 3.719] -2.829458 1.766352 23.0 -0.198780
使用qcut
,根据样本分位数计算出等大小的桶,就是等长桶。通过传递labels=False
来获得分位数数值。
grouping = pd.qcut(df.data1, 10, labels=False)
grouped = df.data2.groupby(grouping)
result = grouped.apply(get_stats).unstack()
print(result)
# min max count mean
# data1
# 0 -3.678934 3.022862 100.0 0.029658
# 1 -2.319813 2.646502 100.0 0.094035
# 2 -2.873727 2.470840 100.0 0.023866
# 3 -2.196701 2.042251 100.0 0.021232
# 4 -2.154161 2.020809 100.0 0.110834
# 5 -2.723061 2.415626 100.0 0.057365
# 6 -2.291470 2.536159 100.0 0.020866
# 7 -2.064083 1.799356 100.0 -0.081025
# 8 -3.405679 1.792581 100.0 -0.009705
# 9 -2.469285 2.600849 100.0 -0.061721
示例:使用指定分组值填充缺失值 ¶
在清除缺失值时,有时会使用dropna
来去除缺失值,有时使用修正值或来自于其他数据的值来输入(填充)到null
值(NA
)。 fillna
是一个可以使用的正确工具。
例如下面例子中使用使用平均值来填充NA值:
data = (100, 110, 120, 130, 140, 150)
s = pd.Series(data)
print(s)
# 0 100
# 1 110
# 2 120
# 3 130
# 4 140
# 5 150
# dtype: float64
将数据中的一些值设置为缺失值:
s[::2] = np.nan
print(s)
# 0 NaN
# 1 110.0
# 2 NaN
# 3 130.0
# 4 NaN
# 5 150.0
# dtype: float64
result = s.fillna(s.mean()) # 110, 130, 150的平均值是130
print(result)
# 0 130.0
# 1 110.0
# 2 130.0
# 3 130.0
# 4 130.0
# 5 150.0
# dtype: float64
下面的例子是按组填充NA值:
- 方法1,对数据分组后使用
apply
。 - 方法2,在每个数据块上都调用
fillna
的函数。
data = (100, 110, 120, 130, 140, 150, 160, 170)
states = ['Ohio', 'New York', 'Vermont', 'Florida', 'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4 # 4个East和4个West拼接的列表list
s = pd.Series(data, index=states)
print(s)
# Ohio 100
# New York 110
# Vermont 120
# Florida 130
# Oregon 140
# Nevada 150
# California 160
# Idaho 170
# dtype: int64
将数据中的一些值设置为缺失值:
s[['Vermont', 'Nevada', 'Idaho']] = np.nan
print(s)
# Ohio 100.0
# New York 110.0
# Vermont NaN
# Florida 130.0
# Oregon 140.0
# Nevada NaN
# California 160.0
# Idaho NaN
# dtype: float64
result = s.groupby(group_key).mean()
print(result)
# East 113.333333
# West 150.000000
# dtype: float64
用上面得出的分组平均值来填充NA。
fill_mean = lambda g: g.fillna(g.mean())
result = s.groupby(group_key).apply(fill_mean)
print(result)
# Ohio 100.000000
# New York 110.000000
# Vermont 113.333333
# Florida 130.000000
# Oregon 140.000000
# Nevada 150.000000
# California 160.000000
# Idaho 150.000000
# dtype: float64
如果已经在代码中为每个分组预定义了填充值,可以利用每个分组都有的内置的name
属性,实现填充NA
。
fill_value = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_value[g.name])
result = s.groupby(group_key).apply(fill_func)
print(result)
# Ohio 100.0
# New York 110.0
# Vermont 0.5
# Florida 130.0
# Oregon 140.0
# Nevada -1.0
# California 160.0
# Idaho -1.0
# dtype: float64
示例:随机采样与排列 ¶
假设想从大数据集中抽取随机样本(有或没有替换)以用于蒙特卡罗模拟目的或某些其他应用程序。 有很多方法来执行“抽取”,这里使用Series的sample方法。
为了演示,这里介绍一种构造一副英式扑克牌的方法:
# 梅花clubs、方块diamonds、红桃hearts、黑桃spades。
suits = ['H', 'S', 'C', 'D']
card_val = (list(range(1, 11)) + [10] * 3) * 4
# card_val [
# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10,
# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10,
# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10,
# 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10
# ]
base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
# base_names: ['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'K', 'Q']
生成了一个长度为52
的Series, Series的索引包含了牌名,Series的值可以用游戏(为了保持简单,让’A’为1 ):
cards = []
for suit in ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)
print(deck)
# 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
# AS 1
# 2S 2
# 3S 3
# 4S 4
# 5S 5
# 6S 6
# 7S 7
# 8S 8
# 9S 9
# 10S 10
# JS 10
# KS 10
# QS 10
# AC 1
# 2C 2
# 3C 3
# 4C 4
# 5C 5
# 6C 6
# 7C 7
# 8C 8
# 9C 9
# 10C 10
# JC 10
# KC 10
# QC 10
# AD 1
# 2D 2
# 3D 3
# 4D 4
# 5D 5
# 6D 6
# 7D 7
# 8D 8
# 9D 9
# 10D 10
# JD 10
# KD 10
# QD 10
# dtype: int64
从这副牌中拿出五张牌可以写成:
def draw(_deck, n=5):
return _deck.sample(n)
print(draw(deck))
# KD 10
# 2S 2
# 5C 5
# 6C 6
# QD 10
# dtype: int64
假设要从每个花色中随机抽取两张牌。由于花色是牌名的最后两个字符,可以基于这点进行分组,并使用apply
:
get_suit = lambda card: card[-1] # 最后一个字母是花色
result = deck.groupby(get_suit).apply(draw, n=2)
print(result)
# C 10C 10
# 3C 3
# D KD 10
# AD 1
# H 5H 5
# 7H 7
# S 3S 3
# 5S 5
# dtype: int64
或者也可以写成:
result = deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
print(result)
# JC 10
# 8C 8
# QD 10
# 4D 4
# 10H 10
# 6H 6
# 7S 7
# KS 10
# dtype: int64
示例:分组加权平均和相关性 ¶
在groupby
的拆分-应用-联合的范式下,DataFrame的列间操作或两个Seriese之间的操作,例如实现分组加权平均。
下面例子,使用一个包含分组键和权重值的数据集:
dt = np.random.randn(8)
wt = np.random.randn(8)
df = pd.DataFrame(
{
'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
'data': dt,
'weight': wt
}
)
print(df)
# category data weight
# 0 a -0.250764 -0.085285
# 1 a 0.167155 -1.361254
# 2 a 0.399306 1.755542
# 3 a -0.514477 0.270124
# 4 b -0.005558 0.886514
# 5 b 0.607596 -1.384315
# 6 b -1.029627 -0.845340
# 7 b -0.294204 1.253965
通过category
进行分组加权平均如下:
grouped = df.groupby('category')
get_wavg = lambda g: np.average(g['data'], weights=g['weight'])
result = grouped.apply(get_wavg)
print(result)
# category
# a 0.614499
# b 3.863947
# dtype: float64
另一个例子,一个从雅虎财经上获得的数据集,该数据集包含一些标普500 (SPX符号)和股票的收盘价:
close_px = pd.read_csv('../examples/stock_px_2.csv', parse_dates=True, index_col=0)
print(close_px.info())
# <class 'pandas.core.frame.DataFrame'>
# DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
# Data columns (total 4 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 AAPL 2214 non-null float64
# 1 MSFT 2214 non-null float64
# 2 XOM 2214 non-null float64
# 3 SPX 2214 non-null float64
# dtypes: float64(4)
# memory usage: 86.5 KB
# None
print(close_px[-4:])
# AAPL MSFT XOM SPX
# 2011-10-11 400.29 27.00 76.27 1195.54
# 2011-10-12 402.19 26.96 77.16 1207.25
# 2011-10-13 408.43 27.18 76.37 1203.66
# 2011-10-14 422.00 27.27 78.11 1224.58
目标任务:计算一个DataFrame,它包含标普指数(SPX)每日收益的年度相关性(通过百分比变化计算)。
首先创建一个计算每列与’SPX’列成对关联的函数:
spx_corr = lambda x: x.corrwith(x['SPX'])
之后,使用pct_change
计算close-px
百分比的变化:
rets = close_px.pct_change().dropna() # Percentage change between the current and a prior element.
print(rets)
# AAPL MSFT XOM SPX
# 2003-01-03 0.006757 0.001421 0.000684 -0.000484
# 2003-01-06 0.000000 0.017975 0.024624 0.022474
# ... ... ... ... ...
# 2011-10-14 0.033225 0.003311 0.022784 0.017380
# [2213 rows x 4 columns]
最后,按年对百分比变化进行分组,可以使用单行函数从每个行标签中提取每个datetime
标签的year
属性:
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
result = by_year.apply(spx_corr)
print(result)
# 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
可以计算内部列相关性。这里计算了苹果和微软的年度相关性:
result = by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
print(result)
# 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
示例:逐组线性回归 ¶
定义以下regress
(回归)函数(使用statsmodels
计量经济学库),该函数对每个数据块执行普通最小二乘(OLS)回归:
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回报上的年度线性回归:
result = by_year.apply(regress, 'AAPL', ['SPX'])
print(result)
# 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
数据透视表与交叉表 ¶
数据透视表 ¶
数据透视表是电子表格程序和其他数据分析软件中常见的数据汇总工具。 它根据一个或多个键聚合一张表的数据,将数据在矩形格式中排列,其中一些分组键是沿着行的,另一些是沿着列的。
Python中的pandas透视表是通过这里所介绍的groupby工具以及使用分层索引的重塑操作实现的。
DataFrame拥有一个pivot_table
方法,并且还有还一个顶层的pandas.pivot_table
函数。
除了为groupby
提供一个方便接口,pivot_table
还可以添加部分总计,也称作边距。
import pandas as pd
import numpy as np
根据下面的小费数据集,计算一张在行方向上按day
和smoker
排列的分组平均值(默认的pivot_table
聚合类型)的表。
pivot_table
选项:
- values: 需要聚合的列名,默认情况下聚合所有数值型的列。
- index: 在结果透视表的行上进行分组的列名或者其他分组键。
tips = pd.read_csv('../examples/tips.csv')
tips['tip_pct'] = tips['tip'] / (tips['total_bill'] - tips['tip'])
样本数据。
print(tips.head(5))
# 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
计算在行方向上按day
和smoker
排列的分组平均值。也可以直接使用groupby
实现。
result = tips.pivot_table(index=['day', 'smoker'])
print(result)
# size tip tip_pct total_bill
# day smoker
# Fri No 2.250000 2.812500 0.179740 18.420000
# Yes 2.066667 2.714000 0.216293 16.813333
# Sat No 2.555556 3.102889 0.190412 19.661778
# Yes 2.476190 2.875476 0.179833 21.276667
# Sun No 2.929825 3.167895 0.193617 20.506667
# Yes 2.578947 3.516842 0.322021 24.120000
# Thur No 2.488889 2.673778 0.193424 17.113111
# Yes 2.352941 3.030000 0.198508 19.190588
在tip_pct
和size
上进行聚合,并根据time
分组。将把smoker
放入表的列,而将day
放入表的行:
result = tips.pivot_table(
['tip_pct', 'size'],
index=['time', 'day'],
columns='smoker'
)
print(result)
# 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
行和列标签,其中相应的值是单层中所有数据的分组统计值。
这里All
的值是均值,且该均值是不考虑吸烟者与非吸烟者(All
列)或行分组中任何两级的(All
行)。
result = tips.pivot_table(
['tip_pct', 'size'],
index=['time', 'day'],
columns='smoker',
margins=True
)
print(result)
# 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
。例如,count
或者len
将给出一张分组大小的交叉表(计数或出现频率):
result = tips.pivot_table(
['tip_pct', 'size'],
index=['time', 'day'],
columns='smoker',
aggfunc=len,
margins=True
)
print(result)
# size tip_pct
# smoker No Yes All No Yes All
# time day
# Dinner Fri 3.0 9.0 12 3.0 9.0 12
# Sat 45.0 42.0 87 45.0 42.0 87
# Sun 57.0 19.0 76 57.0 19.0 76
# Thur 1.0 NaN 1 1.0 NaN 1
# Lunch Fri 1.0 6.0 7 1.0 6.0 7
# Thur 44.0 17.0 61 44.0 17.0 61
# All 151.0 93.0 244 151.0 93.0 244
对于空值NA
,传递一个fill_value
。
result = tips.pivot_table(
['tip_pct', 'size'],
index=['time', 'day'],
columns='smoker',
aggfunc='mean',
fill_value=0,
margins=True
)
print(result)
# 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 0.000000 2.000000 0.190114 0.000000 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
交叉表:crosstab ¶
交叉表(简写为crosstab)是数据透视表的一个特殊情况,计算的是分组中的频率。crosstab
的前两个参数可是数组、Series或数组的列表。
sample = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
nationality = ['USA', 'Japan', 'USA', 'Japan', 'Japan', 'Japan', 'USA', 'USA', 'Japan', 'USA']
handedness = ['Right-handed', 'Left-handed', 'Right-handed', 'Right-handed', 'Left-handed', 'Right-handed',
'Right-handed', 'Left-handed', 'Right-handed', 'Right-handed']
df = pd.DataFrame(
{
'sample': sample,
'nationality': nationality,
'handedness': handedness
}
)
print(df)
# 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
按照国籍和惯用性来总结这些数据,可以使用pivot_table
来实现这个功能,但是pandas.crosstable
函数更为方便:
result = pd.crosstab(df.nationality, df.handedness, margins=True)
print(result)
# handedness Left-handed Right-handed All
# nationality
# Japan 2 3 5
# USA 1 4 5
# All 3 7 10
在小费数据中可以这么做:
result = pd.crosstab(['tips.time', tips.day], tips.smoker, margins=True)
print(result)
# smoker No Yes All
# row_0 day
# tips.time Fri 4 15 19
# Sat 45 42 87
# Sun 57 19 76
# Thur 45 17 62
# All 151 93 244