10. scikit-learn Models — Theory & Hyperparameters


10.1 Linear Regression

Intuition: Fit a hyperplane that minimises the sum of squared residuals between predictions and true values.

Closed-Form Solution (Normal Equation)

Requires to be invertible (fails with perfect multicollinearity). — slow when (features) is large.

Gradient Descent Variants

VariantBatch SizeProsCons
Batch GDAll Stable convergenceSlow on large data
Stochastic GD (SGD)1Fast, can escape local minimaNoisy, oscillates
Mini-Batch GD (e.g. 32)Best of bothMost commonly used
from sklearn.linear_model import LinearRegression, SGDRegressor
 
lr  = LinearRegression()   # uses normal equation (via SVD internally)
sgd = SGDRegressor(
    loss='squared_error',
    learning_rate='invscaling',  # 'constant', 'optimal', 'invscaling', 'adaptive'
    eta0=0.01,
    max_iter=1000,
    tol=1e-3,
    random_state=42
)

Assumptions (Gauss-Markov): Linearity, no perfect multicollinearity, homoscedasticity, no autocorrelation of errors, .


10.2 Ridge, Lasso & Elastic Net (Regularised Regression)

Regularisation adds a penalty on the magnitude of weights to prevent overfitting. The penalty controls a bias-variance tradeoff — larger penalty = simpler model = more bias, less variance.

Ridge (L2 Regularisation)

  • Shrinks all coefficients toward zero but rarely to exactly zero.
  • The penalty is differentiable everywhere → smooth optimisation.
  • Has a closed-form solution: — adding makes the matrix always invertible, fixing multicollinearity.
  • Use when: Many features each contribute a small amount.
from sklearn.linear_model import Ridge, RidgeCV
ridge = Ridge(alpha=1.0)
ridge_cv = RidgeCV(alphas=[0.01, 0.1, 1, 10, 100], cv=5)  # auto-tunes alpha

Lasso (L1 Regularisation)

  • Can drive some coefficients to exactly zero → automatic feature selection.
  • The penalty is non-differentiable at zero → solved via coordinate descent.
  • Produces sparse solutions.
  • Use when: Only a few features are truly relevant.
from sklearn.linear_model import Lasso, LassoCV
lasso = Lasso(alpha=0.1)
lasso_cv = LassoCV(cv=5, random_state=42)  # selects best alpha via CV

Elastic Net (L1 + L2)

Combines Ridge’s stability with Lasso’s sparsity. l1_ratio : 0 = Ridge, 1 = Lasso.

from sklearn.linear_model import ElasticNet
en = ElasticNet(alpha=0.1, l1_ratio=0.5)

Key Hyperparameter: alpha ()

  • alpha = 0 → plain Linear Regression.
  • alpha → stronger penalty → simpler model → risk of underfitting.
  • Use RidgeCV / LassoCV / ElasticNetCV to tune automatically.

TIP

Always scale features before Ridge/Lasso/ElasticNet. The penalty treats all coefficients equally, so unscaled features will be penalised unfairly.


10.3 Polynomial Regression

Intuition: Linear regression fits a line. To fit curves, augment features with polynomial terms, then run linear regression on the expanded feature space.

This is still linear in the parameters — just in a higher-dimensional feature space.

from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.linear_model import Ridge
 
pipe = Pipeline([
    ('poly',   PolynomialFeatures(degree=3, include_bias=False)),
    ('scaler', StandardScaler()),
    ('model',  Ridge(alpha=1.0))   # always regularise polynomial regression
])

WARNING

High degree → exponential feature explosion + severe overfitting. Always combine with regularisation and cross-validate the degree.


10.4 Logistic Regression

Intuition: Models the probability that a sample belongs to class 1 by squashing a linear combination of features through the sigmoid function.

Decision rule: if , else .

Loss — Binary Cross-Entropy (Log-Loss):

Multiclass: Sigmoid is replaced by softmax; loss becomes categorical cross-entropy. multi_class='ovr' trains binary classifiers; multi_class='multinomial' trains a joint softmax model.

from sklearn.linear_model import LogisticRegression
 
clf = LogisticRegression(
    C=1.0,                  # inverse of regularisation: C = 1/lambda
    penalty='l2',           # 'l1', 'l2', 'elasticnet', None
    solver='lbfgs',         # 'lbfgs' (L2), 'liblinear' (L1), 'saga' (all)
    max_iter=1000,
    class_weight='balanced',# corrects for class imbalance
    multi_class='auto',
    random_state=42
)

Key Hyperparameters

ParameterMeaningNotes
C — inverse regularisation strengthLarger C = less regularised
penaltyRegularisation type'l1' needs solver='liblinear' or 'saga'
solverOptimisation algorithm'saga' supports all penalties + large datasets
class_weightHandle imbalance'balanced' weights inversely to class frequency
max_iterConvergence iterationsIncrease if ConvergenceWarning

10.5 Support Vector Machines (SVM)

Intuition: Find the hyperplane that separates classes with the maximum margin. The margin is the gap between the hyperplane and the nearest training samples from each class — called support vectors because they “support” (define) the decision boundary.

Equivalently:

Hard vs. Soft Margin

  • Hard margin: Requires perfect linear separability. Fails with any noise or overlap.
  • Soft margin: Allows violations using slack variables :

  • High : Penalises misclassifications heavily → narrow margin → risk overfitting.
  • Low : Allows more violations → wider margin → better generalisation.

The Kernel Trick

Data in the original space may not be linearly separable, but may be in a higher-dimensional space. Kernels compute the dot product in that space without explicitly transforming the data:

KernelFormulaUse when
LinearData is linearly separable; high-dimensional (text)
RBF (Gaussian)General-purpose default; smooth non-linear boundary
PolynomialModerate non-linearity; image classification
SigmoidRarely used
from sklearn.svm import SVC, SVR, LinearSVC
 
clf = SVC(
    kernel='rbf',
    C=1.0,
    gamma='scale',       # 'scale'=1/(n_features*Var(X)); 'auto'=1/n_features
    probability=True,    # enable predict_proba (uses Platt scaling; adds cost)
    class_weight='balanced',
    random_state=42
)
 
# For large datasets: LinearSVC is much faster than SVC(kernel='linear')
lsvc = LinearSVC(C=1.0, max_iter=2000)

Key Hyperparameters

ParameterEffect
CMargin/misclassification tradeoff — high C = harder boundary
kernelShape of decision boundary
gammaRBF bandwidth — high gamma = narrow Gaussian = complex boundary (overfit)
degreePolynomial kernel degree

IMPORTANT

SVMs are highly sensitive to feature scale — always StandardScale before fitting. They also don’t natively produce probability estimates; probability=True adds a calibration step (Platt scaling) that increases training time.


10.6 Decision Trees

Intuition: Recursively partition the feature space by asking binary yes/no questions. At each internal node, pick the feature and threshold that best separates the classes. Leaves contain the predicted value.

Splitting Criteria

Gini Impurity (default for DecisionTreeClassifier): Ranges from 0 (pure) to (maximally impure).

Entropy / Information Gain:

MSE Reduction (for regression trees): split that minimises within-child variance.

Gini and Entropy produce nearly identical results in practice. Gini is slightly faster to compute (no log).

from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, export_text, plot_tree
 
clf = DecisionTreeClassifier(
    criterion='gini',         # 'gini' or 'entropy'
    max_depth=5,              # None = grow fully (overfits)
    min_samples_split=10,     # min samples to split an internal node
    min_samples_leaf=5,       # min samples to be a leaf
    max_features=None,        # features to consider at each split
    ccp_alpha=0.0,            # post-pruning: higher = more pruning
    class_weight='balanced',
    random_state=42
)
 
clf.fit(X_train, y_train)
 
# Visualise the tree
print(export_text(clf, feature_names=list(X_train.columns)))

Hyperparameter Effects

Parameter↑ value →Risk
max_depthMore complexOverfitting
min_samples_splitSimplerUnderfitting
min_samples_leafSimplerUnderfitting
ccp_alphaMore prunedUnderfitting

Feature Importance

importances = pd.Series(clf.feature_importances_, index=X_train.columns)
importances.sort_values(ascending=False).head(10).plot(kind='bar')

Advantages: Interpretable, no scaling needed, handles mixed types, captures non-linear relationships. Disadvantages: High variance (small data changes → completely different tree), overfits without pruning.


10.7 Ensemble Methods — The Big Picture

The core idea: a single model has high variance or high bias. Combining many models can simultaneously reduce both.

MethodHow models are combinedWhat it reducesBase learner
BaggingParallel training on bootstrap samples; aggregate by voting/averagingVarianceAny
Random ForestBagging + random feature subsets at each splitVariance (more than bagging alone)Decision Trees
AdaBoostSequential; reweight samples based on previous errorsBiasStumps (depth-1 trees)
Gradient BoostingSequential; each tree fits the negative gradient (residuals) of lossBias + VarianceShallow trees
VotingAverage/majority vote of diverse modelsBothAny diverse set
StackingMeta-learner trained on base model predictionsBothAny

10.8 Bagging (Bootstrap AGGregatING)

Algorithm:

  1. Draw bootstrap samples from training data (sampling with replacement).
  2. Train one base learner on each sample independently (can be parallelised).
  3. Aggregate: majority vote (classification) or average (regression).

Why it works: Each model has high variance individually. Because each is trained on a different random sample, their errors are uncorrelated. The average of uncorrelated models with variance has variance .

Out-of-Bag (OOB) Error: Each bootstrap sample leaves out ~37% of the original data (on average). These left-out samples can be used as a free validation set — no need for a separate val split.

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
 
bag = BaggingClassifier(
    estimator=DecisionTreeClassifier(max_depth=None),
    n_estimators=100,
    max_samples=1.0,       # fraction of samples per bootstrap
    max_features=1.0,      # fraction of features per bootstrap
    bootstrap=True,        # True = bagging; False = pasting (no replacement)
    oob_score=True,        # compute OOB score
    n_jobs=-1,
    random_state=42
)
bag.fit(X_train, y_train)
print(f"OOB Score: {bag.oob_score_:.4f}")

10.9 Random Forest

Random Forest = Bagging + Random Feature Subsets at each split.

While vanilla Bagging draws bootstrap samples of rows, every tree still evaluates all features at every node split. If one feature is extremely dominant (e.g. income), every tree will pick income as its root split, making the trees highly correlated and limiting variance reduction.

Random Forest solves this by randomly sampling features ( for classification, for regression) at every single node split inside every tree.

Why Feature Sampling at Each Split Works (Mathematical Proof & Intuition)

The variance of an ensemble of trees, each with variance and pairwise correlation , is:

  • As , the second term .
  • The remaining irreducible ensemble variance is .

In Vanilla Bagging, because strong features appear at the top of almost every tree, the correlation remains high (~0.5–0.7).
In Random Forest, forcing trees to select from a random subset of features at every node prevents dominant features from appearing everywhere. This drives down significantly (~0.1–0.2), enabling much greater total variance reduction.

from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
 
rf = RandomForestClassifier(
    n_estimators=200,       # number of trees; more is generally better (diminishing returns)
    max_depth=None,         # None = grow fully; limit to prevent overfitting
    max_features='sqrt',    # 'sqrt' (classification), 1.0 (all features = vanilla bagging), or 'log2'
    min_samples_split=2,
    min_samples_leaf=1,
    bootstrap=True,
    oob_score=True,         # free validation via out-of-bag samples
    class_weight='balanced',
    n_jobs=-1,
    random_state=42
)
 
rf.fit(X_train, y_train)
print(f"OOB accuracy: {rf.oob_score_:.4f}")
print(f"Test accuracy: {rf.score(X_test, y_test):.4f}")
 
# Feature importances
importances = pd.Series(rf.feature_importances_, index=X_train.columns)
print(importances.sort_values(ascending=False).head(10))

Key Hyperparameters

ParameterEffect
n_estimatorsMore trees = lower variance (diminishing returns past ~200)
max_depthLimit tree growth to prevent overfitting
max_featuresLower = more decorrelation between trees = lower variance
min_samples_leafLarger = smoother predictions (regression), less overfit
oob_scoreFree performance estimate on unseen data

TIP

Random Forest’s OOB score is an unbiased estimate of generalisation performance — often close to 5-fold CV but computed for free as a byproduct of training. Use it as a quick sanity check.


10.10 AdaBoost (Adaptive Boosting)

Intuition: Train a sequence of weak learners (typically decision stumps — 1-split decision trees). After each round:

  1. Sample weights () are updated: misclassified samples get higher weights, forcing the next learner to focus on hard cases.
  2. Model weight () is calculated: more accurate weak learners receive higher voting power in the final decision.

Step-by-Step Mathematical Algorithm (AdaBoost.M1 for Classification )

  1. Initialise Sample Weights:

  2. For iteration : a. Fit weak learner using current sample weights . b. Compute total weighted error : c. Compute model voting weight : (If is small, is large and positive. If , .) d. Update sample weights:

    • Correctly classified (): (weight drops).
    • Misclassified (): (weight rises). e. Renormalise weights: so they sum to 1.
  3. Final Ensemble Prediction:


Step-by-Step Numerical Toy Example

Consider 5 samples in 1D space with binary labels :

Sample Feature Label Initial Weight
11+10.20
22+10.20
33-10.20
44+10.20
55-10.20

Round 1 ()

  • Selected Stump : Split at , else .
    • Predictions: .
    • Sample 4 () is misclassified; all others correct.
  • Weighted Error: .
  • Model Weight :
  • Update Weights:
    • Correct ():
    • Incorrect ():
    • Sum of raw weights = .
  • Normalised Weights : .
    (Sample 4 now holds 50% of total sample weight!)

Round 2 ()

  • Selected Stump : Forced to fix Sample 4 split at , else .
    • Predictions: .
    • Sample 3 () is misclassified; Sample 4 is now correct!
  • Weighted Error: .
  • Model Weight :

Ensemble Output for Sample 4 ()

  • , .
  • Combined Score: .
  • ✅ — Stump 2’s higher weight () overrode Stump 1’s mistake.

AdaBoost for Regression (AdaBoost.R2)

In regression (), errors are continuous rather than binary:

  1. Relative Error ():
  2. Average Model Error (): .
  3. Confidence Factor (): .
  4. Sample Weight Update: (Small error weight multiplied by , decreasing it).
  5. Final Prediction: Takes the Weighted Median of all weak trees using tree weights , ensuring robustness against outlier predictions.
from sklearn.ensemble import AdaBoostClassifier, AdaBoostRegressor
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
 
ada_clf = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1),  # decision stump
    n_estimators=200,
    learning_rate=1.0,
    algorithm='SAMME.R',  # uses real probabilities instead of discrete predictions
    random_state=42
)
 
ada_reg = AdaBoostRegressor(
    estimator=DecisionTreeRegressor(max_depth=3),
    n_estimators=100,
    learning_rate=1.0,
    loss='linear',        # 'linear', 'square', or 'exponential'
    random_state=42
)
HyperparameterEffect
n_estimatorsNumber of boosting rounds; too many can overfit
learning_rateShrinks contribution of each weak learner; lower rate requires more estimators
estimatorBase learner (default: max_depth=1 stump for clf, max_depth=3 for reg)
loss (Regressor)Relative loss formulation (linear, square, exponential)

Pros: Simple, fast, low hyperparameter tuning required; often beats Random Forest on clean data.
Cons: Extremely sensitive to noise and outliers (noisy samples keep getting upweighted exponentially).


10.11 Gradient Boosting

Intuition: Instead of adjusting sample weights like AdaBoost, each new tree in Gradient Boosting directly fits the negative gradient of the loss function (the pseudo-residuals) with respect to the current model’s predictions.


Gradient Descent in Weight Space vs. Function Space

TechniqueWhere Optimization HappensUpdate Rule
Standard Gradient DescentParameter space (weights )
Gradient BoostingFunction space (predictions )

Here, the new decision tree is trained to approximate the negative gradient .


Mathematical Algorithm (Regression with MSE Loss)

Given dataset and learning rate :

  1. Initialise Constant Base Prediction:

  2. For iteration : a. Compute pseudo-residuals for all samples: (For MSE Loss , pseudo-residual is simply ). b. Fit a regression tree to predict targets . c. Update the ensemble prediction:


Step-by-Step Numerical Toy Example

Predict House Price (₹ Lakhs) based on Size (sq ft) with learning rate :

House (Size in sq ft) (Price in ₹ Lakhs)
150030
2100050
3150070

Step 1: Base Prediction

Predictions: .

Round 1 ()

  1. Pseudo-Residuals :
    • House 1 ():
    • House 2 ():
    • House 3 ():
  2. Fit Tree to :
    • Split at :
      • Left (): Houses 1, 2
      • Right (): House 3
  3. Update Ensemble Prediction :
    • House 1:
    • House 2:
    • House 3:

Round 2 ()

  1. Pseudo-Residuals :
    • House 1:
    • House 2:
    • House 3:
  2. Fit Tree to :
    • Split at :
      • Left (): House 1
      • Right (): Houses 2, 3
  3. Update Ensemble Prediction :
    • House 1:
    • House 2:
    • House 3:

Notice how residuals shrink with each round as predictions move closer to true .


from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor, HistGradientBoostingClassifier
 
gb = GradientBoostingClassifier(
    n_estimators=300,
    learning_rate=0.05,   # shrinkage — lower = needs more trees
    max_depth=4,          # typically 3–6 for GBM
    subsample=0.8,        # stochastic GB: use 80% of samples per tree (reduces variance)
    max_features='sqrt',  # random feature subsets at each split
    min_samples_leaf=10,
    random_state=42
)
 
# HistGradientBoostingClassifier: faster sklearn implementation (like LightGBM)
hgb = HistGradientBoostingClassifier(
    max_iter=300,
    learning_rate=0.05,
    max_depth=4,
    l2_regularization=0.1,
    early_stopping=True,   # uses a validation set to stop early
    random_state=42
)

The Learning Rate — n_estimators Tradeoff

Lower learning_rate → each tree contributes less → model changes slowly → need more trees but generalises better. Rule of thumb: set learning_rate low (0.01–0.1) and use early stopping to find the right n_estimators.


10.12 XGBoost

XGBoost (Extreme Gradient Boosting) is a highly optimised implementation of gradient boosting with several algorithmic improvements:

  1. Regularised objective: Adds L1 () and L2 () penalties on leaf weights directly in the loss: where = number of leaves, = leaf weights.

  2. Second-order Taylor expansion: Uses both gradient (first derivative) and Hessian (second derivative) of the loss for more accurate tree fitting.

  3. Column (feature) subsampling — like Random Forest, reducing overfitting.

  4. Sparsity-aware split finding — handles missing values natively.

  5. Histogram-based approximate splitting — much faster than exact greedy.

import xgboost as xgb
 
xgb_clf = xgb.XGBClassifier(
    n_estimators=500,
    learning_rate=0.05,
    max_depth=5,
    subsample=0.8,         # row subsampling per tree
    colsample_bytree=0.8,  # feature subsampling per tree
    colsample_bylevel=0.8, # feature subsampling per level
    reg_alpha=0.1,         # L1 on leaf weights
    reg_lambda=1.0,        # L2 on leaf weights
    gamma=0.0,             # min loss reduction to make a split
    min_child_weight=1,    # min sum of hessian in a child (controls overfitting)
    scale_pos_weight=1,    # for imbalanced: set to neg/pos ratio
    eval_metric='logloss',
    early_stopping_rounds=20,
    use_label_encoder=False,
    random_state=42,
    n_jobs=-1
)
 
# Fitting with early stopping
eval_set = [(X_val, y_val)]
xgb_clf.fit(X_train, y_train, eval_set=eval_set, verbose=50)

Key Hyperparameters

ParameterEffect
n_estimatorsNumber of boosting rounds
learning_rateShrinkage per tree
max_depthTree depth; 3–8 typical
subsampleRow subsampling (0.5–1.0)
colsample_bytreeFeature subsampling per tree
reg_alpha / reg_lambdaL1/L2 regularisation
gammaMin impurity reduction for a split
min_child_weightLarger = more conservative splits
early_stopping_roundsStop if val metric doesn’t improve for N rounds

10.13 LightGBM

LightGBM (Light Gradient Boosting Machine) by Microsoft. Key innovations over XGBoost:

  1. Leaf-wise (best-first) tree growth instead of level-wise: grows the leaf with the greatest loss reduction first → deeper, more asymmetric trees → faster convergence.
  2. Histogram-based binning: bins continuous features into ~256 buckets, dramatically reducing split-finding cost.
  3. GOSS (Gradient-based One-Side Sampling): keeps samples with large gradients (hard examples) and randomly samples from those with small gradients → faster without losing accuracy.
  4. EFB (Exclusive Feature Bundling): bundles mutually exclusive sparse features → reduces effective feature count.
import lightgbm as lgb
 
lgb_clf = lgb.LGBMClassifier(
    n_estimators=500,
    learning_rate=0.05,
    max_depth=-1,           # -1 = no limit; control via num_leaves instead
    num_leaves=31,          # key parameter: 2^max_depth is a good upper bound
    subsample=0.8,
    colsample_bytree=0.8,
    reg_alpha=0.1,
    reg_lambda=1.0,
    min_child_samples=20,   # min samples in a leaf
    class_weight='balanced',
    random_state=42,
    n_jobs=-1
)
 
lgb_clf.fit(
    X_train, y_train,
    eval_set=[(X_val, y_val)],
    callbacks=[lgb.early_stopping(stopping_rounds=20), lgb.log_evaluation(50)]
)

LightGBM vs. XGBoost:

LightGBMXGBoost
Tree growthLeaf-wise (asymmetric)Level-wise (symmetric)
SpeedFaster on large datasetsSlightly slower
MemoryLowerHigher
Hyperparameter to control depthnum_leavesmax_depth
Categorical supportNative (categorical_feature=)Requires encoding

10.14 CatBoost

CatBoost (short for Categorical Boosting) by Yandex addresses two primary flaws in traditional gradient boosting: target leakage in categorical encoding and prediction drift in boosting tree construction.


1. Ordered Target Encoding

Standard Target Encoding replaces a category with the mean target value of that category in the training set. However, using sample ‘s target to calculate sample ‘s own feature encoding causes severe target leakage.

CatBoost’s Solution: For sample at position in a random permutation , compute its target statistic using only samples appearing BEFORE in that permutation:

Where is the global target mean (prior) and is a smoothing weight. Because sample ‘s target is never used to compute its own feature encoding, leakage is completely eliminated.


2. Ordered Boosting (Preventing Prediction Drift)

In standard boosting, the pseudo-residual is computed using a model trained on a dataset that already included sample . Because slightly memorises , is artificially small, causing prediction drift.

CatBoost’s Solution: Train separate auxiliary prefix models along a random permutation :

  • Model is trained only on samples through .
  • To compute the residual for sample , CatBoost queries .
  • Since has never seen sample , the residual is an honest, out-of-sample error.

Step-by-Step Numerical Toy Example of Ordered Boosting

Consider 4 samples in a random permutation order:

OrderSample (Feature) (Target)
1stA110
2ndB220
3rdC330
4thD440
  1. Train Supported Prefix Models:

    • : Trained only on
    • : Trained only on
    • : Trained only on
  2. Compute Unbiased Residuals:

    • For Sample B: Residual ( has never seen ).
    • For Sample C: Residual ( has never seen ).
    • For Sample D: Residual ( has never seen ).
  3. Train Next Tree: The next boosting tree is trained on these unbiased out-of-sample residuals , preventing target leakage and prediction drift.


3. Symmetric (Oblivious) Trees

CatBoost uses Oblivious Trees, where every node at depth uses the exact same feature and split threshold.

                  Standard Tree                     Symmetric (Oblivious) Tree
              [ Age ≤ 30 ]                                [ Age ≤ 30 ]
             /            \                              /            \
    [ Income ≤ 50k ]   [ Zip ≤ 90210 ]          [ Income ≤ 50k ]   [ Income ≤ 50k ]
  • Execution Speed: Predictions evaluate as bitwise operations/lookup tables, enabling up to 8x faster CPU/GPU inference.
  • Regularisation: Forces balanced, symmetric tree structures, preventing deep overfitted branches.

from catboost import CatBoostClassifier
 
cat_clf = CatBoostClassifier(
    iterations=500,
    learning_rate=0.05,
    depth=6,
    l2_leaf_reg=3,          # L2 regularisation
    cat_features=['city', 'gender', 'occupation'],  # pass raw string columns!
    eval_metric='AUC',
    early_stopping_rounds=50,
    random_seed=42,
    verbose=100
)
 
cat_clf.fit(X_train, y_train, eval_set=(X_val, y_val))

Why CatBoost shines: You can pass raw string categorical columns directly — no encoding needed. It handles the encoding internally in a leakage-free way.

Boosting Libraries Comparison

sklearn GBMXGBoostLightGBMCatBoost
SpeedSlowFastFastestFast
Native categoricalsPartial
Missing values
GPU support
Tree structureAsymmetricSymmetric (level-wise)Asymmetric (leaf-wise)Oblivious (Symmetric)
Leakage-free categoricals

10.15 Voting Classifier & Regressor

Combines predictions of diverse models. Works best when models make uncorrelated errors.

from sklearn.ensemble import VotingClassifier, VotingRegressor
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
 
voting = VotingClassifier(
    estimators=[
        ('lr',  LogisticRegression(max_iter=1000)),
        ('rf',  RandomForestClassifier(n_estimators=100, random_state=42)),
        ('svc', SVC(probability=True, kernel='rbf'))
    ],
    voting='soft',   # 'hard': majority vote; 'soft': average probabilities (usually better)
    weights=[1, 2, 1]  # optional: weight models by their quality
)
 
voting.fit(X_train, y_train)

Hard voting — each model votes for a class; majority wins. Soft voting — average the predicted probabilities; pick the class with the highest average. Requires all models to support predict_proba. Usually better because it accounts for prediction confidence.


10.16 Stacking

Trains a meta-learner (blender) on the out-of-fold predictions of the base models. The meta-learner learns how to combine the base predictions optimally.

from sklearn.ensemble import StackingClassifier
 
stacking = StackingClassifier(
    estimators=[
        ('rf',  RandomForestClassifier(n_estimators=100, random_state=42)),
        ('svc', SVC(probability=True, kernel='rbf')),
        ('lgr', LogisticRegression(max_iter=1000))
    ],
    final_estimator=LogisticRegression(),   # meta-learner
    cv=5,              # base models generate OOF predictions via 5-fold CV
    stack_method='predict_proba',  # 'predict_proba' or 'predict'
    passthrough=False  # if True, pass original features to meta-learner too
)
 
stacking.fit(X_train, y_train)

Why OOF predictions? If base models predict on the same data they were trained on, the meta-learner sees over-fitted predictions. Using cross-validation ensures the meta-learner trains on out-of-fold predictions — what the base models will produce on truly unseen data.


10.17 K-Nearest Neighbours (KNN)

Intuition: A lazy learner — no training phase at all. To predict a new sample, find its nearest neighbours in the stored training data and aggregate their labels.

Distance metrics:

  • Euclidean (): — most common
  • Manhattan (): — more robust in high dimensions
  • Minkowski (general):
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
 
knn = KNeighborsClassifier(
    n_neighbors=5,
    metric='minkowski',
    p=2,               # p=2 → Euclidean; p=1 → Manhattan
    weights='uniform', # 'uniform': equal weight; 'distance': closer = more weight
    algorithm='auto',  # 'auto', 'ball_tree', 'kd_tree', 'brute'
    n_jobs=-1
)

Choosing

  • Small (e.g., 1, 3): Very local → captures fine-grained patterns → high variance, sensitive to noise.
  • Large (e.g., 50, 100): Very smooth → high bias, ignores local structure.
  • Rule of thumb: Start with ; tune via CV.
HyperparameterEffect
n_neighborsLower → more complex boundary
weights'distance' helps when nearby points are more relevant
metricEuclidean for numeric; Manhattan for sparse/high-dimensional

IMPORTANT

KNN is at prediction time — every new query scans all training points. For large datasets, use approximate nearest-neighbour libraries (FAISS, Annoy) or algorithm='ball_tree'/'kd_tree' for exact but faster search. Always scale features first.


10.18 Naïve Bayes

Intuition: Apply Bayes’ theorem with the strong (“naïve”) assumption that all features are conditionally independent given the class.

The log is used to avoid underflow from multiplying many small probabilities.

Variants by Likelihood Model

| Variant | assumption | Use case | |:---|:---|:---| | GaussianNB | — Gaussian per feature per class | Continuous features | | MultinomialNB | Multinomial distribution over counts | Text classification (word counts, TF-IDF) | | BernoulliNB | Bernoulli (binary 0/1 per feature) | Binary text features (word present/absent) | | ComplementNB | Uses complement class statistics | Imbalanced text classification (often outperforms MultinomialNB) | | CategoricalNB | Categorical distribution per feature | Purely categorical tabular data |

from sklearn.naive_bayes import GaussianNB, MultinomialNB, BernoulliNB, ComplementNB
 
gnb = GaussianNB(var_smoothing=1e-9)   # adds small variance to prevent zero probabilities
mnb = MultinomialNB(alpha=1.0)         # alpha = Laplace smoothing (prevents zero likelihoods)
bnb = BernoulliNB(alpha=1.0, binarize=0.0)  # binarize: threshold for binary features

Laplace / Additive Smoothing (alpha): If a word never appeared in class during training, , making the entire product zero. Smoothing adds a small count to every feature count.

Pros: Extremely fast ( training), works well on small data, naturally multiclass, robust to irrelevant features. Cons: The independence assumption is almost always violated; correlated features cause systematic bias.


10.19 Multi-Layer Perceptron (MLP)

Intuition: A neural network made of layers of neurons. Each neuron computes a weighted sum of its inputs, applies a non-linear activation function, and passes the result to the next layer. By stacking many such layers, the network can approximate any function.

Architecture

Input Layer → [Hidden Layer 1] → [Hidden Layer 2] → ... → Output Layer
   x             h₁ = σ(W₁x + b₁)    h₂ = σ(W₂h₁ + b₂)       ŷ

Forward pass:

Activation Functions

FunctionFormulaPropertiesUse in
ReLUFast, no vanishing gradient for Hidden layers (default)
Leaky ReLUFixes “dying ReLU” (zero gradient for )Hidden layers
TanhOutput in , zero-centredHidden layers (older)
SigmoidOutput in Binary output layer
SoftmaxOutputs sum to 1 (probability distribution)Multiclass output layer
Linear / IdentityNo non-linearityRegression output layer

Backpropagation

The loss gradient is propagated backward through the chain rule:

This is computed efficiently layer-by-layer using the chain rule. The computed gradients are used by the optimiser to update weights.

Optimisers

OptimiserUpdate RuleNotes
SGDSimple; needs careful lr tuning
SGD + Momentum; Smoother convergence
AdamAdaptive per-parameter lr using first and second moment estimatesDefault for most deep learning; robust
lbfgsQuasi-Newton methodGood for small datasets; full-batch

MLPClassifier / MLPRegressor

from sklearn.neural_network import MLPClassifier, MLPRegressor
 
mlp = MLPClassifier(
    hidden_layer_sizes=(256, 128, 64),  # 3 hidden layers
    activation='relu',          # 'relu', 'tanh', 'logistic', 'identity'
    solver='adam',              # 'adam', 'sgd', 'lbfgs'
    alpha=1e-4,                 # L2 regularisation on weights
    batch_size='auto',          # 'auto' = min(200, n_samples) for adam/sgd
    learning_rate='adaptive',   # 'constant', 'invscaling', 'adaptive' (sgd only)
    learning_rate_init=1e-3,    # initial learning rate
    max_iter=500,
    early_stopping=True,        # hold out 10% of train as validation set
    validation_fraction=0.1,
    n_iter_no_change=10,        # stop if val score doesn't improve for 10 epochs
    random_state=42
)

Key Hyperparameters

ParameterEffect
hidden_layer_sizesNetwork architecture — more/deeper layers → more capacity
activationNon-linearity; 'relu' is almost always the right default
solver'adam' for most cases; 'lbfgs' for small datasets
alphaL2 regularisation — controls overfitting
learning_rate_initStep size for weight updates
early_stoppingPrevents overfitting using a validation hold-out

IMPORTANT

MLP is sensitive to feature scale — always StandardScale before fitting. Also, initialisation is random — results vary across runs unless random_state is fixed. For serious deep learning work, use PyTorch or TensorFlow instead of sklearn’s MLP.


10.20 K-Means Clustering

Intuition: Partition samples into clusters by iteratively assigning each sample to its nearest centroid and updating centroids.

Algorithm:

1. Initialise K centroids (randomly or via K-Means++)
2. Repeat until convergence:
   a. Assign each sample to the nearest centroid (Voronoi assignment)
   b. Update each centroid = mean of all samples assigned to it

Objective (Inertia = Within-Cluster Sum of Squares):

K-Means++ Initialisation: Instead of random initialisation, choose each subsequent centroid with probability proportional to its squared distance from the nearest already-chosen centroid. This gives better starting points and faster convergence.

from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
 
km = KMeans(
    n_clusters=5,
    init='k-means++',  # 'k-means++' (smart init) or 'random'
    n_init=10,         # run 10 times with different seeds; keep the best
    max_iter=300,
    tol=1e-4,
    random_state=42
)
 
km.fit(X_scaled)
labels   = km.labels_          # cluster assignment for each sample
centers  = km.cluster_centers_ # centroid coordinates
inertia  = km.inertia_         # total WCSS
 
# Predict cluster for new data
new_labels = km.predict(X_new)

Choosing K — Elbow Method

inertias = []
K_range = range(2, 15)
for k in K_range:
    km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
    km.fit(X_scaled)
    inertias.append(km.inertia_)
 
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('K'); plt.ylabel('Inertia'); plt.title('Elbow Method')
# Pick K at the "elbow" — where the rate of decrease slows down sharply

Silhouette Score

Measures how tight and well-separated clusters are. For sample : where = mean distance to other samples in the same cluster, = mean distance to samples in the nearest different cluster.

Range: ; higher is better. → well-clustered; → on boundary; → misassigned.

from sklearn.metrics import silhouette_score, silhouette_samples
 
# Overall score
sil = silhouette_score(X_scaled, km.labels_)
print(f"Silhouette Score: {sil:.4f}")
 
# Per-sample scores (plot to identify poorly clustered samples)
sample_sil = silhouette_samples(X_scaled, km.labels_)

Limitations of K-Means: Assumes spherical clusters of equal size; sensitive to outliers (which pull centroids); must specify upfront; non-deterministic (use n_init > 1).


10.21 DBSCAN

Intuition: Density-Based Spatial Clustering. Groups together samples in dense regions; marks sparse samples as noise. Can find arbitrarily shaped clusters; no need to specify .

Two parameters:

  • eps (): neighbourhood radius.
  • min_samples: minimum points in the -neighbourhood to be a core point.

Point types:

  • Core point: has min_samples points within eps (including itself).
  • Border point: within eps of a core point, but not a core point itself.
  • Noise point: not a core point and not reachable from any core point. Labelled .

Cluster formation: Start at an unvisited core point; expand by density-reachability (recursively add all points within eps).

from sklearn.cluster import DBSCAN
 
db = DBSCAN(
    eps=0.5,          # neighbourhood radius — tune with a k-distance plot
    min_samples=5,    # min points to form a core point
    metric='euclidean',
    n_jobs=-1
)
 
labels = db.fit_predict(X_scaled)
 
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise    = (labels == -1).sum()
print(f"Clusters: {n_clusters}, Noise points: {n_noise}")

Tuning eps — k-Distance Plot

from sklearn.neighbors import NearestNeighbors
import numpy as np
 
k = 5  # same as min_samples
nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled)
distances, _ = nbrs.kneighbors(X_scaled)
 
# Sort the k-th nearest neighbour distances
kth_distances = np.sort(distances[:, -1])[::-1]
plt.plot(kth_distances)
plt.xlabel('Points (sorted)'); plt.ylabel(f'{k}th nearest neighbour distance')
plt.title('k-Distance Graph — pick eps at the "elbow"')
K-MeansDBSCAN
Cluster shapeSpherical onlyArbitrary
specificationRequiredNot needed
Noise handlingNone (all assigned)Labels noise as
Scalability — fast with index
SensitivityTo outliers (centroid shift)To eps, min_samples

10.22 Principal Component Analysis (PCA)

Intuition: Find the directions of maximum variance in the data. Project the data onto these directions to get a lower-dimensional representation that preserves as much information as possible.

Algorithm

  1. Centre the data: .
  2. Compute the covariance matrix: .
  3. Compute eigenvectors and eigenvalues of .
  4. The -th principal component = projection onto .
  5. Keep top- eigenvectors: where .

Explained variance ratio of PC : — the fraction of total variance captured.

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
 
# Fit PCA
pca = PCA(n_components=None, random_state=42)  # None = keep all components
pca.fit(X_scaled)
 
# Scree plot — choose k where explained variance levels off
plt.figure(figsize=(8, 4))
plt.plot(np.cumsum(pca.explained_variance_ratio_), 'o-')
plt.xlabel('Number of Components')
plt.ylabel('Cumulative Explained Variance')
plt.axhline(y=0.95, color='r', linestyle='--', label='95% variance')
plt.legend()
plt.title('PCA Scree Plot')
 
# Apply with chosen k
pca_k = PCA(n_components=0.95)  # keep enough components to explain 95% variance
X_reduced = pca_k.fit_transform(X_scaled)
print(f"Components to explain 95% variance: {pca_k.n_components_}")
print(f"Explained variance ratio: {pca_k.explained_variance_ratio_}")

PCA in a Pipeline

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
 
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('pca',    PCA(n_components=50)),
    ('model',  SVC(kernel='rbf'))
])
pipe.fit(X_train, y_train)

Use Cases

  • Visualisation: Compress to 2–3 components for scatter plots.
  • Noise reduction: Later PCs capture noise more than signal; dropping them denoises data.
  • Speeding up models: Reduce dimensionality before passing to expensive models.
  • Multicollinearity removal: PCs are orthogonal by construction.

CAUTION

Always scale before PCA. A feature with variance 10,000 (e.g., income in rupees) will dominate the first principal component over a feature with variance 1 (e.g., number of children), regardless of which is more informative. After scaling, PCA treats all features equally.