pandas 扩展数组
可扩展性是 pandas 在过去几个版本开发中的一个主要主题。本文介绍了 pandas 扩展数组接口:其背后的动机以及它可能如何影响 pandas 用户。最后,我们将探讨扩展数组如何塑造 pandas 的未来。
扩展数组只是 pandas 0.24.0 中的一项变更。请查看更新日志以获取完整的变更列表。
动机
Pandas 构建在 NumPy 之上。你可以大致将 Series 定义为 NumPy 数组的包装器,将 DataFrame 定义为带有共享索引的 Series 集合。这并非完全正确,原因有很多,但我想重点关注“NumPy 数组的包装器”这一部分。更准确的说法是“类数组对象的包装器”。
Pandas 主要使用 NumPy 的内置数据表示;我们在某些地方对其进行了限制,在其他地方进行了扩展。例如,pandas 的早期用户非常关注时区感知的日期时间(timezone-aware datetimes),而 NumPy 不支持此功能。因此 pandas 内部定义了一个 DatetimeTZ
dtype(模仿 NumPy dtype),并允许你在 Index
、Series
中以及作为 DataFrame
中的列来使用该 dtype。该 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 with Timezone、Period、Interval 和 Sparse)现在都构建在 ExtensionArray 接口之上。用户不应注意到很多变化。你将主要注意到的是,在较少的地方数据被强制转换为 object
dtype,这意味着你的代码将运行得更快,并且你的类型将更稳定。这包括将 Period
和 Interval
数据存储在 Series
中(以前会强制转换为 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 和可空整数 dtypes,我们可以本地表示带有缺失值的整数数据。
>>> 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-dtype 数据,.values
返回一个 Period
对象的 NumPy 数组,创建成本很高。对于时区感知数据,.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 社区的众多贡献者、维护者和机构合作伙伴。