Fix "ValueError: Unknown label type: continuous" in scikit-learn

Fix "ValueError: Unknown label type: continuous" in scikit-learn

I ran into this one on a dataset where the target column looked completely reasonable, five clean-looking values, printed fine, no NaNs. The classifier still refused to fit. It turned out one row out of several thousand had a stray decimal in it, and that single row was enough to make scikit-learn treat the entire column as a regression target instead of a set of classes. This article reproduces the error, digs into what type_of_target is actually checking (it is not what most explanations claim), and walks through the two real fixes depending on what your target is supposed to mean.

Reproducing the Error

Any classifier raises this the moment its target array looks, to scikit-learn, like a continuous quantity rather than a set of discrete labels:

import numpy as np
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(0)
X = rng.random((20, 3))
y = rng.random(20)  # a continuous score, 0 to 1

LogisticRegression().fit(X, y)
# ValueError: Unknown label type: continuous. Maybe you are trying to fit a
# classifier, which expects discrete classes on a regression target with
# continuous values.

Scikit-learn 1.9's own message already tells you the likely cause, which is a real improvement over older versions that just said "Unknown label type: 'continuous'" with no hint at all. The unhelpful case is the one this article is actually about: a target that looks discrete, prints like clean integers, and still gets flagged.

What type_of_target Actually Checks

The classification of a target array (binary, multiclass, or continuous) is decided by sklearn.utils.multiclass.type_of_target, and it's worth calling directly, because the rule it applies is narrower than most explanations suggest:

from sklearn.utils.multiclass import type_of_target

X = rng.random((100, 3))
y_classes = np.array([1.0, 2.0, 3.0, 4.0, 5.0] * 20)
print(type_of_target(y_classes))
# multiclass

That's 100 rows and only 5 distinct values, all stored as float64. It's read as multiclass, not continuous. The count of unique values, and the fact that the dtype is float rather than int, both turn out to be irrelevant. What actually matters is whether every value, once cast, is exactly integer-valued.

The One-Bad-Row Trap

Change a single entry in that same array to a non-integer value and the classification flips for the entire array, not just the one row:

y_dirty = y_classes.copy()
y_dirty[0] = 1.5

print(type_of_target(y_dirty))
# continuous

LogisticRegression().fit(X, y_dirty)
# ValueError: Unknown label type: continuous. Maybe you are trying to fit a
# classifier, which expects discrete classes on a regression target with
# continuous values.

99 of 100 values are still perfectly clean class labels. One bad row, likely from a join that introduced an averaged or interpolated value, a unit-conversion slip, or manual data entry, is enough to make scikit-learn conclude the whole column is a regression target. The error message doesn't point at the offending row, so on a real dataset this is the part that costs time: the fix is almost never in the model code, it's in whatever produced the target column.

Where to actually look: y[y != y.astype(int)] (or the pandas equivalent, y[y % 1 != 0]) finds every offending value directly, instead of scanning the dataset by eye.

Why a Pandas NaN-Driven Float Upcast Is a Red Herring

A common assumption is that the dtype itself is the problem, since pandas silently upcasts an integer column to float64 the moment it contains any NaN. That upcast, on its own, does not trigger this error:

import pandas as pd

s = pd.Series([1, 2, 3, None, 1, 2, 3, 2, 1], dtype='float64')
print(s.dtype)
# float64

clean = s.dropna()
print(type_of_target(clean.values))
# multiclass

Once the NaN rows are dropped, every remaining value is still integer-valued, so type_of_target reads it as discrete classes despite the float64 dtype. Chasing the dtype instead of the actual values is a common dead end when debugging this; the dtype upcast that made a column "look wrong" usually isn't the real cause.

Fix 1: It's Genuinely a Regression Problem

If the target is a real continuous quantity, a price, a duration, a probability, the correct fix is to stop fitting a classifier at all:

from sklearn.linear_model import LinearRegression

LinearRegression().fit(X, y)  # y is genuinely continuous, this is correct

This sounds obvious written out, but it's the actual fix in a surprising number of real cases: a target column gets fed into a classifier by habit, or because an earlier version of the pipeline used discrete labels and the target definition changed underneath it without anyone updating the model choice.

Fix 2: Bin It Deliberately

If the underlying question really is a small number of categories, bin the continuous values explicitly rather than relying on the data happening to already look discrete:

from sklearn.preprocessing import KBinsDiscretizer

binner = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='quantile')
y_binned = binner.fit_transform(y.reshape(-1, 1)).ravel()

print(type_of_target(y_binned))
# multiclass

LogisticRegression().fit(X, y_binned)  # works

For fixed, meaningful cutoffs (an amount above or below a specific threshold, for instance) pd.cut() with explicit bin edges is usually clearer than KBinsDiscretizer's automatic quantile or uniform strategies, since the boundaries are ones you chose on purpose rather than ones a stray decimal value could have accidentally implied.

Summary: Which Fix Applies

Situation                                              Fix
────────────────────────────────────────────────      ─────────────────────────────────────────────
Target is genuinely a continuous quantity               Use a regressor, not a classifier
Target should be a small number of categories,           Bin explicitly (pd.cut / KBinsDiscretizer),
  and looks discrete except for a few outliers             don't rely on the data happening to be clean
Error appears on a column that looked fine               Check y[y % 1 != 0] for the actual bad row(s)
Suspect the dtype (float64 from a NaN upcast)             Usually a red herring; check the values, not the dtype