"""Cluster bootstrap for accuracies and paired differences. Items nested in generating templates (clusters) are not independent; the bootstrap resamples clusters with replacement. Paired differences use identical items for both conditions. """ from __future__ import annotations from dataclasses import dataclass import numpy as np @dataclass class CI: estimate: float low: float high: float level: float n_items: int n_clusters: int n_boot: int def to_dict(self) -> dict: return self.__dict__.copy() def _cluster_index(clusters: list[str]) -> tuple[np.ndarray, int]: uniq = {c: i for i, c in enumerate(dict.fromkeys(clusters))} return np.array([uniq[c] for c in clusters]), len(uniq) def cluster_bootstrap_mean(values: np.ndarray, clusters: list[str], n_boot: int = 2000, level: float = 0.95, seed: int = 0) -> CI: values = np.asarray(values, dtype=float) cid, k = _cluster_index(clusters) rng = np.random.default_rng(seed) sums = np.bincount(cid, weights=values, minlength=k) counts = np.bincount(cid, minlength=k).astype(float) stats = np.empty(n_boot) for b in range(n_boot): draw = rng.integers(0, k, k) stats[b] = sums[draw].sum() / counts[draw].sum() a = (1 - level) / 2 return CI(float(values.mean()), float(np.quantile(stats, a)), float(np.quantile(stats, 1 - a)), level, len(values), k, n_boot) def 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: """CI for mean(a) - mean(b) on identical items (paired), resampling clusters.""" return cluster_bootstrap_mean(np.asarray(a, float) - np.asarray(b, float), clusters, n_boot, level, seed) def seeds_summary(values: list[float]) -> dict: v = np.asarray(values, float) return {"n_seeds": int(len(v)), "mean": float(v.mean()), "sd": float(v.std(ddof=1)) if len(v) > 1 else None, "min": float(v.min()), "max": float(v.max())}