import streamlit as st import pandas as pd import json import plotly.express as px from pathlib import Path import plotly.graph_objects as go st.set_page_config(page_title="Monitoring Drift", layout="wide") # Charger le rapport Evidently with open("Monitoring/drift_report.json", "r", encoding="utf-8") as f: drift_data = json.load(f) # # Tests pour vérifier la structure du json drift # for m in drift_data["metrics"]: # if m["metric"] == "ColumnSummaryMetric": # st.json(m) # break # for m in drift_data["metrics"]: # if m["metric"] == "ColumnSummaryMetric": # st.write(m) # break # for m in drift_data["metrics"]: # if m["metric"] == "ColumnSummaryMetric": # print("======") # print(m.keys()) # print(m["result"].keys()) # break # for m in drift_data["metrics"]: # if m["metric"] == "ColumnDriftMetric": # st.json(m) # break # metrics_names = set( # m["metric"] # for m in drift_data["metrics"]) # st.write(metrics_names) # st.json(drift_data["metrics"][0]) # Charger les logs bruts df_logs = pd.read_parquet("logs/predictions_log.parquet") df_logs["timestamp"] = pd.to_datetime(df_logs["timestamp_x"], unit="s") # Charger les logs après optimisation df_logs_opti = pd.read_parquet("logs/predictions_log_batch.parquet") df_logs_opti["timestamp_model"] = pd.to_datetime(df_logs_opti["timestamp_model"], unit="s") # st.write(df_logs.head()) # st.write(df_logs_opti.head()) st.title("📊 Monitoring du Drift — Dashboard MLOps") # ONGLET 1 — Vue d’ensemble tab1, tab2, tab3, tab4, tab5 = st.tabs(["VUE D'ENSEMBLE", "DERIVE PAR COLONNE", "LOGS BRUTS", "IMPACT OPTIMISATION", "COMPARAISON UNITAIRE vs BATCH"]) with tab1: # Colonnes en dérive - Résultat global dataset_drift = next( m for m in drift_data["metrics"] if m["metric"] == "DatasetDriftMetric") drift_res = dataset_drift["result"] with st.container(): st.markdown("""

Performance Globale Colonnes

""", unsafe_allow_html=True) col1, col2, col3, col4 = st.columns([1,1,1,1]) st.markdown("
", unsafe_allow_html=True) col1.metric("Prédictions", len(df_logs)) col2.metric("Colonnes analysées", drift_res["number_of_columns"]) col3.metric("Colonnes en dérive", drift_res["number_of_drifted_columns"]) col4.metric( "Taux de dérive", f"{100*drift_res['share_of_drifted_columns']:.1f}%") # KPI API with st.container(): st.markdown("""

Performance API

""", unsafe_allow_html=True) c1, c2, c3, c4, c5, c6 = st.columns([1,1,1,1,1,1]) st.markdown("
", unsafe_allow_html=True) c1.metric("Latence moyenne", f"{df_logs['latency_ms'].mean():.2f}") c2.metric("Temps d'inférence moyen", f"{df_logs['inference_ms_x'].mean():.2f}") c3.metric("CPU moyen", f"{df_logs['cpu_percent'].mean():.1f}%") c4.metric("RAM moyenne", f"{df_logs['ram_percent'].mean():.1f}%") c5.metric("Charge système", f"{df_logs['system_load'].mean():.2f}") c6.metric("Threads moyens", f"{df_logs['num_threads'].mean():.0f}") # KPI Métier with st.container(): st.markdown("""

Performance Métier

""", unsafe_allow_html=True) m1, m2, m3 = st.columns([1,1,1]) st.markdown("
", unsafe_allow_html=True) m1.metric("Probalité moyenne", f"{df_logs['score_x'].mean():.2f}") m2.metric("Seuil optimal moyen", f"{df_logs['threshold_x'].mean():.3f}") m3.metric("Décision majoritaire", df_logs["decision_x"].mode()[0]) with st.container(): g1, g2 = st.columns([1,1]) # Graphique distribution des scores score_df = (df_logs["score_x"].value_counts().reset_index()) fig = px.bar(score_df, x = "score_x", y = "count", title = "Répartition des scores") g1.plotly_chart(fig, width = "stretch") # Graphique distribution décisions decision_df = (df_logs["decision_x"].value_counts().reset_index()) fig = px.bar(decision_df, x = "decision_x", y = "count", title = "Répartition des décisions") g2.plotly_chart(fig, width = "stretch") with st.container(): h1, h2, h3 = st.columns([1,1,1]) # Latence dans le temps fig = px.line(df_logs, x="timestamp", y="latency_ms", title = "Distribution de la latence dans le temps") h1.plotly_chart(fig, width = "stretch") # CPU dans le temps fig = px.line(df_logs, x="timestamp", y="cpu_percent", title = "Distribution du CPU dans le temps") h2.plotly_chart(fig, width = "stretch") # RAM dans le temps fig = px.line(df_logs, x="timestamp", y="ram_percent", title = "Distribution de la RAM dans le temps") h3.plotly_chart(fig, width = "stretch") # Graphique distribution colonnes en dérive drift_graph = pd.DataFrame({ "Etat": [ "En dérive", "Sans dérive"], "Nombre": [ drift_res["number_of_drifted_columns"], drift_res["number_of_columns"] - drift_res["number_of_drifted_columns"]]}) fig = px.bar(drift_graph, x = "Etat", y = "Nombre", title = "Répartition des colonnes en dérive") st.plotly_chart(fig, width = "stretch") # Liste des colonnes en drift drifted_cols = [] for m in drift_data["metrics"]: if m["metric"] != "ColumnDriftMetric": continue if m['result']['drift_detected']: drifted_cols.append(m["result"]["column_name"]) st.subheader("Colonnes en dérive") st.write(drifted_cols) # ONGLET 2 — Dérive par colonne with tab2: st.header("📈 Dérive par colonne") summary_dict = {} for m in drift_data["metrics"]: if m["metric"] != "ColumnSummaryMetric": continue result = m['result'] summary_dict[result["column_name"]] = { "missing": result["current_characteristics"]["missing"], "missing_pct": result["current_characteristics"]["missing_percentage"], "unique": result["current_characteristics"]["unique"]} drift_rows = [] for m in drift_data["metrics"]: if m["metric"] != "ColumnDriftMetric": continue result = m['result'] col = result["column_name"] # missing_data = (summary_dict[col]["current_characteristics"]["missing"]) drift_rows.append({ "Colonne": col, "Type": result["column_type"], "Distance drift": result["drift_score"], "Dérive(%)": round(result["drift_score"] * 100, 2), "Seuil(%)": round(result["stattest_threshold"] * 100, 2), "Drift détecté": result["drift_detected"], "Test": result["stattest_name"], "Nombre valeurs manquantes": summary_dict[col]["missing"], "Taux valeurs manquantes": summary_dict[col]["missing_pct"], "Nombre de valeurs uniques": summary_dict[col]["unique"]}) df_drift = pd.DataFrame(drift_rows) df_drift_filtered = (df_drift[df_drift["Drift détecté"] == True].drop_duplicates(subset=["Colonne"])) # uniquement les colonnes en dérive pour voir le taux de dérive par colonne st.dataframe(df_drift_filtered.sort_values("Distance drift", ascending = False), width = "stretch") selected_col = st.selectbox("Choisir une colonne", df_drift_filtered["Colonne"], key = "drift_column") col_stats = df_drift_filtered[df_drift_filtered["Colonne"] == selected_col].iloc[0] metric = next(m for m in drift_data["metrics"]if (m["metric"] == "ColumnDriftMetric" and m["result"]["column_name"] == selected_col)) x_ref = metric["result"]["reference"]["small_distribution"]["x"] y_ref = metric["result"]["reference"]["small_distribution"]["y"] x_cur = metric["result"]["current"]["small_distribution"]["x"] y_cur = metric["result"]["current"]["small_distribution"]["y"] summary_metric = next(m for m in drift_data["metrics"] if (m["metric"] == "ColumnSummaryMetric" and m["result"]["column_name"] == selected_col)) missing_ref = summary_metric["result"]["reference_characteristics"]["missing"] missing_cur = summary_metric["result"]["current_characteristics"]["missing"] with st.container(): st.write(f"Analyse détaillée: {selected_col}") st.markdown(f""" - **Distance drift** : `{col_stats['Distance drift']:.4f}` - **Dérive (%)** : `{col_stats['Dérive(%)']} %` - **Seuil (%)** : `{col_stats['Seuil(%)']} %` - **Drift détecté** : `{"Oui" if col_stats['Drift détecté'] else "Non"}` """) d1, d2 = st.columns([1,1]) # Distribution des données fig = go.Figure() fig.add_bar(x = x_ref, y = y_ref, name = "Référence") fig.add_bar(x = x_cur, y = y_cur, name = "Production") fig.update_layout(barmode = "group", title = f"Distribution - {selected_col}", xaxis_title = "Valeurs", yaxis_title = "Densité", legend_title = "") d1.plotly_chart(fig, width = "stretch") # Distribution valeurs manquantes fig_missing = go.Figure() fig_missing.add_bar(x = ["Référence"], y = [missing_ref], name = "Référence") fig_missing.add_bar(x = ["Production"], y = [missing_cur], name = "Production") fig_missing.update_layout(barmode="group", title=f"Valeurs manquantes - {selected_col}", yaxis_title = "Nombre de valeurs manquantes") d2.plotly_chart(fig_missing, width = "stretch") # ONGLET 3 — Résumé par colonne (finalement faire plutôt un tableau?) # with tab3: # st.header("📘 Résumé statistique par colonne") # summaries = {} # for m in drift_data["metrics"]: # if m["metric"] != "ColumnSummaryMetric": # continue # result2 = m['result'] # if ("column_name" not in result2 or result2["column_name"] is None or "reference_characteristics" not in result2 or "current_characteristics" not in result2): # continue # summaries[result2["column_name"]] = result2 # options = sorted(summaries.keys()) # col_selectione = st.selectbox("Choisir une colonne", options, index=0) # # col_selectione = st.selectbox("Choisir une colonne", sorted(summaries.keys()), key="summary_column") # summary = summaries[col_selectione] # ref = summary["reference_characteristics"] # cur = summary["current_characteristics"] # st.subheader(f"🔹 {col_selectione}") # col1, col2 = st.columns(2) # with col1: # st.markdown("### Référence") # st.dataframe(pd.DataFrame(ref.items(), columns=["Métrique", "Valeur"]), width = True) # with col2: # st.markdown("### Production") # st.dataframe(pd.DataFrame(cur.items(), columns=["Métrique", "Valeur"]), width = True) # # comparaison synthétique # compare_df = pd.DataFrame({ # "Métrique": ["count", "missing", "mean", "std", "min", "p25", "p50", "p75", "max", "unique"], # "Référence": [ref.get("count"), ref.get("missing"), ref.get("mean"), ref.get("std"), ref.get("min"), ref.get("p25"), # ref.get("p50"), ref.get("p75"), ref.get("max"), ref.get("unique")], # "Production": [cur.get("count"), cur.get("missing"), cur.get("mean"), cur.get("std"), cur.get("min"), cur.get("p25"), # cur.get("p50"), cur.get("p75"), cur.get("max"), cur.get("unique")]}) # st.markdown("### Comparaison directe") # st.dataframe(compare_df, width = True) # ONGLET 4 — Logs bruts with tab3: st.header("📄 Logs bruts (production)") st.dataframe(df_logs.head(200)) col = st.selectbox("Visualiser une colonne :", df_logs.columns) fig = px.histogram(df_logs, x=col, nbins=30, title=f"Distribution de {col}") st.plotly_chart(fig, width="stretch") # ONGLET 5 - KPI API après optimisation du processing with tab4: # KPI API with st.container(): st.markdown("""

Performance API (Batch)

""", unsafe_allow_html=True) if df_logs_opti.empty: st.warning("Aucun batch n'a encore été exécuté. Lance un batch pour voir les KPI.") else: k1, k2, k3, k4, k5, k6 = st.columns([1,1,1,1,1,1]) k1.metric("Latence moyenne", f"{df_logs_opti['latency_ms_ops'].mean():.2f}") k2.metric("Temps d'inférence moyen", f"{df_logs_opti['inference_ms_model'].mean():.2f}") k3.metric("CPU moyen", f"{df_logs_opti['cpu_percent_ops'].mean():.1f}%") k4.metric("RAM moyenne", f"{df_logs_opti['ram_percent_ops'].mean():.1f}%") k5.metric("Charge système", f"{df_logs_opti['system_load_ops'].mean():.2f}") k6.metric("Threads moyens", f"{df_logs_opti['num_threads_ops'].mean():.0f}") st.markdown("
", unsafe_allow_html=True) with st.container(): o1, o2, o3 = st.columns([1,1,1]) # Latence dans le temps fig = px.line(df_logs_opti, x="timestamp_model", y="latency_ms_ops", title = "Latence du batch dans le temps") o1.plotly_chart(fig, width = "stretch") # CPU dans le temps fig = px.line(df_logs_opti, x="timestamp_model", y="cpu_percent_ops", title = "CPU utilisé par le batch") o2.plotly_chart(fig, width = "stretch") # RAM dans le temps fig = px.line(df_logs_opti, x="timestamp_model", y="ram_percent_ops", title = "RAM dutilisé par le batch") o3.plotly_chart(fig, width = "stretch") fig = px.scatter(df_logs_opti, x = "batch_size_model", y = "latency_ms_ops", trendline = "ols", title = "Relation entre batch_size et latence") st.plotly_chart(fig, use_container_width=True) # ONGLET 6 - Compaaison des kpis avant et après optimisation with tab5: if df_logs.empty or df_logs_opti.empty: st.warning("Exécute au moins une requête unitaire et un batch pour afficher la comparaison.") else: # Calculs df_unit = df_logs.copy() df_batch = df_logs_opti.copy() # Latence par client df_unit["latence_par_client"] = df_unit["latency_ms"] df_batch["latence_par_client"] = df_batch["latency_ms_ops"] / df_batch["batch_size_model"] # Inférence par client df_unit["inference_par_client"] = df_unit["inference_ms_x"] df_batch["inference_par_client"] = df_batch["inference_ms_model"] / df_batch["batch_size_model"] # CPU / RAM / Load / Threads df_unit["cpu"] = df_unit["cpu_percent"] df_batch["cpu"] = df_batch["cpu_percent_ops"] df_unit["ram"] = df_unit["ram_percent"] df_batch["ram"] = df_batch["ram_percent_ops"] df_unit["load"] = df_unit["system_load"] df_batch["load"] = df_batch["system_load_ops"] df_unit["threads"] = df_unit["num_threads"] df_batch["threads"] = df_batch["num_threads_ops"] # KPI st.subheader("Performances par client") p1, p2, p3 = st.columns([1,1,1]) # st.markdown("""

Latence par client

""", unsafe_allow_html=True) p1.metric("Latence par client (unitaire)", f"{df_unit['latence_par_client'].mean():.2f} ms") p2.metric("Latence par client (batch)", f"{df_batch['latence_par_client'].mean():.2f} ms") p3.metric("Gain", f"{df_unit['latence_par_client'].mean() / df_batch['latence_par_client'].mean():.1f}") # st.markdown("
", unsafe_allow_html=True) i1, i2, i3 = st.columns([1,1,1]) # st.markdown("""

Inférence par client

""", unsafe_allow_html=True) i1.metric("Inférence par client (unitaire)", f"{df_unit['inference_par_client'].mean():.2f} ms") i2.metric("Inférence par client (batch)", f"{df_batch['inference_par_client'].mean():.2f} ms") i3.metric("Gain", f"{df_unit['inference_par_client'].mean() / df_batch['inference_par_client'].mean():.1f}") st.markdown("
", unsafe_allow_html=True) # kpi CPU/RAM/LOAD/THREADS st.subheader("Charge système") s1, s2, s3, s4 = st.columns([1,1,1,1]) s1.metric("CPU (%) unitaire", f"{df_unit['cpu'].mean():.1f}%") s2.metric("CPU (%) batch", f"{df_batch['cpu'].mean():.1f}%") s3.metric("RAM (%) unitaire", f"{df_unit['ram'].mean():.1f}%") s4.metric("RAM (%) batch", f"{df_batch['ram'].mean():.1f}%") s5, s6, s7, s8 = st.columns(4) s5.metric("Load unitaire", f"{df_unit['load'].mean():.2f}") s6.metric("Load batch", f"{df_batch['load'].mean():.2f}") s7.metric("Threads unitaire", f"{df_unit['threads'].mean():.0f}") s8.metric("Threads batch", f"{df_batch['threads'].mean():.0f}") st.markdown("
", unsafe_allow_html=True) # Graphiques comparatifs st.subheader("Visualisations comparatives") c1, c2 = st.columns(2) fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode = "Batch")]), x = "mode", y = "latence_par_client", title = "Latence par client : Unitaire vs Batch") c1.plotly_chart(fig, use_container_width=True) fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode = "Batch")]), x = "mode", y = "inference_par_client", title = "Inférence par client : Unitaire vs Batch") c2.plotly_chart(fig, use_container_width=True) st.markdown("
", unsafe_allow_html=True) # CPU fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode = "Batch")]), x = "mode", y = "cpu", title = "CPU (%) : Unitaire vs Batch") st.plotly_chart(fig, use_container_width=True) st.markdown("
", unsafe_allow_html=True) # RAM fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode="Batch")]), x = "mode", y = "ram", title = "RAM (%) : Unitaire vs Batch") st.plotly_chart(fig, use_container_width=True) st.markdown("
", unsafe_allow_html=True) # Load fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode = "Batch")]), x = "mode", y = "load", title = "Charge système : Unitaire vs Batch") st.plotly_chart(fig, use_container_width=True) st.markdown("
", unsafe_allow_html=True) # Threads fig = px.box(pd.concat([df_unit.assign(mode = "Unitaire"), df_batch.assign(mode = "Batch")]), x = "mode", y = "threads", title = "Threads : Unitaire vs Batch") st.plotly_chart(fig, use_container_width=True) st.markdown("
", unsafe_allow_html=True)