Two subtractions and a division. The denominator is where the trouble lives.
values = [10, 20, 30, 40] low, high = min(values), max(values) [(v - low) / (high - low) for v in values] # -> [0.0, 0.333…, 0.666…, 1.0] # Subtracting the minimum moves the column to start at 0; # dividing by the range squeezes it to end at 1. values = [7, 7, 7] max(values) - min(values) # -> 0 # every value is 0 away from the minimum, out of a total range of 0.
- Two steps: shift, then squash
v − low moves the smallest value to zero; dividing by the range brings the largest to one. Doing them in the other order does not work, and doing only the division scales the column without moving it, so the minimum ends up somewhere arbitrary. Naming the two numbers before the comprehension also means min() and max() are each called once rather than once per value.
- A constant column is not a scaling problem, it is a data problem
If every value is identical, the column carries no information at all — it cannot help a model distinguish anything. sklearn quietly maps it to zeros and carries on; refusing instead means somebody finds out that a feature they are paying to collect is constant. Neither is wrong, but the silent version is how a useless column survives to production.
- Min-max is not the only scaling, and it is fragile
One wild outlier sets the maximum, and every real value is then crushed into a narrow band near zero. Standardising by the mean and deviation spreads things out more sensibly for anything roughly bell-shaped, which is the next exercise. Min-max earns its place when you genuinely need a bounded range — pixel values, a progress bar, an opacity.
Handle the empty list first, because min() on nothing raises. Then find the smallest and largest once each, and check the distance between them before you divide by it.
low, high = min(values), max(values). If high == low the column is constant and there is no scaling to do. Otherwise each value becomes (value - low) / (high - low).
Compute low and high before the comprehension — putting min(values) inside it re-scans the whole list once per value, which is the accidentally quadratic pattern from running-total wearing different clothes.
def scale(values): if not values: return ____ low, high = ____(values), ____(values) span = high - low if span == ____: raise ____(f’every value is {low}, so there is no range to scale') return [(value - ____) / span for value in values]
def scale(values):
# min() and max() both raise on an empty sequence, so this comes first.
if not values:
return []
low, high = min(values), max(values)
span = high - low
if span == 0:
raise ValueError(
f"every value is {low}, so there is no range to scale"
)
return [(value - low) / span for value in values]
An empty column is not a problem: there is nothing to scale, and [] is the correct answer to 'scale nothing'. A constant column *is* a problem: there is something to scale and no way to do it. Getting those two the same way round is what stops a caller’s empty batch from raising and their useless feature from passing silently.
min() and max() each walk the whole list. Inside the comprehension they would run once per value, turning a linear function into a quadratic one — invisible on the cases here and a real cost on a column with a million rows. Any time a loop body calls something that scans the same sequence, check whether it belongs above the loop.
'every value is 7' tells somebody immediately which column they are looking at and what is wrong with it — frequently enough to identify the bug without opening the data. 'cannot scale a constant column' is true, and sends them to go and look.
sklearn.preprocessing.MinMaxScaler is what you would use, and the reason is not that it is better arithmetic — it is that it remembers the minimum and maximum from the training data and applies them to everything afterwards. That is the thing this function cannot do and the next exercise is about: scaling a test set using its own range is data leakage, and it is the most common serious mistake in a first machine learning pipeline. Also note MinMaxScaler maps a constant column to zeros rather than refusing, so if you swap this for it, that refusal quietly becomes a shrug.
- Add a feature_range argument so the column can be squeezed into, say, −1 to 1, and check that the formula generalises without a second branch.
- Return the low and high alongside the scaled values, which is the beginning of being able to apply the same scaling to another list.
- Scale the outlier case with a percentile-based range — clip at the 5th and 95th — and compare what happens to the three small values.