Pandas分组函数groupby、聚合函数agg和转换函数transform
  utcwpaXdbjbR 2023年12月02日 18 0


pandas中的分组函数groupby()可以完成各种分组操作,聚合函数agg()可以将多个函数的执行结果聚合到一起,这两类函数经常在一起使用。

groupby用法和参数介绍

groupby(self, by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=no_default, observed=False, dropna=True):

by: 指定根据哪个/哪些字段分组,默认值是None,按多个字段分组时传入列表。by参数可以按位置参数的方式传入。

axis: 设置按列分组还是按行分组,0或index表示按列分组,1或columns表示按行分组,默认值为0。

level: 当DataFrame的索引为多重索引时,level参数指定用于分组的索引,可以传入多重索引中索引的下标(0,1…)或索引名,多个用列表传入。

level参数不能与by参数同时使用,如果两者同时存在,当by参数传入的是多重索引中的索引,则level不生效,当by参数传入的是DataFrame的列名,则报错。

as_index: 分组结果默认将分组列的值作为索引,如果按单列分组,结果默认是单索引,如果按多列分组,结果默认是多重索引。将as_index设置为False可以重置索引(0,1…)。

sort: 结果按分组列的值升序排列,将sort设置为False则不排序,可以提升性能。

dropna: 默认情况下,分组列的NaN在分组结果中不保留,将dropna设置为False,可以保留NaN分组。

其他三个参数不用关注,group_keys参数在源码中未使用,squeeze参数因为类型不兼容,官方已弃用,observed参数表示重设索引时,保留创建的全NaN行。

分组对象的内部结构

import pandas as pd

df = pd.DataFrame({
    'name': ["张三", "李四", "小明", "小张"],
    'sex': ['男', '男', '男', '女'],
    'age': [21, 22, 21, 23]
})
print(df)
#     name   sex  age
# 0   张三   男   21
# 1   李四   男   22
# 2   小明   男   21
# 3   小张   女   23
grouped = df.groupby('sex')
print(type(grouped))
# pandas.core.groupby.generic.DataFrameGroupBy

groupby()分组得到的是一个DataFrameGroupBy对象,直接打印DataFrameGroupBy对象只能看到它的内存地址,看不到内部的结构。

for name, group in grouped:
    print(name)
    print(group)
    print()

# 女
#   name sex  age
# 3   小张   女   23

# 男
#   name sex  age
# 0   张三   男   21
# 1   李四   男   22
# 2   小明   男   21
for group in grouped:
    print(group)
    print(type(group), type(group[0]), type(group[1]))
('女',   name sex  age
3   小张   女   23)
<class 'tuple'> <class 'str'> <class 'pandas.core.frame.DataFrame'>
('男',   name sex  age
0   张三   男   21
1   李四   男   22
2   小明   男   21)
<class 'tuple'> <class 'str'> <class 'pandas.core.frame.DataFrame'>

DataFrameGroupBy是一个可迭代对象,可以转换成list打印,也可以直接遍历打印出来。遍历出来的是一个个元组,每个元组对应一个分组,元组的第一个元素与分组列里的值对应,元组的第二个元素是分到当前小组的数据,是一个DataFrame。

DataFrameGroupBy对象的内部结构为:[(分组名1, 子DataFrame1), (分组名2, 子DataFrame2), …],相当于groupby()将DataFrame按字段值分成了多个小的DataFrame,然后将字段值和小的DataFrame用元组的方式保存在DataFrameGroupBy对象中。

print(grouped.groups)
group_name = [gn for gn in grouped.groups.keys()]
print(group_name)
group = grouped.get_group(group_name[2])
print('-'*40, '\n', group, sep='')
{'女': [3], '男': [0, 1, 2]}
['女', '男']

分组对象的groups属性可以返回分组信息,结果是一个形似字典的对象,由分组名和此分组数据在原DataFrame中的行索引组成。
借用groups可以提取出所有分组的分组名,分组对象的get_group()方法可以返回指定分组名的子DataFrame。

按多重索引分组

# 按多重索引分组
multi_grouped = df.set_index(['sex', 'age'])
print('-'*40, '\n', multi_grouped, sep='')
# ----------------------------------------
#         name
# sex age
# 男   21    张三
#     22    李四
#     21    小明
# 女   23    小张

# 按多重索引中的指定索引进行分组
grouped = multi_grouped.groupby(level='sex')
print('-'*40, '\n', grouped.count(), sep='')
# ----------------------------------------
#      name
# sex
# 女       1
# 男       3

# 按多重索引中的多个索引分组
grouped = multi_grouped.groupby(level=[0, 1])
print('-'*40, '\n', grouped.count(), sep='')
# ----------------------------------------
#          name
# sex age
# 女   23      1
# 男   21      2
#     22      1

level参数用于设置按多重索引中的指定索引分组,level传入的方式可以是索引name,也可以是索引在多重索引中的下标,还可以是排除某个索引外的其他索引。指定多个时用列表的方式传入。

# 传递给by参数,结果一样
grouped = multi_grouped.groupby('sex')
print('-'*40, '\n', grouped.count(), sep='')
grouped = multi_grouped.groupby(['sex', 'age'])
print('-'*40, '\n', grouped.count(), sep='')

多重索引中的索引也可以传给groupby()的by参数,分组结果与将多重索引作为DataFrame的列是一样的。
如果用DataFrame的列作为分组列,多重索引会被转换成列保留在结果中。也可以用多重索引中的索引与列一起组合分组,相当于先对DataFrame重设索引再分组。
分组结果的索引默认是分组列的值,将as_index设置为False可以重置索引,相当于先分组再调用reset_index()函数。

提取分组结果的指定列

print('-'*40)
grouped = df.groupby('sex')
for name, group in grouped['name']:
    print(name)
    print(group)
# ----------------------------------------
# 女
# 3    小张
# Name: name, dtype: object
# 男
# 0    张三
# 1    李四
# 2    小明
# Name: name, dtype: object

print('-'*40)
grouped = df['name'].groupby(df['sex'])
for name, group in grouped:
    print(name)
    print(group)
# ----------------------------------------
# 女
# 3    小张
# Name: name, dtype: object
# 男
# 0    张三
# 1    李四
# 2    小明
# Name: name, dtype: object

从groupby()分组结果中取一列,得到的是一个SeriesGroupBy对象,直接打印SeriesGroupBy对象只能看到它的内存地址,看不到内部的结构。
SeriesGroupBy的内部结构与DataFrameGroupBy相似,SeriesGroupBy对象的内部结构为:[(分组名1, 子Series1), (分组名2, 子Series2), …],因此SeriesGroupBy也可以转换成list打印,也可以遍历取出每一个元素。
也可以先指定需要获取的列,再按DataFrame的另一个列进行分组,结果与先分组再获取指定列相同。

agg()参数和用法介绍

agg(self, func=None, axis=0, *args, **kwargs):

func: 用于聚合数据的函数,如max()、mean()、count()等,函数必须满足传入一个DataFrame能正常使用,或传递到DataFrame.apply()中能正常使用。

func参数可以接收函数的名字、函数名的字符串、函数组成的列表、行/列标签和函数组成的字典。

axis: 设置按列还是按行聚合。设置为0或index,表示对每列应用聚合函数,设置为1或columns,表示对每行应用聚合函数。

*args: 传递给函数func的位置参数。

**kwargs: 传递给函数func的关键字参数。

返回的数据分为三种:scalar(标量)、Series或DataFrame。

scalar: 当Series.agg()聚合单个函数时返回标量。

Series: 当DataFrame.agg()聚合单个函数时,或Series.agg()聚合多个函数时返回Series。

DataFrame: 当DataFrame.agg()聚合多个函数时返回DataFrame。

传入单个参数

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'Col-1': [1, 3, 5], 'Col-2': [2, 4, 6],
     'Col-3': [9, 8, 7], 'Col-4': [3, 6, 9]},
    index=['A', 'B', 'C'])
print(df)
#    Col-1  Col-2  Col-3  Col-4
# A      1      2      9      3
# B      3      4      8      6
# C      5      6      7      9

res1 = df.agg(np.mean)
print('-' * 30, '\n', res1, sep='')
# ------------------------------
# Col-1    3.0
# Col-2    4.0
# Col-3    8.0
# Col-4    6.0
# dtype: float64

res2 = df.mean()  # 调用Python内置函数
print('-' * 30, '\n', res2, sep='')
# ------------------------------
# Col-1    3.0
# Col-2    4.0
# Col-3    8.0
# Col-4    6.0
# dtype: float64

res3 = df['Col-1'].agg(np.mean)
print('-' * 30, '\n', res3, sep='')
# ------------------------------
# 3.0

DataFrame应用单个函数时,agg()的结果与用apply()的结果等效,用DataFrame调用Python的内置函数也可以实现相同效果。
Series对象在agg()中传入单个函数,聚合结果为标量值,也就是单个数据。

多种方式传入函数func

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'Col-1': [1, 3, 5], 'Col-2': [2, 4, 6],
     'Col-3': [9, 8, 7], 'Col-4': [3, 6, 9]},
    index=['A', 'B', 'C'])
print(df)
#    Col-1  Col-2  Col-3  Col-4
# A      1      2      9      3
# B      3      4      8      6
# C      5      6      7      9

# 用列表的方式传入
res4 = df.agg([np.mean, np.max, np.sum])
print('-' * 30, '\n', res4, sep='')
# ------------------------------
#       Col-1  Col-2  Col-3  Col-4
# mean    3.0    4.0    8.0    6.0
# amax    5.0    6.0    9.0    9.0
# sum     9.0   12.0   24.0   18.0

# 用字典的方式传入
res5 = df.agg({'Col-1': [sum, max], 'Col-2': [sum, min], 'Col-3': [max, min]})
print('-' * 30, '\n', res5, sep='')
# ------------------------------
#      Col-1  Col-2  Col-3
# sum    9.0   12.0    NaN
# max    5.0    NaN    9.0
# min    NaN    2.0    7.0

# 函数名用字符串的方式传入
res6 = df.agg({'Col-1': ['sum', 'max'], 'Col-2': ['sum', 'min'], 'Col-3': ['max', 'min']})
print('-' * 30, '\n', res6, sep='')
# ------------------------------
#      Col-1  Col-2  Col-3
# sum    9.0   12.0    NaN
# max    5.0    NaN    9.0
# min    NaN    2.0    7.0

在agg()中,可以用列表的方式传入多个函数,会将这些函数在每一列的执行结果聚合到一个DataFrame中,结果DataFrame中的索引为对应的函数名。

也可以用字典的方式按列/行指定聚合函数,会将指定列/行与对应函数的执行结果聚合到一个DataFrame中,列/行和函数没有对应关系的位置填充空值。

在上面的情况中,函数名都可以换成用字符串的方式传入,结果一样。

# 用元组的方式按列/行传入函数
res7 = df.agg(X=('Col-1', 'sum'), Y=('Col-2', 'max'), Z=('Col-3', 'min'),)
print('-' * 30, '\n', res7, sep='')
res8 = df.agg(X=('Col-1', 'sum'), Y=('Col-2', 'max'), Zmin=('Col-3', 'min'), Zmax=('Col-3', 'max'))
print('-' * 30, '\n', res8, sep='')
# ------------------------------
#    Col-1  Col-2  Col-3
# X    9.0    NaN    NaN
# Y    NaN    6.0    NaN
# Z    NaN    NaN    7.0
# ------------------------------
#       Col-1  Col-2  Col-3
# X       9.0    NaN    NaN
# Y       NaN    6.0    NaN
# Zmin    NaN    NaN    7.0
# Zmax    NaN    NaN    9.0

agg()还支持将不同的列/行和函数组合成元组,赋值给一个自定义的索引名,聚合结果DataFrame的索引按自定义的值重命名。

用这种方式传入函数时,元组中只能有两个元素:列/行名和一个函数,不能同时传入多个函数,如果要对同一列/行执行多个函数,需要用多个元组多次赋值。

传入自定义函数和匿名函数

def fake_mean(s):
    return (s.max()+s.min())/2

res9 = df.agg([fake_mean, lambda x: x.mean()])
print('-' * 40, '\n', res9, sep='')
res10 = df.agg([fake_mean, lambda x: x.max(), lambda x: x.min()])
print('-' * 40, '\n', res10, sep='')
# ----------------------------------------
#            Col-1  Col-2  Col-3  Col-4
# fake_mean    3.0    4.0    8.0    6.0
# <lambda>     3.0    4.0    8.0    6.0
# ----------------------------------------
#            Col-1  Col-2  Col-3  Col-4
# fake_mean    3.0    4.0    8.0    6.0
# <lambda>     5.0    6.0    9.0    9.0
# <lambda>     1.0    2.0    7.0    3.0

传入自定义函数和匿名函数时,聚合结果中对应的索引也是显示函数名字,匿名函数显示<lambda>,有多个匿名函数时,同时显示<lambda>

这里需要注意,只有匿名函数可以传入重复的函数,自定义函数和内置函数等不能重复,会报错SpecificationError: Function names must be unique if there is no new column names assigned。

自定义实现describe函数的效果

import pandas as pd
import numpy as np
from functools import partial

df = pd.DataFrame(
    {'Col-1': [1, 3, 5], 'Col-2': [2, 4, 6],
     'Col-3': [9, 8, 7], 'Col-4': [3, 6, 9]},
    index=['A', 'B', 'C'])

print(df.describe())

# 20%分为数
per_20 = partial(pd.Series.quantile, q=0.2)
per_20.__name__ = '20%'
# 80%分为数
per_80 = partial(pd.Series.quantile, q=0.8)
per_80.__name__ = '80%'
res11 = df.agg([np.min, per_20, np.median, per_80, np.max])
print('-' * 40, '\n', res11, sep='')
Col-1  Col-2  Col-3  Col-4
count    3.0    3.0    3.0    3.0
mean     3.0    4.0    8.0    6.0
std      2.0    2.0    1.0    3.0
min      1.0    2.0    7.0    3.0
25%      2.0    3.0    7.5    4.5
50%      3.0    4.0    8.0    6.0
75%      4.0    5.0    8.5    7.5
max      5.0    6.0    9.0    9.0
----------------------------------------
        Col-1  Col-2  Col-3  Col-4
amin      1.0    2.0    7.0    3.0
20%       1.8    2.8    7.4    4.2
median    3.0    4.0    8.0    6.0
80%       4.2    5.2    8.6    7.8
amax      5.0    6.0    9.0    9.0

describe()函数包含了数值个数、均值、标准差、最小值、1/4分位数、中位数、3/4分位数、最大值。
用agg()函数可以聚合实现describe()相同的效果,只要将函数组合在一起传给agg()即可。所以我们可以根据自己的需要来增加或裁剪describe()中的内容。

上面的例子中,pd.Series.quantile()是pandas中求分位数的函数,默认是求中位数,指定q参数可以计算不同的分位数。

partial()是Python的functools内置库中的函数,作用是给传入它的函数固定参数值,如上面分别固定quantile()的q参数为0.2/0.8。

分组聚合结合使用

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'Col-1': [1, 3, 5], 'Col-2': [2, 4, 6],
     'Col-3': [9, 8, 7], 'Col-4': [3, 6, 9]},
    index=['A', 'B', 'C'])

# 先用groupby()分组再用agg()聚合
res12 = df.groupby('Col-1').agg([np.min, np.max])
print('-' * 40, '\n', res12, sep='')
# 分组后只聚合某一列
res13 = df.groupby('Col-1').agg({'Col-2': [np.min, np.mean, np.max]})
print('-' * 40, '\n', res13, sep='')

# ----------------------------------------
#       Col-2      Col-3      Col-4
#        amin amax  amin amax  amin amax
# Col-1
# 1         2    2     9    9     3    3
# 3         4    4     8    8     6    6
# 5         6    6     7    7     9    9
# ----------------------------------------
#       Col-2
#        amin mean amax
# Col-1
# 1         2  2.0    2
# 3         4  4.0    4
# 5         6  6.0    6

agg()经常接在分组函数groupby()的后面使用,先分组再聚合,分组之后可以对所有组聚合,也可以只聚合需要聚合的组。

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'Col-1': [1, 3, 5], 'Col-2': [2, 4, 6],
     'Col-3': [9, 8, 7], 'Col-4': [3, 6, 9]},
    index=['A', 'B', 'C'])

res14 = df.groupby('Col-1').agg(
    c2_min=pd.NamedAgg(column='Col-2', aggfunc='min'),
    c3_min=pd.NamedAgg(column='Col-3', aggfunc='min'),
    c2_sum=pd.NamedAgg(column='Col-2', aggfunc='sum'),
    c3_sum=pd.NamedAgg(column='Col-3', aggfunc='sum'),
    c4_sum=pd.NamedAgg(column='Col-4', aggfunc='sum')
)
print('-' * 40, '\n', res14, sep='')

# ----------------------------------------
#        c2_min  c3_min  c2_sum  c3_sum  c4_sum
# Col-1
# 1           2       9       2       9       3
# 3           4       8       4       8       6
# 5           6       7       6       7       9

pd.NamedAgg可以对聚合进行更精准的定义,它包含column和aggfunc两个定制化的字段,column设置用于聚合的列,aggfunc设置用于聚合的函数。
借助pd.NamedAgg,可以给column和aggfunc的组合自定义命名,自定义命名体现为聚合结果中的列名。

转换函数transform

transform()是pandas中的转换函数,对DataFrame执行传入的函数后返回一个相同形状的DataFrame。

transform()参数和用法介绍

transform(func, axis=0, *args, **kwargs):
func: 用于转换数据的函数,函数必须满足传入一个DataFrame能正常使用,或传递到DataFrame.apply()中能正常使用。

func可以接收函数的名字、函数名的字符串、函数组成的列表、行/列标签和函数名组成的字典。

axis: 设置按列还是按行转换。设置为0或index,表示对每列应用转换函数,设置为1或columns,表示对每行应用转换函数。

args: 传递给函数func的位置参数。

kwargs: 传递给函数func的关键字参数。

返回数据:

返回的结果是一个与自身形状相同的DataFrame。

transform()传入单个函数

import pandas as pd
import numpy as np

df = pd.DataFrame({'Col-1': [1, 3, 5], 'Col-2': [5, 7, 9]})
print(df)
#    Col-1  Col-2
# 0      1      5
# 1      3      7
# 2      5      9

res1 = df.transform(np.square)
print(res1)
#    Col-1  Col-2
# 0      1     25
# 1      9     49
# 2     25     81

res2 = df.transform('sqrt')
print(res2)
#       Col-1     Col-2
# 0  1.000000  2.236068
# 1  1.732051  2.645751
# 2  2.236068  3.000000

res3 = df.transform(lambda x: x*10)
print(res3)
#    Col-1  Col-2
# 0     10     50
# 1     30     70
# 2     50     90

在transform()中传入单个函数进行转换,transform()的结果与apply()/applymap()等效。

函数可以是库函数、自定义函数或匿名函数。因为transform()的返回结果与自身形状相同,所以不支持直接传入会将DataFrame“降维”的函数,如会将Series处理成标量的聚合函数min,mean,std等。传入这些函数时,会报错:ValueError: Function did not transform.

虽然transform()是按行/列来处理数据,但它对数据的处理有点像元素级的处理。上面这种传入单个函数对DataFrame做行列级处理的情况,更推荐使用apply()。

transform()传入多个函数

import pandas as pd
import numpy as np

df = pd.DataFrame({'Col-1': [1, 3, 5], 'Col-2': [5, 7, 9]})
print(df)
#    Col-1  Col-2
# 0      1      5
# 1      3      7
# 2      5      9

res5 = df.transform([np.square, np.sqrt])
print(res5)
#    Col-1            Col-2
#   square      sqrt square      sqrt
# 0      1  1.000000     25  2.236068
# 1      9  1.732051     49  2.645751
# 2     25  2.236068     81  3.000000
# 传入相同名字的函数时,只有最后一个函数生效
res6 = df.transform([np.abs, lambda x: x+10, lambda x:np.square(x)])
print(res6)
#      Col-1             Col-2
#   absolute <lambda> absolute <lambda>
# 0        1        1        5       25
# 1        3        9        7       49
# 2        5       25        9       81

在transform()中传入多个函数对DataFrame进行转换,结果中的索引变成多层索引,第一层索引是DataFrame的列名,第二层索引是执行的函数名。按第一层索引来比较,DataFrame的形状并没有变化。

在传入多个函数时,传入的方式是用列表传入,此时如果有多个名字相同的函数,只有最后一个函数生效。(聚合函数agg()中多个匿名函数可以同时生效)

按字典的方式传入函数

import pandas as pd
import numpy as np

df = pd.DataFrame({'Col-1': [1, 3, 5], 'Col-2': [5, 7, 9]})
print(df)
#    Col-1  Col-2
# 0      1      5
# 1      3      7
# 2      5      9

res7 = df.transform({'Col-1': np.square, 'Col-2': np.sqrt})
print(res7)
#    Col-1     Col-2
# 0      1  2.236068
# 1      9  2.645751
# 2     25  3.000000

res8 = df.transform({'Col-1': [np.square, lambda x: x*10], 'Col-2': np.sqrt})
print(res8)
#    Col-1              Col-2
#   square <lambda>      sqrt
# 0      1       10  2.236068
# 1      9       30  2.645751
# 2     25       50  3.000000

在transform()中可以用字典的方式给不同的列传入不同的函数,如果字典中的value有列表,则结果的索引变成多层索引。

transform()对Series进行转换

import pandas as pd
import numpy as np

s = pd.Series(range(3))
print(s)
# 0    0
# 1    1
# 2    2
# dtype: int64

res9 = s.transform(lambda x: x+10)
print(res9)
print(res9.shape)
# 0    10
# 1    11
# 2    12
# dtype: int64
# (3,)

print('-' * 20)
res10 = s.transform([np.square, lambda x: x*x])
print(res10)
print(res10.shape)
print(type(res10))
# --------------------
#    square  <lambda>
# 0       0         0
# 1       1         1
# 2       4         4
# (3, 2)
# <class 'pandas.core.frame.DataFrame'>

transform()传入单个函数对Series转换时,结果是Series,传入多个函数对Series转换时,结果是DataFrame,列名是调用的函数名。

在这种情况下,Series的形状发生了变化。(也可以理解成结果是多层索引,第一层是Series的列索引,第二层是函数名,只是Series的列索引没有显示。)

与groupby()配合使用

import pandas as pd
import numpy as np

score_df = pd.DataFrame(
    {'姓名': ['Tom', 'Tom', 'Tom', 'Andy', 'Andy', 'Andy', 'Andy', 'Kitty', 'Kitty', 'Kitty'],
     '课程': ['高数', '英语', '体育', '高数', '英语', '体育', '计算机', '高数', '英语', '体育'],
     '成绩': [60, 90, 80, 90, 80, 70, 90, 70, 90, 80],
     '评级': ['C', 'A', 'B', 'A', 'B', 'C', 'A', 'C', 'A', 'B']}
)
print(score_df)
#       姓名   课程  成绩 评级
# 0    Tom   高数  60  C
# 1    Tom   英语  90  A
# 2    Tom   体育  80  B
# 3   Andy   高数  90  A
# 4   Andy   英语  80  B
# 5   Andy   体育  70  C
# 6   Andy  计算机  90  A
# 7  Kitty   高数  70  C
# 8  Kitty   英语  90  A
# 9  Kitty   体育  80  B

result = score_df.groupby('姓名')['成绩'].transform(np.sum)
print('-'*30, '\n', result, sep='')
# ------------------------------
# 0    230
# 1    230
# 2    230
# 3    330
# 4    330
# 5    330
# 6    330
# 7    240
# 8    240
# 9    240
# Name: 成绩, dtype: int64

result = score_df.groupby('姓名')['成绩'].agg(np.sum)
print('-'*30, '\n', result, sep='')
# ------------------------------
# 姓名
# Andy     330
# Kitty    240
# Tom      230
# Name: 成绩, dtype: int64

在与分组函数groupby()配合使用时,transform()转换的结果与agg()聚合的结果不一样,transform()会保持每一个分组的形状与原始数据形状相同,而agg()会将每个分组的结果聚合成一个标量值。
这是transform()和agg()最主要的功能差异,也是最有用的一点,在适合的场景里非常有用。

计算每个人的成绩与同一门课程平均成绩的差。

import pandas as pd
import numpy as np

score_df = pd.DataFrame(
    {'姓名': ['Tom', 'Tom', 'Tom', 'Andy', 'Andy', 'Andy', 'Andy', 'Kitty', 'Kitty', 'Kitty'],
     '课程': ['高数', '英语', '体育', '高数', '英语', '体育', '计算机', '高数', '英语', '体育'],
     '成绩': [60, 90, 80, 90, 80, 70, 90, 70, 90, 80],
     '评级': ['C', 'A', 'B', 'A', 'B', 'C', 'A', 'C', 'A', 'B']}
)

# 第一种方法:分两步,先求得平均成绩,再相减
score_df['平均成绩'] = score_df.groupby('课程')['成绩'].transform(np.mean)
print('-'*30, '\n', score_df, sep='')
score_df['成绩差异'] = score_df['成绩'] - score_df['平均成绩']
print('-'*30, '\n', score_df, sep='')

# 第二种方法:用匿名函数一步到位
score_df['成绩差异'] = score_df.groupby('课程')['成绩'].transform(lambda x: x - x.mean())
print('-'*30, '\n', score_df, sep='')
# ------------------------------
#       姓名   课程  成绩 评级       平均成绩       成绩差异
# 0    Tom   高数  60  C  73.333333 -13.333333
# 1    Tom   英语  90  A  86.666667   3.333333
# 2    Tom   体育  80  B  76.666667   3.333333
# 3   Andy   高数  90  A  73.333333  16.666667
# 4   Andy   英语  80  B  86.666667  -6.666667
# 5   Andy   体育  70  C  76.666667  -6.666667
# 6   Andy  计算机  90  A  90.000000   0.000000
# 7  Kitty   高数  70  C  73.333333  -3.333333
# 8  Kitty   英语  90  A  86.666667   3.333333
# 9  Kitty   体育  80  B  76.666667   3.333333

第一步,使用groupby()函数按课程分组,然后使用transform()计算出每门课程成绩的平均值。这里transform()会保证结果的形状与原来相同,极大地方便了下一步计算差值。
第二步,用成绩减平均成绩,就可以得到成绩与此门课程平均成绩的差。

实际使用时,这两个步骤可以直接合并成一步,因为“平均成绩”只是用于计算差值的中间值,不需要保存,直接一步计算出差值即可。

填充缺失值,用每门课程的平均值填充第四位同学的成绩。

import pandas as pd
import numpy as np

score_df = pd.DataFrame(
    {'姓名': ['Tom', 'Tom', 'Tom', 'Andy', 'Andy', 'Andy', 'Andy', 'Kitty', 'Kitty', 'Kitty'],
     '课程': ['高数', '英语', '体育', '高数', '英语', '体育', '计算机', '高数', '英语', '体育'],
     '成绩': [60, 90, 80, 90, 80, 70, 90, 70, 90, 80],
     '评级': ['C', 'A', 'B', 'A', 'B', 'C', 'A', 'C', 'A', 'B']}
)

score_truman = pd.DataFrame(
    {'姓名': ['Truman', 'Truman', 'Truman'],
     '课程': ['高数', '英语', '体育'],
     '成绩': [np.NAN, np.NAN, np.NAN],
     '评级': [np.NAN, np.NAN, np.NAN]}
)
score_df = pd.concat([score_df, score_truman]).reset_index()
print('-'*30, '\n', score_df, sep='')
# ------------------------------
#     index      姓名   课程    成绩   评级
# 0       0     Tom   高数  60.0    C
# 1       1     Tom   英语  90.0    A
# 2       2     Tom   体育  80.0    B
# 3       3    Andy   高数  90.0    A
# 4       4    Andy   英语  80.0    B
# 5       5    Andy   体育  70.0    C
# 6       6    Andy  计算机  90.0    A
# 7       7   Kitty   高数  70.0    C
# 8       8   Kitty   英语  90.0    A
# 9       9   Kitty   体育  80.0    B
# 10      0  Truman   高数   NaN  NaN
# 11      1  Truman   英语   NaN  NaN
# 12      2  Truman   体育   NaN  NaN


score_df['成绩'] = score_df.groupby('课程')['成绩'].transform(lambda x: x.fillna(x.mean()))
print('-'*30, '\n', score_df, sep='')
# ------------------------------
#     index      姓名   课程         成绩   评级
# 0       0     Tom   高数  60.000000    C
# 1       1     Tom   英语  90.000000    A
# 2       2     Tom   体育  80.000000    B
# 3       3    Andy   高数  90.000000    A
# 4       4    Andy   英语  80.000000    B
# 5       5    Andy   体育  70.000000    C
# 6       6    Andy  计算机  90.000000    A
# 7       7   Kitty   高数  70.000000    C
# 8       8   Kitty   英语  90.000000    A
# 9       9   Kitty   体育  80.000000    B
# 10      0  Truman   高数  73.333333  NaN
# 11      1  Truman   英语  86.666667  NaN
# 12      2  Truman   体育  76.666667  NaN

用平均值来填充同一组数据中的空值,这是数据处理时非常常用的方式,借用transform()可以一步完成此功能。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月02日 0

暂无评论

推荐阅读
utcwpaXdbjbR