Data Audit Report — SoccerPredictAI¶
Date: 2026-04-24
Auditor: GitHub Copilot (Claude Sonnet 4.6)
Scope: Data layer — ingestion, storage, versioning, freshness, validation boundary
Method: прямой анализ кода (src/data/, src/app/tasks/export.py, src/pipelines/source.py, dvc.yaml, airflow/dags/)
1. Source & Ingestion¶
Откуда приходят данные¶
Источник: WhoScored.com (через Selenoid-браузерную сессию)
Технология scraping: Celery task livescores в очереди api (src/app/tasks/livescores.py)
Метод: HTTP-операторы Airflow → PATCH/GET FastAPI endpoints → Celery task dispatching
Ingestion flow¶
WhoScored.com
│ (Selenoid browser session)
▼
Celery worker-api (очередь "api")
│ scraping results (JSON)
▼
PostgreSQL (таблицы: match, match_raw)
│
Airflow DAG: soccer_etl_export_matches_to_source (schedule=None, manual)
│ GET /sources/export/{table_name}
▼
MinIO (bucket: MINIO_BUCKET_DATA_RAW)
│ match.parquet + match_raw.parquet
▼
DVC stage: load_data_from_sources
│ src/data/storage.py → export_data_raw()
▼
data/raw/match.parquet + data/raw/match_raw.parquet
Ключевые выводы¶
- Scraping запускается через 4 отдельных DAG (
etl_livescores_01–04), все с@hourly, что позволяет охватить разные временные окна - Экспорт PostgreSQL → MinIO (
etl_export_01.py) запускается вручную (schedule=None) — нет автоматизированного trigg-а для обновления raw parquet
2. Storage Contracts¶
Canonical источники¶
| Артефакт | Путь | Тип | Описание |
|---|---|---|---|
match_raw.parquet |
data/raw/match_raw.parquet |
MinIO → DVC | Исходные данные без обработки |
match.parquet |
data/raw/match.parquet |
MinIO → DVC | То же содержимое, используется downstream |
Различия match.parquet и match_raw.parquet¶
⚠️ Проблема: src/data/source.py вызывает export_data_raw() для обоих файлов одинаково — оба скачиваются из MINIO_BUCKET_DATA_RAW по имени файла как ключу. Нет явного различия в семантике на уровне кода — оба ключа выводятся из local_path.name. Фактически оба файла существуют в MinIO с разными ключами (match.parquet и match_raw.parquet), но их смысловое разграничение не задокументировано в коде.
Downstream usage:
- match_raw.parquet → validate_raw → export_metadata → preprocessing
- match.parquet → объявлен как outs, но в downstream stages используется только match_raw.parquet
⚠️ P1 риск: match.parquet объявлен как DVC out, но не используется ни одной downstream-стадией как dep — потенциально мёртвый артефакт.
3. Data Versioning¶
Метаданные snapshot¶
В export_data_raw() (src/data/storage.py) для каждого скаченного файла создаётся .minio.json sidecar:
| Поле | Источник | Статус |
|---|---|---|
etag |
MinIO ETag | ✅ |
size |
ContentLength | ✅ |
last_modified |
MinIO LastModified (ISO) | ✅ |
bucket |
имя bucket | ✅ |
key |
имя объекта | ✅ |
ingested_at |
UTC wall-clock при инgesте | ✅ |
| content hash | — | ❌ отсутствует (используется ETag, который может быть составным для multipart) |
DVC tracking: Все .minio.json sidecar-файлы включены в DVC outs (persist: true), что обеспечивает полную цепочку происхождения.
4. Freshness & Idempotency¶
Smart-skip логика¶
changed = (
(not local_path.exists())
or (local_etag != remote_etag)
or (local_size != remote_size)
)
Оценка: - ✅ Есть smart-skip по ETag + size — повторный запуск без изменений на MinIO не перезаписывает файл - ✅ Idempotency сохранена — повторный запуск с теми же данными не создаёт проблем - ✅ ETag + size сравнение надёжно для однофайловых объектов MinIO
Риски freshness¶
| Риск | Описание | Severity |
|---|---|---|
| Ручной export trigger | etl_export_01 запускается вручную → raw parquet может устареть без уведомления |
P1 |
| Scraping → PostgreSQL задержка | Между @hourly scraping и экспортом нет явной проверки completeness данных |
P2 |
| Частичное обновление | Два файла (match + match_raw) скачиваются последовательно — при сбое между ними состояние может быть рассогласованным | P2 |
5. Data Validation Boundary¶
Расположение validation¶
DVC Pipeline validation stages:
match_raw.parquet → validate_raw (GE: src/data_quality/raw.py)
finished.parquet → validate_finished (GE: src/data_quality/finished.py)
future.parquet → validate_future (GE: src/data_quality/future.py)
features.parquet → validate_features (GE: src/data_quality/features.py)
Оценка:
- ✅ Validation перед каждой ключевой стадией — Good
- ✅ GE-результаты сохраняются как JSON в data/evaluation/
- ❌ Нет validation на MinIO перед ingestion — данные скачиваются и сохраняются без schema-check
- ❌ DVC stage validate_interim упомянут в contract tests (tests/contract/test_pipeline_contracts.py) как ожидаемый, но отсутствует в dvc.yaml — расхождение contracts ↔ pipeline
6. Risks¶
| ID | Severity | Описание | Компонент |
|---|---|---|---|
| D-01 | P0 | validate_interim stage ожидается в contract tests, но отсутствует в dvc.yaml — тест test_expected_stage_exists упадёт |
tests/contract/, dvc.yaml |
| D-02 | P1 | match.parquet объявлен как DVC out, но не используется как dep ни в одной стадии |
dvc.yaml |
| D-03 | P1 | etl_export_01 (PostgreSQL → MinIO) запускается только вручную — нет автоматизации, нет алертинга об устаревших данных |
airflow/dags/etl_export_01.py |
| D-04 | P1 | ETag MinIO для multipart-uploads не является content hash — silent false-positive skip теоретически возможен при collisions | src/data/storage.py |
| D-05 | P2 | Нет валидации schema при скачивании из MinIO — некорректный parquet попадёт в pipeline только на стадии validate_raw |
src/data/source.py |
| D-06 | P2 | При сбое между скачиванием match.parquet и match_raw.parquet состояние может быть неконсистентным (нет транзакционности) |
src/data/source.py |
| D-07 | P2 | Нет content hash (MD5/SHA) файла — ETag + size не гарантируют полной bit-exact проверки | src/data/storage.py |
7. Summary¶
| Аспект | Статус |
|---|---|
| Ingestion flow задокументирован | ✅ |
| Smart-skip (ETag + size) | ✅ |
| Sidecar metadata (.minio.json) | ✅ |
| Data validation (GE) в pipeline | ✅ |
| Export automation | ❌ Ручной |
| validate_interim в pipeline | ❌ Отсутствует (contract тест сломан) |
| match.parquet downstream usage | ❌ Мёртвый артефакт |