Skip to content

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

Ключевые выводы

  1. Scraping запускается через 4 отдельных DAG (etl_livescores_01–04), все с @hourly, что позволяет охватить разные временные окна
  2. Экспорт 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.parquetvalidate_rawexport_metadatapreprocessing - 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 ❌ Мёртвый артефакт