Capítulo 8 de 25 11 secciones 17 min

Las columnas entre ellas, que es donde estaba la trampa

La matriz que delata una fuga sin entrenar nada, y la correlación de 0,2351 que se evapora al partir por segmento.

La matriz de correlaciones se lee con Spearman y no con Pearson, porque Pearson pide que la relación sea una recta: monto y precio_unitario dan 0,814 con una y 0,446 con la otra. Ahí sale la fuga sin entrenar nada, porque monto_final_facturado correlaciona 0,8897 con el objetivo. Y el hallazgo grande: monto correlaciona +0,2351 con la compra en las 3.000 filas y dentro de cada segmento se va a cero, con p por encima de 0,39 en los cuatro. El monto no predice la compra, predice el segmento. El PCA aquí no sirve: hacen falta cuatro componentes de cinco para llegar al 90%.

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 21 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))
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

ρ=16di2n(n21)

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())
                       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}')
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}')
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 21 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}')
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.
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)}')
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())
               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 21 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))
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())
                   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)
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}')
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}')
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 21 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}%')
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()))
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}')
                         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)
  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

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

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.
¿Tienes alguna duda o consulta?