2022年2月23日 星期三

Python 學習筆記 : Matplotlib 資料視覺化 (三) 進階篇

距離上一次學習 Matplotlib (2020-05) 已時隔近兩年, 我的資料科學學習進度進行得非常緩慢. 最近為了將母校圖書館的借書趕快讀完還回去 (才能借新的書), 所以回頭把 Matplotlib 做個收尾測試, 結果整理如下. 本系列之前的文章參考 :

Python 學習筆記 : Matplotlib 資料視覺化 (一) 基本篇
Python 學習筆記 : Matplotlib 資料視覺化 (二) 統計圖

本篇主要測試 Matplotlib 的進階功能, 包括 : 
  1. 顯示中文
  2. 顯示格線
  3. 填滿色彩
  4. 儲存圖檔
  5. 風格設定
  6. 文字註解
  7. 繪製子圖
  8. 設定畫布
  9. 對數座標
主要參考書籍 :

Hands-on Matplotlib (Ashwin Pajankar, 2022, Appress)
# 一步到位! Python 程式設計 (旗標, 陳惠貞)
Python資料科學學習手冊 (Oreilly, 2017) 


一. 顯示中文 : 

Matplotlib 使用 True Type 字型 (ttf) 顯示文字, 因為安裝 Matplotlib 套件時設定檔預設的字型是英文, 所以無法顯示中文字, 若 X 軸 或 Y 軸標籤 (label), 或圖形標題 (title) 等區塊直接使用中文, 執行時將出現警告訊息 (missing font), 且中文會以方塊顯示, 例如 :


測試 1-1 : 繪製一周平均溫度變化圖時直接使用中文 [看原始碼]

import matplotlib.pyplot as plt

week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.show()

此程式在呼叫 title(), xlabel(), 以及 ylabel() 時直接使用中文, 執行時雖然會繪製圖形, 但交談區會出現如下警告訊息, 表示無法找到中文字型 :

C:\Python37\lib\site-packages\matplotlib\backends\backend_agg.py:214: RuntimeWarning: Glyph 28331 missing from current font.
  font.set_text(s, 0.0, flags=flags) 

繪製出來的圖形中, 預期應該顯示中文的區塊都以方塊顯示 : 




解決中文顯示的問題大部分的書上都提到要去修改預設的設定檔 matplotlibrc, 此檔位置可以用 matplotlib.matplotlib_fname() 函式查詢 : 

>>> import matplotlib   
>>> matplotlib.matplotlib_fname()    
'C:\\Python37\\lib\\site-packages\\matplotlib\\mpl-data\\matplotlibrc'

但做法有點麻煩, 參考 :


對於 Windows 使用者而言, 因為都有內建微軟正黑體 (JhengHei) 與雅黑體 (Yahei) 中文字型, 因此最簡單的方法是在程式中直接用 plt.rcParams 屬性動態指定系統內建的中文字型即可, 例如 : 
  • plt.rcParams["font.family"]=["Microsoft JhengHei"] : 微軟正黑體
  • plt.rcParams["font.family"]=["Microsoft Yahei"] : 微軟雅黑體
除了中文顯示問題外, 有時負數的負號可能也有同樣問題, 這時可以用如下設定解決 : 
  • plt.rcParams["axes.unicode_minus"]=False
例如 : 


測試 1-2 : 繪製一周平均溫度變化圖時以 rcParams 設定中文字型 [看原始碼]

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.show()

此例添加 plt.rcParams() 設定後即可顯示中文了, 結果如下 : 




可見不論是標題還是 X/Y 軸標籤上的中文都能正常顯示了. 

參考 :



二. 顯示格線 : 

格線 (grid) 是繪圖區內等間隔的垂直與水平線, 有助於判讀線形投影到軸座標上的刻度值, Matplotlib 預設不顯示格線, 但可呼叫 plt.grid() 來顯示格線, 語法如下 :

plt.grid(visible=False [, which='major', axis='both', color, linestyle, linewidth, alpha] 

常用參數說明如下 :
  • visible : 設定是否顯示格線 (預設 False 不顯示)
  • which : 設定顯示哪種格線 (預設 'major')
  • axis : 指定顯示哪軸格線 (預設 'both', 可用 'x' 指定 X 軸或 'y' 指定 Y 軸)
  • color : 設定格線顏色
  • linestyle : 設定格線樣式
  • linewidth : 設定格線寬度 (pixel)
  • alpha : 設定格線透明度 (預設 1 不透明)
參考 : 


下面是顯示 X 軸與 Y 軸格線的範例 : 


測試 2-1 : 顯示 X 軸與 Y 軸格線 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.grid(True)       # 顯示顯示 X 軸與 Y 軸格線
plt.show()

此例只是在上面範例中加入 plt.grid(True), 結果如下 :




下面是只顯示 X 軸格線的範例 : 


測試 2-2 : 只顯示 X 軸格線 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.grid(axis='x')     # 只顯示 X 軸格線
plt.show()

結果如下 : 




下面是只顯示 Y 軸格線的範例 : 


測試 2-3 : 只顯示 Y 軸格線 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.grid(axis='y')        # 只顯示 Y 軸格線
plt.show()   

結果如下 :




下面範例是測試 plt.grid() 的格線樣式 :


測試 2-4 : 設定格線樣式 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.grid(True,
         color='blue',
         linestyle=':',
         linewidth=1,
         alpha=0.5)        # 設定格線樣式為藍色虛線
plt.show()

結果如下 :




有了格線對於判讀取線上的值就容易多了. 


三. 填滿色彩 : 

在繪製數據資料圖形時為了強調常需要將某個區域用鮮明的顏色填滿, Matplotlib 有一個 fill_between() 函數可以用來填滿指定的 Y 軸區間, 其語法如下 :

plt.fill_between(x, y1 [, y2=0, color='#1F77B4', alpha=1])

其中 x 為 X 座標序列數據, y1 與 y2 是要填滿顏色的 Y 軸區間, y2 是預設值為 0 的備選參數, 亦即預設是填滿 y1 與 0 之間的區域. 備選參數 color 為要填滿的顏色 (預設是 #1F77B4), 可以用 "#" 開頭的 16 進位顏色碼字串, 或十進位的 (r, g, b) 元組, alpha 則為填滿顏色的透明度 (0~1, 1 是完全不透明, 0 是全透明), 參考 :


其實也可以使用 CSS 顏色名稱字串, 參考 :


例如 : 


測試 3-1 : 填滿函數的 Y 軸區域 (預設到 y=0) [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0, 10, 100)
y1=x**2
plt.plot(x, y1)
plt.fill_between(x, y1)     # 以預設顏色填滿 y1 與 y=0 之間的區域
plt.show()

此例繪製了一個 y1=x**2 的平方函數, 然後呼叫 fill_between() 填滿顏色, 由於未指定 y2 參數, 所以是填滿 y1 與 y=0 之間的區域, 結果如下 : 




下面範例是測試有指定 y2 的情形, 此例是填滿 y1=x**2 與 y2=x 這兩條線之間的區域 :


測試 3-2 : 填滿函數的 Y 軸區域 (y=x**2 與 y=x 之間, 不透明) [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0, 10, 100)
y1=x**2
y2=x
plt.plot(x, y1, 'red', x, y2, 'blue')
plt.fill_between(x, y1, y2, color="yellow")    # 以黃色填滿兩條線間之區域
plt.show()



其中上面那條紅線是 y1=x**2, 而下方的藍線則是 y2=x, 預設是不透明 alpha=1 的填滿顏色. 

下面則是指定透明度參數 alpha 的例子


測試 3-3 : 填滿函數的 Y 軸區域 (y=x**2 與 y=x 之間, 半透明) [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0, 10, 100)
y1=x**2
y2=x
plt.plot(x, y1, 'red', x, y2, 'blue')
plt.fill_between(x, y1, y2, color='yellow', alpha=0.5)    # 指定透明度=0.5
plt.show()

此例添加 alpha 參數指定填滿區域顏色為半透明, 結果如下 : 




可見半透明的填滿顏色淡了一半. 

下面是在 sin() 與 cos() 之間填滿顏色的範例 :


測試 3-4 : 填滿正弦與餘弦函數的 Y 軸區域 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0,10,100)            #X 軸
y1=np.sin(x)                              #Y軸1=sin(x)
y2=np.cos(x)                             #Y軸2=cos(x)
plt.plot(x, y1, '-r', label='sin(x)')    #指定資料之圖例標籤
plt.plot(x, y2, ':b', label='cos(x)')    #指定資料之圖例標籤
plt.legend()                               #顯示圖例
plt.title('functions')                   #設定圖形標題
plt.xlabel('X')                           #設定 X 軸標籤
plt.ylabel('Y')                           #設定 Y 軸標籤
plt.fill_between(x, y1, y2, color='yellow')      # 填滿 sin() 與 cos() 間的 Y 軸區域
plt.show()

結果如下 : 




以上是用 fill_between() 填滿兩條線間的 Y 軸區域 (垂直方向), Matplotlib 還有一個函數 fill_betweenx() 是填滿兩條線之間的 X 軸區域, 其語法如下 :

plt.fill_betweenx(y, x1 [, x2=0, color='#1F77B4', alpha=1])

其中 x2 預設為 0, 亦即預設是填滿 y 函數與 Y 軸 (x=0) 之間的 X 軸區域, 參考 :


下面範例以填滿函數 y=x**2 與 Y 軸 (x=0) 的區域為例 : 


測試 3-5 : 填滿函數的 X 軸區域 (預設到 x=0) [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

y=np.linspace(0, 100, 100)
x1=np.sqrt(y)                     # 從 y 取方根得到 x1
plt.plot(x1, y, color='red')
plt.fill_betweenx(y, x1)     # 以預設顏色填滿 x1 與 x=0 之間的區域
plt.show()

已知 y=x**2 求 x, 要用平方的反函數開方根來得到 x1, 未指定 x2 就是預設 x2=0, 結果如下 : 




可見填滿區域為 y=x**2 以左與 x=0 以右中間的區域. 

下面是指定 x2 為 y=x 這條線的範例 :


測試 3-6 : 填滿函數的 X 軸區域 (y=x**2 與 y=x 之間) [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

y=np.linspace(0, 100, 100)
x1=np.sqrt(y)
x2=y
plt.plot(x1, y, color='red')
plt.plot(x2, y, color='blue')
plt.fill_betweenx(y, x1, x2, color='yellow')
plt.show()

此處 y=x 這條線反求 X 軸之值為 x2=y, 結果如下 :




參考:



四. 儲存圖檔 : 

用 Matplotlib 繪製的圖形可呼叫 savefig() 函數儲存成外部圖檔, 其語法為 : 

plt.savefig(filename [, bbox_inches=None])   

其中 filename 為圖檔名稱, 支援如下主要檔案格式 : 
  • png (預設)
  • jpg 
  • svg
  • pdf
如果 filename 不含副檔名則預設為 png 檔. 存成 svg 檔可用瀏覽器開啟, 而 pdf 則需 pdf reader 軟體才能開啟. 備選參數 bbox_inches 為以英寸表示的圖表外框, 若設為 "tight", 則存檔時會去除外框, 只儲存圖形本身 (其實感覺不出差異). 參考 : 


全部的支援檔案格式可用 Figure 物件 (畫布) 的 canvas.get_supported_filetypes() 函式檢視 :

>>> import matplotlib.pyplot as plt    
>>> fig=plt.figure()                                        # 建立 Figure 物件
>>> fig.canvas.get_supported_filetypes()   
{'ps': 'Postscript', 'eps': 'Encapsulated Postscript', 'pdf': 'Portable Document Format', 'pgf': 'PGF code for LaTeX', 'png': 'Portable Network Graphics', 'raw': 'Raw RGBA bitmap', 'rgba': 'Raw RGBA bitmap', 'svg': 'Scalable Vector Graphics', 'svgz': 'Scalable Vector Graphics', 'jpg': 'Joint Photographic Experts Group', 'jpeg': 'Joint Photographic Experts Group', 'tif': 'Tagged Image File Format', 'tiff': 'Tagged Image File Format'}

例如 :


測試 4-1 : 將檔案儲存為 jpg 檔 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.savefig('溫度變化圖.jpg')   
plt.show()

此例是在上面的測試 1-2 程式後面加了一個 savefig('溫度變化圖'), 執行後在此程式所在資料夾裡果然新增了一個 '溫度變化圖.jpg' 的圖檔, 開啟後結果如下 : 




可見圖形與呼叫 plt.show() 所繪製的圖相同. 注意, 如果檔名沒有指定副檔名, 例如 plt.savefig('溫度變化圖' ), 則預設會儲存為 '溫度變化圖.png' 檔.  

如果存成 pdf 檔, 要注意若使用中文會出現 "TrueType font is missing table" 的執行時期錯誤 (RuntimeError), 例如 :


測試 4-2 : 將檔案儲存為 pdf 檔 (中文字型) [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')
plt.savefig('溫度變化圖.pdf')     
plt.show()

結果出現如下錯誤 :

RuntimeError: TrueType font is missing table

這是因為後端的 pdf 沒有中文字型所致, 如果改成英文就可以順利存成 pdf 檔了, 例如 : 


測試 4-3 : 將檔案儲存為 pdf 檔 (英文字型) [看原始碼] 

import matplotlib.pyplot as plt

week=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('Temperature Change')                      
plt.xlabel('Weekday')                                                 
plt.ylabel('Celsius')
plt.savefig('溫度變化圖.pdf')   
plt.show()


五. 風格設定 : 

Matplotlib 內建了許多繪圖風格 (style), 可以用 plt.style.available 屬性來查詢 :

>>> import matplotlib.pyplot as plt     
>>> plt.style.available                     # 查詢內建風格
['Solarize_Light2', '_classic_test_patch', 'bmh', 'classic', 'dark_background', 'fast', 'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn', 'seaborn-bright', 'seaborn-colorblind', 'seaborn-dark', 'seaborn-dark-palette', 'seaborn-darkgrid', 'seaborn-deep', 'seaborn-muted', 'seaborn-notebook', 'seaborn-paper', 'seaborn-pastel', 'seaborn-poster', 'seaborn-talk', 'seaborn-ticks', 'seaborn-white', 'seaborn-whitegrid', 'tableau-colorblind10'] 

可見有許多是 Seaborn 類別的風格, 在之前的測試中所使用的其實是名為 default 的預設風格 (不在上面串列中), 可以呼叫 plt.style.use() 函式並傳入風格名稱來改變繪圖風格, 例如 : 


測試 5-1 : 將繪圖風格更改為 ggplot [看原始碼] 

import matplotlib.pyplot as plt

plt.style.use('ggplot')        # 使用 R 語言 ggplot2 繪圖風格
week=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('Temperature Change')                      
plt.xlabel('Weekday')                                                 
plt.ylabel('Celsius')  
plt.show()

此例使用 R 語言鼎鼎有名的 ggplot2 風格, 結果如下 : 




可見 ggplot 風格預設就已有格線了. Seaborn 風格與 ggplot 類似, 例如 : 


測試 5-2 : 將繪圖風格更改為 seaborn [看原始碼] 

import matplotlib.pyplot as plt

plt.style.use('seaborn')      
week=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('Temperature Change')                      
plt.xlabel('Weekday')                                                 
plt.ylabel('Celsius')  
plt.show()

結果如下 :




下面範例 classic 風格 : 


測試 5-3 : 將繪圖風格更改為 classic [看原始碼] 

import matplotlib.pyplot as plt

plt.style.use('classic')     
week=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('Temperature Change')                      
plt.xlabel('Weekday')                                                 
plt.ylabel('Celsius')  
plt.show()

結果如下 : 




注意, 在 Notebook 環境中只要使用 plt.style.use() 更改風格後, 往下的繪圖都會延續套用此風格, 如果要返回預設風格需呼叫 plt.style.use('default'). 


六. 文字註解 : 

繪圖區除了圖形以外還可以呼叫 plt.text() 在指定座標點繪製文字以便標註圖形中的某個資料點或轉折點, 用來說明或註解圖形, 語法如下 :

plt.text(x, y, str [, color='black', fontsize=10, alpha=1, ha='left', va='center'])   

常用參數說明如下 :
  • x, y : 開始繪製文字的起始座標.
  • str : 要繪製的文字內容.
  • color : 文字顏色 (預設黑色), 可用顏色名稱字串如 'blue', 十六進制色碼字串 '#11ff2a', 或十進制 (R, G, B) 色碼元組. 
  • fontsize (或 size) : 字型大小 (預設 10)
  • alpha : 值為 0~1 的透明度 (0 為全透明, 1 為不透明, 預設 1).
  • ha : 水平對齊, 可用 'left' (預設), 'center', 'right'.
  • va : 垂直對齊, 可用 'center' (預設), 'top', 'bottom', 'baseline', 'center_baseline'
參考 : 


例如 : 


測試 6-1 : 呼叫 plt.text() 繪製文字 [看原始碼]  

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.text('一', 23.2, '太平洋高壓使溫度回升')     
plt.text('三', 28, '冷鋒過境溫度驟降', color='blue', fontsize=16, alpha=0.5)    
plt.show()

此例分別在座標 ('一', 23.2) 與 ('三', 28) 開始繪製文字, 結果如下 : 




這樣在圖形上加註文字可增進可讀性. 但 plt.text() 只能標註文字, 如果能添加箭號會更具有指引作用, 這可以改用 plt.annotate() 函式來繪製, 其常用語法如下 : 

plt.annotate(text, xy [, xytext, arrowprops])     

參數說明如下 :
  • text : 註解的文字 (字串)
  • xy : 被註解的座標位置 (即箭頭所指位置), 是一個 (x, y) 元組
  • xytext : 註解文字第一個字元的座標位置, 也是一個 (x, y) 元組, 預設值為 xy
  • arrowprops : 箭頭的樣式, 是一個由樣式參數組成的字典    
其實 arrowprops 字典中的樣式屬性頗多, 常用樣式例如 : 
  • facecolor : 箭號顏色
  • width : 箭身寬度 (pixel, 預設 4)
  • headwidth : 箭身寬度 (pixel, 預設 12)
  • shrink : 兩端長度內縮比例 (避免與目標點太接近), 值 0~1, 0=原長度不內縮
此外 arrowprops 樣式字典還提供了 'Fancy' 風格的箭頭造型, 可繪製弧線箭頭或各式造型的箭頭, 這主要是透過 arrowstyle 屬性來設定, 參考 :


例如 :


測試 6-2 : 呼叫 plt.annotate() 繪製文字(無箭頭) [看原始碼]  

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.annotate('太平洋高壓使溫度回升', xy=('一', 23.2))   # 標註文字
plt.annotate('冷鋒過境溫度驟降', xy=('三', 28))              # 標註文字
plt.show()

此例分別在座標 ('一', 23.2) 與 ('三', 28) 兩處進行標註, 結果如下 : 




可見與上面用 plt.text() 的標註效果類似. 但 plt.annotate() 特殊之處是可以加上箭頭, 例如 :


測試 6-3 : 呼叫 plt.annotate() 繪製有箭頭的文字 (預設寬度) [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.annotate('太平洋高壓使溫度回升',
             xy=('一', 23.7),
             xytext=('一', 22),
             arrowprops=dict(facecolor='blue', shrink=0.05))    # 兩端內縮 5% 的藍色箭號
plt.annotate('冷鋒過境溫度驟降',
             xy=('三', 29.2),
             xytext=('四', 28),
             arrowprops=dict(facecolor='red', shrink=0.05))     # 兩端內縮 5% 的紅色箭號
plt.show()

此例以 plt.annotate() 繪製了兩個有箭號的標註文字, 結果如下 : 




可見預設的箭頭與箭身寬度似乎有點胖, 可用 width 與 headwidth 設定, 例如 : 


測試 6-4 : 呼叫 plt.annotate() 繪製有箭頭的文字 (指定寬度) [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.annotate('太平洋高壓使溫度回升',
             xy=('一', 23.7),
             xytext=('一', 22),
             arrowprops=dict(width=1, headwidth=6))    # 指定箭頭與箭身寬度
plt.annotate('冷鋒過境溫度驟降',
             xy=('三', 29.2),
             xytext=('四', 28),
             arrowprops=dict(width=1, headwidth=6))    # 指定箭頭與箭身寬度
plt.show()

此例指定箭頭寬度 6px, 箭身寬度 1px, 兩端不內縮以預設顏色繪製, 結果如下 : 




可見預設不內縮 (shrink=0) 時箭頭座標要稍微離目標點有個小距離, 否則會與圖形曲線交叉, 這就是 shrink 參數的用途, 即箭頭座標可以直接設為圖形上的目標資料點, 然後用 shrink 讓箭頭末端不會與該點交叉. 

下面是弧線箭頭的範例 :


測試 6-5 : 呼叫 plt.annotate() 繪製有弧線箭頭的文字 [看原始碼] 

import matplotlib.pyplot as plt

plt.rcParams["font.family"]=["Microsoft JhengHei"]    # 使用微軟正黑體
week=['日','一','二','三','四','五','六']
temperature=[25.4, 23.7, 28.6, 29.2, 24.8, 22.5, 21.9]
plt.plot(week, temperature)
plt.title('溫度變化圖')                      
plt.xlabel('星期')                                                 
plt.ylabel('攝氏')     
plt.annotate('太平洋高壓使溫度回升',
             xy=('一', 23.7),
             xytext=('三', 22),
             arrowprops=dict(arrowstyle='->',
                             connectionstyle='angle3, angleA=0, angleB=-90'))  # 0 到 -90 度角弧線
plt.annotate('冷鋒過境溫度驟降',
             xy=('三', 29.2),
             xytext=('四', 28),
             arrowprops=dict(arrowstyle='->'
                             connectionstyle='arc3, rad=-0.2'))    # -0.2 弧度之弧線
plt.show()

此例使用 arrowstyle 屬性設定箭頭格式, 以 connectionstyle 屬性設定箭身的弧線角度, 第一個箭頭使用 'angle3' 樣式, 方向從箭尾 (angleA) 的 0 度到箭頭 (angleB) 的 -90 度 (即從底部向上); 第二的箭頭使用 'arc3' 樣式, 弧度為 -0.2, 結果如下 : 




更多箭號樣式範例可參考 "Python資料科學學習手冊" 這本書的第四章. 


七. 繪製子圖 : 

之前的測試範例都只是繪製單圖, 雖然可以同時繪製多個圖形, 但這些圖形都是套疊在一張圖上, 而所謂的子圖 (subplot) 是指可在稱為 figure 的容器 (畫布) 上利用網格座標繪製兩個以上的繪圖區, 每一個繪圖區稱為 axes.

呼叫 plt.subplot() 函式可建立多個子圖, 語法如下 : 

plt.subplot(nrow, ncol, index)

參考 : 


此函式會建立 nrow * ncol 的二維網格容器, 然後定位到索引為 index 的網格, 其中 nrow 為網格的列數, ncol 為欄數, 而 index 為網格的索引 (以左上角的 1 起始, 依序向右向下遞增), 例如 :




因此 subplot(3, 2, 4) 表示定位到一個 3*2 網格中索引為 4 的那個繪圖區, 每一個繪圖區都有獨立的座標系統. 定位好後呼叫的 plt.plot() 即在此區繪圖, 若要更換繪圖區需再呼叫 subplot(). 注意, 也可以將 (3, 2, 4) 簡寫成 (324). 

下面範例以迴圈在畫布容器上建立 3*2 的網格的六個繪圖區 :


測試 7-1 : 以迴圈產生 3*2 網格的六個子圖 [看原始碼]  

import matplotlib.pyplot as plt

for i in range(1, 7):
    plt.subplot(2, 3, i)
    plt.text(0.3, 0.5, str((2, 3, i)))
plt.show()

此例在迴圈中依序呼叫 subplot() 定位 3*2 網格中的一個子繪圖區, 並在其中繪製文字 (子圖座標), 結果如下 :



由於沒有呼叫 plt.plot() 繪製函數圖形, 所以預設 X/Y 軸範圍均為 0~1. 可見預設的版面配置會讓座標軸刻度標籤重疊, 解決辦法是在呼叫 plt.show() 之前先呼叫 plt.tight_layout(), 結果如下 : 




如果要合併儲存格, 例如合併一整欄或一整列為一大格, 可以將要合併的欄 (ncol) 或列 (nrow) 指定為 1, 例如欲合併 2*2 網格中的第二列, 則在呼叫 subplot(2,2,1) 與 subplot(2,2,2) 後要呼叫 subplot(2, 1, 2), 相當於將原來的 2*2 網格降階為 2*1, 而這塊是其中的第 2 塊, 如下圖所示 : 




測試 7-1-1 : 合併 2*2 網格的第 2  列整列為一個儲存格 [看原始碼]  

import numpy as np
import matplotlib.pyplot as plt

plt.subplot(221)                
plt.subplot(222)
plt.subplot(212)
plt.show()

結果如下 : 




如果要將 2*2 網格第一欄合併為一個儲存格可以這麼做 :




測試 7-1-2 : 合併 2*2 網格的第 1  欄整欄為一個儲存格 [看原始碼]  

import numpy as np
import matplotlib.pyplot as plt

plt.subplot(121)                
plt.subplot(222)
plt.subplot(224)
plt.show()

此例在處理第一欄時把原 2*2 網格降階為 1*2, 第一欄這格是 121, 另外兩格則恢復為 2*2 配置, 所以是 222 與 224, 結果如下 :




較複雜的版面配置需要使用物件導向模式用法. 

下面範例是在 2*1 網格中繪製兩個函數 :


測試 7-2 : 在 2*1 網格的兩個子圖上繪製函數圖形 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(-5, 5, 100)
plt.subplot(211)                 # 定位 (2, 1, 1) 子圖
y=x                                    # 在 (2, 1, 1) 子圖上繪製函數 y=x
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(212)                 # 定位 (2, 1, 2) 子圖
y=x**2                               # 在 (2, 1, 2) 子圖上繪製函數 y=x
plt.title('y=x**2')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

此例先後用 plt.subplot() 選擇要繪圖的子圖, 然後在上面分別繪製 y=x 與 y=x**2 函數, 在呼叫下一個 plt.subplot() 之前, 任何繪圖設定都是作用在此子圖上, 結果如下 :




可見上圖的 X 軸刻度會與下圖的標題部分重疊, 這是 pyplot 速成繪圖方式不完美之處, 需要用物件導向方式設定間隔來解決. 

下面範例是將 y=x 與 y=x**2 這兩個函數改在 1*2 網格繪製 : 


測試 7-3 : 在 1*2 網格的兩個子圖上繪製函數圖形 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(-5, 5, 100)
plt.subplot(121)  
y=x
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(122)   
y=x**2
plt.title('y=x**2')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

結果如下 :



下面是 2*2 網格的子圖範例: 


測試 7-4 : 在 2*2 網格的兩個子圖上繪製函數圖形 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(-5, 5, 100)
plt.subplot(221)
y=x
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(222)
y=x**2
plt.title('y=x**2')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(223)
y=x**3
plt.title('y=x**3')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(224)
y=x**4
plt.title('y=x**4')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

結果如下 :




上面範例的 X 軸都是一樣的, 下面範例展示繪圖區各自獨立的座標系統 :


測試 7-5 : 獨立坐標系的子圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(-5, 5, 100)
plt.subplot(221)
y=x                            # 繪製線性函數
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(222)
x=np.linspace(-np.pi, np.pi, 100)
y=np.sin(x)                 # 繪製正弦波函數
plt.title('y=sin(x)')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(223)
x=np.linspace(-3, 3, 100)
y=np.exp(x)                 # 繪製指數函數
plt.title('y=e**x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.subplot(224)
x=np.linspace(0.001, 10, 100)
y=np.log(x)                   # 繪製對數函數
plt.title('y=log(x)')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

結果如下 : 




可見這四個子圖都具有各自的 X/Y 軸座標. 

除了使用 plt.subplot() 外還可以用 plt.axes() 函式來建立子圖, 它與 plt.subplot() 不同之處是必須自行安排各子圖在畫布上的位置與大小, 而用 plt.subplot() 則不需要, 它會自動劃分畫布區域給各子圖. plt.axes() 是以畫布的左下角做為參考點, 以相對距離的方式建立一個繪圖區, 語法如下 :

plt.axes(rect)     

參數 rect 是一個元組 (tuple) 或串列, 格式為 (left, button, width, height), 說明如下 :
  • left : 與畫布左邊界之距離
  • right : 與畫布下邊界之距離
  • width : 繪圖區寬度
  • height : 繪圖區高度
這四個值都是 0~1 的浮點數, 表示佔畫布尺寸的比例, 若未傳入參數相當於 (0, 0, 1, 1), 即建立佔據整個畫布之繪圖區, 參考 :


例如 : 


測試 7-6 : 用 plt.axes() 建立獨立的子圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt
plt.axes([0.05, 0.05, 0.4, 0.95])      # 繪圖區原點座標 (0.05, 0.05) 寬高 (0.4, 0.95)
x=np.linspace(-5, 5, 100)
y=x                                    
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)

plt.axes([0.55, 0.05, 0.4, 0.95])       # 繪圖區原點座標 (0.55, 0.05) 寬高 (0.4, 0.95)         
y=x**2                              
plt.title('y=x**2')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

此例以畫布比例座標 (0.05, 0.05) 與 (0.55, 0.05) 為原點, 分別建立兩個寬高比例為 (0.4, 0.95) 的子圖, 結果如下 : 




因為 plt.axes() 必須自行指定子圖得位置與大小, 因此可以做出疊圖效果, 例如 :


測試 7-7 : 用 plt.axes() 建立套疊的子圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt
plt.axes()            # 建立一個佔據整個畫布的子圖
x=np.linspace(-5, 5, 100)
y=x                                    
plt.title('y=x')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)

plt.axes([0.55, 0.2, 0.3, 0.3])         # 建立一個尺寸較小的套疊子圖         
y=x**2                              
plt.title('y=x**2')                      
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)    
plt.show()

此例先呼叫 plt.axes() 不傳入參數, 這樣會建立一個與畫布幾乎一樣大的繪圖區 (子圖), 然後在其上繪製 y=x 圖形. 接著再呼叫 plt.axes() 並傳入第二子圖的尺寸, 因為它比較小且位於右下角, 因此看起來像是疊上去的, 結果如下 : 




更多關於子圖的細部調校須要使用物件導向寫法才能做到.


八. 設定畫布 :

Matplotlib 是一個物件導向的繪圖套件, 它是在一個 Figure 容器物件 (畫布) 上切割出若干具有獨立座標系的繪圖區 (Axes 物件), 然後在各自繪圖區上進行繪圖. 在使用 Matlab 快速繪圖模式時, pyplot 會自動產生 Figure 與 Axes 物件, 並套用預設的大小, 解析度, 顏色等設定值. 如果要自行設定, 則需呼叫 plt.figure() 函式並傳入參數來建立自訂的畫布, 其語法為 : 

plt.figure([num, figsize, dpi, facecolor, edgecolor, linewidth, frameon, tight_layout])    

其中 num 為識別不同畫布的唯一編號, figsize 為畫布大小 [width, height] (單位為英吋, 預設為 [6.4, 4.8]), dpi 為解析度 (預設 100), facecolor 為背景色 (預設為白色), edgecolor為邊緣顏色 (預設為白色), frameon 為是否有邊框 (預設 True, 有), tight_layout 為多個畫布之間是否有間閣 (預設 False, 無). 注意, plt.figure() 必須是 import 後的第一個指令, 否則會出現兩個繪圖視窗 (第一個是預設 figure). 參考 : 

  
例如 : 


測試 8-1 : 呼叫 plt.figure() 自定畫布 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

plt.figure(figsize=[8, 6],
           dpi=120,
           facecolor='cyan',
           edgecolor='blue',
           linewidth=2,
           frameon=False,
           tight_layout=True)
x=np.linspace(-5, 5, 100)
y=x**2
plt.title('y=x**2')
plt.xlabel('x')                                                 
plt.ylabel('y') 
plt.plot(x, y)
plt.show()

此例先用 plt.figure() 設定畫布的繪圖參數後, 再進行繪圖作業, 結果如下 :



畫布大小變大了, 但不知何故 facecolor, edgecolor, 以及 linewidth 這三個參數卻沒有效果.


九. 對數座標 :

有些數據的範圍很大, 但需要注意的地方卻是在值很大的部分, 這時用對數座標來繪圖較能看出數據變化的趨勢, 因為對數可以把大數的 scale 縮小. 在 Matplotlib 中可以呼叫下列三個函式來繪製對數座標的圖形 : 
  1. plt.semilogx(x, y) : X 軸為對數座標 (Y 軸為正常座標)
  2. plt.semilogy(x, y) : Y 軸為對數座標 (X 軸為正常座標)
  3. plt.loglog(x, y) : X 軸與 Y 軸都是對數座標
例如 : 


測試 9-1 : 呼叫 plt.semilogx() 繪製 X 軸對數座標圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0.01, 10, 100)
y=np.sin(2 * np.pi * x)
plt.semilogx(x, y)             # X 軸為對數座標
plt.show()

此例繪製 X 軸座標範圍從 0.01 至 10 (1000 倍) 的正弦波形, 結果如下 : 




可見 X 軸上的線性刻度其實並不等距, 每一大格差距是 10 倍, 可見對數把原本差 1000 倍的刻度縮小到等距的刻度上了. 

下面是 Y 軸以對數座標繪製的範例 : 


測試 9-2 : 呼叫 plt.semilogy() 繪製 Y 軸對數座標圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0.01, 10, 100)
y=np.sin(2 * np.pi * x)
plt.semilogy(x, y)     
plt.show()

此例改為 Y 軸對數, 但 X 軸正常, 結果如下 : 




下面是 X 軸與 Y 軸都是對數座標的範例 : 


測試 9-3 : 呼叫 plt.semilogy() 繪製 Y 軸對數座標圖 [看原始碼] 

import numpy as np
import matplotlib.pyplot as plt

x=np.linspace(0.01, 10, 100)
y=np.sin(2 * np.pi * x)
plt.loglog(x, y)               # X/Y 軸都是對數座標
plt.show()

結果如下 : 




以上對數座標測試參考 "Hands-on Matplotlib" 這本書的第六章. 

沒有留言 :