{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El proyecto entero\n",
    "\n",
    "Todo el libro en un solo archivo, y la pregunta con la que se termina de verdad: a quién llamo el lunes.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 28 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/proyecto-final/\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": [
    "Veintitrés capítulos y llegamos a lo único que le importa a quien pagó por esto:\n",
    "**a quién llamo el lunes** 📞\n",
    "\n",
    "Este capítulo es el proyecto entero seguido, sin desviarse. Cada decisión lleva\n",
    "al lado el capítulo donde la discutimos, para que puedas volver.\n",
    "\n",
    "Y termina donde tiene que terminar un proyecto de datos: en una decisión de\n",
    "negocio con su precio medido al lado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El encargo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El equipo comercial puede visitar 200 clientes al trimestre. Hoy los elige por\n",
    "costumbre. La pregunta es si podemos elegirlos mejor.\n",
    "\n",
    "Fíjate que el encargo no dice \"hazme un modelo\". Dice 200. Ese número es el que\n",
    "manda, y es el que va a decidir todo lo que viene 🎯\n",
    "\n",
    "Antes de tocar nada, tres preguntas al que encarga, que son las del capítulo\n",
    "2 aplicadas aquí:\n",
    "\n",
    "- 🎯 **¿Qué se va a hacer distinto?** Se cambia el criterio con\n",
    "el que se arma la ruta de visitas. Si la respuesta hubiera sido \"nada, es para\n",
    "tener el dato\", no hay proyecto.\n",
    "\n",
    "- 📅 **¿Cuándo se decide?** Al empezar el trimestre, así que solo\n",
    "vale lo que se sabe antes de la visita. Eso deja fuera media tabla.\n",
    "\n",
    "- 💰 **¿Contra qué se compara el éxito?** Contra elegir por\n",
    "costumbre. Como no tenemos registro de la costumbre, usamos el azar como listón,\n",
    "que es más exigente de lo que parece.\n",
    "\n",
    "Y una cuarta que casi nadie hace y ahorra disgustos: **¿qué pasa con\n",
    "los que no entran en la lista?** Porque la respuesta por defecto es \"no\n",
    "se les visita\", y eso tiene consecuencias que vamos a ver en el paso 6 y que no\n",
    "salen en ninguna métrica 👀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 1. Cargar y limpiar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Del capítulo 4, tal cual, sin cambiar nada."
   ]
  },
  {
   "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 roc_auc_score\n",
    "from sklearn.model_selection import GroupShuffleSplit, 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",
    "MARGEN = 0.25          # el negocio se queda con 25 centavos de cada sol vendido\n",
    "CUPO = 200             # visitas que caben en el trimestre\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",
    "crudo = pd.read_csv(URL)\n",
    "datos = carga_limpia(URL)\n",
    "print('filas en el CSV:', len(crudo))\n",
    "print('filas después de quitar duplicadas:', len(datos))\n",
    "print('montos que se habrían perdido con to_numeric a secas: S/',\n",
    "      round(pd.to_numeric(crudo['monto'], errors='coerce').isna().sum() and\n",
    "            datos.loc[pd.to_numeric(crudo['monto'], errors='coerce').reindex(datos.index).isna(),\n",
    "                      'monto'].sum(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Treinta y siete filas duplicadas fuera y casi medio millón de soles rescatados\n",
    "de la coma decimal. Eso ya justifica el capítulo 4 entero 💰"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 2. Las variables, y sobre todo las que no"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Del capítulo 9 vienen las banderas de nulo, que era el hallazgo bueno. Del capítulo 12 viene la disciplina de no meter nada que no exista el día que predices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "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",
    "COLS = NUMERICAS + CATEGORICAS\n",
    "\n",
    "datos = prepara(datos)\n",
    "print('columnas que entran:', len(COLS))\n",
    "print()\n",
    "print('la bandera que valía la pena:')\n",
    "print(datos.groupby('sin_compra_previa')['compro'].agg(['size', 'mean']).round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Veinte puntos de diferencia entre las filas donde falta la fecha de la última\n",
    "compra y las demás. **El nulo tenía información** y por eso se marca\n",
    "antes de rellenarlo.\n",
    "\n",
    "Y lo que dejamos fuera a propósito: `monto_final_facturado`, que en\n",
    "el capítulo 12 daba 0,9994 de AUC y en producción predecía cero de 750 porque no\n",
    "existe todavía. La lista de columnas prohibidas es tan importante como la de\n",
    "columnas usadas 🚫"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 3. Qué pregunta estoy respondiendo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí es donde la mayoría de los proyectos se tuercen, y es una sola decisión:\n",
    "cómo partir los datos.\n",
    "\n",
    "En el capítulo 16 usamos `GroupShuffleSplit` para que un cliente no estuviera en los dos lados. En el capítulo 27 partimos por fecha. Las dos están\n",
    "bien, porque **responden preguntas distintas**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gs = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42)\n",
    "i, j = next(gs.split(datos[COLS], datos['compro'], groups=datos['cliente_id']))\n",
    "\n",
    "def arma_modelo():\n",
    "    return 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",
    "    ])\n",
    "\n",
    "por_cliente = arma_modelo().fit(datos[COLS].iloc[i], datos['compro'].iloc[i])\n",
    "print('cliente que nunca vi :', round(roc_auc_score(\n",
    "    datos['compro'].iloc[j], por_cliente.predict_proba(datos[COLS].iloc[j])[:, 1]), 4))\n",
    "\n",
    "CORTE = pd.Timestamp('2026-01-01')\n",
    "pasado = datos[datos['fecha'] < CORTE]\n",
    "futuro = datos[datos['fecha'] >= CORTE].copy()\n",
    "modelo = arma_modelo().fit(pasado[COLS], pasado['compro'])\n",
    "futuro['prob'] = modelo.predict_proba(futuro[COLS])[:, 1]\n",
    "print('próximo trimestre    :', round(roc_auc_score(futuro['compro'], futuro['prob']), 4))\n",
    "print('clientes en los dos lados de la partición por fecha:',\n",
    "      len(set(pasado['cliente_id']) & set(futuro['cliente_id'])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,6843 contra 0,7227. Casi cuatro puntos de diferencia por elegir una partición\n",
    "en vez de la otra.\n",
    "\n",
    "Y no es que una mienta. La de grupos dice **qué tan bien le va con un\n",
    "cliente que nunca vio**. La de fecha dice **qué tan bien le va el\n",
    "próximo trimestre con la cartera que ya tiene**, donde 453 clientes\n",
    "aparecen a los dos lados porque son los mismos de siempre.\n",
    "\n",
    "El encargo era priorizar la cartera actual. Entonces la partición correcta es\n",
    "la de fecha, y el 0,7227 es el número honesto para este encargo. Si el encargo\n",
    "fuera captar clientes nuevos, el número honesto sería 0,6843 y habría que decirlo\n",
    "así 🧭"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 4. El modelo, y contra qué se compara"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Del capítulo 13: probamos ocho y ganó la regresión logística. Del capítulo 14: un modelo se compara contra la tontería más simple que exista, no contra el cero."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "base = futuro['compro'].mean()\n",
    "print('si llamo a todos, compra el:', round(base, 4))\n",
    "print('AUC del modelo            :', round(roc_auc_score(futuro['compro'], futuro['prob']), 4))\n",
    "print()\n",
    "orden = futuro.sort_values('prob', ascending=False)\n",
    "print('de los 200 mejores según el modelo, compran:',\n",
    "      int(orden.head(CUPO)['compro'].sum()))\n",
    "print('de 200 al azar, comprarían                 :', round(CUPO * base, 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "163 contra 114. **Cuarenta y nueve ventas más con el mismo esfuerzo**,\n",
    "y eso es lo que hay que decir en la reunión, no el 0,7227 😊"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 5. Ordenar por lo que de verdad importa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí hay un error que yo cometí durante años: ordenar por probabilidad.\n",
    "\n",
    "Una bodega con 90% de probabilidad y S/200 de pedido vale menos que un mayorista\n",
    "con 60% y S/3.000. Lo que se ordena es el **valor esperado**, que es\n",
    "la probabilidad multiplicada por lo que hay en juego:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "futuro['valor_esperado'] = futuro['prob'] * futuro['monto'] * MARGEN\n",
    "\n",
    "por_prob = futuro.nlargest(CUPO, 'prob')\n",
    "por_valor = futuro.nlargest(CUPO, 'valor_esperado')\n",
    "\n",
    "for nombre, lista in [('ordenando por probabilidad', por_prob),\n",
    "                      ('ordenando por valor esperado', por_valor)]:\n",
    "    ventas = int(lista['compro'].sum())\n",
    "    margen = lista.loc[lista['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    print(f'{nombre:30} {ventas} ventas   S/{margen:,.2f} de margen')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ordenar por valor esperado consigue menos ventas (147 contra 163) y bastante\n",
    "más plata (S/76.302 contra S/56.859).\n",
    "\n",
    "Dieciséis ventas menos y S/19.443 más. Si tu jefe mide ventas cerradas,\n",
    "entrega la primera lista; si mide margen, la segunda. **Pregunta antes de\n",
    "entregar**, porque las dos son defendibles y solo una es la que te pidieron 🤔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 6. El problema de la lista"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a mirar quién está en esa lista tan rentable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(por_valor.groupby('segmento').agg(\n",
    "    en_la_lista=('prob', 'size'), compraron=('compro', 'sum')).to_string())\n",
    "print()\n",
    "print('bodegas que hubo en el trimestre:', int((futuro['segmento'] == 'Bodega').sum()))\n",
    "print('de esas, compraron              :', int(futuro.loc[futuro['segmento'] == 'Bodega', 'compro'].sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "192 mayoristas, 8 de horeca y **cero bodegas**. Ninguna. En todo\n",
    "el trimestre hubo 237 bodegas y 86 de ellas compraron, y el modelo no manda a\n",
    "visitar ni una 😬\n",
    "\n",
    "Esto no es un fallo técnico. El modelo hace exactamente lo que le pedimos:\n",
    "maximizar plata. Las bodegas compran menos y compran más barato, así que nunca\n",
    "ganan una comparación por valor esperado.\n",
    "\n",
    "Pero si esa lista se ejecuta cuatro trimestres seguidos, el negocio se queda sin\n",
    "canal de bodegas, y eso no aparece en ninguna métrica del capítulo 14 📉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 7. Cuánto cuesta no abandonar a nadie"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La solución del capítulo 24 aplicada aquí es un cupo por segmento: 50 y 50 y 50\n",
    "y 50 en vez de 200 al mejor postor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "con_cuota = pd.concat([g.nlargest(50, 'valor_esperado')\n",
    "                       for _, g in futuro.groupby('segmento')])\n",
    "\n",
    "for nombre, lista in [('sin cuota', por_valor), ('con cuota', con_cuota)]:\n",
    "    ventas = int(lista['compro'].sum())\n",
    "    margen = lista.loc[lista['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    print(f'{nombre}: {ventas} ventas   S/{margen:,.2f} de margen')\n",
    "print()\n",
    "print(con_cuota.groupby('segmento').agg(\n",
    "    en_la_lista=('prob', 'size'), compraron=('compro', 'sum')).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La cuota cuesta **S/32.870 de margen y 15 ventas** en el trimestre.\n",
    "Y a cambio pone 50 bodegas en la lista, de las que 24 compran.\n",
    "\n",
    "Ese es todo el trabajo, honestamente. No decidir por el negocio: ponerle el\n",
    "precio exacto a las dos opciones y dejar que decida quien responde por ellas 🤝\n",
    "\n",
    "Y si te preguntan qué harías tú, yo diría la cuota, porque el modelo se\n",
    "entrena con lo que pasó y una lista que nunca visita bodegas deja de generar datos\n",
    "de bodegas. Al año siguiente el modelo va a saber todavía menos de ellas, y esa\n",
    "bola de nieve no se ve venir en ningún tablero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 8. Lo que se entrega"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No se entrega un cuaderno. Se entrega esto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "entrega = con_cuota.sort_values('valor_esperado', ascending=False)[\n",
    "    ['cliente_id', 'segmento', 'ciudad', 'canal', 'monto', 'prob', 'valor_esperado']].copy()\n",
    "entrega['prob'] = entrega['prob'].round(4)\n",
    "entrega['valor_esperado'] = entrega['valor_esperado'].round(2)\n",
    "\n",
    "print(entrega.head(10).to_string(index=False))\n",
    "print()\n",
    "print('clientes en la lista :', len(entrega))\n",
    "print('valor esperado total : S/', round(entrega['valor_esperado'].sum(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un archivo que el equipo comercial abre y usa, con el nombre del cliente, por\n",
    "qué está ahí y cuánto vale la visita.\n",
    "\n",
    "Y con eso van tres cosas más, que son las que separan un proyecto de un\n",
    "ejercicio:\n",
    "\n",
    "- 📄 Una página: qué se predice, con qué datos, qué mide (0,7227), contra qué se\n",
    "compara (114 ventas al azar) y qué no sabe hacer.\n",
    "\n",
    "- 🚦 La función `revisa` del capítulo 27 corriendo antes de cada\n",
    "entrega, para que nadie prediga sobre datos rotos.\n",
    "\n",
    "- 📅 Una fecha para volver a mirar, con el rango de bailoteo ya medido para no\n",
    "asustarse en vano."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El cupo manda más que el modelo\n",
    "\n",
    "Repite la comparación con cupos distintos y mira dónde deja\n",
    "de valer la pena."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for cupo in [50, 100, 200, 400, 800]:\n",
    "    top = futuro.nlargest(cupo, 'valor_esperado')\n",
    "    margen = top.loc[top['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    azar = cupo / len(futuro) * futuro.loc[futuro['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    print(f'cupo={cupo:4d}  margen=S/{margen:10,.2f}  al azar=S/{azar:10,.2f}  '\n",
    "          f'ganancia=S/{margen - azar:9,.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cupo=  50  margen=S/ 27,912.45  al azar=S/  6,693.23  ganancia=S/21,219.23\n",
    "cupo= 100  margen=S/ 47,099.72  al azar=S/ 13,386.46  ganancia=S/33,713.26\n",
    "cupo= 200  margen=S/ 76,302.41  al azar=S/ 26,772.92  ganancia=S/49,529.49\n",
    "cupo= 400  margen=S/108,388.92  al azar=S/ 53,545.84  ganancia=S/54,843.09\n",
    "cupo= 800  margen=S/130,691.38  al azar=S/107,091.67  ganancia=S/23,599.71\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La ganancia sube hasta el cupo 400 y después se desinfla, porque cuando visitas\n",
    "a todo el mundo el orden deja de importar.\n",
    "\n",
    "Esta tabla vale más que cualquier métrica para decidir si contratar más\n",
    "vendedores. **El modelo solo sirve cuando no alcanza para todos** 📊"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El margen que no es igual para todos\n",
    "\n",
    "Hasta aquí usamos 25% parejo. Pon márgenes distintos por\n",
    "categoría y mira si la lista cambia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MARGENES = {'Abarrotes': 0.18, 'Bebidas': 0.22, 'Cuidado personal': 0.35,\n",
    "            'Limpieza': 0.28, 'Snacks': 0.30}\n",
    "futuro['ve2'] = futuro['prob'] * futuro['monto'] * futuro['categoria'].map(MARGENES)\n",
    "otra = futuro.nlargest(CUPO, 've2')\n",
    "\n",
    "print('coinciden con la lista anterior:',\n",
    "      len(set(otra.index) & set(por_valor.index)), 'de', CUPO)\n",
    "print()\n",
    "print(otra.groupby('categoria').size().to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "coinciden con la lista anterior: 177 de 200\n",
    "\n",
    "categoria\n",
    "Abarrotes           32\n",
    "Bebidas             33\n",
    "Cuidado personal    48\n",
    "Limpieza            39\n",
    "Snacks              48\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Coinciden 177 de 200, así que cambiar el margen mueve 23 clientes. Menos de lo\n",
    "que yo esperaba, y es porque el monto pesa mucho más que el margen en la\n",
    "multiplicación.\n",
    "\n",
    "Y esos márgenes me los inventé yo para el ejercicio. En un proyecto de verdad\n",
    "salen de contabilidad, y conseguirlos suele costar más reuniones que entrenar el\n",
    "modelo entero 📑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto de esto es suerte\n",
    "\n",
    "Repite la lista con varios cortes de fecha y mira si la\n",
    "ganancia aguanta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for corte in ['2025-10-01', '2026-01-01', '2026-03-01']:\n",
    "    c = pd.Timestamp(corte)\n",
    "    a, b = datos[datos['fecha'] < c], datos[datos['fecha'] >= c].copy()\n",
    "    m = arma_modelo().fit(a[COLS], a['compro'])\n",
    "    b['prob'] = m.predict_proba(b[COLS])[:, 1]\n",
    "    b['ve'] = b['prob'] * b['monto'] * MARGEN\n",
    "    top = b.nlargest(CUPO, 've')\n",
    "    margen = top.loc[top['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    azar = CUPO / len(b) * b.loc[b['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "    print(f'{corte}  futuro={len(b):4d}  ganancia=S/{margen - azar:9,.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "2025-10-01  futuro=1509  ganancia=S/61,836.00\n",
    "2026-01-01  futuro= 992  ganancia=S/49,529.49\n",
    "2026-03-01  futuro= 667  ganancia=S/37,016.97\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La ganancia sale positiva en los tres cortes y de tamaño parecido. No era la\n",
    "suerte de una fecha 👍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La lista sin el segmento\n",
    "\n",
    "Si el segmento es la columna que ordena casi todo, mira qué\n",
    "lista sale sin él."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SIN = [c for c in COLS if c != 'segmento']\n",
    "sin_seg = 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'))]),\n",
    "         ['ciudad', 'canal', 'categoria']),\n",
    "    ])),\n",
    "    ('mod', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "]).fit(pasado[SIN], pasado['compro'])\n",
    "\n",
    "futuro['ve3'] = (sin_seg.predict_proba(futuro[SIN])[:, 1] * futuro['monto'] * MARGEN)\n",
    "otra = futuro.nlargest(CUPO, 've3')\n",
    "print(otra.groupby('segmento').size().to_string())\n",
    "print()\n",
    "print('ventas:', int(otra['compro'].sum()),\n",
    "      ' margen: S/', round(otra.loc[otra['compro'] == 1, 'monto'].sum() * MARGEN, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "segmento\n",
    "Horeca         9\n",
    "Mayorista    191\n",
    "\n",
    "ventas: 145  margen: S/ 75651.94\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sigue sin haber bodegas. Quitar la columna no arregla nada, porque el monto y el\n",
    "resto de columnas ya cuentan quién es mayorista.\n",
    "\n",
    "Esto es importante y se malentiende mucho: **borrar la variable no borra\n",
    "el sesgo**. Si quieres que la lista incluya bodegas, la cuota es explícita y\n",
    "funciona; quitar la columna es un gesto que se siente bien y no hace nada 🎭"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Poner la incertidumbre en la entrega\n",
    "\n",
    "Marca en la lista los clientes donde el modelo no está\n",
    "seguro, con la predicción conforme del capítulo 24."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_ent, X_cal, y_ent, y_cal = train_test_split(pasado[COLS], pasado['compro'],\n",
    "                                              test_size=0.3, random_state=42,\n",
    "                                              stratify=pasado['compro'])\n",
    "m_conf = arma_modelo().fit(X_ent, y_ent)\n",
    "p_cal = m_conf.predict_proba(X_cal)\n",
    "fallo = 1 - p_cal[np.arange(len(y_cal)), y_cal.values]\n",
    "k = int(np.ceil((len(fallo) + 1) * 0.80))\n",
    "liston = 1 - np.sort(fallo)[k - 1]\n",
    "\n",
    "conj = m_conf.predict_proba(futuro[COLS]) >= liston\n",
    "futuro['seguro'] = conj.sum(axis=1) == 1\n",
    "\n",
    "lista = futuro.nlargest(CUPO, 'valor_esperado')\n",
    "print('en la lista, el modelo está seguro de:', int(lista['seguro'].sum()), 'de', CUPO)\n",
    "print('aciertan cuando está seguro :',\n",
    "      round(lista.loc[lista['seguro'], 'compro'].mean(), 4))\n",
    "print('aciertan cuando no lo está  :',\n",
    "      round(lista.loc[~lista['seguro'], 'compro'].mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "en la lista, el modelo está seguro de: 163 de 200\n",
    "aciertan cuando está seguro : 0.773\n",
    "aciertan cuando no lo está  : 0.5676\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De los 200 de la lista, el modelo está seguro de 163. Y en esos acierta el\n",
    "77,30%, contra el 56,76% de los 37 donde duda.\n",
    "\n",
    "Una columna más en el archivo y el vendedor sabe a cuáles llamar primero.\n",
    "\n",
    "Es de las cosas que más agradecen y menos cuesta poner. Una probabilidad sola\n",
    "invita a creerle; una probabilidad con un \"de este no estoy segura\" al lado invita\n",
    "a pensar 🧠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de entregar la lista con el índice de pandas\n",
    "\n",
    "Intenta guardar la entrega y mira qué sale."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import tempfile\n",
    "\n",
    "ruta = os.path.join(tempfile.gettempdir(), 'entrega.csv')\n",
    "entrega.to_csv(ruta)\n",
    "vuelta = pd.read_csv(ruta)\n",
    "print(vuelta.columns.tolist())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "['Unnamed: 0', 'cliente_id', 'segmento', 'ciudad', 'canal', 'monto', 'prob', 'valor_esperado']\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el regalito: `Unnamed: 0`. El `to_csv` guardó el\n",
    "índice de pandas como una columna sin nombre, y el archivo que abre el equipo\n",
    "comercial empieza con números que nadie sabe qué son.\n",
    "\n",
    "Y ahora el error de verdad, que es lo que pasa cuando alguien asume que la\n",
    "primera columna es el cliente:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vuelta.iloc[:, 0].str.startswith('C').all()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "AttributeError: Can only use .str accessor with string values, not integer\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se arregla con `entrega.to_csv(ruta, index=False)`, y es el último\n",
    "paso del proyecto, el que nadie revisa, y el que hace que tu trabajo llegue bien o\n",
    "llegue raro 📤"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La página que va con el modelo\n",
    "\n",
    "Genera el resumen que acompaña la entrega, con números y no\n",
    "con adjetivos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "resumen = {\n",
    "    'entrenado con': f\"{pasado['fecha'].min().date()} a {pasado['fecha'].max().date()}\",\n",
    "    'filas de entrenamiento': len(pasado),\n",
    "    'columnas': len(COLS),\n",
    "    'AUC próximo trimestre': round(roc_auc_score(futuro['compro'], futuro['prob']), 4),\n",
    "    'ventas de la lista': int(por_valor['compro'].sum()),\n",
    "    'ventas al azar': round(CUPO * futuro['compro'].mean(), 1),\n",
    "    'margen de la lista': round(por_valor.loc[por_valor['compro'] == 1, 'monto'].sum() * MARGEN, 2),\n",
    "    'precio de la cuota por segmento': round(\n",
    "        por_valor.loc[por_valor['compro'] == 1, 'monto'].sum() * MARGEN\n",
    "        - con_cuota.loc[con_cuota['compro'] == 1, 'monto'].sum() * MARGEN, 2),\n",
    "    'no sabe hacer': 'clientes nuevos (ahí baja a 0.6843)',\n",
    "}\n",
    "for k, v in resumen.items():\n",
    "    print(f'{k:32} {v}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "entrenado con                    2025-01-01 a 2025-12-31\n",
    "filas de entrenamiento           2008\n",
    "columnas                         13\n",
    "AUC próximo trimestre            0.7227\n",
    "ventas de la lista               147\n",
    "ventas al azar                   114.1\n",
    "margen de la lista               76302.41\n",
    "precio de la cuota por segmento  32869.86\n",
    "no sabe hacer                    clientes nuevos (ahí baja a 0.6843)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nueve líneas. Eso es el proyecto entero contado para alguien que no va a leer tu\n",
    "código nunca.\n",
    "\n",
    "Si solo te llevas una cosa del libro, que sea esta: **el trabajo termina\n",
    "cuando alguien puede tomar una decisión con lo que le diste**, no cuando el\n",
    "modelo entrena 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "El proyecto termina con una lista de 200 clientes. ¿Qué lleva cada fila además del nombre?\n",
    "\n",
    "a) La probabilidad y el valor esperado, para poder ordenar por lo que importa\n",
    "\n",
    "b) Solo la probabilidad, que es lo que el modelo predice\n",
    "\n",
    "c) El segmento y la ciudad\n",
    "\n",
    "d) Nada más: la lista ya está ordenada por el modelo\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Un cliente con 60% y ticket de 3.000 vale más que uno con 80% y ticket de 100.\n",
    "\n",
    "*c)* Sirven de contexto, pero no ordenan la lista.\n",
    "\n",
    "*d)* Ordenada por probabilidad, que no es lo mismo que ordenada por plata.\n",
    "\n",
    "Lo que se entrega no es un modelo: es a quién llamar mañana."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el número de la última diapositiva"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "El entregable final: la lista de 200 clientes captura 147 ventas y S/76.302 de margen. Está medido sobre datos reales y es el número que va en la última diapositiva.\n",
    "\n",
    "```\n",
    "lista = ordenados.head(200)\n",
    "\n",
    "margen = lista['margen'].sum()\n",
    "print(margen)   # 76302\n",
    "\n",
    "# \"el modelo genera S/76.302\"\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "El modelo no genera esos S/76.302. Los **encuentra**, que no es lo mismo 🎯 Una parte de esos 200 clientes habría comprado igual sin que nadie los llamara, y ese trozo no es mérito del modelo: ya estaba ahí.\n",
    "\n",
    "Lo que el modelo aporta de verdad es la diferencia contra lo que habría pasado sin él, y eso no se lee en la lista. Se mide comparando con lo que consigue el criterio que ya usaban, o dejando un grupo sin llamar y viendo qué hace.\n",
    "\n",
    "Prometer los S/76.302 enteros es la forma más rápida de que el proyecto siguiente no exista, porque a los seis meses alguien va a comparar la facturación real contra esa promesa y no van a cuadrar. Se entrega el número con la frase al lado: **esto es lo que captura la lista, y esto otro es lo que habría pasado sin ella**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🧭 La partición se elige por la pregunta: 0,7227 para la cartera actual y\n",
    "0,6843 para clientes nuevos. Las dos son ciertas.\n",
    "\n",
    "- 📢 A la reunión no se lleva el AUC, se lleva \"163 ventas contra 114\".\n",
    "\n",
    "- 💰 Se ordena por valor esperado, no por probabilidad: 16 ventas menos y\n",
    "S/19.443 más de margen.\n",
    "\n",
    "- 😬 La lista óptima dejó fuera a las 237 bodegas del trimestre, y 86 de ellas\n",
    "compraron.\n",
    "\n",
    "- 🤝 La cuota por segmento cuesta S/32.870 y 15 ventas. Tu trabajo es poner ese\n",
    "precio sobre la mesa, no decidir.\n",
    "\n",
    "- 🎭 Quitar la columna del segmento no quita el sesgo: la lista sigue sin\n",
    "bodegas.\n",
    "\n",
    "- 📤 Se entrega un archivo con nombres, probabilidad y valor, más una página de\n",
    "resumen y una fecha para volver a mirar.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El modelo no genera el dinero. Lo encuentra. Y confundir esas dos cosas es lo que hace que no haya proyecto siguiente."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y si quieres seguir por el camino de la carrera y no solo del modelo, eso está en [cómo ser analista de datos](https://missyera.com/blog/ser-analista-de-datos/) 💼\n",
    "\n",
    "Y hasta aquí el libro. Veinticuatro capítulos desde \"qué es un modelo\" hasta una lista\n",
    "de 200 clientes con su precio al lado 🎓\n",
    "\n",
    "Si llegaste hasta acá haciendo los ejercicios, ya sabes más de machine learning\n",
    "aplicado que mucha gente que lo pone en el perfil. En serio.\n",
    "\n",
    "En el capítulo 29 te dejo dónde seguir: la documentación que sí vale la pena,\n",
    "los libros que yo leí y las cosas que este libro no cubre.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y cuando el modelo tiene que salir del cuaderno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tienes un modelo entrenado, con su umbral elegido por costo y su lista de clientes priorizada. Eso ya es más de lo que mucha gente que pone \"machine learning\" en el perfil sabe hacer 🎓\n",
    "\n",
    "Lo que este libro no cubre es lo que pasa después: que ese modelo corra solo cada semana, que alguien se entere cuando empiece a fallar, y que puedas demostrar por qué decidió lo que decidió. Eso ya no es modelar, es ingeniería, y es justo lo que trabajamos en [el curso de ingeniería de IA en producción](https://missyera.com/cursos/ingenieria-de-ia/).\n",
    "\n",
    "Son cuatro semanas y el entregable es un sistema tuyo, sobre un proceso real de tu trabajo, con las mediciones que demuestran que acierta. No un proyecto de ejemplo con datos de juguete.\n",
    "\n",
    "Si por ahora te quedas en el cuaderno, tranqui, no pasa nada. El capítulo de referencias te deja por dónde seguir sola sin gastar un sol, y si prefieres que te lo explique en video en vez de leerlo, el bloque de machine learning de [los cursos de datos grabados](https://missyera.com/cursos/datos/) es justo esto mismo dado en clase 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 28 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/proyecto-final/\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
}
