Кратко:

22 сентября 2025г. вышла версия 3.10 XGBoost. Основной фишкой новой версии стал "категориальный ре-кодер(categorical re-coder)". Он сохраняет категории в модели и так же может перекодировать данные на этапе инференса. И целью этой статьи является сравнить возможности новой версии XGBoost c лидером обработки категориальных данных, CatBoost.

Основные вопросы:

  • Кто обучает на сырых данных?

  • Что такое этот категориальный ре-кодер?

  • Можно ли обучить модель полностью на сырых данных и получить приемлемый результат?

  • Стал ли XGBoost альтернативой CatBoost для работы с категориальными данными?

Кто обучает на сырых данных?

Так кто же обучает на грязных данных?

Банальный вопрос, банальный ответ. Все и никто одновременно. Это на самом деле деликатный вопрос. Каждый из нас не хочет думать о том, что когда-то написал "говнокод", нарушил pep8 и наконец обучил модель не взглянув на данные.

Это плохо?

Безусловно.

Это экономит время?

В краткосрочной перспективе - безусловно. В долгосрочной - почти никогда.

Надо от этого избавляться?

Конечно, иначе вы не поднимите свою F1-метрику на 0.0001, бизнес потеряет деньги, а вы работу.

Давайте смотреть правде в глаза: мир не идеален, а дедлайны горят. Стремление бросить сырые данные в модель и получить хоть какой-то базовый результат - это не признак лени, а часто прагматичная необходимость быстро оценить потенциал фичей или запустить MVP. Проблема в том, что до недавнего времени для данных с категориями этот путь в XGBoost был усыпан сложностями в кодировке. Вы не могли просто взять и скормить модели столбцы с городами или названиями товаров. Вам приходилось вручную заниматься кодированием - превращать текст в числа с помощью One-Hot Encoding или Ordinal Encoder. One-Hot раздувал размерность до космических масштабов, а Ordinal Encoder заставлял модель думать, что «Великий Новгород» > «Нижний Новгород» просто потому, что вы присвоили им числа 1 и 2, что бессмысленно для деревьев. Это был тот самый ручной труд. И самое главное - вы должны были тщательно следить, чтобы одна и та же схема кодирования применялась к обучающим и тестовым данным, иначе модель уверенно выдавала бы полную ерунду.

Новый категориальный ре-кодер в XGBoost - это и есть тот самый инструмент, который должен избавить нас от рутины обработки данных. Он не поощряет полное незнание своих данных, но он снимает с нас огромный пласт рутины и потенциальных ошибок, позволяя сосредоточиться на действительно важных вещах - feature engineering более высокого уровня и интерпретации результатов.

Итог нашего рассуждения описывает картинка ниже.

Всегда смотрите данные
Всегда смотрите данные

Что такое этот категориальный ре-кодер?

Категориальный ре-кодер в XGBoost - это механизм, который позволяет автоматически обрабатывать категориальные признаки в данных. В отличие от предыдущих версий, где XGBoost не сохранял информацию о кодировке категорий, ре-кодер теперь запоминает как закодированы данные. Он поддерживает категории в виде строк и требует строгой согласованности типов между обучающими и тестовыми данными. Архитектура ре-кодера состоит из двух основных компонентов: система хранения категорий(cat_container.cc, cat_container.cu, cat_container.cuh, cat_container.h) и механизм перекодирования (encoder)

В R поддержка пока отсутствует, и кодировку нужно делать вручную. Более полную информацию по внедрению функционала работы с категориальными данными можете получить вот здесь

Основная проблема в предыдущих версиях заключалась в несогласованности данных: пользователь должен был самостоятельно обеспечивать одинаковую предобработку (например, с помощью OrdinalEncoder из scikit-learn) для обучения и инференса. Без этого модель могла неправильно интерпретировать категории. Ре-кодер решает эту проблему, делая XGBoost более удобным для работы с категориальными данными, особенно в больших пайплайнах машинного обучения. Он снижает время разработки пайплайна, риск ошибок, а также приближает библиотеку к лидеру по работе с категориальными данными CatBoost, где такая обработка встроена изначально.

XGBoost извлекает уникальные категории из входных данных и сохраняет их в контейнере. Во время инференса он автоматически перекодирует новые данные на основе сохранённой схемы, обеспечивая правильность кодирования без копирования данных.

Схема работы XGBoost
Схема работы XGBoost

Извлечение уникальных значений

На первый взгляд извлечение категорий может быть элементарным. Вызываем метод unique() и получаем уникальные значения нужной нам категории, но есть одно 'НО'. В контексте машинного обучения этот процесс требует строгой детерминированности. Порядок возвращаемых уникальных значений может различаться в зависимости от встречаемости. Для XGBoost это не допустимо, поскольку алгоритм полагается на детерминированное кодирование на всех этапах(обучение, валидация, предсказание).

Детерминированные подхода XGBoost состоит из 4 шагов при обучении:

  1. Извлечение уникальных значений из обучающей выборки c сохранением исходного порядка;

  2. Создание отсортированных индексов с помощью алгоритма argsort(C++) - исходный порядок категорий не меняется, но создаются дополнительные индексы для эффективного бинарного поиска;

  3. Фильтрация пропущенных значений и валидация;

  4. Сохранение полного набора категорий внутри модели в специальном контейнере.

Такой подход обеспечивает полную воспроизводимость результатов и корректную работу модели на новых данных, даже если они приходят из разных источников или в измененном порядке.

Проверка корректности категорий

В идеальном мире все категории в новых данных были бы знакомы модели. В реальности же ситуация, когда модель встречает категории, которые не видела при обучении, является нормой. В XGBoost используется строгая валидация категорий, чтобы избежать таких ситуаций. Она реализуется в виде следующих шагов:

Шаг 1: Бинарный поиск категории
Во время ре-кодинга для каждой категории из новых данных выполняется бинарный поиск в отсортированном списке тренировочных категорий.

Шаг 2: Ошибка при неизвестных категориях
Если категория не найдена в тренировочном наборе, то мы получим исключение с сообщением об ошибке.

Думаю, вам уже могла прийти мысль: "Что, если мы хотим сохранить модель? Нам нужно будет сохранять помимо весов еще и список категориальных значений?"

Специальный контейнер CatContainer, который хранит всю информацию о категориальных признаках, является неотъемлемой частью модели, наряду с весами деревьев и параметрами бустинга. Когда вы вызываете стандартные методы save_model() или load_model(), XGBoost автоматически сохраняет или загружает этот контейнер вместе с остальными данными. Это больше не требует от вас никаких дополнительных действий - вы работаете с моделью как с единым целым, а внутри нее уже хранится вся схема кодирования. По сути, модель теперь стала умнее - она помнит не только закономерности в данных, но и то, как сами данные должны выглядеть.

Стал ли XGBoost альтернативой CatBoost для работы с категориальными данными?

Чтобы ответить на этот вопрос объективно, а не расскладывать карты таро, проведём сравнительный эксперимент по обучению моделей XGBoost и CatBoost на различных сырых данных и проанализируем полученные результаты.

Подготовка к эксперименту

Импортируем необходимые библиотеки и функции. В этой статье я не буду детально расписывать работу каждой функции, так как считаю их интуитивно понятными в контексте пайплайна.

# Базовые библиотеки
import pandas as pd 
from tqdm.notebook import tqdm

# Функции обучения моделей
from models.catboost import test_catboost
from models.xgboost import test_xgb_manual_encoding, test_xgb_with_recoder

# Вспомогательные утилиты
from models.utils import (
    calculation_metrics, 
    calculation_info_data, 
    load_data, 
    count_unique_data
)

Теперь определим параметры моделей для честного сравнения. Мы специально сфокусируемся на сравнении встроенных механизмов обработки категориальных признаков, чтобы выявить сильные стороны каждой реализации при работе с сырыми данными.

xgb_params = {
    'n_estimators': 100,        # Количество деревьев    
    'max_depth': 6,             # Максимальная глубина деревьев
    'learning_rate': 0.1,       # Скорость обучения
    'subsample': 0.8,           # Доля данных для каждого дерева
    'colsample_bytree': 0.8,    # Доля признаков для каждого дерева
    'reg_lambda': 1.0,          # L2 регуляризация
    'random_state': 42,         # Seed для воспроизводимости
    'n_jobs': -1,               # Использование всех ядер
    
    # Специфичные для категорий:
    'tree_method': 'hist',      # Метод для построения деревьев
    'max_cat_to_onehot': 5      # Порог для one-hot кодирования
}

catboost_params = {
    'iterations': 100,          # Количество итераций (аналог n_estimators)
    'depth': 6,                 # Максимальная глубина деревьев
    'learning_rate': 0.1,       # Скорость обучения
    'subsample': 0.8,           # Доля данных для каждого дерева
    'colsample_bylevel': 0.8,   # Доля признаков для каждого дерева
    'l2_leaf_reg': 1.0,         # L2 регуляризация
    'random_state': 42,         # Seed для воспроизводимости
    'thread_count': -1,         # Использование всех ядер

    # Специфичные для категорий
    'one_hot_max_size': 5,      # Порог one-hot кодирования категорий
    
    # Выкл отображения в консоли
    'verbose': False            # Отключение вывода обучения
}

Создадим конфигурационный список testing_models для систематического тестирования различных подходов к работе с категориальными данными:

testing_models = [
    {
        "name": "catBoost",
        "model_func": test_catboost,
        "params": catboost_params,
    },
    {
        "name": "xgb_manual_encoder",
        "model_func": test_xgb_manual_encoding,
        "params": xgb_params
    },
    {
        "name": "xgb_with_recoder",
        "model_func": test_xgb_with_recoder,
        "params": xgb_params
    },
]

Обратите внимание: мы создали два варианта XGBoost для сравнения разных стратегий работы с категориальными признаками.

  • xgb_manual_encoder: Традиционный подход с предобработкой через OneHotEncoder и OrdinalEncoder из sklearn

  • xgb_with_recoder: Подход с использованием встроенного категориального ре-кодера XGBoost

Такое разделение позволит нам оценить, насколько автоматизированное решение приближается по качеству к ручной настройке.

Возьмём 7 разнообразных датасетов с Kaggle, охватывающих как задачи классификации, так и регрессии. Это позволит оценить модели в различных условиях и на данных разного масштаба:

Список используемых датасетов:

Каждый датасет описывается словарём с единой структурой:

dataset = {
    "name": "my_dataset",
    "url": "https://www.kaggle.com/competitions/my_dataset",
    "file": "data\\classification_bank_dataset.csv", # локальный путь
    "task": "clf",                          # clf - задча классификации / reg - задача регрессии
    "target": "target_name",                # Целевая переменная 
    "trash_columns": ['id']                 # Фичи, которые явно являются мусорными
}

Словари датасетов хранятся в списке info_datasets

Запускаем сравнительное тестирование всех моделей на всех датасетах:

results = []

pbar_datasets = tqdm(
    info_datasets,
    unit="dataset",
    colour="#037503",
    leave=True
)

for info_dataset in pbar_datasets:
    pbar_datasets.set_description(f"Dataset '{info_dataset['name']}'")

    X_train, X_test, y_train, y_test = load_data(info_dataset)
    info_columns_dataset = calculation_info_data(X_train)

    pbar_models = tqdm(
        testing_models,
        unit="model",
        colour="#09ff00",
        leave=False
    )

    for test_model in pbar_models:
        pbar_models.set_description(f"Processing '{test_model['name']}'")
        # Вызываем функцию модели
        model, y_pred, train_time = test_model["model_func"](
            X_train.copy(), 
            X_test.copy(), 
            y_train.copy(), 
            params=test_model["params"],
            task_type=info_dataset["task"]
        )
        
        metrics = calculation_metrics(y_pred, y_test, info_dataset["task"])
        results.append({
            "model_name": test_model["name"],
            "dataset_name": info_dataset["name"],
            "train_time": train_time,
            **info_columns_dataset,
            **metrics
        })
    pbar_models.close()
pbar_datasets.close()

Вполне простая логика:

  1. Загружаем датасет - разделяем на train/test и автоматически кодируем целевую переменную для классификации

  2. Получаем информацию - анализируем состав признаков (количественные/категориальные)

  3. Обучаем модель - каждая модель получает идентичные данные и параметры для честного сравнения

  4. Предсказываем - получаем предсказания на тестовой выборке

  5. Рассчитываем метрики - accuracy/F1 для классификации, R²/RMSE для регрессии

  6. Повторяем цикл - пока не пройдём по каждому датасету каждой моделью

Такой подход гарантирует, что все модели сравниваются в абсолютно одинаковых условиях.

Результаты эксперимента представлены на таблицах ниже.

Задача регрессии

Датасет

Модель

Время (с)

RMSE

MAE

Flight Delay(33 колонки, 5.7M строк)72.7% числовых

CatBoost

118.36

0.9784

4.34

0.96

XGBoost Manual

57.05

0.9298

7.69

0.76

XGBoost Auto

42.97

0.9144

8.42

0.98

Road Accident Risk(12 колонок, 414K строк)33.3% числовых

CatBoost

4.13

0.8680

0.06

0.04

XGBoost Manual

2.88

0.8701

0.06

0.04

XGBoost Auto

2.56

0.8701

0.06

0.04

All Computer Prices(32 колонки, 80K строк)59.4% числовых

CatBoost

4.12

0.8615

197.99

138.67

XGBoost Manual

1.76

0.8531

205.03

143.60

XGBoost Auto

34.42

0.8203

216.52

149.35

House Prices(79 колонок, 1.2K строк)45.6% числовых

CatBoost

2.64

0.8654

28658.26

17025.43

XGBoost Manual

0.50

0.8984

25020.42

15807.90

XGBoost Auto

0.49

0.8916

25440.36

15584.49

Задача классификации

Датасет

Модель

Время (с)

Accuracy

F1-score

Recall

Precision

Bank Dataset(16 колонок, 600K строк)43.8% числовых

CatBoost

12.08

0.9275

0.6690

0.6040

0.7496

XGBoost Manual

5.58

0.9318

0.6961

0.6434

0.7580

XGBoost Auto

4.61

0.9326

0.7007

0.6502

0.7599

Mental Health(16 колонок, 209K строк)100% категориальных

CatBoost

6.08

0.9256

0.9596

0.9996

0.9227

XGBoost Manual

2.75

0.9618

0.9789

0.9999

0.9586

XGBoost Auto

3.10

0.9551

0.9753

0.9999

0.9518

Life Style Data(53 колонки, 16K строк)73.6% числовых

CatBoost

4.14

0.4868

0.4957

0.5148

0.4780

XGBoost Manual

0.69

0.4985

0.4980

0.5077

0.4887

XGBoost Auto

0.98

0.4963

0.4954

0.5046

0.4865

Результат полученный после обучения наших моделей мы разберём в заключительной главе статьи.

Показательный разбор датасета с сложными категориальными данными

От общего к частному - после масштабного эксперимента с 7 датасетами погрузимся в разбор задачи прогнозирования арестов по данным о преступлениях. Давайте разберём датасет Crime & Consequence: A Metropolitan Dataset.

Посмотрев на количество уникальных значений для каждой фичи, даже опытный
data scientist невольно ахнет. Возникает закономерный вопрос: выживут ли
наши модели в этих условиях без предварительной обработки?

Фича

Уникальных значений

Тип данных

Date

3 022 335

object

Location

839 274

object

Latitude

838 172

float64

Longitude

837 646

float64

Три миллиона уникальных дат - это уже не фича, это хронологический хаос.

Ужасы предобработки
Ужасы предобработки

Вы думаете, XGBoost справится с такой нагрузкой?

Немножко вам времени на размышление...

Ещё немного...

К сожалению вы правы, он не смог справиться. Наша модель застряла на этапе кодирования фич. На изображении можете увидеть этот процесс

Обучение xgboost_recoder на датасете с огромным кол. уникальных значений
Обучение xgboost_recoder на датасете с огромным кол. уникальных значений

И получился вот такой результат, в котором отсутсвует XGBoost с использование ре-кодера.

Датасет

Модель

Время (с)

Accuracy

F1-score

Recall

Precision

Crime & Consequence(19 колонок, 6.7M строк)47.4% числовых

CatBoost

177.37

0.8924

0.7493

0.6367

0.9104

XGBoost Manual

132.41

0.8910

0.7455

0.6321

0.9084

XGBoost Auto (НЕ обучилась)

-

-

-

-

-

Итог обучения показал, что на датасете Crime & Consequence: A Metropolitan Dataset с высоким количеством уникальных значений в категориальных признаках CatBoost демонстрирует не просто стабильность, а абсолютное превосходство. Пока XGBoost с автоматическим кодированием безнадёжно зависал, CatBoost
не только успешно обучился, но и показал лучшие метрики.

Заключение:

Проведя эксперимент, мы можем однозначно ответить на главный вопрос статьи: XGBoost действительно можно считать альтернативой CatBoost для работы с категориальными данными, НО с важными оговорками.

На датасетах малого и среднего размера XGBoost с автоматическим ре-кодером демонстрирует почти магическую способность он действительно обучается на "сырых" категориальных данных, избавляя нас от рутины ручного кодирования. Точность в 95-96% на Mental Health Dataset убедительно доказывает, что теперь можно быстро прототипировать, не погружаясь в тонкости One-Hot Encoding или Ordinal Encoder.

Однако на больших данных с огромным количеством уникальных значений, как в случае с Crime Dataset на 6.7 млн записей, магия исчезает. XGBoost с ре-кодером не смог обучиться за 60 минут, в то время как CatBoost стабильно выдал результат. Это критически важный момент. Автоматизация имеет свои пределы.

Таким образом, для MVP и быстрого прототипирования XGBoost с ре-кодером становится хорошим инструментом, тогда как для эксплуатации на сложных данных CatBoost сохраняет статус непобеждённого лидера.

Итоговый вердикт: да, теперь можно обучать XGBoost на "сырых" категориальных данных и получать приемлемые результаты, но волшебства не существует. XGBoost не перегнал CatBoost, но подарил нам ценную свободу выбора там, где раньше её не было, сделав работу с категориальными признаками более доступной без потери качества на типичных задачах.

Комментарии (0)