Повышение эффективности сопоставления Python

Если у нас есть следующие входные данные, и мы хотели бы сохранить строки, если их «столбец APPID» (4-й столбец) одинаков, а их столбец «Категория» (18-й столбец) — это одна «Клетка» и одна «Биохимическая» или одна «Клетка» и один «Фермент».

A , APPID , C , APP_ID , D , E , F , G , H , I , J , K , L , M , O , P , Q , Категория , S , T
,,, APP-1 ,, ,,,,,,,,,,,, Cell ,,
,,, APP-1 ,,,,,,,,,,,,,, Enzyme ,,
,,, APP-2 ,,,,,,,,,,,,, Cell ,,
,,, APP-3 ,,,,,,,,,,,,,, Cell ,,
,,, APP -3,,,,,,,,,,,,,,Биохим,,

Идеальный выход будет

A , APPID , C , APP_ID , D , E , F , G , H , I , J , K , L , M , O , P , Q , Категория , S , T
,,, APP-1 ,, ,,,,,,,,,,,, Фермент,,
,,, АРР-3 ,,,,,,,,,,,,,,, Биохимический ,,
,,, АРР-1 ,,,,,,,,,,,,, Cell ,,
,,, APP-3 ,,,,,,,,,,,,,, Cell ,,

«APP-1» сохраняется, потому что их столбец 3 одинаков, а их категория - одна «Клетка», а другая - «Фермент». То же самое для «АРР-3», у которого в столбце «Категория» одна «Клетка», а другая «Биохимическая».

Следующая попытка может помочь:

import os

App=["1"]

for a in App:
    outname="App_"+a+"_target_overlap.csv"
    out=open(outname,'w')
    ticker=0
    cell_comp_id=[]
    final_comp_id=[]

    # make compound with cell activity (to a target) list first

    filename="App_"+a+"_target_Detail_average.csv"
    if os.path.exists(filename):
        file = open (filename)
        line=file.readlines()
        if(ticker==0): # Deal with the title
            out.write(line[0])
            ticker=ticker+1

            for c in line[1:]:
                c=c.split(',')
                if(c[17]==" Cell "):
                     cell_comp_id.append(c[3])
    else:
        cell_comp_id=list(set(cell_comp_id))

# while we have list of compounds with cell activity, now we search the Bio and Enz and make one final compound list

    if os.path.exists(filename):

        for c in line[1:]:
            temporary_line=c #for output_temp
            c=c.split(',')
            for comp in cell_comp_id:
                if (c[3]==comp and c[17]==" Biochemical "):
                    final_comp_id.append(comp)
                    out.write(str(temporary_line))
                elif (c[3]==comp and c[17]==" Enzyme "):
                    final_comp_id.append(comp)
                    out.write(str(temporary_line))
    else:
        final_comp_id=list(set(final_comp_id))

# After we obatin a final compound list in target a , we go through all the csv again for output the cell data

    filename="App_"+a+"_target_Detail_average.csv"

    if os.path.exists(filename):

        for c in line[1:]:
            temporary_line=c #for output_temp
            c=c.split(',')
            for final in final_comp_id:
                if (c[3]==final and c[17]==" Cell "):
                    out.write(str(temporary_line))

    out.close()

Когда входной файл небольшой (десятки тысяч строк), этот скрипт может закончить свою работу за разумное время. Однако входные файлы становятся миллионами или миллиардами строк, этот скрипт будет выполняться вечно (дни...). Я думаю, проблема в том, что мы создаем список APPID с «Cell» в 18-м столбце. Затем мы возвращаемся, чтобы сравнить этот список «Ячейка» (возможно, полмиллиона строк) со всем файлом (например, 1 миллион строк): Если какой-либо APPID в списке ячеек и весь файл одинаковый и 18-й столбец строки в целом файле «Фермент» или «Биохимический», мы сохраняем информацию. Этот шаг кажется очень трудоемким.

Я вот думаю, может быстрее подготовить словари "Клетка", "Фермент" и "Биохимический" и сравнить их? Могу ли я узнать, есть ли у какого-нибудь гуру лучший способ его обработки? Любой пример/комментарий будет полезен. Спасибо.

  • Мы используем питон 2.7.6.

person Chubaka    schedule 06.08.2014    source источник
comment
Это может лучше подойти для codereview.stackexchange.com.   -  person jonrsharpe    schedule 07.08.2014
comment
Можете ли вы сделать какие-либо предположения о распределении APPID? В вашем примере ввода оба экземпляра APP-1 находятся рядом друг с другом. Можете ли вы положиться на то, что это всегда так?   -  person skrrgwasme    schedule 07.08.2014
comment
Разместите свой код на pastebin.com или gist.github.com . Есть проблема с отступом кода в вопросе, и мне не очевидно, как вы этого хотите.   -  person pts    schedule 07.08.2014
comment
То, как вы откладываете свой код, делает его очень трудным для понимания. Не могли бы вы изменить отступ, используя 2 или 4 пробела для каждого уровня отступа?   -  person pts    schedule 07.08.2014
comment
Присвоение final_comp_id=list(set(final_comp_id)) и другое подобное мне непонятны. Возможно, это из-за неправильного отступа. Похоже, вы конвертируете пустой список обратно в пустой список. Это ваше намерение?   -  person pts    schedule 07.08.2014
comment
Для очень длинных файлов ваша программа использует ЦП на 100% или меньше? Если он использует меньше, то, вероятно, диск слишком медленный или у вас недостаточно памяти.   -  person pts    schedule 07.08.2014
comment
Какие вычислительные ресурсы у вас есть? billion of lines может перегрузить даже самый оптимизированный код на обычном компьютере.   -  person Andrew Johnson    schedule 07.08.2014
comment
Привет Андрей, у нас есть внутренний кластер. Существует около нескольких сотен процессоров. Файлов для обработки около 400, и каждый из них содержит около 1 миллиона строк.   -  person Chubaka    schedule 07.08.2014
comment
Привет, Скотт Лоусон, ты прав! Все одинаковые APPID будут отсортированы прямо друг к другу!   -  person Chubaka    schedule 07.08.2014
comment
Привет птс, спасибо! Кстати, я просто копирую скрипт сюда и тестирую в Linux python 2.7.6, и я не вижу никаких проблем с отступами.   -  person Chubaka    schedule 07.08.2014
comment
Вот основное сообщение, как вы хотите: gist.github.com/anonymous/719bd036a393c208f4eb У нас есть 100% CPU для запуска.   -  person Chubaka    schedule 07.08.2014
comment
@ Чубака: Спасибо. Некоторые строки в вашем коде (например, final_comp_id=list(set(final_comp_id)) и if(ticker==0):, что всегда верно) кажутся мне ненужными. Не могли бы вы перепроверить, что они действительно не нужны, и удалить их? Это не сделает его быстрее, но улучшит мое понимание этого. Кроме того, пожалуйста, избегайте .readlines(), как предложил Дэниел в своем ответе. Если код все еще работает медленно, опубликуйте новую версию, в противном случае примите ответ Дэниела.   -  person pts    schedule 07.08.2014


Ответы (2)


эффективное чтение файла(ов)

Одна большая проблема заключается в том, что вы читаете файл за один раз, используя readlines. Это потребует загрузки ВСЕХ в память за один раз. Я сомневаюсь, что у вас есть столько памяти.

Пытаться:

with open(filename) as fh:
    out.write(fh.readline()) # ticker
    for line in fh: #iterate through lines 'lazily', reading as you go.
        c = line.split(',')

код стиля для начала. Это должно очень помочь. Здесь, в контексте:

# make compound with cell activity (to a target) list first

if os.path.exists(filename):
    with open(filename) as fh:
        out.write(fh.readline()) # ticker
        for line in fh:
            cols = line.split(',')
            if cols[17] == " Cell ":
                cell_comp_id.append(cols[3])

синтаксис with open(...) as — это очень распространенная идиома Python, которая автоматически обрабатывает закрытие файла, когда вы заканчиваете блок with или если есть ошибка. Очень полезный.

наборы

Следующее, как вы предлагаете, лучше использовать sets.

Вам не нужно каждый раз заново создавать набор, вы можете просто обновить его, чтобы добавить элементы. Вот пример кода set (написан в стиле интерпретатора python, >>> в начале означает, что это строка для ввода — на самом деле не вводите бит >>>!):

>>> my_set = set()
>>> my_set
set()

>>> my_set.update([1,2,3])
>>> my_set
set([1,2,3])

>>> my_set.update(["this","is","stuff"])
>>> my_set
set([1,2,3,"this","is","stuff"])

>>> my_set.add('apricot')
>>> my_set
set([1,2,3,"this","is","stuff","apricot"])

>>> my_set.remove("is")
>>> my_set
set([1,2,3,"this","stuff","apricot"])

поэтому вы можете добавлять элементы и удалять их из набора, не создавая новый набор с нуля (что вы делаете каждый раз с битом cell_comp_id=list(set(cell_comp_id)).

Вы также можете получить различия, пересечения и т. д.:

>>> set(['a','b','c','d']) & set(['c','d','e','f'])
set(['c','d'])

>>> set([1,2,3]) | set([3,4,5])
set([1,2,3,4,5])

Дополнительную информацию см. в документах.

Итак, давайте попробуем что-то вроде:

cells = set()
enzymes = set()
biochemicals = set()

with open(filename) as fh:
    out.write(fh.readline()) #ticker
    for line in fh:
        cols = line.split(',')
        row_id = cols[3]
        row_category = cols[17]

        if row_category == ' Cell ':
            cells.add(row_id)
        elif row_category == ' Biochemical ':
            biochemicals.add(row_id)
        elif row_category == ' Enzyme ':
            enzymes.add(row_id)

Теперь у вас есть наборы клеток, биохимических веществ и ферментов. Вам нужно только их поперечное сечение, поэтому:

cells_and_enzymes = cells & enzymes
cells_and_biochemicals = cells & biochemicals

Затем вы можете снова просмотреть все файлы и просто проверить, есть ли row_id (или c[3]) в одном из этих списков, и если да, распечатать его.

На самом деле вы можете объединить эти два списка еще больше:

cells_with_enz_or_bio = cells_and_enzymes | cells_and_biochemicals

которые были бы клетками, у которых есть ферменты или биохимические вещества.

Итак, когда вы просматриваете файлы во второй раз, вы можете сделать:

if row_id in cells_with_enz_or_bio:
    out.write(line)

после всего этого?

Простого использования этих предложений может быть достаточно, чтобы помочь вам. Однако вы все еще храните в памяти целые наборы клеток, биохимических веществ и ферментов. И вы по-прежнему просматриваете файлы дважды.

Итак, есть два способа потенциально ускорить его, оставаясь при этом с одним процессом Python. Я не знаю, сколько памяти у вас есть. Если вам не хватает памяти, это может немного замедлить работу.

уменьшая наборы, как мы идем.

Если у вас есть миллион записей, и 800 000 из них являются парными (имеют запись клетки и биохимическую запись), то к тому времени, когда вы дойдете до конца списка, вы будете хранить 800 000 идентификаторов в наборах. Чтобы уменьшить использование памяти, как только мы определили, что хотим вывести запись, мы можем сохранить эту информацию (которую мы хотим напечатать запись) в файл на диске и прекратить хранить ее в памяти. Затем мы могли бы прочитать этот список позже, чтобы выяснить, какие записи нужно напечатать.

Поскольку это увеличивает дисковый ввод-вывод, это может быть медленнее. Но если у вас заканчивается память, это может уменьшить подкачку и, таким образом, ускорить работу. Трудно сказать.

with open('to_output.tmp','a') as to_output:
    for a in App:
        # ... do your reading thing into the sets ...

        if row_id in cells and (row_id in biochemicals or row_id in enzymes):
            to_output.write('%s,' % row_id)
            cells.remove(row_id)
            biochemicals.remove(row_id)
            enzymes.remove(row_id)

после того как вы прочитали все файлы, у вас теперь есть файл (to_output.tmp), который содержит все идентификаторы, которые вы хотите сохранить. Итак, вы можете прочитать это обратно в python:

with open('to_output.tmp') as ids_file:
    ids_to_keep = set(ids_file.read().split(','))

что означает, что вы можете затем при втором прогоне файлов просто сказать:

if row_id in ids_to_keep:
    out.write(line)

используя dict вместо наборов:

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

cells = {}
enzymes = {}
biochemicals = {}

with open(filename) as fh:
    out.write(fh.readline()) #ticker
    for line in fh:
        cols = line.split(',')
        row_id = cols[3]
        row_category = cols[17]

        if row_category == ' Cell ':
            cells[row_id] = line
        elif row_category == ' Biochemical ':
            biochemicals[row_id] = line
        elif row_category == ' Enzyme ':
            enzymes[row_id] = line

        if row_id in cells and row_id in biochemicals:
            out.write(cells[row_id])
            out.write(biochemicals[row_id])
            if row_id in enzymes:
                out.write(enzymes[row_id])
        elif row_id in cells and row_id in enzymes:
            out.write(cells[row_id])
            out.write(enzymes[row_id])

Проблема с этим методом заключается в том, что если какие-либо строки дублируются, он запутается.

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

Надеюсь, это поможет :-)

person Daniel    schedule 06.08.2014
comment
Привет Даниил, спасибо за ваш ответ. Я немного запутался, какую деталь заменить. Не могли бы вы просветить? Спасибо. - person Chubaka; 07.08.2014
comment
@Chubaka Я добавил больше примеров и некоторые другие идеи, которые вы могли бы использовать, чтобы ускорить процесс. Идея Скотта об использовании библиотек многопроцессорности или потоков для распараллеливания всего тоже может помочь — в зависимости от вашей настройки. Методы, о которых я упоминаю, могут быть достаточно легко объединены с этим. - person Daniel; 07.08.2014
comment
Реально помогает!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - person Chubaka; 07.08.2014
comment
Новый скрипт со словарями завершает работу за считанные секунды! Оригинальные сценарии могут занять около 18 дней (оценка), лол. - person Chubaka; 07.08.2014
comment
@Чубака: Круто! Я рад. :-) Это решено? - person Daniel; 07.08.2014
comment
Привет, Даниэль, я все еще имею дело с тем, что если какие-либо строки дублируются, это приведет к путанице. - person Chubaka; 08.08.2014
comment
Ключи dict уникальны. Поэтому, если у вас есть apricot, apple и banana в качестве ключей, и вы попытаетесь снова добавить что-то с именем apricot, то оно заменит первое apricot. В этом случае вы, вероятно, могли бы обойти это, добавив в бит if row_category == ' Cell ' дополнительный if row_id in cells:\n out.write(line)\ncontinue, который затем сказал бы, что если эта строка уже есть в ячейках, выведите эту новую строку, но не выполняйте проверку на row_id в ячейках и в биохимических веществах или ферментах проверьте это время. Имеет ли это смысл? - person Daniel; 08.08.2014

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

Вот общий алгоритм:

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

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

В частности, мне нравится использовать многопроцессорный пул, затем создавать список позиций начала/остановки фрагмента, а затем использовать pool.map(). Это будет заблокировано до тех пор, пока все не закончатся, и результаты каждого подпроцесса будут доступны, если вы поймаете возвращаемое значение из вызова карты.

Например, вы можете сделать что-то подобное в своих подпроцессах:

# assume we have passed in a byte position to start and end at, and a file name:

with open("fi_name", 'r') as fi:
    fi.seek(chunk_start)
    chunk = fi.readlines(chunk_end - chunk_start)

retriever = operator.itemgetter(3, 17) # extracts only the elements we want
APPIDs = {}

for line in chunk:

    ID, category = retriever(line.split())
    try:
        APPIDs[ID].append(category) # we've seen this ID before, add category to its list
    except KeyError:
        APPIDs[ID] = [category] # we haven't seen this ID before - make an entry

# APPIDs entries will look like this:
# 
# <APPID> : [list of categories]

return APPIDs

В вашем основном процессе вы должны получить все возвращенные словари и устранить дубликаты или перекрытия, а затем вывести что-то вроде этого:

for ID, categories in APPIDs.iteritems():
    if ('Cell' in categories) and ('Biochemical' in categories or 'Enzyme' in categories):
         # print or whatever

Пара замечаний/оговорок:

  • Обратите внимание на нагрузку на жесткий диск/SSD/где бы ни находились ваши данные. Если ваш текущий метод уже максимально использует свою пропускную способность, вы, вероятно, не увидите никаких улучшений производительности от этого. Вместо этого вы можете попробовать реализовать тот же алгоритм с поточностью.
  • Если вы получаете большую нагрузку на жесткий диск, которая НЕ связана с перегрузкой памяти, вы также можете уменьшить количество одновременных подпроцессов, которые вы разрешаете в пуле. Это приведет к меньшему количеству запросов на чтение к диску, но при этом будет использоваться по-настоящему параллельная обработка.
  • Ищите закономерности во входных данных, которые вы можете использовать. Например, если вы можете полагаться на то, что совпадающие APPID будут находиться рядом друг с другом, вы можете фактически выполнять все свои сравнения в подпроцессах и позволить вашему основному процессу зависать до тех пор, пока не придет время объединить структуры данных подпроцесса.

TL;DR

Разбейте файл на фрагменты и обрабатывайте их параллельно с помощью библиотеки многопроцессорной обработки.

person skrrgwasme    schedule 06.08.2014