File size: 3,475 Bytes
9f9fbec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b784950
9f9fbec
b784950
 
 
 
 
 
 
9f9fbec
 
 
 
 
 
 
 
 
b784950
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f9fbec
 
 
 
 
 
b784950
 
9f9fbec
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from __future__ import annotations

import numpy as np


def bootstrap_mean_ci(
    values: np.ndarray | list[float],
    *,
    confidence: float = 0.95,
    n_resamples: int = 5000,
    seed: int = 42,
) -> tuple[float, float]:
    """Percentile bootstrap CI for a sample mean."""
    x = np.asarray(values, dtype=float)
    x = x[np.isfinite(x)]
    if x.size == 0:
        return float('nan'), float('nan')
    if x.size == 1:
        value = float(x[0])
        return value, value
    rng = np.random.default_rng(seed)
    sample_idx = rng.integers(0, x.size, size=(int(n_resamples), x.size))
    means = x[sample_idx].mean(axis=1)
    alpha = (1.0 - float(confidence)) / 2.0
    low, high = np.quantile(means, [alpha, 1.0 - alpha])
    return float(low), float(high)


def paired_bootstrap_difference_ci(
    a: np.ndarray | list[float],
    b: np.ndarray | list[float],
    *,
    confidence: float = 0.95,
    n_resamples: int = 5000,
    seed: int = 42,
) -> tuple[float, float]:
    """Bootstrap CI for mean(a - b), preserving paired rows."""
    x = np.asarray(a, dtype=float)
    y = np.asarray(b, dtype=float)
    mask = np.isfinite(x) & np.isfinite(y)
    x = x[mask]
    y = y[mask]
    if x.size == 0:
        return float('nan'), float('nan')
    diff = x - y
    return bootstrap_mean_ci(
        diff,
        confidence=confidence,
        n_resamples=n_resamples,
        seed=seed,
    )


def paired_sign_flip_pvalue(
    a: np.ndarray | list[float],
    b: np.ndarray | list[float],
    *,
    n_permutations: int = 20000,
    seed: int = 42,
    exact_max_nonzero_pairs: int = 18,
) -> float:
    """Two-sided paired sign-flip test for a non-zero mean difference.

    For small effective samples the test is enumerated exactly. Zero-difference
    pairs are removed before deciding whether exact enumeration is feasible;
    their sign cannot change the statistic. Larger samples use a deterministic
    Monte-Carlo approximation.
    """
    x = np.asarray(a, dtype=float)
    y = np.asarray(b, dtype=float)
    mask = np.isfinite(x) & np.isfinite(y)
    diff = (x - y)[mask]
    if diff.size == 0:
        return float('nan')
    observed = abs(float(diff.mean()))
    if observed == 0.0:
        return 1.0

    nonzero = diff[np.abs(diff) > 0.0]
    if nonzero.size == 0:
        return 1.0

    # Preserve the original mean denominator: zero-difference pairs contribute
    # to n but never to a sign-flipped numerator.
    denominator = float(diff.size)
    if nonzero.size <= int(exact_max_nonzero_pairs):
        count = 1 << int(nonzero.size)
        extreme = 0
        for bits in range(count):
            signs = np.fromiter(
                (1.0 if bits & (1 << idx) else -1.0 for idx in range(nonzero.size)),
                dtype=float,
                count=nonzero.size,
            )
            statistic = abs(float(np.sum(signs * nonzero) / denominator))
            if statistic >= observed - 1e-15:
                extreme += 1
        return float(extreme / count)

    rng = np.random.default_rng(seed)
    extreme = 0
    batch = 2000
    remaining = int(n_permutations)
    while remaining > 0:
        n = min(batch, remaining)
        signs = rng.choice(np.array([-1.0, 1.0]), size=(n, nonzero.size))
        permuted = np.abs((signs * nonzero).sum(axis=1) / denominator)
        extreme += int(np.count_nonzero(permuted >= observed))
        remaining -= n
    return float((extreme + 1) / (int(n_permutations) + 1))