Add compute_party_bootstrap_cis() to political_axis.py with tests

Pure numpy function that computes bootstrap confidence intervals for
party centroid vectors. Handles N>=2 (bootstrap), N=1 (degenerate CI),
and N=0 (excluded) cases. Uses np.random.default_rng for reproducibility.
This commit is contained in:
2026-03-29 23:13:30 +02:00
parent ef96edf478
commit cd8aeec997
2 changed files with 198 additions and 0 deletions
+77
View File
@@ -619,3 +619,80 @@ def compute_svd_spectrum(
sv2 = s**2
evr = sv2 / (sv2.sum() + 1e-20) * 100
return list(evr) # already sorted descending by SVD
def compute_party_bootstrap_cis(
party_vectors: Dict[str, List[np.ndarray]],
n_boot: int = 1000,
ci: float = 95.0,
seed: int = 42,
) -> Dict[str, Dict]:
"""Compute bootstrap confidence intervals for party centroid vectors.
For each party, resamples its MP vectors with replacement to build a
distribution of centroid estimates, then extracts percentile-based
confidence intervals per dimension.
Args:
party_vectors: mapping of party name → list of individual MP vectors
(each a numpy array of consistent length, e.g. 50 dimensions).
n_boot: number of bootstrap replicates.
ci: confidence level as a percentage (e.g. 95.0 for 95% CI).
seed: random seed for reproducibility (used with ``np.random.default_rng``).
Returns:
Dict mapping party name → dict with keys ``centroid``, ``ci_lower``,
``ci_upper``, ``std``, and ``n_mps``. Parties with no MPs (empty
list) are excluded from the output.
"""
alpha = 100.0 - ci
lo_pct = alpha / 2.0
hi_pct = 100.0 - lo_pct
result: Dict[str, Dict] = {}
for party, vectors in party_vectors.items():
n_mps = len(vectors)
if n_mps == 0:
continue
mat = np.vstack(vectors) # (n_mps, dim)
centroid = np.mean(mat, axis=0)
if n_mps == 1:
result[party] = {
"centroid": centroid,
"ci_lower": centroid.copy(),
"ci_upper": centroid.copy(),
"std": np.zeros_like(centroid),
"n_mps": 1,
}
continue
rng = np.random.default_rng(seed)
boot_centroids = np.empty((n_boot, mat.shape[1]))
for b in range(n_boot):
idx = rng.integers(0, n_mps, size=n_mps)
boot_centroids[b] = mat[idx].mean(axis=0)
ci_lower = np.percentile(boot_centroids, lo_pct, axis=0)
ci_upper = np.percentile(boot_centroids, hi_pct, axis=0)
std = np.std(boot_centroids, axis=0)
result[party] = {
"centroid": centroid,
"ci_lower": ci_lower,
"ci_upper": ci_upper,
"std": std,
"n_mps": n_mps,
}
_logger.info(
"Bootstrap CIs computed for %d parties (n_boot=%d, ci=%.1f%%)",
len(result),
n_boot,
ci,
)
return result