3. A dataset that is auditable from end to end
Level 6: From data to decision
An auditable dataset is one in which any number in any cell can be traced back to its origin and to the exact list of operations that produced it. It sounds like bureaucracy right up until the day a result changes and you have no way of knowing whether it was the world, the vendor or you that changed.
Lineage: every number's biography
Data lineage is simply that: the complete biography of a value. Without the jargon: if a number in your table cannot tell you its life story, it is not a data point, it is a rumor in numeric format. And telling its life story means five concrete things:
- The exact source, not the vendor's name: the endpoint, the file or the regulatory filing it came from.
- The moment of download, because the same query made two months later can return a different figure.
- The raw file that contained it, stored exactly as it arrived and with its cryptographic hash.
- The transformations, in order, from that raw file to the final column the strategy uses.
- The human decisions applied along the way, with their criterion and the alternative that was rejected.
There are three questions that record has to be able to answer without anybody remembering anything. Where did it come from? What was done to it? What was decided about it when it looked doubtful? If all three have written answers, the dataset is auditable; if any of them depends on your memory, it is not.
The manifest that travels with the table
Each version of the dataset comes with a metadata file generated by the same process that produces it. Never written by hand afterwards.
import hashlib, json, os, subprocess
import datetime as dt
CLAVE = os.environ.get("CLAVE_PROVEEDOR", "tu_clave_aqui") # never in the manifest
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)The cryptographic hash is the piece almost everybody skips and the only one that tells apart two scenarios that look very much alike: that your code changed, or that the vendor rewrote its history without telling you. The row count is no use for that, because revising values does not change the count.
The transformations are an ordered list, not a heap
The order matters and you have to be able to recite it. Nulling out impossible prices before or after computing the adjustment factors gives different results; filtering on market capitalization before or after applying the join with the fundamentals changes the universe. The practical rule: the raw file is never touched, it is stored exactly as it arrived and everything else is rebuilt from it with a script that runs from beginning to end in one go. If your dataset only exists because one day you ran three cells of a notebook in a certain order, it is not reproducible.
Doubtful decisions get written down at the time
There will be cases no rule covers: a company that changes its financial year, a price that shoots up and might be real, a badly reflected merger. Every time you decide something, write in the same place which criterion you applied, on which rows, and which alternative you rejected and why. A year from now that note is worth more than the code, because the code says what you did and the note says why.
And there is a point of sequence that separates serious work from the rest: those decisions are taken and written down before looking at any result. The moment you know that nulling out 412 rows improves your return and keeping them worsens it, you have stopped deciding about the data and started choosing the answer you like. The decision log is, at bottom, a defense against yourself, and that is why its value depends entirely on when it gets written.
A report that generates itself
And finally, the dataset has to pass a fixed examination every time it is rebuilt.
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")
# Coverage: a symbol that disappears halfway through produces no nulls and no duplicates.
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}Note the coverage check: the rows that are missing produce no nulls and no duplicates, so no value-based check sees them. That is the gap through which the survivorship bias of level 2 slips in. The report is stored alongside the manifest, with the same date, and compared with the previous run's: what matters is not only that it is fine today, but what has moved since yesterday.
A case: the reproduction that does not add up
Hou, Xue and Zhang repeated some four hundred and fifty published anomalies applying uniform criteria and found that a large majority, around two thirds, stopped being statistically significant. It was not bad faith or programming errors: the difference came from decisions nobody had written down, such as the minimum market capitalization, the size weighting, the treatment of extreme values and the rebalancing date. That is this module's entire argument. If the decisions in a dataset are not written down, not even you will be able to reproduce your own result a year from now, and you will not know whether the fault was the world's or yours.