Persistence, inspection and reproducibility

joblib versus pickle trade-offs, pinning library versions, set_config and random_state discipline, and inspecting what a fitted pipeline actually learned.

Saving and loading

import joblib
import sklearn

joblib.dump(model, "model_v3.joblib", compress=3)

bundle = {
    "model": model,
    "sklearn": sklearn.__version__,
    "features": list(feature_names),
    "trained_at": "2026-09-18",
    "metrics": {"average_precision": 0.42},
}
joblib.dump(bundle, "model_v3.bundle.joblib")

loaded = joblib.load("model_v3.bundle.joblib")["model"]
print(loaded.predict(df.head()))         # raw DataFrame in, labels out
OptionBest forCaveat
joblib.dumpNumPy-heavy estimatorsNot portable across library versions
pickleSmall, simple objectsSame version sensitivity, slower on large arrays
skopsSharing models untrusted third parties cannot inject code throughExtra dependency, not supported by every tool
ONNX exportServing outside PythonSome transformers have no equivalent operator

A pickle is code, not data: loading one executes what it references. Only load artefacts you produced yourself, and never a file that arrived from an untrusted source.

Pinning and reproducibility

from sklearn import get_config, set_config

set_config(display="diagram")              # notebook rendering of pipelines
print(get_config()["assume_finite"])       # current global settings

# seed every randomised step you own
SEED = 42
set_config(transform_output="pandas")      # keep column names through transformers
  • Pin scikit-learn, numpy, scipy, pandas and joblib in one environment file; a model trained on one minor version may refuse to unpickle on another.
  • random_state must be set on the split, the estimator, and any sampler or search object — one missing seed and the run is not reproducible.
  • Record the data snapshot or query alongside the artefact, not just the code revision.
  • Save the fitted pipeline, never the bare estimator, so preprocessing travels with the model.
⚠️
Unpickling an artefact from an untrusted source can execute arbitrary code. Treat model files like executables: sign them, store them in a controlled location, and prefer safe formats when you must accept a model built elsewhere.

Inspecting a fitted pipeline

import pandas as pd

print(model)
print(list(model.named_steps))                     # step names in order
print(model.named_steps["prep"].transformers_)     # column routing

names = model[:-1].get_feature_names_out()         # everything before the estimator
print(len(names), names[:5])

clf = model.named_steps["clf"]
coefs = pd.Series(clf.coef_[0], index=names).sort_values(key=abs, ascending=False)
print(coefs.head(10))                              # which engineered columns matter

print(model.n_features_in_)                        # what the pipeline expects on input
  • named_steps reaches into the pipeline by name; slicing with model[:-1] gives the preprocessing part alone.
  • get_feature_names_out() resolves one-hot and polynomial expansions back to readable names, which is what makes coefficients and importances interpretable.
  • Compare n_features_in_ and the input schema against the current service payload — a mismatch is the first thing to check when predictions look wrong.
  • Fitted attributes always end with an underscore: coef_, classes_, feature_importances_.

FAQ

joblib or pickle?
Use joblib for anything containing NumPy arrays, which is almost every fitted estimator; it is faster and more compact. Both are equally version-sensitive, and neither is safe to load from an untrusted source.
Why does loading my model fail in production?
Nearly always a library version mismatch or a missing custom transformer class. Pin the versions in both environments, and ship custom classes in an importable package rather than defining them in a notebook.

Splitting data and cross-validation strategies Feature engineering and text features

Last refreshed 2026-09-18.