以下情境为教学假设,人物与数字不代表经核实的真实案例。
集中度不是一个百分比,而是一个概率:某个客户、某个渠道或某条收入线突然消失时,收入会少多少。前五大客户占比是最常用的入口,但它只回答「前五家加起来多少」,不回答「其中一家有多大」「剩下的是不是足够分散」「这个渠道能不能替换」。
第 4 章的 4.2 节把集中度作为收入质量的一个分项,用的是「10000 − 最大暴露口」。那是一次性的质量评分,权重 30%。本章把集中度做成一个独立的、可以按月更新的模型:最大客户、前三大、前五大、HHI 四个视角,四档阈值,以及每档对应的动作。1.2 节的收入线 HHI 也只是一次组合评分;这里要区分「客户集中」和「收入线集中」,不能用一个 HHI 同时回答两个问题。
三个案例的集中度类型完全不同:
| 案例 | 最显眼的集中度数字 | 口径 | 风险类型 |
|---|---|---|---|
| LedgerLite | 最大客户 9%,前五家 31% | 客户收入 | 客户集中,但长尾厚 |
| Tidecast | Steam 80% 销量、74.7% 净收入 | 销售渠道 | 平台集中 |
| Nord Studio | 前 3 家 48%,服务收入 72.7% | 客户收入 + 收入线 | 客户与服务线双重集中 |
Tidecast 的 80% 和 Nord 的 72.7% 经常被放在同一张表里比较,但它们是两种东西:前者是渠道占比,后者是收入线占比。把它们当成「客户集中度」,会得出错误的动作。
口径先定:最大客户、前三大、前五大
同一个客户群,换一个分母,集中度会变。常见的四个口径是:
- 最大客户占比:最大一家 ÷ 总收入。它衡量「单点故障」。
- 前三大占比:前三家 ÷ 总收入。它衡量「小团体同时离开」的风险。
- 前五大占比:前五家 ÷ 总收入。它是尽职调查里最常用的门槛。
- HHI:各客户占比平方和 × 10000。它衡量整个分布,而不是前几名。
三个案例能拿到的数字并不一样:
| 案例 | 最大客户 | 前三大 | 前五大 | 不能从事实表推出的数 |
|---|---|---|---|---|
| LedgerLite | 9% | 未给出(≤31%) | 31% | 第二到第五家的单家占比 |
| Tidecast | Steam 74.7% 净收入 | 不适用 | 不适用 | 单一玩家占比 |
| Nord Studio | 未给出(≤48%) | 48% | 未给出(≥48%) | 前两大客户占比 |
这里必须澄清一个常见混淆:Nord Studio 的 72.7% 是服务收入占总收入的比例,不是前两大客户的占比。案例事实表给的是「前 3 家客户合计 48%」,没有给前两大客户的具体数字。把 72.7% 当成前两大客户占比,会把「收入线集中」误读成「客户集中」,进而把估值折价的原因也归错。
LedgerLite 的前三大也没有直接给出。我们只知道最大一家 9%、前五家 31%,所以前三家最多 31%,具体是多少取决于第二、第三家。第 7.1 节的教学拆解假设第二到第五家各占 5.5%,前三家合计 20%;这个假设可以用来演示,但不能当成事实表数字。
口径定好之后,才能谈阈值。前五大 31% 在 LedgerLite 是「中等」,在 Nord 如果只看前三大 48% 就已经是「集中」;同一个 30% 上下,含义不同,因为客户数和分布不同。
前五大占比:一个入口,三个盲区
前五大占比的公式很简单:
前五大占比 = 前五大客户收入之和 ÷ 总收入
LedgerLite 的前五大是 31%,换算成金额是 74,474 美元;最大一家 9%,是 21,622 美元;其余 255 家合计 69%,是 165,766 美元。前五家的平均单客年收入是 14,895 美元,长尾的平均单客年收入是 650 美元,相差 22.9 倍。
这个数字有三个盲区。
第一,它不告诉你前五家内部是否均匀。31% 可以是「9% + 4 × 5.5%」,也可以是「9% + 22% + 0 + 0 + 0」。前者的风险是五家一起走,后者的风险是第二家一家走。两种结构的应对动作不同:前者要管五份合同,后者要管一份合同。
第二,它不告诉你长尾有多厚。LedgerLite 的 255 家客户贡献 69%,所以即使前五家全部流失,仍有 69% 的收入和 255 个客户;如果一家公司只有 10 个客户,前五家 31% 意味着剩下五家贡献 69%,长尾非常薄。同样是 31%,客户数 260 和客户数 10 是两种生意。
第三,它不区分「客户集中」和「渠道集中」。Tidecast 没有「前五大客户」,因为 9,600 次买断交易里没有稳定的客户账户;它的集中度在渠道:Steam 占销量 80%,按净收入算是 74.7%。用「前五大客户占比」去问 Tidecast,会得到一个没有意义的 0 或「不适用」。
一个更稳的做法是同时看三个数:最大客户、前五大、客户总数。LedgerLite 是 9% / 31% / 260,Tidecast 是「不适用 / 不适用 / 9,600 次交易」,Nord 是「未给出 / 未给出 / 前 3 家 48%」。三个数放在一起,才能判断「31%」是厚长尾里的 31%,还是薄客户里的 31%。
HHI:比前五大更细的分布指标
HHI 的公式是:
HHI = Σ(第 i 个客户占比)² × 10000
结果在 0 到 10000 之间:0 表示无限分散,10000 表示只有一个客户。它比前五大更细,因为每一个客户都进入平方和;它也比前五大更难造假,因为不能只报前几名。
按 7.1 节的三层拆解,假设第二到第五家各占 5.5%、长尾 255 家平均分配 69%,LedgerLite 的 HHI 是:
HHI = 9%² + 4 × 5.5%² + 255 × (69% ÷ 255)²
= 0.0081 + 0.0121 + 0.001867
= 0.022067 → 221 bps
最大一家贡献 81 bps,四家重要客户贡献 121 bps,255 家长尾只贡献 18.7 bps。长尾的厚度把 HHI 压在 221,远低于「集中」的阈值。这个数不是事实表直接给的,而是教学拆解;如果第二到第五家的实际分布更不均,HHI 会更高。
三个案例的 HHI 对比如下:
| 案例 | 分布口径 | HHI | 解读 |
|---|---|---|---|
| LedgerLite | 客户收入,按三层教学拆解 | 221 | 分散;长尾把指数压低 |
| Tidecast | 渠道净收入 74.7% / 25.3% | 6217 | 高危;Steam 一家独大 |
| Nord Studio | 前 3 家 48%,其余未知 | ≥768 | 至少中等;实际取决于前三家内部差距 |
Nord 的 768 是一个下界:如果前三大各占 16%,平方和是 3 × 16%² = 768 bps;如果三家是 30%、10%、8%,平方和会变成 1064 bps。仅凭「前 3 家 48%」不能确定 HHI,只能确定它不低于 768。这也是为什么口径必须写出来:一个下界和一个点估计,不能放在同一张表里比较。
如果把 Nord 的收入线而不是客户作为单位,HHI 是 72.7%² + 27.3%² = 6031 bps,和 1.2 节的结果一致。同一个 6031,在收入线口径下是「咨询一条线占七成」,在客户口径下不是。两个 HHI 不能互换。
四档阈值与对应动作
把四个视角合在一起,可以定义四档。阈值是教学参数,不是行业标准;换一套阈值,档位会变,但「哪个视角最差」不会变。
| 档位 | 最大客户/渠道 | 前三大 | 前五大 | HHI | 动作 |
|---|---|---|---|---|---|
| 分散 | < 10% | < 20% | < 25% | < 500 | 正常续约管理,季度复盘 |
| 中等 | 10%–20% | 20%–40% | 25%–50% | 500–1500 | 建前五名续约日历,准备备选 |
| 集中 | 20%–30% | 40%–60% | 50%–80% | 1500–3000 | 合同条款、信用监控、分散计划 |
| 高危 | ≥ 30% | ≥ 60% | ≥ 80% | ≥ 3000 | 估值折价、依赖披露、退出预案 |
规则是「取最差」:四个视角里只要有一个落入更高档,整体就按更高档处理。一个 35% 的最大客户不能因为前五大只有 40% 就被判成「中等」。
三个案例的落点:
- LedgerLite:最大客户 9% 落在分散,前五大 31% 落在中等,HHI 221 落在分散;整体是中等。动作是建前五名续约日历,按季度跟踪,不需要应急方案。
- Tidecast:渠道 74.7% 落在高危,HHI 6217 也落在高危;整体是高危。动作不是「多找几个玩家」,而是降低平台依赖:官网占比从 20% 往上做、平台条款备份、把 Wishlist 和邮件列表变成可迁移资产。
- Nord Studio:前三大 48% 落在集中,服务收入 72.7% 在收入线口径下也是集中,转介绍依赖 55%;整体是集中。动作是客户分散、服务产品化、合伙人依赖条款,而不是单纯「多签几个客户」。
同一个「集中」档,三种动作完全不同:客户集中靠客户分散,平台集中靠渠道迁移,服务线集中靠产品化。阈值只告诉你「该动作了」,不告诉你「动哪个」。平台集中的退出路径见 平台风险与退出 。
集中度如何进入估值
Nord Studio 的估值折价是本章最直接的例子。标准锚是 4.5 × SDE = 154,440 美元;买方报价下调到 3.0 × SDE = 102,960 美元;差额是 51,480 美元,相当于标准锚的 33.3%。案例事实表给出的原因是「服务收入占 72.7%、依赖两位合伙人」;集中度是这两个原因的共同表现,不是唯一的独立原因。
| 案例 | 估值锚 | 集中度类型 | 对倍数的影响 |
|---|---|---|---|
| LedgerLite | 3.2 × ARR = $768,768 | 客户前五 31%,长尾厚 | 不构成折价主因;战略买方看经常性 |
| Tidecast | 1.8 × 收入 = $248,708 | Steam 80% 销量 | 平台风险压低倍数,但 IP 可授权 |
| Nord Studio | 4.5 × SDE = $154,440;买方 3.0 × SDE = $102,960 | 前 3 家 48%,服务 72.7% | 折价 33.3%,客户与服务线双重集中 |
LedgerLite 的前五家 31% 没有阻止 3.2 × ARR 的报价,因为它的收入是经常性的、客户数是 260、最大一家只有 9%。集中度不是「超过 30% 就一定折价」,而是「买方要花多少时间验证这个收入明年还在」。Nord 的问题不是前三大 48% 本身,而是这 48% 背后是两位合伙人的时间和关系;客户走了,关系也走了,买方买不到可迁移的资产。
Tidecast 的 80% 是平台集中,不是客户集中。平台集中的估值影响通常体现在倍数和交易结构上:买方会要求平台协议的可转让性、历史销量数据、退款与分成条款。它的 1.8 × 收入低于 LedgerLite 的 3.2 × ARR,不能全部归因于集中度,但平台依赖是重要一项。
把集中度写成代码
下面的 concentration.ts 把四个视角固化成函数:topNShare 算前 N 大占比,hhi 算客户 HHI,concentrationMetrics 一次返回最大、前三、前五和 HHI,concentrationBand 按「取最差」规则给出四档。所有金额用整数美分,比率用基点,非法输入返回 Result 错误。
// file: src/unit/concentration.ts
// tier: L1
// verify: node --test test/unit/concentration.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 CustomerRevenue = {
name: string;
annualCents: Cents;
};
export type ConcentrationMetrics = {
totalCents: Cents;
count: number;
top1Bps: Bps;
top3Bps: Bps;
top5Bps: Bps;
hhi: Bps;
};
export type ConcentrationBand =
| "diversified"
| "moderate"
| "concentrated"
| "critical";
function validate(customers: CustomerRevenue[]): Result<Cents> {
if (customers.length === 0) {
return fail("at least one customer is required");
}
for (const customer of customers) {
if (customer.name.trim() === "") {
return fail("customer name must not be empty");
}
if (!Number.isInteger(customer.annualCents) || customer.annualCents < 0) {
return fail(`invalid annual revenue for ${customer.name}`);
}
}
const total = sum(customers.map((customer) => customer.annualCents));
if (total <= 0) {
return fail("total customer revenue must be positive");
}
return ok(total);
}
function descending(customers: CustomerRevenue[]): CustomerRevenue[] {
return [...customers].sort((a, b) => b.annualCents - a.annualCents);
}
/** Sum of the largest n customers divided by total revenue, in basis points. */
export function topNShare(
customers: CustomerRevenue[],
n: number,
): Result<Bps> {
const checked = validate(customers);
if (!checked.ok) {
return checked;
}
if (!Number.isInteger(n) || n <= 0 || n > customers.length) {
return fail("n must be an integer between 1 and the customer count");
}
const top = descending(customers)
.slice(0, n)
.reduce((acc, customer) => acc + customer.annualCents, 0);
return ok(divide(top, checked.value));
}
/** Herfindahl-Hirschman index over customers, normalized to 0-10000. */
export function hhi(customers: CustomerRevenue[]): Result<Bps> {
const checked = validate(customers);
if (!checked.ok) {
return checked;
}
const index = customers.reduce((acc, customer) => {
const share = divide(customer.annualCents, checked.value);
return acc + share * share;
}, 0);
return ok(Math.round(index / 10000));
}
export function concentrationMetrics(
customers: CustomerRevenue[],
): Result<ConcentrationMetrics> {
const checked = validate(customers);
if (!checked.ok) {
return checked;
}
const top1 = topNShare(customers, 1);
if (!top1.ok) {
return top1;
}
const top3 = topNShare(customers, Math.min(3, customers.length));
if (!top3.ok) {
return top3;
}
const top5 = topNShare(customers, Math.min(5, customers.length));
if (!top5.ok) {
return top5;
}
const index = hhi(customers);
if (!index.ok) {
return index;
}
return ok({
totalCents: checked.value,
count: customers.length,
top1Bps: top1.value,
top3Bps: top3.value,
top5Bps: top5.value,
hhi: index.value,
});
}
function worse(a: ConcentrationBand, b: ConcentrationBand): ConcentrationBand {
const order: ConcentrationBand[] = [
"diversified",
"moderate",
"concentrated",
"critical",
];
return order.indexOf(a) >= order.indexOf(b) ? a : b;
}
function bandFromTop1(bps: Bps): ConcentrationBand {
if (bps < 1000) return "diversified";
if (bps < 2000) return "moderate";
if (bps < 3000) return "concentrated";
return "critical";
}
function bandFromTop3(bps: Bps): ConcentrationBand {
if (bps < 2000) return "diversified";
if (bps < 4000) return "moderate";
if (bps < 6000) return "concentrated";
return "critical";
}
function bandFromTop5(bps: Bps): ConcentrationBand {
if (bps < 2500) return "diversified";
if (bps < 5000) return "moderate";
if (bps < 8000) return "concentrated";
return "critical";
}
function bandFromHhi(bps: Bps): ConcentrationBand {
if (bps < 500) return "diversified";
if (bps < 1500) return "moderate";
if (bps < 3000) return "concentrated";
return "critical";
}
/**
* The band is the worst of four views: largest customer, top three,
* top five and HHI. A single dominant customer must not hide behind
* a low top-five total.
*/
export function concentrationBand(
metrics: ConcentrationMetrics,
): ConcentrationBand {
const views: ConcentrationBand[] = [
bandFromTop1(metrics.top1Bps),
bandFromTop3(metrics.top3Bps),
bandFromTop5(metrics.top5Bps),
bandFromHhi(metrics.hhi),
];
return views.reduce(worse);
}
代码里有四个刻意的边界。
第一,topNShare 先排序再取前 N,所以输入顺序不影响结果;调用方不需要自己保证客户列表按收入降序。
第二,n 不能超过客户数,也不能为 0。只有 3 个客户时,前五大占比就是前三大占比,函数会报错而不是悄悄返回 100%;调用方必须显式处理「客户数少于 5」的情况。
第三,hhi 用整数基点计算平方和,最后除以 10000 归一化。260 个客户的长尾占比很小,浮点误差会累积;整数运算保证同一组数据每次得到同一个结果。
第四,concentrationBand 取四个视角的最差档位,而不是平均。集中度是风险指标,平均会把一个 35% 的最大客户稀释成一个「看起来还行」的中等档。
小结
- 集中度不是一个百分比,而是「某个客户、渠道或收入线消失时,收入会少多少」的概率。
- 四个口径:最大客户、前三大、前五大、HHI;前五大 31% 在 260 家客户里是中等,在 10 家客户里可能是高危。
- LedgerLite:最大 9%、前五 31%、HHI 约 221,整体中等;长尾厚度是它的缓冲。
- Tidecast:Steam 销量 80%、净收入 74.7%,HHI 6217,属于平台高危;动作是渠道迁移,不是多找玩家。
- Nord Studio:前 3 家 48%,服务收入 72.7%,客户 HHI 至少 768,收入线 HHI 6031;属于客户与服务线双重集中。
- 四档阈值要「取最差」:任一视角进入更高档,整体就按更高档处理。
- 集中度进入估值的方式是「买方验证收入可持续性的成本」;Nord 的 4.5× SDE 被下调到 3.0× SDE,折价 33.3%,集中度是共同表现,不是唯一原因。
阅读导航:上一节:7.1 客户分层:按收入、按成本 · 下一节:7.3 续约、扩张与收缩收入 。
常见错误与边界
- 把前五大占比当成集中度的全部。 31% 可以是九家均匀,也可以是一家独大;必须同时看最大客户和 HHI。
- 把 Nord 的 72.7% 当成前两大客户占比。 72.7% 是服务收入占总收入的比例;事实表给的是前 3 家客户 48%,没有前两大客户占比。
- 把 Tidecast 的 Steam 80% 当成客户集中。 它是渠道集中,分层对象是渠道,不是玩家账户。
- 用客户 HHI 和收入线 HHI 互相替代。 Nord 的客户 HHI 至少 768,收入线 HHI 是 6031;两个数回答两个问题。
- 把 HHI 下界当点估计。 只知道前 3 家 48% 时,HHI 只能确定不低于 768;把它写成「Nord 的 HHI 是 768」会低估实际集中度。
- 阈值不取最差。 四个视角平均会把 35% 的最大客户稀释成中等;集中度是风险指标,必须按最差档处理。
- 前五大 31% 就认为安全。 31% 的收入是 74,474 美元,刚好覆盖 72,000 美元运营费用;前五家集体流失会把 SDE 打到约 67,540 美元(变动成本同比例下降);最保守口径是 55,766 美元。
- 把集中度折价归因于单一原因。 Nord 的折价同时来自服务收入 72.7% 和两位合伙人依赖;集中度是表现,不是唯一原因。
- 平台集中用「多找几个玩家」解决。 Tidecast 的动作是官网占比、平台条款备份和可迁移的邮件列表,不是把 9,600 个买家拆成「大客户」。
- 只看静态占比,不看可替换性。 一个 9% 的标准客户和一个 9% 的定制客户,替换成本完全不同;集中度要和合同期限、迁移成本一起看。
集中度回答「少一个会不会疼」,下一节回答「现有的客户明年还能留下多少」。前者是分布,后者是流动;两张表放在一起,才能判断收入是「集中但稳定」还是「分散但漏水」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。