Skip to content

تصدير 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:

bash
pip install "pyarrow>=12.0.0"

تشغيل التصدير

Parquet أحد تنسيقات التصدير المسجّلة في Potato، لذا تكتبه أداة سطر أوامر التصدير مباشرة:

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

يقرأ Potato التعليقات التوضيحية المسجّلة ضمن output_annotation_dir في ملف التهيئة، ويكتب جداول Parquet في مجلد الإخراج الذي تحدده.

التصدير وفق جدول زمني

الدراسة الطويلة تحتاج عادة إلى لقطة Parquet دون الاعتماد على أن يتذكر أحد تشغيل سطر الأوامر. عند ضبط export_annotation_format، يكتب Potato التصدير دوريًا أثناء سير التعليق التوضيحي:

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

تُحفظ عمليات التصدير التلقائي في <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_idstringمعرّف العنصر المعلَّق عليه
user_idstringمعرّف المعلّق
<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_idstringمعرّف العنصر المعلَّق عليه
user_idstringمعرّف المعلّق
schema_namestringاسم مخطط النطاقات
startintإزاحة الحرف الذي يبدأ عنده النطاق
endintإزاحة الحرف الذي ينتهي عنده النطاق
labelstringتسمية النطاق
textstringالمحتوى النصي للنطاق
target_fieldstringحقل العرض الذي رُسم فيه النطاق، و"" في صفحة ذات حقل واحد
titlestringالعنوان المعروض للتسمية
span_idstringمعرّف النطاق المخزَّن، schema_label_start_end
format_coordsstringJSON: الصفحة والمربع لنطاق في PDF، والصف والعمود لنطاق في جدول بيانات
additional_partsstringJSON: المقاطع الأخرى لنطاق غير متصل
kb_id, kb_source, kb_labelstringالكيان الذي يشير إليه نطاق مرتبط

اقرأ target_field كلما كان للدراسة أكثر من هدف نطاقات. مخطط النطاقات الواحد يسجّل في كل حقل من حقول أهداف النطاقات في الصفحة، لذا يختلف الحقل داخل المخطط الواحد، ولا يمكن استرجاعه من span_id: الكلمات نفسها المعلَّمة في حقلين تنتج صفوفًا متطابقة في كل ما عدا ذلك.

يُشتق text من الإزاحات وقت التصدير بدلًا من تخزينه، لذا لا يمكن أن يتعارض معها أبدًا.

items.parquet

صف واحد لكل عنصر مصدر، يحمل البيانات الأصلية.

العمودالنوعالوصف
item_idstringمعرّف العنصر
<field_name>يختلفعمود واحد لكل حقل في البيانات الأصلية (تُسلسَل القواميس والقوائم المتداخلة بصيغة JSON)

phase_responses.parquet

يُكتب عندما تكون الدراسة قد جمعت بيانات مراحل، مثل نموذج موافقة أو استبيان بعد الدراسة، ويكون export_include_phase_data مضبوطًا على true. القيمة الافتراضية للمفتاح هي false، لذا فالدراسة التي جمعت الموافقة واستبيانًا بعد الدراسة لا تحصل على أي phase_responses.parquet حتى تفعّله، ويسرد التصدير الإجابات المستبعدة في تحذير.

خيارات التصدير

مرّر خيارات التنسيق عبر --option key=value، مع تكرار الخيار لكل واحد منها:

bash
python -m potato.export -c config.yaml -f parquet -o ./parquet_export/ \
    --option compression=zstd --option include_items=false
الخيارالقيمة الافتراضيةالوصف
compressionsnappyترميز الضغط: snappy أو gzip أو zstd أو lz4 أو none
include_itemstrueكتابة items.parquet مع البيانات المصدر
include_spanstrueكتابة spans.parquet عند وجود تعليقات توضيحية على نطاقات
row_group_sizeالقيمة الافتراضية في PyArrowحجم مجموعة الصفوف في annotations.parquet

snappy هو الخيار الافتراضي المناسب لمعظم المشاريع، لأنه يفك الضغط بسرعة كافية لتبقى القراءة تفاعلية. اختر zstd عندما تكون مجموعة البيانات كبيرة بما يكفي ليصبح حجم الملف مهمًا، لأنه يضغط أفضل بكثير بسرعة مماثلة.

تحميل بيانات 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 أيضًا مُصدِّر huggingface يبني الأقسام نفسها مباشرة، مما يغني عن الملفات الوسيطة عندما تكون الوجهة هي 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)

مثال كامل

مشروع تعليق توضيحي على النطاقات يُبقي JSONL تنسيقًا للعمل ويكتب Parquet إلى جانبه للتحليل:

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

بعد التعليق التوضيحي، جدول النطاقات هو الذي يحمل إزاحات الكيانات:

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

قراءات إضافية

للاطلاع على تفاصيل التنفيذ، راجع الوثائق المصدرية.