pgx
AI 에이전트를 위한 경량 Postgres, Rust로 구현, 하나의 런타임에서 수천 개의 격리된 데이터베이스 제공
Apache Spark와 Delta Lake 파이프라인으로 7,950만 건의 NYC 옐로우 택시 데이터를 처리
Apache Spark + Delta Lake pipeline over 79.5M NYC yellow-taxi trips: data quality, gold tables, streaming, MLlib forecast and a tuning study.
데이터Python · HTMLGitHub @neilss170온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Apache Spark + Delta Lake lakehouse over 79.5M NYC taxi trips: medallion layers, data-quality quarantine, Structured Streaming, MLlib forecast, tuning study, live dashboard.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗An Apache Spark 4.1 + Delta Lake pipeline over 79.5 million NYC yellow-taxi trips (all of 2023–2024). It runs medallion layers (bronze, silver, gold) with data-quality rules and a quarantine table, a Structured Streaming replay (from files and from Kafka), a Spark MLlib demand forecast and a measured tuning study. Everything runs in Docker on a laptop: one Spark container, plus a Kafka broker for the Kafka replay.
Live dashboard: https://neilss170.github.io/nyc-taxi-lakehouse/ (five tabs: overview, a choropleth of all 263 taxi zones, data-quality breakdown, forecast vs actuals, and the tuning study with the streaming check; plain HTML + SVG, no framework)
Every number below comes from a file in reports/, written by the pipeline itself.
| Stage | What it does | Result |
|---|---|---|
| Bronze | Lands 24 monthly Parquet files in one Delta table, normalising schema drift | 79,479,946 rows, reconciled exactly against source file metadata |
| Silver | Dedupes, applies 15 profiled rules (10 errors, 5 warnings), quarantines failures with reasons | 78,245,114 rows pass (98.45%); 1,234,826 quarantined; 6 duplicates; every row accounted for |
| Gold | Six business tables: daily KPIs, zone × hour demand, zones, borough flows, hour × weekday, airports | All reconcile to the silver total; a gap in the published source data is detected automatically |
| Streaming | Replays Dec 2024 day by day: 15-min zone windows, 1 h watermark, foreachBatch MERGE | 3,585,099 rows in 31 micro-batches (~53k rows/s); all 240,424 windows match the batch result |
| Kafka | Publishes Dec 2024 to a 3-partition topic keyed by zone; Spark consumes it in two runs, the second a restart from checkpointed offsets | 3,585,099 messages, each consumed once across the restart (~41k rows/s); 0 dropped by the watermark; all 240,424 windows match the batch result |
| Forecast | Day-ahead hourly pickups for the 88 busiest zones (98.8% of trips), GBT vs naive baselines | Held-out Q4 2024 WAPE 19.7% vs 26.2% for the best baseline (24.8% lower error) |
| Tuning | Six before/after experiments on the real silver table | Median of 5 rounds: partition pruning 12×, broadcast join 8.4×, Parquet vs CSV 2.9× |
Profiling the raw data changed the design. These are the findings worth knowing:
int64 and passenger_count as double, and calls a column
airport_fee. Every later month uses int32/int64 and calls it Airport_fee. Bronze maps both onto one
canonical schema, matching column names case-insensitively, and a test covers both variants.total_amount. The gap is
almost always exactly $2.50 (congestion surcharge) or $1.75 (airport fee), and 83% of those rows come from vendor 1.
That vendor folds those fees into extra, so this is a reporting convention. It's tagged as a warning, not rejected.payment_type = 0 with null passenger count and rate code. They are real trips,
so they stay in silver with a warning tag.Each experiment times the same query under two settings on the real silver table: the median of 3 runs after a
warm-up, run end to end with Spark's noop sink. One round of that turned out to be noisy (a from-scratch rerun moved
some speed-ups by half), so the study runs 5 rounds, each in a fresh Spark JVM, and reports the median speed-up with
its min–max range. Every round is kept in reports/benchmark_rounds/. Where Delta's file
statistics explain the result, the file count is shown too; for Z-order it is read straight from the Delta log's
per-file min/max stats.
| Experiment | Baseline → tuned | Files read | Median seconds | Speed-up (median) | Range over 5 rounds |
|---|---|---|---|---|---|
| Partition pruning (Dec 2024 out of 24 months) | date_format(pickup_ts) filter → filter on partition column | 48 → 2 | 4.45 → 0.37 | 12.0× | 9.2–13.2× |
| Broadcast join (78M trips × 265 zones) | sort-merge join → broadcast hash join | 13.57 → 1.61 | 8.4× | 6.4–9.4× | |
| Columnar format (one month, 2 of 30 columns) | CSV → Delta/Parquet | 1.48 → 0.44 | 2.9× | 2.6–3.6× | |
| Z-order data skipping (JFK only, 6 months) | random layout → OPTIMIZE ZORDER BY pu_location_id | 48 → 2 of 20 could match | 0.39 → 0.24 | 1.6× | 1.4–4.8× |
| File compaction (6 months) | 1,200 small files → OPTIMIZE | 1,200 → 1 | 0.74 → 0.55 | 1.4× | 1.1–1.8× |
| Adaptive query execution (1,000 shuffle partitions) | AQE off → on | 3.84 → 3.17 | 1.2× | 1.2–1.4× |
What to read from this:
pickup_date directly would still be fast,
because Delta skips files using min/max stats. date_format(pickup_ts, ...) hides the column from both partition
pruning and those stats.The small-file problem was real in this project too. Silver was first written straight out of a 200-partition shuffle: 4,800 files averaging 0.59 MB. Writing each month as 2 files gives 48 files of about 49 MB. Each month's write now runs on 2 tasks instead of 200, so the silver build got slower (735 s → 921 s); every read downstream gets the benefit.
The forecast is a day-ahead model of hourly pickups per zone. Every feature is at least 24 hours old, the zone × hour grid is zero-filled, the zone-level mean is fit on training rows only, and the split is by time (train before 2024-10-01).
| Model | WAPE | MAE (trips/h) | RMSE |
|---|---|---|---|
| Gradient-boosted trees (MLlib) | 19.69% | 10.87 | 23.13 |
| Same hour last week | 26.19% | 14.46 | 32.93 |
| Same hour yesterday | 28.35% | 15.64 | 35.55 |
| Mean of same hour, last 7 days | 28.89% | 15.95 | 33.93 |
lag_168 (same hour last week) carries 74% of the feature importance, because weekly seasonality dominates taxi demand.
You need Docker. The data (1.3 GB download, about 5.3 GB once all layers are built) lives in a Docker volume.
docker compose build
docker compose run --rm spark python -m lakehouse all # download -> bronze -> ... -> benchmark
docker compose run --rm spark python -m pytest # tests
python -m http.server -d site 8000 # dashboard (after: cp -r reports site/reports)
docker compose --profile kafka up -d --wait kafka # single-node Kafka (KRaft), only for the kafka stage
docker compose run --rm spark python -m lakehouse kafka # also lets the Kafka tests run instead of skipping
You can run stages one at a time (python -m lakehouse silver gold). Each stage overwrites only the
months it processes, so re-running a stage is safe. Set LAKEHOUSE_START / LAKEHOUSE_END (YYYY-MM)
to change the date range, and SPARK_DRIVER_MEMORY to change the heap size (default 5g).
src/lakehouse/
download.py TLC files + SHA-256 manifest, retries, size check
bronze.py canonical schema, replaceWhere per month, source reconciliation
quality.py declarative error / warning rules
silver.py dedupe, tag, split to silver / quarantine, row conservation check
gold.py business tables, source-gap flag, dashboard extracts
zonemap.py TLC zone shapefile -> simplified SVG paths for the dashboard map
streaming.py file-stream replay, watermark, foreachBatch MERGE, batch parity check
kafka_stream.py Kafka producer + consumer replay with a restart, offset and batch parity checks
forecast.py dense grid, leakage-safe features, GBT vs baselines
benchmark.py before/after timing harness, Delta log file-stats reader
vacuum.py removes data files no retained Delta version references
tests/ runs on a local Spark session in Docker and on GitHub Actions
reports/ JSON output of every stage (the source of every number here)
site/ static dashboard (GitHub Pages)
local[*] Spark in an 8 GB Docker VM. The timings are laptop numbers; the ratios are what matter.