cassionData Analysis

Lesson 2 of 8

Unit · Compute it before you believe it

The thirty-seven households zero-filling would misclassify

Zero-filling an incomplete Household Hunger Scale moves the prevalence by three tenths of a point and can misclassify every one of the thirty-seven households it touches. Which of those matters depends on whether your output is a percentage or a list.

PythonR135 minIntegrated Food Security Phase Classification (IPC)Sphere StandardsCore Humanitarian Standard (CHS)

Two more instruments, two more rules

The Household Hunger Scale and the reduced Coping Strategy Index sit beside the Food Consumption Score in almost every food security survey, and each carries a scoring rule that a naive sum() breaks in a different way.

Instrument What it asks The rule that gets broken
HHS Three questions on going without food, scored 0–2 each Valid only when all three are answered
rCSI Five behaviours, days in the last seven, weighted The weights are not all 1, and the fifth is 3

The Household Hunger Scale

Three questions, deliberately few, deliberately severe: was there no food of any kind in the house, did anyone go to sleep hungry, did anyone go a whole day and night without eating. Each is scored 0 for never, 1 for rarely or sometimes, 2 for often.

import pandas as pd

survey = pd.read_csv("food-security-survey-2024.v1.csv")
HHS = ["hhs_no_food_in_house", "hhs_sleep_hungry",
       "hhs_day_and_night_without_eating"]

complete = survey[HHS].notna().all(axis=1)
score = survey.loc[complete, HHS].sum(axis=1)

bands = pd.cut(score, [-1, 1, 3, 6],
               labels=["little or none", "moderate", "severe"])
print(f"complete: {complete.sum()} of {len(survey)}")
print((bands.value_counts(normalize=True) * 100).round(1))
library(dplyr)

hhs <- c("hhs_no_food_in_house", "hhs_sleep_hungry",
         "hhs_day_and_night_without_eating")

survey |>
  filter(if_all(all_of(hhs), ~ !is.na(.x))) |>
  mutate(score = rowSums(across(all_of(hhs))),
         band = cut(score, c(-1, 1, 3, 6),
                    labels = c("little or none", "moderate", "severe"))) |>
  count(band) |> mutate(share = n / sum(n))
Band Households Share
Little or no hunger (0–1) 1,186 57.2%
Moderate (2–3) 564 27.2%
Severe (4–6) 325 15.7%

2,075 of 2,112 households answered all three. The thirty-seven that did not are the interesting ones.

What zero-filling actually costs

The instinct is that dropping the incomplete responses loses data, so fill the gap with zero and keep them. Try it and measure.

zero_filled = survey[HHS].fillna(0).sum(axis=1)
naive = pd.cut(zero_filled, [-1, 1, 3, 6],
               labels=["little or none", "moderate", "severe"])
print((naive.value_counts(normalize=True) * 100).round(1))
survey |>
  mutate(score = rowSums(across(all_of(hhs)), na.rm = TRUE)) |>
  count(band = cut(score, c(-1, 1, 3, 6)))
Complete cases Zero-filled
Little or none 57.2% 57.5%
Moderate 27.2% 26.9%
Severe 15.7% 15.5%

Three tenths of a percentage point. On a prevalence, zero-filling is practically harmless here, and if you stop at the table you will conclude it does not matter.

Now look at the households rather than the percentage.

partial = survey.loc[~complete, HHS]
print(partial.sum(axis=1).value_counts().sort_index())
survey |> filter(if_any(all_of(hhs), is.na)) |>
  mutate(observed = rowSums(across(all_of(hhs)), na.rm = TRUE)) |> count(observed)

Twenty-nine of the thirty-seven score 0 or 1 on the questions they did answer, so zero-filling classifies them as little or no hunger. The unanswered question is worth up to 2 points. A household sitting at 1 with one question missing could be at 1 or at 3 — little hunger or moderate — and nothing in the data decides it.

So the cost of zero-filling depends entirely on what the analysis produces.

  • If the output is a prevalence, the damage is three tenths of a point and you should say you zero-filled and move on.
  • If the output is a targeting list, you have just told thirty-seven households they are food secure on the strength of a question nobody asked.

Name the output before choosing the rule. This is the same decision as the CMAM cure-rate denominator and the water point functionality rate: the defensible choice is not a property of the data, it is a property of the decision the number feeds.

The reduced Coping Strategy Index

Five behaviours, days in the last seven, and the weights are the whole point.

RCSI = {
    "rcsi_less_preferred_food": 1,
    "rcsi_borrowed_food": 2,
    "rcsi_limit_portion_size": 1,
    "rcsi_restrict_adult_consumption": 3,
    "rcsi_reduce_meal_numbers": 1,
}
rcsi = sum(survey[column] * weight for column, weight in RCSI.items())
print(f"median {rcsi.median():.0f}, mean {rcsi.mean():.1f}, max {rcsi.max():.0f}")
print(f"rCSI 19 or above: {(rcsi >= 19).mean():.1%}")
rcsi_weights <- c(rcsi_less_preferred_food = 1, rcsi_borrowed_food = 2,
                  rcsi_limit_portion_size = 1,
                  rcsi_restrict_adult_consumption = 3,
                  rcsi_reduce_meal_numbers = 1)

Median 19, mean 19.5, and 50.9% at or above 19. The weight of 3 on restricting adult consumption so children can eat is not arbitrary — it is the behaviour most predictive of deterioration, and an unweighted sum would bury it among four behaviours weighted 1.

Two things about the rCSI that are routinely got wrong.

It has no universal threshold. Unlike the FCS, the rCSI’s cut-offs are context-specific and usually set against the distribution in the same population. A report quoting “rCSI above 19” as though 19 were a standard has borrowed a number from another country’s analysis.

It goes up before consumption goes down, and it can also go down in a crisis, when a household has exhausted the strategies. A falling rCSI beside a falling FCS is worse news than a rising one.

Three instruments, three analysable samples

denominators = pd.DataFrame({
    "instrument": ["Food Consumption Score", "Household Hunger Scale",
                   "reduced Coping Strategy Index"],
    "analysable": [1989, 2075, 2112],
    "excluded": [123, 37, 0],
    "rule": ["all eight groups answered", "all three questions answered",
             "complete in this file"],
})
print(denominators)
# Print this before the results, every time.

Three numbers on three denominators, and none of them is 2,112. The difference is small enough to be invisible in a table and large enough to matter when someone subtracts two of your percentages.

What comes next

Consumption and hunger say what a household is experiencing. Neither says what it is doing about it, and the next lesson is the module that does — where the scoring rule is not a sum at all.

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.