本节目标:用弹性解释「收入为什么在某个折扣掉头」,并把折扣的副作用从毛收入里扣掉,找出净收入最优折扣。
上一节找到了毛收入峰值折扣(30%),但没回答「为什么偏偏是 30%」。答案是价格弹性:弹性跨过单位弹性那一点,收入就掉头。而且折扣不是免费的——它把未来的原价单提前卖掉、侵蚀参考价、赶走高价值用户,这三项副作用让净收入最优折扣比毛收入最优更浅。本节和广告变现那篇《游戏广告变现设计 2026》 是分工关系:那篇给的是广告变现的清单,本节做的是可跑的价格弹性与折扣副作用模型。内购点结构是第 2 章、LTV 是第 3 章,本节不越界。
4.3 价格弹性与副作用
4.3.1 收入为什么会在某个折扣掉头
收入 R = 价格 × 数量。降价时价格线性下降,数量却按弹性上升:|E| > 1 时数量涨得比价格快,收入上升;|E| < 1 时数量涨得慢,收入下降。|E| 恰好等于 1 的那一点就是收入峰值。所以「峰值折扣」不是拍脑袋定的,而是弹性曲线的拐点。
4.3.2 D 层:价格-需求点与副作用系数表
下面这份是 D 层配置:9 个价格-需求点(和上一节同一张需求表)、以及三项副作用随折扣变化的系数表。futureUnits 是未来窗口的原价需求,pullForwardPct 是「被提前卖掉」的比例,erosionPct 是未来原价需求永久下降的比例,whaleChurnPermille 是高价值用户流失的千分比。所有系数都是示例假设,真实值需真实业务环境(X)。
{
"version": 1,
"game": "merge-island",
"note": "价格弹性与折扣副作用口径(D 层)。价格-需求点与副作用系数均为示例假设;真实弹性、参考价侵蚀与鲸鱼流失需真实业务环境,命令行不可复现(X)。金额一律以「分」记账,比例一律整数百分比 / 千分比,无浮点。",
"currency": "fen",
"listPriceFen": 3000,
"priceLadder": [
{"priceFen": 3000, "units": 1000},
{"priceFen": 2700, "units": 1350},
{"priceFen": 2400, "units": 1750},
{"priceFen": 2100, "units": 2150},
{"priceFen": 1800, "units": 2450},
{"priceFen": 1500, "units": 2620},
{"priceFen": 1200, "units": 2720},
{"priceFen": 900, "units": 2760},
{"priceFen": 600, "units": 2780}
],
"sideEffects": {
"futureUnits": 1000,
"whaleCount": 200,
"whaleArppuFen": 20000,
"table": [
{"discountPct": 0, "pullForwardPct": 0, "erosionPct": 0, "whaleChurnPermille": 0},
{"discountPct": 10, "pullForwardPct": 8, "erosionPct": 1, "whaleChurnPermille": 2},
{"discountPct": 20, "pullForwardPct": 15, "erosionPct": 3, "whaleChurnPermille": 6},
{"discountPct": 30, "pullForwardPct": 22, "erosionPct": 6, "whaleChurnPermille": 12},
{"discountPct": 40, "pullForwardPct": 30, "erosionPct": 10, "whaleChurnPermille": 22},
{"discountPct": 50, "pullForwardPct": 38, "erosionPct": 16, "whaleChurnPermille": 36},
{"discountPct": 60, "pullForwardPct": 45, "erosionPct": 24, "whaleChurnPermille": 55},
{"discountPct": 70, "pullForwardPct": 50, "erosionPct": 34, "whaleChurnPermille": 80},
{"discountPct": 80, "pullForwardPct": 52, "erosionPct": 40, "whaleChurnPermille": 100}
]
}
}
4.3.3 S 层:弧弹性 + 毛收入 + 副作用 + 净收入
下面这段是 S 层仿真脚本:算相邻价格点的弧弹性、找单位弹性拐点,再逐档算毛收入、三项副作用损失和净收入,最后对比毛最优与净最优。全程整数,金额以分记账。
#!/usr/bin/env python3
# file: sim/l04_elasticity.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l04_elasticity.py --config config/ --out out/
"""价格弹性与折扣副作用:毛收入最优 ≠ 净收入最优。
读 config/l04_elasticity.json:价格-需求点 + 副作用系数表。
1) 弧弹性:相邻点 |E| = (ΔQ·(P1+P2)) / ((Q1+Q2)·ΔP),千分比;|E|>1000 为富有弹性;
2) 毛收入 price × units,找毛收入峰值折扣;
3) 副作用三项:需求前置(把未来原价单提前卖掉)+ 参考价侵蚀(未来原价需求永久下降)
+ 鲸鱼流失(高价值用户因深折而流失);
4) 净收入 = 毛收入 - 三项损失,找净收入峰值折扣;对比毛最优与净最优的深度差与收入差。
全程整数(分)。产出 out/l04_elasticity.json + out/l04_elasticity.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 arc_elasticity_milli(p1, q1, p2, q2):
"""弧弹性绝对值(千分比):(ΔQ·(P1+P2)) / ((Q1+Q2)·ΔP)。价格降、需求升时为正。"""
dp, dq = p1 - p2, q2 - q1
if dp <= 0 or (q1 + q2) == 0:
return 0
return dq * (p1 + p2) * 1000 // ((q1 + q2) * dp)
def discount_pct(list_price_fen, price_fen):
return (list_price_fen - price_fen) * 100 // list_price_fen
def side_loss(list_price_fen, units, effect, se):
"""三项副作用损失(分):需求前置 + 参考价侵蚀 + 鲸鱼流失。"""
pull_units = units * effect["pullForwardPct"] // 100
pull_loss = pull_units * list_price_fen
erosion_loss = se["futureUnits"] * list_price_fen * effect["erosionPct"] // 100
churn = se["whaleCount"] * effect["whaleChurnPermille"] // 1000
whale_loss = churn * se["whaleArppuFen"]
return {"pullUnits": pull_units, "pullLossFen": pull_loss, "erosionLossFen": erosion_loss,
"whaleChurn": churn, "whaleLossFen": whale_loss,
"totalLossFen": pull_loss + erosion_loss + whale_loss}
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_elasticity")
lp, ladder, se = cfg["listPriceFen"], cfg["priceLadder"], cfg["sideEffects"]
eff_by_d = {e["discountPct"]: e for e in se["table"]}
os.makedirs(args.out, exist_ok=True)
elast = [{"fromDiscountPct": discount_pct(lp, a["priceFen"]),
"toDiscountPct": discount_pct(lp, b["priceFen"]),
"elasticityMilli": arc_elasticity_milli(a["priceFen"], a["units"], b["priceFen"], b["units"])}
for a, b in zip(ladder, ladder[1:])]
for e in elast:
e["elastic"] = e["elasticityMilli"] > 1000
rows = []
for pt in ladder:
d = discount_pct(lp, pt["priceFen"])
gross = pt["priceFen"] * pt["units"]
loss = side_loss(lp, pt["units"], eff_by_d[d], se)
rows.append({"discountPct": d, "priceFen": pt["priceFen"], "units": pt["units"],
"grossFen": gross, **loss, "netFen": gross - loss["totalLossFen"]})
gross_best = max(rows, key=lambda r: r["grossFen"])
net_best = max(rows, key=lambda r: r["netFen"])
unit_elastic_at = next((e["fromDiscountPct"] for e in elast if not e["elastic"]), None)
with open(os.path.join(args.out, "l04_elasticity.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["discountPct", "priceFen", "units", "grossFen", "pullLossFen",
"erosionLossFen", "whaleLossFen", "totalLossFen", "netFen"])
for r in rows:
w.writerow([r["discountPct"], r["priceFen"], r["units"], r["grossFen"],
r["pullLossFen"], r["erosionLossFen"], r["whaleLossFen"],
r["totalLossFen"], r["netFen"]])
report = {"source": "sim/l04_elasticity.py", "tier": "S", "game": cfg["game"],
"note": cfg["note"], "listPriceFen": lp, "elasticity": elast, "rows": rows,
"unitElasticFromDiscountPct": unit_elastic_at,
"grossBestDiscountPct": gross_best["discountPct"], "grossBestFen": gross_best["grossFen"],
"netBestDiscountPct": net_best["discountPct"], "netBestFen": net_best["netFen"],
"netAtGrossBestFen": gross_best["netFen"],
"gapFen": gross_best["grossFen"] - gross_best["netFen"],
"csv": os.path.join(args.out, "l04_elasticity.csv")}
with open(os.path.join(args.out, "l04_elasticity.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
for e in elast:
print(f"discount {e['fromDiscountPct']:>2}%->{e['toDiscountPct']:>2}% "
f"|E|={e['elasticityMilli']:>5}permille elastic={e['elastic']}")
for r in rows:
print(f"discount={r['discountPct']:>2}% gross={r['grossFen']:>8} loss={r['totalLossFen']:>8} "
f"(pull={r['pullLossFen']} ero={r['erosionLossFen']} whale={r['whaleLossFen']}) "
f"net={r['netFen']:>8}")
print(f"unit-elastic from discount={unit_elastic_at}%")
print(f"grossBest={gross_best['discountPct']}% ({gross_best['grossFen']}分) "
f"netBest={net_best['discountPct']}% ({net_best['netFen']}分) "
f"netAtGrossBest={gross_best['netFen']}分 gap={report['gapFen']}分")
return 0
if __name__ == "__main__":
sys.exit(main())
本机实跑(金额单位「分」):弧弹性从 0→10% 的 |E|=2829‰ 一路降到 70→80% 的 18‰;20→30% 段 |E|=1538‰(仍富有弹性),30→40% 段掉到 847‰(转为缺乏弹性)——单位弹性拐点在 30%,正是上一节的毛收入峰值。净收入侧:毛最优 30%(4515000 分),净最优 20%(3304000 分),净最优比毛最优浅 10 个百分点。
4.3.4 逐条读输出
- 单位弹性拐点 = 30%,和毛收入峰值重合。这不是巧合:
R = P × Q,收入对价格的导数符号由 |E| 是否大于 1 决定。|E| 从富有(>1000‰)切到缺乏(<1000‰)的位置,就是收入从升转降的位置。弹性给了峰值一个因果解释,而不只是一个观测值。 - 副作用随折扣非线性放大,需求前置是大头。30% 折时总损失 1639000 分,其中需求前置 1419000 分(约 87%)、参考价侵蚀 180000 分、鲸鱼流失 40000 分。深折把「本来会原价买」的未来需求提前折价卖掉,这一项最贵。
- 净最优比毛最优浅:毛最优 30%、净最优 20%。在毛最优那个点上,净收入只有 2876000 分,比毛收入 4515000 分少了 1639000 分(gap)。按毛收入决策,会把折扣打得太深。
- 净收入在 60% 起转负(-1348000 分):深折的副作用损失超过了毛收入。这解释了为什么「大促打到骨折」通常不可持续。数字是同机相对量级,绝对数值不可移植。
4.3.5 B 层:副作用强度敏感度基准
副作用系数是假设,结论稳不稳?下面这段是 B 层基准脚本:把三项系数整体乘以 0%~200% 的强度系数,复算净最优折扣。它是软告警——边界退化只打印 WARN,不硬失败。
#!/usr/bin/env python3
# file: sim/l04_elasticity_bench.py
# tier: B
# verify: /opt/homebrew/bin/python3 sim/l04_elasticity_bench.py --config config/ --out out/
"""价格弹性副作用的敏感度基准(软告警,不硬失败):扫副作用强度,看净收入最优折扣怎么漂。
副作用系数(需求前置 + 参考价侵蚀 + 鲸鱼流失)都带主观性。本基准把它们整体乘以一个
强度系数(0% ~ 200%),复算净收入最优折扣,检验「净最优比毛最优更浅」这个结论是否稳健。
全程整数,复用 S 层的折算逻辑。产出 out/l04_elasticity_bench.json。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from l04_elasticity import discount_pct, load, side_loss # noqa: E402
SCALES_PCT = (0, 50, 100, 150, 200)
def scale_effect(effect, scale_pct):
return {"pullForwardPct": effect["pullForwardPct"] * scale_pct // 100,
"erosionPct": effect["erosionPct"] * scale_pct // 100,
"whaleChurnPermille": effect["whaleChurnPermille"] * scale_pct // 100}
def net_best_for(cfg, scale_pct):
lp = cfg["listPriceFen"]
se = cfg["sideEffects"]
eff_by_d = {e["discountPct"]: e for e in se["table"]}
best_d, best_net = 0, None
for pt in cfg["priceLadder"]:
d = discount_pct(lp, pt["priceFen"])
gross = pt["priceFen"] * pt["units"]
loss = side_loss(lp, pt["units"], scale_effect(eff_by_d[d], scale_pct), se)
net = gross - loss["totalLossFen"]
if best_net is None or net > best_net:
best_d, best_net = d, net
return best_d, best_net
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_elasticity")
os.makedirs(args.out, exist_ok=True)
deepest = max(discount_pct(cfg["listPriceFen"], p["priceFen"]) for p in cfg["priceLadder"])
rows = []
for s in SCALES_PCT:
d, net = net_best_for(cfg, s)
rows.append({"scalePct": s, "netBestDiscountPct": d, "netBestFen": net})
# 软告警:净最优退到边界(0% 或最深档),说明副作用模型把折扣一棍子打死 / 或副作用失效
warns = [f"强度 {r['scalePct']}% 时净最优折扣退到边界 {r['netBestDiscountPct']}%"
for r in rows if r["netBestDiscountPct"] in (0, deepest)]
discounts = [r["netBestDiscountPct"] for r in rows]
report = {"source": "sim/l04_elasticity_bench.py", "tier": "B", "scalesPct": list(SCALES_PCT),
"rows": rows, "netBestRangePct": [min(discounts), max(discounts)],
"warnings": warns, "ok": not warns}
with open(os.path.join(args.out, "l04_elasticity_bench.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
for r in rows:
print(f"scale={r['scalePct']:>3}% netBestDiscount={r['netBestDiscountPct']:>2}% "
f"net={r['netBestFen']}分")
for w in warns:
print(f" WARN {w}")
print(f"== 弹性副作用基准:{'OK' if report['ok'] else 'WARN'}(软告警不硬失败)"
f" 净最优区间={report['netBestRangePct']}")
return 0
if __name__ == "__main__":
sys.exit(main())
本机实跑:强度 0% 时净最优 30%(与毛最优一致),50% 与 100% 时都是 20%,150% 时 10%,200% 时退到边界 0% 并打出 WARN;净最优区间 [0%, 30%]。结论是:「净最优比毛最优更浅」在所有非零强度下都成立(方向稳健),但「浅多少」随副作用系数量级剧烈漂移——所以这套模型给的是方向,不是精确折扣点。
4.3.6 三条可复用判断
- 用弹性找拐点,别靠试错。峰值折扣就是 |E| 跨过 1 的位置;算一遍弧弹性就知道,不用一轮轮上线试。
- 按净收入决策,不按毛收入。深折的副作用不是零:需求前置最贵。按毛收入选折扣会系统性打得太深。
- 系数是假设,用区间说话。副作用系数带主观性,跑一遍强度敏感度基准,给一个折扣区间而非一个点。
小结
- 价格弹性解释收入为何在某个折扣掉头:|E| 跨过 1 的拐点即收入峰值。本机实跑拐点在 30%。
- 折扣三项副作用(需求前置 / 参考价侵蚀 / 鲸鱼流失)从毛收入里扣掉,净最优从 30% 收到 20%。
- 强度敏感度基准显示「净最优更浅」方向稳健,但深度随系数量级漂移,结论应给区间。
- 所有数字为示例假设下的同机相对量级,绝对数值不可移植;真实弹性与副作用需真实业务环境(X)。
到这里,第 4 章的三块拼图齐了:档位锚点定静态价格(4.1),折扣 + 限时 + 稀缺做动态促销(4.2),弹性与副作用给出决策边界(4.3)。定价只决定「单个用户值多少」,接下来要解决「用户从哪来、花多少钱买」——下一章进入用户获取。
阅读导航:上一节:折扣、限时与稀缺 · 下一节:用户获取渠道机制 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。