{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ocho modelos\n",
    "\n",
    "Logística, árbol, bosque, boosting, vecinos, Bayes y SVM sobre las mismas columnas, y por qué la de 1958 queda primera.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 13 de **Machine learning desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/machine-learning-desde-cero/los-modelos/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hasta aquí hemos usado un solo modelo, la regresión logística, porque hacía\n",
    "falta uno para poder hablar de lo demás. Hoy probamos ocho 🎰\n",
    "\n",
    "Y quiero que apuestes antes de mirar: **¿cuál crees que va a ganar?** Apúntalo mentalmente, porque la respuesta es la lección del capítulo y funciona mucho mejor si primero te mojas 🎲\n",
    "\n",
    "Y te adelanto el final, porque es lo interesante del capítulo: **gana la\n",
    "regresión logística**, que es el más simple y el más viejo de todos. Los\n",
    "que salen en las noticias quedan por debajo.\n",
    "\n",
    "Esto pasa mucho más de lo que te cuentan, y saber por qué es más útil que\n",
    "saberse los ocho algoritmos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "def prepara(v):\n",
    "    v = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "    v['sin_descuento'] = v['descuento'].isna().astype(int)\n",
    "    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)\n",
    "    v['precio_unitario'] = v['monto'] / v['unidades']\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "def arma(clasificador):\n",
    "    \"\"\"El pipeline del capítulo 11 con el modelo que le pases.\"\"\"\n",
    "    pre = ColumnTransformer([\n",
    "        ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                          ('e', StandardScaler())]), NUMERICAS),\n",
    "        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),\n",
    "    ])\n",
    "    return Pipeline([('pre', pre), ('mod', clasificador)])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "print(X_tr.shape, X_te.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Regresión logística: la que hay que entender"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "p=σ(z)=11+e−z,z=b+∑jwjxj\n",
    "\n",
    "el modelo suma las columnas con un peso cada una y esa suma la aplasta entre 0 y 1 para que se pueda leer como probabilidad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le da un peso a cada columna, los suma y convierte esa suma en una\n",
    "probabilidad. Nada más. Es de 1958 y sigue ganando."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logistica = arma(LogisticRegression(max_iter=1000, random_state=42)).fit(X_tr, y_tr)\n",
    "print('exactitud:', round(logistica.score(X_te, y_te), 4))\n",
    "print('AUC      :', round(roc_auc_score(y_te, logistica.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sus dos ventajas son enormes y no se ven en el número: **se puede\n",
    "explicar** (cada peso dice cuánto suma o resta cada columna) y\n",
    "**no se rompe** con datos que no vio.\n",
    "\n",
    "Su límite: solo sabe sumar. No puede aprender que \"WhatsApp funciona\n",
    "*solo* para mayoristas\", que en el capítulo 4 vimos que pasa. Esas\n",
    "interacciones hay que dárselas escritas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Árbol de decisión: el que se lee"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Gini=1−∑kpk2\n",
    "\n",
    "cuánto se mezclan las clases dentro de una hoja: cero si todas son iguales y máximo si están mitad y mitad"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.tree import DecisionTreeClassifier\n",
    "\n",
    "arbol = arma(DecisionTreeClassifier(max_depth=4, random_state=42)).fit(X_tr, y_tr)\n",
    "print('exactitud:', round(arbol.score(X_te, y_te), 4))\n",
    "print('AUC      :', round(roc_auc_score(y_te, arbol.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Peor que la logística. Un árbol solo hace cortes rectos (\"monto mayor que\n",
    "532\") y con cuatro niveles se queda corto.\n",
    "\n",
    "Lo que sí tiene es que se puede dibujar y enseñar en una reunión, y eso a\n",
    "veces vale más que cinco puntos de AUC 🌳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. El mismo árbol sin límite: la lección del capítulo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "H=−∑kpklog2pk\n",
    "\n",
    "lo mismo medido en bits, o sea cuántas preguntas de sí o no harían falta para adivinar la clase de una fila"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "libre = arma(DecisionTreeClassifier(random_state=42)).fit(X_tr, y_tr)\n",
    "print('en train:', round(libre.score(X_tr, y_tr), 4))\n",
    "print('en test :', round(libre.score(X_te, y_te), 4))\n",
    "print('AUC     :', round(roc_auc_score(y_te, libre.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**1,0 en entrenamiento y 0,5613 en examen.** Cien por ciento\n",
    "perfecto en lo que ya vio, y peor que el listón en lo que no 😱\n",
    "\n",
    "Eso es el sobreajuste, el overfitting, en su forma más pura: el árbol creció hasta que cada\n",
    "hoja tenía una sola venta, o sea que memorizó las 2.250 filas una por una. Un\n",
    "diccionario, no un modelo.\n",
    "\n",
    "Guárdate el par de números. **1,0 en train es siempre una alarma**,\n",
    "nunca un logro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Bosque aleatorio: muchos árboles votando"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "\n",
    "bosque = arma(RandomForestClassifier(n_estimators=300, random_state=42,\n",
    "                                     n_jobs=-1)).fit(X_tr, y_tr)\n",
    "print('en train:', round(bosque.score(X_tr, y_tr), 4))\n",
    "print('AUC     :', round(roc_auc_score(y_te, bosque.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,677. Mejor que el árbol solo (0,6643) y todavía por debajo de la logística\n",
    "(0,7214).\n",
    "\n",
    "La idea del bosque es preciosa: entrena 300 árboles, cada uno con una muestra\n",
    "distinta de filas y de columnas, y los hace votar. Los errores de uno los\n",
    "corrigen los otros.\n",
    "\n",
    "Pero fíjate en el `train`: **1,0 otra vez**. Los\n",
    "árboles siguen memorizando por dentro; lo que pasa es que el promedio de 300\n",
    "memorizaciones distintas generaliza algo mejor. Y por defecto scikit-learn los\n",
    "deja crecer sin límite 😬"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "podado = arma(RandomForestClassifier(n_estimators=300, max_depth=5,\n",
    "                                     random_state=42, n_jobs=-1)).fit(X_tr, y_tr)\n",
    "print('en train:', round(podado.score(X_tr, y_tr), 4))\n",
    "print('AUC     :', round(roc_auc_score(y_te, podado.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Limitando la profundidad a 5: el train baja a 0,704 y el AUC sube a\n",
    "**0,7115**. Casi cuatro puntos ganados por poner un freno.\n",
    "\n",
    "Es de las cosas más rentables que existen y casi nadie la hace, porque\n",
    "`RandomForestClassifier()` a secas ya \"funciona\" 🌲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5 y 6. Boosting: los árboles que se corrigen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝔼[(y−f^)2]=Sesgo2⏟modelo muy simple+Varianza⏟modelo muy pegado+σ2⏟ruido, no se toca\n",
    "\n",
    "el error se parte en tres, y bajar uno de los dos primeros normalmente sube el otro, mientras el tercero no se puede tocar por más modelo que le eches"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier\n",
    "\n",
    "boosting = arma(GradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr)\n",
    "print('boosting     :', round(roc_auc_score(y_te, boosting.predict_proba(X_te)[:, 1]), 4))\n",
    "\n",
    "rapido = arma(HistGradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr)\n",
    "print('hist boosting:', round(roc_auc_score(y_te, rapido.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,6876 y 0,6799. Los dos por debajo de la logística.\n",
    "\n",
    "El boosting es distinto del bosque: en vez de 300 árboles independientes,\n",
    "entrena uno detrás de otro y **cada uno se dedica a los casos que falló el\n",
    "anterior**. Suele ser lo que gana las competencias con datos de tabla, y\n",
    "aquí no gana. Ya llegaremos a por qué.\n",
    "\n",
    "`HistGradientBoosting` es la versión rápida, la que compite con\n",
    "XGBoost y LightGBM, y además **acepta nulos sin imputar**, que es\n",
    "comodísimo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7 y 8. Los otros dos que hay que conocer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.naive_bayes import GaussianNB\n",
    "\n",
    "vecinos = arma(KNeighborsClassifier(n_neighbors=25)).fit(X_tr, y_tr)\n",
    "print('vecinos    :', round(roc_auc_score(y_te, vecinos.predict_proba(X_te)[:, 1]), 4))\n",
    "\n",
    "bayes = arma(GaussianNB()).fit(X_tr, y_tr)\n",
    "print('naive bayes:', round(roc_auc_score(y_te, bayes.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Vecinos** no aprende nada: guarda todas las filas y, cuando le\n",
    "preguntas, busca las 25 más parecidas y vota. Es lentísimo con datos grandes y\n",
    "necesita sí o sí que las columnas estén escaladas, cosa que nuestro pipeline ya\n",
    "hace.\n",
    "\n",
    "**Naive Bayes** saca 0,7145, que es el segundo mejor de los ocho.\n",
    "Y es el más simple de todos: asume que las columnas son independientes entre sí,\n",
    "cosa que es falsa siempre, y aun así funciona. Se llama \"naive\" (ingenuo) por\n",
    "eso 🤓"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La tabla completa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Modelo | AUC | Train | Para qué sirve |\n",
    "|---|---|---|---|\n",
    "| **Regresión logística** | **0,7214** | 0,6658 | Casi siempre el primero. Se explica y no se rompe |\n",
    "| Naive Bayes | 0,7145 | 0,6480 | Rapidísimo, buen listón, muy usado en texto |\n",
    "| Bosque podado | 0,7115 | 0,7040 | Cuando hay interacciones y no linealidad |\n",
    "| Boosting | 0,6876 | 0,7493 | Suele ganar con más datos y con ajuste |\n",
    "| SVM | 0,6826 | 0,7320 | Pocos datos y muchas columnas |\n",
    "| Hist boosting | 0,6799 | 0,9387 | Como el boosting pero rápido y aguanta nulos |\n",
    "| Bosque sin podar | 0,6770 | 1,0000 | Lo de arriba, mal configurado |\n",
    "| Vecinos | 0,6766 | 0,6787 | Didáctico. En producción casi nunca |\n",
    "| Árbol d=4 | 0,6643 | 0,6658 | Cuando hay que dibujarlo en una reunión |\n",
    "| Árbol sin límite | 0,5525 | 1,0000 | Nada. Es el ejemplo de qué no hacer |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la columna `train` de arriba abajo: los que están arriba\n",
    "tienen train parecido al test, y los que están abajo lo tienen altísimo.\n",
    "**Esa columna predice el orden mejor que el nombre del algoritmo** 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Y si los juntamos a todos?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es la pregunta que sale sola mirando la tabla, y tiene nombre:\n",
    "**apilar modelos**, o *stacking*. La idea es bonita. Entrenas\n",
    "varios modelos distintos, y encima pones uno chiquito cuyo único trabajo es\n",
    "aprender **cuándo hacerle caso a cada uno**.\n",
    "\n",
    "No es lo mismo que el bosque aleatorio, ojo. Ahí eran muchos árboles iguales\n",
    "votando. Acá son modelos de familias distintas, que se equivocan en cosas\n",
    "distintas, y alguien arriba decidiendo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import StackingClassifier\n",
    "\n",
    "base = [('log', arma(LogisticRegression(max_iter=1000))),\n",
    "        ('bosque', arma(RandomForestClassifier(n_estimators=300, random_state=42))),\n",
    "        ('boost', arma(GradientBoostingClassifier(random_state=42)))]\n",
    "\n",
    "apilado = StackingClassifier(estimators=base, final_estimator=LogisticRegression(), cv=5)\n",
    "apilado.fit(X_tr, y_tr)\n",
    "print('apilado :', round(roc_auc_score(y_te, apilado.predict_proba(X_te)[:, 1]), 4))\n",
    "\n",
    "for nombre, modelo in base:\n",
    "    modelo.fit(X_tr, y_tr)\n",
    "    print(f'{nombre:8}:', round(roc_auc_score(y_te, modelo.predict_proba(X_te)[:, 1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres modelos, uno arriba coordinando, validación cruzada por dentro, bastante\n",
    "más tiempo de entrenamiento. Y saca **0,7206 contra los 0,7214 de la\n",
    "regresión logística sola**.\n",
    "\n",
    "Pierde. Por poquito, pero pierde, y encima cuesta varias veces más de entrenar\n",
    "y es muchísimo más difícil de explicar en una reunión.\n",
    "\n",
    "Y tiene su explicación, que es la misma de todo el capítulo. Apilar gana\n",
    "cuando los modelos de abajo son parecidos de buenos y se equivocan en sitios\n",
    "distintos. Acá uno saca 0,72 y los otros dos 0,68, así que el de arriba no tiene\n",
    "gran cosa que combinar: lo mejor que puede hacer es copiar al primero, y le sale\n",
    "un pelín peor por el ruido de intentarlo 📊\n",
    "\n",
    "Guarda la técnica igual. En competencias de datos apilar es lo que gana, y en\n",
    "esos datos sí se cumple la condición. Lo que no hay que hacer es traerla acá por\n",
    "defecto: **si la complicación no se paga en el número, no se paga**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué gana la simple, que es lo que hay que entender"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los modelos complicados ganan cuando hay algo complicado que aprender. Aquí\n",
    "no lo hay, y por tres razones concretas:\n",
    "\n",
    "- 📉 **Poca señal.** El mejor AUC del archivo es 0,72. Cuando la\n",
    "señal es débil, lo que un modelo flexible encuentra de más suele ser ruido.\n",
    "\n",
    "- 📊 **Pocas filas.** 2.250 para entrenar es poco. El boosting y\n",
    "los bosques empiezan a brillar con decenas de miles.\n",
    "\n",
    "- ➕ **Relaciones simples.** Del capítulo 4: más monto, más\n",
    "cierre; más satisfacción, más cierre. Todo monótono, que es justo lo que una\n",
    "recta hace bien.\n",
    "\n",
    "Y la regla que yo sigo: **empieza siempre por la logística, y solo\n",
    "cambia si otro modelo le gana de verdad**. Con \"de verdad\" quiero decir\n",
    "en validación cruzada, que es el capítulo 16, y no en una sola partición 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El SVM, sin la trampa de las probabilidades\n",
    "\n",
    "Entrena una máquina de vectores de soporte y saca su AUC\n",
    "sin pedirle probabilidades."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.svm import SVC\n",
    "\n",
    "svm = arma(SVC(random_state=42)).fit(X_tr, y_tr)\n",
    "print('exactitud:', round(svm.score(X_te, y_te), 4))\n",
    "print('AUC      :', round(roc_auc_score(y_te, svm.decision_function(X_te)), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "exactitud: 0.644\n",
    "AUC      : 0.6826\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en `decision_function` en vez de\n",
    "`predict_proba`: el SVM no da probabilidades de forma natural, da una\n",
    "distancia a la frontera. Para el AUC da igual, porque el AUC solo mira el\n",
    "**orden** 📐\n",
    "\n",
    "Pedirle probabilidades con `probability=True` lo hace muchísimo más\n",
    "lento y además está en camino de desaparecer de scikit-learn."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuántos vecinos\n",
    "\n",
    "Prueba k = 1, 5, 25 y 100 y mira el patrón."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for k in (1, 5, 25, 100):\n",
    "    m = arma(KNeighborsClassifier(n_neighbors=k)).fit(X_tr, y_tr)\n",
    "    print(f'k={k:3}  train {m.score(X_tr, y_tr):.4f}  '\n",
    "          f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "k=  1  train 1.0000  AUC 0.5658\n",
    "k=  5  train 0.7289  AUC 0.6201\n",
    "k= 25  train 0.6787  AUC 0.6766\n",
    "k=100  train 0.6520  AUC 0.6985\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con k=1 el train da 1,0 (cada fila es su propio vecino más cercano) y el AUC\n",
    "se hunde. Al subir k, el train baja y el AUC sube.\n",
    "\n",
    "Ese `k` es el ejemplo más limpio que existe del equilibrio entre\n",
    "memorizar y generalizar: **es literalmente un dial** 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Qué columnas usa el bosque\n",
    "\n",
    "Las cinco columnas más importantes según el bosque\n",
    "podado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "nombres = podado.named_steps['pre'].get_feature_names_out()\n",
    "peso = podado.named_steps['mod'].feature_importances_\n",
    "for i in np.argsort(peso)[::-1][:5]:\n",
    "    print(f'{nombres[i]:30} {peso[i]:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "num__monto                     0.1872\n",
    "num__satisfaccion              0.1281\n",
    "num__precio_unitario           0.1193\n",
    "cat__segmento_Bodega           0.1046\n",
    "num__sin_compra_previa         0.0894\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ojo con esta lista, que engaña: la importancia de un bosque\n",
    "**favorece a las columnas con muchos valores distintos**, porque\n",
    "ofrecen más sitios donde cortar.\n",
    "\n",
    "Aquí se ve: el monto se lleva 0,1872 y `sin_compra_previa`, que en\n",
    "el capítulo 4 medimos en veinte puntos de tasa de cierre, se queda en 0,0894, la\n",
    "mitad. No es que valga la mitad: es que es una columna de dos valores y solo\n",
    "ofrece un sitio donde cortar.\n",
    "\n",
    "La forma honesta de medir importancia es otra y está en el capítulo 20 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El árbol, dibujado en palabras\n",
    "\n",
    "Enseña las primeras reglas del árbol de profundidad 3."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.tree import export_text\n",
    "\n",
    "corto = arma(DecisionTreeClassifier(max_depth=3, random_state=42)).fit(X_tr, y_tr)\n",
    "nombres3 = corto.named_steps['pre'].get_feature_names_out()\n",
    "print(export_text(corto.named_steps['mod'], feature_names=list(nombres3))[:700])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "|--- num__monto <= -0.64\n",
    "|   |--- num__satisfaccion <= 1.09\n",
    "|   |   |--- num__sin_compra_previa <= 0.83\n",
    "|   |   |   |--- class: 0\n",
    "|   |   |--- num__sin_compra_previa >  0.83\n",
    "|   |   |   |--- class: 0\n",
    "|   |--- num__satisfaccion >  1.09\n",
    "|   |   |--- num__precio_unitario <= -0.46\n",
    "|   |   |   |--- class: 0\n",
    "|   |   |--- num__precio_unitario >  -0.46\n",
    "|   |   |   |--- class: 1\n",
    "|--- num__monto >  -0.64\n",
    "|   |--- num__sin_compra_previa <= 0.83\n",
    "|   |   |--- num__satisfaccion <= 0.36\n",
    "|   |   |   |--- class: 1\n",
    "|   |   |--- num__satisfaccion >  0.36\n",
    "|   |   |   |--- class: 1\n",
    "|   |--- num__sin_compra_previa >  0.83\n",
    "|   |   |--- num__visita_numero <= -0.43\n",
    "|   |   |   |--- class: 1\n",
    "|   |   |--- num__visita_\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso se puede pegar en un correo y lo entiende cualquiera. Es la razón por la\n",
    "que los árboles sobreviven: **en algunos sitios, poder explicar la regla\n",
    "vale más que acertar más**.\n",
    "\n",
    "Un banco que rechaza un crédito tiene que decir por qué. Con esto puede; con\n",
    "un bosque de 300 árboles, no 🏦"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El boosting, cuánto le importa la tasa de aprendizaje\n",
    "\n",
    "Prueba tres tasas y mira el sobreajuste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for tasa in (0.01, 0.1, 0.5):\n",
    "    m = arma(GradientBoostingClassifier(learning_rate=tasa,\n",
    "                                        random_state=42)).fit(X_tr, y_tr)\n",
    "    print(f'tasa {tasa:<5} train {m.score(X_tr, y_tr):.4f}  '\n",
    "          f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "tasa 0.01  train 0.6796  AUC 0.6977\n",
    "tasa 0.1   train 0.7493  AUC 0.6876\n",
    "tasa 0.5   train 0.9044  AUC 0.6515\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 0,01 aprende despacito y generaliza mejor; con 0,5 se lanza y memoriza.\n",
    "Esa es la perilla más importante del boosting, y la regla es\n",
    "**tasa baja y muchos árboles**.\n",
    "\n",
    "Con 0,01 este boosting ya se acerca a la logística. Con más datos la\n",
    "pasaría 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de pedirle probabilidades a quien no las tiene\n",
    "\n",
    "Pídele `predict_proba` a un SVM normal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "svm.predict_proba(X_te)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "AttributeError: This 'Pipeline' has no attribute 'predict_proba'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No lo tiene. Y es un error honesto: en vez de inventarse un número, el objeto\n",
    "te dice que esa operación no existe para él.\n",
    "\n",
    "Es un buen recordatorio de que **no todos los modelos dan lo\n",
    "mismo**. Si tu código pide `predict_proba` a ciegas, el día que\n",
    "alguien cambie el clasificador se rompe 🔌"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Los ocho de una pasada\n",
    "\n",
    "Un bucle que los entrena todos y los ordena por AUC. Es el\n",
    "que yo corro al empezar cualquier proyecto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "candidatos = {\n",
    "    'logistica': LogisticRegression(max_iter=1000, random_state=42),\n",
    "    'naive bayes': GaussianNB(),\n",
    "    'bosque podado': RandomForestClassifier(n_estimators=300, max_depth=5,\n",
    "                                            random_state=42, n_jobs=-1),\n",
    "    'boosting': GradientBoostingClassifier(random_state=42),\n",
    "    'vecinos': KNeighborsClassifier(n_neighbors=25),\n",
    "    'arbol d=4': DecisionTreeClassifier(max_depth=4, random_state=42),\n",
    "}\n",
    "\n",
    "resultados = []\n",
    "for nombre, clf in candidatos.items():\n",
    "    m = arma(clf).fit(X_tr, y_tr)\n",
    "    resultados.append((roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]),\n",
    "                       m.score(X_tr, y_tr), nombre))\n",
    "\n",
    "for auc, train, nombre in sorted(resultados, reverse=True):\n",
    "    print(f'{nombre:15} AUC {auc:.4f}   train {train:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "logistica       AUC 0.7214   train 0.6658\n",
    "naive bayes     AUC 0.7145   train 0.6480\n",
    "bosque podado   AUC 0.7115   train 0.7040\n",
    "boosting        AUC 0.6876   train 0.7493\n",
    "vecinos         AUC 0.6766   train 0.6787\n",
    "arbol d=4       AUC 0.6643   train 0.6658\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese bucle son diez líneas y contesta en cinco segundos la pregunta que más\n",
    "tiempo se pierde discutiendo: *¿qué modelo usamos?*\n",
    "\n",
    "Y ojo con lo que **no** hace: no ajusta hiperparámetros ni valida\n",
    "en varias particiones. Sirve para descartar rápido, no para decidir. Decidir es\n",
    "el capítulo 16 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "La regresión logística le gana a un bosque aleatorio en estos datos. ¿Qué significa?\n",
    "\n",
    "a) Que la relación es sencilla y no hace falta un modelo complicado\n",
    "\n",
    "b) Que el bosque está mal configurado\n",
    "\n",
    "c) Que hay pocos datos\n",
    "\n",
    "d) Que hay que usar redes neuronales\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Puede ser, pero con los ajustes por defecto y esta cantidad de datos, lo normal es lo que salió.\n",
    "\n",
    "*c)* Con 3.000 filas un bosque funciona de sobra. La pregunta es qué forma tiene la relación.\n",
    "\n",
    "*d)* Ese es el capítulo 1 del libro de deep learning, y ahí también pierde.\n",
    "\n",
    "Empezar por la simple no es humildad: es la forma de saber si la complicada aporta algo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El examen que dejó de serlo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Entrenas los ocho, los ordenas por su nota en el examen y te quedas con el mejor. Es exactamente lo que hace la tabla de este capítulo.\n",
    "\n",
    "```\n",
    "for nombre, mod in modelos.items():\n",
    "    mod.fit(X_tr, y_tr)\n",
    "    auc[nombre] = evalua(mod, X_te, y_te)\n",
    "\n",
    "ganador = max(auc, key=auc.get)\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Acabas de usar el examen ocho veces para tomar una decisión, así que el examen dejó de ser un examen 📄 El AUC del ganador ya no es lo que va a rendir con datos nuevos: es lo que rinde el mejor de ocho intentos sobre esas filas concretas, y parte de esa ventaja es suerte.\n",
    "\n",
    "Cuantos más modelos comparas, más se infla. Con ocho es poco y con cincuenta combinaciones de hiperparámetros ya es mucho, que es justo lo que hace una búsqueda automática.\n",
    "\n",
    "Lo correcto son **tres trozos y no dos**: uno para entrenar, otro para elegir entre modelos y un tercero que no se toca hasta el final y se usa una sola vez. Cuando no dan los datos para tres, se elige con validación cruzada sobre el entrenamiento y el examen se reserva para el número que vas a reportar. Eso es el capítulo 16."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🥇 Aquí gana la regresión logística con 0,7214, por delante de bosques y\n",
    "boosting. Empieza siempre por ella.\n",
    "\n",
    "- 🚨 Un árbol sin límite da 1,0 en train y 0,5525 en test. 1,0 en train es una\n",
    "alarma, nunca un logro.\n",
    "\n",
    "- 🌲 El bosque por defecto crece sin límite: limitándolo a profundidad 5, el AUC\n",
    "sube de 0,677 a 0,7115.\n",
    "\n",
    "- 📈 El boosting entrena árboles que corrigen al anterior. Brilla con muchos\n",
    "datos; con 2.250 filas, no.\n",
    "\n",
    "- 🎚️ El `k` de los vecinos y la tasa del boosting son diales de\n",
    "memorizar contra generalizar.\n",
    "\n",
    "- 📐 La columna `train` ordena los modelos mejor que su nombre.\n",
    "\n",
    "- 🔍 La importancia de columnas de un bosque favorece a las que tienen muchos\n",
    "valores. No es de fiar.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El modelo complicado no inventa señal donde no la hay. Solo añade formas de equivocarse."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y si quieres ver estos mismos modelos con redes neuronales al lado, y cuándo ganan y cuándo no, eso es el [libro de deep learning desde cero](https://missyera.com/guias/deep-learning-desde-cero/) 🧠\n",
    "\n",
    "En el capítulo 14 dejamos de mirar un solo número: matriz de confusión,\n",
    "precisión, exhaustividad y por qué un 95% de exactitud puede ser una\n",
    "vergüenza.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preguntas frecuentes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué es el algoritmo KNN?Vecinos más cercanos: para predecir algo mira los casos más parecidos que ya conoce y copia lo que pasó con ellos. No entrena nada, se guarda los datos y compara.\n",
    "\n",
    "¿Qué es un árbol de decisión?Una cadena de preguntas de sí o no que va partiendo los datos. Es el modelo más fácil de explicar a alguien que no es técnico, y por eso vale mucho más de lo que su precisión sugiere.\n",
    "\n",
    "¿Cuál es la diferencia entre clasificación y regresión?La clasificación predice una etiqueta, como compra o no compra. La regresión predice un número, como cuánto va a comprar.\n",
    "\n",
    "¿Qué modelo elijo para empezar?El más simple que resuelva tu caso, y compararlo siempre contra una línea base tonta. Si un modelo con árboles no le gana a \"predice siempre el promedio\", el problema no era el modelo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 13 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/los-modelos/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
