Spaces:
Running
Running
Claude Haiku 5.5
fix(jasmine): le chat voit les donnees des graphiques C et H (residus vs predits, stats, historique)
373f99b Download src/modules/forecasting.py from klydekushy/Vortex-Flux: direct link, hf CLI and curl.
- Browser
- Download file 39.4 kB
-
https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/forecasting.py
- Command line
-
hf download hf://spaces/klydekushy/Vortex-Flux/src/modules/forecasting.py
-
curl -L -o forecasting.py https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/forecasting.py
39.4 kB
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| import pandas as pd | |
| import sys | |
| import os | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import numpy as np | |
| sys.path.append(os.path.join(os.path.dirname(__file__), '..')) | |
| from Analytics.VortexOutFlux import VortexOutFlux | |
| from Analytics.DataSyncForecasting import actualiser_forecasting_depuis_prets, forecasting_needs_sync | |
| # ============================================================================ | |
| # CSS — STYLE TACTIQUE / MILITAIRE | |
| # Seules les règles SCOPED sous #forecasting-module sont conservées ici. | |
| # Les règles globales (.stApp h1/h2/h3, metrics, boutons, etc.) sont | |
| # centralisées dans streamlit_app.py pour garantir l'uniformité entre modules. | |
| # ============================================================================ | |
| def apply_forecasting_styles(): | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&display=swap'); | |
| /* ── FORCE JETBRAINS MONO dans le scope forecasting ── */ | |
| #forecasting-module, | |
| #forecasting-module p, | |
| #forecasting-module div:not([class*="Icon"]), | |
| #forecasting-module label, | |
| #forecasting-module input, | |
| #forecasting-module select, | |
| #forecasting-module textarea, | |
| #forecasting-module h1, | |
| #forecasting-module h2, | |
| #forecasting-module h3 { | |
| font-family: 'JetBrains Mono', 'Courier New', monospace !important; | |
| } | |
| /* Protection icônes Material */ | |
| .stIcon, .material-icons, .material-symbols-rounded, [class*="Icon"], svg, svg * { | |
| font-family: 'Material Symbols Rounded', 'Material Icons', sans-serif !important; | |
| } | |
| /* ── HEADERS — scope #forecasting-module uniquement ── | |
| (Les règles .stApp h1/h2/h3 sont retirées d'ici : elles vivent | |
| dans streamlit_app.py et s'appliquent uniformément à tous les modules) */ | |
| #forecasting-module h1 { | |
| font-size: 1.5rem !important; | |
| font-weight: 800 !important; | |
| letter-spacing: 3px !important; | |
| text-transform: uppercase; | |
| color: #a8b8c8 !important; | |
| border-bottom: 1px solid rgba(168, 184, 200, 0.25); | |
| padding-bottom: 10px; | |
| margin-bottom: 20px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| #forecasting-module h2 { | |
| font-size: 0.85rem !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 2.5px !important; | |
| text-transform: uppercase; | |
| color: #6a7a8a !important; | |
| margin-bottom: 14px !important; | |
| margin-top: 0 !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| #forecasting-module h3 { | |
| font-size: 0.8rem !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 2px !important; | |
| text-transform: uppercase; | |
| color: #58a6ff !important; | |
| margin-bottom: 10px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| /* ── METRICS ── */ | |
| [data-testid="stMetric"] { | |
| background: rgba(15, 20, 26, 0.85) !important; | |
| border: 1px solid rgba(100, 120, 140, 0.25) !important; | |
| border-top: 2px solid rgba(100, 120, 140, 0.5) !important; | |
| padding: 14px 16px !important; | |
| border-radius: 0px !important; | |
| } | |
| [data-testid="stMetricLabel"] p, | |
| [data-testid="stMetricLabel"] span, | |
| [data-testid="stMetricLabel"] div { | |
| color: #5a6a7a !important; | |
| font-size: 0.65rem !important; | |
| font-weight: 700 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 1.5px !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| [data-testid="stMetricValue"] div, | |
| [data-testid="stMetricValue"] p { | |
| color: #a8b8c8 !important; | |
| font-size: 1.3rem !important; | |
| font-weight: 700 !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| [data-testid="stMetricDelta"] div { | |
| font-size: 0.8rem !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| /* ── CAPTIONS ── */ | |
| [data-testid="stCaptionContainer"], | |
| [data-testid="stCaptionContainer"] p, | |
| .stCaption, .stCaption p { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 0.7rem !important; | |
| color: #4a5a6a !important; | |
| letter-spacing: 0.5px !important; | |
| } | |
| /* ── ALERTS ── */ | |
| [data-testid="stAlert"] p, | |
| [data-testid="stAlert"] span { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 0.8rem !important; | |
| } | |
| [data-testid="stAlert"] { | |
| border-radius: 0 !important; | |
| } | |
| /* ── EXPANDERS ── */ | |
| [data-testid="stExpander"] summary { | |
| font-size: 0.72rem !important; | |
| letter-spacing: 1.5px !important; | |
| text-transform: uppercase !important; | |
| color: #5a6a7a !important; | |
| } | |
| [data-testid="stExpander"] summary p { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| /* ── BOUTONS ── */ | |
| .stButton > button { | |
| background: transparent !important; | |
| border: 1px solid rgba(88, 166, 255, 0.5) !important; | |
| color: #7a8a9a !important; | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 0.72rem !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 1.5px !important; | |
| text-transform: uppercase !important; | |
| border-radius: 0 !important; | |
| padding: 8px 16px !important; | |
| transition: all 0.15s ease !important; | |
| box-shadow: none !important; | |
| outline: none !important; | |
| } | |
| .stButton > button p { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| .stButton > button:hover { | |
| border-color: #58a6ff !important; | |
| color: #58a6ff !important; | |
| background: rgba(88, 166, 255, 0.1) !important; | |
| } | |
| .stButton > button:focus { | |
| box-shadow: none !important; | |
| outline: none !important; | |
| } | |
| /* ── SPINNER ── */ | |
| [data-testid="stSpinner"] p { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| } | |
| /* ── SÉPARATEURS ── */ | |
| hr { | |
| border: none !important; | |
| border-top: 1px solid rgba(80, 100, 120, 0.2) !important; | |
| margin: 22px 0 !important; | |
| } | |
| /* ── CARTES SIGNAL / PRÉDICTION ── */ | |
| .tac-card { | |
| background: rgba(15, 20, 26, 0.85); | |
| border: 1px solid rgba(100, 120, 140, 0.25); | |
| border-left: 3px solid rgba(100, 120, 140, 0.5); | |
| padding: 18px 20px; | |
| margin: 0; | |
| } | |
| .tac-card.signal-reduce { border-left-color: #c0392b; } | |
| .tac-card.signal-maintain{ border-left-color: #5a6a7a; } | |
| .tac-card.signal-increase{ border-left-color: #58a6ff; } | |
| .tac-card-label { | |
| font-size: 0.62rem; font-weight: 700; letter-spacing: 2px; | |
| text-transform: uppercase; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; margin-bottom: 6px; | |
| } | |
| .tac-card-value { | |
| font-size: 1.6rem; font-weight: 800; color: #a8b8c8; | |
| font-family: 'JetBrains Mono', monospace; line-height: 1.1; margin-bottom: 4px; | |
| } | |
| .tac-card-sub { | |
| font-size: 0.75rem; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; margin-top: 4px; | |
| } | |
| .tac-card-evolution { | |
| font-size: 1.1rem; font-weight: 700; | |
| font-family: 'JetBrains Mono', monospace; margin-top: 6px; | |
| } | |
| .tac-card-evolution.up { color: #c0392b; } | |
| .tac-card-evolution.down { color: #58a6ff; } | |
| .tac-card-evolution.flat { color: #5a6a7a; } | |
| /* ── SIGNAL OPÉRATIONNEL ── */ | |
| .signal-block { | |
| background: rgba(10, 14, 18, 0.9); | |
| border: 1px solid rgba(100, 120, 140, 0.2); | |
| border-left: 4px solid; | |
| padding: 16px 20px; | |
| margin-top: 16px; | |
| } | |
| .signal-block.reduce { border-left-color: #c0392b; } | |
| .signal-block.maintain { border-left-color: #5a6a7a; } | |
| .signal-block.increase { border-left-color: #58a6ff; } | |
| .signal-header { | |
| font-size: 0.62rem; font-weight: 700; letter-spacing: 2.5px; | |
| text-transform: uppercase; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; margin-bottom: 8px; | |
| } | |
| .signal-title { | |
| font-size: 0.9rem; font-weight: 800; letter-spacing: 2px; | |
| text-transform: uppercase; font-family: 'JetBrains Mono', monospace; margin-bottom: 8px; | |
| } | |
| .signal-title.reduce { color: #c0392b; } | |
| .signal-title.maintain { color: #8a9aaa; } | |
| .signal-title.increase { color: #58a6ff; } | |
| .signal-body { | |
| font-size: 0.82rem; color: #7a8a9a; | |
| font-family: 'JetBrains Mono', monospace; line-height: 1.6; | |
| } | |
| /* ── CARTE FORECAST MOIS ── */ | |
| .forecast-month-card { | |
| background: rgba(15, 20, 26, 0.85); | |
| border: 1px solid rgba(100, 120, 140, 0.2); | |
| padding: 14px 16px; margin: 8px 0; | |
| } | |
| .fmc-month { | |
| font-size: 0.65rem; letter-spacing: 2px; text-transform: uppercase; | |
| color: #5a6a7a; font-family: 'JetBrains Mono', monospace; margin-bottom: 6px; | |
| } | |
| .fmc-amount { | |
| font-size: 1.3rem; font-weight: 700; color: #a8b8c8; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .fmc-bounds { | |
| font-size: 0.72rem; color: #4a5a6a; | |
| font-family: 'JetBrains Mono', monospace; margin-top: 4px; | |
| } | |
| .fmc-margin { | |
| font-size: 0.7rem; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; margin-top: 2px; | |
| } | |
| /* ── SECTION TAG ── */ | |
| .section-tag { | |
| display: inline-block; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.6rem; font-weight: 700; letter-spacing: 2.5px; | |
| text-transform: uppercase; color: #3a4a5a; | |
| border: 1px solid rgba(60, 80, 100, 0.25); | |
| padding: 3px 8px; margin-bottom: 14px; | |
| } | |
| /* ── CONFIDENCE BLOCK ── */ | |
| .confidence-block { | |
| background: rgba(10, 14, 18, 0.9); | |
| border: 1px solid rgba(100, 120, 140, 0.2); | |
| border-left: 4px solid #58a6ff; | |
| padding: 18px 22px; margin-top: 0; | |
| } | |
| .confidence-label { | |
| font-size: 0.62rem; font-weight: 700; letter-spacing: 2.5px; | |
| text-transform: uppercase; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; margin-bottom: 10px; | |
| } | |
| .confidence-score { | |
| font-size: 2.2rem; font-weight: 800; | |
| font-family: 'JetBrains Mono', monospace; line-height: 1; margin-bottom: 6px; | |
| } | |
| .confidence-score.high { color: #2ecc71; } | |
| .confidence-score.medium { color: #f39c12; } | |
| .confidence-score.low { color: #c0392b; } | |
| .confidence-detail { | |
| font-size: 0.75rem; color: #5a6a7a; | |
| font-family: 'JetBrains Mono', monospace; line-height: 1.7; margin-top: 8px; | |
| } | |
| .confidence-bar-track { | |
| height: 4px; background: rgba(60, 80, 100, 0.3); | |
| margin: 10px 0 6px 0; position: relative; | |
| } | |
| .confidence-bar-fill { | |
| height: 4px; position: absolute; top: 0; left: 0; | |
| transition: width 0.3s ease; | |
| } | |
| .confidence-bar-fill.high { background: #2ecc71; } | |
| .confidence-bar-fill.medium { background: #f39c12; } | |
| .confidence-bar-fill.low { background: #c0392b; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================================================================ | |
| # HELPERS DE RENDU | |
| # ============================================================================ | |
| def _fmt(n): | |
| return f"{int(round(n)):,}".replace(",", " ") | |
| def render_signal_block(signal_data): | |
| css_cls = { | |
| 'REDUIRE': ('reduce', 'ORDRE : REDUIRE LES ENGAGEMENTS'), | |
| 'MAINTENIR': ('maintain', 'ORDRE : MAINTENIR LES ENGAGEMENTS'), | |
| 'AUGMENTER': ('increase', 'ORDRE : AUGMENTER LES ENGAGEMENTS'), | |
| } | |
| cls, titre = css_cls.get(signal_data['signal'], ('maintain', 'ORDRE : MAINTENIR')) | |
| return f""" | |
| <div class="signal-block {cls}"> | |
| <div class="signal-header">Analyse predictive — {signal_data['mois_predit']}</div> | |
| <div class="signal-title {cls}">{titre}</div> | |
| <div class="signal-body">{signal_data['message_signal']}</div> | |
| </div> | |
| """ | |
| def render_next_month_cards(signal_data): | |
| evo = signal_data['evolution_pct'] | |
| if evo > 2: | |
| evo_class, evo_sign = 'up', '+' | |
| elif evo < -2: | |
| evo_class, evo_sign = 'down', '' | |
| else: | |
| evo_class, evo_sign = 'flat', '' | |
| signal_cls = { | |
| 'REDUIRE': 'signal-reduce', | |
| 'MAINTENIR': 'signal-maintain', | |
| 'AUGMENTER': 'signal-increase', | |
| }.get(signal_data['signal'], 'signal-maintain') | |
| card_predit = f""" | |
| <div class="tac-card {signal_cls}"> | |
| <div class="tac-card-label">Capital predit — {signal_data['mois_predit']}</div> | |
| <div class="tac-card-value">{_fmt(signal_data['capital_predit'])} XOF</div> | |
| <div class="tac-card-sub">Modele de regression lineaire</div> | |
| </div> | |
| """ | |
| card_evo = f""" | |
| <div class="tac-card"> | |
| <div class="tac-card-label">Evolution predictible vs {signal_data['mois_actuel']}</div> | |
| <div class="tac-card-value">{_fmt(signal_data['capital_reel_actuel'])} XOF</div> | |
| <div class="tac-card-sub">Capital reel — {signal_data['mois_actuel']}</div> | |
| <div class="tac-card-evolution {evo_class}">{evo_sign}{evo:.1f}%</div> | |
| </div> | |
| """ | |
| return card_predit, card_evo | |
| def render_forecast_month_card(row): | |
| montant = row['Montant_Predit'] | |
| lower = row['Borne_Inf'] | |
| upper = row['Borne_Sup'] | |
| margin = ((upper - lower) / 2) / montant * 100 if montant > 0 else 0 | |
| mois = row['Date'].strftime('%B %Y').upper() | |
| return f""" | |
| <div class="forecast-month-card"> | |
| <div class="fmc-month">{mois}</div> | |
| <div class="fmc-amount">{_fmt(montant)} XOF</div> | |
| <div class="fmc-bounds">Intervalle 95% : {_fmt(lower)} — {_fmt(upper)} XOF</div> | |
| <div class="fmc-margin">Marge d erreur : +/- {margin:.1f}%</div> | |
| </div> | |
| """ | |
| def render_confidence_block(confidence_data): | |
| score = confidence_data['score'] | |
| if score >= 70: | |
| css_cls = 'high' | |
| elif score >= 40: | |
| css_cls = 'medium' | |
| else: | |
| css_cls = 'low' | |
| bar_width = min(score, 100) | |
| return f""" | |
| <div class="confidence-block"> | |
| <div class="confidence-label">Indice de confiance — Prediction mois suivant</div> | |
| <div class="confidence-score {css_cls}">{score:.0f}%</div> | |
| <div class="confidence-bar-track"> | |
| <div class="confidence-bar-fill {css_cls}" style="width:{bar_width}%"></div> | |
| </div> | |
| <div class="confidence-detail"> | |
| Ecart moyen absolu : {_fmt(confidence_data['mae_moy'])} XOF | |
| | Ecart relatif moyen : {confidence_data['mape']:.1f}%<br> | |
| Stabilite des ecarts (CV) : {confidence_data['cv_residus']:.1f}% | |
| | R² du modele : {confidence_data['r_squared']:.3f}<br> | |
| {confidence_data['interpretation']} | |
| </div> | |
| </div> | |
| """ | |
| def render_comparison_table_html(df_cmp): | |
| rows_html = "" | |
| for _, row in df_cmp.iterrows(): | |
| evo = row['Evolution_Pct'] | |
| if evo is None or (isinstance(evo, float) and np.isnan(evo)): | |
| evo_str = "—" | |
| evo_color = "#5a6a7a" | |
| elif evo > 0: | |
| evo_str = f"+{evo:.1f}%" | |
| evo_color = "#c0392b" | |
| elif evo < 0: | |
| evo_str = f"{evo:.1f}%" | |
| evo_color = "#58a6ff" | |
| else: | |
| evo_str = "0.0%" | |
| evo_color = "#5a6a7a" | |
| rows_html += f""" | |
| <tr> | |
| <td>{row['Mois']}</td> | |
| <td style="text-align:right">{_fmt(row['Capital_Reel'])} XOF</td> | |
| <td style="text-align:right; color:{evo_color}">{evo_str}</td> | |
| <td style="text-align:right">{_fmt(row['Capital_Predit'])} XOF</td> | |
| </tr> | |
| """ | |
| # La couleur "bleu" du theme BLEU devient ambre en theme NOIR (core/theme.py). | |
| from core import theme | |
| couleur_baisse = "Ambre" if theme.est_noir() else "Bleu" | |
| return f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * {{ margin:0; padding:0; box-sizing:border-box; }} | |
| body {{ background: transparent; font-family: 'JetBrains Mono', 'Courier New', monospace; }} | |
| table {{ width: 100%; border-collapse: collapse; font-family: 'JetBrains Mono', 'Courier New', monospace; font-size: 0.82rem; }} | |
| th {{ background: rgba(15, 20, 26, 0.95); border-bottom: 1px solid rgba(100, 120, 140, 0.3); padding: 10px 14px; text-align: left; font-size: 0.62rem; letter-spacing: 1.8px; text-transform: uppercase; color: #5a6a7a; font-weight: 700; }} | |
| td {{ padding: 9px 14px; border-bottom: 1px solid rgba(50, 60, 70, 0.3); color: #8a9aaa; background: rgba(15, 20, 26, 0.6); }} | |
| tr:hover td {{ background: rgba(30, 38, 48, 0.7); color: #a8b8c8; }} | |
| .caption {{ font-size: 0.68rem; color: #3a4a5a; font-family: 'JetBrains Mono', monospace; padding: 8px 2px 0 2px; letter-spacing: 0.4px; }} | |
| </style> | |
| </head> | |
| <body> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Mois</th> | |
| <th style="text-align:right">Capital Reel</th> | |
| <th style="text-align:right">Evolution Reelle</th> | |
| <th style="text-align:right">Capital Predit</th> | |
| </tr> | |
| </thead> | |
| <tbody>{rows_html}</tbody> | |
| </table> | |
| <div class="caption"> | |
| EVOLUTION REELLE : variation du capital sorti vs mois precedent. | |
| Rouge = hausse des sorties. {couleur_baisse} = baisse des sorties. | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| # ============================================================================ | |
| # MODULE PRINCIPAL | |
| # ============================================================================ | |
| def _stats_residus(valeurs): | |
| """Resume numerique de la distribution des residus (graphique H), pour le chat JASMINE.""" | |
| if not valeurs: | |
| return {} | |
| import statistics | |
| return { | |
| "n": len(valeurs), "moyenne": round(statistics.fmean(valeurs), 2), | |
| "ecart_type": round(statistics.pstdev(valeurs), 2) if len(valeurs) > 1 else 0.0, | |
| "min": round(min(valeurs), 2), "max": round(max(valeurs), 2), | |
| } | |
| def show_forecasting_module(client, sheet_name): | |
| apply_forecasting_styles() | |
| st.markdown('<div id="forecasting-module">', unsafe_allow_html=True) | |
| st.header("JASMINE — PREVISION DES FLUX DE SORTIE") | |
| st.caption("ANALYSE PREDICTIVE / REGRESSION LINEAIRE / TEMPS REEL") | |
| st.divider() | |
| col_sync1, col_sync2 = st.columns([4, 1]) | |
| with col_sync1: | |
| st.markdown(""" | |
| <div style="background:rgba(10,14,18,0.7);border-left:3px solid rgba(80,100,120,0.4);padding:10px 14px;"> | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:0.75rem;color:#5a6a7a;"> | |
| SOURCE : Prets_Master / Date_Deblocage + Montant_Capital. | |
| Cliquez sur SYNC pour actualiser les donnees de la feuille Forecasting. | |
| </span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col_sync2: | |
| if st.button("SYNC", use_container_width=True): | |
| with st.spinner("Synchronisation..."): | |
| result = actualiser_forecasting_depuis_prets(client, sheet_name) | |
| if result['success']: | |
| stats = result['stats'] | |
| st.success(f"OK — {stats['nb_conserves']} conserves, " | |
| f"{stats['nb_mis_a_jour']} mis a jour, " | |
| f"{stats['nb_ajoutes']} ajoutes.") | |
| with st.expander("DETAILS SYNCHRONISATION", expanded=False): | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Conserves", stats['nb_conserves']) | |
| c2.metric("Mis a jour", stats['nb_mis_a_jour']) | |
| c3.metric("Ajoutes", stats['nb_ajoutes']) | |
| st.rerun() | |
| else: | |
| st.error(f"ERREUR : {result['message']}") | |
| try: | |
| if forecasting_needs_sync(client, sheet_name): | |
| with st.spinner("Actualisation automatique des previsions..."): | |
| actualiser_forecasting_depuis_prets(client, sheet_name) | |
| sh = client.open(sheet_name) | |
| ws_forecasting = sh.worksheet("Forecasting") | |
| df_forecasting = pd.DataFrame(ws_forecasting.get_all_records()) | |
| if df_forecasting.empty or len(df_forecasting) < 2: | |
| st.warning("Donnees insuffisantes — minimum 2 points requis.") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| if 'Date' not in df_forecasting.columns or 'Montant_Total_Sortie' not in df_forecasting.columns: | |
| st.error("Structure invalide. Colonnes requises : Date, Montant_Total_Sortie") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| analyzer = VortexOutFlux(df_forecasting) | |
| except Exception as e: | |
| st.error(f"Erreur de connexion : {e}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| try: | |
| predictions_result = analyzer.predict_next_months(n_months=3) | |
| if 'error' in predictions_result: | |
| st.error(f"Erreur modele : {predictions_result['error']}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| predictions_df = predictions_result['predictions'] | |
| models = predictions_result['models'] | |
| viz_data = analyzer.generate_visualization_data(predictions_result) | |
| interpretations = viz_data['interpretations'] | |
| except Exception as e: | |
| st.error(f"Erreur lors du calcul predictif : {e}") | |
| st.exception(e) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| # Chat JASMINE scoppé à cette page : questions sur les prévisions, l'historique et le signal. | |
| from modules.jasmine_widget import render_scoped_widget | |
| render_scoped_widget( | |
| client, sheet_name, | |
| scope_label="Prévision des flux de sortie (historique Forecasting, prévisions 3 mois, régression linéaire)", | |
| context_data={ | |
| "historique_recent": df_forecasting.tail(24).to_dict("records"), | |
| "previsions_3_mois": predictions_df.to_dict("records") if hasattr(predictions_df, "to_dict") else predictions_df, | |
| "interpretations": interpretations, | |
| "nb_points_historique": int(len(df_forecasting)), | |
| "metriques_modele": viz_data["metrics"], | |
| # Graphique C : historique reel vs ajuste par le modele, et prevision | |
| "graphique_C_historique": [ | |
| {"date": d, "reel": round(v, 2), "ajuste_modele": round(p, 2)} | |
| for d, v, p in zip(viz_data["historical"]["dates"], viz_data["historical"]["values"], | |
| viz_data["historical"]["predictions_hist"]) | |
| ], | |
| "graphique_C_prevision": viz_data["future"], | |
| # Graphique H (section « H — ANALYSE DES RESIDUS ») : un point par mois = (valeur predite, residu = reel - predit) | |
| "graphique_H_residus_vs_predits": [ | |
| {"valeur_predite": round(p, 2), "residu": round(r, 2)} | |
| for p, r in zip(viz_data["residuals"]["predictions"], viz_data["residuals"]["values"]) | |
| ], | |
| "graphique_H_stats_residus": _stats_residus(viz_data["residuals"]["values"]), | |
| "graphique_H_lecture": ("Axe X = valeur predite par le modele, axe Y = residu (reel - predit). " | |
| "Un bon modele a des residus epars autour de 0 sans structure ; " | |
| "une tendance ou une forme en eventail indique un biais ou une heteroscedasticite. " | |
| "Second panneau : distribution des residus (histogramme)."), | |
| }, | |
| key_prefix="forecasting", | |
| ) | |
| # ========================================================================= | |
| # SECTION A — SIGNAL OPÉRATIONNEL | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">A — Signal operationnel</div>', unsafe_allow_html=True) | |
| try: | |
| signal_data = analyzer.compute_next_month_signal(models) | |
| card_predit, card_evo = render_next_month_cards(signal_data) | |
| col_c1, col_c2 = st.columns(2) | |
| with col_c1: | |
| st.markdown(card_predit, unsafe_allow_html=True) | |
| with col_c2: | |
| st.markdown(card_evo, unsafe_allow_html=True) | |
| st.markdown(render_signal_block(signal_data), unsafe_allow_html=True) | |
| except Exception as e: | |
| st.warning(f"Signal non disponible : {e}") | |
| # ========================================================================= | |
| # SECTION B — KPIs HISTORIQUES | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">B — Donnees historiques</div>', unsafe_allow_html=True) | |
| nb_points = len(analyzer.df) | |
| flux_moy = analyzer.df['Montant_Total_Sortie'].mean() | |
| flux_total = analyzer.df['Montant_Total_Sortie'].sum() | |
| periode = (f"{analyzer.df['Date'].min().strftime('%m/%Y')} — " | |
| f"{analyzer.df['Date'].max().strftime('%m/%Y')}") | |
| st.markdown(f""" | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:8px;"> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">Points</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{nb_points}</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">Flux moyen</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{_fmt(flux_moy)} XOF</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">Flux total</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{_fmt(flux_total)} XOF</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">Periode</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.1rem;font-weight:700;color:#a8b8c8;">{periode}</div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ========================================================================= | |
| # SECTION C — GRAPHIQUE PRINCIPAL | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">C — Visualisation historique et prediction</div>', unsafe_allow_html=True) | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=viz_data['historical']['dates'], y=viz_data['historical']['values'], | |
| mode='lines+markers', name='Capital reel sorti', | |
| line=dict(color='#a8b8c8', width=1.5), | |
| marker=dict(size=5, symbol='circle', color='#a8b8c8'), | |
| )) | |
| fig.add_trace(go.Scatter( | |
| x=viz_data['historical']['dates'], y=viz_data['historical']['predictions_hist'], | |
| mode='lines', name='Regression (historique)', | |
| line=dict(color='#3a5a8a', width=1, dash='dot'), | |
| )) | |
| fig.add_trace(go.Scatter( | |
| x=viz_data['future']['dates'], y=viz_data['future']['predictions'], | |
| mode='lines+markers', name='Prediction', | |
| line=dict(color='#58a6ff', width=1.5, dash='dash'), | |
| marker=dict(size=7, symbol='diamond', color='#58a6ff'), | |
| )) | |
| fig.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(88,166,255,0.07)', | |
| line=dict(color='rgba(88,166,255,0)'), | |
| name='Intervalle 95%', showlegend=True, | |
| )) | |
| last_hist_date = pd.Timestamp(viz_data['historical']['dates'][-1]) | |
| fig.add_vline( | |
| x=last_hist_date.timestamp() * 1000, | |
| line_width=1, line_dash='dot', line_color='rgba(100,120,140,0.4)', | |
| annotation_text='HISTORIQUE / PREDICTION', | |
| annotation_position='top left', | |
| annotation_font_size=9, annotation_font_color='#4a5a6a', | |
| ) | |
| fig.update_layout( | |
| height=440, margin=dict(l=0, r=0, t=30, b=0), | |
| plot_bgcolor='rgba(10,14,18,0.9)', paper_bgcolor='rgba(15,20,26,0.0)', | |
| font=dict(family='JetBrains Mono, Courier New, monospace', color='#5a6a7a', size=10), | |
| xaxis=dict(gridcolor='rgba(50,65,80,0.25)', linecolor='rgba(80,100,120,0.3)', | |
| tickcolor='rgba(80,100,120,0.3)', title_text=''), | |
| yaxis=dict(gridcolor='rgba(50,65,80,0.25)', linecolor='rgba(80,100,120,0.3)', | |
| tickcolor='rgba(80,100,120,0.3)', title_text='XOF'), | |
| legend=dict(bgcolor='rgba(15,20,26,0.85)', bordercolor='rgba(80,100,120,0.2)', | |
| borderwidth=1, font=dict(size=10, family='JetBrains Mono, monospace')), | |
| hovermode='x unified', | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ========================================================================= | |
| # SECTION D — PRÉDICTIONS 3 MOIS | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">D — Predictions 3 prochains mois</div>', unsafe_allow_html=True) | |
| cols_pred = st.columns(3) | |
| for idx, (_, row) in enumerate(predictions_df.iterrows()): | |
| with cols_pred[idx]: | |
| st.markdown(render_forecast_month_card(row), unsafe_allow_html=True) | |
| # ========================================================================= | |
| # SECTION E — TABLEAU COMPARATIF | |
| # ========================================================================= | |
| st.divider() | |
| from core import exercice as ex | |
| annee_cmp = ex.exercice_selectionne() | |
| debut_cmp, fin_cmp = ex.bornes(annee_cmp) if annee_cmp else (pd.Timestamp("2000-01-01"), None) | |
| titre_cmp = f"exercice {annee_cmp}" if annee_cmp else "toutes annees" | |
| st.markdown(f'<div class="section-tag">E — Comparatif reel vs predit ({titre_cmp})</div>', unsafe_allow_html=True) | |
| try: | |
| df_cmp = analyzer.build_comparison_table(models, cutoff_date=debut_cmp, end_date=fin_cmp) | |
| if df_cmp.empty: | |
| st.info(f"Aucune donnee disponible pour l'{titre_cmp}.") | |
| else: | |
| nb_lignes = len(df_cmp) | |
| hauteur_table = 45 + nb_lignes * 38 + 40 | |
| components.html(render_comparison_table_html(df_cmp), height=hauteur_table, scrolling=False) | |
| except Exception as e: | |
| st.warning(f"Tableau non disponible : {e}") | |
| # ========================================================================= | |
| # SECTION F — PERFORMANCE DU MODÈLE | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">F — Performance du modele</div>', unsafe_allow_html=True) | |
| r2 = models['r_squared'] | |
| r = models['r_coefficient'] | |
| mae = models['mae'] | |
| rmse = models['rmse'] | |
| pente = models['pente'] | |
| st.markdown(f""" | |
| <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:16px;"> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">R²</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{r2:.3f}</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">R</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{r:.3f}</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">MAE</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.1rem;font-weight:700;color:#a8b8c8;">{_fmt(mae)} XOF</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">RMSE</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.1rem;font-weight:700;color:#a8b8c8;">{_fmt(rmse)} XOF</div> | |
| </div> | |
| <div style="background:rgba(15,20,26,0.85);border:1px solid rgba(100,120,140,0.25);border-top:2px solid rgba(100,120,140,0.5);padding:14px 16px;"> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#5a6a7a;margin-bottom:6px;">Pente</div> | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;font-weight:700;color:#a8b8c8;">{pente:.2f} XOF/j</div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="margin-top:4px;background:rgba(10,14,18,0.7);border-left:3px solid rgba(80,100,120,0.35); | |
| padding:12px 16px;font-family:'JetBrains Mono',monospace;font-size:0.78rem;color:#5a6a7a;line-height:1.7;"> | |
| <strong style="color:#7a8a9a;letter-spacing:1px;">TENDANCE : {interpretations['tendance']}</strong><br> | |
| {interpretations['message_principal']}<br> | |
| {interpretations['message_fiabilite']}<br> | |
| {interpretations['message_precision']} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ========================================================================= | |
| # SECTION G — INDICE DE CONFIANCE | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown('<div class="section-tag">G — Indice de confiance de la prediction</div>', unsafe_allow_html=True) | |
| try: | |
| confidence_data = analyzer.compute_confidence_score(models) | |
| st.markdown(render_confidence_block(confidence_data), unsafe_allow_html=True) | |
| except Exception as e: | |
| st.warning(f"Indice de confiance non disponible : {e}") | |
| # ========================================================================= | |
| # SECTION H — RÉSIDUS | |
| # ========================================================================= | |
| st.divider() | |
| with st.expander("H — ANALYSE DES RESIDUS (DIAGNOSTIC DU MODELE)", expanded=False): | |
| fig_res = make_subplots( | |
| rows=1, cols=2, | |
| subplot_titles=("Residus vs Valeurs Predites", "Distribution des Residus"), | |
| specs=[[{"type": "scatter"}, {"type": "histogram"}]], | |
| ) | |
| fig_res.add_trace(go.Scatter( | |
| x=viz_data['residuals']['predictions'], y=viz_data['residuals']['values'], | |
| mode='markers', marker=dict(color='#58a6ff', size=6, opacity=0.7), name='Residus', | |
| ), row=1, col=1) | |
| fig_res.add_hline(y=0, line_dash='dot', line_color='rgba(100,120,140,0.5)', row=1, col=1) | |
| fig_res.add_trace(go.Histogram( | |
| x=viz_data['residuals']['values'], | |
| marker=dict(color='#3a5a8a', line=dict(color='#5a7aaa', width=0.5)), | |
| nbinsx=12, name='Distribution', | |
| ), row=1, col=2) | |
| _ax = dict(gridcolor='rgba(50,65,80,0.25)', linecolor='rgba(80,100,120,0.3)', | |
| tickcolor='rgba(80,100,120,0.3)') | |
| fig_res.update_xaxes(**_ax) | |
| fig_res.update_yaxes(**_ax) | |
| fig_res.update_layout( | |
| height=320, margin=dict(l=0, r=0, t=30, b=0), | |
| plot_bgcolor='rgba(10,14,18,0.9)', paper_bgcolor='rgba(15,20,26,0.0)', | |
| font=dict(family='JetBrains Mono, monospace', color='#5a6a7a', size=9), | |
| showlegend=False, | |
| ) | |
| st.plotly_chart(fig_res, use_container_width=True) | |
| st.markdown(""" | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:0.7rem;color:#4a5a6a;"> | |
| Un bon modele presente des residus aleatoirement distribues autour de zero, sans structure apparente. | |
| </span> | |
| """, unsafe_allow_html=True) | |
| # ========================================================================= | |
| # SECTION I — NOTE MÉTHODOLOGIQUE | |
| # ========================================================================= | |
| st.divider() | |
| st.markdown(""" | |
| <div style="font-family:'JetBrains Mono',monospace;font-size:0.7rem;color:#3a4a5a;line-height:1.8;padding:12px 0;"> | |
| NOTE METHODOLOGIQUE — Modele : regression lineaire simple (sklearn). | |
| Intervalles de confiance : 95% par methode d erreur standard des residus. | |
| Signal operationnel : seuil de reduction a -10%, seuil d augmentation a +10%. | |
| Indice de confiance : base sur MAPE, CV des residus et R² du modele. | |
| Mise a jour des donnees : automatique si perimee (+ de 12h) ou absente, ou manuellement via bouton SYNC. | |
| Pour des patterns saisonniers complexes, envisager SARIMA ou Prophet. | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) |