#34883·scikit-learn

[BUG] `compute_class_weight` coerces class labels to int in 1.9, breaking dict lookup for string labels

Author: TonyBagnallCreated Sep 4, 2026Updated Sep 17, 2026
LabelsBug

Introduce yourself

hi, I am a core developer for the aeon toolkit and an academic at the university of Southampton. I found this bug when trying to raise our upper bound for scikit

https://github.com/aeon-toolkit/aeon/pull/3787

For context, we have a range of algorithms that use scikit components, in this case the ExtraTreeClassifier and RandomForestClassifier. We also maintain a range of data archives including the UCR archive, hosted here

http://timeseriesclassification.com/ Our classification data always loads class values as strings, so for example, "1"/"2" or "Run"/"Jump". The basic problem is that relevant parts of scikit 1.9 now always parse class value strings to integers, leading to dictionary errors.

I had a good look and couldnt see this issue, sorry if its been fixed already.

Describe the bug and give evidence about its user-facing impact

In 1.9, fitting a forest ensemble with class_weight="balanced" raises ValueError when the class labels are strings that happen to parse as integers ("1", "2", ...). The same code works in 1.8 and earlier. Labels that are genuine integers, or strings that do not parse as integers ("a", "b"), are unaffected.

The problem comes from a coercion added to the user-defined-dictionary branch of compute_class_weight in #32644 (commit 9ca766b, "FEA Add array API support for LogisticRegression with LBFGS"):

           for i, c in enumerate(classes):
              try:
                  c = int(c)
              except ValueError:  # `classes` contains strings
                  c = str(c)
              if c in class_weight:

The intent looks like normalising array scalars to Python scalars so that dict lookup by key works under a non-numpy namespace. But int("1") succeeds, so it never reaches the except clause that would have preserved the string.

That branch is reached even though the user asked for "balanced", because BaseForest._validate_y_class_weight does not pass "balanced" straight through. It round-trips (sklearn/ensemble/_forest.py:861):

  # Computing class_weight (dict or list) for the "balanced" option.
  class_weight_k_vect = compute_class_weight("balanced", classes=self.classes_[k], ...)
  class_weight_k = {key: val for (key, val) in zip(self.classes_[k], class_weight_k_vect)}
  ...
  expanded_class_weight = compute_sample_weight(class_weight, y_original)

It first expands "balanced" into an explicit dict keyed by the original labels — {"1": 1.0, "2": 1.0}, string keys — and then feeds that dict back in. The second call goes through the dictionary branch, the keys are coerced to int, every lookup fails, and all classes are reported as unweighted.

Estimators that pass "balanced" directly to compute_class_weight (DecisionTreeClassifier, RidgeClassifierCV, SVC, ...) take the dedicated elif class_weight == "balanced" branch, which uses a LabelEncoder and never coerces, so they are not affected.

Steps/Code to Reproduce

import numpy as np
  from sklearn.ensemble import RandomForestClassifier

  X = np.random.RandomState(0).rand(20, 5)
  y = np.array(["1"] * 10 + ["2"] * 10)   # string labels that parse as integers

  RandomForestClassifier(n_estimators=5, class_weight="balanced").fit(X, y)

Or against compute_class_weight directly, without the forest:

  import numpy as np
  from sklearn.utils.class_weight import compute_class_weight

  compute_class_weight(
      {"1": 1.0, "2": 1.0}, classes=np.array(["1", "2"]), y=np.array(["1", "2", "1"])
  )

Expected Results

Expected Results

Fits without error, as in 1.8.0 and earlier. The direct compute_class_weight call returns array([1., 1.]).

Actual Results

Traceback (most recent call last):
    File "repro.py", line 7, in <module>
      RandomForestClassifier(n_estimators=5, class_weight="balanced").fit(X, y)
    File "sklearn/base.py", line 1403, in wrapper
      return fit_method(estimator, *args, **kwargs)
    File "sklearn/ensemble/_forest.py", line 393, in fit
      y, expanded_class_weight = self._validate_y_class_weight(y, sample_weight)
    File "sklearn/ensemble/_forest.py", line 879, in _validate_y_class_weight
      expanded_class_weight = compute_sample_weight(class_weight, y_original)
    File "sklearn/utils/_param_validation.py", line 191, in wrapper
      return func(*args, **kwargs)
    File "sklearn/utils/class_weight.py", line 236, in compute_sample_weight
      weight_k = compute_class_weight(
    File "sklearn/utils/_param_validation.py", line 191, in wrapper
      return func(*args, **kwargs)
    File "sklearn/utils/class_weight.py", line 117, in compute_class_weight
      raise ValueError(
  ValueError: The classes, [1, 2], are not in class_weight

The message is also misleading: it reports the classes as [1, 2] (post-coercion integers) when the real classes are the strings ["1", "2"], which are the dict keys.

A mixed example shows the failure is per-label and driven purely by whether int() succeeds — with classes ["0", "a"] only "0" is reported:

This effects both ExtraTreesClassifier and RandomForestClassifier

Versions

System:
      python: 3.12.9 [MSC v.1942 64 bit (AMD64)]
     machine: Windows-11-10.0.26200-SP0

  Python dependencies:
        sklearn: 1.9.0
            pip: 26.1.2
     setuptools: 82.0.1
          numpy: 2.4.6
          scipy: 1.18.0
         Cython: None
         pandas: 2.2.3
     matplotlib: 3.10.1
         joblib: 1.4.2
  threadpoolctl: 3.5.0

Interest in fixing the bug

yes I would be happy to, I would not convert strings to ints at all, leave them as string and would not use a try ... except block in this context and avoid the round trip, but would need to look more closely, claude suggests this fix (and has helped me pin this down)

Convert the labels to Python scalars without altering their values, rather than parsing
  them. move_to is already imported and used earlier in the same function:

           for i, c in enumerate(classes):
  -            try:
  -                c = int(c)
  -            except ValueError:  # `classes` contains strings
  -                c = str(c)
  +        # convert to python scalars so that lookup by dict key works for any
  +        # array namespace, without altering the label values themselves
  +        for i, c in enumerate(move_to(classes, xp=np, device="cpu").tolist()):
               if c in class_weight:

  tolist() maps each element to its natural Python type — str stays str, int stays int — and
  both hash-match the user's dict keys, which is what the array-API port needed. Unlike
  int(), it never alters a label's value, so it cannot introduce this class of mismatch for
  any label type.

A narrower guard such as if not isinstance(c, str) around the existing int() call also fixes it, which is probably what I would do, but I obviously dont have the wider picture.

Source: scikit-learn/scikit-learn