Groupby详解
在日常的数据分析中,经常需要将数据根据某个(多个)字段划分为不同的群体(group)进行分析,如电商领域将全国的总销售额根据省份进行划分,分析各省销售额的变化情况,社交领域将用户根据画像(性别、年龄)进行细分,研究用户的使用情况和偏好等。在Pandas中,上述的数据处理操作主要运用groupby
完成,这篇文章就介绍一下groupby
的基本原理及对应的agg
、transform
和apply
操作。
为了后续图解的方便,采用模拟生成的10个样本数据,代码和数据如下:
company=["A","B","C"]
data=pd.DataFrame({
"company":[company[x] for x in np.random.randint(0,len(company),10)],
"salary":np.random.randint(5,50,10),
"age":np.random.randint(15,50,10)})
groupby的基本原理
在Pandas中,实现分组操作的代码很简单,仅需一行代码,在这里,将上面的数据集按照company
字段进行划分:
In [5]: group = data.groupby("company")
将上述代码输入ipython
后,会得到一个DataFrameGroupBy
对象
In [6]: group
Out[6]: <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002B7E2650240>
那这个生成的DataFrameGroupBy
是啥呢?对data
进行了groupby
后发生了什么?ipython
所返回的结果是其内存地址,并不利于直观地理解,为了看看group
内部究竟是什么,这里把group
转换成list
的形式来看一看:
In [8]: list(group)
Out[8]:
[('A', company salary age
3 A 20 22
6 A 23 33),
('B', company salary age
4 B 10 17
5 B 21 40
8 B 8 30),
('C', company salary age
0 C 43 35
1 C 17 25
2 C 8 30
7 C 49 19)]
转换成列表的形式后,可以看到,列表由三个元组组成,每个元组中,第一个元素是组别(这里是按照company
进行分组,所以最后分为了A
,B
,C
),第二个元素的是对应组别下的DataFrame
,整个过程可以图解如下:
总结来说,groupby
的过程就是将原有的DataFrame
按照groupby
的字段(这里是company
),划分为若干个分组DataFrame
,被分为多少个组就有多少个分组DataFrame
。所以说,在groupby
之后的一系列操作(如agg
、apply
等),均是基于分组DataFrame
的操作。理解了这点,也就基本摸清了Pandas中groupby
操作的主要原理。下面来讲讲groupby
之后的常见操作。
agg分组聚合
聚合操作是groupby
后常见的操作,会写SQL
的朋友对此应该是非常熟悉了。聚合操作可以用来求和、均值、最大值、最小值等,下面的表格列出了Pandas中常见的聚合操作。
针对样例数据集,如果我想计算不同公司员工的平均年龄和平均薪水,可以按照下方的代码进行:
In [12]: data.groupby("company").agg('mean')
Out[12]:
salary age
company
A 21.50 27.50
B 13.00 29.00
C 29.25 27.25
如果想对针对不同的列求不同的值,比如要计算不同公司员工的平均年龄以及薪水的中位数,可以利用字典指定进行聚合操作:
In [17]: data.groupby('company').agg({'salary':'median','age':'mean'})
Out[17]:
salary age
company
A 21.5 27.50
B 10.0 29.00
C 30.0 27.25
agg
聚合过程可以图解如下(第二个例子为例):
transform
transform
是一种什么数据操作?和agg
有什么区别呢?为了更好地理解transform
和agg
的不同,下面从实际的应用场景出发进行对比。
在上面的agg
中,我们学会了如何求不同公司员工的平均薪水,如果现在需要在原数据集中新增一列avg_salary
,代表员工所在的公司的平均薪水(相同公司的员工具有一样的平均薪水),该怎么实现呢?如果按照正常的步骤来计算,需要先求得不同公司的平均薪水,然后按照员工和公司的对应关系填充到对应的位置,不用transform
的话,实现代码如下:
In [21]: avg_salary_dict = data.groupby('company')['salary'].mean().to_dict()
In [22]: data['avg_salary'] = data['company'].map(avg_salary_dict)
In [23]: data
Out[23]:
company salary age avg_salary
0 C 43 35 29.25
1 C 17 25 29.25
2 C 8 30 29.25
3 A 20 22 21.50
4 B 10 17 13.00
5 B 21 40 13.00
6 A 23 33 21.50
7 C 49 19 29.25
8 B 8 30 13.00
如果使用transform
的话,仅需要一行代码:
In [24]: data['avg_salary'] = data.groupby('company')['salary'].transform('mean')
In [25]: data
Out[25]:
company salary age avg_salary
0 C 43 35 29.25
1 C 17 25 29.25
2 C 8 30 29.25
3 A 20 22 21.50
4 B 10 17 13.00
5 B 21 40 13.00
6 A 23 33 21.50
7 C 49 19 29.25
8 B 8 30 13.00
还是以图解的方式来看看进行groupby
后transform
的实现过程(为了更直观展示,图中加入了company
列,实际按照上面的代码只有salary
列):
图中的大方框是transform
和agg
所不一样的地方,对agg
而言,会计算得到A
,B
,C
公司对应的均值并直接返回,但对transform
而言,则会对每一条数据求得相应的结果,同一组内的样本会有相同的值,组内求完均值后会按照原索引的顺序返回结果,如果有不理解的可以拿这张图和agg
那张对比一下。
apply
apply
应该是大家的老朋友了,它相比agg
和transform
而言更加灵活,能够传入任意自定义的函数,实现复杂的数据操作。在Pandas数据处理三板斧,你会几板?中,介绍了apply
的使用,那在groupby
后使用apply
和之前所介绍的有什么区别呢?
区别是有的,但是整个实现原理是基本一致的。两者的区别在于,对于groupby
后的apply
,以分组后的分组DataFrame
作为参数传入指定函数的,基本操作单位是DataFrame
,而之前介绍的apply
的基本操作单位是Series
。还是以一个案例来介绍groupby
后的apply
用法。
假设我现在需要获取各个公司年龄最大的员工的数据,该怎么实现呢?可以用以下代码实现:
In [38]: def get_oldest_staff(x):
...: df = x.sort_values(by = 'age',ascending=True)
...: return df.iloc[-1,:]
...:
In [39]: oldest_staff = data.groupby('company',as_index=False).apply(get_oldest_staff)
In [40]: oldest_staff
Out[40]:
company salary age
0 A 23 33
1 B 21 40
2 C 43 35
可以看到,此处的apply
和上篇文章中所介绍的作用原理基本一致,只是传入函数的参数由Series
变为了此处的分组DataFrame
。
最后,关于apply
的使用,这里有个小建议,虽然说apply
拥有更大的灵活性,但apply
的运行效率会比agg
和transform
更慢。所以,groupby
之后能用agg
和transform
解决的问题还是优先使用这两个方法,实在解决不了了才考虑使用apply
进行操作。
map、apply、applymap详解
在日常的数据处理中,经常会对一个DataFrame
进行逐行、逐列和逐元素的操作,对应这些操作,Pandas中的map
、apply
和applymap
可以解决绝大部分这样的数据处理需求。这篇文章就以案例附带图解的方式,为大家详细介绍一下这三个方法的实现原理,相信读完本文后,不论是小白还是Pandas的进阶学习者,都会对这三个方法有更深入的理解。
本文演示的数据集是模拟生成的,想练手的可以按下方的代码生成。
boolean=[True,False]
gender=["男","女"]
color=["white","black","yellow"]
data=pd.DataFrame({
"height":np.random.randint(150,190,100),
"weight":np.random.randint(40,90,100),
"smoker":[boolean[x] for x in np.random.randint(0,2,100)],
"gender":[gender[x] for x in np.random.randint(0,2,100)],
"age":np.random.randint(15,90,100),
"color":[color[x] for x in np.random.randint(0,len(color),100) ]})
数据集如下所示,各列分别代表身高、体重、是否吸烟、性别、年龄和肤色。
Series数据处理
1. map用法
如果需要把数据集中gender
列的男替换为1,女替换为0,怎么做呢?绝对不是用for循环实现,使用Series.map()
可以很容易做到,最少仅需一行代码。
#①使用字典进行映射
data["gender"] = data["gender"].map({"男":1, "女":0})
#②使用函数
def gender_map(x):
gender = 1 if x == "男" else 0
return gender
#注意这里传入的是函数名,不带括号
data["gender"] = data["gender"].map(gender_map)
那map
在实际过程中是怎么运行的呢?请看下面的图解(为了方便展示,仅截取了前10条数据)
不论是利用字典还是函数进行映射,map
方法都是把对应的数据逐个当作参数传入到字典或函数中,得到映射后的值。
2. apply
同时Series对象还有apply
方法,apply
方法的作用原理和map
方法类似,区别在于apply
能够传入功能更为复杂的函数。怎么理解呢?一起看看下面的例子。
假设在数据统计的过程中,年龄age
列有较大误差,需要对其进行调整(加上或减去一个值),由于这个加上或减去的值未知,故在定义函数时,需要加多一个参数bias
,此时用map
方法是操作不了的(传入map
的函数只能接收一个参数),apply
方法则可以解决这个问题。
def apply_age(x,bias):
return x+bias
#以元组的方式传入额外的参数
data["age"] = data["age"].apply(apply_age,args=(-3,))
可以看到age列都减了3,当然,这里只是简单举了个例子,当需要进行复杂处理时,更能体现apply
的作用。
总而言之,对于Series而言,map
可以解决绝大多数的数据处理需求,但如果需要使用较为复杂的函数,则需要用到apply
方法。
DataFrame数据处理
1. apply
对DataFrame
而言,apply
是非常重要的数据处理方法,它可以接收各种各样的函数(Python内置的或自定义的),处理方式很灵活,下面通过几个例子来看看apply
的具体使用及其原理。
在进行具体介绍之前,首先需要介绍一下DataFrame
中axis
的概念,在DataFrame
对象的大多数方法中,都会有axis
这个参数,它控制了你指定的操作是沿着0轴还是1轴进行。axis=0
代表操作对列columns
进行,axis=1
代表操作对行row
进行,如下图所示。
如果还不是很了解,没关系,下面会分别对apply
沿着0轴以及1轴的操作进行讲解,继续往下走。
假设现在需要对data
中的数值列分别进行取对数和求和的操作,这时可以用apply
进行相应的操作,因为是对列进行操作,所以需要指定axis=0
,使用下面的两行代码可以很轻松地解决我们的问题。
# 沿着0轴求和
data[["height","weight","age"]].apply(np.sum, axis=0)
# 沿着0轴取对数
data[["height","weight","age"]].apply(np.log, axis=0)
实现的方式很简单,但调用apply
时究竟发生了什么呢?过程是怎么实现的?还是通过图解的方式来一探究竟。(取前五条数据为例)
当沿着轴0(axis=0)
进行操作时,会将各列(columns
)默认以Series
的形式作为参数,传入到你指定的操作函数中,操作后合并并返回相应的结果。
那如果在实际使用中需要按行进行操作(axis=1
),那整个过程又是怎么实现的呢?
在数据集中,有身高和体重的数据,所以根据这个,我们可以计算每个人的BMI指数(体检时常用的指标,衡量人体肥胖程度和是否健康的重要标准),计算公式是:体重指数BMI=体重/身高的平方(国际单位kg/㎡)
,因为需要对每个样本进行操作,这里使用axis=1
的apply
进行操作,代码如下:
def BMI(series):
weight = series["weight"]
height = series["height"]/100
BMI = weight/height**2
return BMI
data["BMI"] = data.apply(BMI,axis=1)
还是用图解的方式来看看这个过程到底是怎么实现的(以前5条数据为例)。
当apply
设置了axis=1
对行进行操作时,会默认将每一行数据以Series
的形式(Series的索引为列名)传入指定函数,返回相应的结果。
总结一下对DataFrame
的apply
操作:
- 当
axis=0
时,对每列columns
执行指定函数;当axis=1
时,对每行row
执行指定函数。 - 无论
axis=0
还是axis=1
,其传入指定函数的默认形式均为Series
,可以通过设置raw=True
传入numpy数组
。 - 对每个Series执行结果后,会将结果整合在一起返回(若想有返回值,定义函数时需要
return
相应的值) - 当然,
DataFrame
的apply
和Series
的apply
一样,也能接收更复杂的函数,如传入参数等,实现原理是一样的,具体用法详见官方文档。
2. applymap
applymap
的用法比较简单,会对DataFrame
中的每个单元格执行指定函数的操作,虽然用途不如apply
广泛,但在某些场合下还是比较有用的,如下面这个例子。
为了演示的方便,新生成一个DataFrame
df = pd.DataFrame(
{
"A":np.random.randn(5),
"B":np.random.randn(5),
"C":np.random.randn(5),
"D":np.random.randn(5),
"E":np.random.randn(5),
}
)
df
现在想将DataFrame
中所有的值保留两位小数显示,使用applymap
可以很快达到你想要的目的,代码和图解如下:
df.applymap(lambda x:"%.2f" % x)
转载出处:
https://www.jianshu.com/p/b50941b6d229
https://zhuanlan.zhihu.com/p/100064394?utm_source=wechat_sessio