scikit-learn
custom classifier
GridSearchCV
machine learning
Python

scikit learn custom classifier compatible with GridSearchCV

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

A custom classifier can work with GridSearchCV, but only if it follows scikit-learn's estimator contract closely. GridSearchCV clones estimators, sets parameters, fits fresh instances, and expects learned state to appear only after fit. Most compatibility bugs come from constructor design, missing learned attributes, or skipping scikit-learn's validation helpers.

Follow the Estimator Contract

The core rules are simple and strict:

  • inherit from ClassifierMixin and BaseEstimator
  • put mixins on the left and BaseEstimator on the right
  • expose tunable parameters in __init__
  • avoid training logic in __init__
  • return self from fit
  • store learned attributes with trailing underscores

According to the scikit-learn developer documentation, BaseEstimator provides parameter handling used by GridSearchCV, and estimator parameters should be explicit __init__ keyword arguments rather than hidden in *args or **kwargs. That is what makes cloning and parameter search work cleanly. Source: Developing scikit-learn estimators, BaseEstimator.

A Minimal Classifier That Works With GridSearchCV

This example implements a simple binary classifier that predicts based on one feature and a threshold. It is not meant to be a strong model. It is meant to show the API shape that GridSearchCV expects.

python
1import numpy as np
2from sklearn.base import BaseEstimator, ClassifierMixin
3from sklearn.utils.multiclass import unique_labels
4from sklearn.utils.validation import check_is_fitted, validate_data
5
6
7class ThresholdClassifier(ClassifierMixin, BaseEstimator):
8    def __init__(self, feature_index=0, threshold=0.5):
9        self.feature_index = feature_index
10        self.threshold = threshold
11
12    def fit(self, X, y):
13        X, y = validate_data(self, X, y)
14        self.classes_ = unique_labels(y)
15        if len(self.classes_) != 2:
16            raise ValueError("ThresholdClassifier supports binary targets only")
17        return self
18
19    def predict(self, X):
20        check_is_fitted(self, "classes_")
21        X = validate_data(self, X, reset=False)
22        mask = X[:, self.feature_index] >= self.threshold
23        return np.where(mask, self.classes_[1], self.classes_[0])

A few details matter here. validate_data checks the input and also sets attributes such as n_features_in_. classes_ is stored during fit, which is part of the standard classifier contract. check_is_fitted prevents prediction before training.

Running GridSearchCV

Once the estimator follows the contract, hyperparameter search looks ordinary.

python
1import numpy as np
2from sklearn.model_selection import GridSearchCV
3
4X = np.array([
5    [0.1, 1.0],
6    [0.3, 0.8],
7    [0.7, 0.2],
8    [0.9, 0.1],
9    [0.6, 0.4],
10    [0.2, 0.9],
11])
12y = np.array([0, 0, 1, 1, 1, 0])
13
14search = GridSearchCV(
15    estimator=ThresholdClassifier(),
16    param_grid={
17        "feature_index": [0, 1],
18        "threshold": [0.25, 0.5, 0.75],
19    },
20    cv=3,
21    scoring="accuracy",
22)
23
24search.fit(X, y)
25print(search.best_params_)
26print(search.best_score_)

If your estimator is cloneable and stateless before fit, this works without special handling.

Designing __init__ Correctly

A surprising number of custom estimators fail because the constructor does too much. __init__ should assign parameters to attributes and stop there. Do not load training data, compute statistics, or create fit-dependent state in the constructor.

That is not just a style preference. GridSearchCV repeatedly clones the estimator and then calls set_params. If your constructor has hidden side effects, grid search can become inconsistent or fail in ways that are hard to debug.

If the estimator needs randomness, expose random_state as a parameter and use it inside fit, not during object creation.

Pipelines and Nested Parameter Names

Compatibility with GridSearchCV also means compatibility with Pipeline. Once the classifier is inside a pipeline, parameter names gain the step prefix.

python
1from sklearn.model_selection import GridSearchCV
2from sklearn.pipeline import Pipeline
3from sklearn.preprocessing import StandardScaler
4
5pipe = Pipeline([
6    ("scale", StandardScaler()),
7    ("clf", ThresholdClassifier()),
8])
9
10search = GridSearchCV(
11    pipe,
12    param_grid={
13        "clf__feature_index": [0, 1],
14        "clf__threshold": [0.25, 0.5, 0.75],
15    },
16    cv=3,
17)

If get_params and set_params work properly through BaseEstimator, this pattern works automatically.

Validate the Estimator Early

Scikit-learn provides check_estimator to catch contract violations early.

python
from sklearn.utils.estimator_checks import check_estimator

check_estimator(ThresholdClassifier())

This is worth running before building a larger training workflow around the custom estimator. It catches problems that would otherwise surface later in cross-validation, serialization, or pipeline composition.

Common Pitfalls

A common mistake is putting data-dependent logic in __init__. That breaks cloning and makes parameter search unreliable.

Another issue is forgetting to expose tunable parameters as explicit constructor arguments. If a parameter is not in __init__, GridSearchCV cannot tune it in the normal way.

Developers also often omit learned attributes such as classes_ or skip fit checks before prediction. That makes the estimator behave unlike built-in classifiers and can break utilities that expect standard attributes.

Finally, be careful with inheritance order. The scikit-learn developer guide explicitly recommends placing mixins such as ClassifierMixin before BaseEstimator for correct method resolution behavior.

Summary

  • 'GridSearchCV compatibility depends on following the scikit-learn estimator API exactly.'
  • Keep __init__ limited to explicit parameter assignment.
  • Store learned state in trailing-underscore attributes during fit.
  • Use validate_data and check_is_fitted to align with current scikit-learn expectations.
  • Test custom estimators with GridSearchCV, Pipeline, and check_estimator early.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.