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.
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 have | Why you do it | Data science twin |
|---|---|---|
| Infrastructure as code, not hand edits | rebuild identically | notebook plus a .py script in Git, not cells alone |
| Pinned image tags, never latest | same bits every deploy | pinned requirements, not a bare pip install |
| Config in version control | one source of truth | data path and params in a config, not hardcoded in a cell |
| Idempotent runbooks | rerun gives the same state | Restart kernel and Run All, top to bottom |
| Immutable artifact per release | a number you can trace | fixed 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.
# 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.
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
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.
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.
References
- Python documentation, venv, creation of virtual environments
- nbstripout, strip output from Jupyter and IPython notebooks as a Git filter
- uv documentation, locking environments and pip compile
- nbconvert documentation, executing notebooks from the command line
- Data Science Series, reproducible Python setup
- Infra to Data Science, the Complete Guide


DrJha