← Все курсы · Курс SQL · Все руководства
Анализ удержания когорты в SQL, шаг за шагом
К концу этой страницы вы можете отнести каждого клиента к тому месяцу, в котором он прибыл, измерить, сколько из каждой группы прибытия возвращается в каждом последующем месяце, построить треугольник, который это показывает, и точно сказать, какое из двух разумных определений «сохраненного» использует ваш номер. Прошло около двадцати пяти минут, и каждый запрос и результат, представленные ниже, были выполнены.
Вот что нужно сделать сегодня, прежде чем писать что-либо из этого. Запишите, что будет означать одна строка вашей готовой таблицы, в предложении с двумя датами: «из клиентов, чья первая покупка была в месяце X, столько же купили снова в месяце Y». Все на этой странице — это предложение, преобразованное в SQL, и большинство чисел удержания, которые оказались неправильными, были неправильными в предложении.
Вкратце: пометьте каждого клиента по его первому месяцу, пометьте каждое событие, через сколько месяцев оно произошло, а затем посчитайте отдельных клиентов по паре.
Форма, к которой вы стремитесь, — это идея, поэтому она отражает картину.
- Что такое когорта и решение, которое ее определяет
- Шаг первый: первый месяц для всех
- Шаг второй: сколько месяцев спустя
- Шаг третий: треугольник
- Засчитывается в счет ставок
- Клиенты, а не покупки
- Два определения сохраненного
- Сохранение доходов
- Полное до и после
- Пограничные случаи, нарушающие когортную таблицу
- Почему это работает
- Использование этого в вашем собственном проекте
- Все на одном экране
1. Что такое когорта и решение, которое ее определяет
Когорта — это группа клиентов, у которых есть общая отправная точка, и почти всегда этой отправной точкой является момент их прибытия. Сгруппируйте по месяцам прибытия, а затем наблюдайте за каждой группой с течением времени. Причина, по которой это вообще нужно делать, заключается в том, что в одном общем числе удержанных смешиваются люди, присоединившиеся на прошлой неделе, и люди, присоединившиеся три года назад, и это разные группы населения, имеющие разный опыт.
Перед пояснением: в вашей таблице указаны дата первой покупки, дата регистрации и дата первого входа в систему. Скажите, какой из них определяет когорту.
Какой бы из них вы ни смогли защитить на собрании, и вам придется выбрать его перед написанием SQL, потому что впоследствии ответ меняется на каждое число. Первая покупка подходит для удержания вопроса о платежном поведении. Регистрация подойдет, если вы измеряете, конвертирует ли продукт людей. Это не те же самые клиенты и не те же месяцы, и таблица, в которой написано «когорта», не говоря уже об этом, не является выводом. Записать это - это все определение метрики, применяется здесь.
На этой странице используется первая покупка в таблице покупок с тремя столбцами: клиент, дата, сумма. Девятнадцать рядов, двенадцать клиентов.
2. Шаг первый: первый месяц для всех
Один агрегат дает каждому клиенту группу, и это единственное место, где слово MIN появляется.
SELECT customer_id,
MIN(purchase_date) AS first_purchase,
DATE_TRUNC('month', MIN(purchase_date))::DATE AS cohort
FROM purchases
GROUP BY 1;
C1 | 2026-01-06 | 2026-01-01
C2 | 2026-01-09 | 2026-01-01
C3 | 2026-01-14 | 2026-01-01
C4 | 2026-01-21 | 2026-01-01
C5 | 2026-01-28 | 2026-01-01
C6 | 2026-02-03 | 2026-02-01
...
Две вещи, на которые стоит обратить внимание. Когорта представляет собой Дата, первое число месяца, а не строка, по всем причинам, указанным в руководство по датам: он сортируется в хронологическом порядке, его можно вычесть и присоединить к календарю. И каждый покупатель появляется ровно один раз, поэтому эту таблицу можно безопасно присоединить к покупкам, ничего не умножая.
Подсчет дает знаменатели для всего анализа:
2026-01-01 | 5
2026-02-01 | 4
2026-03-01 | 3
Пять, четыре и три, что в сумме дает двенадцать, количество клиентов. Проверяйте эту сумму каждый раз. Если размер когорты не соответствует количеству ваших клиентов, значит, кто-то сделал две первые покупки, а это означает, что ключ клиента не уникален.
3. Шаг второй: сколько месяцев спустя
Соедините таблицу первых месяцев с каждой покупкой и вычтите месяцы.
SELECT p.customer_id,
f.cohort,
DATE_TRUNC('month', p.purchase_date)::DATE AS activity_month,
DATEDIFF('month', f.cohort, DATE_TRUNC('month', p.purchase_date)) AS month_no
FROM purchases p
JOIN first_p f USING (customer_id);
C1 | 2026-01-01 | 2026-01-01 | 0
C1 | 2026-01-01 | 2026-02-01 | 1
C1 | 2026-01-01 | 2026-03-01 | 2
C3 | 2026-01-01 | 2026-01-01 | 0
C3 | 2026-01-01 | 2026-02-01 | 1
C6 | 2026-02-01 | 2026-02-01 | 0
C6 | 2026-02-01 | 2026-03-01 | 1
...
что month_no столбец — вот и вся хитрость. Он преобразует календарное время во время с момента прибытия, поэтому клиента, присоединившегося в январе, и клиента, присоединившегося в марте, можно сравнить в одном возрасте. Без него вы можете сравнивать только календарные месяцы, что смешивает возрасты и является причиной того, что простой ежемесячный подсчет активности никогда не отвечает на вопрос об удержании.
Две детали. Усеките обе части до месяца перед вычитанием, иначе покупка 2-го числа следующего месяца и покупка 30-го числа того же месяца окажутся в разных корзинах с разницей в день. А месяц 0 существует по своей конструкции: все активны в том месяце, в котором они прибыли, поэтому первый столбец когортной таблицы всегда указывает размер когорты.
4. Шаг третий: треугольник
Теперь посчитайте отдельных клиентов на когорту за месяц. Здесь он изложен так, как его читают люди, с номерами месяцев вверху:
cohort | m0 | m1 | m2
2026-01-01 | 5 | 3 | 2
2026-02-01 | 4 | 2 | .
2026-03-01 | 3 | . | .
Прочтите первую строку: пять клиентов пришли в январе, трое из них купили снова в феврале, двое купили в марте. Сверьте его вручную со списком на третьем этапе, и вы найдете C1, C2 и C3 в первом месяце, а также C1 и C2 во втором месяце.
Пробелы в правом нижнем углу не являются недостающими данными. Февральская когорта существует всего два месяца, поэтому у нее еще нет месяца 2, а у марта нет месяца 1. Это самая важная вещь, которую нужно сделать правильно при представлении одного из них, потому что заполнение этих ячеек нулями превращает «мы еще не знаем» в «никто не вернулся», а затем среднее значение по столбцу падает по причине, не имеющей ничего общего с продуктом.
Запрос один CASE за столбец, что подходит для трех и громоздко для двенадцати. Версия, которая масштабируется, вместо этого возвращает одну строку на ячейку и позволяет инструменту создания отчетов расположить ее:
SELECT cohort, month_no, COUNT(DISTINCT customer_id) AS active
FROM act
GROUP BY 1, 2
ORDER BY 1, 2;
5. Учитывается в ставках
Подсчеты — это то, что вы проверяете; цены - это то, что вы сравниваете. Объединение размеров когорт обратно дает и то, и другое.
cohort | size | month_no | active | retention_pct
2026-01-01 | 5 | 0 | 5 | 100.0
2026-01-01 | 5 | 1 | 3 | 60.0
2026-01-01 | 5 | 2 | 2 | 40.0
2026-02-01 | 4 | 0 | 4 | 100.0
2026-02-01 | 4 | 1 | 2 | 50.0
2026-03-01 | 3 | 0 | 3 | 100.0
Месяц 0 по определению равен 100% в каждой строке, что является скорее полезной проверкой, чем выводом: если он не равен 100%, объединение теряет строки.
Теперь сравнение, для которого существует таблица. Январь сохранил 60% в первом месяце, а февраль сохранил 50%. Скажите вслух, действительно ли это снижение, прежде чем читать дальше.
Это три клиента против двух в когортах из пяти и четырех человек. Перемещение одного клиента меняет январь на 20 пунктов, а февраль — на 25. На основе этих данных честным выводом является то, что когорты слишком малы для сравнения, и вы говорите об этом, публикуя подсчеты рядом со ставками, поэтому в таблице выше есть и то, и другое. Проценты в крошечных знаменателях — это наиболее явно неправильные цифры в аналитике.
6. Клиенты, а не покупки
Перед пояснением: один январский покупатель покупает в январе дважды. Скажите, что это делает с ячейкой месяца 0, если вы считаете с помощью COUNT(*).
Добавление именно этого клиента в данные и параллельное выполнение обоих подсчетов:
cohort | month_no | purchases | customers
2026-01-01 | 0 | 7 | 6
2026-01-01 | 1 | 3 | 3
2026-01-01 | 2 | 2 | 2
Семь покупок, шесть клиентов. В когорте шесть человек и COUNT(*) утверждает семь, поэтому месяц 0 получается со значением 116,7%, а все, что ниже него, разделено на неверный знаменатель.
Удержание — это вопрос о людях, поэтому каждый показатель в когортной таблице COUNT(DISTINCT customer_id). Единственное исключение — это когда вы действительно имеете в виду частоту заказов, и тогда столбец следует называть иначе, чем «Удержание».
7. Два определения оставшегося
Это та часть, которая решает, согласны ли две команды, и она невидима в SQL, если вы ее не ищете.
Добавьте в январскую когорту одного клиента, который покупает в январе, ничего не покупает в феврале и возвращается в марте. Когорта увеличилась с пяти до шести, и треугольник теперь гласит:
cohort | size | month_no | active | retention_pct
2026-01-01 | 6 | 0 | 6 | 100.0
2026-01-01 | 6 | 1 | 3 | 50.0
2026-01-01 | 6 | 2 | 3 | 50.0
Удержание не упало между месяцем 1 и месяцем 2. Согласно этому определению, его вообще нельзя рассматривать как кривую выживания, потому что клиент, который терпит неудачу и возвращается, учитывается снова. У этого определения есть имя, которое стоит использовать: активен в месяце N.
Другое определение спрашивает, кто проявлял активность каждый месяц с момента прибытия. По тем же шести клиентам:
cohort_size | active in m2 | active every month
6 | 3 | 2
Трое против двух, что составляет 50% против 33,3% для той же когорты в том же месяце. Ни один из запросов не является неправильным. Они отвечают на разные вопросы, и разница — один клиент, который взял отпуск на месяц.
Что использовать, зависит от бизнеса. Для подписки, срок действия которой означает отмену, фраза «активна каждый месяц с тех пор» ближе к истине. Для магазина, где люди покупают, когда им что-то нужно, «активен в месяце N» является разумным, и пробел не является убытком. Запишите выбор в заголовок столбца, а не в комментарий, поскольку заголовок столбца — единственная перемещаемая часть.
8. Сохранение доходов
Тот же скелет с SUM(amount) вместо подсчета отвечает на другой и зачастую более полезный вопрос: не сколько вернулось, а сколько.
cohort | month_no | revenue
2026-01-01 | 0 | 610
2026-01-01 | 1 | 380
2026-01-01 | 2 | 250
2026-02-01 | 0 | 460
2026-02-01 | 1 | 310
2026-03-01 | 0 | 420
Эти шесть ячеек в сумме дают 2430, то есть общую сумму таблицы покупок, поэтому никакие деньги не были потеряны или пересчитаны на пути через объединение. Это сверка является проверкой, которую необходимо сохранить: каждая ячейка таблицы доходов когорты, сложенная вместе, должна равняться исходной сумме.
Если прочитать его рядом со таблицей клиентов, он заслуживает своего места. Январь сохранила 60% клиентов в первый месяц и 62,3% выручки, 380 из 610, поэтому те, кто остался, тратили примерно на том же уровне. Когда эти два процента резко расходятся, получается следующее: сохранение половины ваших клиентов и 90% ваших денег — это совсем другое дело, чем сохранение половины каждого из них.
Представьте на мгновение свой собственный стол для клиентов. Если бы вы построили это сегодня, что бы вы ожидали упасть быстрее: количество или доход? Это ожидание стоит записать, прежде чем запускать его.
Полное до и после
Тот же вопрос: удерживаем ли мы клиентов, которых получаем?
До
SELECT DATE_TRUNC('month', purchase_date) AS month,
COUNT(DISTINCT customer_id) AS active
FROM purchases GROUP BY 1 ORDER BY 1;
2026-01-01 | 5
2026-02-01 | 7
2026-03-01 | 7
Ежемесячно активные клиенты, что выглядит как рост и ничего не говорит об удержании. Он смешивает новых клиентов с возвращающимися, поэтому месяц может оставаться неизменным, пока каждый существующий клиент уйдет и будет заменен. Нет ни знаменателя, ни возраста, ни способа отличить эти две истории друг от друга.
после
WITH first_p AS (
SELECT customer_id, DATE_TRUNC('month', MIN(purchase_date))::DATE AS cohort
FROM purchases GROUP BY 1 -- 12 customers
), sizes AS (
SELECT cohort, COUNT(*) AS n FROM first_p GROUP BY 1 -- 5, 4, 3
), act AS (
SELECT f.cohort,
DATEDIFF('month', f.cohort, DATE_TRUNC('month', p.purchase_date)) AS month_no,
p.customer_id
FROM purchases p JOIN first_p f USING (customer_id) -- 19 rows, unchanged
), grid AS (
SELECT cohort, month_no, COUNT(DISTINCT customer_id) AS active
FROM act GROUP BY 1, 2
)
SELECT g.cohort, s.n AS cohort_size, g.month_no, g.active,
ROUND(100.0 * g.active / s.n, 1) AS retention_pct
FROM grid g JOIN sizes s USING (cohort)
ORDER BY g.cohort, g.month_no;
Четыре именованных шага с количеством строк в каждом, знаменателем для каждой ставки и столбцом месяца 0 со значением 100 % в качестве встроенной проверки. Треугольник честен в том, что еще не произошло.
Заявление, и именно оно является причиной существования седьмого раздела: в одной и той же январской когорте в том же месяце сохраняется 50% или 33,3%, в зависимости от того, какое определение слова вы использовали, а длина SQL для обоих составляет четыре строки.
Пограничные случаи, нарушающие когортную таблицу
Шесть, которые пройдут.
Нули в будущих ячейках. A LEFT JOIN в полную сетку пар когорт по месяцам поможет заполнить все пробелы нулями, включая месяцы, которые еще не произошли. Фильтровать по cohort + month_no <= the last complete month прежде чем кто-либо усреднит столбец.
Неполный текущий месяц. Самый последний столбец всегда является неполным, поэтому он всегда выглядит как свернувшийся. Либо исключите это, либо назовите это, та же дисциплина, что и неполный месяц в тренде.
Ключ клиента — это не один клиент. Если люди могут создать две учетные записи, вторая создает новую когорту, а первая, похоже, уходит. То есть Разрешение объекта, и это одновременно льстит оттоку и снижает удержание клиентов.
Возвраты и отмены учитываются как активность. Строка возврата — это строка покупки в большинстве схем. Решите, считается ли клиент, который только что-то вернул, удержанным, и отфильтруйте его до этапа когорты, а не после него.
Заполненная история. Если старые данные были перенесены с указанием даты загрузки, а не даты события, каждый перенесенный клиент присоединяется к одной и той же фальшивой когорте, которая выглядит как одна огромная группа со странным сроком хранения. Прежде чем верить чему-либо, проверьте размеры когорт на предмет подозрительного всплеска.
Крошечные когорты. Когорта из четырех человек не может дать процент, достойный сравнения. Опубликуйте подсчеты и установите нижний предел, ниже которого вы будете показывать только подсчеты, тот же аргумент, что и нижний предел для малых групп в GROUP BY и HAVING.
Почему это работает
Когортная таблица представляет собой анализ выживаемости с учетом статистики. Причина, по которой это должен быть треугольник, та же самая, по которой исследования выживаемости существуют как отдельная область: новейшие субъекты не наблюдались так долго, как самые старые, поэтому их более поздние результаты скорее неизвестны, чем отрицательны. Стандартный подход к этой ситуации, оценивающий выживаемость по записям, в которых за некоторыми субъектами просто не наблюдали достаточно долго, - это то, что изложили Каплан и Мейер, и ключевой шаг заключается в том, что ненаблюдаемый период должен быть исключен, а не засчитан как неудача (Каплан & Мейер, 1958, Журнал Американской статистической ассоциации, 53(282), 457–481). Заполнение нижних правых ячеек нулями — это именно та ошибка, которую призван избежать их метод, и это самая распространенная ошибка в колоде удержания.
Другая половина страницы, два определения, вообще не является проблемой SQL. Анализ клиентской базы рассматривает «все еще клиент» как нечто, что нужно моделировать, а не наблюдать, потому что в условиях отсутствия контракта никто не говорит вам, что они ушли; разрыв в покупках соответствует как оттоку, так и тому, что клиент совершает покупки дважды в год (Фейдер & Харди, 2009, Журнал интерактивного маркетинга, 23(1), 61–69). Это как раз тот клиент из седьмого отдела, который пропустил февраль. Будут ли они считаться сохраненными в марте, это выбор моделирования, и самое честное, что нужно сделать в отчете, — это указать этот выбор в заголовке столбца.
Одно замечание в шпаргалке ниже. Он создан для того, чтобы его просматривали и вспоминали, а не перечитывали, потому что сам акт тестирования сам по себе является обучающим мероприятием, а не просто его измерением (Краснее & выбирать карпа, 2006, Перспективы психологической науки, 1(3), 181–210).
Использование этого в вашем собственном проекте
Создание всего за один раз — вот почему групповые запросы оказываются непроверяемыми. Вместо этого сделайте это по порядку.
- Сначала напишите предложение. «Из клиентов, чей первый X был в месяце A, многие сделали Y в месяце B». Обе половины нуждаются в определении.
- Создайте первый шаг самостоятельно и убедитесь, что размеры когорт увеличивают количество ваших клиентов.
- Создайте второй шаг самостоятельно и убедитесь, что количество строк по-прежнему равно количеству событий, с которых вы начали. Соединение, которое меняет, что-то умножает.
- Убедитесь, что столбец месяца 0 равен 100 %. прежде чем смотреть на что-либо еще.
- Публикация имеет значение помимо ставок, и скройте процент тех случаев, когда когорта слишком мала, чтобы ее поддерживать.
- Отметьте неполные ячейки как неизвестные, никогда не равен нулю и исключает текущий неполный месяц.
Если у вас под рукой есть бумага, один дополнительный эскиз стоит пяти минут. Нарисуйте пустой треугольник с вашими месяцами по бокам и номерами месяцев вверху, затем заштрихуйте ячейки, которые могут содержать числа сегодняшнего дня. Незаштрихованная область — это то, что ваш запрос не должен заполнять, и нарисовать ее быстрее, чем спорить о ней после того, как колода построена.
Все на одном экране
Это лист поиска. Закройте правую колонку, проработайте левую и произнесите каждый ответ вслух, прежде чем проверять его.
| вещь | Что он делает |
|---|---|
| когорта | Клиенты, у которых общий стартовый месяц. |
| Первое решение | Какое событие запускает часы: регистрация, первая покупка, первый вход. |
| Шаг первый | MIN(date) за клиента, сокращенно до месяца. |
| Размер когорты | Должно увеличиться количество ваших клиентов. |
| Шаг второй | DATEDIFF('month', cohort, activity_month). |
| Обрезать обе стороны | Или покупка, сделанная с разницей в день, попадает в другую корзину. |
| Месяц 0 | Всегда размер когорты. Всегда 100%. Проверка, а не вывод. |
| Шаг третий | COUNT(DISTINCT customer_id) на когорту в месяц. |
| Никогда НЕ СЧИТАЙТЕ(*) | Две покупки за один месяц — это 117% удержания. |
| Пустые ячейки | Месяцы, которых не было. Не нули. |
| активен в месяце N | Учитывает возвращающегося просроченного клиента. Можно подняться. |
| Активен каждый месяц | Кривая выживания. Можно только спуститься. |
| Пропасть между ними | В той же когорте здесь 50% против 33,3%. |
| Сохранение доходов | Тот же скелет, SUM(amount). Ячейки должны добавляться к исходной сумме. |
| Читаем их вместе | Число клиентов сократилось на 40%, а выручка упала на 38% — это совсем другая история, чем 40% и 10%. |
| Маленькие когорты | Один клиент поднимает ставку на двадцать пунктов. Покажите счет. |
| Текущий месяц | Частичный, поэтому всегда выглядит как обвал. Исключить или пометить. |
Еще одна мысль, и мне бы искренне хотелось услышать ответы других людей. Тот, который я помню, представлял собой колоду удержания, где последние три когорты были заполнены нулями за месяцы, которые они не прожили, поэтому каждая диаграмма была наклонена вниз вправо, и вся компания потратила четверть, объясняя снижение, которое было артефактом запроса. Что такого в запросе вам сказала когортная таблица?
Ссылки
- Каплан Э.Л. и Мейер П. (1958). Непараметрическая оценка по неполным наблюдениям. Журнал Американской статистической ассоциации, 53(282), 457–481.
- Фейдер, П.С., и Харди, Б.Г.С. (2009). Вероятностные модели для анализа клиентской базы. Журнал интерактивного маркетинга, 23(1), 61–69.
- Рёдигер, Х.Л., и Карпике, Дж.Д. (2006). Сила проверки памяти: фундаментальные исследования и последствия для образовательной практики. Перспективы психологической науки, 1(3), 181–210.
SQL для аналитиков составляет 458 страниц, групповые запросы создают одно соединение за раз, поэтому у каждого процента есть знаменатель, который вы можете назвать.
SQL для аналитиков, 19 $ →тот Курс SQL дает вам повторения: таблицы первых событий, которые нужно построить, номера месяцев, которые нужно вычислить, треугольники, которые нужно собрать, и карточки с информацией о том, какой именно счет принадлежит какой ячейке. Теперь вы владеете методом; комплект - это практика. Ничего устанавливать, учетная запись не требуется.
Откройте набор SQL →