萬字Matplotlib實操總結,幾行代碼實現數據繪圖

https://zhuanlan.zhihu.com/p/270319722

導讀

Matplotlib 是一個 Python 的 2D繪圖庫,它以各種硬拷貝格式和跨平臺的交互式環境生成出版質量級別的圖形。通過 Matplotlib,開發者可以僅需要幾行代碼,便可以生成繪圖,直方圖,功率譜,條形圖,錯誤圖,散點圖等。

以下內容來自Github,爲《PythonDataScienceHandbook[1]》(Python 數據科學手冊[2])第四章Matplotlib介紹部分。全部內容都在以下環境演示通過:

  • numpy:1.18.5
  • pandas:1.0.5
  • matplotlib:3.2.1

1.簡單的折線圖

對於圖表來說,最簡單的莫過於作出一個單一函數 的圖像。本節中我們首先來介紹創建這種類型圖表。本節和後續小節中,我們都會使用下面的代碼將我們需要的包載入到 notebook 中:

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np

對於所有的 Matplotlib 圖表來說,我們都需要從創建圖形和維度開始。圖形和維度可以使用下面代碼進行最簡形式的創建:

fig = plt.figure()
ax = plt.axes()

在 Matplotlib 中,圖形(類plt.Figure的一個實例)可以被認爲是一個包括所有維度、圖像、文本和標籤對象的容器。維度(類plt.Axes的一個實例)就是你上面看到的圖像,一個有邊界的格子包括刻度和標籤,最終還有我們畫在上面的圖表元素。在本書中,我們會使用變量名fig來指代圖形對象,以及變量名ax來指代維度變量。

一旦我們創建了維度,我們可以使用ax.plot方法將數據繪製在圖表上。下面是一個簡單的正弦函數圖形:

fig = plt.figure()
ax = plt.axes()

x = np.linspace(0, 10, 1000)
ax.plot(x, np.sin(x));

同樣的,我們可以使用 pylab 接口(MATLAB 風格的接口)幫我們在後臺自動創建這兩個對象:

plt.plot(x, np.sin(x));

如果我們需要在同一幅圖形中繪製多根線條,只需要多次調用plot函數即可:

plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x));

這就是在 Matplotlib 中繪製簡單函數圖像的所有接口了。下面我們深入瞭解一下控制座標軸和線條外觀的細節。

調整折線圖:線條顏色和風格

你可能第一個想到需要進行調整的部分就是線條的顏色和風格。plt.plot()函數接受額外的參數可以用來指定它們。通過指定color關鍵字參數可以調整顏色,這個字符串類型參數基本上能用來代表任何你能想到的顏色。可以通過多種方式指定顏色參數:

所有 HTML 顏色名稱可以在這裏[3]找到。

plt.plot(x, np.sin(x - 0), color='blue')        # 通過顏色名稱指定
plt.plot(x, np.sin(x - 1), color='g')           # 通過顏色簡寫名稱指定(rgbcmyk)
plt.plot(x, np.sin(x - 2), color='0.75')        # 介於0-1之間的灰階值
plt.plot(x, np.sin(x - 3), color='#FFDD44')     # 16進制的RRGGBB值
plt.plot(x, np.sin(x - 4), color=(1.0,0.2,0.3)) # RGB元組的顏色值,每個值介於0-1
plt.plot(x, np.sin(x - 5), color='chartreuse'); # 能支持所有HTML顏色名稱值

如果沒有指定顏色,Matplotlib 會在一組默認顏色值中循環使用來繪製每一條線條。

類似的,通過linestyle關鍵字參數可以指定線條的風格:

plt.plot(x, x + 0, linestyle='solid')
plt.plot(x, x + 1, linestyle='dashed')
plt.plot(x, x + 2, linestyle='dashdot')
plt.plot(x, x + 3, linestyle='dotted');

# 還可以用形象的符號代表線條風格
plt.plot(x, x + 4, linestyle='-')  # 實線
plt.plot(x, x + 5, linestyle='--') # 虛線
plt.plot(x, x + 6, linestyle='-.') # 長短點虛線
plt.plot(x, x + 7, linestyle=':');  # 點線

如果你喜歡更簡潔的代碼,這些linestyle和color參數能夠合併成一個非關鍵字參數,傳遞給plt.plot()函數:

plt.plot(x, x + 0, '-g')  # 綠色實線
plt.plot(x, x + 1, '--c') # 天青色虛線
plt.plot(x, x + 2, '-.k') # 黑色長短點虛線
plt.plot(x, x + 3, ':r');  # 紅色點線

上面的單字母顏色碼是 RGB 顏色系統以及 CMYK 顏色系統的縮寫,被廣泛應用在數字化圖像的顏色系統中。

還有很多其他的關鍵字參數可以對摺線圖的外觀進行精細調整;可以通過在 IPython 中使用幫助工具查看plt.plot()函數的文檔來獲得更多細節內容。

調整折線圖:座標軸範圍

Matplotlib 會自動選擇非常合適的座標軸範圍來繪製你的圖像,但是有些情況下你也需要自己進行相關調整。使用plt.xlim()和plt.ylim()函數可以調整座標軸的範圍:

plt.plot(x, np.sin(x))

plt.xlim(-1, 11)
plt.ylim(-1.5, 1.5);

如果某些情況下你希望將座標軸反向,你可以通過上面的函數實現,將參數順序顛倒即可:

plt.plot(x, np.sin(x))

plt.xlim(10, 0)
plt.ylim(1.2, -1.2);

相關的函數還有plt.axis()(注意:這不是plt.axes()函數,函數名稱是 i 而不是 e)。這個函數可以在一個函數調用中就完成 x 軸和 y 軸範圍的設置,傳遞一個[xmin, xmax, ymin, ymax]的列表參數即可:

plt.plot(x, np.sin(x))
plt.axis([-1, 11, -1.5, 1.5]);

當然plt.axis()函數不僅能設置範圍,還能像下面代碼一樣將座標軸壓縮到剛好足夠繪製折線圖像的大小:

plt.plot(x, np.sin(x))
plt.axis('tight');

還可以通過設置'equal'參數設置x軸與y軸使用相同的長度單位:

plt.plot(x, np.sin(x))
plt.axis('equal');

更多關於設置 axis 屬性的內容請查閱plt.axis函數的文檔字符串。

 

折線圖標籤

本節最後介紹一下在折線圖上繪製標籤:標題、座標軸標籤和簡單的圖例。

標題和座標軸標籤是最簡單的這類標籤,Matplotlib 提供了函數用來方便的設置它們:

plt.plot(x, np.sin(x))
plt.title("A Sine Curve")
plt.xlabel("x")
plt.ylabel("sin(x)");

這些標籤的位置、大小和風格可以通過上面函數的可選參數進行設置。參閱 Matplotlib 在線文檔和這些函數的文檔字符串可以獲得更多的信息。

當一幅圖中繪製了多條折線時,如果能夠繪製一個線條對應的圖例能讓圖表更加清晰。Matplotlib 也內建了函數來快速創建圖例。估計你也猜到了,通過plt.legend()函數可以實現這個需求。雖然有很多種正確的方法來指定圖例,作者認爲最簡單的方法是通過在繪製每條線條時指定對應的label關鍵字參數來使用這個函數:

plt.plot(x, np.sin(x), '-g', label='sin(x)')
plt.plot(x, np.cos(x), ':b', label='cos(x)')
plt.axis('equal')

plt.legend();

上圖可見,plt.legend()函數繪製的圖例線條與圖中的折線無論風格和顏色都保持一致。查閱plt.legend文檔字符串可以獲得更多相關信息;我們在[自定義圖表圖例]一節中也會討論更高級的圖例應用。

 

額外內容:Matplotlib 的坑

雖然大多數的plt函數都可以直接轉換爲ax的方法進行調用(例如plt.plot() → ax.plot(),plt.legend() → ax.legend()等),但是並不是所有的命令都能應用這種情況。特別是用於設置極值、標籤和標題的函數都有一定的改變。下表列出了將 MATLAB 風格的函數轉換爲面向對象的方法的區別:

  • plt.xlabel() → ax.set_xlabel()
  • plt.ylabel() → ax.set_ylabel()
  • plt.xlim() → ax.set_xlim()
  • plt.ylim() → ax.set_ylim()
  • plt.title() → ax.set_title()

在面向對象接口中,與其逐個調用上面的方法來設置屬性,更常見的使用ax.set()方法來一次性設置所有的屬性:

ax = plt.axes()
ax.plot(x, np.sin(x))
ax.set(xlim=(0, 10), ylim=(-2, 2),
       xlabel='x', ylabel='sin(x)',
       title='A Simple Plot');

 

2.簡單散點圖

另一種常用的圖表類型是簡單散點圖,它是折線圖的近親。不像折線圖,圖中的點連接起來組成連線,散點圖中的點都是獨立分佈的點狀、圓圈或其他形狀。本節開始我們也是首先將需要用到的圖表工具和函數導入到 notebook 中:

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np

使用 plt.plot 繪製散點圖

在上一節中,我們介紹了plt.plot/ax.plot方法繪製折線圖。這兩個方法也可以同樣用來繪製散點圖:

x = np.linspace(0, 10, 30)
y = np.sin(x)

plt.plot(x, y, 'o', color='black');

傳遞給函數的第三個參數是使用一個字符代表的圖表繪製點的類型。就像你可以使用'-'或'--'來控制線條的風格那樣,點的類型風格也可以使用短字符串代碼來表示。所有可用的符號可以通過plt.plot文檔或 Matplotlib 在線文檔進行查閱。大多數的代碼都是非常直觀的,我們使用下面的例子可以展示那些最通用的符號:

rng = np.random.RandomState(0)
for marker in ['o', '.', ',', 'x', '+', 'v', '^', '<', '>', 's', 'd']:
    plt.plot(rng.rand(5), rng.rand(5), marker,
             label="marker='{0}'".format(marker))
plt.legend(numpoints=1)
plt.xlim(0, 1.8);

而且這些符號代碼可以和線條、顏色代碼一起使用,這會在折線圖的基礎上繪製出散點:

plt.plot(x, y, '-ok');

plt.plot還有很多額外的關鍵字參數用來指定廣泛的線條和點的屬性:

plt.plot(x, y, '-p', color='gray',
         markersize=15, linewidth=4,
         markerfacecolor='white',
         markeredgecolor='gray',
         markeredgewidth=2)
plt.ylim(-1.2, 1.2);

plt.plot函數的這種靈活性提供了很多的可視化選擇。查閱plt.plot幫助文檔獲得完整的選項說明。

 

使用plt.scatter繪製散點圖

第二種更強大的繪製散點圖的方法是使用plt.scatter函數,它的使用方法和plt.plot類似:

plt.scatter(x, y, marker='o');

plt.scatter和plt.plot的主要區別在於,plt.scatter可以針對每個點設置不同屬性(大小、填充顏色、邊緣顏色等),還可以通過數據集合對這些屬性進行設置。

讓我們通過一個隨機值數據集繪製不同顏色和大小的散點圖來說明。爲了更好的查看重疊的結果,我們還使用了alpha關鍵字參數對點的透明度進行了調整:

rng = np.random.RandomState(0)
x = rng.randn(100)
y = rng.randn(100)
colors = rng.rand(100)
sizes = 1000 * rng.rand(100)

plt.scatter(x, y, c=colors, s=sizes, alpha=0.3,
            cmap='viridis')
plt.colorbar();  # 顯示顏色對比條

注意圖表右邊有一個顏色對比條(這裏通過colormap()函數輸出),圖表中的點大小的單位是像素。使用這種方法,散點的顏色和大小都能用來展示數據信息,在希望展示多個維度數據集合的情況下很直觀。

例如,當我們使用 Scikit-learn 中的鳶尾花數據集,裏面的每個樣本都是三種鳶尾花中的其中一種,並帶有仔細測量的花瓣和花萼的尺寸數據:

from sklearn.datasets import load_iris
iris = load_iris()
features = iris.data.T

plt.scatter(features[0], features[1], alpha=0.2,
            s=100*features[3], c=iris.target, cmap='viridis')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1]);

 

我們可以從上圖中看出,可以通過散點圖同時展示該數據集的四個不同維度:圖中的(x, y)位置代表每個樣本的花萼的長度和寬度,散點的大小代表每個樣本的花瓣的寬度,而散點的顏色代表一種特定的鳶尾花類型。如上圖的多種顏色和多種屬性的散點圖對於我們分析和展示數據集時都非常有幫助。

plot 和 scatter 對比:性能提醒

除了上面說的plt.plot和plt.scatter對於每個散點不同屬性的支持不同之外,還有別的因素影響對這兩個函數的選擇嗎?對於小的數據集來說,兩者並無差別,當數據集增長到幾千個點時,plt.plot會明顯比plt.scatter的性能要高。造成這個差異的原因是plt.scatter支持每個點使用不同的大小和顏色,因此渲染每個點時需要完成更多額外的工作。而plt.plot來說,每個點都是簡單的複製另一個點產生,因此對於整個數據集來說,確定每個點的展示屬性的工作僅需要進行一次即可。對於很大的數據集來說,這個差異會導致兩者性能的巨大區別,因此,對於大數據集應該優先使用plt.plot函數。

 

3.誤差可視化

對於任何的科學測量來說,精確計算誤差與精確報告測量值基本上同等重要。例如,設想我正在使用一些天文物理學觀測值來估算哈勃常數,即本地觀測的宇宙膨脹係數。我從一些文獻中知道這個值大概是 71 (km/s)/Mpc,而我測量得到的值是 74 (km/s)/Mpc,。這兩個值是否一致?在僅給定這些數據的情況下,這個問題的答案是,無法回答。

Mpc(百萬秒差距)參見秒差距[4]

如果我們將信息增加一些,給出不確定性:最新的文獻表示哈勃常數的值大約是 71 2.5 (km/s)/Mpc,我的測量值是 74 5 (km/s)/Mpc。這兩個值是一致的嗎?這就是一個可以準確回答的問題了。

在數據和結果的可視化中,有效地展示這些誤差能使你的圖表涵蓋和提供更加完整的信息。

 

基礎誤差條

調用一個 Matplotlib 函數就能創建一個基礎的誤差條:

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
import numpy as np

x = np.linspace(0, 10, 50)
dy = 0.8
y = np.sin(x) + dy * np.random.randn(50)

plt.errorbar(x, y, yerr=dy, fmt='.k');

這裏的fmt參數是用來控制線條和點風格的代碼,與plt.plot有着相同的語法,參見[簡單的折線圖]和[簡單的散點圖]。

除了上面的基本參數,errorbar函數還有很多參數可以用來精細調節圖表輸出。使用這些參數你可以很容易的個性化調整誤差條的樣式。作者發現通常將誤差線條顏色調整爲淺色會更加清晰,特別是在數據點比較密集的情況下:

plt.errorbar(x, y, yerr=dy, fmt='o', color='black',
             ecolor='lightgray', elinewidth=3, capsize=0);

除了上面介紹的參數,你還可以指定水平方向的誤差條(xerr),單邊誤差條和其他很多的參數。參閱plt.errorbar的幫助文檔獲得更多信息。

 

連續誤差

在某些情況下可能需要對連續值展示誤差條。雖然 Matplotlib 沒有內建的函數能直接完成這個任務,但是你可以通過簡單將plt.plot和plt.fill_between函數結合起來達到目標。

這裏我們會採用簡單的高斯過程迴歸方法,Scikit-Learn 提供了 API。這個方法非常適合在非參數化的函數中獲得連續誤差。我們在這裏不會詳細介紹高斯過程迴歸,僅僅聚焦在如何繪製連續誤差本身:

譯者注:新版的 sklearn 修改了高斯過程迴歸實現方法,下面代碼做了相應修改。

from sklearn.gaussian_process import GaussianProcessRegressor

# 定義模型和一些符合模型的點
model = lambda x: x * np.sin(x)
xdata = np.array([1, 3, 5, 6, 8])
ydata = model(xdata)

# 計算高斯過程迴歸,使其符合 fit 數據點
gp = GaussianProcessRegressor()
gp.fit(xdata[:, np.newaxis], ydata)

xfit = np.linspace(0, 10, 1000)
yfit, std = gp.predict(xfit[:, np.newaxis], return_std=True)
dyfit = 2 * std  # 兩倍sigma ~ 95% 確定區域

我們現在有了xfit、yfit和dyfit,作爲對我們數據的連續擬合值以及誤差限。當然我們也可以像上面一樣使用plt.errorbar繪製誤差條,但是事實上我們不希望在圖標上繪製 1000 個點的誤差條。於是我們可以使用plt.fill_between函數在誤差限區域內填充一道淺色的誤差帶來展示連續誤差:

# 可視化結果
plt.plot(xdata, ydata, 'or')
plt.plot(xfit, yfit, '-', color='gray')

plt.fill_between(xfit, yfit - dyfit, yfit + dyfit,
                 color='gray', alpha=0.2)
plt.xlim(0, 10);

注意上面我們調用fill_between函數:我們傳遞了的參數包括 x 值,y 值的低限,然後是 y 值的高限,結果是圖表中介於低限和高限之間的區域會被填充。

上圖爲我們提供了一個非常直觀的高斯過程迴歸展示:在觀測點的附近,模型會被限制在一個很小的區域內,反映了這些數據的誤差比較小。在遠離觀測點的區域,模型開始發散,反映了這時的數據誤差比較大。

如果需要獲得plt.fill_between(以及類似的plt.fill函數)更多參數的信息,請查閱函數的幫助文檔或 Matplotlib 在線文檔。

 

4.密度和輪廓圖

有些情況下,我們需要在二維圖表中使用輪廓或顏色區域來展示三維的數據(可以設想等高線地圖或溫度分佈圖)。Matplotlib 提供了三個有用的函數來處理這項任務:plt.contour繪製輪廓圖,plt.contourf來繪製填充區域顏色的圖表以及plt.imshow來展示圖像。本節會介紹幾個使用它們的例子。當然我們還是首先從將需要使用的包導入 notebook 和初始化工作開始:

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
import numpy as np

 

三維可視化函數

我們首先使用一個簡單的函數 繪製一個輪廓圖來進行說明,我們用來作爲數組廣播運算的例子:

def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)

輪廓圖可以使用plt.contour函數進行創建。它接收三個參數:x參數代表三維網格的平面橫軸座標,y參數代表三維網格的平面縱軸座標,而z參數代表三維網格的高度座標。最容易用來準備這種網格數據的是np.meshgrid函數,可以將兩個一維的數組構造成一個二維的網格:

x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 40)

X, Y = np.meshgrid(x, y)
Z = f(X, Y)

下面我們可以繪製標準的輪廓線圖表:

plt.contour(X, Y, Z, colors='black');

 

圖中值得注意的是,當使用單色繪製輪廓圖時,虛線代表的是負數的數值,而實線代表的是正數。而輪廓線可以通過指定cmap參數來設置線條的色圖。下例中展示了使用色圖且繪製了更多的輪廓線的例子,會在整個數據範圍區域內等距分佈有 20 條輪廓線:

plt.contour(X, Y, Z, 20, cmap='RdGy');

上例中我們選擇了RdGy(Red-Gray的縮寫)色圖,這對於聚集的數據來說是一個不錯的選擇。Matplotlib 有大量的顏色圖可供使用,你可以通過在 IPython 中對plt.cm模塊使用 TAB 自動補全方法就可以看到:

plt.cm.<TAB>

上面的圖看起來比第一幅圖好多了,但是線條之間的空隙還是有點讓人混淆。我們可以將上面的圖改爲填充輪廓圖來解決這個問題,使用plt.contourf()函數(注意函數名最後有個 f,代表填充 fill),這個函數的語法基本上與plt.contour()保持一致。

並且我們加上了plt.colorbar()函數,這個函數會在圖表邊上創建一個顏色圖例用以展示顏色所表示的數值區域:

plt.contourf(X, Y, Z, 20, cmap='RdGy')
plt.colorbar();

有了圖例,很容易可以看出黑色區域代表着“峯”,而紅色區域代表這“谷”。

上圖有一個缺點,那就是圖中顏色的階梯是離散的而不是連續的,這通常不是我們想要的。我們可以通過設置很高的輪廓線數量來改善,但是這會導致繪製圖表的性能降低:Matplotlib 必須在每個顏色階梯上繪製一條新的輪廓多邊形。更好的辦法是使用plt.imshow()函數,它會將一個二維的網格圖表轉換爲一張圖像。

下面的例子展示了該方法:

plt.imshow(Z, extent=[0, 5, 0, 5], origin='lower',
           cmap='RdGy')
plt.colorbar()
plt.axis(aspect='image');

然而,在使用imshow()的時候也有一些坑:

  • plt.imshow()不接受 x 和 y 網格值作爲參數,因此你需要手動指定extent參數[xmin, xmax, ymin, ymax]來設置圖表的數據範圍。
  • plt.imshow()使用的是默認的圖像座標,即左上角座標點是原點,而不是通常圖表的左下角座標點。這可以通過設置origin參數來設置。
  • plt.imshow()會自動根據輸入數據調整座標軸的比例;這可以通過參數來設置,例如,plt.axis(aspect='image')能讓 x 和 y 軸的單位一致。

最後,有時可能需要將輪廓圖和圖像結合起來。例如,下例中我們使用了半透明的背景圖像(通過alpha參數設置透明度),然後在背景圖層之上繪製了輪廓圖,並帶有每個輪廓的數值標籤(使用plt.clabel()函數繪製標籤):

contours = plt.contour(X, Y, Z, 3, colors='black')
plt.clabel(contours, inline=True, fontsize=8)

plt.imshow(Z, extent=[0, 5, 0, 5], origin='lower',
           cmap='RdGy', alpha=0.5)
plt.colorbar();

通過組合使用plt.contour、plt.contourf和plt.imshow這三個函數,基本可以滿足我們繪製所有這種在二維圖標上的三維數據的需求。需要了解更多函數的參數信息,參考它們的文檔字符串。如果你對於使用三維圖表展示這種數據感興趣,參見[在 matplotlib 中創建三維圖表]。

 

5.直方圖,分桶和密度

一個簡單的直方圖可以是我們開始理解數據集的第一步。前面我們看到了 Matplotlib 的直方圖函數,我們可以用一行代碼繪製基礎的直方圖,當然首先需要將需要用的包導入 notebook:

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

data = np.random.randn(1000)

plt.hist(data);

hist()函數有很多的參數可以用來調整運算和展示;下面又一個更加個性化的直方圖展示:

譯者注:normed 參數已經過時,此處對代碼進行了相應修改,使用了替代的 density 參數。

plt.hist(data, bins=30, density=True, alpha=0.5,
         histtype='stepfilled', color='steelblue',
         edgecolor='none');

plt.hist文檔中有更多關於個性化參數的信息。作者發現聯合使用histtype='stepfilled'和alpha參數設置透明度在對不同分佈的數據集進行比較展示時很有用:

x1 = np.random.normal(0, 0.8, 1000)
x2 = np.random.normal(-2, 1, 1000)
x3 = np.random.normal(3, 2, 1000)

kwargs = dict(histtype='stepfilled', alpha=0.3, density=True, bins=40)

plt.hist(x1, **kwargs)
plt.hist(x2, **kwargs)
plt.hist(x3, **kwargs);

如果你只是需要計算直方圖的數值(即每個桶的數據點數量)而不是展示圖像,np.histogram()函數可以完成這個目標:

counts, bin_edges = np.histogram(data, bins=5)
print(counts)

[ 49 273 471 183  24]

 

二維直方圖和分桶

正如前面我們可以在一維上使用數值對應的直線劃分桶一樣,我們也可以在二維上使用數據對應的點來劃分桶。本節我們介紹幾種實現的方法。首先定義數據集,從多元高斯分佈中獲得x和y數組:

mean = [0, 0]
cov = [[1, 1], [1, 2]]
x, y = np.random.multivariate_normal(mean, cov, 10000).T

 

plt.hist2d:二維直方圖

繪製二維直方圖最直接的方法是使用 Matplotlib 的plt.hist2d函數:

plt.hist2d(x, y, bins=30, cmap='Blues')
cb = plt.colorbar()
cb.set_label('counts in bin')

類似plt.hist,plt.hist2d有許多額外的參數來調整分桶計算和圖表展示,可以通過文檔瞭解更多信息。而且,plt.hist有np.histogram,plt.hist2d也有其對應的函數np.histogram2d。如下例:

counts, xedges, yedges = np.histogram2d(x, y, bins=30)

如果要獲得更高維度的分桶結果,參見np.histogramd函數文檔。

 

plt.hexbin:六角形分桶

剛纔的二維分桶是沿着座標軸將每個桶分爲正方形。另一個很自然的分桶形狀就是正六邊形。對於這個需求,Matplotlib 提供了plt.hexbin函數,它也是在二維平面上分桶展示,不過每個桶(即圖表上的每個數據格)將會是六邊形:

plt.hexbin(x, y, gridsize=30, cmap='Blues')
cb = plt.colorbar(label='count in bin')

plt.hexbin有許多有趣的參數,包括能對每個點設置權重和將每個桶的輸出數據結果改爲任意的 NumPy 聚合結果(帶權重的平均值,帶權重的標準差等)。

 

核密度估計

另外一個常用來統計多維數據密度的工具是核密度估計(KDE)。目前我們只需要知道 KDE 被認爲是一種可以用來填補數據的空隙並補充上平滑變化數據的方法就足夠了。快速和簡單的 KDE 算法已經在scipy.stats模塊中有了成熟的實現。下面我們就一個簡單的例子來說明如何使用 KDE 和繪製相應的二維直方圖:

from scipy.stats import gaussian_kde

# 產生和處理數據,初始化KDE
data = np.vstack([x, y])
kde = gaussian_kde(data)

# 在通用的網格中計算得到Z的值
xgrid = np.linspace(-3.5, 3.5, 40)
ygrid = np.linspace(-6, 6, 40)
Xgrid, Ygrid = np.meshgrid(xgrid, ygrid)
Z = kde.evaluate(np.vstack([Xgrid.ravel(), Ygrid.ravel()]))

# 將圖表繪製成一張圖像
plt.imshow(Z.reshape(Xgrid.shape),
           origin='lower', aspect='auto',
           extent=[-3.5, 3.5, -6, 6],
           cmap='Blues')
cb = plt.colorbar()
cb.set_label("density")

KDE 有着光滑的長度,可以在細節和光滑度中有效的進行調節(一個例子是方差偏差權衡)。這方面有大量的文獻介紹:高斯核密度估計gaussian_kde使用了經驗法則來尋找輸入數據附近的優化光滑長度值。

其他的 KDE 實現也可以在 SciPy 中找到,每一種都有它的優點和缺點;參見sklearn.neighbors.KernelDensity和statsmodels.nonparametric.kernel_density.KDEMultivariate。要繪製基於 KDE 進行可視化的圖表,Matplotlib 寫出的代碼會比較冗長。

 

6.自定義圖標圖例

圖例可以爲可視化賦予實際含義,爲不同的圖標元素附上明確說明。我們前面看到了一些簡單的圖例創建例子;本小節中我們來介紹一下在 Matplotlib 中自定義圖例的位置和進行美化的方法。

可以使用plt.legend()函數來創建最簡單的圖例,這個函數能自動創建任何帶有標籤屬性的圖表元素的圖例:

import matplotlib.pyplot as plt
plt.style.use('classic')

%matplotlib inline
import numpy as np

x = np.linspace(0, 10, 1000)
fig, ax = plt.subplots()
ax.plot(x, np.sin(x), '-b', label='Sine')
ax.plot(x, np.cos(x), '--r', label='Cosine')
ax.axis('equal')
leg = ax.legend();

但除此之外還有很多能自定義圖例的方法。例如,我們可以指定圖例位置並且去除邊框:

ax.legend(loc='upper left', frameon=False)
fig

我們可以使用ncol屬性設置圖例中每行的列數:

ax.legend(frameon=False, loc='lower center', ncol=2)
fig

還可以使用圓角方框(fancybox)或者增加陰影,設置方框的透明度(alpha 值)或修改文字的邊距:

ax.legend(fancybox=True, framealpha=1, shadow=True, borderpad=1)
fig

 

要獲取更多 legend 函數的可用選項信息,請參考plt.legend的文檔字符串。

 

選擇設置圖例的元素

正如我們前面例子所示,繪製的圖例默認包括所有帶標籤的元素。如果這不是想要的效果,我們可以調整哪些元素和標籤會出現在圖例當中,這可以通過設置 plot 函數或方法返回的對象實現。plt.plot函數能夠同時產生多條折線,然後將這些線條的實例列表返回。將其中的部分實例傳遞到plt.legend()函數就能設置哪些線條會出現在圖例中,再通過一個標籤的列表指定圖例的名稱:

y = np.sin(x[:, np.newaxis] + np.pi * np.arange(0, 2, 0.5))
lines = plt.plot(x, y)

# lines是一個線條實例的列表
plt.legend(lines[:2], ['first', 'second']);

作者更加傾向於使用第一種方式,因爲更加清晰。通過將標籤應用在圖表元素上,然後繪製到圖例中:

plt.plot(x, y[:, 0], label='first')
plt.plot(x, y[:, 1], label='second')
plt.plot(x, y[:, 2:])
plt.legend(framealpha=1, frameon=True);

請注意默認情況下,legend 會忽略所有不帶標籤的元素。

 

散點大小的圖例

某些情況下默認的圖例不足以滿足特定的可視化需求。例如,你在使用散點的大小來標記數據的某個特徵,然後希望創建一個相應的圖例。下面的例子是加州城市人口的散點圖,我們使用散點的大小表現該城市的面積,散點的顏色來表現城市的人口數量(自然對數值)。我們希望使用一個圖例來指明散點尺寸的比例,同時用一個顏色條來說明人口數量,我們可以通過自定義繪製一些標籤數據來實現尺寸圖例:

譯者注:新版 Matplotlib 已經取消 aspect 參數,此處改爲使用新的'scaled'參數調用 axis 函數。

import pandas as pd
cities = pd.read_csv(r'D:\python\Github學習材料\Python數據科學手冊\data\california_cities.csv')

# 提取我們感興趣的數據
lat, lon = cities['latd'], cities['longd']
population, area = cities['population_total'], cities['area_total_km2']

# 繪製散點圖,使用尺寸代表面積,顏色代表人口,不帶標籤
plt.scatter(lon, lat, label=None,
            c=np.log10(population), cmap='viridis',
            s=area, linewidth=0, alpha=0.5)
plt.axis('scaled')
plt.xlabel('longitude')
plt.ylabel('latitude')
plt.colorbar(label='log$_{10}$(population)')
plt.clim(3, 7)

# 下面我們創建圖例:
# 使用空列表繪製圖例中的散點,使用不同面積和標籤,帶透明度
for area in [100, 300, 500]:
    plt.scatter([], [], c='k', alpha=0.3, s=area,
                label=str(area) + ' km$^2$')
plt.legend(scatterpoints=1, frameon=False, labelspacing=1, title='City Area')

plt.title('California Cities: Area and Population');

之前的圖例都關聯着圖表上的一些對象,因此如果我們需要展示圖例的話我們首先需要繪製圖表元素。在上例中,我們需要的圖例對象(灰色圓圈)不在圖表上,因此我們採用繪製空列表的方式將它們仿造在圖表上(實際上圖上沒有點),但是還是需要注意,只有那些帶標籤的元素纔會出現在圖例中。

通過繪製空列表,我們創建了三個帶標籤的對象,然後就可以出現在圖例當中,這個圖例就能表示出有關城市面積的相關信息。這個策略在很多複雜可視化圖表構建過程中都被用到。

最後我們注意到這個圖表實際上是一個地理位置圖表,如果我們能在上面繪製州界線或其他地圖相關的元素的話,會更加清晰。Matplotlib 提供了一個 Basemap 額外工具集來實現這個目標。

 

多重圖例

有時候我們可能需要在同一個圖表維度中設計多個圖例。不幸的是,Matplotlib 並沒有提供很簡單的方式實現:通過標準的legend接口,只能在整張圖表上創建一個圖例。如果你試圖使用plt.legend()或ax.legend()創建第二個圖例,那麼第二條語句創建的圖例會覆蓋第一條語句創建的。我們只能通過從底層開始來創建一個新的圖例 artist 這種方法來解決這個問題,然後使用ax.add_artist()的底層方法手動將第二個作者加到圖表上:

fig, ax = plt.subplots()

lines = []
styles = ['-', '--', '-.', ':']
x = np.linspace(0, 10, 1000)

for i in range(4):
    lines += ax.plot(x, np.sin(x - i * np.pi / 2),
                     styles[i], color='black')
ax.axis('equal')

# 指定第一個圖例的線條和標籤
ax.legend(lines[:2], ['line A', 'line B'],
          loc='upper right', frameon=False)

# 手動創建第二個圖例,並將作者添加到圖表中
from matplotlib.legend import Legend
leg = Legend(ax, lines[2:], ['line C', 'line D'],
             loc='lower right', frameon=False)
ax.add_artist(leg);

上例展示了用來組成任何 Matplotlib 圖表的底層 artist 對象的簡單說明。如果你去查看ax.legend()的源代碼(你可以通過 IPython 的ax.legend幫助工具做到),你可以看到這個方法包含了用來構建合適Legend的 artist 對象的邏輯,構建的對象被保存在legend_屬性當中,當繪製時被添加到圖表上進行展示。

 

7.個性化顏色條

圖例可以將離散的點標示爲離散的標籤。對於建立在不同顏色之上的連續的值(點線面)來說,標註了的顏色條是非常方便的工具。Matplotlib 的顏色條是獨立於圖表之外的一個類似於比色卡的圖形,用來展示圖表中不同顏色的數值含義。因爲本書是使用黑白打印的,本節內容中的所有帶色彩的圖都可以在()中找到。我們還是首先導入本節需要的包和模塊:

import matplotlib.pyplot as plt
plt.style.use('classic')

%matplotlib inline
import numpy as np

通過plt.colorbar函數可以創建最簡單的顏色條,在本節中我們會多次看到:

x = np.linspace(0, 10, 1000)
I = np.sin(x) * np.cos(x[:, np.newaxis])

plt.imshow(I)
plt.colorbar();

我們下面來討論如何個性化顏色條以及在不同的場合高效的使用它們。

 

自定義顏色條

顏色條可以通過cmap參數指定使用的色譜系統(或叫色圖):

plt.imshow(I, cmap='gray');

所有可用的色圖都可以在plt.cm模塊中找到;在 IPython 中使用 Tab 自動補全功能能列出所有的色圖列表:

plt.cm.<TAB>

但是知道在哪裏選擇色圖只是第一步,更重要的是在各種選項中選出合適的色圖。這個選擇比你預料的要微妙的多。

 

選擇色圖

在可視化方案中選擇顏色完整的介紹說明超出了本書的範圍,如果你對這個課題和相關內容有興趣,可以參考文章["繪製更漂亮圖表的 10 個簡單規則"]()。Matplotlib 的在線文檔也有一章關於色圖選擇的有趣討論[5]。

通常來說,你應該注意以下三種不同類型的色圖:

  • 序列色圖:這類型的色譜只包括一個連續序列的色系(例如binary或viridis)。
  • 分化色圖:這類型的色譜包括兩種獨立的色系,這兩種顏色有着非常大的對比度(例如RdBu或PuOr)。
  • 定性色圖:這類型的色圖混合了非特定連續序列的顏色(例如rainbow或jet)。

jet色圖,在 Matplotlib 2.0 版本之前都是默認的色圖,是定性色圖的一個例子。jet作爲默認色圖的位置其實有點尷尬,因爲定性圖通常都不是對定量數據進行展示的好選擇。原因是定性圖通常都不能在範圍增加時提供亮度的均勻增長。

我們可以通過將jet顏色條轉換爲黑白來看到這點:

from matplotlib.colors import LinearSegmentedColormap

def grayscale_cmap(cmap):
    """返回給定色圖的灰度版本"""
    cmap = plt.cm.get_cmap(cmap) # 使用名稱獲取色圖對象
    colors = cmap(np.arange(cmap.N)) # 將色圖對象轉爲RGBA矩陣,形狀爲N×4

    # 將RGBA顏色轉換爲灰度
    # 參考 http://alienryderflex.com/hsp.html
    RGB_weight = [0.299, 0.587, 0.114] # RGB三色的權重值
    luminance = np.sqrt(np.dot(colors[:, :3] ** 2, RGB_weight)) # RGB平方值和權重的點積開平方根
    colors[:, :3] = luminance[:, np.newaxis] # 得到灰度值矩陣
    # 返回相應的灰度值色圖
    return LinearSegmentedColormap.from_list(cmap.name + "_gray", colors, cmap.N)


def view_colormap(cmap):
    """將色圖對應的灰度版本繪製出來"""
    cmap = plt.cm.get_cmap(cmap)
    colors = cmap(np.arange(cmap.N))

    cmap = grayscale_cmap(cmap)
    grayscale = cmap(np.arange(cmap.N))

    fig, ax = plt.subplots(2, figsize=(6, 2),
                           subplot_kw=dict(xticks=[], yticks=[]))
    ax[0].imshow([colors], extent=[0, 10, 0, 1])
    ax[1].imshow([grayscale], extent=[0, 10, 0, 1])

view_colormap('jet')

注意一下上面的灰度圖中亮條紋的位置。即使在上述彩色圖中,也出現了這種不規則的亮條紋,這會導致眼睛被區域中亮條紋所吸引,這很可能造成閱讀者被不重要的數據集部分干擾了。更好的選擇是使用類似viridis這樣的色圖(Matplotlib 2.0 後默認色圖),它們被設計爲有着均勻的亮度變化。因此它們無論是在彩色圖中還是在灰度圖中都有着同樣的亮度變化:

view_colormap('viridis')

如果你更喜歡彩虹方案,另一個好的選擇是使用cubehelix色圖:

view_colormap('cubehelix')

對於其他的情況,例如某種正負分佈的數據集,雙色顏色條如RdBu(Red-Blue)會很常用。然而正如你從下面例子看到的,如果將雙色顏色條轉化爲灰度的話,正負或兩級的信息就會丟失:

view_colormap('RdBu')

後面我們會看到更多使用這些色圖的例子。

Matplotlib 中有大量可用的色圖;要看到它們的列表,你可以使用 IPython 來探索plt.cm模塊。要在 Python 中更加正規的使用顏色,你可以查看 Seaborn 庫的工具和文檔。

 

顏色限制和擴展

Matplotlib 允許你對顏色條進行大量的自定義。顏色條本身就是一個plt.Axes對象,因此所有軸和刻度定製的技巧都可以應用在上面。顏色條也有着一些有趣的自定義行爲:例如,我們可以縮小顏色的範圍並且通過設置extend參數將超出範圍之外的數值展示爲頂部和底部的三角箭頭形狀。這對於展示一些受到噪聲干擾的數據時非常方便:

# 在I數組中人爲生成不超過1%的噪聲
speckles = (np.random.random(I.shape) < 0.01)
I[speckles] = np.random.normal(0, 3, np.count_nonzero(speckles))

plt.figure(figsize=(10, 3.5))
# 不考慮去除噪聲時的顏色分佈
plt.subplot(1, 2, 1)
plt.imshow(I, cmap='RdBu')
plt.colorbar()
# 設置去除噪聲時的顏色分佈
plt.subplot(1, 2, 2)
plt.imshow(I, cmap='RdBu')
plt.colorbar(extend='both')
plt.clim(-1, 1);

注意到在左邊的圖表中,默認的顏色閾值是包括了噪聲的,因此整體的條紋形狀都被噪聲數據沖刷淡化了。而右邊的圖表,我們手動設置了顏色的閾值,並在繪製顏色條是加上了extend參數來表示超出閾值的數據。對於我們的數據來說,右圖比左圖要好的多。

 

離散顏色條

色圖默認是連續的,但是在某些情況下你可能需要展示離散值。最簡單的方法是使用plt.cm.get_cmap()函數,在傳遞某個色圖名稱的同時,還額外傳遞一個顏色分桶的數量值參數給該函數:

plt.imshow(I, cmap=plt.cm.get_cmap('Blues', 6))
plt.colorbar()
plt.clim(-1, 1);

離散色圖的使用方式和其他色圖沒有任何區別。

 

例子:手寫數字

最後我們來看一個很有實用價值的例子,讓我們實現對一些手寫數字圖像數據的可視化分析。這個數據包含在 Sciki-Learn 中,以供包含有將近 2,000 張 大小的不同筆跡的手寫數字縮略圖。

首先,我們下載這個數據集,然後使用plt.imshow()將其中部分數據展示出來:

# 讀取數字0-5的手寫圖像,然後使用Matplotlib展示頭64張縮略圖
from sklearn.datasets import load_digits
digits = load_digits(n_class=6)

fig, ax = plt.subplots(8, 8, figsize=(6, 6))
for i, axi in enumerate(ax.flat):
    axi.imshow(digits.images[i], cmap='binary')
    axi.set(xticks=[], yticks=[])

因爲每個數字都是使用 64 個像素點渲染出來的,我們可以認爲每個數字是一個 64 維空間中的點:每個維度代表這其中一個像素的灰度值。但是要在圖表中將這麼高維度空間的聯繫可視化出來是非常困難的。有一種做法是使用降維技術,比方說使用流形學習來減少數據的維度然而不會丟失數據中有效的信息。

我們來看一下將這些手寫數字圖像數據映射到二維流形學習當中:

# 使用Isomap將手寫數字圖像映射到二維流形學習中
from sklearn.manifold import Isomap
iso = Isomap(n_components=2)
projection = iso.fit_transform(digits.data)

我們使用離散顏色條來展示結果,設置ticks和clim來進一步美化結果的顏色條:

# 繪製圖表結果
plt.scatter(projection[:, 0], projection[:, 1], lw=0.1,
            c=digits.target, cmap=plt.cm.get_cmap('cubehelix', 6))
plt.colorbar(ticks=range(6), label='digit value')
plt.clim(-0.5, 5.5)

我們從流形學習中的映射中可以觀察到一些有趣現象:例如,圖表中 5 和 3 有一些重疊的部分,這表示一些手寫體中 5 和 3 是比較難以辨別的,因此對於自動識別算法來說這是比較容易混淆的部分。而 0 和 1,它們在圖表中距離很遠,這表示兩者比較容易辨別,不太可能造成混淆。這個圖表分析與我們的直覺一致,因爲 5 和 3 顯然比 0 和 1 看起來更加接近。

 

8.多個子圖表

在一些情況中,如果能將不同的數據圖表並列展示,對於我們進行數據分析和比較會很有幫助。Matplotlib 提供了子圖表的概念來實現這一點:單個圖表中可以包括一組小的 axes 用來展示多個子圖表。這些子圖表可以是插圖,網格狀分佈或其他更復雜的佈局。在本節中我們會介紹 Matplotlib 中用來構建子圖表的四個函數。

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')
import numpy as np

plt.axes:手動構建子圖表

構建 axes 作爲子圖表的最基礎方法就是使用plt.axes函數。正如我們前面已經看到,默認情況下,這個函數夠創建一個標準的 axes 對象填滿整個圖表區域。plt.axes函數也可以接收一個可選的列表參數用來指定在 axes 在整個圖表中的座標點位置。列表中有四個數值分別爲[left, bottom, width, height](取值都是 0-1),代表着子圖表的左邊、底部、寬度、高度在整個圖表中左邊、底部、寬度、高度所佔的比例值。

例如,我們可以在距離左邊和底部 65%的位置,以插圖的形式放置一個寬度和高度都是 20%子圖表,上述數值應該爲[0.65, 0.65, 0.2, 0.2]:

ax1 = plt.axes()  # 標準圖表
ax2 = plt.axes([0.65, 0.65, 0.2, 0.2]) #子圖表

與上述等價的面向對象接口的語法是fig.add_axes()。我們使用這個方法來創建兩個垂直堆疊的子圖表:

fig = plt.figure() # 獲得figure對象
ax1 = fig.add_axes([0.1, 0.5, 0.8, 0.4],
                   xticklabels=[], ylim=(-1.2, 1.2)) # 左邊10% 底部50% 寬80% 高40%
ax2 = fig.add_axes([0.1, 0.1, 0.8, 0.4],
                   ylim=(-1.2, 1.2)) # 左邊10% 底部10% 寬80% 高40%

x = np.linspace(0, 10)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x));

這樣我們就有兩個子圖表(上面的子圖表沒有 x 軸刻度),這兩個子圖表正好吻合:上面圖表的底部是整個圖表高度 50%位置,而下面圖表的頂部也是整個圖表的 50%位置(0.1+0.4)。

 

 

plt.subplot:簡單網格的子圖表

將子圖表的行與列對齊是一個很常見的需求,因此 Matplotlib 提供了一些簡單的函數來實現它們。這些函數當中最底層的是plt.subplot(),它會在網格中創建一個子圖表。函數接受三個整數參數,網格行數,網格列數以及該網格子圖表的序號(從左上角向右下角遞增):

for i in range(1, 7):
    plt.subplot(2, 3, i)
    plt.text(0.5, 0.5, str((2, 3, i)),
             fontsize=18, ha='center')

plt.subplots_adjust函數用來調整這些子圖表之間的距離。下面的代碼使用了與plt.subplot()等價的面向對象接口方法fig.add_subplot():

fig = plt.figure()
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(1, 7):
    ax = fig.add_subplot(2, 3, i)
    ax.text(0.5, 0.5, str((2, 3, i)),
           fontsize=18, ha='center')

上例中我們指定了plt.subplots_adjust函數的hspace和wspace參數,它們代表這沿着高度和寬度方向子圖表之間的距離,單位是子圖表的大小(在本例中,距離是子圖表寬度和高度的 40%)。

 

plt.subplots:一句代碼設置所有網格子圖表

上面的方法在我們需要創建大量的子圖表網格時會變得非常冗長乏味,特別是如果我們需要將內部圖表 x 軸和 y 軸標籤隱藏的情況下。因此,plt.subplots在這種情況下是一個合適的工具(注意末尾有個 s)。這個函數會一次性創建所有的網格子圖表,而不是單個網格,並將它們存儲在一個 NumPy 數組中返回。參數是行數和列數,還有兩個可選的關鍵字參數sharex和sharey,可以讓你指定不同子圖表之間的關聯。

下面我們來創建一個 網格的子圖表,其中每一行的子圖表共享它們的 y 軸,而每一列的子圖表共享它們的 x 軸:

fig, ax = plt.subplots(2, 3, sharex='col', sharey='row')

注意上面我們設置了sharex和sharey之後,內部子圖表的 x 軸和 y 軸的標籤就自動被去掉了。返回值中 ax 是一個 NumPy 數組,裏面含有每一個子圖表的實例,你可以使用 NumPy 索引的語法很簡單的獲得它們:

# axes是一個2×3的數組,可以通過[row, col]進行索引訪問
for i in range(2):
    for j in range(3):
        ax[i, j].text(0.5, 0.5, str((i, j)),
                      fontsize=18, ha='center')
fig

並且相對於plt.subplot,plt.subplots()更復合 Python 從 0 開始進行索引的習慣。

 

 

plt.GridSpec:更復雜的排列

當你需要子圖表在網格中佔據多行或多列時,plt.GridSpec()正是你所需要的。plt.GridSpec()對象並不自己創建圖表;它只是一個可以被傳遞給plt.subplot()的參數。例如,一個兩行三列並帶有指定的寬度高度間隔的 gridspec 可以如下創建:

grid = plt.GridSpec(2, 3, wspace=0.4, hspace=0.3)

使用這個對象我們可以指定子圖表的位置和佔據的網格,僅需要使用熟悉的 Python 切片語法即可:

plt.subplot(grid[0, 0])
plt.subplot(grid[0, 1:])
plt.subplot(grid[1, :2])
plt.subplot(grid[1, 2]);

這種靈活的網格對齊控制方式有着廣泛的應用。作者經常在需要創建多個直方圖的聯合圖表中使用這種方法,如下例:

# 構建二維正態分佈數據
mean = [0, 0]
cov = [[1, 1], [1, 2]]
x, y = np.random.multivariate_normal(mean, cov, 3000).T

# 使用GridSpec創建網格並加入子圖表
fig = plt.figure(figsize=(6, 6))
grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)
main_ax = fig.add_subplot(grid[:-1, 1:])
y_hist = fig.add_subplot(grid[:-1, 0], xticklabels=[], sharey=main_ax)
x_hist = fig.add_subplot(grid[-1, 1:], yticklabels=[], sharex=main_ax)

# 在主圖表中繪製散點圖
main_ax.plot(x, y, 'ok', markersize=3, alpha=0.2)

# 分別在x軸和y軸方向繪製直方圖
x_hist.hist(x, 40, histtype='stepfilled',
            orientation='vertical', color='gray')
x_hist.invert_yaxis() # x軸方向(右下)直方圖倒轉y軸方向

y_hist.hist(y, 40, histtype='stepfilled',
            orientation='horizontal', color='gray')
y_hist.invert_xaxis() # y軸方向(左上)直方圖倒轉x軸方向

這種沿着數據各自方向分佈並繪製相應圖表的需求是很通用的,因此在 Seaborn 包中它們有專門的 API 來實現。

 

9.文本和標註

創建一個優秀的可視化圖表的關鍵在於引導讀者,讓他們能理解圖表所講述的故事。在一些情況下,這個故事可以通過純圖像的方式表達,不需要額外添加文字,但是在另外一些情況中,圖表需要文字的提示和標籤才能將故事講好。也許標註最基本的類型就是圖表的標籤和標題,但是其中的選項參數卻有很多。讓我們在本節中使用一些數據來創建可視化圖表並標註這些圖表來表達這些有趣的信息。首先還是需要將要用到的模塊和包導入 notebook:

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.style.use('seaborn-whitegrid')
import numpy as np
import pandas as pd

 

例子:節假日對美國出生率的影響

本例中的數據可以在  下載。

我們先按照前面的方式進行同樣的數據清洗程序,然後以圖表展示這個結果:

births = pd.read_csv(r'D:\python\Github學習材料\Python數據科學手冊\data\births.csv')

quartiles = np.percentile(births['births'], [25, 50, 75])
mu, sig = quartiles[1], 0.74 * (quartiles[2] - quartiles[0])
births = births.query('(births > @mu - 5 * @sig) & (births < @mu + 5 * @sig)')

births['day'] = births['day'].astype(int)

births.index = pd.to_datetime(10000 * births.year +
                              100 * births.month +
                              births.day, format='%Y%m%d')
births_by_date = births.pivot_table('births',
                                    [births.index.month, births.index.day])
births_by_date.index = [pd.datetime(2012, month, day)
                        for (month, day) in births_by_date.index]

C:\Users\gdc\Anaconda3\lib\site-packages\ipykernel_launcher.py:15: FutureWarning: The pandas.datetime class is deprecated and will be removed from pandas in a future version. Import from datetime module instead.
  from ipykernel import kernelapp as app

fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax);

當我們繪製了這樣的圖表來表達數據時,如果我們能對一些圖表的特性作出標註來,這對吸引讀者的注意力通常是非常有幫助的。這可以通過調用plt.text或ax.text函數來實現,它們可以在某個特定的 x,y 軸位置輸出一段文字:

fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax)

# 在折線的特殊位置標註文字
style = dict(size=10, color='gray')

ax.text('2012-1-1', 3950, "New Year's Day", **style)
ax.text('2012-7-4', 4250, "Independence Day", ha='center', **style)
ax.text('2012-9-4', 4850, "Labor Day", ha='center', **style)
ax.text('2012-10-31', 4600, "Halloween", ha='right', **style)
ax.text('2012-11-25', 4450, "Thanksgiving", ha='center', **style)
ax.text('2012-12-25', 3850, "Christmas ", ha='right', **style)

# 設置標題和y軸標籤
ax.set(title='USA births by day of year (1969-1988)',
       ylabel='average daily births')

# 設置x軸標籤月份居中
ax.xaxis.set_major_locator(mpl.dates.MonthLocator())
ax.xaxis.set_minor_locator(mpl.dates.MonthLocator(bymonthday=15))
ax.xaxis.set_major_formatter(plt.NullFormatter())
ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%h'));

ax.text方法接收 x 位置、y 位置、一個字符串和額外可選的關鍵字參數可以用來設置顏色、大小、樣式、對齊等文本格式。上面我們使用了ha='right'和ha='center',這裏的ha是*hirizonal alignment(水平對齊)*的縮寫。要查閱更多的可用參數,請查看plt.text()和mpl.text.Text()的文檔字符串內容。

 

轉換和文本位置

在剛纔的例子中,我們將文字標註根據數據位置進行了定位。有些時候我們需要將文字標註獨立於數據位置而根據圖表位置進行定位。Matplotlib 通過轉換完成這項工作。

任何的圖形顯示框架都需要在座標系統之間進行轉換的機制。例如,一個數據點位於 被轉換爲圖表中的某個位置,進而轉換爲屏幕上顯示的像素。這樣的座標轉換在數學上都相對來說比較直接,而且 Matplotlib 提供了一系列的工具實現了轉換(這些工具可以在matplotlib.transforms模塊中找到)。

一般來說,用戶很少需要關注這些轉換的細節,但是當考慮將文本在圖表上展示時,這些知識卻比較有用。在這種情況中,下面三種定義好的轉換是比較有用的:

  • ax.transData:與數據座標相關的轉換
  • ax.tranAxes:與 Axes 尺寸相關的轉換(單位是 axes 的寬和高)
  • ax.tranFigure:與 figure 尺寸相關的轉換(單位是 figure 的寬和高)

下面我們來看看使用這些轉換將文字寫在圖表中不同位置的例子:

fig, ax = plt.subplots(facecolor='lightgray')
ax.axis([0, 10, 0, 10])

# transform=ax.transData是默認的,這裏寫出來是爲了明確對比
ax.text(1, 5, ". Data: (1, 5)", transform=ax.transData)
ax.text(0.5, 0.1, ". Axes: (0.5, 0.1)", transform=ax.transAxes)
ax.text(0.2, 0.2, ". Figure: (0.2, 0.2)", transform=fig.transFigure);

注意默認情況下,文字是在指定座標位置靠左對齊的:這裏每個字符串開始的"."的位置就是每種轉換的座標位置。

transData座標給定的是通常使用的 x 和 y 軸座標位置。transAxes座標給定的是從 axes 左下角開始算起(白色區域)的座標位置,使用的是寬度和長度的佔比。transFigure座標類似,給定的是從 figure 左下角開始算起(灰色區域)的座標位置,使用的也是寬度和長度的佔比。

因此如果我們改變了軸的最大長度,只有transData座標會收到影響,其他兩個還是保持在相同位置:

ax.set_xlim(0, 2)
ax.set_ylim(-6, 6)
fig

這個變化可以通過動態改變軸的最大長度看的更加清楚:如果你在 notebook 執行這段代碼,你可以將%matplotlib inline改爲%matplotlib notebook,然後使用圖表的菜單來交互式的改變圖表。

 

箭頭和標註

除了刻度標籤和文字標籤,另一種常用的標註是箭頭。

在 Matplotlib 中繪製箭頭通常比你想象的難得多。雖然有plt.arrow()函數,作者不建議使用它:這個函數繪製的箭頭是一個 SVG 對象,因此在圖表使用不同的比例的情況會產生問題,結果通常不能讓用戶滿意。因此,作者建議使用plt.annotate()函數。這個函數會繪製一些文字以及一個箭頭,並且箭頭可以非常靈活的進行配置。

下面我們提供一些參數來使用annotate函數:

%matplotlib inline

fig, ax = plt.subplots()

x = np.linspace(0, 20, 1000)
ax.plot(x, np.cos(x))
ax.axis('equal')

ax.annotate('local maximum', xy=(6.28, 1), xytext=(10, 4),
            arrowprops=dict(facecolor='black', shrink=0.05))

ax.annotate('local minimum', xy=(5 * np.pi, -1), xytext=(2, -6),
            arrowprops=dict(arrowstyle="->",
                            connectionstyle="angle3,angleA=0,angleB=-90"));

箭頭的樣式是使用箭頭屬性字典值進行控制的,裏面有很多可用的參數。這些參數在 Matplotlib 的在線文檔中已經有了很詳細的說明,因此在這裏就不將這部分內容重複介紹一遍了。我們在前面出生率圖上再使用一些參數進行更多的說明:

fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax)

# 爲圖表添加標註
ax.annotate("New Year's Day", xy=('2012-1-1', 4100),  xycoords='data',
            xytext=(50, -30), textcoords='offset points',
            arrowprops=dict(arrowstyle="->",
                            connectionstyle="arc3,rad=-0.2"))

ax.annotate("Independence Day", xy=('2012-7-4', 4250),  xycoords='data',
            bbox=dict(boxstyle="round", fc="none", ec="gray"),
            xytext=(10, -40), textcoords='offset points', ha='center',
            arrowprops=dict(arrowstyle="->"))

ax.annotate('Labor Day', xy=('2012-9-4', 4850), xycoords='data', ha='center',
            xytext=(0, -20), textcoords='offset points')
ax.annotate('', xy=('2012-9-1', 4850), xytext=('2012-9-7', 4850),
            xycoords='data', textcoords='data',
            arrowprops={'arrowstyle': '|-|,widthA=0.2,widthB=0.2', })

ax.annotate('Halloween', xy=('2012-10-31', 4600),  xycoords='data',
            xytext=(-80, -40), textcoords='offset points',
            arrowprops=dict(arrowstyle="fancy",
                            fc="0.6", ec="none",
                            connectionstyle="angle3,angleA=0,angleB=-90"))

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章