3 เคล็ดลับ Polars เพื่อการจัดการข้อมูลประสิทธิภาพสูง

3 Polars Tricks for High-Performance Data Manipulation

Polars ได้รับความเร็วมาจากสองส่วนหลัก คือ Expression Engine ที่เขียนและประมวลผลด้วย Rust บนทุกคอร์ที่มี และ Query Optimizer ที่จะเขียนลำดับการทำงานของคุณใหม่ก่อนเริ่มทำงานจริง

สคริปต์ Polars ที่ทำงานช้าเกือบทุกตัวมักจะขาดคุณสมบัติอย่างใดอย่างหนึ่งในสองอย่างนี้ สิ่งที่น่าลำบากใจคือเวอร์ชันที่ทำงานเร็วและเวอร์ชันที่ทำงานช้าอาจดูคล้ายกันมากบนหน้ากระดาษ และนี่คือ 3 จุดสำคัญที่มักส่งผลต่อประสิทธิภาพอย่างชัดเจน

ตัวอย่างเหล่านี้จะใช้ข้อมูลการเดินทางของ NYC Yellow Taxi เป็นเวลาหนึ่งเดือน ซึ่งเผยแพร่โดย TLC ในรูปแบบ Parquet โดยคุณสามารถดาวน์โหลดข้อมูลมาทดสอบก่อนได้ดังนี้:

curl -O https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2026-01.parquet

ข้อมูลด้านล่างทั้งหมดได้รับการตรวจสอบความถูกต้องกับ Polars เวอร์ชัน 1.44.2 เรียบร้อยแล้ว

เคล็ดลับที่ 1: การ Scan ไฟล์แทนการ Read

ฟังก์ชัน pl.read_parquet จะดึงเนื้อหาของไฟล์ทั้งหมดเข้าสู่หน่วยความจำแล้วจึงทำการกรองข้อมูล แต่ pl.scan_parquet จะส่งคืน LazyFrame กลับมาแทน ซึ่งจะบันทึกคำสั่งที่คุณต้องการโดยยังไม่ลงมือทำจริง

ช่องว่างนี้เองที่ทำให้ Optimizer แสดงฝีมือ โดยมันจะดันการกรอง (Filter) และการเลือกคอลัมน์ลงไปที่ขั้นตอนการ Scan โดยตรง ทำให้การคัดกรองข้อมูลเกิดขึ้นขณะกำลังอ่านไฟล์ แถวที่ไม่ต้องการจะไม่ถูกถอดรหัส (Decoded) ช่วยประหยัดพลังประมวลผลมหาศาล

import polars as pl
 
q = (
    pl.scan_parquet("yellow_tripdata_2026-01.parquet")
    .filter(pl.col("fare_amount") > 50)
    .select("PULocationID", "tip_amount")
    .group_by("PULocationID")
    .agg(pl.col("tip_amount").mean())
)
 
# นี่คือการอธิบายแผนการทำงาน ไม่ใช่ข้อมูล
print(q.explain())
df = q.collect()

จะไม่มีการประมวลผลจริงจนกว่าจะถึงคำสั่ง collect() ส่วนคำสั่ง explain() จะแสดงแผนการทำงานที่ Optimizer สร้างขึ้น ซึ่งคุณจะเห็นว่ามีการผลัก Predicate (pushed-down) และการตัดคอลัมน์ที่ไม่จำเป็นออกไปแล้ว:

AGGREGATE[maintain_order: false]
  [col("tip_amount").mean()] BY [col("PULocationID")]
  FROM
  simple π 2/2 ["PULocationID", "tip_amount"]
    Parquet SCAN [yellow_tripdata_2026-01.parquet]
    PROJECT 3/20 COLUMNS
    SELECTION: col("fare_amount") > 50.0
    ESTIMATED ROWS: 3724889

นิสัยที่ควรเลิกคือการเรียก collect() ระหว่างกลางของเชนคำสั่ง เพราะทุกครั้งที่เรียก collect() จะเปรียบเสมือนการสร้างกำแพงที่ Optimizer ไม่สามารถมองทะลุผ่านไปปรับแต่งการทำงานได้

เคล็ดลับที่ 2: ค่ารายกลุ่ม (Per-Group Values) โดยไม่ต้องวนรอบ Group-By

การหาค่าเฉลี่ยหรือยอดรวมรายกลุ่มกลับมาใส่ในทุกแถวมักถูกทำด้วยการ group_by และ agg แล้วนำมา Join กลับ ซึ่งวิธีนี้ทำให้ต้องประมวลผลสองรอบและวุ่นวายกับการจัดการ Key ที่ใช้ Join แต่การใช้ .over() สามารถจบงานได้ใน Expression เดียวภายในรอบเดียว โดยยังคงลำดับแถวเดิมไว้

ตัวอย่างการคำนวณสัดส่วนค่าโดยสารในแต่ละโซน:

df = pl.DataFrame({
    "pickup_zone": ["A", "A", "B", "B"],
    "fare_amount": [30.0, 70.0, 25.0, 75.0],
})
 
out = df.with_columns(
    (pl.col("fare_amount") / pl.col("fare_amount").sum().over("pickup_zone"))
    .alias("share_of_zone")
)
print(out)

ผลลัพธ์ที่ได้จะแสดงสัดส่วนของแต่ละแถวเทียบกับผลรวมของโซนนั้นๆ ทันที นอกจากนี้ over ยังรองรับ order_by ทำให้การทำผลรวมสะสม (Running Total) รายกลุ่มทำได้ง่ายขึ้นเพียงบรรทัดเดียว โดยไม่ต้องเสียเวลา Sort และ Join ใหม่

เคล็ดลับที่ 3: การเอา Python ออกจาก Loop

ฟังก์ชัน map_elements จะส่งค่าในคอลัมน์ไปให้ Python ประมวลผลทีละค่า ซึ่งช้ากว่า Expression API แบบปกติมาก จน Polars จะแจ้งเตือน PolarsInefficientMapWarning หากพบว่าสามารถใช้การทำงานแบบ Native แทนได้ โดยเฉพาะการทำงานแบบมีเงื่อนไขที่ควรใช้ when/then/otherwise แทน

ตัวอย่างการจัดกลุ่มช่วงข้อมูล (Banding) แบบ Native:

banded = df.with_columns(
    pl.when(pl.col("fare_amount") > 50)
    .then(pl.lit("high"))
    .when(pl.col("fare_amount") > 20)
    .then(pl.lit("medium"))
    .otherwise(pl.lit("low"))
    .alias("fare_band")
)
print(banded)

วิธีนี้ได้ผลลัพธ์เหมือนกับการใช้ map_elements แต่ CPU จะประมวลผลได้รวดเร็วกว่าเนื่องจาก Polars จะคำนวณทุกเงื่อนไขแบบขนานภายใน Engine ของตัวเองโดยไม่ต้องพึ่งพาตัวแปลภาษา Python

บทสรุป

หัวใจสำคัญของทั้งสามเคล็ดลับคือการรักษาการทำงานให้อยู่ภายใน Engine ของ Polars ให้ได้นานที่สุด แทนที่จะส่งงานกลับไปให้ Python หรือดึงข้อมูลลงหน่วยความจำโดยไม่จำเป็น หากสคริปต์ของคุณทำงานช้า ให้ลองตรวจสอบว่าคุณได้ใช้ Scan แทน Read หรือใช้ Expression แทนการ Loop แล้วหรือยัง

Source: KDnuggets
ดูแลงานแปลและเรียบเรียงโดย SirilukP

ความคิดเห็น (0)

เข้าสู่ระบบเพื่อร่วมแสดงความเห็น

สมัครสมาชิก

มาเป็นคนแรกที่แสดงความเห็นกันเลยโบร