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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user