---
module: svd
date: 2026-04-16
category: docs/solutions/logic-errors
problem_type: logic_error
component: rails_view
severity: medium
symptoms:
  - "SVD tab displayed VVD component 1 score of 0.108"
  - "Compass displayed VVD component 1 score of 0.335"
  - "Significant numerical discrepancy between two views showing same data"
root_cause: scope_issue
resolution_type: code_fix
tags:
  - svd
  - voting-analysis
  - filter-scope
  - party-scores
---

# SVD Tab Party Scores Don't Match Compass

## Problem

The SVD tab displayed VVD component 1 score as 0.108 while the compass visualization showed 0.335 — a 3x discrepancy caused by including inactive/historical MPs in the SVD calculation.

## Symptoms

- **SVD tab**: VVD comp1 score = 0.108 (incorrect)
- **Compass**: VVD comp1 score = 0.335 (correct, verified against compass reference)
- **Discrepancy**: ~3x difference between views
- **Scope**: Affects all parties but most visible for VVD (82 historical MPs vs ~50 active)

## What Didn't Work

N/A — straightforward fix once root cause identified through comparison with compass implementation at `explorer.py:1473`.

## Solution

The `_get_aligned_party_scores()` function in `views/svd.py` was missing an `active_MP` filter when calculating party means for the current parliament window.

**Before (buggy code):**

```python
def _get_aligned_party_scores(party_id: str, dimension: str, ...) -> list:
    raw_scores = execute_query(score_query, ...)
    # Missing: no active_MP filter
    return raw_scores
```

**After (fixed code):**

```python
def get_aligned_party_scores(party_id: str, dimension: str, ...) -> list:
    raw_scores = execute_query(score_query, ...)

    # Filter to only active MPs (matches compass behavior at explorer.py:1473)
    active_mps = {m[0] for m in active_mp_query if m[0] is not None}
    scores = [s for s in raw_scores if s[0] in active_mps]

    return scores
```

Key changes:
1. Extracted function to module-level for testability
2. Added active MP filtering using the same query pattern as compass (`explorer.py:1473`)
3. Filter ensures only MPs in current parliament window are included

**Verification:**
- Without filter: VVD comp1 = 0.1083
- With filter: VVD comp1 = 0.3366 (matches compass reference of 0.3350)
- Test suite: 169/169 tests passing

## Why This Works

The root cause was including all 82 historical VVD MPs instead of only the active ones. The database (`data/motions.db`) contains MPs from multiple parliaments, and the `_get_aligned_party_scores()` function wasn't filtering by `active_MP`. The compass correctly applied this filter, explaining the discrepancy.

## Prevention

1. **Test suite**: Comprehensive tests in `tests/svd_test.py` covering alignment calculations with active MP filtering
2. **Cross-view validation**: Compare SVD and compass scores for each party — assert values match within tolerance
3. **Query pattern documentation**: All score queries must include `active_MP` filter when calculating party means
4. **Code review checklist**: Require active_MP filter for any new score calculation queries
5. **Automated regression**: Add CI check that runs comparison between SVD tab and compass for all parties

## Related Issues

- `docs/solutions/logic-errors/svd-theme-divergence-from-party-positions.md` — Related domain issue: SVD scores not matching actual party positions
- `docs/solutions/logic-errors/svd-component-labels-mismatch.md` — Related theme: Labels/data alignment mismatches
- `docs/solutions/best-practices/svd-labels-voting-patterns-not-semantics.md` — Core principle: SVD captures voting patterns, verify against actual voting data
