{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cuando la pregunta no es predecir\n",
    "\n",
    "Coeficientes con su valor p y su intervalo, odds que se dicen en voz alta, y la columna que el boosting adora y la regresión no distingue del ruido.\n",
    "\n",
    "Cuaderno de práctica del capítulo 22 de **Machine learning desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/machine-learning-desde-cero/regresion-con-statsmodels/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"Y29uc3QgICAgICAgICAgICAgICAgMTIuMDUKc2F0aXNmYWNjaW9uICAgICAgICAgIDEuMDAKbW9udG8gICAgICAgICAgICAgICAgIDEuMzMKdW5pZGFkZXMgICAgICAgICAgICAgIDEuMjgKcHJlY2lvX3VuaXRhcmlvICAgICAgIDEuNjAKc2luX2NvbXByYV9wcmV2aWEgICAgIDEuMDA=\",\n",
    "    2: \"ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2luICAgICBjb24gICBwX3NpbiAgIHBfY29uCkMoY2FuYWwpW1QuVGllbmRhXSAgICAgICAgIDAuNjQyOSAgMC42NDE0ICAwLjAwMDAgIDAuMDAwMApDKGNhbmFsKVtULldlYl0gICAgICAgICAgICAwLjQ3ODEgIDAuNDc4MSAgMC4wMDAwICAwLjAwMDAKQyhjYW5hbClbVC5XaGF0c0FwcF0gICAgICAgMC44NTAyICAwLjg0NzAgIDAuMDAwMCAgMC4wMDAwCkMoc2VnbWVudG8pW1QuSG9yZWNhXSAgICAgIDEuMDQ0NCAgMS4wNDM4ICAwLjAwMDAgIDAuMDAwMApDKHNlZ21lbnRvKVtULk1heW9yaXN0YV0gICAxLjI3OTkgIDEuMjgyOSAgMC4wMDAwICAwLjAwMDAKQyhzZWdtZW50bylbVC5NaW5pbWFya2V0XSAgMC43OTg5ICAwLjc5MzEgIDAuMDAwMCAgMC4wMDAwCkludGVyY2VwdCAgICAgICAgICAgICAgICAgLTEuNzYyNyAtMS44NzMyICAwLjAwMDAgIDAuMDAwMAptb250byAgICAgICAgICAgICAgICAgICAgICAwLjAwMDIgIDAuMDAwMSAgMC4xNTYxICAwLjQ1NzUKcHJlY2lvX3VuaXRhcmlvICAgICAgICAgICAgICAgTmFOICAwLjAwMDUgICAgIE5hTiAgMC4wNjE4CnNhdGlzZmFjY2lvbiAgICAgICAgICAgICAgIDAuMjg2OCAgMC4yODgxICAwLjAwMDAgIDAuMDAwMApzaW5fY29tcHJhX3ByZXZpYSAgICAgICAgIC0wLjkwMDMgLTAuODk2MSAgMC4wMDAwICAwLjAwMDAKdW5pZGFkZXMgICAgICAgICAgICAgICAgICAgICAgTmFOICAwLjAwOTQgICAgIE5hTiAgMC4yNDky\",\n",
    "    3: \"Y29udHJhIEJvZGVnYToKQyhzZWdtZW50bylbVC5Ib3JlY2FdICAgICAgICAyLjg0MTcKQyhzZWdtZW50bylbVC5NYXlvcmlzdGFdICAgICAzLjU5NjEKQyhzZWdtZW50bylbVC5NaW5pbWFya2V0XSAgICAyLjIyMzIKCmNvbnRyYSBNYXlvcmlzdGE6CkMoc2VnbWVudG8sIFRyZWF0bWVudChyZWZlcmVuY2U9J01heW9yaXN0YScpKVtULkJvZGVnYV0gICAgICAgIDAuMjc4MQpDKHNlZ21lbnRvLCBUcmVhdG1lbnQocmVmZXJlbmNlPSdNYXlvcmlzdGEnKSlbVC5Ib3JlY2FdICAgICAgICAwLjc5MDIKQyhzZWdtZW50bywgVHJlYXRtZW50KHJlZmVyZW5jZT0nTWF5b3Jpc3RhJykpW1QuTWluaW1hcmtldF0gICAgMC42MTgyCgpzYXRpc2ZhY2Npb24gZW4gbG9zIGRvczogMC4yODY4IHkgMC4yODY4\",\n",
    "    4: \"bG9nLXZlcm9zaW1pbGl0dWQgc2luIGNhbmFsOiAtMTcxOC41OQpsb2ctdmVyb3NpbWlsaXR1ZCBjb24gY2FuYWw6IC0xNjg5LjcKY2hpMiA9IDU3Ljc3IGNvbiAzIGdyYWRvcyBkZSBsaWJlcnRhZCwgcCA9IDEuNzU4ZS0xMgpwc2V1ZG8gUjI6IDAuMDkwMSAtPiAwLjEwNTQ=\",\n",
    "    5: \"IHNlZ21lbnRvICAgIGNhbmFsICBzYXRpc2ZhY2Npb24gICBtb250byAgY29tcHJvICByZXNpZHVvCk1heW9yaXN0YSBXaGF0c0FwcCAgICAgICAgICAgNS4wIDM0NDAuODMgICAgICAgMCAgIC0zLjIzMwpNYXlvcmlzdGEgICBUaWVuZGEgICAgICAgICAgIDUuMCAxOTkxLjgxICAgICAgIDAgICAtMi41OTkKTWF5b3Jpc3RhICAgVGllbmRhICAgICAgICAgICA1LjAgMTk4MC41MyAgICAgICAwICAgLTIuNTk2Ck1heW9yaXN0YSAgIFRpZW5kYSAgICAgICAgICAgNS4wIDE3OTYuMDYgICAgICAgMCAgIC0yLjU1OQpNYXlvcmlzdGEgV2hhdHNBcHAgICAgICAgICAgIDQuMCAyMTM1LjQ4ICAgICAgIDAgICAtMi41MjYKTWF5b3Jpc3RhICAgVGllbmRhICAgICAgICAgICA1LjAgMTU4OC4zNSAgICAgICAwICAgLTIuNTE3CgpyZXNpZHVvcyBwb3IgZW5jaW1hIGRlIHwzfDogMSBkZSAyNzY5\",\n",
    "    6: \"QVVDIGRlIHN0YXRzbW9kZWxzOiAwLjczNDMKZmlsYXMgZGUgZXhhbWVuICAgOiA2OTMKCmxvIHF1ZSBOTyB0cmFlIGRlIGZhYnJpY2E6CiAgLSBwaXBlbGluZQogIC0gdmFsaWRhY2lvbiBjcnV6YWRhCiAgLSBpbXB1dGFjaW9uCiAgLSBvbmUtaG90IGNvbiBoYW5kbGVfdW5rbm93bgogIC0gcHJlZGljdF9wcm9iYQ==\",\n",
    "    7: \"ICAgICAgICAgICAgICBtZWFuICBjb3VudCAgICBvZGRzICBvZGRzX3ZzX2JvZGVnYQpzZWdtZW50byAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCkJvZGVnYSAgICAgIDAuMzc0MCAgICA2NTUgIDAuNTk3NiAgICAgICAgICAxLjAwMDAKSG9yZWNhICAgICAgMC42MzE1ICAgIDY5MiAgMS43MTM3ICAgICAgICAgIDIuODY3OQpNYXlvcmlzdGEgICAwLjcxNDkgICAgNjg0ICAyLjUwNzcgICAgICAgICAgNC4xOTY1Ck1pbmltYXJrZXQgIDAuNTY3OCAgICA3MzggIDEuMzEzNSAgICAgICAgICAyLjE5ODEKCmxvIHF1ZSBkaWNlIGVsIG1vZGVsbywgZGVzY29udGFuZG8gY2FuYWwsIHNhdGlzZmFjY2lvbiB5IGxvIGRlbWFzOgpDKHNlZ21lbnRvKVtULkhvcmVjYV0gICAgICAgIDIuODQxNwpDKHNlZ21lbnRvKVtULk1heW9yaXN0YV0gICAgIDMuNTk2MQpDKHNlZ21lbnRvKVtULk1pbmltYXJrZXRdICAgIDIuMjIzMg==\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo el libro hasta aquí ha ido de **predecir**: dame una fila y\n",
    "te digo la probabilidad. Este capítulo va de la otra pregunta, la que también\n",
    "llega y que se contesta con otras herramientas 🔬\n",
    "\n",
    "Y te la hago a ti primero: **¿cuántas veces has contestado \"sí, influye\" sin poder decir cuánto ni con qué margen?** Este capítulo es para no tener que volver a hacerlo 🔬\n",
    "\n",
    "\"¿Ser Mayorista influye de verdad en que cierre la venta, o es que los\n",
    "mayoristas casualmente compran por WhatsApp y lo que influye es el canal?\"\n",
    "\n",
    "Eso no es una predicción. Es una pregunta sobre **si el efecto\n",
    "existe**, y scikit-learn no está hecho para contestarla. Le pides los\n",
    "coeficientes y te da trece números pelados, sin errores estándar, sin\n",
    "intervalos y sin valores p.\n",
    "\n",
    "Para eso está statsmodels, que es la librería que usaría alguien de\n",
    "estadística y que en este mundo se nombra poco 💜"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los mismos coeficientes, con lo que les falta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Empecemos por lo importante: **no es otro modelo**. Es la misma\n",
    "regresión logística del capítulo 13, ajustada igual."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import statsmodels.api as sm\n",
    "import statsmodels.formula.api as smf\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v['ciudad'] = (v['ciudad'].str.strip().str.lower()\n",
    "                   .str.normalize('NFKD')\n",
    "                   .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "    for col in ['fecha', 'fecha_ultima_compra']:\n",
    "        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "ventas = carga_limpia(URL)\n",
    "ventas['sin_compra_previa'] = ventas['fecha_ultima_compra'].isna().astype(int)\n",
    "ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']\n",
    "datos = ventas.dropna(subset=['satisfaccion']).copy()\n",
    "\n",
    "FORMULA = ('compro ~ satisfaccion + monto + sin_compra_previa '\n",
    "           '+ C(segmento) + C(canal)')\n",
    "modelo = smf.logit(FORMULA, data=datos).fit(disp=0)\n",
    "\n",
    "print('statsmodels', sm.__version__)\n",
    "print('filas usadas:', int(modelo.nobs))\n",
    "print(modelo.summary().tables[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa tabla es lo que statsmodels añade, y son cuatro columnas 🔎\n",
    "\n",
    "- 📏 **`std err`**: cuánto se movería ese coeficiente\n",
    "si repitieras el estudio con otra muestra. Es la barra de error.\n",
    "\n",
    "- ➗ **`z`**: el coeficiente dividido entre esa barra,\n",
    "o sea cuántas veces cabe el ruido dentro del efecto.\n",
    "\n",
    "- 🎲 **`P>|z|`**: la probabilidad de ver algo así\n",
    "de grande si el efecto real fuera cero.\n",
    "\n",
    "- 📐 **`[0.025 0.975]`**: el intervalo donde\n",
    "razonablemente está el valor de verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "z=β^ee(β^),IC95%=exp(β^±1,96ee)\n",
    "\n",
    "el coeficiente dividido entre su error estándar dice cuántas veces cabe el ruido dentro del efecto, y el intervalo se lee en odds al pasarlo por la exponencial"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si esas palabras te suenan a chino, están todas explicadas de cero en el\n",
    "[libro de estadística](https://missyera.com/guias/estadistica-desde-cero/), que es el que\n",
    "va antes que este 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo mismo con sklearn, para que no haya duda"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "X = pd.get_dummies(\n",
    "    datos[['satisfaccion', 'monto', 'sin_compra_previa', 'segmento', 'canal']],\n",
    "    drop_first=True).astype(float)\n",
    "sk = LogisticRegression(max_iter=5000, C=1e9).fit(X, datos['compro'])\n",
    "\n",
    "print(pd.Series(sk.coef_[0], index=X.columns).round(4).to_string())\n",
    "print()\n",
    "print('satisfaccion  statsmodels', round(modelo.params['satisfaccion'], 4),\n",
    "      '| sklearn', round(float(sk.coef_[0][list(X.columns).index('satisfaccion')]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Idénticos hasta la cuarta cifra 🤝\n",
    "\n",
    "Ese `C=1e9` es lo único que hay que saber para que coincidan:\n",
    "scikit-learn regulariza por defecto y statsmodels no, así que hay que apagar la\n",
    "regularización poniendo la `C` altísima.\n",
    "\n",
    "Y ojo, que ese detalle explica una cosa que confunde a mucha gente: si sacas\n",
    "coeficientes de scikit-learn sin tocar la `C`, están encogidos hacia\n",
    "cero a propósito. Sirven para predecir. Para contarlos en una reunión, no."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los odds, que es como se dice en voz alta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un coeficiente de 1,2799 no se puede decir en una reunión. Pasado por la\n",
    "exponencial, sí."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "log(p1−p)=b+∑jwjxj\n",
    "\n",
    "la misma fórmula al revés: cada peso dice cuánto mueve el logaritmo de la ventaja, y por eso un peso de 0,7 se lee como multiplicar la ventaja por dos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tabla = pd.DataFrame({'odds': np.exp(modelo.params), 'p': modelo.pvalues})\n",
    "intervalo = np.exp(modelo.conf_int())\n",
    "tabla['ic_bajo'], tabla['ic_alto'] = intervalo[0], intervalo[1]\n",
    "print(tabla.round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora sí se puede leer en voz alta 🗣️\n",
    "\n",
    "- 🏭 Ser **Mayorista multiplica por 3,60** las probabilidades a\n",
    "favor de que cierre, comparado con Bodega. Y el intervalo va de 2,33 a 5,55, así\n",
    "que aunque el número exacto no lo sepamos, que multiplica al menos por dos sí.\n",
    "\n",
    "- ⭐ Cada punto de **satisfacción multiplica por 1,33**.\n",
    "\n",
    "- 🆕 Ser **cliente nuevo multiplica por 0,41**, o sea que las\n",
    "reduce a menos de la mitad.\n",
    "\n",
    "- 📱 **WhatsApp multiplica por 2,34** contra Marketplace, que es\n",
    "la categoría de referencia.\n",
    "\n",
    "Ese \"comparado con Bodega\" y ese \"contra Marketplace\" no son un detalle: en\n",
    "una variable categórica **todos los coeficientes se leen contra la\n",
    "categoría que falta**, que es la primera por orden alfabético. Si te\n",
    "olvidas de decirlo, la frase queda mal 🏷️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La que no se distingue de nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hay una fila de esa tabla que no se parece a las demás."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cruza = tabla[(tabla['ic_bajo'] < 1) & (tabla['ic_alto'] > 1)]\n",
    "print(cruza.round(4).to_string() if len(cruza) else 'ninguna')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`monto`, con p de 0,1561 y un intervalo que va de 0,9999 a 1,0004,\n",
    "o sea que cruza el 1 😐\n",
    "\n",
    "Cruzar el 1 en odds es lo mismo que cruzar el cero en el coeficiente: con\n",
    "estos datos **no se puede distinguir de no tener efecto**. Y fíjate\n",
    "en cómo lo he dicho, porque la frase importa: no es \"el monto no influye\". Es\n",
    "\"con estas 2.769 filas no lo puedo separar del ruido\".\n",
    "\n",
    "Y ahora el detalle que a mí me parece lo mejor de todo el libro 🤯\n",
    "\n",
    "En el capítulo 21, `monto` era la **columna\n",
    "número uno** del LightGBM, con 0,2342 de SHAP. Aquí no llega a\n",
    "distinguirse de cero.\n",
    "\n",
    "No se contradicen. Dicen cosas distintas sobre la misma columna:\n",
    "\n",
    "- 📈 La regresión pregunta si el monto empuja **en línea recta**:\n",
    "más soles, más probabilidad. Y en línea recta no dice nada.\n",
    "\n",
    "- 🪜 El árbol puede trocear el monto donde quiera y quedarse con \"por debajo\n",
    "de 300 soles pasa una cosa y por encima de 1.500 otra\". Eso sí lo encuentra.\n",
    "\n",
    "La lección práctica: **un valor p alto no significa que la columna sea\n",
    "inútil para predecir**. Significa que su efecto no es una recta. Tirar\n",
    "columnas por su valor p es de los errores que más modelos empeora 🚮"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que de verdad se dice en la reunión"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los odds son correctos y siguen sin ser lo que la gente entiende. Nadie\n",
    "piensa en \"multiplicar las probabilidades a favor\". La gente piensa en puntos\n",
    "porcentuales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "marginal = modelo.get_margeff(at='overall')\n",
    "print(marginal.summary().tables[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto ya es castellano 🎯\n",
    "\n",
    "- ⭐ Cada punto de satisfacción sube la probabilidad de cierre\n",
    "**6,05 puntos**.\n",
    "\n",
    "- 🏭 Ser Mayorista en vez de Bodega la sube **27,00 puntos**.\n",
    "\n",
    "- 🆕 Ser cliente nuevo la baja **18,99 puntos**.\n",
    "\n",
    "El efecto marginal es la pendiente media: cuánto se mueve la probabilidad si\n",
    "esa variable sube una unidad y todo lo demás se queda quieto.\n",
    "\n",
    "Y digo \"media\" a propósito, porque en una logística la pendiente no es la\n",
    "misma en todos lados: donde la probabilidad ya es 0,95 casi no se puede subir\n",
    "más. El número de arriba es el promedio de todas las filas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Más datos afilan lo real y no arreglan lo que no está"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La demostración que enseña qué es de verdad un valor p 🔬"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for n in (500, 1000, 2000, len(datos)):\n",
    "    trozo = datos.sample(n=min(n, len(datos)), random_state=7)\n",
    "    m = smf.logit(FORMULA, data=trozo).fit(disp=0)\n",
    "    print(f'{len(trozo):5d} filas -> monto coef {m.params[\"monto\"]:+.6f} '\n",
    "          f'p {m.pvalues[\"monto\"]:.4f}   satisfaccion p {m.pvalues[\"satisfaccion\"]:.2e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira las dos columnas por separado, que cuentan historias opuestas.\n",
    "\n",
    "**La satisfacción** va de p 5,44e-06 a 4,12e-22. Cada vez más\n",
    "seguro. Eso es lo que hace un efecto que existe: con más datos la barra de error\n",
    "se encoge y el efecto se ve más claro.\n",
    "\n",
    "**El monto** pasea. Su coeficiente empieza en −0,000358, se va a\n",
    "−0,000017, sube a +0,000120 y acaba en +0,000159. **Cambia de\n",
    "signo** por el camino. Y su p va de 0,21 a 0,93 a 0,36 a 0,16, sin\n",
    "acercarse nunca a nada.\n",
    "\n",
    "Un efecto que existe se afila con datos. Uno que no existe se pasea, y ese\n",
    "paseo es exactamente lo que mide el valor p 🎲"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error: cuando el modelo se niega a ajustar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a meterle la trampa del capítulo 12: una\n",
    "columna que *es* la respuesta."
   ]
  },
  {
   "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",
    "    con_soplon = datos.copy()\n",
    "    con_soplon['soplon'] = con_soplon['compro']\n",
    "    smf.logit('compro ~ soplon + satisfaccion', data=con_soplon).fit(disp=0)\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",
    "LinAlgError: Singular matrix\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto es precioso y no lo esperaba la primera vez 🤩\n",
    "\n",
    "Se llama **separación perfecta**: existe una columna que separa\n",
    "las dos clases sin errores, así que el coeficiente que mejor ajusta es infinito.\n",
    "Y como no hay número que sirva, el álgebra se rompe y sale un\n",
    "`LinAlgError`.\n",
    "\n",
    "Compara eso con lo que pasó en el capítulo de fuga: scikit-learn cogió la\n",
    "columna soplona, entrenó tan contento y devolvió 0,9994 de AUC. Nadie avisó de\n",
    "nada.\n",
    "\n",
    "Statsmodels **se niega a ajustar**. Y aunque sea molesto, es la\n",
    "mejor detección de fuga que hay: si tu regresión no converge, mira si alguna\n",
    "columna sabe demasiado antes de tocar nada más 🚨"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el silencio que sí duele"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solo_satisfaccion = smf.logit('compro ~ satisfaccion', data=ventas).fit(disp=0)\n",
    "print('filas del archivo :', len(ventas))\n",
    "print('filas que usó     :', int(solo_satisfaccion.nobs))\n",
    "print('descartadas       :', len(ventas) - int(solo_satisfaccion.nobs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "231 filas fuera y ni un aviso 😨\n",
    "\n",
    "Statsmodels borra las filas con nulos y sigue. Le pasé el archivo entero y\n",
    "ajustó con 2.769, o sea que el 7,7% de las ventas no participó en las\n",
    "conclusiones y en ningún sitio del `summary()` lo dice a gritos.\n",
    "\n",
    "Por eso en este capítulo el `dropna` va escrito arriba, a mano.\n",
    "Que la decisión de qué filas entran la tomes tú y no la librería, y que quede\n",
    "por escrito cuántas eran 📋\n",
    "\n",
    "Y acuérdate del capítulo 9: esas filas no son un\n",
    "descuido, son la columna `sin_satisfaccion`. Si el hueco significa\n",
    "algo, descartar las filas te lo tira a la basura."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. ¿Se pisan las columnas entre ellas?\n",
    "\n",
    "Si dos columnas dicen casi lo mismo, sus coeficientes se\n",
    "vuelven locos y sus intervalos se ensanchan. Eso se mide con el VIF."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Meter las tres y ver si algo se mueve\n",
    "\n",
    "El VIF dice que no hay problema. Compruébalo de la forma\n",
    "que de verdad importa: añadiendo las columnas y mirando si los demás\n",
    "coeficientes se mueven."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La categoría de referencia, cambiada a propósito\n",
    "\n",
    "Todos los coeficientes se leen contra Bodega porque es la\n",
    "primera del abecedario. Ponla contra Mayorista y mira qué cambia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Un modelo dentro de otro\n",
    "\n",
    "¿Aporta algo el canal, sabiendo ya el segmento? Se compara\n",
    "el modelo con y sin él, con una prueba de razón de verosimilitud."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Los residuos, para encontrar filas raras\n",
    "\n",
    "Un residuo grande es una fila que el modelo no explica.\n",
    "Míralas, que suelen contar algo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Predecir con statsmodels, y por qué no lo harías\n",
    "\n",
    "Se puede, claro. Mide su AUC contra la logística de\n",
    "scikit-learn del resto del libro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La misma pregunta sin regresión, para comprobar\n",
    "\n",
    "Antes de creerte un coeficiente, mira el número crudo. Si\n",
    "no se parecen, algo pasa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Un coeficiente sale con p de 0,16 y su intervalo de odds va de 0,9999 a 1,0004. ¿Qué escribes en el informe?\n",
    "\n",
    "a) Que con estos datos no se distingue de no tener efecto\n",
    "\n",
    "b) Que esa variable no influye en la compra\n",
    "\n",
    "c) Que influye poco, porque el odds es casi 1\n",
    "\n",
    "d) Que hace falta quitarla del modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y el resumen que sale demasiado bonito"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Depuración de variables de manual: se quita la menos significativa, se vuelve a ajustar, y así hasta que todas bajan de 0,05. Al final el resumen sale precioso.\n",
    "\n",
    "```\n",
    "while True:\n",
    "    m = sm.Logit(y, X).fit(disp=0)\n",
    "    peor = m.pvalues.idxmax()\n",
    "    if m.pvalues[peor] < 0.05:\n",
    "        break\n",
    "    X = X.drop(columns=[peor])\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🧮 statsmodels no es otro modelo: es la misma regresión logística con lo\n",
    "que le falta a scikit-learn. Error estándar, z, valor p e intervalo.\n",
    "\n",
    "- 🔧 Para que los coeficientes coincidan hay que apagar la regularización de\n",
    "scikit-learn con `C=1e9`. Sin eso están encogidos a propósito y no\n",
    "se cuentan en una reunión.\n",
    "\n",
    "- 🗣️ Los odds se dicen en voz alta: Mayorista multiplica por 3,60 (entre 2,33\n",
    "y 5,55), cada punto de satisfacción por 1,33 y ser cliente nuevo por 0,41.\n",
    "\n",
    "- 🏷️ Todo coeficiente categórico se lee contra la categoría que falta, que es\n",
    "la primera del abecedario. Sin decirlo, la frase queda mal.\n",
    "\n",
    "- 🎯 El efecto marginal es lo que de verdad entiende la gente: cada punto de\n",
    "satisfacción sube 6,05 puntos de probabilidad y ser cliente nuevo baja 18,99.\n",
    "\n",
    "- 😐 `monto` tiene p de 0,1561 y su intervalo cruza el 1. Eso es\n",
    "\"con estos datos no lo distingo del ruido\", no es \"no influye\".\n",
    "\n",
    "- 🤯 Y esa misma columna era la número uno del boosting en el capítulo\n",
    "21. No se contradicen: la regresión pregunta si empuja en línea\n",
    "recta y el árbol puede trocearla. Tirar columnas por su valor p empeora\n",
    "modelos.\n",
    "\n",
    "- 🔬 Con más filas, la satisfacción pasa de p 5,44e-06 a 4,12e-22 y el monto\n",
    "se pasea hasta cambiar de signo. Un efecto real se afila con datos; uno que no\n",
    "está, no.\n",
    "\n",
    "- 🚨 Una columna que es la respuesta hace que la regresión ni ajuste:\n",
    "separación perfecta y `LinAlgError`. Es la mejor detección de fuga\n",
    "que hay, y scikit-learn en el mismo caso devolvía 0,9994 de AUC tan contento.\n",
    "\n",
    "- 🕳️ Y el silencio que sí duele: statsmodels borra las filas con nulos sin\n",
    "avisar. Le pasé 3.000 y ajustó con 2.769.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un coeficiente sin intervalo no es un hallazgo. Es una cifra con suerte."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Intervalos, error estándar y qué contesta de verdad un valor p: todo eso viene del [libro de estadística desde cero](https://missyera.com/guias/estadistica-desde-cero/) 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué viene ahora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya sabemos predecir, explicar el modelo entero, explicar una fila y decir si\n",
    "un efecto existe. Falta la pregunta más incómoda de todas 😬\n",
    "\n",
    "El capítulo 24 abre el modelo por grupos: ¿acierta\n",
    "igual con las bodegas que con los mayoristas? Y después, cuánta confianza merece\n",
    "cada predicción por separado.\n",
    "\n",
    "Lo que verás allí:\n",
    "\n",
    "- ⚖️ Las métricas del capítulo 14 calculadas dentro de cada\n",
    "segmento, que es donde salen las diferencias que el promedio esconde.\n",
    "\n",
    "- 🎲 Cuánto de esa diferencia es real y cuánto es la lotería de la partición,\n",
    "medido con diez sorteos.\n",
    "\n",
    "- 📏 Calibración: si el modelo dice 70%, ¿de cada diez cierran siete?\n",
    "\n",
    "- 🛡️ Y la predicción conforme, que en vez de un número da un conjunto y\n",
    "promete cubrir la respuesta el 90% de las veces."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 22 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/regresion-con-statsmodels/\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
}
