pandas 扩展数组

可扩展性是 pandas 在过去几个版本开发中的一个主要主题。这篇文章介绍了 pandas 扩展数组接口:它背后的动机以及它可能如何影响你作为 pandas 用户。最后,我们看看扩展数组如何塑造 pandas 的未来。

扩展数组只是 pandas 0.24.0 中的众多变化之一。查看 whatsnew 以获取完整的变更日志。

动机

Pandas 建立在 NumPy 之上。你可以粗略地将 Series 定义为 NumPy 数组的包装器,将 DataFrame 定义为具有共享索引的 Series 集合。由于多种原因,这并不完全正确,但我希望关注“NumPy 数组的包装器”部分。更准确地说应该是“围绕类数组对象的包装器”。

Pandas 主要使用 NumPy 的内置数据表示;我们在某些地方限制了它,在其他地方扩展了它。例如,pandas 的早期用户非常关心时区感知日期时间,而 NumPy 不支持。因此,pandas 在内部定义了一个 DatetimeTZ dtype(它模仿 NumPy dtype),并允许你在 IndexSeries 中使用该 dtype,以及作为 DataFrame 中的列。该 dtype 携带了 tzinfo,但本身不是有效的 NumPy dtype。

再举一个例子,考虑 Categorical。它实际上包含了 *两个* 数组:一个用于 categories,另一个用于 codes。但它可以像其他任何列一样存储在 DataFrame 中。

pandas 添加的每种扩展类型本身都有用,但维护成本很高。代码库的大部分都需要知道如何处理 NumPy 数组或这些其他类型的特殊数组。这使得向 pandas 添加新的扩展类型变得非常困难。

Anaconda, Inc. 有一位客户经常处理包含 IP 地址的数据集。他们想知道是否应该在 pandas 中添加一个 IPArray。最终,我们认为它没有通过成本效益测试,不适合包含在 pandas 本身中,但我们对为第三方 pandas 扩展定义接口感兴趣。任何实现此接口的对象都可以在 pandas 中使用。我能够在 pandas 之外编写 cyberpandas,但感觉就像使用 pandas 中内置的任何其他 dtype 一样。

现状

从 pandas 0.24.0 开始,pandas 的所有内部扩展数组(Categorical、带时区的 Datetime、Period、Interval 和 Sparse)现在都构建在 ExtensionArray 接口之上。用户不应该注意到太多变化。您会注意到的主要变化是,在更少的地方将事物转换为 object dtype,这意味着您的代码将运行得更快,并且您的类型将更加稳定。这包括在 Series 中存储 PeriodInterval 数据(以前被转换为 object dtype)。

此外,我们将能够相对轻松地添加新的扩展数组。例如,0.24.0(可选)解决了 pandas 最长期久的痛点之一:将缺失值转换为整数 dtype 值为浮点数。

>>> 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 和可空整数 dtype,我们可以原生表示具有缺失值的整数数据。

>>> 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 的根本问题在于它有两个目的

  1. 提取支持 Series、Index 或 DataFrame 的数组
  2. 将 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 数组。对于周期类型数据,.values 返回一个包含 Period 对象的 NumPy 数组,这很昂贵。对于时区感知数据,.values 会转换为 UTC 并丢弃时区信息。这些意外情况(不同的类型,或昂贵或有损的转换)源于试图将这些扩展数组塞进 NumPy 数组。但扩展数组的全部意义在于表示 NumPy无法原生表示的数据。

为了解决 .values 问题,我们将它的角色拆分为两个专门的方法

  1. 使用 .array 获取对底层数据的零拷贝引用
  2. 使用 .to_numpy() 获取数据的(可能很昂贵,有损的)NumPy 数组。

因此,对于我们的 Categorical 示例,

>>> ser.array
[a, b, a]
Categories (3, object): ['a', 'b', 'c']

>>> ser.to_numpy()
array(['a', 'b', 'a'], dtype=object)

总结一下

您不再需要 .values 了。

可能的未来路径

扩展数组带来了许多令人兴奋的机会。目前,pandas 使用 Python 对象在 NumPy 数组中表示字符串数据,这很慢。像 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 社区中众多贡献者、维护者和 机构合作伙伴