From b424bede20e8f367f02f346e96d17a06c657620a Mon Sep 17 00:00:00 2001 From: willwheeler Date: Thu, 18 Jun 2026 21:22:27 +0000 Subject: [PATCH] Update forecast.py --- forecast.py.md | 281 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 1 deletion(-) diff --git a/forecast.py.md b/forecast.py.md index b421104..42aed62 100644 --- a/forecast.py.md +++ b/forecast.py.md @@ -1,2 +1,281 @@ <-- [[Home]] -will finish tomorrow + +# Forecast — Python pipeline + +Train short-horizon BACnet trend models from sample JS files, evaluate accuracy on a held-out tail, write chart-ready JSON, and optionally experiment with extra inputs before changing production settings. + +**Consumer:** [[test.php]] loads `forecast_output.json` for dashed forecast lines on the power and phase charts. + +[[Home]] + +--- + +## On this page + +- [Pipeline](#pipeline) +- [Prerequisites](#prerequisites) +- [forecast.py](#forecastpy) +- [forecast_output.json](#forecast_outputjson) +- [forecast_comp.py](#forecast_comppy) +- [Workflow cheat sheet](#workflow-cheat-sheet) +- [Troubleshooting](#troubleshooting) + +--- + +## Pipeline + + js/veris-sample.js ──┐ + js/rtu-sample.js ──┼──► forecast.py ──► forecast_output.json ──► test.php (Load forecast) + │ + └──► forecast_comp.py ──► console MAE only (experiments) + +| Step | Tool | Output | +|------|------|--------| +| 1 | Sample JS files | `{ PointName: [{x,y},...], ... }` | +| 2 | **forecast.py** | `forecast_output.json` + MAE per point in console | +| 3 | **test.php** | Dashed overlay on Line P + Phase compare | +| *(optional)* | **forecast_comp.py** | Compare single-point vs multi-point MAE (no JSON) | + +Sample files use the same `{x, y}` shape as live **`getData`** from `index.php`, so the pipeline matches production data once you point the scripts at live exports. + +--- + +## Prerequisites + + pip install pandas scikit-learn skforecast + +| Package | Used for | +|---------|----------| +| **pandas** | Time series, resampling, align | +| **scikit-learn** | `Ridge` regressor | +| **skforecast** | `ForecasterRecursive` (lag-based forecasting) | + +Run all commands from the **project root** (folder containing `forecast.py`). + +--- + +## forecast.py + +Primary script. One model **per BACnet point**, backtest on the **last 20%** of history, then predict **`FUTURE_STEPS`** ahead and write JSON. + +### Run + + python forecast.py + +No CLI arguments. Edit constants at the top of the file. + +### Settings + +| Constant | Default | Meaning | +|----------|---------|---------| +| `FUTURE_STEPS` | `100` | How many future timestamps to predict | +| `LAGS` | `25` | How many past readings the model sees each step | +| `JOBS` | see below | Which file + point names to process | + +Default **`JOBS`**: + + JOBS = [ + ("js/veris-sample.js", ["P", "Ia", "Ib", "Ic"]), + ("js/rtu-sample.js", ["ServRmTmp"]), + ] + +Add a `(js_file, [names…])` tuple to forecast more points. Re-run to refresh JSON. + +### What it does (per point) + +1. **Load** — Parse `var xxxData = { ... };` from the `.js` file (regex + JSON). +2. **Series** — Build pandas `Series`: `x` → datetime (ms), `y` → float. +3. **Resample** — Median gap between samples → even grid; interpolate missing steps. +4. **Backtest** — **First 80%** train, **last 20%** predict → print **MAE** (mean absolute error). +5. **Forecast** — Retrain on **100%** of history → predict `FUTURE_STEPS` forward. +6. **Write** — Append to `forecast_output.json` under the point name. + +### Model + +- **ForecasterRecursive** (skforecast) — each step uses recent lags; predictions feed forward. +- **Ridge** (sklearn) — linear model, fast and stable on smooth BAS trends. +- **MAE** — average absolute miss on the hidden 20%; same units as the point (kW, °F, A, etc.). + +### Console example + + P — 432 readings + MAE (last 20%): 11.17 + + Ia — 432 readings + MAE (last 20%): 0.42 + ... + + Saved forecasts for: P, Ia, Ib, Ic, ServRmTmp + +### Output file + +Writes **`forecast_output.json`** in the project root (overwrites each run). + +--- + +## forecast_output.json + +Generated by `forecast.py`. Static file the browser fetches — not generated by PHP. + +### Schema + +Top-level keys = **point names**. Each value: + +| Field | Type | Description | +|-------|------|-------------| +| `mae` | number | Backtest error on last 20% | +| `future` | array | Predicted readings after the last historical sample | + +Each `future` item: + +| Field | Type | Description | +|-------|------|-------------| +| `x` | integer | Unix time in **milliseconds** (same as sample JS / `getData`) | +| `y` | number | Predicted value (2 decimal places) | + +### Example + + { + "P": { + "mae": 11.17, + "future": [ + { "x": 1781713080000, "y": 19.05 }, + { "x": 1781713200000, "y": 19.04 } + ] + } + } + +### Default keys (from current `JOBS`) + +| Key | Source file | Used on dashboard | +|-----|-------------|-------------------| +| `P` | veris-sample.js | **Line P** chart (+ forecast) | +| `Ia`, `Ib`, `Ic` | veris-sample.js | **Phase compare** (+ forecast) | +| `ServRmTmp` | rtu-sample.js | In JSON; RTU spark overlay not wired yet | + +Charts only overlay forecast when: + +1. `forecast_output.json` has that key, **and** +2. Loaded chart data includes the same point name (live or sample). + +See [[test.php#forecast-overlay]] for how `basForecastSeries()` attaches the dashed line. + +### MAE in JSON + +Stored for each point but **not shown in the UI yet**. Useful when comparing runs after changing `LAGS` or `FUTURE_STEPS`. + +--- + +## forecast_comp.py + +**Research / experiment script.** Answers: *“If I add another BACnet point as input, does the target predict better?”* + +Does **not** write JSON. Does **not** change charts. Run when tuning **before** updating `forecast.py` or exog lists. + +### Run + + python forecast_comp.py + +### Settings (top of file) + +| Constant | Example | Meaning | +|----------|---------|---------| +| `JS_FILE` | `"js/rtu-sample.js"` | Sample data file | +| `TARGET` | `"RaTmp"` | Point you want to predict | +| `WITH_POINTS` | `["FanCmdOvr"]` | Extra columns for the combined model | +| `LAGS` | `26` | Lag window (can differ from `forecast.py`) | + +All points must exist in the same JS file. Rows are **time-aligned**; timestamps with any missing value are dropped. + +### Two models (same 80/20 split) + +| Model | Inputs | +|-------|--------| +| **A — alone** | `TARGET` history only (like one point in `forecast.py`) | +| **B — combined** | `TARGET` + every name in `WITH_POINTS` at each timestamp (exogenous / exog) | + +### Example output + + File: js/rtu-sample.js + Target: RaTmp + Also using: FanCmdOvr + Aligned readings: 412 + + Backtest on last 20% of sample data: + RaTmp only: MAE = 1.24 + RaTmp + [FanCmdOvr]: MAE = 0.89 + + Extra points helped — about 0.35 better on average. + +| Verdict | Condition | +|---------|-----------| +| Extras helped | `mae_alone - mae_combined > 0.05` | +| Extras hurt | difference < -0.05 | +| About the same | otherwise | + +### Quality warnings (read before trusting MAE) + +The script prints warnings when results can mislead: + +- **Flat target** in the test slice → MAE ≈ 0 but model learned nothing useful +- **Exog identical** to target → combined MAE ~0 is not a breakthrough +- **Very high correlation** between exog and target +- **Lookup table** — each exog value maps to one target value +- **Flat exog** in the test slice + +If combined MAE < 0.01 and warnings appeared, treat the win as suspicious. + +### When to use which script + +| Goal | Script | +|------|--------| +| Ship forecasts to the dashboard | **forecast.py** | +| Ask “should FanSts / mode / setpoint help predict room temp?” | **forecast_comp.py** | +| Tune lags or point combos before changing `JOBS` | **forecast_comp.py** | + +--- + +## Workflow cheat sheet + +**Offline demo (matches sample + forecast):** + + pip install pandas scikit-learn skforecast + python forecast.py + open test.php?sample=1&forecast=1 + +**Experiment with extra RTU points:** + + # edit TARGET / WITH_POINTS in forecast_comp.py + python forecast_comp.py + # if helpful, consider adding logic to forecast.py later + +**Refresh after sample JS changes:** + + python forecast.py + # in browser: Load forecast (or hard refresh with ?forecast=1) + +--- + +## Troubleshooting + +| Problem | Likely cause | Fix | +|---------|--------------|-----| +| `ModuleNotFoundError: skforecast` | Deps not installed | `pip install pandas scikit-learn skforecast` | +| Point missing from JSON | Not in `JOBS` | Add name to `JOBS`, re-run | +| test.php: “No forecast_output.json” | File not generated | Run `forecast.py` from project root | +| Forecast loads, no dashed lines | Point name mismatch | JSON key must match chart series (e.g. `P`, not `Demand` unless that’s the key) | +| MAE = 0.00 in comp script | Flat or duplicate data | Read warnings in console | +| `TARGET not found in JS_FILE` | Typo or wrong file | Check exact BACnet name in sample JS | +| Predictions look flat | Short history or very stable signal | Normal for Ridge on flat power; try more data or different point | + +--- + +## Related + +- [[Home]] — repo overview and links +- [[test.php]] — dashboard, live `getData`, forecast overlay UI +- **index.php** — live trend API (future: feed export into `forecast.py`) + +--- + +*Forecast wiki — Python training, JSON output, and experiment scripts.* \ No newline at end of file