将numpy结构化数组子集转换为不复制的numpy数组

3
假设我有以下的numpy结构化数组:
In [250]: x
Out[250]: 
array([(22, 2, -1000000000, 2000), (22, 2, 400, 2000),
       (22, 2, 804846, 2000), (44, 2, 800, 4000), (55, 5, 900, 5000),
       (55, 5, 1000, 5000), (55, 5, 8900, 5000), (55, 5, 11400, 5000),
       (33, 3, 14500, 3000), (33, 3, 40550, 3000), (33, 3, 40990, 3000),
       (33, 3, 44400, 3000)], 
       dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])

我正在尝试将上述数组的一个子集修改为常规numpy数组。对于我的应用程序来说,非常重要的一点是不能创建副本(只能创建视图)。
通过使用以下函数从以上结构化数组检索字段:
def fields_view(array, fields):
    return array.getfield(numpy.dtype(
        {name: array.dtype.fields[name] for name in fields}
    ))

如果我对领域“f2”和“f3”感兴趣,我会采取以下步骤:
In [251]: y=fields_view(x,['f2','f3'])
In [252]: y
Out [252]:
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
       (5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
       (3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)], 
       dtype={'names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12})

直接从原始结构化数组的“f2”和“f3”字段获取ndarray的方法。然而,对于我的应用程序来说,建立这个中介结构化数组是必要的,因为这个数据子集是一个类的属性。

如果不进行复制,我无法将中介结构化数组转换为常规的numpy数组。

In [253]: y.view(('<f4', len(y.dtype.names)))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-54-f8fc3a40fd1b> in <module>()
----> 1 y.view(('<f4', len(y.dtype.names)))

ValueError: new type not compatible with array.

这个函数也可以用来将记录数组转换为ndarray:

def recarr_to_ndarr(x,typ):

    fields = x.dtype.names
    shape = x.shape + (len(fields),)
    offsets = [x.dtype.fields[name][1] for name in fields]
    assert not any(np.diff(offsets, n=2))
    strides = x.strides + (offsets[1] - offsets[0],)
    y = np.ndarray(shape=shape, dtype=typ, buffer=x,
               offset=offsets[0], strides=strides)
    return y

然而,我遇到了以下错误:
In [254]: recarr_to_ndarr(y,'<f4')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-65-2ebda2a39e9f> in <module>()
----> 1 recarr_to_ndarr(y,'<f4')

<ipython-input-62-8a9eea8e7512> in recarr_to_ndarr(x, typ)
      8     strides = x.strides + (offsets[1] - offsets[0],)
      9     y = np.ndarray(shape=shape, dtype=typ, buffer=x,
---> 10                offset=offsets[0], strides=strides)
     11     return y
     12 

TypeError: expected a single-segment buffer object

如果我创建一个副本,该函数就可以正常工作:

In [255]: recarr_to_ndarr(np.array(y),'<f4')
Out[255]: 
array([[  2.00000000e+00,  -1.00000000e+09],
       [  2.00000000e+00,   4.00000000e+02],
       [  2.00000000e+00,   8.04846000e+05],
       [  2.00000000e+00,   8.00000000e+02],
       [  5.00000000e+00,   9.00000000e+02],
       [  5.00000000e+00,   1.00000000e+03],
       [  5.00000000e+00,   8.90000000e+03],
       [  5.00000000e+00,   1.14000000e+04],
       [  3.00000000e+00,   1.45000000e+04],
       [  3.00000000e+00,   4.05500000e+04],
       [  3.00000000e+00,   4.09900000e+04],
       [  3.00000000e+00,   4.44000000e+04]], dtype=float32)

两个数组似乎没有什么区别:

In [66]: y
Out[66]: 
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
       (5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
       (3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)], 
      dtype={'names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12})

In [67]: np.array(y)
Out[67]: 
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
       (5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
       (3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)], 
      dtype={'names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12})
2个回答

2

这个答案有点长而且啰嗦。我从之前处理数组视图的工作中了解到的知识开始,然后试图将其与您的函数联系起来。

================

在您的情况下,所有字段的长度都是4个字节,包括浮点数和整数。然后我可以将其视为全部整数或全部浮点数:

In [1431]: x
Out[1431]: 
array([(22, 2.0, -1000000000.0, 2000), (22, 2.0, 400.0, 2000),
       (22, 2.0, 804846.0, 2000), (44, 2.0, 800.0, 4000),
       (55, 5.0, 900.0, 5000), (55, 5.0, 1000.0, 5000),
       (55, 5.0, 8900.0, 5000), (55, 5.0, 11400.0, 5000),
       (33, 3.0, 14500.0, 3000), (33, 3.0, 40550.0, 3000),
       (33, 3.0, 40990.0, 3000), (33, 3.0, 44400.0, 3000)], 
      dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])
In [1432]: x.view('i4')
Out[1432]: 
array([        22, 1073741824, -831624408,       2000,         22,
       1073741824, 1137180672,       2000,         22, 1073741824,
       1229225696,       2000,         44, 1073741824, 1145569280,
      ....     3000])
In [1433]: x.view('f4')
Out[1433]: 
array([  3.08285662e-44,   2.00000000e+00,  -1.00000000e+09,
         2.80259693e-42,   3.08285662e-44,   2.00000000e+00,
  ....   4.20389539e-42], dtype=float32)

这个视图是一维的。我可以重新调整形状和切片这两个浮点数列。

In [1434]: x.shape
Out[1434]: (12,)
In [1435]: x.view('f4').reshape(12,-1)
Out[1435]: 
array([[  3.08285662e-44,   2.00000000e+00,  -1.00000000e+09,
          2.80259693e-42],
       [  3.08285662e-44,   2.00000000e+00,   4.00000000e+02,
          2.80259693e-42],
         ...
       [  4.62428493e-44,   3.00000000e+00,   4.44000000e+04,
          4.20389539e-42]], dtype=float32)

In [1437]: x.view('f4').reshape(12,-1)[:,1:3]
Out[1437]: 
array([[  2.00000000e+00,  -1.00000000e+09],
       [  2.00000000e+00,   4.00000000e+02],
       [  2.00000000e+00,   8.04846000e+05],
       [  2.00000000e+00,   8.00000000e+02],
       ...
       [  3.00000000e+00,   4.44000000e+04]], dtype=float32)

通过进行一些现场数学计算并查看 x 中的结果,可以验证这是一个视图:

In [1439]: y=x.view('f4').reshape(12,-1)[:,1:3]
In [1440]: y[:,0] += .5
In [1441]: y
Out[1441]: 
array([[  2.50000000e+00,  -1.00000000e+09],
       [  2.50000000e+00,   4.00000000e+02],
       ...
       [  3.50000000e+00,   4.44000000e+04]], dtype=float32)
In [1442]: x
Out[1442]: 
array([(22, 2.5, -1000000000.0, 2000), (22, 2.5, 400.0, 2000),
       (22, 2.5, 804846.0, 2000), (44, 2.5, 800.0, 4000),
       (55, 5.5, 900.0, 5000), (55, 5.5, 1000.0, 5000),
       (55, 5.5, 8900.0, 5000), (55, 5.5, 11400.0, 5000),
       (33, 3.5, 14500.0, 3000), (33, 3.5, 40550.0, 3000),
       (33, 3.5, 40990.0, 3000), (33, 3.5, 44400.0, 3000)], 
      dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])

如果字段大小不同,这可能是不可能的。例如,如果浮点数为8个字节。关键是想象结构化数据的存储方式,并想象是否可以将其视为多列的简单dtype。并且字段选择必须等效于基本切片。使用['f1','f4']进行操作相当于使用[:,[0,3]]进行高级索引,这必须是复制品。
==========
“直接”字段索引是:
z = x[['f2','f3']].view('f4').reshape(12,-1)
z -= .5

该函数修改 z,但带有一个 futurewarning。同时它不会修改 xz 已变为一份拷贝。我还可以通过查看 z.__array_interface__['data'],即数据缓冲区位置(并与 xy 进行比较)来确认这一点。

=================

你的 fields_view 确实创建了一个结构化视图:

In [1480]: w=fields_view(x,['f2','f3'])
In [1481]: w.__array_interface__['data']
Out[1481]: (151950184, False)
In [1482]: x.__array_interface__['data']
Out[1482]: (151950184, False)

这段代码可以用来修改变量x,例如w['f2'] -= .5。因此,它比直接使用x[['f2','f3']]更加灵活。

w的数据类型为:

dtype({'names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12})

在你的recarr_to_ndarr函数中添加print(shape, typ, offsets, strides),我得到了如下结果(py3)

In [1499]: recarr_to_ndarr(w,'<f4')
(12, 2) <f4 [4, 8] (16, 4)
....
ValueError: ndarray is not contiguous

In [1500]: np.ndarray(shape=(12,2), dtype='<f4', buffer=w.data, offset=4, strides=(16,4))
...
BufferError: memoryview: underlying buffer is not contiguous

那个“连续”的问题一定是指w.flags中显示的值:
In [1502]: w.flags
Out[1502]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : False
  ....

有趣的是,w.dtype.descr 将“offsets”转换为未命名字段:

In [1506]: w.__array_interface__
Out[1506]: 
{'data': (151950184, False),
 'descr': [('', '|V4'), ('f2', '<f4'), ('f3', '<f4')],
 'shape': (12,),
 'strides': (16,),
 'typestr': '|V12',
 'version': 3}

无论哪种方式,w都有一个非连续的数据缓冲区,不能用于创建新数组。展开后,数据缓冲区看起来像这样:

xoox|xoox|xoox|...   
# x 4 bytes we want to skip
# o 4 bytes we want to use
# | invisible bdry between records in x

我构建的y具有以下特点:
In [1511]: y.__array_interface__
Out[1511]: 
{'data': (151950188, False),
 'descr': [('', '<f4')],
 'shape': (12, 2),
 'strides': (16, 4),
 'typestr': '<f4',
 'version': 3}

因此,它访问具有4字节偏移量的o字节,然后(16,4)步幅和(12,2)形状。

如果我修改您的ndarray调用以使用原始的x.data,它就可以工作:

In [1514]: xx=np.ndarray(shape=(12,2), dtype='<f4', buffer=x.data, offset=4, strides=(16,4))
In [1515]: xx
Out[1515]: 
array([[  2.00000000e+00,  -1.00000000e+09],
       [  2.00000000e+00,   4.00000000e+02],
           ....
       [  3.00000000e+00,   4.44000000e+04]], dtype=float32)

使用与我的y相同的array_interface:

In [1516]: xx.__array_interface__
Out[1516]: 
{'data': (151950188, False),
 'descr': [('', '<f4')],
 'shape': (12, 2),
 'strides': (16, 4),
 'typestr': '<f4',
 'version': 3}

非常感谢您提供如此详细的答案!它帮助我解决了我的问题,详情请见更新后的帖子。 - snowleopard

1

hpaulj说得对,问题在于结构化数组的子集不是连续的。有趣的是,我找到了一种方法使数组子集连续,具体方法如下:

  def view_fields(a, fields):
        """
        `a` must be a numpy structured array.
        `names` is the collection of field names to keep.

        Returns a view of the array `a` (not a copy).
        """
        dt = a.dtype
        formats = [dt.fields[name][0] for name in fields]
        offsets = [dt.fields[name][1] for name in fields]
        itemsize = a.dtype.itemsize
        newdt = np.dtype(dict(names=fields,
                              formats=formats,
                              offsets=offsets,
                              itemsize=itemsize))
        b = a.view(newdt)
        return b

In [5]: view_fields(x,['f2','f3']).flags
Out[5]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : True
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False

旧函数:
In [10]: fields_view(x,['f2','f3']).flags
Out[10]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : False
  OWNDATA : False
  WRITEABLE : True
  ALIGNED : True
  UPDATEIFCOPY : False

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接