Skip to content

Commit c9b34ef

Browse files
committed
feat(forecaster): add v1 training pipeline, reference data, and ops workflows
Introduce the forecaster v1 build/evaluation scripts, feature store modules, housing finance helpers, and supporting docs/data so forecasting and admin operations are reproducible end-to-end. Made-with: Cursor
1 parent e621b60 commit c9b34ef

56 files changed

Lines changed: 22514 additions & 65 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ data/processed/.http_cache/
88
**/.http_cache/
99
data/reference/planning_areas.geojson
1010
!data/reference/planning_areas.geojson
11+
data/feedback/*
12+
!data/feedback/.gitkeep
13+
models/
14+
mlruns/
15+
mlflow.db
16+
mlflow.db-*
1117

1218
# Python
1319
.venv/

Makefile

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PYTHON ?= python3
22
PIP ?= $(PYTHON) -m pip
33

4-
.PHONY: install test ruff app report web clean-data rent-data health-serve
4+
.PHONY: install test ruff app report web clean-data rent-data bto-data health-serve forecaster-v1 forecaster-v1-verify forecaster-v1-deepchecks forecaster-v1-near-term-eval forecaster-feedback-prepare fetch-mrt-reference fetch-mrt-travel-times
55

66
install:
77
$(PIP) install -e ".[dev]"
@@ -20,6 +20,10 @@ clean-data:
2020
rent-data:
2121
$(PYTHON) -m singapore_eda.rent_ingest -o data/raw/median_rent_hdb.csv
2222

23+
# HDB BTO historical + future-facing datasets (launch pricing, completion status, property base)
24+
bto-data:
25+
$(PYTHON) -m singapore_eda.bto_ingest -o data/reference
26+
2327
app:
2428
$(PYTHON) -m streamlit run streamlit_app.py
2529

@@ -33,6 +37,27 @@ analysis:
3337
fetch-geo:
3438
$(PYTHON) scripts/fetch_reference_geo.py
3539

40+
fetch-mrt-reference:
41+
$(PYTHON) scripts/fetch_mrt_reference.py
42+
43+
fetch-mrt-travel-times:
44+
$(PYTHON) scripts/fetch_mrt_travel_times.py
45+
3646
# Optional: pip install -e ".[api]" — liveness/readiness + /metrics for operations
3747
health-serve:
3848
$(PYTHON) -m singapore_eda.health_server
49+
50+
forecaster-v1:
51+
$(PYTHON) scripts/build_forecaster_v1.py --input data/raw/hdb_resale_2017_onwards.csv
52+
53+
forecaster-v1-verify:
54+
$(PYTHON) scripts/verify_forecaster_repro.py
55+
56+
forecaster-v1-deepchecks:
57+
$(PYTHON) scripts/run_deepchecks_forecaster.py
58+
59+
forecaster-v1-near-term-eval:
60+
$(PYTHON) scripts/run_forecaster_near_term_eval.py --input data/raw/hdb_resale_2017_onwards.csv
61+
62+
forecaster-feedback-prepare:
63+
$(PYTHON) scripts/prepare_feedback_dataset.py

README.md

Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ From the repository root, the scripts create `.venv`, install the package, optio
2626

2727
| OS | Command |
2828
|----|--------|
29-
| **Windows** | Double-click or run: `run-all.cmd` |
30-
| **macOS / Linux** | `chmod +x run-all.sh` once, then `./run-all.sh` |
29+
| **Windows** | Double-click or run: `run-all-machinelearning.cmd` |
30+
| **macOS / Linux** | `chmod +x run-all-machinelearning.sh` once, then `./run-all-machinelearning.sh` |
3131

3232
- For faster repeat runs, set `RUN_ALL_SKIP_INSTALL=1` to skip dependency bootstrap when your `.venv` is already ready.
3333
- In the Streamlit UI, the **Project & method** tab states the **problem, data, storage, models, and how users interact** (dashboard / deployment). For a full **planning-area map** (not the two-polygon test fixture), run `python scripts/fetch_reference_geo.py` and set the map path to `data/reference/planning_areas.geojson`.
@@ -42,15 +42,210 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
4242
pip install -e ".[dev]"
4343
```
4444

45-
Then: `make test` · `hdb-download -o data/raw/hdb_resale_2017_onwards.csv` (or `--max-rows 20000` for a faster sample) · `make analysis` or `python scripts/run_analysis.py` · `streamlit run streamlit_app.py`.
45+
Then: `make test` · `hdb-download -o data/raw/hdb_resale_2017_onwards.csv` (or `--max-rows 20000` for a faster sample) · `hdb-bto-download -o data/reference` · `make analysis` or `python scripts/run_analysis.py --refresh-bto` · `streamlit run streamlit_app.py`.
4646

47-
**Geo (Graph tab, planning-area edges):** `pip install -e ".[geo]"` and run `make fetch-geo` or `scripts/fetch_reference_geo.py` to pull the official [MP2025 planning-area boundary (No Sea)](https://data.gov.sg/datasets/d_2cc750190544007400b2cfd5d7f53209/view) GeoJSON (old static `geo.data.gov.sg` MP14 links are no longer served). **Rent / gross yield:** set `HDB_MEDIAN_RENT_RESOURCE_ID` and `hdb-rent-download -o data/raw/median_rent_hdb.csv`, or use [`tests/fixtures/median_rent_sample.csv`](tests/fixtures/median_rent_sample.csv). Override staleness in the app with `SINGAPORE_EDA_RENT_TTL_HOURS`. Tranche merge and other CLI details: `hdb-download --help`, `hdb-resale-merge --help`.
47+
## HDB Forecaster V1 (Postgres + Streamlit)
48+
49+
This repository now includes a dedicated V1 regression forecaster path with:
50+
- **Leakage-safe temporal split** (train/validation/test ordered by transaction month),
51+
- **LightGBM + XGBoost candidate training** with automatic winner selection by validation holdout performance,
52+
- **SHAP local explainability** for top prediction drivers,
53+
- **Prediction interval** from split-conformal absolute-residual calibration (default nominal coverage 0.80),
54+
- **Optional Postgres curated table write** for a production-style feature store.
55+
- **Empirical lease time-decay feature** fitted from historical data (captures
56+
age/lease effects without forcing literal zero-value end state).
57+
- **MLflow experiment tracking** (params, metrics, artifacts) via config.
58+
- **Deepchecks data integrity validation** hook via config.
59+
- **Feedback loop capture** from Streamlit predictions to local feedback store.
60+
- **Optional location/accessibility features** when available in enriched data
61+
(`planning_area`, `region_ocr`, `mrt_station_count`, `nearest_mrt_km_proxy`).
62+
- **Leakage-safe lag market features** (`lag_town_median_price`, segment lag medians,
63+
and prior transaction-count liquidity proxies).
64+
65+
### Build artifacts
66+
67+
Run either:
68+
69+
```bash
70+
python scripts/build_forecaster_v1.py --input data/raw/hdb_resale_2017_onwards.csv
71+
```
72+
73+
or:
74+
75+
```bash
76+
make forecaster-v1
77+
```
78+
79+
Artifacts are written to `models/forecaster_v1/`:
80+
- `model.joblib` (model + feature columns + guardrail bounds),
81+
- `metadata.json` (metrics, split windows, provenance, feature contract).
82+
- `train_config.json` (full reproducible training config snapshot).
83+
- `model_card.md` (run-level model summary, benchmark, and intended-use notes).
84+
85+
### Configuration management
86+
87+
All forecaster hyperparameters and tracking toggles are version-controlled in:
88+
89+
- `configs/forecaster_v1.yaml`
90+
91+
This includes:
92+
- train split and seed,
93+
- candidate model list and model-specific hyperparameters,
94+
- MLflow enablement and experiment name,
95+
- Deepchecks output settings,
96+
- feedback storage path and governed feedback materialization paths.
97+
- MLflow tracking backend now defaults to local SQLite (`sqlite:///mlflow.db`) to avoid deprecated filesystem tracking store warnings.
98+
99+
### One-command ML end-to-end runners
100+
101+
- **macOS / Linux:** `./run-all-machinelearning.sh`
102+
- **Windows:** `run-all-machinelearning.cmd`
103+
- Refresh official MRT references: `make fetch-mrt-reference`
104+
- Fetch official MRT-to-CBD travel times (OneMap auth required): `make fetch-mrt-travel-times`
105+
- Local dev: put `ONEMAP_API_EMAIL` and `ONEMAP_API_PASSWORD` in repo-root `.env` (gitignored),
106+
or export them in your shell.
107+
108+
What these scripts do:
109+
- prepare `.venv` and install dependencies,
110+
- fetch resale/rent data when missing (with fixture fallback),
111+
- run lint + tests,
112+
- build forecaster artifacts,
113+
- run inference smoke-check,
114+
- run vulnerability audit (`pip-audit` on `requirements.txt`),
115+
- launch Streamlit unless `ML_OPEN_STREAMLIT=0`.
116+
- Optional hard gate: set `ML_ENFORCE_NEAR_TERM_GATE=1` to fail the run when near-term promotion gate does not pass.
117+
118+
### Optional Postgres load
119+
120+
To write curated training rows to Postgres before model training:
121+
122+
```bash
123+
python scripts/build_forecaster_v1.py \
124+
--input data/raw/hdb_resale_2017_onwards.csv \
125+
--postgres-dsn "postgresql://user:password@localhost:5432/hdb"
126+
```
127+
128+
### Streamlit usage
129+
130+
After artifacts exist, start the app and use the **Forecaster V1** tab:
131+
132+
```bash
133+
streamlit run streamlit_app.py
134+
```
135+
136+
The tab returns:
137+
- point estimate,
138+
- p10-p90 uncertainty interval,
139+
- top SHAP contributors,
140+
- range warnings when inputs fall outside training bounds.
141+
- constrained dropdowns for `storey_range` (no free-text entry),
142+
- correlated input guardrails for `flat_type` + `flat_model` + floor-area range,
143+
- floor-area input in either `sqm` or `sqft` (auto-converted to model input),
144+
- lease guardrails (commence year cannot exceed transaction year; optional auto 99-year remaining lease),
145+
- `Resale` / `BTO` profile mode with BTO naming + lease prefill when BTO reference rows are available,
146+
- graph-analytics context (town trend + percentile position),
147+
- optional market-anchor blend toward recent comparable medians for stability.
148+
- lease time-decay coefficient display for transparency of leasehold adjustment.
149+
- nearest comparable-transactions panel with similarity distance and median/IQR context.
150+
- reliability score driven by comparables density, interval width, OOD warnings, and segment RMSE.
151+
- segment error slices from `metadata.json` (town and flat-type) for transparency.
152+
- human-readable segment benchmark tables with plain-language guidance (`lower error is better`, `higher sample size is better`).
153+
- location/connectivity premium context (planning area, region tier, MRT proxy, and CBD access heuristic).
154+
- optional scenario overlay controls for school-zone, shopping-hub, and fast-CBD-access premium assumptions.
155+
- new **Housing economics details** tab for end-to-end ownership estimation:
156+
- MOP timeline and estimated sale timing checks
157+
- grants checklist, estimated return-to-government, and levy
158+
- HDB/bank loan cashflow schedule with CPF vs cash split
159+
- COV/premium, renovation, taxes/fees, and itemized cost ledger
160+
- profit waterfall and breakdown tables
161+
- bank package modelling for common `2-year fixed -> SORA + spread` path
162+
- repricing/refinancing simulation with admin/legal/valuation/clawback and lock-in penalty impacts
163+
164+
### Housing economics details (estimator)
165+
166+
The app now includes an estimator-focused page for BTO/resale/EC/private planning using policy defaults in:
167+
168+
- `configs/housing_finance_v1.yaml`
169+
170+
Core implementation lives in:
171+
172+
- `src/singapore_eda/housing_finance/models.py`
173+
- `src/singapore_eda/housing_finance/policy_defaults.py`
174+
- `src/singapore_eda/housing_finance/calculators.py`
175+
- `src/singapore_eda/housing_finance/formatters.py`
176+
177+
Run and verify:
178+
179+
```bash
180+
streamlit run streamlit_app.py
181+
pytest tests/test_housing_finance.py
182+
```
183+
184+
For detailed formulas, assumptions, and interpretation guidance, see:
185+
186+
- `docs/housing_details_v1_runbook.md`
187+
188+
### Verification checklist
189+
190+
```bash
191+
make test
192+
make ruff
193+
python -m pip_audit -r requirements.txt
194+
python scripts/run_forecaster_near_term_eval.py --input data/raw/hdb_resale_2017_onwards.csv
195+
python scripts/prepare_feedback_dataset.py
196+
```
197+
198+
`pip-audit` is required before release handoff; record critical/high findings and remediation.
199+
200+
### Verified end-to-end run (latest)
201+
202+
Validated with:
203+
204+
```bash
205+
ML_OPEN_STREAMLIT=0 ML_RUN_DEEPCHECKS=1 RUN_ALL_SKIP_INSTALL=1 ./run-all-machinelearning.sh
206+
```
207+
208+
Observed status:
209+
- lint + tests passed (`48 passed`)
210+
- forecaster artifacts built
211+
- inference smoke-check passed
212+
- Deepchecks step executed (environment warning may be emitted by upstream deepchecks)
213+
- vulnerability scan passes after upgrading to a secure `mlflow` line
214+
215+
Security note:
216+
- `mlflow` baseline is pinned to `>=3.11.1,<4` to clear known CVEs reported against the previous `2.22.4` line.
217+
- Keep `python -m pip_audit -r requirements.txt` in release checks to catch newly disclosed vulnerabilities.
218+
219+
### Reproducibility and recovery
220+
221+
- Repro check: `make forecaster-v1-verify`
222+
- Deepchecks (on demand): `make forecaster-v1-deepchecks`
223+
- Runbook: [`docs/forecaster_v1_runbook.md`](docs/forecaster_v1_runbook.md)
224+
- Decision log (what we chose and why): [`docs/forecaster_v1_decisions.md`](docs/forecaster_v1_decisions.md)
225+
- Model benchmark evidence (LightGBM vs XGBoost): [`docs/forecaster_v1_model_benchmark.md`](docs/forecaster_v1_model_benchmark.md)
226+
- Near-term rolling backtest / calibration / ablation artifacts: `reports/forecaster_v1/near_term_eval.{json,md}`
227+
- Near-term eval now enforces a minimum rolling-window count and emits a promotion gate (`coverage_pass`, `width_pass`, `pass`) in JSON.
228+
- MRT references sourced from official data.gov.sg APIs via `scripts/fetch_mrt_reference.py`:
229+
- URA MP14 MRT Station Symbol (`d_649357d5cb04ddbef9166dfcf1fa8d21`)
230+
- URA Master Plan 2003 MRT Name (`d_dbc192abee39f51efecc0adbe9f1a75d`)
231+
- Official travel-time file via `scripts/fetch_mrt_travel_times.py`:
232+
- output: `data/reference/mrt_travel_time_to_cbd.csv`
233+
- credentials: set `ONEMAP_API_EMAIL` and `ONEMAP_API_PASSWORD`
234+
- source API: OneMap routing (`routeType=pt`, `mode=TRANSIT`)
235+
236+
**Geo (Graph tab, planning-area edges):** `pip install -e ".[geo]"` and run `make fetch-geo` or `scripts/fetch_reference_geo.py` to pull the official [MP2025 planning-area boundary (No Sea)](https://data.gov.sg/datasets/d_2cc750190544007400b2cfd5d7f53209/view) GeoJSON (old static `geo.data.gov.sg` MP14 links are no longer served). **Rent / gross yield:** set `HDB_MEDIAN_RENT_RESOURCE_ID` and `hdb-rent-download -o data/raw/median_rent_hdb.csv`, or use [`tests/fixtures/median_rent_sample.csv`](tests/fixtures/median_rent_sample.csv). **BTO historical + future supply references:** run `hdb-bto-download -o data/reference` (writes launch price range, completion status, property information, plus `hdb_bto_reference.csv`). Override staleness in the app with `SINGAPORE_EDA_RENT_TTL_HOURS`. Tranche merge and other CLI details: `hdb-download --help`, `hdb-resale-merge --help`.
237+
238+
Forecaster training/inference now consumes BTO-derived town/year context features when these files are present:
239+
- `bto_launch_count_town_3y`
240+
- `bto_avg_price_range_mid_town_3y`
241+
- `bto_under_construction_units_town`
242+
- `bto_completed_units_town`
48243

49244
## Data
50245

51246
**Primary resale table:** [Resale flat prices (registration) from Jan-2017 onwards](https://data.gov.sg/datasets/d_8b84c4ee58e3cfc0ece0d773c8ca6abc/view) (HDB, [Open Data Licence](https://data.gov.sg/open-data-licence)).
52247

53-
**Reference tables (in-repo):** [`data/reference/`](data/reference/) — town → planning area / OCR, mature vs young labels, MRT access proxy, optional EIP block stub, future MRT example rows.
248+
**Reference tables (in-repo):** [`data/reference/`](data/reference/) — town → planning area / OCR, mature vs young labels, MRT access proxy, optional EIP block stub, and official MRT reference exports fetched from government APIs.
54249

55250
## Live demo (two URLs)
56251

@@ -71,6 +266,9 @@ Then: `make test` · `hdb-download -o data/raw/hdb_resale_2017_onwards.csv` (or
71266
- Main file path: `streamlit_app.py`
72267
- (Optional) Python version: 3.12
73268
6. Add secrets in Streamlit Cloud (**App settings → Secrets**) using [`.streamlit/secrets.toml.example`](.streamlit/secrets.toml.example).
269+
- For OneMap travel-time fetches, include:
270+
- `ONEMAP_API_EMAIL`
271+
- `ONEMAP_API_PASSWORD`
74272
7. Deploy and copy your Streamlit URL (typically `https://<app-name>.streamlit.app`).
75273
8. Replace the placeholders below and commit:
76274

@@ -101,6 +299,17 @@ SINGAPORE_EDA_RENT_BOOTSTRAP_MAX_ROWS = "200000"
101299
SINGAPORE_EDA_AUTO_FETCH_GEO_ON_TINY = "1"
102300
```
103301

302+
Optional OneMap secrets for official travel-time refresh jobs:
303+
304+
```toml
305+
ONEMAP_API_EMAIL = "your_onemap_email"
306+
ONEMAP_API_PASSWORD = "your_onemap_password"
307+
```
308+
309+
Local-only development alternative:
310+
- put the same values into repo-root `.env` (already gitignored),
311+
- run `make fetch-mrt-travel-times`.
312+
104313
## Production: data.gov.sg limits, cache, health API, ops UI
105314

106315
Official **per-10-second** limits (see [`API rate limits`](https://guide.data.gov.sg/developer-guide/api-overview/api-rate-limits)) depend on whether you use an API key and its **tier**:
@@ -169,7 +378,7 @@ This section is intentionally direct: the project is useful for **learning and e
169378
### Weaknesses and cons (what to be careful about)
170379

171380
- **Exploratory, not a full research pipeline:** Methods are **standard EDA and simple models** (e.g. OLS-style stats, k-means, ETS). They are **not** a complete econometric or ML validation programme; assumptions are not fully audited in this repo.
172-
- **Reference data and proxies:** In-repo tables (town→planning area, MRT access, maturity labels) and optional files (e.g. EIP **stub**, future MRT **examples**) are **simplifications** or **placeholders** where noted in code or docs. They can be **wrong**, **incomplete**, or **out of date** relative to ground truth.
381+
- **Reference data and proxies:** In-repo tables (town→planning area, MRT access, maturity labels) and optional files (e.g. EIP **stub**) are simplifications where noted. Even when sourced from official datasets/APIs, joins and transformations can still be wrong, incomplete, or stale relative to current ground truth.
173382
- **Optional or stubbed features:** Some modules are explicitly limited (e.g. **sun exposure** is largely placeholder unless enabled and configured; see package layout). Treat their outputs as **illustrative**.
174383
- **Sample and default limits:** Default downloads cap row counts; partial samples **do not** represent the full market. Stale or cached data may not reflect the latest government release.
175384
- **Upstream and maintenance risk:** API formats, tranche boundaries, and CKAN behaviour **change**; the pipeline may need maintenance after upstream changes.

configs/forecaster_v1.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
train:
2+
train_frac: 0.70
3+
val_frac: 0.15
4+
random_state: 42
5+
model_dir: models/forecaster_v1
6+
candidate_models:
7+
- xgboost
8+
- lightgbm
9+
use_lag_features: false
10+
11+
models:
12+
xgboost:
13+
n_estimators: 500
14+
learning_rate: 0.05
15+
max_depth: 6
16+
subsample: 0.85
17+
colsample_bytree: 0.85
18+
reg_alpha: 0.0
19+
reg_lambda: 1.0
20+
objective: reg:squarederror
21+
lightgbm:
22+
n_estimators: 500
23+
learning_rate: 0.05
24+
max_depth: -1
25+
num_leaves: 63
26+
subsample: 0.85
27+
colsample_bytree: 0.85
28+
verbose: -1
29+
30+
tracking:
31+
mlflow_enabled: true
32+
mlflow_tracking_uri: "sqlite:///mlflow.db"
33+
mlflow_experiment: singapore-eda-forecaster-v1
34+
35+
validation:
36+
deepchecks_enabled: false
37+
deepchecks_output_dir: reports/deepchecks/forecaster_v1
38+
interval_nominal_coverage: 0.80
39+
rolling_min_windows: 3
40+
coverage_tolerance: 0.10
41+
max_mean_interval_width: 220000.0
42+
43+
feedback:
44+
store_path: data/feedback/forecaster_feedback.csv
45+
validated_path: data/feedback/forecaster_feedback_validated.csv
46+
retraining_path: data/feedback/forecaster_feedback_retraining.csv
47+
retention_days: 365
48+
min_comment_redact_digits: 6

0 commit comments

Comments
 (0)