Training & Evaluation Audit Report — SoccerPredictAI¶
Date: 2026-04-24
Auditor: GitHub Copilot (Claude Sonnet 4.6)
Scope: Training, evaluation, CV, model selection, feature consistency, calibration
Method: анализ src/models/, src/pipelines/classification.py, src/pipelines/tune.py, src/pipelines/final_train.py, params.yaml, src/data/splitting.py
1. Data Split & CV¶
Train/Test split¶
| Параметр | Значение | Источник |
|---|---|---|
| Метод | time-based | src/data/splitting.py:split_time_based_on() |
test_start |
2024-01-01 | params.yaml: temporal.test_start |
| Holdout | startTimeUtc >= 2024-01-01 |
temporal boundary |
| Train/Val | startTimeUtc < 2024-01-01 |
всё до этой даты |
Утечка времени: ✅ Нет. Отсечка по времени строгая, индекс по startTimeUtc.
Walk-forward CV folds¶
| Параметр | Значение | Источник |
|---|---|---|
| Метод | year-based walk-forward | src/data/splitting.py:make_year_folds() |
| Folds | 2022, 2023 (2 fold) | params.yaml: temporal.folds_start_year=2022, folds_end_year=2024 |
| Логика | для fold year y: train = всё до y-01-01, valid = [y-01-01, y+1-01-01) |
Fold 1: train = everything before 2022-01-01, valid = 2022
Fold 2: train = everything before 2023-01-01, valid = 2023
Holdout: 2024+
✅ Walk-forward CV — правильный подход для временных рядов. Нет data leakage между folds.
2. Training Setup¶
Stage: classification_models¶
| Аспект | Значение |
|---|---|
| Models | baseline (DummyClassifier), logreg, sgd_logloss, HGBT, xgb |
| Features | side=diff, window_sizes=[1,3], ELO + sex categorical |
fracs_for_train |
[0.001, 0.002] ← smoke-режим |
| Target | outcome_1x2 (0=home, 1=draw, 2=away) |
| CV | walk-forward 2 folds |
⚠️ P0: fracs_for_train: [0.001, 0.002] — тренирует на 0.1% и 0.2% от train-сета. Это smoke-конфигурация. Результаты не репрезентативны для production. Лучшая модель выбирается из toy-runs.
Stage: tune_xgb¶
| Аспект | Значение |
|---|---|
n_trials |
2 ← smoke |
frac |
0.1 |
| Метрика | mean CV log-loss |
| Backend | Optuna, SQLite in-memory |
⚠️ P0: n_trials=2 — Optuna с 2 trials не может найти значимо лучшие гиперпараметры. Tuning результаты non-meaningful.
Stage: final_train¶
| Аспект | Значение |
|---|---|
| Model | best_model_name из classification_models (run_id.json) |
| Params | best_params из xgb_best_params.json |
| Calibration | isotonic, calib_frac=0.15, min_calib_samples=100 |
| Calibration split | temporальный (не random) — earliest (1-calib_frac) для training, latest calib_frac для calibration |
| Holdout evaluation | ✅ одна оценка в конце — правильно |
✅ Calibration design корректен: temporal split для calibration, no random leakage.
3. Metrics¶
Метрики, логируемые в MLflow¶
| Стадия | CV метрики | Holdout метрики |
|---|---|---|
| classification_models | logloss per fold, mean CV logloss | holdout logloss |
| tune_xgb | mean CV logloss per trial | — |
| final_train | — | holdout logloss, ECE (raw), ECE (calibrated), accuracy, confusion matrix |
Калибрация:
- Raw ECE логируется для сравнения с calibrated
- ECE вычисляется через compute_ece() в src/models/metrics.py
Segment metrics: compute_segment_metrics() логируется в final_train — breakdown по группам
4. Model Selection¶
Механизм выбора лучшей модели¶
В make_classification_runs():
_run_candidates: list[tuple[str, float]] = []
# ... для каждой модели / frac:
_run_candidates.append((run_id, holdout_logloss))
# ...
best_run_id = min(_run_candidates, key=lambda x: x[1])[0]
✅ Выбор по holdout log-loss — korektно.
⚠️ P1: При fracs_for_train=[0.001, 0.002] и нескольких моделях selection происходит по очень малой выборке — результаты нестабильны. Лучшая модель на 0.1% данных может быть совсем другой моделью при 100%.
Использование holdout для selection¶
classification_models использует holdout logloss для выбора best run_id. Это нарушает принцип holdout-only-for-final-evaluation при fracs_for_train iterations — каждая итерация оценивается на holdout.
⚠️ P2: Holdout виден для model selection в classification_models. Это не катастрофическая утечка (выбор между моделями, а не настройка параметров по holdout), но снижает объективность финальной оценки. final_train потом снова оценивает на том же holdout — значения не независимы.
5. Inconsistencies¶
Feature set между стадиями¶
| Стадия | side | window_sizes | source |
|---|---|---|---|
| classification_models | diff | [1,3] | params.yaml |
| tune_xgb | diff | [1,3] | params.yaml |
| final_train | diff | [1,3] | из run_id.json (best_model_name) → params |
| batch_inference | diff | [1,3] | params.yaml |
✅ Все стадии используют одинаковый feature set через select_model_features().
window_sizes расхождение (потенциальное)¶
features.window_sizes = [1,2,3,5,10](feature engineering)classification.window_sizes = [1,3](model input)
Нет runtime guard. Если изменить classification.window_sizes добавив [7], а features.window_sizes не включает 7 — silent NaN.
6. Findings (P0/P1/P2)¶
| ID | Severity | Описание |
|---|---|---|
| TR-01 | P0 | fracs_for_train: [0.001, 0.002] — smoke-конфиг, тренируется на 0.1-0.2% данных. Все метрики и model selection non-representative |
| TR-02 | P0 | tuning.n_trials=2 — smoke-конфиг, Optuna tuning бессмысленный при 2 trials |
| TR-03 | P1 | Holdout используется для model selection в classification_models — не только для финальной отчётности. Holdout не полностью blind |
| TR-04 | P1 | ablation_study stage не включён в DAG зависимостей tune_xgb и final_train — ablation результаты не влияют на model selection |
| TR-05 | P2 | Нет seed у HGBT/XGB в classification pipeline для CV runs — воспроизводимость на уровне прогона |
| TR-06 | P2 | best_model_name в run_id.json содержит победителя classification_models, но final_train применяет tuned params из xgb_best_params.json к этому model_name — если winner != xgb, params применяются к неправильной модели |