{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cada columna contra el objetivo\n",
    "\n",
    "Qué prueba toca según el tipo, por qué el valor p solo no sirve, y las cuatro columnas de ruido puro que salieron significativas.\n",
    "\n",
    "Cuaderno de práctica del capítulo 7 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/eda-bivariado/\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: \"ICAgICAgICAgIGNvbHVtbmEgICAgICAgdGlwbyAgICAgICBwcnVlYmEgICAgICAgICAgICBwICBlZmVjdG8gICAgICBtZWRpZGEgICBwX2FqdXN0YWRhICAgICAgICAgICAgICAgICB2ZXJlZGljdG8KICAgICAgICAgc2VnbWVudG8gY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgNy4xOTI4ODFlLTQyICAwLjI1NDUgViBkZSBDcmFtZXIgNy45MTIxNjllLTQxICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgICAgICAgICAgbW9udG8gICBudW1lcmljYSBtYW5uLXdoaXRuZXkgNi4yOTkzOTVlLTM4ICAwLjYzNzQgICAgICAgICBBVUMgMy40NjQ2NjdlLTM3ICAgICAgICAgICAgICAgICAgICAgZW50cmEKICBwcmVjaW9fdW5pdGFyaW8gICBudW1lcmljYSBtYW5uLXdoaXRuZXkgMi41ODA4MTNlLTI0ICAwLjYwODYgICAgICAgICBBVUMgOS40NjI5ODJlLTI0ICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgICBzYXRpc2ZhY2Npb24gICBudW1lcmljYSBtYW5uLXdoaXRuZXkgMy44MzQ2NjRlLTIxICAwLjYwMjYgICAgICAgICBBVUMgMS4wNTQ1MzNlLTIwICAgICAgICAgICAgICAgICAgICAgZW50cmEKc2luX2NvbXByYV9wcmV2aWEgY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgMS42NzUwNTBlLTE3ICAwLjE1NTUgViBkZSBDcmFtZXIgMy42ODUxMTBlLTE3ICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgICAgICAgICAgY2FuYWwgY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgNC4zMTM5NjZlLTE0ICAwLjE0NzUgViBkZSBDcmFtZXIgNy45MDg5MzhlLTE0ICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgIHNpbl9kZXNjdWVudG8gY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgMS4xMzYwMDllLTA3ICAwLjA5NjggViBkZSBDcmFtZXIgMS43ODUxNThlLTA3ICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgICAgICBkZXNjdWVudG8gICBudW1lcmljYSBtYW5uLXdoaXRuZXkgOS44NTA4NTZlLTAzICAwLjUzMTAgICAgICAgICBBVUMgMS4zNTQ0OTNlLTAyICAgICAgICAgICAgICAgICAgICAgZW50cmEKICAgICAgICAgdW5pZGFkZXMgICBudW1lcmljYSBtYW5uLXdoaXRuZXkgMi4zNDIxMDZlLTAxICAwLjUxMjcgICAgICAgICBBVUMgMi44NjI1NzRlLTAxIG5vIHNlIGRpc3Rpbmd1ZSBkZWwgcnVpZG8KICAgICAgICBjYXRlZ29yaWEgY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgOC4wNjczNDRlLTAxICAwLjAyMzIgViBkZSBDcmFtZXIgOC44NzQwNzhlLTAxIG5vIHNlIGRpc3Rpbmd1ZSBkZWwgcnVpZG8KICAgICAgICAgICBjaXVkYWQgY2F0ZWdvcmljYSAgICAgICAgIGNoaTIgOS44MTMyNTFlLTAxICAwLjAxNTYgViBkZSBDcmFtZXIgOS44MTMyNTFlLTAxIG5vIHNlIGRpc3Rpbmd1ZSBkZWwgcnVpZG8=\",\n",
    "    2: \"Qm9kZWdhICAgICAgIG4gIDcwNyAgViAwLjEzMjggIHAgMC4wMDYwICBtZWpvciBjYW5hbCBXaGF0c0FwcCAgIDAuNDU1ICBwZW9yIE1hcmtldHBsYWNlIDAuMjc2CkhvcmVjYSAgICAgICBuICA3NDIgIFYgMC4xODc1ICBwIDAuMDAwMCAgbWVqb3IgY2FuYWwgVGllbmRhICAgICAwLjc1OSAgcGVvciBNYXJrZXRwbGFjZSAwLjUwNQpNYXlvcmlzdGEgICAgbiAgNzUwICBWIDAuMTQyNCAgcCAwLjAwMTYgIG1lam9yIGNhbmFsIFdoYXRzQXBwICAgMC44MTkgIHBlb3IgTWFya2V0cGxhY2UgMC42MzcKTWluaW1hcmtldCAgIG4gIDgwMSAgViAwLjE5NDYgIHAgMC4wMDAwICBtZWpvciBjYW5hbCBXaGF0c0FwcCAgIDAuNjkyICBwZW9yIE1hcmtldHBsYWNlIDAuNDE2\",\n",
    "    3: \"ViBkZSAwLjA1IC0+IGxvIGRldGVjdGFyaWEgZWwgIDUzLjMlIGRlIGxhcyB2ZWNlcwpWIGRlIDAuMDggLT4gbG8gZGV0ZWN0YXJpYSBlbCAgOTQuMyUgZGUgbGFzIHZlY2VzClYgZGUgMC4xMCAtPiBsbyBkZXRlY3RhcmlhIGVsICA5OS42JSBkZSBsYXMgdmVjZXMKViBkZSAwLjE1IC0+IGxvIGRldGVjdGFyaWEgZWwgMTAwLjAlIGRlIGxhcyB2ZWNlcwoKbGEgViBxdWUgc2FsaW8gZW4gY2l1ZGFkOiAwLjAxNTY=\",\n",
    "    4: \"dW5pZGFkZXMgICAgICAgICAgIHQgZGUgU3R1ZGVudCBwIDMuNjAxZS0wMSAgIE1hbm4tV2hpdG5leSBwIDIuMzQyZS0wMSAgIGNvaW5jaWRlbgptb250byAgICAgICAgICAgICAgdCBkZSBTdHVkZW50IHAgMS4wMzFlLTI4ICAgTWFubi1XaGl0bmV5IHAgNi4yOTllLTM4ICAgY29pbmNpZGVuCmRlc2N1ZW50byAgICAgICAgICB0IGRlIFN0dWRlbnQgcCAxLjA2MmUtMDIgICBNYW5uLVdoaXRuZXkgcCA5Ljg1MWUtMDMgICBjb2luY2lkZW4Kc2F0aXNmYWNjaW9uICAgICAgIHQgZGUgU3R1ZGVudCBwIDEuNjExZS0yMSAgIE1hbm4tV2hpdG5leSBwIDMuODM1ZS0yMSAgIGNvaW5jaWRlbgpwcmVjaW9fdW5pdGFyaW8gICAgdCBkZSBTdHVkZW50IHAgMS4wMjBlLTEwICAgTWFubi1XaGl0bmV5IHAgMi41ODFlLTI0ICAgY29pbmNpZGVu\",\n",
    "    5: \"ZmFsdGFfZGVzY3VlbnRvICAgICAgICAgICAgICBudWxvcyAgNTk1ICBWIDAuMDk2OCAgcCAxLjEzNmUtMDcgIGNvbiBkYXRvIDAuNjAyICBzaW4gZGF0byAwLjQ4MQpmYWx0YV9zYXRpc2ZhY2Npb24gICAgICAgICAgIG51bG9zICAyMzEgIFYgMC4wMjI5ICBwIDIuMDkxZS0wMSAgY29uIGRhdG8gMC41NzQgIHNpbiBkYXRvIDAuNjE5CmZhbHRhX2ZlY2hhX3VsdGltYV9jb21wcmEgICAgbnVsb3MgIDU0NCAgViAwLjE1NTUgIHAgMS42NzVlLTE3ICBjb24gZGF0byAwLjYxNCAgc2luIGRhdG8gMC40MTQ=\",\n",
    "    6: \"ICAgMTEgcHJ1ZWJhcyAtPiB1bmEgcCBkZSAwLDAwMSBTSSBzb2JyZXZpdmUgYSBCZW5qYW1pbmktSG9jaGJlcmcgfCBsaXN0w7NuIGRlIEJvbmZlcnJvbmkgMC4wMDQ1NDUKICAgNDAgcHJ1ZWJhcyAtPiB1bmEgcCBkZSAwLDAwMSBTSSBzb2JyZXZpdmUgYSBCZW5qYW1pbmktSG9jaGJlcmcgfCBsaXN0w7NuIGRlIEJvbmZlcnJvbmkgMC4wMDEyNTAKICAyMDAgcHJ1ZWJhcyAtPiB1bmEgcCBkZSAwLDAwMSBubyBzb2JyZXZpdmUgYSBCZW5qYW1pbmktSG9jaGJlcmcgfCBsaXN0w7NuIGRlIEJvbmZlcnJvbmkgMC4wMDAyNTAKIDEwMDAgcHJ1ZWJhcyAtPiB1bmEgcCBkZSAwLDAwMSBubyBzb2JyZXZpdmUgYSBCZW5qYW1pbmktSG9jaGJlcmcgfCBsaXN0w7NuIGRlIEJvbmZlcnJvbmkgMC4wMDAwNTA=\",\n",
    "    7: \"Y2F0ZWdvcmljYXMgY29udHJhIGVsIG1vbnRvIChLcnVza2FsLVdhbGxpcyk6CiAgc2VnbWVudG8gICAgIEggIDI0MDEuNDcgIHAgMC4wMDBlKzAwICBlcHNpbG9uMiAwLjgwMDYKICBjYW5hbCAgICAgICAgSCAgICAgMC45MyAgcCA4LjE3OWUtMDEgIGVwc2lsb24yIC0wLjAwMDcKICBjaXVkYWQgICAgICAgSCAgICAgMC41NSAgcCA5LjkwMWUtMDEgIGVwc2lsb24yIC0wLjAwMTUKICBjYXRlZ29yaWEgICAgSCAgICAgNS42OSAgcCAyLjIzOGUtMDEgIGVwc2lsb24yIDAuMDAwNgoKbnVtZXJpY2FzIGNvbnRyYSBlbCBtb250byAoU3BlYXJtYW4pOgogIHVuaWRhZGVzICAgICByaG8gKzAuMDIyNCAgcCAyLjE5MGUtMDEKICBzYXRpc2ZhY2Npb24gcmhvIC0wLjAwNTIgIHAgNy44NjRlLTAxCiAgZGVzY3VlbnRvICAgIHJobyAtMC4wMjAxICBwIDMuMjM0ZS0wMQ==\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora la pregunta que de verdad importa: **¿cuál de estas columnas\n",
    "tiene que ver con lo que queremos predecir?** 🎯\n",
    "\n",
    "Y no a ojo. A ojo cualquier diferencia parece grande, sobre todo cuando ya\n",
    "tienes una teoría de por qué debería estarlo. Aquí cada cruce lleva una prueba y\n",
    "un tamaño del efecto al lado.\n",
    "\n",
    "Al final del capítulo van a quedar dos columnas descartadas con nombre y\n",
    "apellido, y una que parece buenísima y que en el capítulo\n",
    "20 al modelo no le va a aportar nada 🪤"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué prueba toca según el tipo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| La columna es | El objetivo es | Prueba | Tamaño del efecto |\n",
    "|---|---|---|---|\n",
    "| Categórica | Categórico | Chi cuadrado | V de Cramér |\n",
    "| Numérica | Categórico | Mann-Whitney | d de Cohen, o el AUC |\n",
    "| Numérica | Numérico | Correlación de Spearman | El propio coeficiente |\n",
    "| Categórica | Numérico | Kruskal-Wallis | Epsilon cuadrado |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nuestro objetivo, `compro`, es categórico, así que aquí usamos las\n",
    "dos primeras filas. Las otras dos están para cuando el objetivo sea un número,\n",
    "que es el otro tipo de problema del capítulo\n",
    "1 📋\n",
    "\n",
    "Y uso Mann-Whitney y no la t de Student a propósito: la t supone normalidad y\n",
    "en el capítulo 6 quedó claro que ninguna columna la cumple.\n",
    "Mann-Whitney compara posiciones y no le importa la forma."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from scipy import stats\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "ventas = carga_limpia(URL)\n",
    "ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']\n",
    "ventas['sin_compra_previa'] = ventas['fecha_ultima_compra'].isna().astype(int)\n",
    "ventas['sin_descuento'] = ventas['descuento'].isna().astype(int)\n",
    "\n",
    "objetivo = ventas['compro']\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria',\n",
    "               'sin_compra_previa', 'sin_descuento']\n",
    "\n",
    "print('filas:', len(ventas), '| tasa de compra:', round(objetivo.mean(), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que se comete en el primer cruce"
   ]
  },
  {
   "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",
    "    stats.chi2_contingency(ventas['segmento'])\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",
    "TypeError: '<' not supported between instances of 'str' and 'int'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El chi cuadrado no recibe una columna: recibe una **tabla de\n",
    "contingencia**, o sea el cruce ya hecho. Es el error de la primera vez y\n",
    "el mensaje no ayuda nada, porque habla de comparar textos con números 🤷‍♀️\n",
    "\n",
    "Lo que hay que pasarle es `pd.crosstab(columna, objetivo)`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las categóricas: chi cuadrado y V de Cramér"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El chi cuadrado dice si la diferencia se distingue del azar. La V de Cramér\n",
    "dice si es grande, que no es lo mismo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "V=χ2n(k−1)\n",
    "\n",
    "reparte el chi cuadrado entre las filas y los niveles para que el número no dependa del tamaño de la muestra, y así va siempre de cero a uno"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def cramer(tabla):\n",
    "    chi2 = stats.chi2_contingency(tabla)[0]\n",
    "    n = tabla.values.sum()\n",
    "    return np.sqrt(chi2 / (n * (min(tabla.shape) - 1)))\n",
    "\n",
    "filas = []\n",
    "for c in CATEGORICAS:\n",
    "    tabla = pd.crosstab(ventas[c], objetivo)\n",
    "    chi2, p, gl, _esperado = stats.chi2_contingency(tabla)\n",
    "    tasas = ventas.groupby(c)['compro'].mean()\n",
    "    filas.append({'columna': c, 'niveles': len(tabla), 'chi2': round(chi2, 2),\n",
    "                  'p': p, 'cramer_v': round(cramer(tabla), 4),\n",
    "                  'rango_tasas': round(tasas.max() - tasas.min(), 4)})\n",
    "\n",
    "print(pd.DataFrame(filas).sort_values('cramer_v', ascending=False)\n",
    "      .to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léela de arriba abajo, que cuenta una historia entera 📖\n",
    "\n",
    "**El segmento manda**: V de 0,2545 y 34,92 puntos entre el mejor\n",
    "y el peor. Después el canal y la bandera de cliente nuevo, las dos alrededor de\n",
    "0,15.\n",
    "\n",
    "Y abajo del todo, **`ciudad` y `categoria`**\n",
    "con V de 0,0156 y 0,0232 y valores p de 0,98 y 0,81. Esas dos no dicen nada, y\n",
    "no lo dicen tan claramente que ya se puede escribir en el informe.\n",
    "\n",
    "Esto es exactamente lo que salió en el libro de estadística con las mismas\n",
    "ventas, por otro camino. Que dos análisis independientes coincidan es lo que\n",
    "convierte un hallazgo en un hecho ✅\n",
    "\n",
    "Ojo con el rango de tasas, que es la columna que más engaña: cuantos más\n",
    "niveles tiene una variable, más fácil es que dos de ellos se separen por azar.\n",
    "Por eso la V va al lado, que sí lo tiene en cuenta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las numéricas: Mann-Whitney y el tamaño del efecto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "d=x¯1−x¯0sp\n",
    "\n",
    "la distancia entre las dos medias medida en desviaciones típicas, que es la forma de decir si la diferencia es grande sin depender de las unidades"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "filas = []\n",
    "for c in NUMERICAS:\n",
    "    si = ventas.loc[objetivo == 1, c].dropna()\n",
    "    no = ventas.loc[objetivo == 0, c].dropna()\n",
    "    u, p = stats.mannwhitneyu(si, no)\n",
    "    auc = u / (len(si) * len(no))\n",
    "    juntas = np.sqrt(((len(si) - 1) * si.var() + (len(no) - 1) * no.var())\n",
    "                     / (len(si) + len(no) - 2))\n",
    "    d = (si.mean() - no.mean()) / juntas\n",
    "    filas.append({'columna': c, 'media_si': round(si.mean(), 2),\n",
    "                  'media_no': round(no.mean(), 2), 'p': p,\n",
    "                  'cohen_d': round(d, 4), 'auc': round(auc, 4)})\n",
    "\n",
    "tabla = pd.DataFrame(filas)\n",
    "print(tabla.reindex(tabla['auc'].sub(0.5).abs().sort_values(ascending=False).index)\n",
    "      .to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese `auc` es un regalo del propio Mann-Whitney: el estadístico U\n",
    "dividido entre el producto de los dos tamaños **es** el AUC de esa\n",
    "columna usada sola como modelo. Sale gratis y se lee de una: 0,5 es no saber\n",
    "nada y 0,6374 es lo que da `monto` por su cuenta 🎁\n",
    "\n",
    "La d de Cohen dice lo mismo en otra escala. Las convenciones son 0,2 pequeño,\n",
    "0,5 mediano y 0,8 grande, así que aquí **no hay ninguna grande**:\n",
    "la mayor es `monto` con 0,4061.\n",
    "\n",
    "Y abajo, `unidades` con p de 0,234 y d de 0,0338. Esa es la tercera\n",
    "columna que se cae, y van tres."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La trampa que este capítulo tiene que dejar montada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en `sin_descuento`, en la tabla de las categóricas: V de\n",
    "0,0968 y **12,10 puntos** de diferencia entre quien tiene descuento\n",
    "y quien no."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cruce = pd.crosstab(ventas['sin_descuento'], objetivo, normalize='index')\n",
    "print((100 * cruce).round(2).to_string())\n",
    "print()\n",
    "chi2, p, gl, _e = stats.chi2_contingency(pd.crosstab(ventas['sin_descuento'], objetivo))\n",
    "print(f'chi2 {chi2:.2f}   p {p:.3e}   V de Cramer '\n",
    "      f'{cramer(pd.crosstab(ventas[\"sin_descuento\"], objetivo)):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con descuento cierra el 60,17% y sin descuento el 48,07%. Doce puntos, con\n",
    "una p de 1,14e-07 🤝\n",
    "\n",
    "Esa columna parece de las buenas. Y en el capítulo\n",
    "20, cuando midamos qué usa el modelo, va a salir en\n",
    "**−0,0002**. Cero patatero.\n",
    "\n",
    "No es que este capítulo se equivoque. Es que un análisis bivariado mira cada\n",
    "columna *sola*, y ahí `sin_descuento` dice mucho. Cuando el\n",
    "modelo ya tiene las otras doce columnas, esa información **ya se la está\n",
    "dando otra**, y entonces esta no añade nada.\n",
    "\n",
    "Guárdate esa idea, porque es la limitación de todo este capítulo y el motivo\n",
    "por el que existe el siguiente 🔗"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y otra trampa, en la dirección contraria"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`monto` es la mejor de las numéricas: AUC 0,6374 y p de 6,30e-38,\n",
    "que es un número absurdamente pequeño.\n",
    "\n",
    "Pues en el capítulo 22, con las demás columnas\n",
    "dentro del mismo modelo, `monto` sale con **p de\n",
    "0,1561** y su intervalo cruza el 1.\n",
    "\n",
    "La explicación es la misma vista al revés: aquí el monto se lleva el mérito\n",
    "de cosas que no son suyas. Los mayoristas compran más caro *y* cierran\n",
    "más, así que mirando el monto solo, parece que el monto explica el cierre.\n",
    "Descontando el segmento, casi nada.\n",
    "\n",
    "Las dos trampas juntas son la lección del capítulo: **un análisis\n",
    "bivariado ordena la investigación, no decide el modelo** 🧭\n",
    "\n",
    "Y fíjate en que van en direcciones contrarias, que es lo que las hace\n",
    "peligrosas:\n",
    "\n",
    "- 🪤 `sin_descuento` parece que aporta y no aporta. Si te fías,\n",
    "metes una columna de más, que es barato.\n",
    "\n",
    "- 🎣 `monto` parece que explica la compra y lo que explica es el\n",
    "segmento. Si te fías, sales de la reunión diciendo \"subid el ticket medio y\n",
    "cerraréis más\", que es una recomendación de negocio equivocada y cara.\n",
    "\n",
    "El segundo error no lo arregla ningún modelo, porque el modelo ni se entera:\n",
    "la frase se dijo en una reunión, con una tabla de Excel delante y sin entrenar\n",
    "nada 💸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuarenta columnas de puro ruido"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de fiarte de una tabla de valores p, hay que saber cuántos aparecen sin\n",
    "que haya nada. Se mide inventando columnas que no significan nada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from statsmodels.stats.multitest import multipletests\n",
    "\n",
    "rng = np.random.default_rng(7)\n",
    "y = objetivo.values\n",
    "ruido = rng.normal(size=(len(ventas), 40))\n",
    "\n",
    "ps = [stats.mannwhitneyu(ruido[y == 1, j], ruido[y == 0, j])[1] for j in range(40)]\n",
    "sobrevive, ajustadas, _a, _b = multipletests(ps, alpha=0.05, method='fdr_bh')\n",
    "\n",
    "print('columnas de ruido con p menor que 0,05:', int(sum(p < 0.05 for p in ps)), 'de 40')\n",
    "print('y despues del ajuste                  :', int(sobrevive.sum()), 'de 40')\n",
    "print('la mejor p sin ajustar:', round(min(ps), 4))\n",
    "print('las que se colaron    :', sorted(round(p, 4) for p in ps if p < 0.05))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro de cuarenta. Y no hay nada ahí, las inventé yo con un generador de\n",
    "números al azar 😱\n",
    "\n",
    "Con cuarenta pruebas al 5% se esperan dos falsos positivos, así que cuatro\n",
    "está dentro de lo normal. El mejor de esos falsos sale con p de 0,0065, que en\n",
    "cualquier informe pasaría por hallazgo.\n",
    "\n",
    "El ajuste de Benjamini-Hochberg los mata los cuarenta. Ese ajuste ordena los\n",
    "valores p y sube el listón según cuántas pruebas hiciste, y aquí acierta de\n",
    "pleno: no había nada y no deja pasar nada 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El mismo ajuste sobre las columnas de verdad"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nombres, ps = [], []\n",
    "for c in CATEGORICAS:\n",
    "    nombres.append(c)\n",
    "    ps.append(stats.chi2_contingency(pd.crosstab(ventas[c], objetivo))[1])\n",
    "for c in NUMERICAS:\n",
    "    si = ventas.loc[objetivo == 1, c].dropna()\n",
    "    no = ventas.loc[objetivo == 0, c].dropna()\n",
    "    nombres.append(c)\n",
    "    ps.append(stats.mannwhitneyu(si, no)[1])\n",
    "\n",
    "sobrevive, ajustadas, _a, _b = multipletests(ps, alpha=0.05, method='fdr_bh')\n",
    "print(pd.DataFrame({'columna': nombres, 'p': ps, 'p_ajustada': ajustadas,\n",
    "                    'sobrevive': sobrevive}).to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las mismas tres se caen: `ciudad`, `categoria` y\n",
    "`unidades`. Y las ocho que quedan sobreviven con margen de sobra,\n",
    "porque sus valores p son tan pequeños que ningún ajuste razonable las toca.\n",
    "\n",
    "Cuando el ajuste no cambia ninguna conclusión, como aquí, es buena señal:\n",
    "significa que tus hallazgos no dependían de dónde pusiste el listón 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El informe bivariado completo, en una tabla\n",
    "\n",
    "Junta las dos mitades del capítulo en una sola función que\n",
    "recorra todas las columnas y aplique la prueba que toque."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La misma pregunta partida por segmento\n",
    "\n",
    "Una relación global puede ser distinta dentro de cada\n",
    "grupo. Repite el cruce del canal dentro de cada segmento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto habría hecho falta para detectar lo que no salió\n",
    "\n",
    "`ciudad` salió con p de 0,98. ¿Es que no hay\n",
    "efecto, o es que no había datos suficientes? Se contesta al revés: qué efecto\n",
    "habrías detectado con este tamaño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Qué pasa si usas la prueba equivocada\n",
    "\n",
    "La t de Student supone normalidad y ninguna columna la\n",
    "cumple. Compara lo que dicen las dos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Los nulos también son una columna\n",
    "\n",
    "Ya lo hicimos con el descuento. Hazlo con todas: convierte\n",
    "cada patrón de nulos en una bandera y crúzalo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuántos pares mirar antes de que el ajuste te coma\n",
    "\n",
    "Si en vez de once columnas tuvieras doscientas, ¿cuánto\n",
    "tendría que valer una p para sobrevivir al ajuste?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Lo mismo, si el objetivo fuera un número\n",
    "\n",
    "Las otras dos filas de la tabla del principio. Cruza las\n",
    "columnas contra `monto` en vez de contra `compro`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Cruzas 40 columnas contra el objetivo y cuatro salen con p por debajo de 0,05. ¿Qué concluyes?\n",
    "\n",
    "a) Nada todavía: con 40 pruebas al 5% se esperan dos por puro azar\n",
    "\n",
    "b) Que esas cuatro son las que importan\n",
    "\n",
    "c) Que las otras 36 no sirven\n",
    "\n",
    "d) Que hay que bajar el umbral a 0,01"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y la que sale de correr esto veinte veces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "El barrido de toda la vida: cruzar cada columna con el objetivo y quedarse con las que salen significativas. Corre en dos segundos y saca lista.\n",
    "\n",
    "```\n",
    "for col in ventas.columns:\n",
    "    p = prueba(ventas[col], ventas['compro'])\n",
    "    if p < 0.05:\n",
    "        print(col, 'significativa')\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": [
    "- 🧮 Cada tipo de cruce tiene su prueba: chi cuadrado con V de Cramér para\n",
    "las categóricas, Mann-Whitney con d de Cohen o AUC para las numéricas.\n",
    "\n",
    "- 📐 El valor p dice si se distingue del azar y el tamaño del efecto dice si\n",
    "importa. Los dos, siempre, y en esta tabla no hay ni un efecto grande: el mayor\n",
    "es `monto` con d de 0,4061.\n",
    "\n",
    "- 🎁 El AUC de una columna sola sale gratis del propio Mann-Whitney: es U\n",
    "dividido entre el producto de los dos tamaños.\n",
    "\n",
    "- 🥇 Manda el segmento, con V de 0,2545 y 34,92 puntos entre el mejor y el\n",
    "peor nivel.\n",
    "\n",
    "- 🚪 Se caen tres con nombre y apellido: `ciudad` (p 0,98),\n",
    "`categoria` (p 0,81) y `unidades` (p 0,23).\n",
    "\n",
    "- 🪤 `sin_descuento` tiene 12,10 puntos de diferencia y p de\n",
    "1,14e-07, y al modelo con las otras doce columnas le va a aportar −0,0002.\n",
    "Bivariado no es lo mismo que dentro de un modelo.\n",
    "\n",
    "- 🔄 Y al revés: `monto` es la mejor numérica aquí, con p de\n",
    "6,30e-38, y controlando por segmento y canal se queda en 0,1561. Se estaba\n",
    "llevando el mérito de otra columna.\n",
    "\n",
    "- 😱 De 40 columnas de ruido puro, cuatro salieron con p menor que 0,05 y la\n",
    "mejor con 0,0065. El ajuste de Benjamini-Hochberg mata las cuarenta.\n",
    "\n",
    "- 🧭 Un análisis bivariado ordena la investigación. No decide el modelo.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un valor p sin su tamaño del efecto al lado no es evidencia. Es una coincidencia bien presentada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo de valor p, tamaño del efecto y comparaciones múltiples viene de la estadística clásica, y está explicado despacio 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": [
    "Ya sabemos qué columna tiene que ver con el objetivo. Falta la otra mitad:\n",
    "**qué tienen que ver las columnas entre ellas** 🕸️\n",
    "\n",
    "Y esa es justo la pregunta que explica las dos trampas de este capítulo. Si\n",
    "el monto se lleva el mérito del segmento es porque están relacionados, y eso\n",
    "aquí no se puede ver.\n",
    "\n",
    "El capítulo 8 mira la maraña:\n",
    "\n",
    "- 🔗 La matriz de correlaciones, y por qué se lee con Spearman.\n",
    "\n",
    "- 🪞 Columnas que dicen lo mismo con otro nombre, y qué hacer con ellas.\n",
    "\n",
    "- 📉 Reducir trece columnas a unas pocas y ver cuánta información sobrevive.\n",
    "\n",
    "- 🎭 Y una relación que se da la vuelta al partir por grupos, que es lo que\n",
    "hace que un informe entero diga lo contrario de lo que pasa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 7 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/eda-bivariado/\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
}
