Getting Started
What is Machine Learning?
Machine learning builds systems that generalize from data rather than follow hand-written rules. The three paradigms (supervised, unsupervised, reinforcement) cover most real problems. Following a structured workflow — especially holding out a clean test set — prevents the cardinal sin of overfitting to your evaluation.
# Machine Learning: systems that learn patterns from data
# instead of being explicitly programmed.
# Three main paradigms:
# - Supervised: learn f: X -> y from labeled (X, y) pairs
# - Unsupervised: find structure in unlabeled X
# - Reinforcement: learn actions that maximize reward via trial-and-error
# Typical workflow:
# 1. Define problem & collect data
# 2. Preprocess & feature engineering
# 3. Split train / validation / test
# 4. Train model, tune hyperparameters
# 5. Evaluate on test set
# 6. Deploy & monitorCore Libraries
scikit-learn is the workhorse for tabular/classical ML — consistent fit/predict/transform API across all algorithms. NumPy and Pandas handle data wrangling; Matplotlib/Seaborn for visualization. For deep learning choose PyTorch (research-friendly) or TensorFlow/Keras (production-friendly) — both cover the same ground.
# The standard ML stack in Python
import numpy as np # numerical arrays
import pandas as pd # tabular data
import matplotlib.pyplot as plt # plotting
import seaborn as sns # statistical plots
# scikit-learn: classical ML (trees, SVM, linear models, preprocessing)
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# Deep learning (pick one):
import torch # PyTorch
# import tensorflow as tf # TensorFlow/Keras
# Install:
# pip install numpy pandas matplotlib seaborn scikit-learn torchTrain / Validation / Test Split
Never touch the test set until the final evaluation — using it for tuning leaks information and inflates your reported metrics. stratify=y preserves class balance, critical for imbalanced classification. random_state makes splits reproducible. For small datasets use k-fold CV instead of a fixed validation set.
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, # 20% held out for test
random_state=42, # reproducibility
stratify=y, # keep class proportions (classification only)
)
# For a third validation set:
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.4, stratify=y, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42)
# Now: 60% train, 20% val, 20% testFirst Model (Iris)
The Iris dataset is the 'Hello World' of ML — 150 flower samples, 4 features, 3 classes. This snippet shows the canonical sklearn pattern: load data, split, instantiate a model, fit, predict, evaluate. classification_report gives per-class precision/recall/F1, far more informative than accuracy alone for imbalanced problems.
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print("Accuracy:", accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred, target_names=iris.target_names))Reproducibility
Set seeds for numpy, random, and your DL framework so weight inits and shuffling are reproducible. Pass random_state to every stochastic sklearn estimator. Full determinism (especially on GPU) also needs deterministic algorithms and env flags — useful when debugging results that 'should' match but don't.
import numpy as np
import random
import torch
SEED = 42
np.random.seed(SEED)
random.seed(SEED)
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(SEED)
# For sklearn, pass random_state to each estimator:
# RandomForestClassifier(random_state=SEED)
# Force deterministic operations (slower but fully reproducible):
torch.use_deterministic_algorithms(True)
# Optional: pin GPU non-determinism
import os
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"Data Preprocessing
Handling Missing Values
Fitting an imputer on the full dataset leaks test statistics into training — always fit on train only. Median is more robust to outliers than mean. For categorical features use 'most_frequent' or 'constant'. KNN imputation can capture multivariate structure but is expensive on large data. Consider whether missingness itself is informative (add an 'is_missing' indicator).
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputer
df = pd.DataFrame({"age": [25, np.nan, 35, 45],
"income": [50000, 60000, np.nan, 80000]})
# Drop rows with any missing values (use sparingly)
df_drop = df.dropna()
# Mean/median/mode imputation
imp = SimpleImputer(strategy="median") # 'mean','most_frequent','constant'
df_imputed = pd.DataFrame(imp.fit_transform(df), columns=df.columns)
# KNN imputer (uses similar samples)
knn_imp = KNNImputer(n_neighbors=3)
df_knn = pd.DataFrame(knn_imp.fit_transform(df), columns=df.columns)
# Always fit imputers on TRAINING data only, then transform test
imp.fit(X_train)
X_train_imp = imp.transform(X_train)
X_test_imp = imp.transform(X_test) # uses train mediansScaling & Normalization
Many algorithms (SVM, KNN, logistic regression, neural nets, PCA) are scale-sensitive — tree models generally aren't. StandardScaler is the default; MinMaxScaler when you need bounded ranges (e.g. images); RobustScaler when outliers would distort StandardScaler. ALWAYS fit on train and transform test with the same fitted scaler — never fit_transform on test.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Standardization: mean 0, std 1 (most common)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # use train mean/std!
# Min-max: scale to [0, 1]
minmax = MinMaxScaler()
X_mm = minmax.fit_transform(X_train)
# Robust to outliers: uses median & IQR
robust = RobustScaler()
X_robust = robust.fit_transform(X_train)Encoding Categorical Variables
LabelEncoder is for the target; OneHotEncoder for nominal features (no inherent order — one-hot avoids implying red < green < blue). OrdinalEncoder is for ordered categories like sizes. handle_unknown='ignore' prevents errors on unseen categories at test time. High-cardinality categoricals (e.g. zip codes) are better handled with target/count encoding to avoid blowing up dimensionality.
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, OrdinalEncoder
# Label encode TARGET (classification): strings -> integers
le = LabelEncoder()
y = le.fit_transform(["cat", "dog", "cat", "bird"]) # [0, 1, 0, 2]
le.inverse_transform([0, 1, 2]) # back to strings
# One-hot encode FEATURES (nominal, no order)
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
X_ohe = ohe.fit_transform(df[["color"]]) # red/green/blue -> 3 cols
# Ordinal encode (ordered categories)
oe = OrdinalEncoder(categories=[["low", "medium", "high"]])
X_ord = oe.fit_transform(df[["size"]])
# For high-cardinality features, consider target encoding
# or just drop them if they're IDs.Handling Imbalanced Data
Imbalanced data skews models toward the majority class. The cleanest fix is class_weight='balanced' (no data duplication). SMOTE synthesizes new minority samples but can create noise — apply only to train. Oversampling and undersampling work but can overfit or lose data. Always evaluate with precision, recall, F1, and AUC — never accuracy alone on imbalanced data.
from sklearn.utils import resample
from imblearn.over_sampling import SMOTE
from sklearn.utils.class_weight import compute_class_weight
# 1. Class weights (no resampling — preferred for trees/linear)
classes = np.unique(y_train)
weights = compute_class_weight("balanced", classes=classes, y=y_train)
class_weight = dict(zip(classes, weights))
model = RandomForestClassifier(class_weight=class_weight)
# 2. Oversample minority
df_majority = df[df.y == 0]
df_minority = df[df.y == 1]
df_minority_up = resample(df_minority, replace=True,
n_samples=len(df_majority), random_state=42)
# 3. SMOTE: synthesize new minority samples via k-nearest neighbors
smote = SMOTE(random_state=42)
X_res, y_res = smote.fit_resample(X_train, y_train)
# Always resample on TRAINING data only — never the test set!Outlier Detection
Outliers distort means, standard deviations, and linear models. Z-score is fast but assumes normality and is itself distorted by outliers (use robust IQR instead). Isolation Forest handles high dimensions and non-linear boundaries. Decide whether to drop, cap (winsorize), or keep outliers depending on domain — sometimes outliers are exactly the cases you care about (fraud, anomalies).
import numpy as np
from sklearn.ensemble import IsolationForest
# Z-score: |x - mean| / std > 3 is an outlier
z_scores = np.abs((X - X.mean()) / X.std())
outliers = (z_scores > 3).any(axis=1)
# IQR method (more robust to outliers than z-score)
Q1, Q3 = X.quantile(0.25), X.quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = ((X < (Q1 - 1.5 * IQR)) | (X > (Q3 + 1.5 * IQR))).any(axis=1)
# Isolation Forest (good for high-dimensional data)
iso = IsolationForest(contamination=0.05, random_state=42)
outliers_iso = iso.fit_predict(X) # -1 = outlier, 1 = inlier
# Remove or cap (winsorize) outliers before training
X_clean = X[~outliers]Feature Engineering
Polynomial & Interaction Features
PolynomialFeatures lets linear models capture non-linear relationships by adding x^2, x*y, etc. as new features. interaction_only avoids pure squares, useful when you only want cross-terms. Feature count explodes combinatorially — always pair with regularization (Ridge/Lasso) to prevent overfitting, or use it after dimensionality reduction.
from sklearn.preprocessing import PolynomialFeatures
# Generate degree-2 features including interactions
poly = PolynomialFeatures(degree=2, interaction_only=False,
include_bias=False)
X_poly = poly.fit_transform(X)
# For X = [a, b], produces: [a, b, a^2, a*b, b^2]
# interaction_only=True keeps just [a, b, a*b] — no squares
poly_int = PolynomialFeatures(degree=2, interaction_only=True,
include_bias=False)
X_int = poly_int.fit_transform(X)
# Useful for linear models to capture non-linearity
# WARNING: feature count grows quickly — use with regularizationBinning & Discretization
Binning converts continuous features to discrete buckets, helping models capture non-linear effects and reducing sensitivity to outliers. Quantile bins (equal frequency) avoid the empty-bin problem of equal-width bins. Domain-specific bins (e.g. age groups) often beat automated ones. One-hot encode binned features so the model treats buckets as separate categories.
from sklearn.preprocessing import KBinsDiscretizer
import pandas as pd
# Equal-width bins (uniform spacing)
df["age_bin"] = pd.cut(df["age"], bins=5, labels=False)
# Equal-frequency bins (quantiles — same count per bin)
df["age_quantile"] = pd.qcut(df["age"], q=5, labels=False,
duplicates="drop")
# KBinsDiscretizer with strategy
kbd = KBinsDiscretizer(n_bins=5, encode="onehot-dense",
strategy="quantile") # 'uniform','kmeans'
X_binned = kbd.fit_transform(X[["age"]])
# Custom bins with domain knowledge
df["age_group"] = pd.cut(df["age"],
bins=[0, 18, 35, 60, 100],
labels=["child","young","adult","senior"])Text Feature Extraction
Bag-of-words counts are simple but treat all words equally; TF-IDF downweights words common across documents. ngram_range=(1,2) captures short phrases. For modern NLP use pretrained embeddings (sentence-transformers, BERT) — they capture semantics that bag-of-words cannot. Always set max_features and min_df to control vocabulary size and noise.
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
corpus = [
"machine learning is fun",
"deep learning is a subset of machine learning",
"python is great for data science",
]
# Bag of words (counts)
cv = CountVectorizer(max_features=1000, stop_words="english",
ngram_range=(1, 2))
X_counts = cv.fit_transform(corpus)
# TF-IDF: downweights common words
tfidf = TfidfVectorizer(max_features=1000, stop_words="english",
ngram_range=(1, 2), min_df=2)
X_tfidf = tfidf.fit_transform(corpus)
# For production, consider sentence-transformers for embeddings:
# from sentence_transformers import SentenceTransformer
# model = SentenceTransformer("all-MiniLM-L6-v2")
# embeddings = model.encode(corpus)Date & Time Features
Raw datetime columns are useless to models — extract meaningful parts (hour, day-of-week, month). Cyclic features (sin/cos) preserve continuity: hour 23 is close to hour 0, which raw integer encoding misses. is_weekend and holidays capture human-cycle patterns. days_since is a simple trend feature for time-series.
import pandas as pd
df["date"] = pd.to_datetime(df["date"])
# Extract calendar parts
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["day"] = df["date"].dt.day
df["dayofweek"] = df["date"].dt.dayofweek # 0=Mon
df["dayofyear"] = df["date"].dt.dayofyear
df["hour"] = df["date"].dt.hour
df["is_weekend"] = df["date"].dt.dayofweek >= 5
# Cyclic encoding (preserves continuity: hour 23 -> 0)
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
df["month_sin"] = np.sin(2 * np.pi * df["month"] / 12)
df["month_cos"] = np.cos(2 * np.pi * df["month"] / 12)
# Time elapsed (useful as a trend feature)
df["days_since"] = (df["date"] - df["date"].min()).dt.daysFeature Selection
More features isn't better — irrelevant ones add noise and overfitting risk. SelectKBest is fast and statistical; RFE is thorough but slow (retrains the model repeatedly); SelectFromModel uses a tree's feature_importances_ as a one-shot filter. Correlation filters remove redundant features cheaply. Always do selection on TRAINING data only and apply the same selector to test.
from sklearn.feature_selection import (SelectKBest, f_classif,
RFE, SelectFromModel)
from sklearn.ensemble import RandomForestClassifier
# 1. Univariate: keep top-k by statistical test
selector = SelectKBest(f_classif, k=10)
X_new = selector.fit_transform(X, y)
selected = selector.get_support(indices=True)
# 2. Recursive Feature Elimination (model-based, iterative)
rfe = RFE(RandomForestClassifier(random_state=42),
n_features_to_select=10)
X_rfe = rfe.fit_transform(X, y)
# 3. Select from model importance
sfm = SelectFromModel(RandomForestClassifier(random_state=42),
threshold="median")
X_sfm = sfm.fit_transform(X, y)
# 4. Correlation filter — drop one of highly-correlated pairs
corr = pd.DataFrame(X).corr().abs()
to_drop = [c for c in corr.columns
if any(corr[c] > 0.95)]
X_filtered = X.drop(columns=to_drop)Supervised Learning
Linear & Logistic Regression
Linear models are fast, interpretable, and a strong baseline. LogisticRegression uses L2 regularization by default; lower C means stronger regularization. For multiclass it uses softmax. Coefficients show feature importance but only after scaling — unscaled features make coefficients incomparable. Always start with a linear baseline before trying complex models.
from sklearn.linear_model import LinearRegression, LogisticRegression
# Regression: predict a continuous value
lr = LinearRegression()
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)
# Inspect coefficients
print("Coef:", lr.coef_, "Intercept:", lr.intercept_)
# Classification: predict a class (uses softmax for multiclass)
clf = LogisticRegression(
penalty="l2", # 'l1' (Lasso), 'elasticnet', None
C=1.0, # inverse of regularization strength
max_iter=1000,
multi_class="multinomial",
)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test) # class probabilitiesDecision Trees & Random Forests
Decision trees are interpretable but high-variance — small data changes yield very different trees. Random forests fix this by averaging many trees trained on bootstrapped samples with random feature subsets (bagging). max_features='sqrt' is the standard for classification. Feature importance from forests is biased toward high-cardinality features — try permutation_importance for a fairer view.
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
# Single tree (high variance, easy to overfit)
tree = DecisionTreeClassifier(
max_depth=5, # limit depth to prevent overfitting
min_samples_leaf=10, # min samples per leaf
random_state=42,
)
tree.fit(X_train, y_train)
# Random forest: ensemble of trees on bootstrapped samples
rf = RandomForestClassifier(
n_estimators=200, # number of trees
max_depth=None,
max_features="sqrt", # features per split
n_jobs=-1, # use all CPUs
random_state=42,
)
rf.fit(X_train, y_train)
# Feature importance (averaged across trees)
importances = pd.Series(rf.feature_importances_, index=X.columns)Gradient Boosting (XGBoost / LightGBM)
Gradient boosting builds trees sequentially, each correcting the previous's errors — usually the best performance on tabular data. sklearn's GradientBoosting is slow; HistGradientBoosting is its fast histogram-based equivalent. XGBoost and LightGBM are the production/competition standards with native missing-value handling and early stopping. Always use early_stopping with a validation set to find the optimal number of trees.
from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier
import lightgbm as lgb
import xgboost as xgb
# sklearn (slow on large data)
gbm = GradientBoostingClassifier(
n_estimators=200, learning_rate=0.1, max_depth=3, random_state=42)
gbm.fit(X_train, y_train)
# sklearn's faster histogram-based version (like LightGBM)
hgbm = HistGradientBoostingClassifier(
max_iter=200, learning_rate=0.1, max_leaf_nodes=31, random_state=42)
hgbm.fit(X_train, y_train)
# XGBoost (popular in competitions)
xgb_clf = xgb.XGBClassifier(
n_estimators=200, learning_rate=0.1, max_depth=6,
subsample=0.8, colsample_bytree=0.8, n_jobs=-1,
eval_metric="logloss", random_state=42)
xgb_clf.fit(X_train, y_train, eval_set=[(X_val, y_val)], early_stopping_rounds=10)
# LightGBM (fastest on large data)
lgb_clf = lgb.LGBMClassifier(n_estimators=200, learning_rate=0.1, num_leaves=31)
lgb_clf.fit(X_train, y_train, eval_set=(X_val, y_val))Support Vector Machines
SVMs find the maximum-margin hyperplane separating classes — powerful for small-to-medium datasets with clear boundaries. Feature scaling is mandatory (SVMs are distance-based). The RBF kernel handles non-linearity; tune C (regularization) and gamma (kernel width) via grid search. SVC scales poorly with samples (O(n^2)–O(n^3)) — for >50k samples use LinearSVC or a tree ensemble.
from sklearn.svm import SVC, SVR
from sklearn.preprocessing import StandardScaler
# SVC requires scaling!
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# Kernels: 'linear', 'rbf' (Gaussian), 'poly', 'sigmoid'
svm = SVC(
kernel="rbf",
C=1.0, # regularization (higher = fit harder)
gamma="scale", # kernel width
probability=True, # enable predict_proba (slower)
random_state=42,
)
svm.fit(X_train_s, y_train)
y_pred = svm.predict(X_test_s)
# SVR for regression
svr = SVR(kernel="rbf", C=1.0, epsilon=0.1)
svr.fit(X_train_s, y_train)k-Nearest Neighbors
KNN is the simplest ML algorithm: store the training data, then predict by voting among the k nearest neighbors. Scaling is essential (distance-based). Small k = low bias/high variance (overfits); large k = high bias/low variance (underfits). KNN is slow at prediction time on large datasets — use approximate nearest neighbors (FAISS, HNSW) for production.
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
# KNN: classify by majority vote of k nearest training samples
knn = KNeighborsClassifier(
n_neighbors=5, # k — try odd values to break ties
weights="uniform", # or 'distance' (closer = more weight)
metric="minkowski", # 'euclidean','manhattan', etc.
p=2, # 2=euclidean, 1=manhattan
n_jobs=-1,
)
knn.fit(X_train_scaled, y_train)
y_pred = knn.predict(X_test_scaled)
# Choose k via cross-validation
from sklearn.model_selection import cross_val_score
for k in [1, 3, 5, 7, 9, 11]:
score = cross_val_score(KNeighborsClassifier(n_neighbors=k),
X_train_scaled, y_train, cv=5).mean()
print(f"k={k}: {score:.3f}")Unsupervised Learning
K-Means Clustering
K-Means partitions data into k spherical clusters by minimizing within-cluster variance. n_init=10 runs the algorithm from 10 random inits and keeps the best (default changed in sklearn 1.4). Choose k via the elbow (inertia) or silhouette score (silhouette is more principled). K-Means assumes spherical, equal-sized clusters — use DBSCAN or Gaussian Mixture for non-spherical clusters.
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# Choose k via the elbow method or silhouette score
inertias, silhouettes = [], []
for k in range(2, 11):
km = KMeans(n_clusters=k, n_init=10, random_state=42)
labels = km.fit_predict(X)
inertias.append(km.inertia_)
silhouettes.append(silhouette_score(X, labels))
# Final model with chosen k
kmeans = KMeans(n_clusters=4, n_init=10, random_state=42)
clusters = kmeans.fit_predict(X)
centers = kmeans.cluster_centers_
# Predict cluster for new points
new_labels = kmeans.predict(X_new)DBSCAN (Density-Based)
DBSCAN finds clusters of arbitrary shape and automatically detects outliers (labeled -1) — no need to specify k. eps is the critical parameter; tune it via the k-distance plot (the 'elbow' of sorted k-nearest-neighbor distances). min_samples controls noise sensitivity. DBSCAN struggles with varying density — HDBSCAN handles that better. It can't predict new points directly; refit or use HDBSCAN's approximate_predict.
from sklearn.cluster import DBSCAN
# DBSCAN groups densely-connected points; marks sparse ones as noise (-1)
db = DBSCAN(
eps=0.5, # max distance between points in a cluster
min_samples=5, # min points to form a dense region
metric="euclidean",
)
labels = db.fit_predict(X)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = list(labels).count(-1)
# Tune eps via the k-distance plot (k = min_samples)
from sklearn.neighbors import NearestNeighbors
nn = NearestNeighbors(n_neighbors=5).fit(X)
distances, _ = nn.kneighbors(X)
distances = np.sort(distances[:, -1])
# DBSCAN doesn't have predict() — refit to predict on new data
# Or use HDBSCAN for a more robust, parameter-light alternativeHierarchical Clustering
Hierarchical clustering builds a tree (dendrogram) of nested clusters — useful when you want to understand cluster structure at multiple granularities. The dendrogram visually shows where to cut for any number of clusters. 'ward' linkage (minimizes within-cluster variance) is the most common; 'single' is brittle (chaining). AgglomerativeClustering doesn't scale well beyond ~10k samples — use K-Means or HDBSCAN for larger data.
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt
# Compute linkage matrix (for dendrogram)
Z = linkage(X, method="ward") # 'ward','complete','average','single'
# Plot dendrogram to choose number of clusters
plt.figure(figsize=(10, 5))
dendrogram(Z, truncate_mode="level", p=5)
plt.axhline(y=Z[-4, 2], color="r", linestyle="--") # cut at 4 clusters
plt.show()
# Agglomerative clustering with chosen k
agg = AgglomerativeClustering(
n_clusters=4,
linkage="ward", # minimizes variance
metric="euclidean",
)
labels = agg.fit_predict(X)Gaussian Mixture Models
GMM is a probabilistic counterpart to K-Means: each cluster is a Gaussian with its own mean and covariance, so it handles elliptical clusters and gives soft assignments (probabilities). covariance_type='full' is most flexible but parameter-heavy; 'diag' is a good compromise. Choose k via BIC (lower is better). GMM can also generate new samples — useful for data augmentation.
from sklearn.mixture import GaussianMixture
# GMM: probabilistic clustering — each cluster is a Gaussian
gmm = GaussianMixture(
n_components=4,
covariance_type="full", # 'spherical','tied','diag','full'
n_init=10,
random_state=42,
)
gmm.fit(X)
labels = gmm.predict(X)
probs = gmm.predict_proba(X) # soft clustering (responsibilities)
# Generative: sample new data
X_sampled, y_sampled = gmm.sample(100)
# Choose n_components via BIC (lower is better)
bics = []
for k in range(1, 11):
g = GaussianMixture(n_components=k, n_init=10, random_state=42).fit(X)
bics.append(g.bic(X))Anomaly Detection
Anomaly detection identifies rare, suspicious items — fraud, defects, intrusions. Isolation Forest is the general-purpose winner (fast, scales well, handles high dimensions). One-Class SVM works well when you have a clean 'normal' training set. LOF catches local anomalies that global methods miss. contamination sets the expected anomaly rate; tune it based on domain knowledge.
from sklearn.ensemble import IsolationForest
from sklearn.svm import OneClassSVM
from sklearn.neighbors import LocalOutlierFactor
# Isolation Forest: isolates anomalies with shallow trees
iso = IsolationForest(contamination=0.05, random_state=42)
labels = iso.fit_predict(X) # -1 anomaly, 1 normal
scores = iso.decision_function(X) # lower = more anomalous
# One-Class SVM: learns a boundary around normal data
ocsvm = OneClassSVM(kernel="rbf", nu=0.05, gamma="scale")
labels = ocsvm.fit_predict(X_train_normal)
# Local Outlier Factor: density-based (good for local anomalies)
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.05)
labels = lof.fit_predict(X)
# For new data: lof.negative_outlier_factor_ (lower = more anomalous)Regression
Linear Regression & Regularized Variants
OLS minimizes squared error without regularization — high variance on multicollinear data. Ridge (L2) shrinks coefficients smoothly, good when many features matter. Lasso (L1) drives some coefficients to zero, performing feature selection. ElasticNet blends both, useful when features are correlated. Always scale features before regularized regression so penalty is fair across features.
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
# Ordinary Least Squares
ols = LinearRegression()
ols.fit(X_train, y_train)
# Ridge (L2): shrinks coefficients, keeps all features
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)
# Lasso (L1): shrinks some coefficients to 0 (feature selection)
lasso = Lasso(alpha=0.1, max_iter=10000)
lasso.fit(X_train_scaled, y_train)
print("Non-zero coefs:", (lasso.coef_ != 0).sum())
# ElasticNet: mix of L1 and L2
en = ElasticNet(alpha=0.1, l1_ratio=0.5) # l1_ratio=1 -> pure Lasso
en.fit(X_train_scaled, y_train)Regression Evaluation Metrics
RMSE penalizes large errors (squared) and is in the same units as y — most common metric. MAE is more interpretable and robust to outliers. R2 is the fraction of variance explained; 0 means your model is no better than predicting the mean. MAPE gives percentage error but is undefined when y=0. Choose metrics based on business cost: if large errors are catastrophic, use RMSE; if all errors are equally costly, use MAE.
from sklearn.metrics import (mean_squared_error, mean_absolute_error,
r2_score, mean_absolute_percentage_error)
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred) # sensitive to outliers
rmse = np.sqrt(mse) # same units as y
mae = mean_absolute_error(y_test, y_pred) # robust to outliers
r2 = r2_score(y_test, y_pred) # 1=perfect, 0=mean, <0=worse
mape = mean_absolute_percentage_error(y_test, y_pred) # % error
print(f"RMSE: {rmse:.3f}")
print(f"MAE: {mae:.3f}")
print(f"R2: {r2:.3f}")
print(f"MAPE: {mape:.1%}")
# Adjusted R2 accounts for number of features
n, p = X_test.shape
adj_r2 = 1 - (1 - r2) * (n - 1) / (n - p - 1)Decision Tree Regression
Tree-based regressors make piecewise-constant predictions — they can't extrapolate beyond training data ranges. Random forests reduce variance via averaging; gradient boosting reduces bias via sequential correction. Quantile regression (loss='quantile') predicts a percentile rather than the mean — combine lower/upper quantile models for prediction intervals, useful when you need uncertainty estimates.
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
# Single tree (piecewise constant predictions)
dt = DecisionTreeRegressor(max_depth=5, min_samples_leaf=10,
random_state=42)
dt.fit(X_train, y_train)
# Random forest regressor (averages trees)
rf = RandomForestRegressor(n_estimators=200, max_features="sqrt",
n_jobs=-1, random_state=42)
rf.fit(X_train, y_train)
# Gradient boosting regressor
gb = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1,
max_depth=3, random_state=42)
gb.fit(X_train, y_train)
# Quantile regression — predict ranges instead of points
from sklearn.ensemble import GradientBoostingRegressor
gbr_lower = GradientBoostingRegressor(loss="quantile", alpha=0.1)
gbr_upper = GradientBoostingRegressor(loss="quantile", alpha=0.9)
gbr_lower.fit(X_train, y_train)
gbr_upper.fit(X_train, y_train)Regularization Path
The regularization path shows how Lasso coefficients shrink to zero as alpha increases — visualizing feature importance under different penalty strengths. Features that survive at high alpha are the most predictive. This is a great diagnostic for understanding which features matter and for picking a reasonable alpha range before cross-validation.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import lasso_path
# Compute Lasso coefficients for many alpha values
alphas, coefs, _ = lasso_path(X_train_scaled, y_train,
alphas=np.logspace(-3, 1, 100))
# Plot: each line is a coefficient vs alpha
plt.figure(figsize=(8, 5))
for coef in coefs:
plt.plot(alphas, coef)
plt.xscale("log")
plt.xlabel("alpha (regularization strength)")
plt.ylabel("coefficient")
plt.title("Lasso regularization path")
plt.axhline(0, color="k", lw=0.5)
plt.show()Residual Analysis
Residual plots diagnose model misspecification. A random cloud around 0 means the model fits well; a U-shape suggests missing non-linearity (add polynomial features or use a tree); a funnel shape indicates heteroscedasticity (try log-transforming y). Normality of residuals matters for inference (confidence intervals) but not for prediction accuracy.
import matplotlib.pyplot as plt
import seaborn as sns
residuals = y_test - y_pred
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 1. Residuals vs predicted — should look random around 0
axes[0].scatter(y_pred, residuals, alpha=0.5)
axes[0].axhline(0, color="r", linestyle="--")
axes[0].set_xlabel("Predicted"); axes[0].set_ylabel("Residual")
# 2. Residual distribution — should be ~normal
sns.histplot(residuals, kde=True, ax=axes[1])
axes[1].set_xlabel("Residual")
plt.show()
# Patterns in residuals suggest model issues:
# - U-shape: missing non-linearity (add polynomial features)
# - Funnel: heteroscedasticity (try log-transforming y)
# - Skew: try transforming y or use a different lossClassification
Binary Classification Basics
predict returns hard class labels (threshold 0.5); predict_proba returns probabilities — usually more useful. The default 0.5 threshold is rarely optimal: lower it for higher recall (catch more positives, e.g. cancer screening), raise it for higher precision (fewer false alarms, e.g. spam filter). ROC-AUC measures how well probabilities rank positives above negatives, independent of threshold.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (confusion_matrix, classification_report,
roc_auc_score, precision_recall_curve)
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)[:, 1] # P(class=1)
# Confusion matrix: TN FP / FN TP
cm = confusion_matrix(y_test, y_pred)
print(cm)
print(classification_report(y_test, y_pred))
# Adjust threshold (default 0.5) to trade precision for recall
y_pred_custom = (y_proba > 0.3).astype(int) # higher recall
# ROC-AUC: probability ranking quality (threshold-independent)
print("ROC-AUC:", roc_auc_score(y_test, y_proba))Confusion Matrix & Derived Metrics
Accuracy is misleading on imbalanced data — a 99%-negative dataset gives 99% accuracy by predicting all negatives. Precision and recall expose the precision-recall tradeoff: precision answers 'of predicted positives, how many are real?', recall answers 'of actual positives, how many did we catch?'. F1 is their harmonic mean. Use macro average when classes should be weighted equally, weighted when they should reflect their frequency.
from sklearn.metrics import (confusion_matrix, precision_score,
recall_score, f1_score)
# Binary: confusion_matrix returns [[TN, FP], [FN, TP]]
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
accuracy = (tp + tn) / (tp + tn + fp + fn)
precision = tp / (tp + fp) # of predicted positives, how many correct
recall = tp / (tp + fn) # of actual positives, how many caught
f1 = 2 * precision * recall / (precision + recall) # harmonic mean
specificity = tn / (tn + fp) # true negative rate
# sklearn shortcuts
print("Precision:", precision_score(y_test, y_pred))
print("Recall: ", recall_score(y_test, y_pred))
print("F1: ", f1_score(y_test, y_pred))
# Multi-class: average='macro' (unweighted) or 'weighted' (by class size)
print("Macro F1:", f1_score(y_test, y_pred, average="macro"))ROC & PR Curves
ROC curves plot recall (TPR) against false-positive rate across thresholds; the area under the curve (AUC) summarizes ranking quality (0.5 = random, 1.0 = perfect). PR curves are more informative than ROC on imbalanced data — ROC can look deceptively good when negatives dominate. Aim for the upper-left corner on ROC and upper-right on PR.