Skip to content

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:

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

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

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

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

ColonnaTipoDescrizione
instance_idstringL'ID dell'elemento annotato
user_idstringL'ID dell'annotatore
<schema_name>variabileUna 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.

ColonnaTipoDescrizione
instance_idstringL'ID dell'elemento annotato
user_idstringL'ID dell'annotatore
schema_namestringNome dello schema di span
startintOffset di carattere in cui inizia lo span
endintOffset di carattere in cui finisce lo span
labelstringL'etichetta dello span
textstringIl contenuto testuale dello span
target_fieldstringIl campo di visualizzazione in cui è stato tracciato lo span, "" in una pagina con un solo campo
titlestringIl titolo visualizzato dell'etichetta
span_idstringL'id dello span memorizzato, schema_label_start_end
format_coordsstringJSON: pagina e riquadro per uno span PDF, riga e colonna per uno span di foglio di calcolo
additional_partsstringJSON: gli altri intervalli di uno span discontinuo
kb_id, kb_source, kb_labelstringL'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.

ColonnaTipoDescrizione
item_idstringL'ID dell'elemento
<field_name>variabileUna 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:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
OpzionePredefinitoDescrizione
compressionsnappyCodec di compressione: snappy, gzip, zstd, lz4 o none
include_itemstrueScrive items.parquet con i dati sorgente
include_spanstrueScrive spans.parquet, quando esistono annotazioni di span
row_group_sizePredefinito di PyArrowDimensione 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

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 include anche un esportatore huggingface che costruisce direttamente gli stessi split, evitando i file intermedi quando la destinazione è l'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)

Esempio completo

Un progetto di annotazione di span che mantiene JSONL come formato di lavoro e scrive Parquet accanto per l'analisi:

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

Dopo l'annotazione, è la tabella degli span a contenere gli offset delle entità:

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

Ulteriori letture

Per i dettagli di implementazione, consulta la documentazione sorgente.