Persoonsreconstructies voor de Gouda Tijdmachine — uitleg versie 1 (5 juli 2026)

De pipeline verwerkt ruim anderhalf miljoen persoonsvermeldingen: losse waarnemingen van personen in historische bronnen - een regel in een volkstelling, een naam op een gezinskaart, een vermelding in een adresboek. Ze komen uit twee bronnen: de Gouda-Tijdmachine-dump (±1,05 M, SAMH) en een OpenArchieven/non-SAMH-oogst (±0,58 M). Dezelfde historische persoon komt in tientallen bronnen voor - vaak in beide bronnen - maar niets in de data zegt dat die vermeldingen bij elkaar horen. Dit document beschrijft de pipeline die ze 73% van deze persoonsvermeldingen samenbrengt tot bijna 217 duizend persoonsreconstructies: één knoop per (vermoedelijke) historische persoon, met provenance en een expliciete zekerheidsscore per onderliggende vermelding.

 

Requirements

Voordat het conceptuele model en de implementatie aan bod komen, eerst de eisen die het reconstructie-algoritme moet inlossen. Ze zijn deels functioneel (wat moet het opleveren) en deels kwalitatief (onder welke randvoorwaarden); samen stuurden ze elke ontwerpkeuze die ook verderop in dit document worden beschreven.

Functionele eisen

  • R1 » Vermeldingen samenbrengen. Losse persoonsvermeldingen (ook wel persoonsobservaties genoemd) uit heterogene historische bronnen worden automatisch gegroepeerd tot persoonsreconstructies: één knoop per vermoedelijke historische persoon.
  • R2 » Brondata onaangetast. Observaties blijven brongetrouw en onveranderlijk; het algoritme legt een interpretatielaag erbovenop en wijzigt de bronnen nooit.
  • R3 » Geen verzonnen personen. Een reconstructie ontstaat alleen uit werkelijke observaties. Sinds 2026-07-11 krijgt ook een singleton (één observatie) een 1-lid-reconstructie, zodat élke persoonsvermelding één stabiele reconstructie-URI heeft en de presentatie uniform is; de provenance blijft eerlijk (één prov:wasDerivedFrom, géén gtm:confidence want er zijn geen interne paren). Uitgesloten blijven volledig naamloze vermeldingen en verponding-regels die een gebouw/instelling beschrijven.
  • R4 » Volledige herleidbaarheid. Elke reconstructie wijst via provenance terug naar haar observaties én naar de run (versie + drempel) die haar produceerde.
  • R5 » Zekerheid per bewering. Elke observatie-in-reconstructie draagt een expliciete zekerheidsscore en de zwakste-schakel-score, zodat afnemers zelf strenger kunnen filteren dan de pipeline deed.
  • R6 » Belang relaties. Familierelaties (partner/ouder/kind) worden van observatie- naar reconstructieniveau geheven, inclusief signalering van tegenstrijdigheden (betwiste ouders).

Kwaliteits- en randvoorwaarden

  • R7 » Transparant, geen black-box. De matchbeslissing is verklaarbaar: een gewogen som van benoembare kenmerken met een feature-breakdown per paar; de gewichten zijn met de hand instelbaar.
  • R8 » Precisie boven volledigheid. Liever een beoordeelbare grijze zone dan een foute samenvoeging; harde afwijzingen en merge-guards voorkomen onmogelijke en transitief-doorgeslagen merges.
  • R9 » Gekalibreerde zekerheid. De zekerheidsscore is empirisch gekalibreerd, zodat een drempel een uitspraak over precisie is en geen willekeurig getal.
  • R10 » Standaarden hergebruiken. Het algoritme verzint geen eigen datamodel: het consumeert bestaande vocabulaires (PiCo/PNV/schema.org/PROV/RiCo) en levert de uitkomst als RDF (N-Triples).
  • R11 » Stabiele identifiers. Reconstructie-URI's blijven over reruns heen stabiel (persistent UUID-register), met navolgbare tombstones bij fusie of splitsing.
  • R12 » Reproduceerbaar en modulair. De uitkomst is deterministisch en elke fase is idempotent en los herdraaibaar.
  • R13 » Menselijk beoordeelbaar. Grijze-zone-paren, betwiste relaties en zwak verbonden clusters zijn via een webapp door mensen te beoordelen; die oordelen overleven reruns en voeden de volgende kalibratie.
  • R14 » Haalbaar op één machine. Een volledige run over ~1,05M observaties past in het geheugen van één machine en duurt ±15 minuten; drempels en gewichten zijn configureerbaar.

De rest van dit document laat zien hoe elke fase deze eisen invult: het conceptuele model (R1-R3) en de provenance (R4-R5) in §1 en §9, de transparante scoring (R7) in §7, de guards en grijze zone (R8) in §7-§8, de kalibratie (R9) in §10, de standaarden (R10) in §2, en het UUID-register (R11) in §9.

 

1. Conceptueel model

De pipeline volgt het Persons in Context-model (PiCo), dat een strikt onderscheid maakt tussen twee soorten entiteiten:

  • Een persoonsobservatie (pico:PersonObservation) is één waarneming van een persoon in één bron: "D. G. van Vreumingen" in het adresboek van 1902 is een andere observatie dan "Dirk Gijsbertus van Vreumingen" in een huwelijksakte uit 1879 - ook als het dezelfde persoon betreft. Observaties zijn brongetrouw en onveranderlijk.
  • Een persoonsreconstructie (pico:PersonReconstruction) is een interpretatie: de bewering dat een verzameling observaties over dezelfde historische persoon gaat. Reconstructies zijn herleidbaar (elke bewering wijst terug naar haar observaties), gedateerd (elke run is een prov:Activity) en vervangbaar (een betere matcher levert betere reconstructies zonder de brondata te raken).

Drie ontwerpprincipes sturen de hele pipeline:

  1. Transparantie boven black-box. De matching gebruikt een gewogen som van benoembare kenmerken, geen geleerd model. Elk paar observaties heeft een feature-breakdown (welke kenmerken droegen hoeveel bij) die integraal in de beoordelingswebapp getoond wordt. Dit maakt fouten verklaarbaar en gewichten bijstuurbaar.
  2. Liever grijze zone dan foute merge. Paren met te weinig bewijs worden niet samengevoegd maar geparkeerd in een beoordeelbare grijze zone. Een observatie die nergens aan koppelt wordt een 1-lid-reconstructie: liever een eerlijke singleton-reconstructie dan een geforceerde merge.
  3. Provenance overal. Elke reconstructie draagt per lid een zekerheidsscore, elke run een versie- en drempelvermelding; afnemers kunnen zelf strenger filteren dan de pipeline deed.

 

2. Standaarden en vocabulaires

Standaard / vocabulaire Namespace Rol in de pipeline
Persons in Context (PiCo) https://personsincontext.org/model# Kerntypen PersonObservation en PersonReconstruction; pico:hasAge, pico:hasRole op observaties
Person Name Vocabulary (PNV) https://w3id.org/pnv# Naamdelen: baseSurname, surnamePrefix, patronym, initials, literalName; ook op de reconstructie (pnv:hasName)
schema.org https://schema.org/ Feitelijke persoonsvelden: givenName, familyName, birthDate, deathDate, birthPlace, gender, spouse/parent/children, datePublished, temporalCoverage
PROV-O http://www.w3.org/ns/prov# Herleidbaarheid: wasDerivedFrom, qualifiedDerivation (met zekerheid), wasGeneratedBy; hadPrimarySource koppelt de observatie aan haar bronscan én - voor SAMH-genealogie - aan de akte-URI (zelfde-akte-constraint)
Records in Contexts (RiCo-O) https://www.ica.org/standards/RiC/ontology# Registerperiodes: hasBeginningDate/hasEndDate op RecordSets (bevolkingsregisters)
Thesaurus Historische Persoonsgegevens https://terms.personsincontext.org/... Rollen van observaties (overledene, huwelijkspartner, kind)
gemeentegeschiedenis.nl https://gemeentegeschiedenis.nl/gemeentenaam/... Plaats-URI's: plaatsvergelijking op identiteit i.p.v. string
Historical International Standard Classification of Occupations (HISCO) via api.coret.org/hisco Beroepsnormalisatie in de brondata (deels)
Archival Resource Key (ARK) https://n2t.net/ark:/60537/... Persistente identifiers van observaties en scans
N-Triples RDF 1.1 Uitwisselformaat voor input (dump) en alle output
eigen namespace gtm: https://www.goudatijdmachine.nl/def# confidence, minEdgeScore, disputedParent, softwareVersion, threshold, adrespredikaten (straat, huisNummer)

De pipeline verzint dus geen eigen datamodel: hij consumeert PiCo/PNV/PROV/ schema.org zoals de Gouda Tijdmachine die al publiceert, en produceert persoonsreconstructies conform PiCo met PROV-provenance in dezelfde stijl.

 

3. Architectuur

 

Technische componenten BRONNEN goudatijdmachine.nt + non-samh.nt.gz LOD-dump (SAMH) + OpenArchieven (non-SAMH) MariaDB gendex, synoniem_voornaam/-familienaam SPARQL / Omeka S API alleen voor steekproef-verificatie, niet voor bulk VERWERKING · Python 3.13 (venv) pipeline-scripts 10 ... 75 run_all.sh [startfase] · env: T_ACCEPT / T_GREY / BLOCK_CAP / NON_SAMH_NT (2e bron, fase 11) volledige run ± 30 min · 1,63M obs, in-memory gedeelde modules config.py - paden, drempels, gewichten normalize.py - namen, initialen, datums scoring.py - score_pair() + harde regels bibliotheken DuckDB (SQL, parquet) · pyarrow (bulk-I/O) rapidfuzz (Jaro-Winkler) · metaphone (DM) jellyfish (classic metaphone) · Flask OPSLAG (data/, niet persistent vereist - alles reproduceerbaar) data/raw/*.parquet obs_props · pnv · rel · genid2 · itemsets · obs_A2A · rel_A2A gtm_edges · partof · ric_dates · A2A_oid_map (persistent!) data/pico.duckdb obs · rel · obs_ctx · cand · pairs · clusters recon · recon_rel · reject_stats · block_stats lexicons gender_names.parquet (74k voornamen) syn_voornaam.tsv · syn_familienaam.tsv OUTPUT (out/) N-Triples reconstructions.nt (+.gz) · 217k recon · 1,98 GB samh_datepublished.nt · 990k triples inferred_genders.nt · 1,03M triples rapporten stats.md - dekking, verdelingen, guards calibration.md - precisie per band samples/ - steekproeven + oordelen (JSONL) BEOORDELING webapp/app.py read-only op pico.duckdb oordelen → data/review_verdicts.duckdb (apart bestand: overleeft pipeline-reruns) Validatie: rapper (raptor2) op alle N-Triples-output · invarianten per fase (tellingen, geen reconstructie < 2 leden) Kalibratie: gestratificeerde steekproef per scoreband, 8 onafhankelijke LLM-beoordelaars, interne validatie met zelfde-akte-paren

De pipeline is een reeks losse Python-scripts rond één DuckDB-database, naar het voorbeeld van de "aanknopingspunten v3"-matcher van genealogieonline (die 76M personen in ±1,5 uur verwerkt). Bewuste keuzes:

  • Python 3.13 + DuckDB in een venv, alles op één machine. Bij 1,05M observaties past het hele probleem ruim in geheugen; een volledige run duurt ±15 minuten. Gedistribueerde infrastructuur zou alleen complexiteit toevoegen. DuckDB doet het zware verzamelwerk (joins, sampling, aggregaties, parquet-I/O); de per-paar-scoring is een strakke Python-lus met C-gebaseerde bibliotheken (rapidfuzz voor Jaro-Winkler, metaphone voor Double Metaphone).
  • Elke fase is een idempotent script (10_extract.py ... 75_calibrate.py), aangestuurd door run_all.sh [startfase]. Tussenproducten staan in parquet of DuckDB-tabellen; elke fase kan los herdraaid worden. Dure externe stappen (gendex-dump van 1,2 GB, lexicon-aggregatie van 12 minuten) zijn gecached en verversen alleen met FORCE=1.
  • Configuratie op één plek (config.py): paden, drempels (T_ACCEPT, T_GREY, BLOCK_CAP), alle scoregewichten en de lexicon-parameters. Drempels zijn ook als omgevingsvariabele te overriden, zodat een "maximale zekerheid"-run (T_ACCEPT=80 ./run_all.sh 50) geen codewijziging vergt.
  • Bulk-I/O via Arrow. Rijen één-voor-één in DuckDB stoppen (executemany) bleek ~100× trager dan een pyarrow-tabel registreren en met INSERT INTO ... SELECT overnemen; de pipeline gebruikt overal het laatste patroon.

 

4. Databronnen en extractie

Fase 10 - streaming parse van de LOD-dump

De bron is de N-Triples-dump die de Gouda Tijdmachine zelf publiceert (goudatijdmachine.nt, 5,4 GB, ~35M triples). Omdat het bestand subject-gesorteerd is, kan een enkele sequentiële pass volstaan: regels worden per subject-blok gebufferd en bij subjectwissel geflusht. Het rdf:type-triple in het blok beslist of het een pico:PersonObservation is. Blank nodes vergen aparte behandeling:

  • _:gennaam-<omeka-id> - PNV-naamdelen; de join met de observatie loopt via het Omeka-id (blank-node-labels zijn niet stabiel tussen dumps).
  • _:genid2-<sha> - beroepen, rollen en soms plaatsen, gekoppeld via schema:additionalType; naam en type worden verzameld en in fase 20 opgelost.

De parser haalt in ±40 seconden acht parquet-bestanden uit de dump: observatie-eigenschappen, PNV-naamdelen, familierelaties (ARK→ARK), titels van álle resources (voor bronclassificatie), adres-edges, isPartOf-verwijzingen en RiCo-registerdatums.

Fase 11 - tweede bron: OpenArchieven/PiCo

Naast de SAMH-via-Omeka-S-dump verwerkt de pipeline een tweede bron: non-samh.nt.gz (gzip N-Triples, ±0,58 M pico:PersonObservation), geoogst van Open Archieven (geboren Gouwenaars die voorkomen in andere archieven dan SAMH). Die records zijn structureel anders - schema.org-velden rechtstreeks op de observatie, géén omeka-o#id/item-set, blank-node-labels _:<record>xgenid<N>, en prov:hadPrimarySource wijst naar het bron-record (schema:ArchiveComponent) i.p.v. een akte of scan. Om de SAMH-via-Omeka-S-parser niet te raken, is er een aparte adapter (11_extract_A2A.py) die het gzip-bestand streamt en elke persoon meteen in de definitieve obs-vorm normaliseert (dezelfde normalize.py-helpers), plus een relatiebestand. Fase 20 voegt beide bronnen samen; alles stroomafwaarts blijft ongewijzigd. Drie bijzonderheden:

  • Stabiele synthetische oid's. De hele pipeline sleutelt op een geheel getal oid, en de UUID-continuïteit (fase 55) hangt af van stabiel lidmaatschap. Omdat deze records geen o#id hebben, deelt de adapter oid's uit een persistent register (data/A2A_oid_map.parquet) in een gereserveerd hoog bereik (≥ 10⁹, disjunct van de ~7-cijferige omeka-id's), sequentieel en herbruikbaar - dezelfde vermelding houdt haar oid over heroogsten heen.
  • Bronlabels oa:*. Er is geen item-set-titel; het bronlabel komt uit schema:additionalType van het record (§5).
  • Same-record-guard. Het record-URI wordt deed_uri, zodat de bestaande same_deed-afwijzing voorkomt dat twee in één record genoemde personen samensmelten (naast de related_persons-guard voor expliciete verwanten).

Fuller-fidelity: de pnv-blank node levert surnamePrefix/patronym, de registerperiode (bv. "Bevolkingsregister 1874-1893") vult het bronjaar en zet living_reg, en de leeftijd geeft - met dat bronjaar - een geboortejaar-interval. Beperking: non-SAMH-geboorteplaatsen zijn plaatsnamen (strings), geen URI's, dus non-SAMH↔SAMH-plaatsovereenkomst scoort op de zwakkere place_name_match (+6) i.p.v. birthplace_same (+10).

Fase 12 - lexicons uit Genealogie Online

Drie hulptabellen komen uit de MariaDB van Genealogie Online (en zijn dus deels gebaseerd op van genealogen afkomstige stambomen):

Lexicon Bron Inhoud Gebruik
Geslacht per voornaam gendex (46M personen met geslacht) 73.956 voornamen met F/M-verdeling, gefilterd op vertrouwen > 0,75 én n ≥ 10 geslachtsinferentie (§5)
Voornaamsynoniemen synoniem_voornaam ±8.000 ongerichte paren (aagje~agatha, teunis~theunis) voornaam- en gezinscontextvergelijking
Familienaamsynoniemen synoniem_familienaam 458.004 gesymmetriseerde paren na normalisatie ('s Gravemade~Schrama) achternaamvergelijking + blocking-pass 6

Bewust zonder transitieve sluiting: synoniemen gelden alleen als direct paar. Transitief sluiten smelt naamgroepen tot onbruikbare megaclusters (een les uit het Genealogie Online aanknopingspunten-project).

 

5. Normalisatie en verrijking

Fase 20 bouwt de centrale obs-tabel: één rij per observatie met alle matchbare, genormaliseerde kenmerken. De belangrijkste bewerkingen:

  • Naamdelen. PNV-velden hebben voorrang; ontbreken die, dan wordt schema:familyName gesplitst met een Nederlandse tussenvoegsellijst ("van der Berg" → prefix "van der", hoofddeel "berg"). Alles lowercase en diacrietvrij. Van de eerste voornaam en het achternaam-hoofddeel wordt een Double Metaphone-code berekend (fonetische sleutel voor blocking).
  • Initialen. Records als "D. G. van Vreumingen" krijgen is_initials_only = true; volledige namen krijgen afgeleide initialen ("Dirk Gijsbertus" → dg) zodat beide soorten in dezelfde blocking-pass vallen. Zit er een los achternaamwoord in het voornaamveld ("A. C. Visser" naast familienaam "Silvius" — dubbele achternamen die de bron inconsistent splitst), dan wordt dat gesplitst naar initialen "A. C." + familienaam "Visser Silvius"; anders zouden de initialen acv worden en co-blockt de vermelding nergens mee.
  • Datums met precisievlag. xsd:date versus xsd:gYear en patronen als "1809-10-00" leveren een precisie (dag/maand/jaar) op. Cruciaal voor de conflictregels: een jaar-precies "1580" mag nooit hard botsen met "1580-05-01".
  • Vermeldingsjaar-keten. Voor bronnen zonder eigen datums wordt het registratiejaar afgeleid, in volgorde van prioriteit: 1. jaartal in de item-set-titel ("Volkstelling Gouda 1830"); 2. schema:datePublished op de observatie zelf (adresboeken: 1867-1906); 3. "Periode: \<jaar>" in de titel van de bronscan (BS-registers, 645k observaties); 4. de registerperiode via scan → schema:isPartOf → RiCo-RecordSet met rico:hasBeginningDate/hasEndDate (bevolkingsregisters, 345k observaties, bereiken zoals 1880-1900). Daarmee heeft 99,9998% van de SAMH-records (op 2 na alle 990.156) een bronjaar of -bereik.
  • Bronclassificatie. De SAMH-bron wordt uit de item-set-titel bepaald (samh, volkstelling1830/40, adresboek, begraafplaats ...). De non-SAAH-records (fase 11) krijgen een oa:*-label uit schema:additionalType van het bron-record: de PiCo-sourcetype-URI's 554 → oa:bevolkingsregister, 552/553/551 → oa:burgerlijkestand, 549 → oa:dtb, plus trefwoord­regels op de vrije-tekst-labels (Militie*oa:militie, gevangenis*/CABR*oa:justitie, Bidprentjes*oa:bidprentjes, rest oa:overig). Een aparte woordenschat met oa:-prefix houdt de statistiek leesbaar en botst nooit met de SAMH-labels.
  • Geslachtsinferentie. schema:gender staat op slechts 6.137 observaties. Voor observaties zonder geslacht en zonder initialen wordt het gendex-lexicon geraadpleegd: bij vertrouwen > 75% krijgt de observatie een afgeleid geslacht, gemarkeerd als gender_src='inferred' (tegenover 'rdf' voor brongeslacht). Over beide bronnen samen krijgt zo de ruime meerderheid van de 1.631.282 observaties een geslacht (SAMH ~98%; non-SAMH 142k geregistreerd + 434k afgeleid). Het onderscheid rdf/inferred bepaalt hoe hard een geslachtsconflict weegt (§7).
  • Geboortejaar-interval uit leeftijd. Leeftijd × vermeldingsjaar(-bereik) levert een interval [jaar_min − leeftijd, jaar_max − leeftijd] op (ook voor non-SAMH-bevolkingsregisters via hun registerperiode).
  • Gezinscontext (obs_ctx). De observatie-relaties over beide bronnen (schema:spouse/parent/children, 263k partner- en 379k ouder-kind-paren) worden gedenormaliseerd tot per-observatie-lijsten: partnervoornamen, vader-/moedervoornaam, kindvoornamen. Zo kan de scoring gezinscontext vergelijken zonder joins per paar.
  • Verponding-prefix. Verponding-vermeldingen hebben een register-prefix in schema:name ("Verponding 993 (Kleiweg),1772, Hendrik Spamay"). Dat prefix wordt gestript zodat obs.name de persoonsnaam is; het origineel blijft bewaard in name_raw. De naamdelen kwamen al uit givenName/familyName en waren dus al schoon.
  • Weduwe-notatie. Adresboeken en registers noteren weduwen als "wed. L. C. Kint", soms met meisjesnaam: "wed. L. C. Kint geb. van Dam" of "wed. L. C. geb. van Dam Kint". Het weduwe-token wordt herkend (is_widow; geslacht 'f', gender_src='widow'), de initialen zijn die van de mán, en de meisjesnaam achter "geb." gaat naar maiden_surname. Zonder deze stap kregen de drie Kint-varianten drie onverenigbare naamprofielen (given_first='wed', surname_norm='kint geb van dam', initialen 'wlc'/'lcg') en konden ze nooit clusteren. Draait ná de samenvoeging van beide bronnen en dekt dus ook de a2a-records.
  • Pand-locatiepunt. obs.locatiepunt is het pand-locatiepunt uit geo:hasGeometry, in drie afleidingsstappen: direct op de observatie (adresboeken), via de gtm:plaatselijkeAanduiding (volkstellingen/verponding), of via de bronscan (bevolkingsregisters). Het verbindt vermeldingen van hetzelfde pand over bronnen heen (§7, pand_match).

 

6. Blocking

Alle paren vergelijken is onhaalbaar (1,05M² ≈ 5,5·10¹¹). Blocking beperkt de vergelijking tot kandidaten die minstens één sleutel delen. Zes passes, elk gericht op een ander foutmodel in de bronnen:

pass sleutel vangt
P1 metaphone(achternaam) + metaphone(1e voornaam) spellingvarianten (fonetisch)
P2 achternaam + 1e voornaam (exact, genormaliseerd) identieke namen
P3 achternaam + geboortejaar (±1) voornaamvarianten
P4 achternaam + patroniem oude bronnen zonder datums
P5 metaphone(achternaam) + 1e initiaal initialen ↔ volledige namen
P6 least(achternaam, synoniempartner) + 1e voornaam familienaamsynoniemen die fonetisch verschillen
P7 metaphone per achternaam-token + 1e voorletter inconsistent gesplitste dubbele achternamen ("visser silvius" / "sylvius visser" / "silvius")

Blokken groter dan BLOCK_CAP (300) worden hiërarchisch hersleuteld via een saltketen (geboortedecennium → tweede voornaam → patroniem/straat → bron; bij initialen: tweede initiaal). Wat na de volledige keten nog te groot is - vrijwel uitsluitend óngedateerde naamgenoten van topnamen zoals "Johanna de Jong" of de Van den Berg/Van der Burg-synoniemgroep - wordt niet overgeslagen maar afgehandeld met een sorted-neighbourhood-venster (Hernández & Stolfo): binnen het blok gesorteerd op tweede voornaam, patroniem en straat worden alleen paren binnen 20 posities geëmit, lineair in plaats van kwadratisch. Daarmee is "uncovered" structureel nul. Pass 6 emit bovendien alleen sleutels als minstens één kant van het synoniempaar zeldzaam is (frequentie ≤ cap): topnaam-paren zijn fonetisch en exact al verbonden. Netto leveren de zes passes 65,9M unieke kandidaatparen op.

Naast de zes passes is er een kleine meisjesnaam-brug: weduwen met een "geb. X"-meisjesnaam worden direct gepaard met dragers van die achternaam (vrouw of geslacht onbekend). Die paren delen geen enkele naam-sleutel (de weduwe staat onder de naam van haar man), dus geen van de reguliere passes zou ze genereren; het volume is zo klein (tientallen weduwen) dat een directe join volstaat.

 

7. Scoren van kandidaatparen

 

Pipeline persoonsreconstructies (bin/pico) goudatijdmachine.nt (5,4 GB, SAMH) 1.048.623 pico:PersonObservation · N-Triples non-samh.nt.gz OpenArchieven/PiCo · 582.659 obs · gzip MariaDB genealogieonline gendex (46M F/M) · synoniem_voornaam/-familienaam 10 · extractie (streaming parse, ~40 s) subject-blokken bufferen · blank nodes (pnv, genid2) → 8 parquet-bestanden (obs, pnv, rel, titels, partof, RiC ...) 11 · non-SAMH-adapter gzip-parse → obs-vorm oa:*-bron · synth. stabiele oid 12 · lexicons (gecached) geslacht per voornaam (conf > 0,75; n ≥ 10) voornaam- en familienaamsynoniemen 20 · normalisatie → data/pico.duckdb (~70 s) · SAMH + non-SAMH samengevoegd naamdelen · double metaphone · initialen · datumprecisie (d/m/y) vermeldingsjaar: item-set-titel → datePublished → “Periode:” → isPartOf + RiCo-datums geslachtsinferentie (98% dekking) · adres-URI’s · gezinscontext (obs_ctx) tabellen: obs · rel · obs_ctx · syn · syn_surname 30 · blocking, 6 passes (~40 s) fonetisch · exact · achternaam+geboortejaar · patroniem · initialen · synoniembrug BLOCK_CAP 300 met hiërarchische hersleuteling (+decennium / +2e initiaal) → cand: 105M unieke kandidaatparen (beide bronnen) 40 · scoren (~10 min, één Python-proces) transparante gewogen som (0-100) met feature-breakdown per paar harde afwijzingen: dag-datumconflict · zelfde akte/record · periode · kruis-checks · relatie ronde 2: tweede-orde gezinsbonus · initialen-ambiguïteit · webapp-overrides (must-/cannot-link) → pairs: 22,7M ≥ 40 (grijze zone) · 6,11M ≥ 55 (geaccepteerd) 50 · clusteren + relatie-lifting (~50 s) union-find, edges aflopend op score, merge-guards (datums, geslacht, akte, grootte) singletons vervallen · per-observatie-zekerheid = gem. edge-score naar co-leden canonicalisatie naamdelen/datums · relaties observatie → reconstructie → 218.016 reconstructies met 1.202.065 observaties (73,7%) · 47k cross-source 60 · reconstructions.nt 218k pico:PersonReconstruction (1,98 GB) prov:qualifiedDerivation + gtm:confidence geliftede spouse/parent/children 62 · samh_datepublished.nt 645k sdo:datePublished (gYear) 345k sdo:temporalCoverage (“1880/1900”) + inferred_genders.nt (1,03M sdo:gender) 70/75 · rapportage stats.md: dekking, bron×bron, verdelingen calibration.md: precisie per band, drempeladvies (LLM-steekproef) 80 · beoordelingswebapp reconstructie-browser · per-observatie-zekerheid · feature-uitleg per paar grijze-zone-wachtrij (40-55) · betwiste ouders · oordelen → review_verdicts.duckdb leest read-only uit pico.duckdb; oordelen overleven pipeline-reruns bron pipelinefase output (N-Triples / rapport) menselijke beoordeling

 

Elk kandidaatpaar krijgt een score 0-100 uit een transparante gewogen som. De volledige gewichtstabel (uit config.py):

kenmerk punten kenmerk punten
voornaam exact +25 geboortejaar-conflict (> 2 jr) −40
voornaam synoniem +22 overlijden dag-exact / jaar +20 / +8
voornaam JW ≥ 0,92 / ≥ 0,85 +18 / +10 overlijdensjaar-conflict −30
voornaam hard conflict −25 geboorteplaats-URI gelijk / ongelijk +10 / −12
tweede-voornaamconflict −10    
initialen ⊑ voornamen +15 plaatsnaam-string gelijk +6
initialen beide, gelijk / prefix +12 / +6 geslachtsconflict −40
initialen-conflict −15 leeftijd-afgeleid jaar past (±2) +6
achternaam exact +25 leeftijd-afgeleid conflict (> 5) −10
achternaam synoniem +20 partner-voornaamoverlap +20
achternaam JW ≥ 0,92 / ≥ 0,85 +18 / +8 vader- / moedervoornaam gelijk +15 / +15
achternaam-tokens: alle gematcht (volgorde-onafh.) / subset +18 / +18    
achternaam-conflict (JW < 0,75) −30 ouder-voornaamoverlap (geslacht onbekend) +15
patroniem gelijk / conflict +12 / −8 kindnamen-overlap (Jaccard ≥ 0,5) +10
geboortedatum dag-exact +30 zelfde straat-URI + huisnummer / alleen straat +12 / +6
geboortejaar gelijk / ±1 +12 / +6 beroep gelijk¹ +7
zelfde pand-locatiepunt +10 tweede-orde gezinsbonus +15
weduwen van dezelfde man (naam + initialen) +20 weduwen: meisjesnaam gelijk / conflict +15 / −20
meisjesnaam weduwe = achternaam ander +30    

Jaro-Winkler (JW) is de fuzzy-stringmaat voor namen; geschatte geboortejaren worden als intervallen vergeleken (kloof = kleinst mogelijke afstand tussen de intervallen), zodat een registerbereik van twintig jaar geen valse conflicten geeft.

¹ Beroepen worden hoofdletterongevoelig en |-gesplitst vergeleken; de varianten die "geen beroep" betekenen ("Geen", "Zonder", "Zonder beroep"...) tellen als één canoniek token, terwijl een echt beroep dat toevallig "zonder" bevat ("Vragtschipper zonder vast verblijf") ongemoeid blijft (normalize.occ_tokens).

Harde afwijzingen gaan vóór de somscore - deze paren krijgen geen score maar een afwijsreden:

regel aantal (laatste run) motivering
beide geboortedatums dag-precies en onverenigbaar 14,1M dag-precisie is de sterkste bron van waarheid; een waarschijnlijke transcriptiefout (zelfde jaar, maand of dag ±1, of dag/maand verwisseld) wordt géén harde afwijzing maar telt neutraal, zodat naam + geboortejaar de match kunnen dragen
idem overlijden 429k  
periode-onmogelijk 2,85M vermelding in een levende-personen-bron (adresboek, volkstelling, bevolkingsregister) buiten de leefperiode van de kandidaat
vermeld vóór geboorte 11,3M universele tijdlijn-kruischeck: niemand wordt in wélke bron dan ook vermeld vóór het eigen (geschatte) geboortejaar; marge 1 jaar (geregistreerd) / 5 jaar (leeftijd-geschat)
overleden vóór geboorte 10,8k kruislingse datumcheck (overlijden A vs geboorte B): vangt naamhergebruik na kindersterfte, dat de gelijksoortige veld-tegen-veld-vergelijkingen ontglipt
generatiekloof 41,5k gezinscontext-overlap + ≥ 18 jaar tussen geboortejaar-intervallen: het vader-zoon/vernoemingspatroon (de tijdlijn-kruischecks vangen een deel al eerder af)
expliciet gerelateerd 25,2k de twee observaties zijn volgens de bron elkaars ouder/kind of partner - per definitie twee personen
geslachtsconflict (beide uit bron) 1.674 alleen hard bij twee géregistreerde geslachten; afgeleide geslachten geven de −40-penalty
zelfde akte 450 twee personen in één akte (bruid/bruidegom) zijn nooit dezelfde

Initialen tellen +15 (prefix-compatibel, "dg" ⊑ "dirk gijsbertus"); met de achternaam erbij (+25) komt zo'n paar op 40 — grijze zone, geen automatische merge. Maar valt het vermeldingsjaar van de initialen-vermelding binnen de geregistreerde leefperiode van een volledige-naam-kandidaat (adresboeken/kiezerslijsten vermelden volwassenen), dan geeft een periode-fit-bonus (+18) samen 58 — boven de drempel, dus wél een merge. Zo koppelt de founding use-case "D. G. van Vreumingen" (adresboek 1875-1903) aan "Dirk Gijsbertus van Vreumingen" (geb. 1842, ovl. 1907). Bescherming tegen veelvoorkomende namen: bij meer dan drie verschillende volledige-voornaam-varianten op dezelfde initialen+achternaam worden alle betrokken paren als ambigu teruggezet naar de grijze zone.

Weduwen worden weduwe-bewust gescoord. Twee weduwe-vermeldingen van dezelfde man (mansnaam + -initialen identiek, +20) halen met achternaam (+25) en gelijke initialen (+12) samen 57 — de drie Kint-varianten clusteren zo; een gelijke meisjesnaam bevestigt (+15), een stríjdige meisjesnaam (opeenvolgende echtgenotes) kost −20. Bij precies één weduwe met initialen wordt voornaam-/initialen-evidentie overgeslagen (het zijn de initialen van de man, die zeggen niets over de vrouw zelf) en kan haar meisjesnaam de achternaam van de ander matchen (+30, in plaats van de reguliere achternaamvergelijking die "Kint" vs "van Dam" onterecht als conflict zou tellen). Die +30 alleen blijft onder de grijze zone; met pand- of adres-evidentie erbij komt het paar in de webapp-wachtrij terecht.

Na de eerste scoringsronde volgt een tweede-orde-ronde: als de partners (of ouders/kinderen) van observatie A en observatie B zelf een geaccepteerd paar vormen, krijgt (A, B) +15. Zo versterken gezinnen elkaar - het sterkste signaal in genealogische data.

Twee drempels verdelen de uitkomst: ≥ 55 wordt een cluster-edge, 40-55 is de grijze zone (beoordeelbaar in de webapp, telt niet mee in reconstructies), < 40 vervalt. De werkdrempel is verlaagd van 60 naar 55 na kalibratieronde #2: de band 55-60 meet 98% precisie sinds de tijdlijn-kruischecks. De laatste run leverde 6,11M geaccepteerde edges (≥ 55) en 16,6M paren in de grijze zone (40-55).

 

8. Clusteren en relatie-lifting

Naïeve connected components zou transitief-gekoppelde ketens produceren (A≈B, B≈C, dus A=C - ook wanneer A en C elkaar uitsluiten). Daarom werkt fase 50 met een union-find met merge-guards: edges worden in aflopende scorevolgorde verwerkt, en vóór elke samenvoeging wordt het samengestelde clusterprofiel gecontroleerd:

guard blokkeert (laatste run, beide bronnen)
clustergrootte > 80 20.801 merges
onverenigbare dag-precieze geboortedatums (transcriptiefouten getolereerd) 383.606
idem overlijden (dag-precies) 106.167
geregistreerde geboortejaren meer dan 11 jaar uiteen (naamgenoot-/homoniem-conflatie) 448
lid geregistreerd geboren ná het overlijden van een ander lid (marge +1 jaar) 7.445
cohesie: subclusters > 3 leden mergen alleen op een edge ≥ 75, of (tweede pass) op ≥ 2 onafhankelijke cross-edges 63.589
twee leden uit dezelfde akte / hetzelfde OpenArchieven-record 34.133
twee verschillende geboorteplaats-URI's 18.123
geslachtsconflict 131

De twee geboortejaar-sanity-guards werken op clusterniveau (niet paarsgewijs) en zijn daarmee robuust tegen ketens via jaarloze vermeldingen: één persoon kan geen geregistreerde geboortejaren decennia uiteen hebben, en niemand wordt geboren ná het overlijden van hetzelfde cluster. Ze vangen de naamgenoot-/homoniem-conflaties die vooral zichtbaar werden na de verhoging van de clustergrootte-cap naar 80 (grootvader/kleinzoon met dezelfde naam die via ongedateerde vermeldingen aan elkaar hingen).

Cohort-splitsing (post-cluster). Een subtielere conflatie ontsnapt aan de merge-guards: twee gelijknamige personen uit verschillende generaties (bv. Johannes Wiezer, sjouwer, geboren ~1807, en Johannes Hendricus Wiezer, pijpmaker, geboren 1871) raken gekoppeld via ongedateerde “Johannes Wiezer”-brugvermeldingen. Op paarniveau is dat niet te scheiden - de afgekorte voornaam scoort exact, de tweede naam ontbreekt (neutraal), en de gezinscontext-bonus tilt de brug over de drempel; hetzelfde patroon geldt voor tienduizenden terechte afgekorte-naam-koppelingen. Het onderscheid is alleen op clusterniveau zichtbaar: twee ver-uiteenliggende geboortecohorten. Daarom splitst een naronde elk cluster waarvan de effectieve geboortejaren (geregistreerd, anders leeftijd-geschat) meer dan 12 jaar uiteenlopen én in cohorten (> 8 jaar gat) uiteenvallen die elk ≥ 2 gedateerde leden hebben. Ongedateerde brugvermeldingen worden via labelpropagatie toegewezen aan het cohort waarmee ze het sterkst verbonden zijn. Zo werd de Wiezer-reconstructie netjes gesplitst (sjouwer-cohort 1805-1815 vs pijpmaker-cohort 1868-1872, met de juiste beroepen aan weerszijden). Het gat van 8 jaar is veilig omdat de leeftijd-geschatte geboortejaren van één persoon in 95% van de gevallen binnen 5 jaar liggen; de eis van ≥ 2 gedateerde leden per cohort voorkomt dat een losse ruis-uitschieter een splitsing veroorzaakt. Zo worden ook nabije generatie-conflaties gevangen: de reconstructie “Johannes Petrus Spruijt” bleek drie personen — een schippersknecht (~1798), een kleidieper (1818) en een koperslager (1829) — die via ongedateerde “Jan/Johannes Spruijt”-vermeldingen aan elkaar hingen, met een onmogelijke canon (beroep in 1825 bij geboortejaar 1818). Deze run splitste 1.700 zulke generatie-conflaties; er wordt alleen gesplitst bij positief bewijs van meerdere cohorten, dus de afgekorte-naam-koppelingen blijven intact.

Vervolgens:

  • Singletons worden 1-lid-reconstructies (sinds 2026-07-11) - elke niet-geclusterde vermelding krijgt een eigen reconstructie met stabiele URI. Uitzonderingen: volledig naamloze vermeldingen en verponding-instituutsregels ("teldershuisje", "leprooshuis"). Singletons hebben geen interne paar-scores; hun per-observatie-zekerheid is "n.v.t." (geen gtm:confidence/minEdgeScore). Heeft geen enkel lid een volledige voornaam (alleen initialen zoals "B. Nieuwenhuizen", of alleen een ruwe naam zoals bij kerkrekeningen/1000vangouda), dan wordt de meest voorkomende ruwe ledennaam de reconstructienaam.
  • Per-observatie-zekerheid: gtm:confidence = gemiddelde pair-score van die observatie naar haar mede-leden; gtm:minEdgeScore = de zwakste verbinding. Een cluster van vijf observaties waarvan er één slechts via één 62-punts-edge hangt, maakt dat expliciet zichtbaar.
  • Canonicalisatie: meest precieze datum wint; naamdelen komen van het lid met de volledigste voornaamvorm (initialen worden nooit canoniek als een voluit geschreven naam beschikbaar is); beroepen worden verenigd.
  • Relatie-lifting: observatie-relaties worden geheven naar reconstructieniveau (recon_rel met support-telling). Consistentieregel: per kind-reconstructie hooguit één vader- en één moeder-reconstructie - extra kandidaten worden gtm:disputedParent (7.629 gevallen, een kwaliteitssignaal dat in de webapp een eigen wachtrij heeft). Zelfreferenties (cluster dat zijn eigen ouder is) markeren het cluster als verdacht.

Resultaat van de laatste run (drempel 55, beide bronnen): 647.033 reconstructies, waarvan 218.027 meerledig (1.202.103 observaties, 73,7% van alle 1.631.282 vermeldingen) en 429.006 singleton-reconstructies; slechts 173 vermeldingen kregen geen reconstructie (74 naamloos, 99 verponding-instituten). Meerledige reconstructies tellen gemiddeld 5,5 observaties, met een bewuste cap op 80 leden (van 40 verhoogd nadat bleek dat honderden goed-gedocumenteerde personen — adresboeken over 40+ jaar — op de oude grens werden afgekapt; de consistentie-guards blijven de eigenlijke bescherming). Het cross-source-effect is het punt van de tweede bron: 47.445 reconstructies bevatten zowel een SAMH- als een non-SAMH-vermelding (148.954 non-SAMH-observaties koppelen zo aan een SAMH-persoon); 68.814 reconstructies zijn non-SAMH-only en 101.757 SAMH-only. De 6,11 M edges ≥ 55 zijn ~37% meer dan de SAMH-only-run (4,46 M), uit een kandidaatruimte van 105 M paren (was 66 M) - de +55% observaties verhogen vooral het aantal kandidaten met gedeelde achternaam.

 

9. Outputmodel

 

Datamodel: van observatie naar reconstructie (PiCo + PROV) pico:PersonObservation <https://n2t.net/ark:/60537/b...> (BS-huwelijk 1879) schema:name “Dirk Gijsbertus van Vreumingen” pnv-naamdelen · beroep · schema:spouse → ARK pico:PersonObservation (OpenArchieven) <openarchieven.nl/..._Person3> · oa:bevolkingsregister “Dirk Gijsbert van Vreumingen” · leeftijd 41 synth. oid ≥ 10⁹ · afgeleid sdo:gender pico:PersonObservation <ark...> (adresboek 1902, initialen) “D. G. van Vreumingen” · sdo:datePublished 1902 beroep · adres · periode-fit → lid van de reconstructie OpenArchieven-record (schema:ArchiveComponent) schema:additionalType → oa:bevolkingsregister record-URI = deed_uri (same-record-guard) registerperiode “Bevolkingsregister 1874-1893” → bronjaar + living_reg + geboortejaar-interval prov:hadPrimarySource pico:PersonReconstruction · schema:Person <.../id/reconstructie-<uuid4>> (stabiel via UUID-register, fase 55) schema:name “Dirk Gijsbertus van Vreumingen”@nl schema:birthDate (meest precieze) · birthPlace-URI · gender schema:hasOccupation (unie) · prov:wasGeneratedBy → run schema:spouse / parent / children → andere reconstructies (betwist: gtm:disputedParent) pnv:PersonName (<recon#name>) pnv:givenName “Dirk Gijsbertus” · pnv:surnamePrefix “van” pnv:baseSurname “Vreumingen” · pnv:literalName ... pnv:hasName prov:Derivation (<recon#d-<oid>>, één per lid) prov:entity → <ark-observatie> gtm:confidence “0.85”^^xsd:decimal (gem. edge-score) gtm:minEdgeScore “0.80”^^xsd:decimal (zwakste verbinding) prov:qualifiedDerivation prov:wasDerivedFrom (score 85) prov:wasDerivedFrom (score 80) initialen + periode-fit, score 58 → lid → bronjaar-signaal (registerperiode) Volle pijlen: triples in reconstructions.nt · gestippeld: afleidingen tijdens de pipeline (niet gematerialiseerd of alleen in hulpbestanden) Reconstructie-URI is deterministisch over het lidmaatschap: zelfde leden ⇒ zelfde URI bij herrun; ander lidmaatschap ⇒ bewust nieuwe URI

 

De reconstructies verschijnen uitsluitend als N-Triples (out/reconstructions.nt, 7,0M triples, gevalideerd met rapper). Per reconstructie:

<https://www.goudatijdmachine.nl/id/reconstructie-7c3a9f21-4d8e-5b6a-9f1c-2e0d8a4b6f3d>
  a pico:PersonReconstruction, schema:Person ;
  schema:name "Dirk Gijsbertus van Vreumingen"@nl ;
  pnv:hasName <.../reconstructie-7c3a9f21-...#name> ;  # givenName/baseSurname/surnamePrefix
  schema:birthDate "1867-05-02"^^xsd:date ;   # meest precieze uit de leden
  schema:birthPlace <https://gemeentegeschiedenis.nl/gemeentenaam/Gouda> ;
  schema:gender schema:Male ;
  schema:hasOccupation "Sigarenfabrikant" ;   # unie over de leden
  prov:wasDerivedFrom <https://n2t.net/ark:/60537/b...> ; # per lid
  prov:qualifiedDerivation <...#d-427> ;
  schema:spouse <.../andere-reconstructie> ;
  prov:wasGeneratedBy <.../id/reconstructie/run/2026-07-05> .

<...#d-427> a prov:Derivation ;
  prov:entity <https://n2t.net/ark:/60537/b...> ;
  gtm:confidence "0.85"^^xsd:decimal ;      # gemiddelde edge-score / 100
  gtm:minEdgeScore "0.80"^^xsd:decimal .      # zwakste verbinding

 

Zie ook een "live" voorbeeld van de persoonsreconstructie van Dirk Gijsbertus van Vreumingen. Hierin is goed te zien dat de verschillende schrijfwijzen van de naam, inclusief die met initialen, zijn gevonden. Er is een compleet beeld van het gezin en voorouders, waarvoor ook bronnen van buiten SAMH voor zijn gebruikt (huwelijksakten kinderen bij Noord-Hollands Archief en Stadsarchief Rotterdam).

 

Ontwerpbeslissingen:

  • Stabiele URI's via een UUID-register (fase 55): elke reconstructie krijgt .../id/reconstructie-{uuid}. Een persistent register (data/recon_register.duckdb, overleeft pipeline-reruns) bewaart per UUID het lidmaatschap; elke run matcht zijn clusters op ledenoverlap met het register (wederzijds-beste-overlap, deterministische tiebreaks). Bij een splitsing behoudt het grootste fragment de UUID; bij een fusie wint de grootste leverancier en krijgen de verliezers een tombstone (dcterms:isReplacedBy in out/retired_uris.nt); verdwenen reconstructies worden geïnvalideerd (prov:invalidatedAtTime). Een identieke herrun erft aantoonbaar 100% van de UUID's.
  • Zekerheid als qualified derivation (hash-URI's, geen blank nodes): de score staat op de verbinding reconstructie↔observatie, niet op de observatie zelf - dezelfde observatie kan in een volgende run met een andere zekerheid in een andere reconstructie zitten.
  • Run-provenance: één prov:Activity per run met gtm:softwareVersion en gtm:threshold, zodat afnemers weten onder welk regime de reconstructies ontstonden.

Twee bijproducten van de pipeline zijn zelfstandig bruikbare verrijkingen van de bróndata (observatieniveau):

bestand inhoud
out/samh_datepublished.nt 645.249 × schema:datePublished (exact registratiejaar, gYear) en 344.905 × schema:temporalCoverage ("1880/1900") voor SAMH-vermeldingen
out/inferred_genders.nt 1.029.403 × schema:gender afgeleid uit het voornamenlexicon (> 75% zekerheid)

10. Evaluatie en statistieken

Kalibratiemethode

De zekerheidsscore is gekalibreerd met een gestratificeerde steekproef: 100 willekeurige paren per scoreband (reservoir-sampling, vaste seed), onafhankelijk beoordeeld door acht parallelle LLM-beoordelaars (één per band, met strikte instructie: "same" alleen bij echte evidentie, bij twijfel "uncertain"). Daarnaast een interne validatie die geen beoordelaar nodig heeft: paren uit dezelfde akte zijn gegarandeerd verschillende personen; van 861 zulke paren zou 8,2% zonder de zelfde-akte-regel ≥ 60 scoren - een stabiele bovengrens voor de false-positive-druk van het scoremodel.

Precisie per scoreband (ronde 2026-07-05 #2)

band n same different uncertain precisie¹ strikt² volume in run
40-45 100 4 90 6 4% 4% - (grijze zone)
45-50 100 0 65 35 0% 0% -
50-55 100 5 38 57 12% 5% -
55-60 100 62 1 37 98% 62% 859k
60-70 100 70 10 20 88% 70% 411k
70-80 100 51 18 31 74% 51% 269k
80-90 100 95 0 5 100% 95% 2.213k
90+ 100 96 2 2 98% 96% 703k

¹ same/(same+different) · ² uncertain telt als fout · n=100/band ⇒ marge ±3-5%

De verdeling is sterk bimodaal: onder 55 vrijwel alleen non-matches (de grijze zone doet zijn werk), boven 80 vrijwel alleen echte matches. De foutenanalyse van deze ronde leidde direct tot de twee tijdlijn-kruischecks (vermeld-vóór-geboorte universeel, overleden-vóór-geboorte) en de tweede-voornaamconflict-penalty uit §7; hergescoord op dezelfde 800 gelabelde paren wijzen die 91 'different'-paren hard af zonder één 'same'-paar te verliezen, waarmee 60-70 op ~92% en 70-80 op ~80% uitkomt. Opvallend is dat 55-60 (98%) daarmee de beste band onder 80 is: alle nieuwe evidentie (synoniemen, registerperiodes, adres) tilt echte matches omhoog, terwijl 60-80 deels gevuld raakt met paren die op gestapelde zwakkere bonussen drijven.

Volume-gewogen precisie: drempel 55 → ~97,4% over 4,46M edges (met de kruis-checks verrekend; 96,6% op de ruwe ronde-2-cijfers); drempel 80 → 99,5% over 2,92M edges. Daarom is 55 sinds deze ronde de werkdrempel: de band 55-60 voegt 859k edges toe op 98% precisie - vooral bronoverstijgende koppelingen (de volkstellingsdekking verdubbelde ruim, naar 62-70%). Bij drempel 80 verdwijnen vrijwel alle bronoverstijgende koppelingen (volkstellingen, adresboeken, begraafplaatsen halen zelden dag-precieze datumovereenkomst), en juist die koppelingen zijn het doel van een tijdmachine. Afnemers die maximale zekerheid willen, filteren op gtm:confidence ≥ 0.80 - de informatie zit in de output.

Dekking per bron (laatste run)

bron observaties in reconstructie %
SAMH (BS + bevolkingsregister + genealogie) 990.156 845.861 85,4
oa: bevolkingsregister (non-SAMH) 412.866 194.790 47,2
oa: burgerlijke stand (non-SAMH) 137.902 107.495 78,0
oa: overige registers (non-SAMH) 20.216 12.514 61,9
oa: militie / justitie / bidprentjes / dtb 11.675 5.489 47,0
begraafplaatsen 14.568 6.597 45,3
volkstelling 1840 14.469 10.116 69,9
volkstelling 1830 12.577 7.780 61,9
DTB trouwen 1771-1795 6.137 2.289 37,3
verponding 1806 3.970 1.899 47,8
ambten / RKD / DBNL / Ecartico / OGS / adresboek ... 6.746 ~2.400 -
totaal 1.631.282 1.202.065 73,7

De non-SAMH-bevolkingsregisters halen een lager percentage (47,2%) dan de SAMH-BS: het zijn vaak losse gezins-/adresregels die alleen via naam + geboortejaar + gezinscontext koppelen (hun geboorteplaats is een string, geen URI), terwijl de non-SAMH-burgerlijke-stand (78%) meer dag-precieze datums draagt.

Van persoonsvermelding naar reconstructie 1.631.282 vermeldingen uit twee bronfamilies · bandbreedte ∝ aantal vermeldingen SAMH 990.156 · 60,7% v.d. vermeldingen · 85% gereconstrueerd non-SAMH — bevolkingsregister 412.866 · 25,3% v.d. vermeldingen · 47% gereconstrueerd non-SAMH — burgerlijke stand 137.902 · 8,5% v.d. vermeldingen · 78% gereconstrueerd non-SAMH — overige registers 31.891 · 2,0% v.d. vermeldingen · 57% gereconstrueerd Volkstellingen 1830/1840 27.046 · 1,7% v.d. vermeldingen · 70% gereconstrueerd Overige GTM-bronnen 16.853 · 1,0% v.d. vermeldingen · 35% gereconstrueerd Begraafplaatsen 14.568 · 0,9% v.d. vermeldingen · 46% gereconstrueerd In persoonsreconstructie 1.202.065 vermeldingen (73,7%) → 218.016 reconstructies (gemiddeld 5,5 leden) Singleton — niet te reconstrueren 429.217 vermeldingen (26,3%) enige overgebleven vermelding van die persoon stroom naar een reconstructie blijft singleton

De adresboek-initialen koppelen nu automatisch aan de volledige naam wanneer het vermeldingsjaar binnen de geregistreerde leefperiode valt (zie de initialen-regel in §7): de adresboek-dekking steeg daardoor van 80,6% naar 88%, met 260 van de 291 initialen-only-vermeldingen gelinkt. Alleen echt ambigue gevallen (meerdere volledige-voornaam-varianten op dezelfde initialen) blijven in de grijze zone voor de webapp-wachtrij.

Kwaliteitssignalen per run

Elke run rapporteert zijn eigen tegenspraak: 634k door guards geweigerde merges (uitgesplitst naar reden), 7.629 betwiste ouder-relaties, het aantal ambigue initialen-paren en het uncovered-percentage van de blocking. Deze signalen zijn de agenda voor de volgende verbeteriteratie - zo kwamen de registerperiodes, de geslachtsinferentie en de ouder/kind-guard tot stand (uitsplitsing van de geweigerde merges: zie de guard-tabel in §8).

Bekende beperkingen

  • De LLM-beoordelaars zien dezelfde velden als de scorer; systematische fouten die in de data zelf zitten (bijv. een verkeerd overgenomen datum) vangt geen van beide. De banden zijn onderling eerlijk vergelijkbaar, maar geen absolute waarheid.
  • Megablokken van veelvoorkomende naam-combinaties worden sinds de saltketen + het sorted-neighbourhood-venster volledig afgedekt (uncovered = 0), maar binnen het venster (20 posities) worden verre buren in zo'n blok nog steeds niet vergeleken - ongedateerde naamgenoten van topnamen blijven dus lastiger volledig te matchen dan gedateerde.
  • Reconstructie-URI's zijn stabiel via het UUID-register (fase 55), maar de continuïteitsregels zijn heuristisch: bij zeer grote herschikkingen (bv. een drastisch andere drempel) kan een UUID naar een wezenlijk andere samenstelling verschuiven. De tombstones en het register maken zulke verschuivingen wel altijd navolgbaar.
  • Gewichten zijn met de hand gezet en per steekproef gekalibreerd, niet geleerd. Een Fellegi-Sunter-benadering is beproefd (45_fs_weights.py, out/fs_report.md): unsupervised EM collabeert in de geblokte kandidaatruimte (de latente klasse wordt "deelt blocking-sleutel", niet "zelfde persoon"); de supervised variant (m uit de LLM-labels, u uit random paren) levert plausibele gewichten en wint nipt op de gelabelde paren, maar verliest op de zelfde-akte-sanity door labeldekkingsgaten op zeldzame uitkomsten (dag-precieze overlijdensmatches, adres). Het handmodel blijft daarom default; FS is draaibaar via SCORING=fs en wordt productierijp zodra gerichte labels de dekking sluiten.

 

11. Beoordelingswebapp

De Flask webapp/app.py (vooralsnog alleen localhost) leest read-only uit de DuckDB en biedt vier ingangen: een reconstructie-browser (sorteerbaar op laagste zekerheid, met per lid de confidence en per paar de volledige feature-breakdown), de grijze-zone-wachtrij, de lijst betwiste ouder-relaties en het oordelenoverzicht. Namen linken naar de bronpagina's. Menselijke oordelen landen in een apart bestand (data/review_verdicts.duckdb) en overleven pipeline-reruns.

De grijze-zone-wachtrij toont per twijfelpaar (score 40-55) een veld-voor-veld-vergelijking van beide vermeldingen - naam, geslacht, datums, plaatsen, adres, beroep, volledige gezinscontext en bronjaar - met kleurcodering op dezelfde drempels als het scoremodel (groen = overeenkomst, rood = tegenspraak, wit = niet vergelijkbaar). Een oordeel (zelfde/anders/twijfel) verbergt het paar direct en wordt server-side uit de wachtrij gefilterd. Er wordt overwogen om deze beoordelingsapp onderdeel kan worden van de crowdsourcing website van de Gouda Tijdmachine, Vele Panden genaamd.

Terugkoppeling in de pipeline (human-in-the-loop)

De oordelen zijn geen dood archief: verdicts.py voert ze als harde constraints terug in een volgende run.

  • Een "zelfde"-oordeel is een must-link. Fase 40 forceert een edge (score 100), óók als de blocking-fase het paar niet als kandidaat genereerde; fase 50 negeert dan de zachte merge-guards (clustergrootte, cohesie). Een harde data-tegenspraak met een derde clusterlid (twee onverenigbare dag-precieze geboortedatums, geslachtsconflict, zelfde akte) blijft wel gelden en wordt als human_same_blocked gelogd, zodat een menselijke vergissing geen cluster stilletjes corrumpeert.
  • Een "anders"-oordeel is een cannot-link. Fase 40 wijst het paar af (human_rejected); fase 50 houdt de twee observaties ook transitief uit hetzelfde cluster (union-find met een verboden-verzameling per cluster, gelogd als human_separated) - een klassieke must-link/cannot-link-uitbreiding van geconstrainede clustering. Bij een directe tegenspraak wint cannot-link van must-link.

Reconstructies met een bevestigd paar krijgen in de output gtm:humanVerified true. Daarnaast worden de oordelen als hoogwaardige labels meegewogen in de Fellegi-Sunter-schatting (fase 45), waar ze bij overlap het LLM-oordeel overschrijven. Zo verschuift elk mensuur beoordeling zowel de directe reconstructie als, op termijn, het model.

 

12. Referenties