以追加模式保存numpy数组

55

是否可以将numpy数组附加到已经存在的npy文件中进行保存,类似于np.save(filename,arr,mode='a')

我有几个函数需要遍历一个大型数组的行。由于内存限制,我不能一次性创建整个数组。为了避免重复创建每一行,我想将每一行创建一次并将其附加到先前在文件中的上一行。稍后,我可以在mmap_mode下加载npy文件,在需要时访问其中的切片。

7个回答

38

编辑:这个答案有点过时了,请参考第二个关于NpyAppendArray的答案。我不建议在2023年使用HDF5,而是使用numpy或zarr。

内置的.npy文件格式非常适合处理小数据集,而且不需要依赖除numpy之外的外部模块。

然而,当你开始拥有大量数据时,最好使用专门设计用于处理此类数据集的文件格式,例如HDF5[1]

例如,以下是使用PyTablesnumpy数组保存为HDF5的解决方案,

步骤1:创建可扩展的EArray存储

import tables
import numpy as np

filename = 'outarray.h5'
ROW_SIZE = 100
NUM_COLUMNS = 200

f = tables.open_file(filename, mode='w')
atom = tables.Float64Atom()

array_c = f.create_earray(f.root, 'data', atom, (0, ROW_SIZE))

for idx in range(NUM_COLUMNS):
    x = np.random.rand(1, ROW_SIZE)
    array_c.append(x)
f.close()

步骤2:如有需要,向现有数据集中添加行

f = tables.open_file(filename, mode='a')
f.root.data.append(x)

步骤三:读取数据的子集

f = tables.open_file(filename, mode='r')
print(f.root.data[1:10,2:20]) # e.g. read from disk only this part of the dataset

7
谢谢您指向PyTables。使用Array类的简单方法对我来说已经足够了。我很好奇为什么np.save没有追加模式。如果有必要,我想它会被实现的。 - user3820991
5
2018年是否仍然是最佳方法? - Moondra
3
HDF5作为比npy更优秀的文件格式是一个有争议的论点。越来越多的论文表明,HDF5实际上是一个非常麻烦的文件格式,例如exdir正在转向将数据保存在numpy文件中。 - Xaser
2
是的,这个答案有点过时了。现在zarr也可以作为一个选择。随意编辑答案。 - rth
3
2022年的最佳实践有什么建议吗? - Tom S
显示剩余2条评论

19
我编写了一个库来追加到Numpy的`.npy`文件中。以下是摘录:

https://pypi.org/project/npy-append-array

NpyAppendArray

通过在增长轴(C顺序为0,Fortran顺序为-1)上追加创建Numpy的.npy文件。它的行为类似于numpy.concatenate,但不同之处在于结果存储在一个.npy文件中,可以重复使用进行进一步的追加。创建后,文件可以通过内存映射读取(例如添加mmap_mode="r"),这样就可以创建和读取文件(可选地)大于计算机的主内存。

安装

conda install -c conda-forge npy-append-array

或者

pip install npy-append-array

示例

from npy_append_array import NpyAppendArray
import numpy as np

arr1 = np.array([[1,2],[3,4]])
arr2 = np.array([[1,2],[3,4],[5,6]])

filename = 'out.npy'

with NpyAppendArray(filename, delete_if_exists=True) as npaa:
    npaa.append(arr1)
    npaa.append(arr2)
    npaa.append(arr2)
    
data = np.load(filename, mmap_mode="r")

print(data)

实现细节

NpyAppendArray包含了Numpy软件包中修改过的、部分版本的format.py。它确保创建的数组头具有21个(=len(str(8*2**64-1)))字节的空余空间。这样可以在不增加数组头大小的情况下容纳一个最大化的维度数组(对于64位机器)。这使得我们可以简单地在向.npy文件末尾添加数据时重写头文件。


有没有办法让它们保持为单独的数组,而不是合并成一个巨大的数组? - Keegan Jay

12

这是对Mohit Pandey回答的扩展,展示了一个完整的保存/加载示例。它在Python 3.6和Numpy 1.11.3中进行了测试。

from pathlib import Path
import numpy as np
import os

p = Path('temp.npy')
with p.open('ab') as f:
    np.save(f, np.zeros(2))
    np.save(f, np.ones(2))

with p.open('rb') as f:
    fsz = os.fstat(f.fileno()).st_size
    out = np.load(f)
    while f.tell() < fsz:
        out = np.vstack((out, np.load(f)))

out = 数组([[ 0., 0.], [ 1., 1.]])


1
谢谢!只有一个注意点:对于行数很多的文件,这种加载方式会太慢。与其使用vstack(每次都会创建一个新的完整矩阵),不如先创建完整矩阵,然后填充行。例如: for i in range(size[0]): data[i,:] = np.load(f)``` - Khodeir

7

.npy文件包含头文件,其中包含数组的形状和数据类型。如果您知道最终数组的外观,您可以自己编写头文件,然后将数据分块。例如,这是连接二维矩阵的代码:

import numpy as np
import numpy.lib.format as fmt

def get_header(fnames):
    dtype = None
    shape_0 = 0
    shape_1 = None
    for i, fname in enumerate(fnames):
        m = np.load(fname, mmap_mode='r') # mmap so we read only header really fast
        if i == 0:
            dtype = m.dtype
            shape_1 = m.shape[1]
        else:
            assert m.dtype == dtype
            assert m.shape[1] == shape_1
        shape_0 += m.shape[0]
    return {'descr': fmt.dtype_to_descr(dtype), 'fortran_order': False, 'shape': (shape_0, shape_1)}

def concatenate(res_fname, input_fnames):
    header = get_header(input_fnames)
    with open(res_fname, 'wb') as f:
        fmt.write_array_header_2_0(f, header)
        for fname in input_fnames:
            m = np.load(fname)
            f.write(m.tostring('C'))

如果您需要更通用的解决方案(在追加时直接编辑标头), 您将不得不采用像[1]中的 fseek 技巧。
灵感来自于 [1]: https://mail.scipy.org/pipermail/numpy-discussion/2009-August/044570.html (不能直接使用) [2]: https://docs.scipy.org/doc/numpy/neps/npy-format.html [3]: https://github.com/numpy/numpy/blob/master/numpy/lib/format.py

1
使用numpy.save将数据附加到已存在的文件中,我们应该使用:
f_handle = file(filename, 'a')
numpy.save(f_handle, arr)
f_handle.close()

我已经检查过它在Python 2.7和NumPy 1.10.4中可以运行。
我从这里适应了代码,其中讲述了savetxt方法。

7
我刚刚检查了一下,在 python 2.7.12numpy 1.12.1 中不起作用。数组仍然保持不变,没有添加任何内容。另外请注意,您提供的链接是关于 savetxt 方法,而不是 np.save - Dennis Golomazov
2
我已经成功地使用了Python 3.5和Numpy 1.11.3中的这种堆叠模式。虽然需要以二进制模式打开文件。 - PaxRomana99
@PaxRomana99:这是我得到的结果: with Path('/tmp/npy').open('wb') as f: np.save(f, np.zeros(2)) with Path('/tmp/npy').open('ab') as f: np.save(f, np.ones(2)) np.load('/tmp/npy') 输出:array([0., 0.]) 希望得到 array([[0., 0.], [1., 1.]]) - ethanabrooks
1
@ethanabrooks:我已经添加了一个答案,展示了一个示例模式。 - PaxRomana99
1
这应该是使用 open 而不是 file 吗? - Shamoon
显示剩余3条评论

0
你可以尝试读取文件,然后添加新数据。
import numpy as np
import os.path

x = np.arange(10) #[0 1 2 3 4 5 6 7 8 9]

y = np.load("save.npy") if os.path.isfile("save.npy") else [] #get data if exist
np.save("save.npy",np.append(y,x)) #save the new

经过两次操作后:

print(np.load("save.npy")) #[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9]

3
这样做非常低效,因为您需要加载NumPy文件,而该文件甚至可能不适合内存。 - Vladimir Vargas

0
以下内容基于PaxRomana99的答案。 它创建了一个类,您可以使用该类保存和加载数组。 理想情况下,每次添加新数组时,还应更改npy文件的标头,以修改形状描述(有关标头描述,请参见此处
import numpy as np
import pickle

from pathlib import Path
import os


class npyAppendableFile():
    def __init__(self, fname, newfile=True):
        '''
        Creates a new instance of the appendable filetype
        If newfile is True, recreate the file even if already exists
        '''
        self.fname=Path(fname)
        if newfile:
            with open(self.fname, "wb") as fh:
                fh.close()
        
    def write(self, data):
        '''
        append a new array to the file
        note that this will not change the header
        '''
        with open(self.fname, "ab") as fh:
            np.save(fh, data)
            
    def load(self, axis=2):
        '''
        Load the whole file, returning all the arrays that were consecutively
        saved on top of each other
        axis defines how the arrays should be concatenated
        '''
        
        with open(self.fname, "rb") as fh:
            fsz = os.fstat(fh.fileno()).st_size
            out = np.load(fh)
            while fh.tell() < fsz:
                out = np.concatenate((out, np.load(fh)), axis=axis)
            
        return out
    
    
    def update_content(self):
        '''
        '''
        content = self.load()
        with open(self.fname, "wb") as fh:
            np.save(fh, content)

    @property
    def _dtype(self):
        return self.load().dtype

    @property
    def _actual_shape(self):
        return self.load().shape
    
    @property
    def header(self):
        '''
        Reads the header of the npy file
        '''
        with open(self.fname, "rb") as fh:
            version = np.lib.format.read_magic(fh)
            shape, fortran, dtype = np.lib.format._read_array_header(fh, version)
        
        return version, {'descr': dtype,
                         'fortran_order' : fortran,
                         'shape' : shape}
                
        
      
arr_a = np.random.rand(5,40,10)
arr_b = np.random.rand(5,40,7)    
arr_c = np.random.rand(5,40,3)    

f = npyAppendableFile("testfile.npy", True)        

f.write(arr_a)
f.write(arr_b)
f.write(arr_c)

out = f.load()

print (f.header)
print (f._actual_shape)

# after update we can load with regular np.load()
f.update_content()


new_content = np.load('testfile.npy')
print (new_content.shape)


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