Lesson 7 of 8
Unit · What the numbers mean
The area whose cases doubled is the one that improved
Saint-Louis-du-Nord's monthly intake goes from 5.8 cases to 10.2 after July. Nothing about violence in that area changed. A service opened, and a case curve measures whether people can reach a desk — never how many of them needed to.
The curve, and the obvious reading
import pandas as pd
cases = pd.read_csv("protection-case-management-2024.v1.csv")
monthly = cases.groupby(["admin2", "opened_month"]).size().unstack(fill_value=0)
print(monthly)
library(dplyr)
cases |> count(admin2, opened_month) |>
tidyr::pivot_wider(names_from = opened_month, values_from = n, values_fill = 0)
One area’s line goes up and stays up.
area = cases[cases["admin2"] == "Saint-Louis-du-Nord"]
month = area["opened_month"].str[5:].astype(int)
before = (month <= 6).sum() / 6
after = (month >= 7).sum() / 6
print(f"Jan-Jun: {before:.1f} cases a month")
print(f"Jul-Dec: {after:.1f} cases a month ({after / before - 1:+.0%})")
cases |>
filter(admin2 == "Saint-Louis-du-Nord") |>
mutate(half = if_else(as.integer(substr(opened_month, 6, 7)) <= 6, "H1", "H2")) |>
count(half) |> mutate(per_month = n / 6)
5.8 cases a month before July, 10.2 after — a 74% rise, while no other area moves by more than eleven points either way.
The obvious reading is that protection incidents in Saint-Louis-du-Nord rose 74% in the second half of the year. That reading is wrong, and it is the single most common misuse of protection data.
What a case count is a count of
A case exists when a person reached a service, was willing to disclose, and a caseworker opened a file. So the count is a product of four things, only the last of which is what people read it as:
| The count depends on | Which changes when |
|---|---|
| A service existing within reach | One opens, one closes, a road is cut |
| People knowing it exists | An outreach campaign runs |
| People being willing to disclose | Trust rises or falls; a bad experience circulates |
| Someone opening a file | Staffing, workload, referral practice |
| Incidence in the population | — |
Every one of the first four moved in Saint-Louis-du-Nord in July, because a service opened. The fifth is not measured by this file and cannot be.
print("Case count = incidence x reporting rate x service availability x recording")
print("A change in the product does not tell you which factor moved.")
# One equation, five unknowns, one observation.
The test that distinguishes them
You cannot prove the curve is a reporting effect from the curve alone. You can gather evidence, and three checks are usually available.
Did anything change in the service? The strongest evidence and the least statistical. A new service, a new outreach worker, a changed intake form or a partner closing all produce a step change with a date attached.
Did the composition change? Saint-Louis-du-Nord’s general protection share goes from 31.4% to 41.0% while GBV falls from 40.0% to 32.8% — the new arrivals skew toward the least stigmatised category, which is what improved access looks like rather than what a surge in violence looks like.
def mix(frame):
return frame["case_category"].value_counts(normalize=True).round(3)
print("before July:", mix(area[month <= 6]).to_dict())
print("after July: ", mix(area[month >= 7]).to_dict())
cases |> filter(admin2 == "Saint-Louis-du-Nord") |>
mutate(half = if_else(as.integer(substr(opened_month, 6, 7)) <= 6, "H1", "H2")) |>
count(half, case_category) |> mutate(share = n / sum(n), .by = half)
Did other areas move? A rise confined to one area with an administrative explanation is a reporting effect. A rise across every area at the same time is more likely to be real, or a change in the reporting system.
halves = cases.assign(half=(cases["opened_month"].str[5:].astype(int) > 6))
shift = halves.groupby(["admin2", "half"]).size().unstack()
shift["change"] = shift[True] / shift[False] - 1
print((shift["change"] * 100).round(1).sort_values())
cases |> mutate(half = as.integer(substr(opened_month, 6, 7)) > 6) |>
count(admin2, half) |> tidyr::pivot_wider(names_from = half, values_from = n)
One area up 74.3%; the other five range from −9.4% to +11.3%. A single area moving seven times as far as the widest of the others has an administrative cause, and the job is to find it rather than to publish an incidence claim.
Say it in the report, in the sentence next to the number
Cases opened, Saint-Louis-du-Nord
Jan-Jun 35 cases 5.8 per month
Jul-Dec 61 cases 10.2 per month +74%
A case management service opened in the area in July. This figure measures
the number of people who reached a service and disclosed; it is not a
measure of the number of incidents. The rise is consistent with improved
access and cannot be read as a rise in violence.
The other five areas moved between -9% and +11% over the same period.
The disclaimer belongs beside the number, not in a footnote. A protection case curve without that sentence will be quoted as an incidence trend within a week, usually by someone quoting your report accurately.
The consequence people miss
If a case count measures access, then a programme that succeeds should see its case count rise, and a falling case count is ambiguous at best.
outcomes = pd.DataFrame({
"observation": ["cases rise", "cases fall"],
"good reading": ["outreach and access improved",
"incidents fell"],
"bad reading": ["violence increased",
"service became unreachable, or trust was lost"],
})
print(outcomes)
# The same number supports opposite conclusions. The context decides.
So a protection programme cannot use case volume as a performance indicator in either direction, and the indicators that survive are the ones this course has been building: pathway completion, time to service, caseload, closure reasons. Each of those has a denominator that is not the population.
Where prevalence actually comes from
Named plainly, because the question will be asked.
Population-based prevalence surveys, designed for the purpose, with specialised interviewer training and ethical protocols that a service dataset does not have. They are expensive, infrequent and the only instrument that answers the question.
Never from service data, at any level of sophistication. No adjustment for reporting rate rescues a denominator that is “people who reached a service”.
print("Question: how many women in this district experienced violence this year?")
print("Answerable from this dataset: no")
print("Instrument that answers it: a population-based prevalence survey")
# Saying "we cannot answer that with this" is the answer.
What comes next
The last lesson assembles everything into a protection report — including the section that states which analyses were requested and declined, and why that section belongs in the document rather than in an email nobody keeps.