- BrainTools - https://www.braintools.ru -

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели

Вот когда каждый грамм действительно имеет значение: если вам нужно спрогнозировать вес птицы перед продажей, чтобы экономить на кормах и оптимизировать производство. Меня зовут Михаил Чирков, я data scientist в R-Style Softlab и сегодня хочу поделиться с вами кейсом прогнозирования с помощью XGBoost, этот проект мы делали в рамках внедрения BI-системы для птицефабрики. 

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 1

Проект, исходный датасет и предобработка

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

Для решения был выбран алгоритм XGBoost — одна из наиболее мощных и популярных реализаций градиентного бустинга. Технический стек проекта включает в себя Python 3.12, библиотеки для машинного обучения (Scikit-learn, XGBoost, CatBoost), инструменты для работы с данными (pandas, Seaborn) и оптимизации гиперпараметров (Optuna).

Пока изучал датасет из 265 214 строк и 36 колонок с данными за почти четыре года, много узнал о курицах, количестве потребляемого корма и воды, температурах в помещениях, влажности, вентиляции и других параметрах. 

Как часто бывает в реальных проектах, данные оказались далеки от идеала: пропущенные значения, несоответствия типов данных, аномальные показатели. Например, встречались записи с нулевой влажностью и температурой (привет с Марса?). Некоторые колонки, относящиеся к убою и перемещению птицы, были заполнены менее, чем на 20%. 

Предобработка данных в итоге включала: 

  • Очистку от пропущенных значений

df = df.dropna()

Колонки с критически высоким процентом пропущенных значений (более 80%) были удалены из анализа:

df = df.drop(['УбойПродажаГоловы', 'ЗаселениеВес', 'ЗаселениеГоловы', 'УбойВес', 'УбойГоловы', 'ПлановыйУбойВес',        'ПлановыйУбойГоловы', 'РаннийУбойВес', 'РаннийУбойГоловы', 'УбойПродажаВес'], axis=1)

Также была произведена конвертация колонки «Дата» в соответствующий формат:

df['Дата'] = pd.to_datetime(df['Дата'])
  • Построение корреляционной матрицы

preview = df.loc[:, ~df.columns.isin(['IDСерия', 'Дата'])]
corr_matrix = preview.corr()
plt.figure(figsize=(15, 10))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Корреляционная матрица')
plt.show()
  • Обработку аномальных значений. Нулевые и аномально высокие значения рабочей температуры, влажности и вентиляции были заменены на медианные значения (медиана выбрана в связи с тем, что более устойчива к выбросам, чем среднее).

median_value_humid = df['ВлажностьРаб'].median()
median_value_temp = df['ТемператураРаб'].median()
median_value_vent = df['Вентиляция'].median()
df.loc[df['ВлажностьРаб'] == 0, 'ВлажностьРаб'] = median_value_humid
df.loc[df['ТемператураРаб'] == 0, 'ТемператураРаб'] = median_value_temp 
df.loc[df['Вентиляция'] == 0, 'Вентиляция'] = median_value_vent
df.loc[df['ВлажностьРаб'] > 100, 'ВлажностьРаб'] = median_value_humid

Корреляционная матрица, выбор признаков, разделение датасета 

Корреляционная матрица позволяет выявить взаимосвязи между признаками и целевой переменной, в нашем случае целевой переменной была «ВесНарастающийИтог». 

Вот что получилось при построении: 

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 2

Как можно увидеть, большинство признаков слабо коррелируют с целевой переменной, что может говорить о комплексных и нелинейных зависимостях. Кроме того, матрица выявила сильную корреляцию между температурными показателями («ТемператураMax», «ТемператураMin», «ТемператураРаб»). Это неудивительно, но создавало риск мультиколлинеарности, которая может негативно сказаться на работе модели — в итоге за наиболее репрезентативный показатель я взял «ТемператураРаб». Сильная корреляция между «СреднийВесНорма» и «ДеньВыращивания» объясняется тем, что показатель веса, по сути, является производной от дня выращивания. Включение обоих признаков в модель создало бы «утечку данных» (data leakage), что привело бы к переобучению модели и искусственно высоким показателям на тестовых данных, но плохой работе на реальных. Это классическая ловушка в машинном обучении, которую важно было избежать.

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

  • ID

  • Дата

  • ДеньВыращивания

  • ТемператураРаб

  • ВлажностьРаб

  • Вентиляция

  • Корм

  • Вода

  • ГоловыНарастающийИтог

  • Реальный вес (она же — «ВесНарастающийИтог»)

После предобработки датасет был разделен на обучающую (80%) и тестовую (20%) выборки: 

features = ['ДеньВыращивания', 'ТемператураРаб', 'ВлажностьРаб', 'Вентиляция','Корм', 'Вода', 'ГоловыНарастающийИтог']
target = ['Реальный вес']
X = df[features]
y = df[target]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Градиентный бустинг и выбор алгоритмов 

Поскольку перед нами стоит задача регрессии (предсказание веса птиц), в качестве базовой модели (baseline) я выбрал XGBoost со стандартными параметрами. Этот алгоритм отлично справляется с большими массивами данных (в отличие от линейной регрессии или случайного леса), находит сложные нелинейные закономерности между признаками и целевой переменной, а также поддерживает параллельные вычисления — что значительно ускоряет работу.

Для сравнения решил протестировать еще два алгоритма: CatBoostRegressor и стекинговую модель, где в качестве базовой модели выступил Random Forest Regressor, а в роли мета-модели — уже знакомый нам XGBoostRegressor.

Градиентный бустинг — это метод ансамблевого обучения, который строит последовательность слабых моделей (обычно деревьев решений), где каждое последующее дерево «учится» предсказывать градиент ошибки [2] предыдущих деревьев, тем самым уменьшая её. Представьте себе компанию разработчиков, где каждый следующий исправляет баги предыдущего 🙃 — настоящая командная работа!

Основной принцип работы 

  • Первая модель (обычно дерево решений) обучается на исходных данных и делает предсказания. 

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

  • Вторая модель обучается на этих ошибках, пытаясь минимизировать остатки. 

  • Процесс продолжается, и каждая новая модель корректирует ошибки предыдущих. 

  • Итоговое предсказание – сумма предсказаний всех моделей

Теперь коротко о каждом типе моделей. 

XGBoost (Extreme Gradient Boosting). Это оптимизированная версия градиентного бустинга с высокой производительностью. Основные его преимущества по сравнению с обычным градиентным бустингом: использует L1 (Lasso) и L2 (Ridge) регуляризацию, что предотвращает переобучение. Также XGBoost строит деревья решений по одному на каждой итерации, а затем суммирует их прогнозы.

Catboost. Работает по схожему принципу, но отличается следующими моментами: не требует явного кодирования категориальных признаков (one-hot, label encoding) – он сам их преобразует; также использует особый способ построения деревьев, который предотвращает утечку данных и переобучение.

Случайный лес.  Данная модель является ансамблем решающих деревьев, строит несколько деревьев на разных подвыборках данных и усредняет их предсказания (для регрессии). 

Принцип работы:

1. Бутстрэпинг (Bootstrap sampling). Каждое дерево обучается на случайной подвыборке данных с заменой, то есть один и тот же объект может попасть в выборку несколько раз, а другой – не попасть вовсе.

2. Случайное подмножество признаков (Random feature selection). Для каждой вершины дерева случайно выбирается только часть признаков (feature bagging). Это снижает корреляцию между деревьями и повышает устойчивость модели.

3. Обучение множества деревьев (Ensemble of decision trees). Каждое дерево строится самостоятельно, без обрезки глубины (deep trees). Благодаря бутстрэпингу и случайному выбору признаков, деревья получаются разными.

4. Объединение предсказаний (Ensemble learning). Усредняются предсказания всех деревьев.

Базовая настройка моделей и первые результаты

XGBoost

  • Инициализируем стоковую модель XGBRegressor:

XGB_model = XGBRegressor(random_state=42, n_jobs=-1, eval_metric='rmse')

где random_state=42 – задает фиксированное начальное состояние для воспроизводимости, n_jobs=-1 – включает многопоточное обучение (использует все процессорные ядра), eval_metric=’rmse’ – метрика качества (RMSE – корень из средней квадратичной ошибки).

  • Обучим модель и оценим ее качество работы на тестовой выборке

XGB_model.fit(X_train, y_train)
y_pred = XGB_model predict(X_test)
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"R^2: {r2:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"MAE: {mae:.2f}")

Результаты XGBoost

  • R²: 0.99

  • RMSE: 2945.97

  • MAE: 1498.84

CatBoost

cb_model = CatBoostRegressor(silent=True, random_state=22)
cb_model.fit(X_train, y_train)
y_pred = cb_model.predict(X_test)
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)  
r2 = r2_score(y_test, y_pred)
print(f"R^2: {r2:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"MAE: {mae:.2f}")

Результаты CatBoost

  • R²: 0.99

  • RMSE: 2787.50

  • MAE: 1404.66

Стекинговая модель

  • Инициализируем базовую модель

base_models = [('rf', RandomForestRegressor(random_state=42, n_jobs=-1))]
  • Инициализируем мета-модель и сам StackingRegressor

final_model = XGBRegressor(random_state=42, n_jobs=-1, eval_metric='rmse')
stacking = StackingRegressor(estimators=base_models, final_estimator=final_model)
  • Собираем пайплайн, обучаем и тестируем модель

pipeline = Pipeline(steps=[('stacking', stacking)])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
rmse = mean_squared_error(y_test, y_pred, squared=False)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"R^2: {r2:.2f}")
print(f"RMSE: {rmse:.2f}")
print(f"MAE: {mae:.2f}")

Результаты стекинговой модели

  • R²: 0.99

  • RMSE: 3037.86

  • MAE: 1379.53

Оптимизация гиперпараметров с использованием Optuna

Для автоматизации процесса поиска оптимальных параметров и минимизации времени использовалась библиотека Optuna. Задача — снизить MSE за счет подбора ключевых параметров каждой модели.

Optuna позволила автоматически искать значения ключевых гиперпараметров, таких как:

  • n_estimators (число деревьев в модели),

  • learning_rate (шаг обучения),

  • max_depth (глубина деревьев),

  • min_child_weight (минимальный вес узла),

  • subsample (доля данных для построения каждого дерева),

  • colsample_bytree (доля признаков для работы каждого дерева).

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

  • XGBRegressor — 9,7%,

  • CatBoost — 9,3%,

  • StackingRegressor — 10,2%.

Среднее значение веса в нашем датасете составляет 27 860 граммов. Соответственно, даже отклонения до 9-10% находятся в приемлемых пределах и говорят о высоком качестве моделей. 

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 3

Интерпретация важности признаков 

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

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 4

Сравнение прогноза с фактическими данными 

Для теста моделей заказчик предоставил таблицу с партиями, которые на момент формирования базы данных не были закрыты. На них и сделаем прогноз и сравним с фактом. Проделав такие же манипуляции с данными (как с исходным датасетом), сделаем прогноз моделью Catboost:

x = data[features]
y_pred = cb.predict(x)
data['Прогнозный вес'] = y_pred
data['Ошибка'] = abs(data['Прогнозный вес'] - data['Реальный вес'])/data['Прогнозный вес'] * 100

Далее сгруппируем полученный результат и вычислим среднюю ошибку по партии:

dz = data.groupby('ID')['Ошибка'].mean().reset_index()

ID.     Ошибка

589     4.376792

1027   4.554168

2499   3.376872

6395   6.476118

6407   7.115308

6410   4.380329

6411   3.547634

Как видим, ошибка варьируется от 3 до 7%, что довольно неплохо. 

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 5

Подготовка прогнозных данных 

База данных охватывает период с 09.12.2020 по 23.07.2024. Для подготовки набора данных я отфильтровал партии, актуальные на дату 23.07.2024. Используя фильтр по дню выращивания, выбрал 17 партий, где возраст птицы был менее 40 дней, и далее выделил несколько из них для демонстрации прогноза.

Для демонстрации возможностей модели выполнили предварительный расчёт ключевых показателей, включая:

  • Слияние (конкатенацию) данных партий с данными по птичникам, чтобы связать каждую партию с соответствующим птичником.

  • Расчёт падежа по партиям: определение дневных изменений количества голов и среднего значения по птичнику.

  • Заполнение данных смоделированными показателями, учитывающими день выращивания (например, динамика потребления корма и воды, температуры и влажности).

  • Формирование итогового датасета и выполнение прогноза модели по подготовленным данным.

Для каждой выбранной партии производились расчёты:

  • Сортировка данных по ID партии и дню выращивания, а также расчёт дневного падежа (убыль количества голов) с использованием группировки.

  • Расчёт среднего значения падежа для каждой партии.

Далее на основе текущих данных был проведён расчёт:

  • Средних темпов прироста корма и воды.

  • Динамического уменьшения количества голов с фиксированным шагом.

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

На основе известных данных последнего дня (2024-07-22) был сформирован полный датасет, содержащий как актуальные данные, так и прогнозы на следующие несколько дней. Итоговый набор данных сформирован путём объединения фактических и прогнозных данных, что позволяет продемонстрировать результативность модели на выбранных партиях.

Как спрогнозировать вес птицы с помощью XGBoost: от предобработки данных до оптимизации модели - 6

Краткие итоги 

Проект достиг своей основной цели – модель довольно точно прогнозирует вес, о чем свидетельствуют солидные показатели R² и низкий RMSE на тестовой выборке. Существующие ограничения можно преодолеть при поступлении дополнительных данных от заказчика. Перспективным направлением для улучшения точности может стать разработка стекинговой модели, объединяющей предсказания CatBoost, XGBoost и Случайного леса в один ансамбль.  

Модель была сохранена с помощью joblib в формат .pkl, в дальнейшем она может быть интегрирована в рабочий процесс через FastAPI. Данный подход дает возможность развернуть решение в облаке с помощью Docker и настроить автоматизацию, при которой данные будут поступать раз в сутки для моментального прогнозирования веса. 

В дальнейшем проект может быть расширен до прогнозирования не только веса птицы, но и расхода пищи и воды, что сделает решение еще более значимым с точки зрения [3] оптимизации бизнес-процессов.

Автор: R_STYLE_SOFTLAB

Источник [4]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/13566

URLs in this post:

[1] обучения: http://www.braintools.ru/article/5125

[2] ошибки: http://www.braintools.ru/article/4192

[3] зрения: http://www.braintools.ru/article/6238

[4] Источник: https://habr.com/ru/companies/rshb/articles/894260/?utm_campaign=894260&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Рейтинг@Mail.ru
Rambler's Top100