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>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Tests para preregister_hypothesis (pre-registro inmutable, anti-HARKing).
|
|
|
|
Importa el modulo hoja directamente (`preregister_hypothesis`) para no depender
|
|
de que el paquete reexporte la funcion en su __init__ (lo integra el orquestador
|
|
al cerrar el grupo papers). El pytest del repo resuelve el modulo hoja por su
|
|
nombre directo.
|
|
|
|
Todos los tests son hermeticos y deterministas: usan el fixture `tmp_path` de
|
|
pytest; NUNCA escriben en `papers/`.
|
|
"""
|
|
|
|
from preregister_hypothesis import preregister_hypothesis
|
|
|
|
|
|
def _parse_frontmatter(text: str) -> dict:
|
|
parts = text.split("---", 2)
|
|
fm = {}
|
|
for line in parts[1].splitlines():
|
|
line = line.strip()
|
|
if not line or ":" not in line:
|
|
continue
|
|
key, _, value = line.partition(":")
|
|
fm[key.strip()] = value.strip()
|
|
return fm
|
|
|
|
|
|
HYP = {"h0": "no hay diferencia entre A y B", "h1": "el grupo A > grupo B"}
|
|
PLAN = {
|
|
"test": "welch_t_test",
|
|
"effect_size_metric": "cohens_d",
|
|
"decision_rule": "rechazar H0 si p<0.05 tras Holm y |d|>=0.5",
|
|
"planned_n": 100,
|
|
"multiple_correction": "holm",
|
|
}
|
|
|
|
|
|
def test_golden_congela_y_escribe_archivo(tmp_path):
|
|
paper = tmp_path / "0001-x"
|
|
paper.mkdir()
|
|
|
|
res = preregister_hypothesis(str(paper), HYP, PLAN)
|
|
|
|
assert res["status"] == "frozen"
|
|
pre = paper / "preregistration.md"
|
|
assert pre.exists()
|
|
|
|
text = pre.read_text(encoding="utf-8")
|
|
fm = _parse_frontmatter(text)
|
|
assert fm["status"] == "frozen"
|
|
assert fm["paper_slug"] == "0001-x"
|
|
assert fm["content_hash"] # no vacio
|
|
assert fm["frozen_at"] # no vacio
|
|
assert res["content_hash"] == fm["content_hash"]
|
|
assert res["frozen_at"] == fm["frozen_at"]
|
|
|
|
|
|
def test_idempotente_mismo_input_no_reescribe(tmp_path):
|
|
paper = tmp_path / "0001-x"
|
|
paper.mkdir()
|
|
pre = paper / "preregistration.md"
|
|
|
|
first = preregister_hypothesis(str(paper), HYP, PLAN)
|
|
assert first["status"] == "frozen"
|
|
bytes_before = pre.read_bytes()
|
|
|
|
second = preregister_hypothesis(str(paper), HYP, PLAN)
|
|
assert second["status"] == "unchanged"
|
|
# Mismo hash y frozen_at original preservado.
|
|
assert second["content_hash"] == first["content_hash"]
|
|
assert second["frozen_at"] == first["frozen_at"]
|
|
# El archivo NO cambio byte a byte (incl. frozen_at).
|
|
assert pre.read_bytes() == bytes_before
|
|
|
|
|
|
def test_inmutabilidad_anti_harking_rechaza_contenido_distinto(tmp_path):
|
|
paper = tmp_path / "0001-x"
|
|
paper.mkdir()
|
|
pre = paper / "preregistration.md"
|
|
|
|
preregister_hypothesis(str(paper), HYP, PLAN)
|
|
bytes_frozen = pre.read_bytes()
|
|
|
|
# Intento de re-congelar con una hipotesis DISTINTA (HARKing) -> rechazado.
|
|
hyp_tramposo = {"h0": "no hay diferencia", "h1": "el grupo B > grupo A (cambiado tras ver datos)"}
|
|
res = preregister_hypothesis(str(paper), hyp_tramposo, PLAN)
|
|
|
|
assert res["status"] == "error"
|
|
# Asercion mas importante: el archivo en disco SIGUE siendo el original.
|
|
assert pre.read_bytes() == bytes_frozen
|
|
|
|
|
|
def test_error_paper_dir_inexistente_no_crash_no_crea(tmp_path):
|
|
missing = tmp_path / "no-existe"
|
|
res = preregister_hypothesis(str(missing), HYP, PLAN)
|
|
|
|
assert res["status"] == "error"
|
|
# No se creo el directorio ni el archivo.
|
|
assert not missing.exists()
|
|
assert not (missing / "preregistration.md").exists()
|