150 lines
16 KiB
Markdown
150 lines
16 KiB
Markdown
# 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 Исходный промпт
|
||
```
|
||
|
||
## Ограничения и развитие
|
||
|
||
Решение рассчитано на небольшую выгрузку, целиком помещающуюся в память, и использует полную замену данных. Валюта одна; возвраты и бесплатные позиции не моделируются. Разрешение конфликтов зависит от порядка строк.
|
||
|
||
БД и файловые отчеты не объединены общей транзакцией: при сбое публикации отчетов после записи БД требуется повторный запуск. Конкурентная публикация отчетов и большие объемы данных не проверялись.
|
||
|
||
Следующие шаги для расширения решения — согласовать бизнес-правила версий и возвратов, перейти к потоковой обработке, добавить миграции схемы, мониторинг и согласованную публикацию БД с отчетами.
|