เทคนิคที่ 066 · PYTHON / JAVASCRIPT

แผนภาพการไหลSankey Diagram

ตามรอยว่าของไหลจากไหนไปไหน และหายที่ไหน

4
ขั้นสูงควรรู้ก่อน · เข้าใจกราฟสัดส่วน และรู้จักข้อมูลแบบ edge list
ลองเล่น

01ภาพรวมของกราฟนี้

Sankey diagram แสดงการไหลของปริมาณจากโหนดหนึ่งไปอีกโหนด โดยความหนาของเส้นแปรตามปริมาณ จุดแข็งคือทำให้ "การสูญเสียระหว่างทาง" กลายเป็นสิ่งที่เห็นได้ เพราะความกว้างที่หดลงคือปริมาณที่หายไป

เปรียบเทียบให้เห็นภาพ

เหมือนแผนที่แม่น้ำที่สายน้ำใหญ่แตกเป็นสายย่อย บางสายไหลต่อไปทะเล บางสายซึมหายไปกลางทาง ความกว้างของสายบอกปริมาณน้ำ

ใช้ที่ไหนบ้าง — กรวยการขายที่ตามรอยผู้ใช้ทีละขั้น การไหลของพลังงานจากแหล่งถึงการใช้งาน งบประมาณจากแหล่งรายได้ถึงรายจ่าย หรือการย้ายถิ่น

สร้างอย่างไร

  1. ขั้นที่ 1จัดข้อมูลเป็นรายการเส้นเชื่อม source, target, value
  2. ขั้นที่ 2ตรวจว่าผลรวมขาเข้าและขาออกของโหนดกลางสมดุล
  3. ขั้นที่ 3จัดเรียงโหนดให้เส้นตัดกันน้อยที่สุด
  4. ขั้นที่ 4ติดตัวเลขที่โหนดและเส้นหลัก

ตัวอย่างคลาสสิก · แผนภาพการเดินทัพของนโปเลียนโดย Charles Minard ปี 1869

Minard วาดการเดินทัพไปมอสโกและถอยกลับ โดยความหนาของแถบคือจำนวนทหารที่เหลือ จาก 422,000 คนเหลือ 10,000 คน ความกว้างที่หดลงเรื่อย ๆ บอกเรื่องราวได้ชัดกว่าตัวเลขใด ๆ Edward Tufte เรียกมันว่าอาจเป็นกราฟสถิติที่ดีที่สุดเท่าที่เคยมีมา

Wikipedia · Charles Joseph Minard

02อธิบายภาษาคนเข้าใจง่าย ๆ

คำถามที่กราฟทั่วไปตอบไม่ได้คือ "ของ 100 ชิ้นที่เข้ามา ไปจบที่ไหนบ้าง" เพราะมันต้องตามรอยข้ามหลายขั้นพร้อมกัน

Sankey ตอบด้วยการให้ความหนาของเส้นแทนปริมาณ เส้นใหญ่คือทางที่ของส่วนใหญ่ไหลไป เส้นเล็กคือทางที่มีน้อย และการที่เส้นแตกออกก็คือการแยกทาง

สิ่งที่ทรงพลังที่สุดคือมันทำให้การสูญเสียเห็นได้ ถ้าโหนดหนึ่งรับเข้ามากว้าง 100 แต่ส่งออกรวมกว้างแค่ 60 ช่องว่าง 40 นั้นคือสิ่งที่หายไป ซึ่งเป็นคำถามที่สำคัญที่สุดในงานวิเคราะห์กรวย

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

ข้อจำกัดคือจำนวนเส้น เกินประมาณ 60 เส้นภาพจะพันกันจนตามรอยไม่ได้ ทางแก้คือรวมโหนดเล็กเป็น "อื่น ๆ" ก่อนวาดเสมอ

03ข้อมูลที่ต้องใช้

รูปทรงของข้อมูลรายการเส้นเชื่อม source–target–value
จำนวนมิติที่เข้ารหัสได้3 มิติ
จำนวนแถวที่เหมาะ5–40 เส้นเชื่อมขั้นต่ำ 3 แถว
เส้นฐานศูนย์ไม่เกี่ยวข้องไม่มีแกนค่า ความหมายอยู่ที่ความหนาของเส้นเทียบกัน

คอลัมน์ที่ต้องเตรียม

คอลัมน์ทำหน้าที่ชนิดเข้ารหัสด้วยจำนวนค่าที่เหมาะหมายเหตุ
sourceต้นทางโหนดpath3–20 โหนด★ ชื่อโหนดต้องสะกดตรงกันทุกแถว ระบบไม่รวมชื่อที่พิมพ์ต่างกันให้
targetปลายทางโหนดpath3–20 โหนดห้ามสร้างวงจรย้อนกลับ กราฟนี้รองรับเฉพาะการไหลไปข้างหน้า
valueปริมาณที่ไหลตัวเลขต่อเนื่องsizeบวกเท่านั้นค่าติดลบวาดไม่ได้ ถ้ามีการไหลกลับให้แยกเป็นเส้นของตัวเอง

ต้องเตรียมข้อมูลอย่างไรก่อนวาด

  1. ทำให้ชื่อโหนดเป็นมาตรฐานเดียวกัน ตัดช่องว่างและจัดตัวพิมพ์
  2. ตรวจว่าผลรวมขาเข้าและขาออกของโหนดกลางสมดุลกัน ถ้าไม่สมดุลต้องเพิ่มโหนด "สูญเสีย" ให้เห็น
  3. รวมโหนดที่ปริมาณน้อยกว่า 2% เป็น "อื่น ๆ" ก่อนวาด
  4. ตรวจว่าไม่มีวงจร เพราะ layout algorithm จะวนไม่จบ

ค่าที่หายไป — แถวที่ขาด source, target หรือ value ต้องตัดออกและรายงาน เพราะจะทำให้ยอดรวมของโหนดผิดโดยที่ภาพยังดูปกติ

สเกลของแกน — ความหนาของเส้นแปรตรงกับค่า ห้ามใช้ log เพราะจะทำลายคุณสมบัติ "ผลรวมของเส้นย่อยเท่ากับเส้นใหญ่" ซึ่งเป็นเหตุผลเดียวที่ใช้กราฟนี้

กราฟจะเริ่มพังเมื่อไร

เกิน 60 เส้น เส้นจะพันกันจนตามรอยไม่ได้ — เปลี่ยนไป alluvial-diagram ที่จัดเป็นขั้นชัดเจน หรือรวมโหนดเล็กเป็น "อื่น ๆ"

หน้าตาไฟล์ที่ต้องเตรียม

source,target,value
เข้าชมเว็บ,ดูสินค้า,7975
เข้าชมเว็บ,ออกทันที,4078

04อ่านแล้วเห็นอะไร

สิ่งที่เห็นได้จากภาพ

  • เส้นทางไหนใหญ่ที่สุด ปริมาณส่วนใหญ่ไหลไปทางไหน
  • หายที่ขั้นไหนมากที่สุด ความกว้างที่หดลงคือการสูญเสีย
  • โหนดไหนเป็นคอขวดที่ทุกอย่างต้องผ่าน
  • เส้นทางที่ไม่คาดคิด เช่น การย้อนกลับไปขั้นก่อนหน้า

สิ่งที่กราฟนี้ตอบไม่ได้

  • อ่านตัวเลขที่แม่นยำจากความหนาไม่ได้ ต้องติดป้ายกำกับ
  • ไม่บอกว่าเวลาผ่านไปเท่าไรระหว่างขั้น
  • เทียบสองช่วงเวลาบนภาพเดียวไม่ได้ ต้องวาดสองภาพ

อ่านตามลำดับนี้

  1. หาเส้นที่หนาที่สุดจากต้นทางก่อน
  2. ตามเส้นนั้นไปจนสุดปลายทาง
  3. ดูโหนดที่ความกว้างขาออกน้อยกว่าขาเข้า ซึ่งคือจุดที่สูญเสีย

คำถามที่ตอบได้

  • ของที่เข้ามาไปจบที่ไหนบ้าง
  • หายมากที่สุดตรงขั้นไหน
  • เส้นทางหลักคืออะไร

สัญญาณว่ามีอะไรผิด

  • ผลรวมขาเข้าไม่เท่าขาออกที่โหนดกลาง แปลว่าข้อมูลขาดหรือมีการนับซ้ำ
  • เส้นเล็กจำนวนมากจนดูเป็นฝอย แปลว่ายังไม่ได้รวมเป็น "อื่น ๆ"

05เหมาะกับการนำเสนอแบบไหน

ผู้ชมที่เหมาะผู้อ่านที่ต้องการเข้าใจกระบวนการ ไม่ใช่แค่ตัวเลขสรุป
เวลาที่ผู้ชมต้องใช้ต้องอธิบาย
ขนาดเล็กสุดที่ยังอ่านได้520×320 px
รายงานเชิงวิเคราะห์
สื่อสารกับคนทั่วไป
สไลด์ผู้บริหาร
สิ่งพิมพ์
มือถือต้องการความกว้างมากพอที่เส้นจะแยกออกจากกัน
dashboard ที่อัปเดตตลอดต้องใช้เวลาตามรอย ไม่เหมาะกับการเหลือบดู

ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย

  1. ติดตัวเลขที่โหนดทุกโหนดและที่เส้นหลัก
  2. ทำเครื่องหมายจุดที่สูญเสียมากที่สุด
  3. ใช้สีตามต้นทางเพื่อให้ตามสายได้

รูปแบบชื่อกราฟที่ควรใช้

จาก <ปริมาณ> ที่เข้ามา เหลือ <ปริมาณ> ที่ปลายทาง โดยหายมากที่สุดที่ <ขั้น>

06เอาไปใช้ต่อ

โค้ดทุกชิ้นอ่านไฟล์ sankey-diagram-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง

# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด sankey-diagram-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("sankey-diagram-sample.csv")
df["value"] = pd.to_numeric(df["value"], errors="coerce")
MINSHARE = 0   # ← ค่าจาก Playground

fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)

# matplotlib ไม่มี sankey ที่ใช้งานง่าย — แสดงเป็นตารางการไหลแทน
pivot = df.pivot_table(index="source", columns="target", values="value", aggfunc="sum").fillna(0)
im = ax.imshow(pivot, cmap="Blues", aspect="auto")
ax.set_xticks(range(len(pivot.columns))); ax.set_xticklabels(pivot.columns, rotation=35, ha="right", fontsize=8)
ax.set_yticks(range(len(pivot.index))); ax.set_yticklabels(pivot.index, fontsize=8)
for i in range(len(pivot.index)):
    for j in range(len(pivot.columns)):
        if pivot.iloc[i, j] > 0:
            ax.text(j, i, f"{pivot.iloc[i, j]:,.0f}", ha="center", va="center", fontsize=7)
fig.colorbar(im, ax=ax, label="ปริมาณที่ไหล", shrink=0.7)

ax.spines[["top", "right"]].set_visible(False)   # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("sankey-diagram.png", dpi=160)
print("เขียน sankey-diagram.png แล้ว ·", len(df), "แถว")

ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*" · ทดสอบกับเวอร์ชัน 3.10

07ลองเล่นเพื่อเข้าใจ

ข้อมูลตัวอย่าง · 8 แถว · seed 239● คำนวณใหม่ในเบราว์เซอร์ทันทีที่ปรับค่า
เกณฑ์รวมเส้นเล็กเป็น "อื่น ๆ"
เข้าชมเว็บ → ดูสินค้า · 7,975 (66.2%)เข้าชมเว็บ → ออกทันที · 4,078 (33.8%)ดูสินค้า → ใส่ตะกร้า · 3,083 (25.6%)ดูสินค้า → ออกกลางทาง · 5,112 (42.4%)ใส่ตะกร้า → กรอกที่อยู่ · 1,952 (16.2%)ใส่ตะกร้า → ทิ้งตะกร้า · 1,166 (9.7%)กรอกที่อยู่ → ชำระเงินสำเร็จ · 1,418 (11.8%)กรอกที่อยู่ → ชำระไม่สำเร็จ · 457 (3.8%)เข้าชมเว็บ · 12,053เข้าชมเว็บ · 12,053ดูสินค้า · 8,195ดูสินค้า · 8,195ออกทันที · 4,078ออกทันที · 4,078ออกกลางทาง · 5,112ออกกลางทาง · 5,112ใส่ตะกร้า · 3,118ใส่ตะกร้า · 3,118กรอกที่อยู่ · 1,952กรอกที่อยู่ · 1,952ทิ้งตะกร้า · 1,166ทิ้งตะกร้า · 1,166ชำระเงินสำเร็จ · 1,418ชำระเงินสำเร็จ · 1,418ชำระไม่สำเร็จ · 457ชำระไม่สำเร็จ · 457
จำนวนเส้นที่แสดง8
สัดส่วนที่หายระหว่างทาง84.4%
เส้นที่ใหญ่ที่สุดเข้าชมเว็บ → ดูสินค้า
ดูข้อมูลเป็นตาราง (8 แถว)
sankey diagram 8 เส้น จาก 9 โหนด
จากไปปริมาณ
เข้าชมเว็บดูสินค้า7,975
เข้าชมเว็บออกทันที4,078
ดูสินค้าใส่ตะกร้า3,083
ดูสินค้าออกกลางทาง5,112
ใส่ตะกร้ากรอกที่อยู่1,952
ใส่ตะกร้าทิ้งตะกร้า1,166
กรอกที่อยู่ชำระเงินสำเร็จ1,418
กรอกที่อยู่ชำระไม่สำเร็จ457

ลองแล้วจะเห็นอะไร

  • รวมเส้นที่เล็กกว่า (%): เพิ่มเกณฑ์แล้วสังเกตว่าภาพสะอาดขึ้นมากแต่ข้อมูลบางส่วนหายไป — การรวมเส้นเล็กเป็นการตัดสินใจเชิงบรรณาธิการที่ต้องเขียนบอกเสมอ

ข้อควรระวัง

  1. ผลรวมขาเข้าไม่เท่าขาออกที่โหนดกลาง โดยไม่เพิ่มโหนด "สูญเสีย" ให้เห็น
  2. ใส่เส้นมากเกินไปจนพันกันและตามรอยไม่ได้
  3. ใช้สเกล log กับความหนาของเส้น ซึ่งทำลายคุณสมบัติการบวกที่เป็นเหตุผลเดียวที่ใช้กราฟนี้
  4. ชื่อโหนดสะกดไม่ตรงกันจนกลายเป็นคนละโหนด
  5. ใช้กับข้อมูลที่ไม่ใช่การไหลจริง เช่น เอาความสัมพันธ์ทั่วไปมาวาดเป็นการไหล

เมื่อไหร่ไม่ควรใช้กราฟนี้

  • มีขั้นเดียวและไม่มีการแตกสายใช้แทน: funnel-chart
  • ทุกขั้นมีชุดหมวดเดียวกันใช้แทน: alluvial-diagram
  • การไหลเป็นสองทางหรือมีวงจรใช้แทน: chord-diagram
  • สนใจสัดส่วน ณ จุดเวลาเดียวใช้แทน: treemap หรือ stacked-bar

กราฟที่เกี่ยวข้อง

เทคนิคถัดไป · แผนภาพคอร์ด