Update forecast.py
+280
-1
@@ -1,2 +1,281 @@
|
|||||||
<-- [[Home]]
|
<-- [[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.*
|
||||||
Reference in New Issue
Block a user