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

摘要

本PDEP提议:

这将为用户带来即时的好处,并为未来带来显著的进一步好处打开大门。

背景

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

截至 pandas 2.0,人们可以可行地利用 PyArrow 作为 NumPy 的替代数据表示形式,其优势包括:

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

动机

虽然前一段描述的所有功能目前都是可选的,但 PyArrow 已深入集成到 pandas 的许多领域。我们的路线图指出 pandas 致力于更好的 Apache Arrow 互操作性 [^1],并且 Python 生态系统内外的许多项目 [^2] 正在采用 Arrow 格式或与之交互,因此将 PyArrow 设为必需的依赖项进一步表明了对 Arrow 生态系统的信心(并提高了与之的互操作性)。

即时用户好处 1:pyarrow 字符串

当前,当用户在不指定数据类型的情况下将字符串数据传递给 pandas 构造函数时,结果数据类型是 object,与 pyarrow 字符串相比,其内存使用和性能要差得多。自从 1.2.0 起支持 pyarrow 字符串后,要求 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 object 字符串快得多。

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:嵌套数据类型

当前,如果您尝试在 pandas Series 中存储 dict,您将再次得到可怕的 object dtype。

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 逻辑(需要进一步调查); - factorization; - 日期时间/时区操作。

缺点

包含 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 也需要注意 PyArrow 对该 Python 版本的 wheel 支持,然后再发布新的 pandas 版本。

常见问题

问:为什么 pandas 不能直接使用 numpy string 和 numpy void 数据类型,而非要使用 pyarrow string 和 pyarrow struct?

:NumPy strings 尚未可用,而 pyarrow strings 已经可用。NumPy void 数据类型与 pyarrow struct 不同,无法带来与基于 Arrow 的其他数据框库相同的互操作性优势。

问:所有的 pyarrow dtypes 都准备好了吗?现在就将它们设为默认类型是否为时过早?

:它们到 3.0 版本时很可能已经准备就绪 - 然而,我们(目前)不会将它们全部设为默认类型。例如,pd.Series([1, 2, 3]) 将继续自动推断为 np.int64。我们只会改变那些当前没有 numpy 支持的等效类型且被存储为 object dtype 的默认设置,例如字符串和嵌套数据类型。

PDEP-10 历史

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