Lesson 3 of 13 · 11 min
Encoding Sequences: k-mers and One-Hot
Machine learning estimators consume numeric tensors, so DNA and protein sequences must be encoded before they reach a model. Two representations dominate: a bag of k-mers, which counts short overlapping subwords into a fixed-dimensional vector for inputs of any length while discarding where each k-mer occurs, and positional one-hot encoding, which preserves the exact residue at every coordinate of a fixed-length window. The right choice depends on whether the biological signal lives in composition or in position.
k-mer Frequency Vectors
from sklearn.feature_extraction.text import CountVectorizer
seqs = ["ACGTACGT", "ACGTTTGA", "TTTTACGT"]
k = 3
# analyzer='char' with ngram_range=(k, k) counts overlapping k-mers.
# lowercase=False keeps the vocabulary in canonical uppercase.
vec = CountVectorizer(analyzer="char", ngram_range=(k, k), lowercase=False)
X = vec.fit_transform(seqs)
print(vec.get_feature_names_out()[:6])
print(X.toarray())
print(X.shape)['ACG' 'CGT' 'GTA' 'GTT' 'TAC' 'TGA']
[[2 2 1 0 1 0 0 0 0]
[1 1 0 1 0 1 0 1 1]
[1 1 0 0 1 0 1 0 2]]
(3, 9)
Dimensionality warning: the k-mer feature space grows as A to the power k, where A is the alphabet size. For DNA (A=4) that is 64 columns at k=3, 4096 at k=6, and over a million at k=10; for protein (A=20) it is already 160000 at k=4. A single length-L sequence has at most L minus k plus 1 nonzero k-mers, so the vectors become extremely sparse as k grows. Keep k small, or switch to feature hashing with HashingVectorizer or a min_df plus VarianceThreshold filter to keep the matrix tractable.
Fixed Columns and Reverse Complements
from itertools import product
def revcomp(kmer):
return kmer.translate(str.maketrans("ACGT", "TGCA"))[::-1]
def kmer_vector(seq, k=3, canonical=False):
kmers = ["".join(p) for p in product("ACGT", repeat=k)]
if canonical: # collapse strand-equivalent k-mers
kmers = sorted({min(km, revcomp(km)) for km in kmers})
index = {km: i for i, km in enumerate(kmers)}
vec = [0] * len(kmers)
for i in range(len(seq) - k + 1):
km = seq[i:i + k]
if canonical:
km = min(km, revcomp(km))
if km in index: # windows with N or gaps are skipped
vec[index[km]] += 1
return kmers, vec
cols_full, v_full = kmer_vector("ACGTACGT", k=3)
cols_canon, v_canon = kmer_vector("ACGTACGT", k=3, canonical=True)
print(len(cols_full), len(cols_canon))
print(sum(v_full), sum(v_canon))
print("ACG and", revcomp("ACG"), "-> canonical", min("ACG", revcomp("ACG")))64 32
6 6
ACG and CGT -> canonical ACG
Enumerating the full alphabet with itertools.product gives a deterministic column order that is identical across train and test, unlike a corpus-learned CountVectorizer vocabulary. Canonicalization matters when the strand of origin is unknown, as in ChIP-seq or unstranded assays, because a k-mer and its reverse complement describe the same double-stranded event; collapsing them roughly halves the feature space, exactly what tools like Jellyfish do with the canonical flag. When position rather than composition carries the signal, switch to a positional one-hot matrix of shape L by A that a convolutional model can learn motifs from directly.
import numpy as np
def one_hot(seq, alphabet="ACGT"):
lut = {b: i for i, b in enumerate(alphabet)}
oh = np.zeros((len(seq), len(alphabet)), dtype=np.float32)
idx = np.array([lut.get(b, -1) for b in seq])
valid = idx >= 0
# N, gaps, and ambiguity codes map to -1 and stay all-zero rows
oh[np.arange(len(seq))[valid], idx[valid]] = 1.0
return oh
m = one_hot("ACGTN")
print(m.shape)
print(m)(5, 4)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]
[0. 0. 0. 0.]]
Try it yourself: Build a labeled set of 12 bp windows centered on true splice-donor sites (which carry an invariant GT at a fixed offset) and decoy windows. Encode each window two ways: a bag of 3-mers with kmer_vector, and a flattened positional one-hot of shape 12 by 4 giving a 48-length vector. Train a LogisticRegression on each encoding and compare ROC-AUC on a held-out split; the one-hot representation should win because the GT signal lives at a fixed position that the position-agnostic k-mer counts scramble away.