Vortex-Flux / src /modules /forecasting.py
klydekushy's picture
Update src/modules/forecasting.py
a591653 verified
import streamlit as st
import pandas as pd
import sys
import os
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
# Import du module d'analyse
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from Analytics.VortexOutFlux import VortexOutFlux
from Analytics.DataSyncForecasting import actualiser_forecasting_depuis_prets
# === CSS AMÉLIORÉ - STYLE GOTHAM PROFESSIONNEL ===
def apply_forecasting_styles():
st.markdown("""
<style>
/* === STYLES SPÉCIFIQUES MODULE FORECASTING === */
/* Wrapper pour isolation */
#forecasting-module {
font-family: 'Space Grotesk', sans-serif;
}
/* Headers spécifiques */
#forecasting-module h1 {
font-size: 1.8rem !important;
margin-bottom: 24px !important;
color: #58a6ff !important;
border-bottom: 2px solid rgba(88, 166, 255, 0.3);
padding-bottom: 12px;
}
#forecasting-module h2 {
font-size: 1.4rem !important;
margin-bottom: 16px !important;
color: #8b949e !important;
}
#forecasting-module h3 {
font-size: 1.1rem !important;
margin-bottom: 12px !important;
color: #58a6ff !important;
}
/* Metrics cards pour KPIs */
#forecasting-module [data-testid="stMetric"] {
background: rgba(22, 27, 34, 0.7);
border: 1px solid rgba(88, 166, 255, 0.4);
padding: 16px !important;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
#forecasting-module [data-testid="stMetric"] label {
color: #8b949e !important;
font-size: 0.75rem !important;
font-weight: 600 !important;
text-transform: uppercase;
letter-spacing: 0.8px;
}
#forecasting-module [data-testid="stMetric"] [data-testid="stMetricValue"] {
color: #58a6ff !important;
font-size: 1.6rem !important;
font-weight: 700 !important;
}
#forecasting-module [data-testid="stMetric"] [data-testid="stMetricDelta"] {
color: #54bd4b !important;
font-size: 0.9rem !important;
}
/* Carte de prédiction style Palantir - MINIMALISTE */
.forecast-card {
background: rgba(22, 27, 34, 0.4);
border: 1px solid rgba(88, 166, 255, 0.2);
border-radius: 4px;
padding: 16px;
margin: 12px 0;
transition: border-color 0.2s ease;
}
.forecast-card:hover {
border-color: rgba(88, 166, 255, 0.4);
}
.forecast-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, rgba(88, 166, 255, 0.8), rgba(88, 166, 255, 0.3));
}
.forecast-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(88, 166, 255, 0.2);
}
.forecast-card-title {
font-size: 1.2rem;
font-weight: 700;
color: #c9d1d9;
margin: 0;
}
.forecast-card-badge {
background: rgba(88, 166, 255, 0.15);
color: #58a6ff;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
border: 1px solid rgba(88, 166, 255, 0.3);
}
.forecast-card-badge.positive {
background: rgba(88, 166, 255, 0.15);
color: #58a6ff;
border-color: rgba(88, 166, 255, 0.3);
}
.forecast-card-badge.negative {
background: rgba(139, 148, 158, 0.15);
color: #8b949e;
border-color: rgba(139, 148, 158, 0.3);
}
.forecast-card-body {
color: #c9d1d9;
}
.forecast-card-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid rgba(48, 54, 61, 0.3);
}
.forecast-card-row:last-child {
border-bottom: none;
}
.forecast-card-label {
color: #8b949e;
font-size: 0.9rem;
font-weight: 500;
}
.forecast-card-value {
color: #c9d1d9;
font-weight: 600;
font-size: 1rem;
}
.forecast-card-value.highlight {
color: #58a6ff;
font-size: 1.2rem;
font-weight: 700;
}
/* Bloc d'interprétation */
.interpretation-box {
background: rgba(88, 166, 255, 0.05);
border-left: 4px solid #58a6ff;
border-radius: 4px;
padding: 16px;
margin: 16px 0;
}
.interpretation-box h4 {
color: #58a6ff !important;
margin: 0 0 12px 0 !important;
font-size: 1rem !important;
}
.interpretation-box p {
color: #c9d1d9;
margin: 8px 0;
line-height: 1.6;
font-size: 0.9rem;
}
/* Bloc métriques de performance */
.metrics-performance {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 6px;
padding: 16px;
margin: 16px 0;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 12px;
}
.metric-item {
background: rgba(13, 17, 23, 0.8);
padding: 12px;
border-radius: 4px;
border-left: 3px solid #58a6ff;
}
.metric-item-label {
color: #8b949e;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.metric-item-value {
color: #58a6ff;
font-size: 1.3rem;
font-weight: 700;
}
/* Bouton de synchronisation - STYLE GOTHAM MONOCHROME */
[data-testid="column"] .stButton[data-baseweb="button"] > button[kind="primary"] {
background: rgba(88, 166, 255, 0.15) !important;
border: 2px solid #58a6ff !important;
color: #58a6ff !important;
font-weight: 700 !important;
text-transform: uppercase;
letter-spacing: 1px;
}
[data-testid="column"] .stButton[data-baseweb="button"] > button[kind="primary"]:hover {
background: rgba(88, 166, 255, 0.25) !important;
border-color: #79b8ff !important;
box-shadow: 0 0 20px rgba(88, 166, 255, 0.3) !important;
}
/* Divider */
#forecasting-module hr {
background: rgba(88, 166, 255, 0.3);
height: 2px;
margin: 24px 0;
}
/* Alert boxes */
#forecasting-module .stAlert {
border-radius: 6px;
padding: 12px 16px !important;
}
#forecasting-module .stAlert[kind="info"] {
background: rgba(88, 166, 255, 0.1) !important;
border-left: 4px solid #58a6ff !important;
}
#forecasting-module .stAlert[kind="warning"] {
background: rgba(243, 156, 18, 0.1) !important;
border-left: 4px solid #f39c12 !important;
}
#forecasting-module .stAlert[kind="success"] {
background: rgba(84, 189, 75, 0.1) !important;
border-left: 4px solid #54bd4b !important;
}
/* Expanders */
#forecasting-module .streamlit-expanderHeader {
background: rgba(22, 27, 34, 0.7) !important;
border-left: 3px solid rgba(88, 166, 255, 0.6) !important;
padding: 12px 16px !important;
font-weight: 600 !important;
color: #8b949e !important;
}
#forecasting-module .streamlit-expanderHeader:hover {
background: rgba(33, 38, 45, 0.9) !important;
border-left-color: rgba(88, 166, 255, 0.9) !important;
}
#forecasting-module .streamlit-expanderContent {
background: rgba(13, 17, 23, 0.7);
border: 1px solid rgba(48, 54, 61, 0.6);
padding: 16px !important;
}
/* Animation de chargement */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-indicator {
animation: pulse 2s ease-in-out infinite;
color: #58a6ff;
text-align: center;
padding: 20px;
}
</style>
""", unsafe_allow_html=True)
def render_forecast_card(month_data):
"""Génère une carte de prédiction mensuelle style Gotham"""
date_str = month_data['Date'].strftime("%B %Y")
montant = month_data['Montant_Predit']
lower = month_data['Borne_Inf']
upper = month_data['Borne_Sup']
# Calcul de la marge d'erreur
margin = ((upper - lower) / 2) / montant * 100 if montant > 0 else 0
html = f"""
<div class="forecast-card">
<div class="forecast-card-header">
<h4 class="forecast-card-title">{date_str}</h4>
<span class="forecast-card-badge">Prédiction</span>
</div>
<div class="forecast-card-body">
<div class="forecast-card-row">
<span class="forecast-card-label">Montant Prédit</span>
<span class="forecast-card-value highlight">{montant:,.0f} XOF</span>
</div>
<div class="forecast-card-row">
<span class="forecast-card-label">Intervalle de Confiance (95%)</span>
<span class="forecast-card-value">{lower:,.0f} - {upper:,.0f} XOF</span>
</div>
<div class="forecast-card-row">
<span class="forecast-card-label">Marge d'Erreur</span>
<span class="forecast-card-value">± {margin:.1f}%</span>
</div>
</div>
</div>
"""
return html
def show_forecasting_module(client, sheet_name):
"""Module principal de prévision des flux de sortie"""
# Appliquer les styles
apply_forecasting_styles()
# Wrapper pour isolation
st.markdown('<div id="forecasting-module">', unsafe_allow_html=True)
st.header("JASMINE - PRÉDICTION DES FLUX DE SORTIE")
st.caption("Analyse prédictive en temps réel basée sur la régression linéaire")
# === SECTION SYNCHRONISATION ===
st.divider()
col_sync1, col_sync2 = st.columns([3, 1])
with col_sync1:
st.markdown("""
<div style="background: rgba(88, 166, 255, 0.05); border-left: 4px solid #58a6ff; padding: 12px; border-radius: 4px;">
<p style="color: #8b949e; margin: 0; font-size: 0.9rem;">
<strong style="color: #58a6ff;"> Synchronisation automatique :</strong>
Les données de flux de sortie sont calculées automatiquement depuis les déblocages de prêts (Prets_Master).
Cliquez sur le bouton pour actualiser.
</p>
</div>
""", unsafe_allow_html=True)
with col_sync2:
if st.button(" Synchroniser", use_container_width=True, type="primary"):
with st.spinner("Synchronisation en cours..."):
result = actualiser_forecasting_depuis_prets(client, sheet_name)
if result['success']:
stats = result['stats']
st.success(f"✅ {result['message']}")
# Afficher les détails
with st.expander(" Détails de la synchronisation", expanded=True):
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Mois Conservés", stats['nb_conserves'],
help="Mois gardés de l'historique manuel")
with col2:
st.metric("Mois Mis à Jour", stats['nb_mis_a_jour'],
help="Mois recalculés depuis Prets_Master")
with col3:
st.metric("Mois Ajoutés", stats['nb_ajoutes'],
help="Nouveaux mois détectés")
if stats['nb_mis_a_jour'] > 0:
st.info(f"**Mois mis à jour :** {', '.join(stats['mois_mis_a_jour'])}")
if stats['nb_ajoutes'] > 0:
st.success(f"**Nouveaux mois ajoutés :** {', '.join(stats['mois_ajoutes'])}")
# Forcer le rechargement
st.rerun()
else:
st.error(f"❌ {result['message']}")
try:
# Connexion à Google Sheets
sh = client.open(sheet_name)
ws_forecasting = sh.worksheet("Forecasting")
# Chargement des données
df_forecasting = pd.DataFrame(ws_forecasting.get_all_records())
if df_forecasting.empty or len(df_forecasting) < 2:
st.warning("⚠️ Données insuffisantes pour effectuer une prédiction. Minimum 2 points de données requis.")
st.markdown('</div>', unsafe_allow_html=True)
return
# Vérification des colonnes
if 'Date' not in df_forecasting.columns or 'Montant_Total_Sortie' not in df_forecasting.columns:
st.error("❌ Structure de données invalide. Colonnes requises : 'Date', 'Montant_Total_Sortie'")
st.markdown('</div>', unsafe_allow_html=True)
return
# Initialisation de l'analyseur
analyzer = VortexOutFlux(df_forecasting)
# Section 1 : Vue d'ensemble des données
st.divider()
st.subheader(" Vue d'Ensemble des Données Historiques")
col1, col2, col3, col4 = st.columns(4)
with col1:
nb_points = len(analyzer.df)
st.metric("Points de Données", nb_points)
with col2:
flux_moyen = analyzer.df['Montant_Total_Sortie'].mean()
st.metric("Flux Moyen", f"{flux_moyen:,.0f} XOF")
with col3:
flux_total = analyzer.df['Montant_Total_Sortie'].sum()
st.metric("Flux Total", f"{flux_total:,.0f} XOF")
with col4:
date_debut = analyzer.df['Date'].min().strftime("%m/%Y")
date_fin = analyzer.df['Date'].max().strftime("%m/%Y")
st.metric("Période", f"{date_debut} - {date_fin}")
# Section 2 : Prédictions
st.divider()
st.subheader(" Prédictions pour les 3 Prochains Mois")
with st.spinner("Calcul des prédictions en cours..."):
predictions_result = analyzer.predict_next_months(n_months=3)
if 'error' in predictions_result:
st.error(f"❌ {predictions_result['error']}")
st.markdown('</div>', unsafe_allow_html=True)
return
predictions_df = predictions_result['predictions']
models = predictions_result['models']
# Affichage des cartes de prédiction
cols = st.columns(3)
for idx, (_, row) in enumerate(predictions_df.iterrows()):
with cols[idx]:
st.markdown(render_forecast_card(row), unsafe_allow_html=True)
# Section 3 : Analyse de Tendance et Interprétation
st.divider()
st.subheader(" Analyse de Tendance")
interpretations = analyzer.get_interpretation(models)
# Affichage de la tendance principale
tendance_badge_class = "positive" if interpretations['tendance'] == "CROISSANCE" else "negative"
st.markdown(f"""
<div class="forecast-card">
<div class="forecast-card-header">
<h4 class="forecast-card-title">Tendance Détectée</h4>
<span class="forecast-card-badge {tendance_badge_class}">{interpretations['tendance']}</span>
</div>
<div class="forecast-card-body">
<div class="forecast-card-row">
<span class="forecast-card-label">Variation Mensuelle Moyenne</span>
<span class="forecast-card-value highlight">{abs(interpretations['pente_mensuelle']):,.0f} XOF/mois ({interpretations['pct_variation']:.1f}%)</span>
</div>
<div class="forecast-card-row">
<span class="forecast-card-label">Force de la Corrélation</span>
<span class="forecast-card-value">{interpretations['force_correlation'].upper()}</span>
</div>
<div class="forecast-card-row">
<span class="forecast-card-label">Qualité du Modèle</span>
<span class="forecast-card-value">{interpretations['qualite_modele'].upper()}</span>
</div>
</div>
</div>
""", unsafe_allow_html=True)
# Interprétation textuelle
st.markdown(f"""
<div class="interpretation-box">
<h4> Interprétation</h4>
<p><strong>Tendance :</strong> {interpretations['message_principal']}</p>
<p><strong>Fiabilité :</strong> {interpretations['message_fiabilite']}</p>
<p><strong>Précision :</strong> {interpretations['message_precision']}</p>
</div>
""", unsafe_allow_html=True)
# Section 4 : Métriques de Performance du Modèle
st.divider()
st.subheader(" Performance du Modèle")
col1, col2, col3, col4, col5 = st.columns(5)
with col1:
st.metric("R² (R-Carré)", f"{models['r_squared']:.3f}",
help="Coefficient de détermination - Pourcentage de variance expliquée")
with col2:
st.metric("R (Corrélation)", f"{models['r_coefficient']:.3f}",
help="Coefficient de corrélation")
with col3:
st.metric("MAE", f"{models['mae']:,.0f} XOF",
help="Mean Absolute Error - Erreur moyenne absolue")
with col4:
st.metric("RMSE", f"{models['rmse']:,.0f} XOF",
help="Root Mean Squared Error - Erreur quadratique moyenne")
with col5:
pente_jour = models['pente']
st.metric("Pente", f"{pente_jour:.2f} XOF/jour",
help="Croissance par jour ordinal")
# Section 5 : Visualisations
st.divider()
st.subheader(" Visualisations Analytiques")
# Préparation des données de visualisation
viz_data = analyzer.generate_visualization_data(predictions_result)
# Graphique principal : Historique + Prédictions
fig_main = go.Figure()
# Données historiques
fig_main.add_trace(go.Scatter(
x=viz_data['historical']['dates'],
y=viz_data['historical']['values'],
mode='lines+markers',
name='Flux Historiques',
line=dict(color='#58a6ff', width=2),
marker=dict(size=8, symbol='circle')
))
# Prédictions futures
fig_main.add_trace(go.Scatter(
x=viz_data['future']['dates'],
y=viz_data['future']['predictions'],
mode='lines+markers',
name='Prédictions',
line=dict(color='#54bd4b', width=2, dash='dash'),
marker=dict(size=10, symbol='diamond')
))
# Intervalle de confiance
fig_main.add_trace(go.Scatter(
x=viz_data['future']['dates'] + viz_data['future']['dates'][::-1],
y=viz_data['future']['upper_bound'] + viz_data['future']['lower_bound'][::-1],
fill='toself',
fillcolor='rgba(84, 189, 75, 0.2)',
line=dict(color='rgba(84, 189, 75, 0)'),
name='Intervalle de Confiance (95%)',
showlegend=True
))
fig_main.update_layout(
title=dict(
text="Flux de Sortie Mensuels : Historique & Prédictions",
font=dict(size=16, color='#58a6ff')
),
xaxis=dict(
title="Date",
gridcolor='rgba(48, 54, 61, 0.3)',
color='#8b949e'
),
yaxis=dict(
title="Montant (XOF)",
gridcolor='rgba(48, 54, 61, 0.3)',
color='#8b949e'
),
plot_bgcolor='rgba(13, 17, 23, 0.8)',
paper_bgcolor='rgba(22, 27, 34, 0.9)',
font=dict(color='#c9d1d9', family='Space Grotesk'),
hovermode='x unified',
legend=dict(
bgcolor='rgba(22, 27, 34, 0.8)',
bordercolor='rgba(88, 166, 255, 0.3)',
borderwidth=1
),
height=500
)
st.plotly_chart(fig_main, use_container_width=True)
# Graphiques secondaires : Résidus
with st.expander(" Analyse des Résidus (Diagnostic du Modèle)", expanded=False):
fig_residuals = make_subplots(
rows=1, cols=2,
subplot_titles=("Graphique des Résidus", "Distribution des Résidus"),
specs=[[{"type": "scatter"}, {"type": "histogram"}]]
)
# Graphique des résidus
fig_residuals.add_trace(
go.Scatter(
x=viz_data['residuals']['predictions'],
y=viz_data['residuals']['values'],
mode='markers',
marker=dict(color='#58a6ff', size=8),
name='Résidus'
),
row=1, col=1
)
# Ligne à zéro
fig_residuals.add_hline(
y=0, line_dash="dash", line_color="#f39c12",
row=1, col=1
)
# Distribution des résidus
fig_residuals.add_trace(
go.Histogram(
x=viz_data['residuals']['values'],
marker=dict(color='#58a6ff', line=dict(color='#c9d1d9', width=1)),
name='Distribution',
nbinsx=15
),
row=1, col=2
)
fig_residuals.update_xaxes(title_text="Valeurs Prédites (XOF)", row=1, col=1, gridcolor='rgba(48, 54, 61, 0.3)', color='#8b949e')
fig_residuals.update_yaxes(title_text="Résidus (XOF)", row=1, col=1, gridcolor='rgba(48, 54, 61, 0.3)', color='#8b949e')
fig_residuals.update_xaxes(title_text="Résidus (XOF)", row=1, col=2, gridcolor='rgba(48, 54, 61, 0.3)', color='#8b949e')
fig_residuals.update_yaxes(title_text="Fréquence", row=1, col=2, gridcolor='rgba(48, 54, 61, 0.3)', color='#8b949e')
fig_residuals.update_layout(
plot_bgcolor='rgba(13, 17, 23, 0.8)',
paper_bgcolor='rgba(22, 27, 34, 0.9)',
font=dict(color='#c9d1d9', family='Space Grotesk'),
showlegend=False,
height=400
)
st.plotly_chart(fig_residuals, use_container_width=True)
st.caption("**Note :** Les résidus doivent être aléatoirement distribués autour de zéro pour un bon modèle.")
# Section 6 : Tableau des données
with st.expander(" Tableau Détaillé des Prédictions", expanded=False):
# Formatage du DataFrame pour l'affichage
display_df = predictions_df.copy()
display_df['Date'] = display_df['Date'].dt.strftime('%B %Y')
display_df['Montant_Predit'] = display_df['Montant_Predit'].apply(lambda x: f"{x:,.0f} XOF")
display_df['Borne_Inf'] = display_df['Borne_Inf'].apply(lambda x: f"{x:,.0f} XOF")
display_df['Borne_Sup'] = display_df['Borne_Sup'].apply(lambda x: f"{x:,.0f} XOF")
display_df.columns = ['Date', 'Montant Prédit', 'Borne Inférieure (95%)', 'Borne Supérieure (95%)']
st.dataframe(display_df, use_container_width=True, hide_index=True)
# Section 7 : Informations complémentaires
st.divider()
st.info("""
**ℹ️ À Propos de ce Modèle**
- **Modèle utilisé :** Régression Linéaire avec Intervalles de Confiance à 95%
- **Actualisation :** Les prédictions se mettent à jour automatiquement à chaque ajout de données dans la feuille "Forecasting"
- **Utilisation :** Ce modèle est adapté pour des tendances linéaires. Pour des patterns saisonniers complexes, envisagez des modèles avancés (SARIMA, Prophet)
- **Recommandation :** Vérifiez régulièrement les résidus et le R² pour assurer la qualité du modèle
""")
except Exception as e:
st.error(f"❌ Erreur lors du chargement des données : {e}")
st.exception(e)
st.markdown('</div>', unsafe_allow_html=True)