Repository: tacitvenom/genomics_algo

Issue:
Frequent Words with Mismatches
Find the most frequent k-mers with mismatches in a string.

Input: A string Text as well as integers k and d. (You may assume k ≤ 12 and d ≤ 3.)
Output: All most frequent k-mers with up to d mismatches in Text.



Comments:

Patch:
diff --git a/genomics_algo/miscellaneous_algorithms/misc_algos.py b/genomics_algo/miscellaneous_algorithms/misc_algos.py
index 0b937b1..9703fcc 100644
--- a/genomics_algo/miscellaneous_algorithms/misc_algos.py
+++ b/genomics_algo/miscellaneous_algorithms/misc_algos.py
@@ -1,8 +1,13 @@
+from itertools import product
 import numpy as np
 
-from typing import List, Tuple
+from typing import List, Set, Tuple
 
-from genomics_algo.utilities.misc_utilities import get_frequency_map
+from genomics_algo.utilities.misc_utilities import (
+    get_frequency_map,
+    validate_bases_in_genome,
+)
+from genomics_algo.utilities.string_cmp import find_hamming_distance
 
 
 def find_most_freq_k_substring(
@@ -56,3 +61,57 @@ def find_minimum_gc_skew_location(genome: str) -> int:
         gc_skew[index + 1] += genome[index] == "G"
         gc_skew[index + 1] -= genome[index] == "C"
     return np.where(gc_skew == gc_skew.min())[0] - 1
+
+
+def find_frequent_kmers_with_mismatches(genome: str, k: int, d: int) -> Set[str]:
+    """Determine most frequent k-mers with at most `d` mismatches.
+    A most frequent k-mer with up to `d` mismatches in `genome` is simply a string pattern maximising
+    the total number of occurrences of said pattern in `genome` with at most `d` mismatches.
+    Note that the pattern does not need to actually appear as a substring of `genome`.
+    >>> find_frequent_kmers_with_mismatches('ACGTTGCATGTCGCATGATGCATGAGAGCT', 4, 1)-{'ATGC', 'GATG', 'ATGT'}
+    set()
+
+    Parameters
+    ----------
+    genome : str
+        String representation of genome.
+    k: int
+        Length of kmers to find.
+    d: int
+        Number of allowed mismatches in kmers.
+
+    Returns
+    -------
+    Set[str]
+        Set of most frequent kmers with up to d mismatches
+    """
+
+    n = len(genome)
+    chars = {"A", "C", "G", "T"}
+    # input validation:
+    validate_bases_in_genome(genome)
+    if n < k or k < d or d < 0:
+        raise ValueError(
+            f"The input values for genome, k and d don't make sense. It must hold: len(genome)>=k, k>=d, d>=0. Received: len(genome)={n}, k={k}, d={d}."
+        )
+    if k > 12 or d > 3:
+        raise Warning(
+            f"The large input values k={k} and/or d={d} might cause long run times."
+        )
+
+    frequency_map = {}
+    # FIXME here ALL possible patterns of length k are created -> should be optimised
+    possible_patterns = ["".join(p) for p in product(chars, repeat=k)]
+    for i in range(n - k + 1):
+        pattern = genome[i : i + k]
+        for kmer in possible_patterns:
+            if find_hamming_distance(pattern, kmer) <= d:
+                if kmer in frequency_map.keys():
+                    frequency_map[kmer] += 1
+                else:
+                    frequency_map[kmer] = 1
+
+    most_frequent = max(frequency_map.values())
+    return {
+        kmer for kmer, frequency in frequency_map.items() if frequency == most_frequent
+    }
diff --git a/genomics_algo/utilities/misc_utilities.py b/genomics_algo/utilities/misc_utilities.py
index 4fa1603..c737773 100644
--- a/genomics_algo/utilities/misc_utilities.py
+++ b/genomics_algo/utilities/misc_utilities.py
@@ -70,3 +70,11 @@ def get_frequency_map(text: str, substring_length: int) -> Dict[str, int]:
         else:
             freq_map[substr] = 1
     return freq_map
+
+
+def validate_bases_in_genome(genome: str) -> bool:
+    """Validates a genome string for existing bases.
+    Raises ``ValueError`` if ``genome`` contains bases other than defined in ``Bases`` class."""
+    set_diff = set(genome).difference({Bases.A, Bases.C, Bases.G, Bases.T})
+    if not set_diff == set():
+        raise ValueError(f"Genome contains invalid bases: {set_diff}")


Test patch:
diff --git a/genomics_algo/tests/miscellaneous_algorithms/test_misc_algos.py b/genomics_algo/tests/miscellaneous_algorithms/test_misc_algos.py
index 85c96b7..f1f14a8 100644
--- a/genomics_algo/tests/miscellaneous_algorithms/test_misc_algos.py
+++ b/genomics_algo/tests/miscellaneous_algorithms/test_misc_algos.py
@@ -5,6 +5,7 @@ from genomics_algo.utilities.read_files import read_genome
 from genomics_algo.miscellaneous_algorithms.misc_algos import (
     find_pattern_clumps,
     find_minimum_gc_skew_location,
+    find_frequent_kmers_with_mismatches,
 )
 
 
@@ -53,3 +54,100 @@ def test_find_minimum_gc_skew_location_in_genome():
     genome = read_genome("genomics_algo/tests/test_data/e_coli.txt")
     result = find_minimum_gc_skew_location(genome)
     np.testing.assert_array_equal([3923619, 3923620, 3923621, 3923622], result)
+
+
+def test_find_frequent_kmers_with_mismatches_raises():
+    with pytest.raises(Warning):
+        find_frequent_kmers_with_mismatches("ACGTTGCAACGTTGCA", 13, 3)
+    with pytest.raises(Warning):
+        find_frequent_kmers_with_mismatches("ACGTTGCAACGTTGCA", 12, 4)
+    with pytest.raises(ValueError) as e:
+        find_frequent_kmers_with_mismatches("ACGT", 6, -1)
+        assert "Received: len(genome)=4, k=6, d=-1." in str(e.value)
+
+
+@pytest.mark.skip(reason="Takes around 2 minutes to execute")
+def test_find_frequent_kmers_with_mismatches_benchmark():
+    res = find_frequent_kmers_with_mismatches("ACGTTGCAACGTTGCA", 12, 3)
+    assert len(res) == 32855
+
+
+def test_find_frequent_kmers_with_mismatches():
+    """Some debug datasets taken from:
+    http://bioinformaticsalgorithms.com/data/debugdatasets/replication/FrequentWordsWithMismatchesProblem.pdf
+    """
+
+    """ Dataset 1
+    This dataset checks that the implementation includes k-mers that do not actually appear in Text.
+    Notice here that, although none of the output k-mers except for AA actually appear in Text, they
+    are all valid because they appear in Text with up to 1 mismatch (i.e. 0 or 1 mismatch).
+    """
+    genome1 = "AAAAAAAAAA"
+    k1 = 2
+    d1 = 1
+    expected1 = {"AA", "AC", "AG", "CA", "AT", "GA", "TA"}
+    result1 = find_frequent_kmers_with_mismatches(genome1, k1, d1)
+    assert result1 == expected1
+
+    """ Dataset 2
+    This dataset makes sure that the implementation is not accidentally swapping k and d.
+    """
+    genome2 = "AGTCAGTC"
+    k2 = 4
+    d2 = 2
+    expected2 = {
+        "TCTC",
+        "CGGC",
+        "AAGC",
+        "TGTG",
+        "GGCC",
+        "AGGT",
+        "ATCC",
+        "ACTG",
+        "ACAC",
+        "AGAG",
+        "ATTA",
+        "TGAC",
+        "AATT",
+        "CGTT",
+        "GTTC",
+        "GGTA",
+        "AGCA",
+        "CATC",
+    }
+    result2 = find_frequent_kmers_with_mismatches(genome2, k2, d2)
+    assert result2 == expected2
+
+    """ Dataset 3
+    This dataset makes sure you are not finding patterns in the Reverse Complement of genome
+    """
+    genome3 = "AATTAATTGGTAGGTAGGTA"
+    k3 = 4
+    d3 = 0
+    expected3 = {"GGTA"}
+    result3 = find_frequent_kmers_with_mismatches(genome3, k3, d3)
+    assert result3 == expected3
+
+    """ Dataset 4
+    This dataset first checks that k-mers with exactly d mismatches are being found. Then, it
+    checks that k-mers with less than d mismatches are being allowed (i.e. you are not only allowing
+    k-mers with exactly d mismatches). Next, it checks that you are not returning too few k-mers.
+    Last, it checks that you are not returning too many k-mers.
+    """
+    genome4 = "ATA"
+    k4 = 3
+    d4 = 1
+    expected4 = {"GTA", "ACA", "AAA", "ATC", "ATA", "AGA", "ATT", "CTA", "TTA", "ATG"}
+    result4 = find_frequent_kmers_with_mismatches(genome4, k4, d4)
+    assert result4 == expected4
+
+    """ Dataset 5
+    This dataset checks that your code is not looking for k-mers in the Reverse Complement
+    of genome.
+    """
+    genome5 = "AAT"
+    k5 = 3
+    d5 = 0
+    expected5 = {"AAT"}
+    result5 = find_frequent_kmers_with_mismatches(genome5, k5, d5)
+    assert result5 == expected5