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 + ручные записи для ELOparse_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() — каждый матч видит только результаты предыдущих матчей команды. ✅
- При
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() без явной ошибки |