{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Las columnas entre ellas\n",
    "\n",
    "La matriz que delata una fuga sin entrenar nada, y la correlación de 0,2351 que se evapora al partir por segmento.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 8 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/eda-multivariado/\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": [
    "El capítulo anterior dejó dos preguntas colgando, y las dos se contestan\n",
    "igual: **mirando las columnas entre ellas** 🕸️\n",
    "\n",
    "`sin_descuento` tenía doce puntos y no le va a aportar nada al\n",
    "modelo. `monto` era la mejor de las numéricas y en el capítulo\n",
    "22 se queda en p de 0,1561.\n",
    "\n",
    "Las dos cosas pasan por lo mismo: una columna no vive sola. Y este capítulo\n",
    "termina con un resultado que a mí me sigue pareciendo el más bonito del libro\n",
    "🤯"
   ]
  },
  {
   "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 scipy import stats\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['precio_unitario'] = ventas['monto'] / ventas['unidades']\n",
    "\n",
    "NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "             'precio_unitario', 'monto_final_facturado', 'compro']\n",
    "\n",
    "print('columnas que entran en la maraña:', len(NUMERICAS))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El error de la primera matriz"
   ]
  },
  {
   "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",
    "    ventas[['segmento', 'monto']].corr()\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: could not convert string to float: 'Horeca'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una correlación necesita números y `segmento` es texto. Para\n",
    "cruzar dos categóricas entre ellas hace falta otra cosa, y la vemos más abajo\n",
    "🔤"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La matriz, con Spearman y no con Pearson"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ρ=1−6∑di2n(n2−1)\n",
    "\n",
    "la correlación calculada sobre los puestos y no sobre los valores, así que mide si una sube cuando la otra sube sin pedirle que sea una recta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas[NUMERICAS].corr(method='spearman').round(3).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Antes de leerla, la pregunta que casi nadie hace: **¿por qué\n",
    "Spearman?**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "spearman = ventas[NUMERICAS].corr(method='spearman')\n",
    "pearson = ventas[NUMERICAS].corr(method='pearson')\n",
    "\n",
    "pares = []\n",
    "for i, a in enumerate(NUMERICAS):\n",
    "    for j, b in enumerate(NUMERICAS):\n",
    "        if i < j:\n",
    "            pares.append((abs(spearman.iloc[i, j] - pearson.iloc[i, j]),\n",
    "                          a, b, spearman.iloc[i, j], pearson.iloc[i, j]))\n",
    "\n",
    "for diferencia, a, b, s, p in sorted(pares, reverse=True)[:4]:\n",
    "    print(f'{a:22s} {b:22s} spearman {s:+.3f}  pearson {p:+.3f}  '\n",
    "          f'se llevan {diferencia:.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el motivo 👀\n",
    "\n",
    "`monto` y `precio_unitario` dan 0,814 con Spearman y\n",
    "0,446 con Pearson. Casi el doble.\n",
    "\n",
    "Pearson mide si la relación es una **recta**, y esas dos\n",
    "columnas suben juntas pero no en línea recta: en el capítulo\n",
    "6 vimos que `precio_unitario` tiene asimetría\n",
    "6,734, así que cuatro ventas enormes le doblan la recta a Pearson y le esconden\n",
    "la relación.\n",
    "\n",
    "Spearman trabaja con los puestos, no con los valores. La venta más cara es la\n",
    "número uno, valga 3.000 o 300.000. Por eso no se deja arrastrar 📊"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los pares que hay que mirar sí o sí"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = ventas[NUMERICAS].corr(method='spearman').abs()\n",
    "for i, a in enumerate(NUMERICAS):\n",
    "    for j, b in enumerate(NUMERICAS):\n",
    "        if i < j and c.iloc[i, j] > 0.5:\n",
    "            print(f'{a:22s} {b:22s} {c.iloc[i, j]:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres pares, y cada uno cuenta una cosa distinta:\n",
    "\n",
    "- 🪞 `monto` con `precio_unitario`, 0,8142. Normal: la\n",
    "segunda sale de dividir la primera. Son casi la misma columna y por eso en el\n",
    "capítulo 22 se repartían el efecto.\n",
    "\n",
    "- 💰 `monto` con `monto_final_facturado`, 0,5397.\n",
    "También esperable, son dos formas de contar el mismo dinero.\n",
    "\n",
    "- 🚨 `monto_final_facturado` con `compro`,\n",
    "**0,8897**. Y esa no es normal en absoluto.\n",
    "\n",
    "Una columna que correlaciona 0,89 con lo que quieres predecir **no es\n",
    "una buena columna: es una alarma**. Es la fuga del capítulo\n",
    "12, y aquí sale sin entrenar ningún modelo, mirando una\n",
    "matriz 🎯\n",
    "\n",
    "La regla que uso: por encima de 0,8 contra el objetivo, primero desconfío y\n",
    "después celebro. Casi siempre resulta que la columna se rellena después de que\n",
    "pase lo que quiero predecir."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y las categóricas entre ellas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para dos categóricas no hay correlación, hay V de Cramér, que es la misma del\n",
    "capítulo 7 usada entre columnas en vez de contra el\n",
    "objetivo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']\n",
    "\n",
    "def cramer(tabla):\n",
    "    chi2 = stats.chi2_contingency(tabla)[0]\n",
    "    n = tabla.values.sum()\n",
    "    return np.sqrt(chi2 / (n * (min(tabla.shape) - 1)))\n",
    "\n",
    "for i, a in enumerate(CATEGORICAS):\n",
    "    for j, b in enumerate(CATEGORICAS):\n",
    "        if i < j:\n",
    "            print(f'{a:12s} {b:12s} V {cramer(pd.crosstab(ventas[a], ventas[b])):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las seis por debajo de 0,05, o sea prácticamente independientes 😌\n",
    "\n",
    "Eso significa que saber el segmento no te dice nada del canal, y que el\n",
    "one-hot de las cuatro no va a crear columnas redundantes. Aburrido otra vez, y\n",
    "otra vez es buena noticia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora sí: la relación que se evapora"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta es la que cierra el hilo de tres capítulos 🎬"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "global_ = stats.spearmanr(ventas['monto'], ventas['compro'])\n",
    "print(f'global      rho {global_.statistic:+.4f}  p {global_.pvalue:.2e}  '\n",
    "      f'n {len(ventas)}')\n",
    "\n",
    "for segmento in sorted(ventas['segmento'].unique()):\n",
    "    trozo = ventas[ventas['segmento'] == segmento]\n",
    "    r = stats.spearmanr(trozo['monto'], trozo['compro'])\n",
    "    print(f'{segmento:12s} rho {r.statistic:+.4f}  p {r.pvalue:.4f}  n {len(trozo)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Míralo despacio, porque es de esas cosas que hay que ver dos veces 🤯\n",
    "\n",
    "En las 3.000 ventas juntas, el monto correlaciona +0,2351 con la compra, con\n",
    "una p de 6,01e-39. Un número que en cualquier informe se escribe como hallazgo.\n",
    "\n",
    "Y **dentro de cada segmento no queda nada**: +0,0324, −0,0128,\n",
    "+0,0061, +0,0262. Las cuatro con p por encima de 0,39, y con setecientas filas\n",
    "cada una, que son de sobra para detectar algo de ese tamaño.\n",
    "\n",
    "La explicación está en dos tablas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas.groupby('segmento')['monto'].agg(['mean', 'median', 'count'])\n",
    "      .round(2).to_string())\n",
    "print()\n",
    "print(ventas.groupby('segmento')['compro'].mean().round(4).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mayorista compra por 1.856 soles de media y cierra el 72,40%. La bodega\n",
    "compra por 178 y cierra el 37,48%.\n",
    "\n",
    "O sea que **el monto no predice la compra: predice el segmento**,\n",
    "y el segmento sí predice la compra. Cuando miras las 3.000 juntas, el monto se\n",
    "lleva un mérito que no es suyo 🎭\n",
    "\n",
    "Esto tiene nombre, se llama *confusión* o variable confusora, y es la\n",
    "razón por la que el capítulo 22 daba p de 0,1561\n",
    "para el monto: allí el segmento estaba dentro del modelo, así que ya no le\n",
    "dejaba llevarse nada prestado.\n",
    "\n",
    "Y la razón por la que un análisis bivariado, él solo, **puede llevarte\n",
    "a una conclusión completamente equivocada**. \"Sube el ticket medio y\n",
    "cerrarás más\" es exactamente lo que se deduce del +0,2351, y aquí está la prueba\n",
    "de que no 🚫"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reducir trece columnas a unas pocas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuando hay muchas columnas que dicen lo mismo, se pueden resumir. El PCA\n",
    "busca las direcciones donde los datos más varían y las pone primero."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.decomposition import PCA\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "SOLO_NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "                  'precio_unitario']\n",
    "M = ventas[SOLO_NUMERICAS].fillna(ventas[SOLO_NUMERICAS].median())\n",
    "Z = StandardScaler().fit_transform(M)\n",
    "\n",
    "pca = PCA().fit(Z)\n",
    "acumulado = np.cumsum(pca.explained_variance_ratio_)\n",
    "for i, (parte, total) in enumerate(zip(pca.explained_variance_ratio_, acumulado), 1):\n",
    "    print(f'componente {i}: explica {100 * parte:5.2f}%   acumulado {100 * total:6.2f}%')\n",
    "print()\n",
    "print('componentes para llegar al 90%:', int(np.argmax(acumulado >= 0.9) + 1),\n",
    "      'de', len(SOLO_NUMERICAS))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí el PCA **no sirve para nada**, que también es un\n",
    "resultado 😅\n",
    "\n",
    "Hacen falta cuatro componentes de cinco para llegar al 90%. Cambiar cinco\n",
    "columnas que se entienden por cuatro que no se entienden es un mal negocio.\n",
    "\n",
    "Tiene todo el sentido: la matriz de correlaciones ya decía que estas columnas\n",
    "son casi independientes. El PCA solo comprime lo que está repetido, y aquí no\n",
    "hay casi nada repetido.\n",
    "\n",
    "El escalado antes del PCA no es opcional, por cierto. Sin él,\n",
    "`monto` mandaría en la primera componente solo por ir en miles\n",
    "mientras `satisfaccion` va de 1 a 5 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Qué mira cada componente"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "carga = pd.DataFrame(pca.components_[:2].T, index=SOLO_NUMERICAS,\n",
    "                     columns=['CP1', 'CP2'])\n",
    "print(carga.round(3).to_string())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La primera componente es `precio_unitario` (0,712) contra\n",
    "`unidades` (−0,472): comprar caro y poco, o barato y mucho. Eso es\n",
    "una idea de negocio con nombre, y es lo único interpretable que sale de aquí.\n",
    "\n",
    "La segunda mezcla unidades, monto y descuento con signos distintos, y ya no\n",
    "se puede decir en voz alta. Ese es el precio del PCA: ganas compresión y pierdes\n",
    "la capacidad de explicar 🔇"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El buscador de columnas gemelas\n",
    "\n",
    "Automatiza lo de arriba: una función que devuelva los pares\n",
    "que hay que revisar antes de modelar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gemelas(v, columnas, listón=0.7):\n",
    "    c = v[columnas].corr(method='spearman').abs()\n",
    "    avisos = []\n",
    "    for i, a in enumerate(columnas):\n",
    "        for j, b in enumerate(columnas):\n",
    "            if i < j and c.iloc[i, j] >= listón:\n",
    "                avisos.append(f'{a} y {b}: {c.iloc[i, j]:.4f}')\n",
    "    return avisos\n",
    "\n",
    "for listón in (0.5, 0.7, 0.9):\n",
    "    encontradas = gemelas(ventas, NUMERICAS, listón)\n",
    "    print(f'con listón {listón}: {len(encontradas)} pares')\n",
    "    for a in encontradas:\n",
    "        print('   ', a)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "con listón 0.5: 3 pares\n",
    "    monto y precio_unitario: 0.8142\n",
    "    monto y monto_final_facturado: 0.5397\n",
    "    monto_final_facturado y compro: 0.8897\n",
    "con listón 0.7: 2 pares\n",
    "    monto y precio_unitario: 0.8142\n",
    "    monto_final_facturado y compro: 0.8897\n",
    "con listón 0.9: 0 pares\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese listón no tiene un valor correcto: depende de para qué. Para una\n",
    "regresión, dos columnas a 0,9 se pisan y hay que quitar una. Para un boosting,\n",
    "casi da igual 🌳\n",
    "\n",
    "Lo que sí es universal es mirar la lista antes de entrenar, porque un par a\n",
    "0,89 contra el objetivo no es colinealidad, es una fuga."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Qué pasa si quitas una de las gemelas\n",
    "\n",
    "`monto` y `precio_unitario` van a\n",
    "0,81. Mide qué se pierde quitando una."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import cross_val_score, StratifiedKFold\n",
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n",
    "y = ventas['compro']\n",
    "juegos = {\n",
    "    'las dos': ['monto', 'precio_unitario', 'satisfaccion', 'unidades'],\n",
    "    'solo monto': ['monto', 'satisfaccion', 'unidades'],\n",
    "    'solo precio': ['precio_unitario', 'satisfaccion', 'unidades'],\n",
    "}\n",
    "for nombre, cols in juegos.items():\n",
    "    X = ventas[cols].fillna(ventas[cols].median())\n",
    "    modelo = make_pipeline(StandardScaler(),\n",
    "                           LogisticRegression(max_iter=1000, random_state=42))\n",
    "    s = cross_val_score(modelo, X, y, cv=cv, scoring='roc_auc')\n",
    "    print(f'{nombre:14s} AUC {s.mean():.4f} +/- {s.std():.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "las dos        AUC 0.6594 +/- 0.0164\n",
    "solo monto     AUC 0.6576 +/- 0.0172\n",
    "solo precio    AUC 0.6384 +/- 0.0112\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ojo, que no son intercambiables 🪞\n",
    "\n",
    "Con las dos sale 0,6594 y quitando `precio_unitario` sale 0,6576:\n",
    "dieciocho diezmilésimas, o sea nada. Pero quitando `monto` se cae a\n",
    "0,6384, veintiún milésimas menos, que ya es más que la desviación de la\n",
    "validación cruzada.\n",
    "\n",
    "Así que van a 0,81 de correlación y aun así una vale más que la otra. Eso es\n",
    "lo que la matriz no puede decirte: **correlacionadas no es lo mismo que\n",
    "equivalentes**.\n",
    "\n",
    "La regla práctica: cuando dos columnas se solapan, se prueba quitando cada\n",
    "una y se mira el AUC. Y a igualdad de resultado, te quedas con la que vas a\n",
    "poder recoger mañana sin problemas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. La confusión, buscada a propósito\n",
    "\n",
    "Repite lo de `monto` con todas las numéricas:\n",
    "correlación global contra correlación dentro de los segmentos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for c in ('monto', 'precio_unitario', 'satisfaccion', 'unidades', 'descuento'):\n",
    "    juntas = ventas[[c, 'compro', 'segmento']].dropna()\n",
    "    total = stats.spearmanr(juntas[c], juntas['compro']).statistic\n",
    "    dentro = [stats.spearmanr(g[c], g['compro']).statistic\n",
    "              for _n, g in juntas.groupby('segmento')]\n",
    "    print(f'{c:18s} global {total:+.4f}   dentro de segmento: '\n",
    "          f'min {min(dentro):+.4f} max {max(dentro):+.4f}   '\n",
    "          f'media {np.mean(dentro):+.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "monto              global +0.2351   dentro de segmento: min -0.0128 max +0.0324   media +0.0129\n",
    "precio_unitario    global +0.1858   dentro de segmento: min -0.0111 max +0.0257   media +0.0090\n",
    "satisfaccion       global +0.1794   dentro de segmento: min +0.1282 max +0.2229   media +0.1808\n",
    "unidades           global +0.0217   dentro de segmento: min -0.0214 max +0.0413   media +0.0155\n",
    "descuento          global +0.0526   dentro de segmento: min +0.0107 max +0.0925   media +0.0673\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y aquí está el contraste que lo explica todo 🔍\n",
    "\n",
    "`monto` pasa de +0,2351 a +0,0129 de media, y\n",
    "`precio_unitario` de +0,1858 a +0,0090. Los dos se desploman, porque\n",
    "los dos dependían del segmento.\n",
    "\n",
    "`satisfaccion` pasa de +0,1794 a **+0,1808**. No se\n",
    "mueve ni una milésima: esa relación es suya y no se la debe a nadie 💪\n",
    "\n",
    "Por eso en el capítulo 22 la satisfacción sale\n",
    "con p de 4,12e-22 y el monto con 0,1561, aunque aquí, mirados solos, el monto\n",
    "parezca mejor.\n",
    "\n",
    "Esta comparación es de las cosas más útiles y más rápidas del análisis\n",
    "exploratorio, y casi nunca se hace. Cuesta seis líneas y evita escribir una\n",
    "recomendación que no se sostiene."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El PCA con las categóricas dentro\n",
    "\n",
    "Arriba solo entraron cinco numéricas. Mete también el\n",
    "one-hot de las cuatro categóricas y mira si cambia algo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "M2 = pd.get_dummies(\n",
    "    ventas[SOLO_NUMERICAS + CATEGORICAS], columns=CATEGORICAS).astype(float)\n",
    "M2 = M2.fillna(M2.median())\n",
    "Z2 = StandardScaler().fit_transform(M2)\n",
    "pca2 = PCA().fit(Z2)\n",
    "acumulado2 = np.cumsum(pca2.explained_variance_ratio_)\n",
    "\n",
    "print('columnas de entrada:', M2.shape[1])\n",
    "print('componentes para el 90%:', int(np.argmax(acumulado2 >= 0.9) + 1))\n",
    "print('las tres primeras explican:',\n",
    "      f'{100 * acumulado2[2]:.2f}%')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "columnas de entrada: 24\n",
    "componentes para el 90%: 17\n",
    "las tres primeras explican: 22.10%\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con el one-hot dentro sale aún peor, y es esperable: cada variable categórica\n",
    "se convierte en varias columnas casi independientes entre sí 🧩\n",
    "\n",
    "Moraleja: el PCA brilla cuando tienes cincuenta columnas numéricas que miden\n",
    "variantes de lo mismo (sensores, encuestas, píxeles). Con trece columnas de\n",
    "negocio bien elegidas, estorba."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La matriz, pero de nulos\n",
    "\n",
    "Las columnas también se relacionan por dónde les faltan\n",
    "datos. Cruza los patrones de nulos entre ellas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "huecos = ventas[['descuento', 'satisfaccion', 'fecha_ultima_compra']].isna()\n",
    "huecos.columns = ['falta_desc', 'falta_satis', 'falta_fecha']\n",
    "\n",
    "print(huecos.mean().round(4).to_string())\n",
    "print()\n",
    "print(huecos.astype(int).corr(method='spearman').round(4).to_string())\n",
    "print()\n",
    "print('filas a las que les falta más de una cosa:',\n",
    "      int((huecos.sum(axis=1) > 1).sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "falta_desc     0.1983\n",
    "falta_satis    0.0770\n",
    "falta_fecha    0.1813\n",
    "\n",
    "             falta_desc  falta_satis  falta_fecha\n",
    "falta_desc       1.0000      -0.0339      -0.0019\n",
    "falta_satis     -0.0339       1.0000      -0.0029\n",
    "falta_fecha     -0.0019      -0.0029       1.0000\n",
    "\n",
    "filas a las que les falta más de una cosa: 169\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si los nulos correlacionaran entre ellos, no serían tres problemas sino uno:\n",
    "un proceso del negocio que se saltó y se llevó tres campos por delante 🕳️\n",
    "\n",
    "Cuando salen independientes, como aquí, cada hueco se trata por su cuenta y\n",
    "cada bandera del capítulo 9 aporta algo distinto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El mapa de calor, en texto\n",
    "\n",
    "Sin librería de gráficos: pinta la matriz con caracteres\n",
    "para verla de un vistazo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "c = ventas[NUMERICAS].corr(method='spearman')\n",
    "escala = ' .:-=+*#%@'\n",
    "\n",
    "print(' ' * 24 + ''.join(f'{n[:6]:>7s}' for n in NUMERICAS))\n",
    "for a in NUMERICAS:\n",
    "    fila = ''\n",
    "    for b in NUMERICAS:\n",
    "        nivel = int(abs(c.loc[a, b]) * (len(escala) - 1))\n",
    "        fila += f'{escala[nivel] * 3:>7s}'\n",
    "    print(f'{a:24s}{fila}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "                         unidad  monto descue satisf precio monto_ compro\n",
    "unidades                    @@@                         ===              \n",
    "monto                              @@@                  ###    ===    :::\n",
    "descuento                                 @@@                            \n",
    "satisfaccion                                     @@@           ...    ...\n",
    "precio_unitario             ===    ###                  @@@    ---    ...\n",
    "monto_final_facturado              ===           ...    ---    @@@    %%%\n",
    "compro                             :::           ...    ...    %%%    @@@\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una matriz de trece por trece con números no se lee. Con densidad, sí: el ojo\n",
    "encuentra los bloques oscuros solo 👁️\n",
    "\n",
    "Y en un cuaderno de verdad esto se hace con un mapa de calor de dos líneas.\n",
    "Lo pongo en texto porque así se ve exactamente qué está pintando y porque\n",
    "funciona en cualquier sitio, incluido un correo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El informe multivariado, en avisos\n",
    "\n",
    "Cierra el bloque de EDA con la función que resume las tres\n",
    "comprobaciones de este capítulo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def revisa_maraña(v, numericas, objetivo='compro'):\n",
    "    avisos = []\n",
    "    c = v[numericas].corr(method='spearman')\n",
    "    for i, a in enumerate(numericas):\n",
    "        for j, b in enumerate(numericas):\n",
    "            if i < j and abs(c.iloc[i, j]) >= 0.7:\n",
    "                if objetivo in (a, b):\n",
    "                    avisos.append(f'ALARMA: {a} y {b} van a {c.iloc[i, j]:.3f}. '\n",
    "                                  'Sospecha de fuga.')\n",
    "                else:\n",
    "                    avisos.append(f'gemelas: {a} y {b} van a {c.iloc[i, j]:.3f}')\n",
    "    for col in numericas:\n",
    "        if col == objetivo:\n",
    "            continue\n",
    "        juntas = v[[col, objetivo, 'segmento']].dropna()\n",
    "        total = stats.spearmanr(juntas[col], juntas[objetivo]).statistic\n",
    "        dentro = np.mean([stats.spearmanr(g[col], g[objetivo]).statistic\n",
    "                          for _n, g in juntas.groupby('segmento')])\n",
    "        if abs(total) > 0.1 and abs(dentro) < abs(total) / 3:\n",
    "            avisos.append(f'confusion: {col} correlaciona {total:+.3f} en total '\n",
    "                          f'y {dentro:+.3f} dentro de segmento')\n",
    "    return avisos\n",
    "\n",
    "for aviso in revisa_maraña(ventas, NUMERICAS):\n",
    "    print(' ', aviso)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  gemelas: monto y precio_unitario van a 0.814\n",
    "  ALARMA: monto_final_facturado y compro van a 0.890. Sospecha de fuga.\n",
    "  confusion: monto correlaciona +0.235 en total y +0.013 dentro de segmento\n",
    "  confusion: precio_unitario correlaciona +0.186 en total y +0.009 dentro de segmento\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro líneas de salida y las cuatro son conversaciones que hay que tener\n",
    "antes de entrenar nada 📋\n",
    "\n",
    "Con esto se cierra el bloque de exploración. Nueve capítulos para llegar aquí\n",
    "puede parecer mucho, y sin embargo esta función acaba de encontrar una fuga y\n",
    "una variable confusora **sin entrenar un solo modelo** 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Una columna correlaciona +0,24 con el objetivo en las 3.000 filas, y dentro de cada segmento la correlación se va a cero. ¿Qué está pasando?\n",
    "\n",
    "a) Que lo que correlaciona con el objetivo es el segmento, no la columna\n",
    "\n",
    "b) Que hacen falta más datos dentro de cada segmento\n",
    "\n",
    "c) Que la relación es real pero no lineal\n",
    "\n",
    "d) Que hay que quitar el segmento del modelo para verla\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Con setecientas filas por grupo se detectaría una correlación de ese tamaño sin problema.\n",
    "\n",
    "*c)* Spearman no pide que sea lineal: mide si una sube cuando la otra sube.\n",
    "\n",
    "*d)* Quitar la variable que explica lo que ves es la forma más rápida de creerte algo falso.\n",
    "\n",
    "Una relación que desaparece al partir por grupos era del grupo, no de la columna."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El filtro automático que no filtra nada"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "El filtro de columnas redundantes de toda la vida: se calcula la matriz de correlaciones, y lo que pase de 0,8 con otra se cae. Dos líneas y a otra cosa.\n",
    "\n",
    "```\n",
    "c = ventas.corr().abs()\n",
    "redundantes = [col for col in c\n",
    "               if (c[col] > 0.8).sum() > 1]\n",
    "\n",
    "# redundantes: []  -> 'no hay ninguna'\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "`corr()` usa Pearson por defecto, y Pearson solo ve las relaciones que son una recta 📏 En este mismo archivo, monto y precio_unitario dan **0,446 con Pearson y 0,814 con Spearman**. El filtro de arriba las deja pasar a las dos porque 0,446 no llega a 0,8, y te quedas convencida de que no hay redundancia.\n",
    "\n",
    "La relación está ahí, solo que no es una recta: es monótona. Por eso en este capítulo la matriz se lee con `method='spearman'`, y por eso un umbral automático sobre la matriz equivocada es peor que no poner ninguno: te da permiso para no mirar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 📊 La matriz se lee con Spearman, no con Pearson. `monto` y\n",
    "`precio_unitario` dan 0,814 con una y 0,446 con la otra, porque\n",
    "Pearson pide que la relación sea una recta.\n",
    "\n",
    "- 🚨 `monto_final_facturado` correlaciona 0,8897 con el objetivo.\n",
    "Por encima de 0,8 contra lo que quieres predecir, primero se desconfía y\n",
    "después se celebra.\n",
    "\n",
    "- 🪞 `monto` y `precio_unitario` a 0,81 son casi la\n",
    "misma columna, y por eso en la regresión se repartían el efecto.\n",
    "\n",
    "- 🔤 Las cuatro categóricas están por debajo de 0,05 de V entre ellas, o sea\n",
    "prácticamente independientes.\n",
    "\n",
    "- 🤯 Y la grande: el monto correlaciona +0,2351 con la compra en las 3.000\n",
    "filas y **dentro de cada segmento se va a cero**. El monto no\n",
    "predice la compra, predice el segmento.\n",
    "\n",
    "- 🎭 Eso se llama variable confusora, explica la p de 0,1561 del capítulo\n",
    "22 y demuestra que un análisis bivariado solo\n",
    "puede llevarte a una conclusión completamente equivocada.\n",
    "\n",
    "- 📉 El PCA aquí no sirve: hacen falta cuatro componentes de cinco para el\n",
    "90%. Comprime lo repetido, y estas columnas no se repiten.\n",
    "\n",
    "- 🔇 Y lo que sí se lee de él, la primera componente, es una idea de negocio:\n",
    "comprar caro y poco contra barato y mucho.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos columnas que dicen lo mismo no son el doble de información. Son la mitad de sitio para pensar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El PCA que sale al final de este capítulo tiene su explicación despacio en el [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": [
    "Nueve capítulos de exploración y ya sabemos qué columna sirve, cuál engaña y\n",
    "cuál es una fuga. Toca convertir todo eso en decisiones 🧱\n",
    "\n",
    "El capítulo 9 saca cinco columnas nuevas de las que\n",
    "ya hay, y es donde de verdad se gana: cambiar de modelo te da décimas y\n",
    "construir una columna buena te da puntos enteros.\n",
    "\n",
    "Lo que verás allí:\n",
    "\n",
    "- 🕳️ El hueco convertido en dato, que es el hallazgo más grande de todo el\n",
    "bloque de exploración.\n",
    "\n",
    "- 💵 El precio por unidad, que aquí salió correlacionado con el monto y que\n",
    "aun así aporta.\n",
    "\n",
    "- 📊 El monto partido en tramos, para que un modelo lineal pueda ver lo que\n",
    "solo veía el árbol.\n",
    "\n",
    "- 🗑️ Una que no sirve, para ver cómo se descarta con argumentos.\n",
    "\n",
    "- 🪤 Y una trampa que sube el AUC treinta puntos y no vale nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 8 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/eda-multivariado/\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
}
