⚙️ Setup¶
import numpy as np
import pandas as pd
from scipy import stats
rng = np.random.default_rng(122)
print("Ready.")
Ready.
mean, sd = 70, 8
for x in [86, 62]:
z = (x - mean) / sd
side = "above" if z > 0 else "below"
print(f"score {x}: z = ({x} - {mean}) / {sd} = {z:+.1f} -> {abs(z):.1f} SD {side} the mean")
score 86: z = (86 - 70) / 8 = +2.0 -> 2.0 SD above the mean score 62: z = (62 - 70) / 8 = -1.0 -> 1.0 SD below the mean
Answer: 86 gives z = +2.0 (two SDs above the mean); 62 gives z = −1.0 (one SD below). The sign tells direction, the magnitude tells distance in standard-deviation units.
for subj, m, s in [("History", 80, 5), ("Physics", 75, 13)]:
z = (88 - m) / s
print(f"{subj:8} z = (88 - {m}) / {s} = {z:+.2f}")
History z = (88 - 80) / 5 = +1.60 Physics z = (88 - 75) / 13 = +1.00
Answer: History gives z = +1.60, Physics z = +1.00. The identical raw 88 is a stronger result in History, because it sits further above its class average in standard-deviation terms. Raw scores from different distributions are not comparable; z-scores are.
mean, sd, x = 170, 7, 184
z = (x - mean) / sd
pct = stats.norm.cdf(z) * 100
print(f"z = ({x} - {mean}) / {sd} = {z:+.2f}")
print(f"percentile ~ {pct:.1f}th (via the standard normal curve)")
z = (184 - 170) / 7 = +2.00 percentile ~ 97.7th (via the standard normal curve)
Answer: z = +2.0, which maps to about the 97.7th percentile. This percentile reading relies on heights being approximately normal. For skewed or heavy-tailed data the z is still valid, but it would NOT translate to that percentile, the empirical 68-95-99.7 rule only holds for roughly normal data.
data = np.array([48, 50, 51, 49, 52, 50, 47, 53, 51, 95])
z = (data - data.mean()) / data.std()
print("z-scores:", np.round(z, 2))
flagged = data[np.abs(z) > 2.5]
print("flagged |z|>2.5:", flagged)
z-scores: [-0.49 -0.34 -0.27 -0.41 -0.19 -0.34 -0.56 -0.12 -0.27 2.98] flagged |z|>2.5: [95]
Answer: The value 95 has the largest z and is flagged as a potential outlier. Caveat worth remembering: because the mean and SD are not robust, one extreme value inflates the SD and can shrink its own z, so on messier data a median/MAD modified z-score is the sturdier test.
raw = rng.lognormal(mean=1, sigma=0.7, size=3000)
z = stats.zscore(raw)
print(f"raw : mean {raw.mean():.2f}, sd {raw.std():.2f}, skew {stats.skew(raw):+.2f}")
print(f"std : mean {z.mean():.2f}, sd {z.std():.2f}, skew {stats.skew(z):+.2f}")
raw : mean 3.44, sd 2.85, skew +4.14 std : mean 0.00, sd 1.00, skew +4.14
Answer: After standardizing, the mean is ≈ 0 and the sd is ≈ 1, but the skewness is unchanged, the data is just as right-skewed as before. Standardization is a linear transform: it re-centers and re-scales but never reshapes. To reduce skew you need a nonlinear transform like the log (Chapter 11).