Files
fn_registry/python/functions/datascience/preregister_hypothesis.md
T
egutierrez 4f1530797e feat(datascience): rigor experimental para papers — effect size, IC, Holm + preregistro inmutable
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>
2026-06-30 20:42:12 +02:00

6.3 KiB

name, kind, lang, domain, version, purity, signature, description, tags, params, output, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags params output uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path
preregister_hypothesis function py datascience 1.0.0 impure def preregister_hypothesis(paper_dir: str, hypotheses: dict, analysis_plan: dict) -> dict Pre-registra (congela) la hipotesis y el plan de analisis de un paper ANTES de mirar los datos: antidoto al HARKing (Hypothesizing After the Results are Known). Escribe/actualiza <paper_dir>/preregistration.md con un frontmatter (paper_slug, frozen_at, content_hash, status) y un cuerpo markdown DETERMINISTA derivado de (hypotheses, analysis_plan) (mismo input -> mismo cuerpo byte a byte, claves ordenadas alfabeticamente). El content_hash es sha256 del cuerpo NORMALIZADO (strip por linea + colapso de blancos), nunca del frontmatter. Una vez status=frozen es INMUTABLE: re-congelar con el mismo contenido es idempotente (no reescribe, devuelve unchanged) y re-congelar con contenido distinto se RECHAZA (no sobrescribe, devuelve error) para que no se pueda ajustar la hipotesis a los resultados. Estilo dict-no-throw: nunca lanza.
papers
preregistration
reproducibility
anti-harking
python
name desc
paper_dir ruta del directorio del paper, p.ej. 'papers/0001-mi-paper'. Debe existir (no se crea aqui). El paper_slug del frontmatter es el basename del dir. Si no existe o no es str -> {status:error, path, note} sin crash ni creacion.
name desc
hypotheses dict de hipotesis, p.ej. {'h0': 'no hay diferencia ...', 'h1': 'el grupo A > grupo B ...'}. Se renderiza en la seccion '## Hypotheses' con una linea por clave, ordenadas alfabeticamente para determinismo.
name desc
analysis_plan dict con el plan de analisis, p.ej. {'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'}. Se renderiza en '## Analysis plan' con una linea por clave (ordenadas alfabeticamente). Acepta valores no-str (int, etc.).
dict dict-no-throw (NUNCA lanza). status='frozen' cuando escribe el archivo por primera vez o congela un draft previo ({status, path, content_hash, frozen_at}). status='unchanged' cuando ya estaba frozen con el mismo content_hash: no reescribe y preserva el archivo byte-identico incl. el frozen_at original ({status, path, content_hash, frozen_at}). status='error' cuando paper_dir no existe, ya esta frozen con un hash distinto (rechazo anti-HARKing, no sobrescribe), inputs invalidos o error de I/O ({status, path, note, [content_hash]}).
false error_go_core
hashlib
true
test_golden_congela_y_escribe_archivo
test_idempotente_mismo_input_no_reescribe
test_inmutabilidad_anti_harking_rechaza_contenido_distinto
test_error_paper_dir_inexistente_no_crash_no_crea
python/functions/datascience/preregister_hypothesis_test.py python/functions/datascience/preregister_hypothesis.py

Ejemplo

import os, tempfile
from datascience import preregister_hypothesis

# Un directorio de paper que ya existe.
paper_dir = tempfile.mkdtemp(prefix="0001-")

hypotheses = {
    "h0": "no hay diferencia entre el grupo A y el grupo B",
    "h1": "el grupo A tiene mayor conversion que el grupo B",
}
analysis_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",
}

# 1) Primera vez: congela y escribe <paper_dir>/preregistration.md
r1 = preregister_hypothesis(paper_dir, hypotheses, analysis_plan)
print(r1["status"])        # -> "frozen"
print(r1["content_hash"])  # sha256 del cuerpo

# 2) Mismo input: idempotente, no reescribe.
r2 = preregister_hypothesis(paper_dir, hypotheses, analysis_plan)
print(r2["status"])        # -> "unchanged"

# 3) Cambiar la hipotesis tras congelar (HARKing): rechazado, archivo intacto.
r3 = preregister_hypothesis(paper_dir, {"h0": "...", "h1": "otra cosa"}, analysis_plan)
print(r3["status"])        # -> "error"

Cuando usarla

Llamala al ARRANCAR el analisis de un paper, antes de tocar los datos, para dejar por escrito (y firmado por hash) que vas a probar y como vas a decidir. Es el primer paso de un flujo reproducible: pre-registras la hipotesis y el plan (test, effect_size_metric, decision_rule, planned_n, multiple_correction), y solo despues corres el analisis y comparas con lo pre-registrado. Si mas tarde el analisis "descubre" otra hipotesis que encaja mejor con los datos, el pre-registro congelado deja en evidencia el cambio: no se puede reescribir. Combinala con effect_size_cohens_d y fdr_correction para cerrar el plan declarado (effect size + correccion de multiples comparaciones).

Gotchas

  • Inmutabilidad (el corazon): una vez status: frozen, el pre-registro NO se puede editar. Re-congelar con el MISMO contenido es idempotente (unchanged, no reescribe, preserva incluso el frozen_at original). Re-congelar con contenido DISTINTO devuelve error y deja el archivo intacto: asi se mata el HARKing. Para cambiar de verdad la hipotesis hay que borrar el archivo a mano y asumir explicitamente que ya no es un pre-registro valido.
  • dict-no-throw: la funcion NUNCA lanza. Cualquier error previsible (directorio inexistente, inputs no-dict, fallo de I/O, excepcion inesperada) se captura y se devuelve como {"status": "error", "note": ...}. Siempre incluye path (la ruta esperada del preregistration.md).
  • El hash es SOLO del cuerpo, nunca del frontmatter: el frontmatter contiene el propio content_hash y el frozen_at (timestamp), asi que incluirlos en el hash seria circular y romperia la idempotencia. El cuerpo se normaliza antes de hashear (strip por linea + colapso de lineas en blanco + strip final): cambios irrelevantes de whitespace no alteran el hash, pero cambios de contenido SI.
  • Determinismo: el cuerpo se genera con las claves de hypotheses y analysis_plan ordenadas alfabeticamente, de modo que el orden de insercion del dict no afecta al hash. Mismo (hypotheses, analysis_plan) -> mismo cuerpo y mismo hash, byte a byte.
  • No crea el directorio del paper: si paper_dir no existe, devuelve error sin crear nada (ni el dir ni el archivo).