soma.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().

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.

Generated from soma describe on this file.

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

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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

Source file: /corpus/data/cohort_retention.cell · examples/corpus/data/cohort_retention.cell