2025, Dec 02 12:02

RandomForestClassifier предсказывает одни нули? Причина — one-hot кодирование цели

Разбираем, почему RandomForestClassifier в scikit-learn возвращает одни нули: причина — one-hot кодирование цели. Покажем, как починить пайплайн без смены модели

Если RandomForestClassifier предсказывает одни нули, в большинстве случаев дело не в модели, а в подготовке данных. Частая ошибка — one-hot кодирование целевой переменной для задач многоклассовой классификации. Ниже — краткое объяснение, как это происходит, почему из‑за этого ломаются предсказания и как исправить проблему, не меняя общий конвейер препроцессинга.

Как воспроизвести проблему

Следующий пример повторяет типичный end‑to‑end пайплайн: импутация, кодирование категориальных признаков для фичей, масштабирование числовых признаков и обучение RandomForestClassifier. Ключевой момент: целевая переменная кодируется one-hot до обучения модели.

import pandas as pd
import numpy as np

# Загружаем данные
frame = pd.read_csv("train.csv")
X_tr = frame.iloc[:, 1:-1].values
y_tr = frame.iloc[:, [-1]].values

frame = pd.read_csv("test.csv")
X_te = frame.iloc[:, 1:].values

# Заполняем пропущенные значения
from sklearn.impute import SimpleImputer
miss = SimpleImputer(missing_values=np.nan, strategy="most_frequent")
miss.fit(X_tr[:, :])
X_tr[:, :] = miss.transform(X_tr[:, :])
X_te[:, :] = miss.transform(X_te[:, :])

# Разделяем индексы столбцов по предполагаемому типу
idx_num = []
idx_cat = []
for j in range(len(X_tr[0])):
    if type(X_tr[0][j]) == int or type(X_tr[0][j]) == float:
        idx_num.append(j)
    elif type(X_tr[0][j]) == str:
        idx_cat.append(j)

# One-hot кодируем категориальные признаки
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

pipe_x = ColumnTransformer(
    transformers=[('ohe', OneHotEncoder(), idx_cat)],
    remainder='passthrough'
)
X_tr = np.array(pipe_x.fit_transform(X_tr))
X_te = np.array(pipe_x.transform(X_te))

# One-hot кодирование целевой переменной (источник проблемы)
pipe_y = ColumnTransformer(
    transformers=[('ohe', OneHotEncoder(), [0])],
    remainder='passthrough',
    sparse_threshold=0
)
y_tr = np.array(pipe_y.fit_transform(y_tr))

# Масштабируем числовые признаки
from sklearn.preprocessing import StandardScaler
scale = StandardScaler()
X_tr[:, idx_num] = scale.fit_transform(X_tr[:, idx_num])
X_te[:, idx_num] = scale.transform(X_te[:, idx_num])

# Обучаем модель
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=500, max_depth=25, random_state=42)
forest.fit(X_tr, y_tr)

y_hat = forest.predict(X_te)

# Обратное преобразование предсказанной one-hot цели
enc_y = pipe_y.named_transformers_['ohe']
y_hat_labels = enc_y.inverse_transform(y_hat)

Почему в итоге получаются одни нули

RandomForestClassifier ожидает в качестве цели метки классов. Согласно документации для fit:

Значения цели (метки классов в задачах классификации, вещественные числа — в регрессии).

Передача one-hot закодированной цели превращает одну многоклассовую задачу в формат multi-output или multilabel. Это не то, что нужно в данной ситуации, и это может приводить к вырожденным предсказаниям, например к векторам из нулей, которые затем при inverse_transform сводятся к одному исходу. Коротко: корень проблемы — one-hot кодирование цели.

Как исправить: оставьте y в виде меток

В этом случае не кодируйте y в one-hot для RandomForestClassifier. Оставьте целевую переменную в исходном виде — с метками классов; scikit-learn обработает её корректно.

import pandas as pd
import numpy as np

# Загружаем данные
tbl = pd.read_csv("train.csv")
X_tr = tbl.iloc[:, 1:-1].values
y_tr = tbl.iloc[:, [-1]].values  # Оставляем метки как есть

tbl = pd.read_csv("test.csv")
X_te = tbl.iloc[:, 1:].values

# Импутация
from sklearn.impute import SimpleImputer
imput = SimpleImputer(missing_values=np.nan, strategy="most_frequent")
imput.fit(X_tr[:, :])
X_tr[:, :] = imput.transform(X_tr[:, :])
X_te[:, :] = imput.transform(X_te[:, :])

# Определяем числовые и категориальные столбцы
num_idx = []
cat_idx = []
for k, val in enumerate(X_tr[0]):
    if isinstance(val, int) or isinstance(val, float):
        num_idx.append(k)
    elif isinstance(val, str):
        cat_idx.append(k)

# Кодируем только категориальные признаки
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

enc_x = ColumnTransformer(
    transformers=[('ohe', OneHotEncoder(), cat_idx)],
    remainder='passthrough'
)
X_tr = np.array(enc_x.fit_transform(X_tr))
X_te = np.array(enc_x.transform(X_te))

# Масштабируем числовые признаки
from sklearn.preprocessing import StandardScaler
std = StandardScaler()
X_tr[:, num_idx] = std.fit_transform(X_tr[:, num_idx])
X_te[:, num_idx] = std.transform(X_te[:, num_idx])

# Обучаем классификатор на метках
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=500, max_depth=25, random_state=42)
rf.fit(X_tr, y_tr)

# Предсказываем метки классов напрямую
y_hat = rf.predict(X_te)
print(f"y_hat: {y_hat}")

Если кодировщики признаков встречают в X_te ранее невидимые категории, они завершатся с ошибкой. Убедитесь, что категории, присутствующие в тестовом наборе, покрыты обучающими данными. В примере ниже обучающий фрагмент содержит все классы удобрений, встречающиеся в тестовом, поэтому кодирование проходит без проблем.

id,Temparature,Humidity,Moisture,Soil Type,Crop Type,Nitrogen,Potassium,Phosphorous,Fertilizer Name
0,37,70,36,Clayey,Sugarcane,36,4,5,28-28
1,27,69,65,Sandy,Millets,30,6,18,28-28
2,29,63,32,Sandy,Millets,24,12,16,17-17-17
3,35,62,54,Sandy,Barley,39,12,4,10-26-26
4,35,58,43,Red,Paddy,37,2,16,DAP
5,30,59,29,Red,Pulses,10,0,9,20-20
6,27,62,53,Sandy,Paddy,26,15,22,28-28
7,36,62,44,Red,Pulses,30,12,35,14-35-14
8,36,51,32,Loamy,Tobacco,19,17,29,17-17-17
9,28,50,35,Red,Tobacco,25,12,16,20-20
10,30,45,35,Black,Ground Nuts,20,2,19,28-28
11,25,69,42,Black,Wheat,25,12,26,30-30

Почему это важно

Непреднамеренное преобразование многоклассовой задачи в multilabel или multi-output из‑за кодирования цели может скрыть проблемы во время обучения и привести к вводящим в заблуждение предсказаниям. Для RandomForestClassifier в режиме классификации держите y в виде меток; scikit‑learn корректно обработает цель без ручного one-hot кодирования. Для признаков строковые значения допустимы — OneHotEncoder превратит их в числовые массивы перед подачей в модель. Практический совет при отладке — прогонять конвейер на небольшом, хорошо знакомом наборе данных, чтобы проверить ожидаемое поведение, и убедиться, что обучающие категории покрывают категории в тестовом сплите.

Итоги

Если RandomForestClassifier возвращает столбец нулей или один и тот же класс, сначала проверьте, не кодировали ли вы целевую переменную в one-hot. Решение простое: передавайте в fit вектор меток напрямую и позвольте библиотеке самой обработать цель. One-hot оставляйте для категориальных признаков, следите за покрытием категорий между train и test и валидируйте пайплайн на небольшом поднаборе, чтобы заранее замечать ошибки в настройке.