Setting up the ML Environment (Scikit-Learn, TensorFlow, PyTorch)
What you’ll learn
- why the environment is part of the model, not infrastructure around it
- venv against conda, and the one question that decides between them
- what to pin, what to leave loose, and the difference between a requirements file and a lock file
- a sanity-check script that fails loudly rather than failing at 3 a.m.
- how to record enough that a result can be reproduced a year from now
Why this page exists
A trained model is not just a .joblib.joblib file. It is that file plus the exact library versions
that can deserialise it and reproduce its arithmetic.
Unpickle a scikit-learn model into a different minor version and you get, at best, an
InconsistentVersionWarningInconsistentVersionWarning and slightly different predictions; at worst, an exception. Change
NumPy’s BLAS backend and floating-point sums reassociate, so a k-means run converges to a different
local minimum. Neither of these announces itself.
So: one isolated environment per project, versions pinned, and a script that verifies the environment before you trust anything it produces.
flowchart TD A["A reproducible result"] --> B["The code
(git)"] A --> C["The data
(a versioned snapshot)"] A --> D["The environment
(pinned versions)"] A --> E["The seeds
(random_state everywhere)"] B --> F["Same numbers next year"] C --> F D --> F E --> F
Miss any one of the four and the result is not reproducible. Most people track the first and are surprised by the other three.
venv or conda
One question decides it: do you need non-Python dependencies?
CUDA toolkits, MKL, GDAL, and some compiled scientific libraries are not Python packages, and pip cannot install them properly. Conda can.
venvvenv + pip | conda / mamba | |
|---|---|---|
| Ships with Python | ✅ | ❌ separate install |
| Installs non-Python deps | ❌ | ✅ |
| Speed | Fast | Slower to solve (use mamba) |
| Disk per environment | Small | Large |
| Standard in web/backend | ✅ | ❌ |
| Standard in scientific computing | ❌ | ✅ |
| Reproducible lock | pip freezepip freeze / pip-compilepip-compile | conda env exportconda env export |
flowchart TD
A["New ML project"] --> B{"Need CUDA, MKL,
GDAL or other system libs?"}
B -->|"yes"| C["conda (or mamba)"]
B -->|"no"| D["venv + pip"]
C --> E{"Deep learning?"}
D --> E
E -->|"no"| F["scikit-learn only"]
E -->|"yes — research, flexibility"| G["PyTorch"]
E -->|"yes — production, Keras API"| H["TensorFlow"]
For everything in this module, venvvenv + pip is sufficient. Nothing here needs a GPU.
venv
python -m venv .venv
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txtpython -m venv .venv
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txtconda
conda create -n ml python=3.11
conda activate ml
conda install -c conda-forge scikit-learn pandas matplotlib jupyterlabconda create -n ml python=3.11
conda activate ml
conda install -c conda-forge scikit-learn pandas matplotlib jupyterlabAdd .venv/.venv/ to .gitignore.gitignore. The environment is described by the requirements file, not committed
as files.
The stack
Everything in this module runs on the first five:
| Package | What it is for | Needed for this module |
|---|---|---|
numpynumpy | Arrays and the numeric foundation | ✅ |
pandaspandas | Tabular data | ✅ |
scikit-learnscikit-learn | Models, preprocessing, evaluation | ✅ |
matplotlibmatplotlib | Plotting | ✅ |
scipyscipy | Statistics, sparse matrices, linkage | ✅ |
joblibjoblib | Saving fitted models | ✅ |
jupyterlabjupyterlab | Notebooks for exploration | Recommended |
seabornseaborn | Statistical plots on top of matplotlib | Optional |
torchtorch | Deep learning — research-first | Deep learning module only |
tensorflowtensorflow | Deep learning — production-first, Keras API | Deep learning module only |
Do not install PyTorch or TensorFlow yet. They are large, they pull in GPU dependencies you do not need, and nothing in this module uses them. Add them when you reach the deep learning content.
Pinning: what and how
Three levels, and they are not interchangeable.
Loose — requirements.txtrequirements.txt for humans
numpy
pandas
scikit-learn
matplotlib
scipy
joblib
jupyterlabnumpy
pandas
scikit-learn
matplotlib
scipy
joblib
jupyterlabFine for a tutorial. Useless for reproducibility: pip install -rpip install -r next year installs whatever is
current.
Pinned — direct dependencies
numpy==2.4.6
pandas==3.0.3
scikit-learn==1.9.0
matplotlib==3.11.0
scipy==1.17.1
joblib==1.5.3numpy==2.4.6
pandas==3.0.3
scikit-learn==1.9.0
matplotlib==3.11.0
scipy==1.17.1
joblib==1.5.3Better. Still incomplete — it does not pin transitive dependencies, so a change deep in the tree can still alter your results.
Locked — everything, exactly
pip freeze > requirements.lock.txt # simple, includes transitive deps
# or, better:
pip install pip-tools
pip-compile requirements.in -o requirements.txt # resolved, hash-pinned, with commentspip freeze > requirements.lock.txt # simple, includes transitive deps
# or, better:
pip install pip-tools
pip-compile requirements.in -o requirements.txt # resolved, hash-pinned, with commentsUse requirements.inrequirements.in (loose, human-edited) plus a generated lock file. Commit both. That way you
know why each package is there and exactly which version was used.
Pin the Python version too
.python-version.python-version for pyenv, or the python=3.11python=3.11 in your conda command, or a line in the README.
scikit-learn drops Python versions on a schedule, and “it works on my machine” is very often “my
machine has 3.11 and CI has 3.9”.
See it move
The three levels differ only in what happens on a day you are not looking. The sketch runs a
release timeline: new versions of four packages land over time, and two machines install from the
same repository — one reading a loose requirements.inrequirements.in, one reading a lock file. Every install is a
fresh pip installpip install, exactly as CI does it.
Nothing in the loose panel is a bug. Every version it picks is a legitimate release that its maintainers tested. The failure is that the set is chosen by the calendar rather than by you, so the environment that produced a saved model is no longer reconstructible — and the day it breaks is never the day it drifted.
The sanity check
Write this once, run it whenever an environment is new or something is behaving oddly. The point is that it fails loudly.
"""Verify the ML environment. Exit non-zero if anything is wrong."""
import importlib
import platform
import sys
REQUIRED = {
"numpy": "2.0",
"pandas": "2.0",
"sklearn": "1.4",
"matplotlib": "3.8",
"scipy": "1.11",
"joblib": "1.3",
}
OPTIONAL = ["seaborn", "torch", "tensorflow"]
def version_tuple(v):
"""'1.9.0' -> (1, 9, 0), ignoring any suffix like 'rc1'."""
out = []
for part in v.split("."):
digits = "".join(c for c in part if c.isdigit())
if not digits:
break
out.append(int(digits))
return tuple(out)
def main():
print(f"python {platform.python_version()} ({sys.executable})")
problems = []
for name, minimum in REQUIRED.items():
try:
mod = importlib.import_module(name)
except ImportError:
problems.append(f"{name} is NOT INSTALLED (need >= {minimum})")
print(f" {name:12s} MISSING")
continue
found = getattr(mod, "__version__", "0")
ok = version_tuple(found) >= version_tuple(minimum)
print(f" {name:12s} {found:10s} {'ok' if ok else f'TOO OLD (need >= {minimum})'}")
if not ok:
problems.append(f"{name} {found} < {minimum}")
for name in OPTIONAL:
try:
mod = importlib.import_module(name)
print(f" {name:12s} {getattr(mod, '__version__', '?'):10s} (optional)")
except ImportError:
print(f" {name:12s} {'-':10s} (optional, not installed)")
# Does the stack actually run end to end?
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
X, y = load_iris(return_X_y=True)
score = cross_val_score(LogisticRegression(max_iter=500), X, y, cv=5).mean()
print(f"\nsmoke test: iris 5-fold accuracy {score:.4f}")
if score < 0.9:
problems.append(f"smoke test scored only {score:.4f}")
if problems:
print("\nPROBLEMS:")
for p in problems:
print(f" - {p}")
sys.exit(1)
print("environment OK")
if __name__ == "__main__":
main()"""Verify the ML environment. Exit non-zero if anything is wrong."""
import importlib
import platform
import sys
REQUIRED = {
"numpy": "2.0",
"pandas": "2.0",
"sklearn": "1.4",
"matplotlib": "3.8",
"scipy": "1.11",
"joblib": "1.3",
}
OPTIONAL = ["seaborn", "torch", "tensorflow"]
def version_tuple(v):
"""'1.9.0' -> (1, 9, 0), ignoring any suffix like 'rc1'."""
out = []
for part in v.split("."):
digits = "".join(c for c in part if c.isdigit())
if not digits:
break
out.append(int(digits))
return tuple(out)
def main():
print(f"python {platform.python_version()} ({sys.executable})")
problems = []
for name, minimum in REQUIRED.items():
try:
mod = importlib.import_module(name)
except ImportError:
problems.append(f"{name} is NOT INSTALLED (need >= {minimum})")
print(f" {name:12s} MISSING")
continue
found = getattr(mod, "__version__", "0")
ok = version_tuple(found) >= version_tuple(minimum)
print(f" {name:12s} {found:10s} {'ok' if ok else f'TOO OLD (need >= {minimum})'}")
if not ok:
problems.append(f"{name} {found} < {minimum}")
for name in OPTIONAL:
try:
mod = importlib.import_module(name)
print(f" {name:12s} {getattr(mod, '__version__', '?'):10s} (optional)")
except ImportError:
print(f" {name:12s} {'-':10s} (optional, not installed)")
# Does the stack actually run end to end?
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
X, y = load_iris(return_X_y=True)
score = cross_val_score(LogisticRegression(max_iter=500), X, y, cv=5).mean()
print(f"\nsmoke test: iris 5-fold accuracy {score:.4f}")
if score < 0.9:
problems.append(f"smoke test scored only {score:.4f}")
if problems:
print("\nPROBLEMS:")
for p in problems:
print(f" - {p}")
sys.exit(1)
print("environment OK")
if __name__ == "__main__":
main()Run it:
python check_env.pypython check_env.pyThe sys.exit(1)sys.exit(1) matters. A checker that prints a warning and returns zero will be ignored by CI
and by you.
Reproducibility beyond versions
Pinning is necessary and not sufficient. Four more habits:
Seed everything. Every scikit-learn estimator with randomness takes random_staterandom_state. Set it. Set
it in train_test_splittrain_test_split too. An unseeded split makes every number on the page unrepeatable.
train_test_split(X, y, test_size=0.2, random_state=42)
RandomForestClassifier(n_estimators=200, random_state=42)
KMeans(n_clusters=4, n_init=10, random_state=42)train_test_split(X, y, test_size=0.2, random_state=42)
RandomForestClassifier(n_estimators=200, random_state=42)
KMeans(n_clusters=4, n_init=10, random_state=42)Version the data, not just the code. A hash of the input file, or a snapshot path with a date. “I reran it and got different numbers” is usually a changed input.
Record the environment with the artefact. When you save a model, save the versions next to it:
import json
import platform
import joblib
import sklearn
joblib.dump(model, "model.joblib")
with open("model.meta.json", "w") as fh:
json.dump({
"python": platform.python_version(),
"sklearn": sklearn.__version__,
"trained_at": "2026-08-03", # pass the real timestamp in
"data_snapshot": "orders_2026-07-31.parquet",
"random_state": 42,
}, fh, indent=2)import json
import platform
import joblib
import sklearn
joblib.dump(model, "model.joblib")
with open("model.meta.json", "w") as fh:
json.dump({
"python": platform.python_version(),
"sklearn": sklearn.__version__,
"trained_at": "2026-08-03", # pass the real timestamp in
"data_snapshot": "orders_2026-07-31.parquet",
"random_state": 42,
}, fh, indent=2)Expect the version warning. When you load a model saved by a different scikit-learn version you
will see InconsistentVersionWarningInconsistentVersionWarning. Do not suppress it — it is telling you the predictions may
differ.
CPU or GPU
Short version: you do not need a GPU for this module. Everything here runs on a laptop CPU in seconds.
| Workload | CPU | GPU |
|---|---|---|
| scikit-learn (all of it) | ✅ Fine | Not supported anyway |
| Gradient boosting on ≤ 1M rows | ✅ Fine | Marginal |
| Small neural nets | ✅ Fine | Faster |
| CNNs on real images | ⚠️ Slow | ✅ Necessary |
| Fine-tuning a language model | ❌ | ✅ Necessary |
scikit-learn has no GPU support and does not want any — it parallelises across CPU cores with
n_jobs=-1n_jobs=-1. Use that instead:
cross_val_score(model, X, y, cv=5, n_jobs=-1)
GridSearchCV(pipe, grid, cv=5, n_jobs=-1)
RandomForestClassifier(n_estimators=500, n_jobs=-1)cross_val_score(model, X, y, cv=5, n_jobs=-1)
GridSearchCV(pipe, grid, cv=5, n_jobs=-1)
RandomForestClassifier(n_estimators=500, n_jobs=-1)When you do need a GPU, rent one rather than buying: Colab, Kaggle notebooks, or a cloud instance.
Pitfalls
Installing into the system Python. Two projects will eventually need incompatible versions, and on some Linux distributions you can break the OS package manager. Always create an environment.
pip freezepip freeze on an environment you installed everything into. You end up with 200 lines and no
idea which six you actually asked for. Keep a requirements.inrequirements.in of direct dependencies and generate
the lock from it.
Not pinning the Python version. The most common cause of a green local run and a red CI run.
Installing PyTorch and TensorFlow “just in case”. Several gigabytes, GPU dependencies, and occasionally conflicting NumPy requirements — for libraries this module never imports.
Ignoring InconsistentVersionWarningInconsistentVersionWarning. It means the model was pickled under a different version.
The predictions may be fine. They may not be. Either way you no longer know.
Forgetting seeds. An unseeded train_test_splittrain_test_split makes every metric you quote unrepeatable, and
the difference between two models becomes indistinguishable from the split noise.
Committing the virtual environment. .venv/.venv/ in .gitignore.gitignore, always.
Recap
- The environment is part of the model, alongside code, data and seeds.
venvvenv+ pip unless you need non-Python dependencies; then conda. This module needs onlyvenvvenv.- Five packages carry everything here: numpy, pandas, scikit-learn, matplotlib, scipy.
- Keep a loose
requirements.inrequirements.inand generate a pinned lock file from it. Pin Python too. - Write a
check_env.pycheck_env.pythat exits non-zero — a silent checker is not a checker. - Seed everything, version the data, and store provenance next to the artefact.
- No GPU is needed for this module. Use
n_jobs=-1n_jobs=-1instead.
You unpickle a model saved under scikit-learn 1.4 into an environment running 1.9. What happens?
The pickle format is not a stable interface across versions. Sometimes it loads and behaves the same, sometimes the internals moved and predictions shift, sometimes it raises. That uncertainty is exactly why the version belongs with the artefact.
Show answer
B — You get an InconsistentVersionWarning; it may load and predict slightly differently, or it may break — The pickle format is not a stable interface across versions. Sometimes it loads and behaves the same, sometimes the internals moved and predictions shift, sometimes it raises. That uncertainty is exactly why the version belongs with the artefact.
When is conda the right choice over venv?
That single question decides it. Everything in this module is pure Python plus wheels, so venv and pip are sufficient — and lighter, faster and more standard outside scientific computing.
Show answer
B — When you need non-Python dependencies such as CUDA, MKL or GDAL that pip cannot install properly — That single question decides it. Everything in this module is pure Python plus wheels, so venv and pip are sufficient — and lighter, faster and more standard outside scientific computing.
Why keep requirements.in separately from a generated lock file?
A bare pip freeze gives you 200 pinned lines with no indication which six you chose deliberately. Splitting intent from resolution means you can upgrade deliberately and still reproduce exactly.
Show answer
B — The .in file records WHICH packages you actually asked for; the lock records exactly what got installed, transitive dependencies included — A bare pip freeze gives you 200 pinned lines with no indication which six you chose deliberately. Splitting intent from resolution means you can upgrade deliberately and still reproduce exactly.
Your check_env.py prints a warning about an old package and returns exit code 0. What is wrong?
Automation reads exit codes, not prose. A checker that always succeeds provides false assurance, which is worse than having no checker at all — you will trust results that were produced in a broken environment.
Show answer
B — A zero exit code means CI treats a broken environment as passing, so nobody sees the warning — Automation reads exit codes, not prose. A checker that always succeeds provides false assurance, which is worse than having no checker at all — you will trust results that were produced in a broken environment.
Do you need a GPU to complete this module?
scikit-learn is CPU-only by design. Everything in this module runs in seconds on a laptop. Set n_jobs=-1 on cross_val_score, GridSearchCV and the forest estimators to use all your cores.
Show answer
B — No — scikit-learn has no GPU support at all and parallelises across CPU cores with n_jobs=-1 — scikit-learn is CPU-only by design. Everything in this module runs in seconds on a laptop. Set n_jobs=-1 on cross_val_score, GridSearchCV and the forest estimators to use all your cores.
🧪 Try It Yourself
Exercise 1 – Check the core libraries
Exercise 2 – Compare versions properly
Exercise 3 – Handle an optional dependency gracefully
Exercise 4 – Prove that seeds matter
Exercise 5 – Record provenance with the model
Exercise 6 – Audit an environment against its lock file
Next
That completes Phase 1. Go back to the phase overview for the practice project, or start Phase 2 - Data Preprocessing & Feature Engineering, where the 19 lines of data work in the reference pipeline get unpacked one at a time.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
