PDEP-10: PyArrow 作为默认字符串推断实现的必需依赖项

摘要

本 PDEP 提出

这将为用户带来 **立竿见影的益处**,并为未来带来更多重大的益处。

背景

PyArrow 是 pandas 的一个可选依赖项,它为 pandas 提供了广泛的补充功能

截至 pandas 2.0,人们可以将 PyArrow 作为 NumPy 的替代数据表示形式,它具有以下优点:

  1. 所有数据类型一致的 NA 支持;
  2. 更广泛的数据类型支持,例如 decimaldate 和嵌套类型;
  3. 与其他基于 Arrow 的数据框库更好的互操作性。

动机

虽然上一段中描述的所有功能目前都是可选的,但 PyArrow 已被广泛集成到 pandas 的许多领域。我们的路线图指出 pandas 努力实现更好的 Apache Arrow 互操作性 [^1],并且许多项目 [^2],无论是在 Python 生态系统内还是之外,都采用或与 Arrow 格式交互,使 PyArrow 成为必需的依赖项,这为 Arrow 生态系统提供了额外的信心信号(以及提高与它的互操作性)。

直接用户收益 1:pyarrow 字符串

目前,当用户将字符串数据传递到 pandas 构造函数中而没有指定数据类型时,结果数据类型为 object,与 pyarrow 字符串相比,它的内存使用量和性能要差得多。由于 pyarrow 字符串支持自 1.2.0 版本起可用,因此在 3.0 中要求 pyarrow 将允许 pandas 将推断的类型默认设置为更有效的 pyarrow 字符串类型。

In [1]: import pandas as pd

In [2]: pd.Series(["a"]).dtype
# Current behavior
Out[2]: dtype('O')

# Future behavior in 3.0
Out[2]: string[pyarrow]

Dask 开发人员调查了 pyarrow 字符串的性能和内存 这里,发现它们比当前的 object dtype 有显著改进。

小演示

import string
import random

import pandas as pd


def random_string() -> str:
    return "".join(random.choices(string.printable, k=random.randint(10, 100)))


ser_object = pd.Series([random_string() for _ in range(1_000_000)])
ser_string = ser_object.astype("string[pyarrow]")\

PyArrow 支持的字符串比 NumPy 对象字符串快得多

str.len

In[1]: %timeit ser_object.str.len()
118 ms ± 260 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In[2]: %timeit ser_string.str.len()
24.2 ms ± 187 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

str.startswith

In[3]: %timeit ser_object.str.startswith("a")
136 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In[4]: %timeit ser_string.str.startswith("a")
11 ms ± 19.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

直接用户收益 2:嵌套数据类型

目前,如果您尝试将dict存储在 pandas Series中,您将再次获得可怕的object数据类型。

In [6]: pd.Series([{'a': 1, 'b': 2}, {'a': 2, 'b': 99}])
Out[6]:
0     {'a': 1, 'b': 2}
1    {'a': 2, 'b': 99}
dtype: object

如果需要pyarrow,这可以自动推断为pyarrow.struct,这将再次带来内存和性能改进。

直接用户收益 3:互操作性

其他基于 Arrow 的数据帧库越来越受欢迎。拥有相同的内存表示将提高与它们的互操作性,因为诸如

import pandas as pd
import polars as pl

df = pd.DataFrame(
  {
    'a': ['one', 'two'],
    'b': [{'name': 'Billy', 'age': 3}, {'name': 'Bob', 'age': 4}],
  }
)
pl.from_pandas(df)

之类的操作可以是零拷贝的。使用多个数据帧库的用户将能够更容易地在它们之间切换。

未来用户收益

需要 PyArrow 将简化 pandas 中的相关开发,并可能改进 NumPy 功能,这些功能更适合 PyArrow,包括

开发者收益

首先,这将简化基于 pyarrow 的数据类型的开发,因为它将避免可选的依赖项检查。

其次,它可能消除冗余功能:- read_parquet 中的 fastparquet 引擎;- 可能简化 read_csv 逻辑(需要更多调查);- 因式分解;- 日期时间/时区操作。

缺点

包含 PyArrow 自然会增加 pandas 的安装大小。例如,使用 pip 从 wheels 安装 pandas 和 PyArrow,numpy 和 pandas 需要大约 70MB,而包含 PyArrow 需要额外的 120MB。安装大小的增加将对在空间受限的开发或部署环境(如 AWS Lambda)中使用 pandas 产生负面影响。

此外,如果用户在没有通过 pip installconda install 提供 wheels 的环境中安装 pandas,则用户在从源代码安装时还需要构建 Arrow C++ 和相关依赖项。这些环境包括

最后,pandas 的开发和发布需要考虑 PyArrow 的开发和发布节奏。例如,在支持新发布的 Python 版本时,pandas 还需要在发布新版本的 pandas 之前考虑 PyArrow 对该 Python 版本的 wheel 支持。

常见问题解答

问:为什么 Pandas 不能直接使用 NumPy 字符串和 NumPy void 数据类型,而要使用 PyArrow 字符串和 PyArrow 结构体?

:NumPy 字符串目前还不可用,而 PyArrow 字符串已经可用。NumPy void 数据类型与 PyArrow 结构体不同,无法带来与其他基于 Arrow 的数据框库相同的互操作性优势。

问:所有 PyArrow 数据类型都准备好了吗?现在将它们设置为默认值是不是太早了?

:它们很可能在 3.0 版本中准备好 - 但是,我们还没有将它们设置为默认值。例如,pd.Series([1, 2, 3]) 将继续自动推断为 np.int64。我们只会更改当前没有 numpy 支持的等效类型且存储为 object 数据类型的默认值,例如字符串和嵌套数据类型。

PDEP-10 历史

[^1] https://pandas.ac.cn/docs/development/roadmap.html#apache-arrow-interoperability [^2] https://arrow.apache.ac.cn/powered_by/