|
|
|
@ -481,7 +481,6 @@ def load_positions( |
|
|
|
""" |
|
|
|
""" |
|
|
|
from analysis.political_axis import compute_2d_axes |
|
|
|
from analysis.political_axis import compute_2d_axes |
|
|
|
|
|
|
|
|
|
|
|
# Use only annual windows (quarterly windows are excluded by get_uniform_dim_windows). |
|
|
|
|
|
|
|
all_available = get_uniform_dim_windows(db_path) |
|
|
|
all_available = get_uniform_dim_windows(db_path) |
|
|
|
|
|
|
|
|
|
|
|
if not all_available: |
|
|
|
if not all_available: |
|
|
|
@ -539,6 +538,56 @@ def load_active_mps(db_path: str) -> set: |
|
|
|
return explorer_data.load_active_mps(db_path) |
|
|
|
return explorer_data.load_active_mps(db_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_aligned_party_scores( |
|
|
|
|
|
|
|
db_path: str, window: str, active_mps: set | None = None |
|
|
|
|
|
|
|
) -> Dict[str, np.ndarray]: |
|
|
|
|
|
|
|
"""Get party scores for all N components from aligned PCA positions. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For current_parliament, pass active_mps to filter to only seated MPs |
|
|
|
|
|
|
|
(matching the compass behaviour). Historical windows include all MPs. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
|
|
|
db_path: Path to DuckDB database |
|
|
|
|
|
|
|
window: Window identifier (e.g. 'current_parliament', '2025') |
|
|
|
|
|
|
|
active_mps: Set of active MP names to filter current_parliament by. |
|
|
|
|
|
|
|
Required when window is 'current_parliament' to match compass. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
from analysis.political_axis import compute_nd_axes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
annual_windows = get_uniform_dim_windows(db_path) |
|
|
|
|
|
|
|
scores_by_window, _ = compute_nd_axes( |
|
|
|
|
|
|
|
db_path, window_ids=annual_windows, n_components=10 |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
window_scores = scores_by_window.get(window, {}) |
|
|
|
|
|
|
|
if not window_scores: |
|
|
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# For current_parliament, filter to active MPs (still seated) to match compass. |
|
|
|
|
|
|
|
# Historical windows include all MPs active at the time — no restriction needed. |
|
|
|
|
|
|
|
if window == "current_parliament" and active_mps is not None: |
|
|
|
|
|
|
|
window_scores = {mp: sc for mp, sc in window_scores.items() if mp in active_mps} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Load party map to convert MP names to parties |
|
|
|
|
|
|
|
_party_map = load_party_map(db_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Aggregate MP scores to party centroids per component |
|
|
|
|
|
|
|
n_comps = 10 |
|
|
|
|
|
|
|
party_scores_agg: Dict[str, List[np.ndarray]] = {} |
|
|
|
|
|
|
|
for mp_name, scores in window_scores.items(): |
|
|
|
|
|
|
|
party = _party_map.get( |
|
|
|
|
|
|
|
mp_name, _party_map.get(mp_name.split("(")[0].strip(), None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
if party: |
|
|
|
|
|
|
|
party_scores_agg.setdefault(party, []).append(scores[:n_comps]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Compute mean scores per party for each component |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
party: np.mean(np.vstack(score_list), axis=0) |
|
|
|
|
|
|
|
for party, score_list in party_scores_agg.items() |
|
|
|
|
|
|
|
if score_list |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_party_discipline( |
|
|
|
def compute_party_discipline( |
|
|
|
db_path: str, |
|
|
|
db_path: str, |
|
|
|
start_date: str, |
|
|
|
start_date: str, |
|
|
|
@ -2640,35 +2689,10 @@ def build_svd_components_tab(db_path: str) -> None: |
|
|
|
# This ensures consistency between compass and SVD components tab. |
|
|
|
# This ensures consistency between compass and SVD components tab. |
|
|
|
def _get_aligned_party_scores(window: str) -> Dict[str, np.ndarray]: |
|
|
|
def _get_aligned_party_scores(window: str) -> Dict[str, np.ndarray]: |
|
|
|
"""Get party scores for all N components from aligned PCA positions.""" |
|
|
|
"""Get party scores for all N components from aligned PCA positions.""" |
|
|
|
from analysis.political_axis import compute_nd_axes |
|
|
|
active_mps = ( |
|
|
|
|
|
|
|
load_active_mps(db_path) if window == "current_parliament" else None |
|
|
|
annual_windows = get_uniform_dim_windows(db_path) |
|
|
|
|
|
|
|
scores_by_window, _ = compute_nd_axes( |
|
|
|
|
|
|
|
db_path, window_ids=annual_windows, n_components=10 |
|
|
|
|
|
|
|
) |
|
|
|
) |
|
|
|
window_scores = scores_by_window.get(window, {}) |
|
|
|
return get_aligned_party_scores(db_path, window, active_mps) |
|
|
|
if not window_scores: |
|
|
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Load party map to convert MP names to parties |
|
|
|
|
|
|
|
_party_map = load_party_map(db_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Aggregate MP scores to party centroids per component |
|
|
|
|
|
|
|
n_comps = 10 |
|
|
|
|
|
|
|
party_scores_agg: Dict[str, List[np.ndarray]] = {} |
|
|
|
|
|
|
|
for mp_name, scores in window_scores.items(): |
|
|
|
|
|
|
|
party = _party_map.get( |
|
|
|
|
|
|
|
mp_name, _party_map.get(mp_name.split("(")[0].strip(), None) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
if party: |
|
|
|
|
|
|
|
party_scores_agg.setdefault(party, []).append(scores[:n_comps]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Compute mean scores per party for each component |
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
party: np.mean(np.vstack(score_list), axis=0) |
|
|
|
|
|
|
|
for party, score_list in party_scores_agg.items() |
|
|
|
|
|
|
|
if score_list |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Extract 1D scores for this component using Procrustes-aligned PCA scores. |
|
|
|
# Extract 1D scores for this component using Procrustes-aligned PCA scores. |
|
|
|
# All 10 components use _get_aligned_party_scores (compute_nd_axes with annual-only |
|
|
|
# All 10 components use _get_aligned_party_scores (compute_nd_axes with annual-only |
|
|
|
|