因為篇幅太長, 所以把物件導向拆成三篇, 本系列之前的測試文章參考 :
更多 Python 筆記參考 :
本篇測試參考書籍如下 :
# Hands-on Matplotlib (Ashwin Pajankar, 2022, Appress)
# Python 資料科學學習手冊 (何敏煌譯, 2017, 碁峰)
# 王者歸來-Python 在大數據科學計算上的最佳實作 (佳魁, 張若愚)
# Python 程式設計學習經典 (碁峰 2018)
# 新手村逃脫! 初心者的 Python 機器學習攻略 (博碩 2020, 郭耀仁)
# 必學! Python 資料科學, 機器學習最強套件 (旗標 2021)
四. 使用 GridSpec 物件為子圖排版 :
在前一篇物件導向 (上) 的測試中, 不論是呼叫 fig.add_subplot() 或 plt.subplots(), 子圖都是以網格矩陣排版, 沒有放置子圖 AxesSubplot 物件的那一格會出現一個空缺, 沒辦法合併儲存格. 在進階篇中有介紹合併儲存格的做法, 但那只能整列或整欄合併. 如果要對網格作較彈性的儲存格合併來排版, 必須以物件導向模式使用 GridSpec 類別的物件來設定版面, 參考 :
GridSpec 類別放在 Matplotlib 的 gridspec 模組中, 使用前須先匯入, 例如 :
>>> import matplotlib.pyplot as plt
>>> import matplotlib.gridspec as gridspec
>>> type(gridspec)
<class 'module'>
首先呼叫 GridSpec 類別的建構式 GridSpec() 來建立 GridSpec 物件, 常用語法如下 :
grid=gridspec.GridSpec(nrow, ncol [figure, wspace, hspace])
此建構式會傳回一個 GridSpec 物件. 參數說明如下 :
- nrows 與 ncols 表示子圖網格的列數與欄數 (必要參數)
- figure 為 Figure 物件, 用於 contrained_layout 為 True 時.
- wspace 與 hspace 為相對於子圖尺寸 (寬高) 之百分比做為子圖間距 (值 0~1)
以建立如下版面為例, 基本架構為 2*3 網格, 然後將右上角與左下角兩格合併形成 4 個繪圖區 :
例如建立間距是 0.4 倍子圖寬高尺寸的 2*3 網格 :
>>> grid=gridspec.GridSpec(2, 3, wspace=0.4, hspace=0.4)
>>> type(grid)
<class 'matplotlib.gridspec.GridSpec'>
注意, 不指定參數關鍵字的話, 第一參數為 nrows, 第二參數為 ncols, 為了增進程式可讀性, 最好還是指定關鍵字.
接著建立畫布物件, 然後依據繪圖區在網格中的位置指定 GridSpec 物件的索引, 將其傳給 Figure 物件的 add_subplot() 方法來建立 4 個繪圖區 (子圖) 物件 :
>>> fig=plt.figure()
>>> ax1=fig.add_subplot(grid[0, 0])
>>> ax2=fig.add_subplot(grid[0, 1:])
>>> ax3=fig.add_subplot(grid[1, :2])
>>> ax4=fig.add_subplot(grid[1, 2])
>>> plt.show()
此處 ax1 子圖位置是 2*3 網格中的第 0 列第 0 行, 故傳入 grid[0, 0]; ax2 子圖位置是 2*3 網格中的第 0 列第 1 行與第 2 行的合併儲存格, 故傳入 grid[0, 1:], 其中欄索引 1: 表示索引 1 以後全部; ax3 子圖位置是 2*3 網格中的第 1 列第 0 行與第 1 行的合併儲存格, 故傳入 grid[1, :2], 其中欄索引 :2 表示索引 2 以前全部 (不含 2, 即欄索引 0 與 1); ax1 子圖位置是 2*3 網格中的第 1 列第 2 行, 故傳入 grid[1, 2], 結果如下 :
注意, 若傳入 GridSpec() 的間距 wspace 與 hspace 太小 (例如 0.2), 可能會使坐標軸刻度標籤會與子圖標題部分重疊, 需要視畫布大小微調.
完整程式碼參考 :
測試 : 使用 GridSpec 物件排版 (1) [看原始碼]
在前一篇測試中, 子圖間距的懶人設定法有兩個, 一是呼叫 fig.tight_layout() 函式讓版面緊密, 但這方法在使用 GridSpec 物件排版時無效, 而且會出現如下警告 :
>>> fig.tight_layout()
matplotlib_fig_adjust_subplot.py:1: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
第二個方法是呼叫 fig.subplots_adjust(), 這雖然不會出現警告, 但也同樣無效.
如果不想微調 wspace 與 hspace 的麻煩, 可以在建立 Figure 物件時傳入 constrained_layout=True 參數讓版面受限制並自動調整間距, 然後於建立 GridSpec 物件時將 Figure 物件傳給 figure 參數, 例如 :
>>> fig=plt.figure(constrained_layout=True) # 讓版面受限
>>> grid=gridspec.GridSpec(nrows=2, ncols=3, figure=fig) # 指定 Figure 物件
>>> ax1=fig.add_subplot(grid[0, 0])
>>> ax2=fig.add_subplot(grid[0, 1:])
>>> ax3=fig.add_subplot(grid[1, :2])
>>> ax4=fig.add_subplot(grid[1, 2])
>>> plt.show()
結果如下 :
除了直接使用 GridSpec 類別來排版外, 也可以利用 Figure 物件的 add_gridspec() 方法來建立 GridSpec 物件, 這樣就不需要匯入 gridspec.GridSpec 類別了. 語法如下 :
grid=fig.add_gridspec(nrow, ncol [figure, wspace, hspace])
參數用法與上面的 GridSpec() 建構式一樣, 例如 :
>>> import matplotlib.pyplot as plt
>>> fig=plt.figure()
>>> grid=fig.add_gridspec(nrows=2, ncols=3, wspace=0.4, hspace=0.4)
>>> type(grid)
<class 'matplotlib.gridspec.GridSpec'>
>>> ax1=fig.add_subplot(grid[0, 0])
>>> ax2=fig.add_subplot(grid[0, 1:])
>>> ax3=fig.add_subplot(grid[1, :2])
>>> ax4=fig.add_subplot(grid[1, 2])
>>> plt.show()
可見 fig.add_gridspec() 同樣會傳回 GridSpec 物件, 結果與上面用 GridSpect 物件排版的一樣.
當然也可以用 constrained_layout 參數讓 Figure 畫布自動調節版面與子圖間距 :
>>> import matplotlib.pyplot as plt
>>> fig=plt.figure(constrained_layout=True) # 讓版面受限
>>> grid=fig.add_gridspec(nrows=2, ncols=3) # 建立 2*3 網格之 GridSpec 物件
>>> ax1=fig.add_subplot(grid[0, 0])
>>> ax2=fig.add_subplot(grid[0, 1:])
>>> ax3=fig.add_subplot(grid[1, :2])
>>> ax4=fig.add_subplot(grid[1, 2])
>>> plt.show()
結果與上面用 GridSpect 物件排版的一樣. 完整程式碼參考 :
測試 : 使用 GridSpec 物件排版 (4) [看原始碼]
更複雜的版面配置範例如下 :
此例將 3*3 網格作局部合併成 5 個子圖, 配置時主要的工作是利用切片技巧決定各子圖的索引, 其中冒號與負號的運用是重點, 例如 :
>>> import matplotlib.pyplot as plt
>>> fig=plt.figure(constrained_layout=True) # 讓版面受限
>>> grid=fig.add_gridspec(nrows=3, ncols=3) # 建立 3*3 網格之 GridSpec 物件
>>> fig=plt.figure(constrained_layout=True)
>>> grid=fig.add_gridspec(nrows=3, ncols=3)
>>> ax1=fig.add_subplot(grid[0, :])
>>> ax2=fig.add_subplot(grid[1, :-1])
>>> ax3=fig.add_subplot(grid[2, 0])
>>> ax4=fig.add_subplot(grid[2, 1])
>>> ax5=fig.add_subplot(grid[1:, 2])
>>> plt.show()
此例使用 constrained_layout 來限制版面配置, 由畫布自動調節版面與子圖間距, 結果如下 :
完整程式碼參考 :
測試 : 使用 GridSpec 物件排版 (5) [看原始碼]
利用 GridSpec 物件可以彈性地對子圖網格進行排版, 下面是改編自 Matplotlib 官網與 "Python 資料科學學習手冊" 書中第四章的多軸值方圖範例, 參考 :
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> np.random.seed(42) # 設定隨機種子
>>> x=np.random.randn(1000)
>>> y=np.random.randn(1000)
>>> grid=fig.add_gridspec(nrows=2, ncols=2, wspace=0.05, hspace=0.05) # 2*2 網格
>>> ax=fig.add_subplot(grid[1, 0]) # 繪製散佈圖用
>>> ax_histx=fig.add_subplot(grid[0, 0]) # 繪製垂直直方圖用
>>> ax_histy=fig.add_subplot(grid[1, 1]) # 繪製水平直方圖用
>>> ax_histx.set_xticklabels([]) # 取消垂直直方圖 X 軸刻度標籤
>>> ax_histy.set_yticklabels([]) # 取消水平直方圖 Y 軸刻度標籤
>>> ax.scatter(x, y) # 繪製散佈圖
<matplotlib.collections.PathCollection object at 0x0000020AB16D0F28>
>>> ax_histx.hist(x, bins=10) # 繪製垂直直方圖
(array([ 7., 21., 101., 194., 277., 220., 129., 42., 5., 4.]), array([-3.28006281, -2.57456585, -1.86906889, -1.16357192, -0.45807496,
0.247422 , 0.95291897, 1.65841593, 2.36391289, 3.06940985,
3.77490682]), <a list of 10 Patch objects>)
>>> ax_histy.hist(y, bins=10, orientation='horizontal') # 繪製水平直方圖
(array([ 8., 24., 69., 190., 258., 222., 164., 49., 13., 3.]), array([-3.21292639, -2.54757846, -1.88223054, -1.21688261, -0.55153469,
0.11381324, 0.77916116, 1.44450909, 2.10985701, 2.77520494,
3.44055286]), <a list of 10 Patch objects>)
>>> plt.show()
此例使用隨機數來繪製散佈圖與 X, Y 軸直方圖, 直方圖分成 10 個 bin (值區段), 散佈圖放在 [1, 1] 網格; X 軸直方圖 (垂直) 放在 [0, 0] 網格; Y 軸直方圖 (水平) 放在 [1, 1] 網格, 並用 set_xticklabels([]) 與 set_yticklabels([]) 拿掉直方圖的刻度標籤, 結果如下 :
從直方圖的分布可知這近似常態分布, 如果將 bin 設大一些例如 50 就很接近常態分佈了.
完整程式碼參考 :
測試 : 使用 GridSpec 物件排版 (6) [看原始碼]
五. 使用 GridSpec 物件為子圖排版 :
在 "王者歸來-Python 在大數據科學計算上的最佳實作" 這本書的第四章介紹了使用 pyplot.subplot2grid() 函式配置版面的方法, 觀念與網頁 HTML 表格或 Excel 合併儲存格類似, 比上面使用 GridSpec 類別與 Figure 物件的 add_gridspec() 方法排版更直觀. 常用語法如下 :
ax=plt.subplot2grid(shape, loc [, rowspan=1, colspan=1])
參數說明如下 :
- shape : 是 (列數, 行數) 組成之網格形狀 (元組)
- loc : 子圖左上角在網格中的座標位置, 以 (列, 行) 元組表示
- rowspan : 子圖所佔行數, 即橫向合併格數
- colspan : 子圖所佔列數, 即直向合併格數
此函式還有其它關鍵字參數, 用法與 Figure 物件的 add_subplot() 方法相同. 此函式會傳回一個 AxesSubplot 物件, 例如 :
>>> import matplotlib.pyplot as plt
>>> fig=plt.figure(figsize=(8, 6))
>>> ax1=plt.subplot2grid((3, 3), (0, 0), colspan=2)
>>> type(ax1)
<class 'matplotlib.axes._subplots.AxesSubplot'>
>>> ax2=plt.subplot2grid((3, 3), (0, 2), rowspan=2) # 直向合併 2 格
>>> ax3=plt.subplot2grid((3, 3), (1, 0), rowspan=2) # 直向合併 2 格
>>> ax4=plt.subplot2grid((3, 3), (2, 1), colspan=2) # 橫向合併 2 格
>>> ax5=plt.subplot2grid((3, 3), (1, 1))
>>> ax1.text(0.5, 0.5, 'ax1', fontsize=16, ha='center')
Text(0.5, 0.5, 'ax1')
>>> ax2.text(0.5, 0.5, 'ax2', fontsize=16, ha='center')
Text(0.5, 0.5, 'ax2')
>>> ax3.text(0.5, 0.5, 'ax3', fontsize=16, ha='center')
Text(0.5, 0.5, 'ax3')
>>> ax4.text(0.5, 0.5, 'ax4', fontsize=16, ha='center')
Text(0.5, 0.5, 'ax4')
>>> ax5.text(0.5, 0.5, 'ax5', fontsize=16, ha='center')
Text(0.5, 0.5, 'ax5')
>>> fig.tight_layout()
>>> plt.show()
此處使用 Axes 物件的 text() 方法在子圖中央打上名稱, 結果如下 :
完整程式碼參考 :
測試 : 使用 plt.subplot2grid() 物件排版 [看原始碼]
沒有留言:
張貼留言