Capítulo 9 de 29 12 secciones 17 min

Compartir

Construir columnas

Cinco columnas nuevas: tres que valen puntos enteros, una que no sirve y una que es una trampa con el 30% de los datos.

Esta es mi parte favorita del oficio y la que menos se enseña: cambiar de modelo da décimas, construir una buena columna da puntos enteros. Lo más rentable son las banderas de hueco, que aquí valen veinte, doce y cuatro puntos y medio. Y lo más peligroso también vive aquí, porque la columna que mejor se ve suele ser la que te está copiando la respuesta 🧱

Aquí es donde de verdad se gana 🔧

Antes de empezar, piensa en tu trabajo un segundo: ¿qué sabes tú de tu negocio que no esté escrito en ninguna columna? Eso que sabes es exactamente lo que vamos a convertir en dato 💡

Cambiar de modelo te da décimas. Construir una columna buena te da puntos enteros. Y en el capítulo 4 ya encontramos dos que valen oro sin haberlas escrito todavía: los huecos.

Este capítulo va de eso: convertir lo que hay en cosas que un modelo pueda usar. En inglés se llama feature engineering, que es como lo vas a ver escrito en todas partes, y en español ingeniería de características. Yo le digo construir columnas porque es literalmente lo que se hace. Y también de lo contrario, porque vamos a construir cinco columnas y solo tres van a servir. Enseñar las que no sirven es la mitad del oficio.

De dónde salen las columnas nuevas: de lo que ya hay en el archivo salen las banderas de hueco y los ratios y tramos, y a eso se le suma lo que la persona sabe del negocio, que es lo que da puntos enteros en vez de décimas.
La caja de abajo a la derecha es la que no se puede automatizar. Cambiar de modelo da décimas; saber que un hueco en el descuento significa algo da puntos.

El punto de partida

Cada capítulo de este libro arranca solo, así que empezamos por la función de limpieza del capítulo 4.

import pandas as pd

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

def carga_limpia(url):
    """Lo del capítulo 4, en una función."""
    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)
print(ventas.shape)
(3000, 14)

Columna 1: el hueco convertido en dato

En el capítulo 4 vimos que sin fecha de última compra se cierra el 41,4% y con fecha el 61,4%. Esa señal está ahí y el modelo no la puede ver, porque un nulo para scikit-learn es un problema, no un dato.

La solución es de una línea y es la técnica más rentable de todo el capítulo:

ventas['sin_compra_previa'] = ventas['fecha_ultima_compra'].isna().astype(int)
ventas['sin_descuento'] = ventas['descuento'].isna().astype(int)

print(ventas.groupby('sin_compra_previa')['compro'].agg(['count', 'mean']).round(4))
                   count    mean
sin_compra_previa               
0                   2456  0.6140
1                    544  0.4136

Ahí está, ahora sí en una columna de ceros y unos que cualquier modelo entiende. Veinte puntos de diferencia guardados 💎

La regla: cuando un hueco significa algo, se marca antes de rellenarlo. Primero la bandera, después el relleno. Así el modelo tiene las dos cosas: un valor con el que trabajar y la información de que ese valor era inventado.

Columna 2: el precio por unidad

Ni el monto ni las unidades por separado decían gran cosa. Su cociente sí.

ventas['precio_unitario'] = ventas['monto'] / ventas['unidades']
print(ventas.groupby('compro')['precio_unitario'].median().round(2))
compro
0    39.27
1    61.72
Name: precio_unitario, dtype: float64

Mediana de S/39 en las que no se cerraron y S/62 en las que sí. Y en tramos se ve todavía mejor:

tramo = pd.qcut(ventas['precio_unitario'], 4)
print(ventas.groupby(tramo, observed=True)['compro'].agg(['count', 'mean']).round(4))
                    count    mean
precio_unitario                  
(-599.901, 23.941]    750  0.4400
(23.941, 53.024]      750  0.5667
(53.024, 121.764]     750  0.6107
(121.764, 3586.26]    750  0.6933

De 44% en el cuarto más barato a 69% en el más caro. Veinticinco puntos, y esa columna no existía hace dos líneas 🌟

Esto se llama ratio y es de las cosas que más funcionan: dividir dos columnas que ya tienes. Piensa siempre en "por unidad", "por día", "porcentaje del total". Un modelo lineal no puede inventarse una división, así que si la necesita hay que dársela hecha.

Columna 3: el monto en tramos

tramo_monto = pd.qcut(ventas['monto'], 4)
print(ventas.groupby(tramo_monto, observed=True)['compro'].agg(['count', 'mean']).round(4))
                     count    mean
monto                             
(-2497.721, 252.69]    750  0.3960
(252.69, 533.63]       750  0.5680
(533.63, 1068.685]     750  0.6373
(1068.685, 4236.71]    750  0.7093

De 39,6% a 70,9%, y en orden. Eso es señal de verdad y encima monótona: cuanto más grande el monto, más se cierra.

Cuando la relación es así de ordenada, la columna original ya sirve tal cual y no hace falta partirla en tramos. Los tramos son útiles cuando la relación no es ordenada: por ejemplo si se cerrara mucho abajo y arriba y poco en el medio, ahí una columna numérica no lo puede expresar y los tramos sí 📊

Columna 4: la que no sirve

Ahora una que suena obvia y no funciona.

ventas['dia_semana'] = ventas['fecha'].dt.dayofweek
print(ventas.groupby('dia_semana')['compro'].agg(['count', 'mean']).round(4))
            count    mean
dia_semana               
0             415  0.5952
1             473  0.5687
2             431  0.5963
3             418  0.5622
4             421  0.5701
5             420  0.5929
6             422  0.5592

Todos entre 0,559 y 0,596. Cuatro puntos entre el mejor y el peor día, con unas 420 filas cada uno. Eso es ruido.

Y aquí va algo importante: esto también es un resultado. Si alguien en la reunión dice "los martes se vende peor", ya tienes la respuesta medida en vez de una opinión.

La columna se puede dejar (un buen modelo la ignora sola) o quitar. Yo la quito, porque cada columna inútil es una oportunidad más de que el modelo memorice ruido 🧹

Columna 5: la trampa

Esta parece la mejor idea del capítulo. Tenemos la fecha de la venta y la de la última compra, así que la antigüedad del cliente sale sola:

ventas['dias_desde_ultima'] = (ventas['fecha'] - ventas['fecha_ultima_compra']).dt.days
print(ventas['dias_desde_ultima'].describe().round(1))
count    2456.0
mean       72.8
std       190.3
min      -385.0
25%       -65.0
50%        69.0
75%       213.2
max       522.0
Name: dias_desde_ultima, dtype: float64

Mira el mínimo: -385 😳

negativos = ventas['dias_desde_ultima'] < 0
print('cuántos:', int(negativos.sum()), f'({negativos.mean():.1%})')
print(ventas.loc[negativos, ['fecha', 'fecha_ultima_compra', 'dias_desde_ultima']].head(3).to_string(index=False))
cuántos: 904 (30.1%)
     fecha fecha_ultima_compra  dias_desde_ultima
2025-06-05          2025-08-08              -64.0
2025-02-02          2026-01-18             -350.0
2025-06-29          2025-10-10             -103.0

904 filas, el 30% del archivo, donde la "última compra" es posterior a la venta. O sea que esa columna no dice "la última compra antes de esta": dice la última compra del cliente en todo el archivo, futuro incluido.

Un modelo entrenado con eso aprende de compras que en el momento de predecir todavía no habían pasado. Es la misma fuga del capítulo 12, y esta versión es mucho peor que la del monto_final_facturado porque no salta a la vista: la columna tiene un nombre inocente y valores razonables 🚨

La versión bien hecha, y lo que pasa

Se arregla calculando la fecha anterior de cada cliente ordenando por fecha:

ventas = ventas.sort_values(['cliente_id', 'fecha'])
fecha_anterior = ventas.groupby('cliente_id')['fecha'].shift(1)
ventas['dias_reales'] = (ventas['fecha'] - fecha_anterior).dt.days

print('negativos ahora:', int((ventas['dias_reales'] < 0).sum()))
print('nulos (primera venta del cliente):', int(ventas['dias_reales'].isna().sum()))
negativos ahora: 0
nulos (primera venta del cliente): 617

Cero negativos, y 617 nulos que son exactamente las primeras ventas de cada uno de los 617 clientes. Eso ya es correcto: solo mira hacia atrás.

¿Y sirve?

tramo_dias = pd.cut(ventas['dias_reales'], [-1, 30, 90, 180, 10000],
                    labels=['0-30', '31-90', '91-180', '+180'])
print(ventas.groupby(tramo_dias, observed=True)['compro'].agg(['count', 'mean']).round(4))
             count    mean
dias_reales               
0-30           736  0.5774
31-90          853  0.5803
91-180         515  0.5748
+180           279  0.5591

Todos alrededor de 0,57. No sirve de nada 😐

Y esa es la parte honesta del capítulo. Hicimos el trabajo bien, arreglamos la fuga, y la columna correcta no tiene señal. Pasa muchísimo y hay que contarlo, porque si solo te enseñan los casos donde todo sale bonito, el día que te salga esto vas a pensar que lo hiciste mal.

Tres columnas más que probé, y las tres son inútiles

Esta sección iba a ser "tres ideas más de columnas". La escribo distinta porque las medí y no funcionó ninguna, y eso enseña más 🤷‍♀️

Las tres son las que sugiere cualquier tutorial: desmontar la fecha, marcar el fin de semana, y comparar cada venta con lo normal de ese cliente.

v = ventas.drop_duplicates(subset='id_venta').sort_values(['cliente_id', 'fecha']).copy()
v['monto'] = pd.to_numeric(v['monto'].astype(str).str.replace(',', '.'), errors='coerce')
v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)
v['precio_unitario'] = v['monto'] / v['unidades']

v['dia_semana'] = pd.to_datetime(v['fecha'], format='mixed').dt.dayofweek
v['fin_de_semana'] = (v['dia_semana'] >= 5).astype(int)
v['ticket_vs_cliente'] = v['monto'] / v.groupby('cliente_id')['monto'].transform('median')

print(v.groupby('dia_semana')['compro'].agg(['count', 'mean']).round(3))
            count   mean
dia_semana              
0             415  0.595
1             473  0.569
2             431  0.596
3             418  0.562
4             421  0.570
5             420  0.593
6             422  0.559

Mira la columna de la derecha antes de seguir: de 0,559 a 0,596 🫥

Los siete días de la semana compran prácticamente igual. No hay un lunes flojo ni un viernes fuerte. Y eso, que parece un no-resultado, ya te dice que esa columna no va a servir para nada.

Aun así la mido, porque mirar la tabla no es lo mismo que medir el efecto en el modelo:

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

BASE = ['unidades', 'monto', 'descuento', 'satisfaccion',
        'precio_unitario', 'sin_compra_previa']
CAT = ['ciudad', 'segmento', 'canal', 'categoria']


def arma2(num, cat):
    return Pipeline([
        ('p', ColumnTransformer([
            ('n', Pipeline([('i', SimpleImputer(strategy='median')),
                            ('s', StandardScaler())]), num),
            ('c', Pipeline([('i', SimpleImputer(strategy='most_frequent')),
                            ('o', OneHotEncoder(handle_unknown='ignore'))]), cat),
        ])),
        ('m', LogisticRegression(max_iter=1000, random_state=42)),
    ])


def prueba(num, etiqueta):
    X = v[num + CAT]
    Xtr, Xte, ytr, yte = train_test_split(X, v['compro'], test_size=0.25,
                                          random_state=42, stratify=v['compro'])
    m = arma2(num, CAT).fit(Xtr, ytr)
    auc = roc_auc_score(yte, m.predict_proba(Xte)[:, 1])
    print(f'{etiqueta:<34} {auc:.4f}')
    return auc


base = prueba(BASE, 'solo las de siempre')
prueba(BASE + ['dia_semana'], '+ día de la semana')
prueba(BASE + ['fin_de_semana'], '+ fin de semana')
ratio = prueba(BASE + ['ticket_vs_cliente'], '+ ticket contra su propia mediana')

print()
print('lo que aporta el ratio:', round(ratio - base, 4))
solo las de siempre                0.7218
+ día de la semana                 0.7215
+ fin de semana                    0.7212
+ ticket contra su propia mediana  0.7219

lo que aporta el ratio: 0.0

0,7218 sin ellas. Y con ellas: 0,7215, 0,7212 y 0,7219 😐

La tercera es la que más ilusión me hacía. "Esta venta es el doble de lo que suele comprar este cliente" suena a señal de verdad, y aporta una diezmilésima, que es lo mismo que decir nada.

Y aquí está la lección incómoda de este capítulo, que va contra lo que promete casi todo lo que vas a leer sobre construir columnas 💊

Lo que de verdad pasa cuando construyes columnas

La mayoría no aporta. En este capítulo llevamos ocho columnas construidas y solo dos movieron el número. Esa proporción no es mala suerte: es lo normal, y quien te cuente lo contrario te está enseñando el caso que le salió bien.

Una columna que no aporta no es gratis. Ocupa sitio, hay que calcularla también en producción, y añade un motivo más de que algo se rompa el día que cambie el archivo. Si no gana nada, se quita.

La forma de saberlo es medir, y medir así. Con el mismo reparto de entrenamiento y prueba, la misma semilla y el mismo modelo, cambiando solo la columna. Cualquier otra cosa compara dos experimentos distintos.

Y una advertencia sobre estos números tan parecidos: una diferencia de tres diezmilésimas no es una diferencia. Con otro random_state se dan la vuelta. Para decidir de verdad entre dos columnas que se parecen tanto hace falta validación cruzada, que es el capítulo 16 🎲

Entonces, ¿por qué probarlas?

Porque probar es barato y no probar sale caro 💡

Las tres de arriba costaron seis líneas y cuatro minutos. La alternativa es decidir por intuición cuál va a funcionar, y la intuición aquí acierta poco: yo habría apostado por el ratio y me habría equivocado.

Lo caro no es probar diez columnas y tirar ocho. Lo caro es quedarte con las diez porque nunca mediste cuáles servían, y arrastrarlas a producción para siempre 🎒

El detalle que quedó abierto

Fíjate en algo raro, porque es una pregunta de verdad y no la voy a tapar.

La bandera sin_compra_previa (que la fecha venga vacía) vale veinte puntos. Pero "es la primera venta de este cliente en el archivo" no vale nada:

primera = ventas['dias_reales'].isna()
print('primera venta del cliente:', round(ventas.loc[primera, 'compro'].mean(), 4))
print('el resto:                 ', round(ventas.loc[~primera, 'compro'].mean(), 4))
primera venta del cliente: 0.5851
el resto:                  0.5757

0,5851 contra 0,5757. Nada.

O sea que el hueco en fecha_ultima_compra no significa "cliente nuevo", significa otra cosa que este archivo no me deja averiguar. A lo mejor es "el sistema viejo no lo tenía", a lo mejor es "no se pudo verificar".

Eso no se resuelve con código: se resuelve con la pregunta del capítulo 2, la que va en el contrato de columnas. ¿Qué significa un hueco aquí?

Y mientras no haya respuesta, la bandera se usa (funciona) pero se reporta con la advertencia puesta. Un modelo que se apoya en algo que nadie sabe explicar es un modelo frágil 🤔

Ejercicios

Siete. Intenta antes de abrir 💛

1. El mes del año

¿Hay estacionalidad en la tasa de cierre?

ventas['mes'] = ventas['fecha'].dt.month
print(ventas.groupby('mes')['compro'].mean().round(4))
mes
1     0.5782
2     0.6176
3     0.5710
4     0.5697
5     0.5479
6     0.5856
7     0.5671
8     0.6090
9     0.5531
10    0.5635
11    0.5989
12    0.5849
Name: compro, dtype: float64

De 0,548 en mayo a 0,618 en febrero. Siete puntos, algo más que el día de la semana pero todavía poco, y con unas 250 filas por mes el margen de error se come buena parte de esa diferencia.

Con dos años de datos no se puede afirmar estacionalidad. Con cinco, quizá 📅

2. La bandera de satisfacción vacía

Construye la tercera bandera de hueco y mira si aporta.

ventas['sin_satisfaccion'] = ventas['satisfaccion'].isna().astype(int)
print(ventas.groupby('sin_satisfaccion')['compro'].agg(['count', 'mean']).round(4))
                  count    mean
sin_satisfaccion               
0                  2769  0.5742
1                   231  0.6190

0,619 contra 0,574: cuatro puntos y medio con 231 filas. Es la más floja de las tres banderas y aun así yo la dejaría, porque cuesta una línea y no molesta.

Las banderas de hueco son de las pocas columnas que se pueden crear "por si acaso" sin culpa 🚩

3. El descuento, cuando existe

Entre las ventas que sí tuvieron descuento, ¿más descuento significa más cierre?

con = ventas[ventas['descuento'].notna()]
tramo = pd.qcut(con['descuento'], 4)
print(con.groupby(tramo, observed=True)['compro'].agg(['count', 'mean']).round(4))
                 count    mean
descuento                     
(-0.001, 0.063]    608  0.5757
(0.063, 0.127]     601  0.5574
(0.127, 0.185]     602  0.6362
(0.185, 0.25]      594  0.6380

Mira bien, porque no es plano ni es una recta: los dos tramos de abajo están en 0,576 y 0,557, y los dos de arriba en 0,636 y 0,638. Hay un escalón justo en el descuento mediano (12,7%) y nada más.

O sea: por debajo del 12,7% da igual cuánto descuentes, y por encima también da igual. Lo único que mueve la aguja es cruzar ese umbral, y son ocho puntos.

Eso es una recomendación comercial completa y sale de tres líneas: subir un descuento del 5% al 12% no sirve de nada, y subirlo del 18% al 25% tampoco. Todo lo que no sea cruzar el escalón es margen regalado 💰

Y fíjate en la técnica: esto es exactamente lo que los tramos sirven para encontrar. Con la columna numérica cruda, un modelo lineal habría buscado una recta y no la hay; con el tramo, el escalón se ve solo.

4. Cuántas veces ha aparecido este cliente

Un contador acumulado por cliente, mirando solo hacia atrás.

ventas['visita_numero'] = ventas.groupby('cliente_id').cumcount() + 1
tramo = pd.cut(ventas['visita_numero'], [0, 1, 3, 6, 100],
               labels=['1ª', '2ª a 3ª', '4ª a 6ª', '7ª o más'])
print(ventas.groupby(tramo, observed=True)['compro'].agg(['count', 'mean']).round(4))
               count    mean
visita_numero               
1ª               617  0.5851
2ª a 3ª         1126  0.5906
4ª a 6ª          986  0.5659
7ª o más         271  0.5498

Casi plano, con una bajadita: 0,585 en la primera visita y 0,550 a partir de la séptima. Tres puntos y medio en el extremo, con solo 271 filas ahí. Es poco y es la dirección contraria a la que esperaba.

El cumcount() sobre datos ordenados por fecha es la forma correcta de contar historia sin mirar el futuro, y en este archivo no aporta nada.

Guárdate la técnica igual, porque en datos de verdad "cuántas veces ha comprado antes" suele ser de las mejores columnas que existen 🔢

5. El error de construir sobre nulos

Intenta convertir a entero la columna de días, que tiene huecos.

ventas['dias_reales'].astype(int)
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')

Cannot convert non-finite values (NA or inf) to integer. Los nulos no caben en un entero de numpy.

Se arregla con .astype('Int64') (con I mayúscula, que es el entero de pandas y sí admite nulos) o rellenando primero. Y es de los errores que más aparecen justo en este paso, porque las columnas construidas casi siempre heredan los huecos de sus padres 🕳️

6. Todas las columnas nuevas, medidas de una vez

Una tabla con cuánto separa cada columna nueva, para decidir cuáles se quedan.

nuevas = ['sin_compra_previa', 'sin_descuento', 'sin_satisfaccion']
for col in nuevas:
    g = ventas.groupby(col)['compro'].mean()
    print(f'{col:20} {g[0]:.4f} vs {g[1]:.4f}   diferencia: {abs(g[1] - g[0]):.4f}')
sin_compra_previa    0.6140 vs 0.4136   diferencia: 0.2004
sin_descuento        0.6017 vs 0.4807   diferencia: 0.1210
sin_satisfaccion     0.5742 vs 0.6190   diferencia: 0.0448

Veinte puntos, doce puntos y cuatro puntos y medio. Las tres se quedan, y ahora sabes en qué orden confiar en ellas.

Este bucle de cuatro líneas es lo que yo corro cada vez que creo columnas nuevas. Una columna que no separa nada no se defiende con "es que tiene sentido": se mide 📏

7. La función de preparación completa

Junta la limpieza del capítulo 4 y las columnas de este, en algo reutilizable.

def prepara(v):
    """Columnas nuevas, sin ninguna que mire al futuro."""
    v = v.sort_values(['cliente_id', 'fecha']).copy()

    # Banderas de hueco: primero marcar, después rellenar.
    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)
    v['sin_descuento'] = v['descuento'].isna().astype(int)
    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)

    # Ratio.
    v['precio_unitario'] = v['monto'] / v['unidades']

    # Historia mirando solo hacia atrás.
    anterior = v.groupby('cliente_id')['fecha'].shift(1)
    v['dias_reales'] = (v['fecha'] - anterior).dt.days
    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1

    return v

datos = prepara(carga_limpia(URL))
print(datos.shape)
print([c for c in datos.columns if c not in carga_limpia(URL).columns])
(3000, 20)
['sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'precio_unitario', 'dias_reales', 'visita_numero']

Seis columnas nuevas, y fíjate en lo que no está: dias_desde_ultima, la de los negativos, y monto_final_facturado, que venía en el archivo. Las dos miran al futuro y no entran ni de broma 🚫

Esta función es el punto de partida de todos los capítulos que vienen.

Comprueba que lo tienes

Creas una columna nueva y el AUC sube de 0,69 a 0,95. ¿Qué es lo primero que piensas?

  • Que probablemente esa columna no existe cuando hay que predecir
  • Que encontré la variable clave
  • Que hay que crear más columnas parecidas
  • Que el modelo por fin está bien configurado

Y la columna que parece la mejor de todas

La trampa

La columna más tentadora que se puede construir, y la que más veces he visto en cuadernos ajenos: la tasa de compra histórica de cada cliente. Tiene todo el sentido del mundo y sube el AUC a 0,7666, por encima del mejor modelo del libro.

ventas['cliente_tasa'] = (
    ventas.groupby('cliente_id')['compro']
    .transform('mean'))
Qué está mal

Esa media se calcula sobre todas las filas, incluidas las que van a caer en el examen. Para cada venta, su propio resultado está metido dentro del promedio que la describe: el modelo se está mirando la respuesta de reojo 👀

Calculada como toca, usando solo las filas de entrenamiento, esa misma columna da 0,5048, que es tirar una moneda. Y sin ella, con las dos numéricas peladas, sale 0,5964. O sea que la columna estrella, bien hecha, es peor que no tenerla: hay 617 clientes para 3.037 ventas, así que casi nadie repite lo suficiente como para que su media signifique algo.

Esto tiene nombre y capítulo propio: es el 12.

Lo que te llevas

  • 🚩 El hueco se marca antes de rellenarlo. Aquí, veinte puntos, doce puntos y cuatro puntos y medio en tres líneas.
  • ➗ Los ratios funcionan: monto / unidades separa veinticinco puntos y no existía.
  • 📊 Los tramos sirven cuando la relación no es ordenada. Si es monótona, la columna numérica ya vale.
  • 😐 El día de la semana no aporta nada, y eso también se reporta.
  • 🚨 Cuidado con las columnas de tiempo: el 30% de fecha_ultima_compra es posterior a la venta. Se arregla con groupby().shift(1) sobre datos ordenados.
  • 🤷‍♀️ Hacerlo bien a veces mata la señal. La versión correcta de los días no aporta nada, y contarlo es parte del trabajo.
  • 📏 Cada columna nueva se mide contra el objetivo antes de quedarse. "Tiene sentido" no es un argumento.

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

La columna que mejor se ve suele ser la que te está copiando la respuesta.

Muchas de estas columnas se pueden construir antes, al traer los datos. Si te toca hacerlo en la consulta, está en el libro de SQL desde cero 🗄️

En el capítulo 11 metemos todo esto dentro de un pipeline, que es lo que impide que la preparación se te escape a los datos de examen.

Que tengas lindo día! 🌸

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

Los términos que salen en este capítulo

Están todos en el glosario de IA, con su definición corta.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?