Appearance
📘 Buổi 10: Visualization — Data đẹp, insight mạnh
Một chart tốt = 1000 dòng dữ liệu. Kỹ năng visualization quyết định bạn trình bày insight tốt cỡ nào.
🎯 Mục tiêu buổi học
Sau buổi này, học viên sẽ:
- Tạo publication-quality charts với Matplotlib: line, bar, scatter, histogram
- Sử dụng Seaborn cho statistical visualization: heatmap, pair plot, violin
- Customize chart: colors, labels, annotations, subplots
- Áp dụng visualization best practices: Tufte, IBCS
📋 Tổng quan
Ở Buổi 9, bạn đã thực hiện EDA hoàn chỉnh — histogram, scatter plot, correlation matrix — và phát hiện insight từ dữ liệu. Nhưng những biểu đồ EDA thường "nhanh và bẩn" — đủ để bạn hiểu, nhưng chưa đủ để trình bày cho sếp, cho stakeholder, hay đưa vào report. Manager không cần nhìn plt.show() mặc định — họ cần biểu đồ rõ ràng, có title, có annotation, consistent color, đúng chart type. Đó chính là lúc bạn cần master Matplotlib & Seaborn.
Matplotlib là thư viện visualization gốc của Python — mọi thư viện khác (Seaborn, Pandas plot, Plotly) đều xây trên nó. Nắm Matplotlib nghĩa là bạn kiểm soát từng pixel trên biểu đồ. Seaborn xây trên Matplotlib nhưng tập trung vào statistical visualization — với 1 dòng code, bạn tạo được biểu đồ mà Matplotlib cần 20 dòng. Hai thư viện bổ sung nhau: Seaborn để tạo nhanh, Matplotlib để tinh chỉnh.
Nhớ lại hành trình OSEMN: Buổi 7 Obtain, Buổi 8 Scrub, Buổi 9 Explore. Buổi 10 là bước iNterpret — biến insight thành visual story mà ai cũng hiểu.
mermaid
flowchart LR
A["📥 Obtain<br/>Buổi 7: Python đọc file"] --> B["🧹 Scrub<br/>Buổi 8: Pandas Data Cleaning"]
B --> C["🔍 Explore<br/>Buổi 9: EDA"]
C --> D["📊 iNterpret<br/>✅ Buổi 10: Visualization"]
D --> E["🤖 Model<br/>Buổi 12+"]
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:3px💡 Tại sao cần visualization chuyên nghiệp?
| Tình huống | Chart EDA (nhanh) | Chart Report (chuyên nghiệp) |
|---|---|---|
| Scatter plot doanh thu | plt.scatter(x, y) — không label | Có title, axis label, trend line, annotation outlier |
| Biểu đồ cột theo tháng | Màu mặc định, không legend | Consistent color palette, data labels, brand colors |
| Heatmap correlation | Kích thước nhỏ, chữ chồng | Font size phù hợp, mask triangle, color scale rõ |
| Dashboard cho sếp | Nhiều chart rời rạc | Subplot layout thống nhất, white space hợp lý |
Matplotlib Architecture — Figure, Axes, Artist
Trước khi viết code, hãy hiểu cấu trúc 3 tầng của Matplotlib — điều này giúp bạn biết cần gọi method nào ở đâu:
mermaid
flowchart TD
A["🖼️ Figure<br/>Khung chứa toàn bộ — như trang giấy"] --> B["📊 Axes<br/>Vùng vẽ biểu đồ — 1 Figure có thể nhiều Axes"]
B --> C["🎨 Artist<br/>Mọi thứ nhìn thấy: line, bar, text, legend, title"]
A --> D["fig = plt.figure()<br/>fig, axes = plt.subplots(2, 2)"]
B --> E["ax.plot(), ax.bar()<br/>ax.set_title(), ax.set_xlabel()"]
C --> F["Line2D, Rectangle, Text<br/>Annotation, Legend, Spine"]⚠️ Sai lầm phổ biến: plt.plot() vs ax.plot()
plt.plot()— stateful API (pyplot): nhanh nhưng khó kiểm soát khi nhiều chartax.plot()— object-oriented API: rõ ràng, mỗi Axes độc lập, luôn dùng cách này cho report
python
# ❌ Pyplot — khó quản lý khi nhiều chart
plt.plot(x, y)
plt.title("Chart 1")
plt.show()
# ✅ Object-oriented — kiểm soát hoàn toàn
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, y)
ax.set_title("Chart 1")
plt.show()📌 Phần 1: Matplotlib Fundamentals
Các loại chart cơ bản — Line, Bar, Scatter, Histogram
Mỗi loại chart phù hợp với một loại dữ liệu và câu hỏi khác nhau. Dưới đây là 4 chart type quan trọng nhất:
python
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# Tạo dữ liệu sales mẫu
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = [850, 920, 1100, 980, 1250, 1180,
1350, 1420, 1280, 1500, 1650, 1800] # triệu VND
costs = [600, 650, 750, 700, 820, 780,
850, 900, 830, 950, 1000, 1050]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Line Chart — xu hướng theo thời gian
axes[0, 0].plot(months, revenue, marker="o", color="#2196F3",
linewidth=2, label="Revenue")
axes[0, 0].plot(months, costs, marker="s", color="#FF5722",
linewidth=2, label="Costs")
axes[0, 0].set_title("Revenue vs Costs (2025)", fontsize=14, fontweight="bold")
axes[0, 0].set_ylabel("Triệu VND")
axes[0, 0].legend()
axes[0, 0].grid(axis="y", alpha=0.3)
# → Line chart: thấy rõ revenue tăng đều, costs tăng chậm hơn — profit margin mở rộng
# 2. Bar Chart — so sánh giữa các nhóm
categories = ["Electronics", "Fashion", "Food", "Beauty", "Sports"]
sales = [4500, 3200, 2800, 1900, 1500]
colors = ["#1976D2", "#388E3C", "#F57C00", "#7B1FA2", "#C62828"]
axes[0, 1].barh(categories, sales, color=colors, edgecolor="white")
axes[0, 1].set_title("Sales by Category (triệu VND)", fontsize=14, fontweight="bold")
axes[0, 1].set_xlabel("Triệu VND")
for i, v in enumerate(sales):
axes[0, 1].text(v + 50, i, f"{v:,}", va="center", fontweight="bold")
# → Horizontal bar: dễ đọc label dài, Electronics dẫn đầu gấp 3 lần Sports
# 3. Scatter Plot — quan hệ giữa 2 biến
np.random.seed(42)
ad_spend = np.random.uniform(10, 100, 50)
conversions = ad_spend * 2.5 + np.random.normal(0, 15, 50)
axes[1, 0].scatter(ad_spend, conversions, c=conversions, cmap="viridis",
s=60, alpha=0.7, edgecolors="white")
axes[1, 0].set_title("Ad Spend vs Conversions", fontsize=14, fontweight="bold")
axes[1, 0].set_xlabel("Ad Spend (triệu VND)")
axes[1, 0].set_ylabel("Conversions")
# → Scatter: thấy rõ mối quan hệ tuyến tính dương — chi quảng cáo tăng → conversion tăng
# 4. Histogram — phân phối dữ liệu
order_values = np.random.lognormal(mean=14, sigma=0.8, size=5000)
axes[1, 1].hist(order_values, bins=50, color="#26A69A", edgecolor="white", alpha=0.8)
axes[1, 1].axvline(np.median(order_values), color="red", linestyle="--",
label=f"Median: {np.median(order_values):,.0f}")
axes[1, 1].set_title("Distribution of Order Values", fontsize=14, fontweight="bold")
axes[1, 1].set_xlabel("Order Value (VND)")
axes[1, 1].legend()
# → Histogram: phân phối lệch phải (right-skewed) — đa số đơn hàng giá trị thấp, ít đơn giá trị cao
plt.tight_layout()
plt.savefig("basic_charts.png", dpi=150, bbox_inches="tight")
plt.show()Customization cơ bản — Title, Label, Legend, Grid
Một chart tốt cần 5 yếu tố bắt buộc: title, axis labels, legend (nếu nhiều series), grid (giúp đọc giá trị), và source/note (trong report chính thức).
python
fig, ax = plt.subplots(figsize=(12, 6))
# Dữ liệu doanh thu theo quý
quarters = ["Q1'24", "Q2'24", "Q3'24", "Q4'24", "Q1'25", "Q2'25"]
actual = [2800, 3100, 3500, 4200, 3900, 4500]
target = [3000, 3000, 3500, 4000, 4000, 4500]
ax.bar(quarters, actual, color="#1565C0", width=0.4, label="Actual", zorder=3)
ax.plot(quarters, target, color="#E53935", linewidth=2, marker="D",
markersize=8, label="Target", zorder=4)
# Customization — 5 yếu tố bắt buộc
ax.set_title("Quarterly Revenue: Actual vs Target", fontsize=16,
fontweight="bold", pad=15) # ① Title
ax.set_xlabel("Quarter", fontsize=12) # ② X Label
ax.set_ylabel("Revenue (triệu VND)", fontsize=12) # ③ Y Label
ax.legend(fontsize=11, loc="upper left") # ④ Legend
ax.grid(axis="y", alpha=0.3, linestyle="--") # ⑤ Grid
# Data labels trên mỗi bar
for i, v in enumerate(actual):
color = "green" if v >= target[i] else "red"
ax.text(i, v + 50, f"{v:,}", ha="center", fontweight="bold",
color=color, fontsize=10)
# Note: source data
ax.text(0.0, -0.12, "Source: Finance Department | Unit: triệu VND",
transform=ax.transAxes, fontsize=9, color="gray", style="italic")
ax.set_ylim(0, 5500)
ax.spines[["top", "right"]].set_visible(False) # Remove top & right border
plt.tight_layout()
plt.show()
# → Actual vượt target từ Q3'24 (xanh), Q1'25 không đạt (đỏ), Q2'25 đạt đúng mục tiêuSubplots & Figure Layout
Khi cần trình bày nhiều charts cùng lúc — báo cáo dashboard-style:
python
# Dashboard layout: 2 hàng, chart trên rộng gấp đôi
fig = plt.figure(figsize=(16, 10))
# GridSpec — kiểm soát layout linh hoạt hơn subplots
from matplotlib.gridspec import GridSpec
gs = GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.3)
ax1 = fig.add_subplot(gs[0, :2]) # Hàng 1, chiếm 2/3 chiều rộng
ax2 = fig.add_subplot(gs[0, 2]) # Hàng 1, 1/3 bên phải
ax3 = fig.add_subplot(gs[1, 0]) # Hàng 2, trái
ax4 = fig.add_subplot(gs[1, 1]) # Hàng 2, giữa
ax5 = fig.add_subplot(gs[1, 2]) # Hàng 2, phải
# Chart 1: Revenue Trend (lớn, chiếm 2/3)
ax1.plot(months, revenue, marker="o", color="#1976D2", linewidth=2.5)
ax1.fill_between(months, revenue, alpha=0.1, color="#1976D2")
ax1.set_title("Monthly Revenue Trend", fontsize=14, fontweight="bold")
ax1.set_ylabel("Triệu VND")
ax1.grid(axis="y", alpha=0.3)
# Chart 2: Category Pie (nhỏ, bên phải)
ax2.pie(sales, labels=categories, autopct="%1.0f%%",
colors=colors, startangle=90, textprops={"fontsize": 9})
ax2.set_title("Sales by Category", fontsize=12, fontweight="bold")
# Chart 3-5: KPIs nhỏ
for ax, title, value, delta in [
(ax3, "Total Revenue", "18.3B VND", "+12%"),
(ax4, "Avg Order Value", "1.25M VND", "+5%"),
(ax5, "Active Customers", "32,450", "+8%")
]:
ax.text(0.5, 0.6, value, ha="center", va="center",
fontsize=20, fontweight="bold", transform=ax.transAxes)
ax.text(0.5, 0.3, delta, ha="center", va="center",
fontsize=16, color="green", fontweight="bold", transform=ax.transAxes)
ax.set_title(title, fontsize=12, fontweight="bold")
ax.axis("off")
fig.suptitle("Sales Dashboard — June 2025", fontsize=18, fontweight="bold", y=1.02)
plt.savefig("dashboard.png", dpi=150, bbox_inches="tight")
plt.show()
# → Dashboard layout: chart quan trọng nhất chiếm diện tích lớn, KPI cards bên dưới📌 savefig() — Lưu chart cho report
python
# Các format phổ biến
fig.savefig("chart.png", dpi=150, bbox_inches="tight") # PNG — phổ biến nhất
fig.savefig("chart.svg", bbox_inches="tight") # SVG — vector, scale không vỡ
fig.savefig("chart.pdf", bbox_inches="tight") # PDF — cho báo cáo indpi=150: đủ cho slide/report.dpi=300: cho in ấn chất lượng cao.bbox_inches="tight": tự cắt viền trắng thừa — luôn dùng.
📌 Phần 2: Seaborn — Statistical Visualization
Seaborn xây trên Matplotlib nhưng đặc biệt mạnh ở statistical visualization — tự tính toán thống kê và vẽ sẵn, bạn chỉ cần chỉ đúng cột.
Distribution Plots — Hiểu phân phối dữ liệu
python
import seaborn as sns
import pandas as pd
import numpy as np
# Tạo dataset thực tế — đơn hàng e-commerce
np.random.seed(42)
n = 2000
df = pd.DataFrame({
"order_value": np.random.lognormal(14.2, 0.7, n),
"customer_age": np.random.normal(32, 8, n).astype(int),
"category": np.random.choice(["Electronics", "Fashion", "Food", "Beauty"], n,
p=[0.3, 0.3, 0.25, 0.15]),
"region": np.random.choice(["North", "South", "Central"], n, p=[0.4, 0.4, 0.2]),
"satisfaction": np.random.choice([1, 2, 3, 4, 5], n, p=[0.05, 0.1, 0.25, 0.35, 0.25])
})
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 1. histplot — histogram + KDE cùng lúc
sns.histplot(df["order_value"], bins=40, kde=True, color="steelblue", ax=axes[0])
axes[0].set_title("Distribution of Order Value", fontweight="bold")
axes[0].set_xlabel("Order Value (VND)")
# → Right-skewed: đa số đơn hàng < 2M, ít đơn giá trị rất cao
# 2. kdeplot — so sánh phân phối giữa các nhóm
sns.kdeplot(data=df, x="order_value", hue="category", fill=True,
alpha=0.4, ax=axes[1])
axes[1].set_title("Order Value by Category", fontweight="bold")
# → Electronics có giá trị cao hơn rõ rệt so với Food
# 3. ecdfplot — Cumulative Distribution
sns.ecdfplot(data=df, x="order_value", hue="region", ax=axes[2])
axes[2].set_title("ECDF — Order Value by Region", fontweight="bold")
# → 80% đơn hàng có giá trị < 3M VND ở cả 3 vùng
plt.tight_layout()
plt.show()Categorical Plots — So sánh giữa nhóm
python
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. boxplot — phân phối + outlier theo nhóm
sns.boxplot(data=df, x="category", y="order_value", palette="Set2", ax=axes[0, 0])
axes[0, 0].set_title("Order Value by Category", fontweight="bold")
axes[0, 0].set_ylabel("Order Value (VND)")
# → Electronics có median cao nhất, Food có spread nhỏ nhất
# 2. violinplot — box plot + KDE — thấy hình dạng phân phối
sns.violinplot(data=df, x="category", y="order_value",
palette="pastel", inner="quartile", ax=axes[0, 1])
axes[0, 1].set_title("Violin Plot — Order Value", fontweight="bold")
# → Beauty có phân phối bimodal (2 đỉnh) — có 2 phân khúc giá
# 3. countplot — đếm số lượng theo nhóm
sns.countplot(data=df, x="satisfaction", hue="region",
palette="Blues_d", ax=axes[1, 0])
axes[1, 0].set_title("Satisfaction Score by Region", fontweight="bold")
axes[1, 0].set_xlabel("Satisfaction (1-5)")
# → Score 4 chiếm đa số ở cả 3 vùng, South có tỷ lệ score 5 cao hơn
# 4. barplot — trung bình + confidence interval
sns.barplot(data=df, x="category", y="order_value", hue="region",
palette="rocket", ci=95, ax=axes[1, 1])
axes[1, 1].set_title("Avg Order Value: Category × Region", fontweight="bold")
axes[1, 1].set_ylabel("Avg Order Value (VND)")
# → Error bar (CI 95%) cho thấy sự khác biệt có ý nghĩa thống kê hay không
plt.tight_layout()
plt.show()Relational & Matrix Plots — Mối quan hệ giữa biến
python
# Thêm cột numerical để tạo correlation
df["quantity"] = np.random.poisson(3, n) + 1
df["discount_pct"] = np.random.uniform(0, 30, n).round(1)
df["revenue"] = df["order_value"] * df["quantity"] * (1 - df["discount_pct"] / 100)
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# 1. scatterplot — với hue, size, style
sns.scatterplot(data=df.sample(500), x="order_value", y="revenue",
hue="category", size="quantity", sizes=(20, 200),
alpha=0.6, ax=axes[0])
axes[0].set_title("Order Value vs Revenue", fontweight="bold")
# → Mỗi chấm encode 4 biến: x, y, color (category), size (quantity)
# 2. heatmap — correlation matrix
numerical_cols = ["order_value", "customer_age", "quantity", "discount_pct", "revenue"]
corr_matrix = df[numerical_cols].corr()
sns.heatmap(corr_matrix, annot=True, fmt=".2f", cmap="RdBu_r",
center=0, vmin=-1, vmax=1, square=True,
linewidths=0.5, ax=axes[1])
axes[1].set_title("Correlation Heatmap", fontweight="bold")
# → revenue tương quan mạnh với order_value (0.85) và quantity (0.72)
# → discount_pct tương quan âm nhẹ với revenue (-0.15)
plt.tight_layout()
plt.show()python
# pairplot — scatter matrix cho TẤT CẢ cặp biến
# Chạy chậm hơn, nhưng cho cái nhìn toàn diện nhất
g = sns.pairplot(df[["order_value", "customer_age", "quantity", "revenue", "category"]],
hue="category", palette="husl", diag_kind="kde",
plot_kws={"alpha": 0.5, "s": 20})
g.fig.suptitle("Pair Plot — All Variable Relationships", y=1.02, fontsize=14)
plt.show()
# → Pair plot = "EDA toàn cảnh" — ngay lập tức thấy cặp biến nào có pattern
# → Diagonal: KDE cho mỗi biến, chia theo category💡 Khi nào dùng Seaborn vs Matplotlib?
| Mục đích | Seaborn | Matplotlib |
|---|---|---|
| Nhanh, statistical | ✅ sns.boxplot() — 1 dòng | ❌ Cần 10+ dòng code |
| Custom pixel-perfect | ❌ Giới hạn customization | ✅ Kiểm soát từng chi tiết |
| Heatmap, pairplot | ✅ Built-in, đẹp sẵn | ❌ Phải tự code |
| Branding, corporate style | ❌ Khó tuỳ chỉnh sâu | ✅ Toàn quyền kiểm soát |
| Thực tế | Dùng Seaborn tạo nhanh | Dùng Matplotlib tinh chỉnh |
📌 Phần 3: Advanced Customization
Color Palettes — Chọn màu đúng cách
Màu sắc không chỉ là thẩm mỹ — nó encode thông tin. Chọn sai palette có thể gây hiểu nhầm hoặc loại bỏ người đọc bị mù màu.
python
fig, axes = plt.subplots(2, 3, figsize=(18, 8))
# 6 loại palette phổ biến trong Seaborn
palettes = {
"Qualitative\n(phân loại)": "Set2",
"Sequential\n(tuần tự)": "Blues",
"Diverging\n(phân kỳ)": "RdBu",
"Pastel\n(nhẹ nhàng)": "pastel",
"Dark\n(đậm)": "dark",
"Corporate\n(tùy chỉnh)": ["#1A237E", "#1565C0", "#42A5F5", "#90CAF9", "#E3F2FD"]
}
for ax, (name, pal) in zip(axes.flat, palettes.items()):
sns.barplot(x=["A", "B", "C", "D", "E"],
y=[50, 35, 45, 28, 40], palette=pal, ax=ax)
ax.set_title(name, fontsize=12, fontweight="bold")
ax.set_ylim(0, 60)
plt.suptitle("Color Palette Guide", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()⚠️ Color Accessibility — Đừng quên người mù màu
- 8% nam giới bị mù màu đỏ-xanh lá (deuteranopia). Chart dùng đỏ vs xanh lá → họ không phân biệt được.
- ✅ Dùng palette colorblind-safe:
"colorblind","Set2","Paired" - ✅ Kết hợp color + pattern/shape: dùng marker khác nhau (
o,s,^), hoặc linestyle (-,--,:) - ✅ Kiểm tra bằng tool: Coblis hoặc thư viện
colorspacious
python
# Palette an toàn cho mù màu
sns.set_palette("colorblind")
# Kết hợp color + marker shape
markers = ["o", "s", "^", "D"]
for i, cat in enumerate(df["category"].unique()):
subset = df[df["category"] == cat].sample(100)
plt.scatter(subset["order_value"], subset["revenue"],
label=cat, marker=markers[i], alpha=0.6, s=50)
plt.legend()
plt.title("Colorblind-safe: Color + Shape encoding")
plt.show()Annotations — Ghi chú trực tiếp lên chart
Annotation biến chart từ "hình vẽ" thành "câu chuyện". Thay vì bắt người đọc tự tìm insight, bạn chỉ thẳng cho họ.
python
fig, ax = plt.subplots(figsize=(12, 6))
# Dữ liệu revenue hàng tháng
months_num = np.arange(1, 13)
revenue_2025 = [850, 920, 1100, 980, 1250, 1180, 1350, 1420, 1280, 1500, 1650, 1800]
ax.plot(months_num, revenue_2025, marker="o", color="#1565C0",
linewidth=2.5, markersize=8, zorder=3)
ax.fill_between(months_num, revenue_2025, alpha=0.08, color="#1565C0")
# Annotation 1: Đánh dấu peak
peak_idx = np.argmax(revenue_2025)
ax.annotate(
f"Peak: {revenue_2025[peak_idx]:,}M VND",
xy=(months_num[peak_idx], revenue_2025[peak_idx]), # điểm được chỉ
xytext=(months_num[peak_idx] - 2, revenue_2025[peak_idx] + 150), # vị trí text
fontsize=12, fontweight="bold", color="#1B5E20",
arrowprops=dict(arrowstyle="->", color="#1B5E20", lw=2),
bbox=dict(boxstyle="round,pad=0.3", facecolor="#E8F5E9", edgecolor="#1B5E20")
)
# Annotation 2: Đánh dấu dip (giảm bất thường)
dip_idx = 3 # April
ax.annotate(
f"Dip: {revenue_2025[dip_idx]:,}M\n(Tết Holiday Effect)",
xy=(months_num[dip_idx], revenue_2025[dip_idx]),
xytext=(months_num[dip_idx] + 1.5, revenue_2025[dip_idx] - 150),
fontsize=11, color="#B71C1C",
arrowprops=dict(arrowstyle="->", color="#B71C1C", lw=1.5),
bbox=dict(boxstyle="round,pad=0.3", facecolor="#FFEBEE", edgecolor="#B71C1C")
)
# Annotation 3: Vùng highlight
ax.axvspan(10, 12, alpha=0.1, color="green", label="Q4 — Peak Season")
ax.set_xticks(months_num)
ax.set_xticklabels(months)
ax.set_title("Monthly Revenue 2025 — Annotated", fontsize=15, fontweight="bold")
ax.set_ylabel("Revenue (triệu VND)")
ax.legend(loc="upper left")
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.show()
# → Annotation giúp người đọc ngay lập tức thấy: peak tháng 12, dip tháng 4,
# Q4 là peak season — không cần đọc thêm bất kỳ text nàoMulti-panel Figures & Style Themes
python
# Seaborn built-in themes
fig, axes = plt.subplots(1, 4, figsize=(20, 4))
themes = ["whitegrid", "darkgrid", "white", "dark"]
x = ["A", "B", "C", "D"]
y = [25, 40, 30, 35]
for ax, theme in zip(axes, themes):
with sns.axes_style(theme):
sns.barplot(x=x, y=y, palette="Blues_d", ax=ax)
ax.set_title(f'style="{theme}"', fontsize=12, fontweight="bold")
ax.set_ylim(0, 50)
plt.suptitle("Seaborn Style Themes", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()python
# Custom style cho corporate report
plt.rcParams.update({
"figure.facecolor": "white",
"axes.facecolor": "white",
"axes.edgecolor": "#CCCCCC",
"axes.labelcolor": "#333333",
"axes.titlesize": 14,
"axes.titleweight": "bold",
"axes.grid": True,
"grid.alpha": 0.3,
"grid.color": "#E0E0E0",
"font.family": "sans-serif",
"font.size": 11,
"text.color": "#333333",
"xtick.color": "#666666",
"ytick.color": "#666666",
})
# → Áp dụng 1 lần, tất cả chart trong notebook sẽ theo style này
# → Phù hợp cho report chuyên nghiệp, consistent branding📖 Tổng hợp Matplotlib rcParams thường dùng
| Parameter | Mô tả | Giá trị ví dụ |
|---|---|---|
figure.figsize | Kích thước mặc định | (12, 6) |
figure.dpi | Độ phân giải | 100 (screen), 150 (report) |
axes.titlesize | Font size tiêu đề | 14 |
axes.labelsize | Font size label trục | 12 |
axes.grid | Hiển thị grid | True / False |
axes.spines.top | Viền trên | False (thường ẩn) |
axes.spines.right | Viền phải | False (thường ẩn) |
lines.linewidth | Độ dày line mặc định | 2.0 |
font.family | Font chữ | "sans-serif" |
savefig.dpi | DPI khi lưu file | 150 |
📌 Phần 4: Visualization Best Practices
Chart Selection Guide — Data Type → Chart Type
Chọn sai chart type = giấu insight. Bảng dưới đây là "cheat sheet" giúp bạn chọn chart phù hợp với loại dữ liệu và câu hỏi phân tích:
| Câu hỏi phân tích | Data Type | Chart Type | Seaborn / Matplotlib |
|---|---|---|---|
| Xu hướng theo thời gian | Time series | Line chart | ax.plot() / sns.lineplot() |
| So sánh giữa nhóm | Categorical | Bar chart | ax.bar() / sns.barplot() |
| Phân phối 1 biến | Numerical | Histogram / KDE | sns.histplot() / sns.kdeplot() |
| Phân phối theo nhóm | Num + Cat | Box / Violin plot | sns.boxplot() / sns.violinplot() |
| Quan hệ 2 biến | Num + Num | Scatter plot | ax.scatter() / sns.scatterplot() |
| Tương quan nhiều biến | Multiple Num | Heatmap | sns.heatmap() |
| Tất cả cặp biến | Multiple Num | Pair plot | sns.pairplot() |
| Tỷ lệ phần trăm | Categorical | Pie / Donut (hạn chế) | ax.pie() |
| Thành phần theo thời gian | Time + Cat | Stacked bar / Area | ax.stackplot() |
| Đếm frequency | Categorical | Count plot | sns.countplot() |
⚠️ 3 chart type nên TRÁNH
- Pie chart với > 5 slices: mắt người so sánh góc rất kém — dùng bar chart thay thế
- 3D charts: thêm chiều không thêm thông tin, distort perception — luôn dùng 2D
- Dual-axis chart: 2 trục y scale khác nhau dễ gây hiểu nhầm — dùng 2 chart riêng hoặc normalize
Tufte's Data-Ink Ratio — Tối giản nhưng đủ thông tin
Edward Tufte, cha đẻ data visualization hiện đại, đặt ra nguyên tắc:
Nghĩa là: mọi pixel trên chart phải truyền tải thông tin. Xóa bỏ mọi thứ không cần thiết.
python
# ❌ TRƯỚC: Chart nhiều "noise" — data-ink ratio thấp
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
categories = ["Electronics", "Fashion", "Food", "Beauty", "Sports"]
values = [4500, 3200, 2800, 1900, 1500]
# Chart xấu — nhiều "junk" visual
axes[0].bar(categories, values, color=["red", "blue", "green", "orange", "purple"],
edgecolor="black", linewidth=2)
axes[0].set_title("BAD: Low Data-Ink Ratio ❌", fontsize=14, fontweight="bold")
axes[0].grid(True, which="both", linewidth=1.5) # Grid quá đậm
# → Grid dày, viền đen, 5 màu random, không sorted — mắt không biết nhìn đâu
# ✅ SAU: Chart tối giản — data-ink ratio cao
sorted_idx = np.argsort(values)[::-1]
sorted_cats = [categories[i] for i in sorted_idx]
sorted_vals = [values[i] for i in sorted_idx]
axes[1].barh(sorted_cats, sorted_vals, color="#1565C0", edgecolor="none", height=0.6)
axes[1].set_title("GOOD: High Data-Ink Ratio ✅", fontsize=14, fontweight="bold")
axes[1].spines[["top", "right", "bottom"]].set_visible(False)
axes[1].xaxis.set_visible(False) # Ẩn trục X — data label đã đủ
for i, v in enumerate(sorted_vals):
axes[1].text(v + 50, i, f"{v:,}", va="center", fontsize=12, fontweight="bold")
# → Sorted, 1 màu, horizontal (dễ đọc), data labels, bỏ viền thừa
plt.tight_layout()
plt.show()IBCS Standards — International Business Communication Standards
IBCS là bộ chuẩn quốc tế cho business chart, được các tập đoàn lớn (SAP, Deloitte) áp dụng:
python
# IBCS-style chart: Actual vs Plan vs Previous Year
fig, ax = plt.subplots(figsize=(12, 6))
quarters = ["Q1", "Q2", "Q3", "Q4"]
actual = [2800, 3100, 3500, 4200] # Đen đậm — AC (Actual)
plan = [3000, 3000, 3500, 4000] # Viền nét đứt — PL (Plan)
prev_yr = [2500, 2700, 3100, 3600] # Xám — PY (Previous Year)
x = np.arange(len(quarters))
width = 0.25
# IBCS: Actual = đen solid, Plan = viền trống, PY = xám
ax.bar(x - width, prev_yr, width, color="#B0BEC5", label="PY (Previous Year)", zorder=2)
ax.bar(x, actual, width, color="#212121", label="AC (Actual)", zorder=2)
ax.bar(x + width, plan, width, fill=False, edgecolor="#212121",
linewidth=2, label="PL (Plan)", zorder=2)
# Variance indicators: AC vs PL
for i in range(len(quarters)):
diff = actual[i] - plan[i]
color = "#2E7D32" if diff >= 0 else "#C62828"
symbol = "▲" if diff >= 0 else "▼"
ax.text(x[i], max(actual[i], plan[i]) + 100,
f"{symbol} {abs(diff):,}", ha="center", fontsize=10,
fontweight="bold", color=color)
ax.set_xticks(x)
ax.set_xticklabels(quarters)
ax.set_title("Revenue: AC vs PL vs PY (IBCS Style)", fontsize=14, fontweight="bold")
ax.set_ylabel("Revenue (triệu VND)")
ax.legend(loc="upper left", frameon=False)
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", alpha=0.2)
plt.tight_layout()
plt.show()
# → IBCS: AC đen (nổi bật), PL viền trống (so sánh), PY xám (đã qua)
# → Variance ▲▼ ngay trên bar — người đọc thấy ngay đạt/không đạt targetCommon Visualization Mistakes
⚠️ 7 lỗi visualization phổ biến nhất
1. Truncated Y-axis — Cắt trục Y không bắt đầu từ 0 → phóng đại sự khác biệt
python
# ❌ y-axis bắt đầu từ 950 → chênh lệch nhỏ trông rất lớn
ax.set_ylim(950, 1050) # Misleading!
# ✅ y-axis bắt đầu từ 0
ax.set_ylim(0, 1100)2. Rainbow colormap cho sequential data — Dùng jet hoặc rainbow → không biết giá trị nào cao/thấp
python
# ❌ cmap="jet" — đỏ/xanh không có thứ tự logic
# ✅ cmap="viridis" hoặc "Blues" — từ nhạt đến đậm theo giá trị3. Overplotting scatter — 50,000 điểm chồng lên nhau → không thấy pattern
python
# ❌ 50K points, alpha=1.0 → blob đen
# ✅ Giảm alpha, hoặc dùng hexbin / 2D histogram
ax.hexbin(x, y, gridsize=30, cmap="Blues") # Giải pháp overplotting4. Sử dụng quá nhiều màu — > 7 màu → mắt không phân biệt → dùng highlight 1-2 màu, còn lại xám
5. Thiếu context — Chart không có title, label, unit → người đọc không biết đang xem gì
6. Sai chart type — Dùng line chart cho categorical data, pie chart cho 15 categories
7. Không consistent — Mỗi chart trong report dùng palette khác nhau, font khác nhau → unprofessional
Accessibility Checklist
Biểu đồ tốt phải readable cho mọi người — bao gồm người mù màu, người in đen trắng, và người xem trên màn hình nhỏ:
📌 Accessibility checklist cho mỗi chart
- [ ] Colorblind-safe palette:
"colorblind","Set2", hoặc test bằng simulator - [ ] Redundant encoding: không chỉ dùng color — thêm marker, pattern, hoặc label
- [ ] Sufficient contrast: text đủ đậm trên background, minimum 4.5:1 ratio
- [ ] Font size ≥ 10pt: title ≥ 14pt, label ≥ 12pt, tick ≥ 10pt
- [ ] Alt text: nếu đưa vào web/slide, viết mô tả cho screen reader
- [ ] Print-friendly: kiểm tra chart khi in đen trắng — vẫn readable không?
🔑 Từ khóa chính
| Tiếng Việt | English | Giải thích |
|---|---|---|
| Trực quan hóa dữ liệu | Data Visualization | Biểu diễn dữ liệu bằng hình ảnh — chart, graph, map |
| Biểu đồ phân tán | Scatter Plot | Hiển thị mối quan hệ giữa 2 biến numerical — mỗi điểm = 1 observation |
| Bản đồ nhiệt | Heatmap | Ma trận màu thể hiện giá trị — thường dùng cho correlation matrix |
| Bảng màu | Color Palette | Bộ màu sắc sử dụng trong chart — qualitative, sequential, diverging |
| Chú thích | Annotation | Ghi chú trực tiếp trên chart — mũi tên, text box, highlight |
| Biểu đồ con | Subplot | Nhiều chart trong 1 figure — so sánh side-by-side |
| Tỷ lệ data-ink | Data-Ink Ratio | Nguyên tắc Tufte: tối đa ink cho data, tối thiểu ink trang trí |
| Biểu đồ violin | Violin Plot | Kết hợp box plot + KDE — thấy hình dạng phân phối đầy đủ |
| Biểu đồ cặp | Pair Plot | Scatter matrix cho tất cả cặp biến — tổng quan relationship |
| Hỗ trợ tiếp cận | Accessibility | Thiết kế chart cho mọi người — mù màu, in đen trắng, screen reader |
📊 Tổng kết buổi học
✅ Checklist — Bạn đã nắm được
- [ ] Hiểu kiến trúc Matplotlib: Figure → Axes → Artist
- [ ] Tạo 4 chart cơ bản: line, bar, scatter, histogram với Matplotlib
- [ ] Customize: title, labels, legend, grid, data labels, annotations
- [ ] Sử dụng
subplots()vàGridSpeccho multi-panel layout - [ ]
savefig()với DPI và format phù hợp (PNG, SVG, PDF) - [ ] Seaborn distribution plots:
histplot,kdeplot,ecdfplot - [ ] Seaborn categorical plots:
boxplot,violinplot,countplot,barplot - [ ] Seaborn relational:
scatterplot,heatmap,pairplot - [ ] Chọn color palette phù hợp: qualitative, sequential, diverging
- [ ] Thiết kế chart accessibility: colorblind-safe, redundant encoding
- [ ] Annotation chuyên nghiệp: mũi tên, text box, highlight vùng
- [ ] Áp dụng Tufte data-ink ratio: tối giản, sorted, bỏ viền thừa
- [ ] IBCS style: Actual đen, Plan viền, PY xám, variance indicators
- [ ] Tránh 7 lỗi phổ biến: truncated axis, rainbow cmap, overplotting...
🗺️ Hành trình học tập
Buổi 3-4: Excel — Pivot Table, biểu đồ cơ bản
↓
Buổi 5-6: SQL — Query, JOIN, GROUP BY, Window Functions
↓
Buổi 7: Python — Lập trình cơ bản, đọc/ghi file
↓
Buổi 8: Pandas & Numpy — Data Cleaning chuyên nghiệp
↓
Buổi 9: EDA — Khám phá dữ liệu, phát hiện insight
↓
✅ Buổi 10: Visualization — Matplotlib & Seaborn chuyên sâu
↓
→ Buổi 11: Storytelling with Data — Kể chuyện bằng dữ liệuBạn vừa hoàn thành kỹ năng trình bày insight bằng hình ảnh — kỹ năng tạo nên sự khác biệt giữa DA "chạy code" và DA "truyền thông hiệu quả". Chart đẹp không phải mục đích — chart đúng, rõ ràng, và actionable mới là mục đích. Từ buổi sau, bạn sẽ học cách kết hợp nhiều chart thành một data story hoàn chỉnh — storytelling with data.
🔗 Tài liệu tham khảo
- Matplotlib Official Documentation — Tài liệu chính thức, đầy đủ nhất
- Seaborn Official Tutorial — Hướng dẫn Seaborn từ cơ bản đến nâng cao
- Matplotlib Gallery — Hàng trăm ví dụ chart có code sẵn
- Edward Tufte — The Visual Display of Quantitative Information — Sách kinh điển về data visualization
- IBCS Standards — Chuẩn quốc tế cho business charts
- Seaborn Color Palettes — Hướng dẫn chọn palette chuyên sâu
- Coblis Color Blindness Simulator — Kiểm tra chart cho người mù màu
- From Data to Viz — Decision tree chọn chart type theo data type