Giới Thiệu
Trong thế giới khoa học dữ liệu, việc chuyển từ một notebook Jupyter tĩnh sang một ứng dụng web động và tương tác là một bước ngoặt lớn. Điều này cho phép các bên liên quan khám phá dữ liệu, kiểm tra giả thuyết và thu thập thông tin một cách độc lập. Trong khi các công cụ như Tableau hay Power BI có vị trí của chúng, một cách tiếp cận dựa trên mã nguồn bằng Python mang lại sự linh hoạt và sức mạnh không thể sánh bằng.
Bài viết này sẽ đi sâu vào ba thư viện Python mạnh mẽ để xây dựng bảng điều khiển và báo cáo: Streamlit, Dash và Bokeh. Chúng ta sẽ khám phá triết lý đằng sau từng công cụ, xây dựng một bảng điều khiển tương tác đơn giản với cả ba và hướng dẫn triển khai ứng dụng lên đám mây, kèm theo một kho lưu trữ GitHub và tự động hóa CI/CD.
🚀 Các Đối Thủ
Chúng ta hãy gặp gỡ ba framework trực quan hóa này. Mỗi công cụ có cách tiếp cận riêng để biến mã Python thành ứng dụng web tương tác.
1. Streamlit ✨
Giới thiệu: Cách nhanh nhất để xây dựng và chia sẻ ứng dụng dữ liệu.
Streamlit là công cụ ưu tiên cho các nhà khoa học dữ liệu muốn tạo ra những ứng dụng đẹp mắt và chức năng với ít nỗ lực và không cần suy nghĩ về phát triển web truyền thống. Triết lý cốt lõi của nó là sự đơn giản. Bạn viết một kịch bản Python như bình thường, và Streamlit tự động chạy lại mã của bạn từ đầu đến cuối mỗi khi người dùng tương tác với một widget.
Ưu điểm:
- ✅ Dễ học.
- ✅ Ít mã mẫu.
- ✅ Cập nhật tự động khi tương tác với widget.
- ✅ Hệ sinh thái phong phú với các thành phần tùy chỉnh.
Nhược điểm:
- ❌ Mô hình "chạy lại mọi thứ" có thể không hiệu quả cho các ứng dụng phức tạp hoặc chạy lâu.
- ❌ Ít kiểm soát hơn về bố cục và kiểu dáng chi tiết so với các công cụ khác.
2. Plotly Dash 📊
Giới thiệu: Xây dựng ứng dụng web phân tích cho Python. Không cần JavaScript.
Dash, được tạo ra bởi đội ngũ đứng sau thư viện biểu đồ Plotly, là một framework mạnh mẽ để xây dựng các ứng dụng sẵn sàng cho sản xuất. Nó cung cấp một "canvas trắng" có cấu trúc hơn, nơi bạn định nghĩa bố cục bằng các lớp Python mô phỏng HTML và sau đó kết nối các thành phần tương tác bằng các "callback" rõ ràng.
Ưu điểm:
- ✅ Bố cục có thể tùy chỉnh và linh hoạt cao.
- ✅ Có thể mở rộng cho các ứng dụng phức tạp, đa trang.
- ✅ Xuất sắc cho các ứng dụng yêu cầu quản lý trạng thái chính xác.
- ✅ Là một phần của hệ sinh thái Plotly mạnh mẽ.
Nhược điểm:
- ❌ Đường cong học tập dốc hơn với nhiều mã mẫu hơn.
- ❌ Cần hiểu sâu hơn về các khái niệm như bố cục và callback.
3. Bokeh 🎨
Giới thiệu: Trực quan hóa tương tác cho các trình duyệt web hiện đại.
Bokeh là, về cốt lõi, một thư viện trực quan hóa, nhưng nó đi kèm với một thành phần máy chủ mạnh mẽ cho phép bạn xây dựng các ứng dụng tương tác đầy đủ. Nó xuất sắc trong việc xử lý các tập dữ liệu lớn và dữ liệu streaming một cách hiệu quả. Điểm mạnh của nó nằm ở sự kiểm soát chi tiết mà nó cung cấp cho bạn về từng yếu tố biểu đồ và các công cụ liên kết và chọn dữ liệu mạnh mẽ.
Ưu điểm:
- ✅ Xuất sắc cho tính tương tác hiệu suất cao trên các tập dữ liệu lớn.
- ✅ Cung cấp mức độ kiểm soát cao đối với thiết kế và tương tác biểu đồ.
- ✅ Có thể được sử dụng như một thư viện độc lập hoặc với máy chủ của nó cho các ứng dụng đầy đủ.
Nhược điểm:
- ❌ Có thể dài dòng hơn để tạo cấu trúc bảng điều khiển đầy đủ so với Streamlit.
- ❌ API có thể cảm thấy ít "Pythonic" hơn ban đầu so với Streamlit.
🐧 Dự Án Demo: Khám Phá Chim Cánh Cụt Palmer
Để so sánh các công cụ này, chúng ta sẽ xây dựng cùng một ứng dụng đơn giản trong cả ba: một trình khám phá biểu đồ phân tán cho tập dữ liệu nổi tiếng về chim cánh cụt Palmer. Người dùng sẽ có thể chọn loài để hiển thị và chọn các biến cho các trục X và Y.
Trước tiên, hãy cài đặt các thư viện của chúng ta:
bash
pip install streamlit pandas plotly-express dash bokeh
1. Ví dụ Streamlit
Hãy chú ý vào sự sạch sẽ và dễ đọc của mã này. Chúng ta sử dụng st.sidebar để đặt các widget của mình và khu vực chính cho biểu đồ. Mã đọc như một kịch bản đơn giản.
python
import streamlit as st
import pandas as pd
import plotly.express as px
# Tải dữ liệu
@st.cache_data # Lưu cache dữ liệu để cải thiện hiệu suất
def load_data():
df = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv')
df.dropna(inplace=True)
return df
df = load_data()
# --- Cấu hình trang ---
st.set_page_config(
page_title="🐧 Khám Phá Chim Cánh Cụt Palmer",
page_icon="🧊",
layout="centered"
)
st.title("🐧 Khám Phá Chim Cánh Cụt Palmer")
st.markdown("Khám phá tập dữ liệu chim cánh cụt Palmer bằng **Streamlit**.")
# --- Thanh bên cho đầu vào của người dùng ---
st.sidebar.header("📊 Điều Khiển Biểu Đồ")
# Chọn loài
species_list = ['All'] + sorted(df['species'].unique().tolist())
selected_species = st.sidebar.selectbox("Chọn Loài", species_list)
# Chọn trục
numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns.tolist()
x_axis = st.sidebar.selectbox("Chọn Trục X", numeric_columns, index=numeric_columns.index('bill_length_mm'))
y_axis = st.sidebar.selectbox("Chọn Trục Y", numeric_columns, index=numeric_columns.index('bill_depth_mm'))
# --- Lọc dữ liệu ---
if selected_species != 'All':
filtered_df = df[df['species'] == selected_species]
else:
filtered_df = df
# --- Hiển thị biểu đồ ---
st.subheader(f"Biểu Đồ Phân Tán: {x_axis} so với {y_axis}")
if not filtered_df.empty:
fig = px.scatter(
filtered_df,
x=x_axis,
y=y_axis,
color='species',
hover_name='species',
title=f'Mối quan hệ giữa {x_axis} và {y_axis}'
)
st.plotly_chart(fig, use_container_width=True)
else:
st.warning("Không có dữ liệu cho loài đã chọn.")
st.markdown("---")
st.write("Nguồn Dữ Liệu:")
st.dataframe(filtered_df.head())
2. Ví dụ Dash
Dash có cấu trúc hơn. Chúng ta định nghĩa một bố cục tĩnh và sau đó tạo một hàm @app.callback lắng nghe các thay đổi từ đầu vào (hộp chọn loài, hộp chọn trục X, hộp chọn trục Y) và cập nhật đầu ra (biểu đồ phân tán chim cánh cụt).
python
import dash
from dash import dcc, html, Input, Output
import pandas as pd
import plotly.express as px
# Tải dữ liệu
df = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv')
df.dropna(inplace=True)
# --- Khởi tạo ứng dụng ---
app = dash.Dash(__name__)
server = app.server # Phơi bày máy chủ để triển khai
numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns.tolist()
species_list = [{'label': 'All', 'value': 'All'}] + [{'label': s, 'value': s} for s in sorted(df['species'].unique())]
# --- Bố cục ứng dụng ---
app.layout = html.Div(style={'fontFamily': 'sans-serif'}, children=[
html.H1("🐧 Khám Phá Chim Cánh Cụt Palmer (Dash)", style={'textAlign': 'center'}),
html.P("Khám phá tập dữ liệu chim cánh cụt Palmer bằng Plotly Dash.", style={'textAlign': 'center'}),
html.Div(style={'display': 'flex', 'padding': '20px'}, children=[
# Div điều khiển
html.Div(style={'width': '25%', 'paddingRight': '20px'}, children=[
html.H3("📊 Điều Khiển Biểu Đồ"),
html.Label("Chọn Loài"),
dcc.Dropdown(id='species-dropdown', options=species_list, value='All'),
html.Br(),
html.Label("Chọn Trục X"),
dcc.Dropdown(id='x-axis-dropdown', options=[{'label': i, 'value': i} for i in numeric_columns], value='bill_length_mm'),
html.Br(),
html.Label("Chọn Trục Y"),
dcc.Dropdown(id='y-axis-dropdown', options=[{'label': i, 'value': i} for i in numeric_columns], value='bill_depth_mm'),
]),
# Div biểu đồ
html.Div(style={'width': '75%'}, children=[
dcc.Graph(id='penguin-scatter-plot')
])
])
])
# --- Callback cho tính tương tác ---
@app.callback(
Output('penguin-scatter-plot', 'figure'),
[Input('species-dropdown', 'value'),
Input('x-axis-dropdown', 'value'),
Input('y-axis-dropdown', 'value')]
)
def update_graph(selected_species, x_axis, y_axis):
if selected_species == 'All' or selected_species is None:
filtered_df = df
else:
filtered_df = df[df['species'] == selected_species]
fig = px.scatter(
filtered_df,
x=x_axis,
y=y_axis,
color='species',
hover_name='species',
title=f'Mối quan hệ giữa {x_axis} và {y_axis}'
)
fig.update_layout(transition_duration=500)
return fig
# --- Chạy ứng dụng ---
if __name__ == '__main__':
app.run_server(debug=True)
3. Ví dụ Bokeh
Bokeh yêu cầu chúng ta suy nghĩ nhiều hơn về các nguồn dữ liệu và cách các glyph (như hình tròn) được cập nhật. Ở đây, chúng ta tạo một ColumnDataSource và một hàm callback sẽ sửa đổi dữ liệu trong nguồn này khi một widget thay đổi.
python
import pandas as pd
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Select
from bokeh.layouts import column, row
from bokeh.palettes import Category10_3
# Tải dữ liệu
df = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv')
df.dropna(inplace=True)
# Phân loại loài thành màu sắc
species_unique = sorted(df['species'].unique())
color_map = {species: Category10_3[i] for i, species in enumerate(species_unique)}
df['color'] = df['species'].map(color_map)
# Tạo một ColumnDataSource
source = ColumnDataSource(data=df)
# --- Tạo các widget ---
numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns.tolist()
species_list = ['All'] + species_unique
x_axis_select = Select(title="Chọn Trục X", value="bill_length_mm", options=numeric_columns)
y_axis_select = Select(title="Chọn Trục Y", value="bill_depth_mm", options=numeric_columns)
species_select = Select(title="Chọn Loài", value="All", options=species_list)
# --- Tạo biểu đồ ---
p = figure(height=500, width=700, title="Biểu Đồ Phân Tán Chim Cánh Cụt", tooltips=[("Loài", "@species"), ("X", "@x"), ("Y", "@y")])
p.circle(x="x", y="y", source=source, size=10, color="color", legend_field="species")
p.xaxis.axis_label = x_axis_select.value
p.yaxis.axis_label = y_axis_select.value
# --- Định nghĩa Callback ---
def update_plot(attr, old, new):
# Lọc dữ liệu dựa trên lựa chọn loài
if species_select.value == 'All':
filtered_df = df
else:
filtered_df = df[df.species == species_select.value]
# Cập nhật dữ liệu nguồn
source.data = {
'x': filtered_df[x_axis_select.value],
'y': filtered_df[y_axis_select.value],
'species': filtered_df['species'],
'color': filtered_df['color']
}
# Cập nhật nhãn trục
p.xaxis.axis_label = x_axis_select.value
p.yaxis.axis_label = y_axis_select.value
# Gắn callback vào thuộc tính 'value' của mỗi widget
for widget in [x_axis_select, y_axis_select, species_select]:
widget.on_change('value', update_plot)
# --- Sắp xếp bố cục ---
controls = column(species_select, x_axis_select, y_axis_select, width=200)
layout = row(controls, p)
# Khởi tạo dữ liệu biểu đồ
update_plot(None, None, None)
curdoc().add_root(layout)
curdoc().title = "🐧 Khám Phá Chim Cánh Cụt Palmer (Bokeh)"
📦 Kho Lưu Trữ GitHub
Một triển khai hoạt động có sẵn tại đây: GitHub Repository
✅ Kết Luận: Bạn Nên Sử Dụng Công Cụ Nào?
- Chọn Streamlit nếu: Bạn là một nhà khoa học dữ liệu cần xây dựng một công cụ đẹp mắt và tương tác nhanh chóng. Bạn coi trọng sự đơn giản và tốc độ hơn kiểm soát chi tiết. Thích hợp cho các mẫu, công cụ nội bộ và demo mô hình ML.
- Chọn Dash nếu: Bạn đang xây dựng một ứng dụng phức tạp, sẵn sàng cho sản xuất yêu cầu bố cục cụ thể, chức năng đa trang và quản lý trạng thái mạnh mẽ. Bạn thoải mái với nhiều mã mẫu và kiến trúc dựa trên callback.
- Chọn Bokeh nếu: Nhu cầu chính của bạn là biểu đồ tương tác cao, hiệu suất cao, đặc biệt với các tập dữ liệu lớn hoặc streaming. Bạn muốn kiểm soát chi tiết đối với các trực quan hóa của mình và thoải mái xây dựng UI xung quanh chúng.