# -*- coding:utf-8 -*-
# 《python for data analysis》第九章
# 数据聚合与分组运算
import pandas as pd
import numpy as np
import time
# 分组运算过程 -> split-apply-combine
# 拆分 应用 合并
start = time.time()
np.random.seed(10)
# 1、GroupBy技术
# 1.1、引文
df = pd.DataFrame({
'key1': ['a', 'b', 'a', 'b', 'a'],
'key2': ['one', 'two', 'two', 'two', 'one'],
'data1': np.random.randint(1, 10, 5),
'data2': np.random.randn(5)
})
print(df)
print('\n')
grouped = df['data1'].groupby(df['key1']) # split,将data1列按照key1列进行分组
res = grouped.mean() # apply and combine, 将各组数据取平均之后汇合成一个DataFrame or Series
print(res)
print('\n')
# 上面的data['key1'](即传入groupby的对象)称为分组键,分组键可以是多个列
print(df['data1'].groupby([df['key1'], df['key2']]).mean()) # 结果为一个层次化索引的Series
print('\n')
# print(df['data1'].groupby(['key1', 'key2'])) # 与上一行等价,即可用columns名来替代series作为分组键
print(df.groupby('key1').mean()) # 聚合(split+apply+combine)只对数值列(data1、data2)进行操作,非数值列(key2)会被过滤
print('\n')
print(df.groupby('key1').size()) # 各分组的元素个数
print('\n')
# 1.2、对分组进行迭代
# groupby对象像list、dictionary那样支持迭代
for name, group in df.groupby('key1'):
print(name)
print(group) # split之后仍然保留非数值列
# 多重键用元组(tuple)表示
print('\n')
for (key1, key2), group in df.groupby(['key1', 'key2']):
print(key1)
print(key2)
print(group)
# groupby缺省在axis=0上进行分组,可显示指定axis=1进行分组
# 可理解为对样本进行分组和对特征进行分组
# 1.3、选取一个或一组列
# 实质:groupby对象的索引
# 1.1节中对部分列进行聚合操作写为df['data1'].groupby('key1'),该操作也可以通过对整个dataframe的groupby对象进行索引获得
print('\n')
print(df.groupby('key1')['data1'].mean())
print('\n')
# 1.4、通过字典或Series进行分组
df = pd.DataFrame(np.random.randn(5, 5), index=['a', 'b', 'c', 'd', 'a'], columns=['zoo', 'zip', 'zero', 'zz', 'zzz'])
mapping = {'a': 'red', 'b': 'green', 'c': 'red', 'd': 'green', 'e': 'pink'}
print(df.groupby(mapping).mean())
print('\n')
# 1.5、通过函数进行分组
print(df.groupby(len, axis=1).sum()) # 对axis=1上的各列名求取字符串长度,长度值作为分组依据
print('\n')
# 注意,传入groupby的分组键可以是多种类型的混合
key = ['one', 'two', 'one', 'two', 'one']
print(df.groupby([len, key], axis=1).sum())
print('\n')
# 1.6、根据索引级别分组
# 当index或者columns为层次化的索引时,在groupby中可以指定按照哪一层索引进行分组
columns = pd.MultiIndex.from_arrays([['A', 'A', 'A', 'B', 'B'], [1, 2, 3, 4, 5]], names=['level1', 'level2'])
df = pd.DataFrame(np.random.randn(5, 5), columns=columns)
print(df.groupby(level='level1', axis=1).sum()) # level关键字用于指定需要进行分组的索引层次
print('-----------------------------------↑section1')
# 2、数据聚合
# 2.1、引文
# groupby方法实现了split(分组),配合.sum()、.mean()等方法又实现了apply和combine,即聚合
# 具体地,聚合的方法可以是Series的各种方法,具体实现过程为
# step1——通过groupby将series进行切片(分组)——split
# step2——应用各种聚合函数,即上文提到的Series的各种方法,对各个切片进行操作——apply
# step3——将各个切片的运算结果进行组装——combine
# 除了Series已有的各种方法,还可以自己定义聚合函数并应用于groupby对象,通过agg或者aggregate方法传入
def pk2pk(gb):
return gb.max() - gb.min()
pk2pk_lambda = lambda gb: gb.max() - gb.min()
df = pd.DataFrame({
'key1': ['a', 'b', 'a', 'b', 'a'],
'key2': ['one', 'two', 'two', 'two', 'one'],
'data1': np.random.randint(1, 10, 5),
'data2': np.random.randn(5)
})
print(df)
print(df.groupby('key1').agg(pk2pk)) # 普通函数
print(df.groupby('key1').aggregate(pk2pk_lambda)) # lambda函数,agg和aggregat等价
print('\n')
# 2.2、面向列的多函数应用
# 2.2与2.3节介绍高级的聚合功能,以某个关于小费的数据集为例。
data = pd.read_csv('./data_set/tips.csv')
# 新增一列“小费占总额的比例”
data['tip_p'] = data['tip'] / data['total_bill']
print(data.head())
print('\n')
# 高级聚合:对不同的列采用不同的聚合函数,或一次性采用多个聚合函数
grouped = data.groupby(['smoker', 'day']) # groupby object
grouped_p = grouped['tip_p'] # groupby object的一个切片
print(grouped_p.agg('mean')) # 对切片使用mean方法
print('\n')
# 注意:agg里面传入预设函数以string形式,传入自定义函数以函数名形式
# 传入一组函数,则会形成一个以函数名为列名的columns
print(grouped_p.agg(['mean', 'std', pk2pk, pk2pk_lambda]))
print('\n')
# lambda函数的缺省函数名均为<lambda>,无辨识度,需要别的方式来区分,即自定义函数名
# 自定义列名,以(name,function)的元组形式传入即可,其中name为自定义的名称,function为函数名
print(grouped_p.agg([('Mean', 'mean'), ('Std', 'std'), ('Peak2Peak', pk2pk), ('Peak2Peak_2', pk2pk_lambda)]))
print('\n')
# 更一般的情形,可以对dataframe的多个列采用多个聚合函数,此时的聚合结果将是一个层次化索引的dataframe
# 这相当于先对各列进行聚合再concat(axis=1)到一起
print(grouped['tip', 'tip_p'].agg(['mean', 'std', pk2pk, pk2pk_lambda]))
print('\n')
# 若对不同列采用不同的聚合函数,通过向agg方法传入一个从列名映射到函数名的字典即可
print(grouped['tip', 'tip_p'].agg({'tip': 'mean', 'tip_p': 'std'}))
print('')
print(grouped['tip', 'tip_p'].agg({'tip': ['mean', 'std', pk2pk], 'tip_p': ['sum', pk2pk_lambda]}))
print('\n')
# 2.3、以“无索引”的形式返回聚合数据
# 默认情况下,分组键会成为结果的索引,通过groupby函数的as_index关键字置为False即可以无索引方式返回,分组键会转而成为聚合结果的列(Series)
print(data.groupby(['smoker'], as_index=False).mean())
print('---------------------------------------↑section2')
# 3、分组级运算与转换
# 分组运算除了上面提到的聚合(各种聚合函数),还可以通过transform和apply实现更多的分组运算
# transform不改变原有dataframe(or series)的index,将分组运算的结果广播到各分组的各个元素中去,形成一个或多个新列
print(data)
print('\n')
print(data.groupby('day').transform(np.mean)) # 可用concat和原dataframe拼接到一起,axis = 1
# 3.1、apply:一般性的'拆分-应用-合并'
# apply可传入任意处理序列的函数,返回的结果完全由apply传入的函数决定
sort = lambda df, column, n: df.sort_values(by=column)[-n:]
print(data.groupby('smoker').apply(sort, column='tip', n=10)) # 返回按tip从大到小排列的前10行
print('')
# 上例中分组键会和原index构成层次化索引,但其实分组键的信息已经包含在原dataframe中了,可在分组时设置group_keys关键字为False来禁用分组键
print(data.groupby('smoker', group_keys=False).apply(sort, column='tip', n=10))
print('\n')
# 3.2、分位数与桶分析
# 这一节的内容是将qcut和cut的运算结果传入groupby函数实现按区间分组
df = pd.DataFrame({
'data1': np.random.randn(100),
'data2': np.random.randn(100)})
cut_data1 = pd.cut(df['data1'], 5) # 等区间长度切割成5段
# 按照data1的分段结果对data2进行分组,并统计每个分组的数量、平均值、标准差、最大值与最小值
print(df['data2'].groupby(cut_data1).apply(
lambda gp: {'count': gp.count(), 'max': gp.max(), 'min': gp.min(), 'std': gp.std(), 'mean': gp.mean()}).unstack())
print('')
# qcut也是同理,将桶由等长度变成了等数量
print(df['data2'].groupby(pd.qcut(df['data1'], 5)).apply(
lambda gp: {'count': gp.count(), 'max': gp.max(), 'min': gp.min(), 'std': gp.std(), 'mean': gp.mean()}).unstack())
print('\n')
# 3.3、示例:用特定于分组的值填充缺失值
# 其实就是先分组,再每组apply填充缺失值函数fillna
df = pd.DataFrame({
'key1': ['a', 'b', 'a', 'b', 'a'],
'key2': ['one', 'two', 'two', 'two', 'one'],
'data1': np.random.randint(1, 10, 5),
'data2': np.random.randn(5)
})
df.ix[2:3,'data2']=np.nan
print(df)
print('')
df = df.groupby('key1',group_keys=False).apply(lambda gp:gp.fillna(gp.mean()))
print(df)
print('-------------------------------------↑section3')
# 其余实例,均为关于apply的应用实例,传入不同函数,包括随机采样、取相关系数、线性回归等
# 4、透视表与交叉表
# 透视表与交叉表均可通过groupby实现,可以认为是groupby的快捷方式
# 4.1、透视表(pivot)
# 以day和time为axis=0方向分组,smoker为axis=1方向分组,透视表方法缺省聚合类型为计算各分组的平均数
print(data.pivot_table(['tip','size'], index=['day', 'time'], columns='smoker'))
# 上述过程也可通过groupby实现
print(data.groupby(['day', 'time', 'smoker'])['size', 'tip'].mean().unstack())
# pivot_table()函数中关键字margins设为True可以添加分项小计,包括行与列
print('\n')
print(data.pivot_table(['tip','size'], index=['day', 'time'], columns='smoker', margins=True))
# pivot_table默认的聚合函数是取平均,可通过aggfunc关键字进行显式指定
print('')
print(data.pivot_table(['tip','size'], index=['day', 'time'], columns='smoker', margins=True, aggfunc=sum))
# 4.2、交叉表(crosstab)
# 交叉表是一种用于计算分组的频率(频数)的特殊透视表
print('\n')
print(pd.crosstab([data['time'], data['day']], data['smoker'], margins=True))
print('--------------total time is %.5f s' % (time.time() - start))
# that's all