3. Un conjunto de datos auditable de principio a fin
Nivel 6: De los datos a la decisión
Un conjunto de datos auditable es aquel en el que cualquier número de cualquier celda puede rastrearse hasta su origen y hasta la lista exacta de operaciones que lo produjeron. Suena a burocracia hasta el día en que un resultado cambia y no tienes forma de saber si cambió el mundo, el proveedor o tú.
Linaje: la biografía de cada número
El linaje del dato es simplemente eso: la biografía completa de un valor. Sin jerga: si un número de tu tabla no puede contar su vida, no es un dato, es un rumor con formato numérico. Y contar su vida significa cinco cosas concretas:
- La fuente exacta, no el nombre del proveedor: el endpoint, el fichero o la presentación regulatoria de la que salió.
- El momento de la descarga, porque la misma consulta hecha dos meses después puede devolver otra cifra.
- El fichero bruto que lo contenía, guardado tal cual llegó y con su huella criptográfica.
- Las transformaciones, en orden, desde ese bruto hasta la columna final que usa la estrategia.
- Las decisiones humanas que se aplicaron por el camino, con su criterio y su alternativa descartada.
Hay tres preguntas que ese registro tiene que poder responder sin que nadie recuerde nada. ¿De dónde salió? ¿Qué se le hizo? ¿Qué se decidió sobre él cuando resultó dudoso? Si las tres tienen respuesta escrita, el conjunto es auditable; si alguna depende de tu memoria, no lo es.
El manifiesto que viaja con la tabla
Cada versión del conjunto se acompaña de un fichero de metadatos generado por el mismo proceso que la produce. Nunca escrito a mano después.
import hashlib, json, os, subprocess
import datetime as dt
CLAVE = os.environ.get("CLAVE_PROVEEDOR", "tu_clave_aqui") # nunca en el manifiesto
def huella(ruta):
h = hashlib.sha256()
with open(ruta, "rb") as f:
for bloque in iter(lambda: f.read(1 << 20), b""):
h.update(bloque)
return h.hexdigest()
manifiesto = {
"conjunto": "panel_sp500_diario",
"generado_utc": dt.datetime.utcnow().isoformat(timespec="seconds"),
"commit": subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip(),
"fuentes": [
{"campo": "cierre_ajustado", "origen": "proveedor A, endpoint /eod",
"descargado": "2026-08-03", "fichero": "bruto/eod_2026-08-03.csv.gz",
"sha256": huella("bruto/eod_2026-08-03.csv.gz")},
{"campo": "bpa", "origen": "SEC EDGAR, companyfacts",
"descargado": "2026-08-03", "fichero": "bruto/companyfacts_2026-08-03.zip",
"sha256": huella("bruto/companyfacts_2026-08-03.zip")},
],
"transformaciones": [
"1. recortar al calendario oficial de sesiones del mercado",
"2. aplicar factores de splits y dividendos sobre el precio bruto",
"3. unir fundamentales por fecha_publicacion con merge_asof backward",
"4. anular precios con volumen cero",
],
"decisiones": {
"precios_volumen_cero": "se anulan, no se interpolan; 412 filas",
"empresas_sin_identificador": "se excluyen y se listan aparte; 7 simbolos",
},
}
json.dump(manifiesto, open("panel_sp500_diario.manifiesto.json", "w"), indent=2)La huella criptográfica es la pieza que casi todo el mundo se salta y la única que distingue dos escenarios que se parecen mucho: que tu código haya cambiado o que el proveedor haya reescrito su histórico sin decírtelo. El número de filas no sirve para eso, porque revisar valores no cambia el conteo.
Las transformaciones son una lista ordenada, no un montón
El orden importa y hay que poder recitarlo. Anular precios imposibles antes o después de calcular los factores de ajuste da resultados distintos; filtrar por capitalización antes o después de aplicar la unión con los fundamentales cambia el universo. La regla práctica: el fichero bruto no se toca jamás, se guarda tal cual llegó y todo lo demás se reconstruye desde él con un script que se ejecuta entero de principio a fin. Si tu conjunto solo existe porque un día ejecutaste tres celdas de un cuaderno en cierto orden, no es reproducible.
Las decisiones dudosas se escriben en el momento
Habrá casos que ninguna regla cubra: una empresa que cambia de ejercicio fiscal, un precio que se dispara y podría ser real, una fusión mal reflejada. Cada vez que decidas algo, escribe en el mismo sitio qué criterio aplicaste, sobre qué filas, y qué alternativa descartaste y por qué. Dentro de un año esa nota vale más que el código, porque el código dice qué hiciste y la nota dice por qué.
Y hay un detalle de orden que separa el trabajo serio del resto: esas decisiones se toman y se escriben antes de mirar ningún resultado. En cuanto sabes que anular 412 filas mejora tu rentabilidad y conservarlas la empeora, has dejado de decidir sobre los datos y has empezado a elegir la respuesta que te gusta. El registro de decisiones es, en el fondo, una defensa contra ti mismo, y por eso su valor depende por completo del momento en que se escribe.
Un informe que se genera solo
Y por último, el conjunto tiene que pasar un examen fijo cada vez que se reconstruye.
def informe_auditoria(df, clave=("simbolo", "fecha"), col_precio="cierre_ajustado"):
fallos, notas = [], []
notas.append(f"filas {len(df)} simbolos {df['simbolo'].nunique()}")
notas.append(f"rango {df['fecha'].min().date()} a {df['fecha'].max().date()}")
if df.duplicated(list(clave)).any():
fallos.append("hay claves duplicadas")
if df["fecha"].max() > pd.Timestamp.today():
fallos.append("hay fechas en el futuro")
if not df[col_precio].dropna().gt(0).all():
fallos.append("hay precios no positivos")
# Cobertura: un simbolo que desaparece a mitad no genera nulos ni duplicados.
por_ano = df.groupby(df["fecha"].dt.year)["simbolo"].nunique()
notas.append("simbolos por ano: " + str(por_ano.to_dict()))
saltos = df.groupby("simbolo")[col_precio].pct_change().abs()
notas.append(f"saltos diarios por encima del 50 por ciento: {int((saltos > 0.5).sum())}")
notas.append(f"nulos en {col_precio}: {df[col_precio].isna().mean():.3%}")
return {"ok": not fallos, "fallos": fallos, "notas": notas}Fíjate en la comprobación de cobertura: las filas que faltan no producen nulos ni duplicados, así que ninguna comprobación de valores las ve. Es el hueco por el que se cuela el sesgo de supervivencia del nivel 2. El informe se guarda junto al manifiesto, con la misma fecha, y se compara con el de la ejecución anterior: lo que interesa no es solo que hoy esté bien, sino qué se ha movido desde ayer.
Caso: la reproducción que no cuadra
Hou, Xue y Zhang repitieron unas cuatrocientas cincuenta anomalías publicadas aplicando criterios homogéneos y encontraron que una mayoría amplia, en torno a dos tercios, dejaba de ser estadísticamente significativa. No fue por mala fe ni por errores de programación: la diferencia salía de decisiones que nadie había escrito, como el mínimo de capitalización, la ponderación por tamaño, el tratamiento de los valores extremos y la fecha de rebalanceo. Ese es el argumento entero de este módulo. Si en un conjunto de datos las decisiones no están escritas, ni siquiera tú podrás reproducir tu propio resultado dentro de un año, y no sabrás si la culpa fue del mundo o tuya.