CSV อื้ม... ใหญ่ไป เรามี Parquet

เวลาเราทำงานกับข้อมูลส่วนใหญ่ หรือ แม้กระทั่งไปเราโหลดข้อมูลจากเว็บต่าง ๆ เราก็จะได้ข้อมูลมาในรูปแบบของ CSV (Comma Seperated Values) สำหรับข้อมูลเล็ก ๆ มันก็ไม่น่าจะไม่มีปัญหาเท่าไหร่ เราจะ Query หรือ Reduce อะไรก็ไม่น่าใช่ปัญหา แต่ ถ้าข้อมูลของเราใหญ่ขึ้นละ นั่นแหละ การใช้ CSV มันเริ่มจะเป็นปัญหาแล้ว วันนี้เรามีของเล่นตัวนึงมาเพื่อจัดการกับปัญหาพวกนี้เลยคือ Parquet บอกเลยว่าใช้ไม่ยาก และโหดใช้ได้เลย

รู้จักกับ CSV กันก่อน

ก่อนที่เราจะไปรู้จักกับ Parquet เราอยากจะพามาทำความรู้จักกับไฟล์ที่เราใช้เก็บข้อมูลกันบ่อย ๆ อย่าง CSV กันก่อน อย่างที่เราบอก มันย่อมาจาก Comma Sperated Values สิ่งที่มันทำ มันก็ทำตามชื่อเลยคือ การคั่นค่า (Value) ด้วยเครื่องหมาย Comma ทำให้ลักษณะข้อของข้อมูลก็จะเป็น หลาย ๆ บรรทัด โดยที่แต่ละบรรทัด มันจะเป็น ข้อมูลใน Column นั้น ๆ และคั่นด้วย Comma เป็นอันจบ เราเรียกการเก็บข้อมูลแบบนี้ว่า Column-Based Storage

สิ่งที่ทำให้ CSV มันถูกนำมาใช้กันเยอะมาก ๆ นั่นเป็นเพราะ ความง่าย และความสะดวกของมัน ที่เราสามารถ อ่าน หรือ เขียนข้อมูลลง CSV File ได้ง่าย เคลื่อนย้ายสะดวก และ จัดการได้ง่าย แต่เพราะสิ่งที่ CSV เป็นนี่แหละ ทำให้ CSV ไปไม่รอดกับข้อมูลขนาดใหญ่ ๆ โดยเฉพาะข้อมูลที่มีจำนวนมาก ๆ และเราต้องการที่จะ Query มันขึ้นมาสักตัว

input_file = open('sample.csv', 'r')

results = []
line = input_file.readline()
headers = line.rstrip().split(',')
target_col_index = headers.index('target_column')

while (line := input_file.readline()) != '' :
    
    items = line.rstrip().split(',')
    results.append(items[target_col_index])


input_file.close()

วิธีที่เราจะดึงข้อมูลสัก 1 Column ขึ้นมา เราจะต้องไล่อ่านข้อมูลทีละ Record หรือก็คือ ทีละบรรทัด และ ตัดเฉพาะส่วนที่เราต้องการออกมา ทำให้การอ่าน CSV ขนาดใหญ่ ๆ สัก 1-2 Columns เป็นเรื่องที่มีราคาแพงมาก ใช้เวลาในการทำงานนานมาก ๆ ทำให้มันเป็นปัญหาของ CSV เลยก็ว่าได้

Parquet มาช่วยแล้ว !

Parquet เป็นรูปแบบของการเก็บข้อมูลแบบนึงที่อยู่ใน Apache สิ่งที่มันต่างจาก CSV อย่างสิ้นเชิงเลยคือ วิธีการเก็บข้อมูล มันใช้การเก็บข้อมูลแบบ Columns-based ถ้าเราบอกแค่นี้ หลาย ๆ คนก็อาจจะคิดว่า แล้วมันจะต่างกันยังไงละ แค่เปลี่ยนวิธีการเก็บเท่านั้นเอง สุดท้ายข้อมูลมันก็ต้องมีขนาดเท่ากันสิ มันจะมาช่วยเรื่องขนาดอะไรได้ยังไง

ก่อนเราจะไปคุยเรื่องขนาด เราอยากมาพูดถึงวิธีการเข้าถึงข้อมูลก่อน ด้วยความที่ Parquet มันเป็น Column-based Storage ทำให้เวลาเราจะเข้าถึงข้อมูลเป็น Column มันจะทำได้อย่างเร็วเลยละ เพราะมันไม่ต้องมานั่ง Loop แล้ว Filter ของที่เราไม่ต้องการออก แต่มันสามารถดูดสิ่งที่เราต้องการมาให้ได้เลยตรง ๆ ทำให้เร็วกว่า

ส่วนเรื่องขนาด บอกเลยว่า การทำ Columns-based ทำให้ขนาดต่างได้แน่นอน ย้อนกลับไปที่เรื่องของการทำ Data Compression ที่ถ้า Entropy ของข้อมูลยิ่งเยอะ ก็ทำให้เรามีโอกาสที่จะได้ Compression Ratio สูงมากขึ้นเท่านั้น ใช่แล้ว ใน Column เดียวกัน มันเป็นข้อมูลเรื่องเดียวกัน นั่นแปลว่า ค่ามันก็น่าจะมีความใกล้เคียงอะไรอยู่บ้างแหละ ทำให้มี Entropy ที่สูง สูงกว่าการเก็บพวก CSV เยอะมาก ๆ เลยละ เลยทำให้ Parquet ยังไง ๆ ก็มีขนาดที่เล็กกว่า CSV แน่นอน

Hand-on กับ Parquet

ในการทำงานกับ Parquet บน Python สามารถทำได้ง่ายมาก ๆ โดยเฉพาะถ้าเราทำงานกับ Pandas อยู่แล้ว เพราะใน Pandas มันมีคำสั่งสำหรับการอ่าน และ เขียน Parquet Format มาให้เราเลย เราไม่ต้องไปสนใจอะไรแล้ว เรามาลองด้วยการ สร้าง ข้อมูลมั่ว ๆ ขนาดใหญ่ ๆ ดูกันดีกว่า น่าจะทำให้เห็นได้ว่า ถ้าเราบันทึกเป็น CSV และ Parquet ขนาดมันจะต่างกันได้มากแค่ไหน

import pandas as pd
import random
import time

n = 1000000

sample_df = pd.DataFrame({
   'col_a' : [random.randint(0,10000) for i in range(n)],
   'col_b' : [random.randint(0,10000) for i in range(n)],
   'col_c' : [random.randint(0,10000) for i in range(n)],
   'col_d' : [random.randint(0,10000) for i in range(n)],
   'col_e' : [random.randint(0,10000) for i in range(n)],
   'col_f' : [random.randint(0,10000) for i in range(n)],
})

time_start = time.time()
sample_df.to_csv('sample.csv', index=False)
csv_elapsed = time.time() - time_start

time_start = time.time()
sample_df.to_parquet('sample.parquet', index=False)
parquet_elapsed = time.time() - time_start()

print('Saving CSV' , csv_elapsed)
print('Saving Parquet' , parquet_elapsed)
Saving CSV 2.129817247390747
Saving Parquet 0.1676945686340332

อันนี้เราลองรันใน Google Colab เราจะเห็นได้เลยว่า เวลาที่เราใช้ในการบันทึกข้อมูล เร็วมาก ๆ เร็วกว่า 1170.06% เลยทีเดียว ถือว่าโหดมาก ๆ ความเร็วในการเขียนเรียกได้ว่า ทำเอา CSV ร้องไห้กลับบ้านเลยทีเดียว

import os

print('CSV File Size', os.stat('sample.csv').st_size, 'Byte(s)')
print('Parquet File Size', os.stat('sample.parquet').st_size, 'Byte(s)')
CSV File Size 29336706 Byte(s)
Parquet File Size 10758017 Byte(s)

ถ้าเราลองมาดูขนาดของไฟล์ที่เขียนลงไปบ้าง ไม่ต้องดูอะไรมาก เราก็เห็นเลยตรง ๆ ว่า Parquet มีขนาดที่เล็กกว่าอย่างเห็นได้ชัดมาก ๆ ถ้าคิดเป็น Percentage ขนาดของไฟล์ Parquet เล็กกว่า CSV ถึง 63.33% เลย ถือว่าเยอะมาก ๆ ลองคิดว่า ถ้าเราคุยกับคนที่ถือข้อมูลขนาดสัก 1-10 TB ดูคือจะเห็นความต่างได้ชัดเจนเลยทีเดียว

start_time = time.time()
parquet_df = pd.read_parquet('sample.parquet')
parquet_elapsed = time.time() - start_time

start_time = time.time()
csv_df = pd.read_csv('sample.csv')
csv_elapsed = time.time() - start_time

print('Loading CSV' , csv_elapsed)
print('Loading Parquet' , parquet_elapsed)
Loading CSV 0.34826040267944336
Loading Parquet 0.08813595771789551

หลังจากเราเขียนลงไปในเครื่องแล้ว เราจะต้องโหลดกลับเข้ามาได้แล้ว เราลองมาเทียบเวลาในการโหลดกันบ้างดีกว่า เช่นเดิม ดูที่ผลลัพธ์แล้วก็คือ เห้อออ เลยทีเดียว เพราะ Parquet ทำได้เร็วกว่า CSV แน่ ๆ ถ้าคิดเป็น Percentage ก็คือลดเวลาลงได้ถึง 74.69% เลย

สรุป

เขียนไปเขียนมา เหมือนมาป้ายยา ให้เปลี่ยนมาใช้ Parquet กันหมด แต่ใจเย็น ๆ ก่อนฮ่า ๆ ถึงแม้กว่า Parquet จะเก็บข้อมูลได้โคตรมีประสิทธิภาพสูงมาก ๆ และสามารถเอาไปใช้งานได้เยอะมาก ๆ แต่ ๆ เอาเข้าจริง เวลาเราใช้งาน ถ้าเก็บข้อมูลง่าย ๆ เราก็จะเก็บเป็น CSV นะ เพราะ CSV มันสามารถเปิดได้ในหลาย ๆ โปรแกรม ทำให้มันเป็น Format กลางเวลาเราจะเอาข้อมูลไปทำงานในหลาย ๆ โปรแกรมอยู่ดี แต่เราจะใช้ Parquet สำหรับการเก็บข้อมูลที่เราจะทำงานแค่บน Python หรือว่าเป็นข้อมูลที่ เราจะเก็บยาว ๆ ก็จะใช้แหละ เพื่อประหยัดพื้นที่