Picarones / picarones /report /robustness_projection_render.py
Claude
refactor(report): consolidate 27 render helpers into render_helpers.py
2d6c41d unverified
Raw
History Blame
9.44 kB
"""Rendu HTML « Déficit projeté de robustesse » — Sprint 88
(A.I.8 vue HTML).
Suite directe ``picarones/core/robustness_projection.py``
(Sprint 81). Pattern identique aux autres rendus : server-
side, pas de JS, anti-injection systématique.
Note d'intégration
------------------
La robustesse synthétique (``picarones.measurements.robustness``) est
exécutée par la CLI ``picarones robustness`` indépendamment du
benchmark principal. Pour produire la vue de projection,
l'utilisateur compose :
.. code-block:: python
from picarones.measurements.robustness import analyze_robustness
from picarones.measurements.robustness_projection import (
project_robustness_on_corpus,
aggregate_projection_per_engine,
)
from picarones.report.robustness_projection_render import (
build_robustness_projection_html,
)
rob = analyze_robustness(corpus, [engine]) # Sprint 8
projection = project_robustness_on_corpus(
rob.curves,
[doc.image_quality.as_dict() for doc in benchmark.docs],
) # Sprint 81
aggregated = aggregate_projection_per_engine(projection)
html = build_robustness_projection_html(
projection, aggregated, labels,
)
Vue
---
1. **Tableau résumé par moteur** : déficit total attendu,
nombre de types de dégradation, pire dégradation.
2. **Tableau détaillé par couple (moteur × dégradation)** :
docs, docs avec data, déficit, % docs au-dessus du seuil
critique.
Les cellules « déficit » sont colorées par gradient vert
(faible) → orange → rouge (≥ 5 points de CER projetés).
Adaptive : ``""`` si la projection est vide (aucune courbe ou
aucun document avec qualité).
"""
from __future__ import annotations
from html import escape as _e
from typing import Optional
from picarones.report.render_helpers import color_traffic_light
def _build_summary_table(
aggregated: dict,
labels: dict[str, str],
) -> str:
if not aggregated:
return ""
h_engine = labels.get("robproj_engine", "Moteur")
h_total = labels.get("robproj_total", "Déficit total (pts CER)")
h_n_types = labels.get("robproj_n_types", "Types évalués")
h_worst = labels.get("robproj_worst", "Pire dégradation")
parts = [
'<table style="border-collapse:collapse;width:100%;'
'font-size:.9rem;margin-bottom:.8rem">',
'<thead><tr>',
]
for col in (h_engine, h_total, h_n_types, h_worst):
parts.append(
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
f'border-bottom:1px solid #ccc;font-weight:600">'
f'{_e(col)}</th>'
)
parts.append("</tr></thead><tbody>")
# Tri par déficit décroissant
rows = sorted(
aggregated.items(),
key=lambda kv: -float(
kv[1].get("total_expected_deficit") or 0.0
),
)
for engine, info in rows:
deficit = float(info.get("total_expected_deficit") or 0.0)
n_types = int(info.get("n_degradation_types") or 0)
worst_type = info.get("worst_degradation_type")
worst_deficit = info.get("worst_degradation_deficit")
color = color_traffic_light(abs(deficit), low_is_good=True, scale_max=0.05)
worst_str = (
f"{_e(str(worst_type))} ({worst_deficit * 100:+.1f})"
if worst_type and isinstance(worst_deficit, (int, float))
else "—"
)
parts.append(
f'<tr>'
f'<td style="padding:.4rem .6rem">{_e(str(engine))}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'background:{color};font-family:monospace;font-weight:600">'
f'{deficit * 100:+.2f}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_types}</td>'
f'<td style="padding:.4rem .6rem">{worst_str}</td>'
f'</tr>'
)
parts.append("</tbody></table>")
return "".join(parts)
def _build_detail_table(
projection: dict,
labels: dict[str, str],
) -> str:
if not projection:
return ""
h_engine = labels.get("robproj_engine", "Moteur")
h_deg_type = labels.get("robproj_deg_type", "Dégradation")
h_n_docs = labels.get("robproj_n_docs", "Docs")
h_n_with_data = labels.get("robproj_n_with_data", "Docs avec data")
h_deficit = labels.get("robproj_deficit", "Δ CER projeté (pts)")
h_above = labels.get("robproj_above", "Docs ≥ seuil critique")
parts = [
'<table style="border-collapse:collapse;width:100%;'
'font-size:.9rem">',
'<thead><tr>',
]
for col in (h_engine, h_deg_type, h_n_docs,
h_n_with_data, h_deficit, h_above):
parts.append(
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
f'border-bottom:1px solid #ccc;font-weight:600">'
f'{_e(col)}</th>'
)
parts.append("</tr></thead><tbody>")
# Tri stable : par moteur puis type de dégradation
for engine in sorted(projection):
per_type = projection[engine] or {}
for deg_type in sorted(per_type):
entry = per_type[deg_type] or {}
n_docs = int(entry.get("n_docs") or 0)
n_with_data = int(entry.get("n_docs_with_data") or 0)
deficit = entry.get("deficit_vs_baseline")
n_above = int(entry.get("n_docs_above_critical") or 0)
if isinstance(deficit, (int, float)):
color = color_traffic_light(abs(float(deficit)), low_is_good=True, scale_max=0.05)
deficit_str = f"{float(deficit) * 100:+.2f}"
deficit_cell = (
f'<td style="padding:.4rem .6rem;text-align:right;'
f'background:{color};font-family:monospace">'
f'{deficit_str}</td>'
)
else:
deficit_cell = (
'<td style="padding:.4rem .6rem;text-align:right;'
'opacity:.4">—</td>'
)
parts.append(
f'<tr>'
f'<td style="padding:.4rem .6rem">{_e(str(engine))}</td>'
f'<td style="padding:.4rem .6rem">{_e(str(deg_type))}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_docs}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_with_data}</td>'
f'{deficit_cell}'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_above}</td>'
f'</tr>'
)
parts.append("</tbody></table>")
return "".join(parts)
def build_robustness_projection_html(
projection: Optional[dict],
aggregated: Optional[dict] = None,
labels: Optional[dict[str, str]] = None,
) -> str:
"""Construit la vue HTML « Déficit projeté de robustesse ».
Parameters
----------
projection:
Sortie de ``project_robustness_on_corpus`` (Sprint 81),
forme ``{engine: {deg_type: {...}}}``. Si ``None`` ou
vide, retourne ``""``.
aggregated:
Sortie de ``aggregate_projection_per_engine`` (Sprint
81). Si ``None``, sera calculé à partir de
``projection``.
labels:
Dict i18n. Clés sous le préfixe ``robproj_*``.
Returns
-------
str
Section HTML, ou ``""`` si projection vide.
"""
if not projection:
return ""
if aggregated is None:
from picarones.measurements.robustness_projection import (
aggregate_projection_per_engine,
)
aggregated = aggregate_projection_per_engine(projection)
labels = labels or {}
title = labels.get(
"robproj_title",
"Déficit projeté de robustesse sur le corpus réel",
)
note = labels.get(
"robproj_note",
"Projection des courbes de dégradation synthétique sur "
"les caractéristiques d'image réelles. Le déficit total "
"suppose l'indépendance des dégradations — c'est une "
"approximation utile pour le diagnostic, pas un verdict.",
)
summary_table = _build_summary_table(aggregated or {}, labels)
detail_table = _build_detail_table(projection, labels)
if not summary_table and not detail_table:
return ""
h_summary = labels.get("robproj_summary", "Résumé par moteur")
h_detail = labels.get(
"robproj_detail", "Détail par couple (moteur × dégradation)",
)
parts = [
'<section class="robproj-section" style="margin:1.5rem 0">',
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.7rem">'
f'{_e(note)}</div>',
]
if summary_table:
parts.append(
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
f'{_e(h_summary)}</div>'
)
parts.append(summary_table)
if detail_table:
parts.append(
f'<div style="font-weight:600;margin:.6rem 0 .3rem 0">'
f'{_e(h_detail)}</div>'
)
parts.append(detail_table)
parts.append('</section>')
return "".join(parts)
__all__ = ["build_robustness_projection_html"]