"A result you cannot reproduce is a rumor with a confidence interval."
A Reproducible AI Agent
The Big Picture
The previous six sections taught you to choose formats, cut windows, avoid leakage, build honest splits, calibrate per device, and reason about label quality. Each of those is a decision, and every decision is a place where a future teammate (or future you) can silently do something different and get a different number. This section is about freezing those decisions so the same raw bytes always produce the same dataset, and so a downstream consumer can detect the day your sensors start lying. Two ideas carry the weight: a versioned pipeline that makes the transformation from raw stream to training tensor deterministic and auditable, and a data contract that states, in machine-checkable form, what each stage promises about the data it emits. Together they turn "it worked on my laptop in March" into a build you can rerun in one command a year later.
Six weeks after the results were signed off, a model's accuracy quietly slid two points, and nothing in the code had changed: a resampler had picked up a new library default, and every training window was now subtly different from the one that produced the reported number. Stopping that silent rewrite is the whole job of this section, and it builds directly on machinery you already have: the sensor formats and timestamping of Chapter 3 and the windowing, split, and calibration choices of Sections 5.2 through 5.6, which we now treat as a single chain to be versioned. This section stays clear of Chapter 69 (operating deployed models across a fleet) and Chapter 65 (evaluation protocols); here the deliverable is a dataset build that anyone can regenerate byte-for-byte and trust on arrival.
Why sensor pipelines rot, and what reproducibility actually requires
What. A sensor pipeline is the ordered sequence of transformations that turns raw device output into model-ready tensors: decode, resample, synchronize, filter, window, normalize, label, split. Why it rots. Every stage hides a moving part. A resampler pulls in a new SciPy release with a different default; a firmware update changes the accelerometer full-scale range; someone re-runs the split with a fresh random seed; a timezone assumption flips at a daylight-saving boundary. None of these throw an error. They just quietly change the dataset, and six weeks later a model that "regressed" is really a model trained on different data.
How reproducibility is achieved. That quiet two-point slide could just as easily have been a failed regulatory audit or a wearable recalled from thousands of wrists, and by the time anyone notices, the raw evidence needed to explain it is often already overwritten. The only real defense is to make the build itself refuse to change without saying so. Determinism is not one thing but four, and you need all four to actually hold. First, fix the inputs: content-address the raw data so a hash identifies the exact bytes, not a mutable path. Second, fix the code: pin the pipeline to a commit and pin every library version. Third, fix the configuration: put window length, stride, seed, filter cutoffs, and split policy in a versioned config file, never in a notebook cell. Fourth, fix the environment and randomness: seed every random operation and record the interpreter and operating system (OS). The output identity you want is a function of all four:
$$\text{dataset\_id} = H\big(\text{raw\_hash},\ \text{code\_commit},\ \text{config\_hash},\ \text{env\_hash}\big)$$where \(H\) is a cryptographic hash. If any input changes, the id changes, and you have caught a silent mutation before it reached a model. If all four match, re-running the build reproduces the same tensors in practice, which is the whole game. In short: version the recipe, not the result, and every unrequested change is forced to announce itself as a changed id. Figure 5.7.1 shows the four pinned inputs converging through the hash into a single identity that any consumer can check on arrival.
H combines them into one dataset_id. Any silent change to any input flips the id, so a mismatch between the built id and the expected one is caught before the data reaches a model.Mental Model
Think of the dataset_id like the batch code stamped on a carton of milk. That code is not a name someone typed; it is computed from the exact farm, the exact day, and the exact processing line, so it falls out of the ingredients rather than being chosen. If the dairy quietly switches suppliers, the code on the next carton changes on its own, and a shop that receives a carton whose code does not match the one on its invoice knows something was swapped before anyone tastes it. Your four inputs (raw bytes, code commit, config, environment) are the farm, day, and line; the hash is the stamp; a mismatched id at delivery is the swapped carton caught at the loading dock rather than in a customer's cereal.
Key Insight
The reproducibility failure that actually bites sensor teams is almost never model randomness; it is silent input drift. A model retrain is loud and expected. A resampler that changed its interpolation default, or a raw-data directory someone quietly re-uploaded with a bug fix, changes your data with no signal at all. Content-addressing the raw bytes and hashing the config is worth more than a hundred fixed seeds, because it converts an undetectable change into a changed id that fails a check. Reproducibility is less about repeating success and more about making unrequested change impossible to hide.
Common Misconception
The misconception is that reproducible implies correct: readers assume that if a build regenerates byte-for-byte it must be right. Reproducibility only guarantees that the same inputs yield the same outputs, so a pipeline with a wrong filter cutoff or a leaky split will reproduce that flaw perfectly on every run; content-addressing catches unrequested change, while it is the data contract, not the hash, that catches data which is wrong.
The bug that only appeared on Tuesdays after lunch
Content-addressing has a strange payoff: it turns "works on my machine" into a testable claim, and the history of computing is full of non-reproducibility that took years to pin down because nobody hashed their inputs. The most famous is the Pentium FDIV bug of 1994, where a handful of missing entries in a lookup table made Intel's flagship chip return subtly wrong division results for a tiny fraction of operands. The error was one part in billions and surfaced only on specific inputs, so for months it read as flaky software rather than deterministic hardware. Thomas Nicely, a number theorist counting prime pairs, finally isolated it precisely because his computation was reproducible: the same sum came out wrong the same way every run, which is the signature of a real defect rather than noise. The lesson for sensor pipelines is the inverse and just as useful: if you cannot reproduce a result bit-for-bit, you cannot even tell whether a discrepancy is a bug or randomness. Reproducibility is not a bureaucratic nicety; it is the instrument that makes a bug visible at all.
Data contracts: promises the pipeline must keep
A versioned pipeline guarantees that the same bytes come out every run, but it says nothing about whether those bytes are physically sensible; that gap is exactly what a data contract closes. What. A data contract is an explicit, machine-checkable specification of what a dataset or a pipeline stage guarantees: the schema (channel names, dtypes, units), the physical ranges, the sampling rate and its allowed jitter, the null and gap policy, and the label vocabulary. It is the application programming interface (API) of your data. Why. Without a contract, every consumer re-discovers the data's quirks by trial and error, and a producer can break a downstream model just by shipping a new sensor batch in millivolts instead of volts. A contract moves that failure to the boundary where it belongs: the moment bad data arrives, a validation gate rejects it with a specific reason, instead of a model degrading mysteriously a month later.
How. A contract has two halves. The declarative schema lists fields, types, and units and is checked cheaply on every batch. The statistical expectations assert distributional facts: the accelerometer magnitude at rest sits near \(9.81\ \mathrm{m/s^2}\), the fraction of missing samples per window stays below a threshold, the inter-sample interval clusters at \(\Delta t = 1/f_s\). These expectations connect directly to the sensor physics of Chapter 2: a contract that knows gravity's magnitude can flag a mislabeled axis or a wrong full-scale setting automatically. When. Enforce the contract at three gates: at ingestion (raw data crossing into your system), between stages (each transform validates its output), and at the training boundary (the final tensor matches what the model expects). Contracts should fail closed: a violation halts the build rather than passing suspect data through.
Practical Example: the wind farm that shipped a unit change
An industrial team monitoring gearbox vibration on a fleet of wind turbines had a stable anomaly detector running for a year. One maintenance cycle, a vendor replaced a batch of accelerometers with a newer model that reported acceleration in \(g\) rather than \(\mathrm{m/s^2}\), a factor of about \(9.81\). No code changed, no error fired, and the ingestion path happily accepted the new files. The detector's false-alarm rate collapsed toward zero because every reading now looked nine times quieter than the trained baseline, so real faults slid under the threshold. A one-line data contract, "root-mean-square vibration on a healthy unit lies in \([2, 40]\ \mathrm{m/s^2}\)", would have rejected the very first file from the new sensors and named the reason. After the incident the team added unit and range checks at ingestion; the next hardware swap was caught in minutes, not quarters. This is the same class of failure the normalization discussion in Chapter 37 revisits under real drift.
Real-World Application: Tesla Autopilot fleet data
By public accounts, Tesla's Autopilot team versions the multi-sensor clips (camera, radar, inertial measurement unit (IMU)) mined from its fleet as immutable, content-addressed datasets, so a retrained perception network can be traced to the exact set of clips and labeling revision that produced it and rebuilt on demand. When a labeling policy or an auto-labeler changes, the affected clips get a new dataset identity rather than silently overwriting the old one, which is what lets the team A/B two models on provably identical evaluation data. This is the four-part id of this section operating at fleet scale, where "is this the same data" must have an auditable answer before a model ships to millions of cars.
Building a versioned pipeline in practice
Knowing what a contract must promise tells you nothing about the machinery that enforces it every run; that machinery is the versioned pipeline itself. How to structure it. Express the pipeline as a directed acyclic graph (DAG) of stages, each a pure function from input artifact to output artifact, tagged with the hash of its inputs and config. Two properties follow for free: caching, since a stage whose input hash is unchanged is skipped, and lineage, since every tensor can name the raw file, commit, and config that produced it. A reviewer asking where a training example came from gets a lookup, not an archaeology project. When to version what. Version the raw data immutably by content address, the code by git commit, the config in the repo, and the dataset as a manifest (a small metadata file that records the four-part id and the inputs it came from). Never version derived tensors by copying them around; version the recipe and cache its output.
A DAG is a set of stages joined by dependency edges that never form a cycle. Because no edge cycles back, there is always a well-defined order in which to run the stages, and no stage can depend, directly or transitively, on its own output. That acyclic structure is exactly what lets the build compute each stage's input hash before it runs, which is what makes caching and lineage possible at all. The build walks the graph in topological order, meaning every stage is visited only after all the stages it depends on, and skips any stage whose combined input-and-config hash matches the previous run. Reach for a DAG runner instead of a linear script the moment stages start sharing intermediate artifacts or you want partial rebuilds; a plain top-to-bottom script is fine only while the pipeline is a single unbranched chain. Figure 5.7.2 illustrates a versioned pipeline DAG with per-stage hashing, caching, and lineage.
Checkpoint
So far: model the pipeline as an acyclic graph of pure-function stages, hash each stage's inputs and config so unchanged stages are cached and every output can name its lineage, and version the recipe (raw by content address, code by commit, config in the repo) rather than copying derived tensors around.
The snippet below computes the dataset identity from its four inputs and writes a manifest. It is deliberately small; the point is that the identity is a deterministic function of pinned inputs, so two people running it on the same commit and config get the same id and can prove they built the same data.
import hashlib, json, subprocess, sys, platform
from pathlib import Path
def sha256_file(path, chunk=1 << 20):
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(chunk), b""):
h.update(block)
return h.hexdigest()
def hash_str(s):
return hashlib.sha256(s.encode()).hexdigest()
def build_manifest(raw_files, config):
raw_hash = hash_str("".join(sorted(sha256_file(p) for p in raw_files)))
code_commit = subprocess.check_output(
["git", "rev-parse", "HEAD"]).decode().strip()
config_hash = hash_str(json.dumps(config, sort_keys=True))
env_hash = hash_str(f"{sys.version}|{platform.platform()}")
dataset_id = hash_str(raw_hash + code_commit + config_hash + env_hash)[:16]
return {
"dataset_id": dataset_id,
"raw_hash": raw_hash, "code_commit": code_commit,
"config_hash": config_hash, "env_hash": env_hash,
"config": config,
}
if __name__ == "__main__":
cfg = {"window_len": 256, "stride": 64, "fs_hz": 50,
"split": "subject_disjoint", "seed": 7}
manifest = build_manifest(sorted(Path("raw").glob("*.parquet")), cfg)
Path("dataset_manifest.json").write_text(json.dumps(manifest, indent=2))
print("dataset_id:", manifest["dataset_id"])
dataset_id from raw-data hashes, the git commit, a sorted config hash, and an environment fingerprint. Re-running on the same inputs reproduces the id; any silent change to raw bytes, code, config, or interpreter flips it, which is exactly the alarm you want.Step-Through: computing a dataset_id and watching it flip
Trace build_manifest on a tiny build with two raw files. Say a.parquet hashes to 7c1f... and b.parquet to 3e90... (real SHA-256 values are 64 hex chars; we show the first four). Step 1, sort the two file hashes: sorted order puts 3e90... before 7c1f..., so the concatenation is "3e90...7c1f..." and its hash gives raw_hash = d4a2.... Step 2, git rev-parse HEAD returns code_commit = 9fb17c2. Step 3, the config {"window_len":256,"stride":64,"fs_hz":50,"split":"subject_disjoint","seed":7} serialized with sorted keys hashes to config_hash = 5511.... Step 4, the environment string "3.14.0 ... | Windows-11" hashes to env_hash = ab08.... Step 5, concatenate all four (d4a2... + 9fb17c2 + 5511... + ab08...) and hash, take the first 16 chars: dataset_id = 2f6b9c07e1a3d845. Now change one thing only: bump SciPy so b.parquet re-resamples to bytes that hash to 3e91.... The sorted concat becomes "3e91...7c1f...", raw_hash becomes c07e..., and the final dataset_id becomes 81d40a9f5c2be773. Same code, same config, same interpreter, yet the id flipped: exactly the alarm you wanted. Notice too that swapping the file listing order (b before a) leaves the id unchanged, because the sort in step 1 erased the ordering before hashing.
Note the two subtle correctness moves in the code. Raw hashes are sorted before concatenation so file order does not affect the id, and the config is serialized with sort_keys=True so a reordered dictionary still hashes identically. Both prevent spurious id changes that would train teams to ignore the alarm. This same discipline underpins the leakage-safe benchmarking of Chapter 65: a benchmark is only fair if everyone provably built the same splits.
Right Tool: let a data-versioning stack carry the plumbing
The manifest above is instructive but it is roughly 30 lines you would then have to grow into caching, remote storage, and a lineage graph, easily a few hundred more. Purpose-built tools do this for you. Data Version Control (DVC) turns dvc add raw/ plus a short dvc.yaml stage file into content-addressed raw data, cached deterministic stages, and a git-tracked lineage graph in under ten lines of config; a rebuild after an unchanged input is a no-op it detects automatically. For the contract half, Pandera or Great Expectations expresses a full schema-plus-range check as a short declarative object instead of dozens of hand-written asserts, and produces a readable failure report naming the offending column and rows. The rule of thumb: hand-roll the manifest once so you understand the four-part id, then adopt the tools so caching, storage, and validation reports are not your code to maintain.
Research Frontier
The manifest and contract in this section are per-team conventions; the field is now standardizing them. MLCommons' Croissant format (Akhtar et al., "Croissant: A Metadata Format for ML-Ready Datasets," NeurIPS 2024) defines a machine-readable JSON-LD (JSON for Linked Data, a JSON encoding that attaches shared semantic meaning to each field) schema that ships a dataset's structure, provenance, and column-level semantics inside the dataset itself, and it is already the ingestion format for Hugging Face, Kaggle, and OpenML. Its 2024 Croissant-RAI extension adds responsible-AI and data-collection fields well suited to sensor provenance, moving the schema half of a data contract away from bespoke Pandera objects and toward a portable standard that a downstream tool can read without running any of your code.
When reproducibility is worth the cost, and when it is not
Now that you can build a fully versioned pipeline, the harder question is when the machinery earns its keep, because all of this determinism is not free. What it costs. Full versioning adds friction: hashing large raw archives takes time, you must rebuild pinned environments, and a contract that fails closed will occasionally block a build at an inconvenient hour. When to pay it. Pay in full whenever you will publish, audit, or ship a result to a device you cannot easily update, and whenever more than one person touches the pipeline. A clinical study, a regulated wearable (the validation regime of Chapter 34), and any leaderboard submission all demand it, because the question "is this the same data" must have a provable answer. When to lighten up. A solo exploratory notebook that will be thrown away next week does not need a content-addressed remote; pin the seed and the library versions and move on. The skill is matching the ceremony to the stakes: enough determinism that you can trust and defend the number, not so much that exploration grinds to a halt.
Exercise
Take a small human-activity dataset and build it twice through the same windowing and split code, but on the second run bump one library (say, change your resampling library's minor version) without touching your own code. Compute the dataset_id both times using the manifest approach above. Confirm the id changes even though your code did not, then write the two-line data contract (sampling interval and accelerometer-magnitude-at-rest range) that would have flagged whichever run produced physically wrong values. Report which of the four id components differed and why.
Try It: catch a silent unit change with a 20-line contract
You need only numpy, pandas, and pandera (pip install pandera).
- Generate a synthetic resting-accelerometer file:
df = pd.DataFrame({"accel_mag": np.random.normal(9.81, 0.05, 10000)}), then save it withdf.to_parquet("good.parquet"). - Write a Pandera schema with one range check:
DataFrameSchema({"accel_mag": Column(float, Check.in_range(2, 40))}), matching the physical range of resting acceleration in m/s squared, and confirmschema.validate(df)passes. - Simulate the vendor swap to units of g with
bad = df / 9.81, runschema.validate(bad, lazy=True), and read the failure report naming the out-of-range column. - Compute a
dataset_idforgood.parquetand for a re-savedbad.parquetusing thebuild_manifestfunction from this section, and confirm the two ids differ even though your validation code never changed. - Add a second clause on the sampling interval (assert the median
diffof a timestamp column equals1/fswithin a small tolerance) and re-run to watch both contract clauses enforced in one pass.
Self-Check
1. A colleague says "I fixed all the random seeds, so my dataset is reproducible." Name one common way the dataset can still silently change, and how content-addressing would catch it.
2. Why should a data contract be enforced at ingestion rather than only at the training boundary? Give a failure that only an ingestion-time check catches early.
3. In the dataset_id formula, why are the raw file hashes sorted and the config serialized with sorted keys before hashing? What false alarm does each choice prevent?
Lab 5
build leakage-safe subject- and device-disjoint splits for human activity recognition; measure the accuracy gap vs a naive random split.
Bibliography
Reproducibility and pipeline versioning
Kuprieiev, R. et al. (2024). DVC: Data Version Control. Documentation and open-source project.
The canonical tool for content-addressed data, cached deterministic pipeline stages, and git-tracked lineage; the practical realization of the four-part dataset id in this section.
A case-study survey of what makes computational workflows reproducible in practice; grounds the code, config, environment, and data pinning that this section formalizes.
Data contracts and validation
Great Expectations (2024). An open-source framework for data validation and documentation.
Expresses schema plus statistical expectations as declarative checks with human-readable failure reports; the tooling behind the fail-closed contract gates described here.
A lightweight schema-and-range validation library for tabular sensor data; a few lines replace dozens of hand-written asserts at each stage boundary.
Google's production data-validation system for ML; formalizes schema inference, anomaly detection, and the training-serving skew that data contracts are designed to prevent.
Experiment tracking and ML operations
Introduces run tracking, parameters, and artifact logging that pair with a versioned pipeline to make a training result auditable end to end.
The classic account of how unstable data dependencies and undeclared consumers accumulate silent risk; the intellectual case for data contracts and versioned inputs.
Gebru, T. et al. (2021). Datasheets for Datasets. Communications of the ACM.
Argues that every dataset should ship documented provenance, composition, and intended use; the human-readable companion to the machine-checkable contract.
What's Next
In Chapter 6, we open Part II and start turning clean, versioned streams into clean signals: moving averages, finite impulse response (FIR) and infinite impulse response (IIR) filters, and the band-pass and notch designs that strip noise while preserving the phenomenon. Every filter you meet there is a pipeline stage in the sense of this section, with a cutoff and a design choice that belongs in your versioned config and under a data contract; reproducibility is not left behind when we move from data engineering to signal processing, it comes along as a habit.