Эффективный способ создания массивных массивов из двоичных файлов
У меня очень большие наборы данных, которые хранятся в двоичных файлах на жестком диске. Ниже приведен пример структуры файла:
Заголовок файла
149 Byte ASCII Header
Запуск записи
4 Byte Int - Record Timestamp
Пример запуска
2 Byte Int - Data Stream 1 Sample
2 Byte Int - Data Stream 2 Sample
2 Byte Int - Data Stream 3 Sample
2 Byte Int - Data Stream 4 Sample
Пример конца
Есть 122,880 образцов на запись и 713 записей на файл. Это дает общий размер 700 910 521 байт. Частота выборки и количество записей иногда меняются, поэтому мне приходится кодировать для определения количества каждого файла.
В настоящее время код, который я использую для импорта этих данных в массивы, работает следующим образом:
from time import clock
from numpy import zeros , int16 , int32 , hstack , array , savez
from struct import unpack
from os.path import getsize
start_time = clock()
file_size = getsize(input_file)
with open(input_file,'rb') as openfile:
input_data = openfile.read()
header = input_data[:149]
record_size = int(header[23:31])
number_of_records = ( file_size - 149 ) / record_size
sample_rate = ( ( record_size - 4 ) / 4 ) / 2
time_series = zeros(0,dtype=int32)
t_series = zeros(0,dtype=int16)
x_series = zeros(0,dtype=int16)
y_series = zeros(0,dtype=int16)
z_series = zeros(0,dtype=int16)
for record in xrange(number_of_records):
time_stamp = array( unpack( '<l' , input_data[ 149 + (record * record_size) : 149 + (record * record_size) + 4 ] ) , dtype = int32 )
unpacked_record = unpack( '<' + str(sample_rate * 4) + 'h' , input_data[ 149 + (record * record_size) + 4 : 149 + ( (record + 1) * record_size ) ] )
record_t = zeros(sample_rate , dtype=int16)
record_x = zeros(sample_rate , dtype=int16)
record_y = zeros(sample_rate , dtype=int16)
record_z = zeros(sample_rate , dtype=int16)
for sample in xrange(sample_rate):
record_t[sample] = unpacked_record[ ( sample * 4 ) + 0 ]
record_x[sample] = unpacked_record[ ( sample * 4 ) + 1 ]
record_y[sample] = unpacked_record[ ( sample * 4 ) + 2 ]
record_z[sample] = unpacked_record[ ( sample * 4 ) + 3 ]
time_series = hstack ( ( time_series , time_stamp ) )
t_series = hstack ( ( t_series , record_t ) )
x_series = hstack ( ( x_series , record_x ) )
y_series = hstack ( ( y_series , record_y ) )
z_series = hstack ( ( z_series , record_z ) )
savez(output_file, t=t_series , x=x_series ,y=y_series, z=z_series, time=time_series)
end_time = clock()
print 'Total Time',end_time - start_time,'seconds'
В настоящее время это занимает около 250 секунд на 700 МБ файла, что для меня кажется очень высоким. Есть ли более эффективный способ, которым я мог бы это сделать?
Окончательное решение
Использование метода numpy fromfile с пользовательским dtype сокращает время выполнения до 9 секунд, в 27 раз быстрее, чем исходный код выше. Окончательный код ниже.
from numpy import savez, dtype , fromfile
from os.path import getsize
from time import clock
start_time = clock()
file_size = getsize(input_file)
openfile = open(input_file,'rb')
header = openfile.read(149)
record_size = int(header[23:31])
number_of_records = ( file_size - 149 ) / record_size
sample_rate = ( ( record_size - 4 ) / 4 ) / 2
record_dtype = dtype( [ ( 'timestamp' , '<i4' ) , ( 'samples' , '<i2' , ( sample_rate , 4 ) ) ] )
data = fromfile(openfile , dtype = record_dtype , count = number_of_records )
time_series = data['timestamp']
t_series = data['samples'][:,:,0].ravel()
x_series = data['samples'][:,:,1].ravel()
y_series = data['samples'][:,:,2].ravel()
z_series = data['samples'][:,:,3].ravel()
savez(output_file, t=t_series , x=x_series ,y=y_series, z=z_series, fid=time_series)
end_time = clock()
print 'It took',end_time - start_time,'seconds'
Ответы
Ответ 1
Некоторые подсказки:
Что-то вроде этого (непроверено, но вы поняли):
import numpy as np
file = open(input_file, 'rb')
header = file.read(149)
# ... parse the header as you did ...
record_dtype = np.dtype([
('timestamp', '<i4'),
('samples', '<i2', (sample_rate, 4))
])
data = np.fromfile(file, dtype=record_dtype, count=number_of_records)
# NB: count can be omitted -- it just reads the whole file then
time_series = data['timestamp']
t_series = data['samples'][:,:,0].ravel()
x_series = data['samples'][:,:,1].ravel()
y_series = data['samples'][:,:,2].ravel()
z_series = data['samples'][:,:,3].ravel()
Ответ 2
Numpy поддерживает отображение двоичных данных из данных непосредственно в массив, например объекты через numpy.memmap. Возможно, вы сможете скопировать файл и извлечь данные, которые вам нужны, с помощью смещений.
Для правильности верности просто используйте numpy.byteswap для того, что вы прочитали. Вы можете использовать условное выражение для проверки континентности хост-системы:
if struct.pack('=f', np.pi) == struct.pack('>f', np.pi):
# Host is big-endian, in-place conversion
arrayName.byteswap(True)
Ответ 3
Одной вопиющей неэффективностью является использование hstack
в цикле:
time_series = hstack ( ( time_series , time_stamp ) )
t_series = hstack ( ( t_series , record_t ) )
x_series = hstack ( ( x_series , record_x ) )
y_series = hstack ( ( y_series , record_y ) )
z_series = hstack ( ( z_series , record_z ) )
На каждой итерации это выделяет немного больший массив для каждой серии и копирует все данные, которые были прочитаны до сих пор. Это связано с большим количеством ненужного копирования и потенциально может привести к деструкции памяти.
Я бы накапливал значения time_stamp
в списке и делал один hstack
в конце и делал бы то же самое для record_t
и т.д.
Если это не приносит достаточных улучшений производительности, я бы прокомментировал тело цикла и начал бы возвращать вещи за один раз, чтобы узнать, где именно потрачено время.
Ответ 4
У меня есть удовлетворительные результаты с аналогичной проблемой (многоканальные файлы двоичных данных с несколькими разрешениями) с помощью array
и struct.unpack
. В моей проблеме я хотел получить непрерывные данные для каждого канала, но файл имел структуру с интервальной ориентацией, а не структуру, ориентированную на канал.
"Секрет" состоит в том, чтобы сначала прочитать весь файл и только затем распределить срезы известного размера в нужные контейнеры (в приведенном ниже коде self.channel_content[channel]['recording']
- объект типа array
):
f = open(somefilename, 'rb')
fullsamples = array('h')
fullsamples.fromfile(f, os.path.getsize(wholefilename)/2 - f.tell())
position = 0
for rec in xrange(int(self.header['nrecs'])):
for channel in self.channel_labels:
samples = int(self.channel_content[channel]['nsamples'])
self.channel_content[channel]['recording'].extend(fullsamples[position:position+samples])
position += samples
Конечно, я не могу сказать, что это лучше или быстрее, чем другие предоставленные ответы, но, по крайней мере, это то, что вы могли бы оценить.
Надеюсь, что это поможет!