跳转到内容

质性编码中的案例与案例属性

在 Potato 中按参与者归组访谈摘录,为案例附上地点或年龄组等属性,并按案例或按属性统计代码。

Potato 中的案例把属于同一个分析单元的条目归为一组,例如一位访谈参与者,或来自同一个机构的全部文档。Potato 在服务器启动时根据数据中的某个字段建立案例,在每个案例上保存地点或年龄组等属性,并让管理员按案例或按案例属性统计代码。QDA 模式默认开启案例,在其他任何研究中,你也可以用一个配置块开启案例。案例功能随 Potato 2.6.0 推出,与 QDA 模式属于同一个版本。

分析单元是研究据以得出结论的对象。在访谈研究中,分析单元通常是参与者,而 Potato 编码的单位是摘录。每条摘录是数据文件中的一个条目。案例把这两个层级连接起来。有了案例,应用在一条摘录上的代码会计入说出这条摘录的参与者,参与者的地点或年龄组也会适用于该访谈的每一条摘录。

从 NVivo 或 ATLAS.ti 转来的研究者熟悉这个概念,只是名称不同。NVivo 的帮助文档写道,案例分类“describe the type of case”(描述案例的类型),每个分类带有你选择记录的属性。帮助页面给出的例子中包括年龄和地点。ATLAS.ti 手册把文档组描述为“quasi dichotomous variables”(准二分变量)(例如名为 gender::female 的组)。ATLAS.ti 在代码-文档表(Code-Document Table)中跨文档和文档组比较代码。Potato 的案例最接近 NVivo 的案例。Potato 没有分类类型,一个项目中的所有案例共用一套扁平的属性。下文介绍的代码交叉表承担代码-分组表的作用。交叉表的每个单元格统计的是摘录数。

根据条目字段检测案例

Potato 直接从条目本身读取案例。每个条目在某个字段中写明自己所属的案例,该字段取值相同的条目构成一个案例。下面的文件 data/excerpts.jsonl 每行是一个 JSON 对象,文件包含来自三场访谈的六条摘录:

json
{"id": "p01-01", "text": "I stopped going because the bus only runs twice a day.", "participant_id": "P01", "site": "rural", "age_group": "18-34"}
{"id": "p01-02", "text": "The nurse remembered my name, and that mattered more than the wait.", "participant_id": "P01"}
{"id": "p02-01", "text": "My copay went up and I started skipping refills.", "participant_id": "P02", "site": "urban", "age_group": "55+"}
{"id": "p02-02", "text": "I never know which clinic is open on Saturdays.", "participant_id": "P02"}
{"id": "p03-01", "text": "The bus takes an hour each way, so I go only when it is urgent.", "participant_id": "P03", "site": "rural", "age_group": "35-54"}
{"id": "p03-02", "text": "The doctor explained everything twice without rushing me.", "participant_id": "P03"}

服务器启动时,Potato 读取每个条目上由 cases.key 指定的字段,并把条目归入同名的案例。某个名称第一次出现时,Potato 就创建对应的案例。没有设置 cases.key 时,Potato 依次检查 case_id、participant_id 和 respondent_id,并使用其中第一个取值非空的字段。Potato 对每个条目分别做这项检查。因此,如果文件中有些行带 case_id、另一些行带 participant_id,得到的案例会分别以这两个字段命名。设置 cases.key 可以避免这种混用。对于上面的文件,服务器日志会报告 Cases auto-detect: 3 case(s), 6 instance(s) assigned。

Potato 只读取顶层字段,从不读取嵌套在其他键下的字段。案例字段没有取值的条目不属于任何案例,一个条目最多属于一个案例。Potato 在每次启动时都会重新运行检测,并且只添加或更新记录。当你添加访谈后重启,Potato 会把新的摘录放进相应的案例,并保留原有的案例。Potato 从不删除案例,你从数据中移除的参与者仍会出现在案例列表中。

Potato 把案例、案例属性和案例成员关系保存在 <task_dir>/project.sqlite 中。同一个项目数据库还保存码本和备忘录。案例归在研究的 annotation_task_name 之下。如果你重命名任务,Potato 会在下次启动时建立一套新的案例,并把旧的那套留在文件中。

配置

下面的配置开启 QDA 模式,用一个跨度方案承载代码,按 participant_id 归组摘录,并把 site 和 age_group 复制到每个案例上:

yaml
annotation_task_name: Clinic Access Interviews
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/excerpts.jsonl
item_properties:
  id_key: id
  text_key: text
user_config:
  allow_all_users: true
 
qda_mode:
  enabled: true
 
cases:
  key: participant_id
  attributes: [site, age_group]
 
annotation_schemes:
- annotation_type: span
  name: codes
  description: Highlight a passage and apply a code
  codebook: true
  labels: [transport, cost, scheduling, provider trust]

QDA 模式会开启案例,这里的 cases 块只指定键和属性。对于不在 QDA 模式下的研究,请在该块中加上 enabled: true。Potato 读取的全部选项如下:

选项默认值说明
cases.enabled未设置(跟随 qda_mode.enabled)true 在任何研究中开启案例。false 即使在 QDA 模式下也关闭案例。
cases.key未设置(依次检查 case_id、participant_id 和 respondent_id)其取值作为案例名称的条目字段。
cases.auto_detecttrue在服务器启动时根据数据建立案例。设为 false 时,Potato 不再建立新案例,此前启动时的归属保持不变。
cases.attributes[]作为案例属性复制到每个案例上的条目字段。

Potato 依照由这四个键组成的封闭列表检查 cases 块。如果配置中有拼错的键,例如 atributes,python -m potato.validate_cli config.yaml 会报告该键并建议改为 attributes。

案例属性

案例属性记录的是关于参与者而非某一条摘录的信息,例如研究地点或年龄组。启动时,Potato 把 cases.attributes 中列出的每个字段从每个条目复制到该条目所属的案例上。因此,参与者层面的字段只需要出现在每位参与者的一条摘录上。在上面的数据中,site 和 age_group 只出现在每场访谈的第一条摘录上。从电子表格导出、每一行都带有该字段的数据同样适用。

当同一案例的摘录取值不一致时,按数据文件顺序最后读取的摘录决定案例的取值。值为 null 的字段会被跳过,但空字符串算作一个取值,并会替换之前的取值。请在该行中省略这个字段,而不要写 ""。Potato 把所有属性值都存为文本,34 和 "34" 最终是同一个值。

虽然标注页面不显示案例属性,但任何已登录的标注者都能通过 /api/cases 读取每个案例及其属性。作为案例属性保存的实验条件,对一位本应不知晓条件的编码者来说,只需一次请求就能看到。在这种设计中,请不要把条件放进 Potato 载入的数据文件,而是在事后把条件合并到导出结果中。

按案例属性统计代码

对于每个代码和某一属性的每个取值,代码交叉表统计获得该代码的摘录数。管理仪表板页面说明了管理员密钥的来源。管理员从 /admin/api/code_crosstab 请求这张表,并在 X-API-Key 请求头中发送管理员密钥:

bash
curl -H "X-API-Key: $KEY" "http://localhost:8000/admin/api/code_crosstab?attribute=site"

两位编码者处理完示例数据后,响应如下:

json
{
  "attribute_key": "site",
  "codes": ["codes::cost", "codes::provider trust", "codes::transport"],
  "values": ["rural", "urban"],
  "cells": [
    {"code": "codes::cost", "value": "urban", "count": 1},
    {"code": "codes::provider trust", "value": "rural", "count": 2},
    {"code": "codes::transport", "value": "rural", "count": 1}
  ],
  "n_instances": 6,
  "schema_filter": null
}

Potato 按五条规则填写单元格:

  • 以摘录为单位。 每个单元格统计的是摘录数。两位编码者都对同一条摘录应用了某个代码,或一位编码者在同一条摘录中把某个代码高亮了两次时,该代码对这条摘录只计一次。在示例中,两位编码者都在摘录 p01-01 上编码了 transport,而 transport/rural 单元格的值为 1。
  • 回退到案例。 Potato 先从摘录本身取属性,摘录没有取值时再回退到摘录所属的案例。摘录 p01-02 没有 site 字段,Potato 根据案例 P01 把这条摘录计入 rural。
  • 什么算作代码。 代码的名称形如 scheme::label。选中的标签和高亮的跨度会被计入。滑块的数值和自由文本不计入。
  • 分母。 n_instances 是在该属性上有取值的摘录数,无论是否已编码。摘录和案例都没有取值的摘录不进入这张表。
  • 单个方案。 在请求中加上 &schema=codes 后,表中只统计一个标注方案的代码。

要按案例统计代码,请把案例键本身作为属性传入(例如 attribute=participant_id)。这样得到的表每位参与者占一列。由于案例键字段出现在案例的每一条摘录上,按案例统计的表不需要回退。

交叉表把所有编码者合并统计,没有按编码者或按案例的筛选。一次请求只涵盖一个属性,响应只有 JSON 格式。Potato 2.13.1 的管理仪表板没有交叉表视图。如需按编码者统计,请使用下文导出一节介绍的引文报告中的 coder 列。

通过 API 读取案例

两个只读端点向任何已登录的会话返回案例,无需管理员密钥:

方法路径返回内容
GET/api/cases研究中的每个案例,按名称排序,附带属性
GET/api/cases/instance/<id>某个条目所属的案例及其属性,或 {"case": null}

对于没有会话的请求,端点返回 401;案例关闭时返回 503。一条案例记录包含生成的 id、取自数据的 name、attributes、created_by(检测得到的案例为 cases-autodetect),以及 created_at 和 updated_at 中的 Unix 时间戳。

导出案例

在 2.13.1 中,Potato 没有任何导出器会写出案例。REFI-QDA .qdpx 导出包含来源、代码、编码记录和编码者,但不包含案例和案例属性。在 NVivo 或 ATLAS.ti 中打开由该文件生成的项目时,项目中没有案例,也没有分类。只有当配置显式设置 cases.enabled: true 时,Potato 才会就这一遗漏打印警告。依赖 QDA 模式默认值的研究不会收到警告。

案例数据保存在 project.sqlite 中。用 sqlite3 命令行工具运行两条查询,即可把案例成员关系和案例属性写入 CSV:

bash
sqlite3 -header -csv project.sqlite "SELECT d.instance_id, c.name AS case_name FROM case_documents d JOIN cases c ON c.id = d.case_id ORDER BY c.name;" > cases.csv
sqlite3 -header -csv project.sqlite "SELECT c.name AS case_name, a.key AS attribute, a.value FROM cases c JOIN case_attributes a ON a.case_id = c.id ORDER BY c.name, a.key;" > case_attributes.csv

文件 cases.csv 每条摘录一行。cases.csv 的 instance_id 列与引文报告的 instance_id 列相对应。用下面的命令写出引文报告:

bash
python -m potato.export -c config.yaml --format quotation_report -o export/

该命令把每个已编码段落作为一行写入 export/quotations.csv。各列分别是代码、引用的文本、字符偏移量、条目和编码者。按 instance_id 把引文报告与 cases.csv 连接,即可为每条已编码引文附上参与者,再按 case_name 连接一次即可加上属性。

局限

  • 不能手动指定。 案例只能在启动时通过检测产生。没有任何页面或 API 调用能把条目移到另一个案例,或编辑属性。要更改案例归属或属性,请编辑数据文件并重启 Potato。
  • 只有一层。 案例不能包含其他案例,一个条目只属于一个案例。焦点小组的一条摘录可以属于该小组,也可以属于某一位发言者,但不能同时属于两者。
  • 代码附着在摘录上。 QDA 模式无法对整个案例进行编码。
  • 按条目计算一致性。 编码者间一致性按条目计算,不按案例分组。

上游的案例示例

上游示例从 Potato 源码目录运行。示例中的六条摘录各带一个 participant_id 和一个 condition,配置把 condition 复制到每个案例上:

bash
python potato/flask_server.py start examples/advanced/cases-example/config.yaml -p 8000

登录,编码几条摘录,然后带上管理员密钥请求 /admin/api/code_crosstab?attribute=condition,即可看到按条件统计的代码。

延伸阅读

实现细节请参阅源文档。