spb/wpai0001_uqo
Public
1"""Cluster bootstrap for accuracies and paired differences.23Items nested in generating templates (clusters) are not independent; the bootstrap resamples4clusters with replacement. Paired differences use identical items for both conditions.5"""6from __future__ import annotations78from dataclasses import dataclass910import numpy as np111213@dataclass14class CI:15 estimate: float16 low: float17 high: float18 level: float19 n_items: int20 n_clusters: int21 n_boot: int2223 def to_dict(self) -> dict:24 return self.__dict__.copy()252627def _cluster_index(clusters: list[str]) -> tuple[np.ndarray, int]:28 uniq = {c: i for i, c in enumerate(dict.fromkeys(clusters))}29 return np.array([uniq[c] for c in clusters]), len(uniq)303132def cluster_bootstrap_mean(values: np.ndarray, clusters: list[str], n_boot: int = 2000, level: float = 0.95, seed: int = 0) -> CI:33 values = np.asarray(values, dtype=float)34 cid, k = _cluster_index(clusters)35 rng = np.random.default_rng(seed)36 sums = np.bincount(cid, weights=values, minlength=k)37 counts = np.bincount(cid, minlength=k).astype(float)38 stats = np.empty(n_boot)39 for b in range(n_boot):40 draw = rng.integers(0, k, k)41 stats[b] = sums[draw].sum() / counts[draw].sum()42 a = (1 - level) / 243 return CI(float(values.mean()), float(np.quantile(stats, a)), float(np.quantile(stats, 1 - a)), level, len(values), k, n_boot)444546def cluster_bootstrap_paired_diff(a: np.ndarray, b: np.ndarray, clusters: list[str], n_boot: int = 2000, level: float = 0.95, seed: int = 0) -> CI:47 """CI for mean(a) - mean(b) on identical items (paired), resampling clusters."""48 return cluster_bootstrap_mean(np.asarray(a, float) - np.asarray(b, float), clusters, n_boot, level, seed)495051def seeds_summary(values: list[float]) -> dict:52 v = np.asarray(values, float)53 return {"n_seeds": int(len(v)), "mean": float(v.mean()), "sd": float(v.std(ddof=1)) if len(v) > 1 else None,54 "min": float(v.min()), "max": float(v.max())}55