cassionData Analysis

Lesson 3 of 8

Unit · Who is a case

The case definitions, and the clause everyone drops

Severe, moderate and global acute malnutrition, the cut-offs the sector agreed on, and the oedema clause that makes every "count below the threshold" implementation wrong.

PythonR120 minWHO Child Growth StandardsSphere StandardsUNICEF indicator definitions

The definitions

Three terms, and they nest.

Term Weight-for-height z MUAC Oedema
SAM — severe acute malnutrition below -3 below 115 mm present
MAM — moderate acute malnutrition -3 to below -2 115 to below 125 mm —
GAM — global acute malnutrition below -2 below 125 mm present

GAM is SAM plus MAM. It is not a separate condition; it is the total burden of acute malnutrition, and it is the figure the IPC phases and the 15% emergency threshold are defined against.

Two structural points before any code.

Each row is a complete definition on its own. A child is SAM by weight-for-height, or by MUAC, or by oedema — the criteria are alternatives, not requirements. Lesson 4 is entirely about how far the first two disagree.

The oedema column is an OR, not an AND. A child with bilateral pitting oedema is severe whatever their measurements say. That clause is one term in a boolean expression and it is the term most often left out.

The implementation that is wrong

# WRONG: the definition everybody writes first
sam_wrong = (smart["whz"] < -3).sum()
# WRONG
sum(smart$whz < -3, na.rm = TRUE)

It is wrong in two ways at once. It drops the oedematous children, and na.rm or a null-propagating comparison silently drops the children with no z-score — so both the numerator and the denominator are quietly different from what the label claims.

The implementation that is right

import pandas as pd

analysable = smart[smart["whz"].between(-5, 5)].copy()
oedema = analysable["oedema"] == True

analysable["sam"] = (analysable["whz"] < -3) | oedema
analysable["mam"] = (analysable["whz"].between(-3, -2, inclusive="left")) & ~oedema
analysable["gam"] = analysable["sam"] | analysable["mam"]

n = len(analysable)
for label in ["sam", "mam", "gam"]:
    print(f"{label.upper():4} {analysable[label].sum():>4} / {n}  "
          f"{analysable[label].mean():.1%}")
analysable <- smart |> filter(between(whz, -5, 5))

analysable <- analysable |>
  mutate(sam = whz < -3 | oedema,
         mam = whz >= -3 & whz < -2 & !oedema,
         gam = sam | mam)

analysable |> summarise(across(c(sam, mam, gam), list(n = sum, rate = mean)), n = n())

852 analysable children: GAM 14.9%, SAM 3.8%.

Three details in that code are load-bearing.

MAM excludes oedema explicitly. An oedematous child whose z-score falls in the moderate band is severe, not moderate, and without the & ~oedema they would be counted in both — so SAM plus MAM would exceed GAM.

The bounds are half-open. -3 <= z < -2. A child at exactly -3.0 is severe. Getting this wrong moves a handful of children and is the kind of thing two analysts discover they disagree on at the worst moment.

The denominator is stated. 852, not 930 — the difference is children with no computable z-score and children outside the flagging bounds, and lesson 2 required you to report those three groups separately.

Check that the parts sum to the whole

assert (analysable["sam"] & analysable["mam"]).sum() == 0, "a child is both"
assert analysable["gam"].sum() == analysable["sam"].sum() + analysable["mam"].sum()
stopifnot(sum(analysable$sam & analysable$mam) == 0,
          sum(analysable$gam) == sum(analysable$sam) + sum(analysable$mam))

Two assertions, and they catch the oedema mistake, the bound mistake and any future edit that breaks the nesting. Run them every time.

The MUAC definitions, on the register

The screening register has MUAC and oedema and no weight or height, so it supports the MUAC-based definitions only.

muac = pd.read_csv("muac-screening-artibonite-2024.v1.csv")
measured = muac[muac["muac_mm"].notna()].copy()
oed = measured["oedema"] == True

measured["sam"] = (measured["muac_mm"] < 115) | oed
measured["gam"] = (measured["muac_mm"] < 125) | oed

print(f"n = {len(measured):,}  GAM {measured['gam'].mean():.1%}  "
      f"SAM {measured['sam'].mean():.1%}")
muac |>
  filter(!is.na(muac_mm)) |>
  summarise(n = n(),
            gam = mean(muac_mm < 125 | oedema),
            sam = mean(muac_mm < 115 | oedema))

Name the measure in the indicator. gam_muac_percent and gam_whz_percent are different indicators that both get called “GAM”, and the indicator design course’s rule about putting the measure in the name is not pedantry here — the next lesson shows the two disagreeing on three-quarters of severe cases.

What a definition does not say

Three things the case definition deliberately leaves out, each of which someone will assume.

Age. The definitions apply to children 6 to 59 months. Below six months the standards and the case definitions are different; above 59 months they do not apply at all. Check the age range before classifying.

out_of_scope = ~analysable["age_months"].between(6, 59)
print(f"{out_of_scope.sum()} children outside 6-59 months")
sum(!between(analysable$age_months, 6, 59), na.rm = TRUE)

Admission. A case definition says who is malnourished. Whether they are admitted depends on the programme’s protocol, which may use one criterion, both, or MUAC only for community screening and weight-for-height at the site. The register’s admission_criterion records what the site entered, not everything the child met.

Severity within severe. SAM with complications needs inpatient care; SAM without needs outpatient. The definition does not distinguish them, and the appetite test and clinical signs that do are not in an anthropometric dataset.

A case definition is a classification rule, not a treatment decision. Confusing the two produces a caseload figure that does not match any programme’s admissions and an argument nobody can resolve from the data.

What comes next

You can classify a child by either measure. The next lesson puts the two measures on the same children and finds that they disagree on three-quarters of severe cases — and that the disagreement has a cause you can name in one number.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.