Data Analyst › pandas

merge (join ใน pandas)

ประมาณ 30 นาที

เป้าหมายของบท

  • เชื่อมตารางด้วย pd.merge()/.merge() ทั้งแบบ inner/left/right/outer
  • หา "แถวที่ไม่มีคู่" ด้วย how='left' แล้วกรอง .isna()
  • เชื่อมได้มากกว่า 2 ตาราง และระวังปัญหาแถวเบิ้ล (fan-out) — แนวคิดเดียวกับ SQL JOIN ทุกประการ

จาก JOIN สู่ merge

บทนี้คือ “SQL JOIN” เวอร์ชัน pandas — แนวคิดเหมือนกันทุกประการ (เทียบกับ da/sql/03-joins ได้เลย) ต่างกันแค่ syntax:

orders.merge(customers, on='customer_id')

how=‘inner’/‘left’/‘right’/‘outer’

how= เก็บแถวไหนไว้
'inner' (ค่าเริ่มต้น) เฉพาะแถวที่จับคู่ได้ทั้งสองฝั่ง
'left' ทุกแถวจากตารางซ้าย ต่อให้หาคู่ในตารางขวาไม่เจอก็ใส่ NaN แทน
'right' ทุกแถวจากตารางขวา (ตรงข้ามกับ left)
'outer' ทุกแถวจากทั้งสองฝั่ง ไม่ว่าจะจับคู่ได้หรือไม่

ลองเชื่อม customers กับ orders ดู: inner/right ได้ 6,938 แถว (จำนวนออเดอร์) ส่วน left/outer ได้ 7,399 แถว (เพิ่มมา 461 แถว คือลูกค้าที่ไม่เคยสั่งซื้อเลย) — ผลเดียวกับที่เจอตอนเรียน SQL JOIN ทุกประการ

หา “แถวที่ไม่มีคู่” (anti-join)

merged = customers.merge(orders, on='customer_id', how='left')
merged[merged['order_id'].isna()]

หลักการ: how='left' ทำให้แถวที่จับคู่ไม่ได้มีค่า NaN ในคอลัมน์ของตารางขวา กรองด้วย .isna() บนคอลัมน์คีย์ ของตารางขวาก็จะเหลือเฉพาะแถวที่ “ไม่มีคู่” จริงๆ

เชื่อมได้มากกว่า 2 ตาราง — และปัญหาแถวเบิ้ล (fan-out)

order_items.merge(orders, on='order_id').merge(products, on='product_id')

ต่อ .merge() กันได้เรื่อยๆ แต่ต้องระวัง fan-out: ถ้าตารางที่เชื่อมมีความสัมพันธ์แบบ “หนึ่งต่อหลาย” (1 ออเดอร์มีหลายรายการสินค้า) การ merge จะคูณแถวขึ้นตามจำนวนรายการ ทำให้ len(...) นับเกินจำนวนที่ต้องการ วิธีแก้คือใช้ .nunique() บนคีย์ของตารางที่ไม่อยากให้นับซ้ำ

Self-merge

merge ตารางกับตัวเองก็ทำได้ ใช้เมื่อต้องการเทียบแถวในตารางเดียวกันเอง เช่น “หาสินค้าคู่ที่ราคาใกล้กัน” ต้องใส่ suffixes=(...) กันชื่อคอลัมน์ที่ซ้ำกันชนกัน และมักต้องมีเงื่อนไข id_1 < id_2 กันไม่ให้นับคู่ซ้ำ

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

ลูกค้าคนไหนที่ยังไม่เคยสั่งซื้อเลย

ทีมการตลาดอยากส่งโปรโมชันไปกระตุ้นลูกค้าที่สมัครแล้วแต่ยังไม่เคยสั่งซื้อเลย

  1. ลอง merge แบบ inner (ค่าเริ่มต้น) ก่อน — สังเกตว่าจำนวนแถวคือจำนวนออเดอร์ ไม่ใช่จำนวนลูกค้า เพราะลูกค้าคนเดียวมีได้หลายออเดอร์

    Python

รายได้รวมแต่ละหมวดสินค้า (เฉพาะออเดอร์ completed)

อยากรู้ว่าหมวดไหนทำรายได้ให้ร้านมากที่สุด นับเฉพาะออเดอร์ที่จบสมบูรณ์ (completed)

  1. เชื่อม order_items กับ orders แล้วกับ products (merge ได้มากกว่า 2 ตารางต่อกันเรื่อยๆ)

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

จำนวนออเดอร์ (นับไม่ซ้ำ) ที่ซื้อสินค้าหมวด Electronics เก็บไว้ในตัวแปร n_orders เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

เชื่อม order_items กับ products ด้วย product_id แล้วนับจำนวนแถวทั้งหมด เก็บไว้ในตัวแปร n_rows

🟢 พื้นฐานยังไม่ได้ทำ

เชื่อม orders กับ customers เรียงตาม order_id เอาแค่ 5 แถวแรก แสดง order_id, city เก็บไว้ในตัวแปร result

🟢 พื้นฐานยังไม่ได้ทำ

จำนวนลูกค้าที่ไม่เคยสั่งซื้อเลย เก็บไว้ในตัวแปร n_customers

🟢 พื้นฐานยังไม่ได้ทำ

รายได้รวม (quantity × unit_price) ของออเดอร์ที่ completed ทั้งหมด ปัด 2 ตำแหน่ง เก็บไว้ในตัวแปร total_revenue

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

หมวดสินค้าที่มีรายได้มากที่สุด (เฉพาะ completed) เก็บไว้ในตัวแปร result (Series ที่มี 1 แถว)

🟡 ประยุกต์ยังไม่ได้ทำ

5 ลูกค้าเมือง Bangkok ที่ใช้จ่ายมากที่สุด (เฉพาะ completed) เรียงมากไปน้อย เก็บไว้ในตัวแปร result

🟡 ประยุกต์ยังไม่ได้ทำ

จำนวนลูกค้าที่ไม่เคยสั่งซื้อเลย แยกตามเมือง เรียงมากไปน้อย เก็บไว้ในตัวแปร result

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

จำนวนคู่สินค้าในหมวดเดียวกันที่ราคาต่างกันไม่เกิน 10 บาท (self-merge, นับแต่ละคู่ครั้งเดียว) เก็บไว้ในตัวแปร n_pairs

Quiz

1. how='inner' (ค่าเริ่มต้น) กับ how='left' ต่างกันยังไง
2. ทำไม len(...) หลัง merge กับตารางลูก (order_items) ถึงมากกว่าจำนวนออเดอร์จริง
3. จะหา "แถวที่ไม่มีคู่" ทำยังไง

สรุป

  • how='left' เก็บทุกแถวจากตารางซ้าย ใส่ NaN เมื่อหาคู่ในตารางขวาไม่เจอ how='inner' (ค่าเริ่มต้น) ตัดแถวที่ไม่มีคู่ทิ้ง
  • หา "แถวที่ไม่มีคู่" ด้วย merge(how='left') แล้วกรองคอลัมน์จากตารางขวาด้วย .isna()
  • merge กับตารางแบบ one-to-many ทำให้แถวเบิ้ล (fan-out) ระวัง len(...) นับซ้ำ ใช้ .nunique() แทน

คำถามสัมภาษณ์

pandas merge() กับ SQL JOIN เหมือนหรือต่างกันยังไง

แนวคิดเดียวกันทุกประการ (how='inner'/'left'/'right'/'outer' ตรงกับ SQL JOIN แต่ละแบบ) ต่างกันแค่ syntax เท่านั้น ปัญหา fan-out และวิธีหา "แถวที่ไม่มีคู่" ก็เหมือนกันเป๊ะ เพราะเป็นคุณสมบัติของความสัมพันธ์ระหว่างตาราง ไม่ใช่ของภาษาใดภาษาหนึ่งโดยเฉพาะ