{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El pipeline\n",
    "\n",
    "ColumnTransformer, imputar, codificar, y diez puntos por encima del listón que casi no vienen del modelo.\n",
    "\n",
    "Cuaderno de práctica del capítulo 11 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/el-pipeline/\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: \"MjgKWydjYXRfX2NpdWRhZF9hcmVxdWlwYScgJ2NhdF9fY2l1ZGFkX2NoaWNsYXlvJyAnY2F0X19jaXVkYWRfY3VzY28nCiAnY2F0X19jaXVkYWRfbGltYScgJ2NhdF9fY2l1ZGFkX3BpdXJhJyAnY2F0X19jaXVkYWRfdHJ1amlsbG8nCiAnY2F0X19zZWdtZW50b19Cb2RlZ2EnICdjYXRfX3NlZ21lbnRvX0hvcmVjYScgJ2NhdF9fc2VnbWVudG9fTWF5b3Jpc3RhJwogJ2NhdF9fc2VnbWVudG9fTWluaW1hcmtldCdd\",\n",
    "    2: \"c29sbyBudW3DqXJpY2FzOiAoMC42NiwgMC42OTMpCmNvbiB0b2RvICAgICAgOiAoMC42NzMzLCAwLjcyMTQp\",\n",
    "    3: \"c2luIGJhbmRlcmFzOiAoMC42NiwgMC43MTAxKQpjb24gYmFuZGVyYXM6ICgwLjY3MzMsIDAuNzIxNCk=\",\n",
    "    4: \"W1swLjM0MzIgMC42NTY4XV0=\",\n",
    "    5: \"aWd1YWw6IFRydWU=\",\n",
    "    6: \"VmFsdWVFcnJvcjogY29sdW1ucyBhcmUgbWlzc2luZzogeydjYW5hbCd9\",\n",
    "    7: \"cHJvYmFiaWxpZGFkIGRlIHF1ZSBjaWVycmU6IDAuODQ5OQ==\",\n",
    "    8: \"cHJvYmFiaWxpZGFkIGRlIGNvbXByYTogMC42NzQ3CnByZWRpY2Npw7NuICAgICAgICAgICAgOiAx\",\n",
    "    9: \"Y29uIGxhIGNpdWRhZCBpbnZlbnRhZGE6IDAuNjU2OApjb24gbGEgZGUgdmVyZGFkICAgICAgIDogMC42NzQ3\",\n",
    "    10: \"VmFsdWVFcnJvcjogY29sdW1ucyBhcmUgbWlzc2luZzogeydjaXVkYWQnfQ==\",\n",
    "    11: \"cHJlZGljZSBpZ3VhbCBjb24gdHJlcyBodWVjb3M6IDAuNjg3CnkgbGFzIG1lZGlhbmFzIHF1ZSB0aWVuZSBkZW50cm86ClsxLjIwMDBlKzAxIDUuMjQ2MmUrMDIgMS4yMDAwZS0wMSAzLjAwMDBlKzAwIDUuMTIzMGUrMDEgMC4wMDAwZSswMAogMC4wMDAwZSswMCAwLjAwMDBlKzAwIDMuMDAwMGUrMDBd\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llevamos cuatro capítulos preparando datos y todavía no hemos entrenado un\n",
    "modelo de verdad. Hoy sí, y además con el número que va a valer para todo el\n",
    "resto del libro 🎯\n",
    "\n",
    "Antes, una pregunta: **¿cuántas veces has tenido que rehacer un análisis porque no te acordabas del orden exacto en que tocaste los datos?** El pipeline existe para que esa pregunta deje de tener sentido 📦\n",
    "\n",
    "Pero antes hay que resolver un problema práctico: todo lo del capítulo 9 hay\n",
    "que hacerlo **igual** en los datos de entrenamiento, en los de\n",
    "examen y en producción. Y si lo haces a mano tres veces, en la tercera te\n",
    "equivocas. Siempre.\n",
    "\n",
    "Para eso está el `Pipeline`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\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',\n",
    "                                  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",
    "    anterior = v.groupby('cliente_id')['fecha'].shift(1)\n",
    "    v['dias_reales'] = (v['fecha'] - anterior).dt.days\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "print(datos.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los dos errores que te va a dar scikit-learn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes del pipeline, vamos a chocarnos con las dos paredes, porque el pipeline\n",
    "existe justo para no chocarse con ellas."
   ]
  },
  {
   "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",
    "    from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "    LogisticRegression().fit(datos[['unidades', 'satisfaccion']], datos['compro'])\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: Input X contains NaN.\n",
    "LogisticRegression does not accept missing values encoded as NaN natively. For supervised learning, you might want to consider sklearn.ensemble.HistGradientBoostingClassifier and Regressor which accept missing values encoded as NaNs natively. Alternatively, it is possible to preprocess the data, for instance by using an imputer transformer in a pipeline or drop samples with missing values. See https://scikit-learn.org/stable/modules/impute.html You can find a list of all estimators that handle NaN values at the following page: https://scikit-learn.org/stable/modules/impute.html#estimators-that-handle-nan-values\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Input X contains NaN.* Pared número uno: **los modelos no\n",
    "aceptan huecos**. Hay que rellenarlos, y eso se llama imputar."
   ]
  },
  {
   "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",
    "    from sklearn.preprocessing import OneHotEncoder\n",
    "\n",
    "    oh = OneHotEncoder(handle_unknown='error')\n",
    "    oh.fit(datos[['ciudad']].head(100))\n",
    "    oh.transform(pd.DataFrame({'ciudad': ['huancayo']}))\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 ['huancayo'] in column 0 during transform\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Found unknown categories ['huancayo']*. Pared número dos: si en\n",
    "producción aparece una ciudad que no estaba al entrenar, revienta.\n",
    "\n",
    "Ese error es **bueno** mientras desarrollas y\n",
    "**malísimo** a las tres de la mañana en producción. Por eso se pone\n",
    "`handle_unknown='ignore'`: la categoría nueva se codifica como todo\n",
    "ceros y el modelo sigue funcionando 🌙"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las columnas no se tratan todas igual"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una columna numérica se rellena con la mediana y se escala. Una de texto se\n",
    "rellena con la más frecuente y se convierte en ceros y unos. Son dos recetas\n",
    "distintas para el mismo `DataFrame`, y eso lo resuelve\n",
    "`ColumnTransformer`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion',\n",
    "             'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "preparacion = ColumnTransformer([\n",
    "    ('num', Pipeline([\n",
    "        ('rellenar', SimpleImputer(strategy='median')),\n",
    "        ('escalar', StandardScaler()),\n",
    "    ]), NUMERICAS),\n",
    "    ('cat', Pipeline([\n",
    "        ('rellenar', SimpleImputer(strategy='most_frequent')),\n",
    "        ('codificar', OneHotEncoder(handle_unknown='ignore')),\n",
    "    ]), CATEGORICAS),\n",
    "])\n",
    "print(len(NUMERICAS), 'numéricas y', len(CATEGORICAS), 'categóricas')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y fíjate en lo que **no** está en esas listas:\n",
    "`monto_final_facturado` y `dias_desde_ultima`. Las dos\n",
    "miran al futuro, y por eso las decisiones de los capítulos 2 y 9 terminan siendo dos listas de nombres 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El pipeline completo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "\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",
    "\n",
    "modelo = Pipeline([\n",
    "    ('preparacion', preparacion),\n",
    "    ('clasificador', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "])\n",
    "\n",
    "modelo.fit(X_tr, y_tr)\n",
    "print('exactitud:', round(modelo.score(X_te, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**0,6733** 🎉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los porqués de estas líneas\n",
    "\n",
    "Este bloque tiene cuatro decisiones y las cuatro se defienden solas en cuanto sabes qué pasa si las quitas:\n",
    "\n",
    "- ✂️ **Se parte ANTES de preparar nada.** Es el orden y no un detalle. Si preparas primero, la media y las categorías con las que preparas llevan dentro las filas del examen, y el examen deja de serlo.\n",
    "\n",
    "- ⚖️ **`stratify=y`.** El trozo escondido mantiene el mismo porcentaje de ventas cerradas que el original. Sin esto, el azar puede darte un examen que no se parece al problema.\n",
    "\n",
    "- 📦 **Un solo objeto `Pipeline`.** La preparación y el modelo viajan juntos, así que es imposible aplicar una transformación al entrenamiento y olvidarla en producción. Ese olvido es de los errores más caros y no da ningún error.\n",
    "\n",
    "- 🎲 **`random_state=42` en los dos sitios.** En el reparto y en el modelo. Sin él, cada corrida devuelve un número distinto y no puedes comparar nada con nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora el número que le da sentido:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.dummy import DummyClassifier\n",
    "\n",
    "tonto = DummyClassifier(strategy='most_frequent').fit(X_tr, y_tr)\n",
    "print('el listón:', round(tonto.score(X_te, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,5773 el listón, 0,6733 el modelo. **Casi diez puntos de\n",
    "ganancia**, contra el punto escaso del capítulo 1.\n",
    "\n",
    "Y esos diez puntos no vinieron del modelo: la regresión logística es de lo más\n",
    "simple que hay. Vinieron de los capítulos 4 y 9, de limpiar y de construir columnas 🧹"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué hace por dentro, para que no sea magia"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(modelo.named_steps['preparacion'].transform(X_tr).shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2.250 filas y **28 columnas**, cuando le dimos 13. Las nueve\n",
    "numéricas siguen siendo nueve, y las cuatro de texto se convirtieron en\n",
    "diecinueve: seis ciudades, cuatro segmentos, cuatro canales y cinco categorías.\n",
    "\n",
    "Eso es lo que hace `OneHotEncoder`: una columna de sí/no por cada\n",
    "valor posible. Por eso hay que tener cuidado con las columnas que tienen\n",
    "muchísimos valores distintos, como un `cliente_id` con 617: te crea\n",
    "617 columnas y el modelo se pone a memorizar clientes 🫠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué el pipeline y no hacerlo a mano"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Voy a ser honesta con algo, porque en muchos sitios te lo cuentan\n",
    "exagerado.\n",
    "\n",
    "Si rellenas los huecos con la mediana de *todo* el archivo antes de\n",
    "partir en train y test, técnicamente estás filtrando información del examen al\n",
    "entrenamiento. En este caso concreto el resultado sale **exactamente\n",
    "igual**, porque la mediana de 3.000 filas y la de 2.250 se parecen\n",
    "muchísimo.\n",
    "\n",
    "Así que el argumento de verdad para usar pipeline no es ese susto. Son estos\n",
    "tres:\n",
    "\n",
    "- 🔁 **No repetirte.** Las mismas transformaciones se aplican\n",
    "solas a train, a test y a la fila que llegue mañana. Sin copiar y pegar, que es\n",
    "donde se cuelan los bugs.\n",
    "\n",
    "- ✅ **Validación cruzada honesta.** En el capítulo 16 vamos a\n",
    "partir los datos cinco veces, y la preparación tiene que rehacerse dentro de cada\n",
    "partición. Sin pipeline eso es imposible de escribir bien.\n",
    "\n",
    "- 📦 **Un solo objeto.** El modelo entrenado incluye la limpieza.\n",
    "Se guarda en un archivo y quien lo use no tiene que saber qué hiciste.\n",
    "\n",
    "Ese último punto es el que más vale en la empresa: **lo que se despliega\n",
    "no es \"el modelo\", es el pipeline entero**. Si mandas solo el\n",
    "clasificador, quien lo reciba tiene que reproducir tus trece transformaciones de\n",
    "memoria, y no va a poder 📦"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La columna que se cae sin que nadie avise"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este es el fallo del `ColumnTransformer` y no da error, no da\n",
    "aviso, y el modelo entrena tan contento 😖\n",
    "\n",
    "Imagina que armas la preparación con prisa y en las numéricas pones solo las\n",
    "cinco primeras, olvidándote de las otras cuatro:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "olvidadiza = ColumnTransformer([\n",
    "    ('num', StandardScaler(), NUMERICAS[:5]),\n",
    "    ('cat', OneHotEncoder(handle_unknown='ignore'), CATEGORICAS),\n",
    "])\n",
    "\n",
    "print('columnas que le doy   :', len(X_tr.columns))\n",
    "print('columnas que declaré  :', len(NUMERICAS[:5] + CATEGORICAS))\n",
    "print('columnas que salen    :', olvidadiza.fit_transform(X_tr.fillna(0)).shape[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le di trece columnas, declaré nueve, y salieron 24 en vez de 28 🕳️\n",
    "\n",
    "Las cuatro que no nombré **no se quedaron sin transformar: se\n",
    "tiraron**. Ese es el valor por defecto de `remainder`, que es\n",
    "`'drop'`. Lo que no aparece en ninguna lista, desaparece.\n",
    "\n",
    "Y ahí está lo peligroso: el modelo entrena, da un número, y ese número es\n",
    "peor de lo que podría ser sin que nada te diga por qué. Si la columna que se\n",
    "cayó era la buena, acabas de tirar tu mejor señal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "conserva = ColumnTransformer([\n",
    "    ('num', StandardScaler(), NUMERICAS[:5]),\n",
    "    ('cat', OneHotEncoder(handle_unknown='ignore'), CATEGORICAS),\n",
    "], remainder='passthrough')\n",
    "\n",
    "print('con remainder passthrough:', conserva.fit_transform(X_tr.fillna(0)).shape[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "28, las de siempre. Con `passthrough` las que no nombraste pasan\n",
    "tal cual, sin escalar ni nada 🚪\n",
    "\n",
    "¿Y cuál conviene? Yo dejo el `drop` por defecto **a\n",
    "propósito**, pero comprobando. El `drop` es más seguro a largo\n",
    "plazo: el día que alguien añada una columna nueva al CSV, con\n",
    "`passthrough` se te cuela en el modelo sin que la mires, y con\n",
    "`drop` se queda fuera hasta que decidas.\n",
    "\n",
    "Lo que no puede faltar es la comprobación, y cabe en una línea:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "declaradas = set(NUMERICAS) | set(CATEGORICAS)\n",
    "sobran = set(X_tr.columns) - declaradas\n",
    "\n",
    "print('columnas sin declarar:', sorted(sobran) or 'ninguna')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa línea al lado del `ColumnTransformer` convierte un fallo\n",
    "silencioso en uno que se ve 👀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cómo se llaman las columnas al salir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El pipeline te entrega una matriz de números sin nombres, y eso es un\n",
    "problema en cuanto quieras explicar algo. Menos mal que se pueden pedir:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nombres = modelo.named_steps['preparacion'].get_feature_names_out()\n",
    "\n",
    "print('cuántas salen:', len(nombres))\n",
    "print('las primeras :', list(nombres[:4]))\n",
    "print('las últimas  :', list(nombres[-4:]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí se ve de dónde salen las 28 columnas 🏷️\n",
    "\n",
    "El prefijo dice de qué bloque viene cada una: `num__` de las\n",
    "numéricas, `cat__` de las categóricas. Y fíjate en las últimas: el\n",
    "`OneHotEncoder` convirtió `categoria` en una columna por\n",
    "valor, con el valor pegado al nombre.\n",
    "\n",
    "Trece columnas entraron y salieron 28 porque las cuatro categóricas se\n",
    "abrieron en una columna por categoría. Ese es todo el misterio de por qué el\n",
    "número cambia.\n",
    "\n",
    "Sin esta lista, el capítulo 20 sería imposible: los\n",
    "coeficientes salen en el mismo orden que estos nombres, y sin ellos tendrías 28\n",
    "números sin saber a qué corresponde cada uno 🔢"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El pipeline es el artefacto, no el modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí está la razón de fondo por la que todo esto va junto y no separado 📦"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import joblib\n",
    "import os\n",
    "import tempfile\n",
    "\n",
    "ruta = os.path.join(tempfile.mkdtemp(), 'modelo.joblib')\n",
    "joblib.dump(modelo, ruta)\n",
    "\n",
    "print('pesa', round(os.path.getsize(ruta) / 1024, 1), 'KB')\n",
    "\n",
    "otro = joblib.load(ruta)\n",
    "\n",
    "print('mismas predicciones:', (otro.predict(X_te) == modelo.predict(X_te)).all())\n",
    "print('misma exactitud    :', round(otro.score(X_te, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cinco kilobytes y medio, y al cargarlo predice exactamente igual 💾\n",
    "\n",
    "Y lo que hay dentro de ese archivo no es solo el clasificador. Están\n",
    "**las medianas** con las que rellenar los huecos, **las\n",
    "medias y desviaciones** con las que escalar, y **la lista de\n",
    "categorías** que vio cada columna. Todo lo que aprendió durante el\n",
    "entrenamiento y que hace falta para tratar una fila nueva igual que a las de\n",
    "entrenamiento.\n",
    "\n",
    "Si hubieras guardado solo el modelo, mañana tendrías que acordarte de con qué\n",
    "mediana rellenaste, en qué orden iban las categorías y si escalaste antes o\n",
    "después. Y no te vas a acordar 🙃\n",
    "\n",
    "Por eso el archivo que se manda a producción es el pipeline entero. Lo que\n",
    "recibe es una fila igual a la del CSV original, y lo que devuelve es una\n",
    "predicción. Nada en medio que alguien tenga que recordar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Los nombres de las columnas que salen\n",
    "\n",
    "Qué nombres tienen las 28 columnas que produce la\n",
    "preparación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Sin las columnas de texto\n",
    "\n",
    "Cuánto se pierde si tiras las cuatro categóricas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Sin las banderas de hueco\n",
    "\n",
    "Y cuánto valen las tres columnas del capítulo 9."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La ciudad nueva, ahora sin reventar\n",
    "\n",
    "Comprueba que el pipeline aguanta una ciudad que nunca\n",
    "vio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Guardar el pipeline entero\n",
    "\n",
    "Guarda el modelo con su limpieza y vuélvelo a cargar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de olvidarse una columna\n",
    "\n",
    "Pásale al modelo un DataFrame al que le falta una\n",
    "columna."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Una fila nueva, de principio a fin\n",
    "\n",
    "Simula una oportunidad que llega hoy y pídele al modelo su\n",
    "probabilidad."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Una fila nueva, de principio a fin\n",
    "\n",
    "Pásale al pipeline una sola fila y mira qué devuelve."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 8\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. Una ciudad que el modelo nunca vio\n",
    "\n",
    "Cámbiale la ciudad a una que no está en los datos de\n",
    "entrenamiento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 9\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10. ¿Y si falta una columna?\n",
    "\n",
    "Quítale una columna a la fila y mira el mensaje."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 10\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 11. Lo que el pipeline se guardó para siempre\n",
    "\n",
    "Pásale una fila con tres huecos y busca con qué los\n",
    "rellenó."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 11\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "¿Por qué el escalado tiene que ir dentro del Pipeline y no antes?\n",
    "\n",
    "a) Porque si se calcula sobre todos los datos, el test se cuela en el entrenamiento\n",
    "\n",
    "b) Porque queda más ordenado\n",
    "\n",
    "c) Porque el Pipeline es más rápido\n",
    "\n",
    "d) Porque scikit-learn lo exige"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo razonable que sale caro"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Quieres mirar cómo quedan las columnas escaladas antes de meterlas al pipeline, que es lo razonable. Y ya que está hecho, se aprovecha.\n",
    "\n",
    "```\n",
    "esc = StandardScaler().fit(X)\n",
    "X_esc = esc.transform(X)\n",
    "\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(\n",
    "    X_esc, y, test_size=0.25)\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": [
    "- 🚫 Los modelos no comen nulos ni texto. Imputar y codificar no es opcional.\n",
    "\n",
    "- 🧩 `ColumnTransformer` aplica una receta a las numéricas y otra a\n",
    "las de texto, en el mismo DataFrame.\n",
    "\n",
    "- 🌙 `handle_unknown='ignore'` para que una categoría nueva no tumbe\n",
    "producción.\n",
    "\n",
    "- 📈 Primer modelo de verdad: 0,6733 contra un listón de 0,5773. Diez puntos, y\n",
    "casi todos vienen de limpiar y construir columnas, no del modelo.\n",
    "\n",
    "- 🔢 Las 13 columnas se convierten en 28 al codificar. Ojo con las categóricas\n",
    "de muchos valores.\n",
    "\n",
    "- 🔁 El pipeline no es solo por la fuga: es por no repetirte, por poder validar\n",
    "bien y por poder guardar un solo objeto.\n",
    "\n",
    "- 📦 Lo que se despliega es el pipeline entero, nunca el clasificador\n",
    "solo.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo que se guarda no es el modelo. Es todo lo que hubo que hacerle a los datos para llegar hasta él."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si las clases y los objetos de scikit-learn te resultan raros, el [libro de Python desde cero](https://missyera.com/guias/python-desde-cero/) los explica antes de que hagan falta 🐍\n",
    "\n",
    "En el capítulo 14 nos ponemos serias con el listón: por qué el modelo tonto es\n",
    "obligatorio, cuál elegir, y el caso donde el 95% de exactitud es una vergüenza.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 11 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/el-pipeline/\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
}
