Esportazione Parquet
Esporta le annotazioni di Potato in Apache Parquet, un formato a colonne per l'analisi su larga scala con pandas, DuckDB, Spark, Polars e Hugging Face Datasets.
Novità della v2.3.0
Apache Parquet è un formato di archiviazione a colonne pensato per i carichi di lavoro analitici. Sui dataset di annotazione di grandi dimensioni comprime meglio di JSON o CSV, legge un sottoinsieme di colonne senza toccare il resto del file e si carica in modo nativo in pandas, DuckDB, PyArrow, Spark, Polars e Hugging Face Datasets.
L'esportazione Parquet richiede PyArrow:
pip install "pyarrow>=12.0.0"Eseguire l'esportazione
Parquet è uno dei formati di esportazione registrati di Potato, quindi la CLI di esportazione lo scrive direttamente:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/Potato legge le annotazioni registrate sotto l'output_annotation_dir della configurazione e scrive le tabelle Parquet nella directory di output indicata.
Esportazione pianificata
In uno studio di lunga durata di solito si vuole uno snapshot Parquet senza dipendere da qualcuno che si ricordi di eseguire la CLI. Con export_annotation_format impostato, Potato scrive l'esportazione periodicamente mentre l'annotazione è in corso:
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: 300Le esportazioni automatiche finiscono in <output_annotation_dir>/exports/<format>/, quindi le tabelle Parquet compaiono sotto output/exports/parquet/. Le annotazioni vere e proprie sono sempre salvate come <output_annotation_dir>/<user>/user_state.json, indipendentemente da questa impostazione.
La vecchia chiave output_annotation_format è deprecata. Potato la legge come export_annotation_format e registra un avviso, quindi rinominala.
File di output
Un'esportazione produce fino a quattro tabelle, ciascuna relativa a un diverso livello dei dati.
annotations.parquet
Una riga per ogni coppia (istanza, annotatore), con gli schemi appiattiti in colonne.
| Colonna | Tipo | Descrizione |
|---|---|---|
instance_id | string | L'ID dell'elemento annotato |
user_id | string | L'ID dell'annotatore |
| <schema_name> | variabile | Una colonna per ogni schema di annotazione |
I tipi di colonna seguono il tipo di annotazione: radio e select diventano stringhe con l'etichetta selezionata, likert, slider e number diventano float64 (o stringhe per una scala con etichette nominali), multiselect diventa una lista di stringhe e text diventa una stringa.
Uno schema che memorizza più risposte contemporaneamente riceve una colonna per ogni sotto-risposta, con nome <schema>.<key>. Uno schema multirate chiamato handling produce handling.Urgency e handling.Customer tone; constant_sum e soft_label si distribuiscono allo stesso modo. Una selezione singola resta in una sola colonna, perché ciò che si usa in un dataframe è severity con il valore "Serious".
Ogni colonna compare in ogni riga, riempita con null dove un annotatore non ha risposto, così uno schema raggiunto solo da alcuni annotatori è comunque presente nel file.
spans.parquet
Una riga per ogni annotazione di span, scritta quando lo studio ha schemi di span.
| Colonna | Tipo | Descrizione |
|---|---|---|
instance_id | string | L'ID dell'elemento annotato |
user_id | string | L'ID dell'annotatore |
schema_name | string | Nome dello schema di span |
start | int | Offset di carattere in cui inizia lo span |
end | int | Offset di carattere in cui finisce lo span |
label | string | L'etichetta dello span |
text | string | Il contenuto testuale dello span |
target_field | string | Il campo di visualizzazione in cui è stato tracciato lo span, "" in una pagina con un solo campo |
title | string | Il titolo visualizzato dell'etichetta |
span_id | string | L'id dello span memorizzato, schema_label_start_end |
format_coords | string | JSON: pagina e riquadro per uno span PDF, riga e colonna per uno span di foglio di calcolo |
additional_parts | string | JSON: gli altri intervalli di uno span discontinuo |
kb_id, kb_source, kb_label | string | L'entità a cui punta uno span collegato |
Leggi target_field ogni volta che uno studio ha più di un target di span. Un singolo schema di span registra in tutti i campi target di span della pagina, quindi il campo varia all'interno di uno schema, e span_id non permette di ricavarlo: le stesse parole marcate in due campi producono righe altrimenti identiche.
text viene ricavato dagli offset al momento dell'esportazione invece di essere memorizzato, quindi non può mai essere in disaccordo con essi.
items.parquet
Una riga per ogni elemento sorgente, con i dati originali.
| Colonna | Tipo | Descrizione |
|---|---|---|
item_id | string | L'ID dell'elemento |
| <field_name> | variabile | Una colonna per ogni campo dei dati originali (dizionari e liste annidati vengono serializzati in JSON) |
phase_responses.parquet
Viene scritto quando lo studio ha raccolto dati di fase, come un modulo di consenso o un questionario post-studio, e export_include_phase_data è true. La chiave vale false per impostazione predefinita, quindi uno studio che ha raccolto il consenso e un questionario post-studio non ottiene alcun phase_responses.parquet finché non la attivi, e l'esportazione elenca le risposte escluse in un avviso.
Opzioni di esportazione
Passa le opzioni di formato con --option key=value, ripetendo il flag per ciascuna:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| Opzione | Predefinito | Descrizione |
|---|---|---|
compression | snappy | Codec di compressione: snappy, gzip, zstd, lz4 o none |
include_items | true | Scrive items.parquet con i dati sorgente |
include_spans | true | Scrive spans.parquet, quando esistono annotazioni di span |
row_group_size | Predefinito di PyArrow | Dimensione dei row group per annotations.parquet |
snappy è la scelta predefinita giusta per la maggior parte dei progetti, perché decomprime abbastanza velocemente da mantenere interattive le letture. Scegli zstd quando il dataset è abbastanza grande da rendere rilevante la dimensione dei file, perché comprime molto meglio a una velocità simile.
Caricare dati 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 include anche un esportatore huggingface che costruisce direttamente gli stessi split, evitando i file intermedi quando la destinazione è l'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)Esempio completo
Un progetto di annotazione di span che mantiene JSONL come formato di lavoro e scrive Parquet accanto per l'analisi:
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: trueDopo l'annotazione, è la tabella degli span a contenere gli offset delle entità:
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())Ulteriori letture
- Formati di esportazione: COCO, YOLO, CoNLL e gli altri esportatori registrati
- Fonti di dati remote: caricamento di dati da archivi cloud
- Dashboard di amministrazione: monitoraggio dell'avanzamento dell'annotazione
Per i dettagli di implementazione, consulta la documentazione sorgente.