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に登録済みのエクスポート形式の1つなので、エクスポートCLIが直接書き出します。

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

Potatoは設定のoutput_annotation_dirに記録されたアノテーションを読み込み、指定した出力ディレクトリにParquetテーブルを書き出します。

定期的なエクスポート

長期間の調査では、誰かがCLIの実行を忘れないことに頼らずに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として読み込み、警告をログに記録するので、名前を変更してください。

出力ファイル

エクスポートは最大4つのテーブルを生成し、それぞれがデータの異なるレベルを扱います。

annotations.parquet

(インスタンス、アノテーター)のペアごとに1行で、スキーマはカラムに展開されます。

カラム型説明
instance_idstringアノテーション対象アイテムのID
user_idstringアノテーターのID
<schema_name>可変アノテーションスキーマごとに1カラム

カラムの型はアノテーションタイプに従います。radioとselectは選択されたラベルを持つ文字列、likert、slider、numberはfloat64(名前付きラベルを持つスケールの場合は文字列)、multiselectは文字列のリスト、textは文字列になります。

複数の回答を同時に保存するスキーマは、サブ回答ごとに<schema>.<key>という名前のカラムを1つずつ持ちます。handlingという名前のmultirateスキーマはhandling.Urgencyとhandling.Customer toneを生成し、constant_sumとsoft_labelも同じように展開されます。単一選択は1カラムのままです。データフレームで扱いやすいのは、severityが"Serious"を持つ形だからです。

すべてのカラムがすべての行に現れ、アノテーターが回答しなかった箇所はnullで埋められます。そのため、一部のアノテーターしか到達しなかったスキーマもファイルに含まれます。

spans.parquet

スパンアノテーションごとに1行で、調査にスパンスキーマがある場合に書き出されます。

カラム型説明
instance_idstringアノテーション対象アイテムのID
user_idstringアノテーターのID
schema_namestringスパンスキーマの名前
startintスパンが始まる文字オフセット
endintスパンが終わる文字オフセット
labelstringスパンのラベル
textstringスパンのテキスト内容
target_fieldstringスパンが描かれた表示フィールド。フィールドが1つのページでは""
titlestringラベルの表示タイトル
span_idstring保存されたスパンのid、schema_label_start_end
format_coordsstringJSON。PDFスパンではページとボックス、スプレッドシートのスパンでは行と列
additional_partsstringJSON。不連続なスパンの残りの範囲
kb_id, kb_source, kb_labelstringリンクされたスパンが指すエンティティ

調査に複数のスパンターゲットがある場合は、必ずtarget_fieldを読んでください。1つのスパンスキーマはページ上のすべてのスパンターゲットフィールドに記録するため、フィールドはスキーマ内で変わります。そしてspan_idからはそれを復元できません。同じ語句を2つのフィールドでマークすると、それ以外はまったく同じ行ができます。

textは保存されず、エクスポート時にオフセットから導出されるため、オフセットと食い違うことはありません。

items.parquet

ソースアイテムごとに1行で、元のデータを保持します。

カラム型説明
item_idstringアイテムのID
<field_name>可変元データのフィールドごとに1カラム(ネストされた辞書やリストはJSONにシリアライズされます)

phase_responses.parquet

同意フォームや調査後アンケートなどのフェーズデータを調査が収集しており、かつexport_include_phase_dataがtrueの場合に書き出されます。このキーのデフォルトはfalseです。そのため、同意と調査後アンケートを集めた調査でも、この設定を有効にするまでphase_responses.parquetは作られず、エクスポートは除外された回答を警告として一覧表示します。

エクスポートオプション

フォーマットオプションは--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スパンアノテーションがある場合に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)

完全な例

作業用の形式として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

アノテーション後、エンティティのオフセットを持つのはスパンテーブルです。

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

参考資料

実装の詳細については、ソースドキュメントを参照してください。