# Las columnas entre ellas

URL canónica: https://missyera.com/guias/machine-learning-desde-cero/eda-multivariado/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Durante años miré las correlaciones con Pearson porque es lo que sale por defecto, y Pearson pide que la relación sea una recta. Aquí monto y precio_unitario dan 0,814 con Spearman y 0,446 con Pearson: es el mismo par de columnas y una de las dos lecturas te dice que no pasa nada. La maraña entre columnas es donde se esconde lo caro 🕸️

Capítulo 8 de 29 del libro [Machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero.md).

El capítulo anterior dejó dos preguntas colgando, y las dos se contestan igual: **mirando las columnas entre ellas** 🕸️

`sin_descuento` tenía doce puntos y no le va a aportar nada al modelo. `monto` era la mejor de las numéricas y en el capítulo 22 se queda en p de 0,1561.

Las dos cosas pasan por lo mismo: una columna no vive sola. Y este capítulo termina con un resultado que a mí me sigue pareciendo el más bonito del libro 🤯

## El punto de partida

```
import numpy as np
import pandas as pd
from scipy import stats

URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'

def carga_limpia(url):
    v = pd.read_csv(url).drop_duplicates()
    v['ciudad'] = (v['ciudad'].str.strip().str.lower()
                   .str.normalize('NFKD')
                   .str.encode('ascii', 'ignore').str.decode('utf-8'))
    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))
    for col in ['fecha', 'fecha_ultima_compra']:
        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')
        falta = f.isna() & v[col].notna()
        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')
        v[col] = f
    return v

ventas = carga_limpia(URL)
ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']

NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion',
             'precio_unitario', 'monto_final_facturado', 'compro']

print('columnas que entran en la maraña:', len(NUMERICAS))
```

```text
columnas que entran en la maraña: 7
```

## El error de la primera matriz

```
ventas[['segmento', 'monto']].corr()
```

```
ValueError: could not convert string to float: 'Horeca'
```

Una correlación necesita números y `segmento` es texto. Para cruzar dos categóricas entre ellas hace falta otra cosa, y la vemos más abajo 🔤

## La matriz, con Spearman y no con Pearson

`ρ = 1−(6∑d_i^2)/(n(n^2−1))`

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

```
print(ventas[NUMERICAS].corr(method='spearman').round(3).to_string())
```

```text
                       unidades  monto  descuento  satisfaccion  precio_unitario  monto_final_facturado  compro
unidades                  1.000  0.022     -0.005         0.005           -0.493                  0.015   0.022
monto                     0.022  1.000     -0.020        -0.005            0.814                  0.540   0.235
descuento                -0.005 -0.020      1.000        -0.019           -0.009                  0.005   0.053
satisfaccion              0.005 -0.005     -0.019         1.000           -0.008                  0.138   0.179
precio_unitario          -0.493  0.814     -0.009        -0.008            1.000                  0.439   0.186
monto_final_facturado     0.015  0.540      0.005         0.138            0.439                  1.000   0.890
compro                    0.022  0.235      0.053         0.179            0.186                  0.890   1.000
```

Antes de leerla, la pregunta que casi nadie hace: **¿por qué Spearman?**

```
spearman = ventas[NUMERICAS].corr(method='spearman')
pearson = ventas[NUMERICAS].corr(method='pearson')

pares = []
for i, a in enumerate(NUMERICAS):
    for j, b in enumerate(NUMERICAS):
        if i < j:
            pares.append((abs(spearman.iloc[i, j] - pearson.iloc[i, j]),
                          a, b, spearman.iloc[i, j], pearson.iloc[i, j]))

for diferencia, a, b, s, p in sorted(pares, reverse=True)[:4]:
    print(f'{a:22s} {b:22s} spearman {s:+.3f}  pearson {p:+.3f}  '
          f'se llevan {diferencia:.3f}')
```

```text
monto                  precio_unitario        spearman +0.814  pearson +0.446  se llevan 0.369
monto_final_facturado  compro                 spearman +0.890  pearson +0.606  se llevan 0.283
monto                  monto_final_facturado  spearman +0.540  pearson +0.740  se llevan 0.200
unidades               precio_unitario        spearman -0.493  pearson -0.409  se llevan 0.083
```

Ahí está el motivo 👀

`monto` y `precio_unitario` dan 0,814 con Spearman y 0,446 con Pearson. Casi el doble.

Pearson mide si la relación es una **recta**, y esas dos columnas suben juntas pero no en línea recta: en el capítulo 6 vimos que `precio_unitario` tiene asimetría 6,734, así que cuatro ventas enormes le doblan la recta a Pearson y le esconden la relación.

Spearman trabaja con los puestos, no con los valores. La venta más cara es la número uno, valga 3.000 o 300.000. Por eso no se deja arrastrar 📊

## Los pares que hay que mirar sí o sí

```
c = ventas[NUMERICAS].corr(method='spearman').abs()
for i, a in enumerate(NUMERICAS):
    for j, b in enumerate(NUMERICAS):
        if i < j and c.iloc[i, j] > 0.5:
            print(f'{a:22s} {b:22s} {c.iloc[i, j]:.4f}')
```

```text
monto                  precio_unitario        0.8142
monto                  monto_final_facturado  0.5397
monto_final_facturado  compro                 0.8897
```

Tres pares, y cada uno cuenta una cosa distinta:

- 🪞 `monto` con `precio_unitario`, 0,8142. Normal: la segunda sale de dividir la primera. Son casi la misma columna y por eso en el capítulo 22 se repartían el efecto.
- 💰 `monto` con `monto_final_facturado`, 0,5397. También esperable, son dos formas de contar el mismo dinero.
- 🚨 `monto_final_facturado` con `compro`, **0,8897**. Y esa no es normal en absoluto.

Una columna que correlaciona 0,89 con lo que quieres predecir **no es una buena columna: es una alarma**. Es la fuga del capítulo 12, y aquí sale sin entrenar ningún modelo, mirando una matriz 🎯

La regla que uso: por encima de 0,8 contra el objetivo, primero desconfío y después celebro. Casi siempre resulta que la columna se rellena después de que pase lo que quiero predecir.

## Y las categóricas entre ellas

Para dos categóricas no hay correlación, hay V de Cramér, que es la misma del capítulo 7 usada entre columnas en vez de contra el objetivo.

```
CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']

def cramer(tabla):
    chi2 = stats.chi2_contingency(tabla)[0]
    n = tabla.values.sum()
    return np.sqrt(chi2 / (n * (min(tabla.shape) - 1)))

for i, a in enumerate(CATEGORICAS):
    for j, b in enumerate(CATEGORICAS):
        if i < j:
            print(f'{a:12s} {b:12s} V {cramer(pd.crosstab(ventas[a], ventas[b])):.4f}')
```

```text
ciudad       segmento     V 0.0267
ciudad       canal        V 0.0372
ciudad       categoria    V 0.0480
segmento     canal        V 0.0319
segmento     categoria    V 0.0443
canal        categoria    V 0.0340
```

Las seis por debajo de 0,05, o sea prácticamente independientes 😌

Eso significa que saber el segmento no te dice nada del canal, y que el one-hot de las cuatro no va a crear columnas redundantes. Aburrido otra vez, y otra vez es buena noticia.

![Cinco barras con la correlación entre el monto y la compra. La de arriba, con las 3.000 ventas juntas, mide +0,2351. Las cuatro de abajo, una por segmento, están pegadas al cero.](https://missyera.com/static/diagramas/webp/g-ml-confusion-monto.webp)

*La relación entre el monto y la compra existe cuando miras las 3.000 ventas juntas y desaparece dentro de cada segmento. El monto no predice la compra: predice el segmento, y el segmento sí predice la compra.*

## Y ahora sí: la relación que se evapora

Esta es la que cierra el hilo de tres capítulos 🎬

```
global_ = stats.spearmanr(ventas['monto'], ventas['compro'])
print(f'global      rho {global_.statistic:+.4f}  p {global_.pvalue:.2e}  '
      f'n {len(ventas)}')

for segmento in sorted(ventas['segmento'].unique()):
    trozo = ventas[ventas['segmento'] == segmento]
    r = stats.spearmanr(trozo['monto'], trozo['compro'])
    print(f'{segmento:12s} rho {r.statistic:+.4f}  p {r.pvalue:.4f}  n {len(trozo)}')
```

```text
global      rho +0.2351  p 6.01e-39  n 3000
Bodega       rho +0.0324  p 0.3903  n 707
Horeca       rho -0.0128  p 0.7275  n 742
Mayorista    rho +0.0061  p 0.8681  n 750
Minimarket   rho +0.0262  p 0.4594  n 801
```

Míralo despacio, porque es de esas cosas que hay que ver dos veces 🤯

En las 3.000 ventas juntas, el monto correlaciona +0,2351 con la compra, con una p de 6,01e-39. Un número que en cualquier informe se escribe como hallazgo.

Y **dentro de cada segmento no queda nada**: +0,0324, −0,0128, +0,0061, +0,0262. Las cuatro con p por encima de 0,39, y con setecientas filas cada una, que son de sobra para detectar algo de ese tamaño.

La explicación está en dos tablas:

```
print(ventas.groupby('segmento')['monto'].agg(['mean', 'median', 'count'])
      .round(2).to_string())
print()
print(ventas.groupby('segmento')['compro'].mean().round(4).to_string())
```

```text
               mean   median  count
segmento
Bodega       178.70   183.26    707
Horeca       755.18   743.00    742
Mayorista   1856.34  1869.26    750
Minimarket   414.92   413.74    801

segmento
Bodega        0.3748
Horeca        0.6321
Mayorista     0.7240
Minimarket    0.5693
```

El mayorista compra por 1.856 soles de media y cierra el 72,40%. La bodega compra por 178 y cierra el 37,48%.

O sea que **el monto no predice la compra: predice el segmento**, y el segmento sí predice la compra. Cuando miras las 3.000 juntas, el monto se lleva un mérito que no es suyo 🎭

Esto tiene nombre, se llama *confusión* o variable confusora, y es la razón por la que el capítulo 22 daba p de 0,1561 para el monto: allí el segmento estaba dentro del modelo, así que ya no le dejaba llevarse nada prestado.

Y la razón por la que un análisis bivariado, él solo, **puede llevarte a una conclusión completamente equivocada**. "Sube el ticket medio y cerrarás más" es exactamente lo que se deduce del +0,2351, y aquí está la prueba de que no 🚫

## Reducir trece columnas a unas pocas

Cuando hay muchas columnas que dicen lo mismo, se pueden resumir. El PCA busca las direcciones donde los datos más varían y las pone primero.

```
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

SOLO_NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion',
                  'precio_unitario']
M = ventas[SOLO_NUMERICAS].fillna(ventas[SOLO_NUMERICAS].median())
Z = StandardScaler().fit_transform(M)

pca = PCA().fit(Z)
acumulado = np.cumsum(pca.explained_variance_ratio_)
for i, (parte, total) in enumerate(zip(pca.explained_variance_ratio_, acumulado), 1):
    print(f'componente {i}: explica {100 * parte:5.2f}%   acumulado {100 * total:6.2f}%')
print()
print('componentes para llegar al 90%:', int(np.argmax(acumulado >= 0.9) + 1),
      'de', len(SOLO_NUMERICAS))
```

```text
componente 1: explica 31.95%   acumulado  31.95%
componente 2: explica 20.64%   acumulado  52.58%
componente 3: explica 20.28%   acumulado  72.86%
componente 4: explica 19.42%   acumulado  92.28%
componente 5: explica  7.72%   acumulado 100.00%

componentes para llegar al 90%: 4 de 5
```

Y aquí el PCA **no sirve para nada**, que también es un resultado 😅

Hacen falta cuatro componentes de cinco para llegar al 90%. Cambiar cinco columnas que se entienden por cuatro que no se entienden es un mal negocio.

Tiene todo el sentido: la matriz de correlaciones ya decía que estas columnas son casi independientes. El PCA solo comprime lo que está repetido, y aquí no hay casi nada repetido.

El escalado antes del PCA no es opcional, por cierto. Sin él, `monto` mandaría en la primera componente solo por ir en miles mientras `satisfaccion` va de 1 a 5 📏

## Qué mira cada componente

```
carga = pd.DataFrame(pca.components_[:2].T, index=SOLO_NUMERICAS,
                     columns=['CP1', 'CP2'])
print(carga.round(3).to_string())
```

```text
                   CP1    CP2
unidades        -0.472  0.613
monto            0.519  0.555
descuento       -0.022 -0.562
satisfaccion    -0.033  0.028
precio_unitario  0.712 -0.014
```

La primera componente es `precio_unitario` (0,712) contra `unidades` (−0,472): comprar caro y poco, o barato y mucho. Eso es una idea de negocio con nombre, y es lo único interpretable que sale de aquí.

La segunda mezcla unidades, monto y descuento con signos distintos, y ya no se puede decir en voz alta. Ese es el precio del PCA: ganas compresión y pierdes la capacidad de explicar 🔇

## Ejercicios

**1. El buscador de columnas gemelas**

Automatiza lo de arriba: una función que devuelva los pares que hay que revisar antes de modelar.

```
def gemelas(v, columnas, listón=0.7):
    c = v[columnas].corr(method='spearman').abs()
    avisos = []
    for i, a in enumerate(columnas):
        for j, b in enumerate(columnas):
            if i < j and c.iloc[i, j] >= listón:
                avisos.append(f'{a} y {b}: {c.iloc[i, j]:.4f}')
    return avisos

for listón in (0.5, 0.7, 0.9):
    encontradas = gemelas(ventas, NUMERICAS, listón)
    print(f'con listón {listón}: {len(encontradas)} pares')
    for a in encontradas:
        print('   ', a)
```

```text
con listón 0.5: 3 pares
    monto y precio_unitario: 0.8142
    monto y monto_final_facturado: 0.5397
    monto_final_facturado y compro: 0.8897
con listón 0.7: 2 pares
    monto y precio_unitario: 0.8142
    monto_final_facturado y compro: 0.8897
con listón 0.9: 0 pares
```

Ese listón no tiene un valor correcto: depende de para qué. Para una regresión, dos columnas a 0,9 se pisan y hay que quitar una. Para un boosting, casi da igual 🌳

Lo que sí es universal es mirar la lista antes de entrenar, porque un par a 0,89 contra el objetivo no es colinealidad, es una fuga.

**2. Qué pasa si quitas una de las gemelas**

`monto` y `precio_unitario` van a 0,81. Mide qué se pierde quitando una.

```
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.pipeline import make_pipeline

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
y = ventas['compro']
juegos = {
    'las dos': ['monto', 'precio_unitario', 'satisfaccion', 'unidades'],
    'solo monto': ['monto', 'satisfaccion', 'unidades'],
    'solo precio': ['precio_unitario', 'satisfaccion', 'unidades'],
}
for nombre, cols in juegos.items():
    X = ventas[cols].fillna(ventas[cols].median())
    modelo = make_pipeline(StandardScaler(),
                           LogisticRegression(max_iter=1000, random_state=42))
    s = cross_val_score(modelo, X, y, cv=cv, scoring='roc_auc')
    print(f'{nombre:14s} AUC {s.mean():.4f} +/- {s.std():.4f}')
```

```text
las dos        AUC 0.6594 +/- 0.0164
solo monto     AUC 0.6576 +/- 0.0172
solo precio    AUC 0.6384 +/- 0.0112
```

Y ojo, que no son intercambiables 🪞

Con las dos sale 0,6594 y quitando `precio_unitario` sale 0,6576: dieciocho diezmilésimas, o sea nada. Pero quitando `monto` se cae a 0,6384, veintiún milésimas menos, que ya es más que la desviación de la validación cruzada.

Así que van a 0,81 de correlación y aun así una vale más que la otra. Eso es lo que la matriz no puede decirte: **correlacionadas no es lo mismo que equivalentes**.

La regla práctica: cuando dos columnas se solapan, se prueba quitando cada una y se mira el AUC. Y a igualdad de resultado, te quedas con la que vas a poder recoger mañana sin problemas.

**3. La confusión, buscada a propósito**

Repite lo de `monto` con todas las numéricas: correlación global contra correlación dentro de los segmentos.

```
for c in ('monto', 'precio_unitario', 'satisfaccion', 'unidades', 'descuento'):
    juntas = ventas[[c, 'compro', 'segmento']].dropna()
    total = stats.spearmanr(juntas[c], juntas['compro']).statistic
    dentro = [stats.spearmanr(g[c], g['compro']).statistic
              for _n, g in juntas.groupby('segmento')]
    print(f'{c:18s} global {total:+.4f}   dentro de segmento: '
          f'min {min(dentro):+.4f} max {max(dentro):+.4f}   '
          f'media {np.mean(dentro):+.4f}')
```

```text
monto              global +0.2351   dentro de segmento: min -0.0128 max +0.0324   media +0.0129
precio_unitario    global +0.1858   dentro de segmento: min -0.0111 max +0.0257   media +0.0090
satisfaccion       global +0.1794   dentro de segmento: min +0.1282 max +0.2229   media +0.1808
unidades           global +0.0217   dentro de segmento: min -0.0214 max +0.0413   media +0.0155
descuento          global +0.0526   dentro de segmento: min +0.0107 max +0.0925   media +0.0673
```

Y aquí está el contraste que lo explica todo 🔍

`monto` pasa de +0,2351 a +0,0129 de media, y `precio_unitario` de +0,1858 a +0,0090. Los dos se desploman, porque los dos dependían del segmento.

`satisfaccion` pasa de +0,1794 a **+0,1808**. No se mueve ni una milésima: esa relación es suya y no se la debe a nadie 💪

Por eso en el capítulo 22 la satisfacción sale con p de 4,12e-22 y el monto con 0,1561, aunque aquí, mirados solos, el monto parezca mejor.

Esta comparación es de las cosas más útiles y más rápidas del análisis exploratorio, y casi nunca se hace. Cuesta seis líneas y evita escribir una recomendación que no se sostiene.

**4. El PCA con las categóricas dentro**

Arriba solo entraron cinco numéricas. Mete también el one-hot de las cuatro categóricas y mira si cambia algo.

```
M2 = pd.get_dummies(
    ventas[SOLO_NUMERICAS + CATEGORICAS], columns=CATEGORICAS).astype(float)
M2 = M2.fillna(M2.median())
Z2 = StandardScaler().fit_transform(M2)
pca2 = PCA().fit(Z2)
acumulado2 = np.cumsum(pca2.explained_variance_ratio_)

print('columnas de entrada:', M2.shape[1])
print('componentes para el 90%:', int(np.argmax(acumulado2 >= 0.9) + 1))
print('las tres primeras explican:',
      f'{100 * acumulado2[2]:.2f}%')
```

```text
columnas de entrada: 24
componentes para el 90%: 17
las tres primeras explican: 22.10%
```

Con el one-hot dentro sale aún peor, y es esperable: cada variable categórica se convierte en varias columnas casi independientes entre sí 🧩

Moraleja: el PCA brilla cuando tienes cincuenta columnas numéricas que miden variantes de lo mismo (sensores, encuestas, píxeles). Con trece columnas de negocio bien elegidas, estorba.

**5. La matriz, pero de nulos**

Las columnas también se relacionan por dónde les faltan datos. Cruza los patrones de nulos entre ellas.

```
huecos = ventas[['descuento', 'satisfaccion', 'fecha_ultima_compra']].isna()
huecos.columns = ['falta_desc', 'falta_satis', 'falta_fecha']

print(huecos.mean().round(4).to_string())
print()
print(huecos.astype(int).corr(method='spearman').round(4).to_string())
print()
print('filas a las que les falta más de una cosa:',
      int((huecos.sum(axis=1) > 1).sum()))
```

```text
falta_desc     0.1983
falta_satis    0.0770
falta_fecha    0.1813

             falta_desc  falta_satis  falta_fecha
falta_desc       1.0000      -0.0339      -0.0019
falta_satis     -0.0339       1.0000      -0.0029
falta_fecha     -0.0019      -0.0029       1.0000

filas a las que les falta más de una cosa: 169
```

Si los nulos correlacionaran entre ellos, no serían tres problemas sino uno: un proceso del negocio que se saltó y se llevó tres campos por delante 🕳️

Cuando salen independientes, como aquí, cada hueco se trata por su cuenta y cada bandera del capítulo 9 aporta algo distinto.

**6. El mapa de calor, en texto**

Sin librería de gráficos: pinta la matriz con caracteres para verla de un vistazo.

```
c = ventas[NUMERICAS].corr(method='spearman')
escala = ' .:-=+*#%@'

print(' ' * 24 + ''.join(f'{n[:6]:>7s}' for n in NUMERICAS))
for a in NUMERICAS:
    fila = ''
    for b in NUMERICAS:
        nivel = int(abs(c.loc[a, b]) * (len(escala) - 1))
        fila += f'{escala[nivel] * 3:>7s}'
    print(f'{a:24s}{fila}')
```

```text
                         unidad  monto descue satisf precio monto_ compro
unidades                    @@@                         ===
monto                              @@@                  ###    ===    :::
descuento                                 @@@
satisfaccion                                     @@@           ...    ...
precio_unitario             ===    ###                  @@@    ---    ...
monto_final_facturado              ===           ...    ---    @@@    %%%
compro                             :::           ...    ...    %%%    @@@
```

Una matriz de trece por trece con números no se lee. Con densidad, sí: el ojo encuentra los bloques oscuros solo 👁️

Y en un cuaderno de verdad esto se hace con un mapa de calor de dos líneas. Lo pongo en texto porque así se ve exactamente qué está pintando y porque funciona en cualquier sitio, incluido un correo.

**7. El informe multivariado, en avisos**

Cierra el bloque de EDA con la función que resume las tres comprobaciones de este capítulo.

```
def revisa_maraña(v, numericas, objetivo='compro'):
    avisos = []
    c = v[numericas].corr(method='spearman')
    for i, a in enumerate(numericas):
        for j, b in enumerate(numericas):
            if i < j and abs(c.iloc[i, j]) >= 0.7:
                if objetivo in (a, b):
                    avisos.append(f'ALARMA: {a} y {b} van a {c.iloc[i, j]:.3f}. '
                                  'Sospecha de fuga.')
                else:
                    avisos.append(f'gemelas: {a} y {b} van a {c.iloc[i, j]:.3f}')
    for col in numericas:
        if col == objetivo:
            continue
        juntas = v[[col, objetivo, 'segmento']].dropna()
        total = stats.spearmanr(juntas[col], juntas[objetivo]).statistic
        dentro = np.mean([stats.spearmanr(g[col], g[objetivo]).statistic
                          for _n, g in juntas.groupby('segmento')])
        if abs(total) > 0.1 and abs(dentro) < abs(total) / 3:
            avisos.append(f'confusion: {col} correlaciona {total:+.3f} en total '
                          f'y {dentro:+.3f} dentro de segmento')
    return avisos

for aviso in revisa_maraña(ventas, NUMERICAS):
    print(' ', aviso)
```

```text
  gemelas: monto y precio_unitario van a 0.814
  ALARMA: monto_final_facturado y compro van a 0.890. Sospecha de fuga.
  confusion: monto correlaciona +0.235 en total y +0.013 dentro de segmento
  confusion: precio_unitario correlaciona +0.186 en total y +0.009 dentro de segmento
```

Cuatro líneas de salida y las cuatro son conversaciones que hay que tener antes de entrenar nada 📋

Con esto se cierra el bloque de exploración. Nueve capítulos para llegar aquí puede parecer mucho, y sin embargo esta función acaba de encontrar una fuga y una variable confusora **sin entrenar un solo modelo** 💛

Comprueba que lo tienes

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?

- Que lo que correlaciona con el objetivo es el segmento, no la columna
- Que hacen falta más datos dentro de cada segmento
- Que la relación es real pero no lineal
- Que hay que quitar el segmento del modelo para verla

## El filtro automático que no filtra nada

La trampa

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.

```
c = ventas.corr().abs()
redundantes = [col for col in c
               if (c[col] > 0.8).sum() > 1]

# redundantes: []  -> 'no hay ninguna'
```

**Qué está mal**

`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.

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.

## Lo que te llevas

- 📊 La matriz se lee con Spearman, no con Pearson. `monto` y `precio_unitario` dan 0,814 con una y 0,446 con la otra, porque Pearson pide que la relación sea una recta.
- 🚨 `monto_final_facturado` correlaciona 0,8897 con el objetivo. Por encima de 0,8 contra lo que quieres predecir, primero se desconfía y después se celebra.
- 🪞 `monto` y `precio_unitario` a 0,81 son casi la misma columna, y por eso en la regresión se repartían el efecto.
- 🔤 Las cuatro categóricas están por debajo de 0,05 de V entre ellas, o sea prácticamente independientes.
- 🤯 Y la grande: el monto correlaciona +0,2351 con la compra en las 3.000 filas y **dentro de cada segmento se va a cero**. El monto no predice la compra, predice el segmento.
- 🎭 Eso se llama variable confusora, explica la p de 0,1561 del capítulo 22 y demuestra que un análisis bivariado solo puede llevarte a una conclusión completamente equivocada.
- 📉 El PCA aquí no sirve: hacen falta cuatro componentes de cinco para el 90%. Comprime lo repetido, y estas columnas no se repiten.
- 🔇 Y lo que sí se lee de él, la primera componente, es una idea de negocio: comprar caro y poco contra barato y mucho.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Dos columnas que dicen lo mismo no son el doble de información. Son la mitad de sitio para pensar.

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/) 📊

## Qué viene ahora

Nueve capítulos de exploración y ya sabemos qué columna sirve, cuál engaña y cuál es una fuga. Toca convertir todo eso en decisiones 🧱

El capítulo 9 saca cinco columnas nuevas de las que ya hay, y es donde de verdad se gana: cambiar de modelo te da décimas y construir una columna buena te da puntos enteros.

Lo que verás allí:

- 🕳️ El hueco convertido en dato, que es el hallazgo más grande de todo el bloque de exploración.
- 💵 El precio por unidad, que aquí salió correlacionado con el monto y que aun así aporta.
- 📊 El monto partido en tramos, para que un modelo lineal pueda ver lo que solo veía el árbol.
- 🗑️ Una que no sirve, para ver cómo se descarta con argumentos.
- 🪤 Y una trampa que sube el AUC treinta puntos y no vale nada.

## Seguir leyendo

- Anterior: [Cada columna contra el objetivo](https://missyera.com/guias/machine-learning-desde-cero/eda-bivariado.md)
- Siguiente: [Construir columnas](https://missyera.com/guias/machine-learning-desde-cero/construir-columnas.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/machine-learning-desde-cero/eda-multivariado/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
