Vancouver 2026 candidate explorer: methods report

Generated 2026-10-17T14:24:55+00:00. Election day 2026-10-17. All data is pre-election; no results are included.

1. Ballot

Candidates: 130 (mayor 8, council 56, Park Board 35, school trustee 31).

2. Evidence collection

Sources registered: 484 (479 with extractable text). Each source keeps its original bytes, extracted text, URL, publisher, publication date when stated, retrieval time, SHA-256 hash and the route used (direct or Internet Archive). Inaccessible URLs are logged (1 at build time).

Source discovery was done by research agents that recorded URLs only; every fact shown in the app is a verbatim sentence from a cached source.

3. Claims

Claims: 12341 (8200 canonical after de-duplication). Each claim is one verbatim sentence with candidate_id (or party_id for platforms), topic(s), evidence type, source and passage id, date, verification status and uncertainty.

Evidence types: individual_position, party_commitment, elected_experience, professional_experience, education, reported_action, claimed_outcome (a campaign's own claim of a result, never treated as verified), commentary.

Classification is rule-based (keyword patterns published in vanelect/claims.py), so every label can be audited. It is not perfect: misclassified sentences are expected, and the uncertainty field and source links let readers check.

Attribution: a sentence is assigned to a candidate when it is on that candidate's own page, under a heading that names them, or when it names them. Party platform text is attributed to the party only and is never added to an individual's personal record.

Missing values are distinct: not_found (no statement in collected sources), party_platform_only, none_no_council_seat (no voting record because they never sat on Council), and per-vote values Absent, Abstain, No Vote, Declared Conflict (recusal) and Ineligible. None of those is a vote against.

Housing forms are tagged separately and never merged: SRO, self-contained micro-unit, supportive housing, shelter, co-op, social housing, market rental.

4. Representations

Embedding model: BAAI/bge-small-en-v1.5 (sentence-transformers, revision 5c38ec7c405ec4b44b94cc5a9bb96e735b38267a, 384 dimensions, max 512 tokens). Preprocessing: NFKC normalise, collapse whitespace, verbatim sentences only, no prefixes. Every chunk vector is L2-normalised.

Views are kept separate: (1) individual policy positions, (2) party platform, embedded once per party and linked to candidates, (3) experience and governance, (4) reported actions and campaign-claimed outcomes. Council recorded votes form a separate agreement-based map.

Aggregation: chunks are de-duplicated (exact sentence duplicates per subject, then cosine >= 0.95). Chunks are averaged per topic and re-normalised. Topic vectors are then averaged with equal (or user-chosen) weights over the topics that have evidence, so a prolific campaign does not outweigh a terse one. Missing topics are excluded, never zero-filled.

"Include party platform" mode: for each topic, the candidate's vector and the party's are summed and re-normalised. Topics only the platform covers are filled from the platform and marked.

Insufficient evidence: fewer than 3 chunks or fewer than 2 topics (policy and actions views). Such candidates are listed, not plotted.

5. Analysis

Sensitivity (per candidate): * source dropout: 25 runs, each source dropped with p=0.2; Procrustes-aligned PCA displacement and neighbour Jaccard; * topic weights: 25 Dirichlet(alpha=4.0) re-weightings; * party-platform toggle: neighbour Jaccard between modes; * UMAP seeds [42, 7, 1234, 99, 2026]; * projection fidelity: 2-D neighbours compared with original-space neighbours. A placement is flagged unstable when median displacement > 0.5 or neighbour Jaccard < 0.34.

Results of the sensitivity tests (this build)

race view plotted PC1+PC2 variance unstable 2-D distorted same-party top-3 neighbours
mayor policy:individual 6 50% 0 0
mayor policy:with_platform 7 42% 2 0
mayor experience:individual 7 44% 1 0
mayor actions:individual 4 71% 0 1
council policy:individual 24 17% 12 20 20%
council policy:with_platform 53 39% 2 17 100%
council experience:individual 29 16% 8 19 26%
council actions:individual 10 33% 3 1 33%
park policy:individual 12 27% 4 2 12%
park policy:with_platform 32 52% 1 7 83%
park experience:individual 10 33% 4 4 5%
park actions:individual 5 63% 0 0 0%
school policy:individual 8 41% 4 0 58%
school policy:with_platform 31 52% 2 2 88%
school experience:individual 13 30% 5 4 37%
school actions:individual 4 75% 2 0 33%

Reading: with individual evidence only, most nearest neighbours cross party lines. Adding the shared party platform pulls each candidate toward their party, so same-party neighbour rates rise sharply. Verbatim text shared by several candidates of one party is retyped shared_campaign_language and kept out of individual vectors (cross-candidate cosine >= the near-duplicate threshold is treated the same way).

Removing source and outlet effects

Two diagnostic findings shaped the representation. Before debiasing, the main PCA axes mostly separated genres (third-person campaign bios vs. questionnaire answers vs. platform bullet lists), and source type could be predicted from a chunk's embedding about two times out of three. Three steps were applied, in order (config debias):

  1. Stance-less campaign narrative is typed biography and kept out of the policy view; a name mention counts as a position only when the candidate is the subject or speaker.
  2. Source-genre centering: the mean vector of each (view, source type) is subtracted from its chunks.
  3. Concept erasure: directions along which the same candidate's chunks differ by source (within-candidate contrasts) are estimated by SVD and projected out of every chunk in the view.
  4. Topic centering: each topic vector is expressed relative to the race's mean statement on that topic.
view erased dims stage source-type acc. publisher acc. within-candidate source acc. over chance between-candidate variance share
actions 32 raw 0.674 0.622 0.236 0.326
actions 32 genre_centred 0.108 0.602 0.291 0.304
actions 32 erased 0.159 0.431 0.038 0.353
experience 32 raw 0.485 0.37 -0.074 0.353
experience 32 genre_centred 0.045 0.266 -0.059 0.342
experience 32 erased 0.059 0.196 -0.248 0.373
policy 32 raw 0.657 0.5 0.296 0.243
policy 32 genre_centred 0.115 0.403 0.34 0.229
policy 32 erased 0.072 0.366 -0.104 0.226

Accuracies are leave-one-out nearest-centroid. After debiasing, source type is at or below chance, and a candidate's own sources are close to indistinguishable, while the between-candidate share of variance is unchanged. Publisher remains partly predictable because most publishers are party sites covering only their own candidates, so publisher and party (and candidate) are confounded; erasing it further would erase the candidates' content. Axis descriptions on the site (passages, distinguishing words, topic drivers) are automated interpretations computed after debiasing, shown only when an axis keeps its direction under source dropout.

6. Council votes

Source: https://opendata.vancouver.ca/explore/dataset/council-voting-records/ (modified 2026-10-13T13:49:14+00:00, sha256 8168def08ae8ee6d...). The City says minutes remain the official record. Motions are matched by (meeting_id, vote_number). Agreement is computed only on matching contested motions (not carried unanimously) where both members cast For/Against, requires >= 100 shared votes, and is never compared across different councils (2018-22 and 2022-26 are separate maps).

Park Board and School Board votes are not published as open data. Minutes PDFs in the source registry are parsed (vanelect/board_votes.py) for motions, results and named In favour / Opposed / Absent / Abstained lists and conflict declarations. Coverage is partial (only the meetings collected), and no agreement map is built from them.

7. Coverage and limitations

8. Reproduce

make setup # once make build # fetch, extract, claims, embed, analyse, export make serve