Back to the lesson·Lesson 2 of 8·What not to collect
Nineteen cells of one
The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.
What this lesson covers
- The dataset is anonymous and the table is not
- Why four and not one
- A rule, written as code
- What to do instead of publishing the cell
- The equity finding survives the rule
- The request that has to be refused
- What comes next
Speaker notes
Cross-tabulate area, category, age band and sex and this anonymous dataset produces 200 cells, 85 of them holding four cases or fewer and 19 holding exactly one. In a GBV dataset a cell of one is a person, and the table cannot leave the building.The dataset is anonymous and the table is not — In Python
import pandas as pd referrals = pd.read_csv("protection-referrals-2024.v1.csv") cells = referrals.groupby( ["admin2", "case_category", "age_band", "sex"], dropna=False ).size() print(f"{len(cells)} cells") print(f" holding 1 case: {(cells == 1).sum()}") print(f" holding 4 or fewer: {(cells <= 4).sum()}")Speaker notes
Every direct identifier is gone. That protects against reading a row and knowing who it is. It does not protect against counting.The dataset is anonymous and the table is not — In R
library(dplyr) referrals |> count(admin2, case_category, age_band, sex) |> summarise(cells = n(), of_one = sum(n == 1), of_four_or_fewer = sum(n <= 4))The dataset is anonymous and the table is not
- 200 cells, 85 of them with four cases or fewer, 19 with exactly one
Speaker notes
200 cells, 85 of them with four cases or fewer, 19 with exactly one. A cell of one says: in this district, there was one GBV case involving a girl aged 0–11 this year. Anyone who works in that district may know exactly who that is. The dataset never named her; the table did.Why four and not one
- A cell of two identifies both people to each other — and to anyone who knows one of them
- Differencing recovers suppressed cells — If a row total is published and every cell in it but one is published, the…
Speaker notes
The instinct is to suppress only the cells of one. That is not enough, for two reasons. A cell of two identifies both people to each other, and to anyone who knows one of them. Differencing recovers suppressed cells. If a row total is published and every cell in it but one is published, the missing one is arithmetic.Why four and not one — In Python
by_area = referrals.groupby(["admin2", "case_category"]).size().unstack() print(by_area) print("\nIf one cell is suppressed and the row total is printed,") print("the suppressed cell is the total minus the others.")Why four and not one — In R
referrals |> count(admin2, case_category) |> tidyr::pivot_wider(names_from = case_category, values_from = n)Why four and not one
- So a suppression rule needs a threshold and a secondary suppression — suppress small cells, then suppress enough…
Speaker notes
So a suppression rule needs a threshold and a secondary suppression: suppress small cells, then suppress enough additional cells that the small ones cannot be recovered by subtraction. A threshold of five is the common floor in this sector; some agencies use ten.A rule, written as code — In Python (cont.)
THRESHOLD = 5 def suppress(counts, threshold=THRESHOLD): """Primary suppression, then a secondary pass so rows cannot be differenced.""" table = counts.copy() small = table < threshold table = table.mask(small, other=pd.NA) # Secondary: if a row has exactly one suppressed cell, the row total gives # it away. Suppress the next smallest cell in that row as well. for index, row in table.iterrows(): if row.isna().sum() == 1: remaining = counts.loc[index][~small.loc[index]] if len(remaining): table.loc[index, remaining.idxmin()] = pd.NA return tableA rule, written as code — In R
suppress <- function(x, threshold = 5) ifelse(x < threshold, NA, x) # Plus the secondary pass: a row with exactly one NA is not suppressed at all.A rule, written as code
- Write the rule as a function and apply it to every table — A rule applied by eye is applied inconsistently, and the one…
Speaker notes
Write the rule as a function and apply it to every table. A rule applied by eye is applied inconsistently, and the one table it is forgotten on is the one that gets forwarded.What to do instead of publishing the cell
- Aggregate up — Drop one dimension
Speaker notes
Suppression is the last resort. Three better answers come first, and each keeps the information the requester actually wanted. Aggregate up. Drop one dimension. Area by category has no cell under five; area by category by age by sex has eighty-five.What to do instead of publishing the cell — In Python
coarse = referrals.groupby(["admin2", "case_category"]).size() print(f"cells under 5: {(coarse < 5).sum()} of {len(coarse)}")What to do instead of publishing the cell — In R
referrals |> count(admin2, case_category) |> summarise(small = sum(n < 5))What to do instead of publishing the cell
- Report the rate, not the count, on a denominator large enough to carry it — "Completion is 31% for cases reporting a…
- Answer the question rather than supplying the table — A requester asking for area by age by sex usually wants to know…
Speaker notes
Report the rate, not the count, on a denominator large enough to carry it. "Completion is 31% for cases reporting a disability" is publishable; "3 of 11 cases in Mirebalais" is not. Answer the question rather than supplying the table. A requester asking for area by age by sex usually wants to know whether services reach children. That is one number on a large denominator, and it is publishable.The equity finding survives the rule — In Python
def completion(frame): consenting = frame[frame["consent_to_refer"]] reached = consenting[consenting["referral_accepted"] & consenting["days_to_first_service"].notna()] return len(reached), len(consenting), len(reached) / len(consenting) disability = referrals["disability_reported"].isin([True, "true", "Yes"]) print("reported: %d/%d = %.1f%%" % completion(referrals[disability])) print("not reported: %d/%d = %.1f%%" % completion(referrals[~disability]))Speaker notes
The worry is that suppression destroys the analysis. Test it.The equity finding survives the rule — In R
referrals |> filter(consent_to_refer) |> summarise(reached = sum(referral_accepted & !is.na(days_to_first_service)), n = n(), .by = disability_reported) |> mutate(completion = reached / n)The equity finding survives the rule
- 26.7% against 46.2%, on denominators of 202 and 1,436 — The nineteen-point equity gap — the most important finding in…
- The analyses that fail the disclosure test are almost never the ones that matter — A four-way disaggregation of 1,850…
Speaker notes
26.7% against 46.2%, on denominators of 202 and 1,436. The nineteen-point equity gap — the most important finding in this dataset — is computed on denominators far above any suppression threshold and is entirely publishable. The analyses that fail the disclosure test are almost never the ones that matter. A four-way disaggregation of 1,850 cases is rarely answering a question anybody asked; it is usually the result of putting every categorical variable into agroupbyand seeing what comes out.The request that has to be refused — Example (cont.)
Request: case counts by commune, month and incident category. Response: declined. Commune-by-month cells hold zero to three cases. At that resolution a count identifies individuals to anyone working in the area, and the dataset cannot be re-identified back but a person can be. What I can provide instead: - the same counts at district by quarter, no cell below five - the completion rate and time to service for each commune, on annual denominators - a written answer to the question the request is for, if you tell me what decision it feeds If the requirement is genuinely commune-level operational planning, the right route is a data sharing agreement with the case management agency,Speaker notes
Some do have to be refused, and it is worth rehearsing the sentence.The request that has to be refused
- Offer three alternatives and ask what decision the request feeds — A refusal with no alternative gets escalated and…
Speaker notes
Offer three alternatives and ask what decision the request feeds. A refusal with no alternative gets escalated and overturned by someone who has not read this lesson; a refusal with three alternatives usually ends with one of them being accepted.What comes next
- The rules are established.
Speaker notes
The rules are established. The next unit is the analysis itself — starting with the gate at the front of the pathway that decides who is in the denominator at all, and it is not a data quality decision.