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'.
This commit is contained in:
@@ -551,3 +551,71 @@ def compute_2d_axes(
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown method '%s'" % method)
|
||||
|
||||
|
||||
def compute_svd_spectrum(
|
||||
db_path: str,
|
||||
window_ids: Optional[List[str]] = None,
|
||||
normalize_vectors: bool = True,
|
||||
) -> List[float]:
|
||||
"""Return explained variance ratios (%) for all SVD components, sorted descending.
|
||||
|
||||
Uses the same Procrustes-aligned multi-window matrix as compute_2d_axes so the
|
||||
scree plot is consistent with the compass axes.
|
||||
|
||||
Args:
|
||||
db_path: path to duckdb
|
||||
window_ids: optional ordered list of windows (defaults to all)
|
||||
normalize_vectors: whether to L2-normalise each MP vector before stacking
|
||||
|
||||
Returns:
|
||||
List of EVR percentages sorted descending (e.g. [24.1, 10.4, 7.2, ...])
|
||||
"""
|
||||
import importlib
|
||||
|
||||
_trajectory = importlib.import_module("analysis.trajectory")
|
||||
|
||||
if window_ids is None:
|
||||
window_ids = _trajectory._load_window_ids(db_path)
|
||||
|
||||
raw_window_vecs: Dict[str, Dict[str, np.ndarray]] = {}
|
||||
for wid in window_ids:
|
||||
raw_window_vecs[wid] = _trajectory._load_mp_vectors_for_window(db_path, wid)
|
||||
|
||||
if not raw_window_vecs:
|
||||
return []
|
||||
|
||||
# Pad to uniform dimension before Procrustes alignment
|
||||
max_dim = max(v.shape[0] for d in raw_window_vecs.values() for v in d.values())
|
||||
padded: Dict[str, Dict[str, np.ndarray]] = {}
|
||||
for wid, d in raw_window_vecs.items():
|
||||
padded[wid] = {
|
||||
e: np.pad(v, (0, max_dim - v.shape[0])) if v.shape[0] < max_dim else v
|
||||
for e, v in d.items()
|
||||
}
|
||||
|
||||
aligned = _trajectory._procrustes_align_windows(padded)
|
||||
|
||||
all_vecs = []
|
||||
for d in aligned.values():
|
||||
for v in d.values():
|
||||
if normalize_vectors:
|
||||
n = np.linalg.norm(v)
|
||||
all_vecs.append(v / n if n > 1e-10 else v)
|
||||
else:
|
||||
all_vecs.append(v)
|
||||
|
||||
if not all_vecs:
|
||||
return []
|
||||
|
||||
M = np.vstack(all_vecs)
|
||||
Mc = M - M.mean(axis=0)
|
||||
try:
|
||||
_, s, _ = np.linalg.svd(Mc, full_matrices=False)
|
||||
except np.linalg.LinAlgError:
|
||||
_logger.exception("SVD failed in compute_svd_spectrum")
|
||||
return []
|
||||
|
||||
sv2 = s**2
|
||||
evr = sv2 / (sv2.sum() + 1e-20) * 100
|
||||
return list(evr) # already sorted descending by SVD
|
||||
|
||||
Reference in New Issue
Block a user