{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Los errores que invalidan un análisis entero\n",
    "\n",
    "Simpson fabricado con ventas de verdad, regresión a la media, p-hacking y las filas que no son independientes.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 17 de **Estadística 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/estadistica-desde-cero/errores-que-invalidan/\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": [
    "Todos los errores de este capítulo tienen algo en común: **no dan\n",
    "error** 😶\n",
    "\n",
    "Los cálculos salen, los números se ven bien, el informe se entrega. Y la\n",
    "conclusión es la contraria de la verdad."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "from scipy import stats\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "\n",
    "def carga_limpia(url):\n",
    "    \"\"\"La misma del capítulo 2.\"\"\"\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",
    "print('filas:', len(v))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y una pregunta incómoda: **¿alguna vez seguiste partiendo los datos hasta que salió algo?** Yo sí, y sin mala intención. Este capítulo es sobre todas las formas de engañarte sola 🎩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. La paradoja de Simpson"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Empiezo por la buena noticia: **este archivo no la tiene**. Lo\n",
    "comprobé antes de escribir el capítulo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tasas = v.pivot_table(index='canal', columns='segmento',\n",
    "                      values='compro', aggfunc='mean')\n",
    "print(tasas.round(4))\n",
    "print()\n",
    "print('composicion de cada canal:')\n",
    "print(pd.crosstab(v['canal'], v['segmento'], normalize='index').round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la segunda tabla: los cuatro canales tienen prácticamente la misma\n",
    "mezcla de segmentos, entre el 21% y el 29% de cada uno. Cuando la composición\n",
    "está equilibrada, Simpson no puede aparecer 🛡️\n",
    "\n",
    "Así que vamos a fabricarlo. Y lo voy a hacer con **ventas de verdad de\n",
    "este archivo**: no invento ni un dato, solo elijo cuáles miro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mp = v[v['canal'] == 'Marketplace']\n",
    "wa = v[v['canal'] == 'WhatsApp']\n",
    "\n",
    "# Marketplace casi solo mayoristas, WhatsApp casi solo bodegas.\n",
    "marketplace = pd.concat([mp[mp['segmento'] == 'Mayorista'],\n",
    "                         mp[mp['segmento'] == 'Bodega'].head(20)])\n",
    "whatsapp = pd.concat([wa[wa['segmento'] == 'Bodega'],\n",
    "                      wa[wa['segmento'] == 'Mayorista'].head(20)])\n",
    "\n",
    "print('EN TOTAL')\n",
    "print('  Marketplace: %.4f (n=%d)' % (marketplace['compro'].mean(), len(marketplace)))\n",
    "print('  WhatsApp:    %.4f (n=%d)' % (whatsapp['compro'].mean(), len(whatsapp)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Conclusión evidente: **Marketplace cierra mejor que WhatsApp**,\n",
    "60,00% contra 50,51%. Casi diez puntos.\n",
    "\n",
    "Ahora lo mismo, mirando dentro de cada segmento:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for seg in ['Bodega', 'Mayorista']:\n",
    "    a = marketplace[marketplace['segmento'] == seg]\n",
    "    b = whatsapp[whatsapp['segmento'] == seg]\n",
    "    print('%-10s Marketplace %.4f (n=%3d) | WhatsApp %.4f (n=%3d)'\n",
    "          % (seg, a['compro'].mean(), len(a), b['compro'].mean(), len(b)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se dio la vuelta 🤯\n",
    "\n",
    "En Bodega gana WhatsApp: 45,45% contra 25,00%. En Mayorista gana WhatsApp:\n",
    "95,00% contra 63,68%. **WhatsApp gana en los dos grupos y pierde en el\n",
    "total.**\n",
    "\n",
    "Eso es la paradoja de Simpson, y lo que la produce está en la composición:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('composicion de Marketplace:')\n",
    "print((marketplace['segmento'].value_counts(normalize=True) * 100).round(1))\n",
    "print()\n",
    "print('composicion de WhatsApp:')\n",
    "print((whatsapp['segmento'].value_counts(normalize=True) * 100).round(1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Marketplace está lleno de mayoristas, que cierran mucho de por sí. WhatsApp\n",
    "está lleno de bodegas, que cierran poco. El total no compara canales:\n",
    "**compara clientelas** 🎭\n",
    "\n",
    "Y cuidado con la moraleja fácil, que suele ser \"hay que mirar por grupos\". No\n",
    "siempre 🙅 Si el canal fuera la causa de que lleguen unos u otros clientes,\n",
    "partir por segmento estaría borrando justo el efecto que buscas.\n",
    "\n",
    "La regla honesta: **hay que decidir qué se controla antes de mirar los\n",
    "datos**, y con una razón. Si eliges después, siempre puedes encontrar el\n",
    "corte que te da la respuesta que querías."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. La regresión a la media"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este me parece el más bonito de todos, y el que más decisiones estropea 📉\n",
    "\n",
    "Ordeno las ventas de cada cliente y miro qué pasa *después* de una\n",
    "venta muy grande o muy chica:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "orden = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "orden['siguiente'] = orden.groupby('cliente_id')['monto'].shift(-1)\n",
    "con_siguiente = orden.dropna(subset=['siguiente'])\n",
    "\n",
    "alto = con_siguiente['monto'].quantile(0.8)\n",
    "bajo = con_siguiente['monto'].quantile(0.2)\n",
    "mejores = con_siguiente[con_siguiente['monto'] >= alto]\n",
    "peores = con_siguiente[con_siguiente['monto'] <= bajo]\n",
    "\n",
    "print('media de todas: %.2f' % con_siguiente['monto'].mean())\n",
    "print()\n",
    "print('el 20%% mas alto: esta venta %.2f -> la siguiente %.2f'\n",
    "      % (mejores['monto'].mean(), mejores['siguiente'].mean()))\n",
    "print('el 20%% mas bajo: esta venta %.2f -> la siguiente %.2f'\n",
    "      % (peores['monto'].mean(), peores['siguiente'].mean()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los que compraron 2.108 bajan a 794. Los que compraron 131 suben a 782. Los\n",
    "dos aterrizan en la media de 800,52 🛬\n",
    "\n",
    "Y ahora imagina el informe: *\"los clientes que atendimos con el programa\n",
    "premium bajaron un 62%\"*. O al revés: *\"la campaña de recuperación subió\n",
    "las ventas de los clientes flojos un 494%\"*.\n",
    "\n",
    "Las dos frases serían falsas, y las dos saldrían de datos correctos. No pasó\n",
    "nada: **los extremos vuelven al centro solos**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('correlacion entre una venta y la siguiente del mismo cliente: %.4f'\n",
    "      % stats.pearsonr(con_siguiente['monto'], con_siguiente['siguiente'])[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cero. La venta de hoy no dice nada de la siguiente, así que después de una\n",
    "venta enorme lo esperable es una venta *normal*, que comparada con la\n",
    "enorme parece una caída 📊\n",
    "\n",
    "La defensa: **si seleccionaste a alguien por ser extremo, necesitas un\n",
    "grupo de control**. Sin comparar contra clientes igual de extremos que no\n",
    "recibieron el programa, no se puede decir nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. El p-hacking, o buscar hasta encontrar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya lo vimos por partes en los capítulos 12, 13 y 15. Aquí está entero:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import product\n",
    "\n",
    "encontrados = []\n",
    "for col, col2 in product(['ciudad', 'canal', 'categoria'], repeat=2):\n",
    "    if col == col2:\n",
    "        continue\n",
    "    for val, val2 in product(v[col].unique(), v[col2].unique()):\n",
    "        sub = v[(v[col] == val) & (v[col2] == val2)]\n",
    "        if len(sub) < 80:\n",
    "            continue\n",
    "        a = sub.loc[sub['segmento'] == 'Mayorista', 'compro']\n",
    "        b = sub.loc[sub['segmento'] == 'Bodega', 'compro']\n",
    "        pv = stats.ttest_ind(a, b, equal_var=False).pvalue\n",
    "        encontrados.append((pv, col, val, col2, val2, len(sub)))\n",
    "\n",
    "encontrados.sort()\n",
    "print('subgrupos probados: %d' % len(encontrados))\n",
    "print()\n",
    "for pv, col, val, col2, val2, n in encontrados[:3]:\n",
    "    print('%s=%s y %s=%s (n=%d) -> p = %.3g' % (col, val, col2, val2, n, pv))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"En Web, categoría Snacks, la diferencia entre mayoristas y bodegas es\n",
    "abrumadora: p = 0,00000000035\" 🎉\n",
    "\n",
    "Suena a hallazgo. Y es basura, porque probé **144 subgrupos** y\n",
    "reporté el mejor. Con 144 intentos al 5%, encontrar algo espectacular está\n",
    "garantizado.\n",
    "\n",
    "Lo peor de este error es que casi nadie lo hace a propósito. Se hace probando\n",
    "cosas, que es lo que uno debe hacer, y luego contando solo lo que salió.\n",
    "\n",
    "Las defensas, por orden de eficacia:\n",
    "\n",
    "- 📝 Escribe la hipótesis **antes** de mirar.\n",
    "\n",
    "- 🔢 Di cuántas cosas probaste. Siempre.\n",
    "\n",
    "- ➗ Corrige el alfa (Bonferroni, capítulo 13).\n",
    "\n",
    "- 🔁 Guarda datos que no miraste y comprueba el hallazgo ahí."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Las filas que no son independientes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todas las pruebas del libro suponen que cada fila es un caso independiente. Y\n",
    "en el capítulo 2 vimos que aquí hay **617 clientes en 3.000 ventas**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "por_cliente = (v.groupby(['cliente_id', 'segmento'])['compro']\n",
    "               .mean().reset_index())\n",
    "\n",
    "a_ventas = v.loc[v['segmento'] == 'Horeca', 'compro']\n",
    "b_ventas = v.loc[v['segmento'] == 'Minimarket', 'compro']\n",
    "a_clientes = por_cliente.loc[por_cliente['segmento'] == 'Horeca', 'compro']\n",
    "b_clientes = por_cliente.loc[por_cliente['segmento'] == 'Minimarket', 'compro']\n",
    "\n",
    "print('contando ventas:   n = %d y %d -> p = %.5f'\n",
    "      % (len(a_ventas), len(b_ventas),\n",
    "         stats.ttest_ind(a_ventas, b_ventas, equal_var=False).pvalue))\n",
    "print('contando clientes: n = %d y %d -> p = %.5f'\n",
    "      % (len(a_clientes), len(b_clientes),\n",
    "         stats.ttest_ind(a_clientes, b_clientes, equal_var=False).pvalue))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí me llevé una sorpresa y la publico tal cual 😅\n",
    "\n",
    "Yo esperaba que al contar clientes en vez de ventas el resultado se debilitara,\n",
    "porque la muestra baja de 742 a 438. Y pasó al revés: el p mejoró, de 0,01181 a\n",
    "0,00716.\n",
    "\n",
    "La explicación es que promediar las ventas de cada cliente **quita\n",
    "ruido**, y ese ruido pesaba más que las filas perdidas.\n",
    "\n",
    "Pero que aquí saliera bien no salva la regla, y es importante: en general,\n",
    "tratar 3.000 ventas de 617 clientes como 3.000 casos independientes\n",
    "**infla la confianza**. Aquí no lo hizo, y en tu tabla puede\n",
    "hacerlo 🎯\n",
    "\n",
    "La comprobación cuesta cinco líneas, como acabas de ver. Hazla siempre que la\n",
    "misma persona, tienda o máquina aparezca varias veces."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. El sesgo de selección"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este ya salió en el capítulo 8 y fue culpa mía, así que lo repito aquí como\n",
    "ejemplo 🙋"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dias_con_ventas = v.groupby(v['fecha'].dt.date).size()\n",
    "rango = (v['fecha'].max() - v['fecha'].min()).days + 1\n",
    "\n",
    "print('dias que aparecen en el archivo: %d' % len(dias_con_ventas))\n",
    "print('dias que hay en el periodo:      %d' % rango)\n",
    "print('dias sin ninguna venta:          %d' % (rango - len(dias_con_ventas)))\n",
    "print()\n",
    "print('ventas por dia contando solo los dias con ventas: %.4f'\n",
    "      % dias_con_ventas.mean())\n",
    "print('ventas por dia contando todos los dias:           %.4f'\n",
    "      % (len(v) / rango))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres días desaparecidos, y con ellos la respuesta cambia: 5,5866 ventas\n",
    "diarias contando solo los días con actividad, 5,5556 contando todos.\n",
    "\n",
    "Aquí la diferencia es chica porque solo faltan 3 días de 540. Pero el\n",
    "mecanismo es el importante: **agrupar por lo que existe borra lo que no\n",
    "existe**, y nadie lo ve porque las filas que faltan no están ahí para\n",
    "protestar 👻\n",
    "\n",
    "Es el mismo error que mirar solo a los clientes que siguen contigo, o solo a\n",
    "los proyectos que terminaron, o solo a las empresas que sobrevivieron."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error del capítulo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El que más análisis rompe de verdad, y no da error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('filas del archivo:              %d' % len(v))\n",
    "print('filas que ve un groupby normal: %d' % v.groupby('satisfaccion').size().sum())\n",
    "print('con dropna=False:               %d'\n",
    "      % v.groupby('satisfaccion', dropna=False).size().sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`groupby` tira las filas cuya clave es nula, en silencio 🕳️\n",
    "\n",
    "231 ventas desaparecen de cualquier análisis que agrupe por satisfacción, y no\n",
    "hay ningún aviso. Si además sumas montos por grupo, el total no cuadra con el\n",
    "total del archivo y te vas a volver loca buscando dónde.\n",
    "\n",
    "Y el que sí frena, por comparar:"
   ]
  },
  {
   "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",
    "    otro = pd.DataFrame({'cliente_id': range(10), 'nivel': 1})\n",
    "    v.merge(otro, on='cliente_id')\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",
    "ValueError: You are trying to merge on str and int64 columns for key 'cliente_id'. If you wish to proceed you should use pd.concat\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese te para porque los tipos no coinciden. Menos mal, porque un merge mal\n",
    "hecho es de los que más daño hacen: si la tabla de la derecha tiene claves\n",
    "repetidas, **te devuelve más filas de las que tenías** y todos los\n",
    "promedios cambian sin que nadie lo note.\n",
    "\n",
    "La costumbre que salva: `print(len(df))` antes y después de cada\n",
    "merge 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y dos más, y el segundo es el error número cuatro otra vez"
   ]
  },
  {
   "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.groupby('ciudad').apply(lambda t: t['monto'].mean(), include_groups='si')\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",
    "ValueError: include_groups=True is no longer allowed.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra opción que quiere `True` o `False`. Y de paso te enseña algo de las librerías: esa opción **existía y la quitaron**. El código que copies de un tutorial de hace tres años puede fallar por esto y no porque esté mal pensado 🕰️"
   ]
  },
  {
   "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.ttest_rel(v['monto'].head(10), v['monto'].head(9))\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",
    "ValueError: Array shapes are incompatible for broadcasting.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La prueba pareada compara **el antes y el después de la misma unidad**, así que necesita la misma cantidad de los dos lados. Que se queje del tamaño es la versión amable del error número cuatro de este capítulo: cuando las filas están emparejadas, tratarlas como si fueran independientes cambia el resultado."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Practica 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Fabrica tú un Simpson al revés\n",
    "\n",
    "Construye el caso contrario: que Marketplace gane en los dos\n",
    "segmentos y pierda en el total."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "marketplace2 = pd.concat([mp[mp['segmento'] == 'Bodega'],\n",
    "                          mp[mp['segmento'] == 'Mayorista'].head(15)])\n",
    "whatsapp2 = pd.concat([wa[wa['segmento'] == 'Mayorista'],\n",
    "                       wa[wa['segmento'] == 'Bodega'].head(15)])\n",
    "\n",
    "print('EN TOTAL')\n",
    "print('  Marketplace %.4f (n=%d) | WhatsApp %.4f (n=%d)'\n",
    "      % (marketplace2['compro'].mean(), len(marketplace2),\n",
    "         whatsapp2['compro'].mean(), len(whatsapp2)))\n",
    "print()\n",
    "for seg in ['Bodega', 'Mayorista']:\n",
    "    x = marketplace2[marketplace2['segmento'] == seg]\n",
    "    y = whatsapp2[whatsapp2['segmento'] == seg]\n",
    "    print('%-10s Marketplace %.4f | WhatsApp %.4f' % (seg, x['compro'].mean(),\n",
    "                                                      y['compro'].mean()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "EN TOTAL\n",
    "  Marketplace 0.3010 (n=196) | WhatsApp 0.7957 (n=186)\n",
    "\n",
    "Bodega     Marketplace 0.2762 | WhatsApp 0.5333\n",
    "Mayorista  Marketplace 0.6000 | WhatsApp 0.8187\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí no se dio la vuelta: WhatsApp gana en el total y en los dos\n",
    "segmentos 🤷\n",
    "\n",
    "Y eso también enseña algo. **La paradoja no aparece siempre que\n",
    "desbalanceas**: hace falta que el desbalance vaya justo en contra de la\n",
    "diferencia real.\n",
    "\n",
    "En este montaje puse a Marketplace las bodegas (que cierran poco) y a WhatsApp\n",
    "los mayoristas (que cierran mucho), o sea que el desbalance *refuerza* la\n",
    "ventaja que WhatsApp ya tenía en lugar de darle la vuelta.\n",
    "\n",
    "Para conseguir la paradoja hay que empujar en dirección contraria, que es lo\n",
    "que hicimos arriba 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La regresión a la media, mes a mes\n",
    "\n",
    "Repite el experimento con meses en vez de ventas: coge los\n",
    "mejores clientes de un mes y mira el siguiente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mensual = (v.assign(mes=v['fecha'].dt.to_period('M'))\n",
    "           .groupby(['cliente_id', 'mes'])['monto'].sum().reset_index())\n",
    "mensual = mensual.sort_values(['cliente_id', 'mes'])\n",
    "mensual['siguiente'] = mensual.groupby('cliente_id')['monto'].shift(-1)\n",
    "m = mensual.dropna(subset=['siguiente'])\n",
    "\n",
    "top = m[m['monto'] >= m['monto'].quantile(0.9)]\n",
    "print('media general: %.2f' % m['monto'].mean())\n",
    "print('el 10%% mejor: este mes %.2f -> el siguiente %.2f'\n",
    "      % (top['monto'].mean(), top['siguiente'].mean()))\n",
    "print('caida aparente: %.1f%%'\n",
    "      % (100 * (top['siguiente'].mean() / top['monto'].mean() - 1)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "media general: 916.54\n",
    "el 10% mejor: este mes 2813.82 -> el siguiente 905.51\n",
    "caida aparente: -67.8%\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una caída del 67,8% en tus mejores clientes 😱\n",
    "\n",
    "Y no pasó absolutamente nada. Fíjate en que el mes siguiente, 905,51, está\n",
    "prácticamente en la media general de 916,54. O sea que siguen siendo\n",
    "buenos clientes, solo que ya no están en su mejor mes.\n",
    "\n",
    "Este es exactamente el informe que hunde a un equipo comercial: se elige a los\n",
    "mejores del trimestre, se les mide el siguiente, y todos \"empeoran\". La\n",
    "selección misma garantiza el resultado 🪤"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto infla el p-hacking, medido\n",
    "\n",
    "Con datos donde no hay ningún efecto, prueba veinte\n",
    "subgrupos y quédate con el mejor. ¿Qué p obtienes?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador = np.random.default_rng(7)\n",
    "v['ruido'] = generador.normal(size=len(v))\n",
    "\n",
    "mejores_p = []\n",
    "for _ in range(200):\n",
    "    grupo = generador.integers(0, 2, size=len(v))\n",
    "    ps = []\n",
    "    for corte in range(20):\n",
    "        sub = v['ruido'][generador.integers(0, 2, size=len(v)).astype(bool)]\n",
    "        g2 = grupo[:len(sub)]\n",
    "        ps.append(stats.ttest_ind(sub[g2 == 1], sub[g2 == 0]).pvalue)\n",
    "    mejores_p.append(min(ps))\n",
    "\n",
    "mejores_p = np.array(mejores_p)\n",
    "print('probando 20 subgrupos sin ningun efecto real:')\n",
    "print('  el mejor p es menor que 0.05 en el %.1f%% de los intentos'\n",
    "      % (100 * (mejores_p < 0.05).mean()))\n",
    "print('  mediana del mejor p: %.4f' % np.median(mejores_p))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "probando 20 subgrupos sin ningun efecto real:\n",
    "  el mejor p es menor que 0.05 en el 64.0% de los intentos\n",
    "  mediana del mejor p: 0.0331\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El 64,0% de las veces encuentras algo \"significativo\" donde no hay nada 🎰\n",
    "\n",
    "Y mira la mediana: 0,0331. O sea que el resultado *típico* de esta\n",
    "búsqueda ya pasa el corte de 0,05.\n",
    "\n",
    "Con veinte subgrupos, encontrar un hallazgo es lo normal, no la excepción. Y\n",
    "quien lee tu informe no ve los diecinueve que no reportaste.\n",
    "\n",
    "Por eso la pregunta más útil que se le puede hacer a cualquier análisis es\n",
    "**\"¿cuántas cosas probaste?\"**. Si la respuesta es \"no sé\", el\n",
    "resultado no se puede evaluar 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El groupby que se come las filas, en euros\n",
    "\n",
    "Suma el monto agrupando por satisfacción y comprueba si\n",
    "cuadra con el total del archivo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "total = v['monto'].sum()\n",
    "por_satisfaccion = v.groupby('satisfaccion')['monto'].sum().sum()\n",
    "\n",
    "print('total del archivo:     %.2f' % total)\n",
    "print('sumando por grupos:    %.2f' % por_satisfaccion)\n",
    "print('se perdieron:          %.2f soles (%.1f%%)'\n",
    "      % (total - por_satisfaccion, 100 * (total - por_satisfaccion) / total))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "total del archivo:     2411283.83\n",
    "sumando por grupos:    2213729.23\n",
    "se perdieron:          197554.60 soles (8.2%)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "197.554,60 soles evaporados, el 8,2% de la facturación 💸\n",
    "\n",
    "Son las 231 ventas cuya satisfacción está vacía. Y no hay ningún aviso: la\n",
    "tabla por grupos se ve perfecta, suma bien dentro de sí misma, y simplemente no\n",
    "incluye ese dinero.\n",
    "\n",
    "Si esa tabla va a un informe al lado del total del archivo, alguien va a\n",
    "preguntar por qué no cuadran y nadie va a saber contestar.\n",
    "\n",
    "La costumbre: **después de cualquier groupby, compara el total contra el\n",
    "original**. Una línea, y te ahorra la tarde 🧾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El merge que multiplica filas\n",
    "\n",
    "Fabrica una tabla de clientes con claves repetidas y\n",
    "comprueba qué le hace a tu archivo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clientes = pd.DataFrame({\n",
    "    'cliente_id': list(v['cliente_id'].unique()[:5]) * 2,\n",
    "    'nivel': range(10),\n",
    "})\n",
    "\n",
    "afectadas = v['cliente_id'].isin(clientes['cliente_id']).sum()\n",
    "unido = v.merge(clientes, on='cliente_id')\n",
    "\n",
    "print('clientes en la tabla nueva: %d (pero solo %d distintos)'\n",
    "      % (len(clientes), clientes['cliente_id'].nunique()))\n",
    "print('ventas de esos clientes:    %d' % afectadas)\n",
    "print('filas despues del merge:    %d' % len(unido))\n",
    "print()\n",
    "print('media del monto antes:  %.2f' % v.loc[v['cliente_id'].isin(clientes['cliente_id']), 'monto'].mean())\n",
    "print('media del monto despues: %.2f' % unido['monto'].mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "clientes en la tabla nueva: 10 (pero solo 5 distintos)\n",
    "ventas de esos clientes:    34\n",
    "filas despues del merge:    68\n",
    "\n",
    "media del monto antes:  627.80\n",
    "media del monto despues: 627.80\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "34 ventas entraron y salieron 68 😬\n",
    "\n",
    "Cada venta se duplicó porque cada cliente aparecía dos veces en la tabla de la\n",
    "derecha. El merge no se inventó nada: hizo exactamente lo que se le pidió.\n",
    "\n",
    "Aquí la media no se mueve (627,80 antes y después), porque duplicar todo por\n",
    "igual no cambia un promedio.\n",
    "Pero cualquier **suma** se duplica, cualquier **conteo**\n",
    "se duplica, y cualquier prueba estadística cree que tiene el doble de evidencia\n",
    "de la que tiene.\n",
    "\n",
    "La defensa es de una línea y no me la salto nunca:\n",
    "`print(len(df))` antes y después. Si el número sube, la tabla de la\n",
    "derecha tiene claves repetidas 🔑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Tu lista de comprobación\n",
    "\n",
    "Escribe una función que reciba un DataFrame y avise de las\n",
    "cosas de este capítulo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def revisa(df, id_repetido=None):\n",
    "    avisos = []\n",
    "    if id_repetido and df[id_repetido].nunique() < len(df):\n",
    "        avisos.append('%d filas para %d %s distintos: las filas no son independientes'\n",
    "                      % (len(df), df[id_repetido].nunique(), id_repetido))\n",
    "    nulos = df.isna().sum()\n",
    "    for col in nulos[nulos > 0].index:\n",
    "        avisos.append('%s tiene %d nulos: un groupby por esa columna perdera esas filas'\n",
    "                      % (col, nulos[col]))\n",
    "    return avisos\n",
    "\n",
    "\n",
    "for aviso in revisa(v, id_repetido='cliente_id'):\n",
    "    print('-', aviso)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "- 3000 filas para 617 cliente_id distintos: las filas no son independientes\n",
    "- descuento tiene 595 nulos: un groupby por esa columna perdera esas filas\n",
    "- fecha_ultima_compra tiene 544 nulos: un groupby por esa columna perdera esas filas\n",
    "- satisfaccion tiene 231 nulos: un groupby por esa columna perdera esas filas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los avisos que importan, en cuatro líneas de código 🙌\n",
    "\n",
    "Los cuatro avisos son ciertos, y el tercero es uno que no habíamos mirado en\n",
    "todo el libro: `fecha_ultima_compra` tiene 544 nulos, así que\n",
    "cualquier análisis de recencia pierde el 18% de las ventas 👀\n",
    "\n",
    "Fíjate en lo poco que hace esta función: contar filas distintas y contar\n",
    "nulos. Con eso sola ya cubre dos de los cinco errores del capítulo, que son los\n",
    "dos que más se cuelan porque no dejan rastro 🧾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Cuarenta columnas de ruido puro\n",
    "\n",
    "Genera cuarenta columnas al azar y cuenta cuántas correlacionan con el monto al 5%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g4 = np.random.default_rng(9)\n",
    "ruido = pd.DataFrame(g4.normal(size=(len(v), 40)))\n",
    "objetivo = v['monto'].values\n",
    "significativas = sum(stats.pearsonr(ruido[c], objetivo).pvalue < 0.05\n",
    "                     for c in ruido.columns)\n",
    "print('columnas de puro ruido probadas: 40')\n",
    "print('salen significativas al 5%%: %d' % significativas)\n",
    "print('lo esperado por azar es 2')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "columnas de puro ruido probadas: 40\n",
    "salen significativas al 5%: 2\n",
    "lo esperado por azar es 2\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos de cuarenta, clavado 🎯\n",
    "\n",
    "Esas dos columnas **no significan absolutamente nada**: las acabo de fabricar con un generador de números al azar. Y si te las enseñara sin contarte que probé cuarenta, las dos parecerían hallazgos con su valor p y todo.\n",
    "\n",
    "Por eso el p-hacking no necesita mala fe. Basta con probar muchas cosas y contar solo las que salieron. La defensa es aburrida y funciona: **di cuántas comparaciones hiciste** 🕳️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Las filas que son el mismo cliente\n",
    "\n",
    "Cuenta cuántos clientes distintos hay de verdad y compara el promedio con todas las filas contra el de un pedido por cliente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "primeros = v.sort_values('fecha').groupby('cliente_id').head(1)\n",
    "print('pedidos en total: %d' % len(v))\n",
    "print('clientes distintos: %d' % v['cliente_id'].nunique())\n",
    "print('pedidos por cliente: %.2f' % (len(v) / v['cliente_id'].nunique()))\n",
    "print()\n",
    "print('promedio con todas las filas:      %.2f' % v['monto'].mean())\n",
    "print('promedio con un pedido por cliente: %.2f' % primeros['monto'].mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "pedidos en total: 3000\n",
    "clientes distintos: 617\n",
    "pedidos por cliente: 4.86\n",
    "\n",
    "promedio con todas las filas:      803.76\n",
    "promedio con un pedido por cliente: 813.43\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres mil filas y seiscientos diecisiete clientes 😳\n",
    "\n",
    "Cada cliente aparece casi cinco veces. Así que cuando calculas un intervalo con n = 3.000 estás **mintiéndote sobre cuánta información tienes**: no son 3.000 opiniones independientes, son 617 clientes repetidos.\n",
    "\n",
    "Aquí los dos promedios se parecen (803,76 contra 813,43) y aun así el problema está: lo que cambia mucho no es el promedio, es el intervalo, que sale demasiado estrecho. Es el error número cuatro de este capítulo con los números delante 🔗"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cien hallazgos hechos con una moneda"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora la trampa, que es el error que invalida más análisis de todos los de este capítulo, y el que menos se ve 🪙"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Buscas dónde está la diferencia. Pruebas por ciudad, por segmento, por canal, por categoría, por combinaciones, y al final aparece un grupo con p menor que 0,05.\n",
    "\n",
    "```\n",
    "import numpy as np\n",
    "from scipy import stats\n",
    "\n",
    "rng = np.random.default_rng(2026)\n",
    "ps = []\n",
    "for _ in range(100):\n",
    "    marca = rng.random(len(v)) < 0.5\n",
    "    ps.append(prueba(v['compro'][marca],\n",
    "                     v['compro'][~marca]))\n",
    "\n",
    "print(sum(p < 0.05 for p in ps), round(min(ps), 4))\n",
    "# 10 0.0143\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Diez hallazgos de cien, y los cien grupos los hice tirando una moneda 🪙 El mejor da p igual a 0,0143 y compara 0,599 contra 0,554.\n",
    "\n",
    "Si escribieras eso en un informe sonaría perfecto: \"el grupo A convierte cuatro puntos y medio más, con p menor que 0,05\". Y el grupo A no existe. Lo inventó un generador de números aleatorios.\n",
    "\n",
    "Eso es lo que significa un umbral de 0,05: aceptas equivocarte una de cada veinte veces. Si haces veinte pruebas, esperas **una falsa por construcción**. Si haces cien, esperas cinco. Buscar hasta encontrar garantiza encontrar.\n",
    "\n",
    "Las dos defensas que uso: **decir qué vas a probar antes de mirar**, y si vas a explorar de verdad, exigir mucho más que 0,05 y **confirmar el hallazgo con datos que no usaste para encontrarlo**. Un hallazgo que no se repite el mes siguiente no era un hallazgo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Elegiste a los mejores clientes del trimestre y el siguiente bajaron un 62,6%. ¿Qué pasó?\n",
    "\n",
    "a) Nada: los extremos vuelven al centro solos, y hacía falta un grupo de control\n",
    "\n",
    "b) El programa que se les aplicó les hizo daño\n",
    "\n",
    "c) Se cansaron de comprar\n",
    "\n",
    "d) Hay un error en los datos\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Puede ser, pero hay una explicación que no necesita ningún programa. ¿Por qué los elegiste?\n",
    "\n",
    "*c)* Los del 20% más bajo subieron en el mismo periodo. ¿También se animaron solos?\n",
    "\n",
    "*d)* Los datos están bien. Es la selección la que garantiza el resultado.\n",
    "\n",
    "La correlación entre una venta y la siguiente del mismo cliente es -0,0099: después de una enorme, lo esperable es una normal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🎭 Simpson: WhatsApp ganaba en los dos segmentos (45,45% contra 25,00% y\n",
    "95,00% contra 63,68%) y perdía en el total. Lo produce la composición, no los\n",
    "canales.\n",
    "\n",
    "- 📉 Regresión a la media: el 20% de ventas más altas promediaba 2.108,22 y su\n",
    "siguiente venta 794,42. La correlación entre una venta y la siguiente es\n",
    "-0,0099. Por meses, el 10% mejor \"cae\" un 67,8%.\n",
    "\n",
    "- 🎰 P-hacking: probando 20 subgrupos sin ningún efecto real, el 64,0% de las\n",
    "veces encuentras algo significativo, y la mediana del mejor p es 0,0331.\n",
    "\n",
    "- 👥 3.000 ventas de 617 clientes no son 3.000 casos independientes. Aquí\n",
    "contar por cliente mejoró el p, y en general lo empeora.\n",
    "\n",
    "- 👻 `groupby` tira las filas con clave nula sin avisar: 231 ventas\n",
    "y 197.554,60 soles, el 8,2% de la facturación.\n",
    "\n",
    "- 🔑 Un merge con claves repetidas duplica filas. 34 entraron, 68 salieron.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ninguno de estos errores se arregla con más estadística. Se arreglan\n",
    "pensando antes de calcular."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ojo con el de las filas que no son independientes, porque la forma más común\n",
    "de fabricarlo es un JOIN mal hecho que duplica filas sin avisar. Ese está medido\n",
    "en el [libro de SQL](https://missyera.com/guias/sql-desde-cero/) 🔗"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se acabó la teoría 🎓 En el capítulo 22 hacemos el proyecto completo de\n",
    "principio a fin: una pregunta de negocio, los datos, el análisis, y el informe\n",
    "que se puede defender en una reunión."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 17 de **Estadística desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/estadistica-desde-cero/errores-que-invalidan/\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
}
