01ภาพรวมของกราฟนี้
Strip plot ไม่สรุปอะไรเลย มันวาดทุกจุดข้อมูลตามค่าจริงและเบียดตำแหน่งเล็กน้อยไม่ให้ทับกันสนิท เมื่อข้อมูลมีไม่มาก นี่คือวิธีที่ซื่อสัตย์ที่สุด เพราะค่าสรุปอย่างมัธยฐานจากข้อมูล 8 จุดอาจให้ภาพที่มั่นใจเกินจริง
เปรียบเทียบให้เห็นภาพ
เหมือนเทเมล็ดถั่วลงบนไม้บรรทัดแล้วให้มันกระจายตามขนาด เราเห็นเมล็ดทุกเมล็ดโดยไม่มีใครมาสรุปให้
ใช้ที่ไหนบ้าง — ผลการทดลองที่มีตัวอย่างน้อย คะแนนของทีมขนาดเล็ก หรือใช้วาดทับ box plot เพื่อแสดงข้อมูลจริง
สร้างอย่างไร
- ขั้นที่ 1วาดจุดทุกจุดตามค่าของมัน
- ขั้นที่ 2เบียดตำแหน่งในแกนตั้งฉากเล็กน้อย
- ขั้นที่ 3ใช้ความโปร่งใสเมื่อจุดเริ่มเยอะ
- ขั้นที่ 4เพิ่มเส้นมัธยฐานถ้าต้องการค่าสรุป
ตัวอย่างคลาสสิก · ขบวนการแสดงจุดข้อมูลจริงในวารสารชีววิทยา
วารสารหลายฉบับเริ่มบังคับให้งานวิจัยที่มีตัวอย่างน้อยแสดงจุดข้อมูลทุกจุด แทนที่จะแสดงเฉพาะค่าเฉลี่ยกับขา เพราะพบว่าข้อมูลที่กระจายคนละแบบให้กราฟแท่งหน้าตาเหมือนกันได้ และการเห็นจุดจริงเปลี่ยนการตีความอย่างมีนัยสำคัญ
02อธิบายภาษาคนเข้าใจง่าย ๆ
เมื่อข้อมูลมีแค่ 10-30 จุดต่อกลุ่ม การวาด box plot เป็นการสรุปที่อันตราย เพราะควอไทล์จากข้อมูลไม่กี่จุดแกว่งมาก และภาพกล่องที่เรียบร้อยทำให้ดูน่าเชื่อถือเกินความจริง
Strip plot เลือกทางตรงข้ามคือไม่สรุปเลย วาดทุกจุดที่มีจริงลงไป ผู้อ่านจึงเห็นด้วยตาตัวเองว่ามีข้อมูลกี่จุดและมันเกาะกลุ่มกันอย่างไร
ปัญหาเดียวคือจุดที่มีค่าเท่ากันจะทับกันสนิทจนนับไม่ได้ ทางแก้คือเบียดตำแหน่งในแนวตั้งฉากเล็กน้อย ซึ่งเรียกว่า jitter การเบียดนี้ไม่มีความหมายใด ๆ มีไว้เพื่อให้เห็นจุดเท่านั้น
เพราะ jitter ไม่มีความหมาย จึงต้องใช้ให้น้อยที่สุดเท่าที่ยังเห็นจุดได้ ถ้าเบียดมากเกินไป ผู้อ่านอาจคิดว่าตำแหน่งในแนวนั้นสื่ออะไรบางอย่าง
เมื่อข้อมูลเริ่มเยอะเกิน 200-300 จุดต่อกลุ่ม การเบียดจะไม่พอ ต้องเปลี่ยนไปใช้ beeswarm ที่จัดจุดอย่างเป็นระบบ หรือกลับไปใช้ box plot เพราะตอนนั้นการสรุปเริ่มเชื่อถือได้แล้ว
03ข้อมูลที่ต้องใช้
คอลัมน์ที่ต้องเตรียม
| คอลัมน์ | ทำหน้าที่ | ชนิด | เข้ารหัสด้วย | จำนวนค่าที่เหมาะ | หมายเหตุ |
|---|---|---|---|---|---|
group | กลุ่ม | หมวดหมู่ | x | 1–10 กลุ่ม | ถ้ามีกลุ่มเดียวก็ใช้ได้ |
value | ค่าที่วัดได้ | ตัวเลขต่อเนื่อง | y | ต่อเนื่อง | ต้องเป็นข้อมูลดิบทุกจุด |
ต้องเตรียมข้อมูลอย่างไรก่อนวาด
- ตัดค่าว่างออก
- กำหนดขนาดของ jitter ให้น้อยที่สุดเท่าที่ยังเห็นจุด
- ตัดสินใจว่าจะเพิ่มเส้นมัธยฐานหรือไม่
ค่าที่หายไป — ตัดค่าว่างและรายงานจำนวน เพราะจุดที่เห็นคือจำนวนข้อมูลจริงที่ผู้อ่านจะนับ
สเกลของแกน — linear เป็นหลัก ใช้ log ได้ถ้าค่ากินหลายเท่า
กราฟจะเริ่มพังเมื่อไร
เกิน 300 จุดต่อกลุ่ม จุดจะทับกันจนนับไม่ได้ — ใช้ beeswarm-plot หรือ box-plot
หน้าตาไฟล์ที่ต้องเตรียม
group,value
…04อ่านแล้วเห็นอะไร
สิ่งที่เห็นได้จากภาพ
- จำนวนข้อมูลจริงของแต่ละกลุ่ม ซึ่งนับได้ด้วยตา
- ตำแหน่งของทุกจุดโดยไม่มีการสรุป
- ช่องว่างในข้อมูลที่การสรุปจะกลบไป
สิ่งที่กราฟนี้ตอบไม่ได้
- ไม่ให้ค่าสรุปโดยตรง ต้องเพิ่มเส้นมัธยฐานเอง
- เมื่อจุดเยอะจะทับกันจนประเมินความหนาแน่นผิด
อ่านตามลำดับนี้
- นับหรือกะจำนวนจุดของแต่ละกลุ่ม
- ดูว่าจุดเกาะกันที่ระดับไหน
- หาจุดที่แยกตัวออกไป
คำถามที่ตอบได้
- กลุ่มนี้มีข้อมูลกี่จุดจริง ๆ
- จุดเกาะกลุ่มกันหรือกระจาย
- มีจุดที่โดดออกไปไหม
สัญญาณว่ามีอะไรผิด
- ใช้ jitter มากจนดูเหมือนตำแหน่งแนวนอนมีความหมาย
- จุดทับกันจนดูเหมือนข้อมูลน้อยกว่าความจริง
05เหมาะกับการนำเสนอแบบไหน
ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย
- เขียนจำนวนจุดของแต่ละกลุ่มไว้กำกับ
- เพิ่มเส้นมัธยฐานเมื่อต้องการค่าสรุป
- ระบุว่ามีการใส่ jitter และมันไม่มีความหมาย
รูปแบบชื่อกราฟที่ควรใช้
ข้อมูลทั้ง <จำนวน> จุดของ <กลุ่ม> เกาะอยู่ระหว่าง <ช่วง>
06เอาไปใช้ต่อ
โค้ดทุกชิ้นอ่านไฟล์ strip-plot-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง
# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด strip-plot-sample.csv จากหน้านี้ แล้ววางไว้ข้างสคริปต์
import matplotlib
matplotlib.use("Agg") # รันแบบไม่มีหน้าจอ ถ้ารันในเครื่องตัวเองลบบรรทัดนี้ได้
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
C1, C2, C3 = "#2a78d6", "#eb6834", "#1baf7a" # palette ของคลัง เปลี่ยนเป็นสีองค์กรได้
INK, SURFACE = "#0b0b0b", "#fcfcfb"
df = pd.read_csv("strip-plot-sample.csv")
df["value"] = pd.to_numeric(df["value"], errors="coerce")
NOTE = "count" # ← ค่าจาก Playground
fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)
groups = list(df["group"].unique())
rng = np.random.default_rng(42) # ตรึง seed ให้ jitter เหมือนเดิมทุกครั้ง
for i, g in enumerate(groups):
v = df[df["group"] == g]["value"]
jitter = rng.uniform(-0.12, 0.12, len(v)) # jitter ไม่มีความหมาย มีไว้กันจุดทับ
ax.scatter(np.full(len(v), i) + jitter, v, s=34, alpha=0.6, color=C1)
ax.plot([i - 0.2, i + 0.2], [v.median()] * 2, color=INK, linewidth=2)
ax.set_xticks(range(len(groups)))
ax.set_xticklabels([f"{g}\n(n={(df['group'] == g).sum()})" for g in groups])
ax.spines[["top", "right"]].set_visible(False) # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("strip-plot.png", dpi=160)
print("เขียน strip-plot.png แล้ว ·", len(df), "แถว")07ลองเล่นเพื่อเข้าใจ
ดูข้อมูลเป็นตาราง (4 แถว)
| กลุ่ม | n | ต่ำสุด | Q1 | มัธยฐาน | Q3 | สูงสุด |
|---|---|---|---|---|---|---|
| สาขา A | 20 | 50.0 | 55.7 | 58.0 | 60.2 | 63.4 |
| สาขา B | 20 | 55.9 | 62.6 | 64.3 | 67.1 | 72.3 |
| สาขา C | 20 | 55.6 | 60.2 | 61.4 | 63.9 | 67.2 |
| สาขา D | 20 | 66.2 | 72.4 | 74.5 | 78.1 | 84.9 |
ลองแล้วจะเห็นอะไร
- สังเกตอะไร: ลองนับจุดของแต่ละกลุ่ม แล้วเทียบกับหน้า box-plot ที่ใช้ข้อมูลเดียวกัน จะเห็นว่ากล่องไม่เคยบอกเลยว่ามีข้อมูลกี่จุด
ข้อควรระวัง
- ใส่ jitter มากเกินไปจนผู้อ่านคิดว่าตำแหน่งแนวนอนมีความหมาย
- ใช้กับข้อมูลเยอะเกินไปจนจุดทับกันและประเมินความหนาแน่นผิด
- ไม่ตรึง seed ของ jitter ทำให้ภาพเปลี่ยนทุกครั้งที่รัน
- ไม่ใส่เส้นมัธยฐาน ทำให้ผู้อ่านต้องกะค่ากลางเอง
เมื่อไหร่ไม่ควรใช้กราฟนี้
- มีข้อมูลมากกว่า 300 จุดต่อกลุ่ม→ใช้แทน: beeswarm-plot หรือ box-plot
- ต้องการค่าสรุปเป็นหลัก→ใช้แทน: box-plot
- ต้องการเห็นรูปร่างการกระจาย→ใช้แทน: violin-plot หรือ histogram