spss-cross-analysis
SPSS 问卷交叉分析报表生成
核心脚本
scripts/spss_cross_analyzer.py — 完整的 SPSS 交叉分析引擎。
依赖
pandas, openpyxl, scipy, numpy
可选:pyreadstat(性能更好)。无 pyreadstat 时自动使用内置纯 Python SAV 读取器。
安装依赖:
pip install pandas openpyxl scipy numpy
# 可选
pip install pyreadstat
工作流程
1. 加载数据
import sys
sys.path.insert(0, "<skill_dir>/scripts")
from spss_cross_analyzer import SPSSCrossAnalyzer
analyzer = SPSSCrossAnalyzer()
analyzer.load_spss_file("/path/to/file.sav")
加载后默认去掉前两列(通常为编号/ID),可在脚本中调整。
2. 分析题型(可选但推荐)
results = analyzer.analyze_question_types()
# 或导出 Markdown 分析报告
analyzer.export_analysis_report_markdown("/path/to/report.md")
输出每道题的类型(单选/多选/矩阵/打分/填空/三维矩阵多选)、取值范围、子字段等。 将此报告展示给用户,帮助用户决定列字段和行字段的选择。
3. 选择列字段和行字段(交互决策点)
原始脚本使用 input() 交互,在 Skill 模式下需由 Agent 代替用户做出或引导选择。
列字段选择(交叉表的表头):
# 方式A:自动检测 banner 字段
banner_fields = [col for col in analyzer.df.columns if 'banner' in col.lower()]
if banner_fields:
analyzer.column_fields = banner_fields
analyzer._is_banner_column = True
analyzer.column_question_mapping = analyzer._build_banner_question_mapping(banner_fields)
analyzer._build_banner_display_info(analyzer._get_banner_groups_ordered())
# 方式B:手动指定字段列表
analyzer.column_fields = ["S1", "S2", "S3"]
analyzer._is_banner_column = False
analyzer.column_question_mapping = analyzer._build_question_mapping(analyzer.column_fields)
行字段选择(交叉的维度,交互决策点):
先列出所有可选行字段(排除列字段后),展示给用户,让用户选择排除哪些。
# 第一步:列出候选行字段(排除列字段)
col_set = set(analyzer.column_fields)
candidate_fields = [c for c in analyzer.df.columns if c not in col_set]
# 第二步:自动识别填空题和元数据字段
open_fields = [c for c in candidate_fields if analyzer._is_open_ended_field(c)]
# 识别元数据字段(取值极多的非问卷字段)
meta_fields = []
for c in candidate_fields:
if c in set(open_fields):
continue
nunique = analyzer.df[c].nunique()
dtype = str(analyzer.df[c].dtype)
if dtype == 'object' or nunique > 200:
meta_fields.append(c)
交互流程(必须严格按此步骤向用户展示和提问):
第一步:展示自动识别的排除列表,让用户确认。
向用户展示以下信息:
📝 自动识别的填空题/开放题(将排除):
- S8_其他 [S8_xxx_其他]
- A1_输入文本 [A1_xxx_输入文本]
...
⚠️ 建议额外排除的元数据字段:
- 答题时长 [取值1749种]
- 用户ID [取值263种]
...
以上共 XX 个字段将被排除。
然后提问(用 AskUserQuestion):
- 选项1: "同意排除"(确认使用上述排除列表)
- 选项2: "我来调整"(用户要手动增减排除字段)
第二步:如果用户选"我来调整",展示输入规范并等待用户输入。
向用户发送以下说明:
请输入要调整的排除字段,格式说明:
- 添加排除:直接输入字段名或前缀,如 BC_S2, D4, Q89
- 移除排除(保留该字段):在前面加 !,如 !答题时长, !D4_L2
- 多个用逗号分隔
- 前缀匹配:输入 BC 会排除所有 BC 开头的字段(BC_1, BC_2, ...)
- 留空或输入"确认"表示不做调整
示例:"BC_S2, Q89, !答题时长" 表示额外排除 BC_S2 和 Q89,但保留答题时长
等待用户输入后解析:
# 解析用户输入
user_input = "BC_S2, Q89, !答题时长" # 示例
extra_exclude = set()
remove_exclude = set()
for item in user_input.split(','):
item = item.strip()
if not item or item == '确认':
continue
if item.startswith('!'):
remove_exclude.add(item[1:].strip())
else:
extra_exclude.add(item.strip())
# 构建最终排除集
exclude_set = set(open_fields) | set(meta_fields)
# 移除用户指定保留的
for rm in remove_exclude:
exclude_set.discard(rm)
# 前缀移除
exclude_set = {f for f in exclude_set if not f.startswith(rm + '_') and f != rm}
# 添加用户指定排除的(支持前缀展开)
for prefix in extra_exclude:
for c in candidate_fields:
if c.startswith(prefix + '_') or c == prefix:
exclude_set.add(c)
row_fields = [c for c in candidate_fields if c not in exclude_set]
analyzer.row_fields = row_fields
analyzer.row_question_mapping = analyzer._build_question_mapping(row_fields)
关键:必须向用户展示将被排除的字段列表,让用户确认或补充。不要静默排除。 用户选"我来调整"时,必须先展示输入格式说明,等用户输入后再继续。
识别题型和配置矩阵模式:
analyzer.field_types, analyzer.matrix_groups, analyzer.multi_groups = \
analyzer._identify_field_types(analyzer.row_fields)
# 矩阵题处理模式:'independent'(每个子题单独表) 或 'merged'(合并为一张表)
analyzer.matrix_mode = 'independent' # 或 'merged'
三维矩阵多选题处理模式(交互决策点——逐题选择):
三维矩阵多选题(如 D8、Q4-1 等)有三种拆分模式。必须逐道题向用户展示结构并让用户选择。
交互流程:
- 先打印
len(analyzer.tri_matrix_groups)和所有 key(注意 key 可能带尾部空格,如'Q12 ',展示时用.strip()),确认总数。 - 遍历
analyzer.tri_matrix_groups的每一个 key,对每道题:- 展示该题的字段结构、中间维度、最后维度的含义
- 让用户选择该题的拆分模式
- 校验:展示的题目数量必须等于
len(analyzer.tri_matrix_groups)。如有遗漏,停下来补全。 - 分别设置每道题的模式(设置
tri_matrix_mode时使用原始 key(含空格),不要 strip)
示例展示格式:
🔷 三维矩阵题 D8(15个子字段,5×3 结构):
标签:最近一年,您在各平台使用客服的方式
中间维度(5组):携程 / 去哪儿 / 飞猪 / 美团 / 同程
最后维度(3项):在线客服 / 电话客服 / 两者都用
请选择此题的拆分方式:
1. 按最后段拆分 → 生成 3 张子表(在线客服、电话客服、两者都用)
2. 按中间段拆分 → 生成 5 张子表(携程、去哪儿、飞猪、美团、同程)
3. 多选模式 → 不拆分,当作普通多选题处理
对每道题用 AskUserQuestion 提问,或在有多道题时先一次性展示所有题目的结构, 然后让用户逐个或批量指定(如"D8,D8-1,D8-2 按中间段;Q4-1,Q11-1 按最后段")。
# 逐题设置
analyzer.tri_matrix_mode = {} # 题号 -> 模式
# 示例:用户逐题指定的结果
tri_matrix_choices = {
'D8': 'split_by_mid', # 按中间段(平台)拆分
'D8-1': 'split_by_mid',
'D8-2': 'split_by_mid',
'Q4-1': 'split_by_last', # 按最后段拆分
'Q11-1': 'split_by_last',
}
for question, mode in tri_matrix_choices.items():
analyzer.tri_matrix_mode[question] = mode
# 可选: 'split_by_last' / 'split_by_mid' / 'multi'
analyzer._build_row_field_groups()
关键规则:
- 不能对所有三维矩阵题统一使用同一种拆分模式。每道题的维度含义不同,必须分别展示并让用户选择。
- 必须展示
tri_matrix_groups中的全部题目,不得遗漏。展示后核对数量 =len(analyzer.tri_matrix_groups)。 tri_matrix_groups的 key 可能包含尾部空格(如'Q12 '、'Q20 '),设置tri_matrix_mode时必须使用原始 key,不要 strip。展示给用户时可以 strip。
4. 生成交叉表
analyzer.generate_cross_tables()
5. 配置百分比格式(交互决策点)
# 是否保留百分号
analyzer._keep_percent_sign = True # True=显示%, False=纯数字
# 小数位数
analyzer._decimal_places = 0 # 0=整数, 1-4=保留小数
6. Grid Summary(交互决策点)
将多道同类题的关键汇总指标压缩到一张交叉表中。必须向用户主动提问是否需要。
交互流程:
- 向用户解释 Grid Summary 功能:将多道同类题(如 B9-B14 品牌选择题)的汇总压缩到一张表。
- 提问:"是否需要生成 Grid Summary?如果需要,请输入题目编号前缀(用逗号分隔多组,如 B9,B10 或 D8-1)。留空则跳过。"
- 如果用户输入了题号,按以下代码匹配字段并配置:
# 用户输入的 Grid Summary 题号组
grid_groups = ["B9", "B10"] # 示例,由用户提供
all_columns = list(analyzer.df.columns)
for group_prefix in grid_groups:
matched = analyzer._match_grid_summary_fields(group_prefix, all_columns)
if not matched:
print(f"⚠ '{group_prefix}' 未匹配到任何字段,跳过")
continue
config = analyzer._analyze_grid_summary_structure(group_prefix, matched)
if config:
analyzer.grid_summary_configs.append(config)
if analyzer.grid_summary_configs:
analyzer._generate_grid_summary_data()
analyzer._compute_grid_summary_insert_positions()
- 如果用户说"不需要"或留空,跳过此步。
7. 导出 Excel
analyzer.create_header_config()
output_path = "/path/to/output.xlsx"
result = analyzer.generate_excel_with_complex_header(output_path, config_path=None)
# config_path 可传入自定义表头配置 JSON
Banner 模式下会自动生成 3 个文件(频数/百分比/显著性),每个 banner 组一个 sheet。
8. 导出表头配置(可选)
analyzer.export_header_config("/path/to/config.json")
# 用户可修改 custom_name、custom_question_name 后通过 --config 参数重新生成
交互决策点汇总
以下节点需要向用户确认或根据数据自动判断:
| 步骤 | 决策 | 默认建议 |
|---|---|---|
| 列字段 | 使用 banner 还是手动选择 | 有 banner 字段时自动使用 |
| 行字段排除 | 展示自动排除列表 → 用户确认或调整(展示输入格式说明) | 排除填空题+元数据 |
| 矩阵题 | 独立模式还是合并模式 | 独立模式 |
| 三维矩阵 | 逐题展示维度结构,让用户选拆分模式 | 按最后段拆分 |
| 三维矩阵附加行 | 是否有选项需要作为附加行 | 无附加行 |
| 百分比格式 | 百分号+小数位数 | 保留百分号,不保留小数 |
| Grid Summary | 主动提问是否需要,需要则输入题号前缀 | 不启用 |
处理策略:
- 先运行
analyze_question_types()生成分析报告给用户看。 - 逐步向用户提问上述决策点(每个都必须问,不能跳过或静默使用默认值)。
- 特别注意:
- 行字段排除:必须列出将被排除的填空题,问用户是否还需额外排除其他字段。
- Grid Summary:必须主动问用户是否需要,不能默认跳过不问。
- 如果用户明确说"默认"或"自动",才使用上表的默认建议。
- 设置完毕后一次性执行生成流程。
输出文件
- Excel 交叉表:频数表(ABS)、百分比表(%)、显著性表(%sig)
- Banner 模式:3个 xlsx 文件(_频数/_百分比/_显著性),每个文件包含多个 banner sheet
- 非 Banner 模式:1个 xlsx 文件,3个 sheet
- 表头配置 JSON:可修改自定义名称后重新生成
- Markdown 分析报告:题目类型、数据质量、命名模式等
显著性检验
- 方法:Z检验 (两比例检验) + Bessel校正
- 多重比较校正:Benjamini-Hochberg FDR (α=0.05)
- Banner 模式下按子维度分组进行跨列显著性检验
- 显著的单元格标注字母标记(如 "56%ab"),黄色高亮
注意事项
- 前两列默认被去掉(通常是编号/受访者ID),如需保留需修改脚本
- 填空题/开放题自动排除出行字段
- Banner 列字段不做行交叉(避免自交叉)
- 非 Banner 模式下列字段会自动加入行字段(表头题也做行交叉)
- 打分题自动补全缺失分值(如 0-10 量表中数据缺少某些值也会显示完整行)