pandas 数据分组

分组操作

  • 学习目标
    • 应用groupby 进行分组,并对分组数据进行聚合,转换和过滤
    • 应用自定义函数处理分组之后的数据

1 aggregate聚合

  • 在SQL中我们经常使用 GROUP BY 将某个字段,按不同的取值进行分组, 在pandas中也有groupby函数
  • 分组之后,每组都会有至少1条数据, 将这些数据进一步处理返回单个值的过程就是聚合,比如 分组之后计算算术平均值, 或者分组之后计算频数,都属于聚合

1.1 单变量分组聚合

需求:加载data/gapminder数据集,计算每一年的平均寿命

  • 加载数据
import pandas as pd
df = pd.read_csv('data/gapminder.tsv', sep='\t')
df
  • 通过df.groupby('年份').平均寿命.mean()来计算每一年的平均寿命
df.groupby('year').lifeExp.mean()
  • 过程及输出截图如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZ4ixxvf-1630499358929)(./img/聚合-01.png)]

  • 解析:

    • groupby语句创建了若干组, 例如上面例子中, 对year字段分组, 会将数据中不同年份作为分组结果
    # 对year字段(年份)进行去重,返回series对象
    years = df.year.unique()
    years
    
    • 挑选一个年份,提取df子集
    # 针对1952年的数据取子集
    y1952 = df.loc[df.year==1952,:]
    y1952
    
    • 原df中某一年的子集,再选择平均寿命字段,最后计算该字段的平均值
    y1952.lifeExp.mean()
    
    • 整个运行过程如下图所示

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkHdCsVZ-1630499358932)(./img/聚合-02.png)]

    • groupby 语句会针对每个不同年份重复上述过程,并把所有结果放入一个DataFrame中返回
    • mean函数不是唯一的聚合函数, Pandas内置了许多方法, 都可以与groupby语句搭配使用

1.2 Pandas内置的聚合函数

  • 可以与groupby一起使用的函数
Pandas方法Numpy函数说明
countnp.count_nonzero频率统计(不包含NaN值)
size频率统计(包含NaN值)
meannp.mean求平均值
median中位数
stdnp.std标准差
minnp.min最小值
quantilenp.percentile分位数
maxnp.max求最大值
sumnp.sum求和
varnp.var方差
describe计数、平均值、标准差,最小值、分位数、最大值
first返回第一行
last返回最后一行
nth返回第N行(Python从0开始计数)
  • 需求:使用groupby对洲进行分组,同时对分组结果利用describe函数对平均寿命进行统计,返回基本统计信息
df.groupby('continent').lifeExp.describe()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tm6vuPlY-1630499358934)(./img/聚合-03.png)]

1.3 其他聚合函数

1.3.1 使用numpy库的聚合函数

计算各大洲的平均寿命

  • 可以使用Numpy库的mean函数
import numpy as np
df.groupby('continent').lifeExp.agg(np.mean)
# 输出结果如下
continent
Africa      48.865330
Americas    64.658737
Asia        60.064903
Europe      71.903686
Oceania     74.326208
Name: lifeExp, dtype: float64
  • agg和aggregate效果一样
df.groupby('continent').lifeExp.aggregate(np.mean)
1.3.2 使用自定义的函数

计算各大洲的平均寿命

  • 如果想在聚合的时候,使用非Pandas或其他库提供的计算, 可以自定义函数,然后再aggregate中调用它
def my_mean(values):
    '''计算平均值
    '''
    n = len(values) # 获取数据条目数
    sum = 0
    for value in values:
        sum += value
    return(sum/n)
# 调用自定义函数
df.groupby('continent').lifeExp.agg(my_mean)
# 输出结果如下
continent
Africa      48.865330
Americas    64.658737
Asia        60.064903
Europe      71.903686
Oceania     74.326208
Name: lifeExp, dtype: float64

1.3.3 自定义聚合函数传入多个参数

需求:计算全球平均预期寿命的平均值,与分组之后的平均值做差

  • 自定义函数可以有多个参数, 具体用法如下;第一个参数接受来自DataFrame分组这之后的值, 其余参数可自定义
df.groupby('指定分组的字段').lifeExp.agg(
    自定义的函数名,
    #自定义函数的默认参数是df.groupby分组后传入的seriers对象【不用写】
    自定义函数所需要的额外参数1=参数1的值,
    自定义函数所需要的额外参数2=参数2的值,
    ...
)

  • 现在我们来计算本小节的需求,代码及计算结果如下
# 计算各大洲的平均寿命和全球平均寿命(整个数据集的平均寿命)的差值
def my_mean_diff(values, diff_value):
    '''计算平均值和diff_value的差值并返回
    '''
    n = len(values)
    sum = 0
    for value in values:
        sum+=value
    mean = sum/n
    return(mean-diff_value)
# 计算整个数据集的平均寿命
global_mean = df.lifeExp.mean()
print(global_mean)
# 调用自定义函数并传参
df.groupby('continent').lifeExp.agg(my_mean_diff, 
                                    diff_value=global_mean)
# 输出结果如下
59.47443936619713

continent
Africa     -10.609109
Americas     5.184297
Asia         0.590464
Europe      12.429247
Oceania     14.851769
Name: lifeExp, dtype: float64

1.4 同时传入多个函数

需求:按年份计算全球寿命的最大值、最小值,以及平均值

  • 分组之后可以同时使用多个聚合函数:可以把它们全部放入一个Python列表, 然后把整个列表传入agg或aggregate中
# 按年份计算全球寿命的最大值、最小值,以及平均值
df.groupby('year').lifeExp.agg([np.mean, np.max, np.min])
# 输出结果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WTCPzXPC-1630499358936)(./img/聚合-04.png)]

1.5 向agg中传入字典

需求:按年计算全球平均寿命、全球总人口,以及人均GDP的平均值

  • agg函数中可以传入字典,字典的key是df的列名,与key对应的value是pandas内置的聚合计算函数、其名称的字符串(可以查看本章1.2节内容可知Pandas常用内置聚合计算函数)
new_df = df.groupby('year').agg({'lifeExp':'mean', 
                                 'pop':'sum', 
                                 'gdpPercap':'mean'})
new_df

  • 对返回的新df可以通过rename进行重命名
new_df.rename(columns={'lifeExp':'平均寿命','pop':'人口','gdpPercap':'人均Gdp'}).reset_index()

  • 输出过程结果如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-unpuMemH-1630499358938)(./img/聚合-05.png)]

2 transform转换

  • transform 转换,需要把DataFrame中的值传递给一个函数, 而后由该函数"转换"数据。
  • aggregate(聚合) 返回单个聚合值,但transform 不会减少数据量

2.1 transform使用内置函数

  • 加载数据
import pandas as pd
df = pd.read_excel('data/sales_transactions.xlsx')
df

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eein8CDw-1630499358939)(./img/transfrom-01.png)]

数据解读:数据包含了不同的订单(order),以及订单里的不同商品的数量(quantity)、单价(unit price)和总价(ext price)

  • 现在我们的任务是为数据表添加一列,该订单的价钱总和:即计算每一单的总价,并且在原df中添加该列
# 关键步骤:使用transform调用pandas内置聚合函数sum,根据相同的order,对ext price字段求和
df.groupby('order')['ext price'].transform('sum')
# 输出结果如下
0      576.12
1      576.12
2      576.12
3     8185.49
4     8185.49
5     8185.49
6     8185.49
7     8185.49
8     3724.49
9     3724.49
10    3724.49
11    3724.49
Name: ext price, dtype: float64

  • 具体实现上述需求的完整代码
# 计算每一单的总价,并且在原df中添加该列
# 直接在原df中添加列
df['Order_Total'] = df.groupby('order')['ext price'].transform('sum')
df

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wAglUSzX-1630499358941)(./img/transfrom-02.png)]

  • 整个过程中发生变化的过程如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQJpRxUm-1630499358942)(./img/transfrom-03.png)]

2.2 transform使用自定义函数

transform除了可以使用pandas的内置函数以外,transform还可以使用自定义的函数

  • 重新加载数据
# 重新加载数据
df = pd.read_excel('data/sales_transactions.xlsx')
df

  • 需求:批量修改商品的单价,使其单价增加1元,并添加新列new unit price
# 自定义一个计算函数
def returnOrderTotal(x):
    return x + 1
# transform使用自定义的函数,注意此时传入的函数名没有引号
df['new unit price'] = df.groupby('order')['unit price'].transform(returnOrderTotal)
df

  • 输出结果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vW030LgX-1630499358943)(./img/transfrom-04.png)]

2.3 transform自定义函数使用多个参数

transform使用的自定义函数如果有多个参数时,如下

# transform自定义函数有多个参数
def returnOrderTotal(x, y):
    return x + y
df['new unit price'] = df.groupby('order')['unit price'].transform(returnOrderTotal, y=3)
df
# 输出结果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lnV70PI1-1630499358944)(./img/transfrom-05.png)]

2.4 transform分组填充缺失值

某些特定情况下,可以考虑将列进行分组,分组之后取平均再填充缺失值

  • 加载数据
# 加载数据 设定随机种子 随机抽取10行数据 
tips_10 = pd.read_csv('data/tips.csv').sample(10, random_state=42)
tips_10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKsKDvD1-1630499358950)(./img/transfrom-06.png)]

  • 构建缺失值
# 构建缺失值
print([i for i in tips_10['total_bill']])
import numpy as np
tips_10['total_bill'] = [
    np.NaN, 8.77, 24.55, 25.89, 13.0, 
    np.NaN, 28.44, 12.48, np.NaN, np.NaN
]
tips_10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxIqDNPu-1630499358951)(./img/transfrom-07.png)]

  • 查看缺失情况
# 通过groupby之后再通过count函数计算有效值的数量,即可看出缺失值的情况
count_sex = tips_10.groupby('sex').count()
count_sex
# 通过下图得知:total_bill列,最少有4个缺失值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0TntYco-1630499358952)(./img/transfrom-08.png)]

  • transform使用自定义函数填充缺失值
def fill_nan_mean(x):
    # 求平均
    avg = x.mean()
    # 填充缺失值:非空值不变,空值被填入
    return x.fillna(avg)

total_bill_group_mean = tips_10.groupby('sex').total_bill.transform(fill_nan_mean)
print(total_bill_group_mean)
tips_10['total_bill'] = total_bill_group_mean
tips_10
# 输出结果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-247i6G37-1630499358952)(./img/transfrom-09.png)]

  • 对比total_bill 和 fill_total_bill 发现 Male 和 Female 的填充值不同

3 filter过滤

需求:某一家餐馆要开连锁分店,新分店要采购餐桌;买什么样的餐桌就是一个问题:需要确定餐桌的可坐人数;可以从老店的就餐情况数据中进行分析

  • 加载data/tips.csv餐厅就餐数据
tips = pd.read_csv('data/tips.csv')
tips

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7zTJTkJg-1630499358953)(./img/过滤-1.png)]

  • 查看用餐人数统计
# 用餐人数统计:对size列的值、出现的次数进行统计
tips['size'].value_counts()
# 返回结果如下
2    156
3     38
4     37
5      5
6      4
1      4
Name: size, dtype: int64

  • 结果显示,人数为1、5和6人的数据比较少,考虑将这部分数据过滤掉;这个时候就可以使用df.groupby().filter()groupby方法后接filter方法,filter传入一个返回布尔值的函数,该函数的入参就是groupby分组之后的每一组数据,返回False的数据会被过滤掉
# 按人数分组之后过滤:只保留 相同人数出现的次数大于30的数据(lambda x: x['size'].count()>30)
tips_filtered = tips.groupby('size').filter(lambda x: x['size'].count()>30)
tips_filtered

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ZMV972L-1630499358954)(./img/过滤-2.png)]

  • 重新统计得出结论:新餐厅应该买4人桌
tips_filtered['size'].value_counts()
# 返回结果如下
2    156
3     38
4     37
Name: size, dtype: int64

4 DataFrameGroupBy对象

4.1 分组对象

  • 准备数据
tips_10 = pd.read_csv('data/tips.csv').sample(10, random_state=42)
tips_10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4IScT14L-1630499358954)(./img/分组-1.png)]

  • 调用groupby 创建分组对象
# 调用groupby 创建分组对象
grouped = tips_10.groupby('sex')
grouped
# 返回结果如下
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000020591F63CF8>

  • grouped是一个DataFrameGroupBy对象,如果想查看计算过的分组,可以借助groups属性实现,返回的结果是一个字典,字段的值是一个列表,列表中的每一个元素是原始数据DataFrame的行索引值
grouped.groups
# 返回结果如下
{'Female': [198, 124, 101], 'Male': [24, 6, 153, 211, 176, 192, 9]}

  • 在DataFrameGroupBy对象基础上,直接就可以进行aggregate,transform计算了
# 计算按sex分组后,所有列的平均值,但只返回了数值列的结果,非数值列不会计算平均值
grouped.mean()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lzhyjbKd-1630499358955)(./img/分组-2.png)]

  • 通过get_group选择分组
female = grouped.get_group('Female')
female

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fCMxHIJW-1630499358956)(./img/分组-3.png)]

4.2 遍历分组

DataFrameGroupBy对象只能遍历不能通过下标取值

  • 通过DataFrameGroupBy对象,可以遍历所有分组,相比于在groupby之后使用aggregate、transform和filter,有时候使用for循环解决问题更简单
# 遍历返回的对象是一个元组
for sex_group in grouped:
    print(sex_group)

显示结果:

('Female',      total_bill   tip     sex smoker   day    time  size
198       13.00  2.00  Female    Yes  Thur   Lunch     2
124       12.48  2.52  Female     No  Thur   Lunch     2
101       15.38  3.00  Female    Yes   Fri  Dinner     2)
('Male',      total_bill   tip   sex smoker   day    time  size
24        19.82  3.18  Male     No   Sat  Dinner     2
6          8.77  2.00  Male     No   Sun  Dinner     2
153       24.55  2.00  Male     No   Sun  Dinner     4
211       25.89  5.16  Male    Yes   Sat  Dinner     4
176       17.89  2.00  Male    Yes   Sun  Dinner     2
192       28.44  2.56  Male    Yes  Thur   Lunch     2
9         14.78  3.23  Male     No   Sun  Dinner     2)

  • DataFrameGroupBy对象直接传入索引,会报错
grouped[0]

显示结果:

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-75-2ce84a56ac6b> in <module>()
----> 1 grouped[0]

e:\python\data\lib\site-packages\pandas\core\groupby\generic.py in __getitem__(self, key)
1642                 stacklevel=2,
1643             )
-> 1644         return super().__getitem__(key)
1645 
1646     def _gotitem(self, key, ndim: int, subset=None):

e:\python\data\lib\site-packages\pandas\core\base.py in __getitem__(self, key)
226         else:
227             if key not in self.obj:
--> 228                 raise KeyError(f"Column not found: {key}")
229             return self._gotitem(key, ndim=1)
230 

KeyError: 'Column not found: 0'

for sex_group in grouped:
    #遍历grouped对象,查看sex_group数据类型
    print(type(sex_group))
    # 查看元素个数
    print(len(sex_group))
    # 查看第一个元素
    print(sex_group[0])
    # 查看第一个元素数据类型
    print(type(sex_group[0]))
    # 查看第二个元素
    print(sex_group[1])
    # 查看第二个元素数据类型
    print(type(sex_group[1]))
    break

显示结果:

<class 'tuple'>
2
Female
<class 'str'>
total_bill   tip     sex smoker   day    time  size
198       13.00  2.00  Female    Yes  Thur   Lunch     2
124       12.48  2.52  Female     No  Thur   Lunch     2
101       15.38  3.00  Female    Yes   Fri  Dinner     2
<class 'pandas.core.frame.DataFrame'>

4.3 多个分组

前面使用的groupby语句只包含一个变量,可以在groupby中添加多个变量

  • 比如上面用到的消费数据集,可以使用groupby按性别和用餐时间分别计算小费数据的平均值
group_avg = tips.groupby(['sex','time']).mean()
group_avg
# 通过下图可以得出看出:
# 1. 晚餐给的小费多
# 2. 男性给的小费多
# 3. 晚餐比午餐花费更多

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63IAqScC-1630499358957)(./img/分组-4.png)]

  • 查看分组之后结果的列名
group_avg.columns
# 输出结果如下
Index(['total_bill', 'tip', 'size'], dtype='object')

  • 查看分组之后结果的行索引值
group_avg.index
# 输出结果如下
MultiIndex([('Female', 'Dinner'),
            ('Female',  'Lunch'),
            (  'Male', 'Dinner'),
            (  'Male',  'Lunch')],
           names=['sex', 'time'])

  • 可以看到,多个分组之后返回的df的索引是MultiIndex多级索引,如果想得到一个普通的DataFrame,可以在结果上调用reset_index方法
group_avg.reset_index()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4CFZOEnG-1630499358958)(./img/分组-5.png)]

  • 也可以在分组的时候通过as_index = False参数(默认是True),效果与调用reset_index()一样
 tips.groupby(['sex','time'], as_index=False).mean()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiujuBzk-1630499358958)(./img/分组-6.png)]

小结

  • 分组是数据分析中常见的操作,有助于从不同角度观察数据
  • 分组之后可以得到DataFrameGroupby对象,该对象可以进行聚合、转换、过滤操作
  • 分组之后的数据处理可以使用已有的内置函数,也可以使用自定义函数
  • 分组不但可以对单个字段进行分组,也可以对多个字段进行分组,多个字段分组之后可以得到MultiIndex数据,可以通过reset_index方法将数据变成普通的DataFrame
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值