from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware import pandas as pd import os from typing import Optional app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) DATA_PATH = "/data/prenoms.csv" df_cache = None autocomplete_cache = None alltime_cache = None # total all-time par (prenom, sexe) def get_df(): global df_cache, autocomplete_cache if df_cache is None: if not os.path.exists(DATA_PATH): raise HTTPException(status_code=503, detail="Fichier prenoms.csv non trouvé dans /data/") df_cache = pd.read_csv(DATA_PATH, sep=";", dtype={"sexe": int, "prenom": str, "periode": int, "valeur": int}) df_cache = df_cache[df_cache["prenom"] != "_PRENOMS_RARES"] df_cache["longueur"] = df_cache["prenom"].str.len() # Précalcul all-time total (utilisé par /api/top) global alltime_cache alltime_cache = df_cache.groupby(["prenom", "sexe"])["valeur"].sum().reset_index() alltime_cache.columns = ["prenom", "sexe", "total"] # Build autocomplete index: prenom -> total all-time, sexe majority agg = df_cache.groupby(["prenom", "sexe"])["valeur"].sum().reset_index() totals = agg.groupby("prenom")["valeur"].sum().reset_index() totals.columns = ["prenom", "total"] # dominant sexe dom = agg.loc[agg.groupby("prenom")["valeur"].idxmax(), ["prenom","sexe"]] merged = totals.merge(dom, on="prenom") autocomplete_cache = merged.sort_values("total", ascending=False).reset_index(drop=True) return df_cache def get_autocomplete(): get_df() return autocomplete_cache @app.get("/api/status") def status(): if os.path.exists(DATA_PATH): try: df = get_df() return {"ok": True, "rows": len(df), "prenoms": df["prenom"].nunique()} except Exception as e: return {"ok": False, "error": str(e)} return {"ok": False, "error": "Fichier manquant"} @app.get("/api/autocomplete") def autocomplete(q: str, limit: int = 8): ac = get_autocomplete() prefix = q.upper().strip() if not prefix: return [] matches = ac[ac["prenom"].str.startswith(prefix)].head(limit) return matches.to_dict(orient="records") @app.get("/api/evolution/{prenom}") def evolution(prenom: str): df = get_df() prenom = prenom.upper() sub = df[df["prenom"] == prenom].groupby(["periode", "sexe"])["valeur"].sum().reset_index() if sub.empty: raise HTTPException(status_code=404, detail=f"Prénom '{prenom}' introuvable") return sub.to_dict(orient="records") @app.get("/api/compare") def compare(a: str, b: str): df = get_df() result = {} for prenom in [a.upper(), b.upper()]: sub = df[df["prenom"] == prenom].groupby(["periode", "sexe"])["valeur"].sum().reset_index() result[prenom] = sub.to_dict(orient="records") return result @app.get("/api/heatmap/{prenom}") def heatmap(prenom: str): """Returns total births per decade per sexe""" df = get_df() prenom = prenom.upper() sub = df[df["prenom"] == prenom].copy() if sub.empty: raise HTTPException(status_code=404, detail=f"Prénom '{prenom}' introuvable") sub["decade"] = (sub["periode"] // 10) * 10 agg = sub.groupby(["decade", "sexe"])["valeur"].sum().reset_index() return agg.to_dict(orient="records") @app.get("/api/search") def search( q: Optional[str] = None, sexe: Optional[int] = None, longueur_min: int = 1, longueur_max: int = 20, popularite_min: int = 0, popularite_max: int = 10000000, peak_min: Optional[int] = None, peak_max: Optional[int] = None, periode_min: int = 1900, periode_max: int = 2024, limit: int = 50 ): df = get_df() # Total all-time (pour popularité et affichage) agg_all = df.groupby(["prenom", "sexe", "longueur"])["valeur"].sum().reset_index() agg_all.columns = ["prenom", "sexe", "longueur", "total"] # Total sur la période choisie (pour filtrer les prénoms actifs sur cette période) sub = df[(df["periode"] >= periode_min) & (df["periode"] <= periode_max)] agg_period = sub.groupby(["prenom", "sexe"])["valeur"].sum().reset_index() agg_period.columns = ["prenom", "sexe", "total_periode"] # Garder seulement les prénoms présents sur la période (au moins 1 naissance) agg = agg_all.merge(agg_period, on=["prenom", "sexe"], how="inner") if sexe is not None: agg = agg[agg["sexe"] == sexe] agg = agg[(agg["longueur"] >= longueur_min) & (agg["longueur"] <= longueur_max)] if q: agg = agg[agg["prenom"].str.startswith(q.upper())] # Popularité filtrée sur total all-time agg = agg[(agg["total"] >= popularite_min) & (agg["total"] <= popularite_max)] agg = agg.sort_values("total_periode", ascending=False).head(limit) return agg.to_dict(orient="records") @app.get("/api/top") def top( sexe: Optional[int] = None, periode_min: int = 1900, periode_max: int = 2024, order: str = "desc", page: int = 1, per_page: int = 50 ): df = get_df() # assure que alltime_cache est initialisé # Total sur la période (pour tri) sub = df[(df["periode"] >= periode_min) & (df["periode"] <= periode_max)] agg_period = sub.groupby(["prenom", "sexe"])["valeur"].sum().reset_index() agg_period.columns = ["prenom", "sexe", "total_periode"] # Joindre avec all-time (précalculé) agg = alltime_cache.merge(agg_period, on=["prenom","sexe"], how="inner") if sexe is not None: agg = agg[agg["sexe"] == sexe] agg = agg.sort_values("total_periode", ascending=(order=="asc")) total_count = len(agg) offset = (page - 1) * per_page page_data = agg.iloc[offset:offset+per_page].copy() return {"total": total_count, "page": page, "per_page": per_page, "data": page_data.to_dict(orient="records")} @app.get("/api/stats") def stats(): df = get_df() return { "total_naissances": int(df["valeur"].sum()), "nb_prenoms": int(df["prenom"].nunique()), "periode_min": int(df["periode"].min()), "periode_max": int(df["periode"].max()), }