问卷统计(交互式,只做统计)
分工:清洗(去重 / 人口学关联或完整性 / 专注度 / 10 条异常规则 / 自定义剔除)和加权(IPF)都在 survey-weight skill 里完成;本 skill 只拿它的产出做统计。
- 用户给的是原始问卷(未清洗、无 weight)→ 告诉用户先用 survey-weight 清洗加权;用户明确说"不加权、直接统计"时,可直接对该表统计(不做清洗,汇报里注明"未清洗")。
- 用户给的是 survey-weight 的 加权后数据.xlsx 或清洗目录下的 问卷数据表_merged.xlsx → 直接进入本流程。
每个阶段:先展示信息 → AskUserQuestion 确认 → 执行 → 汇报。不要一口气跑完。
⚠️ 交互铁律:选项必须自解释
用户界面只显示 question 和 label,description 看不到:
- label 必须写出具体内容,不能用"全部维度""默认"这类代称;放不下就写进 question。
- 涉及题号的选项写"题号+题干简称",如 43 税前月收入。
- "自定义/自己输入"类选项在 question 里写明输入格式和例子,并按用户在输入框写的原文执行。
运行环境
PY=/Users/susanlao/.workbuddy/binaries/python/envs/default/bin/python # 已装 pandas/openpyxl/numpy
ST=~/.workbuddy/skills/survey-stats/scripts/stats_pipeline.py
每个子命令最后一行输出 @@RESULT@@ {json},只解析这一行:> log 2>&1; grep -a "@@RESULT@@" log。统计耗时 1~3 分钟,timeout 600000。
流程
阶段 0:准备
标准输入:survey-weight 的产物 <…>/加权输出_YYYYMMDD/加权/加权后数据.xlsx(单 sheet Sheet1;已清洗、带 weight 列和 性别/年龄/年龄分段/学历/职业/城市分级/操作系统/设备型号 等人口学列,题目列为 N.题干)。也接受同目录的 加权后数据.csv、清洗目录下的 问卷数据表_merged.xlsx,或任意含题号列的 xlsx/csv;文件名、sheet 名、是否有 weight 列都不限。人口学列也不是必需,缺失时按阶段 1 的「没有人口信息」分支走,照样出整体统计。
工作目录:输入在 加权输出_*/加权/ 下时,默认建在同一个 加权输出_* 里,与 清洗/、加权/ 并列:加权输出_YYYYMMDD/统计/(已存在则加 _HHMM);其它情况建在输入文件同级 统计输出_YYYYMMDD/。不改原文件。
$PY $ST prepare --dir <工作目录> --file <加权后数据.xlsx> [--sheet <sheet名>]
复制为 统计用数据.xlsx(统计的工作副本,内容同时写进统计表的「原始数据」页),并去掉非题目元数据列(语言/IP/UA/地理位置*/人口学是否匹配/OpenID是否重复/出生年份/学历_加权 等辅助列——否则会被当成题目统计)。返回 dropped_meta_cols、weight_cols。
阶段 1:体检
$PY $ST inspect --dir <工作目录>
用表格汇报:行数、权重列(weight_cols:范围、均值、合计、有效样本量 effective_n)、可用人口学维度(dimension_cols_present,逐个列出并附 dimension_col_map 里对应的实际列名)、年龄分段(age_groups,从数据的 年龄分段 列读出,与清洗/加权时一致)、题目清单(题号+题干)。
dimension_cols_present 与 stats 阶段用的是同一套识别逻辑,带题号的人口题也会算进来(如 43.您的性别 → 性别,见 dimension_cols_from_questions)。汇报时把「维度 → 实际列名」一并列出,请用户核对,避免把题干含「性别/年龄/学历/城市/职业」等关键词的普通题误当维度。
attention_col_present 为 true 只是说明专注度题还在表里(会作为普通题统计),不做剔除。
没有人口信息也能跑:has_demographics=false(dimension_cols_present 为空)不是错误,流程照常,输出仍有 封面 / 数据统计 / 原始数据 三页,只是「数据统计」页只有整体人群列、没有按人群拆分的交叉列,封面样本结构为空。加权(有 weight 列时)同样可用。汇报里明确写一句「该数据无人口学信息,只出整体统计,未做人群交叉」。
阶段 2:确认统计配置(一次 AskUserQuestion,最多 4 问;无人口维度时 3 问)
| # | question | labels |
|---|---|---|
| 1 | 数据带权重列「<col>」:<n> 人有值,范围 <min>~<max>,有效样本量约 <effective_n>/<n>。统计是否按这列加权?(仅当 weight_cols 非空时问;无权重列则不问,直接不加权) |
用「<col>」出加权统计表 / 不加权(只给这两个) |
| 2 | 调研名称?(印在封面,输出文件名为「<名称>结果_YYYYMMDD.xlsx」;可在下方输入框自写) |
<根据题目内容拟的名称> / 腾讯调研(默认名) |
3(仅当 has_demographics=true 时问) |
人口学交叉维度?当前数据可用:<逐个列出维度名与实际列名> |
<全部可用维度用顿号列出>(推荐) / <去掉操作系统、设备型号后的清单> / 自选(在下方写维度名) |
| 4 | 是否把某些题目也作为交叉维度(每题与所有其他题交叉)?要指定请在下方输入框写题号,如:1,43 或 30-33 |
不需要 / <推荐题号+题干简称>,如「43 税前月收入」 / 自己输入题号(在下方输入框写) |
has_demographics=false时跳过第 3 问(最多只问 3 问),stats不传--dimensions;这一问不出现空选项,也不问用户「要不要补人口信息」。若用户仍想要分组对比,在第 4 问用题号做题目间交叉作为替代,并在汇报里说明「分组依据是题目而非人口学维度」。- 可选维度全集:职业, 收入, 性别, 年龄分组, 学历, 城市等级, 婚姻状况, 操作系统, 设备型号。只列
dimension_cols_present里有的;缺收入/婚姻时说明原因,有收入题时建议用题号交叉。 - 题号输入:取用户原文,先
$PY $ST check --dir <工作目录> --cross "<原文>"校验(支持1,43、1,43、1、43、1.43/1。43(按两个题号处理,并在正文告知用户这样理解)、30-33、30~33),用表格列出「题号 | 题干」;cross_missing非空要告知。all= 全部题,很慢需提醒。 - 多个权重列时,问题 1 的第一个选项按列名逐个列出。
阶段 3:执行统计
$PY $ST stats --dir <工作目录> --survey-name "<名称>" [--dimensions "职业,性别,年龄分组,学历,城市等级"] \
[--cross 1,43] [--weight --weight-col weight]
- 加权:stats 默认把权重等比缩放到均值 1(
weight_info.rescaled_to),加权总数 = 样本数,占比不变;--no-weight-rescale按原值。缺失或 ≤0 的权重按 0 计,missing_or_nonpositive> 0 时要告知人数。 - 年龄分段默认用数据里的
年龄分段;用户要换分段时用--age-groups "18岁以下,18-24岁,…"(数据里有数值年龄列才能重算)。
阶段 4:交付
present_files:只交付统计结果 Excel。统计用数据.xlsx 是工作副本(内容已在统计表「原始数据」页),留在目录里但不交付、不在回复里列出。
最终回复:输入文件、样本数、是否加权(列名、有效样本量;effective_n < n 的 20% 时一句话提醒加权结果波动大)、交叉维度与交叉题号、输出文件路径。统计表里加权时"回答人数"是加权数(可能带小数)。
附录:怎么向用户解释「有效样本量」(ESS)
用户常问"有效样本量约 X 人是什么意思"。ESS = (Σw)² / Σw²(Kish 公式),回答时按下面四层讲,先算好实际数字再开口:
- 一句话定义:加权后这份数据实际相当于多少个"一人一票"的样本。权重全相等时 ESS = n;权重越悬殊 ESS 越低。
- 根因:列出各加权维度每档的「人数占比 → 加权占比 → 放大倍数」,指出放大倍数最大的档位(通常就是 ESS 掉下去的原因,如"初中及以下 20 人要占 59.6%,放大 24 倍")。顺带给出权重集中度:权重最大的 1/5/20/100 人各占总权重百分之多少。
- 后果:
deff = n / ESS,误差放大sqrt(deff)倍。给出同一估计值(取 50%)下不加权与加权的 95% 置信区间半宽对比(如 ±3.5pp → ±17.9pp)。 - 建议(ESS < n 的 20% 时必须给,不改流程,由用户决定):换更贴近样本的目标(网民口径 CNNIC 而非七普全人口);只保留放大倍数接近 1 的维度(如只按性别加权,ESS 通常回到 n 的 99% 以上);对权重做上限截断(trimming);或直接不加权、在报告里注明样本结构。
关键规则 / 坑
- 统计核心
scripts/run_merged_data_cli.py(+parallel_cross_analysis.py;加权时自动走单线程)。merge_tables.py/user_agent_parser.py仅作为其依赖保留(自定义筛选规则解析),不要用来清洗。 - 封面素材(横版_中英文_彩/白.png、tencent_logo.png 等)在 scripts/ 下,
run_merged_data_cli.py从脚本目录读取,不要移走。 - 默认注册主库已于 2026-10-15 移到 survey-weight 的
assets/(清洗归 survey-weight)。本 skill 与 survey-weight 之间没有路径依赖,可以独立改名或移动。assets/标准版本-城市分级2025.xlsx是遗留文件,统计不用。 - 识别为维度的列名:性别 / 年龄分段(或年龄分组)/ 学历 / 职业 / 城市分级(或城市等级)/ 操作系统 / 设备型号 / 婚姻状况 / 月收入;带题号的人口题(如
43.您的性别)会被find_dimension_columns第四轮识别为维度,且该题本身不再单独出频数(表里留一行全 0)。inspect自 2.3.0 起复用同一套识别逻辑,体检结果与统计阶段一致。 prepare的KEEP_META白名单:不带题号且不在白名单的列会被直接丢弃(实测年龄段/城市级别/device都被 drop)。看到dropped_meta_cols里有人口列时要主动告知用户,建议改成规范列名或加题号前缀后重跑,不要静默丢失。- 误判风险:题干含「性别/年龄/学历/城市/职业」等关键词的普通题目会被当维度,导致该题选项统计全 0。体检时列出
dimension_col_map(维度 → 实际列名)请用户核对;发现误判就让用户改题干列名后重跑。
版本备份
修改前备份到 ~/.workbuddy/skill-backups/survey-stats_<YYYYMMDD_HHMMSS>/。
(2026-10-15 由 survey-full-pipeline 改名为 survey-stats;改名前的备份仍叫 survey-full-pipeline_*,改名前最后一版为 survey-full-pipeline_20261015_180356_final。)
- _20261013_195436:v1.0(注册表关联模式)
- _20261014_100516:v1.1(明细表模式 + 自带 weight 三选)
- _20261014_193429:v1.2(自带 weight 两选:用 weight / 不加权)
- _20261014_194011_v1.3:v1.3(统计表扁平化,已回退不用)
- _20261015_171128:v2.0(输出目录固定为输入同级 统计输出_日期/)。v2.1 明确标准输入为 加权输出_/加权/加权后数据.xlsx,输出放 加权输出_/统计/。
- _20261015_165833:v1.2 最终版(含清洗+加权+统计全流程)。v2.0 起只做统计,清洗/加权移到 survey-weight;如需恢复全流程,从这份备份拷回 scripts/pipeline.py 等。
- survey-stats_20261103_144316:v2.2.1,升 2.3.0 前的完整备份。
- v2.3.0(2026-11-03):inspect 与 stats 阶段共用维度识别,新增 has_demographics、dimension_col_map、dimension_cols_from_questions;SKILL.md 增加「无人口信息」分支(has_demographics=false 时跳过第 3 问)。
已验证
2026-10-15 用 survey-weight 产出的 高校培训/加权输出_性别年龄学历/加权后数据.xlsx(900 行,weight 有效样本 60)跑通 prepare → inspect → check → stats(加权 + 第 1 题交叉)。
2026-10-15 用 高校培训/加权输出_20261015/加权/加权后数据.xlsx(804 行 × 122 列,Sheet1,weight 有效样本 30)跑通 prepare → inspect → stats --weight,无需改脚本。
2026-11-03 用 12 行临时样例验证三种输入:① 完全无人口列(dimension_cols_present: [],不加权/加权各一次,输出 3 页、只有整体人群列);② 带 43.您的性别 人口题(识别为 性别 维度,男/女交叉列正常);③ 人口列列名不规范(年龄段 / 城市级别 / device 被 prepare 丢弃)。改 2.3.0 后重跑 stats 回归通过。