{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Clasificador de dominio (desviación covariable)\n", "===============================================\n", "Una alternativa para detectar una desviación covariable es entrenando un clasificar binario el cual utilice todos los predictores de los conjuntos de datos origen y objetivo. Este clasificador intetará predecir a que conjunto de datos pertenece cada observación, es decir que nuestra variable objetivo será `source` y `target`. Si los dos conjuntos de datos son lo suficientemente diferentes, esto implicar[a que el clasificador podrá facilmente discriminar entre los dos conjuntos de datos y por lo tanto mostrará una buena performance. Utilizaremos esto como un síntoma de que el conjunto de datos ha cambiado.\n", "\n", "Para poder utilizar este método, necesitaremos realizar un test de hipótesis binomial el cual compruebe que tan probable es obtener tal performance en nuestro clasificador bajo la suposición de que no hay una desviación. Si el `p-value` (la probabilidad de que el modelo tenga al menos tal performance de clasificación bajo la suposición nula) es bajo, entonces esto significa que una desviación podría haber ocurrido (rechazamos la hipótesis nula en favor de la alternativa). En la práctica dispararemos una alerta cuando el `p-value` del test de hipótesis binomial es menor que el nivel de significancia que estamos buscando." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ejemplo\n", "-------\n", "En este ejemplo, usaremos los datos de IRIS dataset para generar lotes con distribuciones distintas de los datos, lo que provocará una desviación en los datos que utilizaremos productivos. El conjunto de datos de IRIS es parte de la biblioteca sklearn que constan de 3 tipos diferentes de longitud de pétalo y sépalo (Setosa, Versicolour y Virginica), descriptos por la longitud del sépalo, el ancho del sépalo, la longitud del pétalo y el ancho del pétalo:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "from sklearn import datasets\n", "\n", "iris = datasets.load_iris()\n", "X = iris.data[:,:4]\n", "y = iris.target" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Para ilustrar el problema, de forma tendenciosa, filtraremos nuestro conjunto de datos para que algunas muestras especificas no sean incluidas en el conjunto de entrenamiento. En este caso, seleccionaremos todas las muestras donde el predictor `ancho del sépalo` es mayor a 2.8. Veamos primero cual es la distribución de los valores y rangos de los predictores:" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
longitud del sepaloancho del sepalolongitud del petaloancho del pétalo
count150.000000150.000000150.000000150.000000
mean5.8433333.0573333.7580001.199333
std0.8280660.4358661.7652980.762238
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000
\n", "
" ], "text/plain": [ " longitud del sepalo ancho del sepalo longitud del petalo \\\n", "count 150.000000 150.000000 150.000000 \n", "mean 5.843333 3.057333 3.758000 \n", "std 0.828066 0.435866 1.765298 \n", "min 4.300000 2.000000 1.000000 \n", "25% 5.100000 2.800000 1.600000 \n", "50% 5.800000 3.000000 4.350000 \n", "75% 6.400000 3.300000 5.100000 \n", "max 7.900000 4.400000 6.900000 \n", "\n", " ancho del pétalo \n", "count 150.000000 \n", "mean 1.199333 \n", "std 0.762238 \n", "min 0.100000 \n", "25% 0.300000 \n", "50% 1.300000 \n", "75% 1.800000 \n", "max 2.500000 " ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.DataFrame(X, columns=['longitud del sepalo',\n", " 'ancho del sepalo',\n", " 'longitud del petalo',\n", " 'ancho del pétalo'])\n", "df.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Generemos los conjuntos de datos de entrenamiento filtrando entonces los valores que mencionamos anteriormente. Esto hará que nuestro proceso de entrenamiento nunca haya visto instancias de datos donde `ancho del sepalo` es menor que 2.8, y por lo tanto no sabrá como clasificar estas instancias.\n", "> Nota: Esta forma de separar los datos en conjuntos de entrenamiento es ficticia y se utiliza simplemente para demostrar el concepto." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "X_train, X_test = [data for _, data in df.groupby(df[\"ancho del sepalo\"] <= 2.8)]\n", "y_train = y[X_train.index]\n", "y_test = y[X_test.index]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Entrenaremos un modelo para resolver el problema utilizando el conjunto de datos que seleccionamos." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from sklearn import svm\n", "from sklearn.model_selection import train_test_split\n", "\n", "model = svm.SVC(C=1.0, kernel='linear', gamma=0.5, probability=True)\n", "model = model.fit(X_train, y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Simulando un cambio en el proceso de recolección de datos\n", "---------------------------------------------------------\n", "Supongamos que cuando desplegamos el modelo en producción, efectivamente vemos la totalidad del conjunto de datos, es decir, con instancias que tienen valores que el modelo nunca vio. En este caso, es natural pensar que la performance del modelo se degradará, pero recordemos que en producción no tenemos acceso a los valores reales de la variable objetivo. Por lo tanto, deberemos decidir si nuestro modelo debería o no reentrenarse basandonos solo en los predictores.\n", "\n", "Para realizar esto, generaremos un nuevo conjunto de datos que es la unión de los conjuntos de datos que se utilizaron para entrenamiento y los conjuntos de datos que vemos en producción. Adicionalmente agregaremos una columna para indicar a que set de datos pertenece." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "all_observations = np.concatenate([X, X_train], axis=0)\n", "all_labels = np.concatenate([np.zeros(shape=(X.shape[0],)), \n", " np.ones(shape=(X_train.shape[0],))], axis=0)" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "import lightgbm as lgb\n", "\n", "params = {\n", " 'objective': 'binary',\n", " 'max_depth': 100,\n", " 'num_leaves': 900,\n", " 'feature_fraction': 0.2,\n", " 'bagging_fraction': 0.8,\n", " 'boosting': 'gbdt',\n", " 'verbose': -1\n", " }\n", "\n", "lgb_data = lgb.Dataset(all_observations, label=all_labels)\n", "gbm = lgb.train(params, lgb_data, num_boost_round=200)" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import matthews_corrcoef, confusion_matrix\n", "\n", "y_pred = gbm.predict(all_observations).round(0)\n", "errors = np.count_nonzero(all_labels - y_pred)\n", "successes = len(all_labels) - errors" ] }, { "cell_type": "code", "execution_count": 106, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.5270730525677394" ] }, "execution_count": 106, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from scipy import stats\n", "\n", "stats.binom_test(successes, n=len(all_labels), p=len(X_train)/len(all_labels), alternative='greater')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "La hipotesis nula de que ambos conjuntos de datos son igual es rechazada en favor de la alternativa, con un nivel de significancia de 5% ya que el p-value retornado es mas pequeño que el valor critico de 5%.\n", "\n", "> Nota: En general, este tipo de pruebas debe realizarse sobre multiples muestreos de los conjuntos de datos para obtener un test más robusto. Estos pasos se han omitido en este ejemplo por simplicidad, pero recomendamos la lectura de: [arXiv:1810.11953](https://arxiv.org/abs/1810.11953)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Implicancias\n", "------------\n", "Por diseño, esta técnica solo puede ser utilizada para detectar desviaciones en los predictores y por lo tanto no es una técnica apropiada para detectar desviaciones en la variable objetivo.\n", "\n", "Adicionalmente, esta técnica requiere ejecutar este procedimiento cada vez que un nuevo lote de datos está disponible. Sin embargo, tiene una ventaja muy importante y es que permite analizar cuales son las observaciones que más difieren del set de datos de origen. Entonces, podemos obtener mayor información de qué es lo que está sucediendo con los datos o que suposiciones incorrectas hemos realizado. Es especialmente útil cuando solo algunos de los predictores ha cambiado.\n" ] } ], "metadata": { "interpreter": { "hash": "bea38c2984299ac640e8421861d34b2e05ee614f6236d2975c05eeb77366835f" }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.11" } }, "nbformat": 4, "nbformat_minor": 2 }