Files
data_engineer_test/README.md
T
2026-11-13 22:02:30 +03:00

150 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ETL-пайплайн для анализа выручки
Тестовое задание на позицию Data-инженера. Решение загружает CSV с заказами, очищает данные, рассчитывает выручку по дням и категориям и сохраняет результат в SQLite. Три SQL-запроса формируют рейтинг категорий, ежедневную динамику и сигналы аномальной выручки.
**Стек:** Python 3.12, SQLite, стандартная библиотека Python. Сторонние зависимости не требуются. Данные синтетические; фиксированный seed обеспечивает воспроизводимость.
## Результат
В проект включены готовая [база данных](output/sales.sqlite), [отчет качества](reports/etl/quality.json), [реестр отказов](reports/etl/rejected.csv) и результаты SQL-запросов. Их можно изучить без повторного запуска.
| Показатель | Значение |
|---|---:|
| Входных строк | 301 |
| Принято | 290 |
| Отклонено | 11 |
| Выручка | 991 193,95 ₽ |
| Категорий | 3 |
| Дней в аналитическом календаре | 30 |
| Сигналов аномальной выручки | 1 |
Показатели относятся к демонстрационному набору, а не к реальному бизнесу.
## Быстрый запуск
Требуется Python 3.12 или новее; проверенная версия — **3.12.14**. Распакуйте проект и выполните команды из его корневого каталога:
```bash
python scripts/generate_data.py --output run/raw/orders.csv --seed 42
python -m src.pipeline --input run/raw/orders.csv --db run/output/sales.sqlite --reports run/reports/etl
python sql/run_queries.py --db run/output/sales.sqlite --output run/reports/sql
```
Результаты появятся в каталоге `run/`: исходный CSV, база `output/sales.sqlite`, отчеты очистки в `reports/etl/` и аналитические CSV в `reports/sql/`. Повторный запуск заменяет результаты по указанным путям. Готовые артефакты из корня проекта сохраняются.
Для обработки предоставленного примера вместо генерации укажите `--input data/raw/orders.csv` во второй команде. Если интерпретатор доступен как `python3`, замените имя команды. Особенности использованного окружения вынесены в [протокол запуска](docs/verification/RUN_LOG.md).
Запуск тестов:
```bash
python -m unittest discover -s tests -v
```
## Как устроен пайплайн
```text
CSV → проверка структуры → очистка и дедупликация
→ факты и дневные агрегаты в SQLite → аналитические SQL → CSV
```
1. **Загрузка.** Чтение UTF-8 CSV и проверка заголовка. Каждая запись сохраняет связь с физическим номером строки исходного файла.
2. **Очистка.** Проверка обязательных полей, дат, количества и цены; нормализация строк; обработка повторов и конфликтов ключа.
3. **Расчет.** Выручка строки равна количеству, умноженному на цену. Цена разбирается через `Decimal`; дальнейшие расчеты и хранение используют целые копейки.
4. **Запись.** Факты и агрегаты по дню и категории заменяются в одной транзакции SQLite. После успешной записи публикуются отчеты качества.
5. **Аналитика.** Отдельный скрипт выполняет три SQL-запроса, открывая базу только для чтения, и сохраняет результаты в CSV.
Python и SQLite выбраны для локального воспроизводимого запуска без внешней инфраструктуры. Разделение фактов и агрегатов позволяет сверить итоговые суммы с очищенными строками заказов.
## Данные и правила очистки
Единица учета — **строка заказа**, уникальный ключ — `(order_id, line_id)`. Все поля CSV обязательны.
| Поле | Назначение и допустимые значения |
|---|---|
| `order_id` | Непустой идентификатор заказа |
| `line_id` | Непустой идентификатор строки внутри заказа |
| `order_date` | Календарный день: `YYYY-MM-DD`, `DD.MM.YYYY` или `YYYY/MM/DD` |
| `category` | Непустая категория; пробелы и регистр нормализуются |
| `quantity` | Положительное целое количество |
| `unit_price` | Положительная цена в RUB; точка как разделитель, до двух дробных знаков |
Крайние пробелы удаляются во всех полях. В категории последовательности пробелов заменяются одним, регистр приводится через `casefold`. Обязательные значения не восстанавливаются предположениями. Неоднозначные даты, например `01/02/2026`, отклоняются. Время и часовой пояс не моделируются.
Нули, отрицательные значения, `NaN`, `Infinity` и значения за пределами допустимой арифметики `int64` не принимаются. Переполнение общей выручки останавливает загрузку до изменения БД. Нарушенная структура CSV и многострочные поля также завершают запуск до записи.
**Дубли и конфликты.** Валидация предшествует дедупликации. Для каждого ключа сохраняется первая валидная запись. Последующая запись с теми же нормализованными значениями считается дублем; с отличающимися — конфликтом ключа. Обе отклоняются. Это явное допущение: источник не содержит версии записи или времени изменения для выбора более актуальной строки.
### Схема SQLite
| Таблица | Первичный ключ | Содержание |
|---|---|---|
| `facts` | `order_id`, `line_id` | Дата, категория, количество, цена и выручка в копейках, номер исходной строки |
| `daily_category` | `order_date`, `category` | Выручка в копейках за день по категории |
Подробные интерфейсы и правила описаны в [контракте данных](coordination/CONTRACT.md).
## Контроль качества
Для каждой отклоненной строки реестр отказов содержит исходные значения, номер строки и одну основную причину. Если дефектов несколько, применяется приоритет из таблицы ниже. Строка учитывается в статистике отказов ровно один раз.
| Причина | Код в отчете | Строк |
|---|---|---:|
| Не заполнено обязательное поле | `missing_required` | 2 |
| Недопустимая или неоднозначная дата | `invalid_date` | 2 |
| Некорректное количество | `invalid_quantity` | 2 |
| Некорректная цена | `invalid_price` | 3 |
| Повтор нормализованной записи | `duplicate` | 1 |
| Разные записи с одинаковым ключом | `conflicting_key` | 1 |
| **Всего** | | **11** |
Баланс сходится: **301 = 290 + 11**. Отдельно учтены 290 принятых строк с исправленным представлением полей. Генератор намеренно добавляет пробелы в каждую категорию, поэтому нормализация затрагивает все принятые записи. Этот счетчик не увеличивает число отказов.
## Аналитические запросы
| Запрос | Правило | Результат |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| [Топ категорий](sql/top_categories.sql) | Сумма выручки по категории, сортировка по убыванию | [3 категории](reports/sql/top_categories.csv) |
| [Ежедневная динамика](sql/daily_dynamics.sql) | Календарь от первой до последней даты, дни без продаж заполнены нулем, изменение к предыдущему дню | [30 дней](reports/sql/daily_dynamics.csv) |
| [Аномалии](sql/anomalies.sql) | Выручка дня превышает среднюю за предыдущие 7 календарных дней более чем втрое | [1 сигнал](reports/sql/anomalies.csv) |
Лидер по выручке — **книги: 399 048,85 ₽**; далее электроника — 330 080,50 ₽ и дом — 262 064,60 ₽. В динамике 13 января отражено с нулевой выручкой; изменение для первого дня не рассчитывается.
Для поиска аномалий нужны семь дней истории и ненулевая базовая сумма; отсутствующие продажи считаются нулями. Выявлен сигнал **23 января 2026 года**, выручка — **451 449,20 ₽**. Всплеск намеренно заложен в синтетический генератор для демонстрации запроса. Аналитический сигнал сам по себе не означает ошибку исходных данных.
## Проверка надежности
Шесть автоматических тестов прошли успешно. Они проверяют ручную фикстуру с ожидаемой выручкой **2080 копеек**, правила очистки и ключей, причины отказов, числовые ограничения, повторную загрузку, пустой и полностью отклоненный набор, а также откат при искусственно вызванном сбое SQL-записи.
Идемпотентность проверяется сравнением отсортированных бизнес-данных после двух запусков, а не побайтовым сравнением SQLite-файлов. Обе таблицы обновляются в одной транзакции: ошибка записи сохраняет предыдущий результат. Пустой или полностью отклоненный вход намеренно заменяет прежний набор пустым.
Дополнительно проверены целостность SQLite, равенство агрегатов суммам фактов, воспроизводимость исходного CSV и отчетов, а также выполнение команд из распакованного комплекта. Доказательства: [приемка](coordination/ACCEPTANCE.md) и [повторная проверка](docs/FINAL_RECHECK.md).
## Работа с ИИ
В проекте сохранен [исходный промпт выполнения задания](DATA_ENGINEER_PROMPT.md): требования, распределение ролей, границы работы и критерии приемки. [Рабочий журнал ИИ](coordination/AI_LOG.md) фиксирует существенные задания, решения и обнаруженные ошибки; он не является полной стенограммой.
Реализация и независимые тесты выполнялись параллельно, затем те же исполнители подготовили SQL и документацию. Первичная реализация с приемкой заняла **8 минут 33 секунды** календарного времени. Последующая повторная проверка и редактура документации в этот интервал не входят. Это время работы команды ИИ, а не оценка трудозатрат человека.
## Состав проекта
```text
src/ ETL и запись в SQLite
scripts/generate_data.py Генератор синтетического CSV
tests/ Независимые автоматические тесты
data/raw/orders.csv Исходный пример
output/sales.sqlite Готовая база данных
sql/ Три запроса и скрипт их выполнения
reports/ Качество, отказы и результаты SQL
coordination/ Контракт, решения, журнал ИИ и приемка
docs/ Протоколы проверок и тезисы для собеседования
DATA_ENGINEER_PROMPT.md Исходный промпт
```
## Ограничения и развитие
Решение рассчитано на небольшую выгрузку, целиком помещающуюся в память, и использует полную замену данных. Валюта одна; возвраты и бесплатные позиции не моделируются. Разрешение конфликтов зависит от порядка строк.
БД и файловые отчеты не объединены общей транзакцией: при сбое публикации отчетов после записи БД требуется повторный запуск. Конкурентная публикация отчетов и большие объемы данных не проверялись.
Следующие шаги для расширения решения — согласовать бизнес-правила версий и возвратов, перейти к потоковой обработке, добавить миграции схемы, мониторинг и согласованную публикацию БД с отчетами.