Exportación Parquet
Exporta anotaciones de Potato a Apache Parquet, un formato columnar para análisis a gran escala con pandas, DuckDB, Spark, Polars y Hugging Face Datasets.
Nuevo en v2.3.0
Apache Parquet es un formato de almacenamiento columnar pensado para cargas de trabajo analíticas. En conjuntos de datos de anotación grandes comprime mejor que JSON o CSV, lee un subconjunto de columnas sin tocar el resto del archivo y se carga de forma nativa en pandas, DuckDB, PyArrow, Spark, Polars y Hugging Face Datasets.
La exportación Parquet necesita PyArrow:
pip install "pyarrow>=12.0.0"Ejecutar la exportación
Parquet es uno de los formatos de exportación registrados de Potato, así que la CLI de exportación lo escribe directamente:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/Potato lee las anotaciones registradas en el output_annotation_dir de la configuración y escribe las tablas Parquet en el directorio de salida que indiques.
Exportar de forma programada
En un estudio de larga duración normalmente se quiere una instantánea en Parquet sin depender de que alguien se acuerde de ejecutar la CLI. Con export_annotation_format, Potato escribe la exportación periódicamente mientras la anotación está en curso:
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: 300Las exportaciones automáticas se guardan en <output_annotation_dir>/exports/<format>/, por lo que las tablas Parquet aparecen en output/exports/parquet/. Las anotaciones en sí se almacenan siempre como <output_annotation_dir>/<user>/user_state.json, independientemente de este ajuste.
La clave antigua output_annotation_format está obsoleta. Potato la lee como export_annotation_format y registra una advertencia, así que cámbiale el nombre.
Archivos de salida
Una exportación produce hasta cuatro tablas, cada una de las cuales cubre un nivel distinto de los datos.
annotations.parquet
Una fila por cada par (instancia, anotador), con los esquemas aplanados en columnas.
| Columna | Tipo | Descripción |
|---|---|---|
instance_id | string | El ID del ítem anotado |
user_id | string | El ID del anotador |
| <schema_name> | varía | Una columna por cada esquema de anotación |
Los tipos de columna siguen el tipo de anotación: radio y select pasan a ser cadenas con la etiqueta seleccionada, likert, slider y number pasan a ser float64 (o cadena en una escala con etiquetas con nombre), multiselect pasa a ser una lista de cadenas y text pasa a ser una cadena.
Un esquema que almacena varias respuestas a la vez recibe una columna por cada subrespuesta, con el nombre <schema>.<key>. Un esquema multirate llamado handling produce handling.Urgency y handling.Customer tone; constant_sum y soft_label se reparten de la misma manera. Una selección única se queda en una sola columna, porque lo que resulta útil en un dataframe es que severity contenga "Serious".
Todas las columnas aparecen en todas las filas, con null donde un anotador no respondió, de modo que un esquema al que solo llegaron algunos anotadores sigue presente en el archivo.
spans.parquet
Una fila por cada anotación de span, que se escribe cuando el estudio tiene esquemas de span.
| Columna | Tipo | Descripción |
|---|---|---|
instance_id | string | El ID del ítem anotado |
user_id | string | El ID del anotador |
schema_name | string | Nombre del esquema de span |
start | int | Desplazamiento de carácter donde empieza el span |
end | int | Desplazamiento de carácter donde termina el span |
label | string | La etiqueta del span |
text | string | El contenido textual del span |
target_field | string | En qué campo de visualización se trazó el span, "" en una página de un solo campo |
title | string | El título visible de la etiqueta |
span_id | string | El id del span almacenado, schema_label_start_end |
format_coords | string | JSON: página y recuadro para un span de PDF, fila y columna para uno de hoja de cálculo |
additional_parts | string | JSON: los demás rangos de un span discontinuo |
kb_id, kb_source, kb_label | string | La entidad a la que apunta un span enlazado |
Lee target_field siempre que un estudio tenga más de un destino de span. Un mismo esquema de span registra en todos los campos de destino de span de la página, así que el campo varía dentro de un esquema, y span_id no permite recuperarlo: las mismas palabras marcadas en dos campos producen filas idénticas en todo lo demás.
text se deriva de los desplazamientos en el momento de la exportación en lugar de almacenarse, así que nunca puede contradecirlos.
items.parquet
Una fila por cada ítem de origen, con los datos originales.
| Columna | Tipo | Descripción |
|---|---|---|
item_id | string | El ID del ítem |
| <field_name> | varía | Una columna por cada campo de los datos originales (los diccionarios y listas anidados se serializan en JSON) |
phase_responses.parquet
Se escribe cuando el estudio recogió datos de fase, como un formulario de consentimiento o una encuesta posterior al estudio, y export_include_phase_data es true. La clave vale false por defecto, así que un estudio que recogió el consentimiento y un cuestionario posterior no obtiene ningún phase_responses.parquet hasta que la actives, y la exportación indica en una advertencia las respuestas que quedaron fuera.
Opciones de exportación
Pasa las opciones de formato con --option key=value, repitiendo la opción una vez por cada una:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| Opción | Valor por defecto | Descripción |
|---|---|---|
compression | snappy | Códec de compresión: snappy, gzip, zstd, lz4 o none |
include_items | true | Escribe items.parquet con los datos de origen |
include_spans | true | Escribe spans.parquet, cuando existen anotaciones de span |
row_group_size | Valor por defecto de PyArrow | Tamaño del grupo de filas para annotations.parquet |
snappy es el valor por defecto adecuado para la mayoría de los proyectos, porque descomprime lo bastante rápido como para que las lecturas sigan siendo interactivas. Elige zstd cuando el conjunto de datos sea tan grande que el tamaño del archivo importe, ya que comprime bastante mejor a una velocidad similar.
Cargar datos 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 también incluye un exportador huggingface que construye las mismas particiones directamente, lo que evita los archivos intermedios cuando el destino es el 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)Ejemplo completo
Un proyecto de anotación de spans que mantiene JSONL como formato de trabajo y escribe Parquet junto a él para el análisis:
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: trueTras la anotación, la tabla de spans es la que contiene los desplazamientos de las entidades:
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())Lecturas adicionales
- Formatos de Exportación: COCO, YOLO, CoNLL y los demás exportadores registrados
- Fuentes de Datos Remotas: carga de datos desde almacenamiento en la nube
- Panel de Administración: seguimiento del progreso de la anotación
Para detalles de implementación, consulta la documentación fuente.