// Package peercomparison contains the privacy-bounded, merchant-weighted // comparison calculation shared by analytics and page insights. package peercomparison import ( "fmt" "sort" ) const ( MinimumPeerCount = 10 MinimumPeerSessions = 1000 ) type State string const ( StateReady State = "READY" StateSuppressed State = "SUPPRESSED" ) type MetricDefinition struct { ID string LowerIsBetter bool } var metricDefinitions = []MetricDefinition{ {ID: "payment_success_rate"}, {ID: "verification_completion_rate"}, {ID: "retry_recovery_rate"}, {ID: "init_time_ms_p50", LowerIsBetter: true}, {ID: "init_time_ms_p95", LowerIsBetter: true}, {ID: "verify_time_ms_p50", LowerIsBetter: true}, {ID: "verify_time_ms_p95", LowerIsBetter: true}, {ID: "paid_fee_rate", LowerIsBetter: true}, } func MetricDefinitions() []MetricDefinition { return append([]MetricDefinition(nil), metricDefinitions...) } // MerchantValues contains one aggregate value per merchant. It intentionally // carries no merchant data other than the internal id used to exclude the // active merchant and the session count used for suppression. type MerchantValues struct { MerchantID int64 SessionCount int64 Values [8]*float64 } type Metric struct { MetricID string `json:"metricId"` CohortMedian string `json:"cohortMedian"` MerchantPerformancePercentile string `json:"merchantPerformancePercentile"` Direction string `json:"direction"` } type Result struct { State State `json:"state"` Reason string `json:"reason,omitempty"` EligiblePeerCount int `json:"eligiblePeerCount"` CombinedPeerSessions int64 `json:"combinedPeerSessionCount"` Metrics []Metric `json:"metrics,omitempty"` } func Calculate(activeMerchantID int64, terminalScoped bool, merchants []MerchantValues) Result { result := Result{State: StateSuppressed} if terminalScoped { result.Reason = "TERMINAL_SCOPE_UNSUPPORTED" return result } var active *MerchantValues peers := make([]MerchantValues, 0, len(merchants)) for index := range merchants { merchant := merchants[index] if merchant.MerchantID == activeMerchantID { active = &merchants[index] continue } peers = append(peers, merchant) result.CombinedPeerSessions += merchant.SessionCount } result.EligiblePeerCount = len(peers) if len(peers) < MinimumPeerCount || result.CombinedPeerSessions < MinimumPeerSessions { result.Reason = "INSUFFICIENT_COHORT" return result } if active == nil { result.Reason = "INSUFFICIENT_COHORT" return result } result.State = StateReady result.Metrics = make([]Metric, 0, len(metricDefinitions)) for index, definition := range metricDefinitions { activeValue := active.Values[index] values := make([]float64, 0, len(peers)) for _, peer := range peers { if peer.Values[index] != nil { values = append(values, *peer.Values[index]) } } // Keep the public shape stable while making unavailable metric input // explicit through the existing zero representation. if len(values) == 0 || activeValue == nil { result.Metrics = append(result.Metrics, Metric{MetricID: definition.ID, CohortMedian: "0.00", MerchantPerformancePercentile: "0.00", Direction: direction(definition)}) continue } sort.Float64s(values) median := values[len(values)/2] if len(values)%2 == 0 { median = (values[len(values)/2-1] + median) / 2 } below, equal := 0, 0 for _, value := range values { if definition.LowerIsBetter { if *activeValue < value { below++ } else if *activeValue == value { equal++ } } else if *activeValue > value { below++ } else if *activeValue == value { equal++ } } percentileBPS := (2*below + equal) * 5000 / len(values) result.Metrics = append(result.Metrics, Metric{MetricID: definition.ID, CohortMedian: fmt.Sprintf("%.2f", median), MerchantPerformancePercentile: fmt.Sprintf("%.2f", float64(percentileBPS)/100), Direction: direction(definition)}) } return result } func direction(definition MetricDefinition) string { if definition.LowerIsBetter { return "LOWER_IS_BETTER" } return "HIGHER_IS_BETTER" }