01ภาพรวมของกราฟนี้
Cleveland dot plot ทิ้งทั้งแท่งและก้าน เหลือเพียงจุดเดียวต่อรายการ เพราะไม่มีองค์ประกอบใดสื่อ "ความยาวจากศูนย์" อีกแล้ว การย่อช่วงแกนจึงทำได้อย่างซื่อสัตย์ นี่ทำให้มันเป็นเครื่องมือเดียวในกลุ่มนี้ที่อ่านความต่างระดับไม่กี่เปอร์เซ็นต์ได้
เปรียบเทียบให้เห็นภาพ
เหมือนปักหมุดบนเส้นจำนวน เราสนใจแค่ว่าหมุดอยู่ตรงไหนเทียบกัน ไม่ได้สนใจว่าหมุดอยู่ห่างจากเลขศูนย์แค่ไหน จึงซูมเข้าไปดูช่วงที่หมุดกระจุกได้
ใช้ที่ไหนบ้าง — เทียบอายุขัยเฉลี่ยระหว่างประเทศที่ต่างกันไม่กี่ปี เทียบคะแนนความพึงพอใจที่อยู่ระหว่าง 4.1 ถึง 4.6 หรือเทียบค่าที่ต่างกันน้อยแต่สำคัญ
สร้างอย่างไร
- ขั้นที่ 1เรียงตามค่า
- ขั้นที่ 2เลือกช่วงแกนที่ครอบข้อมูลพอดีและระบุว่าไม่ได้เริ่มที่ศูนย์
- ขั้นที่ 3วาดจุดที่ตำแหน่งค่า
- ขั้นที่ 4ลากเส้นจาง ๆ ตามแถวเพื่อช่วยลากสายตา
ตัวอย่างคลาสสิก · การทดลองของ Cleveland และ McGill ปี 1984
ทั้งสองทดลองกับคนจริงว่าช่องทางการเข้ารหัสภาพแบบไหนอ่านค่าได้แม่นที่สุด ผลคือ "ตำแหน่งบนแกนร่วม" ชนะทุกอย่าง และนำไปสู่การเสนอ dot plot เป็นทางเลือกแทนกราฟแท่งในกรณีที่ค่าต่างกันน้อย ซึ่งเป็นงานที่ยังถูกอ้างอิงจนถึงทุกวันนี้
02อธิบายภาษาคนเข้าใจง่าย ๆ
ปัญหาที่กราฟแท่งแก้ไม่ได้คือ เมื่อค่าทุกตัวใกล้กันมาก เช่น อายุขัยของประเทศพัฒนาแล้วที่อยู่ระหว่าง 79 ถึง 84 ปี แท่งทุกแท่งจะยาวเกือบเท่ากันจนแยกไม่ออก
เราแก้ด้วยการตัดแกนไม่ได้ เพราะแท่งสื่อความยาว การตัดแกนจะทำให้ภาพโกหกทันที นี่คือทางตันที่ dot plot เกิดขึ้นมาเพื่อแก้
วิธีคือทิ้งทั้งแท่งและก้าน เหลือแค่จุดเดียวที่ตำแหน่งของค่า พอไม่มีอะไรสื่อ "ระยะจากศูนย์" แล้ว การซูมแกนเข้าไปที่ช่วง 78-85 ปีจึงไม่ได้บิดอะไรเลย มันเป็นแค่การเปลี่ยนกำลังขยายของกล้อง
ข้อแลกเปลี่ยนคือคนอ่านต้องดูแกนก่อนเสมอ ซึ่งต่างจากกราฟแท่งที่อ่านความยาวได้เลย ดังนั้นต้องเขียนให้ชัดว่าแกนเริ่มที่เท่าไร และควรใส่ค่าอ้างอิงเช่นค่าเฉลี่ยไว้ในภาพ
William Cleveland ผู้เสนอวิธีนี้ทำการทดลองกับคนจริงและพบว่ามันอ่านได้แม่นกว่ากราฟแท่งในสถานการณ์แบบนี้ ภาพจำคือ ค่าต่างกันมาก ใช้แท่ง ค่าต่างกันน้อย ใช้จุด
03ข้อมูลที่ต้องใช้
คอลัมน์ที่ต้องเตรียม
| คอลัมน์ | ทำหน้าที่ | ชนิด | เข้ารหัสด้วย | จำนวนค่าที่เหมาะ | หมายเหตุ |
|---|---|---|---|---|---|
category | รายการ | หมวดหมู่ | y | 5–40 รายการ | รองรับรายการเยอะเพราะแต่ละแถวใช้ที่น้อย |
value | ค่า | ตัวเลขต่อเนื่อง | x | ต่อเนื่อง | ไม่จำเป็นต้องมีศูนย์ที่มีความหมาย เช่น อุณหภูมิหรือดัชนี |
ต้องเตรียมข้อมูลอย่างไรก่อนวาด
- เรียงตามค่า
- เลือกช่วงแกนให้ครอบข้อมูลพอดีแต่ไม่บีบจนจุดชนขอบ
- เตรียมค่าอ้างอิงเช่นค่าเฉลี่ยหรือค่ามัธยฐานไว้ลากเส้น
ค่าที่หายไป — แสดงแถวไว้พร้อมเครื่องหมายว่าไม่มีข้อมูล เพื่อให้ผู้อ่านรู้ว่ารายการนี้มีอยู่
สเกลของแกน — linear หรือ log ก็ได้ เพราะไม่มีองค์ประกอบใดสื่อความยาว แต่ต้องระบุสเกลให้ชัด
กราฟจะเริ่มพังเมื่อไร
เกิน 45 รายการ แถวจะชิดจนกดและอ่านลำบาก
หน้าตาไฟล์ที่ต้องเตรียม
category,value
…04อ่านแล้วเห็นอะไร
สิ่งที่เห็นได้จากภาพ
- อันดับของทุกรายการแม้ค่าจะต่างกันเล็กน้อย
- กลุ่มของรายการที่เกาะกันเป็นก้อน
- ช่องว่างที่แยกกลุ่มหนึ่งออกจากอีกกลุ่ม
- รายการที่โดดออกไปไกลจากกลุ่มหลัก
สิ่งที่กราฟนี้ตอบไม่ได้
- ไม่บอกสัดส่วน — จุดที่อยู่ขวาสุดไม่ได้แปลว่ามีค่าเป็นสองเท่าของจุดซ้ายสุด
- ไม่บอกว่าความต่างมีนัยสำคัญหรือไม่ ต้องเพิ่ม error bar
อ่านตามลำดับนี้
- อ่านช่วงของแกนก่อนเสมอ เพราะแกนอาจไม่ได้เริ่มที่ศูนย์
- ดูตำแหน่งจุดเทียบกัน ไม่ใช่เทียบกับขอบภาพ
- หาช่องว่างที่แบ่งจุดเป็นกลุ่ม
คำถามที่ตอบได้
- รายการเหล่านี้แบ่งเป็นกี่กลุ่มโดยธรรมชาติ
- รายการที่สนใจอยู่เหนือหรือใต้ค่าเฉลี่ย
- ความต่างระหว่างอันดับหนึ่งกับอันดับสุดท้ายกว้างแค่ไหนในหน่วยจริง
สัญญาณว่ามีอะไรผิด
- ไม่ระบุว่าแกนเริ่มที่เท่าไร
- ช่วงแกนบีบจนความต่าง 0.1% ดูเหมือนคนละโลก
- ไม่มีค่าอ้างอิงให้เทียบ
05เหมาะกับการนำเสนอแบบไหน
ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย
- เขียนตัวใหญ่ว่าแกนเริ่มที่เท่าไร
- ลากเส้นค่าเฉลี่ยหรือค่ามัธยฐานไว้ให้เทียบ
- ระบายสีจุดของรายการที่สนใจ
รูปแบบชื่อกราฟที่ควรใช้
<รายการ> อยู่ที่ <ค่า> สูงกว่าค่ากลางที่ <ค่ากลาง> อยู่ <ส่วนต่าง>
06เอาไปใช้ต่อ
โค้ดทุกชิ้นอ่านไฟล์ dot-plot-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง
# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด dot-plot-sample.csv จากหน้านี้ แล้ววางไว้ข้างสคริปต์
import matplotlib
matplotlib.use("Agg") # รันแบบไม่มีหน้าจอ ถ้ารันในเครื่องตัวเองลบบรรทัดนี้ได้
import matplotlib.pyplot as plt
import pandas as pd
C1, C2, C3 = "#2a78d6", "#eb6834", "#1baf7a" # palette ของคลัง เปลี่ยนเป็นสีองค์กรได้
INK, SURFACE = "#0b0b0b", "#fcfcfb"
df = pd.read_csv("dot-plot-sample.csv")
df["value"] = pd.to_numeric(df["value"], errors="coerce")
SORT = "value-desc" # ← ค่าจาก Playground
fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)
d = df.sort_values("value")
ax.hlines(d["category"], d["value"].min() * 0.98, d["value"], color="#e6e4de", linewidth=1) # เส้นนำสายตา
ax.plot(d["value"], d["category"], "o", markersize=9, color=C1)
ax.axvline(d["value"].mean(), color=INK, linestyle="--", linewidth=1.2) # ค่าอ้างอิงให้เทียบ
ax.set_xlim(d["value"].min() * 0.95, d["value"].max() * 1.05) # ตัดแกนได้ เพราะไม่มี mark ใดสื่อความยาว
ax.set_xlabel("ยอดขาย (แกนไม่ได้เริ่มที่ศูนย์)")
ax.spines[["top", "right"]].set_visible(False) # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("dot-plot.png", dpi=160)
print("เขียน dot-plot.png แล้ว ·", len(df), "แถว")07ลองเล่นเพื่อเข้าใจ
ดูข้อมูลเป็นตาราง (12 แถว)
| หมวด | ค่า |
|---|---|
| กรุงเทพฯ | 502 |
| ชลบุรี | 464 |
| ขอนแก่น | 457 |
| เชียงใหม่ | 455 |
| ภูเก็ต | 419 |
| สงขลา | 363 |
| นครราชสีมา | 344 |
| สุราษฎร์ธานี | 275 |
| อุดรธานี | 267 |
| พิษณุโลก | 216 |
| ระยอง | 183 |
| นครปฐม | 172 |
ลองแล้วจะเห็นอะไร
- การเรียงลำดับ: เรียงตามค่าแล้วจะเห็นทันทีว่ารายการแบ่งเป็นกี่กลุ่ม ซึ่งเป็นสิ่งที่กราฟชนิดนี้ทำได้ดีกว่ากราฟแท่ง
ข้อควรระวัง
- ตัดแกนแล้วไม่เขียนบอก ทำให้ผู้อ่านตีความความต่างเกินจริงหลายเท่า
- บีบช่วงแกนจนความต่างที่ไม่มีนัยสำคัญดูเหมือนช่องว่างมหาศาล
- ใช้กับข้อมูลที่ค่าต่างกันมาก ซึ่งกราฟแท่งสื่อสารได้ดีกว่า
- ไม่ใส่ค่าอ้างอิงให้เทียบ ทำให้จุดลอยอยู่โดยไม่มีบริบท
เมื่อไหร่ไม่ควรใช้กราฟนี้
- ค่าต่างกันหลายเท่าและศูนย์มีความหมาย→ใช้แทน: bar-chart หรือ horizontal-bar
- ผู้ชมดูผ่าน ๆ และไม่มีเวลาตรวจแกน→ใช้แทน: bar-chart ที่ตีความผิดได้ยากกว่า
- ต้องเทียบสองจุดเวลา→ใช้แทน: dumbbell-chart