Table of Contents
<-- Home
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.
On this page
- Pipeline
- Prerequisites
- forecast.py
- forecast_output.json
- forecast_comp.py
- Workflow cheat sheet
- 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)
- Load — Parse
var xxxData = { ... };from the.jsfile (regex + JSON). - Series — Build pandas
Series:x→ datetime (ms),y→ float. - Resample — Median gap between samples → even grid; interpolate missing steps.
- Backtest — First 80% train, last 20% predict → print MAE (mean absolute error).
- Forecast — Retrain on 100% of history → predict
FUTURE_STEPSforward. - Write — Append to
forecast_output.jsonunder 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:
forecast_output.jsonhas that key, and- 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.