Более эффективный способ извлечения строк из огромного файла
У меня есть файл данных с идентификаторами 1 786 916 записей, и я хотел бы получить соответствующие записи из другого файла, который содержит около 4,8 миллиона записей (в данном случае последовательности ДНК, но в основном просто текст). Я написал для этого скрипт на python, но для его запуска требуются целые годы (день 3, и только 12% сделано). Поскольку я относительный новичок в python, мне было интересно, есть ли у кого-нибудь предложения, как сделать это быстрее.
Вот пример файла данных с идентификаторами (идентификатор в этом примере - ANICH889-10):
ANICH889-10 k__Animalia; p__Arthropoda; c__Insecta; o__Lepidoptera; f__Psychidae; g__Ardiosteres; s__Ardiosteres sp. ANIC9
ARONW984-15 k__Animalia; p__Arthropoda; c__Arachnida; o__Araneae; f__Clubionidae; g__Clubiona; s__Clubiona abboti
Вот пример второго файла, который содержит записи:
>ASHYE2081-10|Creagrura nigripesDHJ01|COI-5P|HM420985
ATTTTATACTTTTTATTAGGAATATGATCAGGAATAATTGGTCTTTCAATAAGAATCATTATCCGTATTGAATTAAGAAATCCAGGATCTATTATTAATAATGACCAAATTTATAATTCATTAATTACTATACACGCACTATTAATAATTTTTTTTTTAGTTATACCTGTAATAATTGGAGGATTTGGAAATTGATTAATTCCTATTATAATTGGAGCCCCAGATATAGCATTTCCACGAATAAACAATCTTAGATTTTGATTATTAATCCCATCAATTTTCATATTAATATTAAGATCAATTACTAATCAAGGTGTAGGAACAGGATGAACAATATATCCCCCATTATCATTAAATATAAATCAAGAAGGAATATCAATAGATATATCAATTTTTTCTTTACATTTAGCAGGAATATCCTCAATTTTAGGATCAATTAATTTCATTTCAACTATTTTAAATATAAAATTTATTAATTCTAATTATGATCAATTAACTTTATTTTCATGATCAATTCTAATTACTACTATTTTATTATTACTAGCAGTCCCTGTATTAGCAGGAGCAATTACTATAATTTTAACTGATCGAAATTTAAATACTTCTTTTTTTGATCCTAGAGGAGGAGGAGATCCAATTT-----------------
>BCISA145-10|Hemiptera|COI-5P
AACTCTATACTTTTTACTAGGATCCTGGGCAGGAATAGTAGGAACATCATTAAGATGAATAATCCGAATTGAACTAGGACAACCTGGATCTTTTATTGGAGATGACCAAACTTATAATGTAATTGTAACTGCCCACGCATTTGTAATAATTTTCTTTATAGTTATACCAATTATAATTGGAGGATTTGGAAATTGATTAATTCCCTTAATAATTGGAGCACCCGATATAGCATTCCCACGAATGAATAACATAAGATTTTGATTGCTACCACCGTCCCTAACACTTCTAATCATAAGTAGAATTACAGAAAGAGGAGCAGGAACAGGATGAACAGTATACCCTCCATTATCCAGAAACATCGCCCATAGAGGAGCATCTGTAGATTTAGCAATCTTTTCCCTACATCTAGCAGGAGTATCATCAATTTTAGGAGCAGTTAACTTCATTTCAACAATTATTAATATACGACCAGCAGGAATAACCCCAGAACGAATCCCATTATTTGTATGATCTGTAGGAATTACAGCACTACTACTCCTACTTTCATTACCCGTACTAGCAGGAGCCATTACCATACTCTTAACTGACCGAAACTTCAATACTTCTTTTTTTGACCCTGCTGGAGGAGGAGATCCCATCCTATATCAACATCTATTC
Однако во втором файле последовательности ДНК разбиты на несколько строк, а не на одну строку, и они не всегда имеют одинаковую длину.
РЕДАКТИРОВАТЬ
Вот мой желаемый результат:
>ANICH889-10
GGGATTTGGTAATTGATTAGTTCCTTTAATA---TTGGGGGCCCCTGACATAGCTTTTCCTCGTATAAATAATATAAGATTTTGATTATTACCTCCCTCTCTTACATTATTAATTTCAAGAAGAATTGTAGAAAATGGAGCTGGGACTGGATGAACTGTTTACCCTCCTTTATCTTCTAATATCGCCCATAGAGGAAGCTCTGTAGATTTA---GCAATTTTCTCTTTACATTTAGCAGGAATTTCTTCTATTTTAGGAGCAATTAATTTTATTACAACAATTATTAATATACGTTTAAATAATTTATCTTTCGATCAAATACCTTTATTTGTTTGAGCAGTAGGAATTACAGCATTTTTACTATTACTTTCTTTACCTGTATTAGCTGGA---GCTATTACTATATTATTAACT---------------------------------------------------------------------------
>ARONW984-15
TGGTAACTGATTAGTTCCATTAATACTAGGAGCCCCTGATATAGCCTTCCCCCGAATAAATAATATAAGATTTTGACTTTTACCTCCTTCTCTAATTCTTCTTTTATCAAGGTCTATTATNGAAAATGGAGCA---------GGAACTGGCTGAACAGTTTACCCTCCCCTTTCTTNTAATATTTCCCATGCTGGAGCTTCTGTAGATCTTGCAATCTTTTCCCTACACCTAGCAGGTATTTCCTCAATCCTAGGGGCAGTTAAT------TTTATCACAACCGTAATTAACATACGCTCTAGAGGAATTACATTTGATCGAATGCCTTTATTTGTATGATCTGTATTAATTACAGCTATTCTTCTACTACTCTCCCTCCCAGTATTAGCAGGGGCTATTACAATACTACTCACAGACCGAAATTTAAAT-----------------------------------
Вот скрипт Python, который я написал для этого:
from Bio import SeqIO
from Bio.Seq import Seq
import csv
import sys
#Name of the datafile
Taxonomyfile = "02_Arthropoda_specimen_data_less.txt"
#Name of the original sequence file
OrigTaxonSeqsfile = "00_Arthropoda_specimen.fasta"
#Name of the output sequence file
f4 = open("02_Arthropoda_specimen_less.fasta", 'w')
#Reading the datafile and extracting record IDs
TaxaKeep = []
with open(Taxonomyfile, 'r') as f1:
datareader = csv.reader(f1, delimiter='\t')
for item in datareader:
TaxaKeep.append(item[0])
print(len(TaxaKeep))
#Filtering sequence file to keep only those sequences with the desired IDs
datareader = SeqIO.parse(OrigTaxonSeqsfile, "fasta")
for seq in datareader:
for item in TaxaKeep:
if item in seq.id:
f4.write('>' + str(item) + '\n')
f4.write(str(seq.seq) + '\n')
Я думаю, что проблема в том, что я перебираю список из 1,7 миллионов имен записей для каждой из 4,8 миллионов записей. Я думал о создании словаря или чего-то подобного для 4,8 миллионов записей, но я не мог понять, как. Любые предложения (в том числе не-Python предложения)?
Спасибо!
3 ответа
Я думаю, что вы можете добиться большого прироста производительности, улучшив свой внешний вид.
Используя set()
может помочь вам с этим Наборы предназначены для очень быстрого поиска данных, и они не хранят повторяющиеся значения, что делает их идеальным выбором для фильтрации данных. Итак, давайте сохраним все идентификаторы таксономии из входного файла в наборе.
from Bio import SeqIO
from Bio.Seq import Seq
import csv
import sys
taxonomy_file = "02_Arthropoda_specimen_data_less.txt"
orig_taxon_sequence_file = "00_Arthropoda_specimen.fasta"
output_sequence_file = "02_Arthropoda_specimen_less.fasta"
# build a set for fast look-up of IDs
with open(taxonomy_file, 'r', newline='') as fp:
datareader = csv.reader(fp, delimiter='\t')
first_column = (row[0] for row in datareader)
taxonomy_ids = set(first_column)
# use the set to speed up filtering the input FASTA file
with open(output_sequence_file, 'w') as fp:
for seq in SeqIO.parse(orig_taxon_sequence_file, "fasta"):
if seq.id in taxonomy_ids:
fp.write('>')
fp.write(seq.id)
fp.write(seq.seq)
fp.write('\n')
- Я переименовал несколько ваших переменных. Называть переменную совершенно бессмысленно
f4
только для записи "#Name файла выходной последовательности" в комментарии выше. Почему бы не избавиться от комментария и назвать переменнуюoutput_sequence_file
напрямую? (row[0] for row in datareader)
это генератор понимания. Генератор - это итеративный объект, что означает, что он еще не вычисляет список идентификаторов - он просто знает, что делать. Это экономит время и память, не создавая временный список. Через одну строкуset()
Конструктор, который принимает итерации, будет строить набор из всех идентификаторов в первом столбце.- Во втором блоке мы используем
if seq.id in taxonomy_ids
проверить, должен ли выводиться идентификатор последовательности.in
очень быстро на съемках. - Я зову
.write()
четыре раза вместо того, чтобы строить временную строку из четырех предметов. Я предполагаюseq.id
а такжеseq.seq
уже строкиstr()
на них не очень надо. - Я не знаю много о формате файлов FASTA, но быстрый взгляд на документацию BioPython предполагает, что использование
SeqIO.write()
будет лучшим способом создания формата.
Вы правы, рассуждая, что используя два вложенных for
циклы, которые вы собираетесь потратить время, необходимое для выполнения одной операции для 4.8 million * 1.7 million
повторяется.
Именно поэтому мы будем использовать базу данных SQLite для хранения всей информации, содержащейся в OrigTaxonSeqsfile
, Почему SQLite? Так как
- SQLite поставляется с Python
- SQLite поддерживает индексирование
Я не могу начать объяснять теорию CS, но индексы - это Бог, когда дело доходит до поиска данных в таких случаях, как ваш.
Как только данные проиндексированы, вы просто ищите каждый идентификатор записи из Taxonomyfile
в базе данных и запишите это в свой f4
конечный выходной файл.
Следующий код должен работать так, как вы хотите, он имеет следующие преимущества:
- показывает прогресс, достигнутый в количестве обработанных строк
- нужен только Python 3, био библиотеки не нужны
- использует генераторы, поэтому ни один файл не должен быть прочитан в память все сразу
- не зависит от list / set / dict, потому что (в данном конкретном случае) они могут потреблять слишком много оперативной памяти
Вот код
import sqlite3
from itertools import groupby
from contextlib import contextmanager
Taxonomyfile = "02_Arthropoda_specimen_data_less.txt"
OrigTaxonSeqsfile = "00_Arthropoda_specimen.fasta"
@contextmanager
def create_db(file_name):
""" create SQLite db, works as context manager so file is closed safely"""
conn = sqlite.connect(file_name, isolation_level="IMMEDIATE")
cur = conn.connect()
cur.execute("""
CREATE TABLE taxonomy
( _id INTEGER PRIMARY KEY AUTOINCREMENT
, record_id TEXT NOT NULL
, record_extras TEXT
, dna_sequence TEXT
);
CREATE INDEX idx_taxn_recID ON taxonomy (record_id);
""")
yield cur
conn.commit()
conn.close()
return
def parse_fasta(file_like):
""" generate that yields tuple containing record id, extra info
in tail of header and the DNA sequence with newline characters
"""
# inspiration = https://www.biostars.org/p/710/
try:
from Bio import SeqIO
except ImportError:
fa_iter = (x[1] for x in groupby(file_like, lambda line: line[0] == ">"))
for header in fa_iter:
# remove the >
info = header.__next__()[1:].strip()
# seprate record id from rest of the seqn info
x = info.split('|')
recID, recExtras = x[0], x[1:]
# build the DNA seq using generator
sequence = "".join(s.strip() for s in fa_iter.__next__())
yield recID, recExtras, sequence
else:
fasta_sequences = SeqIO.parse(file_like, 'fasta')
for fasta in fasta_sequences:
info, sequence = fasta.id, fasta.seq.tostring()
# seprate record id from rest of the seqn info
x = info.split('|')
recID, recExtras = x[0], x[1:]
yield recID, recExtras, sequence
return
def prepare_data(txt_file, db_file):
""" put data from txt_file into db_file building index on record id """
i = 0
src_gen = open(txt_file, mode='rt')
fasta_gen = parse_fasta(src_gen)
with create_db(db_file) as db:
for recID, recExtras, dna_seq in fasta_gen:
db.execute("""
INSERT INTO taxonomy
(record_id, record_extras, dna_sequence) VALUES (?,?,?)
""",
[recID, recExtras, dna_seq]
)
if i % 100 == 0:
print(i, 'lines digested into sql database')
src_gen.close()
return
def get_DNA_seq_of(recordID, src):
""" search for recordID in src database and return a formatted string """
ans = ""
exn = src.execute("SELECT * FROM taxonomy WHERE record_id=?", [recordID])
for match in exn.fetchall():
a, b, c, dna_seq = match
ans += ">%s\n%s\n" % (recordID, dna_seq)
return ans
def main():
# first of all prepare an optimized database
db_file = txt_file + ".sqlite"
prepare_data(OrigTaxonSeqsfile)
# now start searching and writing
progress = 0
db = sqlite3.connect(db_file)
cur = db.cursor()
out_file = open("02_Arthropoda_specimen_less.fasta", 'wt')
taxa_file = open(Taxonomyfile, 'rt')
with taxa_file, out_file:
for line in taxa_file:
question = line.split("\t")[0]
answer = get_DNA_seq_of(question, cur)
out_file.write(answer)
if progress % 100 == 0:
print(progress, 'lines processed')
db.close()
if __name__ == '__main__':
main()
Не стесняйтесь спрашивать любые разъяснения.
Если вы получили какие-либо ошибки или если результат не соответствует ожиданиям, пожалуйста, пришлите мне образец 200 строк, каждый из Taxonomyfile
а также OrigTaxonSeqsfile
и я обновлю код.
Увеличение скорости
Ниже приведена приблизительная оценка, говоря только о дисковых операциях ввода-вывода, так как это самая медленная часть.
позволять a = 4.8 million
а также b = 1.7 million
,
При старом подходе вам придется выполнять дисковый ввод-вывод a * b
то есть 8160 миллиардов раз.
В моем подходе, когда вы выполняете индексацию (то есть 2 раза), вы должны искать 1,7 миллиона записей. Таким образом, в моем подходе общее время будет, 2 * (a + b)
т.е. 13 миллионов дискового ввода-вывода, что тоже не мало, но этот подход более чем в 600 тысяч раз быстрее
Почему бы и нет dict()
?
Босс и профессор ругают меня, если я обнаружил, что использую слишком много ЦП / ОЗУ. Если у вас есть система, более простой подход, основанный на диктовке:
from itertools import groupby
Taxonomyfile = "02_Arthropoda_specimen_data_less.txt"
OrigTaxonSeqsfile = "00_Arthropoda_specimen.fasta"
def parse_fasta(file_like):
""" generate that yields tuple containing record id, extra info
in tail of header and the DNA sequence with newline characters
"""
from Bio import SeqIO
fasta_sequences = SeqIO.parse(file_like, 'fasta')
for fasta in fasta_sequences:
info, sequence = fasta.id, fasta.seq.tostring()
# seprate record id from rest of the seqn info
x = info.split('|')
recID, recExtras = x[0], x[1:]
yield recID, recExtras, sequence
return
def prepare_data(txt_file, db_file):
""" put data from txt_file into dct """
i = 0
with open(txt_file, mode='rt') as src_gen:
fasta_gen = parse_fasta(src_gen)
for recID, recExtras, dna_seq in fasta_gen:
dct[recID] = dna_seq
if i % 100 == 0:
print(i, 'lines digested into sql database')
return
def get_DNA_seq_of(recordID, src):
""" search for recordID in src database and return a formatted string """
ans = ""
dna_seq = src[recordID]
ans += ">%s\n%s\n" % (recordID, dna_seq)
return ans
def main():
# first of all prepare an optimized database
dct = dict()
prepare_data(OrigTaxonSeqsfile, dct)
# now start searching and writing
progress = 0
out_file = open("02_Arthropoda_specimen_less.fasta", 'wt')
taxa_file = open(Taxonomyfile, 'rt')
with taxa_file, out_file:
for line in taxa_file:
question = line.split("\t")[0]
answer = get_DNA_seq_of(question, dct)
out_file.write(answer)
if progress % 100 == 0:
print(progress, 'lines processed')
return
if __name__ == '__main__':
main()
Я попросил дать разъяснения в комментарии к вашему вопросу, но на данный момент вы не отвечаете (никакой критики не намерено), поэтому я постараюсь ответить на ваш вопрос, прежде чем идти, основываясь на своем коде на следующих предположениях.
- Во 2-м файле данных каждая запись занимает две строки, первая строка является своего рода заголовком, а вторая - последовательностью ACGT.
- В строке заголовка у нас есть префикс
">"
затем некоторые поля разделяются"|"
и первое из этих полей - это идентификатор всей двухстрочной записи.
В соответствии с вышеуказанными предположениями
# If possible, no hardcoded filenames, use sys.argv and the command line
import sys
# command line sanity check
if len(sys.argv) != 4:
print('A descriptive error message')
sys.exit(1)
# Names of the input and output files
fn1, fn2, fn3 = sys.argv[1:]
# Use a set comprehension to load the IDs from the first file
IDs = {line.split()[0] for line in open(fn1)} # a set
# Operate on the second file
with open(fn2) as f2:
# It is possible to use `for line in f2: ...` but here we have(?)
# two line records, so it's a bit different
while True:
# Try to read two lines from file
try:
header = f2.next()
payload = f2.next()
# no more lines? break out from the while loop...
except StopIteration:
break
# Sanity check on the header line
if header[0] != ">":
print('Incorrect header line: "%s".'%header)
sys.exit(1)
# Split the header line on "|", find the current ID
ID = header[1:].split("|")[0]
# Check if the current ID was mentioned in the first file
if ID in IDs:
# your code
Поскольку внутреннего цикла нет, это должно быть на 6 порядков быстрее... еще неизвестно, сделает ли он то, что вам нужно:-)