{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Predecir aprobaciones de tarjetas de crédito\n", "\n", "## Solicitudes de tarjetas de crédito\n", "\n", "Los bancos comerciales reciben infinidad de solicitudes de tarjetas de crédito. Muchos de ellos son rechazados por diversas razones, como saldos elevados de préstamos, bajos niveles de ingresos o demasiadas consultas sobre el informe crediticio de una persona, por ejemplo. El análisis manual de estas aplicaciones es tedioso, propenso a errores y requiere mucho tiempo. Afortunadamente, esta tarea se puede automatizar con el poder del machine learning o aprendizaje automático y casi todos los bancos comerciales lo hacen hoy en día. En este post, crearemos un predictor automático de aprobación de tarjetas de crédito utilizando técnicas de aprendizaje automático, tal como lo hacen los bancos reales.\n", "\n", "\"Credit\n", "\n", "Usaremos el conjunto de datos [Credit Card Approval](http://archive.ics.uci.edu/ml/datasets/credit+approval) del Repositorio de Machine Learning de UCI. La estructura de este trabajo será la siguiente:\n", "\n", "- Primero, comenzaremos cargando y viendo el conjunto de datos.\n", "- Veremos que el dataset tiene una mezcla de *features* (características o predictores) numéricas y no numéricas, valores en diferentes rangos, y además una cantidad considerable de datos faltantes.\n", "- Tendremos que preprocesar el dataset para asegurarnos de que el modelo de machine learning que elijamos pueda hacer buenas predicciones.\n", "- Una vez que nuestros datos estén en buena forma, haremos un análisis de datos exploratorio para formar nuestras intuiciones.\n", "- Finalmente, crearemos un modelo de machine learning que pueda predecir si se aceptará o no la solicitud de una persona para una tarjeta de crédito.\n", "\n", "## Carguemos y observemos los datos\n", "\n", "Dado que estos datos son confidenciales, el contribuyente de este dataset ha anonimizado los nombres de las funciones. \n" ] }, { "cell_type": "code", "execution_count": 1, "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", " \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", "
0123456789101112131415
0b30.830.000ugwv1.25tt1fg002020+
1a58.674.460ugqh3.04tt6fg00043560+
2a24.500.500ugqh1.50tf0fg00280824+
3b27.831.540ugwv3.75tt5tg001003+
4b20.175.625ugwv1.71tf0fs001200+
\n", "
" ], "text/plain": [ " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", "0 b 30.83 0.000 u g w v 1.25 t t 1 f g 00202 0 +\n", "1 a 58.67 4.460 u g q h 3.04 t t 6 f g 00043 560 +\n", "2 a 24.50 0.500 u g q h 1.50 t f 0 f g 00280 824 +\n", "3 b 27.83 1.540 u g w v 3.75 t t 5 t g 00100 3 +\n", "4 b 20.17 5.625 u g w v 1.71 t f 0 f s 00120 0 +" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "# Cargamos dataset\n", "cc_apps = pd.read_csv(\"datasets/cc_approvals.data\", header=None)\n", "\n", "# Un vistazo a los datos\n", "cc_apps.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "El resultado puede parecer un poco confuso a primera vista, pero intentemos descubrir las características más importantes de una aplicación de tarjeta de crédito. \n", "\n", "Como dijimos, los predictores de este conjunto de datos se han anonimizado para proteger la privacidad, pero [este blog](http://rstudio-pubs-static.s3.amazonaws.com/73039_9946de135c0a49daa7a0a9eda4a67a72.html) nos brinda una descripción general bastante buena de cuales pueden ser los probables predictores. Las características probables en una solicitud de tarjeta de crédito típica podrían ser `Género`, `Edad`, `Deuda`, `Estado Civil`, `Cliente bancario`, `Nivel de educación`, `Etnia`, `Años de empleo`, `Incumplimiento previo`, `Empleado`, `Puntuación de crédito`, `Licencia de conductor`, `Ciudadano`, `Código postal`, `Ingresos` y finalmente el `Estado de aprobación`. Esto nos da un buen punto de partida y podemos mapear estas características con respecto a las columnas en nuestro dataset.\n", "\n", "Como podemos ver, el dataset tiene una combinación de predictores numéricos y no numéricos. Esto se puede solucionar con un poco de preprocesamiento, pero antes de hacerlo, investiguemos un poco más para ver si hay otros problemas del conjunto de datos que deban solucionarse. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 2 7 10 14\n", "count 690.000000 690.000000 690.00000 690.000000\n", "mean 4.758725 2.223406 2.40000 1017.385507\n", "std 4.978163 3.346513 4.86294 5210.102598\n", "min 0.000000 0.000000 0.00000 0.000000\n", "25% 1.000000 0.165000 0.00000 0.000000\n", "50% 2.750000 1.000000 0.00000 5.000000\n", "75% 7.207500 2.625000 3.00000 395.500000\n", "max 28.000000 28.500000 67.00000 100000.000000\n", "\n", "\n", "\n", "RangeIndex: 690 entries, 0 to 689\n", "Data columns (total 16 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 0 690 non-null object \n", " 1 1 690 non-null object \n", " 2 2 690 non-null float64\n", " 3 3 690 non-null object \n", " 4 4 690 non-null object \n", " 5 5 690 non-null object \n", " 6 6 690 non-null object \n", " 7 7 690 non-null float64\n", " 8 8 690 non-null object \n", " 9 9 690 non-null object \n", " 10 10 690 non-null int64 \n", " 11 11 690 non-null object \n", " 12 12 690 non-null object \n", " 13 13 690 non-null object \n", " 14 14 690 non-null int64 \n", " 15 15 690 non-null object \n", "dtypes: float64(2), int64(2), object(12)\n", "memory usage: 86.4+ KB\n", "None\n", "\n", "\n", " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", "673 ? 29.50 2.000 y p e h 2.000 f f 0 f g 00256 17 -\n", "674 a 37.33 2.500 u g i h 0.210 f f 0 f g 00260 246 -\n", "675 a 41.58 1.040 u g aa v 0.665 f f 0 f g 00240 237 -\n", "676 a 30.58 10.665 u g q h 0.085 f t 12 t g 00129 3 -\n", "677 b 19.42 7.250 u g m v 0.040 f t 1 f g 00100 1 -\n", "678 a 17.92 10.210 u g ff ff 0.000 f f 0 f g 00000 50 -\n", "679 a 20.08 1.250 u g c v 0.000 f f 0 f g 00000 0 -\n", "680 b 19.50 0.290 u g k v 0.290 f f 0 f g 00280 364 -\n", "681 b 27.83 1.000 y p d h 3.000 f f 0 f g 00176 537 -\n", "682 b 17.08 3.290 u g i v 0.335 f f 0 t g 00140 2 -\n", "683 b 36.42 0.750 y p d v 0.585 f f 0 f g 00240 3 -\n", "684 b 40.58 3.290 u g m v 3.500 f f 0 t s 00400 0 -\n", "685 b 21.08 10.085 y p e h 1.250 f f 0 f g 00260 0 -\n", "686 a 22.67 0.750 u g c v 2.000 f t 2 t g 00200 394 -\n", "687 a 25.25 13.500 y p ff ff 2.000 f t 1 t g 00200 1 -\n", "688 b 17.92 0.205 u g aa v 0.040 f f 0 f g 00280 750 -\n", "689 b 35.00 3.375 u g c h 8.290 f f 0 t g 00000 0 -\n" ] } ], "source": [ "# Resumen estadístico\n", "cc_apps_description = cc_apps.describe()\n", "print(cc_apps_description)\n", "\n", "print(\"\\n\")\n", "\n", "# Características del dataset\n", "cc_apps_info = cc_apps.info()\n", "print(cc_apps_info)\n", "\n", "print(\"\\n\")\n", "\n", "# Un vistazo a los últimos registros\n", "print(cc_apps.tail(17))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Manejo de los valores faltantes\n", "\n", "Mediante las observaciones anteriores, hemos descubierto algunos problemas en el dataset que afectarán el rendimiento de nuestros modelos de machine learning si no se modifican:\n", "\n", "- Contiene datos numéricos, puntualmente las *features* 2, 7, 10 y 14, de tipo `float64` o `int64`, y categóricos o no numéricos, de tipo `object` para las características restantes. \n", "- Posee valores con rangos disímiles. Algunas características tienen un rango de valores que va de 0 a 28, mientras otras tienen máximos que alcanzan los 100000. \n", "- Presenta valores faltantes. Los mismos están etiquetados con el caracter '?', que se puede ver, por ejemplo, en el valor de la feature 0 de la fila 673 en la muestra anterior.\n", "\n", "Vamos a ocuparnos ahora de estos valores faltantes. Comencemos reemplazando temporalmente estos signos de interrogación con valores nulos `NaN`. " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", "673 NaN 29.50 2.000 y p e h 2.000 f f 0 f g 00256 17 -\n", "674 a 37.33 2.500 u g i h 0.210 f f 0 f g 00260 246 -\n", "675 a 41.58 1.040 u g aa v 0.665 f f 0 f g 00240 237 -\n", "676 a 30.58 10.665 u g q h 0.085 f t 12 t g 00129 3 -\n", "677 b 19.42 7.250 u g m v 0.040 f t 1 f g 00100 1 -\n", "678 a 17.92 10.210 u g ff ff 0.000 f f 0 f g 00000 50 -\n", "679 a 20.08 1.250 u g c v 0.000 f f 0 f g 00000 0 -\n", "680 b 19.50 0.290 u g k v 0.290 f f 0 f g 00280 364 -\n", "681 b 27.83 1.000 y p d h 3.000 f f 0 f g 00176 537 -\n", "682 b 17.08 3.290 u g i v 0.335 f f 0 t g 00140 2 -\n", "683 b 36.42 0.750 y p d v 0.585 f f 0 f g 00240 3 -\n", "684 b 40.58 3.290 u g m v 3.500 f f 0 t s 00400 0 -\n", "685 b 21.08 10.085 y p e h 1.250 f f 0 f g 00260 0 -\n", "686 a 22.67 0.750 u g c v 2.000 f t 2 t g 00200 394 -\n", "687 a 25.25 13.500 y p ff ff 2.000 f t 1 t g 00200 1 -\n", "688 b 17.92 0.205 u g aa v 0.040 f f 0 f g 00280 750 -\n", "689 b 35.00 3.375 u g c h 8.290 f f 0 t g 00000 0 -\n" ] } ], "source": [ "import numpy as np\n", "\n", "# Reemplazamos los '?'s con NaN\n", "cc_apps = cc_apps.replace('?', np.nan)\n", "\n", "# Observemos nuevamente el valor de la feature 0 para la fila 673\n", "print(cc_apps.tail(17))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Si deseamos observarlo más claro, hay 12 valores faltantes para la feature 0." ] }, { "cell_type": "code", "execution_count": 4, "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", " \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", " \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", " \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", "
0123456789101112131415
248NaN24.5012.750ugcbb4.750tt2fg00073444+
327NaN40.833.500ugibb0.500ff0fs011600-
346NaN32.251.500ugcv0.250ff0tg00372122-
374NaN28.170.585ugaav0.040ff0fg002601004-
453NaN29.750.665ugwv0.250ff0tg003000-
479NaN26.502.710ypNaNNaN0.085ff0fs000800-
489NaN45.331.000ugqv0.125ff0tg002630-
520NaN20.427.500ugkv1.500tt1fg00160234+
598NaN20.080.125ugqv1.000ft1fg00240768+
601NaN42.251.750ypNaNNaN0.000ff0tg001501-
641NaN33.172.250ypccv3.500ff0tg00200141-
673NaN29.502.000ypeh2.000ff0fg0025617-
\n", "
" ], "text/plain": [ " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n", "248 NaN 24.50 12.750 u g c bb 4.750 t t 2 f g 00073 444 +\n", "327 NaN 40.83 3.500 u g i bb 0.500 f f 0 f s 01160 0 -\n", "346 NaN 32.25 1.500 u g c v 0.250 f f 0 t g 00372 122 -\n", "374 NaN 28.17 0.585 u g aa v 0.040 f f 0 f g 00260 1004 -\n", "453 NaN 29.75 0.665 u g w v 0.250 f f 0 t g 00300 0 -\n", "479 NaN 26.50 2.710 y p NaN NaN 0.085 f f 0 f s 00080 0 -\n", "489 NaN 45.33 1.000 u g q v 0.125 f f 0 t g 00263 0 -\n", "520 NaN 20.42 7.500 u g k v 1.500 t t 1 f g 00160 234 +\n", "598 NaN 20.08 0.125 u g q v 1.000 f t 1 f g 00240 768 +\n", "601 NaN 42.25 1.750 y p NaN NaN 0.000 f f 0 t g 00150 1 -\n", "641 NaN 33.17 2.250 y p cc v 3.500 f f 0 t g 00200 141 -\n", "673 NaN 29.50 2.000 y p e h 2.000 f f 0 f g 00256 17 -" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Filtramos los valores NaN para la columna 0\n", "cc_apps[cc_apps[0].isna()]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hemos reemplazado todos los signos de interrogación con NaN. Esto nos ayudará al momento de aplicar la estrategia de valores faltantes que vamos a realizar.\n", "\n", "Una pregunta importante que surge aquí es *¿por qué le damos tanta importancia a los valores perdidos? ¿No se pueden simplemente ignorar?*\n", "\n", "Ignorar los valores perdidos puede afectar en gran medida el rendimiento del modelo de machine learning. Si bien podría ignorar los valores faltantes, nuestro modelo también perdería información potencialmente útil del dataset para su entrenamiento. Debido a esto, hay muchos modelos que no pueden manejar valores faltantes implícitamente.\n", "\n", "Entonces, para evitar este problema, vamos a imputar o \"llenar\" los valores faltantes con una estrategia llamada *mean imputation*. Esta estrategia lo que hace es reemplazar los valores faltantes con el valor de la media para todos los valores de esa característica en el dataset. Obviamente, esto aplica solo para las features de tipo numéricas." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 12\n", "1 12\n", "2 0\n", "3 6\n", "4 6\n", "5 9\n", "6 9\n", "7 0\n", "8 0\n", "9 0\n", "10 0\n", "11 0\n", "12 0\n", "13 13\n", "14 0\n", "15 0\n", "dtype: int64" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Imputamos los valores faltantes con la media\n", "cc_apps.fillna(cc_apps.mean(), inplace=True)\n", "\n", "# Contamos el número de NaNs para verificar\n", "cc_apps.isnull().sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Como vemos, nos hemos ocupado con éxito de los valores faltantes presentes en las columnas numéricas. Todavía hay algunos valores faltantes que imputar para las columnas 0, 1, 3, 4, 5, 6 y 13. Todas estas columnas contienen datos categóricos (no numéricos) y por eso la estrategia de imputación media no funcionaría aquí. Esto necesita un tratamiento diferente.\n", "\n", "Vamos a imputar estos valores faltantes con los valores más frecuentes presentes en sus respectivas columnas. Esta es una buena práctica cuando se trata de imputar valores faltantes para datos categóricos en general. \n", "\n", "Para hacerlo, recorreremos cada una de las columnas del DataFrame y sólo en aquellas con valores categóricos imputaremos el valor que mayor recuento tenga para dicha columna." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0 0\n", "1 0\n", "2 0\n", "3 0\n", "4 0\n", "5 0\n", "6 0\n", "7 0\n", "8 0\n", "9 0\n", "10 0\n", "11 0\n", "12 0\n", "13 0\n", "14 0\n", "15 0\n", "dtype: int64" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Recorremos cada columna de cc_apps\n", "for col in cc_apps.columns:\n", " # Chequeamos si la columna es de tipo 'object'\n", " if cc_apps[col].dtypes == 'object':\n", " # Imputamos con el valor más frecuente\n", " cc_apps = cc_apps.fillna(cc_apps[col].value_counts().index[0])\n", "\n", "# Volvemos a contar el número de NaNs en el dataset para verificar\n", "cc_apps.isnull().sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Preprocesamiento y división del dataset\n", "\n", "Hemos solucionado el problema de los valores faltantes.\n", "\n", "Todavía se necesita un preprocesamiento de datos menor pero esencial antes de continuar con la construcción de nuestro modelo. Vamos a dividir estos pasos de preprocesamiento restantes en tres tareas principales:\n", "\n", " 1. Convertir los datos categóricos en numéricos.\n", " 2. Dividir los datos en conjuntos de entrenamiento y pruebas (*train and test sets*).\n", " 3. Escalar los valores de las características a un rango uniforme.\n", "\n", "Primero, convertiremos todos los valores no numéricos en valores numéricos. Hacemos esto porque no solo da como resultado un cálculo más rápido, sino que también muchos modelos de machine learning (especialmente los desarrollados con scikit-learn) requieren que los datos estén en un formato estrictamente numérico. Haremos esto utilizando una técnica llamada *label encoding*. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from sklearn.preprocessing import LabelEncoder\n", "\n", "# Instanciamos LabelEncoder\n", "le = LabelEncoder()\n", "\n", "# Recorremos todos los valores de cada columna y extraemos su tipo de dato\n", "for col in cc_apps.columns:\n", " # Chequeamos si la columna es de tipo 'object'\n", " if cc_apps[col].dtypes == 'object':\n", " # Usamos LabelEncoder para realizar la transformación numérica\n", " cc_apps[col]=le.fit_transform(cc_apps[col])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hemos convertido todos los valores categóricos en valores numéricos.\n", "\n", "Ahora, dividiremos nuestro dataset en un conjunto de entrenamiento y otro de pruebas que utilizaremos en esas dos fases diferentes del modelado respectivamente.\n", "\n", "Idealmente, no se debe utilizar ninguna información de los datos del conjunto de pruebas para escalar los datos de entrenamiento ni mucho menos se deben utilizar durante el proceso de entrenamiento del modelo de machine learning. Por lo tanto, primero dividiremos los datos y luego aplicaremos el proceso de reescalamiento.\n", "\n", "Además, podemos intuír que algunos datos como la `Licencia de conductor` y el `Código Postal` no son tan significativos al momento a predecir las aprobaciones de tarjetas de crédito como sí lo son otros datos de este dataset. Por lo tanto, deberíamos descartarlos para diseñar nuestro modelo de machine learning con el mejor conjunto de características. En la literatura sobre ciencia de datos, esto a menudo se denomina _**selección de características** (feature selection)_. " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "\n", "# Eliminamos las features 11 y 13 y convertimos el DataFrame en un NumPy array\n", "cc_apps = cc_apps.drop([11, 13], axis=1)\n", "cc_apps = cc_apps.to_numpy()\n", "\n", "# Separamos características y etiquetas en variables distintas\n", "X, y = cc_apps[:,0:12] , cc_apps[:,13]\n", "\n", "# Dividimos el dataset en conjuntos de entrenamiento y prueba\n", "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Los datos ya fueron divididos en dos conjuntos separados. Sólo nos queda un paso final antes de que podamos entrenar nuesto modelo, el escalado de las variables.\n", "\n", "Cuando un dataset tiene rangos variables, como en este caso, es posible que un pequeño cambio en una característica en particular no genere un efecto significativo en otra, lo que puede causar muchos problemas en el modelado predictivo. De aquí la necesidad de llevar todas las características a una escala similar. \n", "\n", "Intentemos comprender qué significan estos valores escalados en el mundo real. Usemos `Puntuación de Crédito` como ejemplo. El puntaje crediticio de una persona es su solvencia basada en su historial crediticio. Cuanto mayor sea este número, se considera que una persona es más confiable desde el punto de vista financiero. Por lo tanto, un puntaje crediticio de 1 será el más alto, ya que estamos escalando todos los valores al rango entre 0 y 1." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from sklearn.preprocessing import MinMaxScaler\n", "\n", "# Instanciamos MinMaxScaler y lo utilizamos para escalar X_train y X_test\n", "scaler = MinMaxScaler(feature_range=(0, 1))\n", "rescaledX_train = scaler.fit_transform(X_train)\n", "rescaledX_test = scaler.fit_transform(X_test)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Entrenando el modelo\n", "\n", "Esencialmente, predecir si una solicitud de tarjeta de crédito será aprobada o no es una tarea de [clasificación](https://en.wikipedia.org/wiki/Statistical_classification). [Según UCI](http://archive.ics.uci.edu/ml/machine-learning-databases/credit-screening/crx.names), nuestro conjunto de datos contiene más instancias que corresponden al estado \"Denegado\" que las instancias correspondientes al estado \"Aprobado\". Específicamente, de 690 casos, hay 383 (55,5%) aplicaciones que fueron denegadas y 307 (44,5%) aplicaciones que fueron aprobadas.\n", "\n", "Esto nos da un punto de referencia. Un buen modelo de aprendizaje automático debería poder predecir con precisión el estado de las aplicaciones con respecto a estas estadísticas.\n", "\n", "¿Qué modelo deberíamos elegir? \n", "\n", "Una pregunta que debe hacerse es: ¿las características que afectan el proceso de decisión de aprobación de la tarjeta de crédito están correlacionadas entre sí? Aunque podemos medir la correlación, en este caso nos limitaremos a confiar en nuestra intuición de que, de hecho, están correlacionados por ahora. Debido a esta correlación, aprovecharemos el hecho de que los modelos lineales generalizados funcionan bien en estos casos. Comencemos nuestro modelado de machine learning con un modelo de **logistic regression** (modelo lineal generalizado). " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "LogisticRegression()" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.linear_model import LogisticRegression\n", "\n", "# Instanciamos el clasificador LogisticRegression con sus parámetros por defecto\n", "logreg = LogisticRegression()\n", "\n", "# Entrenamos logreg con los datos escalados\n", "logreg.fit(rescaledX_train, y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Evaluemos la performance\n", "\n", "Ya tenemos nuestro modelo entrenado, pero ¿qué tan bien funciona?\n", "\n", "Evaluaremos nuestro modelo con el conjunto de prueba respecto a la exactitud de la clasificación, es decir, evaluando la métrica `accuracy`, pero también echaremos un vistazo a la matriz de confusión del modelo. \n", "\n", "En nuestro caso de estudio, es igualmente importante ver si nuestro modelo es capaz de predecir como aprobadas aquellas solicitudes realmente aprobadas tanto como predecir como denegadas aquellas originalmente rechazadas. Si nuestro modelo no está funcionando bien en este aspecto, entonces podría terminar aprobando solicitudes que deberían haber sido rechazadas. La matriz de confusión nos ayuda a ver el desempeño de nuestro modelo desde estos aspectos. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Accuracy: 0.8377192982456141\n", "[[93 10]\n", " [27 98]]\n" ] } ], "source": [ "from sklearn.metrics import confusion_matrix\n", "\n", "# Utilizamos el estimador logreg para predecir instancias sobre el test set y las almacenamos\n", "y_pred = logreg.predict(rescaledX_test)\n", "\n", "# Obtenemos la puntuación \"accuracy score\"\n", "print(\"Accuracy: \", logreg.score(rescaledX_test, y_test))\n", "\n", "# Mostramos la matriz de confusión del modelo\n", "print(confusion_matrix(y_test, y_pred))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Ajustando el modelo\n", "\n", "¡Nuestro modelo fue bastante bueno! Pudo producir un _accuracy_ de casi el 84%.\n", "\n", "En la matriz de confusión, el primer elemento de la primera fila representa los verdaderos negativos, es decir, el número de instancias negativas (solicitudes denegadas) predichas correctamente por el modelo. El último elemento de la segunda fila representa los verdaderos positivos, es decir, el número de instancias positivas (solicitudes aprobadas) predichas correctamente por el modelo.\n", "\n", "Veamos si podemos mejorarlo. Podemos realizar una búsqueda en cuadrícula -_grid search_- de los parámetros del modelo para mejorar su capacidad para predecir las solicitudes de tarjetas de crédito.\n", "\n", "La implementación de scikit-learn de logistic regression consta de diferentes hiperparámetros, pero en este caso buscaremos en la cuadrícula sólo los siguientes:\n", "\n", "- `tol`\n", "- `max_iter` " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import GridSearchCV\n", "\n", "# Definimos la grilla de valores para 'tol' y 'max_iter'\n", "tol = [0.01, 0.001, 0.0001]\n", "max_iter = [100, 150, 200]\n", "\n", "# Creamos un diccionario con 'tol' y 'max_iter' como claves y las listas anteriores como sus valores\n", "param_grid = dict(tol=tol, max_iter=max_iter)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hemos definido la cuadrícula de valores de hiperparámetros y los hemos convertido en un formato de diccionario único que `GridSearchCV()` espera como uno de sus parámetros. Ahora, comenzaremos la búsqueda en la cuadrícula para ver qué valores funcionan mejor.\n", "\n", "Crearemos una instancia de `GridSearchCV()` con nuestro modelo **logreg** anterior y todos los datos que tenemos. En lugar de pasar `X_train` y `X_test` por separado, proporcionaremos `X` (versión escalada) e `y`. También indicaremos a `GridSearchCV()` que realice [cross-validation](https://es.wikipedia.org/wiki/Validaci%C3%B3n_cruzada) de cinco pliegues.\n", "\n", "Finalizaremos este proyecto almacenando la puntuación mejor lograda y los mejores parámetros respectivos.\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mejor puntuación: 0.850725 , utilizando {'max_iter': 100, 'tol': 0.01}\n" ] } ], "source": [ "# Instanciamos GridSearchCV con los parámetros requeridos\n", "grid_model = GridSearchCV(estimator=logreg, param_grid=param_grid, cv=5)\n", "\n", "# Utilizamos nuevamente 'scaler' para escalar X\n", "rescaledX = scaler.fit_transform(X)\n", "\n", "# Entrenamos el modelo\n", "grid_model_result = grid_model.fit(rescaledX, y)\n", "\n", "# Obtenemos los valores de los hiperparámetros que mejores resultados arrojan\n", "best_score, best_params = grid_model_result.best_score_, grid_model_result.best_params_\n", "print(\"Mejor puntuación: %f , utilizando %s\" % (best_score, best_params))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conclusiones\n", "\n", "Al crear este predictor de solicitudes de tarjetas de crédito, abordamos algunos de los pasos de preprocesamiento más conocidos, como el **escalado**, la **codificación de etiquetas** y la **imputación de valores faltantes**. Terminamos con algo de **machine learning** para predecir si la solicitud de una persona para una tarjeta de crédito se aprobaría o no, dada cierta información sobre esa persona. \n", "\n", "La idea es que fuera algo introductorio. Más adelante podríamos retomarlo para evaluar el modelo con otras métricas más significativas e incluso probar y comparar con otros modelos de clasificación diferentes." ] } ], "metadata": { "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.5" } }, "nbformat": 4, "nbformat_minor": 4 }