Min-max scaling and standardising, applied to a column with one outlier in it. Look at where the ordinary values end up.
values = [10, 11, 12, 13, 100] # min-max: the four ordinary values are squeezed into 3% of the range # -> [0.0, 0.011, 0.022, 0.033, 1.0] # standardised: they keep their spacing, and the outlier is visibly far out # -> [-0.54, -0.51, -0.49, -0.46, 2.00] # A z-score of 0 means 'exactly average'. Roughly: # |z| < 1 ordinary about two thirds of a normal distribution # |z| > 2 notable about one in twenty # |z| > 3 worth a look about one in three hundred
- The unit is the standard deviation, which is why it is comparable
Dividing by the deviation cancels the units — centimetres over centimetres, rupees over rupees — so what is left is a dimensionless number that means the same thing in every column. That is the whole trick, and it is why a model fed standardised features stops caring which one happened to be measured in thousands.
- It is not bounded, and that is the point
Min-max guarantees 0 to 1 and pays for it by letting a single outlier decide the range. A z-score has no bounds at all, so an extreme value comes out as a large number instead of compressing everything else — which is exactly what you want if the reason you are looking is to find the extreme value.
- A constant column has no unit to divide by
Zero deviation means every value is the mean, so every distance is 0 and the division is 0/0. There is no answer: the column carries no information and cannot be expressed in units of its own spread. Refusing it is the same decision as in min-max scaling, for the same reason.
This is the variance exercise plus one division. Get the mean and the deviation first, refuse the cases where the deviation is unusable, then map every value through the same formula.
Handle the empty list before computing anything. Then mean = sum / count and the deviation is the square root of the variance, with the divisor chosen by the sample flag. A deviation of 0 is the refusal.
Compute mean and deviation once, above the comprehension — putting either inside it re-walks the whole list per value. Check the deviation rather than the variance: they are zero together, and the deviation is the number you are about to divide by.
import math def z_scores(values, sample=True): count = len(values) if count == 0: return ____ divisor = count - 1 if sample else count if divisor <= 0: raise ValueError('not enough values to standardise') mean = sum(values) / ____ deviation = math.sqrt( sum((value - mean) ** 2 for value in values) / ____ ) if deviation == ____: raise ____(f’every value is {mean}, so there is no spread') return [(value - mean) / ____ for value in values]
import math
def z_scores(values, sample=True):
count = len(values)
if count == 0:
return []
divisor = count - 1 if sample else count
if divisor <= 0:
raise ValueError(
f"not enough values to standardise: {count} of them"
)
mean = sum(values) / count
deviation = math.sqrt(
sum((value - mean) ** 2 for value in values) / divisor
)
if deviation == 0:
raise ValueError(
f"every value is {mean}, so there is no spread to measure against"
)
return [(value - mean) / deviation for value in values]
Empty returns early, because nothing else can run without a mean. Then the divisor, because the deviation cannot be computed without it. Then the zero deviation, because that can only be known once it exists. Each guard sits immediately before the line it protects, which is what makes the sequence checkable rather than a pile of validation at the top.
They are zero at the same time, so either works — but the deviation is the number in the division below, and a guard should be about the value it is guarding. It also avoids a subtle trap: on a column of very small numbers a tiny non-zero variance can have a deviation that rounds to something usable, or the reverse, and testing the thing you divide by means the two can never disagree.
count, divisor and deviation are three different numbers doing three different jobs, and mixing them up produces scores that are wrong by a constant factor — which is very hard to spot, because the shape of the distribution still looks right. The mean always divides by count; only the variance uses the divisor.
sklearn.preprocessing.StandardScaler is the real answer, and as with min-max the reason is not the arithmetic: it remembers the training mean and deviation and applies them to everything afterwards. Standardising a test set using its own statistics is leakage — the next exercise is the bug report. Note StandardScaler divides by n by default and maps a constant column to zeros rather than refusing. And if the column is badly skewed rather than merely scaled wrong, standardising does not fix it: a log transform or a quantile transform is the tool, because a z-score of a long tail is still a long tail.
- Return the mean and deviation alongside the scores, which is the beginning of being able to standardise a second list the same way.
- Use the median and the median absolute deviation instead, and compare what the outlier case looks like — that is the robust version.
- Flag everything beyond three deviations as an outlier and try it on the outlier case: notice that with only five values, nothing is.