Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions TRAF4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from Bio import SeqIO
from Bio.SeqUtils import gc_fraction


class NuclAcidnucleotideError(ValueError):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Класс NuclAcidnucleotideError:
Этот класс представляет собой пользовательскую ошибку для классов, связанных с нуклеиновыми кислотами. Очень хорошо! Однако, название класса немного запутанное + не соответствует CamelCase. Можно сделать его более ясным. Например, NucleotideError или InvalidNucleotideError.

Suggested change
class NuclAcidnucleotideError(ValueError):
class InvalidNucleotideError(ValueError):

"""Custom error for NucleicAcidSequence classes"""
pass


class BiologicalSequence(str):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В этой дз такое вроде было разрешено, но в целом лучше не наследоваться от встроенных типов данных. Подробнее об этом можно посмотреть в консультации от 28 февраля (примерно 28-я минута)

Suggested change
class BiologicalSequence(str):
class BiologicalSequence():

def __init__(self, sequence):
self.sequence = sequence

def check_seq(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Насколько я помню, BiologicalSequence должен был содержать только абстрактные методы, которые уже переопределялись бы в дочерних классах

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Все так

valid_nucleotide_symbols = {'A', 'C', 'G', 'T', 'U'}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как я поняла из задания, проверки необходимо делать в дочерних классах, это абстрактный класс

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Верно

valid_prot_symbols = {'A','C','D','E','F','G','H','I','K','L','M','N','P','Q','R','S','T','V','W','Y'}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

По PEP-8 здесь стоит поставить пробелы после запятых :) Также можно было бы немного разнести символы по строкам, это бы улучшило восприятие кода

seq_symbols = set(self.sequence.upper())
if seq_symbols.issubset(valid_nucleotide_symbols):
print('NA sequence')
elif seq_symbols.issubset(valid_prot_symbols):
print('AA sequence')
else:
raise ValueError('Incorrect sequence input!')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мне кажется, здесь тоже было бы здорово вызвать кастомную ошибку, тем более что одна из таких у вас уже определена в начале скрипта


def __str__(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

сначала вы насладитесь от str, а потом пишете метод def str. как я понимаю, тут наследование от str- лишнее тогда

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В целом нет, __str__ же задает правило отображения, мы вполне можем хотеть их переопределить

return self.sequence

def __repr__(self):
return f'BiologicalSequence("{self.sequence}")'


class NucleicAcidSequence(BiologicalSequence):
def __init__(self, sequence):
self.complement_dict = None

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

отличный подход с complement_dict = None в родительском классе и его последующей спецификации в дочерних классах

super().__init__(sequence)

def complement(self):
if self.complement_dict == None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

С None лучше использовать is, а не ==:

Suggested change
if self.complement_dict == None:
if self.complement_dict is None:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошо

if self.complement_dict == None:

Лучше

if self.complement_dict is None:

Suggested change
if self.complement_dict == None:
if self.complement_dict is None:

raise NotImplementedError('It is a basic NA class. You should implement it for descendant class: DNASequence or RNASequence.')
result = type(self)(''.join([self.complement_dict[nuc] for nuc in self.sequence]))
return result
Comment on lines +37 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В методе complement можно использовать метод .get для получения значения из словаря. Это позволит избежать ошибок, если нуклеотид отсутствует в словаре.

Suggested change
def complement(self):
if self.complement_dict == None:
raise NotImplementedError('It is a basic NA class. You should implement it for descendant class: DNASequence or RNASequence.')
result = type(self)(''.join([self.complement_dict[nuc] for nuc in self.sequence]))
return result
def complement(self):
if not all(nuc in self.complement_dict for nuc in self.sequence):
raise NuclAcidnucleotideError('Invalid nucleotide in the sequence')
result = type(self)(''.join([self.complement_dict.get(nuc, '') for nuc in self.sequence]))
return result


def gc_content(self):
gc_sum = self.sequence.upper().count('G') + self.sequence.upper().count('C')
return 100 * gc_sum / len(self.sequence)

def __repr__(self):
return f'NucleicAcidSequence("{self.sequence}")'


class DNASequence(NucleicAcidSequence):
def __init__(self, sequence):
super().__init__(sequence)
self.complement_dict = {
'a': 't', 'A': 'T',
't': 'a', 'T': 'A',
'g': 'c', 'G': 'C',
'c': 'g', 'C': 'G'
}
if 'U' in self.sequence.upper():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здорово, что этот биологический момент предусмотрен!

raise NuclAcidnucleotideError('U-contain sequence is not proper DNA sequence')

def transcribe(self):
transcription_dict = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

словарь лучше бы вынести за функцию, в начало класса

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

'a': 'a', 'A': 'A',
't': 'u', 'T': 'U',
'g': 'g', 'G': 'G',
'c': 'c', 'C': 'C'
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Замечательно, что здесь подумали о читабельности кода и вывели элементы словаря на разных строках :)
Потерялась табуляция, возвращаю:

Suggested change
}
transcription_dict = {
'a': 'a', 'A': 'A',
't': 'u', 'T': 'U',
'g': 'g', 'G': 'G',
'c': 'c', 'C': 'C'
}

(по PEP-8 закрывающая скобка идёт на уровне с названием переменной)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут получается, что метод transcribe создает словарь transcription_dict каждый раз, когда вызывается метод. Чтобы этого избежать, можно было бы создать этот словарь как атрибут класса, т.е. просто поставить его перед __init__

result = RNASequence(''.join([transcription_dict[nuc] for nuc in self.sequence]))
return result

def __repr__(self):
return f'DNASequence("{self.sequence}")'


class RNASequence(NucleicAcidSequence):
def __init__(self, sequence):
super().__init__(sequence)
self.complement_dict = {
'a': 'u', 'A': 'U',
'u': 'a', 'U': 'A',
'g': 'c', 'G': 'C',
'c': 'g', 'C': 'G'
}
if 'T' in self.sequence.upper():
raise NuclAcidnucleotideError('T-contain sequence is not proper RNA sequence')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

молодец, что тут и раннее пишешь кастомные ошибки. очень хорошо вышло


def __repr__(self):
return f'RNASequence("{self.sequence}")'


class AminoAcidSequence(BiologicalSequence):
def gravy(self):
"""Calculate GRAVY (grand average of hydropathy) value"""
gravy_aa_values = {'L': 3.8,
'K': -3.9,
'M': 1.9,
'F': 2.8,
'P': -1.6,
'S': -0.8,
'T': -0.7,
'W': -0.9,
'Y': -1.3,
'V': 4.2,
'A': 1.8,
'R': -4.5,
'N': -3.5,
'D': -3.5,
'C': 2.5,
'Q': -3.5,
'E': -3.5,
'G': -0.4,
'H': -3.2,
'I': 4.5}
gravy_aa_sum = 0
for amino_ac in self.sequence.upper():
gravy_aa_sum += gravy_aa_values[amino_ac]
return round(gravy_aa_sum / len(self.sequence), 3)
Comment on lines +94 to +119

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно использовать функцию sum с генератором, чтобы упростить код.

Suggested change
def gravy(self):
"""Calculate GRAVY (grand average of hydropathy) value"""
gravy_aa_values = {'L': 3.8,
'K': -3.9,
'M': 1.9,
'F': 2.8,
'P': -1.6,
'S': -0.8,
'T': -0.7,
'W': -0.9,
'Y': -1.3,
'V': 4.2,
'A': 1.8,
'R': -4.5,
'N': -3.5,
'D': -3.5,
'C': 2.5,
'Q': -3.5,
'E': -3.5,
'G': -0.4,
'H': -3.2,
'I': 4.5}
gravy_aa_sum = 0
for amino_ac in self.sequence.upper():
gravy_aa_sum += gravy_aa_values[amino_ac]
return round(gravy_aa_sum / len(self.sequence), 3)
def gravy(self):
gravy_aa_values = {'L': 3.8, 'K': -3.9, 'M': 1.9, ...}
return round(sum(gravy_aa_values[amino_ac] for amino_ac in self.sequence.upper()) / len(self.sequence), 3)

Comment on lines +93 to +119

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Константу - словарь gravy_aa_values можно определить в начале класса, что поможет улучшить их видимость и управление.

Suggested change
class AminoAcidSequence(BiologicalSequence):
def gravy(self):
"""Calculate GRAVY (grand average of hydropathy) value"""
gravy_aa_values = {'L': 3.8,
'K': -3.9,
'M': 1.9,
'F': 2.8,
'P': -1.6,
'S': -0.8,
'T': -0.7,
'W': -0.9,
'Y': -1.3,
'V': 4.2,
'A': 1.8,
'R': -4.5,
'N': -3.5,
'D': -3.5,
'C': 2.5,
'Q': -3.5,
'E': -3.5,
'G': -0.4,
'H': -3.2,
'I': 4.5}
gravy_aa_sum = 0
for amino_ac in self.sequence.upper():
gravy_aa_sum += gravy_aa_values[amino_ac]
return round(gravy_aa_sum / len(self.sequence), 3)
class AminoAcidSequence(BiologicalSequence):
GRAVY_AA_VALUES = {'L': 3.8, 'K': -3.9, ...}
def gravy(self):
return round(sum(AminoAcidSequence.GRAVY_AA_VALUES[amino_ac] for amino_ac in self.sequence.upper()) / len(self.sequence), 3)


def __repr__(self):
return f'AminoAcidSequence("{self.sequence}")'



Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Затесалась лишняя строка

Suggested change

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лишняя строчка

Suggested change

def make_thresholds(threshold: int | float | tuple) -> tuple:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Такая запись аннотация типов характерна для версии питона 3.10, но вот в более ранних версиях она не будет поддерживаться. Чтобы улучшить совместимость с бОльшим количеством версий, можно приводить всё к записи такого вида:

Suggested change
def make_thresholds(threshold: int | float | tuple) -> tuple:
def make_thresholds(threshold: Union[int, float, tuple]) -> tuple:

Но в этом случае важно не забыть прописать нужные импорты из модуля typing :)

"""Check thresholds input and convert single value to tuple"""
if isinstance(threshold, int) or isinstance(threshold, float):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут можно написать короче:

Suggested change
if isinstance(threshold, int) or isinstance(threshold, float):
if isinstance(threshold, (int, float)):

lower = 0
upper = threshold
else:
lower = threshold[0]
upper = threshold[1]
return lower, upper
Comment on lines +126 to +134

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно изменить make_thresholds для более явного понимания его назначения.

Suggested change
def make_thresholds(threshold: int | float | tuple) -> tuple:
"""Check thresholds input and convert single value to tuple"""
if isinstance(threshold, int) or isinstance(threshold, float):
lower = 0
upper = threshold
else:
lower = threshold[0]
upper = threshold[1]
return lower, upper
def make_thresholds(threshold: int | float | tuple) -> tuple:
if isinstance(threshold, (int, float)):
return 0, threshold
elif isinstance(threshold, tuple):
return threshold
else:
raise ValueError('Invalid threshold input')

Comment on lines +126 to +134

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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



def filter_fastq(input_path: str,
gc_thresholds: int | float | tuple = (20, 80),
len_thresholds: int | float | tuple = (0, 2 ** 32),
quality_threshold: int | float = 0,
output_path: str = 'filtered.fastq'):
"""Filters out sequences from fastq file by the specified conditions:
- GC-content, inside interval include borders, or, if single value, not bigger than specified
- length, inside interval include borders, or, if single value, not bigger than specified
- average phred scores, not less than specified
Default output file name 'filtered.fastq'
"""
records = list(SeqIO.parse(input_path, 'fastq'))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В подобных случаях лучше обрабатывать записи в цикле, а не загружать всё целиком (на больших файлах может не хватить памяти):

Suggested change
records = list(SeqIO.parse(input_path, 'fastq'))
for record in SeqIO.parse(input_path, "fastq"):
...


filtered_1_gc_idxs = []
filtered_2_len_idxs = []
filtered_3_phred_idxs = []
filtered_results = []

min_gc, max_gc = make_thresholds(gc_thresholds)
min_len, max_len = make_thresholds(len_thresholds)

for count, record in enumerate(records):
gc_percent = gc_fraction(record.seq) * 100
if min_gc <= gc_percent <= max_gc:
filtered_1_gc_idxs.append(count)
Comment on lines +158 to +161

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно использовать более читаемую конструкцию for record in records вместо for count, record in enumerate(records).

Suggested change
for count, record in enumerate(records):
gc_percent = gc_fraction(record.seq) * 100
if min_gc <= gc_percent <= max_gc:
filtered_1_gc_idxs.append(count)
for record in records:
gc_percent = gc_fraction(record.seq) * 100
if min_gc <= gc_percent <= max_gc:
filtered_1_gc_idxs.append(record)


for idx in filtered_1_gc_idxs:
if min_len <= len(records[idx].seq) <= max_len:
filtered_2_len_idxs.append(idx)

for idx in filtered_2_len_idxs:
phred_values = records[idx].letter_annotations['phred_quality']
if sum(phred_values) / len(phred_values) >= quality_threshold:
filtered_3_phred_idxs.append(idx)

for idx in filtered_3_phred_idxs:
filtered_results.append(records[idx])
Comment on lines +172 to +173

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно использовать генераторное выражение для создания списка filtered_results.

Suggested change
for idx in filtered_3_phred_idxs:
filtered_results.append(records[idx])
filtered_results = [records[idx] for idx in filtered_3_phred_idxs]

Comment on lines +158 to +173

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

но твой вариант тоже рабочий, это главное


with open(output_path, 'w') as file:
SeqIO.write(filtered_results, file, 'fastq')