如何判断 `DataFrame.to_numpy` 方法是否创建了副本

9

pandas.DataFrame.to_numpy方法有一个copy参数,具有以下文档:

copy : bool,默认为False

是否确保返回的值不是另一个数组的视图。请注意,copy=False不能确保to_numpy()不会复制。相反,copy=True确保即使不是严格必要的也会进行复制。

稍微尝试一下,似乎在对内存相邻且没有混合类型的数据调用to_numpy时,会保留视图。但是如何检查生成的numpy数组是否与创建它的数据帧共享内存,而不更改数据?

内存共享示例:

import pandas as pd
import numpy as np

# some data frame that I expect not to be copied
frame = pd.DataFrame(np.arange(144).reshape(12,12))
array = frame.to_numpy()
array[:] = 0
print(frame)
# Prints:
#     0  1  2  3  4  5  6  7  8  9  10  11
# 0   0  0  0  0  0  0  0  0  0  0   0   0
# 1   0  0  0  0  0  0  0  0  0  0   0   0
# 2   0  0  0  0  0  0  0  0  0  0   0   0
# 3   0  0  0  0  0  0  0  0  0  0   0   0
# 4   0  0  0  0  0  0  0  0  0  0   0   0
# 5   0  0  0  0  0  0  0  0  0  0   0   0
# 6   0  0  0  0  0  0  0  0  0  0   0   0
# 7   0  0  0  0  0  0  0  0  0  0   0   0
# 8   0  0  0  0  0  0  0  0  0  0   0   0
# 9   0  0  0  0  0  0  0  0  0  0   0   0
# 10  0  0  0  0  0  0  0  0  0  0   0   0
# 11  0  0  0  0  0  0  0  0  0  0   0   0

示例不共享内存:

import pandas as pd
import numpy as np

# some data frame that I expect to be copied
types = [int, str, float]
frame = pd.DataFrame({
    i: [types[i%len(types)](value) for value in col]
    for i, col in enumerate(np.arange(144).reshape(12,12).T)
})
array = frame.to_numpy()
array[:] = 0
print(frame)
# Prints:
#     0   1     2   3   4     5   6   7      8    9    10     11
# 0    0  12  24.0  36  48  60.0  72  84   96.0  108  120  132.0
# 1    1  13  25.0  37  49  61.0  73  85   97.0  109  121  133.0
# 2    2  14  26.0  38  50  62.0  74  86   98.0  110  122  134.0
# 3    3  15  27.0  39  51  63.0  75  87   99.0  111  123  135.0
# 4    4  16  28.0  40  52  64.0  76  88  100.0  112  124  136.0
# 5    5  17  29.0  41  53  65.0  77  89  101.0  113  125  137.0
# 6    6  18  30.0  42  54  66.0  78  90  102.0  114  126  138.0
# 7    7  19  31.0  43  55  67.0  79  91  103.0  115  127  139.0
# 8    8  20  32.0  44  56  68.0  80  92  104.0  116  128  140.0
# 9    9  21  33.0  45  57  69.0  81  93  105.0  117  129  141.0
# 10  10  22  34.0  46  58  70.0  82  94  106.0  118  130  142.0
# 11  11  23  35.0  47  59  71.0  83  95  107.0  119  131  143.0
3个回答

6

你可以使用 numpy.shares_memory

# Your first example
print(np.shares_memory(array, frame))  # True, they are sharing memory

# Your second example
print(np.shares_memory(array2, frame2))  # False, they are not sharing memory

还有一个numpy.may_share_memory函数,速度更快,但只能用于确保事物不共享内存(因为它仅检查边界是否重叠),严格来说它并没有回答这个问题。请参阅此处了解差异。
在使用这些numpy函数与pandas数据结构时要小心: np.shares_memory(frame, frame)对于第一个示例返回True,但对于第二个示例返回False,可能是因为第二个示例中的数据帧的__array__方法在幕后创建了一个拷贝。

我会使用np.shares_memory。它不够快,但是np.may_share_memory就像是copy参数一样,可以保证两个数组不共享数据,但只能暗示它们是否共享。 - Martin

1
在第一个情况下,您可以从数组中创建框架。源数组被'原样'用作框架的数据。也就是说,框架只是将其索引和方法添加到原始数组中:
In [377]: arr = np.arange(12).reshape(3,4)                                                    
In [378]: df = pd.DataFrame(arr)                                                              
In [379]: df                                                                                  
Out[379]: 
   0  1   2   3
0  0  1   2   3
1  4  5   6   7
2  8  9  10  11
In [380]: arr1 = df.to_numpy()                                                                
In [381]: arr1                                                                                
Out[381]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

我喜欢使用array_interface字典来比较数组。请注意,data在两个数组中都是相同的:
In [382]: arr.__array_interface__                                                             
Out[382]: 
{'data': (53291792, False),
 'strides': None,
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3, 4),
 'version': 3}
In [383]: arr1.__array_interface__                                                            
Out[383]: 
{'data': (53291792, False),
 'strides': None,
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3, 4),
 'version': 3}

我可以做变异测试。 在第二种情况下,您可以从字典中创建框架。我怀疑在这种情况下,框架实际上是一组pd.Series,但我不确定如何测试。
In [393]: df1 = pd.DataFrame({'a':np.arange(3), 'b':np.ones(3)})                              
In [394]: df1                                                                                 
Out[394]: 
   a    b
0  0  1.0
1  1  1.0
2  2  1.0
In [395]: x = df1.to_numpy()                                                                  
In [396]: x                                                                                   
Out[396]: 
array([[0., 1.],
       [1., 1.],
       [2., 1.]])

dtypes的变化是x是一个副本的很好的指示。 df1的列在dtype上有所不同,而x全为float。

并且通过可变测试:

In [397]: x *= 0                                                                              
In [398]: df1                                                                                 
Out[398]: 
   a    b
0  0  1.0
1  1  1.0
2  2  1.0

另一方面,使用完全浮动的方式构建相同的框架时,数组不是一个副本:
In [399]: df1 = pd.DataFrame({'a':np.arange(3.), 'b':np.ones(3)})                             
In [400]: df1                                                                                 
Out[400]: 
     a    b
0  0.0  1.0
1  1.0  1.0
2  2.0  1.0
In [401]: x = df1.to_numpy()                                                                  
In [402]: x *= 0                                                                              
In [403]: df1                                                                                 
Out[403]: 
     a    b
0  0.0  0.0
1  0.0  0.0
2  0.0  0.0

其他人建议查看标志。 我不确定这是可靠的。 我检查了[396]案例,x没有owndata

我可能没有为您的观察增加太多。 我认为我们需要深入挖掘帧如何存储其数据。 这可能取决于帧的构造方式,还可能取决于如何修改它(例如,当我添加列时会发生什么?)。

df.to_numpy只是np.array(self.values,dtype = dtype,copy = copy)。 在此级别上,它是否是副本取决于dtype转换(如果有)。

df.values是执行以下操作的属性:

self._consolidate_inplace()
return self._data.as_array(transpose=self._AXIS_REVERSED)

df._data 是一个 BlockManager(至少在我的例子中是这样)

如果这是一个 single_block,它的 as_array

np.asarray(mgr.blocks[0].get_values())

我本来想展示不同数据框的BlockMangers,但是刚刚失去了那个交互式Ipython会话。
[379]框架只有一个整数块;[394]框架有两个块,一个是浮点数,一个是整数。
无论如何,to_numpy()方法背后有很多pandas代码。而且很多代码都取决于该框架的数据存储方式。因此,我认为没有简单的确定数组是否为副本的方法。除非在简单、统一的数据框情况下,最好假设它是副本。但是如果您不想修改框架,请小心修改数组。
使用df.to_numpy(copy=True)确保获得副本。
我认为你不能确定获得一个视图。如果df具有统一的匹配dtype,则很有可能是视图,特别是如果构造不太复杂。

====

In [2]: df = pd.DataFrame(np.ones((3,4),int))                                                                   
In [3]: df                                                                                                      
Out[3]: 
   0  1  2  3
0  1  1  1  1
1  1  1  1  1
2  1  1  1  1
In [4]: df.to_numpy().flags                                                                                     
Out[4]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : False              <====
  ...
In [5]: df.to_numpy(copy=True).flags                                                                            
Out[5]: 
  ...
  OWNDATA : True

现在有一个混合数据类型的框架:

In [7]: df1 = pd.DataFrame({'a':np.arange(3), 'b':np.ones(3)})                                                  
In [8]: df1                                                                                                     
Out[8]: 
   a    b
0  0  1.0
1  1  1.0
2  2  1.0

这是一份副本,但不拥有数据。请注意,这是F_CONTIGUOUS;我认为这意味着在生成代码中存在转置,这可以解释为什么没有数据所有权:

In [10]: df1.to_numpy().flags                                                                                   
Out[10]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  ...
In [11]: df1.to_numpy()                                                                                         
Out[11]: 
array([[0., 1.],
       [1., 1.],
       [2., 1.]])

块管理器有两个块,一个用于每种数据类型:
In [12]: df1._data                                                                                              
Out[12]: 
BlockManager
Items: Index(['a', 'b'], dtype='object')
Axis 1: RangeIndex(start=0, stop=3, step=1)
FloatBlock: slice(1, 2, 1), 1 x 3, dtype: float64
IntBlock: slice(0, 1, 1), 1 x 3, dtype: int64

df1.values 是:

return self._data.as_array(transpose=self._AXIS_REVERSED)

"

as_array 不进行转置,同时使用:

"
In [14]: df1._data.as_array()                                                                                   
Out[14]: 
array([[0., 1., 2.],
       [1., 1., 1.]])
In [15]: df1._data.as_array(transpose=True)                                                                     
Out[15]: 
array([[0., 1.],
       [1., 1.],
       [2., 1.]])

因此,to_numpy 使用 np.array(values),存在可能进行 copydtype 操作的情况。 values 通过任务块管理器传递,该管理器至少执行一次 np.asarray() 和一个(可能的)transpose 操作。如果有多个块,则执行一个 _interleave 操作(我没有探索过)。

因此,虽然 to_numpy(copy=True) 确保了复制,但很难预测/检测到在此之前的处理是否已经创建了副本。


我猜测对于问题“pd.DataFrame.to_numpy 何时返回视图”的答案是“每当数据框架仅包含一个数据块时”。感谢您的工作! - Martin

0

要确定任何ndarray是复制品还是视图进入另一个数组,您可以使用owndata标志:

array.flags.owndata

在我的机器上运行时,以上两种情况的结果都是False。

顺便提一下,owndata 的另外几个例子:

a = np.arange(10)
print(a.flags.owndata) ==> True
b = a.reshape([5,2])
print(b.flags.owndata) ==> False. 

np.arange(3).reshape(3,1).flags.owndata 也是 False。 - hpaulj
@hpaulj - 请查看我在帖子上的附加说明。 - Roy2012
1
看代码和标志,我认为只有在 to_numpy(copy=True)(或其数据类型强制更改)时,owndata 才会为真。通常在此之前会有 transpose,它会关闭 owndata - hpaulj
正如你所说,这个标志不能区分这两种情况,因此无法回答这个问题。 - Martin

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