Case study A
How I built one dataset I could genuinely trust out of seven sources that all disagreed with each other — and how I found out, much later, that the thing holding it had been quietly falling apart the entire time.
When I took this on, the dataset already existed. It had been inherited, extended by hand for years, and nobody could tell me which parts of it were still true. Roughly 1,800 entities, each belonging to one of seven independent authorities that publish their own membership separately, in their own formats, on their own schedules.
It was wrong in both directions, and that's what makes this kind of work hard. Missing entries are annoying but survivable — someone eventually asks for one and you notice. Entries that shouldn't be there are the real problem. They look completely valid. They pass every format check you throw at them. And they quietly waste somebody's time every single time the dataset gets used, because nothing ever appears broken.
Anything easy to check was already out of date. Anything current had to be read off a page that was never meant to be read by a machine.
My first instinct was the obvious one: take each authority's published membership list, merge them, done. It failed immediately. Those lists were stale in all seven — every single one. They're maintained for administrative reasons and they lag reality by months, sometimes years. An entity can sit happily on a membership list long after it's stopped doing the thing the dataset is actually about.
So I threw out “compile from lists” and replaced it with four rules. Each one cost me time. Each one caught something the old way had waved straight through.
1,757 records across 161 groupings and 7 authorities, every one carrying the source it was confirmed from and the date I confirmed it. Sixty-two entries came out — each confirmed twice, against independent sources, as not qualifying at all — and thirteen that did qualify were missing entirely and went in.
| Source authority | Groupings | Records | Primary contact named | Coverage |
|---|---|---|---|---|
| Authority 1 — three tiers | 98 | 1,044 | ~95% | strong |
| Authority 2 | 20 | 208 | ~66% | workable |
| Authority 3 — regional | 24 | 370 | ~52% | uneven |
| Authorities 4–7 | 19 | 135 | ~43% | uneven |
| Total | 161 | 1,757 | 99% | 98.5% usable |
Completeness per authority rather than averaged. 1,147 rows clean, 610 flagged for follow-up — reported as a known state, not tidied away.
Coverage runs from about 95% at the best-resourced authority down to about 43% at the smallest. That spread isn't a failure of the search, it tracks publishing practice. On hundreds of pages a primary contact's address is published and the secondary contact's field just sits there empty. I'd much rather say that plainly than average it away, because it tells anyone using this exactly where they'll have to do the work by hand.
The build worked. The container didn't — and I didn't find out until I ran a structural audit, long after I'd finished congratulating myself.
What I'd actually built, without quite noticing, was a four-table relational database living inside a spreadsheet:
Entities one row per organisation └─< Offerings one row per offering links by entity_id └─< Roles one row per person × offering links by offering_id └─ People one row per person links by person_id
A spreadsheet will hold this structure quite happily. It will not enforce a single one of those links, and it will never mention that to you.
Two things went wrong because of it, and both were completely silent.
A batch operation handed new identifiers to around 199 entities without moving their children or clearing out the originals. What I ended up with was 157 duplicate clusters — the same organisation existing twice, with its children divided between the two copies. Neither copy was complete. And nothing looked broken, because every individual row was perfectly valid. Any query against either copy just returned a confident, partial answer.
Every child row carried its own copy of the parent's name instead of pointing at it. The moment one copy got hand-edited, they drifted apart — and the same organisation started appearing under several spellings that no exact-match join was ever going to put back together.
Names couldn't identify the duplicates, because the drifting names were the problem. Coordinates could. Two records sharing a coordinate pair are the same physical organisation no matter how anyone typed the name, and that's what I built the merge map from: 157 high-confidence clusters, plus 65 I held back for manual review — most of which turned out to be genuinely different organisations that just happened to round to the same spot.
The permanent fix was adopting a national registry identifier as the key. A stable external ID that no hand-edit can knock out of alignment, rather than one I'd generated myself.
I'd built a rigorous verification process for my data, and absolutely none for the thing holding it.