14.3 品类在海外市场的差异

同一个品类在北美、日本、韩国、东南亚的表现差异有多大?本节把「相对偏好」与「绝对盘子」两个口径放在一起,用一段 Python 仿真算每个品类的最优市场、每个市场的首选品类,再用「翻转阈值」检查哪些市场的品类结论稳、哪些一碰就翻,把「同品类不同市场」从直觉变成可复算的表。

照例先划边界。站内 Steam 发布专题 (166 篇)是 PC 买断制发行口径,与 H5 小游戏无关;卷一「小游戏市场全景」 讲的是国内平台的分发机制与设计约束(包体、首包、能力矩阵),不涉及海外各市场的品类偏好。本节都不重做,只做一件事:把「同一个品类在不同海外市场表现不同」从直觉变成可复算的表。卷一 3.1 品类分类 与 3.2 品类商业特征矩阵 已经给过一套与地区无关的品类图谱与商业特征;本节把地区这一维加进来——同一个品类,在北美、日本、韩国、东南亚的偏好与盘子差多少。

同一品类,不同市场是不同生意

在国内做小游戏,品类偏好相对同质:微信生态里火的那几个品类,抖音生态里也八九不离十。出海之后就完全不同了——同一个品类,在不同市场可能是完全不同的生意。原因有两层:一层是相对偏好(这个市场的玩家是不是「格外爱」这个品类),另一层是绝对盘子(这个市场的受众规模有多大)。两者经常不一致:一个品类可能在一个小市场偏好极高,在一个大市场却只是普通。

所以要回答「这个品类该主打哪个市场」,不能只看一层。本节的模型同时算两个量:affinityPermille(相对偏好指数)和 marketScore = 受众 × 偏好(绝对盘子的代理量),再看两个口径给出的答案是否一致——不一致的地方,才是真正需要做决策的地方。

品类-市场矩阵(D 层)

下面这段是 D 层配置:六个市场的受众规模、四个品类,以及 affinityPermille[品类][市场] 偏好指数。数值是按各市场公开可观察的品类格局整理的建模输入,标为公开可观察(观察时点 2026-10),不是任何平台后台的真实数据。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "品类在海外各市场差异的配置。口径是「公开可观察机制」的建模假设(观察时点 2026-10,非实测):把各市场的受众规模与各品类在该市场的相对偏好整理成可机读配置,不是任何平台后台的真实数据。regions.audienceMillions=该市场可触达受众(百万);genres=四个品类;affinityPermille[genre][region]=该品类在该市场的相对偏好指数(千分比,0–1000,越高越偏好)。marketScore(genre,region)=audienceMillions×affinityPermille(百万·千分比),是绝对盘子的代理量;affinity 是相对偏好的代理量,两者常不一致——某品类可在小市场强偏好、在大市场只是普通。flipDeltaPermille=若挑战品类的偏好被抬高多少千分比,该市场的首选品类会翻转(越小越敏感)。真实市场偏好与受众规模需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
  "regions": [
    { "id": "na", "name": "北美", "audienceMillions": 200 },
    { "id": "eu", "name": "西欧", "audienceMillions": 180 },
    { "id": "sea", "name": "东南亚", "audienceMillions": 160 },
    { "id": "latam", "name": "拉美", "audienceMillions": 130 },
    { "id": "jp", "name": "日本", "audienceMillions": 90 },
    { "id": "kr", "name": "韩国", "audienceMillions": 45 }
  ],
  "genres": [
    { "id": "merge", "name": "合成" },
    { "id": "puzzle", "name": "消除" },
    { "id": "idle", "name": "放置" },
    { "id": "arcade", "name": "休闲街机" }
  ],
  "affinityPermille": {
    "merge": { "na": 700, "eu": 650, "sea": 600, "latam": 560, "jp": 900, "kr": 820 },
    "puzzle": { "na": 820, "eu": 800, "sea": 780, "latam": 800, "jp": 760, "kr": 700 },
    "idle": { "na": 760, "eu": 720, "sea": 820, "latam": 700, "jp": 680, "kr": 900 },
    "arcade": { "na": 640, "eu": 660, "sea": 700, "latam": 660, "jp": 720, "kr": 760 }
  }
}

读这张表要抓住一点:偏好指数与受众规模是两套独立的输入。北美受众最大(200 百万),但它在四个品类上的偏好都在 640–820 之间,没有极端偏好;韩国受众最小(45 百万),却对「放置」有 900 的极高偏好。把两个量乘起来(marketScore),得到的「首选品类」可能和单看偏好时完全不同——这正是下一节要算的。

S 层:相对偏好与绝对盘子一起看

下面这段是 S 层仿真脚本 sim/g14_genre_region.py。它读配置,同时算两个口径(每品类的相对偏好最佳市场、绝对盘子最佳市场),再算每个市场的首选品类与翻转阈值 flipDeltaPermille,产出 JSON + CSV + SVG:

#!/usr/bin/env python3
# file: sim/g14_genre_region.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g14_genre_region.py --config config/ --out out/
"""品类在海外各市场差异仿真(S 层)。

读 config/g14_genre_region.json,用两个口径同时看「某品类在哪个市场强」:

  1) 相对偏好口径 affinity:某品类在各市场的偏好指数谁最高(该品类在哪个市场「相对」更受偏好)。
  2) 绝对盘子口径 marketScore:marketScore(genre,region)=audienceMillions×affinityPermille,
     谁最大(该品类在哪个市场「绝对」盘子更大)。

两个口径经常不一致:一个品类可能在小市场偏好极高、在大市场只是普通。再看每个市场的首选品类,
并算 flipDeltaPermille:若挑战品类的偏好被抬高多少千分比,该市场的首选品类会翻转——
flipDelta 越小,该市场的品类结论越脆(稳健性软判据,B 的职责)。

口径:受众规模与偏好指数是「公开可观察机制」口径的建模假设(观察时点 2026-10),非实测;
真实市场偏好需真实业务环境,命令行不可复现(X)。产出 out/g14_genre_region.json + .csv + .svg。
全整数、地板除,无浮点。
"""
from __future__ import annotations

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

STABLE_MARGIN = 100  # flipDelta ≥ 100 视为「品类结论稳健」


def draw_svg(path: str, rows: list[dict], genres: list[dict]) -> int:
    """纯 Python SVG:每个市场四个品类的 marketScore 横条(首选品类蓝、其余灰)。"""
    W, ROW, PAD, BAR = 800, 34, 120, 500
    H = 60 + ROW * len(rows)
    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 = "各市场品类盘子(marketScore):首选(蓝)vs 其余(灰)"
    hi = max([r["topScore"] for r in rows] + [1])
    for i, r in enumerate(rows):
        y = 46 + i * ROW
        ET.SubElement(svg, "text", {"x": "16", "y": str(y + 16), "font-size": "12",
                                    "fill": "#374151"}).text = f'{r["name"]}'
        ET.SubElement(svg, "rect", {"x": str(PAD), "y": str(y + 3), "height": "16", "rx": "3",
                                    "width": str(BAR * r["topScore"] // hi), "fill": "#2563eb"})
        ET.SubElement(svg, "text", {"x": str(PAD + BAR + 8), "y": str(y + 16), "font-size": "11",
                                    "fill": "#6b7280"}).text = f'{r["topGenre"]} {r["topScore"]}(flipΔ{r["flipDeltaPermille"]}‰)'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(svg))


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, "g14_genre_region.json"), encoding="utf-8") as f:
        cfg = json.load(f)

    regions = cfg["regions"]
    genres = [g["id"] for g in cfg["genres"]]
    aff = cfg["affinityPermille"]

    def mscore(g: str, r: dict) -> int:
        return r["audienceMillions"] * aff[g][r["id"]]

    # ---- 1) 每品类的「相对偏好」与「绝对盘子」最佳市场 ----
    per_genre = []
    for g in genres:
        by_aff = sorted(regions, key=lambda r: (-aff[g][r["id"]], r["id"]))
        by_mkt = sorted(regions, key=lambda r: (-mscore(g, r), r["id"]))
        per_genre.append({"genre": g, "affinityBest": by_aff[0]["id"], "marketBest": by_mkt[0]["id"]})

    # ---- 2) 每市场的首选品类 + flipDelta ----
    per_region = []
    for r in regions:
        ranked = sorted(genres, key=lambda g: (-mscore(g, r), g))
        top, second = ranked[0], ranked[1]
        gap = aff[top][r["id"]] - aff[second][r["id"]]   # 同市场内两品类偏好差
        per_region.append({"id": r["id"], "name": r["name"], "topGenre": top, "secondGenre": second,
                           "topScore": mscore(top, r), "secondScore": mscore(second, r),
                           "flipDeltaPermille": gap + 1,
                           "stable": 1 if gap + 1 >= STABLE_MARGIN else 0})
    per_region.sort(key=lambda x: (-x["topScore"], x["id"]))

    aff_digest = "|".join(f'{p["genre"]}:{p["affinityBest"]}' for p in per_genre)
    mkt_digest = "|".join(f'{p["genre"]}:{p["marketBest"]}' for p in per_genre)
    reg_digest = "|".join(f'{r["id"]}:{r["topGenre"]}' for r in per_region)
    flip_digest = "|".join(f'{r["id"]}:{r["flipDeltaPermille"]}' for r in per_region)
    digest = f"aff:{aff_digest};mkt:{mkt_digest};reg:{reg_digest};flip:{flip_digest}"

    os.makedirs(a.out, exist_ok=True)
    svg = draw_svg(os.path.join(a.out, "g14_genre_region.svg"), per_region, cfg["genres"])
    with open(os.path.join(a.out, "g14_genre_region.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["region", "name", "genre", "affinityPermille", "marketScore", "isTop"])
        for r in regions:
            for g in genres:
                w.writerow([r["id"], r["name"], g, aff[g][r["id"]], mscore(g, r),
                            1 if g == next(x["topGenre"] for x in per_region if x["id"] == r["id"]) else 0])
    rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
           "genres": genres, "perGenre": per_genre, "perRegion": per_region,
           "stableRegions": [r["id"] for r in per_region if r["stable"] == 1],
           "sensitiveRegions": [r["id"] for r in per_region if r["stable"] == 0],
           "digest": digest, "svg": os.path.join(a.out, "g14_genre_region.svg"), "svgBytes": svg}
    with open(os.path.join(a.out, "g14_genre_region.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"digest": digest, "perGenre": per_genre, "perRegion": per_region,
                      "stableRegions": rep["stableRegions"],
                      "sensitiveRegions": rep["sensitiveRegions"]}, ensure_ascii=False, indent=2))
    return 0


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

在本机跑 /opt/homebrew/bin/python3 sim/g14_genre_region.py --config config/ --out out/,四个品类 × 六个市场的结果是(同机相对量级,绝对数值不可移植):

digest = aff:merge:jp|puzzle:na|idle:kr|arcade:kr;mkt:merge:na|puzzle:na|idle:na|arcade:na;reg:na:puzzle|eu:puzzle|sea:idle|latam:puzzle|jp:merge|kr:idle;flip:na:61|eu:81|sea:41|latam:101|jp:141|kr:81

先看每品类的两个口径(相对偏好 = 偏好指数最高的市场,绝对盘子 = marketScore 最大的市场):

品类相对偏好最佳绝对盘子最佳读法
合成日本(900)北美偏好口径选日本,盘子口径选北美——不一致
消除北美(820)北美一致
放置韩国(900)北美偏好口径选韩国,盘子口径选北美——不一致
休闲街机韩国(760)北美偏好口径选韩国,盘子口径选北美——不一致

四个品类里有三个两个口径不一致。这正是「同一品类不同市场」的核心:合成在日本「相对最受欢迎」(偏好 900),但北美的受众规模(200 百万)让它的绝对盘子(140000)仍然最大。如果只看偏好,你会把合成主打日本;如果只看盘子,你会主打北美——两个答案都对,取决于你的目标是「高转化的小市场」还是「低转化的大市场」。

再看每个市场的首选品类(按 marketScore):

市场受众(百万)首选品类首选盘子次选盘子翻转阈值‰
北美200消除16400015200061
西欧180消除14400012960081
东南亚160放置13120012480041
拉美130消除10400091000101
日本90合成8100068400141
韩国45放置405003690081

这张表最该记住的是最后两列的关系:首选与次选的盘子越接近,翻转阈值 越小。东南亚的翻转阈值只有 41‰——只要「消除」的偏好被抬高 41‰,东南亚的首选就从「放置」翻成「消除」。北美(61)和西欧(81)也偏敏感;而日本要抬高 141‰ 才会翻,拉美要 101‰。换句话说:日本的「合成」结论最稳,东南亚的「放置」结论最脆。

稳定性:哪些市场的品类结论靠得住

flipDeltaPermille 是这一节最实用的输出,因为它把「结论稳不稳」量化了。按阈值 100‰ 分档(软判据,非硬标准):

  • 稳健市场(翻转阈值 ≥ 100‰):拉美(101)、日本(141)——这两个市场的首选品类有足够的领先幅度,小幅偏好波动不会翻盘;
  • 敏感市场(翻转阈值 < 100‰):北美(61)、西欧(81)、东南亚(41)、韩国(81)——这四个市场的首选与次选咬得很紧,一点偏好变化就可能翻转结论。

工程上的用法很直接:在敏感市场不要押单一品类。东南亚的首选(放置)与次选(消除)只差 6400 的盘子,与其二选一,不如把两个品类都上、用真实数据决定加注哪个;而在稳健市场(日本、拉美),可以放心按首选品类排资源。这就是「B 基准」在这里的职责——不是给一个更准的数,而是告诉你哪个数经得起扰动。

这也把本章三节串了起来:平台按什么排序决定谁上榜(14.1),竞品在哪个语言留了缺口决定你从哪进(14.2),品类在哪个市场稳、哪个市场脆决定你押多重(14.3)。三者合起来,才是「发到哪个市场、主打什么品类、用什么语言」的完整答案。下一步,第 15 章 把这三者装配成一份可执行的出海方案。

X 层:真实市场偏好与受众规模不可复现

必须把边界划清。上面算的是偏好模型:给定一份偏好矩阵与受众规模,首选品类是什么、翻转阈值多大。而下面这些属于 X 层,需真实业务环境,命令行不可复现:

  • 各市场的真实品类偏好(同一品类在不同地区、不同时期的偏好差异极大,本节只作「公开可观察机制」引用并标注观察时点 2026-10);
  • 各市场的真实受众规模(可触达用户受平台、设备、网络条件影响,无法用单一数字刻画);
  • 真实的品类转化率与变现能力(偏好高不等于变现强,需真实买量与留存数据才能核算)。

工程上正确的做法是:把可复现的偏好模型与不可复现的真实数据分开——模型回答「如果偏好是这样,哪个品类在哪个市场稳」,真实数据用来在发行后反标定偏好矩阵(把实测的品类占比与留存填回 config,结论立刻反映现实)。

自检表

检查项合格标准
双口径并列相对偏好与绝对盘子同时给出,不合并成单一口径
不一致显式指出哪些品类两个口径不一致,不掩盖分歧
稳定性可算每个市场给出翻转阈值,分稳健 / 敏感两档
敏感市场有对策敏感市场不押单一品类,给出双品类策略
与前三章分工只算「品类在哪稳」,平台 / 竞品 / 落地交邻节
边界清晰真实偏好与受众规模标 X,不写成「实测」

品类差异最危险的错觉,是把「这个品类在 A 市场火」当成「它在所有市场都火」。真正决定你押多重的,是首选品类的领先幅度稳不稳——翻转阈值大的市场可以重仓,阈值小的市场要留后手。到这里,第 14 章「海外榜单与竞品」就完整了:平台看排序机制(14.1)、竞品看本地化缺口(14.2)、品类看市场差异(14.3)。下一章把这套判断装配成一份可执行的出海方案。

阅读导航:上一节:14.2 竞品拆解与本地化机会 · 下一节:15.1 平台组合与接入时间线 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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