Cleaning, Structuring and Grounding Historical Data

One dataset, start to finish, with a coding agent

Jim Clifford, University of Saskatchewan

15 October 2026

Thousands of pages like this…

Two facing pages, 561 and 562, of the Colonial Office List for 1913: dense columns of abbreviated biographies from KELLY to KING

…became the moving atlas on the next slide.
The Colonial Office List for 1913 (52nd publication), pp. 561–562. Scan digitised by Google, from the Internet Archive.

What one arc needs

WODEHOUSE, SIR PHILIP E.—Entered the Ceylon civil service as a writer, May, 1828; … superintendent of Honduras, 1851; … governor of British Guiana; … in 1861 was appointed governor of the Cape of Good Hope.

Printed wording Event Historical place Plotting seat Year
writer, May, 1828 writer British Ceylon Colombo 1828
superintendent of Honduras superintendent British Honduras Belize 1851
governor of British Guiana governor British Guiana Georgetown 1854
governor of the Cape of Good Hope governor Cape Colony Cape Town 1861
governor of Bombay (later editions; India Office List) governor Bombay Presidency Bombay 1872–77
Printed clause“governor of the Cape of Good Hope”
→
Eventgovernor · 1861
→
Checked place identifierCape Colony, a Wikidata item
→
Plotting seatCape Town: a proxy, not a residence
→
ArcBritish Guiana → Cape

Two Lists, one career, mapped

Colonial Office List, 1878 · raw OCRWODEHOUSE, SIR PHILIP E., G.C.S.I. (created, 1876), K.C.B. (Creat. 1862).—Entered the Ceylon civil service as a writer, May, 1828; promoted to be assistant colonial secretary and clerk of the executive and legislative councils, Oct., 1833; district judge of Kandy, 1840; government agent for the western province, 1843; was appointed superintendent of Honduras, 1851; in Feb., 1854, governor of British Guiana; was employed in 1858 on a special mission to the government of Venezuela; and in 1861 was appointed governor of the Cape of Good Hope, and also high commissioner in South Africa. Governor of Bombay, 7th May, 1872, to 30th April, 1877.
India Office List, January 1889 · raw OCRWODEHOUSE, SIR PHILIP EDMUND, K.C.B., G.C.S.I.—Writer, Ceylon civil service, May, 1828; assistant colonial secretary, and clerk of the executive and legislative councils, Oct., 1833; district judge of Kandy, 1840; government agent for the Western Province, 1843; superintendent of Honduras, 1851; governor of British Guiana, 9th Feb., 1854; employed on a special mission to Venezuela, Feb.-May, 1858; C.B., May, 1860; governor and commander-in-chief of the Cape of Good Hope and its dependencies, and Her Majesty's high commissioner for the settling of the affairs of the territories adjacent to the eastern frontier of that colony, 29th Oct., 1861; K.C.B., July, 1862; governor of Bombay, 6th May, 1872, to 30th April, 1877; G.C.S.I., June, 1876.
Ceylon1828–51 Honduras1851 British Guiana1854 Venezuela 1858 Cape of Good Hope1861 Bombay1872–77
One man in two Lists? The same name and a K.C.B. of 1862 in both entries link the records into one career.
The Lists disagree: Bombay from 7 May 1872 (Colonial Office) or 6 May (India Office)? Keep both readings, each with its source.

Two printed records → one linked career → arcs between dated postings, not journeys.

What the records make visible

46,926person records across two Lists
305,164career events
211,581events linked to a Wikidata place
36,431arcs for 16,287 roster records with moves (September build)
  • Administrative accounts of careers, offices and honours.
  • Person counts resolved within each List; their sum includes cross-service overlap.
  • Seats used for plotting; arcs connect postings, not observed journeys.
  • People are another matter: only 1,066 of 27,526 Colonial Office people (3.9%) link to a Wikidata person.

Whose careers does this source make easy to see? Whose does it leave out?

Three questions

The question You decide
Structure What is a record here? What counts as an event
Clean One person or two? Merge, keep apart, review
Ground Which place, which person? Supported, unresolved, wrong
OCR text85 volumes
→
Structureentries → events
→
Cleaneditions → people
→
Groundnames → identifiers
→
Graph and atlasvisualise, then look
↺ See · Fix the rule · Rerun for every affected record

The source

BALMER, F. E.—Dep. chief acctnt., G.P.O., E. Africa Prot., Mar., 1919; ch. acctnt., G.P.O., Kenya, Oct., 1919; ch. acctnt., Kenya-Uganda-Tanganyika post and tel. service, 1933; senr. dep. P.M.G., do., 1935.

85 volumes OCR’d, 1867–1966, including later Dominions and Commonwealth Relations Office Lists. Services of principal officers from 1867; the full Record of Services from 1883. The same person returns in many editions.

What is OCR? Optical character recognition turns a page image into text. These volumes were read by olmOCR, an open model that returns text blocks with their position on the page. OCR errors and printers’ errors look alike in the text; the scan decides.

What is “do.”? Ditto: “the same as before”. The printer saved space by pointing back to an earlier place or office. A reader resolves it without thinking; a program has to be told.

Mark up Balmer

Choose entry 2. Tag the last two positions, their places and years.

Three minutes, then compare:

What does “do.” inherit?
Where should that decision be recorded?

QR code for the mark-up exercise

QR code for the mark-up exercise

jimclifford.ca/imperial-careers-workshop/annotate/

Start with the raw OCR

I showed Claude Code the text and discussed how to structure it.

ch. acctnt., Kenya-Uganda-Tanganyika post and tel. service, 1933; senr. dep. P.M.G., do., 1935.

Could Python do this on its own? What would the rules need to know?

Read a sample together. Agree what counts as a record and what fields to keep.

edition 1936 · printed page 614
OCR page index 832 · block 11
bbox [142, 2184, 827, 2334] · category text

Balmer’s original OCR block: every record keeps its way back to the page

What is Claude Code? Anthropic’s coding agent. It runs on your own computer, reads the files you point it to, and writes and runs Python with your permission. You describe the task in plain language and judge the results; it reports what it did.

It is a commercial service, paid through a Claude subscription or per use. Here it built the pipeline; it did not read the 65,000 career records.

Real rules: mostly right, quietly wrong

Our real rule-based parser: 258 lines of Python, run on every printed biography in 66 editions. It splits a career at semicolons, knows months, abbreviations and ditto marks, and refuses an entry whole when a dated clause will not parse.
EntryPrinted clauseThe rules returnedVerdict Balmer, 1936ch. acctnt., G.P.O., Kenya, Oct., 1919place: Kenya · 1919✓ the regular shape works Balmer, 1936senr. dep. P.M.G., do., 1935place: Dominions Office · 1935✗ two rules collide Connolly, 1940agt. gen. in London for Malta, 1930place: Western Australia · 1930✗ accepted, and wrong Wodehouse, 1867and in 1861 was appointed governor of the Cape of Good Hopeentry refused → Qwen: 9 events, each placed✓ refused, not guessed
78% of 202,100 printed biographies parsed by rules
61% → 96% rules share, 1888 vs 1960 editions
3,755 places dropped mid-clause, recovered later

Rules are fast and exact on regular entries. Each new rule can collide with another, and the dangerous errors are the confident ones.

Too irregular for regular expressions alone

A familiar historical problem: semi-structured data.

The fields recur. The wording and relationships vary.

Source wording What needs interpretation
“do., 1935” Which preceding place does this posting inherit?
“B. Honduras, Apl., 1898” An appointment date; “B.” belongs to the place name
Trimen’s posts, 1867–1875 and 1869–1879 Two concurrent jobs, not one sequence

Finding a year is easier than deciding what it dates.

What is a regular expression? A pattern that matches text by its shape. 1[89]\d\d finds every year from 1800 to 1999 in an entry. It cannot say which posting the year belongs to, or that “do.” points back three clauses.

Develop the hybrid with Claude Code

Agree the fields → try examples → decide which work belongs in each stage.

Stage 1 · reading every edition, about 200,000 printed biographies

Python rulesParse the regular entries; refuse the rest whole.
→
QwenReads the refused entries.
→
LinkEntries joined into people across editions.

Stage 2 · the final records, 65,239 careers

PythonExpands known abbreviations; keeps the rest as printed.
→
Qwen reads every careerOne entry in, one JSON record out: position, place and dates bound across ditto, elided and concurrent clauses.
→
Python checksDrops any place or year not in the source; applies logged fixes.

What is Qwen? A family of open-weight language models from Alibaba’s Qwen team. Open weight means the trained model can be downloaded and run on your own hardware, or reached through a hosted service. The structuring runs used Qwen3.6-35B-A3B: 35 billion parameters in all, about 3 billion active for each word-piece, so one GPU can run it quickly.

Claude Code helped build the pipeline. Qwen read the entries.

Three ways to run Qwen

The script sends each entry to a web address that speaks the OpenAI request format.
Change one address to change where the model runs.

Hosted service

OpenRouter

A broker for many models, open and commercial. Pay per token; no hardware.

Used in June to compare several open models on the same 40 entries, scored against records Claude had structured in-session. Qwen3.6 came closest.

Desktop GPU

NVIDIA DGX Spark

A desktop machine with 128 GB of memory shared with its GPU. vLLM serves the model on the local network.

India Office List: 30,496 entries in 8.24 hours overnight. No per-request bill.

Research cluster

Alliance: Narval, Nibi

Canada’s national research computing. Submit a batch job; it waits in a queue, starts the model, works through a file.

Qwen3.8-27B judged 3,539 identity pairs in 35 minutes on one A100 GPU.

What is vLLM? Open-source software that loads a model onto a GPU and answers requests in the same format as the commercial services, so the same script works locally, on a cluster or through OpenRouter.

The prompt is the decisions

Extract ONLY what is printed; never invent or infer facts.

events: one object per distinct posting. EXPLODE compound clauses.

Place rule, summarised: keep printed wording; flag inheritance and stop at a stated transfer.

birth_year: only if printed. NEVER infer from the earliest career date.

What is a system prompt? The standing instructions sent with each of about 65,000 career records. The entry itself follows as the user message. Every rule here exists because an earlier run broke it.

Read records against the source

Examples from the saved extraction audit, before rule fixes

Source Earlier output Reading to preserve
“do., 1935” place: “do.” Inherit the preceding place
“Apl., 1898” birth_year: 1898 Date of the appointment
“KT. BACH. (1920)” two honours One knighthood
Sampleevery tenth record, then spread across the corpus
→
Read against the sourcerecord beside the printed entry
→
Sort findingsone-off, or a recurring pattern?
→
Logged ruleor individual review

Batch run: 30,496 entries in 8.24 hours.

Replay a past correction

Balmer’s final event · saved extraction output

Beforeplace: “do.”
place_inherited: true
→
RuleA literal ditto, and the previous event has a resolved place → inherit that place and log the reason
→
Afterplace: “Kenya-Uganda-Tanganyika”
place_inherited: true

3 saved records · 12 events · 1 place corrected

  • Other events and fields unchanged.
  • Second run changes nothing.
  • No preceding resolved place? Leave it for review.

Saved before-and-after records

Balmer’s ditto, resolved

BALMER, F. E.—Dep. chief acctnt., G.P.O., E. Africa Prot., Mar., 1919; ch. acctnt., G.P.O., Kenya, Oct., 1919; ch. acctnt., Kenya-Uganda-Tanganyika post and tel. service, 1933; senr. dep. P.M.G., do., 1935.
do. = ditto, “the same as before”: it points back to the last place named.
{ "position": "senior deputy P.M.G.",
  "place": "do.","Kenya-Uganda-Tanganyika",
  "year_start": 1935, "place_inherited": true }
+ change logged: “Printed ditto; previous event has a place.”

A reader resolves it at a glance. A program has to be told, and the change is logged.

Clean: forty editions, one person

A missed link leaves a fragmented career.
A false merge can silently corrupt later analysis.

Ties resolve to no merge.

199,099printed entries, Colonial Office List
→
Strong rulessame name and birth year, or a shared dated event
→
Clusterssame forename, same surname ending, so OCR variants meet
→
Dossiersa model argues each hard case; review queue for the rest
→
27,526person records, with a ledger of reasons

Dossier Every version of every biography in a cluster, laid out for one decision: births, schools, honours, the full dated career.

Ledger An append-only file of merge decisions, each with its reason, so any one can be revisited and reversed.

Decide before the answer

One edition

ABERY, Joseph

M.B.C.M.

1867 · Ceylon · assistant colonial surgeon

Six editions

CARBERY, Joseph

M.B.C.M.

1867 · Ceylon · assistant colonial surgeon

Merge, keep apart, or review? What evidence decides it?

The ledger merges them: consistent name and qualification, an identical dated appointment, and a plausible OCR loss. Preserve both entries and the reason.

Keep apart; keep the evidence

Born 1836

A’BECKETT, Thomas

Lincoln’s Inn

Victorian judge · knighted 1909

Born 1898

BECKETT, Thomas Rumbold

Judd School

Nigerian telegraph engineer

Different schools and careers, not only different birth years. Keep apart.

The merge rules, in brief Merge only on positive evidence: an overlapping dated event, a shared honour with its year, near-identical education, or an OCR variant with a consistent career. Birth year alone never splits: it is often misread. Different spelled forenames keep people apart.

Ground: retrieve, read, decide

Every plotted posting needs a place.
Every shared person identifier asserts an identity.

Mention“Victoria, 1912”
→
Retrievecandidate items from a search tool
→
Readtype, dates, location against the career
→
Decide and recordsupported · unresolved · wrong, with evidence and date

No supported match? Keep the local identifier.

What is Wikidata? A QID? A free, shared knowledge base. Each item has an identifier: Q plus a number. Q1800510 is Philip Wodehouse. Linking to it lets other projects join their data to ours.

What is MCP? Model Context Protocol: a standard way to hand a model a tool, such as a Wikidata search. The answer comes back as retrieved evidence, not from the model’s memory.

One entry, three kinds of link

LOGAN, Sir William Marston, K.B.E. (1944) … —b. 1889; ed. Bristol and Brasenose Coll., Oxford …; ch. sec., N. Rhod., 1937; ag. gov., Dec., 1937 …; gov. and c.-in-c., Seychelles, 1942; retd., 1947 …
Places · WikidataMCP search “N. Rhod.” → Northern Rhodesia “Seychelles” → Seychelles Read the candidate’s type, country and dates; prefer the colonial-era entity. 131,343 of 152,132 placed events (86%)
Person · QLever query name · b. 1889 · Northern Rhodesia → William Marston Logan Q108649219. Surname, given names, birth year and office all agree; died 1968, after his last listing. 1,066 of 27,526 people (3.9%)
School · WikidataMCP search “Brasenose Coll., Oxford” → Brasenose College Q899030. “Bristol” stays unlinked: a city, not a named school. 11,056 of 15,775 education links (70%)

Places are many and easy to check. People need more evidence, so most stay with a local identifier.

WikidataMCP: search by meaning, then read

Printed string“Penang, S. Sttlnts.”
→
Normalised query“Penang Straits Settlements”
→
search_itemscandidates ranked by meaning, not spelling
→
get_statementsinstance of · country · located in · coordinates
→
Cache the decisionwith its evidence, so the graph rebuilds without new lookups
Rules the agent followed

Prefer the colonial-era entity: British Ceylon, not Sri Lanka.

Leave compounds and ambiguous initialisms (“Ken. and Uga.”, “N.S.”) ungrounded.

Never write an identifier from memory.

What the checks caught

A later check of every identifier caught five invented by parallel agents: Dehra, Ayrshire, Rohilkund, Neemuch, Narsinghpur.

Fuzzy reuse flipped N./S. and E./W.; only exact spelling variants were folded (166).

What is vector search? Each Wikidata item is stored as a list of numbers that captures what its description means, so a query finds items close in meaning, not only in spelling. It still returns candidates, not answers: on 11 October “Penang Straits Settlements” put the whole colony first, the modern Malaysian state eighth and Penang Island 24th. Reading the statements decides.

QLever: ask exactly when you can

Who held a position named “Governor … Northern Rhodesia”? Give their birth dates.

SELECT ?person ?personLabel ?dob WHERE { ?pos rdfs:label ?posLabel . FILTER(CONTAINS(?posLabel, "Northern Rhodesia")) FILTER(CONTAINS(?posLabel, "Governor")) ?person wdt:P39 ?pos ; wdt:P31 wd:Q5 . OPTIONAL { ?person wdt:P569 ?dob } }

1 Birth within two years of the List’s 1889

2 Same surname and given names: LOGAN, William Marston

3 Death check: died 1968; last listed 1948 ✓

Wikidata holders returnedborn
William Marston Logan1889
Sir John Maybin1889
John Waddington1890
Hubert Winthrop Young1885
Ronald Storrs1881
Herbert Stanley1872
James Crawford Maxwell1869
1,095 senior officials without a link
198 colony names queried
124 people linked this way

What are SPARQL and QLever? SPARQL is the query language for Wikidata: ask for every item matching a pattern of statements, such as “held this office and was born in…”. QLever, from the University of Freiburg, is a fast public engine that runs such queries over all of Wikidata.

What reading the corpus costs

Measured on a random sample of 2,000 India Office entries and 2,000 Qwen records:

~640tokens read per entry: 470 of prompt, about 150 of entry
~330tokens written per entry: the JSON record
30,496India Office List entries
Route Price 30,496 entries
OpenRouter, Qwen3.8-27B, 8 Oct $0.0239 read / $2.175 written per million tokens (50% promotion) ≈ $22
Same model, regular price about twice the promotion ≈ $45
DGX Spark desktop, Qwen3.6 (as run) the machine and its electricity 8.24 hours, no bill per request

Almost all of the hosted cost is the writing: the record the model returns, not the text it reads.

What is a token? The unit models read and write in, roughly three-quarters of a word. Services charge per million tokens, with separate prices for reading (input) and writing (output).

A past repair: which Victoria?

Brewster’s entry: “defeated by Sir Richard McBride, in Victoria, 1912.”

An earlier atlas placed a British Columbia politician in Australia.

Colony of Victoria, AustraliaVictoria, British ColumbiaVictoria, Hong KongVictoria, SeychellesVictoria, Labuan

One printed word; several imperial places with that name.

BeforeOne “Victoria” lookup, reused for every entry that printed the word → Australia
→
Completed fixResolve the place per person, from the surrounding career: McBride, the legislative assembly, British Columbia
→
AfterVictoria, British Columbia; every affected entry re-emitted

Which Victoria?

Brewster: “defeated by Sir Richard McBride, in Victoria, 1912.”

Victoria, British Columbia Colony of Victoria Victoria, Hong Kong Victoria, Labuan Victoria, Seychelles Before: one match, reused for everyone Career context: McBride · legislative assembly After: Brewster placed in British Columbia

Resolve the place per person, from the surrounding career. Every affected entry is re-emitted.

A past repair: what counts as an event?

Source Earlier graph, after the place fix
Defeated, Victoria, 1912 Legislative assembly member for Victoria, 1912–1916

Would you accept this office-held assertion?

A defeat does not establish an appointment. Repaired: preserve the defeat as an event; remove the office-held assertion. Check other defeat clauses.

A plausible map can conceal a misread source.

What a map cannot check

The map shows

What looks wrong

A politician in the wrong hemisphere. A jump no career could make. Two nodes with one name.

The map hides

What is missing

A missed link leaves two plausible short careers. An ungrounded posting simply vanishes. A false merge can look like an ordinary career.

Use a labelled sample, including the cases the pipeline rejected.

Check both the repaired cases and other records the rule changed.

Three decisions, with reasons

Work from the saved teaching cases.

R1 · Blakely: accept the record?
R3 · Abery / Carbery: accept the merge?
R8 · Harris: accept a link with a disputed birth year?

Decide. Give the evidence. Say what you would check next.

Six minutes working; four minutes discussing R8.

QR code for the three-decision exercise

QR code for the three-decision exercise

jimclifford.ca/imperial-careers-workshop/review/

Your first twenty records

  • Show the coding agent your raw text, including a hard case.
  • Discuss what Python rules can handle and what needs interpretation.
  • Agree a schema and review two examples against the source.
  • Build the hybrid; run a small sample and record corrections.
  • Rerun, check what changed, then measure a larger run.

Write one sentence: what counts as a record in your collection?

Keep the decision with the evidence

Structure · Clean · Ground

Build. Extract. Visualise. See. Fix. Rerun.

QR code for the workshop site

QR code for the workshop site

jimclifford.ca/imperial-careers-workshop
Exercises, worked answers, prompt, replay and glossary.