Reproducible Notebooks Best Practices

Apply reproducibility best practices in notebooks — Data Science with Python.

Focus: apply reproducibility best practices in notebooks

Sponsored

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 NameError or worse, silently different results.
  • Environment drift: You installed package version X six months ago. Your colleague has version Y. The output of pd.read_csv or 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:

  1. Environment: The exact versions of Python and all libraries. This is your "ingredient list."
  2. Determinism: Every random operation is seeded, so the same inputs always produce the same outputs. This is your "cooking temperature."
  3. 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.txt or environment.yml file, 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:

  1. 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.

  2. Pin your dependencies — Use pip freeze > requirements.txt or construct a conda env export > environment.yml. This captures the exact versions of every package, from numpy to plotly.

  3. 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.

  4. 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.

  5. 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.

  6. Document as you go — Write markdown cells that explain the why of each major step: "Drop rows where age is NaN because these are likely data entry errors." Future you (and others) will thank you.

  7. 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 share requirements.txt, or they didn't install it. Provide requirements.txt and instruct them to run pip 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.seed or random_state.

  • Problem: You get different plot dimensions or formatting when others run. Fix: Matplotlib defaults changed across versions. Pin your matplotlib version in requirements.txt, or set explicit figure sizes in code.

  • Problem: FileNotFoundError for a data file. Fix: Use pathlib and relative paths, and include a README noting 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.txt or environment.yml, so collaborators get different package versions.
  • Relying on cell execution order and hidden state; running the notebook fresh after a restart often throws NameError or changes results.
  • Not documenting the rationale for data transformations, leaving future readers guessing why certain steps were done.

Variations

  1. Use pip-tools to compile requirements.in into a fully pinned requirements.txt for more controlled dependency management.
  2. Wrap your entire analysis in a Python package with setup.py and a single entry point script for maximum reproducibility and testability.
  3. Adopt reproducibility tools like DVC or Pachyderm to 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.