Xây dựng Dashboard Phân tích Tài chính bằng Python & Streamlit (Web App)

1. Mục tiêu bài học

Sau khi hoàn thành bài học này, người học có thể:

  • Hiểu kiến trúc Web App Tài chính với Streamlit: Lấy dữ liệu trực tiếp từ SQLite Database để hiển thị dạng bảng tương tác và biểu đồ thời gian thực (Real-time).

  • Thiết kế hệ thống KPI Cards: Hiển thị các chỉ số cốt lõi (Doanh thu, Chi phí, Lợi nhuận thuần, Cash Flow) kèm biến động (Delta) so với kỳ trước.

  • Trực quan hóa Dữ liệu Tài chính: Sử dụng Plotly để vẽ biểu đồ doanh thu - chi phí, cơ cấu tài sản, và biểu đồ dòng tiền (Waterfall chart/Bar chart).

  • Xây dựng bộ lọc tương tác (Interactive Filters): Cho phép người dùng lọc báo cáo theo khoảng thời gian (Date Range) và xem chi tiết theo từng Tài khoản/Trung tâm chi phí.

2. Tổng quan về Streamlit trong Ứng dụng Kế toán

Streamlit là framework Python cho phép chuyển đổi các script dữ liệu thành ứng dụng Web tương tác mà không cần kiến thức về HTML/CSS/JavaScript.

┌─────────────────────────┐     ┌─────────────────────────┐     ┌─────────────────────────┐
│ SQLite Database         │ ──> │ Pandas & Query Engine   │ ──> │ Streamlit Dashboard     │
│ (Sổ nhật ký chung,      │     │ (Xử lý KPI, Aggregation)│     │ (KPIs, Plotly Charts,   │
│ Bảng cân đối tài khoản) │     │                         │     │ Data Tables, Filters)   │
└─────────────────────────┘     └─────────────────────────┘     └─────────────────────────┘

3. Mã nguồn Python: Xây dựng Dashboard Tài chính

Dưới đây là mã nguồn ứng dụng Streamlit hoàn chỉnh (app.py).

import sqlite3
import pandas as pd
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go

# ---------------------------------------------------------
# 1. CẤU HÌNH TRANG & CSS CƠ BẢN
# ---------------------------------------------------------
st.set_page_config(
    page_title="Financial Executive Dashboard",
    page_icon="📊",
    layout="wide"
)

st.title("📊 Financial Executive Dashboard - Năm 2026")
st.caption("Hệ thống Phân tích Chỉ số Tài chính & Báo cáo Ban Giám Đốc")

# ---------------------------------------------------------
# 2. HÀM TRUY VẤN DỮ LIỆU TỪ SQLITE
# ---------------------------------------------------------
@st.cache_data
def load_financial_data():
    """Mô phỏng/Truy vấn dữ liệu kế toán từ Database SQLite."""
    # Kết nối CSDL SQLite thực tế: conn = sqlite3.connect('accounting.db')
    
    # Tạo DataFrame mô phỏng kết quả P&L theo tháng
    data_pnl = {
        "Tháng": [f"Tháng {i}" for i in range(1, 10)],
        "Doanh Thu": [120000, 135000, 110000, 145000, 160000, 155000, 170000, 185000, 190000],
        "Giá Vốn": [72000, 81000, 68000, 87000, 96000, 93000, 102000, 111000, 114000],
        "Chi Phí Quản Lý": [25000, 26000, 24000, 27000, 28000, 28000, 29000, 30000, 31000],
    }
    df = pd.DataFrame(data_pnl)
    df["Lợi Nhuận Gộp"] = df["Doanh Thu"] - df["Giá Vốn"]
    df["Lợi Nhuận Thuần"] = df["Lợi Nhuận Gộp"] - df["Chi Phí Quản Lý"]
    return df

df_pnl = load_financial_data()

# ---------------------------------------------------------
# 3. SIDEBAR: BỘ LỌC TƯƠNG TÁC
# ---------------------------------------------------------
st.sidebar.header("⚙️ Bộ Lọc Báo Cáo")
selected_months = st.sidebar.multiselect(
    "Chọn Khoảng Thời Gian:",
    options=df_pnl["Tháng"].tolist(),
    default=df_pnl["Tháng"].tolist()
)

# Lọc dữ liệu theo tháng được chọn
df_filtered = df_pnl[df_pnl["Tháng"].isin(selected_months)]

if df_filtered.empty:
    st.warning("Vui lòng chọn ít nhất một tháng để hiển thị dữ liệu.")
    st.stop()

# ---------------------------------------------------------
# 4. KHU VỰC KPI CARDS (THÔNG SỐ CỐT LÕI)
# ---------------------------------------------------------
total_revenue = df_filtered["Doanh Thu"].sum()
total_cogs = df_filtered["Giá Vốn"].sum()
total_net_profit = df_filtered["Lợi Nhuận Thuần"].sum()
net_margin = (total_net_profit / total_revenue * 100) if total_revenue > 0 else 0

col1, col2, col3, col4 = st.columns(4)

with col1:
    st.metric(
        label="Tổng Doanh Thu", 
        value=f"{total_revenue:,.0f} VNĐ", 
        delta="+8.5% so với kỳ trước"
    )

with col2:
    st.metric(
        label="Tổng Giá Vốn", 
        value=f"{total_cogs:,.0f} VNĐ", 
        delta="-2.1%", 
        delta_color="inverse"
    )

with col3:
    st.metric(
        label="Lợi Nhuận Thuần", 
        value=f"{total_net_profit:,.0f} VNĐ", 
        delta="+12.3%"
    )

with col4:
    st.metric(
        label="Biên Lợi Nhuận Thuần", 
        value=f"{net_margin:.1f}%", 
        delta="+1.2%"
    )

st.divider()

# ---------------------------------------------------------
# 5. BIỂU ĐỒ PHÂN TÍCH (PLOTLY CHARTS)
# ---------------------------------------------------------
chart_col1, chart_col2 = st.columns(2)

with chart_col1:
    st.subheader("📈 Xu Hướng Doanh Thu & Lợi Nhuận")
    fig_trend = go.Figure()
    fig_trend.add_trace(go.Scatter(x=df_filtered["Tháng"], y=df_filtered["Doanh Thu"], mode='lines+markers', name='Doanh Thu', line=dict(color='#1F4E78', width=3)))
    fig_trend.add_trace(go.Bar(x=df_filtered["Tháng"], y=df_filtered["Lợi Nhuận Thuần"], name='Lợi Nhuận Thuần', marker_color='#2CA02C'))
    fig_trend.update_layout(barmode='overlay', xaxis_title="Kỳ Báo Cáo", yaxis_title="VNĐ", legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1))
    st.plotly_chart(fig_trend, use_container_width=True)

with chart_col2:
    st.subheader("🍰 Cơ Cấu Chi Phí & Lợi Nhuận")
    cost_structure = {
        "Hạng Mục": ["Giá Vốn Hàng Bán", "Chi Phí Quản Lý", "Lợi Nhuận Thuần"],
        "Giá Trị": [total_cogs, df_filtered["Chi Phí Quản Lý"].sum(), total_net_profit]
    }
    fig_pie = px.pie(
        cost_structure, 
        values="Giá Trị", 
        names="Hạng Mục", 
        hole=0.4,
        color_discrete_sequence=['#E377C2', '#FF7F0E', '#2CA02C']
    )
    st.plotly_chart(fig_pie, use_container_width=True)

# ---------------------------------------------------------
# 6. BẢNG DỮ LIỆU CHI TIẾT & XUẤT DATA
# ---------------------------------------------------------
st.subheader("📋 Bảng Chi Tiết Số Liệu Kế Toán")
st.dataframe(
    df_filtered.style.format({
        "Doanh Thu": "{:,.0f}",
        "Giá Vốn": "{:,.0f}",
        "Chi Phí Quản Lý": "{:,.0f}",
        "Lợi Nhuận Gộp": "{:,.0f}",
        "Lợi Nhuận Thuần": "{:,.0f}"
    }),
    use_container_width=True
)

# Chức năng tải xuống CSV
csv = df_filtered.to_csv(index=False).encode('utf-8')
st.download_button(
    label="📥 Tải Dữ Liệu Dạng CSV",
    data=csv,
    file_name="financial_report_data.csv",
    mime="text/csv"
)

4. Hướng dẫn Chạy Ứng dụng Web Streamlit

  1. Cài đặt thư viện phụ thuộc:

    Bash
    pip install streamlit pandas plotly
    
  2. Chạy ứng dụng Web: Lưu đoạn mã trên thành file app.py và thực thi lệnh sau trên Terminal / Command Prompt:

    Bash
    streamlit run app.py
    

    Ứng dụng sẽ tự động mở giao diện trình duyệt tại địa chỉ http://localhost:8501.

5. Câu hỏi suy ngẫm & Bài tập thực hành

Bài tập 1: Trắc nghiệm Kiến thức Streamlit

Để đảm bảo dữ liệu truy vấn từ SQLite không bị load lại liên tục mỗi khi người dùng thao tác trên giao diện Dashboard, chúng ta sử dụng decorator nào của Streamlit?

A. @st.experimental_rerun

B. @st.cache_data

C. @st.on_change

Bài tập 2: Lập trình Mở rộng

Hãy viết thêm một đoạn mã truy vấn vào app.py để lấy danh sách các Tài khoản Chi phí (Đầu 6) từ bảng GeneralLedger (trong SQLite DB đã tạo ở Bài 2.1) và tạo biểu đồ cột nằm ngang (Horizontal Bar Chart) hiển thị Top 5 Tài khoản Chi phí lớn nhất trong kỳ.

Đáp án gợi ý:

  • Bài tập 1:

    • Đáp án đúng là B (@st.cache_data). Decorator này lưu kết quả truy vấn vào bộ nhớ đệm (cache), tối ưu hiệu năng ứng dụng web khi làm việc với cơ sở dữ liệu lớn.

  • Bài tập 2:

import sqlite3
import pandas as pd
import plotly.express as px

# Kết nối SQLite và truy vấn Top 5 Chi phí
conn = sqlite3.connect('accounting.db')
query = """
    SELECT account_code, SUM(debit) as total_expense
    FROM general_ledger
    WHERE account_code LIKE '6%'
    GROUP BY account_code
    ORDER BY total_expense DESC
    LIMIT 5
"""
df_top_expenses = pd.read_sql_query(query, conn)
conn.close()

# Vẽ biểu đồ cột nằm ngang
fig_exp = px.bar(
    df_top_expenses, 
    x="total_expense", 
    y="account_code", 
    orientation='h',
    title="Top 5 Tài Khoản Chi Phí Lớn Nhất",
    labels={"total_expense": "Chi Phí (VNĐ)", "account_code": "Mã TK"}
)
st.plotly_chart(fig_exp, use_container_width=True)