{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Un intervalo de confianza para cualquier cosa\n",
    "\n",
    "El bootstrap en tres líneas, comprobado contra la fórmula que ya conoces. Y el caso donde da un número precioso y falso.\n",
    "\n",
    "Cuaderno de práctica del capítulo 11 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/remuestreo-y-bootstrap/\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": [
    "En el capítulo 10 aprendimos a ponerle un\n",
    "intervalo al promedio. Y quedó algo colgando que igual ni notaste 🥾\n",
    "\n",
    "Ese intervalo salió de una fórmula, y esa fórmula solo existe para el\n",
    "promedio. Para la mediana no la hay. Ni para el percentil 90, ni para la razón\n",
    "entre dos cosas, ni para casi nada de lo que te van a preguntar en una reunión de\n",
    "verdad."
   ]
  },
  {
   "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",
    "\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",
    "m = v['monto'].dropna()\n",
    "\n",
    "ee = m.std() / np.sqrt(len(m))\n",
    "bajo, alto = stats.t.interval(0.95, len(m) - 1, m.mean(), ee)\n",
    "print('promedio %.2f, y su intervalo va de %.2f a %.2f' % (m.mean(), bajo, alto))\n",
    "print('mediana  %.2f, y su intervalo va de ...' % m.median())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el problema, escrito con puntos suspensivos 😅\n",
    "\n",
    "Y fíjate que la mediana es la que más falta hace aquí: 533,63 contra 803,76 de\n",
    "promedio, porque los montos tienen cola y el promedio se va para arriba. Eso ya\n",
    "lo vimos en el capítulo 3. O sea que el número que\n",
    "mejor describe estas ventas es justo el que no sabemos acompañar.\n",
    "\n",
    "Antes de seguir, piénsalo tú: **¿de dónde saldría el intervalo si\n",
    "pudieras repetir el experimento mil veces?** Guárdate esa respuesta, que\n",
    "es todo el capítulo 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La idea, que es de las más bonitas que hay"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un intervalo de confianza contesta \"cuánto se movería este número si volviera\n",
    "a tomar la muestra\". El problema es que no puedes volver a tomarla: los 3.000\n",
    "pedidos son los que hay.\n",
    "\n",
    "El truco del bootstrap es descarado y funciona: **si no puedes repetir\n",
    "el experimento, repite la muestra**. Se saca de tus propios datos una\n",
    "muestra del mismo tamaño, *con reemplazo*, o sea permitiendo que una fila\n",
    "salga dos veces y otra ninguna. Eso da una muestra parecida pero no idéntica. Y\n",
    "se hace cinco mil veces.\n",
    "\n",
    "Lo de \"con reemplazo\" no es un detalle técnico, es la idea entera. Sin\n",
    "reemplazo estarías barajando las mismas 3.000 filas y saldría siempre el mismo\n",
    "número."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def bootstrap(datos, resumen, veces=5000, semilla=7):\n",
    "    \"\"\"El intervalo de lo que sea, sin una sola formula.\"\"\"\n",
    "    generador = np.random.default_rng(semilla)\n",
    "    datos = np.asarray(datos)\n",
    "    repes = [resumen(generador.choice(datos, len(datos), replace=True))\n",
    "             for _ in range(veces)]\n",
    "    return np.percentile(repes, [2.5, 97.5])\n",
    "\n",
    "\n",
    "print('la formula del promedio dice: de %.2f a %.2f' % (bajo, alto))\n",
    "print('y el bootstrap dice         : de %.2f a %.2f'\n",
    "      % tuple(bootstrap(m.values, np.mean)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Iguales hasta el primer decimal 🎯\n",
    "\n",
    "Y ese es el orden que hay que respetar siempre: **primero se comprueba\n",
    "el método donde ya sabes la respuesta, y después se usa donde no**. Si el\n",
    "bootstrap no hubiera reproducido la fórmula, no habría por qué creerle en lo que\n",
    "viene ahora.\n",
    "\n",
    "Fíjate también en lo que la función NO tiene: ninguna distribución, ninguna t\n",
    "de Student, ningún supuesto de normalidad. Solo remuestrear y mirar los\n",
    "percentiles 2,5 y 97,5, que dejan el 95% en medio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la mediana, que era lo que no se podía"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('mediana: %.2f' % m.median())\n",
    "print('su intervalo del 95%%: de %.2f a %.2f'\n",
    "      % tuple(bootstrap(m.values, np.median)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya está. Cambiando `np.mean` por `np.median` 💜\n",
    "\n",
    "Eso es lo que hace al bootstrap tan útil: la función no sabe ni le importa\n",
    "qué le pasas. Cualquier cosa que sepas calcular sobre una muestra, sabes\n",
    "acompañarla de su intervalo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Algo que ninguna fórmula te da"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora una de verdad, de las que te piden. Cuánto vale un punto de\n",
    "satisfacción, medido en soles de mediana:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sat = v[['monto', 'satisfaccion']].dropna()\n",
    "observada = sat['monto'].median() / sat['satisfaccion'].median()\n",
    "\n",
    "generador = np.random.default_rng(7)\n",
    "filas = np.arange(len(sat))\n",
    "repes = []\n",
    "for _ in range(2000):\n",
    "    t = sat.iloc[generador.choice(filas, len(filas), replace=True)]\n",
    "    repes.append(t['monto'].median() / t['satisfaccion'].median())\n",
    "\n",
    "print('filas con las dos columnas: %d' % len(sat))\n",
    "print('soles de mediana por punto de satisfaccion: %.4f' % observada)\n",
    "print('intervalo del 95%%: de %.4f a %.4f' % tuple(np.percentile(repes, [2.5, 97.5])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una razón entre dos medianas, con su intervalo. Búscale la fórmula a eso 🙃\n",
    "\n",
    "Y mira una cosa importante del código: se remuestrean **las filas\n",
    "enteras**, no cada columna por su lado. Si remuestrearas monto y\n",
    "satisfacción por separado romperías la relación entre las dos, que es justo lo\n",
    "que estás midiendo. Cuando el número junta dos columnas, la unidad que se\n",
    "remuestrea es la fila."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparar dos grupos sin ninguna prueba"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y esto es lo que más te va a servir. La pregunta de siempre: ¿vende más caro\n",
    "Web o WhatsApp?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "canales = v.dropna(subset=['monto', 'canal'])\n",
    "web = canales[canales['canal'] == 'Web']['monto'].values\n",
    "wsp = canales[canales['canal'] == 'WhatsApp']['monto'].values\n",
    "\n",
    "generador = np.random.default_rng(7)\n",
    "difs = [np.median(generador.choice(web, len(web), replace=True))\n",
    "        - np.median(generador.choice(wsp, len(wsp), replace=True))\n",
    "        for _ in range(3000)]\n",
    "\n",
    "print('Web %d pedidos, WhatsApp %d' % (len(web), len(wsp)))\n",
    "print('la mediana de Web supera a la de WhatsApp en %.2f soles'\n",
    "      % (np.median(web) - np.median(wsp)))\n",
    "print('intervalo del 95%%: de %.2f a %.2f' % tuple(np.percentile(difs, [2.5, 97.5])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Doce soles de diferencia, y el intervalo va de **-45,08 a\n",
    "64,80** 🫠\n",
    "\n",
    "Cruza el cero. Con estos datos, la mediana de Web podría estar 45 soles por\n",
    "debajo de la de WhatsApp igual de bien que 65 por encima. La diferencia que\n",
    "viste no aguanta.\n",
    "\n",
    "Y esto es lo que hace el bootstrap tan honesto: no te da un sí o un no como\n",
    "una prueba de hipótesis, te da **el rango de lo que es compatible con tus\n",
    "datos**, y ahí se ve solo que doce soles no significan nada. Es la misma\n",
    "lección del capítulo 14, contada por otro camino."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si sabes calcularlo sobre una muestra, sabes ponerle un intervalo. El bootstrap no pide fórmula, pide que los datos sean una muestra de verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dónde se rompe, que es lo que casi nadie cuenta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo enseño porque un método que parece servir para todo da miedo, y con\n",
    "razón. Pídele al bootstrap un intervalo para el pedido **más caro**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "extremos = [np.max(np.random.default_rng(s).choice(m.values, len(m), replace=True))\n",
    "            for s in range(3000)]\n",
    "print('el pedido mas caro que existe: %.2f' % m.max())\n",
    "print('intervalo bootstrap del maximo: de %.2f a %.2f'\n",
    "      % tuple(np.percentile(extremos, [2.5, 97.5])))\n",
    "print('repeticiones que devuelven ese mismo maximo: %d de 3000'\n",
    "      % int(np.sum(np.array(extremos) == m.max())))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira el techo del intervalo 😳\n",
    "\n",
    "Es exactamente 4.236,71, que es el máximo observado. Y no es casualidad: el\n",
    "bootstrap remuestrea de lo que hay, así que **nunca puede devolver un\n",
    "número mayor que el más grande que viste**. De hecho 1.895 de las 3.000\n",
    "repeticiones devuelven ese mismísimo valor, porque en una muestra de 3.000 con\n",
    "reemplazo es rarísimo que el más caro no salga.\n",
    "\n",
    "Y ese intervalo es falso. El pedido más caro que tu negocio puede recibir el\n",
    "mes que viene no está acotado por el más caro que recibió hasta hoy. La pregunta\n",
    "es legítima; el bootstrap no la sabe contestar.\n",
    "\n",
    "La regla, y sirve para más cosas que esta: **el bootstrap funciona para\n",
    "números que dependen de toda la muestra, y falla para los que dependen de un solo\n",
    "dato del borde**. El promedio y la mediana usan todo. El máximo, el\n",
    "mínimo y el percentil 99 con pocos datos dependen de una fila, y ahí no hay\n",
    "remuestreo que valga."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error que sale al escribirlo mal"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y este error es bonito porque el que revienta es justo el concepto:"
   ]
  },
  {
   "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",
    "    generador = np.random.default_rng(7)\n",
    "    generador.choice(wsp, len(canales), replace=False)\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: Cannot take a larger sample than population when replace is False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*No puedes tomar una muestra más grande que la población sin reemplazo.*\n",
    "Pues eso es el bootstrap dicho al revés: **solo se puede remuestrear\n",
    "porque se permite repetir filas**. Quítale el reemplazo y el método deja\n",
    "de existir 🔑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y en tu trabajo, ¿cuándo lo sacas?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Lo que te piden | Qué haría yo |\n",
    "|---|---|\n",
    "| El ticket promedio con su margen de error | La fórmula del capítulo 10. Está resuelto y es instantáneo |\n",
    "| El ticket **típico**, o sea la mediana, con su margen | Bootstrap. No hay fórmula sencilla |\n",
    "| El percentil 90 del tiempo de entrega | Bootstrap, y con cuidado: cuanto más al borde, menos fiable |\n",
    "| Cuánto sube el monto por cada punto de satisfacción | Bootstrap sobre las filas enteras, como arriba |\n",
    "| El pedido más grande que podemos esperar | Bootstrap no. Eso es teoría de valores extremos y es otro mundo |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La última fila es la que hay que saber decir en voz alta en una reunión, y es\n",
    "justo la que este capítulo mide 🔍\n",
    "\n",
    "Si quieres el original, el bootstrap lo publicó Bradley Efron en\n",
    "[Bootstrap Methods: Another Look at the\n",
    "Jackknife](https://projecteuclid.org/journals/annals-of-statistics/volume-7/issue-1/Bootstrap-Methods-Another-Look-at-the-Jackknife/10.1214/aos/1176344552.full) (1979), y scipy trae una versión con más opciones en\n",
    "[scipy.stats.bootstrap](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.bootstrap.html), que hace lo mismo\n",
    "que nuestras tres líneas y además corrige el sesgo 📄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Un equipo quiere el intervalo de la mediana del ticket y escribe esto. Sale un intervalo tan estrecho que lo presentan como la gran precisión de sus datos.\n",
    "\n",
    "```\n",
    "repes = []\n",
    "for _ in range(5000):\n",
    "    muestra = generador.choice(montos, len(montos), replace=False)\n",
    "    repes.append(np.median(muestra))\n",
    "\n",
    "print(np.percentile(repes, [2.5, 97.5]))   # [533.63, 533.63]\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis. El 1 es el que quiero que hagas sí o sí, porque es la trampa de arriba\n",
    "ejecutada 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Rompe el reemplazo y míralo\n",
    "\n",
    "Corre el bootstrap con `replace=False` y el mismo\n",
    "tamaño, y compara el ancho del intervalo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El percentil 90 del monto\n",
    "\n",
    "Ponle intervalo al percentil 90 y compáralo con el de la\n",
    "mediana."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuántas repeticiones hacen falta\n",
    "\n",
    "Prueba con 200, 1.000, 5.000 y 20.000 y mira cuándo deja de\n",
    "moverse el intervalo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Compáralo con la prueba del capítulo anterior\n",
    "\n",
    "Sobre Web contra WhatsApp, corre también una prueba de\n",
    "Mann-Whitney y mira si las dos dicen lo mismo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Un intervalo para una correlación\n",
    "\n",
    "Ponle intervalo a la correlación entre monto y satisfacción,\n",
    "remuestreando filas enteras."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Bootstrap con doce filas\n",
    "\n",
    "Quédate con 12 pedidos al azar y ponle intervalo al\n",
    "promedio, con bootstrap y con la fórmula de la t."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. ¿Cuántos remuestreos hacen falta de verdad?\n",
    "\n",
    "Saca el mismo intervalo del promedio con 100, con 1000 y con 10000 remuestreos, y compara los anchos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Un intervalo para cada ciudad\n",
    "\n",
    "Saca el intervalo del promedio ciudad por ciudad y mira si alguno se queda fuera de los demás."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Te piden el intervalo del tiempo de entrega en el percentil 99, y tienes 80 pedidos. ¿Qué haces?\n",
    "\n",
    "a) Lo calculas y avisas de que con 80 datos ese intervalo es poco fiable\n",
    "\n",
    "b) Bootstrap y ya, que sirve para cualquier cosa\n",
    "\n",
    "c) La fórmula de la t, que es más seria\n",
    "\n",
    "d) Dices que no se puede calcular"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🥾 Bootstrap es remuestrear tus datos con reemplazo miles de veces y mirar\n",
    "los percentiles 2,5 y 97,5 de lo que salga.\n",
    "\n",
    "- ✅ Primero se comprueba donde ya hay fórmula: 776,94 a 830,85 contra 776,84\n",
    "a 830,68. Después se usa donde no la hay.\n",
    "\n",
    "- 🔁 Sirve para la mediana, para una razón, para una diferencia y para\n",
    "cualquier cosa que sepas calcular sobre una muestra.\n",
    "\n",
    "- 🧩 Cuando el número junta dos columnas, se remuestrean **filas\n",
    "enteras**.\n",
    "\n",
    "- 🚫 Y falla para lo que depende de un dato del borde: el intervalo del máximo\n",
    "tiene por techo el máximo que ya viste, y eso es mentira.\n",
    "\n",
    "En el capítulo 12 el valor p se calculó barajando\n",
    "etiquetas, que es este mismo truco con otra cara: cuando no hay fórmula, se\n",
    "simula. Y el vocabulario suelto está en el\n",
    "[glosario de IA](https://missyera.com/glosario-ia/) 📖\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 11 de **Estadística desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/estadistica-desde-cero/remuestreo-y-bootstrap/\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
}
