pandas 扩展数组 #
在最近的几个版本中,可扩展性一直是 pandas 开发的一个主要主题。这篇博文介绍了 pandas 扩展数组接口:它的动机以及它可能如何影响你作为 pandas 用户。最后,我们来看看扩展数组如何塑造 pandas 的未来。
扩展数组只是 pandas 0.24.0 中众多更改之一。有关完整的更新日志,请参阅 whatsnew。
动机 #
Pandas 构建在 NumPy 之上。你可以大致将 Series 定义为 NumPy 数组的包装器,将 DataFrame 定义为具有共享索引的 Series 的集合。由于几个原因,这并不完全正确,但我想专注于“NumPy 数组的包装器”这一部分。更准确的说法是“数组状对象的包装器”。
Pandas 主要使用 NumPy 内置的数据表示;我们在某些地方对其进行了限制,在其他地方对其进行了扩展。例如,pandas 的早期用户非常关心时区感知的日期时间,而 NumPy 不支持这一点。因此,pandas 内部定义了一个 DatetimeTZ 数据类型(它模仿 NumPy 数据类型),并允许你在 Index、Series 中使用该数据类型,以及作为 DataFrame 中的一列。该数据类型携带 tzinfo,但本身并不是有效的 NumPy 数据类型。
再举一个例子,考虑 Categorical。这实际上是由两个数组组成的:一个用于 categories,一个用于 codes。但它可以像其他列一样存储在 DataFrame 中。
Pandas 添加的每一种扩展类型本身都很有用,但维护成本很高。代码库的大部分都需要了解如何处理 NumPy 数组或其他特殊数组。这使得向 pandas 添加新的扩展类型变得非常困难。
Anaconda, Inc. 有一个客户经常处理包含 IP 地址的数据集。他们想知道是否值得向 pandas 添加一个 IPArray。最后,我们认为它不值得作为 pandas 本身的一部分,但我们对为 pandas 的第三方扩展定义接口很感兴趣。任何实现此接口的对象都将被允许在 pandas 中使用。我能够将 cyberpandas 编写在 pandas 之外,但感觉就像使用 pandas 内置的任何其他数据类型一样。
现状 #
截至 pandas 0.24.0,pandas 的所有内部扩展数组(Categorical、带时区的 Datetime、Period、Interval 和 Sparse)现在都构建在 ExtensionArray 接口之上。用户应该不会注意到太多变化。你注意到的主要变化是,将数据转换为 object 数据类型的次数会减少,这意味着你的代码运行速度会更快,类型也会更稳定。这包括将 Period 和 Interval 数据存储在 Series 中(以前这些数据会被转换为 object 数据类型)。
此外,我们将能够相对轻松地添加新的扩展数组。例如,0.24.0(可选)解决了 pandas 最长期的痛点之一:缺失值会将整数类型的值转换为浮点数。
>>> int_ser = pd.Series([1, 2], index=[0, 2])
>>> int_ser
0 1
2 2
dtype: int64
>>> int_ser.reindex([0, 1, 2])
0 1.0
1 NaN
2 2.0
dtype: float64
有了新的 IntegerArray 和可空的整数数据类型,我们可以原生表示带有缺失值的整数数据。
>>> int_ser = pd.Series([1, 2], index=[0, 2], dtype=pd.Int64Dtype())
>>> int_ser
0 1
2 2
dtype: Int64
>>> int_ser.reindex([0, 1, 2])
0 1
1 NaN
2 2
dtype: Int64
有一点确实会稍微改变你访问 Series 或 Index 中存储的原始(未标记)数组的方式,这偶尔会很有用。也许你调用的方法只适用于 NumPy 数组,或者你想禁用自动对齐。
过去,你会听到诸如“使用 .values 从 Series 或 DataFrame 中提取 NumPy 数组”之类的话。如果资源足够好,它们会告诉你这并不完全正确,因为有一些例外。我想深入探讨这些例外。
.values 的根本问题在于它有两个用途:
- 提取 Series、Index 或 DataFrame 的后端数组
- 将 Series、Index 或 DataFrame 转换为 NumPy 数组
如上所述,Series 或 Index 的“数组”后端可能不是 NumPy 数组,而可能是扩展数组(来自 pandas 或第三方库)。例如,考虑 Categorical,
>>> cat = pd.Categorical(['a', 'b', 'a'], categories=['a', 'b', 'c'])
>>> ser = pd.Series(cat)
>>> ser
0 a
1 b
2 a
dtype: category
Categories (3, object): ['a', 'b', 'c']
>>> ser.values
[a, b, a]
Categories (3, object): ['a', 'b', 'c']
在这种情况下,.values 是一个 Categorical,而不是 NumPy 数组。对于 period 数据类型的数据,.values 返回一个 Period 对象数组,创建起来成本很高。对于带时区的数据,.values 会转换为 UTC 并丢弃时区信息。这些类型的意外(不同的类型,或昂贵或有损的转换)源于试图将这些扩展数组塞入 NumPy 数组。但扩展数组的重点在于表示 NumPy 无法原生表示的数据。
为了解决 .values 的问题,我们将它的角色分成了两个专用方法:
- 使用
.array获取底层数据的零拷贝引用 - 使用
.to_numpy()获取数据的(可能昂贵、有损的)NumPy 数组。
所以以我们的 Categorical 示例为例:
>>> ser.array
[a, b, a]
Categories (3, object): ['a', 'b', 'c']
>>> ser.to_numpy()
array(['a', 'b', 'a'], dtype=object)
总结一下:
.array将始终是一个 ExtensionArray,并且始终是对数据的零拷贝引用。.to_numpy()始终是一个 NumPy 数组,因此你可以可靠地对其调用 ndarray 特定的方法。
你将不再需要 .values。
可能的未来路径 #
扩展数组开辟了许多令人兴奋的机会。目前,pandas 使用 NumPy 数组中的 Python 对象来表示字符串数据,这很慢。像 Apache Arrow 这样的库为变长字符串提供了原生支持,而 Fletcher 库为 Arrow 数组提供了 pandas 扩展数组。它将允许 GeoPandas 更有效地存储几何数据。Pandas(或第三方库)将能够支持嵌套数据、带单位的数据、地理数据、GPU 数组。请继续关注 pandas 生态系统页面,该页面将跟踪第三方扩展数组。这对 pandas 的发展来说是一个激动人心的时期。
其他想法 #
我想强调的是,这是一个接口,而不是一个具体的数组实现。我们没有在 pandas 中重新实现 NumPy。相反,这是一种将任何数组状数据结构(一个或多个 NumPy 数组、一个 Apache Arrow 数组、一个 CuPy 数组)放入 DataFrame 中的方法。我认为让 pandas 摆脱数组的范畴,转而关注更高级别的表格数据,对该项目来说是健康的发展。
这与 NumPy 的 __array_ufunc__ 协议和 NEP-18 完美配合。即使对象不支持 NumPy 内存,你也可以在它们上使用熟悉的 NumPy API。
升级 #
这些新功能都已在最近发布的 pandas 0.24 中提供。
conda
conda install -c conda-forge pandas
pip
pip install --upgrade pandas
一如既往,我们很乐意在 邮件列表、@pandas-dev 或 问题跟踪器 上收到反馈。
感谢 pandas 社区中众多贡献者、维护者和 机构合作伙伴。