Bakterijska DNK je ciklična. Ipak, replikacija bakterijske DNK počinje od istog (istih regiona) - oriC regiona. Unutar oriC regiona nalaze se kratke DNK sekvence koje predstavljaju mesta za koja se protein DnaA vezuje i počinje proces replikacije. Te kratke sekvence, mesta vezivanja DnaA proteina, nazivaju DnaA boksovi. Evolucija je obezbedila višestruka ponavljanja DnaA boksova unutar oriC regiona, kako bi se sa većom verovatnoćom obezbedilo pronalaženje mesta i otpočela replikacija.
Problem pronalaženja DnaA boksova svodimo na problem pronalaženja višestruko ponovljenih kratkih sekvenci unutar teksta.
Naivni pristup pronalaženja višestruko ponovljenih sekvenci zasniva se na izdvajanju svih podniski dužine k (k-grami) i proveri koliko puta su izdvojene podniske pronađene u tekstu.
Prednosti: Implementacija je jednostavna, predstavlja intuitivno rešenje navedenog problema
Mane: Loša efikasnost algoritma, vremenska složenost - k · |Tekst| · (|Tekst| - k) = O(k · |Tekst|2)
Funkcija pattern_count prebrojava pojavljivanje niske pattern u okviru niske text
def pattern_count(text, pattern):
n = len(text)
k = len(pattern)
count = 0
for i in range(0, n - k + 1):
current_pattern = text[i:i + k]
if current_pattern == pattern:
count += 1
return count
pattern_count('XABABABX', 'AB')
Funkcija frequent_words pronalazi najzastupljenije nukleotidne podsekvence dužine k u zadatom tekstu
def frequent_words(text, k):
frequent_patterns = set([]) # Skup koji ce cuvati sve najzastupljenije sekvence, bez duplikata
n = len(text)
# Niz koja ce sluziti za brojanje pojavljivanja podniski.
# Indeksi su poceci podniski a vrednosti brojevi pojavljivanja podniske
counts = [1 for i in range(0, n - k + 1)]
# Izdvajanje svih podniski i njihovo prebrojavanje
for i in range(0, n - k + 1):
pattern = text[i:i + k]
counts[i] = pattern_count(text, pattern)
max_count = max(counts)
for i in range(0, n - k + 1):
if counts[i] == max_count:
frequent_patterns.add(text[i:i + k])
return list(frequent_patterns)
frequent_words('ATATGCTAGTGTCGATGTGCTA', 4)
dna = 'ATCAATGATCAACGTAAGCTTCTAAGCATGATCAAGGTGCTCACACAGTTTATCCACAACCTGAGTGGATGACATCAAGATAGGTCGTTGTATCTCCTTCCTCTCGTACTCTCATGACCACGGAAAGATGATCAAGAGAGGATGATTTCTTGGCCATATCGCAATGAATACTTGTGACTTGTGCTTCCAATTGACATCTTCAGCGCCATATTGCGCTGGCCAAGGTGACGGAGCGGGATTACGAAAGCATGATCATGGCTGTTGTTCTGTTTATCTTGTTTTGACTGAGACTTGTTAGGATAGACGGTTTTTCATCACTGACTAGCCAAAGCCTTACTCTGCCTGACATCGACCGTAAATTGATAATGAATTTACATGCTTCCGCGACGATTTACCTCTTGATCATCGATCCGATTGAAGATCTTCAATTGTTAATTCTCTTGCCTCGACTCATAGCCATGATGAGCTCTTGATCA'
k = 9
print(faster_frequent_words(dna, k))
Kako se DNK sastoji od 4 različitih tipova nukleotida (Adenin, Timin, Citozin, Guanin) to se i niska koja predstavlja DNK sastoji samo od karaktera 'A', 'T', 'C' i 'G', pa je broj različitih DNK sekvenci dužine k jednak 4k. Kako su, u konkretnom problemu pronalaženja početka replikacije, DnaA boksovi kratke sekvence (do 10-tak nukleotida) sledi da je k mali broj. Iz tog razloga, poboljšanje efikasnosti se može dobiti mapiranjem svih nukleotidnih sekvenci u broj 0 - 4k i čuvanjem njihovog broja pojavljivanja.
Prednosti: Bolja vremenska složenost - O(|Tekst|)
Mane: Lošija prostorna složenost - O(4k), za male vrednosti k ova složenost je podnošljiva
Ako se za karakterima koji označavaju nukleotide sekvence uvede preslikavanje koje svaki karakter preslikava u cifru od 0 do 3, sekvenca zapisana ovim karakterima se može tumačiti kao broj u brojčanom sistemu sa osnovom 4. Ukupan broj takvih brojeva koji se mogu zapisati sa k cifara je 4k.
Neka je preslikavanje definisano kao:
A -> 0
T -> 1
C -> 2
G -> 3
Sekvenca AAA (dužine 3) bi se preslikavala u broj 0 (000) dok bi se sekvenca GGG preslikavala u broj
3 · 4 2 + 3 · 4 1 + 3 · 4 0 = 4 3 - 1 = 4 k - 1 = 64
ATCG -> (0123)4 = 0 · 43 + 1 · 42 + 2 · 4 + 3 = 27
Funkcija symbol_to_number vrši preslikavanje simbola nukleotida u odgovarajuće numeričke vrednosti, cifre iz brojčanog sistema sa osnovom 4.
def symbol_to_number(symbol):
mapping = {
'A': 0,
'T': 1,
'C': 2,
'G': 3
}
return mapping[symbol]
symbol_to_number('C')
Funkcija pattern_to_symbol vrši preslikavanje cele nukleotidne sekvence u odgovarajući dekadni broj.
Prevođenje se vrši rekurzivno primenom Hornerove sheme:
pattern_to_number('ATCG') = pattern_to_number('ATC') * 4 + symbol_to_number('G')
def pattern_to_number(pattern):
if (len(pattern) == 1):
return symbol_to_number(pattern)
else:
last_symbol = pattern[-1]
return pattern_to_number(pattern[:-1]) * 4 + symbol_to_number(last_symbol)
pattern_to_number('GGG')
Slično, kao u slučaju prevođenja sekvence u broj, prevođenje broja u sekvencu vrši se prevođenjem vrednosti iz dekadnog u vrednost u sistemu sa osnovom 4. Ukoliko je dužina dobijene sekvence kraća od tražene sekvence dužine k, sekvenca se može dopuniti ciframa 0 (karakterom 'A') sa leve strane
ATC = AATC = AAATC
Funkcija number_to_symbol za unetu cifru iz sistema sa osnovom 4 vraća odgovarajući simbol nukleotida.
def number_to_symbol(number):
mapping = {
0: 'A',
1: 'T',
2: 'C',
3: 'G'
}
return mapping[number]
Funkcija number_to_pattern prevodi broj u nukleotidnu sekvencu dužine k.
Prevođenje se vrši rekurzivno:
number_to_pattern (27, 4) = number_to_pattern(27 // 4, k - 1) + number_to_symbol(27 % 4)
def number_to_pattern(number, k):
if k == 1:
return number_to_symbol(number)
else:
prefix = number // 4
remainder = number % 4
return number_to_pattern(prefix, k - 1) + number_to_symbol(remainder)
number_to_pattern(27, 4)
Funkcija computing_frequencies formira niz brojaca pojava podniski duzine k u tekstu. Podniske se prevode u numeričku formu i pri svakoj pojavi se odgovarajući brojač uvećava za 1.
def computing_frequencies(text, k):
frequency_array = [0 for x in range(4**k)]
for i in range(0, len(text) - k + 1):
pattern = text[i:i + k]
j = pattern_to_number(pattern)
frequency_array[j] += 1
return frequency_array
Funkcija faster_frequent_words pronalazi najzastupljenije nukleotidne podsekvence u zadatom tekstu korišćenjem optimizacije preslikavanjem podsekvenci dužine k u numeričke vrednosti.
def faster_frequent_words(text, k):
frequent_patterns = set([])
frequency_array = computing_frequencies(text, k) # Prebrojavanje podsekvenci
max_count = max(frequency_array)
for i in range(0, 4**k):
if frequency_array[i] == max_count:
frequent_patterns.add(number_to_pattern(i, k))
return list(frequent_patterns)
faster_frequent_words('ATATGCTAGTGTCGATGTGCTA', 4)
Navedeni kod može imati složenost O(|Tekst|) samo u slučaju kada je tekst dužine 4k ili više, zbog petlje koja prolazi 4k zato je potrebno dodatno optimizovati kod. Pravo rešenje vremenske složenosti O(|Tekst|) dobija se korišćenjem sufiksnog stabla.
dna = 'ATCAATGATCAACGTAAGCTTCTAAGCATGATCAAGGTGCTCACACAGTTTATCCACAACCTGAGTGGATGACATCAAGATAGGTCGTTGTATCTCCTTCCTCTCGTACTCTCATGACCACGGAAAGATGATCAAGAGAGGATGATTTCTTGGCCATATCGCAATGAATACTTGTGACTTGTGCTTCCAATTGACATCTTCAGCGCCATATTGCGCTGGCCAAGGTGACGGAGCGGGATTACGAAAGCATGATCATGGCTGTTGTTCTGTTTATCTTGTTTTGACTGAGACTTGTTAGGATAGACGGTTTTTCATCACTGACTAGCCAAAGCCTTACTCTGCCTGACATCGACCGTAAATTGATAATGAATTTACATGCTTCCGCGACGATTTACCTCTTGATCATCGATCCGATTGAAGATCTTCAATTGTTAATTCTCTTGCCTCGACTCATAGCCATGATGAGCTCTTGATCA'
k = 9
print(faster_frequent_words(dna, k))
Kako DnaA boksovi mogu sadržati pojedine mutacije, pronalaženje najzastupljenijih podeskvenci mora uzeti u obzir i eventualna dozvoljena odstupanja između sadržaja ponovljenih podsekvenci.
Hamingovo rastojanje definise se kao broj pozicija na kojima se dve niske razlikuju.
def hamming_distance(string1, string2):
n = len(string1)
m = len(string2)
distance = 0
for i in range(min(n, m)):
if string1[i] != string2[i]:
distance += 1
return distance + max(n, m) - min(n, m)
hamming_distance('ATC','AGG')
Funkcija neighbors generiše listu niski koje su na najviše d Hamingovom rastojanju od zadate niske.
def neighbors(pattern, d):
if d == 0:
return {pattern}
if len(pattern) == 1:
return {'A', 'C', 'G', 'T'}
neighborhood = set([])
sufix_neighbors = neighbors(pattern[1:], d)
for text in sufix_neighbors:
if hamming_distance(pattern[1:], text) < d:
for x in ['A', 'C', 'G', 'T']:
neighborhood.add(x + text)
else:
neighborhood.add(pattern[0] + text)
return list(neighborhood)
neighbors('AAAA',1)
Funkcija immediate_neighbors generiše listu niski koje su na Hamingovom rastojanju 1 od zadate niske.
def immediate_neighbors(pattern):
neighborhood = {pattern}
pattern_list = list(pattern) # Konverzija zbog imutabilnosti niski
for i in range(len(pattern)):
for nucleotide in ['A', 'T', 'C', 'G']:
if nucleotide != pattern[i]:
neighbor = pattern_list[:]
neighbor[i] = nucleotide
neighborhood.add(''.join(neighbor))
return list(neighborhood)
immediate_neighbors('AAA')
Funkcija iterative_neighbors predstavlja iterativnu implementaciju funkcije neighbors.
def iterative_neighbors(pattern, d):
neighborhood = {pattern}
for j in range(d):
new_neighbors = []
for current_pattern in neighborhood:
new_neighbors += immediate_neighbors(current_pattern)
for neighbor in new_neighbors:
neighborhood.add(neighbor)
return list(neighborhood)
iterative_neighbors('AAAA', 1)
Funkcija approximate_pattern_count prebrojava pojavljivanje podniski u tekstu koje su na d Hamingovom rastojanju od zadate niske.
def approximate_pattern_count(text, pattern, d):
k = len(pattern)
count = 0
for i in range(len(text) - k + 1):
if hamming_distance(pattern, text[i:i + k]) <= d:
count += 1
return count
Funkcija frequent_words_with_missmatches pronalazi listu svih najzastupljenijih podniski dužine k u zadatom tekstu, dopuštajući pri poređenju niski d Hamingovo rastojanje. Drugim rečima, funkcija pronalazi najzastupljenije podsekvence dužine k sa najviše d dozvoljenih mutacija.
def frequent_words_with_missmatches(text, k, d):
frequent_patterns = set([])
close = [0 for i in range(4**k)]
frequency_array = [0 for i in range(4**k)]
for i in range(len(text) - k + 1):
neighborhood = neighbors(text[i:i + k], d)
for pattern in neighborhood:
index = pattern_to_number(pattern)
close[index] = 1
for i in range(4**k):
if close[i]:
pattern = number_to_pattern(i, k)
frequency_array[i] = approximate_pattern_count(text, pattern, d)
max_count = max(frequency_array)
for i in range(4**k):
if frequency_array[i] == max_count:
pattern = number_to_pattern(i, k)
frequent_patterns.add(pattern)
return list(frequent_patterns)
frequent_words_with_missmatches('ATATGCTAGTGTCGATGTGCTA', 4, 2)
def calculate_skew(dna_sequence):
skew = [0 for c in dna_sequence]
last = 0
for i in range(0,len(dna_sequence)):
if dna_sequence[i] == 'G':
skew[i] = last + 1
elif dna_sequence[i] == 'C':
skew[i] = last - 1
else:
skew[i] = last
last = skew[i]
return skew
print(calculate_skew('ACTAGTAGTGCACTAGCTAGCTA'))
import matplotlib.pyplot as plt
def draw_skew(dna_sequence):
skew = calculate_skew(dna_sequence)
n = len(skew)
positions = [i for i in range(n)]
plt.plot(positions, skew)
draw_skew('ACTAGTAGTGCACTAGCTAGCTA')
input_file = open('ecoli.txt')
dna_sequence = input_file.read()
draw_skew(dna_sequence)