Upload significance_test.py with huggingface_hub
Browse files- significance_test.py +150 -0
significance_test.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Significance testing for Hadith vs Dense NDCG@10 differences.
|
| 3 |
+
Reads query_deltas.csv, runs paired t-test and randomization test."""
|
| 4 |
+
import csv, math, random, sys
|
| 5 |
+
import numpy as np
|
| 6 |
+
from scipy import stats as scipy_stats
|
| 7 |
+
|
| 8 |
+
CSV_PATH = "/workspace/rankings/query_deltas.csv" if len(sys.argv) < 2 else sys.argv[1]
|
| 9 |
+
|
| 10 |
+
def paired_t_test(a, b):
|
| 11 |
+
"""Paired two-sided t-test. Returns t-statistic, p-value."""
|
| 12 |
+
diffs = np.array(a) - np.array(b)
|
| 13 |
+
n = len(diffs)
|
| 14 |
+
mean_d = np.mean(diffs)
|
| 15 |
+
std_d = np.std(diffs, ddof=1)
|
| 16 |
+
t_stat = mean_d / (std_d / math.sqrt(n)) if std_d > 0 else 0.0
|
| 17 |
+
# approximate p-value from t-distribution
|
| 18 |
+
from scipy.stats import t
|
| 19 |
+
p = 2 * t.sf(abs(t_stat), df=n-1)
|
| 20 |
+
return t_stat, p
|
| 21 |
+
|
| 22 |
+
def randomization_test(a, b, n_permutations=100_000):
|
| 23 |
+
"""Paired randomization test. Returns p-value (fraction of permutations with |delta| >= observed)."""
|
| 24 |
+
diffs = np.array(a) - np.array(b)
|
| 25 |
+
observed = np.abs(np.mean(diffs))
|
| 26 |
+
n = len(diffs)
|
| 27 |
+
count_extreme = 0
|
| 28 |
+
for _ in range(n_permutations):
|
| 29 |
+
# Randomly flip each pair's sign
|
| 30 |
+
signs = np.random.choice([-1, 1], size=n)
|
| 31 |
+
perm_mean = np.abs(np.mean(diffs * signs))
|
| 32 |
+
if perm_mean >= observed:
|
| 33 |
+
count_extreme += 1
|
| 34 |
+
return (count_extreme + 1) / (n_permutations + 1) # +1 for continuity
|
| 35 |
+
|
| 36 |
+
def cohens_d(a, b):
|
| 37 |
+
"""Cohen's d effect size."""
|
| 38 |
+
diffs = np.array(a) - np.array(b)
|
| 39 |
+
mean_d = np.mean(diffs)
|
| 40 |
+
std_d = np.std(diffs, ddof=1)
|
| 41 |
+
return mean_d / std_d if std_d > 0 else 0.0
|
| 42 |
+
|
| 43 |
+
# ββ Load ββ
|
| 44 |
+
print(f"Reading {CSV_PATH} ...")
|
| 45 |
+
rows = []
|
| 46 |
+
with open(CSV_PATH) as f:
|
| 47 |
+
reader = csv.DictReader(f)
|
| 48 |
+
for row in reader:
|
| 49 |
+
rows.append(row)
|
| 50 |
+
|
| 51 |
+
print(f"Loaded {len(rows)} queries\n")
|
| 52 |
+
|
| 53 |
+
# Extract named variants
|
| 54 |
+
variants = ["dense+T", "dense+S", "dense+M", "dense+TS", "dense+MT", "dense+MS", "dense+MTS"]
|
| 55 |
+
dense_col = "dense"
|
| 56 |
+
|
| 57 |
+
dense_ndcg = np.array([float(r[dense_col]) for r in rows])
|
| 58 |
+
|
| 59 |
+
print("=" * 80)
|
| 60 |
+
print("SIGNIFICANCE TEST REPORT β Hadith vs Dense (paired test)")
|
| 61 |
+
print("=" * 80)
|
| 62 |
+
print(f"{'Variant':<12} {'Mean Ξ':>8} {'Std Ξ':>8} {'t':>8} {'p(t-test)':>10} {'p(random)':>10} {'Cohen d':>8} {'Signif':>8}")
|
| 63 |
+
print("-" * 80)
|
| 64 |
+
|
| 65 |
+
results = []
|
| 66 |
+
for v in variants:
|
| 67 |
+
v_ndcg = np.array([float(r[v]) for r in rows])
|
| 68 |
+
deltas = v_ndcg - dense_ndcg
|
| 69 |
+
|
| 70 |
+
mean_d = np.mean(deltas)
|
| 71 |
+
std_d = np.std(deltas, ddof=1)
|
| 72 |
+
|
| 73 |
+
# Paired t-test
|
| 74 |
+
t_stat, p_ttest = paired_t_test(v_ndcg, dense_ndcg)
|
| 75 |
+
|
| 76 |
+
# Randomization test (10K permutations for speed; increase for final)
|
| 77 |
+
p_rand = randomization_test(v_ndcg, dense_ndcg, n_permutations=10_000)
|
| 78 |
+
|
| 79 |
+
# Cohen's d
|
| 80 |
+
d_cohen = cohens_d(v_ndcg, dense_ndcg)
|
| 81 |
+
|
| 82 |
+
# Significance label
|
| 83 |
+
if p_ttest < 0.001:
|
| 84 |
+
sig = "***"
|
| 85 |
+
elif p_ttest < 0.01:
|
| 86 |
+
sig = "**"
|
| 87 |
+
elif p_ttest < 0.05:
|
| 88 |
+
sig = "*"
|
| 89 |
+
else:
|
| 90 |
+
sig = "n.s."
|
| 91 |
+
|
| 92 |
+
results.append((v, mean_d, std_d, t_stat, p_ttest, p_rand, d_cohen, sig))
|
| 93 |
+
print(f"{v:<12} {mean_d:>+8.6f} {std_d:>8.6f} {t_stat:>+8.3f} {p_ttest:>10.6f} {p_rand:>10.6f} {d_cohen:>+8.4f} {sig:>8}")
|
| 94 |
+
|
| 95 |
+
print("-" * 80)
|
| 96 |
+
print("\nSignificance codes: *** p<0.001 ** p<0.01 * p<0.05 n.s. = not significant")
|
| 97 |
+
print()
|
| 98 |
+
|
| 99 |
+
# ββ Summary statistics ββ
|
| 100 |
+
print("\n" + "=" * 80)
|
| 101 |
+
print("SUMMARY STATISTICS β Per-Query Deltas")
|
| 102 |
+
print("=" * 80)
|
| 103 |
+
print(f"{'Variant':<12} {'Mean':>8} {'Median':>8} {'Std':>8} {'Min':>8} {'Max':>8} {'Q1':>8} {'Q3':>8} {'%Neg':>8} {'%Zero':>8}")
|
| 104 |
+
print("-" * 80)
|
| 105 |
+
|
| 106 |
+
for v, md, std_d, _, _, _, _, _ in results:
|
| 107 |
+
deltas = np.array([float(r[v]) for r in rows]) - dense_ndcg
|
| 108 |
+
median = np.median(deltas)
|
| 109 |
+
mn = np.min(deltas)
|
| 110 |
+
mx = np.max(deltas)
|
| 111 |
+
q1 = np.percentile(deltas, 25)
|
| 112 |
+
q3 = np.percentile(deltas, 75)
|
| 113 |
+
pct_neg = 100 * np.mean(deltas < 0)
|
| 114 |
+
pct_zero = 100 * np.mean(deltas == 0)
|
| 115 |
+
print(f"{v:<12} {md:>+8.6f} {median:>+8.6f} {std_d:>8.6f} {mn:>+8.4f} {mx:>+8.4f} {q1:>+8.6f} {q3:>+8.6f} {pct_neg:>7.1f}% {pct_zero:>7.1f}%")
|
| 116 |
+
|
| 117 |
+
print("-" * 80)
|
| 118 |
+
|
| 119 |
+
# ββ Best sweep ββ
|
| 120 |
+
print("\n" + "=" * 80)
|
| 121 |
+
print("BEST SWEEP β Tawatur threshold/tau sweep vs Dense")
|
| 122 |
+
print("=" * 80)
|
| 123 |
+
# Find best sweep variant
|
| 124 |
+
sweep_keys = [k for k in rows[0].keys() if k.startswith("tawatur_ct=")]
|
| 125 |
+
sweep_results = []
|
| 126 |
+
for k in sweep_keys:
|
| 127 |
+
v = np.array([float(r[k]) for r in rows])
|
| 128 |
+
deltas = v - dense_ndcg
|
| 129 |
+
mean_d = np.mean(deltas)
|
| 130 |
+
sweep_results.append((k, mean_d, np.std(deltas, ddof=1)))
|
| 131 |
+
|
| 132 |
+
sweep_results.sort(key=lambda x: -x[1])
|
| 133 |
+
print(f"{'Sweep':<30} {'Mean Ξ':>10} {'Std Ξ':>10}")
|
| 134 |
+
print("-" * 50)
|
| 135 |
+
for k, md, sd in sweep_results[:5]:
|
| 136 |
+
print(f"{k:<30} {md:>+10.6f} {sd:>10.6f}")
|
| 137 |
+
print(f"{'...':<30}")
|
| 138 |
+
for k, md, sd in sweep_results[-3:]:
|
| 139 |
+
print(f"{k:<30} {md:>+10.6f} {sd:>10.6f}")
|
| 140 |
+
|
| 141 |
+
# ββ N queries helped/hurt ββ
|
| 142 |
+
print("\n" + "=" * 80)
|
| 143 |
+
print("HELPED vs HURT β Query counts")
|
| 144 |
+
print("=" * 80)
|
| 145 |
+
for v in variants:
|
| 146 |
+
deltas = np.array([float(r[v]) for r in rows]) - dense_ndcg
|
| 147 |
+
helped = np.sum(deltas > 0.001) # >0.001 to avoid floating-point noise
|
| 148 |
+
hurt = np.sum(deltas < -0.001)
|
| 149 |
+
neutral = len(deltas) - helped - hurt
|
| 150 |
+
print(f"{v:<12}: helped={helped:>4} ({100*helped/len(deltas):>5.1f}%) hurt={hurt:>4} ({100*hurt/len(deltas):>5.1f}%) neutral={neutral:>4} ({100*neutral/len(deltas):>5.1f}%)")
|