parent
ea6e33fa3f
commit
0a39fa0fe3
@ -0,0 +1,103 @@ |
|||||||
|
# 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) |
||||||
Loading…
Reference in new issue