AI-система оптимізації меню ресторану
Інжиніринг меню з AI — це не просто "прибрати блюда, що погано продаються". Система аналізує прибутковість, популярність, сезонність, час приготування, вплив на кухонні операції та прогнозує попит для закупівель. McDonald's та Starbucks використовують ML для квартального перегляду меню.
Аналіз меню: матриця Зірок/Загадок/Робочих коней/Собак
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 для меню:
- Зірки: висока маржа + висока популярність → зберегти, просувати
- Загадки: висока маржа + низька популярність → перейменувати/перемістити
- Робочі коні: низька маржа + висока популярність → скоротити вартість
- Собаки: низька маржа + низька популярність → видалити
"""
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:
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 []
Регулярний AI-аналіз меню (щоквартально) підвищує contribution margin на 3-6 відсоткових пункти. Видалення нижніх 10% в категорії "собаки" рідко знижує виручку більш ніж на 2% — гості переходять на альтернативи. Прогнозування попиту зменшує відходи їжі на 15-25%.







