Există un adevăr tăcut în machine learning-ul tabular peste care majoritatea cursurilor trec: modelul rareori contează la fel de mult ca feature-urile. Poți lua același set de date, să-i arunci aceiași gradient-boosted trees de două ori și o versiune o bate pe cealaltă cu zece puncte procentuale pur și simplu fiindcă cineva și-a petrecut un weekend gândindu-se la ce să pună pe partea de input. Acel weekend e feature engineering și asta e lecția care, mai mult decât oricare alta din acest modul, îți va mișca numerele.
Vom parcurge categoriile de feature engineering, apoi vom vorbi despre leakage, bug-ul care îți umflă în tăcere metricile și îți distruge producția. Majoritatea codului efectiv trăiește în pipeline-uri pe care le-ai văzut deja în lecția 49; partea grea e gândirea.
Ce înseamnă cu adevărat „engineering” aici
Feature engineering e actul de a transforma coloane brute în reprezentări din care un model poate învăța. Unele transformări sunt mecanice (centrare și scalare). Unele cer cunoștințe de domeniu (să știi că o tranzacție la 3 dimineața e mai suspectă decât una la 3 după-amiaza). Unele sunt pur și simplu inteligente (codarea momentului zilei ca sin(2*pi*hour/24) și cos(2*pi*hour/24) ca modelul să vadă că 23:00 e aproape de 00:00).
Categoriile pe care le voi parcurge:
- Scaling
- Codarea variabilelor categorice
- Interacțiuni
- Feature-uri de timp
- Agregări
- Feature-uri de text
- Tratarea valorilor lipsă
Și apoi secțiunea despre leakage, care e cea mai importantă parte a acestei lecții și partea pe care majoritatea tutorialelor grăbite o sar.
Scaling
Majoritatea modelelor non-tree țin la scala input-urilor tale. Modelele liniare, neural networks, k-nearest neighbors și SVM-urile toate fac mai prost, uneori catastrofal de prost, când o feature variază de la 0 la 1 și alta de la 0 la 1.000.000. Soluția e să standardizezi fiecare coloană numerică la medie 0 și abatere standard 1:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Modelele tree-based, random forests, XGBoost, LightGBM, nu țin la asta. Ele împart pe praguri, iar un prag pe o valoare înmulțită cu 1000 e același cu un prag pe valoarea însăși. Nu pierde timp scalând pentru trees.
Două variante de știut. MinMaxScaler rescalează la [0, 1], ceea ce vrei pentru input-uri în neural networks cu funcții de activare mărginite. RobustScaler folosește mediana și intervalul intercuartil, ceea ce vrei când datele tale au outlieri, o singură valoare mare nu va trage media și nu va umfla abaterea standard.
Pentru feature-uri numerice cu coadă foarte lungă (income, sume de tranzacții, page views), aplică mai întâi un log transform: np.log1p(x), adică log(1+x), care tratează zerourile elegant. După log, distribuția e mai aproape de simetrică și scalarea face ce trebuie.
Codarea variabilelor categorice
Categoriile trebuie să devină numere. Codarea potrivită depinde de cardinalitate și dacă există ordine.
Cardinalitate joasă, fără ordine: one-hot encoding. Fiecare categorie devine propria coloană 0/1.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
X_encoded = encoder.fit_transform(X[["country", "plan"]])
handle_unknown="ignore" e ce te salvează în producție când apare o categorie nouă care nu era în datele de training. Fără el, serviciul tău crapă în momentul în care marketingul lansează într-o țară nouă.
Cardinalitate înaltă: nu face one-hot. Dacă ai 50.000 de user IDs unice sau 10.000 de SKU-uri de produs, one-hot îți dă o matrice rară de 50.000 de coloane pe care majoritatea modelelor o îneacă. Cele două opțiuni reale:
- Target encoding: înlocuiește fiecare categorie cu media variabilei target pentru acea categorie. Deci dacă ai o coloană
countryși rata medie de conversie pentru utilizatorii din Italia e 4,2%, atuncicountry=ITdevine0.042. Puternic, dar o capcană de leakage (mai multe mai jos). Folosește bibliotecacategory_encoderssauTargetEncoderdin sklearn (adăugat în 1.3, cu cross-fitting corect ca să eviți leakage-ul). - Hashing: trece categoria printr-o funcție de hash modulo
n, ajungând lancoloane. Rapid, cu pierdere, fără leakage. Bun pentru cardinalitate foarte mare unde nu îți pasă de identitatea exactă a categoriei.
Categorii ordonate: ordinal encoding e ok. ["small", "medium", "large"] la [0, 1, 2] păstrează ordinea. Dar nu face asta niciodată pe categorii neordonate, minți modelul despre distanță.
Interacțiuni
Uneori două feature-uri înseamnă ceva doar împreună. Income singur nu îți spune mare lucru; income per membru de gospodărie da. Ora-zilei singură îți spune ceva; ora-zilei combinată cu ziua-săptămânii îți spune mult mai mult.
Pentru modelele tree, interacțiunile vin gratis, un tree care se împarte pe income, apoi pe household_size într-o ramură a descoperit o interacțiune. Pentru modelele liniare, trebuie să le scrii explicit:
import pandas as pd
df["income_per_member"] = df["income"] / df["household_size"]
df["weekend_x_hour"] = df["is_weekend"] * df["hour"]
Sau folosește PolynomialFeatures ca să generezi automat toate produsele perechi, util pentru seturi mici de feature-uri, periculos pentru cele mari fiindcă numărul de interacțiuni explodează cuadratic.
Feature-uri de timp
Timestamp-urile trebuie despachetate. O coloană datetime brută poartă aproape niciun semnal pe care modelul să-l poată folosi direct; extragerea componentelor da:
df["hour"] = df["timestamp"].dt.hour
df["day_of_week"] = df["timestamp"].dt.dayofweek
df["month"] = df["timestamp"].dt.month
df["is_weekend"] = df["day_of_week"].isin([5, 6]).astype(int)
df["is_business_hour"] = df["hour"].between(9, 17).astype(int)
Pentru feature-uri ciclice, oră, zi a săptămânii, lună, există o problemă subtilă: dacă codezi ora ca 0-23, modelul vede 23 și 0 la 23 de unități distanță, când în realitate sunt la 1 oră distanță. Codarea ciclică prin sin/cos rezolvă:
import numpy as np
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
Acum modelul vede corect trecerea peste 0. Același truc merge pentru ziua săptămânii (perioadă 7), lună (perioadă 12), ziua anului (perioadă 365,25).
Nu include timestamp-ul brut decât dacă încerci să captezi un trend pe termen lung, și chiar și atunci, folosește (timestamp - reference_date).days ca valoarea să fie mărginită.
Agregări
Aici se ascunde elevația reală pe probleme la nivel de utilizator sau de tranzacție. Pentru fiecare utilizator, calculezi feature-uri precum:
- Suma totală cheltuită în ultimele 7 zile
- Numărul de tranzacții în ultimele 30 de zile
- Suma medie a tranzacțiilor în ultimele 90 de zile
- Zile de la ultima tranzacție
În pandas, asta e un groupby plus o agregare cu fereastră. Capcana, și acum alunecăm în secțiunea de leakage, e că „ultimele 7 zile” trebuie să însemne ultimele 7 zile relativ la momentul predicției. Dacă predicția ta e pentru tranzacția T, atunci agregările trebuie să folosească doar date cu timestamp strict înaintea lui T. Includerea lui T însuși în agregare e o scurgere: modelul ajunge să vadă răspunsul.
# Gresit (scurge): agregare peste toate datele
df["user_total_spend"] = df.groupby("user_id")["amount"].transform("sum")
# Corect: rolling window strict inainte de fiecare rand
df = df.sort_values(["user_id", "timestamp"])
df["spend_past_7d"] = (
df.groupby("user_id")
.rolling("7D", on="timestamp", closed="left")["amount"]
.sum()
.reset_index(level=0, drop=True)
)
closed="left" exclude rândul curent. Fără el, ai scurs.
Feature-uri de text
Coloanele de text liber, descrieri, search queries, ticket-uri de support, nu intră brute. Cele două căi clasice:
Bag of words / TF-IDF: numără termenii, ponderează cu inverse document frequency. Ieftin, interpretabil, surprinzător de competitiv pe documente scurte.
from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))
X_text = vec.fit_transform(df["description"])
Embeddings: trece textul printr-un model de sentence transformer și folosește vectorul rezultat ca feature. Semnal mult mai bun, mai ales când contează mai mult sensul decât cuvintele exacte. În 2026, standardul e sentence-transformers de la Hugging Face; modelul mic all-MiniLM-L6-v2 e rapid pe CPU și suficient de bun pentru majoritatea problemelor tabular-plus-text.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(df["description"].tolist()) # array (n, 384)
Concatenează coloanele de embedding la celelalte feature-uri și tratează-le ca pe orice alt input numeric.
Valori lipsă
Datele reale au goluri. Modelele în general nu le tratează nativ (XGBoost și LightGBM sunt excepții, ele se împart direct pe missingness). Trei strategii:
- Impute: înlocuiește cele lipsă cu media, mediana sau moda. Mediana e de obicei default-ul mai sigur pentru feature-uri numerice.
- Sentinel value: înlocuiește cu o valoare pe care modelul nu o poate confunda cu date reale.
-999pentru o feature cu valori pozitive,"_missing_"pentru o categorie. - Coloană indicator: adaugă o coloană separată 0/1 care spune dacă valoarea era lipsă. Adesea combinată cu imputarea, fiindcă faptul în sine al lipsei poate fi predictiv.
from sklearn.impute import SimpleImputer
df["age_missing"] = df["age"].isna().astype(int) # indicatorul
imputer = SimpleImputer(strategy="median")
df[["age"]] = imputer.fit_transform(df[["age"]])
Pentru modele tree-based cu suport nativ pentru valori lipsă (XGBoost, LightGBM), pur și simplu lasă NaN-urile, biblioteca le va trata și adesea face mai bine decât ar face imputarea ta.
Leakage: bug-ul care te minte
Acum partea care îți ia mai mult să o înveți decât oricare alta. Leakage se întâmplă când informații care nu ar fi disponibile la momentul predicției se strecoară în feature-urile tale de training. Modelul le prinde, scoreează grozav în cross-validation și se duce de râpă în producție fiindcă semnalul scurs nu există la inferență.
Două gusturi principale:
Target leakage: o feature include informații despre target. Exemplul clasic: predicția dacă un client va pleca, cu o feature precum cancellation_processed_date. Acel câmp se populează doar după ce plecarea s-a întâmplat. Includerea lui dă modelului un predictor aproape perfect care nu există atunci când chiar vrei să prezici.
Soluția: gândește-te bine când devine fiecare feature disponibilă. Dacă prezici la momentul T, fiecare feature trebuie să poată fi derivată din date dinaintea lui T. Dacă o coloană s-ar putea popula după evenimentul target, aruncă-o sau recalculeaz-o ca de la T.
Train-test leakage: preprocessing-ul folosește statistici din test set. Dacă faci fit la un StandardScaler pe tot setul de date înainte de split, scaler-ul a văzut media datelor de test. Scorul de cross-validation se umflă cu o cantitate mică care, pe probleme strânse, e diferența dintre lansăm și nu lansăm.
Soluția e exact pentru ce au fost gândite pipeline-urile scikit-learn: înfășoară preprocessing-ul într-un Pipeline, pasează-l către cross_val_score și biblioteca refittează totul în interiorul fiecărui fold automat.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = Pipeline([
("scale", StandardScaler()),
("clf", LogisticRegression(max_iter=1000)),
])
# In interiorul fiecarui fold, scaler-ul refitteaza doar pe portiunea de training a foldului.
scores = cross_val_score(pipe, X, y, cv=5)
Dacă faci agregări sau target encodings în afara pipeline-ului, te-ai întors la prevenția manuală a leakage-ului: split mai întâi, apoi calculează feature-uri folosind doar datele de training, apoi transformă datele de test cu transformatorul fittat pe training.
O verificare utilă din burtă: dacă modelul tău scoreează suspect de bine din prima încercare, să zicem 99% accuracy pe o problemă pe care expertul tău de domeniu zice că e grea, presupune leakage până se demonstrează contrariul. Modelele reale pe probleme reale sunt imperfecte. Un model perfect înseamnă de obicei un model care trage cu ochiul.
O notă despre asistența AI
Când ceri unui asistent AI să scrie cod de feature engineering pentru un set de date, e bun la pattern-urile standard, dă-i un eșantion din datele tale și va sugera scaler-i, encoder-i și împărțiri de feature-uri de timp rezonabile. Pe ce nu e de încredere e la semantica de timp a agregărilor. Va sugera bucuros feature-uri precum „media tuturor tranzacțiilor anterioare” fără o fereastră sau un closed="left", iar codul rezultat va scurge viitorul în prezent.
Citește mereu codul de feature sugerat de AI cu o întrebare în minte: „în momentul în care facem o predicție, fiecare coloană de input conține doar informații care erau disponibile înainte de acel moment?” Dacă răspunsul e nu pentru orice coloană, repară-l înainte să te încrezi în metrici.
Închidere
Feature engineering e partea înceată, neglamoroasă a ML-ului tabular și totodată partea în care munca dă roade. Transformările din această lecție acoperă majoritatea cazurilor pe care le vei întâlni pe seturi reale de date; disciplina împotriva leakage-ului e ce-ți păstrează numerele cinstite. Lecția următoare, trecem tot prin modele tree-based, familia care, în 2026, încă câștigă pe probleme tabulare.