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 :
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 :
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 :
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: 300Les 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.
| Colonne | Type | Description |
|---|---|---|
instance_id | string | L'identifiant de l'élément annoté |
user_id | string | L'identifiant de l'annotateur |
| <schema_name> | variable | Une 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.
| Colonne | Type | Description |
|---|---|---|
instance_id | string | L'identifiant de l'élément annoté |
user_id | string | L'identifiant de l'annotateur |
schema_name | string | Nom du schéma d'empans |
start | int | Décalage du caractère où commence l'empan |
end | int | Décalage du caractère où se termine l'empan |
label | string | L'étiquette de l'empan |
text | string | Le contenu textuel de l'empan |
target_field | string | Le champ d'affichage dans lequel l'empan a été tracé, "" sur une page à un seul champ |
title | string | Le titre affiché de l'étiquette |
span_id | string | L'id de l'empan stocké, schema_label_start_end |
format_coords | string | JSON : page et cadre pour un empan PDF, ligne et colonne pour un empan de tableur |
additional_parts | string | JSON : les autres plages d'un empan discontinu |
kb_id, kb_source, kb_label | string | L'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.
| Colonne | Type | Description |
|---|---|---|
item_id | string | L'identifiant de l'élément |
| <field_name> | variable | Une 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 :
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| Option | Par défaut | Description |
|---|---|---|
compression | snappy | Codec de compression : snappy, gzip, zstd, lz4 ou none |
include_items | true | Écrit items.parquet avec les données source |
include_spans | true | Écrit spans.parquet, quand des annotations d'empans existent |
row_group_size | Valeur par défaut de PyArrow | Taille 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
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 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
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 :
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: trueAprès l'annotation, c'est la table des empans qui contient les décalages des entités :
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
- Formats d'export : COCO, YOLO, CoNLL et les autres exportateurs enregistrés
- Sources de données distantes : charger des données depuis un stockage cloud
- Tableau de bord d'administration : suivre l'avancement de l'annotation
Pour les détails d'implémentation, consultez la documentation source.