, ,

Reproducible Data Work for Infra Engineers, Venvs, Git and Notebooks (Infra to Data Science Series, Part 6)

You already enforce reproducibility in ops with pinned images and config in Git. Here is how to apply the same three habits, one pinned environment, clean notebook version control and fixed seeds, to your metrics analysis so it survives a fresh clone.

Infra to Data Science Series · Part 6 of 26

A capacity notebook I handed a teammate ran clean on my laptop and died on his with NameError, name cpu is not defined, on a cell that had a green checkmark in my own session an hour earlier. Nothing was wrong with the code, only with the order I had run the cells in, and my kernel was the one place on earth where the missing piece still existed.

Who this is for: An infrastructure engineer, SRE or platform admin who already versions Terraform, pins image tags in a Dockerfile and never edits production by hand, now wanting the same guarantees for data work. You have the loaded metrics frame from last part and want the analysis to survive a fresh clone on someone else machine. Terms on first use: a virtual environment is an isolated per project set of Python packages; pinning means recording exact versions so an install repeats; a kernel is the live Python process a notebook talks to, holding all your variables; a seed fixes a random number generator so a run reproduces.
Key takeaways: You already enforce reproducibility in ops, infrastructure as code, pinned images, config in Git, and data work needs the same three habits, one pinned environment, clean version control, fixed seeds. A notebook that shows green in your session is not reproducible; running mine from a fresh kernel reproduced the same NameError in three seconds, the one check almost nobody does. pip freeze pinned 7 packages from the 3 I asked for, so a snapshot records transitive versions you never chose, useful but not a true lock. Without a fixed seed a test set incident rate wobbled between 3.019 and 3.051 percent across runs; with random_state 42 it was 3.108 both times.

Reproducibility you already enforce in operations

You do not need convincing that reproducibility matters, because your job already depends on it. A host you cannot rebuild from code is a liability. An image tagged latest that pulls something different next week is an outage waiting for a trigger. Config that lives only in one admin head is a bus factor of one. Every one of those instincts transfers directly to data science, where the failure is quieter but identical in shape: a result nobody can reproduce is a result nobody should trust, including you three weeks later when a stakeholder asks how you got the number.

So the move here is not to learn a new discipline, it is to map the one you have onto a new medium. Below is that map, and it is the artifact worth keeping from this part, because each ops habit on the left has a data twin on the right that you can adopt today without a single new concept.

Ops habit you already haveWhy you do itData science twin
Infrastructure as code, not hand editsrebuild identicallynotebook plus a .py script in Git, not cells alone
Pinned image tags, never latestsame bits every deploypinned requirements, not a bare pip install
Config in version controlone source of truthdata path and params in a config, not hardcoded in a cell
Idempotent runbooksrerun gives the same stateRestart kernel and Run All, top to bottom
Immutable artifact per releasea number you can tracefixed seed plus recorded versions per result

None of this is theoretical for the running project. Last part we loaded a month of one cluster metrics, fixed the dtype trap and reduced it to a tidy daily frame. This part we make that work reproducible, so the next reader, very possibly future you, can clone it and get the same 240 row daily frame without a mystery. The Data Science Series covers the tooling depth in its reproducible Python setup part; here the goal is the operator translation.

One pinned environment per project

Start where you would start a service: an isolated environment with pinned versions. A virtual environment gives each project its own package set, so upgrading pandas for one analysis cannot silently break another. Create it, activate it, install what you need with exact versions, then freeze. Watch the freeze output closely, because it tells you something most tutorials skip.

flowchart LR
  A[Your notebook and data] --> B[Commit code, requirements.txt, seed]
  B --> C[Teammate clones the repo]
  C --> D[python -m venv then pip install -r]
  D --> E[Restart kernel, Run All]
  E --> F{Same numbers}
  F -->|Yes| G[Reproducible result]
  F -->|No| H[Unpinned dep or hidden state]
The loop a result must pass to count as reproducible. Two branches can break it, an unpinned dependency and hidden kernel state, and the rest of this part closes both.
# tested on Python 3.10.12, pandas 2.2.3, numpy 2.0.2, scikit-learn 1.5.2
python -m venv .venv
source .venv/bin/activate
pip install pandas==2.2.3 numpy==2.0.2 scikit-learn==1.5.2
pip freeze > requirements.txt
cat requirements.txt
joblib==1.5.3
numpy==2.0.2
pandas==2.2.3
python-dateutil==2.9.0.post0
scikit-learn==1.5.2
scipy==1.15.3
threadpoolctl==3.6.0

I asked for three packages and freeze recorded seven. Those extra four, scipy, joblib, threadpoolctl and python-dateutil, are transitive dependencies that scikit-learn and pandas pulled in. That is the useful part: your requirements file now pins the whole resolved set, so a colleague gets the exact scipy you tested against, not whatever resolves fresh six months from now. It is also the trap, because a flat freeze mixes the packages you chose with the ones chosen for you, and offers no hashes to prove the download was not tampered with.

One more reason to pin the whole set and not just your own packages: a colleague ran my unpinned requirements on Python 3.11 while I sat on 3.10, pip resolved a newer scipy for him, a numeric default had shifted between the two versions, and our supposedly identical notebook disagreed in the third decimal place. That is a hard bug to chase, because the code is byte for byte the same and only the resolved dependency moved underneath it. A committed lock file removes the guess by fixing every version, yours and the transitive ones, to exactly what you tested.

Verdict: pip freeze is a snapshot, not a lock, and treating the two as the same is the mistake to avoid. For a solo analysis a frozen requirements.txt is fine and I use it daily. As soon as more than one person or a CI job depends on the result, move up to a real lock: keep a short list of only your direct dependencies and compile it into a hashed, fully pinned lock with pip-compile from pip-tools, or with uv, which produces a uv.lock you commit and never edit by hand. My pick for a shared project is uv for speed, pip-tools if you want to stay closest to plain pip. The one to avoid is installing into your base Python with no environment at all.

Notebooks lie about what they actually ran

Here is the failure from the opening line, reproduced on purpose so you can see the mechanism. A notebook does not run itself top to bottom as you write it. You jump around, run a cell, edit an earlier one, run it again, and the kernel accumulates state in whatever order you clicked. Every cell carries an execution_count, the little number in the brackets, and reading those numbers exposes the order you truly ran. Consider two cells where the one that uses a variable sits first in the file but was executed second.

# cell that appears FIRST in the file, but ran SECOND, execution_count [2]
hot = cpu[cpu > threshold]
print('rows over threshold:', len(hot))

# cell that appears SECOND in the file, but ran FIRST, execution_count [1]
import numpy as np
cpu = np.array([12, 55, 33, 78, 41])
threshold = 40

In your session this looks perfect. You defined cpu and threshold, then scrolled up and ran the analysis cell, so both have green checkmarks. But the saved order on disk is analysis first, definitions second. Run it the way anyone else will, fresh from the top, and it breaks.

$ jupyter nbconvert --to notebook --execute analysis.ipynb --output out.ipynb
[NbConvertApp] Executing notebook with kernel: python3
----> 1 hot = cpu[cpu > threshold]
nbclient.exceptions.CellExecutionError: An error occurred while executing the following cell:
NameError: name 'cpu' is not defined

NameError, name cpu is not defined, from a notebook that looked finished. This fix is a habit, not a tool: before you trust or share a notebook, hit Restart Kernel and Run All, which wipes hidden state and runs every cell top to bottom exactly as a clone would. If it survives that, the execution_count column reads 1, 2, 3 straight down and you have a reproducible artifact. If it does not, you just found a bug that would otherwise have surfaced on a reviewer machine, which is the worst place to find it. This is the operator instinct for idempotent runbooks, applied without change.

Keeping notebooks friendly to Git

A notebook is JSON on disk, and it stores your code, your outputs, and an execution_count for every cell in the same file. That means rerunning a notebook with zero code changes still rewrites the file, because the counts bump and any printed number or plot is re baked into the JSON. Commit that and your Git history fills with diffs that carry no information. Here is what changes when I only rerun one unchanged cell.

$ git diff nb.ipynb
- "execution_count": 1, ... "text/plain": ["25.39"]
+ "execution_count": 7, ... "text/plain": ["25.41"]

Nothing in the source changed, yet Git sees a modified file because the count went 1 to 7 and a rounding wobble moved the printed mean from 25.39 to 25.41. On a real multi cell notebook this is dozens of noisy lines per commit, and it makes review and merges miserable. A clean fix is nbstripout, a tool that installs as a Git filter and strips outputs and counts from what Git sees while leaving your working copy fully intact.

pip install nbstripout
nbstripout --install          # wires the clean filter into this repo
echo '*.ipynb filter=nbstripout' >> .gitattributes

After that, committing the reran notebook shows no diff at all, because Git only ever sees the code. One contrarian note some tutorials get wrong: do not commit rendered outputs into the tracked notebook thinking it documents your results. It does the opposite, it turns every rerun into a false change and buries the real code edits. Keep outputs out of version control and reproduce them from a clean run instead. Being able to reproduce a run on demand is the same reflex that makes tracing and observability work in language model systems, where you keep the inputs and the versions so a bad result can be replayed rather than guessed at.

Seeds and results you can defend

The last source of a non reproducible number is randomness you did not pin. Splitting data into train and test, sampling rows, initialising a model, all draw from a random number generator, and if you never fix its seed, two runs of the identical code give different answers. Small differences, but real, and they undermine any claim you make about a metric. I split a synthetic 345,600 row frame the same way four times to show it.

import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split

df = pd.DataFrame({'cpu_pct': np.random.default_rng(0).normal(25, 8, 345600).round(2)})
df['incident'] = (df['cpu_pct'] > 40).astype(int)

def rate(seed):
    _, test = train_test_split(df, test_size=0.2, random_state=seed)
    return round(test['incident'].mean() * 100, 3)

print('no fixed seed, run 1:', rate(None))
print('no fixed seed, run 2:', rate(None))
print('fixed seed 42, run 1:', rate(42))
print('fixed seed 42, run 2:', rate(42))
no fixed seed, run 1: 3.051
no fixed seed, run 2: 3.019
fixed seed 42, run 1: 3.108
fixed seed 42, run 2: 3.108
Test set incident rate, seeded vs unseededpercent, x axis zoomed to 2.90 to 3.20, 20 percent hold out of 345,600 rowsno seed, run 13.051no seed, run 23.019seed 42, run 13.108seed 42, run 23.1082.903.20Unseeded runs disagree by 0.032 points; the two seeded runs are exactly equal
Same code, four runs, x axis zoomed so a third of a percent is visible. The two seeded bars are identical; the two unseeded bars are not.

Pass random_state to every function that samples or splits, in scikit-learn, pandas sample, and NumPy generators, and record the value. A fixed seed does not make your model right, it makes your result repeatable, which is the precondition for judging whether it is right. The place this bites hardest is model evaluation, where an unpinned split quietly changes your reported accuracy between runs and hides real problems like leakage; the Data Science Series treats that carefully in model evaluation and leakage.

Project status and the reproducibility checklist

Where the project stands now: last part loaded and tidied a month of cluster metrics, this part wraps that work so it reproduces. Your metrics analysis lives in a project folder with a .venv you can rebuild from requirements.txt, a notebook that passes Restart and Run All cleanly, nbstripout keeping the Git history readable, and a fixed seed on every split. That is phase two of the roadmap from Part 4 landing on schedule, and it is exactly the state a hiring manager means when they ask whether you can hand a project to someone else.

War story: The handoff from the opening was mine, and I spent forty minutes on it before the penny dropped. My teammate pinged that my capacity notebook threw NameError on cpu on his machine, and I bounced it back twice sure it worked, because it did work, on my screen. I finally ran Restart Kernel and Run All on my own laptop and watched it fail in three seconds with the identical error. My kernel had been holding a variable from a cell I had run, then moved below the one that used it. That afternoon cost me the assumption that a green notebook is a finished notebook, and I have run Restart and Run All before every share since.

Turning a proven notebook into an installable, tested package is the next rung of this ladder, and the Data Science Series walks it in notebook to package. You are not there yet and do not need to be; a clean, pinned, seeded notebook that survives a fresh clone is already ahead of most analysis code in production.

Pin one environment before your next analysis

If you do one thing after this, create a .venv for your metrics project, install pandas at a pinned version, and run pip freeze into a requirements.txt you commit. Then open your notebook and hit Restart Kernel and Run All once, watching for any cell that fails now but seemed fine before, because that cell was living on hidden state and just told you the truth. Add a random_state to your split, install nbstripout so your history stays clean, and you have applied every reproducibility habit you already trust in ops to your first data project. Next part reframes your infrastructure telemetry itself as a dataset, the raw material this whole series models, so bring the pinned, reproducible project you just built.

Infra to Data Science Series · Part 6 of 26
« Previous: Part 5  |  Guide  |  Next: Part 7 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading