cassionData Analysis

Back to the lessonLesson 2 of 8What 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.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 24

    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.
  2. Slide 2 / 24

    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.
  3. Slide 3 / 24

    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))
  4. Slide 4 / 24

    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.
  5. Slide 5 / 24

    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.
  6. Slide 6 / 24

    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.")
  7. Slide 7 / 24

    Why four and not one — In R

    referrals |> count(admin2, case_category) |>
      tidyr::pivot_wider(names_from = case_category, values_from = n)
  8. Slide 8 / 24

    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.
  9. Slide 9 / 24

    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 table
  10. Slide 10 / 24

    A rule, written as code — In Python (cont.)

    
    print(suppress(by_area))
  11. Slide 11 / 24

    A 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.
  12. Slide 12 / 24

    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.
  13. Slide 13 / 24

    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.
  14. Slide 14 / 24

    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)}")
  15. Slide 15 / 24

    What to do instead of publishing the cell — In R

    referrals |> count(admin2, case_category) |> summarise(small = sum(n < 5))
  16. Slide 16 / 24

    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.
  17. Slide 17 / 24

    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.
  18. Slide 18 / 24

    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)
  19. Slide 19 / 24

    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 a groupby and seeing what comes out.
  20. Slide 20 / 24

    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.
  21. Slide 21 / 24

    The request that has to be refused — Example (cont.)

      not an extract.
  22. Slide 22 / 24

    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.
  23. Slide 23 / 24

    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.
  24. Slide 24 / 24

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson