{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Abrir la caja negra\n",
    "\n",
    "Coeficientes, odds e importancia por permutación, y la columna que valía doce puntos y no aporta nada.\n",
    "\n",
    "Cuaderno de práctica del capítulo 20 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/explicar-el-modelo/\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: \"Y2F0X19zZWdtZW50b19NYXlvcmlzdGEgICAgICAgICAgKzAuNDcyNQpjYXRfX3NlZ21lbnRvX0hvcmVjYSAgICAgICAgICAgICArMC4zMjQ4CmNhdF9fc2VnbWVudG9fTWluaW1hcmtldCAgICAgICAgICswLjA2NjYKY2F0X19zZWdtZW50b19Cb2RlZ2EgICAgICAgICAgICAgLTAuNjgzMg==\",\n",
    "    2: \"ZGVzdmlhY2nDs24gdMOtcGljYSBkZSBzYXRpc2ZhY2Npw7NuOiAxLjM2NDEKcG9yIGRlc3ZpYWNpw7NuOiAwLjQxNTIKcG9yIHB1bnRvICAgICA6IDAuMzA0MwpvZGRzIHBvciBwdW50bzogMS4zNTU3\",\n",
    "    3: \"c2VnbWVudG8gICAgICAgICAgICAgKzAuMDM2NQpzYXRpc2ZhY2Npb24gICAgICAgICArMC4wMjcwCm1vbnRvICAgICAgICAgICAgICAgICswLjAyNTcKc2luX2NvbXByYV9wcmV2aWEgICAgKzAuMDIxMwpjYW5hbCAgICAgICAgICAgICAgICArMC4wMjA2\",\n",
    "    4: \"Y29uIDQgY29sdW1uYXMgOiAwLjcxOTMKY29uIDEzIGNvbHVtbmFzOiAwLjcyMTQ=\",\n",
    "    5: \"ZWwgbW9kZWxvIGRpam86IDAuOTAzMgpsYSB2ZXJkYWQgZXJhIDogMAoKc2VnbWVudG8gICAgICAgIE1heW9yaXN0YQpjYW5hbCAgICAgICAgICAgICAgVGllbmRhCnNhdGlzZmFjY2lvbiAgICAgICAgICA1LjAKbW9udG8gICAgICAgICAgICAgMTc5Ni4wNgpOYW1lOiAxMzUsIGR0eXBlOiBvYmplY3Q=\",\n",
    "    6: \"VmFsdWVFcnJvcjogTGVuZ3RoIG9mIHZhbHVlcyAoMjgpIGRvZXMgbm90IG1hdGNoIGxlbmd0aCBvZiBpbmRleCAoMTMp\",\n",
    "    7: \"ICAgICAgICAgIGNvbHVtbmEgIGltcG9ydGFuY2lhICBkZXN2aWFjaW9uCiAgICAgICAgIHNlZ21lbnRvICAgICAgIDAuMDkwOSAgICAgIDAuMDExNwogICAgIHNhdGlzZmFjY2lvbiAgICAgICAwLjA0NjQgICAgICAwLjAwNzYKc2luX2NvbXByYV9wcmV2aWEgICAgICAgMC4wMzYxICAgICAgMC4wMDgyCiAgICAgICAgICAgIGNhbmFsICAgICAgIDAuMDMzNCAgICAgIDAuMDA2NAogICAgICAgICAgICBtb250byAgICAgICAwLjAwNjcgICAgICAwLjAwMTkKICBwcmVjaW9fdW5pdGFyaW8gICAgICAgMC4wMDYwICAgICAgMC4wMDMx\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo que nadie entiende no se usa. Y esto no es filosofía: es lo que\n",
    "pasa cuando el equipo comercial recibe una lista de 200 nombres y nadie sabe\n",
    "decir por qué están esos y no otros 🤷‍♀️\n",
    "\n",
    "Una pregunta para ti: **¿te ha tocado defender un resultado que no sabías explicar del todo?** Es de las sensaciones más incómodas de este trabajo, y se arregla antes de la reunión, no durante 😬\n",
    "\n",
    "En este capítulo abrimos la caja. Y de paso vamos a encontrar una columna que\n",
    "medimos en doce puntos en el capítulo 4 y que al modelo **no le aporta\n",
    "nada**, lo cual tiene una explicación bonita."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El punto de partida"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\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",
    "def prepara(v):\n",
    "    v = v.sort_values(['cliente_id', 'fecha']).copy()\n",
    "    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)\n",
    "    v['sin_descuento'] = v['descuento'].isna().astype(int)\n",
    "    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)\n",
    "    v['precio_unitario'] = v['monto'] / v['unidades']\n",
    "    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',\n",
    "             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']\n",
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "pre = ColumnTransformer([\n",
    "    ('num', Pipeline([('r', SimpleImputer(strategy='median')),\n",
    "                      ('e', StandardScaler())]), NUMERICAS),\n",
    "    ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),\n",
    "                      ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),\n",
    "])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos['compro']\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "\n",
    "modelo = Pipeline([('pre', pre),\n",
    "                   ('mod', LogisticRegression(max_iter=1000, random_state=42))]).fit(X_tr, y_tr)\n",
    "print('AUC:', round(modelo.score(X_te, y_te), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los coeficientes, que es lo que la logística regala"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una regresión logística le pone un número a cada columna. Ese número\n",
    "**es** la explicación, no una aproximación de la explicación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nombres = modelo.named_steps['pre'].get_feature_names_out()\n",
    "pesos = modelo.named_steps['mod'].coef_[0]\n",
    "\n",
    "for i in np.argsort(np.abs(pesos))[::-1][:8]:\n",
    "    print(f'{nombres[i]:32} {pesos[i]:+.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se leen así: **positivo empuja hacia el sí, negativo hacia el no, y el\n",
    "tamaño dice cuánto**.\n",
    "\n",
    "Ser Bodega es lo que más resta (-0,68) y ser Mayorista lo que más suma\n",
    "(+0,47). Marketplace resta, WhatsApp suma. Todo eso ya lo habíamos visto a mano\n",
    "en el capítulo 1, y ahora sale solo y con todas las columnas a la vez.\n",
    "\n",
    "Eso último es la gracia: en el capítulo 1 miramos el segmento\n",
    "*ignorando* el resto. El coeficiente dice cuánto aporta el segmento\n",
    "**una vez descontado todo lo demás** 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Convertirlos a algo que se pueda decir en voz alta"
   ]
  },
  {
   "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": "markdown",
   "metadata": {},
   "source": [
    "Los coeficientes están en log-odds, que no se explican en una reunión. Se\n",
    "arregla con una exponencial:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in np.argsort(np.abs(pesos))[::-1][:6]:\n",
    "    print(f'{nombres[i]:32} multiplica las probabilidades por {np.exp(pesos[i]):.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora sí: **ser Mayorista multiplica por 1,6 las probabilidades a favor\n",
    "de cerrar; ser Bodega las multiplica por 0,5, o sea las corta a la mitad**.\n",
    "\n",
    "Ojo con una trampa de vocabulario que se comete siempre: eso son\n",
    "*odds*, no probabilidades. \"Multiplica por 1,6 las probabilidades\" es\n",
    "incorrecto en sentido estricto; lo correcto es \"la razón de probabilidades\". En\n",
    "una reunión yo digo lo primero y lo aclaro si alguien pregunta, y en un informe\n",
    "escrito lo pongo bien 📝\n",
    "\n",
    "Y un aviso más importante: los coeficientes de las numéricas están\n",
    "**escalados** por el `StandardScaler`, así que +0,4152 en\n",
    "satisfacción significa \"por cada desviación típica de satisfacción\", no \"por cada\n",
    "punto\". Se puede desescalar, y casi siempre no hace falta: para comparar\n",
    "importancia entre columnas, escalado es justo lo que quieres."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La forma que funciona con cualquier modelo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los coeficientes solo existen en los modelos lineales. Para un bosque o un\n",
    "boosting hace falta otra cosa, y la buena se llama\n",
    "**importancia por permutación**.\n",
    "\n",
    "La idea es preciosa de tan simple: *revuelve una columna al azar y mira\n",
    "cuánto empeora el modelo*. Si empeora mucho, esa columna importaba. Si no\n",
    "cambia nada, no servía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.inspection import permutation_importance\n",
    "\n",
    "r = permutation_importance(modelo, X_te, y_te, n_repeats=10,\n",
    "                           random_state=42, scoring='roc_auc', n_jobs=-1)\n",
    "\n",
    "for i in r.importances_mean.argsort()[::-1]:\n",
    "    print(f'{X_te.columns[i]:20} {r.importances_mean[i]:+.4f} ± {r.importances_std[i]:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el ranking honesto, medido en **puntos de AUC perdidos**:\n",
    "\n",
    "- 🥇 **segmento**: 0,0909. Si lo revuelves, el modelo pierde nueve\n",
    "puntos. Es la columna del libro.\n",
    "\n",
    "- 🥈 **satisfacción**: 0,0464.\n",
    "\n",
    "- 🥉 **sin_compra_previa**: 0,0361, la bandera que construimos en\n",
    "el capítulo 9 con una línea.\n",
    "\n",
    "- 4️⃣ **canal**: 0,0334.\n",
    "\n",
    "Y de ahí para abajo, todo por debajo de 0,007. **Cuatro columnas hacen\n",
    "el modelo** y las otras nueve están de adorno.\n",
    "\n",
    "Fíjate en tres ventajas enormes de esta técnica: funciona con cualquier\n",
    "modelo, se mide en la unidad que te importa (aquí AUC) y viene con\n",
    "**desviación**, así que sabes cuándo dos columnas están\n",
    "empatadas 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La columna que valía doce puntos y no aporta nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira `sin_descuento` en esa lista: -0,0002. Cero.\n",
    "\n",
    "Y en el capítulo 4 medimos que sin descuento se cierra el 48,1% y con\n",
    "descuento el 60,2%. **Doce puntos de diferencia.** ¿Cómo puede no\n",
    "aportar nada?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('la bandera sola:')\n",
    "print(datos.groupby('sin_descuento')['compro'].mean().round(4))\n",
    "print()\n",
    "print('el descuento imputado, por bandera:')\n",
    "imputado = datos['descuento'].fillna(datos['descuento'].median())\n",
    "print(pd.DataFrame({'bandera': datos['sin_descuento'],\n",
    "                    'descuento': imputado}).groupby('bandera')['descuento'].describe()[['min', 'max']].round(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la explicación: cuando la bandera vale 1, el imputador rellenó el\n",
    "descuento con la mediana, y ese valor exacto (0,127) prácticamente solo aparece\n",
    "en esas filas.\n",
    "\n",
    "O sea que **la columna `descuento` ya lleva dentro la\n",
    "información de la bandera**. Son redundantes, y al revolver una la otra\n",
    "sigue diciendo lo mismo.\n",
    "\n",
    "Esa es la trampa grande de la importancia por permutación y hay que saberla:\n",
    "**con dos columnas que dicen lo mismo, las dos salen sin importancia**,\n",
    "porque el modelo se apoya en la que quede. No significa que ninguna importe:\n",
    "significa que sobra una 🔍\n",
    "\n",
    "La forma de comprobarlo es quitarlas de verdad, que es el ejercicio 4."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Explicar UNA predicción"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo de arriba explica el modelo. Pero en la reunión te van a preguntar por una\n",
    "fila: *¿por qué a este cliente le pusiste 0,85?*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fila = X_te.iloc[[3]]\n",
    "prob = modelo.predict_proba(fila)[0, 1]\n",
    "transformada = modelo.named_steps['pre'].transform(fila)[0]\n",
    "aporte = transformada * pesos\n",
    "\n",
    "print('probabilidad:', round(prob, 4))\n",
    "print()\n",
    "for i in np.argsort(np.abs(aporte))[::-1][:6]:\n",
    "    print(f'{nombres[i]:32} {aporte[i]:+.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es una explicación completa de esa fila: cada número dice cuánto empujó\n",
    "cada característica de *este* cliente.\n",
    "\n",
    "Y sumado da exactamente el resultado, se puede comprobar:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "total = aporte.sum() + modelo.named_steps['mod'].intercept_[0]\n",
    "print('suma de aportes + intercepto:', round(total, 4))\n",
    "print('probabilidad reconstruida   :', round(1 / (1 + np.exp(-total)), 4))\n",
    "print('la que dio el modelo        :', round(prob, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Idénticas 🎯 Eso es lo que hace que un modelo lineal sea auditable de verdad:\n",
    "no hay nada escondido, la predicción **es** la suma."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El coeficiente que cambia de signo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todo lo de arriba tiene una letra pequeña que hay que leer antes de contarle\n",
    "a nadie qué columna importa 🔬\n",
    "\n",
    "En estos datos hay dos columnas que dicen casi lo mismo:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('correlación monto / monto_final_facturado:',\n",
    "      round(datos['monto'].corr(datos['monto_final_facturado']), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0,74. No son la misma columna, pero se parecen mucho. Ahora mira qué le pasa\n",
    "al coeficiente del monto según esté sola o acompañada:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "\n",
    "def coef_de(cols):\n",
    "    p = Pipeline([\n",
    "        ('p', ColumnTransformer([('n', Pipeline([\n",
    "            ('i', SimpleImputer(strategy='median')),\n",
    "            ('s', StandardScaler()),\n",
    "        ]), cols)])),\n",
    "        ('m', LogisticRegression(max_iter=1000, random_state=42)),\n",
    "    ])\n",
    "    p.fit(datos[cols], datos['compro'])\n",
    "    return dict(zip(cols, p.named_steps['m'].coef_[0].round(3)))\n",
    "\n",
    "\n",
    "print('sola      :', coef_de(['monto', 'unidades', 'satisfaccion']))\n",
    "print('acompañada:', coef_de(['monto', 'monto_final_facturado',\n",
    "                              'unidades', 'satisfaccion']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El coeficiente del monto pasa de **+0,461 a −2,029** 😱\n",
    "\n",
    "Los mismos datos, la misma columna, el mismo modelo. Lo único que cambió es\n",
    "que al lado hay otra columna parecida. Y no cambió de tamaño: **cambió de\n",
    "signo**.\n",
    "\n",
    "Con el primer modelo dirías \"cuanto más grande la venta, más probable que\n",
    "compre\". Con el segundo dirías exactamente lo contrario. Las dos frases salen de\n",
    "mirar un coeficiente, y las dos serían igual de defendibles delante de alguien\n",
    "que no sepa esto.\n",
    "\n",
    "### Por qué pasa\n",
    "\n",
    "Un coeficiente no significa \"el efecto de esta columna\". Significa\n",
    "**\"el efecto de esta columna manteniendo las demás quietas\"**, y esa\n",
    "diferencia lo es todo 🔑\n",
    "\n",
    "Cuando dos columnas se mueven juntas, \"mantener la otra quieta\" es una\n",
    "situación que casi no existe en los datos. El modelo tiene que inventarse cómo\n",
    "repartir el crédito entre las dos, y le vale cualquier reparto que dé el mismo\n",
    "resultado: `+15,3` a una y `−2,0` a la otra suma lo mismo\n",
    "que un reparto más razonable.\n",
    "\n",
    "Se llama multicolinealidad, y aquí tienes su síntoma en pantalla: coeficientes\n",
    "enormes, de signos opuestos, en columnas que se parecen.\n",
    "\n",
    "### Qué hacer con esto\n",
    "\n",
    "- 🔎 **Mira las correlaciones antes de leer coeficientes.** Si dos\n",
    "columnas pasan de 0,7, no leas sus coeficientes por separado.\n",
    "\n",
    "- ✂️ **Quita una de las dos.** Si dicen casi lo mismo, la segunda\n",
    "no está aportando información nueva, solo confusión.\n",
    "\n",
    "- 📊 **Usa permutación en vez de coeficientes.** Lo de arriba mide\n",
    "cuánto empeora el modelo al estropear una columna, y eso no se reparte de forma\n",
    "arbitraria.\n",
    "\n",
    "Y un aviso sobre esta columna en concreto, para que no la copies:\n",
    "`monto_final_facturado` es además **una fuga**. Es lo que\n",
    "se facturó, o sea que existe después de que la venta pasara, y un modelo que la\n",
    "usa está mirando el futuro. Lo desarrollo en el capítulo\n",
    "12.\n",
    "\n",
    "La he usado aquí porque es el ejemplo más limpio que tengo de dos columnas\n",
    "correlacionadas, y porque las dos cosas van juntas más veces de lo que parece:\n",
    "una columna que se parece demasiado a la respuesta suele ser sospechosa por los\n",
    "dos motivos a la vez 🕵️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sobre SHAP, que es lo que te van a preguntar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "SHAP es la técnica estándar para esto con modelos complicados. Reparte la\n",
    "predicción entre las columnas de una forma que viene de teoría de juegos y tiene\n",
    "propiedades matemáticas bonitas.\n",
    "\n",
    "Vive en una librería aparte (`pip install shap`) y no la uso en\n",
    "este libro por dos razones honestas:\n",
    "\n",
    "- 📦 No está instalada en el entorno donde se ejecutan estos capítulos, y este\n",
    "libro no publica código que no corre.\n",
    "\n",
    "- ➕ Para un modelo lineal, **los aportes de SHAP son exactamente lo que\n",
    "acabamos de calcular a mano**. Con tres líneas y sin dependencias.\n",
    "\n",
    "Cuándo sí vale la pena instalarlo: cuando el modelo que ganó es un bosque o un\n",
    "boosting y necesitas explicar filas concretas. Para eso no hay atajo y SHAP es lo\n",
    "mejor que hay 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Los coeficientes del segmento, ordenados\n",
    "\n",
    "Saca solo los cuatro segmentos y ordénalos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Desescalar un coeficiente\n",
    "\n",
    "Traduce el peso de la satisfacción a \"por cada punto de\n",
    "satisfacción\"."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La importancia según el bosque, comparada\n",
    "\n",
    "Pon al lado la importancia del bosque y la de permutación, y\n",
    "mira si coinciden."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Quitar de verdad las columnas que no aportan\n",
    "\n",
    "Entrena solo con las cuatro columnas que salieron arriba y\n",
    "compara."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La fila que el modelo se equivoca\n",
    "\n",
    "Busca un caso donde el modelo estaba muy seguro y falló, y\n",
    "explícalo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de leer los nombres al revés\n",
    "\n",
    "Intenta emparejar coeficientes con las columnas originales\n",
    "en vez de con las transformadas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La tabla que entregas con el modelo\n",
    "\n",
    "Junta coeficiente, odds e importancia en una sola tabla,\n",
    "lista para el informe."
   ]
  },
  {
   "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",
    "Una columna tenía doce puntos de diferencia entre grupos y el modelo no la usa. ¿Qué pasó?\n",
    "\n",
    "a) Que otra columna ya traía esa información\n",
    "\n",
    "b) Que el modelo está mal entrenado\n",
    "\n",
    "c) Que la diferencia de doce puntos era mentira\n",
    "\n",
    "d) Que hay que forzar al modelo a usarla"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El ranking que ordena otra cosa"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Ya tienes el modelo entrenado y te piden qué variable pesa más. Ordenas los coeficientes por tamaño y sale la respuesta.\n",
    "\n",
    "```\n",
    "coef = pd.Series(modelo.coef_[0],\n",
    "                 index=X.columns)\n",
    "print(coef.abs().sort_values(\n",
    "    ascending=False).head(3))\n",
    "\n",
    "# satisfaccion  0.2745\n",
    "# unidades      0.0049\n",
    "# monto         0.0006\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": [
    "- 📊 En una logística, los coeficientes *son* la explicación. Positivo\n",
    "empuja al sí, negativo al no.\n",
    "\n",
    "- 🗣️ Se dicen en voz alta con `exp()`: ser Mayorista multiplica las\n",
    "odds por 1,6 y ser Bodega las corta a la mitad.\n",
    "\n",
    "- 🔀 La importancia por permutación funciona con cualquier modelo, se mide en\n",
    "la unidad que te importa y trae desviación.\n",
    "\n",
    "- 🥇 Cuatro columnas hacen el modelo: segmento, satisfacción, sin_compra_previa\n",
    "y canal. Las otras nueve están de adorno.\n",
    "\n",
    "- 🔍 Dos columnas que dicen lo mismo salen las dos sin importancia. No sobra\n",
    "información: sobra una columna.\n",
    "\n",
    "- 🧾 Una predicción concreta se explica sumando aportes, y la suma reconstruye\n",
    "la probabilidad exacta.\n",
    "\n",
    "- 📦 Para un modelo lineal, SHAP da lo mismo que esas tres líneas. Instálalo\n",
    "cuando el modelo sea un bosque o un boosting.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un modelo que nadie sabe explicar no se usa. Y uno que no se usa no existe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Explicar un modelo a quien no es técnico es media profesión. De eso va el [artículo de cómo ser analista de datos](https://missyera.com/blog/ser-analista-de-datos/) 🗣️\n",
    "\n",
    "En el capítulo 24 vamos a lo que el modelo aprendió sin que nadie se lo\n",
    "pidiera: sesgo, grupos que salen peor parados y cuánta confianza merece cada\n",
    "predicción.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 20 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/explicar-el-modelo/\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
}
