本节目标:把「档位怎么定、谁当锚点」从拍脑袋变成一组可复算的整数——价值密度、支配关系与选择份额。
站内有一篇 广告变现设计清单式短文 ,讲的是广告位怎么摆、变现点怎么列——那是一份可以直接照抄的清单。本章换一个方向:不列清单,而是搭一套可跑的价格弹性与折扣模型——档位之间怎么相互参照、折扣该打多深、价格弹性多大、副作用会吃掉多少收入,全部用本机实跑的确定数字说话。本节先解决第一件事:定价的锚点与档位。
第 2 章讲了内购点的结构(一次买什么、分成几档),第 3 章讲了 LTV 与回本。本节不重复这两块,只回答一个更前置的问题:同一批内购内容,价格标签怎么排——哪一档负责引流、哪一档负责走量、哪一档负责把整组价格的感知拉高。
4.1 定价锚点与档位设计
4.1.1 价格是相对的,不是绝对的
没有人能凭空判断「3000 分(30 元)买 100 颗钻石」是贵还是便宜。人判断价格靠的是参照:旁边那一档多少钱、能买多少东西。定价设计里最反直觉的一条经验就来自这里——一个档位的价值,往往由它旁边那个档位决定。
三个角色因此分工不同:
- 引流档:价格最低,门槛低到几乎不用想。它的任务不是赚钱,是让用户完成「第一次付费」这个动作。
- 走量档:整组里性价比最高的那一档,是你希望大多数人选的那一档,也是真正的收入主力。
- 锚点档:价格最高、单位价值未必最高。它存在的意义是把整组价格的感知拉高——当 6800 分那一档摆在那里,3000 分那一档看起来就没那么贵了。
再加一个诱饵档(decoy),就是经典的「诱饵效应」:它的内容比走量档差、价格却差不多,几乎没人会选它,但它的存在会让走量档显得划算。本节要做的,就是把「显得划算」这件事量化。
4.1.2 D 层:档位与选择模型配置
下面这份是 D 层配置:四个档位(引流 / 走量 / 诱饵 / 锚点)的价格与内容价值单位,加上一个离散选择模型的参数。金额一律以「分」记账,价值单位用「钻石」这样的等价物表示——它把不同档位的「内容多少」压到一个可比较的刻度上。
{
"version": 1,
"game": "merge-island",
"note": "定价档位设计口径(D 层)。档位价格、内容价值单位与选择模型参数均为示例假设;真实平台的档位分布与转化需真实业务环境,命令行不可复现(X)。金额一律以「分」记账,比例一律整数百分比 / 千分比,无浮点。",
"currency": "fen",
"valueUnit": "diamond",
"scenarios": {
"base": ["entry", "main", "top"],
"decoy": ["entry", "main", "decoy", "top"]
},
"tiers": [
{"id": "entry", "label": "体验包", "priceFen": 600, "valueUnits": 15, "role": "entry"},
{"id": "main", "label": "主力包", "priceFen": 3000, "valueUnits": 100, "role": "main"},
{"id": "decoy", "label": "诱饵包", "priceFen": 3000, "valueUnits": 55, "role": "decoy"},
{"id": "top", "label": "豪华包", "priceFen": 6800, "valueUnits": 180, "role": "top"}
],
"choice": {
"priceSensitivity": 3,
"dominateBonusPct": 25
}
}
两个情景共用同一批档位:base 只有引流 / 走量 / 锚点三档,decoy 多塞了一个诱饵档。priceSensitivity 是选择模型的价格敏感度指数,dominateBonusPct 是「支配加成」——被严格支配的诱饵档,会把份额推给支配它的那一档。
4.1.3 S 层:价值密度、支配与份额
下面这段是 S 层仿真脚本。它先算每个档位的价值密度(每元能买到的价值单位),再统计每个档位严格支配了几个档位(价值不低于、价格不高于,且至少一项严格),把支配关系折成效用加成,最后用「效用^k 归一化」做离散选择,输出两情景下的份额千分比。全程整数,幂运算用 Python 原生大整数。
#!/usr/bin/env python3
# file: sim/l04_tiers.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l04_tiers.py --config config/ --out out/
"""定价档位设计:价值密度、锚点位置与诱饵效应(整数、可复算)。
读 config/l04_tiers.json:档位清单 + 选择模型参数 + 两个情景(基础 / 加入诱饵)。
1) 每档价值密度 density = valueUnits * 1000000 // priceFen(每元价值,越大越划算);
2) 严格支配计数:价值不低于、价格不高于,且至少一项严格的档位数;
3) 效用 utility = density * (100 + dominateBonusPct * dominates) // 100;
4) 离散选择 share_i = utility_i^k / Σ utility_j^k(整数幂,千分比,补偿到 1000);
5) 对比基础集与加入诱饵档后主力档份额的变化——诱饵效应。
全程整数。产出 out/l04_tiers.json + out/l04_tiers.csv。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import sys
def load(cfg_dir, name):
with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
return json.load(f)
def density(t):
return t["valueUnits"] * 1000000 // t["priceFen"]
def dominates(a, b):
"""a 严格支配 b:价值不低于、价格不高于,且至少一项严格。"""
return (a["valueUnits"] >= b["valueUnits"] and a["priceFen"] <= b["priceFen"]
and (a["valueUnits"] > b["valueUnits"] or a["priceFen"] < b["priceFen"]))
def dominate_count(t, tiers):
return sum(1 for o in tiers if o["id"] != t["id"] and dominates(t, o))
def utility(t, tiers, choice):
"""效用 = 价值密度 ×(1 + 支配加成)。支配加成把被支配诱饵的份额拉向支配者。"""
return density(t) * (100 + choice["dominateBonusPct"] * dominate_count(t, tiers)) // 100
def shares_permille(tiers, choice):
"""share_i ∝ utility_i^k。整数幂 + 地板除,最大项补偿使总和 = 1000。"""
utils = [utility(t, tiers, choice) for t in tiers]
powers = [u ** choice["priceSensitivity"] for u in utils]
total = sum(powers)
raw = [p * 1000 // total for p in powers]
raw[raw.index(max(raw))] += 1000 - sum(raw)
return utils, raw
def anchor_permille(t, tiers):
"""锚点比:该档价格相对最贵档(价格锚点)的千分比。"""
return t["priceFen"] * 1000 // max(x["priceFen"] for x in tiers)
def pack(tiers, utils, shares):
return [{"id": t["id"], "label": t["label"], "role": t["role"], "priceFen": t["priceFen"],
"valueUnits": t["valueUnits"], "density": density(t),
"dominates": dominate_count(t, tiers), "anchorPermille": anchor_permille(t, tiers),
"utility": u, "sharePermille": s} for t, u, s in zip(tiers, utils, shares)]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load(args.config, "l04_tiers")
by_id = {t["id"]: t for t in cfg["tiers"]}
choice = cfg["choice"]
base = [by_id[i] for i in cfg["scenarios"]["base"]]
decoy = [by_id[i] for i in cfg["scenarios"]["decoy"]]
os.makedirs(args.out, exist_ok=True)
base_rows = pack(base, *shares_permille(base, choice))
decoy_rows = pack(decoy, *shares_permille(decoy, choice))
# 纯价值密度对照(关掉支配加成):证明诱饵效应来自支配加成,而非「多了一个低分档」
plain = {"priceSensitivity": choice["priceSensitivity"], "dominateBonusPct": 0}
plain_rows = pack(decoy, *shares_permille(decoy, plain))
share_of = lambda rows, tid: next(r["sharePermille"] for r in rows if r["id"] == tid)
main_base, main_decoy, main_plain = (share_of(base_rows, "main"), share_of(decoy_rows, "main"),
share_of(plain_rows, "main"))
best_value = max(base, key=density)["id"]
with open(os.path.join(args.out, "l04_tiers.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["scenario", "id", "role", "priceFen", "valueUnits", "density",
"dominates", "utility", "sharePermille"])
for label, rows in (("base", base_rows), ("decoy", decoy_rows)):
for r in rows:
w.writerow([label, r["id"], r["role"], r["priceFen"], r["valueUnits"],
r["density"], r["dominates"], r["utility"], r["sharePermille"]])
report = {"source": "sim/l04_tiers.py", "tier": "S", "game": cfg["game"], "note": cfg["note"],
"choice": choice, "anchorPriceFen": max(t["priceFen"] for t in cfg["tiers"]),
"bestValueTier": best_value, "baseRows": base_rows, "decoyRows": decoy_rows,
"mainSharePermille": {"base": main_base, "decoy": main_decoy,
"decoyPlainDensity": main_plain},
"decoyEffectPermille": main_decoy - main_base,
"csv": os.path.join(args.out, "l04_tiers.csv")}
with open(os.path.join(args.out, "l04_tiers.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
for label, rows in (("base", base_rows), ("decoy", decoy_rows)):
for r in rows:
print(f"{label:>5} {r['id']:>6} {r['priceFen']:>5}分/{r['valueUnits']:>3}u "
f"density={r['density']:>6} dom={r['dominates']} share={r['sharePermille']}permille")
print(f"bestValue={best_value} anchorPrice={report['anchorPriceFen']}fen")
print(f"main share: base={main_base}permille decoy={main_decoy}permille "
f"(plainDensity={main_plain}) effect={main_decoy - main_base}permille")
return 0
if __name__ == "__main__":
sys.exit(main())
本机实跑输出:
base entry 600分/ 15u density= 25000 dom=0 share=219permille
base main 3000分/100u density= 33333 dom=0 share=521permille
base top 6800分/180u density= 26470 dom=0 share=260permille
decoy entry 600分/ 15u density= 25000 dom=0 share=138permille
decoy main 3000分/100u density= 33333 dom=1 share=644permille
decoy decoy 3000分/ 55u density= 18333 dom=0 share=54permille
decoy top 6800分/180u density= 26470 dom=0 share=164permille
bestValue=main anchorPrice=6800fen
main share: base=521permille decoy=644permille (plainDensity=481) effect=123permille
4.1.4 逐条读输出
- 价值密度最高的是主力包(33333),不是最贵的豪华包(26470)。每元买到的钻石,3000 分那一档最多。这符合设计意图:主力包就是「走量档」,性价比要最好。豪华包密度更低,它不靠性价比取胜,靠的是价格锚点作用——把整组价格的感知拉高。
- 基础三档的份额是 219 / 521 / 260(体验 / 主力 / 豪华)。主力包已经拿到一半以上份额,但体验档还占了两成——这是「门槛低」的自然结果。
- 加入诱饵档后,主力包份额从 521‰ 升到 644‰(
effect=123permille)。诱饵包自己只拿到 54‰,几乎没人选,但它把份额从体验档(219→138)和豪华包(260→164)那里抽了出来,几乎全部转给了主力包。这就是诱饵效应。 plainDensity=481是关键对照。这一列是把「支配加成」关掉(dominateBonusPct=0)后,诱饵情景下主力包的份额——只有 481‰,比不加诱饵的 521‰ 还低。也就是说,如果只看价值密度,多塞一个低分档只会稀释主力份额;诱饵效应完全来自「支配关系」这一项。
要强调:这些份额是同机相对量级,绝对数值不可移植——换一组档位、换一个敏感度指数,份额都会变。可移植的是这套流程:档位与模型参数是输入,份额是输出,改一个参数就能立刻复算。
4.1.5 P 层:同一模型的 TS 内核
同一套模型在 core/ 里有一份 TypeScript 内核,供后续的网页原型与宿主复用。它的关键是幂运算用 BigInt——33333^3 已经超过 2^53,用 Number 会丢精度;除法一律整数地板除,末位补偿使份额总和恰好 1000。
// file: core/src/l04/tiers.ts
// tier: P
// verify: node --test 'core/test/l04_*.test.ts'
// 《合成小岛》定价档位内核:价值密度 + 支配计数 + 离散选择份额。
// 全整数;幂运算用 BigInt(density^3 会超 2^53);禁 enum / namespace / Math.random。
export interface Tier {
readonly id: string;
readonly label: string;
readonly priceFen: number;
readonly valueUnits: number;
readonly role: "entry" | "main" | "decoy" | "top";
}
export interface ChoiceModel {
readonly priceSensitivity: number;
readonly dominateBonusPct: number;
}
export interface TierScore {
readonly id: string;
readonly density: number;
readonly dominates: number;
readonly utility: number;
readonly sharePermille: number;
}
/** 价值密度:每元买到的价值单位 ×1000000(整数)。越大越划算。 */
export function valueDensity(valueUnits: number, priceFen: number): number {
return Math.floor((valueUnits * 1000000) / priceFen);
}
/** a 是否严格支配 b:价值不低于、价格不高于,且至少一项严格。 */
export function dominates(a: Tier, b: Tier): boolean {
const geValue = a.valueUnits >= b.valueUnits;
const lePrice = a.priceFen <= b.priceFen;
const strict = a.valueUnits > b.valueUnits || a.priceFen < b.priceFen;
return geValue && lePrice && strict;
}
/** 该档严格支配的档位数(不含自身)。 */
export function dominateCount(tier: Tier, tiers: Tier[]): number {
let n = 0;
for (const other of tiers) if (other.id !== tier.id && dominates(tier, other)) n += 1;
return n;
}
/** 效用 = 价值密度 ×(1 + 支配加成)。支配加成把「被支配的诱饵」的份额拉向支配者。 */
export function utility(tier: Tier, tiers: Tier[], choice: ChoiceModel): number {
const dom = dominateCount(tier, tiers);
return Math.floor((valueDensity(tier.valueUnits, tier.priceFen) *
(100 + choice.dominateBonusPct * dom)) / 100);
}
/** 离散选择:share_i ∝ utility_i^k。整数幂用 BigInt,末位补偿使总和 = 1000。 */
export function sharesPermille(tiers: Tier[], choice: ChoiceModel): TierScore[] {
const utils = tiers.map((t) => utility(t, tiers, choice));
const k = BigInt(choice.priceSensitivity);
const powers = utils.map((u) => BigInt(u) ** k);
const total = powers.reduce((a, b) => a + b, 0n);
const raw = powers.map((p) => Number((p * 1000n) / total));
let hi = 0;
for (let i = 1; i < raw.length; i += 1) if (raw[i] > raw[hi]) hi = i;
raw[hi] += 1000 - raw.reduce((a, b) => a + b, 0);
return tiers.map((t, i) => ({
id: t.id,
density: valueDensity(t.valueUnits, t.priceFen),
dominates: dominateCount(t, tiers),
utility: utils[i],
sharePermille: raw[i],
}));
}
/** 价值密度最高的档位 id(并列时取价格更低者)。 */
export function bestValueTier(tiers: Tier[]): string {
let best = tiers[0];
for (const t of tiers) {
const d = valueDensity(t.valueUnits, t.priceFen);
const bd = valueDensity(best.valueUnits, best.priceFen);
if (d > bd || (d === bd && t.priceFen < best.priceFen)) best = t;
}
return best.id;
}
配套单测 core/test/l04_tiers.test.ts 用 node --test 'core/test/l04_*.test.ts' 跑,逐条断言「密度整数、支配关系单向、份额总和 = 1000、诱饵使主力份额上升且不靠降低密度、敏感度越高越集中」。P 层的输出与 S 层逐位一致(主力份额 521‰ / 644‰ / 481‰ 三个数字两边相同),这正是「一份配置、跨语言一致」的验收口径。
4.1.6 三条可复用判断
- 先定走量档,再定锚点。先想清楚「你希望大多数人选哪一档」,把它的价值密度做成整组最高;最贵那一档负责拉高感知,密度可以低一点。
- 诱饵靠的是支配,不是低价。诱饵档必须被走量档严格支配(同价更低价值,或同价值更高价格)才有效;一个单纯「更便宜的档」只会稀释份额。
- 把模型参数写进配置。
priceSensitivity与dominateBonusPct是假设,不是真理——它们是输入,改了就能立刻复算份额,别把它们硬编码进代码。
小结
- 价格是相对的:一个档位的价值由它旁边的档位决定;引流 / 走量 / 锚点 / 诱饵四类角色分工不同。
- D 层配置把档位价格、价值单位与选择模型写成唯一真源;S 层算价值密度、支配关系与份额。
- 本机实跑:主力包价值密度最高(33333);加入诱饵档后主力份额从 521‰ 升到 644‰,而关掉支配加成反而降到 481‰——诱饵效应来自支配关系。
- P 层 TS 内核与 S 层逐位一致,幂运算用 BigInt;份额是同机相对量级,绝对数值不可移植。
档位和锚点定下来,下一个问题是卖的时候打不打折:折扣该打多深、限时和稀缺怎么叠加。这是下一节的事。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。