commit a61e276ba047520165356fdd708e4b608843a935 Author: willwheeler Date: Wed Jun 17 20:32:58 2026 +0000 Upload files to "/" diff --git a/forecast.py b/forecast.py new file mode 100644 index 0000000..69680c3 --- /dev/null +++ b/forecast.py @@ -0,0 +1,85 @@ +""" +Predict chart points from sample BAS data. + +Run: + python forecast.py + +Output: + forecast_output.json (test.php reads this for dashed forecast lines) +""" + +import json +import re +from pathlib import Path + +import pandas as pd +from sklearn.linear_model import Ridge +from sklearn.metrics import mean_absolute_error +from skforecast.recursive import ForecasterRecursive + +# --- settings you can change --- +FUTURE_STEPS = 100 # how many readings to predict ahead +LAGS = 25 # how many past readings the model looks at each step + +# (sample file, point names) — one model per point +JOBS = [ + ("js/veris-sample.js", ["P", "Ia", "Ib", "Ic"]), + ("js/rtu-sample.js", ["ServRmTmp"]), # RTU room temp (spark chart later) +] + +folder = Path(__file__).parent + + +def load_sample_js(path: str) -> dict: + text = (folder / path).read_text(encoding="utf-8") + return json.loads(re.search(r"=\s*(\{.*\})\s*;?\s*$", text, re.DOTALL).group(1)) + + +def forecast_point(data: dict, point: str) -> dict: + """Train one model for one point. Returns mae + future [{x, y}, ...].""" + + # 1) BUILD TIME SERIES from [{x, y}, ...] rows + rows = data[point] + times = pd.to_datetime([r["x"] for r in rows], unit="ms") # x = timestamp in milliseconds + values = [float(r["y"]) for r in rows] # y = value as a number + series = pd.Series(values, index=times, name=point).sort_index() # oldest first + step = series.index.to_series().diff().median() # typical gap (~2 min Veris, ~32s RTU) + series = series.resample(step).mean().interpolate() # even spacing so lags line up + + print(f"\n{point} — {len(series)} readings") + + # 2) TRAIN + TEST on the last 20% (see how wrong the model is) + split = int(len(series) * 0.8) + train, test = series.iloc[:split], series.iloc[split:] # first 80% train, last 20% test + + model = ForecasterRecursive(estimator=Ridge(), lags=LAGS) # uses the lag values to make new predictions make sense + model.fit(y=train) + guesses = model.predict(steps=len(test)) # try to predict the hidden 20% + guesses.index = test.index + mae = mean_absolute_error(test, guesses) # average error from the test data + print(f" MAE (last 20%): {mae:.2f}") + + # 3) PREDICT THE FUTURE (retrain on all data, then forecast ahead) + model.fit(y=series) + future = model.predict(steps=FUTURE_STEPS) + future.index = pd.date_range(series.index[-1] + step, periods=FUTURE_STEPS, freq=step) + + # 4) FORMAT for charts — same {x, y} shape as veris-sample.js + return { + "mae": round(mae, 2), + "future": [ + {"x": int(t.timestamp() * 1000), "y": round(float(v), 2)} + for t, v in future.items() + ], + } + + +output = {} +for js_path, points in JOBS: + data = load_sample_js(js_path) + for point in points: + output[point] = forecast_point(data, point) + +out_file = folder / "forecast_output.json" +out_file.write_text(json.dumps(output, indent=2), encoding="utf-8") +print(f"\nSaved forecasts for: {', '.join(output.keys())}")