新字符串数据类型的迁移指南 (pandas 3.0)#

即将发布的 pandas 3.0 版本引入了一种新的、默认的字符串数据类型。这在升级到 pandas 3.0 时很可能会带来一些工作量,本页面提供了您可能遇到问题的概述,并提供了如何解决这些问题的指导。

此新 dtype 已在 pandas 2.3 版本中提供,您可以通过以下方式启用它:

pd.options.future.infer_string = True

这允许您在 3.0 版本最终发布之前测试您的代码。

注意

本迁移指南侧重于当您目前将 object dtype 用于字符串数据(这是 pandas < 3.0 中的默认设置)时所需的更改和迁移步骤。如果您已在使用其中一种选择加入的字符串 dtype,则可以继续使用它而无需更改。有关更多详细信息,请参阅 现有可空 StringDtype 用户的指南

背景#

历史上,pandas 一直使用 NumPy 的 object dtype 作为存储文本数据的默认方式。这主要有两个缺点。首先,object dtype 并非仅限于字符串:任何 Python 对象都可以存储在 object 类型的数组中,而不仅仅是字符串,对于用户来说,看到带有字符串的列的 dtype 为 object 是令人困惑的。其次,这并不总是非常高效(无论是性能还是内存使用方面)。

自 pandas 1.0 起,就提供了一个选择加入的字符串数据类型,但尚未将其设为默认,并且使用 pd.NA 标量来表示缺失值。

Pandas 3.0 将字符串的默认 dtype 更改为一种新的字符串数据类型,它是现有可选字符串数据类型的一个变体,但使用 NaN 作为缺失值指示符,以与其他默认数据类型保持一致。

为了提高性能,默认情况下,新的字符串数据类型将使用 pyarrow 包(如果已安装;否则,它在内部使用 object dtype 作为回退)。

有关更多背景和详细信息,请参阅 PDEP-14:pandas 3.0 的专用字符串数据类型

新默认字符串 dtype 简介#

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

作为默认 dtype,它将在 IO 方法或构造函数中用于推断 dtype 且输入被推断为字符串数据的情况。

>>> pd.Series(["a", "b", None])
0      a
1      b
2    NaN
dtype: str

它也可以使用 "str" 别名显式指定。

>>> pd.Series(["a", "b", None], dtype="str")
0      a
1      b
2    NaN
dtype: str

同样,像 read_csv()read_parquet() 等函数现在将在读取字符串数据时使用新的字符串 dtype。

与当前的 object dtype 不同,新的字符串 dtype 将只存储字符串。这意味着如果您尝试在其列中存储非字符串值,它将引发错误(有关更多详细信息,请参见下文)。

新字符串 dtype 的缺失值始终表示为 NaN (np.nan),其缺失值行为与其他默认 dtype 类似。

此新的字符串 dtype 在其他方面应与用户习惯使用的现有 object dtype 行为相同。例如,通过 str 访问器的所有特定于字符串的方法都将保持不变。

>>> ser = pd.Series(["a", "b", None], dtype="str")
>>> ser.str.upper()
0    A
1    B
2  NaN
dtype: str

注意

新的默认字符串 dtype 是 pandas.StringDtype 类的实例。该 dtype 可以构造为 pd.StringDtype(na_value=np.nan),但对于一般用途,我们建议使用更短的 "str" 别名。

行为差异概述及如何解决#

dtype 不再是 numpy 的“object”dtype#

在推断或读取字符串数据时,生成的 DataFrame 列或 Series 的数据类型将默默地变为新的 "str" dtype,而不是 numpy 的 "object" dtype,这可能会对您的代码产生一些影响。

新的字符串 dtype 是 pandas 数据类型(“扩展 dtype”),不再是 numpy np.dtype 实例。因此,将字符串列的 dtype 传递给 numpy 函数将不再起作用(例如,将其传递给 numpy 函数的 dtype= 参数,或使用 np.issubdtype 来检查 dtype)。

检查 dtype#

在检查 dtype 时,代码当前可能如下操作:

>>> ser = pd.Series(["a", "b", "c"])
>>> ser.dtype == "object"

来检查带有字符串数据的列(通过检查 dtype 是否为 "object")。这在 pandas 3+ 中将不再有效,因为在新的默认字符串 dtype 下,ser.dtype 将变为 "str",上述检查将返回 False

要检查带有字符串数据的列,您应该改为使用:

>>> ser.dtype == "str"

如何编写兼容的代码

对于应在 pandas 2.x 和 3.x 上都工作的代码,您可以使用 pandas.api.types.is_string_dtype() 函数。

>>> pd.api.types.is_string_dtype(ser.dtype)
True

这将返回 True,用于 object dtype 和 string dtypes。

硬编码使用 object dtype#

如果您有在构造函数中硬编码 dtype 的代码,例如:

>>> pd.Series(["a", "b", "c"], dtype="object")

这将继续使用 object dtype。您需要更新这些代码以确保您获得新字符串 dtype 的好处。

如何编写兼容的代码?

首先,在许多情况下,仅删除特定数据类型并让 pandas 进行推断就足够了。但如果您想明确指定,可以指定 "str" dtype。

>>> pd.Series(["a", "b", "c"], dtype="str")

这实际上也与 pandas 2.x 兼容,因为在 pandas < 3 中,dtype="str" 被视为 object dtype 的别名。

注意

虽然在构造函数中使用 dtype="str" 与 pandas 2.x 兼容,但在 pandas 2.x 中,将其指定为 astype() 的 dtype 会遇到字符串化缺失值的问题。有关更多详细信息,请参阅 astype(str) 保留缺失值 部分。

要在 pandas 2.x 和 3.x 中以兼容的方式使用 select_dtypes() 选择字符串列,无法使用 "str"。虽然这在 pandas 3.x 中有效,但在 pandas 2.x 中会引发错误。作为替代方案,您可以选择 object(用于 pandas 2.x)和 "string"(用于 pandas 3.x;后者也将选择默认的 str dtype 且在 pandas 2.x 中不会引发错误)。

# can use ``include=["str"]`` for pandas >= 3
>>> df.select_dtypes(include=["object", "string"])

缺失值哨兵现在始终为 NaN#

在使用 object dtype 时,支持多种可能的缺失值哨兵,包括 Nonenp.nan。使用新的默认字符串 dtype,缺失值哨兵始终为 NaN(np.nan)。

# with object dtype, None is preserved as None and seen as missing
>>> ser = pd.Series(["a", "b", None], dtype="object")
>>> ser
0       a
1       b
2    None
dtype: object
>>> print(ser[2])
None

# with the new string dtype, any missing value like None is coerced to NaN
>>> ser = pd.Series(["a", "b", None], dtype="str")
>>> ser
0      a
1      b
2    NaN
dtype: str
>>> print(ser[2])
nan

通常,这在依赖 pandas 方法中的缺失值行为时不会有问题(例如,ser.isna() 的结果与之前相同)。但如果您依赖于 None 的确切存在值,则可能会影响您的代码。

如何编写兼容的代码?

在检查缺失值时,应使用 pandas.isna() 函数,而不是检查 Nonenp.nan 的确切值。这是检查缺失值最稳健的方式,因为它无论 dtype 和具体的缺失值哨兵如何都能工作。

>>> pd.isna(ser[2])
True

一个注意事项:此函数同时适用于标量和类数组,在后一种情况下,它将返回一个布尔数组。在布尔上下文中(例如,if pd.isna(..): ..)使用它时,请确保只向它传递一个标量。

“setitem”操作现在将因非字符串数据而引发错误#

使用新的字符串 dtype,任何尝试在 Series 或 DataFrame 中设置非字符串值的操作都将引发错误。

>>> ser = pd.Series(["a", "b", None], dtype="str")
>>> ser[1] = 2.5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
...
TypeError: Invalid value '2.5' for dtype 'str'. Value should be a string or missing value, got 'float' instead.

如果您依赖于 object dtype 能够容纳任何 Python 对象的灵活性,但您的初始数据被推断为字符串,那么您的代码可能会受到此更改的影响。

如何编写兼容的代码?

您可以更新您的代码以确保只在该类列中设置字符串值,或者您也可以显式确保该列首先具有 object dtype。这可以通过在构造函数中显式指定 dtype 来完成,或者使用 astype() 方法来实现。

>>> ser = pd.Series(["a", "b", None], dtype="str")
>>> ser = ser.astype("object")
>>> ser[1] = 2.5

当使用 pandas 2.x 时,此 astype("object") 调用是多余的,但此代码适用于所有版本。

无效的 Unicode 输入#

Python 允许使用表示无效 Unicode 数据的内置 str 对象。由于 object dtype 可以容纳任何 Python 对象,因此您可能有一个包含此类无效 Unicode 数据的 pandas Series。

>>> ser = pd.Series(["\u2600", "\ud83d"], dtype=object)
>>> ser
0    ☀
1    \ud83d
dtype: object

但是,在使用 pyarrow 作为后端的字符串 dtype 时,它只能存储有效的 Unicode 数据,否则将引发错误。

>>> ser = pd.Series(["\u2600", "\ud83d"])
---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)
...
UnicodeEncodeError: 'utf-8' codec can't encode character '\ud83d' in position 0: surrogates not allowed

如果您想保留之前的行为,可以显式指定 dtype=object 来继续使用 object dtype。

当您有字节数据并希望使用 decode() 将其转换为字符串时,decode() 方法现在有一个 dtype 参数,以便在这种用例中能够指定 object dtype 而不是默认的 string dtype。

Series.values() 现在返回一个 ExtensionArray#

对于 object dtype,对 Series 使用 .values 将返回底层的 NumPy 数组。

>>> ser = pd.Series(["a", "b", np.nan], dtype="object")
>>> type(ser.values)
<class 'numpy.ndarray'>

但是,对于新的字符串 dtype,将返回底层的 ExtensionArray。

>>> ser = pd.Series(["a", "b", pd.NA], dtype="str")
>>> ser.values
<ArrowStringArray>
['a', 'b', nan]
Length: 3, dtype: str

如果您的代码需要 NumPy 数组,您应该使用 Series.to_numpy()

>>> ser = pd.Series(["a", "b", pd.NA], dtype="str")
>>> ser.to_numpy()
['a' 'b' nan]

总的来说,您应该始终优先使用 Series.to_numpy() 来获取 NumPy 数组,或者使用 Series.array() 来获取 ExtensionArray,而不是使用 Series.values()

重要的 bug 修复#

astype(str) 保留缺失值#

pandas-dev/pandas#25353 中所述,将缺失值字符串化是一个长期存在的“bug”或错误特性,但修复它会引入一个重大的行为更改。

在 pandas < 3 中,使用 astype(str)astype("str") 时,操作会将每个元素转换为字符串,包括缺失值。

# OLD behavior in pandas < 3
>>> ser = pd.Series([1.5, np.nan])
>>> ser
0    1.5
1    NaN
dtype: float64
>>> ser.astype("str")
0    1.5
1    nan
dtype: object
>>> ser.astype("str").to_numpy()
array(['1.5', 'nan'], dtype=object)

请注意,NaNnp.nan)被转换为字符串 "nan"。这并非预期行为,并且与其他 dtype 处理缺失值的方式不一致。

在 pandas 3 中,此行为已修复,现在 astype("str") 将强制转换为新的字符串 dtype,该 dtype 会保留缺失值。

# NEW behavior in pandas 3
>>> pd.options.future.infer_string = True
>>> ser = pd.Series([1.5, np.nan])
>>> ser.astype("str")
0    1.5
1    NaN
dtype: str
>>> ser.astype("str").to_numpy()
array(['1.5', nan], dtype=object)

如果您想保留将每个对象转换为字符串的旧行为,可以使用 ser.map(str)。如果您想在保留缺失值的情况下进行此类转换,并且该方式兼容 pandas 2.x 和 3.x,可以使用 ser.map(str, na_action="ignore")(仅限 pandas 3.x,您可以使用 ser.astype("str"))。

如果您想在 pandas 2.x 和 3.x 中分别转换为 object 或 string dtype,而无需字符串化每个单独的元素,您将需要使用条件检查 pandas 版本。例如,将具有字符串类别的分类 Series 转换为其密集非分类版本,并具有 object 或 string dtype。

>>> import pandas as pd
>>> ser = pd.Series(["a", np.nan], dtype="category")
>>> ser.astype(object if pd.__version__ < "3" else "str")

prod() 因字符串数据而引发错误#

在 pandas < 3 中,在具有字符串数据的 Series 上调用 prod() 方法通常会引发错误,除非 Series 为空或仅包含一个字符串(可能带有缺失值)。

>>> ser = pd.Series(["a", None], dtype=object)
>>> ser.prod()
'a'

当 Series 包含多个字符串时,它将引发 TypeError。在使用灵活的 object dtype 时,此行为在 pandas 3 中保持不变。但由于使用了新的字符串 dtype,这通常会始终引发错误,而不管字符串的数量。

>>> ser = pd.Series(["a", None], dtype="str")
>>> ser.prod()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
...
TypeError: Cannot perform reduction 'prod' with string dtype

对于现有可空 StringDtype 的用户#

虽然 pandas 3.0 引入了一个新的 _默认_ 字符串数据类型,但 pandas 自 pandas 1.0 起就有一个选择加入的可空字符串数据类型,可以使用 dtype="string" 来指定。此可空字符串 dtype 使用 pd.NA 作为缺失值指示符。此外,自 pandas 1.5 起,通过 ArrowDtype(通过使用 dtypes_backend="pyarrow"),也可以使用专用字符串 dtype。

如果您已在使用其中一种可空字符串 dtype,例如通过指定 dtype="string",通过使用 convert_dtypes(),或通过在 IO 函数中指定 dtype_backend 参数,您可以继续使用它而无需更改。

上述迁移指南适用于当前(< 3.0)将 object dtype 用于字符串数据的代码。