- Зачем:
- нужен следующий практический шаг после модулей 1-2: доставить raw-данные в MinIO и впервые прочитать их через Spark.
- Что:
- добавлен ноутбук модуля 3 с raw ingest, first read, проверкой схемы, null-анализом и поиском аномалий.
- обновлены START_HERE, stack reference и docker-compose для data bundle в ./data и mount в /opt/data.
- добавлены правила игнорирования data bundle, .gitkeep для пустой директории и обновлён статус плана модуля.
- Проверка:
- docker compose config.
- выполнение notebooks/03_raw_ingest_and_first_read.ipynb на локальном data bundle.
Этот документ нужен для первого входа в курс и прохождения Модуля 1: поднять стенд, проверить сервисы, открыть интерфейсы и выполнить базовый smoke test.
Этот документ нужен для первого входа в курс: поднять стенд, проверить сервисы, открыть интерфейсы, выполнить базовый smoke test и подготовить локальный data bundle для следующих модулей.
## Маршрут прохождения
@@ -9,7 +9,8 @@
3. Убедиться, что контейнеры живы.
4. Открыть основные UI.
5. Зайти в Jupyter и выполнить `notebooks/01_environment_and_smoke_test.ipynb`.
6. При проблемах использовать логи и шаги диагностики из этого документа.
6. Перед Модулем 3 подготовить учебный датасет в `./data/nyc_taxi`.
7. При проблемах использовать логи и шаги диагностики из этого документа.
"- желательно пройти `notebooks/02_lakehouse_mental_model.ipynb`;\n",
"- заранее скачай data bundle в `./data/nyc_taxi` по инструкции из `START_HERE.md`.\n",
"\n",
"В классическом `PostgreSQL / Greenplum` данные часто уже лежат внутри системы. В Lakehouse путь более явный: сначала исходные файлы должны попасть в object storage, и только потом мы начинаем поверх них читать, профилировать и строить таблицы.\n",
"\n",
"| | Классический DWH | Lakehouse |\n",
"| --- | --- | --- |\n",
"| Где стартует работа | Данные уже внутри СУБД | Исходные файлы сначала попадают в storage |\n",
"| Первая практическая задача | `SELECT` из готовой таблицы | Загрузить raw и прочитать raw |\n",
"| Что важно не сломать | Таблицу / схему БД | Неизменяемый raw-слой |\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a5c56637",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import re\n",
"from datetime import datetime\n",
"\n",
"import boto3\n",
"import pandas as pd\n",
"from botocore.exceptions import ClientError\n",
"from pyspark.sql import SparkSession, functions as F\n",
"Набор файлов должен быть понятным и фиксированным. В этом модуле мы работаем не с «каким-то случайным parquet из интернета», а с заранее определённым учебным набором.\n"
]
},
{
"cell_type": "markdown",
"id": "dabd90f2",
"metadata": {},
"source": [
"## 2. Загрузка в raw-зону MinIO\n",
"\n",
"Raw-слой в Lakehouse похож на воспроизводимый входной `stg`-контур: мы сохраняем исходные файлы как есть, под оригинальными именами, не переписывая их содержимое.\n",
"\n",
"Конвенция пути в этом курсе:\n",
"\n",
"- bucket: `lakehouse`\n",
"- raw prefix: `raw/nyc_taxi/`\n",
"- полный путь для чтения: `s3a://lakehouse/raw/nyc_taxi/`\n",
"\n",
"Важно: raw-зона не смешивается с `warehouse/`, где потом будут жить Iceberg-таблицы.\n",
"\n",
"Повторный запуск этой секции должен быть безопасным:\n",
"\n",
"- если объекта ещё нет в raw-зоне, мы его загружаем;\n",
"- если объект уже есть и размер совпадает, мы пропускаем загрузку;\n",
"- если объект уже есть, но размер отличается, это сигнал проблемы с данными или bundle, и мы падаем сразу, без перезаписи raw.\n"
"Если объект уже существовал, мы не перезаписываем его без необходимости. Совпадающий размер означает, что повторный запуск секции идемпотентен. Несовпадающий размер считается конфликтом и останавливает сценарий до любых изменений в raw.\n",
"\n",
"На этом шаге данные уже лежат в storage. `Spark` может читать их напрямую по `s3a://` пути, а для `Trino` понадобится зарегистрированная таблица. До неё мы дойдём в Модулях 4 и 6.\n"
]
},
{
"cell_type": "markdown",
"id": "696d6adc",
"metadata": {},
"source": [
"## 3. Первое чтение raw PARQUET через Spark\n",
"\n",
"Теперь читаем raw-файлы напрямую из `MinIO` по `s3a://` пути. Это и есть первая важная практика Lakehouse: вычислитель (`Spark`) работает поверх файлов в object storage, а не только поверх локальной файловой системы или таблиц внутри одной СУБД.\n",
"\n",
"Здесь важно различать два режима работы:\n",
"\n",
"- `spark.read.parquet(\"s3a://...\")` читает raw-файлы напрямую по физическому пути;\n",
"- `spark.table(\"lakehouse....\")` читает уже зарегистрированную управляемую таблицу через каталог.\n",
"\n",
"Сейчас мы ещё не создавали `Iceberg`-таблицу, поэтому работаем именно через `spark.read.parquet(...)`. К `spark.table(...)` вернёмся в Модуле 4.\n"
"print(f\"Количество parquet-файлов в наборе: {len(yellow_files)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5e147271",
"metadata": {},
"outputs": [],
"source": [
"yellow_df.show(10, truncate=False)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "efa037e6",
"metadata": {},
"outputs": [],
"source": [
"input_files = sorted(yellow_df.inputFiles())\n",
"pd.DataFrame({\"input_file\": input_files})\n"
]
},
{
"cell_type": "markdown",
"id": "e8866f38",
"metadata": {},
"source": [
"Обрати внимание: мы читаем сразу несколько файлов wildcard-путём. Это обычный рабочий сценарий для raw-слоя, где данные часто приходят партиями по датам, дням или месяцам.\n",
"\n",
"В этом модуле мы намеренно не используем `cache()` для всего raw DataFrame. Для учебного профилирования важнее устойчиво прочитать исходные файлы, чем пытаться держать весь raw-срез в памяти executor-ов.\n"
]
},
{
"cell_type": "markdown",
"id": "9701728a",
"metadata": {},
"source": [
"## 4. Схема, типы и null-значения\n",
"\n",
"Следующий шаг после первого чтения: понять, какие колонки пришли, какие у них типы и где уже на raw-слое встречаются `null`.\n",
"\n",
"Здесь важно не перепутать диагностику и очистку. В raw-слое мы наблюдаем и фиксируем реальность данных, а не исправляем её.\n"
"`Null` в raw-данных сам по себе не является багом загрузки. Это нормальная часть профиля источника. Позже, на `bronze` и `silver`, мы будем принимать явные решения: какие поля очищать, что отбрасывать, а что нормализовать.\n"
]
},
{
"cell_type": "markdown",
"id": "69e6b77e",
"metadata": {},
"source": [
"## 5. Базовые метрики и аномалии\n",
"\n",
"Теперь быстро профилируем данные и ищем несколько очевидных проблемных паттернов:\n",
"\n",
"- отрицательный `fare_amount`;\n",
"- нулевая дистанция при ненулевой сумме;\n",
"- слишком большой `total_amount`;\n",
"- `pickup` вне ожидаемого диапазона дат.\n",
"\n",
"Диапазон дат будем выводить из имён файлов, а не хардкодить руками.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3a4ed596",
"metadata": {},
"outputs": [],
"source": [
"numeric_columns = [\n",
" column\n",
" for column, spark_type in yellow_df.dtypes\n",
" if spark_type.startswith((\"tinyint\", \"smallint\", \"int\", \"bigint\", \"float\", \"double\", \"decimal\"))\n",
"Реальные данные почти всегда содержат выбросы, странные записи и неоднозначные значения. Это не повод немедленно переписывать raw-файлы. Наоборот, raw нужен именно затем, чтобы исходный срез можно было перечитать и переработать заново с новыми правилами.\n"
]
},
{
"cell_type": "markdown",
"id": "095c9ac6",
"metadata": {},
"source": [
"## 6. Самостоятельное задание\n",
"\n",
"Используй raw-данные, которые уже лежат в `MinIO`:\n",
"\n",
"1. Прочитай `taxi_zone_lookup.csv` из raw-зоны через `Spark`.\n",
"2. Выведи схему и первые 10 строк.\n",
"3. Посчитай количество зон по `Borough`.\n",
"4. Найди `top-5` `pickup`-локаций в поездках и присоедини к ним названия зон.\n",
"5. Найди ещё одну аномалию или интересный паттерн и коротко опиши его.\n",
"\n",
"Подсказка: CSV-файл лежит по пути `s3a://lakehouse/raw/nyc_taxi/taxi_zone_lookup.csv`.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fe530d7d",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код: прочитай taxi_zone_lookup.csv из raw-зоны через Spark\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "99fb5256",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код: выведи схему и первые 10 строк\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "219a40fc",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код: посчитай количество зон по Borough и top-5 pickup locations с join на lookup\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5c944f60",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код: найди ещё одну аномалию или интересный паттерн в данных\n"
]
},
{
"cell_type": "markdown",
"id": "e23dfe47",
"metadata": {},
"source": [
"## 7. Почему raw-слой важен\n",
"\n",
"Raw-слой полезен не потому, что в нём удобно жить постоянно, а потому что он даёт устойчивую исходную точку. Если завтра поменяются правила очистки, дедупликации или нормализации, мы не обязаны заново скачивать источник и гадать, что именно уже успели «подправить». Мы просто перечитываем raw и перестраиваем downstream-слои.\n",
"\n",
"Для инженера с опытом `PostgreSQL / Greenplum` полезна такая аналогия: raw похож на аккуратный внешний `staging`-контур, но в Lakehouse он обычно хранится в object storage как набор файлов, а не как таблица внутри одной СУБД.\n",
"\n",
"Отдельно важно, что загрузка источника вынесена из ноутбука в onboarding. Это делает практику менее хрупкой: ноутбук не зависит от внешней сети, а набор данных остаётся одинаковым у всех студентов.\n",
"\n",
"Наконец, raw не стоит чинить «на месте». Если в исходных данных есть `null`, отрицательные значения или странные даты, это сигнал для явных трансформаций в `bronze` и `silver`, а не повод тихо переписать входные файлы.\n"
]
},
{
"cell_type": "markdown",
"id": "8849c73f",
"metadata": {},
"source": [
"## 8. Checkpoint\n",
"\n",
"Проверь себя:\n",
"\n",
"1. Можешь ли ты показать файлы raw-зоны в `MinIO` через код или `MinIO Console`?\n",
"2. Сколько колонок в схеме `Yellow Taxi` и какие типы там встречаются?\n",
"3. Какие колонки содержат `null` и почему это не обязательно баг загрузки?\n",
"4. Зачем нужен raw-слой, если дальше всё равно будут трансформации?\n",
"5. Почему мы не скачиваем датасет прямо из ноутбука?\n",
"6. Что произойдёт, если источник завтра поменяет формат файлов?\n"
]
},
{
"cell_type": "markdown",
"id": "371c4c9d",
"metadata": {},
"source": [
"## 9. Завершение\n",
"\n",
"Мы намеренно не удаляем raw-данные из `MinIO`. В Модуле 4 они понадобятся для построения первого управляемого `bronze`-слоя на `Iceberg`.\n"
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.