# SVD Axis Labels: Enforce Canonical Left/Right Orientation ## Problem SVD axis labels do not consistently reflect the repository's political convention: right-wing parties (PVV, FVD, JA21, SGP) must appear on the RIGHT side of all axes in visualizations. Current code uses inconsistent inline party sets across files (`svd_labels.py` has 9 right parties, `political_axis.py` has 6), and no automated validation exists to enforce the convention. ## Goals - Guarantee that canonical right-wing parties (PVV, FVD, JA21, SGP) appear on the RIGHT side of all SVD axes. - Centralize canonical party sets in a single source of truth. - Automatically flip axis orientation when canonical parties appear on the wrong side. - Add unit tests and CI validation to prevent regressions. - Maintain backward compatibility for existing callers. ## Architecture ### New Module: `analysis/config.py` ```python """Canonical political party sets for axis orientation validation.""" CANONICAL_RIGHT = frozenset({"PVV", "FVD", "JA21", "SGP"}) CANONICAL_LEFT = frozenset({ "SP", "PvdA", "GL", "GroenLinks", "GroenLinks-PvdA", "DENK", "PvdD", "Volt" }) ``` ### Updated Module: `analysis/svd_labels.py` - Import canonical sets from `analysis.config`. - Replace inline `RIGHT_PARTIES` / `LEFT_PARTIES` with config values. - Export backward-compat aliases: ```python from analysis.config import CANONICAL_RIGHT, CANONICAL_LEFT RIGHT_PARTIES = CANONICAL_RIGHT LEFT_PARTIES = CANONICAL_LEFT ``` - Update `compute_flip_direction()` to use `CANONICAL_RIGHT` for flip decisions. ### Data Flow 1. Compute raw component scores/centroids (existing logic). 2. Project canonical right party vectors onto the component. 3. Determine where canonical right parties lie along the component axis. 4. If majority of `CANONICAL_RIGHT` items are on the left side, set `flip = True`. 5. When `flip` is True, swap/negate the axis and flip label semantics before returning to callers. 6. Numerical PCs remain intact; only orientation + labels change at presentation boundary. ### Error Handling & Fallbacks - **No canonical parties present**: Log warning, fall back to existing behavior (no flip), optionally surface UI notice. - **Ambiguous placement**: Use majority rule with deterministic tie-breaking (tie → do not flip, log warning). - **Backward compatibility**: Preserve public functions (`get_svd_label`, `get_fallback_labels`, `compute_flip_direction`); only orientation may flip. ## Testing Strategy ### Unit Tests (`tests/test_svd_labels.py`) - **Synthetic flip test**: Create synthetic vectors where canonical right parties have negative component values; assert `compute_flip_direction` returns `True` and labels map correctly. - **Synthetic no-flip test**: Canonical right parties on positive side; assert `compute_flip_direction` returns `False`. - **Real window test**: Load a representative window and assert every item from `CANONICAL_RIGHT` appears on the right side after flip logic. - **Backward compat test**: Existing tests in `test_svd_labels.py` continue to pass after import swap. ### CI Validation - Add pytest test group enforcing the rule: ```bash pytest -q tests/test_svd_labels.py::test_canonical_right_on_right ``` - Test loads at least one representative window and asserts PVV/FVD/JA21/SGP appear on the right side of the principal political axis. - Prevents regressions in PR pipeline. ## Migration Plan 1. Create `analysis/config.py` with canonical sets. 2. Update `analysis/svd_labels.py` imports and export aliases. 3. Update `compute_flip_direction` to use canonical sets. 4. Add/update unit tests in `tests/test_svd_labels.py`. 5. Run full test suite to verify backward compatibility. 6. Commit changes locally (no push until user approves). ### Out of Scope (Follow-up) - `analysis/political_axis.py` currently uses different party sets for PCA centroid orientation. Recommended follow-up: align with `config.py` in separate PR. - UI notice for missing canonical parties (non-blocking enhancement). ## Success Criteria - `CANONICAL_RIGHT` and `CANONICAL_LEFT` defined in `analysis/config.py`. - `svd_labels.py` imports from config; exports aliases for backward compat. - All existing tests pass after changes. - New test `test_canonical_right_on_right` passes. - Running `pytest tests/test_svd_labels.py -q` shows no regressions. - Right-wing parties (PVV, FVD, JA21, SGP) consistently appear on the RIGHT side of all axes in visualizations. ## Related Files - `analysis/config.py` (new) - `analysis/svd_labels.py` (update) - `tests/test_svd_labels.py` (update) - `docs/solutions/best-practices/svd-labels-voting-patterns-not-semantics.md` (existing convention doc) - `AGENTS.md` (convention reference)