Dacă iei un singur lucru practic din acest modul, fie acela: pe date tabulare, în 2026, modelele tree-based încă câștigă. Nu „câștigă uneori, în funcție de set”. Câștigă ca default. Lucrarea din 2022 de la Grinsztajn, Oyallon și Varoquaux, „Why do tree-based models still outperform deep learning on tabular data”, a stabilit asta cu rigoare, iar patru ani mai târziu nimic nu a deplasat-o. Motivele sunt structurale, ajungem la ele la final, dar consecința e că atunci când vezi un CSV cu rânduri și coloane și un target, mișcarea corectă din prima e un gradient-boosted tree.
Lecția asta acoperă cele trei biblioteci care contează, XGBoost, LightGBM, CatBoost, plus random forest-ul din scikit-learn ca baseline puternic. Vom face fit pe aceeași problemă cu toate, vom vorbi despre hyperparametrii care chiar mișcă acul și vom discuta când să apelezi la care.
Ce e un tree, pe scurt
Un decision tree pune o secvență de întrebări da/nu despre feature-uri:
e income > 50000?
├── da: e age > 30?
│ ├── da: prezice "approve"
│ └── nu: prezice "deny"
└── nu: e employment_years > 5?
├── da: prezice "approve"
└── nu: prezice "deny"
Atât. Fiecare nod intern se împarte pe o feature și un prag; fiecare frunză dă o predicție. Algoritmul de training alege fiecare împărțire ca să maximizeze un criteriu de puritate (Gini, entropie, reducere de mean squared error), greedy, local optim, rapid.
Un singur tree face overfitting. Antrenează-l suficient de adânc și memorează setul de training. Remediul sunt ansamblurile: combini mai multe trees ca erorile lor individuale să se anuleze. Două tipuri de ansambluri domină.
Bagging (random forest): crește multe trees, fiecare pe un eșantion bootstrap de date și un subset aleator de feature-uri la fiecare împărțire, apoi mediază predicțiile lor. Aleatoritatea decorelează tree-urile, deci medierea reduce varianța.
Boosting (gradient boosting): crește tree-urile secvențial, fiecare antrenat să repare erorile ansamblului de până atunci. Mai lent, mai atent, de obicei mai precis.
În practică, gradient boosting bate random forest pe majoritatea problemelor tabulare, dar random forest e un default grozav, e robust, are puțini hyperparametri și produce un baseline puternic în trei linii de cod.
Random forest
Hai să începem cu baseline-ul. În scikit-learn:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
rf = RandomForestClassifier(
n_estimators=500, # numarul de trees, mai multe e mai bine, pana la un platou
max_depth=None, # lasa tree-urile sa creasca complet; bagging-ul gestioneaza overfit-ul
min_samples_leaf=2, # regularizare blanda
n_jobs=-1, # foloseste toate core-urile CPU
random_state=42,
)
scores = cross_val_score(rf, X, y, cv=5, scoring="roc_auc")
print(scores.mean())
Random forest e iertător. Hyperparametrii default îți dau 80% din acuratețea atinsă pe majoritatea problemelor. Reglează n_estimators în sus până se aplatizează scorul, setează min_samples_leaf la poate 2-5 și ai terminat.
Tratează și valori lipsă elegant dacă lași HistGradientBoostingClassifier din scikit-learn să facă asta, dar RandomForestClassifier simplu nu, fă imputare mai întâi.
Bibliotecile de boosting: XGBoost, LightGBM, CatBoost
Acum la adevăratele cai de muncă. Toate trei sunt biblioteci de gradient boosting, toate trei urmează API-ul .fit / .predict din scikit-learn și sunt în mare parte interschimbabile. Fiecare are punctele ei forte.
XGBoost (din 2014) a fost biblioteca care a pus gradient boosting pe hartă. A dominat Kaggle ani buni. Matur, prietenos cu GPU-ul, multe butoane. În 2026 e pe versiunea 2.x, cu un API Python stabil și suport îmbunătățit pentru categorice.
uv add xgboost
from xgboost import XGBClassifier
xgb = XGBClassifier(
n_estimators=2000,
learning_rate=0.05,
max_depth=6,
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=1.0,
eval_metric="auc",
early_stopping_rounds=50,
n_jobs=-1,
random_state=42,
)
xgb.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
verbose=False,
)
LightGBM (Microsoft, 2017) e mai rapid decât XGBoost pe majoritatea problemelor, mai ales pe seturi mari. Pretenția lui de glorie e creșterea leaf-wise în loc de depth-wise: crește tree-ul prin extinderea repetată a frunzei cu cea mai mare reducere de loss, ceea ce converge mai repede, dar e mai predispus la overfitting pe seturi mici dacă nu regularizezi. Acum pe versiunea 4.x.
uv add lightgbm
from lightgbm import LGBMClassifier
lgbm = LGBMClassifier(
n_estimators=2000,
learning_rate=0.05,
num_leaves=63, # leaf-wise, e analogul pentru max_depth
min_child_samples=20,
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=1.0,
n_jobs=-1,
random_state=42,
)
lgbm.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
callbacks=[lgb.early_stopping(50)],
)
CatBoost (Yandex, 2017) tratează nativ feature-urile categorice, le pasezi ca string-uri și descoperă intern un target encoding ordonat care nu scurge. Dacă setul tău e greu pe categorice, CatBoost e adesea cea mai ușoară victorie fiindcă sari peste un întreg pas de codare. E și cel mai prietenos cu începătorii, default-uri rezonabile, mai puțin tuning necesar.
uv add catboost
from catboost import CatBoostClassifier
cat = CatBoostClassifier(
iterations=2000,
learning_rate=0.05,
depth=6,
l2_leaf_reg=3,
cat_features=["country", "plan", "device"], # spune-i ce coloane sunt categorice
early_stopping_rounds=50,
random_seed=42,
verbose=False,
)
cat.fit(X_train, y_train, eval_set=(X_val, y_val))
Pe care s-o alegi? În practică, cele trei sunt la câteva puncte procentuale una de alta pe majoritatea problemelor. Default-urile mele: LightGBM pentru iterație rapidă, CatBoost când datele au multe categorice, XGBoost când echipa deja îl știe. Încearcă cel puțin două pe orice problemă serioasă și alege-o pe cea care scoreează cel mai bine pe un set ținut deoparte.
Hyperparametrii care contează
Gradient boosting are multe butoane. Pe majoritatea le poți lăsa la default. Cei care chiar mișcă scorul:
n_estimators (sau iterations în CatBoost). Numărul de trees în ansamblu. Mai multe e mai bine până la un punct, peste el începi să memorezi datele de training. Răspunsul corect e „cât poți de multe înainte ca validation loss să nu se mai îmbunătățească”, ceea ce gestionează early stopping pentru tine. Setează asta la un număr mare, gen 2000-5000, și lasă early_stopping_rounds să te taie.
learning_rate (și eta). Cât corectează fiecare tree nou eroarea anterioară. Learning rate mai mic plus mai multe trees câștigă de obicei, dar durează mai mult la fit. 0,05 e un default rezonabil. 0,01 dacă ai timp și vrei ultimii 0,5% de acuratețe. 0,1-0,3 pentru prototyping rapid.
max_depth (XGBoost, CatBoost) sau num_leaves (LightGBM). Cât de complex poate fi fiecare tree individual. Tree-uri mai adânci se potrivesc pe pattern-uri mai nuanțate și fac overfitting mai repede. 4-10 acoperă majoritatea cazurilor. num_leaves din LightGBM e cam 2^max_depth, deci num_leaves=63 e comparabil cu max_depth=6.
min_child_weight (XGBoost) / min_samples_leaf (random forest) / min_child_samples (LightGBM). Numărul minim de eșantioane (sau eșantioane ponderate) cerute pentru a forma o frunză. Valori mai mari înseamnă mai multă regularizare. 20 e un default rezonabil pentru seturi medii.
subsample și colsample_bytree. Fracțiunea de rânduri și coloane de eșantionat pentru fiecare tree. Setarea lor sub 1,0 (de obicei 0,7-0,9) introduce stochasticitate care acționează ca bagging peste boosting. Aproape întotdeauna ajută puțin.
reg_lambda / l2_leaf_reg. Regularizare L2 pe weights-urile frunzelor. Default-ul e ok în majoritatea cazurilor; reglează dacă vezi overfit.
Adevărul cinstit: cu default-uri rezonabile plus early stopping, gradient boosting îți dă 90% din acuratețea atinsă. Restul de 10% cere tuning serios, ideal cu o unealtă precum Optuna care face căutare bayesiană peste spațiul de parametri.
import optuna
from lightgbm import LGBMClassifier
from sklearn.model_selection import cross_val_score
def objective(trial):
params = {
"n_estimators": 2000,
"learning_rate": trial.suggest_float("lr", 0.01, 0.1, log=True),
"num_leaves": trial.suggest_int("num_leaves", 16, 256),
"min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
"subsample": trial.suggest_float("subsample", 0.6, 1.0),
"colsample_bytree": trial.suggest_float("colsample_bytree", 0.6, 1.0),
"reg_lambda": trial.suggest_float("reg_lambda", 1e-3, 10.0, log=True),
}
model = LGBMClassifier(**params, n_jobs=-1, random_state=42)
return cross_val_score(model, X, y, cv=5, scoring="roc_auc").mean()
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)
print(study.best_params, study.best_value)
Ăsta e pattern-ul de producție: pipeline plus Optuna plus early stopping plus un set de test ținut deoparte la care te uiți o singură dată.
Early stopping
Ăsta e trucul care transformă „alege n_estimators cu grijă” în „alege un număr mare și lasă biblioteca să-și dea seama”. Pasează un validation set; biblioteca urmărește validation loss la fiecare rundă; dacă nu se îmbunătățește pentru early_stopping_rounds runde consecutive, training-ul se oprește și cea mai bună iterație e restaurată.
xgb.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
verbose=False,
)
# Dupa fit, xgb.best_iteration iti spune unde s-a oprit.
Validation set-ul trebuie să fie separat de test set-ul tău. Test set-ul e pentru evaluare finală; validation set-ul e pentru alegerea hyperparametrilor și oprire. Confundarea lor e o variantă de leakage.
Aceeași problemă, trei biblioteci, una lângă alta
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
# Un set tabular realist
data = fetch_openml("adult", version=2, as_frame=True)
X, y = data.data, (data.target == ">50K").astype(int)
X = pd.get_dummies(X, drop_first=True) # gestionare rapida a categoricelor
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42,
)
X_tr, X_val, y_tr, y_val = train_test_split(
X_train, y_train, test_size=0.2, stratify=y_train, random_state=42,
)
models = {
"RandomForest": RandomForestClassifier(n_estimators=500, n_jobs=-1, random_state=42),
"XGBoost": XGBClassifier(n_estimators=1000, learning_rate=0.05, max_depth=6,
early_stopping_rounds=50, eval_metric="auc",
n_jobs=-1, random_state=42),
"LightGBM": LGBMClassifier(n_estimators=1000, learning_rate=0.05, num_leaves=63,
n_jobs=-1, random_state=42),
}
for name, m in models.items():
if name == "RandomForest":
m.fit(X_tr, y_tr)
else:
m.fit(X_tr, y_tr, eval_set=[(X_val, y_val)], verbose=False)
auc = roc_auc_score(y_test, m.predict_proba(X_test)[:, 1])
print(f"{name:12s} AUC = {auc:.4f}")
Într-o rulare rapidă pe setul adult, așteaptă-te la cifre în zona 0,91-0,93 ROC AUC, cu booster-ii depășind forest-ul cu un punct procentual sau cam atât. Pe o problemă reală de producție, ăsta e punctul tău de start, alege câștigătorul, fă feature engineering (lecția 50), reglează cu Optuna și ești bine intrat în teritoriul predicțiilor utile.
De ce trees încă bat deep learning pe date tabulare
Lucrarea din 2022 a lui Grinsztajn et al. a formalizat ce zicea de ani buni lumea din practică. Trei motive structurale:
-
Datele tabulare au feature-uri eterogene. Un rând ar putea avea age, country, suma tranzacției și tenure-ul clientului, feature-uri măsurate în unități complet diferite, cu distribuții diferite și fără structură spațială care să le conecteze. Neural networks au fost construite pentru input-uri unde valorile vecine înseamnă ceva (pixeli, cuvinte). Trees tratează fiecare feature independent, ceea ce se potrivește cu felul în care sunt structurate datele tabulare în realitate.
-
Trees tratează bine funcții target nenetede. O mică schimbare de income ar putea inversa o decizie de creditworthiness; deep nets, cu fluxul lor neted de gradient, se chinuie să reprezinte frontiere ascuțite. Trees le reprezintă nativ.
-
Trees tratează valori lipsă și categorice fără preprocessing. Bibliotecile moderne de boosting se împart direct pe missingness și (în cazul CatBoost) pe valori categorice fără one-hot. Neural nets au nevoie de tot în formă numerică densă, ceea ce e mai fragil și pierde informații.
A existat progres pe deep learning tabular, TabNet, FT-Transformer, SAINT, TabPFN, și pe unele probleme specifice egalează sau bat boosting-ul. Dar „egalează sau bate boosting-ul” e ștacheta de depășit, iar pe o problemă tabulară medie în 2026, începi cu LightGBM și termini cu LightGBM.
Închidere
Trei lecții înăuntru. Știi API-ul scikit-learn, știi ce face feature-uri bune și ce scurge și știi să faci fit pe un model tree-based competitiv cu orice altceva pe date tabulare. E suficient ca să tratezi grosul problemelor reale de ML pentru care lumea e plătită să le rezolve. Lecțiile următoare din modul merg dincolo de tabular, evaluare făcută ca lumea, neural networks, NLP și povestea de productionization, dar dacă te-ai opri aici și ai face mereu doar pipeline-uri plus boosted trees, ai fi deja înaintea majorității echipelor.