Chuyển tới nội dung chính

🐍 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:

WidgetHàmTrả về
Nút bấmst.button()bool
Hộp kiểmst.checkbox()bool
Chọn mộtst.selectbox()giá trị chọn
Chọn nhiềust.multiselect()list
Thanh trượtst.slider()int/float/tuple
Nhập sốst.number_input()số
Nhập chữst.text_input()str
Chọn ngàyst.date_input()date
Tải filest.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_chartPrototype nhanh, không cần tùy biến
st.plotly_chartCần tương tác, nhiều trace, tooltip
st.altair_chartChart 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:

DecoratorDùng choGhi chú
@st.cache_dataDataFrame, list, dict, sốTrả về bản sao — an toàn
@st.cache_resourceModel ML, kết nối DB, client APITrả về cùng object — chia sẻ

⚠️ Tham số của hàm cache phải hashable. Truyền list hay dict sẽ lỗi — hãy chuyển thành tuple hoặc frozenset.


🔄 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í​

  1. Push code lên GitHub (kèm requirements.txt).
  2. Vào https://share.streamlit.io → New app.
  3. Chọn repo, branch, file app.py → Deploy.
  4. 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.toml

Không commit secrets.toml lên Git.


🧪 Bài tập thực hành​

  1. Thêm widget st.file_uploader cho phép tải CSV và phân tích dữ liệu của chính bạn.
  2. Thêm nút Tải xuống bằng st.download_button để xuất DataFrame ra CSV.
  3. Vẽ thêm biểu đồ khối lượng bằng st.bar_chart.
  4. Tính và hiển thị RSI 14; tô đỏ vùng RSI > 70.
  5. Dùng @st.cache_data cho hàm đọc dữ liệu và đo thời gian trước/sau bằng time.perf_counter().
  6. Tạo trang thứ hai so sánh 2 cặp giao dịch.

⚠️ Lỗi thường gặp​

LỗiNguyên nhânCách sửa
UnhashableParamErrorTruyền list/dict vào hàm cacheĐổi sang tuple/frozenset
App tự reset mỗi lần bấmDùng biến thường thay vì session_stateDùng st.session_state
App rất chậmThiếu cacheThêm @st.cache_data
DuplicateWidgetIDHai widget cùng label/keyThêm key= duy nhất
Chart không hiệnGọi st.plotly_chart ngoài with tabĐặt đúng trong block
ModuleNotFoundError khi deployThiếu trong requirements.txtpip 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_message và 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


📚 Học tiếp​