Repository navigation
Add the Czech election candidate registers as a scored local corpus (cz-candidates) - #963
Merged
Merged
Conversation
cz-candidates keeps every candidate on ČSÚ's registers for the 2022 municipal and 2025 Chamber elections (volby.gov.cz, CC BY 4.0): each distinct set of the four boxes a candidate filed (the title before the name, given names, surname, the title after it) in one election, and a name composed from them in the Czech convention, TITULPRED JMENO PRIJMENI, TITULZA. 170,181 records on 2026-10-30, 36% titled. The fetch reads each register's zip off its election's index page, since the file name carries a date stamp, and refuses a register missing a box's column, cut inside its last row, holding another number of candidates than its pin, or with its titles emptied. local_agreement.py scores it by election: the title against the box before the name, the suffix against the box after it (the first source with one, so no suffix is compared inside the surname box), and nothing set aside, since the title box holds degrees the default vocabulary mostly does not list. --locale cs scores parser_for(locales.CS) against a record of its own. Every field agreed for 96.96% of records under the default and 96.97% under cs; the misses are mostly DiS. after the name read as a title, two-word surnames, and Ing. et Ing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd refuse an unknown --locale first - The unit is "records": a record is a distinct set of boxes in one election, and 31,770 of kv2022's rows fold into another. - A comma typed inside a box before the last (21 records) makes a comma shape nobody wrote in the composed name. Such a record is kept as filed and counted (records_with_a_comma_inside_a_box), an accepted cost stated in the docstring and README. - local_agreement.py now validates --locale before reading a record or scoring a base, whose refusals stood in for it. - The register pin's comment no longer calls a register final: kv2022's carries by-elections through 2024 and was re-issued on 2026-03-28. - The 1,059 set-aside split and the DiS. counts now state their predicates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
derek73
force-pushed
the
feat/cz-candidates-corpus
branch
from
October 31, 2026 02:49
4472555 to
e106979
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A new scored local corpus,
cz-candidates: the candidate registers the Czech Statistical Office (ČSÚ) publishes on volby.gov.cz, starting with the 2022 municipal elections (kv2022) and the 2025 Chamber election (ps2025). It is the first local corpus where titles are stated separately from the name. Each candidate filed four boxes: the title before the name (TITULPRED), the given names (JMENO), the surname (PRIJMENI) and the title after it (TITULZA). 36% of the records carry a title, nearly all academic degrees.tools/differential/local_volby.pyfetches each election's index page and reads the register's CSV zip name off it. The zip name carries a date stamp, and a guessed stamp gets an HTML 404 page, so the name is never hard-coded. An answer that is not a zip is retried and then refused. The fetch reads the UTF-8csv_od/member by column name, because the elections' registers differ in every other column. It keeps one record per distinct set of boxes per election, so the corpus counts records, not candidates: 31,770 of kv2022's rows fold into another record, most of them different people sharing all four boxes. The fetch refuses:VOLBY_ELECTIONS. A register is not final once its election is held: kv2022's carries by-elections through 2024 and was re-issued on 2026-03-28, so the pin is what protects;Adding an election is one row in
VOLBY_ELECTIONS.TITULPRED JMENO PRIJMENI, TITULZA(Ing. Jan Novák, Ph.D.), so it has no case, spacing or punctuation of its own. The module docstring, the README and the credit line all say so.meta.jsonrecordsVOLBY_READ_VERSION, and a corpus built by a different version ofcomposeis refused on load. A comma typed inside a box before the last (Dopitová, roz. Liškováin the surname box,Ing., Bc.in the title box: 21 records) makes a comma shape nobody wrote. Those records are kept as filed and counted (records_with_a_comma_inside_a_box), an accepted cost stated in the docstring and README: the score reads the boxes, so dropping the records would only hide the miss.local_agreement.pychanges:Fielded.sets_asideflag switches off the title set-aside. That rule was built for PSC's free-text title box. Here it would have excused 1,059 records, each a degree or rank the parser missed and none a job typed into the box. Of them, 756 holddisin the box after the name (re.search(r"\bdis\b", box.replace(".", "").casefold())), and 179 of the rest holdetin the title box (re.search(r"\bet\b", box); 182 by that test alone, since 3 hold both).--locale csscoresparser_for(locales.CS)against its own record (agreement.cs.jsonl, with kept--baserecords keyed by locale too). A pack's score and the default's are never read as each other. An unknown locale is refused before any record is read or base scored.parser_ofjoins_SCORER_CODE, andread,scoreand_read_chunkchange. Every existing shared agreement record (psc-individuals,orcid-researchers,patentsview-inventors) will be refused as written by another scorer until re-recorded with--record --tree <base>.--basecopes on its own: its kept records are keyed by the fingerprint, so it scores the base again.SOURCES, sosnapshot.py --local,tally,name_usage.pyandmeasure.pyall pick it up. There is a README table row and paragraph, and a sentence in AGENTS.md's "Validating a new parsing rule". There is no release-log bullet: Add Companies House's PSC register as a local corpus, with a local agreement score #849 and Add the nepal-candidates local corpus: Nepal's 2079 BS local election candidates #866 added none for a tools-only change.Measured on the fetch of 2026-10-30
There are 170,181 records: 165,736 of kv2022's 197,506 candidates and 4,445 of ps2025's 4,475. The fetch took 25 s and the score 3.4 s. Each cell is default /
locales.CS:The survey's sample gave 95.0% for the post-nominal box. That figure was taken over titled candidates, most of whom left the box after the name empty, so it agreed trivially. Over the boxes that are actually filled, agreement is 56%, because
DiS.fills 42% of them.What the misses are, on the whole corpus (default / cs):
DiS.after the name, the box after the name missed. Predicate: the box after the name holdsdis(re.search(r"\bdis\b", box.replace(".", "").casefold())), 3,248 records, and that box disagrees in 3,240. Of those, 3,232 readDiS.as a title (the parsed title holdsdisby the same test):Jan Novák, DiS.gives titleDiS.. The other 8 read the whole name before the comma as the family (DiS. et DiS.6, a bareDiS1,BBus, DiS.1). The mechanism: the part after the comma is not suffix vocabulary, so it reads as a family comma (rules/C1). rules/H2 then reads the unlisted abbreviation at the head of the given part as a title, and rules/C8 reads the part before the comma as a whole name. Any unlisted abbreviation behaves the same:Jan Novák, Xyz.gives titleXyz., and so dodipl. um.(27),Dr.(10) andArtD..Pešová Řehořováis read as middlePešová. WithPolicy(two_family_names=True)the family box's misses fall from 1,683 to 825 (default) and 986 (cs), and the given box's from 1,828 / 1,644 to 743 / 732.Ing. et Ing.(102 of 102 missed),Bc. et Bc.(38 of 38).Mgr. et Mgr.reads correctly in 218 of 223, becauseMgr.is in the vocabulary. Undercs,JUDr. et Mgr.reads correctly too, because the pack addsJUDr..Novák ml.177,st.57 under cs). It reads as the suffix it is, and the score counts it against the empty box after the name. Here the box is out of place, not the parse.BA,BSc,B.A., 36) read as given names;BA (Hons)read with(Hons)as a nickname;roz.typed into the surname box; and the surnamesCehandMlsread as credentials.This PR does not change parser behaviour. Issue drafts for the first three shapes are with the orchestrating session for approval and have not been filed.
Checks
uv run pytest: 33,546 passed.uv run mypy,uv run ruff check: clean.tests/v2/test_local_volby.py, a fixture-served index page and zip covering each refusal. The last-row check has a negative control: with its short-row arm removed, the cut-inside-a-quoted-field case fails.test_local_agreement.pytests cover the suffix box,sets_aside, and--localewith its record, its kept--baserecord and its workers.🤖 Generated with Claude Code