Back to the lesson·Lesson 2 of 8·Measuring a child
From weight and height to a z-score
The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.
What this lesson covers
- What a z-score says
- Three z-scores, three questions
- The LMS method
- The lookup key has three parts
- Computing it
- Flagging: two rules, two answers
- The standard deviation is already a finding
- Save the z-scores with their inputs
- What comes next
Speaker notes
The LMS method, the reference table lookup, the three z-scores and what each measures. Computed on 930 children it gives a mean of -0.67 and a standard deviation of 1.22 — and that second number is already a finding.What a z-score says
- A z-score answers one question: how far is this child from the median of a healthy reference population, in standard deviations?
Speaker notes
A z-score answers one question: how far is this child from the median of a healthy reference population, in standard deviations? A weight-for-height z-score of -2 means this child weighs what a child two standard deviations below the median weighs, for their height. It is not a percentage of anything, and it is not comparable to a percentile without conversion. The reference is the WHO 2006 Child Growth Standards, built from a multi-country study of children raised in conditions that do not constrain growth — breastfed, non-smoking households, adequate healthcare. That construction is deliberate: the standards describe how children should grow, not how children in any particular place do.Three z-scores, three questions
Score Compares Deficit it detects Recovers? Weight-for-height (WHZ) Weight against height Acute — wasting Yes, in weeks Height-for-age (HAZ) Height against age Chronic — stunting No Weight-for-age (WAZ) Weight against age Both together — underweight Partly Three z-scores, three questions
- Weight-for-age is the one to be careful with — It mixes the two deficits, so a low WAZ does not say whether the child…
Speaker notes
Weight-for-age is the one to be careful with. It mixes the two deficits, so a low WAZ does not say whether the child is wasted, stunted or both, and the programme response differs. It survives in growth monitoring because it needs no height board, and it should not be used to classify acute malnutrition. This course computes weight-for-height, because that is what the case definitions and the IPC thresholds are built on.The LMS method
- L — the Box-Cox power that normalises a skewed distribution
- M — the median
- S — the coefficient of variation
Speaker notes
The standards are published as three parameters per sex, per measurement, per 0.1 cm of length or height:The LMS method — In Python
import pandas as pd reference = pd.read_csv("who-2006-weight-for-lenhei.csv") print(reference.head(3)) print(f"{len(reference)} rows: sex x standard x lenhei in 0.1 cm steps")The LMS method — In R
reference <- readr::read_csv("who-2006-weight-for-lenhei.csv") nrow(reference)Speaker notes
2,404 rows. Read the table; do not reimplement the interpolation from a textbook. R has the officialanthropackage; Python has no maintained equivalent that installs cleanly, which is exactly why this table is committed here.The lookup key has three parts — In Python
def lookup_key(row): lying = row["measured_lying"] == "true" standard = "L" if row["age_months"] < 24 else "H" lenhei = row["height_cm"] if standard == "L" and not lying: lenhei += 0.7 elif standard == "H" and lying: lenhei -= 0.7 return row["sex"], standard, round(lenhei, 1)Speaker notes
This is the part that goes wrong, and the joining course's lab was built on it.The lookup key has three parts — In R
lookup_key <- function(sex, age, height, lying) { standard <- if (age < 24) "L" else "H" lenhei <- height + if (standard == "L" && !lying) 0.7 else if (standard == "H" && lying) -0.7 else 0 list(sex = sex, lorh = standard, lenhei = round(lenhei, 1)) }The lookup key has three parts
- The standard follows the age — not the position
- The adjustment goes on the measurement, not the standard — You are converting the measurement into the one the standard…
- The rounding is to one decimal, on both sides, in one place — A float computed from an adjustment is not the same float…
Speaker notes
Three things in that function are decisions, not mechanics. The standard follows the age, not the position. A 30-month-old measured lying is still assessed against the height standard, with the adjustment applied. The adjustment goes on the measurement, not the standard. You are converting the measurement into the one the standard expects. The rounding is to one decimal, on both sides, in one place. A float computed from an adjustment is not the same float as one read from a CSV, which is the inexact-key join the joining course spent a lab on.Computing it — In Python
reference["key"] = list(zip(reference["sex"], reference["lorh"], reference["lenhei"].round(1))) lms = reference.set_index("key")[["l", "m", "s"]] def whz(row): key = lookup_key(row) if key not in lms.index or pd.isna(row["weight_kg"]): return None l, m, s = lms.loc[key] return (((row["weight_kg"] / m) ** l) - 1) / (l * s) smart["whz"] = smart.apply(whz, axis=1) print(f"{smart['whz'].notna().sum()} of {len(smart)} computable")Computing it — In R
# In R, use the official package rather than the table: # anthro::anthro_zscores(sex = ..., age = ..., weight = ..., lenhei = ..., measure = ...)Computing it
- Missing weight or age — 32 children, and they are missing data.
- Height outside the reference range — the length standard runs 45.0 to 110.0 cm and the height standard 65.0 to…
- A measurement in the wrong unit — four heights recorded in metres, which the joining course's lab found by…
- Report the three separately — "66 children excluded" hides that some were data errors you could have fixed
Speaker notes
864 of 930 computable. The 66 that are not split into three groups and the distinction matters: Report the three separately. "66 children excluded" hides that some were data errors you could have fixed.Flagging: two rules, two answers
- WHO flags — fixed bounds, exclude z outside -5 to +5. Absolute, comparable across surveys.
- SMART flags — relative, exclude observations more than 3 SD from the survey's own mean. Adapts to the survey, and…
Speaker notes
Extreme z-scores are excluded before any prevalence, and there are two conventions.Flagging: two rules, two answers — In Python
who_flagged = smart["whz"].between(-5, 5) mean, sd = smart["whz"].mean(), smart["whz"].std() smart_flagged = smart["whz"].between(mean - 3 * sd, mean + 3 * sd) print(f"WHO flags keep {who_flagged.sum()}, SMART flags keep {smart_flagged.sum()}")Flagging: two rules, two answers — In R
smart <- smart |> mutate(who_ok = between(whz, -5, 5), smart_ok = between(whz, mean(whz, na.rm = TRUE) - 3 * sd(whz, na.rm = TRUE), mean(whz, na.rm = TRUE) + 3 * sd(whz, na.rm = TRUE)))Speaker notes
They exclude different children and give slightly different rates. State which you used; a SMART plausibility report requires it, and two surveys using different rules are not directly comparable.The standard deviation is already a finding — In Python
analysable = smart.loc[who_flagged, "whz"] print(f"n = {len(analysable)}, mean = {analysable.mean():.2f}, " f"sd = {analysable.std():.2f}")The standard deviation is already a finding — In R
smart |> filter(who_ok) |> summarise(n = n(), mean = mean(whz), sd = sd(whz))Speaker notes
852 children, mean -0.67, standard deviation 1.22. The mean is unremarkable — a population somewhat below the reference median, which is normal in this sector. The standard deviation is not. A well-measured survey produces a weight-for-height SD between about 0.8 and 1.2, because the reference population's SD is 1 by construction and real populations are only slightly more variable. 1.22 sits at the edge of acceptable, and an SD above the range means one of three things: measurement error inflating the spread, a genuinely heterogeneous population, or a data entry problem. It is the single most informative number in a plausibility report, and lesson 5 is about reading it properly. Note also what it does to the prevalence. A wider distribution puts more children past a fixed cut-off, so an inflated SD raises GAM without any child being more malnourished.Save the z-scores with their inputs — In Python
smart[["child_id", "cluster", "team", "age_months", "sex", "weight_kg", "height_cm", "measured_lying", "oedema", "whz"]].to_csv( "outputs/smart_with_zscores.csv", index=False)Save the z-scores with their inputs — In R
readr::write_csv(smart, here::here("outputs", "smart_with_zscores.csv"))Speaker notes
Keep the inputs beside the output. When somebody disagrees with a prevalence, the argument is almost never about the arithmetic — it is about the adjustment, the flagging rule or the excluded children, and all three are recoverable only if the inputs travelled with the result.What comes next
- You have a z-score for every analysable child.
Speaker notes
You have a z-score for every analysable child. The next unit turns it into a classification — the case definitions for severe and moderate acute malnutrition, and the clinical sign that overrides both.