{
  "id": "P137",
  "slug": "cohort-retention",
  "key": "P137-cohort-retention",
  "title": "Cohort retention",
  "summary": "Users who signed up together leave as a product gets worse; the mean tolerance of those still here rises every period while the whole cohort's never moves, because the product chose who is left to be measured.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P137 - Cohort retention\n\nUsers who signed up together, each with a tolerance from 1 to 10, and a\nproduct that annoys them a little more every period. A user leaves once\nthe annoyance passes their tolerance.\n\n- `main.eml` - the menu, the cohort, period by period, and the settings\n- `cohort.eml` - the users, the means rounded to a tenth, and the periods\n\nHow each part works:\n\n- Each period the annoyance rises by a set step. Those whose tolerance it\n  has passed leave; the rest stay. Means are worked out in whole numbers\n  and rounded half up to a tenth.\n- As in the corpus case `survivorship-in-cohorts`, a mean over the users\n  still here is a mean over a sample the product picked. With 40 users and\n  a step of 1, the mean tolerance of those still here rises from 5.5 to\n  9.5 while the product gets worse every period, and over the whole\n  cohort it stays at 5.5: nobody's tolerance changed.\n- It never falls, at any setting: every user who leaves has less\n  tolerance than every user who stays.\n- Headroom, tolerance less annoyance, has two pulls on it: the annoyance\n  pulls it down, the leavers going pulls it up. Here it falls every\n  period; the program counts the periods each way rather than assuming.\n\nWhat is checked: menu choices 1 to 5; 10 to 200 users; an annoyance step\nfrom 1 to 3 and 1 to 12 periods. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the cohort, and 10 periods with a step of 1;\n- a step of 2 over 6 periods.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- users: a cancel, 9, 201 and x; steps: a cancel, 0 and 4, then 2 with a\n  cancel, 0 and 13 periods;\n- 10 users with a step of 3 over 12 periods, everyone gone by period 5.\n\nBuilt on the verified corpus case `survivorship-in-cohorts` (a statistic\nover the users still here rises as the product gets worse).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P137 cohort retention: the cohort, period by period with means over\n# those still here and over everyone, and the settings.\nimport cohort\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef t_text(t):\n    if t < 0:\n        return \"-\"\n    return str(int(t / 10)) + \".\" + str(t % 10)\n\ndef show(users):\n    (str(len(users)) + \" users signed up together; how many have each tolerance:\") ^0\n    \"\" => line\n    for v in [1:10]:\n        0 => c\n        for t in users:\n            if t == v:\n                c + 1 => c\n        if line != \"\":\n            line + \"  \" => line\n        line + str(v) + \": \" + str(c) => line\n    (\"  \" + line) ^0\n    \"Each period the product annoys a little more; a user whose tolerance it passes leaves.\" ^0\n\ndef table(users, step, count):\n    cohort.periods(users, step, count) => rows\n    \"  period  annoyance   here   tolerance, here   tolerance, all   headroom, here   leavers\" ^0\n    for p in [0:len(rows) - 1]:\n        rows[p] => r\n        (\"  \" + lpad(str(p + 1), 6) + lpad(str(r[0]), 11) + lpad(str(r[1]), 7) + lpad(t_text(r[2]), 18) + lpad(t_text(r[3]), 17) + lpad(t_text(r[4]), 17) + lpad(t_text(r[5]), 10)) ^0\n    0 => rose\n    0 => fell\n    0 => hup\n    0 => hdown\n    for p in [1:len(rows) - 1]:\n        if rows[p][1] > 0 and rows[p - 1][1] > 0:\n            if rows[p][2] > rows[p - 1][2]:\n                rose + 1 => rose\n            if rows[p][2] < rows[p - 1][2]:\n                fell + 1 => fell\n            if rows[p][4] > rows[p - 1][4]:\n                hup + 1 => hup\n            if rows[p][4] < rows[p - 1][4]:\n                hdown + 1 => hdown\n    (\"The mean tolerance of those still here rose in \" + str(rose) + \" periods and fell in \" + str(fell) + \"; over the whole cohort it stayed at \" + t_text(rows[0][3]) + \", since nobody's tolerance changed.\") ^0\n    \"Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.\" ^0\n    (\"Headroom, tolerance less annoyance, rose in \" + str(hup) + \" periods and fell in \" + str(hdown) + \": the annoyance pulls it down, and the leavers going pulls it up.\") ^0\n\ndef ask_int(prompt, low, high):\n    plain(input(prompt)) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return 0 - 1\n    if len(t) > 3:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    return int(t)\n\n40 => n\n1 => step\n10 => count\ncohort.cohort(n) => users\n\n\"== Cohort retention ==\" ^0\n\"Users who signed up together, a product that gets a little worse every period, and averages over those who stayed.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the cohort  2) period by period  3) users  4) annoyance  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(users)\n    elif choice == \"2\":\n        table(users, step, count)\n    elif choice == \"3\":\n        ask_int(\"users (10 to 200)> \", 10, 200) => x\n        if x > 0:\n            x => n\n            cohort.cohort(n) => users\n            (str(n) + \" users now.\") ^0\n    elif choice == \"4\":\n        ask_int(\"annoyance added each period (1 to 3)> \", 1, 3) => a\n        if a > 0:\n            ask_int(\"periods (1 to 12)> \", 1, 12) => b\n            if b > 0:\n                a => step\n                b => count\n                (\"The annoyance now rises by \" + str(step) + \" a period, over \" + str(count) + \" periods.\") ^0\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import cohort\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef t_text(t):\n    if t < 0:\n        return \"-\"\n    return str(int(t / 10)) + \".\" + str(t % 10)\n\ndef show(users):\n    print(str(len(users)) + \" users signed up together; how many have each tolerance:\")\n    line = \"\"\n    for v in range(1, 11):\n        c = 0\n        for t in users:\n            if t == v:\n                c = c + 1\n        if line != \"\":\n            line = line + \"  \"\n        line = line + str(v) + \": \" + str(c)\n    print(\"  \" + line)\n    print(\"Each period the product annoys a little more; a user whose tolerance it passes leaves.\")\n\ndef table(users, step, count):\n    rows = cohort.periods(users, step, count)\n    print(\"  period  annoyance   here   tolerance, here   tolerance, all   headroom, here   leavers\")\n    for p in range(0, len(rows)):\n        r = rows[p]\n        print(\"  \" + lpad(str(p + 1), 6) + lpad(str(r[0]), 11) + lpad(str(r[1]), 7) + lpad(t_text(r[2]), 18) + lpad(t_text(r[3]), 17) + lpad(t_text(r[4]), 17) + lpad(t_text(r[5]), 10))\n    rose = 0\n    fell = 0\n    hup = 0\n    hdown = 0\n    for p in range(1, len(rows)):\n        if rows[p][1] > 0 and rows[p - 1][1] > 0:\n            if rows[p][2] > rows[p - 1][2]:\n                rose = rose + 1\n            if rows[p][2] < rows[p - 1][2]:\n                fell = fell + 1\n            if rows[p][4] > rows[p - 1][4]:\n                hup = hup + 1\n            if rows[p][4] < rows[p - 1][4]:\n                hdown = hdown + 1\n    print(\"The mean tolerance of those still here rose in \" + str(rose) + \" periods and fell in \" + str(fell) + \"; over the whole cohort it stayed at \" + t_text(rows[0][3]) + \", since nobody's tolerance changed.\")\n    print(\"Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.\")\n    print(\"Headroom, tolerance less annoyance, rose in \" + str(hup) + \" periods and fell in \" + str(hdown) + \": the annoyance pulls it down, and the leavers going pulls it up.\")\n\ndef ask_int(prompt, low, high):\n    t = plain(input(prompt))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return 0 - 1\n    if len(t) > 3:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    return int(t)\n\nn = 40\nstep = 1\ncount = 10\nusers = cohort.cohort(n)\nprint(\"== Cohort retention ==\")\nprint(\"Users who signed up together, a product that gets a little worse every period, and averages over those who stayed.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the cohort  2) period by period  3) users  4) annoyance  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(users)\n    elif choice == \"2\":\n        table(users, step, count)\n    elif choice == \"3\":\n        x = ask_int(\"users (10 to 200)> \", 10, 200)\n        if x > 0:\n            n = x\n            users = cohort.cohort(n)\n            print(str(n) + \" users now.\")\n    elif choice == \"4\":\n        a = ask_int(\"annoyance added each period (1 to 3)> \", 1, 3)\n        if a > 0:\n            b = ask_int(\"periods (1 to 12)> \", 1, 12)\n            if b > 0:\n                step = a\n                count = b\n                print(\"The annoyance now rises by \" + str(step) + \" a period, over \" + str(count) + \" periods.\")\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "cohort.eml",
      "eml": "# P137 cohort retention - users who signed up together, each with a\n# tolerance from 1 to 10, and a product that annoys them a little more every\n# period. A user leaves once the annoyance passes their tolerance. As in the\n# corpus case survivorship-in-cohorts, a number worked out over the users\n# who are still here is worked out over a sample the product picked: the\n# mean tolerance of the survivors rises while the product gets worse, and\n# over the whole cohort it does not move.\n\ndef mix(i, salt):\n    i * 7919 + salt * 104729 + 12345 => x\n    for k in [1:3]:\n        (x * x + 7) % 1000000007 => x\n    return x % 1000000\n\ndef cohort(n):\n    # tolerances from 1 to 10\n    [] => out\n    for i in [1:n]:\n        out + [1 + mix(i, 1) % 10] => out\n    return out\n\ndef tenths(p, q):\n    # p / q in tenths, rounded half up, for q > 0\n    int((20 * p + q) / (2 * q)) => t\n    while t * 2 * q > 20 * p + q:\n        t - 1 => t\n    while (t + 1) * 2 * q <= 20 * p + q:\n        t + 1 => t\n    return t\n\ndef periods(users, step, count):\n    # per period [annoyance, still here, mean tolerance of those here (in\n    # tenths, -1 if nobody), mean of the whole cohort, mean headroom of\n    # those here (tolerance - annoyance, tenths), leavers' mean tolerance\n    # (tenths, -1 if none left this period)]\n    [] => out\n    0 => s_all\n    for t in users:\n        s_all + t => s_all\n    [] => here\n    for t in users:\n        here + [t] => here\n    for p in [0:count - 1]:\n        step * p => annoy\n        [] => stay\n        [] => gone\n        for t in here:\n            if t >= annoy:\n                stay + [t] => stay\n            else:\n                gone + [t] => gone\n        stay => here\n        0 => s\n        for t in here:\n            s + t => s\n        0 - 1 => mean\n        0 - 1 => head\n        if len(here) > 0:\n            tenths(s, len(here)) => mean\n            tenths(s - annoy * len(here), len(here)) => head\n        0 - 1 => left\n        if len(gone) > 0:\n            0 => g\n            for t in gone:\n                g + t => g\n            tenths(g, len(gone)) => left\n        out + [[annoy, len(here), mean, tenths(s_all, len(users)), head, left]] => out\n    return out\n",
      "python": "def mix(i, salt):\n    x = i * 7919 + salt * 104729 + 12345\n    for k in range(1, 4):\n        x = (x * x + 7) % 1000000007\n    return x % 1000000\n\ndef cohort(n):\n    out = []\n    for i in range(1, n+1):\n        out = out + [1 + mix(i, 1) % 10]\n    return out\n\ndef tenths(p, q):\n    t = int((20 * p + q) / (2 * q))\n    while t * 2 * q > 20 * p + q:\n        t = t - 1\n    while (t + 1) * 2 * q <= 20 * p + q:\n        t = t + 1\n    return t\n\ndef periods(users, step, count):\n    out = []\n    s_all = 0\n    for t in users:\n        s_all = s_all + t\n    here = []\n    for t in users:\n        here = here + [t]\n    for p in range(0, count):\n        annoy = step * p\n        stay = []\n        gone = []\n        for t in here:\n            if t >= annoy:\n                stay = stay + [t]\n            else:\n                gone = gone + [t]\n        here = stay\n        s = 0\n        for t in here:\n            s = s + t\n        mean = 0 - 1\n        head = 0 - 1\n        if len(here) > 0:\n            mean = tenths(s, len(here))\n            head = tenths(s - annoy * len(here), len(here))\n        left = 0 - 1\n        if len(gone) > 0:\n            g = 0\n            for t in gone:\n                g = g + t\n            left = tenths(g, len(gone))\n        out = out + [[annoy, len(here), mean, tenths(s_all, len(users)), head, left]]\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n3\n\n3\n9\n3\n201\n3\nx\n4\n\n4\n0\n4\n4\n4\n2\n\n4\n2\n0\n4\n2\n13\n3\n10\n1\n4\n3\n12\n2\n5\n",
      "screen": "== Cohort retention ==\nUsers who signed up together, a product that gets a little worse every period, and averages over those who stayed.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 3\nusers (10 to 200)> \nCancelled.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 3\nusers (10 to 200)> 9\nType a whole number from 10 to 200.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 3\nusers (10 to 200)> 201\nType a whole number from 10 to 200.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 3\nusers (10 to 200)> x\nType a whole number from 10 to 200.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> \nCancelled.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 0\nType a whole number from 1 to 3.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 4\nType a whole number from 1 to 3.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 2\nperiods (1 to 12)> \nCancelled.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 2\nperiods (1 to 12)> 0\nType a whole number from 1 to 12.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 2\nperiods (1 to 12)> 13\nType a whole number from 1 to 12.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 3\nusers (10 to 200)> 10\n10 users now.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 1\n10 users signed up together; how many have each tolerance:\n  1: 2  2: 0  3: 1  4: 1  5: 0  6: 2  7: 1  8: 1  9: 2  10: 0\nEach period the product annoys a little more; a user whose tolerance it passes leaves.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 3\nperiods (1 to 12)> 12\nThe annoyance now rises by 3 a period, over 12 periods.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 2\n  period  annoyance   here   tolerance, here   tolerance, all   headroom, here   leavers\n       1          0     10               5.4              5.4              5.4         -\n       2          3      8               6.5              5.4              3.5       1.0\n       3          6      6               7.5              5.4              1.5       3.5\n       4          9      2               9.0              5.4              0.0       6.8\n       5         12      0                 -              5.4                -       9.0\n       6         15      0                 -              5.4                -         -\n       7         18      0                 -              5.4                -         -\n       8         21      0                 -              5.4                -         -\n       9         24      0                 -              5.4                -         -\n      10         27      0                 -              5.4                -         -\n      11         30      0                 -              5.4                -         -\n      12         33      0                 -              5.4                -         -\nThe mean tolerance of those still here rose in 3 periods and fell in 0; over the whole cohort it stayed at 5.4, since nobody's tolerance changed.\nEveryone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.\nHeadroom, tolerance less annoyance, rose in 0 periods and fell in 3: the annoyance pulls it down, and the leavers going pulls it up.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n4\n2\n6\n2\n5\n",
      "screen": "== Cohort retention ==\nUsers who signed up together, a product that gets a little worse every period, and averages over those who stayed.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 1\n40 users signed up together; how many have each tolerance:\n  1: 3  2: 3  3: 3  4: 4  5: 6  6: 6  7: 5  8: 6  9: 2  10: 2\nEach period the product annoys a little more; a user whose tolerance it passes leaves.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 2\n  period  annoyance   here   tolerance, here   tolerance, all   headroom, here   leavers\n       1          0     40               5.5              5.5              5.5         -\n       2          1     40               5.5              5.5              4.5         -\n       3          2     37               5.9              5.5              3.9       1.0\n       4          3     34               6.2              5.5              3.2       2.0\n       5          4     31               6.5              5.5              2.5       3.0\n       6          5     27               6.9              5.5              1.9       4.0\n       7          6     21               7.5              5.5              1.5       5.0\n       8          7     15               8.1              5.5              1.1       6.0\n       9          8     10               8.6              5.5              0.6       7.0\n      10          9      4               9.5              5.5              0.5       8.0\nThe mean tolerance of those still here rose in 8 periods and fell in 0; over the whole cohort it stayed at 5.5, since nobody's tolerance changed.\nEveryone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.\nHeadroom, tolerance less annoyance, rose in 0 periods and fell in 9: the annoyance pulls it down, and the leavers going pulls it up.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 4\nannoyance added each period (1 to 3)> 2\nperiods (1 to 12)> 6\nThe annoyance now rises by 2 a period, over 6 periods.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 2\n  period  annoyance   here   tolerance, here   tolerance, all   headroom, here   leavers\n       1          0     40               5.5              5.5              5.5         -\n       2          2     37               5.9              5.5              3.9       1.0\n       3          4     31               6.5              5.5              2.5       2.5\n       4          6     21               7.5              5.5              1.5       4.6\n       5          8     10               8.6              5.5              0.6       6.5\n       6         10      2              10.0              5.5              0.0       8.3\nThe mean tolerance of those still here rose in 5 periods and fell in 0; over the whole cohort it stayed at 5.5, since nobody's tolerance changed.\nEveryone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.\nHeadroom, tolerance less annoyance, rose in 0 periods and fell in 5: the annoyance pulls it down, and the leavers going pulls it up.\n\n1) the cohort  2) period by period  3) users  4) annoyance  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "survivorship-in-cohorts",
      "caseId": "271-survivorship-in-cohorts",
      "title": "Survivorship in cohorts — satisfaction rises as the product gets worse"
    }
  ],
  "updated": "2026-10-17"
}
