{ "cells": [ { "cell_type": "markdown", "id": "aeb4cd48", "metadata": {}, "source": [ "# **Analyse du drift & de la performance**\n", "\n", "Ce notebook a pour objectif :\n", "\n", "- **d’évaluer le data drift** entre :\n", " - les données de référence (jeu d’entraînement ou échantillon représentatif)\n", " - les données de production issues des logs de l’API\n", "\n", " Le data drift permet de détecter si les distributions des variables évoluent dans le temps. \n", " Ces changements peuvent dégrader les performances du modèle, notamment lorsque les distributions de production s’éloignent de celles observées lors de l’entraînement.\n", "\n", " Pour cela, nous utilisons la librairie **Evidently AI**, qui permet d’analyser le drift **colonne par colonne**, après avoir soigneusement harmonisé les colonnes entre les deux jeux de données (types, valeurs manquantes, colonnes constantes, etc.).\n", "\n", "- **d’évaluer la performance du pipeline de prédiction**, non pas uniquement au niveau du modèle, mais surtout au niveau du **preprocessing**, afin d’identifier les véritables axes d’optimisation.\n", "\n", " En effet, dans un contexte MLOps, la latence d’un modèle en production dépend autant du modèle que du pipeline de transformation. \n", " Nous mesurons donc :\n", " - le temps de preprocessing,\n", " - le temps de prédiction du modèle,\n", " - la différence entre appels unitaires et prédiction en batch,\n", " - et nous analysons les résultats du profiling pour identifier les goulots d’étranglement.\n", "\n" ] }, { "cell_type": "code", "execution_count": 23, "id": "ee429e61", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import cProfile\n", "import pstats\n", "import joblib\n", "import time\n", "\n", "from evidently.report import Report\n", "from evidently.metrics import ColumnDriftMetric" ] }, { "cell_type": "markdown", "id": "1149740f", "metadata": {}, "source": [ "## **Analyse du Data Drift avec Evidently**" ] }, { "cell_type": "code", "execution_count": 24, "id": "9a27a53d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "((500, 327), (213, 406), (213, 406))" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Chargement du pipeline\n", "pipe = joblib.load(\"../BestModel/pipeline_complet.joblib\")\n", "\n", "# Données de référence (version simplifiée)\n", "df_reference_raw = joblib.load(\"../data/app_test_small.joblib\")\n", "\n", "# Données de production (logs unitaires)\n", "df_production_raw = pd.read_parquet(\"../logs/predictions_log.parquet\")\n", "\n", "# Données de production (logs batch)\n", "df_batch_raw = pd.read_parquet(\"../logs/predictions_log.parquet\")\n", "\n", "df_reference_raw.shape, df_production_raw.shape, df_batch_raw.shape" ] }, { "cell_type": "markdown", "id": "276796a6", "metadata": {}, "source": [ "### Sélection des colonnes communes entre référence production" ] }, { "cell_type": "code", "execution_count": 25, "id": "a66434ea", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Index(['num__SK_ID_CURR', 'num__BUREAU_SK_ID_BUREAU_max',\n", " 'num__POS_SK_DPD_min', 'num__CC_SK_DPD_DEF_max',\n", " 'num__PREV_NAME_TYPE_SUITE_Family', 'num__PREV_DAYS_FIRST_DRAWING_sum',\n", " 'num__PREV_NAME_GOODS_CATEGORY_Other', 'num__DAYS_LAST_PHONE_CHANGE',\n", " 'num__CC_CNT_DRAWINGS_OTHER_CURRENT_mean',\n", " 'num__PREV_DAYS_FIRST_DRAWING_max',\n", " ...\n", " 'bool__FLAG_DOCUMENT_12', 'bool__FLAG_DOCUMENT_13',\n", " 'bool__FLAG_DOCUMENT_14', 'bool__FLAG_DOCUMENT_15',\n", " 'bool__FLAG_DOCUMENT_16', 'bool__FLAG_DOCUMENT_17',\n", " 'bool__FLAG_DOCUMENT_18', 'bool__FLAG_DOCUMENT_19',\n", " 'bool__FLAG_DOCUMENT_20', 'bool__FLAG_DOCUMENT_21'],\n", " dtype='object', length=382)" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Transformer les colonnes de référence\n", "df_reference_transformed = pipe[:-1].transform(df_reference_raw)\n", "df_reference_transformed = pd.DataFrame(df_reference_transformed, columns=pipe[:-1].get_feature_names_out())\n", "df_reference_transformed.columns\n" ] }, { "cell_type": "code", "execution_count": 26, "id": "e02ce960", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Index(['num__SK_ID_CURR', 'num__BUREAU_SK_ID_BUREAU_max',\n", " 'num__POS_SK_DPD_min', 'num__CC_SK_DPD_DEF_max',\n", " 'num__PREV_NAME_TYPE_SUITE_Family', 'num__PREV_DAYS_FIRST_DRAWING_sum',\n", " 'num__PREV_NAME_GOODS_CATEGORY_Other', 'num__DAYS_LAST_PHONE_CHANGE',\n", " 'num__CC_CNT_DRAWINGS_OTHER_CURRENT_mean',\n", " 'num__PREV_DAYS_FIRST_DRAWING_max',\n", " ...\n", " 'ram_percent', 'system_load', 'num_threads', 'method', 'status',\n", " 'error_message', 'latency_ms', 'request_size_bytes',\n", " 'response_size_bytes', 'batch_size_y'],\n", " dtype='object', length=406)" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_production_raw.columns" ] }, { "cell_type": "code", "execution_count": 27, "id": "d9282066", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "382" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "common_cols = df_reference_transformed.columns.intersection(df_production_raw.columns)\n", "len(common_cols)" ] }, { "cell_type": "code", "execution_count": 28, "id": "eb15c29d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "((500, 382), (213, 382))" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_reference = df_reference_transformed[common_cols].copy()\n", "df_production = df_production_raw[common_cols].copy()\n", "\n", "df_reference.shape, df_production.shape" ] }, { "cell_type": "markdown", "id": "3b53669d", "metadata": {}, "source": [ "### Harmonisation des colonnes" ] }, { "cell_type": "code", "execution_count": 29, "id": "a0aee959", "metadata": {}, "outputs": [], "source": [ "for col in common_cols:\n", " # bool → int\n", " if df_reference[col].dtype == bool:\n", " df_reference[col] = df_reference[col].astype(int)\n", "\n", " if df_production[col].dtype == bool:\n", " df_production[col] = df_production[col].astype(int)\n", "\n", " ref_num = pd.api.types.is_numeric_dtype(df_reference[col])\n", " prod_num = pd.api.types.is_numeric_dtype(df_production[col])\n", "\n", " # Les deux numériques\n", " if ref_num and prod_num:\n", " df_reference[col] = pd.to_numeric(df_reference[col], errors=\"coerce\").astype(\"float64\")\n", " df_production[col] = pd.to_numeric(df_production[col], errors=\"coerce\").astype(\"float64\")\n", "\n", " # Sinon → string\n", " else:\n", " df_reference[col] = df_reference[col].fillna(\"MISSING\").astype(str)\n", " df_production[col] = df_production[col].fillna(\"MISSING\").astype(str)\n" ] }, { "cell_type": "markdown", "id": "ec1bec15", "metadata": {}, "source": [ "### Suppression des colonnes vides et constantes" ] }, { "cell_type": "code", "execution_count": 30, "id": "078e82b0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "275\n", "213\n", "500\n" ] } ], "source": [ "valid_cols = [\n", " col for col in common_cols\n", " if df_reference[col].notna().sum() > 0 and df_production[col].notna().sum() > 0]\n", "\n", "non_constant_cols = [\n", " col for col in valid_cols\n", " if df_reference[col].nunique(dropna=True) > 1\n", " and df_production[col].nunique(dropna=True) > 1]\n", "\n", "df_reference = df_reference[non_constant_cols]\n", "df_production = df_production[non_constant_cols]\n", "\n", "print(len(non_constant_cols))\n", "print(len(df_production))\n", "print(len(df_reference))\n" ] }, { "cell_type": "markdown", "id": "a9f6fc6c", "metadata": {}, "source": [ "### Drift par colonne" ] }, { "cell_type": "code", "execution_count": 31, "id": "ab0ca561", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "(275, 2)\n" ] } ], "source": [ "drift_results = []\n", "\n", "for col in non_constant_cols:\n", " report = Report(metrics = [ColumnDriftMetric(column_name = col)])\n", "\n", " try:\n", " report.run(reference_data = df_reference[[col]], current_data = df_production[[col]])\n", "\n", " result = report.as_dict()\n", " drift_detected = result[\"metrics\"][0][\"result\"][\"drift_detected\"]\n", "\n", " drift_results.append({\n", " \"colonne\": col,\n", " \"drift_detected\": drift_detected})\n", "\n", " except Exception as e:\n", " drift_results.append({\"colonne\": col, \"drift_detected\": \"ERREUR\", \"exception\": str(e)})\n", "\n", "df_drift = pd.DataFrame(drift_results)\n", "print(df_drift.shape)\n", "report_drift = report\n" ] }, { "cell_type": "markdown", "id": "8ce86333", "metadata": {}, "source": [ "### Résumé" ] }, { "cell_type": "code", "execution_count": 32, "id": "d80c7637", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n", "c:\\Users\\Lenovo\\anaconda3\\envs\\mlops\\Lib\\site-packages\\scipy\\stats\\_stats_py.py:8022: RuntimeWarning: divide by zero encountered in divide\n", " terms = (f_obs_float - f_exp)**2 / f_exp\n" ] } ], "source": [ "from evidently.metrics import DatasetDriftMetric\n", "report = Report(metrics=[DatasetDriftMetric()])\n", "report.run(reference_data=df_reference, current_data=df_production)\n", "\n", "res = report.as_dict()" ] }, { "cell_type": "code", "execution_count": 33, "id": "9da53766", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Colonnes analysées : 275\n", "Colonnes en dérive : 18\n", "Taux de dérive : 6.5%\n" ] } ], "source": [ "dataset_drift = next(\n", " m for m in res[\"metrics\"]\n", " if m[\"metric\"] == \"DatasetDriftMetric\")\n", "\n", "drift_res = dataset_drift[\"result\"]\n", "\n", "nb_cols = drift_res[\"number_of_columns\"]\n", "nb_drift = drift_res[\"number_of_drifted_columns\"]\n", "taux = nb_drift / nb_cols\n", "\n", "print(\"Colonnes analysées :\", nb_cols)\n", "print(\"Colonnes en dérive :\", nb_drift)\n", "print(f\"Taux de dérive : {taux:.1%}\")\n" ] }, { "cell_type": "code", "execution_count": 34, "id": "28838cdc", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| \n", " | colonne | \n", "drift_detected | \n", "
|---|---|---|
| 6 | \n", "num__PREV_PRODUCT_COMBINATION_POS others witho... | \n", "True | \n", "
| 57 | \n", "num__BUREAU_STATUS_4 | \n", "True | \n", "
| 60 | \n", "num__PREV_NAME_CASH_LOAN_PURPOSE_Buying a holi... | \n", "True | \n", "
| 66 | \n", "num__PREV_NAME_GOODS_CATEGORY_Vehicles | \n", "True | \n", "
| 84 | \n", "num__PREV_NAME_CASH_LOAN_PURPOSE_Medicine | \n", "True | \n", "
| 86 | \n", "num__PREV_NAME_CASH_LOAN_PURPOSE_Wedding / gif... | \n", "True | \n", "
| 89 | \n", "num__DAYS_REGISTRATION | \n", "True | \n", "
| 94 | \n", "num__PREV_NAME_GOODS_CATEGORY_Jewelry | \n", "True | \n", "
| 123 | \n", "num__PREV_CODE_REJECT_REASON_NUNIQUE | \n", "True | \n", "
| 133 | \n", "num__PREV_CODE_REJECT_REASON_SCOFR | \n", "True | \n", "
| 145 | \n", "num__PREV_NAME_CASH_LOAN_PURPOSE_Other | \n", "True | \n", "
| 146 | \n", "num__PREV_NAME_GOODS_CATEGORY_Homewares | \n", "True | \n", "
| 148 | \n", "num__PREV_NAME_SELLER_INDUSTRY_Industry | \n", "True | \n", "
| 150 | \n", "num__PREV_PRODUCT_COMBINATION_POS industry wit... | \n", "True | \n", "
| 194 | \n", "num__PREV_NAME_TYPE_SUITE_Other_B | \n", "True | \n", "
| 221 | \n", "card_faible__NAME_EDUCATION_TYPE_Incomplete hi... | \n", "True | \n", "
| 253 | \n", "card_faible__WALLSMATERIAL_MODE_Wooden | \n", "True | \n", "
| 259 | \n", "bool__FLAG_WORK_PHONE | \n", "True | \n", "
| \n", " | num__SK_ID_CURR | \n", "num__BUREAU_SK_ID_BUREAU_max | \n", "num__POS_SK_DPD_min | \n", "num__CC_SK_DPD_DEF_max | \n", "num__PREV_NAME_TYPE_SUITE_Family | \n", "num__PREV_DAYS_FIRST_DRAWING_sum | \n", "num__PREV_NAME_GOODS_CATEGORY_Other | \n", "num__DAYS_LAST_PHONE_CHANGE | \n", "num__CC_CNT_DRAWINGS_OTHER_CURRENT_mean | \n", "num__PREV_DAYS_FIRST_DRAWING_max | \n", "... | \n", "ram_percent | \n", "system_load | \n", "num_threads | \n", "method | \n", "status | \n", "error_message | \n", "latency_ms | \n", "request_size_bytes | \n", "response_size_bytes | \n", "batch_size_y | \n", "
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | \n", "-0.302429 | \n", "0.326039 | \n", "0.249338 | \n", "NaN | \n", "-0.535936 | \n", "-0.300165 | \n", "0.23674 | \n", "-0.987792 | \n", "NaN | \n", "0.246419 | \n", "... | \n", "78.3 | \n", "0.0 | \n", "21 | \n", "POST | \n", "success | \n", "None | \n", "383.741379 | \n", "12798.0 | \n", "0.0 | \n", "NaN | \n", "
| 1 | \n", "-0.147132 | \n", "0.553947 | \n", "0.249338 | \n", "NaN | \n", "-0.535936 | \n", "0.742804 | \n", "0.23674 | \n", "0.895050 | \n", "NaN | \n", "0.246419 | \n", "... | \n", "78.3 | \n", "0.0 | \n", "21 | \n", "POST | \n", "success | \n", "None | \n", "102.715015 | \n", "12775.0 | \n", "0.0 | \n", "NaN | \n", "
| 2 | \n", "-0.429638 | \n", "0.448635 | \n", "0.249338 | \n", "NaN | \n", "0.494736 | \n", "-0.821649 | \n", "0.23674 | \n", "0.962033 | \n", "NaN | \n", "0.246419 | \n", "... | \n", "78.4 | \n", "0.0 | \n", "21 | \n", "POST | \n", "success | \n", "None | \n", "67.815304 | \n", "12875.0 | \n", "0.0 | \n", "NaN | \n", "
| 3 | \n", "0.246223 | \n", "0.491943 | \n", "0.249338 | \n", "0.691535 | \n", "2.556081 | \n", "4.392488 | \n", "0.23674 | \n", "-2.192274 | \n", "1.997573 | \n", "0.246419 | \n", "... | \n", "78.4 | \n", "0.0 | \n", "21 | \n", "POST | \n", "success | \n", "None | \n", "89.750528 | \n", "12927.0 | \n", "0.0 | \n", "NaN | \n", "
| 4 | \n", "-1.169600 | \n", "0.699197 | \n", "0.249338 | \n", "NaN | \n", "-0.535936 | \n", "-0.821649 | \n", "0.23674 | \n", "0.744033 | \n", "NaN | \n", "0.246419 | \n", "... | \n", "78.3 | \n", "0.0 | \n", "21 | \n", "POST | \n", "success | \n", "None | \n", "68.776608 | \n", "12836.0 | \n", "0.0 | \n", "NaN | \n", "
5 rows × 406 columns
\n", "