from __future__ import annotations from collections import Counter, defaultdict from math import log from typing import Dict, Iterable, List, Mapping, Sequence, Tuple Token = str Pair = Tuple[Token, Token] def get_pair_counts(sequences: Mapping[Tuple[Token, ...], int]) -> Counter[Pair]: """Count adjacent token pairs across weighted token sequences.""" counts: Counter[Pair] = Counter() for tokens, freq in sequences.items(): if len(tokens) < 2: continue for i in range(len(tokens) - 1): counts[(tokens[i], tokens[i + 1])] += freq return counts def get_next_token_distributions( sequences: Mapping[Tuple[Token, ...], int], ) -> Dict[Pair, Counter[Token]]: """For each pair (a,b), count which token c follows it in observed sequences.""" follow: Dict[Pair, Counter[Token]] = defaultdict(Counter) for tokens, freq in sequences.items(): for i in range(len(tokens) - 2): pair = (tokens[i], tokens[i + 1]) follow[pair][tokens[i + 2]] += freq return follow def normalize(counter: Mapping[Token, int]) -> Dict[Token, float]: total = float(sum(counter.values())) if total == 0: return {} return {k: v / total for k, v in counter.items()} def shannon_entropy_from_probs(probabilities: Iterable[float]) -> float: entropy = 0.0 for p in probabilities: if p > 0: entropy -= p * log(p) return entropy def distribution_entropy(counter: Mapping[Token, int]) -> float: return shannon_entropy_from_probs(normalize(counter).values())