Транспонировать большой массив без загрузки в память - PullRequest
6 голосов
/ 25 июня 2019

У меня есть большой сжатый файл (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 или другие методы, просто загружает всю таблицу наОЗУ, и я просто имею в своем распоряжении 6 ГБ.

По этой причине я хотел использовать метод, который записывает каждую транспонированную строку в открытый файл, а не хранит ее в ОЗУ.Я придумал следующий код:

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)

ОБНОВЛЕНИЕ

Ответ пользователя 7813790 приведет меня к этому коду:

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), транспонируя их и добавляя их в выходной файл.

Транспонирован 1-й чанк:

[[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]]

2-й транспонированный чанк:

[[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 в виде столбцов, используя out.seek().Насколько я понимаю, 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 столбцов ×> 1 млн строк.

ОБНОВЛЕНИЕ 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 ]

5 голосов
/ 25 июня 2019

В вашем рабочем, но медленном решении вы читаете входной файл 5000 раз - это не будет быстрым, но единственный простой способ минимизировать чтение - это прочитать все это в памяти.

Вы можете попробовать компромисс, когда вы читаете, скажем, пятьдесят столбцов за раз в память (~ 50 МБ) и записывать их в файл в виде строк.Таким образом, вы прочитали бы файл только 100 разПопробуйте несколько различных комбинаций, чтобы получить компромисс между производительностью и памятью, которым вы довольны.

Вы можете сделать это в течение трех вложенных циклов:

  1. Цикл по числу блоков (100в этом случае)
  2. Цикл по строкам входного файла
  3. Цикл по количеству столбцов в вашем чанке (50 здесь)

В вашем внутреннемВ большинстве циклов вы собираете значения столбцов в виде строки в двумерный массив, по одной строке для каждого среднего цикла.В самом внешнем цикле вы очищаете массив перед входом во внутренние циклы и затем распечатываете его в файл в виде строк.Для каждой итерации цикла 1. Вы будете записывать пятьдесят строк по миллиону столбцов.

Вы не можете вставить в середину обычного файла, не загрузив весь целевой файл в память - вам нужносдвиньте завершающие байты вперед вручную.Однако, поскольку вы знаете точный размер файла, вы можете предварительно выделить его и всегда искать позицию при записи каждого байта;вероятно, не очень быстро выполнить 5 миллиардов запросов, либо ... Если ваши единицы и нули распределяются достаточно равномерно, вы можете инициализировать файл со всеми нулями, а затем записывать только те (или наоборот), чтобы уменьшить число пополамof seeks.

Редактировать: Добавлены подробности о том, как можно реализовать чанкинг.

1 голос
/ 25 июня 2019

Если все ваши числа равны 0 или 1, тогда каждая строка имеет одинаковую длину (в байтах), поэтому вы можете использовать file.seek для перемещения по файлу (вместо чтения и игнорирования данных). Тем не менее, это может быть не так эффективно с gzipped входным файлом. Поскольку вы пишете несжатый файл, вы также можете использовать seek для перемещения по выходным данным.

Более эффективный способ транспонирования массива состоит в чтении фрагмента, который помещается в ОЗУ (например, 1000x1000), используйте numpy.transpose для транспонирования фрагмента, а затем запишите блок в его местоположение в транспонированном массиве. С вашим массивом, который имеет 5000 столбцов, но 1 миллион строк, вероятно, будет проще всего использовать фрагменты 5000x5000, т. Е. Читать 5000 полных строк входной матрицы за раз. Это позволяет избежать необходимости seek в сжатом входном файле. Затем вы должны записать этот блок в выходной файл, оставив пустое пространство для столбцов, которые поступают из последующих строк ввода.

Подробнее о том, как записать чанки в выходной файл 5000xN (как указано в комментарии):

Для записи первого фрагмента 5000x5000:

  • Искать в начале файла
  • Написать первый ряд фрагмента (5000 элементов)
  • Поиск в начале второй строки выходных данных (т. Е. Смещение 2N в файле или 2N + 1, если у вас есть окончания строки CRLF)
  • Напишите второй ряд фрагмента
  • Искать начало третьей строки файла
  • и т.д.

Для записи второго блока:

  • Поиск в позиции 5000 (начиная с нуля) первой строки вывода
  • Написать первый ряд фрагмента
  • Искать позицию 5000 второго выходного ряда
  • Напишите второй ряд фрагмента
  • и т.д.
...