{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Las métricas\n",
    "\n",
    "Matriz de confusión, precisión, recall y AUC, con un modelo que gana al de antes en el número y pierde contra el listón.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 14 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/metricas/\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": [
    "Llevamos siete capítulos diciendo \"exactitud 0,6733\" como si eso fuera el\n",
    "modelo. No lo es 📐\n",
    "\n",
    "Antes de seguir, contéstate esto: **¿qué te duele más, llamar a diez clientes que no iban a comprar, o dejar escapar a uno que sí?** De esa respuesta sale toda la decisión de este capítulo 💭\n",
    "\n",
    "La exactitud es un número solo, y un modelo se equivoca de dos maneras\n",
    "distintas que casi nunca cuestan lo mismo. En este capítulo vamos a separarlas, y\n",
    "al final vas a ver un modelo con **92% de exactitud que es peor que no\n",
    "hacer nada**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "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.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['precio_unitario'] = v['monto'] / v['unidades']\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "def arma(numericas, categoricas):\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",
    "    return Pipeline([('pre', pre),\n",
    "                     ('mod', LogisticRegression(max_iter=1000, random_state=42))])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "modelo = arma(NUMERICAS, CATEGORICAS).fit(X_tr, y_tr)\n",
    "print('exactitud:', round(modelo.score(X_te, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las cuatro casillas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo empieza aquí, y esta tabla es la que hay que saber de memoria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import confusion_matrix\n",
    "\n",
    "prediccion = modelo.predict(X_te)\n",
    "print(confusion_matrix(y_te, prediccion))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se lee así: las filas son **la verdad** y las columnas\n",
    "**lo que dijo el modelo**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "|  | Dijo que no | Dijo que sí |\n",
    "|---|---|---|\n",
    "| **No compró** | 157 aciertos | 160 falsas alarmas |\n",
    "| **Sí compró** | 85 se le escaparon | 348 aciertos |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los nombres formales, que vas a ver en todos lados:\n",
    "\n",
    "- ✅ **Verdaderos positivos** (348): dijo sí y era sí.\n",
    "\n",
    "- ✅ **Verdaderos negativos** (157): dijo no y era no.\n",
    "\n",
    "- 🔔 **Falsos positivos** (160): dijo sí y era no. La falsa\n",
    "alarma. Aquí, una llamada que no valía la pena.\n",
    "\n",
    "- 💸 **Falsos negativos** (85): dijo no y era sí. El que se\n",
    "escapó. Aquí, una venta perdida.\n",
    "\n",
    "**La pregunta que hay que hacer siempre: ¿cuál de los dos errores duele\n",
    "más?** Aquí una llamada de más cuesta quince minutos y una venta perdida\n",
    "cuesta S/800. No son ni parecidos, y sin embargo la exactitud los cuenta\n",
    "igual 😳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los dos números que separan los dos errores"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Precisión=VPVP+FP,Recall=VPVP+FN\n",
    "\n",
    "de los que marcaste cuántos acertaste, y de los que había cuántos cazaste, que son dos preguntas distintas y casi siempre en conflicto"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import precision_score, recall_score, f1_score\n",
    "\n",
    "print('precisión:', round(precision_score(y_te, prediccion), 4))\n",
    "print('recall   :', round(recall_score(y_te, prediccion), 4))\n",
    "print('F1       :', round(f1_score(y_te, prediccion), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y en cristiano:\n",
    "\n",
    "- 🎯 **Precisión 0,685.** De los que dije que sí, acerté el 68,5%.\n",
    "Contesta: *cuando el modelo avisa, ¿cuánto le creo?*\n",
    "\n",
    "- 🕸️ **Recall 0,8037.** De todos los que sí compraron, atrapé el\n",
    "80,4%. Contesta: *¿a cuántos de los buenos se me escapan?*\n",
    "\n",
    "- ⚖️ **F1 0,7396.** El promedio de los dos, para cuando quieres un\n",
    "solo número. Yo casi nunca lo uso para decidir: prefiero mirar los dos.\n",
    "\n",
    "La forma de acordarse: **la precisión mira la columna del \"dijo que sí\";\n",
    "el recall mira la fila del \"sí compró\"**.\n",
    "\n",
    "Y siempre están en tensión. Si quieres atrapar a más compradores, tienes que\n",
    "avisar más veces y te vas a equivocar más. Si quieres equivocarte menos, avisas\n",
    "menos y se te escapan más. Eso se maneja con el umbral, que es el capítulo 15 🎚️"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import classification_report\n",
    "\n",
    "print(classification_report(y_te, prediccion, digits=4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese informe es el que yo pego en el correo. Fíjate en que da los números\n",
    "**para las dos clases**: el recall de la clase 0 es 0,4953, o sea\n",
    "que de los que no iban a comprar solo detecta la mitad. La exactitud sola te\n",
    "escondía eso 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El AUC, que ya venía apareciendo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AUC=P(p^+>p^−)\n",
    "\n",
    "la probabilidad de que el modelo le dé más puntaje a un caso positivo que a uno negativo tomados al azar, y por eso 0,5 es tirar una moneda"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import roc_auc_score, average_precision_score\n",
    "\n",
    "probabilidad = modelo.predict_proba(X_te)[:, 1]\n",
    "print('AUC ROC:', round(roc_auc_score(y_te, probabilidad), 4))\n",
    "print('AUC PR :', round(average_precision_score(y_te, probabilidad), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las tres métricas de arriba dependen de dónde pongas el corte. El AUC no:\n",
    "mide si el modelo **ordena** bien, o sea si a los que compran les da\n",
    "más probabilidad que a los que no.\n",
    "\n",
    "Se lee fácil: **0,5 es tirar una moneda y 1,0 es perfecto**. Y hay\n",
    "una interpretación preciosa: 0,7214 significa que si tomas al azar un comprador y\n",
    "un no comprador, el 72% de las veces el modelo le da más probabilidad al\n",
    "comprador.\n",
    "\n",
    "El **AUC PR** (0,7542) es su primo para cuando la clase positiva\n",
    "es rara. Lo vamos a necesitar en la sección siguiente 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El 92% que es peor que no hacer nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L=−1n∑i=1n[yilogpi+(1−yi)log(1−pi)]\n",
    "\n",
    "castiga estar seguro y equivocarse mucho más que estar dudando, que es justo lo que se quiere de una probabilidad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora el ejemplo que vale el capítulo entero.\n",
    "\n",
    "Cambiamos la pregunta a una más útil para el negocio: *¿esta oportunidad va\n",
    "a cerrar por más de S/2.000?* Y le quitamos al modelo las columnas de monto,\n",
    "porque si no estaría copiando la respuesta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_grande = ((datos['compro'] == 1) & (datos['monto'] > 2000)).astype(int)\n",
    "print('qué porcentaje son:', round(y_grande.mean(), 4))\n",
    "\n",
    "SIN_MONTO = [c for c in NUMERICAS if c not in ('monto', 'precio_unitario')]\n",
    "Xg = datos[SIN_MONTO + CATEGORICAS]\n",
    "Xg_tr, Xg_te, yg_tr, yg_te = train_test_split(Xg, y_grande, test_size=0.25,\n",
    "                                              random_state=42, stratify=y_grande)\n",
    "\n",
    "grande = arma(SIN_MONTO, CATEGORICAS).fit(Xg_tr, yg_tr)\n",
    "print('exactitud:', round(grande.score(Xg_te, yg_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**92,27% de exactitud** 🎉 Un número que en una diapositiva se ve\n",
    "espectacular.\n",
    "\n",
    "Ahora el listón:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.dummy import DummyClassifier\n",
    "\n",
    "tonto = DummyClassifier(strategy='most_frequent').fit(Xg_tr, yg_tr)\n",
    "print('el listón:', round(tonto.score(Xg_te, yg_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "92,40%. **El modelo es PEOR que decir \"no\" a todo el mundo** 😱\n",
    "\n",
    "Y la matriz explica por qué:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pred_g = grande.predict(Xg_te)\n",
    "print(confusion_matrix(yg_te, pred_g))\n",
    "print('recall:', round(recall_score(yg_te, pred_g, zero_division=0), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De las 57 ventas grandes de verdad, **encontró cero**. El modelo\n",
    "aprendió que decir \"no\" siempre acierta el 92% de las veces, y hace exactamente\n",
    "eso.\n",
    "\n",
    "Ese es el modelo con 92% de exactitud que no sirve para nada, y no es un\n",
    "ejemplo inventado: es lo que pasa cada vez que alguien mide un problema\n",
    "desbalanceado con exactitud."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pero el modelo no es malo, y esto es lo bonito"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob_g = grande.predict_proba(Xg_te)[:, 1]\n",
    "print('AUC ROC:', round(roc_auc_score(yg_te, prob_g), 4))\n",
    "print('AUC PR :', round(average_precision_score(yg_te, prob_g), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**AUC 0,92.** El modelo ordena estupendamente: sabe perfectamente\n",
    "quién tiene más pinta de venta grande.\n",
    "\n",
    "Y mira el AUC PR: 0,3741 cuando la clase positiva es solo el 7,6%. Ese número\n",
    "se compara contra la tasa base, así que 0,3741 contra 0,076 es\n",
    "**cinco veces mejor que el azar**. Ahí se ve para qué sirve esta\n",
    "métrica: el ROC dice 0,92 y suena a perfecto, y el PR pone el logro en su\n",
    "tamaño 📐\n",
    "\n",
    "Lo que está mal no es el modelo, es **el corte en 0,5**. Con un\n",
    "7,6% de positivos, casi nadie llega a 0,5 de probabilidad, así que todo cae del\n",
    "lado del \"no\".\n",
    "\n",
    "Dos conclusiones y las dos son de las importantes del libro:\n",
    "\n",
    "- 📏 **La exactitud sola no se reporta nunca.** Siempre con el\n",
    "listón al lado.\n",
    "\n",
    "- 🎚️ **Un modelo con buen AUC y mal recall no está roto: está mal\n",
    "cortado.** Eso se arregla y es el capítulo 15."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuál mirar según el caso"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "F1=2Precisión·RecallPrecisión+Recall\n",
    "\n",
    "la media armónica de las dos, que se hunde en cuanto una de ellas es mala en vez de disimularlo como haría un promedio normal"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Si tu problema es... | Mira | Por qué |\n",
    "|---|---|---|\n",
    "| Clases parecidas y los dos errores cuestan igual | Exactitud, con listón | Es la más fácil de explicar |\n",
    "| Que no se te escape ninguno (fraude, enfermedad) | Recall | Un falso negativo es lo caro |\n",
    "| Que no molestes a quien no toca (correo, llamadas) | Precisión | Un falso positivo es lo caro |\n",
    "| Comparar modelos sin decidir el corte | AUC ROC | No depende del umbral |\n",
    "| La clase positiva es rara (menos del 10%) | AUC PR | El ROC se ve bien aunque el modelo sirva poco |\n",
    "| Solo puedes atender N casos | Precisión en el top N | Es la que corresponde a la capacidad del capítulo 2 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. La matriz en soles\n",
    "\n",
    "Pon precio a las cuatro casillas: una llamada de más cuesta\n",
    "S/15 y una venta perdida S/800."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tn, fp, fn, tp = confusion_matrix(y_te, prediccion).ravel()\n",
    "print('falsas alarmas :', fp, '->', f'S/{fp * 15:,}')\n",
    "print('ventas perdidas:', fn, '->', f'S/{fn * 800:,}')\n",
    "print('costo total    :', f'S/{fp * 15 + fn * 800:,}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "falsas alarmas : 160 -> S/2,400\n",
    "ventas perdidas: 85 -> S/68,000\n",
    "costo total    : S/70,400\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "S/2.400 de llamadas de más contra S/68.000 de ventas perdidas.\n",
    "**El error caro es veintiocho veces el barato**, y la exactitud los\n",
    "contaba igual.\n",
    "\n",
    "Esa cuenta de tres líneas es la que convierte una métrica en una conversación\n",
    "de negocio, y es la base del capítulo 15 💰"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La precisión en el top 200\n",
    "\n",
    "Del capítulo 2: el equipo hace 200 llamadas. ¿Cuántas de\n",
    "esas 200 cierran?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "orden = np.argsort(probabilidad)[::-1]\n",
    "top = orden[:200]\n",
    "print('cierran en el top 200:', int(y_te.iloc[top].sum()), 'de 200')\n",
    "print('precisión en el top  :', round(y_te.iloc[top].mean(), 4))\n",
    "print('tasa general         :', round(y_te.mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cierran en el top 200: 159 de 200\n",
    "precisión en el top  : 0.795\n",
    "tasa general         : 0.5773\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llamando a los 200 mejores según el modelo se cierra el 79,5% (159 ventas),\n",
    "contra el 57,7% de llamar al azar. **Veintidós puntos de mejora**, y\n",
    "esa es la métrica que de verdad le importa a quien paga 📞\n",
    "\n",
    "Traducido: las mismas 200 llamadas traen 159 ventas en vez de 115. Cuarenta y\n",
    "cuatro ventas más por el mismo trabajo, y ahí sí se puede poner un número de\n",
    "soles en la propuesta 💰\n",
    "\n",
    "Fíjate en que no aparece por ningún lado la palabra exactitud."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La curva ROC, en tres puntos\n",
    "\n",
    "Qué pasa con precisión y recall en tres umbrales\n",
    "distintos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for u in (0.3, 0.5, 0.7):\n",
    "    p = (probabilidad >= u).astype(int)\n",
    "    print(f'umbral {u}  precisión {precision_score(y_te, p, zero_division=0):.4f}  '\n",
    "          f'recall {recall_score(y_te, p):.4f}  avisa {p.sum():4d} veces')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "umbral 0.3  precisión 0.6122  recall 0.9700  avisa  686 veces\n",
    "umbral 0.5  precisión 0.6850  recall 0.8037  avisa  508 veces\n",
    "umbral 0.7  precisión 0.7945  recall 0.4018  avisa  219 veces\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la tensión en una tabla. Con el umbral en 0,3 el recall sube a 0,97\n",
    "y la precisión baja a 0,61, y avisa 686 veces. Con 0,7 la precisión sube a 0,79 y\n",
    "el recall se cae a 0,40, avisando solo 219 veces.\n",
    "\n",
    "**El modelo es el mismo en las tres filas.** Lo único que cambió\n",
    "es dónde pusimos la raya 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El listón, para las dos preguntas\n",
    "\n",
    "Compara modelo y listón en los dos problemas del capítulo,\n",
    "uno al lado del otro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t1 = DummyClassifier(strategy='most_frequent').fit(X_tr, y_tr)\n",
    "print('cierra          modelo', round(modelo.score(X_te, y_te), 4),\n",
    "      '| listón', round(t1.score(X_te, y_te), 4))\n",
    "print('cierra > S/2000 modelo', round(grande.score(Xg_te, yg_te), 4),\n",
    "      '| listón', round(tonto.score(Xg_te, yg_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cierra          modelo 0.6733 | listón 0.5773\n",
    "cierra > S/2000 modelo 0.9227 | listón 0.924\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el primero ganamos diez puntos. En el segundo perdemos por poquito, con un\n",
    "número más alto.\n",
    "\n",
    "**Un 92% peor que un 67%.** Si alguna vez dudas de por qué el\n",
    "listón es obligatorio, esta es la tabla 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Los dos errores de medir\n",
    "\n",
    "Pásale a `f1_score` las probabilidades en vez de\n",
    "las predicciones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f1_score(y_te, probabilidad)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: Classification metrics can't handle a mix of binary and continuous targets\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*can't handle a mix of binary and continuous targets*. Las métricas de\n",
    "clasificación quieren **etiquetas** (0 y 1), no probabilidades.\n",
    "\n",
    "La regla para no confundirse: `predict()` para precisión, recall,\n",
    "F1 y la matriz; `predict_proba()` solo para el AUC y para la curva.\n",
    "Confundirlos es de los errores más frecuentes, y menos mal que este avisa.\n",
    "\n",
    "El otro error de medir es peor porque **no** avisa:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('al derecho:', round(precision_score(y_te, prediccion), 4))\n",
    "print('al revés  :', round(precision_score(prediccion, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "al derecho: 0.685\n",
    "al revés  : 0.8037\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,685 y 0,8037. Los dos son números creíbles y solo uno es la precisión: el\n",
    "segundo es en realidad el recall, porque al cambiar el orden le dijiste a\n",
    "scikit-learn que la predicción era la verdad 🔄\n",
    "\n",
    "El orden es siempre `(verdad, predicción)`, en todas las métricas y\n",
    "sin excepción."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuando la clase rara es la que importa\n",
    "\n",
    "Sobre el problema de las ventas grandes, mira el informe\n",
    "completo en vez de la exactitud."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(classification_report(yg_te, pred_g, digits=4, zero_division=0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "              precision    recall  f1-score   support\n",
    "\n",
    "           0     0.9239    0.9986    0.9598       693\n",
    "           1     0.0000    0.0000    0.0000        57\n",
    "\n",
    "    accuracy                         0.9227       750\n",
    "   macro avg     0.4619    0.4993    0.4799       750\n",
    "weighted avg     0.8537    0.9227    0.8868       750\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la fila de la clase 1: precisión 0, recall 0, F1 0, y 57 casos de\n",
    "soporte. El informe te lo dice en la cara.\n",
    "\n",
    "Y mira el `macro avg`, que promedia las dos clases sin pesarlas:\n",
    "0,48. Ese número sí refleja lo que pasa, y por eso en problemas desbalanceados se\n",
    "mira el macro y no el `weighted` 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Tu informe de una línea\n",
    "\n",
    "Una función que devuelva todo lo que hay que reportar de un\n",
    "modelo, para no olvidarte nada nunca más."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def informe(m, X, y_verdad, nombre=''):\n",
    "    \"\"\"Todo lo que va en el correo, con el listón incluido.\"\"\"\n",
    "    p = m.predict(X)\n",
    "    prob = m.predict_proba(X)[:, 1]\n",
    "    tonto = y_verdad.value_counts(normalize=True).max()\n",
    "    print(f'--- {nombre}')\n",
    "    print(f'  listón     {tonto:.4f}')\n",
    "    print(f'  exactitud  {m.score(X, y_verdad):.4f}')\n",
    "    print(f'  precisión  {precision_score(y_verdad, p, zero_division=0):.4f}')\n",
    "    print(f'  recall     {recall_score(y_verdad, p, zero_division=0):.4f}')\n",
    "    print(f'  AUC ROC    {roc_auc_score(y_verdad, prob):.4f}')\n",
    "    print(f'  AUC PR     {average_precision_score(y_verdad, prob):.4f}')\n",
    "\n",
    "informe(modelo, X_te, y_te, 'cierra la venta')\n",
    "informe(grande, Xg_te, yg_te, 'cierra por más de S/2000')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "--- cierra la venta\n",
    "  listón     0.5773\n",
    "  exactitud  0.6733\n",
    "  precisión  0.6850\n",
    "  recall     0.8037\n",
    "  AUC ROC    0.7214\n",
    "  AUC PR     0.7542\n",
    "--- cierra por más de S/2000\n",
    "  listón     0.9240\n",
    "  exactitud  0.9227\n",
    "  precisión  0.0000\n",
    "  recall     0.0000\n",
    "  AUC ROC    0.9200\n",
    "  AUC PR     0.3741\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis números y el listón arriba del todo, que es donde tiene que estar.\n",
    "\n",
    "Con esos seis, cualquiera que lea tu correo puede juzgar el modelo sin\n",
    "confiar en ti, y eso es exactamente lo que quieres 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu modelo tiene 92% de exactitud y el 92% de los casos son de una sola clase. ¿Es bueno?\n",
    "\n",
    "a) No se sabe: decir siempre la clase mayoritaria da lo mismo\n",
    "\n",
    "b) Sí, 92% es alto\n",
    "\n",
    "c) No, porque 92% es poco para producción\n",
    "\n",
    "d) Depende de la velocidad del modelo\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Compáralo con lo que consigue un modelo que no mira nada.\n",
    "\n",
    "*c)* El problema no es que sea poco, es que ese número no distingue nada.\n",
    "\n",
    "*d)* La velocidad no cambia lo que mide la exactitud.\n",
    "\n",
    "Con clases desbalanceadas, la exactitud es la métrica que más miente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La métrica que se elige mirando el resultado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Calculas las cuatro métricas como manda el capítulo, miras la tabla y escribes el informe con la que mejor describe al modelo. Suena razonable.\n",
    "\n",
    "```\n",
    "print(accuracy_score(y_te, pred))   # 0.67\n",
    "print(precision_score(y_te, pred))  # 0.71\n",
    "print(recall_score(y_te, pred))     # 0.58\n",
    "print(f1_score(y_te, pred))         # 0.64\n",
    "\n",
    "# al informe va la precision, 0.71\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Elegiste la métrica **después** de ver los números, y a partir de ahí ya no estás midiendo el modelo: estás buscando la foto en la que sale favorecido 📸\n",
    "\n",
    "Y no hace falta mala fe, pasa solo. Si el recall hubiera salido 0,78 habrías escrito recall, con la misma sensación de estar siendo objetiva.\n",
    "\n",
    "La métrica se elige **antes** de entrenar y sale de una pregunta que no es estadística: cuál de los dos errores duele más. Si llamar de más cuesta S/15 y perder una venta cuesta S/800, la respuesta estaba decidida desde el primer día y no depende de lo que salga. Escribe cuál vas a mirar en el contrato, y así el número que salga no puede moverte la vara."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🔢 La matriz de confusión primero: 348 aciertos, 160 falsas alarmas y 85 que\n",
    "se escaparon.\n",
    "\n",
    "- 🎯 Precisión es \"cuando avisa, cuánto le creo\" (0,685). Recall es \"a cuántos\n",
    "buenos se me escapan\" (0,8037).\n",
    "\n",
    "- 📐 El AUC no depende del umbral: mide si el modelo ordena. 0,7214 significa\n",
    "que acierta el orden el 72% de las veces.\n",
    "\n",
    "- 😱 El modelo de ventas grandes tiene 92,27% de exactitud, el listón 92,40%, y\n",
    "encuentra cero de 57. La exactitud sola no se reporta nunca.\n",
    "\n",
    "- 🎚️ Ese mismo modelo tiene 0,92 de AUC: no está roto, está mal cortado.\n",
    "\n",
    "- 💰 Las cuatro casillas se convierten en soles y ahí se ve que el error caro\n",
    "es veintiocho veces el barato.\n",
    "\n",
    "- 📞 Con capacidad limitada, la métrica es la precisión en el top N: 84% en las\n",
    "200 mejores contra 57,7% al azar.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La métrica no se elige mirando el resultado. Se elige sabiendo cuál de los dos errores duele más."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Precisión, recall y curvas ROC vienen de la estadística clásica, y están explicadas despacio en el [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📐\n",
    "\n",
    "En el capítulo 15 movemos el umbral y ponemos precio a cada error, que es donde\n",
    "el modelo se convierte en una decisión de negocio.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 14 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/metricas/\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
}
