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())延伸閱讀
有關實現詳情,請參閱源文件。