{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Gobernanza de datos\n",
    "\n",
    "Lo que una senior escribe antes de tocar un modelo, y que casi nunca aparece: el perfil de calidad, los sesgos declarados y el número que decide si el proyecto sirve.\n",
    "\n",
    "Cuaderno de práctica del capítulo 3 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/gobernanza-y-sesgos/\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: \"ZGUgMjAyNS0wMS0wMSBhIDIwMjYtMDYtMjQKbWVzZXM6IDE4CnZlbnRhcyBwb3IgbWVzOiBkZSAxMjIgYSAxOTIKZWwgdWx0aW1vIG1lczogMTIy\",\n",
    "    2: \"ICAgICAgICAgICAgICB0YXNhICB2ZW50YXMgIHZzX2dsb2JhbApzZWdtZW50byAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCkJvZGVnYSAgICAgIDAuMzc0OCAgICAgNzA3ICAgIC0wLjIwMjgKSG9yZWNhICAgICAgMC42MzIxICAgICA3NDIgICAgIDAuMDU0NApNYXlvcmlzdGEgICAwLjcyNDAgICAgIDc1MCAgICAgMC4xNDYzCk1pbmltYXJrZXQgIDAuNTY5MyAgICAgODAxICAgIC0wLjAwODQ=\",\n",
    "    3: \"bWVkaWEgaWdub3JhbmRvIGh1ZWNvczogMC4xMjUyCm1lZGlhIGNvbiBodWVjb3MgPSAwOiAgIDAuMTAwNAoKdGFzYSBkZSBjb21wcmEgY29uIGRlc2N1ZW50bzogICAgMC42MDE3CnRhc2EgZGUgY29tcHJhIHNpbiBkZXNjdWVudG86ICAgIDAuNDgwNw==\",\n",
    "    4: \"ICAgICAgICAgICAgICBjb2x1bW5hICAgICAgICAgICB0aXBvICBudWxvcyAgIHBjdCAgZGlzdGludG9zICAgICAgICAgICAgIGF2aXNvCiAgICAgICAgICAgICBpZF92ZW50YSAgICAgICAgICBpbnQ2NCAgICAgIDAgIDAuMDAgICAgICAgMzAwMCAgICAgaWRlbnRpZmljYWRvcgogICAgICAgICAgICAgICAgZmVjaGEgZGF0ZXRpbWU2NFt1c10gICAgICAwICAwLjAwICAgICAgICA1MzcgICAgICAgICAgICAgICAgICAKICAgICAgICAgICBjbGllbnRlX2lkICAgICAgICAgICAgc3RyICAgICAgMCAgMC4wMCAgICAgICAgNjE3ICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgIGNpdWRhZCAgICAgICAgICAgIHN0ciAgICAgIDAgIDAuMDAgICAgICAgICAgNiAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgc2VnbWVudG8gICAgICAgICAgICBzdHIgICAgICAwICAwLjAwICAgICAgICAgIDQgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGNhbmFsICAgICAgICAgICAgc3RyICAgICAgMCAgMC4wMCAgICAgICAgICA0ICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGNhdGVnb3JpYSAgICAgICAgICAgIHN0ciAgICAgIDAgIDAuMDAgICAgICAgICAgNSAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgdW5pZGFkZXMgICAgICAgICAgaW50NjQgICAgICAwICAwLjAwICAgICAgICAgMzAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIG1vbnRvICAgICAgICBmbG9hdDY0ICAgICAgMCAgMC4wMCAgICAgICAyOTY0IDIxIGZ1ZXJhIGRlIHJhbmdvCiAgICAgICAgICAgIGRlc2N1ZW50byAgICAgICAgZmxvYXQ2NCAgICA1OTUgMTkuODMgICAgICAgIDI1MSAgICAgICAgICAgICAgICAgIAogIGZlY2hhX3VsdGltYV9jb21wcmEgZGF0ZXRpbWU2NFt1c10gICAgNTQ0IDE4LjEzICAgICAgICAzOTkgICAgICAgICAgICAgICAgICAKICAgICAgICAgc2F0aXNmYWNjaW9uICAgICAgICBmbG9hdDY0ICAgIDIzMSAgNy43MCAgICAgICAgICA1ICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgIGNvbXBybyAgICAgICAgICBpbnQ2NCAgICAgIDAgIDAuMDAgICAgICAgICAgMiAgICAgICAgICAgICAgICAgIAptb250b19maW5hbF9mYWN0dXJhZG8gICAgICAgIGZsb2F0NjQgICAgICAwICAwLjAwICAgICAgIDE3MjMgICAgICAgICAgICAgICAgICA=\",\n",
    "    5: \"c29sbyBlbCBzZWdtZW50byAgIEFVQyAwLjYzNDYgLT4gMTM5LjggY2llcnJlcyBkZSAyMDAgbGxhbWFkYXMgLT4gUy8gNzk3NDkKbWluaW1vICAgICAgICAgICAgIEFVQyAwLjcwMDAgLT4gMTUxLjUgY2llcnJlcyBkZSAyMDAgbGxhbWFkYXMgLT4gUy8gODY0NjYKb2JqZXRpdm8gICAgICAgICAgIEFVQyAwLjc1MDAgLT4gMTYwLjUgY2llcnJlcyBkZSAyMDAgbGxhbWFkYXMgLT4gUy8gOTE2MDI=\",\n",
    "    6: \"RklDSEEgREUgR09CRVJOQU5aQQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnVuaWRhZCBkZSBhbmFsaXNpczogdW5hIHZlbnRhCmZpbGFzOiAzMDAwIHwgY2xpZW50ZXM6IDYxNyB8IHBlcmlvZG86IDIwMjUtMDEtMDEgYSAyMDI2LTA2LTI0CmV0aXF1ZXRhOiBjb21wcm8gKDEgPSBzZSBjZXJybyksIHRhc2EgYmFzZSAwLjU3NzcKY29sdW1uYXMgY29uIGh1ZWNvczogZGVzY3VlbnRvIDE5LjglLCBmZWNoYV91bHRpbWFfY29tcHJhIDE4LjElLCBzYXRpc2ZhY2Npb24gNy43JQpzZW5zaWJsZXM6IGNpdWRhZCAobWVkaWRhLCBubyBkaXNjcmltaW5hKQp1bWJyYWw6IHBpc28gMC42MzQ2IHwgbWluaW1vIDAuNzAgfCBvYmpldGl2byAwLjc1CnJpZXNnbyBkZWNsYXJhZG86IGxvcyBncnVwb3MgZXN0YW4gZGVtYXNpYWRvIGVxdWlsaWJyYWRvcywKICBwcm9iYWJsZW1lbnRlIGVzIHVuYSBtdWVzdHJhIHkgbm8gZWwgdm9sY2FkbyBlbnRlcm8=\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este capítulo es el que separa a alguien que entrena modelos de alguien que\n",
    "entrega proyectos 🎓\n",
    "\n",
    "Empiezo preguntándote algo: **¿alguna vez te han pedido un modelo sin decirte contra qué se iba a comparar?** A mí muchas veces, y siempre acaba igual: el que decide si sirve es quien lo mira, y decide después de verlo 🎯\n",
    "\n",
    "En el capítulo 2 escribimos el contrato: qué es una fila, qué significa la\n",
    "etiqueta, en qué momento se predice. Ahora falta lo otro, que es igual de\n",
    "aburrido de escribir y salva igual de proyectos:\n",
    "\n",
    "- 📋 **El perfil de calidad.** Columna por columna, qué tan\n",
    "confiable es lo que hay dentro.\n",
    "\n",
    "- ⚖️ **Los sesgos declarados.** Quién está de más y quién está de\n",
    "menos en estos datos.\n",
    "\n",
    "- 🔒 **La política de lo sensible.** Qué columnas no se usan, y\n",
    "por qué.\n",
    "\n",
    "- 🎯 **El umbral de aceptación.** A partir de qué número el\n",
    "proyecto sirve. Escrito ANTES de ver el resultado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El perfil de calidad, en una tabla"
   ]
  },
  {
   "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",
    "\n",
    "\n",
    "def carga_limpia(url):\n",
    "    \"\"\"La del capítulo 4, resumida.\"\"\"\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',\n",
    "                                  errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "\n",
    "v = carga_limpia(URL)\n",
    "\n",
    "perfil = pd.DataFrame({\n",
    "    'tipo': v.dtypes.astype(str),\n",
    "    'nulos': v.isna().sum(),\n",
    "    'pct_nulos': (100 * v.isna().mean()).round(2),\n",
    "    'distintos': v.nunique(),\n",
    "})\n",
    "print(perfil.to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa tabla es la primera página del informe, y se lee de una 👀\n",
    "\n",
    "Tres columnas tienen huecos y ninguna es poca cosa:\n",
    "**descuento** con el 19,83%, **fecha_ultima_compra**\n",
    "con el 18,13% y **satisfaccion** con el 7,70%. Eso son tres\n",
    "decisiones que hay que tomar antes de modelar, no tres detalles.\n",
    "\n",
    "Y la columna de valores distintos ya delata los tipos, que es el capítulo\n",
    "siguiente: 3.000 valores distintos en 3.000 filas es un identificador, y 4 o 5\n",
    "valores es una categoría por mucho que esté escrita con números."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los duplicados, contra la clave de negocio"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`drop_duplicates()` quita filas idénticas. Pero el duplicado que\n",
    "duele es otro: **la misma venta cargada dos veces con distinto\n",
    "identificador**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('filas:            %d' % len(v))\n",
    "print('id_venta unicos:  %d' % v['id_venta'].nunique())\n",
    "print('duplicados por cliente + fecha + monto: %d'\n",
    "      % v.duplicated(subset=['cliente_id', 'fecha', 'monto']).sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Limpio ✅ Pero fíjate en que lo comprobé contra la clave de negocio\n",
    "(*quién, cuándo, cuánto*) y no contra el identificador, que por definición\n",
    "nunca se repite.\n",
    "\n",
    "Si esto hubiera salido con 40 duplicados, todo lo que viene después estaría\n",
    "contando cuarenta ventas dos veces, y el modelo creería tener más evidencia de\n",
    "la que tiene."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los rangos que no pueden ser"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['unidades', 'monto', 'descuento', 'satisfaccion']:\n",
    "    s = v[col]\n",
    "    print('%-13s de %9.2f a %9.2f | negativos %3d | ceros %3d'\n",
    "          % (col, s.min(), s.max(), (s < 0).sum(), (s == 0).sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí están las 21 ventas negativas otra vez 🔴\n",
    "\n",
    "Lo importante de este paso no es encontrarlas: es **escribir qué rango\n",
    "es posible para cada columna antes de mirar**. Un descuento va de 0 a 1,\n",
    "una satisfacción de 1 a 5, unas unidades no pueden ser cero. Cuando eso está\n",
    "escrito, el día que llegue un archivo nuevo la comprobación es automática."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los sesgos, declarados"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La pregunta es: *¿estos datos representan al negocio, o representan a una\n",
    "parte?*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['segmento', 'canal', 'ciudad']:\n",
    "    p = v[col].value_counts(normalize=True)\n",
    "    print('%-9s el mas chico %.4f | el mas grande %.4f | desbalance %.2fx'\n",
    "          % (col, p.min(), p.max(), p.max() / p.min()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un desbalance de 1,13 quiere decir que el grupo más representado tiene un 13%\n",
    "más de filas que el menos representado. Eso es **equilibradísimo**,\n",
    "y me hace levantar una ceja 🤨\n",
    "\n",
    "En datos reales de una distribuidora es rarísimo que los cuatro segmentos\n",
    "tengan casi las mismas ventas. Lo anoto en el informe como lo que es: una\n",
    "sospecha de que este archivo fue muestreado, no volcado entero. Y si fue\n",
    "muestreado, **las proporciones que salgan de aquí no son las del\n",
    "negocio**.\n",
    "\n",
    "Eso no invalida el modelo (el modelo aprende relaciones, no proporciones),\n",
    "pero sí invalida cualquier frase del tipo \"el 25% de nuestras ventas son de\n",
    "mayoristas\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo sensible"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí no hay nombres ni documentos de identidad, pero sí hay una columna que\n",
    "merece política: `ciudad`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(v.groupby('ciudad')['compro'].agg(['mean', 'count']).round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las seis ciudades cierran prácticamente igual, así que **usar ciudad no\n",
    "va a discriminar a nadie por dónde vive**, sencillamente porque no aporta\n",
    "información.\n",
    "\n",
    "Pero fíjate en el orden de la frase, porque importa: primero se mide, después\n",
    "se decide. Si Cusco cerrara el 20% y Lima el 80%, meter ciudad en el modelo\n",
    "significaría que a un cliente de Cusco se le llama menos *por ser de\n",
    "Cusco*, y eso ya no es una decisión técnica 🔒\n",
    "\n",
    "La política que escribo es esta: *ciudad entra al modelo porque no\n",
    "discrimina; si en una versión futura empieza a pesar, se saca y se documenta*."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora el que casi nadie escribe"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El umbral de aceptación. **¿A partir de qué número este proyecto vale\n",
    "la pena?**\n",
    "\n",
    "La tentación es decir \"un AUC de 0,80 estaría bien\". Ese número no sale de\n",
    "ningún sitio. Lo que hay que hacer es medir contra qué se compara:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.dummy import DummyClassifier\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "cat = ['segmento', 'canal', 'categoria', 'ciudad']\n",
    "num = ['unidades', 'monto']\n",
    "X, y = v[cat + num], v['compro']\n",
    "Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=7,\n",
    "                                      stratify=y)\n",
    "\n",
    "tonto = DummyClassifier(strategy='stratified', random_state=7).fit(Xtr, ytr)\n",
    "print('1. tirar una moneda:     %.4f'\n",
    "      % roc_auc_score(yte, tonto.predict_proba(Xte)[:, 1]))\n",
    "\n",
    "solo_seg = Pipeline([\n",
    "    ('pre', ColumnTransformer([('c', OneHotEncoder(handle_unknown='ignore'),\n",
    "                                ['segmento'])])),\n",
    "    ('clf', LogisticRegression(max_iter=1000)),\n",
    "]).fit(Xtr[['segmento']], ytr)\n",
    "print('2. solo el segmento:     %.4f'\n",
    "      % roc_auc_score(yte, solo_seg.predict_proba(Xte[['segmento']])[:, 1]))\n",
    "\n",
    "completo = Pipeline([\n",
    "    ('pre', ColumnTransformer([\n",
    "        ('cat', OneHotEncoder(handle_unknown='ignore'), cat),\n",
    "        ('num', StandardScaler(), num)])),\n",
    "    ('clf', LogisticRegression(max_iter=1000)),\n",
    "]).fit(Xtr, ytr)\n",
    "print('3. las seis columnas:    %.4f'\n",
    "      % roc_auc_score(yte, completo.predict_proba(Xte)[:, 1]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo despacio, porque esto cambia el proyecto entero 😳\n",
    "\n",
    "Una sola columna, el segmento, consigue **0,6346**. Las seis\n",
    "juntas consiguen **0,6554**. Las otras cinco columnas, todas\n",
    "juntas, aportan **dos centésimas**.\n",
    "\n",
    "Y eso hay que escribirlo en el informe ANTES de seguir, porque significa\n",
    "una cosa muy concreta: **si al final el modelo saca 0,66, no hacía falta\n",
    "ningún modelo**. Bastaba con una tabla de cuatro filas que diga la tasa\n",
    "de cierre de cada segmento.\n",
    "\n",
    "El umbral de aceptación que escribo es este:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Nivel | AUC | Qué significa |\n",
    "|---|---|---|\n",
    "| Piso | 0,6346 | Lo que da mirar solo el segmento. Por debajo, el modelo estorba |\n",
    "| Mínimo para seguir | 0,70 | Suficiente por encima del piso como para justificar mantenerlo |\n",
    "| Objetivo | 0,75 | Lo que haría falta para cambiar cómo se reparte el equipo comercial |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los capítulos 5 y 9 construyen columnas nuevas, y ahí se ve si se llega. Lo\n",
    "que no se puede hacer es fijar el umbral *después* de ver el resultado,\n",
    "que es exactamente lo que pasa cuando nadie lo escribió antes 📝"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error del capítulo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copio el chequeo de duplicados de otro proyecto, donde la columna de dinero\n",
    "se llamaba distinto:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    v.duplicated(subset=['cliente_id', 'fecha', 'monto_total'])\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "KeyError: Index(['monto_total'], dtype='str')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te frena, y eso está bien 👍 Copiar un script de gobernanza de un proyecto a\n",
    "otro es lo normal, y este error es lo que te obliga a mirar si las columnas se\n",
    "llaman igual.\n",
    "\n",
    "Ahora el que **no** te frena, que es el que quería enseñarte.\n",
    "Quiero el perfil de calidad de golpe con `describe()`, que es lo que\n",
    "hace todo el mundo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(v.describe(include='all').loc['mean'].to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira lo que devuelve 😳\n",
    "\n",
    "La media de `id_venta` es 11.499,50, que es el número de factura\n",
    "promedio y no existe. La media de `fecha` es el 29 de septiembre de\n",
    "2025, que es la fecha promedio de las ventas y tampoco significa nada. Y las\n",
    "columnas de texto salen como `NaN`, o sea que en una tabla de catorce\n",
    "filas hay cinco que son huecos y dos que son disparates.\n",
    "\n",
    "Y ninguna dio error 😑\n",
    "\n",
    "La lección: **el perfil de calidad no es `describe()`**.\n",
    "describe() te da estadísticos; un perfil te dice cuántos huecos hay, cuántos\n",
    "valores distintos, qué tipo tiene cada columna y si el rango es posible. Son\n",
    "cosas distintas y la primera no sustituye a la segunda."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Practica 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. La cobertura temporal, que decide el split\n",
    "\n",
    "Mira cuánto tiempo cubren los datos y si todos los meses\n",
    "pesan igual. De eso depende cómo hay que partir en entrenamiento y test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La tasa base por grupo, que es la línea de fairness\n",
    "\n",
    "Antes de entrenar, deja escrito qué tasa de compra tiene\n",
    "cada segmento. Es contra eso que se va a comparar el modelo después."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Qué pasa si el hueco significa cero\n",
    "\n",
    "La columna descuento tiene 19,83% de nulos. Mide cuánto\n",
    "cambia la conclusión según qué supongas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Una función de perfil que puedas reutilizar\n",
    "\n",
    "Empaqueta el perfil de calidad para poder correrlo sobre\n",
    "cualquier archivo nuevo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El umbral, traducido a plata\n",
    "\n",
    "Un AUC no le dice nada a nadie. Traduce la diferencia entre\n",
    "0,6346 y 0,75 a algo que se pueda defender."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. La ficha de gobernanza, entera\n",
    "\n",
    "Junta todo en el bloque que iría al principio del informe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Antes de entrenar mides que una sola columna, el segmento, da un AUC de 0,6346. ¿Para qué sirve ese número?\n",
    "\n",
    "a) Es el piso: por debajo de ahí el modelo estorba en vez de ayudar\n",
    "\n",
    "b) Para saber que el segmento es la variable más importante\n",
    "\n",
    "c) Para descartar las otras cinco columnas\n",
    "\n",
    "d) Para nada, porque el modelo final va a usar todas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La que parece que arregla el sesgo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Te piden que el modelo \"no discrimine por ciudad\". Alguien lo resuelve en una línea y cierra el ticket. Léelo despacio.\n",
    "\n",
    "```\n",
    "X = ventas.drop(columns=['ciudad'])\n",
    "\n",
    "# \"listo, ya no puede discriminar\n",
    "#  por ciudad\"\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": [
    "- 📋 El perfil de calidad va antes que el modelo. Aquí: tres columnas con\n",
    "huecos, la peor con el 19,83%.\n",
    "\n",
    "- 🔁 Los duplicados se buscan contra la clave de negocio (quién, cuándo,\n",
    "cuánto), no contra el identificador, que nunca se repite.\n",
    "\n",
    "- ⚖️ Los grupos están equilibrados a 1,13x, que es sospechosamente parejo para\n",
    "datos reales. Se declara como riesgo.\n",
    "\n",
    "- 🎯 **El umbral de aceptación se escribe antes de ver el\n",
    "resultado.** Aquí el piso es 0,6346, que es lo que da mirar una sola\n",
    "columna.\n",
    "\n",
    "- 🕳️ El hueco de descuento no es aleatorio: predice la compra. Eso se\n",
    "convierte en columna, no se tapa.\n",
    "\n",
    "- 📄 El riesgo declarado es la línea más difícil de escribir y la que hace que\n",
    "te crean.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El umbral se escribe antes. Después ya no es un umbral, es una excusa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y si lo que te falta para escribir ese umbral es la estadística de debajo, está entera en el [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📊"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con la ficha escrita, toca mirar los datos de verdad 🕵️\n",
    "\n",
    "El capítulo 4 limpia lo que está sucio: las filas repetidas, las cuatro\n",
    "formas de escribir Lima, el dinero que se evapora al convertirlo y los dos\n",
    "formatos de fecha metidos en la misma columna.\n",
    "\n",
    "Y de ahí en adelante el libro sigue el orden que a mí me costó años aprender,\n",
    "que es el contrario del que parece:\n",
    "\n",
    "- 🔍 **Primero se entiende lo que hay.** Limpiar, auditar tipos,\n",
    "mirar cada columna por separado y luego contra la etiqueta.\n",
    "\n",
    "- 🧱 **Después se construye.** Las columnas que no venían en el\n",
    "archivo y que son las que de verdad mueven la aguja.\n",
    "\n",
    "- 🤖 **Y el modelo llega casi al final**, cuando ya está decidido\n",
    "todo lo que importa.\n",
    "\n",
    "Si el modelado te ocupa más tiempo que entender los datos, lo que estás\n",
    "escribiendo es un script y no un proyecto. Esa proporción es la que separa a una\n",
    "senior de alguien que sabe llamar a `.fit()` 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 3 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/gobernanza-y-sesgos/\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
}
