Skip to content

Feature Audit Report — SoccerPredictAI

Date: 2026-04-24 Auditor: GitHub Copilot (Claude Sonnet 4.6) Scope: Feature pipeline — engineering, leakage, contract, training ↔ inference consistency Method: прямой анализ кода (src/features/, src/pipelines/features.py, src/pipelines/inference.py, params.yaml)


1. Feature Inventory

Группы features (по коду и features_meta)

Группа Prefix паттерн Стороны Windows Статус
Rolling stats (all) all_{metric}_mean_w{n} + all_coverage_w{n} home, away, diff 1,2,3,5,10
Rolling stats (season) season_{metric}_mean_w{n} + season_coverage_w{n} home, away, diff 1,2,3,5,10
Rolling stats (tournament) tournament_{metric}_mean_w{n} + tournament_coverage_w{n} home, away, diff 1,2,3,5,10
Rolling stats (home/away split) ha_{metric}_mean_w{n} + ha_coverage_w{n} home, away, diff 1,2,3,5,10
ELO home_elo_pre, away_elo_pre, diff_elo_pre home, away, diff n/a (window=0)
H2H ❌ не реализовано
rest_days ❌ не реализовано

Stats cols: win, draw, loss, goals_for, goals_against (5 метрик × 4 группы × 5 windows = 100 rolling mean + 20 coverage = 120 rolling features, × 3 стороны = 360 + 3 ELO = 363 features в features.parquet)

Model input (classification): отфильтровано через select_model_features() — side=diff, window_sizes=[1,3], include_elo=True → diff_* (w1,w3) + diff_elo_pre + sex

features.parquet vs features_meta.parquet

  • features_meta.parquet строится из parse_feature(col) для rolling stats + ручные записи для ELO
  • parse_feature() использует col.rsplit("_w", 1) — применимо только к rolling features с суффиксом _w{n}
  • ⚠️ P1: parse_feature() будет ломаться при попытке парсинга ELO-колонок (нет _w суффикса) — ELO добавляется в meta отдельно вручную, но features.py сначала пытается вызвать parse_feature для всех колонок df_features.columns включая ELO

Проверка: df_features_meta = pd.DataFrame(parse_feature(col) for col in df_features.columns) — если ELO-колонки уже добавлены к df_features до построения meta, это вызовет исключение при rsplit("_w", 1).

Фактически: ELO добавляется к df_features после строки df_features_meta = pd.DataFrame(parse_feature(col) for col in df_features.columns) — порядок корректный, мета строится только из rolling-колонок. ✅


2. Feature Groups Audit

Rolling stats

Группа Training Inference (batch) В features_meta Консистентность
all
season
tournament
ha (home/away)

Вывод: Все 4 группы rolling stats реализованы одинаково в features.py (training) и inference.py (batch serving) — использование одних и тех же функций build_team_match_table, add_rolling_features, to_match_level.

ELO

Аспект Training Inference (batch) В features_meta
Реализован
Параметры из params.yaml
Pre-match (no leakage) ✅ (documented in elo.py)
group_col = tournamentId

H2H

  • Не реализован. select_model_features() принимает include_h2h=False — явно выключен в API и помечен как «kept for forward compatibility».

rest_days

  • Не реализован. Аналогично H2H — include_rest_days=True в сигнатуре, но в features_meta нет записей с metric == "rest_days".

3. Train ↔ Inference Consistency (КРИТИЧНО)

Функциональное сравнение

Компонент Training (features.py) Batch Inference (inference.py) Online (tasks/predict.py)
build_team_match_table ❌ не вызывается
add_rolling_features (4 группы) ❌ не вызывается
to_match_level ❌ не вызывается
compute_elo_ratings ❌ не вызывается
select_model_features ❌ не вызывается

Online inference (POST /predict/): принимает готовые features из request body — инженеринг фич НЕ переносится. Клиент сам должен предоставить уже вычисленные фичи. Это задокументировано в schema: "Feature dict matching the model's input schema."

Риск: Нет server-side feature recomputation для online endpoint — клиент несёт ответственность за корректность. Если клиент вычислит фичи неправильно, модель получит incorrect input без ошибки.

Порядок признаков

  • select_model_features() возвращает список из features_meta["name"].tolist() — порядок детерминирован порядком строк в meta файле
  • Все 3 точки (training, batch inference, online) используют одну и ту же select_model_features() → порядок консистентен ✅

4. Leakage Analysis

Rolling features

Ключевой механизм: .shift(1) перед .rolling().sum() — каждый матч видит только результаты предыдущих матчей команды. ✅

df_shifted = df_grouped[stats_cols].shift(1)
  • При min_periods=1 первый матч команды получает coverage_w{n} = 1/n, что является корректным подходом
  • df_team_match отсортирован по [teamId, startTimeUtc, id] — временной порядок гарантирован ✅

ELO features

Используются pre-match рейтинги (перед обновлением по результату). Документировано явно в docstring elo.py:

"The value attached to each match row is the PRE-match rating (before the result is known), which guarantees zero data leakage."

✅ Нет утечки.

Target в features

  • outcome_1x2 создаётся в preprocess.py как argmax([homeWin, draw, awayWin])
  • В build_team_match_table используется df["outcome_1x2"].values == 0/1/2 для вычисления win/draw/loss
  • Важно: rolling stats строятся только из исторических матчей через shift(1) — target текущего матча не попадает в его же фичи ✅

5. Parameter Usage

Параметр В params.yaml В features.py В inference.py В select.py
features.window_sizes входной параметр
features.stats_cols
features.elo.*
classification.window_sizes ✅ (для select) входной параметр
classification.side входной параметр

⚠️ P1: В inference.py используются оба features.window_sizes (для вычисления rolling) и classification.window_sizes (для select_model_features). Если classification.window_sizes ⊄ features.window_sizes, некоторые фичи будут NaN. В текущих params: features.window_sizes = [1,2,3,5,10], classification.window_sizes = [1,3] — subset ✅. Но нет runtime проверки этого инварианта.


6. Findings

ID Severity Описание
F-01 P1 Online POST /predict/ не вычисляет фичи server-side — полагается на клиента. Нет server-side feature contract enforcement для ad-hoc запросов
F-02 P1 Нет runtime проверки classification.window_sizes ⊆ features.window_sizes — молчаливые NaN при неправильной конфигурации
F-03 P2 H2H и rest_days объявлены в select_model_features() API как поддерживаемые параметры, но не реализованы — вводящий в заблуждение интерфейс
F-04 P2 coverage_w{n} колонки присутствуют в features.parquet, но не включаются в model input (нет в features_meta с нужными filter-условиями) — их ценность не используется
F-05 P3 Нет schema validation на входе compute_all_match_features() — отсутствие колонки (напр. outcome_1x2) приведёт к NaN/0 из-за _prepare_future_rows() без явной ошибки