從一張圖到一整面牆
上一篇〈Python matplotlib 入門〉裡,我們幫一家手搖飲料店畫出了折線圖、長條圖、散佈圖,也解決了中文變方框的問題。每次都是一張圖、一個視窗。
但真正在看數據的時候,你不會想開四個視窗來回切——老闆要的是一眼看完:這週業績走勢、哪個品項最好賣、天氣熱不熱有沒有差、客單價落在哪個區間,全部擺在同一張畫面上。這種「一張畫面塞多張圖」的東西就是儀表板(dashboard),而 matplotlib 用來做這件事的工具,叫做 subplots。
這篇假設你已經會畫基本的 plot / bar / scatter / hist,也知道怎麼設定中文字型(不記得的話回上一篇補一下)。而且這次資料不再手寫——我們會讀一份真實的交易明細 CSV,用它做出整個儀表板。
先搞懂:fig 和 ax 到底是什麼
這是整篇最重要的觀念,先用生活比喻講清楚,後面所有程式碼都是它的延伸。
想像你要辦一場攝影展。你手上有:
- 一個大相框,掛在牆上、下方還刻了展覽的大標題——這個相框就是
fig(figure),代表整張畫布。存檔存的是它、整張圖的大標題是它的事、多大張(尺寸)也是它決定的。 - 相框裡切成好幾格,每一格擺一張小照片——每一格就是一個
ax(axes)。要畫什麼線、什麼長條、這一格的小標題叫什麼,都是各自那一格ax的事。
一句話記住:fig 管整體(畫布、大標題、存檔),ax 管單格(畫內容、小標題)。 之前我們用 plt.plot() 畫圖時,其實 matplotlib 在背後偷偷幫你開了一個 fig 和一個 ax,只是你沒感覺到。現在要放四張圖,就得自己把這個結構叫出來、親手掌控。

上面這張就是我們接下來要填的「空相框」。四個格子現在都是空的,各自標了名字——這些名字怎麼來的,就是下一段的重點。
一行叫出畫布:plt.subplots(2, 2)
要一次拿到「一個 fig + 一批空格子」,用 plt.subplots():
import matplotlib.pyplot as plt
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
plt.show()這一行資訊量很大,拆開看:
plt.subplots(2, 2):建立一個 2 列 × 2 欄 的格子,總共 4 格。- 它一次回傳兩個東西(所以左邊用
fig, ax兩個變數去接):第一個是fig(整張畫布),第二個是ax——注意,這裡的ax不是單一格子,而是一個裝著 4 個格子的 2×2 陣列。 figsize=(12, 8):整張畫布的尺寸,單位是吋(寬 12 吋、高 8 吋)。這是fig的屬性,決定整塊畫布多大,不是單一格子的大小。
執行後就會看到剛才那張空相框的樣子(只是還沒填標籤)。
ax 用 [列, 欄] 索引,對照上面那張圖
既然 ax 是個 2×2 陣列,要指定「哪一格」就用 ax[列, 欄],列和欄都是從 0 開始數。對照上面那張結構圖:
ax[0, 0]→ 第 0 列第 0 欄 → 左上ax[0, 1]→ 第 0 列第 1 欄 → 右上ax[1, 0]→ 第 1 列第 0 欄 → 左下ax[1, 1]→ 第 1 列第 1 欄 → 右下
搞混的時候就回頭看那張空相框:第一個數字是「第幾列(上下)」,第二個數字是「第幾欄(左右)」。這套索引記熟了,剩下的就是往每一格塞圖。
先把交易明細讀進來、整理成四份資料
真實世界的資料不會手寫在程式裡,而是一份交易明細。這裡用一家飲料店 10 天、共 2040 筆的訂單紀錄 drink_sales.csv,每一列就是一筆訂單:
訂單編號,日期,星期,品項,溫度,數量,杯型,甜度,冰量,加料,單價,加料費,金額,付款方式
OD000001,2026-07-01,星期三,珍珠奶茶,28,1,中杯,微糖,去冰,布丁,60,15,75,行動支付
OD000002,2026-07-01,星期三,紅茶拿鐵,28,1,中杯,半糖,正常冰,無,70,0,70,行動支付
OD000003,2026-07-01,星期三,翡翠綠茶,28,1,大杯,微糖,正常冰,無,55,0,55,信用卡
...(點我下載 drink_sales.csv,跟著這篇一起跑。)
一筆一筆的明細沒辦法直接畫成「每日業績」或「品項排行」,要先聚合(aggregation)——用 groupby 把明細整理成儀表板四張圖各自要的形狀。這是資料分析最常做的一步:
import pandas as pd
orders = pd.read_csv("drink_sales.csv", parse_dates=["日期"]) # 2040 筆交易明細
daily = orders.groupby("日期")["金額"].sum() # 每日業績=當天金額加總
by_item = orders.groupby("品項")["數量"].sum().sort_values(ascending=False) # 品項銷量=各品項數量加總
day_qty = orders.groupby("日期").agg(溫度=("溫度", "first"), 數量=("數量", "sum")) # 每日溫度與總銷量
amounts = orders["金額"] # 每一筆消費金額(整欄)四行各自在做什麼:
daily:把同一天的所有訂單金額加總,得到每天的總營業額(一條有 10 個點的Series,日期當索引)。by_item:把同品項的數量加總、再由多到少排序,看哪個品項最好賣(這份資料裡冠軍是珍珠奶茶)。day_qty:一天一列,同時算當天溫度("first"取當天第一筆的溫度)和總杯數,等下用來看「熱不熱」和「賣多少」的關係。amounts:客單價分布不需要聚合,直接把整欄金額拿去畫直方圖就好。
接下來畫圖用的就是這四個變數,一律不再手寫數字——資料全部從那份 CSV 來。
關鍵轉換:畫圖要用「那一格的方法」
這裡是從單圖跨到多圖時最容易卡住的地方。之前畫單張圖,我們寫的是 plt.bar(...)、plt.title(...)——plt 畫的是「目前作用中的那張圖」。但現在有四格,plt.bar() 到底畫進哪一格?matplotlib 只能猜,一多子圖就容易畫錯格。
正確做法是:明確告訴它畫進哪一格——不呼叫 plt,改呼叫「那個 ax 自己的方法」。函式名幾乎一樣,只是前面從 plt. 換成 ax[i,j].:
| 單張圖的寫法 | 畫進指定子圖的寫法 |
|---|---|
plt.bar(...) | ax[0,1].bar(...) |
plt.scatter(...) | ax[1,0].scatter(...) |
plt.hist(...) | ax[1,1].hist(...) |
plt.title("...") | ax[0,1].set_title("...") |
特別注意標題那一列:子圖的小標題要用 ax.set_title(...),不是 plt.title(...)。plt.title 畫的是「目前作用中」的圖,多子圖時你根本分不清「目前」是哪一格,標題就會跑到奇怪的地方。認明 ax.set_title 就對了。
先把右上角的「品項銷量」長條圖填進去試試(by_item 就是上一段 groupby 出來的):
import matplotlib.pyplot as plt
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
ax[0,1].bar(by_item.index, by_item.values) # 畫進「右上」那一格
ax[0,1].set_title("品項銷量") # 這一格的小標題
plt.show()執行後,右上角出現一張長條圖,其他三格還是空白——因為我們只對 ax[0,1] 動手。想填哪格就換索引,邏輯完全一樣。
![只對 ax[0,1] 呼叫 bar,就只有右上格出現長條圖,其餘三格維持空白](/blog/subplots/partial-onecell.png)
pandas 也能指定畫進哪一格:.plot(ax=...)
左上角要放的是業績趨勢折線圖,資料就是剛剛 groupby 出來的 daily(日期當索引的 Series)。pandas 的 Series/DataFrame 本身就會畫圖,要把它畫進指定的格子,只要傳一個 ax= 參數:
daily.plot(ax=ax[0,0], marker="o") # ax= 指定畫進「左上」那一格
ax[0,0].set_title("業績趨勢")daily.plot(ax=ax[0,0]) 的意思是「pandas 你來畫,但畫在我指定的 ax[0,0] 這一格裡」。marker="o" 讓每個資料點都標一個小圓點,趨勢的轉折看得更清楚。這個 ax= 的概念很通用——用過 seaborn 的人會發現它的 axes-level 函式也是靠 ax= 指定子圖,是同一套思路。
剩下兩格:散佈圖與直方圖
左下放溫度 vs 銷量的散佈圖,看氣溫和賣量有沒有關聯;右下放客單價分布的直方圖,看金額大多落在哪一段。資料一樣是前面 groupby 出來的 day_qty 和 amounts,畫法一樣是「那一格的方法」:
ax[1,0].scatter(day_qty["溫度"], day_qty["數量"]); ax[1,0].set_title("溫度 vs 銷量")
ax[1,1].hist(amounts, bins=15); ax[1,1].set_title("金額分布")兩點說明:
- 散佈圖用每天的溫度對總杯數——這份資料裡溫度越高、賣得越多,點會往右上排成一條斜斜的雲,一眼就看出正相關。
- 直方圖直接把整欄
amounts(2040 筆金額)用bins=15切成 15 個區間去數次數,畫出來會是中間高、兩側低的形狀,看得出客單價大多集中在哪一段。
到這裡,四格都填滿了。
大標題與小標題:fig.suptitle vs ax.set_title
四張圖有各自的小標題了,但整張儀表板還缺一個「總標題」。這正好對應 fig / ax 的分工:
ax.set_title("..."):各子圖上方的小標題(是「那一格」的事)。fig.suptitle("..."):整張畫布最上方的大標題(是「整個 fig」的事,suptitle 的 p 是 super,「凌駕全部之上」的標題)。
fig.suptitle("飲料店銷售分析儀表板", fontsize=15, fontweight="bold")fontsize 調字級、fontweight="bold" 讓大標題粗體,看起來更像正式的儀表板抬頭。
收尾兩件事:tight_layout 與存檔
多子圖幾乎一定會遇到一個問題:格子挨得太近,某一格的標題會壓到隔壁格的座標軸、或大標題和子標題疊在一起。像下面這張「還沒排版」的樣子——擠成一團:

手動一格一格調間距很累,交給 matplotlib 自動排就好:
plt.tight_layout()plt.tight_layout() 會自動計算、把各子圖的間距撐開,讓標題、軸標都不打架。多子圖的版面,加這一行幾乎是標配。
排好之後,把成果存成一張高解析度圖檔:
plt.savefig("dashboard.png", dpi=150, bbox_inches="tight")dpi=150:每吋 150 點的解析度,比預設清楚,貼報告或簡報都夠用。bbox_inches="tight":存檔時把圖四周多餘的白邊裁掉,成品更緊湊。
(存檔是 plt.savefig,你也可以寫成 fig.savefig——因為存的本來就是整張 fig,兩種寫法等價。)
中文字型:沿用上一篇那兩行就好
如果你的中文標題又變成方框,別緊張,這是上一篇講過的老問題。開頭補這兩行設定即可,這裡不再展開,細節看〈matplotlib 入門那篇〉:
plt.rcParams['font.family'] = ['Microsoft JhengHei', 'Arial Unicode MS', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False(font.family 和上一篇用的 font.sans-serif 都能指定中文字型,效果一樣;列多個字型是備援,matplotlib 會挑第一個系統裡找得到的。非 Windows 就把 Microsoft JhengHei 換成你系統有的中文字型。)
完整程式碼
把「讀 CSV → groupby 整理 → 一格一格填」整串接起來,就是一支能直接跑的儀表板程式(記得先下載 drink_sales.csv 放在同一個資料夾):
import matplotlib.pyplot as plt
import pandas as pd
# 中文顯示設定(詳見上一篇)
plt.rcParams['font.family'] = ['Microsoft JhengHei', 'Arial Unicode MS', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False
# 1) 讀交易明細,groupby 整理成四份資料
orders = pd.read_csv("drink_sales.csv", parse_dates=["日期"])
daily = orders.groupby("日期")["金額"].sum()
by_item = orders.groupby("品項")["數量"].sum().sort_values(ascending=False)
day_qty = orders.groupby("日期").agg(溫度=("溫度", "first"), 數量=("數量", "sum"))
amounts = orders["金額"]
# 2) 建立 2×2 畫布,拿到 fig 和裝著 4 格的 ax 陣列
fig, ax = plt.subplots(2, 2, figsize=(12, 8))
# 3) 各格分別畫圖 + 設小標題
daily.plot(ax=ax[0,0], marker="o"); ax[0,0].set_title("業績趨勢")
ax[0,1].bar(by_item.index, by_item.values); ax[0,1].set_title("品項銷量")
ax[1,0].scatter(day_qty["溫度"], day_qty["數量"]); ax[1,0].set_title("溫度 vs 銷量")
ax[1,1].hist(amounts, bins=15); ax[1,1].set_title("金額分布")
# 4) 大標題、排版、存檔
fig.suptitle("飲料店銷售分析儀表板", fontsize=15, fontweight="bold")
plt.tight_layout()
plt.savefig("dashboard.png", dpi=150, bbox_inches="tight")
plt.show()跑出來就是這張——同一份交易明細,groupby 出四個角度,排成 2×2、各有小標題、上面一個大抬頭,這就是最基本、也最實用的儀表板:

小結
從一份交易明細到一整面儀表板,關鍵就這幾步:
pd.read_csv讀明細,groupby(...).sum()聚合成每張圖要的資料——畫圖前的資料整理,往往才是重點。fig, ax = plt.subplots(2, 2, figsize=(12, 8)):一次拿到整張畫布fig和裝著 4 格的ax陣列。ax[列, 欄]索引:[0,0]左上、[0,1]右上、[1,0]左下、[1,1]右下。- 畫進某格用「那格的方法」:
ax[i,j].bar / .scatter / .hist、標題用ax[i,j].set_title;pandas 則靠.plot(ax=...)指定格子。 fig.suptitle是整張的大標題,ax.set_title是各格的小標題;最後用plt.tight_layout()排間距、plt.savefig()存高解析成品。
抓住「fig 管整張、ax 管單格」這條主線,再加上「先 groupby 把資料整理好」,多子圖儀表板就不再是難關。想再往下走——把儀表板變成能互動、能篩選的網頁圖表,或接上真實資料庫的數據——歡迎到「Python 資料視覺化」課程繼續練。