← All Posts
From Scratch · C++ · Machine Learning · Data Preparation and Preprocessing

Feature Scaling in C++

When one feature ranges over thousands and another over hundredths, any method that compares features by magnitude, distances, dot products, gradient steps, is dominated by the large-scale feature. Scaling puts every feature on a comparable footing. This post implements the two standard scalers from scratch, and shows the one rule that matters: fit the scaler on the training set, then apply it to the test set.

Two Scalers

Standardization (z-score) centers each feature to mean $0$ and scales it to standard deviation $1$:

$$z = \frac{x - \mu}{\sigma}.$$

Min-max normalization linearly maps each feature into $[0, 1]$:

$$x' = \frac{x - \min}{\max - \min}.$$

Z-score is the usual default; it tolerates outliers better than min-max, whose range is set by the two most extreme points. In both, the statistics ($\mu, \sigma$ or $\min, \max$) are learned once on the training data.

A Standardizer

The scaler is a tiny object with fit (compute per-feature mean and std) and transform (apply them). Separating the two is what enforces the golden rule: fit sees only training data, transform runs on both.

#include <bits/stdc++.h>
using namespace std;
using Matrix = vector<vector<double>>;

struct Standardizer {                 // z-score: (x - mean) / std, per feature
    vector<double> mean, std;

    void fit(const Matrix& X) {
        int N = X.size(), D = X[0].size();
        mean.assign(D, 0);
        std.assign(D, 0);
        for (auto& row : X)
            for (int f = 0; f < D; ++f) mean[f] += row[f];
        for (int f = 0; f < D; ++f) mean[f] /= N;
        for (auto& row : X)
            for (int f = 0; f < D; ++f)
                std[f] += (row[f] - mean[f]) * (row[f] - mean[f]);
        for (int f = 0; f < D; ++f)
            std[f] = sqrt(std[f] / N) + 1e-12;            // guard against zero variance
    }

    Matrix transform(const Matrix& X) const {
        Matrix out = X;
        for (auto& row : out)
            for (int f = 0; f < (int)row.size(); ++f)
                row[f] = (row[f] - mean[f]) / std[f];
        return out;
    }
};

Min-max is the same shape: fit records each feature's min and max, and transform returns (x - min) / (max - min).

Running It

Make two features on wildly different scales, feature 0 around $50 \pm 10$ and feature 1 around $0 \pm 0.01$, fit on train, and check the transformed statistics:

int main() {
    mt19937 rng(7);
    normal_distribution<double> f0(50, 10), f1(0, 0.01);
    Matrix Xtr, Xte;
    for (int i = 0; i < 400; ++i) Xtr.push_back({f0(rng), f1(rng)});
    for (int i = 0; i < 100; ++i) Xte.push_back({f0(rng), f1(rng)});

    Standardizer scaler;
    scaler.fit(Xtr);                      // learn mean/std from TRAIN only
    Matrix Ztr = scaler.transform(Xtr);
    Matrix Zte = scaler.transform(Xte);   // TEST uses the same train stats
    // ... print per-feature mean and std of Xtr, Ztr, Zte ...
}
before: train f0 mean=49.80 std=10.27 f1 mean=0.000 std=0.010 z-score: train f0 mean=-0.000 std=1.000 f1 mean=-0.000 std=1.000 test f0 mean=-0.062 std=0.895 (uses TRAIN mean/std, not its own)

After scaling, both training features have mean $0$ and standard deviation $1$, so feature 1 is no longer drowned out by feature 0. The test set does not come out at exactly mean $0$, std $1$, and that is correct: it is transformed with the training mean and standard deviation, so its own statistics land close to but not exactly on $0$ and $1$. Re-fitting on the test set would be a leak.

Do trees need this? No. The decision tree, forest, and boosted trees split on one feature at a time with thresholds, so any monotonic rescaling leaves every split, and the whole model, unchanged. Scaling earns its keep for distance-based methods (k-NN, k-means) and gradient-based ones (linear/logistic regression, neural nets), where feature magnitude directly drives the result.