Kiểm Tra Giữa Kì

Author

Hồ Đình Sơn - MSSV: 25E1020052

Published

25 tháng 5, 2026


1 Bài 1. Từ bảng dữ liệu đến câu hỏi phân tích

Cho bộ dữ liệu murders trong package dslabs.

1.1 Hiển thị 6 dòng đầu tiên

library(dslabs)
head(murders, 6)
       state abb region population total
1    Alabama  AL  South    4779736   135
2     Alaska  AK   West     710231    19
3    Arizona  AZ   West    6392017   232
4   Arkansas  AR  South    2915918    93
5 California  CA   West   37253956  1257
6   Colorado  CO   West    5029196    65

1.2 Các biến chính trong bộ dữ liệu

Biến Mô tả
state Tên đầy đủ của các bang tại Mỹ
abb Tên viết tắt (2 ký tự) của các bang
region Vùng địa lý của bang
population Tổng dân số
total Tổng số vụ giết người

1.3 5 câu hỏi phân tích

  • C1. Tiểu bang nào có tổng số vụ giết người bằng súng cao nhất nước Mỹ?
  • C2. Sự chênh lệch về quy mô dân số giữa 4 vùng địa lý lớn tại Mỹ ra sao?
  • C3. Quy mô dân số và tổng số vụ giết người của các bang có mối quan hệ thuận với nhau không?
  • C4. Có tiểu bang nào có mật độ dân cư rất thấp nhưng số vụ giết người lại vọt lên cao bất thường không?
  • C5. Nếu tính thêm biến phụ là Tỷ lệ giết người trên mỗi 100,000 dân, tỷ lệ này phân hóa như thế nào giữa các miền?

1.4 Loại biểu đồ phù hợp

Loại biểu đồ Câu hỏi
Scatterplot C3, C4
Barplot C1, C2, C5

1.5 Câu hỏi bổ sung

Bang nào có dân số lớn nhất?

murders$state[which.max(murders$population)]
[1] "California"

Bang nào có tổng số vụ giết người cao nhất?

murders$state[which.max(murders$total)]
[1] "California"

Dân số và tổng số vụ giết người có liên hệ với nhau không?

Có. Vì dân số và tổng số vụ giết người có liên hệ mật thiết, nên nếu chỉ nhìn vào “tổng số vụ” để đánh giá một bang có an toàn hay không sẽ bị sai lệch. Đó là lý do trong khoa học dữ liệu, người ta phải tính thêm biến phụ là Tỷ lệ giết người trên 100,000 dân (Murder Rate) để so sánh chính xác hơn.

Tỷ lệ giết người khác nhau như thế nào giữa các vùng?

Vùng South thường có tỷ lệ giết người trung bình cao hơn các vùng còn lại, trong khi Northeast thấp nhất.


2 Bài 2. Vẽ scatterplot đầu tiên bằng ggplot2

Sử dụng dữ liệu murders.

library(dslabs)
library(ggplot2)
data("murders")

ggplot(data = murders, aes(x = population / 10^6, y = total)) +
  geom_point(color = "blue", size = 3, alpha = 0.7) +
  labs(
    title = "Mối quan hệ giữa Dân số và Tổng số vụ giết người",
    x = "Dân số (Triệu người)",
    y = "Tổng số vụ giết người"
  ) +
  theme_light()

2.1 Nhận xét

Biểu đồ cho thấy có mối quan hệ thuận chiều giữa dân số và tổng số vụ giết người: bang càng đông dân thì số vụ giết người càng cao.

2.2 Câu hỏi phụ: Vì sao scatterplot phù hợp hơn barplot?

  • Scatterplot: Tìm mối quan hệ (tương quan) giữa 2 biến số, dùng các dấu chấm trên hệ tọa độ X-Y để xem biến này tăng thì biến kia tăng hay giảm, đồng thời phát hiện các điểm bất thường.
  • Barplot: So sánh và xếp hạng giữa các danh mục, dùng chiều cao cột để thấy chênh lệch lớn/nhỏ giữa các đối tượng.

Vì đề yêu cầu nhận xét mối quan hệ giữa dân số và tổng số vụ giết người nên chọn Scatterplot.


3 Bài 3. Phân tích các thành phần của một biểu đồ ggplot2

Phân tích đoạn mã:

murders |>
  ggplot(aes(x = population/10^6, y = total, color = region)) +
  geom_point()

3.1 Các thành phần

Thành phần Giá trị Giải thích
Data murders Bộ dữ liệu đầu vào
Geometry geom_point() Vẽ dạng điểm (scatterplot)
Aesthetic mappings x = population/10^6, y = total, color = region Ánh xạ dữ liệu lên hệ trục và màu sắc

3.2 Vai trò của color = region

color = region giúp phân nhóm và so sánh dữ liệu trực quan theo vùng miền. ggplot2 tự động gán màu khác nhau cho từng vùng và tạo bảng chú thích (legend).

3.3 Viết lại với tất cả điểm màu xanh

murders |>
  ggplot(aes(x = population / 10^6, y = total)) +
  geom_point(color = "blue")


4 Bài 4. So sánh aesthetic mapping và non-aesthetic argument

4.1 Biểu đồ 1 — color = region trong aes()

ggplot(murders, aes(x = population / 10^6, y = total, color = region)) +
  geom_point()

Tạo biểu đồ phân tán với các điểm có nhiều màu sắc khác nhau và tự động xuất hiện bảng chú thích (Legend) ở bên cạnh.

4.2 Biểu đồ 2 — color = "blue" ngoài aes()

ggplot(murders, aes(x = population / 10^6, y = total)) +
  geom_point(color = "blue")

Tạo biểu đồ phân tán với toàn bộ các điểm đồng nhất màu xanh dương, không có bảng chú thích.

4.3 So sánh và giải thích

Biểu đồ 1 (color = region trong aes()) Biểu đồ 2 (color = "blue" ngoài aes())
Màu sắc Nhiều màu theo nhóm Đồng nhất một màu
Legend Có tự động Không có
Ý nghĩa Màu đại diện cho biến dữ liệu Màu thuần túy trang trí

color = region trong aes(): Kết nối màu sắc với biến dữ liệu, hệ thống đọc cột region để phân chia màu sắc.

color = "blue" ngoài aes(): Ép buộc định dạng cố định, bỏ qua thuộc tính dữ liệu.

4.4 Lỗi phổ biến sinh viên hay mắc

Sinh viên thường viết chuỗi ký tự tên màu cố định vào bên trong aes(), ví dụ: aes(..., color = "blue") với mục đích muốn toàn bộ biểu đồ thành màu xanh — nhưng kết quả sẽ không như mong đợi.


5 Bài 5. Vẽ barplot và sắp xếp category

5.1 Tính murder rate

library(dplyr)
library(dslabs)

murders <- murders %>%
  mutate(rate = total / population * 100000)

5.2 Barplot tất cả các bang

library(ggplot2)

ggplot(murders, aes(x = state, y = rate)) +
  geom_bar(stat = "identity") +
  coord_flip()

5.3 Sắp xếp giảm dần và lọc Top 10

murders_desc <- murders %>% arrange(desc(rate))

top10_murders <- murders_desc %>% slice_head(n = 10)

ggplot(top10_murders, aes(x = reorder(state, rate), y = rate)) +
  geom_bar(stat = "identity", fill = "skyblue") +
  coord_flip() +
  labs(
    title = "Top 10 Bang có Murder Rate Cao Nhất",
    x = "Bang",
    y = "Tỷ lệ (trên 100,000 dân)"
  )

5.4 Nhận xét: Sắp xếp theo alphabet

Nếu sắp xếp theo thứ tự Alphabet, biểu đồ sẽ rất khó đọc vì các thanh dài, ngắn bị nằm xen kẽ lộn xộn. Người xem không thể nhận biết ngay bang nào đứng vị trí thứ nhất, thứ nhì mà phải tự dò tìm. Sự trồi sụt bất quy tắc này gây nhiễu thị giác, làm giảm tốc độ nắm bắt thông tin cốt lõi.


6 Bài 6. Histogram, density plot và boxplot

Sử dụng bộ dữ liệu heights trong package dslabs.

6.1 Histogram của biến height

library(ggplot2)
library(dslabs)

ggplot(heights, aes(x = height)) +
  geom_histogram(binwidth = 1, fill = "skyblue", color = "black") +
  labs(title = "Histogram of Heights", x = "Height (inches)", y = "Count")

6.2 Density plot của biến height

ggplot(heights, aes(x = height)) +
  geom_density(fill = "aquamarine", alpha = 0.5) +
  labs(title = "Density Plot of Heights", x = "Height (inches)", y = "Density")

6.3 Boxplot so sánh chiều cao theo giới tính

ggplot(heights, aes(x = sex, y = height, fill = sex)) +
  geom_boxplot() +
  labs(title = "Boxplot of Heights by Sex", x = "Sex", y = "Height (inches)")

6.4 So sánh ưu điểm và hạn chế

Biểu đồ Ưu điểm Hạn chế
Histogram Dễ hiểu, hiển thị tần suất thực tế Phụ thuộc vào binwidth
Density Plot Đường cong mịn, thấy rõ hình dáng phân phối Không cho thấy số quan sát thực tế
Boxplot Tóm tắt nhanh thống kê, phát hiện outliers Che giấu hình dáng phân phối chi tiết

6.5 Kết luận

  • Quan sát phân phối tổng quát: Density plot — đường cong mịn giúp nhận diện hình dáng phân phối.
  • So sánh nam và nữ: Boxplot — đặt các hộp cạnh nhau để đối chiếu trung vị và khoảng biến thiên.

7 Bài 7. Phát hiện biểu đồ gây hiểu nhầm

7.1 Giải thích lý do gây hiểu nhầm

Khi trục y bắt đầu từ 90 thay vì 0, cột công ty A tương ứng với 5 đơn vị (95 - 90), còn công ty B tương ứng với 10 đơn vị (100 - 90). Cột B trông cao gấp đôi cột A, trong khi thực tế B chỉ hơn A khoảng 5.26%.

7.2 Vẽ lại biểu đồ với trục y bắt đầu từ 0

library(ggplot2)

data_doanhthu <- data.frame(
  Cong_ty = c("A", "B"),
  Doanh_thu = c(95, 100)
)

ggplot(data_doanhthu, aes(x = Cong_ty, y = Doanh_thu, fill = Cong_ty)) +
  geom_bar(stat = "identity", width = 0.5) +
  scale_y_continuous(limits = c(0, 110)) +
  labs(
    title = "So sánh doanh thu chính xác giữa hai công ty",
    x = "Công ty",
    y = "Doanh thu"
  ) +
  theme_minimal()

7.3 So sánh cảm nhận thị giác

  • Bắt đầu từ 90: Cột B trông cao gấp đôi A — gây hiểu nhầm nghiêm trọng.
  • Bắt đầu từ 0: Hai cột gần như tương đương, phản ánh đúng thực tế.

7.4 Nguyên tắc

Trục y của biểu đồ cột luôn phải bắt đầu từ số 0.


8 Bài 8. Tái tạo scatterplot hoàn chỉnh cho dữ liệu murders

library(ggplot2)
library(dplyr)
library(ggrepel)
library(dslabs)

r_national <- murders %>%
  summarize(rate = sum(total) / sum(population) * 10^6) %>%
  pull(rate)

ggplot(murders, aes(x = population / 10^6, y = total, color = region, label = abb)) +
  geom_point(size = 3, alpha = 0.7) +
  geom_text_repel(show.legend = FALSE) +
  scale_x_log10() +
  scale_y_log10() +
  geom_abline(
    intercept = log10(r_national), slope = 1,
    color = "darkgray", linetype = "dashed", linewidth = 0.8
  ) +
  labs(
    title = "Tương quan giữa tổng số vụ giết người và quy mô dân số",
    subtitle = "Dữ liệu các bang tại Mỹ (Đường đứt nét thể hiện mức trung bình toàn quốc)",
    x = "Dân số (Đơn vị: Triệu người - Log Scale)",
    y = "Tổng số vụ giết người (Log Scale)",
    color = "Vùng (Region)"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))

8.1 Câu hỏi phân tích

Những bang nào nằm phía trên đường trung bình?

Tiêu biểu nhất là DC (District of Columbia), theo sau là LA (Louisiana), MD (Maryland), MS (Mississippi), và SC (South Carolina).

Vùng nào có xu hướng murder rate cao hơn?

Vùng South có xu hướng murder rate cao hơn hẳn các vùng khác. Vùng Northeast thường nằm dưới đường trung bình.

Vì sao dùng log scale?

  • Tránh co cụm dữ liệu: Dân số và số vụ án chênh lệch quá lớn; log scale trải đều các điểm.
  • Thẳng hóa tỷ lệ: Biến mối quan hệ tỷ lệ thành đường thẳng tham chiếu trực quan.
  • Dễ quan sát tổng thể: Nhìn rõ nhãn của cả bang nhỏ lẫn bang lớn.

9 Bài 9. Thiết kế biểu đồ tốt hơn pie chart

9.1 Dữ liệu

library(ggplot2)

df <- data.frame(
  Nganh = c("Data Science", "AI", "Business Analytics", "Software Engineering", "Cybersecurity"),
  Sinh_vien = c(120, 95, 80, 150, 60)
)

9.2 Pie chart

ggplot(df, aes(x = "", y = Sinh_vien, fill = Nganh)) +
  geom_bar(stat = "identity", width = 1) +
  coord_polar("y", start = 0) +
  labs(title = "Biểu đồ tròn (Pie Chart) số lượng sinh viên") +
  theme_void()

9.3 Barplot

ggplot(df, aes(x = Nganh, y = Sinh_vien, fill = Nganh)) +
  geom_bar(stat = "identity") +
  labs(
    title = "Biểu đồ cột (Barplot) số lượng sinh viên",
    x = "Ngành học",
    y = "Số sinh viên"
  ) +
  theme_minimal()

9.4 So sánh hai biểu đồ

Pie chart Barplot
Đọc giá trị Khó — phải ước góc/diện tích Dễ — so chiều cao cột
So sánh nhiều nhóm Rất kém khi ≥ 5 nhóm Tốt
Trục số Không có

9.5 Vì sao barplot tốt hơn pie chart?

  • Giới hạn nhận thức thị giác: ≥ 5 nhóm thì pie chart bị chia quá nhiều mảnh, gây rối mắt.
  • Độ chính xác: Barplot có trục số cụ thể, pie chart không có — người xem phải đoán diện tích.

9.6 Barplot cải tiến — sắp xếp giảm dần

ggplot(df, aes(x = reorder(Nganh, -Sinh_vien), y = Sinh_vien, fill = Nganh)) +
  geom_bar(stat = "identity") +
  labs(
    title = "Biểu đồ cột cải tiến (Sắp xếp giảm dần)",
    x = "Ngành học",
    y = "Số sinh viên"
  ) +
  theme_minimal()


10 Bài 10. Faceting với dữ liệu Gapminder

library(ggplot2)
library(dplyr)
library(dslabs)

gapminder_filtered <- gapminder %>%
  filter(year %in% c(1962, 1980, 2000, 2012))

ggplot(gapminder_filtered, aes(x = fertility, y = life_expectancy, color = continent)) +
  geom_point(size = 2, alpha = 0.7) +
  facet_wrap(~year, nrow = 2) +
  labs(
    title = "Mối quan hệ giữa Tỷ lệ sinh và Tuổi thọ theo thời gian",
    x = "Tỷ lệ sinh (Fertility rate)",
    y = "Tuổi thọ trung bình (Life expectancy)",
    color = "Châu lục"
  ) +
  theme_minimal()

10.1 Nhận xét xu hướng

Từ 1962 đến 2012, thế giới có xu hướng rõ rệt: tỷ lệ sinh giảmtuổi thọ tăng. Các điểm dịch chuyển từ góc dưới bên phải sang góc trên bên trái.

10.2 Sự khác biệt giữa các châu lục

  1. Châu Âu và Châu Mỹ dẫn đầu: Ngay từ 1962 đã có tuổi thọ cao và tỷ lệ sinh thấp ổn định.
  2. Châu Phi chuyển dịch chậm hơn: Vẫn chiếm phần lớn nhóm có tỷ lệ sinh cao nhất và tuổi thọ thấp nhất, dù đã cải thiện dần.

11 Bài 11. Time series plot về tuổi thọ

11.1 Chọn 5 quốc gia

Châu lục Quốc gia
Châu Á Vietnam
Châu Âu Germany
Châu Mỹ Brazil
Châu Phi South Africa
Châu Đại Dương Australia

11.2 Vẽ line chart

library(gapminder)
library(tidyverse)

selected_countries <- c("Vietnam", "Germany", "Brazil", "South Africa", "Australia")

df_filtered <- gapminder %>%
  filter(country %in% selected_countries)

df_labels <- df_filtered %>%
  filter(year == max(year))

ggplot(df_filtered, aes(x = year, y = lifeExp, color = country, group = country)) +
  geom_line() +
  geom_point() +
  geom_text(data = df_labels, aes(label = country), hjust = -0.2, vjust = 0.5) +
  scale_x_continuous(limits = c(1952, 2012), breaks = seq(1952, 2007, by = 5)) +
  labs(x = "Year", y = "Life Expectancy") +
  theme(legend.position = "none")

11.3 Nhận xét

Việt Nam cải thiện tuổi thọ nhanh nhất: từ 40.4 tuổi (1952) lên 74.2 tuổi (2007) — tăng 33.8 tuổi trong 55 năm (trung bình 0.62 tuổi/năm).

11.4 Câu hỏi phụ

  • Legend gây khó đọc: Khi nhiều đường chồng chéo, mắt người phải liên tục đảo qua lại giữa đường và legend, mất thời gian nhận diện.
  • Khi nào gắn nhãn trực tiếp: Khi số đường ≤ 6–7 và các đường không quá gần nhau ở điểm cuối — nhãn trực tiếp giúp đọc nhanh hơn nhiều.

12 Bài 12. So sánh trước và sau log transformation

12.1 Scatterplot gốc (trục thường)

library(gapminder)
library(tidyverse)

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.5) +
  labs(x = "GDP per Capita", y = "Life Expectancy")

12.2 Scatterplot với log scale

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.5) +
  scale_x_log10() +
  labs(x = "GDP per Capita (Log Scale)", y = "Life Expectancy")

12.3 So sánh

Trục thường Log scale
Phân bố điểm Nén cụm ở phía trái Trải đều
Dạng quan hệ Đường cong phi tuyến Tuyến tính rõ rệt
Quốc gia nghèo Bị che khuất Tách rời, dễ quan sát

12.4 Vì sao log transformation hữu ích?

Dữ liệu lệch phải có phần lớn tập trung ở giá trị nhỏ, chỉ một số ít giá trị cực lớn. Log transformation “thu hẹp” giá trị lớn“kéo giãn” giá trị nhỏ, biến phân phối lệch về dạng chuẩn đối xứng.

12.5 Log scale giúp nhìn rõ nhóm nghèo và trung bình

  • Linear scale: Chia đều (0, 20k, 40k…) → quốc gia nghèo bị nén sát vạch 0.
  • Log scale: Chia theo cấp số nhân (100, 1,000, 10,000…) → phóng to vùng thu nhập thấp.

13 Bài 13. Show the data: Không chỉ vẽ trung bình

13.1 Tạo dữ liệu và vẽ biểu đồ

library(tidyverse)

set.seed(42)
data <- tibble(
  Lớp = rep(c("A", "B", "C"), each = 50),
  Điểm = c(
    rnorm(50, mean = 7, sd = 0.2),
    rnorm(50, mean = 7, sd = 1.5),
    c(rnorm(25, mean = 5, sd = 0.5), rnorm(25, mean = 9, sd = 0.5))
  )
)

# Barplot trung bình
df_mean <- data %>% group_by(Lớp) %>% summarize(Mean = mean(Điểm))

ggplot(df_mean, aes(x = Lớp, y = Mean, fill = Lớp)) +
  geom_col() +
  labs(title = "Barplot: Chỉ hiển thị điểm trung bình", y = "Điểm trung bình") +
  theme(legend.position = "none")

# Boxplot + jitter
ggplot(data, aes(x = Lớp, y = Điểm, fill = Lớp)) +
  geom_boxplot(alpha = 0.5, outlier.shape = NA) +
  geom_jitter(width = 0.2, alpha = 0.6) +
  labs(title = "Boxplot + Jitter: Hiển thị toàn bộ dữ liệu") +
  theme(legend.position = "none")

13.2 Vì sao chỉ nhìn trung bình có thể sai?

  • Bỏ sót độ đồng đều: Lớp A rất đồng đều, lớp B phân hóa cực mạnh — nhưng cùng trung bình.
  • Bỏ sót phân cực: Lớp C bị chia làm hai nhóm cực đoan; trung bình 7 điểm nhưng không có học sinh nào thực sự đạt 7.

14 Bài 14. Thiết kế heatmap cho dữ liệu bệnh truyền nhiễm

14.1 Heatmap bệnh Sởi (Measles)

library(dslabs)
library(tidyverse)

dat_measles <- us_contagious_diseases %>%
  filter(disease == "Measles") %>%
  filter(state %in% c("California", "New York", "Texas"))

ggplot(dat_measles, aes(x = year, y = state, fill = count)) +
  geom_tile() +
  geom_vline(xintercept = 1963, color = "blue", linetype = "dashed", linewidth = 1) +
  scale_fill_gradient(low = "white", high = "red") +
  labs(
    title = "Bản đồ nhiệt bệnh Sởi",
    x = "Năm",
    y = "Bang",
    fill = "Số ca nhiễm"
  ) +
  theme_minimal()

14.2 Nhận xét xu hướng

  • Trước 1963: Các ô màu đỏ đậm liên tục — số ca bệnh sởi rất cao.
  • Sau 1963: Màu chuyển đột ngột sang trắng/hồng nhạt — số ca giảm gần như bằng không ngay sau khi vaccine được đưa vào sử dụng.

14.3 Vì sao heatmap phù hợp?

  • Trực quan hóa mật độ bằng màu sắc: Thay vì hàng chục đường chồng chéo, màu sắc giúp nhận ra đỉnh dịch hay vùng an toàn ngay lập tức.
  • Nén không gian hiệu quả: Hiển thị đồng thời hàng trăm ô dữ liệu (bang × năm) trên một màn hình.

15 Bài 15. Phân tích và sửa một biểu đồ sai nguyên tắc

15.1 Mô tả biểu đồ ban đầu

Sử dụng dữ liệu số sinh viên theo ngành: Data Science (120), AI (95), Business Analytics (80), Software Engineering (150), Cybersecurity (60).

15.2 Ba lỗi chính

  1. Trục Y không bắt đầu từ 0: Bắt đầu từ 50, khiến Cybersecurity (60) trông ngắn hơn nhiều so với thực tế.
  2. Sắp xếp category không hợp lý: Các ngành xếp theo thứ tự ngẫu nhiên, người xem khó tìm thứ hạng.
  3. Không có tiêu đề và nhãn trục rõ ràng: Thiếu ngữ cảnh, nhãn bị khuất do tên dài.

15.3 Vẽ lại biểu đồ đúng nguyên tắc

library(ggplot2)

df <- data.frame(
  major = c("Data Science", "AI", "Business Analytics", "Software Engineering", "Cybersecurity"),
  students = c(120, 95, 80, 150, 60)
)

ggplot(df, aes(x = reorder(major, students), y = students)) +
  geom_col() +
  coord_flip() +
  scale_y_continuous(limits = c(0, 160), expand = c(0, 0)) +
  labs(
    title = "Số lượng sinh viên theo ngành học",
    x = "Ngành học",
    y = "Số sinh viên"
  ) +
  theme_minimal()

15.4 Vì sao phiên bản mới tốt hơn?

  • Trung thực: Trục Y từ 0, chiều dài cột phản ánh đúng tỉ lệ thực tế.
  • Dễ hiểu: coord_flip() giúp đọc tên ngành rõ ràng; reorder() thể hiện thứ hạng ngay lập tức.

15.5 Nguyên tắc thiết kế (150–200 từ)

Trực quan hóa dữ liệu đòi hỏi sự trung thựctối giản. Nguyên tắc cốt lõi đầu tiên là tính toàn vẹn của dữ liệu: đối với biểu đồ cột, trục cơ sở bắt đầu từ số 0 là quy tắc bắt buộc; việc cắt cụp trục Y sẽ làm sai lệch nhận thức thị giác và bóp méo sự thật.

Nguyên tắc thứ hai là giảm gánh nặng nhận thức. Dữ liệu danh mục cần được sắp xếp theo thứ tự để người xem lập tức nhận ra quy luật mà không phải tự so sánh thủ công. Đồng thời, cấu trúc biểu đồ phải thân thiện với việc đọc hiểu — chẳng hạn như chuyển sang cột ngang khi nhãn chữ quá dài.

Cuối cùng, hãy loại bỏ mọi yếu tố gây nhiễu. Hiệu ứng 3D, màu sắc sặc sỡ không cần thiết, bảng chú giải thừa — tất cả phải lược bỏ. Biểu đồ đạt hiệu quả cao nhất khi truyền tải thông tin một cách rõ ràng, gọn gàng và không thể cắt giảm thêm chi tiết nào nữa.


16 Bài 16. Ecological fallacy trong trực quan hóa dữ liệu

library(gapminder)
library(tidyverse)

df_2007 <- gapminder %>% filter(year == 2007)

# 1 & 2. GDP trung bình và barplot
tb <- df_2007 %>%
  group_by(continent) %>%
  summarize(gdpPercap = mean(gdpPercap))

ggplot(tb, aes(x = reorder(continent, gdpPercap), y = gdpPercap, fill = continent)) +
  geom_col(show.legend = FALSE) +
  labs(title = "GDP trung bình (2007)", x = "Châu lục", y = "GDP/người (USD)") +
  theme_minimal()

# 3. Boxplot + jitter từng quốc gia
ggplot(df_2007, aes(x = reorder(continent, gdpPercap, FUN = mean), y = gdpPercap, fill = continent)) +
  geom_boxplot(outlier.shape = NA, show.legend = FALSE, alpha = 0.4) +
  geom_jitter(width = 0.2, alpha = 0.6, show.legend = FALSE) +
  labs(title = "Chi tiết GDP từng quốc gia (2007)", x = "Châu lục", y = "GDP/người (USD)") +
  theme_minimal()

16.1 So sánh hai biểu đồ

  • Barplot: Một con số trung bình duy nhất — cảm giác phân bậc đồng đều và tuyệt đối.
  • Boxplot + Jitter: Thấy rõ phân hóa nội bộ cực lớn và sự chồng lấn giữa các châu lục.

16.2 Vì sao kết luận “mọi quốc gia trong châu lục A đều giàu hơn B” có thể sai?

Trung bình dễ bị kéo lệch bởi vài quốc gia siêu giàu (outliers). Một châu lục có mức trung bình cao không có nghĩa là mọi quốc gia đều giàu — con số tổng thể đó che lấp nhiều quốc gia nghèo hơn bên trong.

16.3 Ecological fallacy là gì?

Ecological fallacy là lỗi logic khi lấy đặc điểm đại diện của một nhóm (tập thể) để áp đặt cho tính chất của từng cá thể đơn lẻ trong nhóm đó. Ví dụ: dù Châu Âu có GDP trung bình cao, vẫn có nhiều quốc gia Đông Âu có GDP thấp hơn một số quốc gia Châu Á — barplot không cho ta thấy điều này.


17 Bài 17. Dùng trực quan hóa để phát hiện bất thường trong dữ liệu

17.1 Tạo dữ liệu và vẽ histogram

library(ggplot2)

set.seed(42)
score <- c(
  rnorm(150, mean = 72, sd = 10),
  rep(65, 40),
  rnorm(10, mean = 63, sd = 0.5)
)
score <- pmin(pmax(round(score), 0), 100)

df <- data.frame(
  student_id = 1:length(score),
  exam_score = score
)

ggplot(df, aes(x = exam_score)) +
  geom_histogram(binwidth = 2, fill = "steelblue", color = "white") +
  geom_vline(xintercept = 65, color = "red", linewidth = 1, linetype = "dashed") +
  labs(
    title = "Exam Scores Distribution",
    x = "Exam Score",
    y = "Students"
  ) +
  theme_minimal()

17.2 Quan sát bất thường

Biểu đồ xuất hiện một cột cao vọt bất thường tại điểm 65. Khoảng điểm 63–64 ngay trước đó hầu như trống rỗng, làm đứt gãy hình dáng phân phối thông thường.

17.3 Ba giả thuyết giải thích

  1. Nâng điểm/Làm tròn: Giám khảo chủ động làm tròn 63–64 lên 65 để sinh viên vừa đủ điểm đậu.
  2. Thiết kế thang điểm: Cấu trúc bài thi hoặc quy trình phúc khảo tự động cộng điểm cho bài sát nút.
  3. Chiến lược ôn thi: Sinh viên chỉ học vừa đủ để qua môn, tạo ra sự tập trung tự nhiên tại ngưỡng 65.

17.4 Dữ liệu cần thu thập thêm

  • Điểm từng câu: Xem bài đạt 65 nhờ phân bố đều hay bất thường ở một câu nào đó.
  • Examiner ID: Hiện tượng này xảy ra ở tất cả giám khảo hay chỉ một vài người?
  • Lịch sử điểm gốc và phúc khảo: Đối chiếu điểm trên bài giấy với điểm nhập hệ thống.

17.5 Kết luận thận trọng

Biểu đồ xuất hiện cấu trúc phi tự nhiên nhưng không thể khẳng định gian lận, vì hiện tượng này có thể do quy tắc làm tròn hợp lệ hoặc chiến lược ôn thi. Tuy nhiên, sự mất cân đối nghiêm trọng giữa 63–64 và 65 là dấu hiệu cảnh báo rõ rệt, gợi ý cần điều tra chuyên sâu để đảm bảo tính minh bạch.


18 Bài 18. Một dữ liệu, hai đối tượng người xem

18.1 Phiên bản 1: Dành cho nhà phân tích dữ liệu

library(gapminder)
library(tidyverse)

df_2007 <- gapminder %>% filter(year == 2007)

ggplot(df_2007, aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
  geom_point(alpha = 0.7) +
  scale_x_log10() +
  facet_wrap(~continent) +
  labs(
    title = "Phân tích đa biến: GDP và Tuổi thọ (2007)",
    subtitle = "Phân mảnh theo Châu lục - Kích thước điểm thể hiện quy mô dân số",
    x = "GDP per Capita (Log Scale)",
    y = "Life Expectancy (Years)",
    size = "Population",
    color = "Continent"
  ) +
  theme_bw()

18.2 Phiên bản 2: Dành cho người xem phổ thông

df_pop <- gapminder %>%
  filter(year == 2007) %>%
  group_by(continent) %>%
  summarize(mean_lifeExp = mean(lifeExp))

ggplot(df_pop, aes(x = reorder(continent, mean_lifeExp), y = mean_lifeExp)) +
  geom_col(fill = "steelblue", width = 0.6) +
  coord_flip() +
  labs(
    title = "Người dân tại Châu Âu và Châu Úc có tuổi thọ trung bình cao nhất",
    x = NULL,
    y = "Tuổi thọ trung bình (năm)"
  ) +
  theme_minimal()

18.3 Câu hỏi phụ

Hai phiên bản khác nhau như thế nào?

Phiên bản 1 (Nhà phân tích) Phiên bản 2 (Người phổ thông)
Biến số 4 biến (GDP, tuổi thọ, dân số, châu lục) 2 biến
Dữ liệu Từng điểm quốc gia thô Tổng hợp trung bình
Cấu trúc Facet 5 ô Một biểu đồ đơn
Tiêu đề Mô tả kỹ thuật Câu khẳng định kết luận

Vì sao biểu đồ EDA không nhất thiết phù hợp để trình bày trước công chúng?

Biểu đồ EDA dùng để đào sâu, tìm lỗi và phát hiện xu hướng ẩn — đòi hỏi người xem phải tự mày mò và có chuyên môn. Người xem phổ thông cần thông điệp đã được lọc sạch nhiễu, hiểu được ngay trong vài giây.

Giữ lại và loại bỏ gì?

  • Loại bỏ: Điểm dữ liệu thô từng quốc gia, biến dân số (pop) và GDP, chia ô (facet).
  • Giữ lại: Biến châu lục và tuổi thọ — cốt lõi của thông điệp.

19 Bài 19. Xử lý overplotting

19.1 Biểu đồ gốc (bị overplotting nặng)

library(gapminder)
library(tidyverse)

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point() +
  scale_x_log10() +
  labs(title = "Gốc: Overplotting nặng")

19.2 Kỹ thuật 1: Alpha + Jitter

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_jitter(alpha = 0.3, width = 0.02) +
  scale_x_log10() +
  labs(title = "Kỹ thuật 1: Alpha + Jitter")

19.3 Kỹ thuật 2: Faceting theo châu lục

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.4) +
  scale_x_log10() +
  facet_wrap(~continent) +
  labs(title = "Kỹ thuật 2: Faceting theo Châu lục")

19.4 Kỹ thuật 3: Hexbin

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_hex(bins = 30) +
  scale_x_log10() +
  scale_fill_viridis_c() +
  labs(title = "Kỹ thuật 3: Hexbin Density")

19.5 So sánh và đánh giá

Kỹ thuật Giữ thông tin Dễ hiểu với người không chuyên
Alpha + Jitter Cao — giữ từng điểm Trung bình
Faceting Cao nhất — bảo toàn outliers Cao
Hexbin Trung bình — gộp thành ô mật độ Cao nhất

Giữ nhiều thông tin nhất: Faceting kết hợp Alpha + Jitter.

Dễ hiểu nhất: Hexbin hoặc Faceting.

19.6 Nhận xét về sự đánh đổi

  • Tính chính xác ↔︎ Tính dễ đọc: Giữ điểm thô chính xác nhất nhưng overplotting làm biểu đồ vô giá trị.
  • Tính dễ đọc ↔︎ Chi tiết: Hexbin tăng dễ đọc nhưng mất chi tiết cá thể.
  • Tính thẩm mỹ: Alpha và Faceting cân bằng giữa thoáng đãng thị giác và bảo toàn cấu trúc dữ liệu.

20 Bài 20. Mini-project: Trực quan hóa review khách sạn Việt Nam

20.1 5 câu hỏi phân tích

  • Q1: Xu hướng mức độ hài lòng (sentiment) thay đổi như thế nào qua các năm?
  • Q2: Thành phố nào có chất lượng dịch vụ khách sạn được đánh giá cao nhất?
  • Q3: Khía cạnh nào là điểm mạnh và nhận nhiều phàn nàn nhất?
  • Q4: Phân bố điểm rating giữa các thành phố có chênh lệch không?
  • Q5: Khách nước ngoài có góc nhìn khác biệt so với khách nội địa không?

20.2 Tạo dữ liệu giả lập

library(tidyverse)
set.seed(42)

cities     <- c("Hà Nội", "TP.HCM", "Đà Nẵng", "Nha Trang", "Hội An")
aspects    <- c("service", "room", "location", "cleanliness", "price")
sentiments <- c("Positive", "Neutral", "Negative")
languages  <- c("Vietnamese", "English")

hotel_data <- tibble(
  hotel_id        = sample(1:50, 1000, replace = TRUE),
  city            = sample(cities, 1000, replace = TRUE,
                           prob = c(0.3, 0.3, 0.2, 0.1, 0.1)),
  year            = sample(2019:2023, 1000, replace = TRUE),
  rating          = round(runif(1000, 1, 5), 1),
  sentiment       = sample(sentiments, 1000, replace = TRUE,
                           prob = c(0.55, 0.25, 0.20)),
  aspect          = sample(aspects, 1000, replace = TRUE),
  review_language = sample(languages, 1000, replace = TRUE,
                           prob = c(0.65, 0.35))
)

20.3 Q1 — Line chart: Xu hướng Sentiment theo thời gian

trend_data <- hotel_data %>%
  count(year, sentiment) %>%
  group_by(year) %>%
  mutate(percent = n / sum(n) * 100)

ggplot(trend_data, aes(x = year, y = percent, color = sentiment, group = sentiment)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 3) +
  scale_color_manual(values = c("Negative" = "red", "Neutral" = "gray", "Positive" = "green")) +
  labs(
    title = "Xu hướng Sentiment của Khách sạn VN (2019–2023)",
    x = "Năm", y = "Tỷ lệ (%)"
  ) +
  theme_minimal()

20.4 Q2 & Q3 — Heatmap: Sentiment theo Thành phố và Khía cạnh

heatmap_data <- hotel_data %>%
  mutate(score = case_when(
    sentiment == "Positive" ~ 1,
    sentiment == "Neutral"  ~ 0,
    TRUE                    ~ -1
  )) %>%
  group_by(city, aspect) %>%
  summarize(mean_score = mean(score), .groups = "drop")

ggplot(heatmap_data, aes(x = aspect, y = city, fill = mean_score)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(low = "red", mid = "white", high = "green", midpoint = 0) +
  labs(
    title = "Ma trận Sentiment theo Khía cạnh và Thành phố",
    x = "Khía cạnh", y = "Thành phố", fill = "Chỉ số hài lòng"
  ) +
  theme_minimal()

20.5 Q4 — Violin plot + Jitter: Phân bố Rating theo Thành phố

ggplot(hotel_data, aes(x = city, y = rating, fill = city)) +
  geom_violin(alpha = 0.5) +
  geom_jitter(width = 0.2, alpha = 0.3, size = 0.8) +
  labs(
    title = "Phân bố Rating theo Thành phố",
    x = "Thành phố", y = "Rating"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

20.6 Q5 — Stacked bar + Facet: Sentiment theo Ngôn ngữ review

lang_data <- hotel_data %>%
  count(review_language, city, sentiment) %>%
  group_by(review_language, city) %>%
  mutate(percent = n / sum(n) * 100)

ggplot(lang_data, aes(x = city, y = percent, fill = sentiment)) +
  geom_bar(stat = "identity") +
  facet_wrap(~review_language) +
  scale_fill_manual(values = c("Negative" = "#e74c3c", "Neutral" = "#95a5a6", "Positive" = "#2ecc71")) +
  coord_flip() +
  labs(
    title = "Cơ cấu Sentiment theo Ngôn ngữ Review và Thành phố",
    x = "Thành phố", y = "Tỷ lệ (%)", fill = "Sentiment"
  ) +
  theme_minimal()

20.7 Cách tránh hiểu sai khi so sánh thành phố có số review khác nhau

  1. Dùng tỷ lệ phần trăm thay vì số lượng tuyệt đối khi so sánh.
  2. Dùng Boxplot thay vì Barplot trung bình: Thành phố ít mẫu sẽ có dải phân phối rộng hơn, cho thấy độ tin cậy thấp hơn.

20.8 Kết luận (250–300 từ)

Dựa trên kết quả trực quan hóa dữ liệu, ngành khách sạn Việt Nam giai đoạn 2019–2023 ghi nhận xu hướng hồi phục và chuyển biến tích cực. Biểu đồ đường cho thấy tỷ lệ đánh giá tích cực (Positive) tăng trưởng ổn định sau giai đoạn biến động, chứng minh nỗ lực nâng cao dịch vụ hậu đại dịch.

Khi đi sâu vào ma trận nhiệt (Heatmap) giữa các thành phố và khía cạnh (aspect), Đà NẵngNha Trang nổi lên là những điểm sáng với chỉ số hài lòng cao toàn diện, đặc biệt ở yếu tố Vị trí (Location) và Vệ sinh (Cleanliness). Ngược lại, các trung tâm đô thị lớn như Hà NộiTP.HCM đối mặt với nhiều thách thức hơn khi trục Giá cả (Price) và Dịch vụ (Service) thường rơi vào dải trung tính hoặc tiêu cực, phản ánh áp lực cạnh tranh và kỳ vọng khắt khe từ khách hàng.

Violin plot cho thấy phân bố rating tại các thành phố du lịch như Hội An và Đà Nẵng tập trung ở mức cao hơn và ít phân tán hơn so với các đô thị lớn. Biểu đồ phân mảnh (Facet) theo ngôn ngữ review chỉ ra khách quốc tế có xu hướng đánh giá khắt khe hơn đối với chất lượng nhân sự và dịch vụ.

Kết quả này gợi ý các nhà quản lý tại đô thị lớn cần ưu tiên tối ưu hóa trải nghiệm dịch vụcân đối chính sách giá để thu hẹp khoảng cách giữa chi phí và sự hài lòng thực tế của du khách. Đồng thời, cần lưu ý rằng các thành phố có ít review hơn (Hội An, Nha Trang) có thể có kết quả bị ảnh hưởng bởi cỡ mẫu nhỏ — cần thu thập thêm dữ liệu để tăng độ tin cậy của phân tích.