ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python解析EPQ少年版.doc:从题库构建到批量评分的完整方案

Python解析EPQ少年版.doc:从题库构建到批量评分的完整方案 简介这份资源提供艾森克人格问卷少年版的标准DOC版本专为7-15岁儿童及青少年设计适用于教育评估、临床心理筛查和家庭教育指导等场景。问卷包含完整的88道测题与指导语围绕外倾性、神经质、精神质等核心维度展开并延伸覆盖情绪控制、社交能力、冲动控制及行为习惯等评估内容可帮助教师、心理工作者和家长系统了解青少年的个性倾向与行为模式。资源包为单文件压缩包共1个DOC文档大小46KB内容清晰、可直接打印或在线使用。目前已有123人学习适合需要获取标准化青少年人格测量工具的心理学专业学生、研究者及一线教育工作者。1. 艾森克人格问卷少年版这份.doc先把它变成可计算的题库在心理测评系统、学校心理健康档案、医院量表门诊的部署现场艾森克人格问卷少年版.doc 这类老式 Word 文档几乎是标配。它长得很像一份普通的问卷实际上只是一堆没有版式的题号和选项四个维度、几十道二选一题目、一套按性别和年龄区分的常模。直接手工录入不是不行但一次测评几百份答卷逐行誊写带来的错题和漏题往往比想象中严重。这份内容要解决的就是这件事把 doc 解析成结构化题库把计分规则固化成配置把原始分换算成 T 分并生成报告最终把这份文档变成一个能批量处理答卷的量表计算工具。阅读者最好已经上了一两年 Python 班对正则和 JSON 有基本概念其余部分可以照着抄。2. 用 LibreOffice 与正则表达式把 EPQ 少年版的.doc 题面拆成结构化题库2.1 为什么不能直接按字符串读取 .doc.doc 属于 OLE2 复合文档格式本质上是二进制封装下的一套“文件系统”Word 正文被切成碎片存放在 WordDocument 流里再经 CP1252 或 UTF-16 编码转换。用 Python 直接open(EPQ少年版.doc, encodingutf-8)读出来的是一堆乱码拿正则硬抠也抠不出完整句子。真正可靠的落地方案有三种我按维护成本排了一张表方案平台中文支持依赖适合场景LibreOffice headlessLinux / macOS / Windows好LibreOffice 本体批量导入与生产服务器antiword类 Unix一般旧编码会乱antiword 包简单英文文档win32com WordWindows好已安装 MS Office需要保留版式或转 docx我一般优先用 LibreOffice headless无界面、可脚本化、不依赖 Office 授权。转换命令只一行libreoffice --headless --convert-to txt:Text (encoded):UTF8 --outdir ./epq_txt ./EPQ少年版.doc参数里--headless表示不弹界面txt:Text (encoded):UTF8指定导出为 UTF-8 编码的文本文件--outdir指定输出目录。命令行执行后会在./epq_txt下生成同名 txt。这里有个容易踩的坑转换机器的系统语言包里没有中文字体时导出后的中文会变成????在最小化 Docker 镜像里尤其常见。2.2 转换后的正文如何切成“题号 题面”结构转换后的 txt 一般长这样开头是问卷标题后续每行一道题题面可能跨行选项可能是“是 / 否”也可能被排版成“A.是 B.否”。逐行解析比一把梭正则更稳因为可以按行状态维护当前题块import re from pathlib import Path def parse_epq_items(lines): q_re re.compile(r^\s*(\d{1,3})\s*[\.、]\s(.?)\s*$) opt_re re.compile(r^\s*[(]?\s*是\s*[/、]?\s*否\s*[)]?\s*$) items, cur [], None for line in lines: line line.strip() if not line: continue m q_re.match(line) if m: if cur: items.append(cur) cur {id: int(m.group(1)), text: m.group(2), options: []} elif cur is not None: if opt_re.match(line): cur[options].append(是) cur[options].append(否) else: cur[text] line if cur: items.append(cur) return items raw Path(epq_txt/EPQ少年版.txt).read_text(encodingutf-8, errorsreplace) items parse_epq_items(raw.splitlines())q_re用行首锚点匹配题号避免题面里“得分 5 分”这类数字被误判为新题opt_re只匹配独立成行的“是 / 否”防止正文里的“是否”两字被吃进选项。跨行题面在else分支里累加进text最终得到{id:1, text:…, options:[是,否]}的列表直接可以落库或缓存。如果手里的 doc 是“A.是 B.否”排版把opt_re改成匹配 A/B 选项的样式即可后续逻辑不用动。2.3 抽完题先做三件事别急着计分拿到 items 之后先做三件校验题号是否从 1 连续到 N、是否出现重复题号、每道题是否都有完整的两个选项。脚本很短但能救回多数脏数据def validate_items(items, expected88): ids [it[id] for it in items] missing [i for i in range(1, expected 1) if i not in set(ids)] dup sorted({x for x in ids if ids.count(x) 1}) bad_opts [it[id] for it in items if len(it[options]) ! 2] return { count: len(items), missing: missing, duplicates: dup, bad_options: bad_opts, } print(validate_items(items))expected88对应常见中文修订版 EPQ 少年版的题数如果你手里的版本不是 88 题直接改成实际题数。missing里出现的题号通常是题面里的数字干扰了q_re打开 txt 定位后手动补记录。选项缺失的题目不要急着丢弃在计分阶段会专门处理缺答后面代码会体现这一点。3. 给 EPQ 少年版四个维度建计分模型处理反向题与缺失答案3.1 E/N/P/L 四维度不是四个“考试科目”EPQ 少年版沿用成人版的四维结构E 内外向、N 神经质、P 精神质、L 掩饰性。E 高倾向表现为好交际、爱刺激和精力旺盛N 高则是情绪波动明显、易焦虑、恢复慢P 反映孤独、冷漠和适应困难的程度但 P 分高不等于“精神有问题”L 是效度量表用来识别答题者是否在往社会赞许方向作答。四者的计分方式完全一致每道题归属且仅归属一个维度答案只有“是 / 否”两个键其中一个键记 1 分另一个记 0 分。这里说的“反向题”不是把分数倒扣而是“计分键”本身表达选哪个选项才得分。比如某题归属 N 维度计分键是“是”选“是”得 1 分选“否”得 0 分换一道归属 P 维度的题计分键可能是“否”选“否”才得 1 分。因此实现时不需要把题目拆成正向表和反向表两张只给每题记一个计分键字段即可。四维度的高分倾向和低分倾向在最后的报告模板里会直接复用维度英文名高分倾向低分倾向EExtraversion外向、活跃、渴望刺激内向、安静、偏好独处NNeuroticism焦虑、敏感、情绪易起伏情绪稳定、反应平和PPsychoticism孤独、冷淡、适应欠佳温和、讲合作LLie掩饰倾向、社会赞许性高作答较坦诚L 维度在少年版里要特别留意低龄儿童取悦成人的倾向天然偏高不能直接套用成人的效度阈值。具体阈值放在第 4 章常模部分处理。3.2 用一份 JSON 把计分键和维度归属固定下来常见做法是把量表的计分规则固化成 JSON让题库、计分、报告三层解耦。每条的answer字段代表该题得 1 分的那个选项{ name: EPQ-Junior, dimensions: [E, N, P, L], keys: [ {id: 1, dim: E, answer: 是}, {id: 2, dim: N, answer: 是}, {id: 3, dim: P, answer: 否} ] }这份 JSON 的维护必须对照题面逐题核对尤其是 P 维度题量少且分散漏一题会把原始分整体拉低。我的建议是从 doc 解析出的题目列表与这份 keys 放在同一个目录用覆盖率检查确认每个维度的计分题数再进计分流程。维度名的缩写E/N/P/L要全文统一不要在 JSON 里写“内外向”又在代码里写“E”造成字段对不上。3.3 计分引擎原始分、缺失率与维度覆盖一起算计分引擎不需要花哨功能返回原始分的同时把缺失题列表带出来就够了def score_sheet(answers: dict, epq_key: dict) - tuple: raw {d: 0 for d in epq_key[dimensions]} missing [] for rule in epq_key[keys]: aid str(rule[id]) val answers.get(aid) if val not in (是, 否): missing.append(rule[id]) continue if val rule[answer]: raw[rule[dim]] 1 return raw, missinganswers是“题号 - 是/否”的字典来自前端答题页或纸质答题卡识别结果。missing里累积的是既没答“是”也没答“否”的题号也就是缺答。缺答不是简单跳过必须在原始分旁边附带缺失率字段def missing_rate(raw, epq_key, missing): total {d: 0 for d in epq_key[dimensions]} for rule in epq_key[keys]: total[rule[dim]] 1 rates {} for d in raw: m sum(1 for r in epq_key[keys] if r[dim] d and r[id] in missing) rates[d] m / total[d] if total[d] else 0.0 return rates某一维度缺失率超过 20%我会在该维度报告里直接标注“样本不足解释时降权”而不是强行出 T 分。整卷缺答超过 5 题的答卷按无效卷处理更稳妥这条规则建议写进 README避免后续分析时把半张卷当有效数据混进常模对照。4. 少年版常模对照、T 分换算与测评报告生成4.1 常模表按性别 × 年龄组组织别把成年人的表套上去EPQ 少年版的原始分没有绝对意义必须转换到 T 分之后才能跨性别、跨年龄段比较。少年版常模按性别和年龄分组常见分组方式为 7 岁、8-9 岁、10-11 岁、12-13 岁、14-15 岁男女分别建表。每组对 E、N、P、L 各维护一个均数 M 和标准差 SD{ norm: { male: { 10-11: {E: {M: 18.3, SD: 3.4}, N: {M: 11.2, SD: 4.8}}, 12-13: {E: {M: 18.8, SD: 3.6}, N: {M: 10.6, SD: 5.0}} }, female: { 10-11: {E: {M: 17.6, SD: 3.2}, N: {M: 12.4, SD: 4.9}} } } }上面的数值只是展示结构用的占位数据实际数值需要替换成正规渠道获得的修订版少年版常模。选年龄组时要注意边界12 岁划到 10-11 组还是 12-13 组以常模表说明为准。程序里用一个函数统一做年龄段归属判断避免同一份卷在不同脚本里分组不一致。4.2 T 分换算与分级解释离差 T 分的计算式是T 50 10 * (raw - M) / SD换算直接复用上一章的rawdef raw_to_t(raw_score: float, dim: str, sex: str, age_group: str, norm: dict) - float: row norm[sex][age_group][dim] t 50.0 10.0 * (raw_score - row[M]) / row[SD] return round(min(90.0, max(10.0, t)), 1) t_scores {d: raw_to_t(raw[d], d, sex, age_group, norm) for d in raw}min/max把 T 分截到 10~90 区间防止离群原始分算出负值或超过 100 的分数。换算后按 T 分区间归类报告措辞通常用这张表T 分区间归类报告措辞示例以 E 为例 56.7典型型外向好交际渴望刺激43.3 ~ 56.7中间型倾向适中依情境切换 43.3典型型低内向好静节制交往43.3 和 56.7 对应50 ± (2/3)*10这条离差边界在心理测评报告里是比“≥60 算高”更常用的划界习惯。L 分单独设一道效度闸门T 分达到 60 以上整份报告要加“可能存在掩饰倾向”的提示T 分达到 70 以上直接判定结果参考价值受限建议重测。注意L 分只是效度参考不是诊断依据。少年儿童 L 分偏高常与年龄相关的取悦倾向有关判断时先看对应性别年龄组常模里的 L 分均值再决定是否作废。4.3 用 python-docx 把报告落成 Word 文件T 分算完测评系统通常要交付给班主任、校医或家长一份可读的报告用 python-docx 生成比 HTML 打印更稳定from docx import Document def render_report(student, t_scores, flags): doc Document() doc.add_heading(f{student[name]} 艾森克人格问卷少年版测评报告, 0) table doc.add_table(rows1, cols4) table.style Light Grid Accent 1 hdr table.rows[0].cells for i, dim in enumerate([E 内外向, N 神经质, P 精神质, L 掩饰性]): hdr[i].text dim row table.add_row().cells for i, dim in enumerate([E, N, P, L]): row[i].text str(t_scores[dim]) for flag in flags: doc.add_paragraph(flag) doc.save(f{student[name]}_EPQ报告.docx)表格列标题与 T 分顺序保持同源用同一个[E,N,P,L]列表去驱动表头和填充能避免最常见的“列头对错位”。flags里放 L 分效度提示和缺失率提示每条独立成段。结果解释的文字模板我一般放到独立 YAML 文件里由 T 分分段结果去选措辞而不是在渲染函数里堆 if-else后续换常模或改措辞都不用动代码。5. 守着批量答卷跑批时加两道乱答识别再导汇总5.1 最长连续同选项是廉价但有探测力的乱答指标纸质问卷数字化之后最容易混进来的是连续很长时间不出错、连选同一选项的答卷。不需要机器学习用一个滑动窗口统计“最长连续相同选项”的长度就能识别def suspicious_sequence(answers, total88, max_run10): seq [answers.get(str(i)) for i in range(1, total 1) if answers.get(str(i)) in (是, 否)] if not seq: return True, 0 run best 1 for a, b in zip(seq, seq[1:]): run run 1 if a b else 1 best max(best, run) return best max_run, best这个判断只依赖选项键不看答题时长和外部特征。批量评分前先跑一遍筛选把候选问题卷打上标记要不要直接作废我一般交给主试老师确认不让算法一刀切。5.2 批量评分脚本把每个 JSON 答卷汇成一张 summary.csv工程上更常见的形态是每个学生一份 JSON 答题文件放在scans/目录跑批后输出汇总表import csv, json from pathlib import Path def batch_score(folder: Path): rows [] for f in sorted(folder.glob(*.json)): sheet json.loads(f.read_text(encodingutf-8)) raw, missing score_sheet(sheet[answers], EPQ_KEY) rates missing_rate(raw, EPQ_KEY, missing) rows.append({ file: f.name, **raw, missing: len(missing), invalid: any(v 0.2 for v in rates.values()) or len(missing) 5, }) with open(summary.csv, w, newline, encodingutf-8-sig) as fh: writer csv.DictWriter(fh, fieldnamesrows[0].keys()) writer.writeheader() writer.writerows(rows)utf-8-sig编码让 Excel 打开 CSV 不乱码rows[0].keys()依赖所有答卷字段一致所以score_sheet里的keys必须是同一份 EPQ_KEY。跑批阶段不做 T 分转换只保留原始分和缺失标记把常模更新和 T 分换算留在报告生成阶段这样一改常模不用重新跑全部原始数据。本文还有配套的精品资源点击获取
返回列表