Commit Graph
273 Commits
Author SHA1 Message Date
sgeboers f96e804b67 update: refresh SVD axis labels based on current parliament motions (2025-2026)
- Re-ran generate_svd_json.py for current_parliament window (100 rows, 10 components)
- Computed party centroid scores per axis from 150 matched MPs
- Updated all 10 SVD_THEMES entries with accurate labels, Dutch explanations
  and correct positive/negative pole party attributions
- Key findings: PC1=rechts-links, PC2=populistisch nationalisme vs mainstream,
  PC3=verzorgingsstaat vs bezuinigingen, PC6=klimaat & energie,
  PC8=Europese defensie-integratie
- Added axis_analysis_data.json and party_svd_scores.json as analysis artifacts
2026-03-29 21:24:14 +02:00
sgeboers fc16664c5e fix: open DuckDB read_only in trajectory helpers to avoid lock conflict with Streamlit
Both _load_window_ids and _load_mp_vectors_for_window only read from the DB.
Opening without read_only=True caused an IOException when Streamlit already held
a read-only lock, silently returning an empty scree plot.
2026-03-29 21:10:33 +02:00
sgeboers 98b2583efd fix: scree plot now shows true EVR from Procrustes-aligned multi-window SVD
Previously load_scree_data computed L2-norms per dimension on current_parliament
vectors only, giving ~11% for PC1. This was inconsistent with the compass which
uses all windows + Procrustes alignment and gets PC1=24.1%.

Added compute_svd_spectrum() helper to political_axis.py that reuses the same
alignment pipeline. load_scree_data now delegates to it. _render_scree_plot
no longer re-normalizes (inputs are already EVR percentages). Hover label
updated to 'verklaarde variantie'.
2026-03-29 21:06:51 +02:00
sgeboers e0f17e8b83 Revert "fix: use annual-only windows for SVD to restore EVR (~20% PC1)"
This reverts commit ffd8b191ef.
2026-03-29 20:58:49 +02:00
sgeboers ffd8b191ef fix: use annual-only windows for SVD to restore EVR (~20% PC1)
Quarterly windows (29 of 41 total) diluted PC1 explained variance ratio
from ~20% down to ~14.6%. The fix splits the vector collection loop into:
- pca_vecs: annual windows only (re.match r'^\d{4}$') -> M_pca used for SVD
- all_vecs: every window -> M used for projections onto derived axes

Centering for SVD and global_mean for projection both now use M_pca.mean(axis=0)
so axes are consistent. Falls back to all windows if no annual windows exist.
2026-03-29 20:12:24 +02:00
sgeboers 2cca1000ca refactor: move _render_axis_motions to module level 2026-03-29 19:43:13 +02:00
sgeboers ab9b91e4a8 fix: close duckdb connections safely, swap x/y_axis vectors, fix EVR caption after axis swap 2026-03-29 19:42:22 +02:00
sgeboers ea3c68ece9 refactor: extract _render_axis_motions helper, use literal emoji in expander 2026-03-29 19:37:26 +02:00
sgeboers 37300f2c4e feat: add motion expander to compass tab — shows top motions per axis 2026-03-29 19:34:45 +02:00
sgeboers 9d219d63ee test: add neither-axis-LR edge case + document swap pass-through 2026-03-29 19:33:09 +02:00
sgeboers 74b3f10d07 feat: add axis swap — left-right goes on horizontal axis when detected 2026-03-29 19:29:32 +02:00
sgeboers 95c5ab9302 fix: generate interpretation string when motion path wins without ideology 2026-03-29 19:26:43 +02:00
sgeboers 1ff280e0e3 feat: restructure classify_axes — motion projection as primary label source 2026-03-29 19:20:56 +02:00
sgeboers 62daad321e fix: add outer exception handling to motion helpers in axis_classifier 2026-03-29 19:18:33 +02:00
sgeboers 96224be6ee feat: add motion-loading helpers to axis_classifier 2026-03-29 19:16:57 +02:00
sgeboers 1e52a8a8cc fix: deterministic tie handling and regex matching in _classify_from_titles 2026-03-29 19:15:31 +02:00
sgeboers 71e4b68926 fix: correct docstring for _classify_from_titles return value 2026-03-29 15:00:08 +02:00
sgeboers f8d9af7d9d feat: add _classify_from_titles keyword classifier to axis_classifier 2026-03-29 14:57:19 +02:00
sgeboers 6c4dd81723 feat: expose global_mean in compute_2d_axes axes dict 2026-03-29 14:50:05 +02:00
sgeboers 93a2287c04 docs: add motion-driven axis labeling implementation plan 2026-03-29 14:36:28 +02:00
sgeboers 9dcf6201bb Add design spec for motion-driven axis labeling
Replaces static ideology CSV as primary axis classification signal with
per-year motion projection + Dutch keyword classifier. Adds axis-swap
logic so left-right is conventionally on X when present. Adds Option C
UI expander showing top motions per axis pole.
2026-03-29 14:19:54 +02:00
sgeboers 392fd3afce fix: add per-window X-axis orientation correction
The global PCA X-axis flip uses centroids averaged across all windows,
which can leave individual windows with left/right inverted (e.g. PvdA
appearing right of VVD in 2020). Mirror the existing per-window Y-axis
correction to also check and flip X values per window.
2026-03-29 01:15:21 +01:00
sgeboers 34c08a40fa feat: use dynamic axis labels in compass and trajectories UI
Replace hardcoded 'Links-Rechts' / 'Progressief-Conservatief' axis labels
with values from classify_axes(). Add per-year interpretation caption when
axis quality score is below the 0.65 correlation threshold.
2026-03-29 01:04:54 +01:00
sgeboers 5ec1f7af75 feat: add axis classifier with party ideology reference data
classify_axes() correlates per-party PCA positions against party_ideologies.csv
to assign honest dynamic labels (Links-Rechts, Coalitie-Oppositie, etc.)
instead of always assuming the first PCA axis is left-right.
2026-03-29 01:00:55 +01:00
sgeboers 23849c9cb6 docs: add axis classification implementation plan 2026-03-29 00:58:53 +01:00
sgeboers 6b811364c5 docs: add deployment note to axis classification spec
CSV files committed to git, baked into Docker image — no rsync needed.
2026-03-29 00:53:54 +01:00
sgeboers bb5f2961d1 docs: fix two spec ambiguities in axis classification design
- Clarify CSV path derivation from db_path (same data/ directory)
- Handle current_parliament window exclusion from modal label voting
2026-03-29 00:52:11 +01:00
sgeboers bed911b92c docs: add axis classification design spec
Add design for honest PCA axis labeling — validates each compass axis
against a party ideology reference CSV and labels dynamically (Links–Rechts,
Coalitie–Oppositie, or fallback) instead of hardcoding Left–Right always.
2026-03-29 00:51:34 +01:00
sgeboers 50f8a06c6d fix: connection leak, Rice index excludes absences, per-party motion count guard 2026-03-28 23:50:09 +01:00
sgeboers bcf9407957 feat: add voting discipline section below political compass 2026-03-28 23:45:38 +01:00
sgeboers ab99b7de18 fix: replace sideways Y-axis arrows with proper top/bottom annotations 2026-03-28 23:41:01 +01:00
sgeboers aac8a89118 fix: add missing party justifications in SVD_THEMES axes 3 and 5 explanations 2026-03-28 23:39:24 +01:00
sgeboers b6c2a9bacf fix: update SVD_THEMES axes 3-5 descriptions to reflect stable multi-year patterns 2026-03-28 23:37:10 +01:00
sgeboers 6914b2284a Add implementation plan for compass UI improvements 2026-03-28 23:33:03 +01:00
sgeboers c5b39ced5f Add design doc for compass UI improvements (axes 3-5, Y-axis arrows, discipline section) 2026-03-28 23:30:32 +01:00
sgeboers 064cd059d4 fix: per-window Y-axis correction for political compass
The global orientation check using party centroids averaged across all
windows was insufficient — individual windows (notably 2023) could still
have conservative parties above progressive ones on the Y-axis.

Added a per-window flip in compute_2d_axes (PCA branch) that checks
prog_avg_y vs cons_avg_y for each window independently and negates all
Y values in that window when cons > prog. Flipped window IDs are stored
in axis_def['y_flipped_windows'] for diagnostics.

Moved the canonical party set definitions outside the orientation try-
block so they are always in scope for the per-window correction.

Added test_per_window_y_orientation to cover the case where one window
is globally fine but locally inverted.
2026-03-28 22:45:40 +01:00
sgeboers 6329d6a256 UI improvements + add axis orientation test
- Rename app to 'Motief: de stematlas' in Home.py
- Remove PCA variance caption from compass tab
- Hardcode db_path and window_size; remove sidebar inputs
- Change trajectories default to [CDA, D66, VVD]
- Move quiz to pages/1_Stemwijzer.py; wrap in st.form
- Remove quiz tab from main explorer
- Add pytest dev dep + fix test fixtures (_load_mp_vectors_for_window)
- Add test_pca_axis_orientation with proper PCA variance dominance
2026-03-28 22:27:39 +01:00
sgeboers 72fbe0008e fix(ansible): add headless and CORS flags for reverse proxy 2026-03-28 22:01:04 +01:00
sgeboers b50ee650de fix(ansible): create data directory on server before rsync 2026-03-28 21:39:33 +01:00
sgeboers cbab8f080d fix(ansible): use rsync with checksum for motions.db sync 2026-03-28 21:09:17 +01:00
sgeboers 0bd1c08cb2 fix(ansible): ignore pkill errors, add motions.db sync task 2026-03-28 21:04:49 +01:00
sgeboers 22067fd162 fix(ansible): use full path for uv binary in shell tasks 2026-03-28 21:02:01 +01:00
sgeboers de6ed29bf7 fix(ansible): use port 222 for Gitea SSH, write SSH config on server 2026-03-28 20:46:57 +01:00
sgeboers 13cb746d06 fix(ansible): correct deploy key path to /home/webapps/.ssh/ed25519 2026-03-28 20:39:45 +01:00
sgeboers a4481af8e2 fix(ansible): use webapps deploy key for git clone instead of agent forwarding 2026-03-28 20:33:09 +01:00
sgeboers 8579da68bd fix(ansible): add git.sgeboers.nl to known_hosts before git clone 2026-03-28 20:29:06 +01:00
sgeboers 57083e496d chore(ansible): add ansible.cfg with SSH agent forwarding for private repo access 2026-03-28 20:25:36 +01:00
sgeboers c0d1c59bd7 chore(deploy): rewrite ansible for motief on motief.sgeboers.nl, remove drone
- Replace gtfs/bokeh deploy with motief/streamlit (port 8501)
- Update inventory to motief.sgeboers.nl
- Remove stale .drone.yml
- Add CI guard to forbid .env in repo
- Add env removal report and secrets rotation checklist
2026-03-28 20:12:36 +01:00
sgeboers 35f4667982 chore(secrets): stop tracking .env and add to .gitignore 2026-03-28 20:02:54 +01:00
sgeboers 445f0bfb24 feat(ansible-example): add @ansible/example package, tests, CI, publish & deploy workflows, docs and changelog 2026-03-28 20:00:31 +01:00