Инженерный блог

Борьба с фудкостом в донерной: Как 4 точки фастфуда победили списание лаваша и мяса с помощью предсказания спроса

20.08.2026
Шарафутдинов Р.

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

Представьте типичную картину в сети из 4 точек популярного стритфуда в Алматы (одна точка на Саяхате возле автовокзала, две у студенческих общежитий КазНУ и КазНТУ на Тимирязева и Сатпаева, и еще одна возле Центрального стадиона на Абая). В половине седьмого утра шеф-донерщик Ермек заходит на кухню. Перед ним стоят два пути, и оба ведут к финансовой катастрофе:

Если Ермек перестрахуется и насадит на вертикальный гриль огромный 45-килограммовый вертел маринованной курицы, а к обеду внезапно зарядит проливной алматинский дождь и студенты останутся в корпусах — к полуночи 18 килограммов обжаренного, заветренного мяса отправятся в утиль (по СанПиНу повторно разогревать и замораживать готовое мясо строго запрещено). Туда же полетят 80 упаковок сухого, ломкого лаваша.

Если же Ермек решит сэкономить и поставит скромный 20-килограммовый вертел, а на Центральном стадионе неожиданно назначат вечерний матч «Кайрата» или у студентов начнется сессионный марафон — уже в 18:30 мясо на вертеле превратится в голую металлическую палку. Разъяренные голодные гости развернутся и уйдут к конкурентам, а сеть потеряет 160 000 тенге чистой выручки за один вечер.

Владелец сети ежемесячно списывал в мусор продуктов на 1 850 000 тенге, а фудкост (доля себестоимости продуктов в выручке) пробивал потолок в 41.8% при здоровой норме фастфуда в 28–30%.

Мы подошли к задаче чисто по-инженерному: заменили интуицию поваров на машинное обучение временных рядов в Google Cloud BigQuery ML (ARIMA_PLUS_XREG) и связали его с прогнозом погоды, календарем городских событий и Telegram-ботом кухни. Затраты на облачную инфраструктуру составили $4.15 в месяц, а фудкост сети рухнул до 29.2%. Ниже — подробный инженерный разбор того, как мы это сделали.

1. Почему фастфуд теряет миллионы: Проблема «длинного плеча» заготовок

Главная трудность донерного бизнеса кроется в технологическом процессе маринования и дефростации (разморозки):

  1. Lead Time маринования мяса (12–14 часов): Нельзя просто так достать филе из морозилки и через 10 минут поставить на вертел. Мясо нарезается тонкими слайсами, массируется со специями и зреет в рассоле минимум 12 часов. Решение о том, сколько мяса жарить завтра, повар обязан принять сегодня вечером;
  2. Хрупкость тонкого армянского лаваша: Свежий лаваш при контакте с воздухом теряет эластичность за 6–8 часов. Если вскрыть лишние пачки, лаваш при сворачивании донера рвется, соус вытекает, а клиент требует возврат денег;
  3. Эффект локационных аномалий Алматы: В точке на Саяхате трафик напрямую зависит от задержек междугородних автобусов и пробок на Райымбека; на Тимирязева — от расписания пар и сессий; на Центральном стадионе — от расписания футбольной премьер-лиги и концертов.

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

2. Архитектура предиктивного пайплайна: BigQuery ML + Cloud Scheduler + Telegram

Вместо развертывания тяжелых Python-кластеров с Pandas и Scikit-learn мы реализовали всю аналитику и ML прямо внутри хранилища данных Google Cloud BigQuery:

┌─────────────────────────────────────────────────────────────┐
│ 1. Источники сырых данных (Data Ingestion)                  │
│ - Чеки и модификаторы из кассовой системы (iiko / Syrve)    │
│ - OpenWeather API (Осадки, температура, сила ветра)         │
│ - Календарь матчей, концертов и сессий вузов (КазНУ/Сатпаев)│
└──────────────────────────────┬──────────────────────────────┘
                               │ Потоковая загрузка (Streaming)
                               ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Озеро данных и модель BigQuery ML                        │
│ - Модель: ARIMA_PLUS_XREG (Авто-подбор сезонности, праздники)│
│ - Разрешение: Почасовое (Hourly) по 4 филиалам              │
│ - Обучение: 180 дней истории продаж (1.4 млн транзакций)    │
└──────────────────────────────┬──────────────────────────────┘
                               │
            ┌──────────────────┴──────────────────┐
            │ Ежедневный триггер в 06:00 (Cron)   │ Расчет техкарты
            ▼                                     ▼
┌──────────────────────────────┐    ┌──────────────────────────────┐
│ Google Cloud Scheduler       │    │ Cloud Run Functions (Python) │
│ - Запуск инференса модели    │    │ - Пересчет донеров в кг мяса │
│ - Формирование JSON-плана    │    │ - Добавление буфера упека 30%│
│ - Проверка аномалий спроса   │    │ - Telegram бот шеф-поваров   │
└──────────────────────────────┘    └──────────────────────────────┘

3. Исходный код SQL модели ARIMA_PLUS_XREG и Python генератора заготовок

Алгоритм ARIMA_PLUS_XREG в BigQuery ML автоматически раскладывает временной ряд на тренд, недельную и суточную сезонность, учитывает государственные праздники Республики Казахстан и влияние внешних факторов:

import os
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, Any, List
from google.cloud import bigquery
from google.cloud import secretmanager
import requests

# Инициализация BigQuery клиента для прогнозирования спроса в общепите
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] [DONER-ML] %(message)s")
logger = logging.getLogger("doner_forecasting")

PROJECT_ID = os.getenv("GCP_PROJECT_ID", "almaty-doner-fastfood-ml")
DATASET_ID = "fastfood_analytics"
bq_client = bigquery.Client(project=PROJECT_ID)

SQL_TRAIN_ARIMA_MODEL = """
CREATE OR REPLACE MODEL `fastfood_analytics.doner_demand_arima_model`
OPTIONS(
  model_type = 'ARIMA_PLUS_XREG',
  time_series_timestamp_col = 'order_hour',
  time_series_data_col = 'doner_portions_sold',
  time_series_id_col = 'branch_id',
  auto_arima = TRUE,
  data_frequency = 'HOURLY',
  holiday_region = 'KZ'
) AS
SELECT
  TIMESTAMP_TRUNC(order_created_at, HOUR) AS order_hour,
  branch_id,
  SUM(quantity) AS doner_portions_sold,
  ANY_VALUE(is_rainy) AS is_rainy,
  ANY_VALUE(is_student_exam_period) AS is_student_exam_period,
  ANY_VALUE(is_stadium_match_day) AS is_stadium_match_day,
  ANY_VALUE(traffic_congestion_index) AS traffic_congestion_index
FROM
  `fastfood_analytics.pos_orders_enriched`
WHERE
  order_created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 180 DAY)
GROUP BY
  order_hour, branch_id;
"""

SQL_FORECAST_TOMORROW = """
SELECT
  branch_id,
  forecast_timestamp,
  ROUND(forecast_value, 0) AS predicted_portions,
  ROUND(prediction_interval_lower_bound, 0) AS safe_min_portions,
  ROUND(prediction_interval_upper_bound, 0) AS peak_max_portions
FROM
  ML.FORECAST(
    MODEL `fastfood_analytics.doner_demand_arima_model`,
    STRUCT(24 AS horizon, 0.90 AS confidence_level),
    (
      SELECT
        branch_id,
        future_hour AS order_hour,
        is_rainy,
        is_student_exam_period,
        is_stadium_match_day,
        traffic_congestion_index
      FROM
        `fastfood_analytics.future_exogenous_factors`
      WHERE
        future_hour BETWEEN TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
        AND TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 2 DAY)
    )
  )
ORDER BY
  branch_id, forecast_timestamp ASC;
"""

def generate_morning_prep_plan(forecast_records: List[Dict[str, Any]]) -> Dict[str, Any]:
    """
    Расчет технологической карты заготовок мяса и лаваша на смену:
    - 1 классический донер = 120 г мяса (с учетом упека 30% требуется 172 г сырого маринованного мяса)
    - 1 донер = 1 тонкий лаваш + 10% резерв на брак/рваный лаваш
    """
    total_portions = sum(item["predicted_portions"] for item in forecast_records)
    raw_meat_kg_required = round(total_portions * 0.172, 1)
    lavash_packs_required = int(total_portions * 1.10)
    
    plan = {
        "calculated_at": datetime.utcnow().isoformat(),
        "total_predicted_doners": int(total_portions),
        "raw_chicken_meat_kg": raw_meat_kg_required,
        "lavash_units_to_defrost": lavash_packs_required,
        "peak_rush_hours": [
            item["forecast_timestamp"].strftime("%H:00") 
            for item in forecast_records if item["predicted_portions"] > 35
        ]
    }
    logger.info(f"📊 План заготовок сформирован: {plan['total_predicted_doners']} донеров, {plan['raw_chicken_meat_kg']} кг мяса, {plan['lavash_units_to_defrost']} лавашей.")
    return plan

def send_telegram_dispatch_to_kitchen(telegram_bot_token: str, chat_id: str, plan: Dict[str, Any]):
    """
    Отправка утреннего задания шеф-повару донерной в 07:00 утра.
    """
    message = (
        f"🌯 *УТРЕННИЙ ПЛАН ЗАГОТОВОК (ИИ BIGQUERY ML)*\n"
        f"📅 Смена: {datetime.now().strftime('%d.%m.%Y')}\n\n"
        f"🎯 Прогноз продаж: *{plan['total_predicted_doners']} шт.*\n"
        f"🥩 Насадить на вертел (сырое мясо): *{plan['raw_chicken_meat_kg']} кг*\n"
        f"🫓 Разморозить лаваша: *{plan['lavash_units_to_defrost']} шт.*\n"
        f"🔥 Часы пик (наплыв гостей): {', '.join(plan['peak_rush_hours']) or 'Равномерно'}\n\n"
        f"💡 _Рекомендация:_ Замариновать второй вертел к 16:30 из-за вечернего матча на стадионе!"
    )
    url = f"https://api.telegram.org/bot{telegram_bot_token}/sendMessage"
    requests.post(url, json={"chat_id": chat_id, "text": message, "parse_mode": "Markdown"})
    logger.info("✅ Утренний план отправлен в Telegram кухни.")

Смотреть код на GitHub (OZAT-kz)

Технические детали алгоритма:

  • Параметр holiday_region = 'KZ': BigQuery автоматически знает все официальные выходные, праздники Наурыз, Курбан-айт и перенесенные дни отдыха в Казахстане, мгновенно корректируя ожидаемый трафик;
  • Экзогенные регрессоры (XREG): Флаги is_rainy (дождь снижает пешеходный трафик на 38%), is_student_exam_period (рост ночных заказов на 65%) и is_stadium_match_day (всплеск на 140% за 2 часа до игры);
  • Конвертация в сырье с коэффициентом упека: Модель выдает прогноз в штуках готовых донеров. Скрипт на Python автоматически пересчитывает их в килограммы сырого маринованного мяса с учетом 30%-й потери массы при обжарке на гриле и вычисляет точное число пачек лаваша с 10%-м страховым резервом.

4. Конфигурация запуска в Cloud Scheduler и Cloud Run

Для автоматического утреннего пересчета используется бессерверная связка Cloud Scheduler и контейнера Cloud Run с минимальным масштабом 0:

apiVersion: v1
kind: ConfigMap
metadata:
  name: doner-ml-forecast-cron-config
  namespace: fastfood-production
data:
  CRON_SCHEDULE: "0 6 * * *" # Запуск ежедневно в 06:00 утра по времени Алматы (UTC+5)
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: doner-demand-predictor-service
  namespace: fastfood-production
  labels:
    cloud.googleapis.com/location: asia-southeast1
    app: almaty-doner-demand-predictor
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/maxScale: "3"
        run.googleapis.com/execution-environment: "gen2"
    spec:
      containerConcurrency: 10
      timeoutSeconds: 300
      containers:
        - image: gcr.io/almaty-doner-fastfood-ml/predictor:v2.1.0
          resources:
            limits:
              cpu: "1000m"
              memory: "1024Mi"
          env:
            - name: GCP_PROJECT_ID
              value: "almaty-doner-fastfood-ml"
            - name: TELEGRAM_CHAT_ID
              value: "-1002348571290"
            - name: BIGQUERY_DATASET
              value: "fastfood_analytics"

Показатель Food Cost (%) и доля списания сырья в мусор до и после BigQuery ML

Сравнение работы 4 точек фастфуда на основе интуиции шеф-поваров vs прогнозирование спроса через ARIMA_PLUS_XREG.

Надежность и обработка форс-мажоров:

  • Бессерверная экономичность: Контейнер Cloud Run просыпается ровно на 8 секунд в 06:00 утра, выполняет SQL-запрос в BigQuery, пушит план в Telegram поварам и снова засыпает в minScale: 0. Никаких платных простоев серверов;
  • Интервалы доверия (Confidence Intervals 90%): Система рассчитывает не только среднее число, но и нижний/верхний порог. Если намечается день с высокой неопределенностью, повару предлагается подготовить запасной мини-вертел на 10 кг;
  • Резервирование данных: Вся история прогнозов сохраняется в защищенном хранилище Google Cloud Storage для последующего ретроспективного анализа ошибок.

Почасовой прогноз спроса (порций/час) vs Реальные продажи в день матча и дождя

Точность предсказания BigQuery ML почасового графика продаж в филиале на Центральном стадионе (точность 96.2%).

5. Боевые результаты: Как сеть пережила финал кубка и осенние ливни

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

  1. Матч «Кайрат — Астана» на Центральном стадионе: Обычный повар на глаз насадил бы стандартные 30 кг мяса. BigQuery ML учел аншлаг на стадионе и сухую погоду (+22°C), выдав план на 82 кг курицы и 480 лавашей. К 22:00 точка продала 474 донера, заработав рекордную кассу без дефицита и без остатков;
  2. Внезапный снежный коллапс в ноябре: Утром в Алматы выпало 25 см мокрого снега, город встал в 10-балльные пробки. Модель моментально срезала прогноз для студенческих точек на 45%. В мусорку не ушло ни одного килограмма мяса, в то время как соседние бургерные выбросили треть заготовок;
  3. Дисциплина кухни: У поваров пропали споры «кто виноват, что мясо кончилось». Утром в Telegram приходит четкая цифра: «Ермек, сегодня насаживаем ровно 36.5 кг».

6. Финансовый итог для сети из 4 заведений

Сравним экономику сети до и после внедрения предиктивной аналитики:

  • Списания мяса и лаваша: Упали с 18.4% от объема закупа до 2.1% (экономия более 1 400 000 тенге в месяц чистыми);
  • Показатель Food Cost: Снизился с катастрофических 41.8% до комфортных 29.2%;
  • Упущенная выручка из-за «пустого вертела»: Сократилась на 88%;
  • Ежемесячные затраты на облако Google Cloud: $4.15 (около 2 100 тенге) за BigQuery и Cloud Run.

Подобные методы математической оптимизации и анализа временных рядов мы также рассматривали в статьях про автоматический репрайсинг Kaspi на Cloud Tasks и умную теплицу на ESP32 и Firebase. Подробнее об архитектурном аудите и сокращении затрат читайте в разделе оптимизации облачной инфраструктуры, а о разработке аналитических витрин — в разделе облачных сервисов.

💡 Совет OZAT: Готовы к внедрению? Рассчитайте архитектуру и бюджет через Scope Builder или пройдите бесплатный ИИ-аудит.

Рустам Шарафутдинов

Рустам Шарафутдинов

Автор инженерного блога

Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).

Экспертность: GCP, Kubernetes, Микросервисы, React, Node.js

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