Reproducible Machine Learning with Scikit-learn: Pipelines, Seeds, and Checks
Reproducibility is the difference between a one-off experiment and a result you can stand behind. In this guide, you will learn how to build a scikit-learn reproducible pipeline that is deterministic by construction, easy to rerun, and straightforward to audit. The focus is on practical steps: fixing randomness, wiring preprocessing and modeling into a single Pipeline, recording environment state, and checking that your output actually matches what you expect.
The examples assume a standard scikit-learn workflow, but the principles apply whether you are training on tabular data, survival outcomes, or any other supervised task. Before you start, inspect the scikit-learn skill page to confirm the API and options you intend to use.
Why a seed is necessary but not sufficient
Setting random_state on every stochastic component is the first and most important step. It makes the same code produce the same weights, splits, and feature selections on the same machine. However, recording the seed alone does not guarantee identical results across machines, hardware accelerators, or library versions. NumPy, PyTorch, and scikit-learn all accept explicit seeds — for example numpy.random.seed, torch.manual_seed, and sklearn's random_state — but cross-hardware and cross-version reproducibility requires pinning versions and, ideally, a workflow manager.
If your project involves survival analysis, the same caution applies: scikit-survival has its own stochastic components that also need random_state where supported. Treat every estimator you touch as a potential source of nondeterminism until you confirm otherwise.
Step 1: Pin your environment
Before writing code, capture the versions that produced your baseline. Record the Python version, scikit-learn version, and any deep-learning or data-handling libraries you rely on. A requirements file or environment YAML is the practical artifact here.
If you later move to a cluster or a container, pinning versions is what keeps the new run comparable to the old one. There is no magic flag for this — it is bookkeeping, and it is the cheapest insurance you have.
Step 2: Build a single Pipeline
The core of a reproducible pipeline is to combine preprocessing and modeling into one Pipeline object. This prevents data leakage between train and test and makes the entire transform-plus-fit operation a single, rerunnable unit.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(random_state=42, max_iter=1000)),
])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
Key settings to fix:
random_stateontrain_test_split, the estimator, and any feature-selection step.max_iteror equivalent convergence limits, so early-stopping behavior does not drift.n_jobsandverboseif you care about consistent timing or logging.
Avoid fitting scalers or selectors outside the pipeline, and never transform the test set with statistics computed on the full dataset. Both are common sources of irreproducibility in the sense that results will not generalize or rerun.
Step 3: Lock down external randomness
If you use NumPy for data augmentation, sampling, or custom shuffling, call numpy.random.seed (or, preferably, a np.random.Generator with a fixed seed) before the training call. If your pipeline wraps PyTorch models, call torch.manual_seed and set the CUDA seed as well — GPU kernels can introduce nondeterminism even when CPU code is deterministic.
Record the seed in the same artifact as your model metrics: a results log, a JSON metadata file, or a run summary. A seed without a version record is useless, because the same seed with a different library version may produce a different number.
Step 4: Version your data and code
Randomness is only half the problem. If the training data changes even slightly, your pipeline will change too, and no seed will explain the difference. Version your code with Git, and version your data wherever possible. For large or hierarchical datasets, HDF5 is a practical choice: it is a binary format for large hierarchical datasets with chunking and compression, which makes it efficient for repeated reads during training.
In single-cell work, AnnData (.h5ad) builds on HDF5 to store annotated single-cell data matrices together with observation and variable metadata. If you are working in that space, treat the .h5ad file as a first-class input and version it alongside your code.
Step 5: Automate reruns with a workflow manager
When your analysis grows beyond a single notebook, define the steps as rules with declared inputs and outputs. Snakemake and Nextflow are workflow managers that define multi-step analyses as rules/processes with declared inputs and outputs, enabling reruns, parallel execution, and portable pipelines.
A well-written rule set makes reproducibility structural rather than manual: rerunning the workflow from scratch should regenerate identical outputs if the inputs and environment are unchanged. This is also where you record the full command line and environment state as part of each rule's output metadata.
Checks before you trust the result
Before treating any metric as a finding, run through this checklist:
- Rerun from a cold start. Delete your outputs, clear any caches, and rerun the full pipeline. The numbers should match the original within rounding.
- Verify the seed is actually in effect. Change
random_stateto a different value and confirm the results change; then restore it and confirm they return to the original. - Check for data leakage. Ensure no test-derived statistic entered preprocessing, and that imputation, scaling, and encoding were fit only on training data inside the pipeline.
- Confirm version pinning. Reinstall from your pinned requirements file and confirm the scikit-learn version matches what you recorded.
- Test portability. If possible, run the same inputs on a different machine or container and compare outputs; document any differences rather than ignoring them.
- Interpretability sanity check. Use a model-agnostic explainer such as shap to confirm the features driving predictions are sensible and stable across reruns.
If any of these checks fails, treat the result as untrusted and investigate the failure point before reporting anything.
Common pitfalls
- Treating a fixed seed as proof of reproducibility. It is necessary but not sufficient across hardware and library versions.
- Fitting preprocessing outside the pipeline, which silently leaks test information.
- Overlooking nondeterministic steps in custom code, data loaders, or GPU operations.
- Forgetting to version the data, so a "different result" is actually a different input.
- Assuming default settings are stable. Convergence limits, parallelism, and solver choices can all shift outcomes.
Where to verify before you run
Each of the tools discussed here has detailed, version-specific documentation that should be checked before you rely on it. Inspect the source documentation on the linked ScienceSkills skill pages: scikit-learn, shap, and scikit-survival. For help locating documentation, environment setup, or example projects, the get-available-resources skill page is a good starting point.
Note: this article is a planning guide, not a benchmarked execution report. It describes recommended settings and checks, but actual behavior depends on your versions, data, and hardware. Verify against the linked skill pages and your own reruns before trusting any result.