"""
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"""
{row['Mois']}
{_fmt(row['Capital_Reel'])} XOF
{evo_str}
{_fmt(row['Capital_Predit'])} XOF
"""
# 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"""
Mois
Capital Reel
Evolution Reelle
Capital Predit
{rows_html}
EVOLUTION REELLE : variation du capital sorti vs mois precedent.
Rouge = hausse des sorties. {couleur_baisse} = baisse des sorties.
"""
# ============================================================================
# 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('
', 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("""
SOURCE : Prets_Master / Date_Deblocage + Montant_Capital.
Cliquez sur SYNC pour actualiser les donnees de la feuille Forecasting.
""", 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('
', 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('', unsafe_allow_html=True)
return
analyzer = VortexOutFlux(df_forecasting)
except Exception as e:
st.error(f"Erreur de connexion : {e}")
st.markdown('', 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('', 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('', 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('
A — Signal operationnel
', 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('
""", unsafe_allow_html=True)
# =========================================================================
# SECTION G — INDICE DE CONFIANCE
# =========================================================================
st.divider()
st.markdown('
G — Indice de confiance de la prediction
', 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("""
Un bon modele presente des residus aleatoirement distribues autour de zero, sans structure apparente.
""", unsafe_allow_html=True)
# =========================================================================
# SECTION I — NOTE MÉTHODOLOGIQUE
# =========================================================================
st.divider()
st.markdown("""
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.