Upload files to "/"
This commit is contained in:
+85
@@ -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())}")
|
||||||
Reference in New Issue
Block a user