分享

Python 数据分析——Pandas 分组运算

 禁忌石 2023-09-24
Python 数据分析——Pandas 分组运算

所谓分组运算是指使用特定的条件将数据分为多个分组,然后对每个分组进行运算,最后再将结果整合起来。Pandas中的分组运算由DataFrame或Series对象的groupby()方法实现。

下面以某种药剂的实验数据'dose.csv'为例介绍如何使用分组运算分析数据。在该数据集中使用了“ABCD”4种不同的药剂处理方式(Tmt),针对不同性别(Gender)、不同年龄(Age)的患者进行药剂实验,记录下药剂的投药量(Dose)与两种药剂反应(Response)。

Python 数据分析——Pandas 分组运算

一、groupby()方法

如图1所示,分组操作中涉及两组数据:源数据和分组数据。将分组数据传递给源数据的groupby()方法以完成分组。groupby()的axis参数默认为0,表示对源数据的行进行分组。源数据中的每行与分组数据中的每个元素对应,分组数据中的每个唯一值对应一个分组。由于图中的分组数据中有两个唯一值,因此得到两个分组。

groupby()并不立即执行分组操作,而只是返回保存源数据和分组数据的GroupBy对象。在需要获取每个分组的实际数据时,GroupBy对象才会执行分组操作。

Python 数据分析——Pandas 分组运算

图1 groupby()分组示意图

当分组用的数据在源数据中时,可以直接通过列名指定分组数据。当源数据是DataFrame类型时,groupby()方法返回一个DataFrameGroupBy对象。若源数据是Series类型,则返回SeriesGroupBy对象。在下面的例子中使用Tmt列对源数据分组:

tmt_group = dose_df.groupby('Tmt')print type(tmt_group)<class 'pandas.core.groupby.DataFrameGroupBy'

还可以使用列表传递多组分组数据给groupby(),例如下面的程序使用处理方式与年龄对源数据分组:

tmt_age_group = dose_df.groupby(['Tmt', 'Age'])

当分组数据不在源数据中时,可以直接传递分组数据。在下面的例子中对长度与源数据的行数相同、取值范围为[0,5)的随机整数数组进行分组,这样就将源数据随机分成了5组:

random_values = np.random.randint(0, 5, dose_df.shape[0])random_group = dose_df.groupby(random_values)

当分组数据可以通过源数据的行索引计算时,可以将计算函数传递给groupby()。下面的例子使用行索引值除以3的余数进行分组,因此将源数据的每行交替地分为3组。这是因为源数据的行索引为从0开始的整数序列。

alternating_group = dose_df.groupby(lambda n:n % 3)

上述三种分组数据可以任意自由组合,例如下面的例子同时使用源数据中的性别列、函数以及数组进行分组:

crazy_group = dose_df.groupby(['Gender', lambda n: n % 2, random_values])

二、GroupBy对象

使用len()可以获取分组数:

print len(tmt_age_group), len(crazy_group)10 20

GroupBy对象支持迭代接口,它与字典的iteritems()方法类似,每次迭代得到分组的键和数据。当使用多列数据分组时,与每个组对应的键是一个元组:

for key, df in tmt_age_group:print 'key =', key, ', shape =', df.shapekey = ('A', '50s') , shape = (39, 6)key = ('A', '60s') , shape = (26, 6)key = ('B', '40s') , shape = (13, 6)key = ('B', '50s') , shape = (13, 6)key = ('B', '60s') , shape = (39, 6)key = ('C', '40s') , shape = (13, 6)key = ('C', '50s') , shape = (13, 6)key = ('C', '60s') , shape = (39, 6)key = ('D', '50s') , shape = (52, 6)key = ('D', '60s') , shape = (13, 6)

由于Python的赋值语句支持迭代接口,因此可以使用下面的语句快速为每个分组数据指定变量名。这是因为我们知道只有4种药剂处理方式,并且GroupBy对象默认会对分组键进行排序。可以将groupby()的sort参数设置为False以关闭排序功能,这样可以稍微提高大量分组时的运算速度。

(_, df_A), (_, df_B), (_, df_C), (_, df_D) = tmt_group

由于GroupBy对象有keys属性,因此无法通过dict(tmt_group)直接将其转换为字典,可以先将其转换为迭代器,再转换为字典dict(iter(tmt_group))。

get_group()方法可以获得与指定的分组键对应的数据,例如:

Python 数据分析——Pandas 分组运算

对GroupBy的下标操作将获得一个只包含源数据中指定列的新GroupBy对象,通过这种方式可以先使用源数据中的某些列进行分组,然后选择另一些列进行后续计算。

print tmt_group['Dose']print tmt_group[['Response1', 'Response2']]<pandas.core.groupby.SeriesGroupBy object at 0x0C6076F0><pandas.core.groupby.DataFrameGroupBy object at 0x0C6077F0

GroupBy类中定义了__getattr__()方法,因此当获取GroupBy中未定义的属性时,将按照下面的顺序操作:

·如果属性名是源数据对象的某列的名称,则相当于GroupBy[name],即获取针对该列的GroupBy对象。

·如果属性名是源数据对象的方法,则相当于通过apply()对每个分组调用该方法。注意Pandas中定义了转换为apply()的方法集合,只有在此集合之中的方法才会被自动转换。关于apply()方法将在下一小节详细介绍。

下面的程序得到对源数据中的Dose列进行分组的GroupBy对象:

print tmt_group.Dosepandas.core.groupby.SeriesGroupBy object at 0x05D96B70

三、分组-运算-合并

通过GroupBy对象提供的agg()、transform()、filter()以及apply()等方法可以实现各种分组运算。每个方法的第一个参数都是一个回调函数,该函数对每个分组的数据进行运算并返回结果。这些方法根据回调函数的返回结果生成最终的分组运算结果。

1.agg()-聚合

agg()对每个分组中的数据进行聚合运算。所谓聚合运算是指将一组由N个数值组成的数据转换为单个数值的运算,例如求和、平均值、中间值甚至随机取值等都是聚合运算。其回调函数接收的数据是表示每个分组中每列数据的Series对象,若回调函数不能处理Series对象,则agg()会接着尝试将整个分组的数据作为DataFrame对象传递给回调函数。回调函数对其参数进行聚合运算,将Series对象转换为单个数值,或将DataFrame对象转换为Series对象。agg()返回一个DataFrame对象,其行索引为每个分组的键,而列索引为源数据的列索引。

在图2中,上方两个表格显示了两个分组中的数据,而下方左侧两个表格显示了对这两个分组执行聚合运算之后的结果。其中最左侧的表格是执行g.agg(np.max)的结果。由于np.max()能对Series对象进行运算,因此agg()将分组a和分组b中的每列数据分别传递给np.max()以计算每列的最大值,并将所有最大值聚合成一个DataFrame对象。例如分组a中的B列传递给np.max()的计算结果为6,该数值存放在结果的第a行、第B列中。

Python 数据分析——Pandas 分组运算

图2 agg()和transform()的运算示意图

左侧第二个表格对应的程序为:

g.agg(lambda df: df.loc[(df.A + df.B).idxmax()])

由于在回调函数中访问了属性A和B,这两个属性在表示每列数据的Series对象中不存在,因此传递Series对象给回调函数的尝试失败。于是agg()接下来尝试将表示整个分组数据的DataFrame对象传递给回调函数。该回调函数每次返回结果中的一行,例如图中分组b对应的运算结果为第b行。该回调函数返回每个分组中A+B最大的那一行。

下面是对tmt_group进行聚合运算的例子。❶计算每个分组中每列的平均值,注意结果中自动剔除了无法求平均值的字符串列。❷找到每个分组中Response1最大的那一行,由于回调函数对表示整个分组的DataFrame进行运算,因此结果中包含了源数据中的所有列。

Python 数据分析——Pandas 分组运算

2.transform()-转换

transform()对每个分组中的数据进行转换运算。与agg()相同,首先尝试将表示每列的Series对象传递给回调函数,如果失败,将表示整个分组的DataFrame对象传递给回调函数。回调函数的返回结果与参数的形状相同,transform()将这些结果按照源数据的顺序合并在一起。

图5-6的下方右侧两个表格为transform()的运算结果,它们对应的回调函数分别对Series和DataFrame对象进行处理。注意这两个表格的行索引与源数据的行索引相同。

下面是对tmt_group进行转换运算的例子。❶回调函数能对Series对象进行运算,因此运算结果中不包含源数据中的字符串列。❷由于Series对象没有assign()方法,因此transform()在尝试Series失败之后,将表示整个分组的DataFrame对象传递给回调函数。该回调函数只对Response1列进行转换。

Python 数据分析——Pandas 分组运算

3.filter()-过滤

filter()对每个分组进行条件判断。它将表示每个分组的DataFrame对象传递给回调函数,该函数返回True或False,以决定是否保留该分组。filter()的返回结果是过滤掉一些行之后的DataFrame对象,其行索引与源数据的行索引的顺序一致。

下面的程序保留Response1列的最大值小于11的分组,注意结果中包含用于分组的列Tmt:

Python 数据分析——Pandas 分组运算

4.apply()-运用

apply()将表示每个分组的DataFrame对象传递给回调函数并收集其返回值,并将这些返回值按照某种规则合并。apply()的用法十分灵活,可以实现上述agg()、transform()和filter()方法的功能。它会根据回调函数的返回值的类型选择恰当的合并方式,然而这种自动选择有时会得到令人费解的结果。

图3显示了对包含a和b两个分组的GroupBy对象g执行apply()后得到的4种结果:

Python 数据分析——Pandas 分组运算

图3 apply()的运算示意图​

如图3(左一)所示,回调函数为DataFrame.max,它计算DataFrame对象中每列的最大值,返回一个以列名为索引的Series对象,因此对于所有的分组数据返回的索引都是相同的。这种情况下apply()的结果与agg()相同,是一个以每个分组的键为行索引、以所有返回对象的索引为列索引的DataFrame对象。

如图3(左二)所示,当回调函数返回的Series对象的索引不是全部一致时,apply()将这些Series对象沿垂直方向连接在一起,得到一个多级索引的Series对象。多级索引由分组的键和每个Series对象的索引构成。

在图3(左三)中,回调函数返回的是DataFrame对象,并且结果的行索引与参数的行索引是同一索引对象,即满足如下条件:

df.index is (df - df.min()).index

则apply()返回的DataFrame对象的行索引与源数据的索引一致,这与filter()的结果相同。

在图3(左四)中,回调函数的返回结果与(图3左三)相同,但由于使用[:]复制了整个数据,因此其返回对象与参数的索引不是同一对象。这种情况下,将按照分组的顺序沿垂直方向将所有返回结果连接在一起,得到一个多级索引的DataFrame对象。多级索引由分组的键和每个DataFrame对象的索引构成。

注意目前的版本采用is判断索引是否相同,很容易引起混淆,未来的版本可能会对这一点进行修改。

下面计算tmt_group的每个分组中每列的最大值和平均值。注意最大值的结果中包含字符串列,而平均值的结果中不包含字符串列:

Python 数据分析——Pandas 分组运算

下面的程序从每个分组的Response1列随机取两个数值。❶由于sample()保留与数值对应的标签,因此结果是一个多级标签的Series对象。❷对sample()的结果调用reset_index()方法,这样所有返回结果的标签全部相同,因此得到的结果是一个DataFrame对象,其每一行与一个分组对应。

Python 数据分析——Pandas 分组运算
Python 数据分析——Pandas 分组运算

当回调函数的返回值是DataFrame对象时,根据其行标签是否与参数对象的行标签为同一对象,会得到不同的结果:

Python 数据分析——Pandas 分组运算

当回调函数返回None时,将忽略该返回值,因此可以实现filter()的功能。下面的程序从Response1的均值大于5的分组中随机取两行数据:

Python 数据分析——Pandas 分组运算

Pandas使用Cython对一些常用的聚合功能进行了优化处理,例如mean()、median()、var()等。此外,GroupBy还自动将一些常用的DataFrame方法用apply()包装。因此,通过GroupBy对象调用这些方法就相当于将这些方法作为回调函数传递给apply()。

下面的例子分别调用Cython编写的提速方法mean()和使用apply()包装之后的quantile()方法:

Python 数据分析——Pandas 分组运算

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多