///|
fn score(value : Int, low : Int, high : Int) -> Int {
if value <= low {
1
} else if value >= high {
5
} else {
3
}
}
///|
fn segment_for(
recency : Int,
frequency : Int,
monetary : Int,
) -> (Segment, String) {
if recency <= 7 && frequency >= 3 && monetary >= 300 {
(Champions, "recent activity + repeat purchases + high value")
} else if recency <= 14 && frequency >= 2 {
(Loyal, "recent activity + repeat purchases + developing value")
} else if recency > 30 && frequency > 1 {
(AtRisk, "long silence after repeat purchases")
} else if recency > 60 {
(Dormant, "no recent activity")
} else {
(New, "early customer relationship")
}
}
///|
fn customer_metrics(
events : Array[Event],
customer_id : String,
as_of : Int,
) -> CustomerMetrics {
let mut first_day = 1_000_000_000
let mut last_day = 0
let mut frequency = 0
let mut monetary = 0
for event in events {
if event.customer_id == customer_id {
if event.day < first_day {
first_day = event.day
}
if event.day > last_day {
last_day = event.day
}
frequency += 1
monetary += event.amount
}
}
let recency = as_of - last_day
let r_score = if recency <= 7 { 5 } else if recency <= 30 { 3 } else { 1 }
let f_score = score(frequency, 1, 4)
let m_score = score(monetary, 50, 500)
let (segment, explanation) = segment_for(recency, frequency, monetary)
{
customer_id,
first_day,
last_day,
recency,
frequency,
monetary,
r_score,
f_score,
m_score,
segment,
explanation,
}
}
///|
/// Analyze events with deterministic RFM rules and an explanation for every segment.
pub fn analyze(events : Array[Event], as_of : Int) -> Report {
let ids : Array[String] = []
for event in events {
if !ids.contains(event.customer_id) {
ids.push(event.customer_id)
}
}
let customers = ids.map(id => customer_metrics(events, id, as_of))
{ customers, }
}