Был выбран домен Acetone_carb_G, основные параметры которого приведены в таблице:
ID | Accession | Type | Seed | Full | Uniprot | Average length | Average %id | Average coverage | Change status | Description |
---|---|---|---|---|---|---|---|---|---|---|
Acetone_carb_G | PF08882 | Domain | 27 | 307 | 1790 | 106.8 | 36 | 48.03 | Changed | Acetone carboxylase gamma subunit |
Для работы была выбрана двухдоменная архитектура A0A1H6FYJ5_THEAL Hydantoinase_B - Acetone_carb_G (в порядке следования доменов). Такая архитектура состоит из 38 последовательностей.
По данным pfam, длина профиля для данного домена равна 114.
import pandas as pd
import plotly.express as px
import plotly.io as pio
from Bio import SeqIO
from Bio.Seq import Seq
from collections import defaultdict
from sklearn.metrics import auc, f1_score
pio.renderers.default = 'iframe'
Для начала был получен список AC, содержащих двухдоменную архитектуру.
ac_2d = []
with open('2dac.txt') as records:
for line in records:
ac_2d.append(line.split()[0])
Далее были получены последовательности всех белков содержащих Acetone_carb_G, а после, уже из него, были получены последовательности двухдоменных белков. Как и ожидалось, вытащились 38 последовательностей.
full = SeqIO.parse('full.fasta', 'fasta')
records = [SeqIO.SeqRecord(seq = i.seq, id = i.id, description = '') for i in full if i.id in ac_2d]
SeqIO.write(records, '2dac.fasta', 'fasta')
38
len_full = [len(i) for i in SeqIO.parse('full.fasta', 'fasta')]
ac_full = [i.id for i in SeqIO.parse('full.fasta', 'fasta')]
with open('ac_full.txt', 'w') as out:
for i in ac_full:
print(i, file = out)
Результат выравнивания (muscle) и pезультат ревизии (jalview). Во время ревизии удалялись слишком длинные/слишком короткие последовательности, колонки, состоящие из гепов, а также небольшие участки до первого домена и участки после второго домена. После ревизии осталось 26 последовательностей. Список AC был также получен.
alig = SeqIO.parse('alig.fasta', 'fasta')
ac_alig = [i.id.split('/')[0] for i in alig]
with open('ac_alig.txt', 'w') as out:
for i in ac_alig:
print(i, file = out)
Теперь всё готово к постройке HMM. Команды ниже - это команды для создания профиля (его длина 702 а.о.), его калибровки, а также результата работы профиля по выборке full.
hmm2build profile alig.fasta
hmm2calibrate profile
hmmsearch --cpu=1 profile full.fasta &> hmmsearch_log.txt
Вычада hmmsearh была вручную переделана в нужную табличку.
hmm = pd.read_csv('hmm_table.csv')
hmm.head()
e-value | score | bias | seq | |
---|---|---|---|---|
0 | 0.0 | 1393.9 | 20.7 | A0A2H5VYN0_9BACT |
1 | 0.0 | 1295.8 | 0.0 | A0A562QB61_9BACI |
2 | 0.0 | 1289.4 | 0.0 | A0A2N5MCK9_9BACI |
3 | 0.0 | 1288.8 | 33.1 | A0A5B8UD28_9ACTN |
4 | 0.0 | 1277.4 | 46.5 | A0A5B8U2X4_9ACTN |
Далее был получен список находок по профилю.
ac_hmm = list(hmm.seq)
with open('ac_hmm.txt', 'w') as out:
for i in ac_hmm:
print(i, file = out)
Теперь попробуем сделать табличку, в которой в качестве индексов были бы названия последовательностей, а в качестве колонок: содержит ли нужную архитектуру? - architecture; использовалась ли в выравнивании? - alig; нашлась ли в hmm? - hmm. Часть таблицы представлена ниже, но она получилась неслишком информативной.
def checker(ac_sm):
result = {}
for i in ac_full:
if i in ac_sm:
result[i] = 1
else:
result[i] = 0
return result
dd = defaultdict(list)
for d in (checker(ac_2d), checker(ac_alig), checker(ac_hmm)):
for key, value in d.items():
dd[key].append(value)
df = pd.DataFrame(dd, index = ['architecture', 'alig', 'hmm']).transpose()
df.head()
architecture | alig | hmm | |
---|---|---|---|
A0A1I3KG61_9BACL | 0 | 0 | 1 |
A0A133XH84_9RHOO | 0 | 0 | 1 |
A0A0Q8KZJ6_9RHIZ | 0 | 0 | 1 |
Q0RWI2_RHOJR | 0 | 0 | 1 |
A0A4U0GLW4_9GAMM | 0 | 0 | 1 |
Для получения хорошей информативности - объединим обе таблички.
merged = pd.merge(df, hmm, how = 'right', left_on = df.index, right_on = 'seq')
merged.to_csv('merged.csv')
Теперь таблица выглядит куда более информативной. С ней и будем работать далее.
merged
architecture | alig | hmm | e-value | score | bias | seq | |
---|---|---|---|---|---|---|---|
0 | 1 | 1 | 1 | 0.000000 | 1393.9 | 20.7 | A0A2H5VYN0_9BACT |
1 | 1 | 1 | 1 | 0.000000 | 1295.8 | 0.0 | A0A562QB61_9BACI |
2 | 1 | 1 | 1 | 0.000000 | 1289.4 | 0.0 | A0A2N5MCK9_9BACI |
3 | 1 | 1 | 1 | 0.000000 | 1288.8 | 33.1 | A0A5B8UD28_9ACTN |
4 | 1 | 1 | 1 | 0.000000 | 1277.4 | 46.5 | A0A5B8U2X4_9ACTN |
... | ... | ... | ... | ... | ... | ... | ... |
302 | 0 | 0 | 1 | 0.000012 | 16.0 | 0.1 | A0A1M7FN25_9RHOB |
303 | 0 | 0 | 1 | 0.000030 | 14.7 | 0.0 | D4YQC9_9MICO |
304 | 0 | 0 | 1 | 0.000340 | 11.2 | 0.2 | A0A1R1SBK6_9ACTN |
305 | 0 | 0 | 1 | NaN | 0.0 | NaN | A0A143B505_9DELT |
306 | 0 | 0 | 1 | NaN | 0.0 | NaN | A0A511VFX0_9BACL |
307 rows × 7 columns
Расчитает False Positive Rate = 1 - spec.
fpr = []
for i in range(len(merged.architecture)):
try:
zeros = merged.architecture[:i].value_counts()[0]
except KeyError:
zeros = 0
fpr.append(zeros/269) # 269 - число последовательностей из full минус двухдоменные последовательности
Теперь посчитает True Positive Rate = sens.
tpr = []
for i in range(len(merged.architecture)):
try:
ones = merged.architecture[:i].value_counts()[1]
except KeyError:
ones = 0
tpr.append(ones/38) # 38 - двухдоменные последовательности
Далее визуализируем всё получившееся - получим ROC кривую. В нашем случае получился очень сильно почти идеальный классификатор (AUC = 0.9963). Возможно, это связано с резким падением score. Для идеальной классификации используется вес 95.9.
(график интерактивный, при наведении мышкой на линию сверху высвечивается score, а снизу значения параметров)
fig = px.area(x = fpr, y = tpr,
title = f'ROC кривая (AUC={auc(fpr, tpr):.4f})',
labels = dict(x = '1-spec', y = 'sens'),
width=700, height=500,
hover_name = merged.score
)
fig.add_shape(
type = 'line', line = dict(dash='dash'),
x0 = 0, x1 = 1, y0 = 0, y1 = 1
)
fig.update_yaxes(scaleanchor="x", scaleratio=1)
fig.update_xaxes(constrain='domain')
fig.update_layout(margin=dict(l=0, r=0, t=50, b=20))
fig.show()
Также был произведен расчет F1-score:
f1_score(merged.hmm, merged.architecture)
0.22028985507246376
Как и предполагалось выше, наблюдается действительно резкий скачок в в распределении весов.
fig = px.scatter(y = merged.score, hover_name = merged.seq)
fig.update_layout(
margin=dict(l=10, r=10, t=10, b=10),
showlegend = False,
yaxis_title = 'score',
xaxis_title = '',
hovermode = 'closest'
)
fig.show()
Также была построена гистограмма распредления длин белков, входящих в выборку full, а также маленький боксплотик сверху.
Видно, что белки образует два кластера. Собственно, те, которые подлиннее, состоят из двудоменной архитектуры (> 300 а.к.), а те, которые короче (их ~ 90% от full), состоят только из одного домена.
fig = px.histogram(x = len_full, marginal = 'box', histnorm = 'percent')
fig.update_layout(
margin=dict(l=10, r=10, t=10, b=10),
showlegend = False,
yaxis_title = '%',
xaxis_title = 'длина',
hovermode = 'closest',
height=500
)
fig.show()
Итого, получился идеальный профиль, для предсказания исследумой двухдоменной структуры.
!jupyter nbconvert --to html hmm.ipynb
[NbConvertApp] Converting notebook hmm.ipynb to html [NbConvertApp] Writing 626249 bytes to hmm.html