使用字符串和字节数组#

虽然 NumPy 主要是一个数值库,但使用字符串或字节的 NumPy 数组通常也很方便。最常见的两种用例是:

  • 处理从数据文件中加载或内存映射的数据,其中数据的一个或多个字段是字符串或字节字符串,并且该字段的最大长度是可预知的。这通常用于名称或标签字段。

  • 使用 NumPy 索引和广播功能处理长度未知的 Python 字符串数组,这些数组可能或可能不为每个值定义数据。

对于第一种用例,NumPy 提供了固定宽度的 numpy.voidnumpy.str_numpy.bytes_ 数据类型。对于第二种用例,NumPy 提供了 numpy.dtypes.StringDType。下面我们将介绍如何处理固定宽度和可变宽度字符串数组,如何在两种表示形式之间进行转换,并提供一些关于如何最有效地处理 NumPy 中字符串数据的建议。

固定宽度数据类型#

在 NumPy 2.0 之前,固定宽度的 numpy.str_numpy.bytes_numpy.void 数据类型是 NumPy 中处理字符串和字节字符串的唯一可用类型。因此,它们分别被用作字符串和字节字符串的默认 dtype。

>>> np.array(["hello", "world"])
array(['hello', 'world'], dtype='<U5')

这里检测到的数据类型是 '<U5',即小端 Unicode 字符串数据,最大长度为 5 个 Unicode 码点。

字节字符串类似

>>> np.array([b"hello", b"world"])
array([b'hello', b'world'], dtype='|S5')

由于这是一种单字节编码,字节顺序为 ‘|’(不适用),检测到的数据类型是最大 5 个字符的字节字符串。

您还可以使用 numpy.void 来表示字节字符串

>>> np.array([b"hello", b"world"]).astype(np.void)
array([b'\x68\x65\x6C\x6C\x6F', b'\x77\x6F\x72\x6C\x64'], dtype='|V5')

这在处理不易被表示为字节字符串的字节流时最有用,而是被视为 8 位整数的集合。

可变宽度字符串#

新增于版本 2.0。

注意

numpy.dtypes.StringDType 是 NumPy 的一个新增功能,它利用 NumPy 对灵活的自定义数据类型的新支持来实现,并且不如旧的 NumPy 数据类型那样经过广泛的生产工作流程测试。

通常,现实世界的字符串数据没有可预测的长度。在这种情况下,使用固定宽度字符串会很麻烦,因为要存储所有数据而不被截断,需要在创建数组之前知道您想要存储在数组中最长字符串的长度。

为了支持这种情况,NumPy 提供了 numpy.dtypes.StringDType,它以 UTF-8 编码的形式在 NumPy 数组中存储可变宽度字符串数据。

>>> from numpy.dtypes import StringDType
>>> data = ["this is a longer string", "short string"]
>>> arr = np.array(data, dtype=StringDType())
>>> arr
array(['this is a longer string', 'short string'], dtype=StringDType())

请注意,与固定宽度字符串不同,StringDType 不按数组元素的*:*:*长度进行参数化,任意长度的字符串都可以存储在同一个数组中,而无需为短字符串中的填充字节预留存储空间。

另请注意,与固定宽度字符串和大多数其他 NumPy 数据类型不同,StringDType 不会将字符串数据存储在“主”ndarray 数据缓冲区中。相反,数组缓冲区用于存储有关字符串数据在内存中存储位置的元数据。这种差异意味着期望数组缓冲区包含字符串数据的代码将无法正常工作,需要更新以支持StringDType

缺失数据支持#

字符串数据集通常不完整,需要一个特殊标签来指示某个值是缺失的。默认情况下,StringDType 没有对缺失值的特殊支持,除了空字符串用于填充空数组这一事实。

>>> np.empty(3, dtype=StringDType())
array(['', '', ''], dtype=StringDType())

可选地,您可以通过将na_object作为初始化器的关键字参数来创建一个支持缺失值的StringDType实例。

>>> dt = StringDType(na_object=None)
>>> arr = np.array(["this array has", None, "as an entry"], dtype=dt)
>>> arr
array(['this array has', None, 'as an entry'],
      dtype=StringDType(na_object=None))
>>> arr[1] is None
True

na_object可以是任何任意的 Python 对象。常见的选择是 numpy.nanfloat('nan')None,一个专门用于表示缺失数据的对象,如 pandas.NA,或者一个(希望)唯一的字符串,如 "__placeholder__"

NumPy 对 NaN 类哨兵和字符串哨兵有特殊处理。

NaN 类缺失数据哨兵#

NaN 类哨兵在算术运算结果中会返回自身。这包括 Python 的 nan 浮点数和 Pandas 缺失数据哨兵 pd.NA。NaN 类哨兵在字符串运算中继承了这些行为。这意味着,例如,与任何其他字符串相加的结果是哨兵。

>>> dt = StringDType(na_object=np.nan)
>>> arr = np.array(["hello", np.nan, "world"], dtype=dt)
>>> arr + arr
array(['hellohello', nan, 'worldworld'], dtype=StringDType(na_object=nan))

遵循 nan 在浮点数组中的行为,NaN 类哨兵会排序到数组末尾。

>>> np.sort(arr)
array(['hello', 'world', nan], dtype=StringDType(na_object=nan))

字符串缺失数据哨兵#

字符串缺失数据值是 strstr 子类型的实例。如果将此类数组传递给字符串操作或进行类型转换,“缺失”条目被视为具有字符串哨兵给定的值。比较操作类似地直接使用哨兵值处理缺失条目。

其他哨兵#

其他对象,如 None,也支持作为缺失数据哨兵。如果数组中使用此类哨兵的缺失数据存在,则字符串操作将引发错误。

>>> dt = StringDType(na_object=None)
>>> arr = np.array(["this array has", None, "as an entry"])
>>> np.sort(arr)
Traceback (most recent call last):
...
TypeError: '<' not supported between instances of 'NoneType' and 'str'

强制转换为非字符串#

默认情况下,非字符串数据会被强制转换为字符串。

>>> np.array([1, object(), 3.4], dtype=StringDType())
array(['1', '<object object at 0x7faa2497dde0>', '3.4'], dtype=StringDType())

如果不需要这种行为,可以通过在初始化器中设置 coerce=False 来创建禁用字符串强制转换的 DType 实例。

>>> np.array([1, object(), 3.4], dtype=StringDType(coerce=False))
Traceback (most recent call last):
...
ValueError: StringDType only allows string data when string coercion is disabled.

这允许在 NumPy 使用的数据传递过程中进行严格的数据验证来创建数组。将 coerce=True 设置为恢复默认行为,允许强制转换为字符串。

转换为固定宽度字符串和从固定宽度字符串转换#

StringDType 支持在 numpy.str_numpy.bytes_numpy.void 之间进行往返转换。转换为固定宽度字符串在需要将字符串内存映射到 ndarray 或需要固定宽度字符串来读写具有已知最大字符串长度的列式数据格式时最为有用。

在所有情况下,转换为固定宽度字符串都需要指定允许的最大字符串长度。

>>> arr = np.array(["hello", "world"], dtype=StringDType())
>>> arr.astype(np.str_)  
Traceback (most recent call last):
...
TypeError: Casting from StringDType to a fixed-width dtype with an
unspecified size is not currently supported, specify an explicit
size for the output dtype instead.

The above exception was the direct cause of the following
exception:

TypeError: cannot cast dtype StringDType() to <class 'numpy.dtypes.StrDType'>.
>>> arr.astype("U5")
array(['hello', 'world'], dtype='<U5')

numpy.bytes_ 转换对于只包含 ASCII 字符的字符串数据最有用,因为 UTF-8 编码中无法用单个字节表示 ASCII 范围之外的字符,因此会被拒绝。

任何有效的 Unicode 字符串都可以转换为 numpy.str_,尽管由于 numpy.str_ 对所有字符都使用 32 位 UCS4 编码,这通常会浪费内存,因为实际的文本数据可以通过更节省内存的编码得到很好的表示。

此外,任何有效的 Unicode 字符串都可以转换为 numpy.void,将 UTF-8 字节直接存储在输出数组中。

>>> arr = np.array(["hello", "world"], dtype=StringDType())
>>> arr.astype("V5")
array([b'\x68\x65\x6C\x6C\x6F', b'\x77\x6F\x72\x6C\x64'], dtype='|V5')

必须小心确保输出数组有足够的空间来存储字符串的 UTF-8 字节,因为 UTF-8 字节流的字节数不一定等于字符串的字符数。