Parquet 导出
将标注导出为 Apache Parquet 格式,用于高效的大规模数据处理。
Parquet 导出
v2.3.0 新增
Apache Parquet 是一种面向分析工作负载优化的列式存储格式。对于大型标注数据集,它相比 JSON 和 CSV 具有显著优势:更小的文件大小(通常 5-10 倍压缩)、列子集查询更快的读取速度,以及几乎所有数据科学工具(pandas、DuckDB、PyArrow、Spark、Polars、Hugging Face Datasets)的原生支持。
Potato 可以直接将标注导出为 Parquet 格式,生成三个结构化文件,覆盖所有标注类型。
启用 Parquet 导出
作为主输出格式
yaml
output_annotation_dir: "output/"
output_annotation_format: "parquet"作为辅助导出(保留 JSON 为主格式)
yaml
output_annotation_dir: "output/"
output_annotation_format: "jsonl"
parquet_export:
enabled: true
output_dir: "output/parquet/"
auto_export: true # export after each annotation session通过 CLI 按需导出
bash
python -m potato.export parquet --config config.yaml --output ./parquet_output/输出文件
Parquet 导出生成三个文件,分别代表标注数据的不同层级。
1. annotations.parquet
主输出文件。每行对应一个(实例、标注者、方案)组合。
| 列名 | 类型 | 描述 |
|---|---|---|
instance_id | string | 实例标识符 |
annotator | string | 标注者用户名 |
schema_name | string | 标注方案名称 |
value | string | 标注值(复杂类型使用 JSON 编码) |
timestamp | timestamp | 标注创建时间 |
duration_ms | int64 | 此实例所花时间(毫秒) |
session_id | string | 标注会话标识符 |
对于简单标注类型(radio、likert、text),value 包含原始值。对于复杂类型(multiselect、spans、events),value 包含 JSON 字符串。
2. spans.parquet
用于基于 span 的标注类型(span、span_link、event_annotation、coreference)。每行对应一个标注的 span。
| 列名 | 类型 | 描述 |
|---|---|---|
instance_id | string | 实例标识符 |
annotator | string | 标注者用户名 |
schema_name | string | 标注方案名称 |
span_id | string | 唯一 span 标识符 |
text | string | Span 文本内容 |
start_offset | int32 | 字符起始偏移量 |
end_offset | int32 | 字符结束偏移量 |
label | string | Span 标签 |
field | string | 源字段(用于多字段 span 标注) |
links | string | JSON 编码的链接数据(用于 span_link) |
attributes | string | JSON 编码的附加属性 |
3. items.parquet
数据集中每个实例的元数据。每行对应一个实例。
| 列名 | 类型 | 描述 |
|---|---|---|
instance_id | string | 实例标识符 |
text | string | 主要文本内容 |
annotation_count | int32 | 已收到的标注数量 |
annotators | string | 标注者用户名的 JSON 列表 |
status | string | 实例状态(pending、in_progress、complete) |
metadata | string | JSON 编码的实例元数据 |
压缩选项
yaml
parquet_export:
enabled: true
output_dir: "output/parquet/"
compression: snappy # snappy (default), gzip, zstd, lz4, brotli, none
row_group_size: 50000 # rows per row group (affects read performance)
use_dictionary: true # dictionary encoding for string columns
write_statistics: true # column statistics for query optimization压缩对比
| 算法 | 压缩比 | 写入速度 | 读取速度 | 最适用场景 |
|---|---|---|---|---|
snappy | 中等 | 快 | 快 | 通用(默认) |
gzip | 高 | 慢 | 中等 | 归档、小文件 |
zstd | 高 | 快 | 快 | 大小和速度的最佳平衡 |
lz4 | 低 | 非常快 | 非常快 | 速度敏感的工作负载 |
brotli | 非常高 | 非常慢 | 中等 | 最大压缩 |
none | 无 | 最快 | 最快 | 调试 |
对于大多数标注项目,默认的 snappy 压缩是不错的选择。对于文件大小重要的大型数据集,使用 zstd。
加载 Parquet 数据
pandas
python
import pandas as pd
annotations = pd.read_parquet("output/parquet/annotations.parquet")
spans = pd.read_parquet("output/parquet/spans.parquet")
items = pd.read_parquet("output/parquet/items.parquet")
# Filter to a specific schema
sentiment = annotations[annotations["schema_name"] == "sentiment"]
# Compute inter-annotator agreement
from sklearn.metrics import cohen_kappa_score
pivot = sentiment.pivot(index="instance_id", columns="annotator", values="value")
kappa = cohen_kappa_score(pivot.iloc[:, 0], pivot.iloc[:, 1])DuckDB
sql
-- Direct query without loading into memory
SELECT instance_id, value, COUNT(*) as annotator_count
FROM 'output/parquet/annotations.parquet'
WHERE schema_name = 'sentiment'
GROUP BY instance_id, value
ORDER BY annotator_count DESC;
-- Join annotations with items
SELECT a.instance_id, i.text, a.value, a.annotator
FROM 'output/parquet/annotations.parquet' a
JOIN 'output/parquet/items.parquet' i
ON a.instance_id = i.instance_id
WHERE a.schema_name = 'sentiment';PyArrow
python
import pyarrow.parquet as pq
# Read specific columns only (fast for wide tables)
table = pq.read_table(
"output/parquet/annotations.parquet",
columns=["instance_id", "value", "annotator"]
)
# Convert to pandas
df = table.to_pandas()
# Read with row group filtering
parquet_file = pq.ParquetFile("output/parquet/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
# Load directly from Parquet files
dataset = load_dataset("parquet", data_files={
"annotations": "output/parquet/annotations.parquet",
"spans": "output/parquet/spans.parquet",
"items": "output/parquet/items.parquet",
})
# Access as a regular HF dataset
print(dataset["annotations"][0])
# Push to Hugging Face Hub
dataset["annotations"].push_to_hub("my-org/my-annotations", split="train")Polars
python
import polars as pl
annotations = pl.read_parquet("output/parquet/annotations.parquet")
# Fast aggregation
label_counts = (
annotations
.filter(pl.col("schema_name") == "sentiment")
.group_by("value")
.agg(pl.count().alias("count"))
.sort("count", descending=True)
)
print(label_counts)增量导出
对于长期运行的标注项目,启用增量导出以避免每次重新导出整个数据集:
yaml
parquet_export:
enabled: true
output_dir: "output/parquet/"
incremental: true
partition_by: date # date, annotator, or none使用 partition_by: date 时,Parquet 文件被组织到按日期分区的目录中:
text
output/parquet/
annotations/
date=2026-03-01/part-0.parquet
date=2026-03-02/part-0.parquet
date=2026-03-03/part-0.parquet
spans/
date=2026-03-01/part-0.parquet
items/
part-0.parquet
分区数据集可以被所有主要工具作为单个逻辑表读取:
python
# pandas reads partitioned directories automatically
df = pd.read_parquet("output/parquet/annotations/")
# DuckDB handles partitions natively
# SELECT * FROM 'output/parquet/annotations/**/*.parquet'配置参考
yaml
parquet_export:
enabled: true
output_dir: "output/parquet/"
# When to export
auto_export: true # export after each session (default: false)
export_on_shutdown: true # export when server stops (default: true)
# File settings
compression: snappy
row_group_size: 50000
use_dictionary: true
write_statistics: true
# Incremental settings
incremental: false
partition_by: none # none, date, annotator
# Schema-specific options
flatten_complex_types: false # flatten JSON values into columns
include_raw_json: true # include raw JSON alongside flattened columns
# Span export
export_spans: true # generate spans.parquet
export_items: true # generate items.parquet完整示例
yaml
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
labels:
- name: PERSON
color: "#3b82f6"
- name: ORGANIZATION
color: "#22c55e"
- name: LOCATION
color: "#f59e0b"
output_annotation_dir: "output/"
output_annotation_format: "jsonl"
parquet_export:
enabled: true
output_dir: "output/parquet/"
compression: zstd
auto_export: true
export_spans: true
export_items: true标注完成后,加载并分析:
python
import pandas as pd
spans = pd.read_parquet("output/parquet/spans.parquet")
# Entity type distribution
print(spans["label"].value_counts())
# Average span length by type
spans["length"] = spans["end_offset"] - spans["start_offset"]
print(spans.groupby("label")["length"].mean())延伸阅读
有关实现详情,请参阅源文档。