{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Auditoría de tipos\n",
    "\n",
    "Las ocho categorías en que cae cualquier columna, con el detector que las clasifica solo y las dos que este archivo tenía disfrazadas.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 5 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/auditoria-de-tipos/\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": [
    "Ya está limpio. Ahora la pregunta que casi nadie hace antes de modelar:\n",
    "**¿qué es realmente cada columna?** 🕵️\n",
    "\n",
    "Y no vale mirar `dtypes`, porque pandas te dice cómo está\n",
    "*guardada* la columna, no qué *es*. Un `int64` puede\n",
    "ser un número, un identificador, una categoría o una fecha mal leída, y los\n",
    "cuatro casos se tratan distinto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las ocho categorías"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Qué es | Cómo se reconoce | Qué se hace con ella |\n",
    "|---|---|---|\n",
    "| **Identificador** | Tantos valores distintos como filas | Fuera del modelo |\n",
    "| **Numérica continua** | Muchos decimales distintos | Escalar |\n",
    "| **Numérica discreta** | Enteros, decenas de valores | Escalar, o tramos |\n",
    "| **Categórica nominal** | Pocos niveles, sin orden | One-hot |\n",
    "| **Categórica ordinal** | Pocos niveles, con orden | Codificar respetando el orden |\n",
    "| **Alta cardinalidad** | Cientos de niveles | Agrupar, o codificar por target |\n",
    "| **Fecha** | Tipo datetime | Sacarle columnas, nunca usarla cruda |\n",
    "| **Constante** | Un valor domina casi todo | Fuera: no distingue nada |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en que ninguna de las ocho se decide mirando `dtypes` 😌"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El detector"
   ]
  },
  {
   "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",
    "\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",
    "\n",
    "v = carga_limpia(URL)\n",
    "\n",
    "\n",
    "def audita(df, ordinales=()):\n",
    "    \"\"\"Clasifica cada columna en una de las ocho categorias.\"\"\"\n",
    "    filas = []\n",
    "    for col in df.columns:\n",
    "        s = df[col]\n",
    "        distintos = s.nunique()\n",
    "        ratio = distintos / len(df)\n",
    "        dominante = s.value_counts(normalize=True, dropna=True).iloc[0]\n",
    "\n",
    "        if pd.api.types.is_datetime64_any_dtype(s):\n",
    "            tipo = 'fecha'\n",
    "        elif ratio > 0.98:\n",
    "            tipo = 'identificador'\n",
    "        elif dominante > 0.95:\n",
    "            tipo = 'constante'\n",
    "        elif col in ordinales:\n",
    "            tipo = 'ordinal'\n",
    "        elif distintos <= 15:\n",
    "            tipo = 'nominal'\n",
    "        elif not pd.api.types.is_numeric_dtype(s):\n",
    "            tipo = 'alta cardinalidad'\n",
    "        elif pd.api.types.is_integer_dtype(s):\n",
    "            tipo = 'discreta'\n",
    "        else:\n",
    "            tipo = 'continua'\n",
    "\n",
    "        filas.append((col, str(s.dtype), distintos, round(ratio, 4),\n",
    "                      round(dominante, 4), tipo))\n",
    "\n",
    "    return pd.DataFrame(filas, columns=['columna', 'dtype', 'distintos',\n",
    "                                        'ratio', 'dominante', 'tipo'])\n",
    "\n",
    "\n",
    "print(audita(v, ordinales=('satisfaccion',)).to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el archivo entero clasificado en una pasada 🙌\n",
    "\n",
    "Y hay tres filas que merecen que pares 👇"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. cliente_id: 617 niveles"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('niveles: %d' % v['cliente_id'].nunique())\n",
    "print('el mas frecuente aparece %d veces' % v['cliente_id'].value_counts().iloc[0])\n",
    "print('columnas que crearia un one-hot: %d' % v['cliente_id'].nunique())\n",
    "print('filas por columna nueva: %.2f' % (len(v) / v['cliente_id'].nunique()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Codificar esto con one-hot añadiría **617 columnas** al modelo, y\n",
    "tocarían a menos de cinco filas por columna 😵\n",
    "\n",
    "Eso no es información, es ruido con formato de tabla: el modelo puede\n",
    "aprenderse cada cliente de memoria y no generalizar a ninguno nuevo.\n",
    "\n",
    "Las salidas de una alta cardinalidad son tres, y las tres son decisiones de\n",
    "negocio:\n",
    "\n",
    "- ✂️ **Sacarla.** Es lo que hace este libro, porque la pregunta\n",
    "es sobre ventas y no sobre clientes.\n",
    "\n",
    "- 🧺 **Agrupar.** Quedarse con los 20 clientes más grandes y meter\n",
    "el resto en \"otros\".\n",
    "\n",
    "- 🎯 **Codificar por target.** Reemplazar cada cliente por su tasa\n",
    "histórica de compra. Funciona muy bien y es fuga de información servida si se\n",
    "calcula sobre todos los datos, así que va dentro del pipeline y solo con el\n",
    "entrenamiento."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. satisfaccion: float64 pero ordinal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(v['satisfaccion'].value_counts().sort_index().to_string())\n",
    "print()\n",
    "print('dtype: %s | niveles: %d' % (v['satisfaccion'].dtype,\n",
    "                                   v['satisfaccion'].nunique()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cinco niveles guardados como decimales 🙃 Y decimales solo porque hay nulos:\n",
    "en pandas una columna de enteros con huecos se vuelve `float`.\n",
    "\n",
    "La diferencia práctica entre tratarla como nominal o como ordinal es esta:\n",
    "\n",
    "- 🔢 **Como ordinal**, 1 a 5, el modelo aprende que ir de 4 a 5\n",
    "es lo mismo que ir de 1 a 2. Una columna.\n",
    "\n",
    "- 🧩 **Como nominal**, one-hot de cinco columnas, el modelo puede\n",
    "aprender que el 3 se porta raro sin tener que respetar el orden. Cinco\n",
    "columnas.\n",
    "\n",
    "Y esto se decide midiendo, no opinando. Lo hace el ejercicio 3."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. monto_final_facturado: el 42,23% de las filas valen lo mismo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('valor mas comun: %.2f' % v['monto_final_facturado'].mode()[0])\n",
    "print('cuantas filas lo tienen: %d (%.2f%%)'\n",
    "      % ((v['monto_final_facturado'] == 0).sum(),\n",
    "         100 * (v['monto_final_facturado'] == 0).mean()))\n",
    "print()\n",
    "print(v.groupby('compro')['monto_final_facturado'].agg(['min', 'max', 'count']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la trampa del archivo, cazada por el detector sin que nadie le\n",
    "avisara 🎣\n",
    "\n",
    "El 42,23% de las filas tienen exactamente cero, y esas son **justo las\n",
    "que no compraron**. La columna no es un monto: es la etiqueta escrita de\n",
    "otra forma.\n",
    "\n",
    "Un auditor de tipos no sabe que eso es fuga, pero sí levanta la bandera de\n",
    "\"aquí hay un valor que domina casi la mitad de la tabla\", y esa bandera es la\n",
    "que te hace mirar. La fuga como tal la vemos en el capítulo 12."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error del capítulo"
   ]
  },
  {
   "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",
    "    v['satisfaccion'].astype(int)\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",
    "IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los 231 nulos 🕳️ Una columna de enteros con huecos no se puede convertir a\n",
    "entero, porque no existe el entero \"vacío\".\n",
    "\n",
    "Y aquí hay algo que sí sirve de verdad y casi nadie usa: pandas tiene un\n",
    "entero que admite nulos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "convertida = v['satisfaccion'].astype('Int64')\n",
    "print('dtype: %s' % convertida.dtype)\n",
    "print(convertida.head(4).to_string())\n",
    "print('nulos conservados: %d' % convertida.isna().sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La `I` mayúscula es toda la diferencia: `Int64` es el\n",
    "entero de pandas que admite huecos, y `int64` el de numpy que no.\n",
    "\n",
    "Con eso la columna deja de fingir que es decimal, y cualquier detector que\n",
    "mire `dtypes` la va a clasificar mejor 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Practica 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Las columnas que el modelo NO debe ver\n",
    "\n",
    "Con la auditoría hecha, arma la lista de lo que se descarta\n",
    "y escribe por qué cada una."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "auditoria = audita(v, ordinales=('satisfaccion',))\n",
    "fuera = auditoria[auditoria['tipo'].isin(['identificador', 'constante',\n",
    "                                          'alta cardinalidad'])]\n",
    "print(fuera[['columna', 'tipo', 'distintos']].to_string(index=False))\n",
    "print()\n",
    "print('columnas del archivo: %d' % len(v.columns))\n",
    "print('descartadas por tipo: %d' % len(fuera))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "   columna              tipo  distintos\n",
    "  id_venta     identificador       3000\n",
    "cliente_id alta cardinalidad        617\n",
    "     monto     identificador       2964\n",
    "\n",
    "columnas del archivo: 14\n",
    "descartadas por tipo: 3\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres columnas fuera antes de mirar una sola relación... y una de las tres\n",
    "está mal 🚨\n",
    "\n",
    "`id_venta` y `cliente_id` son correctas: la primera es\n",
    "un identificador puro y la segunda tiene 617 niveles.\n",
    "\n",
    "Pero **`monto` no es un identificador**, y mi propio\n",
    "detector dice que sí. El motivo está en el umbral que puse: marco como\n",
    "identificador todo lo que tenga más del 98% de valores distintos, y el monto\n",
    "tiene 2.964 valores distintos en 3.000 filas, o sea un 98,80%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('valores distintos: %d de %d' % (v['monto'].nunique(), len(v)))\n",
    "print('ratio: %.4f' % (v['monto'].nunique() / len(v)))\n",
    "print('valores que se repiten: %d' % (v['monto'].value_counts() > 1).sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "valores distintos: 2964 de 3000\n",
    "ratio: 0.9880\n",
    "valores que se repiten: 35\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Solo 35 montos se repiten, y es lo esperable en una columna de dinero con dos\n",
    "decimales: que dos ventas den exactamente 1.043,25 es casualidad, no un patrón.\n",
    "\n",
    "Lo dejo publicado tal cual porque es la lección del capítulo entero:\n",
    "**el detector propone y tú decides**. Un umbral automático no\n",
    "distingue \"cada fila tiene su propio código\" de \"esta magnitud tiene muchos\n",
    "decimales\", y las dos cosas dan un ratio de 0,99.\n",
    "\n",
    "La regla que uso yo, y que ninguna función puede aplicar sola: un\n",
    "identificador no se puede sumar. Se puede sumar el monto de dos ventas y el\n",
    "resultado significa algo; sumar dos números de factura no significa nada 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Un identificador que parece número\n",
    "\n",
    "Comprueba qué pasa si dejas id_venta dentro del modelo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X = pd.get_dummies(v[['segmento', 'canal', 'unidades', 'monto']],\n",
    "                   drop_first=True)\n",
    "y = v['compro']\n",
    "Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=7,\n",
    "                                      stratify=y)\n",
    "\n",
    "bosque = RandomForestClassifier(n_estimators=120, random_state=7).fit(Xtr, ytr)\n",
    "print('sin id: %.4f' % roc_auc_score(yte, bosque.predict_proba(Xte)[:, 1]))\n",
    "\n",
    "Xtr2, Xte2 = Xtr.copy(), Xte.copy()\n",
    "Xtr2['id_venta'] = v.loc[Xtr.index, 'id_venta']\n",
    "Xte2['id_venta'] = v.loc[Xte.index, 'id_venta']\n",
    "bosque2 = RandomForestClassifier(n_estimators=120, random_state=7).fit(Xtr2, ytr)\n",
    "print('con id: %.4f' % roc_auc_score(yte, bosque2.predict_proba(Xte2)[:, 1]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin id: 0.5901\n",
    "con id: 0.6184\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí me llevé un susto 😳\n",
    "\n",
    "Meter el identificador **subió** el AUC. Yo iba a escribir que\n",
    "el modelo se gasta capacidad en ruido y empeora, y salió lo contrario.\n",
    "\n",
    "Antes de explicarlo, hay que ver si fue casualidad de esta partición:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "sin_id, con_id = [], []\n",
    "for semilla in range(10):\n",
    "    a, b, ya, yb = train_test_split(X, y, test_size=0.25, random_state=semilla,\n",
    "                                    stratify=y)\n",
    "    m1 = RandomForestClassifier(n_estimators=120, random_state=7).fit(a, ya)\n",
    "    sin_id.append(roc_auc_score(yb, m1.predict_proba(b)[:, 1]))\n",
    "\n",
    "    a2, b2 = a.copy(), b.copy()\n",
    "    a2['id_venta'] = v.loc[a.index, 'id_venta']\n",
    "    b2['id_venta'] = v.loc[b.index, 'id_venta']\n",
    "    m2 = RandomForestClassifier(n_estimators=120, random_state=7).fit(a2, ya)\n",
    "    con_id.append(roc_auc_score(yb, m2.predict_proba(b2)[:, 1]))\n",
    "\n",
    "sin_id, con_id = np.array(sin_id), np.array(con_id)\n",
    "print('sin id: %.4f' % sin_id.mean())\n",
    "print('con id: %.4f' % con_id.mean())\n",
    "print('veces que el id ayuda: %d de 10' % (con_id > sin_id).sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin id: 0.5803\n",
    "con id: 0.6143\n",
    "veces que el id ayuda: 10 de 10\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Diez de diez. No es casualidad 🤨\n",
    "\n",
    "Y sin embargo el identificador no tiene ninguna relación con la etiqueta:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy import stats\n",
    "\n",
    "r, p = stats.pearsonr(v['id_venta'], v['compro'])\n",
    "print('correlacion id_venta con compro: r = %.4f | p = %.4f' % (r, p))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "correlacion id_venta con compro: r = 0.0022 | p = 0.9034\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "r = 0,0022 con p = 0,9034, o sea nada de nada. Entonces, ¿de dónde sale la\n",
    "mejora?\n",
    "\n",
    "El control lo resuelve. En vez del identificador, le meto una columna de\n",
    "**ruido puro**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador = np.random.default_rng(7)\n",
    "v['ruido'] = generador.normal(size=len(v))\n",
    "\n",
    "con_ruido = []\n",
    "for semilla in range(10):\n",
    "    a, b, ya, yb = train_test_split(X, y, test_size=0.25, random_state=semilla,\n",
    "                                    stratify=y)\n",
    "    a3, b3 = a.copy(), b.copy()\n",
    "    a3['ruido'] = v.loc[a.index, 'ruido']\n",
    "    b3['ruido'] = v.loc[b.index, 'ruido']\n",
    "    m3 = RandomForestClassifier(n_estimators=120, random_state=7).fit(a3, ya)\n",
    "    con_ruido.append(roc_auc_score(yb, m3.predict_proba(b3)[:, 1]))\n",
    "\n",
    "print('sin nada:       %.4f' % sin_id.mean())\n",
    "print('con id_venta:   %.4f' % con_id.mean())\n",
    "print('con ruido puro: %.4f' % np.mean(con_ruido))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin nada:       0.5803\n",
    "con id_venta:   0.6143\n",
    "con ruido puro: 0.6006\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está 🎯 Una columna de números al azar **también** sube el\n",
    "AUC.\n",
    "\n",
    "O sea que la mejora no viene del identificador: viene de darle al bosque una\n",
    "columna continua más por donde partir. Con solo cuatro columnas el bosque estaba\n",
    "corto de sitios donde cortar, y cualquier variable con muchos valores le da\n",
    "particiones más finas que, en este caso, promedian mejor.\n",
    "\n",
    "Lo que esto NO significa es que meter identificadores sea buena idea. Significa\n",
    "tres cosas y las tres importan:\n",
    "\n",
    "- 🧪 **Un experimento sin control no prueba nada.** Si me quedo\n",
    "en \"el id sube el AUC\", habría publicado una conclusión falsa con diez\n",
    "particiones de respaldo.\n",
    "\n",
    "- 🌲 **Este bosque estaba mal dimensionado** para cuatro\n",
    "columnas, y eso se arregla en el capítulo de validación, no metiéndole basura.\n",
    "\n",
    "- 🚨 **En un archivo donde los identificadores se asignen por orden**\n",
    "(por fecha, por sucursal, por lote), esto deja de ser una curiosidad y se vuelve\n",
    "fuga de información pura."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. ¿Ordinal o nominal? Que lo diga el AUC\n",
    "\n",
    "Decide el tipo de satisfaccion midiendo, no opinando."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "con_dato = v.dropna(subset=['satisfaccion'])\n",
    "y2 = con_dato['compro']\n",
    "\n",
    "como_numero = con_dato[['satisfaccion']]\n",
    "como_categoria = pd.get_dummies(con_dato['satisfaccion'].astype(int),\n",
    "                                prefix='sat')\n",
    "\n",
    "for nombre, Xs in [('ordinal (1 columna)', como_numero),\n",
    "                   ('nominal (5 columnas)', como_categoria)]:\n",
    "    a, b, ya, yb = train_test_split(Xs, y2, test_size=0.25, random_state=7,\n",
    "                                    stratify=y2)\n",
    "    m = LogisticRegression(max_iter=1000).fit(a, ya)\n",
    "    print('%-22s AUC %.4f' % (nombre, roc_auc_score(yb, m.predict_proba(b)[:, 1])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ordinal (1 columna)    AUC 0.6150\n",
    "nominal (5 columnas)   AUC 0.6150\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Prácticamente lo mismo, y eso ya es la respuesta 🎯\n",
    "\n",
    "Cuando las dos formas dan igual, se elige la de **una columna**:\n",
    "menos parámetros, más fácil de explicar y menos oportunidades de sobreajustar.\n",
    "\n",
    "La versión nominal solo se justifica si al medirla saliera claramente mejor,\n",
    "que sería la señal de que algún nivel se porta distinto de lo que su posición\n",
    "sugiere. Aquí no pasa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La alta cardinalidad, agrupada\n",
    "\n",
    "En vez de tirar cliente_id, prueba a quedarte con los\n",
    "grandes y agrupar el resto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "frecuencia = v['cliente_id'].value_counts()\n",
    "grandes = frecuencia.head(20).index\n",
    "\n",
    "agrupado = v['cliente_id'].where(v['cliente_id'].isin(grandes), 'otros')\n",
    "print('niveles antes:   %d' % v['cliente_id'].nunique())\n",
    "print('niveles despues: %d' % agrupado.nunique())\n",
    "print('filas en \"otros\": %d (%.1f%%)'\n",
    "      % ((agrupado == 'otros').sum(), 100 * (agrupado == 'otros').mean()))\n",
    "print()\n",
    "print(v.groupby(agrupado)['compro'].mean().sort_values().tail(4).round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "niveles antes:   617\n",
    "niveles despues: 21\n",
    "filas en \"otros\": 2791 (93.0%)\n",
    "\n",
    "cliente_id\n",
    "C0433    0.7000\n",
    "C0515    0.7273\n",
    "C0594    0.7778\n",
    "C0108    0.8182\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 617 niveles a 21 🧺\n",
    "\n",
    "Pero mira el porcentaje que cae en \"otros\": los veinte clientes más grandes\n",
    "son una parte chica del archivo, así que la columna nueva es casi toda \"otros\" y\n",
    "aporta poquísimo.\n",
    "\n",
    "Y hay un problema peor escondido: los clientes de arriba tienen entre diez y\n",
    "catorce ventas cada uno. Estimar una tasa de compra con catorce ventas es\n",
    "estimarla con un margen de error enorme, y el modelo se la va a creer como si\n",
    "fuera exacta.\n",
    "\n",
    "Por eso aquí la decisión es sacarla. Agrupar sirve cuando los niveles grandes\n",
    "concentran de verdad, no cuando la cola es todo 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La fecha, que no es una columna sino diez\n",
    "\n",
    "Saca de fecha todo lo que se puede sacar, y mira cuál\n",
    "sirve."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = v['fecha']\n",
    "derivadas = pd.DataFrame({\n",
    "    'mes': f.dt.month,\n",
    "    'dia_semana': f.dt.dayofweek,\n",
    "    'dia_mes': f.dt.day,\n",
    "    'trimestre': f.dt.quarter,\n",
    "    'fin_de_mes': (f.dt.day >= 25).astype(int),\n",
    "    'fin_de_semana': (f.dt.dayofweek >= 5).astype(int),\n",
    "})\n",
    "\n",
    "for col in derivadas.columns:\n",
    "    tasas = v.groupby(derivadas[col])['compro'].mean()\n",
    "    print('%-14s de %.4f a %.4f  (brecha %.4f)'\n",
    "          % (col, tasas.min(), tasas.max(), tasas.max() - tasas.min()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "mes            de 0.5479 a 0.6176  (brecha 0.0697)\n",
    "dia_semana     de 0.5592 a 0.5963  (brecha 0.0370)\n",
    "dia_mes        de 0.4865 a 0.6602  (brecha 0.1737)\n",
    "trimestre      de 0.5663 a 0.5876  (brecha 0.0213)\n",
    "fin_de_mes     de 0.5617 a 0.5817  (brecha 0.0200)\n",
    "fin_de_semana  de 0.5760 a 0.5783  (brecha 0.0023)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis columnas de una sola fecha, y ahora se ve cuáles valen 📅\n",
    "\n",
    "La brecha es cuánto separa al mejor valor del peor. Cuidado con leerla sola:\n",
    "`dia_mes` tiene 31 valores y por puro azar alguno va a destacar, así\n",
    "que su brecha siempre parece grande. Con `fin_de_semana`, que solo\n",
    "tiene dos, no hay dónde esconderse.\n",
    "\n",
    "Esa comparación entre columnas con distinto número de niveles es justo lo que\n",
    "el capítulo de EDA bivariado hace bien, con un test por medio 🔬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El auditor, con recomendación incluida\n",
    "\n",
    "Amplía el detector para que además diga qué hacer con cada\n",
    "columna."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "QUE_HACER = {\n",
    "    'identificador': 'fuera del modelo',\n",
    "    'constante': 'fuera: no distingue nada',\n",
    "    'alta cardinalidad': 'agrupar o codificar por target, dentro del pipeline',\n",
    "    'nominal': 'one-hot',\n",
    "    'ordinal': 'una columna respetando el orden',\n",
    "    'discreta': 'escalar, o partir en tramos',\n",
    "    'continua': 'escalar',\n",
    "    'fecha': 'derivar columnas, nunca usarla cruda',\n",
    "}\n",
    "\n",
    "auditoria = audita(v, ordinales=('satisfaccion',))\n",
    "auditoria['que_hacer'] = auditoria['tipo'].map(QUE_HACER)\n",
    "print(auditoria[['columna', 'tipo', 'que_hacer']].to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "              columna              tipo                                           que_hacer\n",
    "             id_venta     identificador                                    fuera del modelo\n",
    "                fecha             fecha                derivar columnas, nunca usarla cruda\n",
    "           cliente_id alta cardinalidad agrupar o codificar por target, dentro del pipeline\n",
    "               ciudad           nominal                                             one-hot\n",
    "             segmento           nominal                                             one-hot\n",
    "                canal           nominal                                             one-hot\n",
    "            categoria           nominal                                             one-hot\n",
    "             unidades          discreta                         escalar, o partir en tramos\n",
    "                monto     identificador                                    fuera del modelo\n",
    "            descuento          continua                                             escalar\n",
    "  fecha_ultima_compra             fecha                derivar columnas, nunca usarla cruda\n",
    "         satisfaccion           ordinal                     una columna respetando el orden\n",
    "               compro           nominal                                             one-hot\n",
    "monto_final_facturado          continua                                             escalar\n",
    "                ruido     identificador                                    fuera del modelo\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa tabla es el puente entre el EDA y el pipeline, y es lo que se pega en el\n",
    "informe 📋\n",
    "\n",
    "Lo que la hace útil no es la clasificación: es que **cada fila lleva\n",
    "una decisión al lado**. Cuando alguien pregunte por qué el modelo no usa\n",
    "cliente_id, la respuesta está escrita desde el principio y no se improvisa en la\n",
    "reunión.\n",
    "\n",
    "Y ojo con una cosa: el detector propone, no decide. `satisfaccion`\n",
    "salió ordinal porque yo se lo dije; sin esa lista habría salido nominal. La\n",
    "máquina puede contar valores distintos, pero **saber si hay orden es cosa\n",
    "tuya** 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu detector marca la columna monto como identificador porque el 98,80% de sus valores son distintos. ¿Qué haces?\n",
    "\n",
    "a) Lo corrijo a mano: un identificador no se puede sumar y un monto sí\n",
    "\n",
    "b) Subo el umbral del detector hasta que monto deje de salir\n",
    "\n",
    "c) Saco monto del modelo, porque el detector lo dijo\n",
    "\n",
    "d) Nada, el detector sabe más que yo\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Puede que funcione aquí. ¿Y en el archivo del mes que viene, con otro número de decimales?\n",
    "\n",
    "*c)* Estarías tirando la segunda columna más útil del archivo por una regla automática.\n",
    "\n",
    "*d)* El detector cuenta valores distintos. No sabe qué significa ninguna columna.\n",
    "\n",
    "El detector propone y tú decides. Un umbral no distingue"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La línea que parece inofensiva"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Una línea para convertir todo el texto en números, que es lo que pide scikit-learn. Corre, no da error, y la siguiente celda tarda un rato.\n",
    "\n",
    "```\n",
    "X = pd.get_dummies(ventas)\n",
    "print(X.shape)\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Se ha llevado `cliente_id` por delante, y esa columna tiene **617 niveles distintos**. O sea 617 columnas nuevas, una por cliente, cada una con un uno y 3.036 ceros 🧨\n",
    "\n",
    "El modelo se aprende clientes en vez de aprender comportamientos, y con el cliente 618 no sabe qué hacer. Y no falla en tu cuaderno: falla en producción, el día que llegue alguien nuevo. Por eso la auditoría de tipos va **antes** de codificar nada: un identificador y una categoría se escriben igual en pandas y no se tratan igual jamás."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🎭 `dtypes` dice cómo está guardada la columna, no qué es. Un\n",
    "`int64` puede ser número, identificador, categoría o fecha mal\n",
    "leída.\n",
    "\n",
    "- 🔢 Ocho categorías, y ninguna se decide mirando el dtype: se deciden contando\n",
    "valores distintos, mirando cuánto domina el más frecuente y sabiendo qué\n",
    "significa la columna.\n",
    "\n",
    "- 🚪 617 niveles en `cliente_id`: un one-hot añadiría 617 columnas\n",
    "a menos de cinco filas cada una.\n",
    "\n",
    "- 🔍 `satisfaccion` viene como `float64` solo porque\n",
    "tiene nulos. `Int64` con I mayúscula es el entero de pandas que\n",
    "admite huecos.\n",
    "\n",
    "- 🎣 El detector levantó la bandera de `monto_final_facturado` sin\n",
    "saber nada de fuga: el 42,23% de las filas valen exactamente lo mismo.\n",
    "\n",
    "- ⚠️ Y se equivocó con `monto`, que marcó como identificador porque\n",
    "el 98,80% de sus valores son distintos. El detector propone; tú decides.\n",
    "\n",
    "- 🧪 Meter el identificador subió el AUC en 10 de 10 particiones, y el control\n",
    "con una columna de ruido puro demostró que no era el identificador: era que el\n",
    "bosque estaba corto de columnas por donde partir.\n",
    "\n",
    "- 📋 La auditoría termina en una tabla con una decisión por columna, y esa\n",
    "tabla es la que se pega en el informe.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El tipo de una columna no lo declara pandas. Lo declara el negocio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si esta discusión de tipos te suena de las bases de datos, es la misma: en el [libro de SQL desde cero](https://missyera.com/guias/sql-desde-cero/) se decide al crear la tabla, y ahí duele más equivocarse 🗄️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con cada columna clasificada, toca construir las que no vienen en el\n",
    "archivo 🧱\n",
    "\n",
    "Porque hasta aquí solo hemos ordenado lo que había. Y lo que de verdad mueve\n",
    "la aguja en un proyecto de ML casi nunca es el algoritmo: son las columnas que\n",
    "alguien se inventó porque conoce el negocio.\n",
    "\n",
    "El capítulo 9 saca cinco de las que ya hay:\n",
    "\n",
    "- 🕳️ El hueco del descuento convertido en dato, que es lo que quedó\n",
    "pendiente en el capítulo de gobernanza.\n",
    "\n",
    "- 💵 El precio por unidad, que en el libro de estadística resultó ir de 26,59\n",
    "a 279,61 soles según el segmento.\n",
    "\n",
    "- 📊 El monto partido en tramos.\n",
    "\n",
    "- 🗑️ Una que no sirve, para que veas cómo se descarta.\n",
    "\n",
    "- 🪤 Y una trampa que sube el AUC treinta puntos y no vale nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 5 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/auditoria-de-tipos/\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
}
