Skip to content

Exportação para Parquet

Exporte anotações do Potato para o Apache Parquet, um formato colunar para análise em larga escala com pandas, DuckDB, Spark, Polars e Hugging Face Datasets.

Novidade na v2.3.0

O Apache Parquet é um formato de armazenamento colunar feito para cargas de trabalho analíticas. Em grandes conjuntos de dados de anotação, ele comprime melhor que JSON ou CSV, lê um subconjunto de colunas sem tocar no resto do arquivo e carrega nativamente no pandas, DuckDB, PyArrow, Spark, Polars e Hugging Face Datasets.

A exportação para Parquet precisa do PyArrow:

bash
pip install "pyarrow>=12.0.0"

Executando a exportação

O Parquet é um dos formatos de exportação registrados do Potato, então a CLI de exportação o grava diretamente:

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

O Potato lê as anotações registradas no output_annotation_dir da configuração e grava as tabelas Parquet no diretório de saída que você indicar.

Exportação periódica

Um estudo de longa duração geralmente precisa de um snapshot em Parquet sem depender de alguém lembrar de rodar a CLI. Com export_annotation_format definido, o Potato grava a exportação periodicamente enquanto a anotação está em andamento:

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

As exportações automáticas vão para <output_annotation_dir>/exports/<format>/, então as tabelas Parquet aparecem em output/exports/parquet/. As anotações em si são sempre armazenadas como <output_annotation_dir>/<user>/user_state.json, independentemente dessa configuração.

A chave antiga output_annotation_format está obsoleta. O Potato a lê como export_annotation_format e registra um aviso, então renomeie-a.

Arquivos de saída

Uma exportação gera até quatro tabelas, cada uma cobrindo um nível diferente dos dados.

annotations.parquet

Uma linha por par (instância, anotador), com os esquemas achatados em colunas.

ColunaTipoDescrição
instance_idstringO ID do item anotado
user_idstringO ID do anotador
<schema_name>variaUma coluna por esquema de anotação

Os tipos de coluna seguem o tipo de anotação: radio e select viram strings com o rótulo selecionado, likert, slider e number viram float64 (ou string, no caso de uma escala com rótulos nomeados), multiselect vira uma lista de strings e text vira uma string.

Um esquema que armazena várias respostas de uma vez recebe uma coluna por sub-resposta, com o nome <schema>.<key>. Um esquema multirate chamado handling gera handling.Urgency e handling.Customer tone; constant_sum e soft_label se distribuem da mesma forma. Uma seleção única fica em uma só coluna, porque o que serve em um dataframe é severity contendo "Serious".

Toda coluna aparece em toda linha, preenchida com null onde um anotador não respondeu, de modo que um esquema que só alguns anotadores alcançaram continua presente no arquivo.

spans.parquet

Uma linha por anotação de span, gravada quando o estudo tem esquemas de span.

ColunaTipoDescrição
instance_idstringO ID do item anotado
user_idstringO ID do anotador
schema_namestringNome do esquema de span
startintDeslocamento de caractere onde o span começa
endintDeslocamento de caractere onde o span termina
labelstringO rótulo do span
textstringO conteúdo textual do span
target_fieldstringEm qual campo de exibição o span foi marcado, "" em uma página com um único campo
titlestringO título exibido do rótulo
span_idstringO id do span armazenado, schema_label_start_end
format_coordsstringJSON: página e caixa para um span de PDF, linha e coluna para um de planilha
additional_partsstringJSON: os demais intervalos de um span descontínuo
kb_id, kb_source, kb_labelstringA entidade para a qual um span vinculado aponta

Leia target_field sempre que um estudo tiver mais de um alvo de span. Um único esquema de span registra em todos os campos-alvo de span da página, então o campo varia dentro de um esquema, e span_id não permite recuperá-lo: as mesmas palavras marcadas em dois campos produzem linhas idênticas em todo o resto.

text é derivado dos deslocamentos no momento da exportação em vez de ser armazenado, então nunca pode discordar deles.

items.parquet

Uma linha por item de origem, com os dados originais.

ColunaTipoDescrição
item_idstringO ID do item
<field_name>variaUma coluna por campo dos dados originais (dicionários e listas aninhados são serializados em JSON)

phase_responses.parquet

Gravado quando o estudo coletou dados de fase, como um formulário de consentimento ou um questionário pós-estudo, e export_include_phase_data é true. A chave tem false como padrão, então um estudo que coletou consentimento e um questionário pós-estudo não recebe nenhum phase_responses.parquet até que você a ative, e a exportação lista as respostas deixadas de fora em um aviso.

Opções de exportação

Passe opções de formato com --option key=value, repetindo a flag para cada uma:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
OpçãoPadrãoDescrição
compressionsnappyCodec de compressão: snappy, gzip, zstd, lz4 ou none
include_itemstrueGrava items.parquet com os dados de origem
include_spanstrueGrava spans.parquet, quando existem anotações de span
row_group_sizePadrão do PyArrowTamanho do row group para annotations.parquet

snappy é o padrão certo para a maioria dos projetos, porque descomprime rápido o bastante para que as leituras continuem interativas. Escolha zstd quando o conjunto de dados for grande o suficiente para que o tamanho do arquivo importe, já que ele comprime bem melhor em velocidade parecida.

Carregando dados em 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")

O Potato também inclui um exportador huggingface que monta os mesmos splits diretamente, o que evita os arquivos intermediários quando o destino é o 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)

Exemplo completo

Um projeto de anotação de spans que mantém JSONL como formato de trabalho e grava Parquet ao lado dele para análise:

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

Depois da anotação, é a tabela de spans que traz os deslocamentos das 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())

Leitura adicional

Para detalhes de implementação, consulte a documentação de origem.