Parquet-Export
Potato-Annotationen nach Apache Parquet exportieren, ein spaltenorientiertes Format für groß angelegte Analysen mit pandas, DuckDB, Spark, Polars und Hugging Face Datasets.
Neu in v2.3.0
Apache Parquet ist ein spaltenorientiertes Speicherformat für analytische Arbeitslasten. Bei großen Annotationsdatensätzen komprimiert es besser als JSON oder CSV, liest eine Teilmenge der Spalten, ohne den Rest der Datei anzufassen, und lässt sich nativ in pandas, DuckDB, PyArrow, Spark, Polars und Hugging Face Datasets laden.
Der Parquet-Export benötigt PyArrow:
pip install "pyarrow>=12.0.0"Den Export ausführen
Parquet ist eines der registrierten Exportformate von Potato, daher schreibt die Export-CLI es direkt:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/Potato liest die Annotationen, die unter dem output_annotation_dir der Konfiguration gespeichert sind, und schreibt die Parquet-Tabellen in das angegebene Ausgabeverzeichnis.
Export nach Zeitplan
Bei einer lang laufenden Studie will man meist einen Parquet-Snapshot, ohne dass jemand daran denken muss, die CLI auszuführen. Wenn export_annotation_format gesetzt ist, schreibt Potato den Export regelmäßig, während die Annotation läuft:
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: 300Automatische Exporte landen in <output_annotation_dir>/exports/<format>/, die Parquet-Tabellen erscheinen also unter output/exports/parquet/. Die Annotationen selbst werden unabhängig von dieser Einstellung immer als <output_annotation_dir>/<user>/user_state.json gespeichert.
Der ältere Schlüssel output_annotation_format ist veraltet. Potato liest ihn als export_annotation_format und protokolliert eine Warnung, benennen Sie ihn also um.
Ausgabedateien
Ein Export erzeugt bis zu vier Tabellen, die jeweils eine andere Ebene der Daten abdecken.
annotations.parquet
Eine Zeile pro Paar (Instanz, Annotator), wobei die Schemas zu Spalten aufgefächert sind.
| Spalte | Typ | Beschreibung |
|---|---|---|
instance_id | string | Die ID des annotierten Elements |
user_id | string | Die ID des Annotators |
| <schema_name> | variiert | Eine Spalte pro Annotationsschema |
Die Spaltentypen folgen dem Annotationstyp: radio und select werden zu Strings mit dem ausgewählten Label, likert, slider und number werden zu float64 (oder zu einem String bei einer Skala mit benannten Labels), multiselect wird zu einer Liste von Strings und text zu einem String.
Ein Schema, das mehrere Antworten gleichzeitig speichert, erhält eine Spalte pro Teilantwort mit dem Namen <schema>.<key>. Ein multirate-Schema namens handling erzeugt handling.Urgency und handling.Customer tone; constant_sum und soft_label werden auf dieselbe Weise aufgeteilt. Eine Einfachauswahl bleibt in einer Spalte, denn severity mit dem Wert "Serious" ist das, was sich in einem Dataframe verwenden lässt.
Jede Spalte steht in jeder Zeile und ist mit null gefüllt, wo ein Annotator nicht geantwortet hat, sodass ein Schema, das nur einige Annotatoren erreicht haben, trotzdem in der Datei vorhanden ist.
spans.parquet
Eine Zeile pro Span-Annotation, geschrieben, wenn die Studie Span-Schemas hat.
| Spalte | Typ | Beschreibung |
|---|---|---|
instance_id | string | Die ID des annotierten Elements |
user_id | string | Die ID des Annotators |
schema_name | string | Name des Span-Schemas |
start | int | Zeichen-Offset, an dem der Span beginnt |
end | int | Zeichen-Offset, an dem der Span endet |
label | string | Das Label des Spans |
text | string | Der Textinhalt des Spans |
target_field | string | In welchem Anzeigefeld der Span gezogen wurde, "" auf einer Seite mit nur einem Feld |
title | string | Der angezeigte Titel des Labels |
span_id | string | Die ID des gespeicherten Spans, schema_label_start_end |
format_coords | string | JSON: Seite und Box bei einem PDF-Span, Zeile und Spalte bei einem Tabellen-Span |
additional_parts | string | JSON: die weiteren Bereiche eines nicht zusammenhängenden Spans |
kb_id, kb_source, kb_label | string | Die Entität, auf die ein verknüpfter Span verweist |
Lesen Sie target_field, sobald eine Studie mehr als ein Span-Ziel hat. Ein einzelnes Span-Schema zeichnet in jedem Span-Zielfeld der Seite auf, das Feld variiert also innerhalb eines Schemas, und span_id kann es nicht wiederherstellen: Dieselben Wörter, in zwei Feldern markiert, ergeben ansonsten identische Zeilen.
text wird beim Export aus den Offsets abgeleitet statt gespeichert und kann ihnen daher nie widersprechen.
items.parquet
Eine Zeile pro Quellelement, mit den Originaldaten.
| Spalte | Typ | Beschreibung |
|---|---|---|
item_id | string | Die ID des Elements |
| <field_name> | variiert | Eine Spalte pro Feld der Originaldaten (verschachtelte Dicts und Listen werden als JSON serialisiert) |
phase_responses.parquet
Wird geschrieben, wenn die Studie Phasendaten erhoben hat, etwa ein Einwilligungsformular oder eine Umfrage nach der Studie, und export_include_phase_data den Wert true hat. Der Schlüssel steht standardmäßig auf false, daher erhält eine Studie, die eine Einwilligung und einen Fragebogen nach der Studie erhoben hat, keine phase_responses.parquet, bis Sie ihn einschalten, und der Export listet die ausgelassenen Antworten in einer Warnung auf.
Exportoptionen
Übergeben Sie Formatoptionen mit --option key=value und wiederholen Sie das Flag für jede Option:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| Option | Standard | Beschreibung |
|---|---|---|
compression | snappy | Kompressionscodec: snappy, gzip, zstd, lz4 oder none |
include_items | true | Schreibt items.parquet mit den Quelldaten |
include_spans | true | Schreibt spans.parquet, wenn Span-Annotationen vorhanden sind |
row_group_size | PyArrow-Standard | Row-Group-Größe für annotations.parquet |
snappy ist für die meisten Projekte der richtige Standard, weil es schnell genug dekomprimiert, dass Lesevorgänge interaktiv bleiben. Wählen Sie zstd, wenn der Datensatz so groß ist, dass die Dateigröße eine Rolle spielt, da es bei ähnlicher Geschwindigkeit deutlich besser komprimiert.
Parquet-Daten laden
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 liefert außerdem einen huggingface-Exporter mit, der dieselben Splits direkt erzeugt. Das erspart die Zwischendateien, wenn der Hub das Ziel ist.
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)Vollständiges Beispiel
Ein Span-Annotationsprojekt, das JSONL als Arbeitsformat beibehält und Parquet für die Analyse daneben schreibt:
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: trueNach der Annotation enthält die Span-Tabelle die Entitäts-Offsets:
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())Weiterführende Informationen
- Exportformate: COCO, YOLO, CoNLL und die anderen registrierten Exporter
- Remote-Datenquellen: Daten aus Cloud-Speicher laden
- Admin-Dashboard: den Annotationsfortschritt überwachen
Implementierungsdetails finden Sie in der Quelldokumentation.