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 กันไม่ให้นับคู่ซ้ำ
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
ลูกค้าคนไหนที่ยังไม่เคยสั่งซื้อเลย
ทีมการตลาดอยากส่งโปรโมชันไปกระตุ้นลูกค้าที่สมัครแล้วแต่ยังไม่เคยสั่งซื้อเลย
ลอง merge แบบ inner (ค่าเริ่มต้น) ก่อน — สังเกตว่าจำนวนแถวคือจำนวนออเดอร์ ไม่ใช่จำนวนลูกค้า เพราะลูกค้าคนเดียวมีได้หลายออเดอร์
รายได้รวมแต่ละหมวดสินค้า (เฉพาะออเดอร์ completed)
อยากรู้ว่าหมวดไหนทำรายได้ให้ร้านมากที่สุด นับเฉพาะออเดอร์ที่จบสมบูรณ์ (completed)
เชื่อม
order_itemsกับordersแล้วกับproducts(merge ได้มากกว่า 2 ตารางต่อกันเรื่อยๆ)
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
จำนวนออเดอร์ (นับไม่ซ้ำ) ที่ซื้อสินค้าหมวด 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
สรุป
- 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 และวิธีหา "แถวที่ไม่มีคู่" ก็เหมือนกันเป๊ะ
เพราะเป็นคุณสมบัติของความสัมพันธ์ระหว่างตาราง ไม่ใช่ของภาษาใดภาษาหนึ่งโดยเฉพาะ