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>
101 lines
6.3 KiB
Markdown
101 lines
6.3 KiB
Markdown
---
|
|
name: preregister_hypothesis
|
|
kind: function
|
|
lang: py
|
|
domain: datascience
|
|
version: "1.0.0"
|
|
purity: impure
|
|
signature: "def preregister_hypothesis(paper_dir: str, hypotheses: dict, analysis_plan: dict) -> dict"
|
|
description: "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."
|
|
tags: [papers, preregistration, reproducibility, anti-harking, python]
|
|
params:
|
|
- name: paper_dir
|
|
desc: "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: hypotheses
|
|
desc: "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: analysis_plan
|
|
desc: "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.)."
|
|
output: "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]})."
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: "error_go_core"
|
|
imports: [hashlib]
|
|
tested: true
|
|
tests: ["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"]
|
|
test_file_path: "python/functions/datascience/preregister_hypothesis_test.py"
|
|
file_path: "python/functions/datascience/preregister_hypothesis.py"
|
|
---
|
|
|
|
## Ejemplo
|
|
|
|
```python
|
|
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).
|