cassionData Analysis

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.

PythonR120 minCore Humanitarian Standard (CHS)UNICEF indicator definitionsOECD DAC evaluation criteria

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.

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.