《独立开发者的商业指南·收入与财务工程》13.1 三张表的横向对照

三张表放在一起才看得出结构差异。本节把 LedgerLite、Tidecast、Nord Studio 的利润表、现金流量表与资产负债表压到同一套五层结构上,做一次横向对照与勾稽检查,指出哪些行可以直接比、哪些行因为口径不同不能比,并给出两张缺失报表的补法。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

第 3 章已经把三个案例各自的利润表、现金流量表和资产负债表算过一遍。那一章的任务是「每一张表怎么搭」,这一章的任务是「三张表放在一起看什么」。后者更难,因为横向对照会把口径差异放大:同一行数字在不同案例里可能代表不同的东西,直接比大小会得出错误结论。

这一节做三件事:把三案例压到同一套五层结构上,检查资产负债表是否勾稽,再用现金转换率解释「净利润为什么不是现金」。最后列出五处不能直接比的地方,作为进入卷二前的口径备忘录。

统一后的五层结构

利润表有十几行,但真正决定结构的只有五层:收入、毛利、SDE、税前利润、净利润。把三案例代入同一套公式,得到下表。金额单位为美元,比率按精确金额计算后四舍五入。

层公式LedgerLiteTidecastNord Studio
收入确认收入$240,240$138,171$154,320
毛利收入 − 变动成本$202,240$138,171$130,320
SDE毛利 − 运营费用$130,240$121,171$34,320
税前利润SDE − 所有者薪酬$70,240$121,171$34,320
净利润税前 − 所得税$58,299$92,090$25,740

三行的毛利率分别是 84.2%、100.0%、84.4%,看起来差距不大;但 SDE 率是 54.2%、87.7%、22.2%,差了近四倍。差距出在运营费用:LedgerLite 72,000,Tidecast 17,000,Nord 96,000。

Tidecast 的 100.0% 毛利率不是经营优势,而是列示口径。它的平台分成、退款、支付费已经在收入之前扣掉,利润表上不再重复列为变动成本。如果把标价收入 191,904 美元放在第一行,毛利率会变成 72.0%,但 SDE 仍是 121,171 美元。横向对照时,先看毛利率的分子分母是不是同一种收入,再比大小。

Nord 的 SDE 率 22.2% 也不能简单读成「效率低」。它的 96,000 美元是两位合伙人的工资,属于服务交付的必要成本。把工资移到毛利之下,毛利率会从 84.4% 掉到 22.2%,SDE 不变。同一个生意,分类位置不同,两个比率就互换。这就是第 3 章反复强调「分类决定比率」的原因。

资产负债表的勾稽

三个案例里只有 LedgerLite 给出了完整的资产负债表。它的资产合计 110,000 美元,负债 30,000 美元,权益 80,000 美元,两边相等。

项金额说明
现金$95,000银行对账单口径
应收$12,000已开票未回款
固定资产$3,000设备,未计折旧
资产合计$110,000
递延收入$22,000已收未确认
应付$8,000供应商账期
权益$80,000期初权益加留存收益
负债与权益合计$110,000

勾稽检查只有一条:资产 = 负债 + 权益。这条不成立时,先查三处:递延收入的期间归属、固定资产的折旧、权益里的留存收益结转。三处都查过仍然对不上,通常是有一笔应计费用没有入账,或者一笔在途资金被同时记在两边。

Tidecast 和 Nord Studio 没有完整的资产负债表,这不是省略,而是案例设定只给出了利润表与现金口径。真实尽调中,这两张缺失的表恰恰是买方最先索要的材料:没有资产负债表,就无法确认应收、递延收入、应付和权益的真实余额。第 12 章的数据室清单把「三张报表齐全」列为必备项,原因就在这里。

现金转换率

净利润和经营现金流的差额,来自非现金项目与营运资金变化。用「经营现金流 ÷ 净利润」得到的比率,可以快速判断利润的含金量。

案例净利润经营现金流转换率差额来源
LedgerLite$58,299$64,299110.3%递延收入 +$8,000、应收 −$2,000
Tidecast$92,090$92,090100.0%案例按可支配现金列示,无营运资金调整
Nord Studio$25,740$25,740100.0%同上,应收变化未单列

LedgerLite 的转换率高于 100%,是因为年付客户提前付钱,递延收入增加 8,000 美元,现金比利润多。这不是异常,而是订阅生意的常态:只要年付比例稳定,递延收入会持续为现金流提供缓冲。

Tidecast 和 Nord 的 100.0% 是简化口径,不代表它们没有营运资金变化。Tidecast 的平台月结 30 天、Nord 的 Net 30 账期,都会在年末形成应收或应付。案例只给出了年末应收占年收入的比例(0% 与 7.3%),没有给出期初期末的完整余额,所以无法还原完整的间接法现金流量表。

这里有一条实用规则:转换率高于 100% 时,先确认递延收入是否可持续;转换率低于 80% 时,先查应收是否在堆积。两端都正常,才说明利润和现金在同一条线上。

五处不能直接比的地方

横向对照最容易犯的错,是把不同口径的数字放在同一列比较。以下五处必须加注,否则结论会反。

第一,Tidecast 的收入是净得,不是标价。 它的 138,171 美元已经扣掉平台分成、退款与支付费。拿它和 LedgerLite 的 240,240 美元比「收入规模」,等于拿净额比总额。

第二,SDE 是否扣除所有者薪酬不一致。 LedgerLite 扣了 60,000 美元创始人薪酬,Nord 没有扣两位合伙人的 96,000 美元工资。比较 SDE 前先把口径拉平,否则 130,240 与 34,320 的差距会被高估。

第三,毛利率的分母不同。 Tidecast 的 100.0% 以净得为分母,LedgerLite 与 Nord 以确认收入为分母。三者只有在同一分母下才能比较。

第四,现金流的口径不同。 LedgerLite 给出的是完整经营现金流,Tidecast 与 Nord 给出的是「可支配现金」,即 SDE 减所得税,没有营运资金调整。

第五,资产负债表只有一家完整。 另外两家缺表,不代表它们的资产负债结构简单,只代表案例没有给出。

把这五条写进自己的对照表,横向比较才有意义。缺一条,数字就会「看起来能比」,实际上不能。

小结

三张表的横向对照,先统一五层结构,再查资产负债表勾稽,最后用现金转换率判断利润含金量。三个案例的 SDE 率从 22.2% 到 87.7%,差异主要来自运营费用与列示口径,不是经营效率的简单排序。

baseline.ts 把这三步写成了可复算的函数:buildBaseline 统一五层结构,checkBalanceSheet 做勾稽,cashBridge 还原净利润到现金的桥。三个案例的数据代入后,得到的数字与上表一致。

阅读导航:上一节:12.3 常见红旗与提前修复 · 下一节:13.2 单位经济与估值区间 。

常见错误与边界

  1. 把净得当收入比规模。 Tidecast 的 138,171 是净得,标价收入是 191,904。跨案例比收入前先确认是不是同一层。
  2. 用毛利率给生意排序。 毛利率受列示口径影响极大,Tidecast 的 100.0% 与 Nord 的 84.4% 不在同一个分母上。
  3. 看到 SDE 高就认为效率高。 SDE 没有扣除所有者薪酬时,会把「人少」误读成「效率高」。
  4. 忽略缺失的资产负债表。 缺表不是没问题,而是没证据。尽调时缺表会被当作风险。
  5. 把转换率当成质量指标单独用。 转换率高可能来自递延收入堆积,转换率低可能来自应收账期,必须结合营运资金一起看。
  6. 勾稽对不上就调数字。 先找未入账的应计项,不要为了让表平而改一个科目。

边界:本节只处理案例给出的口径。真实报表可能有更多科目、更多期间调整,横向对照前应先做一次科目映射,把三家的科目对齐到同一套五层结构。

// file: src/fin/baseline.ts
// tier: L1
// verify: node --test test/fin/baseline.test.ts

import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type BaselineInput = {
  name: string;
  revenueCents: Cents;
  variableCostsCents: Cents;
  operatingExpensesCents: Cents;
  founderSalaryCents: Cents;
  taxRateBps: Bps;
  operatingCashFlowCents: Cents;
};

export type BaselineRow = {
  name: string;
  revenueCents: Cents;
  grossProfitCents: Cents;
  grossMarginBps: Bps;
  sdeCents: Cents;
  sdeMarginBps: Bps;
  pretaxCents: Cents;
  taxCents: Cents;
  netIncomeCents: Cents;
  netMarginBps: Bps;
  cashConversionBps: Bps;
};

export type BalanceCheck = {
  assetsCents: Cents;
  claimsCents: Cents;
  balanced: boolean;
  differenceCents: Cents;
};

function checkAmount(value: Cents, label: string): string | null {
  if (!Number.isInteger(value) || value < 0) {
    return `${label} must be a non-negative integer amount in cents`;
  }
  return null;
}

/**
 * Reduce one business to the same five-line structure used across the book.
 * Gross profit, SDE, pretax income and net income are derived here so the
 * three cases can be compared without re-deriving formulas by hand.
 */
export function buildBaseline(input: BaselineInput): Result<BaselineRow> {
  const fields: Array<[Cents, string]> = [
    [input.revenueCents, "revenue"],
    [input.variableCostsCents, "variable costs"],
    [input.operatingExpensesCents, "operating expenses"],
    [input.founderSalaryCents, "founder salary"],
    [input.operatingCashFlowCents, "operating cash flow"],
  ];
  for (const [value, label] of fields) {
    const error = checkAmount(value, label);
    if (error) {
      return fail(error);
    }
  }
  if (
    !Number.isInteger(input.taxRateBps) ||
    input.taxRateBps < 0 ||
    input.taxRateBps > 10000
  ) {
    return fail("tax rate must be between 0 and 10000 bps");
  }
  if (input.variableCostsCents > input.revenueCents) {
    return fail("variable costs cannot exceed revenue");
  }

  const grossProfitCents = input.revenueCents - input.variableCostsCents;
  const sdeCents = grossProfitCents - input.operatingExpensesCents;
  const pretaxCents = sdeCents - input.founderSalaryCents;
  const taxCents = pretaxCents > 0 ? applyRate(pretaxCents, input.taxRateBps) : 0;
  const netIncomeCents = pretaxCents - taxCents;

  return ok({
    name: input.name,
    revenueCents: input.revenueCents,
    grossProfitCents,
    grossMarginBps: divide(grossProfitCents, input.revenueCents),
    sdeCents,
    sdeMarginBps: divide(sdeCents, input.revenueCents),
    pretaxCents,
    taxCents,
    netIncomeCents,
    netMarginBps: divide(netIncomeCents, input.revenueCents),
    cashConversionBps: divide(input.operatingCashFlowCents, netIncomeCents),
  });
}

/**
 * The balance sheet must satisfy assets = liabilities + equity. A non-zero
 * difference is almost always an unbooked accrual, a missing deferred balance
 * or an equity roll-forward that was never closed.
 */
export function checkBalanceSheet(
  assetsCents: Cents,
  liabilitiesCents: Cents,
  equityCents: Cents,
): Result<BalanceCheck> {
  for (const [value, label] of [
    [assetsCents, "assets"],
    [liabilitiesCents, "liabilities"],
    [equityCents, "equity"],
  ] as Array<[Cents, string]>) {
    const error = checkAmount(value, label);
    if (error) {
      return fail(error);
    }
  }
  const claimsCents = liabilitiesCents + equityCents;
  return ok({
    assetsCents,
    claimsCents,
    balanced: assetsCents === claimsCents,
    differenceCents: assetsCents - claimsCents,
  });
}

/**
 * Net income and operating cash flow differ by non-cash items and working
 * capital. This helper keeps the sign convention explicit: a deferred revenue
 * increase is a positive adjustment because cash arrived before revenue.
 */
export function cashBridge(
  netIncomeCents: Cents,
  nonCashCents: Cents,
  deferredRevenueChangeCents: Cents,
  receivableChangeCents: Cents,
): Cents {
  return (
    netIncomeCents + nonCashCents + deferredRevenueChangeCents - receivableChangeCents
  );
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查