54 lines
2.8 KiB
Go
54 lines
2.8 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// DemandForecast is one zone-day the engine expects.
|
|
//
|
|
// ─── Why this is stored rather than computed on read ───────────────────────
|
|
//
|
|
// The forecast is produced in Python (AI_engine/prediction), because the model
|
|
// is: Prophet where it beats a seasonal baseline, the baseline otherwise. The
|
|
// console and any staffing decision need it in Go. So the engine writes here
|
|
// and the backend serves it — the same split the agent registry and the
|
|
// decision log already use, and the reason the /internal/* surface exists.
|
|
//
|
|
// It is also the honest shape: a forecast is a thing produced at a moment by a
|
|
// model, not a function of the current table. Recomputing it on every read
|
|
// would make yesterday's number unrecoverable, which is exactly what you want
|
|
// when asking "was the forecast any good".
|
|
type DemandForecast struct {
|
|
Forecastid int `json:"forecastid" gorm:"primaryKey;column:forecastid;autoIncrement"`
|
|
|
|
// Zone is the first three digits of a pickup pincode — the same grain the
|
|
// hub console scopes on (pickuppincode LIKE '641%') and the same grain
|
|
// internal/prediction calibrates ETA at. Keeping one definition of "zone"
|
|
// across both is deliberate.
|
|
Zone string `json:"zone" gorm:"column:zone;size:8;not null;uniqueIndex:uq_demandforecast_zone_day,priority:1"`
|
|
|
|
// Forday is the day being predicted, not the day it was predicted on.
|
|
Forday time.Time `json:"forday" gorm:"column:forday;not null;uniqueIndex:uq_demandforecast_zone_day,priority:2;index"`
|
|
|
|
Expectedbookings int `json:"expectedbookings" gorm:"column:expectedbookings;not null"`
|
|
|
|
// Model and Reason record WHICH model produced this and why it was chosen —
|
|
// "prophet beat the weekly baseline over 12 folds (18% lower MAE)", or
|
|
// "prophet is not installed in this image". Stored because the choice is
|
|
// made per zone from that zone's own history, so without it nobody can tell
|
|
// whether a bad forecast came from a bad model or from a thin series.
|
|
Model string `json:"model" gorm:"column:model;size:32;not null"`
|
|
Reason string `json:"reason" gorm:"column:reason"`
|
|
|
|
// Observations is how many days of history the forecast was fitted on, and
|
|
// Baselinemae/Modelmae are the backtest scores. A forecast with 31
|
|
// observations and a model barely beating the baseline deserves less trust
|
|
// than one with 400, and this is what lets a reader see that rather than
|
|
// taking the number at face value.
|
|
Observations int `json:"observations" gorm:"column:observations;default:0"`
|
|
Baselinemae *float64 `json:"baselinemae" gorm:"column:baselinemae"`
|
|
Modelmae *float64 `json:"modelmae" gorm:"column:modelmae"`
|
|
|
|
Generatedat time.Time `json:"generatedat" gorm:"column:generatedat;not null"`
|
|
}
|
|
|
|
func (DemandForecast) TableName() string { return "demandforecast" }
|