Cohort analysis: bucket users by signup month, then measure how many were still active in a later month
Demonstrates inner_join across two tables, group_by + agg, and computing a derived retention-rate column with map().
A Soma program from the data part of the verified corpus: 59 lines, cells Cohort, CohortTests. It passes soma check with soma 3.2.4.
How it is built
The cell Cohort has no face and no memory. Handlers: signups (line 6), march_active (line 17), joined (line 27), retention (line 30) and run (line 39).
The file also holds 1 test cell (CohortTests) with 4 assertions, run by soma test.
The source
cohort_retention.cell
// Cohort analysis: bucket users by signup month, then measure how many were
// still active in a later month. Demonstrates inner_join across two tables,
// group_by + agg, and computing a derived retention-rate column with map().
cell Cohort {
on signups() {
return list(
map("user", "a", "cohort", "jan"),
map("user", "b", "cohort", "jan"),
map("user", "c", "cohort", "jan"),
map("user", "d", "cohort", "feb"),
map("user", "e", "cohort", "feb")
)
}
// Activity flag in March (1 = active).
on march_active() {
return list(
map("user", "a", "active", 1),
map("user", "b", "active", 0),
map("user", "c", "active", 1),
map("user", "d", "active", 1),
map("user", "e", "active", 0)
)
}
on joined() { return signups() |> inner_join(march_active(), "user") }
// Per-cohort: size and number still active.
on retention() {
let agged = joined() |> agg("cohort", "active:sum", "active:count")
return agged |> map(r => {
let rate = floor(r.active_sum * 100 / r.active_count)
r |> with("retention_pct", rate)
})
|> sort_by("retention_pct", "desc")
}
on run() {
for r in retention() {
print("{r.cohort}: {r.active_sum}/{r.active_count} = {r.retention_pct}%")
}
return retention()
}
}
cell test CohortTests {
rules {
// all 5 users join cleanly
assert len(joined()) == 5
// jan: 3 users, 2 active (a,c) -> floor(2*100/3) = 66
assert (retention() |> filter_by("cohort", "=", "jan"))[0].retention_pct == 66
// feb: 2 users, 1 active (d) -> 50
assert (retention() |> filter_by("cohort", "=", "feb"))[0].retention_pct == 50
// jan retains better than feb
assert retention()[0].cohort == "jan"
}
}What the toolchain says
$ soma check cohort_retention.cell
✓ All checks passed.$ soma test cohort_retention.cell
test CohortTests ...
✓ assert len(joined()) == 5
✓ assert (retention() |> filter_by("cohort", "=", "jan"))[0].retention_pct == 66
✓ assert (retention() |> filter_by("cohort", "=", "feb"))[0].retention_pct == 50
✓ assert retention()[0].cohort == "jan"
4 tests: 4 passed, 0 failed$ soma verify cohort_retention.cell
Verifying cohort_retention.cell...
No state machine in this program: nothing to prove beyond `soma check` (invariants and lifecycles are what verify proves).
VERIFY OK (vacuous — no state machine)Builtins used
agg, filter_by, floor, inner_join, len, list, map, print, sort_by, with