2022年9月30日 星期五

Python 學習筆記 : Matplotlib 資料視覺化 (六) 物件導向篇 (中)

因為篇幅太長, 所以把物件導向拆成三篇, 本系列之前的測試文章參考 : 


四. 使用 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 物件排版 (2) [看原始碼]


除了直接使用 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 物件排版的一樣.

完整程式碼參考 :

測試  : 使用 GridSpec 物件排版 (3) [看原始碼]


當然也可以用 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() 物件排版 [看原始碼]

沒有留言 :