Parquet 导出
把 Potato 标注导出为 Apache Parquet,一种列式格式,可用 pandas、DuckDB、Spark、Polars 和 Hugging Face Datasets 做大规模分析。
v2.3.0 新增
Apache Parquet 是一种为分析型负载而生的列式存储格式。对于大型标注数据集,它的压缩率优于 JSON 或 CSV,读取部分列时不必碰文件的其余部分,并且在 pandas、DuckDB、PyArrow、Spark、Polars 和 Hugging Face Datasets 中都能原生加载。
Parquet 导出需要 PyArrow:
pip install "pyarrow>=12.0.0"运行导出
Parquet 是 Potato 已注册的导出格式之一,所以导出命令行工具可以直接写出它:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/Potato 会读取配置中 output_annotation_dir 下记录的标注,并把 Parquet 表写进你指定的输出目录。
定时导出
长期运行的研究通常希望拿到 Parquet 快照,而不必靠人记得去跑命令行。设置 export_annotation_format 可以让 Potato 在标注进行期间定期写出导出文件:
output_annotation_dir: "output/"
# A list is accepted, so Parquet can sit alongside another format
export_annotation_format:
- parquet
- jsonl
# Seconds between automatic exports (default: 60)
auto_export_interval: 300自动导出会落在 <output_annotation_dir>/exports/<format>/,所以 Parquet 表会出现在 output/exports/parquet/ 下。无论这项设置如何,标注本身始终存放为 <output_annotation_dir>/<user>/user_state.json。
较旧的 output_annotation_format 键已废弃。Potato 会把它当作 export_annotation_format 读取并记录一条警告,所以还是改名为好。
输出文件
一次导出最多产生四张表,各自覆盖数据的不同层级。
annotations.parquet
每一行对应一个(实例,标注者)组合,各方案被展平成列。
| 列 | 类型 | 说明 |
|---|---|---|
instance_id | string | 被标注条目的 ID |
user_id | string | 标注者的 ID |
| <schema_name> | 不定 | 每个标注方案一列 |
列的类型跟随标注类型:radio 和 select 变成保存所选标签的字符串,likert、slider 和 number 变成 float64(如果刻度带命名标签则为字符串),multiselect 变成字符串列表,text 变成字符串。
一次存下多个答案的方案,每个子答案得到一列,命名为 <schema>.<key>。一个名为 handling 的 multirate 方案会产生 handling.Urgency 和 handling.Customer tone;constant_sum 和 soft_label 的展开方式相同。单选仍然只占一列,因为在数据框里真正好用的就是 severity 里存着 "Serious"。
每一列都会出现在每一行上,标注者没有回答的地方填 null,所以只有部分标注者做到的方案仍然存在于文件中。
spans.parquet
每一行对应一条 span 标注,当研究中含有 span 方案时写出。
| 列 | 类型 | 说明 |
|---|---|---|
instance_id | string | 被标注条目的 ID |
user_id | string | 标注者的 ID |
schema_name | string | span 方案的名称 |
start | int | span 起始处的字符偏移量 |
end | int | span 结束处的字符偏移量 |
label | string | 该 span 的标签 |
text | string | 该 span 的文本内容 |
target_field | string | span 是在哪个显示字段中标出的,单字段页面上为 "" |
title | string | 该标签的显示标题 |
span_id | string | 所存 span 的 id,形如 schema_label_start_end |
format_coords | string | JSON:PDF span 的页码和方框,电子表格 span 的行和列 |
additional_parts | string | JSON:不连续 span 的其余区间 |
kb_id、kb_source、kb_label | string | 已链接的 span 所指向的实体 |
只要一项研究有多个 span 目标,就要读 target_field。单个 span 方案会在页面上每个 span 目标字段中记录,所以同一个方案内部字段也会变化,而 span_id 无法还原它:同样的词在两个字段中被标出,产生的行在其余部分完全相同。
text 是导出时由偏移量推出来的,而不是存下来的,所以它永远不会与偏移量不一致。
items.parquet
每一行对应一个源条目,承载原始数据。
| 列 | 类型 | 说明 |
|---|---|---|
item_id | string | 条目的 ID |
| <field_name> | 不定 | 原始数据中每个字段一列(嵌套的字典和列表会被 JSON 序列化) |
phase_responses.parquet
当研究收集了阶段数据(例如知情同意书或研究后问卷),并且配置把这些数据纳入导出时才会写出。一项既收集知情同意又收集研究后问卷的研究,如果没有纳入阶段数据,这张表一行也不会有,所以在认定文件存在之前先检查 export_include_phase_data。
导出选项
通过 --option key=value 传入格式选项,每个选项重复一次该参数:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| 选项 | 默认值 | 说明 |
|---|---|---|
compression | snappy | 压缩编码:snappy、gzip、zstd、lz4 或 none |
include_items | true | 写出带源数据的 items.parquet |
include_spans | true | 当存在 span 标注时写出 spans.parquet |
row_group_size | PyArrow 默认值 | annotations.parquet 的行组大小 |
对多数项目来说 snappy 是合适的默认值,因为它解压足够快,读取仍然是交互式的。当数据集大到文件体积变得重要时选 zstd,它在速度相近的情况下压缩效果好得多。
加载 Parquet 数据
pandas
import pandas as pd
annotations = pd.read_parquet("parquet_export/annotations.parquet")
spans = pd.read_parquet("parquet_export/spans.parquet")
items = pd.read_parquet("parquet_export/items.parquet")
# One column per schema, so a schema is selected by name
print(annotations["sentiment"].value_counts())
# Compute inter-annotator agreement on the sentiment schema
from sklearn.metrics import cohen_kappa_score
pivot = annotations.pivot(index="instance_id", columns="user_id", values="sentiment")
kappa = cohen_kappa_score(pivot.iloc[:, 0], pivot.iloc[:, 1])DuckDB
-- Direct query without loading into memory
SELECT instance_id, sentiment, COUNT(*) AS annotator_count
FROM 'parquet_export/annotations.parquet'
GROUP BY instance_id, sentiment
ORDER BY annotator_count DESC;
-- Join annotations with the source items
SELECT a.instance_id, i.text, a.sentiment, a.user_id
FROM 'parquet_export/annotations.parquet' a
JOIN 'parquet_export/items.parquet' i
ON a.instance_id = i.item_id;PyArrow
import pyarrow.parquet as pq
# Read specific columns only (fast for wide tables)
table = pq.read_table(
"parquet_export/annotations.parquet",
columns=["instance_id", "user_id", "sentiment"]
)
df = table.to_pandas()
parquet_file = pq.ParquetFile("parquet_export/annotations.parquet")
print(f"Row groups: {parquet_file.metadata.num_row_groups}")
print(f"Total rows: {parquet_file.metadata.num_rows}")Hugging Face Datasets
from datasets import load_dataset
dataset = load_dataset("parquet", data_files={
"annotations": "parquet_export/annotations.parquet",
"spans": "parquet_export/spans.parquet",
"items": "parquet_export/items.parquet",
})
print(dataset["annotations"][0])
dataset["annotations"].push_to_hub("my-org/my-annotations", split="train")Potato 还自带一个 huggingface 导出器,可以直接构建同样的数据划分;当目的地就是 Hub 时,这样可以省掉中间文件。
Polars
import polars as pl
annotations = pl.read_parquet("parquet_export/annotations.parquet")
label_counts = (
annotations
.group_by("sentiment")
.agg(pl.len().alias("count"))
.sort("count", descending=True)
)
print(label_counts)完整示例
一个 span 标注项目,把 JSONL 保留为工作格式,同时写出 Parquet 供分析使用:
annotation_task_name: "NER Annotation Project"
task_dir: "."
data_files:
- "data/documents.jsonl"
item_properties:
id_key: doc_id
text_key: text
annotation_schemes:
- annotation_type: span
name: entities
description: "Highlight named entities"
labels:
- name: PERSON
- name: ORGANIZATION
- name: LOCATION
output_annotation_dir: "output/"
# Both formats are written periodically to output/exports/<format>/
export_annotation_format:
- jsonl
- parquet
auto_export_interval: 300
user_config:
allow_all_users: true标注完成后,携带实体偏移量的是 span 表:
import pandas as pd
spans = pd.read_parquet("output/exports/parquet/spans.parquet")
# Entity type distribution
print(spans["label"].value_counts())
# Average span length by type
spans["length"] = spans["end"] - spans["start"]
print(spans.groupby("label")["length"].mean())延伸阅读
有关实现详情,请参阅源文档。