From 074716bc160e7183b40b70a34ad55739de30c7ac Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 22 Jun 2026 22:21:37 +0200 Subject: [PATCH] feat(observatoire): restructurer tuile en 3 barres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Barre 1 (grise) : % marchés avec champs considérations renseignés - Barre 2 (rose) : % positifs parmi les marchés renseignés — sociales - Barre 3 (verte) : % positifs parmi les marchés renseignés — env Co-Authored-By: Claude Sonnet 4.6 --- src/figures.py | 66 ++++++++++++++++++++++------------------- tests/test_figures.py | 69 ++++++++++++++++++------------------------- 2 files changed, 64 insertions(+), 71 deletions(-) diff --git a/src/figures.py b/src/figures.py index 4641c61..ae322ca 100644 --- a/src/figures.py +++ b/src/figures.py @@ -736,20 +736,18 @@ CONSIDERATIONS_COLUMNS = { def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int]]: - """Part des marchés (uid distincts) ayant au moins une considération. + """Statistiques considérations pour l'observatoire. - Renvoie pour chaque clé (sociales, environnementales) : - - (count_pos, pct) : positifs / tous les uid distincts - - (count_ren, pct) suffixé _renseignees : non-null uid comme dénominateur, - positifs comme numérateur (= part de positifs parmi les marchés renseignés) + Clés renvoyées : + - "champs_renseignes" : (count_ren_sociales, pct / total) + - "{key}_renseignees" : (count_ren, pct positifs parmi renseignés) Colonne absente -> (0, 0). """ names = lff.collect_schema().names() present = {key: col for key, col in CONSIDERATIONS_COLUMNS.items() if col in names} - stats: dict[str, tuple[int, int]] = {} + stats: dict[str, tuple[int, int]] = {"champs_renseignes": (0, 0)} for key in CONSIDERATIONS_COLUMNS: - stats[key] = (0, 0) stats[f"{key}_renseignees"] = (0, 0) if not present: @@ -767,22 +765,22 @@ def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int] return stats for key, col in present.items(): - count_pos = agg.filter(pl.col(col).str.contains(CONSIDERATIONS_REGEX)).height count_ren = agg.filter(pl.col(col).is_not_null()).height count_pos_ren = agg.filter( pl.col(col).is_not_null() & (pl.col(col) != "Sans objet") ).height pct_pos_ren = round(100 * count_pos_ren / count_ren) if count_ren > 0 else 0 - stats[key] = (count_pos, round(100 * count_pos / total)) stats[f"{key}_renseignees"] = (count_ren, pct_pos_ren) + if key == "sociales": + stats["champs_renseignes"] = (count_ren, round(100 * count_ren / total)) return stats -# (clé, libellé, couleur principale, couleur renseignée) -CONSIDERATIONS_DISPLAY = [ - ("sociales", "Sociales", "#CC6677", "#E5B2BB"), - ("environnementales", "Environnementales", "#117733", "#88BB99"), +# (clé stats, libellé, couleur) +CONSIDERATIONS_RENSEIGNEES = [ + ("sociales_renseignees", "Sociales", "#CC6677"), + ("environnementales_renseignees", "Environnementales", "#117733"), ] @@ -795,13 +793,31 @@ def _progress_bar(pct: int, color: str) -> dbc.Progress: def get_considerations_card_content(lff: pl.LazyFrame) -> html.Div: - """Quatre barres groupées par type : considération positive + valeur renseignée.""" + """Trois barres : champs renseignés + part positive pour sociales et environnementales.""" stats = compute_considerations_stats(lff) - blocks = [] - for key, label, color, color_ren in CONSIDERATIONS_DISPLAY: - count_pos, pct_pos = stats[key] - count_ren, pct_ren = stats[f"{key}_renseignees"] + count_ren, pct_ren = stats["champs_renseignes"] + blocks = [ + html.Div( + className="mb-3", + children=[ + html.Div( + className="d-flex justify-content-between", + children=[ + html.Span("Champs considérations renseignés"), + html.Span( + f"{format_number(count_ren)} marchés", + className="text-muted", + ), + ], + ), + _progress_bar(pct_ren, "#6c757d"), + ], + ) + ] + + for key, label, color in CONSIDERATIONS_RENSEIGNEES: + count, pct = stats[key] blocks.append( html.Div( className="mb-3", @@ -811,22 +827,12 @@ def get_considerations_card_content(lff: pl.LazyFrame) -> html.Div: children=[ html.Span(label), html.Span( - f"{format_number(count_pos)} marchés", + f"parmi les {format_number(count)} marchés renseignés", className="text-muted", ), ], ), - _progress_bar(pct_pos, color), - html.Div( - className="d-flex justify-content-end mt-1", - children=[ - html.Span( - f"parmi les {format_number(count_ren)} marchés renseignés", - className="text-muted", - ), - ], - ), - _progress_bar(pct_ren, color_ren), + _progress_bar(pct, color), ], ) ) diff --git a/tests/test_figures.py b/tests/test_figures.py index 040c92e..2b38eda 100644 --- a/tests/test_figures.py +++ b/tests/test_figures.py @@ -10,25 +10,25 @@ def test_compute_considerations_stats_basic(): lff = _make_lff( [ - # uid u1 : social oui (Clause), env non (Sans objet) + # u1 : social oui (Clause), env non (Sans objet) { "uid": "u1", "considerationsSociales": "Clause sociale", "considerationsEnvironnementales": "Sans objet", }, - # uid u2 : social non (Sans objet), env oui (Critère) + # u2 : social non (Sans objet), env oui (Critère) { "uid": "u2", "considerationsSociales": "Sans objet", "considerationsEnvironnementales": "Critère environnemental", }, - # uid u3 : social oui (Marché réservé compte), env null + # u3 : social oui (Marché réservé), env null { "uid": "u3", "considerationsSociales": "Marché réservé", "considerationsEnvironnementales": None, }, - # uid u4 : aucune considération + # u4 : social autre valeur (pas "Sans objet"), env null { "uid": "u4", "considerationsSociales": "Pas de considération sociale", @@ -39,13 +39,11 @@ def test_compute_considerations_stats_basic(): stats = compute_considerations_stats(lff) - # 4 marchés au total. Social : u1, u3 -> 2/4 = 50%. Env : u2 -> 1/4 = 25%. - assert stats["sociales"] == (2, 50) - assert stats["environnementales"] == (1, 25) - # Renseignées : dénominateur = non-null ; numérateur = non-null ET != "Sans objet". - # Social : 4 non-null, 3 != "Sans objet" (u1/u3/u4) -> (4, 75%). - # Env : 3 non-null, 1 != "Sans objet" (u2) -> (3, 33%). + # champs_renseignes : basé sur sociales. 4 non-null / 4 total -> (4, 100%). + assert stats["champs_renseignes"] == (4, 100) + # Sociales renseignées : dén=4 non-null, num=3 != "Sans objet" (u1/u3/u4) -> (4, 75%). assert stats["sociales_renseignees"] == (4, 75) + # Env renseignées : dén=3 non-null (u1/u2/u4), num=1 != "Sans objet" (u2) -> (3, 33%). assert stats["environnementales_renseignees"] == (3, 33) @@ -54,7 +52,7 @@ def test_compute_considerations_stats_dedup_per_uid(): lff = _make_lff( [ - # uid u1 présent 2 fois (2 titulaires) -> compté une seule fois + # u1 présent 2 fois (2 titulaires) -> compté une seule fois { "uid": "u1", "considerationsSociales": "Clause sociale", @@ -75,11 +73,10 @@ def test_compute_considerations_stats_dedup_per_uid(): stats = compute_considerations_stats(lff) - # 2 marchés distincts. Social : u1 -> 1/2 = 50%. - assert stats["sociales"] == (1, 50) - assert stats["environnementales"] == (0, 0) - # Social : 2 non-null, 1 positif -> (2, 50%). Env : 2 non-null, 0 positif -> (2, 0%). + # 2 uid distincts. Social 2 non-null, 1 != "Sans objet" (u1) -> (2, 50%). + assert stats["champs_renseignes"] == (2, 100) assert stats["sociales_renseignees"] == (2, 50) + # Env 2 non-null, 0 != "Sans objet" -> (2, 0%). assert stats["environnementales_renseignees"] == (2, 0) @@ -95,10 +92,8 @@ def test_compute_considerations_stats_missing_column(): stats = compute_considerations_stats(lff) - # Colonne env absente -> (0, 0) sans exception. Social : 1/2 = 50%. - assert stats["sociales"] == (1, 50) - assert stats["environnementales"] == (0, 0) - # Social : 2 non-null, 1 positif -> (2, 50%). Env absente -> (0, 0). + # Colonne env absente -> (0, 0). Social : 2 non-null, 1 != "Sans objet" -> (2, 50%). + assert stats["champs_renseignes"] == (2, 100) assert stats["sociales_renseignees"] == (2, 50) assert stats["environnementales_renseignees"] == (0, 0) @@ -116,13 +111,12 @@ def test_compute_considerations_stats_empty(): stats = compute_considerations_stats(lff) - assert stats["sociales"] == (0, 0) - assert stats["environnementales"] == (0, 0) + assert stats["champs_renseignes"] == (0, 0) assert stats["sociales_renseignees"] == (0, 0) assert stats["environnementales_renseignees"] == (0, 0) -def test_get_considerations_card_content_returns_four_progress_bars(): +def test_get_considerations_card_content_returns_three_progress_bars(): import dash_bootstrap_components as dbc from dash import html @@ -158,23 +152,16 @@ def test_get_considerations_card_content_returns_four_progress_bars(): find_progress(children, found) return found - all_bars = find_progress(div, []) - inner_bars = [b for b in all_bars if getattr(b, "bar", False)] - # 4 barres internes : 2 positives + 2 renseignées - assert len(inner_bars) == 4 + inner_bars = [b for b in find_progress(div, []) if getattr(b, "bar", False)] + assert len(inner_bars) == 3 - social_pos, social_ren, env_pos, env_ren = inner_bars - # Sociales positives : u1 -> 1/2 = 50% - assert social_pos.value == 50 - assert social_pos.color == "#CC6677" - assert social_pos.style["color"] == "white" - # Sociales renseignées : 2 non-null, 1 positif (u1) -> 50% - assert social_ren.value == 50 - assert social_ren.color == "#E5B2BB" - # Environnementales positives : u2 -> 1/2 = 50% - assert env_pos.value == 50 - assert env_pos.color == "#117733" - assert env_pos.style["color"] == "white" - # Environnementales renseignées : 2 non-null (u1 "Sans objet", u2), 1 positif (u2) -> 50% - assert env_ren.value == 50 - assert env_ren.color == "#88BB99" + bar_ren, bar_social, bar_env = inner_bars + # Bar 1 : champs renseignés (2/2 = 100%, gris) + assert bar_ren.value == 100 + assert bar_ren.color == "#6c757d" + # Bar 2 : sociales parmi renseignés (u1 != "Sans objet" -> 1/2 = 50%, rose) + assert bar_social.value == 50 + assert bar_social.color == "#CC6677" + # Bar 3 : env parmi renseignés (u2 != "Sans objet" -> 1/2 = 50%, vert) + assert bar_env.value == 50 + assert bar_env.color == "#117733"