Собесов

Яндекс ML — TwoFeatureGame: предсказать баланс матча по двум признакам

ML / Data ScienceБинарная классификацияСложнаяSenior

Условие

В мобильной игре каждый матч сводит двух игроков. Для каждого игрока есть две числовые фичи feature_1 и feature_2. Нужно построить модель, бинарно классифицирующую пары игроков на «сбалансированный матч» / «несбалансированный».

Дан train-сэмпл (player1_features, player2_features, target) (без явных уровней опытности — целевая переменная — единственный сигнал) и test-сэмпл без меток.

Метрика: матч считается «сбалансированным», если игроки одинакового уровня опытности (явно — это скрытая семантика). Финальная валидация — процент сбалансированных матчей на тестовой выборке: матчи группируются по уровням опытности (скрытой) согласно предсказаниям модели; среди матчей одного уровня нас интересует, какая доля на самом деле сбалансирована. Целевой порог: ≥ 65%, что эквивалентно условию |Accuracy − 1/2| ≥ √20/20 ≈ 0.27 (обоснование в условии).

Решение

Подход

Из условия видно, что вход — пары игроков; без явного «уровня». Идея в том, чтобы модель выучила:

  • какой признак характеризует игрока (можно агрегировать пары),
  • сбалансированный матч ↔ игроки близки по этому скрытому уровню.

Двухуровневый pipeline:

  1. Embedding игрока: построить признаки для одного игрока, например, через (feature_1, feature_2), нормализованные, и трансформации f1·f2, f1², f2².
  2. Признаки пары: разности и произведения полученных embedding'ов:
    x_pair = (|emb_p1 − emb_p2|, emb_p1 + emb_p2, emb_p1 ⊙ emb_p2)
    
    Это симметричный (по перестановке игроков) набор фич — важно, иначе модель будет учиться на порядке.
  3. Классификатор: GBM (CatBoost / LightGBM) с logloss / AUC на пары; с калибровкой через CalibratedClassifierCV.

Реализация

import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score
from catboost import CatBoostClassifier
 
 
def make_pair_features(p1: np.ndarray, p2: np.ndarray) -> np.ndarray:
    # симметричные фичи
    diff = np.abs(p1 - p2)
    summ = p1 + p2
    prod = p1 * p2
    return np.concatenate([diff, summ, prod], axis=1)
 
 
def fit_predict(train: pd.DataFrame, test: pd.DataFrame):
    # train: ['p1_f1', 'p1_f2', 'p2_f1', 'p2_f2', 'target']
    p1 = train[['p1_f1', 'p1_f2']].values
    p2 = train[['p2_f1', 'p2_f2']].values
    y = train['target'].values
    X = make_pair_features(p1, p2)
 
    model = CatBoostClassifier(
        depth=6, iterations=2000, learning_rate=0.05,
        loss_function='Logloss', eval_metric='Accuracy',
        verbose=0, random_seed=42,
    )
 
    # CV для отбора лучшей итерации
    cv = StratifiedKFold(5, shuffle=True, random_state=42)
    oof = np.zeros(len(y))
    for tr, va in cv.split(X, y):
        m = model.copy()
        m.fit(X[tr], y[tr], eval_set=(X[va], y[va]), use_best_model=True)
        oof[va] = m.predict_proba(X[va])[:, 1]
 
    print("CV accuracy:", accuracy_score(y, (oof > 0.5).astype(int)))
 
    # финальная модель на всём
    model.fit(X, y, verbose=0)
 
    Xte = make_pair_features(
        test[['p1_f1', 'p1_f2']].values,
        test[['p2_f1', 'p2_f2']].values,
    )
    return (model.predict_proba(Xte)[:, 1] > 0.5).astype(int)

Анализ метрики

Условие пишет: «процент сбалансированных матчей среди игроков одного уровня». Если модель «угадывает уровень» с accuracy a, то в группе матчей одного предсказанного уровня доля действительно одного уровня будет (или (1 − a)² если предсказали неправильно — это ещё хуже). Чтобы достичь 65%, нужно a > 0.81 или a < 0.19 (т. е. модель содержательная, не равноценная). В терминах |accuracy − 0.5| ≥ √20/20 ≈ 0.27.

Подводные камни

  1. Симметрия пары игроков. Если фичи зависят от порядка (p1, p2), модель «выучит порядок» и не обобщится. Используйте симметричные функции (sum/abs-diff/product).
  2. Лик target. Если в test есть пары, повторяющиеся в train (с теми же игроками) — обязательно нужна группа по player_id в CV.
  3. Несбалансированные классы. Балансированных матчей часто меньше — class_weight='balanced' или scale_pos_weight.
  4. Калибровка. Для метрики типа accuracy чёткое решение «> 0.5» зависит от калибровки; используйте isotonic или Platt если нужно подвинуть порог.
  5. Игнорировать корреляцию между фичами и популяцией. Если в train много пар с очень разными игроками, а в test — близкие, распределение сдвинуто (covariate shift).

Эталонный ответ

Симметричный набор pair-фич (|p1−p2|, p1+p2, p1·p2) + GBM (CatBoost) с CV → accuracy ≥ 0.81. Это даёт |acc − 0.5| ≥ 0.27 и обеспечивает целевые 65% в групповой метрике.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти