{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El contrato de datos\n",
    "\n",
    "Las cuatro preguntas que deciden si el proyecto sirve, y las cuatro tienen respuesta medible en el archivo.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 2 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/el-contrato-de-datos/\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": [
    "Este es el capítulo que casi ningún curso enseña y el que decide si el\n",
    "proyecto sirve o no sirve 💛\n",
    "\n",
    "Y antes de nada, una pregunta para ti: **¿cuál fue el último informe que hiciste y que nadie usó para decidir nada?** Piénsalo, porque casi siempre no falló el análisis. Faltó esta conversación 💭\n",
    "\n",
    "Va antes del código a propósito, porque todo lo que viene después depende de\n",
    "él. Y no es teoría de consultora: son cuatro preguntas concretas que hay que\n",
    "hacer, y las cuatro tienen respuesta medible en el archivo.\n",
    "\n",
    "La escena de siempre: \"Yera, queremos un modelo que prediga qué clientes van\n",
    "a comprar\". Suena clarísimo y no lo es. Vamos a ver por qué."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pregunta 1. ¿Qué se va a hacer distinto?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes que nada, esta. Si la respuesta es \"tener el dato\", el proyecto no\n",
    "empieza.\n",
    "\n",
    "Un modelo no sirve para saber: sirve para **decidir distinto**.\n",
    "Y esa decisión tiene que existir antes de que exista el modelo.\n",
    "\n",
    "- ✅ \"Vamos a llamar a los 200 clientes con más probabilidad de comprar.\"\n",
    "Perfecto: hay una acción, un número de llamadas y alguien que las hace.\n",
    "\n",
    "- ✅ \"Vamos a darle 10% de descuento a los que están a punto de irse.\"\n",
    "Perfecto también, y encima define el costo de equivocarse.\n",
    "\n",
    "- ❌ \"Queremos entender mejor a nuestros clientes.\" Eso no es un modelo, es un\n",
    "análisis, y es más barato y más útil. Capítulo 14 del\n",
    "[libro de SQL](https://missyera.com/guias/sql-desde-cero/).\n",
    "\n",
    "- ❌ \"Para el directorio.\" Ese proyecto muere en tres meses, y lo digo con\n",
    "cariño 🫠\n",
    "\n",
    "De esa respuesta salen dos cosas que vas a necesitar sí o sí: **cuántas\n",
    "predicciones se pueden atender** (si el call center hace 200 llamadas,\n",
    "tu lista tiene 200 nombres, no 3.000) y **cuánto cuesta cada tipo de\n",
    "error**. Las dos vuelven en el capítulo 15."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pregunta 2. ¿Una fila es qué, exactamente?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí es donde se cae la mitad de los proyectos, y se ve en un número."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "ventas = pd.read_csv(URL)\n",
    "\n",
    "print('filas:', len(ventas))\n",
    "print('clientes distintos:', ventas['cliente_id'].nunique())\n",
    "print('tasa por venta:', round(ventas['compro'].mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3.037 ventas de 617 clientes, y el 57,7% de las ventas se cierra.\n",
    "\n",
    "Ahora la misma pregunta mirando clientes en vez de ventas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "por_cliente = ventas.groupby('cliente_id')['compro'].max()\n",
    "print('clientes que compraron alguna vez:', round(por_cliente.mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**93%.** Contra el 57,7% de antes.\n",
    "\n",
    "Los dos números son correctos y contestan preguntas distintas. \"¿Se cierra\n",
    "esta venta?\" es 57,7%. \"¿Este cliente compra alguna vez?\" es 93%. Si el cliente\n",
    "te pide lo segundo y tú entrenas lo primero, entregas un modelo impecable que no\n",
    "sirve 😳\n",
    "\n",
    "Y hay un tercer corte, que suele ser el que de verdad quieren:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tasa = ventas.groupby('cliente_id')['compro'].mean()\n",
    "print('clientes que compran siempre:', round((tasa == 1).mean(), 4))\n",
    "print('clientes que no compran nunca:', round((tasa == 0).mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Solo el 11,8% compra siempre y el 7% no compra nunca. O sea que\n",
    "**el 81% de los clientes a veces sí y a veces no**, y esos son los\n",
    "interesantes: los que dependen de algo que puedes cambiar.\n",
    "\n",
    "La pregunta que hay que hacer en la reunión, palabra por palabra:\n",
    "*¿una fila de tu reporte es una venta o es un cliente?* 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pregunta 3. ¿Qué significa exactamente la etiqueta?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"Compró\" parece que no necesita definición. Sí la necesita.\n",
    "\n",
    "¿Compró si pagó? ¿Si le llegó el pedido? ¿Y si devolvió? ¿Y si compró\n",
    "S/12 cuando el mínimo rentable son S/300?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "monto = pd.to_numeric(ventas['monto'].str.replace(',', '.'), errors='coerce')\n",
    "venta_grande = ((ventas['compro'] == 1) & (monto > 500)).astype(int)\n",
    "\n",
    "print('objetivo \"cerró\":        ', round(ventas['compro'].mean(), 4))\n",
    "print('objetivo \"cerró y > 500\":', round(venta_grande.mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "57,7% contra 35,2%. **Es el mismo archivo y son dos problemas\n",
    "distintos**, con distinto listón, distinta dificultad y distinto valor\n",
    "para el negocio.\n",
    "\n",
    "Y fíjate en que la segunda definición es más útil casi siempre: al call\n",
    "center no le sirve saber que alguien va a comprar un chicle 🍬\n",
    "\n",
    "La definición del objetivo se escribe, se firma y se pega arriba del cuaderno.\n",
    "Cambiarla a mitad de proyecto significa tirar todo lo medido."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pregunta 4. ¿En qué momento se predice?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta es la que más caro sale y la que nadie hace. Se llama el\n",
    "**momento de la predicción**: qué se sabe y qué no se sabe cuando\n",
    "el modelo tiene que contestar.\n",
    "\n",
    "Mira esta columna del archivo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas.groupby('compro')['monto_final_facturado'].agg(['count', 'min', 'max', 'mean']).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todas las ventas que no se cerraron tienen exactamente 0,00. Todas las que sí,\n",
    "tienen un monto.\n",
    "\n",
    "O sea que esa columna **es la respuesta escrita de otra forma**.\n",
    "Un modelo que la use va a acertar el 100%, y va a fallar el 100% en producción,\n",
    "porque el día que tienes que predecir todavía no se facturó nada.\n",
    "\n",
    "Eso se llama **fuga de información** y tiene el capítulo 12\n",
    "entero, con la demostración numérica. Aquí me interesa la parte que no es\n",
    "técnica: **se caza preguntando, no programando**. Por cada columna\n",
    "de tu tabla, una pregunta:\n",
    "\n",
    "*Esto, ¿cuándo se rellena?*\n",
    "\n",
    "Si la respuesta es \"cuando se cierra la venta\", fuera. Si es \"el mes\n",
    "siguiente\", fuera. Si es \"cuando el cliente reclama\", fuera. Y ojo, que las\n",
    "peores no se llaman `monto_final_facturado`: se llaman\n",
    "`score_riesgo` o `segmento_asignado` y las calculó otro\n",
    "equipo con información que tú no ves 🕵️‍♀️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El contrato, en una tabla"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto es lo que yo pego arriba del cuaderno antes de escribir una línea:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Punto | Ejemplo con este archivo |\n",
    "|---|---|\n",
    "| **Decisión** | Qué llamadas hace el equipo comercial esta semana |\n",
    "| **Capacidad** | 200 llamadas por semana, o sea 200 nombres |\n",
    "| **Unidad** | Una fila es una oportunidad de venta |\n",
    "| **Objetivo** | `compro = 1`, o sea que se cerró |\n",
    "| **Momento** | Al registrarse la oportunidad, antes de gestionarla |\n",
    "| **Prohibido** | `monto_final_facturado`: solo existe después |\n",
    "| **Costo de fallar** | Llamar de más: 15 minutos. No llamar a quien iba a comprar: la venta entera |\n",
    "| **Listón** | 57,7% de cierre sin modelo |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ocho líneas. Escribirlas cuesta una reunión de cuarenta minutos y evita el\n",
    "mes que se pierde cuando alguien dice \"ah, pero yo pensaba que era por\n",
    "cliente\" 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El otro contrato: el de las columnas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y hay una parte más aburrida que también hay que acordar, porque si no un\n",
    "lunes te llega el archivo con las columnas renombradas y todo revienta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas.dtypes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí ya hay dos avisos: `monto` es texto cuando debería ser un\n",
    "número (viene con coma decimal), y `fecha` también es texto. Los dos\n",
    "son el capítulo 4.\n",
    "\n",
    "Lo que se acuerda con quien manda los datos: **los nombres de las\n",
    "columnas, su tipo, sus valores posibles y qué significa un hueco**. Esa\n",
    "última es la que más se olvida y la que más vale, y por eso tiene su propia\n",
    "sección en el capítulo 4 🕳️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Un contrato que no se pueda comprobar no es un contrato"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo de arriba está escrito en prosa, y la prosa se pudre. Dentro de seis\n",
    "meses nadie va a releer esa tabla antes de correr el cuaderno 📄\n",
    "\n",
    "Así que el contrato se escribe en código, y se comprueba solo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CONTRATO = {\n",
    "    'id_venta':     {'tipo': 'int64',   'nulos': 0.0,  'unico': True},\n",
    "    'cliente_id':   {'tipo': 'str',     'nulos': 0.0,  'unico': False},\n",
    "    'monto':        {'tipo': 'float64', 'nulos': 0.05, 'min': 0},\n",
    "    'unidades':     {'tipo': 'int64',   'nulos': 0.0,  'min': 1},\n",
    "    'satisfaccion': {'tipo': 'float64', 'nulos': 0.10, 'min': 1, 'max': 5},\n",
    "    'compro':       {'tipo': 'int64',   'nulos': 0.0,  'valores': {0, 1}},\n",
    "}\n",
    "\n",
    "print(len(CONTRATO), 'columnas con reglas')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada regla es una frase de la tabla de arriba, escrita de forma que una\n",
    "máquina la pueda revisar. \"El monto nunca es negativo\" se convierte en\n",
    "`'min': 0`. \"Como mucho un 10% de satisfacciones sin contestar\" se\n",
    "convierte en `'nulos': 0.10` 🔒\n",
    "\n",
    "Y ahora la función que las revisa. Es más corta de lo que parece:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def revisa(tabla, contrato):\n",
    "    problemas = []\n",
    "    for col, regla in contrato.items():\n",
    "        if col not in tabla.columns:\n",
    "            problemas.append(f'{col}: no está en la tabla')\n",
    "            continue\n",
    "        s = tabla[col]\n",
    "        if str(s.dtype) != regla['tipo']:\n",
    "            problemas.append(f'{col}: es {s.dtype} y el contrato dice {regla[\"tipo\"]}')\n",
    "        hueco = s.isna().mean()\n",
    "        if hueco > regla['nulos']:\n",
    "            problemas.append(f'{col}: {hueco:.1%} de huecos, el tope es {regla[\"nulos\"]:.0%}')\n",
    "        if regla.get('unico') and s.duplicated().any():\n",
    "            problemas.append(f'{col}: hay {s.duplicated().sum()} repetidos y debería ser único')\n",
    "        # Solo si de verdad es numérica. Un validador que revienta al mirar una\n",
    "        # columna rota no sirve: se cae justo cuando hace falta.\n",
    "        numerica = pd.api.types.is_numeric_dtype(s)\n",
    "        if 'min' in regla and numerica and s.min() < regla['min']:\n",
    "            problemas.append(f'{col}: mínimo {s.min()}, el contrato dice {regla[\"min\"]}')\n",
    "        if 'max' in regla and numerica and s.max() > regla['max']:\n",
    "            problemas.append(f'{col}: máximo {s.max()}, el contrato dice {regla[\"max\"]}')\n",
    "        if 'valores' in regla and not set(s.dropna().unique()) <= regla['valores']:\n",
    "            problemas.append(f'{col}: valores fuera de {regla[\"valores\"]}')\n",
    "    return problemas\n",
    "\n",
    "\n",
    "fallos = revisa(ventas, CONTRATO)\n",
    "print(len(fallos), 'incumplimientos')\n",
    "for f in fallos:\n",
    "    print('  -', f)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos, y los dos son de verdad 🔍\n",
    "\n",
    "**37 id_venta repetidos.** El contrato decía que una fila es una\n",
    "venta y que `id_venta` la identifica. Si se repite 37 veces, o hay\n",
    "duplicados o una fila no es lo que creíamos. Las dos posibilidades hay que\n",
    "resolverlas antes de entrenar nada.\n",
    "\n",
    "**El monto es texto.** Ya lo sabíamos, pero fíjate en que no\n",
    "hizo falta que nadie se acordara: la comprobación lo dijo sola.\n",
    "\n",
    "Y una decisión de diseño que parece un detalle: esa línea de\n",
    "`numerica`. Sin ella, la función revienta al intentar comparar el\n",
    "monto de texto con cero, y se cae **antes** de contarte los otros\n",
    "problemas. Un validador tiene que aguantar los datos rotos, porque los datos\n",
    "rotos son su único trabajo 🛡️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que el contrato encuentra después de limpiar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora la parte interesante. Arreglamos los dos problemas y volvemos a\n",
    "pasarlo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "limpias = ventas.drop_duplicates(subset='id_venta').copy()\n",
    "limpias['monto'] = pd.to_numeric(limpias['monto'].astype(str).str.replace(',', '.'),\n",
    "                                 errors='coerce')\n",
    "\n",
    "for f in revisa(limpias, CONTRATO):\n",
    "    print('  -', f)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aparece uno que antes estaba escondido: **hay un monto de\n",
    "-2.497,72** 😳\n",
    "\n",
    "Estaba ahí desde el principio, pero no se podía ver: mientras la columna era\n",
    "texto, la regla del mínimo ni se comprobaba. Limpiar un problema destapó otro,\n",
    "y eso pasa constantemente.\n",
    "\n",
    "De ahí sale la costumbre que quiero que te lleves: **el contrato se\n",
    "vuelve a pasar después de cada limpieza**, no una vez al principio. Cada\n",
    "arreglo cambia lo que se puede comprobar.\n",
    "\n",
    "¿Y qué se hace con ese monto negativo? Depende de qué sea, y eso no lo dicen\n",
    "los datos. Puede ser una devolución mal registrada, un ajuste contable o un\n",
    "dedazo. Preguntar cuál de las tres es exactamente el trabajo de este capítulo:\n",
    "el contrato no es un filtro, es **una lista de preguntas para quien conoce\n",
    "el negocio** 💬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dónde vive esta comprobación"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres sitios, y los tres a la vez 📍\n",
    "\n",
    "- 🧪 **Al cargar los datos, en el cuaderno.** Si el archivo de\n",
    "este mes viene distinto, te enteras en la primera celda y no en la quinta hora.\n",
    "\n",
    "- 🚦 **Antes de entrenar.** Un modelo entrenado sobre datos que\n",
    "incumplen el contrato es un modelo que no se puede defender.\n",
    "\n",
    "- 🌙 **En producción, con los datos que llegan.** Y ahí no para\n",
    "el proceso: avisa. De eso va el capítulo 27.\n",
    "\n",
    "Lo importante es que sea **el mismo código en los tres**. Un\n",
    "contrato que se comprueba de una forma en el cuaderno y de otra en producción es\n",
    "dos contratos, y el día que no cuadren vas a tardar semanas en descubrir cuál de\n",
    "los dos mentía 🌀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cuántas oportunidades tiene cada cliente\n",
    "\n",
    "Distribución de filas por cliente: mínimo, mediana y\n",
    "máximo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas['cliente_id'].value_counts().describe().round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "count    617.00\n",
    "mean       4.92\n",
    "std        2.24\n",
    "min        1.00\n",
    "25%        3.00\n",
    "50%        5.00\n",
    "75%        6.00\n",
    "max       14.00\n",
    "Name: count, dtype: float64\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 1 a 14 oportunidades por cliente, con mediana 5. Ese dato importa para el\n",
    "capítulo 16: si un cliente aparece 14 veces y sus filas caen mitad en\n",
    "entrenamiento y mitad en examen, el modelo lo \"conoce\" cuando no debería.\n",
    "\n",
    "Se llama fuga por grupo y se arregla partiendo por cliente y no por fila 🔒"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El objetivo, definido en soles\n",
    "\n",
    "Cuánto cambia el listón si el objetivo es \"cerró y pasa de\n",
    "S/1.000\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mil = ((ventas['compro'] == 1) & (monto > 1000)).astype(int)\n",
    "print('tasa:', round(mil.mean(), 4))\n",
    "print('positivos:', int(mil.sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "tasa: 0.1906\n",
    "positivos: 579\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 1.753 casos positivos bajamos a 579, o sea del 57,7% al 19,1%. El problema\n",
    "se puso mucho más difícil y también mucho más valioso: esas 579 son las ventas\n",
    "que de verdad mueven el mes.\n",
    "\n",
    "Y ojo con algo: cuando los positivos bajan del 20%, las métricas de siempre\n",
    "empiezan a mentir. Con un 19% de positivos, decir \"no\" a todo el mundo ya acierta\n",
    "el 81% de las veces 🎯 Eso es el capítulo 19 entero.\n",
    "\n",
    "Fíjate en el patrón: **cada vez que el objetivo se afina, el listón\n",
    "sube y el problema se pone más difícil**. No es un motivo para no\n",
    "afinarlo, es un motivo para saberlo antes y no llevarse el susto en la\n",
    "presentación."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La columna prohibida, en una línea\n",
    "\n",
    "Demuestra que `monto_final_facturado` es la\n",
    "respuesta disfrazada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print((ventas['monto_final_facturado'] > 0).astype(int).equals(ventas['compro']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`True`. Esa columna convertida a \"mayor que cero\" **es\n",
    "idéntica al objetivo**, fila por fila, en las 3.037.\n",
    "\n",
    "Esta comprobación cabe en una línea y la hago con todas las columnas\n",
    "sospechosas antes de modelar. Cuando da `True`, la columna se va y\n",
    "no hay discusión 🚫"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Qué se sabe antes y qué después\n",
    "\n",
    "Clasifica las 14 columnas del archivo según si se conocen\n",
    "antes de gestionar la venta o no."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ANTES = ['id_venta', 'fecha', 'cliente_id', 'ciudad', 'segmento', 'canal',\n",
    "         'categoria', 'unidades', 'monto', 'descuento',\n",
    "         'fecha_ultima_compra', 'satisfaccion']\n",
    "\n",
    "DESPUES = ['compro', 'monto_final_facturado']\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No lleva salida porque no es código, es una decisión. Y esa lista de\n",
    "`ANTES` es literalmente la que vas a ver en el capítulo 11 cuando\n",
    "armemos el pipeline.\n",
    "\n",
    "Dos de esas doce son discutibles y merecen preguntarse:\n",
    "`descuento` (¿se ofreció antes o se negoció durante?) y\n",
    "`satisfaccion` (¿es de compras anteriores o de esta?). En este\n",
    "archivo son de antes, y por eso están. En tu trabajo, pregunta 🤔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El error que te va a pasar en la primera reunión\n",
    "\n",
    "El cliente te dice \"agrupa por cliente\" y tú escribes lo\n",
    "que suena."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas.groupby('cliente')['compro'].mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "KeyError: 'cliente'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*KeyError: 'cliente'*. La columna se llama `cliente_id`.\n",
    "\n",
    "Parece una tontería y es justo lo que evita el contrato de columnas: los\n",
    "nombres se escriben una vez, se acuerdan, y no se adivinan. Cuando el archivo\n",
    "tiene 80 columnas y te lo manda otro equipo, esto pasa diez veces al día 🫠"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuántas llamadas caben\n",
    "\n",
    "Si el equipo hace 200 llamadas por semana y hay 3.037\n",
    "oportunidades, ¿qué porcentaje de la lista puedes atender?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('oportunidades:', len(ventas))\n",
    "print('capacidad semanal:', 200)\n",
    "print('porcentaje atendible:', round(100 * 200 / len(ventas), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "oportunidades: 3037\n",
    "capacidad semanal: 200\n",
    "porcentaje atendible: 6.6\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El 6,6%. Y eso cambia el problema entero: no necesitas un modelo que acierte\n",
    "en todas, necesitas uno que **ponga arriba a las 200 mejores**.\n",
    "\n",
    "Es una métrica distinta y tiene nombre, precisión en el top K, y sale en el\n",
    "capítulo 14. Sin la pregunta de la capacidad nunca habrías sabido que era esa la\n",
    "que había que mirar 📞"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El contrato de tu propio proyecto\n",
    "\n",
    "Copia esta plantilla y llénala con un caso tuyo, aunque sea\n",
    "inventado. Es el ejercicio que más te va a servir de todo el capítulo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "DECISIÓN     : ...qué se hace distinto con la predicción\n",
    "CAPACIDAD    : ...cuántos casos se pueden atender\n",
    "UNIDAD       : ...una fila es un/una ...\n",
    "OBJETIVO     : ...la etiqueta vale 1 cuando ...\n",
    "MOMENTO      : ...se predice en el instante en que ...\n",
    "PROHIBIDO    : ...columnas que no existen en ese instante\n",
    "COSTO FN     : ...qué pasa si digo que no y era que sí\n",
    "COSTO FP     : ...qué pasa si digo que sí y era que no\n",
    "LISTÓN       : ...qué se acierta hoy sin modelo\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si alguna línea no la puedes llenar, esa es tu próxima pregunta al cliente. Y\n",
    "si son tres o más las que no puedes llenar, todavía no hay proyecto: hay una\n",
    "idea 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. El negativo, de cerca\n",
    "\n",
    "Antes de preguntar nada, mira quiénes son."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "limpias = ventas.drop_duplicates(subset='id_venta').copy()\n",
    "limpias['monto'] = pd.to_numeric(limpias['monto'].astype(str).str.replace(',', '.'),\n",
    "                                 errors='coerce')\n",
    "\n",
    "neg = limpias[limpias['monto'] < 0]\n",
    "\n",
    "print('cuántos son      :', len(neg))\n",
    "print('qué % de las filas:', f'{len(neg) / len(limpias):.2%}')\n",
    "print('cuánto suman     :', round(neg['monto'].sum(), 2))\n",
    "print('valores de compro:', sorted(neg['compro'].unique()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cuántos son      : 21\n",
    "qué % de las filas: 0.70%\n",
    "cuánto suman     : -8954.04\n",
    "valores de compro: [np.int64(0)]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Veintiuno, el 0,7% de las filas, casi nueve mil soles en negativo. Y lo\n",
    "importante está en la última línea: **los veintiuno tienen compro\n",
    "igual a cero** 🎯\n",
    "\n",
    "Eso ya es una hipótesis con la que ir a preguntar: parece que las\n",
    "devoluciones se registran como visitas sin compra y con el monto en negativo.\n",
    "\n",
    "Pero ojo con darla por buena. Hay 1.246 filas más con `compro` en\n",
    "cero y monto positivo, así que **no todas las no-compras son\n",
    "devoluciones**. Lo único que sabemos es que los negativos son un\n",
    "subconjunto pequeño y muy consistente.\n",
    "\n",
    "Con eso ya se puede escribir el correo: \"encontré 21 filas con monto negativo,\n",
    "todas con compro en cero. ¿Son devoluciones? ¿Las quieres dentro o fuera del\n",
    "análisis?\". Eso es una pregunta que se contesta en dos minutos. \"Hay datos\n",
    "raros\" no 💬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. Las columnas que el contrato no vigila\n",
    "\n",
    "Mira dos que no metí en el contrato y decide si deberían\n",
    "estar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('descuento  min:', limpias['descuento'].min(),\n",
    "      ' max:', limpias['descuento'].max(),\n",
    "      ' nulos:', f\"{limpias['descuento'].isna().mean():.1%}\")\n",
    "print('unidades   min:', limpias['unidades'].min(),\n",
    "      ' max:', limpias['unidades'].max())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "descuento  min: 0.0  max: 0.25  nulos: 19.8%\n",
    "unidades   min: 1  max: 30\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El descuento va de 0 a 0,25 y tiene un 19,8% de huecos 🕳️\n",
    "\n",
    "Ese 19,8% es la respuesta: **una de cada cinco filas no tiene\n",
    "descuento registrado**. Si el contrato dijera `'nulos': 0.05`\n",
    "saltaría siempre, y un aviso que salta siempre se ignora en dos semanas.\n",
    "\n",
    "Así que la regla honesta aquí no es un tope bajo, es un tope realista más la\n",
    "pregunta de si ese hueco significa \"sin descuento\" o \"no lo apuntamos\". Son\n",
    "cosas distintas y cambian el análisis entero.\n",
    "\n",
    "Las unidades, en cambio, van de 1 a 30 sin huecos, así que\n",
    "`'min': 1, 'max': 30` es una regla que puedes poner sin miedo. Y el\n",
    "día que llegue un pedido de 500 unidades, el contrato te va a avisar de que algo\n",
    "cambió en el negocio 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 10. El archivo del mes que viene\n",
    "\n",
    "Simula un archivo con más huecos de la cuenta y comprueba\n",
    "que el contrato lo pilla."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "del_mes = limpias.sample(300, random_state=7).copy()\n",
    "del_mes['satisfaccion'] = del_mes['satisfaccion'].where(del_mes.index % 3 != 0)\n",
    "\n",
    "print('huecos en satisfacción:', f\"{del_mes['satisfaccion'].isna().mean():.1%}\")\n",
    "print()\n",
    "for f in revisa(del_mes, CONTRATO):\n",
    "    print('  -', f)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "huecos en satisfacción: 39.0%\n",
    "\n",
    "  - monto: mínimo -2497.72, el contrato dice 0\n",
    "  - satisfaccion: 39.0% de huecos, el tope es 10%\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "39% de huecos contra un tope del 10%, y el contrato lo dice con el número\n",
    "delante 🚨\n",
    "\n",
    "Esto es exactamente lo que pasa en la vida real: alguien quita el campo de\n",
    "satisfacción del formulario, o lo hace opcional, y nadie avisa al equipo de\n",
    "datos. El modelo sigue corriendo, sigue dando predicciones, y va empeorando\n",
    "despacio.\n",
    "\n",
    "Con el contrato puesto, en la primera celda del cuaderno sale el aviso.\n",
    "**Sin él, te enteras cuando alguien de negocio pregunta por qué el modelo\n",
    "ya no acierta**, tres meses después ⏳"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "¿Cuándo se decide qué significa cada columna?\n",
    "\n",
    "a) Antes de tocar el modelo, y por escrito\n",
    "\n",
    "b) Cuando el modelo dé resultados raros\n",
    "\n",
    "c) Lo decide quien programa, mirando los datos\n",
    "\n",
    "d) No hace falta si el modelo funciona\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Para entonces ya entrenaste con una definición que nadie acordó.\n",
    "\n",
    "*c)* Los datos no dicen de dónde vienen ni quién los mantiene.\n",
    "\n",
    "*d)* Funciona hasta que la fuente cambia sin avisar, y ahí no hay a qué comparar.\n",
    "\n",
    "Una senior define primero qué datos hay y qué decisiones se toman con ellos. Recién después modela."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la que se cuela por el hueco de la pregunta 4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Te pasan este script \"que ya da 100%\". El código corre, la columna existe y el número es real. Mira de dónde sale esa columna antes de seguir.\n",
    "\n",
    "```\n",
    "X = ventas.drop(columns=['compro'])\n",
    "X['facturo'] = (ventas['monto_final_facturado']\n",
    "                > 0).astype(int)\n",
    "\n",
    "# ...y el modelo acierta el 100%\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "La columna `monto_final_facturado` se rellena **después de que la venta se cierra**. Convertirla a \"mayor que cero\" es copiar la respuesta y ponerle otro nombre: en las 3.037 filas de este archivo esa columna es idéntica al objetivo, letra por letra 🎭\n",
    "\n",
    "Por eso la pregunta 4 del contrato es en qué momento se predice. No es burocracia: es lo único que caza esto antes de que el modelo llegue a una reunión. El día que se use de verdad, esa columna estará vacía, porque la venta todavía no ha pasado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🎬 Si nadie va a hacer nada distinto con la predicción, no hay proyecto.\n",
    "\n",
    "- 🧱 La unidad manda: aquí, 57,7% por venta contra 93% por cliente. Son dos\n",
    "preguntas y solo una es la que te pidieron.\n",
    "\n",
    "- 🏷️ El objetivo se define por escrito: \"cerró\" da 57,7% y \"cerró y pasa de\n",
    "S/500\" da 35,2%.\n",
    "\n",
    "- ⏰ El momento de la predicción es lo que caza la fuga, y se caza\n",
    "preguntando por cada columna cuándo se rellena.\n",
    "\n",
    "- 🚫 `monto_final_facturado` convertida a \"mayor que cero\" es\n",
    "idéntica al objetivo en las 3.037 filas. Fuera.\n",
    "\n",
    "- 📞 La capacidad cambia la métrica: con 200 llamadas para 3.037\n",
    "oportunidades, lo que importa es el orden, no el acierto global.\n",
    "\n",
    "- 📋 Ocho líneas de contrato cuestan una reunión y ahorran un mes.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un proyecto de datos no empieza con datos. Empieza con la decisión que va a cambiar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 4 abrimos el archivo de verdad y nos peleamos con la suciedad:\n",
    "Lima escrita de cuatro formas, montos con coma, fechas en dos formatos y tres\n",
    "columnas con huecos.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 2 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/el-contrato-de-datos/\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
}
