Skip to content

📘 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ẽ:

  1. Tạo publication-quality charts với Matplotlib: line, bar, scatter, histogram
  2. Sử dụng Seaborn cho statistical visualization: heatmap, pair plot, violin
  3. Customize chart: colors, labels, annotations, subplots
  4. Á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ốngChart EDA (nhanh)Chart Report (chuyên nghiệp)
Scatter plot doanh thuplt.scatter(x, y) — không labelCó title, axis label, trend line, annotation outlier
Biểu đồ cột theo thángMàu mặc định, không legendConsistent color palette, data labels, brand colors
Heatmap correlationKích thước nhỏ, chữ chồngFont size phù hợp, mask triangle, color scale rõ
Dashboard cho sếpNhiều chart rời rạcSubplot 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 chart
  • ax.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êu

Subplots & 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 in
  • dpi=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 đíchSeabornMatplotlib
Nhanh, statisticalsns.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 nhanhDù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ào

Multi-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
ParameterMô tảGiá trị ví dụ
figure.figsizeKích thước mặc định(12, 6)
figure.dpiĐộ phân giải100 (screen), 150 (report)
axes.titlesizeFont size tiêu đề14
axes.labelsizeFont size label trục12
axes.gridHiển thị gridTrue / False
axes.spines.topViền trênFalse (thường ẩn)
axes.spines.rightViền phảiFalse (thường ẩn)
lines.linewidthĐộ dày line mặc định2.0
font.familyFont chữ"sans-serif"
savefig.dpiDPI khi lưu file150

📌 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ệucâu hỏi phân tích:

Câu hỏi phân tíchData TypeChart TypeSeaborn / Matplotlib
Xu hướng theo thời gianTime seriesLine chartax.plot() / sns.lineplot()
So sánh giữa nhómCategoricalBar chartax.bar() / sns.barplot()
Phân phối 1 biếnNumericalHistogram / KDEsns.histplot() / sns.kdeplot()
Phân phối theo nhómNum + CatBox / Violin plotsns.boxplot() / sns.violinplot()
Quan hệ 2 biếnNum + NumScatter plotax.scatter() / sns.scatterplot()
Tương quan nhiều biếnMultiple NumHeatmapsns.heatmap()
Tất cả cặp biếnMultiple NumPair plotsns.pairplot()
Tỷ lệ phần trămCategoricalPie / Donut (hạn chế)ax.pie()
Thành phần theo thời gianTime + CatStacked bar / Areaax.stackplot()
Đếm frequencyCategoricalCount plotsns.countplot()

⚠️ 3 chart type nên TRÁNH

  1. 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ế
  2. 3D charts: thêm chiều không thêm thông tin, distort perception — luôn dùng 2D
  3. 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:

Data-Ink Ratio=Data InkTotal InkMaximize

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 target

Common 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 overplotting

4. 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ệtEnglishGiải thích
Trực quan hóa dữ liệuData VisualizationBiểu diễn dữ liệu bằng hình ảnh — chart, graph, map
Biểu đồ phân tánScatter PlotHiển thị mối quan hệ giữa 2 biến numerical — mỗi điểm = 1 observation
Bản đồ nhiệtHeatmapMa trận màu thể hiện giá trị — thường dùng cho correlation matrix
Bảng màuColor PaletteBộ màu sắc sử dụng trong chart — qualitative, sequential, diverging
Chú thíchAnnotationGhi chú trực tiếp trên chart — mũi tên, text box, highlight
Biểu đồ conSubplotNhiều chart trong 1 figure — so sánh side-by-side
Tỷ lệ data-inkData-Ink RatioNguyên tắc Tufte: tối đa ink cho data, tối thiểu ink trang trí
Biểu đồ violinViolin PlotKết hợp box plot + KDE — thấy hình dạng phân phối đầy đủ
Biểu đồ cặpPair PlotScatter matrix cho tất cả cặp biến — tổng quan relationship
Hỗ trợ tiếp cậnAccessibilityThiế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()GridSpec cho 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ệu

Bạ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

  1. Matplotlib Official Documentation — Tài liệu chính thức, đầy đủ nhất
  2. Seaborn Official Tutorial — Hướng dẫn Seaborn từ cơ bản đến nâng cao
  3. Matplotlib Gallery — Hàng trăm ví dụ chart có code sẵn
  4. Edward Tufte — The Visual Display of Quantitative Information — Sách kinh điển về data visualization
  5. IBCS Standards — Chuẩn quốc tế cho business charts
  6. Seaborn Color Palettes — Hướng dẫn chọn palette chuyên sâu
  7. Coblis Color Blindness Simulator — Kiểm tra chart cho người mù màu
  8. From Data to Viz — Decision tree chọn chart type theo data type