Spaces:
Running
Running
| """Coût marginal par erreur évitée — Sprint 91 (A.II.6 chantier 2). | |
| Sprint 91 — A.II.6 chantier 2 du plan d'évolution 2026. | |
| Pourquoi ce module | |
| ------------------ | |
| La vue Pareto (Sprint 20) trace CER vs coût mais n'arbitre pas | |
| quel surcoût est *raisonnable* pour quelle réduction d'erreur. | |
| Une institution avec un budget contraint a besoin d'une | |
| réponse opérationnelle : | |
| *« Passer de Tesseract à Mistral OCR coûte 0,83 € par | |
| erreur évitée — décider selon votre budget par millier | |
| d'erreurs corrigées. »* | |
| Formule | |
| ------- | |
| Pour deux moteurs A et B où B fait **moins** d'erreurs que A | |
| (donc B est plus précis) : | |
| .. code:: | |
| coût_marginal = (coût_B − coût_A) / (errors_A − errors_B) | |
| - Si ``cost_B > cost_A`` et ``errors_B < errors_A`` : | |
| ``cost_per_avoided_error > 0`` (cas standard, B coûte plus | |
| pour moins d'erreurs). | |
| - Si ``cost_B ≤ cost_A`` et ``errors_B < errors_A`` : | |
| ``cost_per_avoided_error ≤ 0`` (cas idéal, B est strictement | |
| meilleur). | |
| - Si ``errors_B ≥ errors_A`` : non comparable dans ce sens | |
| (B n'évite pas d'erreur), retourne ``None``. | |
| Sortie | |
| ------ | |
| ``compute_marginal_cost(cost_a, errors_a, cost_b, errors_b)`` | |
| retourne ``{cost_per_avoided_error, n_errors_avoided, | |
| cost_delta, dominated}`` ou ``None`` si non comparable. | |
| ``compute_marginal_cost_matrix(per_engine)`` retourne, pour | |
| chaque paire ordonnée ``(A → B)`` où B est plus précis, le | |
| coût marginal correspondant. Trié par coût marginal croissant | |
| (meilleur ratio en tête). | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| def compute_marginal_cost( | |
| cost_a: float, | |
| errors_a: float, | |
| cost_b: float, | |
| errors_b: float, | |
| ) -> Optional[dict]: | |
| """Coût marginal du passage A → B (B plus précis). | |
| Retourne ``None`` si : | |
| - ``errors_b >= errors_a`` (B n'évite pas d'erreur) ; | |
| - les valeurs ne sont pas finies. | |
| """ | |
| try: | |
| ca = float(cost_a) | |
| cb = float(cost_b) | |
| ea = float(errors_a) | |
| eb = float(errors_b) | |
| except (TypeError, ValueError): | |
| return None | |
| if ea <= eb: | |
| # B ne fait pas mieux que A → pas de gain à mesurer. | |
| return None | |
| n_avoided = ea - eb | |
| cost_delta = cb - ca | |
| cost_per_avoided = cost_delta / n_avoided | |
| dominated = cost_delta <= 0 # B aussi cher ou moins → cas idéal | |
| return { | |
| "cost_per_avoided_error": cost_per_avoided, | |
| "n_errors_avoided": n_avoided, | |
| "cost_delta": cost_delta, | |
| "dominated": dominated, | |
| } | |
| def compute_marginal_cost_matrix( | |
| per_engine: dict[str, dict], | |
| ) -> Optional[dict]: | |
| """Pour chaque paire A → B où B fait moins d'erreurs, calcule | |
| le coût marginal. | |
| Parameters | |
| ---------- | |
| per_engine: | |
| Map ``{engine_name: {"cost": float, "errors": float}}``. | |
| Returns | |
| ------- | |
| dict | None | |
| ``{ | |
| "pairs": list[ | |
| {"engine_a", "engine_b", "cost_per_avoided_error", | |
| "n_errors_avoided", "cost_delta", "dominated"} | |
| ], # triée par cost_per_avoided_error croissant | |
| }`` | |
| ou ``None`` si moins de 2 moteurs. | |
| """ | |
| if not per_engine or len(per_engine) < 2: | |
| return None | |
| engines = sorted(per_engine.keys()) | |
| pairs: list[dict] = [] | |
| for a in engines: | |
| for b in engines: | |
| if a == b: | |
| continue | |
| data_a = per_engine[a] | |
| data_b = per_engine[b] | |
| try: | |
| ca = float(data_a.get("cost")) | |
| ea = float(data_a.get("errors")) | |
| cb = float(data_b.get("cost")) | |
| eb = float(data_b.get("errors")) | |
| except (TypeError, ValueError): | |
| continue | |
| result = compute_marginal_cost(ca, ea, cb, eb) | |
| if result is None: | |
| continue | |
| entry = {"engine_a": a, "engine_b": b} | |
| entry.update(result) | |
| pairs.append(entry) | |
| if not pairs: | |
| return None | |
| pairs.sort(key=lambda p: p["cost_per_avoided_error"]) | |
| return {"pairs": pairs} | |
| __all__ = [ | |
| "compute_marginal_cost", | |
| "compute_marginal_cost_matrix", | |
| ] | |