Thanks to visit codestin.com
Credit goes to github.com

Skip to content

_MultimetricScorer deals with _accept_sample_weights inconsistently #31599

Open
@atharva-kelkar

Description

@atharva-kelkar

Describe the bug

When one of the scorers in _MultimetricScorer is not a Scorer object, it is handled incorrectly.

See line here. If the scorers passed to MultimetricScorer are of the following type: [Scorer, function], it raises an error because the attribute _accept_sample_weight does not exist for the second scorer (function in this case). This (possibly) bug is present only in 1.7.0 since before this, the sample_weight kwarg was being passed to all functions without a check of accepting sample weights.

Possible fix: Use if hasattr(scorer, '_accept_sample_weight') or if isinstance(scorer, _BaseScorer) before checking for the _accept_sample_weight attribute.

Steps/Code to Reproduce

import numpy as np
from sklearn.base import BaseEstimator, RegressorMixin
from sklearn.metrics._scorer import _BaseScorer, _MultimetricScorer

# Step 1: Define a simple estimator
class SimpleEstimator(BaseEstimator, RegressorMixin):
    def fit(self, X, y):
        self.mean_ = np.mean(y)
        return self

    def predict(self, X):
        return np.full(X.shape[0], self.mean_)

# Step 2: Define a custom scorer inheriting from _BaseScorer and a function which estimates score
class SimpleScorer(_BaseScorer):
    def _score(self, method_caller, estimator, X, y_true, sample_weight=None):
        y_pred = method_caller(estimator, "predict", X)
        return self._score_func(y_true, y_pred, **self._kwargs)

def default_score(estimator, X, y, sample_weight=None, **kws):
    return estimator.score(X, y, sample_weight=sample_weight)

def mse(y, y_pred):
    return np.mean((y - y_pred)**2)

# Step 3: Create a _MultimetricScorer with multiple scorers
scorers = {
    "mse": SimpleScorer(mse, sign=1, kwargs={}),
    "default": default_score
}
multi_scorer = _MultimetricScorer(scorers=scorers)

# Step 4: Generate sample data
X = np.random.rand(10, 2)
y = np.random.rand(10)
sample_weight = np.random.rand(10)

# Step 5: Fit the estimator and evaluate using the multi-scorer
estimator = SimpleEstimator()
estimator.fit(X, y)
scores = multi_scorer(estimator, X, y, sample_weight=sample_weight)

print("Scores:", scores)

Expected Results

Expected result would be scores for each of the scorers without an error being raised.

Actual Results

AttributeError                            Traceback (most recent call last)
Cell In[6], line 4
      2 estimator = SimpleEstimator()
      3 estimator.fit(X, y)
----> 4 scores = multi_scorer(estimator, X, y, sample_weight=sample_weight)
      6 print("Scores:", scores)

File ~/anaconda3/envs/scoring-311-2/lib/python3.11/site-packages/sklearn/metrics/_scorer.py:144, in _MultimetricScorer.__call__(self, estimator, *args, **kwargs)
    142     if "sample_weight" in kwargs:
    143         for name, scorer in self._scorers.items():
--> 144             if scorer._accept_sample_weight():
    145                 routed_params[name].score["sample_weight"] = kwargs[
    146                     "sample_weight"
    147                 ]
    149 for name, scorer in self._scorers.items():

AttributeError: 'function' object has no attribute '_accept_sample_weight'

Versions

The `***` below is to obscure some personal details.


System:
    python: 3.11.13 | packaged by conda-forge | (main, Jun  4 2025, 14:52:34) [Clang 18.1.8 ]
executable: /Users/***/anaconda3/envs/***/bin/python3.11
   machine: macOS-15.5-arm64-arm-64bit

Python dependencies:
      sklearn: 1.7.0
          pip: 25.1.1
   setuptools: 80.9.0
        numpy: 2.2.6
        scipy: 1.15.2
       Cython: None
       pandas: 2.3.0
   matplotlib: None
       joblib: 1.5.1
threadpoolctl: 3.6.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 14
         prefix: libopenblas
       filepath: /Users/***/anaconda3/envs/***/lib/libopenblas.0.dylib
        version: 0.3.29
threading_layer: openmp
   architecture: VORTEX

       user_api: openmp
   internal_api: openmp
    num_threads: 14
         prefix: libomp
       filepath: /Users/***/anaconda3/envs/***/lib/libomp.dylib
        version: None

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions