01ภาพรวมของกราฟนี้
Flow map ลากเส้นจากต้นทางไปปลายทางโดยความหนาแปรตามปริมาณ เหมาะกับการย้ายถิ่น การค้า หรือการเดินทาง จุดอ่อนคือเส้นตัดกันเยอะจนอ่านไม่ได้เมื่อมีเส้นทางมากกว่าสามสิบเส้น
เปรียบเทียบให้เห็นภาพ
เหมือนแผนภาพเส้นทางบินในนิตยสารบนเครื่องบิน ที่เส้นหนาแปลว่าเส้นทางนั้นมีเที่ยวบินเยอะ
ใช้ที่ไหนบ้าง — การย้ายถิ่นระหว่างจังหวัด เส้นทางการค้าระหว่างประเทศ การเดินทางไปทำงานข้ามเขต หรือการไหลของสินค้าในโซ่อุปทาน
สร้างอย่างไร
- ขั้นที่ 1เตรียมข้อมูลคู่ต้นทางปลายทางพร้อมปริมาณ
- ขั้นที่ 2ลากเส้นโค้งเพื่อลดการทับซ้อน
- ขั้นที่ 3ให้ความหนาแปรตามปริมาณและใส่หัวลูกศรบอกทิศ
- ขั้นที่ 4แสดงเฉพาะเส้นทางที่ใหญ่ที่สุดเมื่อเส้นเยอะ
ตัวอย่างคลาสสิก · แผนที่การไหลของสินค้าโดย Charles Minard
Minard วาดแผนที่การไหลจำนวนมากในคริสต์ศตวรรษที่ 19 เช่น การส่งออกถ่านหินและการย้ายถิ่น โดยใช้ความหนาของแถบแทนปริมาณ ซึ่งวางรากฐานให้กับรูปแบบนี้ทั้งหมด และยังเป็นแรงบันดาลใจโดยตรงของ sankey diagram ที่ตัดแผนที่ออกแล้วเหลือแต่การไหล
02อธิบายภาษาคนเข้าใจง่าย ๆ
ข้อมูลการเคลื่อนย้ายมีสามส่วนพร้อมกัน คือมาจากไหน ไปไหน และมากแค่ไหน ซึ่งตารางแสดงได้แต่ทำให้เห็นรูปแบบเชิงพื้นที่ไม่ได้
Flow map รวมทั้งสามส่วนไว้ในภาพเดียว โดยปลายเส้นบอกต้นทางปลายทาง หัวลูกศรบอกทิศ และความหนาบอกปริมาณ
ผลคือรูปแบบใหญ่ ๆ เช่น ทุกเส้นทางใหญ่มุ่งเข้าเมืองหลวง หรือการค้ากระจุกอยู่ในภูมิภาคเดียวกัน ปรากฏขึ้นทันที
ราคาที่จ่ายอย่างแรกคือเส้นตัดกัน เมื่อเกินราวสามสิบเส้น ภาพจะกลายเป็นพันกันจนบอกไม่ได้ว่าเส้นไหนไปไหน
อย่างที่สองคือเส้นทางสั้นเสียเปรียบ เส้นระหว่างสองเมืองที่อยู่ใกล้กันมองเห็นยากกว่าเส้นยาวข้ามประเทศ แม้จะมีปริมาณมากกว่า ซึ่งเป็นการบิดที่ติดมากับรูปแบบนี้
ทางแก้ที่ใช้ได้จริงคือตัดให้เหลือเฉพาะเส้นทางที่ใหญ่ที่สุดสิบถึงยี่สิบเส้น แล้วระบุไว้ตรง ๆ ว่าตัดไปเท่าไรและเหลือสัดส่วนเท่าไรของทั้งหมด
03ข้อมูลที่ต้องใช้
คอลัมน์ที่ต้องเตรียม
| คอลัมน์ | ทำหน้าที่ | ชนิด | เข้ารหัสด้วย | จำนวนค่าที่เหมาะ | หมายเหตุ |
|---|---|---|---|---|---|
province | เขตต้นทาง | เชิงพื้นที่ | path | 5–40 จุด | ต้องมีพิกัด |
regionไม่บังคับ | ภูมิภาค | หมวดหมู่ | color | 4–8 กลุ่ม | ใช้สีบอกภูมิภาคต้นทาง |
rate | ปริมาณที่ไหล | ตัวเลขต่อเนื่อง | size | บวกเท่านั้น | ควบคุมความหนาของเส้น |
ต้องเตรียมข้อมูลอย่างไรก่อนวาด
- ★ ตัดให้เหลือเฉพาะเส้นทางที่ใหญ่ที่สุดและระบุสัดส่วนที่เหลือ
- หาพิกัดของต้นทางและปลายทาง
- รวมเส้นทางไปกลับเป็นเส้นเดียวถ้าทิศทางไม่สำคัญ
ค่าที่หายไป — เส้นทางที่ไม่มีพิกัดครบต้องตัดออกและรายงานจำนวน
สเกลของแกน — ความหนาแปรตรงกับปริมาณ และเส้นหนาที่สุดไม่ควรเกินราวยี่สิบพิกเซล
กราฟจะเริ่มพังเมื่อไร
เกิน 40 เส้นทาง เส้นพันกันจนอ่านไม่ได้
หน้าตาไฟล์ที่ต้องเตรียม
province,region,rate
กรุงเทพฯ,เหนือ,26.6
เชียงใหม่,อีสาน,34.7
…04อ่านแล้วเห็นอะไร
สิ่งที่เห็นได้จากภาพ
- เส้นทางที่มีปริมาณมากที่สุด
- ศูนย์กลางที่มีเส้นเข้าออกมากที่สุด
- ทิศทางรวมของการเคลื่อนย้าย
- ภูมิภาคที่เชื่อมกันเองมากกว่าเชื่อมออกนอก
สิ่งที่กราฟนี้ตอบไม่ได้
- ★ เส้นทางระหว่างที่ใกล้กันมองเห็นยากกว่าเส้นทางไกล แม้ปริมาณจะมากกว่า
- อ่านปริมาณที่แม่นยำจากความหนา
- เส้นที่ถูกเส้นอื่นทับ
อ่านตามลำดับนี้
- มองหาเส้นที่หนาที่สุดก่อน
- ดูจุดที่มีเส้นเข้าออกมากที่สุด
- อ่านคำอธิบายว่าตัดเส้นทางเล็กออกไปเท่าไร
คำถามที่ตอบได้
- เส้นทางไหนมีปริมาณมากที่สุด
- ที่ไหนเป็นศูนย์กลาง
- การเคลื่อนย้ายไหลไปทางไหนโดยรวม
สัญญาณว่ามีอะไรผิด
- ไม่ระบุว่าตัดเส้นทางเล็กออกไปกี่เส้น
- เส้นตัดกันจนตามไม่ได้
- ไม่มีหัวลูกศรทั้งที่ทิศทางสำคัญ
05เหมาะกับการนำเสนอแบบไหน
ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย
- ★ ระบุว่าแสดงกี่เส้นทางจากทั้งหมดกี่เส้นทาง
- ติดค่ากำกับเส้นที่ใหญ่ที่สุดสามเส้น
- ใช้ความโปร่งใสเพื่อให้เห็นเส้นที่ทับกัน
รูปแบบชื่อกราฟที่ควรใช้
การไหลของ <สิ่งที่ไหล> โดย <ต้นทาง> ไป <ปลายทาง> มากที่สุด
06เอาไปใช้ต่อ
โค้ดทุกชิ้นอ่านไฟล์ flow-map-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง
# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด flow-map-sample.csv จากหน้านี้ แล้ววางไว้ข้างสคริปต์
import matplotlib
matplotlib.use("Agg") # รันแบบไม่มีหน้าจอ ถ้ารันในเครื่องตัวเองลบบรรทัดนี้ได้
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
import numpy as np
C1, C2, C3 = "#2a78d6", "#eb6834", "#1baf7a" # palette ของคลัง เปลี่ยนเป็นสีองค์กรได้
INK, SURFACE = "#0b0b0b", "#fcfcfb"
df = pd.read_csv("flow-map-sample.csv")
df["rate"] = pd.to_numeric(df["rate"], errors="coerce")
NOTE = "short" # ← ค่าจาก Playground
fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)
base = gpd.read_file("provinces.geojson")
base.plot(ax=ax, facecolor="#f2f1ed", edgecolor="#e2e0da", linewidth=0.5)
top = df.nlargest(15, "rate") # ตัดให้เหลือเส้นทางใหญ่
for _, r in top.sort_values("rate").iterrows(): # วาดเส้นเล็กก่อน
x0, y0 = COORD[r["source"]]; x1, y1 = COORD[r["target"]]
ax.annotate("", xy=(x1, y1), xytext=(x0, y0),
arrowprops=dict(arrowstyle="-|>", color=C1, alpha=0.7,
lw=0.8 + r["rate"] / top["rate"].max() * 5,
connectionstyle="arc3,rad=0.18"))
ax.set_axis_off()
ax.set_title(f"แสดง {len(top)} จาก {len(df)} เส้นทาง", loc="left")
ax.spines[["top", "right"]].set_visible(False) # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("flow-map.png", dpi=160)
print("เขียน flow-map.png แล้ว ·", len(df), "แถว")07ลองเล่นเพื่อเข้าใจ
เส้นที่ยาวกว่ากินพื้นที่มากกว่า จึงดูสำคัญกว่าทั้งที่ปริมาณอาจน้อยกว่า
ดูข้อมูลเป็นตาราง (9 แถว)
| ต้นทาง | ปลายทาง | ปริมาณ |
|---|---|---|
| เชียงใหม่ | อุดรธานี | 26.6 |
| พิษณุโลก | นครปฐม | 34.7 |
| อุดรธานี | ระยอง | 32.7 |
| ขอนแก่น | สงขลา | 34.4 |
| นครราชสีมา | อุดรธานี | 26.8 |
| กรุงเทพฯ | ระยอง | 10.4 |
| ชลบุรี | สงขลา | 23.1 |
| ระยอง | อุดรธานี | 20.1 |
| สุราษฎร์ธานี | นครปฐม | 17.9 |
ลองแล้วจะเห็นอะไร
- สังเกตอะไร: สังเกตว่าเส้นสั้นที่หนาเท่ากับเส้นยาวกลับดึงสายตาน้อยกว่ามาก เพราะพื้นที่หมึกรวมน้อยกว่า
ข้อควรระวัง
- ไม่ตัดเส้นทางเล็กออก ทำให้เส้นพันกันจนอ่านไม่ได้
- ไม่ระบุว่าแสดงกี่เส้นทางจากทั้งหมดเท่าไร
- ลืมว่าเส้นทางสั้นเสียเปรียบเชิงสายตาโดยโครงสร้าง
- วาดเส้นใหญ่ก่อนเส้นเล็ก ทำให้เส้นเล็กทับเส้นใหญ่
เมื่อไหร่ไม่ควรใช้กราฟนี้
- ตำแหน่งทางภูมิศาสตร์ไม่สำคัญ→ใช้แทน: sankey-diagram หรือ chord-diagram
- มีเส้นทางมากกว่า 40 เส้น→ใช้แทน: adjacency-matrix
- สนใจแค่ปริมาณรวมของแต่ละที่→ใช้แทน: graduated-symbol-map