Chapter 156 · Tools & Workflow · Challenge Solutions
Reproducibility & Version Control · Solutions
Worked solutions: seed and verify determinism, hash a dataset, scaffold a project, run a real git commit history, and audit a run log for reproducibility.
In [1]:
import subprocess, tempfile, os, textwrap
def sh(*args, cwd):
"Run a shell command in a directory and return its combined output."
r = subprocess.run(args, cwd=cwd, capture_output=True, text=True)
return (r.stdout + r.stderr).strip()
def git(repo, *args):
return sh("git", *args, cwd=repo)
def new_repo():
"Create a fresh, isolated git repository in a temp folder (never touches your real work)."
d = tempfile.mkdtemp(prefix="repro_")
git(d, "init", "-q", "-b", "main")
git(d, "config", "user.name", "Analyst"); git(d, "config", "user.email", "analyst@example.com")
return d
def write(repo, name, text):
with open(os.path.join(repo, name), "w") as f: f.write(textwrap.dedent(text).lstrip("\n"))
print("git", git(tempfile.gettempdir(), "--version").split()[-1] if False else subprocess.run(["git","--version"],capture_output=True,text=True).stdout.strip())
import numpy as np, pandas as pd, hashlib, json
from pathlib import Path
git git version 2.50.1 (Apple Git-155)
Challenge 1 · Prove a function is deterministic¶
Write a seeded random summary and show two calls with the same seed match, while two unseeded calls differ.
In [2]:
def summary(seed=None):
rng = np.random.default_rng(seed)
return round(rng.normal(100, 15, 50).mean(), 4)
print("seed=1 twice :", summary(1), summary(1), "-> equal:", summary(1) == summary(1))
print("no seed twice:", summary(), summary())
seed=1 twice : 99.4588 99.4588 -> equal: True no seed twice: 101.3072 97.7326
Challenge 2 · Detect a changed dataset¶
Hash a dataframe, change one value, and confirm the hash changes.
In [3]:
def dhash(df): return hashlib.sha256(df.to_csv(index=False).encode()).hexdigest()[:12]
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
before = dhash(df); df2 = df.copy(); df2.loc[1, "b"] = 99
print("before:", before, "| after 1-cell edit:", dhash(df2), "| changed:", before != dhash(df2))
before: f67a232f1bb8 | after 1-cell edit: e60319c98c6f | changed: True
Challenge 3 · Scaffold a project with a .gitignore¶
Create the standard folders and a .gitignore that excludes data and outputs.
In [4]:
import tempfile
root = Path(tempfile.mkdtemp())
for f in ["data", "src", "notebooks", "outputs"]: (root / f).mkdir()
(root / ".gitignore").write_text("data/\noutputs/\n.venv/\n")
print("created:", sorted(p.name for p in root.iterdir()))
print(".gitignore:\n" + (root / ".gitignore").read_text())
created: ['.gitignore', 'data', 'notebooks', 'outputs', 'src'] .gitignore: data/ outputs/ .venv/
Challenge 4 · Build a three-commit history¶
Initialize a repo and make three commits, then show the log.
In [5]:
repo = new_repo()
for i, msg in enumerate(["Initial commit", "Add analysis", "Fix a bug"], 1):
write(repo, "work.py", f"version = {i}\n")
git(repo, "add", "."); git(repo, "commit", "-m", msg)
print(git(repo, "log", "--pretty=format:%h %s"))
2b526fc Fix a bug 5275d63 Add analysis 8f2ed7d Initial commit
Challenge 5 · Audit a run for reproducibility¶
Given a run record, report whether all four pillars (code, data, seed, environment) are present.
In [6]:
run = {"git_commit": "a1f3c9", "data_hash": "e3b0c4", "seed": 42, "sklearn_version": "1.3.0"}
pillars = {"code": "git_commit", "data": "data_hash", "seed": "seed", "environment": "sklearn_version"}
missing = [name for name, key in pillars.items() if run.get(key) in (None, "")]
print("reproducible!" if not missing else f"NOT reproducible, missing: {missing}")
reproducible!