好看的圖表千篇一律,能交互的可視化數據萬里挑一

好看的圖表千篇一律,能交互的可視化數據萬里挑一

來自專欄 景略集智

數據可視化不僅在我們的工作中可以起到重要作用,而且也能像藝術一樣賞心悅目。藉助多種可視化工具,如 ggplots 和 matplotlib,我們可以創建出有趣的可視化。之前我們也分享過如何用 Python 創建 9 種常見的可視化圖表

景略集智:超實用!用Python進行數據可視化的9種常見方法!?

zhuanlan.zhihu.com圖標

不過用它們我們很難製作出互動式圖表,比如當我們想以 3D 方式展示數據的時候,我們就無法從多個角度查看數據。這個時候互動式可視化就能為我們解決這個問題,不僅能讓我們以互動的形式獲取數據傳遞的信息,也能讓數據可視化更美觀更有逼格,如圖所示:

今天我們就分享一下如何用 Python 和 Bokeh 庫製作互動式可視化圖表。首先我們會用 Bokeh 生成一些簡單的可視化圖表,熟悉它的操作,然後用它創建互動式直方圖。

Bokeh 基本原理

Bokeh 的主要理念就是通過每次創建一個圖層的方式來創建互動式圖形。我們首先會創建一個圖表,然後為圖表添加叫做「Glyphs」的元素。對於用過 ggplot 的人來說,Bokeh 中的 Glyphs 基本上就跟 ggplot 里每次往圖形里添加的 geom 一樣。根據不同用途,Glyphs 可以有不同的形狀:圓圈、線條、條形、弧形等等。

我們先用方塊和圓圈製作一個基本的圖表,展示一下 Bokeh 的理念。首先,我們用 figure 方法製作一個圖形,然後通過調用合適的方法和輸入數據將我們的 Glyphs(也就是方塊和圓圈)應用到圖表上。最後我們展示出圖表。

# bokeh 基本原理from bokeh.plotting import figurefrom bokeh.io import show, output_notebook# 用labels創建空的圖形p = figure(plot_width = 600, plot_height = 600, title = Example Glyphs, x_axis_label = X, y_axis_label = Y)# 樣本數據squares_x = [1, 3, 4, 5, 8]squares_y = [8, 7, 3, 1, 10]circles_x = [9, 12, 4, 3, 15]circles_y = [8, 4, 11, 6, 10]# 添加方塊Glyphp.square(squares_x, squares_y, size = 12, color = navy, alpha = 0.6)# 添加圓圈Glyphp.circle(circles_x, circles_y, size = 12, color = red)# 設定在notebook中輸出圖形output_notebook()# 展示圖形show(p)

上述代碼會生成如下略顯簡陋的圖表:

當然我們使用任何繪圖庫都能很容易的創建這樣的一幅圖表,但 Bokeh 上還有一些免費的可配置工具,比如平移、放大縮小、選擇和保存圖形等,當我們想深度分析數據時,使用起來非常方便。

本文我們會用到紐約機場航班數據,記錄了 2013 年紐約機場大約 30 萬次航班的數據(cran.r-project.org/web/)。現在我們展示航班延誤數據。

在製作圖表之前,我們先載入數據,並做個簡單的檢查(加粗部分為輸出):

# 將數據從CSV讀取為數據幀flights = pd.read_csv(../data/flights.csv, index_col=0)# 為感興趣的列總結數據狀況flights[arr_delay].describe()count 327346.000000mean 6.895377std 44.633292min -86.00000025% -17.00000050% -5.00000075% 14.000000max 1272.000000

從對數據的總結中可以看到:我們一共有 327,346 個航班,最短延誤時間為 -86 分鐘(也就是提前了 86 分鐘),最長延誤時間為 1272 分鐘,居然延誤了 21 小時!有大約 75% 的航班只晚點了 14 分鐘,所以我們可以假設超過 1000 分鐘的數字可能為異常值(不是說它們不合理,只是太極端了)。在我們的直方圖中,會重點關注延誤時間在 -60 分鐘和 +120 分鐘之間的航班。

在我們初次可視化一個單獨的變數時,通常會選擇直方圖,因為它能直觀的展示數據的分布狀況。在我們要創建的直方圖中,X 軸的位置表示航班延誤了多少分鐘,條形的高度表示對應的航班數量。雖然 Bokeh 並沒有內置的直方圖 Glyph,但我們可以使用方塊 Glyphs 自己創建,它能讓我們指明每個條形的底部、頂部和左右邊緣。

我們會使用 Numpy 的 histogram 函數為條形創建數據,它可以計算每個指定條形中的數據點數量。我們會使用 5 分鐘長度的條形,也就是說函數會計算每 5 分鐘延誤間隔的航班數量。在生成數據後,我們將其放在 Pandas 數據幀中,以一個對象保存所有數據。這步的操作方法見這裡:pandas.pydata.org/panda 。 雖然這裡的代碼對理解 Bokeh 來說不是很重要,但熟悉使用 Numpy 和 Pandas 在數據科學中還是很重要的。

"""Bins will be five minutes in width, so the number of bins is (length of interval / 5). Limit delays to [-60, +120] minutes using the range."""arr_hist, edges = np.histogram(flights[arr_delay], bins = int(180/5), range = [-60, 120])# 將信息放入數據幀delays = pd.DataFrame({arr_delay: arr_hist, left: edges[:-1], right: edges[1:]})

我們的數據會如下所示:

其中 flight 列表示從 left 列至 right 列每個延誤間隔中的航班數量計數。在這裡我們會製作一個新的 Bokeh 圖表,添加一個方塊 Glyph,指明合適的參數:

# 創建空白圖表p = figure(plot_height = 600, plot_width = 600, title = Histogram of Arrival Delays, x_axis_label = Delay (min)], y_axis_label = Number of Flights)# 添加一個方塊Glyphp.quad(bottom=0, top=delays[flights], left=delays[left], right=delays[right], fill_color=red, line_color=black)# 展示圖表show(p)

生成這張圖形的大部分工作主要是數據格式編排,這在數據科學中很常見。從我們的圖形中可以看到航班延誤幾乎呈輕微的正偏態分布,並在右側出現厚尾現象。

用 Python 創建基本的直方圖,還有很多簡單的方法,比如用 matplotlib 只需幾行代碼就能生成同樣的圖形。不過,使用 Bokeh 製作圖形時,有很多工具能讓我們很容易的和數據之間產生交互。

添加交互性

首先我們給圖形添加一種被動型交互,也就是查看圖形的人可以篩選哪些數據不予展示。被動型交互通常扮演著「檢察員」的角色,因為可以讓人們更詳細的「調查」數據。其中一種比較有用的被動型交互就是讓用戶的滑鼠滑過數據點時,會浮現信息提示框,這在 Bokeh 中稱為HoverTool(bokeh.pydata.org/en/lat)。

要想添加信息提示框,我們需要將來自數據幀中的數據源改為 ColumnDataSource(Bokeh 中的一個重要概念)。這是一個對象,可以特別用於繪製包括數據和幾種方法及屬性的圖形。ColumnDataSource 能讓我們為圖形添加註釋和交互性,也能從 Pandas 數據幀中構造。實際數據會保存在一個目錄中,可以通過 ColumnDataSource 的數據屬性訪問。這裡我們創建來自我們的數據幀中的數據源,查看和數據幀的列對應的數據目錄的 key。

# 導入ColumnDataSource 類from bokeh.models import ColumnDataSource# 將數據幀轉換為column data sourcesrc = ColumnDataSource(delays)src.data.keys()dict_keys([flights, left, right, index])

在我們用 ColumnDataSource 添加 Glyphs 時,我們會將 ColumnDataSource 作為 source 參數輸入,並用字元串引用列名:

# 這次添加一個帶有數據源的方塊Glyphp.quad(source = src, bottom=0, top=flights, left=left, right=right, fill_color=red, line_color=black)

注意代碼是如何僅用一行字元串而不是之前的 df[column] 格式,就引用了具體的數據列,比如「flight」「left」和「right」。

Bokeh 中的 HoverTool

剛看時 HoverTool 的語法看著可能會有點複雜,不過熟悉了之後你會發現很簡單。我們傳遞我們的 HoverTool 實例以 Python 元組的形式傳入一列 tooltips,在 Python 元組中第一個元素為數據標籤,第二個元素會引用我們想突出強調的具體數據。我們可以用$引用圖表的屬性,比如 X 軸或 Y 軸位置,或用 @ 引用我們數據源中的具體欄位。這可能聽起來有點困惑,所以下面舉例示範如何用 HoverTool 做到這兩點:

# Hover tool 用@引用我們自己的數據域 # 用$引用圖形中的位置h = HoverTool(tooltips = [(Delay Interval Left , @left), ((x,y), ($x, $y))])

這裡,我們用 @ 引用 ColumnDataSource 中的 left 數據域(和原始數據幀中的 left 列對應),用 $ 引用游標的(x,y)位置,結果如下:

(x,y)位置是滑鼠在圖形上的位置,但對我們的直方圖來說並不是很有用,因為我們是要找到某個給定條形中的航班數量(出現在條形上方)。為了修正這一點,我們會變更我們的 tooltip 實例來引用正確的列。將 tooltip 中顯示的數據進行格式編排會令人比較苦惱,我們可以在數據幀中以正確的格式化創建另一個列。例如,假如我想讓我的 tooltip 展示某個給定條形的全部間隔,我可以在數據幀中創建一個格式化後的列:

# 添加一個列顯示每個間隔的範圍delays[f_interval] = [%d to %d minutes % (left, right) for left, right in zip(delays[left], delays[right])]

然後我們將該數據幀轉換為一個 ColumnDataSource,並在 HoverTool 調用中訪問這個列。下面的代碼就是用了一個引用了兩個格式化列的 HoverTool 創建圖形,並將該工具添加至圖形:

# 創建空白圖形p = figure(plot_height = 600, plot_width = 600, title = Histogram of Arrival Delays, x_axis_label = Delay (min)], y_axis_label = Number of Flights)# 這次創建一個帶數據源的方塊Glyphp.quad(bottom=0, top=flights, left=left, right=right, source=src, fill_color=red, line_color=black, fill_alpha = 0.75, hover_fill_alpha = 1.0, hover_fill_color = navy)# 添加一個hover tool 引用格式化的數據列hover = HoverTool(tooltips = [(Delay, @f_interval), (Num of Flights, @f_flights)])# 為圖形設定風格p = style(p)# 為圖形添加hover toolp.add_tools(hover)# 顯示圖形show(p)

在 Bokeh 中,我們通過將元素添加至原始圖形中的方式將元素包含在圖形中。注意在 p.quad 的 Glyph 調用中,還有一些額外元素,比如 hover_fill_alpha和hover_fill_color,它們可以改變當我們用滑鼠滑過條形時 Glyph 的外觀。我們還可以用 style 函數添加圖形風格。

至於圖形的美感,我們通常可以寫一個可以應用到任何圖形的函數。最終圖形如下所示:

我們用滑鼠滑過不同的條形時,就會得到每個條形所對應的詳細統計數據:延誤間隔和間隔內航班數量。如果覺得自己製作的圖表不錯,可以將其保存為 HTML 文件分享出去:

# 導入savings 函數from bokeh.io import output_file# 指定輸出文件並保存output_file(hist.html)show(p)

創建互動式直方圖

我們製作好了一幅基本的直方圖,但談不上高逼格。雖然別人在圖形可以看到航班延誤的分布狀況,然而除此外並沒有特別驚艷的地方。

如果我們想創建引人注目的可視化,我們可以讓他人通過和圖表之間的互動自行探究數據。比如,在我們創建的這個直方圖中,一個很有價值的交互功能就是人們能夠選擇具體的航線進行比較,或者能在菜單選項中更改條形的寬度以更詳細的查看數據。我們用 Bokeh 就能實現這些功能。

主動型互動

Bokeh 中有兩種交互方式:被動型交互和主動型交互。我們前面分享了如何為圖表添加簡單的被動型交互功能,也就是說能讓用戶更詳細的查看圖表,但是卻不能改變圖表展示的信息。

那麼第二種互動式圖形之所以被稱為主動型交互,是因為它可以改變在圖形上實際展示的數據,比如選擇數據的子集(例如具體的航線)和改變多項式回歸擬合的角度等等。在 Bokeh 中有很多種類型的主動型交互,但這裡我們重點關注叫做 Widget(即窗口小部件)的交互功能,它是可以用滑鼠點擊的元素,從而讓我們可以控制圖形的某些方面。

widget示例

主動型互動式可視化會讓人們覺得看數據就像是一種享受,因為能讓我們按自己的方式探索數據,也比被動型可視化更能讓我們感知和總結數據信息。誇獎的話這裡不再多說,直接瞅瞅怎麼用 Bokeh 製作主動型互動式可視化。

交互概要

如果為圖形添加主動型交互,我們需要用到一些函數。對於 Bokeh 中的 Widget 交互來說,主要有 3 個實現函數:

  • Make_dataset() 格式化被展示的具體數據
  • Make_plot() 利用規定數據繪製圖形
  • Update()根據用戶的選擇更新圖形。

格式化數據

在製作圖形前,我們需要準備好後面會被展示的數據。對於我們的互動式直方圖來說,我們會為用戶提供三種可控的參數:

  • 展示的航空公司(在代碼中稱為carriers)
  • 圖形中的展示範圍,例如:-60 到+120分鐘
  • 直方圖條形的寬度,默認為5分鐘

至於為圖形準備數據集的函數,我們需要能夠指明每個參數。我們載入所有相關的數據並進行檢查,展示一下如何在 make_dataset 函數中轉換數據。

在該數據集中,每一行是一個單獨的航班。arr_delay 列是航班延誤的多好分鐘(負數表示提前到達)。在前面我們簡單處理過數據,得知一共有 327,236 個航班,最小延誤時間為 -86 分鐘,最長延誤時間為 +1272 分鐘。在 make_dataset 函數中,我們想根據數據幀中的 name 列選擇航線,並根據 arr_delay 列限制航班。

為了準備直方圖使用的數據,我們會用 Numpy 的 histogram 函數,它會計算每個條形中的數據點數量。在我們的例子中,也就是每個指明延誤間隔中的航班數量。在前面我們製作了所有航班的直方圖,但這裡我們會製作出每個航空公司的直方圖。由於每個航空公司的航班數量變化很大,我們可以以百分比的形式展示延誤航班。這樣,圖形中條形的高度就表示某個航空公司的延誤航班的比例。為了能將計數轉變為百分比,我們會將計數除以航空公司的總計數。

下面是準備數據集的全部代碼。函數取一列我們想包含在內的 carriers(即航空公司),需要繪製的最短和最長延誤時間,以及以分鐘為單位的指明條形寬度。

def make_dataset(carrier_list, range_start = -60, range_end = 120, bin_width = 5): # 檢查以確保範圍起始值小於結束值! assert range_start < range_end, "Start must be less than end!" by_carrier = pd.DataFrame(columns=[proportion, left, right, f_proportion, f_interval, name, color]) range_extent = range_end - range_start # 循環訪問所有航空公司 for i, carrier_name in enumerate(carrier_list): # 航空公司的子集 subset = flights[flights[name] == carrier_name] # 創建一個指定了條形和範圍的直方圖 arr_hist, edges = np.histogram(subset[arr_delay], bins = int(range_extent / bin_width), range = [range_start, range_end]) # 將計數除以總數並獲取百分比並創建 df arr_df = pd.DataFrame({proportion: arr_hist / np.sum(arr_hist), left: edges[:-1], right: edges[1:] }) # 格式化百分比 arr_df[f_proportion] = [%0.5f % proportion for proportion in arr_df[proportion]] # 格式化間隔 arr_df[f_interval] = [%d to %d minutes % (left, right) for left, right in zip(arr_df[left], arr_df[right])] # 為labels分配航空公司 arr_df[name] = carrier_name # 為每個航空公司分配不同的顏色 arr_df[color] = Category20_16[i] # 添加到全部數據幀 by_carrier = by_carrier.append(arr_df) # 全部數據幀 by_carrier = by_carrier.sort_values([name, left]) # 將數據幀轉換為column data sourcereturn ColumnDataSource(by_carrier)

雖然我們本文是在講解如何使用 Bokeh,但是沒有格式化的數據,是沒法製作圖形的,因此我們將這部分代碼也展示了出來。

函數運行於所有航空公司的結果如下:

提示一下,我們是用 Bokeh 的方塊 Glyph 來製作直方圖,所以我們需要提供 Glyph 的左側、右側和頂部(底部會固定為 0)。它們分別在 left,right 和 proportion列。Color 列會給每個航空公司分配一個唯一的顏色,f_ 列會為信息提示框提供格式處理後的文本數據。

下一個實現函數為 make_plot,它會接受 ColumnDataSource(bokeh 中一種用於繪圖的對象類型),返回圖形對象:

def make_plot(src): # 帶有正確labels的空白圖形 p = figure(plot_width = 700, plot_height = 700, title = Histogram of Arrival Delays by Carrier, x_axis_label = Delay (min), y_axis_label = Proportion) # 方塊 glyphs 以創建直方圖 p.quad(source = src, bottom = 0, top = proportion, left = left, right = right, color = color, fill_alpha = 0.7, hover_fill_color = color, legend = name, hover_fill_alpha = 1.0, line_color = black) # Hover tool with vline mode hover = HoverTool(tooltips=[(Carrier, @name), (Delay, @f_interval), (Proportion, @f_proportion)], mode=vline) p.add_tools(hover) # Styling p = style(p) return p

如果我們傳入一個有所有航空公司的數據源,代碼會輸出如下圖形:

該直方圖太過擁擠,因為在同一張圖上繪製了 16 家航空公司!如果有太多重疊信息,我們沒法比較航空公司。幸好,我們可以添加 widget,讓圖形更簡潔,也更容易比較信息。

創建 widget 交互

我們用 Bokeh 創建好了基本圖形後,通過 widget 添加交互功能就比較容易了。我們想要的第一個 widget 就是一個選擇工具箱,可以讓人們選擇需要顯示的航空公司。這裡的控制操作會是一個有很多選項的勾選框,在 Bokeh 中稱為 CheckboxGroup。我們導入 CheckboxGroup 類並創建一個有兩個參數的實例,以此製作選擇工具。實例的兩個參數為:labels:我們想緊貼每個條形顯示的值;active:查看的初始條形。創建含有所有航空公司的CheckboxGroup 的代碼如下:

from bokeh.models.widgets import CheckboxGroup# Create the checkbox selection element, available carriers is a # list of all airlines in the datacarrier_selection = CheckboxGroup(labels=available_carriers, active = [0, 1])

Bokeh 中勾選框的 labels 必須為字元串,而 active 的值則為整數。這意味著在圖形中,AirTran Airways Corporation』映射到的 active 值為 0,Alaska Airlines Inc.』映射到的active 值為 1。假如我們想將選擇的勾選框匹配到航空公司,我們需要確保能找到與所選整數 active 值相關的字元串名字。我們可以用 widget 的 .labels 和 .active 屬性做到這點:

# 從選定值中選取航空公司[carrier_selection.labels[i] for i in carrier_selection.active][AirTran Airways Corporation, Alaska Airlines Inc.]

在製作好選擇 widget 後,我們需要將所選航空公司的勾選框和圖形中顯示的信息連起來。這一步我們使用 CheckboxGroup的.on_change 方法以及我們定義的一個 update 函數來完成。Update 函數會一直取 3 個實參:attr,old 和 new,並根據用戶的選擇控制操作來更新圖形。我們改變圖形中展示的數據,其實就是通過改變我們在 make_plot 函數中傳入 Glyphs 的數據源來實現的。這裡聽起來可能有點抽象,我們就舉一個 update 函數的下例子,展示它如何改變直方圖,顯示哪些航線:

# Update 函數取3個默認函數def update(attr, old, new): # Get the list of carriers for the graph carriers_to_plot = [carrier_selection.labels[i] for i in carrier_selection.active] # 根據選定的航空公司和之前定義的make_dataset函數 # 制定一個新的數據集 new_src = make_dataset(carriers_to_plot, range_start = -60, range_end = 120, bin_width = 5) # 更新方塊 glpyhs 中使用的數據源 src.data.update(new_src.data)

這裡我們根據 CheckboxGroup 中的選定的航空公司,檢索航空公司的列表以進行展示。該列表會傳遞到 make_dataset 函數中,後者會返回一個新列數據源。我們調用 src.data.update 和傳入來自新數據源的數據來更新 Glyphs 中所用的數據源。最終,我們會用 .on_change 方法,將 carrier_selection 這個 widget 中的改動和 update 函數連起來。

# Link a change in selected buttons to the update functioncarrier_selection.on_change(active, update)

每當選擇或未選擇一個不同的航空公司,上述操作都會調用 update 函數。最終結果是只有和所選航空公司對應的 Glyphs 才會在直方圖上畫出來,如下所示:

更多交互功能

現在我們已經知道了為圖表創建一個交互操作的基本流程,那麼我們接著就可以增加更多的元素。我們每次創建一個 widget,就會寫一個 update 函數來改變圖形上展示的數據,並使用 .on_change 方法將 update 函數和 widget 相連。我們也可以重新編寫 update 函數從 widget 提取我們需要的值,通過這種方法用同一 update 函數增加多個元素。

下面我們實際操作一下,為圖形添加另外兩個控制操作:添加一個滾動條,可以讓我們選擇直方圖中條形的寬度;添加一個範圍滾動條,可以讓我們設置航班的最長和最短延誤時間。 創建這兩個 widget 的新的 update 函數如下:

# 滾動條用來選擇條形寬度,值為選定數字binwidth_select = Slider(start = 1, end = 30, step = 1, value = 5, title = Delay Width (min))# 當值變化時更新圖形binwidth_select.on_change(value, update)# 範圍滾動條用來改變直方圖中的最大和最小值。range_select = RangeSlider(start = -60, end = 180, value = (-60, 120), step = 5, title = Delay Range (min))# 當值變化時更新圖形range_select.on_change(value, update)# 說明全部3種控制操作的update函數def update(attr, old, new): # 找到選定的航空公司 carriers_to_plot = [carrier_selection.labels[i] for i in carrier_selection.active] # 將條形寬度改為選型的值 bin_width = binwidth_select.value # 範圍滾動條的值為一個元組(start,end) range_start = range_select.value[0] range_end = range_select.value[1] # 創建新的ColumnDataSource new_src = make_dataset(carriers_to_plot, range_start = range_start, range_end = range_end, bin_width = bin_width) # 更新圖形上的數據 src.data.update(new_src.data)

標準的滾動條和範圍滾動條如下所示:

如果有需要,我們也可以用 update 函數改變圖形的其它方面,比如可以改變名稱的文本信息來匹配條形的寬度:

# Change plot title to match selectionbin_width = binwidth_select.valuep.title.text = Delays with %d Minute Bin Width % bin_width

在 Bokeh 中還有很多類型的交互功能,但是我們現在創建的這三個操控功能就完全能讓我們「把玩」圖形和圖形互動了!

整合圖形

我們互動式圖形的所有元素都已準備就緒,我們有 3 個必需的函數:make_dataset,make_plot 和 update函數,它們可以根據用戶的操作和相應的 widget,自行更改圖形。現在我們定義一個圖形布局,將所有的元素整合在一個圖上。

from bokeh.layouts import column, row, WidgetBoxfrom bokeh.models import Panelfrom bokeh.models.widgets import Tabs# Put controls in a single elementcontrols = WidgetBox(carrier_selection, binwidth_select, range_select) # Create a row layoutlayout = row(controls, p) # Make a tab with the layout tab = Panel(child=layout, title = Delay Histogram)tabs = Tabs(tabs=[tab])

最終我們大功告成,製作了一個互動式數據可視化圖形!!

效果如下:


本項目GitHub庫地址:

github.com/WillKoehrsen


推薦閱讀:

超實用!用Python進行數據可視化的9種常見方法!
昨晚AI送給我一首歌,我很感動接著告訴他有待提高。
從入門到進階,人工智慧資源匯總(十二月)
用深度學習處理服飾圖片識別(附代碼練習題)

TAG:數據可視化 | 數據分析 | 景略集智 |