This file records major implementation decisions for V1, including alternatives considered and rationale.
- Decision: Train/validation/test are split by chronological
month. - Alternatives considered: random split, stratified random split.
- Why: random splits leak future market conditions into training and overstate generalization for time-evolving prices. Temporal splits better approximate real deployment behavior.
- Trade-off: evaluation is usually harsher and can vary across regimes.
- Decision: Start with core columns (town, flat type/model, storey range, area, lease-related fields, month-derived year/month).
- Alternatives considered: include many external features immediately (school ranks, URA overlays, macro rates).
- Why: smaller feature surface reduces integration risk and debugging complexity; enables clear attribution of model lift in V1.
- Trade-off: potential ceiling on predictive performance until richer features are added.
- Decision: Add explicit curated schema (
feature_store.py) and validate non-null constraints before writes. - Alternatives considered: write ad-hoc dataframes directly without schema checks.
- Why: explicit contracts prevent silent schema drift and support reproducible downstream training.
- Trade-off: slightly more upfront boilerplate.
- Decision: Store
source_path,source_sha256, and ingestion timestamp in model metadata. - Alternatives considered: keep only model metrics.
- Why: reproducibility and auditability require traceability to exact input data snapshot.
- Trade-off: metadata payload is larger.
- Decision: Train both model families and select by validation holdout performance (
val_rmse, thenval_mae); keep test metrics for one-time final reporting only. - Alternatives considered: fix to one model family.
- Why: avoids framework bias and bases final choice on observed data performance.
- Trade-off: longer training runtime and extra dependency footprint.
- Selected model family:
xgboost - Observed test performance: lower RMSE/MAE than LightGBM on current dataset snapshot.
- Note: this is not permanent; selection can change with data distribution shifts.
- Recorded evidence:
docs/forecaster_v1_model_benchmark.md
- Decision: train with
log1p(resale_price)target and convert back viaexpm1. - Alternatives considered: train directly on price levels.
- Why: log target improves stability for right-skewed price distributions and often reduces sensitivity to high-price outliers.
- Trade-off: interpretation requires care when mapping back to absolute-dollar errors.
- Decision: use split-conformal absolute-residual calibration on validation holdout for prediction intervals.
- Alternatives considered: conformal prediction, quantile regression models, Bayesian approaches.
- Why: simple, fast, and sufficiently interpretable for V1 while keeping implementation maintainable.
- Trade-off: interval calibration can degrade under distribution shift.
- Decision: split train/validation/test on unique month buckets, never row-count boundaries.
- Alternatives considered: row-count split on sorted rows.
- Why: month-level boundary purity prevents mixed-month leakage between adjacent splits and better reflects forecast deployment cadence.
- Trade-off: split row counts can be less balanced when month volumes vary.
- Decision: keep lag-market features off by default (
train.use_lag_features: false) and warn during inference if model expects lag fields but request omits them. - Alternatives considered: always-on lag features with training-median defaults at inference.
- Why: avoids hidden train-serve skew from synthetic default lag values.
- Trade-off: may reduce raw accuracy ceiling until a deterministic online lag-state service is introduced.
- Decision: keep raw feedback log, plus deterministic materialization into validated and retraining-eligible datasets with retention and redaction.
- Alternatives considered: direct retraining from raw feedback CSV.
- Why: raw user feedback is noisy and may include sensitive free text; governance improves quality and compliance posture.
- Trade-off: added operational step (
scripts/prepare_feedback_dataset.py) before feedback-assisted retraining.
- Decision: produce top local contributors from SHAP values per inference request.
- Alternatives considered: permutation importance only, gain-only feature importance.
- Why: local explanations are more useful for end-user case-level reasoning than global-only importance.
- Trade-off: added dependency/runtime overhead.
- Decision: warn when numeric inputs fall outside observed training bounds.
- Alternatives considered: no warnings, hard rejection.
- Why: soft warnings preserve usability while signaling extrapolation risk.
- Trade-off: warnings are heuristic and not full out-of-distribution detection.
- Decision: show model metadata and decision-support disclaimer in UI.
- Alternatives considered: output prediction only.
- Why: transparency and responsible usage are essential for trust and interview-grade rigor.
- Trade-off: more UI text, but clearer expectations.
- Decision: include
forecaster_v1_runbook.mdand reproducibility smoke check script. - Alternatives considered: rely on informal chat/tribal workflow.
- Why: operational reliability requires explicit procedures for build, verification, and recovery.
- Trade-off: documentation maintenance overhead.
- Decision: use
pip-audit -r requirements.txtas release gate. - Alternatives considered: audit full global environment only.
- Why: repository-scoped dependencies are the deployable surface; global environment includes unrelated packages that can obscure actionable project risk.
- Trade-off: misses vulnerabilities in unrelated local global packages by design.
- Decision: integrate MLflow logging directly in training path.
- Alternatives considered: ZenML pipeline orchestration, no experiment tracker.
- Why: MLflow provides lightweight and fast adoption for parameter/metric/artifact tracking with minimal architecture change.
- Trade-off: orchestration remains script-driven (not full DAG/pipeline management as ZenML would provide).
- Security follow-up:
pip-auditcurrently reports known CVEs on the pinned MLflow line; upgrade is required before production posture.
- Decision: all train splits, candidate models, and model-specific params are maintained in
configs/forecaster_v1.yaml. - Alternatives considered: hardcoded Python constants only.
- Why: version-controlled config is easier to review, compare, and reproduce across runs.
- Trade-off: additional config parsing layer must be kept in sync with code.
- Decision: deepchecks validation is integrated and configurable (
deepchecks_enabled). - Alternatives considered: no formal data validation, custom one-off checks only.
- Why: Deepchecks gives standardized, inspectable data quality diagnostics before training.
- Trade-off: extra dependency/runtime overhead; enabled by toggle to keep default runs fast.
- Decision: add Streamlit feedback submission (rating, optional actual price, comments, payload snapshot) to local CSV.
- Alternatives considered: external feedback tools only, no feedback capture in app.
- Why: closes the loop between predictions and observed user outcomes for future iteration prioritization.
- Trade-off: feedback quality depends on user participation and data hygiene.
- Decision: fetch MRT reference files from official data.gov.sg APIs (
scripts/fetch_mrt_reference.py) and regenerate local reference files. - Alternatives considered: keep static sample CSV rows.
- Why: sample placeholders are not acceptable for production-like analytics; official agency-published data improves traceability and trust.
- Trade-off: dependency on network/API availability and rate-limit behavior.
- Decision: implement
scripts/fetch_mrt_travel_times.pyusing OneMap routing API and credentials from secrets/env/.env. - Alternatives considered: only heuristic travel-time proxy from distance/region.
- Why: official routing durations provide higher-fidelity accessibility features than static distance-only proxies.
- Trade-off: authenticated API dependency, credential management overhead, and variable runtime due to route calls.
- Decision: school-zone, shopping, and CBD-access premiums are exposed as optional UI overlays and clearly labeled as scenario assumptions.
- Alternatives considered: hard-code pseudo-premium coefficients directly into model predictions.
- Why: avoids presenting unvalidated policy/lifestyle premiums as model-ground truth; preserves user flexibility while maintaining methodological transparency.
- Trade-off: users must set assumptions explicitly; overlay outputs are not "fully model-inferred" values.