Custom logic that breaks the contract fails subtly in cross-validation and grid search.
To plug your own code into pipelines and searches, write a class that follows the estimator contract:
- Inherit
BaseEstimator(givesget_params,set_params, a repr and HTML display) and a mixin:TransformerMixin(addsfit_transform),ClassifierMixinorRegressorMixin(addscoreand tags). Mixins go beforeBaseEstimatorin the bases. __init__only stores its arguments, unchanged, as attributes with the same names. No validation, no defaults computed from data.cloneandset_paramsrely on this.fit(X, y=None)validates input, learns state into attributes ending in_, and returnsself.transform/predictcheck the estimator is fitted (check_is_fitted(self)) and use only the learned state.
Validate with sklearn.utils.validation.validate_data(self, X, reset=True) in fit and reset=False afterwards: it converts input, records n_features_in_ and feature_names_in_, and checks consistency. Adding OneToOneFeatureMixin gives get_feature_names_out for column-preserving transformers.
sklearn.utils.estimator_checks.check_estimator(MyEstimator()) runs the library's conformance test suite, and parametrize_with_checks turns it into pytest tests. Declare special behaviour such as accepting NaN via __sklearn_tags__.
For a stateless function, a FunctionTransformer is simpler than a class.
Example
import numpy as np
from sklearn.base import BaseEstimator, OneToOneFeatureMixin, TransformerMixin
from sklearn.utils.validation import check_is_fitted, validate_data
class Winsorizer(OneToOneFeatureMixin, TransformerMixin, BaseEstimator):
"""Clip each feature to quantiles learned on the training data."""
def __init__(self, lower=0.05, upper=0.95):
self.lower = lower # store params unchanged, nothing else
self.upper = upper
def fit(self, X, y=None):
X = validate_data(self, X, reset=True)
self.low_, self.high_ = np.quantile(X, [self.lower, self.upper], axis=0)
return self
def transform(self, X):
check_is_fitted(self)
X = validate_data(self, X, reset=False)
return np.clip(X, self.low_, self.high_)
X = np.arange(100.0).reshape(-1, 1)
w = Winsorizer(upper=0.9).fit(X)
print(w.transform([[-50.0], [500.0]]).ravel()) # [ 4.95 89.1 ]
print(w.get_params()) # {'lower': 0.05, 'upper': 0.9}Remember
__init__stores parameters unchanged; all learning happens infit- Learned attributes end in
_;fitreturnsself - Run
check_estimatorto verify the contract
Watch out for
- Validating or transforming parameters in
__init__, which breakscloneandset_params.