新字符串数据类型的迁移指南 (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 时,支持多种可能的缺失值哨兵,包括 None 和 np.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() 函数,而不是检查 None 或 np.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)
请注意,NaN(np.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 用于字符串数据的代码。