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

{date_str}

Prédiction
Montant Prédit {montant:,.0f} XOF
Intervalle de Confiance (95%) {lower:,.0f} - {upper:,.0f} XOF
Marge d'Erreur ± {margin:.1f}%
""" 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('
', 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("""

Synchronisation automatique : 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.

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

Tendance Détectée

{interpretations['tendance']}
Variation Mensuelle Moyenne {abs(interpretations['pente_mensuelle']):,.0f} XOF/mois ({interpretations['pct_variation']:.1f}%)
Force de la Corrélation {interpretations['force_correlation'].upper()}
Qualité du Modèle {interpretations['qualite_modele'].upper()}
""", unsafe_allow_html=True) # Interprétation textuelle st.markdown(f"""

Interprétation

Tendance : {interpretations['message_principal']}

Fiabilité : {interpretations['message_fiabilite']}

Précision : {interpretations['message_precision']}

""", 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('', unsafe_allow_html=True)