以下情境为教学假设,人物与数字不代表经核实的真实案例。
260 家付费客户不是一个均匀的整体,而是一条从 21,622 美元到几百美元的长尾。ARPA 77 美元是这条分布的平均值,它把最大客户和最小客户抹平成同一个数。用平均值做决策,最常见的后果是:对头部客户按标准条款服务,对长尾客户按定制方式服务,两边的成本都被低估。
客户分层要回答两个不同的问题:谁贡献收入,谁消耗成本。按收入分层告诉你「失去谁会疼」,按成本分层告诉你「服务谁最贵」。只做第一层,会把高收入误当成高利润;只做第二层,会把低成本误当成高价值。这一节把两层都做出来,再放回同一张表。
第 1 章的 1.2 节已经做过一次收入线组合评分,第 4 章的 4.2 节把集中度作为收入质量的一个分项。它们都是一次性的结构快照。本章要把分层变成可以按月更新的模型:每一层有多少客户、贡献多少收入、摊到多少服务成本、毛利份额是多少。先看清分布,再谈集中度和留存。
为什么平均数会骗人
LedgerLite 的事实表只给了三个客户层数字:260 家客户、前 5 家占收入 31%、最大一家占 9%。这三个数足以把客户拆成三层,但拆法需要说明。下面把前 5 家拆成「最大一家」和「其余四家」,其余 255 家合并为长尾:
| 层 | 客户数 | 年收入 | 收入占比 | 单客年收入 | 单客月收入 |
|---|---|---|---|---|---|
| 关键客户 | 1 | $21,622 | 9.0% | $21,622 | $1,802 |
| 重要客户 | 4 | $52,852 | 22.0% | $13,213 | $1,101 |
| 长尾客户 | 255 | $165,766 | 69.0% | $650 | $54 |
| 合计 | 260 | $240,240 | 100.0% | $924 | $77 |
拆法如下:最大一家 9% × 240,240 = 21,622 美元;前五家 31% × 240,240 = 74,474 美元,减去最大一家得到其余四家 52,852 美元;剩下的 255 家拿 165,766 美元。每一层的单客收入都是该层收入除以客户数。这些数全部由事实表的 260、9%、31%、240,240 推出,没有新增案例事实。
这张表最重要的一行是最后一行:整体单客月收入 77 美元,但关键客户是 1,802 美元,长尾是 54 美元,相差 33 倍。平均数 77 美元既不代表头部,也不代表尾部,它只代表「如果所有客户完全一样」时的假想客户。分层的第一件事,就是拒绝这个假想客户。
Tidecast 的分层对象不是客户账户,而是 9,600 次买断交易。它没有「客户数」这个稳定基数,只有渠道:Steam 80%、官网 20%。把买断玩家硬分成「大客户/小客户」没有财务意义,因为每个买家只付一次 19.99 美元。Nord Studio 则是另一种结构:咨询线按工时计费,产品线按 90 家订阅客户计费,两条线的分层单位不同,不能合并成一张客户表。
按收入分层:头部、中部、长尾
按收入分层的目的不是排名,而是决定「用哪种服务方式」。三层可以用三个问题来定义:
- 关键客户:失去它,年收入立刻少 5% 以上,且短期找不到替代。
- 重要客户:单家占比 1%–5%,有一定定制需求,但仍有标准化空间。
- 长尾客户:单家占比低于 1%,数量多,服务必须自助化。
按这个定义,LedgerLite 的关键客户只有 1 家,重要客户 4 家,长尾 255 家。三层的收入占比是 9%、22%、69%。这个结构有两个容易被误读的地方。
第一,长尾不是「不重要」。255 家客户贡献 69% 的收入,接近七成。失去 10 家长尾客户,按单客 650 美元计算,一年少 6,501 美元;失去关键客户,一年少 21,622 美元。前者约等于后者的 30%,但前者只需要 10 个客户同时离开,后者只需要 1 个。两种损失的「触发难度」不同,不能用同一个百分比衡量。
第二,前五家 31% 不算极端集中,但也不是「很安全」。如果前五家同时不续约,收入立刻少 31%,而固定成本不会同步下降。LedgerLite 的运营费用是 72,000 美元/年,31% 的收入是 74,474 美元,刚好覆盖运营费用。换句话说,前五家集体流失会把 SDE 从 130,240 美元打到约 67,540 美元(毛利减少约 62,700 美元,运营费用不变;如果变动成本完全不随收入下降,最保守是 55,766 美元)。这个测算是教学推演,不是预测,但它说明「31%」这个数字在利润表上的分量。
分层之后,动作应该跟着层走,而不是跟着平均数走:
| 层 | 收入特征 | 主要风险 | 优先动作 |
|---|---|---|---|
| 关键客户 | 单家 9%,定制多 | 单点流失、条款让步 | 续约前 6 个月介入,准备备选方案 |
| 重要客户 | 单家 5.5%,有扩张空间 | 被当成大客户服务,成本失控 | 标准化 onboarding,设定服务边界 |
| 长尾客户 | 单家 0.27%,数量多 | 支持成本吃掉毛利 | 自助文档、模板回复、最低套餐 |
按成本分层:服务成本不随收入等比例
收入分层只看分子,成本分层要看分母。LedgerLite 的变动成本是 38,000 美元/年,拆成三块:云 18,000、支付 12,000、支持 8,000。这三块的成本驱动不同:
- 云和支付随收入变化,适合按收入摊分。
- 支持随账户数变化,适合按账户数摊分。
按这个口径,30,000 美元的云加支付按收入摊,8,000 美元的支持按账户数摊:
| 层 | 云+支付摊分 | 支持摊分 | 总服务成本 | 毛利 | 毛利率 |
|---|---|---|---|---|---|
| 关键客户 | $2,700.04 | $30.76 | $2,730.80 | $18,891.20 | 87.37% |
| 重要客户 | $6,599.90 | $123.07 | $6,722.97 | $46,129.03 | 87.28% |
| 长尾客户 | $20,700.06 | $7,846.17 | $28,546.23 | $137,219.77 | 82.78% |
| 合计 | $30,000.00 | $8,000.00 | $38,000.00 | $202,240.00 | 84.18% |
这张表的关键不是绝对金额,而是毛利率的排序:关键客户 87.37%,重要客户 87.28%,长尾客户 82.78%,首尾差 4.59 个百分点。差距来自支持成本按账户摊:每个账户分到 30.77 美元支持成本,长尾客户的单客年收入只有 650 美元,支持成本占收入的 4.7%;关键客户的单客年收入 21,622 美元,同样的 30.77 美元只占 0.14%。
如果换一个摊法,结论会变。把 38,000 美元全部按收入摊,三层毛利率都等于 84.18%,分层就失去了成本信息;把支持成本按「大客户支持强度是长尾的 10 倍」加权摊,关键客户的毛利率会下降,长尾会上升。摊法不是中立的,它决定了你看到的风险。可复算的前提是:把驱动写出来,把假设标出来。
Nord Studio 的成本层更直接:咨询线 110 小时/月是时间上限,服务成本按工时走,不按客户数走。它的「关键客户」不是收入最大的客户,而是占用合伙人时间最多的客户。Tidecast 的平台分成按收入走,退款按交易走,也没有「每账户支持成本」这一层;它的成本分层只能按渠道做。
收入份额与毛利份额:两张表放一起
把收入份额和毛利份额并排,才能看出哪一层在「养」哪一层:
| 层 | 收入占比 | 毛利占比 | 单客毛利 | 单客服务成本 |
|---|---|---|---|---|
| 关键客户 | 9.0% | 9.34% | $18,891.20 | $2,730.80 |
| 重要客户 | 22.0% | 22.81% | $11,532.26 | $1,680.74 |
| 长尾客户 | 69.0% | 67.85% | $538.12 | $111.95 |
| 合计 | 100.0% | 100.0% | $777.85 | $146.15 |
毛利占比之和是 100.00%(934 + 2281 + 6785 = 10000 bps),没有尾差。关键客户的收入占比 9.0%,毛利占比 9.34%,略高于收入占比,因为它的毛利率最高;长尾客户的收入占比 69.0%,毛利占比 67.85%,略低于收入占比。三层之间没有出现「头部补贴尾部」的戏剧性反转,但单客毛利差距很大:关键客户 18,891 美元,长尾客户 538 美元,相差 35 倍。
这个结构给出三条动作线:
第一,关键客户要「保」。保的不是价格,而是续约确定性和服务边界。关键客户的毛利率最高,但它也是最容易被定制需求拖垮的一层;一旦支持强度从 1 倍变成 10 倍,87% 的毛利率会迅速下降。
第二,重要客户要「长」。四家客户贡献 22% 收入,单客毛利 11,532 美元,介于关键和长尾之间。这一层最值得做扩张:它们的需求已经形成,但还没有大到必须定制。扩张收入的目标应该放在这一层,而不是继续给关键客户加功能。
第三,长尾要「省」。255 家客户贡献 69% 收入,单客毛利 538 美元,服务成本 112 美元。长尾的毛利绝对值不小,但每一家都需要支持;如果支持成本从 8,000 美元涨到 16,000 美元,长尾的毛利率会从 82.78% 降到 78.0% 左右(教学推演)。长尾的动作不是涨价,而是把服务成本压到可控:文档、模板、自动回复、最低套餐;具体做法见 自助支持的验收 。
分层不是一次性的分类。客户会升级、降级、流失,层的边界也会随价格和成本变化。一个季度重算一次,比年初分一次更可靠。
把分层写成代码
下面的 tiers.ts 把三个动作固化成函数:allocateServiceCost 按收入、账户数或平均三种驱动摊分共享成本;tierContribution 计算每层的收入份额、毛利份额和单客经济;tierReport 把摊分和贡献串起来。所有金额用整数美分,比率用基点,非法输入返回 Result 错误。
// file: src/unit/tiers.ts
// tier: L1
// verify: node --test test/unit/tiers.test.ts
import { type Cents, sum } from "../core/money.ts";
import { type Bps, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type CustomerTier = {
name: string;
customers: number;
annualRevenueCents: Cents;
serviceCostCents: Cents;
};
export type CostDriver = "revenue" | "accounts" | "equal";
export type TierContribution = {
name: string;
customers: number;
annualRevenueCents: Cents;
serviceCostCents: Cents;
grossProfitCents: Cents;
grossMarginBps: Bps;
revenueShareBps: Bps;
profitShareBps: Bps;
revenuePerCustomerCents: Cents;
costPerCustomerCents: Cents;
};
type Totals = {
revenue: Cents;
cost: Cents;
profit: Cents;
};
function validate(tiers: CustomerTier[]): Result<Totals> {
if (tiers.length === 0) {
return fail("at least one tier is required");
}
for (const tier of tiers) {
if (tier.name.trim() === "") {
return fail("tier name must not be empty");
}
if (!Number.isInteger(tier.customers) || tier.customers <= 0) {
return fail(`invalid customer count for ${tier.name}`);
}
if (
!Number.isInteger(tier.annualRevenueCents) ||
tier.annualRevenueCents < 0
) {
return fail(`invalid annual revenue for ${tier.name}`);
}
if (
!Number.isInteger(tier.serviceCostCents) ||
tier.serviceCostCents < 0
) {
return fail(`invalid service cost for ${tier.name}`);
}
}
const revenue = sum(tiers.map((tier) => tier.annualRevenueCents));
if (revenue <= 0) {
return fail("total tier revenue must be positive");
}
const cost = sum(tiers.map((tier) => tier.serviceCostCents));
return ok({ revenue, cost, profit: revenue - cost });
}
function weightsFor(tiers: CustomerTier[], driver: CostDriver): number[] {
if (driver === "revenue") {
return tiers.map((tier) => tier.annualRevenueCents);
}
if (driver === "accounts") {
return tiers.map((tier) => tier.customers);
}
return tiers.map(() => 1);
}
/**
* Allocate a shared service cost across tiers with integer cents.
* The last tier receives the rounding remainder so the parts always
* sum exactly to the requested total.
*/
export function allocateServiceCost(
tiers: CustomerTier[],
totalCostCents: Cents,
driver: CostDriver,
): Result<Cents[]> {
const checked = validate(tiers);
if (!checked.ok) {
return checked;
}
if (!Number.isInteger(totalCostCents) || totalCostCents < 0) {
return fail("total service cost must be a non-negative integer number of cents");
}
const weights = weightsFor(tiers, driver);
const weightTotal = weights.reduce((acc, weight) => acc + weight, 0);
if (weightTotal <= 0) {
return fail("allocation weights must sum to a positive value");
}
let allocated = 0;
return ok(
weights.map((weight, index) => {
if (index === weights.length - 1) {
return totalCostCents - allocated;
}
const part = Math.floor((totalCostCents * weight) / weightTotal);
allocated += part;
return part;
}),
);
}
/**
* Revenue share, gross profit share and per-customer economics for
* each tier. Service costs are whatever `serviceCostCents` holds,
* so the caller decides whether they came from revenue, accounts or
* a mixed allocation.
*/
export function tierContribution(
tiers: CustomerTier[],
): Result<TierContribution[]> {
const checked = validate(tiers);
if (!checked.ok) {
return checked;
}
return ok(
tiers.map((tier) => {
const grossProfit = tier.annualRevenueCents - tier.serviceCostCents;
return {
name: tier.name,
customers: tier.customers,
annualRevenueCents: tier.annualRevenueCents,
serviceCostCents: tier.serviceCostCents,
grossProfitCents: grossProfit,
grossMarginBps: divide(grossProfit, tier.annualRevenueCents),
revenueShareBps: divide(tier.annualRevenueCents, checked.value.revenue),
profitShareBps:
checked.value.profit > 0
? divide(grossProfit, checked.value.profit)
: 0,
revenuePerCustomerCents: Math.round(
tier.annualRevenueCents / tier.customers,
),
costPerCustomerCents: Math.round(
tier.serviceCostCents / tier.customers,
),
};
}),
);
}
/**
* Allocate one shared cost with a single driver and return the tier
* contributions. Callers that split cloud, payment and support across
* different drivers can call `allocateServiceCost` twice and add the
* parts before calling `tierContribution`.
*/
export function tierReport(
tiers: CustomerTier[],
totalCostCents: Cents,
driver: CostDriver,
): Result<TierContribution[]> {
const parts = allocateServiceCost(tiers, totalCostCents, driver);
if (!parts.ok) {
return parts;
}
return tierContribution(
tiers.map((tier, index) => ({
...tier,
serviceCostCents: tier.serviceCostCents + parts.value[index],
})),
);
}
代码里有四个刻意的边界。
第一,allocateServiceCost 的最后一层拿余数,保证各层摊分之和精确等于总成本。前两层用 Math.floor,余数可能是几分钱;如果不处理,报表会出现「摊分合计 37,999.99」这种无法对账的尾差。
第二,驱动是显式参数,不是函数内部猜的。云和支付按收入、支持按账户数,调用方要自己组合;代码不会替你决定哪种驱动更合理。这个选择本身是财务判断,不是技术细节。
第三,tierContribution 只在总毛利为正时计算毛利占比。总毛利为负时,占比没有意义,函数返回 0,而不是返回一个负的百分比让人误读。
第四,单客收入和服务成本都做整数美分四舍五入。长尾单客年收入 650.06 美元,显示成 650 美元;单客服务成本 111.95 美元,显示成 112 美元。展示层可以有小数,计算层必须保持整数美分。
小结
- 260 家客户的平均 ARPA 77 美元会掩盖分布:关键客户月收入 1,802 美元,长尾客户 54 美元,相差 33 倍。
- 按收入分层:最大一家 9%、其余四家 22%、长尾 255 家 69%;前五家合计 31%。
- 按成本分层:云和支付按收入摊,支持按账户摊;关键客户毛利率 87.37%,长尾 82.78%,首尾差 4.59 个百分点。
- 收入份额与毛利份额接近,但单客毛利相差 35 倍;分层动作应跟随层,而不是跟随平均数。
- Tidecast 的 9,600 次买断交易没有稳定客户基数,分层对象是渠道;Nord 的咨询按工时、产品按账户,两条线不能合成一张客户表。
- 关键客户保续约和服务边界,重要客户做扩张,长尾客户压服务成本。
- 分层模型要按季度重算,因为客户会升级、降级和流失,成本驱动也会变。
- 本节只做了静态分层;下一节把「前五大占比」和 HHI 变成集中度阈值与应对动作。
阅读导航:上一节:6.3 套餐组合的收入结构 · 下一节:7.2 集中度风险:前五大客户占比 。
常见错误与边界
- 用平均 ARPA 代表所有客户。 77 美元是 260 家客户的加权平均;关键客户 1,802 美元、长尾 54 美元,用平均数定价会同时得罪两头。
- 把前五家 31% 当成「很安全」。 31% 的收入是 74,474 美元,刚好覆盖 72,000 美元的运营费用;前五家集体流失会把 SDE 打到接近 55,766 美元。
- 只按收入分层,不看服务成本。 长尾收入占 69%,但支持成本按账户摊,毛利率比关键客户低 4.59 个百分点;只看收入会高估长尾的利润贡献。
- 把支持成本按收入摊。 支持不是随收入等比例变化的;按收入摊会把成本推给头部,掩盖长尾的服务负担。
- 摊分不处理尾差。 整数美分摊分后各层之和可能差几分钱;不把余数给最后一层,报表无法对账。
- 把 Tidecast 的买家当客户账户。 9,600 次买断交易没有稳定的客户基数,分层对象是渠道,不是买家。
- 把 Nord 的咨询客户和产品客户合成一张表。 咨询按工时、产品按账户,两条线的分层单位和成本驱动不同。
- 把关键客户的高毛利当成「可以无限定制」。 关键客户毛利率最高,但支持强度一旦上升,87% 会迅速下降;保的是续约,不是无限让步。
- 分层一次就固定。 客户会升级、降级、流失,价格和成本也会变;至少每季度重算一次。
- 把分层结果当估值依据。 分层只描述收入与成本的分布,不直接决定倍数;估值还要看集中度、留存和增长,见第 11 章。
分层是把「260 家客户」翻译成「三类经济关系」的过程。下一节会把注意力从层内移到层间:前五大客户占比、HHI 和平台依赖,回答「如果一层突然消失,收入会少多少」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。