Ya está limpio y ya sabemos qué es cada columna. Toca mirarlas una por una, sin cruzarlas con nada 🔎
Y esto no es un trámite. Lo que salga aquí decide cosas muy concretas más adelante: qué se escala, qué se transforma, qué se parte en tramos, qué imputar con la media y qué con la mediana. Saltarse este capítulo es tomar todas esas decisiones a ojo.
Tranqui, que son cuatro números por columna y una función que los saca todos 💜
La ficha de una columna
El resumen que yo saco de cada columna numérica antes de decidir nada.
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']
def ficha(v, columnas):
filas = []
for c in columnas:
s = v[c].dropna()
filas.append({
'columna': c,
'n': len(s),
'nulos_%': round(100 * v[c].isna().mean(), 2),
'media': round(s.mean(), 2),
'mediana': round(s.median(), 2),
'ds': round(s.std(), 2),
'cv': round(s.std() / s.mean(), 3) if s.mean() else np.nan,
'asimetria': round(stats.skew(s), 3),
'curtosis': round(stats.kurtosis(s), 3),
'min': round(s.min(), 2),
'max': round(s.max(), 2),
})
return pd.DataFrame(filas)
print(ficha(ventas, NUMERICAS).to_string(index=False))
columna n nulos_% media mediana ds cv asimetria curtosis min max
unidades 3000 0.00 11.60 12.00 5.77 0.498 0.184 -0.337 1.00 30.00
monto 3000 0.00 803.76 533.63 751.99 0.936 1.340 1.541 -2497.72 4236.71
descuento 2405 19.83 0.13 0.13 0.07 0.573 -0.016 -1.160 0.00 0.25
satisfaccion 2769 7.70 3.01 3.00 1.42 0.472 -0.015 -1.309 1.00 5.00
precio_unitario 3000 0.00 121.28 53.02 255.15 2.104 6.734 60.015 -599.90 3586.26
monto_final_facturado 3000 0.00 481.26 191.89 678.86 1.411 1.764 2.776 0.00 4171.70
Esa tabla ya cuenta media docena de cosas. Vamos por partes 🧵
Los cuatro números que de verdad se miran
Media contra mediana. Si se parecen, la columna es
simétrica. Si la media es mucho mayor, hay cola por la derecha:
monto tiene media 803,76 y mediana 533,63, o sea que la mitad de
las ventas está por debajo de 534 y unas pocas grandes tiran del promedio.
El coeficiente de variación, que es la desviación dividida entre la media.
la desviación dividida entre la media, que sirve para comparar cuánto varían dos columnas medidas en unidades distintas
Sirve para lo que la desviación sola no puede: comparar columnas medidas en
unidades distintas. unidades tiene CV 0,498 y
precio_unitario tiene 2,104, o sea que el precio por unidad varía
cuatro veces más en términos relativos, aunque los dos números
crudos no se puedan comparar.
Asimetría y curtosis, que en una campana perfecta valen cero las dos.
la primera dice hacia qué lado se estira la cola y la segunda cuánto pesan los extremos, y las dos valen cero en una campana perfecta
Y aquí salta la que se lleva el premio: precio_unitario, con
asimetría 6,734 y curtosis 60,015. Eso no es una cola larga, eso es un cohete
🚀
Una columna así rompe cualquier modelo lineal si entra tal cual, y encima el escalado estándar no la arregla: restarle la media y dividir por la desviación no cambia la forma, solo la mueve de sitio.
El error que se comete recorriendo columnas
Lo natural cuando quieres la ficha de todo es meter un bucle sobre
ventas.columns. No acaba bien.
stats.shapiro(ventas['segmento'])
ValueError: could not convert string to float: 'Horeca'
Por eso la función de arriba recibe una lista de columnas en vez de recorrerlas todas. La lista sale de la auditoría de tipos del capítulo 5, que es exactamente para lo que servía 🗂️
Y ojo con la otra versión del mismo problema, que es peor porque no da error:
si le pasas una columna con nulos, shapiro devuelve
nan tan tranquilo. De ahí el dropna() dentro de la
ficha.
Normalidad: la prueba contesta otra pregunta
Todo el mundo aprende a preguntar si una columna es normal. Preguntémoslo.
for c in NUMERICAS:
s = ventas[c].dropna()
w, p = stats.shapiro(s.sample(min(4000, len(s)), random_state=7))
ad = stats.anderson(s, dist='norm')
print(f'{c:22s} shapiro W {w:.4f} p {p:.3e} anderson {ad.statistic:8.2f} '
f'(critico al 5% {ad.critical_values[2]:.3f})')
unidades shapiro W 0.9869 p 5.181e-16 anderson 6.57 (critico al 5% 0.752) monto shapiro W 0.8378 p 4.460e-48 anderson 175.97 (critico al 5% 0.752) descuento shapiro W 0.9580 p 8.410e-26 anderson 23.61 (critico al 5% 0.752) satisfaccion shapiro W 0.8866 p 4.297e-41 anderson 100.52 (critico al 5% 0.752) precio_unitario shapiro W 0.4011 p 6.850e-72 anderson 502.35 (critico al 5% 0.752) monto_final_facturado shapiro W 0.7424 p 5.966e-56 anderson 274.05 (critico al 5% 0.752)
Las seis rechazadas, y no por poco: la más suave sale con p de 5,18e-16 😅
Aquí es donde casi todo el mundo concluye "es que con muchos datos la prueba rechaza siempre". Yo también lo pensaba, así que lo medí.
rng = np.random.default_rng(7)
print('datos normales de verdad:')
for n in (30, 100, 1000, 3000, 20000):
p = stats.shapiro(rng.normal(0, 1, n)).pvalue
print(f' n {n:6d} p {p:.4f} {"pasa" if p > 0.05 else "RECHAZA"}')
datos normales de verdad: n 30 p 0.5097 pasa n 100 p 0.8481 pasa n 1000 p 0.4186 pasa n 3000 p 0.4519 pasa n 20000 p 0.5194 pasa
Pues no. Los datos normales pasan la prueba con 30 y con 20.000 🤨
Así que la conclusión honesta es la incómoda: estas seis columnas sencillamente no son normales. No es un artefacto del tamaño.
Donde sí manda el tamaño es en las desviaciones chicas. Aquí una mezcla que es 95% normal y 5% de algo con más dispersión, veinte veces por cada tamaño:
print('95% normal y 5% de otra cosa, 20 intentos por tamano:')
for n in (50, 200, 1000, 3000, 10000):
ps = []
for semilla in range(20):
g = np.random.default_rng(semilla)
mezcla = np.where(g.random(n) < 0.95, g.normal(0, 1, n), g.normal(0, 3, n))
ps.append(stats.shapiro(mezcla).pvalue)
ps = np.array(ps)
print(f' n {n:6d} p mediana {np.median(ps):.2e} rechaza en '
f'{int((ps < 0.05).sum())}/20')
95% normal y 5% de otra cosa, 20 intentos por tamano: n 50 p mediana 7.05e-02 rechaza en 10/20 n 200 p mediana 4.37e-05 rechaza en 19/20 n 1000 p mediana 3.36e-14 rechaza en 20/20 n 3000 p mediana 6.17e-27 rechaza en 20/20 n 10000 p mediana 1.25e-45 rechaza en 20/20
Ahí está el efecto del tamaño, dicho bien: con 50 filas esa desviación se cuela la mitad de las veces y con 1.000 no se le escapa ninguna.
O sea que la prueba no rechaza de más con muchos datos: rechaza de menos con pocos. Es lo mismo visto al revés y cambia la conclusión entera.
Y aun así yo casi nunca uso la prueba, por una razón práctica: contesta "¿es exactamente normal?" cuando la pregunta útil es "¿cuánto se desvía?". Eso lo contestan la asimetría y la curtosis, que además te dicen hacia dónde 📐
Cuántos atípicos hay, según a quién preguntes
Tres formas de contarlos, las tres estándar.
for c in NUMERICAS:
s = ventas[c].dropna()
q1, q3 = s.quantile([0.25, 0.75])
ri = q3 - q1
tukey = ((s < q1 - 1.5 * ri) | (s > q3 + 1.5 * ri)).sum()
z = (np.abs(stats.zscore(s)) > 3).sum()
mad = stats.median_abs_deviation(s)
robusto = (np.abs(0.6745 * (s - s.median()) / mad) > 3.5).sum() if mad else 0
print(f'{c:22s} tukey {tukey:5d} z>3 {z:5d} z robusto {robusto:5d} '
f'de {len(s)}')
unidades tukey 10 z>3 7 z robusto 0 de 3000 monto tukey 203 z>3 32 z robusto 222 de 3000 descuento tukey 0 z>3 0 z robusto 0 de 2405 satisfaccion tukey 0 z>3 0 z robusto 0 de 2769 precio_unitario tukey 277 z>3 54 z robusto 316 de 3000 monto_final_facturado tukey 270 z>3 50 z robusto 434 de 3000
Mira monto: 203, 32 y 222. Siete veces más según el método 😬
Y no es que uno esté mal, es que miden cosas distintas. El z clásico usa la media y la desviación, que los propios atípicos inflan, así que se le esconden. El z robusto usa la mediana y la MAD, que no se dejan mover, y por eso encuentra más.
La consecuencia práctica es dura de aceptar: no existe "cuántos atípicos tiene esta columna". Existe cuántos encuentra el método que elegiste, y ese método hay que decirlo cuando se reporta el número.
Fíjate además en las dos que salen en cero por los tres caminos:
descuento y satisfaccion. Cuando los tres métodos
coinciden, ahí sí se puede afirmar algo 👍
Lo que ninguna ficha resume
for c in ('monto', 'precio_unitario', 'monto_final_facturado'):
s = ventas[c]
print(f'{c:22s} negativos {int((s < 0).sum()):4d} '
f'ceros {int((s == 0).sum()):5d}')
monto negativos 21 ceros 0 precio_unitario negativos 21 ceros 0 monto_final_facturado negativos 0 ceros 1267
Veintiún montos negativos. Son las devoluciones del capítulo 4, y aparecen aquí otra vez porque el mínimo de la ficha ya lo cantaba: −2497,72.
Y monto_final_facturado con 1.267 ceros, que es el 42,23% de las
ventas. Esa columna se delata sola sin cruzarla con nada:
print(ventas['monto_final_facturado'].describe().round(2).to_string())
print()
print('valen exactamente 0:', int((ventas['monto_final_facturado'] == 0).sum()),
f"({100 * (ventas['monto_final_facturado'] == 0).mean():.2f}%)")
count 3000.00 mean 481.26 std 678.86 min 0.00 25% 0.00 50% 191.89 75% 668.36 max 4171.70 valen exactamente 0: 1267 (42.23%)
El primer cuartil vale 0 y la mediana 191,89. Una columna de dinero donde el 25% inferior es exactamente cero no es una columna de dinero: es dos cosas metidas en una, y una de ellas es "no pasó nada" 🚩
Todavía no sabemos que es una fuga (eso es el capítulo 12), pero ya sabemos que hay que preguntar por ella. Y eso lo dio el análisis de una sola columna.
Las categóricas, que aquí no dan guerra
for c in ('ciudad', 'segmento', 'canal', 'categoria'):
cuenta = ventas[c].value_counts(dropna=False)
print(f'{c:12s} niveles {len(cuenta):3d} '
f'el mas comun {cuenta.index[0]!r} {100 * cuenta.iloc[0] / len(ventas):5.2f}% '
f'el mas raro {cuenta.index[-1]!r} {100 * cuenta.iloc[-1] / len(ventas):5.2f}%')
ciudad niveles 6 el mas comun 'arequipa' 18.20% el mas raro 'lima' 15.70% segmento niveles 4 el mas comun 'Minimarket' 26.70% el mas raro 'Bodega' 23.57% canal niveles 4 el mas comun 'Web' 26.40% el mas raro 'WhatsApp' 23.97% categoria niveles 5 el mas comun 'Abarrotes' 21.33% el mas raro 'Bebidas' 18.83%
Todas repartidas entre el 15,70% y el 26,70%, y ninguna pasa de seis niveles. Aburrido, y lo digo como un elogio 😌
Lo aburrido aquí significa que no hay que hacer nada: sin categorías raras no hay que agrupar niveles, y con seis niveles el one-hot no infla la tabla.
Cuando esta tabla sale fea (una categoría con el 95%, o cuarenta niveles con tres filas cada uno) es cuando empiezan las decisiones difíciles, y esas están en el capítulo 5.
Ejercicios
1. La ficha completa, con recomendación
Amplía la ficha para que además diga qué hacer con cada columna según su forma.
def recomienda(fila):
if abs(fila['asimetria']) > 2:
return 'muy sesgada: log o tramos'
if abs(fila['asimetria']) > 0.8:
return 'sesgada: probar log'
if fila['cv'] > 1:
return 'varia mucho: escalar seguro'
return 'escalar y listo'
tabla = ficha(ventas, NUMERICAS)
tabla['que_hacer'] = tabla.apply(recomienda, axis=1)
print(tabla[['columna', 'asimetria', 'cv', 'que_hacer']].to_string(index=False))
columna asimetria cv que_hacer
unidades 0.184 0.498 escalar y listo
monto 1.340 0.936 sesgada: probar log
descuento -0.016 0.573 escalar y listo
satisfaccion -0.015 0.472 escalar y listo
precio_unitario 6.734 2.104 muy sesgada: log o tramos
monto_final_facturado 1.764 1.411 sesgada: probar log
Esos cortes en 0,8 y en 2 son convenciones, no leyes de la naturaleza. Sirven para ordenar la conversación, no para decidir solas 📋
Lo valioso de la tabla es lo mismo que en el capítulo 5: cada fila lleva una decisión al lado, y esa decisión queda escrita antes de que nadie la pregunte en una reunión.
2. El logaritmo, a ver si arregla el cohete
La receta clásica para una columna sesgada es el
logaritmo. Compruébalo con precio_unitario, que tenía asimetría
6,734.
s = ventas['precio_unitario']
positivos = s[s > 0]
print('original asimetria', round(stats.skew(positivos), 3),
' curtosis', round(stats.kurtosis(positivos), 3))
print('log asimetria', round(stats.skew(np.log(positivos)), 3),
' curtosis', round(stats.kurtosis(np.log(positivos)), 3))
print('raiz cuadrada asimetria', round(stats.skew(np.sqrt(positivos)), 3),
' curtosis', round(stats.kurtosis(np.sqrt(positivos)), 3))
print()
print('filas que el log deja fuera por no ser positivas:', int((s <= 0).sum()))
original asimetria 6.766 curtosis 60.161 log asimetria 0.271 curtosis 0.272 raiz cuadrada asimetria 2.807 curtosis 12.177 filas que el log deja fuera por no ser positivas: 21
El logaritmo hace su trabajo y de sobra. Pero mira la última línea: deja fuera 21 filas, las de los montos negativos.
Ese es el precio que nadie menciona. Si transformas con log tienes que decidir qué haces con los no positivos, y "no hacer nada" significa perderlos 🕳️
3. Cuánto cambia la ficha si quitas los atípicos
Antes de borrar un atípico conviene ver cuánto de la columna se lleva por delante.
s = ventas['monto']
q1, q3 = s.quantile([0.25, 0.75])
ri = q3 - q1
dentro = s[(s >= q1 - 1.5 * ri) & (s <= q3 + 1.5 * ri)]
print(f'{"":12s} {"n":>6s} {"media":>10s} {"mediana":>10s} {"ds":>10s} {"asim":>8s}')
for nombre, datos in (('con todo', s), ('sin atipicos', dentro)):
print(f'{nombre:12s} {len(datos):6d} {datos.mean():10.2f} '
f'{datos.median():10.2f} {datos.std():10.2f} {stats.skew(datos):8.3f}')
print()
print('soles que se van:', round(float(s.sum() - dentro.sum()), 2))
print('porcentaje del dinero total:',
round(100 * (s.sum() - dentro.sum()) / s.sum(), 2), '%')
n media mediana ds asim con todo 3000 803.76 533.63 751.99 1.340 sin atipicos 2797 670.93 494.47 558.71 1.214 soles que se van: 534679.47 porcentaje del dinero total: 22.17 %
Ahí está el argumento de verdad contra borrar atípicos por sistema 💸
Son 203 filas de 3.000, o sea el 6,77%. Y se llevan 534.679 soles, que es el 22,17% de todo el dinero del archivo.
Borrar el 6,77% de las filas para perder el 22,17% de la facturación deja una columna más bonita y un negocio que ya no es el tuyo. Y fíjate en que la asimetría apenas baja, de 1,340 a 1,214: ni siquiera arregla lo que se supone que venía a arreglar.
4. La misma columna, mirada por trozos
Una ficha global puede esconder dos poblaciones. Saca la
ficha de monto dentro de cada segmento.
por_segmento = ventas.groupby('segmento')['monto'].agg(
n='count', media='mean', mediana='median', ds='std',
asimetria=lambda s: stats.skew(s))
por_segmento['cv'] = por_segmento['ds'] / por_segmento['media']
print(por_segmento.round(3).to_string())
print()
print('global: media', round(ventas['monto'].mean(), 2),
'asimetria', round(stats.skew(ventas['monto']), 3))
n media mediana ds asimetria cv segmento Bodega 707 178.698 183.260 69.587 -0.774 0.389 Horeca 742 755.179 742.995 277.276 -0.427 0.367 Mayorista 750 1856.337 1869.255 719.551 -0.455 0.388 Minimarket 801 414.917 413.740 148.680 -0.678 0.358 global: media 803.76 asimetria 1.34
Esto es lo mismo que salió en el libro de estadística con la media y la mediana, y aquí sale todavía más fuerte: la forma global no es la forma de nadie 🎭
La asimetría global es +1,340 y dentro de los cuatro segmentos es negativa en los cuatro: −0,774, −0,427, −0,455 y −0,678. No es que se reduzca, es que cambia de signo.
Traducido: no tienes una columna con cola a la derecha. Tienes cuatro poblaciones muy distintas (178 soles de media en bodega, 1.856 en mayorista) y la cola de la derecha son los mayoristas enteros.
Eso no se arregla con un logaritmo. Se arregla con la columna que las
distingue, que ya la tienes y se llama segmento 🏷️
5. Los valores repetidos, que delatan cosas
Cuando un valor concreto se repite mucho en una columna continua, casi siempre es un relleno o un tope.
for c in NUMERICAS:
cuenta = ventas[c].value_counts(dropna=True)
if cuenta.empty:
continue
valor, veces = cuenta.index[0], cuenta.iloc[0]
print(f'{c:22s} el valor mas repetido es {valor:10.2f} y sale {veces:5d} veces '
f'({100 * veces / ventas[c].notna().sum():5.2f}%)')
unidades el valor mas repetido es 13.00 y sale 208 veces ( 6.93%) monto el valor mas repetido es 204.47 y sale 3 veces ( 0.10%) descuento el valor mas repetido es 0.05 y sale 20 veces ( 0.83%) satisfaccion el valor mas repetido es 5.00 y sale 563 veces (20.33%) precio_unitario el valor mas repetido es 185.03 y sale 2 veces ( 0.07%) monto_final_facturado el valor mas repetido es 0.00 y sale 1267 veces (42.23%)
Una columna continua de verdad casi no repite valores. Cuando uno se lleva un porcentaje gordo, la pregunta es de dónde sale ese número 🔍
Aquí lo canta monto_final_facturado con su cero. Y ojo con
unidades y satisfaccion, que repiten mucho por otra
razón perfectamente sana: son discretas y tienen pocos valores posibles.
6. Cuánto pesa lo que falta
Los nulos ya salieron en el capítulo 4. Aquí la pregunta es otra: ¿faltan juntos?
banderas = ventas[['descuento', 'satisfaccion']].isna()
banderas.columns = ['falta_descuento', 'falta_satisfaccion']
print(pd.crosstab(banderas['falta_descuento'],
banderas['falta_satisfaccion']).to_string())
print()
esperado = banderas['falta_descuento'].mean() * banderas['falta_satisfaccion'].mean()
juntos = (banderas['falta_descuento'] & banderas['falta_satisfaccion']).mean()
print('si faltaran independientes, coincidirian el', round(100 * esperado, 2), '%')
print('coinciden de verdad el ', round(100 * juntos, 2), '%')
falta_satisfaccion False True falta_descuento False 2209 196 True 560 35 si faltaran independientes, coincidirian el 1.53 % coinciden de verdad el 1.17 %
Aquí la respuesta es que no: si faltaran independientes coincidirían el 1,53% de las filas y coinciden el 1,17%. Prácticamente lo mismo, y si acaso un poco menos 🤷♀️
O sea que son dos huecos con dos causas distintas, y hay que tratarlos por separado. Aburrido otra vez, y otra vez es la respuesta que quieres.
La comprobación importa por el caso contrario. Si dos columnas faltaran juntas mucho más de lo que toca por azar, no faltarían por descuido: faltarían por una razón, normalmente un proceso del negocio que no se ejecutó, y entonces son un solo problema disfrazado de dos 🕳️
7. La ficha como control de calidad automático
Convierte todo esto en una función que devuelve avisos, no números. Es lo que se corre cada vez que llega un archivo nuevo.
def avisos(v, columnas):
fuera = []
for c in columnas:
s = v[c].dropna()
if v[c].isna().mean() > 0.15:
fuera.append(f'{c}: falta el {100 * v[c].isna().mean():.1f}%')
if abs(stats.skew(s)) > 2:
fuera.append(f'{c}: asimetria {stats.skew(s):.2f}, muy sesgada')
if (s < 0).any():
fuera.append(f'{c}: {int((s < 0).sum())} valores negativos')
repetido = s.value_counts()
if not repetido.empty and repetido.iloc[0] / len(s) > 0.3:
fuera.append(f'{c}: el valor {repetido.index[0]} se lleva el '
f'{100 * repetido.iloc[0] / len(s):.1f}%')
return fuera
for aviso in avisos(ventas, NUMERICAS):
print(' ', aviso)
monto: 21 valores negativos descuento: falta el 19.8% precio_unitario: asimetria 6.73, muy sesgada precio_unitario: 21 valores negativos monto_final_facturado: el valor 0.0 se lleva el 42.2%
Seis avisos y ninguno es un error: son seis preguntas que hay que hacerle a quien manda los datos antes de modelar 📋
Esta función es la hermana pequeña de la revisa del capítulo
23. Aquella vigila que el archivo de mañana se parezca
al de hoy; esta vigila que el de hoy tenga sentido.
Comprueba que lo tienes
La prueba de normalidad rechaza tus seis columnas numéricas con p por debajo de 1e-15. ¿Qué haces?
- Mirar la asimetría y la curtosis, que dicen cuánto se desvía
- Transformar las seis con logaritmo para normalizarlas
- Nada: casi ningún modelo del libro pide normalidad
- Quitar los atípicos hasta que la prueba pase
Lo que te llevas
- 📋 Cuatro números por columna: media contra mediana, coeficiente de variación, asimetría y curtosis. Con eso ya se decide qué escalar y qué transformar.
- 🚀
precio_unitariotiene asimetría 6,734 y curtosis 60,015. El escalado estándar no arregla eso: mueve la columna de sitio pero no le cambia la forma. - 📏 El CV compara columnas con unidades distintas: 0,498 en
unidadescontra 2,104 enprecio_unitario. - 🧪 Las seis columnas fallan la prueba de normalidad, y no es cosa del tamaño: los datos normales de verdad la pasan con 30 filas y con 20.000.
- 🔬 Donde sí manda el tamaño es al revés: una desviación chica se cuela la mitad de las veces con 50 filas y no se le escapa ninguna con 1.000. La prueba no rechaza de más con muchos datos, rechaza de menos con pocos.
- 😬 Los atípicos de
montoson 203, 32 o 222 según el método. No existe "cuántos atípicos tiene", existe cuántos encuentra el que elegiste. - 🚩
monto_final_facturadose delata sin cruzarla con nada: su primer cuartil vale 0 y el 42,23% de las filas también. - 💸 Borrar los atípicos de
montoquita el 6,77% de las filas y mucho más del dinero, porque son justo las ventas grandes. - 😌 Las cuatro categóricas están repartidas entre el 15,70% y el 26,70%, sin niveles raros. Aburrido, y aquí eso es una buena noticia.
Qué viene ahora
Cada columna por separado ya no tiene secretos. Falta la pregunta que de verdad importa: ¿cuál de ellas tiene que ver con lo que queremos predecir? 🎯
El capítulo 7 cruza cada columna con el objetivo, y no a ojo: con la prueba que corresponde a cada tipo y con el tamaño del efecto al lado, que es lo que separa "hay diferencia" de "la diferencia importa".
Lo que verás allí:
- 🧮 Qué prueba toca según el tipo de columna, en una tabla que se puede pegar en la pared.
- 📐 Por qué el valor p solo no sirve, y qué se pone al lado.
- 🎣 Las columnas que parecen decir algo y no dicen nada, con el ajuste por comparaciones múltiples.
- 🪤 Y una columna con doce puntos de diferencia que al modelo no le va a aportar nada, que es la trampa del capítulo 19 vista desde el otro lado.