交叉表分群画像 deck
把**交叉表(banner / crosstab)**做成一份能直接给客户讲的 segmentation & targeting deck: 选对分群维度 → 每群一张同题的画像卡 → 排优先级 → 过一遍一致性 QC。
这个 skill 真正值钱的地方不是画 PPT,是最后一关 QC。 客户最容易当场问倒你的三个问题——"这几个百分比加起来怎么是 101%?"、 "你写着 CORE TARGET 为什么排第二?"、"这个群才 108 个人你敢下这个结论?"—— 都由
validate_deck.py在出片前拦住。
When to Use
触发场景:
- 用户拿着调研数据(CSV / Excel / SPSS-Persig 导出 / 已有 banner JSON)要"做分群 / 做人群画像 / 定目标人群"
- 需要回答"该打哪群人""这群人有什么特点""每个群的规模多大"
- 已有 deck 需要复核一致性(可单独用
validate_deck.py)
不适用:
- 只有单个总体、没有分组维度的数据分析 → 用普通 topline
- 需要做统计建模(聚类 / 因子 / 潜类)→ 本 skill 假定分群口径已经定好,只做呈现与优先级
- 定性深访/座谈会整理
五步工作流
① 解析 parse_banner.py 任何来源 -> 统一 banner.json
② 比选 plan_segments.py 用数据挑分群方案 + 群规模整数化 + 跨国构成反推
③ 取数 extract_persona.py 六模块固定人群卡
④ 出片 build_deck.py PPTX(封面 / 框架 / 规模 / 人群卡 xN / 优先级)
⑤ QC validate_deck.py 14 条一致性规则,FAIL 即回炉
run_pipeline.py 上面五步一键串起来
出片版式(v2,13.33×7.5in 宽屏)
| 页 | 内容 | 关键设计 |
|---|---|---|
| 1 | 封面 | 深色底 0B1B2E + 底部每群主色色带,后面所有页的颜色都从这里取 |
| 2 | How to read a persona card | 六个固定模块的读法,跨群可比 |
| 3 | How the segments are defined | 左 THE SPLIT(分群维度)+ 右 SEGMENT SIZE(各群规模条,条色 = 该群主色)+ 小样本警示 |
| 4..N+3 | 人群卡 xN | 顶部 tint 头带 + accent 竖条 + 占比 chip deep + 六模块 3×2 卡片 + KEY ACTION |
| N+4 | Targeting Priority Summary | 深色双栏:左 SEGMENT PRIORITY RANKING / 右 WHAT WE DO ABOUT IT |
配色只需给 1 个 accent,其余四色自动派生(与 client deck 逐色吻合):
deep = accent × 0.75 # 深色文字、chip、编号底
tint = 白 + accent × 0.25 # 页面顶部头带
head = 白 + accent × 0.20 # 模块头背景
band = 白 + accent × 0.15 # Positioning 条
主色清单写在 assets/theme.json 的 accents,按人群顺序分配;单群可用 segment.accent 或 segment.palette 覆盖。
封面色带、metrics 页规模条、人群卡头带、summary 编号块四处同源,所以视觉上天然一一对应。
环境准备(第一次用必须先做)
pip3 install python-pptx # 必需:生成 / 读取 PPTX
pip3 install openpyxl # 可选:仅当输入是 .xlsx 时才需要
python3 scripts/smoke_test.py # 期望 ALL PASS
没装 python-pptx 时,build_deck.py 会直接退出并提示安装命令;
validate_deck.py 会把"deck 读不出来"记成 R00 FAIL(而不是崩溃),
其余不依赖 deck 的规则照常输出——避免 QC 静默失效。
smoke_test.py 的 D 项(打包)依赖本机的 skill-creator 插件,找不到会显示
[SKIP],不影响 A/B/C 的结论。
本机(macOS / WorkBuddy 托管环境)实测可用的确切命令:系统
python3没装 python-pptx,且不建议往系统环境里装包,一律用托管 venv 的解释器:PY=/Users/zhaoyijin/.workbuddy/binaries/python/envs/default/bin/python3 "$PY" -m pip install python-pptx openpyxl # 已装:python-pptx 1.0.2 / openpyxl 3.1.5 "$PY" scripts/smoke_test.py # 实测 ALL PASS "$PY" scripts/run_pipeline.py --config assets/testdata/demo_config.json --out /tmp/seg_out用系统
python3直跑会在build_deck.py处报缺依赖;validate_deck.py虽不崩, 但会把"deck 读不出来"记成 R00 FAIL,deck 相关规则全部跳过。
关键原则(必须遵守,否则 deck 会被问倒)
1. 每群一张卡,模块题目必须跨群固定
一张卡片六个模块,所有人群页面的模块标题完全一致,否则无法横向对比:
| 模块 | 回答的问题 | 常用题 | |---|---|---| | WHO THEY ARE | 这群人是谁(人口/肤质/人生阶段) | 画像题 | | CATEGORY IMPORTANCE | 品类在他们生活里为什么重要 | 品类态度题 | | UNMET NEEDS | 现有产品还没解决的痛点 | 需求缺口题 | | USAGE OCCASION | 什么场景下用 | 使用场合题(固定维度)| | ATTITUDE & LIFESTYLE | 价值观与生活方式 | AIO 题 | | NEW OPPORTUNITY | Top 3 机会 + 显著高于其他群的机会 | 新品意愿题 |
2. 数字取整:整数 + 合计必须是 100
- 对外材料不出现小数百分比(
15.2%→15%),用ROUND_HALF_UP,别用 Python 默认的银行家舍入(79.5会变成79,客户对着 Excel 一算就说你错)。 - 一组占比必须合计 100%,四舍五入会飘(
41.6/14.9/34.7/8.9→42/15/35/9 = 101%),用最大余数法修正。脚本已内置round_to_100()。
3. 显著性只留结论,不露标注
客户层面只写"该人群显著高于其他人群的特征 + 百分比",不要 (sig > B)。
validate_deck.py 的 R07 会扫描并拦下。
4. 角色标签与优先级排名不能互相打架
这是最容易翻车的地方。每群给一个角色标签(它对这个生意的角色)和一个优先级顺位(下一份资源投给谁),两者的隐含顺位必须一致:
| 标签 | 隐含顺位 | 含义 | |---|---|---| | SCALE BASE / VOLUME FRANCHISE / CORE TARGET | #1 | 量最大,守基本盘 | | GROWTH ENGINE | #2 | 最有增长空间,投新品/新claim | | SPECIALIST CONVERTER | #3 | 低卷入但可被单一场景转化 | | HIGH-VALUE NICHE | #4 | 小而精,控投入挖洞察 |
反面教材:写着 CORE TARGET 却在 summary 排 #2 → QC 直接 FAIL(R03)。
如果排名第一的不是最大人群,必须在 summary 页写明理由(R04 会 WARN 提醒)。
5. 小样本必须带 caveat
base < 100(默认阈值,可调)的人群页要显式写"方向性参考、投放前需验证"(R05)。
快速开始
# 一键跑通(用自带脱敏演示数据)
python3 scripts/run_pipeline.py \
--config assets/testdata/demo_config.json \
--out /tmp/seg_out --skip-deck # 去掉 --skip-deck 会同时出 PPTX
# 只想复核已有 deck
python3 scripts/validate_deck.py \
--persona /tmp/seg_out/persona.json \
--deck /tmp/seg_out/deck.pptx \
--banner /tmp/seg_out/banner.json
产物:banner.json(规范化数据)、plan.json(方案比选+人群规模)、persona.json(人群卡内容)、deck.pptx、qc.json(QC 明细)。
输入数据怎么给
方式 A — 长表 CSV / Excel(最干净,一行一个"选项 × 人群"):
| table_id | question | scheme | group_code | group_name | base | item | value | sig | |---|---|---|---|---|---|---|---|---| | Q27 | Usage occasion | need_gender | A | Oily-skin Female | 456 | spot first appears | 62.1 | BCD |
方式 B — 已是标准 banner JSON:--format json。
方式 C — Persig 的 tables.pkl:--format persig,多套 banner 并排时用
--persig-schemes "0-3=skintype_gender,4-7=skintype_age" 一次切开。
方式 D — Persig 导出的 Excel(客户最常给的形态):--format persig-xlsx,
用 --persig-groups "Traveler,Domestic" 挑出要用的那套分组。
方式 E — 逐受访者原始明细(raw data):当手头只有问卷原始数据(每行一个受访者、每列一个变量)时,
设置 raw_format: "raw" 并在配置中提供 schemes(分群变量与分组定义)和 codebook(选项文本映射)。
脚本会自动执行交叉分析生成 banner.json,后续步骤与 A-D 完全一致。
{
"raw": "/path/to/raw_data.xlsx",
"raw_format": "raw",
"schemes": [{
"code": "skintype_gender",
"vars": [
{"col": "q34", "label": "Skin type",
"groups": {"Oily":["Oily skin"], "Non-oily":["Combination","Dry","Sensitive","Normal"]}},
{"col": "q35", "label": "Gender",
"groups": {"Female":["Female"], "Male":["Male"]}}
]
}],
"codebook": "/path/to/codebook.json"
}
raw 路线注意事项:
- 原始数据不含选项文本,需提供
codebook.json(按题号 → 选项值→文本映射); 可先用parse_raw.py --input ... --config ... --out banner.json --dump-codebook cb.json自动生成初始映射再人工校对。modules中每个模块建议加"question": "q27."这样的题号前缀, 用于按题号定位表(比脆弱的 table id 更稳定)。- 百分比为未加权原始百分比,与 Persig 加权结果可能存在 <2pp 的正常差异。
四种/五种方式都归一到同一份 banner JSON,后续步骤完全一样。
真实数据一定是脏的(已实测,务必先看这节)
在 236 张真实 Persig 表 + 908 个 Persig Excel 表块上验证,下面四条是常态,脚本已内置处理:
- 同一张表里混着三种口径。
0.1382(小数)、20.27(百分数)、101(频数)会出现在同一列。 整表一刀切会全表错位——实测整表判断吻合率 26.7%,逐单元格 80.6%。%号是唯一可靠信号;没%且数值 ≤1 才 ×100。 - 选项里混着统计行。
Total mentions/Average No. of Mentions/Proportions/Means: Columns Tested/____分隔线 /SF0002990406表编号, 一个 908 表块的导出里剔掉了 1700 行。 (NET)汇总行会霸占 Top N。NET 是子项之和,必然最高,四个群的Functional Benefit(NET)都在 90% 左右——选它等于没说。默认剔除。- 量表题的 Top N 是刻度。
Q38 [SA-Grid]这类题跑top_own会得到4 43% · Strongly agree 34% · 3 20%。默认剔除,量表题请改用 T2B 汇总行。
另外两个坑:
- Persig 导出里混着"表头行"(列是标题文字或汇总值,不是分组)。以出现次数最多的 列结构为基准自动剔除(实测 236 张里剔掉 6 张)。
- 同一维度在不同表块可能落在不同列(
age在 A/B 列、也可能在 C/D 列)。 分组 code 用分组名而不是列字母,否则一个 2 群维度会被拆成 4 个群。
config 的 segments 块:键必须用分组 code
segments 是整份配置里唯一必须手写的部分(其他都能从数据算出来)。字段:
| 字段 | 必填 | 作用 |
|---|---|---|
| display | 否 | 卡片标题的人群名;不写就用数据里的原始分组名 |
| role_label | 是 | 角色标签,见「关键原则 4」,R03 校验的就是它和排名的匹配 |
| priority_rank | 是 | 1..N 且不重复,R04 / R12 校验 |
| headline | 否 | 卡片顶部一句话结论 |
| key_action | 否 | 卡片底部行动建议 |
| caveat | 否 | 默认 true;设 false 会关掉小样本警示,进而触发 R05 FAIL |
| share | 否 | 覆盖算出来的占比,一般不要动 |
⚠️ 最容易犯的错:拿人群名当键写。
"segments": { "Oily × Female": {"role_label": "..."} } // ✗ 脚本按 code 查,一个都匹配不上 "segments": { "A": {"display": "Oily × Female", ...} } // ✓写错不会崩,但四群的
role_label/priority_rank/headline/key_action全落空 —— 页面变成"无标签、无排名",R12 报「有 N 个人群没取到 priority_rank」。 看到这句提示,第一件事就是查键名。分组 code 从哪来:跑完
parse_*.py后看banner.json的groups[].code(A/B/C/D…), 或一行打印出来:python3 -c "import json;d=json.load(open('/tmp/seg_out/banner.json'));\ print([(g['code'],g['name']) for g in d['groups']])"
数据要满足什么才能做分群比选
plan_segments.py 会同时评估多个分群方案(比如 need_gender 4 群 vs age 3 群),按下面顺序排序:
- 有显著差异的题数(区分力,越多越好)
- 显著单元格占比
- 最小群样本量是否达标
- 组数少者优先(好落地)
经验值:如果两套方案的样本量差不多,就选显著差异题数更多的那套;如果其中一套最小群 < 100,直接把结论降到"方向性"。
一致性问题排查(QC 规则表)
| 规则 | 检查项 | 级别 | |---|---|---| | R01 | 各人群占比合计 = 100 | FAIL | | R02 | 各人群样本量合计 = 总样本 | FAIL | | R03 | 角色标签隐含顺位 = 实际优先级排名 | FAIL | | R04 | 排名 #1 ≠ 最大人群时需说明理由 | WARN | | R05 | base < 阈值的人群页带 caveat 措辞 | FAIL | | R06 | 不出现小数百分比(>1 位小数) | FAIL | | R07 | 不出现 sig / 显著性标注 | FAIL | | R08 | 各人群页模块标题集合一致 | FAIL | | R09 | deck 数字能回溯到 persona / 源表 | WARN | | R10 | 单页出现 ≥2 个互斥最高级 | WARN | | R11 | 角色标签不重复 | FAIL | | R12 | 优先级排名 1..N 连续 != summary 顺序一致 | FAIL | | R13 | 非最大人群却用 largest/biggest | FAIL | | R14 | persona 证据数字 = 源表数字 | FAIL |
交付前自检清单
- [ ]
smoke_test.py全 PASS(环境正常) - [ ]
validate_deck.py0 FAIL;WARN 逐条人工确认 - [ ] 每张人群卡六个模块都有数,且数字与源表一致
- [ ] summary 页的排名顺序与顶层建议(投谁、守谁)逻辑自洽
- [ ] 存在 n<100 的人群时,措辞已降级为"方向性"
文件说明
segmentation-deck/
├── SKILL.md
├── scripts/
│ ├── parse_banner.py 多来源 -> banner.json
│ ├── parse_raw.py 逐受访者原始明细 -> banner.json(raw 路线)
│ ├── plan_segments.py 方案比选 / 整数化 / 跨国构成反推
│ ├── extract_persona.py 六模块人群卡取数
│ ├── build_deck.py PPTX 生成
│ ├── validate_deck.py 14 条一致性 QC
│ ├── run_pipeline.py 一键串流程
│ ├── make_demo_data.py 生成脱敏演示数据
│ └── smoke_test.py 自检
├── references/
│ ├── module-library.md 六模块取数口径与常见题号映射
│ ├── label-taxonomy.md 角色标签词典(含顺位约定)
│ └── qc-checklist.md QC 逐条解释与修复动作
└── assets/
├── theme.json 配色
└── testdata/ 脱敏演示数据 + 干净/埋错两套配置
├── demo_banner.csv 长表演示数据
├── demo_config.json 干净配置(smoke_test A / 快速开始)
└── demo_config_broken.json 埋错配置(smoke_test B)
testdata 是运行依赖,打包时不要排除。 SKILL.md 的「快速开始」和
smoke_test.py的 A/B 两项都要读这三个文件,缺了会直接报错、自检跑不出来。 若demo_banner.csv不慎丢失,可一键重建:python3 scripts/make_demo_data.py --out assets/testdata/demo_banner.csv(两份 config 是手写配置、无法自动生成,丢了只能从仓库恢复。)
已在真实项目上验证
| 项目 | 数据 | 结果 |
|---|---|---|
| 某日系药妆品牌 · 祛痘膏(8 市场 n=809) | Persig tables.pkl,236 表 × 8 列 | 复现 38/9/42/11 = 100%,合计 n=809;Q27 base 299/73/325/90、Q41 46/50/46/43 与交付版逐项吻合;QC 0 FAIL / 0 WARN |
| 某日系药妆品牌 · 祛痘膏 raw data(8 市场 n=809) | 逐受访者明细 xlsx(809×4441 列),raw 路线 | 复现 base 304/74/339/92 = 100%(与 Persig 加权差异 <2pp);codebook 覆盖六模块 98 条选项文本;QC 0 FAIL / 0 WARN;R14 证据数字与源表一致 |
| 某日系药妆品牌 · 足部贴(5 市场 n=508) | Persig Excel,908 表块 × 28 列 | 复现 Traveler 52% / Domestic 48%;方案比选 travel_type(207 题显著) > work_status(170) > age(165) > gender(43),与业务判断一致;QC 0 FAIL / 0 WARN |
上面两个项目的 v2 版式均已跑通并渲染检查:祛痘项目 8 页(封面 4 群色带 / 框架 / 规模 / 4 张人群卡 / 优先级)、足部贴 6 页(2 群), 所有页面无文字溢出卡片、无底部裁切,四处主色同源一一对应。
细节规则见 references/;遇到 R0x 报错先查 references/qc-checklist.md。
这套东西做不到什么
脚本替你完成的是计算和一致性检查。下面三件事它给不出答案,只能靠人:
- 用哪个维度切分群 —— 性别×肤质、年龄×使用场景、还是购买频次×忠诚度? 同一份数据能切出十几套方案,哪一套能落进生意动作,取决于品类结构、渠道 和品牌所处阶段,不是卡方值能决定的。
- 人群叫什么名字 —— "油性肌肤女性"是描述,不是洞察。一个能让人记住、 愿意在内部反复引用的标签,要拿捏得住分寸,得看过足够多同类项目。
- 最后一页那句话 —— "所以我们下一步该做什么"。这是客户真正付钱买的东西, 也是脚本唯一不会碰的部分。
如果你正好卡在这三件事上,或者手上是多市场、多波次、需要横向比选的复杂项目:
MREJ — 市场调研分群与人群策略 承接:分群方案设计 / deck 交付 / 批量项目 / 团队方法培训 联系:https://73ea90f4292d4f75a9c6f516a84c98af.app.workbuddy.host (微信 EjayyyyyyZ / yj_zhao95@163.com)
免费版和付费服务的差别不在功能,在上面这三件事。
Scan to join WeChat group