Skip to content

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:

bash
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:

bash
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:

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

Automatische 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.

SpalteTypBeschreibung
instance_idstringDie ID des annotierten Elements
user_idstringDie ID des Annotators
<schema_name>variiertEine 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.

SpalteTypBeschreibung
instance_idstringDie ID des annotierten Elements
user_idstringDie ID des Annotators
schema_namestringName des Span-Schemas
startintZeichen-Offset, an dem der Span beginnt
endintZeichen-Offset, an dem der Span endet
labelstringDas Label des Spans
textstringDer Textinhalt des Spans
target_fieldstringIn welchem Anzeigefeld der Span gezogen wurde, "" auf einer Seite mit nur einem Feld
titlestringDer angezeigte Titel des Labels
span_idstringDie ID des gespeicherten Spans, schema_label_start_end
format_coordsstringJSON: Seite und Box bei einem PDF-Span, Zeile und Spalte bei einem Tabellen-Span
additional_partsstringJSON: die weiteren Bereiche eines nicht zusammenhängenden Spans
kb_id, kb_source, kb_labelstringDie 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.

SpalteTypBeschreibung
item_idstringDie ID des Elements
<field_name>variiertEine 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:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
OptionStandardBeschreibung
compressionsnappyKompressionscodec: snappy, gzip, zstd, lz4 oder none
include_itemstrueSchreibt items.parquet mit den Quelldaten
include_spanstrueSchreibt spans.parquet, wenn Span-Annotationen vorhanden sind
row_group_sizePyArrow-StandardRow-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

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 liefert außerdem einen huggingface-Exporter mit, der dieselben Splits direkt erzeugt. Das erspart die Zwischendateien, wenn der Hub das Ziel ist.

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)

Vollständiges Beispiel

Ein Span-Annotationsprojekt, das JSONL als Arbeitsformat beibehält und Parquet für die Analyse daneben schreibt:

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

Nach der Annotation enthält die Span-Tabelle die Entitäts-Offsets:

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

Weiterführende Informationen

Implementierungsdetails finden Sie in der Quelldokumentation.