Главная / Статьи

Игра в волейбол с ИИ-агентами для прогнозирования оборота ресторанов

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

Издатель: REACTAPP.TOP18 мин чтения4112 слов

Источники: **How ‘Playing Volleyball’ with AI Agents Helped Me Predict Restaurant Turnover —**and Win a Hackathon

Представьте хакатон, в котором сама модель составляет лишь половину работы. Другая половина заключается в том, чтобы занять несколько ИИ-агентов реализацией конкурирующих идей, не позволяя ни одному из них принимать решения, относящиеся к человеку. В этой статье описывается одна из таких попыток — хакатон по прогнозированию оборота в ресторанах, организованный компанией Great Learning совместно с Университетом Техаса в Остине и его бизнес-школой МакКомбс. В ней рассматривается эффективный рабочий процесс машинного обучения, строгая верификация и ансамбль моделей, который, по словам самих участников проекта, показал хорошие результаты на соревновании.

Слой оркестрации представлял собой фреймворк под названием DontPanic. Его рабочие процессы следуют логике волейбольной подачи: агент получает задание, планирует следующий шаг, передаёт работу специализированным подразделениям и в конце передаёт результат, при этом «мяч» всегда возвращается к человеку, принимающему решения. Агенты ускоряют выполнение задач, но не принимают окончательных решений.

1. Задача

Участники должны были спрогнозировать годовой оборот ресторанов по всей Индии на основе 3,493 маркированных учебных записей и 500 немаркированных тестовых записей. Существовало примерно 32 фактора, влияющих на прогноз: местоположение, тип кухни, формат ресторана, наличие удобств, популярность в социальных сетях и оценки качества обслуживания клиентов. Под «оборотом» здесь подразумеваются годовые продажи, а не уход сотрудников. Критерием оценки служил RMSE — показатель, при котором ошибки сначала возведаются в квадрат, а затем берется среднее значение; поэтому несколько крупных ошибок могут сильно повлиять на итоговый результат.

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

2. Инструментарий

Все элементы проекта находились в VS Code в виде модульного Python-пакета в папке src/, вместо набора не связанных между собой экспериментов в формате ноутбуков. Этот выбор оказался удачным, поскольку очистка данных, создание функций, проверка корректности и вывод результатов выполнялись с использованием одного и того же повторно используемого кода. Благодаря этому функции не могли вести себя по-разному во время обучения и при генерации окончательного результата.

3. Оркестрация агентов

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

Это важно, потому что эксперименты с искусственным интеллектом предлагают один за другим правдоподобные следующие шаги. Агент, действующий самостоятельно, будет продолжать оптимизировать тот сигнал, который он видел в последний раз, а человек может потерять нить повествования и не понимать, изменились ли гипотеза, протокол проверки или критерии оценки. Представьте DontPanic как свод правил, согласно которым проводились эксперименты: агенты могли свободно продолжать действовать, но не могли переписывать правила так, чтобы это кто-то не заметил.

Каждый период работы агента описывался манифестом. Например, манифест для работы Claude CLI указывает необходимые команды и файлы, способы проверки готовности, те компоненты, которые находятся в ведении фреймворка, действия, которые должен выполнить оператор, и действия, запрещённые к выполнению:

{
  "schema_version": "1.0.0",
  "id": "agent-claude-cli",
  "title": "Claude CLI agent runtime",
  "kind": "agent_cli",
  "category": "agent-cli",
  "summary": "External agent CLI used by DontPanic as an implementer, auditor, or goal-auditor runtime.",
  "setup_required": true,
  "setup_doc": "docs/GETTING_STARTED.md#setup-tracks--pick-yours",
  "default_in_profiles": ["core"],
  "use_cases": ["U1", "U3", "U4", "U5", "U6", "U7"],
  "requires": {
    "commands": ["claude"],
    "env": [],
    "files": ["~/.dontpanic/config.json"],
    "services": ["Claude account / Claude Code or CLI runtime"],
    "auth": ["Claude CLI authenticated by operator"],
    "config": [
      "roles.implementer",
      "roles.auditor",
      "roles.goal_auditor"
    ]
  },
  "verify": {
    "doctor_profiles": ["core"],
    "probes": ["cli:claude", "agent_role_config"],
    "readiness": "Ready when the claude command is present and the selected DontPanic role points at claude."
  },
  "owner_boundary": {
    "dontpanic_core": [
      "agent registry entry",
      "dispatch envelope",
      "audit/signoff capture",
      "quota and breaker handling",
      "role configuration"
    ],
    "adapter": [
      "subprocess executor for claude",
      "stream/output parsing",
      "timeout and envelope normalization"
    ],
    "operator": [
      "Claude CLI installation",
      "account authentication",
      "model/quota management",
      "local CLI updates"
    ]
  },
  "mutation_boundary": {
    "writes_dontpanic_state": true,
    "external_mutation": "agent_subprocess_execution",
    "allowed_path": "DontPanic supervisor invokes claude through the governed executor and captures artifacts through audit/signoff writers.",
    "forbidden": [
      "Bypassing supervisor gates",
      "Untracked agent writes outside declared plan scope",
      "Treating Claude account configuration as DontPanic-owned state"
    ]
  },
  "notes": [
    "Claude is an external runtime, not bundled by DontPanic.",
    "Core profile may also support other agent CLIs; this manifest records the Claude-specific path."
  ],
  "setup_steps": [
    {
      "id": "install_claude_cli",
      "what": "Install the Claude CLI so DontPanic can spawn agent runs through the governed executor.",
      "automatable": true,
      "command_template": "npm install -g @anthropic-ai/claude-cli",
      "verify_probe": "cli:claude",
      "human_required_reason": null
    },
    {
      "id": "authenticate_claude",
      "what": "Authenticate the Claude CLI against the operator's Claude account or API key.",
      "automatable": false,
      "command_template": "claude auth login --token <YOUR_CLAUDE_API_TOKEN>",
      "verify_probe": "agent_role_config",
      "human_required_reason": "Operator must approve scopes for their Claude account and treat the API token as a secret; DontPanic does not provision Claude credentials on the operator's behalf."
    }
  ]
}

3.1 Ралли в пяти шагах

  1. Подача: до начала любого эксперимента человек сформулировал бизнес-задачу, ограничения, метрики, критерии приемки и текущий лучший вариант решения.
  2. Прием: агент для поиска информации изучил имеющиеся данные, выявил аномалии и предложил проверяемые гипотезы вместо того, чтобы сразу переписывать модель.
  3. Настройка: после одобрения гипотезы агент по реализации преобразовал ее в модульный код, детерминистичные результаты и определение эксперимента, которое можно было повторить.
  4. Атака: агент-рецензент проверял на наличие утечек информации, ошибок в кросс-валидации, нестабильности параметров и неясного происхождения данных, а также выяснял, выдерживает ли полученный эффект сравнение с текущим лучшим вариантом решения.
  • Решение принимал человек: выбор между продвижением, отклонением, расширением или изменением стратегии зависел от него. Агенты могли предлагать варианты действий, но окончательное решение оставалось за человеком.
  • Настоящей выгодой был контролируемый параллелизм, а не просто высокая скорость программирования. Разные агенты могли заниматься различными способами возникновения ошибок, в то время как человек обеспечивал соблюдение целей продукта, его практической осуществимости и подотчетности. Все это создавало атмосферу, ближе к работе небольшой команды над продуктом, чем к одной сессии общения с чат-ботом.

    3.2 Эффективная реализация с использованием методологии Lean ML

    Рабочий процесс агентов также был привязан к модели лёгкого жизненного цикла продукта с целью снижения затрат на эксперименты на этапе разработки, затруднения фальсификации данных на этапе тестирования и предотвращения случайных изменений в производственной среде. Разделение на этапы DEV/UAT/PROD становилось всё более ценным по мере увеличения количества экспериментов. Новый агент мог присоединяться без необходимости опираться на память о прошлых диалогах, поскольку авторитетные данные хранились в виде файлов, манифестов, прогнозов вне структуры данных, реестра лучших примеров и письменных решений о продвижении. Важный принцип: пусть агенты обеспечивают скорость, а arteфакты и контрольные точки — достоверность.

    4. Проверка данных перед тем, как им верить

    Перед моделированием файлы обучения и тестирования были проанализированы с использованием алгоритма SHA-256, причем проверка проводилась столбец за столбцом. Это не было формальностью — несколько особенностей могли изменить пространство признаков во время вывода результатов и повлиять на точность прогнозов, не вызывая при этом видимых ошибок.

    • В наборе данных для обучения было 3,493 строки и 34 столбца, тогда как в наборе для тестирования — 500 строк и 33 столбца, поскольку в последнем отсутствовала целевая переменная.
    • В столбцах Ambience и Restaurant Tier в наборах данных для обучения и тестирования использовались разные типы данных, поэтому для того, чтобы модели получали идентичные признаки, требовалась явная приведение типов.
    • В наборе данных для обучения один столбец назывался Endorsed By, а в наборе для тестирования — Endoresed By. Без соответствующей связи этот столбец мог полностью отсутствовать в результатах тестирования.
  • Не было дублирующихся строк и дублирующихся векторов-прогностиков, поэтому данные на уровне строк были чистыми.
  • Номер регистрации выполнял функцию идентификатора, а не бизнес-прогностика (коэффициент Пирсона около -0,017, коэффициент Спирмена около -0,022). Его не включали в модели и сохраняли только для сопоставления строк в конце.
  • 5. Исследование данных

    5.1 Целевая переменная с сильным правым смещением

    Годовой оборот имел сильно вытянутую правую часть распределения. 90-й процентиль составлял примерно 48 млн рупий, 95-й — около 57 млн рупий, 99-й — примерно 100 млн рупий, а максимальное значение достигало 400 млн рупий; коэффициент искривления составлял около 6,65. Применение преобразования log1p сделало распределение почти симметричным; были сравнены исходные данные, данные после преобразования log1p и данные по методу Yeo-Johnson, позже также тестировалась версия с учетом площади помещения. С точки зрения бизнеса большинство ресторанов находятся в довольно узком диапазоне доходов, в то время как небольшая группа крайне крупных заведений вызывает значительные финансовые ошибки при неверном прогнозировании их показателей.

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

    5.2 Для обработки отсутствующих данных требуется более одного правила

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

    Обычные случаи отсутствия данных, такие как популярность в социальных сетях, общая оценка, атмосфера и категория, обрабатывались с использованием методов замещения на основе обучающих данных вместе с явными индикаторами отсутствия данных. Это позволяет модели различать оценку, полученную методом аппроксимации, и ситуацию, когда ничего не было зафиксировано. Структурные случаи отсутствия данных, связанные с живой музыкой, комедийными шоу, выгодными предложениями и прямыми трансляциями спортивных мероприятий, обозначались с помощью флагов наличия таких услуг, как has_live_music и has_comedy. Пустая оценка может означать отсутствие соответствующего сервиса, что представляет собой другое состояние бизнеса по сравнению с неизвестной оценкой.

    ORDINARY_MISSINGNESS_FLAGS = {
        "facebook_popularity_quotient": "facebook_missing",
        "instagram_popularity_quotient": "instagram_missing",
        "overall_restaurant_rating": "overall_rating_missing",
        "ambience": "ambience_missing",
        "restaurant_tier": "tier_missing",
    }
    
    STRUCTURAL_AVAILABILITY_FLAGS = {
        "live_music_rating": "has_live_music",
        "comedy_gigs_rating": "has_comedy",
        "value_deals_rating": "has_value_deals",
        "live_sports_rating": "has_live_sports",
    }
    
    
    def add_missingness_features(df: pd.DataFrame) -> pd.DataFrame:
        """Add ordinary-null and structural-availability indicators before imputation."""
        required_columns = set(ORDINARY_MISSINGNESS_FLAGS) | set(
            STRUCTURAL_AVAILABILITY_FLAGS
        )
        missing_columns = sorted(required_columns - set(df.columns))
        if missing_columns:
            raise KeyError(f"Missing required columns for missingness features: {missing_columns}")
    
        df = df.copy()
        for column, flag in ORDINARY_MISSINGNESS_FLAGS.items():
            df[flag] = df[column].isna().astype("int8")
        for column, flag in STRUCTURAL_AVAILABILITY_FLAGS.items():
            df[flag] = df[column].notna().astype("int8")
        return df
    

    5.3 Хаос категорий: 297 вариантов написания названий городов

    В столбце «Город» содержалось 297 необработанных вариантов написания названий городов, относящихся к гораздо меньшему числу реальных рынков. Различались регистр букв и пробелы, встречались опечатки и алиасы, а некоторые записи представляли собой сложные строки с названиями нескольких городов. Для очистки данных использовался подход, сочетающий проверку с помощью алгоритма fuzzy-matching из библиотеки rapidfuzz, тщательно составленный словарь алиасов и явную обработку сложных записей. Флаги редких городов и показатели частоты упоминания городов были сохранены, поскольку размер рынка и наличие данных могут влиять на оценку оборота. Шаг сопоставления предлагает лишь кандидатов для проверки человеком и никогда не изменяет сами значения:

    def generate_city_candidate_matches(
        city: pd.Series,
        minimum_similarity: float = 80.0,
    ) -> pd.DataFrame:
        """Return nearest fuzzy matches for review; this never changes city values."""
        normalized = normalize_city_values(city, aliases={})
        counts = normalized.value_counts()
        names = [name for name in counts.index if name != "unknown"]
        rows = []
        for raw_city in names:
            candidates = [candidate for candidate in names if candidate != raw_city]
            match = process.extractOne(raw_city, candidates, scorer=fuzz.ratio)
            if match is not None and match[1] >= minimum_similarity:
                rows.append(
                    {
                        "Raw City": raw_city,
                        "Candidate": match[0],
                        "Similarity": float(match[1]),
                        "Count": int(counts[raw_city]),
                    }
                )
        return pd.DataFrame(
            rows,
            columns=["Raw City", "Candidate", "Similarity", "Count"],
        ).sort_values(["Similarity", "Count", "Raw City"], ascending=[False, False, True])
    

    5.4 Пары переменных

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

    5.5 Более широкая картина

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

    5.6 Характеристики, основанные на времени

    Даты открытия делились на календарные компоненты и характеристики возраста. Рестораны с более длительной историей, как правило, зарабатывали больше, но это рассматривалось как связь, а не как причинно-следственная зависимость, поскольку возраст может служить показателем узнаваемости бренда, сохранения точек продаж, постоянного спроса или накопленных масштабов. Фиксированная дата отсчета обеспечивала воспроизводимость этих характеристик.

    6. Проблема дисбаланса в задаче регрессии

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

    Поэтому для проверки использовалась стратифицированная по целевой переменной 5-кратная кросс-валидация. Непрерывную целевую переменную разделили на 10 квантильных интервалов лишь для распределения данных по группам, в то время как модели продолжали обучаться и оцениваться на основе первоначальных показателей оборота. Финальные проверки проводились с использованием независимых начальных значений (17, 42 и 101). Это снизило вероятность того, что кандидат станет лучшим просто потому, что один удачный способ разделения данных попал крайние значения в более легкие группы.

    7. Инжиниринг признаков

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

    • Флаги отсутствия и наличия данных, как описано выше.
    • Календарные характеристики, получаемые из даты начала (год, месяц, квартал, день недели), а также возраст в месяцах и годах и категория «период начала» с четырьмя уровнями.
    • Количество кухонь, определяемое путем анализа списка кухонь, разделенного запятыми.
    • Иерархия городов от исходных данных до очищенных, затем до основных городов и, наконец, до штатов; существуют флаги для сложных записей, регионов и алиасов, а также показатели частоты использования и редких городов по трём порогам.
  • Категориальные взаимодействия, отражающие комбинации с иной экономической эффективностью, чем у их составляющих: тема и тип ресторана, тема и местоположение, алкогольные напитки и их типы, а также некоторые пары развлечений и форматов.
  • Числовые взаимодействия для регуляризованной линейной семьи: уровень ресторана и уровень города, социальная видимость и уровень ресторана, опыт клиента и социальная видимость, а также возраст и популярность. Это позволило преобразовать некоторые нелинейные бизнес-взаимосвязи в форму, пригодную для использования алгоритмом Ridge.
  • Кодировки частоты и порядка для каждой категориальной колонки, обучаемые на тренировочных данных и повторно используемые на тестовых.
  • Основной фиксированный алгоритм использовал 71 предиктор. Дополнительные специально созданные варианты тестировались с помощью метода абляции вместо автоматического включения, причем тот или иной признак попадал в состав модели благодаря улучшению результатов на тестах, а не из-за интуитивной привлекательности.

    8. Использование внешних исходных данных

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

    • Для каждого города: численность населения, количество и размер домохозяйств, логарифм численности населения, координаты, а также маркеры «да/нет» для крупных городов, столичного региона, прибрежных городов, столиц штатов и городов с населением более миллиона человек.
    • На уровне штата: численность населения, доля городского населения, плотность населения, ВВП на душу населения, валовой внутренний продукт и ВВП на душу населения, а также количество внутренних и иностранных туристических посещений, с рассчитанным показателем туризма на душу населения.

    Результат был намеренно консервативным. Несколько внешних признаков показывали сигналы в отдельных тестах, но семейство моделей не улучшило итоговую версию в рамках полного протокола замены и повышения качества, поэтому оно так и не попало в список лучших решений. Это полезный урок для бизнес-моделирования: переменная может казаться правдоподобной и способной к прогнозированию по отдельности, но при наличии более сильных взаимосвязанных сигналов она приносит мало пользы.

    9. Модели и причины

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

    10. Настройка

    В ходе разработки каждая группа получила собственное исследование в Optuna, оцениваемое с помощью показателя RMSE для данных вне группы на основе многократных разбиений с стратификацией по целевым значениям. Поиск кандидатов и их улучшение осуществлялись с использованием внутренней валидации, а не таблицы лидеров; это важно, поскольку последующий этап оптимизации для соревнований не должен путаться с процессом обобщения модели.

    Самая эффективная чистая комбинация включала CatBoost, Ridge, ElasticNet и вариант Ridge с стабилизацией дисперсии через квадратный корень (WR01) в пропорциях примерно 30%, 10%, 6% и 54%. Показатель RMSE для данных вне группы при многократных тестах составлял примерно 20,10 миллионов рупий. Значительная доля WR01 говорит о том, что преобразованная линейная модель сама по себе не являлась лучшим инструментом для обучения, но её особая геометрия ошибок сделала её очень ценной в составе ансамбля.

    11. Что помогло, а что — нет

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

    • Сначала была определена базовая линия. Средние и медианные показатели предикторов установили минимальное значение RMSE в пределах 21,6–21,7 млн, что облегчало выявление моделей, которые были лишь сложными по структуре.
    • Регуляризация играла важную роль. Методы Ridge и ElasticNet значительно превосходили обычный OLS, поскольку созданное пространство данных содержало коррелированные сигналы и взаимодействия.
    • CatBoost обеспечивал нелинейную и категориальную мощность, однако считалось, что настройка параметров не принесёт дополнительной пользы. Один многообещающий результат от CatBoost улучшил качество оптимизации примерно на 258 тыс., но не выдержал проверки на независимых данных и был отклонён из-за переобучения.
  • Инжиниринг признаков зависел от модели. Кодировки целевой переменной, взаимодействия признаков, частоты категорий и характеристики срока службы относились к наиболее полезным группам признаков, в то время как некоторые интуитивные агрегаты при наличии более сильных предикторов приносили незначительную пользу.
  • Дополнение данными о городе и штате извне принесло полезный, но отрицательный результат: некоторые тесты на составляющие улучшились, замена моделей в ансамбле ухудшилась, и эта модель не вошла в число лучших решений.
  • Ансамбли обеспечили наиболее надежный прирост качества, поскольку CatBoost и регуляризованные линейные варианты допускали различные ошибки. Критерием оценки служил наименьший квадратичный ошибка, полученный от взаимодополняющих моделей, а не максимальная сложность.
  • Классификаторы для обработки редких случаев и модели коррекции остатков показали перспективный потенциал для диагностики, но улучшение показателя RMSE оказалось недостаточным для их применения в практике. Полезность для диагностики и полезность для прогнозирования рассматривались как отдельные аспекты.
  • 12. Оценки для разных чемпионов

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

    Что касается соревнований, модель LB05 снизила публичный показатель RMSE на 9,195,461.72 по сравнению с S08, что составляет примерно 76.1%. Это результат из топ-листа, а не доказательство того, что развернутая модель вдруг обладает высокой обобщаемостью с показателем RMSE в 2.89 млн. Вот скрипт, который использует замороженные компоненты и формирует первую смешанную модель:

    from __future__ import annotations
    
    from datetime import datetime, timezone
    from pathlib import Path
    import time
    import json
    
    import joblib
    import numpy as np
    import pandas as pd
    
    from src import config, data, models, submission
    from src.pipeline import FeaturePipeline
    from main import _prepare_catboost_matrices, _prepare_model_matrices
    
    
    S01_ID = "S01"
    S01_WEIGHTS = {"catboost": 0.50, "ridge": 0.45, "elasticnet": 0.05}
    
    
    def run(output_dir: Path | None = None) -> tuple[Path, Path]:
        output_dir = output_dir or config.SUBMISSIONS_DIR
        output_dir.mkdir(parents=True, exist_ok=True)
        manifest_path = config.EXPERIMENTS_DIR / "frozen_v1_manifest.json"
        if not manifest_path.exists():
            raise FileNotFoundError(f"Frozen V1 manifest is required: {manifest_path}")
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        if manifest.get("feature_pipeline") != "processed_v1" or manifest.get("predictor_count") != 71:
            raise ValueError("Frozen V1 manifest does not describe the processed_v1 71-predictor contract")
    
        train_raw = data.load_train()
        test_raw = data.load_test()
        pipeline = FeaturePipeline().fit(train_raw)
        train = pipeline.transform(train_raw)
        test = pipeline.transform(test_raw)
        target = config.INTERNAL_TARGET
    
        X_train = train.drop(columns=[target, config.INTERNAL_ID_COL], errors="ignore")
        X_test = test.drop(columns=[config.INTERNAL_ID_COL], errors="ignore")
        y = train[target].astype(float).to_numpy()
        X_numeric, X_test_numeric = _prepare_model_matrices(X_train, X_test)
        X_cat, X_test_cat = _prepare_catboost_matrices(X_train, X_test)
    
        predictions = {}
        timings = {}
    
        start = time.perf_counter()
        catboost = models.get_catboost(**{
            key: value for key, value in manifest["catboost_config"].items()
            if key != "native_categorical"
        })
        cat_features = [
            index for index, dtype in enumerate(X_cat.dtypes)
            if str(dtype) in {"object", "string", "category"}
        ]
        catboost.fit(X_cat, y, cat_features=cat_features, verbose=False)
        predictions["catboost"] = catboost.predict(X_test_cat)
        timings["catboost_seconds"] = time.perf_counter() - start
    
        start = time.perf_counter()
        ridge = models.get_ridge(**manifest["ridge_config"])
        ridge.fit(X_numeric, y)
        predictions["ridge"] = ridge.predict(X_test_numeric)
        timings["ridge_seconds"] = time.perf_counter() - start
    
        start = time.perf_counter()
        elasticnet = models.get_elasticnet(**manifest["elasticnet_config"])
        elasticnet.fit(X_numeric, y)
        predictions["elasticnet"] = elasticnet.predict(X_test_numeric)
        timings["elasticnet_seconds"] = time.perf_counter() - start
    
        weights = manifest["ensemble_weights"]
        blend_prediction = sum(weights[name] * predictions[name] for name in weights)
        submission_df = submission.build_submission(test_raw, blend_prediction)
        submission_path = output_dir / "S01_frozen_v1_blend.csv"
        submission.write_submission(submission_df, submission_path, expected_rows=len(test_raw))
    
        component_paths = {}
        for name, prediction in predictions.items():
            component_paths[name] = output_dir / f"S01_{name}_predictions.csv"
            pd.DataFrame({config.ID_COL: test_raw[config.ID_COL].to_numpy(), config.TARGET: prediction}).to_csv(
                component_paths[name], index=False
            )
    
        metadata = {
            "submission_id": S01_ID,
            "model": "Frozen V1 Blend",
            "feature_version": "frozen production",
            "oof_rmse": 20097389.205713525,
            "catboost_weight": S01_WEIGHTS["catboost"],
            "ridge_weight": S01_WEIGHTS["ridge"],
            "elasticnet_weight": S01_WEIGHTS["elasticnet"],
            "public_leaderboard_rmse": None,
            "leaderboard_rank": None,
            "timestamp_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            "submission_file": str(submission_path),
            "component_files": {name: str(path) for name, path in component_paths.items()},
            "fit_method": "fit_each_frozen_component_on_all_3493_processed_v1_training_rows",
            "catboost_production_iterations_rule": "fixed_manifest_value_no_early_stopping",
            "manifest_path": str(manifest_path),
            **timings,
        }
        metadata_path = output_dir / "S01_metadata.json"
        metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
        return submission_path, metadata_path
    
    
    if __name__ == "__main__":
        for path in run():
            print(path)
    

    13. Результаты на соревнованиях против способности к обобщению

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

    Следовательно, окончательное значение RMSE в таблице лидеров, составляющее примерно 2,893 млн, следует рассматривать как характерное исключительно для соревнований и потенциально свидетельствующее об переобучении модели на этом наборе данных для оценки. Разумной оценкой того, как будет работать основная модель с новыми ресторанами, остается результат повторного кросс-валидирования, составляющий примерно 20,10 млн. Разделение этих двух показателей крайне важно как для научной, так и для бизнес-анализа.

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

    14. Предположения

    • Предполагалось, что данные для обучения и тестирования происходят из достаточно похожих бизнес-процессов, чтобы кросс-валидация имела смысл. Никаких формальных корректировок из-за смещения ковариатов не проводилось, за исключением проверок структуры и распределения.
    • Номер регистрации считался исключительно идентификатором и не включался в чистую модель, поскольку у него не было обоснованной связи с сменами.
    • Предполагалось, что отсутствующие оценки удобств содержат структурную информацию о отсутствии соответствующего сервиса, что легло в основу дизайна флага доступности.
  • Все используемые статистические показатели предобработки должны были браться из обучающих данных и без изменений использоваться при прогнозировании; это правило соблюдается самой системой, поэтому она никогда не зависела от памяти аналитика.
  • Внешние рыночные данные считались приемлемыми только в том случае, если они улучшали результаты повторной проверки и работы ансамблей. Одних лишь показателей правдоподобия было недостаточно.
  • 15. Рекомендации

    • Рассматривайте качество данных как показатель производительности модели. Несоответствия схемы, фрагментированные названия городов и несогласованные категориальные данные могут уничтожить полезную информацию ещё до того, как алгоритм сможет её увидеть.
    • Разделяйте случаи «отсутствия данных из-за неизвестности» и случаи «отсутствия данных из-за отсутствия возможности их получения». Во втором случае это часто является полезной характеристикой, а не проблемой.
    • При несбалансированных целях по доходу следует отслеживать значение RMSE для крайних значений, степень концентрации квадратов ошибок, а также общую сумму ошибок. Несколько крупных клиентов могут определить, является ли прогноз финансово полезным.
    • Используйте четкие критерии для продвижения: поиск кандидатов, подтверждение на независимых примерах и замена составов моделей. Это предотвращает случайное признание выгоды, полученной на одном разделении данных, лучшим результатом.
    • Рассматривайте искусственный интеллект-агента как операционную модель, а не как разрешение на автономное продвижение. Пусть агенты предлагают решения, создают модели и оспаривают результаты, в то время как решения, касающиеся ключевых моделей и продуктов, остаются в руках людей.
    • Храните записи фактов в месте, отличном от окна чата. Наборы данных, манифесты, прогнозы вне диапазона, журналы экспериментов, выбранные лучшие решения и решения о их продвижении должны сохраняться, чтобы новый агент мог опираться на эти данные, а не на историю предыдущих сообщений.
    • При экспериментах с ИИ необходимо разделять среды DEV, UAT и PROD. Свобода творчества допускается в среде DEV, в то время как репликация и критический анализ происходят в среде UAT; среда PROD должна быть детерминированной, зафиксированной и обратимой.

    16. Уроки

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

    Победитель финального соревнования достиг значения RMSE около 2,893 млн рупий после этапа оптимизации, адаптированного под условия оценки. Этот показатель определил исход соревнования, однако он приводится отдельно, поскольку может свидетельствовать о переобучении модели, и его не следует рассматривать как ожидаемую погрешность для будущих ресторанов.

    Более общий урок заключается в практических действиях: сначала проводите проверку модели, прежде чем начинать настройки; сохраняйте результаты неудачных экспериментов; принимайте к использованию только те улучшения, которые подтверждаются в разных условиях; и никогда не путайте модель, показавшую лучшие результаты на тестовых данных, с моделью, которой можно доверять в реальных условиях работы. А если задействовано несколько ИИ-агентов, заставьте их «играть в волейбол»: пусть они активно выполняют свои задачи, но убедитесь, что важные решения снова принимаются человеком до того, как будет засчитан очко.