在稳定的 NumPy 中复制一个 sub-recarray

Copy a sub-recarray in stable NumPy

假设我在 numpy.recarray 中有数据,我想提取其中的一些列。我希望这是一个有效的副本,因为数据可能很大(我不想复制所有内容)但我可能会更改这些功能而不想更改 data(我不想查看)。

今天,我会做以下事情:

data = np.array([(1.0, 2.0, 0), (3.0, 4.0, 1)], 
            dtype=[('feature_1', float), ('feature_2', float), ('result', int)])
data = data.view(np.recarray)

features = data[['feature_1', 'feature_2']]

但是,它从 NumPy 中引发了以下 FutureWarning

/path/to/numpy/core/records.py:513: FutureWarning: Numpy has detected that you may be viewing or writing to an array returned by selecting multiple fields in a structured array.

This code may break in numpy 1.15 because this will return a view instead of a copy -- see release notes for details.

return obj.view(dtype=(self.dtype.type, obj.dtype))

这个警告非常受欢迎,因为我不想在更新 NumPy 时发生重大更改。然而,即使仔细阅读发行说明,也不清楚什么是最好的解决方案来编写在提取列时实现这种复制行为的东西,并且在即将发布的版本中保持稳定。

在我的特殊情况下,需要接近最佳的效率,并且 Pandas 不可用。在这些情况下,这种情况的最佳解决方法是什么?

如前所述,多字段选择处于不断变化的状态。我最近更新到 1.14.2,并且行为恢复到 1.14.0 之前的状态。

In [114]: data = np.array([(1.0, 2.0, 0), (3.0, 4.0, 1)], 
     ...:             dtype=[('feature_1', float), ('feature_2', float), ('resul
     ...: t', int)])
     ...:             
In [115]: data
Out[115]: 
array([(1., 2., 0), (3., 4., 1)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8'), ('result', '<i8')])
In [116]: features = data[['feature_1', 'feature_2']]
In [117]: features
Out[117]: 
array([(1., 2.), (3., 4.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])

(我省略了额外的 recarray 转换层。)

在 1.14.0 中,此 dtype 将包含一个 offset 值,表明 features 是一个视图,而不是一个副本。

我可以更改 features 的值而不更改 data:

In [124]: features['feature_1']
Out[124]: array([1., 3.])
In [125]: features['feature_1'] = [4,5]
In [126]: features
Out[126]: 
array([(4., 2.), (5., 4.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])
In [127]: data
Out[127]: 
array([(1., 2., 0), (3., 4., 1)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8'), ('result', '<i8')])

但如果不深入研究开发讨论,我不能说长期解决方案是什么。理想情况下,它应该能够获取 view(它维护原始数据缓冲区的 link)和一个副本,一个独立且可自由修改的数组。

我怀疑 copy 版本会遵循 recfunctions 的做法,即使用新的 dtype 构造一个新数组,然后逐字段复制数据。

In [132]: data.dtype.descr
Out[132]: [('feature_1', '<f8'), ('feature_2', '<f8'), ('result', '<i8')]
In [133]: dt = data.dtype.descr[:-1]
In [134]: dt
Out[134]: [('feature_1', '<f8'), ('feature_2', '<f8')]
In [135]: arr = np.zeros(data.shape, dtype=dt)
In [136]: arr
Out[136]: 
array([(0., 0.), (0., 0.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])
In [137]: for name in arr.dtype.fields:
     ...:     arr[name] = data[name]
     ...:     
In [138]: arr
Out[138]: 
array([(1., 2.), (3., 4.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])

或另一个 recfunctions 函数:

In [159]: rf.drop_fields(data, 'result')
Out[159]: 
array([(1., 2.), (3., 4.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])

recfunctions 具有可以复制复杂数据类型的代码,这些数据类型具有嵌套数据类型等。但是对于像这样简单的单层数据类型,简单的字段名迭代就足够了。

一般来说,结构化数组(和 recarray)有很多记录,但字段数量有限。所以按名称复制字段是比较有效的。

In [150]: import numpy.lib.recfunctions as rf
In [154]: arr = np.zeros(data.shape, dtype=dt)
In [155]: rf.recursive_fill_fields(data, arr)
Out[155]: 
array([(1., 2.), (3., 4.)],
      dtype=[('feature_1', '<f8'), ('feature_2', '<f8')])

但请注意其代码结尾为:

output = np.empty(base.shape, dtype=newdtype)
output = recursive_fill_fields(base, output)

开发说明在某些时候提到了一个 recfunctions.compress_fields 函数,但显然从未真正添加过。

您可以检查结果是否是具有

的视图
features = data[['feature_1', 'feature_2']]
if np.may_share_memory(features, data):
    features = features.copy()

更脆弱的是检查版本号:

features = data[['feature_1', 'feature_2']]
if np.lib.NumpyVersion(np.__version__) < np.lib.NumpyVersion('1.15.0'):
    features = features.copy()

请注意,像这样调用复制确实会占用不必要的内存(整个数组的内存)