PDEP-14:适用于 pandas 3.0 的专用字符串数据类型

摘要

本 PDEP 提议引入一个专用字符串 dtype,它将在 pandas 3.0 中默认使用

这将为用户提供一个期待已久的、适用于 3.0 的合适字符串 dtype,同时 1) (暂) 不将 PyArrow 设为硬性依赖项,而只是一个默认使用的依赖项,以及 2) 为未来的改进留有空间 (例如不同的缺失值语义,使用 NumPy 2.0 字符串等)。

背景

目前,pandas 默认将文本数据存储在 object-dtype 的 NumPy 数组中。目前的实现有两个主要缺点。首先,object dtype 并非字符串专用:任何 Python 对象都可以存储在 object-dtype 数组中,不仅仅是字符串,并且将 object 视为包含字符串的列的 dtype 会让用户感到困惑。其次:这效率不高 (在 Series 上执行的所有字符串方法最终都会调用单个字符串对象的 Python 方法)。

为了解决第一个问题,一个用于字符串数据的专用扩展 dtype 已在 pandas 1.0 中添加。到目前为止,这始终是可选启用 (opt-in) 的,要求用户明确指定该 dtype (使用 dtype="string"dtype=pd.StringDtype())。支持此字符串 dtype 的数组最初与默认实现几乎相同,即一个包含 Python 字符串的 object-dtype NumPy 数组。

为了解决第二个问题 (性能),pandas 为 PyArrow 包中字符串内核的开发做出了贡献,并且在 pandas 1.3 中添加了一个由 PyArrow 支持的字符串 dtype 变体。这可以通过可选启用字符串 dtype 中的 storage 关键字来指定 (pd.StringDtype(storage="pyarrow"))。

自引入以来,StringDtype 始终是可选启用的,并一直使用实验性的 pd.NA 标记来表示缺失值 (它也在 pandas 1.0 中引入)。然而,截至目前,pandas 尚未采取步骤将 pd.NA 用于任何默认 dtype,因此 StringDtype 在缺失值行为上与默认数据类型存在偏差。

在 2023 年,PDEP-10 提议在 pandas 3.0 中默认开始使用由 PyArrow 支持的字符串 dtype (即对字符串数据推断此类型,而不是 object dtype)。为确保我们可以使用由 PyArrow 支持而非 Python 对象支持的 StringDtype 变体 (以获得更好的性能),它提议将 pyarrow 设为 pandas 新的必需运行时依赖项。

与此同时,NumPy 也在开发一种原生变长字符串数据类型,该类型已在 NumPy 2.0 及更高版本中可用。这为在 pandas 中实现不受 Python 对象支持的字符串数据类型提供了一个潜在的 PyArrow 替代方案。

在 PDEP-10 被接受后,该提议的两个方面一直在重新考虑中

对于第二个方面,StringDtype 的另一个变体在 pandas 2.1 中引入,它仍然由 PyArrow 支持,但遵循 pandas 用于所有其他默认数据类型的默认缺失值语义 (并使用 NaN 作为缺失值标记) (GH-54792)。当时,这个新变体的 storage 选项被称为 "pyarrow_numpy",以区别于使用 pd.NA 的现有 "pyarrow" 选项 (但本 PDEP 提出了一个更好的命名方案,请参阅下面的“命名”小节)。

这种最后的 dtype 变体是用户目前 (pandas 2.2) 在启用 future.infer_string 选项时获得的字符串数据类型 (以启用旨在成为 pandas 3.0 中默认行为的功能)。

提议

为了能在 pandas 3.0 中推进字符串数据类型,本 PDEP 提议

  1. 对于 pandas 3.0,默认启用 "str" 字符串 dtype,即创建 pandas 对象时 (例如构造函数中的推断、I/O 函数),此字符串 dtype 将用作文本数据的默认 dtype。
  2. 这个默认字符串 dtype 将遵循与其他默认数据类型相同的缺失值行为,并使用 NaN 作为缺失值标记。
  3. 如果安装了 PyArrow,该字符串 dtype 将使用 PyArrow,否则回退到内部一个功能等效 (但较慢) 的版本。这个回退实现可以 (通过少量代码添加) 重用现有的由 numpy object-dtype 支持的 StringArray。
  4. 安装指南将被更新,明确鼓励用户安装 pyarrow 以获得默认的用户体验。

这些默认启用的字符串 dtypes 将不再被视为实验性的。

字符串 dtype 的默认推断

默认情况下,pandas 将对字符串数据推断这种新的字符串 dtype,而不是 object dtype (创建 pandas 对象时,例如构造函数或 IO 函数)。

在 pandas 2.2 中,可以使用现有的 future.infer_string 选项来可选启用未来的默认行为

>>> pd.options.future.infer_string = True
>>> pd.Series(["a", "b", None])
0      a
1      b
2    NaN
dtype: string

目前 (pandas 2.2),现有选项仅启用基于 PyArrow 的未来 dtype。对于剩余的 2.x 版本,此选项将扩展到在未安装 PyArrow 时也能工作,以便在该情况下启用 object-dtype 回退。

缺失值语义

如背景部分所述,原始的 StringDtype 始终使用实验性的 pd.NA 标记来表示缺失值。除了使用 pd.NA 作为缺失值的标量外,这本质上意味着

然而,截至目前,所有其他默认数据类型仍然使用 NaN 语义来处理缺失值。因此,本提议认为,一个新的默认字符串 dtype 也应该仍然使用相同的默认缺失值语义,并在对字符串列执行操作时返回默认数据类型,以便在此时与其他默认 dtypes 保持一致。

实际上,这意味着默认字符串 dtype 将使用 NaN 作为缺失值标记,并且

由于原始的 StringDtype 实现已经在操作中使用 pd.NA 并返回掩码整数和布尔数组,因此需要一个使用 NaN 和默认数据类型的现有 dtypes 的新变体。使用 pd.NA 的原始 StringDtype 变体将继续供已经使用它的用户使用。

Object-dtype “回退”实现

为了避免 pandas 3.0 对 PyArrow 的硬性依赖,本 PDEP 提议保留一个“回退”选项,以防未安装 PyArrow。原始的、由 numpy object-dtype Python 字符串数组支持的 StringDtype 可以大部分重用于此 (添加该 dtype 的新变体),而一个新的 StringArray 子类只需要少量修改即可遵循上述缺失值语义 (GH-58451)。

对于 pandas 3.0,这是最现实的选择,因为此实现已经可用很长时间了。在 3.0 之后,进一步的改进,例如使用 NumPy 2.0 (GH-58503) 或 nanoarrow (GH-58552),仍可探索,但在那时,这属于实现细节,不应对用户产生直接影响 (性能除外)。

对于使用 pd.NA 的原始 StringDtype 变体,目前默认的存储是 "python" (基于 object-dtype 的实现)。对于此变体,也提议遵循相同的逻辑来确定默认存储,即如果可用则默认使用 "pyarrow",否则回退到 "python"

命名

考虑到这个话题的悠久历史,dtypes 的命名是一个困难的话题。

首先,应该承认大多数用户不应需要使用特定于存储的选项。期望用户指定一个通用名称 (例如 "str""string"),这将为他们提供默认的字符串 dtype (这取决于是否安装了 PyArrow)。

对于用于指定 dtype 的通用字符串别名,"string" 已经用于使用 pd.NAStringDtype。本 PDEP 提议将 "str" 用于使用 NaN 的新默认 StringDtype。这确保了使用 dtype="string" 的代码的向后兼容性,选择它也是因为 dtype="str"dtype=str 目前已经可以确保你的数据转换为字符串 (结果只使用 object dtype)。

但对于测试目的和想要精确控制 StringDtype 变体的高级用例,我们需要某种方式来指定它,并将其与其他字符串 dtypes 区分开。

目前 (pandas 2.2),StringDtype(storage="pyarrow_numpy") 用于使用 NaN 的新变体,其中 "pyarrow_numpy" 存储用于区分使用 pd.NA 的现有 "pyarrow" 选项。然而,"pyarrow_numpy" 是一个相当令人困惑的选项,并且通用性不好。因此,本 PDEP 提议采用如下概述的新命名方案,并且 "pyarrow_numpy" 将在 pandas 2.3 中作为别名被废弃,并在 pandas 3.0 中被移除。

保留 StringDtypestorage 关键字,用于区分字符串数据的底层存储 (使用 pyarrow 或 python 对象),但引入了一个额外的 na_value 参数,用于区分使用 NA 语义和 NaN 语义的变体。

指定 dtype 的不同方式概述以及由此产生的数据的具体 dtype

用户指定 具体 dtype 字符串别名 备注
未指定 (推断) StringDtype(storage="pyarrow"\|"python", na_value=np.nan) "str" (1)
"str"StringDtype(na_value=np.nan) StringDtype(storage="pyarrow"\|"python", na_value=np.nan) "str" (1)
StringDtype("pyarrow", na_value=np.nan) StringDtype(storage="pyarrow", na_value=np.nan) "str"
StringDtype("python", na_value=np.nan) StringDtype(storage="python", na_value=np.nan) "str"
StringDtype("pyarrow") StringDtype(storage="pyarrow", na_value=pd.NA) "string[pyarrow]"
StringDtype("python") StringDtype(storage="python", na_value=pd.NA) "string[python]"
"string"StringDtype() StringDtype(storage="pyarrow"\|"python", na_value=pd.NA) "string[pyarrow]" 或 "string[python]" (1)
StringDtype("pyarrow_numpy") StringDtype(storage="pyarrow", na_value=np.nan) "string[pyarrow_numpy]" (2)

备注

对于新的默认字符串 dtype,只有 "str" 别名可以作为字符串来指定该 dtype,即 pandas 不会提供通过字符串别名明确指定底层存储 (pyarrow 或 python) 的方式。这个字符串别名只是一个方便的快捷方式,对于大多数用户来说,"str" 就足够了 (他们不需要指定存储),而明确的 pd.StringDtype(storage=..., na_value=np.nan) 仍然可用于更精细的控制。

对于使用 pd.NA 的现有变体也是如此,通过字符串别名指定存储可能会被废弃,但这留待单独决定。

替代方案

为何不推迟引入默认字符串 dtype?

为了避免在其他讨论和变更尚不明朗时 (最终将 pyarrow 设为必需依赖项?将 pd.NA 设为默认缺失值标记?使用新的 NumPy 2.0 功能?彻底改革所有 dtypes 以使用逻辑数据类型系统?) 引入新的字符串 dtype,可以推迟引入默认字符串 dtype,直到那些其他讨论有了更明确的结果。具体来说,这将避免为字符串 dtype 暂时切换到使用 NaN,而在未来版本中我们可能会默认切换回 pd.NA

然而

  1. 推迟是有代价的:它进一步推迟了引入专用字符串 dtype 的时间,这对于用户来说具有显著的好处,无论是在可用性上,还是 (对于已安装 PyArrow 的用户群而言) 在性能上。
  2. 如果 pandas 最终过渡到将 pd.NA 用作默认缺失值标记,将需要为所有 pandas 数据类型提供迁移路径,因此围绕此问题的挑战不会是字符串 dtype 所独有的,从而不是推迟此事的理由。

现在为 3.0 进行此更改将使大多数用户受益,并且 PDEP 作者认为这值得付出“又一个 dtype”所带来的额外复杂性的代价 (对于其他数据类型,我们也已经有多个变体)。

为何不使用现有的带有 pd.NA 的 StringDtype?

再增加更多字符串 dtype 的变体不会让事情变得更令人困惑吗?确实如此,这个提议确实不幸地引入了更多字符串 dtype 的变体。然而,这样做的原因是为了确保实际的默认用户体验更不容易混淆,并且新的字符串 dtype 与其他默认数据类型更吻合。

如果新的默认字符串数据类型使用 pd.NA,那么在进行一些操作后,用户很容易得到一个混合了使用 NaN 语义和使用 NA 语义的列的 DataFrame (从而得到一个 DataFrame,其列可能包含两种不同的 int64、两种不同的 float64、两种不同的 bool 等 dtypes)。这将导致非常令人困惑的默认体验。

通过提议的 StringDtype 新变体,这将确保对于默认体验,用户只会看到一种整数 dtype、一种 bool dtype 等。目前来说,用户应该只在明确可选启用此功能时,才会获得使用 pd.NA 的列。

命名替代方案

本 PDEP 的初版提议使用 "string" 别名和默认的 pd.StringDtype() 类构造函数来指定新的默认 dtype。然而,这引发了关于 dtype=pd.StringDtype()dtype="string" 现有用户的向后兼容性的大量讨论,这些用法使用 pd.NA 来表示缺失值。

在讨论期间,提出了几种替代方案,包括替代的关键字名称和使用不同的构造函数。最后,本 PDEP 提议使用不同的字符串别名 ("str"),但目前保留使用现有的 pd.StringDtype (带有现有的 storage 关键字,但新增了一个 na_value 关键字),以尽量减少更改,将 dtype 系统的更大规模改革 (可能包括不同的构造函数或命名空间) 留待未来讨论。有关完整讨论,请参阅 GH-58613

一个后果是,当为默认 dtype 使用类构造函数时,必须使用非默认参数,即用户需要指定 pd.StringDtype(na_value=np.nan) 才能获得使用 NaN 的默认 dtype。因此,pandas 文档将侧重于 dtype="str" 的用法。

向后兼容性

最显著的向后不兼容更改将是,包含字符串数据的列将不再具有 object dtype。因此,假定为 object dtype 的代码 (例如 ser.dtype == object) 将需要更新。此更改是在主要版本中进行的硬性中断,因为提前对更改的推断发出警告被认为过于嘈杂。

为了允许提前测试代码,用户可以使用 pd.options.future.infer_string = True 选项。

除此之外,实际的字符串专用功能 (例如 .str 访问器方法) 应通常都能照常工作。

通过保留当前的缺失值语义,本提议在此方面也大多是向后兼容的。当在 object dtype 中存储字符串时,然而,pandas 确实也允许使用 None 作为缺失值指示符 (并且在某些情况下,例如 shift 方法,pandas 甚至自己引入了这一点)。对于目前使用 None 作为缺失值标记的所有情况,这将改为始终使用 NaN

对于 StringDtype 的现有用户

已经可选启用使用 pd.NAStringDtype 的现有代码应通常都能照常工作。本 PDEP 的最新版本保留了 dtype="string"dtype=pd.StringDtype() 表示 pd.NA dtype 变体的行为。

它确实提议对于可选启用的 pd.NA 变体,也将默认存储更改为 "pyarrow" (如果可用),但这应该对用户可见的影响有限,甚至没有影响。

时间表

未来的由 PyArrow 支持的字符串 dtype 已在 pandas 2.1 中通过功能标志启用 (通过设置 pd.options.future.infer_string = True 来启用)。

使用 numpy object-dtype 的变体也可以回溯到 2.2.x 分支,以便更容易测试。提议将其作为 2.3.0 版本发布 (从 2.2.x 分支创建,考虑到主分支已经包含许多针对 3.0 的其他更改),以及对命名方案的更改。

那么 2.3.0 版本将具有所有未来字符串功能可用 (包括默认字符串 dtype 中基于 pyarrow 和基于 object-dtype 的变体)。

对于 pandas 3.0,这个 future.infer_string 标志默认启用。

PDEP-14 历史