Skip to content

millerharry/data-pipeline

Repository files navigation

data-pipeline · Melbourne pedestrian foot-traffic

A small, reproducible, tested ETL + analysis pipeline that ingests the City of Melbourne's Pedestrian Counting System open data, cleans and validates it, loads it into DuckDB, and answers a real question with three clear charts.

CI Python Ruff License: MIT

The question this answers:

When and where is Melbourne's CBD busiest on foot, and how does the rhythm differ between weekdays and weekends?


What this demonstrates

  • A real ETL pipeline, not a script dump: four cleanly separated stages (ingest → transform → load → analyze) with a single reproducible entry point (make pipeline).
  • The cleaning/aggregation logic is the tested core. Pure, side-effect-free functions in transform.py with 43 passing tests that assert the data-quality contract exactly (nulls, negatives, duplicates, type coercion, feature derivation, aggregation correctness).
  • A hand-rolled data-quality gate (validate.py) that fails the pipeline fast if an invariant is violated, the kind of guardrail that keeps a dataset trustworthy in production.
  • Runs fully offline. A clearly-labelled synthetic sample (in the dataset's true schema) is committed, so git clone && make pipeline just works with no keys and no network. A live Socrata fetch is available behind a flag.
  • Idiomatic, strictly-typed Python: from __future__ import annotations, typed signatures, docstrings, ruff-clean, src/ layout, pyproject.toml, a Makefile and CI on Python 3.11 + 3.12.

The dataset

City of Melbourne: Pedestrian Counting System (Socrata resource b2ak-trbp). A network of automated sensors across the CBD records an hourly pedestrian count at each location. It is auth-free public data and one of Melbourne's most-loved open datasets.

Column Meaning
Date_Time, Year, Month, Mdate, Day, Time Timestamp of the reading (and its parts)
Sensor_ID, Sensor_Name The sensor / location
Hourly_Counts Pedestrians counted in that hour

⚠️ Sample data: the committed data/raw/pedestrian_counts_sample.csv is a synthetic sample generated by ingest.generate_sample() in the exact upstream schema, with realistic commuter/leisure/seasonal patterns and a few deliberate defects (sensor dropouts, a negative reading, a stray-whitespace label, a duplicate row) so the cleaning stage has genuine work to do. It is not real measured data. To run against the real API: python -m data_pipeline.pipeline --source remote.


Architecture

flowchart LR
    A["ingest.py<br/>sample or Socrata API"] --> B["transform.py<br/>clean + derive features"]
    B --> C["validate.py<br/>data-quality gate"]
    C --> D["load.py<br/>DuckDB warehouse<br/>+ Parquet"]
    D --> E["analyze.py<br/>3 charts → reports/"]

    subgraph tested["unit-tested core"]
        B
        C
    end
Loading
data/raw/*.csv ──▶ clean DataFrame ──▶ ✓ validated ──▶ pedestrian.duckdb ──▶ reports/*.png
                  (transform.clean)   (validate)        (fact + aggregates)   (analyze)

Each stage is an importable, independently-testable module; pipeline.py wires them together and exposes a CLI.


Key findings

(from the committed 70-day sample: 20,119 cleaned hourly readings, ~10.4M pedestrians across 12 CBD sensors)

1. Two completely different daily rhythms

Foot-traffic by hour of day

Weekdays show the classic commuter double-peak: a morning spike at 08:00 (~977/hr) and a larger evening surge at 17:00 (~1,321/hr). Weekends collapse that into a single, later early-afternoon hump peaking at 13:00 (~1,151/hr). At 03:00 the city is ~130× quieter than its weekday evening peak.

2. Foot-traffic is concentrated at transport hubs

Top 10 busiest sensors

Volume is dominated by the major interchanges and the Bourke Street retail core. Flinders Street Station Underpass tops the list (~1.33M total), followed by Melbourne Central and Town Hall (West).

3. The day-of-week × hour fingerprint

Weekday × hour heatmap

The heatmap makes the structure obvious: bright weekday commuting corridors (brightest Friday at 17:00) that dissolve into the broad, later weekend afternoons. Weekday mornings carry ~75% more foot-traffic than weekend mornings (695 vs 397/hr avg), while afternoons are near-identical; the commuter signal is almost entirely a morning-and-evening, Monday-to-Friday phenomenon.


Key technical decisions

Decision Why
pandas for transforms Mature, expressive, and the lingua franca of data work; the cleaning logic reads like the spec it implements.
DuckDB as the load target An embedded, zero-config analytical engine: columnar SQL over the cleaned data with no server to run. It also exports the clean fact table to Parquet natively (no pyarrow dependency).
Synthetic-but-realistic sample, committed The repo must tell its story offline and in CI with no keys or network. The generator is seeded, so runs are byte-for-byte reproducible.
Validation returns a report and can raise Callers can log every check; raise_for_status() turns hard failures into a non-zero exit so a bad dataset never flows downstream silently.
Object-oriented Matplotlib (Figure/FigureCanvasAgg) Renders headlessly in CI without mutating any global backend, so importing the package never breaks a notebook's inline plotting.
Pure transform functions, no I/O The core is trivial to unit-test; aggregations are cross-checked against independent groupby computations in the tests.

Project structure

data-pipeline/
├── src/data_pipeline/
│   ├── config.py      # paths + the true upstream schema (single source of truth)
│   ├── ingest.py      # synthetic sample generator + optional Socrata download
│   ├── transform.py   # cleaning + aggregations  <- the tested core
│   ├── validate.py    # hand-rolled data-quality contract
│   ├── load.py        # DuckDB warehouse (fact table, aggregates, views, Parquet)
│   ├── analyze.py     # 3 charts -> reports/
│   └── pipeline.py    # orchestration + CLI (`python -m data_pipeline.pipeline`)
├── tests/             # pytest: transform / validate / load / pipeline (43 tests)
├── notebooks/analysis.ipynb   # executed walkthrough with embedded charts
├── data/raw/          # committed synthetic sample (offline)
├── reports/           # committed chart images
├── Makefile           # make pipeline / test / lint / verify
└── .github/workflows/ci.yml

Getting started

Requires Python 3.11+.

git clone https://github.com/millerharry/data-pipeline.git
cd data-pipeline

make pipeline      # creates a venv, installs deps, runs ingest->transform->load->report

make pipeline writes the DuckDB warehouse to data/processed/ and refreshes the charts in reports/. Other targets:

make install   # set up the local virtualenv only
make test      # run the test suite
make lint      # ruff
make verify    # lint + test + pipeline (what CI runs)
make notebook  # launch the analysis notebook
make help      # list all targets

Prefer to drive it directly?

python -m pip install -e ".[dev]"
python -m data_pipeline.pipeline                   # offline sample (default)
python -m data_pipeline.pipeline --source remote   # live City of Melbourne API

No secrets are needed. See .env.example for the (optional) remote-source settings.


Testing

make test          # or: pytest

The suite (43 tests, fully offline) covers:

  • Cleaning contract: nulls / negatives / duplicates are dropped, timestamps parsed, sensor names trimmed, types coerced, is_weekend/daypart derived, and clean() proven pure (input frame untouched).
  • Aggregations: shapes, ordering, and values cross-checked against independent groupby computations; totals reconcile to the source sum.
  • Data-quality validation: passes on clean data; each invariant (negatives, duplicates, out-of-range hours, row-count floor, missing columns) is asserted to fail when violated.
  • Load: DuckDB tables/views are created, row counts round-trip, and aggregate totals reconcile.
  • End-to-end: the whole pipeline runs into a temp dir and produces all artefacts, deterministically.

ruff check . is clean and enforced in CI.


Reproducing the analysis

Everything is deterministic (seeded RNG), so a fresh make pipeline reproduces the committed charts exactly. To explore interactively, open notebooks/analysis.ipynb (already executed with embedded outputs); it reuses the same package functions, so the notebook and the pipeline can never drift apart.


Why I built this

I wanted a compact, honest showcase of the data-operations discipline I practised at Clutterbot, where I worked on real-world ML dataset workflows on AWS: collecting raw data, cleaning and validating it, loading it somewhere queryable, and turning it into something a human can act on, all reproducibly and with the cleaning logic under test so the dataset stays trustworthy as it grows.

This project distils that pattern down to its essence on a public, genuinely interesting Melbourne dataset: ingest → transform → validate → load → analyse, reproducible end-to-end, with the core logic covered by tests.

Maps to: my data-collection & ML-dataset work at Clutterbot.


License

MIT © 2026 Harry Miller. See LICENSE.

About

Reproducible ETL + analysis on City of Melbourne pedestrian open data: pandas to DuckDB to charts, validated and tested.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors