Python Viz 2 — Matplotlib: nền tảng kiểm soát toàn diện
Nếu Bài 1 — Tổng quan giúp bạn chọn đúng thư viện cho từng tình huống, thì Matplotlib là chỗ bạn quay về mỗi khi cần kiểm soát tuyệt đối từng chi tiết của một biểu đồ. Nó ra đời năm 2003, là thư viện vẽ hình nền tảng của cả hệ sinh thái Python: seaborn, pandas .plot(), và phần lớn output tĩnh của scikit-learn đều dựng trên Matplotlib. Hiểu Matplotlib không phải để "vẽ đẹp nhanh" — cái đó seaborn làm tốt hơn — mà để khi cần một biểu đồ báo cáo đúng chuẩn thương hiệu, đúng định dạng tiền tệ, xuất PDF sắc nét nhúng vào tài liệu gửi Ban điều hành, bạn biết chính xác phải chỉnh gì.
Kiến trúc: Figure / Axes / Artist
Mọi hình trong Matplotlib đều là cây phân cấp ba tầng. Nắm được mô hình này thì 80% sự bối rối khi dùng thư viện sẽ biến mất.
- Figure là cả tờ giấy: đặt kích thước (
figsize), độ phân giải (dpi), màu nền, và có thể chứa nhiều Axes. - Axes (số nhiều của "axis" nhưng ở đây nghĩa là một vùng biểu đồ, đừng nhầm với trục) là nơi dữ liệu thực sự được vẽ. Một Figure có thể chứa 1 hay nhiều Axes. Mỗi Axes có hệ toạ độ riêng, tiêu đề riêng, hai (hoặc nhiều)
Axis. - Axis là trục X/Y: quản lý giới hạn, vị trí tick, và định dạng nhãn tick.
- Artist là khái niệm bao trùm: bất cứ thứ gì hiển thị được đều là một Artist — đường (
Line2D), cột (Rectangle/Bar), chữ (Text), chú giải (Legend), khung trục (Spine). Khi bạn gọiax.plot(...)thực chất là tạo ra Artist và gắn vào Axes.
OO API vs pyplot state-based — và vì sao chọn OO
Matplotlib có hai phong cách viết. Kiểu pyplot state-based thao tác trên "hình hiện hành" ngầm định:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [4, 5, 6])
plt.title("Doanh số")
plt.show()
Kiểu này gọn cho việc thử nhanh trong notebook, nhưng nguy hiểm ở chỗ mọi lệnh plt.* tác động lên một trạng thái toàn cục — khi có nhiều subplot, hoặc khi hàm này gọi hàm kia, bạn không còn chắc "hình hiện hành" là cái nào.
Kiểu hướng đối tượng (OO) tạo tường minh Figure và Axes rồi gọi method trên chúng:
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [4, 5, 6])
ax.set_title("Doanh số")
fig.savefig("out.png")
plt.subplots() trả về một Figure và một (hoặc mảng) Axes. Từ đó mọi thao tác đều đi qua ax cụ thể. Đây là phong cách khuyến nghị chính thức của Matplotlib vì:
- Rõ ràng, không mơ hồ: luôn biết đang vẽ lên Axes nào — quan trọng khi có nhiều subplot.
- Đóng gói được: viết hàm
def ve_bieu_do(ax, df): ...nhận Axes làm tham số, tái sử dụng ở bất kỳ đâu trong layout. - Kiểm soát chi tiết: hầu hết tính năng nâng cao chỉ lộ ra ở cấp object (
ax.xaxis.set_major_formatter(...)).
Quy tắc thực dụng: dùng plt.subplots() để tạo, dùng plt.style / plt.rcParams để cấu hình toàn cục, còn lại thao tác qua ax và fig.
Các loại biểu đồ cơ bản
Tất cả đều là method trên ax:
| Loại | Method | Dùng khi |
|---|---|---|
| Đường (line) | ax.plot(x, y) | Xu hướng theo thời gian |
| Cột (bar) | ax.bar(x, h) / ax.barh | So sánh giữa các nhóm rời rạc |
| Phân tán | ax.scatter(x, y) | Quan hệ giữa hai biến liên tục |
| Histogram | ax.hist(data, bins=...) | Phân phối một biến |
| Boxplot | ax.boxplot(data) | Phân phối + ngoại lệ, so nhóm |
| Vùng (area) | ax.fill_between(x, y) | Nhấn khối lượng tích luỹ |
| Tròn (pie) | ax.pie(sizes) | Hạn chế dùng — xem bên dưới |
Về pie chart: mắt người kém trong so sánh diện tích/góc, nên biểu đồ tròn chỉ nên dùng khi có 2–3 lát và tỷ lệ chênh rõ. Với "tỷ trọng dư nợ theo 8 chi nhánh", một biểu đồ cột ngang (barh) sắp xếp giảm dần luôn dễ đọc hơn. Nguyên tắc chọn dạng biểu đồ được bàn kỹ ở Chọn loại biểu đồ.
Nhiều subplot và trục đôi
plt.subplots(nrows, ncols) trả mảng Axes để bố trí lưới đều:
fig, axes = plt.subplots(2, 2, figsize=(10, 7))
axes[0, 0].plot(...)
axes[1, 1].bar(...)
fig.tight_layout() # tự dãn khoảng cách tránh chồng nhãn
Khi lưới không đều (một biểu đồ lớn, hai nhỏ), dùng gridspec:
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(10, 6))
gs = gridspec.GridSpec(2, 2)
ax_big = fig.add_subplot(gs[:, 0]) # cột trái chiếm cả 2 hàng
ax_top = fig.add_subplot(gs[0, 1])
ax_bot = fig.add_subplot(gs[1, 1])
Trục đôi (twinx) khi hai chuỗi khác đơn vị dùng chung trục X — ví dụ dư nợ (tỷ đồng) và số lượng hồ sơ (đếm):
ax2 = ax.twinx() # chia sẻ trục X, thêm trục Y bên phải
ax.bar(x, du_no) # trục trái
ax2.plot(x, so_ho_so, "r-o") # trục phải
Lưu ý: trục đôi dễ gây hiểu nhầm vì hai thang đo độc lập — chỉ dùng khi hai chuỗi thực sự liên quan và luôn ghi rõ đơn vị mỗi trục.
Tuỳ biến biểu đồ
Đây là phần Matplotlib toả sáng. Mọi thành phần đều chỉnh được.
Nhãn, tiêu đề, legend:
ax.set_xlabel("Chi nhánh")
ax.set_ylabel("Dư nợ (tỷ VND)")
ax.set_title("Dư nợ cho vay theo chi nhánh — Q2/2026")
ax.legend(loc="upper right", frameon=False)
Định dạng tick — số tiền và ngày tháng. Đây là chỗ báo cáo ngân hàng hay sai. Dùng FuncFormatter để hiển thị "1.500 tỷ" thay vì "1500000000000":
from matplotlib.ticker import FuncFormatter
import matplotlib.dates as mdates
# định dạng tiền: chia tỷ, thêm dấu phân cách nghìn kiểu VN
def dinh_dang_ty(x, pos):
return f"{x/1e9:,.0f} tỷ".replace(",", ".")
ax.yaxis.set_major_formatter(FuncFormatter(dinh_dang_ty))
# định dạng ngày trên trục thời gian
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m/%Y"))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=1))
Màu và colormap. Truyền color="#0F4C81" cho màu cố định, hoặc dùng colormap cho dữ liệu liên tục (cmap="viridis" — an toàn cho người mù màu, không dùng "jet"). Với dữ liệu phân loại, gán bảng màu thương hiệu cố định để mỗi chi nhánh luôn một màu qua mọi báo cáo.
Style/theme. plt.style.use("seaborn-v0_8-whitegrid") đổi toàn bộ diện mạo trong một dòng. Xem danh sách bằng plt.style.available. Có thể chồng nhiều style; style đứng sau ghi đè trước.
Annotate, giới hạn trục, lưới:
ax.annotate("Đỉnh Q2", xy=(x_peak, y_peak), xytext=(x_peak, y_peak*1.15),
arrowprops=dict(arrowstyle="->"))
ax.set_ylim(0, None) # BẮT ĐẦU TỪ 0 cho biểu đồ cột (xem cạm bẫy)
ax.grid(True, axis="y", alpha=0.3)
ax.spines[["top", "right"]].set_visible(False) # bỏ khung thừa
Định dạng chuyên nghiệp và chuẩn hoá thương hiệu
Ba yếu tố quyết định biểu đồ trông "chuyên nghiệp" hay "notebook nháp": cỡ chữ nhất quán, độ phân giải đủ, và nhất quán giữa mọi biểu đồ.
Xuất file — chọn định dạng theo mục đích:
fig.savefig("bao_cao.png", dpi=200, bbox_inches="tight") # nhúng slide/web
fig.savefig("bao_cao.pdf", bbox_inches="tight") # in ấn, vector
fig.savefig("bao_cao.svg") # chỉnh sửa tiếp
- PNG: ảnh raster, cần
dpicao (150–300) cho bản in; nhẹ, phổ biến. - SVG/PDF: ảnh vector, sắc nét ở mọi cỡ phóng — bắt buộc cho tài liệu in và báo cáo chính thức.
bbox_inches="tight"cắt sát viền, tránh lề trắng thừa.
rcParams — trái tim của tính nhất quán thương hiệu. Thay vì lặp lại font, màu, cỡ ở từng biểu đồ, đặt một lần ở đầu module hoặc trong file matplotlibrc:
import matplotlib as mpl
mpl.rcParams.update({
"figure.figsize": (9, 5),
"figure.dpi": 110,
"savefig.dpi": 200,
"font.size": 11,
"axes.titlesize": 14,
"axes.titleweight": "bold",
"axes.grid": True,
"axes.axisbelow": True, # lưới nằm dưới dữ liệu
"axes.spines.top": False,
"axes.spines.right": False,
"grid.alpha": 0.3,
})
Đóng gói khối này thành hàm apply_ncb_style() trong một module dùng chung, mọi biểu đồ toàn team sẽ ra cùng một "khuôn mặt". Đây là bước biến các bản vẽ rời rạc thành một bộ nhận diện — nền tảng cho việc dựng dashboard bàn ở Thiết kế dashboard.
Cạm bẫy và mẹo
- Trục Y cắt (không bắt đầu từ 0) trên biểu đồ cột phóng đại chênh lệch một cách sai lệch: hai cột thực chất chênh 3% có thể trông như gấp đôi. Với biểu đồ cột luôn để trục Y bắt đầu từ 0 (
ax.set_ylim(0, ...)). Với line chart theo dõi biến động nhỏ thì cắt trục chấp nhận được nhưng phải ghi chú rõ. Chủ đề bóp méo thị giác này được nói kỹ ở Thiết kế dashboard. - Quá nhiều màu: mỗi màu nên mang một ý nghĩa. Trên 6–7 màu là mắt bắt đầu loạn; gom nhóm hoặc dùng sắc độ cùng tông.
- Chartjunk: viền 3D, đổ bóng, gradient trang trí, đường lưới đậm — mọi thứ không tải thông tin đều làm loãng thông điệp. Bỏ spine trên/phải, làm nhạt lưới, để dữ liệu nổi lên.
- Hiệu năng nhiều điểm:
scattervới hàng trăm nghìn điểm sẽ chậm và bết. Thay bằngax.hexbin(x, y)(gộp ô lục giác),ax.hist2d, hoặc lấy mẫu. Với đường rất nhiều điểm, hạ độ phân giải dữ liệu trước khi vẽ. - Đóng Figure trong vòng lặp: khi sinh hàng loạt biểu đồ (mỗi chi nhánh một file), gọi
plt.close(fig)sausavefigđể giải phóng bộ nhớ — quên bước này là rò rỉ RAM khi chạy batch.
Matplotlib là nền của seaborn và pandas
Điều quan trọng cần nhớ: df.plot() của pandas và mọi hàm của seaborn trả về một Axes của Matplotlib. Nghĩa là bạn có thể để seaborn vẽ phần thống kê phức tạp, rồi dùng chính các method Matplotlib ở bài này (ax.set_title, set_major_formatter, annotate, savefig) để tinh chỉnh và chuẩn hoá thương hiệu. Kỹ năng Matplotlib không bao giờ thừa — nó là lớp nền cho seaborn & pandas plotting. Nắm chắc Figure/Axes/Artist ở đây rồi, những thư viện cấp cao chỉ còn là lối tắt.
Ví dụ đầy đủ: dư nợ theo chi nhánh + đường xu hướng (minh hoạ)
Ví dụ dưới đây minh hoạ cách kết hợp biểu đồ cột (dư nợ theo chi nhánh) với line (xu hướng tổng dư nợ theo tháng, trục phải), định dạng tiền tệ và chuẩn thương hiệu. Số liệu là giả định.
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.ticker import FuncFormatter
import numpy as np
# --- chuẩn thương hiệu NCB (đặt 1 lần) ---
NCB_BLUE = "#0F4C81"
NCB_GOLD = "#C9A227"
mpl.rcParams.update({
"font.size": 11, "axes.titlesize": 14, "axes.titleweight": "bold",
"axes.spines.top": False, "axes.spines.right": False,
"axes.axisbelow": True, "grid.alpha": 0.3,
})
# --- dữ liệu minh hoạ ---
chi_nhanh = ["Hà Nội", "TP.HCM", "Đà Nẵng", "Hải Phòng", "Cần Thơ"]
du_no = np.array([4200, 3800, 1500, 1200, 900]) * 1e9 # VND
thang = ["01", "02", "03", "04", "05", "06"]
tong_du_no = np.array([10.8, 11.1, 11.0, 11.4, 11.5, 11.6]) * 1e12
def dinh_dang_ty(x, pos):
return f"{x/1e9:,.0f}".replace(",", ".")
fig, ax = plt.subplots(figsize=(9, 5))
# cột: dư nợ theo chi nhánh, sắp xếp giảm dần
order = np.argsort(du_no)[::-1]
bars = ax.bar([chi_nhanh[i] for i in order], du_no[order],
color=NCB_BLUE, width=0.6, label="Dư nợ chi nhánh")
ax.set_ylabel("Dư nợ (tỷ VND)")
ax.set_ylim(0, None) # cột LUÔN bắt đầu từ 0
ax.yaxis.set_major_formatter(FuncFormatter(dinh_dang_ty))
ax.set_title("Dư nợ cho vay theo chi nhánh — Q2/2026")
# nhãn giá trị trên đầu cột
for b in bars:
ax.annotate(f"{b.get_height()/1e9:,.0f}".replace(",", "."),
xy=(b.get_x() + b.get_width()/2, b.get_height()),
xytext=(0, 3), textcoords="offset points",
ha="center", fontsize=9)
ax.grid(True, axis="y")
ax.legend(loc="upper right", frameon=False)
fig.tight_layout()
fig.savefig("du_no_chi_nhanh.pdf", bbox_inches="tight") # vector cho báo cáo in
fig.savefig("du_no_chi_nhanh.png", dpi=200, bbox_inches="tight")
plt.close(fig)
Nếu cần một biểu đồ ghép cột + line xu hướng trên cùng Figure, thêm subplot thứ hai hoặc dùng twinx() như phần trục đôi ở trên; nguyên tắc định dạng và xuất file không đổi.
Lấy dữ liệu cho biểu đồ từ sandbox
Trong sandbox (PostgreSQL read-only), có thể lấy số liệu tổng hợp để đẩy vào Matplotlib. Ví dụ tổng số dư theo loại tiền tệ để vẽ cột:
-- ▶ 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;
Kéo kết quả này về bằng pandas.read_sql, rồi ax.bar(df["currency"], df["tong_so_du"]) là ra biểu đồ.
Use case thực tế
Bối cảnh (số liệu ước lượng, minh hoạ): Khối Quản trị rủi ro NCB cần bộ báo cáo dư nợ hàng quý gửi Ban điều hành, gồm ~20 biểu đồ: dư nợ theo chi nhánh, theo nhóm nợ, xu hướng 12 tháng, tỷ lệ nợ xấu. Trước đây mỗi analyst tự vẽ bằng Excel, mỗi người một font, một bảng màu, trục Y người thì từ 0 người thì cắt — nên báo cáo trông chắp vá và có lần một biểu đồ trục cắt khiến lãnh đạo hiểu nhầm mức tăng nợ xấu.
Giải pháp bằng Matplotlib:
- Xây một module
ncb_charts.py: hàmapply_ncb_style()nạp rcParams (font, cỡ chữ, bỏ spine thừa, bảng màu thương hiệu), và các hàmbar_theo_chi_nhanh(ax, df),line_xu_huong(ax, df)nhận Axes làm tham số. - Ràng buộc quy tắc: mọi biểu đồ cột
set_ylim(0, ...), tiền tệ luôn quaFuncFormatterđịnh dạng "tỷ", mỗi chi nhánh một màu cố định để dễ nhận qua các trang. - Script chạy hàng quý: truy vấn số liệu (như câu SELECT ở trên, mở rộng theo schema thật của kho dữ liệu), lặp qua từng loại báo cáo,
savefigra PDF vector để nhúng tài liệu in và PNG 200 DPI cho bản slide,plt.close()sau mỗi hình.
Kết quả ước lượng: thời gian dựng bộ báo cáo giảm từ ~1,5 ngày công thủ công xuống còn một lần chạy script vài phút; biểu đồ đồng nhất về nhận diện; và quan trọng nhất, quy tắc trục-từ-0 được cưỡng chế trong code nên loại bỏ nguy cơ biểu đồ gây hiểu nhầm. Khi số liệu quý mới về, chỉ cần chạy lại — tái lập hoàn toàn (reproducible), không thao tác tay.
Ghi nhớ
- Kiến trúc ba tầng: Figure (trang giấy) chứa nhiều Axes (vùng vẽ có hệ toạ độ riêng); mọi thứ hiển thị được là một Artist.
- Dùng OO API
fig, ax = plt.subplots()và thao tác quaax/fig— rõ ràng, đóng gói được, kiểm soát sâu. Tránhplt.*state-based ngoài việc thử nhanh. - Chọn dạng biểu đồ theo dữ liệu: cột để so nhóm, line cho xu hướng, hist/box cho phân phối; hạn chế pie.
- Định dạng tick tiền tệ/ngày bằng
FuncFormattervàDateFormatter— đừng để trục hiện số hàng tỷ thô. - Chuẩn hoá thương hiệu một lần qua rcParams/
plt.style; đóng gói thành hàm dùng chung toàn team. - Xuất PDF/SVG (vector) cho báo cáo in, PNG dpi cao cho slide;
bbox_inches="tight"; nhớplt.close()khi chạy batch. - Cạm bẫy: biểu đồ cột phải bắt đầu từ 0, tránh quá nhiều màu và chartjunk, dùng
hexbinkhi quá nhiều điểm. - Matplotlib là nền của seaborn & pandas plotting: mọi biểu đồ cấp cao vẫn trả về Axes để bạn tinh chỉnh bằng kỹ năng ở bài này.
Nguồn tham khảo
- Matplotlib Documentation — tài liệu chính thức, bao gồm User Guide và API Reference.
- Matplotlib User Guide — "The Lifecycle of a Plot" và "Matplotlib Application Interfaces (APIs)": giải thích OO API vs pyplot state-based.
- Matplotlib API Reference — mục
matplotlib.ticker(FuncFormatter),matplotlib.dates(DateFormatter) vàmatplotlib.gridspec. - Matplotlib — "Customizing Matplotlib with style sheets and rcParams" và tệp
matplotlibrcmẫu. - J. D. Hunter, "Matplotlib: A 2D Graphics Environment", Computing in Science & Engineering, vol. 9, no. 3, 2007, pp. 90–95.
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.
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.
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ướ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ẻ!