Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f9e8965cd | ||
|
|
0d17c6364a | ||
|
|
fafb53cb3d | ||
|
|
cd47fd5a83 |
+79
-42
@@ -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,
|
||||||
@@ -1416,7 +1465,16 @@ def build_compass_tab(db_path: str, window_size: str) -> None:
|
|||||||
active_mps = load_active_mps(db_path)
|
active_mps = load_active_mps(db_path)
|
||||||
|
|
||||||
# Sort windows: year windows first (ascending), current_parliament last.
|
# Sort windows: year windows first (ascending), current_parliament last.
|
||||||
year_windows = sorted(w for w in positions_by_window if w != "current_parliament")
|
# Exclude the current calendar year — it is already fully covered by current_parliament
|
||||||
|
# and showing both creates confusion (2026 ⊂ current_parliament).
|
||||||
|
import datetime as _dt
|
||||||
|
|
||||||
|
_current_year = str(_dt.date.today().year)
|
||||||
|
year_windows = sorted(
|
||||||
|
w
|
||||||
|
for w in positions_by_window
|
||||||
|
if w != "current_parliament" and w != _current_year
|
||||||
|
)
|
||||||
has_current = "current_parliament" in positions_by_window
|
has_current = "current_parliament" in positions_by_window
|
||||||
windows = year_windows + (["current_parliament"] if has_current else [])
|
windows = year_windows + (["current_parliament"] if has_current else [])
|
||||||
|
|
||||||
@@ -1576,22 +1634,14 @@ def build_compass_tab(db_path: str, window_size: str) -> None:
|
|||||||
xaxis={"range": [-1, 1]},
|
xaxis={"range": [-1, 1]},
|
||||||
yaxis={"range": [-0.6, 0.6]},
|
yaxis={"range": [-0.6, 0.6]},
|
||||||
)
|
)
|
||||||
_add_y_direction_annotations(fig)
|
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.plotly_chart(fig, use_container_width=True)
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
_x_interp = axis_def.get("x_interpretation", {}).get(window_idx, "")
|
_x_interp = axis_def.get("x_interpretation", {}).get(window_idx, "")
|
||||||
_y_interp = axis_def.get("y_interpretation", {}).get(window_idx, "")
|
|
||||||
if (
|
if (
|
||||||
_x_interp
|
_x_interp
|
||||||
and axis_def.get("x_quality", {}).get(window_idx, 1.0) < _THRESHOLD
|
and axis_def.get("x_quality", {}).get(window_idx, 1.0) < _THRESHOLD
|
||||||
):
|
):
|
||||||
st.caption(_x_interp)
|
st.caption(_x_interp)
|
||||||
if (
|
|
||||||
_y_interp
|
|
||||||
and axis_def.get("y_quality", {}).get(window_idx, 1.0) < _THRESHOLD
|
|
||||||
):
|
|
||||||
st.caption(_y_interp)
|
|
||||||
|
|
||||||
# Voting discipline analysis
|
# Voting discipline analysis
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
@@ -2568,9 +2618,14 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
# Default party scores already loaded earlier for sidebar controls.
|
# Default party scores already loaded earlier for sidebar controls.
|
||||||
# ALL components 1-10 use raw (non-aligned) SVD vectors.
|
# ALL components 1-10 use raw (non-aligned) SVD vectors.
|
||||||
# The compass uses Procrustes-aligned PCA — separate visualization.
|
# The compass uses Procrustes-aligned PCA — separate visualization.
|
||||||
# Get available windows from svd_vectors
|
# Get available windows from svd_vectors; exclude current year (covered by current_parliament)
|
||||||
|
import datetime as _dt
|
||||||
|
|
||||||
|
_current_year = str(_dt.date.today().year)
|
||||||
available_windows = get_uniform_dim_windows(db_path)
|
available_windows = get_uniform_dim_windows(db_path)
|
||||||
year_windows = sorted(w for w in available_windows if w != "current_parliament")
|
year_windows = sorted(
|
||||||
|
w for w in available_windows if w != "current_parliament" and w != _current_year
|
||||||
|
)
|
||||||
has_current = "current_parliament" in available_windows
|
has_current = "current_parliament" in available_windows
|
||||||
svd_windows = year_windows + (["current_parliament"] if has_current else [])
|
svd_windows = year_windows + (["current_parliament"] if has_current else [])
|
||||||
|
|
||||||
@@ -2634,37 +2689,15 @@ 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
|
# Extract 1D scores for this component using Procrustes-aligned PCA scores.
|
||||||
_party_map = load_party_map(db_path)
|
# All 10 components use _get_aligned_party_scores (compute_nd_axes with annual-only
|
||||||
|
# windows). This is mathematically identical to the compass x/y positions for
|
||||||
# Aggregate MP scores to party centroids per component
|
# components 1 and 2, and consistently uses the same aligned data for 3-10.
|
||||||
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 aligned PCA scores
|
|
||||||
party_1d_coords: dict = {}
|
party_1d_coords: dict = {}
|
||||||
aligned_all_scores = _get_aligned_party_scores(svd_window)
|
aligned_all_scores = _get_aligned_party_scores(svd_window)
|
||||||
for party, all_scores in aligned_all_scores.items():
|
for party, all_scores in aligned_all_scores.items():
|
||||||
@@ -2718,7 +2751,11 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
if view_mode == "Tijdtraject" and selected_parties_for_trajectory:
|
if view_mode == "Tijdtraject" and selected_parties_for_trajectory:
|
||||||
# Load party scores for all windows and render time trajectory
|
# Load party scores for all windows and render time trajectory
|
||||||
available_windows = get_uniform_dim_windows(db_path)
|
available_windows = get_uniform_dim_windows(db_path)
|
||||||
year_windows = sorted(w for w in available_windows if w != "current_parliament")
|
year_windows = sorted(
|
||||||
|
w
|
||||||
|
for w in available_windows
|
||||||
|
if w != "current_parliament" and w != _current_year
|
||||||
|
)
|
||||||
has_current = "current_parliament" in available_windows
|
has_current = "current_parliament" in available_windows
|
||||||
all_windows = year_windows + (["current_parliament"] if has_current else [])
|
all_windows = year_windows + (["current_parliament"] if has_current else [])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user