简介:这份资源是一套基于Python构建的常用心理学评估量表数据库源码,面向心理学研究者、临床评估人员及教育测评开发者,用于解决量表数据分散、检索效率低、人工处理易出错等问题。压缩包共1043个文件,约61.63MB,其中411个py脚本承担数据处理与逻辑实现,249个rst与119个txt文档提供说明,127个json与1个pickle用于结构化数据存储,123个pdf及6个docx收录量表手册与评估工具,另有ttf字体、toml构建配置、gitignore与license等工程文件,目录组织体现较完整的项目实践。内容覆盖贝克抑郁量表、汉密尔顿焦虑量表、明尼苏达多项人格问卷等成熟工具,并借助numpy、pandas、matplotlib、scikit-learn及SQLite/MySQL完成统计分析与数据管理。目前已有364人学习,适合希望快速搭建量表管理平台、理解数据库设计与Python工程结构的读者参考。
1. 心理学评估量表数据库:为什么用 Python 把纸笔量表搬进系统里
心理咨询室、医院精神科、高校心理中心,每天都要处理大量量表:SCL-90、SDS、SAS、EPQ、MMPI、PHQ-9、GAD-7……纸笔时代,发卷、回收、逐题算分、查常模、写报告,一个来访者耗掉咨询师半小时。更麻烦的是,不同量表计分规则完全不同——有的正向计分,有的反向计分,有的分维度算因子分,有的还要按性别年龄查常模表换算 T 分。手工算错一个反向题,整份报告就废了。
用 Python 做一套心理学评估量表数据库,核心就三件事:把量表结构化成数据、把计分规则写成可配置的逻辑、把结果存成可查询可追溯的记录。它适合心理信息化开发者、做心理健康产品的后端工程师,也适合想给自己课题组搭一套测评系统的研究生。热搜里 python、数据库设计、源码这几个词,恰好对应了这条路径的三个关键环节。下面我按实际落地顺序,把表结构、计分引擎、避坑点一次讲透。
2. 量表数据建模:题目、维度、选项怎么拆表才不返工
2.1 先想清楚量表的四层结构
一套量表拆开看是四层:量表(Scale)→ 维度/因子(Dimension)→ 题目(Item)→ 选项(Option)。SCL-90 有 90 题、10 个因子;EPQ 有 88 题、4 个维度外加一个测谎维度。题目和维度不是简单的一对一,一道题可能同时属于多个因子(比如某些量表有交叉载荷),所以题目和维度之间必须是多对多关系,中间要一张关联表。
很多人第一版图省事,把题目直接塞进量表表的一个 JSON 字段里,结果做到计分和统计就傻眼了——想按维度聚合、想查某道题的历史作答分布,全得在应用层解析 JSON,SQL 根本没法用。血泪经验:量表结构一定要拆成规范的关系表,JSON 只用来存那些真正不固定的元信息。
2.2 核心表结构设计
下面这套表结构是我在几个心理测评项目里反复用过的,字段做了精简,保留关键部分。
-- 量表主表 CREATE TABLE scale ( id SERIAL PRIMARY KEY, code VARCHAR(32) UNIQUE NOT NULL, -- 量表编码,如 SCL90、PHQ9 name VARCHAR(128) NOT NULL, -- 中文名 description TEXT, -- 指导语 item_count INT NOT NULL, -- 题目总数 scoring_type VARCHAR(32) NOT NULL, -- 计分类型:sum/mean/factor version VARCHAR(16) DEFAULT '1.0', created_at TIMESTAMP DEFAULT NOW() ); -- 维度/因子表 CREATE TABLE dimension ( id SERIAL PRIMARY KEY, scale_id INT NOT NULL REFERENCES scale(id), code VARCHAR(32) NOT NULL, -- 因子编码,如 SOM(躯体化) name VARCHAR(64) NOT NULL, sort_order INT DEFAULT 0 ); -- 题目表 CREATE TABLE item ( id SERIAL PRIMARY KEY, scale_id INT NOT NULL REFERENCES scale(id), item_no INT NOT NULL, -- 题号,从 1 开始 content TEXT NOT NULL, -- 题干 reverse BOOLEAN DEFAULT FALSE, -- 是否反向计分 UNIQUE (scale_id, item_no) ); -- 题目-维度关联(多对多) CREATE TABLE item_dimension ( item_id INT NOT NULL REFERENCES item(id), dimension_id INT NOT NULL REFERENCES dimension(id), weight NUMERIC(4,2) DEFAULT 1.0, -- 载荷权重,默认 1 PRIMARY KEY (item_id, dimension_id) ); -- 选项表 CREATE TABLE option ( id SERIAL PRIMARY KEY, item_id INT NOT NULL REFERENCES item(id), value INT NOT NULL, -- 计分分值,如 1~5 label VARCHAR(64) NOT NULL -- 选项文字 );逻辑说明:scale存量表元信息,scoring_type决定后面用哪种计分引擎;item.reverse是反向计分开关,这是最容易出错的地方;item_dimension用weight支持因子载荷,普通量表填 1.0 即可;option.value直接存分值,避免应用层再映射。
参数说明:code用大写英文加数字,方便程序里做常量;item_no从 1 开始而不是 0,因为量表题号天然从 1 数;weight用 NUMERIC 而不是 FLOAT,避免浮点误差累积。
2.3 用 Python 把量表定义灌进数据库
实际项目里,量表定义通常来自 Excel 或 CSV。我一般写一个导入脚本,把 Excel 解析成上面这套结构。用 SQLAlchemy 做 ORM,pandas 读表。
import pandas as pd from sqlalchemy import create_engine, text engine = create_engine("postgresql://user:pwd@localhost:5432/psych") def import_scale(scale_meta: dict, items_df: pd.DataFrame): """scale_meta: 量表元信息; items_df: 含 item_no, content, reverse, dimension_code, value 列""" with engine.begin() as conn: # 1. 插入量表,拿到自增 id scale_id = conn.execute(text(""" INSERT INTO scale (code, name, description, item_count, scoring_type) VALUES (:code, :name, :desc, :cnt, :stype) RETURNING id """), scale_meta).scalar() # 2. 收集所有维度并插入 dim_map = {} for code in items_df["dimension_code"].unique(): dim_id = conn.execute(text(""" INSERT INTO dimension (scale_id, code, name) VALUES (:sid, :code, :code) RETURNING id """), {"sid": scale_id, "code": code}).scalar() dim_map[code] = dim_id # 3. 逐题插入,同时建关联和选项 for _, row in items_df.iterrows(): item_id = conn.execute(text(""" INSERT INTO item (scale_id, item_no, content, reverse) VALUES (:sid, :no, :content, :rev) RETURNING id """), {"sid": scale_id, "no": int(row["item_no"]), "content": row["content"], "rev": bool(row["reverse"])}).scalar() conn.execute(text(""" INSERT INTO item_dimension (item_id, dimension_id, weight) VALUES (:iid, :did, 1.0) """), {"iid": item_id, "did": dim_map[row["dimension_code"]]}) # 选项:假设每题选项分值固定为 1~5 for v in range(1, 6): conn.execute(text(""" INSERT INTO option (item_id, value, label) VALUES (:iid, :v, :label) """), {"iid": item_id, "v": v, "label": f"选项{v}"}) print(f"量表导入完成,scale_id={scale_id}") # 调用示例 meta = {"code": "PHQ9", "name": "病人健康问卷抑郁量表", "desc": "过去两周内,以下问题困扰您的频率", "cnt": 9, "stype": "sum"} df = pd.read_excel("phq9.xlsx") import_scale(meta, df)逻辑说明:用engine.begin()包住整个导入,任何一步失败自动回滚,不会留下半套脏数据。先插量表拿scale_id,再插维度建映射,最后逐题插并建关联,顺序不能乱,因为外键依赖。
参数说明:RETURNING id是 PostgreSQL 语法,MySQL 用lastrowid,SQLite 用last_insert_rowid(),换库时注意改。items_df的列名要和代码里一致,Excel 表头建议固定为item_no / content / reverse / dimension_code。
提示:导入前先跑一遍
SELECT code FROM scale检查量表是否已存在,避免重复导入产生两套同编码量表,后面查询会串数据。
3. 计分引擎:反向计分、因子分、常模换算怎么写成可配置逻辑
3.1 计分规则为什么不能硬编码
新手最容易犯的错,是把计分逻辑写成一堆 if-else 塞在业务代码里。PHQ-9 是 9 题求和,SCL-90 要算 10 个因子分加总分加阳性项目数,EPQ 要算 4 个维度原始分再查常模转 T 分。如果每个量表写一段代码,加一个新量表就要改代码、重新部署,维护成本极高。
正确做法是把计分规则抽象成「取数 → 变换 → 聚合」三步,规则用配置描述。取数决定哪些题参与,变换处理反向计分,聚合决定是求和还是求均值。这样新增量表只加数据不改代码。
3.2 反向计分与因子分的实现
反向计分是心理学量表特有的坑。一道 5 点计分的题,原始作答 1 分,反向计分后应该是 5 分,公式是max_value + min_value - raw。如果量表选项分值不是 1~5 而是 0~3,公式里的 min 和 max 就得跟着变,所以这两个值必须从 option 表里动态取,不能写死。
def score_item(raw_value: int, reverse: bool, min_v: int, max_v: int) -> int: """单题计分:反向题做镜像翻转""" if reverse: return max_v + min_v - raw_value return raw_value def compute_scores(scale_id: int, answers: dict, conn): """answers: {item_no: raw_value}""" # 1. 取出该量表所有题目及反向标记 items = conn.execute(text(""" SELECT i.id, i.item_no, i.reverse, MIN(o.value) AS min_v, MAX(o.value) AS max_v FROM item i JOIN option o ON o.item_id = i.id WHERE i.scale_id = :sid GROUP BY i.id, i.item_no, i.reverse """), {"sid": scale_id}).fetchall() item_score = {} for it in items: raw = answers.get(it.item_no) if raw is None: raise ValueError(f"题 {it.item_no} 未作答") item_score[it.id] = score_item(raw, it.reverse, it.min_v, it.max_v) # 2. 按维度聚合因子分 rows = conn.execute(text(""" SELECT d.code, d.name, idim.item_id, idim.weight FROM dimension d JOIN item_dimension idim ON idim.dimension_id = d.id WHERE d.scale_id = :sid """), {"sid": scale_id}).fetchall() factor = {} for r in rows: factor.setdefault(r.code, {"name": r.name, "sum": 0.0, "n": 0}) factor[r.code]["sum"] += item_score[r.item_id] * float(r.weight) factor[r.code]["n"] += 1 result = {} for code, f in factor.items(): result[code] = { "name": f["name"], "raw_score": round(f["sum"], 2), "mean_score": round(f["sum"] / f["n"], 2) if f["n"] else 0 } result["total"] = round(sum(item_score.values()), 2) return result逻辑说明:第一步把每道题的原始分按反向规则转成计分分,第二步按item_dimension把题目分归到各因子,同时累加权重和题目数,最后同时给出因子原始分和均分。SCL-90 看的是因子均分(1~5),PHQ-9 看的是总分(0~27),两种都能覆盖。
参数说明:answers的 key 用题号而不是 item_id,因为前端提交的通常是题号;weight默认 1.0,只有做验证性因子分析的量表才需要调;mean_score保留两位小数,和临床报告习惯一致。
3.3 常模换算与结果分级
很多量表算完原始分还不能直接下结论,要查常模转成标准分或 T 分。EPQ 的 T 分公式是T = 50 + 10 * (raw - M) / SD,其中 M 和 SD 来自常模表,且按性别、年龄段不同而不同。常模表单独建一张,用scale_id + dimension_code + gender + age_min + age_max做联合索引。
CREATE TABLE norm ( id SERIAL PRIMARY KEY, scale_id INT NOT NULL REFERENCES scale(id), dimension_code VARCHAR(32) NOT NULL, gender CHAR(1) NOT NULL, -- M/F/A(全部) age_min INT NOT NULL, age_max INT NOT NULL, mean NUMERIC(6,2) NOT NULL, -- 常模均值 M std NUMERIC(6,2) NOT NULL, -- 常模标准差 SD cutoff_low NUMERIC(6,2), -- 分级下界 cutoff_high NUMERIC(6,2) -- 分级上界 );查询时按来访者的性别和年龄命中对应行,代入公式算 T 分,再用cutoff_low/high判断落在哪个等级。这里有个常见误用:常模是有适用人群的,拿成人常模去解释青少年得分,结论会偏得离谱。所以norm表里一定要把适用年龄段存清楚,查询时严格匹配。
注意:常模数据涉及版权和伦理,正式产品里要确认量表授权,别随手从网上抄一份常模就上线。
4. 避坑与排查:量表系统上线后最容易翻车的 5 个地方
4.1 反向计分漏标,整份报告全错
现象:某维度得分异常偏高或偏低,和临床访谈对不上。原因:导入题目时reverse字段漏填或填反,反向题被当正向题算了。解决:导入后跑一条校验 SQL,统计每个量表反向题数量,和量表手册核对;再抽几份已知结果的答卷做回归测试,得分对不上就回头查reverse。
4.2 题目-维度多对多关系丢失
现象:因子分算出来题目数不对,比如某因子应该有 12 题却只聚合了 10 题。原因:item_dimension插入时用了INSERT ... ON CONFLICT DO NOTHING,或者导入脚本对同一题只处理了第一个维度。解决:导入后执行SELECT dimension_id, COUNT(*) FROM item_dimension GROUP BY dimension_id,和手册里的因子题目数逐一比对。
4.3 浮点误差导致临界值判断错误
现象:来访者总分 49.999 被判定为「正常」,实际应该是 50 分「轻度」。原因:因子分用 FLOAT 累加,浮点误差累积。解决:分值字段统一用 NUMERIC 或整数存储,聚合时先转 Decimal 再算,最后再 round。Python 里用decimal.Decimal而不是float。
4.4 并发提交导致答卷重复入库
现象:同一来访者同一量表出现两条记录,统计时被算两次。原因:前端重复点击提交,后端没有幂等控制。解决:答卷表加(user_id, scale_id, session_id)唯一约束,或者提交时带一个客户端生成的submit_token,服务端做去重。
4.5 常模查询命中多条记录
现象:T 分算出来忽高忽低,同一份答卷两次结果不同。原因:norm表里年龄段区间有重叠,或者性别字段有A(全部)和具体性别同时命中。解决:查询时按gender精确匹配优先、A兜底,年龄段用age_min <= age AND age <= age_max并加ORDER BY取第一条,同时在导入常模时做区间重叠校验。
5. 进阶技巧:用 Python 做量表信效度校验与答卷质量筛查
量表系统跑起来之后,真正体现价值的是数据质量。我一般会在系统里加两个后台任务:一是信效度校验,二是作答质量筛查。
信度用 Cronbach's α 系数,衡量内部一致性,α 低于 0.7 说明这个维度题目可能有问题。用 Python 的pingouin或手写公式都能算。效度做验证性因子分析,semopy库可以跑 CFA,看 RMSEA、CFI 这些指标。这些不用实时算,每周跑一次批处理即可。
import numpy as np import pandas as pd def cronbach_alpha(df: pd.DataFrame) -> float: """df: 每列一个题目,每行一个被试的计分后得分""" k = df.shape[1] item_var = df.var(axis=0, ddof=1).sum() total_var = df.sum(axis=1).var(ddof=1) return (k / (k - 1)) * (1 - item_var / total_var) # 从数据库拉某量表某维度的计分后数据 df = pd.read_sql(""" SELECT a.user_id, i.item_no, a.score_value FROM answer a JOIN item i ON i.id = a.item_id WHERE i.scale_id = 1 """, engine).pivot(index="user_id", columns="item_no", values="score_value") alpha = cronbach_alpha(df) print(f"Cronbach α = {alpha:.3f}")逻辑说明:pivot把长表转成「行=被试、列=题目」的宽表,这是算 α 的标准输入格式。ddof=1用样本方差而不是总体方差,和 SPSS 结果一致。
参数说明:α 在 0.7~0.9 之间算良好,超过 0.95 反而可能说明题目冗余。样本量建议至少 100 份再算,太少波动大。
作答质量筛查主要看三个指标:作答时间过短(比如 90 题 3 分钟做完)、规律性作答(连续 10 题选同一个选项)、矛盾作答(正反向配对题答案不一致)。这些规则写成 SQL 或 Python 定时任务,标记可疑答卷,咨询师复核时优先看这些。
我自己的习惯是:每导入一套新量表,先手工造 3 份已知结果的答卷跑一遍全流程,确认计分、常模、分级都对,再开放给真实用户。这个「后悔药」步骤帮我挡过好几次反向计分漏标的翻车。量表系统不怕功能少,就怕算错分——算错一次,用户对整个平台的信任就没了。希望帮到你。
本文还有配套的精品资源,点击获取