Raw scraped e-commerce data is only useful if someone can actually explore it. A one-off notebook analysis answers today's question — but the moment a business user (or a recruiter reviewing the work) wants to slice the data differently, filter by category, or drill into a specific segment, it means going back to the code. There's no self-serve way to ask a new question of the data.
A multi-page BI dashboard, backed by a real PostgreSQL database, that lets anyone explore the Lazada vs Zalora price war dataset interactively — no code required:
- Live KPIs — total products, average discount, platform breakdown, at a glance
- Interactive comparison — filter by category and price range, charts update instantly
- Category drill-down — pick a category to see every product in it, sortable, with summary stats
- One-command deployment —
docker compose up --buildseeds the database and serves the dashboard, zero manual steps
- 📈 KPI Overview — total products, avg discount %, products on discount, per-platform breakdown (count, avg discount, avg rating)
- 🔍 Interactive Filters — category dropdown + price range slider, shared across pages, charts update live via Dash callbacks
- 📊 Price Comparison Charts — avg discount by platform, price distribution (log-scale box plot), product count by category
- 🗂️ Category Drill-Down — cross-category discount comparison, or a sortable product table for any selected category
- 🐘 PostgreSQL Backend — real relational database, not a static CSV — schema includes indexes and a discount-filtered view
- 🐳 Docker-First — Compose stack with a health-checked Postgres service and automatic, idempotent data seeding on first boot
- ♿ Accessible Color Palette — platform colors validated for colorblind-safe separation and contrast on light/dark surfaces
- ✅ Integration Tested — 14 tests running against the real database, auto-skipping cleanly if Postgres isn't reachable
| Layer | Tool | Purpose |
|---|---|---|
| Dashboard Framework | Plotly Dash | Multi-page app, callbacks, interactive components |
| Charts | Plotly Graph Objects | Bar charts, box plots, all built as pure functions |
| Database | PostgreSQL 16 | Relational store for the product dataset |
| ORM / Connection | SQLAlchemy | Query execution, connection pooling |
| Data Processing | Pandas | Query result shaping, numeric coercion |
| Deployment | Docker Compose | App + database, healthcheck-gated startup |
| Config | python-dotenv | DB credentials from .env, never hardcoded |
| Testing | pytest | 14 integration tests against real migrated data |
| Page | URL | What it shows |
|---|---|---|
| Overview | / |
KPI cards: total products, avg discount %, products on discount, per-platform breakdown |
| Price Comparison | /comparison |
Lazada vs Zalora charts with category + price-range filters, updating live |
| Categories | /categories |
Category-level discount comparison, or a sortable detail table for a selected category |
git clone https://github.com/rizalcodes/ecommerce-bi-dashboard.git
cd ecommerce-bi-dashboard
docker compose up --buildThat's it — Postgres starts, waits to be healthy, the app seeds the database automatically from data/raw/, and the dashboard is live at http://localhost:8050.
Port note: the database maps to host port 5433 (not 5432), since a native PostgreSQL install often already occupies 5432. This only affects host-side tools (psql, tests, local dev) — the app itself connects internally on the standard port.
python -m venv .venv
.venv\Scripts\activate # Windows (source .venv/bin/activate on macOS/Linux)
pip install -r requirements-dev.txt
copy .env.example .env # fill in; for the compose db use DB_PORT=5433
docker compose up -d db
python data/migrate.py # one-time seed (idempotent, safe to re-run)
python app.pydocker compose up -d db
pytestecommerce-bi-dashboard/
├── app.py # Dash entry point, page registration
├── pages/
│ ├── overview.py # KPI summary
│ ├── comparison.py # Filtered charts
│ └── categories.py # Category drill-down
├── data/
│ ├── db.py # SQLAlchemy engine, credentials from .env
│ ├── queries.py # All SQL lives here
│ ├── migrate.py # Idempotent seed script
│ ├── schema.sql # Table + index + view definitions
│ └── raw/ # Source CSVs (Lazada + Zalora)
├── components/ # Reusable filters, KPI cards, charts
├── tests/ # 14 integration tests
├── docker-compose.yml
└── Dockerfile
Rizal
Built with Plotly Dash, PostgreSQL, and the belief that a chart you can't filter is just a screenshot.