(點選上方公眾號,可快速關註一起學Python)
作者 | Will Koehrsen
翻譯 | Lemon
譯文出品 | Python資料之道 (ID:PyDataRoad)
本文透過一個專案案例,詳細的介紹瞭如何從 Bokeh 基礎到構建 Bokeh 互動式應用程式的過程,內容循序漸進且具有很高的實用性。本文共有兩萬字左右,屬於純乾貨分享,強烈推薦大家閱讀後續內容。
本文由以下幾個大的部分組成:
- Bokeh 基礎介紹
- 在 Bokeh 中新增主動互動功能
- 在 Bokeh 中建立互動式視覺化應用程式
可用於資料科學的資源正在迅速發展,這在視覺化領域尤其明顯,似乎每週都有另一種選擇。 隨著所有這些進步,有一個共同的趨勢:增加互動性。 人們喜歡在靜態圖中檢視資料,但他們更喜歡的是使用資料來檢視更改引數如何影響結果。
關於我的研究,一份報告告訴建築物所有者他們可以透過改變他們的空調(AC)使用計劃表節省多少電力是很好的,但是給他們一個互動式圖表更有效,他們可以選擇不同的使用計劃表,看看他們的選擇如何影響用電量。
最近,受到互動圖的趨勢和不斷學習新工具的渴望的啟發,我一直在使用 Bokeh,一個 Python 庫。 我為我的研究專案構建的儀錶板中顯示了 Bokeh 互動功能的一個示例,如下:
雖然我不能分享這個專案背後的程式碼,但我可以透過一個使用公開資料構建完全互動式 Bokeh 應用程式的例子。
本文將介紹使用 Bokeh 建立應用程式的整個過程。 首先,我們將介紹 Bokeh 的基礎內容, 我們將使用 nycflights13
資料集,該資料集記錄了 2013年超過 300,000 個航班。首先,我們將專註於視覺化單個變數,在這種情況下,航班的到達延遲時間為幾分鐘,我們將從構造基本直方圖開始。
一、Bokeh 基礎
Bokeh 的主要概念是圖形一次構建一層。 我們首先建立一個圖形(figure),然後在圖形中新增稱為圖形符號(glyphs)的元素。 glyphs 可以根據所需的用途呈現多種形狀:圓形(circles),線條(lines) ,補丁(patches),條形(bars),弧形(arcs)等。
讓我們透過製作帶有正方形和圓形的基本圖表來說明 glyphs 的概念。 首先,我們使用 figure
方法建立一個圖,然後透過呼叫適當的方法並傳入資料將我們的 glyphs 附加到 figure 中。 最後,我們展示了所做的圖表。
(註:手機上可以透過左右滑動來檢視程式碼)
# bokeh basics
from bokeh.plotting import figure
from bokeh.io import show, output_notebook
# Create a blank figure with labels
p = figure(plot_width = 600, plot_height = 600,
title = 'Example Glyphs',
x_axis_label = 'X', y_axis_label = 'Y')
# Example data
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]
# Add squares glyph
p.square(squares_x, squares_y, size = 12, color = 'navy', alpha = 0.6)
# Add circle glyph
p.circle(circles_x, circles_y, size = 12, color = 'red')
# Set to output the plot in the notebook
output_notebook()
# Show the plot
show(p)
圖示如下:
現在讓我們開始展示航班延誤資料,在進入圖表之前,應該載入資料並對其進行簡要檢查:
# Read the data from a csv into a dataframe
flights = pd.read_csv('../data/flights.csv', index_col=0)
# Summary stats for the column of interest
flights['arr_delay'].describe()
out[]:
count 327346.000000
mean 6.895377
std 44.633292
min -86.000000
25% -17.000000
50% -5.000000
75% 14.000000
max 1272.000000
上述統計資料提供了可以用來決策的資訊:共有 327,346 次航班,最短延誤時間為-86 分鐘(意味著航班提前 86 分鐘),最長延遲時間為 1272 分鐘,驚人的 21 小時! 75% 的分位數僅在 14 分鐘,因此我們可以假設超過 1000 分鐘的數字可能是異常值(這並不意味著它們是非法的,只是極端的)。
下麵將重點關註直方圖的 -60 分鐘到 +120 分鐘之間的延遲。
直方圖是單個變數的初始視覺化的常見選擇,因為它顯示了資料的分佈。 x 位置是被稱為區間(bins)的變數的值,並且每個柱子的高度表示每個區間中的資料點的計數(數量)。 在我們的例子中,x 位置將代表以分鐘為單位的到達延遲,高度是相應 bin 中的航班數量。 Bokeh 沒有內建的直方圖,但是我們可以使用 quad
來製作我們自己的直方圖。
為條形圖(bars)建立資料,我們將使用 Numpy 的 histogram
函式來計算每個指定 bin 中的資料點數。 我們將使用 5 分鐘長度的時間間隔(bins),這意味著該功能將計算每五分鐘延遲間隔的航班數量。 生成資料後,我們將其放在 Pandas 的 dataframe 中,以將所有資料儲存在一個物件中。
"""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])
# Put the information in a dataframe
delays = pd.DataFrame({'arr_delay': arr_hist,
'left': edges[:-1],
'right': edges[1:]})
資料如下:
flights
列是從 left
到 right
的每個延遲間隔內的航班數量。 從這裡開始,我們可以建立一個新的 Bokeh 圖形,並新增一個指定適當引數的 quad
:
# Create the blank plot
p = figure(plot_height = 600, plot_width = 600,
title = 'Histogram of Arrival Delays',
x_axis_label = 'Delay (min)]',
y_axis_label = 'Number of Flights')
# Add a quad glyph
p.quad(bottom=0, top=delays['flights'],
left=delays['left'], right=delays['right'],
fill_color='red', line_color='black')
# Show the plot
show(p)
從上述圖表來看,我們看到到達延遲幾乎正態分佈,右側有輕微的正偏斜或重尾。
當然,其實有更簡單的方法可以在 Python 中建立基本直方圖,比如可以使用幾行 matplotlib 程式碼完成相同的結果。 但是,我們想在 Bokeh 圖中新增直方圖併進行互動演示。
增加互動性
本文介紹的第一種互動方式是被動互動。 這些是讀者可以採取的不會改變所顯示資料的動作。 這些被稱為檢查員(inspectors),因為它們允許讀者更詳細地 “檢視” 資料。 一個有用的檢查器是當使用者將滑鼠懸停在資料點上時出現的提示工具,在 Bokeh 中稱為 HoverTool 。
為了新增提示工具(tooltips),我們需要將資料源從 dataframe 更改為 ColumnDataSource
(CDS),這是 Bokeh 中的一個關鍵概念。 CDS 是一個專門用於繪圖的物件,包括資料以及多個方法和屬性。 CDS 允許我們為圖形新增註釋和互動性,並且可以從pandas 的 dataframe 構建。 實際資料本身儲存在可透過 CDS 的 data 屬性訪問的字典中。
在這裡,我們從 dataframe 建立原始碼,並檢視資料字典中與 dataframe 列對應的鍵。
# Import the ColumnDataSource class
from bokeh.models import ColumnDataSource
# Convert dataframe to column data source
src = ColumnDataSource(delays)
src.data.keys()
out:
dict_keys(['flights', 'left', 'right', 'index'])
當我們使用 CDS 新增 glyphs 時,我們傳入 CDS 作為 source 引數並使用字串取用列名:
# Add a quad glyph with source this time
p.quad(source = src, bottom=0, top='flights',
left='left', right='right',
fill_color='red', line_color='black')
註意程式碼如何透過單個字串而不是之前的 df ['column']
格式取用特定資料列,例如’flights’,’left’ 和 ‘right’。
Bokeh 中的 HoverTool
HoverTool 的語法起初可能看起來有些複雜,但透過練習它們很容易建立。
我們將 HoverTool
實體作為 Python 元組的 “tooltips” 串列傳遞,其中第一個元素是資料的標簽,第二個元素取用我們想要突出顯示的特定資料。 我們可以使用 $
取用圖表的任一屬性,例如 x 或 y 位置,或使用 @
取用我們資料源中的特定欄位。
這可能聽起來有點令人困惑,所以這裡有一個 HoverTool 的例子:
# Hover tool referring to our own data field using @ and
# a position on the graph using $
h = HoverTool(tooltips = [('Delay Interval Left ', '@left'),
('(x,y)', '($x, $y)')])
在這裡,我們使用 @
取用 ColumnDataSource 中的 left
資料欄位(對應於原始 dataframe 的 ‘left’ 列),並使用 $
取用游標的(x,y)位置。 結果如下:
(x,y)位置是圖表上滑鼠的位置,對我們的直方圖不是很有幫助,因為我們要找到給定條形中對應於條形頂部的航班數量。 為瞭解決這個問題,我們將改變我們的 tooltip 實體以取用正確的列。 格式化提示工具中顯示的資料可能令人沮喪,因此我通常在 dataframe 中使用正確的格式建立另一列。 例如,如果我希望我的提示工具顯示給定欄的整個間隔,我在 dataframe 中建立一個格式化的列:
# Add a column showing the extent of each interval
delays['f_interval'] = ['%d to %d minutes' % (left, right) for left, right in zip(delays['left'], delays['right'])]
然後,我將此 dataframe 轉換為 ColumnDataSource 併在我的 HoverTool 呼叫中訪問此列。
下麵的程式碼使用懸停工具建立繪圖,取用兩個格式化的列並將工具新增到繪圖中:
# Create the blank plot
p = figure(plot_height = 600, plot_width = 600,
title = 'Histogram of Arrival Delays',
x_axis_label = 'Delay (min)]',
y_axis_label = 'Number of Flights')
# Add a quad glyph with source this time
p.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')
# Add a hover tool referring to the formatted columns
hover = HoverTool(tooltips = [('Delay', '@f_interval'),
('Num of Flights', '@f_flights')])
# Style the plot
p = style(p)
# Add the hover tool to the graph
p.add_tools(hover)
# Show the plot
show(p)
在 Bokeh 樣式中,透過將元素新增到原始圖形中來包含元素。 註意在 p.quad
呼叫中,還有一些額外的引數, hover_fill_alpha
和 hover_fill_color
,當將滑鼠懸停在條形圖上時會改變 glyph 的外觀。
我還使用 style
函式添加了樣式。 當使用樣式時,我會保持簡單並專註於標簽的可讀性。 圖的主要觀點是顯示資料,新增不必要的元素只會減少圖形的用處! 最終的圖形如下:
當將滑鼠懸停在不同的欄上時,會得到該欄的精確統計資料,顯示該區間內的間隔和航班數。 如果我們為圖形感到自豪,可以將其儲存到html檔案中進行分享:
# Import savings function
from bokeh.io import output_file
# Specify the output file and save
output_file('hist.html')
show(p)
上面這張圖完成了工作,但它不是很吸引人! 讀者可以看到航班延誤的分佈接近正態分佈(略有正偏斜),但他們沒有理由再花費更多的時間來分析該圖。
如果想要建立更具吸引力的視覺化圖表,我們可以允許使用者透過互動自己來探索資料。 例如,在直方圖中,一個有價值的特徵是能夠選擇特定航空公司進行比較,或者選擇更改 bins 的寬度以更精細地檢查資料。
幸運的是,這些都是可以使用 Bokeh 在現有繪圖之上新增的功能。 直方圖的初始開發可能似乎涉及一個簡單的繪圖,但現在我們看到使用像 Bokeh 這樣強大的庫的回報!
二、在 Bokeh 中新增主動互動
Bokeh中有兩類互動:被動互動和主動互動。 前面介紹的被動互動也稱為檢查器(inspectors),因為它們允許使用者更詳細地查閱圖表中的資訊,但不會更改顯示的資訊。 一個示例是當使用者將滑鼠懸停在資料點上時顯示的提示資訊,如下:
第二類互動稱為主動互動,因為它會更改繪圖上顯示的實際資料。 這可以是從選擇資料子集(例如特定航空公司)到改變多項式回歸擬合自由度的任何事情。 Bokeh 中有多種型別的主動互動,但在這裡我們將重點關註所謂的“小部件”(“widgets”),可以點選的元素,並讓使用者控製圖形的某些方面。
當檢視圖表時,我喜歡使用主動互動,因為它們允許我自己探索資料。 我發現從我自己的資料(來自設計師的某個方向)而不是從完全靜態的圖表中發現資料的結論更具洞察力。 此外,為使用者提供一定的自由度使他們能夠略微不同的解釋,從而產生有關資料集的有益討論。
主動互動的實現方法
一旦我們開始新增主動互動,我們需要超越單行程式碼併進入封裝特定操作的函式。 對於 Bokeh 小部件(widgets)互動,有三個主要功能要實現:
- make_dataset(): 按特定格式整理要顯示的特定資料
- make_plot(): 使用指定的資料繪圖
- update(): 根據使用者選擇更新繪圖
整理資料
在製作繪圖之前,需要設計將要顯示的資料。 對於互動式直方圖,將為使用者提供三個可控引數:
- 航空公司 (在程式碼中稱為 carriers)
- 延遲的時間範圍,比如: -60 至 +120 分鐘
- 直方圖的寬度(即 bin 大小),預設值為 5 分鐘
對於為繪圖建立資料集的函式,我們需要允許指定每個引數。 為了告知我們如何在 make_dataset
函式中轉換資料,我們可以載入所有相關資料併進行檢查。
在此資料集中,每行是一個單獨的航班。 arr_delay
列是以分鐘為單位的航班到達延遲(負數表示航班早到)。 從前面的描述中我們知道有 327,236 個航班,最小延遲為 -86 分鐘,最大延遲為 +1272 分鐘。 在 make_dataset
函式中,我們希望根據 dataframe 中的 name
列選擇航空公司,並透過 arr_delay
列限制航班數量。
為了生成直方圖的資料,我們使用 numpy 中的 histogram
函式來計算每個bin中的資料點數。在示例中,這是每個指定延遲間隔內的航班數量。 在前面內容中,為所有航班製作了直方圖,但現在我們將針對每個航空公司進行。
由於每個航空公司的航班數量差異很大,我們可以按比例顯示延遲,而不是原始計數。 也就是說,圖上的高度表示的是,在相應的 bin 區間,特定航空公司中該航班相對應於所有航班的延遲比例。 為了從計數到比例,我們將計數除以該航空公司的航班總數。
下麵是製作資料集的完整程式碼,該函式接收我們想要包括的航空公司串列,要繪製的最小和最大延遲,以及以分鐘為單位的指定 bin 寬度。
def make_dataset(carrier_list, range_start = -60, range_end = 120, bin_width = 5):
# Check to make sure the start is less than the end!
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
# Iterate through all the carriers
for i, carrier_name in enumerate(carrier_list):
# Subset to the carrier
subset = flights[flights['name'] == carrier_name]
# Create a histogram with specified bins and range
arr_hist, edges = np.histogram(subset['arr_delay'],
bins = int(range_extent / bin_width),
range = [range_start, range_end])
# Divide the counts by the total to get a proportion and create df
arr_df = pd.DataFrame({'proportion': arr_hist / np.sum(arr_hist),
'left': edges[:-1], 'right': edges[1:] })
# Format the proportion
arr_df['f_proportion'] = ['%0.5f' % proportion for proportion in arr_df['proportion']]
# Format the interval
arr_df['f_interval'] = ['%d to %d minutes' % (left, right) for left,
right in zip(arr_df['left'], arr_df['right'])]
# Assign the carrier for labels
arr_df['name'] = carrier_name
# Color each carrier differently
arr_df['color'] = Category20_16[i]
# Add to the overall dataframe
by_carrier = by_carrier.append(arr_df)
# Overall dataframe
by_carrier = by_carrier.sort_values(['name', 'left'])
# Convert dataframe to column data source
return ColumnDataSource(by_carrier)
上述執行結果如下:
提醒一下,我們使用 Bokeh 中 quad
函式來製作直方圖,因此我們需要提供該圖形符號的左、右和頂部(底部將固定為0)引數。 它們分別位於 “left”,“right” 和 “proportion” 列中。 color 列為每個顯示的航空公司提供了唯一的顏色, f_
列為 tooltips 提供了格式化文字。
下一個要實現的功能是 make_plot
。 該函式應該採用 ColumnDataSource(Bokeh中用於繪圖的特定型別的物件)並傳回繪圖物件:
def make_plot(src):
# Blank plot with correct 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')
# Quad glyphs to create a histogram
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 家航空公司在同一圖表上繪製! 如果想比較航空公司,由於資訊重疊,這幾乎是不可能的。 幸運的是,我們可以新增小部件(widgets)以使繪圖更清晰並實現快速比較。
建立互動的小部件
一旦我們在 Bokeh 中建立基本圖形,透過視窗小部件新增互動相對簡單。 我們想要的第一個小部件是一個選擇框,允許讀者選擇要顯示的航空公司。 該控制元件將是一個核取方塊,允許根據需要進行盡可能多的選擇,併在 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 data
carrier_selection = CheckboxGroup(labels=available_carriers,
active = [0, 1])
Bokeh 核取方塊中的標簽必須是字串,而活動值是整數。 這意味著在圖形中 ‘AirTran Airways Corporation’ 對應數字 0 ,’Alaska Airlines Inc.’ 對應數值 1。 當想要將所選核取方塊與航空公司匹配時,需要確保查詢與所選整數活動值關聯的字串名稱。 我們可以使用小部件的 .labels
和 .active
屬性來做到這一點:
# Select the airlines names from the selection values
[carrier_selection.labels[i] for i in carrier_selection.active]
out:
['AirTran Airways Corporation', 'Alaska Airlines Inc.']
製作複選的小部件後,需要將選定的航空公司核取方塊連結到圖表上顯示的資訊。 這是使用 CheckboxGroup 的 .on_change
方法和我們定義的 update
函式完成的。 update 函式總是有三個引數: attr
, old
, new
並根據選擇控制元件更新繪圖。 我們更改圖表上顯示的資料的方法是改變我們傳遞給 make_plot
函式中的 glyph(s) 的資料源。 這可能聽起來有點抽象,所以這裡是有一個 update
函式的例子,它改變了直方圖以顯示所選的航空公司:
# Update function takes three default parameters
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 a new dataset based on the selected carriers and the
# make_dataset function defined earlier
new_src = make_dataset(carriers_to_plot,
range_start = -60,
range_end = 120,
bin_width = 5)
# Update the source used in the quad glpyhs
src.data.update(new_src.data)
在這裡,我們將檢查基於 CheckboxGroup 中所選航空公司顯示的航空公司串列。 此串列將傳遞給 make_dataset
函式,該函式傳回一個新的列資料源。 我們透過呼叫 src.data.update
並從新資料源傳入資料來更新 glyphs 中使用的源的資料。 最後,為了將 carrier_selection
小部件中的更改連結到 update
函式,我們必須使用 .on_change
方法(稱為事件處理程式)。
# Link a change in selected buttons to the update function
carrier_selection.on_change('active', update)
只要選擇或取消選擇不同的航空公司,就會呼叫更新功能。 最終結果是在直方圖上僅繪製了與所選航空公司相對應的圖形 ,如下所示:
更多的互動式控制
現在我們知道了建立控制元件的基本工作流程,可以新增更多元素。 每次,我們建立視窗小部件,編寫更新函式以更改繪圖上顯示的資料,並使用事件處理程式將更新功能連結到視窗小部件。 我們甚至可以透過重寫函式來從多個元素中使用相同的更新函式,以從小部件中提取需要的值。
為了練習,我們將新增兩個額外的控制元件:一個 Slider,用於選擇直方圖的 bin 寬度;一個 RangeSlider,用於設定要顯示的最小和最大延遲。 以下是製作這些小部件和新的 update
函式的程式碼:
# Slider to select the binwidth, value is selected number
binwidth_select = Slider(start = 1, end = 30,
step = 1, value = 5,
title = 'Delay Width (min)')
# Update the plot when the value is changed
binwidth_select.on_change('value', update)
# RangeSlider to change the maximum and minimum values on histogram
range_select = RangeSlider(start = -60, end = 180, value = (-60, 120),
step = 5, title = 'Delay Range (min)')
# Update the plot when the value is changed
range_select.on_change('value', update)
# Update function that accounts for all 3 controls
def update(attr, old, new):
# Find the selected carriers
carriers_to_plot = [carrier_selection.labels[i] for i in carrier_selection.active]
# Change binwidth to selected value
bin_width = binwidth_select.value
# Value for the range slider is a tuple (start, end)
range_start = range_select.value[0]
range_end = range_select.value[1]
# Create new ColumnDataSource
new_src = make_dataset(carriers_to_plot,
range_start = range_start,
range_end = range_end,
bin_width = bin_width)
# Update the data on the plot
src.data.update(new_src.data)
標準的 slider 和 range slider 如下所示:
除了使用更新功能顯示的資料之外,還可以更改繪圖的其他方面。例如,要更改標題文字以匹配 bin 寬度,可以執行以下操作:
# Change plot title to match selection
bin_width = binwidth_select.value
p.title.text = 'Delays with %d Minute Bin Width' % bin_width
在 Bokeh 中還有許多其他型別的互動,但是現在,我們的三個控制元件允許使用者在圖表上“玩”很多!
把它們放在一起
我們的互動圖表的所有元素都已到位。 我們有三個必要的函式: make_dataset
, make_plot
和 update
來根據控制元件和小部件本身改變繪圖。 我們透過定義佈局將所有這些元素連線到一個頁面上。
from bokeh.layouts import column, row, WidgetBox
from bokeh.models import Panel
from bokeh.models.widgets import Tabs
# Put controls in a single element
controls = WidgetBox(carrier_selection, binwidth_select, range_select)
# Create a row layout
layout = row(controls, p)
# Make a tab with the layout
tab = Panel(child=layout, title = 'Delay Histogram')
tabs = Tabs(tabs=[tab])
我將整個佈局放在一個選項卡上,當我們完成一個完整的應用程式時,我們可以將每個繪圖放在一個單獨的選項卡上。 所有這些工作的最終結果如下:
三、在 Bokeh 中建立互動式視覺化應用程式
接下來將重點介紹 Bokeh 應用程式的結構,而不是繪圖細節,但後續會提供所有內容的完整程式碼。我們將繼續使用 NYCFlights13 資料集,這是 2013年 紐約 3 個機場的航班的真實航班資訊集合。
要自己執行完整的應用程式,首先請確保安裝了Bokeh(使用 pip install bokeh
)。
其次,本專案的原始碼地址為:https://github.com/liyangbit/PyDataRoad,從該地址中下載 bokeh_app.zip
檔案夾,解壓縮,開啟目錄中的命令視窗,然後鍵入 bokeh serve --show bokeh_app
。 這將設定一個本地 Bokeh 伺服器併在瀏覽器中開啟該應用程式。
最終的產品
在進入細節之前,讓我們來看看我們的標的是什麼,這樣可以看到這些產品是如何組合在一起的。