docs(plan): добавлен план модуля 3 по raw-ingest
- Зачем: - нужен согласованный 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-проверка стенда и ноутбука не выполнялась.
This commit is contained in:
@@ -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/<original_filename>`. Отдельный 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-слой.
|
||||
- Автоматизированный скрипт скачивания (может быть добавлен позже, не является блокером).
|
||||
Reference in New Issue
Block a user