عندما يتعلق الأمر بتصفية البيانات من ملفات Parquet باستخدام pandas، يمكن استخدام عدة استراتيجيات. وفي حين أنه من المعتاد الاعتراف بأن تقسيم البيانات يمكن أن يحسن بشكل كبير كفاءة في عمليات التصفية، هناك طرق إضافية لتحسين أداء استعلامات البيانات المخزنة في ملفات Parquet. التقسيم هو مجرد إحدى الخيارات.
التصفية حسب الحقول المقسمة
كما ذكرنا، هذا الأسلوب ليس فقط الأكثر دراية ولكن أيضاً عادة ما يكون الأكثر تأثيراً من حيث تحسين الأداء. التفسير وراء هذا سهل. عندما يتم استخدام القسم، يصبح ممكنًا تجاهل الحاجة إلى قراءة ملفات كاملة أو حتى دلائل كاملة من الملفات (المعروفة باسم دفع المسودة)، مما يؤدي إلى تحسين كبير ومثير في الأداء.
import pandas as pd
import time
from faker import Faker
fake = Faker()
MIL=1000000
NUM_OF_RECORDS=10*MIL
FOLDER="/tmp/out/"
PARTITIONED_PATH=f"{FOLDER}partitioned_{NUM_OF_RECORDS}/"
NON_PARTITIONED_PATH_PREFIX=f"{FOLDER}non_partitioned_{NUM_OF_RECORDS}.parquet"
print(f"Creating fake data")
data = {
'id': range(NUM_OF_RECORDS), # Generate IDs from 1 to 100
'name': [fake.name() for _ in range(NUM_OF_RECORDS)],
'age': [fake.random_int(min=18, max=99) for _ in range(NUM_OF_RECORDS)],
'state': [fake.state() for _ in range(NUM_OF_RECORDS)],
'city': [fake.city() for _ in range(NUM_OF_RECORDS)],
'street': [fake.street_address() for _ in range(NUM_OF_RECORDS)]
}
df = pd.DataFrame(data)
# الكتابة بدون تقسيم
df.to_parquet(path=NON_PARTITIONED_PATH)
# الكتابة في البيانات المقسمة
df.to_parquet(path=PARTITIONED_PATH, partition_cols=['state'])
# قراءة الغير مقسمة
start_time = time.time()
df1 = pd.read_parquet(path=NON_PARTITIONED_PATH)
df1 = df1[df1['state']=='California']
runtime1 = (time.time()) - start_time # 37 sec
# قراءة البيانات المقسمة
start_time = time.time()
df2 = pd.read_parquet(path=PARTITIONED_PATH, filters=[('state','==','California')])
runtime2 = (time.time()) - start_time # 0.20 sec
تحسن الوقت (جنباً إلى جنب مع استخدام أقل للذاكرة واستخدام CPU) كبير، إنخفض من 37 ثانية إلى 0.20 ثانية فقط.
التصفية حسب الحقول غير المقسمة
في المثال أعلاه، رأينا كيف يمكن أن تحسن التصفية على أساس حقل مقسمة جلب البيانات. ومع ذلك، هناك حالات حيث لا يمكن تقسيم البيانات بشكل فعال حسب الحقل المحدد الذي نرغب في تصفيته. علاوة على ذلك، في بعض الحالات، يتطلب التصفية على أساس حقول متعددة. هذا يعني أن جميع ملفات الإدخال ستُفتح، وهو ما يمكن أن يكون مضرًا للأداء.
بفضل، يوفر Parquet حلًا ذكيًا للتصدي لهذه المشكلة. تنقسم ملفات Parquet إلى مجموعات صفية. داخل كل مجموعة صفية، يخزن Parquet بيانات ما قبلية. تشمل هذه البيانات الما قبلية القيم الصغرى والعظمى لكل حقل.
عند كتابة ملفات Parquet باستخدام Pandas، يمكنك اختيار عدد السجلات التي سيحتويها كل مجموعة تحكم.
عند استخدام Pandas لقراءة ملفات Parquet مع المرشحات، تستفيد مكتبة Pandas من هذه البيانات الما قبلية في Parquet لتصفية البيانات بكفاءة عند تحميلها إلى الذاكرة. إذا كان الحقل المرغوب خارج النطاق الصغرى/العظمى لمجموعة صف، يتم تخطي تلك المجموعة الصفية بكل خفة.
df = pd.DataFrame(data)
# كتابة بيانات غير مقسمة، مع تحديد حجم مجموعة الصف
df.to_parquet(path=PATH_TO_PARQUET_FILE, row_group_size=1000000)
# قراءة بيانات غير مقسمة وتصفية حسب مجموعات الصف فقط
df = pd.read_parquet(path=DATASET_PATH, filters=[('state','==','California')])
يمكن مشاهدة البيانات الما قبلية داخل ملفات Parquet باستخدام PyArrow
.
>>> import pyarrow.parquet as pq
>>> parquet_file = pq.ParquetFile(PATH_TO_PARQUET_FILE)
>>> parquet_file.metadata
<pyarrow._parquet.FileMetaData object at 0x125b21220>
created_by: parquet-cpp-arrow version 11.0.0
num_columns: 6
num_rows: 1000000
num_row_groups: 10
format_version: 2.6
serialized_size: 9325
>>> parquet_file.metadata.row_group(0).column(3)
<pyarrow._parquet.ColumnChunkMetaData object at 0x125b5b180>
file_offset: 1675616
file_path:
physical_type: BYTE_ARRAY
num_values: 100000
path_in_schema: state
is_stats_set: True
statistics:
<pyarrow._parquet.Statistics object at 0x115283590>
has_min_max: True
min: Alabama
max: Wyoming
null_count: 0
distinct_count: 0
num_values: 100000
physical_type: BYTE_ARRAY
logical_type: String
converted_type (legacy): UTF8
compression: SNAPPY
encodings: ('RLE_DICTIONARY', 'PLAIN', 'RLE')
has_dictionary_page: True
dictionary_page_offset: 1599792
data_page_offset: 1600354
total_compressed_size: 75824
total_uncompressed_size: 75891
لاحظ أن عدد مجموعات الصف مذكور في بيانات ما قبلية الملف بأكمله، والقيم الصغرى والعظمى مذكورة في قسم الإحصائيات لكل عمود لكل مجموعة صف.
ومع ذلك، هناك طريقة لتحسين هذه الميزة في Parquet أكثر من ذلك: الفرز.
تصفية حسب الحقول المرتبة
كما ذكر في القسم السابق، جزء من البيانات الما قبلية التي يخزنها Parquet تشمل القيم الصغرى والعظمى لكل حقل داخل كل مجموعة صف. عندما يتم فرز البيانات بناءً على الحقل الذي نعتزم تصفيته، يكون لدى Pandas احتمال أكبر لتخطي المزيد من مجموعات الصف.
على سبيل المثال، لنفترض وجود مجموعة بيانات تشمل قائمة بالسجلات، حيث يمثل أحد الحقول “الولاية”. إذا كانت السجلات غير مصنفة، هناك احتمال كبير أن كل ولاية تظهر في معظم مجموعات الصفوف. على سبيل المثال، انظر إلى بيانات الوصف في القسم السابق. يمكنك أن ترى أن مجموعة الصف الأولى وحدها تحمل جميع الولايات من “ألاباما” إلى “وايومينغ”.
ومع ذلك، إذا قمنا بفرز البيانات بناءً على حقل “الولاية”، هناك احتمال كبير لتخطي العديد من مجموعات الصفوف.
df = pd.DataFrame(data)
# فرز البيانات بناءً على "الولاية"
df.sort_values("state").to_parquet(path=NON_PARTITIONED_SORTED_PATH)
الآن، دعونا نلقي نظرة ثانية على بيانات الوصف ونرى كيف تغيرت.
>>> parquet_file = pq.ParquetFile(PATH_TO_PARQUET_FILE)
>>> parquet_file.metadata.row_group(0).column(3).statistics.min
'Alabama'
>>> parquet_file.metadata.row_group(0).column(3).statistics.max
'Kentucky'
>>> parquet_file.metadata.row_group(1).column(3).statistics.min
'Kentucky'
>>> parquet_file.metadata.row_group(1).column(3).statistics.max
'North Dakota'
>>> parquet_file.metadata.row_group(2).column(3).statistics.min
'North Dakota'
>>> parquet_file.metadata.row_group(2).column(3).statistics.max
'Wyoming'
كما ترى، بعد فرزها حسب الولاية، تأثرت قيم min-max وفقًا لذلك؛ كل مجموعة صفوف تحمل جزءًا من الولايات بدلاً من جميع الولايات. هذا يعني أن القراءة باستخدام الفلترات الآن يجب أن تكون أسرع بكثير.
الآن، دعونا نرى كيف يؤثر ذلك على أداء قراءة البيانات. لم يتغير الكود لقراءة البيانات.
# قراءة البيانات غير المقسمة وتصفيتها حسب مجموعات الصفوف، المدخل مصنف حسب الولاية
start_time = time.time()
df = pd.read_parquet(path=DATASET_PATH, filters=[('state','==','California')])
runtime = (time.time()) - start_time # 0.24 seconds
مذهل، الأداء هنا تقريبًا بنفس جودة استخدام التقسيمات.
ينطبق هذا المبدأ على البيانات المقسمة وغير المقسمة على حد سواء. يمكننا استخدام كلا الطريقتين في نفس الوقت. إذا أردنا في بعض الأحيان تصفية البيانات بناءً على الحقل A وفي أوقات أخرى بناءً على الحقل B، فإن التقسيم حسب الحقل A والفرز حسب الحقل B يمكن أن يكون خيارًا جيدًا.
في حالات أخرى، على سبيل المثال، حيث يكون الحقل الذي نرغب في تصفيته حقلًا ذو عدد كبير من القيم المميزة، يمكننا التقسيم حسب بعض التجزئة (التصغير) للقيمة وفرز البيانات داخلها حسب القيمة الفعلية للحقل، بهذه الطريقة نستمتع بمزايا كلا الطريقتين – التقسيم والمجموعات الصفوف.
قراءة جزء من الأعمدة
على الرغم من استخدامها أقل شيوعًا، فإن طريقة أخرى لتحقيق نتائج أفضل خلال جلب البيانات تتضمن اختيار الحقول المحددة التي تكون ضرورية لمهمتك. قد يؤدي هذا الاستراتيجية في بعض الأحيان إلى تحسينات في الأداء. ويرجع ذلك إلى طبيعة التنسيق Parquet. تم تنفيذ Parquet بتنسيق عمودي، مما يعني إنتاجها بيانات عمودية داخل كل مجموعة صفوف. قراءة بعض الأعمدة فقط يعني تجاهل الأعمدة الأخرى.
start_time = time.time()
df = pd.read_parquet(path=NON_PARTITIONED_SORTED_PATH, columns=["name", "state"])
runtime = (time.time()) - start_time # 0.08 seconds
ليس من المستغرب أن التحسن في الأداء كبير.
الخاتمة
على الرغم من أن تقسيم البيانات عادة ما يكون الطريقة الأمثل، إلا أنه ليس ممكنًا دائمًا. يمكن أن يؤدي فرز البيانات إلى تحسينات كبيرة. قد نتجاهل المزيد من مجموعات الصفوف هذه. بالإضافة إلى ذلك، إذا كان ذلك ممكنًا، فإن اختيار الأعمدة الضرورية فقط هو دائمًا خيار جيد.
ساعدك هذا المنشور على فهم كيفية استغلال قوة Parquet وpandas لتحسين الأداء.
هنا مخرج البرنامج يحتوي على جميع الأمثلة المذكورة مسبقًا بالإضافة إلى مقارنات الزمن.
Source:
https://dzone.com/articles/parquet-data-filtering-with-pandas