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:
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:
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:
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: 300As 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.
| Coluna | Tipo | Descrição |
|---|---|---|
instance_id | string | O ID do item anotado |
user_id | string | O ID do anotador |
| <schema_name> | varia | Uma 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.
| Coluna | Tipo | Descrição |
|---|---|---|
instance_id | string | O ID do item anotado |
user_id | string | O ID do anotador |
schema_name | string | Nome do esquema de span |
start | int | Deslocamento de caractere onde o span começa |
end | int | Deslocamento de caractere onde o span termina |
label | string | O rótulo do span |
text | string | O conteúdo textual do span |
target_field | string | Em qual campo de exibição o span foi marcado, "" em uma página com um único campo |
title | string | O título exibido do rótulo |
span_id | string | O id do span armazenado, schema_label_start_end |
format_coords | string | JSON: página e caixa para um span de PDF, linha e coluna para um de planilha |
additional_parts | string | JSON: os demais intervalos de um span descontínuo |
kb_id, kb_source, kb_label | string | A 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.
| Coluna | Tipo | Descrição |
|---|---|---|
item_id | string | O ID do item |
| <field_name> | varia | Uma 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:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| Opção | Padrão | Descrição |
|---|---|---|
compression | snappy | Codec de compressão: snappy, gzip, zstd, lz4 ou none |
include_items | true | Grava items.parquet com os dados de origem |
include_spans | true | Grava spans.parquet, quando existem anotações de span |
row_group_size | Padrão do PyArrow | Tamanho 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
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")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
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:
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: trueDepois da anotação, é a tabela de spans que traz os deslocamentos das 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())Leitura adicional
- Formatos de exportação: COCO, YOLO, CoNLL e os demais exportadores registrados
- Fontes de dados remotas: carregamento de dados a partir de armazenamento em nuvem
- Painel administrativo: acompanhamento do progresso da anotação
Para detalhes de implementação, consulte a documentação de origem.