Knowing what clone does explains why search results, CV and nested pipelines behave as they do.
Meta-estimators (pipelines, searches, cross-validation, ensembles) never fit the object you passed. They call sklearn.base.clone(est), which builds a new, unfitted estimator with the same parameters: it reads get_params(deep=False), recursively clones nested estimators, and calls the constructor again.
Consequences worth knowing:
- After
cross_val_score(model, X, y),modelis still unfitted. Usecross_validate(..., return_estimator=True)to get the fitted fold models. GridSearchCVleaves your original estimator untouched; the trained model issearch.best_estimator_.- Fitted attributes (
coef_,classes_) are never copied byclone; usecopy.deepcopyif you really need a fitted copy. - A
random_stategiven as an integer makes clones reproducible; aRandomStateinstance is shared and advanced between clones, so results differ.
get_params(deep=True) returns nested parameters with __ paths, exactly the names grid search accepts. set_params(**{"step__param": v}) writes them and returns the estimator.
To check fitted state use check_is_fitted(est), which looks for attributes ending in _. Estimators also carry tags (est.__sklearn_tags__()) describing capabilities such as NaN support or whether they need y, which meta-estimators consult.
Example
from sklearn.base import clone
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, cross_validate
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True)
pipe = make_pipeline(StandardScaler(), LogisticRegression(C=0.5))
cross_val_score(pipe, X, y, cv=3)
print(hasattr(pipe[-1], "coef_")) # False (CV fitted clones, not pipe)
res = cross_validate(pipe, X, y, cv=3, return_estimator=True)
print(res["estimator"][0][-1].coef_.shape) # (3, 4)
copy = clone(pipe.fit(X, y))
print(hasattr(copy[-1], "coef_"), copy.get_params()["logisticregression__C"]) # False 0.5
print([k for k in pipe.get_params() if k.startswith("standardscaler__")])
# ['standardscaler__copy', 'standardscaler__with_mean', 'standardscaler__with_std']Remember
- Meta-estimators fit clones, never your original object
clonecopies parameters, not fitted state- Use integer
random_statefor reproducible clones
Watch out for
- Expecting a model passed to cross_val_score to be fitted afterwards.