前两节把市场读成了结构、把竞品量成了坐标,这一节要把两者收成一句话:我们打算在哪个位置、做一件别人没占住的事。这就是差异化机会。照例先说分工:/h5游戏产业全景/ 那篇是 H5 小游戏的产业综述,讲格局与趋势;本章给的是可量化的榜单结构分析与机会评估——把「哪里有位置」从一句直觉,变成一张可打分、可排序、可做敏感性检验的表。/posts/products/
下的 PRD 系列是产品需求文档,回答「要做成什么样」;本章给的是可跑的数据分析与机会评估,回答「值不值得往那个方向做」。PRD 是终点,本章是决策前的输入。
从「别人做了什么」到「哪里有位置」
2.2 的竞品表回答的是「别人做到了什么程度」,但一张竞品表本身不告诉你该往哪走。差异化机会要同时满足三个条件,缺一不可:
- 有空白——这个方向还没被头部占满,玩家需求没被充分满足。
- 竞争可控——不是最挤的赛道,进入后还有生存与获客空间。
- 能变现——这个方向的玩家愿意付费、或者广告效率够高。
三者常常互相矛盾:最空白的往往变现弱,最赚钱的往往最挤。所以不能靠单一指标排序,必须把它们放进一个加权评分里权衡——而权重本身,又必须经得起推敲。这就是本节要建的两样东西:机会矩阵(S 层)与权重敏感性基准(B 层)。
机会矩阵的三维定义
把上面三条件翻译成可打分的三个维度(0~100 整数分):
| 维度 | 含义 | 方向 | 打分依据(公开可观察 / 示例) |
|---|---|---|---|
| 空白度 gap | 该细分被满足的程度 | 越高越空白 | 榜上同类数量、玩法同质化程度 |
| 竞争度 competition | 该细分的拥挤程度 | 越高越挤 | 头部集中度、买量激烈程度 |
| 变现力 monetization | 该细分的变现效率 | 越高越好 | 付费深度、广告形态契合度 |
评分时,竞争度是反向项——越挤分越低,所以机会分里用 (100 - competition) 作惩罚项。候选机会本身是「品类 × 机制」的组合,比如「合成 + 装扮」「塔防 + 合成」——注意这里只是列出候选并打分,不决定做哪个(那是第 3 章的活)。
下面这段是 D 层配置:一份机会矩阵,含三维定义与权重,以及 8 个候选机会的示例评分,另附 B 层要用的权重敏感性网格。
{
"version": 1,
"observation": "示例数据(非真实市场调研)",
"disclaimer": "本表的候选机会与三个评分均为作者构造的示例,用于演示「空白度 × 竞争度 × 变现力」加权评分与权重敏感性分析的算法,不代表对任何真实细分市场的调研结论。真实市场规模与竞争强度需真实业务环境,命令行不可复现(X)。",
"dimensions": [
{ "id": "gap", "name": "空白度", "weight": 40, "note": "越高越空白,越有差异化空间" },
{ "id": "monetization", "name": "变现力", "weight": 40, "note": "越高越容易变现" },
{ "id": "competition", "name": "竞争度", "weight": 20, "note": "越高越拥挤,评分时取 (100 - competition) 作惩罚项" }
],
"opportunities": [
{ "id": "merge-dressup", "name": "合成 + 装扮", "gap": 70, "competition": 45, "monetization": 75 },
{ "id": "merge-idle", "name": "合成 + 放置", "gap": 55, "competition": 70, "monetization": 80 },
{ "id": "merge-level", "name": "合成 + 关卡", "gap": 50, "competition": 75, "monetization": 78 },
{ "id": "match3-decorate", "name": "消除 + 装修", "gap": 30, "competition": 90, "monetization": 88 },
{ "id": "puzzle-narrative", "name": "益智 + 叙事", "gap": 65, "competition": 40, "monetization": 55 },
{ "id": "sim-merge", "name": "模拟经营 + 合成", "gap": 60, "competition": 55, "monetization": 70 },
{ "id": "tower-merge", "name": "塔防 + 合成", "gap": 72, "competition": 35, "monetization": 60 },
{ "id": "idle-collect", "name": "放置 + 收集", "gap": 40, "competition": 85, "monetization": 82 }
],
"sensitivityGrid": {
"gap": [20, 30, 40, 50],
"monetization": [20, 30, 40, 50],
"competitionMin": 10,
"competitionMax": 30,
"topK": 3
}
}
三个维度分都是示例评分,disclaimer 把「非真实调研」写死在文件里。权重的选择本身也是产品判断——这里给空白度与变现力各 40、竞争度 20,因为对一个小团队来说「有位置」和「能赚钱」比「挤不挤」更重要。这个权重凭什么? 这正是 B 层要回答的问题:如果换个权重结论就翻盘,那这个结论就不牢靠。
加权评分(S 层)
下面这段是 S 层仿真脚本,读 config/ch02_opportunity.json,按权重把三维评分加权成 0~100 的机会分并排序,产出 JSON + CSV + 一个纯 Python SVG 排序条形图。全程整数地板除:机会分 = (wGapgap + wMonetmonetization + wComp(100-competition)) // 100*。
# file: sim/ch02_opportunity_matrix.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch02_opportunity_matrix.py --config config/ --out out/
"""差异化机会矩阵(示例数据口径):读 config/ch02_opportunity.json,把每个候选机会在
「空白度 / 竞争度 / 变现力」三维上按权重加权成 0~100 的机会分,产出 JSON + CSV + SVG。
机会分 = (wGap*gap + wMonet*monetization + wComp*(100-competition)) // 100,全程整数地板除。
纪律:输入是示例评分,不是真实市场调研。真实细分市场规模与竞争强度属 X,命令行不可复现。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load_config(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def weights(cfg: dict) -> dict:
return {d["id"]: d["weight"] for d in cfg["dimensions"]}
def opportunity_score(opp: dict, w: dict) -> int:
"""三维加权机会分(0~100 整数):竞争度取 (100 - competition) 作惩罚项。"""
return (w["gap"] * opp["gap"] + w["monetization"] * opp["monetization"]
+ w["competition"] * (100 - opp["competition"])) // 100
def rank(cfg: dict) -> list[dict]:
w = weights(cfg)
rows = []
for o in cfg["opportunities"]:
rows.append({"id": o["id"], "name": o["name"], "gap": o["gap"],
"competition": o["competition"], "monetization": o["monetization"],
"score": opportunity_score(o, w)})
rows.sort(key=lambda r: (-r["score"], r["id"]))
return rows
def write_csv(path: str, rows: list[dict]) -> None:
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "name", "gap", "competition", "monetization", "score"])
for r in rows:
w.writerow([r["id"], r["name"], r["gap"], r["competition"], r["monetization"], r["score"]])
def write_svg(path: str, rows: list[dict]) -> None:
left, top, bar_h, gap = 140, 56, 30, 40
width, height = left + 420, top + gap * len(rows) + 24
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(width),
"height": str(height), "viewBox": f"0 0 {width} {height}"})
ET.SubElement(svg, "text", {"x": str(left), "y": "30", "font-size": "16",
"font-family": "sans-serif"}).text = "差异化机会分排序(示例数据,0~100)"
for i, r in enumerate(rows):
y = top + i * gap
ET.SubElement(svg, "text", {"x": str(left - 10), "y": str(y + 20), "font-size": "12",
"text-anchor": "end", "font-family": "sans-serif"}).text = r["name"]
ET.SubElement(svg, "rect", {"x": str(left), "y": str(y), "width": str(int(3.4 * r["score"])),
"height": str(bar_h), "fill": "#7c3aed"})
ET.SubElement(svg, "text", {"x": str(left + 348), "y": str(y + 20), "font-size": "12",
"font-family": "sans-serif"}).text = f"{r['score']}"
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load_config(os.path.join(args.config, "ch02_opportunity.json"))
os.makedirs(args.out, exist_ok=True)
rows = rank(cfg)
write_csv(os.path.join(args.out, "ch02_opportunity.csv"), rows)
write_svg(os.path.join(args.out, "ch02_opportunity.svg"), rows)
report = {"source": "sim/ch02_opportunity_matrix.py", "observation": cfg["observation"],
"disclaimer": cfg["disclaimer"], "weights": weights(cfg), "ranking": rows,
"top1": rows[0]["id"], "artifacts": ["out/ch02_opportunity.csv", "out/ch02_opportunity.svg"]}
with open(os.path.join(args.out, "ch02_opportunity.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({"top1": report["top1"], "ranking": rows}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
同机实跑(8 个候选机会,节选输出):
top1: merge-dressup(合成 + 装扮) 机会分=69
ranking(机会分降序):
合成 + 装扮 gap=70 competition=45 monetization=75 69
塔防 + 合成 gap=72 competition=35 monetization=60 65
模拟经营 + 合成 gap=60 competition=55 monetization=70 61
合成 + 放置 gap=55 competition=70 monetization=80 60
益智 + 叙事 gap=65 competition=40 monetization=55 60
合成 + 关卡 gap=50 competition=75 monetization=78 56
放置 + 收集 gap=40 competition=85 monetization=82 51
消除 + 装修 gap=30 competition=90 monetization=88 49
产物: out/ch02_opportunity.csv out/ch02_opportunity.svg
读法:「合成 + 装扮」以 69 分居首——空白度不低(70)、竞争可控(45)、变现力中上(75),三项都均衡。「消除 + 装修」变现力最高(88),但因为空白度只有 30、竞争度高达 90,被惩罚项拉到了垫底——这就是「最赚钱的往往最挤」的量化体现。如果只看变现力排序,会得出完全相反的结论。
B 基准:评分权重敏感性
单靠一组权重排出来的名次,最容易被质疑的一句话是:「你换个权重试试?」B 层基准就是来回答这句话的。下面这段是 B 层基准脚本:把三个权重放进网格遍历(竞争度权重由 100 - 前两者 推出并限定区间),每个组合重算一次排序,统计每个机会进 Top-K 的频次与首位频次,把机会分成「稳健」与「权重敏感」。脚本永远 exit 0,只出软告警,不判决对错。
# file: sim/ch02_opportunity_sensitivity.py
# tier: B
# verify: /opt/homebrew/bin/python3 sim/ch02_opportunity_sensitivity.py --config config/ --out out/
"""B 基准:机会矩阵的「权重敏感性」。
把空白度 / 变现力 / 竞争度的权重在网格上遍历(竞争度权重由 100 - 前两者推出,限定在区间内),
每个权重组合重算一次机会分排序,统计每个候选机会进入 Top-K 的频次与首位频次,
据此把机会分成「稳健」(几乎所有权重组合都进 Top-K)与「敏感」(进不进 Top-K 取决于权重)。
软告警不硬失败:脚本永远 exit 0,只把权重敏感的机会列进 warnings 供决策,不作对错判决。
产出 out/ch02_opportunity_sensitivity.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 ch02_opportunity_matrix import load_config, opportunity_score # noqa: E402
def grid_combos(g: dict) -> list[dict]:
combos = []
for wg in g["gap"]:
for wm in g["monetization"]:
wc = 100 - wg - wm
if g["competitionMin"] <= wc <= g["competitionMax"]:
combos.append({"gap": wg, "monetization": wm, "competition": wc})
return combos
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load_config(os.path.join(args.config, "ch02_opportunity.json"))
g = cfg["sensitivityGrid"]
top_k = g["topK"]
combos = grid_combos(g)
os.makedirs(args.out, exist_ok=True)
tally: dict[str, dict] = {o["id"]: {"name": o["name"], "topK": 0, "first": 0} for o in cfg["opportunities"]}
n = len(combos)
for combo in combos:
scored = sorted(((o["id"], opportunity_score(o, combo)) for o in cfg["opportunities"]),
key=lambda t: (-t[1], t[0]))
for rank, (oid, _score) in enumerate(scored[:top_k]):
tally[oid]["topK"] += 1
if rank == 0:
tally[oid]["first"] += 1
rows = []
for oid, t in tally.items():
rows.append({"id": oid, "name": t["name"],
"topKRateBp": t["topK"] * 10000 // n,
"firstRateBp": t["first"] * 10000 // n,
"topKCount": t["topK"], "firstCount": t["first"]})
rows.sort(key=lambda r: (-r["topKRateBp"], -r["firstRateBp"], r["id"]))
stable = [r["id"] for r in rows if r["topKRateBp"] >= 9000]
sensitive = [r["id"] for r in rows if 0 < r["topKRateBp"] < 9000]
never = [r["id"] for r in rows if r["topKRateBp"] == 0]
report = {"source": "sim/ch02_opportunity_sensitivity.py", "kind": "benchmark", "severity": "soft",
"note": "同机相对量级,绝对数值不可移植;这是权重敏感性对照,不是对错判决。",
"gridCombos": n, "topK": top_k, "rows": rows,
"stable": stable, "sensitive": sensitive, "neverTopK": never,
"softWarnings": sensitive}
with open(os.path.join(args.out, "ch02_opportunity_sensitivity.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2))
sys.exit(0) # B 层软告警:永远成功退出
if __name__ == "__main__":
main()
同机实跑(9 个权重组合,节选输出):
gridCombos: 9 topK: 3
stable(进 Top-3 率 >= 90%): 合成 + 装扮(topK 9/9, 首位 8/9)
塔防 + 合成(topK 9/9, 首位 1/9)
sensitive(软告警): 合成 + 放置(4/9) 益智 + 叙事(4/9) 模拟经营 + 合成(1/9)
neverTopK: 放置 + 收集 消除 + 装修 合成 + 关卡
产物: out/ch02_opportunity_sensitivity.json
读法:9 个权重组合里,「合成 + 装扮」和「塔防 + 合成」几乎总是进 Top-3——它们是权重稳健的机会,不管你更看重空白还是变现,都稳居前列,值得优先考虑。而「合成 + 放置」「益智 + 叙事」「模拟经营 + 合成」进不进 Top-3 完全取决于权重怎么定——它们是权重敏感的,软告警提醒:对这几个机会,别急着下结论,先想清楚「我们到底更看重什么」。垫底的「消除 + 装修」在所有权重组合里都进不了 Top-3,可以安全排除。
这条软告警才是本节最有用的产物:它把「我们选了合成 + 装扮」从一句主观判断,变成了「在权重不确定的情况下,这个选择依然稳健」的可复核结论。同时它诚实地告诉你:如果团队里有人坚持「变现力最重要」,那么排名会变——这不是错误,而是需要被摆到台面上讨论的分歧。
从矩阵到下一步
机会矩阵跑完,你手里有了两样东西:一张排序表和一份稳健性报告。它们不是终点,而是下一章的输入:
- 排序表告诉你在候选里哪些综合分高;
- 稳健性报告告诉你哪些结论经得起权重质疑。
但矩阵不决定最终做哪个品类——因为机会分只考虑了「市场位置」,还没考虑「团队能力、开发成本、平台约束」。把这两者合起来做加权决策,是第 3 章品类选择模型的事。本节只负责把「哪里有位置」这件事,从直觉变成一张能被打分、被质疑、被复核的表。
小结
- 差异化机会要同时满足有空白、竞争可控、能变现,三者常互相矛盾,必须加权权衡。
- 机会矩阵三维:空白度 gap / 竞争度 competition / 变现力 monetization,竞争度作惩罚项。
- 聚合全用整数算术:机会分 = (wGapgap + wMonetmonetization + wComp(100-competition)) // 100*。
- 示例跑出:「合成 + 装扮」居首(69);「消除 + 装修」变现最高却因太挤垫底。
- B 基准遍历权重网格,把机会分成稳健(合成 + 装扮 / 塔防 + 合成)与权重敏感(合成 + 放置等),软告警不硬失败。
- 真实市场规模与竞争强度属 X;本节全部为示例数据,矩阵只评估市场位置,选型决策在第 3 章。
阅读导航:上一节:2.2 竞品拆解方法 · 下一节:品类分类体系 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。