Spaces:
Sleeping
Sleeping
Claude
docs(sprint-H.8): cleanup obsolete legacy/shim language in production docstrings
e407ec0 unverified | """Score de spécialisation inter-moteurs — Sprint 89 (A.II.8b). | |
| Sprint 89 — A.II.8b du plan d'évolution 2026. | |
| Pourquoi ce module | |
| ------------------ | |
| La matrice de divergence taxonomique (Sprint 35 | |
| ``inter_engine.taxonomy_divergence_matrix``) répond à *« à quel | |
| point ces moteurs se trompent-ils différemment ? »*. Ce | |
| sprint la transforme en un **score de spécialisation** lisible | |
| et complète la lecture par : | |
| - une **classification** discrète (similar / distinct / | |
| highly_specialized) que le chercheur peut consommer sans | |
| avoir à interpréter une distance ; | |
| - un **top-N des paires** les plus spécialisées, qui répond | |
| directement à la question *« quels moteurs sont les meilleurs | |
| candidats pour un voting ensemble ? »*. | |
| Ce module **ne recommande pas** de pipeline d'ensemble — il | |
| fournit l'observation factuelle et laisse le chercheur arbitrer. | |
| Convention de score | |
| ------------------- | |
| On utilise la **Jensen-Shannon divergence** déjà calculée par | |
| ``inter_engine.jensen_shannon_divergence`` : elle est | |
| symétrique, bornée dans [0, 1], et son interprétation est | |
| intuitive : | |
| - ≈ 0 → profils taxonomiques identiques | |
| - 1 → distributions totalement disjointes | |
| Dépendances | |
| ----------- | |
| S'appuie strictement sur ``picarones.evaluation.metrics.inter_engine`` (Sprint | |
| 35) — pas de double calcul, pas de logique nouvelle de | |
| divergence. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Optional | |
| from picarones.evaluation.metrics.inter_engine import jensen_shannon_divergence | |
| logger = logging.getLogger(__name__) | |
| # Seuils par convention éditoriale. La roadmap ne fixe rien : | |
| # ces seuils sont des **guides de lecture**, pas des verdicts. | |
| # Le chercheur peut les surcharger via ``classify_specialization``. | |
| DEFAULT_THRESHOLDS = ( | |
| ("similar", 0.10), | |
| ("distinct", 0.30), | |
| ("highly_specialized", 1.01), # tout score ≥ 0.30 | |
| ) | |
| def compute_specialization_score( | |
| taxonomy_a: dict[str, float], | |
| taxonomy_b: dict[str, float], | |
| ) -> float: | |
| """Score de spécialisation entre deux moteurs ∈ [0, 1]. | |
| 0 = mêmes erreurs, 1 = erreurs totalement disjointes. | |
| Délègue à ``jensen_shannon_divergence`` (Sprint 35). | |
| """ | |
| return jensen_shannon_divergence(taxonomy_a, taxonomy_b) | |
| def classify_specialization( | |
| score: float, | |
| thresholds: Optional[tuple[tuple[str, float], ...]] = None, | |
| ) -> str: | |
| """Classe le score en catégorie discrète. | |
| Convention : | |
| - score < 0.10 → ``similar`` | |
| - 0.10 ≤ score < 0.30 → ``distinct`` | |
| - score ≥ 0.30 → ``highly_specialized`` | |
| L'utilisateur peut passer ses propres ``thresholds`` (liste | |
| triée par valeur croissante de tuples ``(label, max_score)``). | |
| """ | |
| rules = thresholds or DEFAULT_THRESHOLDS | |
| for label, max_score in rules: | |
| if score < max_score: | |
| return label | |
| # Garde-fou : si aucun seuil ne match, dernière catégorie | |
| return rules[-1][0] | |
| def compute_specialization_matrix( | |
| taxonomies: dict[str, dict[str, float]], | |
| ) -> Optional[dict]: | |
| """Matrice de spécialisation symétrique entre tous les moteurs. | |
| Parameters | |
| ---------- | |
| taxonomies: | |
| Map ``{engine_name: {error_class: count_or_proportion}}``. | |
| Returns | |
| ------- | |
| dict | None | |
| ``{ | |
| "engines": list[str], | |
| "matrix": list[list[float]], # carrée, symétrique | |
| "n_pairs": int, # paires distinctes | |
| "max_score": float, | |
| "max_pair": (str, str) | None, | |
| }`` ; ``None`` si moins de 2 moteurs. | |
| """ | |
| if not taxonomies or len(taxonomies) < 2: | |
| return None | |
| engines = sorted(taxonomies.keys()) | |
| n = len(engines) | |
| matrix = [[0.0] * n for _ in range(n)] | |
| n_pairs = 0 | |
| max_score = 0.0 | |
| max_pair: Optional[tuple[str, str]] = None | |
| for i in range(n): | |
| for j in range(i + 1, n): | |
| score = compute_specialization_score( | |
| taxonomies[engines[i]], taxonomies[engines[j]], | |
| ) | |
| matrix[i][j] = score | |
| matrix[j][i] = score | |
| n_pairs += 1 | |
| if score > max_score: | |
| max_score = score | |
| max_pair = (engines[i], engines[j]) | |
| return { | |
| "engines": engines, | |
| "matrix": matrix, | |
| "n_pairs": n_pairs, | |
| "max_score": max_score, | |
| "max_pair": max_pair, | |
| } | |
| def top_specialized_pairs( | |
| matrix_data: Optional[dict], | |
| n: int = 5, | |
| *, | |
| min_score: float = 0.0, | |
| ) -> list[dict]: | |
| """Top-N paires de moteurs triées par score décroissant. | |
| Returns | |
| ------- | |
| list[dict] | |
| Une liste de ``{ | |
| "engine_a": str, "engine_b": str, | |
| "score": float, "category": str, | |
| }`` triée par score décroissant. Liste vide si | |
| ``matrix_data`` est ``None`` ou que toutes les paires | |
| sont sous ``min_score``. | |
| """ | |
| if not matrix_data: | |
| return [] | |
| engines = matrix_data["engines"] | |
| matrix = matrix_data["matrix"] | |
| pairs: list[dict] = [] | |
| for i, engine_a in enumerate(engines): | |
| for j in range(i + 1, len(engines)): | |
| score = matrix[i][j] | |
| if score < min_score: | |
| continue | |
| pairs.append({ | |
| "engine_a": engine_a, | |
| "engine_b": engines[j], | |
| "score": score, | |
| "category": classify_specialization(score), | |
| }) | |
| pairs.sort(key=lambda p: -p["score"]) | |
| return pairs[:n] | |
| __all__ = [ | |
| "DEFAULT_THRESHOLDS", | |
| "compute_specialization_score", | |
| "classify_specialization", | |
| "compute_specialization_matrix", | |
| "top_specialized_pairs", | |
| ] | |