以下情境为教学假设,人物与数字不代表经核实的真实案例。
上一节把 LTV 拆成三种算法,说明同一个客户可以算出不同的生命周期价值。这一节换一个方向:不再追求「算得更准」,而是问「什么时候该停下来」。单位经济失效很少以「亏损」的形式突然出现,它通常先表现为几个指标同时变差,而报表上的收入还在增长。
五个信号是:毛利率、LTV/CAC、CAC 回本期、月流失率、GRR。前四个来自 2.1 节的四个数,第五个来自 2.2 节的队列留存。它们不是五个孤立的 KPI,而是一条链上的五个检查点:毛利决定每个客户能贡献多少,LTV/CAC 决定获客值不值,回本期决定现金多久能回来,流失率决定 LTV 能维持多久,GRR 决定存量收入漏多少。
LedgerLite 是主案例。它在四个信号上是绿灯,却在 GRR 上亮黄灯。这个组合比「五项全红」更有教学价值:它说明单位经济失效不一定是「数字很差」,也可能是「数字很好,但结构在漏」。
从指标到信号:阈值与四种状态
指标和信号的区别在于「可判定」。ARPA 77 美元是一个指标,但它不告诉你该不该继续投入;LTV/CAC 30.9 是一个指标,配上「低于 3.0 就观察」才变成信号。阈值不是宇宙常数,而是把连续数字切成可行动区间的约定。
本节采用一组教学阈值。它们来自常见的订阅业务经验,不是案例事实,也不是行业定律;换一个业务阶段,阈值可以调整,但必须提前写下来,不能等结果出来再改。
| 信号 | 计算 | 绿灯 | 黄灯 | 红灯 |
|---|---|---|---|---|
| 毛利率 | 毛利 ÷ 收入 | ≥ 75.0% | 60.0%–74.9% | < 60.0% |
| LTV/CAC | LTV ÷ CAC | ≥ 3.0x | 1.0x–2.9x | < 1.0x |
| CAC 回本期 | CAC ÷ 月毛利 | ≤ 12 个月 | 12.1–18 个月 | > 18 个月 |
| 月流失率 | 当月流失收入 ÷ 期初 MRR | ≤ 3.0% | 3.1%–5.0% | > 5.0% |
| GRR | 存量客户续费与收缩后的收入 ÷ 期初收入 | ≥ 85.0% | 80.0%–84.9% | < 80.0% |
每个信号有四种状态,而不是三种:绿灯、黄灯、红灯,以及「未知」。未知不是绿灯。缺少 CAC 时,LTV/CAC 和回本期都无法判定;买断制没有月流失时,流失信号不适用。把「不适用」写成「通过」,是单位经济评分里最常见的自欺。
判定顺序也重要:只要有一个红灯,整体就是红灯;没有红灯但有黄灯,整体是黄灯;没有红黄灯但有未知,整体是「数据不足」;五项全绿才是健康。这个顺序承认了一件事:一个红灯足以否定一组单位经济,而一个未知不足以肯定它。
五个信号分别在防什么
五个信号对应五种不同的失效方式。把它们分开看,才能知道该修哪里。
毛利率防的是「卖得越多,变动成本越高」。 LedgerLite 的毛利率 84.2%,变动成本 38,000 美元,其中云 18,000、支付 12,000、支持 8,000。毛利率低于 75% 时,每 100 美元收入留下的毛利不足以覆盖获客和运营;低于 60% 时,规模扩张会先耗尽现金。对一人公司,毛利率还有一个隐含作用:它决定你能不能靠自己的时间同时做产品、支持和销售。
LTV/CAC 防的是「获客比客户终身价值还贵」。 LedgerLite 的 LTV 4,322 美元、CAC 140 美元,比值 30.9,远高于 3.0。低于 3.0 时,获客投入的回收空间变薄,任何流失率上升或 CAC 上涨都可能把比值压到 1 附近;低于 1.0 时,每获取一个客户都在毁灭价值。这个信号依赖 LTV 的口径,所以必须和 2.2 节的算法说明一起看。
回本期防的是「账上先没钱」。 LedgerLite 的回本期 2.2 个月,意味着获客成本在第三个月初就由毛利收回。超过 12 个月,增长会先消耗现金;超过 18 个月,即使 LTV/CAC 看起来不错,现金流也可能撑不到回收那天。回本期是 LTV/CAC 的「时间版本」:一个说总量,一个说节奏。
月流失率防的是「LTV 的地基在塌」。 LedgerLite 的月流失 1.5%,年化毛留存 (1 − 1.5%)¹² = 83.4%,与案例的 GRR 83% 基本一致。月流失超过 3.0% 时,年化毛留存降到 69.4%,LTV 会缩短三分之一以上;超过 5.0% 时,年化毛留存只剩 54.0%,获客投入很难回收。流失率的可怕之处在于它同时影响 LTV 和收入增长,而报表上往往先看到「新签在涨」。
GRR 防的是「扩张掩盖了漏损」。 LedgerLite 的 GRR 83%,NRR 102%。这两个数放在一起,说明存量客户每年漏掉 17 个百分点,扩张又补回 19 个百分点,净留存 +2%。GRR 低于 85% 时,存量收入的漏损已经偏高;低于 80% 时,即使扩张不错,NRR 也可能被拖到 100% 以下。NRR 是结果,GRR 是地基;只看 NRR,会把「扩张很猛」误读成「留存很好」。
五个信号之间有重合,但重合是故意的。LTV/CAC 和回本期都依赖毛利与 CAC;月流失和 GRR 都描述留存。重合意味着同一个问题会在多个信号上显形,也意味着不能只挑一个好看的信号来汇报。
LedgerLite:四个绿灯与一个黄灯
把 LedgerLite 的数字代入五个信号:
| 信号 | 数值 | 状态 | 判据 |
|---|---|---|---|
| 毛利率 | 84.2% | 绿灯 | ≥ 75.0% |
| LTV/CAC | 30.9x | 绿灯 | ≥ 3.0x |
| CAC 回本期 | 2.2 个月 | 绿灯 | ≤ 12 个月 |
| 月流失率 | 1.5% | 绿灯 | ≤ 3.0% |
| GRR | 83.0% | 黄灯 | 80.0%–84.9% |
四项绿灯说明 LedgerLite 的单位经济在「获客—回本—留存」这条主链上是健康的:每个客户贡献 64.83 美元月毛利,140 美元 CAC 在 2.2 个月收回,LTV 是 CAC 的 30.9 倍,月流失只有 1.5%。如果只看这四个数,结论会是「继续投入」。
GRR 83% 把这个结论从「健康」改成「观察」。83% 的毛留存意味着每年有 17% 的存量收入漏掉;NRR 102% 之所以还是正的,是因为扩张补回了 19 个百分点。扩张不可能永远补得上:如果 GRR 从 83% 降到 80%,同样 19 个百分点的扩张只能得到 99% 的 NRR;如果 GRR 降到 79%,NRR 变成 98%,存量收入开始净萎缩。
这就是「四个绿灯一个黄灯」的含义:单位经济没有失效,但它的健康依赖扩张持续发生。扩张来自涨价、加购、升级,这些动作一旦放缓,GRR 的 17% 漏损就会直接暴露在 NRR 上。收入的「质量」问题会在第 4 章展开,见 收入质量 ;本节只负责把它标成黄灯。
黄灯不等于停下。它要求的动作是「先查清楚 17% 的漏损从哪里来」:是客户倒闭、是降级、还是某一类客户天然短命。查清楚之前,继续按 30.9 的 LTV/CAC 扩张,等于在用一个可能被高估的 LTV 做决策。
Tidecast 与 Nord Studio:缺失的信号不是绿灯
Tidecast 的评分是「两绿一黄两未知」。毛利率用净得率 72.0%,落在 60.0%–74.9% 的黄灯区;LTV/CAC 是 14.39 ÷ 1.25 = 11.5,绿灯;回本期在成交时完成,绿灯;月流失和 GRR 不适用,标为未知。它的 72.0% 已经扣掉退款 4%、Steam 分成 30% 和官网支付费 5%,但没有扣营销、服务器和工具;28 个百分点的「漏损」主要来自平台分成,这是买断制的结构成本,不是管理失误。买断制没有订阅存量,用月流失和 GRR 去判它没有意义;硬填一个「0% 流失」会让评分虚高,正确做法是保留未知。
Nord Studio 的评分是「两绿三未知」,整体为数据不足。产品线毛利率用混合毛利率 84.4%,绿灯;产品月流失 3.0%,恰好等于阈值,绿灯;但案例没有单列营销费用,CAC 不可复算,LTV/CAC 和回本期都是未知;GRR 也没有单列,同样是未知。它的服务线年续约率 70%,折算成月流失是 1 − 70%^(1÷12) = 2.93%,也在绿灯区;但服务收入占 72.7%,产品只占 27.3%,两条线的客户和成本结构不同,不能用一个混合评分代替两条线分别评分。CAC 缺失不是小事,它让「获客值不值」这个核心问题无法回答;获客如何归因见 获客归因与复盘 。
两个案例的共同教训是:未知必须保留为未知。Tidecast 如果把流失填成 0%,会得到「五项全绿」的假象;Nord Studio 如果拍一个 CAC,会得到一个看起来精确、其实没有依据的 LTV/CAC。评分的价值不在于给出一个漂亮的总分,而在于告诉你「哪一项还没法判断」。
把五个信号写成代码
下面的 signals.ts 把五个阈值固化成函数。每个 check 返回一个 Signal,包含 id、标签、状态、数值、单位、阈值和说明;evaluateSignals 汇总成 SignalReport,给出四种状态的数量和最终判定。数值缺失时返回 unknown,不参与绿灯计数。
// file: src/unit/signals.ts
// tier: L1
// verify: node --test test/unit/signals.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps } from "../core/rate.ts";
export type SignalId =
| "gross-margin"
| "ltv-cac"
| "payback"
| "churn"
| "grr";
export type SignalStatus = "ok" | "watch" | "fail" | "unknown";
export type Signal = {
id: SignalId;
label: string;
status: SignalStatus;
value: number | null;
unit: "bps" | "months" | "ratio";
threshold: string;
note: string;
};
export type SignalInput = {
grossMarginBps?: Bps;
ltvCents?: Cents;
cacCents?: Cents;
paybackMonths?: number;
monthlyChurnBps?: Bps;
grrBps?: Bps;
};
export type SignalVerdict = "healthy" | "watch" | "fail" | "incomplete";
export type SignalReport = {
signals: Signal[];
counts: { ok: number; watch: number; fail: number; unknown: number };
verdict: SignalVerdict;
};
function missing(
id: SignalId,
label: string,
unit: Signal["unit"],
threshold: string,
): Signal {
return {
id,
label,
status: "unknown",
value: null,
unit,
threshold,
note: "输入不足,无法判定",
};
}
export function checkGrossMargin(grossMarginBps?: Bps): Signal {
const threshold = "ok >= 75.0%;watch 60.0%–74.9%;fail < 60.0%";
if (
grossMarginBps === undefined ||
!Number.isInteger(grossMarginBps) ||
grossMarginBps < 0 ||
grossMarginBps > 10000
) {
return missing("gross-margin", "毛利率", "bps", threshold);
}
let status: SignalStatus = "ok";
if (grossMarginBps < 6000) {
status = "fail";
} else if (grossMarginBps < 7500) {
status = "watch";
}
return {
id: "gross-margin",
label: "毛利率",
status,
value: grossMarginBps,
unit: "bps",
threshold,
note:
status === "ok"
? "毛利足以覆盖获客与运营"
: "毛利偏薄,先检查变动成本口径",
};
}
export function checkLtvCac(
ltvCents?: Cents,
cacCents?: Cents,
): Signal {
const threshold = "ok >= 3.0x;watch 1.0x–2.9x;fail < 1.0x";
if (
ltvCents === undefined ||
cacCents === undefined ||
!Number.isInteger(ltvCents) ||
ltvCents < 0 ||
!Number.isInteger(cacCents) ||
cacCents <= 0
) {
return missing("ltv-cac", "LTV / CAC", "ratio", threshold);
}
const value = Math.round((ltvCents / cacCents) * 100) / 100;
let status: SignalStatus = "ok";
if (value < 1) {
status = "fail";
} else if (value < 3) {
status = "watch";
}
return {
id: "ltv-cac",
label: "LTV / CAC",
status,
value,
unit: "ratio",
threshold,
note:
status === "ok"
? "获客投入有足够的毛利回收空间"
: "获客回报偏薄,需检查 LTV 口径",
};
}
export function checkPayback(payback?: number): Signal {
const threshold = "ok <= 12 个月;watch 12.1–18 个月;fail > 18 个月";
if (
payback === undefined ||
!Number.isFinite(payback) ||
payback < 0
) {
return missing("payback", "CAC 回本期", "months", threshold);
}
let status: SignalStatus = "ok";
if (payback > 18) {
status = "fail";
} else if (payback > 12) {
status = "watch";
}
return {
id: "payback",
label: "CAC 回本期",
status,
value: payback,
unit: "months",
threshold,
note:
status === "ok"
? "获客成本可在一年内由毛利收回"
: "回本慢于一年,增长会先消耗现金",
};
}
export function checkChurn(monthlyChurnBps?: Bps): Signal {
const threshold = "ok <= 3.0%/月;watch 3.1%–5.0%/月;fail > 5.0%/月";
if (
monthlyChurnBps === undefined ||
!Number.isInteger(monthlyChurnBps) ||
monthlyChurnBps < 0 ||
monthlyChurnBps > 10000
) {
return missing("churn", "月流失率", "bps", threshold);
}
let status: SignalStatus = "ok";
if (monthlyChurnBps > 500) {
status = "fail";
} else if (monthlyChurnBps > 300) {
status = "watch";
}
return {
id: "churn",
label: "月流失率",
status,
value: monthlyChurnBps,
unit: "bps",
threshold,
note:
status === "ok"
? "流失速度在可管理范围"
: "流失偏快,LTV 会被显著压缩",
};
}
export function checkGrr(grrBps?: Bps): Signal {
const threshold = "ok >= 85.0%;watch 80.0%–84.9%;fail < 80.0%";
if (
grrBps === undefined ||
!Number.isInteger(grrBps) ||
grrBps < 0 ||
grrBps > 20000
) {
return missing("grr", "毛利留存率 GRR", "bps", threshold);
}
let status: SignalStatus = "ok";
if (grrBps < 8000) {
status = "fail";
} else if (grrBps < 8500) {
status = "watch";
}
return {
id: "grr",
label: "毛利留存率 GRR",
status,
value: grrBps,
unit: "bps",
threshold,
note:
status === "ok"
? "存量收入漏损可控"
: "存量收入漏损偏高,需看扩张是否掩盖了流失",
};
}
/** Run all five checks and return a structured verdict. */
export function evaluateSignals(input: SignalInput): SignalReport {
const signals = [
checkGrossMargin(input.grossMarginBps),
checkLtvCac(input.ltvCents, input.cacCents),
checkPayback(input.paybackMonths),
checkChurn(input.monthlyChurnBps),
checkGrr(input.grrBps),
];
const counts = { ok: 0, watch: 0, fail: 0, unknown: 0 };
for (const signal of signals) {
counts[signal.status] += 1;
}
let verdict: SignalVerdict = "healthy";
if (counts.fail > 0) {
verdict = "fail";
} else if (counts.watch > 0) {
verdict = "watch";
} else if (counts.unknown > 0) {
verdict = "incomplete";
}
return { signals, counts, verdict };
}
代码有三个刻意的设计。
第一,Signal.value 允许 null,status 允许 unknown。这不是类型系统的装饰,而是评分逻辑的一部分:缺 CAC 时,LTV/CAC 不是 0,也不是「通过」,而是「还不知道」。报表里保留 null,比填一个默认值更诚实。
第二,阈值写在每个 check 函数的 threshold 字段里,和判定逻辑放在一起。改阈值时只需要改一处,报告里的说明会自动跟着变;不会出现「代码按 3.0 判、文档写 2.5」的漂移。
第三,整体判定用「红灯优先、黄灯其次、未知再次」的顺序。counts.unknown 只在前两者都为零时才影响结论;这意味着「一绿四未知」不会被误判成健康,而「四绿一未知」也不会被误判成失败。测试覆盖了 LedgerLite 的 4 绿 1 黄、Tidecast 的 2 绿 1 黄 2 未知、Nord Studio 的 2 绿 3 未知,以及五项全红和全部未知的边界。
小结
- 指标是数字,信号是带阈值的判定;五个信号分别防毛利过薄、获客过贵、回本过慢、流失过快、存量漏损。
- 本节阈值:毛利率 75%/60%,LTV/CAC 3.0x/1.0x,回本期 12/18 个月,月流失 3.0%/5.0%,GRR 85%/80%;阈值是教学约定,不是行业定律。
- 状态有四种:绿灯、黄灯、红灯、未知;未知不是绿灯,缺失数据不能填成通过。
- LedgerLite 是四绿一黄:84.2% 毛利、30.9x LTV/CAC、2.2 个月回本、1.5% 月流失都是绿灯;GRR 83% 是黄灯,因为 17% 的存量漏损靠 19 个百分点的扩张补回。
- Tidecast 是两绿一黄两未知:72.0% 净得率黄灯,11.5x LTV/CAC 和成交即回本绿灯,月流失与 GRR 不适用。
- Nord Studio 是两绿三未知:84.4% 毛利和 3.0% 产品月流失绿灯,CAC、回本期、GRR 因缺少营销与留存口径无法判定。
- 整体判定顺序是红灯 > 黄灯 > 未知 > 健康;一个红灯足以否定一组单位经济,一个未知不足以肯定它。
- 黄灯的动作是查清漏损来源,不是立刻停下;红灯的动作才是重新设计单位经济或停止扩张。
阅读导航:上一节:2.2 LTV 的三种算法与适用边界 · 下一节:3.1 利润表:收入、成本、费用的口径 。
常见错误与边界
- 把未知填成绿灯。 Tidecast 没有月流失,填 0% 会得到五项全绿的假象;正确状态是 unknown。
- 只报总分,不报分项。 「三项绿灯」可能掩盖一个红灯;五个信号的用途是定位问题,不是打一个总分。
- 阈值事后调整。 看到 GRR 83% 就把黄灯线改成 80%,等于取消信号;阈值必须在看结果之前写死。
- 把 GRR 和 NRR 混为一谈。 LedgerLite 的 GRR 83%、NRR 102%;只看 NRR 会以为留存很好,实际存量每年漏 17%。
- 用收入毛利率替代毛利。 毛利率的分母是收入、分子是毛利;用收入算回本期会低估回本时间。
- 在买断制里套订阅信号。 Tidecast 没有月流失和 GRR,硬套会得到无意义的判定;它的核心信号是净得率和单位获客成本。
- 在混合业务里用一个混合评分。 Nord Studio 的服务线年续约 70%、产品线月流失 3.0%,两条线要分开评分,不能平均。
- 把回本期和 LTV/CAC 当成同一个信号。 一个说总量,一个说节奏;LTV/CAC 高但回本期长的业务,仍可能被现金流拖死。
- 忽略流失率的年化效应。 月流失 3.0% 对应年化毛留存 69.4%,不是 64%;月流失 5.0% 对应 54.0%,不是 60%。
- 把黄灯当成「没问题」。 黄灯是「继续观察并查原因」,不是「可以忽略」;LedgerLite 的 83% GRR 就是靠扩张撑住的黄灯。
五个信号把单位经济从「四个数」变成「一组判断」。它们不能替你做战略决定,但能告诉你什么时候该停下来查一查。下一章会把单位经济接进利润表,看 84.2% 的毛利率如何经过运营费用、创始人薪酬和所得税,变成 130,240 美元的 SDE 和 58,299 美元的净利润;届时这五个信号会变成年度报表上的具体科目。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。