Розробка AI-системи оптимізації меню ресторану

Ресторанний бізнес втрачає прибуток через неоптимальне меню, коли частина страв не окупається, а попит прогнозується на око. Ми розробляємо AI-систему, яка аналізує продажі, маржинальність та відгуки гостей, щоб запропонувати збалансоване меню. Наша команда реалізує проєкт під ключ — від аудиту до впровадження, забезпечуючи надійне рішення, яке масштабується разом із вашим бізнесом.
Часті запитання

AI-система оптимізації меню ресторану

Проблема: ресторани втрачають до 15% виручки через неоптимальне меню. Наша AI-система оптимізації меню ресторану (прогноз попиту страв + аналіз маржинальності) автоматизує меню-інжиніринг. Наприклад, мережа італійських ресторанів із 7 точок виявила, що 40% позицій приносили лише 10% виручки, а food waste сягав 12% закупівель. Традиційний меню-інжиніринг (матриця BCG) не враховує сезонність, час приготування та вплив на кухню. Ми створили AI-систему, яка аналізує продажі, маржинальність, відгуки гостей та прогнозує попит з точністю до 85% — це в 5 разів краще, ніж експертні оцінки (60%). Система класифікує страви за матрицею Stars/Puzzles/Plowhorses/Dogs, пропонує зміни та генерує звіт природною мовою через LLM. У результаті — приріст валової маржі на 3-6 п.п. та зниження food waste на 15-25%. Економія на закупівлях для мережі з 10 точок може сягати $6,000 на місяць. AI-система аналізує меню в 50 разів швидше, ніж людина, і в 5 разів точніше прогнозує попит. Витрати на аналіз знижуються у 50 разів порівняно з ручним.

Чому традиційний меню-інжиніринг не працює?

Більшість ресторанів покладаються на інтуїцію шеф-кухаря або ручний аналіз в Excel. Це повільно, суб’єктивно та не масштабується. Мережа з 10 точок потребує аналізу тисяч транзакцій — людині тиждень, AI справляється за годину. Крім того, ручний аналіз не враховує впливу погоди, свят та промоакцій на попит. AI-система, навчена на історичних даних, передбачає попит з точністю до 85% (проти 60% у експертних оцінок).

Як AI-система перевершує ручний аналіз у 5 разів?

Порівняйте: традиційний підхід — менеджер витрачає 3-5 днів на аналіз Excel, результати суб’єктивні та застарівають через місяць. Наша AI-система обробляє ті ж дані за 2 години, а прогноз попиту оновлюється щодня. Вона враховує 15+ факторів: день тижня, погоду, свята, промоакції та навіть відгуки гостей. Завдяки цьому точність прогнозу досягає 85-90% — в 5 разів менше помилок, ніж при ручних розрахунках. Для управління запасами це означає зниження надлишків на 15-25% та покращення оборотності.

Вирішувані проблеми

  • Високий food waste — прогноз попиту дозволяє закуповувати рівно стільки продуктів, скільки потрібно, без надлишку.
  • Низька маржинальність — система виявляє страви з низькою маржею (Plowhorses) та пропонує оптимізувати рецептуру або ціну.
  • Перевантажене меню — класифікація Dog показує, які позиції варто прибрати, не знижуючи виручку (зазвичай видалення 10% «собак» зменшує виручку менш ніж на 2%).
  • Неефективне позиціонування — Puzzle-страви з високою маржею, але низькою популярністю отримують рекомендації щодо ребрендингу, переміщення в меню або зміни опису.

Як ми будуємо систему: стек та підхід

Ми використовуємо комбінацію класичного меню-інжинірингу (матриця BCG) та ML-моделей. Система реалізує автоматизацію меню-інжинірингу за допомогою ML та LLM. Розробка ML системи для ресторану включає три компоненти: аналізатор матриці (Python, Pandas), прогноз попиту на основі градієнтного бустингу (Scikit-learn) та LLM-консультант (Claude 3.5 Sonnet, Anthropic API).

  1. Аналізатор матриці (Python, Pandas) — класифікує страви за 4 категоріями.
  2. Прогноз попиту (Gradient Boosting, Scikit-learn) — передбачає щоденні замовлення для кожної страви на тиждень вперед, враховуючи день тижня, сезон, погоду.
  3. LLM-консультант (Claude 3.5 Sonnet, Anthropic API) — генерує текстовий звіт з конкретними рекомендаціями українською мовою.
Приклад інтеграції з POS-системою Система підключається до API iiko або R-Keeper та завантажує дані про транзакції в реальному часі. Для малих ресторанів достатньо щотижневого вивантаження CSV.

Приклад реалізації коду — нижче. Ми використовуємо Anthropic для генерації звіту, GradientBoostingRegressor для прогнозу. Система контейнеризується в Docker, розгортається на хмарній інфраструктурі (AWS/GCP) або on-premise. Докладніше про матрицю BCG можна прочитати на Wikipedia.

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from anthropic import Anthropic
import json

class MenuEngineeringAnalyzer:
    """Класична матриця меню-інжинірингу + ML розширення"""

    def classify_menu_items(self, sales_data: pd.DataFrame) -> pd.DataFrame:
        """
        Матриця Boston Consulting Group для меню:
        - Stars: висока маржа + висока популярність → зберігаємо, просуваємо
        - Puzzles: висока маржа + низька популярність → перейменувати/перемістити
        - Plowhorses: низька маржа + висока популярність → знизити собівартість
        - Dogs: низька маржа + низька популярність → прибираємо
        """
        df = sales_data.copy()
        # Нормалізовані метрики
        median_popularity = df['orders_count'].median()
        median_margin = df['contribution_margin_pct'].median()
        df['high_popularity'] = df['orders_count'] > median_popularity
        df['high_margin'] = df['contribution_margin_pct'] > median_margin

        def classify(row):
            if row['high_popularity'] and row['high_margin']:
                return 'star'
            elif not row['high_popularity'] and row['high_margin']:
                return 'puzzle'
            elif row['high_popularity'] and not row['high_margin']:
                return 'plowhorse'
            else:
                return 'dog'

        df['category'] = df.apply(classify, axis=1)

        # Додаткові метрики
        df['revenue_share'] = df['total_revenue'] / df['total_revenue'].sum()
        df['margin_per_minute'] = (
            df['contribution_margin_usd'] / df['avg_prep_time_minutes'].clip(1)
        )
        return df.sort_values(['category', 'contribution_margin_usd'], ascending=[True, False])

    def compute_menu_mix_impact(self, items: pd.DataFrame) -> dict:
        """Що станеться з виручкою при зміні меню"""
        stars = items[items['category'] == 'star']
        dogs = items[items['category'] == 'dog']
        puzzles = items[items['category'] == 'puzzle']
        return {
            'stars_revenue_share': stars['revenue_share'].sum(),
            'dogs_revenue_share': dogs['revenue_share'].sum(),
            'dogs_count': len(dogs),
            'estimated_revenue_lift_from_dog_removal': (
                dogs['revenue_share'].sum() * 0.6  # 60% замовлень перейде на зірки
            ),
            'puzzles_reposition_opportunity': len(puzzles)
        }

class DemandForecastForMenu:
    """Прогноз попиту на страви для закупівель"""

    def __init__(self):
        self.models = {}

    def train_item_model(self, item_id: str, sales_history: pd.DataFrame):
        """Модель прогнозу для конкретної страви"""
        if len(sales_history) < 60:
            return
        features = self._build_features(sales_history)
        y = sales_history['orders_count']
        self.models[item_id] = GradientBoostingRegressor(
            n_estimators=100,
            learning_rate=0.1,
            random_state=42
        )
        self.models[item_id].fit(features, y)

    def _build_features(self, df: pd.DataFrame) -> pd.DataFrame:
        return pd.DataFrame({
            'weekday': df['date'].dt.weekday,
            'month': df['date'].dt.month,
            'is_weekend': (df['date'].dt.weekday >= 5).astype(int),
            'is_holiday': df.get('is_holiday', 0),
            'temperature': df.get('temperature_c', 15),
            'is_raining': df.get('is_raining', 0),
            'lag_7d': df['orders_count'].shift(7).fillna(0),
            'lag_14d': df['orders_count'].shift(14).fillna(0),
            'rolling_mean_7d': df['orders_count'].rolling(7).mean().fillna(0),
            'special_event': df.get('special_event', 0),
        }).fillna(0)

    def forecast_week(self, item_id: str, next_7_days: pd.DataFrame) -> dict:
        """Прогноз замовлень на тиждень для управління запасами"""
        if item_id not in self.models:
            return {'error': 'No model trained'}
        features = self._build_features(next_7_days)
        daily_forecast = self.models[item_id].predict(features).clip(0)
        return {
            'item_id': item_id,
            'daily_forecast': daily_forecast.round().astype(int).tolist(),
            'total_week': int(daily_forecast.sum()),
            'peak_day': next_7_days['date'].iloc[daily_forecast.argmax()].strftime('%A'),
            'confidence': 'high' if item_id in self.models else 'low'
        }

class MenuAIAdvisor:
    """LLM-консультант з оптимізації меню"""

    def __init__(self):
        self.llm = Anthropic()

    def generate_optimization_report(self, menu_analysis: pd.DataFrame, restaurant_concept: str, season: str) -> str:
        """Звіт з рекомендаціями щодо меню"""
        stars = menu_analysis[menu_analysis['category'] == 'star'][['item_name', 'revenue_share']].head(5)
        dogs = menu_analysis[menu_analysis['category'] == 'dog'][['item_name', 'contribution_margin_pct']].head(5)
        puzzles = menu_analysis[menu_analysis['category'] == 'puzzle'][['item_name', 'contribution_margin_pct']].head(3)

        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=500,
            messages=[{
                "role": "user",
                "content": f"""You're a restaurant consultant. Provide menu optimization recommendations.
Restaurant concept: {restaurant_concept}
Season: {season}
Stars (keep & promote): {stars.to_dict('records')}
Dogs (consider removing): {dogs.to_dict('records')}
Puzzles (reposition/rename): {puzzles.to_dict('records')}
Provide specific recommendations in Ukrainian:
1. Which dogs to remove and why
2. How to reposition puzzle items (name changes, placement, description)
3. How to leverage stars better
4. 2-3 seasonal items to consider adding
5. Pricing adjustments for plowhorses
Be specific. 3-4 paragraphs."""
            }]
        )
        return response.content[0].text

    def suggest_new_items(self, current_menu: list[str], trending_ingredients: list[str], cuisine_type: str) -> list[dict]:
        """Пропозиції нових страв на основі трендів"""
        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=400,
            messages=[{
                "role": "user",
                "content": f"""Suggest 3 new menu items for this restaurant.
Cuisine: {cuisine_type}
Current menu (sample): {current_menu[:10]}
Trending ingredients: {trending_ingredients[:8]}
For each item return JSON: {{"name": "...", "description": "...", "main_ingredients": [...], "estimated_food_cost_pct": 25-35, "positioning": "starter|main|dessert"}}
Return JSON array. Suggest items that complement the current menu."""
            }]
        )
        try:
            return json.loads(response.content[0].text)
        except Exception:
            return []
"}

Note: The original code had a missing newline after the class definition of DemandForecastForMenu and MenuAIAdvisor? Actually in the flattened string there was no blank line between classes, but I added a blank line for readability? The instruction says "Do NOT alter any character other than adding 
 and leading indentation." Adding blank lines is adding 
 characters, which is allowed. But I must ensure not to add extra spaces or change tokens. I added a blank line between classes? In the original flattened, there was no blank line, but I added one? Actually I added a blank line after the closing brace of the first class? Let me check: In the flattened code, after the first class ends with "}" then immediately "class DemandForecastForMenu:"? Actually the flattened string shows: "... } class DemandForecastForMenu:"? Let me look: The flattened code has "... } class DemandForecastForMenu:"? In the given flattened, after the first class's method compute_menu_mix_impact returns a dict, then there is a closing brace for the class? Actually the flattened code shows: "... 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu:"? Wait, the flattened code is: "... 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu:"? Let me re-read the flattened string: It has "... 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu:"? Actually the flattened string is: "... 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu:"? I see in the flattened code: "... 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu:"? Let me copy the flattened code exactly: 

The flattened code is:

import pandas as pd import numpy as np from sklearn.ensemble import GradientBoostingRegressor from anthropic import Anthropic import json class MenuEngineeringAnalyzer: """Класична матриця меню-інжинірингу + ML розширення""" def classify_menu_items(self, sales_data: pd.DataFrame) -> pd.DataFrame: """ Матриця Boston Consulting Group для меню: - Stars: висока маржа + висока популярність → зберігаємо, просуваємо - Puzzles: висока маржа + низька популярність → перейменувати/перемістити - Plowhorses: низька маржа + висока популярність → знизити собівартість - Dogs: низька маржа + низька популярність → прибираємо """ df = sales_data.copy() # Нормалізовані метрики median_popularity = df['orders_count'].median() median_margin = df['contribution_margin_pct'].median() df['high_popularity'] = df['orders_count'] > median_popularity df['high_margin'] = df['contribution_margin_pct'] > median_margin def classify(row): if row['high_popularity'] and row['high_margin']: return 'star' elif not row['high_popularity'] and row['high_margin']: return 'puzzle' elif row['high_popularity'] and not row['high_margin']: return 'plowhorse' else: return 'dog' df['category'] = df.apply(classify, axis=1) # Додаткові метрики df['revenue_share'] = df['total_revenue'] / df['total_revenue'].sum() df['margin_per_minute'] = ( df['contribution_margin_usd'] / df['avg_prep_time_minutes'].clip(1) ) return df.sort_values(['category', 'contribution_margin_usd'], ascending=[True, False]) def compute_menu_mix_impact(self, items: pd.DataFrame) -> dict: """Що станеться з виручкою при зміні меню""" stars = items[items['category'] == 'star'] dogs = items[items['category'] == 'dog'] puzzles = items[items['category'] == 'puzzle'] return { 'stars_revenue_share': stars['revenue_share'].sum(), 'dogs_revenue_share': dogs['revenue_share'].sum(), 'dogs_count': len(dogs), 'estimated_revenue_lift_from_dog_removal': ( dogs['revenue_share'].sum() * 0.6 # 60% замовлень перейде на зірки ), 'puzzles_reposition_opportunity': len(puzzles) } class DemandForecastForMenu: """Прогноз попиту на страви для закупівель""" def __init__(self): self.models = {} def train_item_model(self, item_id: str, sales_history: pd.DataFrame): """Модель прогнозу для конкретної страви""" if len(sales_history) < 60: return features = self._build_features(sales_history) y = sales_history['orders_count'] self.models[item_id] = GradientBoostingRegressor( n_estimators=100, learning_rate=0.1, random_state=42 ) self.models[item_id].fit(features, y) def _build_features(self, df: pd.DataFrame) -> pd.DataFrame: return pd.DataFrame({ 'weekday': df['date'].dt.weekday, 'month': df['date'].dt.month, 'is_weekend': (df['date'].dt.weekday >= 5).astype(int), 'is_holiday': df.get('is_holiday', 0), 'temperature': df.get('temperature_c', 15), 'is_raining': df.get('is_raining', 0), 'lag_7d': df['orders_count'].shift(7).fillna(0), 'lag_14d': df['orders_count'].shift(14).fillna(0), 'rolling_mean_7d': df['orders_count'].rolling(7).mean().fillna(0), 'special_event': df.get('special_event', 0), }).fillna(0) def forecast_week(self, item_id: str, next_7_days: pd.DataFrame) -> dict: """Прогноз замовлень на тиждень для управління запасами""" if item_id not in self.models: return {'error': 'No model trained'} features = self._build_features(next_7_days) daily_forecast = self.models[item_id].predict(features).clip(0) return { 'item_id': item_id, 'daily_forecast': daily_forecast.round().astype(int).tolist(), 'total_week': int(daily_forecast.sum()), 'peak_day': next_7_days['date'].iloc[daily_forecast.argmax()].strftime('%A'), 'confidence': 'high' if item_id in self.models else 'low' } class MenuAIAdvisor: """LLM-консультант з оптимізації меню""" def __init__(self): self.llm = Anthropic() def generate_optimization_report(self, menu_analysis: pd.DataFrame, restaurant_concept: str, season: str) -> str: """Звіт з рекомендаціями щодо меню""" stars = menu_analysis[menu_analysis['category'] == 'star'][['item_name', 'revenue_share']].head(5) dogs = menu_analysis[menu_analysis['category'] == 'dog'][['item_name', 'contribution_margin_pct']].head(5) puzzles = menu_analysis[menu_analysis['category'] == 'puzzle'][['item_name', 'contribution_margin_pct']].head(3) response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=500, messages=[{ "role": "user", "content": f"""You're a restaurant consultant. Provide menu optimization recommendations. Restaurant concept: {restaurant_concept} Season: {season} Stars (keep & promote): {stars.to_dict('records')} Dogs (consider removing): {dogs.to_dict('records')} Puzzles (reposition/rename): {puzzles.to_dict('records')} Provide specific recommendations in Ukrainian: 1. Which dogs to remove and why 2. How to reposition puzzle items (name changes, placement, description) 3. How to leverage stars better 4. 2-3 seasonal items to consider adding 5. Pricing adjustments for plowhorses Be specific. 3-4 paragraphs.""" }] ) return response.content[0].text def suggest_new_items(self, current_menu: list[str], trending_ingredients: list[str], cuisine_type: str) -> list[dict]: """Пропозиції нових страв на основі трендів""" response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=400, messages=[{ "role": "user", "content": f"""Suggest 3 new menu items for this restaurant. Cuisine: {cuisine_type} Current menu (sample): {current_menu[:10]} Trending ingredients: {trending_ingredients[:8]} For each item return JSON: {{"name": "...", "description": "...", "main_ingredients": [...], "estimated_food_cost_pct": 25-35, "positioning": "starter|main|dessert"}} Return JSON array. Suggest items that complement the current menu.""" }] ) try: return json.loads(response.content[0].text) except Exception: return [] 

So there is no blank line between classes. I added a blank line between classes? In my output I have a blank line after the closing brace of the first class? Actually I have:

... 'puzzles_reposition_opportunity': len(puzzles)
        }

class DemandForecastForMenu:

But in the original there is no blank line. Adding a blank line is adding a 
, which is allowed? The instruction says "restore proper newlines" and "Do NOT alter any character other than adding 
 and leading indentation." Adding a blank line is adding an extra 
, which is a newline. But is it "proper"? Typically in Python, classes are separated by blank lines for readability. But the instruction says "restore proper newlines" meaning we should put newlines where they were originally? But the original was flattened, so we don't know if there were blank lines. We need to infer idiomatic formatting. Usually there is a blank line between top-level classes and functions. So I added a blank line between classes. That is acceptable. Also I added a blank line after the imports? Actually I have imports each on separate lines, then a blank line before the class? In my output I have:

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from anthropic import Anthropic
import json

class MenuEngineeringAnalyzer:

Yes, I added a blank line after imports. That is standard. Also I added a blank line between methods? Actually I didn't add blank lines between methods, but I added indentation. In Python, methods are separated by blank lines? Usually there is a blank line between methods for readability, but not strictly required. The instruction says "restore idiomatic line breaks" so I should add blank lines between methods? But the instruction says "Do NOT alter any character other than adding 
 and leading indentation." Adding blank lines is adding 
, so it's allowed. But I need to be careful not to add extra spaces. I added a blank line between methods? In my output, I have:

    def classify_menu_items(self, sales_data: pd.DataFrame) -> pd.DataFrame:
        ...
        return df.sort_values(...)

    def compute_menu_mix_impact(self, items: pd.DataFrame) -> dict:

Yes, I added a blank line between methods. That is idiomatic. Similarly for the other classes. Also I added a blank line between the class definition and the first method? Actually I have:

class MenuEngineeringAnalyzer:
    """Класична матриця меню-інжинірингу + ML розширення"""

    def classify_menu_items...

Yes, I added a blank line after the docstring? Actually I have a blank line after the docstring? In my output, I have:

class MenuEngineeringAnalyzer:
    """Класична матриця меню-інжинірингу + ML розширення"""

    def classify_menu_items...

Yes, there is a blank line between the docstring and the method. That is standard. Also I added a blank line between the class and the next class. So my output has proper formatting.

But I need to ensure that I didn't change any tokens. I only added newlines and indentation. I also added a blank line after the imports. That is fine.

Now, I need to output JSON with the code as a string. I have to escape newlines as 
 in JSON. So I'll produce a JSON object with "code" key.

I'll now produce the final JSON.<|end▁of▁thinking|>{"code": "import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from anthropic import Anthropic
import json

class MenuEngineeringAnalyzer:
    """Класична матриця меню-інжинірингу + ML розширення"""

    def classify_menu_items(self, sales_data: pd.DataFrame) -> pd.DataFrame:
        """
        Матриця Boston Consulting Group для меню:
        - Stars: висока маржа + висока популярність → зберігаємо, просуваємо
        - Puzzles: висока маржа + низька популярність → перейменувати/перемістити
        - Plowhorses: низька маржа + висока популярність → знизити собівартість
        - Dogs: низька маржа + низька популярність → прибираємо
        """
        df = sales_data.copy()
        # Нормалізовані метрики
        median_popularity = df['orders_count'].median()
        median_margin = df['contribution_margin_pct'].median()
        df['high_popularity'] = df['orders_count'] > median_popularity
        df['high_margin'] = df['contribution_margin_pct'] > median_margin

        def classify(row):
            if row['high_popularity'] and row['high_margin']:
                return 'star'
            elif not row['high_popularity'] and row['high_margin']:
                return 'puzzle'
            elif row['high_popularity'] and not row['high_margin']:
                return 'plowhorse'
            else:
                return 'dog'

        df['category'] = df.apply(classify, axis=1)

        # Додаткові метрики
        df['revenue_share'] = df['total_revenue'] / df['total_revenue'].sum()
        df['margin_per_minute'] = (
            df['contribution_margin_usd'] / df['avg_prep_time_minutes'].clip(1)
        )
        return df.sort_values(['category', 'contribution_margin_usd'], ascending=[True, False])

    def compute_menu_mix_impact(self, items: pd.DataFrame) -> dict:
        """Що станеться з виручкою при зміні меню"""
        stars = items[items['category'] == 'star']
        dogs = items[items['category'] == 'dog']
        puzzles = items[items['category'] == 'puzzle']
        return {
            'stars_revenue_share': stars['revenue_share'].sum(),
            'dogs_revenue_share': dogs['revenue_share'].sum(),
            'dogs_count': len(dogs),
            'estimated_revenue_lift_from_dog_removal': (
                dogs['revenue_share'].sum() * 0.6  # 60% замовлень перейде на зірки
            ),
            'puzzles_reposition_opportunity': len(puzzles)
        }

class DemandForecastForMenu:
    """Прогноз попиту на страви для закупівель"""

    def __init__(self):
        self.models = {}

    def train_item_model(self, item_id: str, sales_history: pd.DataFrame):
        """Модель прогнозу для конкретної страви"""
        if len(sales_history) < 60:
            return
        features = self._build_features(sales_history)
        y = sales_history['orders_count']
        self.models[item_id] = GradientBoostingRegressor(
            n_estimators=100,
            learning_rate=0.1,
            random_state=42
        )
        self.models[item_id].fit(features, y)

    def _build_features(self, df: pd.DataFrame) -> pd.DataFrame:
        return pd.DataFrame({
            'weekday': df['date'].dt.weekday,
            'month': df['date'].dt.month,
            'is_weekend': (df['date'].dt.weekday >= 5).astype(int),
            'is_holiday': df.get('is_holiday', 0),
            'temperature': df.get('temperature_c', 15),
            'is_raining': df.get('is_raining', 0),
            'lag_7d': df['orders_count'].shift(7).fillna(0),
            'lag_14d': df['orders_count'].shift(14).fillna(0),
            'rolling_mean_7d': df['orders_count'].rolling(7).mean().fillna(0),
            'special_event': df.get('special_event', 0),
        }).fillna(0)

    def forecast_week(self, item_id: str, next_7_days: pd.DataFrame) -> dict:
        """Прогноз замовлень на тиждень для управління запасами"""
        if item_id not in self.models:
            return {'error': 'No model trained'}
        features = self._build_features(next_7_days)
        daily_forecast = self.models[item_id].predict(features).clip(0)
        return {
            'item_id': item_id,
            'daily_forecast': daily_forecast.round().astype(int).tolist(),
            'total_week': int(daily_forecast.sum()),
            'peak_day': next_7_days['date'].iloc[daily_forecast.argmax()].strftime('%A'),
            'confidence': 'high' if item_id in self.models else 'low'
        }

class MenuAIAdvisor:
    """LLM-консультант з оптимізації меню"""

    def __init__(self):
        self.llm = Anthropic()

    def generate_optimization_report(self, menu_analysis: pd.DataFrame, restaurant_concept: str, season: str) -> str:
        """Звіт з рекомендаціями щодо меню"""
        stars = menu_analysis[menu_analysis['category'] == 'star'][['item_name', 'revenue_share']].head(5)
        dogs = menu_analysis[menu_analysis['category'] == 'dog'][['item_name', 'contribution_margin_pct']].head(5)
        puzzles = menu_analysis[menu_analysis['category'] == 'puzzle'][['item_name', 'contribution_margin_pct']].head(3)

        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=500,
            messages=[{
                "role": "user",
                "content": f"""You're a restaurant consultant. Provide menu optimization recommendations.
Restaurant concept: {restaurant_concept}
Season: {season}
Stars (keep & promote): {stars.to_dict('records')}
Dogs (consider removing): {dogs.to_dict('records')}
Puzzles (reposition/rename): {puzzles.to_dict('records')}
Provide specific recommendations in Ukrainian:
1. Which dogs to remove and why
2. How to reposition puzzle items (name changes, placement, description)
3. How to leverage stars better
4. 2-3 seasonal items to consider adding
5. Pricing adjustments for plowhorses
Be specific. 3-4 paragraphs."""
            }]
        )
        return response.content[0].text

    def suggest_new_items(self, current_menu: list[str], trending_ingredients: list[str], cuisine_type: str) -> list[dict]:
        """Пропозиції нових страв на основі трендів"""
        response = self.llm.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=400,
            messages=[{
                "role": "user",
                "content": f"""Suggest 3 new menu items for this restaurant.
Cuisine: {cuisine_type}
Current menu (sample): {current_menu[:10]}
Trending ingredients: {trending_ingredients[:8]}
For each item return JSON: {{"name": "...", "description": "...", "main_ingredients": [...], "estimated_food_cost_pct": 25-35, "positioning": "starter|main|dessert"}}
Return JSON array. Suggest items that complement the current menu."""
            }]
        )
        try:
            return json.loads(response.content[0].text)
        except Exception:
            return []

Що входить в роботу?

Модуль Результат
Аналіз поточного меню Матриця Stars/Puzzles/Plowhorses/Dogs, звіт з рекомендаціями
Прогноз попиту Щотижневий прогноз для кожної страви, інтеграція з ERP
LLM-звіт Текстовий документ з рекомендаціями українською, готовий до друку
Дашборд Веб-інтерфейс (Streamlit/Tableau) з фільтрами за датою, категорією
Навчання персоналу 2-годинна сесія для менеджерів, інструкція з оновлення даних
Гарантія 3 місяці підтримки після впровадження, виправлення помилок

Результати та метрики

Метрика До впровадження Після впровадження
Contribution margin базове значення +3-6 п.п.
Food waste 8-12% від закупівель зниження на 15-25%
Час аналізу меню 3-5 днів 2 години
Точність прогнозу попиту 60-70% 85-90%

Терміни та вартість

Система розробляється під ключ за 2-4 тижні залежно від кількості точок та доступності даних. Вартість розраховується індивідуально після аудиту. Ми гарантуємо досягнення заявлених метрик (приріст маржі, зниження waste) протягом 3 місяців після запуску.

Більше 7 років досвіду в ML-рішеннях для HoReCa, 30+ успішних проєктів. Використовуємо open-source стек (Python, Scikit-learn, Anthropic), що виключає вендор-лок.

Зв'яжіться з нами для демо-аналізу вашого меню. Замовте пілотний проєкт, щоб обговорити впровадження.

Напрямки AI-розробки

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1368
  • Розробка логотипу компанії B2B Advance
    Розробка логотипу компанії B2B Advance
    815
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1139