work on model training
This commit is contained in:
@@ -24,14 +24,16 @@ type webServer struct {
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Site string `json:"site"`
|
||||
Model string `json:"model"`
|
||||
RangeStart time.Time `json:"range_start"`
|
||||
RangeEnd time.Time `json:"range_end"`
|
||||
Observations []db.ObservationPoint `json:"observations"`
|
||||
Forecast db.ForecastSeries `json:"forecast"`
|
||||
Latest *db.ObservationPoint `json:"latest"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Site string `json:"site"`
|
||||
Model string `json:"model"`
|
||||
RangeStart time.Time `json:"range_start"`
|
||||
RangeEnd time.Time `json:"range_end"`
|
||||
Observations []db.ObservationPoint `json:"observations"`
|
||||
Forecast db.ForecastSeries `json:"forecast"`
|
||||
Latest *db.ObservationPoint `json:"latest"`
|
||||
LatestRainPredict *db.RainPredictionPoint `json:"latest_rain_prediction,omitempty"`
|
||||
RainPredictionRange []db.RainPredictionPoint `json:"rain_predictions,omitempty"`
|
||||
}
|
||||
|
||||
func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, addr string) error {
|
||||
@@ -171,15 +173,33 @@ func (s *webServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
const rainModelName = "rain_next_1h"
|
||||
|
||||
latestRainPrediction, err := s.db.LatestRainPrediction(r.Context(), s.site.Name, rainModelName)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to query latest rain prediction", http.StatusInternalServerError)
|
||||
log.Printf("web dashboard latest rain prediction error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rainPredictionRange, err := s.db.RainPredictionSeriesRange(r.Context(), s.site.Name, rainModelName, start, end)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to query rain predictions", http.StatusInternalServerError)
|
||||
log.Printf("web dashboard rain prediction range error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := dashboardResponse{
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
Site: s.site.Name,
|
||||
Model: s.model,
|
||||
RangeStart: start,
|
||||
RangeEnd: end,
|
||||
Observations: observations,
|
||||
Forecast: forecast,
|
||||
Latest: latest,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
Site: s.site.Name,
|
||||
Model: s.model,
|
||||
RangeStart: start,
|
||||
RangeEnd: end,
|
||||
Observations: observations,
|
||||
Forecast: forecast,
|
||||
Latest: latest,
|
||||
LatestRainPredict: latestRainPrediction,
|
||||
RainPredictionRange: rainPredictionRange,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -501,6 +501,40 @@ function buildRainProbabilitySeries(points) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildRainProbabilitySeriesFromPredictions(points) {
|
||||
return points.map((p) => {
|
||||
const t = new Date(p.ts).getTime();
|
||||
if (Number.isNaN(t)) {
|
||||
return { x: null, y: null };
|
||||
}
|
||||
if (p.probability === null || p.probability === undefined) {
|
||||
return { x: t, y: null };
|
||||
}
|
||||
return {
|
||||
x: t,
|
||||
y: Math.round(Number(p.probability) * 1000) / 10,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function thresholdSeries(range, threshold) {
|
||||
if (!range || !range.axisStart || !range.axisEnd || threshold === null || threshold === undefined) {
|
||||
return [];
|
||||
}
|
||||
const y = Math.round(Number(threshold) * 1000) / 10;
|
||||
return [
|
||||
{ x: range.axisStart.getTime(), y },
|
||||
{ x: range.axisEnd.getTime(), y },
|
||||
];
|
||||
}
|
||||
|
||||
function predictionAgeMinutes(prediction) {
|
||||
if (!prediction || !prediction.ts) return null;
|
||||
const ts = new Date(prediction.ts).getTime();
|
||||
if (Number.isNaN(ts)) return null;
|
||||
return (Date.now() - ts) / (60 * 1000);
|
||||
}
|
||||
|
||||
function updateWeatherIcons(latest, rainProb) {
|
||||
const sunEl = document.getElementById("live-icon-sun");
|
||||
const cloudEl = document.getElementById("live-icon-cloud");
|
||||
@@ -693,10 +727,26 @@ function renderDashboard(data) {
|
||||
const obsFiltered = filterRange(obs, rangeStart, rangeEnd);
|
||||
const forecast = filterRange(forecastAll, rangeStart, rangeEnd);
|
||||
const forecastLine = extendForecastTo(forecast, rangeEnd);
|
||||
const rainPredictions = filterRange(data.rain_predictions || [], rangeStart, rangeEnd);
|
||||
const latestRainPrediction = data.latest_rain_prediction || null;
|
||||
const latestPredictionAgeMin = predictionAgeMinutes(latestRainPrediction);
|
||||
const modelPredictionFresh = latestPredictionAgeMin !== null && latestPredictionAgeMin <= 90;
|
||||
const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h");
|
||||
const rainProb = computeRainProbability(latest);
|
||||
const modelRainProb = modelPredictionFresh && latestRainPrediction && latestRainPrediction.probability !== null && latestRainPrediction.probability !== undefined
|
||||
? {
|
||||
prob: Number(latestRainPrediction.probability),
|
||||
label: classifyRainProbability(Number(latestRainPrediction.probability)),
|
||||
source: "model",
|
||||
}
|
||||
: null;
|
||||
const heuristicRainProb = computeRainProbability(latest);
|
||||
const rainProb = modelRainProb || heuristicRainProb;
|
||||
if (rainProb) {
|
||||
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`);
|
||||
const sourceLabel = rainProb.source === "model" ? "model" : "heuristic";
|
||||
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label}, ${sourceLabel})`);
|
||||
} else if (latestRainPrediction && latestRainPrediction.probability !== null && latestRainPrediction.probability !== undefined) {
|
||||
const stalePct = Math.round(Number(latestRainPrediction.probability) * 100);
|
||||
updateText("live-rain-prob", `${stalePct}% (stale model)`);
|
||||
} else {
|
||||
updateText("live-rain-prob", "--");
|
||||
}
|
||||
@@ -901,13 +951,20 @@ function renderDashboard(data) {
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "predicted rain probability (%)",
|
||||
data: buildRainProbabilitySeries(obsFiltered),
|
||||
label: rainPredictions.length ? "model rain probability (%)" : "heuristic rain probability (%)",
|
||||
data: rainPredictions.length ? buildRainProbabilitySeriesFromPredictions(rainPredictions) : buildRainProbabilitySeries(obsFiltered),
|
||||
borderColor: colors.rain,
|
||||
backgroundColor: "rgba(78, 168, 222, 0.18)",
|
||||
fill: true,
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
label: "decision threshold (%)",
|
||||
data: thresholdSeries(range, latestRainPrediction ? latestRainPrediction.threshold : null),
|
||||
borderColor: "#f4b942",
|
||||
borderDash: [6, 4],
|
||||
yAxisID: "y",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: rainProbOptions,
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</div>
|
||||
<div class="chart-card" data-chart="chart-rain-prob">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">Predicted Rain Probability (Observed Inputs)</div>
|
||||
<div class="chart-title">Predicted Rain Probability (Model)</div>
|
||||
<button class="chart-link" data-chart="chart-rain-prob" title="Copy chart link">Share</button>
|
||||
</div>
|
||||
<div class="chart-canvas">
|
||||
|
||||
Reference in New Issue
Block a user