一、分组(groupby)
在数据的实际使用过程中常常需要对数据按某一列进行分组,例如将观察对象分成男生组和女生组、老人组和小孩组等。我们可以借助前面提到的取子集的方法从完整数据集中将其分割成不同的DataFrame:
因为链接sns的属于源成功与否可能与网络状态相关,部分同事可能链不上,提供离线版本数据源,用read_csv方法读取
tips.csv
import seaborn as snsimport pandas as pdtips_10 = sns.load_dataset('tips').sample(10, random_state=42)print(tips_10)# 取出女生样本tips_10_female = tips_10.loc[tips_10['sex'] == 'Female', :]# 男生样本tips_10_male = tips_10.loc[tips_10['sex'] == 'Male', :]print(tips_10_female)print(tips_10_male)
在上面的例子中,只有两个分组,因此我们只需要取两次子集就可以得到所有的分组,但是当组别比较多时使用上述方法就比较烦琐,Pandas提供了一个内置函数,可以方便对数据集进行分组:
grouped = tips_10.groupby('sex')print(grouped)
需要注意的是,输出groupby的分组结果是一个内存地址,其数据类型是DataFrameGroupBy对象,背后其实没有进行任何计算。当调用分组结果的属性或者方法时,才会进一步计算:
# 查看groupby的实际分组# 只会返回索引print(grouped.groups)# 计算平均值avgs = grouped.mean()print(avgs)# 从返回结果来看只有部分列计算了平均值# EAFP原则:“应取得原谅而非获得许可”,针对所有有可能的列执行该计算# 并静默删除其余列# 选择分组female = grouped.get_group('Female')print(female)# 遍历分组for sex_group in grouped:print(sex_group)# 返回数组# 根据数组的结构,修改循环,获取详细信息for sex_group in grouped:# 获取对象的类型print('the type is: {}\n'.format(type(sex_group)))# 获取对象的长度print('the length is: {}\n'.format(len(sex_group)))# 获取第一个元素first_element = sex_group[0]print('the first element is: {}\n'.format(first_element))# 第一个元素的类型print('it has a type of: {}\n'.format(type(first_element)))# 获取第二个元素second_element = sex_group[1]print('the second element is: {}\n'.format(second_element))# 第二个元素的类型print('it has a type of: {}\n'.format(type(second_element)))# 输出sex_groupprint('the group is:')print(sex_group)
二、聚合(agg)
聚合是分组之后常用的操作之一,一般来说聚合是指获取多个值并返回单个值的过程,如计算平均值等:
import seaborn as snsimport pandas as pdtips = sns.load_dataset('tips')avg_by_sex = tips.groupby('sex').agg('mean')print(avg_by_sex)
在Pandas的分组结果中,通过agg方法来调用聚合操作,常见的聚合操作见下表:
| 函数 | 用途 |
|---|---|
| min | 最小值 |
| max | 最大值 |
| sum | 求和 |
| mean | 均值 |
| median | 中位数 |
| std | 标准差 |
| var | 方差 |
| count | 计数 |
实际上,上述函数聚合操作可以直接进行调用,之所以引入agg方法是因为其还支持更多更复杂的操作。
# 一次进行多种聚合操作aggs_by_sex = tips.groupby('sex').agg(['min', 'mean', 'max'])print(aggs_by_sex)# 通过字典的形式对不同列采取不同的聚合操作dict_by_sex = tips.groupby('sex').agg({'total_bill': 'min','tip': 'mean','size': 'max'})# 传入自定义函数def my_mean(values):"""计算平均值"""sum = values.sum()n = len(values)return (sum / n)my_mean_by_sex = tips.groupby('sex').agg(my_mean)# 自定义函数存在多个输入参数时,使用方法和apply方法的调用类似
三、转换(transform)
前面提到的聚合操作是接受多个值并返回单个(聚合)值,而接下来我们要介绍的转换则是接受多个值并返回与这些值一一对应的转换值,即transform方法不会减少数据量。
假设在前面的数据集中,我们需要在原有的数据集中新增一列平均数,按照之前的聚合操作,实现的逻辑如下:
# 将分组后的结果转换为字典avg_by_sex_dict = tips.groupby('sex')['total_bill'].agg('mean').to_dict()print(avg_by_sex_dict)# 通过map方法将sex的值映射为对应的均值tips['avg_bill'] = tips['sex'].map(avg_by_sex_dict)
如果使用transform方法的话,只需要一行代码:
tips['avg_bill'] = tips.groupby('sex')['total_bill'].transform('mean')# 查看输出数据的行数print(tips.shape)transform_mean = tips.groupby('sex')['total_bill'].transform('mean')print(tips.shape)
transform的其他使用方法和和agg聚合操作类似,两者最主要的区别就在于返回的结果数据量是否减少了。
四、其他分组操作
4.1. 过滤器(filter)
顾名思义,主要用于对分组后的数据按照某种规则进行过滤操作。与groupby的例子一样,过滤操作也可以使用常规的取子集的操作来完成相同的处理,只是效率会更低一点。
# 重新加载tips数据集tips = sns.load_dataset('tips')# 查看原始数据的行数print(tips.shape)# 查看不同size的次数print(tips['size'].value_counts())
从输出结果来看,size为1、5和6的情况相对较少,对于使用数据的人来说可能不需要关注这些数据,因此需要过滤掉这些数据,可以在分组运算的基础之上使用filter方法来实现:
# 过滤数据,使每组至少包含30个观测值tips_filtered = tips.groupby('size').filter(lambda x: x['size'].count() >= 30)# 查看输出结果print(tips_filtered.shape)print(tips_filtered['size'].value_counts())
4.2. 应用(apply)
apply在之前的课程中介绍过,它相比agg和transform而言更加灵活,能够传入任意自定义的函数,实现复杂的数据操作。那在groupby后使用apply和之前所介绍的有什么区别呢?
两者的区别在于,对于groupby后的apply,以分组后的子DataFrame作为参数传入指定函数的,基本操作单位是DataFrame,而之前介绍的apply的基本操作单位是Series。下面以一个例子来说明apply的使用:
# 假设我们需要分别提取每一天消费最高的数据信息def max_bill(x):"""提取消费最高的数据"""df = x.sort_values(by='total_bill', ascending=False)return df.iloc[0, :]# 调用applymax_bill_info = tips.groupby('day').apply(max_bill)print(max_bill_info)
首先从自定义函数的函数体来看,其接受了一个DataFrame x,并将x按照total_bill进行降序排序,最后返回排序后的第一行,即最大值所在行。最后在调用时先根据day进行分组,分组后得到几个子DataFrame,将这些子DataFrame 都应用max_bill这个函数即可得到每个分组的最大值。
