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