《移动游戏产品的架构·商业化与发行工程》6.3 广告收入核算与 eCPM 口径

同样叫 eCPM,分母是请求、填充还是完成,收入能差出数百万元。本节用消消 80 万 DAU、3.5 次激励、92% 填充与 88% 完成率,把 378 万元广告月流水逐层拆开,给出估算与结算的对账函数,并解释方舟 90 万元如何由 1.5 次激励与 ¥80 eCPM 得到。

两个看板,两个数字

运营看板上写着消消的激励视频 eCPM 是 ¥45,财务账本上写着广告月流水约 378 万元;方舟的 eCPM 更高,是 ¥80,广告月流水却只有约 90 万元。如果只看 eCPM,会得出「方舟广告效率更高」的结论;如果只看流水,又会觉得方舟的广告几乎没有做起来。

两个数字都没错,错的是把它们放在同一个分母上比较。eCPM 是「每千次某种展示的收入」,分母可以是请求、填充、展示或完成;分子可以是平台结算、后台估算或扣除无效流量后的净额。分母和分子不写清楚,eCPM 就只是一个看起来精确、实际无法复现的比值。

本节把广告收入拆成一条可计算的链路:请求进入,按填充率变成展示,按完成率变成可计费完成,再乘 eCPM 得到收入。最后把估算与结算分开记账,说明月底对账时应该先对齐什么。

eCPM 的分母到底是什么

同一个广告位,同一天,同一个 eCPM,换一个分母就能得到三个不同的收入。以消消的一天为例:

分母口径分母数量日收入月收入(按 30 天)
请求3,458,498¥155,632.41≈467 万元
填充3,181,818¥143,181.81≈430 万元
完成2,800,000¥126,000.00378 万元

表里的三个分母来自同一条漏斗:3,458,498 次请求按 92% 填充得到 3,181,818 次填充,再按 88% 完成得到 2,800,000 次完成。eCPM 固定为 ¥45,分母越大,算出来的收入越高。只有完成口径的 378 万元与事实表里的广告月流水一致。

这不是说另外两个口径是错的。请求口径适合做容量规划:要准备多少带宽、多少并发、多少广告位库存;填充口径适合做 SDK 比价:同一个请求在不同广告源下能拿到多少填充;完成口径适合做收入核算:只有真正看完的展示才会被广告平台计费。三种口径各有用途,但不能混用。

常见的错误是拿「请求 eCPM」去乘「完成次数」,或者拿「完成 eCPM」去乘「请求次数」。前者会低估,后者会高估,而且误差会随着填充率与完成率变化,月底对账时很难解释。

为什么 3.5 次只能读成完成次数

事实表里写的是「人均激励次数/日 3.5 次」。这个「次数」如果不说明是哪一层,收入就会出现三种答案。消消的 DAU 是 80 万,按 3.5 次计算:

80 万 × 3.5 = 280 万次/日
280 万 ÷ 1000 × ¥45 = ¥12.6 万/日
¥12.6 万 × 30 = ¥378 万/月

只有把 3.5 读成「完成次数」,这条算式才与事实表的 378 万元一致。若把 3.5 读成请求次数,还要再乘 92% 与 88%,月收入会降到约 306 万元;若读成填充次数,再乘 88%,约为 333 万元。同一组数字,三种读法,差距足够让一个投放决策转向。

因此,广告指标的定义必须写到分母这一层。产品文档里的「人均激励次数」应该明确写成「人均完成激励次数」;如果确实要统计请求次数,就单独命名,不要复用同一个词。指标名称省下的几个字,会在收入核算时变成一场跨部门的争论。

方舟的 1.5 次同样按完成口径读:25 万 DAU × 1.5 = 37.5 万次完成/日,37.5 万 ÷ 1000 × ¥80 = ¥3 万/日,30 天正好 90 万元。两个案例的数字都能闭合,前提是口径一致。

估算链路:请求 → 填充 → 完成

收入估算从请求开始,而不是从 DAU 开始。DAU 只能告诉我们有多少人,不能告诉我们每个人发起了多少次请求。完整链路是:

请求数 × 填充率 = 填充数
填充数 × 完成率 = 完成数
完成数 ÷ 1000 × eCPM = 收入

反过来,如果已知完成数与两个比率,也能估算请求数。消消每天 280 万次完成,填充率 92%、完成率 88%,则请求数约为 280 万 ÷ (0.92 × 0.88) ≈ 345.8 万次。方舟每天 37.5 万次完成,填充率 88%、完成率 93%,请求数约为 37.5 万 ÷ (0.88 × 0.93) ≈ 45.8 万次。

指标消消方舟
日完成次数2,800,000375,000
填充率92%88%
完成率88%93%
反推日请求次数≈3,458,498≈458,211
激励 eCPM¥45¥80
日广告收入¥126,000¥30,000
月广告收入≈378 万元≈90 万元

这张表里,方舟的 eCPM 几乎翻倍,但日完成次数只有消消的约八分之一,最终日收入只有消消的约四分之一。高 eCPM 不能弥补低频次;反过来说,高频次也不能掩盖低完成率。收入是三个因子的乘积,任何单指标都不能单独解释结果。

取整也会影响对账。填充数与完成数在代码里是整数,按比率计算后要四舍五入;如果先算收入再取整,和先算人数再取整,最后可能差几分钱。工程上应固定「先算整数人数,再算金额」的顺序,并在报表里保留原始比率,避免月底出现无法解释的分差。

可运行产物:收入核算与对账

下面的实现把漏斗、eCPM 分母、估算和对账拆成独立的纯函数。金额统一用整数分,eCPM 也用分表示;estimateRevenueCents 显式接收分母口径,调用方不能隐式选择。

// file: src/biz/ad-revenue.ts
// tier: L1
// verify: node --test test/biz/ad-revenue.test.ts

export type EcpmBasis = "request" | "filled" | "completed";

export interface AdFunnel {
  readonly requests: number;
  readonly fillRate: number;
  readonly completionRate: number;
}

export interface FunnelCounts {
  readonly requests: number;
  readonly filled: number;
  readonly completed: number;
}

export interface Variance {
  readonly deltaCents: number;
  readonly deltaRate: number;
}

function assertRate(value: number, name: string): void {
  if (!Number.isFinite(value) || value < 0 || value > 1) {
    throw new Error(`${name} must be within [0, 1], got ${value}`);
  }
}

function assertCents(value: number, name: string): void {
  if (!Number.isInteger(value) || value < 0) {
    throw new Error(`${name} must be a non-negative integer, got ${value}`);
  }
}

export function countFunnel(funnel: AdFunnel): FunnelCounts {
  if (!Number.isInteger(funnel.requests) || funnel.requests < 0) {
    throw new Error(`requests must be a non-negative integer, got ${funnel.requests}`);
  }
  assertRate(funnel.fillRate, "fillRate");
  assertRate(funnel.completionRate, "completionRate");

  const filled = Math.round(funnel.requests * funnel.fillRate);
  const completed = Math.round(filled * funnel.completionRate);
  return { requests: funnel.requests, filled, completed };
}

export function impressionsFor(counts: FunnelCounts, basis: EcpmBasis): number {
  switch (basis) {
    case "request":
      return counts.requests;
    case "filled":
      return counts.filled;
    case "completed":
      return counts.completed;
  }
}

export function estimateRevenueCents(
  funnel: AdFunnel,
  ecpmCents: number,
  basis: EcpmBasis,
): number {
  assertCents(ecpmCents, "ecpmCents");
  const counts = countFunnel(funnel);
  const impressions = impressionsFor(counts, basis);
  return Math.round((impressions * ecpmCents) / 1000);
}

export function effectiveEcpmCents(revenueCents: number, impressions: number): number {
  assertCents(revenueCents, "revenueCents");
  if (!Number.isInteger(impressions) || impressions < 0) {
    throw new Error(`impressions must be a non-negative integer, got ${impressions}`);
  }
  if (impressions === 0) {
    return 0;
  }
  return Math.round((revenueCents * 1000) / impressions);
}

export function reconcile(estimatedCents: number, settledCents: number): Variance {
  assertCents(estimatedCents, "estimatedCents");
  assertCents(settledCents, "settledCents");
  const deltaCents = settledCents - estimatedCents;
  const deltaRate =
    estimatedCents === 0 ? (settledCents === 0 ? 0 : 1) : deltaCents / estimatedCents;
  return { deltaCents, deltaRate };
}

export function formatYuan(cents: number): string {
  assertCents(cents, "cents");
  return `¥${(cents / 100).toFixed(2)}`;
}

export function formatEstimate(
  funnel: AdFunnel,
  ecpmCents: number,
  basis: EcpmBasis,
): string {
  const counts = countFunnel(funnel);
  const revenue = estimateRevenueCents(funnel, ecpmCents, basis);
  return `basis=${basis} requests=${counts.requests} filled=${counts.filled} completed=${counts.completed} revenue=${formatYuan(revenue)}`;
}

export function formatVariance(variance: Variance): string {
  const sign = variance.deltaCents < 0 ? "-" : "";
  const absolute = Math.abs(variance.deltaCents);
  return `delta=${sign}¥${(absolute / 100).toFixed(2)} rate=${(variance.deltaRate * 100).toFixed(2)}%`;
}

countFunnel 先校验请求数与两个比率,再按「先填充、后完成」的顺序取整;estimateRevenueCents 根据 basis 选择分母,避免把请求 eCPM 乘到完成数上。effectiveEcpmCents 是反向函数:给定收入与分母,算出实际 eCPM,用于验证报表里的数字是否自洽。

reconcile 只做减法与比例,不判断「差多少算异常」。异常阈值是运营策略,不是数学事实;把阈值写进核算函数,会让同一个函数在不同产品上给出不同的对账结论。函数只负责把差异算清楚,判断留给对账流程。

结算口径:账单不是估算的复印件

估算回答「按今天的漏斗和 eCPM,应该有多少收入」,结算回答「广告平台最终付了多少钱」。两者之间至少隔着四层差异:

  1. 范围不同。 估算通常按单个广告位或单个产品计算,结算账单可能按平台、地区、广告位类型拆分,还包含品牌合作、底价保量等其他条目。
  2. 无效流量扣减。 广告平台会剔除作弊点击、异常展示和无效完成。事实表里消消的作弊点击占比是 1.5%,方舟是 0.6%;这个比例描述的是点击质量,不能直接当成收入扣减比例,但方向是明确的。
  3. 结算周期与汇率。 海外收入按平台结算周期入账,涉及汇率换算;估算如果按本地货币计算,月底必然出现差额。
  4. 取整与归因窗口。 平台按自己的归因窗口统计完成,客户端按自己的会话统计完成,两边的边界事件可能落在不同的一天。

因此,估算与结算要放在两个账本里,用 reconcile 算差异,而不是用结算数反推一个「正确」的 eCPM。反推出来的 eCPM 会把范围差异、扣减和汇率全部塞进一个比值,看起来对上了,实际上失去了排查能力。

对账的第一步不是调参数,而是对齐范围:估算覆盖了哪些广告位、哪些地区、哪些日期;账单覆盖了哪些;差异里有多少来自范围,多少来自扣减,多少来自汇率。范围没对齐之前,任何 eCPM 调整都是在猜。

双案例:两种频率,两种收入

消消的广告月流水约 378 万元,占月流水 38%;激励视频人均每日 3.5 次,eCPM ¥45,填充率 92%,完成率 88%。它靠高频、低门槛的激励覆盖大量不付费玩家。

方舟的广告月流水约 90 万元,只占月流水 3%;激励视频人均每日 1.5 次,eCPM ¥80,填充率 88%,完成率 93%。eCPM 更高但频次低,广告始终是内购的补充,收入只有消消的约四分之一。

取舍:把 eCPM 当唯一 KPI 的代价

eCPM 是一个容易被优化的指标。提高底价、增加插屏、拉长激励视频、减少低价值广告源,都能在短期内推高 eCPM;代价可能是填充率下降、完成率下降、玩家留存下降。消消的填充率 92%、完成率 88%,说明它已经在供给与体验之间做了取舍;如果继续追求更高的 eCPM,首先受损的往往是填充率。

更稳妥的做法是把「完成口径的广告收入/DAU」作为主指标,同时观察填充率、完成率与次日留存。这个指标能同时反映单价、频次和转化,不容易被单一手段拉高。它也有缺点:收入/DAU 会把买量带来的新增用户混进分母,短期投放会稀释指标;因此还要按自然量与买量分开看。

另一个取舍是估算的时效与精度。日估算可以每天更新,但受取整、迟到回调和时区影响;月结算更准,但反馈太慢,无法指导当天的运营。案例里的做法是两套都保留:日估算用于发现异常,月结算用于确认收入,两者之间用差异率连接,而不是互相替代。

小结

  • eCPM 必须带分母:请求、填充、完成三种口径的收入可以差出数百万元。
  • 消消的 3.5 次只有读成完成次数,才能与 378 万元广告月流水闭合;方舟的 1.5 次与 ¥80 eCPM 对应 90 万元。
  • 收入链路是请求 × 填充率 × 完成率 × eCPM,取整顺序要固定,原始比率要保留。
  • 估算与结算分开记账,先对齐范围,再分析无效流量、结算周期与汇率差异。
  • 把 eCPM 当唯一 KPI 会牺牲填充率、完成率与留存,主指标应看完成口径的收入/DAU。

阅读导航:上一节:6.2 广告 SDK 生命周期与填充失败兜底 · 下一节:7.1 归因链路:点击、安装、激活与事件回传 。

常见错误与边界

  1. 把 eCPM 直接当成收入。eCPM 是每千次展示的单价,不是总量;没有分母和展示次数,它无法回答这个月赚了多少钱。
  2. 不写清楚 eCPM 的分母。请求、填充、完成三种口径能差出数百万元,指标名相同但含义不同,跨团队沟通必然出错。
  3. 用请求数乘完成口径的 eCPM。这会把没有填充、没有完成的请求也算成收入,估算系统性偏高。
  4. 忽略填充率。消消 92%、方舟 88% 的填充率意味着每 100 次请求都有真实损失,跳过这一步会高估收入。
  5. 忽略完成率。激励视频只有完成才计费,消消 88%、方舟 93% 的完成率必须进入公式,不能只用展示数。
  6. 拿激励视频 eCPM 和插屏 eCPM 直接比较。消消激励 eCPM ¥45、插屏 ¥18,两者的展示场景与计费方式不同,直接比较会误导库存分配。
  7. 把作弊点击比例当成收入扣减比例。1.5% 与 0.6% 描述的是点击质量,实际扣减以平台账单为准,不能直接相乘。
  8. 日估算和月结算混在一个账本。两者的范围、时区和扣减口径不同,混在一起会让差异无法归因。
  9. 对账第一步就调 eCPM 参数。范围没对齐之前调参数,只是把差异藏进单价,下个月还会以另一种形式出现。
  10. 只看 eCPM,不看完成次数与留存。高 eCPM 可能来自更激进的广告形式,代价是填充率、完成率和次日留存同时下降。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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