Подготовка данных

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

Краткое содержание

В этой статье мы рассмотрим подготовку данных для рекомендательных систем с использованием библиотеки sota-recommender.

pip install sota-recommender

Загрузка датасетов

Библиотека предоставляет встроенные загрузчики для популярных датасетов и утилиту для создания синтетических данных.

MovieLens

MovieLens - самый популярный датасет для рекомендательных систем. Содержит рейтинги фильмов от пользователей.

from recommender.data import load_movielens

# Доступные размеры: '100k', '1m', '10m', '20m', '25m'
df = load_movielens(size='100k')

print(df.head())
print(f"Shape: {df.shape}")
print(f"Columns: {df.columns.tolist()}")

Выход:

   user_id  item_id  rating  timestamp
0        1      193     5.0  978300760
1        1      661     3.0  978302109
...

Shape: (100000, 4)
Columns: ['user_id', 'item_id', 'rating', 'timestamp']

Характеристики датасетов:

Размер Пользователи Items Рейтинги Размер файла
100k 943 1,682 100,000 ~5 MB
1m 6,040 3,706 1,000,209 ~24 MB
10m 69,878 10,677 10,000,054 ~265 MB
20m 138,493 27,278 20,000,263 ~533 MB
25m 162,541 59,047 25,000,095 ~666 MB

Amazon Reviews

Датасет отзывов товаров на Amazon. Поддерживаются различные категории.

from recommender.data import load_amazon

# Доступные категории: 'Books', 'Electronics', 'Movies', etc.
df = load_amazon(
    category='Books',
    max_reviews=100000  # Ограничение для быстрой загрузки
)

print(df.head())

Формат:

   user_id  item_id  rating  timestamp
0   A123...  B001...     5.0  1234567890
...

Book-Crossing

Датасет рейтингов книг.

from recommender.data import load_book_crossing

df = load_book_crossing()
print(f"Загружено {len(df)} рейтингов")

Синтетические данные

Для тестирования и экспериментов можно создать синтетический датасет:

from recommender.data import create_synthetic_dataset

df = create_synthetic_dataset(
    n_users=1000,        # Количество пользователей
    n_items=500,         # Количество items
    n_interactions=10000,# Количество взаимодействий
    implicit=True,       # Implicit (True) или explicit (False)
    seed=42              # Для воспроизводимости
)

print(df.head())

Загрузка собственных данных

Если у вас есть свои данные, приведите их к формату DataFrame с нужными колонками:

import pandas as pd

# Пример: загрузка из CSV
df = pd.read_csv('my_data.csv')

# Необходимые колонки:
# - user_id: ID пользователя (int или str)
# - item_id: ID item (int или str)
# - rating: рейтинг или 1 для implicit (float)
# - timestamp: временная метка (опционально, int или datetime)

# Переименование колонок при необходимости
df = df.rename(columns={
    'userId': 'user_id',
    'movieId': 'item_id',
    'score': 'rating',
    'datetime': 'timestamp'
})

print(df.head())

Создание InteractionDataset

InteractionDataset - основной класс для работы с данными взаимодействий в библиотеке.

Базовое создание

from recommender import InteractionDataset

# Создание датасета
dataset = InteractionDataset(
    df,
    implicit=True,  # Тип данных: implicit или explicit
    min_user_interactions=0,  # Минимум взаимодействий на пользователя
    min_item_interactions=0   # Минимум взаимодействий на item
)

# Информация о датасете
print(f"Пользователей: {dataset.n_users}")
print(f"Items: {dataset.n_items}")
print(f"Взаимодействий: {len(dataset.data)}")
print(f"Плотность: {dataset.density:.4f}")

Implicit vs Explicit

Implicit feedback - бинарные взаимодействия (клик, просмотр, покупка):

  • Значения: 0 или 1
  • Используется для: EASE, SLIM, ALS, NCF, LightGCN, SASRec
# Implicit feedback
dataset = InteractionDataset(df, implicit=True)

Explicit feedback - явные рейтинги (1-5 звёзд):

  • Значения: непрерывные (обычно 1-5)
  • Используется для: SVD, SVD++
# Explicit feedback
dataset = InteractionDataset(df, implicit=False)

Фильтрация данных

Часто нужно отфильтровать редких пользователей и items:

dataset = InteractionDataset(
    df,
    implicit=True,
    min_user_interactions=5,  # Минимум 5 взаимодействий на пользователя
    min_item_interactions=3   # Минимум 3 взаимодействия на item
)

# Фильтрация происходит итеративно до сходимости

Работа с данными

# Получить все данные
data = dataset.data  # DataFrame

# Конвертация в разреженную матрицу
matrix = dataset.to_csr_matrix()  # scipy.sparse.csr_matrix
print(f"Matrix shape: {matrix.shape}")  # (n_users, n_items)

# Получить items для пользователя
user_items = dataset.get_user_items(user_id=1)
print(f"User 1 items: {user_items}")

# Получить пользователей для item
item_users = dataset.get_item_users(item_id=100)
print(f"Item 100 users: {item_users}")

Препроцессинг

Библиотека предоставляет различные функции препроцессинга.

Фильтрация по количеству взаимодействий

from recommender.data import filter_by_interaction_count

# Фильтрация редких пользователей и items
df_filtered = filter_by_interaction_count(
    df,
    min_user_interactions=5,
    min_item_interactions=5,
    max_iterations=10  # Максимум итераций
)

print(f"До: {len(df)} взаимодействий")
print(f"После: {len(df_filtered)} взаимодействий")

Бинаризация (преобразование в implicit)

from recommender.data import binarize_implicit_feedback

# Преобразование explicit ratings в implicit feedback
# Например, рейтинг >= 4.0 считается положительным
df_implicit = binarize_implicit_feedback(
    df,
    threshold=4.0,  # Порог для положительного feedback
    rating_col='rating'
)

# Теперь rating = 1 для всех положительных взаимодействий
print(df_implicit['rating'].unique())  # [1]

Нормализация рейтингов

from recommender.data import normalize_ratings

# Нормализация рейтингов (для explicit feedback)
df_norm = normalize_ratings(
    df,
    method='minmax',  # 'minmax', 'standard', или 'none'
    rating_col='rating'
)

# MinMax: приводит к [0, 1]
# Standard: z-score нормализация (mean=0, std=1)

Обработка временных меток

from recommender.data import add_time_features

# Добавление временных признаков
df_with_time = add_time_features(df, timestamp_col='timestamp')

# Добавляются колонки:
# - hour: час дня (0-23)
# - day_of_week: день недели (0-6)
# - month: месяц (1-12)
# - year: год

Разделение данных

Правильное разделение данных на train/val/test критично для корректной оценки модели.

Random Split

Случайное разделение - самая простая стратегия:

# Разделение на train и test
train_data, test_data = dataset.split(
    test_size=0.2,
    strategy='random',
    seed=42
)

print(f"Train: {len(train_data)} взаимодействий")
print(f"Test: {len(test_data)} взаимодействий")

# Разделение на train, validation и test
train_data, val_data, test_data = dataset.split(
    test_size=0.2,
    val_size=0.1,  # 10% для validation
    strategy='random',
    seed=42
)

Temporal Split

Разделение по времени - более реалистичная оценка (требуется timestamp):

train_data, test_data = dataset.split(
    test_size=0.2,
    strategy='temporal',
    seed=42
)

# Train содержит ранние взаимодействия
# Test содержит поздние взаимодействия

Когда использовать:

  • Когда важна временная динамика
  • Для sequential моделей (SASRec)
  • Для более реалистичной оценки

Leave-One-Out Split

Последнее взаимодействие каждого пользователя идёт в test:

train_data, test_data = dataset.split(
    strategy='leave_one_out',
    seed=42
)

# Для каждого пользователя:
# - Train: все взаимодействия кроме последнего
# - Test: последнее взаимодействие

Когда использовать:

  • Стандартная практика в sequential рекомендациях
  • Когда важно предсказать следующее действие
  • Для SASRec и подобных моделей

Stratified Split

Сохранение распределения по пользователям:

from recommender.data import stratified_split

train, test = stratified_split(
    df,
    test_size=0.2,
    user_col='user_id'
)

# Гарантирует, что каждый пользователь есть в train и test

Negative Sampling

Для implicit feedback часто нужны негативные примеры (items, с которыми пользователь НЕ взаимодействовал).

Стратегии семплирования

Библиотека предоставляет 5 стратегий negative sampling:

1. Uniform Sampling

Равномерное случайное семплирование из всех items:

from recommender.data import UniformSampler, create_negative_samples

# Создание sampler
sampler = UniformSampler(
    n_items=dataset.n_items,
    seed=42
)

# Генерация негативных примеров
train_with_negatives = create_negative_samples(
    interactions_df=train_data.data,
    sampler=sampler,
    n_negatives_per_positive=4  # 4 негативных на 1 позитивный
)

print(f"Исходно: {len(train_data.data)} примеров")
print(f"С негативами: {len(train_with_negatives)} примеров")

2. Popularity-based Sampling

Семплирование пропорционально популярности items:

from recommender.data import PopularitySampler

# Подсчёт популярности
item_popularity = train_data.data['item_id'].value_counts().to_dict()

# Создание sampler
sampler = PopularitySampler(
    n_items=dataset.n_items,
    item_popularity=item_popularity,
    seed=42
)

train_with_negatives = create_negative_samples(
    train_data.data,
    sampler,
    n_negatives_per_positive=4
)

Преимущество: Более сложные негативные примеры (популярные items сложнее отличить).

3. In-batch Negative Sampling

Использование позитивов других пользователей в батче как негативов:

from recommender.data import InBatchSampler

sampler = InBatchSampler()

# Используется внутри DataLoader во время обучения
# Эффективно для deep learning моделей

4. DNS (Dynamic Negative Sampling)

Семплирование сложных негативов на основе текущей модели:

from recommender.data import DNSSampler

sampler = DNSSampler(
    model=model,  # Текущая модель
    n_candidates=100  # Количество кандидатов для выбора
)

# Выбирает негативы с высоким score (сложные для модели)

5. Hard Negative Mining

Выбор самых сложных негативов:

from recommender.data import HardNegativeSampler

sampler = HardNegativeSampler(
    model=model,
    difficulty='hard'  # 'easy', 'medium', 'hard'
)

Использование в обучении

# Создание обучающих данных с негативами
train_with_negatives = create_negative_samples(
    train_data.data,
    sampler=UniformSampler(n_items=dataset.n_items),
    n_negatives_per_positive=4
)

# Добавляется колонка 'label': 1 для позитивов, 0 для негативов
print(train_with_negatives['label'].value_counts())
# 1    10000  (позитивы)
# 0    40000  (негативы)

# Использование для обучения
model = NCFRecommender()
model.fit(train_with_negatives)

Создание последовательностей

Для sequential моделей (SASRec) нужно создавать последовательности действий пользователей.

Основное использование

from recommender.data import create_sequences

# Создание последовательностей
sequences, targets = create_sequences(
    df,
    max_seq_length=50,  # Максимальная длина последовательности
    user_col='user_id',
    item_col='item_id',
    time_col='timestamp'  # Важно: сортировка по времени
)

print(f"Создано {len(sequences)} последовательностей")
print(f"Пример последовательности: {sequences[0]}")
print(f"Соответствующий target: {targets[0]}")

Формат:

sequences[0]  # [item1, item2, item3, ..., item_n]
targets[0]    # item_{n+1}  (следующий item для предсказания)

Padding

Короткие последовательности дополняются padding'ом:

# Последовательности короче max_seq_length дополняются нулями слева
sequences = [
    [0, 0, 0, 5, 12, 34],  # короткая последовательность
    [1, 3, 7, 11, 23, 45]  # полная последовательность
]

Sliding Window

Создание множественных подпоследовательностей:

from recommender.data import create_sequences_sliding_window

sequences, targets = create_sequences_sliding_window(
    df,
    max_seq_length=10,
    stride=5  # Сдвиг окна
)

# Из последовательности [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
# Создаёт:
# seq1: [1,2,3,4,5,6,7,8,9,10] -> target: 11
# seq2: [6,7,8,9,10,11,12,13,14,15] -> target: 16
# и т.д.

Примеры полных pipeline'ов

Pipeline 1: Implicit Feedback

from recommender import InteractionDataset, load_movielens
from recommender.data import filter_by_interaction_count, binarize_implicit_feedback

# 1. Загрузка
df = load_movielens(size='1m')

# 2. Фильтрация
df = filter_by_interaction_count(df, min_user_interactions=5, min_item_interactions=5)

# 3. Бинаризация
df = binarize_implicit_feedback(df, threshold=4.0)

# 4. Создание датасета
dataset = InteractionDataset(df, implicit=True)

# 5. Разделение
train, test = dataset.split(test_size=0.2, strategy='random', seed=42)

print(f"✅ Готов к обучению: {len(train)} train, {len(test)} test")

Pipeline 2: Explicit Feedback

# 1. Загрузка
df = load_movielens(size='100k')

# 2. Фильтрация
df = filter_by_interaction_count(df, min_user_interactions=10)

# 3. Нормализация (опционально)
from recommender.data import normalize_ratings
df = normalize_ratings(df, method='minmax')

# 4. Создание датасета
dataset = InteractionDataset(df, implicit=False)

# 5. Разделение
train, test = dataset.split(test_size=0.2, strategy='random', seed=42)

print(f"✅ Готов к обучению explicit модели")

Pipeline 3: Sequential Data

from recommender.data import create_sequences

# 1. Загрузка (важно: нужен timestamp!)
df = load_movielens(size='1m')

# 2. Фильтрация
df = filter_by_interaction_count(df, min_user_interactions=10)

# 3. Бинаризация
df = binarize_implicit_feedback(df, threshold=4.0)

# 4. Создание последовательностей
sequences, targets = create_sequences(
    df,
    max_seq_length=50,
    user_col='user_id',
    item_col='item_id',
    time_col='timestamp'
)

# 5. Temporal split
dataset = InteractionDataset(df, implicit=True)
train, test = dataset.split(test_size=0.2, strategy='temporal')

print(f"✅ Готов к обучению SASRec")

Best Practices

1. Фильтрация данных

# ✅ Хорошо: фильтруйте редких пользователей/items
dataset = InteractionDataset(
    df,
    min_user_interactions=5,
    min_item_interactions=5
)

# ❌ Плохо: оставляете пользователей с 1-2 взаимодействиями
# Это приводит к переобучению и плохой генерализации

2. Выбор стратегии split

# ✅ Хорошо: temporal split для realistic оценки
train, test = dataset.split(strategy='temporal')

# ⚠️ Осторожно: random split может быть слишком оптимистичным
# Используйте random только для быстрого прототипирования

3. Negative sampling

# ✅ Хорошо: используйте 4-10 негативов на позитив
sampler = UniformSampler(n_items=dataset.n_items)
train_data = create_negative_samples(train, sampler, n_negatives_per_positive=4)

# ❌ Плохо: слишком мало или слишком много негативов
# <4: модель не учится различать
# >20: замедляет обучение без улучшения качества

4. Сохранение воспроизводимости

# ✅ Всегда указывайте seed
dataset.split(test_size=0.2, seed=42)
sampler = UniformSampler(n_items=n, seed=42)

Что дальше?

Теперь, когда вы знаете как подготовить данные, можно переходить к моделям:

AI на дровах 🪵
Привет! Меня зовут Семён, я работаю в сфере ML и аналитики данных и пишу в блог nerdit.ru статьи о своем опыте и том, что может пригодиться начинающим в начале их пути изучения больших данных.

Подписаться на новости Nerd IT

Не пропустите последние выпуски. Зарегистрируйтесь сейчас, чтобы получить полный доступ к статьям.
jamie@example.com
Подписаться