Cohort retention
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.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Users who signed up together, each with a tolerance from 1 to 10, and a product that annoys them a little more every period. A user leaves once the annoyance passes their tolerance.
main.eml- the menu, the cohort, period by period, and the settingscohort.eml- the users, the means rounded to a tenth, and the periods
How each part works:
- Each period the annoyance rises by a set step. Those whose tolerance it has passed leave; the rest stay. Means are worked out in whole numbers and rounded half up to a tenth.
- As in the corpus case
survivorship-in-cohorts, a mean over the users still here is a mean over a sample the product picked. With 40 users and a step of 1, the mean tolerance of those still here rises from 5.5 to 9.5 while the product gets worse every period, and over the whole cohort it stays at 5.5: nobody's tolerance changed. - It never falls, at any setting: every user who leaves has less tolerance than every user who stays.
- Headroom, tolerance less annoyance, has two pulls on it: the annoyance pulls it down, the leavers going pulls it up. Here it falls every period; the program counts the periods each way rather than assuming.
What is checked: menu choices 1 to 5; 10 to 200 users; an annoyance step from 1 to 3 and 1 to 12 periods. An empty answer cancels.
Sessions: sessions/basic.in gives: - the cohort, and 10 periods with a step of 1; - a step of 2 over 6 periods.
sessions/bad-input.in gives: - menu choices 0 and x; - users: a cancel, 9, 201 and x; steps: a cancel, 0 and 4, then 2 with a cancel, 0 and 13 periods; - 10 users with a step of 3 over 12 periods, everyone gone by period 5.
Built on the verified corpus case survivorship-in-cohorts (a statistic over the users still here rises as the product gets worse).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Cohort retention ==
Users who signed up together, a product that gets a little worse every period, and averages over those who stayed.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 0
Pick a number from 1 to 5.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> x
Pick a number from 1 to 5.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 3
users (10 to 200)>
Cancelled.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 3
users (10 to 200)> 9
Type a whole number from 10 to 200.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 3
users (10 to 200)> 201
Type a whole number from 10 to 200.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 3
users (10 to 200)> x
Type a whole number from 10 to 200.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)>
Cancelled.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 0
Type a whole number from 1 to 3.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 4
Type a whole number from 1 to 3.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 2
periods (1 to 12)>
Cancelled.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 2
periods (1 to 12)> 0
Type a whole number from 1 to 12.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 2
periods (1 to 12)> 13
Type a whole number from 1 to 12.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 3
users (10 to 200)> 10
10 users now.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 1
10 users signed up together; how many have each tolerance:
1: 2 2: 0 3: 1 4: 1 5: 0 6: 2 7: 1 8: 1 9: 2 10: 0
Each period the product annoys a little more; a user whose tolerance it passes leaves.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 3
periods (1 to 12)> 12
The annoyance now rises by 3 a period, over 12 periods.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 2
period annoyance here tolerance, here tolerance, all headroom, here leavers
1 0 10 5.4 5.4 5.4 -
2 3 8 6.5 5.4 3.5 1.0
3 6 6 7.5 5.4 1.5 3.5
4 9 2 9.0 5.4 0.0 6.8
5 12 0 - 5.4 - 9.0
6 15 0 - 5.4 - -
7 18 0 - 5.4 - -
8 21 0 - 5.4 - -
9 24 0 - 5.4 - -
10 27 0 - 5.4 - -
11 30 0 - 5.4 - -
12 33 0 - 5.4 - -
The 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.
Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.
Headroom, tolerance less annoyance, rose in 0 periods and fell in 3: the annoyance pulls it down, and the leavers going pulls it up.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 5
Bye.
What was typed (33 lines)
0
x
3
3
9
3
201
3
x
4
4
0
4
4
4
2
4
2
0
4
2
13
3
10
1
4
3
12
2
5
basic
interpreter: byte-equal== Cohort retention ==
Users who signed up together, a product that gets a little worse every period, and averages over those who stayed.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 1
40 users signed up together; how many have each tolerance:
1: 3 2: 3 3: 3 4: 4 5: 6 6: 6 7: 5 8: 6 9: 2 10: 2
Each period the product annoys a little more; a user whose tolerance it passes leaves.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 2
period annoyance here tolerance, here tolerance, all headroom, here leavers
1 0 40 5.5 5.5 5.5 -
2 1 40 5.5 5.5 4.5 -
3 2 37 5.9 5.5 3.9 1.0
4 3 34 6.2 5.5 3.2 2.0
5 4 31 6.5 5.5 2.5 3.0
6 5 27 6.9 5.5 1.9 4.0
7 6 21 7.5 5.5 1.5 5.0
8 7 15 8.1 5.5 1.1 6.0
9 8 10 8.6 5.5 0.6 7.0
10 9 4 9.5 5.5 0.5 8.0
The 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.
Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.
Headroom, tolerance less annoyance, rose in 0 periods and fell in 9: the annoyance pulls it down, and the leavers going pulls it up.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 4
annoyance added each period (1 to 3)> 2
periods (1 to 12)> 6
The annoyance now rises by 2 a period, over 6 periods.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 2
period annoyance here tolerance, here tolerance, all headroom, here leavers
1 0 40 5.5 5.5 5.5 -
2 2 37 5.9 5.5 3.9 1.0
3 4 31 6.5 5.5 2.5 2.5
4 6 21 7.5 5.5 1.5 4.6
5 8 10 8.6 5.5 0.6 6.5
6 10 2 10.0 5.5 0.0 8.3
The 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.
Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.
Headroom, tolerance less annoyance, rose in 0 periods and fell in 5: the annoyance pulls it down, and the leavers going pulls it up.
1) the cohort 2) period by period 3) users 4) annoyance 5) quit
choice> 5
Bye.
What was typed (7 lines)
1
2
4
2
6
2
5
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P137 cohort retention: the cohort, period by period with means over
# those still here and over everyone, and the settings.
import cohort
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def t_text(t):
if t < 0:
return "-"
return str(int(t / 10)) + "." + str(t % 10)
def show(users):
(str(len(users)) + " users signed up together; how many have each tolerance:") ^0
"" => line
for v in [1:10]:
0 => c
for t in users:
if t == v:
c + 1 => c
if line != "":
line + " " => line
line + str(v) + ": " + str(c) => line
(" " + line) ^0
"Each period the product annoys a little more; a user whose tolerance it passes leaves." ^0
def table(users, step, count):
cohort.periods(users, step, count) => rows
" period annoyance here tolerance, here tolerance, all headroom, here leavers" ^0
for p in [0:len(rows) - 1]:
rows[p] => r
(" " + 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
0 => rose
0 => fell
0 => hup
0 => hdown
for p in [1:len(rows) - 1]:
if rows[p][1] > 0 and rows[p - 1][1] > 0:
if rows[p][2] > rows[p - 1][2]:
rose + 1 => rose
if rows[p][2] < rows[p - 1][2]:
fell + 1 => fell
if rows[p][4] > rows[p - 1][4]:
hup + 1 => hup
if rows[p][4] < rows[p - 1][4]:
hdown + 1 => hdown
("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
"Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured." ^0
("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
def ask_int(prompt, low, high):
plain(input(prompt)) => t
if t == "":
"Cancelled." ^0
return 0 - 1
if len(t) > 3:
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
for c in t:
if c < "0" or c > "9":
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
if int(t) < low or int(t) > high:
("Type a whole number from " + str(low) + " to " + str(high) + ".") ^0
return 0 - 1
return int(t)
40 => n
1 => step
10 => count
cohort.cohort(n) => users
"== Cohort retention ==" ^0
"Users who signed up together, a product that gets a little worse every period, and averages over those who stayed." ^0
True => running
while running:
"" ^0
"1) the cohort 2) period by period 3) users 4) annoyance 5) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show(users)
elif choice == "2":
table(users, step, count)
elif choice == "3":
ask_int("users (10 to 200)> ", 10, 200) => x
if x > 0:
x => n
cohort.cohort(n) => users
(str(n) + " users now.") ^0
elif choice == "4":
ask_int("annoyance added each period (1 to 3)> ", 1, 3) => a
if a > 0:
ask_int("periods (1 to 12)> ", 1, 12) => b
if b > 0:
a => step
b => count
("The annoyance now rises by " + str(step) + " a period, over " + str(count) + " periods.") ^0
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import cohort
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def t_text(t):
if t < 0:
return "-"
return str(int(t / 10)) + "." + str(t % 10)
def show(users):
print(str(len(users)) + " users signed up together; how many have each tolerance:")
line = ""
for v in range(1, 11):
c = 0
for t in users:
if t == v:
c = c + 1
if line != "":
line = line + " "
line = line + str(v) + ": " + str(c)
print(" " + line)
print("Each period the product annoys a little more; a user whose tolerance it passes leaves.")
def table(users, step, count):
rows = cohort.periods(users, step, count)
print(" period annoyance here tolerance, here tolerance, all headroom, here leavers")
for p in range(0, len(rows)):
r = rows[p]
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))
rose = 0
fell = 0
hup = 0
hdown = 0
for p in range(1, len(rows)):
if rows[p][1] > 0 and rows[p - 1][1] > 0:
if rows[p][2] > rows[p - 1][2]:
rose = rose + 1
if rows[p][2] < rows[p - 1][2]:
fell = fell + 1
if rows[p][4] > rows[p - 1][4]:
hup = hup + 1
if rows[p][4] < rows[p - 1][4]:
hdown = hdown + 1
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.")
print("Everyone who left had less tolerance than everyone who stayed: the product chose who is left to be measured.")
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.")
def ask_int(prompt, low, high):
t = plain(input(prompt))
if t == "":
print("Cancelled.")
return 0 - 1
if len(t) > 3:
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
for c in t:
if c < "0" or c > "9":
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
if int(t) < low or int(t) > high:
print("Type a whole number from " + str(low) + " to " + str(high) + ".")
return 0 - 1
return int(t)
n = 40
step = 1
count = 10
users = cohort.cohort(n)
print("== Cohort retention ==")
print("Users who signed up together, a product that gets a little worse every period, and averages over those who stayed.")
running = True
while running:
print("")
print("1) the cohort 2) period by period 3) users 4) annoyance 5) quit")
choice = plain(input("choice> "))
if choice == "1":
show(users)
elif choice == "2":
table(users, step, count)
elif choice == "3":
x = ask_int("users (10 to 200)> ", 10, 200)
if x > 0:
n = x
users = cohort.cohort(n)
print(str(n) + " users now.")
elif choice == "4":
a = ask_int("annoyance added each period (1 to 3)> ", 1, 3)
if a > 0:
b = ask_int("periods (1 to 12)> ", 1, 12)
if b > 0:
step = a
count = b
print("The annoyance now rises by " + str(step) + " a period, over " + str(count) + " periods.")
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
cohort.eml
eml# P137 cohort retention - users who signed up together, each with a
# tolerance from 1 to 10, and a product that annoys them a little more every
# period. A user leaves once the annoyance passes their tolerance. As in the
# corpus case survivorship-in-cohorts, a number worked out over the users
# who are still here is worked out over a sample the product picked: the
# mean tolerance of the survivors rises while the product gets worse, and
# over the whole cohort it does not move.
def mix(i, salt):
i * 7919 + salt * 104729 + 12345 => x
for k in [1:3]:
(x * x + 7) % 1000000007 => x
return x % 1000000
def cohort(n):
# tolerances from 1 to 10
[] => out
for i in [1:n]:
out + [1 + mix(i, 1) % 10] => out
return out
def tenths(p, q):
# p / q in tenths, rounded half up, for q > 0
int((20 * p + q) / (2 * q)) => t
while t * 2 * q > 20 * p + q:
t - 1 => t
while (t + 1) * 2 * q <= 20 * p + q:
t + 1 => t
return t
def periods(users, step, count):
# per period [annoyance, still here, mean tolerance of those here (in
# tenths, -1 if nobody), mean of the whole cohort, mean headroom of
# those here (tolerance - annoyance, tenths), leavers' mean tolerance
# (tenths, -1 if none left this period)]
[] => out
0 => s_all
for t in users:
s_all + t => s_all
[] => here
for t in users:
here + [t] => here
for p in [0:count - 1]:
step * p => annoy
[] => stay
[] => gone
for t in here:
if t >= annoy:
stay + [t] => stay
else:
gone + [t] => gone
stay => here
0 => s
for t in here:
s + t => s
0 - 1 => mean
0 - 1 => head
if len(here) > 0:
tenths(s, len(here)) => mean
tenths(s - annoy * len(here), len(here)) => head
0 - 1 => left
if len(gone) > 0:
0 => g
for t in gone:
g + t => g
tenths(g, len(gone)) => left
out + [[annoy, len(here), mean, tenths(s_all, len(users)), head, left]] => out
return out
Python projection (cohort.py)
def mix(i, salt):
x = i * 7919 + salt * 104729 + 12345
for k in range(1, 4):
x = (x * x + 7) % 1000000007
return x % 1000000
def cohort(n):
out = []
for i in range(1, n+1):
out = out + [1 + mix(i, 1) % 10]
return out
def tenths(p, q):
t = int((20 * p + q) / (2 * q))
while t * 2 * q > 20 * p + q:
t = t - 1
while (t + 1) * 2 * q <= 20 * p + q:
t = t + 1
return t
def periods(users, step, count):
out = []
s_all = 0
for t in users:
s_all = s_all + t
here = []
for t in users:
here = here + [t]
for p in range(0, count):
annoy = step * p
stay = []
gone = []
for t in here:
if t >= annoy:
stay = stay + [t]
else:
gone = gone + [t]
here = stay
s = 0
for t in here:
s = s + t
mean = 0 - 1
head = 0 - 1
if len(here) > 0:
mean = tenths(s, len(here))
head = tenths(s - annoy * len(here), len(here))
left = 0 - 1
if len(gone) > 0:
g = 0
for t in gone:
g = g + t
left = tenths(g, len(gone))
out = out + [[annoy, len(here), mean, tenths(s_all, len(users)), head, left]]
return out