Spaces:
Sleeping
Sleeping
| 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("""<p style='font-size: 22px; font-style: italic;'>Performance Globale Colonnes</p>""", unsafe_allow_html=True) | |
| col1, col2, col3, col4 = st.columns([1,1,1,1]) | |
| st.markdown("<br>", 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("""<p style='font-size: 22px; font-style: italic;'>Performance API</p>""", unsafe_allow_html=True) | |
| c1, c2, c3, c4, c5, c6 = st.columns([1,1,1,1,1,1]) | |
| st.markdown("<br>", 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("""<p style='font-size: 22px; font-style: italic;'>Performance Métier</p>""", unsafe_allow_html=True) | |
| m1, m2, m3 = st.columns([1,1,1]) | |
| st.markdown("<br>", 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("""<p style='font-size: 22px; font-style: italic;'>Performance API (Batch)</p>""", 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("<br>", 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("""<p style='font-size: 22px; font-style: italic;'>Latence par client</p>""", 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("<br>", unsafe_allow_html=True) | |
| i1, i2, i3 = st.columns([1,1,1]) | |
| # st.markdown("""<p style='font-size: 22px; font-style: italic;'>Inférence par client</p>""", 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("<br>", 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("<hr>", 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("<br>", 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("<br>", 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("<br>", 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("<br>", 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("<br>", unsafe_allow_html=True) |