feat(observatoire): restructurer tuile en 3 barres
- 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 <noreply@anthropic.com>
This commit is contained in:
+36
-30
@@ -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),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
+28
-41
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user