Vortex-Flux / src /modules /ml_dashboard.py
klydekushy's picture
Update src/modules/ml_dashboard.py
3962238 verified
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
import numpy as np
import time
def show_ml_features(client, sheet_name):
# Fonction helper pour charger les données
def get_data_from_sheet(sheet_name_tab):
try:
# Assurez-vous que l'objet 'client' et 'sheet_name' sont définis en amont dans votre script
sh = client.open(sheet_name)
ws = sh.worksheet(sheet_name_tab)
return pd.DataFrame(ws.get_all_records())
except:
return pd.DataFrame()
# Chargement des données nécessaires
# Note: 'client' et 'sheet_name' doivent être définis avant ce bloc
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")
# === En-tête ===
st.header("OBJECT EXPLORER")
# Note: 'sheet_name' doit être défini
st.success(f"✅ Liaison Directe établie")
# === 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. Bénéfices nets 2026
benefices_nets = 0
benefices_variation = 0
if not df_prets_master.empty and 'Cout_Credit' in df_prets_master.columns:
df_prets_master['Cout_Credit_Num'] = pd.to_numeric(df_prets_master['Cout_Credit'], errors='coerce').fillna(0)
benefices_nets = df_prets_master['Cout_Credit_Num'].sum()
# Simulation variation (peut être calculée vs mois précédent si date disponible)
benefices_variation = 5.2 # Placeholder - à calculer avec historique
# 6. Objectif 2026
objectif_2026 = 2_200_000
progression_objectif = (benefices_nets / objectif_2026) * 100 if objectif_2026 > 0 else 0
# 7. Reste à générer
reste_a_generer = max(0, objectif_2026 - benefices_nets)
reste_variation = -((reste_a_generer / objectif_2026) * 100) if objectif_2026 > 0 else 0
# 8. Capital total sorti
capital_total_sorti = 0
nb_prets_total = 0
if not df_prets_master.empty and 'Montant_Capital' in df_prets_master.columns:
# Convertir Montant_Capital en numérique si pas déjà fait
if 'Montant_Capital_Num' not in df_prets_master.columns:
df_prets_master['Montant_Capital_Num'] = pd.to_numeric(df_prets_master['Montant_Capital'], errors='coerce').fillna(0)
# Prêts NON UPDATED - on prend de Prets_Master
prets_non_updated = df_prets_master[df_prets_master['Statut'] != 'UPDATED']
nb_prets_total = len(prets_non_updated)
capital_total_sorti = prets_non_updated['Montant_Capital_Num'].sum()
# Prêts UPDATED - on prend depuis Prets_Update
if not df_prets_update.empty and 'ID_Pret' in df_prets_update.columns:
df_prets_update['Montant_Capital_Num'] = pd.to_numeric(df_prets_update['Montant_Capital'], errors='coerce').fillna(0)
prets_updated_ids = df_prets_master[df_prets_master['Statut'] == 'UPDATED']['ID_Pret'].tolist()
prets_updated_montants = df_prets_update[df_prets_update['ID_Pret'].isin(prets_updated_ids)]
capital_total_sorti += prets_updated_montants['Montant_Capital_Num'].sum()
nb_prets_total += len(prets_updated_montants)
# Ajouter aussi les prêts qui sont UNIQUEMENT dans Prets_Update (nouveaux prêts)
prets_update_nouveaux = df_prets_update[~df_prets_update['ID_Pret'].isin(df_prets_master['ID_Pret'].tolist())]
capital_total_sorti += prets_update_nouveaux['Montant_Capital_Num'].sum()
nb_prets_total += len(prets_update_nouveaux)
# Ajoutez le ration bénéfice net sur total sortie
# === 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(
"BÉNÉFICES NETS 2026",
f"{benefices_nets:,.0f} XOF".replace(',', ' '),
delta=f"▲ {benefices_variation:.1f}%" if benefices_variation > 0 else "Stable"
)
col6.metric(
"OBJECTIF 2026",
f"{objectif_2026:,.0f} XOF".replace(',', ' '),
delta=f"{progression_objectif:.1f}% atteint"
)
col7.metric(
"RESTE À GÉNÉRER",
f"{reste_a_generer:,.0f} XOF".replace(',', ' '),
delta=f"▼ {abs(reste_variation):.1f}%" if reste_a_generer > 0 else "✓ Objectif atteint !"
)
# Affichage de la métrique
col8.metric(
"CAPITAL TOTAL SORTI",
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'}"
)
#======> ANALYSE MENSUELLE DES BÉNÉFICES <=====
st.divider()
st.subheader("ANALYSE MENSUELLE DES BÉNÉFICES 2026")
# Préparation des données mensuelles
if not df_prets_master.empty and 'Date_Deblocage' in df_prets_master.columns and 'Cout_Credit' in df_prets_master.columns:
# Conversion des dates et montants
df_prets_master['Date_Deblocage_dt'] = pd.to_datetime(df_prets_master['Date_Deblocage'], format='%d/%m/%Y', errors='coerce')
df_prets_master['Cout_Credit_Num'] = pd.to_numeric(df_prets_master['Cout_Credit'], errors='coerce').fillna(0)
# Filtrer uniquement l'année 2026
df_2026 = df_prets_master[df_prets_master['Date_Deblocage_dt'].dt.year == 2026].copy()
if not df_2026.empty:
# Créer la colonne Mois (numérique)
df_2026['Mois'] = df_2026['Date_Deblocage_dt'].dt.month
# CRÉER d'abord benefices_mensuels
benefices_mensuels = df_2026.groupby('Mois')['Cout_Credit_Num'].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['Montant_Capital'], 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 groupées avec Plotly (pleine largeur)
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(139, 148, 158, 0.3)', width=1.5)
),
text=benefices_mensuels['Benefice'].apply(lambda x: f"{x:,.0f}".replace(',', ' ')),
textposition='outside',
textfont=dict(size=11, color='#c9d1d9', family='Space Grotesk'),
opacity=0.85,
hovertemplate='<b>%{x}</b><br>Bénéfice: %{y:,.0f} XOF<extra></extra>'
))
# Barres du capital sorti
fig_mensuel.add_trace(go.Bar(
x=benefices_mensuels['Mois_Nom'],
y=benefices_mensuels['Capital_Sorti'],
name='Capital Sorti',
marker=dict(
color='#f85149',
line=dict(color='rgba(139, 148, 158, 0.3)', width=1.5)
),
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.85,
hovertemplate='<b>%{x}</b><br>Capital Sorti: %{y:,.0f} XOF<extra></extra>'
))
# Mise en forme
fig_mensuel.update_layout(
title={
'text': 'Évolution des bénéfices et capital sorti mensuels (2026)',
'font': {'size': 16, 'color': '#c9d1d9', 'family': 'Space Grotesk'},
'x': 0,
'xanchor': 'left'
},
plot_bgcolor='rgba(13, 17, 23, 0.8)',
paper_bgcolor='rgba(22, 27, 34, 0.3)',
font={'color': '#8b949e', 'family': 'Space Grotesk', 'size': 12},
xaxis={
'title': '',
'gridcolor': 'rgba(48, 54, 61, 0.3)',
'linecolor': 'rgba(48, 54, 61, 0.5)',
'tickfont': {'family': 'Space Grotesk', 'size': 13},
'tickangle': -45
},
yaxis={
'title': {
'text': 'Montant (XOF)',
'font': {'size': 13, 'family': 'Space Grotesk'}
},
'gridcolor': 'rgba(48, 54, 61, 0.3)',
'linecolor': 'rgba(48, 54, 61, 0.5)',
'tickfont': {'family': 'Space Grotesk', 'size': 12}
},
barmode='group',
showlegend=True,
legend=dict(
orientation='h',
yanchor='bottom',
y=1.02,
xanchor='right',
x=1,
font={'family': 'Space Grotesk', 'size': 12, 'color': '#c9d1d9'}
),
height=550,
margin=dict(t=100, b=120, l=80, r=30)
)
# === TREEMAP DES VARIATIONS MENSUELLES ===
st.markdown("<h3 style='font-size: 1.1rem; color: #8b949e; margin-top: 32px; margin-bottom: 16px;'>Répartition mensuelle des bénéfices</h3>", unsafe_allow_html=True)
# Préparer les données pour le treemap (uniquement les mois avec bénéfices > 0)
df_treemap = benefices_mensuels[benefices_mensuels['Benefice'] > 0].copy()
if not df_treemap.empty:
# Ajouter les pourcentages
total_benefices = df_treemap['Benefice'].sum()
df_treemap['Pourcentage'] = (df_treemap['Benefice'] / total_benefices * 100).round(1)
# Créer des labels avec nom du mois + pourcentage
df_treemap['Label'] = df_treemap.apply(
lambda row: f"{row['Mois_Nom']}<br>{row['Pourcentage']:.1f}%",
axis=1
)
# Créer le treemap
fig_treemap = go.Figure(go.Treemap(
labels=df_treemap['Label'],
parents=[""] * len(df_treemap), # Tous au même niveau
values=df_treemap['Benefice'],
text=df_treemap['Benefice'].apply(lambda x: f"{x:,.0f} XOF".replace(',', ' ')),
textposition="middle center",
textfont=dict(size=11, color='#ffffff', family='Space Grotesk', weight=600),
marker=dict(
colors=df_treemap['Benefice'],
colorscale=[
[0, '#1c2128'], # Très faible - gris foncé
[0.2, '#30363d'], # Faible - gris
[0.4, '#58a6ff'], # Moyen - bleu principal
[0.6, '#79c0ff'], # Bon - bleu clair
[0.8, '#a5d6ff'], # Très bon - bleu très clair
[1, '#c9d1d9'] # Excellent - bleu blanc
],
line=dict(color='#0d1117', width=2),
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(13, 17, 23, 0.8)',
paper_bgcolor='rgba(22, 27, 34, 0.3)',
font={'color': '#ffffff', 'family': 'Space Grotesk', 'size': 11},
height=400,
margin=dict(t=10, b=10, l=10, r=10)
)
st.plotly_chart(fig_treemap, use_container_width=True)
# Légende des variations sous le treemap
st.markdown("<h4 style='font-size: 0.9rem; color: #8b949e; margin-top: 24px; margin-bottom: 12px;'>Variations mensuelles</h4>", unsafe_allow_html=True)
# Afficher les variations en grille 3 colonnes
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]
# Couleur et symbole
if variation > 0:
couleur = "#54bd4b"
symbole = "▲"
elif variation < 0:
couleur = "#f85149"
symbole = "▼"
else:
couleur = "#8b949e"
symbole = "→"
st.markdown(f"""
<div style='
background: rgba(22, 27, 34, 0.5);
border-left: 3px solid {couleur};
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
font-size: 0.85rem;
'>
<div style='display: flex; justify-content: space-between; align-items: center;'>
<span style='color: #c9d1d9; font-weight: 500;'>{mois_nom}</span>
<span style='color: {couleur}; font-weight: 600;'>
{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("Total 2026", f"{total_benefices:,.0f} XOF".replace(',', ' '))
with col_stat2:
mois_max = df_treemap.loc[df_treemap['Benefice'].idxmax(), 'Mois_Nom']
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("ℹ️ Aucun bénéfice enregistré pour créer la visualisation")
else:
st.warning("⚠️ Colonnes 'Date_Deblocage' ou 'Cout_Credit' introuvables dans Prets_Master")
# === GOTHAM SURVEILLANCE THEME CSS ===
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
/* Fond global - tons surveillance */
.stApp {
background: linear-gradient(135deg, #0d1117 0%, #161b22 50%, #1c2128 100%);
}
/* Application Space Grotesk UNIQUEMENT aux contenus textuels - PAS aux boutons ni icônes */
.stApp h1, .stApp h2, .stApp h3, .stApp h4, .stApp h5, .stApp h6,
.stApp p:not([data-testid]),
.stMarkdown,
.stText {
font-family: 'Space Grotesk', sans-serif !important;
}
/* Headers style surveillance discrète */
.stApp h1, .stApp h2, .stApp h3 {
color: #58a6ff !important;
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 500 !important;
letter-spacing: 0.5px;
text-shadow: none;
}
.stApp h1 {
font-size: 1.8rem !important;
border-bottom: 1px solid rgba(88, 166, 255, 0.2);
padding-bottom: 12px;
}
.stApp h2 {
font-size: 1.3rem !important;
color: #8b949e !important;
}
/* Metrics cards - style ops center */
[data-testid="stMetric"] {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 6px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
}
[data-testid="stMetric"] label {
color: #8b949e !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.8px;
font-family: 'Space Grotesk', sans-serif !important;
}
[data-testid="stMetric"] [data-testid="stMetricValue"] {
color: #c9d1d9 !important;
font-size: 1.6rem !important;
font-weight: 600 !important;
text-shadow: none;
font-family: 'Space Grotesk', sans-serif !important;
}
[data-testid="stMetric"] [data-testid="stMetricDelta"] {
color: #58a6ff !important;
font-size: 0.85rem !important;
font-weight: 400;
font-family: 'Space Grotesk', sans-serif !important;
}
/* Boutons style ops - SANS Space Grotesk */
.stButton > button {
background: rgba(22, 27, 34, 0.8);
border: 1px solid rgba(48, 54, 61, 1);
color: #c9d1d9 !important;
font-weight: 500;
font-size: 0.85rem;
letter-spacing: 0.5px;
border-radius: 4px;
padding: 10px 20px;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.stButton > button:hover {
background: rgba(33, 38, 45, 1);
border-color: rgba(88, 166, 255, 0.4);
box-shadow: 0 0 8px rgba(88, 166, 255, 0.15);
}
/* Expanders style surveillance */
.streamlit-expanderHeader {
background: rgba(22, 27, 34, 0.4);
border-left: 2px solid rgba(88, 166, 255, 0.5);
color: #8b949e !important;
font-weight: 500;
font-size: 0.9rem;
letter-spacing: 0.3px;
padding: 12px 16px;
border-radius: 3px;
font-family: 'Space Grotesk', sans-serif !important;
}
.streamlit-expanderHeader:hover {
background: rgba(22, 27, 34, 0.6);
border-left-color: rgba(88, 166, 255, 0.8);
}
/* Dataframe style */
.stDataFrame {
border: 1px solid rgba(48, 54, 61, 0.6);
border-radius: 4px;
overflow: hidden;
font-size: 0.85rem;
}
/* Info boxes */
.stAlert {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-left: 3px solid rgba(88, 166, 255, 0.6);
border-radius: 4px;
color: #8b949e;
font-size: 0.9rem;
font-family: 'Space Grotesk', sans-serif !important;
}
/* Checkbox */
.stCheckbox label {
color: #8b949e !important;
font-weight: 500;
font-size: 0.85rem;
font-family: 'Space Grotesk', sans-serif !important;
}
/* Divider */
hr {
border: none;
height: 1px;
background: rgba(48, 54, 61, 0.6);
margin: 2rem 0;
}
/* Download button - SANS Space Grotesk */
.stDownloadButton > button {
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.4);
color: #58a6ff !important;
font-weight: 600;
}
.stDownloadButton > button:hover {
background: rgba(88, 166, 255, 0.15);
border-color: rgba(88, 166, 255, 0.6);
}
/* Scrollbar customization */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(13, 17, 23, 0.4);
}
::-webkit-scrollbar-thumb {
background: rgba(48, 54, 61, 0.8);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(88, 166, 255, 0.3);
}
</style>
""", unsafe_allow_html=True)
# === APPEL DU MODULE ML FEATURE STORE ===
from Analytics.ML_Feature_Store_Analytics import show_ml_feature_store
show_ml_feature_store(client, sheet_name)