從一張圖到一整面牆
上一篇〈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 吋)。這是 的屬性,決定整塊畫布多大,不是單一格子的大小。
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["金額"] # 每一筆消費金額(整欄)關鍵轉換:畫圖要用「那一格的方法」
這裡是從單圖跨到多圖時最容易卡住的地方。之前畫單張圖,我們寫的是 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("...") |
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("金額分布")兩點說明:
大標題與小標題: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——因為存的本來就是整張 ,兩種寫法等價。)
中文字型:沿用上一篇那兩行就好
如果你的中文標題又變成方框,別緊張,這是上一篇講過的老問題。開頭補這兩行設定即可,這裡不再展開,細節看〈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()小結
從一份交易明細到一整面儀表板,關鍵就這幾步:
- `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 資料視覺化」課程繼續練。
