fix: keep growth analysis available per terminal

This commit is contained in:
2026-08-21 02:57:46 +03:30
parent 7951664d08
commit e6afaa7a10
11 changed files with 180 additions and 45 deletions

View File

@@ -17,20 +17,17 @@ func BuildForecast(history []DailyPoint, catalog []Event, start time.Time, horiz
}
result := Forecast{StartDate: normalizeDate(start), HorizonDays: horizonDays, Confidence: ConfidenceInsufficientData, SuppressionReason: SuppressionInsufficientHistory, Points: []ForecastPoint{}}
ordered := normalizedHistory(history, start)
if len(ordered) < minimumHistoryDays {
if len(ordered) == 0 {
return result
}
result.Backtest = backtest(ordered, catalog)
result.Confidence = classifyConfidence(result.Backtest)
if result.Confidence == ConfidenceInsufficientData {
return result
}
result.SuppressionReason = ""
result.Points = forecastPoints(ordered, catalog, result.StartDate, horizonDays)
result.Points = forecastPoints(ordered, catalog, result.StartDate, horizonDays, result.Confidence)
return result
}
func forecastPoints(history []DailyPoint, catalog []Event, start time.Time, horizon int) []ForecastPoint {
func forecastPoints(history []DailyPoint, catalog []Event, start time.Time, horizon int, confidence Confidence) []ForecastPoint {
sessionResiduals, amountResiduals := modelResiduals(history, catalog)
points := make([]ForecastPoint, 0, horizon)
for index := 0; index < horizon; index++ {
@@ -38,8 +35,8 @@ func forecastPoints(history []DailyPoint, catalog []Event, start time.Time, hori
baselineSessions, baselineAmount, predictedSessions, predictedAmount, eventIDs := predictCenter(history, catalog, day)
points = append(points, ForecastPoint{
Date: day, BaselineSessions: baselineSessions, BaselineAmountRial: baselineAmount,
PaidSessions: rangeFromResiduals(predictedSessions, sessionResiduals),
PaidAmountRial: rangeFromResiduals(predictedAmount, amountResiduals), EventIDs: eventIDs,
PaidSessions: forecastRange(predictedSessions, sessionResiduals, confidence),
PaidAmountRial: forecastRange(predictedAmount, amountResiduals, confidence), EventIDs: eventIDs,
})
}
return points
@@ -64,6 +61,11 @@ func weekdayMedian(history []DailyPoint, weekday time.Weekday, value func(DailyP
values = append(values, value(history[index]))
}
}
if len(values) == 0 {
for index := len(history) - 1; index >= 0 && len(values) < 8; index-- {
values = append(values, value(history[index]))
}
}
return median(values)
}
@@ -100,6 +102,13 @@ func rangeFromResiduals(center int64, residualValues []int64) Range {
}
}
func forecastRange(center int64, residualValues []int64, confidence Confidence) Range {
if confidence == ConfidenceLow && len(residualValues) == 0 {
return Range{P10: center / 2, P50: center, P90: center + center/2}
}
return rangeFromResiduals(center, residualValues)
}
func quantile(values []int64, percentile int) int64 {
if len(values) == 0 {
return 0
@@ -186,7 +195,7 @@ func predictCenter(history []DailyPoint, catalog []Event, day time.Time) (int64,
func classifyConfidence(backtest BacktestResult) Confidence {
if backtest.Origins < 4 {
return ConfidenceInsufficientData
return ConfidenceLow
}
worstMAPE := maxInt64(backtest.SessionMAPEBPS, backtest.AmountMAPEBPS)
if worstMAPE <= 2_000 && backtest.IntervalCoverageBPS >= 7_000 {
@@ -195,7 +204,7 @@ func classifyConfidence(backtest BacktestResult) Confidence {
if worstMAPE <= 4_000 && backtest.IntervalCoverageBPS >= 5_000 {
return ConfidenceMedium
}
return ConfidenceInsufficientData
return ConfidenceLow
}
type familyUplift struct{ sessions, amount int64 }