Reproducible Notebooks Best Practices
Apply reproducibility best practices in notebooks — Data Science with Python.
Focus: apply reproducibility best practices in notebooks
You've built a beautiful analysis in a Jupyter notebook — clean charts, insightful code, a narrative that flows. Now imagine revisiting it three months later... or worse, a colleague trying to run it on their machine. The plot doesn't reproduce, the numbers look different, and the error message is cryptic. That's the silent killer of data science work: lack of reproducibility. In this lesson, you'll learn how to apply reproducibility best practices in your notebooks — turning chaotic experiments into dependable, shareable deliverables that anyone (including future you) can run with confidence.
The problem this lesson solves
The pain is real and universal. Data science projects are often exploratory by nature, but that exploration often happens in a sea of hidden state, unpinned dependencies, and magical numbers. Let's break down the common culprits that make notebooks non-reproducible:
- Hidden state: You ran cells in a specific order, and some variables exist only because of a cell you executed hours ago. A fresh kernel won't have them, leading to
NameErroror worse, silently different results. - Environment drift: You installed package version X six months ago. Your colleague has version Y. The output of
pd.read_csvor a model fit can change subtly—or catastrophically—between versions. - Non-determinism: Random seeds are not set, so every run of a machine learning cell produces different numbers. Your "reproducible" results are a roll of the dice.
- Lack of documentation: Why did you transform that column in that odd way? Why did you drop those rows? The code says what you did, but not why — making it impossible to trust or modify later.
The cost is high: wasted hours debugging, failed audits, retracted findings, and a reputation for shoddy work. Reproducibility is not optional — it's the foundation of trustworthy data science.
Core concept / mental model
Think of a reproducible notebook as a "stateful recipe" that anyone can execute from scratch and get the same result. It has three pillars:
- Environment: The exact versions of Python and all libraries. This is your "ingredient list."
- Determinism: Every random operation is seeded, so the same inputs always produce the same outputs. This is your "cooking temperature."
- Documentation: The narrative explains why each step exists. This is the "cook's notes."
A non-reproducible notebook is like a recipe that says "add a pinch of salt" — but the pinch depends on who's cooking. A reproducible notebook says "add 2.0 grams of salt" — and even better, it tells you why that amount matters.
In code terms, this mental model translates to three concrete practices:
- Pin your environment with a
requirements.txtorenvironment.ymlfile, and record your runtime versions. - Seed every source of randomness (Python's
random, NumPy, scikit-learn, TensorFlow/PyTorch). - Write code that is self-contained and ordered: no hidden state, no dependency on cell execution order.
How it works step by step
Applying reproducibility isn't a single action — it's a set of habits woven into your notebook workflow. Here's a step-by-step approach you can adopt immediately:
-
Start clean with a kernel of truth — Always start with a fresh kernel, and run your notebook from top to bottom before sharing it. This catches hidden state and order dependencies.
-
Pin your dependencies — Use
pip freeze > requirements.txtor construct aconda env export > environment.yml. This captures the exact versions of every package, fromnumpytoplotly. -
Record your environment at the top — A markdown cell that states what Python version and major libraries you used provides a quick sanity check for anyone running the notebook.
-
Set a global seed — At the very top, after imports, set seeds for
random,numpy.random, and hand them to any ML libraries. This ensures that anything random stays reproducible. -
Seed before every randomness-dependent cell (if needed) — Some libraries, like TensorFlow, need their own seed setting. If you use a data splitting function like
train_test_split, pass the seed to it. -
Document as you go — Write markdown cells that explain the why of each major step: "Drop rows where
ageis NaN because these are likely data entry errors." Future you (and others) will thank you. -
Do a final "clean run" — Restart the kernel and choose Run All. If it fails, fix the issue. If it succeeds, you've got a reproducible notebook.
Hands-on walkthrough
Let's put these principles into practice with a simple, complete example. We'll create a notebook that loads data, does a bit of preprocessing, and fits a random forest model. We'll make it fully reproducible.
Step 1: Pin your environment
First, in terminal, generate a list of your installed packages:
pip freeze > requirements.txt
Or for conda users:
conda env export > environment.yml
The requirements.txt will look something like this (truncated):
numpy==1.24.2
pandas==1.5.3
scikit-learn==1.2.1
matplotlib==3.6.3
Step 2: Start your notebook with a reproducible setup
At the top of your notebook, add a markdown cell describing the environment, and then in the first code cell, set seeds and record versions:
# To make this notebook reproducible, run this cell first.
import random
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# Set all random seeds for reproducibility
random.seed(42)
np.random.seed(42)
# Print versions for reference – matches requirements.txt
print(f"Python {sys.version}")
print(f"numpy {np.__version__}")
print(f"pandas {pd.__version__}")
print(f"scikit-learn {sklearn.__version__}")
Pro tip: In Jupyter, you can also use
%config InlineBackend.figure_format = 'retina'for crisp plots, but that doesn't affect reproducibility.
Expected output (your versions may differ):
Python 3.10.12 | packaged by conda | ...
numpy 1.24.2
pandas 1.5.3
scikit-learn 1.2.1
Step 3: Design your data loading and preprocessing to be self-contained
Avoid loading data from an absolute path that only works on your machine. Instead, use relative paths or a data-loading function with error handling:
import os
def load_and_clean_data(path: str) -> pd.DataFrame:
"""Load CSV and perform minimal cleaning.
Assumes the file is in the same directory as the notebook.
"""
if not os.path.isfile(path):
raise FileNotFoundError(f"Data file {path} not found. Please check the path.")
# Example: load a dataset with a target column 'target'
df = pd.read_csv(path)
# Drop rows with missing target
df = df.dropna(subset=['target'])
# Convert a feature to numeric, forcing errors to NaN
df['numeric_feature'] = pd.to_numeric(df['numeric_feature'], errors='coerce')
# Fill missing values in the numeric feature with the median (document the choice!)
df['numeric_feature'] = df['numeric_feature'].fillna(df['numeric_feature'].median())
return df
# Load data – this will fail loudly if the file is missing
# df = load_and_clean_data('data/my_dataset.csv')
This function is self-contained, documented, and raises a clear error if the file is missing — so troubleshooting is immediate.
Step 4: Build a reproducible model pipeline
Now, split data and train a model, using the global seed:
# Assuming df is loaded from the previous cell
X = df.drop(columns=['target'])
y = df['target']
# Split data; seed the split for reproducibility
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train a random forest with a fixed seed
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
print(f"Train accuracy: {clf.score(X_train, y_train):.3f}")
print(f"Test accuracy: {clf.score(X_test, y_test):.3f}")
Expected output (your numbers will vary by dataset):
Train accuracy: 0.985
Test accuracy: 0.912
Crucially: if you run this cell again, you'll get exactly the same numbers because of random_state=42. That's deterministic reproducibility in action.
Step 5: Save outputs reproducibly
When you export results or figures, use fixed names and (optionally) include a timestamp to prevent overwrites:
from pathlib import Path
output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
# Save the trained model using joblib
import joblib
joblib.dump(clf, output_dir / "model.joblib")
# Save a summary of results as CSV with today's date
summary = pd.DataFrame({'metric': ['test_accuracy'], 'value': [clf.score(X_test, y_test)]})
summary.to_csv(output_dir / f"results_{pd.Timestamp.now().date()}.csv", index=False)
Again, the output is deterministic and saved in a consistent location.
Compare options / when to choose what
Not all reproducibility tools are created equal. Here's a quick comparison to help you choose what fits your project:
| Approach | Pros | Cons | When to use |
|---|---|---|---|
| Requirements.txt | Lightweight, simple, works with pip | Doesn't capture OS-level packages | Small projects, sharing with Python-savvy users |
| Conda environment.yml | Captures environment, not just Python; allows OS packages | Heavier, conda-specific | Projects with complex dependencies (e.g., geospatial, NLP) |
| Docker container | Full isolation of OS + Python + packages | Steeper learning curve, big images | Enterprise deployment, ensuring identical runtime on any server |
| Set random seeds (in code) | Zero extra tooling, immediate effect | Only controls randomness, not environment | Always! As a baseline habit |
nbconvert --execute |
Executes notebook from scratch, catches errors | Need to run manually or in CI | As final verification before sharing |
Rule of thumb: At minimum, always set seeds and include a requirements.txt. For team or production work, graduate to Docker and automated execution checks.
Troubleshooting & edge cases
Even with best practices, things break. Here are the common pitfalls and how to fix them:
-
Problem:
ModuleNotFoundError: No module named 'sklearn'when your colleague runs your notebook. Fix: You forgot to sharerequirements.txt, or they didn't install it. Providerequirements.txtand instruct them to runpip install -r requirements.txt. -
Problem: Your results differ after a kernel restart. Fix: Hidden state is the likely cause. Run the notebook top-to-bottom with Run All to verify. If numbers change, you likely didn't set a seed in the offending cell — add
random.seedorrandom_state. -
Problem: You get different plot dimensions or formatting when others run. Fix: Matplotlib defaults changed across versions. Pin your
matplotlibversion inrequirements.txt, or set explicit figure sizes in code. -
Problem:
FileNotFoundErrorfor a data file. Fix: Usepathliband relative paths, and include aREADMEnoting where the data should be placed. Don't rely on absolute paths. -
Problem: Random seeds don't make everything reproducible. Fix: Some libraries (e.g., TensorFlow) need additional seeds:
tf.random.set_seed(42). Check each library's docs and set seeds before model initialization.
What you learned & what's next
You now understand the core idea of reproducibility: controlling the environment, seeding randomness, and documenting intent. You've completed a hands-on exercise that pins dependencies, sets global seeds, uses self-contained functions, and saves outputs deterministically — that was the core learning objective, and you've nailed it.
Next lesson: In the Data Science with Python track, you'll move on to sharing and presenting your results — turning a reproducible notebook into a compelling story with visualizations and text. The practices you just learned will make your future notebooks reliable, so your audience can trust every chart you show them.
Practice recap
As a mini-exercise, take your most recent notebook and apply these practices: add a markdown cell with environment info, set global seeds, create a requirements.txt, and run the notebook fresh from a clean kernel. If it fails, fix it until 'Run All' succeeds. You'll immediately notice how much more trustworthy your analysis becomes.
Common mistakes
- Forgetting to set a random seed: every run produces different results, making your analysis non-reproducible.
- Using absolute file paths in notebooks, which break when the notebook is shared or moved to another machine.
- Only sharing the notebook without including
requirements.txtorenvironment.yml, so collaborators get different package versions. - Relying on cell execution order and hidden state; running the notebook fresh after a restart often throws
NameErroror changes results. - Not documenting the rationale for data transformations, leaving future readers guessing why certain steps were done.
Variations
- Use
pip-toolsto compilerequirements.ininto a fully pinnedrequirements.txtfor more controlled dependency management. - Wrap your entire analysis in a Python package with
setup.pyand a single entry point script for maximum reproducibility and testability. - Adopt reproducibility tools like
DVCorPachydermto version data and models alongside your code.
Real-world use cases
- An analyst shares a quarterly sales report notebook with the sales team; pinning versions ensures the charts match the original numbers.
- A research group runs the same ML experiment on multiple machines; seeding randomness gives identical training/validation metrics.
- A data platform team audits a model's training pipeline; a reproducible notebook with pinned dependencies and data paths passes compliance checks.
Key takeaways
- Reproducibility rests on three pillars: environment pinning, deterministic randomness, and thorough documentation.
- Always set global seeds for Python, NumPy, and any ML library to ensure identical results across runs.
- Use
requirements.txt,environment.yml, or Docker to capture your exact dependency set and share it with collaborators. - Make your notebook self-contained: avoid absolute paths and hidden state, and verify with a fresh kernel 'Run All'.
- Document the 'why' behind your steps, not just the 'what', to make your analysis trustworthy and maintainable.
- Saving outputs with consistent naming and timestamping prevents overwriting and preserves evidence of your work.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.