Skip to content

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:

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

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

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

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

ColumnaTipoDescripción
instance_idstringEl ID del ítem anotado
user_idstringEl ID del anotador
<schema_name>varíaUna 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.

ColumnaTipoDescripción
instance_idstringEl ID del ítem anotado
user_idstringEl ID del anotador
schema_namestringNombre del esquema de span
startintDesplazamiento de carácter donde empieza el span
endintDesplazamiento de carácter donde termina el span
labelstringLa etiqueta del span
textstringEl contenido textual del span
target_fieldstringEn qué campo de visualización se trazó el span, "" en una página de un solo campo
titlestringEl título visible de la etiqueta
span_idstringEl id del span almacenado, schema_label_start_end
format_coordsstringJSON: página y recuadro para un span de PDF, fila y columna para uno de hoja de cálculo
additional_partsstringJSON: los demás rangos de un span discontinuo
kb_id, kb_source, kb_labelstringLa 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.

ColumnaTipoDescripción
item_idstringEl ID del ítem
<field_name>varíaUna 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:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
OpciónValor por defectoDescripción
compressionsnappyCódec de compresión: snappy, gzip, zstd, lz4 o none
include_itemstrueEscribe items.parquet con los datos de origen
include_spanstrueEscribe spans.parquet, cuando existen anotaciones de span
row_group_sizeValor por defecto de PyArrowTamañ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

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

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)

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:

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

Tras la anotación, la tabla de spans es la que contiene los desplazamientos de las entidades:

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

Lecturas adicionales

Para detalles de implementación, consulta la documentación fuente.