Files
prenoms-app/main.py
T
2026-03-14 09:33:12 +01:00

169 lines
6.1 KiB
Python

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