stats elections: CBS 37278 percentages to votes; Kiesraad municipal votes and turnout from EML kieskring counts; ballots include blank/invalid
All checks were successful
Deploy production / deploy (push) Successful in 1m14s
All checks were successful
Deploy production / deploy (push) Successful in 1m14s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// A14 elections: NL Tweede Kamer from the Kiesraad open data (data.overheid.nl, CC0) and CBS
|
||||
// 37278 (1918–2006), US presidential results by state from the MIT Election Lab (CC0).
|
||||
// 37278 (1918–2006; votes there are derived from CBS's one-decimal percentages), US presidential results by state from the MIT Election Lab (CC0).
|
||||
// - Kiesraad: per election the "CSV formaat" zip (votes per list per municipality and per
|
||||
// polling station; 2023+ also turnout and seats) plus the EML zips, read with HTTP ranges, for
|
||||
// the official result (seats, election date) and, before 2023, the national total count.
|
||||
@@ -123,6 +123,22 @@ function parseTotaaltelling(xml: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/** EML 510c (kieskring count): votes and turnout per municipality (ReportingUnit "HSBn::0513"). */
|
||||
function parseKieskringUnits(xml: string) {
|
||||
const out: { code: string; name: string; t: ReturnType<typeof parseTotaaltelling> }[] = [];
|
||||
for (const m of xml.matchAll(/<ReportingUnitVotes>([\s\S]*?)<\/ReportingUnitVotes>/g)) {
|
||||
const id = m[1].match(/<ReportingUnitIdentifier Id="[^"]*::(\d{4})">([^<]*)</);
|
||||
if (!id) continue;
|
||||
const name = xmlText(id[2].replace(/^Gemeente\s+/i, '').trim());
|
||||
out.push({
|
||||
code: `GM${id[1]}`,
|
||||
name,
|
||||
t: parseTotaaltelling(`<TotalVotes>${m[1]}</TotalVotes>`)
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type CkanResource = { url: string; name?: string; format?: string };
|
||||
type CkanPackage = { name: string; title: string; resources: CkanResource[] };
|
||||
|
||||
@@ -150,6 +166,8 @@ async function kiesraad(force: boolean, note: (w: string) => void) {
|
||||
// EML zips: find the official result (and, for older years, the national count).
|
||||
let resultaat: ReturnType<typeof parseResultaat> | null = null;
|
||||
let totaal: ReturnType<typeof parseTotaaltelling> | null = null;
|
||||
const hasUitslag = csvZip.find(/_uitslag\.csv$/i).length > 0;
|
||||
const units = new Map<string, ReturnType<typeof parseKieskringUnits>[number]>();
|
||||
for (const r of p.resources.filter((r) => r !== csvRes && /\.zip$/i.test(r.url))) {
|
||||
const zip = await RemoteZip.open(r.url);
|
||||
const re = zip.find(/(^|\/)Resultaat_TK\d+[^/]*\.eml\.xml$/i)[0];
|
||||
@@ -157,7 +175,13 @@ async function kiesraad(force: boolean, note: (w: string) => void) {
|
||||
const tt = zip.find(/(^|\/)Totaaltelling_TK\d+[^/]*\.eml\.xml$/i)[0];
|
||||
if (tt && !totaal && !csvZip.find(/_uitslag\.csv$/i).length)
|
||||
totaal = parseTotaaltelling(await zip.text(tt));
|
||||
if (resultaat && (totaal || csvZip.find(/_uitslag\.csv$/i).length)) break;
|
||||
// Before 2023 the CSV zip has no municipal turnout (and 2017's municipal CSV misses
|
||||
// ~110 municipalities): take both from the kieskring counts where a zip has them.
|
||||
if (!hasUitslag)
|
||||
for (const k of zip.find(/Telling_TK\d+_kieskring_[^/]*\.eml\.xml$/i))
|
||||
for (const u of parseKieskringUnits(await zip.text(k)))
|
||||
if (!units.has(u.code)) units.set(u.code, u);
|
||||
if (resultaat && (totaal || hasUitslag) && (hasUitslag || units.size)) break;
|
||||
}
|
||||
const results: Result[] = [];
|
||||
const turnout: Turnout[] = [];
|
||||
@@ -227,8 +251,34 @@ async function kiesraad(force: boolean, note: (w: string) => void) {
|
||||
}
|
||||
} else {
|
||||
const gem = csvZip.find(/Per_Gemeente\.csv$/i)[0];
|
||||
if (!gem) throw new Error(`${id}: no municipality CSV in ${csvRes.url}`);
|
||||
for (const r of rowsOf(await csvZip.text(gem)))
|
||||
if (!gem && !units.size) throw new Error(`${id}: no municipality CSV in ${csvRes.url}`);
|
||||
for (const u of units.values()) {
|
||||
for (const [party, votes] of u.t.votes)
|
||||
results.push({
|
||||
...base,
|
||||
level: 'muni',
|
||||
area_code: u.code,
|
||||
area_name: u.name,
|
||||
party,
|
||||
votes,
|
||||
seats: null
|
||||
});
|
||||
const valid = [...u.t.votes.values()].reduce((a, b) => a + b, 0);
|
||||
turnout.push({
|
||||
...base,
|
||||
level: 'muni',
|
||||
area_code: u.code,
|
||||
area_name: u.name,
|
||||
electorate: u.t.electorate,
|
||||
ballots: valid + (u.t.blank ?? 0) + (u.t.invalid ?? 0),
|
||||
valid,
|
||||
blank: u.t.blank,
|
||||
invalid: u.t.invalid
|
||||
});
|
||||
}
|
||||
if (units.size) note(`${id}: ${units.size} municipalities from EML kieskring counts`);
|
||||
for (const r of gem ? rowsOf(await csvZip.text(gem)) : [])
|
||||
if (!units.has(gm(r.GemeenteCode)))
|
||||
results.push({
|
||||
...base,
|
||||
level: 'muni',
|
||||
@@ -257,7 +307,8 @@ async function kiesraad(force: boolean, note: (w: string) => void) {
|
||||
area_code: 'NL',
|
||||
area_name: 'Nederland',
|
||||
electorate: totaal.electorate,
|
||||
ballots: totaal.ballots,
|
||||
// <TotalCounted> is the valid count in these files; ballots include blank and invalid.
|
||||
ballots: valid + (totaal.blank ?? 0) + (totaal.invalid ?? 0),
|
||||
valid,
|
||||
blank: totaal.blank,
|
||||
invalid: totaal.invalid
|
||||
@@ -410,6 +461,14 @@ async function cbs37278() {
|
||||
results.set(p.Title, r);
|
||||
}
|
||||
}
|
||||
// 37278 gives vote shares per party (%, 1 decimal) and turnout as a %: convert to
|
||||
// (rounded) vote counts against the valid-vote total so all elections share one shape.
|
||||
const opkomst = d.Opkomst_2 == null ? null : Number(d.Opkomst_2);
|
||||
if (turnout.ballots == null && opkomst != null && turnout.electorate != null)
|
||||
turnout.ballots = Math.round((opkomst / 100) * turnout.electorate);
|
||||
for (const r of results.values())
|
||||
if (r.votes != null)
|
||||
r.votes = turnout.valid != null ? Math.round((r.votes / 100) * turnout.valid) : null;
|
||||
const list = [...results.values()].filter((r) => r.votes || r.seats);
|
||||
const totalSeats = list.reduce((a, r) => a + (r.seats ?? 0), 0) || null;
|
||||
rows += await save(
|
||||
|
||||
Reference in New Issue
Block a user