{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Construir columnas\n",
    "\n",
    "Cinco columnas nuevas: tres que valen puntos enteros, una que no sirve y una que es una trampa con el 30% de los datos.\n",
    "\n",
    "Cuaderno de práctica del capítulo 9 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/construir-columnas/\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: \"bWVzCjEgICAgIDAuNTc4MgoyICAgICAwLjYxNzYKMyAgICAgMC41NzEwCjQgICAgIDAuNTY5Nwo1ICAgICAwLjU0NzkKNiAgICAgMC41ODU2CjcgICAgIDAuNTY3MQo4ICAgICAwLjYwOTAKOSAgICAgMC41NTMxCjEwICAgIDAuNTYzNQoxMSAgICAwLjU5ODkKMTIgICAgMC41ODQ5Ck5hbWU6IGNvbXBybywgZHR5cGU6IGZsb2F0NjQ=\",\n",
    "    2: \"ICAgICAgICAgICAgICAgICAgY291bnQgICAgbWVhbgpzaW5fc2F0aXNmYWNjaW9uICAgICAgICAgICAgICAgCjAgICAgICAgICAgICAgICAgICAyNzY5ICAwLjU3NDIKMSAgICAgICAgICAgICAgICAgICAyMzEgIDAuNjE5MA==\",\n",
    "    3: \"ICAgICAgICAgICAgICAgICBjb3VudCAgICBtZWFuCmRlc2N1ZW50byAgICAgICAgICAgICAgICAgICAgIAooLTAuMDAxLCAwLjA2M10gICAgNjA4ICAwLjU3NTcKKDAuMDYzLCAwLjEyN10gICAgIDYwMSAgMC41NTc0CigwLjEyNywgMC4xODVdICAgICA2MDIgIDAuNjM2MgooMC4xODUsIDAuMjVdICAgICAgNTk0ICAwLjYzODA=\",\n",
    "    4: \"ICAgICAgICAgICAgICAgY291bnQgICAgbWVhbgp2aXNpdGFfbnVtZXJvICAgICAgICAgICAgICAgCjHCqiAgICAgICAgICAgICAgIDYxNyAgMC41ODUxCjLCqiBhIDPCqiAgICAgICAgIDExMjYgIDAuNTkwNgo0wqogYSA2wqogICAgICAgICAgOTg2ICAwLjU2NTkKN8KqIG8gbcOhcyAgICAgICAgIDI3MSAgMC41NDk4\",\n",
    "    5: \"SW50Q2FzdGluZ05hTkVycm9yOiBDYW5ub3QgY29udmVydCBub24tZmluaXRlIHZhbHVlcyAoTkEgb3IgaW5mKSB0byBpbnRlZ2VyLlJlcGxhY2Ugb3IgcmVtb3ZlIG5vbi1maW5pdGUgdmFsdWVzIG9yIGNhc3QgdG8gYW4gaW50ZWdlciB0eXBldGhhdCBzdXBwb3J0cyB0aGVzZSB2YWx1ZXMgKGUuZy4gJ0ludDY0Jyk=\",\n",
    "    6: \"c2luX2NvbXByYV9wcmV2aWEgICAgMC42MTQwIHZzIDAuNDEzNiAgIGRpZmVyZW5jaWE6IDAuMjAwNApzaW5fZGVzY3VlbnRvICAgICAgICAwLjYwMTcgdnMgMC40ODA3ICAgZGlmZXJlbmNpYTogMC4xMjEwCnNpbl9zYXRpc2ZhY2Npb24gICAgIDAuNTc0MiB2cyAwLjYxOTAgICBkaWZlcmVuY2lhOiAwLjA0NDg=\",\n",
    "    7: \"KDMwMDAsIDIwKQpbJ3Npbl9jb21wcmFfcHJldmlhJywgJ3Npbl9kZXNjdWVudG8nLCAnc2luX3NhdGlzZmFjY2lvbicsICdwcmVjaW9fdW5pdGFyaW8nLCAnZGlhc19yZWFsZXMnLCAndmlzaXRhX251bWVybydd\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí es donde de verdad se gana 🔧\n",
    "\n",
    "Antes de empezar, piensa en tu trabajo un segundo: **¿qué sabes tú de tu negocio que no esté escrito en ninguna columna?** Eso que sabes es exactamente lo que vamos a convertir en dato 💡\n",
    "\n",
    "Cambiar de modelo te da décimas. Construir una columna buena te da puntos\n",
    "enteros. Y en el capítulo 4 ya encontramos dos que valen oro sin haberlas\n",
    "escrito todavía: los huecos.\n",
    "\n",
    "Este capítulo va de eso: convertir lo que hay en cosas que un modelo pueda\n",
    "usar. En inglés se llama **feature engineering**, que es como lo\n",
    "vas a ver escrito en todas partes, y en español ingeniería de características.\n",
    "Yo le digo construir columnas porque es literalmente lo que se hace. Y también de lo contrario, porque vamos a construir cinco columnas y\n",
    "**solo tres van a servir**. Enseñar las que no sirven es la mitad\n",
    "del oficio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada capítulo de este libro arranca solo, así que empezamos por la función de\n",
    "limpieza del capítulo 4."
   ]
  },
  {
   "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",
    "    \"\"\"Lo del capítulo 4, en una función.\"\"\"\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",
    "ventas = carga_limpia(URL)\n",
    "print(ventas.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Columna 1: el hueco convertido en dato"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 4 vimos que sin fecha de última compra se cierra el 41,4% y con\n",
    "fecha el 61,4%. Esa señal está ahí y el modelo no la puede ver, porque un nulo\n",
    "para scikit-learn es un problema, no un dato.\n",
    "\n",
    "La solución es de una línea y es la técnica más rentable de todo el capítulo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['sin_compra_previa'] = ventas['fecha_ultima_compra'].isna().astype(int)\n",
    "ventas['sin_descuento'] = ventas['descuento'].isna().astype(int)\n",
    "\n",
    "print(ventas.groupby('sin_compra_previa')['compro'].agg(['count', 'mean']).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está, ahora sí en una columna de ceros y unos que cualquier modelo\n",
    "entiende. Veinte puntos de diferencia guardados 💎\n",
    "\n",
    "**La regla: cuando un hueco significa algo, se marca antes de\n",
    "rellenarlo.** Primero la bandera, después el relleno. Así el modelo tiene\n",
    "las dos cosas: un valor con el que trabajar y la información de que ese valor era\n",
    "inventado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Columna 2: el precio por unidad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ni el monto ni las unidades por separado decían gran cosa. Su cociente sí."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']\n",
    "print(ventas.groupby('compro')['precio_unitario'].median().round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mediana de S/39 en las que no se cerraron y S/62 en las que sí. Y en tramos se\n",
    "ve todavía mejor:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tramo = pd.qcut(ventas['precio_unitario'], 4)\n",
    "print(ventas.groupby(tramo, observed=True)['compro'].agg(['count', 'mean']).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 44% en el cuarto más barato a 69% en el más caro. **Veinticinco\n",
    "puntos, y esa columna no existía hace dos líneas** 🌟\n",
    "\n",
    "Esto se llama **ratio** y es de las cosas que más funcionan:\n",
    "dividir dos columnas que ya tienes. Piensa siempre en \"por unidad\", \"por día\",\n",
    "\"porcentaje del total\". Un modelo lineal no puede inventarse una división, así\n",
    "que si la necesita hay que dársela hecha."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Columna 3: el monto en tramos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tramo_monto = pd.qcut(ventas['monto'], 4)\n",
    "print(ventas.groupby(tramo_monto, observed=True)['compro'].agg(['count', 'mean']).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 39,6% a 70,9%, y en orden. Eso es señal de verdad y encima\n",
    "**monótona**: cuanto más grande el monto, más se cierra.\n",
    "\n",
    "Cuando la relación es así de ordenada, la columna original ya sirve tal cual y\n",
    "no hace falta partirla en tramos. Los tramos son útiles cuando la relación\n",
    "*no* es ordenada: por ejemplo si se cerrara mucho abajo y arriba y poco en\n",
    "el medio, ahí una columna numérica no lo puede expresar y los tramos sí 📊"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Columna 4: la que no sirve"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora una que suena obvia y no funciona."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['dia_semana'] = ventas['fecha'].dt.dayofweek\n",
    "print(ventas.groupby('dia_semana')['compro'].agg(['count', 'mean']).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todos entre 0,559 y 0,596. **Cuatro puntos entre el mejor y el peor\n",
    "día**, con unas 420 filas cada uno. Eso es ruido.\n",
    "\n",
    "Y aquí va algo importante: **esto también es un resultado**. Si\n",
    "alguien en la reunión dice \"los martes se vende peor\", ya tienes la respuesta\n",
    "medida en vez de una opinión.\n",
    "\n",
    "La columna se puede dejar (un buen modelo la ignora sola) o quitar. Yo la\n",
    "quito, porque cada columna inútil es una oportunidad más de que el modelo\n",
    "memorice ruido 🧹"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Columna 5: la trampa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta parece la mejor idea del capítulo. Tenemos la fecha de la venta y la de\n",
    "la última compra, así que la antigüedad del cliente sale sola:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['dias_desde_ultima'] = (ventas['fecha'] - ventas['fecha_ultima_compra']).dt.days\n",
    "print(ventas['dias_desde_ultima'].describe().round(1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira el mínimo: **-385** 😳"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "negativos = ventas['dias_desde_ultima'] < 0\n",
    "print('cuántos:', int(negativos.sum()), f'({negativos.mean():.1%})')\n",
    "print(ventas.loc[negativos, ['fecha', 'fecha_ultima_compra', 'dias_desde_ultima']].head(3).to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "904 filas, el 30% del archivo, donde **la \"última compra\" es posterior\n",
    "a la venta**. O sea que esa columna no dice \"la última compra antes de\n",
    "esta\": dice la última compra del cliente *en todo el archivo*, futuro\n",
    "incluido.\n",
    "\n",
    "Un modelo entrenado con eso aprende de compras que en el momento de predecir\n",
    "todavía no habían pasado. Es la misma fuga del capítulo 12, y esta versión es\n",
    "mucho peor que la del `monto_final_facturado` porque no salta a la\n",
    "vista: la columna tiene un nombre inocente y valores razonables 🚨"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La versión bien hecha, y lo que pasa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se arregla calculando la fecha anterior de cada cliente ordenando por\n",
    "fecha:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas = ventas.sort_values(['cliente_id', 'fecha'])\n",
    "fecha_anterior = ventas.groupby('cliente_id')['fecha'].shift(1)\n",
    "ventas['dias_reales'] = (ventas['fecha'] - fecha_anterior).dt.days\n",
    "\n",
    "print('negativos ahora:', int((ventas['dias_reales'] < 0).sum()))\n",
    "print('nulos (primera venta del cliente):', int(ventas['dias_reales'].isna().sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cero negativos, y 617 nulos que son exactamente las primeras ventas de cada\n",
    "uno de los 617 clientes. Eso ya es correcto: solo mira hacia atrás.\n",
    "\n",
    "¿Y sirve?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tramo_dias = pd.cut(ventas['dias_reales'], [-1, 30, 90, 180, 10000],\n",
    "                    labels=['0-30', '31-90', '91-180', '+180'])\n",
    "print(ventas.groupby(tramo_dias, observed=True)['compro'].agg(['count', 'mean']).round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todos alrededor de 0,57. **No sirve de nada** 😐\n",
    "\n",
    "Y esa es la parte honesta del capítulo. Hicimos el trabajo bien, arreglamos la\n",
    "fuga, y la columna correcta no tiene señal. Pasa muchísimo y hay que contarlo,\n",
    "porque si solo te enseñan los casos donde todo sale bonito, el día que te salga\n",
    "esto vas a pensar que lo hiciste mal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tres columnas más que probé, y las tres son inútiles"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta sección iba a ser \"tres ideas más de columnas\". La escribo distinta\n",
    "porque las medí y no funcionó ninguna, y eso enseña más 🤷‍♀️\n",
    "\n",
    "Las tres son las que sugiere cualquier tutorial: desmontar la fecha, marcar el\n",
    "fin de semana, y comparar cada venta con lo normal de ese cliente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "v = ventas.drop_duplicates(subset='id_venta').sort_values(['cliente_id', 'fecha']).copy()\n",
    "v['monto'] = pd.to_numeric(v['monto'].astype(str).str.replace(',', '.'), errors='coerce')\n",
    "v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "v['precio_unitario'] = v['monto'] / v['unidades']\n",
    "\n",
    "v['dia_semana'] = pd.to_datetime(v['fecha'], format='mixed').dt.dayofweek\n",
    "v['fin_de_semana'] = (v['dia_semana'] >= 5).astype(int)\n",
    "v['ticket_vs_cliente'] = v['monto'] / v.groupby('cliente_id')['monto'].transform('median')\n",
    "\n",
    "print(v.groupby('dia_semana')['compro'].agg(['count', 'mean']).round(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la columna de la derecha antes de seguir: de 0,559 a 0,596 🫥\n",
    "\n",
    "Los siete días de la semana compran prácticamente igual. No hay un lunes\n",
    "flojo ni un viernes fuerte. Y eso, que parece un no-resultado, ya te dice que\n",
    "esa columna no va a servir para nada.\n",
    "\n",
    "Aun así la mido, porque mirar la tabla no es lo mismo que medir el efecto en\n",
    "el modelo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "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",
    "BASE = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "        'precio_unitario', 'sin_compra_previa']\n",
    "CAT = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "\n",
    "def arma2(num, cat):\n",
    "    return Pipeline([\n",
    "        ('p', ColumnTransformer([\n",
    "            ('n', Pipeline([('i', SimpleImputer(strategy='median')),\n",
    "                            ('s', StandardScaler())]), num),\n",
    "            ('c', Pipeline([('i', SimpleImputer(strategy='most_frequent')),\n",
    "                            ('o', OneHotEncoder(handle_unknown='ignore'))]), cat),\n",
    "        ])),\n",
    "        ('m', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "    ])\n",
    "\n",
    "\n",
    "def prueba(num, etiqueta):\n",
    "    X = v[num + CAT]\n",
    "    Xtr, Xte, ytr, yte = train_test_split(X, v['compro'], test_size=0.25,\n",
    "                                          random_state=42, stratify=v['compro'])\n",
    "    m = arma2(num, CAT).fit(Xtr, ytr)\n",
    "    auc = roc_auc_score(yte, m.predict_proba(Xte)[:, 1])\n",
    "    print(f'{etiqueta:<34} {auc:.4f}')\n",
    "    return auc\n",
    "\n",
    "\n",
    "base = prueba(BASE, 'solo las de siempre')\n",
    "prueba(BASE + ['dia_semana'], '+ día de la semana')\n",
    "prueba(BASE + ['fin_de_semana'], '+ fin de semana')\n",
    "ratio = prueba(BASE + ['ticket_vs_cliente'], '+ ticket contra su propia mediana')\n",
    "\n",
    "print()\n",
    "print('lo que aporta el ratio:', round(ratio - base, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,7218 sin ellas. Y con ellas: 0,7215, 0,7212 y 0,7219 😐\n",
    "\n",
    "La tercera es la que más ilusión me hacía. \"Esta venta es el doble de lo que\n",
    "suele comprar este cliente\" suena a señal de verdad, y aporta\n",
    "**una diezmilésima**, que es lo mismo que decir nada.\n",
    "\n",
    "Y aquí está la lección incómoda de este capítulo, que va contra lo que\n",
    "promete casi todo lo que vas a leer sobre construir columnas 💊\n",
    "\n",
    "### Lo que de verdad pasa cuando construyes columnas\n",
    "\n",
    "**La mayoría no aporta.** En este capítulo llevamos ocho\n",
    "columnas construidas y solo dos movieron el número. Esa proporción no es mala\n",
    "suerte: es lo normal, y quien te cuente lo contrario te está enseñando el caso\n",
    "que le salió bien.\n",
    "\n",
    "**Una columna que no aporta no es gratis.** Ocupa sitio, hay que\n",
    "calcularla también en producción, y añade un motivo más de que algo se rompa el\n",
    "día que cambie el archivo. Si no gana nada, se quita.\n",
    "\n",
    "**La forma de saberlo es medir, y medir así.** Con el mismo\n",
    "reparto de entrenamiento y prueba, la misma semilla y el mismo modelo, cambiando\n",
    "solo la columna. Cualquier otra cosa compara dos experimentos distintos.\n",
    "\n",
    "Y una advertencia sobre estos números tan parecidos: una diferencia de tres\n",
    "diezmilésimas **no es una diferencia**. Con otro\n",
    "`random_state` se dan la vuelta. Para decidir de verdad entre dos\n",
    "columnas que se parecen tanto hace falta validación cruzada, que es el capítulo\n",
    "16 🎲\n",
    "\n",
    "### Entonces, ¿por qué probarlas?\n",
    "\n",
    "Porque probar es barato y no probar sale caro 💡\n",
    "\n",
    "Las tres de arriba costaron seis líneas y cuatro minutos. La alternativa es\n",
    "decidir por intuición cuál va a funcionar, y la intuición aquí acierta poco: yo\n",
    "habría apostado por el ratio y me habría equivocado.\n",
    "\n",
    "Lo caro no es probar diez columnas y tirar ocho. Lo caro es quedarte con las\n",
    "diez porque nunca mediste cuáles servían, y arrastrarlas a producción para\n",
    "siempre 🎒"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El detalle que quedó abierto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en algo raro, porque es una pregunta de verdad y no la voy a tapar.\n",
    "\n",
    "La bandera `sin_compra_previa` (que la fecha venga vacía) vale\n",
    "veinte puntos. Pero \"es la primera venta de este cliente en el archivo\" no vale\n",
    "nada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "primera = ventas['dias_reales'].isna()\n",
    "print('primera venta del cliente:', round(ventas.loc[primera, 'compro'].mean(), 4))\n",
    "print('el resto:                 ', round(ventas.loc[~primera, 'compro'].mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,5851 contra 0,5757. Nada.\n",
    "\n",
    "O sea que **el hueco en `fecha_ultima_compra` no significa\n",
    "\"cliente nuevo\"**, significa otra cosa que este archivo no me deja\n",
    "averiguar. A lo mejor es \"el sistema viejo no lo tenía\", a lo mejor es \"no se\n",
    "pudo verificar\".\n",
    "\n",
    "Eso no se resuelve con código: se resuelve con la pregunta del capítulo 2, la\n",
    "que va en el contrato de columnas. *¿Qué significa un hueco aquí?*\n",
    "\n",
    "Y mientras no haya respuesta, la bandera se usa (funciona) pero se reporta con\n",
    "la advertencia puesta. Un modelo que se apoya en algo que nadie sabe explicar es\n",
    "un modelo frágil 🤔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El mes del año\n",
    "\n",
    "¿Hay estacionalidad en la tasa de cierre?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La bandera de satisfacción vacía\n",
    "\n",
    "Construye la tercera bandera de hueco y mira si aporta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El descuento, cuando existe\n",
    "\n",
    "Entre las ventas que sí tuvieron descuento, ¿más descuento\n",
    "significa más cierre?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Cuántas veces ha aparecido este cliente\n",
    "\n",
    "Un contador acumulado por cliente, mirando solo hacia\n",
    "atrás."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El error de construir sobre nulos\n",
    "\n",
    "Intenta convertir a entero la columna de días, que tiene\n",
    "huecos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Todas las columnas nuevas, medidas de una vez\n",
    "\n",
    "Una tabla con cuánto separa cada columna nueva, para decidir\n",
    "cuáles se quedan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La función de preparación completa\n",
    "\n",
    "Junta la limpieza del capítulo 4 y las columnas de este, en\n",
    "algo reutilizable."
   ]
  },
  {
   "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",
    "Creas una columna nueva y el AUC sube de 0,69 a 0,95. ¿Qué es lo primero que piensas?\n",
    "\n",
    "a) Que probablemente esa columna no existe cuando hay que predecir\n",
    "\n",
    "b) Que encontré la variable clave\n",
    "\n",
    "c) Que hay que crear más columnas parecidas\n",
    "\n",
    "d) Que el modelo por fin está bien configurado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y la columna que parece la mejor de todas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "La columna más tentadora que se puede construir, y la que más veces he visto en cuadernos ajenos: la tasa de compra histórica de cada cliente. Tiene todo el sentido del mundo y sube el AUC a 0,7666, por encima del mejor modelo del libro.\n",
    "\n",
    "```\n",
    "ventas['cliente_tasa'] = (\n",
    "    ventas.groupby('cliente_id')['compro']\n",
    "    .transform('mean'))\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": [
    "- 🚩 El hueco se marca antes de rellenarlo. Aquí, veinte puntos, doce puntos y\n",
    "cuatro puntos y medio en tres líneas.\n",
    "\n",
    "- ➗ Los ratios funcionan: `monto / unidades` separa veinticinco\n",
    "puntos y no existía.\n",
    "\n",
    "- 📊 Los tramos sirven cuando la relación no es ordenada. Si es monótona, la\n",
    "columna numérica ya vale.\n",
    "\n",
    "- 😐 El día de la semana no aporta nada, y eso también se reporta.\n",
    "\n",
    "- 🚨 Cuidado con las columnas de tiempo: el 30% de `fecha_ultima_compra`\n",
    "es posterior a la venta. Se arregla con `groupby().shift(1)` sobre\n",
    "datos ordenados.\n",
    "\n",
    "- 🤷‍♀️ Hacerlo bien a veces mata la señal. La versión correcta de los días no\n",
    "aporta nada, y contarlo es parte del trabajo.\n",
    "\n",
    "- 📏 Cada columna nueva se mide contra el objetivo antes de quedarse. \"Tiene\n",
    "sentido\" no es un argumento.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La columna que mejor se ve suele ser la que te está copiando la respuesta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Muchas de estas columnas se pueden construir antes, al traer los datos. Si te toca hacerlo en la consulta, está en el [libro de SQL desde cero](https://missyera.com/guias/sql-desde-cero/) 🗄️\n",
    "\n",
    "En el capítulo 11 metemos todo esto dentro de un pipeline, que es lo que impide\n",
    "que la preparación se te escape a los datos de examen.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 9 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/construir-columnas/\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
}
