Leakage-safe next-hour PM2.5 forecasting with temporal backtesting and conformal uncertainty intervals.
Dự án nhận history PM2.5 theo từng trạm và dự báo giá trị của giờ kế tiếp. Mục tiêu chính là minh họa một quy trình time-series có thể kiểm tra được:
- mọi timestamp được đưa về UTC;
- dữ liệu được regularize về lưới một giờ trước khi tạo feature;
- lag được tra theo clock time thay vì
shift()theo vị trí dòng; - feature chỉ dùng dữ liệu đã có tại forecast origin;
- train, calibration và final test được chia theo thời gian;
- model ML luôn được so với Persistence và Seasonal Naive 24h;
- calibration set riêng tạo conformal prediction interval;
- history runtime không đủ tốt sẽ dùng Persistence.
Đây là prototype dùng một nguồn CSV lịch sử/sample. Kết quả bên dưới không phải tuyên bố về chất lượng không khí toàn TP.HCM.
| Đã triển khai | Chưa nằm trong V1 / roadmap |
|---|---|
| Đọc CSV và kiểm tra schema | Kết nối station API thật |
| Chuẩn hóa UTC và regularize theo giờ | Incremental storage và backfill |
| Lag theo timestamp, rolling causal, time features | Xử lý late-arriving observation theo lịch chạy |
| Temporal train/calibration/test split | Tự động chạy theo lịch |
| Expanding-window backtest | Theo dõi chất lượng dữ liệu theo từng đợt |
| Ridge, HistGradientBoosting, Persistence, Seasonal Naive 24h | Mở rộng benchmark citywide với dữ liệu lớn |
| Split conformal interval | Hiệu chuẩn lại trên các giai đoạn dài hơn |
| FastAPI và Streamlit demo | |
| CI với Ruff, pytest, pip check, smoke train và Docker smoke test |
Bảng này được lấy từ sample data để kiểm tra hệ thống. Không dùng nó để kết luận model ML tốt hơn trên dữ liệu HCMC thực tế.
| Model / baseline | MAE | RMSE | Vai trò |
|---|---|---|---|
| Persistence | 0.250 | 0.292 | Baseline bắt buộc |
| Seasonal Naive 24h | 5.948 | 5.952 | Baseline theo cùng giờ ngày trước |
| Ridge | 0.316 | 0.317 | Ứng viên ML hiện chưa thắng Persistence trên final test |
Sample hiện có khoảng 6 dòng calibration và 8 dòng final test. Conformal interval được triển khai bằng finite-sample split-conformal; PICP quan sát trên final test là 75% so với mục tiêu 90%, vì vậy đây chỉ là minh họa phương pháp, không phải cam kết calibration đáng tin cậy.
| Uncertainty | Target coverage | PICP trên final test | Mean interval width |
|---|---|---|---|
| Split conformal | 90% | 75% | 0.655 µg/m³ |
Mốc timestamp là thời điểm quan trắc. available_at, nếu có, là thời điểm quan trắc được phát hành; feature chỉ được dùng khi available_at <= forecast_origin.
| Field | Available time | Vai trò |
|---|---|---|
PM2.5(t) |
t |
Feature hiện tại |
PM2.5(t+1) |
Tương lai | Target, không được dùng làm feature |
temperature(t) |
t |
Feature nếu có |
humidity(t) |
t |
Feature nếu có |
weather(t+1) |
Tương lai | Bị cấm, trừ khi có forecast source độc lập |
Lag PM2.5 tại t-1h, t-2h... được lookup bằng khóa (station_id, timestamp). Vì vậy một dòng trước đó nhưng cách 2 giờ không bị nhận nhầm là lag 1 giờ. Rolling statistics dùng closed="left", chỉ nhìn về quá khứ trước t.
| Trường hợp | Chính sách |
|---|---|
Duplicate (station_id, timestamp) |
Reject ở loader/runtime; không tự chọn một dòng |
| Missing hour | Insert dòng rỗng khi regularize; giữ NaN để pipeline impute thống nhất |
| Late observation | Không dùng nếu available_at sau forecast origin |
| Out-of-order event | Sort ổn định theo trạm và timestamp trước khi audit/regularize |
| Missing exogenous | Cảnh báo; dùng Persistence để tránh lệch phân phối khi dự báo |
| Station outage hoặc gap lớn | Cảnh báo; dùng Persistence khi vượt allowed_gap_hours |
| History dưới 25 giờ hoặc PM2.5 hiện tại thiếu | Không tạo dự báo, trả lỗi input |
Đây là flowchart duy nhất chi phối source, cấu hình và báo cáo của dự án:
flowchart TD
A[Hourly station CSV] --> B[Load + validate schema and timestamps]
B --> C[UTC normalization + hourly regularization]
C --> D[Clock-time lags + causal rolling + temporal features]
D --> E[Temporal split]
E --> F[Train window]
E --> G[Calibration window]
E --> H[Final test window]
F --> I[Expanding-window backtest]
I --> J[Ridge / HistGradientBoosting]
J --> K[Compare against Persistence]
G --> L[Split conformal residual quantile]
K --> M[Model selection criteria]
L --> M
H --> N[Final evaluation]
M --> N
N --> O[Forecast + uncertainty interval]
O --> P[FastAPI / Streamlit]
Quy trình thực thi:
src/data/loader.pyđọc CSV, kiểm tra cột bắt buộc, ép kiểu số và chuẩn hóatimestamp/available_atvề UTC.src/data/quality.pyaudit duplicate, missing, gap, miền giá trị và availability.src/data/regularization.pychèn các mốc giờ còn thiếu theo từng trạm.src/features/builder.pytạo lag theo clock time, rolling causal, trend, biến thời gian và exogenous features.src/validation/split.pytách train → calibration → final test; target timestamp cũng phải nằm trước biên split.src/validation/backtest.pychạy expanding-window trên các candidate chỉ trong train window.src/forecasting/selection.pychọnbest_cv_model, sau đó quyết địnhforecast_strategylà model đó hoặcpersistence.src/calibration/conformal.pydùng calibration window riêng để tạo quantile và interval.src/evaluationbáo cáo baseline, MAE/RMSE/MASE/skill score, PICP và slice theo trạm.src/inference/input_validation.pykiểm tra history runtime; gap lớn có thể chuyển chiến lược sang Persistence.src/inference/predictor.pydựng feature giống train và trả forecast, mức phân tích, interval và chất lượng input.
hcmc-pm25-forecasting/
├── app/
│ ├── api.py
│ └── dashboard.py
├── configs/
│ └── config.yaml
├── data/
│ └── sample/
│ └── air_quality_sample.csv
├── src/
│ ├── data/
│ │ ├── loader.py
│ │ ├── schema.py
│ │ ├── quality.py
│ │ └── regularization.py
│ ├── features/
│ │ ├── builder.py
│ │ ├── exogenous.py
│ │ ├── lag.py
│ │ ├── rolling.py
│ │ └── temporal.py
│ ├── forecasting/
│ │ ├── baselines.py
│ │ ├── models.py
│ │ ├── selection.py
│ │ └── trainer.py
│ ├── validation/
│ │ ├── split.py
│ │ └── backtest.py
│ ├── calibration/
│ │ └── conformal.py
│ ├── evaluation/
│ │ ├── metrics.py
│ │ ├── slices.py
│ │ └── station_metrics.py
│ ├── artifacts/
│ │ ├── loader.py
│ │ └── writer.py
│ ├── inference/
│ │ ├── input_validation.py
│ │ └── predictor.py
│ ├── pipeline.py
│ ├── config.py
│ ├── report.py
│ └── utils.py
├── artifacts/
│ ├── model.joblib
│ ├── metadata.json
│ ├── evaluation.json
│ └── feature_schema.json
├── notebooks/
│ ├── 01_experiment.ipynb
│ └── 02_colab_reproducibility.ipynb
├── tests/
├── docs/
│ ├── FORECASTING_PROTOCOL.md
│ └── MODEL_CARD.md
├── Dockerfile
├── requirements.txt
├── requirements-dev.txt
├── requirements-colab.txt
└── README.md
artifacts/ là output cục bộ sau khi train và không cần commit. V1 chỉ giữ một
bộ file kết quả hiện tại để chạy demo.
Yêu cầu Python 3.10 trở lên.
python -m venv .venv
# Linux/macOS
source .venv/bin/activate
# Windows PowerShell
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txtChạy các lệnh tương đương CI ở local:
python -m pip check
python -m ruff check src app tests
python -m pytest
python -m src.pipeline train --config configs/config.yaml --no-artifactsGitHub Actions chạy cùng các bước trên với Python 3.10 và 3.11. --no-artifacts bảo đảm CI không ghi model vào repository.
Job Docker của CI chạy train mẫu, build Dockerfile, mount artifacts/, khởi động API
và chờ /health trả HTTP 200.
python -m src.pipeline train --config configs/config.yamlLệnh tạo đúng bốn file hiện tại:
artifacts/model.joblib: pipeline preprocessing + model ứng viên tốt nhất theo CV;artifacts/metadata.json:best_cv_model,forecast_strategy, feature columns, interval và provenance;artifacts/evaluation.json: audit, split summary, backtest, calibration, baseline và final test;artifacts/feature_schema.json: thứ tự feature dùng khi inference.
Tạo báo cáo Markdown từ evaluation:
python -m src.report --input artifacts/evaluation.json --output reports/evaluation_summary.mdHuấn luyện trước, sau đó:
uvicorn app.api:app --reloadGET /health: kiểm tra model hiện tại và strategy.POST /predict: nhận tối thiểu 25 quan trắc của mộtstation_id.POST /v1/predict/raw: alias của/predictcho cùng payload raw.GET /v1/stations/{station_id}/forecast: lấy history gần nhất từ CSV sample.
Ví dụ cấu trúc request (chỉ minh họa schema; request thực tế cần ít nhất 25 dòng):
{
"observations": [
{"timestamp": "2024-01-01T00:00:00", "station_id": "A", "PM2.5": 20.0}
]
}Request thực tế cần ít nhất 25 dòng theo giới hạn API. Response có predicted_pm25, forecast_strategy, interval và data_quality.
Khởi động dashboard để xem dự báo, lịch sử quan trắc và kiểm tra chất lượng dữ liệu:
streamlit run app/dashboard.pyDashboard đọc PM25_API_URL để gọi FastAPI, mặc định là
http://localhost:8000. Nếu API không trả lời, dashboard thử dự báo trực tiếp
bằng Predictor.from_artifact() từ thư mục artifacts/.
Thanh bên cho phép chọn CSV mẫu hoặc tải lên CSV của trạm, chọn một trạm và chọn cửa sổ history 25, 48, 72 hoặc 168 giờ. Giao diện có bốn tab:
-
Forecast: hiển thị origin
$t$ , target$t+1$ , PM2.5 hiện tại, dự báo, khoảng conformal, chiến lượcridge,hist_gradient_boostinghoặcpersistence, cảnh báo chất lượng dữ liệu và biểu đồ. - History & Data Quality: hiển thị các quan trắc gần nhất, độ đầy đủ, gap lớn nhất, chính sách regularization, availability và danh sách feature.
- Compare Stations: so sánh các trạm có ít nhất 25 dòng history cùng dự báo, khoảng conformal và trạng thái dữ liệu.
- Model & Limitations: hiển thị benchmark mẫu, PICP/MPIW mẫu và các giới hạn của prototype.
Khi API chạy ở cổng khác, ví dụ Docker dùng cổng ngoài 8001, đặt biến môi
trường trước khi khởi động dashboard:
$env:PM25_API_URL = "http://localhost:8001"
streamlit run app/dashboard.pyLưu ý phạm vi: Các mức
Thấp/Trung bình/Caolà nhóm phân tích nội bộ thử nghiệm, không phải chỉ số AQI chính thức hay khuyến nghị y tế.
Dockerfile chỉ đóng gói API, không huấn luyện model trong image. Cần tạo bộ file
artifacts/ trước khi chạy container vì thư mục này được loại khỏi build context.
PowerShell trên Windows:
python -m src.pipeline train --config configs/config.yaml
docker build -t hcmc-pm25-forecasting:local .
docker run --rm --name hcmc-pm25-forecasting -p 8000:8000 `
-v "${PWD}\artifacts:/app/artifacts" `
hcmc-pm25-forecasting:localLinux/macOS:
python -m src.pipeline train --config configs/config.yaml
docker build -t hcmc-pm25-forecasting:local .
docker run --rm --name hcmc-pm25-forecasting -p 8000:8000 \
-v "${PWD}/artifacts:/app/artifacts" \
hcmc-pm25-forecasting:localKiểm tra container bằng http://localhost:8000/health; tài liệu API ở
http://localhost:8000/docs. Nếu chưa train hoặc không mount artifacts/,
health check sẽ trả HTTP 503 vì chưa có model.
Nếu cổng 8000 đang được ứng dụng khác sử dụng, đổi ánh xạ cổng ngoài sang
8001:8000 và truy cập http://localhost:8001/health.
docs/FORECASTING_PROTOCOL.md: công thức baseline, split, backtest và conformal protocol.docs/MODEL_CARD.md: phạm vi, hạn chế và cách diễn giải kết quả mẫu.
MIT.