| | 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() |
| |
|