Отлично! Ниже я выполню все три задачи: --- ## ✅ 1. **Схема данных (на основе PostgreSQL + Node.js)** ### 📦 Архитектура: B2B-портал с рекомендациями ``` [Клиент (Frontend)] ↓ [Node.js API (Express)] ↓ [PostgreSQL] ←→ [Рекомендательная система (Node.js + Python)] ↓ [Redis (кеширование рекомендаций)] ``` --- ### 🗃️ Структура таблиц в PostgreSQL ```sql -- Клиенты CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, company_name VARCHAR(255), segment VARCHAR(50), -- например: restaurant, retail, distributor region VARCHAR(100), created_at TIMESTAMP DEFAULT NOW() ); -- Товары CREATE TABLE products ( product_id SERIAL PRIMARY KEY, sku VARCHAR(50) UNIQUE, name VARCHAR(255), category VARCHAR(100), subcategory VARCHAR(100), brand VARCHAR(100), weight NUMERIC, price NUMERIC(10,2), is_perishable BOOLEAN DEFAULT FALSE, shelf_life_days INT, -- срок годности created_at TIMESTAMP DEFAULT NOW() ); -- Складские остатки CREATE TABLE inventory ( inventory_id SERIAL PRIMARY KEY, product_id INT REFERENCES products(product_id), warehouse_region VARCHAR(100), stock_quantity INT, next_delivery_date DATE, updated_at TIMESTAMP DEFAULT NOW() ); -- Заказы CREATE TABLE orders ( order_id SERIAL PRIMARY KEY, customer_id INT REFERENCES customers(customer_id), order_date TIMESTAMP DEFAULT NOW(), total_amount NUMERIC(12,2), status VARCHAR(20) -- например: completed, cancelled ); -- Состав заказа CREATE TABLE order_items ( order_item_id SERIAL PRIMARY KEY, order_id INT REFERENCES orders(order_id), product_id INT REFERENCES products(product_id), quantity INT, price_per_unit NUMERIC(10,2) ); ``` --- ### 🔗 Как это работает с Node.js ```js // Пример запроса истории покупок клиента const getCustomerOrders = async (customerId) => { const query = ` SELECT p.sku, p.name, p.category, oi.quantity, o.order_date FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.customer_id = $1 ORDER BY o.order_date DESC `; const result = await db.query(query, [customerId]); return result.rows; }; ``` --- ### 🧠 Рекомендательная система (внешний сервис на Python) - Python-сервис запускается отдельно (или через `child_process` из Node.js) - Читает данные из PostgreSQL (через `psycopg2`) - Обучает модель FP-Growth - Сохраняет правила в отдельную таблицу или JSON - Node.js запрашивает рекомендации по API или файлу --- ### 🗂️ Дополнительная таблица для рекомендаций ```sql -- Рекомендации (обновляются nightly) CREATE TABLE recommendations ( id SERIAL PRIMARY KEY, product_id INT REFERENCES products(product_id), recommended_skus JSONB, -- ['MILK001', 'BREAD002'] support NUMERIC(5,4), confidence NUMERIC(5,4), lift NUMERIC(5,4), updated_at TIMESTAMP DEFAULT NOW() ); ``` --- ## ✅ 2. **Пример на базе FP-Growth (Python + MLxtend)** ### 📦 Установка зависимостей ```bash pip install pandas psycopg2-binary mlxtend ``` ### 🐍 Код: FP-Growth для поиска ассоциативных правил ```python # fp_growth_recommender.py import pandas as pd import psycopg2 from mlxtend.frequent_patterns import fpgrowth, association_rules import json # Подключение к PostgreSQL conn = psycopg2.connect( host="localhost", database="b2b_food", user="your_user", password="your_password" ) # Загрузка данных query = """ SELECT o.order_id, p.sku FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id ORDER BY o.order_id, p.sku """ df = pd.read_sql(query, conn) conn.close() # Преобразуем в транзакции (каждый заказ — список SKU) basket = df.groupby('order_id')['sku'].apply(list).reset_index() # Создаём one-hot encoded таблицу from mlxtend.preprocessing import TransactionEncoder te = TransactionEncoder() te_ary = te.fit(basket['sku']).transform(basket['sku']) df_encoded = pd.DataFrame(te_ary, columns=te.columns_) # Поиск частых наборов frequent_itemsets = fpgrowth(df_encoded, min_support=0.01, use_colnames=True) # Генерация правил rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.3) rules = rules.sort_values('lift', ascending=False) # Фильтруем: только правила с 1 товаром в antecedents и 1 в consequents rules['antecedent_skus'] = rules['antecedents'].apply(lambda x: list(x)) rules['consequent_skus'] = rules['consequents'].apply(lambda x: list(x)) # Упрощаем: делаем mapping: из какого товара — что рекомендуем recommendations_map = {} for _, row in rules.iterrows(): for antecedent in row['antecedent_skus']: if antecedent not in recommendations_map: recommendations_map[antecedent] = [] for consequent in row['consequent_skus']: recommendations_map[antecedent].append({ 'recommended_sku': consequent, 'confidence': row['confidence'], 'lift': row['lift'] }) # Сохраняем в JSON with open('recommendations_fp.json', 'w', encoding='utf-8') as f: json.dump(recommendations_map, f, indent=2, ensure_ascii=False) print("✅ FP-Growth завершён. Рекомендации сохранены в recommendations_fp.json") ``` --- ### Пример вывода `recommendations_fp.json` ```json { "MILK001": [ { "recommended_sku": "BREAD002", "confidence": 0.75, "lift": 2.1 }, { "recommended_sku": "BUTTER003", "confidence": 0.68, "lift": 1.9 } ], "BREAD002": [ { "recommended_sku": "JAM004", "confidence": 0.62, "lift": 2.3 } ] } ``` --- ## ✅ 3. **MVP системы рекомендаций (документирован)** ### 🎯 Цель MVP Реализовать простую, но работающую систему, которая: - На основе истории заказов находит часто покупаемые вместе товары (FP-Growth) - Учитывает наличие на складе - Возвращает рекомендации при запросе товара --- ### 🧱 Компоненты MVP | Компонент | Технология | |---------|-----------| | Бэкенд | Node.js + Express | | БД | PostgreSQL | | ML | Python (FP-Growth) | | Кеширование | JSON-файл или Redis | | Интерфейс | REST API | --- ### 📁 Структура проекта ``` b2b-recommender/ │ ├── api/ # Node.js сервер │ ├── server.js │ ├── routes/recommend.js │ └── services/recommendService.js │ ├── ml/ # Python-скрипты │ ├── fp_growth_recommender.py │ └── recommendations_fp.json │ ├── data/ # SQL дампы (опционально) │ └── schema.sql │ ├── package.json └── README.md ``` --- ### 🚀 Запуск MVP #### Шаг 1: Запустить FP-Growth (раз в день) ```bash python ml/fp_growth_recommender.py ``` > Генерирует `recommendations_fp.json` #### Шаг 2: Запустить Node.js сервер ```js // api/server.js const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 3000; // Загружаем рекомендации при старте let recommendations = {}; try { const data = fs.readFileSync(path.join(__dirname, '../ml/recommendations_fp.json'), 'utf8'); recommendations = JSON.parse(data); } catch (err) { console.error('❌ Не удалось загрузить рекомендации:', err); } // API: GET /recommend?sku=MILK001 app.get('/recommend', (req, res) => { const { sku } = req.query; if (!sku) return res.status(400).json({ error: 'SKU обязателен' }); const recs = recommendations[sku] || []; // Здесь можно добавить фильтрацию по складу (пример) // Пока просто возвращаем res.json({ product_sku: sku, recommendations: recs.slice(0, 5) // топ-5 }); }); app.listen(PORT, () => { console.log(`✅ Сервер запущен на http://localhost:${PORT}`); }); ``` #### Шаг 3: Запрос ```bash curl "http://localhost:3000/recommend?sku=MILK001" ``` #### Ответ: ```json { "product_sku": "MILK001", "recommendations": [ { "recommended_sku": "BREAD002", "confidence": 0.75, "lift": 2.1 }, { "recommended_sku": "BUTTER003", "confidence": 0.68, "lift": 1.9 } ] } ``` --- ### ✅ Документация MVP (`README.md`) ```markdown # B2B Рекомендательная система (MVP) ## Описание Простая система рекомендаций на основе ассоциативных правил (FP-Growth) для B2B-портала с пищевыми продуктами. ## Функции - Находит товары, которые часто покупают вместе - Возвращает рекомендации по SKU - Учитывает confidence и lift ## Технологии - Node.js - PostgreSQL - Python + MLxtend - FP-Growth ## Запуск 1. Установи зависимости: ```bash npm install pip install pandas psycopg2-binary mlxtend ``` 2. Запусти ML-обработку: ```bash python ml/fp_growth_recommender.py ``` 3. Запусти сервер: ```bash node api/server.js ``` 4. Получи рекомендации: ```bash curl "http://localhost:3000/recommend?sku=MILK001" ``` ## Дальнейшее развитие - Добавить учёт складских остатков - Реализовать персонализацию - Внедрить A/B тесты - Перейти на LightGBM для ранжирования ``` --- ## ✅ Что можно улучшить в будущем | Функция | Как реализовать | |-------|----------------| | Учёт остатков | Добавить JOIN с `inventory` в Python-скрипт | | Персонализация | Добавить CF или embeddings | | Реальное время | Заменить JSON на Redis | | A/B тесты | Добавить `experiment_id` в API | | Интерфейс | Встроить в корзину клиента | --- ## 🎉 Готово! Ты получил: 1. Схему данных в PostgreSQL 2. Работающий пример на FP-Growth 3. Полноценный MVP с документацией Если хочешь — могу: - Добавить интеграцию с Redis - Сделать докеризацию - Написать unit-тесты - Реализовать обновление рекомендаций по cron Пиши, когда будешь готов к следующему шагу! 🚀 Отлично! Давай улучшим MVP: добавим **учёт остатков**, **персонализацию** и реализуем **A/B-тестирование на фронте**. Я покажу, как это сделать технически и архитектурно, с примерами кода и пояснениями. --- ## ✅ 1. Учёт складских остатков ### 🎯 Зачем? Нельзя рекомендовать товар, которого нет на складе или который заканчивается. ### 🔄 Изменения в Python-скрипте (fp_growth_recommender.py) ```python # fp_growth_recommender.py (обновлённая версия) import pandas as pd import psycopg2 import json from mlxtend.frequent_patterns import fpgrowth, association_rules from mlxtend.preprocessing import TransactionEncoder # Подключение к БД conn = psycopg2.connect( host="localhost", database="b2b_food", user="your_user", password="your_password" ) # 1. Загружаем транзакции query_orders = """ SELECT o.order_id, p.sku FROM order_items oi JOIN orders o ON oi.order_id = o.order_id JOIN products p ON oi.product_id = p.product_id ORDER BY o.order_id """ df_orders = pd.read_sql(query_orders, conn) # 2. Генерируем правила (как раньше) basket = df_orders.groupby('order_id')['sku'].apply(list) te = TransactionEncoder() te_ary = te.fit(basket).transform(basket) df_encoded = pd.DataFrame(te_ary, columns=te.columns_) frequent_itemsets = fpgrowth(df_encoded, min_support=0.01, use_colnames=True) rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.3) # 3. Загружаем остатки query_inventory = """ SELECT p.sku, i.stock_quantity, i.warehouse_region FROM inventory i JOIN products p ON i.product_id = p.product_id WHERE i.stock_quantity > 0 -- только доступные """ df_inventory = pd.read_sql(query_inventory, conn) conn.close() available_skus = set(df_inventory['sku'].unique()) # 4. Фильтруем рекомендации по остаткам recommendations_map = {} for _, row in rules.iterrows(): ant_skus = list(row['antecedents']) cons_skus = list(row['consequents']) for ant in ant_skus: if ant not in recommendations_map: recommendations_map[ant] = [] for con in cons_skus: if con in available_skus: # Только если есть на складе recommendations_map[ant].append({ 'recommended_sku': con, 'confidence': float(row['confidence']), 'lift': float(row['lift']) }) # Убираем дубли и сортируем по lift for sku in recommendations_map: recommendations_map[sku] = sorted( recommendations_map[sku], key=lambda x: x['lift'], reverse=True )[:10] # Топ-10 # Сохраняем with open('ml/recommendations_fp.json', 'w', encoding='utf-8') as f: json.dump(recommendations_map, f, indent=2, ensure_ascii=False) print("✅ Рекомендации с учётом остатков сохранены") ``` > Теперь в `recommendations_fp.json` — только **доступные товары**. --- ## ✅ 2. Персонализация рекомендаций ### 🎯 Идея Рекомендовать не просто "часто покупают с молоком", а **"клиенты как ты купили X"**. ### 🔧 Подход: User-Based + Item-Based гибрид #### Шаг 1: В Node.js — получаем рекомендации на основе: - Общих правил (FP-Growth) - Истории покупок клиента - Сегмента клиента (ресторан, магазин и т.д.) #### 📦 Расширим API ```js // routes/recommend.js const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); // Загружаем глобальные рекомендации let globalRecs = {}; try { const data = fs.readFileSync(path.join(__dirname, '../ml/recommendations_fp.json'), 'utf8'); globalRecs = JSON.parse(data); } catch (err) { console.error('❌ Не удалось загрузить рекомендации'); } // Имитация данных клиента (в реальности — из БД) const customerSegments = { 101: 'restaurant', 102: 'retail', 103: 'distributor' }; const customerHistory = { 101: ['MILK001', 'CHEESE005'], // ресторан 102: ['BREAD002', 'JAM004'], // ритейл }; // API: GET /recommend?sku=MILK001&customer_id=101 router.get('/', (req, res) => { const { sku, customer_id } = req.query; if (!sku) return res.status(400).json({ error: 'SKU обязателен' }); let baseRecs = globalRecs[sku] || []; // Если указан customer_id — персонализируем if (customer_id) { const segment = customerSegments[customer_id]; const history = customerHistory[customer_id] || []; // Пример персонализации: // Повышаем вес рекомендаций, если товар из той же категории, что и у клиента baseRecs = baseRecs.map(rec => { const isFamiliarCategory = history.some(h => h.startsWith(rec.recommended_sku.slice(0, 3))); return { ...rec, score: rec.lift * (isFamiliarCategory ? 1.5 : 1.0) // бонус за "похожесть" }; }); // Сортируем по скору baseRecs.sort((a, b) => b.score - a.score); } res.json({ product_sku: sku, recommendations: baseRecs.slice(0, 5) }); }); module.exports = router; ``` ### 🔄 Что можно улучшить: - Подтягивать сегмент и историю из PostgreSQL - Добавить эмбеддинги товаров (например, через Python и сохранять в JSON) - Использовать **collaborative filtering** для поиска похожих клиентов --- ## ✅ 3. A/B-тестирование на фронте ### 🎯 Цель Сравнить две стратегии рекомендаций: - **Группа A**: Обычные (FP-Growth + остатки) - **Группа B**: Персонализированные (с учётом сегмента и истории) --- ### 🔧 Реализация #### 1. Фронтенд: определение группы ```html
``` #### 2. Логирование событий (фронтенд) ```js function logImpression(recs, group) { fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: 'recommendation_impression', customer_id: customerId, ab_group: group, recommended_skus: recs.map(r => r.recommended_sku), timestamp: new Date().toISOString() }) }); } // При клике на рекомендацию function onRecommendClick(sku, group) { fetch('/api/log', { method: 'POST', body: JSON.stringify({ event: 'recommendation_click', customer_id: customerId, ab_group: group, clicked_sku: sku }) }); } ``` #### 3. Логирование в Node.js ```js // routes/log.js const logs = []; // В реальности — пиши в PostgreSQL или Kafka router.post('/', (req, res) => { logs.push(req.body); console.log('Лог:', req.body); res.status(200).send('OK'); }); ``` --- ### 📊 Как анализировать результаты? После недели теста: | Метрика | Группа A | Группа B | |--------|--------|--------| | CTR (click-through rate) | 3.2% | 5.7% ✅ | | Конверсия в покупку | 1.1% | 2.3% ✅ | | Доля рекомендованных в заказе | 8% | 14% ✅ | > Вывод: **Группа B (персонализация) лучше** → делаем её дефолтной. --- ### 🧪 Дополнительные идеи для A/B-тестов | Что тестируем | Вариант A | Вариант B | |--------------|---------|---------| | Количество рекомендаций | 3 | 5 | | Место в интерфейсе | В корзине | На карточке товара | | Тип рекомендаций | "Часто покупают вместе" | "Клиенты как вы купили" | | Цвет кнопки | Синий | Оранжевый | --- ## ✅ Обновлённая архитектура ``` [Frontend] ↓ (A/B группа, события) [Node.js API] ↓ [PostgreSQL] ←→ [Python (FP-Growth + inventory filter)] ↓ [Redis] ← кэш рекомендаций и логов ``` --- ## ✅ Итог: Что мы сделали | Функция | Готово? | Как | |-------|--------|-----| | Учёт остатков | ✅ | Фильтрация в Python по `inventory` | | Персонализация | ✅ | Учёт `customer_id`, сегмента, истории | | A/B-тестирование | ✅ | Группировка на фронте + логирование | | Масштабируемость | ⚠️ | Можно улучшить через Redis и cron | | Интерпретируемость | ✅ | FP-Growth + lift/confidence | --- ## 🚀 Следующие шаги (если захочешь развивать дальше) 1. **Заменить JSON на Redis** для хранения рекомендаций 2. **Запускать Python-скрипт по расписанию** (cron или Airflow) 3. **Добавить CatBoost для Learning to Rank** 4. **Реализовать online-обучение** через bandit-алгоритмы 5. **Интегрировать с email-рассылками** ("Вы купили X — попробуйте Y") --- Если хочешь — могу: - Сделать Docker-образы - Написать скрипт для Airflow - Реализовать CatBoost-ранжирование - Создать дашборд на Grafana Просто скажи, куда двигаться дальше! 🚀