Files
mini-lakehouse-lab/plans/module-05-silver-layer.md
T
ddadminandClaude Opus 4.6 68c530c855 refactor(course): перенесена загрузка taxi_zone_lookup из домашки в демо-секцию
- Зачем:
  - демо-ячейки последующих модулей не должны зависеть от домашних заданий предыдущих — модуль 5 использует lookup в демо-коде для JOIN.
- Что:
  - модуль 4: секция 8 из домашки (5 пустых ячеек) превращена в демо с заполненным кодом (чтение CSV, CTAS).
  - модуль 4: новая домашка (секция 9) — осмотр metadata lookup-таблицы, сравнение с основной.
  - модуль 4: секции перенумерованы (9→10, 10→11), завершение упоминает обе bronze-таблицы.
  - модуль 5: assert-сообщение для lookup обновлено (убрана ссылка на «самостоятельное задание»).
  - планы модулей 4 и 5: обновлены дизайн-решения, структура секций и раздел рисков.
- Проверка:
  - прогнать готовые ячейки модуля 4 → lookup-таблица создаётся автоматически → модуль 5 проходит без ошибок.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:47:05 +03:00

27 KiB
Raw Blame History

Модуль 5. Слой silver и воспроизводимые трансформации

Статус: Ready for validation Последнее обновление: 2026-03-07

Цель

Научить студента переходу от bronze-таблицы «как есть» к осознанно очищенной и обогащённой silver-таблице. Студент формулирует правила трансформаций явно (не зарытыми в код), применяет их к bronze, обогащает таблицу вычисляемыми колонками и join-данными, а затем проверяет результат на трёх уровнях: строки, схема, агрегаты.

Результат для студента

После прохождения модуля студент:

  • понимает, зачем нужен silver-слой и чем он отличается от bronze;
  • умеет сформулировать правила трансформации явно и зафиксировать их как «реестр правил» перед кодом;
  • умеет отфильтровать некачественные записи из bronze по осознанным критериям;
  • умеет нормализовать типы колонок (DOUBLE -> INT для passenger_count, RatecodeID);
  • умеет обработать NULL-значения явным образом (coalesce);
  • умеет добавить вычисляемую колонку (trip_duration_minutes);
  • умеет обогатить данные через JOIN с lookup-таблицей (названия зон pickup/dropoff);
  • умеет выполнить проверки качества результата на трёх уровнях: строки, схема, агрегаты;
  • может сопоставить bronze/silver с привычным ods/dds-мышлением.

Deliverables

  • notebooks/05_silver_layer.ipynb — основной ноутбук Модуля 5;
  • обновление plans/README.md — строка Модуля 5 в оглавлении.

Инфраструктурные изменения не требуются.

Зависимости

Предусловия

  • Стенд поднят и работает (Модуль 1).
  • Студент прошёл Модуль 4: таблицы lakehouse.bronze.nyc_taxi_yellow и lakehouse.bronze.taxi_zone_lookup существуют.

Что используют последующие модули

  • Модуль 6 читает lakehouse.silver.nyc_taxi_yellow через Spark и Trino.
  • Модуль 8 использует silver в финальной практике.

Дизайн-решения

Namespace: lakehouse.silver

Семантическое имя (как lakehouse.bronze в Модуле 4). Подкрепляет мышление слоями, Модуль 6 будет читать lakehouse.silver.*.

Правила трансформаций: явный реестр в markdown перед кодом

Ключевая методическая идея модуля — правила не должны быть зарыты в код. Перед блоком кода стоит markdown-таблица с реестром правил (аналог mapping specification в DWH-практике). Это делает трансформации прозрачными и подкрепляет PRD Risk #4.

Состав трансформаций: три группы

Группа 1 — Фильтрация:

  • F1: fare_amount < 0 — отрицательные тарифы (аномалия из Модуля 3)
  • F2: total_amount > 1000 — экстремальные суммы (аномалия из Модуля 3)
  • F3: tpep_pickup_datetime вне [2024-01-01, 2025-01-01) — записи за пределами учебного диапазона (аномалия из Модуля 3)

Группа 2 — Нормализация и обработка NULL:

  • N1: passenger_count — DOUBLE -> INT, NULL -> 0
  • N2: RatecodeID — DOUBLE -> INT, NULL -> 99 (unknown)
  • N3: congestion_surcharge — NULL -> 0.0
  • N4: Airport_fee — NULL -> 0.0

Группа 3 — Обогащение:

  • E1: trip_duration_minutes — вычисляемая колонка (unix_timestamp(dropoff) - unix_timestamp(pickup)) / 60
  • E2: pickup_borough, pickup_zone — LEFT JOIN с taxi_zone_lookup по PULocationID
  • E3: dropoff_borough, dropoff_zone — LEFT JOIN с taxi_zone_lookup по DOLocationID

Ряд правил намеренно оставлен для самостоятельного задания: фильтрация trip_distance = 0 AND total_amount != 0, нормализация payment_type (BIGINT -> INT с именованными значениями), вычисляемая колонка speed_mph.

Диапазон дат для F3

Фиксированный [2024-01-01, 2025-01-01) — полный 2024 год, а не только каноническое подмножество 2024-01..2024-03 из START_HERE.md. Причина: фильтр ловит записи с явно ошибочным годом (2023, 2025 и т.д.), а не зависит от того, какие месяцы скачал студент. Канонический bundle — 3 месяца, расширенный — до 12, и в обоих случаях диапазон [2024-01-01, 2025-01-01) корректен. В Модуле 3 диапазон выводился из имён файлов через infer_month_bounds, но в silver мы работаем с bronze-таблицей (не с файлами), поэтому фиксированный диапазон проще и уместнее.

JOIN: два LEFT JOIN с алиасами

LEFT JOIN (не INNER), чтобы не терять строки с LocationID без соответствия в lookup. Потеря строк из-за INNER JOIN была бы неявной трансформацией.

Создание таблицы: SQL CTAS с temp view

Тот же паттерн, что в Модуле 4: трансформации через PySpark DataFrame API (.filter(), .withColumn(), .join()), затем createOrReplaceTempView("silver_ready"), затем CREATE OR REPLACE TABLE lakehouse.silver.nyc_taxi_yellow USING iceberg AS SELECT col1, col2, ... FROM silver_ready с явным списком колонок.

Проверки качества: три уровня

  • Строковый (row-level): assert нет fare_amount < 0, нет total_amount > 1000, нет NULL в passenger_count/RatecodeID
  • Схемный (schema-level): наличие новых колонок, проверка типов (passenger_count = INT, RatecodeID = INT)
  • Агрегатный (aggregate-level): таблица-сравнение bronze vs silver (row count, % отфильтрованных, средние fare/distance)

Taxi zone lookup: prerequisite из демо-секции Модуля 4

Lookup создаётся в демонстрационном коде Модуля 4 (Секция 8), а не в самостоятельном задании. Это гарантирует, что студент, прогнавший готовые ячейки Модуля 4, имеет обе bronze-таблицы. Модуль 5 работает только с bronze-таблицами и не должен лезть в raw-слой — это нарушило бы принцип разделения ответственности между слоями (PRD Risk #4). Стратегия: жёсткий assert при отсутствии lakehouse.bronze.taxi_zone_lookup с понятным сообщением и отсылкой к Модулю 4: «Таблица lakehouse.bronze.taxi_zone_lookup не найдена. Вернись в Модуль 4 и выполни Секцию 8.»

Cleanup: нет

Silver нужен Модулям 6 и 8. Spark-сессия останавливается.

План работ

  1. Создать notebooks/05_silver_layer.ipynb по ячеечной структуре, описанной ниже.
  2. Обновить plans/README.md — добавить строку Модуля 5 в таблицу оглавления.
  3. Валидация: убедиться, что ноутбук выполняется сверху вниз на поднятом стенде после прохождения Модуля 4 (bronze-таблицы в каталоге).

Структура ноутбука

Секция 0: Введение

  • [md] Заголовок, цели модуля, prerequisite (Модуль 4). Таблица-сравнение:
Классический DWH (Greenplum) Lakehouse
Источник ods/stg-таблица Bronze Iceberg-таблица
Результат dds/fact-таблица с очищенными данными Silver Iceberg-таблица
Правила трансформаций Mapping specification / ETL-документация Явный реестр правил в ноутбуке перед кодом
Проверки качества dq-check скрипты, data quality framework Asserts на трёх уровнях
Воспроизводимость Повторный запуск ETL CREATE OR REPLACE TABLE ... AS SELECT
  • [md] Ключевая идея: silver = bronze + явные правила трансформации + проверки качества.

Секция 1: Spark-сессия и проверка bronze

  • [code] SparkSession (паттерн Модулей 2-4: без .master(), setLogLevel("ERROR")). Константы, вспомогательные функции (format_bytes, list_objects — переиспользование паттерна из Модуля 4).
  • [code] Assert: spark.table(BRONZE_TABLE).count() > 0 с отсылкой к Модулю 4. Assert: spark.table(BRONZE_LOOKUP_TABLE) существует — при отсутствии жёсткая ошибка с отсылкой к Секции 8 Модуля 4.
  • [md] Обе bronze-таблицы на месте. Теперь строим silver.

Секция 2: Профиль bronze — что нужно трансформировать

  • [md] Прежде чем писать трансформации, понимаем текущее состояние данных. В Модуле 3 мы уже нашли аномалии. Теперь смотрим через bronze.
  • [code] Краткий профиль: null-доли ключевых колонок, количество строк с fare_amount < 0, total_amount > 1000, pickup вне диапазона.
  • [md] Таблица-профиль: какие проблемы нашли, сколько строк затронуто, что будем делать.

Секция 3: Реестр правил трансформации

  • [md] Центральная идея модуля. Фиксируем все правила явно перед кодом. Аналог mapping specification в DWH.
  • [md] Таблица правил (F1-F3, N1-N4, E1-E3) с колонками: #, Колонка/область, Правило, Тип, Обоснование.
  • [md] Зачем это нужно: если через месяц спросят «почему в silver нет строк с отрицательным fare?», ответ в реестре, а не в git blame.

Секция 4: Применение правил — фильтрация

  • [md] Начинаем с фильтрации. Порядок осознанный: сначала фильтруем, потом нормализуем и обогащаем (не тратим ресурсы на трансформацию строк, которые будут удалены).
  • [code] Чтение bronze DataFrame. Применение F1, F2, F3. Подсчёт строк до/после, вывод количества и процента удалённых.
  • [md] Обычно фильтры качества удаляют небольшую долю строк. Если фильтр убирает заметную часть данных — это сигнал перепроверить правило или источник.

Секция 5: Применение правил — нормализация

  • [md] Приводим типы и обрабатываем NULL. Параллель с PostgreSQL: как ALTER COLUMN TYPE integer, но между слоями, а не in-place.
  • [code] Применение N1-N4: coalesce + cast для passenger_count и RatecodeID, coalesce для surcharge и fee. Каждый .withColumn() с комментарием, какому правилу соответствует.

Секция 6: Применение правил — обогащение

  • [md] Добавляем вычисляемые колонки и данные из lookup. LEFT JOIN — не теряем строки. Параллель с денормализацией в PostgreSQL.
  • [code] E1: trip_duration_minutes. E2, E3: два LEFT JOIN с taxi_zone_lookup (алиасы pu_zone, do_zone).
  • [code] Вывод нескольких строк с новыми колонками.

Секция 7: Создание silver-таблицы

  • [md] Используем CTAS с CREATE OR REPLACE. Namespace lakehouse.silver.
  • [md] Явное предупреждение о CREATE OR REPLACE: это полная перезапись таблицы с потерей предыдущего состояния и snapshot-истории. Для учебного silver это осознанный компромисс ради идемпотентности (повторный запуск ноутбука пересоздаёт таблицу с чистого листа). В рабочих сценариях это разрушительная операция — в Модуле 7 студент увидит, почему нужен другой подход. Параллель: DROP TABLE + CREATE TABLE AS SELECT в PostgreSQL.
  • [code] CREATE NAMESPACE IF NOT EXISTS lakehouse.silver. Регистрация temp view. CTAS с явным списком демо-схемы: 24 колонки (19 оригинальных + 5 новых). Это каноническая схема, на которую опираются Модули 6 и 8.
  • [md] Предупреждение о времени выполнения (2-4 мин на 3 мес данных).
  • [code] Верификация: count, printSchema.

Секция 8: Проверки качества результата

8a: Строковый уровень

  • [code] Assert: нет строк с fare_amount < 0, total_amount > 1000, NULL в passenger_count, NULL в RatecodeID.

8b: Схемный уровень

  • [code] Проверка наличия новых колонок (trip_duration_minutes, pickup_borough, pickup_zone, dropoff_borough, dropoff_zone). Проверка типов: passenger_count = int, RatecodeID = int.

8c: Агрегатный уровень

  • [code] Таблица-сравнение bronze vs silver: row count, % отфильтрованных, средние fare_amount, trip_distance.
  • [md] Ориентир: если фильтры удалили больше 5% строк, это повод пересмотреть правила или проверить данные.

Секция 9: bronze vs silver — разделение ответственности

  • [md] Мини-лекция (3-4 абзаца): raw -> bronze -> silver (-> gold, вне курса). Bronze = «как есть», silver = очищенные + обогащённые. Если правило неверно — bronze хранит оригинал, silver можно пересоздать.
  • [md] Таблица-сравнение bronze vs silver по свойствам: данные, NULL, типы, доп. колонки, проверки, правила, «можно пересоздать из».

Секция 10: Самостоятельное задание

  • [md] Инструкция: расширить silver-пайплайн тремя новыми правилами (по одному на каждый тип трансформации), пересоздать таблицу и проверить результат.

    Фильтрация — F4: удалить строки с trip_distance = 0 AND total_amount != 0 (аномалия, найденная в Модуле 3, но не включённая в демонстрацию).

    Нормализация — N5: привести payment_type из BIGINT в INT и дать осмысленное значение для NULL (например, 0 = unknown). Подсказка: паттерн coalesce + cast тот же, что для passenger_count.

    Обогащение — E4: добавить вычисляемую колонку speed_mph — средняя скорость поездки в милях в час: trip_distance / (trip_duration_minutes / 60). Подсказка: нужно обработать деление на ноль (если trip_duration_minutes = 0, результат должен быть NULL, а не ошибка).

    Проверки: после пересоздания silver написать: (1) row-level assert для F4, (2) schema-level проверку наличия speed_mph и типа payment_type, (3) сравнить количество строк до и после добавления F4.

  • [code] 6 пустых ячеек # Ваш код: ... (фильтрация, нормализация, обогащение, пересоздание таблицы, проверки, сравнение).

Секция 11: Checkpoint

  • [md] Вопросы:
    1. Чем silver отличается от bronze?
    2. Зачем фиксировать правила перед кодом?
    3. Какие три уровня проверок качества и зачем?
    4. Почему LEFT JOIN, а не INNER JOIN с lookup?
    5. Что если фильтр удалит 50% строк?
    6. Можно ли пересоздать silver, если обнаружится новая аномалия?

Секция 12: Завершение

  • [md] «Не удаляем silver-таблицу. В Модуле 6 будем читать через Spark и Trino, в Модуле 8 — финальная практика.»
  • [code] spark.stop().

Дизайн-решения по ноутбуку

  • Helper-код: весь код inline в ноутбуке. Студент должен видеть каждый шаг: фильтрацию, нормализацию, join, проверки.
  • Cleanup: нет. Silver нужен Модулям 6 и 8. Spark-сессия останавливается.
  • Порядок трансформаций: фильтрация -> нормализация -> обогащение. Логический порядок: сначала убираем мусор, потом чистим типы, потом добавляем контекст.
  • Spark API: трансформации через PySpark DataFrame API (.filter(), .withColumn(), .join()), запись через SQL CTAS (консистентно с Модулем 4).

Схемы silver-таблицы

Демо-схема (24 колонки) — после секций 4-7

Это каноническая схема, на которую опираются downstream-модули (6, 8). Она создаётся демонстрационным кодом ноутбука и является минимальной гарантированной схемой lakehouse.silver.nyc_taxi_yellow.

VendorID                    BIGINT          (без изменений)
tpep_pickup_datetime        TIMESTAMP       (без изменений)
tpep_dropoff_datetime       TIMESTAMP       (без изменений)
passenger_count             INT             (N1: DOUBLE->INT, NULL->0)
trip_distance               DOUBLE          (без изменений)
RatecodeID                  INT             (N2: DOUBLE->INT, NULL->99)
store_and_fwd_flag          STRING          (без изменений)
PULocationID                BIGINT          (без изменений)
DOLocationID                BIGINT          (без изменений)
payment_type                BIGINT          (без изменений)
fare_amount                 DOUBLE          (без изменений, F1 отфильтровал < 0)
extra                       DOUBLE          (без изменений)
mta_tax                     DOUBLE          (без изменений)
tip_amount                  DOUBLE          (без изменений)
tolls_amount                DOUBLE          (без изменений)
improvement_surcharge       DOUBLE          (без изменений)
total_amount                DOUBLE          (без изменений, F2 отфильтровал > 1000)
congestion_surcharge        DOUBLE          (N3: NULL->0.0)
Airport_fee                 DOUBLE          (N4: NULL->0.0)
trip_duration_minutes       DOUBLE          (E1: новая, вычисляемая)
pickup_borough              STRING          (E2: новая, из lookup JOIN)
pickup_zone                 STRING          (E2: новая, из lookup JOIN)
dropoff_borough             STRING          (E3: новая, из lookup JOIN)
dropoff_zone                STRING          (E3: новая, из lookup JOIN)

Расширенная схема (26 колонок) — после самостоятельного задания (секция 10)

Студент пересоздаёт silver-таблицу с дополнительными правилами. Итоговая таблица содержит 24 демо-колонки + 2 новых, с изменением типа payment_type. Downstream-модули (6, 8) не зависят от расширенной схемы: payment_type INT обратно совместим с BIGINT для чтения, а дополнительные колонки (speed_mph) не используются downstream.

payment_type                INT             (N5: BIGINT->INT, NULL->0; было BIGINT)
speed_mph                   DOUBLE          (E4: новая, trip_distance / (trip_duration_minutes / 60))

Также добавляется фильтр F4 (trip_distance = 0 AND total_amount != 0), который уменьшает количество строк.

Checkpoint

Студент должен уметь:

  • создать silver-таблицу из bronze с явными правилами трансформации;
  • показать реестр правил и объяснить обоснование каждого;
  • показать результаты проверок качества на трёх уровнях;
  • объяснить, чем silver отличается от bronze;
  • объяснить, почему silver можно пересоздать из bronze.

Acceptance Criteria

  • notebooks/05_silver_layer.ipynb можно выполнить сверху вниз в поднятом Jupyter-окружении после прохождения Модуля 4 (bronze-таблицы в каталоге).
  • Ноутбук следует методике курса: объяснение -> демонстрация -> самостоятельное повторение -> checkpoint.
  • Ноутбук использует паттерны из Модулей 1-4: SparkSession без .master(), setLogLevel("ERROR").
  • Ноутбук идемпотентен: повторное выполнение (CREATE OR REPLACE) не дублирует данные и не роняет ячейки.
  • CTAS использует явный список колонок, а не SELECT *.
  • Правила трансформации зафиксированы в markdown-таблице перед кодом, а не только в коде.
  • Проверки качества выполняются на трёх уровнях: строки, схема, агрегаты.
  • Silver-таблица lakehouse.silver.nyc_taxi_yellow остаётся после выполнения ноутбука для использования в Модулях 6 и 8.
  • При отсутствии bronze-таблиц — понятный assert с отсылкой к Модулю 4.
  • Текст ноутбука на русском языке с параллелями к PostgreSQL/Greenplum.
  • plans/README.md содержит строку Модуля 5 в оглавлении.

Риски

  • Taxi zone lookup отсутствует. Lookup создаётся в демо-секции Модуля 4 (Секция 8), поэтому риск минимален — студент должен был прогнать готовые ячейки. Жёсткий assert с отсылкой к Модулю 4. Модуль 5 не создаёт bronze-таблицы самостоятельно — это нарушило бы разделение ответственности между слоями.
  • Время CTAS. JOIN + трансформации — на 30-50% медленнее bronze CTAS. На 3 мес (~7 млн строк): 2-4 мин. Extended: до 5-10 мин. Предупреждение в markdown.
  • Column case sensitivity. Новые колонки (lowercase) рядом с оригинальными (mixed case: VendorID, PULocationID). Нормально для Iceberg, но добавить пояснение.
  • trip_duration_minutes отрицательные. Если dropoff < pickup — длительность отрицательна. Упомянуть как наблюдение, не фильтровать в демо (потенциально — для самостоятельного задания или будущего правила).
  • Идемпотентность. CREATE OR REPLACE TABLE ... AS SELECT (CTAS) для повторных запусков — тот же паттерн, что в Модуле 4. Пересоздаёт таблицу целиком.

Out of Scope

  • Gold-слой и бизнес-витрины.
  • Чтение silver через Trino — отложено до Модуля 6.
  • Сложные трансформации (MERGE, SCD, window functions).
  • dq-фреймворки (Great Expectations, dbt tests) — принцип показан через assert.
  • Time travel и schema evolution — отложено до Модуля 7.
  • Compaction и vacuum — отложено до Модуля 8.
  • Партиционирование silver-таблицы (PRD: вне v1).