library(dslabs)Bài Kiểm tra Giữa kỳ
25E1020035
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.
Yêu cầu:
- Hiển thị 6 dòng đầu tiên của dữ liệu.
- Xác định các biến chính trong bộ dữ liệu.
- Viết ít nhất 5 câu hỏi phân tích có thể trả lời bằng trực quan hóa dữ liệu.
- Cho biết câu hỏi nào phù hợp với scatterplot, câu hỏi nào phù hợp với barplot.
Câu hỏi bổ sung
Bang nào có dân số lớn nhất?
Bang nào có tổng số vụ giết người cao nhất?
Dân số và tổng số vụ giết người có liên hệ với nhau không?
Tỷ lệ giết người khác nhau như thế nào giữa các vùng?
Ans:
1 .
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
2 . Biến chính trong bộ dữ liệu:
state: tên đầy đủ của các tiểu bang Hoa Kỳ
abb: tên viết tắt của tiểu bang
region: Vùng miền của các tiểu bang
population: tổng dân số
total: tổng só vụ giết người
3 . 5 câu hỏi phân tích:
C1: Bang nào có dân số lớn nhất?
C2: Bang nào có tổng số vụ giết người cao nhất?
C3: Bang nào có tỷ lệ giết người cao nhất?
C4: Dân số và tổng số vụ giết người có liên hệ với nhau không?
C5: Tỷ lệ giết người khác nhau như thế nào giữa các vùng?
4 .
Scatterplot: Câu 4, 5
Barplot: Câu 1, 2, 3
Câu hỏi bổ sung
murders$state[which.max(murders$population)][1] "California"
murders$state[which.max(murders$total)][1] "California"
Có, thường có liên hệ dương giữa dân số và tổng số vụ giết người.
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ường thấp hơn
Bài 2. Vẽ scatterplot đầu tiên bằng ggplot2
Sử dụng dữ liệu murders.
Yêu cầu:
Vẽ scatterplot với:
- Trục x: population
- Trục y: total
Viết lại biểu đồ với population / 10^6 để biểu diễn dân số theo đơn vị triệu người.
Thêm tiêu đề, nhãn trục x, nhãn trục y.
Nhận xét mối quan hệ giữa dân số và tổng số vụ giết người.
Câu hỏi phụ
- Vì sao scatterplot phù hợp hơn barplot trong bài này?
Ans:
library(ggplot2)ggplot(murders, aes(x = population, y = total)) +
geom_point() +
labs(
title = "Population and Total murders",
x = "Population",
y = "Total"
)Viết lại biểu đồ:
ggplot(murders, aes(x = population / 10^6, y = total)) +
geom_point() +
labs(
title = "Population(Mil) and Total murders",
x = "Population(Mil)",
y = "Total"
)Câu hỏi phụ:
- scatterplot phù hợp hơn barplot trong bài này vì ta đang xem mối quan hệ giữa hai biến liên tục.
Bài 3. Phân tích các thành phần của một biểu đồ ggplot2
Cho đoạn mã sau:
murders |>
ggplot(aes(x = population/10^6, y = total, color = region)) +
geom_point()Yêu cầu:
Xác định thành phần data.
Xác định thành phần geometry.
Xác định các aesthetic mappings.
4.Giải thích vai trò của color = region.
5.Viết lại đoạn mã sao cho tất cả các điểm có màu xanh, không phân theo vùng.
Ans:
1 . Thành phần data: murders
2 . Thành phần geometry: geom_point()
3 . Các aesthetic mappings:
x = population/10\^6
y = total
color = region
4 . Vai trò của color = region
Dùng để phân biệt các điểm theo vùng
Mỗi vùng có màu khác nhau, giúp dễ nhìn hơn
5 . Viết lại đoạn mã:
murders |>
ggplot(aes(x = population/10^6, y = total)) +
geom_point(color = "blue")Bài 4. So sánh aesthetic mapping và non-aesthetic argument
Sử dụng dữ liệu murders.
Yêu cầu tạo hai biểu đồ:
Biểu đồ 1:
ggplot(murders, aes(x = population/10^6, y = total, color = region)) +
geom_point()Biểu đồ 2:
ggplot(murders, aes(x = population/10^6, y = total)) +
geom_point(color = "blue")Yêu cầu:
Chạy hai đoạn mã.
So sánh kết quả.
Giải thích sự khác nhau giữa color = region trong aes() và color = “blue” ngoài aes().
Nêu một lỗi phổ biến sinh viên hay mắc khi dùng aes().
Ans:
1 .
# Biểu đồ 1
ggplot(murders, aes(x = population/10^6, y = total, color = region)) +
geom_point()# Biểu đồ 2
ggplot(murders, aes(x = population/10^6, y = total)) +
geom_point(color = "blue")2 .
Biểu đồ 1 tô màu các điểm theo từng region
Biểu đồ 2 chỉ tô màu cố định tất cả các điểm
3 . Sự khác nhau:
color = region trong aes() là aesthetic mapping
color = "blue" ngoài aes() là non-aesthetic argument
Khi viết
color = regiontrongaes(). R sẽ hiểu rằng phải thay đổi màu sắc theo biếnregiontrong dữ liệu.Khi viết
color = "blue"ngoàiaes(). R sẽ ấn cố định các điểm thành màu xanh, không phụ thuộc vào dữ liệu.
4 . Lỗi phổ biến của sinh viên:
geom_point(aes(color = "blue"))
- Đây là sai cách trong trường hợp muốn tô toàn bộ điểm màu xanh.
Bài 5. Vẽ barplot và sắp xếp category
Sử dụng dữ liệu murders.
Yêu cầu:
Tính murder rate theo công thức:
`rate = total / population * 100000`Vẽ barplot biểu diễn murder rate của các bang.
Sắp xếp các bang theo murder rate giảm dần.
Chỉ hiển thị 10 bang có murder rate cao nhất.
Nhận xét: Nếu sắp xếp theo alphabet thì biểu đồ khó đọc hơn như thế nào?
Ans:
# 1 .
murder_rate <- murders
murder_rate$rate <- murder_rate$total / murder_rate$population * 100000
# 3 .
top10 <- murder_rate[order(-murder_rate$rate), ][1:10, ]
#4
ggplot(top10, aes(x = reorder(state, rate), y = rate)) +
geom_bar(stat = "identity", fill = "steelblue") +
coord_flip() +
labs(
title = "10 Bang có tỉ lệ giết người cao nhất",
x = "Tỉnh",
y = "Tỷ lệ giết người"
)5 . Việc sắp xếp theo giảm dần giúp biểu đồ dễ đọc và dễ so sánh hơn so với sắp xếp theo alphabet, vì người xem nhìn ngay được bang nào có tỷ lệ cao nhất.
Bài 6. Histogram, density plot và boxplot
Sử dụng bộ dữ liệu heights trong package dslabs.
Yêu cầu:
Vẽ histogram của biến height.
Vẽ density plot của biến height.
Vẽ boxplot so sánh chiều cao theo giới tính.
So sánh ưu điểm và hạn chế của ba loại biểu đồ trên.
Cho biết biểu đồ nào phù hợp nhất để quan sát phân phối tổng quát, biểu đồ nào phù hợp nhất để so sánh nam và nữ.
Ans:
# Histogram
ggplot(heights, aes(x = height)) +
geom_histogram(binwidth = 1, color = "white", fill = "steelblue") +
labs(title = "Histogram of Heights", x = "Heights", y = "Count")# Density plot
ggplot(heights, aes(x = height)) +
geom_density(fill = "purple") +
labs(title = " Density of Heights", x = "Heights", y = "Density")# Boxplot
ggplot(heights, aes(x = sex, y = height, fill = sex)) +
geom_boxplot() +
labs(title = "Heights by sex", x = "Sex", y = "Heights")4 . So sánh biểu đồ:
Histogram: trực quan, dễ hiểu, cho thấy phân phối dữ liệu theo từng khoảng; hạn chế là phụ thuộc vào cách chọn
Density: mượt và rõ xu hướng tổng quát; hạn chế là không thể hiện số lượng quan sát cụ thể theo từng khoảng.
Boxplot: rất tốt để so sánh giữa các nhóm và phát hiện ngoại lệ; hạn chế là không cho thấy chi tiết hình dạng phân phối.
5 .
Density plot là lựa chọn tốt nhất để quan sát phân phối tổng quát
Boxplot là lựa chọn tốt để so sánh giữa nam và nữ
Bài 7. Phát hiện biểu đồ gây hiểu nhầm
Giả sử có biểu đồ cột so sánh doanh thu của hai công ty:
| Công ty | Doanh thu |
|---|---|
| A | 95 |
| B | 100 |
Một biểu đồ cột được vẽ với trục y bắt đầu từ 90 thay vì 0.
Yêu cầu:
Giải thích vì sao biểu đồ này có thể gây hiểu nhầm.
Vẽ lại biểu đồ với trục y bắt đầu từ 0.
So sánh cảm nhận thị giác giữa hai biểu đồ.
Rút ra nguyên tắc khi dùng barplot để so sánh độ lớn.
Ans:
1 . Vì với doanh thu 95 và 100, mặc dù chỉ chênh 5, nhưng khi cắt trục từ 90, phần chênh nhìn bằng mắt sẽ bị phóng đại rất nhiều.
2 . Vẽ lại biểu đồ:
df <- data.frame(
Công_ty = c("A", "B"),
Doanh_thu = c(95, 100)
)
ggplot(df, aes(x = Công_ty, y = Doanh_thu)) +
geom_col(fill = "steelblue") +
ylim(0, 100)3 . So sánh:
Trục bắt đầu từ 90: cột B cao hơn A rất rõ, khiến chênh lệch nhìn như rất lớn.
Trục bắt đầu từ 0: hai cột chỉ lệch nhẹ, phản ánh đúng mức chênh 5 đơn vị.
4 . Nguyên tắc:
- Khi so sánh độ lớn, giá trị nên bắt đầu từ 0 để không bị lệch cảm nhận người xem. chỉ nên cắt khi gặp trường hợp đặc biệt
Bài 8. Tái tạo biểu đồ scatterplot hoàn chỉnh cho dữ liệu murders
Sử dụng dữ liệu murders.
Yêu cầu tạo biểu đồ gồm:
Trục x: dân số theo đơn vị triệu người.
Trục y: tổng số vụ giết người.
Màu sắc thể hiện region.
Dùng log scale cho cả hai trục.
Thêm nhãn viết tắt bang bằng abb.
Thêm đường tham chiếu thể hiện murder rate trung bình toàn quốc.
Thêm tiêu đề, phụ đề, nhãn trục và chú thích rõ ràng.
Câu hỏi phân tích:
Những bang nào nằm phía trên đường trung bình?
Vùng nào có xu hướng murder rate cao hơn?
Vì sao dùng log scale trong biểu đồ này?
Ans:
ggplot(murders, aes(x = population/10^6, y = total, color = region)) +
geom_point(size = 3) +
geom_text(aes(label = abb), hjust = -0.2, size = 3, show.legend = FALSE) +
scale_x_log10() +
scale_y_log10() +
labs(
title = "Số vụ giết người theo dân số tại USA",
subtitle = "Đường đứt nét thể hiện murder rate trung bình toàn quốc",
x = "Dân số (triệu, log scale)",
y = "Tổng vụ giết người (log scale)",
color = "Vùng",
caption = "Nguồn: dslabs:data(murders)"
)Câu hỏi phân tích:
- Bang nào nằm phía trên đường trung bình?
Các bang hay được nhắc đến trong nhóm này gồm Louisiana, Mississippi, Alabama, Maryland, South Carolina, Georgia, Tennessee, Arkansas, Arizona, Missouri, Delaware, New Mexico, Nevada, Michigan, Illinois.
- Vùng nào có xu hướng murder rate cao hơn?
Các bang thuộc vùng South thường có xu hướng nằm cao hơn và xa hơn so với các vùng khác, cho thấy murder rate cao hơn
- Vì sao dùng log scale?
log scale giúp so sánh xu hướng giữa các bang rõ hơn và dễ nhìn hơn.
Bài 9. Thiết kế biểu đồ tốt hơn pie chart
Cho bảng dữ liệu giả định:
| Ngành_học | Số_sinh_viên |
|---|---|
| Data Science | 120 |
| AI | 95 |
| Business Analytics | 80 |
| Software Engineering | 150 |
| Cybersecurity | 60 |
Yêu cầu:
Vẽ pie chart.
Vẽ barplot.
So sánh hai biểu đồ về khả năng đọc và so sánh số liệu.
Giải thích vì sao barplot thường tốt hơn pie chart khi cần so sánh nhiều nhóm.
Cải tiến barplot bằng cách sắp xếp ngành học theo số sinh viên giảm dần.
Ans:
data <- data.frame(
Nganh = c("Data Science", "AI", "Business Analytics", "Software Engineering", "Cybersecurity"),
Sinhvien = c(120, 95, 80, 150, 60)
)
data Nganh Sinhvien
1 Data Science 120
2 AI 95
3 Business Analytics 80
4 Software Engineering 150
5 Cybersecurity 60
# Pie chart
ggplot(data, aes(x = "", y = Sinhvien, fill = Nganh)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y") +
labs(
title = "Phân phối sinh viên theo ngành học (Pie chart)",
fill = "Ngành học",
) +
theme_void()# Barplot
ggplot(data, aes(x = Nganh, y = Sinhvien, fill = Nganh)) +
geom_col() +
labs(
title = "Phân phối sinh viên theo ngành học (Barplot)",
fill = "Ngành học",
x = "Ngành",
y = "Sinh viên"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 15, hjust = 0.5))So sánh biểu đồ:
Pie chart cho thấy mỗi ngành chiếm bao nhiêu phần trong tổng số sinh viên, nhưng so sánh không dễ vì phải ước lượng diện tích
Barplot dễ đọc hơn vì chiều cao cột được so sánh trực tiếp trên cùng một trục, nên nhìn biết ngành nào nhiều sinh viên hơn
Vì sao barplot thường tốt hơn?
Người xem so sánh độ dài cột nhanh và chính xác hơn so với so sánh lát cắt
Barplot phù hợp với nhiều danh mục hơn
Barplot dễ sắp xếp theo lớn đến nhỏ
Cải tiến barplot:
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
data_sorted <- data |> arrange(desc(Sinhvien)) |> mutate(Nganh = factor(Nganh, levels = Nganh))
ggplot(data_sorted, aes(x = Nganh, y = Sinhvien, fill = Nganh)) +
geom_col() +
labs(
title = "Phân phốt sinh viên theo ngành học (Bar Chart - Giảm dần)",
x = "Ngành học",
y = "Số sinh viên",
fill = "Ngành học"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 15, hjust = 0.5))Bài 10. Faceting với dữ liệu Gapminder
Sử dụng dữ liệu gapminder trong package dslabs.
Yêu cầu:
Lọc dữ liệu cho các năm 1962, 1980, 2000 và 2012.
Vẽ scatterplot:
Trục x: fertility
Trục y: life expectancy
Màu sắc: continent
Dùng facet_wrap() để tạo biểu đồ riêng cho từng năm.
Nhận xét xu hướng thay đổi của fertility và life expectancy theo thời gian.
Nêu ít nhất 2 nhận xét về sự khác biệt giữa các châu lục.
Ans:
data(gapminder)
gapminder_4years <- gapminder |>
filter(year %in% c(1962, 1980, 2000, 2012))
ggplot(gapminder_4years, aes(x = fertility, y = life_expectancy, color = continent)) +
geom_point(alpha = 0.7) +
facet_wrap(~ year) +
labs(
title = "Fertility and Life Expectancy by Year",
subtitle = "Dựa vào năm 1962, 1980, 2000, 2012",
x = "Fertility",
y = "Life Expectancy",
color = "Continent"
)data(gapminder)
gapminder_4years <- gapminder |> filter(year %in% c(1962, 1980, 2000, 2012))
ggplot(gapminder_4years, aes(x = fertility, y = life_expectancy, color = continent)) + geom_point(alpha = 0.7) + facet_wrap(~ year) + labs( title = “Fertility and Life Expectancy by Year”, subtitle = “Dựa vào năm 1962, 1980, 2000, 2012”, x = “Fertility”, y = “Life Expectancy”, color = “Continent” )
Nhận xét:
Xu hướng thời gian
Qua các năm, có thể thấy xu hướng chung là fertility giảm dần trong khi life expectancy tăng dần. cho thấy các quốc gia có ít con hơn nhưng tuổi thọ cao hơn theo thời gian
Sự khác biệt giữa các châu lục:
-, Châu Phi thường có fertility cao hơn và life expectancy thấp hơn so với các châu lục khác.
-, Châu Âu và Bắc Mỹ thường có fertility thấp hơn và life expectancy cao hơn.
-, Châu Đại Dương thường có số lượng quốc gia ít nên điểm dữ liệu ít hơn các châu lục khác.
-, Châu Á nằm ở mức trung gian và có sự phân tán khá lớn giữa các quốc gia.
Bài 11. Time series plot về tuổi thọ
Sử dụng dữ liệu gapminder.
Yêu cầu:
Chọn 5 quốc gia bất kỳ thuộc các châu lục khác nhau.
Vẽ line chart biểu diễn life expectancy theo năm.
Mỗi quốc gia là một đường khác nhau.
Thêm nhãn trực tiếp tên quốc gia ở cuối mỗi đường nếu có thể.
Nhận xét quốc gia nào cải thiện tuổi thọ nhanh nhất.
Ans:
library(ggrepel)
countries <- c("Vietnam", "United States", "Brazil", "India", "South Africa")
df <- gapminder |> filter(country %in% countries)
ggplot(df, aes(x = year, y = life_expectancy, color = country)) +
geom_line(linewidth = 1) +
geom_text_repel(
data = df |> group_by(country) |> filter(year == max(year)),
aes(label = country),
nudge_x = 1,
hjust = 0,
show.legend = FALSE
) +
labs(
title = "Life expectancy over time",
subtitle = "5 Quốc gia khác nhau",
x = "Year",
y = "Life expectancy",
color = "Country",
) +
theme_minimal()Nhận xét:
- Trong nhóm ví dụ này, Việt Nam thường là nước cải thiện tuổi thọ khá nhanh, đặc biệt khi so với mức xuất phát thấp hơn ban đầu
Bài 12. So sánh dữ liệu trước và sau log transformation
Sử dụng dữ liệu gapminder.
Yêu cầu:
- Vẽ scatterplot giữa gdp và life_expectancy.
- Vẽ lại biểu đồ với trục x dùng log scale.
- So sánh hai biểu đồ.
- Giải thích vì sao log transformation hữu ích khi dữ liệu bị lệch phải.
- Cho biết log scale giúp nhìn rõ nhóm quốc gia nghèo và trung bình như thế nào.
Ans:
df <- gapminder[gapminder$year == 2007, ]
# ── Tính GDP/người ──────────────────────────────────────────────
df$gdpPercap <- df$gdp / df$population
# ── Biểu đồ 1: Linear scale ────────────────────────────────────
ggplot(df, aes(x = gdpPercap, y = life_expectancy, color = continent)) +
geom_point(alpha = 0.7, size = 3) +
labs(title = "Linear scale",
x = "GDP/người (USD)",
y = "Tuổi thọ (năm)",
color = "Lục địa") +
theme_minimal()Warning: Removed 4 rows containing missing values or values outside the scale range
(`geom_point()`).
# ── Biểu đồ 2: Log scale ───────────────────────────────────────
ggplot(df, aes(x = gdpPercap, y = life_expectancy, color = continent)) +
geom_point(alpha = 0.7, size = 3) +
scale_x_log10(labels = scales::dollar) +
labs(title = "Log scale",
x = "GDP/người (USD, log)",
y = "Tuổi thọ (năm)",
color = "Lục địa") +
theme_minimal()Warning: Removed 4 rows containing missing values or values outside the scale range
(`geom_point()`).
So sánh:
Biểu đồ (linear) cho thấy hầu hết các quốc gia bị dồn về phía trái do một số nước giàu như Mỹ, Na Uy, Singapore kéo trục x rất xa — rất khó phân biệt các nước nghèo và trung bình với nhau.
Biểu đồ (log scale) trải đều các điểm dữ liệu hơn — hiện ra rõ ràng thành 3 cụm riêng biệt.
Log transformation hữu ích khi dữ liệu bị lệch phải vì:
- log scale nén các giá trị lớn lại và kéo giãn các giá trị nhỏ ra.
Log scale giúp nhìn rõ nhóm nghèo và trung bình:
- Trên log scale, các nước như Ethiopia ($690), Nigeria ($2.000), và South Africa ($9.270) được tách ra rõ ràng — giúp thấy được mối tương quan giữa GDP và tuổi thọ ngay trong nhóm nghèo.
Bài 13. Show the data: Không chỉ vẽ trung bình
Cho dữ liệu giả định về điểm số của ba lớp A, B, C.
Yêu cầu:
- Tạo dữ liệu giả lập sao cho:
- Ba lớp có điểm trung bình gần giống nhau.
- Nhưng độ phân tán điểm khác nhau.
- Vẽ barplot thể hiện điểm trung bình của ba lớp.
- Vẽ boxplot hoặc violin plot cho ba lớp.
- Thêm jitter points để hiển thị từng sinh viên.
- Giải thích vì sao chỉ nhìn trung bình có thể dẫn đến kết luận sai.
Ans:
# Tạo dữ liệu
set.seed(42)
df <- data.frame(
lop = rep(c("Lớp A", "Lớp B", "Lớp C"), each = 30),
diem = c(
rnorm(30, mean = 7.5, sd = 0.3), # A
rnorm(30, mean = 7.5, sd = 2.0), # B
c(rnorm(15, mean = 4, sd = 0.5), # C
rnorm(15, mean = 10, sd = 0.5))
)
)
#Tính trung bình
tb <- aggregate(diem ~ lop, data = df, FUN = mean)
# Barplot
ggplot(tb, aes(x = lop, y = diem, fill = lop)) +
geom_bar(stat = "identity") +
labs(title = "Chỉ nhìn trung bình", x = "Lớp", y = "Điểm") +
theme_minimal()# Boxplot
ggplot(df, aes(x = lop, y = diem, fill = lop)) +
geom_boxplot() +
labs(
title = "Toàn bộ dữ liệu", x = "Lớp", y = "Điểm"
) +
theme_minimal()Giải thích:
- Barplot ba lớp trông giống hệt nhau. Chỉ nhìn trung bình dễ dẫn đến kết luận sai như “ba lớp học tốt như nhau” trong khi Lớp C nửa yếu nửa giỏi.
Bài 14. Thiết kế heatmap cho dữ liệu bệnh truyền nhiễm
Giả sử có dữ liệu gồm:
state
year
disease
cases
Yêu cầu:
Chọn một bệnh truyền nhiễm.
Vẽ heatmap:
Trục x: năm
Trục y: bang
Màu sắc: số ca bệnh hoặc tỷ lệ ca bệnh.
Nếu có năm vaccine được giới thiệu, thêm đường dọc đánh dấu năm đó.
Nhận xét xu hướng trước và sau khi vaccine xuất hiện.
Giải thích vì sao heatmap phù hợp với dữ liệu nhiều bang và nhiều năm.
Ans:
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()Bài 15. Phân tích và sửa một biểu đồ sai nguyên tắc
Tìm hoặc tự tạo một biểu đồ có ít nhất 3 vấn đề sau:
Trục y không bắt đầu từ 0 trong barplot.
Dùng pie chart cho quá nhiều nhóm.
Dùng màu sắc khó phân biệt. - Không có tiêu đề hoặc nhãn trục.
Dùng hiệu ứng 3D không cần thiết. - Sắp xếp category không hợp lý.
Làm tròn số liệu quá nhiều hoặc hiển thị quá nhiều chữ số thập phân.
Yêu cầu:
Mô tả biểu đồ ban đầu.
Chỉ ra ít nhất 3 lỗi.
Vẽ lại biểu đồ tốt hơn.
Giải thích vì sao phiên bản mới dễ hiểu và trung thực hơn.
Viết một đoạn ngắn 150–200 từ trình bày nguyên tắc thiết kế rút ra từ bài này.
Ans:
# Tạo biểu đồ
df <- data.frame(
major = c("Data Science", "AI", "Business Analytics", "Software Engineering", "Cybersecurity"),
students = c(120, 95, 80, 150, 60)
)
ggplot(df, aes(x = major, y = students)) +
geom_col() +
labs(
x = "Ngành học",
y = "Số sinh viên"
) +
theme_minimal()Mô tả biểu đồ:
- Biểu đồ ban đầu là một barplot so sánh doanh số của nhiều ngành hoặc nhiều nhóm, nhưng được thiết kế rất kém
Chỉ ra lỗi:
Trục y không bắt đầu từ 0, làm phóng đại sự khác biệt.
Màu sắc khó phân biệt, khiến biểu đồ rối mắt.
Không có tiêu đề, nên người xem không biết đang nhìn dữ liệu gì.
Vẽ lại biểu đồ:
df <- data.frame(
major = c("Data Science", "AI", "Business Analytics", "Software Engineering", "Cybersecurity"),
students = c(120, 95, 80, 150, 60)
)
df$major <- factor(df$major, levels = df$major[order(df$students, decreasing = TRUE)])
ggplot(df, aes(x = major, y = students, fill = major)) +
geom_col() +
labs(
title = "Số sinh viên theo ngành",
x = "Ngành học",
y = "Số sinh viên"
) +
theme_minimal() +
theme(legend.position = "none")Vì sao phiên bản mới tốt hơn
- Phiên bản mới dễ hiểu hơn vì người xem có thể so sánh chiều cao cột trực tiếp trên cùng một trục từ 0. Việc sắp xếp giảm dần giúp nhìn ra ngay ngành nào nhiều sinh viên nhất. Màu sắc đơn giản và có nhãn rõ ràng giúp biểu đồ trung thực và ít gây nhầm lẫn hơn
Đoạn từ 150-200 từ:
- Biểu đồ tốt phải trung thực, dễ đọc và phù hợp với mục tiêu so sánh dữ liệu. Từ bài này, có thể rút ra rằng trục giá trị của barplot nên bắt đầu từ 0 để không làm phóng đại chênh lệch giữa các nhóm. Ngoài ra, màu sắc cần đơn giản và nhất quán để tránh gây rối mắt, còn nhãn trục và tiêu đề phải đầy đủ để người xem hiểu biểu đồ đang nói về điều gì. Nếu có nhiều nhóm, nên sắp xếp theo thứ tự hợp lý, thường là từ lớn đến nhỏ, để việc so sánh trực quan hơn. Pie chart chỉ nên dùng khi số nhóm ít, còn nếu nhóm quá nhiều thì rất khó đọc. Biểu đồ ba chiều hoặc hiệu ứng trang trí không cần thiết cũng nên tránh vì dễ làm mất thông tin thật. Nói chung, một biểu đồ tốt không chỉ đẹp mà còn phải giúp người xem hiểu dữ liệu nhanh, đúng và chính xác.
Bài 16. Ecological fallacy trong trực quan hóa dữ liệu
Sử dụng dữ liệu gapminder.
Yêu cầu:
- Tính GDP trung bình theo châu lục ở một năm cụ thể.
- Vẽ barplot so sánh GDP trung bình giữa các châu lục.
- Sau đó vẽ boxplot hoặc jitter plot thể hiện GDP của từng quốc gia trong mỗi châu lục.
- So sánh hai biểu đồ.
- Giải thích vì sao kết luận “mọi quốc gia trong châu lục A đều giàu hơn châu lục B” có thể là sai.
- Viết một đoạn giải thích khái niệm ecological fallacy bằng ví dụ từ biểu đồ của em.
Ans:
data(gapminder)
df <- gapminder[gapminder$year == 2007, ]
df <- df[!is.na(df$gdp) & !is.na(df$population), ]
df$gdpPercap <- df$gdp / df$population
#Tính trung bình châu lục:
tb <- aggregate(gdpPercap ~ continent, data = df, FUN = mean)
#barplot trung bình
ggplot(tb, aes(x = continent, y = gdpPercap, fill = continent)) +
geom_bar(stat = "identity", show.legend = FALSE) +
labs(title = "GDP/người trung bình theo châu lục (2007)",
x = "Châu lục", y = "GDP/người (USD)") +
theme_minimal()#boxplot toàn bộ dữ liệu gdp
ggplot(df, aes(x = continent, y = gdpPercap, fill = continent)) +
geom_boxplot(alpha = 0.5, show.legend = FALSE) +
geom_jitter(width = 0.2, alpha = 0.6, show.legend = FALSE) +
labs(title = "GDP/người từng quốc gia theo châu lục (2007)",
x = "Châu lục", y = "GDP/người (USD)") +
theme_minimal()So sánh biểu đồ:
Barplot chỉ thấy một con số duy nhất cho mỗi châu lục — trông như Châu Âu và Châu Mỹ đồng đều giàu, Châu Phi đồng đều nghèo.
Boxplot + jitter cho thấy thực tế phức tạp hơn nhiều — trong Châu Phi có nước GDP rất cao (Nam Phi, Nigeria), trong Châu Á có nước cực giàu (Singapore, Nhật) lẫn cực nghèo (Afghanistan, Myanmar) nằm cùng một nhóm.
Giải thích:
Trung bình châu Âu cao hơn châu Phi, nhưng boxplot cho thấy phân phối của hai nhóm chồng lên nhau — tức là có quốc gia châu Phi giàu hơn một số quốc gia châu Âu. Trung bình che giấu sự phân tán bên trong mỗi nhóm.
Ecological fallacy
là lỗi suy luận khi dùng số liệu trung bình của nhóm để kết luận về từng cá thể trong nhóm đó.
Ví dụ từ biểu đồ: barplot cho thấy GDP trung bình Châu Á thấp hơn Châu Âu. Nếu kết luận “Singapore nghèo hơn Romania” — đó là ecological fallacy, vì thực tế Singapore ($47.000/người) giàu hơn Romania ($10.000/người) rất nhiều, dù cùng so sánh trung bình châu lục thì ngược lại.
Bài 17. Dùng trực quan hóa để phát hiện bất thường trong dữ liệu
Giả sử có dữ liệu điểm thi gồm:
- student_id
- exam_score
Trong đó điểm đậu là 65.
Yêu cầu:
Vẽ histogram của exam_score.
Đánh dấu đường dọc tại điểm 65.
Quan sát xem có sự tập trung bất thường tại điểm 65 hay không.
Nếu có nhiều điểm đúng bằng 65 và rất ít điểm 63–64, hãy đưa ra ít nhất 3 giả thuyết giải thích.
Đề xuất thêm dữ liệu cần thu thập để kiểm tra các giả thuyết đó.
Viết kết luận thận trọng: không khẳng định gian lận nếu chỉ có biểu đồ, nhưng chỉ ra vì sao biểu đồ gợi ý cần điều tra thêm.
Ans:
# Tạo dữ liệu
set.seed(42)
score <- c(
rnorm(150, mean = 72, sd = 10), # phân phốt bình thường
rep(65, 40), # tập trung bất thường tại 65
rnorm(10, mean = 63, sd = 0.5) # rất ít điểm 63, 64)
)
score <- pmin(pmax(round(score), 0), 100)
df <- data.frame(
student_id = 1:length(score),
exam_score = score
)
# Histogram
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()Cột tại điểm 65 cao đột biến so với các cột xung quanh
Vùng 63–64 gần như trống — rất ít sinh viên có điểm này
Phân phối bình thường thì các điểm gần nhau phải có số lượng gần nhau, nhưng ở đây có “hố” ngay trước ngưỡng đậu và “đỉnh” ngay tại ngưỡng đậu
3 giả thuyết nếu có nhiều điểm đúng bằng 65:
Giả thuyết 1 — Làm tròn điểm: Giáo viên tự ý nâng điểm 63–64 lên 65 để sinh viên đậu, dẫn đến “hố trống” ở 63–64 và đỉnh bất thường tại 65.
Giả thuyết 2 — Thiết kế đề thi: Đề thi có cấu trúc khiến điểm tự nhiên rơi vào 65 nhiều hơn (ví dụ câu hỏi có thang điểm cố định khiến tổng dễ ra 65).
Giả thuyết 3 — Gian lận: Một nhóm sinh viên biết ngưỡng đậu và cố tình dừng ở mức vừa đủ, hoặc có can thiệp sau khi chấm
Dữ liệu cần thu thập thêm:
Lịch sử điểm các năm trước — bất thường có lặp lại không?
Điểm từng câu hỏi — xem câu nào tạo ra đỉnh tại 65
Danh sách giáo viên chấm — kiểm tra xem bất thường tập trung ở giáo viên nào
Kết luận thận trọng:
- Biểu đồ cho thấy sự tập trung bất thường tại đúng ngưỡng đậu 65 và hố trống rõ ràng ở 63–64 — đây là dấu hiệu thống kê đáng chú ý. Tuy nhiên, chỉ một biểu đồ không đủ để kết luận gian lận.
Bài 18. Một dữ liệu, hai đối tượng người xem
Chọn một bộ dữ liệu bất kỳ, ví dụ:
murders
gapminder
dữ liệu điểm sinh viên
dữ liệu khách sạn
dữ liệu doanh thu
Yêu cầu tạo hai phiên bản biểu đồ:
Phiên bản 1: dành cho nhà phân tích dữ liệu
Có thể nhiều chi tiết.
Có thể dùng facet, nhiều biến, nhiều lớp biểu đồ.
Mục tiêu là khám phá dữ liệu.
Phiên bản 2: dành cho người xem phổ thông
Đơn giản hơn.
Có tiêu đề rõ thông điệp.
Giảm nhiễu thị giác.
Tập trung vào một kết luận chính.
Yêu cầu giải thích:
Hai phiên bản khác nhau như thế nào?
Vì sao biểu đồ dùng cho EDA không nhất thiết phù hợp để trình bày trước công chúng?
Em đã loại bỏ hoặc giữ lại chi tiết nào khi chuyển từ phiên bản phân tích sang phiên bản truyền thông?
Ans:
# --- PHIÊN BẢN 1: CHO NHÀ PHÂN TÍCH (EDA - Khám phá dữ liệu) ---
# Code thêm màu sắc theo vùng (region) để tự tìm tòi xem vùng nào có xu hướng cao/thấp
ggplot(murders, aes(x = population / 10^6, y = total, color = region)) +
geom_point(size = 3) +
labs(
title = "Biểu đồ phân tán: Dân số và Tội phạm",
x = "Dân số (Triệu người)",
y = "Số vụ giết người",
color = "Vùng miền"
)# --- PHIÊN BẢN 2: CHO NGƯỜI XEM PHỔ THÔNG (Báo chí, Thuyết trình) ---
# Code tối giản: Xóa màu sắc rườm rà, tập trung vào 1 thông điệp duy nhất ở tiêu đề
ggplot(murders, aes(x = population / 10^6, y = total)) +
geom_point(color = "darkred", size = 2.5, alpha = 0.7) +
labs(
title = "Cảnh báo: Bang càng đông dân, số vụ tội phạm càng tăng vọt!",
x = "Dân số (Triệu người)",
y = "Tổng số vụ giết người"
) +
theme_minimal()Hai phiên bản khác nhau như thế nào?
Phiên bản 1 (Nhà phân tích): Có 4 màu sắc khác nhau đại diện cho 4 vùng miền, có thêm bảng chú thích (Legend) ở bên phải. Tiêu đề chỉ mô tả chung chung (Trục X và Trục Y).
Phiên bản 2 (Người xem phổ thông): Chỉ có một màu đỏ sẫm duy nhất, không có bảng chú thích. Tiêu đề giống như một dòng tít báo, chỉ thẳng ra kết luận.
Vì sao biểu đồ EDA không hợp để trình bày trước công chúng?
- Khán giả bình thường không có thời gian và chuyên môn để ngồi “giải mã” đồ thị của bạn. Nếu dùng Phiên bản 1, mắt họ sẽ phải liên tục nhìn sang bảng chú thích để xem chấm màu xanh lá là vùng nào, chấm màu tím là vùng nào… điều này làm họ bị rối não (nhiễu thị giác).
Em đã loại bỏ hoặc giữ lại chi tiết nào khi chuyển đổi?
Chi tiết loại bỏ: Em đã xóa bỏ phần color = region trong hàm aes() để bỏ đi 4 màu sắc và làm biến mất bảng chú thích rườm rà.
Chi tiết giữ lại: Giữ nguyên 2 trục tọa độ (Dân số và Tổng số vụ).
Chi tiết thay đổi: Đổi tiêu đề từ dạng mô tả khô khan thành một câu kết luận trực tiếp (“Bang càng đông dân, tội phạm càng tăng vọt”), giúp người xem hiểu ngay ý nghĩa biểu đồ trong vòng 3 giây.
Bài 19. Xử lý overplotting
Sử dụng một bộ dữ liệu có nhiều điểm quan sát, ví dụ dữ liệu quốc gia theo năm trong gapminder.
Yêu cầu:
Vẽ scatterplot ban đầu giữa hai biến có nhiều điểm chồng lên nhau.
Áp dụng ít nhất 3 kỹ thuật xử lý overplotting:
alpha
jitter
faceting
sampling
dùng density/hexbin nếu phù hợp
So sánh kết quả của các kỹ thuật.
Kỹ thuật nào giữ được nhiều thông tin nhất?
Kỹ thuật nào dễ hiểu nhất với người không chuyên?
Viết nhận xét về sự đánh đổi giữa tính chính xác, tính dễ đọc và tính thẩm mỹ.
Ans:
# Lấy dữ liệu và bỏ đi các dòng bị thiếu (NA)
df <- gapminder |>
filter(!is.na(fertility), !is.na(life_expectancy))
# 0. BIỂU ĐỒ GỐC (Bị lỗi Overplotting - Các điểm đen xì đè lên nhau)
ggplot(df, aes(x = fertility, y = life_expectancy)) +
geom_point() +
labs(title = "Biểu đồ gốc: Bị Overplotting nặng")# CÁCH 1: Dùng ALPHA (Làm mờ điểm ảnh)
ggplot(df, aes(x = fertility, y = life_expectancy)) +
geom_point(alpha = 0.1, color = "blue") +
labs(title = "Cách 1: Dùng Alpha (Độ mờ 0.1)")# CÁCH 2: Dùng SAMPLING (Lấy mẫu ngẫu nhiên)
df_nho <- df |> sample_frac(0.1) # Lấy ngẫu nhiên 10%
ggplot(df_nho, aes(x = fertility, y = life_expectancy)) +
geom_point(color = "darkgreen") +
labs(title = "Cách 2: Dùng Sampling (Chỉ vẽ 10% dữ liệu)")# CÁCH 3: Dùng FACETING (Chia thành nhiều ô nhỏ)
ggplot(df, aes(x = fertility, y = life_expectancy)) +
geom_point(alpha = 0.2, color = "purple") +
facet_wrap(~ continent) + # Chia lưới theo châu lục
labs(title = "Cách 3: Dùng Faceting (Chia theo Châu lục)")So sánh nhanh 3 kỹ thuật:
Alpha (Làm mờ): Rất nhanh, giúp bạn nhìn thấy ngay khu vực nào là “điểm nóng” (màu đậm nhất) tập trung nhiều dữ liệu.
Sampling (Lấy mẫu): Làm biểu đồ cực kỳ sạch sẽ, nhẹ, máy tính chạy nhanh, nhưng bị mất đi một lượng dữ liệu thực tế.
Faceting (Chia lưới): Giải tán đám đông bằng cách gom họ về từng khu vực (châu lục). Rất tốt để so sánh các nhóm với nhau.
Kỹ thuật nào giữ được nhiều thông tin nhất?
- Đó là Alpha (Làm mờ)
Kỹ thuật nào dễ hiểu nhất với người không chuyên?
- Đó là Faceting (Chia lưới)
Nhận xét về sự đánh đổi (Tính chính xác - Dễ đọc - Thẩm mỹ):
Trong trực quan hóa dữ liệu, bạn không thể có cả 3 thứ cùng một lúc:
Nếu bạn muốn chính xác 100% (vẽ mọi điểm), biểu đồ sẽ thành một cục mực đen thui \(\rightarrow\) -> Mất tính dễ đọc và thẩm mỹ.
Nếu bạn muốn thẩm mỹ, dễ đọc, sạch sẽ (dùng sampling bỏ bớt điểm đi), biểu đồ sẽ rất đẹp \(\rightarrow\) -> Mất đi tính chính xác tuyệt đối (vì bỏ sót các điểm dị biệt).
Bài 20. Thiết kế mini-project trực quan hóa dữ liệu khách sạn Việt Nam
Giả sử em có bộ dữ liệu review khách sạn Việt Nam gồm:
| hotel_id | city | year | rating | sentiment | aspect | review_language |
|---|---|---|---|---|---|---|
Sinh viên tự cập nhật dữ liệu ở bảng, yêu cầu ít nhất 1000 dòng dữ liệu. Yêu cầu xây dựng một mini-project trực quan hóa dữ liệu.
Cần thực hiện:
Đặt ít nhất 5 câu hỏi phân tích.
Chọn ít nhất 4 loại biểu đồ phù hợp.
Giải thích mỗi biểu đồ trả lời câu hỏi nào.
Phân tích xu hướng sentiment theo thời gian.
So sánh sentiment giữa các thành phố.
So sánh các aspect như service, room, location, cleanliness, price.
Đề xuất cách tránh hiểu sai khi so sánh các thành phố có số lượng review khác nhau.
Viết một đoạn kết luận 250–300 từ trình bày insight chính từ dashboard hoặc báo cáo trực quan hóa.
Gợi ý biểu đồ:
Line chart cho xu hướng theo năm.
Barplot cho so sánh thành phố.
Heatmap cho sentiment theo aspect và thành phố.
Boxplot hoặc violin plot cho phân phối rating.
Facet plot để so sánh theo ngôn ngữ review.
Ans:
library(tidyr)
set.seed(2026)
n_rows <- 1250
hotel_data <- data.frame(
hotel_id = sample(101:150, n_rows, replace = TRUE),
city = sample(c("Hà Nội", "Đà Nẵng", "TP.HCM", "Nha Trang", "Đà Lạt"),
n_rows, replace = TRUE, prob = c(0.3, 0.25, 0.2, 0.15, 0.1)),
year = sample(2019:2023, n_rows, replace = TRUE),
aspect = sample(c("service", "room", "location", "cleanliness", "price"), n_rows, replace = TRUE),
review_language = sample(c("Vietnamese", "English", "Korean"),
n_rows, replace = TRUE, prob = c(0.6, 0.3, 0.1)),
rating = sample(1:5, n_rows, replace = TRUE, prob = c(0.1, 0.1, 0.2, 0.3, 0.3))
)
hotel_data <- hotel_data |>
mutate(sentiment = case_when(
rating >= 4 ~ "Positive",
rating == 3 ~ "Neutral",
rating <= 2 ~ "Negative"
))Đặt 5 Câu hỏi Phân tích & Chọn Biểu đồ:
Xu hướng cảm xúc (sentiment) của du khách biến động như thế nào qua giai đoạn 2019-2023?
Thành phố nào mang lại mức độ hài lòng cao nhất và thấp nhất?
Chất lượng các tiêu chí (aspect) chênh lệch ra sao giữa các thành phố?
Phân phối điểm số (rating) có sự khác biệt nào giữa các ngôn ngữ đánh giá không?
Tiêu chí nào (service, room, price…) nhận được nhiều phàn nàn (Negative) nhất trên toàn quốc?
Mã nguồn R Vẽ 4 Biểu đồ phân tích
#1. 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()#2. 100% Stacked Barplot: So sánh Sentiment giữa các Thành phố
ggplot(hotel_data, aes(x = city, fill = sentiment)) +
geom_bar(position = "fill") + # position = "fill" chính là chìa khóa tạo tỷ lệ 100%
scale_fill_manual(values = c("Negative" = "tomato", "Neutral" = "lightgray", "Positive" = "seagreen")) +
scale_y_continuous(labels = scales::percent) +
labs(title = "Tỷ lệ Cảm xúc khách hàng theo Thành phố",
x = "Thành phố", y = "Tỷ lệ Phần trăm") +
theme_classic()#3. Heatmap: Điểm nóng Aspect theo Thành phố
heatmap_data <- hotel_data |>
group_by(city, aspect) |>
summarize(positive_rate = mean(sentiment == "Positive") * 100, .groups = "drop")
ggplot(heatmap_data, aes(x = aspect, y = city, fill = positive_rate)) +
geom_tile(color = "white") +
scale_fill_gradient(low = "white", high = "dodgerblue") +
labs(title = "Bản đồ nhiệt: Tỷ lệ Hài lòng theo Tiêu chí và Thành phố",
fill = "% Positive") +
theme_minimal()#4. Violin Plot + Facet: Phân phối Rating theo Ngôn ngữ
ggplot(hotel_data, aes(x = review_language, y = rating, fill = review_language)) +
geom_violin(alpha = 0.5) +
geom_jitter(width = 0.1, alpha = 0.2, size = 1) +
facet_wrap(~ aspect) +
labs(title = "Phân phối Điểm đánh giá (Rating) theo Ngôn ngữ & Tiêu chí",
x = "Ngôn ngữ Review", y = "Điểm số (1-5 sao)") +
theme_bw() +
theme(legend.position = "none")Giải pháp tránh hiểu sai khi quy mô dữ liệu chênh lệch:
Tuyệt đối không dùng Count, hãy dùng Proportion (Tỷ lệ %)
Đặt ngưỡng tối thiểu (Threshold)
Báo cáo Insight Tổng kết (Executive Summary):
“Qua quá trình phân tích và trực quan hóa bộ dữ liệu hơn 1.250 đánh giá khách sạn tại Việt Nam giai đoạn 2019-2023, chúng ta có thể rút ra những insight quan trọng về bức tranh toàn cảnh ngành dịch vụ lưu trú. Thứ nhất, về xu hướng thời gian, tỷ lệ đánh giá Tích cực (Positive) có dấu hiệu sụt giảm nhẹ vào giai đoạn 2020-2021 do hệ lụy đứt gãy dịch vụ mùa dịch, nhưng đã phục hồi ấn tượng và duy trì đà tăng tốc ổn định tiến về năm 2023. Thứ hai, khi so sánh mức độ hài lòng giữa các điểm đến thông qua biểu đồ tỷ lệ phần trăm, Đà Nẵng nổi lên là thành phố dẫn đầu về độ ổn định chất lượng, đặc biệt xuất sắc ở khía cạnh phòng ốc (room) và vị trí (location). Ngược lại, hai siêu đô thị là TP.HCM và Hà Nội ghi nhận mật độ phàn nàn cao hơn, tập trung chủ yếu vào yếu tố giá cả (price) và dịch vụ (service), phản ánh sự kỳ vọng khắt khe của tệp khách hàng tại các trung tâm kinh tế. Thứ ba, quan sát dưới lăng kính ngôn ngữ, tệp du khách quốc tế sử dụng tiếng Anh và tiếng Hàn có xu hướng chấm điểm (rating) trung bình khắt khe hơn đáng kể so với du khách nội địa, đặc biệt nhạy cảm với tiêu chuẩn vệ sinh (cleanliness). Tóm lại, các chuỗi khách sạn cần áp dụng chiến lược bản địa hóa: ưu tiên cải thiện chất lượng dịch vụ phòng tại các thành phố lớn, đồng thời chuẩn hóa quy trình vệ sinh để đáp ứng tiêu chuẩn ngày càng cao của luồng khách du lịch quốc tế trong thời kỳ hậu đại dịch.”