cassionData Analysis

Lesson 5 of 8

Unit · Counting people

Reach, coverage, and the people counted twelve times

Three indicators that all sound like "how many people did we help", and the monthly series that turns 4,200 children into 50,000 the moment somebody sums the column.

PythonR90 minUNICEF indicator definitionsSustainable Development Goals (SDG)Results-Based Management (RBM)

Three questions that sound like one

“How many people did the programme help?” has three answers, and a proposal, a donor report and a coverage figure each need a different one.

  • Reach — how many distinct people received something, over a stated period.
  • Service volume — how many services were delivered. Larger than reach whenever anyone comes twice.
  • Coverage — what share of those who needed it got it. Requires a denominator and is the only one that says whether the programme is enough.

The names are used interchangeably in practice, which is how a programme reports 50,000 children reached in a district with 4,200 of them.

The cumulative sum that manufactures people

Here is the mechanism, and it is the single most common way this sector overstates its work.

monthly = (
    muac.assign(month=muac["screening_date"].dt.strftime("%Y-%m"))
    .groupby("month")
    .agg(screenings=("child_id", "size"),
         distinct_children=("child_id", "nunique"))
)
monthly["cumulative_naive"] = monthly["screenings"].cumsum()

print(monthly)
print("sum of monthly counts:  ", monthly["screenings"].sum())
print("distinct children, year:", muac["child_id"].nunique())
monthly <- muac |>
  mutate(month = format(screening_date, "%Y-%m")) |>
  summarise(screenings = n(), distinct_children = n_distinct(child_id), .by = month) |>
  arrange(month) |>
  mutate(cumulative_naive = cumsum(screenings))

c(sum_of_months = sum(monthly$screenings),
  distinct_year = n_distinct(muac$child_id))

On this register the two happen to be close, because it is a one-round campaign and few children were screened twice. That is the exception. In a monthly programme — outpatient therapeutic care, a cash transfer, a health facility — a beneficiary appears in every month they receive something, and twelve monthly counts summed is twelve times a stable caseload.

The arithmetic that goes wrong:

Correct Wrong
Monthly reach, January distinct people in January —
Annual reach distinct people across the year sum of twelve monthly counts
Cumulative reach to date distinct people since programme start running total of monthly counts

A cumulative reach figure can only be computed from person-level data. If your reporting is monthly aggregates, you cannot deduplicate across months, and the honest annual figure is not available — you must either say so or report the maximum month rather than the sum.

annual_reach = muac["child_id"].nunique()
service_volume = len(muac)
print(f"reach {annual_reach:,} children; {service_volume:,} screening events")
c(reach = n_distinct(muac$child_id), volume = nrow(muac))

Deduplicating across months, when you can

first_contact = (
    muac.sort_values("screening_date")
    .drop_duplicates(subset=["child_id"], keep="first")
    .assign(month=lambda d: d["screening_date"].dt.strftime("%Y-%m"))
)
new_by_month = first_contact.groupby("month").size()
print(new_by_month.cumsum())
first_contact <- muac |>
  arrange(screening_date) |>
  distinct(child_id, .keep_all = TRUE) |>
  mutate(month = format(screening_date, "%Y-%m"))

first_contact |> count(month) |> arrange(month) |> mutate(cumulative = cumsum(n))

Counting each person in the month of their first contact gives a cumulative series that rises to the true reach and never exceeds it. It also produces a genuinely useful management number — new beneficiaries per month, which distinguishes a programme that is growing from one that is serving the same people repeatedly.

Report both, always: new this month and total served this month. They answer different questions and either alone is misleading.

Reach is not coverage, and the difference is the denominator

Reach counts who you served. Coverage says what fraction of those who needed the service got it, and it is the only one of the three that can say a programme is insufficient.

reached = muac["child_id"].nunique()
under5_population = 21500      # from the administrative frame, projected

print(f"screening coverage: {reached / under5_population:.1%}")
c(coverage = n_distinct(muac$child_id) / 21500)

Two failures to avoid, and both are common enough to have names.

Admissions over expected caseload is not coverage. Dividing the children admitted to treatment by the caseload you expected tells you how good your expectation was, not what share of malnourished children you reached. Real coverage needs the number of malnourished children, which needs a survey — which is why coverage surveys exist as a separate methodology.

A reach figure with a population denominator is not coverage either unless the population is the population in need. Screening 4,200 of 21,500 under-fives is screening coverage. It says nothing about what share of the malnourished children were found.

Cumulative counts in proposals

Multi-year proposals ask for “people reached over the life of the project”, and this is where the double-counting becomes a compliance issue rather than a methods one.

Three rules that keep it honest:

  • State the deduplication level. “Unique individuals, deduplicated within year, not across years” is a defensible and common compromise. Undocumented deduplication is not.
  • Never sum reach across sectors. A household receiving water, food and a protection service is one household, and adding three sector reach figures triple-counts it. Report by sector, and give a deduplicated total only if you can actually compute one.
  • Say when you cannot. “Aggregate figures are not deduplicated across partners; the true unique reach is lower” is a sentence donors read constantly and respect. The alternative is a number that collapses under audit.

Which one belongs in your LogFrame

If the row is about Use
Programme effort and cost Service volume
People served Reach, with the deduplication level stated
Whether the response is sufficient Coverage, with a needs-based denominator
Whether people who start finish A completion or dropout rate

The last row is the one most often missing, and it is frequently the most informative — the Theory of Change lesson made that case with the 13.8% penta1 to penta3 dropout, which no reach or coverage figure would have revealed.

Reach tells you what you did. Coverage tells you whether it was enough. A report with only the first is a description of activity, and every reader knows it.

What comes next

Almost every indicator in this lesson already has an official definition published by somebody — UNICEF, WHO, the cluster, the SDG framework. The next lesson is about finding it, adopting it, and saying precisely where yours departs from it.

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.