รวม DataFrame ด้วย merge(), join() และ concat() ใน pandas 3.0 — คู่มือฉบับสมบูรณ์

เรียนรู้วิธีรวม DataFrame ใน pandas 3.0 ด้วย merge(), join() และ concat() พร้อมฟีเจอร์ Anti Join ใหม่ (left_anti, right_anti) ตัวอย่างโค้ดใช้งานจริง และ 5 ข้อผิดพลาดที่ต้องระวัง

ทำไมต้องรวม DataFrame? — ปัญหาที่ทุกคนเจอ

ถ้าคุณเคยทำงานกับข้อมูลจริงๆ จะรู้เลยว่า ข้อมูลแทบไม่เคยอยู่ใน DataFrame เดียว คุณอาจมีตาราง "ลูกค้า" อยู่ไฟล์หนึ่ง ตาราง "คำสั่งซื้อ" อยู่อีกไฟล์ และข้อมูล "สินค้า" โผล่มาจาก API แยกต่างหาก ก่อนจะวิเคราะห์อะไรได้สักอย่าง คุณต้อง รวมข้อมูลพวกนี้เข้าด้วยกัน ให้เป็น DataFrame เดียวก่อน

pandas มีเครื่องมือหลัก 3 ตัวสำหรับงานนี้ ได้แก่ pd.concat(), pd.merge(), และ .join() แต่ละตัวเหมาะกับสถานการณ์ต่างกัน และถ้าเลือกผิดตัว... ผลลัพธ์อาจผิดพลาด หรือแถวระเบิดเป็นล้านแถวโดยที่คุณไม่รู้ตัวเลย

ในบทความนี้ ผมจะพาคุณเข้าใจทั้ง 3 ฟังก์ชันแบบลึกซึ้ง พร้อมอัปเดตฟีเจอร์ใหม่ใน pandas 3.0 (มกราคม 2026) โดยเฉพาะ Anti Join แบบใหม่ที่หลายคนรอมานาน รวมถึง 5 ข้อผิดพลาดสุดคลาสสิค ที่เจอกันบ่อยมาก เพื่อที่คุณจะได้ไม่ตกหลุมพรางอีก

pd.concat() — ต่อ DataFrame แนวตั้งและแนวนอน

pd.concat() เป็นฟังก์ชันสำหรับ "ต่อ" DataFrame หลายตัว เข้าด้วยกัน คล้ายกับการวาง Excel Sheet ซ้อนกัน (แนวตั้ง) หรือวางเคียงกัน (แนวนอน)

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

ต่อแนวตั้ง (axis=0) — เพิ่มแถว

กรณีที่เจอบ่อยที่สุดคือการรวมข้อมูลที่มีคอลัมน์เหมือนกัน เช่น ข้อมูลยอดขายรายเดือนที่แยกเป็นหลายไฟล์:

import pandas as pd

df_jan = pd.DataFrame({
    "product": ["A", "B", "C"],
    "sales": [100, 200, 150]
})

df_feb = pd.DataFrame({
    "product": ["A", "B", "D"],
    "sales": [120, 180, 90]
})

# ต่อแนวตั้ง — เพิ่มแถว
result = pd.concat([df_jan, df_feb], ignore_index=True)
print(result)
#   product  sales
# 0       A    100
# 1       B    200
# 2       C    150
# 3       A    120
# 4       B    180
# 5       D     90

เคล็ดลับ: ใช้ ignore_index=True เสมอเมื่อต่อแนวตั้ง ไม่งั้น index จะซ้ำกัน (0, 1, 2, 0, 1, 2) ซึ่งอาจทำให้เกิดปัญหาตอนหลังได้

ต่อแนวนอน (axis=1) — เพิ่มคอลัมน์

df_info = pd.DataFrame({
    "product": ["A", "B", "C"],
    "category": ["electronics", "food", "clothing"]
})

df_price = pd.DataFrame({
    "price": [999, 50, 350],
    "stock": [10, 200, 45]
})

# ต่อแนวนอน — เพิ่มคอลัมน์
result = pd.concat([df_info, df_price], axis=1)
print(result)
#   product    category  price  stock
# 0       A  electronics    999     10
# 1       B        food     50    200
# 2       C    clothing    350     45

คำเตือนสำคัญ: การ concat แนวนอน (axis=1) จับคู่ข้อมูลตาม index ไม่ใช่ตามค่าในคอลัมน์นะครับ ถ้า index ไม่ตรงกัน จะได้แถวที่ผิดตำแหน่งโดยไม่มี error ออกมาเตือนเลย (เคยโดนมาแล้ว เสียเวลา debug ไปตั้งชั่วโมง)

ใช้ keys เพื่อระบุแหล่งที่มา

# ใส่ keys เพื่อบอกว่าข้อมูลมาจากไหน
result = pd.concat(
    [df_jan, df_feb],
    keys=["jan", "feb"],
    names=["month", "row_id"]
)
print(result)
#              product  sales
# month row_id
# jan   0            A    100
#       1            B    200
#       2            C    150
# feb   0            A    120
#       1            B    180
#       2            D     90

วิธีนี้มีประโยชน์มาก ถ้าต้องการรู้ว่าแต่ละแถวมาจากไฟล์ไหน

รวมหลายไฟล์ในโฟลเดอร์ — Best Practice

import glob

# อ่าน CSV ทุกไฟล์ในโฟลเดอร์แล้ว concat ครั้งเดียว
files = glob.glob("data/sales_*.csv")
frames = [pd.read_csv(f) for f in files]
all_sales = pd.concat(frames, ignore_index=True)

สำคัญมาก: อย่า concat ทีละไฟล์ในลูปเด็ดขาด (เช่น result = pd.concat([result, df])) เพราะ pandas จะก๊อปปี้ข้อมูลทั้งหมดทุกรอบ ทำให้ช้าสุดๆ ให้รวบรวมเป็น list ก่อนแล้ว concat ครั้งเดียวเสมอครับ ผมเคยเจอโค้ดแบบนี้ในโปรเจกต์จริง ที่รันจาก 3 วินาทีกลายเป็น 20 นาทีเลย

หมายเหตุสำหรับ pandas 3.0

ตั้งแต่ pandas 2.0 เป็นต้นมา DataFrame.append() ถูก ลบออกไปแล้ว ถ้าเจอโค้ดเก่าที่ใช้อยู่ ให้เปลี่ยนเป็น pd.concat([df1, df2], ignore_index=True) แทนเสมอ

pd.merge() — รวมข้อมูลแบบ SQL Join

pd.merge() คือหัวใจหลักของการรวมข้อมูลใน pandas เลยก็ว่าได้ ทำงานเหมือน SQL JOIN — ใช้คอลัมน์ร่วม (key) ในการจับคู่แถวจาก 2 DataFrame เข้าด้วยกัน ใครที่เคยเขียน SQL มาก่อน จะรู้สึกคุ้นเคยมาก

Inner Join (ค่าเริ่มต้น)

เก็บเฉพาะแถวที่มี key ตรงกันในทั้ง 2 DataFrame:

customers = pd.DataFrame({
    "customer_id": [1, 2, 3, 4],
    "name": ["สมชาย", "สมหญิง", "วิชัย", "พรทิพย์"]
})

orders = pd.DataFrame({
    "order_id": [101, 102, 103],
    "customer_id": [1, 2, 5],
    "amount": [500, 1200, 300]
})

# Inner join — เก็บเฉพาะลูกค้าที่มีคำสั่งซื้อ
result = pd.merge(customers, orders, on="customer_id")
print(result)
#    customer_id    name  order_id  amount
# 0            1  สมชาย       101     500
# 1            2  สมหญิง      102    1200

สังเกตว่า customer_id 3, 4 หายไป (ไม่มี order) และ customer_id 5 ก็หายด้วย (ไม่มีข้อมูลลูกค้า) นี่คือพฤติกรรมปกติของ inner join ที่เก็บเฉพาะคู่ที่แมทช์กัน

Left Join — เก็บทุกแถวจากซ้าย

# Left join — เก็บลูกค้าทุกคน ใครไม่มี order ก็ใส่ NaN
result = pd.merge(customers, orders, on="customer_id", how="left")
print(result)
#    customer_id    name  order_id  amount
# 0            1  สมชาย     101.0   500.0
# 1            2  สมหญิง    102.0  1200.0
# 2            3   วิชัย      NaN     NaN
# 3            4  พรทิพย์     NaN     NaN

left join ใช้บ่อยมากในงานจริง โดยเฉพาะเมื่ออยากรักษาข้อมูลหลักไว้ทุกแถว แล้วเสริมข้อมูลจากตารางอื่นเข้ามา

Outer Join — เก็บทุกแถวจากทั้ง 2 ฝั่ง

# Outer join — เก็บหมดทุกแถว ใส่ NaN ที่ไม่ตรงกัน
result = pd.merge(customers, orders, on="customer_id", how="outer")
print(result)
#    customer_id    name  order_id  amount
# 0            1  สมชาย     101.0   500.0
# 1            2  สมหญิง    102.0  1200.0
# 2            3   วิชัย      NaN     NaN
# 3            4  พรทิพย์     NaN     NaN
# 4            5     NaN     103.0   300.0

merge เมื่อชื่อคอลัมน์ไม่เหมือนกัน

ในโลกจริงนั้น ชื่อคอลัมน์มักไม่ตรงกันสักที ตารางหนึ่งใช้ "emp_id" อีกตารางใช้ "employee_number" ก็ต้องบอก pandas ให้ชัดเจน:

employees = pd.DataFrame({
    "emp_id": [1, 2, 3],
    "name": ["อรุณ", "วรรณ", "ชาติ"]
})

salaries = pd.DataFrame({
    "employee_number": [1, 2, 3],
    "salary": [35000, 42000, 28000]
})

# ใช้ left_on / right_on เมื่อชื่อคอลัมน์ต่างกัน
result = pd.merge(
    employees, salaries,
    left_on="emp_id",
    right_on="employee_number"
)
print(result)
#    emp_id  name  employee_number  salary
# 0       1  อรุณ                1   35000
# 1       2  วรรณ                2   42000
# 2       3  ชาติ                3   28000

ใหม่ใน pandas 3.0: Anti Join — left_anti และ right_anti

โอเค ส่วนนี้คือไฮไลท์ของบทความเลยครับ

ตั้งแต่ pandas 3.0 (มกราคม 2026) เราสามารถใช้ Anti Join ได้โดยตรงแล้ว ไม่ต้องใช้ท่าอ้อมแบบเดิมอีกต่อไป ถ้าใครเคยเขียนท่านี้บ่อยๆ จะดีใจมาก

Anti Join คือการหาแถวที่ อยู่ในตารางหนึ่งแต่ไม่อยู่ในอีกตาราง ก่อนหน้านี้ต้องเขียนแบบอ้อมๆ แบบนี้:

# วิธีเดิม (ก่อน pandas 3.0) — ท่าอ้อมที่ค่อนข้างยุ่งยาก
merged = pd.merge(customers, orders, on="customer_id", how="outer", indicator=True)
result = merged[merged["_merge"] == "left_only"].drop(columns=["_merge"])

พูดตรงๆ โค้ดข้างบนอ่านยากมาก และต้องจำทุกครั้งว่าเขียนยังไง ตั้งแต่ pandas 3.0 ง่ายขึ้นแบบนี้:

# ✅ วิธีใหม่ pandas 3.0 — left_anti
# หาลูกค้าที่ยังไม่เคยสั่งซื้อ
no_orders = pd.merge(
    customers, orders,
    on="customer_id",
    how="left_anti"
)
print(no_orders)
#    customer_id    name
# 0            3   วิชัย
# 1            4  พรทิพย์

# ✅ right_anti — หา order ที่ไม่มีข้อมูลลูกค้า
orphan_orders = pd.merge(
    customers, orders,
    on="customer_id",
    how="right_anti"
)
print(orphan_orders)
#    order_id  customer_id  amount
# 0       103            5     300

ข้อดีของ Anti Join แบบใหม่:

  • โค้ดสั้นลง อ่านง่ายขึ้นเยอะ
  • ไม่ต้องสร้างคอลัมน์ _merge ที่ต้องลบออกทีหลังอีกแล้ว
  • ประสิทธิภาพดีกว่าวิธีเดิม เพราะ pandas ไม่ต้องสร้าง outer join ทั้งหมดก่อนแล้วค่อยกรอง
  • ใช้งานได้ทั้งใน pd.merge(), DataFrame.merge(), และ DataFrame.join()

ตัวอย่างใช้งานจริง: หาสินค้าที่ไม่มีใน stock

นี่เป็นเคสที่เจอบ่อยมากในงาน e-commerce:

catalog = pd.DataFrame({
    "sku": ["SKU001", "SKU002", "SKU003", "SKU004"],
    "name": ["เสื้อยืด", "กางเกง", "หมวก", "รองเท้า"]
})

stock = pd.DataFrame({
    "sku": ["SKU001", "SKU003"],
    "quantity": [50, 20]
})

# หาสินค้าที่หมด stock (ไม่อยู่ในตาราง stock)
out_of_stock = pd.merge(catalog, stock, on="sku", how="left_anti")
print(out_of_stock)
#      sku    name
# 0  SKU002  กางเกง
# 1  SKU004  รองเท้า

.join() — รวม DataFrame ด้วย Index

.join() เป็น method ของ DataFrame ที่ออกแบบมาสำหรับรวมข้อมูลโดยใช้ index เป็นหลัก ภายในจริงๆ มันก็เรียก merge() นั่นแหละ แต่เขียนสั้นกว่าเวลาที่ key อยู่ใน index อยู่แล้ว

# สร้าง DataFrame ที่มี index เป็น key
df_main = pd.DataFrame({
    "name": ["สมชาย", "สมหญิง", "วิชัย"],
    "age": [30, 25, 35]
}, index=["E001", "E002", "E003"])

df_salary = pd.DataFrame({
    "salary": [45000, 52000, 38000],
    "department": ["IT", "HR", "Marketing"]
}, index=["E001", "E002", "E003"])

# join บน index — สั้นและกระชับ
result = df_main.join(df_salary)
print(result)
#       name  age  salary department
# E001  สมชาย   30   45000         IT
# E002  สมหญิง  25   52000         HR
# E003   วิชัย   35   38000  Marketing

เมื่อไหร่ควรใช้ .join(): เมื่อ key ของคุณอยู่ใน index อยู่แล้ว เช่น time series ที่ใช้ DatetimeIndex หรือ DataFrame ที่ set_index() ไว้ก่อนหน้า ถ้า key อยู่ในคอลัมน์ปกติ ให้ใช้ merge() แทนจะดีกว่า

สรุปเปรียบเทียบ: concat vs merge vs join

ก่อนจะเลือกใช้ฟังก์ชันไหน ลองดูตารางนี้ก่อนครับ:

คุณสมบัติconcat()merge()join()
จำนวน DataFrame2 ขึ้นไป2 ตัว2 ตัวขึ้นไป
รวมแนวตั้งได้ไม่ได้ไม่ได้
รวมแนวนอนได้ได้ได้
ใช้คอลัมน์เป็น keyไม่ได้ได้จำกัด
ใช้ index เป็น keyได้ได้ได้ (default)
Anti Joinไม่ได้ได้ (3.0+)ได้ (3.0+)
validate parameterไม่มีมีมี

5 ข้อผิดพลาดที่พบบ่อย (และวิธีแก้)

ส่วนนี้สำคัญมากครับ ผมเห็นคนเจอปัญหาพวกนี้ซ้ำแล้วซ้ำเล่า

1. แถวระเบิดจาก Cartesian Product

นี่คือปัญหาอันตรายที่สุดเลย ถ้าทั้ง 2 DataFrame มี key ซ้ำ pandas จะสร้าง Cartesian product ทำให้แถวเพิ่มขึ้นเป็นทวีคูณ ข้อมูล 1,000 แถวอาจกลายเป็นล้านแถวได้ง่ายๆ

# ⚠️ ทั้งสอง DataFrame มี key ซ้ำ
df_left = pd.DataFrame({"key": ["A", "A"], "val_l": [1, 2]})
df_right = pd.DataFrame({"key": ["A", "A"], "val_r": [10, 20]})

# ได้ 4 แถว แทนที่จะเป็น 2!
bad_result = pd.merge(df_left, df_right, on="key")
print(len(bad_result))  # 4 (2 x 2 = Cartesian product)

# ✅ ป้องกันด้วย validate parameter
try:
    pd.merge(df_left, df_right, on="key", validate="one_to_one")
except pd.errors.MergeError as e:
    print(f"จับได้! {e}")
    # "Merge keys are not unique in left dataset..."

2. NaN ใน key ไม่จับคู่กัน

อันนี้หลายคนไม่รู้ แถวที่มี NaN ใน key column จะ ไม่จับคู่กับอะไรเลย เพราะ NaN != NaN ใน pandas ทำให้แถวเหล่านั้นหายไปจาก inner join แบบเงียบๆ:

df1 = pd.DataFrame({"key": [1, 2, None], "val": ["a", "b", "c"]})
df2 = pd.DataFrame({"key": [1, None, 3], "val": ["x", "y", "z"]})

# NaN rows จะหายไปจาก inner join
result = pd.merge(df1, df2, on="key")
print(result)
# มีแค่ key=1 เท่านั้น — key=None ไม่จับคู่กัน

# ✅ แก้ไข: fillna ก่อน merge ถ้าต้องการจับคู่ค่า null

3. คอลัมน์ซ้ำ _x, _y ที่น่าปวดหัว

ถ้าทั้ง 2 DataFrame มีคอลัมน์ชื่อเดียวกัน (ที่ไม่ใช่ key) pandas จะต่อ _x และ _y ให้อัตโนมัติ ซึ่งพอ DataFrame มีหลายคอลัมน์ จะงงมากว่าอันไหนเป็นอันไหน:

df_a = pd.DataFrame({"id": [1, 2], "name": ["A", "B"], "score": [80, 90]})
df_b = pd.DataFrame({"id": [1, 2], "name": ["A", "B"], "score": [85, 95]})

result = pd.merge(df_a, df_b, on="id")
print(result.columns.tolist())
# ['id', 'name_x', 'score_x', 'name_y', 'score_y']

# ✅ ใช้ suffixes ที่มีความหมาย
result = pd.merge(
    df_a, df_b, on=["id", "name"],
    suffixes=("_midterm", "_final")
)
print(result.columns.tolist())
# ['id', 'name', 'score_midterm', 'score_final']

4. concat แนวนอนกับ index ที่ไม่ตรงกัน

อันนี้แอบร้ายตรงที่ไม่มี error แจ้งเตือน:

df_x = pd.DataFrame({"a": [1, 2, 3]}, index=[0, 1, 2])
df_y = pd.DataFrame({"b": [10, 20, 30]}, index=[1, 2, 3])

# ⚠️ index ไม่ตรง — ได้ NaN โดยไม่มี error!
result = pd.concat([df_x, df_y], axis=1)
print(result)
#      a     b
# 0  1.0   NaN
# 1  2.0  10.0
# 2  3.0  20.0
# 3  NaN  30.0

# ✅ แก้: reset_index ก่อน concat
result = pd.concat(
    [df_x.reset_index(drop=True), df_y.reset_index(drop=True)],
    axis=1
)

5. ลืมใช้ ignore_index กับ concat แนวตั้ง

ดูเหมือนเรื่องเล็ก แต่ index ซ้ำอาจทำให้โค้ดส่วนอื่นพังได้:

# ⚠️ index ซ้ำ — อาจทำให้ loc/iloc ทำงานผิด
result = pd.concat([df_jan, df_feb])
print(result.index.tolist())
# [0, 1, 2, 0, 1, 2] — index ซ้ำ!

# ✅ ใช้ ignore_index=True
result = pd.concat([df_jan, df_feb], ignore_index=True)
print(result.index.tolist())
# [0, 1, 2, 3, 4, 5] — เรียบร้อย

เคล็ดลับเรื่องประสิทธิภาพ

เมื่อข้อมูลเริ่มใหญ่ขึ้น วิธีการ merge มีผลต่อความเร็วอย่างมาก ลองทำตามนี้:

  • ตั้ง key เป็น index ก่อน merge: ถ้าต้อง merge ซ้ำหลายครั้งกับ key เดิม ให้ set_index() ก่อนแล้วใช้ .join() จะเร็วกว่า เพราะ pandas ใช้ hash table ของ index ที่สร้างไว้แล้ว
  • ลด data type ก่อน merge: เปลี่ยน int64 เป็น int32 หรือใช้ category dtype สำหรับ key ที่เป็น string ซ้ำๆ จะลดหน่วยความจำและเร็วขึ้นชัดเจน
  • ใช้ validate เพื่อจับ bug เร็ว: parameter validate="one_to_one" ตรวจสอบ key ก่อน merge เลย ช่วยจับ bug ได้ตั้งแต่เนิ่นๆ แทนที่จะมานั่งหาว่าทำไมแถวเพิ่มทีหลัง
  • pandas 3.0 Copy-on-Write: ตั้งแต่ pandas 3.0 ผลลัพธ์จาก merge จะใช้ lazy copy ไม่ก๊อปปี้ข้อมูลทันที ช่วยประหยัดหน่วยความจำได้เยอะเลย

แผนภาพตัดสินใจ: ควรใช้ฟังก์ชันไหน?

เมื่อต้องรวม DataFrame ลองถามตัวเองตามลำดับนี้ครับ:

  1. ข้อมูลมีโครงสร้างเหมือนกัน และต้องการต่อ (stack) เข้าด้วยกัน? → ใช้ pd.concat()
  2. ต้องการจับคู่ด้วยคอลัมน์ร่วม (key column)? → ใช้ pd.merge()
  3. Key อยู่ใน index อยู่แล้ว? → ใช้ .join() ได้เลย
  4. ต้องการหาข้อมูลที่ "ไม่อยู่" ในอีกตาราง? → ใช้ pd.merge(how="left_anti")

คำถามที่พบบ่อย (FAQ)

merge() กับ join() ต่างกันอย่างไร?

merge() ยืดหยุ่นกว่า สามารถ join บนคอลัมน์ไหนก็ได้ ส่วน join() ออกแบบมาสำหรับ join บน index เป็นหลัก ภายในจริงๆ แล้ว join() ก็เรียก merge() อยู่นั่นแหละ ถ้า key อยู่ใน index ใช้ .join() เขียนสั้นดี ถ้า key อยู่ในคอลัมน์ก็ merge() ไปเลย

DataFrame.append() ยังใช้ได้ไหมใน pandas 3.0?

ไม่ได้แล้วครับ DataFrame.append() ถูกลบออกตั้งแต่ pandas 2.0 ให้ใช้ pd.concat([df1, df2], ignore_index=True) แทนเสมอ

ทำไม merge แล้วแถวเพิ่มขึ้นมากกว่าที่คาดไว้?

เก้าในสิบครั้ง สาเหตุคือ key column มีค่าซ้ำในทั้ง 2 DataFrame ทำให้เกิด Cartesian product แนะนำให้ใช้ validate="one_to_one" หรือ validate="one_to_many" เพื่อตรวจจับปัญหานี้ก่อนที่จะลุกลาม

left_anti join ใน pandas 3.0 คืออะไร?

left_anti join เป็นฟีเจอร์ใหม่ใน pandas 3.0 ที่ช่วยหาแถวใน DataFrame ซ้ายที่ ไม่มี key ตรง กับ DataFrame ขวา เหมือน SQL LEFT ANTI JOIN เลย เหมาะสำหรับหาข้อมูลที่ขาดหาย เช่น ลูกค้าที่ไม่เคยสั่งซื้อ หรือสินค้าที่หมด stock

ควรใช้ concat หรือ merge ในการรวม DataFrame แนวนอน?

ถ้าต้องการจับคู่ข้อมูลตาม ค่าในคอลัมน์ (เช่น customer_id) ให้ใช้ merge() เสมอ ใช้ concat(axis=1) เฉพาะเมื่อต้องการวาง DataFrame เคียงกัน ตามตำแหน่ง index โดยไม่ต้องจับคู่ key ง่ายๆ คือ ถ้าต้องการ "แมทช์" ข้อมูล ใช้ merge ถ้าแค่ "วางข้างๆ กัน" ใช้ concat

เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.