How to Find DNA Binding Motifs in the Genome | Towards Data Science

If I asked you to find me a needle in a haystack, you’d oblige only out of etiquette.
If I then told you there are actually 10 needles in the haystack and they all look slightly different, rage would consume you as punched my teeth out.
This is the mission biologists face when looking for regulatory motifs. These are short DNA segments in the genome that bind transcription factors (TFs). TFs are proteins that raise or lower the transcription levels (the expression) of other genes.
Like the needles, regulatory motifs are often similar to each other but not identical. The problem bioinformaticians face is finding the set of motifs that are most similar to each other across all the genetic data they have.
Once we do, we can understand the mechanisms behind important functions like photosynthesis or tuberculosis dormancy.
I’ve learned how to do that using Python you can learn on Codeacademy. This is how you can do it too.
Generate count and profile matrices for a matrix of DNA motifs.
Create a consensus motif to score the level of conservation between all motifs in our data.
Determine the probability of any possible motif occurring according our profile matrix.
Compile these functions into a greedy search algorithm to scan upstream regions of MTB genes for motifs.
This piece assumes you have a basic knowledge of genetics and Python.
Pretend we already have a matrix of motfis Motifs and we want to measure the similarity — level of conservation — between each one. (The most conserved nucleotide in each column is capitalized.)
Our job:
Compute the most highly conserved nucleotide in each column. (Count())
Generate a ‘consensus’ motif where each base is the most conserved nucleotide from each column in Motifs. (Consensus())
Score the similarity between each motif in Motifs and the consensus motif. (Score())
Create a dictionary count where keys are nucleotides ('A', 'C', 'T', ‘G’) and the values are lists. Each list element is number of times that key’s nucleotide occurs at that index (column) in Motifs :
We initialize an empty dictionary count and populate it with nucleotide keys. Each value is a list of 0s, as long as the first motif in Motifs .
Then we iterate through every element in Motifs ; when we encounter a nucleotide symbol at Motifs[i][j] we increment the value of that symbol’s key at the jth index.
Turn the count dictionary into a frequency dictionary with a Profile(Motifs) function:
Divide each list element in count by the number of motifs in Motifs . We get something like this:
This will come in handy later.
Now, we can call Count() as a subroutine. Take the nucleotide with the highest count at each index of the count dictionary and append to an empty string consensus :
In the outer loop, we iterate through every element in a list value from count. In the inner loop, we iterate through each nucleotide key. This lets us compare every key-value at the jth index. We update m to always be the highest value, and let frequentSymbol= the corresponding nucleotide symbol.
Consensus(Motifs)>>'TCGGGGATTTCC'
To score the level of conservation between motifs in Motifs, we can iterate through every element with index motif[j].
Outer loop ranges through rows, the inner loop ranges through every column.
If the nucleotide symbol at motif[j] doesn’t match consensus[j], we increment score. Higher score = less conservation.
We have a way of measuring conservation between motifs. Now, we need to generate many potential matrices Motifs and find the one that minimizes score.
We can do this with a greedy search algorithm; that means our algorithm will choose the best option available at every step of iteration. More here.
Greedy algorithms have three components:
DATA: It must iterate through a set of data.RULE: It follows a rule for what to do at each step of iteration.RESULT: It adds the result of that rule to the answer at every step.
DATA: It must iterate through a set of data.
RULE: It follows a rule for what to do at each step of iteration.
RESULT: It adds the result of that rule to the answer at every step.
Dna is a matrix of DNA strings that we’re scanning for motifs.
k is the length of the motifs.
t is the number of DNA strings in Dna .
Our algorithm’s components:
DATA: Iterate through all possible k-mers in the first string in Dna. (Outer loop).RULE: Find the best match for those k-mers from every other string in Dna. (Inner loop).RESULT: Append the results to a list Motifs .
DATA: Iterate through all possible k-mers in the first string in Dna. (Outer loop).
RULE: Find the best match for those k-mers from every other string in Dna. (Inner loop).
RESULT: Append the results to a list Motifs .
We can start by creating a tentative list BestMotifs where each motif is just the first k-mer from each DNA string in Dna. We’ll update this list if we create a matrix later on with a better score.
Let’s take the first k-mer from Dna[0] (again) append it to a new list Motifs. We’re going to do this for every possible k-mer in the first DNA string — this is our outer loop.
We’re going to construct the rest of Motifs based on this first k-mer we pull from Dna[0]. (This is step 2, the rule).
Using its profile from the Profile function we made earlier, we want to find the most similar k-mer in the next DNA string, Dna[1].
How? Well, we don’t have a function that does this yet, so let’s write one.
Let’s look at our profile matrix. Each element is the probability of that nucleotide occurring at that position in a motif.
If the profile matrix was randomly generating motifs, the consensus string would occur most frequently.
We can also determine the probability of any other string motif occurring as well. Simply multiply the chances of each base occurring in their respective positions:
Higher probability = better match according to Profile.
Arguments are a DNA string Text and a profile matrix Profile. Text[i] is the nucleotide at index i in Text, as well as a key for a list in Profile. Profile[Text[i]][i] is the probability value at the same index i.
We can now find the probability of any k-mer being a motif — given a probability matrix Profile .
Let’s iterate through all k-mers in a long string of DNA Text and find the most probable one (the best match) according to Profile:
We produce a dictionary containing k-mers and their probabilities, then return the first k-mer with the highest value. (Specifics inline).
Now, given a Profile, a length k for the motif, and a longer string Text to scan along for k-mers, we can return to GreedyMotifSearch.
We find the profile-most probable pattern in the next string of DNA (Dna[1]) and append it to Motifs. (This is step 3, the result). We update the profile P of this new matrix and repeat the process for each DNA string.
When we’ve completed the inner loop, we can compare the scores of Motifs and BestMotifs. If Motifs scores lower, we update BestMotifs; continue this process for every possible k-mer in Dna[0].
We’re not completely done yet. Our score function is imperfect. Notice how some columns have more variety than others, but they still score the same (the second and last column).
A better way of tracking the level of conservation in each column is by measuring entropy.
Each column in a profile matrix is a probability distribution — positive numbers that sum to 1.
Entropy is the uncertainty of this distribution, found by summing each probability times its binary log, and multiplying by -1:
Lower entropy = less uncertainty and greater conservation.
Iterate through every element in profile and update entropy if the probability value is over 0 (otherwise we can’t take its binary log). This kind of formula is used more often in practice, but it’s a footnote here for simplicity’s sake.
Let’s take this baby for a spin:
You may have TB without knowing it. Mycobacterium tuberculosis bacterium (MTB) can stay dormant in your body for years before it has any effect.
Biologists linked hypoxia (oxygen deficiency) to MTB latency, and found the transcription factor (DosR) that helps the bacterium survive low-oxygen conditions.
We’re going to look at the upstream regions for some of the genes DosR affects to try and find some motifs it might bind. (I’ve modified GreedyMotifSearch to use Entropy() instead of Score().)
The DNA strings we’re looking at can be found here, and I’ve collected them into a list called DosR .
Your results:
['ATGACCAGCGAGCTG', 'CCGATCGGCATCACT', 'ACCGTCGATGTGCCC', 'GGGTCAGGTATATTT', 'GTGACCGACGTCCCC', 'CCGCTGGCGACGCTG', 'GTGTCCGGCATGATC', 'CGGCCAGACAAGCTT', 'GCGATAGGTGAGATT', 'CTCATCGCTGTCATC'] 18.492751844642765 CCGACCGGCATGCTC
Congratulations, it worked! Kiss your spouse, high-five a dog, and take a cop car for a joyride.
Right?
If you look at the literature, these are not the motifs your are looking for.
The algorithms and data they use are better— what greedy algorithms have in speed, they lack in accuracy.
In practice, the algorithm we’ve developed today would never be used — much like the roller blades I bought mid-mental breakdown.
This may not be the be-all, end-all algorithm, but it lays the foundation to learn more complex algorithms in the future.
Complex problems can have complex solutions — but you can still build them using simple tools (like all the Python you can learn for free on Codeacademy).
Thanks for reading!
Other bioinformatics pieces I’ve written: