处理字符串和字节数组#

虽然 NumPy 主要是一个数值库,但通常可以方便地处理 NumPy 的字符串或字节数组. 两个最常见的用例是:

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

  • 使用 NumPy 索引和广播与 Python 字符串数组,这些字符串数组的长度未知,并且可能并非每个值都定义了数据.

对于第一种用例,NumPy 提供了固定宽度的 numpy.void , numpy.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 ,它在 NumPy 数组中以 UTF-8 编码存储可变宽度的字符串数据:

>>> 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.nan , float('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))

字符串缺失数据哨兵值#

字符串缺失数据值是 str 的实例或 str 的子类型.如果此类数组传递给字符串操作或强制转换,则“缺失”条目将被视为具有由字符串哨兵值提供的值.比较操作类似地直接使用哨兵值来表示缺失条目.

其他哨兵值#

其他对象,例如 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())

如果不需要此行为,则可以创建一个 DType 实例,通过在初始化程序中设置 coerce=False 来禁用字符串强制转换:

>>> 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')

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

任何有效的 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 字节流的大小(以字节为单位)不一定与字符串中的字符数相同.