rco / app.py.bak2
Phitographix's picture
Upload 369 files
827bfb0 verified
Raw
History Blame Contribute Delete
19 kB
# -*- coding: utf-8 -*-
"""
Boîte à outils — Portail analytique (Haïti)
Application Streamlit conçue pour être intégrée dans WordPress via une <iframe>.
- Source du catalogue : catalogue_indicateurs_FR.csv (libellés en français,
secteurs thématiques attribués)
- Filtres lisibles : Niveau géographique (Pays / Département / Commune),
Désagrégation (Tranche dge / Sexe / Toutes), Secteur, Source, Échelle,
recherche d'indicateur (cascade).
- Désagrégations sexe et tranche dge dérivées des libellés d'indicateurs.
- Par défaut : indicateurs par tranche dge, classés de la date de début à
la date de fin.
- Zone « Americas » supprimée des données.
- Choix des colonnes affichées / téléchargées dans le tableau.
- Graphique sans lignes horizontales + nombre d'indicateurs affichés.
- Bouton « Effacer les filtres ».
Lancement : streamlit run app.py
"""
import math
import os
import re
import pandas as pd
import plotly.express as px
import streamlit as st
ICI = os.path.dirname(os.path.abspath(__file__))
PARQUET = os.path.join(ICI, "donnees_unifiees.parquet")
CATALOGUE = os.path.join(ICI, "catalogue_indicateurs_FR.csv")
AGREGATIONS = {"Moyenne": "mean", "Somme": "sum", "Médiane": "median",
"Minimum": "min", "Maximum": "max"}
# niveau géographique demandé -> valeur dans la colonne niveau_geo
NIVEAUX = {"Pays": "National", "Département": "Admin1", "Commune": "Admin2"}
st.set_page_config(page_title="Boîte à outils — Portail analytique",
layout="wide", initial_sidebar_state="expanded")
# ----------------------------------------------------------------- thème
st.markdown("""
<style>
#MainMenu, footer, header {visibility: hidden;}
.block-container {padding-top: 0.5rem; padding-bottom: 1rem;}
.bandeau {background: linear-gradient(90deg,#12347a 0%,#1c4fb0 100%);
color:#fff; padding:14px 22px; border-radius:6px; margin-bottom:14px;}
.bandeau h1 {font-size:1.5rem; margin:0; font-weight:600;}
.bandeau span {opacity:.85; font-weight:400;}
/* --- sidebar : panneau clair et lisible --- */
section[data-testid="stSidebar"] {background-color:#f3f6fc;
border-right:1px solid #d7e0f2;}
section[data-testid="stSidebar"] * {color:#0d1b3e !important;}
section[data-testid="stSidebar"] h2 {color:#12347a !important; font-size:1.15rem;
margin-bottom:.2rem;}
.sct {color:#12347a; font-weight:700; font-size:.92rem;
text-transform:uppercase; letter-spacing:.04em;
margin:.5rem 0 .1rem; padding-top:.2rem;
border-top:1px solid #d7e0f2;}
.sct.first {border-top:none;}
section[data-testid="stSidebar"] label {font-weight:600 !important;}
.infoligne {color:#12347a; font-weight:600; margin:8px 0 2px;}
.reflet {color:#5b6a8f !important; font-size:.82rem; margin-top:-4px;}
.compteur {display:inline-block; background:#12347a; color:#fff !important;
padding:3px 12px; border-radius:14px; font-weight:600;
font-size:.95rem;}
</style>
""", unsafe_allow_html=True)
# ----------------------------------------------------------------- nettoyage texte
_MOJI = re.compile(r"[ÃÂ]|â€|â‚|�")
def nettoyer(s):
"""Répare les caractères mal encodés (UTF-8 lu comme cp1252) et espaces."""
s = str(s)
if _MOJI.search(s):
for enc in ("cp1252", "latin-1"):
try:
rep = s.encode(enc).decode("utf-8")
if "�" not in rep:
s = rep
break
except Exception:
pass
s = s.replace("\xa0", " ").replace("​", "")
return re.sub(r"\s+", " ", s).strip()
# ----------------------------------------------------------------- sexe / âge
def detect_sexe(nom):
"""Déduit le sexe à partir du libellé de l'indicateur."""
s = str(nom).lower()
fem = re.search(r"female|women|woman|femmes?|fille|girls?|maternal|gender female", s)
# on retire 'female' avant de chercher 'male' pour éviter les faux positifs
reste = re.sub(r"female|women|woman", "", s)
mal = re.search(r"\bmale\b|\bmen\b|\bman\b|hommes?|gar[cç]on|boys?|gender male", reste)
if fem and mal:
return "Mixte"
if fem:
return "Femmes"
if mal:
return "Hommes"
return None
_AGE = [
("Moins de 5 ans",
r"under[- ]?5|under[- ]?five|<\s*5\b|\b0[-–]4\b|\bunder 1\b|infant|neonat|"
r"newborn|6[-–]23 month|child mortality|under-five|moins de 5"),
("Enfants (5-14 ans)",
r"\b5[-–]14|6[-–]14|2[-–]14|10[-–]14|child labor|children age 12[-–]14|school-age"),
("Adolescents (10-24 ans)",
r"adolesc|10[-–]19|15[-–]19|15[-–]24|20[-–]24|young (men|women|people)|youth"),
("Adultes (15-64 ans)",
r"15[-–]49|25[-–]49|18[-–]49|15[-–]64|20[-–]79|\badult|reproductive age|"
r"40[-–]44|45[-–]49"),
("Personnes âgées (60 ans +)",
r"60\s*\+|65\s*\+|elderly|older (persons|people)|aged 60|65 (years )?and"),
]
def detect_age(nom):
"""Déduit une tranche d'âge à partir du libellé de l'indicateur."""
s = str(nom).lower()
for lbl, pat in _AGE:
if re.search(pat, s):
return lbl
# repli : ne retenir que de vrais signaux dge (éviter « years of schooling »)
if re.search(r"\baged?\b|age[- ]group|\bchildren?\b|infant|adolesc|newborn|"
r"neonat|\bans\b|under\s*\d+|\d+\s*\+|"
r"\d+\s*[-–]\s*\d+\s*(year|month|ans)", s):
return "Autre tranche d'âge"
return None
# ordre d'affichage des tranches dge
ORDRE_AGE = ["Moins de 5 ans", "Enfants (5-14 ans)", "Adolescents (10-24 ans)",
"Adultes (15-64 ans)", "Personnes âgées (60 ans +)", "Autre tranche d'âge"]
# ----------------------------------------------------------------- chargement
@st.cache_data(show_spinner="Chargement des données…")
def charger():
df = pd.read_parquet(PARQUET)
# suppression de la zone « Americas »
df = df[df["zone"].astype(str).str.strip().str.lower() != "americas"].copy()
df["annee"] = pd.to_numeric(df["annee"], errors="coerce").astype("Int64")
df["mois"] = pd.to_numeric(df["mois"], errors="coerce").astype("Int64")
# catalogue traduit (UTF-8 avec BOM) : libellés FR + secteurs attribués
cat = pd.read_csv(CATALOGUE, encoding="utf-8-sig")
cat = cat.loc[:, ~cat.columns.str.startswith("Unnamed")]
cat = cat.drop_duplicates(subset="indicateur_complet")
cat = cat[cat["indicateur_complet"].isin(df["indicateur_complet"].unique())].copy()
# libellé affiché / recherché = traduction française
cat["Indicateurs"] = cat["Indicateurs_FR"].map(nettoyer)
cat["source"] = cat["source"].map(nettoyer)
# filtre thématique = nouvelle classification française (Secteur_attribue),
# repli sur l'ancienne colonne « Secteur » si absente
secteur_src = "Secteur_attribue" if "Secteur_attribue" in cat.columns else "Secteur"
cat["Secteur"] = cat[secteur_src].map(nettoyer)
cat["Date_debut"] = pd.to_numeric(cat["Date_debut"], errors="coerce")
cat["Date_fin"] = pd.to_numeric(cat["Date_fin"], errors="coerce")
# détection sexe / tranche dge sur le libellé anglais d'origine
# (extrait de indicateur_complet, après « :: ») — plus fiable que sur le FR
en = cat["indicateur_complet"].astype(str).str.split("::").str[-1].str.strip()
cat["sexe"] = en.map(detect_sexe)
cat["age"] = en.map(detect_age)
return df, cat
df, cat = charger()
ss = st.session_state
v = ss.setdefault("_v", 0) # version des filtres (pour le bouton « effacer »)
def k(nom):
"""Clé de widget versionnée (incrémentée par le bouton Effacer)."""
return f"{nom}_{ss['_v']}"
def effacer_filtres():
ss["_v"] = ss.get("_v", 0) + 1 # nouvelles clés -> widgets réinitialisés
ss["pg"] = 1
st.markdown('<div class="bandeau"><h1>Boîte à outils '
'<span>— Portail analytique</span></h1></div>', unsafe_allow_html=True)
# ----------------------------------------------------------------- filtres
with st.sidebar:
st.markdown("## Filtres")
st.button("🧹 Effacer les filtres", on_click=effacer_filtres,
use_container_width=True)
# --- Niveau géographique ---
st.markdown('<div class="sct first">Niveau géographique</div>',
unsafe_allow_html=True)
f_niveau = st.radio("Afficher les données par", list(NIVEAUX.keys()),
index=0, horizontal=True, key=k("niveau"),
label_visibility="collapsed")
# --- Désagrégation ---
st.markdown('<div class="sct">Désagrégation</div>', unsafe_allow_html=True)
f_desag = st.radio("Type de désagrégation",
["Tranche d'âge", "Sexe", "Toutes"], index=0,
key=k("desag"), label_visibility="collapsed")
base = cat.copy()
if f_desag == "Tranche d'âge":
base = base[base["age"].notna()]
ages = [a for a in ORDRE_AGE if a in base["age"].unique()]
f_age = st.selectbox("Tranche d'âge", ["(toutes)"] + ages, index=0,
key=k("age"))
if f_age != "(toutes)":
base = base[base["age"] == f_age]
elif f_desag == "Sexe":
base = base[base["sexe"].isin(["Femmes", "Hommes", "Mixte"])]
sexes = [s for s in ["Femmes", "Hommes", "Mixte"]
if s in base["sexe"].unique()]
f_sexe = st.selectbox("Sexe", ["(tous)"] + sexes, index=0, key=k("sexe"))
if f_sexe != "(tous)":
base = base[base["sexe"] == f_sexe]
# --- Thématique ---
st.markdown('<div class="sct">Thématique</div>', unsafe_allow_html=True)
secteurs = ["(tous)"] + sorted(base["Secteur"].dropna().unique().tolist())
f_secteur = st.selectbox("Secteur", secteurs, index=0, key=k("secteur"))
if f_secteur != "(tous)":
base = base[base["Secteur"] == f_secteur]
sources = ["(toutes)"] + sorted(base["source"].dropna().unique().tolist())
f_source = st.selectbox("Source", sources, index=0, key=k("source"))
if f_source != "(toutes)":
base = base[base["source"] == f_source]
echelles = ["(toutes)"] + sorted(base["Echelle"].dropna().unique().tolist())
f_echelle = st.selectbox("Échelle géographique", echelles, index=0,
key=k("echelle"))
if f_echelle != "(toutes)":
base = base[base["Echelle"] == f_echelle]
recherche = st.text_input("Recherche d'indicateur", "", key=k("rech"),
placeholder="mot-clé : mortalité, prix, emploi…")
if recherche.strip():
base = base[base["Indicateurs"].astype(str).str.contains(
recherche.strip(), case=False, na=False)]
# --- Indicateurs (classés de la date de début à la date de fin) ---
st.markdown('<div class="sct">Indicateurs</div>', unsafe_allow_html=True)
libelle = dict(zip(cat["indicateur_complet"], cat["Indicateurs"]))
base = base.sort_values(["Date_debut", "Date_fin", "Indicateurs"],
na_position="last")
opts = base["indicateur_complet"].tolist()
# par défaut : les premiers indicateurs (déjà classés par date de début)
defauts = opts[:6]
f_indics = st.multiselect(
"Indicateur (sélection multiple)", options=opts, default=defauts,
format_func=lambda x: libelle.get(x, x)[:75], key=k("indics"),
help="Classés de la date de début à la date de fin. "
"Commencez à taper pour rechercher.")
if f_indics:
sel = cat[cat["indicateur_complet"].isin(f_indics)]
srcs = ", ".join(sorted(sel["source"].dropna().unique())[:4])
st.markdown(f'<div class="reflet">Sources : {srcs or "—"}</div>',
unsafe_allow_html=True)
# --- Fréquence & période ---
st.markdown('<div class="sct">Temporalité</div>', unsafe_allow_html=True)
f_freq = st.radio("Fréquence", ["Année", "Trimestre", "Mois"],
horizontal=True, key=k("freq"))
f_agg_lbl = st.selectbox("Agrégation", list(AGREGATIONS.keys()), index=0,
key=k("agg"))
f_agg = AGREGATIONS[f_agg_lbl]
an_min = int(cat["Date_debut"].dropna().min())
an_max = int(cat["Date_fin"].dropna().max())
f_annees = st.slider("Période", an_min, an_max, (an_min, an_max),
key=k("annees"))
# ----------------------------------------------------------------- corps
if not f_indics:
st.info("⬅️ Sélectionnez au moins un indicateur dans les filtres.")
st.stop()
sous = df[df["indicateur_complet"].isin(f_indics)].copy()
sous = sous[sous["niveau_geo"] == NIVEAUX[f_niveau]]
sous = sous[(sous["annee"] >= f_annees[0]) & (sous["annee"] <= f_annees[1])]
if f_freq in ("Mois", "Trimestre"):
sous = sous[sous["mois"].notna()]
if sous.empty:
st.warning(f"Aucune donnée « {f_freq} » au niveau « {f_niveau} » pour cette "
f"sélection sur {f_annees[0]}–{f_annees[1]}. La plupart des séries "
f"sont annuelles et nationales — essayez « Année » et « Pays ».")
st.stop()
a = sous["annee"].astype(int)
if f_freq == "Année":
sous["sortkey"] = a
sous["periode"] = a.astype(str)
elif f_freq == "Trimestre":
q = ((sous["mois"].astype(int) - 1) // 3 + 1)
sous["sortkey"] = a * 10 + q
sous["periode"] = a.astype(str) + "-T" + q.astype(str)
else:
m = sous["mois"].astype(int)
sous["sortkey"] = a * 100 + m
sous["periode"] = a.astype(str) + "-" + m.astype(str).str.zfill(2)
sous["Indicateur"] = sous["indicateur_complet"].map(lambda x: libelle.get(x, x))
serie = (sous.groupby(["Indicateur", "periode", "sortkey"])["valeur"]
.agg(f_agg).reset_index().sort_values("sortkey"))
ordre = serie.drop_duplicates("periode").sort_values("sortkey")["periode"].tolist()
# ----------------------------------------------------------------- en-tête + graphique
n_indic = sous["Indicateur"].nunique()
st.markdown(f'<span class="compteur">📊 {n_indic} indicateur(s) affiché(s)</span>',
unsafe_allow_html=True)
c1, c2, c3 = st.columns([2, 1, 1])
with c2:
type_g = st.selectbox("Type de graphique", ["Ligne", "Barres", "Aires"], index=0)
with c3:
base100 = st.checkbox("Indice base 100", value=False,
help="Compare des indicateurs d'échelles différentes.")
data_g = serie.copy()
ylab = f"{f_agg_lbl} de la valeur"
if base100:
def b100(g):
ref = g.sort_values("sortkey")["valeur"].iloc[0]
if pd.notna(ref) and ref != 0:
g["valeur"] = g["valeur"] / ref * 100
return g
data_g = data_g.groupby("Indicateur", group_keys=False).apply(b100)
ylab = "Indice (base 100)"
if type_g == "Ligne":
fig = px.line(data_g, x="periode", y="valeur", color="Indicateur", markers=True)
elif type_g == "Barres":
fig = px.bar(data_g, x="periode", y="valeur", color="Indicateur", barmode="group")
else:
fig = px.area(data_g, x="periode", y="valeur", color="Indicateur")
fig.update_xaxes(categoryorder="array", categoryarray=ordre, title=f_freq,
showgrid=False)
# suppression des lignes horizontales (grille de l'axe vertical)
fig.update_yaxes(showgrid=False, zeroline=False)
fig.update_layout(yaxis_title=ylab, legend_title="Indicateur", height=460,
legend=dict(orientation="h", yanchor="bottom", y=-0.45),
margin=dict(l=10, r=10, t=20, b=10))
st.plotly_chart(fig, use_container_width=True)
# ----------------------------------------------------------------- tableau
src_map = dict(zip(cat["indicateur_complet"], cat["source"]))
deb_map = dict(zip(cat["indicateur_complet"], cat["Date_debut"]))
tab = sous[["indicateur_complet", "Indicateur", "desagregation", "zone",
"periode", "sortkey", "valeur", "unite"]].copy()
tab.insert(0, "Source", tab["indicateur_complet"].map(src_map))
tab["_date_debut"] = tab["indicateur_complet"].map(deb_map)
tab = tab.drop(columns="indicateur_complet").rename(columns={
"desagregation": "Désagrégation", "zone": "Zone",
"periode": "Période", "valeur": "Valeur", "unite": "Unité"})
# classé de la date de début à la date de fin, puis chronologiquement
tab = tab.sort_values(["_date_debut", "Indicateur", "sortkey"]).reset_index(drop=True)
tab = tab.drop(columns=["sortkey", "_date_debut"])
for c in ("Désagrégation", "Zone", "Unité"):
tab[c] = tab[c].map(nettoyer)
# choix des colonnes à afficher / télécharger
COLS = ["Source", "Indicateur", "Désagrégation", "Zone", "Période", "Valeur", "Unité"]
st.markdown('<div class="infoligne">Colonnes du tableau (téléchargement) :</div>',
unsafe_allow_html=True)
cols_choisies = st.multiselect("Colonnes à afficher", COLS, default=COLS,
key=k("cols"), label_visibility="collapsed")
if not cols_choisies:
cols_choisies = COLS
tab = tab[cols_choisies]
# réinitialiser la page si la sélection change
sig = (tuple(sorted(f_indics)), f_niveau, f_freq, f_agg, f_annees,
tuple(cols_choisies))
if ss.get("_sig") != sig:
ss["_sig"] = sig
ss["pg"] = 1
g1, g2 = st.columns([3, 1])
with g2:
csv = tab.to_csv(index=False).encode("utf-8-sig")
st.download_button("⬇️ Télécharger (CSV)", csv,
file_name="donnees_selection.csv", mime="text/csv",
use_container_width=True)
# contrôles de pagination
n = len(tab)
pc1, pc2, pc3, pc4, pc5 = st.columns([1.2, 1, 1.4, 1, 4])
with pc5:
taille = st.selectbox("Lignes par page", [25, 50, 100, 200], index=1,
key="psize", label_visibility="collapsed")
npages = max(1, math.ceil(n / taille))
if ss.get("pg", 1) > npages:
ss["pg"] = npages
if ss.get("pg", 1) < 1:
ss["pg"] = 1
def _go(d):
ss["pg"] = min(max(1, ss.get("pg", 1) + d), npages)
with pc1:
st.button("◀ Précédent", on_click=_go, args=(-1,),
disabled=ss.get("pg", 1) <= 1, use_container_width=True)
with pc3:
st.selectbox("Page", list(range(1, npages + 1)), key="pg",
label_visibility="collapsed")
with pc4:
st.button("Suivant ▶", on_click=_go, args=(1,),
disabled=ss.get("pg", 1) >= npages, use_container_width=True)
page = ss.get("pg", 1)
deb = (page - 1) * taille
fin = min(deb + taille, n)
with g1:
st.markdown(f'<div class="infoligne">Affichage de {deb + 1 if n else 0} – {fin} '
f'sur {n} lignes · page {page}/{npages} · '
f'{n_indic} indicateur(s) · niveau : {f_niveau.lower()} · '
f'fréquence : {f_freq.lower()}</div>',
unsafe_allow_html=True)
page_df = tab.iloc[deb:fin]
hauteur = int(38 + 35 * len(page_df)) if len(page_df) else 80
st.dataframe(page_df, use_container_width=True, height=hauteur, hide_index=True)