diff --git a/supervised/base_automl.py b/supervised/base_automl.py index 5310304b..ab9bd56b 100644 --- a/supervised/base_automl.py +++ b/supervised/base_automl.py @@ -673,7 +673,13 @@ def _handle_drastic_imbalance( ] = new_sensitive_features.loc[j] def _save_data_info(self, X, y, sample_weight=None, sensitive_features=None): - target_is_numeric = pd.api.types.is_numeric_dtype(y) + # Resolve category dtype before numeric check so that targets like + # pd.Series([0, 1, 1]).astype('category') are correctly identified + # as numeric (the categories themselves are numeric). + if isinstance(y, pd.Series) and isinstance(y.dtype, pd.CategoricalDtype): + target_is_numeric = pd.api.types.is_numeric_dtype(y.cat.categories) + else: + target_is_numeric = pd.api.types.is_numeric_dtype(y) if self._ml_task == MULTICLASS_CLASSIFICATION: y = y.astype(str) @@ -792,6 +798,19 @@ def _build_dataframe(self, X, y=None, sample_weight=None, sensitive_features=Non y = check_array(y, ensure_2d=False) y = pd.Series(np.array(y), name="target") + # If y is a pd.Series with category dtype (e.g. user called + # df[col].astype('category') on the target column), convert it to + # the underlying dtype so that downstream code (LightGBM, sklearn, + # dtype checks, etc.) can handle it correctly. + if isinstance(y, pd.Series) and isinstance(y.dtype, pd.CategoricalDtype): + if pd.api.types.is_numeric_dtype(y.cat.categories): + # Numeric categories (e.g. 0, 1, 2 stored as category) — + # restore to the actual numeric dtype. + y = y.astype(y.cat.categories.dtype) + else: + # String/object categories — convert to plain object dtype. + y = y.astype(object) + if sample_weight is not None: if isinstance(sample_weight, np.ndarray): sample_weight = check_array(sample_weight, ensure_2d=False) diff --git a/tests/tests_automl/test_data_types.py b/tests/tests_automl/test_data_types.py index 5ba3a14e..fd463d19 100644 --- a/tests/tests_automl/test_data_types.py +++ b/tests/tests_automl/test_data_types.py @@ -45,3 +45,69 @@ def test_encoding_strange_characters(self): start_random_models=1, ) automl.fit(X, y) + + def test_numeric_category_target(self): + """Regression test: target stored as numeric category dtype should not + raise 'ValueError: pandas dtypes must be int, float or bool'. + See: https://github.com/mljar/mljar-supervised/issues/XXX + """ + X = np.random.rand(self.rows, 3) + X = pd.DataFrame(X, columns=[f"f{i}" for i in range(3)]) + # Binary target with integer values stored as category dtype + y = pd.Series(np.random.randint(0, 2, self.rows)).astype("category") + + automl = AutoML( + results_path=self.automl_dir, + total_time_limit=1, + algorithms=["Baseline"], + train_ensemble=False, + explain_level=0, + start_random_models=1, + ) + automl.fit(X, y) + + def test_numeric_category_features_and_target(self): + """Regression test: both X feature columns and y target stored as + numeric category dtype must be handled without errors. + """ + X = pd.DataFrame( + { + "f0": np.random.rand(self.rows), + # Numeric-valued categorical feature — the exact scenario from the issue + "f1": pd.Series( + np.random.randint(1, 5, self.rows) + ).astype("category"), + "f2": pd.Series( + np.random.randint(0, 3, self.rows) + ).astype("category"), + } + ) + y = pd.Series(np.random.randint(0, 2, self.rows)).astype("category") + + automl = AutoML( + results_path=self.automl_dir, + total_time_limit=1, + algorithms=["Baseline"], + train_ensemble=False, + explain_level=0, + start_random_models=1, + ) + automl.fit(X, y) + + def test_string_category_target(self): + """Regression test: target with string-valued category dtype is handled.""" + X = np.random.rand(self.rows, 3) + X = pd.DataFrame(X, columns=[f"f{i}" for i in range(3)]) + y = pd.Series( + np.random.choice(["cat", "dog"], size=self.rows) + ).astype("category") + + automl = AutoML( + results_path=self.automl_dir, + total_time_limit=1, + algorithms=["Baseline"], + train_ensemble=False, + explain_level=0, + start_random_models=1, + ) + automl.fit(X, y)