Условие
В мобильной игре каждый матч сводит двух игроков. Для каждого игрока есть две числовые фичи feature_1 и feature_2. Нужно построить модель, бинарно классифицирующую пары игроков на «сбалансированный матч» / «несбалансированный».
Дан train-сэмпл (player1_features, player2_features, target) (без явных уровней опытности — целевая переменная — единственный сигнал) и test-сэмпл без меток.
Метрика: матч считается «сбалансированным», если игроки одинакового уровня опытности (явно — это скрытая семантика). Финальная валидация — процент сбалансированных матчей на тестовой выборке: матчи группируются по уровням опытности (скрытой) согласно предсказаниям модели; среди матчей одного уровня нас интересует, какая доля на самом деле сбалансирована. Целевой порог: ≥ 65%, что эквивалентно условию |Accuracy − 1/2| ≥ √20/20 ≈ 0.27 (обоснование в условии).
Решение
Подход
Из условия видно, что вход — пары игроков; без явного «уровня». Идея в том, чтобы модель выучила:
- какой признак характеризует игрока (можно агрегировать пары),
- сбалансированный матч ↔ игроки близки по этому скрытому уровню.
Двухуровневый pipeline:
- Embedding игрока: построить признаки для одного игрока, например, через
(feature_1, feature_2), нормализованные, и трансформацииf1·f2,f1²,f2². - Признаки пары: разности и произведения полученных embedding'ов:
Это симметричный (по перестановке игроков) набор фич — важно, иначе модель будет учиться на порядке.x_pair = (|emb_p1 − emb_p2|, emb_p1 + emb_p2, emb_p1 ⊙ emb_p2) - Классификатор: 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, то в группе матчей одного предсказанного уровня доля действительно одного уровня будет a² (или (1 − a)² если предсказали неправильно — это ещё хуже). Чтобы достичь 65%, нужно a > 0.81 или a < 0.19 (т. е. модель содержательная, не равноценная). В терминах |accuracy − 0.5| ≥ √20/20 ≈ 0.27.
Подводные камни
- Симметрия пары игроков. Если фичи зависят от порядка
(p1, p2), модель «выучит порядок» и не обобщится. Используйте симметричные функции (sum/abs-diff/product). - Лик target. Если в
testесть пары, повторяющиеся вtrain(с теми же игроками) — обязательно нужна группа поplayer_idв CV. - Несбалансированные классы. Балансированных матчей часто меньше —
class_weight='balanced'илиscale_pos_weight. - Калибровка. Для метрики типа accuracy чёткое решение «> 0.5» зависит от калибровки; используйте
isotonicилиPlattесли нужно подвинуть порог. - Игнорировать корреляцию между фичами и популяцией. Если в
trainмного пар с очень разными игроками, а вtest— близкие, распределение сдвинуто (covariate shift).
Эталонный ответ
Симметричный набор pair-фич (|p1−p2|, p1+p2, p1·p2) + GBM (CatBoost) с CV → accuracy ≥ 0.81. Это даёт |acc − 0.5| ≥ 0.27 и обеспечивает целевые 65% в групповой метрике.