Skip to content

Export Parquet

Exportez les annotations Potato vers Apache Parquet, un format en colonnes pour l'analyse à grande échelle avec pandas, DuckDB, Spark, Polars et Hugging Face Datasets.

Nouveau dans la v2.3.0

Apache Parquet est un format de stockage en colonnes conçu pour les charges de travail analytiques. Sur de gros jeux de données d'annotation, il compresse mieux que JSON ou CSV, lit un sous-ensemble de colonnes sans toucher au reste du fichier et se charge nativement dans pandas, DuckDB, PyArrow, Spark, Polars et Hugging Face Datasets.

L'export Parquet nécessite PyArrow :

bash
pip install "pyarrow>=12.0.0"

Lancer l'export

Parquet fait partie des formats d'export enregistrés de Potato, donc la CLI d'export l'écrit directement :

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

Potato lit les annotations enregistrées sous le output_annotation_dir de la configuration et écrit les tables Parquet dans le répertoire de sortie que vous indiquez.

Exporter à intervalles réguliers

Sur une étude de longue durée, on veut en général un instantané Parquet sans dépendre de quelqu'un qui pense à lancer la CLI. Avec export_annotation_format, Potato écrit l'export périodiquement pendant que l'annotation est en cours :

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

Les exports automatiques sont écrits dans <output_annotation_dir>/exports/<format>/, donc les tables Parquet apparaissent sous output/exports/parquet/. Les annotations elles-mêmes sont toujours stockées dans <output_annotation_dir>/<user>/user_state.json, quel que soit ce réglage.

L'ancienne clé output_annotation_format est dépréciée. Potato la lit comme export_annotation_format et journalise un avertissement, alors renommez-la.

Fichiers produits

Un export produit jusqu'à quatre tables, chacune couvrant un niveau différent des données.

annotations.parquet

Une ligne par paire (instance, annotateur), avec les schémas aplatis en colonnes.

ColonneTypeDescription
instance_idstringL'identifiant de l'élément annoté
user_idstringL'identifiant de l'annotateur
<schema_name>variableUne colonne par schéma d'annotation

Le type de colonne suit le type d'annotation : radio et select deviennent des chaînes contenant l'étiquette sélectionnée, likert, slider et number deviennent des float64 (ou des chaînes pour une échelle à étiquettes nommées), multiselect devient une liste de chaînes et text devient une chaîne.

Un schéma qui stocke plusieurs réponses à la fois reçoit une colonne par sous-réponse, nommée <schema>.<key>. Un schéma multirate nommé handling produit handling.Urgency et handling.Customer tone ; constant_sum et soft_label se répartissent de la même façon. Une sélection unique reste dans une seule colonne, parce que c'est severity contenant "Serious" qui est exploitable dans un dataframe.

Chaque colonne figure sur chaque ligne, remplie avec null quand un annotateur n'a pas répondu, de sorte qu'un schéma que seuls certains annotateurs ont atteint reste présent dans le fichier.

spans.parquet

Une ligne par annotation d'empan, écrite quand l'étude comporte des schémas d'empans.

ColonneTypeDescription
instance_idstringL'identifiant de l'élément annoté
user_idstringL'identifiant de l'annotateur
schema_namestringNom du schéma d'empans
startintDécalage du caractère où commence l'empan
endintDécalage du caractère où se termine l'empan
labelstringL'étiquette de l'empan
textstringLe contenu textuel de l'empan
target_fieldstringLe champ d'affichage dans lequel l'empan a été tracé, "" sur une page à un seul champ
titlestringLe titre affiché de l'étiquette
span_idstringL'id de l'empan stocké, schema_label_start_end
format_coordsstringJSON : page et cadre pour un empan PDF, ligne et colonne pour un empan de tableur
additional_partsstringJSON : les autres plages d'un empan discontinu
kb_id, kb_source, kb_labelstringL'entité vers laquelle pointe un empan lié

Lisez target_field dès qu'une étude a plus d'une cible d'empans. Un même schéma d'empans enregistre dans tous les champs cibles d'empans de la page, donc le champ varie au sein d'un schéma, et span_id ne permet pas de le retrouver : les mêmes mots marqués dans deux champs produisent des lignes identiques par ailleurs.

text est dérivé des décalages au moment de l'export au lieu d'être stocké, il ne peut donc jamais les contredire.

items.parquet

Une ligne par élément source, avec les données d'origine.

ColonneTypeDescription
item_idstringL'identifiant de l'élément
<field_name>variableUne colonne par champ des données d'origine (les dictionnaires et listes imbriqués sont sérialisés en JSON)

phase_responses.parquet

Écrit quand l'étude a collecté des données de phase, comme un formulaire de consentement ou un questionnaire de fin d'étude, et que export_include_phase_data vaut true. La clé vaut false par défaut, donc une étude qui a recueilli un consentement et un questionnaire de fin d'étude n'obtient aucun phase_responses.parquet tant que vous ne l'activez pas, et l'export signale les réponses laissées de côté dans un avertissement.

Options d'export

Passez les options de format avec --option key=value, en répétant l'option pour chacune :

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
OptionPar défautDescription
compressionsnappyCodec de compression : snappy, gzip, zstd, lz4 ou none
include_itemstrueÉcrit items.parquet avec les données source
include_spanstrueÉcrit spans.parquet, quand des annotations d'empans existent
row_group_sizeValeur par défaut de PyArrowTaille des groupes de lignes pour annotations.parquet

snappy est le bon choix par défaut pour la plupart des projets, car il décompresse assez vite pour que les lectures restent interactives. Choisissez zstd quand le jeu de données est assez gros pour que la taille des fichiers compte, car il compresse nettement mieux à une vitesse comparable.

Charger des données 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 fournit aussi un exportateur huggingface qui construit directement les mêmes splits, ce qui évite les fichiers intermédiaires quand la destination est le 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)

Exemple complet

Un projet d'annotation d'empans qui garde JSONL comme format de travail et écrit Parquet à côté pour l'analyse :

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

Après l'annotation, c'est la table des empans qui contient les décalages des entités :

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

Pour aller plus loin

Pour les détails d'implémentation, consultez la documentation source.