|
|
|
|
@ -14,7 +14,6 @@ Both modes return a dict mapping mp_name → scalar score for the given window. |
|
|
|
|
|
|
|
|
|
import json |
|
|
|
|
import logging |
|
|
|
|
import re |
|
|
|
|
from typing import Dict, List, Optional, Tuple |
|
|
|
|
|
|
|
|
|
import numpy as np |
|
|
|
|
@ -191,23 +190,17 @@ def compute_2d_axes( |
|
|
|
|
|
|
|
|
|
aligned_window_vecs = _trajectory._procrustes_align_windows(raw_window_vecs) |
|
|
|
|
|
|
|
|
|
# Stack all vectors across windows into a single matrix for PCA if needed. |
|
|
|
|
# pca_vecs / pca_index: annual windows only (e.g. "2024") — used for SVD axis derivation. |
|
|
|
|
# all_vecs / entity_index: every window — used for projection onto the derived axes. |
|
|
|
|
pca_vecs = [] |
|
|
|
|
# Stack all vectors across windows into a single matrix for PCA if needed |
|
|
|
|
all_vecs = [] |
|
|
|
|
entity_index = [] # parallel list of (window_id, entity) |
|
|
|
|
for wid, d in aligned_window_vecs.items(): |
|
|
|
|
for ent, v in d.items(): |
|
|
|
|
if normalize_vectors: |
|
|
|
|
n = np.linalg.norm(v) |
|
|
|
|
vec = v / n if n > 1e-10 else v |
|
|
|
|
all_vecs.append(v / n if n > 1e-10 else v) |
|
|
|
|
else: |
|
|
|
|
vec = v |
|
|
|
|
all_vecs.append(vec) |
|
|
|
|
all_vecs.append(v) |
|
|
|
|
entity_index.append((wid, ent)) |
|
|
|
|
if re.match(r"^\d{4}$", wid): |
|
|
|
|
pca_vecs.append(vec) |
|
|
|
|
|
|
|
|
|
if len(all_vecs) == 0: |
|
|
|
|
_logger.info("No vectors loaded for windows %s", window_ids) |
|
|
|
|
@ -215,19 +208,9 @@ def compute_2d_axes( |
|
|
|
|
|
|
|
|
|
M = np.vstack(all_vecs) |
|
|
|
|
|
|
|
|
|
# If no annual windows found, fall back to all windows for SVD. |
|
|
|
|
if len(pca_vecs) == 0: |
|
|
|
|
_logger.warning( |
|
|
|
|
"No annual windows found; falling back to all %d windows for SVD axis derivation", |
|
|
|
|
len(aligned_window_vecs), |
|
|
|
|
) |
|
|
|
|
M_pca = M |
|
|
|
|
else: |
|
|
|
|
M_pca = np.vstack(pca_vecs) |
|
|
|
|
|
|
|
|
|
if method == "pca": |
|
|
|
|
# centre using annual-only mean so SVD axes are not diluted by quarterly windows |
|
|
|
|
Mc = M_pca - M_pca.mean(axis=0) |
|
|
|
|
# centre globally |
|
|
|
|
Mc = M - M.mean(axis=0) |
|
|
|
|
try: |
|
|
|
|
U, s, Vt = np.linalg.svd(Mc, full_matrices=False) |
|
|
|
|
except np.linalg.LinAlgError: |
|
|
|
|
@ -375,8 +358,8 @@ def compute_2d_axes( |
|
|
|
|
evr1 * 100, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# project per-window vectors (centre by annual-window global mean, consistent with SVD axes) |
|
|
|
|
global_mean = M_pca.mean(axis=0) |
|
|
|
|
# project per-window vectors (centre by global mean) |
|
|
|
|
global_mean = M.mean(axis=0) |
|
|
|
|
axes["global_mean"] = global_mean |
|
|
|
|
positions_by_window: Dict[str, Dict[str, Tuple[float, float]]] = { |
|
|
|
|
wid: {} for wid in window_ids |
|
|
|
|
|