From 60079dd1c86ca6009120b8469278091233e9fe37 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 7 Mar 2026 11:43:38 +0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D1=8F=203=20=D0=BF=D0=BE=20raw-ingest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужен согласованный draft модуля про raw-данные, первое чтение в Spark и подготовку data bundle. - Что: - добавлен план `module-03-raw-ingest-and-first-read.md` с deliverables, структурой ноутбука и acceptance criteria. - зафиксированы требования к onboarding-документации, raw-ingest в MinIO и валидации data bundle. - Проверка: - план вручную сверен с `docs/course_program.md`, `docs/course_prd.md` и `docs/stack_reference.md`. - runtime-проверка стенда и ноутбука не выполнялась. --- plans/module-03-raw-ingest-and-first-read.md | 173 +++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 plans/module-03-raw-ingest-and-first-read.md diff --git a/plans/module-03-raw-ingest-and-first-read.md b/plans/module-03-raw-ingest-and-first-read.md new file mode 100644 index 0000000..ad317f2 --- /dev/null +++ b/plans/module-03-raw-ingest-and-first-read.md @@ -0,0 +1,173 @@ +# Модуль 3. Raw-данные и первое чтение в Spark + +**Статус:** `Draft` +**Последнее обновление:** `2026-03-07` + +## Цель + +Научить студента полному циклу работы с raw-данными в Lakehouse: доставка заранее скачанного PARQUET-датасета в raw-зону MinIO, чтение raw-данных через Spark, проверка схемы, типов и базового качества. Студент должен понять, зачем нужен raw-слой и почему raw-данные не следует «чинить на месте». + +## Результат для студента + +После прохождения модуля студент: + +- понимает, как локальный data bundle попадает в raw-зону MinIO; +- умеет загружать PARQUET-файлы в MinIO программно через `boto3`; +- умеет читать raw-данные из MinIO через Spark (`spark.read.parquet` по `s3a://` пути); +- умеет проверять схему DataFrame (`.printSchema()`, `.dtypes`), считать базовые метрики (`.describe()`, `.count()`, null-доли); +- может найти простые аномалии или проблемные значения в реальном датасете; +- понимает, почему raw-слой нужен как воспроизводимая неизменяемая точка входа; +- понимает, почему датасет не следует скачивать из интернета прямо в ноутбуке. + +## Deliverables + +- `notebooks/03_raw_ingest_and_first_read.ipynb` — основной ноутбук Модуля 3; +- изменение `docker-compose.yml` — volume mount `./data:/opt/data:ro` в сервис `jupyter`; +- изменение `.gitignore` — добавление `/data/`; +- изменение `START_HERE.md` — секция «Подготовка учебного датасета (перед Модулем 3)» + обновление маршрута прохождения и секции «Как работать в Jupyter» (добавить упоминание `/opt/data`); +- изменение `docs/stack_reference.md` — обновление секции JupyterLab (добавить `./data` mount); +- обновление индекса планов в `plans/README.md`. + +## Зависимости от инфраструктуры + +### Доставка данных в контейнер + +Добавить volume mount `./data:/opt/data:ro` в секцию `jupyter.volumes` файла `docker-compose.yml` (аналогично `./src:/opt/src:ro`). + +Студент заранее скачивает PARQUET на хост по инструкции из `START_HERE.md`. В ноутбуке студент загружает файлы из `/opt/data/` в MinIO через `boto3`. Это делает процесс наглядным и воспроизводимым. + +```yaml +# docker-compose.yml, секция jupyter.volumes +volumes: + - ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro + - ./notebooks:/opt/work + - ./src:/opt/src:ro + - ./data:/opt/data:ro # учебный data bundle (NYC Taxi) +``` + +### Инструкция скачивания датасета + +Добавить в `START_HERE.md` секцию после «Restart и reset», перед «Что делать дальше»: + +- `mkdir -p data/nyc_taxi` +- 3 команды `curl` для Yellow Tripdata 2024-01..03 с `https://d37ci6vzurychx.cloudfront.net/trip-data/` +- 1 команда `curl` для `taxi_zone_lookup.csv` с `https://d37ci6vzurychx.cloudfront.net/misc/` +- Проверочный `ls -lh data/nyc_taxi/` +- Инструкция фиксирует конкретные файлы (2024-01, 2024-02, 2024-03) как каноническое учебное подмножество; расширенный режим — до 12 месяцев 2024 года +- Пометка: после изменения `docker-compose.yml` (добавления volume) нужен `docker compose up -d` для пересоздания контейнера `jupyter`; после этого новые файлы в `./data` видны сразу без перезапуска + +### Структура raw-зоны в MinIO + +Путь: `s3a://lakehouse/raw/nyc_taxi/`. Отдельный prefix `raw/` не смешивается с `warehouse/` (Iceberg-таблицы). Имена файлов сохраняются оригинальными, чтобы подчеркнуть принцип «raw не изменяется». + +### .gitignore + +Добавить в конец: + +``` +# Dataset (downloaded by student, not stored in repo) +/data/ +``` + +## План работ + +1. Добавить `/data/` в `.gitignore`. +2. Добавить volume mount `./data:/opt/data:ro` в `docker-compose.yml`, секция `jupyter.volumes`. +3. Добавить в `START_HERE.md` секцию «Подготовка учебного датасета (перед Модулем 3)» с воспроизводимыми командами скачивания NYC Taxi Yellow Tripdata PARQUET (3 месяца + taxi zone lookup). Обновить маршрут прохождения (добавить шаг подготовки данных). Обновить секцию «Как работать в Jupyter» — добавить упоминание `./data` mount как `/opt/data`. Обновить секцию «Что делать дальше», чтобы маршрут включал Модуль 3. +4. Обновить `docs/stack_reference.md` — в секции JupyterLab добавить строку `./data` смонтирован read-only как `/opt/data`. +5. Создать `notebooks/03_raw_ingest_and_first_read.ipynb` по ячеечной структуре, описанной ниже. +6. Обновить `plans/README.md` — добавить строку Модуля 3 в таблицу оглавления. +7. Валидация: убедиться, что `docker compose config` проходит без ошибок; выполнить ноутбук сверху вниз на поднятом стенде с data bundle. + +## Структура ноутбука + +### Секция 0: Введение +- **[md]** Заголовок, цели модуля, prerequisite (data bundle из `START_HERE.md`). Таблица-сравнение: «Классический DWH: данные уже внутри СУБД» vs «Lakehouse: данные нужно явно доставить в storage». + +### Секция 1: Проверка data bundle +- **[md]** Зачем проверяем; почему не скачиваем из интернета в ноутбуке (воспроизводимость, сетевые ограничения, разделение ответственности onboarding vs практика). +- **[code]** `os.listdir("/opt/data/nyc_taxi")`, вывод файлов с размерами, assert на наличие PARQUET-файлов и `taxi_zone_lookup.csv` — оба нужны для полного прохождения модуля (включая самостоятельное задание в Секции 6). Понятное сообщение с отсылкой к `START_HERE.md` при отсутствии любого из компонентов. + +### Секция 2: Загрузка в raw-зону MinIO +- **[md]** Что такое raw-зона, параллель с stg-слоем в Greenplum, конвенция путей, почему raw не чинится «на месте». +- **[code]** Демонстрация: перед загрузкой проверяем через `s3.head_object()`, существует ли уже объект с таким ключом. Три ветки: (1) объект не существует — загружаем через `boto3.upload_file()`; (2) объект существует и размер совпадает с локальным файлом — пропускаем с пометкой «already exists, skipping»; (3) объект существует, но размер отличается — **fail fast** с понятной ошибкой (raise), без перезаписи. Это обеспечивает идемпотентность и подкрепляет принцип «raw не перезаписывается». В markdown-пояснении перед ячейкой явно объяснить: повторный запуск безопасен, уже загруженные файлы не перетираются, а конфликт размеров сигнализирует о проблеме с данными. +- **[code]** Верификация: `s3.list_objects_v2()` — листинг raw-зоны. +- **[md]** Данные теперь в storage. Spark может читать их напрямую по `s3a://` пути; для Trino потребуется зарегистрированная таблица (это будет в Модулях 4 и 6). + +### Секция 3: Первое чтение в Spark +- **[md]** Разница `spark.read.parquet("s3a://...")` (raw-файлы) vs `spark.table(...)` (управляемая Iceberg-таблица). Мы пока на этапе raw, Iceberg-таблица появится в Модуле 4. +- **[code]** Создание SparkSession (паттерн из Module 2 — без `.master()`, `setLogLevel("ERROR")`). +- **[code]** Чтение одного файла: `spark.read.parquet("s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_2024-01.parquet")`. +- **[code]** Чтение всех файлов wildcard: `yellow_tripdata_*.parquet` (без привязки к конкретному году в wildcard, чтобы код работал с любыми скачанными файлами). + +### Секция 4: Проверка схемы +- **[md]** Зачем проверять схему: в классическом DWH схема задана DDL, в raw — вычитывается из файлов. Что может пойти не так: разные типы в разных файлах, неожиданные nullable-поля. +- **[code]** `.printSchema()`. +- **[code]** Колонки и типы в удобном формате (`.dtypes`). +- **[code]** Null-анализ ключевых колонок (`VendorID`, `tpep_pickup_datetime`, `passenger_count`, `trip_distance`, `total_amount`) — количество и доля null в каждой. +- **[md]** Обсуждение: null в raw — норма, не исправляем на месте. Очистка — задача bronze/silver слоев (Модули 4-5). + +### Секция 5: Базовые метрики и аномалии +- **[md]** Цель: быстро понять диапазоны значений, выявить очевидные проблемы. +- **[code]** `.describe()` на числовых колонках. +- **[code]** Поиск аномалий: отрицательные `fare_amount`, нулевая дистанция с ненулевой суммой, `total_amount > $1000`, pickup за пределами ожидаемого диапазона дат (определяется динамически из имён файлов, а не хардкодится). +- **[md]** Аномалии в реальных данных — норма, а не баг загрузки. Задача raw — сохранить как есть. + +### Секция 6: Самостоятельное задание +- **[md]** Инструкция: (1) прочитать `taxi_zone_lookup.csv` из raw-зоны через Spark, (2) вывести схему + 10 строк, (3) посчитать количество зон по borough, (4) найти топ-5 pickup locations и join с зонами, (5) найти ещё одну аномалию или интересный паттерн в данных. +- **[code]** 4 пустых ячейки с `# Ваш код: ...`. + +### Секция 7: Почему raw-слой важен +- **[md]** Мини-лекция (3-5 абзацев): raw = неизменяемый архив; параллель с stg/ods в Greenplum; почему не скачиваем в ноутбуке; почему не чиним на месте (можно перестроить bronze/silver заново); в Lakehouse raw-файлы лежат в storage отдельно от compute — Spark читает их напрямую, а для Trino потребуется оформить данные в таблицу. + +### Секция 8: Checkpoint +- **[md]** Вопросы для самопроверки: (1) показать файлы в raw-зоне MinIO, (2) какая схема у Yellow Taxi — сколько колонок, (3) какие колонки содержат null — это баг или норма, (4) зачем raw-слой, если данные потом трансформируются, (5) почему не скачиваем из ноутбука, (6) что произойдёт, если NYC TLC изменит формат файла. + +### Секция 9: Завершение +- **[md]** «Мы намеренно не удаляем raw-данные из MinIO. В Модуле 4 мы будем использовать их для создания bronze-слоя.» +- **[code]** `spark.stop()`. + +### Дизайн-решения по ноутбуку + +- **Helper-код:** весь код inline в ноутбуке. Отдельный модуль в `src/` не создаётся — студент должен видеть каждый шаг явно (`boto3.upload_file()`, `spark.read.parquet()`, `.printSchema()`). +- **Cleanup:** ноутбук НЕ удаляет raw-данные из MinIO — они нужны Модулю 4 (bronze). Spark-сессия останавливается. Явное пояснение в конце ноутбука. + +## Checkpoint + +Студент должен уметь: + +- показать файлы в raw-зоне MinIO (через MinIO Console или через код); +- прочитать raw PARQUET через Spark и вывести несколько строк; +- назвать схему Yellow Taxi (количество колонок, основные типы); +- показать, какие колонки содержат null-значения; +- объяснить, почему raw-слой нужен как воспроизводимая неизменяемая точка входа; +- объяснить, почему данные не скачиваются прямо из ноутбука. + +## Acceptance Criteria + +- `docker-compose.yml` содержит volume mount `./data:/opt/data:ro` для контейнера `jupyter`, и `docker compose config` проходит без ошибок. +- `START_HERE.md` содержит воспроизводимую инструкцию по скачиванию NYC Taxi PARQUET (минимум 3 файла + taxi zone lookup) до запуска Модуля 3. +- `.gitignore` содержит `/data/` для исключения датасета из git. +- `notebooks/03_raw_ingest_and_first_read.ipynb` можно выполнить сверху вниз в поднятом Jupyter-окружении при наличии скачанного data bundle. +- Ноутбук следует методике курса: `объяснение -> демонстрация -> самостоятельное повторение -> checkpoint`. +- Ноутбук использует паттерны из Модулей 1-2: `boto3` для MinIO, SparkSession без `.master()`, `setLogLevel("ERROR")`. +- Raw-данные остаются в MinIO после выполнения ноутбука (cleanup отсутствует намеренно). +- При отсутствии data bundle — понятный assert с отсылкой к `START_HERE.md`. +- `docs/stack_reference.md` обновлён: секция JupyterLab содержит `./data` mount. +- `plans/README.md` содержит строку Модуля 3 в оглавлении. + +## Риски + +- **Размер данных.** 3 месяца Yellow Taxi — примерно 120-150 MB (~40-50 MB на файл). Комфортно для стенда с 10 GB RAM. Extended (12 месяцев, ~600 MB) может замедлить Spark, но не должен вызывать падения. +- **Студент забыл скачать данные.** Assert в Секции 1 с понятным сообщением и ссылкой на `START_HERE.md`. +- **NYC TLC изменит формат/URL.** Ноутбук использует wildcard `yellow_tripdata_*.parquet` без привязки к году. Инструкция в `START_HERE.md` фиксирует конкретные файлы 2024-01..03, но если URL станут недоступны — потребуется обновление инструкции. +- **Директория `data/` не существует.** Docker volume mount создаст пустую директорию, assert в ноутбуке поймает отсутствие файлов. +- **Пересоздание контейнера.** Добавление volume в `docker-compose.yml` требует пересоздания контейнера `jupyter` (`docker compose up -d` достаточно — Docker пересоздаст только изменённый сервис). После этого bind mount уже работает: новые файлы в `./data` видны сразу без дополнительных перезапусков. Указать явно в инструкции. + +## Out of Scope + +- Создание Iceberg-таблиц — отложено до Модуля 4. +- Трансформации и очистка данных — отложено до Модулей 4-5. +- Чтение raw-данных через Trino (Trino не умеет читать произвольные файлы без таблицы) — отложено до Модуля 6. +- Партиционирование, MERGE, streaming, gold-слой. +- Автоматизированный скрипт скачивания (может быть добавлен позже, не является блокером).