LTV по когортам считают на равном возрасте: сравнивают только те ячейки, где каждый исходный клиент уже прожил полное окно 30 или 60 дней к дате среза. В статье — учебный протокол с полным журналом, шестью ячейками, правилами исключений и копируемым промптом для ИИ. Ниже все входы вымышлены: Учебный пример, а не клиентский кейс и не испытание модели. Ценность здесь — наблюдаемая маржинальная contribution на одного исходного клиента за окно H дней, а не полный lifetime и не прогноз.
Ответ сначала
На одном H сравнивать можно только зрелые ячейки. В учебном примере готовы три значения: январь на 30 днях — 175 ₽ на клиента, январь на 60 днях — 185 ₽, февраль на 30 днях — 120 ₽. Остальные три ячейки — null, потому что к дате среза не все их клиенты прожили окно целиком: февральская когорта не дожила до 60 дней, а мартовская не закрыла ни 30, ни 60 дней из-за клиента D с первой оплатой 20 марта.
Три готовых значения — не наблюдение полного lifetime. Это накопленная contribution за фиксированный отрезок от первой оплаты каждого клиента. Разница между 175 и 185 показывает, что произошло за следующий месяц жизни январской когорты, но не говорит, сколько она принесёт дальше и окупила ли привлечение.
Полный вход и политика
Все входы ниже — единый учебный набор. Реестр клиентов содержит пять записей с идентификатором и датой первой оплаты. Журнал транзакций содержит двенадцать строк с идентификатором строки, клиентом, датой, выручкой и переменной себестоимостью.
{
"customers_columns": ["customer_id", "first_paid"],
"customers": [
["A", "2026-01-01"],
["B", "2026-01-15"],
["E", "2026-02-01"],
["C", "2026-03-01"],
["D", "2026-03-20"]
],
"ledger_columns": ["row_id", "customer_id", "date", "receipts_rub", "variable_cost_rub"],
"ledger": [
["a1", "A", "2026-01-01", 200, 100],
["a2", "A", "2026-01-30", 100, 50],
["a2", "A", "2026-01-30", 100, 50],
["a3", "A", "2026-01-31", 100, 60],
["a4", "A", "2026-02-15", -20, 0],
["b1", "B", "2026-01-15", 300, 100],
["b2", "B", "2026-03-16", 200, 100],
["e1", "E", "2026-02-01", 200, 80],
["e2", "E", "2026-03-03", 100, 20],
["c1", "C", "2026-03-01", 500, 200],
["d1", "D", "2026-03-20", 500, 100],
["x1", "X", "2026-01-01", 100, 0]
],
"rules": {
"synthetic": true,
"timezone": "UTC",
"snapshot_exclusive": "2026-04-01",
"registry": "Полная для примера авторитетная история первых оплат; даты first_paid не угадываются по ограниченному ledger.",
"cohort": "Календарный месяц first_paid; клиент закреплён один раз.",
"metric": "observed_contribution_per_initial_customer_Hd, не полный lifetime и не прогноз",
"contribution": "receipts_rub-variable_cost_rub; расходы привлечения, фиксированные расходы, дисконтирование не включены. Это заданная редакционная метрика, не универсальное определение валовой прибыли.",
"window": "Для клиента [first_paid, first_paid+Hdays), H30/60; календарные месяцы не заменяют30дней.",
"maturity": "Ячейка когорты зрелая только если first_paid+Hdays<=snapshot для ВСЕХ исходных клиентов; иначе сумма и среднее null.",
"denominator": "Все исходные клиенты когорты, не только повторившие покупку.",
"refund": "a4: возврат уменьшает receipts на фактическую дату возврата; себестоимость не восстанавливается в этом примере. Это политика ledger, не универсальная финансовая политика.",
"dedup": "Точный дубль row_id=a2 один раз; конфликт того жеID блокирует расчёт. Неизвестный X исключается отдельно, не создаёт клиента.",
"claims": "Не сравнивать разныеH, не считать незрелое 0, не прогнозировать, не делать причинный вывод/решение о канале."
}
}
Политика задаёт срез и границы. Срез — исключительная граница 2026-04-01 по UTC: транзакция, датированная ровно этим днём, в пример не попадает. Когорта — календарный месяц первой оплаты, клиент закрепляется один раз. Contribution — выручка минус переменная себестоимость; расходы привлечения, фиксированные расходы и дисконтирование не включены. Это заданная редакционная метрика, не универсальное определение валовой прибыли.
Окно — полуоткрытый отрезок от даты первой оплаты до неё же плюс H дней, граница начала включается, граница конца нет. Знаменатель — все исходные клиенты когорты, а не только те, кто купил повторно. Зрелость считается по худшему клиенту когорты: если хотя бы один исходный клиент не прожил окно к срезу, вся ячейка остаётся null. Себестоимость при возврате в этом примере не восстанавливается; это политика журнала, а не универсальная финансовая норма.
Сводный реестр учебного примера:
| customer_id | first_paid | когорта |
|---|---|---|
| A | 2026-01-01 | 2026-01 |
| B | 2026-01-15 | 2026-01 |
| E | 2026-02-01 | 2026-02 |
| C | 2026-03-01 | 2026-03 |
| D | 2026-03-20 | 2026-03 |
Расчёт и весь эталон
Дубли и неизвестные клиенты разбираются до любых сумм. Строка a2 встречается в журнале дважды с одинаковыми полями, поэтому считается один раз. Клиент X есть в журнале, но отсутствует в реестре первых оплат, поэтому строка x1 исключается отдельно и клиента не создаёт. Конфликт того же идентификатора строки с разными полями остановил бы расчёт, но в этом наборе его нет.
Граничные строки показывают, зачем нужно полуоткрытое окно. У клиента A первая оплата 1 января. Строка a3 датирована 31 января — это ровно 30 суток, и в 30-дневное окно она не входит, а в 60-дневное входит. У клиента B первая оплата 15 января, строка b2 датирована 16 марта — это ровно 60 суток, и в 60-дневное окно она не входит. Возврат a4 от 15 февраля уменьшает выручку A внутри 60-дневного окна на 20 ₽.
Ячейка за январь на 30 днях: клиенты A и B, знаменатель 2. В окно попадают строки a1, a2 (один раз) и b1. Contribution по строкам: 100, 50 и 200, итого 350 ₽, среднее 175 ₽.
Ячейка за январь на 60 днях: те же два клиента, знаменатель 2. В окно попадают a1, a2 (один раз), a3, a4 и b1. Contribution: 100, 50, 40, минус 20 и 200, итого 370 ₽, среднее 185 ₽.
Ячейка за февраль на 30 днях: клиент E, знаменатель 1. В окно попадает e1: contribution 120 ₽, значение 120 ₽.
Февраль на 60 днях: у E окно заканчивается 2 апреля, позже среза, ячейка незрелая, значения null. Март на 30 и 60 днях: у D 30-дневное окно заканчивается 19 апреля, 60-дневное — 19 мая, оба позже среза, обе ячейки незрелые, значения null.
| Когорта | H, дней | customer_ids | Знаменатель | row_ids | Contribution, ₽ | На клиента, ₽ | Статус |
|---|---|---|---|---|---|---|---|
| 2026-01 | 30 | A, B | 2 | a1, a2, b1 | 350 | 175 | mature |
| 2026-01 | 60 | A, B | 2 | a1, a2, a3, a4, b1 | 370 | 185 | mature |
| 2026-02 | 30 | E | 1 | e1 | 120 | 120 | mature |
| 2026-02 | 60 | E | 1 | null | null | null | immature |
| 2026-03 | 30 | C, D | 2 | null | null | null | immature |
| 2026-03 | 60 | C, D | 2 | null | null | null | immature |
Исключённые строки: a2 с причиной duplicate, x1 с причиной unknown_customer. Никакого выбора только повторных покупателей в знаменателе нет: в знаменатель январской когорты входят все исходные клиенты A и B, независимо от того, повторят ли они покупку; в 30-дневное окно у A попадает a1 и a2 (один раз), у B — b1.
Копируемый промпт
Промпт ниже — не команда сверить ответ с уже известным эталоном, а самостоятельная постановка задачи. Он передаёт все входы и правила и требует собственный расчёт. Запуск кода не предполагается.
Ты проверяешь учебную наблюдаемую ценность клиента. Все данные вымышлены.
Примени ровно переданные rules.
Верни 6 ячеек cohort, H, customer_ids, denominator, row_ids, contribution_sum,
value, status; отдельный excluded с ID/причиной и not_established.
Для незрелой ячейки оставь row_ids, сумму и value null.
Покажи суммы через ID, не используй скрытый эталон.
Не сравнивай H30 с H60, не выдумывай lifetime, причинность или норму.
Если запуск кода недоступен, не утверждай, что выполнял его.
Полный вход:
{"customers_columns": ["customer_id", "first_paid"], "customers": [["A", "2026-01-01"], ["B", "2026-01-15"], ["E", "2026-02-01"], ["C", "2026-03-01"], ["D", "2026-03-20"]], "ledger_columns": ["row_id", "customer_id", "date", "receipts_rub", "variable_cost_rub"], "ledger": [["a1", "A", "2026-01-01", 200, 100], ["a2", "A", "2026-01-30", 100, 50], ["a2", "A", "2026-01-30", 100, 50], ["a3", "A", "2026-01-31", 100, 60], ["a4", "A", "2026-02-15", -20, 0], ["b1", "B", "2026-01-15", 300, 100], ["b2", "B", "2026-03-16", 200, 100], ["e1", "E", "2026-02-01", 200, 80], ["e2", "E", "2026-03-03", 100, 20], ["c1", "C", "2026-03-01", 500, 200], ["d1", "D", "2026-03-20", 500, 100], ["x1", "X", "2026-01-01", 100, 0]], "rules": {"synthetic": true, "timezone": "UTC", "snapshot_exclusive": "2026-04-01", "registry": "Полная для примера авторитетная история первых оплат; даты first_paid не угадываются по ограниченному ledger.", "cohort": "Календарный месяц first_paid; клиент закреплён один раз.", "metric": "observed_contribution_per_initial_customer_Hd, не полный lifetime и не прогноз", "contribution": "receipts_rub-variable_cost_rub; расходы привлечения, фиксированные расходы, дисконтирование не включены. Это заданная редакционная метрика, не универсальное определение валовой прибыли.", "window": "Для клиента [first_paid, first_paid+Hdays), H30/60; календарные месяцы не заменяют 30 дней.", "maturity": "Ячейка когорты зрелая только если first_paid+Hdays<=snapshot для ВСЕХ исходных клиентов; иначе сумма и среднее null.", "denominator": "Все исходные клиенты когорты, не только повторившие покупку.", "refund": "a4: возврат уменьшает receipts на фактическую дату возврата; себестоимость не восстанавливается в этом примере. Это политика ledger, не универсальная финансовая политика.", "dedup": "Точный дубль row_id=a2 один раз; конфликт того же ID блокирует расчёт. Неизвестный X исключается отдельно, не создаёт клиента.", "claims": "Не сравнивать разные H, не считать незрелое 0, не прогнозировать, не делать причинный вывод/решение о канале."}}
Что должно получиться после такого промпта, если модель поняла правила: три зрелые ячейки со значениями 175, 185 и 120 ₽, три незрелые с null, исключения a2 и x1, а в списке неустановленного — полный lifetime, прогноз, причины различий, окупаемость CAC и качество модели. Именно по этим пунктам и стоит сверять ответ, а не по форме изложения.
Пределы и следующий срез
Непроверяемый вывод из такого расчёта — полный lifetime. Окно 30 или 60 дней даёт только наблюдаемую contribution за отрезок; экстраполяция вперёд требует допущений, которых в наборе нет. Второй недопустимый вывод — причинный. Разница между январской когортой на 60 днях и на 30 днях не доказывает эффект канала, продукта или акции; причины различий здесь не установлены.
Третий предел — малые группы. Когорта из одного или двух клиентов — не бизнес-норма и не основание отключать канал. Когорты из разных продуктов, регионов или сезонов сравнивать напрямую на одном графике нельзя: разница может отражать состав, а не поведение.
Нулевую ячейку не прогнозируют. Незрелое значение остаётся null, и следующий срез берётся после того, как окно закроется у всех исходных клиентов когорты. Для февральской когорты это 2 апреля, для мартовской — 19 апреля при H 30 и 19 мая при H 60; проверку этих дат и стоит сделать следующим шагом.
Для соседних задач — когортный анализ по датам и окнам и RFM-сегментация. Первый помогает закрепить определение когорты и границы окон, второй — разложить клиентов по давности и частоте, но ни один не заменяет денежную ячейку на равном возрасте. В справке Google Analytics о когортном исследовании когорта описана как группа пользователей с общим свойством, например одинаковой датой первого посещения. Критерий включения, то есть условие добавления в когорту, и критерий возврата задаются отдельно в той же справке. Эти когорты устроены иначе, чем журнал с customer_id и денежными строками в примере выше.

