{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cuando lo que importa pasa el 7% de las veces\n",
    "\n",
    "Pesos, umbral y remuestreo dan el mismo resultado, y ninguno crea información.\n",
    "\n",
    "Cuaderno de práctica del capítulo 19 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/clases-raras/\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: \"c2luIG5hZGEgICAgICAgUk9DIDAuOTIwMCAgIFBSIDAuMzc0MQpwZXNhZG8gICAgICAgICBST0MgMC45MjAwICAgUFIgMC4zNjg1CnN1Ym11ZXN0cmVvICAgIFJPQyAwLjkwNTIgICBQUiAwLjM3MTEKc29icmVtdWVzdHJlbyAgUk9DIDAuOTIwMCAgIFBSIDAuMzcwNA==\",\n",
    "    3: \"cGVzbyAgIDEgIGVuY29udHLDsyAgMC81NyAgZmFsc2FzIGFsYXJtYXMgICAxCnBlc28gICAzICBlbmNvbnRyw7MgNDEvNTcgIGZhbHNhcyBhbGFybWFzICA3NApwZXNvICAxMiAgZW5jb250csOzIDU3LzU3ICBmYWxzYXMgYWxhcm1hcyAxMjcKcGVzbyAgNTAgIGVuY29udHLDsyA1Ny81NyAgZmFsc2FzIGFsYXJtYXMgMTI4\",\n",
    "    4: \"IDI1IHBvc2l0aXZvcyAgQVVDIDAuOTExMyAgUFIgMC4zMzEzCiA1MCBwb3NpdGl2b3MgIEFVQyAwLjkxNDUgIFBSIDAuMzU1MQoxMDAgcG9zaXRpdm9zICBBVUMgMC45MTYyICBQUiAwLjM2NDAKMTcxIHBvc2l0aXZvcyAgQVVDIDAuOTIwMCAgUFIgMC4zNjg1\",\n",
    "    6: \"c2luIG5hZGEgICAgICAgdG4gNjkyICBmcCAgIDEgIGZuIDU3ICB0cCAgMApwZXNhZG8gICAgICAgICB0biA1NjUgIGZwIDEyOCAgZm4gIDAgIHRwIDU3CnN1Ym11ZXN0cmVvICAgIHRuIDU2NSAgZnAgMTI4ICBmbiAgMCAgdHAgNTcKc29icmVtdWVzdHJlbyAgdG4gNTY2ICBmcCAxMjcgIGZuICAwICB0cCA1Nw==\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de empezar: **¿cuántas veces has visto presumir de un porcentaje de acierto alto sin decir contra qué se comparaba?** Este capítulo va del caso donde esa cifra alta es directamente una mentira 🎭\n",
    "\n",
    "En el capítulo 14 dejamos un modelo tirado: el que predice si una venta va a\n",
    "cerrar por más de S/2.000. Tenía 92,27% de exactitud, un listón de 92,40% y\n",
    "encontraba **cero de las 57** ventas grandes 😱\n",
    "\n",
    "Hoy lo arreglamos. Y de paso vamos a ver que las tres técnicas que todo el\n",
    "mundo recomienda para esto hacen exactamente lo mismo, y que ninguna crea\n",
    "información."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import (average_precision_score, confusion_matrix,\n",
    "                             precision_score, recall_score, roc_auc_score)\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "def prepara(v):\n",
    "    v = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "    v['sin_descuento'] = v['descuento'].isna().astype(int)\n",
    "    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "# Sin monto ni precio_unitario: el objetivo se define con el monto, así que\n",
    "# dárselas sería copiarle la respuesta.\n",
    "NUMERICAS = ['unidades', 'descuento', 'satisfaccion', 'sin_compra_previa',\n",
    "             'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "def arma(clasificador=None):\n",
    "    pre = ColumnTransformer([\n",
    "        ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                          ('e', StandardScaler())]), NUMERICAS),\n",
    "        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),\n",
    "    ])\n",
    "    if clasificador is None:\n",
    "        clasificador = LogisticRegression(max_iter=1000, random_state=42)\n",
    "    return Pipeline([('pre', pre), ('mod', clasificador)])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "y = ((datos['compro'] == 1) & (datos['monto'] > 2000)).astype(int)\n",
    "X = datos[NUMERICAS + CATEGORICAS]\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",
    "print('positivos:', round(y.mean(), 4), '| en train:', int(y_tr.sum()),\n",
    "      '| en test:', int(y_te.sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7,6% de positivos. 171 casos para entrenar y 57 para examinar. Eso ya es\n",
    "desbalance de verdad, y en fraude o en fallas de máquina te vas a encontrar 1%\n",
    "o menos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El problema, una vez más"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def informe(m, nombre):\n",
    "    p = m.predict(X_te)\n",
    "    prob = m.predict_proba(X_te)[:, 1]\n",
    "    tn, fp, fn, tp = confusion_matrix(y_te, p, labels=[0, 1]).ravel()\n",
    "    print(f'{nombre:24} encontró {tp:2d}/57   falsas alarmas {fp:3d}   '\n",
    "          f'recall {recall_score(y_te, p, zero_division=0):.4f}   '\n",
    "          f'precisión {precision_score(y_te, p, zero_division=0):.4f}   '\n",
    "          f'AUC {roc_auc_score(y_te, prob):.4f}')\n",
    "\n",
    "crudo = arma().fit(X_tr, y_tr)\n",
    "informe(crudo, 'sin hacer nada')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cero de 57. El modelo aprendió que decir \"no\" acierta el 92% de las veces y\n",
    "hace eso.\n",
    "\n",
    "No es que sea tonto: **es que le pedimos que maximice los aciertos y eso\n",
    "es lo que maximiza los aciertos**. El problema no está en el modelo,\n",
    "está en lo que le pedimos 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Arreglo 1: pesar las clases"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La forma más limpia. Le decimos que equivocarse con un positivo cuesta más."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pesado = arma(LogisticRegression(max_iter=1000, random_state=42,\n",
    "                                 class_weight='balanced')).fit(X_tr, y_tr)\n",
    "informe(pesado, 'class_weight balanced')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**57 de 57** 🎉 Y 128 falsas alarmas, con una precisión de 0,308.\n",
    "\n",
    "O sea: encuentra absolutamente todas las ventas grandes, a cambio de que de\n",
    "cada tres avisos solo uno sea bueno.\n",
    "\n",
    "¿Eso es bueno? **Depende de lo que cueste cada cosa**, que es el\n",
    "capítulo 15. Si perseguir una venta grande cuesta poco y ganarla vale S/2.000,\n",
    "está regalado. Si cuesta una visita presencial a Chiclayo, no.\n",
    "\n",
    "`class_weight='balanced'` pone el peso de cada clase inversamente\n",
    "proporcional a cuántos casos tiene. Está en casi todos los modelos de\n",
    "scikit-learn y es una sola palabra 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Arreglo 2: mover el umbral"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora lo mismo, sin tocar el modelo, con lo del capítulo 15."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = crudo.predict_proba(X_te)[:, 1]\n",
    "for u in (0.5, 0.2, 0.1, 0.076):\n",
    "    p = (prob >= u).astype(int)\n",
    "    tn, fp, fn, tp = confusion_matrix(y_te, p, labels=[0, 1]).ravel()\n",
    "    print(f'umbral {u:<6} encontró {tp:2d}/57   falsas alarmas {fp:3d}   '\n",
    "          f'precisión {precision_score(y_te, p, zero_division=0):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la última fila: umbral 0,076 (que es justo la tasa de positivos)\n",
    "encuentra las 57 con 128 falsas alarmas y precisión 0,3081.\n",
    "\n",
    "**Exactamente el mismo resultado que `class_weight`.**\n",
    "Hasta el último decimal.\n",
    "\n",
    "No es casualidad: pesar las clases y mover el umbral son **dos formas de\n",
    "escribir la misma decisión**. Cambian dónde está la raya, no lo que el\n",
    "modelo sabe 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Arreglo 3: remuestrear"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El tercero es el que más se recomienda en internet y el que menos me gusta:\n",
    "cambiar los datos para que haya la misma cantidad de cada clase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entrenamiento = X_tr.copy()\n",
    "entrenamiento['objetivo'] = y_tr.values\n",
    "\n",
    "positivos = entrenamiento[entrenamiento['objetivo'] == 1]\n",
    "negativos = entrenamiento[entrenamiento['objetivo'] == 0].sample(\n",
    "    n=len(positivos), random_state=42)\n",
    "balanceado = pd.concat([positivos, negativos]).sample(frac=1, random_state=42)\n",
    "\n",
    "print('de', len(entrenamiento), 'filas a', len(balanceado))\n",
    "\n",
    "submuestreo = arma().fit(balanceado[NUMERICAS + CATEGORICAS], balanceado['objetivo'])\n",
    "informe(submuestreo, 'submuestreo')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra vez 57 de 57 y 128 falsas alarmas. Lo mismo.\n",
    "\n",
    "Y fíjate en el precio que pagó: pasó de entrenar con 2.250 filas a hacerlo con\n",
    "342. **Tiró 1.908 filas de datos buenos**, y el AUC bajó de 0,92 a\n",
    "0,9052.\n",
    "\n",
    "El sobremuestreo (repetir los positivos hasta igualar) es lo contrario y\n",
    "tampoco regala nada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "repetidos = pd.concat([positivos] * (len(entrenamiento[entrenamiento['objetivo'] == 0]) // len(positivos)))\n",
    "sobre = pd.concat([entrenamiento[entrenamiento['objetivo'] == 0], repetidos]).sample(frac=1, random_state=42)\n",
    "\n",
    "sobremuestreo = arma().fit(sobre[NUMERICAS + CATEGORICAS], sobre['objetivo'])\n",
    "informe(sobremuestreo, 'sobremuestreo')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AUC 0,92, igual que el original. Lo único que hizo fue tardar más en\n",
    "entrenar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La métrica que no se entera de que la clase es rara"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de la conclusión hay que mirar una cosa más, y es la que explica por qué\n",
    "todos los arreglos de arriba parecían funcionar 📐\n",
    "\n",
    "Todo este capítulo ha ido reportando AUC, que es el ROC-AUC de siempre. Ahora\n",
    "mírala al lado de su prima, la que casi nadie usa:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('positivos en prueba:', int(y_te.sum()), 'de', len(y_te),\n",
    "      '=', f'{y_te.mean():.1%}')\n",
    "print()\n",
    "\n",
    "for nombre, mod in [('sin hacer nada', crudo), ('class_weight balanced', pesado)]:\n",
    "    pr = mod.predict_proba(X_te)[:, 1]\n",
    "    print(f'{nombre:<22} '\n",
    "          f'ROC-AUC {roc_auc_score(y_te, pr):.4f}   '\n",
    "          f'PR-AUC {average_precision_score(y_te, pr):.4f}')\n",
    "\n",
    "print()\n",
    "print('el listón del ROC-AUC:', 0.5)\n",
    "print('el listón del PR-AUC :', round(y_te.mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,92 contra 0,37. El mismo modelo, la misma prueba, dos maneras de contar 😐\n",
    "\n",
    "Y el listón cambia igual de fuerte. Un ROC-AUC de 0,5 es tirar una moneda,\n",
    "así que 0,92 parece casi perfecto. Un PR-AUC de 0,076 es tirar una moneda,\n",
    "porque ese es el porcentaje de la clase rara, así que 0,37 es\n",
    "**cinco veces mejor que el azar**, que ya es otra frase.\n",
    "\n",
    "### Por qué se separan tanto\n",
    "\n",
    "El ROC-AUC mira las dos clases y aquí una de ellas tiene 693 casos. Acertar\n",
    "en esos 693 es fácil, y eso pesa en el número 🪶\n",
    "\n",
    "El PR-AUC solo mira la clase rara: de los que señalaste, cuántos eran de\n",
    "verdad, y de los que había, cuántos encontraste. Los 693 negativos que acertaste\n",
    "sin esfuerzo no le suman nada.\n",
    "\n",
    "Por eso, cuando la clase rara es el 7%, el ROC-AUC te va a dar siempre un\n",
    "número bonito y el PR-AUC te va a dar el número que se puede defender.\n",
    "\n",
    "### Y lo que confirma la conclusión del capítulo\n",
    "\n",
    "Fíjate en las dos filas: `class_weight='balanced'` deja el ROC-AUC\n",
    "**exactamente igual** y baja un poco el PR-AUC 🤨\n",
    "\n",
    "O sea que el arreglo más famoso para clases raras, medido con la métrica que\n",
    "sabe mirar clases raras, no mejora nada. Cambia dónde cae el corte, que es lo\n",
    "que ya vimos, pero no ordena mejor las probabilidades.\n",
    "\n",
    "Lo que hay que retener, y sirve para el resto del libro: **si la clase\n",
    "positiva es rara, se reporta PR-AUC, y se reporta con su listón al lado**.\n",
    "Un PR-AUC suelto no significa nada, porque su listón cambia con cada conjunto de\n",
    "datos 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La conclusión, que es incómoda y liberadora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira los AUC de los cuatro: 0,9200 sin hacer nada, 0,9200 con pesos, 0,9052\n",
    "con submuestreo, 0,9200 con sobremuestreo.\n",
    "\n",
    "**Ninguna técnica de balanceo mejoró la capacidad del modelo para\n",
    "ordenar.** Lo único que hicieron fue mover el punto de corte, y eso ya\n",
    "sabíamos hacerlo desde el capítulo 15 con una línea.\n",
    "\n",
    "Lo digo claro porque se enseña al revés en muchos sitios:\n",
    "**balancear no crea información**. Si el modelo no sabe distinguir,\n",
    "duplicar filas no le va a enseñar nada nuevo.\n",
    "\n",
    "Lo que sí funciona con clases raras, en orden de rentabilidad:\n",
    "\n",
    "- 1️⃣ **Medir con la métrica correcta.** AUC PR y recall, nunca\n",
    "exactitud.\n",
    "\n",
    "- 2️⃣ **Elegir el umbral por costo.** Capítulo 15.\n",
    "\n",
    "- 3️⃣ **Conseguir más casos positivos**, aunque sea de otro\n",
    "periodo. Esto sí crea información.\n",
    "\n",
    "- 4️⃣ **Construir columnas mejores.** Capítulo 9.\n",
    "\n",
    "- 5️⃣ Y si acaso, `class_weight='balanced'`, que es gratis y no\n",
    "estorba.\n",
    "\n",
    "El SMOTE, que es la técnica que genera positivos sintéticos interpolando entre\n",
    "los que hay, vive en una librería aparte (`imbalanced-learn`) y no\n",
    "está instalada aquí. Merece un aviso: **inventa filas que no existieron**,\n",
    "y en datos de negocio con muchas categóricas suele generar combinaciones\n",
    "imposibles, tipo un mayorista de Cusco que compra por un canal que Cusco no\n",
    "tiene 🤨"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El AUC PR, que es el que hay que mirar\n",
    "\n",
    "Compara AUC ROC y AUC PR de los cuatro modelos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El umbral por costo, aquí\n",
    "\n",
    "Una venta grande deja S/600 de margen y perseguirla cuesta\n",
    "S/40. ¿Dónde está el corte?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El peso, a mano\n",
    "\n",
    "En vez de `'balanced'`, pon tú el peso y mira qué\n",
    "pasa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Cuántos positivos hacen falta\n",
    "\n",
    "Entrena con 25, 50, 100 y los 171 positivos y mira el AUC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuando la clase rara es rarísima\n",
    "\n",
    "Parte sin `stratify` con una clase de solo dos\n",
    "casos y mira qué te devuelve."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. La matriz de las cuatro versiones\n",
    "\n",
    "Las cuatro matrices de confusión juntas, para verlo de un\n",
    "vistazo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La receta para clases raras\n",
    "\n",
    "Escribe el flujo completo, del que puedas copiar en tu\n",
    "próximo proyecto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu clase positiva es el 3% de los datos. ¿Qué haces primero?\n",
    "\n",
    "a) Cambio la métrica antes de tocar los datos\n",
    "\n",
    "b) Remuestreo hasta equilibrar las clases\n",
    "\n",
    "c) Genero datos sintéticos de la clase rara\n",
    "\n",
    "d) Uso un modelo más potente"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el arreglo que se pasa de listo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Tienes 7,6% de positivos, así que remuestreas para equilibrar antes de entrenar. Es lo primero que sale al buscar \"clases desbalanceadas\" y sube el resultado una barbaridad.\n",
    "\n",
    "```\n",
    "X_bal, y_bal = SMOTE().fit_resample(X, y)\n",
    "\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(\n",
    "    X_bal, y_bal, 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": [
    "- 😱 Con 7,6% de positivos, el modelo sin tocar encuentra 0 de 57.\n",
    "\n",
    "- ⚖️ `class_weight='balanced'` encuentra las 57, con 128 falsas\n",
    "alarmas y precisión 0,308.\n",
    "\n",
    "- 🎚️ Mover el umbral a 0,076 da *exactamente* el mismo resultado. Son la\n",
    "misma decisión escrita de dos formas.\n",
    "\n",
    "- 🚫 Balancear no crea información: los AUC son 0,9200, 0,9200, 0,9052 y\n",
    "0,9200.\n",
    "\n",
    "- ✂️ El submuestreo tiró 1.908 filas buenas y bajó el AUC.\n",
    "\n",
    "- 💰 El óptimo de negocio (umbral 0,15) no es el balanceo perfecto (0,076) ni el\n",
    "de fábrica (0,5).\n",
    "\n",
    "- 📈 Lo único que crea información es tener más casos positivos de verdad.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la clase positiva es rara, el problema casi nunca es el modelo. Es la vara con la que lo mides."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El fraude, la morosidad y las averías son casi siempre clases raras. Si tu caso es de empresa, en [soluciones de IA](https://missyera.com/consultoria-ia/) cuento cómo se aterrizan 🏭\n",
    "\n",
    "En el capítulo 20 abrimos la caja: qué mira el modelo, cómo se explica un\n",
    "coeficiente y por qué la importancia que trae el bosque no es de fiar.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 19 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/clases-raras/\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
}
