转置大型数组而不加载到内存中

时间:2019-06-25 11:39:12

标签: python arrays numpy memory transpose

我有一个很大的压缩文件(5000列×1M行),由0和1组成:

0 1 1 0 0 0 1 1 1....(×5000)
0 0 0 1 0 1 1 0 0
....(×1M)

我想对它进行转置,但是使用numpy或其他方法只会将整个表加载到RAM中,而我只有6GB的可用空间。

由于这个原因,我想使用一种方法将每条转置的行写入一个打开的文件,而不是将其存储在RAM中。我想出了以下代码:

import gzip

with open("output.txt", "w") as out:

    with gzip.open("file.txt", "rt") as file:

        number_of_columns = len(file.readline().split())

        # iterate over number of columns (~5000)
        for column in range(number_of_columns):

            # in each iteration, go to the top line to start again
            file.seek(0)

            # initiate list storing the ith column's elements that will form the transposed column
            transposed_column = []

            # iterate over lines (~1M), storing the ith element in the list
            for line in file:
                transposed_column.append(line.split()[column])

            # write the transposed column as a line to an existing file and back again
            out.write(" ".join(transposed_column) + "\n")

但是,这非常慢。有人可以建议我其他解决方案吗?有什么方法可以将列表作为列(而不是一行)附加到现有的打开文件中? (伪代码):

with open("output.txt", w) as out:
    with gzip.open("file.txt", rt) as file:
        for line in file:
            transposed_line = line.transpose()
            out.write(transposed_line, as.column)

更新

user7813790的回答将我引到以下代码:

import numpy as np
import random


# create example array and write to file

with open("array.txt", "w") as out:

    num_columns = 8
    num_lines = 24

    for i in range(num_lines):
        line = []
        for column in range(num_columns):
            line.append(str(random.choice([0,1])))
        out.write(" ".join(line) + "\n")


# iterate over chunks of dimensions num_columns×num_columns, transpose them, and append to file

with open("array.txt", "r") as array:

    with open("transposed_array.txt", "w") as out:

        for chunk_start in range(0, num_lines, num_columns):

            # get chunk and transpose
            chunk = np.genfromtxt(array, max_rows=num_columns, dtype=int).T
            # write out chunk
            out.seek(chunk_start+num_columns, 0)
            np.savetxt(out, chunk, fmt="%s", delimiter=' ', newline='\n')

它需要一个矩阵,例如:

0 0 0 1 1 0 0 0
0 1 1 0 1 1 0 1
0 1 1 0 1 1 0 0
1 0 0 0 0 1 0 1
1 1 0 0 0 1 0 1
0 0 1 1 0 0 1 0
0 0 1 1 1 1 1 0
1 1 1 1 1 0 1 1
0 1 1 0 1 1 1 0
1 1 0 1 1 0 0 0
1 1 0 1 1 0 1 1
1 0 0 1 1 0 1 0
0 1 0 1 0 1 0 0
0 0 1 0 0 1 0 0
1 1 1 0 0 1 1 1
1 0 0 0 0 0 0 0
0 1 1 1 1 1 1 1
1 1 1 1 0 1 0 1
1 0 1 1 1 0 0 0
0 1 0 1 1 1 1 1
1 1 1 1 1 1 0 1
0 0 1 1 0 1 1 1
0 1 1 0 1 1 0 1
0 0 1 0 1 1 0 1

并迭代二维尺寸等于列数(在这种情况下为8)的2D块,将它们转置并将其附加到输出文件中。

第一块换位:

[[0 0 0 1 1 0 0 1]
 [0 1 1 0 1 0 0 1]
 [0 1 1 0 0 1 1 1]
 [1 0 0 0 0 1 1 1]
 [1 1 1 0 0 0 1 1]
 [0 1 1 1 1 0 1 0]
 [0 0 0 0 0 1 1 1]
 [0 1 0 1 1 0 0 1]]

第二块换位:

[[0 1 1 1 0 0 1 1]
 [1 1 1 0 1 0 1 0]
 [1 0 0 0 0 1 1 0]
 [0 1 1 1 1 0 0 0]
 [1 1 1 1 0 0 0 0]
 [1 0 0 0 1 1 1 0]
 [1 0 1 1 0 0 1 0]
 [0 0 1 0 0 0 1 0]]

我正在尝试使用out.seek()将每个新块作为列追加到out文件中。据我所知,seek()将文件开头(即列)的偏移量作为第一个参数,而将0作为第二个参数则是从第一行开始。因此,我猜想下面的代码可以解决问题:

out.seek(chunk_start+num_columns, 0)

但是,它不会在接下来的行中以该偏移量继续。而且,它在第一行的开头添加了n = num_columns个空格。输出:

    0 0 0 1 0 1 1 1 0 1 1 0 1 0 0 0
1 1 0 1 1 0 1 0
1 1 1 0 1 1 1 1
1 1 1 1 1 1 0 0
1 0 1 1 1 0 1 1
1 1 0 1 1 1 1 1
1 0 0 1 0 1 0 0
1 1 0 1 1 1 1 1

对如何正确使用seek()进行此任务有任何见解?即生成此代码:

0 0 0 1 1 0 0 1 0 1 1 1 0 0 1 1 0 1 1 0 1 0 0 0
0 1 1 0 1 0 0 1 1 1 1 0 1 0 1 0 1 1 0 1 1 0 1 0
0 1 1 0 0 1 1 1 1 0 0 0 0 1 1 0 1 1 1 0 1 1 1 1
1 0 0 0 0 1 1 1 0 1 1 1 1 0 0 0 1 1 1 1 1 1 0 0
1 1 1 0 0 0 1 1 1 1 1 1 0 0 0 0 1 0 1 1 1 0 1 1
0 1 1 1 1 0 1 0 1 0 0 0 1 1 1 0 1 1 0 1 1 1 1 1
0 0 0 0 0 1 1 1 1 0 1 1 0 0 1 0 1 0 0 1 0 1 0 0
0 1 0 1 1 0 0 1 0 0 1 0 0 0 1 0 1 1 0 1 1 1 1 1

请注意,这只是一个虚拟测试矩阵,实际矩阵为5008列×> 100万行。

更新2

我已经弄清楚了如何进行这项工作,它还可以利用任何尺寸的块。

import numpy as np
import random


# create example array and write to file

num_columns = 4
num_lines = 8

with open("array.txt", "w") as out:
    for i in range(num_lines):
        line = []
        for column in range(num_columns):
            line.append(str(random.choice([0,1])))
        out.write(" ".join(line) + "\n")


# iterate over chunks of dimensions num_columns×chunk_length, transpose them, and append to file

chunk_length = 7

with open("array.txt", "r") as array:

    with open("transposed_array.txt", "w") as out:

        for chunk_start in range(0, num_lines, chunk_length):

            # get chunk and transpose
            chunk = np.genfromtxt(array, max_rows=chunk_length, dtype=str).T

            # write out chunk
            empty_line = 2 * (num_lines - (chunk_length + chunk_start))

            for i, line in enumerate(chunk):
                new_pos = 2 * num_lines * i + 2 * chunk_start
                out.seek(new_pos)
                out.write(f"{' '.join(line)}{' ' * (empty_line)}"'\n')

在这种情况下,它需要一个这样的数组:

1 1 0 1
0 0 1 0
0 1 1 0
1 1 1 0
0 0 0 1
1 1 0 0
0 1 1 0
0 1 1 1

并使用4列×7行的块将其转置,因此第一个块将是

1 0 0 1 0 1 0
1 0 1 1 0 1 1
0 1 1 1 0 0 1
1 0 0 0 1 0 0

将其写入文件,从内存中删除,然后第二个块是

0
1
1
1

并将其附加到文件中,因此最终结果是:

1 0 0 1 0 1 0 0
1 0 1 1 0 1 1 1
0 1 1 1 0 0 1 1
1 0 0 0 1 0 0 1

2 个答案:

答案 0 :(得分:5)

在工作但缓慢的解决方案中,您正在读取输入文件5,000次-不会很快,但是最大程度减少读取次数的唯一简便方法是在内存中全部读取。

您可以尝试一些折衷办法,例如一次将五十列读入内存(〜50MB),然后将它们作为行写入文件。这样,您将“仅”读取文件100次。尝试几种不同的组合,以获得您满意的性能/内存折衷。

您可以通过三个嵌套循环来完成此操作:

  1. 循环处理块数(在这种情况下为100)
  2. 遍历输入文件的行
  3. 遍历块中的列数(此处为50)

在最内层的循环中,将列值作为一行收集到二维数组中,中间的每个循环各占一行。在最外面的循环中,您在进入内部循环之前先清除数组,然后将其作为行打印到文件中。对于循环1的每次迭代,您将写出50行,每百万列。

如果不将整个目标文件加载到内存中,就不能真正插入普通文件的中间,您需要手动将尾随字节向前移动。但是,由于您知道确切的文件大小,因此可以预先分配文件大小,并在写入每个字节时始终寻求位置。可能不是非常快地完成50亿次搜索,或者...如果您的1和0相当均匀地分布,则可以使用全零初始化文件,然后仅写入1(或相反)以将数字减半寻求。

编辑:添加了有关如何实现分块的详细信息。

答案 1 :(得分:1)

如果您的数字全为0或1,则每一行的长度均相同(以字节为单位),因此您可以使用file.seek在文件中四处移动(而不是读入并忽略数据)。但是,对于压缩后的输入文件,这可能不是那么有效。由于您正在编写未压缩的文件,因此您也可以使用seek在输出中跳转。

一种更有效的转置数组方法是读取适合RAM的块(例如1000x1000),使用numpy.transpose进行转置,然后将其写入转置数组中的位置。对于具有5000列但1M行的数组,使用5000x5000块可能最简单,即读取 一次输入矩阵的完整行为5000。这样可以避免在压缩的输入文件中seek。然后,您必须将此块写入输出文件,在输入的后续行中留出空白列。

有关如何将块写入5000xN输出文件的更多详细信息(如注释中所述):

要写入第一个5000x5000块:

  • 搜索文件的开头
  • 写入块的第一行(5000个元素)
  • 搜索到输出第二行的开头(即文件中的偏移量2N,如果有CRLF行尾,则偏移2N + 1)
  • 写块的第二行
  • 搜索到文件第三行的开头

编写第二个块:

  • 寻求将输出的第一行的位置5000(从零开始)
  • 写块的第一行
  • 寻求在第二个输出行中定位5000
  • 写块的第二行