{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cuándo la campana no aplica\n",
    "\n",
    "La distribución normal, cómo se comprueba de verdad, y por qué el test de normalidad dice que no en cuanto tienes datos.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 5 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/la-campana/\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": [
    "La campana de Gauss es la figura más famosa de la estadística, y también la\n",
    "más aplicada donde no toca 🔔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "f(x)=1σ2πe−12(x−μσ)2\n",
    "\n",
    "la altura de la campana en cada punto, que solo depende de a cuántas desviaciones del centro estás"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Importa por una razón muy concreta: **media docena de herramientas del\n",
    "libro la dan por supuesta**. La regla del 68%, los intervalos de\n",
    "confianza clásicos, la prueba t, la detección de atípicos por desviaciones. Si\n",
    "tus datos no son campana, esas herramientas no se rompen con un error rojo: te\n",
    "devuelven un número equivocado con toda la calma 😶\n",
    "\n",
    "Así que vamos a aprender a comprobarlo. Y de paso vamos a ver que la forma\n",
    "\"oficial\" de comprobarlo, la prueba de normalidad, es bastante inútil.\n",
    "\n",
    "Y contéstate esto antes de seguir: **¿cuántas veces has usado la regla del 68% sin mirar si tus datos eran una campana?** Yo, muchas, y por eso este capítulo existe 🔔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Primero, mirarlo"
   ]
  },
  {
   "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",
    "\n",
    "\n",
    "def histograma(serie, tramos=10):\n",
    "    \"\"\"Un histograma de texto, que se ve igual de bien y no necesita gráficos.\"\"\"\n",
    "    cuenta, bordes = np.histogram(serie.dropna(), bins=tramos)\n",
    "    for i in range(tramos):\n",
    "        print('%8.0f a %8.0f | %4d %s'\n",
    "              % (bordes[i], bordes[i + 1], cuenta[i], '#' * int(cuenta[i] / 20)))\n",
    "\n",
    "\n",
    "histograma(v['monto'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con eso ya sabes que `monto` no es una campana, y no hizo falta\n",
    "ninguna prueba 👀\n",
    "\n",
    "Una campana sube y baja simétrica. Esto sube de golpe, tiene su pico entre\n",
    "196 y 869, y luego baja con una **cola larga por la derecha** que\n",
    "llega hasta 4.237. Y por la izquierda hay cinco filas sueltas en territorio\n",
    "negativo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los dos números que resumen la forma"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['monto', 'unidades', 'satisfaccion', 'descuento']:\n",
    "    s = v[col].dropna()\n",
    "    print('%-13s asimetria %7.4f | curtosis %7.4f' % (col, s.skew(), s.kurtosis()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La asimetría** dice hacia dónde cae la cola. Cero es\n",
    "simétrico, positivo es cola a la derecha, negativo a la izquierda. Como regla de\n",
    "campo, entre -0,5 y 0,5 es bastante simétrico y por encima de 1 la cola ya es\n",
    "evidente.\n",
    "\n",
    "**La curtosis** (la que devuelve pandas es la de exceso) dice si\n",
    "la campana es puntiaguda o achatada. Cero es como la normal, positivo es más\n",
    "picuda con colas más pesadas, negativo es más plana.\n",
    "\n",
    "Ahora léelo como si fueran cuatro personas 🙂\n",
    "\n",
    "- 💰 `monto`: asimetría 1,3405. Cola a la derecha, confirmado.\n",
    "\n",
    "- 📦 `unidades`: 0,1843 y -0,3351. Esto sí parece una campana.\n",
    "\n",
    "- ⭐ `satisfaccion`: simétrica pero con curtosis -1,3089, o sea muy\n",
    "plana. Eso no es campana, es una meseta: los cinco valores salen casi igual de\n",
    "veces.\n",
    "\n",
    "- 🏷️ `descuento`: lo mismo, plana."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ahora la prueba oficial, y la decepción"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La prueba de Shapiro-Wilk contesta \"¿podrían estos datos venir de una\n",
    "normal?\". Si el valor p es chico, la respuesta es no.\n",
    "\n",
    "Se la aplicamos a `unidades`, la que mejor pinta tenía:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(stats.shapiro(v['unidades']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "p = 0,00000000000000052 😬\n",
    "\n",
    "O sea que la prueba dice, con toda la contundencia del mundo, que\n",
    "`unidades` **no** es normal. La misma columna que tenía\n",
    "asimetría 0,18 y que en el capítulo 4 cumplía la regla del 68% clavada\n",
    "(68,70%).\n",
    "\n",
    "¿Quién miente? Ninguno. Mira lo que pasa cuando le doy los mismos datos en\n",
    "trozos de distinto tamaño:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for n in [20, 50, 100, 300, 1000, 3000]:\n",
    "    muestra = v['unidades'].sample(n, random_state=7)\n",
    "    print('con %5d filas -> p = %.6f' % (n, stats.shapiro(muestra).pvalue))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el truco entero 🎩\n",
    "\n",
    "Los mismos datos. Con 50 filas la prueba dice \"normal, adelante\". Con 1.000\n",
    "dice \"de ninguna manera\". **La columna no cambió: cambió cuánta evidencia\n",
    "tienes.**\n",
    "\n",
    "Y no es que Shapiro sea mala. Mira qué pasa con datos que *sí* vienen\n",
    "de una normal de verdad:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador = np.random.default_rng(7)\n",
    "\n",
    "for n in [100, 1000, 3000]:\n",
    "    print('normal de verdad con %5d datos -> p = %.4f'\n",
    "          % (n, stats.shapiro(generador.normal(size=n)).pvalue))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con datos normales de verdad, el p se queda alto por muchos datos que le\n",
    "des ✅\n",
    "\n",
    "O sea que la prueba funciona perfectamente. Lo que pasa es que responde a una\n",
    "pregunta que no es la tuya:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Lo que crees que preguntas | Lo que la prueba contesta |\n",
    "|---|---|\n",
    "| ¿Mis datos son normales? | ¿Tengo evidencia suficiente para afirmar que no lo son exactamente? |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ningún dato real es *exactamente* normal. Con suficientes filas\n",
    "siempre se nota. Por eso, en la práctica: **mira el histograma y la\n",
    "asimetría, y usa la prueba como acompañamiento, nunca como sentencia** 🧭\n",
    "\n",
    "Guárdate esta idea, porque es la misma que va a salir en el capítulo 14 con\n",
    "todas las pruebas: *significativo* no quiere decir *importante*.\n",
    "Con muchos datos, todo sale significativo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que sí decide: comparar cuantiles"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mi comprobación favorita, porque contesta la pregunta práctica de verdad:\n",
    "\"¿me equivoco mucho si trato esto como normal?\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "u = v['unidades']\n",
    "m, s = u.mean(), u.std()\n",
    "\n",
    "for q in [0.05, 0.25, 0.50, 0.75, 0.95]:\n",
    "    print('cuantil %.2f -> observado %6.2f | si fuera normal %6.2f'\n",
    "          % (q, u.quantile(q), stats.norm.ppf(q, m, s)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira qué cerca 😍 En los extremos, que es donde más duele equivocarse, la\n",
    "diferencia es de una décima de unidad.\n",
    "\n",
    "Entonces, ¿es `unidades` normal? Formalmente no, y Shapiro tiene\n",
    "razón. ¿Puedo tratarla como normal para decidir cosas de negocio?\n",
    "**Perfectamente.**\n",
    "\n",
    "Esa es la respuesta madura, y es la que quiero que te lleves: la normalidad\n",
    "no es un sí o un no, es *cuánto me cuesta suponerla* 💡"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuando no hay campana: transformar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Volvamos a `monto`, que sí estaba torcido de verdad. Hay un truco\n",
    "clásico para colas por la derecha: aplicar el logaritmo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "positivos = v.loc[v['monto'] > 0, 'monto']\n",
    "\n",
    "print('filas positivas: %d de %d' % (len(positivos), len(v)))\n",
    "print('asimetria antes:   %.4f' % positivos.skew())\n",
    "print('asimetria despues: %.4f' % np.log(positivos).skew())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 1,4278 a -0,1585. El logaritmo aplasta la cola y deja algo casi\n",
    "simétrico 🪄\n",
    "\n",
    "Funciona porque el logaritmo comprime los números grandes mucho más que los\n",
    "chicos: la distancia entre 100 y 1.000 pasa a ser la misma que entre 1.000 y\n",
    "10.000.\n",
    "\n",
    "Fíjate en el precio de la operación: hay que tirar 21 filas, las que tienen\n",
    "monto negativo o cero, porque el logaritmo de un número negativo no existe.\n",
    "Nunca transformes sin contar cuántas filas te dejas 🧾\n",
    "\n",
    "Y hay un regalo escondido en esto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('mediana de las positivas:  %.2f' % positivos.median())\n",
    "print('exp(media del logaritmo):  %.2f' % np.exp(np.log(positivos).mean()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi el mismo número. Lo que hiciste al promediar en escala logarítmica y\n",
    "volver fue calcular la **media geométrica**, la del capítulo 3. Y\n",
    "en una distribución con cola por la derecha, la media geométrica cae encima de\n",
    "la mediana 🎯\n",
    "\n",
    "Lo que cuesta transformar es la interpretación. Después de la\n",
    "transformación tus resultados están en \"log de soles\", que no significa nada\n",
    "para nadie. Hay que deshacerla para contar el resultado, y ahí es donde se\n",
    "cometen errores. Yo transformo solo cuando la herramienta lo necesita de\n",
    "verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error del capítulo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a intentar partir la satisfacción en diez tramos iguales, que es lo que\n",
    "harías para ver su forma con más detalle:"
   ]
  },
  {
   "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",
    "    pd.qcut(v['satisfaccion'], 10)\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: Bin edges must be unique: Index([1.0, 1.0, 1.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 5.0], dtype='float64', name='satisfaccion').\n",
    "You can drop duplicate edges by setting the 'duplicates' kwarg\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este error es de los buenos, porque te está enseñando algo del dato y no del\n",
    "código 🎓\n",
    "\n",
    "`qcut` parte en tramos con *la misma cantidad de filas cada\n",
    "uno*. Para hacer diez tramos necesita diez cortes distintos, y\n",
    "`satisfaccion` solo tiene cinco valores posibles. Al buscar el borde\n",
    "del decil 10 y del decil 20 encuentra el mismo 1.0 en los dos, y se planta.\n",
    "\n",
    "La tentación es hacerle caso al mensaje y poner\n",
    "`duplicates='drop'`. No lo hagas sin pensar: eso taparía el problema y\n",
    "te dejaría tramos de tamaños muy distintos disfrazados de deciles.\n",
    "\n",
    "Lo correcto es aceptar lo que el error te está diciendo: **esa columna\n",
    "no tiene diez tramos, tiene cinco categorías**. Se cuenta con\n",
    "`value_counts` y se acabó."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y dos más, de intentar meter en la campana lo que no cabe"
   ]
  },
  {
   "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.norm.fit(v['ciudad'])\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: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ajustar una normal es buscar la media y la desviación que mejor encajan, y las dos son cuentas. Sobre nombres de ciudad no hay nada que ajustar, y scipy lo dice con un mensaje bastante peor de lo que merece la pregunta 😅"
   ]
  },
  {
   "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.zscore(v['ciudad'])\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: unsupported operand type(s) for /: 'str' and 'int'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La puntuación z es restar la media y dividir entre la desviación, y ahí se ve la división imposible en el mensaje. Todo este capítulo vive sobre variables numéricas, y las categóricas tienen su propio capítulo en el 13."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Practica 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. ¿De qué forma es la satisfacción?\n",
    "\n",
    "Píntala con el histograma de texto y di qué forma tiene."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(v['satisfaccion'].value_counts().sort_index())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "satisfaccion\n",
    "1.0    560\n",
    "2.0    537\n",
    "3.0    551\n",
    "4.0    558\n",
    "5.0    563\n",
    "Name: count, dtype: int64\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "560, 537, 551, 558, 563. Es **plana**: los cinco valores salen\n",
    "prácticamente las mismas veces 📏\n",
    "\n",
    "A eso se le llama distribución uniforme, y es lo contrario de una campana. En\n",
    "una campana el centro tiene muchos más casos que los extremos; aquí un 1 es tan\n",
    "frecuente como un 3.\n",
    "\n",
    "Esto tiene una lectura de negocio incómoda: si tus clientes puntúan\n",
    "absolutamente al azar entre 1 y 5, la encuesta no está midiendo satisfacción,\n",
    "está midiendo ruido. Una encuesta real casi siempre sale torcida hacia arriba,\n",
    "porque la gente contenta responde más.\n",
    "\n",
    "Y explica el -1,3089 de curtosis del principio. Vamos a comprobar que ese\n",
    "número es justo el de una uniforme:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador = np.random.default_rng(7)\n",
    "uniforme = pd.Series(generador.integers(1, 6, size=3000))\n",
    "\n",
    "print('satisfaccion -> curtosis %.4f | asimetria %.4f'\n",
    "      % (v['satisfaccion'].kurtosis(), v['satisfaccion'].skew()))\n",
    "print('uniforme 1-5 -> curtosis %.4f | asimetria %.4f'\n",
    "      % (uniforme.kurtosis(), uniforme.skew()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "satisfaccion -> curtosis -1.3089 | asimetria -0.0149\n",
    "uniforme 1-5 -> curtosis -1.2944 | asimetria -0.0219\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "-1,3089 contra -1,2944. Clavado 🎯\n",
    "\n",
    "O sea que la curtosis no solo te dice \"no es normal\": te dice\n",
    "*qué* es. Un -1,3 en una variable de cinco valores es la firma de una\n",
    "uniforme."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La normalidad dentro de un segmento\n",
    "\n",
    "En el capítulo 3 vimos que dentro de cada segmento la media\n",
    "y la mediana coincidían. ¿Quiere eso decir que cada segmento es normal?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for seg in ['Bodega', 'Mayorista']:\n",
    "    s = v.loc[v['segmento'] == seg, 'monto']\n",
    "    print('%-10s asimetria %7.4f | curtosis %7.4f | minimo %8.2f'\n",
    "          % (seg, s.skew(), s.kurtosis(), s.min()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "Bodega     asimetria -0.7752 | curtosis  3.4590 | minimo  -224.61\n",
    "Mayorista  asimetria -0.4560 | curtosis  3.0510 | minimo -2497.72\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pues no, y la respuesta es más interesante de lo que esperaba 🤔\n",
    "\n",
    "Los dos segmentos tienen asimetría **negativa**, o sea cola a la\n",
    "*izquierda*, justo al revés que el total. Y curtosis alrededor de 3, que\n",
    "es mucha: colas pesadas.\n",
    "\n",
    "La explicación está en la tercera columna: los mínimos son negativos. Esas\n",
    "ventas negativas, que son poquísimas, tiran de la cola izquierda dentro de cada\n",
    "segmento.\n",
    "\n",
    "Y en el total esa cola izquierda desaparecía porque la aplastaba la cola\n",
    "derecha enorme que creaba la mezcla de segmentos.\n",
    "\n",
    "Conclusión práctica: **simétrico no es lo mismo que normal**. Que\n",
    "la media y la mediana coincidan es necesario pero no suficiente. Hay que mirar\n",
    "también las colas 👀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto te cuesta suponer normalidad en el monto\n",
    "\n",
    "Haz con `monto` la comparación de cuantiles que\n",
    "hicimos con `unidades`. ¿Dónde se rompe más?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m, s = v['monto'].mean(), v['monto'].std()\n",
    "\n",
    "for q in [0.05, 0.25, 0.50, 0.75, 0.95, 0.99]:\n",
    "    obs = v['monto'].quantile(q)\n",
    "    teo = stats.norm.ppf(q, m, s)\n",
    "    print('cuantil %.2f -> observado %8.2f | normal %8.2f | error %+8.2f'\n",
    "          % (q, obs, teo, obs - teo))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cuantil 0.05 -> observado   119.03 | normal  -433.15 | error  +552.18\n",
    "cuantil 0.25 -> observado   252.69 | normal   296.55 | error   -43.86\n",
    "cuantil 0.50 -> observado   533.63 | normal   803.76 | error  -270.13\n",
    "cuantil 0.75 -> observado  1068.68 | normal  1310.97 | error  -242.29\n",
    "cuantil 0.95 -> observado  2431.76 | normal  2040.67 | error  +391.08\n",
    "cuantil 0.99 -> observado  3058.59 | normal  2553.15 | error  +505.44\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aquí sí duele 💥\n",
    "\n",
    "Suponer normalidad dice que el 5% de las ventas debería estar\n",
    "**por debajo de -433,15 soles**. En la realidad el percentil 5 es\n",
    "+119,03, o sea que la normal se equivoca en 552 soles justo ahí. Y por arriba\n",
    "hace lo contrario: el percentil 99 real es 3.058,59 y la normal lo pone en\n",
    "2.553,15, o sea que se queda 505 soles corta.\n",
    "\n",
    "Fíjate en el patrón, que es la firma de una cola por la derecha: la normal se\n",
    "inventa valores muy negativos que no existen y se queda corta en los muy\n",
    "grandes que sí existen.\n",
    "\n",
    "El motivo es de fondo: una normal va de menos infinito a más infinito, y una\n",
    "venta no puede ser muy negativa. Siempre que tu variable tenga un tope natural\n",
    "(no puede bajar de cero, no puede pasar de 100), la normal va a mentir en esa\n",
    "punta.\n",
    "\n",
    "Compáralo con `unidades`, donde el error era de una décima. Misma\n",
    "suposición, dos costes completamente distintos ⚖️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Normaliza tú un dato: la puntuación z\n",
    "\n",
    "Convierte el monto a \"cuántas desviaciones estoy del\n",
    "promedio\" y comprueba qué le pasa a la media y a la desviación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "z = (v['monto'] - v['monto'].mean()) / v['monto'].std()\n",
    "\n",
    "print('media de z:      %.6f' % z.mean())\n",
    "print('desviacion de z: %.6f' % z.std())\n",
    "print('asimetria de z:  %.4f' % z.skew())\n",
    "print()\n",
    "print('la venta mas grande esta a %.2f desviaciones' % z.max())\n",
    "print('la mas chica, a %.2f' % z.min())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "media de z:      -0.000000\n",
    "desviacion de z: 1.000000\n",
    "asimetria de z:  1.3405\n",
    "\n",
    "la venta mas grande esta a 4.57 desviaciones\n",
    "la mas chica, a -4.39\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La media queda en 0 y la desviación en 1, que es lo que hace la puntuación z:\n",
    "poner cualquier variable en la misma escala 📐\n",
    "\n",
    "Pero mira la tercera línea, que es la importante: **la asimetría sigue\n",
    "siendo 1,3405**, exactamente la de antes.\n",
    "\n",
    "Estandarizar **no normaliza**. Mueve y estira, pero no cambia la\n",
    "forma. Es un malentendido de los grandes, y lo alimenta el nombre de\n",
    "`StandardScaler` en scikit-learn, que muchísima gente cree que\n",
    "convierte los datos en normales.\n",
    "\n",
    "La z sirve para comparar variables de escalas distintas y para los modelos que\n",
    "lo necesitan. No sirve para arreglar una cola 🚫"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. El test que se rinde con muchos datos\n",
    "\n",
    "Reproduce el fenómeno del capítulo con datos fabricados:\n",
    "genera una normal con un defecto minúsculo y mira desde qué tamaño lo detecta\n",
    "Shapiro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "generador = np.random.default_rng(11)\n",
    "\n",
    "for n in [100, 500, 2000, 5000]:\n",
    "    casi = np.concatenate([\n",
    "        generador.normal(size=int(n * 0.99)),\n",
    "        generador.normal(loc=6, size=int(n * 0.01)),\n",
    "    ])\n",
    "    print('n=%6d con 1%% de intrusos -> p = %.6f'\n",
    "          % (n, stats.shapiro(casi).pvalue))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "n=   100 con 1% de intrusos -> p = 0.000005\n",
    "n=   500 con 1% de intrusos -> p = 0.000000\n",
    "n=  2000 con 1% de intrusos -> p = 0.000000\n",
    "n=  5000 con 1% de intrusos -> p = 0.000000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con solo un 1% de datos \"intrusos\" el test ya se cae con 100 filas 😮\n",
    "\n",
    "Y aquí está lo que hay que sacar de todo esto. La prueba de normalidad es\n",
    "extremadamente sensible: detecta desviaciones que a ti te dan exactamente igual\n",
    "para tomar una decisión.\n",
    "\n",
    "Un p chiquito no significa \"esto está mal, no lo uses\". Significa \"hay algo\n",
    "que no es exactamente normal\". Que eso importe o no lo decides tú, mirando la\n",
    "comparación de cuantiles del ejercicio 3 🧑‍⚖️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Tu chequeo de forma, en una función\n",
    "\n",
    "Junta todo en algo que puedas correr sobre cualquier\n",
    "columna y te diga qué tienes delante."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def que_forma_tiene(serie, nombre):\n",
    "    s = serie.dropna()\n",
    "    a, k = s.skew(), s.kurtosis()\n",
    "\n",
    "    if abs(a) > 1:\n",
    "        forma = 'cola larga hacia la ' + ('derecha' if a > 0 else 'izquierda')\n",
    "    elif k < -1:\n",
    "        forma = 'plana, parece uniforme'\n",
    "    elif abs(a) < 0.5 and abs(k) < 1:\n",
    "        forma = 'razonablemente acampanada'\n",
    "    else:\n",
    "        forma = 'algo torcida, mirala'\n",
    "\n",
    "    return ('%-13s asimetria %+7.4f | curtosis %+7.4f -> %s'\n",
    "            % (nombre, a, k, forma))\n",
    "\n",
    "\n",
    "for col in ['monto', 'unidades', 'satisfaccion', 'descuento']:\n",
    "    print(que_forma_tiene(v[col], col))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "monto         asimetria +1.3405 | curtosis +1.5458 -> cola larga hacia la derecha\n",
    "unidades      asimetria +0.1843 | curtosis -0.3351 -> razonablemente acampanada\n",
    "satisfaccion  asimetria -0.0149 | curtosis -1.3089 -> plana, parece uniforme\n",
    "descuento     asimetria -0.0157 | curtosis -1.1604 -> plana, parece uniforme\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro columnas etiquetadas en una pasada 🙌\n",
    "\n",
    "Los umbrales (1 para la asimetría, 1 para la curtosis, 0,5) son convenciones\n",
    "de campo, no leyes. Lo importante es que estén escritos y que los uses igual\n",
    "siempre, en vez de decidir a ojo según el día.\n",
    "\n",
    "Y fíjate en lo que **no** hace esta función: no llama a ninguna\n",
    "prueba de normalidad. Después de lo de arriba, ya sabes por qué 😌"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El 68% sobre tres columnas\n",
    "\n",
    "Comprueba en tres columnas qué porcentaje cae dentro de una desviación, y compáralo con el 68,3% que promete la campana."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['monto', 'unidades', 'satisfaccion']:\n",
    "    s = v[col].dropna()\n",
    "    dentro = ((s > s.mean() - s.std()) & (s < s.mean() + s.std())).mean()\n",
    "    print('%-13s dentro de una desviacion: %.1f%%   (la campana dice 68,3%%)'\n",
    "          % (col, dentro * 100))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "monto         dentro de una desviacion: 81.8%   (la campana dice 68,3%)\n",
    "unidades      dentro de una desviacion: 68.7%   (la campana dice 68,3%)\n",
    "satisfaccion  dentro de una desviacion: 59.4%   (la campana dice 68,3%)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una acierta y las otras dos fallan por lados contrarios 😬\n",
    "\n",
    "`unidades` da 68,7% y la campana le queda como un guante. El `monto` da 81,8%, o sea que hay **más** concentración en el centro de la que la campana espera, porque la cola estira la desviación. Y `satisfaccion` da 59,4%, que falla por lo contrario: solo tiene cinco valores posibles y no hay forma de que una escala del 1 al 5 se parezca a una curva continua.\n",
    "\n",
    "Esta comprobación cabe en tres líneas y contesta mejor que cualquier prueba de normalidad, que es de lo que va este capítulo 🔔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8. Cuando el logaritmo sí endereza\n",
    "\n",
    "Aplica logaritmo al monto y mira si la asimetría se va."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "log = np.log(v['monto'][v['monto'] > 0])\n",
    "print('asimetria del monto:      %.3f' % v['monto'].skew())\n",
    "print('asimetria del logaritmo:  %.3f' % log.skew())\n",
    "print('la prueba de normalidad sobre el logaritmo: p = %.6f'\n",
    "      % stats.shapiro(log.sample(500, random_state=0)).pvalue)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "asimetria del monto:      1.340\n",
    "asimetria del logaritmo:  -0.158\n",
    "la prueba de normalidad sobre el logaritmo: p = 0.000011\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira las dos primeras líneas y después la tercera, que se contradicen 🤨\n",
    "\n",
    "La asimetría se fue casi entera: de 1,340 a -0,158, o sea que el logaritmo **enderezó la cola**, que es justo para lo que se usa. Y aun así la prueba de normalidad dice que no, con un p de 0,000011.\n",
    "\n",
    "Las dos cosas son verdad a la vez y esa es la lección: la prueba contesta \"¿es exactamente normal?\" y la respuesta con 500 datos casi siempre es no. Lo que tú necesitas saber es \"¿se parece lo bastante para lo que voy a hacer?\", y eso lo contesta la asimetría, no el valor p 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El 68% que salió 81,6%"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de cerrar, la trampa de este capítulo. La regla es correcta, la cuenta es correcta, y el resultado no cuadra 🔔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Aplicas la regla que todo el mundo sabe: entre la media menos una desviación y la media más una está el 68% de los datos. La usas para poner el rango normal de una venta.\n",
    "\n",
    "```\n",
    "mu, sd = v['monto'].mean(), v['monto'].std()\n",
    "dentro = ((v['monto'] > mu - sd) & (v['monto'] < mu + sd)).mean()\n",
    "\n",
    "print(round(dentro * 100, 1))\n",
    "# 81.8\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "81,8% donde la campana prometía 68,3% 🔔 Trece puntos y medio de diferencia, y no está mal calculado.\n",
    "\n",
    "La regla del 68-95-99,7 **no es una ley de la naturaleza**: es una propiedad de la campana. Y el monto no es una campana, es una distribución con cola a la derecha (asimetría 1,3405), así que hay muchos más datos apelotonados cerca del centro de lo que la campana esperaría.\n",
    "\n",
    "Hay una comprobación de diez segundos que lo canta: `mu - 2 * sd` da -700,22 soles. Si tu rango normal incluye una venta negativa, la campana no es la forma de tus datos.\n",
    "\n",
    "Antes de usar cualquier regla que venga de la campana, mira el histograma. Y si sale con cola, usa percentiles: el rango del 5 al 95 no supone ninguna forma y siempre dice la verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Shapiro suspende unidades con p = 5,2e-16 y la aprueba con p = 0,4422 si le das solo 20 filas. ¿Qué concluyes?\n",
    "\n",
    "a) Que la prueba mide cuánta evidencia tengo, no si los datos son normales\n",
    "\n",
    "b) Que con 20 filas la columna sí es normal y con 3.000 deja de serlo\n",
    "\n",
    "c) Que la prueba de Shapiro está mal implementada\n",
    "\n",
    "d) Que hay que usar siempre muestras de 20 para estas pruebas\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Son exactamente los mismos datos. Lo único que cambió es cuántos le di.\n",
    "\n",
    "*c)* Funciona perfectamente: con datos normales de verdad el p se queda alto aunque le des 3.000 filas.\n",
    "\n",
    "*d)* Eso sería elegir el tamaño hasta que salga lo que quieres, que es el capítulo 17.\n",
    "\n",
    "Ningún dato real es exactamente normal, y con suficientes filas siempre se nota."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 👀 El histograma decide más que cualquier prueba. Diez líneas de texto y ya\n",
    "sabías que `monto` no era una campana.\n",
    "\n",
    "- 📐 Asimetría dice hacia dónde cae la cola; curtosis, si es picuda o plana.\n",
    "Una curtosis de -1,3 en cinco valores es la firma de una uniforme.\n",
    "\n",
    "- 🎭 Las pruebas de normalidad no contestan lo que crees. Los mismos datos dan\n",
    "p = 0,44 con 50 filas y p = 0,000000 con 1.000.\n",
    "\n",
    "- 🧭 La pregunta útil no es \"¿es normal?\" sino \"¿cuánto me cuesta suponer que\n",
    "lo es?\". En unidades el error de cuantiles era de una décima; en monto, la normal\n",
    "se inventaba ventas de -433 soles.\n",
    "\n",
    "- 🪄 El logaritmo endereza colas por la derecha: la asimetría del monto pasó\n",
    "de 1,4278 a -0,1585. Pero cuesta 21 filas y complica la interpretación.\n",
    "\n",
    "- 📏 Estandarizar no normaliza. La z deja media 0 y desviación 1, y la\n",
    "asimetría intacta.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La campana no es cómo son los datos. Es una suposición, y se comprueba."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y para mirar la forma de tus datos sin escribir código, un histograma en un\n",
    "tablero hace exactamente el mismo trabajo:\n",
    "[guía de Power BI](https://missyera.com/guias/power-bi-desde-cero/) 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Han salido tres veces ya: las ventas negativas y las de cuatro mil soles. En\n",
    "el capítulo 6 vamos a por ellas, con las tres formas de detectarlas y la\n",
    "pregunta que casi nadie hace, que es si hay que quitarlas o no 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preguntas frecuentes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué es un histograma?Un gráfico de barras pegadas donde cada barra cuenta cuántos datos caen dentro de un rango. Sirve para ver de un vistazo la forma que tienen tus datos: si están centrados, si tienen cola, si hay dos grupos escondidos.\n",
    "\n",
    "¿Cómo se hace un histograma?Se parte el rango de los datos en intervalos del mismo ancho, se cuenta cuántos datos caen en cada uno y se dibuja una barra por intervalo. Lo único que hay que elegir es cuántos intervalos, y esa decisión cambia lo que ves.\n",
    "\n",
    "¿Qué diferencia hay entre un histograma y un gráfico de barras?Las barras del histograma van pegadas porque el eje es continuo: representan tramos de un número. Las de un gráfico de barras van separadas porque son categorías sueltas, como canal o ciudad.\n",
    "\n",
    "¿Qué es un histograma de frecuencias?Es el mismo histograma llamado por su nombre completo: la altura de cada barra es la frecuencia, o sea cuántas veces aparece un valor dentro de ese tramo.\n",
    "\n",
    "¿Qué es la campana de Gauss?El nombre coloquial de la distribución normal, por la forma que tiene su histograma: simétrica, con casi todo en el medio y colas que se van apagando a los lados.\n",
    "\n",
    "¿Cómo sé si mis datos son normales?Mirando el histograma primero y con la prueba de Shapiro Wilk después. Y ojo con lo que contesta la prueba: con muchos datos casi siempre dice que no, y eso no significa que no te sirvan.\n",
    "\n",
    "¿Cómo se hace un histograma en Excel?Seleccionas los datos, Insertar, gráficos estadísticos, histograma. Excel elige los intervalos solo, y eso es justo lo que conviene revisar a mano: cambiar el ancho cambia la forma que ves."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 5 de **Estadística desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/estadistica-desde-cero/la-campana/\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
}
