Skip to content

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:

bash
pip install "pyarrow>=12.0.0"

執行匯出

Parquet 是 Potato 已註冊的匯出格式之一,所以匯出命令列工具可以直接寫出它:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/

Potato 會讀取配置中 output_annotation_dir 下記錄的標註,並把 Parquet 表寫進你指定的輸出目錄。

定時匯出

長期執行的研究通常希望拿到 Parquet 快照,而不必靠人記得去跑命令列。設定 export_annotation_format 可以讓 Potato 在標註進行期間定期寫出匯出檔案:

yaml
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_idstring被標註條目的 ID
user_idstring標註者的 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_idstring被標註條目的 ID
user_idstring標註者的 ID
schema_namestringspan 方案的名稱
startintspan 起始處的字元偏移量
endintspan 結束處的字元偏移量
labelstring該 span 的標籤
textstring該 span 的文本內容
target_fieldstringspan 是在哪個顯示欄位中標出的,單欄位頁面上為 ""
titlestring該標籤的顯示標題
span_idstring所存 span 的 id,形如 schema_label_start_end
format_coordsstringJSON:PDF span 的頁碼和方框,電子表格 span 的行和列
additional_partsstringJSON:不連續 span 的其餘區間
kb_id、kb_source、kb_labelstring已連結的 span 所指向的實體

只要一項研究有多個 span 目標,就要讀 target_field。單個 span 方案會在頁面上每個 span 目標欄位中記錄,所以同一個方案內部欄位也會變化,而 span_id 無法還原它:同樣的詞在兩個欄位中被標出,產生的行在其餘部分完全相同。

text 是匯出時由偏移量推出來的,而不是存下來的,所以它永遠不會與偏移量不一致。

items.parquet

每一行對應一個源條目,承載原始資料。

列類型說明
item_idstring條目的 ID
<field_name>不定原始資料中每個欄位一列(巢狀的字典和列表會被 JSON 序列化)

phase_responses.parquet

當研究收集了階段資料(例如知情同意書或研究後問卷),並且配置把這些資料納入匯出時才會寫出。一項既收集知情同意又收集研究後問卷的研究,如果沒有納入階段資料,這張表一行也不會有,所以在認定檔案存在之前先檢查 export_include_phase_data。

匯出選項

通過 --option key=value 傳入格式選項,每個選項重複一次該參數:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
選項預設值說明
compressionsnappy壓縮編碼:snappy、gzip、zstd、lz4 或 none
include_itemstrue寫出帶源資料的 items.parquet
include_spanstrue當存在 span 標註時寫出 spans.parquet
row_group_sizePyArrow 預設值annotations.parquet 的行組大小

對多數項目來說 snappy 是合適的預設值,因為它解壓足夠快,讀取仍然是互動式的。當資料集大到檔案體積變得重要時選 zstd,它在速度相近的情況下壓縮效果好得多。

載入 Parquet 資料

pandas

python
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

sql
-- 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

python
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

python
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

python
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 供分析使用:

yaml
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 表:

python
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())

延伸閱讀

有關實現詳情,請參閱源文件。