cassionData Analysis

Back to the lessonLesson 2 of 8Measuring 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.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 23

    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.
  2. Slide 2 / 23

    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.
  3. Slide 3 / 23

    Three z-scores, three questions

    ScoreComparesDeficit it detectsRecovers?
    Weight-for-height (WHZ)Weight against heightAcute — wastingYes, in weeks
    Height-for-age (HAZ)Height against ageChronic — stuntingNo
    Weight-for-age (WAZ)Weight against ageBoth together — underweightPartly
  4. Slide 4 / 23

    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.
  5. Slide 5 / 23

    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:
  6. Slide 6 / 23

    The LMS method — Example

    z = ((weight / M)^L - 1) / (L * S)
  7. Slide 7 / 23

    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")
  8. Slide 8 / 23

    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 official anthro package; Python has no maintained equivalent that installs cleanly, which is exactly why this table is committed here.
  9. Slide 9 / 23

    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.
  10. Slide 10 / 23

    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))
    }
  11. Slide 11 / 23

    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.
  12. Slide 12 / 23

    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")
  13. Slide 13 / 23

    Computing it — In R

    # In R, use the official package rather than the table:
    # anthro::anthro_zscores(sex = ..., age = ..., weight = ..., lenhei = ..., measure = ...)
  14. Slide 14 / 23

    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.
  15. Slide 15 / 23

    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.
  16. Slide 16 / 23

    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()}")
  17. Slide 17 / 23

    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.
  18. Slide 18 / 23

    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}")
  19. Slide 19 / 23

    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.
  20. Slide 20 / 23

    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)
  21. Slide 21 / 23

    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.
  22. Slide 22 / 23

    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.
  23. Slide 23 / 23

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson