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
- Què és l'enginyeria de característiques i per què importa tant
- Features de negoci: l'anàlisi RFM sobre MercaFresh
- Ratios i tendències: esprémer el que ja tens
- Interaccions i polinomis
- Selecció de característiques: quedar-se amb el que aporta
- El dataset final de churn de MercaFresh
- 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=2surten 230 columnes; ambdegree=3, més de 1.700.interaction_only=Truelimita 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:
- 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.)
- 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_totalinum_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.
SelectKBest: el filtre estadístic
SelectKBest: el filtre estadísticSelectKBest 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 triadesf_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 modelAquest 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ó:
- Auditar (03-01):
info(),describe(),nunique(); entendre què representa cada fila. - 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?). - 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.
- Transformar (03-03): corregir asimetries (
log1p, Yeo-Johnson); discretitzar si convé; descompondre dates; agregar per entitat fins a la granularitat del problema. - Crear features (03-06): RFM, ratios, tendències, interaccions triades amb criteri de negoci.
- Codificar (03-04): ordinal amb ordre explícit per als ordinals reals; one-hot per als nominals; cura extrema amb el target encoding.
- Escalar (03-05): segons l'algorisme;
RobustScalersi hi ha outliers legítims. - Seleccionar (03-06): variància gairebé nul·la, redundància per correlació,
SelectKBestcom a garbell. - Transversal: tot pas que aprèn paràmetres fa
fitnomés sobre entrenament; encapsular enPipeline/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_comandesidespesa_mitjana_comandajuntes són parcialment redundants: revisa la matriu de correlació entre features, no només contra l'objectiu. - Seleccionar features amb tot el dataset.
SelectKBestmira l'objectiu: si s'ajusta amb dades que després seran de test, l'avaluació queda contaminada.fita 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
- Què és el Machine Learning?
- Història i evolució del Machine Learning
- Tipus de Machine Learning
- Aplicacions del Machine Learning
- El flux de treball d'un projecte de Machine Learning
Mòdul 2: Fonaments d'Estadística i Probabilitat
- Conceptes bàsics d'estadística
- Distribucions de probabilitat
- Correlació i covariància
- Inferència estadística
- Teorema de Bayes
Mòdul 3: Preprocessament de Dades
- Neteja de dades
- Gestió de dades mancants
- Transformació de dades
- Codificació de variables categòriques
- Normalització i estandardització
- Enginyeria de característiques
Mòdul 4: Algorismes de Machine Learning Supervisat
- Regressió lineal
- Regressió logística
- Arbres de decisió
- Màquines de suport vectorial (SVM)
- K veïns més propers (K-NN)
- Naive Bayes
- Xarxes neuronals
Mòdul 5: Algorismes de Machine Learning No Supervisat
- Clustering: K-means
- Clustering jeràrquic
- Anàlisi de components principals (PCA)
- Anàlisi d'agrupament DBSCAN
- Visualització de dades amb t-SNE i UMAP
Mòdul 6: Avaluació i Validació de Models
- Divisió de dades: entrenament, validació i prova
- Mètriques d'avaluació
- Validació creuada
- Corba ROC i AUC
- Overfitting i underfitting
Mòdul 7: Tècniques Avançades i Optimització
- Regularització: Ridge, Lasso i Elastic Net
- Ensemble Learning
- Gradient Boosting
- Xarxes neuronals profundes (Deep Learning)
- Optimització d'hiperparàmetres
Mòdul 8: Implementació i Desplegament de Models
- Frameworks i biblioteques populars
- Implementació de models en producció
- Manteniment i monitoratge de models
- Consideracions ètiques i de privadesa
Mòdul 9: Projectes Pràctics
- Projecte 1: Predicció de preus d'habitatges
- Projecte 2: Classificació d'imatges
- Projecte 3: Anàlisi de sentiments a les xarxes socials
- Projecte 4: Detecció de fraus
- Projecte 5: Segmentació de clients
