Python Viz 7 — Streamlit: biến script thành data app

13 thg 7, 2026 3 lượt xem
#dashboard
#python
#data-app
#streamlit

Bài 4 — Plotly bạn đã có biểu đồ tương tác chạy trong trình duyệt, nhưng vẫn thiếu một mảnh: giao diện điều khiển. Một biểu đồ Plotly đơn lẻ vẫn cần ai đó sửa code rồi chạy lại để đổi tham số. Người dùng nghiệp vụ không làm được điều đó. Streamlit lấp đúng khoảng trống này: nó biến một file .py thành một web app có ô chọn chi nhánh, thanh trượt kỳ, nút bấm — mà bạn không cần viết một dòng HTML, CSS hay JavaScript nào.

Vì sao Streamlit

Câu chào hàng của Streamlit rất cụ thể: biến script phân tích thành công cụ nội bộ tương tác trong vài giờ. Bạn viết Python như đang viết một notebook, thay print() bằng st.write(), thêm vài widget, và có ngay một app chạy được trên trình duyệt mà đồng nghiệp chỉ cần mở link để dùng.

Điều này quan trọng vì phần lớn nhu cầu trong một team dữ liệu ngân hàng không phải là dashboard "đẹp cấp Ban điều hành", mà là công cụ nội bộ: một màn hình lọc dư nợ theo chi nhánh, một máy tính what-if cho lãi suất, một trang tra soát giao dịch nghi ngờ. Những thứ này nếu chờ đội BI dựng trên Power BI thì mất hàng tuần và qua nhiều vòng yêu cầu; còn analyst tự dựng bằng Streamlit thì gói gọn trong một ngày.

So sánh nhanh với các lựa chọn Python khác:

Công cụĐiểm mạnhĐiểm yếuHợp cho
StreamlitNhanh nhất để có app; API tối giảnMô hình rerun dễ gây bất ngờ về hiệu năngDashboard/công cụ nội bộ, prototype
Dash (Plotly)Kiểm soát callback chi tiết, tuỳ biến layout sâuNhiều "boilerplate", học lâu hơnApp phức tạp, tương tác tinh vi
GradioCực nhanh cho demo ML (input→output)Không hợp dashboard nhiều thành phầnDemo mô hình, form dự đoán

Nguyên tắc chọn: cần một công cụ có nhiều bộ lọc và biểu đồ để tự phục vụ → Streamlit. Cần demo một mô hình (nhập hồ sơ, ra điểm tín dụng) → Gradio. Cần kiểm soát tương tác phức tạp giữa nhiều thành phần → Dash.

Mô hình chạy: rerun từ trên xuống

Đây là khái niệm quan trọng nhất, và cũng là nơi người mới hay sai. Streamlit không dùng mô hình callback (mỗi widget gắn một hàm xử lý). Thay vào đó:

Mỗi khi người dùng tương tác với bất kỳ widget nào, Streamlit chạy lại toàn bộ script từ dòng đầu đến dòng cuối.

Lần chạy lại đó gọi là một rerun. Widget không "gọi hàm"; nó chỉ trả về giá trị hiện tại của nó. Khi bạn kéo thanh trượt, script chạy lại, và tại dòng st.slider(...) nó trả về giá trị mới; mọi dòng phía dưới dùng giá trị đó được tính lại và vẽ lại.

Hệ quả thực tế: nếu ngay đầu script bạn có df = pd.read_sql(big_query, conn), thì mỗi lần người dùng động vào bất kỳ widget nào, câu query nặng đó chạy lại. Đây chính là lý do caching không phải tuỳ chọn mà là bắt buộc (mục dưới).

Widget — nhận đầu vào

Mỗi widget là một lời gọi hàm trả về giá trị người dùng đang chọn:

import streamlit as st

chi_nhanh = st.selectbox("Chi nhánh", ["Tất cả", "Hà Nội", "HCM", "Đà Nẵng"])
nguong   = st.slider("Ngưỡng dư nợ (tỷ)", 0, 500, 50)
tu_ngay  = st.date_input("Từ ngày")
loc      = st.multiselect("Nhóm nợ", [1, 2, 3, 4, 5], default=[1, 2])
if st.button("Xuất báo cáo"):
    st.download_button("Tải CSV", data=df.to_csv(), file_name="baocao.csv")
file = st.file_uploader("Nạp file danh mục", type=["csv", "xlsx"])

Các widget hay dùng: st.selectbox / st.multiselect (chọn từ danh sách), st.slider (dải số), st.date_input (ngày/khoảng ngày), st.text_input / st.number_input, st.button (trả True đúng một rerun khi vừa bấm), st.checkbox, st.radio, st.file_uploader (nhận file người dùng tải lên).

Hiển thị — xuất kết quả

Đối xứng với widget là các lệnh hiển thị:

  • st.dataframe(df) — bảng tương tác (sắp xếp, cuộn); st.table(df) — bảng tĩnh.
  • st.plotly_chart(fig) — nhúng figure Plotly (nối thẳng Bài 4); st.pyplot(fig) cho matplotlib; st.altair_chart(chart) cho Altair.
  • st.metric(label, value, delta) — ô KPI lớn kèm mũi tên tăng/giảm.
  • st.map(df) — bản đồ điểm nhanh (cần cột lat/lon), nối Bài 6 — Geospatial.
  • st.write() / st.markdown() — văn bản, và st.write "thông minh" tự nhận diện kiểu dữ liệu.

Caching — chống chạy lại vô ích

Vì mỗi rerun chạy lại cả script, ta phải nói cho Streamlit biết cái gì không cần tính lại. Có hai decorator, và chọn đúng cái là điểm mấu chốt:

DecoratorDùng choCơ chếVí dụ
@st.cache_dataDữ liệu trả về (DataFrame, dict, số)Trả về bản sao — an toàn khi nhiều phiênKết quả query, file đã đọc, bảng đã tính
@st.cache_resourceTài nguyên dùng chung, không nên nhân bảnTrả về cùng một đối tượng (singleton)Kết nối DB, client, mô hình ML đã nạp
@st.cache_data(ttl=600)          # nhớ 10 phút rồi làm mới
def load_du_no(chi_nhanh: str, tu: str, den: str) -> pd.DataFrame:
    return pd.read_sql(QUERY, get_conn(), params=(chi_nhanh, tu, den))

@st.cache_resource                # một kết nối dùng chung mọi rerun
def get_conn():
    return duckdb.connect("warehouse.duckdb", read_only=True)

Cách hoạt động: Streamlit băm (hash) các tham số đầu vào của hàm. Nếu lần rerun sau gọi hàm với cùng tham số, nó trả kết quả đã lưu, bỏ qua thân hàm. Đổi chi_nhanh → tham số khác → hàm thực chạy lại; giữ nguyên → lấy từ cache. Tham số ttl đặt thời gian sống; max_entries giới hạn số bản ghi để khỏi phình bộ nhớ.

Lỗi kinh điển: đặt @st.cache_data lên hàm trả về kết nối DB. Vì cache_data trả bản sao, kết nối sẽ bị nhân bản/serialize sai → dùng cache_resource. Ngược lại, đừng dùng cache_resource cho DataFrame vì các phiên sẽ chia sẻ cùng một object và sửa của người này ảnh hưởng người kia.

session_state — giữ trạng thái qua các rerun

Vì script chạy lại từ đầu, mọi biến thường bị khởi tạo lại mỗi rerun. Khi cần nhớ thứ gì đó xuyên suốt phiên của một người dùng (bộ đếm, danh sách đã chọn tích luỹ, kết quả trung gian), dùng st.session_state — một dict tồn tại suốt phiên:

if "lan_chay" not in st.session_state:
    st.session_state.lan_chay = 0
if st.button("Tính lại"):
    st.session_state.lan_chay += 1
st.write(f"Đã tính {st.session_state.lan_chay} lần")

Phân biệt: cache dùng chung cho mọi người dùng (dữ liệu giống nhau thì chia sẻ được); session_state riêng cho từng phiên/từng người.

Layout — bố cục màn hình

Mặc định các phần tử xếp dọc từ trên xuống. Bốn công cụ bố cục hay dùng:

  • st.sidebar — cột trái cố định, thường để bộ lọc (st.sidebar.selectbox(...)).
  • st.columns(n) — chia hàng ngang, hợp để đặt hàng KPI cạnh nhau.
  • st.tabs([...]) — nhiều tab trong cùng trang (Tổng quan / Chi tiết / Dữ liệu thô).
  • st.expander — khối gập/mở để giấu bớt (ví dụ bảng dữ liệu chi tiết).

Kết nối dữ liệu

Streamlit chỉ lo phần giao diện; dữ liệu đến từ đâu là quyết định của bạn. Ba nguồn phổ biến trong bối cảnh ngân hàng:

  1. Data warehouse / DB (Postgres, Oracle, Snowflake) — dùng pd.read_sql hoặc st.connection. Query phải tham số hoá (truyền qua params=), tuyệt đối không nối chuỗi từ input người dùng (chống SQL injection).
  2. File Parquet / CSV trên data lake — đọc bằng Polars (nhanh, tiết kiệm RAM) hoặc pandas.
  3. DuckDB — engine SQL nhúng, query thẳng file Parquet cực nhanh mà không cần server. Rất hợp làm "backend" cho một Streamlit app nội bộ: đặt file dữ liệu cạnh app, DuckDB đọc trực tiếp.

Mẫu chuẩn: filter đẩy xuống nguồn. Đừng nạp cả bảng 50 triệu dòng vào app rồi lọc bằng pandas trong RAM; hãy đưa lựa chọn của widget vào mệnh đề WHERE của query để DB/DuckDB chỉ trả về phần cần. Cột chọn (chi nhánh, kỳ) trở thành tham số và cũng chính là khoá cache — nhờ vậy các lựa chọn hay dùng được phục vụ tức thì.

Ví dụ một câu SQL nguồn hợp lệ trên sandbox Postgres — tổng dư nợ (số dư) theo loại tiền tệ, đúng schema demo:

-- ▶ Chạy được
SELECT currency,
       COUNT(*)              AS so_tai_khoan,
       ROUND(SUM(balance), 2) AS tong_so_du
FROM accounts
GROUP BY currency
ORDER BY tong_so_du DESC;

Trong app thật, câu trên sẽ có thêm WHERE gắn với giá trị widget và được bọc trong hàm @st.cache_data.

Ví dụ: dashboard dư nợ theo chi nhánh (minh hoạ)

Ghép các mảnh lại thành một app hoàn chỉnh. Đoạn dưới là minh hoạ (giả định có bảng du_no trong DuckDB với các cột chi_nhanh, ky, du_no, no_xau):

import streamlit as st
import duckdb
import plotly.express as px

st.set_page_config(page_title="Dư nợ theo chi nhánh", layout="wide")

@st.cache_resource
def get_con():
    return duckdb.connect("warehouse.duckdb", read_only=True)

@st.cache_data(ttl=900)
def load(chi_nhanh: str, ky_tu: str, ky_den: str):
    sql = """
        SELECT chi_nhanh, ky,
               SUM(du_no)  AS du_no,
               SUM(no_xau) AS no_xau
        FROM du_no
        WHERE ky BETWEEN ? AND ?
          AND (? = 'Tất cả' OR chi_nhanh = ?)
        GROUP BY chi_nhanh, ky
        ORDER BY ky
    """
    return get_con().execute(
        sql, [ky_tu, ky_den, chi_nhanh, chi_nhanh]
    ).df()

# --- Bộ lọc ở sidebar ---
st.sidebar.header("Bộ lọc")
cn  = st.sidebar.selectbox("Chi nhánh",
        ["Tất cả", "Hà Nội", "HCM", "Đà Nẵng", "Cần Thơ"])
ky1 = st.sidebar.text_input("Kỳ từ", "2025-01")
ky2 = st.sidebar.text_input("Kỳ đến", "2025-06")

df = load(cn, ky1, ky2)          # gọi 1 lần, có cache

# --- Hàng KPI ---
tong   = df["du_no"].sum()
xau    = df["no_xau"].sum()
ty_le  = (xau / tong * 100) if tong else 0
c1, c2, c3 = st.columns(3)
c1.metric("Tổng dư nợ (tỷ)", f"{tong/1e9:,.1f}")
c2.metric("Nợ xấu (tỷ)",     f"{xau/1e9:,.1f}")
c3.metric("Tỷ lệ nợ xấu", f"{ty_le:.2f}%",
          delta=f"{ty_le-2.0:.2f}% so với ngưỡng 2%",
          delta_color="inverse")

# --- Biểu đồ & dữ liệu ---
tab1, tab2 = st.tabs(["Xu hướng", "Dữ liệu thô"])
with tab1:
    fig = px.bar(df, x="ky", y="du_no", color="chi_nhanh",
                 title="Dư nợ theo kỳ")
    st.plotly_chart(fig, use_container_width=True)
with tab2:
    st.dataframe(df, use_container_width=True)

Chạy bằng streamlit run app.py. Ba yếu tố đáng chú ý: hai lớp cache tách bạch kết nối và dữ liệu; filter đẩy vào WHERE có tham số hoá; st.metric cho KPI kèm delta so ngưỡng cảnh báo 2%.

Triển khai & bảo mật

Đây là phần không thể xem nhẹ trong ngân hàng, vì một Streamlit app đọc thẳng dữ liệu khách hàng.

Nơi chạy: có hai hướng — Streamlit Community Cloud (host miễn phí, tiện demo) không dùng cho dữ liệu thật vì đẩy dữ liệu ra ngoài; với NCB bắt buộc self-host trong mạng nội bộ (một container sau reverse proxy, hoặc trên Kubernetes).

Xác thực & phân quyền: Streamlit không có hệ thống đăng nhập/phân quyền tích hợp đủ mạnh. Phải bọc bằng một lớp bên ngoài:

  • Đặt sau reverse proxy (Nginx/OAuth2-proxy) gắn SSO của tổ chức → chỉ người đăng nhập nội bộ vào được.
  • Phân quyền dữ liệu: một cán bộ chi nhánh Hà Nội chỉ được xem dữ liệu chi nhánh mình. Điều này phải áp ở tầng query (lọc theo danh tính người dùng lấy từ header SSO), không phải chỉ ẩn widget — ẩn UI không ngăn được người biết cách gọi thẳng.
  • Không lộ PII: hạn chế hiển thị số CMND/CCCD, số tài khoản đầy đủ; ưu tiên số liệu tổng hợp.

Tài nguyên: vì rerun có thể kích hoạt query nặng, cần đặt ttl/max_entries cho cache, giới hạn CPU/RAM của container, và cân nhắc số phiên đồng thời (Streamlit chạy các phiên trên cùng tiến trình server — không hợp với hàng nghìn người dùng cùng lúc).

Khi nào Streamlit đủ, khi nào cần BI chuẩn

Streamlit đủ khi: người dùng ít (chục người nội bộ), cần nhanh, logic phân tích phức tạp (dễ viết bằng Python), vòng đời công cụ ngắn hoặc hay đổi. Nó không thay được nền tảng BI như Power BI hay Superset khi: cần phục vụ hàng trăm–nghìn người, cần phân quyền theo hàng/cột tinh vi có sẵn, cần lịch làm mới và quản trị tập trung, cần người dùng nghiệp vụ tự kéo-thả tạo báo cáo. Nói ngắn: Streamlit là công cụ của analyst, BI chuẩn là nền tảng của tổ chức.

Cạm bẫy hiệu năng cần nhớ: (1) quên cache → mỗi rerun query lại; (2) nạp cả bảng lớn vào RAM rồi mới lọc thay vì đẩy filter xuống nguồn; (3) đặt sai loại cache cho kết nối; (4) tính toán nặng đặt ngoài hàm cache nên chạy mọi rerun. Bốn lỗi này chiếm gần hết các trường hợp "app chạy chậm".

Use case thực tế

Bối cảnh (số liệu minh hoạ): Phòng Phân tích rủi ro NCB cần một công cụ theo dõi dư nợ và nợ xấu theo chi nhánh, cho phép chạy kịch bản what-if: "nếu nâng ngưỡng phân loại nợ nhóm 2, tỷ lệ nợ xấu toàn hàng thay đổi ra sao?". Theo quy trình cũ, yêu cầu này gửi đội BI, xếp hàng ~2–3 tuần vì đội BI đang tồn đọng nhiều việc.

Cách làm bằng Streamlit (trong ~1 ngày):

  1. Analyst dựng bảng dữ liệu Parquet từ warehouse (dư nợ theo chi nhánh × kỳ × nhóm nợ), đặt cạnh app, dùng DuckDB đọc trực tiếp.
  2. Sidebar: chọn chi nhánh, khoảng kỳ, và thanh trượt ngưỡng nhóm nợ cho kịch bản what-if.
  3. Hàng KPI (st.metric): tổng dư nợ, nợ xấu, tỷ lệ nợ xấu kèm delta so ngưỡng cảnh báo 2%.
  4. Hai tab: biểu đồ xu hướng (Plotly) và bảng dữ liệu chi tiết cho phép tải CSV.
  5. @st.cache_data(ttl=900) cho query — các lựa chọn hay dùng phản hồi tức thì; @st.cache_resource cho kết nối DuckDB.
  6. Triển khai: container nội bộ sau OAuth2-proxy gắn SSO; query tự lọc theo chi nhánh mà người đăng nhập được phép xem.

Kết quả (ước lượng): thời gian từ yêu cầu đến công cụ dùng được rút từ ~2–3 tuần xuống ~1 ngày; ~15 cán bộ rủi ro tự phục vụ, không còn gửi email xin "cắt số" lặp lại; đội BI được giải phóng cho các báo cáo cấp tổ chức. Khi công cụ chứng minh giá trị và cần phục vụ rộng hơn, nó được chuyển giao cho đội BI tái dựng trên nền tảng chuẩn — Streamlit đóng đúng vai bàn đạp prototype.

Ghi nhớ

  • Streamlit biến script Python thuần thành web app tương tác, không cần frontend — nhanh nhất để dựng dashboard/công cụ nội bộ.
  • Mô hình rerun: mỗi tương tác chạy lại toàn bộ script từ đầu; widget chỉ trả giá trị, không gọi callback. Đây là gốc rễ mọi bất ngờ về hiệu năng.
  • Caching là bắt buộc, không tuỳ chọn: @st.cache_data cho dữ liệu (trả bản sao), @st.cache_resource cho kết nối/mô hình (singleton). Tham số hàm = khoá cache.
  • st.session_state giữ trạng thái riêng từng phiên; cache thì chia sẻ mọi người dùng — đừng nhầm hai thứ.
  • Đẩy filter xuống nguồn (WHERE, tham số hoá), dùng DuckDB/Polars; đừng nạp bảng lớn vào RAM rồi lọc.
  • Ngân hàng: self-host nội bộ, xác thực qua SSO/reverse proxy, phân quyền ở tầng query (không chỉ ẩn UI), không lộ PII, giới hạn tài nguyên. Community Cloud chỉ cho demo, không cho dữ liệu thật.
  • Streamlit hợp prototype & công cụ analyst; khi cần phục vụ diện rộng, phân quyền tinh vi, quản trị tập trung thì chuyển sang BI chuẩn (Power BI/Superset).
  • Xem thêm Tổng quan Python Viz để đặt Streamlit vào bức tranh công cụ chung.

Nguồn tham khảo

  • Streamlit Documentation — tài liệu chính thức (API reference, get started).
  • Streamlit Documentation — Develop: Concepts → Running your app / App model (mô hình rerun từ trên xuống).
  • Streamlit Documentation — Caching and state: st.cache_data & st.cache_resource, st.session_state.
  • Streamlit Documentation — Layouts and containers: st.sidebar, st.columns, st.tabs, st.expander.
  • Plotly Python Open Source Graphing Library — figure dùng với st.plotly_chart.
  • DuckDB Documentation — engine SQL nhúng, đọc Parquet trực tiếp.
  • Polars User Guide — đọc/xử lý dữ liệu dạng cột hiệu năng cao.

Bài viết liên quan

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 6

Học cách tổ chức code Python: định nghĩa hàm với tham số vị trí/từ khoá/mặc định, *args/**kwargs, lambda và hàm bậc cao, closure, decorator, generator với yield. Đóng gói code thành module và package, cô lập thư viện bằng môi trường ảo venv, quản lý phụ thuộc với pip và requirements.txt để dự án tái lập được trên mọi máy.

13 thg 7, 2026 5

Biến script thành pipeline đáng tin cậy: cấu trúc project & packaging (uv/poetry), type hints & pydantic, kiểm thử với pytest, logging & cấu hình, đóng gói Docker, và tích hợp CI cho code dữ liệu.

13 thg 7, 2026 5

Hướng dẫn OOP trong Python từ class/instance, kế thừa và super(), đa hình & duck typing, encapsulation tới dunder methods, @property, classmethod/staticmethod, dataclass và type hints (mypy). Kèm nguyên tắc clean code: đặt tên rõ nghĩa, hàm nhỏ, DRY, SOLID cùng chuẩn PEP8 với công cụ ruff/black.

13 thg 7, 2026 5

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!