Python Viz 5 — Altair & Grammar of Graphics

13 thg 7, 2026 3 lượt xem
#python
#altair
#grammar-of-graphics
#vega-lite

Bài 1 — Tổng quan ta đã xếp các thư viện theo mục đích. Matplotlib cho bạn điều khiển từng nét vẽ theo lối imperative (ra lệnh từng bước: vẽ trục, vẽ cột, đặt nhãn). Plotly cho biểu đồ tương tác web. Altair đứng ở một góc tư duy khác hẳn: declarative — bạn không nói "vẽ thế nào" mà nói "vẽ CÁI GÌ", tức là mô tả ánh xạ từ cột dữ liệu sang thuộc tính thị giác, rồi để Vega-Lite tự lo phần dựng hình. Đây chính là hiện thân Python của grammar of graphics (ngữ pháp đồ hoạ), cùng dòng tư tưởng với ggplot2 bên R.

Imperative vs Declarative: khác biệt cốt lõi

Với matplotlib, một biểu đồ là một chuỗi mệnh lệnh: ax.bar(...), rồi ax.set_xticklabels(...), rồi ax.legend(). Bạn chịu trách nhiệm về thứ tựcách dựng từng thành phần. Ưu điểm là kiểm soát tuyệt đối; nhược điểm là mã dài, khó đọc lại "biểu đồ này đang nói gì về dữ liệu".

Với Altair, bạn khai báo một đặc tả (specification): "cột chi_nhanh ánh xạ vào trục x, cột du_no ánh xạ vào trục y, cột nhom_no ánh xạ vào màu". Bạn không viết vòng lặp, không tính vị trí cột, không sắp màu thủ công. Máy đọc đặc tả đó và tự sinh hình. Hệ quả thực tế:

  • Mã ngắn và tự mô tả — đọc .encode(x="chi_nhanh", y="du_no", color="nhom_no") là hiểu ngay ý nghĩa, không cần đọc 20 dòng thủ tục.
  • Đổi biểu đồ = đổi ánh xạ — muốn chuyển từ so sánh sang phân phối, bạn đổi mark và vài dòng encode, không viết lại từ đầu.
  • Nhất quán — cùng một ngữ pháp áp cho mọi loại biểu đồ, không phải nhớ API riêng cho từng loại như matplotlib.

Cái giá phải trả: khi cần hiệu ứng đặc thù ngoài ngữ pháp (chú thích lạ, layout in tinh chỉnh), declarative gò bó hơn imperative.

Cú pháp Altair: Chart → mark → encode

Khung xương của mọi biểu đồ Altair:

import altair as alt

alt.Chart(df).mark_bar().encode(
    x="chi_nhanh:N",
    y="du_no:Q",
    color="nhom_no:N",
    tooltip=["chi_nhanh:N", "du_no:Q"],
)

Ba bộ phận:

  1. alt.Chart(df) — gắn dữ liệu nguồn (thường là một pandas DataFrame). Altair sẽ tuần tự hoá dữ liệu vào JSON.
  2. .mark_*() — chọn hình học (geometric mark): mark_bar, mark_line, mark_point, mark_circle, mark_area, mark_rect (heatmap), mark_boxplot, mark_text. Mark là "nét vẽ" — cột, đường, điểm.
  3. .encode(...)ánh xạ cột dữ liệu vào kênh thị giác (visual channel): x, y, color, size, shape, opacity, tooltip, column, row, theta (góc, cho pie).

Chính bước encode là linh hồn của grammar of graphics: mỗi kênh là một trục thông tin. Thêm color= là thêm một chiều dữ liệu vào biểu đồ mà không cần vẽ thêm gì thủ công.

Kiểu dữ liệu encoding — vì sao cực kỳ quan trọng

Đằng sau mỗi tên cột trong encode có một kiểu dữ liệu (encoding type), viết bằng hậu tố hai ký tự:

Ký hiệuTênÝ nghĩaVí dụ cột
:QQuantitativeSố định lượng, liên tụcdu_no, so_du, amount
:NNominalDanh nghĩa, không thứ tựchi_nhanh, currency, kind
:OOrdinalThứ tự, có xếp hạngnhom_no (1..5), khoang_tuoi
:TTemporalThời giancreated_at, ngay_gd

Kiểu dữ liệu quyết định cách Altair xử lý kênh đó, không chỉ là chú thích:

  • :Q → trục liên tục, chia vạch số, dùng thang màu sequential (gradient) khi map vào color.
  • :N → trục rời rạc, mỗi giá trị một ô/màu riêng, dùng bảng màu categorical.
  • :O → rời rạc nhưng giữ thứ tự khi sắp trục và khi tô màu (thang màu có hướng).
  • :T → Altair hiểu là ngày/giờ, tự parse, cho phép timeUnit (gộp theo tháng/quý) và định dạng trục thời gian.

Nhầm kiểu là lỗi kinh điển. Cột nhom_no chứa số 1–5 nếu để :Q sẽ bị vẽ như trục liên tục 0–5 với gradient màu; để đúng :O mới ra 5 nhóm rời rạc có thứ tự. Cột nam (2023, 2024) để :Q sẽ hiện "2,023.5" trên trục — phải :O hoặc :N. Nếu bỏ hậu tố, Altair đoán từ dtype của pandas, thường đúng với số và ngày nhưng dễ sai với mã danh mục dạng số. Quy tắc an toàn: luôn ghi rõ hậu tố.

Grammar = mark + encoding + transform + scale

Điểm mạnh thứ hai của Altair là transform ngay trong khai báo: bạn không cần biến đổi DataFrame trước bằng pandas rồi mới vẽ, mà mô tả phép biến đổi thành một mắt xích của đặc tả. Vega-Lite thực thi biến đổi đó lúc render.

  • Aggregatey="mean(du_no):Q" hoặc alt.Y("du_no:Q", aggregate="sum"): tự gộp và tính tổng/trung bình theo nhóm x.
  • Binalt.X("so_du:Q", bin=alt.Bin(maxbins=30)): tự chia khoảng để dựng histogram.
  • Filter.transform_filter(alt.datum.currency == "VND"): lọc dòng trong đặc tả.
  • Calculate.transform_calculate(du_no_ty="datum.du_no / 1e9"): tạo cột dẫn xuất.
  • Window.transform_window(rank="rank()", sort=[...]): xếp hạng, luỹ kế, tương tự window function trong SQL.
  • Timeunit — gộp created_at theo yearmonth để vẽ chuỗi tháng.

Ngoài ra mỗi kênh có thể tinh chỉnh qua scale/axis/legend:

alt.Y("du_no:Q",
      scale=alt.Scale(type="log"),
      axis=alt.Axis(title="Dư nợ (log)", format="~s"))

Sơ đồ dưới đây mô tả đường đi của dữ liệu qua ngữ pháp Altair — đây chính là mô hình grammar of graphics:

Điểm cần nhớ ở sơ đồ: selection (vùng người dùng chọn bằng chuột) có thể quay ngược lại tác động vào transform (lọc) hoặc encoding (đổi màu/độ mờ) — đây là gốc rễ của tính tương tác khai báo mà ta sẽ nói ngay dưới.

Ghép biểu đồ: layer, concat, facet, repeat

Ngữ pháp Altair cho phép kết hợp biểu đồ bằng toán tử, không cần API subplot phức tạp:

  • Layer (chồng)chart1 + chart2: chồng hai lớp lên cùng hệ trục. Ví dụ: cột doanh số + đường trung bình động; hoặc điểm scatter + đường hồi quy.
  • Concat ngangchart1 | chart2: đặt cạnh nhau (horizontal concatenation). Concat dọcchart1 & chart2: xếp trên dưới. Dùng ghép các biểu đồ khác loại cạnh nhau.
  • Facet.facet(column="currency:N") hoặc encode column=/row=: nhân bản cùng một biểu đồ cho từng giá trị của một cột (small multiples). Mỗi loại tiền một panel riêng.
  • Repeat.repeat(["du_no", "so_du", "so_gd"]): lặp biểu đồ qua nhiều cột khác nhau, tiện dựng ma trận scatter (SPLOM) để soi tương quan từng cặp biến.

Phân biệt: facet lặp theo giá trị của một cột; repeat lặp theo tên nhiều cột.

Interactivity khai báo — đặc sản của Altair

Đây là nơi Altair toả sáng so với phần còn lại. Tương tác trong Altair không phải viết callback JavaScript, mà khai báo một selection rồi tham chiếu nó trong encoding. Vega-Lite biên dịch thành tương tác chạy trong trình duyệt.

  • alt.selection_interval() — người dùng quét chọn một vùng (khoảng x-y).
  • alt.selection_point() — chọn theo điểm/nhóm khi click (ví dụ click legend để chọn một danh mục).
  • .interactive() — bật nhanh zoom/pan cho biểu đồ.

Sức mạnh thật nằm ở cross-filter: một selection ở biểu đồ A dùng làm điều kiện lọc/tô cho biểu đồ B. Chọn một khoảng ở biểu đồ này, biểu đồ kia lập tức phản ứng — mà chỉ bằng vài dòng đặc tả:

import altair as alt

# brush: vùng người dùng quét chọn trên biểu đồ cột
brush = alt.selection_interval(encodings=["x"])

base = alt.Chart(df)  # df: giao dịch theo chi nhánh & tháng

# Biểu đồ 1 — cột số giao dịch theo tháng, có thể quét chọn khoảng tháng
bars = base.mark_bar().encode(
    x="yearmonth(created_at):T",
    y="count():Q",
    color=alt.condition(brush, alt.value("#1f77b4"), alt.value("lightgray")),
).add_params(brush)

# Biểu đồ 2 — scatter dư nợ vs số dư, CHỈ hiện các điểm thuộc khoảng đã chọn ở biểu đồ 1
points = base.mark_circle().encode(
    x="so_du:Q",
    y="du_no:Q",
    color="chi_nhanh:N",
    tooltip=["chi_nhanh:N", "du_no:Q", "so_du:Q"],
).transform_filter(brush)

dashboard = bars & points   # ghép dọc, liên kết qua brush

Ở đoạn trên, brush được thêm vào biểu đồ cột (add_params) và dùng lại ở scatter qua transform_filter(brush). Không có dòng JavaScript nào; toàn bộ liên kết là khai báo. Quét chọn Q1/2026 trên cột, scatter chỉ còn chi nhánh Q1. Kiểu "liên kết nhiều biểu đồ" này làm bằng Plotly Dash cần callback, còn Altair gói gọn trong đặc tả.

Ví dụ transform aggregate thuần khai báo (minh hoạ) — trung bình dư nợ theo nhóm nợ, tự gộp:

alt.Chart(df).mark_bar().encode(
    x="nhom_no:O",
    y="mean(du_no):Q",         # aggregate ngay trong encoding
    tooltip=[alt.Tooltip("mean(du_no):Q", format=",.0f")],
)

Vega-Lite / Vega: nền tảng và BI-as-code

Altair chỉ là lớp Python sinh ra JSON. Mỗi biểu đồ Altair biên dịch thành một Vega-Lite specification — một đối tượng JSON mô tả toàn bộ biểu đồ. Bạn lấy ra bằng chart.to_json() hoặc chart.to_dict().

Ý nghĩa thực tế rất lớn với đội ngũ BI:

  • BI-as-code — spec JSON là artefact versioning trong Git, review qua pull request, tái dùng. Cùng một spec render được ở Python lẫn JavaScript (vega-embed).
  • Nhúng web — dán spec vào trang HTML với vega-embed là có biểu đồ tương tác, không cần server Python. Hợp để nhúng portal nội bộ.
  • Tách người vẽ khỏi runtime — analyst sinh spec bằng Altair, frontend nhúng vào ứng dụng thật.

Vega-Lite là một grammar cấp cao; bên dưới là Vega (linh hoạt hơn nhưng dài dòng hơn). Altair → Vega-Lite đủ cho hầu hết nhu cầu.

Khi nào Altair, khi nào Plotly/Matplotlib

Tiêu chíAltairPlotlyMatplotlib
Tư duyDeclarative (grammar)Declarative (Express) / objectImperative
Tương tácKhai báo, cross-filter mạnhRất mạnh, có DashGần như không
Liên kết nhiều biểu đồDễ nhất (selection)Cần Dash + callbackKhông
Dữ liệu lớnYếu (mặc định ≤ 5000 dòng)Tốt hơn (WebGL)Tốt (ảnh tĩnh)
Xuất in/PDFĐược nhưng không phải thế mạnhĐược (kaleido)Tốt nhất
Reuse spec (BI-as-code)JSON chuẩn hoáJSON nhưng nặngKhông

Chọn nhanh: cần khám phá liên kết nhiều biểu đồ hoặc muốn spec tái dùng → Altair. Cần dashboard web quy mô, dữ liệu lớn, ứng dụng đầy đủ → Plotly/Dash. Cần hình tĩnh chất lượng in cho báo cáo → Matplotlib. Xem thêm hướng chọn biểu đồ ở chart selection và thiết kế dashboard ở dashboard design.

Hạn chế cần nhớ về dữ liệu lớn

Đây là bẫy hay gặp nhất. Vì Altair nhúng thẳng dữ liệu vào spec JSON, mặc định nó chặn ở 5000 dòng (MaxRowsError). Đừng ném cả bảng transactions triệu dòng vào Altair. Cách xử lý đúng:

  1. Tổng hợp trước bằng SQL/pandas/Polars rồi mới vẽ (thường biểu đồ chỉ cần vài trăm dòng đã gộp) — xem Polars.
  2. Nếu thật cần nhiều dòng, dùng data server hoặc alt.data_transformers.enable("vegafusion") để đẩy transform sang engine ngoài.
  3. Chỉ nới alt.data_transformers.disable_max_rows() khi bạn chắc chắn kích thước còn kiểm soát được — nới bừa sẽ tạo file HTML nặng hàng chục MB.

Việc "tổng hợp trước" hợp tự nhiên với sandbox SQL. Ví dụ chuẩn bị dữ liệu cho biểu đồ cột số dư theo loại tiền, kết quả chỉ vài dòng — quá đủ cho Altair:

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

Hoặc gộp giao dịch theo tháng để làm nguồn cho biểu đồ đường/cột thời gian (:T) — vẫn nhỏ gọn:

-- ▶ Chạy được
SELECT date_trunc('month', created_at) AS thang,
       kind,
       COUNT(*) AS so_gd,
       ROUND(SUM(amount)::numeric, 2) AS tong_tien
FROM transactions
GROUP BY date_trunc('month', created_at), kind
ORDER BY thang;

Đưa kết quả hai truy vấn này vào DataFrame là có ngay nguồn sạch, nhỏ, đúng kiểu cho Altair.

Use case thực tế

Bối cảnh (NCB). Phòng Phân tích Rủi ro cần một công cụ khám phá liên kết cho analyst tự soi danh mục dư nợ 42 chi nhánh trong 24 tháng gần nhất, thay cho việc gửi qua lại hàng chục file Excel. Yêu cầu: chọn một khoảng thời gian ở biểu đồ này thì các biểu đồ khác tự lọc theo, không cần lập trình frontend, và spec phải nhúng được vào portal nội bộ.

Giải pháp bằng Altair — dashboard 3 view liên kết:

  1. Nguồn dữ liệu (đã tổng hợp). Một job chạy đêm gộp transactions/accounts về mức (chi nhánh × tháng × nhóm nợ), ra khoảng 42 × 24 × 5 ≈ 5.000 dòng — sau khi lọc còn ~3.000 dòng, nằm dưới ngưỡng MaxRowsError. Bài học: luôn gộp ở tầng SQL/Polars trước, đừng để Altair nuốt bảng thô.
  2. View A — cột số giao dịch theo tháng, gắn selection_interval(encodings=["x"]). Đây là "bộ điều khiển thời gian".
  3. View B — scatter dư nợ vs số dư theo chi nhánh, transform_filter(brush): chỉ hiện chi nhánh trong khoảng tháng đã chọn ở View A.
  4. View C — heatmap (mark_rect) nhóm nợ × chi nhánh, tô màu bằng tỷ lệ nợ xấu (:Q, thang sequential), cùng chịu lọc từ brush.
  5. Ghép A & (B | C), xuất to_json(), nhúng vào portal bằng vega-embed.

Kết quả ước lượng (minh hoạ, không phải số đo thật): analyst quét chọn 3 tháng cao điểm cuối năm, cả B và C tự lọc, thời gian trả lời một câu hỏi "chi nhánh nào tăng nợ nhóm 3 trong quý này" giảm từ khoảng 15–20 phút thao tác Excel xuống dưới 1 phút. Spec JSON được đưa vào Git, review như code, tái dùng cho báo cáo tháng — đúng tinh thần BI-as-code. So với phương án Plotly Dash, đội tiết kiệm được phần viết callback vì liên kết cross-filter đã nằm trong đặc tả. Về nguyên tắc kể chuyện dữ liệu cho lãnh đạo, tham khảo thêm metrics & KPI.

Ghi nhớ

  • Declarative, không imperative: mô tả "vẽ CÁI GÌ" (ánh xạ dữ liệu → kênh thị giác), để Vega-Lite lo "vẽ THẾ NÀO". Khác matplotlib ra lệnh từng bước.
  • Khung xương: alt.Chart(df).mark_*().encode(...) — mark là hình học, encode là ánh xạ cột vào x/y/color/size/tooltip.
  • Kiểu encoding quyết định hành vi: :Q định lượng (trục liên tục, màu gradient), :N danh nghĩa (rời rạc, màu categorical), :O thứ tự (rời rạc có hướng), :T thời gian (parse + timeUnit). Luôn ghi rõ hậu tố; nhầm kiểu là lỗi kinh điển (mã số danh mục để :Q).
  • Grammar = mark + encoding + transform + scale/axis/legend. Transform (aggregate/filter/bin/window/timeUnit) nằm ngay trong đặc tả, không cần xử lý DataFrame trước.
  • Ghép biểu đồ bằng toán tử: + layer, | concat ngang, & concat dọc, facet (1 biểu đồ × N giá trị), repeat (1 biểu đồ × N cột).
  • Đặc sản Altair — interactivity khai báo: selection_interval/point, .interactive(), và cross-filter bằng cách tham chiếu selection trong transform_filter/condition. Liên kết nhiều biểu đồ chỉ bằng vài dòng, không cần callback JS.
  • Altair sinh Vega-Lite JSON → BI-as-code: versioning, review, nhúng web bằng vega-embed.
  • Hạn chế dữ liệu lớn: mặc định chặn 5000 dòng (MaxRowsError) vì nhúng dữ liệu vào spec. Luôn tổng hợp trước bằng SQL/pandas/Polars; chỉ nới giới hạn khi chắc kích thước.
  • Chọn công cụ: khám phá liên kết / spec tái dùng → Altair; app web dữ liệu lớn → Plotly/Dash; hình in báo cáo → Matplotlib.

Nguồn tham khảo

  • Vega-Altair Documentation — tài liệu chính thức của Altair (Chart, mark, encode, transform, selection, compound charts).
  • Vega-Lite Documentation — đặc tả grammar Vega-Lite mà Altair biên dịch sang.
  • Satyanarayan, Moritz, Wongsuphasawat, Heer (2017). "Vega-Lite: A Grammar of Interactive Graphics." IEEE Transactions on Visualization and Computer Graphics (Proc. InfoVis 2016).
  • Leland Wilkinson (2005). The Grammar of Graphics, 2nd ed. Springer — nền tảng lý thuyết grammar of graphics.
  • Hadley Wickham (2010). "A Layered Grammar of Graphics." Journal of Computational and Graphical Statistics, 19(1) — cách hiện thực grammar theo lớp (nền của ggplot2 và tư duy Altair).

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

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ọ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

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ẻ!