15.3 多平台数据表现与迭代

五平台陆续上线后,案例进入最后一段:看数据、决定往哪加注。本节用可跑的仿真把五平台表现摊平,发现一个陷阱——边际效率最高的 YouTube Playables 样本量最小、指标最不可信。迭代决策必须先用样本门槛筛掉不可信的平台,再谈加注,并给出可播种回放的日收入抖动。

站内有一篇 H5 游戏产业全景 ,那是 H5 产业的行业综述;卷三《上线变现与长线运营》 讲的是国内上线与长线运营的完整方法。本章不重做行业综述、也不重做国内运营方法论,只做一件事:把前 14 章的方法装配成《合成小岛》的出海案例。案例的前两段做完了:五平台按时间线 接好、按落地门禁 送审。这一节是最后一段:上线之后,看数据、决定往哪加注。

上线之后:看什么、怎么决定往哪加注

五平台跑起来后,后台会给出每个平台的 DAU、ARPDAU、留存、收入。但**「优先投谁」不是看一个数就能回答的**——第 4 章已经用多平台收入配比 讲清了「总量口径」与「边际口径」的分歧:总量看现收入(大流量平台占优),边际看增长效率(高 ARPDAU 平台占优)。本节不重讲那两个口径,只补上案例里必须多问的一句:边际效率最高的平台,样本够不够?

这正是第 8 章分语言口径 的同一条纪律——小样本的指标方差极大,样本最小的组往往指标最漂亮。把它搬到平台维度:一个平台如果新增样本太少,它的 D7 留存与净 ARPDAU 可能是噪音,不能据此决定加注。

D 层:数据参数配置

下面这段是 D 层配置 config/g15_data_iter.json。它把五平台的 DAU、毛 ARPDAU、分成、观测窗口新增与 D7 活跃写成唯一真源。口径是公开可观察机制的建模假设,不是任何平台后台实测。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "多平台数据表现与迭代参数。口径是「公开可观察机制」的建模假设,不是任何平台后台实测。dau=该平台当前日活(建模假设);arpdauDeciFen=该平台毛 ARPDAU(0.1 分/DAU);publisherSharePermille=发行方分成千分比;newUsers=观测窗口内新增用户数;d7Active=该窗口新增用户在第 7 日仍活跃数(用于算 D7 与样本方差);minSampleNewUsers=判定指标可信所需最小新增样本。seed + volatilityPermille 驱动日净收入的确定性抖动。真实 DAU、ARPDAU、留存与平台后台数据需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
  "seed": 20261013,
  "daysPerMonth": 30,
  "minSampleNewUsers": 2000,
  "volatilityPermille": 120,
  "platforms": [
    { "id": "gamedistribution", "name": "GameDistribution", "dau": 48000, "arpdauDeciFen": 300, "publisherSharePermille": 700, "newUsers": 9000, "d7Active": 2340 },
    { "id": "poki", "name": "Poki", "dau": 84000, "arpdauDeciFen": 460, "publisherSharePermille": 500, "newUsers": 42000, "d7Active": 11760 },
    { "id": "crazygames", "name": "CrazyGames", "dau": 60000, "arpdauDeciFen": 520, "publisherSharePermille": 500, "newUsers": 30000, "d7Active": 8400 },
    { "id": "fb_instant", "name": "Facebook Instant Games", "dau": 52000, "arpdauDeciFen": 540, "publisherSharePermille": 700, "newUsers": 26000, "d7Active": 7280 },
    { "id": "yt_playables", "name": "YouTube Playables", "dau": 20000, "arpdauDeciFen": 700, "publisherSharePermille": 550, "newUsers": 1800, "d7Active": 594 }
  ]
}

S 层:数据表现与迭代仿真

下面这段是 S 层仿真脚本 sim/g15_data_iter.py。它读上面那份配置,算每个平台的净 ARPDAU、月净收入、收入占比、D7 留存与其 95% 置信半宽,标出样本不足的平台,并用可播种的 64 位 LCG抖动出 30 天日净收入序列,产出 JSON + CSV + SVG。

#!/usr/bin/env python3
# file: sim/g15_data_iter.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g15_data_iter.py --config config/ --out out/
"""多平台数据表现与迭代仿真(S 层:数值仿真 + 纯 Python SVG)。

读 config/g15_data_iter.json,把五平台发到海外后的表现摊平到一张表,并给出迭代决策依据:

  netArpdauDeciFen(i) = arpdauDeciFen * publisherSharePermille // 1000
  monthlyNetFen(i)    = dau * netArpdauDeciFen * daysPerMonth // 10
  revenueSharePermille(i) = monthlyNetFen(i) * 1000 // Σ monthlyNetFen
  d7Permille(i)       = d7Active * 1000 // newUsers
  ci95HalfPermille(i) = 196 * isqrt(1000000 * a*(n-a) // n**3) // 100   # a=d7Active, n=newUsers
  sampleOk(i)         = newUsers >= minSampleNewUsers

日净收入用可播种的 64 位 LCG(带输出置换)抖动,确定性可回放:
  baseDaily = monthlyNetFen // daysPerMonth
  jitter    = lcg(seed, i, d) % (2*vol+1) - vol      # ∈ [-vol, +vol]
  dailyNet  = baseDaily * (1000 + jitter) // 1000

两个口径分开:总量排名按 monthlyNetFen(现收入),边际排名按 netArpdauDeciFen(增长效率);
再给「可信样本下边际最优」,因为小样本平台的边际排名不可据此决策。

口径:全部参数是「公开可观察机制」口径的建模假设,观察时点 2026-10;真实 DAU / ARPDAU /
留存与平台后台数据需真实业务环境,命令行不可复现(X)。产出 JSON + CSV + SVG,全整数,无浮点。
"""
from __future__ import annotations

import argparse
import csv
import json
import math
import os
import xml.etree.ElementTree as ET

MASK = (1 << 64) - 1


def mix(state: int) -> int:
    """LCG 步进 + 输出置换(xorshift+乘),避免低位退化。"""
    state = (state * 6364136223846793005 + 1442695040888963407) & MASK
    state ^= state >> 33
    state = (state * 0xFF51AFD7ED558CCD) & MASK
    state ^= state >> 33
    return state


def jitter(seed: int, i: int, d: int, vol: int) -> int:
    s = (seed * 6364136223846793005 + (i + 1) * 1442695040888963407 + (d + 1) * 2654435761) & MASK
    s = mix(s)
    return s % (2 * vol + 1) - vol


def evaluate(cfg: dict) -> dict:
    n = cfg["daysPerMonth"]
    vol = cfg["volatilityPermille"]
    rows, total = [], 0
    for i, p in enumerate(cfg["platforms"]):
        net = p["arpdauDeciFen"] * p["publisherSharePermille"] // 1000
        monthly = p["dau"] * net * n // 10
        total += monthly
        a, nu = p["d7Active"], p["newUsers"]
        d7 = a * 1000 // nu
        var_mille = 1000000 * a * (nu - a) // (nu ** 3) if nu else 0
        ci = 196 * math.isqrt(var_mille) // 100
        base_daily = monthly // n
        walk = sum(base_daily * (1000 + jitter(cfg["seed"], i, d, vol)) // 1000 for d in range(n))
        rows.append({"id": p["id"], "name": p["name"], "dau": p["dau"],
                     "grossArpdauDeciFen": p["arpdauDeciFen"], "publisherSharePermille": p["publisherSharePermille"],
                     "netArpdauDeciFen": net, "monthlyNetFen": monthly, "d7Permille": d7,
                     "ci95HalfPermille": ci, "newUsers": nu, "sampleOk": nu >= cfg["minSampleNewUsers"],
                     "walkCumFen": walk})
    for r in rows:
        r["revenueSharePermille"] = r["monthlyNetFen"] * 1000 // total if total else 0
    by_total = sorted(rows, key=lambda r: (-r["monthlyNetFen"], r["id"]))
    by_marginal = sorted(rows, key=lambda r: (-r["netArpdauDeciFen"], r["id"]))
    trusted = [r for r in by_marginal if r["sampleOk"]]
    return {"rows": rows, "totalMonthlyNetFen": total,
            "topTotal": by_total[0]["id"], "topMarginal": by_marginal[0]["id"],
            "trustedTop": trusted[0]["id"] if trusted else None,
            "untrusted": [r["id"] for r in rows if not r["sampleOk"]],
            "walkTotalFen": sum(r["walkCumFen"] for r in rows)}


def draw_svg(path: str, res: dict) -> int:
    """纯 Python SVG:每平台一条月净收入横条;样本不足的平台标灰。"""
    rows = res["rows"]
    W, H, PAD, ROW = 800, 74 + 44 * len(rows), 210, 44
    root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                              "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    ET.SubElement(root, "text", {"x": "16", "y": "28", "font-size": "15", "fill": "#111827"}).text = (
        f'《合成小岛》五平台月净收入(分):灰=样本不足')
    ET.SubElement(root, "text", {"x": "16", "y": "48", "font-size": "12", "fill": "#6b7280"}).text = (
        f'总量第一 {res["topTotal"]} · 边际第一 {res["topMarginal"]} · 可信边际第一 {res["trustedTop"]}')
    span, hi = W - PAD - 200, max([r["monthlyNetFen"] for r in rows] + [1])
    for i, r in enumerate(rows):
        y = 66 + i * ROW
        ET.SubElement(root, "text", {"x": "16", "y": str(y + 20), "font-size": "12",
                                     "fill": "#374151"}).text = r["name"]
        w = span * r["monthlyNetFen"] // hi
        ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w), "height": "18",
                                     "rx": "3", "fill": "#16a34a" if r["sampleOk"] else "#9ca3af"})
        ET.SubElement(root, "text", {"x": str(PAD + w + 6), "y": str(y + 20), "font-size": "11",
                                     "fill": "#6b7280"}).text = (
            f'{r["monthlyNetFen"]} · 净ARPDAU {r["netArpdauDeciFen"]} · D7 {r["d7Permille"]}‰±{r["ci95HalfPermille"]}')
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(root))


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    a = ap.parse_args()
    with open(os.path.join(a.config, "g15_data_iter.json"), encoding="utf-8") as f:
        cfg = json.load(f)
    res = evaluate(cfg)
    digest = (f'platforms={len(res["rows"])}|total={res["totalMonthlyNetFen"]}'
              f'|topTotal={res["topTotal"]}|topMarginal={res["topMarginal"]}'
              f'|trustedTop={res["trustedTop"]}|untrusted={len(res["untrusted"])}'
              f'|walkCum={res["walkTotalFen"]}')
    os.makedirs(a.out, exist_ok=True)
    svg_bytes = draw_svg(os.path.join(a.out, "g15_data_iter.svg"), res)
    with open(os.path.join(a.out, "g15_data_iter.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        cols = ("id", "name", "dau", "grossArpdauDeciFen", "publisherSharePermille",
                "netArpdauDeciFen", "monthlyNetFen", "revenueSharePermille", "d7Permille",
                "ci95HalfPermille", "newUsers", "sampleOk", "walkCumFen")
        w.writerow(cols)
        for r in res["rows"]:
            w.writerow([r[k] for k in cols])
    rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
           "seed": cfg["seed"], "daysPerMonth": cfg["daysPerMonth"],
           "minSampleNewUsers": cfg["minSampleNewUsers"], "volatilityPermille": cfg["volatilityPermille"],
           "totalMonthlyNetFen": res["totalMonthlyNetFen"], "topTotal": res["topTotal"],
           "topMarginal": res["topMarginal"], "trustedTop": res["trustedTop"],
           "untrusted": res["untrusted"], "walkTotalFen": res["walkTotalFen"],
           "platforms": res["rows"], "digest": digest,
           "svg": os.path.join(a.out, "g15_data_iter.svg"), "svgBytes": svg_bytes}
    with open(os.path.join(a.out, "g15_data_iter.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"digest": digest, "totalMonthlyNetFen": res["totalMonthlyNetFen"],
                      "topTotal": res["topTotal"], "topMarginal": res["topMarginal"],
                      "trustedTop": res["trustedTop"], "untrusted": res["untrusted"],
                      "walkTotalFen": res["walkTotalFen"],
                      "platforms": [{k: r[k] for k in ("id", "netArpdauDeciFen", "monthlyNetFen",
                                                       "revenueSharePermille", "d7Permille",
                                                       "ci95HalfPermille", "sampleOk", "walkCumFen")}
                                    for r in res["rows"]]},
                     ensure_ascii=False, indent=2))
    return 0


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

在本机跑 /opt/homebrew/bin/python3 sim/g15_data_iter.py --config config/ --out out/,五平台表现是(同机相对量级,绝对数值不可移植):

平台净 ARPDAU月净收入(分)收入占比‰D7‰D7 95% 半宽样本可信
GameDistribution21030240000139260±7✅
Poki23057960000267280±3✅
CrazyGames26046800000215280±3✅
Facebook Instant Games37858968000271280±3✅
YouTube Playables38523100000106330±21❌

结果对照:三个排名指向三个平台

把表读成一个决策,要同时握住三把尺子:

  • 总量第一 = Facebook Instant Games:月净收入 58968000 分(约 59 万元),收入占比 271‰。它靠 700‰ 的高分成与 378 的净 ARPDAU 拿下头名。
  • 边际第一 = YouTube Playables:净 ARPDAU 385 最高,但月净收入垫底(23100000,占比仅 106‰)——因为它的 DAU 只有 20000,流量还没做大。
  • 可信边际第一 = Facebook Instant Games:把样本不足的平台筛掉后,边际第一换成 FB(378)。

Playables 是本节的核心陷阱:它的 D7 留存 330‰ 排第一、净 ARPDAU 385 也排第一,看起来是最该加注的平台;但它只有 1800 个新增样本(低于门槛 2000),D7 的 95% 半宽高达 ±21‰,而可信平台只有 ±3‰。指标最漂亮的平台,恰恰是最不可信的那个——它排在边际第一,是「噪音大」不是「质量高」。

所以迭代决策的正确顺序是:先用样本门槛筛掉不可信的平台,再在可信集合里按边际效率排序。结果是先加注 Facebook Instant Games:它既是总量第一,又是可信边际第一,还贴着上一节的时间线关键路径(slack 1)。Playables 的正确定位不是「忽略」,而是先攒样本——它是边际效率最高的平台,值得继续投小量测试把样本做够,再决定是否重仓。

可播种回放:日收入抖动确定性一致

日净收入那一列用了一个可播种的 64 位 LCG(带输出置换),而不是 Math.random——同样的 seed = 20261013,跑多少次都得到同一串数字。本机连跑两次,输出逐字节相同;walkCumFen 合计 216755762 分(约 216.8 万元),与静态月净收入合计 217068000 分相差约 0.14%,正是抖动幅度 volatilityPermille = 120(±12%)在 30 天里的自然涨落。

摘要行把决策的三把尺子钉了下来:

digest = platforms=5|total=217068000|topTotal=fb_instant|topMarginal=yt_playables|trustedTop=fb_instant|untrusted=1|walkCum=216755762

可播种的意义:出海数据迭代经常要「回放上周的决策」——如果收入抖动是随机数,同一个 seed 复现不了,A/B 对比就没有基准。把 seed 写进 config,任何人都能复算这条 30 天序列。

X 层:真实平台后台数据不可复现

边界必须划清。上面算的是数据模型:给定各平台参数,表现表与排名是什么。下面这些属于 X 层,需真实业务环境,命令行不可复现:

  • 各平台真实的 DAU、ARPDAU 与留存(受平台用户结构、推荐算法、季节共同影响,只能上线后从后台读);
  • 各平台真实的分成比例与结算口径(不同合作档位、不同地区差异大,且平台侧可调);
  • 真实的样本量(新平台刚上线时新增极少,D7 指标方差极大——这正是本节 sampleOk 门槛要拦的东西)。

工程上正确的做法是:把可复现的模型层与不可复现的后台数据分开——模型层固定「怎么算净 ARPDAU、怎么设样本门槛、怎么播种回放」这三条纪律;后台数据在接入后反标定(把平台后台实测的 DAU、ARPDAU、留存填回 config,排名立刻反映现实)。本节所有平台数字都是建模假设,不是任何平台后台的实测。

自检表

检查项合格标准
口径分离总量、边际、可信边际三个排名分开输出,不混成一个
样本门槛低于 minSampleNewUsers 的平台标为不可信,不进可信排名
置信可算给出 D7 的 95% 半宽,量化「样本越少越不可信」
可播种回放日收入抖动用 config 里的 seed,同 seed 输出逐字节一致
全整数净 ARPDAU 与就绪度用整数地板除,无浮点
边界清晰真实 DAU / ARPDAU / 留存标 X,不写成「实测」

多平台数据迭代最容易被带偏的一点,是追着最漂亮的数字加注。本节把这条教训钉成一个可跑的门槛:先看样本够不够,再看效率高不高。案例到这里装配完了——组合、时间线、落地门禁、数据迭代,四段串成一条真实的出海路径。下一章进入全书收官:把这条路径装配成一份可交付的出海发行方案,并做多平台仿真验收。

阅读导航:上一节:15.2 本地化与合规落地 · 下一节:16.1 出海发行方案装配 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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