///|
/// Customer lifecycle stages are deterministic and explainable.
pub enum JourneyStage {
Prospect
Activated
Engaged
Repeat
AtRiskJourney
DormantJourney
} derive(Debug, Eq)
///|
pub struct JourneySnapshot {
customer_id : String
stage : JourneyStage
day : Int
value : Int
reason : String
}
///|
pub struct JourneyTransition {
from : JourneyStage
to : JourneyStage
customers : Int
}
///|
fn journey_stage(metrics : CustomerMetrics) -> JourneyStage {
if metrics.frequency == 0 {
Prospect
} else if metrics.recency > 90 {
DormantJourney
} else if metrics.recency > 30 {
AtRiskJourney
} else if metrics.frequency >= 3 {
Repeat
} else if metrics.recency <= 14 {
Engaged
} else {
Activated
}
}
///|
fn journey_reason(stage : JourneyStage) -> String {
match stage {
Prospect => "no observed value event"
Activated => "first value event is established"
Engaged => "recent activity is present"
Repeat => "repeat activity is established"
AtRiskJourney => "activity is outside the recent window"
DormantJourney => "activity is outside the dormant window"
}
}
///|
/// Convert report metrics into a lifecycle snapshot for dashboards.
pub fn journey_snapshots(
report : Report,
as_of : Int,
) -> Array[JourneySnapshot] {
report.customers.map(customer => {
let stage = journey_stage(customer)
{
customer_id: customer.customer_id,
stage,
day: as_of,
value: customer.monetary,
reason: journey_reason(stage),
}
})
}
///|
/// Count changes between two snapshots with the same customer population.
pub fn journey_transitions(
before : Array[JourneySnapshot],
after : Array[JourneySnapshot],
) -> Array[JourneyTransition] {
let result : Array[JourneyTransition] = []
for old in before {
for current in after {
if old.customer_id == current.customer_id {
let mut found : Int? = None
for index, item in result {
if item.from == old.stage && item.to == current.stage {
found = Some(index)
}
}
match found {
Some(index) =>
result[index] = {
..result[index],
customers: result[index].customers + 1,
}
None =>
result.push({ from: old.stage, to: current.stage, customers: 1 })
}
}
}
}
result.sort_by((left, right) => right.customers - left.customers)
result
}
///|
/// Select customers in a particular lifecycle stage.
pub fn customers_in_stage(
snapshots : Array[JourneySnapshot],
stage : JourneyStage,
) -> Array[JourneySnapshot] {
snapshots.filter(snapshot => snapshot.stage == stage)
}
///|
/// Stable convenience counts for dashboard consumers.
pub fn repeat_customer_count(snapshots : Array[JourneySnapshot]) -> Int {
customers_in_stage(snapshots, Repeat).length()
}
///|
pub fn dormant_customer_count(snapshots : Array[JourneySnapshot]) -> Int {
customers_in_stage(snapshots, DormantJourney).length()
}