Skip to content

fix(search): make the search a classifier when it wraps a classifier - #127

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/92-search-classifier-api
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/92-search-classifier-api

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #92.

RepresentationSearchCV around a classifier did not behave like a classifier: it had no classes_, predict_proba, predict_log_proba or decision_function, and is_classifier(search) was False. Nested evaluation with a probability-based scorer such as cross_val_score(search, X, y, scoring="roc_auc") scored NaN on every fold, and an outer integer cv used KFold instead of StratifiedKFold.

Root cause

RepresentationSearchCV is a plain BaseEstimator that only delegated predict and score to best_estimator_. It did not take its estimator type from the wrapped estimator, and it did not delegate the rest of the classifier API, as scikit-learn's GridSearchCV does.

Fix

  • RepresentationSearchCV.__sklearn_tags__ takes estimator_type, classifier_tags and regressor_tags from the tags of estimator.
  • predict_proba, predict_log_proba and decision_function transform X with best_preprocessor_ and delegate to best_estimator_. As in BaseSearchCV, they are guarded with available_if (_estimator_has), which checks best_estimator_ after fit and the unfitted estimator before, so hasattr on the search reflects the estimator it wraps.
  • classes_ is a property that returns best_estimator_.classes_.
  • fit records n_features_in_, and feature_names_in_ when every column label of X is a string (scikit-learn's rule). A refit on input without string labels removes stale names.
  • predict, score and the new methods share the transform through _best_representation.

Behaviour changes

  • is_classifier(search) and is_regressor(search) follow the wrapped estimator. An outer cross_val_score / cross_validate with an integer cv therefore splits a classifier search with StratifiedKFold, so its outer folds and scores change. The search's own inner splitting is unchanged.
  • New public methods predict_proba, predict_log_proba and decision_function (each only when the wrapped estimator provides it) and new attributes classes_, n_features_in_ and feature_names_in_. A search around a regressor exposes none of the delegated methods, and predict and score are unchanged.

Documentation

  • RepresentationSearchCV class docstring: the search takes the estimator type of estimator; the Attributes section lists classes_, n_features_in_ and feature_names_in_. Docstrings for the delegated methods and classes_.
  • docs/core_concepts/target_awareness.md ("Searching over representations"): new paragraph saying that the search takes the wrapped estimator's type, delegates classes_, predict_proba, predict_log_proba and decision_function, and can be evaluated in an outer stratified cross-validation with scoring="roc_auc".

Tests

  • test_search_takes_the_estimator_type: a LogisticRegression search is a classifier and a Ridge search a regressor, and check_cv builds a StratifiedKFold for the former. Fails on main, passes here.
  • test_classifier_search_delegates_to_the_best_estimator: on string labels, classes_ is ["neg", "pos"], and predict_proba, predict_log_proba and decision_function equal those of best_estimator_ on the best representation. Fails on main, passes here.
  • test_delegated_methods_follow_the_wrapped_estimator: an SVC() search exposes decision_function but not predict_proba, before and after fit; a Ridge search exposes none of the delegated names. Fails on main, passes here.
  • test_unfitted_classifier_search_raises_not_fitted: before fit there is no classes_, and predict_proba raises NotFittedError. Fails on main, passes here.
  • test_nested_cross_val_score_with_roc_auc: nested cross_val_score with scoring="roc_auc" gives finite scores above 0.9. Fails on main, passes here.
  • test_search_records_the_input_features: n_features_in_ and feature_names_in_ after a DataFrame fit; a refit on an array drops feature_names_in_. Fails on main, passes here.

Reproduction

Output of the script from #92 on this branch:

GridSearchCV            is_classifier=True, predict_proba=True, classes_=[0 1]
RepresentationSearchCV  is_classifier=True, predict_proba=True, classes_=[0 1]
nested CV roc_auc: [0.96286526 0.94967532 0.96631494]

Checks

  • just test: 1444 passed, 59 skipped, 7 xfailed; coverage 93.72% (gate 90%).
  • Minimum dependencies (CI job: Python 3.10, numpy 1.24.4, pandas 2.0.3, scipy 1.10.1, scikit-learn 1.6.0): 1431 passed, 72 skipped, 7 xfailed; scripts/quickstart.py passes.
  • just docs: strict build succeeds.
  • just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).
  • Import direction between subpackages is unchanged.

Merge notes

This change was previously part of #97, which is closed in favour of one pull request per issue.

RepresentationSearchCV subclassed plain BaseEstimator and offered only predict
and score, so a search over a classifier had no classes_, predict_proba,
predict_log_proba or decision_function and is_classifier() was False. Nested
evaluation therefore broke: cross_val_score(search, X, y, scoring="roc_auc")
returned nan for every fold (with only a warning), and the outer split was a
plain KFold instead of a StratifiedKFold.

__sklearn_tags__ now takes the estimator type and classifier / regressor tags
of the wrapped estimator, as scikit-learn's search estimators do.
predict_proba, predict_log_proba and decision_function transform with
best_preprocessor_ and delegate to best_estimator_; they are available only
when the estimator provides them (judged from the estimator before fit and
from best_estimator_ after). classes_ is exposed from best_estimator_. fit
records n_features_in_ from best_preprocessor_ and, following scikit-learn,
feature_names_in_ when X has string column labels. Regression usage is
unchanged.

Fixes #92

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RepresentationSearchCV lacks predict_proba, classes_ and classifier tags, so nested roc_auc CV returns NaN

1 participant