Spaces:
Running
Running
Download src/modules/ml_dashboard.py from klydekushy/Vortex-Flux: direct link, hf CLI and curl.
- Browser
- Download file 30.8 kB
-
https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/ml_dashboard.py
- Command line
-
hf download hf://spaces/klydekushy/Vortex-Flux/src/modules/ml_dashboard.py
-
curl -L -o ml_dashboard.py https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/ml_dashboard.py
30.8 kB
| import streamlit as st | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from datetime import datetime | |
| from modules.jasmine_widget import render_scoped_widget | |
| from Analytics.vortex_params import get_objectif_annuel | |
| from core.sheets_cache import get_cached_dfs_batch, get_cached_df | |
| from core import exercice as ex | |
| def show_ml_features(client, sheet_name): | |
| # Fonction helper pour charger les données (mise en cache, 1 appel par feuille) | |
| def get_data_from_sheet(sheet_name_tab): | |
| try: | |
| sh = client.open(sheet_name) | |
| return get_cached_df(sh, sheet_name, sheet_name_tab) | |
| except Exception as e: | |
| from core.errors import log_swallowed | |
| log_swallowed(f"Chargement de l'onglet '{sheet_name_tab}' echoue (tableau de bord traite comme vide)", e) | |
| st.warning(f"[ATTENTION] Impossible de charger l'onglet « {sheet_name_tab} » — les indicateurs ci-dessous peuvent être incomplets.") | |
| return pd.DataFrame() | |
| # Chargement des 4 feuilles necessaires EN UN SEUL appel API groupe (au lieu | |
| # de 4 separes) ; si l'une d'elles manque, le groupe entier echoue et on | |
| # retombe sur le chargement feuille par feuille (resilient, comme avant). | |
| _noms = ["Prets_Master", "Prets_Update", "Capital_Investissement", "Clients_KYC", "Remboursements"] | |
| try: | |
| sh = client.open(sheet_name) | |
| _dfs = get_cached_dfs_batch(sh, sheet_name, _noms) | |
| df_prets_master, df_prets_update, df_capital_invest, df_clients, df_remb = ( | |
| _dfs[n] for n in _noms | |
| ) | |
| except Exception: | |
| df_prets_master = get_data_from_sheet("Prets_Master") | |
| df_prets_update = get_data_from_sheet("Prets_Update") | |
| df_capital_invest = get_data_from_sheet("Capital_Investissement") | |
| df_clients = get_data_from_sheet("Clients_KYC") | |
| df_remb = get_data_from_sheet("Remboursements") | |
| # Exercice choisi dans la barre laterale (None = toutes annees). Regle A : | |
| # benefice = interets + penalites ENCAISSES dans l'annee (Date_Paiement), | |
| # capital sorti = prets debloques dans l'annee (Date_Deblocage). | |
| annee_sel = ex.exercice_selectionne() | |
| annee_courante = annee_sel or ex.exercice_courant() | |
| libelle_annee = str(annee_sel) if annee_sel else "TOUTES ANNÉES" | |
| # === En-tête === | |
| st.markdown("<h1>OBJECT EXPLORER</h1>", unsafe_allow_html=True) | |
| # Base du benefice choisie dans la barre laterale (partagee avec Optimizer, | |
| # Digital Twin, rapports IA et JASMINE) : encaisse ou accorde. | |
| base_benefice = ex.base_benefice() | |
| mode_accorde = ex.est_base_accordee(base_benefice) | |
| st.caption(ex.definition_base(base_benefice)) | |
| # === VÉRIFICATION RÉELLE DE LA LIAISON === | |
| # On s'assure que la table principale a bien été trouvée et contient des données | |
| if not df_prets_master.empty: | |
| # Message de succès "Tactique" avec auto-destruction CSS (Fade out après 5s) | |
| st.markdown(f""" | |
| <style> | |
| @keyframes tacticalFadeOut {{ | |
| 0% {{ opacity: 1; max-height: 50px; margin-bottom: 16px; padding: 12px 16px; border-width: 1px; }} | |
| 80% {{ opacity: 1; max-height: 50px; margin-bottom: 16px; padding: 12px 16px; border-width: 1px; }} | |
| 100% {{ opacity: 0; max-height: 0px; margin-bottom: 0px; padding: 0px; border-width: 0px; overflow: hidden; }} | |
| }} | |
| .tactical-toast {{ | |
| background: rgba(84, 189, 75, 0.08); | |
| border: 1px solid rgba(84, 189, 75, 0.5); | |
| border-left: 4px solid #54bd4b; | |
| color: #54bd4b; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| animation: tacticalFadeOut 10s forwards; | |
| }} | |
| </style> | |
| <div class="tactical-toast"> | |
| [OK] LIAISON DIRECTE ÉTABLIE : {sheet_name} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| # Message d'erreur fixe si la feuille est introuvable ou vide | |
| st.markdown(f""" | |
| <div style="background: rgba(192, 57, 43, 0.08); border: 1px solid rgba(192, 57, 43, 0.3); border-left: 4px solid #c0392b; padding: 12px 16px; margin-bottom: 16px; font-family: 'JetBrains Mono', monospace;"> | |
| <div style="color: #c0392b; font-weight: 800; font-size: 0.75rem; letter-spacing: 1px; text-transform: uppercase;"> | |
| [X] ÉCHEC DE LIAISON : AUCUNE DONNÉE DANS 'Prets_Master' | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # === CALCULS POUR LES MÉTRIQUES === | |
| # 1. Capital de l'entreprise (somme de Capital_Investissement) | |
| capital_entreprise = 0 | |
| capital_variation = 0 | |
| if not df_capital_invest.empty and 'Capital' in df_capital_invest.columns: | |
| df_capital_invest['Capital_Num'] = pd.to_numeric( | |
| df_capital_invest['Capital'].astype(str).str.replace('FCFA', '').str.replace(' ', '').str.strip(), | |
| errors='coerce' | |
| ).fillna(0) | |
| capital_entreprise = df_capital_invest['Capital_Num'].sum() | |
| # Variation = dernier vs précédent | |
| if len(df_capital_invest) > 1: | |
| derniere_entree = df_capital_invest.iloc[-1]['Capital_Num'] | |
| avant_derniere = df_capital_invest.iloc[-2]['Capital_Num'] | |
| if avant_derniere > 0: | |
| capital_variation = ((derniere_entree - avant_derniere) / avant_derniere) * 100 | |
| # 2. Capital dehors (prêts non remboursés) | |
| capital_dehors = 0 | |
| if not df_prets_master.empty and 'Montant_Capital' in df_prets_master.columns: | |
| df_prets_master['Montant_Capital_Num'] = pd.to_numeric(df_prets_master['Montant_Capital'], errors='coerce').fillna(0) | |
| capital_dehors = df_prets_master[df_prets_master['Statut'] != 'TERMINE']['Montant_Capital_Num'].sum() | |
| # 3. Flux attendu (Montant_Total des prêts non remboursés) | |
| flux_attendu = 0 | |
| flux_variation = 0 | |
| if not df_prets_master.empty and 'Montant_Total' in df_prets_master.columns: | |
| df_prets_master['Montant_Total_Num'] = pd.to_numeric(df_prets_master['Montant_Total'], errors='coerce').fillna(0) | |
| flux_attendu = df_prets_master[df_prets_master['Statut'] != 'TERMINE']['Montant_Total_Num'].sum() | |
| # Variation = différence avec capital dehors | |
| if capital_dehors > 0: | |
| flux_variation = ((flux_attendu - capital_dehors) / capital_dehors) * 100 | |
| # 4. Score de liquidité | |
| score_liquidite = 0 | |
| score_tendance = "Stable" | |
| if capital_entreprise > 0: | |
| score_liquidite = (capital_dehors / capital_entreprise) * 10 | |
| score_liquidite = min(score_liquidite, 10) | |
| # Tendance basée sur le ratio | |
| ratio = (capital_dehors / capital_entreprise) * 100 | |
| if ratio > 70: | |
| score_tendance = "▲ Élevé" | |
| elif ratio > 40: | |
| score_tendance = "→ Moyen" | |
| else: | |
| score_tendance = "▼ Faible" | |
| # 5. Benefice de l'exercice (selon la base choisie) et comparaison a N-1 a la meme date | |
| encaisse_par_annee = ex.encaissements_par_exercice(df_remb) | |
| benefices_nets = ex.benefice_exercice(df_prets_master, df_remb, annee_sel, base_benefice) | |
| benefices_variation = 0 | |
| benefice_n1_meme_date = None | |
| if annee_sel: | |
| from datetime import date as _date | |
| aujourdhui = _date.today() | |
| limite = aujourdhui.replace(year=annee_sel - 1) if annee_sel == aujourdhui.year and not (aujourdhui.month == 2 and aujourdhui.day == 29) else None | |
| benefice_n1_meme_date = ex.benefice_exercice(df_prets_master, df_remb, annee_sel - 1, base_benefice, jusqu_au=limite) | |
| if benefice_n1_meme_date > 0: | |
| benefices_variation = (benefices_nets - benefice_n1_meme_date) / benefice_n1_meme_date * 100 | |
| synthese_n = ex.synthese_exercice(df_prets_master, df_remb, annee_courante) | |
| synthese_n1 = ex.synthese_exercice(df_prets_master, df_remb, annee_courante - 1) | |
| # 6. Objectif de l'exercice (source de verite unique, voir Optimizer) | |
| objectif_annuel = get_objectif_annuel(client, sheet_name, annee=annee_courante) | |
| progression_objectif = (benefices_nets / objectif_annuel) * 100 if objectif_annuel > 0 else 0 | |
| # 7. Reste à générer | |
| reste_a_generer = max(0, objectif_annuel - benefices_nets) | |
| reste_variation = -((reste_a_generer / objectif_annuel) * 100) if objectif_annuel > 0 else 0 | |
| # 8. Capital total sorti (prets debloques dans l'exercice) | |
| def _capital_sorti(master, update): | |
| """(capital, nb prets) : Master hors UPDATED + versions Prets_Update | |
| des prets UPDATED + prets presents uniquement dans Prets_Update.""" | |
| if master.empty or 'Montant_Capital' not in master.columns: | |
| return 0, 0 | |
| master = master.assign(Montant_Capital_Num=pd.to_numeric(master['Montant_Capital'], errors='coerce').fillna(0)) | |
| non_updated = master[master['Statut'] != 'UPDATED'] | |
| capital, nb = non_updated['Montant_Capital_Num'].sum(), len(non_updated) | |
| if not update.empty and 'ID_Pret' in update.columns: | |
| update = update.assign(Montant_Capital_Num=pd.to_numeric(update['Montant_Capital'], errors='coerce').fillna(0)) | |
| ids_updated = master.loc[master['Statut'] == 'UPDATED', 'ID_Pret'].tolist() | |
| versions = update[update['ID_Pret'].isin(ids_updated)] | |
| nouveaux = update[~update['ID_Pret'].isin(master['ID_Pret'].tolist())] | |
| capital += versions['Montant_Capital_Num'].sum() + nouveaux['Montant_Capital_Num'].sum() | |
| nb += len(versions) + len(nouveaux) | |
| return capital, nb | |
| master_ex = ex.filtrer_annee(df_prets_master, 'Date_Deblocage', annee_sel) | |
| if annee_sel and 'ID_Pret' in master_ex.columns and not df_prets_update.empty and 'ID_Pret' in df_prets_update.columns: | |
| # Versions des prets du millesime + prets debloques dans l'exercice presents seulement dans Prets_Update. | |
| ids_ex = set(master_ex['ID_Pret'].astype(str)) | |
| dans_millesime = df_prets_update['ID_Pret'].astype(str).isin(ids_ex) | |
| if 'Date_Deblocage' in df_prets_update.columns: | |
| dans_millesime |= ex.annee_de(df_prets_update['Date_Deblocage']).eq(annee_sel).fillna(False).to_numpy() | |
| update_ex = df_prets_update[dans_millesime] | |
| else: | |
| update_ex = df_prets_update | |
| capital_total_sorti, nb_prets_total = _capital_sorti(master_ex, update_ex) | |
| # === AFFICHAGE DES MÉTRIQUES (2 lignes de 4) === | |
| # Ligne 1 | |
| col1, col2, col3, col4 = st.columns(4) | |
| col1.metric( | |
| "CAPITAL D'INVESTISSEMENT", | |
| f"{capital_entreprise:,.0f} XOF".replace(',', ' '), | |
| delta=f"{'▲' if capital_variation > 0 else '▼'} {abs(capital_variation):.1f}%" if capital_variation != 0 else "Stable" | |
| ) | |
| col2.metric( | |
| "CAPITAL DEHORS", | |
| f"{capital_dehors:,.0f} XOF".replace(',', ' '), | |
| delta=f"{(capital_dehors/capital_entreprise*100 if capital_entreprise > 0 else 0):.1f}% du capital investi" | |
| ) | |
| col3.metric( | |
| "FLUX ATTENDU", | |
| f"{flux_attendu:,.0f} XOF".replace(',', ' '), | |
| delta=f"{'▲' if flux_variation > 0 else '▼'} {abs(flux_variation):.1f}% vs capital" if flux_variation != 0 else "Stable" | |
| ) | |
| col4.metric( | |
| "LIQUIDITÉ UTILISÉE", | |
| f"{score_liquidite:.1f}/10", | |
| delta=score_tendance | |
| ) | |
| # Ligne 2 | |
| col5, col6, col7, col8 = st.columns(4) | |
| col5.metric( | |
| f"BÉNÉFICES {'ACCORDÉS' if mode_accorde else 'ENCAISSÉS'} {libelle_annee}", | |
| f"{benefices_nets:,.0f} XOF".replace(',', ' '), | |
| delta=(f"{benefices_variation:+.1f}% vs {annee_courante - 1} à date" if benefice_n1_meme_date else "Pas de N-1 comparable"), | |
| help=("Coût du crédit des prêts débloqués dans l'exercice (date de déblocage)." if mode_accorde | |
| else "Intérêts + pénalités encaissés dans l'exercice (date de paiement)."), | |
| ) | |
| col6.metric( | |
| f"OBJECTIF {annee_courante}", | |
| f"{objectif_annuel:,.0f} XOF".replace(',', ' '), | |
| delta=f"{progression_objectif:.1f}% atteint" | |
| ) | |
| col7.metric( | |
| "RESTE À GÉNÉRER", | |
| f"{reste_a_generer:,.0f} XOF".replace(',', ' '), | |
| delta=f"[BAS] {abs(reste_variation):.1f}%" if reste_a_generer > 0 else "[OK] Objectif atteint" | |
| ) | |
| col8.metric( | |
| f"CAPITAL SORTI {libelle_annee}", | |
| f"{capital_total_sorti:,.0f} XOF".replace(',', ' '), | |
| delta=f"{nb_prets_total} prêt{'s' if nb_prets_total > 1 else ''} tot{'aux' if nb_prets_total > 1 else 'al'}" | |
| ) | |
| # Répartition mensuelle (bénéfices, volume, capital sorti) — sans ça JASMINE | |
| # ne peut répondre à aucune question temporelle ("meilleur mois", tendance...). | |
| repartition_mensuelle = [] | |
| if not df_prets_master.empty and 'Date_Deblocage' in df_prets_master.columns: | |
| df_mois = ex.filtrer_annee(df_prets_master, 'Date_Deblocage', annee_sel) | |
| df_mois['_date_debloc'] = ex.parser_dates(df_mois['Date_Deblocage']) if not df_mois.empty else pd.Series(dtype='datetime64[ns]') | |
| df_mois = df_mois.dropna(subset=['_date_debloc']) | |
| if not df_mois.empty: | |
| df_mois['_mois'] = df_mois['_date_debloc'].dt.strftime('%Y-%m') | |
| df_mois['_cout_credit_num'] = pd.to_numeric(df_mois.get('Cout_Credit', 0), errors='coerce').fillna(0) | |
| df_mois['_montant_capital_num'] = pd.to_numeric(df_mois.get('Montant_Capital', 0), errors='coerce').fillna(0) | |
| agg = ( | |
| df_mois.groupby('_mois') | |
| .agg( | |
| nb_prets=('_cout_credit_num', 'size'), | |
| benefices_xof=('_cout_credit_num', 'sum'), | |
| capital_sorti_xof=('_montant_capital_num', 'sum'), | |
| ) | |
| .reset_index() | |
| .rename(columns={'_mois': 'mois'}) | |
| .sort_values('mois') | |
| ) | |
| repartition_mensuelle = agg.to_dict('records') | |
| render_scoped_widget( | |
| client, sheet_name, | |
| scope_label="Vue d'ensemble des indicateurs financiers (capital, liquidité, bénéfices)", | |
| context_data={ | |
| "capital_entreprise_xof": capital_entreprise, | |
| "capital_dehors_xof": capital_dehors, | |
| "flux_attendu_xof": flux_attendu, | |
| "score_liquidite": score_liquidite, | |
| "exercice_analyse": annee_sel or "toutes annees", | |
| "base_benefice_affichee": base_benefice, | |
| "benefices_nets_xof": benefices_nets, | |
| "benefice_n_moins_1_meme_date_xof": benefice_n1_meme_date, | |
| "objectif_annuel_xof": objectif_annuel, | |
| "synthese_exercice": synthese_n, | |
| "synthese_exercice_precedent": synthese_n1, | |
| "benefice_encaisse_par_exercice": encaisse_par_annee.to_dict("records"), | |
| "reste_a_generer_xof": reste_a_generer, | |
| "capital_total_sorti_xof": capital_total_sorti, | |
| "nb_prets_total": nb_prets_total, | |
| "repartition_mensuelle_nb_prets_benefices_capital_sorti": repartition_mensuelle, | |
| "benefice_exercice_si_base_encaissee_xof": ex.benefice_exercice(df_prets_master, df_remb, annee_sel, ex.BASE_ENCAISSE), | |
| "benefice_exercice_si_base_accordee_xof": ex.benefice_exercice(df_prets_master, df_remb, annee_sel, ex.BASE_ACCORDE), | |
| "guide_controles": ( | |
| "Cette page n'a pas de réglage propre : l'exercice analysé et la BASE DU BÉNÉFICE (Encaissé ou Accordé) se " | |
| "choisissent dans la barre latérale et recalculent tous les chiffres de la page. Passer d'une base à l'autre " | |
| "change le bénéfice (voir benefice_exercice_si_base_encaissee_xof et benefice_exercice_si_base_accordee_xof) " | |
| "mais pas le capital, la liquidité ni le flux attendu. Changer d'exercice change tout ce qui est daté. " | |
| "Le bouton ACTUALISER LES DONNÉES de la barre latérale recharge les feuilles Google Sheets." | |
| ), | |
| "guide_page": ( | |
| "Vue d'ensemble financière de l'entreprise (OBJECT EXPLORER) : capital disponible, " | |
| "capital actuellement prêté, bénéfices générés et progression vers l'objectif annuel." | |
| ), | |
| "definitions_champs": { | |
| "capital_entreprise_xof": "Capital total investi dans l'entreprise (somme de la feuille Capital_Investissement).", | |
| "capital_dehors_xof": "Somme des capitaux (Montant_Capital) des prêts NON terminés — argent actuellement prêté aux clients.", | |
| "flux_attendu_xof": "Somme des montants totaux (capital+intérêts) des prêts non terminés — ce qui doit encore rentrer en caisse.", | |
| "score_liquidite": "Ratio capital_dehors / capital_entreprise x 10 (plafonné à 10) — plus il est élevé, plus une grande part du capital de l'entreprise est immobilisée en prêts en cours.", | |
| "exercice_analyse": "Exercice (annee civile) analyse. Chaque flux est rattache a l'annee de SA date : deblocage -> annee de deblocage, paiement -> annee de paiement. Ne jamais melanger les annees.", | |
| "base_benefice_affichee": "Base choisie par l'utilisateur pour le benefice de la page : 'Encaissé' = interets + penalites recus, au mois du paiement (base officielle de l'objectif) ; 'Accordé' = cout du credit des prets, au mois du deblocage (activite commerciale).", | |
| "benefices_nets_xof": "Benefice de l'exercice selon base_benefice_affichee.", | |
| "benefice_n_moins_1_meme_date_xof": "Benefice encaisse l'annee precedente a la meme date (comparaison a periode egale).", | |
| "objectif_annuel_xof": "Objectif de benefice de l'exercice fixe par le gerant (meme valeur que dans le module Optimizer).", | |
| "synthese_exercice": "Chiffres cles de l'exercice : " + "; ".join(f"{k} = {v}" for k, v in ex.DEFINITIONS_SYNTHESE.items()), | |
| "synthese_exercice_precedent": "Memes chiffres pour l'exercice precedent.", | |
| "benefice_encaisse_par_exercice": "Benefice encaisse (interets + penalites) de chaque exercice de l'historique.", | |
| "reste_a_generer_xof": "Objectif de l'exercice moins benefice deja encaisse (0 si deja atteint).", | |
| "capital_total_sorti_xof": "Capital decaisse sur les prets debloques dans l'exercice (Master + versions mises a jour).", | |
| "repartition_mensuelle_nb_prets_benefices_capital_sorti": "Detail mois par mois des prets debloques dans l'exercice : nombre de prets, cout du credit contractuel et capital sorti.", | |
| }, | |
| }, | |
| key_prefix="dashboard", | |
| ) | |
| #======> ANALYSE MENSUELLE DES BÉNÉFICES <===== | |
| st.divider() | |
| st.markdown(f"<h2>ANALYSE MENSUELLE DES BÉNÉFICES {annee_courante} — {'ACCORDÉS' if mode_accorde else 'ENCAISSÉS'}</h2>", unsafe_allow_html=True) | |
| # Préparation des données mensuelles (base choisie : mois du paiement ou du deblocage) | |
| df_remb_n, col_date_benef = ex.lignes_benefice(df_prets_master, df_remb, annee_courante, base_benefice) | |
| if col_date_benef in df_remb_n.columns: | |
| df_remb_n['Mois'] = ex.parser_dates(df_remb_n[col_date_benef]).dt.month | |
| df_remb_n['Benefice'] = ex.valeurs_benefice(df_remb_n, base_benefice) | |
| df_2026 = ex.filtrer_annee(df_prets_master, 'Date_Deblocage', annee_courante) | |
| df_2026['Mois'] = ex.parser_dates(df_2026['Date_Deblocage']).dt.month if not df_2026.empty else pd.Series(dtype=float) | |
| if not df_remb_n.empty: | |
| # CRÉER d'abord benefices_mensuels | |
| benefices_mensuels = df_remb_n.groupby('Mois')['Benefice'].sum().reset_index() | |
| benefices_mensuels.columns = ['Mois', 'Benefice'] | |
| # Ajouter les noms de mois | |
| mois_noms = {1: 'Janvier', 2: 'Février', 3: 'Mars', 4: 'Avril', | |
| 5: 'Mai', 6: 'Juin', 7: 'Juillet', 8: 'Août', | |
| 9: 'Septembre', 10: 'Octobre', 11: 'Novembre', 12: 'Décembre'} | |
| benefices_mensuels['Mois_Nom'] = benefices_mensuels['Mois'].map(mois_noms) | |
| # Calculer les variations mensuelles (% vs mois précédent) | |
| benefices_mensuels['Variation'] = benefices_mensuels['Benefice'].pct_change() * 100 | |
| benefices_mensuels['Variation'] = benefices_mensuels['Variation'].fillna(0) | |
| # Calculer le capital mensuel sorti (Montant_Capital) | |
| df_2026['Montant_Capital_Num'] = pd.to_numeric(df_2026.get('Montant_Capital', 0), errors='coerce').fillna(0) | |
| capital_mensuel = df_2026.groupby('Mois')['Montant_Capital_Num'].sum().reset_index() | |
| capital_mensuel.columns = ['Mois', 'Capital_Sorti'] | |
| # Fusionner avec les bénéfices mensuels | |
| benefices_mensuels = benefices_mensuels.merge(capital_mensuel, on='Mois', how='left') | |
| benefices_mensuels['Capital_Sorti'] = benefices_mensuels['Capital_Sorti'].fillna(0) | |
| # === GRAPHIQUE EN BARRES (Police Space Grotesk via CSS Global) === | |
| fig_mensuel = go.Figure() | |
| # Barres des bénéfices | |
| fig_mensuel.add_trace(go.Bar( | |
| x=benefices_mensuels['Mois_Nom'], | |
| y=benefices_mensuels['Benefice'], | |
| name='Bénéfices', | |
| marker=dict( | |
| color='#58a6ff', | |
| line=dict(color='rgba(88, 166, 255, 0.4)', width=1) | |
| ), | |
| text=benefices_mensuels['Benefice'].apply(lambda x: f"{x:,.0f}".replace(',', ' ')), | |
| textposition='outside', | |
| textfont=dict(size=11, color='#c9d1d9', family='Space Grotesk'), | |
| opacity=0.9, | |
| hovertemplate='<b>%{x}</b><br>Bénéfice: %{y:,.0f} XOF<extra></extra>' | |
| )) | |
| # Barres du capital sorti (Tactical Red) | |
| fig_mensuel.add_trace(go.Bar( | |
| x=benefices_mensuels['Mois_Nom'], | |
| y=benefices_mensuels['Capital_Sorti'], | |
| name='Capital Sorti', | |
| marker=dict( | |
| color='#c0392b', | |
| line=dict(color='rgba(192, 57, 43, 0.4)', width=1) | |
| ), | |
| text=benefices_mensuels['Capital_Sorti'].apply(lambda x: f"{x:,.0f}".replace(',', ' ')), | |
| textposition='outside', | |
| textfont=dict(size=11, color='#c9d1d9', family='Space Grotesk'), | |
| opacity=0.9, | |
| hovertemplate='<b>%{x}</b><br>Capital Sorti: %{y:,.0f} XOF<extra></extra>' | |
| )) | |
| # Mise en forme Gotham (Fond sombre, grille discrète) | |
| fig_mensuel.update_layout( | |
| title={ | |
| 'text': f'ÉVOLUTION DES BÉNÉFICES ET CAPITAL SORTI MENSUELS ({annee_courante})', | |
| 'font': {'size': 14, 'color': '#8b949e', 'family': 'Space Grotesk'}, | |
| 'x': 0, | |
| 'xanchor': 'left' | |
| }, | |
| plot_bgcolor='rgba(10, 14, 18, 0.9)', | |
| paper_bgcolor='rgba(0, 0, 0, 0)', | |
| font={'color': '#8b949e', 'family': 'Space Grotesk', 'size': 12}, | |
| xaxis={ | |
| 'title': '', | |
| 'gridcolor': 'rgba(50, 65, 80, 0.25)', | |
| 'linecolor': 'rgba(80, 100, 120, 0.3)', | |
| 'tickfont': {'family': 'Space Grotesk', 'size': 12}, | |
| 'tickangle': -45 | |
| }, | |
| yaxis={ | |
| 'title': { | |
| 'text': 'Montant (XOF)', | |
| 'font': {'size': 12, 'family': 'Space Grotesk'} | |
| }, | |
| 'gridcolor': 'rgba(50, 65, 80, 0.25)', | |
| 'linecolor': 'rgba(80, 100, 120, 0.3)', | |
| 'tickfont': {'family': 'Space Grotesk', 'size': 11} | |
| }, | |
| barmode='group', | |
| showlegend=True, | |
| legend=dict( | |
| orientation='h', | |
| yanchor='bottom', | |
| y=1.02, | |
| xanchor='right', | |
| x=1, | |
| font={'family': 'Space Grotesk', 'size': 11, 'color': '#a8b8c8'}, | |
| bgcolor='rgba(10, 14, 18, 0.7)', | |
| bordercolor='rgba(80, 100, 120, 0.3)', | |
| borderwidth=1 | |
| ), | |
| height=450, | |
| margin=dict(t=80, b=80, l=60, r=20) | |
| ) | |
| st.plotly_chart(fig_mensuel, use_container_width=True) | |
| # === TREEMAP DES VARIATIONS MENSUELLES === | |
| st.markdown("<h3 style='font-size: 1rem; color: #58a6ff; margin-top: 32px; margin-bottom: 16px; font-family: JetBrains Mono; letter-spacing: 2px; text-transform: uppercase;'>Répartition mensuelle des bénéfices</h3>", unsafe_allow_html=True) | |
| df_treemap = benefices_mensuels[benefices_mensuels['Benefice'] > 0].copy() | |
| if not df_treemap.empty: | |
| total_benefices = df_treemap['Benefice'].sum() | |
| df_treemap['Pourcentage'] = (df_treemap['Benefice'] / total_benefices * 100).round(1) | |
| df_treemap['Label'] = df_treemap.apply( | |
| lambda row: f"{row['Mois_Nom']}<br>{row['Pourcentage']:.1f}%", | |
| axis=1 | |
| ) | |
| fig_treemap = go.Figure(go.Treemap( | |
| labels=df_treemap['Label'], | |
| parents=[""] * len(df_treemap), | |
| values=df_treemap['Benefice'], | |
| text=df_treemap['Benefice'].apply(lambda x: f"{x:,.0f} XOF".replace(',', ' ')), | |
| textposition="middle center", | |
| textfont=dict(size=12, color='#ffffff', family='Space Grotesk', weight=600), | |
| marker=dict( | |
| colors=df_treemap['Benefice'], | |
| colorscale=[ | |
| [0, '#161b22'], # Très sombre | |
| [0.2, '#21262d'], # Anthracite | |
| [0.4, '#388bfd'], # Bleu moyen | |
| [0.6, '#58a6ff'], # Bleu classique | |
| [0.8, '#79c0ff'], # Bleu clair | |
| [1, '#a5d6ff'] # Très clair | |
| ], | |
| line=dict(color='#0d1117', width=1.5), | |
| showscale=False | |
| ), | |
| hovertemplate='<b>%{label}</b><br>Bénéfice: %{value:,.0f} XOF<br>Part: %{percentParent}<extra></extra>' | |
| )) | |
| fig_treemap.update_layout( | |
| plot_bgcolor='rgba(10, 14, 18, 0.9)', | |
| paper_bgcolor='rgba(0, 0, 0, 0)', | |
| font={'color': '#ffffff', 'family': 'Space Grotesk', 'size': 11}, | |
| height=350, | |
| margin=dict(t=10, b=10, l=0, r=0) | |
| ) | |
| st.plotly_chart(fig_treemap, use_container_width=True) | |
| # Légende des variations sous le treemap | |
| st.markdown("<h4 style='font-size: 0.8rem; color: #8b949e; margin-top: 24px; margin-bottom: 12px; font-family: JetBrains Mono; text-transform: uppercase;'>Variations mensuelles tactiques</h4>", unsafe_allow_html=True) | |
| # Afficher les variations en grille 3 colonnes (Style Tactical Block) | |
| variations_cols = st.columns(3) | |
| for idx, row in df_treemap.iterrows(): | |
| col_index = idx % 3 | |
| with variations_cols[col_index]: | |
| variation = row['Variation'] | |
| mois_nom = row['Mois_Nom'][:3].upper() | |
| # Couleurs militaires strictes | |
| if variation > 0: | |
| couleur = "#54bd4b" | |
| symbole = "▲" | |
| elif variation < 0: | |
| couleur = "#c0392b" | |
| symbole = "▼" | |
| else: | |
| couleur = "#8b949e" | |
| symbole = "→" | |
| # Utilisation exclusive de JetBrains Mono et border-radius: 0 | |
| st.markdown(f""" | |
| <div style=' | |
| background: rgba(15, 20, 26, 0.85); | |
| border: 1px solid rgba(100, 120, 140, 0.25); | |
| border-left: 3px solid {couleur}; | |
| padding: 12px 14px; | |
| margin-bottom: 10px; | |
| border-radius: 0; | |
| font-family: "JetBrains Mono", monospace; | |
| font-size: 0.8rem; | |
| '> | |
| <div style='display: flex; justify-content: space-between; align-items: center;'> | |
| <span style='color: #8b949e; font-weight: 700; letter-spacing: 1px;'>{mois_nom}</span> | |
| <span style='color: {couleur}; font-weight: 800;'> | |
| {symbole} {abs(variation):.1f}% | |
| </span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Résumé | |
| st.divider() | |
| col_stat1, col_stat2, col_stat3 = st.columns(3) | |
| with col_stat1: | |
| st.metric(f"Total {annee_courante}", f"{total_benefices:,.0f} XOF".replace(',', ' ')) | |
| with col_stat2: | |
| mois_max = df_treemap.loc[df_treemap['Benefice'].idxmax(), 'Mois_Nom'].upper() | |
| montant_max = df_treemap['Benefice'].max() | |
| st.metric("Meilleur mois", mois_max, f"{montant_max:,.0f} XOF".replace(',', ' ')) | |
| with col_stat3: | |
| nb_mois_actifs = len(df_treemap) | |
| st.metric("Mois actifs", f"{nb_mois_actifs}/12") | |
| else: | |
| st.info("[INFO] Aucun bénéfice enregistré pour créer la visualisation") | |
| else: | |
| st.warning("[ATTENTION] Colonnes 'Date_Deblocage' ou 'Cout_Credit' introuvables dans Prets_Master") |