1. 程式人生 > >《利用Python進行資料分析·第2版》第9章 繪圖和視覺化

《利用Python進行資料分析·第2版》第9章 繪圖和視覺化

資訊視覺化(也叫繪圖)是資料分析中最重要的工作之一。它可能是探索過程的一部分,例如,幫助我們找出異常值、必要的資料轉換、得出有關模型的 idea 等。另外,做一個可互動的資料視覺化也許是工作的最終目標。Python 有許多庫進行靜態或動態的資料視覺化,但我這裡重要關注於 matplotlib(http://matplotlib.org/)和基於它的庫。

matplotlib 是一個用於創建出版質量圖表的桌面繪圖包(主要是 2D 方面)。該專案是由 John Hunter 於 2002 年啟動的,其目的是為 Python 構建一個 MATLAB 式的繪圖介面。matplotlib 和 IPython 社群進行合作,簡化了從 IPython shell(包括現在的 Jupyter notebook)進行互動式繪圖。matplotlib 支援各種作業系統上許多不同的 GUI 後端,而且還能將圖片匯出為各種常見的向量(vector)和光柵(raster)圖:PDF、SVG、JPG、PNG、BMP、GIF 等。除了幾張,本書中的大部分圖都是用它生成的。

隨著時間的發展,matplotlib 衍生出了多個數據視覺化的工具集,它們使用 matplotlib 作為底層。其中之一是 seaborn(http://seaborn.pydata.org/),本章後面會學習它。

學習本章程式碼案例的最簡單方法是在 Jupyter notebook 進行互動式繪圖。在 Jupyter notebook 中執行下面的語句:

%matplotlib notebook

9.1 matplotlib API 入門

matplotlib 的通常引入約定是:

In [11]: import matplotlib.pyplot as plt

在 Jupyter 中執行 %matplotlib notebook(或在 IPython 中執行 %matplotlib),就可以建立一個簡單的圖形。如果一切設定正確,會看到圖 9-1:

In [12]: import numpy as np

In [13]: data = np.arange(10)

In [14]: data
Out[14]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [15]: plt.plot(data)

雖然 seaborn 這樣的庫和 pandas 的內建繪圖函式能夠處理許多普通的繪圖任務,但如果需要自定義一些高階功能的話就必須學習 matplotlib API。

筆記:雖然本書沒有詳細地討論 matplotlib 的各種功能,但足以將你引入門。matplotlib 的示例庫和文件是學習高階特性的最好資源。

Figure 和 Subplot

matplotlib 的影象都位於 Figure 物件中。你可以用 plt.figure 建立一個新的 Figure:

In [16]: fig = plt.figure()

如果用的是 IPython,這時會彈出一個空視窗,但在 Jupyter 中,必須再輸入更多命令才能看到。plt.figure 有一些選項,特別是 figsize,它用於確保當圖片儲存到磁碟時具有一定的大小和縱橫比。

不能通過空 Figure 繪圖。必須用 add_subplot 建立一個或多個 subplot 才行:

In [17]: ax1 = fig.add_subplot(2, 2, 1)

這條程式碼的意思是:影象應該是 2×2 的(即最多 4 張圖),且當前選中的是 4 個 subplot 中的第一個(編號從 1 開始)。如果再把後面兩個 subplot 也創建出來,最終得到的影象如圖 9-2 所示:

In [18]: ax2 = fig.add_subplot(2, 2, 2)

In [19]: ax3 = fig.add_subplot(2, 2, 3)

提示:使用 Jupyter notebook 有一點不同,即每個小窗重新執行後,圖形會被重置。因此,對於複雜的圖形,,你必須將所有的繪圖命令存在一個小窗裡。

這裡,我們運行同一個小窗裡的所有命令:

fig = plt.figure()
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 3)

如果這時執行一條繪圖命令(如 plt.plot([1.5, 3.5, -2, 1.6])),matplotlib 就會在最後一個用過的 subplot(如果沒有則建立一個)上進行繪製,隱藏建立 figure 和 subplot 的過程。因此,如果我們執行下列命令,你就會得到如圖 9-3 所示的結果:

In [20]: plt.plot(np.random.randn(50).cumsum(), 'k--')

"k--" 是一個線型選項,用於告訴 matplotlib 繪製黑色虛線圖。上面那些由 fig.add_subplot 所返回的物件是 AxesSubplot 物件,直接呼叫它們的例項方法就可以在其它空著的格子裡面畫圖了,如圖 9-4 所示:

In [21]: _ = ax1.hist(np.random.randn(100), bins=20, color='k', alpha=0.3)

In [22]: ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.randn(30))

你可以在 matplotlib 的文件中找到各種圖表型別。

建立包含 subplot 網格的 figure 是一個非常常見的任務,matplotlib 有一個更為方便的方法 plt.subplots,它可以建立一個新的 Figure,並返回一個含有已建立的 subplot 物件的 NumPy 陣列:

In [24]: fig, axes = plt.subplots(2, 3)

In [25]: axes
Out[25]: 
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7fb626374048>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fb62625db00>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fb6262f6c88>],
       [<matplotlib.axes._subplots.AxesSubplot object at 0x7fb6261a36a0>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fb626181860>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x7fb6260fd4e0>]], dtype
=object)

這是非常實用的,因為可以輕鬆地對 axes 陣列進行索引,就好像是一個二維陣列一樣,例如 axes[0,1]。你還可以通過 sharex 和 sharey 指定 subplot 應該具有相同的 X 軸或 Y 軸。在比較相同範圍的資料時,這也是非常實用的,否則,matplotlib 會自動縮放各圖表的界限。有關該方法的更多資訊,請參見表 9-1。

調整 subplot 周圍的間距

預設情況下,matplotlib 會在 subplot 外圍留下一定的邊距,並在 subplot 之間留下一定的間距。間距跟影象的高度和寬度有關,因此,如果你調整了影象大小(不管是程式設計還是手工),間距也會自動調整。利用 Figure 的 subplots_adjust 方法可以輕而易舉地修改間距,此外,它也是個頂級函式:

subplots_adjust(left=None, bottom=None, right=None, top=None,
                wspace=None, hspace=None)

wspace 和 hspace 用於控制寬度和高度的百分比,可以用作 subplot 之間的間距。下面是一個簡單的例子,其中我將間距收縮到了 0(如圖 9-5 所示):

fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
for i in range(2):
    for j in range(2):
        axes[i, j].hist(np.random.randn(500), bins=50, color='k', alpha=0.5)
plt.subplots_adjust(wspace=0, hspace=0)

不難看出,其中的軸標籤重疊了。matplotlib 不會檢查標籤是否重疊,所以對於這種情況,你只能自己設定刻度位置和刻度標籤。後面幾節將會詳細介紹該內容。

顏色、標記和線型

matplotlib 的 plot 函式接受一組 X 和 Y 座標,還可以接受一個表示顏色和線型的字串縮寫。例如,要根據 x 和 y 繪製綠色虛線,你可以執行如下程式碼:

ax.plot(x, y, 'g--')

這種在一個字串中指定顏色和線型的方式非常方便。在實際中,如果你是用程式碼繪圖,你可能不想通過處理字串來獲得想要的格式。通過下面這種更為明確的方式也能得到同樣的效果:

ax.plot(x, y, linestyle='--', color='g')

常用的顏色可以使用顏色縮寫,你也可以指定顏色碼(例如,'#CECECE')。你可以通過檢視 plot 的文件字串檢視所有線型的合集(在 IPython 和 Jupyter 中使用 plot?)。

線圖可以使用標記強調資料點。因為 matplotlib 可以建立連續線圖,在點之間進行插值,因此有時可能不太容易看出真實資料點的位置。標記也可以放到格式字串中,但標記型別和線型必須放在顏色後面(見圖 9-6):

In [30]: from numpy.random import randn

In [31]: plt.plot(randn(30).cumsum(), 'ko--')

還可以將其寫成更為明確的形式:

plot(randn(30).cumsum(), color='k', linestyle='dashed', marker='o')

線上型圖中,非實際資料點預設是按線性方式插值的。可以通過 drawstyle 選項修改(見圖 9-7):

In [33]: data = np.random.randn(30).cumsum()

In [34]: plt.plot(data, 'k--', label='Default')
Out[34]: [<matplotlib.lines.Line2D at 0x7fb624d86160>]

In [35]: plt.plot(data, 'k-', drawstyle='steps-post', label='steps-post')
Out[35]: [<matplotlib.lines.Line2D at 0x7fb624d869e8>]

In [36]: plt.legend(loc='best')

你可能注意到執行上面程式碼時有輸出 <matplotlib.lines.Line2D at ...>。matplotlib 會返回引用了新新增的子元件的物件。大多數時候,你可以放心地忽略這些輸出。這裡,因為我們傳遞了 label 引數到 plot,我們可以建立一個 plot 圖例,指明每條使用 plt.legend 的線。

筆記:你必須呼叫 plt.legend(或使用 ax.legend,如果引用了軸的話)來建立圖例,無論你繪圖時是否傳遞 label 標籤選項。

刻度、標籤和圖例

對於大多數的圖表裝飾項,其主要實現方式有二:使用過程型的 pyplot 介面(例如,matplotlib.pyplot)以及更為面向物件的原生 matplotlib API。

pyplot 介面的設計目的就是互動式使用,含有諸如 xlim、xticks 和 xticklabels 之類的方法。它們分別控制圖表的範圍、刻度位置、刻度標籤等。其使用方式有以下兩種:

  • 呼叫時不帶引數,則返回當前的引數值(例如,plt.xlim() 返回當前的 X 軸繪圖範圍)。
  • 呼叫時帶引數,則設定引數值(例如,plt.xlim([0,10]) 會將 X 軸的範圍設定為 0 到 10)。

所有這些方法都是對當前或最近建立的 AxesSubplot 起作用的。它們各自對應 subplot 物件上的兩個方法,以 xlim 為例,就是 ax.get_xlim 和 ax.set_xlim。我更喜歡使用 subplot 的例項方法(因為我喜歡明確的事情,而且在處理多個 subplot 時這樣也更清楚一些)。當然你完全可以選擇自己覺得方便的那個。

設定標題、軸標籤、刻度以及刻度標籤

為了說明自定義軸,我將建立一個簡單的影象並繪製一段隨機漫步(如圖 9-8 所示):

In [37]: fig = plt.figure()

In [38]: ax = fig.add_subplot(1, 1, 1)

In [39]: ax.plot(np.random.randn(1000).cumsum())

要改變 x 軸刻度,最簡單的辦法是使用 set_xticks 和 set_xticklabels。前者告訴 matplotlib 要將刻度放在資料範圍中的哪些位置,預設情況下,這些位置也就是刻度標籤。但我們可以通過 set_xticklabels 將任何其他的值用作標籤:

In [40]: ticks = ax.set_xticks([0, 250, 500, 750, 1000])

In [41]: labels = ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'],
   ....:                             rotation=30, fontsize='small')

rotation 選項設定 x 刻度標籤傾斜 30 度。最後,再用 set_xlabel 為 X 軸設定一個名稱,並用 set_title 設定一個標題(見圖 9-9 的結果):

In [42]: ax.set_title('My first matplotlib plot')
Out[42]: <matplotlib.text.Text at 0x7fb624d055f8>

In [43]: ax.set_xlabel('Stages')

Y 軸的修改方式與此類似,只需將上述程式碼中的 x 替換為 y 即可。軸的類有集合方法,可以批量設定繪圖選項。前面的例子,也可以寫為:

props = {
    'title': 'My first matplotlib plot',
    'xlabel': 'Stages'
}
ax.set(**props)

新增圖例

圖例(legend)是另一種用於標識圖表元素的重要工具。新增圖例的方式有多種。最簡單的是在新增 subplot 的時候傳入 label 引數:

In [44]: from numpy.random import randn

In [45]: fig = plt.figure(); ax = fig.add_subplot(1, 1, 1)

In [46]: ax.plot(randn(1000).cumsum(), 'k', label='one')
Out[46]: [<matplotlib.lines.Line2D at 0x7fb624bdf860>]

In [47]: ax.plot(randn(1000).cumsum(), 'k--', label='two')
Out[47]: [<matplotlib.lines.Line2D at 0x7fb624be90f0>]

In [48]: ax.plot(randn(1000).cumsum(), 'k.', label='three')
Out[48]: [<matplotlib.lines.Line2D at 0x7fb624be9160>]

在此之後,你可以呼叫 ax.legend() 或 plt.legend() 來自動建立圖例(結果見圖 9-10):

In [49]: ax.legend(loc='best')

legend 方法有幾個其它的 loc 位置引數選項。請檢視文件字串(使用 ax.legend?)。

loc 告訴 matplotlib 要將圖例放在哪。如果你不是吹毛求疵的話,"best" 是不錯的選擇,因為它會選擇最不礙事的位置。要從圖例中去除一個或多個元素,不傳入 label 或傳入 label='nolegend'即可。(中文第一版這裡把 best 錯寫成了 beat)

註解以及在 Subplot 上繪圖

除標準的繪圖型別,你可能還希望繪製一些子集的註解,可能是文字、箭頭或其他圖形等。註解和文字可以通過 text、arrow 和 annotate 函式進行新增。text 可以將文字繪製在圖表的指定座標 (x,y),還可以加上一些自定義格式:

ax.text(x, y, 'Hello world!',
        family='monospace', fontsize=10)

註解中可以既含有文字也含有箭頭。例如,我們根據最近的標準普爾 500 指數價格(來自 Yahoo!Finance)繪製一張曲線圖,並標出 2008 年到 2009 年金融危機期間的一些重要日期。你可以在 Jupyter notebook 的一個小窗中試驗這段程式碼(圖 9-11 是結果):

from datetime import datetime

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

data = pd.read_csv('examples/spx.csv', index_col=0, parse_dates=True)
spx = data['SPX']

spx.plot(ax=ax, style='k-')

crisis_data = [
    (datetime(2007, 10, 11), 'Peak of bull market'),
    (datetime(2008, 3, 12), 'Bear Stearns Fails'),
    (datetime(2008, 9, 15), 'Lehman Bankruptcy')
]

for date, label in crisis_data:
    ax.annotate(label, xy=(date, spx.asof(date) + 75),
                xytext=(date, spx.asof(date) + 225),
                arrowprops=dict(facecolor='black', headwidth=4, width=2,
                                headlength=4),
                horizontalalignment='left', verticalalignment='top')

# Zoom in on 2007-2010
ax.set_xlim(['1/1/2007', '1/1/2011'])
ax.set_ylim([600, 1800])

ax.set_title('Important dates in the 2008-2009 financial crisis')

這張圖中有幾個重要的點要強調:ax.annotate 方法可以在指定的 x 和 y 座標軸繪製標籤。我們使用 set_xlim 和 set_ylim 人工設定起始和結束邊界,而不使用 matplotlib 的預設方法。最後,用 ax.set_title 新增圖示標題。

更多有關注解的示例,請訪問 matplotlib 的線上示例庫。

圖形的繪製要麻煩一些。matplotlib 有一些表示常見圖形的物件。這些物件被稱為塊(patch)。其中有些(如 Rectangle 和 Circle),可以在 matplotlib.pyplot 中找到,但完整集合位於 matplotlib.patches。

要在圖表中新增一個圖形,你需要建立一個塊物件 shp,然後通過 ax.add_patch(shp) 將其新增到 subplot 中(如圖 9-12 所示):

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color='k', alpha=0.3)
circ = plt.Circle((0.7, 0.2), 0.15, color='b', alpha=0.3)
pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]],
                   color='g', alpha=0.5)

ax.add_patch(rect)
ax.add_patch(circ)
ax.add_patch(pgon)

如果檢視許多常見圖表物件的具體實現程式碼,你就會發現它們其實就是由塊 patch 組裝而成的。

將圖表儲存到檔案

利用 plt.savefig 可以將當前圖表儲存到檔案。該方法相當於 Figure 物件的例項方法 savefig。例如,要將圖表儲存為 SVG 檔案,你只需輸入:

plt.savefig('figpath.svg')

檔案型別是通過副檔名推斷出來的。因此,如果你使用的是. pdf,就會得到一個 PDF 檔案。我在釋出圖片時最常用到兩個重要的選項是 dpi(控制 “每英寸點數” 解析度)和 bbox_inches(可以剪除當前圖表周圍的空白部分)。要得到一張帶有最小白邊且解析度為 400DPI 的 PNG 圖片,你可以:

plt.savefig('figpath.png', dpi=400, bbox_inches='tight')

savefig 並非一定要寫入磁碟,也可以寫入任何檔案型的物件,比如 BytesIO:

from io import BytesIO
buffer = BytesIO()
plt.savefig(buffer)
plot_data = buffer.getvalue()

表 9-2 列出了 savefig 的其它選項。

matplotlib 配置

matplotlib 自帶一些配色方案,以及為生成出版質量的圖片而設定的預設配置資訊。幸運的是,幾乎所有預設行為都能通過一組全域性引數進行自定義,它們可以管理影象大小、subplot 邊距、配色方案、字型大小、網格型別等。一種 Python 程式設計方式配置系統的方法是使用 rc 方法。例如,要將全域性的影象預設大小設定為 10×10,你可以執行:

plt.rc('figure', figsize=(10, 10))

rc 的第一個引數是希望自定義的物件,如'figure'、'axes'、'xtick'、'ytick'、'grid'、'legend'等。其後可以跟上一系列的關鍵字引數。一個簡單的辦法是將這些選項寫成一個字典:

font_options = {'family' : 'monospace',
                'weight' : 'bold',
                'size'   : 'small'}
plt.rc('font', **font_options)

要了解全部的自定義選項,請查閱 matplotlib 的配置檔案 matplotlibrc(位於 matplotlib/mpl-data 目錄中)。如果對該檔案進行了自定義,並將其放在你自己的. matplotlibrc 目錄中,則每次使用 matplotlib 時就會載入該檔案。

下一節,我們會看到,seaborn 包有若干內建的繪圖主題或型別,它們使用了 matplotlib 的內部配置。

9.2 使用 pandas 和 seaborn 繪圖

matplotlib 實際上是一種比較低階的工具。要繪製一張圖表,你組裝一些基本元件就行:資料展示(即圖表型別:線型圖、柱狀圖、盒形圖、散佈圖、等值線圖等)、圖例、標題、刻度標籤以及其他註解型資訊。

在 pandas 中,我們有多列資料,還有行和列標籤。pandas 自身就有內建的方法,用於簡化從 DataFrame 和 Series 繪製圖形。另一個庫 seaborn(https://seaborn.pydata.org/),由 Michael Waskom 建立的靜態圖形庫。Seaborn 簡化了許多常見可視型別的建立。

提示:引入 seaborn 會修改 matplotlib 預設的顏色方案和繪圖型別,以提高可讀性和美觀度。即使你不使用 seaborn API,你可能也會引入 seaborn,作為提高美觀度和繪製常見 matplotlib 圖形的簡化方法。

線型圖

Series 和 DataFrame 都有一個用於生成各類圖表的 plot 方法。預設情況下,它們所生成的是線型圖(如圖 9-13 所示):

In [60]: s = pd.Series(np.random.randn(10).cumsum(), index=np.arange(0, 100, 10))

In [61]: s.plot()

該 Series 物件的索引會被傳給 matplotlib,並用以繪製 X 軸。可以通過 use_index=False 禁用該功能。X 軸的刻度和界限可以通過 xticks 和 xlim 選項進行調節,Y 軸就用 yticks 和 ylim。plot 引數的完整列表請參見表 9-3。我只會講解其中幾個,剩下的就留給讀者自己去研究了。

pandas 的大部分繪圖方法都有一個可選的 ax 引數,它可以是一個 matplotlib 的 subplot 物件。這使你能夠在網格佈局中更為靈活地處理 subplot 的位置。

DataFrame 的 plot 方法會在一個 subplot 中為各列繪製一條線,並自動建立圖例(如圖 9-14 所示):

In [62]: df = pd.DataFrame(np.random.randn(10, 4).cumsum(0),
   ....:                   columns=['A', 'B', 'C', 'D'],
   ....:                   index=np.arange(0, 100, 10))

In [63]: df.plot()

plot 屬性包含一批不同繪圖型別的方法。例如,df.plot() 等價於 df.plot.line()。後面會學習這些方法。

筆記:plot 的其他關鍵字引數會被傳給相應的 matplotlib 繪圖函式,所以要更深入地自定義圖表,就必須學習更多有關 matplotlib API 的知識。

DataFrame 還有一些用於對列進行靈活處理的選項,例如,是要將所有列都繪製到一個 subplot 中還是建立各自的 subplot。詳細資訊請參見表 9-4。