Python Viz 5 — Altair & Grammar of Graphics
Ở 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ự và 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
markvà vài dòngencode, 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:
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..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..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ệu | Tên | Ý nghĩa | Ví dụ cột |
|---|---|---|---|
:Q | Quantitative | Số định lượng, liên tục | du_no, so_du, amount |
:N | Nominal | Danh nghĩa, không thứ tự | chi_nhanh, currency, kind |
:O | Ordinal | Thứ tự, có xếp hạng | nhom_no (1..5), khoang_tuoi |
:T | Temporal | Thời gian | created_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éptimeUnit(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.
- Aggregate —
y="mean(du_no):Q"hoặcalt.Y("du_no:Q", aggregate="sum"): tự gộp và tính tổng/trung bình theo nhóm x. - Bin —
alt.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_attheoyearmonthđể 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 ngang —
chart1 | chart2: đặt cạnh nhau (horizontal concatenation). Concat dọc —chart1 & 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 encodecolumn=/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-embedlà 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í | Altair | Plotly | Matplotlib |
|---|---|---|---|
| Tư duy | Declarative (grammar) | Declarative (Express) / object | Imperative |
| Tương tác | Khai báo, cross-filter mạnh | Rất mạnh, có Dash | Gần như không |
| Liên kết nhiều biểu đồ | Dễ nhất (selection) | Cần Dash + callback | Không |
| Dữ liệu lớn | Yế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ặng | Khô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:
- 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.
- 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. - 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:
- Nguồn dữ liệu (đã tổng hợp). Một job chạy đêm gộp
transactions/accountsvề 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ưỡngMaxRowsError. Bài học: luôn gộp ở tầng SQL/Polars trước, đừng để Altair nuốt bảng thô. - 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". - 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. - 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. - Ghép
A & (B | C), xuấtto_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àox/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),:Ndanh nghĩa (rời rạc, màu categorical),:Othứ tự (rời rạc có hướng),:Tthờ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 trongtransform_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.
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.
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.
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.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!