时间线回答了「什么时候做完」,决策复盘回答了「做得对不对」,最后一节回答**「做完之后,数据到底动了没有」。边界先说清:新手漏斗怎么拆是留存漏斗 那一章、单次改动怎么验是 A/B 实验 那一章、版本怎么排是版本列车 那一章、活动怎么做是活动模板 那一章。本节不重讲这些方法**,只把它们的结果摆到同一张时间轴上,看每一次迭代各自拉动了哪条曲线、每投入一天的产出是多少。
还有一条纪律必须写在最前面:本节的所有数字都是示例假设,不是《合成小岛》的真实后台数据。真实的上线表现需要真实业务环境,命令行不可复现(X)。这里给的是一个可替换参数的模型——读者把配置里的基线、衰减率、每次迭代的增量换成自己游戏的经验值,脚本立刻能跑出属于你的轨迹。
上线后的数据不是一条直线
很多人对上线后的数据有一个错误直觉:上线时是多少,之后大概就维持在多少。真实情况是数据从第一天起就在自然衰减——内容被消耗、新鲜感消退、竞品抢走注意力,留存和 ARPDAU 都会往下走。迭代的作用首先不是「增长」,而是「对抗衰减」:把一个本来会掉到谷底的曲线,靠一次次版本更新拉回来。理解了这一点,评估迭代的标准就变了——不是「这次更新带来了多少新增」,而是「这次更新比不更新多留住了多少」。
模型只做两件事:每周先按固定速率衰减,某次迭代在其所在周注入一次持久增量。三条指标各自维护一条以「基线 = 1000」为起点的指数曲线:次日留存 d1 / 7 日留存 d7 / ARPDAU。这样任何一次改动的影响都能被拆成「衰减吃掉多少」和「迭代补回多少」两部分。
D 层:把基线、衰减与迭代写进配置
下面这份是 D 层(静态层)配置,给出三条指标的基线、每周衰减率,以及五次迭代各自的时间、目标指标、增量与投入人天。所有数值为示例假设,真实数值需真实业务环境,命令行不可复现(X)。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "上线后数据表现与迭代模型(D 层)。所有指标以「基线 = 1000」的指数表示,每周自然衰减 decayPermillePerWeek,迭代在某周注入一次持久增量 liftPermille。基线指标与所有增量均为示例假设值,真实数值需真实业务环境,命令行不可复现(X)。全整数(千分比),禁浮点。",
"units": {
"index": "基线 = 1000 的相对指数",
"d1Permille": "次日留存(千分比,示例假设)",
"d7Permille": "7 日留存(千分比,示例假设)",
"arpdauTenths": "ARPDAU(0.1 分,示例假设)"
},
"weeks": 12,
"baseline": { "d1Permille": 300, "d7Permille": 90, "arpdauTenths": 90 },
"decayPermillePerWeek": { "d1": 20, "d7": 12, "arpdau": 25 },
"iterations": [
{ "week": 2, "version": "v1.1", "name": "新手引导缩短", "metric": "d1", "liftPermille": 180, "effortDays": 5 },
{ "week": 4, "version": "v1.2", "name": "广告频次回调", "metric": "arpdau", "liftPermille": 120, "effortDays": 3 },
{ "week": 6, "version": "v1.3", "name": "关卡难度重标定", "metric": "d7", "liftPermille": 200, "effortDays": 6 },
{ "week": 8, "version": "v1.4", "name": "首充礼包上线", "metric": "arpdau", "liftPermille": 200, "effortDays": 4 },
{ "week": 10, "version": "v1.5", "name": "节点活动", "metric": "d1", "liftPermille": 140, "effortDays": 5 }
]
}
基线是 d1 = 300‰(三成次日留存)、d7 = 90‰、ARPDAU = 9.0 分。每周衰减率是指数点:d1 每周掉 20 点、d7 掉 12 点、ARPDAU 掉 25 点。五次迭代分别打在新手引导、广告频次、难度、首充礼包与节点活动上。全整数,禁浮点。
S 层:逐周轨迹与迭代 ROI
下面这段是 S 层脚本,读上面这份配置:对每条指标逐周推进(先衰减、再叠加当周迭代增量),得到三条指数轨迹;同时算一条「纯衰减、不迭代」的对照,两者的差就是迭代对抗衰减的净效果;最后按「每投入一天带来的指数提升」给迭代排 ROI。全整数地板除。
#!/usr/bin/env python3
# file: sim/l14_performance.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l14_performance.py --config config/ --out out/
"""上线后数据表现与迭代仿真:读 config/l14_performance.json。
三条指标(次日留存 d1 / 7 日留存 d7 / ARPDAU)各以「基线 = 1000」的指数表示:
- 每周自然衰减 decayPermillePerWeek(指数点);
- 某次迭代在其所在周注入一次持久增量 liftPermille(指数点)。
逐周推进得到三条指数轨迹,再折算回指标原值。
同时算两条对照:① 纯衰减(不迭代)的期末指数,② 每次迭代的边际提升与「每投入一天的提升」
(liftPermille / effortDays)。全整数地板除。
产出 JSON + CSV + SVG。所有数字为示例假设值,真实数值需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
METRICS = ["d1", "d7", "arpdau"]
COLORS = {"d1": "#2563eb", "d7": "#10b981", "arpdau": "#f59e0b"}
def load(d: str) -> dict:
with open(os.path.join(d, "l14_performance.json"), encoding="utf-8") as f:
return json.load(f)
def trajectory(cfg: dict, with_lifts: bool) -> dict:
"""逐周推进指数;with_lifts=False 时只衰减(对照)。返回 metric -> [index_0..index_W]。"""
weeks = cfg["weeks"]
decay = cfg["decayPermillePerWeek"]
lifts: dict[str, dict[int, int]] = {m: {} for m in METRICS}
if with_lifts:
for it in cfg["iterations"]:
w = it["week"]
lifts[it["metric"]][w] = lifts[it["metric"]].get(w, 0) + it["liftPermille"]
traj = {}
for m in METRICS:
series = [1000]
for w in range(1, weeks + 1):
series.append(series[-1] - decay[m] + lifts[m].get(w, 0))
traj[m] = series
return traj
def to_value(metric: str, index: int, baseline: dict) -> int:
if metric == "d1":
return baseline["d1Permille"] * index // 1000
if metric == "d7":
return baseline["d7Permille"] * index // 1000
return baseline["arpdauTenths"] * index // 1000
def draw_svg(path: str, traj: dict, weeks: int) -> int:
W, H, PAD = 760, 340, 56
lo = min(min(s) for s in traj.values())
hi = max(max(s) for s in traj.values())
span = hi - lo + 1
plot_w, plot_h = W - PAD - 30, H - 90
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(svg, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(svg, "text", {"x": "16", "y": "26", "font-size": "15", "fill": "#111827"}).text = \
"上线后 12 周指标指数(基线=1000,示例假设)"
# 基线参考线
y1000 = H - 40 - (1000 - lo) * plot_h // span
ET.SubElement(svg, "line", {"x1": str(PAD), "y1": str(y1000), "x2": str(W - 30), "y2": str(y1000),
"stroke": "#d1d5db", "stroke-dasharray": "4 4"})
ET.SubElement(svg, "text", {"x": str(W - 96), "y": str(y1000 - 4), "font-size": "11",
"fill": "#9ca3af"}).text = "基线 1000"
for m in METRICS:
pts = []
for w, v in enumerate(traj[m]):
x = PAD + (W - PAD - 30 - PAD) * w // weeks
y = H - 40 - (v - lo) * plot_h // span
pts.append(f"{x},{y}")
ET.SubElement(svg, "polyline", {"points": " ".join(pts), "fill": "none",
"stroke": COLORS[m], "stroke-width": "2"})
ET.SubElement(svg, "text", {"x": str(W - 26), "y": str(H - 40 - (traj[m][-1] - lo) * plot_h // span + 4),
"font-size": "11", "fill": COLORS[m]}).text = m
for w in range(0, weeks + 1, 2):
x = PAD + (W - PAD - 30 - PAD) * w // weeks
ET.SubElement(svg, "text", {"x": str(x - 4), "y": str(H - 24), "font-size": "10",
"fill": "#6b7280"}).text = f"w{w}"
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(svg).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)
weeks, baseline = cfg["weeks"], cfg["baseline"]
with_lifts = trajectory(cfg, True)
decay_only = trajectory(cfg, False)
final = {m: with_lifts[m][-1] for m in METRICS}
final_decay = {m: decay_only[m][-1] for m in METRICS}
iter_roi = []
for it in cfg["iterations"]:
roi = it["liftPermille"] * 1000 // it["effortDays"]
iter_roi.append({"version": it["version"], "name": it["name"], "metric": it["metric"],
"week": it["week"], "liftPermille": it["liftPermille"],
"effortDays": it["effortDays"], "liftPerDayPermille": roi})
iter_roi.sort(key=lambda r: -r["liftPerDayPermille"])
derived = {m: {"baseline": to_value(m, 1000, baseline), "finalIndex": final[m],
"final": to_value(m, final[m], baseline),
"decayOnlyIndex": final_decay[m], "decayOnly": to_value(m, final_decay[m], baseline),
"liftOverDecayIndex": final[m] - final_decay[m]} for m in METRICS}
os.makedirs(a.out, exist_ok=True)
sb = draw_svg(os.path.join(a.out, "l14_performance.svg"), with_lifts, weeks)
with open(os.path.join(a.out, "l14_performance.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["week"] + [f"{m}Index" for m in METRICS] + [f"{m}IndexDecayOnly" for m in METRICS])
for wk in range(weeks + 1):
w.writerow([wk] + [with_lifts[m][wk] for m in METRICS] + [decay_only[m][wk] for m in METRICS])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"weeks": weeks, "iterationCount": len(cfg["iterations"]),
"trajectory": {m: with_lifts[m] for m in METRICS},
"derived": derived, "iterationRoi": iter_roi, "svgBytes": sb}
with open(os.path.join(a.out, "l14_performance.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"finalIndex": final, "decayOnlyIndex": final_decay,
"derived": derived,
"topRoi": [[r["version"], r["liftPerDayPermille"]] for r in iter_roi[:3]]},
ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑它,输出如下(同机相对量级,绝对数值不可移植):
{"finalIndex": {"d1": 1080, "d7": 1056, "arpdau": 1020}, "decayOnlyIndex": {"d1": 760, "d7": 856, "arpdau": 700}, "derived": {"d1": {"baseline": 300, "finalIndex": 1080, "final": 324, "decayOnlyIndex": 760, "decayOnly": 228, "liftOverDecayIndex": 320}, "d7": {"baseline": 90, "finalIndex": 1056, "final": 95, "decayOnlyIndex": 856, "decayOnly": 77, "liftOverDecayIndex": 200}, "arpdau": {"baseline": 90, "finalIndex": 1020, "final": 91, "decayOnlyIndex": 700, "decayOnly": 63, "liftOverDecayIndex": 320}}, "topRoi": [["v1.4", 50000], ["v1.2", 40000], ["v1.1", 36000]]}
十二周后,次日留存指数 1080(折算 324‰,比基线高 24)、7 日留存指数 1056(95‰)、ARPDAU 指数 1020(9.1 分)。如果一次迭代都不做,三条线会分别掉到 760 / 856 / 700——折算成原值就是 d1 只剩 228‰、d7 只剩 77‰、ARPDAU 只剩 6.3 分。也就是说,迭代对抗衰减的净效果是 d1 +320 个指数点、ARPDAU +320 个指数点、d7 +200 个指数点:不迭代的话,留存和收入都会塌掉一大截。
P 层:迭代优先级内核
上面是「已经做完的五次迭代各自值多少」,还差一个前瞻问题:如果产能只够再做一件事,该先做哪件。下面这段是 P 层(内核层)TypeScript,把迭代按「每投入一天的指数提升」排序,轨迹推进逻辑与上面的 Python 逐位一致。跑 node --test 'core/test/l14_*.test.ts' 十个断言全过。
// file: core/src/l14/iteration.ts
// tier: P
// verify: node --test 'core/test/l14_*.test.ts'
//
// 迭代优先级内核:给定一批迭代(各带对某指标指数的 liftPermille 与 effortDays),
// 按「每投入一天带来的指数提升」从高到低排序;轨迹推进与 S 层 Python 的 iterationRoi 逐位一致。
// 全整数地板除,无 Math.random(),只用可擦除语法(禁 enum / namespace / 参数属性 / 装饰器)。
export interface Iteration {
readonly version: string;
readonly metric: string;
readonly liftPermille: number;
readonly effortDays: number;
}
export interface RankedIteration {
readonly version: string;
readonly metric: string;
readonly liftPermille: number;
readonly effortDays: number;
readonly liftPerDayPermille: number;
}
/** 每投入一天带来的指数提升(整数地板除,与 S 层一致)。 */
export function liftPerDay(liftPermille: number, effortDays: number): number {
return effortDays > 0 ? Math.floor(liftPermille * 1000 / effortDays) : 0;
}
/** 按 liftPerDayPermille 从高到低排序;并列时按 version 升序(确定性)。 */
export function prioritize(iterations: readonly Iteration[]): RankedIteration[] {
return iterations
.map((it) => ({
version: it.version, metric: it.metric, liftPermille: it.liftPermille, effortDays: it.effortDays,
liftPerDayPermille: liftPerDay(it.liftPermille, it.effortDays),
}))
.sort((a, b) => b.liftPerDayPermille - a.liftPerDayPermille || (a.version < b.version ? -1 : 1));
}
/** 逐周推进某指标的指数:每周先减 decayPerWeek,再叠加当周的 lift。 */
export function finalIndex(
decayPerWeek: number,
lifts: readonly { readonly week: number; readonly liftPermille: number }[],
weeks: number,
): number {
let v = 1000;
for (let w = 1; w <= weeks; w++) {
v -= decayPerWeek;
for (const l of lifts) if (l.week === w) v += l.liftPermille;
}
return v;
}
按 ROI 排序,五次迭代是 v1.4 首充礼包(50000)> v1.2 广告频次回调(40000)> v1.1 新手引导缩短(36000)> v1.3 难度重标定(33333)> v1.5 节点活动(28000)。
三个真正该记住的结论
第一,高 ROI 的迭代往往不是「大内容」,而是「小改动」。排在最前的 v1.4 与 v1.2 都只花了 3–4 人天,打的是首充礼包与广告频次这类变现参数,单位投入的产出却最高;而 v1.3 难度重标定花了 6 天,ROI 反而靠后。这解释了为什么成熟团队总把「数值与变现调参」排在内容更新前面——它们便宜、见效快、且直接作用在收入曲线上。
第二,迭代的账要算「净效果」,不是「绝对提升」。五次迭代一共注入了 840 个指数点(180+120+200+200+140),但 d1 的期末指数只比基线高 80——因为衰减同期吃掉了 240 点(每周 20 点 × 12 周)。只看「这次更新带来 +180」,会严重高估迭代的功劳;减去衰减后的净值,才是真正留下的东西。
第三,衰减是常数敌人,停更等于倒退。对照很清楚:同样十二周,迭代版本 d1 稳在 324‰,纯衰减版本掉到 228‰。两者差的不是「增长」,而是「有没有崩」。所以「上线后要不要持续迭代」根本不该是一个问题——不迭代,模型里的曲线就是一条下滑线;迭代的价值首先是把这条下滑线拉平。
小结与下一步
这一节把上线后的数据做成了一条「每周衰减 + 迭代注入」的可跑曲线:d1/d7/ARPDAU 三条指数轨迹、每次迭代的 ROI、以及一个优先级内核。三件事记住就够——迭代首先是对抗衰减,看净效果而不是绝对提升;高 ROI 的往往是小改动而非大内容;停更等于倒退。至此,第 14 章的完整案例走完了:时间线(14.1)、决策复盘(14.2)、数据与迭代(14.3)。
案例讲的是「做对了会怎样」,但真实的教训往往来自反面。下一章换个角度,看《合成小岛》这类小游戏最容易掉进去的几个坑——玩法数值与变现、买量与留存、运营与合规的典型失败。
阅读导航:上一节:14.2 关键决策复盘 · 下一节:15.1 玩法、数值与变现的失败 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。