mlopsgenai / src /streamlit_app.py
alexandre-cameron-borges's picture
Update src/streamlit_app.py
c93ee2a verified
import streamlit as st
import pandas as pd
import json
from typing import Any, Dict
from agent import build_agent, chat, ml_predict # ton fichier agent.py
# ========== CONFIG STREAMLIT ==========
st.set_page_config(
page_title="GENAI – Banking Lab",
page_icon="🤖",
layout="wide"
)
# ========== SESSION STATE ==========
if "agent" not in st.session_state:
st.session_state.agent = build_agent()
if "messages" not in st.session_state:
st.session_state.messages = [] # [{"role": "user"/"assistant", "content": "..."}]
if "uploaded_df" not in st.session_state:
st.session_state.uploaded_df = None
agent = st.session_state.agent
# ========= PAGE HEADER GLOBAL =========
st.title("GENAI – Banking Lab")
# ========= NAVIGATION PAR ONGLET EN HAUT =========
tab_eda, tab_ml, tab_chat = st.tabs(["📊 EDA", "🔮 Prédiction ML", "💬 Chatbot"])
# ==================== PAGE 1 : EDA ====================
with tab_eda:
st.header("📊 Analyse Exploratoire – Risque Crédit")
st.markdown(
"""
Explore les caractéristiques des clients et comprends les patterns associés au **risque de défaut**.
"""
)
# ================= CHARGEMENT CSV =================
uploaded_file = st.file_uploader("📂 Charger un fichier CSV (dataset crédit)", type=["csv"])
if uploaded_file:
df = pd.read_csv(uploaded_file)
st.session_state.uploaded_df = df
else:
df = st.session_state.uploaded_df if st.session_state.uploaded_df is not None else None
if df is None:
st.info("👉 Charge un fichier CSV pour commencer l'analyse.")
st.stop()
st.success(f"Dataset chargé : **{df.shape[0]} lignes**, **{df.shape[1]} colonnes**")
# ================= APERCU =================
st.markdown("### 👀 Aperçu du dataset")
st.dataframe(df.head(), use_container_width=True)
# ================= INDICATEURS GLOBAUX =================
default_rate = df["default"].mean() * 100
colA, colB, colC = st.columns(3)
colA.metric("Taux de défaut global", f"{default_rate:.1f} %")
colB.metric("Clients sains", f"{(df['default']==0).sum()}")
colC.metric("Clients en défaut", f"{(df['default']==1).sum()}")
st.markdown("---")
# ================= DISTRIBUTIONS PAR DEFAUT =================
st.markdown("## 📈 Variables clés vs défaut")
numeric_cols = [
"fico_score", "debt_ratio", "income", "years_employed",
"loan_amt_outstanding", "total_debt_outstanding"
]
var = st.selectbox("Choisis une variable à explorer :", numeric_cols)
import altair as alt
chart = alt.Chart(df).mark_bar(opacity=0.7).encode(
x=alt.X(var, bin=alt.Bin(maxbins=30)),
y="count()",
color=alt.Color("default:N", legend=alt.Legend(title="Default (0=OK, 1=Défaut)"))
).properties(width=650, height=350)
st.altair_chart(chart)
st.markdown("---")
# ================= CORRÉLATION =================
st.markdown("## 🔗 Matrice de corrélation")
corr = df.corr(numeric_only=True)
st.dataframe(corr.style.background_gradient(cmap="Reds"), use_container_width=True)
# Top variables explicatives
st.markdown("### 🥇 Variables les plus corrélées avec le défaut")
corr_default = corr["default"].drop("default").sort_values(ascending=False)
st.bar_chart(corr_default)
st.markdown("---")
# ================= SCATTERPLOT =================
st.markdown("## 🧭 Scatterplot – localiser les zones à risque")
x_var = st.selectbox("Axe X", numeric_cols, index=2)
y_var = st.selectbox("Axe Y", numeric_cols, index=0)
scatter = alt.Chart(df).mark_circle(size=60, opacity=0.6).encode(
x=x_var,
y=y_var,
color=alt.Color("default:N", legend=alt.Legend(title="Défaut")),
tooltip=["income", "fico_score", "debt_ratio", "default"]
).properties(width=750, height=450)
st.altair_chart(scatter)
st.success("Analyse EDA terminée ✔️")
# ==================== PAGE 2 : FORMULAIRE PRÉDICTION ML ====================
with tab_ml:
st.header("🔮 Prédiction de risque via le modèle ML (.pkl sur S3)")
st.markdown(
"""
Remplis ce **questionnaire** : nous estimons ensuite le risque de défaut du client,
et nous t’affichons une explication claire et visuelle.
"""
)
col_left, col_right = st.columns([1, 1])
# ========================= FORMULAIRE =========================
with col_left:
st.markdown("### 🎯 Profil client / crédit")
credit_lines = st.number_input(
"Lignes de crédit ouvertes (credit_lines_outstanding)",
min_value=0, max_value=50, value=5
)
loan_amt = st.number_input(
"Montant du prêt en cours (€) – loan_amt_outstanding",
min_value=0, max_value=1_000_000, value=15_000, step=1_000
)
total_debt = st.number_input(
"Dette totale actuelle (€) – total_debt_outstanding",
min_value=0, max_value=1_000_000, value=25_000, step=1_000
)
income = st.number_input(
"Revenu annuel (€) – income",
min_value=1, max_value=1_000_000, value=60_000, step=1_000
)
years = st.number_input(
"Ancienneté dans l'emploi (années) – years_employed",
min_value=0, max_value=50, value=10
)
fico = st.number_input(
"Score FICO – fico_score",
min_value=300, max_value=850, value=720
)
debt_ratio = total_debt / income if income > 0 else 0.0
st.metric("Debt ratio calculé", f"{debt_ratio:.2f}")
default_payload = {
"credit_lines_outstanding": credit_lines,
"loan_amt_outstanding": loan_amt,
"total_debt_outstanding": total_debt,
"income": income,
"years_employed": years,
"fico_score": fico,
"debt_ratio": debt_ratio
}
# ========================= JSON EDITABLE =========================
with col_right:
st.markdown("### 🧾 Payload JSON (optionnel)")
st.caption("Tu peux garder ce JSON tel quel ou l’ajuster manuellement avant la prédiction.")
payload_str = st.text_area(
"Payload envoyé à `ml_predict` :",
value=json.dumps(default_payload, indent=2),
height=260
)
lancer = st.button("🚀 Lancer la prédiction ML", type="primary")
# ========================= PRÉDICTION & AFFICHAGE UX =========================
if lancer:
try:
payload = json.loads(payload_str)
except json.JSONDecodeError as e:
st.error(f"JSON invalide : {e}")
payload = None
if payload is not None:
with st.spinner("Analyse du risque par le modèle…"):
try:
raw = ml_predict.invoke({"payload": payload})
except Exception as e:
st.error(f"Erreur lors de l’appel de ml_predict : {e}")
raw = None
if raw is not None:
# On essaye de parser le JSON retourné par le tool
prediction = None
try:
parsed = json.loads(raw)
prediction = parsed.get("prediction", {})
except Exception:
prediction = None
if prediction is None or not isinstance(prediction, dict):
st.error("La réponse du modèle n’est pas dans le format attendu.")
st.code(raw, language="json")
else:
label_name = prediction.get("label_name", "Résultat inconnu")
risk_level = prediction.get("risk_level", "inconnu")
proba_default = prediction.get("proba_default", None)
explanation = prediction.get("explanation", "")
features_used = prediction.get("features_used", [])
# --------- Traduction du niveau de risque en jauge ----------
if isinstance(proba_default, (float, int)):
proba_pct = max(0.0, min(float(proba_default), 1.0)) * 100
else:
# fallback selon risk_level
mapping = {"faible": 15.0, "modéré": 35.0, "élevé": 70.0}
proba_pct = mapping.get(risk_level, 50.0)
# Couleur / emoji selon le risque
if risk_level == "faible":
emoji = "🟢"
texte_risque = "Risque faible"
elif risk_level == "modéré":
emoji = "🟠"
texte_risque = "Risque modéré"
elif risk_level == "élevé":
emoji = "🔴"
texte_risque = "Risque élevé"
else:
emoji = "⚪"
texte_risque = "Risque non déterminé"
st.markdown("---")
st.subheader("🧠 Résultat de l’analyse du modèle")
# Bloc résumé pour un client
col_r1, col_r2 = st.columns([2, 1])
with col_r1:
st.markdown(
f"""
**Verdict : {emoji} {label_name}**
**Niveau de risque : {texte_risque}**
"""
)
if isinstance(proba_default, (float, int)):
st.markdown(
f"Le modèle estime une probabilité de défaut d’environ **{proba_pct:.1f}%**."
)
if explanation:
st.markdown(f"📝 *{explanation}*")
with col_r2:
st.markdown("### 📊 Jauge de risque")
st.progress(int(proba_pct))
# Features utilisées – version simple
if features_used:
st.markdown("### 🔍 Variables prises en compte")
st.write(", ".join(features_used))
# Détails techniques en expander
with st.expander("🔧 Détails techniques / JSON brut"):
st.markdown("**Réponse brute du tool `ml_predict` :**")
st.code(raw, language="json")
try:
st.markdown("**Vue JSON parsée :**")
st.json(parsed)
except Exception:
pass
st.markdown("---")
st.caption(
"💡 Astuce : cette page sert pour les utilisateurs métier. "
"Les développeurs peuvent récupérer le payload et la réponse brute dans l’expander."
)
# ==================== PAGE 3 : CHATBOT ====================
with tab_chat:
st.header("💬 Chat avec l’agent (web + RAG + ML)")
st.markdown(
"""
Exemple de requêtes :
- *“Résume-moi les frais de tenue de compte pour un non résident.”*
- *“Utilise `rag_search` pour extraire les tarifs de découvert.”*
- *“Appelle `ml_predict` avec {'credit_lines_outstanding': 5, ...} et explique le résultat.”*
"""
)
# Affichage de l'historique
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# Champ d'entrée
prompt = st.chat_input("Pose une question à l’agent…")
if prompt:
# 1. Ajout du message utilisateur
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 2. Appel agent AVEC L’HISTORIQUE COMPLET
with st.chat_message("assistant"):
with st.spinner("L’agent réfléchit…"):
try:
answer = chat(agent, st.session_state.messages)
except Exception as e:
answer = f"❌ ERREUR agent: {e}"
st.markdown(answer)
# 3. Ajout de la réponse assistant dans la mémoire
st.session_state.messages.append({"role": "assistant", "content": answer})