{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Escalar, imputar y codificar\n",
    "\n",
    "Seis escaladores que dan el mismo AUC, uno que vale siete centésimas, y la tabla de decisiones que sale de medirlo todo.\n",
    "\n",
    "Cuaderno de práctica del capítulo 10 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/decisiones-de-preprocessing/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"YSBtZWRpZGEgICAgICAgIEFVQyAwLjcwMTAgKy8tIDAuMDE3MAp0b2RvIHBvciBpZ3VhbCAgQVVDIDAuNjk5NyArLy0gMC4wMTY1\",\n",
    "    2: \"bWVkaWFuYSAgICAgICAgICAgICAgICAgIDUsNjQyIGJ5dGVzCktOTiBjb24gNSB2ZWNpbm9zICAgICAgMTg4LDEwNiBieXRlcw==\",\n",
    "    3: \"U3RhbmRhcmRTY2FsZXIgICBkZXN2aWFjaW9uIGRlIGxhcyAyLjI1MCBmaWxhczogYW50ZXMgMS4wMDAwICBkZXNwdWVzIDEuMDAwMApNaW5NYXhTY2FsZXIgICAgIGRlc3ZpYWNpb24gZGUgbGFzIDIuMjUwIGZpbGFzOiBhbnRlcyAwLjExMzQgIGRlc3B1ZXMgMC4wMjExClJvYnVzdFNjYWxlciAgICAgZGVzdmlhY2lvbiBkZSBsYXMgMi4yNTAgZmlsYXM6IGFudGVzIDAuOTQxNSAgZGVzcHVlcyAxMDMuNjA2Mw==\",\n",
    "    4: \"Y29ycmVjdG8gIEFVQyB0ZXN0IDAuNzIxNApjb24gdHJhbXBhIEFVQyB0ZXN0IDAuNzIxNQ==\",\n",
    "    5: \"Y29uIG9uZS1ob3QgOiAyOCBjb2x1bW5hcwpjb24gb3JkaW5hbCA6IDEzIGNvbHVtbmFzCgogIGNpdWRhZCAgICAgICAgIDYgbml2ZWxlcwogIHNlZ21lbnRvICAgICAgIDQgbml2ZWxlcwogIGNhbmFsICAgICAgICAgIDQgbml2ZWxlcwogIGNhdGVnb3JpYSAgICAgIDUgbml2ZWxlcwoKc2kgY2xpZW50ZV9pZCBlbnRyYXJhIGNvbiBvbmUtaG90OiA2MTcgY29sdW1uYXMgbWFz\",\n",
    "    6: \"Y29sdW1uYXMgcXVlIGxlIHBhc8OpICA6IDEzCmNvbHVtbmFzIHF1ZSB1c8OzICAgICAgOiAyMwpBVUMgMC42NzkzICsvLSAwLjAxODQKCmxvIG1pc21vIGNvbiBsYXMgbnVldmUgbnVtZXJpY2FzOgpBVUMgMC42OTk3ICsvLSAwLjAxNjU=\",\n",
    "    7: \"c2VzZ2FkYXMgICAgIFsncHJlY2lvX3VuaXRhcmlvJ10Kbm9ybWFsZXMgICAgIFsndW5pZGFkZXMnLCAnbW9udG8nLCAnZGVzY3VlbnRvJywgJ3NhdGlzZmFjY2lvbicsICd2aXNpdGFfbnVtZXJvJ10KYmFuZGVyYXMgICAgIFsnc2luX2NvbXByYV9wcmV2aWEnLCAnc2luX2Rlc2N1ZW50bycsICdzaW5fc2F0aXNmYWNjaW9uJ10KY2F0ZWdvcmljYXMgIFsnY2l1ZGFkJywgJ3NlZ21lbnRvJywgJ2NhbmFsJywgJ2NhdGVnb3JpYSddCgpBVUMgMC43MDEyICsvLSAwLjAxNzU=\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya tenemos las columnas. Falta decidir **cómo entra cada una**:\n",
    "qué se escala, con qué se rellenan los huecos y cómo se convierte el texto en\n",
    "números 🔧\n",
    "\n",
    "Y te hago una pregunta incómoda de entrada: **¿cuántas de las decisiones técnicas que defendiste el año pasado podrías justificar hoy con un número?** Este capítulo va de eso 🤔\n",
    "\n",
    "Estas decisiones se toman a ojo casi siempre, copiando lo que había en el\n",
    "proyecto anterior. Aquí las vamos a medir todas, y el resultado va a ser\n",
    "incómodo: **casi ninguna mueve la aguja**.\n",
    "\n",
    "Lo cual no significa que den igual. Significa que no se eligen por el AUC 💜"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El banco de pruebas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los porqués de este banco de pruebas\n",
    "\n",
    "Comparar preprocesamientos parece trivial y tiene tres decisiones dentro, y si alguna se cae la comparación deja de medir lo que crees:\n",
    "\n",
    "- 🔁 **Validación cruzada y no un solo reparto.** Con una sola partición, la diferencia entre dos escaladores se mezcla con la suerte de esa partición. Es exactamente lo que hace falta separar aquí.\n",
    "\n",
    "- 🔒 **Todo dentro del pipeline.** Si el escalador se ajusta fuera, se ajusta con las filas del examen dentro, y entonces estarías comparando seis maneras distintas de hacer trampa.\n",
    "\n",
    "- 🎲 **La misma semilla para los seis.** Si cada uno se evalúa con un reparto distinto, la tabla mide repartos y no escaladores. Es el error más fácil de cometer y el más difícil de ver después."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una función que arma el pipeline con las piezas que le pases y devuelve la\n",
    "validación cruzada. A partir de ahí, todo el capítulo es cambiar una pieza."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.ensemble import HistGradientBoostingClassifier\n",
    "from sklearn.impute import KNNImputer, SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import (KBinsDiscretizer, MinMaxScaler, OneHotEncoder,\n",
    "                                   OrdinalEncoder, PowerTransformer,\n",
    "                                   QuantileTransformer, RobustScaler, StandardScaler)\n",
    "from sklearn.svm import SVC\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",
    "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",
    "cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n",
    "\n",
    "ONEHOT = Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                   ('c', OneHotEncoder(handle_unknown='ignore'))])\n",
    "\n",
    "def prueba(paso_numerico, paso_categorico=ONEHOT, modelo=None):\n",
    "    pre = ColumnTransformer([('num', paso_numerico, NUMERICAS),\n",
    "                             ('cat', paso_categorico, CATEGORICAS)])\n",
    "    tuberia = Pipeline([('pre', pre),\n",
    "                        ('mod', modelo or LogisticRegression(max_iter=2000,\n",
    "                                                             random_state=42))])\n",
    "    s = cross_val_score(tuberia, X_tr, y_tr, cv=cv, scoring='roc_auc')\n",
    "    return s.mean(), s.std()\n",
    "\n",
    "print('banco listo. filas de entrenamiento:', len(X_tr))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Seis escaladores, un solo resultado"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for nombre, escalador in [\n",
    "        ('sin escalar', 'passthrough'),\n",
    "        ('StandardScaler', StandardScaler()),\n",
    "        ('MinMaxScaler', MinMaxScaler()),\n",
    "        ('RobustScaler', RobustScaler()),\n",
    "        ('Quantile a normal', QuantileTransformer(output_distribution='normal',\n",
    "                                                  random_state=42)),\n",
    "        ('Yeo-Johnson', PowerTransformer(method='yeo-johnson'))]:\n",
    "    pasos = [('r', SimpleImputer(strategy='median'))]\n",
    "    if escalador != 'passthrough':\n",
    "        pasos.append(('e', escalador))\n",
    "    media, desviacion = prueba(Pipeline(pasos))\n",
    "    print(f'{nombre:20s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 0,6994 a 0,7007. Trece diezmilésimas entre el mejor y el peor, cuando la\n",
    "desviación de la propia validación cruzada es 0,0165 😐\n",
    "\n",
    "O sea que la diferencia entre no escalar nada y usar la transformación más\n",
    "sofisticada de scikit-learn es **doce veces más chica que el ruido de la\n",
    "medición**. Elegir por ese número es elegir el ruido.\n",
    "\n",
    "Y ojo con `precio_unitario`, que en el capítulo\n",
    "6 tenía asimetría 6,734. Ni siquiera Yeo-Johnson, que está\n",
    "hecho justo para eso, consigue sacarle una décima 🤷‍♀️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Con un boosting, ni eso"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for nombre, escalador in [('sin escalar', 'passthrough'),\n",
    "                          ('StandardScaler', StandardScaler())]:\n",
    "    pasos = [('r', SimpleImputer(strategy='median'))]\n",
    "    if escalador != 'passthrough':\n",
    "        pasos.append(('e', escalador))\n",
    "    media, desviacion = prueba(Pipeline(pasos),\n",
    "                               modelo=HistGradientBoostingClassifier(random_state=42))\n",
    "    print(f'{nombre:20s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mismo número, hasta la cuarta cifra, y no es casualidad 🌳\n",
    "\n",
    "Un árbol pregunta \"¿esta columna es mayor que X?\", y esa pregunta no cambia\n",
    "si multiplicas la columna por mil. Escalar antes de un árbol es trabajo que no\n",
    "hace nada, ni bueno ni malo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora dónde sí decide"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si con esos dos modelos da igual, la conclusión fácil es \"el escalado es un\n",
    "mito\". Vamos a comprobarlo con dos modelos que miden distancias."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for nombre, modelo in [('k vecinos', KNeighborsClassifier(n_neighbors=25)),\n",
    "                       ('SVM rbf', SVC(probability=True, random_state=42))]:\n",
    "    for etiqueta, escalador in [('sin escalar', 'passthrough'),\n",
    "                                ('escalado', StandardScaler())]:\n",
    "        pasos = [('r', SimpleImputer(strategy='median'))]\n",
    "        if escalador != 'passthrough':\n",
    "            pasos.append(('e', escalador))\n",
    "        media, desviacion = prueba(Pipeline(pasos), modelo=modelo)\n",
    "        print(f'{nombre:12s} {etiqueta:12s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está 💥\n",
    "\n",
    "El k vecinos pasa de 0,5863 a 0,6611. **Siete centésimas y media**,\n",
    "que es más de lo que da cualquier cambio de modelo en todo el libro. Y la SVM,\n",
    "cinco centésimas.\n",
    "\n",
    "La razón es de sentido común: esos dos modelos miden distancias entre filas.\n",
    "Sin escalar, `monto` va en miles y `satisfaccion` de 1 a\n",
    "5, así que la distancia entre dos clientes es **casi enteramente la\n",
    "diferencia de monto** y las otras ocho columnas no se enteran.\n",
    "\n",
    "Conclusión honesta y con matices: el escalado no es un mito ni un trámite.\n",
    "**Depende del modelo**, y como en un proyecto se prueban varios\n",
    "(capítulo 13), se escala siempre y se acabó 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuatro formas de rellenar un hueco"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for nombre, imputador in [\n",
    "        ('mediana', SimpleImputer(strategy='median')),\n",
    "        ('media', SimpleImputer(strategy='mean')),\n",
    "        ('constante -1', SimpleImputer(strategy='constant', fill_value=-1)),\n",
    "        ('KNN con 5 vecinos', KNNImputer(n_neighbors=5))]:\n",
    "    media, desviacion = prueba(Pipeline([('r', imputador), ('e', StandardScaler())]))\n",
    "    print(f'{nombre:20s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra vez lo mismo: de 0,6990 a 0,6998, ocho diezmilésimas 😑\n",
    "\n",
    "Y fíjate en que el KNN, que es el caro (mira los cinco vecinos más parecidos\n",
    "de cada fila con hueco), sale **el último**. No por mucho, pero el\n",
    "último.\n",
    "\n",
    "La explicación está en el capítulo 9: la información\n",
    "del hueco ya no está en el hueco, está en las banderas\n",
    "`sin_descuento` y `sin_satisfaccion`. Con la bandera\n",
    "puesta, da igual qué número pongas debajo: el modelo ya sabe que ahí no había\n",
    "nada 🕳️\n",
    "\n",
    "Por eso yo uso la mediana. No porque gane, sino porque es la que menos\n",
    "sorpresas da: no se mueve con un valor raro, no inventa correlaciones y se\n",
    "explica en una frase en la reunión."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El texto, convertido en números"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NUMERICO = Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                     ('e', StandardScaler())])\n",
    "ORDINAL = Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                    ('c', OrdinalEncoder(handle_unknown='use_encoded_value',\n",
    "                                         unknown_value=-1))])\n",
    "\n",
    "for nombre, codificador in [('one-hot', ONEHOT),\n",
    "                            ('ordinal (numero arbitrario)', ORDINAL)]:\n",
    "    for etiqueta, modelo in [('logistica', LogisticRegression(max_iter=2000,\n",
    "                                                              random_state=42)),\n",
    "                             ('boosting', HistGradientBoostingClassifier(random_state=42))]:\n",
    "        media, desviacion = prueba(NUMERICO, codificador, modelo)\n",
    "        print(f'{nombre:28s} {etiqueta:10s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí sí hay diferencia, aunque sea chica: el ordinal cuesta 0,0091 con la\n",
    "logística y 0,0080 con el boosting.\n",
    "\n",
    "Y el motivo es que el ordinal **se inventa un orden**. Le pone\n",
    "0 a Abarrotes, 1 a Bebidas, 2 a Cuidado personal, y el modelo lineal lee eso\n",
    "como que Cuidado personal es el doble que Bebidas. Que es una tontería, pero el\n",
    "modelo no lo sabe 🔢\n",
    "\n",
    "Con cuatro o cinco niveles como aquí, el one-hot añade pocas columnas y\n",
    "gana. Con los 617 niveles de `cliente_id` del capítulo\n",
    "5, el one-hot es imposible y hay que buscar otra cosa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que te espera en producción"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en que arriba puse `handle_unknown` en los dos\n",
    "codificadores. Sin eso:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    codificador = OrdinalEncoder().fit(X_tr[['ciudad']])\n",
    "    codificador.transform([['ciudad-nueva']])\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "ValueError: Found unknown categories ['ciudad-nueva'] in column 0 during transform\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El día que el equipo comercial abra en Cusco, el modelo revienta 💥\n",
    "\n",
    "Y aquí hay una decisión de verdad, no una convención. Las dos opciones son\n",
    "malas de formas distintas:\n",
    "\n",
    "- 💣 **Reventar** es ruidoso, pero avisa. Alguien lo ve el mismo\n",
    "día.\n",
    "\n",
    "- 🤫 **`handle_unknown='ignore'`** sigue funcionando y\n",
    "pone ceros. En el capítulo 27 se ve lo que eso\n",
    "significa: 300 ventas de una ciudad nueva predichas sin que nadie se entere.\n",
    "\n",
    "Mi respuesta es las dos cosas: `ignore` en el codificador para no\n",
    "tumbar el servicio, y una comprobación antes de predecir que avise por su\n",
    "cuenta. Eso es la función `revisa` del capítulo\n",
    "27 🚦"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Partir en tramos, que aquí no sale a cuenta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La idea es buena sobre el papel: si una columna no empuja en línea recta,\n",
    "partirla en tramos deja que un modelo lineal vea escalones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for nombre, transformador in [\n",
    "        ('sin tramos', 'passthrough'),\n",
    "        ('4 tramos por cuantil', KBinsDiscretizer(n_bins=4, encode='onehot-dense',\n",
    "                                                  strategy='quantile')),\n",
    "        ('10 tramos por cuantil', KBinsDiscretizer(n_bins=10, encode='onehot-dense',\n",
    "                                                   strategy='quantile'))]:\n",
    "    pasos = [('r', SimpleImputer(strategy='median'))]\n",
    "    pasos.append(('e', StandardScaler()) if transformador == 'passthrough'\n",
    "                 else ('t', transformador))\n",
    "    media, desviacion = prueba(Pipeline(pasos))\n",
    "    print(f'{nombre:24s} AUC {media:.4f} +/- {desviacion:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pierde, y cuanto más se parte, más pierde: 0,6997 sin tramos, 0,6734 con\n",
    "cuatro y 0,6670 con diez 📉\n",
    "\n",
    "Esto va en contra de lo que yo esperaba al escribir el capítulo, así que\n",
    "merece explicación. Y la explicación está en el capítulo\n",
    "8: la relación entre el monto y la compra **no\n",
    "existe** una vez descuentas el segmento.\n",
    "\n",
    "Partir en tramos sirve para que un modelo lineal capture una relación curva.\n",
    "Si no hay relación, lo único que haces es cambiar una columna con información\n",
    "por cuatro columnas con menos, y encima con las fronteras puestas donde caen los\n",
    "cuantiles y no donde el negocio tiene sentido.\n",
    "\n",
    "La regla que me llevo: **los tramos se justifican con un hallazgo, no\n",
    "con una intuición** 🪜"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La tabla de decisiones"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo el capítulo cabe en esto, y esto es lo que se pega en el informe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Decisión | Cuánto mueve el AUC | Entonces se elige por |\n",
    "|---|---|---|\n",
    "| Qué escalador | 0,0013 | Que no se rompa con un valor raro |\n",
    "| Escalar o no, con árboles | 0,0000 | Da igual, se deja por costumbre |\n",
    "| Escalar o no, con distancias | **0,0748** | Se escala, sin discusión |\n",
    "| Qué imputador | 0,0008 | Que se explique en una frase |\n",
    "| One-hot u ordinal | 0,0091 | One-hot si caben las columnas |\n",
    "| Partir en tramos | **−0,0263** | Solo con un hallazgo detrás |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos de las seis importan, y una de las dos importa *en contra* 😅\n",
    "\n",
    "Si esto te parece decepcionante, mira el otro lado: significa que puedes\n",
    "dejar de discutir cuatro de estas seis decisiones y meter ese tiempo en las que\n",
    "sí mueven, que son construir columnas (capítulo 9) y\n",
    "elegir el umbral (capítulo 15)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cada columna con su tratamiento\n",
    "\n",
    "Hasta aquí todas las numéricas recibieron el mismo trato.\n",
    "Dale a cada una lo que su ficha del capítulo 6 pedía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánto tarda cada decisión\n",
    "\n",
    "Si el AUC no decide, que decida algo. Mide el tamaño del\n",
    "objeto que hay que guardar y cargar en cada arranque."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El escalador contra un valor absurdo\n",
    "\n",
    "Mañana llega una venta con un cero de más por un error de\n",
    "tecleo. Mide qué le hace eso a cada escalador."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Imputar mirando el examen, que es trampa\n",
    "\n",
    "La mediana del imputador tiene que salir solo del\n",
    "entrenamiento. Mide cuánto se infla si sale de todo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuántas columnas salen del otro lado\n",
    "\n",
    "Antes de discutir codificaciones, mira cuántas columnas\n",
    "crea cada una. Es lo que decide si el one-hot es viable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El paso que se come columnas sin decir nada\n",
    "\n",
    "Un `ColumnTransformer` tira todo lo que no le\n",
    "nombres. Compruébalo olvidándote de tres columnas a propósito."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El resumen, en una función que decide sola\n",
    "\n",
    "Cierra el capítulo con la función que arma el\n",
    "preprocesamiento a partir de la auditoría de tipos, sin decidir nada a mano."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Pruebas seis escaladores distintos y los seis dan el mismo AUC, con las diferencias por debajo de la desviación de la validación cruzada. ¿Qué haces?\n",
    "\n",
    "a) Elijo el que menos se rompa el día que llegue un dato raro\n",
    "\n",
    "b) Me quedo con el que salió más alto, aunque sea por poco\n",
    "\n",
    "c) Pruebo más escaladores hasta que alguno destaque\n",
    "\n",
    "d) Quito el escalado, que total da igual"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El ganador que sale del ruido"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Comparas seis escaladores como manda el manual, ordenas por AUC y te quedas con el mejor. El código es correcto y la tabla es real.\n",
    "\n",
    "```\n",
    "for nombre, esc in escaladores.items():\n",
    "    auc[nombre] = evalua(esc)\n",
    "\n",
    "mejor = max(auc, key=auc.get)\n",
    "# 'RobustScaler'  (0.7007)\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 😐 Seis escaladores dan de 0,6994 a 0,7007. Trece diezmilésimas, cuando la\n",
    "desviación de la propia validación cruzada es 0,0165.\n",
    "\n",
    "- 🌳 Con un boosting, escalar da exactamente el mismo número hasta la cuarta\n",
    "cifra. Un árbol pregunta si una columna es mayor que un valor, y eso no cambia\n",
    "al multiplicarla por mil.\n",
    "\n",
    "- 💥 Pero con k vecinos el escalado vale 0,0748 de AUC y con SVM 0,0518. Esos\n",
    "miden distancias, y sin escalar la distancia entre dos clientes es casi\n",
    "enteramente la diferencia de monto.\n",
    "\n",
    "- 🕳️ Cuatro imputadores dan ocho diezmilésimas de diferencia, y el KNN, que\n",
    "es el caro, sale el último. La información del hueco ya está en la bandera.\n",
    "\n",
    "- 🔢 El ordinal cuesta 0,0091 contra el one-hot, porque se inventa un orden:\n",
    "le pone 0 a Abarrotes y 2 a Cuidado personal y el modelo lee que uno es el\n",
    "doble.\n",
    "\n",
    "- 💣 Sin `handle_unknown`, el día que abras en Cusco el modelo\n",
    "revienta. Con `ignore` no revienta y predice mal en silencio, así que\n",
    "hacen falta las dos cosas: ignore y una comprobación aparte.\n",
    "\n",
    "- 📉 Partir el monto en tramos PIERDE: 0,6997 sin tramos, 0,6734 con cuatro y\n",
    "0,6670 con diez. Los tramos se justifican con un hallazgo, no con una\n",
    "intuición.\n",
    "\n",
    "- 🧯 El MinMaxScaler con un monto mal tecleado aplasta las 2.250 filas\n",
    "restantes contra el cero. El RobustScaler ni se entera, y ese es el criterio de\n",
    "verdad para elegirlo.\n",
    "\n",
    "- 🚨 `ColumnTransformer` tira sin avisar toda columna que no le\n",
    "nombres, porque `remainder='drop'` es el valor por defecto. Se\n",
    "comprueba con `get_feature_names_out()`, que es una línea.\n",
    "\n",
    "- ⚖️ Dos de seis decisiones importan, y una importa en contra. Eso libera\n",
    "tiempo para las que sí mueven: construir columnas y elegir el umbral.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la diferencia cabe dentro del ruido, no hay ganador. Hay una moneda."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La desviación de la validación cruzada, y por qué una diferencia dentro de ella no significa nada, viene de la estadística: está en el [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya sabemos qué columnas entran y cómo. Falta que todo eso se ejecute\n",
    "**igual** en entrenamiento, en examen y en producción, sin copiar y\n",
    "pegar 📦\n",
    "\n",
    "El capítulo 11 junta las piezas de este capítulo en un solo\n",
    "objeto que se entrena de una vez, se guarda de una vez y no deja que la\n",
    "preparación se te escape a los datos de examen.\n",
    "\n",
    "Lo que verás allí:\n",
    "\n",
    "- 🔗 El `ColumnTransformer` y el `Pipeline` montados\n",
    "sobre las decisiones que acabamos de tomar aquí.\n",
    "\n",
    "- 🧪 Por qué la preparación tiene que rehacerse **dentro** de\n",
    "cada partición de la validación cruzada, y qué pasa si no.\n",
    "\n",
    "- 📦 Un solo objeto que se guarda con `joblib`, se carga en\n",
    "producción y trae el preprocesamiento dentro.\n",
    "\n",
    "- 🏷️ Y la lista de nombres que salen del otro lado, que es la que vas a\n",
    "necesitar para explicar el modelo.\n",
    "\n",
    "Y ahí sale el número que va a valer para todo el resto del libro: casi diez\n",
    "puntos de AUC por encima del listón, y ninguno de ellos vino del algoritmo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 10 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/decisiones-de-preprocessing/\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
}
