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 и валидируйте пайплайн на небольшом поднаборе, чтобы заранее замечать ошибки в настройке.