{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El umbral\n",
    "\n",
    "El 0,5 que viene de fábrica, cuánto cuesta cada error, y por qué un modelo casi nunca decide: ordena.\n",
    "\n",
    "Cuaderno de práctica del capítulo 15 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/umbral-y-costo/\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: \"MTgwMCA2MDAgNjAwCnVtYnJhbCBlbGVnaWRvIGVuIHZhbGlkYWNpw7NuOiAwLjE4CmNvc3RvIGNvbiBlc2UgdW1icmFsOiBTLzQsNDMwCmNvc3RvIGNvbiAwLDUgICAgICAgOiBTLzU3LDEwNQ==\",\n",
    "    2: \"dW1icmFsIDAuMTQ6IFMvNCw2NTAgZW4gNzUwIG9wb3J0dW5pZGFkZXMgPSBTLzYuMjAgcG9yIG9wb3J0dW5pZGFkCnVtYnJhbCAwLjU6IFMvNzAsNDAwIGVuIDc1MCBvcG9ydHVuaWRhZGVzID0gUy85My44NyBwb3Igb3BvcnR1bmlkYWQ=\",\n",
    "    3: \"dW1icmFsIMOzcHRpbW86IDAuMzkKYSBjdcOhbnRvcyBsbGVnYTogNjA5IGRlIDc1MA==\",\n",
    "    4: \"dW1icmFsIDAuMTQ6IFMvOTcsMTA1CnVtYnJhbCAwLjM6IFMvOTQsNzEwCnVtYnJhbCAwLjU6IFMvNzksMzgwCnVtYnJhbCAwLjc6IFMvNDAsMjE1\",\n",
    "    5: \"VmFsdWVFcnJvcjogQ2xhc3NpZmljYXRpb24gbWV0cmljcyBjYW4ndCBoYW5kbGUgYSBtaXggb2YgYmluYXJ5IGFuZCBjb250aW51b3VzIHRhcmdldHM=\",\n",
    "    6: \"dW1icmFsICBsbGFtYSBhICBjaWVycmEgIHByZWNpc2nDs24gIGNvc3RvCiAgMC4xNCAgICAgIDc0MyAgICAgNDMzICAgICAgMC41ODMgIFMvNCw2NTAKICAwLjI1ICAgICAgNzE4ICAgICA0MjggICAgICAwLjU5NiAgUy84LDM1MAogIDAuNDAgICAgICA2MDAgICAgIDM5MSAgICAgIDAuNjUyICBTLzM2LDczNQogIDAuNTAgICAgICA1MDggICAgIDM0OCAgICAgIDAuNjg1ICBTLzcwLDQwMAogIDAuNjUgICAgICAyOTkgICAgIDIzMSAgICAgIDAuNzczICBTLzE2Miw2MjA=\",\n",
    "    7: \"cHJvYmFiaWxpZGFkIGRlbCBwdWVzdG8gMjAwOiAwLjcxMDkKbGxhbWFuZG8gcG9yIHVtYnJhbCAwLDE0OiA3NDMKbGxhbWFuZG8gcG9yIGNhcGFjaWRhZCAgOiAyMDA=\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo que hemos hecho hasta ahora tenía un 0,5 escondido dentro 🎚️\n",
    "\n",
    "Y va una pregunta: **¿alguna vez has puesto un número redondo en un sitio importante solo porque venía por defecto?** Yo sí, muchas veces, y este capítulo va de una de esas 🫣\n",
    "\n",
    "Cuando llamas a `predict()`, el modelo calcula una probabilidad y\n",
    "la compara contra 0,5. Si pasa, dice que sí. Ese número no lo eligió nadie: es\n",
    "el que viene de fábrica.\n",
    "\n",
    "Y en este capítulo vamos a ver que **elegirlo bien vale S/65.750**\n",
    "sobre 750 oportunidades, sin tocar el modelo."
   ]
  },
  {
   "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 confusion_matrix\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",
    "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",
    "\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",
    "\n",
    "modelo = Pipeline([('pre', pre),\n",
    "                   ('mod', LogisticRegression(max_iter=1000, random_state=42))]).fit(X_tr, y_tr)\n",
    "probabilidad = modelo.predict_proba(X_te)[:, 1]\n",
    "print('750 oportunidades, probabilidades entre',\n",
    "      round(probabilidad.min(), 3), 'y', round(probabilidad.max(), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los dos precios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Del capítulo 2, y esto no lo decide quien programa:\n",
    "\n",
    "- 🔔 **Falso positivo:** llamamos a alguien que no iba a comprar.\n",
    "Cuesta quince minutos del vendedor, digamos **S/15**.\n",
    "\n",
    "- 💸 **Falso negativo:** no llamamos a alguien que sí iba a\n",
    "comprar. Se pierde la venta entera, digamos **S/800**.\n",
    "\n",
    "Cincuenta y tres veces más caro uno que el otro. Y ahora la pregunta:\n",
    "*¿por qué íbamos a cortar en el medio?*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El costo de cada umbral"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Costo(u)=cFPFP(u)+cFNFN(u)\n",
    "\n",
    "el umbral bueno no es 0,5, es el que hace más chica la suma de lo que cuestan los dos errores en tu negocio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COSTO_FP, COSTO_FN = 15, 800\n",
    "\n",
    "def costo(umbral):\n",
    "    prediccion = (probabilidad >= umbral).astype(int)\n",
    "    tn, fp, fn, tp = confusion_matrix(y_te, prediccion, labels=[0, 1]).ravel()\n",
    "    return fp, fn, fp * COSTO_FP + fn * COSTO_FN\n",
    "\n",
    "for u in (0.15, 0.3, 0.5, 0.7, 0.9):\n",
    "    fp, fn, total = costo(u)\n",
    "    print(f'umbral {u:.2f}   falsas alarmas {fp:3d}   ventas perdidas {fn:3d}   S/{total:,}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo de arriba abajo, porque es de las tablas más útiles de todo el libro.\n",
    "\n",
    "Con el 0,5 de fábrica perdemos **S/70.400**. Con 0,15,\n",
    "**S/4.650**.\n",
    "\n",
    "Quince veces menos. Mismo modelo, mismos datos, mismo día. Solo movimos un\n",
    "número 😳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Buscarlo bien"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "candidatos = [(costo(u)[2], round(u, 2)) for u in np.arange(0.01, 0.99, 0.01)]\n",
    "mejor_costo, mejor_umbral = min(candidatos)\n",
    "\n",
    "print('umbral óptimo:', mejor_umbral)\n",
    "print('costo ahí    :', f'S/{mejor_costo:,}')\n",
    "print('costo en 0,5 :', f'S/{costo(0.5)[2]:,}')\n",
    "print('ahorro       :', f'S/{costo(0.5)[2] - mejor_costo:,}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,14 y S/65.750 de ahorro sobre 750 oportunidades. Anualizado sobre el archivo\n",
    "entero, es una cifra que hace que valga la pena el proyecto entero 💰\n",
    "\n",
    "**Y no costó ni una línea de modelo.** Costó preguntar cuánto\n",
    "vale cada error, que es la pregunta del capítulo 2."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que sale de ahí, y que incomoda un poco"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('a cuántos llama con umbral 0,14:', int((probabilidad >= 0.14).sum()), 'de 750')\n",
    "print('a cuántos llama con umbral 0,50:', int((probabilidad >= 0.50).sum()), 'de 750')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "743 de 750. O sea que, con esos precios, **la respuesta correcta es\n",
    "llamar a casi todo el mundo**.\n",
    "\n",
    "Y tiene toda la lógica: si una llamada cuesta S/15 y una venta vale S/800, no\n",
    "llamar es una apuesta malísima. El modelo no hacía falta para decidir a quién\n",
    "llamar.\n",
    "\n",
    "Esto pasa de verdad y hay que decirlo en la reunión en vez de esconderlo. Pero\n",
    "no significa que el modelo no sirva, y la razón es la del capítulo 2:\n",
    "**nadie puede hacer 743 llamadas** 📞"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahí es donde el modelo sí vale"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuando la capacidad es la que manda, la decisión ya no es un umbral: es un\n",
    "orden."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "orden = np.argsort(probabilidad)[::-1]\n",
    "\n",
    "print('capacidad  con modelo  al azar  ventas de más')\n",
    "for k in (100, 200, 300, 500):\n",
    "    con_modelo = int(y_te.iloc[orden[:k]].sum())\n",
    "    al_azar = round(k * y_te.mean())\n",
    "    print(f'{k:9d}  {con_modelo:10d}  {al_azar:7.0f}  {con_modelo - al_azar:+3d}'\n",
    "          f'   S/{(con_modelo - al_azar) * 800:,}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 200 llamadas: 159 ventas usando el modelo contra 115 llamando al azar.\n",
    "**Cuarenta y cuatro ventas más por el mismo trabajo**, o sea\n",
    "S/35.200 🎯\n",
    "\n",
    "Y fíjate en la última fila: con 500 llamadas la ganancia baja a 54. Cuanto más\n",
    "llamas, menos aporta ordenar, porque al final llamas a todos igual. La ganancia\n",
    "de un modelo de ranking **es más grande cuanto más apretada esté la\n",
    "capacidad**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuándo el 0,5 sí es correcto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a mover los precios y ver qué le pasa al umbral óptimo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def optimo(costo_fp, costo_fn):\n",
    "    mejor = None\n",
    "    for u in np.arange(0.01, 0.99, 0.01):\n",
    "        p = (probabilidad >= u).astype(int)\n",
    "        tn, fp, fn, tp = confusion_matrix(y_te, p, labels=[0, 1]).ravel()\n",
    "        total = fp * costo_fp + fn * costo_fn\n",
    "        if mejor is None or total < mejor[0]:\n",
    "            mejor = (total, round(u, 2))\n",
    "    return mejor\n",
    "\n",
    "for cfp in (15, 100, 400, 800):\n",
    "    total, u = optimo(cfp, 800)\n",
    "    print(f'falso positivo S/{cfp:3d} contra falso negativo S/800 -> umbral {u}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la respuesta completa: el umbral sube según el falso positivo se\n",
    "acerca al falso negativo, y **cuando los dos cuestan igual, el óptimo es\n",
    "0,49**, o sea el 0,5 de fábrica.\n",
    "\n",
    "Así que ese 0,5 no está mal: **está bien para un caso concreto, el de\n",
    "los errores que cuestan lo mismo**. Lo que está mal es usarlo sin\n",
    "preguntarse si es tu caso 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cómo se hace bien, que es lo que se olvida"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un aviso importante: **el umbral se elige en validación, no en el\n",
    "examen**. Si lo buscas sobre los mismos datos donde lo mides, estás\n",
    "haciendo lo mismo que el capítulo 12 y el número sale optimista.\n",
    "\n",
    "La forma correcta es partir en tres: entrenar, elegir umbral, y medir. O usar\n",
    "validación cruzada para elegirlo, que es el capítulo 16.\n",
    "\n",
    "Aquí lo hice sobre el examen para que se vea la idea con menos código, y te lo\n",
    "digo en vez de tapártelo 💛\n",
    "\n",
    "Ahora hagámoslo bien, que son cuatro líneas más, y de paso medimos cuánto nos\n",
    "estaba mintiendo el atajo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_ent, X_val, y_ent, y_val = train_test_split(X_tr, y_tr, test_size=0.3,\n",
    "                                              random_state=42, stratify=y_tr)\n",
    "bien = Pipeline([\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",
    "    ('mod', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "]).fit(X_ent, y_ent)\n",
    "p_val = bien.predict_proba(X_val)[:, 1]\n",
    "p_test = bien.predict_proba(X_te)[:, 1]\n",
    "\n",
    "def costo_de(prob, objetivo, umbral):\n",
    "    tn, fp, fn, tp = confusion_matrix(objetivo, (prob >= umbral).astype(int),\n",
    "                                      labels=[0, 1]).ravel()\n",
    "    return fp * COSTO_FP + fn * COSTO_FN\n",
    "\n",
    "umbrales = np.arange(0.05, 0.96, 0.01)\n",
    "elegido = umbrales[np.argmin([costo_de(p_val, y_val, u) for u in umbrales])]\n",
    "tramposo = umbrales[np.argmin([costo_de(p_test, y_te, u) for u in umbrales])]\n",
    "\n",
    "print('umbral elegido en validación:', round(elegido, 2))\n",
    "print('  lo que costó en validación: S/', costo_de(p_val, y_val, elegido))\n",
    "print('  lo que cuesta en el examen: S/', costo_de(p_test, y_te, elegido))\n",
    "print('umbral elegido haciendo trampa:', round(tramposo, 2),\n",
    "      ' cuesta S/', costo_de(p_test, y_te, tramposo))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El umbral honesto sale 0,12 y el tramposo 0,14. Y la diferencia de costo entre\n",
    "los dos, medida en el examen, es de **S/30 sobre S/4.650**.\n",
    "\n",
    "O sea que aquí el atajo casi no mintió, y lo digo aunque me deje sin\n",
    "moraleja dramática 😅\n",
    "\n",
    "Pero fíjate en la otra línea, que es la que sí importa: en validación el\n",
    "umbral parecía costar S/4.230 y en el examen cuesta S/4.680. **Ese es el\n",
    "optimismo de verdad**, y no está en el umbral elegido sino en la cifra\n",
    "que ibas a reportar.\n",
    "\n",
    "La regla se queda igual: el umbral se elige en validación y el número que\n",
    "prometes sale del examen. Que el atajo salga barato en un dataset no significa\n",
    "que salga barato en el tuyo 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El umbral elegido como se debe\n",
    "\n",
    "Parte en tres, elige el umbral en el trozo del medio y\n",
    "mídelo en el último."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El costo por oportunidad\n",
    "\n",
    "Pasa el costo total a soles por oportunidad, que es como se\n",
    "presenta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuando el falso positivo es el caro\n",
    "\n",
    "Al revés: enviar un cupón de S/200 a quien iba a comprar\n",
    "igual, contra perder una venta de S/300."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La ganancia, no el costo\n",
    "\n",
    "Dale la vuelta: en vez de contar lo que se pierde, cuenta lo\n",
    "que se gana."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El error de comparar probabilidades con etiquetas\n",
    "\n",
    "Calcula la matriz de confusión pasándole las probabilidades\n",
    "sin cortar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. La tabla que se lleva a la reunión\n",
    "\n",
    "Cinco umbrales con todo lo que pregunta un gerente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El modelo no decide, el modelo ordena\n",
    "\n",
    "Junta las dos ideas del capítulo: con capacidad de 200,\n",
    "cuál es el corte de probabilidad que corresponde."
   ]
  },
  {
   "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",
    "¿De dónde sale el 0,5 que usa el modelo para decidir?\n",
    "\n",
    "a) De la librería, y casi nunca es el que le conviene a tu negocio\n",
    "\n",
    "b) De la teoría estadística\n",
    "\n",
    "c) De los datos de entrenamiento\n",
    "\n",
    "d) Es el valor que maximiza la exactitud"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el umbral que se queda clavado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Encontraste el umbral óptimo, 0,14, y lo dejas fijo en el código que se va a producción. Con eso ahorras S/65.750 y el proyecto se cierra.\n",
    "\n",
    "```\n",
    "UMBRAL = 0.14\n",
    "\n",
    "def decidir(cliente):\n",
    "    p = modelo.predict_proba(cliente)[0, 1]\n",
    "    return p > UMBRAL\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": [
    "- 🎚️ `predict()` lleva un 0,5 escondido que no eligió nadie.\n",
    "\n",
    "- 💰 Con falso positivo a S/15 y falso negativo a S/800, el óptimo es 0,14 y\n",
    "ahorra S/65.750 sobre 750 oportunidades. Sin tocar el modelo.\n",
    "\n",
    "- ⚖️ El 0,5 solo es correcto cuando los dos errores cuestan igual: con costos\n",
    "iguales el óptimo sale 0,49.\n",
    "\n",
    "- 😐 Con esos precios el óptimo llama a 743 de 750, o sea a casi todos. Hay que\n",
    "decirlo, no esconderlo.\n",
    "\n",
    "- 📞 Y ahí entra la capacidad: con 200 llamadas, el modelo trae 159 ventas\n",
    "contra 115 al azar. Cuarenta y cuatro más por el mismo trabajo.\n",
    "\n",
    "- 🔒 El umbral se elige en validación, nunca en el examen.\n",
    "\n",
    "- 📋 Tu trabajo es poner la tabla de umbrales con soles al lado. La política la\n",
    "elige el negocio.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El 0,5 no lo eligió nadie. Y aun así decide a quién llamas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si te toca defender estos números delante de alguien que decide, el [artículo de cómo ser analista de datos](https://missyera.com/blog/ser-analista-de-datos/) habla justo de esa parte del trabajo 💼\n",
    "\n",
    "En el capítulo 16 dejamos de fiarnos de una sola partición: validación cruzada,\n",
    "búsqueda de hiperparámetros y cómo no engañarse a una misma.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 15 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/umbral-y-costo/\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
}
