Commit Graph
65 Commits
Author SHA1 Message Date
sgeboers 7df961ba83 feat(overton): address 7 critical gaps in Overton window analysis
U1: Temporal trajectory — quarterly granularity reveals immediate
electoral jump at 2024-Q1 (+0.180), peak at 2024-Q4 (0.648), reversion
to 0.334 by 2026-Q1.

U2: 2D extremity temporal — single-score masks divergence. Material
impact decreased (-0.146) while stylistic increased (+0.097).
Wilcoxon p=0.002 confirms systematic divergence.

U3: Systematic mechanism classification — 150 motions. Consensus
framing confirmed (24% high-CS vs 8% low-CS, p=0.014). Post-2024
high-CS dominated by procedural (32%), consensus (24%), targeted
restriction (17%).

U4: Causal timing — shift is electorally driven (after Nov 2023 PVV
election, before Jul 2024 Schoof cabinet). Rules out coalition
dynamics, gradual learning, European contagion.

U5: Left-wing response — barely changed (21.3%→20.2%, -1.1pp).
Centrist shift (d=+1.89) is 18.3x larger than left hardening
(d=-0.75). Volt is only left party that softened (+12.9pp).

U6: Success correlation — significant trend (p<0.001) but success
premium only +3.2%, ceiling effect at 96%+ limits practical meaning.

U7: Synthesis update — integrated all findings, updated verdict
to note electoral-cycle effect and 2026-Q1 reversion.
2026-05-26 23:33:13 +02:00
sgeboers 80c68c0112 fix(right-wing): match store_scores column names and value order to DB schema 2026-05-25 00:01:31 +02:00
sgeboers bf37f84a8b feat(extremity): two-dimensional rescoring with subagent pipeline
- Project-local skill .opencode/skills/score-extremity/ for subagent dispatch
- Orchestrator extremity_rescore_2d.py with load_skill/sample/format/validate/store
- 16 TDD tests covering all orchestrator functions
- 117 motions scored by deepseek v4 flash subagents (12 parallel batches)
- Pearson r=0.45 between stylistic and material dimensions — separable
- Key finding: 36.8% of motions use restrained language for consequential policies
- 2d_extremity_correlation_report.md documents distribution, divergence patterns,
  and implications for the Overton acceptance-without-conversion narrative
2026-05-24 23:13:42 +02:00
sgeboers be007165b1 fix(right-wing): add resume support to extremity and sentiment scorers
Use CREATE TABLE IF NOT EXISTS and skip already-scored motions
to allow resuming interrupted batch runs.
2026-05-24 22:33:44 +02:00
sgeboers 711a410df3 chore: simplify Overton scripts, update README, add stemwijzer.db to gitignore
- Extracted EXTREMITY_BUCKET_ORDER constant and _extremity_bucket() helper (4 duplications removed)
- Merged two-pass query loop in compute_yearly_baseline into single pass
- Removed unused import (mticker), dead code (year_titles_map), 12 obvious comments
- Extracted _fmt_axis() helper in SVD drift script
- Updated README analysis/ description to include right-wing motion analysis
2026-05-24 22:19:21 +02:00
sgeboers 2a081ade25 fix(overton): strict centrist definition + left support analysis
- Reclassified centrist to {D66, CDA, CU, NSC} — removing VVD/BBB
  which are center-right coalition partners
- Added centrist_support_strict (0.251→0.507, d=+0.65), center_right_support,
  and left_support_mp columns via migration script
- Figure 1 now shows center-right (VVD/BBB) support as orange dashed line
- New Figure 3: bar chart of left-party support for right-wing motions
  (0.268→0.202, left opposition hardened)
- New report Section 6 covering left-wing support trends
- All analysis now uses strict centrist definition throughout
2026-05-09 00:45:38 +02:00
sgeboers e478235c84 fix(overton): correct SVD axis interpretation, drop pass rate, synthesis rewrite
- SVD axis 2 sign corrected: negative = nationalist (PVV -0.56, FVD -0.36), positive = kosmopolitisch (Volt +0.27). Centrists moved LEFT on both axes while right-wing moved further right culturally (+0.146 gap). 'Acceptance without conversion' named as unifying interpretation.
- U1: Figure 1 merged to single panel, pass rate removed, 5 centrist_support lines
- U2: Pass rate columns dropped from all breakpoint tables, PR narrative cut
- U3: Findings report rewritten: SVD section replaced, synthesis restructured into 3 tiers, extremity LLM bias qualified
- U4: Axis labels and sign convention added to svd_stability_report.md
- Added centrist_support_mp column (MP-weighted, correlates 0.998 with party-level)
2026-05-09 00:21:46 +02:00
sgeboers 76b499cdc0 feat(analysis): Overton window breakpoint analysis with opposition control and SVD drift
Quantify 2024 breakpoint in centrist support (d=+0.68 overall, d=+0.85 opposition-only),
domain decomposition, extremity-stratified pass rates, and manual LLM audit (75% agreement).
SVD center drift aborted due to axis instability (9/10 consecutive window pairs fail stability threshold).
2026-05-08 23:14:34 +02:00
sgeboers d170444bda feat(analysis): add migration anti-democratic overlap analysis 2026-05-08 22:56:38 +02:00
sgeboers fbf92c82cf feat(right-wing): dual-scoring extremity/sentiment + derived categories
Extremity Scorer (U4 enhanced):
- Now scores BOTH original motion text AND layman explanation separately
- Schema: text_score, text_explanation, layman_score, layman_explanation
- Text scores: 1→7, 2→33, 3→5, 4→5 (mild-to-moderate)
- Layman scores: 1→12, 2→20, 3→17, 4→1 (slightly milder)

Sentiment Analysis (U5 enhanced):
- Now scores BOTH original motion text AND layman explanation separately
- Schema: text_score, text_explanation, layman_score, layman_explanation
- Text sentiment avg: 0.294 (slightly positive)
- Layman sentiment avg: 0.416 (more positive - summaries tone down hostility)

Category Derivation (new):
- Two-phase LLM approach: derive taxonomy from sample, then apply to all
- Discovered 7 categories from 30-motion sample:
  veiligheid/justitie, corona/pandemie, economie/belasting, klimaat/milieu,
  defensie/buitenland, asiel/vreemdelingen, overig
- Applied to 50 motions with distribution shown in DB
- Adds category + category_explanation columns to right_wing_motions
2026-05-05 21:40:58 +02:00
sgeboers f94edc3d04 feat(right-wing): sentiment analysis pipeline for right-wing motions
Implements U5: sentiment_analysis.py uses LLM batch calls (fallback when no
local Dutch sentiment model is available) to score motion sentiment on [-1, 1]
scale.

Design:
- Prompt asks for sentiment from -1 (hostile/aggressive) to 1 (constructive)
- JSON schema enforces numeric score + Dutch explanation
- Batch size 10, max_workers 5 for parallel API calls
- Stores results in  table
- Updates  with avg_sentiment, sentiment_std,
  pct_strongly_negative per year

Sample validation (50 motions): good variance across [-0.9, 1.0] range.
2026-05-05 21:25:42 +02:00
sgeboers d2310edfc4 feat(right-wing): LLM-based policy extremity scoring
Implements U4: extremity_scorer.py uses ai_provider.chat_completion_json_parallel
with a JSON schema enforcing integer 1-5 + Dutch explanation.

Design:
- Batch size 10, max_workers 5 for parallel API calls
- Prompt asks for concrete policy + radicalism score in Dutch
- Stores results in  table (motion_id, score, explanation, error)
- Updates  with yearly averages
- Default sample=50 for validation; --sample -1 scores all motions

Sample validation (50 motions): scores distributed 1→2, 2→34, 3→7, 4→7,
yearly averages ~2.0-2.5 (mild-to-moderate radicalism).
2026-05-05 21:23:08 +02:00
sgeboers 1bc83c4384 feat(right-wing): temporal aggregation of right-wing motion trends
Implements U3: temporal_analysis.py computes yearly_summary from the
right_wing_motions table (U2 output).

Metrics per year:
- total_right_wing, pct_of_total, total_motions
- avg_right_support, avg_left_opposition, centrist_support
- avg_right_keyword_matches, extremity_index (U4 placeholder)
- yoy_right_wing_delta, yoy_pct_delta

Key finding: right-wing motions grew from ~4% (2018) to ~12% (2024-2025)
of all motions, with rising centrist support over time.
2026-05-05 21:20:12 +02:00
sgeboers d3dfb0ce2f feat(right-wing): hybrid motion classifier using keywords + votes
Implements U2: classify_motions.py loads keywords from U1 and classifies
motions as right-wing when:
- right_support >= 60% (CANONICAL_RIGHT parties voting 'voor')
- left_opposition >= 40% (CANONICAL_LEFT parties voting 'tegen')
- AND at least 1 right-wing keyword match in title/body_text

Outputs DuckDB table  with:
- motion_id, year, title, right_support, left_opposition, centrist_support
- right_keyword_matches, left_keyword_matches, classified flag

Classified 2986 of 28331 motions (10.5%) as right-wing.
2026-05-05 21:18:38 +02:00
sgeboers c6f8540671 feat(right-wing): derive right-wing keywords via differential TF-IDF
Implements U1: derive_keywords.py uses party voting patterns to classify
motions as right-wing vs left-wing, then computes differential TF-IDF on
cleaned motion titles to surface policy terms distinctive to right-wing
motions.

Key design choices:
- Vote threshold: 60% of parties in group must vote 'voor'
- Text cleaning strips motion prefixes aggressively (handles multi-word
  surnames, plural 'leden', t.v.v. parentheticals)
- Expanded Dutch stopword list filters procedural and generic noise
- Results written to analysis/right_wing/right_wing_keywords.json

Produces ~50 filtered terms including: asielzoekers, defensie, kernenergie,
boeren, vreemdelingenbeleid, stikstof, asielstop, strafrecht.
2026-05-05 21:14:11 +02:00
sgeboers 272d839a42 feat: agent-native refactor, SVD consistency fixes, UX cleanup, mobile support
- Refactor agent_tools to atomic primitives (24 tools, delete workflows)
- Fix SVD component score inconsistency between single-window and trajectory views
  (same PCA basis, same flip handling, same active-MP filter for current_parliament)
- Fix Dutch spelling: Huidig parliament -> Huidig parlement
- Remove all decorative emojis from UI (app.py, explorer.py, analysis tabs)
- Add dark theme matching sgeboers.nl (mint accent on dark background)
- Remove browser tab favicon and Streamlit chrome (deploy button, running status)
- Remove trajectories debug UI and EMA settings (hardcoded smooth_alpha=0.35)
- Switch layout to centered for mobile readability
- Add responsive CSS for mobile (touch targets, font sizing, overflow prevention)
- Update AGENTS.md and SYSTEM_PROMPT.md with active tool instructions
- Add compound docs for SVD consistency bug
- Update tests: 214 passed, 3 skipped
2026-05-04 21:56:40 +02:00
sgeboers 1f053f7d91 refactor: simplify Explorer to 3 focused tabs — compass, trajectories, SVD
Removes embedding-heavy search and generic browser tabs from the Explorer.
The project does not currently use embeddings meaningfully, so the similarity
search and browser features were dead weight.

Changes:
- explorer.py: Remove search and browser tabs, keep compass/trajectories/SVD
- explorer.py: Remove fused_embeddings and similarity_cache stats from sidebar
- Home.py: Update Explorer description to match new focused layout
- analysis/tabs/__init__.py: Remove search and browser exports
- tests: Update decomposition and import tests for new tab set

Result: Explorer now has 3 focused analytical tabs instead of 5.
2026-05-01 14:46:52 +02:00
sgeboers 07dd393533 cleanup: remove stale .mindmodel, old venvs, orphaned code, and transient artifacts
Removes:
- .mindmodel/ directory and related CI workflows (mindmodel-schedule.yml, mindmodel-validation.yml)
- scripts/mindmodel/ and scripts/validate_mindmodel.py
- src/types/ and src/validators/ (orphaned type modules, only used by mindmodel)
- tests/ci/, tests/scripts/mindmodel/, tests/types/, tests/validators/ (mindmodel-only tests)
- thoughts/ledgers/ and thoughts/shared/ (stale transient directories)
- .venv_axis and .venv_plotly (orphaned virtual environments, ~1.1 GB)
- outputs/blog-charts/ (stale generated HTML files)
- data/*.json sidecars (empty cache artifacts)
- __pycache__ and *.pyc files across repo

Updates:
- .gitignore: remove thoughts/shared/analyses/ entry

Space reclaimed: ~1.1 GB+
2026-05-01 12:11:06 +02:00
sgeboers 6e36fa2604 feat: persist and load explained variance for scree plots
- compute_svd_for_window now computes explained variance ratio (s²/sum(s²))
  and appends it as a metadata row (entity_type='metadata',
  entity_id='explained_variance') to motion_rows
- load_scree_data reads this metadata row from svd_vectors instead of
  querying the non-existent sv_metadata column
- run_svd_for_window counts only entity_type='motion' rows in stored_motion
  so metadata rows don't inflate the count
- Added 5 TDD tests covering load, compute, store, and round-trip

All 227 tests pass.
2026-05-01 10:34:31 +02:00
sgeboers 121c32ae8a fix: make scree and party-axis functions resilient to missing schema artifacts
- load_scree_data: return [] with TODO until schema stores EVR metadata
- load_party_axis_scores: compute from vectors instead of missing table
- load_party_axis_scores_for_window: same vector-based fallback
- load_party_scores_all_windows[_aligned]: check table existence,
  fall back to computing from load_positions when absent

All functions predated decomposition (5afbad1, 2026-04-05) and relied on
party_axis_scores / sv_metadata columns that were never created.
2026-05-01 10:20:55 +02:00
sgeboers 3bdb43f162 refactor: decompose explorer.py into analysis/tabs/ and add scheduler
- Extract 6 tab functions from explorer.py (3097 → 543 lines)
- Create analysis/tabs/_rendering.py with shared plotly helpers
- Move data logic to analysis/explorer_data.py
- Add lazy-import wrappers in explorer.py for backward compat
- Add scheduler.py with PipelineScheduler for daily pipeline runs
- Add test_explorer_decomposition.py (5 tests, all pass)
- Add test_scheduler.py (13 tests, all pass)
- Full test suite: 222 passed, 2 skipped
2026-05-01 01:05:55 +02:00
sgeboers 12807df642 infra: fix CI, config, docker-compose, README, and pre-commit
- Fix mindmodel-schedule.yml to use uv and Python 3.13
- Add pytest.yml for push/PR test gate
- Remove broken scheduler service from docker-compose.yml
- Consolidate config.py into analysis/config.py with backward-compat shim
- Rewrite README.md with quickstart and project overview
- Update pre-commit-config.yaml to enable black, ruff, isort hooks
- Add pyright type-check job (continue-on-error until baseline fixed)
- Update AGENTS.md with Gitea infrastructure note
2026-04-30 23:44:59 +02:00
sgeboers 62d8e15e03 fix: exclude quarterly windows from all PCA/SVD computation
- analysis/explorer_data.py: add AND window_id NOT LIKE '%-Q%' to
  _UNIFORM_DIM_SQL so quarterly windows are filtered at the source
- explorer.py: remove stale comment justifying quarterly inclusion;
  remove redundant '-Q' guard in SVD tab trajectory view
- scripts/recompute_svd.py: replace quarter_bounds() with year_bounds()
  that handles annual window IDs like '2024'; filter window list to
  annual-only before recomputing SVD
2026-04-16 21:34:45 +02:00
sgeboers cf549dcc1c feat(svd): update 8 of 10 axis labels derived from motion content
Revise SVD_THEMES labels based on TF-IDF analysis of top 50 motions
per component (pool size: current_parliament). Manual review of motion
titles ensures labels reflect actual parliamentary content rather than
party position semantics.

Key corrections:
- Axis 1: fiscal/economic policy vs social welfare + international rights
- Axis 4: active international engagement vs restraint
- Axis 5: pragmatic financial support vs progressive individual rights
- Axis 6: fossil fuels/financial incentives vs climate/intl rights
- Axis 7: practical-administrative vs idealistico-procedural (kept)
- Axis 8: European defense cooperation vs domestic socioeconomic policy
- Axis 9: concrete-administrative vs systemic reform
- Axis 10: citizen protection vs government regulation

Subagent analysis caught that axes 5 and 6 are NOT the same
(Nationale soevereiniteit) — manual motion review confirms distinct
content for each. Axes 1, 5, 6 had completely wrong labels.

Refs: thoughts/explorer/svd_label_review.md
See also: docs/brainstorms/2026-04-13-topic-derived-svd-labels-requirements.md
2026-04-13 23:59:50 +02:00
sgeboers 036c3f9a82 Use aligned PCA scores for all SVD components 1-10
- Add compute_nd_axes() for N-component PCA with Procrustes alignment
- Add _get_aligned_party_scores() helper in explorer.py
- Update build_svd_components_tab to use aligned scores for all components
- Compute flip direction from aligned score centroids using CANONICAL_LEFT/RIGHT
2026-04-13 23:22:49 +02:00
sgeboers 4d6c777d54 fix: use CANONICAL_LEFT/RIGHT in compass PCA for consistency with SVD components tab
Previously the compass (political_axis.py) used hardcoded party sets that
excluded Volt and PvdD, while the SVD components tab (svd_labels.py) used
CANONICAL_LEFT/RIGHT which includes them. This caused inconsistencies in
axis orientation where Volt appeared most left on the compass but PvdD
appeared most left in the SVD components visualization.

Changes:
- Import CANONICAL_LEFT/RIGHT from config in political_axis.py
- Replace hardcoded party sets with CANONICAL_LEFT/RIGHT for axis orientation
- Update tests to match new SVD_THEMES labels
2026-04-13 22:50:11 +02:00
sgeboers b1847f8d07 refactor(svd): update all 10 component labels based on motion analysis
Redo theme analysis after pool-based motion assignment change.
New labels reflect actual motion content per component:

1. Economische sectorbelangen versus sociale welvaart
2. Nationalistische versus multilateralistische oriëntatie
3. Verzorgingsstaat versus defensie en nationale veiligheid
4. Internationale instituties en multilateralisme versus nationale soevereiniteit
5. Gemeenschapszin versus individuele rechten
6. Ecologische transitie versus economische conservatie
7. Praktisch-bestuurlijk versus idealistisch-proceduraal
8. Internationale samenwerking versus nationale soevereiniteit
9. Pragmatische probleemoplossing versus regulering
10. Minder overheidsbemoeienis versus meer handhaving
2026-04-13 22:35:03 +02:00
sgeboers 467b0d1be1 fix: SVD tab now uses raw SVD values for ALL components 1-10
Previously, components 1-2 in the SVD tab used Procrustes-aligned PCA
coordinates (from load_positions), which meant the SVD tab showed PCA
dimensions of the 50D aligned space rather than the actual raw SVD
components. This was a fundamental inconsistency — the SVD tab's component 2
showed completely different party ordering than the raw SVD component 2.

Changes:
- explorer.py: Unified all components 1-10 to use raw SVD values via
  load_party_axis_scores_for_window(). Removed the separate
  load_positions() path for components 1-2. Now all components use the
  same data source (50D vectors from svd_vectors table).
- explorer.py: Updated flip computation to cover ALL components 1-10
  (was range 3-11 for components 3-10 only). The compute_flip_direction
  function correctly determines sign for each component.
- explorer.py: Unified rendering to always use _render_party_axis_chart_1d
  (was _render_party_axis_chart for components 1-2 using 2D coords).
- explorer.py: Unified trajectory to always use load_party_scores_all_windows.
- analysis/config.py: Updated component 1 label (simplified explanation,
  removed coalition-specific policy references).
- analysis/config.py: Updated component 2 label to "Nationalistisch versus
  kosmopolitisch" matching raw SVD data (PVV/FVD at positive extreme,
  Volt/DENK/GL-PvdA at negative extreme).
- tests: Updated test assertions to match new labels.
- scripts/validate_svd_themes.py: Verified all components pass right-wing
  alignment check, config flip consistency, and theme pole consistency.

Fixes the core inconsistency: SVD tab component 2 now uses the same raw
SVD data as components 3-10, with consistent party ordering and labels.
The compass remains a separate PCA-based visualization.
2026-04-13 21:51:21 +02:00
sgeboers 1dd660afc7 refactor: make duckdb imports optional in analysis modules
Allow analysis modules to be imported in lightweight test environments
without duckdb installed. Modules that need duckdb for actual queries
still require it at runtime, but import-time failures are handled gracefully.
2026-04-12 21:02:37 +02:00
sgeboers 823df6f9ee fix: resolve SVD axis label alignment and score mismatch in tijdtraject view
Two related bugs fixed:

1. Label alignment: Removed static left_pole/right_pole from SVD_THEMES
   entries. These labels assumed a fixed flip direction but could mismatch
   with runtime flip computation, causing right-wing parties to appear on
   the wrong side. Labels are now always derived from positive_pole,
   negative_pole, and the runtime flip direction.

2. Score mismatch: Changed tijdtraject view for components 3-10 from
   load_party_scores_all_windows_aligned() to load_party_scores_all_windows().
   Procrustes alignment rotates the full 50-dim vector space to align
   components 1-2, but this also transforms components 3-10, making their
   scores incomparable with the single-window view. Per-window flip
   computation already handles orientation alignment for these components.

Also updated svd_labels.py to prefer analysis.config as the canonical
source for SVD_THEMES, falling back to explorer only when config is
unavailable.
2026-04-12 21:02:28 +02:00
sgeboers c71710433a fix: correct axis 4 theme to match actual party positions (NSC/BBB vs D66/CDA/JA21) 2026-04-05 02:20:44 +02:00
sgeboers bce0ed56de fix: add semantic left_pole/right_pole labels to SVD axes 2026-04-05 02:11:19 +02:00
sgeboers 414c16ae9e refactor: extract data loading and trajectory logic from explorer.py
- Move trajectory analysis to analysis/trajectory.py (+136 lines)
- Move projection helpers to analysis/projections.py (+128 lines)
- Extract tab-specific data loaders to analysis/tabs/ (8 modules, +133 lines)
- Remove 702 lines from explorer.py (data loading extracted to
  analysis/explorer_data.py and new modules)
- Add axis label fallback tests (tests/test_axis_label_fallback.py)
- Add session docs: brainstorms, ideation, plans, and test-failures
2026-04-05 00:51:30 +02:00
sgeboers 5afbad11ad feat: add right-wing party axis validation
- Add CANONICAL_RIGHT (PVV, FVD, JA21, SGP) and CANONICAL_LEFT frozensets
  to analysis/config.py as the canonical source of truth
- Update analysis/svd_labels.py to import from config; re-export as
  RIGHT_PARTIES/LEFT_PARTIES for backward compatibility
- Add build_window_party_scores helper to analysis/explorer_data.py
- Add 7 integration tests in tests/test_axis_political_orientation.py
  validating that canonical right parties appear on the right side of SVD
  axes (x=component 1, y=component 2) using real DuckDB data
2026-04-05 00:42:46 +02:00
sgeboers abd3281044 refactor: remove Stemgedrag cohesie section and fallback axis message
- Remove voting discipline (cohesie) section from Political Compass tab
- Remove 'empirisch stempatroon zonder duidelijke ideologische richting' fallback message from axis classifier
- Clean up unused fallback template from _INTERPRETATION_TEMPLATES
2026-04-02 21:46:24 +02:00
sgeboers 5b3cf23d36 refactor: use svd_labels for fallback labels in explorer and axis_classifier (Task 4) 2026-04-02 21:05:30 +02:00
sgeboers 36b58ad50d refactor: use svd_labels module for fallback labels in axis_classifier (Task 3) 2026-04-02 21:02:53 +02:00
sgeboers 5b1be26050 refactor: move SVD_THEMES to module level for import (Task 2) 2026-04-02 21:02:03 +02:00
sgeboers 9f98dbae60 Add debug st.info before st.plotly_chart to diagnose invisible chart 2026-03-31 01:49:38 +02:00
sgeboers 72a8dd2721 Fix RNG re-seeding per party and vectorize bootstrap loop
Move rng initialization before the party loop so each party gets a
unique segment of the random stream instead of identical sequences.
Replace Python bootstrap loop with vectorized numpy indexing.
2026-03-29 23:17:33 +02:00
sgeboers cd8aeec997 Add compute_party_bootstrap_cis() to political_axis.py with tests
Pure numpy function that computes bootstrap confidence intervals for
party centroid vectors. Handles N>=2 (bootstrap), N=1 (degenerate CI),
and N=0 (excluded) cases. Uses np.random.default_rng for reproducibility.
2026-03-29 23:13:30 +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 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 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