Оптимизированная обработка очень больших файлов - PullRequest
5 голосов
/ 25 февраля 2011

Моя задача относительно проста: для каждой строки во входном файле проверить, удовлетворяет ли строка заданному набору условий, и, если это так, записать определенные столбцы этой строки в новый файл.Я написал скрипт на Python, который делает это, но мне нужна помощь: 1) улучшение скорости, 2) лучший способ работы с именами столбцов (так как номера столбцов могут варьироваться от файла к файлу), и 3) лучший способ указать мои условия фильтрации и желаемые выходные столбцы.

1) Файлы, с которыми я работаю, содержат фотометрию для астрономических изображений.Каждый файл занимает около 1e6 строк на 150 столбцов с плавающей запятой, обычно размером более 1 ГБ.У меня есть старый скрипт AWK, который обрабатывает такие файлы примерно за 1 минуту;Мой скрипт на python занимает от 5 до 7 минут.Мне часто приходится подстраивать условия фильтрации и перезапускать несколько раз, пока выходной файл не будет таким, каким я хочу, поэтому скорость определенно желательна.Я обнаружил, что цикл for достаточно быстрый;это то, как я делаю вещи внутри цикла, которые замедляют его.Использование itemgetter для выбора нужных столбцов было большим улучшением по сравнению с чтением всей строки в память, но я не уверен, что могу сделать для дальнейшего увеличения скорости.Может ли это быть так же быстро, как AWK?

2) Я бы хотел работать с именами столбцов, а не с номерами столбцов, так как номер столбца определенной величины (количество фотонов, фон, сигнал кшум и т. д.) может меняться между файлами.В моем скрипте AWK мне всегда нужно проверять правильность номеров столбцов, если указаны условия и выходные столбцы, даже если фильтрация и вывод применяются к одним и тем же количествам.Мое решение в Python было создать словарь, который присваивает номер столбца для каждой величины.Когда файл имеет разные столбцы, мне нужно только указать новый словарь.Возможно, есть лучший способ сделать это?

3) В идеале, мне нужно только указать имена входных и выходных файлов, условия фильтрации и желаемые столбцы для вывода, и они будут найденыв верхней части моего сценария, чтобы мне не пришлось искать код, чтобы просто что-то настроить.Моя главная проблема была с неопределенными переменными.Например, типичным условием является «SNR> 4», но «SNR» (сигнал-шум) фактически не присваивается значение до тех пор, пока строки не начнут считываться из файла фотометрии.Мое решение было использовать комбинацию строк и eval / exec.Опять же, может быть, есть лучший способ?

Я совсем не обучаюсь информатике (я аспирант по астрономии) - обычно я просто взламываю что-то вместе и отлаживаю, пока это не работает.Тем не менее, оптимизация в отношении моих трех пунктов выше стала чрезвычайно важной для моего исследования.Я прошу прощения за длинный пост, но я чувствовал, что детали будут полезны.Любой совет, который вы мне дадите, в дополнение к простой очистке / стилю кодирования, будет очень признателен.

Большое спасибо, Джейк

#! /usr/bin/env python2.6

from operator import itemgetter


infile = 'ugc4305_1.phot'
outfile = 'ugc4305_1_filt.phot'

# names must belong to dicitonary
conditions = 'OBJ <= 2 and SNR1 > 4 and SNR2 > 4 and FLAG1 < 8 and FLAG2 < 8 and (SHARP1 + SHARP2)**2 < 0.075 and (CROWD1 + CROWD2) < 0.1'

input = 'OBJ, SNR1, SNR2, FLAG1, FLAG2, SHARP1, SHARP2, CROWD1, CROWD2'
    # should contain all quantities used in conditions

output = 'X, Y, OBJ, COUNTS1, BG1, ACS1, ERR1, CHI1, SNR1, SHARP1, ROUND1, CROWD1, FLAG1, COUNTS2, BG2, ACS2, ERR2, CHI2, SNR2, SHARP2, ROUND2, CROWD2, FLAG2'

# dictionary of col. numbers for the more important qunatities
columns = dict(EXT=0, CHIP=1, X=2, Y=3, CHI_GL=4, SNR_GL=5, SHARP_GL=6, ROUND_GL=7, MAJAX_GL=8, CROWD_GL=9, OBJ=10, COUNTS1=11, BG1=12, ACS1=13, STD1=14, ERR1=15, CHI1=16, SNR1=17, SHARP1=18, ROUND1=19, CROWD1=20, FWHM1=21, ELLIP1=22, PSFA1=23, PSFB1=24, PSFC1=25, FLAG1=26, COUNTS2=27, BG2=28, ACS2=29, STD2=30, ERR2=31, CHI2=32, SNR2=33, SHARP2=34, ROUND2=35, CROWD2=36, FWHM2=37, ELLIP2=38, PSFA2=39, PSFB2=40, PSFC2=41, FLAG2=42)



f = open(infile)
g = open(outfile, 'w')


# make string that extracts values for testing
input_items = []
for i in input.replace(',', ' ').split():
    input_items.append(columns[i])
input_items = ', '.join(str(i) for i in input_items)

var_assign = '%s = [eval(i) for i in itemgetter(%s)(line.split())]' % (input, input_items) 


# make string that specifies values for writing
output_items = []
for i in output.replace(',', ' ').split():
    output_items.append(columns[i])
output_items = ', '.join(str(i) for i in output_items)

output_values = 'itemgetter(%s)(line.split())' % output_items


# make string that specifies format for writing
string_format = []
for i in output.replace(',', ' ').split():
    string_format.append('%s')
string_format = ' '.join(string_format)+'\n'


# main loop
for line in f:
   exec(var_assign)
   if eval(conditions):
      g.write(string_format % tuple(eval(output_values)))
f.close()
g.close()

Ответы [ 6 ]

2 голосов
/ 26 февраля 2011

Вот как бы я поступил примерно так ...

Это будет продолжаться ~ 35 секунд против ~ 3 минут для вашего оригинального сценария на моей машине.Можно добавить еще несколько оптимизаций (например, нам нужно преобразовать только несколько столбцов в числа с плавающей точкой), но это сокращает время выполнения всего на несколько секунд.

Вы также можете легко использовать csv.DictReader здесь, как предложили несколько человек.Я избегаю этого, так как вам нужно определить собственный диалект, и это всего лишь пара дополнительных строк, чтобы сделать то же самое без него.(Различные классы модулей csv также проверяют более сложное поведение (например, строки в кавычках и т. Д.), О котором вам не нужно беспокоиться в данном конкретном случае. Во многих случаях они очень, очень удобны, но онив этом случае небольшое перерасход.)

Обратите внимание, что вы также можете легко добавлять свои имена infile и outfile в качестве аргументов при вызове скрипта вместо их жесткого кодирования (например, infile = sys.argv[0] и т. д.).Это также позволит вам легко передавать или выводить данные ... (Вы можете проверить длину sys.argv и установить infile или outfile на sys.stdin и / или sys.stdout соответственно)

def main():
    infile = 'ugc4305_1.phot'
    outfile = 'ugc4305_1_filt.phot'
    process_data(infile, outfile)

def filter_conditions(row):
    for key, value in row.iteritems():
        row[key] = float(value)

    cond = (row['OBJ'] <= 2 and row['SNR1'] > 4 
       and row['SNR2'] > 4 and row['FLAG1'] < 8 
       and row['FLAG2'] < 8 
       and (row['SHARP1'] + row['SHARP2'])**2 < 0.075 
       and (row['CROWD1'] + row['CROWD2']) < 0.1
       )
    return cond

def format_output(row):
    output_columns = ('X', 'Y', 'OBJ', 'COUNTS1', 'BG1', 'ACS1', 'ERR1', 'CHI1', 
                     'SNR1', 'SHARP1', 'ROUND1', 'CROWD1', 'FLAG1', 'COUNTS2', 
                     'BG2', 'ACS2', 'ERR2', 'CHI2', 'SNR2', 'SHARP2', 'ROUND2', 
                     'CROWD2', 'FLAG2')
    delimiter = '\t'
    return delimiter.join((row[name] for name in output_columns))

def process_data(infilename, outfilename):
    column_names = ('EXT', 'CHIP', 'X', 'Y', 'CHI_GL', 'SNR_GL', 'SHARP_GL', 
                    'ROUND_GL', 'MAJAX_GL', 'CROWD_GL', 'OBJ', 'COUNTS1', 
                    'BG1', 'ACS1', 'STD1', 'ERR1', 'CHI1', 'SNR1', 'SHARP1', 
                    'ROUND1', 'CROWD1', 'FWHM1', 'ELLIP1', 'PSFA1', 'PSFB1', 
                    'PSFC1', 'FLAG1', 'COUNTS2', 'BG2', 'ACS2', 'STD2', 
                    'ERR2', 'CHI2', 'SNR2', 'SHARP2', 'ROUND2', 'CROWD2', 
                    'FWHM2', 'ELLIP2', 'PSFA2', 'PSFB2', 'PSFC2', 'FLAG2')

    with open(infilename) as infile:
        with open(outfilename, 'w') as outfile:
            for line in infile:
                line = line.strip().split()
                row = dict(zip(column_names, line))
                if filter_conditions(row.copy()):
                    outfile.write(format_output(row) + '\n')

if __name__ == '__main__':
    main()
2 голосов
/ 26 февраля 2011

Мой первый шаг здесь - избавиться от вызовов exec() и eval(). Каждый раз, когда вы проверяете строку, она должна быть скомпилирована, а затем выполнена, увеличивая накладные расходы на вызов функции в каждой строке вашего файла. Не говоря уже о том, что eval имеет тенденцию приводить к грязному, трудному для отладки коду, и его, как правило, следует избегать.

Вы можете начать рефакторинг, поместив свою логику в небольшие, легко понятные функции. Например, вы можете заменить eval(conditions) на функцию, например ::

def conditions(d):
    return (d[OBJ] <= 2 and
            d[SNRI] > 4 and
            d[SNR2] > 4 and
            d[FLAG1] < 8 and ...

Подсказка: если некоторые из ваших условных выражений имеют более высокую вероятность неудачи, поместите их в первую очередь, а python пропустит оценку остальных.

Я бы избавился от словаря имен столбцов и просто установил бы кучу переменных в верхней части вашего файла, а затем обратился бы к столбцам по line[COLNAME]. Это может помочь вам упростить некоторые части, такие как функция условий, и вы можете ссылаться на столбцы по имени, не назначая каждую переменную.

2 голосов
/ 25 февраля 2011

Я не думаю, что вы упомянули об этом, но похоже, что ваши данные находятся в CSV. Вы можете извлечь много пользы из csv.DictReader . Вы можете перебирать файлы по одной строке за раз (избегая загрузки всего объекта в память) и ссылаться на столбцы по их именам.

Вы также должны взглянуть на cProfile , профилировщик Python, если вы еще этого не сделали. Он скажет вам, какие биты вашей программы занимают больше всего времени.

1 голос
/ 26 февраля 2011

Как и в случае с nmichaels, вы можете использовать параметры fieldnames и dialect csv.DictReader для чтения этого файла.Тогда для каждой строки у вас будет словарь.Со словарем вам не нужно будет использовать eval, и вы можете использовать такие выражения, как

if data_item['OBJ'] <= 2 and data_item['SNR1']:
     g.write(data_item['X'], data_item['Y'], data_item['OBJ'])

. То, как вы делаете это сейчас, медленно и сложно из-за всех eval s.Там нет необходимости для этой сложности.

0 голосов
/ 18 декабря 2017

Вы пробовали панды ?

Я считаю, что OBJ, SNR1, ... являются именами столбцов, и я надеюсь, что вы применяете одно и то же условие ко всем строкам.Если это так, я предлагаю вам пойти с пандами.

Ваш фрагмент кода будет выглядеть примерно так ...

import pandas as pd

infile = 'ugc4305_1.phot'
outfile = 'ugc4305_1_filt.phot'

df = pd.read_csv(infile)

condition = (df['OBJ'] <= 2) & (df['SRN1'] > 4) & (df['SRN2'] > 4) & (df['FLAG1'] < 8) & (df['FLAG2'] < 8) & ((df['SHARP1'] + df['SHARP2'])**2 < 0.075) & ((df['CROWD1'] + df['CROWD2']) < 0.1)
newDf = df[condition]

columnNames = ['col1', 'col2', ...] # column names you want in result

newDf = df[columnNames]

newDf.to_csv(outfile)
0 голосов
/ 26 февраля 2011

Если профилирование показывает, что много времени уходит на фактическое чтение и анализ файлов, и вы будете обрабатывать один и тот же необработанный файл много раз, вы можете попытаться создать промежуточный формат файла, оптимизированный для чтения с помощью Python.

Можно попробовать один раз прочитать файл, проанализировать и вывести результат с помощью pickle / cPickle .Затем прочитайте промежуточный файл с помощью команды pickle / cpickle в вашем скрипте фильтра.

Я не знаю достаточно хорошо Python, чтобы сказать, будет ли это быстрее, чем чтение каждой строки, и разделите их.(В c # я бы использовал двоичный сериализатор, но я не знаю, доступен ли он в python).

Если дисковый ввод-вывод является бутылочным горлышком, вы также можете попробовать сжать входные файлы и прочитать их с помощьюмодуль gzip .

...