| import gradio as gr |
| import pandas as pd |
| import numpy as np |
| import matplotlib.pyplot as plt |
|
|
| |
| |
| |
|
|
| DATA_PATH = "data/processed/bv_features.parquet" |
|
|
| df = pd.read_parquet(DATA_PATH) |
| df["date_scrutin"] = pd.to_datetime(df.get("date_scrutin"), errors="coerce") |
| df["tour"] = pd.to_numeric(df.get("tour"), errors="coerce").astype("Int64") |
|
|
| |
| |
| |
| |
| SETE_CODE_INSEE = "34301" |
|
|
| def resolve_code_commune(df_in: pd.DataFrame) -> tuple[pd.DataFrame, str | None]: |
| df_out = df_in.copy() |
| if "code_commune" in df_out.columns: |
| df_out["code_commune"] = df_out["code_commune"].astype("string") |
| return df_out, None |
| if "Code de la commune" in df_out.columns: |
| df_out = df_out.rename(columns={"Code de la commune": "code_commune"}) |
| df_out["code_commune"] = df_out["code_commune"].astype("string") |
| return df_out, None |
| if "code_bv" in df_out.columns: |
| df_out["code_commune"] = df_out["code_bv"].astype(str).str.slice(0, 5) |
| df_out["code_commune"] = df_out["code_commune"].astype("string") |
| valid = df_out["code_commune"].str.len() == 5 |
| if not valid.any(): |
| return df_out, "Impossible de dériver code_commune depuis code_bv (format inattendu)." |
| return df_out, None |
| df_out["code_commune"] = pd.NA |
| return df_out, "Aucune colonne commune disponible (code_commune/Code de la commune/code_bv)." |
|
|
|
|
| df, commune_warning = resolve_code_commune(df) |
| df["code_commune"] = ( |
| df["code_commune"] |
| .astype(str) |
| .str.replace(".0", "", regex=False) |
| .str.replace(r"\D", "", regex=True) |
| .str.zfill(5) |
| .astype("string") |
| ) |
| df_sete = df[df["code_commune"] == SETE_CODE_INSEE].copy() |
| df_sete["tour"] = pd.to_numeric(df_sete["tour"], errors="coerce").astype("Int64") |
|
|
| |
| BASE_BLOCS = [ |
| "droite_modere", |
| "gauche_modere", |
| "gauche_dure", |
| "droite_dure", |
| "centre", |
| "extreme_gauche", |
| "extreme_droite", |
| "autre", |
| ] |
| BLOC_LABELS = [b for b in BASE_BLOCS if f"part_bloc_{b}" in df_sete.columns] |
| BLOC_COLS = [f"part_bloc_{b}" for b in BLOC_LABELS] |
|
|
| |
| |
| |
|
|
| def compute_national_reference(df_all, type_scrutin, tour): |
| """ |
| Calcule les parts nationales par bloc pour un scrutin et un tour donnés. |
| """ |
| if not BLOC_COLS: |
| return {} |
| df_nat = df_all[ |
| (df_all["type_scrutin"] == type_scrutin) |
| & (df_all["tour"] == tour) |
| ] |
|
|
| |
| weights = df_nat["exprimes"].replace(0, np.nan) |
|
|
| national = {} |
| for col in BLOC_COLS: |
| national[col] = np.nansum(df_nat[col] * weights) / np.nansum(weights) |
|
|
| return national |
|
|
|
|
| def table_sete(type_scrutin, tour): |
| if not BLOC_COLS: |
| return pd.DataFrame({"info": ["Colonnes part_bloc_* absentes."]}) |
| tour_val = pd.to_numeric(tour, errors="coerce") |
| if pd.isna(tour_val): |
| return pd.DataFrame({"info": ["Tour invalide."]}) |
| |
| local = df_sete[ |
| (df_sete["type_scrutin"] == type_scrutin) |
| & (df_sete["tour"] == int(tour_val)) |
| ].copy() |
|
|
| if local.empty: |
| return pd.DataFrame({"info": ["Aucune donnée disponible"]}) |
|
|
| |
| nat = compute_national_reference(df, type_scrutin, tour) |
|
|
| |
| rows = [] |
|
|
| for _, row in local.iterrows(): |
| r = { |
| "code_bv": row["code_bv"], |
| "nom_bv": row.get("nom_bv", ""), |
| } |
|
|
| for col in BLOC_COLS: |
| part = row[col] |
| ecart = part - nat.get(col, 0) |
|
|
| r[col.replace("part_bloc_", "")] = round(part * 100, 2) |
| r[col.replace("part_bloc_", "") + "_ecart_nat"] = round(ecart * 100, 2) |
|
|
| rows.append(r) |
|
|
| result = pd.DataFrame(rows) |
|
|
| |
| if "extreme_droite_ecart_nat" in result.columns: |
| result = result.sort_values( |
| "extreme_droite_ecart_nat", ascending=False |
| ) |
|
|
| return result |
|
|
|
|
| def get_bv_timeseries(code_bv: str, tour: int | None) -> pd.DataFrame: |
| if df_sete.empty or not BLOC_COLS: |
| return pd.DataFrame(columns=["date_scrutin"] + BLOC_COLS) |
| subset = df_sete[df_sete["code_bv"].astype(str) == str(code_bv)].copy() |
| subset["tour"] = pd.to_numeric(subset["tour"], errors="coerce").astype("Int64") |
| if tour is not None: |
| subset = subset[subset["tour"] == tour] |
| subset = subset.dropna(subset=["date_scrutin"]).sort_values("date_scrutin") |
| return subset[["date_scrutin"] + BLOC_COLS] |
|
|
|
|
| def plot_bv_timeseries(code_bv: str, tour_choice, bloc_choices=None): |
| tour = None if tour_choice == "Tous" else int(tour_choice) |
| fig, ax = plt.subplots(figsize=(8, 4)) |
| if not BLOC_COLS: |
| ax.text(0.5, 0.5, "Colonnes part_bloc_* absentes.", ha="center", va="center") |
| ax.axis("off") |
| return fig |
| df_ts = get_bv_timeseries(code_bv, tour) |
| if df_ts.empty: |
| tours_avail = ( |
| df_sete[df_sete["code_bv"].astype(str) == str(code_bv)]["tour"] |
| .dropna() |
| .unique() |
| .tolist() |
| ) |
| ax.text( |
| 0.5, |
| 0.5, |
| f"Aucune donnée après filtre tour={tour}. Valeurs disponibles: {sorted(tours_avail)}", |
| ha="center", |
| va="center", |
| wrap=True, |
| ) |
| ax.axis("off") |
| return fig |
|
|
| selected = bloc_choices or BLOC_LABELS |
| selected_cols = [f"part_bloc_{b}" for b in selected if f"part_bloc_{b}" in df_ts.columns] |
| if not selected_cols: |
| ax.text(0.5, 0.5, "Aucun bloc sélectionné.", ha="center", va="center") |
| ax.axis("off") |
| return fig |
| for col in selected_cols: |
| ax.plot(df_ts["date_scrutin"], df_ts[col], label=col.replace("part_bloc_", "")) |
| ax.set_title(f"Évolution politique – BV {code_bv}") |
| ax.set_ylabel("Part des voix (exprimés)") |
| ax.grid(True, alpha=0.3) |
| ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", borderaxespad=0, fontsize=8) |
| fig.autofmt_xdate() |
| fig.tight_layout() |
| return fig |
|
|
|
|
| |
| |
| |
|
|
| def format_bv_label(code_bv: str) -> str: |
| code_str = str(code_bv) |
| if code_str.isdigit() and code_str.startswith(SETE_CODE_INSEE) and len(code_str) == 9: |
| bureau_num = code_str[-4:] |
| return f"BV {int(bureau_num)} ({code_str})" |
| return code_str |
|
|
|
|
| bv_values = ( |
| sorted(df_sete["code_bv"].astype(str).unique().tolist()) |
| if "code_bv" in df_sete.columns |
| else [] |
| ) |
| bv_choices = [(format_bv_label(code), code) for code in bv_values] |
| scrutins = sorted(df_sete["type_scrutin"].unique()) |
| tours = sorted(df_sete["tour"].dropna().unique()) |
| tour_options = ["Tous"] + [str(t) for t in tours] |
| status_messages = [] |
| if commune_warning: |
| status_messages.append(commune_warning) |
| if df_sete.empty: |
| status_messages.append( |
| "Aucune ligne pour la commune 34301 (Sète). Vérifie `code_commune` / le filtre." |
| ) |
| if not BLOC_COLS: |
| status_messages.append("Colonnes part_bloc_* absentes dans bv_features.") |
| missing_blocs = [f"part_bloc_{b}" for b in BASE_BLOCS if f"part_bloc_{b}" not in df_sete.columns] |
| if missing_blocs: |
| status_messages.append(f"Colonnes blocs manquantes: {', '.join(missing_blocs)}") |
| tour_dtype = str(df_sete["tour"].dtype) if "tour" in df_sete.columns else "n/a" |
| tour_sample = sorted(df_sete["tour"].dropna().unique().tolist())[:10] |
| status_messages.append(f"tour dtype: {tour_dtype}") |
| status_messages.append(f"tours disponibles (échantillon): {tour_sample}") |
| status_messages.append( |
| f"df_sete: {len(df_sete)} lignes, {df_sete['code_bv'].nunique() if 'code_bv' in df_sete.columns else 0} BV" |
| ) |
| status_messages.append(f"blocs actifs: {', '.join(BLOC_LABELS) if BLOC_LABELS else 'aucun'}") |
| status_text = "\n".join(f"- {msg}" for msg in status_messages) |
|
|
| with gr.Blocks(title="Résultats électoraux – Bureaux de vote de Sète") as app: |
| gr.Markdown( |
| """ |
| # 🗳️ Résultats électoraux – Ville de Sète |
| |
| **Bureaux de vote uniquement – comparaison au niveau national** |
| |
| Les pourcentages sont exprimés en **% des exprimés**. |
| Les écarts sont en **points par rapport au national**. |
| """ |
| ) |
| if status_text: |
| gr.Markdown(f"**Alertes**\n{status_text}") |
|
|
| with gr.Tabs(): |
| with gr.Tab("Bureaux de vote"): |
| with gr.Row(): |
| type_scrutin = gr.Dropdown( |
| scrutins, |
| label="Type de scrutin", |
| value=scrutins[0] if scrutins else None, |
| ) |
| tour = gr.Dropdown( |
| tours, |
| label="Tour", |
| value=tours[0] if tours else None, |
| ) |
|
|
| output = gr.Dataframe( |
| label="Bureaux de vote – parts locales et écart au national", |
| interactive=False, |
| wrap=True, |
| ) |
|
|
| btn = gr.Button("Afficher") |
|
|
| btn.click( |
| fn=table_sete, |
| inputs=[type_scrutin, tour], |
| outputs=output, |
| ) |
|
|
| with gr.Tab("Évolution temporelle"): |
| bv_selector = gr.Dropdown( |
| bv_choices, |
| label="Bureau de vote", |
| value=bv_values[0] if bv_values else None, |
| ) |
| tour_selector = gr.Dropdown( |
| tour_options, |
| label="Tour", |
| value="Tous", |
| ) |
| blocs_selector = gr.Dropdown( |
| BLOC_LABELS, |
| label="Blocs à afficher", |
| value=BLOC_LABELS, |
| multiselect=True, |
| ) |
| plot = gr.Plot( |
| value=plot_bv_timeseries( |
| bv_values[0] if bv_values else "", "Tous", BLOC_LABELS |
| ) |
| ) |
|
|
| bv_selector.change( |
| fn=plot_bv_timeseries, |
| inputs=[bv_selector, tour_selector, blocs_selector], |
| outputs=plot, |
| ) |
| tour_selector.change( |
| fn=plot_bv_timeseries, |
| inputs=[bv_selector, tour_selector, blocs_selector], |
| outputs=plot, |
| ) |
| blocs_selector.change( |
| fn=plot_bv_timeseries, |
| inputs=[bv_selector, tour_selector, blocs_selector], |
| outputs=plot, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if __name__ == "__main__": |
| app.launch() |
|
|