pgx
PGX — Lightweight Postgres for AI agents. Written in Rust. Thousands of isolated databases in one runtime. 🐘
Translation pending
Apache Spark + Delta Lake pipeline over 79.5M NYC yellow-taxi trips: data quality, gold tables, streaming, MLlib forecast and a tuning study.
Translation pending
DataPython · HTMLGitHub @neilss170OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Apache Spark + Delta Lake lakehouse over 79.5M NYC taxi trips: medallion layers, data-quality quarantine, Structured Streaming, MLlib forecast, tuning study, live dashboard.
Translation pending
Taken from the repository README
Loading the full README.
Read the full 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.