from __future__ import annotations from typing import Any, List, Mapping, Tuple from .features import ENDINGS_PLAIN, PARTICLES def explain_results(feature_dict: Mapping[str, Any], scores: Mapping[str, float]) -> str: """Generate a human-readable explanation of the classification.""" if not scores: return "No scores were produced." best_dialect = max(scores.items(), key=lambda kv: kv[1])[0] best_pct = float(scores[best_dialect]) token_count = int(feature_dict.get("token_count", 0) or 0) particles: Mapping[str, int] = feature_dict.get("particles", {}) or {} endings: Mapping[str, int] = feature_dict.get("endings", {}) or {} infinitives: Mapping[str, int] = feature_dict.get("infinitives", {}) or {} dative_plural: Mapping[str, int] = feature_dict.get("dative_plural_endings", {}) or {} epic_endings: Mapping[str, int] = feature_dict.get("epic_endings", {}) or {} epic_particles: Mapping[str, int] = feature_dict.get("epic_particles", {}) or {} epic_words: Mapping[str, int] = feature_dict.get("epic_words", {}) or {} prepositions: Mapping[str, int] = feature_dict.get("prepositions", {}) or {} koine_words: Mapping[str, int] = feature_dict.get("koine_words", {}) or {} lexical_cues: Mapping[str, int] = feature_dict.get("lexical_cues", {}) or {} doric_cues: Mapping[str, int] = feature_dict.get("doric_cues", {}) or {} poetic_morph: Mapping[str, int] = feature_dict.get("poetic_morph", {}) or {} patterns: Mapping[str, int] = feature_dict.get("patterns", {}) or {} orth: Mapping[str, int] = feature_dict.get("orthography", {}) or {} diagnostics = feature_dict.get("diagnostics", {}) or {} greek_ratio = diagnostics.get("greek_ratio", None) top_gap_pct = diagnostics.get("top_gap_pct", None) contrib = (feature_dict.get("_contributions", {}) or {}).get(best_dialect, {}) # type: ignore[assignment] top_contrib: List[Tuple[str, float]] = sorted(contrib.items(), key=lambda kv: abs(kv[1]), reverse=True)[:8] particle_bits = ", ".join(f"{p}={int(particles.get(p, 0) or 0)}" for p in PARTICLES) ending_bits = ", ".join(f"-{e}={int(endings.get(e, 0) or 0)}" for e in (*ENDINGS_PLAIN, "ᾳ")) orth_bits = ( f"alpha_endings={int(orth.get('alpha_endings', 0) or 0)}, " f"eta_endings={int(orth.get('eta_endings', 0) or 0)}" ) lines: List[str] = [] lines.append(f"Prediction: {best_dialect} (confidence {best_pct:.1f}%)") lines.append(f"Tokens analyzed: {token_count}") if isinstance(greek_ratio, (int, float)): lines.append(f"Greek-script ratio (letters): {float(greek_ratio):.2f}") if float(greek_ratio) < 0.30: lines.append("Warning: input contains little/no Greek; classification is low-evidence.") if token_count < 20: lines.append("Warning: very short passage; confidence may be unreliable.") if isinstance(top_gap_pct, (int, float)) and float(top_gap_pct) < 10.0: lines.append("Warning: scores are clustered; dialect signal is weak.") lines.append("") lines.append("Observed feature counts:") lines.append(f" Particles: {particle_bits}") lines.append(f" Endings: {ending_bits}") lines.append( " Infinitives: " + ", ".join( [ f"-ειν={int(infinitives.get('ειν', 0) or 0)}", f"-μεναι={int(infinitives.get('μεναι', 0) or 0)}", f"-μεν={int(infinitives.get('μεν', 0) or 0)}", ] ) ) lines.append( " Dative plural endings: " + ", ".join( f"-{e}={int(dative_plural.get(e, 0) or 0)}" for e in ("οισι", "ηισι", "αισι", "οις", "αις") ) ) lines.append( " Epic: " + ", ".join( [ f"-{e}={int(epic_endings.get(e, 0) or 0)}" for e in ("οιο", "εσσι", "φι", "ηοσ", "αδεω", "ιδεω") ] + [ f"{p}={int(epic_particles.get(p, 0) or 0)}" for p in ("κε", "κεν", "αρ", "μιν") ] + [ f"{w}={int(epic_words.get(w, 0) or 0)}" for w in ("εννεπε", "αειδε", "μουσα", "μηνιν", "θεα") ] ) ) lines.append( f" Patterns: ττ={int(patterns.get('tt', 0) or 0)}, σσ={int(patterns.get('ss', 0) or 0)}" ) lines.append( " Prepositions: " + ", ".join( [ f"εἰς={int(prepositions.get('εισ', 0) or 0)}", f"ἐς={int(prepositions.get('εσ', 0) or 0)}", ] ) ) lines.append( " Koine function words: " + ", ".join( [ f"ἵνα={int(koine_words.get('ινα', 0) or 0)}", f"ὅτι={int(koine_words.get('οτι', 0) or 0)}", f"καθώς={int(koine_words.get('καθωσ', 0) or 0)}", f"ἐγένετο={int(koine_words.get('εγενετο', 0) or 0)}", ] ) ) lines.append( " Lexicalized cues: " + ", ".join( [ f"TT-stems={int(lexical_cues.get('attic_tt', 0) or 0)}", f"SS-stems={int(lexical_cues.get('ionic_ss', 0) or 0)}", ] ) ) lines.append(f" Doric cue: ἁ-initial={int(doric_cues.get('ha_initial', 0) or 0)}") if poetic_morph: lines.append( " Poetic morph: " + ", ".join( [ f"-μες(1pl)={int(poetic_morph.get('verb_1pl_mes', 0) or 0)}", f"ἄμμι={int(poetic_morph.get('aeolic_ammi', 0) or 0)}", f"ὔμμι={int(poetic_morph.get('aeolic_ummi', 0) or 0)}", ] ) ) lines.append(f" Orthography: {orth_bits}") if top_contrib: lines.append("") lines.append(f"Top contributing rules for {best_dialect}:") for name, delta in top_contrib: lines.append(f" {name}: {delta:+.3f}") lines.append("") lines.append("Note: weights are MVP placeholders; edit dialect_analysis/scoring.py to refine rules.") return "\n".join(lines)