如何切片 numpy 字符串数组的每个元素?

Posted

技术标签:

【中文标题】如何切片 numpy 字符串数组的每个元素?【英文标题】:How can I slice each element of a numpy array of strings? 【发布时间】:2016-12-26 19:14:59 【问题描述】:

Numpy 有一些非常有用的string operations,它可以矢量化通常的 Python 字符串操作。

与这些操作和pandas.str 相比,numpy strings 模块似乎缺少一个非常重要的功能:对数组中的每个字符串进行切片的能力。例如,

a = numpy.array(['hello', 'how', 'are', 'you'])
numpy.char.sliceStr(a, slice(1, 3))
>>> numpy.array(['el', 'ow', 're' 'ou'])

我是否在具有此功能的模块中缺少一些明显的方法?否则,有没有一种快速的矢量化方式来实现这一点?

【问题讨论】:

我不确定问题是什么。谁缺少此功能? Numpy 的 routines.char 似乎缺少它。我编辑了问题以使其更清楚。 我也在寻找这个功能。我想我总是最终使用某种循环。 【参考方案1】:

这是一种矢量化方法 -

def slicer_vectorized(a,start,end):
    b = a.view((str,1)).reshape(len(a),-1)[:,start:end]
    return np.fromstring(b.tostring(),dtype=(str,end-start))

示例运行 -

In [68]: a = np.array(['hello', 'how', 'are', 'you'])

In [69]: slicer_vectorized(a,1,3)
Out[69]: 
array(['el', 'ow', 're', 'ou'], 
      dtype='|S2')

In [70]: slicer_vectorized(a,0,3)
Out[70]: 
array(['hel', 'how', 'are', 'you'], 
      dtype='|S3')

运行时测试-

测试我可以在最后运行的其他作者发布的所有方法,还包括本文前面的矢量化方法。

这是时间安排 -

In [53]: # Setup input array
    ...: a = np.array(['hello', 'how', 'are', 'you'])
    ...: a = np.repeat(a,10000)
    ...: 

# @Alberto Garcia-Raboso's answer
In [54]: %timeit slicer(1, 3)(a)
10 loops, best of 3: 23.5 ms per loop

# @hapaulj's answer
In [55]: %timeit np.frompyfunc(lambda x:x[1:3],1,1)(a)
100 loops, best of 3: 11.6 ms per loop

# Using loop-comprehension
In [56]: %timeit np.array([i[1:3] for i in a])
100 loops, best of 3: 12.1 ms per loop

# From this post
In [57]: %timeit slicer_vectorized(a,1,3)
1000 loops, best of 3: 787 µs per loop

【讨论】:

这是用 Python 2.x 还是 3.x 测试的?使用 3.5.2 我得到 array([b'', b'', b'', b''], dtype='|S2') 作为输出? @Bart 这是 Python 2.7 的版本。将查看 Python 3.x 是否存在问题。感谢您的通知。 @Bart:我稍微编辑了这个答案中的代码以使其与 Python 3 兼容。问题是使用 'S' 作为 dtype,而 Python 3 在任何地方都使用 Unicode,所以 @987654326 @。现在它使用 str 作为 dtype,所以它应该适用于所有版本。 好把戏。再次使用 .view() 而不是 .tostring()/fromstring,您可以将速度提高近 2 倍。第二行可以替换为:return numpy.array(b).view((str,end-start)).flatten()【参考方案2】:

np.char 中的大多数(如果不是全部)函数将现有的str 方法应用于数组的每个元素。它比直接迭代(或vectorize)要快一点,但不是那么快。

没有字符串切片器;至少不是那种名字。最接近的是使用切片进行索引:

In [274]: 'astring'[1:3]
Out[274]: 'st'
In [275]: 'astring'.__getitem__
Out[275]: <method-wrapper '__getitem__' of str object at 0xb3866c20>
In [276]: 'astring'.__getitem__(slice(1,4))
Out[276]: 'str'

迭代方法可以使用frompyfuncvectorize 也使用):

In [277]: a = numpy.array(['hello', 'how', 'are', 'you'])
In [278]: np.frompyfunc(lambda x:x[1:3],1,1)(a)
Out[278]: array(['el', 'ow', 're', 'ou'], dtype=object)
In [279]: np.frompyfunc(lambda x:x[1:3],1,1)(a).astype('U2')
Out[279]: 
array(['el', 'ow', 're', 'ou'], 
      dtype='<U2')

我可以将其视为单个字符数组,然后对其进行切片

In [289]: a.view('U1').reshape(4,-1)[:,1:3]
Out[289]: 
array([['e', 'l'],
       ['o', 'w'],
       ['r', 'e'],
       ['o', 'u']], 
      dtype='<U1')

我仍然需要弄清楚如何将其转换回“U2”。

In [290]: a.view('U1').reshape(4,-1)[:,1:3].copy().view('U2')
Out[290]: 
array([['el'],
       ['ow'],
       ['re'],
       ['ou']], 
      dtype='<U2')

初始查看步骤将数据缓冲区显示为 Py3 字符(这些将是 S 或 Py2 字符串大小写中的字节):

In [284]: a.view('U1')
Out[284]: 
array(['h', 'e', 'l', 'l', 'o', 'h', 'o', 'w', '', '', 'a', 'r', 'e', '',
       '', 'y', 'o', 'u', '', ''], 
      dtype='<U1')

选择 1:3 的列相当于选择a.view('U1')[[1,2,6,7,11,12,16,17]],然后再进行整形和查看。在不深入细节的情况下,我对它需要副本并不感到惊讶。

【讨论】:

a.view('U1').reshape(len(a),-1)[:,1:3].astype(object).sum(axis=1) 工作并且在性能方面是明显的赢家 --- 请参阅我的答案。 如果您先进行复制,您可以使用view 将数组从U1 转换为U2,但我们不需要该副本。原则上,这应该只是对步幅、尺寸和偏移量的简单操作,但我不知道是否有办法在不直接应用 C 例程的情况下做到这一点。 .sum() 的巧妙使用。在这种情况下,它是 string + string 连接。【参考方案3】:

有趣的省略...我猜你总是可以自己写:

import numpy as np

def slicer(start=None, stop=None, step=1):
    return np.vectorize(lambda x: x[start:stop:step], otypes=[str])

a = np.array(['hello', 'how', 'are', 'you'])
print(slicer(1, 3)(a))    # => ['el' 'ow' 're' 'ou']

编辑:这里有一些使用 James Joyce 的 Ulysses 文本的基准。 似乎明显的赢家是@hpaulj 的最后一个策略。 @Divakar 在@hpaulj 的最后一个策略上改进了比赛。

import numpy as np
import requests

ulysses = requests.get('http://www.gutenberg.org/files/4300/4300-0.txt').text
a = np.array(ulysses.split())

# Ufunc
def slicer(start=None, stop=None, step=1):
    return np.vectorize(lambda x: x[start:stop:step], otypes=[str])

%timeit slicer(1, 3)(a)
# => 1 loop, best of 3: 221 ms per loop

# Non-mutating loop
def loop1(a):
    out = np.empty(len(a), dtype=object)
    for i, word in enumerate(a):
        out[i] = word[1:3]

%timeit loop1(a)
# => 1 loop, best of 3: 262 ms per loop

# Mutating loop
def loop2(a):
    for i in range(len(a)):
        a[i] = a[i][1:3]

b = a.copy()
%timeit -n 1 -r 1 loop2(b)
# 1 loop, best of 1: 285 ms per loop

# From @hpaulj's answer
%timeit np.frompyfunc(lambda x:x[1:3],1,1)(a)
# => 10 loops, best of 3: 141 ms per loop

%timeit np.frompyfunc(lambda x:x[1:3],1,1)(a).astype('U2')
# => 1 loop, best of 3: 170 ms per loop

%timeit a.view('U1').reshape(len(a),-1)[:,1:3].astype(object).sum(axis=1)
# => 10 loops, best of 3: 60.7 ms per loop

def slicer_vectorized(a,start,end):
    b = a.view('S1').reshape(len(a),-1)[:,start:end]
    return np.fromstring(b.tostring(),dtype='S'+str(end-start))

%timeit slicer_vectorized(a,1,3)
# => The slowest run took 5.34 times longer than the fastest.
#    This could mean that an intermediate result is being cached.
#    10 loops, best of 3: 16.8 ms per loop

【讨论】:

这比常规循环慢。 它比我机器上的循环略快(带有大数组)。【参考方案4】:

为了解决这个问题,到目前为止,我一直在将 numpy array 转换为 pandas Series 并返回。这不是一个漂亮的解决方案,但它确实有效,而且运行速度相对较快。

a = numpy.array(['hello', 'how', 'are', 'you'])
pandas.Series(a).str[1:3].values
array(['el', 'ow', 're', 'ou'], dtype=object)

【讨论】:

实际上,当我在大型数组上进行计时时,pandas 的常规切片速度并不快(类似于 .str[1:5])。我什至排除了将数组转换为系列的时间。不过对于 .str[::-1] 之类的东西来说它更快。 Pandas 似乎使用了很多对象数组。【参考方案5】:

我完全同意这是一个遗漏,这就是我打开PR #20694 的原因。如果这被接受,您将能够完全按照您的建议做,但使用稍微更传统的名称np.char.slice_

>>> a = np.array(['hello', 'how', 'are', 'you'])
>>> np.char.slice_(a, 1, 3)
array(['el', 'ow', 're' 'ou'])

PR 中的代码功能齐全,因此您可以制作它的工作副本,但它使用了一些技巧来绕过一些限制。

对于这种简单的情况,您可以使用简单切片。从 numpy 1.23.0 开始,您可以查看不同大小的 dtype (PR #20722) 下的非连续数组。这意味着你可以做到

>>> a[:, None].view('U1')[:, 1:3].view('U2').squeeze()
array(['el', 'ow', 're' 'ou'])

【讨论】:

谢谢!这很棒。 @MartínFixman。谢谢。也一直困扰着我。当它被接受时我会告诉你的:)

以上是关于如何切片 numpy 字符串数组的每个元素?的主要内容,如果未能解决你的问题,请参考以下文章

NumPy 数组切片索引

NumPy 基础

NumPy 基础

NumPy 基础

在numpy中获取3D数组的2D切片的平均值

使用numpy数组更改python pandas数据框切片中的元素[重复]