유지율 삼각형을 세우고 세 함정을 숫자로 본다
목표
'모아노트' 의 주간 가입 코호트로 유지율 삼각형을 만들고, 분모·빈칸·생존자라는 세 함정을 숫자로 확인한 뒤 올바른 가중 유지율과 가입 경로별 유지율로 판단한다.
왜 중요한가
전체 활성 수는 새로 들어오는 사람과 떠나는 사람을 한 숫자에 섞는다. 같은 주에 가입한 사람들을 따로 따라가야 제품이 사람을 붙잡는지 보인다. 그리고 그 계산은 분모를 바꾸거나 빈칸을 0 으로 채우는 것만으로 쉽게 좋아지거나 나빠진다 — 광고비와 로드맵이 걸린 숫자라 정의대로 다시 계산되어야 한다.
재료와 정의
/opt/fixtures/founder/product/users.csv·events.csv(지표 모듈과 같은 재료), 가입 경로는/opt/fixtures/founder/product/touches.csv의 가입 전 마지막 접점(user_id,ts,channel에서 ts 가 가장 늦은 줄)의 channel.- 서울 시각 기준 주(월요일 00:00 시작), 0주차 = 2026-03-02, 관측은 15주차까지. 내부 계정(is_internal=1)은 뺀다.
- 코호트 c = 가입 시각의 주차(0~11). N주차 유지 = 주차 c+N 에 session_start 가 1번 이상. 분모는 코호트 가입자 수. c+N > 15 인 칸은 관측 전이라
null. - 모든 함수의 첫 인자는 재료 디렉터리다. 비율은 소수 넷째 자리 반올림.
단계
/root/founder/cohort/sizes.json에 코호트별 가입자 수를{"0": n, ..., "11": n}으로 쓴다./root/founder/cohort/cohort.py에retention(fix, max_n)을 만든다 —{"0": [r0, r1, ..., r_max_n], ...}, 관측 전 칸은None./root/founder/cohort/denominator.json에 코호트 4 의 4주차 유지율을 두 분모로 쓴다:rate_cohort_size(올바른 분모),rate_week0_active(0주차 활동자를 분모로 — 일부러 저지르는 실수)./root/founder/cohort/censor.json에 6주차 유지율을 두 방법으로 쓴다:pooled_rate(관측이 끝난 코호트만, 사람 수 가중),naive_rate_with_zeros(모든 코호트, 관측 전 칸을 0 으로), 그리고eligible_cohorts(관측이 끝난 코호트 수)./root/founder/cohort/survivor.json에 4주차 유지율을 두 명단으로 쓴다:all_users_rate(코호트 0~11 전원, 관측이 끝난 코호트만),survivors_only_rate(15주차에 활동한 사람만 골라 같은 계산),survivors(그 사람 수).cohort.py에pooled(fix, n, unbounded=False)를 더한다 — 관측이 끝난 코호트만 모은 사람 수 가중 유지율{"cohorts": k, "users": u, "retained": r, "rate": 0.0000}. unbounded 가 참이면 "c+N 주차 이후 아무 주에나(c+N 포함) 활동" 으로 센다./root/founder/cohort/channel.json에 가입 경로(마지막 접점)별 4주차 가중 유지율을{"organic_search": r, ...}다섯 채널로 쓴다./root/founder/cohort/report.json에curve(n=0..8 의 bounded 가중 유지율 9개),week4,week4_unbounded,flatten_week(전 주 대비 하락이 0.02 미만이 되는 첫 n, 1 이상),best_channel,worst_channel을 쓴다.
참고
- 주차:
(서울 시각 - datetime(2026, 3, 2, tzinfo=서울)).days // 7 - 사람별 '활동한 주차 집합' 을 먼저 만들어 두면 모든 단계가 집합 검사 한 번이다.
- 흔한 실수: 분모를 0주차 활동자로 잡기, 관측 전 칸을 0 으로 채워 평균하기, 코호트 비율을 단순 평균하기, 지금 남은 사람만 골라 과거를 보기.
코호트별 가입자 수
/root/founder/cohort/sizes.json 에 가입 주차(0~11)별 외부 가입자 수를 문자열 키로 쓴다.
signup_at 을 서울 시각으로 바꿔 2026-03-02 로부터 며칠인지 7 로 나눈 몫이 코호트입니다. 내부 계정은 뺍니다.
유지율 삼각형 — 관측 전 칸은 None
/root/founder/cohort/cohort.py 에 retention(fix, max_n) 을 만든다. 코호트 문자열 키 → n=0..max_n 유지율 목록, c+n>15 인 칸은 None.
사람별로 session_start 가 있던 주차 집합을 먼저 만드세요. 분모는 코호트 가입자 수이고, 관측이 끝나지 않은 칸은 0 이 아니라 None 입니다.
분모를 바꾸면 유지율이 좋아 보인다
/root/founder/cohort/denominator.json 에 코호트 4 의 4주차 유지율을 rate_cohort_size 와 rate_week0_active 두 분모로 쓴다.
분자는 같습니다(주차 8 에 활동한 코호트 4 사람). 분모만 가입자 수와 0주차(주차 4) 활동자 수로 바꿔 봅니다.
관측 전 칸을 0 으로 채우면 곡선이 가짜로 꺾인다
/root/founder/cohort/censor.json 에 6주차 유지율의 pooled_rate·naive_rate_with_zeros·eligible_cohorts 를 쓴다.
올바른 쪽은 c+6 ≤ 15 인 코호트만 모아 전체 유지자 ÷ 전체 가입자입니다. 틀린 쪽은 모든 코호트의 가입자를 분모에 넣습니다(관측 전 사람은 유지 0 으로 세어짐).
지금 남은 사람만 보면 과거가 좋아 보인다
/root/founder/cohort/survivor.json 에 4주차 가중 유지율을 all_users_rate(전원)와 survivors_only_rate(15주차 활동자만)로, 그리고 survivors(그 사람 수)를 쓴다.
두 계산은 코호트·주차 규칙이 같고 사람 명단만 다릅니다. 명단은 가입 시점에 고정하는 것이 원칙이라, 두 번째 숫자는 일부러 저지르는 실수입니다.
가중 유지율 함수 — bounded 와 unbounded
cohort.py 에 pooled(fix, n, unbounded=False) 를 더한다. 관측이 끝난 코호트만, 사람 수 가중, {cohorts, users, retained, rate} 를 돌려준다.
코호트 비율의 단순 평균이 아니라 유지자 합 ÷ 가입자 합입니다. unbounded 는 c+n 주차와 그 뒤 어느 주에든 활동했는지를 봅니다.
가입 경로별 4주차 유지율
/root/founder/cohort/channel.json 에 마지막 접점 채널 다섯 개 각각의 4주차 가중 유지율을 쓴다.
touches.csv 에서 사람마다 ts 가 가장 늦은 줄의 channel 이 가입 경로입니다. 명단만 그 채널 사람으로 좁혀 pooled 와 같은 계산을 합니다.
곡선과 판단을 한 장에
/root/founder/cohort/report.json 에 curve(n=0..8), week4, week4_unbounded, flatten_week, best_channel, worst_channel 을 쓴다.
flatten_week 는 n=1 부터 보며 curve[n-1] − curve[n] < 0.02 인 첫 n 입니다. 채널은 7단계 결과에서 가장 높은 것과 낮은 것입니다.