تصدير Parquet
تصدير التعليقات التوضيحية من Potato إلى Apache Parquet، وهو تنسيق عمودي للتحليل على نطاق واسع باستخدام pandas وDuckDB وSpark وPolars وHugging Face Datasets.
جديد في الإصدار v2.3.0
Apache Parquet تنسيق تخزين عمودي مصمَّم لأحمال العمل التحليلية. في مجموعات بيانات التعليق التوضيحي الكبيرة، يضغط البيانات أفضل من JSON أو CSV، ويقرأ مجموعة فرعية من الأعمدة دون المساس ببقية الملف، ويُحمَّل مباشرة في pandas وDuckDB وPyArrow وSpark وPolars وHugging Face Datasets.
يحتاج تصدير Parquet إلى PyArrow:
pip install "pyarrow>=12.0.0"تشغيل التصدير
Parquet أحد تنسيقات التصدير المسجّلة في Potato، لذا تكتبه أداة سطر أوامر التصدير مباشرة:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/يقرأ Potato التعليقات التوضيحية المسجّلة ضمن output_annotation_dir في ملف التهيئة، ويكتب جداول Parquet في مجلد الإخراج الذي تحدده.
التصدير وفق جدول زمني
الدراسة الطويلة تحتاج عادة إلى لقطة Parquet دون الاعتماد على أن يتذكر أحد تشغيل سطر الأوامر. عند ضبط export_annotation_format، يكتب Potato التصدير دوريًا أثناء سير التعليق التوضيحي:
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تُحفظ عمليات التصدير التلقائي في <output_annotation_dir>/exports/<format>/، لذا تظهر جداول Parquet تحت output/exports/parquet/. أما التعليقات التوضيحية نفسها فتُخزَّن دائمًا في <output_annotation_dir>/<user>/user_state.json بغض النظر عن هذا الإعداد.
المفتاح الأقدم output_annotation_format مُهمَل. يقرؤه Potato على أنه export_annotation_format ويسجّل تحذيرًا، لذا أعد تسميته.
ملفات الإخراج
ينتج التصدير ما يصل إلى أربعة جداول، يغطي كل منها مستوى مختلفًا من البيانات.
annotations.parquet
صف واحد لكل زوج (حالة، معلّق)، مع تسطيح المخططات إلى أعمدة.
| العمود | النوع | الوصف |
|---|---|---|
instance_id | string | معرّف العنصر المعلَّق عليه |
user_id | string | معرّف المعلّق |
| <schema_name> | يختلف | عمود واحد لكل مخطط تعليق توضيحي |
تتبع أنواع الأعمدة نوع التعليق التوضيحي: يصبح radio وselect سلاسل نصية تحمل التسمية المختارة، ويصبح likert وslider وnumber من نوع float64 (أو سلسلة نصية في مقياس بتسميات مسمّاة)، ويصبح multiselect قائمة من السلاسل النصية، ويصبح text سلسلة نصية.
المخطط الذي يخزّن عدة إجابات في آن واحد يحصل على عمود لكل إجابة فرعية، باسم <schema>.<key>. مخطط multirate باسم handling ينتج handling.Urgency وhandling.Customer tone، ويتوزع constant_sum وsoft_label بالطريقة نفسها. أما الاختيار الفردي فيبقى في عمود واحد، لأن severity الذي يحمل القيمة "Serious" هو ما يمكن استخدامه في إطار البيانات (dataframe).
يظهر كل عمود في كل صف، ويُملأ بقيمة null حيث لم يُجب المعلّق، لذا يبقى المخطط الذي وصل إليه بعض المعلّقين فقط موجودًا في الملف.
spans.parquet
صف واحد لكل تعليق توضيحي على نطاق، ويُكتب عندما تتضمن الدراسة مخططات نطاقات.
| العمود | النوع | الوصف |
|---|---|---|
instance_id | string | معرّف العنصر المعلَّق عليه |
user_id | string | معرّف المعلّق |
schema_name | string | اسم مخطط النطاقات |
start | int | إزاحة الحرف الذي يبدأ عنده النطاق |
end | int | إزاحة الحرف الذي ينتهي عنده النطاق |
label | string | تسمية النطاق |
text | string | المحتوى النصي للنطاق |
target_field | string | حقل العرض الذي رُسم فيه النطاق، و"" في صفحة ذات حقل واحد |
title | string | العنوان المعروض للتسمية |
span_id | string | معرّف النطاق المخزَّن، schema_label_start_end |
format_coords | string | JSON: الصفحة والمربع لنطاق في PDF، والصف والعمود لنطاق في جدول بيانات |
additional_parts | string | JSON: المقاطع الأخرى لنطاق غير متصل |
kb_id, kb_source, kb_label | string | الكيان الذي يشير إليه نطاق مرتبط |
اقرأ target_field كلما كان للدراسة أكثر من هدف نطاقات. مخطط النطاقات الواحد يسجّل في كل حقل من حقول أهداف النطاقات في الصفحة، لذا يختلف الحقل داخل المخطط الواحد، ولا يمكن استرجاعه من span_id: الكلمات نفسها المعلَّمة في حقلين تنتج صفوفًا متطابقة في كل ما عدا ذلك.
يُشتق text من الإزاحات وقت التصدير بدلًا من تخزينه، لذا لا يمكن أن يتعارض معها أبدًا.
items.parquet
صف واحد لكل عنصر مصدر، يحمل البيانات الأصلية.
| العمود | النوع | الوصف |
|---|---|---|
item_id | string | معرّف العنصر |
| <field_name> | يختلف | عمود واحد لكل حقل في البيانات الأصلية (تُسلسَل القواميس والقوائم المتداخلة بصيغة JSON) |
phase_responses.parquet
يُكتب عندما تكون الدراسة قد جمعت بيانات مراحل، مثل نموذج موافقة أو استبيان بعد الدراسة، ويكون export_include_phase_data مضبوطًا على true. القيمة الافتراضية للمفتاح هي false، لذا فالدراسة التي جمعت الموافقة واستبيانًا بعد الدراسة لا تحصل على أي phase_responses.parquet حتى تفعّله، ويسرد التصدير الإجابات المستبعدة في تحذير.
خيارات التصدير
مرّر خيارات التنسيق عبر --option key=value، مع تكرار الخيار لكل واحد منها:
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
--option compression=zstd --option include_items=false| الخيار | القيمة الافتراضية | الوصف |
|---|---|---|
compression | snappy | ترميز الضغط: snappy أو gzip أو zstd أو lz4 أو none |
include_items | true | كتابة items.parquet مع البيانات المصدر |
include_spans | true | كتابة spans.parquet عند وجود تعليقات توضيحية على نطاقات |
row_group_size | القيمة الافتراضية في PyArrow | حجم مجموعة الصفوف في annotations.parquet |
snappy هو الخيار الافتراضي المناسب لمعظم المشاريع، لأنه يفك الضغط بسرعة كافية لتبقى القراءة تفاعلية. اختر zstd عندما تكون مجموعة البيانات كبيرة بما يكفي ليصبح حجم الملف مهمًا، لأنه يضغط أفضل بكثير بسرعة مماثلة.
تحميل بيانات 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 أيضًا مُصدِّر huggingface يبني الأقسام نفسها مباشرة، مما يغني عن الملفات الوسيطة عندما تكون الوجهة هي 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)مثال كامل
مشروع تعليق توضيحي على النطاقات يُبقي JSONL تنسيقًا للعمل ويكتب Parquet إلى جانبه للتحليل:
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بعد التعليق التوضيحي، جدول النطاقات هو الذي يحمل إزاحات الكيانات:
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())قراءات إضافية
- تنسيقات التصدير: COCO وYOLO وCoNLL وغيرها من المُصدِّرات المسجّلة
- مصادر البيانات البعيدة: تحميل البيانات من التخزين السحابي
- لوحة تحكم المشرف: متابعة تقدم التعليق التوضيحي
للاطلاع على تفاصيل التنفيذ، راجع الوثائق المصدرية.