El dataset de churn de MercaFresh ja està net, complet, transformat, codificat i escalat. Amb les lliçons anteriors hem reparat i adaptat el que hi havia; en aquesta última lliçó del mòdul fem el salt que sol aportar més rendiment: crear informació nova. L'enginyeria de característiques (feature engineering) consisteix a construir variables que condensin coneixement del negoci —quant fa que un client no compra, quant val, si la seva activitat creix o s'apaga— perquè el model no ho hagi de descobrir pel seu compte. Hi ha una dita molt citada en la professió: les dades i les característiques fixen el sostre del que es pot aconseguir; els algorismes només s'acosten més o menys a aquest sostre. Aquí aprendràs a construir features de negoci (amb el clàssic RFM), interaccions i polinomis, i a seleccionar quines conservar. Tancarem assemblant el dataset definitiu de churn i el checklist de tot el mòdul.

Contingut

  1. Què és l'enginyeria de característiques i per què importa tant
  2. Features de negoci: l'anàlisi RFM sobre MercaFresh
  3. Ratios i tendències: esprémer el que ja tens
  4. Interaccions i polinomis
  5. Selecció de característiques: quedar-se amb el que aporta
  6. El dataset final de churn de MercaFresh
  7. Checklist del preprocessament complet

Què és l'enginyeria de característiques i per què importa tant

Una característica (feature) és cada columna que el model rep com a entrada. L'enginyeria de característiques és l'art de fabricar columnes noves a partir de les existents, incorporant-hi coneixement del domini. Per què sol aportar més que canviar d'algorisme?

  • Els models veuen poc per si mateixos. Una regressió logística (04-02) només combina linealment les seves entrades: si el senyal del churn és al ratio despesa/comanda i li dones despesa i comandes per separat, potser no el trobarà mai. Si li dones el ratio ja calculat, el senyal està servit.
  • El coneixement de negoci no és a les dades crues. Que "un client que feia comandes setmanals i porta 45 dies sense comprar està en risc" ho sap qualsevol gestor de MercaFresh; la taula de comandes, per si sola, no ho diu — cal destil·lar-ho en una columna.
  • És la palanca més rendible. Canviar d'un algorisme decent a un altre de sofisticat sol millorar uns punts; una bona feature nova pot transformar el problema. Per això els equips experimentats dediquen més temps a les features que als models.

La matèria primera ja la tenim de les lliçons anteriors: les agregacions per client de 03-03 van ser el primer pas. Ara les convertim en un sistema.

Features de negoci: l'anàlisi RFM sobre MercaFresh

El marc RFM és un clàssic de l'anàlisi de clients que encaixa com un guant en el churn:

Lletra Significat Pregunta Feature a MercaFresh
R Recència (Recency) Quant fa de la seva última compra? dies_sense_comprar
F Freqüència (Frequency) Cada quant compra? comandes_per_mes
M Valor monetari (Monetary) Quant gasta? despesa_mitjana_comanda

La intuïció de negoci: un client que comprava molt, sovint i fa poc està sa; un la recència del qual creix mentre la seva freqüència cau se n'està anant — exactament el que el model de churn ha d'aprendre. Construïm-ho des de la taula de comandes:

import pandas as pd
import numpy as np

rng = np.random.default_rng(7)

# Taula de comandes simulada: 600 comandes de 80 clients durant un any
n = 600
comandes = pd.DataFrame({
    "id_client": rng.integers(101, 181, size=n),
    "data": pd.Timestamp("2025-08-24")
            + pd.to_timedelta(rng.integers(0, 365, size=n), unit="D"),
    "import_comanda": rng.lognormal(3.4, 0.5, size=n).round(2),
})

DATA_TALL = pd.Timestamp("2026-08-24")   # data fixa d'analisi (recorda 03-03)

rfm = comandes.groupby("id_client").agg(
    ultima_compra=("data", "max"),
    primera_compra=("data", "min"),
    num_comandes=("import_comanda", "count"),
    despesa_total=("import_comanda", "sum"),
)

# R: dies des de l'ultima compra
rfm["recencia_dies"] = (DATA_TALL - rfm["ultima_compra"]).dt.days

# F: comandes per mes de vida com a client (evitem dividir per 0 amb clip)
mesos_vida = ((DATA_TALL - rfm["primera_compra"]).dt.days / 30).clip(lower=1)
rfm["comandes_per_mes"] = (rfm["num_comandes"] / mesos_vida).round(2)

# M: despesa mitjana per comanda
rfm["despesa_mitjana_comanda"] = (rfm["despesa_total"] / rfm["num_comandes"]).round(2)

print(rfm[["recencia_dies", "comandes_per_mes", "despesa_mitjana_comanda"]].head())

Tres detalls d'ofici:

  • La data de tall fixa (no now()) fa el càlcul reproduïble, com vam veure a 03-03.
  • El clip(lower=1) evita divisions per zero amb clients d'un sol dia: les features noves també poden fabricar valors impossibles, i les regles de validació de 03-01 s'hi apliquen igual.
  • Cada feature RFM té una lectura de negoci directa: quan el model digui "la recència és el que més pesa", l'equip de retenció sabrà exactament què fer. Les bones features són també un canal de comunicació amb el negoci.

Una variant molt usada és convertir R, F i M en puntuacions d'1 a 5 amb qcut (el binning de 03-03!) i parlar de "clients 5-5-5" o "1-1-3"; la segmentació fina d'aquests perfils la farem amb clustering al mòdul 5 i al projecte 09-05.

Ratios i tendències: esprémer el que ja tens

Ratios: relacions que les columnes soltes no expliquen

Un ratio posa en relació dues magnituds i sovint diu més que totes dues per separat:

# Tiquet mitja: compres grans i espaiades, o petites i frequents?
rfm["despesa_per_comanda"] = rfm["despesa_total"] / rfm["num_comandes"]

# Intensitat: quina fraccio de la seva vida com a client porta inactiu?
vida_dies = (DATA_TALL - rfm["primera_compra"]).dt.days.clip(lower=1)
rfm["ratio_inactivitat"] = (rfm["recencia_dies"] / vida_dies).round(3)

ratio_inactivitat és una joia per al churn: 60 dies sense comprar són alarmants en un client setmanal (ratio alt) i normals en un de trimestral (ratio baix). El ratio codifica aquest context; les columnes soltes, no.

Tendència d'activitat: creix o s'apaga?

El churn és un procés, no un instant: abans d'anar-se'n, el client es refreda. Una feature de tendència ho captura comparant la seva activitat recent amb l'anterior:

ultim_trimestre = DATA_TALL - pd.Timedelta(days=90)
trimestre_previ = DATA_TALL - pd.Timedelta(days=180)

com_recent = (comandes[comandes["data"] >= ultim_trimestre]
              .groupby("id_client").size().rename("comandes_ult_90d"))
com_previ = (comandes[(comandes["data"] >= trimestre_previ)
                      & (comandes["data"] < ultim_trimestre)]
             .groupby("id_client").size().rename("comandes_prev_90d"))

rfm = rfm.join(com_recent).join(com_previ)
rfm[["comandes_ult_90d", "comandes_prev_90d"]] = (
    rfm[["comandes_ult_90d", "comandes_prev_90d"]].fillna(0))   # sense comandes = 0 (03-02)

# Tendencia: >1 accelera, ~1 estable, <1 s'apaga (+1 per evitar 0/0)
rfm["tendencia"] = ((rfm["comandes_ult_90d"] + 1)
                    / (rfm["comandes_prev_90d"] + 1)).round(2)

Un client amb tendencia = 0.25 (de 8 comandes a 1) és un candidat a churn de llibre encara que la seva despesa històrica sigui alta. Fixa't com reapareixen les lliçons anteriors: fillna(0) és una imputació amb significat de negoci (03-02) i el +1 és el mateix truc d'estabilitat que log1p (03-03).

Interaccions i polinomis

De vegades el senyal és a la combinació de dues variables: l'efecte de la despesa sobre el churn pot dependre del pla (perdre 50 €/mes d'un client bàsic no és el mateix que d'un premium). Una interacció és simplement el producte de dues features; un polinomi hi afegeix a més potències (x², x³) per capturar curvatures.

PolynomialFeatures les genera mecànicament:

from sklearn.preprocessing import PolynomialFeatures

X = rfm[["recencia_dies", "comandes_per_mes"]].head(3)

pf = PolynomialFeatures(degree=2, include_bias=False)
X_pol = pf.fit_transform(X)
print(pf.get_feature_names_out())
# ['recencia_dies' 'comandes_per_mes' 'recencia_dies^2'
#  'recencia_dies comandes_per_mes' 'comandes_per_mes^2']

De 2 columnes en surten 5: les originals, els seus quadrats i el seu producte. Dues advertències i una preferència:

  • Explosió combinatòria: amb 20 features i degree=2 surten 230 columnes; amb degree=3, més de 1.700. interaction_only=True limita als productes, però tot i així creix ràpid.
  • Més columnes fabricades mecànicament = més risc que el model memoritzi soroll (l'overfitting que formalitzarem a 06-05) i més necessitat de selecció (apartat següent) o de regularització (07-01).
  • Preferència d'ofici: abans que polinomis a l'engròs, poques interaccions triades amb el cap. recencia_dies * comandes_per_mes ("quant silenci acumula un client habitualment actiu") val més que vint productes cecs.

Selecció de característiques: quedar-se amb el que aporta

Crear features és fàcil; el perill és acabar amb 80 columnes on 50 són soroll, redundància o pes mort. Més features no sempre és millor: encareixen l'entrenament, dificulten la interpretació i donen al model més corda per ajustar-se a l'atzar. Tres filtres introductoris, de més simple a més formal:

  1. Variància gairebé nul·la

Una columna que gairebé no varia no pot distingir ningú. Si es_client_actiu val 1 per al 99,5 % de les files, no aporta res:

from sklearn.feature_selection import VarianceThreshold

vt = VarianceThreshold(threshold=0.01)   # elimina columnes gairebe constants
X_filtrat = vt.fit_transform(X_numeric)
print(vt.get_support())                   # mascara de columnes conservades

(Compte: el llindar depèn de l'escala, així que s'aplica sobre dades comparables — un altre punt per a l'escalat de 03-05.)

  1. Filtratge per correlació

Dos usos de la matriu de correlació de 02-03, ara com a eina de selecció:

  • Amb l'objectiu: features amb correlació ~0 amb el churn són candidates a sortir (amb la cautela de 02-03: Pearson només veu relacions lineals).
  • Entre si: dues features amb correlació 0,95 entre elles (com solen tenir despesa_total i num_comandes) són gairebé la mateixa informació dues vegades; conservar-ne una simplifica sense perdre senyal.
corr = X_numeric.corr(numeric_only=True)

# Parelles de features molt correlacionades entre si (redundancia)
alta = (corr.abs() > 0.9) & (corr.abs() < 1.0)
print([(a, b) for a in corr.columns for b in corr.columns
       if alta.loc[a, b] and a < b])

És literalment el rànquing de correlacions amb el churn que vam fer al final de 02-03, aplicat ara a decidir quines columnes viuen.

  1. SelectKBest: el filtre estadístic

SelectKBest puntua cada feature amb un test estadístic respecte a l'objectiu i es queda amb les k millors:

from sklearn.feature_selection import SelectKBest, f_classif

skb = SelectKBest(score_func=f_classif, k=5)
X_top = skb.fit_transform(X_numeric, y_churn)
print(dict(zip(X_numeric.columns, skb.scores_.round(1))))
print(X_numeric.columns[skb.get_support()].tolist())   # les 5 triades

f_classif és, en essència, un contrast d'hipòtesis com els de 02-04: difereix la mitjana d'aquesta feature entre els clients que se'n van i els que es queden? Puntuació alta = diferència clara = feature prometedora. És un mètode de filtratge (avalua cada feature aïllada, sense considerar combinacions): ràpid i útil com a garbell inicial, no com a veredicte final. I com tot allò que aprèn de l'objectiu, s'ajusta només amb dades d'entrenament — a aquestes altures del mòdul, el reflex ja hauria de ser automàtic.

Hi ha famílies més sofisticades (selecció embolcallant, PCA com a reducció de dimensionalitat a 05-03, regularització Lasso com a selecció implícita a 07-01); per tancar el preprocessament, aquests tres filtres basten.

El dataset final de churn de MercaFresh

Hora d'assemblar la feina de tot el mòdul en la taula definitiva, una fila per client:

Feature Origen Lliçó
edat, ciutat_* (one-hot), pla_cod Taula de clients neta i codificada 03-01, 03-04
satisfaccio + satisfaccio_absent Imputació amb indicador (MNAR) 03-02
log_despesa_total Transformació logarítmica 03-03
recencia_dies, comandes_per_mes, despesa_mitjana_comanda RFM 03-06
ratio_inactivitat, tendencia Ratios i tendència 03-06
recencia_x_frequencia Interacció triada a mà 03-06
churn (0/1) Variable objectiu: sense compres en 90 dies Definida a 01-05
flowchart TD
    A["Clients crus"] -->|"03-01 neteja"| B["Clients nets"]
    B -->|"03-02 nuls"| C["Clients complets"]
    P["Comandes crues"] -->|"03-03 agregacio"| D["RFM + ratios + tendencia<br/>(03-06)"]
    C --> E["merge per id_client"]
    D --> E
    E -->|"03-03 transformar<br/>03-04 codificar<br/>03-05 escalar"| F["Matriu numerica"]
    F -->|"03-06 seleccio"| G["Dataset final de churn"]

I el preprocessament complet, com a objecte únic executable — el ColumnTransformer que hem anat omplint lliçó a lliçó, ja amb totes les seves branques:

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import (OneHotEncoder, OrdinalEncoder,
                                   PowerTransformer, RobustScaler)

branca_numerica = Pipeline([
    ("imputar", SimpleImputer(strategy="median", add_indicator=True)),  # 03-02
    ("desasimetritzar", PowerTransformer(method="yeo-johnson")),         # 03-03
    ("escalar", RobustScaler()),                                         # 03-05
])

preprocessador = ColumnTransformer([
    ("num", branca_numerica,
     ["edat", "satisfaccio", "recencia_dies", "comandes_per_mes",
      "despesa_mitjana_comanda", "ratio_inactivitat", "tendencia"]),
    ("cat_nominal", OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
     ["ciutat"]),                                                        # 03-04
    ("cat_ordinal", OrdinalEncoder(categories=[["basic", "estandard", "premium"]]),
     ["pla"]),                                                           # 03-04
])

# Al modul 4: preprocessador.fit_transform(X_train) -> alimentar el model

Aquest objecte és el resum executable del mòdul: un sol fit sobre entrenament aprèn medianes, lambdes, quartils i categories; un sol transform prepara qualsevol client futur. Aquest dataset —aquestes features, aquest preprocessador— és el que farem servir conceptualment durant la resta del curs cada vegada que parlem del churn de MercaFresh.

Checklist del preprocessament complet

La llista de control del mòdul, en ordre d'aplicació:

  1. Auditar (03-01): info(), describe(), nunique(); entendre què representa cada fila.
  2. Netejar (03-01): duplicats exactes i lògics; regles de validesa per a valors impossibles; normalitzar text i categories; corregir tipus (to_numeric, to_datetime); diagnosticar outliers (error o realitat?).
  3. Tractar nuls (03-02): diagnosticar el mecanisme (MCAR/MAR/MNAR); eliminar només amb seguretat; imputar (mediana per defecte, per grups si hi ha estructura); indicadors d'absència si l'absència informa.
  4. Transformar (03-03): corregir asimetries (log1p, Yeo-Johnson); discretitzar si convé; descompondre dates; agregar per entitat fins a la granularitat del problema.
  5. Crear features (03-06): RFM, ratios, tendències, interaccions triades amb criteri de negoci.
  6. Codificar (03-04): ordinal amb ordre explícit per als ordinals reals; one-hot per als nominals; cura extrema amb el target encoding.
  7. Escalar (03-05): segons l'algorisme; RobustScaler si hi ha outliers legítims.
  8. Seleccionar (03-06): variància gairebé nul·la, redundància per correlació, SelectKBest com a garbell.
  9. Transversal: tot pas que aprèn paràmetres fa fit només sobre entrenament; encapsular en Pipeline/ColumnTransformer; documentar cada decisió.

(L'ordre 5→6→7 no és casual: les features es creen sobre valors llegibles, després es codifiquen, i l'escalat va al final perquè cada columna nova també necessita escala.)

Errors Comuns i Consells

  • Crear features fent servir el futur. Si el churn es mesura a 90 dies vista, cap feature no es pot calcular amb dades posteriors a la data de tall: és la fuita d'informació en la seva versió temporal, la més subtil de totes.
  • Fabricar centenars de features mecàniques i cap de negoci. PolynomialFeatures(degree=3) no substitueix una conversa amb l'equip de retenció de MercaFresh. Les millors features neixen de preguntes de negoci.
  • No validar les features noves. Un ratio pot generar infinits (divisió per zero) o valors absurds: passa a cada feature nova la mateixa auditoria de 03-01 (describe(), histograma).
  • Conservar features bessones. despesa_total, num_comandes i despesa_mitjana_comanda juntes són parcialment redundants: revisa la matriu de correlació entre features, no només contra l'objectiu.
  • Seleccionar features amb tot el dataset. SelectKBest mira l'objectiu: si s'ajusta amb dades que després seran de test, l'avaluació queda contaminada. fit a train, sempre.
  • No documentar el diccionari de dades. D'aquí a tres mesos ningú no recordarà com es va calcular tendencia. Mantén una taula feature → definició → fórmula → lliçó/data.
  • Consell: cada vegada que se t'acudeixi una feature, escriu primer en una frase quina història de negoci explica ("quant silenci acumula un client actiu"). Si no saps formular la frase, probablement tampoc no és una bona feature.

Exercicis

Exercici 1

Proposa (sense codi) tres features noves per al problema de demanda de MercaFresh (predir quantes unitats de cada producte es vendran demà), inspirades en les tècniques d'aquesta lliçó. Per a cadascuna, escriu la frase de negoci que explica.

Exercici 2

Amb la taula rfm de la lliçó, crea la feature valor_en_risc = despesa_mitjana_comanda * comandes_per_mes * ratio_inactivitat i explica què mesura en termes de negoci. Després calcula la seva correlació de Spearman amb recencia_dies i raona si t'esperaves aquest signe.

Exercici 3

Tens 12 features numèriques i l'objectiu churn. Escriu el codi que: (a) elimini les de variància < 0,01; (b) de les restants, seleccioni les 6 millors amb SelectKBest i f_classif; (c) imprimeixi quines han sobreviscut. Indica sobre quin subconjunt de dades faries els fit.

Solucions

Exercici 1

Respostes orientatives (n'hi ha moltes de vàlides):

  • vendes_mitjana_ult_4_setmanes (agregació temporal): "quant s'ha venut últimament és el millor punt de partida per a demà".
  • es_vigilia_festiu (component de calendari, 03-03): "abans d'un festiu, les compres de frescos es disparen".
  • ratio_preu_vs_categoria (ratio): "un producte temporalment més barat que la seva categoria roba vendes als seus substituts".

L'important: cada feature té una fórmula clara i una història de negoci en una frase.

Exercici 2

rfm["valor_en_risc"] = (rfm["despesa_mitjana_comanda"]
                        * rfm["comandes_per_mes"]
                        * rfm["ratio_inactivitat"]).round(2)

print(rfm["valor_en_risc"].corr(rfm["recencia_dies"], method="spearman"))

Què mesura: la despesa mensual habitual del client (tiquet × freqüència) ponderada pel seu grau d'inactivitat — és a dir, quants euros al mes estan en perill de perdre's. Un client valuós i molt inactiu puntua alt; un de barat o plenament actiu, baix. És una feature de priorització: a qui trucar primer. La correlació amb recencia_dies surt positiva, com era d'esperar: la recència entra al numerador de ratio_inactivitat, així que com més dies sense comprar, més valor en risc — Spearman (02-03) és la tria correcta perquè la relació és monòtona però no lineal.

Exercici 3

from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif

# Tots dos fit NOMES sobre el conjunt d'entrenament (X_train, y_train):
# VarianceThreshold no mira l'objectiu, pero SelectKBest si — i la coherencia
# del flux exigeix ajustar tot el preprocessament amb train.

vt = VarianceThreshold(threshold=0.01)
X_var = vt.fit_transform(X_train)
cols_var = X_train.columns[vt.get_support()]

skb = SelectKBest(score_func=f_classif, k=6)
X_sel = skb.fit_transform(X_var, y_train)
print(cols_var[skb.get_support()].tolist())

# A X_test s'hi apliquen els MATEIXOS objectes ja ajustats:
# X_test_sel = skb.transform(vt.transform(X_test))

Exercici de reflexió addicional: per què SelectKBest amb tot el dataset contaminaria l'avaluació? Perquè puntua cada feature segons la seva relació amb l'objectiu; si veu les etiquetes de test, la selecció resultant està optimitzada també per a test, i l'avaluació posterior ja no mesura generalització real.

Conclusió

Has tancat el mòdul amb la seva lliçó més creativa: què és l'enginyeria de característiques i per què fixa el sostre del model, com construir features de negoci amb el marc RFM (recència, freqüència, valor), ratios i tendències que capturen el refredament d'un client, interaccions i polinomis amb mesura, i tres filtres de selecció —variància gairebé nul·la, correlació i SelectKBest— per quedar-te amb el que aporta. I sobretot, has assemblat el resultat de les sis lliçons en un producte concret: el dataset final de churn de MercaFresh i el seu preprocessador executable, amb el checklist de nou passos com a mapa de tot el mòdul.

Aquest dataset ja no és una taula bruta exportada d'un CRM: és una matriu numèrica, completa, escalada i carregada de coneixement de negoci, a punt per entrenar. Al mòdul 4 arriba per fi el moment que portem tres mòduls preparant: els algorismes d'aprenentatge supervisat, començant per la regressió lineal — el model més simple i el millor lloc per entendre, de veritat, què significa que una màquina aprengui de les dades de MercaFresh.

Curs de Machine Learning

Mòdul 1: Introducció al Machine Learning

Mòdul 2: Fonaments d'Estadística i Probabilitat

Mòdul 3: Preprocessament de Dades

Mòdul 4: Algorismes de Machine Learning Supervisat

Mòdul 5: Algorismes de Machine Learning No Supervisat

Mòdul 6: Avaluació i Validació de Models

Mòdul 7: Tècniques Avançades i Optimització

Mòdul 8: Implementació i Desplegament de Models

Mòdul 9: Projectes Pràctics

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats