Skip to content

Train ↔ Serve Consistency Audit Report — SoccerPredictAI

Date: 2026-04-24 Auditor: GitHub Copilot (Claude Sonnet 4.6) Scope: Расхождения feature pipeline между training, batch inference и online serving Method: анализ src/features/, src/pipelines/features.py, src/pipelines/inference.py, src/app/services/predict.py, src/app/tasks/predict.py, src/app/routers/predict.py


1. Feature Set Consistency

1.1 Инвентаризация feature set по точкам

Точка Источник Метод получения фич
Training (final_train) features.parquet + features_meta.parquet через select_model_features() Computed offline: feature_engineering DVC stage
Batch inference Computed in-process: compute_all_match_features()select_model_features() Re-computed в batch_inference DVC stage
Online serving (POST /predict/) Request body features: dict Клиент предоставляет готовые фичи

Feature columns

Training (final_train):
  select_model_features(features_meta, side='diff', window_sizes=[1,3], include_elo=True)
  → diff_*_mean_w1, diff_*_mean_w3 (all/season/tournament/ha × 5 stats × 2 windows = 40)
  + diff_elo_pre (ELO)
  + coverage_w1, coverage_w3 per group × 4 = 8 coverage
  + sex (categorical)

Batch inference (compute_all_match_features):
  Те же: select_model_features(features_meta, side='diff', window_sizes=[1,3], ...)
  → идентичный набор

Online serving (POST /predict/):
  КЛИЕНТ предоставляет dict — нет server-side enforcement

Совпадение training ↔ batch_inference

Полностью совпадает. Обе точки используют одни и те же: - функции: build_team_match_table, add_rolling_features, to_match_level, compute_elo_ratings - параметры: из одного params.yaml - feature selector: select_model_features() с одинаковыми аргументами

Training ↔ online serving

⚠️ НЕТ SERVER-SIDE FEATURE COMPUTATION. Online POST /predict/ принимает уже готовые фичи от клиента. Это архитектурно задуманная конфигурация (задокументировано в PredictRequest docstring), но создаёт риск:

  • Клиент может прислать неправильные фичи (неправильные window sizes, неправильные stats, устаревшие ELO)
  • Нет schema validation кроме типов (dict of float/int/None)
  • Нет feature completeness check

2. Preprocessing Logic

2.1 Feature engineering: Training vs Batch inference

Компонент Training Batch inference Консистентен
build_team_match_table ✅ одна функция
add_rolling_features (4 группы) ✅ одна функция
to_match_level (home/away → diff) ✅ одна функция
compute_elo_ratings ✅ одна функция
ELO params (k_factor, initial_rating, home_advantage) params.yaml params.yaml
window_sizes classification.window_sizes classification.window_sizes
stats_cols features.stats_cols features.stats_cols

2.2 Feature engineering: Training vs Batch inference (критическое различие)

⚠️ P1 — Исторический контекст в batch inference:

В feature_engineering (training): фичи для матча m используют только finished матчи до m.

В batch_inference (compute_all_match_features): фичи для future матча m используют все finished + все future матчи в объединённой таблице, отсортированной хронологически. Future матчи имеют zeroed outcome columns (_prepare_future_rows), но их startTimeUtc позиционирует их ПОСЛЕ finished матчей. shift(1) гарантирует что будущий матч не видит свой собственный результат (которого нет).

✅ Нет leakage. Но есть subtle asymmetry:

  • При training: rolling stats для финального матча в training set вычисляются на основании предшествующих finished матчей
  • При batch inference: rolling stats для future матча используют те же финальные finished матчи плюс любые more recent finished матчи, которые были добавлены после feature_engineering

batch_inference фичи fresher чем training фичи (используют более актуальный history). Это корректное поведение, но означает небольшой train/serve skew в coverage.


3. Encoding & Scaling

Компонент Training Serving
StandardScaler (num) ✅ в sklearn Pipeline ✅ часть serialized model
SimpleImputer (median for num) ✅ в sklearn Pipeline ✅ часть serialized model
OneHotEncoder (cat cols: sex) ✅ в sklearn Pipeline ✅ часть serialized model
SimpleImputer (most_frequent for cat) ✅ в sklearn Pipeline ✅ часть serialized model

Все preprocessing включён в serialized sklearn Pipeline и загружается через MLflow pyfunc. Нет риска training/serving skew для encoding/scaling.


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

# select_model_features() возвращает features_meta["name"].tolist()
# Порядок определён порядком строк в features_meta.parquet
Аспект Статус
Порядок фиксирован через features_meta
Один и тот же порядок в training и batch inference ✅ (та же функция, тот же файл)
XGBoost не зависит от порядка ✅ (tree-based)
Online serving: порядок диктует клиент ⚠️ Нет enforcement

5. Model Loading в Serving

# PredictionService.predict():
model = mlflow.pyfunc.load_model(f"models:/soccer_clf@champion")
result = model.predict(pd.DataFrame([features]))
  • MLflow pyfunc оборачивает sklearn Pipeline → preprocessing применяется автоматически ✅
  • Нет ручного preprocessing при serving ✅
  • Модель загружается по alias @champion — всегда актуальная registered версия ✅

6. Staleness Risk

Сценарий Риск
batch_inference запущен, но match_features.parquet не загружен в MinIO FeatureLookupService подаст stale данные с предупреждением
Обновлена модель в registry, но Celery worker не перезапущен Worker продолжает использовать старую модель (in-process singleton)
Future матч стал finished (прошёл), но batch_inference не перезапущен Serving подаёт предсказания для уже сыгранных матчей

7. Findings

ID Severity Описание
TS-01 P1 POST /predict/ не вычисляет фичи server-side — клиент несёт ответственность за корректность feature set. Нет validation что переданные фичи соответствуют model schema
TS-02 P1 Нет механизма автоматического reload модели в Celery worker при смене champion — нужен ручной перезапуск
TS-03 P2 Batch inference fresher чем training features (использует more recent history) — небольшой systematic skew в coverage features
TS-04 P2 FeatureLookupService.get_features() удаляет NaN значения из dict — модель получит неполный feature dict, SimpleImputer закроет gaps, но это неявное поведение
TS-05 P2 Нет проверки что match_features.parquet актуален (не старее N часов) при serving — stale predictions без алертинга