You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
motief/docs/solutions/ui-bugs/svd-time-trajectory-score-m...

5.3 KiB

title date module problem_type component symptoms root_cause resolution_type severity tags
SVD time trajectory shows different scores than single-window view for same component and window 2026-05-04 analysis ui_bug analysis [Party position numbers in Tijdtraject view differ from Enkel venster view for the exact same component and window Same party has opposite sign in trajectory vs single-window when theme flip is active Inconsistent numerical values break user trust in the SVD Components tab] logic_error code_fix high [svd pca time-trajectory parliamentary-explorer alignment flip]

SVD Time Trajectory Shows Different Scores Than Single-Window View

Problem

In the parliamentary explorer's "SVD Components" tab, party position numbers differed between the "Enkel venster" (single window) view and the "Tijdtraject" (time trajectory) view for the SAME component and SAME window. Users comparing a specific year across the two views saw inconsistent numerical scores.

Symptoms

  • Selecting component 2, window "2023-2024" in single-window shows PVV at +0.42, but the trajectory view at the same point shows PVV at -0.15
  • Signs invert for certain components when theme["flip"] is True
  • The mismatch occurs even though both views claim to show the same underlying SVD component

What Didn't Work

Initial suspicion that the difference came from Procrustes alignment or data caching issues. Checking whether load_party_scores_all_windows_aligned() vs load_party_scores_all_windows() was the culprit. However, both views were already using the same alignment path.

The real cause was subtler: the trajectory view was computing PCA over a different set of windows than the single-window view, and then ignoring the flip flag entirely.

Solution

Fix 1: Align trajectory PCA computation with single-window computation

In analysis/explorer_data.py, function _get_aligned_trajectory_scores():

def _get_aligned_trajectory_scores(
    db_path: str, windows: List[str], n_components: int = 10
) -> Dict[str, Dict[str, List[float]]]:
    from analysis.political_axis import compute_nd_axes

    all_uniform_windows = get_uniform_dim_windows(db_path)
    scores_by_window, _ = compute_nd_axes(
        db_path, window_ids=all_uniform_windows, n_components=n_components
    )
    # ... rest filters to requested windows

Change: Compute PCA on all uniform-dim windows (matching get_aligned_party_scores), then filter to the requested windows. Previously, _get_aligned_trajectory_scores() passed only a subset of windows (excluding _current_year) to compute_nd_axes(), which produced different principal components, global mean, and flip signs.

Fix 2: Apply theme flip in trajectory rendering

In analysis/tabs/_rendering.py, function _render_svd_time_trajectory():

    idx = comp_sel - 1
    flip = theme.get("flip", False)
    # ...
    for window in sorted_windows:
        scores_by_party = party_scores_by_window.get(window, {})
        for party in selected_parties:
            scores = scores_by_party.get(party, [])
            if scores and len(scores) > idx:
                try:
                    score = float(scores[idx])
                    if flip:
                        score = -score
                    party_trajectories.setdefault(party, []).append((window, score))
                except (ValueError, TypeError):
                    continue

Change: Added flip application to negate scores when theme.get("flip", False) is True. _render_party_axis_chart_1d() already did this, but _render_svd_time_trajectory() completely ignored the flip flag.

Why This Works

  1. Same PCA basis: compute_nd_axes() computes global PCA across all provided windows. When the single-window view used all uniform-dim windows and the trajectory view used a subset, the resulting components, mean centering, and variance explained were different. Passing the same window_ids to compute_nd_axes() guarantees identical PCA bases.

  2. Same flip handling: The single-window view negates scores when flip=True. The trajectory view now does the same, ensuring both views display numerically identical values for the same (window, component, party) tuple.

Prevention

  • When multiple views display the same underlying SVD/PCA data, ensure they all call compute_nd_axes() with the identical set of window IDs
  • Never apply visual transformations (like theme["flip"]) in one view but omit them in another — keep rendering logic symmetric
  • Add a regression test that asserts get_aligned_party_scores(window, comp) equals _get_aligned_trajectory_scores([window], comp)[window] for sampled windows and components
  • Document that compute_nd_axes() is a global operation over its input windows; any subset produces a different coordinate frame
  • analysis/explorer_data.py_get_aligned_trajectory_scores() fix (all uniform windows)
  • analysis/tabs/_rendering.py_render_svd_time_trajectory() fix (flip application)
  • analysis/political_axis.pycompute_nd_axes() global PCA logic
  • Builds on docs/solutions/ui-bugs/svd-compass-components-position-inconsistency.md (consistent alignment for components 1-2)
  • Builds on docs/solutions/ui-bugs/svd-axis-pole-labels-incorrect-after-flip.md (runtime flip mechanism)