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

Проект, исходный датасет и предобработка
Передо мной стояла задача создать модель машинного обучения, которая позволяет предсказывать финальный вес птицы при ежедневном внесении данных и динамически корректировать прогноз с учетом новой информации.
Для решения был выбран алгоритм 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
Корреляционная матрица, выбор признаков, разделение датасета
Корреляционная матрица позволяет выявить взаимосвязи между признаками и целевой переменной, в нашем случае целевой переменной была «ВесНарастающийИтог».
Вот что получилось при построении:

Как можно увидеть, большинство признаков слабо коррелируют с целевой переменной, что может говорить о комплексных и нелинейных зависимостях. Кроме того, матрица выявила сильную корреляцию между температурными показателями («Температура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.
Градиентный бустинг — это метод ансамблевого обучения, который строит последовательность слабых моделей (обычно деревьев решений), где каждое последующее дерево «учится» предсказывать градиент ошибки предыдущих деревьев, тем самым уменьшая её. Представьте себе компанию разработчиков, где каждый следующий исправляет баги предыдущего — настоящая командная работа!
Основной принцип работы
-
Первая модель (обычно дерево решений) обучается на исходных данных и делает предсказания.
-
Ошибка первой модели вычисляется как разница между истинными значениями и предсказанными.
-
Вторая модель обучается на этих ошибках, пытаясь минимизировать остатки.
-
Процесс продолжается, и каждая новая модель корректирует ошибки предыдущих.
-
Итоговое предсказание – сумма предсказаний всех моделей
Теперь коротко о каждом типе моделей.
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% находятся в приемлемых пределах и говорят о высоком качестве моделей.

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

Сравнение прогноза с фактическими данными
Для теста моделей заказчик предоставил таблицу с партиями, которые на момент формирования базы данных не были закрыты. На них и сделаем прогноз и сравним с фактом. Проделав такие же манипуляции с данными (как с исходным датасетом), сделаем прогноз моделью 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%, что довольно неплохо.

Подготовка прогнозных данных
База данных охватывает период с 09.12.2020 по 23.07.2024. Для подготовки набора данных я отфильтровал партии, актуальные на дату 23.07.2024. Используя фильтр по дню выращивания, выбрал 17 партий, где возраст птицы был менее 40 дней, и далее выделил несколько из них для демонстрации прогноза.
Для демонстрации возможностей модели выполнили предварительный расчёт ключевых показателей, включая:
-
Слияние (конкатенацию) данных партий с данными по птичникам, чтобы связать каждую партию с соответствующим птичником.
-
Расчёт падежа по партиям: определение дневных изменений количества голов и среднего значения по птичнику.
-
Заполнение данных смоделированными показателями, учитывающими день выращивания (например, динамика потребления корма и воды, температуры и влажности).
-
Формирование итогового датасета и выполнение прогноза модели по подготовленным данным.
Для каждой выбранной партии производились расчёты:
-
Сортировка данных по ID партии и дню выращивания, а также расчёт дневного падежа (убыль количества голов) с использованием группировки.
-
Расчёт среднего значения падежа для каждой партии.
Далее на основе текущих данных был проведён расчёт:
-
Средних темпов прироста корма и воды.
-
Динамического уменьшения количества голов с фиксированным шагом.
Итоговые данные были дополнены необходимыми категориальными признаками, после чего выполнен прогноз веса. Прогнозируемый вес для каждой партии был рассчитан моделью, а полученные данные дополнены колонкой с прогнозной датой.
На основе известных данных последнего дня (2024-07-22) был сформирован полный датасет, содержащий как актуальные данные, так и прогнозы на следующие несколько дней. Итоговый набор данных сформирован путём объединения фактических и прогнозных данных, что позволяет продемонстрировать результативность модели на выбранных партиях.

Краткие итоги
Проект достиг своей основной цели – модель довольно точно прогнозирует вес, о чем свидетельствуют солидные показатели R² и низкий RMSE на тестовой выборке. Существующие ограничения можно преодолеть при поступлении дополнительных данных от заказчика. Перспективным направлением для улучшения точности может стать разработка стекинговой модели, объединяющей предсказания CatBoost, XGBoost и Случайного леса в один ансамбль.
Модель была сохранена с помощью joblib в формат .pkl, в дальнейшем она может быть интегрирована в рабочий процесс через FastAPI. Данный подход дает возможность развернуть решение в облаке с помощью Docker и настроить автоматизацию, при которой данные будут поступать раз в сутки для моментального прогнозирования веса.
В дальнейшем проект может быть расширен до прогнозирования не только веса птицы, но и расхода пищи и воды, что сделает решение еще более значимым с точки зрения оптимизации бизнес-процессов.
Автор: R_STYLE_SOFTLAB