1. 程式人生 > >利用 Python 中 Bokeh 實現資料視覺化,第二部分:互動

利用 Python 中 Bokeh 實現資料視覺化,第二部分:互動

利用 Python 中 Bokeh 實現資料視覺化,第二部分:互動

超越靜態圖的圖解

本系列的第一部分 中,我們介紹了在 Bokeh(Python 中一個強大的視覺化庫)中建立的一個基本柱狀圖。最後的結果顯示了 2013 年從紐約市起飛的航班延遲到達的分佈情況,如下所示(有一個非常好的工具提示):

這張表完成了任務,但並不是很吸引人!使用者可以看到航班延遲的幾乎是正常的(有輕微的斜率),但他們沒有理由在這個數字上花幾秒鐘以上的時間。

如果我們想建立更吸引人的視覺化資料,可以允許使用者通過互動方式來獲取他們想要的資料。比如,在這個柱狀圖中,一個有價值的特性是能夠選擇指定航空公司進行比較,或者選擇更改容器的寬度來更詳細地檢查資料。辛運的是,我們可以使用 Bokeh 在現有的繪圖基礎上新增這兩個特性。柱狀圖的最初開發似乎只涉及到了一個簡單的圖,但我們現在即將體驗到像 Bokeh 這樣的強大的庫的所帶來的好處!

本系列的所有程式碼都可在 GitHub 上獲得。任何感興趣的人都可以檢視所有的資料清洗細節(資料科學中一個不那麼鼓舞人心但又必不可少的部分),也可以親自執行它們!(對於互動式 Bokeh 圖,我們仍然可以使用 Jupyter Notebook 來顯示結果,我們也可以編寫 Python 指令碼,並執行 Bokeh 伺服器。我通常使用 Jupyter Notebook 進行開發,因為它可以在不重啟伺服器的情況下,就可以很容易的快速迭代和更改繪圖。然後我將它們遷移到伺服器中來顯示最終結果。你可以在 GitHub 上看到一個獨立的指令碼和完整的筆記)。

主動的互動

在 Bokeh 中,有兩類互動:被動的和主動的。第一部分所描述的被動互動也稱為 inspectors,因為它們允許使用者更詳細地檢查一個圖,但不允許更改顯示的資訊。比如,當用戶懸停在資料點上時出現的工具提示:

工具提示,被動互動器

第二類互動被稱為 active,因為它更改了顯示在繪圖上的實際資料。這可以是從選擇資料的子集(例如指定的航空公司)到改變匹配多項式迴歸擬合程度中的任何資料。在 Bokeh 中有多種型別的 active 互動,但這裡我們將重點討論“小部件”,可以被單擊,而且使用者能夠控制某些繪圖方面的元素。

小部件示例(下拉按鈕和單選按鈕組)

當我檢視圖時,我喜歡主動的互動(比如那些在 FlowingData 上的互動),因為它們允許我自己去研究資料。我發現讓人印象更深刻的是從我自己的資料中發現的結論(從設計者那裡獲取的一些研究方向),而不是從一個完全靜態的圖表中發現的結論。此外,給予使用者一定程度的自由,可以讓他們對資料集提出更有用的討論,從而產生不同的解釋。

互動概述

一旦我們開始新增主動互動,我們就需要越過單行程式碼,深入封裝特定操作的函式。對於 Bokeh 小部件的互動,有三個主要函式可以實現:

  • make_dataset() 格式化想要顯示的特定資料
  • make_plot() 用指定的資料進行繪圖
  • update() 基於使用者選擇來更新繪圖

格式化資料

在我們繪製這個圖之前,我們需要規劃將要顯示的資料。對於我們的互動柱狀圖,我們將為使用者提供三個可控引數:

  1. 航班顯示(在程式碼中稱為運營商)
  2. 繪圖中的時間延遲範圍,例如:-60 到 120 分鐘
  3. 預設情況下,柱狀圖的容器寬度是 5 分鐘

對於生成繪圖資料集的函式,我們需要允許指定每個引數。為了告訴我們如何轉換 make_dataset 函式中的資料,我們需要載入所有相關資料進行檢查。

柱狀圖資料

在此資料集中,每一行都是一個單獨的航班。 arr_delay 列是航班到達延誤數分鐘(負數表示航班提前到達)。在第一部分中,我們做了一些資料探索,知道有 327,236 次航班,最小延誤時間為 - 86 分鐘,最大延誤時間為 1272 分鐘。在 make_dataset 函式中,我們想基於 dataframe 中的 name 列來選擇公司,並用 arr_delay 列來限制航班。

為了生成柱狀圖的資料,我們使用 numpy 函式 histogram 來統計每個容器中的資料點數。在我們的示例中,這是每個指定延遲間隔中的航班數。對於第一部分,我們做了一個包含所有航班的柱狀圖,但現在我們會為每一個運營商都提供一個柱狀圖。由於每個航空公司的航班數目有很大差異,我們可以顯示延遲而不是按原始數目顯示,可以按比例顯示。也就是說,圖上的高度對應於特定航空公司的所有航班比例,該航班在相應的容器中有延遲。從計數到比例,我們除以航空公司的總數。

下面是生成資料集的完整程式碼。函式接受我們希望包含的運營商列表,要繪製的最小和最大延遲,以及制定的容器寬度(以分鐘為單位)。

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'])]

        # 為標籤指定運營商
        arr_df['name'] = carrier_name

        # 不同顏色的運營商
        arr_df['color'] = Category20_16[i]

        # 新增到整個 dataframe 中
        by_carrier = by_carrier.append(arr_df)

    # 總體 dataframe
    by_carrier = by_carrier.sort_values(['name', 'left'])
    
    # 將 dataframe 轉換為列資料來源
    return ColumnDataSource(by_carrier)
複製程式碼

(我知道這是一篇關於 Bokeh 的部落格,但在你不能在沒有格式化資料的情況下來生成圖表,因此我使用了相應的程式碼來演示我的方法!)

執行帶有所需運營商的函式結果如下:

作為提醒,我們使用 Bokeh quad 表來製作柱狀圖,因此我們需要提供表的左、右和頂部(底部將固定為 0)。它們分別在羅列在 leftright 以及 proportion。顏色列為每個運營商提供了唯一的顏色,f_ 列為工具提供了格式化文字的功能。

下一個要實現的函式是 make_plot。函式應該接受 ColumnDataSource (Bokeh 中用於繪圖的一種特定型別物件)並返回繪圖物件:

def make_plot(src):
        # 帶有正確標籤的空白圖
        p = figure(plot_width = 700, plot_height = 700, 
                  title = 'Histogram of Arrival Delays by Carrier',
                  x_axis_label = 'Delay (min)', y_axis_label = 'Proportion')

        # 建立柱狀圖的四種符號
        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')

        # vline 模式下的懸停工具
        hover = HoverTool(tooltips=[('Carrier', '@name'), 
                                    ('Delay', '@f_interval'),
                                    ('Proportion', '@f_proportion')],
                          mode='vline')

        p.add_tools(hover)

        # Styling
        p = style(p)

        return p 
複製程式碼

如果我們向所有航空公司傳遞一個源,此程式碼將給出以下繪圖:

這個柱狀圖非常混亂,因為 16 家航空公司都繪製在同一張圖上!因為資訊被重疊了,所以如果我們想比較航空公司就顯得不太現實。辛運的是,我們可以新增小部件來使繪製的圖更清晰,也能夠進行快速地比較。

建立可互動的小部件

一旦我們在 Bokeh 中建立一個基礎圖形,通過小部件新增互動就相對簡單了。我們需要的第一個小部件是允許使用者選擇要顯示的航空公司的選擇框。這是一個允許根據需要進行儘可能多的選擇的複選框控制元件,在 Bokeh 中稱為T CheckboxGroup.。為了製作這個可選工具,我們需要匯入 CheckboxGroup 類來建立帶有兩個引數的例項,labels:我們希望顯示每個框旁邊的值以及 active:檢查選中的初始框。以下建立的 CheckboxGroup 程式碼中附有所需的運營商。

from bokeh.models.widgets import CheckboxGroup

# 建立複選框可選元素,可用的載體是
# 資料中所有航空公司組成的列表
carrier_selection = CheckboxGroup(labels=available_carriers, 
                                  active = [0, 1])
複製程式碼

CheckboxGroup 部件

Bokeh 複選框中的標籤必須是字串,但啟用值需要的是整型。這意味著在在影象 ‘AirTran Airways Corporation’ 中,啟用值為 0,而 ‘Alaska Airlines Inc.’ 啟用值為 1。當我們想要將選中的複選框與 airlines 想匹配時,我們需要確保所選的整型啟用值能匹配與之對應的字串。我們可以使用部件的 .labels.active 屬性來實現。

# 從選擇值中選擇航空公司的名稱
[carrier_selection.labels[i] for i in carrier_selection.active]

['AirTran Airways Corporation', 'Alaska Airlines Inc.']
複製程式碼

在製作完小部件後,我們現在需要將選中的航空公司複選框連結到圖表上顯示的資訊中。這是使用 CheckboxGroup 的 .on_change 方法和我們定義的 update 函式完成的。update 函式總是具有三個引數:attr、old、new,並基於選擇控制元件來更新繪圖。改變圖形上顯示的資料的方式是改變我們傳遞給 make_plot 函式中的圖形的資料來源。這聽起來可能有點抽象,因此下面是一個 update 函式的示例,該函式通過更改柱狀圖來顯示選定的航空公司:

# update 函式有三個預設引數
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)

    # update 在 quad glpyhs 中使用的源
    src.data.update(new_src.data)
複製程式碼

這裡,我們從 CheckboxGroup 中檢索要基於選定航空公司顯示的航空公司列表。這個列表被傳遞給 make_dataset 函式,它返回一個新的列資料來源。我們通過呼叫 src.data.update 以及傳入來自新源的資料更新圖表中使用的源資料。最後,為了將 carrier_selection 小部件中的更改連結到 update 函式,我們必須使用 .on_change 方法(稱為事件處理器)。

# 將選定按鈕中的更改連結到 update 函式
carrier_selection.on_change('active', update)
複製程式碼

在選擇或取消其他航班的時會呼叫 update 函式。最終結果是在柱狀圖中只繪製了與選定航空公司相對應的符號,如下所示:

更多控制元件

現在我們已經知道了建立控制元件的基本工作流程,我們可以新增更多元素。我們每次建立小部件時,編寫 update 函式來更改顯示在繪圖上的資料,通過事件處理器來將 update 函式連結到小部件。我們甚至可以通過重寫函式來從多個元素中使用相同的 update 函式來從小部件中提取我們所需的值。在實踐過程中,我們將新增兩個額外的控制元件:一個用於選擇柱狀圖容器寬度的 Slider,另一個是用於設定最小和最大延遲的 RangeSlider。下面是生成這些小部件和 update 函式的程式碼:

# 滑動 bindwidth,對應的值就會被選中
binwidth_select = Slider(start = 1, end = 30, 
                     step = 1, value = 5,
                     title = 'Delay Width (min)')
# 當值被修改時,更新繪圖
binwidth_select.on_change('value', update)

# RangeSlider 用於修改柱狀圖上的最小最大值
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]
    
    # 修改 binwidth 為選定的值
    bin_width = binwidth_select.value

    # 範圍滑塊的值是一個元組(開始,結束)
    range_start = range_select.value[0]
    range_end = range_select.value[1]
    
    # 建立新的列資料
    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 函式顯示資料之外,我們也可以修改其他的繪圖功能。例如,為了將標題文字與容器寬度匹配,我們可以這樣做:

# 將繪圖示題修改為匹配選擇
bin_width = binwidth_select.value
p.title.text = 'Delays with %d Minute Bin Width' % bin_width
複製程式碼

在 Bokeh 中海油許多其他型別的互動,但現在,我們的三個控制元件允許執行在圖示上“執行”!

把所有內容放在一起

我們的所有互動式繪圖元素都已經說完了。我們有三個必要的函式:make_datasetmake_plotupdate,基於控制元件和系哦啊不見自身來更改繪圖。我們通過定義佈局將所有這些元素連線到一個頁面上。

from bokeh.layouts import column, row, WidgetBox
from bokeh.models import Panel
from bokeh.models.widgets import Tabs

# 將控制元件放在單個元素中
controls = WidgetBox(carrier_selection, binwidth_select, range_select)
    
# 建立行佈局
layout = row(controls, p)
    
# 使用佈局來建立一個選項卡
tab = Panel(child=layout, title = 'Delay Histogram')
tabs = Tabs(tabs=[tab])
複製程式碼

我將整個佈局放在一個選項卡上,當我們建立一個完整的應用程式時,我們可以為每個繪圖都建立一個單獨的選項卡。最後的工作結果如下所示:

可以在 GitHub 上檢視相關程式碼,並繪製自己的繪圖。

下一步和內容

本系列的下一部分將討論如何使用多個繪圖來製作一個完整的應用程式。我們將通過伺服器來展示我們的工作結果,可以通過瀏覽器對其進行訪問,並建立一個完整的儀表盤來探究資料集。

我們可以看到,最終的互動繪圖比原來的有用的多!我們現在可以比較航空公司之間的延遲,並更改容器的寬度/範圍,來了解這些分佈是如何被影響的。增加的互動性提高了繪圖的價值,因為它增加了對資料的支援,並允許使用者通過自己的探索得出結論。儘管設定了初始化的繪圖,但我們仍然可以看到如何輕鬆地將元素和控制元件新增到現有的圖形中。與像 matplotlib 這樣快速簡單的繪相簿相比,使用更重的繪相簿(比如 bokeh)可以定製化繪圖和互動。不同的視覺化庫有不同的優點和用例,但當我們想要增加互動的額外維度時,Bokeh 是一個很好的選擇。希望在這一點上,你有足夠的信心來開發你自己的視覺化繪圖,也希望看到你可以分享自己的創作。

歡迎向我反饋以及建設性的批評,可以在 Twitter @koehrsen_will 上和我聯絡。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄