01ภาพรวมของกราฟนี้
Beeswarm plot จัดตำแหน่งจุดอย่างเป็นระบบให้ไม่ทับกัน ต่างจาก strip plot ที่เบียดแบบสุ่ม ผลคือความกว้างของฝูงที่ระดับค่าใดบอกความหนาแน่นที่ระดับนั้นได้จริง จึงได้ทั้งข้อมูลรายจุดและรูปร่างการกระจายในภาพเดียว
เปรียบเทียบให้เห็นภาพ
เหมือนคนต่อแถวรอลิฟต์ที่ยืนกระจายออกด้านข้างเมื่อคนเยอะ ความกว้างของกลุ่มคนจึงบอกว่าตรงนั้นมีคนมากแค่ไหน
ใช้ที่ไหนบ้าง — แสดงผลการทดลองที่มีตัวอย่างปานกลาง เทียบการกระจายของหลายกลุ่มโดยยังโชว์ทุกจุด หรือใช้แทน box plot เมื่ออยากให้คนเห็นข้อมูลจริง
สร้างอย่างไร
- ขั้นที่ 1เรียงจุดตามค่า
- ขั้นที่ 2จัดตำแหน่งในแกนตั้งฉากให้ไม่ทับกัน
- ขั้นที่ 3เพิ่มเส้นมัธยฐาน
- ขั้นที่ 4ระบุจำนวนจุดของแต่ละกลุ่ม
ตัวอย่างคลาสสิก · Beeswarm ในงานข่าวข้อมูลของ The Economist และ FT
สื่อข้อมูลหันมาใช้ beeswarm มากขึ้นเพราะมันแสดง "ทุกประเทศ" หรือ "ทุกบริษัท" เป็นจุดที่นับได้ แทนที่จะสรุปเป็นค่าเฉลี่ย ผู้อ่านจึงเห็นทั้งรูปแบบรวมและเห็นว่าประเทศที่ตนสนใจอยู่ตรงไหนในภาพเดียว
02อธิบายภาษาคนเข้าใจง่าย ๆ
Strip plot แก้ปัญหาจุดทับกันด้วยการเบียดแบบสุ่ม ซึ่งได้ผลแต่มีข้อเสียคือความกว้างของกลุ่มจุดไม่ได้บอกอะไร มันเป็นแค่ความสุ่ม
Beeswarm จัดตำแหน่งอย่างเป็นระบบแทน โดยวางจุดแรกไว้กลาง จุดถัดไปที่ค่าใกล้กันก็ขยับออกด้านข้างพอดีที่จะไม่ทับ ผลคือฝูงจุดมีรูปร่างที่มีความหมาย
ทันทีที่ตำแหน่งมีความหมาย ความกว้างของฝูงที่ระดับค่าหนึ่งก็กลายเป็นตัวบอกความหนาแน่นที่ระดับนั้น เราจึงได้รูปร่างการกระจายแบบเดียวกับ violin plot มาฟรี
ข้อดีที่เหนือกว่า violin คือทุกจุดยังอยู่ครบและนับได้ ผู้อ่านจึงเห็นทั้งรูปร่างและปริมาณข้อมูลจริง ไม่ต้องเชื่อการประมาณใด ๆ
ข้อจำกัดคือเมื่อข้อมูลเยอะเกินไป ฝูงจะกว้างจนล้นพื้นที่ที่มี ในทางปฏิบัติใช้ได้ดีถึงราว 500 จุดต่อกลุ่ม เกินกว่านั้นต้องกลับไปใช้ violin หรือ box plot
03ข้อมูลที่ต้องใช้
คอลัมน์ที่ต้องเตรียม
| คอลัมน์ | ทำหน้าที่ | ชนิด | เข้ารหัสด้วย | จำนวนค่าที่เหมาะ | หมายเหตุ |
|---|---|---|---|---|---|
group | กลุ่ม | หมวดหมู่ | x | 1–8 กลุ่ม | แต่ละกลุ่มต้องมีพื้นที่กว้างพอให้ฝูงกระจาย |
value | ค่าที่วัดได้ | ตัวเลขต่อเนื่อง | y | ต่อเนื่อง | ข้อมูลดิบทุกจุด |
ต้องเตรียมข้อมูลอย่างไรก่อนวาด
- ตัดค่าว่างออก
- กำหนดขนาดจุดให้เหมาะกับจำนวนข้อมูล
- เตรียมค่ามัธยฐานไว้ลากเส้น
ค่าที่หายไป — ตัดค่าว่างและรายงานจำนวน เพราะจุดที่เห็นคือข้อมูลจริงที่ผู้อ่านนับได้
สเกลของแกน — linear เป็นหลัก และทุกกลุ่มใช้สเกลเดียวกัน
กราฟจะเริ่มพังเมื่อไร
เกิน 500 จุดต่อกลุ่ม ฝูงจะกว้างเกินพื้นที่ — ใช้ violin-plot แทน
หน้าตาไฟล์ที่ต้องเตรียม
group,value
…04อ่านแล้วเห็นอะไร
สิ่งที่เห็นได้จากภาพ
- ความหนาแน่นจากความกว้างของฝูงที่แต่ละระดับ
- จำนวนข้อมูลจริงซึ่งนับได้
- จุดที่แยกตัวออกไปจากฝูง
สิ่งที่กราฟนี้ตอบไม่ได้
- ตำแหน่งในแกนตั้งฉากไม่ได้แทนตัวแปรใด มันเป็นผลของการจัดวางเท่านั้น
- เมื่อข้อมูลเยอะมาก ฝูงจะล้นและบิดรูปร่าง
อ่านตามลำดับนี้
- ดูว่าฝูงกว้างที่ระดับค่าไหน นั่นคือช่วงที่ข้อมูลหนาแน่น
- นับจุดที่แยกตัวออกไป
- ดูเส้นมัธยฐานเพื่ออ่านค่ากลาง
คำถามที่ตอบได้
- ข้อมูลเกาะกลุ่มที่ค่าไหน
- กลุ่มนี้มีข้อมูลกี่จุด
- มีค่าผิดปกติกี่จุด
สัญญาณว่ามีอะไรผิด
- ฝูงล้นออกนอกพื้นที่ของกลุ่มจนทับกลุ่มข้างเคียง
- จุดใหญ่เกินไปจนต้องเบียดมากและรูปร่างบิด
05เหมาะกับการนำเสนอแบบไหน
ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย
- เพิ่มเส้นมัธยฐานพาดฝูง
- เขียนจำนวนจุดของแต่ละกลุ่ม
- ระบายสีจุดที่เป็นกรณีพิเศษให้ต่างออกไป
รูปแบบชื่อกราฟที่ควรใช้
ข้อมูลทั้ง <จำนวน> จุดของ <กลุ่ม> หนาแน่นที่สุดที่ <ค่า>
06เอาไปใช้ต่อ
โค้ดทุกชิ้นอ่านไฟล์ beeswarm-plot-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง
# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด beeswarm-plot-sample.csv จากหน้านี้ แล้ววางไว้ข้างสคริปต์
import matplotlib
matplotlib.use("Agg") # รันแบบไม่มีหน้าจอ ถ้ารันในเครื่องตัวเองลบบรรทัดนี้ได้
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
C1, C2, C3 = "#2a78d6", "#eb6834", "#1baf7a" # palette ของคลัง เปลี่ยนเป็นสีองค์กรได้
INK, SURFACE = "#0b0b0b", "#fcfcfb"
df = pd.read_csv("beeswarm-plot-sample.csv")
df["value"] = pd.to_numeric(df["value"], errors="coerce")
NOTE = "density" # ← ค่าจาก Playground
fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)
sns.swarmplot(data=df, x="group", y="value", ax=ax, color=C1, size=4.5, alpha=0.8)
for i, (g, v) in enumerate(df.groupby("group")["value"]):
ax.plot([i - 0.28, i + 0.28], [v.median()] * 2, color=INK, linewidth=2) # เส้นมัธยฐาน
counts = df.groupby("group").size()
ax.set_xticklabels([f"{k}\n(n={v})" for k, v in counts.items()])
ax.spines[["top", "right"]].set_visible(False) # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("beeswarm-plot.png", dpi=160)
print("เขียน beeswarm-plot.png แล้ว ·", len(df), "แถว")07ลองเล่นเพื่อเข้าใจ
ดูข้อมูลเป็นตาราง (4 แถว)
| กลุ่ม | n | ต่ำสุด | Q1 | มัธยฐาน | Q3 | สูงสุด |
|---|---|---|---|---|---|---|
| สาขา A | 50 | 50.0 | 54.8 | 57.2 | 59.4 | 63.6 |
| สาขา B | 50 | 55.9 | 62.7 | 66.3 | 70.6 | 76.6 |
| สาขา C | 50 | 55.6 | 60.2 | 61.6 | 64.1 | 68.1 |
| สาขา D | 50 | 64.4 | 69.3 | 73.7 | 76.6 | 84.9 |
ลองแล้วจะเห็นอะไร
- สังเกตอะไร: เทียบกับหน้า strip-plot ที่ใช้ข้อมูลเดียวกัน จะเห็นว่าความกว้างของ beeswarm มีความหมาย ส่วนของ strip plot เป็นแค่ความสุ่ม
ข้อควรระวัง
- ใช้กับข้อมูลเยอะเกินไปจนฝูงล้นและบิดรูปร่าง
- ใช้จุดใหญ่เกินไปจนต้องเบียดมากและความกว้างไม่สะท้อนความหนาแน่นจริง
- ตีความตำแหน่งในแกนตั้งฉากว่าเป็นตัวแปร ทั้งที่เป็นแค่การจัดวาง
- ไม่เพิ่มเส้นมัธยฐาน ทำให้ไม่มีค่าสรุปให้อ้างอิง
เมื่อไหร่ไม่ควรใช้กราฟนี้
- มีมากกว่า 500 จุดต่อกลุ่ม→ใช้แทน: violin-plot หรือ box-plot
- ต้องการความกระชับสูงสุด→ใช้แทน: box-plot
- มีข้อมูลน้อยมากและไม่ต้องกังวลเรื่องจุดทับ→ใช้แทน: strip-plot