Am ajuns la modulul 9, cel despre machine learning. Dacă ai citit fiecare lecție până aici, ai abilitățile de data engineering: știi să manipulezi un DataFrame, să construiești un ETL și să vizualizezi rezultatul. ML-ul e ce fac unii cu datele alea după aceea. Nu înlocuiește pipeline-urile curate și SQL-ul bun; e o unealtă care, din când în când, îți permite să prezici ceva util din pattern-urile din datele tale.
Primele trei lecții ale acestui modul sunt deschiderea: biblioteca standard (asta), partea care chiar mută acul (lecția 50, feature engineering) și familia de modele care câștigă pe date tabulare (lecția 51, trees). Restul, neural networks, NLP, sisteme de recomandare, vine mai târziu sau e propriul curs. Începem cu scikit-learn fiindcă e locul unde aproape orice carieră în Python ML începe și unde majoritatea rămâne pentru munca tabulară.
Ce este scikit-learn, în 2026
scikit-learn există din 2007. E pe linia 1.x, ceea ce înseamnă că API-ul e stabil, iar firma la care lucrezi îl are probabil fixat undeva într-un requirements.txt. Nu e cel mai rapid, nu e cel mai profund și nu suportă GPU-uri. Este, totuși, cel mai consistent API din toată data science și e biblioteca care a învățat o generație de practicieni cum trebuie să arate un estimator.
Îl instalezi în modul obișnuit:
uv add scikit-learn
Iar importul e sklearn, nu scikit-learn. Da, e enervant. Așa stă lucrurile de aproape două decenii și nimeni nu o să schimbe asta.
import sklearn
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
Pattern-ul fit/predict
Iată tot modelul mental pentru scikit-learn. Fiecare model, și sunt zeci, expune aceleași două metode:
model.fit(X, y) # invata din date
model.predict(X_new) # prezice pe date noi
Atât. O regresie liniară, un random forest, un support vector machine, un k-nearest neighbors classifier, toate arată așa. Odată ce ai folosit un estimator, le-ai folosit pe toate. Trecerea de la logistic regression la gradient boosting e o modificare de o linie.
Clasificatorii de obicei expun și .predict_proba(X_new), care îți dă probabilități de clasă în loc de etichete dure de clasă, ceea ce de fapt vrei de cele mai multe ori:
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test) # array de 0/1
probabilities = model.predict_proba(X_test) # array de perechi (P(0), P(1))
Convenția de formă e la fel de consistentă: X e bidimensional, cu forma (n_samples, n_features), un NumPy array sau un pandas DataFrame, ambele merg. y e unidimensional, cu lungimea n_samples. Dacă ai o singură feature, tot trebuie s-o reformatezi într-o coloană: X.reshape(-1, 1). Dacă uiți, primești faimoasa eroare „Reshape your data either using array.reshape(-1, 1) if your data has a single feature”, pe care fiecare începător o vede cel puțin o dată.
Transformatorii, lucruri care schimbă X în loc să prezică din el, urmează un pattern ușor diferit:
scaler.fit(X_train) # invata mediile si abaterile standard
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Sau, echivalent:
X_train_scaled = scaler.fit_transform(X_train)
Detaliul crucial: apelezi .fit doar pe datele de training, apoi .transform pe tot. Fitting-ul pe datele de test e o scurgere. Vom reveni la asta în lecția 50.
Familiile de modele
Hai să-ți dau harta. Taxonomia nu e strictă, dar dacă știi care librărie locuiește în ce submodul, accelerezi enorm căutarea în docs.
Modele liniare (sklearn.linear_model). Clasicele: LinearRegression pentru regresie, LogisticRegression pentru clasificare (în pofida numelui). Ridge și Lasso adaugă regularizare L2 și respectiv L1, ceea ce vrei când ai multe feature-uri sau feature-uri corelate. Modelele liniare sunt rapide, interpretabile și șocant de competitive pe multe seturi de date reale, mai ales după un feature engineering bun.
from sklearn.linear_model import Ridge, Lasso, LogisticRegression
ridge = Ridge(alpha=1.0) # alpha controleaza forta regularizarii
lasso = Lasso(alpha=0.1) # Lasso face si selectie de features (seteaza unele weights la zero)
logreg = LogisticRegression(C=1.0, max_iter=1000) # C e regularizarea inversa, atentie
Modele tree-based (sklearn.tree, sklearn.ensemble). DecisionTreeClassifier e cărămida. RandomForestClassifier și GradientBoostingClassifier sunt caii de muncă. În practică, pentru gradient boosting serios, treci la XGBoost sau LightGBM în loc de varianta din scikit-learn, sunt mai rapide și mai precise. Vom face asta în lecția 51. Dar random forest-ul din scikit-learn e cu adevărat bun, iar API-ul e identic, deci e un default rezonabil când vrei un baseline puternic în trei linii.
Instance-based (sklearn.neighbors). KNeighborsClassifier prezice pe baza celor mai apropiate k puncte de training. Simplu, uneori surprinzător de eficient, lent la predict pe seturi mari de date.
Metode kernel (sklearn.svm). SVC și SVR pentru clasificare și regresie. Teorie frumoasă, obișnuiau să domine, acum în mare parte deplasate de trees și de neural nets în munca tabulară și respectiv pe imagini. Tot utile pentru seturi mici-medii și când vrei o frontieră de decizie netedă.
Neural networks (sklearn.neural_network). MLPClassifier există, dar e limitat: fără GPU, fără flexibilitate reală de arhitectură. Dacă chiar îți trebuie un neural net, folosește PyTorch. MLP-ul din scikit-learn e ok pentru o primă schiță.
Clustering, reducere dimensională și restul trăiesc în sklearn.cluster (KMeans, DBSCAN), sklearn.decomposition (PCA, NMF) și sklearn.manifold (t-SNE, lucruri apropiate de UMAP). Urmează același pattern .fit / .transform / .fit_predict.
Preprocessing
Majoritatea modelelor așteaptă numere într-un interval rezonabil. Modulul de preprocessing din scikit-learn are uneltele standard:
from sklearn.preprocessing import (
StandardScaler, # medie 0, std 1, default-ul pentru majoritatea
MinMaxScaler, # rescaleaza la [0, 1], pentru input-uri marginite
RobustScaler, # foloseste mediana si IQR, pentru date cu outlieri
OneHotEncoder, # transforma categoriile in coloane 0/1
OrdinalEncoder, # transforma categoriile in coduri intregi (foloseste doar daca exista ordine)
KBinsDiscretizer, # imparte valorile continue in intervale
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Lucrul de știut: modelele tree-based nu au nevoie de scaling. Ele împart pe praguri, iar un prag pe feature * 1 e același cu un prag pe feature * 1000. Modelele liniare, neural nets, KNN și SVM-urile au nevoie de scaling; fără el, feature-ul cu cel mai mare interval numeric domină tot.
Pentru feature-uri categorice, regula de bun-simț e: cardinalitate joasă (sub ~20 de categorii) primește OneHotEncoder; cardinalitatea înaltă cere target encoding sau trucuri de hashing, pe care le acoperim în lecția 50. Nu folosi niciodată OrdinalEncoder pe o feature care nu are ordine naturală: ["red", "green", "blue"] care devine [0, 1, 2] va minți modelul despre distanțele dintre categorii.
Train/test split
Nu evaluezi un model pe datele din care a învățat. Dacă o faci, primești eroarea de training, care arată mereu fantastic și nu-ți spune nimic despre viitor. Split-ul standard:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42, # seteaza pentru reproductibilitate
stratify=y, # pastreaza proportiile de clase in ambele split-uri, pentru clasificare
)
Chestia cu random_state=42 e o glumă internă, dar e și mișcarea corectă: vrei același split de fiecare dată când rulezi scriptul, altfel debug-ul devine imposibil. Argumentul stratify=y contează mai mult decât realizează lumea: fără el, pe seturi dezechilibrate poți ajunge cu un test set care are, să zicem, 2% din clasa minoritară, în timp ce setul de training are 5%, iar evaluarea ta va fi strâmbă.
Pentru date time-series, nu folosi asta. Folosește TimeSeriesSplit sau pur și simplu taie după dată, split-urile aleatoare scurg viitorul în trecut.
Cross-validation
Un singur split train/test e zgomotos. Cross-validation o face de k ori, mediind rezultatul:
from sklearn.model_selection import cross_val_score
model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
print(scores) # array de 5 scoruri
print(scores.mean(), "+/-", scores.std())
cv=5 e standardul. Pentru clasificare face stratified k-fold by default. Pentru mai mult control, folosește direct StratifiedKFold și pasează-l ca cv.
Pentru tuning final de hyperparametri, GridSearchCV și RandomizedSearchCV înfășoară cross-validation în jurul unei grile sau al unui eșantion aleator de combinații de parametri:
from sklearn.model_selection import GridSearchCV
grid = GridSearchCV(
LogisticRegression(max_iter=1000),
param_grid={"C": [0.01, 0.1, 1.0, 10.0]},
cv=5,
scoring="roc_auc",
)
grid.fit(X_train, y_train)
print(grid.best_params_, grid.best_score_)
În 2026, pentru tuning serios de hyperparametri, majoritatea echipelor folosesc Optuna în loc, care face căutare bayesiană și gestionează mai bine logica de early stopping. Dar GridSearchCV e ok pentru probleme mici.
Pipeline-uri: pattern-ul de producție
Iată partea care face scikit-learn merită învățat chiar dacă în cele din urmă te muți la alte biblioteci. Un Pipeline înlănțuie pași de preprocessing cu un model într-un singur obiect care suportă .fit și .predict:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("scale", StandardScaler()),
("clf", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)
pipe.predict(X_test)
De ce contează asta: pipeline-ul garantează că scaler-ul e fit doar pe datele de training, apoi aplicat pe datele de test. Nu poți să scurgi accidental. Înseamnă și că tot lucrul se serializează ca un singur obiect: pickle.dump(pipe, ...) și ai un artefact deployabil.
Pentru preprocessing diferit pe coloane diferite, ColumnTransformer:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
numeric_features = ["age", "income", "tenure"]
categorical_features = ["country", "plan"]
preprocessor = ColumnTransformer([
("num", StandardScaler(), numeric_features),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
])
pipe = Pipeline([
("prep", preprocessor),
("clf", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)
handle_unknown="ignore" e important, lasă pipeline-ul tău să supraviețuiască categoriilor noi care apar în datele de producție. Fără el, o valoare nevăzută face predict să crape.
Combo-ul pipeline plus cross-validation e pattern-ul de producție. Înfășoară preprocessing-ul și modelul tău într-un Pipeline, pasează pipeline-ul către cross_val_score sau GridSearchCV și ai o evaluare cinstită care nu va minți despre ce poate face modelul tău pe date nevăzute.
scores = cross_val_score(pipe, X, y, cv=5, scoring="roc_auc")
print(scores.mean())
Alegerea metricii potrivite
O notă înainte să închidem: cross_val_score are default-ul accuracy pentru clasificatori, iar accuracy aproape niciodată nu e ce vrei pe date reale. Dacă 5% din userii tăi se duc la concurență și 95% nu, modelul care prezice mereu „nu pleacă” obține 95% accuracy și e inutil. Alege metrica potrivită pentru problema ta:
roc_auccând îți pasă de ranking-ul probabilităților, nu de praguri specifice. Default-ul pentru majoritatea clasificărilor binare.average_precisionmai bun decât ROC AUC când clasele sunt puternic dezechilibrate. Se concentrează pe trade-off-ul precision-recall.f1când ai nevoie de un singur număr care echilibrează precision și recall.neg_log_losscând contează probabilitățile însele, nu doar ranking-urile (de ex. pentru decizii calibrate în aval).neg_mean_squared_error,neg_mean_absolute_error,r2pentru regresie. Negația e o convenție scikit-learn ca „mai mare e mai bine” să funcționeze uniform.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(pipe, X, y, cv=5, scoring="average_precision")
Pentru clasificare cu clase dezechilibrate, cazul comun la fraud, churn, conversion, începe cu average_precision în loc de accuracy. Modelul tău va arăta mai prost pe hârtie și exact ăsta e scopul: acum numerele tale reflectă realitatea.
Persistarea și reîncărcarea
Odată ce ai un pipeline care merge, salvează-l. Unealta convențională e joblib, care gestionează NumPy arrays mai eficient decât pickle simplu:
import joblib
joblib.dump(pipe, "model.pkl")
# Mai tarziu, intr-un proces de serving:
pipe = joblib.load("model.pkl")
predictions = pipe.predict(X_new)
Fixează-ți versiunea de scikit-learn în pyproject.toml, modelele pickled pot eșua la încărcare între schimbări majore de versiune. În producție, pattern-ul standard e să livrezi fișierul cu modelul împreună cu codul care l-a încărcat: aceeași versiune Python, aceleași versiuni de biblioteci, fără surprize.
Asta e lecția. Estimatorii sunt .fit plus .predict. Transformatorii sunt .fit plus .transform. Înfășoară-le pe ambele într-un pipeline ca să nu scurgi. Folosește cross-validation ca să știi ce ai cu adevărat și alege o metrică ce reflectă problema ta. Lecția următoare, feature engineering, e unde se ascund majoritatea câștigurilor reale. Lecția 51, modelele tree-based, ca lumea, e unde se rezolvă majoritatea problemelor de ML tabular.