feat: ground competitive insights in peer metrics
This commit is contained in:
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/peercomparison"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -135,21 +135,17 @@ func (s *Service) Start(ctx context.Context, merchantID int64, period Period, te
|
||||
"reason": "TERMINAL_SCOPE_UNSUPPORTED", "eligiblePeerCount": 0, "combinedPeerSessionCount": 0,
|
||||
"requiredPeerCount": 10, "requiredPeerSessionCount": 1000}
|
||||
} else {
|
||||
peerCount, peerSessions, err := s.peerEligibility(ctx, merchantID, period)
|
||||
peerResult, err := s.peerComparison(ctx, merchantID, period)
|
||||
if err != nil {
|
||||
return Run{}, err
|
||||
}
|
||||
if peerCount < 10 || peerSessions < 1000 {
|
||||
if peerResult.State != peercomparison.StateReady {
|
||||
run.Sections["peerComparison"] = map[string]any{"sectionId": "peer_comparison", "state": "SUPPRESSED", "metadata": metadata,
|
||||
"reason": "INSUFFICIENT_COHORT", "eligiblePeerCount": peerCount, "combinedPeerSessionCount": peerSessions,
|
||||
"reason": peerResult.Reason, "eligiblePeerCount": peerResult.EligiblePeerCount, "combinedPeerSessionCount": peerResult.CombinedPeerSessions,
|
||||
"requiredPeerCount": 10, "requiredPeerSessionCount": 1000}
|
||||
} else {
|
||||
peerMetrics, err := s.peerMetrics(ctx, merchantID, period)
|
||||
if err != nil {
|
||||
return Run{}, err
|
||||
}
|
||||
run.Sections["peerComparison"] = map[string]any{"sectionId": "peer_comparison", "state": "READY", "metadata": metadata,
|
||||
"data": map[string]any{"eligiblePeerCount": peerCount, "combinedPeerSessionCount": peerSessions, "metrics": peerMetrics}}
|
||||
"data": map[string]any{"eligiblePeerCount": peerResult.EligiblePeerCount, "combinedPeerSessionCount": peerResult.CombinedPeerSessions, "metrics": peerResult.Metrics}}
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
@@ -395,85 +391,33 @@ func statusTrendQuery(periodType string) (string, int) {
|
||||
FROM buckets b LEFT JOIN scoped s USING(bucket_start) GROUP BY b.bucket_start ORDER BY b.bucket_start`, map[string]int{"WEEK": 7, "JALALI_MONTH": 31}[periodType]
|
||||
}
|
||||
|
||||
func (s *Service) peerEligibility(ctx context.Context, merchantID int64, p Period) (int64, int64, error) {
|
||||
var peers, sessions int64
|
||||
err := s.pool.QueryRow(ctx, `SELECT count(distinct ps.merchant_id),count(*) FROM payment_sessions ps JOIN merchants m ON m.id=ps.merchant_id
|
||||
WHERE m.category_id=(SELECT category_id FROM merchants WHERE id=$1) AND ps.merchant_id<>$1 AND ps.created_at>=$2 AND ps.created_at<$3`, merchantID, p.StartAt, p.EndAt).Scan(&peers, &sessions)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("calculating peer eligibility: %w", err)
|
||||
}
|
||||
return peers, sessions, nil
|
||||
}
|
||||
|
||||
type peerRow struct {
|
||||
merchantID int64
|
||||
values [8]*float64
|
||||
}
|
||||
|
||||
func (s *Service) peerMetrics(ctx context.Context, merchantID int64, p Period) ([]map[string]any, error) {
|
||||
func (s *Service) peerComparison(ctx context.Context, merchantID int64, p Period) (peercomparison.Result, error) {
|
||||
query := `WITH session_stats AS (SELECT ps.merchant_id,ps.id,ps.status,ps.amount_rial,ps.fee_amount_rial,count(pa.id) attempts
|
||||
FROM payment_sessions ps LEFT JOIN payment_attempts pa ON pa.session_id=ps.id JOIN merchants m ON m.id=ps.merchant_id
|
||||
WHERE m.category_id=(SELECT category_id FROM merchants WHERE id=$1) AND ps.created_at>=$2 AND ps.created_at<$3 GROUP BY ps.id),
|
||||
aggregate AS (SELECT merchant_id,100.0*count(*) filter(where status in ('Paid','Verified'))/nullif(count(*),0) success,
|
||||
aggregate AS (SELECT merchant_id,count(*) session_count,100.0*count(*) filter(where status in ('Paid','Verified'))/nullif(count(*),0) success,
|
||||
100.0*count(*) filter(where status='Verified')/nullif(count(*) filter(where status in ('Paid','Verified')),0) verification,
|
||||
100.0*count(*) filter(where attempts>1 and status in ('Paid','Verified'))/nullif(count(*) filter(where attempts>1),0) recovery,
|
||||
100.0*sum(fee_amount_rial) filter(where status in ('Paid','Verified'))/nullif(sum(amount_rial) filter(where status in ('Paid','Verified')),0) fee FROM session_stats GROUP BY merchant_id),
|
||||
timing AS (SELECT ps.merchant_id,percentile_disc(.5) within group(order by pa.init_time_ms)::float8 ip50,percentile_disc(.95) within group(order by pa.init_time_ms)::float8 ip95,
|
||||
percentile_disc(.5) within group(order by pa.verify_time_ms)::float8 vp50,percentile_disc(.95) within group(order by pa.verify_time_ms)::float8 vp95
|
||||
FROM payment_sessions ps JOIN payment_attempts pa ON pa.session_id=ps.id JOIN merchants m ON m.id=ps.merchant_id WHERE m.category_id=(SELECT category_id FROM merchants WHERE id=$1) AND ps.created_at>=$2 AND ps.created_at<$3 GROUP BY ps.merchant_id)
|
||||
SELECT a.merchant_id,a.success::float8,a.verification::float8,a.recovery::float8,t.ip50,t.ip95,t.vp50,t.vp95,a.fee::float8 FROM aggregate a JOIN timing t USING(merchant_id)`
|
||||
SELECT a.merchant_id,a.session_count,a.success::float8,a.verification::float8,a.recovery::float8,t.ip50,t.ip95,t.vp50,t.vp95,a.fee::float8 FROM aggregate a LEFT JOIN timing t USING(merchant_id)`
|
||||
rows, err := s.pool.Query(ctx, query, merchantID, p.StartAt, p.EndAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculating peer metrics: %w", err)
|
||||
return peercomparison.Result{}, fmt.Errorf("calculating peer metrics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
all := []peerRow{}
|
||||
all := []peercomparison.MerchantValues{}
|
||||
for rows.Next() {
|
||||
var row peerRow
|
||||
if err := rows.Scan(&row.merchantID, &row.values[0], &row.values[1], &row.values[2], &row.values[3], &row.values[4], &row.values[5], &row.values[6], &row.values[7]); err != nil {
|
||||
return nil, fmt.Errorf("scanning peer metrics: %w", err)
|
||||
var row peercomparison.MerchantValues
|
||||
if err := rows.Scan(&row.MerchantID, &row.SessionCount, &row.Values[0], &row.Values[1], &row.Values[2], &row.Values[3], &row.Values[4], &row.Values[5], &row.Values[6], &row.Values[7]); err != nil {
|
||||
return peercomparison.Result{}, fmt.Errorf("scanning peer metrics: %w", err)
|
||||
}
|
||||
all = append(all, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterating peer metrics: %w", err)
|
||||
return peercomparison.Result{}, fmt.Errorf("iterating peer metrics: %w", err)
|
||||
}
|
||||
ids := []string{"payment_success_rate", "verification_completion_rate", "retry_recovery_rate", "init_time_ms_p50", "init_time_ms_p95", "verify_time_ms_p50", "verify_time_ms_p95", "paid_fee_rate"}
|
||||
metrics := make([]map[string]any, 0, 8)
|
||||
for index, id := range ids {
|
||||
active := 0.0
|
||||
peers := []float64{}
|
||||
for _, row := range all {
|
||||
if row.values[index] == nil {
|
||||
continue
|
||||
}
|
||||
if row.merchantID == merchantID {
|
||||
active = *row.values[index]
|
||||
} else {
|
||||
peers = append(peers, *row.values[index])
|
||||
}
|
||||
}
|
||||
if len(peers) == 0 {
|
||||
peers = append(peers, 0)
|
||||
}
|
||||
slices.Sort(peers)
|
||||
median := peers[len(peers)/2]
|
||||
if len(peers)%2 == 0 {
|
||||
median = (peers[len(peers)/2-1] + median) / 2
|
||||
}
|
||||
lowerIsBetter := index >= 3
|
||||
better := 0
|
||||
for _, value := range peers {
|
||||
if (!lowerIsBetter && active >= value) || (lowerIsBetter && active <= value) {
|
||||
better++
|
||||
}
|
||||
}
|
||||
percentile := float64(better) * 100 / float64(len(peers))
|
||||
direction := "HIGHER_IS_BETTER"
|
||||
if lowerIsBetter {
|
||||
direction = "LOWER_IS_BETTER"
|
||||
}
|
||||
metrics = append(metrics, map[string]any{"metricId": id, "cohortMedian": fmt.Sprintf("%.2f", median), "merchantPerformancePercentile": fmt.Sprintf("%.2f", percentile), "direction": direction})
|
||||
}
|
||||
return metrics, nil
|
||||
return peercomparison.Calculate(merchantID, false, all), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user