7.3 留存与付费的联合分析

把留存与付费乘在一起:按「留存档 × 付费档」切四段用户,段 ARPU = 活跃日 ×(IAA + 付费率 × ARPPU),算出混合 ARPU 39.3 分、IAA/IAP 占比 32.9%/67.0%,并用可跑的 Python 联合模型、同口径 TypeScript 内核与单文件 HTML 探索器证明「抬留存」比「抬付费率」更能撬动 ARPU。

留存和付费常被当成两个独立的指标,各有一张报表。但真正决定一款小游戏赚多少钱的,是这两个指标的乘积:一个人留得久,才有机会付费;一个人愿意付费,也得先留得住。本节把两者乘在一起算——按「留存档 × 付费档」切段,看每一段贡献了多少 ARPU。本站那篇小游戏开发综述 是单篇综述口径,把变现与留存各扫一遍;本节把两者乘进同一个公式,做成可跑、可调参、可断言的联合模型。

留存与付费为什么必须联合看

把 ARPU 拆开,结构立刻就清楚了:ARPU = 活跃日 × (每活跃日 IAA 收益 + 付费率 × 每付费用户每活跃日 ARPPU)。右边有两块。第一块 活跃日 × 每活跃日 IAA 完全由留存决定——留得越久,广告看得越多,IAA 收入越高。第二块 活跃日 × 付费率 × ARPPU 由留存和付费共同决定——注意 活跃日 同时出现在两项里,它是乘数。这就是联合分析的核心:留存不只是 IAA 的乘数,它也是 IAP 的乘数。抬高留存,两项一起涨;只抬付费率,只有第二项涨。

D 层:把「留存档 × 付费档」落成配置

下面这份是 D 层(静态层)产物,全整数、没有任何浮点。四个段各带 users(人数)、activeDays(期望活跃日,由留存决定)、payerRatePermille(付费率‰)、arppuDeciFen(每付费用户每活跃日 ARPPU);全局 iaaDeciFenPerActiveDay 是每活跃日的 IAA 收益。全部为示例假设参数,真实留存与付费结构需真实业务环境,命令行不可复现(X)。

{
  "version": 1,
  "game": "merge-island",
  "seed": 20261012,
  "observedAt": "2026-10",
  "note": "留存与付费的联合分析:把用户按「留存档 × 付费档」切成四段,每段用 activeDays(期望活跃日)、payerRatePermille(付费率‰)、arppuDeciFen(每付费用户每活跃日 ARPPU)与全局 iaaDeciFenPerActiveDay(每活跃日 IAA 收益)算段 ARPU:activeDays × (iaa + payerRate × arppu // 1000)。scenarios 用整数百分比抬高留存或付费率,看哪个杠杆更能撬动混合 ARPU。全部为示例假设参数;真实留存与付费结构需真实业务环境,命令行不可复现(X)。全整数,禁浮点。",
  "horizonDays": 30,
  "iaaDeciFenPerActiveDay": 12,
  "segments": [
    { "id": "hr-hp", "name": "高留存·高付费", "users": 3000, "activeDays": 18, "payerRatePermille": 180, "arppuDeciFen": 420 },
    { "id": "hr-lp", "name": "高留存·低付费", "users": 9000, "activeDays": 18, "payerRatePermille": 40, "arppuDeciFen": 200 },
    { "id": "lr-hp", "name": "低留存·高付费", "users": 3000, "activeDays": 6, "payerRatePermille": 220, "arppuDeciFen": 520 },
    { "id": "lr-lp", "name": "低留存·低付费", "users": 15000, "activeDays": 6, "payerRatePermille": 30, "arppuDeciFen": 180 }
  ],
  "scenarios": [
    { "id": "baseline", "name": "基线", "retentionPct": 100, "payerPct": 100 },
    { "id": "retention-up", "name": "留存 +20%", "retentionPct": 120, "payerPct": 100 },
    { "id": "payer-up", "name": "付费率 +20%", "retentionPct": 100, "payerPct": 120 }
  ]
}

S 层:把联合模型算成一张段表

下面这段是 S 层仿真脚本,读 config/l07_joint.json:按公式算出每段的 IAA / IAP 拆解与段 ARPU,汇总成混合 ARPU 与 IAA/IAP 占比,再跑三档场景对照。产出 JSON + CSV + SVG。

#!/usr/bin/env python3
# file: sim/l07_joint.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l07_joint.py --config config/ --out out/
"""留存与付费的联合分析:读 config/l07_joint.json。

把用户按「留存档 × 付费档」切段,每段:iaaArpu = activeDays × iaaDeciFenPerActiveDay;
iapArpu = activeDays × payerRatePermille × arppuDeciFen // 1000;segArpu = iaa + iap。
段收入 = users × segArpu;混合 ARPU = 总收入 // 总用户。scenarios 用整数百分比抬高留存
(activeDays)或付费率(payerRatePermille),回答「哪个杠杆更能撬动 ARPU」。
产出 JSON + CSV + SVG。全整数地板除,无浮点。真实留存与付费结构需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET


def load(d: str) -> dict:
    with open(os.path.join(d, "l07_joint.json"), encoding="utf-8") as f:
        return json.load(f)


def seg_arpu(seg: dict, iaa: int, retention_pct: int = 100, payer_pct: int = 100) -> dict:
    """段 ARPU 与 IAA/IAP 拆解(0.1 分/用户);留存抬高 activeDays,付费抬高 payerRate。"""
    days = seg["activeDays"] * retention_pct // 100
    payer_rate = seg["payerRatePermille"] * payer_pct // 100
    iaa_arpu = days * iaa
    iap_arpu = days * payer_rate * seg["arppuDeciFen"] // 1000
    return {"id": seg["id"], "name": seg["name"], "users": seg["users"], "activeDays": days,
            "payerRatePermille": payer_rate, "iaaArpuDeciFen": iaa_arpu, "iapArpuDeciFen": iap_arpu,
            "arpuDeciFen": iaa_arpu + iap_arpu,
            "revenueDeciFen": seg["users"] * (iaa_arpu + iap_arpu)}


def evaluate(cfg: dict, retention_pct: int, payer_pct: int) -> dict:
    rows = [seg_arpu(s, cfg["iaaDeciFenPerActiveDay"], retention_pct, payer_pct)
            for s in cfg["segments"]]
    users = sum(r["users"] for r in rows)
    revenue = sum(r["revenueDeciFen"] for r in rows)
    iaa = sum(r["users"] * r["iaaArpuDeciFen"] for r in rows)
    iap = sum(r["users"] * r["iapArpuDeciFen"] for r in rows)
    return {"rows": rows, "users": users, "revenueDeciFen": revenue,
            "arpuDeciFen": revenue // users if users else 0,
            "iaaSharePermille": iaa * 1000 // revenue if revenue else 0,
            "iapSharePermille": iap * 1000 // revenue if revenue else 0}


def draw_svg(path: str, rows: list[dict], arpu: int) -> int:
    """纯 Python SVG:每段一根堆叠横条(IAA 蓝 + IAP 橙),长度正比于段 ARPU。"""
    W, PAD, ROW = 760, 176, 56
    H = 84 + ROW * len(rows)
    r = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                           "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(r, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    ET.SubElement(r, "text", {"x": "16", "y": "26", "font-size": "15", "fill": "#111827"}).text = \
        f"各留存×付费段 ARPU 拆解(0.1 分/用户,混合 {arpu / 10:.1f} 分)"
    for k, (c, lab) in enumerate([("#2563eb", "IAA"), ("#f59e0b", "IAP")]):
        ET.SubElement(r, "rect", {"x": str(16 + k * 70), "y": "38", "width": "12", "height": "12", "rx": "2", "fill": c})
        ET.SubElement(r, "text", {"x": str(33 + k * 70), "y": "49", "font-size": "11", "fill": "#374151"}).text = lab
    span, hi = W - PAD - 190, max((s["arpuDeciFen"] for s in rows), default=1) or 1
    for i, s in enumerate(rows):
        y = 58 + i * ROW
        wi, wp = span * s["iaaArpuDeciFen"] // hi, span * s["iapArpuDeciFen"] // hi
        ET.SubElement(r, "text", {"x": "16", "y": str(y + 24), "font-size": "12",
                                  "fill": "#374151"}).text = s["name"]
        ET.SubElement(r, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(wi), "height": "22",
                                  "rx": "3", "fill": "#2563eb"})
        ET.SubElement(r, "rect", {"x": str(PAD + wi), "y": str(y + 6), "width": str(wp),
                                  "height": "22", "rx": "3", "fill": "#f59e0b"})
        ET.SubElement(r, "text", {"x": str(PAD + wi + wp + 8), "y": str(y + 22), "font-size": "12",
                                  "fill": "#6b7280"}).text = f'{s["arpuDeciFen"] / 10:.1f}分'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(r).write(path, encoding="utf-8", xml_declaration=True)
    return os.path.getsize(path)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    a = ap.parse_args()
    cfg = load(a.config)
    base = evaluate(cfg, 100, 100)
    scenarios = []
    for sc in cfg["scenarios"]:
        e = evaluate(cfg, sc["retentionPct"], sc["payerPct"])
        scenarios.append({"id": sc["id"], "name": sc["name"], "retentionPct": sc["retentionPct"],
                          "payerPct": sc["payerPct"], "arpuDeciFen": e["arpuDeciFen"],
                          "revenueDeciFen": e["revenueDeciFen"]})
    top = max(base["rows"], key=lambda s: s["arpuDeciFen"])
    top_share = top["revenueDeciFen"] * 1000 // base["revenueDeciFen"]
    os.makedirs(a.out, exist_ok=True)
    svg = draw_svg(os.path.join(a.out, "l07_joint.svg"), base["rows"], base["arpuDeciFen"])
    with open(os.path.join(a.out, "l07_joint.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "name", "users", "activeDays", "payerRatePermille", "iaaArpuDeciFen",
                    "iapArpuDeciFen", "arpuDeciFen", "revenueDeciFen", "revenueSharePermille"])
        for s in base["rows"]:
            w.writerow([s["id"], s["name"], s["users"], s["activeDays"], s["payerRatePermille"],
                        s["iaaArpuDeciFen"], s["iapArpuDeciFen"], s["arpuDeciFen"], s["revenueDeciFen"],
                        s["revenueDeciFen"] * 1000 // base["revenueDeciFen"]])
    digest = f'arpu:{base["arpuDeciFen"]}:iaa:{base["iaaSharePermille"]}:iap:{base["iapSharePermille"]}|' + \
        ",".join(f'{s["id"]}:{s["arpuDeciFen"]}' for s in base["rows"])
    rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
           "horizonDays": cfg["horizonDays"], "iaaDeciFenPerActiveDay": cfg["iaaDeciFenPerActiveDay"],
           "segments": base["rows"], "totalUsers": base["users"], "blendedArpuDeciFen": base["arpuDeciFen"],
           "totalRevenueDeciFen": base["revenueDeciFen"], "iaaSharePermille": base["iaaSharePermille"],
           "iapSharePermille": base["iapSharePermille"], "topSegment": {"id": top["id"], "revenueSharePermille": top_share},
           "scenarios": scenarios, "digest": digest, "csv": os.path.join(a.out, "l07_joint.csv"),
           "svg": os.path.join(a.out, "l07_joint.svg"), "svgBytes": svg}
    with open(os.path.join(a.out, "l07_joint.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"blendedArpuDeciFen": base["arpuDeciFen"], "totalRevenueDeciFen": base["revenueDeciFen"],
                      "iaaSharePermille": base["iaaSharePermille"], "iapSharePermille": base["iapSharePermille"],
                      "segArpu": {s["id"]: s["arpuDeciFen"] for s in base["rows"]}, "topSegment": top["id"],
                      "topRevenueSharePermille": top_share, "scenarioArpu": {s["id"]: s["arpuDeciFen"] for s in scenarios},
                      "digest": digest}, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

在本机跑它,输出如下(同机相对量级,绝对数值不可移植):

{"blendedArpuDeciFen": 393, "totalRevenueDeciFen": 11802000, "iaaSharePermille": 329, "iapSharePermille": 670, "segArpu": {"hr-hp": 1576, "hr-lp": 360, "lr-hp": 758, "lr-lp": 104}, "topSegment": "hr-hp", "topRevenueSharePermille": 400, "scenarioArpu": {"baseline": 393, "retention-up": 458, "payer-up": 445}, "digest": "arpu:393:iaa:329:iap:670|hr-hp:1576,hr-lp:360,lr-hp:758,lr-lp:104"}

读段表:留存是乘数

把四段摊开(ARPU 单位 0.1 分):

段用户活跃日付费率段 ARPU收入占比
高留存·高付费30001818.0%157.6 分40.0%
高留存·低付费9000184.0%36.0 分27.4%
低留存·高付费3000622.0%75.8 分19.2%
低留存·低付费1500063.0%10.4 分13.2%

两件事一眼可见。第一,活跃日 是乘法因子:高留存段的 IAA 部分是 216(18×12),低留存段只有 72(6×12)——差 3 倍,纯粹因为留得久。第二,「高付费」救不了「低留存」:低留存·高付费段付费率高达 22%(四段最高),但段 ARPU 只有 75.8 分,仍低于高留存·低付费段的……不,其实它高于它。真正扎眼的是人数与贡献的错位:高留存两段合计 12000 人(占 40%),却贡献了 67.4% 的收入;低留存两段 18000 人(占 60%),只贡献 32.4%。

三档场景把「乘数」这件事量化了:基线混合 ARPU 39.3 分;留存 +20% 抬到 45.8 分(+16.5%);付费率 +20% 只抬到 44.5 分(+13.2%)。同样 +20%,抬留存比抬付费率多撬动约 3 个百分点——因为留存同时放大 IAA 和 IAP 两项,而付费率只放大 IAP 一项。这不是「留存比付费重要」的鸡汤,而是公式里 活跃日 出现在两项、付费率 只出现在一项的算术后果。

W 层:单文件 HTML 探索器

要把这条结论变成能上手拨的直觉,最好的办法是做成一个浏览器里打开就能拖的探索器。下面这段是 W 层的构建脚本,把 core/src/l07/joint.ts(与 Python 同口径的 TypeScript 内核,已由 core/test/l07_joint.test.ts 锁住)打包成单文件 HTML。

#!/usr/bin/env node
// file: web/build_l07_joint.mjs
// tier: W
// verify: node web/build_l07_joint.mjs
// 把 core/src/l07/joint.ts 打成单文件 HTML(留存×付费联合 ARPU 探索器)。复用 pack.mjs 的擦除逻辑。

import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { bundle, page, writePage, inlineConfig } from "./pack.mjs";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const cfg = JSON.parse(readFileSync(join(ROOT, "config", "l07_joint.json"), "utf8"));
const js = bundle(ROOT, ["core/src/l07/joint.ts"]);
const script = `const cfg = ${inlineConfig(cfg)};
${js}
const cv = document.getElementById("stage"), g = cv.getContext("2d");
let retentionPct = 100, payerPct = 100, res = null;
const fmt = (df) => Math.floor(df / 10) + "." + (df % 10) + " 分";
function compute() { res = evaluate(cfg, retentionPct, payerPct); draw(); }
function draw() {
  g.clearRect(0, 0, cv.width, cv.height);
  const peak = Math.max(...res.rows.map((r) => r.arpuDeciFen), 1);
  res.rows.forEach((r, i) => {
    const y = 14 + i * 42, span = cv.width - 210;
    const wi = Math.floor(span * r.iaaArpuDeciFen / peak), wp = Math.floor(span * r.iapArpuDeciFen / peak);
    g.fillStyle = "#2563eb"; g.fillRect(120, y, wi, 24);
    g.fillStyle = "#f59e0b"; g.fillRect(120 + wi, y, wp, 24);
    g.fillStyle = "#e2e8f0"; g.font = "13px system-ui";
    g.fillText(r.name, 6, y + 17);
    g.fillText(fmt(r.arpuDeciFen), 120 + wi + wp + 6, y + 17);
  });
  document.getElementById("arpu").textContent = fmt(res.arpuDeciFen);
  document.getElementById("iaa").textContent = Math.floor(res.iaaSharePermille / 10) + "." + (res.iaaSharePermille % 10) + "%";
  document.getElementById("iap").textContent = Math.floor(res.iapSharePermille / 10) + "." + (res.iapSharePermille % 10) + "%";
  globalThis.__pebble = { arpuDeciFen: res.arpuDeciFen, iaaSharePermille: res.iaaSharePermille,
    iapSharePermille: res.iapSharePermille, retentionPct, payerPct,
    segArpu: res.rows.map((r) => [r.id, r.arpuDeciFen]), canvasDrawn: true, api };
}
const api = {
  set(ret, pay) { retentionPct = ret; payerPct = pay; compute();
    return { arpuDeciFen: res.arpuDeciFen, iaaSharePermille: res.iaaSharePermille, iapSharePermille: res.iapSharePermille }; },
  get() { return { arpuDeciFen: res.arpuDeciFen, iaaSharePermille: res.iaaSharePermille,
    iapSharePermille: res.iapSharePermille, retentionPct, payerPct, segArpu: res.rows.map((r) => [r.id, r.arpuDeciFen]) }; },
};
compute();
`;
const body = `  <h1>《合成小岛》· 留存×付费联合 ARPU 探索器</h1>
  <div class="sub">内核来自 core/src/l07/joint.ts:ARPU = 活跃日 ×(IAA + 付费率 × ARPPU),全整数、无 Math.random</div>
  <div class="card row"><div><div class="k">混合 ARPU</div><div class="big" id="arpu">0 分</div></div><div><div class="k">IAA 占比</div><div class="big" id="iaa">0%</div></div><div><div class="k">IAP 占比</div><div class="big" id="iap">0%</div></div></div>
  <div class="card"><canvas id="stage" width="480" height="184"></canvas></div>
  <div class="sub">留存档(蓝= IAA,橙= IAP):抬高留存同时放大两段;只抬付费率仅放大橙段</div>`;
const bytes = writePage(join(ROOT, "web/dist/l07.html"), page({ title: "《合成小岛》留存×付费联合 ARPU 探索器 · l07", body, script, css: "#stage { width: 480px; height: 184px; }" }));
console.log(JSON.stringify({ html: "web/dist/l07.html", htmlBytes: bytes, bundleLines: js.split("\n").length }));

单文件 HTML 的产出要靠 Playwright 无头验证:注入不同的留存 / 付费档,读回页面状态与 canvas 像素,确认「拨动参数 → ARPU 变化」真的发生。下面这段是 W 层验证脚本。

#!/usr/bin/env node
// file: web/verify_l07_joint.mjs
// tier: W
// verify: NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_l07_joint.mjs web/dist/l07.html
// W 层验收:Playwright 无头打开单文件 HTML,调 api 切留存/付费档,读回断言 + 截图。
// playwright 用 createRequire 加载——NODE_PATH 对 ESM 的 import 无效。

import { createRequire } from "node:module";
import { pathToFileURL, fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
import { mkdirSync } from "node:fs";
import assert from "node:assert/strict";

const { chromium } = createRequire(import.meta.url)("playwright");
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const url = pathToFileURL(resolve(ROOT, process.argv[2] ?? "web/dist/l07.html")).href;
mkdirSync(join(ROOT, "out/shots"), { recursive: true });
const browser = await chromium.launch();
const warm = await browser.newPage(); await warm.goto(url); await warm.close(); // 预热:冷启动首个 page 读 canvas 像素恒为空白
const errors = [];
const p = await browser.newPage({ viewport: { width: 800, height: 720 } });
p.on("pageerror", (e) => errors.push(String(e)));
await p.goto(url);
await p.waitForFunction(() => globalThis.__pebble?.canvasDrawn === true, null, { timeout: 10000 });
const base = await p.evaluate(() => globalThis.__pebble.api.get());
const domArpu = (await p.textContent("#arpu")).trim();
const domIaa = (await p.textContent("#iaa")).trim();
const retUp = await p.evaluate(() => globalThis.__pebble.api.set(120, 100));
const payerUp = await p.evaluate(() => globalThis.__pebble.api.set(100, 120));
const painted = await p.evaluate(() => {
  const d = document.getElementById("stage").getContext("2d").getImageData(0, 0, 480, 184).data;
  let n = 0; for (let i = 0; i < d.length; i += 4) if (d[i + 3] > 0 && d[i] < 250) n += 1; return n;
});
await p.screenshot({ path: join(ROOT, "out/shots/l07_joint.png"), fullPage: true });
await p.close();
await browser.close();

assert.equal(errors.length, 0, `页面错误: ${errors.join("; ")}`);
assert.equal(base.arpuDeciFen, 393, "基线混合 ARPU 应为 393");
assert.equal(base.iaaSharePermille, 329, "基线 IAA 占比应为 329‰");
assert.equal(base.iapSharePermille, 670, "基线 IAP 占比应为 670‰");
assert.deepEqual(base.segArpu, [["hr-hp", 1576], ["hr-lp", 360], ["lr-hp", 758], ["lr-lp", 104]]);
assert.equal(retUp.arpuDeciFen, 458, "留存 +20% 后 ARPU 应为 458");
assert.equal(payerUp.arpuDeciFen, 445, "付费率 +20% 后 ARPU 应为 445");
assert.equal(domArpu, "39.3 分", "DOM 混合 ARPU 应显示 39.3 分");
assert.equal(domIaa, "32.9%", "DOM IAA 占比应显示 32.9%");
assert.ok(painted > 300, `canvas 应有绘制内容,实测 ${painted} px`);

console.log(JSON.stringify({
  page: "web/dist/l07.html", base, retUp, payerUp, domArpu, domIaa,
  canvasPaintedPx: painted, screenshot: "out/shots/l07_joint.png",
}));
console.log("SMOKE-W7-3: PASS");

本机跑它,输出如下(同机相对量级,绝对数值不可移植):

{ "page": "web/dist/l07.html", "base": { "arpuDeciFen": 393, "iaaSharePermille": 329, "iapSharePermille": 670 },
  "retUp": { "arpuDeciFen": 458, "iaaSharePermille": 329, "iapSharePermille": 670 },
  "payerUp": { "arpuDeciFen": 445, "iaaSharePermille": 290, "iapSharePermille": 709 },
  "domArpu": "39.3 分", "domIaa": "32.9%", "canvasPaintedPx": 14969 }
SMOKE-W7-3: PASS

浏览器里拨一下两个参数,就能亲手看到那条结论:把留存抬 20%,IAA 和 IAP 占比几乎不变(329‰ / 670‰)——因为两项同比例放大;把付费率抬 20%,占比立刻偏向 IAP(290‰ / 709‰)。留存是乘数,付费率是加法项,这就是全部差别。

小结与下一步

这一节把留存与付费乘进了同一个公式:ARPU = 活跃日 ×(IAA + 付费率 × ARPPU)。Python 仿真、TS 内核、单文件 HTML 探索器三处同口径,得到三个可复现的结论——活跃日是乘法因子(高留存段 IAA 是低留存段的 3 倍)、40% 的高留存用户贡献 67.4% 的收入、同样 +20%,抬留存(+16.5%)比抬付费率(+13.2%)更能撬动 ARPU。

到这里,第 7 章把「留存」这条线走完了:新手漏斗与留存曲线(7.1)、流失预警与召回(7.2)、留存与付费的联合分析(7.3)。但这些结论都建立在假设参数之上——留存锚点是估的、付费率是猜的、召回率是拍的。要确认一个改动到底有没有效,不能靠模型,要靠实验。下一章讲 A/B 实验与数据驱动:怎么设计分流、要多少样本才显著、以及从实验结论到上线决策的那一步。

阅读导航:上一节:流失预警与召回 · 下一节:实验设计与分流 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制