Seaborn中文教程(五):通過「多圖網格」結構化展示多維數據

當探索具有中等數量(不多不少的意思……)維度的數據集時,一個很好的方式是基於不同的子數據集構建不同的實例,並將它們以網格的方式組織在一張圖之中。這種技術有時被稱為「lattice」或「trellis」(大概是格子圖、網格圖),這跟「small multiples」的概念類似(多張更小的子圖)。它能幫助我們快速從複雜的數據中提取大量信息。matplotlib對於創建帶有多個坐標軸(每個坐標軸體系意味著一張子圖)的圖形有著良好的支持,seaborn基於這些來直接地將圖形的排布結構與數據集的結構聯結起來。

要利用這些特性,我們的數據集應該保存在一個pandas DataFrame中,並且應該是Hadley Whickam口中的「tidy data」格式。簡短來說,就是我們的dataframe對象中,每一行是一個觀測樣本,每一列是一個變數。

對於一些更高級的應用來說,我們可以直接使用這篇教程中討論的一些對象來獲得最大的靈活性。一些seaborn函數(如lmplot()/catplot()/pairplot())也隱式地使用了這些對象。很多seaborn函數是坐標軸級別的,它們僅僅針對某個特定的matplotlib Axes來繪圖,而不會修改圖形的屬性;而在這篇教程中即將討論到的這些方法是更高級別的函數,在被調用時,它們會創建一個新的圖形,而且一般情況下對於創建過程更加嚴格。在某些案例中,這些函數和他們依賴的類所需要輸入的參數依賴於不同的介面屬性,比如在lmplot()中我們通過設置高度和寬高比(heightaspect)來控制每個子圖(facet)的大小,而非直接指定整個圖形的大小。這些函數在調用後都會返回這個圖形對象,而且大多數對象都提供了非常方便的方法來改變繪圖的方式,這些方法往往更加抽象和簡單。

import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="ticks")

一、條件式多圖

當我們想要基於不同的數據子集來展示某個變數的分布或者多個變數之間的關係時,FacetGrid類會提供很大的幫助。一個FacetGrid圖可以從三個維度來構建:rowcolhue。前兩個與它返回的坐標軸數組有著之間的關聯;我們可以把hue變數理解為第三個維度,就像長、寬和高一樣,只不過在這裡我們是用不同的顏色來體現它的。

在使用FacetGrid時,我們會通過一個pandas DataFrame以及控制圖形網格的行、列和顏色的變數名稱來初始化一個對象。這些維度變數(控制行、列和顏色的變數)應該是分類變數或者離散變數,然後這些變數的不同水平組合起來就構成了整個圖形的每一個子圖(facet,在這裡可以理解為我們維度拆解的最小粒度)。比如說我們想要檢驗一下tips數據集中午餐和晚餐的差異。

另外,relplot()/catplot()/lmplot()內置了FacetGrid對象,繪圖完成後他們都會返回這個對象,這樣我們就可以進行更多的調整。

tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, col="time")

傳入數據和維度變數名稱會初始化一個網格,其中生成了基於matplotlib的圖形和坐標軸,但是不會在這些坐標軸中畫任何東西(因為我們還沒告訴它們畫什麼)。

在這些網格中畫圖的主要方式是使用FacetGrid.map()方法。我們需要告訴它使用哪個繪圖函數以及使用哪些(哪個)變數。我們用直方圖來看下兩個數據子集中小費的分布情況:

g = sns.FacetGrid(tips, col="time")
g.map(plt.hist, "tip");

這一函數的目標是一步到位地提供一幅完整的成品圖,它在完成繪圖後還會對每個坐標軸添加註釋。想要生成一個戰士變數關係的圖,只需要傳遞更多的變數名稱進去。我們還可以提供關鍵字參數,它會將他們傳遞給繪圖函數:

g = sns.FacetGrid(tips, col="sex", hue="smoker")
g.map(plt.scatter, "total_bill", "tip", alpha=.7)
g.add_legend();

有很多選項可以傳遞給FacetGrid的構造函數,用以控制網格的樣式:

g = sns.FacetGrid(tips, row="smoker", col="time", margin_titles=True)
g.map(sns.regplot, "size", "total_bill", color=".3", fit_reg=False, x_jitter=.1);

需要注意的是,margin_title參數並沒有被matplotlib API提供正式支持,在某些案例中或許並不可用。比如說當圖外邊有圖例時,它是不可用的。

圖的大小是通過每張子圖的高度和寬高比來控制的:

g = sns.FacetGrid(tips, col="day", height=4, aspect=.5)
g.map(sns.barplot, "sex", "total_bill");
/anaconda3/lib/python3.7/site-packages/seaborn/axisgrid.py:715: UserWarning: Using the barplot function without specifying `order` is likely to produce an incorrect plot.
warnings.warn(warning)

它默認會從DataFrame中推導分類的順序。如果用來控制某個方面(facet:維度/軸/角度,看哪個概念能幫助你理解它,你就用哪個名字)的變數是分類變數,那麼分類變數的順序就會被使用。否則,seaborn會使用這些分類在數據集中出現的先後順序。當然,我們完全可以通過*_order參數直接指定某個維度變數的順序:

ordered_days = tips.day.value_counts().index
g = sns.FacetGrid(tips, row="day", row_order=ordered_days, height=1.7, aspect=4)
g.map(sns.distplot, "total_bill", hist=False, rug=True);

我們可以指定某個seaborn調色板,也可以通過字典將hue變數中的每個分類與其對應的matplotlib顏色傳遞給函數(這樣就可以隨心所以使用大量的matplotlib支持的色彩):

pal = dict(Lunch="seagreen", Dinner="gray")
g = sns.FacetGrid(tips, hue="time", palette=pal, height=5)
g.map(plt.scatter, "total_bill", "tip", s=50, alpha=.7, linewidth=.5, edgecolor="white")
g.add_legend();

我們還可以控制hue變數的不同水平展示出來的其他樣式(方面),這些在黑白色調下(比如黑白印刷圖)對於提高圖形的可讀性尤其有用。我們需要將一個字典傳遞給hue_kws參數,在這個字典中,key(字典的鍵)是繪圖函數的關鍵字參數名稱;而value(字典的值)則是一個列表,用於存儲關鍵字參數的取值,其中每個取值對應了hue變數的一個水平。

g = sns.FacetGrid(tips, hue="sex", palette="Set1", height=5, hue_kws={"marker": ["^", "v"]})
g.map(plt.scatter, "total_bill", "tip", s=100, linewidth=.5, edgecolor="white")
g.add_legend();

如果某個維度變數(用於col/row/hue參數的變數,之後不再說明)具有非常多的水平(level:取值),我們可以把它們分布到不同的列,然後把它們「摺疊」到不同的行中。當我們使用這種操作時,我們不能設置row變數。

attend = sns.load_dataset("attention").query("subject <= 12")
g = sns.FacetGrid(attend, col="subject", col_wrap=4, height=2, ylim=(0, 10))
g.map(sns.pointplot, "solutions", "score", color=".3", ci=None);
/anaconda3/lib/python3.7/site-packages/seaborn/axisgrid.py:715: UserWarning: Using the pointplot function without specifying `order` is likely to produce an incorrect plot.
warnings.warn(warning)

當我們已經使用FacetGrid.map()完成了繪圖,我們可能還想對圖形的某些方面做些調整。FacetGrid支持很多在更高層級調整圖形的方法,最常用的是FacetGrid.set(),還有一些更加具體的方法比如FacetGrid.set_axis_labels(),如圖:

with sns.axes_style("white"):
g = sns.FacetGrid(tips, row="sex", col="smoker", margin_titles=True, height=2.5)
g.map(plt.scatter, "total_bill", "tip", color="#334488", edgecolor="white", lw=.5);
g.set_axis_labels("Total bill (US Dollars)", "Tip");
g.set(xticks=[10, 30, 50], yticks=[2, 6, 10]);
g.fig.subplots_adjust(wspace=.02, hspace=.02);

想要做更多定製的話,我們可以直接操作更底層的FigureAxes對象,它們存儲在FacetGrid.figFacetGrid.axes中,其中,FacetGrid.axes是一個二維數組。假如我們沒有指定行和列,那麼我們也可以直接使用FacetGrid.ax來操作那個唯一的坐標軸:

g = sns.FacetGrid(tips, col="smoker", margin_titles=True, height=4)
g.map(plt.scatter, "total_bill", "tip", color="#338844", edgecolor="white", s=50, lw=1)
for ax in g.axes.flat:
ax.plot((0, 50), (0, .2 * 50), c=".2", ls="--")
g.set(xlim=(0, 60), ylim=(0, 14));

二、使用自定義繪圖函數

使用FacetGrid時,我們並非只能使用現成的matplotlibseaborn繪圖函數。不過,如果想要正常工作,我們的自定義函數需要滿足一下規則:

  1. 它必須是一個坐標軸級別的函數(僅對當前活躍的坐標軸進行繪圖)。在matplotlib.pyplot的命名空間中存在的函數都是符合要求的,我們也可以調用plt.gca()(get current axes)來獲取符合要求的坐標軸。
  2. 它必須支持以位置參數的方式接受數據傳入。FacetGrid內部會把我們傳遞給FacetGrid.map()的多組序列數據分別傳遞給這些位置參數。
  3. 它必須支持接受關鍵字參數colorlabel,另外,理想情況下,它還會利用這兩個參數做一些有用的事情。在大多數情況下,接受一個關鍵字參數(**kwargs)的字典並傳遞給底層的繪圖函數是很容易的。

我們來看一個符合最低條件的例子,這個繪圖函數僅接受一組來自某個分類(子數據集)的向量型數據:

from scipy import stats

def quantile_plot(x, **kwargs):
qntls, xr = stats.probplot(x, fit=False)
plt.scatter(xr, qntls, **kwargs)

g = sns.FacetGrid(tips, col="sex", height=4)
g.map(quantile_plot, "total_bill");

如果我們想要繪製一個二元圖,我們需要讓函數接受兩組數據,且x軸對應的數據在前邊,y軸對應的數據在後邊:

def qqplot(x, y, **kwargs):
_, xr = stats.probplot(x, fit=False)
_, yr = stats.probplot(y, fit=False)
plt.scatter(xr, yr, **kwargs)

g = sns.FacetGrid(tips, col="smoker", height=4)
g.map(qqplot, "total_bill", "tip");

由於plt.scatter可以接受關鍵字參數colorlabel並且能正確處理它們,所以我們可以毫不費力地增加一個hue參數(因為它的原理就是給不同的分類數據傳入不同的顏色參數):

g = sns.FacetGrid(tips, hue="time", col="sex", height=4)
g.map(qqplot, "total_bill", "tip")
g.add_legend();

我們還可以通過關鍵字參數控制額外的美學設計來區分hue變數的不同水平:

g = sns.FacetGrid(tips, hue="time", col="sex", height=4,
hue_kws={"marker": ["s", "D"]})
g.map(qqplot, "total_bill", "tip", s=40, edgecolor="w")
g.add_legend();

當然,有時我們完全不想處理colorlabel。這種情況下我們需要顯式地接受他們,然後用自己的邏輯去處理他們。比如下邊這個使用plt.hexbin的例子,在與FacetGridAPI搭配的過程中。如果我們沒有手動調整顏色處理方式的話,它們的表現會不太好:

def hexbin(x, y, color, **kwargs):
cmap = sns.light_palette(color, as_cmap=True)
plt.hexbin(x, y, gridsize=15, cmap=cmap, **kwargs)

with sns.axes_style("white"):
g = sns.FacetGrid(tips, hue="time", col="time", height=4)
g.map(hexbin, "total_bill", "tip", extent=[0, 50, 0, 10]);

三、展示變數間的兩兩(成對)關係

PairGrid也支持我們以同樣的方式快速繪製多個子圖。在PairGrid中,每行每列都被分配給一個不同的變數,所以最後生成的圖片可以展示數據集中所有的成對關係。這種風格的圖形有時被稱作「散點圖矩陣」,因為散點圖是表現兩兩關係最常用的方法。不過PairGrid並不會僅僅局限於散點圖。

了解FacetGridPairGrid之間的區別非常重要。在FacetGrid中,每張圖表現的都是同樣的變數關係,只是每張圖對應著不同的數據子集,數據子集的劃分是由我們指定的維度變數決定的(colrowhue),這些變數相互交叉後產生一系列最小粒度的數據子集,每個子集就對應了一張子圖,也就是說,不管是多少行、多少列還是多少顏色,它們都對應著這些維度變數的取值(水平)。而在PairGrid中,每張子圖都代表了不同的兩個變數間的關係(當然,上三角和下三角會有鏡像的關係,因為它們相當於互換了x軸和y軸)。PairGrid可以對於我們數據集中的變數關係提供一個非常快速、整體(不深入)的總結。

它的基本使用方法與FacetGrid非常類似。首先我們初始化一個網格,然後把繪圖函數傳遞給map方法,它會將我們的繪圖函數在所有的子圖中調用。與PairGrid對應的函數是pairplot(),它失去了一些靈活性,但是讓我們的繪圖更加快捷。

iris = sns.load_dataset("iris")
g = sns.PairGrid(iris)
g.map(plt.scatter);

我們可以在對角線上用不同的函數來展示單變數的分布情況。不過需要注意的是,軸上的刻度與分桶計數或者密度沒有關係(因為我們已經用軸刻度去展示數據的取值了)。

g = sns.PairGrid(iris)
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter);

在這種圖中,我們常常會對屬於不同分類的樣本標記上不同的顏色。比如,iris數據集中有三種不同的鳶尾花,其中每個樣本都有4個特徵,因此我們可以看下它們的區別在哪裡。

g = sns.PairGrid(iris, hue="species")
g.map_diag(plt.hist)
g.map_offdiag(plt.scatter)
g.add_legend();

默認情況下,數據集中所有的數值型變數都會被使用,不過我們也可以僅選用特定的列:

g = sns.PairGrid(iris, vars=["sepal_length", "sepal_width"], hue="species")
g.map(plt.scatter);

我們也可以分別在上三角和下三角中使用不同的函數,用以強調關係的不同角度。

g = sns.PairGrid(iris)
g.map_upper(plt.scatter)
g.map_lower(sns.kdeplot)
g.map_diag(sns.kdeplot, lw=3, legend=False);

事實上,這種對稱的方形網格矩陣只是一個特例,我們可以在行和列上分別使用不同的變數。

g = sns.PairGrid(tips, y_vars=["tip"], x_vars=["total_bill", "size"], height=4)
g.map(sns.regplot, color=".3")
g.set(ylim=(-1, 11), yticks=[0, 5, 10]);

當然,設計(美學)屬性都是可以配置的。比如,我們可以使用不同的調色板、可以給繪圖函數傳遞關鍵字參數。

g = sns.PairGrid(tips, hue="size", palette="GnBu_d")
g.map(plt.scatter, s=50, edgecolor="white")
g.add_legend();

PairGrid很靈活,但是想要快速觀察數據特點的話,使用pairplot()更容易。這個函數默認使用散點圖和直方圖,但是也支持一些其他類型(現在我們還可以在非對角位置上畫回歸圖、在對角位置上畫KDE圖),未來還會支持更多。

sns.pairplot(iris, hue="species", height=2.5);

pairplot()中我們也可以通過關鍵字參數調整設計風格(美學),並且它會返回一個PairGrid對象用於更多的調整。

g = sns.pairplot(iris, hue="species", palette="Set2", diag_kind="kde", height=2.5)


推薦閱讀:

TAG:Python | 可視化 | 數據可視化 |