docs: spec tuile considérations sociales/environnementales (observatoire)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-22 18:09:05 +02:00
parent e44af8add8
commit 826448d6be
@@ -0,0 +1,112 @@
# Tuile « Considérations sociales et environnementales » — Observatoire
## Objectif
Ajouter dans `/observatoire` une tuile (card) qui visualise, à l'aide de barres
de progression « plus ou moins remplies », la part des marchés publics filtrés
qui comportent **au moins une considération sociale** et la part qui comportent
**au moins une considération environnementale**.
La tuile s'insère juste **après la tuile « Type d'achat »**, avec le même style
que les autres cards.
## Données
Colonnes concernées (type `String`, valeurs libres potentiellement composées) :
- `considerationsSociales`
- `considerationsEnvironnementales`
Exemples de valeurs : `Sans objet`, `Clause sociale`, `Critère social`,
`Marché réservé`, `Clause environnementale`, `Critère environnemental`,
`Pas de considération sociale`, `null`, ou des combinaisons
(`Critère social, Clause sociale`).
### Définition « au moins une considération »
Un marché compte comme ayant une considération si la valeur de la colonne
**contient** l'un des mots-clés (insensible à la casse) :
- `Clause`
- `Critère`
- `Marché réservé`
Regex utilisée : `(?i)Clause|Critère|Marché réservé`.
Conséquence (validée avec l'utilisateur) : **`Marché réservé` compte comme
considération sociale**. Les valeurs `Sans objet`, `Pas de considération…` et
`null` ne contiennent aucun de ces mots-clés et ne comptent donc pas.
### Calcul du pourcentage
- **Dédoublonnage par `uid`** : un marché est compté une seule fois même s'il
apparaît sur plusieurs lignes (plusieurs titulaires). On prend la première
valeur de chaque colonne par `uid`.
- **Dénominateur** : **tous** les marchés filtrés (y compris `Sans objet` et
non renseignés) — validé avec l'utilisateur.
- **Numérateur** : nombre de marchés (uid distincts) dont la valeur de colonne
satisfait la regex.
- `pourcentage = round(100 * numérateur / dénominateur)` ; si dénominateur = 0,
pourcentage = 0.
## Composant visuel
Nouvelle fonction `get_considerations_card_content(lff: pl.LazyFrame)` dans
`src/figures.py`, renvoyant un `html.Div` contenant deux barres `dbc.Progress`
empilées :
| Considération | Couleur (px.colors.qualitative.Safe) | Valeur RGB |
| ----------------- | ------------------------------------ | -------------------- |
| Sociales | index 1 (rouge) | `rgb(204, 102, 119)` |
| Environnementales | index 3 (vert) | `rgb(17, 119, 51)` |
Chaque barre :
- `dbc.Progress(value=pourcentage, label=f"{pourcentage} %", style={"backgroundColor": <couleur>})`
- précédée d'un libellé (`Sociales` / `Environnementales`) et suivie du nombre
de marchés concernés (`N marchés`), formaté avec `format_number`.
### Robustesse (colonne absente)
`tests/test.parquet` peut ne pas contenir ces colonnes. La fonction vérifie la
présence de chaque colonne via `lff.collect_schema().names()` ; si une colonne
manque, son pourcentage et son compte valent 0 (pas d'exception), à l'image de
`get_distance_histogram`.
## Intégration
Dans `src/pages/observatoire.py`, fonction `_compute_dashboard_children` :
```python
donut_marche_type = make_donut(lff, "type", per_uid=True, nulls="?")
cards.append(make_card(title="Type d'achat", ...))
# NOUVEAU
considerations = get_considerations_card_content(lff)
cards.append(
make_card(
title="Considérations sociales et environnementales",
subtitle="part des marchés concernés",
fig=considerations,
)
)
```
`make_card` utilise ses dimensions par défaut (`lg=6, xl=4`), comme la tuile
« Type d'achat ».
Import à ajouter : `get_considerations_card_content` depuis `src.figures`.
## Tests
- Test unitaire de `get_considerations_card_content` sur un petit `LazyFrame`
construit en mémoire couvrant : valeur avec considération, `Sans objet`,
`null`, `Marché réservé`, doublon de `uid`. Vérifier les pourcentages
attendus.
- Cas colonne absente → 0 % sans exception.
## Hors périmètre (YAGNI)
- Pas de tooltip détaillé sur les types de considérations.
- Pas de graphe de répartition par type (clause vs critère).
- Pas de nouveau filtre (les filtres existants `social`/`env` restent inchangés).