102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package growth
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func historicalWins(history []DailyPoint, catalog []Event, before time.Time) []EventEffect {
|
|
wins := make([]EventEffect, 0)
|
|
for _, event := range catalog {
|
|
if !event.EndDate.Before(before) {
|
|
continue
|
|
}
|
|
effect := AnalyzeEventEffect(history, event, catalog)
|
|
if effect.Eligible && effect.MaterialWin {
|
|
wins = append(wins, effect)
|
|
}
|
|
}
|
|
slices.SortFunc(wins, func(a, b EventEffect) int {
|
|
aScore := maxInt64(a.PaidSessionUpliftBPS, a.AmountUpliftBPS)
|
|
bScore := maxInt64(b.PaidSessionUpliftBPS, b.AmountUpliftBPS)
|
|
if aScore != bScore {
|
|
if aScore > bScore {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
return strings.Compare(a.EventID, b.EventID)
|
|
})
|
|
return wins
|
|
}
|
|
|
|
func buildOpportunities(wins []EventEffect, catalog []Event, forecast Forecast, start time.Time, limit int) []Opportunity {
|
|
if forecast.Confidence == ConfidenceInsufficientData || limit <= 0 {
|
|
return nil
|
|
}
|
|
eventByID := make(map[string]Event, len(catalog))
|
|
for _, event := range catalog {
|
|
eventByID[event.ID] = event
|
|
}
|
|
forecastByDate := make(map[string]ForecastPoint, len(forecast.Points))
|
|
for _, point := range forecast.Points {
|
|
forecastByDate[dayKey(point.Date)] = point
|
|
}
|
|
best := make(map[string]Opportunity)
|
|
for _, win := range wins {
|
|
historical := eventByID[win.EventID]
|
|
upcoming := make([]Event, 0)
|
|
for _, event := range catalog {
|
|
if !event.EndDate.Before(start) {
|
|
upcoming = append(upcoming, event)
|
|
}
|
|
}
|
|
for _, match := range MatchUpcomingEvents(historical, upcoming) {
|
|
point, ok := forecastByDate[dayKey(match.Event.StartDate)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
candidate := Opportunity{
|
|
EvidenceID: EvidenceID("opportunity", match.Event.ID+":"+win.EventID), EventID: match.Event.ID,
|
|
EventTitleFA: match.Event.TitleFA, Date: normalizeDate(match.Event.StartDate), SimilarityBPS: match.ScoreBPS,
|
|
ExpectedSessions: point.PaidSessions, ExpectedAmountRial: point.PaidAmountRial,
|
|
HistoricalEvidenceID: win.EvidenceID,
|
|
}
|
|
if current, exists := best[match.Event.ID]; !exists || candidate.SimilarityBPS > current.SimilarityBPS {
|
|
best[match.Event.ID] = candidate
|
|
}
|
|
}
|
|
}
|
|
result := make([]Opportunity, 0, len(best))
|
|
for _, opportunity := range best {
|
|
result = append(result, opportunity)
|
|
}
|
|
slices.SortFunc(result, func(a, b Opportunity) int {
|
|
if a.SimilarityBPS != b.SimilarityBPS {
|
|
if a.SimilarityBPS > b.SimilarityBPS {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
if comparison := a.Date.Compare(b.Date); comparison != 0 {
|
|
return comparison
|
|
}
|
|
return strings.Compare(a.EventID, b.EventID)
|
|
})
|
|
if len(result) > limit {
|
|
result = result[:limit]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func filterCatalog(catalog []Event, category string) []Event {
|
|
filtered := make([]Event, 0, len(catalog))
|
|
for _, event := range catalog {
|
|
if category == "" || len(event.Categories) == 0 || slices.Contains(event.Categories, category) {
|
|
filtered = append(filtered, event)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|