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| Option | Best for | Caveat |
|---|---|---|
joblib.dump | NumPy-heavy estimators | Not portable across library versions |
pickle | Small, simple objects | Same version sensitivity, slower on large arrays |
skops | Sharing models untrusted third parties cannot inject code through | Extra dependency, not supported by every tool |
| ONNX export | Serving outside Python | Some 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,pandasandjoblibin one environment file; a model trained on one minor version may refuse to unpickle on another. random_statemust 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 inputnamed_stepsreaches into the pipeline by name; slicing withmodel[:-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.
Related
Splitting data and cross-validation strategies Feature engineering and text features
Last refreshed 2026-09-18.