cassionData Analysis

Lesson 2 of 8

Unit · The project on disk

Git does not forget, which is the point and the danger

A repository remembers every version of every file it has ever held, which is exactly what you want for code and exactly what you cannot allow for a beneficiary list. Deleting the file in a later commit does not remove it.

PythonR135 minCore Humanitarian Standard (CHS)OECD DAC evaluation criteria

The commit that cannot be taken back

git add data/raw/beneficiaries.csv
git commit -m "add survey data"
# ... realised three days later ...
git rm data/raw/beneficiaries.csv
git commit -m "remove data"

The file is still in the repository. It is in the history, it is in every clone, it is on the remote, and it is in the local copy of everyone who pulled. The second commit removed it from the current state and from nothing else.

On a repository holding protection or health data, that is a disclosure. Not a mistake to fix later — a disclosure, with the same obligations as any other.

Which is why the rule is preventive rather than corrective. You cannot un-commit personal data; you can only stop it entering.

The .gitignore you write before the first commit

# Raw data. Nothing under here is ever committed.
data/raw/*
!data/raw/.gitkeep
!data/raw/CHECKSUMS

# Derived data, regenerated by the pipeline
outputs/

# Credentials, in every form they arrive in
.env
*.pem
*_secret*
config.local.*

# Editor and language noise
.Rhistory
.RData
__pycache__/
*.pyc
.DS_Store
.ipynb_checkpoints/

Write it first, commit it first. A .gitignore added after the first commit is a .gitignore that arrived too late for the thing it was meant to catch.

The negations matter. data/raw/* excludes everything, then !data/raw/CHECKSUMS lets back exactly the file that proves which export was used — provenance without content.

What “personal data” means here, precisely

It is broader than a name column, and this sector’s datasets make the point.

Looks anonymous Is not, because
A case ID It joins to a case management system that has the name
GPS coordinates of a household It is an address
Date of birth plus commune plus sex Three fields identify most people in a small commune
A photograph of a form Every field on it
A free-text “notes” column It contains whatever the caseworker typed

The protection course established the arithmetic: a four-way disaggregation of an anonymous dataset produced nineteen cells of one, and a cell of one is an identification.

So the test is not “does it have a name column”. It is whether any combination of what the file holds could pick one person out — and in a small commune it usually can.

The check that runs before the commit does

# .git/hooks/pre-commit — or the pre-commit framework, or a CI step
if git diff --cached --name-only | grep -qE '^data/raw/'; then
  echo "Refusing to commit anything under data/raw/" >&2
  exit 1
fi
# The same check as an R function, run by whatever CI you have.

A hook that refuses is worth more than a policy that reminds. The failure mode is someone in a hurry at 6pm, and a reminder does not survive that.

Add a scan for the shapes secrets take, because a token pasted into a script is the other common case. gitleaks and detect-secrets both do this in a CI step.

What to do if it has already happened

The order matters and the first step is not technical.

One: report it. Your organisation has a data-incident procedure. This is one, and whether the repository was public decides how urgent rather than whether.

Two: rotate anything that was a credential. A committed token is compromised permanently; deleting it changes nothing.

Three: rewrite the history, with git filter-repo or BFG, and force-push. Every existing clone must be deleted and re-cloned — a clone nobody re-cloned still has the file.

Four: assume it was fetched. On a public repository, assume the file was retrieved and mirrored, and treat the disclosure as complete.

git filter-repo --path data/raw/beneficiaries.csv --invert-paths

Step three is the only one that looks like a fix and it is the least important.

Commits on analysis work

The habits differ from software work in two ways worth naming.

Commit the code and the output together when the output is committed. A figure and the script that made it belong in one commit, so a reviewer can see whether the chart matches the change.

Write the why in the message. fix commune names is what the diff already shows. “Normalise four spellings of Nord-Ouest; ungrouped, the worst district splits into four fragments and none of them looks alarming” is the thing a successor needs and cannot recover.

git commit -m "Group four spellings of Nord-Ouest before computing coverage

Ungrouped, the worst district splits into fragments of 727, 33, 22 and 20
households and none of them looks alarming. The 20-household fragment shows
0% open defecation, which a district table would report as a solved problem."

Branch per analysis question, not per day. A branch that answers “does coverage differ by district” can be reviewed and merged; a branch called work cannot.

What a repository of programme analysis should contain

project/
  .gitignore          committed first
  README.md           what it produces, how to run it, what to know
  uv.lock             the environment, pinned
  src/                every transformation
  tests/              the checks that fail loudly
  data/
    raw/              .gitkeep and CHECKSUMS only
    reference/        committed: thresholds, admin codes, lookup tables
  outputs/            ignored

Reference tables are committed and raw data is not, and the line between them is whether the file describes people. WHO growth standards, IPC thresholds and admin boundary codes are reference; a household list is not.

Report it whole

Data handling in this analysis

  No raw data is committed. data/raw/ is excluded by .gitignore, with only a
  CHECKSUMS file tracked so the exports used can be identified.

  A pre-commit hook refuses any staged file under data/raw/ and a CI step
  scans for credential patterns.

  Reference tables (WHO growth standards, IPC thresholds, admin codes) are
  committed; they describe no individual.

  The repository is private and access is limited to the M&E team. Raw
  exports are held in [the organisation's storage], not here.

The last line is the one that gets forgotten and it is the one an audit asks for. Saying where the data is matters as much as saying where it is not.

What comes next

Code in version control still does not reproduce a number if the environment differs. The next lesson pins it, and finds the four things that make a rerun differ when nothing in the repository changed at all.

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.