Spaces:
Sleeping
Sleeping
File size: 9,266 Bytes
93c1e20 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | #!/usr/bin/env python3
import io, os, re, math, zipfile
from typing import Dict, List, Tuple, Set, Optional
import pandas as pd
from Bio import SeqIO
from statsmodels.stats.multitest import multipletests
from scipy.stats import fisher_exact
import matplotlib.pyplot as plt
FA_EXT = (".fasta", ".fa", ".txt", ".fas", ".fna")
def _read_fasta_bytes(name: str, data: bytes) -> List[Tuple[str, str, str]]:
recs = []
with io.BytesIO(data) as bio:
for rec in SeqIO.parse(io.TextIOWrapper(bio, encoding="utf-8"), "fasta"):
header = str(rec.id)
seq = str(rec.seq).upper().replace("\n", "").replace("\r", "")
recs.append((name, header, seq))
return recs
def read_uploaded_fasta_or_zip(uploaded_file) -> List[Tuple[str, str, str]]:
if uploaded_file is None:
return []
name = uploaded_file.name
data = uploaded_file.read()
if name.lower().endswith(".zip"):
results = []
with zipfile.ZipFile(io.BytesIO(data)) as z:
for zi in z.infolist():
if zi.is_dir(): continue
if not any(zi.filename.lower().endswith(ext) for ext in FA_EXT):
continue
file_bytes = z.read(zi.filename)
results.extend(_read_fasta_bytes(os.path.basename(zi.filename), file_bytes))
return results
else:
return _read_fasta_bytes(os.path.basename(name), data)
def clean_protein(seq: str) -> str:
return re.sub(r"[^ACDEFGHIKLMNPQRSTVWY]", "", seq.upper())
def clean_dna(seq: str) -> str:
return re.sub(r"[^ACGTUN]", "", seq.upper())
def reverse_complement_dna(seq: str) -> str:
"""Return reverse complement of a cleaned DNA/RNA-like sequence.
Supports A, C, G, T, U, and N. U is treated as T on the
input strand and complements to A.
"""
seq = clean_dna(seq).replace("U", "T")
table = str.maketrans({"A": "T", "C": "G", "G": "C", "T": "A", "N": "N"})
return seq.translate(table)[::-1]
def get_kmers_noN(sequence: str, k: int) -> List[str]:
s = sequence
out = []
L = len(s)
for i in range(L - k + 1):
kmer = s[i:i+k]
if "N" not in kmer:
out.append(kmer)
return out
def parse_k_input(k_input: str, default_single: int) -> List[int]:
k_input = (k_input or "").strip()
if not k_input:
return [default_single]
if "-" in k_input:
a, b = k_input.split("-", 1)
a = int(a.strip()); b = int(b.strip())
if a > b: a, b = b, a
return list(range(a, b+1))
if "," in k_input:
return [int(x.strip()) for x in k_input.split(",") if x.strip()]
return [int(k_input)]
def derive_serotype_names_from_sources(known_records: List[Tuple[str, str, str]]) -> Dict[str, str]:
counts: Dict[str, int] = {}
for src, header, _ in known_records:
counts[src] = counts.get(src, 0) + 1
name_map: Dict[str, str] = {}
for src, header, _ in known_records:
if counts.get(src, 0) == 1:
sero = os.path.splitext(os.path.basename(src))[0]
else:
sero = header.split()[0]
name_map[header] = sero
return name_map
def compute_unique_kmers_per_serotype(serotype_to_seq: Dict[str, str], is_protein: bool, k_values: List[int]) -> Dict[str, Dict[int, Set[str]]]:
all_sets: Dict[str, Dict[int, Set[str]]] = {g: {} for g in serotype_to_seq}
for g, seq in serotype_to_seq.items():
seq = clean_protein(seq) if is_protein else clean_dna(seq)
for k in k_values:
all_sets[g][k] = set(get_kmers_noN(seq, k))
unique: Dict[str, Dict[int, Set[str]]] = {g: {k: set() for k in k_values} for g in serotype_to_seq}
for k in k_values:
union_all = set().union(*(all_sets[g][k] for g in all_sets))
for g in all_sets:
others_union = union_all - all_sets[g][k]
unique[g][k] = all_sets[g][k] - others_union
return unique
def _score_sequence_against_uniques(seq2: str, unique_kmers: Dict[str, Dict[int, Set[str]]], k_values: List[int], vocab_by_sero: Dict[str, int], fdr_alpha: float) -> Dict:
"""Classify one already-cleaned sequence against the unique k-mer database."""
unk_kmers: Dict[int, Set[str]] = {}
for k in k_values:
unk_kmers[k] = set(get_kmers_noN(seq2, k))
match_counts: Dict[str, int] = {}
total_matches = 0
for g in unique_kmers:
mg = 0
for k in k_values:
mg += len(unique_kmers[g][k].intersection(unk_kmers[k]))
match_counts[g] = mg
total_matches += mg
if total_matches == 0:
predicted = "NoMatch"
conf_present = 0.0
conf_vocab = 0.0
else:
predicted = max(match_counts, key=match_counts.get)
conf_present = match_counts[predicted] / total_matches
conf_vocab = match_counts[predicted] / max(1, vocab_by_sero[predicted])
fisher_p = {}
if total_matches > 0:
sum_vocab_all = sum(vocab_by_sero.values())
for g in unique_kmers:
a = match_counts[g]
b = vocab_by_sero[g] - a
c = total_matches - a
d = (sum_vocab_all - vocab_by_sero[g]) - c
a = max(0, a); b = max(0, b); c = max(0, c); d = max(0, d)
_, p = fisher_exact([[a, b], [c, d]], alternative="greater")
fisher_p[g] = p
groups = list(unique_kmers.keys())
pvals = [fisher_p[g] for g in groups]
_, qvals, _, _ = multipletests(pvals, alpha=fdr_alpha, method="fdr_bh")
fdr_map = {g: q for g, q in zip(groups, qvals)}
else:
fisher_p = {g: 1.0 for g in unique_kmers}
fdr_map = {g: 1.0 for g in unique_kmers}
best_fdr = fdr_map.get(predicted, 1.0) if predicted != "NoMatch" else 1.0
best_group_matches = match_counts.get(predicted, 0) if predicted != "NoMatch" else 0
return {
"Predicted_serotype": predicted,
"Matches_total": total_matches,
"Confidence_by_present": conf_present,
"Confidence_by_serotype_vocab": conf_vocab,
"match_counts": match_counts,
"fisher_p": fisher_p,
"fdr_map": fdr_map,
"best_fdr": best_fdr,
"best_group_matches": best_group_matches,
}
def _is_better_orientation(candidate: Dict, current: Optional[Dict]) -> bool:
"""Choose the orientation with the strongest evidence."""
if current is None:
return True
# Prefer more total unique k-mer matches, then more matches for the predicted group,
# then higher confidence, then lower FDR.
cand_key = (
candidate["Matches_total"],
candidate["best_group_matches"],
candidate["Confidence_by_present"],
-candidate["best_fdr"],
)
curr_key = (
current["Matches_total"],
current["best_group_matches"],
current["Confidence_by_present"],
-current["best_fdr"],
)
return cand_key > curr_key
def classify_unknown_sequences(unknown_records: List[Tuple[str, str, str]], unique_kmers: Dict[str, Dict[int, Set[str]]], is_protein: bool, fdr_alpha: float = 0.05) -> pd.DataFrame:
vocab_by_sero: Dict[str, int] = {}
k_values = sorted({k for g in unique_kmers for k in unique_kmers[g]})
for g in unique_kmers:
vocab_by_sero[g] = sum(len(unique_kmers[g][k]) for k in k_values)
results = []
for src, header, seq in unknown_records:
if is_protein:
seq2 = clean_protein(seq)
best = _score_sequence_against_uniques(seq2, unique_kmers, k_values, vocab_by_sero, fdr_alpha)
orientation_used = "input"
else:
forward_seq = clean_dna(seq).replace("U", "T")
reverse_seq = reverse_complement_dna(forward_seq)
forward = _score_sequence_against_uniques(forward_seq, unique_kmers, k_values, vocab_by_sero, fdr_alpha)
forward["Orientation_used"] = "forward"
reverse = _score_sequence_against_uniques(reverse_seq, unique_kmers, k_values, vocab_by_sero, fdr_alpha)
reverse["Orientation_used"] = "reverse_complement"
best = reverse if _is_better_orientation(reverse, forward) else forward
orientation_used = best["Orientation_used"]
row = {
"Source": src,
"Sequence": header,
"Orientation_used": orientation_used,
"Predicted_serotype": best["Predicted_serotype"],
"Matches_total": best["Matches_total"],
"Confidence_by_present": best["Confidence_by_present"],
"Confidence_by_serotype_vocab": best["Confidence_by_serotype_vocab"],
}
for g in unique_kmers:
row[f"Matches_{g}"] = best["match_counts"][g]
row[f"FisherP_{g}"] = best["fisher_p"][g]
row[f"FDR_{g}"] = best["fdr_map"][g]
results.append(row)
return pd.DataFrame(results)
def plot_counts_by_serotype(simple_df: pd.DataFrame):
fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(111)
counts = simple_df["Predicted_serotype"].value_counts()
ax.bar(counts.index.astype(str), counts.values)
ax.set_xlabel("Predicted serotype")
ax.set_ylabel("Number of sequences")
ax.set_title("Predicted serotype counts")
fig.tight_layout()
return fig
|