目录
第12章 高阶pandas
12.1 分类数据
本节介绍pandas的Categorical类型。
12.1.1 背景和目标
一个列经常会包含重复值,这些重复值是一个小型的不同值的集合。我们已经看见向unique和value_counts这样的函数,它们允许我们从一个数组中提取不同值并分别计算这些不同值的频率:
许多数据系统(用于数据入库、统计计算或其他用途)已经开发出专门的方法,用重复的值来表示数据,以便更有效地存储和计算。在数据入库的操作中,使用所谓的维度表是一种最佳实践,维度表包含了不同值,并将主要观测值存储为引用维度表的整数键:
可以使用take方法来恢复原来的字符串Series:
这种按照整数展现的方式被称为分类或字典编码展现。
不同值的数组可以被称为数据的类别、字典或层级。
12.1.2 pandas中的Categorical类型
pandas拥有特殊的Categorical类型,用于承载基于整数的类别展示或编码的数据。
让我们考虑之前的示例Series:
df['fruit']是一个Python字符串对象组成的数组。我们可以通过调用函数将它转换为Categorical对象:
fruit_cat的值并不是NumPy数组,而是pandas.Categorical的实例:
Categorical对象拥有categories和codes属性:
可以通过分配已转换的结果将DataFrame的一列转换为Categorical对象:
也可以从其他Python序列类型直接生成pandas.Categorical:
如果已经从另一个数据源获得了分类编码数据,你可以使用from_codes构造函数:
除非显式地指定,分类转换是不会指定类别的顺序的。因此categories数组可能会与输入数据的顺序不同。当使用from_codes或其他任意构造函数时,你可以为类别指定一个有意义的顺序:
输出的[foo<bar<baz]表明’foo’的顺序在’bar’之前,以此类推。一个未排序的分类实例可以使用as_ordered进行排序:
最后需要注意,分类数据可以不是字符串。一个分类数组可以包含任一不可变的值类型。
12.1.3 使用Categorical对象进行计算
在pandas中使用Categorical与非编码版本相比(例如字符串数组)整体上是一致的。pandas中的某些部分,比如groupby函数,在与Categorical对象协同工作时性能更好。还有一些函数可以利用ordered标识。
让我们考虑一些随机数字数据,并使用pandas.qcut分箱函数。
计算上面数据的四分位分箱,并提取一些统计值:
虽然样本的四分位数有用,但是在生成一份报告时,四分位数就没有四分位数名称有用了。我们可以通过在qcut函数中使用labels参数来实现这个功能:
被标记的bins分类数据并不包含数据中箱体边界的相关信息,因此我们可以使用groupby来提取一些汇总统计值:
结果中的’quartile’列保留了bins中原始的分类信息,包括顺序:
12.1.3.1 使用分类获得更高性能
如果你在特定的数据集上做了大量的分析,将数据转换为分类数据可以产生大幅的性能提升。DateFrame中一列的分类版本通常也会明显使用更少内存。
考虑一些含有一千万元素的Series以及少量的不同类别:
将labels转换为Categorical对象:
注意到labels比categories使用了明显更多的内存:
分类转换当然不是免费的,但是它是一次性开销:
使用分类对象进行GroupBy操作明显更快,这是因为底层算法使用了基于整数代码的数组而不是字符串数组。
12.1.4 分类方法
Series包含的分类数据拥有一些特殊方法,这些方法类似于Series.str的特殊字符串方法。
这些方法提供了快捷访问类别和代码的方式。考虑下面的Series:
特殊属性cat提供了对分类方法的访问:
假设知道该数据的实际类别集合超出了数据中观察到的四个值。可以使用set_categories方法来改变类别:
虽然看起来数据并未改变,但新类别将反映在使用它们的操作中。例如,value_counts将遵循新的类别(如果存在):
在大型数据集中,分类数据经常被用于节省内存和更高性能的便捷工具。
在过滤了一个大型DataFrame或Series之后,很多类别将不会出现在数据中。
为了帮助解决这个问题,可以使用remove_unused_categories方法来去除未观察到的类别:
pandas中Series的分类方法:
12.1.4.1 创建用于建模的虚拟变量(one-hot编码)
当你使用统计数据或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为one-hot编码。这会产生一个DataFrame,每个不同的类别都是它的一列。这些列包含一个特定类别的出现次数,否则为0。
示例:
pandas.get_dummies函数将一维的分类数据转换为一个包含虚拟变量的DataFrame:
12.2 高阶GroupBy应用
12.2.1 分组转换和“展开”GroupBy
在第10章中,我们在分组操作中学习了apply方法用于执行转换操作。
还有另一个内建方法transform,与apply方法类似但是会对你可以使用的函数种类加上更多的限制:
· transform可以产生一个标量值,并广播到各分组的尺寸数据中
· transform可以产生一个与输入分组尺寸相同的对象
· transform不可改变它的输入
示例:
按’key’分组的均值:
假设想要产生一个Series,它的尺寸和df['value']一样,但值都被按’key'分组的均值替代。可以向transfrom传递匿名函数lambda x: x.mean():
对于内建的聚合函数,可以像GroupBy的agg方法一样传递一个字符串别名:
与apply类似,transform可以与返回Series的函数一起使用,但结果必须和输入有相同的大小。例如,可以使用lambda函数给每个组乘以2:
作为一个更复杂的例子,可以按照每个组的降序计算排名:
考虑一个由简单聚合构成的分组转换函数:
使用transform或apply可以获得等价的结果:
内建的聚合函数如’mean’或’sum’通常会比apply函数更快。
这些函数在与transform一起使用时也会存在一个"快速通过"。这允许执行一个所谓的展开分组操作:
然而一个展开分组操作可能会包含多个分组聚合,矢量化操作的整体优势往往超过了这一点。
12.2.2 分组的时间重新采样
对于时间序列数据,resample方法在语义上是一种基于时间分段的分组操作。下面是一个小的示例表:
可以按’time’进行索引,然后重新采样:
假设DataFrame包含多个时间序列,并按一个附加的分组键列进行了标记:
要为每个’key’的值进行相同的重新采样,我们可以使用pandas.TimeGrouper对象:
time_key = pd.TimeGrouper('5min')
可以设置时间索引,按’key’和time_key进行分组,再聚合:
使用TimeGrouper的一个限制是时间必须是Series或DataFrame的索引。
12.3 方法链技术
在向数据集应用一系列变换时,你可能会发现自己创建了许多临时变量,而这些变量在分析中从未使用过。例如,考虑以下例子:
尽管我们在这里并未使用真实数据,但是这个例子体现了一些新的方法。首先,DataFrame.assign方法是对df[k] = v的赋值方式的一种功能替代。它返回的是一个按指定修改的新的DataFrame,而不是在原对象上进行修改。因此,下面这些表述是等价的:
原位赋值可能比使用assign更为快速,但assign可以实现更方便的方法链:
在做方法链时要牢记你可能会需要引用临时对象。在之前的例子中,我们无法引用load_data的结果,除非它被赋值给临时变量df。为了处理这种情况,assign和很多其他的pandas函数接受函数型的参数,这种参数也被称为可调用参数。
为了展示操作中的可调用对象,考虑下面这段之前讲过的代码段:
上面的代码可以改写为:
这里,load_data的结果没有复制给一个变量,因此传递进[]的函数将会被绑定到方法链那一阶段的对象上。
之后,我们可以继续将整个序列写作一个单链表达式:
12.3.1 pipe方法
使用内建的pandas函数和我们刚才看到的用可调用参数进行方法链接的方式,你可以完成很多工作。然而,有时你需要使用自定义的函数或来自第三方库的函数。这就是pipe(管道)方法出现的原因。
考虑下面一个函数调用序列:
在使用接受并返回Series或DataFrame对象的函数时,你可以调用pipe方法重写代码:
表达式f(df)和df.pipe(f)是等价的,但是pipe使得链式调用更为方便。
将操作的序列泛化成可复用的函数是pipe方法的一个潜在用途。作为示例,让我们考虑从一列中减去分组平均值:
假设你想要对多个列去除均值并方便地改变分组键。此外,你可能想要将转换在方法链中执行。下面是一个示例实现:
之后可以写如下代码:
参考书籍
--《利用Python实现数据分析》