4f1530797e
Subsistema de papers reproducibles (grupo de capacidad `papers`). Añade las funciones estadísticas que un paper honesto necesita y la función que congela la hipótesis antes de mirar los datos (anti-HARKing). Nuevas funciones (puras salvo la última): - effect_size_cohens_d: Cohen's d + Hedges' g (corrección de sesgo para N pequeño) + interpretación cualitativa (negligible/small/medium/large por los umbrales de Cohen). Dict-no-throw ante varianza cero / N insuficiente. - confidence_interval_mean: intervalo de confianza de una media (t de Student) o de la diferencia de medias con Welch (df de Welch–Satterthwaite, sin asumir varianzas iguales). Dict-no-throw; el IC colapsa al punto cuando la varianza es cero. - preregister_hypothesis (impura): congela hipótesis + plan de análisis en papers/<slug>/preregistration.md con frozen_at (UTC) y content_hash (sha256 del cuerpo normalizado, no del frontmatter). Inmutabilidad: una vez frozen, un contenido distinto se RECHAZA sin sobrescribir (mata el HARKing); idempotente si el contenido es idéntico. Siempre dict-no-throw. Extensión: - fdr_correction 1.0.0 -> 1.1.0: añade method="holm" (Holm-Bonferroni step-down, controla FWER, más potente que Bonferroni simple). Reúsa la maquinaria de alineación 1:1 con None/inválidos; no rompe los métodos bh/bonferroni. Reutiliza del registry: fdr_correction (BH + Bonferroni ya existían) como base para Holm. pearson y spearman_corr ya cubrían correlación. Tests: 36 pytest verdes (cohen/hedges 8, confidence/welch 8, fdr/holm/bonferroni 12, preregister 4 + extras), golden contra valores conocidos y validados con scipy. Golden manual del preregistro: congela, idempotente, rechaza edición (bytes en disco idénticos al congelado). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
"""Tests para confidence_interval_mean (IC de la media / diferencia de medias Welch).
|
|
|
|
Importa el modulo hoja directamente (`confidence_interval_mean`) para no depender
|
|
de que el paquete reexporte la funcion en su __init__ (lo integra el orquestador
|
|
al cerrar el grupo).
|
|
|
|
Los golden se calculan con scipy dentro del propio test para que sean robustos:
|
|
la funcion bajo prueba debe coincidir con la referencia de scipy a ~1e-9.
|
|
"""
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
from scipy import stats
|
|
|
|
from confidence_interval_mean import confidence_interval_mean
|
|
|
|
|
|
def test_one_sample_golden_contra_scipy():
|
|
# mean=5.0, n=8. Este dataset tiene sd POBLACIONAL (ddof=0) exactamente 2.0,
|
|
# pero la sd MUESTRAL (ddof=1, la que exige la spec y la que es correcta para
|
|
# el IC de una media con la t) es sqrt(32/7) ~ 2.13809. El golden robusto se
|
|
# calcula con scipy usando se con ddof=1, no con el atajo 2.0/sqrt(8).
|
|
data = [2, 4, 4, 4, 5, 5, 7, 9]
|
|
out = confidence_interval_mean(data, confidence=0.95)
|
|
|
|
n = len(data)
|
|
mean = float(np.mean(data))
|
|
sd = float(np.std(data, ddof=1)) # sample sd ~ 2.13809
|
|
se = sd / math.sqrt(n)
|
|
lo, hi = stats.t.interval(0.95, df=n - 1, loc=mean, scale=se)
|
|
|
|
assert abs(out["mean"] - 5.0) < 1e-9
|
|
assert abs(out["se"] - se) < 1e-12
|
|
assert out["df"] == 7.0
|
|
assert out["n"] == 8
|
|
assert out["confidence"] == 0.95
|
|
assert abs(out["ci_low"] - lo) < 1e-9
|
|
assert abs(out["ci_high"] - hi) < 1e-9
|
|
# Valores tabulados correctos para ddof=1 (no los 3.32793/6.67207 del
|
|
# enunciado, que asumian erroneamente sd=2.0 / ddof=0).
|
|
assert abs(out["ci_low"] - 3.21251) < 1e-3
|
|
assert abs(out["ci_high"] - 6.78749) < 1e-3
|
|
assert "note" not in out
|
|
|
|
|
|
def test_one_sample_distinto_nivel_confianza():
|
|
data = [10.0, 12.0, 11.0, 13.0, 9.0, 14.0]
|
|
out = confidence_interval_mean(data, confidence=0.99)
|
|
|
|
n = len(data)
|
|
mean = float(np.mean(data))
|
|
se = float(np.std(data, ddof=1)) / math.sqrt(n)
|
|
lo, hi = stats.t.interval(0.99, df=n - 1, loc=mean, scale=se)
|
|
|
|
assert abs(out["mean"] - mean) < 1e-12
|
|
assert abs(out["ci_low"] - lo) < 1e-9
|
|
assert abs(out["ci_high"] - hi) < 1e-9
|
|
assert out["df"] == float(n - 1)
|
|
|
|
|
|
def test_welch_diferencia_golden_contra_scipy():
|
|
data = [23.0, 21.0, 25.0, 22.0, 24.0, 26.0]
|
|
other = [18.0, 20.0, 17.0, 19.0, 21.0]
|
|
conf = 0.95
|
|
out = confidence_interval_mean(data, other, confidence=conf)
|
|
|
|
a = np.asarray(data, dtype=float)
|
|
b = np.asarray(other, dtype=float)
|
|
n1, n2 = a.size, b.size
|
|
mean1, mean2 = float(a.mean()), float(b.mean())
|
|
diff = mean1 - mean2
|
|
se1 = float(a.std(ddof=1)) / math.sqrt(n1)
|
|
se2 = float(b.std(ddof=1)) / math.sqrt(n2)
|
|
se = math.sqrt(se1**2 + se2**2)
|
|
df = (se1**2 + se2**2) ** 2 / (se1**4 / (n1 - 1) + se2**4 / (n2 - 1))
|
|
lo, hi = stats.t.interval(conf, df=df, loc=diff, scale=se)
|
|
|
|
assert abs(out["mean"] - diff) < 1e-9
|
|
assert abs(out["mean"] - (mean1 - mean2)) < 1e-9
|
|
assert abs(out["se"] - se) < 1e-12
|
|
assert abs(out["df"] - df) < 1e-9
|
|
assert abs(out["ci_low"] - lo) < 1e-9
|
|
assert abs(out["ci_high"] - hi) < 1e-9
|
|
assert out["n1"] == n1
|
|
assert out["n2"] == n2
|
|
assert out["n"] == n1 + n2
|
|
assert "note" not in out
|
|
|
|
|
|
def test_edge_un_solo_elemento_no_lanza_nan_note():
|
|
out = confidence_interval_mean([5], confidence=0.95)
|
|
assert out["mean"] == 5.0 # la media si esta definida con n=1
|
|
assert math.isnan(out["se"])
|
|
assert math.isnan(out["ci_low"])
|
|
assert math.isnan(out["ci_high"])
|
|
assert math.isnan(out["df"])
|
|
assert out["n"] == 1
|
|
assert "note" in out
|
|
|
|
|
|
def test_edge_lista_vacia_no_lanza_note():
|
|
out = confidence_interval_mean([], confidence=0.95)
|
|
assert math.isnan(out["mean"])
|
|
assert math.isnan(out["ci_low"])
|
|
assert math.isnan(out["ci_high"])
|
|
assert math.isnan(out["se"])
|
|
assert out["n"] == 0
|
|
assert "note" in out
|
|
|
|
|
|
def test_edge_varianza_cero_colapsa_al_punto():
|
|
out = confidence_interval_mean([3, 3, 3], confidence=0.95)
|
|
assert out["mean"] == 3.0
|
|
assert out["se"] == 0.0
|
|
assert out["ci_low"] == 3.0
|
|
assert out["ci_high"] == 3.0
|
|
assert not math.isnan(out["ci_low"])
|
|
assert out["n"] == 3
|
|
assert "note" in out
|
|
|
|
|
|
def test_edge_welch_muestra_vacia_no_lanza_note():
|
|
out = confidence_interval_mean([1.0, 2.0, 3.0], [], confidence=0.95)
|
|
assert math.isnan(out["mean"])
|
|
assert math.isnan(out["ci_low"])
|
|
assert math.isnan(out["se"])
|
|
assert out["n1"] == 3
|
|
assert out["n2"] == 0
|
|
assert "note" in out
|
|
|
|
|
|
def test_edge_welch_n1_uno_no_lanza_note():
|
|
out = confidence_interval_mean([5.0], [1.0, 2.0, 3.0], confidence=0.95)
|
|
# La diferencia de medias si esta definida.
|
|
assert abs(out["mean"] - (5.0 - 2.0)) < 1e-9
|
|
assert math.isnan(out["se"])
|
|
assert math.isnan(out["ci_low"])
|
|
assert math.isnan(out["df"])
|
|
assert "note" in out
|