Перейти к содержанию

Требования к данным рекомендательной системы

~4 минуты чтения

Предварительно: Определение задачи | Компоненты системы | Feature Engineering

Качество рекомендательной системы на 80% определяется данными и features, а не архитектурой модели. Netflix тратит ~$1B/год на контент, но именно данные о 230M пользователях (5B+ событий/день) позволяют персонализировать каталог из 15K+ тайтлов. Типичная production система использует 500-1000 features из 4 источников: user data, item data, interaction data и context. Feature engineering для рекомендаций -- это отдельная дисциплина, где training-serving skew является причиной #1 деградации моделей в production.

Источники данных

1. User Data (Данные пользователей)

-- Таблица пользователей
CREATE TABLE users (
    user_id BIGINT PRIMARY KEY,
    created_at TIMESTAMP,
    country VARCHAR(2),
    language VARCHAR(5),
    device_type VARCHAR(20),
    subscription_tier VARCHAR(20),
    age_group VARCHAR(10),
    gender VARCHAR(10)
);

Feature Engineering:

user_features = {
    # Демография
    "user_age_bucket": "25-34",
    "user_country": "RU",
    "user_language": "ru",

    # Активность
    "days_since_signup": 365,
    "total_sessions": 500,
    "avg_session_duration_min": 25,
    "last_active_hours_ago": 2,

    # Предпочтения (агрегаты)
    "favorite_categories": ["action", "comedy"],
    "avg_item_price_viewed": 1500.0,
    "preferred_time_of_day": "evening",

    # Embeddings
    "user_embedding": [0.1, 0.2, ...],  # 128-dim
}

2. Item Data (Данные items)

-- Таблица items (товары/контент)
CREATE TABLE items (
    item_id BIGINT PRIMARY KEY,
    title VARCHAR(500),
    description TEXT,
    category_id INT,
    subcategory_id INT,
    brand VARCHAR(100),
    price DECIMAL(10,2),
    created_at TIMESTAMP,
    is_active BOOLEAN,
    content_type VARCHAR(50)
);

-- Таблица атрибутов
CREATE TABLE item_attributes (
    item_id BIGINT,
    attribute_name VARCHAR(100),
    attribute_value VARCHAR(500)
);

Feature Engineering:

item_features = {
    # Метаданные
    "item_category": "electronics",
    "item_subcategory": "smartphones",
    "item_brand": "Apple",
    "item_price": 99999,
    "item_age_days": 30,

    # Популярность
    "views_7d": 10000,
    "clicks_7d": 500,
    "purchases_7d": 50,
    "ctr_7d": 0.05,
    "conversion_rate_7d": 0.005,

    # Content features
    "title_embedding": [0.3, 0.1, ...],  # 768-dim BERT
    "image_embedding": [0.5, 0.2, ...],  # 512-dim ResNet

    # Quality signals
    "avg_rating": 4.5,
    "num_reviews": 1000,
    "return_rate": 0.02,
}

3. Interaction Data (Взаимодействия)

-- Event log
CREATE TABLE interactions (
    event_id BIGINT PRIMARY KEY,
    user_id BIGINT,
    item_id BIGINT,
    event_type VARCHAR(20),  -- view, click, add_to_cart, purchase
    timestamp TIMESTAMP,
    session_id VARCHAR(50),
    device_type VARCHAR(20),
    position INT,  -- позиция в списке
    source VARCHAR(50)  -- homepage, search, recommendation
);

Типы событий:

event_types = {
    "impression": 0.0,    # Показан в списке
    "view": 0.1,          # Открыл страницу
    "click": 0.3,         # Кликнул
    "add_to_cart": 0.5,   # Добавил в корзину
    "purchase": 1.0,      # Купил
    "rating_5": 1.0,      # Поставил 5 звёзд
    "rating_1": -1.0,     # Поставил 1 звезду
    "skip": -0.2,         # Пропустил
    "hide": -0.5,         # Скрыл
}

4. Context Data (Контекст)

context_features = {
    # Время
    "hour_of_day": 20,
    "day_of_week": 5,  # Friday
    "is_weekend": True,
    "is_holiday": False,

    # Устройство
    "device_type": "mobile",
    "os": "iOS",
    "app_version": "5.2.1",

    # Сессия
    "items_viewed_this_session": 5,
    "search_query": "iphone case",
    "referring_page": "search_results",

    # Геолокация
    "city": "Moscow",
    "timezone": "Europe/Moscow",
}

Data Pipeline Architecture

graph TD
    AE["App Events<br/>(Kafka)"] --> DL["Data Lake<br/>(S3/GCS)"]
    DB["Database<br/>(PostgreSQL)"] --> DL
    TP["3rd Party<br/>(APIs)"] --> DL
    CT["Content<br/>(S3)"] --> DL

    DL --> FE["Feature Engineering<br/>(Spark/Flink)"]

    FE --> FS["Feature Store<br/>(Feast/Tecton)<br/>Online: Redis/DynamoDB<br/>Offline: S3/BigQuery"]
    FE --> TD["Training Data<br/>(Parquet/TFRecord)<br/>Historical pairs with labels"]

    style AE fill:#e8eaf6,stroke:#3f51b5
    style DB fill:#e8eaf6,stroke:#3f51b5
    style TP fill:#e8eaf6,stroke:#3f51b5
    style CT fill:#e8eaf6,stroke:#3f51b5
    style DL fill:#fff3e0,stroke:#ef6c00
    style FE fill:#f3e5f5,stroke:#9c27b0
    style FS fill:#e8f5e9,stroke:#4caf50
    style TD fill:#e8f5e9,stroke:#4caf50

Feature Store Schema

Online Features (низкая latency)

# Redis/DynamoDB
user_online_features = {
    "user:123": {
        "last_viewed_items": [1, 5, 3, 8],  # последние 10
        "last_categories": ["electronics", "books"],
        "session_click_count": 5,
        "user_embedding": [0.1, 0.2, ...],
    }
}

item_online_features = {
    "item:456": {
        "click_count_1h": 150,
        "purchase_count_1h": 10,
        "current_stock": 500,
        "item_embedding": [0.3, 0.1, ...],
    }
}

Offline Features (batch computed)

# Parquet/BigQuery
user_offline_features = {
    "user_id": 123,
    "total_purchases_30d": 5,
    "total_spend_30d": 15000.0,
    "category_affinity_vector": [0.8, 0.2, 0.1, ...],  # per category
    "brand_affinity_vector": [0.5, 0.3, ...],
    "price_sensitivity": 0.7,  # 0=cheap, 1=expensive
}

Training Data Format

Pointwise (для classification/regression)

# Каждый пример = (user, item, label)
{
    "user_id": 123,
    "item_id": 456,
    "user_features": {...},
    "item_features": {...},
    "context_features": {...},
    "label": 1,  # clicked or not
    "weight": 1.0,  # importance
}

Pairwise (для ranking)

# Каждый пример = (user, positive_item, negative_item)
{
    "user_id": 123,
    "positive_item_id": 456,  # clicked
    "negative_item_id": 789,  # not clicked
    "user_features": {...},
    "positive_item_features": {...},
    "negative_item_features": {...},
}

Listwise (для ranking)

# Каждый пример = (user, [items], [relevance_scores])
{
    "user_id": 123,
    "candidate_items": [456, 789, 101, 202],
    "relevance_labels": [3, 0, 1, 2],  # graded relevance
    "user_features": {...},
    "items_features": [{...}, {...}, {...}, {...}],
}

Data Quality Checks

# Great Expectations / Deequ checks
data_quality_checks = {
    "users": [
        "user_id is unique",
        "created_at is not null",
        "country is in valid_countries",
    ],
    "items": [
        "item_id is unique",
        "price > 0",
        "category_id exists in categories table",
    ],
    "interactions": [
        "user_id exists in users",
        "item_id exists in items",
        "timestamp is not in future",
        "event_type in valid_event_types",
    ],
    "features": [
        "no nulls in required features",
        "embeddings have correct dimension",
        "numerical features in expected range",
    ],
}

Data Freshness Requirements

Data Type Update Frequency Latency Tolerance
User profile Daily 24 hours
Item catalog Hourly 1 hour
Interactions (batch) Hourly 1 hour
Interactions (streaming) Real-time < 1 min
Embeddings Daily 24 hours
Popularity scores Hourly 1 hour

Privacy & Compliance

  1. PII Handling: Hash/encrypt user identifiers
  2. Data Retention: Delete interactions older than X days
  3. Right to be Forgotten: Ability to delete user data
  4. Consent: Only use data with user consent
  5. Anonymization: Aggregate data for analytics

Заблуждение: Для обучения достаточно положительных примеров (кликов)

Модель, обученная только на positive samples, не умеет различать хорошее от среднего. Для качественного ranking нужны negative samples. Но random negatives слишком легкие -- модель не учится. Оптимальная стратегия: hard negatives (items, показанные но не кликнутые на позициях 1-10) + in-batch negatives + easy negatives (random). Соотношение positive:negative обычно 1:4 до 1:10.

Заблуждение: Implicit feedback (клики) = explicit feedback (рейтинги)

Клик != релевантность. Пользователь может кликнуть из любопытства (clickbait CTR до 15%, но watch time < 10 секунд). Для рекомендаций лучше использовать weighted implicit feedback: impression=0, click=0.3, add_to_cart=0.5, purchase=1.0, return=-0.5. YouTube перешёл от оптимизации CTR к watch time и увидел рост долгосрочного engagement на 70%.

Заблуждение: Training-serving skew -- редкая проблема

По данным Google, training-serving skew -- причина #1 деградации ML-моделей в production. Типичный пример: при training используется avg_rating из BigQuery (обновляется раз в сутки), а при serving -- из Redis (обновляется в real-time). Feature Store (Feast/Tecton) решает проблему единым вычислением features для обоих сценариев. Без него модель видит разные данные при training и inference.

Собеседование

Какие данные нужны для рекомендательной системы?

❌ "Нам нужны user-item interactions -- клики и покупки."

✅ "Четыре категории данных: (1) User data -- демография, активность, preferences embedding (128-dim); (2) Item data -- метаданные, popularity scores, content embeddings (BERT 768-dim для текста, ResNet 512-dim для изображений); (3) Interaction data -- event log с весами (impression=0, click=0.3, purchase=1.0), position bias correction; (4) Context -- время, устройство, сессия, геолокация. Итого 500-1000 features на пару user-item."

Как организовать Feature Store?

❌ "Храним всё в Redis, подтягиваем при каждом запросе."

✅ "Двухслойная архитектура: Online store (Redis Cluster, 10 shards, < 5ms) для real-time features (last actions, session data, trending scores). Offline store (S3/BigQuery) для batch features (30-day aggregates, embeddings, affinity vectors). Feature Registry обеспечивает единую точку определения features для training и serving -- это решает training-serving skew. Feast или Tecton как orchestration layer. Критично: одна и та же feature computation pipeline для training data и online serving."

Как формировать training data для ranking?

❌ "Берём все клики как positive, всё остальное как negative."

✅ "Три формата: Pointwise (user, item, label) для CTR prediction, Pairwise (user, pos_item, neg_item) для ranking, Listwise (user, [items], [grades]) для NDCG-оптимизации. Negative sampling стратегия: hard negatives (показанные но не кликнутые, позиции 1-10), in-batch negatives, easy negatives (random). Ratio 1:4-1:10. Важно: position bias correction через Inverse Propensity Weighting, иначе модель учит позицию, а не релевантность."