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

OBJECT EXPLORER

", 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"""
[OK] LIAISON DIRECTE ÉTABLIE : {sheet_name}
""", unsafe_allow_html=True) else: # Message d'erreur fixe si la feuille est introuvable ou vide st.markdown(f"""
[X] ÉCHEC DE LIAISON : AUCUNE DONNÉE DANS 'Prets_Master'
""", 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"

ANALYSE MENSUELLE DES BÉNÉFICES {annee_courante} — {'ACCORDÉS' if mode_accorde else 'ENCAISSÉS'}

", 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='%{x}
Bénéfice: %{y:,.0f} XOF' )) # 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='%{x}
Capital Sorti: %{y:,.0f} XOF' )) # 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("

Répartition mensuelle des bénéfices

", 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']}
{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='%{label}
Bénéfice: %{value:,.0f} XOF
Part: %{percentParent}' )) 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("

Variations mensuelles tactiques

", 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"""
{mois_nom} {symbole} {abs(variation):.1f}%
""", 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")