Min-Max Normalization
Min-max scaling expresses a value as a position relative to two saved endpoints. In its common 0-to-1 form, the training minimum becomes 0 and the training maximum becomes 1. Intermediate values retain their relative spacing.
The word “training” matters. A new value beyond those endpoints can produce an output below 0 or above 1. Clipping the result is a separate, information-losing choice.
Read a value as a fraction of the fitted range
Take training values 10, 20, and 30. The minimum is 10, the maximum is 30, and the range is 20. A value of 20 is 10 units above the minimum, halfway through that range, so it becomes 0.5.
Let $m$ and $M$ be the saved training minimum and maximum for one feature. For a nonconstant feature, $M-m>0$. Transform a value $x$ as
First move the minimum to zero, then divide by the fitted range. These are per-column statistics; the method does not divide each row by that row’s largest entry.
Why future values can leave the interval
With the saved endpoints 10 and 30, a new value of 40 becomes (40 − 10) / 20 = 1.5. A new value of 0 becomes −0.5. Those outputs are the intended affine extrapolation beyond the reference interval, not evidence that the scaler forgot its range.
Fitting again on the new batch would change the endpoints and the meaning of every coordinate. Keep the scaler associated with the trained model. If values regularly exceed the old range, examine whether the population, units, or data source changed.
Clipping enforces bounds by merging values
With clipping enabled, any transformed value above 1 becomes 1 and any below 0 becomes 0. Inputs 30, 35, and 40 would all reach the same upper endpoint. The model loses their difference.
The unclipped inverse is $x=m+z(M-m)$. It restores 40 from 1.5. After clipping to 1, however, the same inverse returns 30. No inverse can determine which of the merged inputs was originally supplied from that clipped value alone.
A hard output bound can be part of a model’s input contract, but clipping does not correct a distribution shift or prove that an outlying observation is invalid. Choose it deliberately and monitor how often it applies.
Python: compare scaling and clipping
Fit both scalers on the same three training values. The only difference is the explicit clipping option.
Run the future-value and inverse examples
import numpy as np
from sklearn.preprocessing import MinMaxScaler
train = np.array([[10.], [20.], [30.]])
new = np.array([[0.], [20.], [40.]])
plain = MinMaxScaler().fit(train)
clipped = MinMaxScaler(clip=True).fit(train)
print(plain.transform(new).ravel().tolist())
print(clipped.transform(new).ravel().tolist())
print(clipped.inverse_transform(clipped.transform(new)).ravel().tolist())
assert np.allclose(plain.inverse_transform(plain.transform(new)), new)
constant = MinMaxScaler().fit([[7.], [7.], [7.]])
assert constant.transform([[7.], [9.]]).ravel().tolist() == [0., 2.]
[-0.5, 0.5, 1.5]
[0.0, 0.5, 1.0]
[10.0, 20.0, 30.0]
An extreme training value can compress the ordinary range
Because extrema define the scale, one extreme reference observation can have a large effect. Compare the encoding of 20 under two fitted pools:
If the extreme value is an error, address the error at its source. If it is valid, compare whether an extrema-based scale suits the task. Robust scaling uses quantiles instead, while standardization uses a mean and standard deviation; each makes a different choice of reference spread.
A constant column needs an explicit convention
For a training feature containing only 7, the fitted minimum and maximum are both 7. The usual denominator is zero. Scikit-learn replaces the zero range with 1 when constructing its scale, so under the default output interval the training values become 0.
A later value of 9 becomes 2 with clipping disabled. The constant training column did not supply a meaningful observed range, so this is a fallback calculation, not a statement that 9 lies inside an estimated 0-to-1 population range. Clipping would turn that 2 into 1.
Use another output interval
For requested endpoints $a<b$ and a nonconstant training feature, first calculate the 0-to-1 fraction, then stretch and shift it:
For output interval [−1, 1], our training values 10, 20, and 30 become −1, 0, and 1. The interval changes the coordinate units; it does not change their ordering or make future inputs stay within the endpoints without clipping.
Separate learned extrema from predefined physical bounds
If a schema guarantees a value lies between known endpoints, such as an unsigned 8-bit intensity from 0 to 255, dividing by that fixed physical range requires no dataset fitting. That is different from estimating a range from the observed training minimum and maximum.
For learned extrema, fit within each training fold and save the scaler with the model. Keep feature order and units consistent. Min-max scaling leaves missing values missing in scikit-learn; use the missing-data workflow when the model requires a fill policy.
This transform neither centers the training mean at zero nor gives every row unit length. The terminology note distinguishes those operations. Evaluate it according to the estimator’s needs, as discussed in when scaling matters.
C++: a saved 0-to-1 transform with optional clipping
The constructor fits finite-valued reference data. Its zero-range fallback matches the simple constant-column example. The inverse undoes the affine map; it cannot recover values lost through clipping.
Compile the min-max transform
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <vector>
class MinMax {
double minimum = 0, span = 1;
bool clip;
public:
explicit MinMax(const std::vector<double>& reference, bool clip_output = false)
: clip(clip_output) {
if (reference.empty()) throw std::invalid_argument("empty reference");
double maximum = reference.front();
minimum = maximum;
for (double x : reference) {
if (!std::isfinite(x)) throw std::invalid_argument("finite values required");
minimum = std::min(minimum, x);
maximum = std::max(maximum, x);
}
span = maximum - minimum;
if (!std::isfinite(span)) throw std::invalid_argument("range overflow");
if (span == 0) span = 1;
}
double transform(double x) const {
const double value = (x - minimum) / span;
return clip ? std::clamp(value, 0.0, 1.0) : value;
}
double inverse(double value) const { return minimum + span * value; }
};
int main() {
const MinMax plain({10, 20, 30}), clipped({10, 20, 30}, true);
std::cout << std::fixed << std::setprecision(1);
for (double x : {0, 20, 40}) {
const double bounded = clipped.transform(x);
std::cout << x << ": scaled=" << plain.transform(x)
<< ", clipped=" << bounded
<< ", inverse after clipping=" << clipped.inverse(bounded) << "\n";
}
}
0.0: scaled=-0.5, clipped=0.0, inverse after clipping=10.0
20.0: scaled=0.5, clipped=0.5, inverse after clipping=20.0
40.0: scaled=1.5, clipped=1.0, inverse after clipping=30.0References and further reading
- Scikit-learn: MinMaxScaler — training extrema, desired intervals, clipping, extrapolation, and inverse behavior.
- Scikit-learn: compare scalers — visual comparisons of how extrema and outliers affect scaling choices.
- Scikit-learn: scaling features to a range — the column-wise affine transformation and its role in preprocessing.