🐍 Ngày 48 Python 365 ngày | Xây dựng dashboard với Streamlit
🗓 Ngày 48: Không cần biết HTML/CSS vẫn có web app
Ba ngày vừa rồi bạn phải học HTML, template, route. Streamlit xóa bỏ tất cả những thứ đó. Bạn viết Python thuần, Streamlit tự biến nó thành web app tương tác. Đây là công cụ được các nhà phân tích dữ liệu dùng nhiều nhất.
🎯 Nguyên tắc cốt lõi của Streamlit: script chạy lại từ trên xuống mỗi khi người dùng tương tác. Hiểu điều này là hiểu 80% Streamlit.
🎯 Mục tiêu bài học
- Cài đặt và chạy app Streamlit đầu tiên.
- Dùng widget: slider, selectbox, text input, file upload.
- Hiển thị dữ liệu:
dataframe,metric,table. - Vẽ chart bằng
st.line_chart, Plotly, Altair. - Hiểu cache và session_state — hai khái niệm quyết định hiệu năng.
- Layout:
columns,sidebar,tabs,expander. - Deploy lên Streamlit Community Cloud miễn phí.
🧰 Cài đặt
python -m venv .venv
.venv\Scripts\activate
pip install streamlit pandas plotly
streamlit run app.py
Trình duyệt tự mở http://localhost:8501.
🚀 Bước 1: App nhỏ nhất
# app.py
import streamlit as st
st.set_page_config(page_title="Phân tích Crypto", page_icon="📈", layout="wide")
st.title("📈 Dashboard Phân tích Crypto")
st.write("Ứng dụng demo cho chuỗi **Python 365 ngày**.")
ten = st.text_input("Tên của bạn", "Học viên")
st.write(f"Xin chào, {ten}! 👋")
Chỉ 6 dòng, bạn đã có web app có input và output.
🎛️ Bước 2: Widget — đầu vào của người dùng
import streamlit as st
import pandas as pd
from datetime import date
st.sidebar.header("⚙️ Bộ lọc")
symbol = st.sidebar.selectbox(
"Cặp giao dịch",
["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"],
index=0,
)
so_ngay = st.sidebar.slider("Số ngày dữ liệu", min_value=7, max_value=365, value=90, step=7)
khoang_thoi_gian = st.sidebar.date_input(
"Khoảng thời gian",
value=(date(2026, 1, 1), date(2026, 10, 1)),
)
chi_bao = st.sidebar.multiselect(
"Chỉ báo kỹ thuật",
["SMA 20", "SMA 50", "EMA 12", "RSI", "MACD"],
default=["SMA 20"],
)
hien_bang_du_lieu = st.sidebar.checkbox("Hiện bảng dữ liệu thô", value=False)
st.write(f"Đang phân tích **{symbol}** trong **{so_ngay}** ngày, chỉ báo: {chi_bao}")
Bảng widget phổ biến:
| Widget | Hàm | Trả về |
|---|---|---|
| Nút bấm | st.button() | bool |
| Hộp kiểm | st.checkbox() | bool |
| Chọn một | st.selectbox() | giá trị chọn |
| Chọn nhiều | st.multiselect() | list |
| Thanh trượt | st.slider() | int/float/tuple |
| Nhập số | st.number_input() | số |
| Nhập chữ | st.text_input() | str |
| Chọn ngày | st.date_input() | date |
| Tải file | st.file_uploader() | UploadedFile |
📊 Bước 3: Hiển thị dữ liệu và chỉ số
import yfinance as yf
@st.cache_data(ttl=300)
def lay_du_lieu(symbol: str, so_ngay: int) -> pd.DataFrame:
"""Tải dữ liệu giá. Cache 5 phút để không gọi lại liên tục."""
df = yf.download(symbol, period=f"{so_ngay}d", interval="1d", progress=False)
if df.empty:
return df
df.columns = [c[0] if isinstance(c, tuple) else c for c in df.columns]
return df
df = lay_du_lieu(symbol, so_ngay)
if df.empty:
st.error("❌ Không lấy được dữ liệu. Kiểm tra lại mã hoặc kết nối mạng.")
st.stop()
gia_moi_nhat = float(df["Close"].iloc[-1])
gia_truoc = float(df["Close"].iloc[-2])
thay_doi = (gia_moi_nhat - gia_truoc) / gia_truoc * 100
# Hàng chỉ số nổi bật
c1, c2, c3, c4 = st.columns(4)
c1.metric("Giá hiện tại", f"${gia_moi_nhat:,.2f}", f"{thay_doi:+.2f}%")
c2.metric("Cao nhất", f"${float(df['High'].max()):,.2f}")
c3.metric("Thấp nhất", f"${float(df['Low'].min()):,.2f}")
c4.metric("Khối lượng TB", f"{float(df['Volume'].mean()):,.0f}")
if hien_bang_du_lieu:
st.dataframe(df.tail(20), use_container_width=True)
st.metric tự động tô xanh khi delta dương và đỏ khi âm — đúng chuẩn dashboard tài chính.
📈 Bước 4: Vẽ biểu đồ
import plotly.graph_objects as go
# --- Cách 1: Chart dựng sẵn của Streamlit (nhanh nhất) ---
st.subheader("Giá đóng cửa")
st.line_chart(df["Close"])
# --- Cách 2: Plotly (tương tác đầy đủ: zoom, hover, tải ảnh) ---
fig = go.Figure()
fig.add_trace(go.Candlestick(
x=df.index, open=df["Open"], high=df["High"],
low=df["Low"], close=df["Close"], name="Nến",
))
for chi in chi_bao:
if chi == "SMA 20":
fig.add_trace(go.Scatter(x=df.index, y=df["Close"].rolling(20).mean(),
name="SMA 20", line=dict(width=1.5)))
elif chi == "SMA 50":
fig.add_trace(go.Scatter(x=df.index, y=df["Close"].rolling(50).mean(),
name="SMA 50", line=dict(width=1.5)))
fig.update_layout(
height=500,
xaxis_rangeslider_visible=False,
title=f"{symbol} — {so_ngay} ngày",
template="plotly_dark",
)
st.plotly_chart(fig, use_container_width=True)
| Cách vẽ | Khi nào dùng |
|---|---|
st.line_chart / st.bar_chart | Prototype nhanh, không cần tùy biến |
st.plotly_chart | Cần tương tác, nhiều trace, tooltip |
st.altair_chart | Chart tĩnh đẹp, code ngắn |
st.pyplot | Đồ thị Matplotlib có sẵn |
⚡ Bước 5: Cache — bắt buộc phải hiểu
Vì script chạy lại từ đầu mỗi lần tương tác, mọi tác vụ nặng sẽ chạy lại. Đây là vấn đề số 1 của người mới.
import time
import numpy as np
# ❌ KHÔNG CACHE: mỗi lần kéo slider là chờ 2 giây
def tinh_toan_cham(n: int):
time.sleep(2)
return np.random.rand(n).sum()
# ✅ CÓ CACHE: chỉ tính khi `n` thay đổi
@st.cache_data(ttl=600) # cache dữ liệu (DataFrame, dict, list)
def tinh_toan_nhanh(n: int):
time.sleep(2)
return np.random.rand(n).sum()
# ✅ CACHE TÀI NGUYÊN: dùng cho kết nối DB, model ML
@st.cache_resource
def tai_model():
# ví dụ: return joblib.load("model.pkl")
return {"ten": "model-gia-lap"}
Phân biệt hai loại cache:
| Decorator | Dùng cho | Ghi chú |
|---|---|---|
@st.cache_data | DataFrame, list, dict, số | Trả về bản sao — an toàn |
@st.cache_resource | Model ML, kết nối DB, client API | Trả về cùng object — chia sẻ |
⚠️ Tham số của hàm cache phải hashable. Truyền
listhaydictsẽ lỗi — hãy chuyển thànhtuplehoặcfrozenset.
🔄 Bước 6: Session State — nhớ trạng thái giữa các lần chạy lại
import streamlit as st
# Khởi tạo một lần
if "lich_su_symbol" not in st.session_state:
st.session_state.lich_su_symbol = []
if "so_lan_phan_tich" not in st.session_state:
st.session_state.so_lan_phan_tich = 0
if st.button("📌 Lưu cặp này vào lịch sử"):
if symbol not in st.session_state.lich_su_symbol:
st.session_state.lich_su_symbol.append(symbol)
st.session_state.so_lan_phan_tich += 1
st.success(f"Đã lưu {symbol}")
st.sidebar.caption(f"Số lần lưu: {st.session_state.so_lan_phan_tich}")
if st.session_state.lich_su_symbol:
st.sidebar.write("**Lịch sử:**")
for s in st.session_state.lich_su_symbol:
st.sidebar.write(f"- {s}")
Khi nào cần session_state?
- Giá trị phải tồn tại qua các lần rerun.
- Đếm số lần bấm nút (biến thường sẽ reset về 0 mỗi lần).
- Điều khiển widget từ nơi khác (dùng
key=).
🧩 Bước 7: Layout — sắp xếp giao diện
# Sidebar
st.sidebar.title("Bộ điều khiển")
# Tabs
tab1, tab2, tab3 = st.tabs(["📈 Biểu đồ", "📋 Dữ liệu", "🧮 Phân tích"])
with tab1:
st.plotly_chart(fig, use_container_width=True)
with tab2:
st.dataframe(df.tail(50), use_container_width=True)
with tab3:
st.subheader("Thống kê mô tả")
st.dataframe(df.describe(), use_container_width=True)
with st.expander("ℹ️ Giải thích các chỉ số"):
st.markdown("""
- **mean**: giá trị trung bình
- **std**: độ lệch chuẩn — đo mức biến động
- **50%**: trung vị (median)
""")
# Cột
cot_trai, cot_phai = st.columns([2, 1])
with cot_trai:
st.info("Vùng rộng gấp đôi")
with cot_phai:
st.success("Vùng hẹp hơn")
# Chia nhóm có viền
with st.container(border=True):
st.write("Nội dung trong khung")
📁 Bước 8: Multi-page app
app.py ← trang chủ
pages/
├── 1_📈_Phan_tich.py
├── 2_📊_So_sanh.py
└── 3_⚙️_Cai_dat.py
Streamlit tự tạo menu điều hướng từ tên file — không cần viết một dòng code nào.
🚀 Bước 9: Deploy miễn phí
- Push code lên GitHub (kèm
requirements.txt). - Vào https://share.streamlit.io → New app.
- Chọn repo, branch, file
app.py→ Deploy. - Nhận URL dạng
https://ten-app.streamlit.app.
pip freeze > requirements.txt
💡 Nếu cần đọc bí mật (API key), dùng
st.secrets:api_key = st.secrets["API_KEY"] # từ .streamlit/secrets.tomlKhông commit
secrets.tomllên Git.
🧪 Bài tập thực hành
- Thêm widget
st.file_uploadercho phép tải CSV và phân tích dữ liệu của chính bạn. - Thêm nút Tải xuống bằng
st.download_buttonđể xuất DataFrame ra CSV. - Vẽ thêm biểu đồ khối lượng bằng
st.bar_chart. - Tính và hiển thị RSI 14; tô đỏ vùng RSI > 70.
- Dùng
@st.cache_datacho hàm đọc dữ liệu và đo thời gian trước/sau bằngtime.perf_counter(). - Tạo trang thứ hai so sánh 2 cặp giao dịch.
⚠️ Lỗi thường gặp
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
UnhashableParamError | Truyền list/dict vào hàm cache | Đổi sang tuple/frozenset |
| App tự reset mỗi lần bấm | Dùng biến thường thay vì session_state | Dùng st.session_state |
| App rất chậm | Thiếu cache | Thêm @st.cache_data |
DuplicateWidgetID | Hai widget cùng label/key | Thêm key= duy nhất |
| Chart không hiện | Gọi st.plotly_chart ngoài with tab | Đặt đúng trong block |
ModuleNotFoundError khi deploy | Thiếu trong requirements.txt | pip freeze > requirements.txt |
📝 Ghi chú
- Streamlit chạy lại toàn bộ script mỗi tương tác — thiết kế code theo hướng idempotent (chạy lại vẫn đúng).
- Không phù hợp cho web nhiều người dùng phức tạp, cần phân quyền chi tiết → dùng Flask/Django/FastAPI.
- Rất phù hợp cho: dashboard nội bộ, demo model ML, công cụ phân tích cá nhân, prototype nhanh.
st.stop()hữu ích để dừng script sớm khi thiếu dữ liệu.- Streamlit có
st.chat_messagevàst.chat_input— xây app chatbot LLM cực nhanh.
🎯 Tóm tắt ngày 48
✅ Hiểu mô hình "rerun từ trên xuống" của Streamlit
✅ Dùng thành thạo widget đầu vào
✅ Hiển thị metric và chart tương tác Plotly
✅ Phân biệt cache_data vs cache_resource
✅ Quản lý trạng thái bằng session_state
✅ Tổ chức layout và multi-page app
✅ Deploy miễn phí lên Streamlit Cloud