Trong quá trình thực hiện bài tiểu luận của học phần Ngôn ngữ lập trình, em đã nhận được sự hướng dẫn tận tình của . Thầy đã dành nhiều thời gian chỉ bảo, góp ý và định hướng giúp em hoàn thiện bài làm một cách hiệu quả nhất. Nhờ những kiến thức và sự hỗ trợ quý báu đó, em đã có cơ hội củng cố thêm hiểu biết và kỹ năng trong việc vận dụng ngôn ngữ lập trình vào phân tích dữ liệu. Mặc dù đã nỗ lực hoàn thành bài tiểu luận với tinh thần trách nhiệm cao nhưng do kiến thức và kinh nghiệm còn hạn chế bài làm khó tránh khỏi những thiếu sót. Em rất mong nhận được những ý kiến đóng góp của thầy để có thể hoàn thiện hơn trong những lần nghiên cứu sau.
Bộ dữ liệu Vehicle Sales Data là dữ liệu công khai trên nền tảng
Kaggle, được thu thập nhằm phục vụ cho các bài toán phân tích và dự đoán
giá xe đã qua sử dụng. Bộ dữ liệu chứa các thông tin như hãng xe, năm
sản xuất, quãng đường đã đi, tình trạng xe và giá bán, qua đó hỗ trợ
đánh giá các yếu tố tác động đến giá trị của xe trên thị trường.
Nạp thư viện cần thiết
# Bộ công cụ xử lý dữ liệu và vẽ đồ thị
library(tidyverse) # Gồm nhiều gói mạnh như dplyr, ggplot2, tidyr, readr,...
# Chạy code và xuất báo cáo trong R Markdown
library(knitr) # Dùng để chạy code và chèn kết quả vào báo cáo
library(kableExtra) # Tạo bảng đẹp, định dạng PDF/HTML/Word
# Làm sạch, xử lý dữ liệu
library(janitor) # Làm sạch tên cột, loại bỏ dòng trống, tạo bảng tần suất
library(lubridate) # Xử lý dữ liệu ngày tháng (năm, tháng, ngày,…)
library(stringr) # Xử lý chuỗi ký tự
library(readxl) # Đọc file Excel (.xls, .xlsx)
library(jsonlite) # Đọc và ghi dữ liệu dạng JSON
library(dplyr) # Lọc, sắp xếp, nhóm, tính toán dữ liệu
library(tidyr) # Chuyển đổi dữ liệu giữa dạng rộng và dài
# Phân tích, mô hình hóa và biểu đồ
library(ggplot2) # Vẽ biểu đồ trực quan
library(ggrepel) # Gắn nhãn đẹp, tránh chồng chữ trên biểu đồ
library(scales) # Định dạng trục số, phần trăm, tiền tệ,...
library(corrr) # Tính ma trận tương quan
library(ggcorrplot) # Vẽ biểu đồ ma trận tương quan
library(broom) # Chuyển kết quả mô hình hồi quy sang dạng bảng tidy
Đọc dữ liệu
Sử dụng hàm read.csv() để đọc dữ liệu từ tệp
C:/data/Nhóm 28-car_prices.csv và lưu vào một biến (data
frame) có tên là df.
df <- read.csv("C:/data/Nhóm 28-car_prices.csv")
Kiểm tra dữ liệu
# Số lượng biến
cat("Số lượng biến:", ncol(df), "\n")
## Số lượng biến: 16
# Số lượng quan quan sát
cat("Số lượng quan sát :", nrow(df), "\n")
## Số lượng quan sát : 558837
# Mô tả các biến
df %>%
summarise(across(everything(), class)) %>%
t() %>%
as.data.frame() %>%
rownames_to_column("Biến") %>%
rename("Kiểu dữ liệu" = 2) %>%
kable(caption = "Danh sách biến và kiểu dữ liệu trong bộ dữ liệu") %>%
kable_styling(full_width = FALSE, position = "center")
| Biến | Kiểu dữ liệu |
|---|---|
| year | integer |
| make | character |
| model | character |
| trim | character |
| body | character |
| transmission | character |
| vin | character |
| state | character |
| condition | integer |
| odometer | integer |
| color | character |
| interior | character |
| seller | character |
| mmr | integer |
| sellingprice | integer |
| saledate | character |
Ý nghĩa thống kê:
Việc đếm số biến (features) và số quan sát (samples) giúp đánh giá kích
thước dữ liệu và khả năng phân tích.
Việc xác định kiểu dữ liệu (numeric/character) quan trọng để chọn phương
pháp xử lý phù hợp.
Kiểm tra dữ liệu bị thiếu, trùng lặp và xử lý
# Kiểm tra giá trị bị thiếu
cat("Giá trị bị thiếu :", sum(is.na(df)), "\n")
## Giá trị bị thiếu : 11964
# Số dòng có NA
cat("Số dòng có giá trị bị thiếu:", sum(!complete.cases(df)), "\n")
## Số dòng có giá trị bị thiếu: 11861
# Số dòng trùng lặp
n_dup_rows <- sum(duplicated(df))
cat("\nSố dòng trùng lặp:", n_dup_rows, "\n")
##
## Số dòng trùng lặp: 0
# ---- Xử lý giá trị bị thiếu ----
# Tạo bảng biến có giá trị thiếu
na_table <- df %>%
summarise(across(everything(), ~sum(is.na(.)))) %>%
pivot_longer(cols = everything(),
names_to = "Biến",
values_to = "Số NA") %>%
filter(`Số NA` > 0) %>% # Chỉ lấy biến có NA
arrange(desc(`Số NA`))
# In bảng kết quả
na_table %>%
kable(caption = "Các biến có giá trị thiếu và số lượng thiếu",
col.names = c("Biến", "Số giá trị thiếu")) %>%
kable_styling(full_width = FALSE, position = "center")
| Biến | Số giá trị thiếu |
|---|---|
| condition | 11820 |
| odometer | 94 |
| mmr | 38 |
| sellingprice | 12 |
| Biến | Min | Mean | Median | Max |
|---|---|---|---|---|
| year | 1982 | 2010.039 | 2012 | 2015 |
| odometer | 1 | 68320.018 | 52254 | 999999 |
| sellingprice | 1 | 13611.359 | 12100 | 230000 |
cat("Số hãng xe:", length(unique(df$make)), "\n")
## Số hãng xe: 97
# top 10 số hãng xe phổ biến
df %>% count(make, sort = TRUE) %>% head(10)
# Nếu tỷ lệ NA nhỏ → loại dòng chứa NA
df <- df %>% drop_na()
cat("Số dòng sau khi loại NA:", nrow(df), "\n")
## Số dòng sau khi loại NA: 546976
library(lubridate) #xử lý dữ liệu ngày tháng
# Cắt chuỗi để lấy 4 ký tự năm từ vị trí 12 đến 15
df$year_saledate <- substr(df$saledate, 12, 15)
df$saledate <- NULL # xóa cột saledate
# Xem kết quả
cat("Các năm xe được bán:", unique(df$year_saledate), "\n")
## Các năm xe được bán: 2014 2015
# Số lượng xe bán
df %>%
filter(year_saledate %in% c(2014, 2015)) %>%
group_by(year_saledate) %>%
summarise(so_xe_ban = n())
# Tính số tuổi mà xe được bán
df <- df %>%
mutate(
year_saledate = as.numeric(year_saledate), # chuyển thành kiểu số
vehicle_age = year_saledate - year, # tính tuổi xe
vehicle_age = ifelse(vehicle_age < 0 | is.na(vehicle_age), 0, vehicle_age) # loại giá trị âm/NA
)
df$make <- toupper(df$make)
df$model <- tolower(trimws(df$model))
toupper Chuẩn hóa chữ hoa tránh trùng dữ liệu kiểu
ví dụ: “toyota” thành “TOYOTA”.
### 1.2.4. Chuẩn hóa quảng đường (odometer) theo thang chuẩn
(z-score)
df$odometer_z <- scale(df$odometer)
df <- df %>% filter(!(vehicle_age >= 20 & odometer > 400000))
cat("Số lượng xe còn lại sau lọc dữ liệu là:", nrow(df))
## Số lượng xe còn lại sau lọc dữ liệu là: 546974
df <- df %>%
filter(!apply(., 1, function(row) any(grepl("[^A-Za-z0-9 .,/-]", row))))
cat("Số lượng xe còn lại sau lọc dữ liệu là:", nrow(df))
## Số lượng xe còn lại sau lọc dữ liệu là: 489558
df <- df %>%
mutate(across(c(condition, odometer, sellingprice, mmr), as.numeric))
df$model <- tolower(trimws(df$model))
Ví dụ “Civic” và “CIVIC” được coi là một
Trình bày các kết quả thống kê mô tả, phân tích đặc điểm chung của dữ liệu và mối quan hệ giữa các biến chính. Các phép thống kê cơ bản giúp hiểu rõ cấu trúc dữ liệu, xu hướng phân phối, và các yếu tố có thể ảnh hưởng đến giá bán xe.
num_vars <- c("year","odometer","sellingprice","mmr","condition")
desc_stats <- df %>%
summarise(across(
all_of(num_vars),
list(
min = ~min(., na.rm = TRUE),
median = ~median(., na.rm = TRUE),
mean = ~mean(., na.rm = TRUE),
max = ~max(., na.rm = TRUE),
sd = ~sd(., na.rm = TRUE)
)
)) %>%
# Đổi từ wide-to-long để dễ tách tên biến và tên thống kê
pivot_longer(everything(), names_to = "stat", values_to = "value") %>%
separate(stat, into = c("variable", "stat"), sep = "_") %>% # tách tên biến & thống kê
pivot_wider(names_from = stat, values_from = value) # xoay bảng thành dạng wide
desc_stats
library(gridExtra)
library(grid) # thêm dòng này để có textGrob()
# Tạo dữ liệu top 20
top20_makes <- df %>%
count(make, sort = TRUE) %>%
slice_head(n = 20) %>%
mutate(
n = comma(n), # ngăn cách bằng dấu phẩy
STT = row_number() # đánh số thứ tự 1–20
)
# Chia 2 bảng: 1–10 và 11–20
top10_makes <- top20_makes %>% slice_head(n = 10)
next10_makes <- top20_makes %>% slice_tail(n = 10)
# Hiển thị song song 2 bảng với caption riêng
grid.arrange(
arrangeGrob(
gridExtra::tableGrob(top10_makes),
top = textGrob("Top 1–10 hãng xe có số lượng nhiều nhất", gp = gpar(fontface = "bold"))
),
arrangeGrob(
gridExtra::tableGrob(next10_makes),
top = textGrob("Top 11–20 hãng xe có số lượng nhiều nhất", gp = gpar(fontface = "bold"))
),
ncol = 2
)
- library(gridExtra) / library(grid): ể ghép và trang
trí bảng trong output.
- filter(!is.na(make))
Kỹ thuật: loại bỏ hàng có make = NA trước khi đếm.
Ý nghĩa thống kê: nếu không loại, NA sẽ được
đếm như một nhãn riêng — có thể gây hiểu sai về “hãng không xác định”
được coi là một nhóm. Quyết định giữ hay loại NA tùy mục tiêu: mô tả dữ
liệu gốc (giữ) hay phân tích hãng thực tế (loại).
- count(make, sort = TRUE, name = “n_raw”)
Kỹ thuật: đếm số hàng cho mỗi giá trị make, trả về cột tên
n_raw (số nguyên). sort = TRUE sắp theo n_raw giảm dần.
Ý nghĩa thống kê: Tạo phân bố tần suất rời rạc của biến danh
mục make. Đây là thông tin cơ bản về tần suất — ai nhiều nhất, ai ít
nhất — rất quan trọng trước mọi phân tích thể loại (ví dụ phân phối mẫu,
lựa chọn nhóm để biểu diễn).
- slice_head(n = 20)
Kỹ thuật: họn 20 dòng đầu (theo thứ tự hiện tại — ở đây là đã
sắp giảm dần theo n_raw).
Ý nghĩa thống kê: tập trung vào phần “đỉnh” (head) của phân bố
— thường chứa các nhãn chiếm phần lớn dữ liệu; giảm nhiễu từ phần đuôi
dài (many rare categories). Đây là cách hay để trình bày top-k phổ
biến.
- mutate(STT = row_number())
Kỹ thuật:
Ý nghĩa thống kê
- mutate(n = comma(n_raw))
Kỹ thuật: Tạo cột n đã được format (chuỗi) với dấu phẩy phân
tách hàng nghìn, dùng scales::comma().
Ý nghĩa thống kê: chỉ tác động hiển thị — làm cho bảng báo cáo
dễ đọc hơn (ví dụ 12,345 thay vì 12345). Quan trọng: comma() chuyển số
sang chuỗi; do đó, nếu sau này cần tính toán (tỉ lệ, cộng dồn), phải
dùng n_raw (numeric), không dùng n (chuỗi).
- top10_makes <- top20_makes %>% slice_head(n = 10) và
next10_makes <- top20_makes %>% slice_tail(n = 10)
Kỹ thuật:chia top20_makes thành hai bảng con: hàng 1–10 và
11–20. slice_head() lấy đầu, slice_tail() lấy cuối (với data đã sắp giảm
dần theo n_raw, slice_tail ở đây lấy hàng 11–20).
Ý nghĩa thống kê: chia nhỏ để hiển thị song song, tránh bảng
quá dài; giữ thứ tự ranking. Về nội dung, cho phép người đọc so sánh
nhóm hàng đầu (1–10) với nhóm đứng sau (11–20) về tần suất tuyệt
đối.
- gridExtra::tableGrob(…), arrangeGrob(…, top = textGrob(…)),
grid.arrange(…, ncol = 2)
Kỹ thuật:
- tableGrob() chuyển dataframe thành một đồ họa bảng
(grob) để vẽ trong đồ họa R.
- arrangeGrob(…, top = textGrob(…)) thêm tiêu
đề/caption phía trên mỗi bảng.
- grid.arrange(…, ncol = 2) bố trí hai bảng cạnh nhau
(2 cột).
Ý nghĩa thống kê: Trình bày song song giúp so sánh nhanh về cấu
trúc tần suất giữa 1–10 và 11–20.
Ma trận tương quan (correlation matrix) giúp đánh giá mức độ quan hệ tuyến tính giữa các biến định lượng
num_df <- df %>% select(all_of(num_vars)) %>% drop_na()
cor_mat <- cor(num_df, use="pairwise.complete.obs")
cor_mat <- cor(num_df, use = "pairwise.complete.obs")
kable(cor_mat, digits = 2, caption = "Bảng ma trận tương quan giữa các biến số")
| year | odometer | sellingprice | mmr | condition | |
|---|---|---|---|---|---|
| year | 1.00 | -0.77 | 0.59 | 0.60 | 0.33 |
| odometer | -0.77 | 1.00 | -0.58 | -0.59 | -0.31 |
| sellingprice | 0.59 | -0.58 | 1.00 | 0.98 | 0.32 |
| mmr | 0.60 | -0.59 | 0.98 | 1.00 | 0.27 |
| condition | 0.33 | -0.31 | 0.32 | 0.27 | 1.00 |
Tạo bảng thể hiện giá bán trung bình và trung vị của 10 hãng xe có số lượng nhiều nhất.
top10_make_meanprice <- df %>%
group_by(make) %>%
summarise(
n = n(),
mean_price = mean(sellingprice, na.rm = TRUE),
median_price = median(sellingprice, na.rm = TRUE)
) %>%
arrange(desc(n)) %>%
slice_head(n = 10) %>%
mutate(
n = comma(n), # thêm dấu phẩy ngăn cách hàng nghìn
mean_price = comma(mean_price, accuracy = 1),
median_price = comma(median_price, accuracy = 1)
) %>%
rename(
"Hãng xe" = make,
"Số lượng" = n,
"Giá trung bình" = mean_price,
"Giá trung vị" = median_price
)
kable(
top10_make_meanprice,
caption = "Top 10 hãng xe có số lượng lớn nhất cùng giá trung bình và giá trung vị"
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("striped", "hover", "condensed")
)
| Hãng xe | Số lượng | Giá trung bình | Giá trung vị |
|---|---|---|---|
| FORD | 79,299 | 14,352 | 13,300 |
| CHEVROLET | 55,094 | 12,077 | 10,600 |
| NISSAN | 47,948 | 11,782 | 12,000 |
| TOYOTA | 36,394 | 12,335 | 12,200 |
| DODGE | 28,212 | 11,345 | 10,600 |
| HONDA | 24,836 | 11,053 | 11,200 |
| HYUNDAI | 20,206 | 11,159 | 11,400 |
| CHRYSLER | 16,017 | 11,325 | 10,500 |
| KIA | 14,846 | 11,949 | 12,200 |
| MERCEDES-BENZ | 14,228 | 21,067 | 20,600 |
if ("year_saledate" %in% names(df)) {
sale_by_year <- df %>%
group_by(year_saledate) %>%
summarise(
n = n(),
mean_price = mean(sellingprice, na.rm = TRUE)
) %>%
mutate(
n = comma(n), # thêm dấu phẩy ngăn cách hàng nghìn
mean_price = comma(mean_price, accuracy = 1)
) %>%
rename(
"Năm bán" = year_saledate,
"Số lượng xe bán" = n,
"Giá bán trung bình" = mean_price
)
kable(
sale_by_year,
caption = "Số lượng và giá bán trung bình của xe theo từng năm"
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("striped", "hover", "condensed")
)
}
| Năm bán | Số lượng xe bán | Giá bán trung bình |
|---|---|---|
| 2014 | 37,826 | 12,397 |
| 2015 | 451,732 | 13,552 |
df %>%
group_by(transmission) %>%
summarise(
So_luong = n(),
Ti_le = round(100 * n() / nrow(df), 2)
) %>%
arrange(desc(So_luong)) %>%
kable(caption = "Phân bố xe theo loại hộp số") %>%
kable_styling(full_width = FALSE, position = "center")
| transmission | So_luong | Ti_le |
|---|---|---|
| automatic | 416995 | 85.18 |
| 57246 | 11.69 | |
| manual | 15317 | 3.13 |
Chuẩn bị số liệu vẽ biểu đồ
set.seed(123)
df_plot <- if(nrow(df) > 100000) dplyr::sample_n(df, 100000) else df
# đảm bảo biến số numeric
df_plot <- df_plot %>%
mutate(
sellingprice = as.numeric(sellingprice),
odometer = as.numeric(odometer),
year = as.numeric(year),
mmr = as.numeric(mmr),
condition = as.numeric(condition),
price_cat = case_when(
sellingprice < 10000 ~ "Low",
sellingprice < 30000 ~ "Med",
TRUE ~ "High")
) %>% drop_na(sellingprice, odometer, year)
top20_makes <- df %>%
count(make, sort = TRUE) %>%
slice_head(n = 20) %>%
mutate(n = as.numeric(gsub(",", "", n)))
ggplot(top20_makes, aes(x = reorder(make, n), y = n, fill = make)) +
geom_col(show.legend = FALSE) +
coord_flip() +
# Hiện số có dấu phẩy ngăn cách hàng nghìn
geom_text(aes(label = scales::comma(n)), hjust = -0.2, size = 3) +
scale_y_continuous(
labels = scales::comma, # trục Y cũng có dấu phẩy ngăn cách
expand = expansion(mult = c(0, 0.1))
) +
labs(
title = "Top 20 hãng xe có số lượng nhiều nhất",
x = "Hãng xe",
y = "Số lượng xe"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5, size = 15),
axis.text.y = element_text(size = 10)
)
- ggplot(top20_makes, aes(x = reorder(make, n), y = n, fill =
make))
Kỹ thuật: Khởi tạo biểu đồ với dataset top20_makes.
reorder(make, n) sắp xếp tên hãng xe theo số lượng n. fill = make gán
màu khác nhau cho từng hãng.
Ý nghĩa thống kê: Giúp biểu đồ dễ đọc hơn: hãng có số lượng cao
sẽ nằm cuối (sau khi xoay ngang). Màu sắc tạo sự phân biệt giữa các
hãng.
- geom_col(show.legend = FALSE)
Kỹ thuật: Vẽ biểu đồ cột (column chart). show.legend = FALSE ẩn
chú thích màu (legend).
Ý nghĩa thống kê:Biểu đồ cột là lựa chọn phù hợp để so sánh tần
suất (số lượng xe) giữa các hãng. Ẩn legend vì tên hãng đã thể hiện trên
trục Y, không cần chú giải màu.
- coord_flip()
Kỹ thuật: Đảo trục X–Y, biến biểu đồ thành cột ngang.
Ý nghĩa thống kê:Biểu đồ cột ngang giúp đọc tên hãng xe dễ hơn
(danh mục dài không bị nghiêng 45°).
- geom_text(aes(label = scales::comma(n)), hjust = -0.2, size =
3)
Kỹ thuật:Thêm nhãn số lượng lên từng cột. comma() giúp hiển thị
dạng 10,000 thay vì 10000. hjust = -0.2 đặt nhãn nằm ngoài đầu
cột.
Ý nghĩa thống kê:Nhãn số rõ ràng giúp người đọc biết giá trị
chính xác, không chỉ ước lượng bằng mắt. Dấu phẩy tăng tính trực quan,
nhất là khi số lớn.
- scale_y_continuous(labels = scales::comma, expand =
expansion(mult = c(0, 0.1)))
Kỹ thuật: Format trục Y với dấu phẩy ở hàng nghìn, expand tạo
khoảng cách phía trên để text không bị cắt.
Ý nghĩa thống kê: Làm trục dễ đọc, tránh gây nhầm lẫn giữa 1000
và 10000. Khoảng trống giúp biểu đồ đẹp và không đè lên title.
- labs(…)
Kỹ thuật: Đặt tiêu đề, tên trục X, trục Y.
Ý nghĩa thống kê:Tăng khả năng diễn giải: người xem biết đang
xem top 20 hãng xe theo số lượng xe xuất hiện trong dataset.
- theme_minimal(base_size = 13)
Kỹ thuật: Chọn giao diện tối giản, font chữ lớn 13pt.
Ý nghĩa thống kê: Giảm nhiễu thị giác, tăng tính chuyên nghiệp
của biểu đồ.
- theme(plot.title = …, axis.text.y = …)
Kỹ thuật: Làm đậm tiêu đề, căn giữa, tăng cỡ chữ trục Y.
Ý nghĩa thống kê: Tiêu đề nổi bật hơn, nhãn trục dễ đọc hơn –
đặc biệt cần thiết khi dữ liệu là tên hãng xe.
p1 <- ggplot(df_plot, aes(x = odometer, y = sellingprice)) +
# Lớp 1: điểm dữ liệu
geom_point(alpha = 0.2, size = 0.6) +
# Lớp 2: hồi quy tuyến tính
geom_smooth(method = "lm", se = TRUE, color = "blue", linewidth = 0.8) +
# Lớp 3: xu hướng phi tuyến (loess)
stat_smooth(method = "loess", se = FALSE, linetype = "dashed", color = "darkred", linewidth = 0.6) +
geom_rug(sides = "b", alpha = 0.1) + # Lớp 4: phân bố biên
annotate("text",
x = Inf, y = Inf,
label = paste0("n = ", nrow(df_plot)),
hjust = 1.1, vjust = 1.5, size = 3) + # Lớp 5: chú thích tổng quan
scale_y_continuous(labels = comma) + # Lớp 6: trục y có dấu phẩy ngăn cách
theme_minimal(base_size = 12) + # Lớp 7: theme tối giản
labs(
title = "Mối quan hệ giữa giá bán và quãng đường đã đi",
x = "Quãng đường đã đi (Odometer, km)",
y = "Giá bán (Selling Price, USD)"
)
print(p1)
- p1 <- ggplot(df_plot, aes(x = odometer, y = sellingprice))
+
Kỹ thuật: Khởi tạo biểu đồ ggplot, xác định trục X và Y.
Ý nghĩa: Cả hai biến đều là continuous → phù hợp để kiểm tra
tương quan, xu hướng, hồi quy.
- geom_point(alpha = 0.2, size = 0.6)
Kỹ thuật: Vẽ scatter plot, alpha = 0.2 giúp chống nhiễu khi
điểm chồng lên nhau (overplotting).
Ý nghĩa: Dùng scatter plot để xem quan hệ tuyến tính / phi
tuyến giữa 2 biến.
- geom_smooth(method = “lm”, se = TRUE, color = “blue”,
linewidth = 0.8)
Kỹ thuật: Thêm đường hồi quy tuyến tính (linear regression). se
= TRUE hiển thị vùng sai số chuẩn 95%.
Ý nghĩa: Cho biết mối quan hệ tuyến tính giữa odometer và
sellingprice. Nếu đường slope âm → xe chạy càng nhiều km → giá giảm (quy
luật hao mòn).
- stat_smooth(method = “loess”, se = FALSE, linetype = “dashed”,
color = “darkred”, linewidth = 0.6)
Kỹ thuật: Thêm đường xu hướng phi tuyến LOESS (local
regression), dạng cong.
Ý nghĩa: Kiểm tra xem mối quan hệ có phi tuyến hay không.
Nếu LOESS ≠ đường tuyến tính → quan hệ có thể cong, ví dụ:
- Giảm nhanh ở giai đoạn đầu
- Ổn định dần khi xe quá cũ (giá trị tiệm cận)
-> Đây là bước kiểm định trực quan giả định tuyến tính trước khi chạy
hồi quy.
- geom_rug(sides = “b”, alpha = 0.1)
Kỹ thuật: Vẽ rug marks dưới trục X, biểu diễn phân bố của biến
odometer.
Ý nghĩa: Giúp phát hiện phân bố lệch (skewness), outliers, tập
trung dữ liệu (cluster).
Ví dụ: nếu nhiều tick nằm < 100k km → đa số xe chưa chạy quá
nhiều.
- annotate(“text”, x = Inf, y = Inf, label = paste0(“n =”,
nrow(df_plot)), hjust = 1.1, vjust = 1.5, size = 3)
Kỹ thuật: Ghi số lượng mẫu ngay trên biểu đồ.
Ý nghĩa: Chỉ cải thiện trực quan, không ảnh hưởng ý
nghĩa.
- theme_minimal(base_size = 12)
Kỹ thuật: Dùng theme tối giản giúp làm nổi bật dữ liệu.
Ý nghĩa: Không ảnh hưởng, chỉ phục vụ trình bày báo cáo.
- labs( title = “Mối quan hệ giữa giá bán và quãng đường đã đi”
x = “Quãng đường đã đi (Odometer, km)”,y = “Giá bán (Selling Price,
USD)”)
Thêm tiêu đề, nhãn trục → bắt buộc trong báo cáo khoa học.
top6 <- df_plot %>% count(make, sort=TRUE) %>% slice_head(n=6) %>% pull(make)
p2d <- df_plot %>% filter(make %in% top6)
p2 <- ggplot(p2d, aes(x=factor(year), y=sellingprice, color=make)) +
geom_jitter(width=0.3, alpha=0.4, size=0.6) +
geom_smooth(aes(group=make), method="loess", se=FALSE) +
geom_violin(aes(x=factor(year), y=sellingprice, fill=make), alpha=0.08,
position=position_dodge(width=0.9), show.legend=FALSE) +
stat_summary(fun=median, geom="point", shape=21, size=2, fill="white") +
geom_text_repel(data = p2d %>% group_by(make) %>% slice_max(sellingprice, n=1),
aes(label = make), size=2.5) +
scale_x_discrete(breaks = seq(min(p2d$year), max(p2d$year), by = 10)) +
labs(title="Giá theo năm cho 6 thương hiệu hàng đầu")
print(p2)
- count() %>% slice_head()
Kỹ thuật: Lấy top 6 hãng có nhiều xe nhất.
Ý nghĩa: Tránh nhiễu từ hãng ít dữ liệu.
- geom_jitter()
Kỹ thuật: Tránh trùng điểm dữ liệu.
Ý nghĩa: Trực quan hóa phân bố thật của giá xe theo năm.
- geom_violin()
Kỹ thuật:Hiển thị phân bố xác suất giá theo từng năm.
Ý nghĩa: So sánh độ lệch, phân tán, outliers.
- stat_summary(fun=median)
Kỹ thuật: Chấm median giá bán.
Ý nghĩa: Median ổn định trước outlier, phản ánh giá điển
hình
- geom_smooth(method=loess)
Kỹ thuật:Vẽ xu hướng giá theo năm cho từng hãng.
Ý nghĩa: Tránh nhiễu từ hãng ít dữ liệu.
- geom_jitter()
Kỹ thuật: Tránh trùng điểm dữ liệu.
Ý nghĩa: Kiểm tra quan hệ phi tuyến theo thời gian.
- geom_text_repel()
Kỹ thuật:Gắn nhãn hãng có giá cao nhất.
Ý nghĩa: Highlight thương hiệu premium.
- scale_x_discrete(breaks = seq(…))
Kỹ thuật: Giảm mật độ nhãn trục năm.
Ý nghĩa: Tránh quá tải thông tin
p3 <- ggplot(df_plot, aes(x = sellingprice)) +
geom_histogram(binwidth = 1000, alpha = 0.5, fill = "steelblue") +
geom_density(aes(y = ..count..), color = "black", size = 0.6) +
geom_vline(
xintercept = quantile(df_plot$sellingprice, c(0.25, 0.5, 0.75), na.rm = TRUE),
linetype = c("dotted", "solid", "dashed"), color = "red"
) +
geom_rug() +
stat_summary(fun = median, geom = "point", aes(y = 0), color = "red", size = 3) +
scale_x_continuous(labels = comma) +
labs(title = "Mô tả phân phối giá bán", x = "Giá bán (USD)", y = "Số lượng")
print(p3)
- geom_histogram(binwidth = 1000)
Kỹ thuật:Vẽ biểu đồ tần suất dạng histogram.
Ý nghĩa thống kê: Quan sát phân phối giá bán theo từng khoảng
$1,000.
- geom_density(aes(y = ..count..))
Kỹ thuật: Thêm đường mật độ KDE chồng lên histogram.
Ý nghĩa thống kê: Giúp nhìn phân phối mượt hơn, dễ phát hiện
lệch / đa đỉnh.
- geom_vline(quantile())
Kỹ thuật:Vẽ 3 vạch Q1, median, Q3.
Ý nghĩa thống kê:Hỗ trợ nhận diện phân phối lệch, xác định vị
trí trung tâm.
- stat_summary(fun = median)
Kỹ thuật:Đánh dấu median = trung vị.
Ý nghĩa thống kê: Trung vị phù hợp khi phân phối lệch về
phải.
- geom_rug()
Kỹ thuật: Hiện điểm phân bố dọc trục X.
Ý nghĩa thống kê:Xác định tập trung và outlier.
- scale_x_continuous(labels = comma)
Kỹ thuật:Format số có dấu phẩy.
Ý nghĩa thống kê:Chỉ phục vụ trình bày.
p4data <- df_plot %>% filter(!is.na(transmission))
p4 <- ggplot(p4data, aes(x = transmission, y = sellingprice, fill = transmission)) +
geom_violin(alpha = 0.25) +
geom_boxplot(width = 0.2, outlier.shape = NA) +
geom_jitter(width = 0.2, alpha = 0.2, size = 0.6) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "white") +
stat_summary(fun.data = mean_cl_boot, geom = "errorbar", width = 0.2) + # Thay mean_cl_normal
geom_text(
data = p4data %>% count(transmission),
aes(x = transmission, y = max(df_plot$sellingprice) * 0.95,
label = paste0("n = ", n)),
size = 3
) +
scale_y_continuous(labels = scales::comma_format(big.mark = ".", decimal.mark = ",")) +
labs(
title = "Giá bán theo loại hộp số",
x = "Loại hộp số",
y = "Giá bán (USD)"
)
print(p4)
- geom_violin()
Kỹ thuật:Hiển thị mật độ phân phối giá bán.
Ý nghĩa thống kê: So sánh hình dạng phân phối giữa các
nhóm.
- geom_boxplot() Kỹ thuật: Hiển thị median,
IQR, outlier.
Ý nghĩa thống kê: Kiểm tra khác biệt mức trung tâm & độ
phân tán.
- geom_jitter())
Kỹ thuật:Hiển thị điểm thô.
Ý nghĩa thống kê:Tránh mất thông tin do boxplot / violin che
khuất.
- stat_summary(fun = median)
Kỹ thuật:Đánh dấu median = trung vị.
Ý nghĩa thống kê: Trung vị phù hợp khi phân phối lệch về
phải.
- mean_cl_boot Kỹ thuật: Vẽ CI 95% quanh trung
bình.
Ý nghĩa thống kê:So sánh mức độ tin cậy giữa nhóm.
- geom_text(n=…)
Kỹ thuật:Thể hiện kích thước mẫu.
Ý nghĩa thống kê:Cần thiết khi so sánh nhóm (statistical
power).
p5 <- ggplot(df_plot, aes(x = odometer, y = sellingprice)) +
geom_point(alpha = 0.15, size = 0.5) +
geom_smooth(method = "loess", se = TRUE) +
geom_density2d(alpha = 0.3) +
geom_rug(alpha = 0.05) +
stat_summary(fun = median, geom = "line",
aes(group = 1), color = "red", linetype = "dashed", size = 1) +
facet_wrap(~ price_cat) +
scale_x_continuous(
limits = c(0, 1000000),
breaks = seq(0, 1000000, 100000),
labels = label_number(big.mark = ".", decimal.mark = ",")
) +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
labs(
title = "Giá bán theo số km đã đi (phân nhóm theo mức giá)",
x = "Số km đã chạy (Odometer)",
y = "Giá bán (USD)"
) +
theme_minimal(base_size = 12) +
theme(
strip.text = element_text(face = "bold", size = 12),
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
)
print(p5)
- geom_point(alpha=…)
Kỹ thuật:Scatterplot làm rõ phân bố.
Ý nghĩa thống kê: Thấy mối quan hệ dạng xu hướng.
- geom_smooth(loess)
Kỹ thuật: Xu hướng phi tuyến.
Ý nghĩa thống kê: Kiểm tra pattern không tuyến tính.
- geom_density2d()
Kỹ thuật:Mật độ điểm trong không gian 2D.
Ý nghĩa thống kê:Xác định vùng tập trung quan sát.
- facet_wrap(~price_cat)
Kỹ thuật:Chia theo nhóm giá.
Ý nghĩa thống kê: Kiểm tra khác biệt slope giữa phân khúc thị
trường.
- stat_summary(fun=median)t
Kỹ thuật: Vẽ đường median.
Ý nghĩa thống kê:Đo xu hướng mạnh hơn mean khi phân phối
lệch.
- geom_text(n=…)
Kỹ thuật:Thể hiện kích thước mẫu.
Ý nghĩa thống kê:Cần thiết khi so sánh nhóm (statistical
power).
top10 <- df_plot %>%
group_by(make) %>%
summarise(
n = n(),
mean_price = mean(sellingprice, na.rm = TRUE),
sd_price = sd(sellingprice, na.rm = TRUE)
) %>%
arrange(desc(n)) %>%
slice_head(n = 10)
p6 <- ggplot(top10, aes(x = reorder(make, n), y = n, fill = mean_price)) +
geom_col() +
geom_text(
aes(
y = n + max(n)*0.02, # đẩy chữ lên trên thanh bar một chút
label = comma(n, big.mark = ".")
),
size = 3,
fontface = "bold"
) +
scale_fill_gradient(
low = "lightblue",
high = "darkblue",
labels = comma_format(big.mark = ".", decimal.mark = ",")
) +
coord_flip() +
labs(
title = "Top 10 hãng xe theo số lượng bán (màu theo giá trung bình)",
x = "Hãng xe",
y = "Số lượng bán",
fill = "Giá trung bình (USD)"
) +
theme_minimal()
print(p6)
- group_by(make)
Kỹ thuật:Gom dữ liệu theo từng hãng xe.
Ý nghĩa thống kê: Xem mỗi hãng là một nhóm phân tích.
- summarise(n = n(), …)
Kỹ thuật: Tính số mẫu (số xe) trong mỗi nhóm.
Ý nghĩa thống kê:n = tổng xe được bán → đo mức độ phổ
biến.
- mean(sellingprice, na.rm=TRUE)
Kỹ thuật:Tính giá bán trung bình.
Ý nghĩa thống kê:Đo mặt bằng giá, thể hiện phân khúc thị
trường.
- sd(sellingprice)
Kỹ thuật:Tính độ lệch chuẩn giá.
Ý nghĩa thống kê: Đo mức phân tán giá, phản ánh sự đa dạng mẫu
xe.
- arrange(desc(n))
Kỹ thuật: Sắp giảm dần theo số lượng bán.
Ý nghĩa thống kê:Chọn hãng bán nhiều nhất trước.
- slice_head(n=10)
Kỹ thuật:Lấy 10 hãng top đầu.
Ý nghĩa thống kê:Dễ nhận diện hãng có thị phần lớn.
- geom_col()
Kỹ thuật:Vẽ biểu đồ cột có chiều cao = số xe bán.
Ý nghĩa thống kê: Xem mỗi hãng là một nhóm phân tích.
- fill = mean_price
Kỹ thuật: Màu sắc cột phụ thuộc giá trung bình.
Ý nghĩa thống kê:So sánh phân khúc giá giữa thương hiệu.
- geom_text(…)
Kỹ thuật:Hiển thị số lượng bán ngay trên cột.
Ý nghĩa thống kê:ránh ước lượng bằng mắt, hỗ trợ đọc chính
xác.
- coord_flip()
Kỹ thuật:Xoay trục → nằm ngang.
Ý nghĩa thống kê: Tăng khả năng đọc tên hãng.
- scale_fill_gradient()
Kỹ thuật:Màu đậm hơn = giá cao hơn.
Ý nghĩa thống kê:Thể hiện mối quan hệ số lượng vs. giá.
num_for_corr <- df_plot %>%
select(year, odometer, sellingprice, mmr, condition) %>%
drop_na()
corrm <- cor(num_for_corr, use = "pairwise.complete.obs")
corr_df <- as.data.frame(as.table(corrm))
p7 <- ggplot(corr_df, aes(Var1, Var2, fill = Freq)) +
geom_tile() +
geom_text(
aes(label = format(round(Freq, 2), decimal.mark = ",")),
size = 3
) +
scale_fill_gradient2(
low = "red", mid = "white", high = "blue", midpoint = 0
) +
geom_point(
data = subset(corr_df, abs(Freq) > 0.6),
aes(Var1, Var2),
shape = 21,
size = 2,
inherit.aes = FALSE
) +
theme_minimal() +
labs(
title = "Ma trận tương quan giữa các biến",
x = "Biến",
y = "Biến",
fill = "Hệ số tương quan"
)
print(p7)
- select(…)
Kỹ thuật:Chọn các biến số để tính tương quan.
Ý nghĩa thống kê: Chỉ những biến liên tục mới có hệ số
Pearson.
- drop_na()
Kỹ thuật: Loại dòng có missing value.
Ý nghĩa thống kê:Tránh sai số khi tính r.
- cor(…, use=“pairwise.complete.obs”)
Kỹ thuật:Tính hệ số tương quan Pearson theo từng cặp quan sát
thực tế.
Ý nghĩa thống kêGiảm mất dữ liệu, không cần
complete-case.
- as.table(…) → as.data.frame()
Kỹ thuật:Chuyển ma trận về dạng long để vẽ ggplot.
Ý nghĩa thống kê: Dễ dàng tạo heatmap.
- geom_tile() Kỹ thuật: Tạo heatmap bằng ô
màu.
Ý nghĩa thống kê:Màu phản ánh độ mạnh của tương quan.
- scale_fill_gradient2()
Kỹ thuật:Đỏ = âm mạnh, xanh = dương mạnh, trắng ≈ 0.
Ý nghĩa thống kê: Tương quan hệ số Pearson r.
- geom_text()
Kỹ thuật:In giá trị r ngay trên ô.
Ý nghĩa thống kê:Đảm bảo đọc chính xác hệ số.
- geom_point(abs(Freq) > 0.6)
Kỹ thuật:Đánh dấu tương quan mạnh.
df_plot <- df_plot %>%
mutate(vehicle_age = ifelse(!is.na(year_saledate),
as.numeric(year_saledate) - year, NA))
medians <- df_plot %>%
group_by(price_cat) %>%
summarise(med = median(odometer, na.rm = TRUE))
p8 <- ggplot(df_plot, aes(x = odometer, y = sellingprice)) +
geom_point(alpha = 0.15, size = 0.5) +
geom_smooth(method = "loess", se = TRUE) +
geom_density2d(alpha = 0.3) +
geom_rug(alpha = 0.05) +
geom_vline(data = medians, aes(xintercept = med),
color="red", linetype="dashed") +
scale_x_continuous(
limits = c(0, 1000000),
breaks = seq(0, 1000000, 100000),
labels = label_number(big.mark = ".", decimal.mark = ",")
) +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
facet_wrap(~ price_cat) +
labs(
title="Giá bán vs số km đã chạy theo nhóm giá (đánh dấu median số km)",
x="Số km đã chạy",
y="Giá bán (USD)"
) +
theme_minimal(base_size = 12) +
theme(
strip.text = element_text(face = "bold", size = 12),
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
)
print(p8)
- mutate(vehicle_age = …)
Kỹ thuật:Tạo biến mới vehicle_age.
Ý nghĩa thống kê: Biến “tuổi xe” là một yếu tố ảnh hưởng giá
bán.
- ifelse(!is.na(year_saledate))
Kỹ thuật:Chỉ tính tuổi xe nếu có năm bán.
Ý nghĩa thống kê:Tránh lỗi NA trong phép trừ.
- as.numeric(year_saledate) - year
Kỹ thuật:Tuổi xe = năm bán – năm sản xuất.
Ý nghĩa thống kê:Hỗ trợ phân tầng để so sánh.
- group_by(price_cat)
Kỹ thuật:Chia xe theo nhóm giá (rẻ, trung bình, đắt).
Ý nghĩa thống kê: Đo mức phân tán giá, phản ánh sự đa dạng mẫu
xe.
- median(odometer)
Kỹ thuật: Tính trung vị số km đã chạy.
Ý nghĩa thống kê:Tránh bị ảnh hưởng bởi outliers (khác
mean).
- geom_point()
Kỹ thuật:Scatterplot từng xe.
Ý nghĩa thống kê:Quan hệ giá vs km chạy dạng phân tán.
- alpha = 0.15
Kỹ thuật:Làm trong suốt để giảm nhiễu.
Ý nghĩa thống kê: Dữ liệu lớn → tránh overplotting.
- geom_smooth(method=“loess”)
Kỹ thuật: Vẽ đường xu hướng phi tuyến.
Ý nghĩa thống kê:Kiểm tra mối quan hệ không tuyến tính.
- geom_density2d()
Kỹ thuật:Đường đẳng mật độ.
Ý nghĩa thống kê:Gợi ý vùng dữ liệu tập trung nhiều nhất.
- geom_rug()
Kỹ thuật:Dấu gạch hai trục.
Ý nghĩa thống kê: Thể hiện phân phối biên.
- geom_vline(… median)
Kỹ thuật:Đường đứng biểu thị median odometer.
Ý nghĩa thống kê:Hỗ trợ so sánh “tuổi chạy trung vị” từng phân
khúc giá.
- facet_wrap(~ price_cat)
Kỹ thuật:Tách biểu đồ theo nhóm giá.
Ý nghĩa thống kê:Giúp trả lời: “Ở nhóm giá rẻ, quan hệ giá–km
có giống nhóm giá cao không?”.
if (require(hexbin)) {
p9 <- ggplot(df_plot %>% drop_na(mmr), aes(x = mmr, y = sellingprice)) +
geom_hex(bins = 40) +
geom_smooth(method = "lm", se = TRUE, color = "red") +
stat_density2d(aes(alpha = ..level..), geom = "polygon") +
geom_rug(alpha = 0.1) +
annotate("text", x = Inf, y = Inf,
label = paste0("Hệ số tương quan = ",
round(cor(df_plot$mmr, df_plot$sellingprice,
use = "complete.obs"), 2)),
hjust = 1.1, vjust = 1.5)
} else {
p9 <- ggplot(df_plot %>% drop_na(mmr), aes(x = mmr, y = sellingprice)) +
geom_point(alpha = 0.2) +
geom_smooth(method = "lm") +
stat_density2d() +
geom_rug() +
annotate("text", x = Inf, y = Inf,
label = "Cài gói hexbin để xem đẹp hơn",
hjust = 1.1)
}
p99 <- p9 + labs(title = "Mối quan hệ giữa MMR và Giá xe",
x = "Giá MMR (USD)",
y = "Giá bán (USD)") +
scale_x_continuous(labels = comma_format(big.mark=".", decimal.mark=",")) +
scale_y_continuous(labels = comma_format(big.mark=".", decimal.mark=",")) +
theme_minimal()
p99
- drop_na(mmr)
Kỹ thuật:Chỉ lấy dòng đủ dữ liệu để tính r.
Ý nghĩa thống kê: Tránh sai số.
- geom_hex(bins=40)
Kỹ thuật:Vẽ biểu đồ mật độ dạng hexagon.
Ý nghĩa thống kê:Tối ưu cho dữ liệu lớn (hơn scatter).
- geom_smooth(method=“lm”)
Kỹ thuật:Vẽ đường hồi quy tuyến tính.
Ý nghĩa thống kê:Kiểm tra mối quan hệ tuyến tính.
- stat_density2d()
Kỹ thuật:Vẽ đường đồng mật độ.
Ý nghĩa thống kê: Gợi ý vùng tập trung dữ liệu.
- annotate(… cor(…))
Kỹ thuật: In hệ số tương quan Pearson r.
Ý nghĩa thống kê:Định lượng độ mạnh quan hệ.
- lm thay vì loess
Kỹ thuật:Vì kỳ vọng quan hệ gần tuyến tính.
Ý nghĩa thống kê:“Giá thị trường → ảnh hưởng trực tiếp đến giá
bán”
p10 <- ggplot(df_plot, aes(x = price_cat, y = odometer, fill = price_cat)) +
geom_boxplot(outlier.shape = NA, alpha = 0.4) +
geom_jitter(width = 0.2, alpha = 0.15, size = 0.6) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "white") +
stat_summary(fun.data = mean_cl_boot, geom = "errorbar", width = 0.2) +
geom_text(
data = df_plot %>% group_by(price_cat) %>% summarise(med = median(odometer, na.rm = TRUE)),
aes(label = paste0("Trung vị: ", format(round(med), big.mark=".", decimal.mark=",")),
y = med),
vjust = -1.2, size = 3
) +
scale_y_continuous(labels = comma_format(big.mark=".", decimal.mark=",")) +
labs(
title = "Số km theo nhóm mức giá",
x = "Nhóm giá",
y = "Số km đã đi (Odometer)"
) +
theme_minimal()
p10
- aes(x=price_cat, y=odometer)
Kỹ thuật:Trục x là nhóm giá, trục y là km đã chạy.
Ý nghĩa thống kê: So sánh phân phối odometer theo từng phân
khúc.
- geom_boxplot(outlier.shape=NA)
Kỹ thuật: Vẽ boxplot và ẩn outliers.
Ý nghĩa thống kê:Tập trung vào phân bố chính, tránh
nhiễu.
- geom_jitter()
Kỹ thuật:Thêm điểm phân tán.
Ý nghĩa thống kê:Cho thấy độ dày dữ liệu, tránh “boxplot ảo
giác”.
- stat_summary(fun=mean)
Kỹ thuật:Chấm mean trên từng box.
Ý nghĩa thống kê: Để so sánh mean ≠ median khi phân phối
lệch.
- mean_cl_boot Kỹ thuật:Vẽ CI bootstrap cho
mean.
Ý nghĩa thống kê:Phép kiểm định độ chính xác trung bình.
- geom_text(… median)
Kỹ thuật:In giá trị trung vị lên biểu đồ.
Ý nghĩa thống kê:Dễ diễn giải: “Nhóm cao cấp chạy ít km
hơn”.
- scale_y_continuous(labels=comma_format)
Kỹ thuật:Hiển thị giá trị có dấu phân cách.
Ý nghĩa thống kê:Tăng tính đọc hiểu.
- theme_minimal()
Kỹ thuật:Phong cách tối giản.
Ý nghĩa thống kê:Trình bày chuẩn báo cáo khoa học.
if("year_saledate" %in% names(df_plot)) {
ts <- df_plot %>%
group_by(year_saledate) %>%
summarise(n = n(),
mean_price = mean(sellingprice, na.rm = TRUE))
p11 <- ggplot(ts, aes(x = year_saledate, y = n)) +
geom_line() +
geom_point() +
geom_smooth(method = "loess", se = TRUE) +
geom_ribbon(aes(
ymin = pmax(0, n - sd(n, na.rm = TRUE)),
ymax = n + sd(n, na.rm = TRUE)
), alpha = 0.1) +
geom_vline(xintercept = 2015, linetype="dashed") +
geom_text(aes(label = format(n, big.mark=".", decimal.mark=",")),
vjust = -1, size=2.8) +
scale_y_continuous(labels = label_comma(big.mark=".", decimal.mark=",")) +
labs(
title="Số lượng xe bán theo năm",
x="Năm",
y="Số xe"
)
} else {
p11 <- ggplot() + ggtitle("Không có cột year_saledate")
}
p11
- group_by(year_saledate)
Kỹ thuật:Tổng hợp theo năm bán xe.
Ý nghĩa thống kê: Chuẩn bị cho phân tích chuỗi thời gian.
- summarise(n=n())
Kỹ thuật: Đếm số xe bán mỗi năm.
Ý nghĩa thống kê:Biến tần suất bán.
- geom_line()
Kỹ thuật:Vẽ chuỗi thời gian.
Ý nghĩa thống kê:Hiển thị xu hướng liên tục.
- geom_point()
Kỹ thuật:Đánh dấu từng năm.
Ý nghĩa thống kê:Tránh hiểu sai về giá trị gốc.
- geom_smooth(method=“loess”)
Kỹ thuật:Đường xu thế mềm.
Ý nghĩa thống kê:Kiểm tra sự biến động phi tuyến.
- geom_ribbon(… ± sd)
Kỹ thuật:Vẽ dải ±1 SD.
Ý nghĩa thống kê:Đo lường mức biến động tự nhiên.
- geom_vline(xintercept=2015)
Kỹ thuật:Đánh dấu mốc sự kiện.
Ý nghĩa thống kê:Nhấn mạnh điểm gãy thời gian.
- geom_text(label=n)
Kỹ thuật:Hiển thị số lượng lên từng điểm.
Ý nghĩa thống kê:Dùng trong báo cáo quản trị.
- scale_y_continuous(…)
Kỹ thuật:Format số có dấu chấm/thập phân.
Ý nghĩa thống kê:Chuẩn trình bày thống kê.
df_plot <- df_plot %>%
filter(!is.na(condition)) %>%
mutate(
condition_group = cut(condition,
breaks = c(0, 5, 10, 20, 50),
labels = c("Rất kém", "Trung bình", "Tốt", "Xuất sắc"))
)
meds <- df_plot %>%
group_by(condition_group) %>%
summarise(med = median(sellingprice, na.rm = TRUE))
p12 <- ggplot(df_plot, aes(x=condition_group, y=sellingprice, fill=condition_group)) +
geom_violin(alpha=0.4) +
geom_boxplot(width=0.12, outlier.shape=NA) +
stat_summary(fun=mean, geom="point", shape=23, size=2.3, fill="white") +
geom_text(data = meds,
aes(label = paste0(format(med, big.mark=".", decimal.mark=","), " USD"),
y = med),
vjust=-0.8, size=3, fontface="bold") +
scale_y_continuous(labels = label_comma(big.mark=".", decimal.mark=",")) +
scale_fill_brewer(palette="Set2") +
labs(
title="Giá xe theo nhóm tình trạng",
x="Tình trạng xe",
y="Giá bán"
) +
coord_cartesian(ylim=c(0, 80000))
p12
- filter(!is.na(condition))
Kỹ thuật:Lọc bỏ các hàng có condition = NA.
Ý nghĩa thống kê: Đảm bảo thống kê chỉ dựa trên quan sát có giá
trị; nếu giữ NA, số lượng và trung bình sẽ bị sai hoặc gây nhầm
lẫn.
- mutate(condition_group = cut(…))
Kỹ thuật: Chuyển biến liên tục condition thành biến phân loại
(4 nhóm) bằng cut() với ngưỡng bạn đặt.
Ý nghĩa thống kê:Phân tầng cho phép so sánh phân phối giá giữa
các mức tình trạng (rất kém → xuất sắc); cần thiết khi muốn báo cáo theo
nhóm.
- group_by(condition_group)
Kỹ thuật:Gom các hàng theo nhóm để áp dụng hàm tóm tắt.
Ý nghĩa thống kê:Tính thống kê mô tả riêng cho từng lớp — bước
cơ bản trong EDA (Exploratory Data Analysis).
- summarise(med = median(…))
Kỹ thuật:Tính trung vị (median) của sellingprice trong mỗi
nhóm, bỏ qua giá trị thiếu.
Ý nghĩa thống kê:Dùng trung vị để phản ánh mức giá điển hình,
ít bị ảnh hưởng bởi các giá trị ngoại lai (xe quá đắt hoặc quá
rẻ).
- ggplot(…, aes(…))
Kỹ thuật:Khởi tạo đồ thị ggplot với trục X là nhóm tình trạng
xe, trục Y là giá bán, tô màu theo nhóm.
Ý nghĩa thống kê:Thiết lập nền tảng cho biểu đồ so sánh phân
phối giá giữa các nhóm chất lượng.
- geom_violin(alpha=0.4)
Kỹ thuật:Vẽ biểu đồ violin (phân phối xác suất đối xứng), làm
mờ (alpha=0.4).
Ý nghĩa thống kê:Hiển thị dạng phân phối của giá bán trong từng
nhóm — cho thấy độ phân tán, đối xứng và mật độ giá.
- geom_boxplot(width=0.12, outlier.shape=NA)
Kỹ thuật:Thêm hộp boxplot chồng lên violin, giới hạn chiều rộng
nhỏ để không che biểu đồ. Loại bỏ ký hiệu điểm ngoại lai.
Ý nghĩa thống kê:Thể hiện tóm tắt 5 giá trị (min, Q1, median,
Q3, max) giúp dễ so sánh vị trí và độ lệch giữa các nhóm.
- stat_summary(fun=mean, geom=“point”, shape=23, size=2.3,
fill=“white”)
Kỹ thuật:Tính giá trị trung bình và vẽ dưới dạng điểm trắng
nhỏ.
Ý nghĩa thống kê:Phân biệt giữa trung bình và trung vị, giúp
nhận biết nếu phân phối bị lệch (mean ≠ median).
- geom_text(data = meds, aes(label=…, y=med), vjust=-0.8,
size=3, fontface=“bold”)
Kỹ thuật:Ghi nhãn giá trị trung vị (med) trên từng nhóm, định
dạng số có dấu chấm ngăn cách hàng nghìn.
Ý nghĩa thống kê:Giúp người đọc nắm rõ giá trị trung vị mà
không cần ước lượng bằng mắt, thuận tiện cho báo cáo.
- scale_y_continuous(labels = label_comma(…))
Kỹ thuật:Hiển thị trục Y dạng “30.000” thay vì “30000”.
Ý nghĩa thống kê:Chuẩn hóa định dạng tiền tệ, tăng tính trực
quan khi trình bày giá bán.
- scale_fill_brewer(palette=“Set2”)
Kỹ thuật:Dùng bảng màu phân biệt nhóm rõ ràng, thân thiện thị
giác.
Ý nghĩa thống kê:Tăng khả năng nhận diện nhóm tình trạng xe,
đặc biệt khi in màu hoặc trình chiếu.
- coord_cartesian(ylim=c(0,80000))
Kỹ thuật:Giới hạn trục Y trong khoảng 0–80.000 để loại bỏ vùng
giá quá cao, giữ khung biểu đồ dễ đọc.
Ý nghĩa thống kê:GTập trung vào vùng giá phổ biến, tránh biến
dạng tỷ lệ do ngoại lệ cực lớn.
### 1.4.14. Minh họa quan hệ tuyến tính giữa các cặp biến
library(GGally) # Cài GGally để xem ma trận tương quan (ggpairs)
p13 <- GGally::ggpairs(
df_plot %>% select(year, odometer, sellingprice, mmr),
upper = list(continuous = GGally::wrap("cor", size = 3)),
lower = list(continuous = "smooth"),
diag = list(continuous = "barDiag")
) +
ggtitle("Ma trận tương quan các biến chính")
p13
- library(GGally)
Kỹ thuật:: Nạp gói GGally, mở rộng từ ggplot2, chuyên dùng cho
các biểu đồ ma trận (pair plot).
Ý nghĩa thống kê: Cho phép trực quan mối quan hệ giữa nhiều
biến cùng lúc (liên tục hoặc phân loại), thay vì chỉ xem từng cặp riêng
lẻ.
- df_plot %>% select(year, odometer, sellingprice,
mmr)
Kỹ thuật: Chọn bốn biến chính từ bộ dữ liệu: năm sản xuất
(year), số km đã chạy (odometer), giá bán (sellingprice), và giá thị
trường tham chiếu (mmr).
Ý nghĩa thống kêĐây là các biến liên quan trực tiếp đến giá trị
và tuổi đời xe, cần xem mức tương quan giữa chúng.
- GGally::ggpairs(…)
Kỹ thuật: Tạo ma trận biểu đồ:
- Trục chéo (diag) hiển thị histogram (hoặc bar) của từng biến.
- Ô dưới đường chéo (lower) hiển thị biểu đồ phân tán
(scatter/smooth).
- Ô trên đường chéo (upper) hiển thị hệ số tương quan
(correlation).
Ý nghĩa thống kê:Giúp đánh giá tổng quan:
- Quan hệ tuyến tính hoặc phi tuyến giữa các biến.
- Mức độ tương quan mạnh/yếu, dương/âm.
- Biến nào có thể gây đa cộng tuyến trong mô hình hồi quy.
p15 <- df_plot %>%
filter(!is.na(transmission)) %>%
count(transmission, price_cat) %>%
group_by(transmission) %>%
mutate(pct = n / sum(n)) %>%
ggplot(aes(x = transmission, y = pct, fill = price_cat)) +
geom_col(color = "black") +
geom_text(aes(label = percent(pct, accuracy = 0.1)),
position = position_stack(vjust = 0.5), size = 3) +
coord_flip() +
labs(title = "Tỷ lệ phân khúc giá theo loại hộp số",
x = "Hộp số", y = "Tỷ lệ (%)",
fill = "Phân khúc giá") +
scale_y_continuous(labels = percent_format())
p15
- filter(!is.na(transmission))
Kỹ thuật:Loại bỏ dòng thiếu thông tin về loại hộp số.
Ý nghĩa thống kê:Đảm bảo chỉ phân tích trên các xe có dữ liệu
hợp lệ, tránh tính sai tỷ lệ phần trăm.
- count(transmission, price_cat)
Kỹ thuật:Đếm số xe theo từng cặp transmission (hộp số) và
price_cat (phân khúc giá).
Ý nghĩa thống kê:Tạo bảng tần suất hai chiều, dùng để tính tỷ
trọng từng phân khúc giá trong mỗi loại hộp số.
- group_by(transmission)
Kỹ thuật:Gom nhóm theo loại hộp số để tính tỷ lệ phần trăm
trong mỗi nhóm.
Ý nghĩa thống kê:Giúp so sánh cấu trúc giá bên trong từng nhóm
hộp số, thay vì toàn bộ thị trường.
- mutate(pct = n / sum(n))
Kỹ thuật:Tính tỷ trọng (phần trăm) mỗi phân khúc giá trong từng
nhóm hộp số.
Ý nghĩa thống kê:Chuyển số đếm sang tỷ lệ, giúp so sánh công
bằng giữa các nhóm có quy mô khác nhau.
- ggplot(aes(…))
Kỹ thuật:Khởi tạo biểu đồ cột với trục X là hộp số, trục Y là
tỷ lệ, tô màu theo phân khúc giá.
Ý nghĩa thống kê:Thể hiện cấu trúc giá trong từng nhóm hộp số,
ví dụ hộp số tự động có nhiều xe ở phân khúc cao.
- Geom_col(color = “black”)
Kỹ thuật:Vẽ cột stacked (xếp chồng), có viền đen.
Ý nghĩa thống kê:Giúp trực quan so sánh tỷ trọng giữa các phân
khúc giá theo loại hộp số.
- geom_text(aes(label = percent(pct, accuracy = 0.1)), position
= position_stack(vjust = 0.5), size = 3)
Kỹ thuật:Hiển thị nhãn phần trăm giữa các cột, căn giữa theo
chiều cao.
Ý nghĩa thống kê:Cho thấy tỷ lệ chính xác từng phân khúc, tăng
khả năng đọc hiểu trực tiếp từ biểu đồ.
- coord_flip() Kỹ thuật:Lật trục hoành – tung
để cột nằm ngang.
Ý nghĩa thống kê:Dễ đọc hơn khi tên nhóm dài (“Automatic”,
“Manual”), phù hợp cho biểu đồ so sánh danh mục.
- scale_y_continuous(labels = percent_format())
Kỹ thuật:Định dạng trục Y theo phần trăm (ví dụ 0.25 →
25%).
Ý nghĩa thống kê:Biểu diễn tỷ lệ đúng bản chất (phần trăm cấu
phần).
p16 <- ggplot(df_plot, aes(x = year, y = odometer, color = price_cat)) +
geom_point(alpha = 0.15) +
geom_smooth(aes(group = price_cat), se = FALSE) +
stat_density2d(aes(alpha = ..level..), geom = "polygon") +
geom_rug(alpha = 0.05) +
stat_summary(fun = median, geom = "line", aes(group = price_cat), linetype = "dashed") +
scale_y_continuous(labels = label_comma(big.mark=".", decimal.mark=",")) +
labs(
title = "Số km đi theo năm sản xuất theo từng nhóm giá",
x = "Năm sản xuất",
y = "Quãng đường đã chạy (km)",
color = "Nhóm giá"
)
p16
- ggplot(df_plot, aes(x = year, y = odometer, color =
price_cat))
Kỹ thuật:Khởi tạo đối tượng ggplot, ánh xạ year → trục x,
odometer → trục y, price_cat → màu điểm/đường.
Ý nghĩa thống kê:Xác định biến quan tâm (năm và quãng đường) và
phân lớp theo nhóm giá để so sánh mối quan hệ giữa các nhóm.
- geom_point(alpha = 0.15)
Kỹ thuật:Vẽ điểm rời cho từng quan sát với độ mờ alpha = 0.15
(15% opacity).
Ý nghĩa thống kê:Hiển thị phân bố rời rạc của các quan sát;
alpha thấp giảm chồng chéo khi dữ liệu dày, giúp thấy vùng mật độ cao
hơn qua đậm độ điểm. - geom_smooth(aes(group = price_cat), se =
FALSE)
Kỹ thuật:Vẽ đường xu hướng (mặc định là LOESS cho dữ liệu liên
tục nhỏ) cho từng nhóm price_cat bằng cách nhóm theo price_cat; se =
FALSE tắt băng sai số quanh đường.
Ý nghĩa thống kê:Minh họa xu hướng trung tâm (mean/locally
smoothed) của quãng đường theo năm trong từng nhóm giá, giúp so sánh
hướng và dốc xu hướng giữa các nhóm.
- geom_rug(alpha = 0.05)
Kỹ thuật:Thêm các vạch nhỏ dọc trục x/y tại vị trí từng quan
sát; alpha = 0.05 làm cho chúng mờ.
Ý nghĩa thống kê:Cung cấp thông tin phân bố biên (marginal
distribution) dọc mỗi trục; giúp nhìn thấy tần suất biên ở các giá trị
nhỏ/ lớn mà có thể không rõ từ scatter.
- stat_summary(fun = median, geom = “line”, aes(group =
price_cat), linetype = “dashed”)
Kỹ thuật:Tính giá trị trung vị odometer theo từng year cho mỗi
price_cat và nối thành đường đứt (dashed).
Ý nghĩa thống kê:Trung vị bền hơn trước ngoại lệ so với trung
bình; đường median cho thấy vị trí trung tâm thực tế của phân bố theo
thời gian cho từng nhóm giá, hữu ích khi phân bố lệch.
- scale_y_continuous(labels = label_comma(big.mark=“.”,
decimal.mark=“,”))
Kỹ thuật:Tùy chỉnh nhãn trục y để nhóm hàng nghìn bằng dấu chấm
và thập phân bằng dấu phẩy.
Ý nghĩa thống kê:Cải thiện khả năng đọc và trình bày các giá
trị quãng đường lớn, không ảnh hưởng đến tính toán nhưng hỗ trợ hiểu số
liệu.
library(ggrepel)
agg_make <- df_plot %>%
group_by(make) %>%
summarise(
n = n(),
mean_price = mean(sellingprice, na.rm = TRUE),
mean_odometer = mean(odometer, na.rm = TRUE)
) %>%
filter(n > 50)
p17 <- ggplot(agg_make, aes(x = mean_odometer, y = mean_price, size = n)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", se = TRUE, color = "red") +
geom_text_repel(aes(label = make), size = 3) +
scale_y_continuous(labels = label_comma(big.mark=".", decimal.mark=",")) +
scale_x_continuous(labels = label_comma(big.mark=".", decimal.mark=",")) +
scale_size_continuous(range = c(2, 10)) +
labs(
title = "Giá trung bình vs Quãng đường trung bình theo hãng",
x = "Quãng đường trung bình (km)",
y = "Giá trung bình (USD)",
size = "Số lượng xe"
)
p17
- library(ggrepel)
Kỹ thuật:Nạp gói ggrepel để dùng geom_text_repel().
Ý nghĩa thống kê:Không trực tiếp thay đổi thống kê; cải thiện
trình bày nhãn để tránh chồng chữ, giúp đọc tên hãng rõ ràng.
- agg_make <- df_plot %>% group_by(make) %>%
summarise(…)
Kỹ thuật:Gom theo make (hãng).
Ý nghĩa thống kê:: Biến đổi dữ liệu từ mức quan sát sang mức
hãng để so sánh đặc trưng trung bình giữa các hãng: cho phép phân tích
liên-hãng thay vì từng xe.
- filter(n > 50)
Kỹ thuật:filter(n > 50).
Ý nghĩa thống kê:Giảm nhiễu từ hãng có mẫu quá nhỏ (ước lượng
mean không ổn định); đảm bảo các điểm đại diện có ý nghĩa thống kê
hơn.
- ggplot(agg_make, aes(x = mean_odometer, y = mean_price, size =
n))
Kỹ thuật:Khởi tạo biểu đồ nơi mỗi điểm là một hãng; trục x =
quãng đường trung bình, trục y = giá trung bình, kích thước điểm theo
n.
Ý nghĩa thống kê:Cho phép trực quan mối quan hệ giữa quãng
đường trung bình và giá trung bình ở mức hãng, đồng thời phản ánh quy mô
mẫu của từng hãng.
- geom_point(alpha = 0.6)
Kỹ thuật:VVẽ điểm với độ mờ 60%.
Ý nghĩa thống kê:Hiển thị các hãng; alpha vừa phải giữ cho nhãn
vẫn rõ khi có chồng lấp.
- geom_smooth(method = “lm”, se = TRUE, color =
“red”)
Kỹ thuật:Vẽ đường hồi quy tuyến tính (OLS) giữa mean_odometer
và mean_price; se = TRUE hiển thị băng sai số (confidence
interval).
Ý nghĩa thống kê:Ước lượng mối quan hệ tuyến tính tổng quát
giữa quãng đường trung bình và giá trung bình ở mức hãng; băng se báo độ
không chắc chắn của ước lượng đường hồi quy.
- geom_text_repel(aes(label = make), size = 3)
Kỹ thuật:Gắn nhãn tên hãng vào điểm, tự động di chuyển nhãn để
không chồng nhau (tự động tránh overlap).
Ý nghĩa thống kê:Giúp xác định trực quan những hãng cụ thể
(outliers, leaders, followers) mà không làm rối biểu đồ; hỗ trợ phân
tích định tính.
- scale_size_continuous(range = c(2, 10))
Kỹ thuật:Điều chỉnh kích thước tối thiểu và tối đa của điểm
theo n.
Ý nghĩa thống kê:Làm rõ tầm quan trọng tương đối của từng hãng
theo số mẫu; hãng có nhiều xe hiển thị lớn hơn, dễ phân biệt
library(broom)
lm1 <- lm(sellingprice ~ odometer + year + condition, data = df_plot)
resid_df <- broom::augment(lm1) %>% drop_na(.resid)
# phân tán phần dư và giá trị dự đoán
p18a <- ggplot(resid_df, aes(.fitted, .resid)) +
geom_point(alpha = 0.2) +
geom_smooth(method = "loess", se = TRUE) +
geom_hline(yintercept = 0, linetype = "dashed") +
geom_rug(sides = "b", alpha = 0.1) +
labs(
title = "Phân tán phần dư vs Giá trị dự đoán",
x = "Giá trị dự đoán",
y = "Phần dư"
)
# Phân phối phần dư
p18b <- ggplot(resid_df, aes(.resid)) +
geom_histogram(bins = 30, alpha = 0.6) +
geom_density(aes(y = ..count..), color = "black") +
labs(
title = "Phân phối phần dư",
x = "Phần dư",
y = "Tần suất"
)
p18 <- cowplot::plot_grid(p18a, p18b, ncol = 1)
p18
- library(broom)
Kỹ thuật:Nạp package broom (hàm như augment, tidy, glance) để
chuyển kết quả mô hình thành bảng dữ liệu tidy.
Ý nghĩa thống kê:broom giúp trích các phần tử chẩn đoán (dự
đoán, phần dư, influential measures) từ mô hình tuyến tính dưới dạng
dataframe để có thể phân tích và trực quan hóa dễ dàng.
- lm1 <- lm(sellingprice ~ odometer + year + condition, data
= df_plot)
Kỹ thuật:lm1 <- lm(sellingprice ~ odometer + year +
condition, data = df_plot).
Ý nghĩa thống kê:Xây mô hình để xét ảnh hưởng đồng thời của
quãng đường, năm sản xuất và tình trạng lên giá bán; kết quả sẽ cho hệ
số ước lượng, sai số chuẩn, p-value, R²… — cơ sở cho kiểm định giả
thuyết về tác động từng biến.
- resid_df <- broom::augment(lm1) %>%
drop_na(.resid)
Kỹ thuật:broom::augment(lm1) tạo dataframe từ lm1 chứa các cột
chẩn đoán quan trọng như .fitted (giá trị dự đoán), .resid (phần dư),
.hat (leverage), .cooksd (Cook’s distance), v.v..
Ý nghĩa thống kê: Chuẩn bị dữ liệu phần dư và giá trị dự đoán
cho kiểm tra giả thiết mô hình (độc lập, đồng nhất phương sai, phân phối
chuẩn). Loại bỏ NA đảm bảo các đồ thị phần dư không bị lỗi và không kéo
lệch phân tích.
- ggplot(resid_df, aes(.fitted, .resid))
Kỹ thuật:Khởi tạo ggplot với dữ liệu resid_df, ánh .fitted lên
trục X và .resid lên trục Y.
Ý nghĩa thống kê:Biểu đồ scatter residuals vs fitted là công cụ
tiêu chuẩn để kiểm tra các giả thiết của mô hình tuyến tính (tính tuyến
tính, đồng nhất phương sai, phát hiện pattern hệ thống).
- geom_point(alpha = 0.2) +
Kỹ thuật:Vẽ điểm cho từng quan sát phần dư với độ mờ 20%.
Ý nghĩa thống kê:Hiển thị phân bố phần dư theo giá trị dự đoán;
alpha thấp giúp giảm chồng chéo nếu dữ liệu nhiều.
- geom_smooth(method = “loess”, se = TRUE) +
Kỹ thuật:Vẽ đường mượt LOESS ước lượng xu hướng trung bình của
phần dư theo .fitted, se = TRUE hiển thị băng sai số cho đường
mượt.
Ý nghĩa thống kê:Nếu đường mượt khác đường ngang tại 0 thì có
pattern có hệ thống (mô hình thiếu biến, không tuyến tính, hoặc biến
tương tác cần thêm). Băng sai số cho thấy độ tin cậy của ước lượng
mượt.
- geom_hline(yintercept = 0, linetype = “dashed”)
+
Kỹ thuật:Vẽ đường ngang tại y = 0 (đứt nét).
Ý nghĩa thống kê:Mốc tham chiếu — phần dư kỳ vọng bằng 0; phân
bố phần dư nên xung quanh 0 và không có xu hướng có hệ thống.
- geom_rug(sides = “b”, alpha = 0.1) +
Kỹ thuật:Thêm các vạch nhỏ dọc theo trục đáy (sides = “b”) cho
vị trí .fitted, mờ 10%.
Ý nghĩa thống kê:Cho biết mật độ biên của giá trị dự đoán; giúp
nhận diện vùng .fitted tập trung nhiều quan sát (có thể liên quan đến
heteroscedasticity trong khu vực đó).
- ggplot(resid_df, aes(.resid)) +
Kỹ thuật:Khởi tạo biểu đồ với biến .resid làm dữ liệu 1
chiều.
Ý nghĩa thống kê:Dùng để kiểm tra giả thuyết phân phối chuẩn
của phần dư (điều kiện quan trọng cho kiểm định t, p-values trong
lm).
- eom_histogram(bins = 30, alpha = 0.6) +
Kỹ thuật:Vẽ histogram có 30 bins, độ mờ 60%.
Ý nghĩa thống kê:Quan sát hình dạng phân phối phần dư (đối
xứng/ lệch/ đa đỉnh), xác định ngoại lệ lớn.
- geom_density(aes(y = ..count..), color = “black”)
+
Kỹ thuật:Vẽ đường mật độ kernel; ánh y vào ..count.. để tỉ lệ
với histogram (không chuẩn hóa).
Ý nghĩa thống kê:So sánh đường mật độ với histogram giúp thấy
rõ hình dạng phân phối; nếu mật độ gần chuẩn (hình chuông đối xứng) →
phần dư xấp xỉ chuẩn.
p19 <- ggplot(df_plot, aes(x = sellingprice)) +
stat_ecdf(geom="step", size=0.8) +
geom_vline(
xintercept = quantile(df_plot$sellingprice, c(0.25, 0.5, 0.75), na.rm = TRUE),
linetype = c("dotted", "solid", "dashed")
) +
annotate(
"rect",
xmin = quantile(df_plot$sellingprice, 0.25, na.rm = TRUE),
xmax = quantile(df_plot$sellingprice, 0.75, na.rm = TRUE),
ymin = 0, ymax = 1,
alpha = 0.08
) +
geom_rug(alpha = 0.2) +
geom_point(
aes(x = median(sellingprice, na.rm = TRUE), y = 0.5),
color = "red", size = 3
) +
labs(
title = "Phân phối tích lũy của giá bán (ECDF)",
x = "Giá bán (USD)",
y = "Xác suất tích lũy"
) +
annotate(
"text",
x = median(df_plot$sellingprice, na.rm = TRUE),
y = 0.55,
label = paste0("Median: ", scales::comma(round(median(df_plot$sellingprice, na.rm = TRUE)))),
color = "red",
size = 3,
vjust = -0.5
) +
theme_minimal()
p19
- ggplot(df_plot, aes(x = sellingprice))
Kỹ thuật:Khởi tạo đối tượng ggplot sử dụng df_plot, ánh biến
sellingprice lên trục X.
Ý nghĩa thống kê:Xác định biến cần phân tích phân phối tích lũy
(ECDF).
- stat_ecdf(geom=“step”, size=0.8)
Kỹ thuật:Vẽ hàm phân phối tích lũy thuận nghiệm (ECDF) dưới
dạng đường bậc thang (step).
Ý nghĩa thống kê:Hiển thị tỷ lệ phần trăm quan sát ≤ mỗi giá
trị — trực quan hoá phân phối toàn bộ mẫu, không cần bin.
- geom_vline(xintercept = quantile(df_plot$sellingprice, c(0.25,
0.5, 0.75), na.rm = TRUE), linetype = c(“dotted”, “solid”,
“dashed”))
Kỹ thuật:Vẽ các đường thẳng đứng tại các quantile Q1, median,
Q3; kiểu nét khác nhau cho mỗi quantile.
Ý nghĩa thống kê:Đánh dấu các mốc phân vị (25%, 50%, 75%) giúp
xác định vị trí trung tâm và lan tỏa của phân phối.
- annotate(“rect”, xmin = quantile(…,0.25), xmax =
quantile(…,0.75), ymin = 0, ymax = 1, alpha = 0.08)
Kỹ thuật:Vẽ vùng chữ nhật mờ trải từ Q1 đến Q3 trên toàn bộ
trục Y.
Ý nghĩa thống kê:Làm nổi bật IQR (interquartile range) — vùng
giữa Q1 và Q3 chứa 50% dữ liệu trung tâm.
- geom_rug(alpha = 0.2)
Kỹ thuật:Thêm các vạch nhỏ dọc trục X tại vị trí từng quan sát;
mờ alpha = 0.2.
Ý nghĩa thống kê:HHiện mật độ biên của mẫu trên trục giá; giúp
thấy các điểm tập trung hay rải rác.
- geom_point(aes(x = median(sellingprice, na.rm = TRUE), y =
0.5), color = “red”, size = 3)
Kỹ thuật:Vẽ một điểm đỏ tại (median, 0.5) — tức điểm trung vị
trên ECDF.
Ý nghĩa thống kê:Nhấn mạnh trực quan vị trí median; trên ECDF
median luôn có giá trị tích lũy = 0.5.
library(patchwork)
top4 <- df_plot %>%
count(make, sort=TRUE) %>%
slice_head(n=4) %>%
pull(make)
# Data median cho text
med_top4 <- df_plot %>%
filter(make %in% top4) %>%
group_by(make) %>%
summarise(med = median(sellingprice, na.rm = TRUE))
## ---- Panel A: Boxplot + jitter + median text ----
pA <- ggplot(df_plot %>% filter(make %in% top4),
aes(x=make, y=sellingprice, fill=make)) +
geom_boxplot(alpha=0.6) +
geom_jitter(width=0.2, alpha=0.2) +
stat_summary(fun=mean, geom="point", shape=23, size=2, fill="white") +
geom_text(data = med_top4,
aes(label=paste0(round(med/1000),"k"), y = med),
vjust=-1.0, fontface="bold") +
labs(
title = "Phân bố giá theo hãng xe",
y = "Giá bán (USD)",
x = "Hãng xe"
) +
theme(legend.position="none")
## ---- Panel B: Odometer vs Price ----
pB <- ggplot(df_plot %>% filter(make %in% top4),
aes(x=odometer, y=sellingprice, color=make)) +
geom_point(alpha=0.25) +
geom_smooth(se=FALSE) +
geom_rug(alpha=0.15) +
stat_density2d(aes(alpha=..level..), geom="polygon") +
geom_abline(linetype="dotted") +
labs(
title="Số km đã chạy vs Giá bán",
x="Số km đã chạy",
y="Giá bán (USD)"
)
## ---- Panel C: Density ----
pC <- ggplot(df_plot %>% filter(make %in% top4),
aes(x=sellingprice, fill=make)) +
geom_density(alpha=0.3) +
geom_vline(xintercept = median(df_plot$sellingprice, na.rm=TRUE),
linetype="dashed") +
geom_rug(alpha=0.15) +
stat_summary(fun=mean, geom="point", aes(y=0), size=2, color="black") +
labs(
title="Mật độ phân phối giá",
x="Giá bán (USD)",
y="Mật độ"
) +
theme(legend.position="none")
## ---- Panel D: Price vs Year ----
pD <- ggplot(df_plot %>% filter(make %in% top4),
aes(x=year, y=sellingprice, color=make)) +
geom_jitter(width=0.2, alpha=0.25) +
geom_smooth(se=FALSE) +
stat_summary(fun=median, geom="line", aes(group=make),
linetype="dashed", size=0.8) +
scale_x_continuous(breaks = seq(min(df_plot$year, na.rm=TRUE),
max(df_plot$year, na.rm=TRUE),
by = 10)) + # ✅ mỗi 10 năm
labs(
title="Giá bán theo năm sản xuất",
x="Năm sản xuất",
y="Giá bán (USD)"
) +
theme_minimal() +
theme(legend.position="none")
## ---- Combine ----
p20 <- (pA + pB) / (pC + pD) +
plot_annotation(
title="Bảng tổng hợp so sánh Top 4 hãng xe",
subtitle="So sánh phân bố giá, số km, xu hướng theo năm và mật độ giá"
)
p20 <- p20 & theme(
plot.title = element_text(size = 10),
axis.title = element_text(size = 9),
axis.text = element_text(size = 7),
strip.text = element_text(size = 8)
)
p20
- library(patchwork)
Kỹ thuật:Nạp gói patchwork để ghép nhiều ggplot lại thành một
layout.
Ý nghĩa thống kê:Không ảnh hưởng tính toán; hỗ trợ trình bày
tổng hợp nhiều biểu đồ để so sánh trực quan.
- top4 <- df_plot %>% count(make, sort=TRUE) %>%
slice_head(n=4) %>% pull(make)
Kỹ thuậtĐếm số quan sát theo make, sắp giảm dần, lấy 4 hãng
hàng đầu, trích tên thành vector top4.
Ý nghĩa thống kê:Chọn các hãng có mẫu lớn nhất để phân tích ổn
định — tránh kết luận từ hãng có mẫu quá nhỏ.
- med_top4 <- df_plot %>% filter(make %in% top4) %>%
group_by(make) %>% summarise(med = median(sellingprice, na.rm =
TRUE))
Kỹ thuật:Lọc chỉ top4, gom theo hãng, tính median giá bán cho
mỗi hãng.
Ý nghĩa thống kê:rung vị phản ánh giá điển hình cho từng hãng,
bớt bị ảnh hưởng bởi ngoại lệ.
- p20 <- (pA + pB) / (pC + pD) + plot_annotation(title=…,
subtitle=…)
Kỹ thuật:Dùng patchwork ghép 4 panel thành ma trận 2x2 và thêm
tiêu đề/subtitle.
Ý nghĩa thống kê:Trưng bày đồng thời nhiều khía cạnh (phân bố,
mối quan hệ, mật độ, xu hướng) để so sánh giữa top4 hãng.
- p20 <- p20 & theme(plot.title = element_text(size =
10), axis.title = element_text(size = 9), axis.text = element_text(size
= 7), strip.text = element_text(size = 8))
Kỹ thuật:Áp dụng theme chung cho tất cả các plot trong
patchwork.
Ý nghĩa thống kê:Đảm bảo readability nhất quán khi in/chiếu;
không ảnh hưởng nội dung số liệu.
Số lượng biến và quan sát
# Nạp dữ liệu
bctc <- read_excel("C:/data/bctc.xlsx")
# Số lượng biến
cat("Số lượng biến:", ncol(bctc), "\n")
## Số lượng biến: 19
# Số lượng quan quan sát
cat("Số lượng quan sát :", nrow(bctc), "\n")
## Số lượng quan sát : 11
Tên biến
# Xem tên các biến
data.frame(
STT = 1:length(names(bctc)),
TenBien = names(bctc)
) %>%
mutate(TenBien = gsub(" ", "\n", TenBien)) %>% # Xuống dòng khi cần
kbl(caption = "Danh sách các biến trong dữ liệu", booktabs = TRUE, longtable = TRUE) %>%
kable_paper(full_width = FALSE) %>%
column_spec(2, width = "10cm") %>% # Giới hạn độ rộng cột tên biến
kable_styling(latex_options = c("hold_position", "scale_down"))
| STT | TenBien |
|---|---|
| 1 | Year |
| 2 | Tổng nợ |
| 3 | VCSH |
| 4 | Tổng tài sản |
| 5 | Cho vay khách hàng |
| 6 | Tiền gửi khách hàng |
| 7 | Thu nhập lãi thuần |
| 8 | Lãi thuần từ hoạt động dịch vụ |
| 9 | Lãi thuần từ kinh doanh ngoại hối và vàng |
| 10 | Lãi thuần từ mua bán chứng khoán kinh doanh, chứng khoán đầu tư và góp vốn đầu tư dài hạn |
| 11 | Thu nhập thuần từ hoạt động khác |
| 12 | Thu nhập từ góp vốn, mua cổ phần |
| 13 | EPS |
| 14 | Lợi nhuận sau thuế |
| 15 | Chi phí hoạt động |
| 16 | Kinh doanh |
| 17 | Đầu tư |
| 18 | Tài chính |
| 19 | Lưu chuyển tiền thuần trong năm |
Data.frame: tạo một bảng dữ liệu mới (dạng data frame) từ các vector.
Kiểm tra dữ liệu bị thiếu và trùng lặp
# Kiểm tra giá trị bị thiếu
cat("Giá trị bị thiếu :", sum(is.na(bctc)), "\n")
## Giá trị bị thiếu : 0
# Số dòng trùng lặp
n_dup_rows <- sum(duplicated(bctc))
cat("\nSố dòng trùng lặp:", n_dup_rows, "\n")
##
## Số dòng trùng lặp: 0
# --- Chuyển Year sang factor ---
bctc <- bctc %>%
mutate(Year = as.factor(Year))
# --- Tạo bảng kiểu dữ liệu ---
bctc_types <- tibble(
STT = seq_along(names(bctc)),
Ten_bien = names(bctc),
Kieu_du_lieu = sapply(bctc, function(x) class(x)[1])
)
# --- Xuất bảng đẹp chuẩn tiểu luận A4 ---
bctc_types %>%
kbl(
caption = "Kiểu dữ liệu trong bộ biến nghiên cứu",
booktabs = TRUE,
align = c("c", "l", "c")
) %>%
kable_paper(full_width = FALSE) %>%
column_spec(1, width = "1.2cm") %>%
column_spec(2, width = "6cm") %>%
column_spec(3, width = "2.5cm") %>%
kable_styling(
latex_options = c("hold_position", "striped"),
font_size = 11
)
| STT | Ten_bien | Kieu_du_lieu |
|---|---|---|
| 1 | Year | factor |
| 2 | Tổng nợ | numeric |
| 3 | VCSH | numeric |
| 4 | Tổng tài sản | numeric |
| 5 | Cho vay khách hàng | numeric |
| 6 | Tiền gửi khách hàng | numeric |
| 7 | Thu nhập lãi thuần | numeric |
| 8 | Lãi thuần từ hoạt động dịch vụ | numeric |
| 9 | Lãi thuần từ kinh doanh ngoại hối và vàng | numeric |
| 10 | Lãi thuần từ mua bán chứng khoán kinh doanh, chứng khoán đầu tư và góp vốn đầu tư dài hạn | numeric |
| 11 | Thu nhập thuần từ hoạt động khác | numeric |
| 12 | Thu nhập từ góp vốn, mua cổ phần | numeric |
| 13 | EPS | numeric |
| 14 | Lợi nhuận sau thuế | numeric |
| 15 | Chi phí hoạt động | numeric |
| 16 | Kinh doanh | numeric |
| 17 | Đầu tư | numeric |
| 18 | Tài chính | numeric |
| 19 | Lưu chuyển tiền thuần trong năm | numeric |
# Tạo bảng dữ liệu
goi_y_bien <- data.frame(
Nhóm = c("Quy mô", "Hiệu quả hoạt động", "Dòng tiền", "Tín dụng (nếu là ngân hàng)"),
`Biến đề xuất` = c(
"Tổng tài sản, Tổng nợ, VCSH",
"Lợi nhuận sau thuế, Chi phí hoạt động, EPS",
"Lưu chuyển tiền thuần trong năm",
"Cho vay khách hàng, Tiền gửi khách hàng"
),
`Ý nghĩa` = c(
"Phản ánh quy mô và cấu trúc tài chính",
"Đánh giá khả năng sinh lời",
"Thể hiện khả năng tạo dòng tiền",
"Liên quan đến hoạt động cốt lõi"
)
)
goi_y_bien %>%
kbl(
caption = "Ý nghĩa các biến",
align = "c",
booktabs = TRUE
) %>%
kable_paper(full_width = FALSE, lightable_options = "striped") %>%
kable_styling(
position = "center",
font_size = 11,
latex_options = c("hold_position", "scale_down")
) %>%
column_spec(1, width = "4cm",
extra_css = "word-wrap:break-word; white-space:pre-wrap;") %>%
column_spec(2, width = "10cm",
extra_css = "word-wrap:break-word; white-space:pre-wrap;")
| Nhóm | Biến.đề.xuất | Ý.nghĩa |
|---|---|---|
| Quy mô | Tổng tài sản, Tổng nợ, VCSH | Phản ánh quy mô và cấu trúc tài chính |
| Hiệu quả hoạt động | Lợi nhuận sau thuế, Chi phí hoạt động, EPS | Đánh giá khả năng sinh lời |
| Dòng tiền | Lưu chuyển tiền thuần trong năm | Thể hiện khả năng tạo dòng tiền |
| Tín dụng (nếu là ngân hàng) | Cho vay khách hàng, Tiền gửi khách hàng | Liên quan đến hoạt động cốt lõi |
# ============================================================
# HÀM DỊCH TIẾNG VIỆT → TIẾNG ANH
# ============================================================
translate_vi_en <- function(texts) {
sapply(texts, function(txt) {
txt <- trimws(as.character(txt))
if (txt == "" || is.na(txt)) return(NA_character_)
txt_enc <- URLencode(txt, reserved = TRUE)
url <- paste0(
"https://translate.googleapis.com/translate_a/single?client=gtx&sl=vi&tl=en&dt=t&q=",
txt_enc
)
res <- tryCatch(jsonlite::fromJSON(url), error = function(e) NULL)
if (is.null(res)) return(NA_character_)
out <- tryCatch(res[[1]][[1]][[1]], error = function(e) NA_character_)
if (!is.character(out)) return(NA_character_)
gsub("\n", " ", trimws(out))
}, USE.NAMES = FALSE)
}
# ============================================================
# HÀM TẠO VIẾT TẮT (SHORT NAME)
# ============================================================
make_abbrev <- function(eng_name) {
if (is.na(eng_name) || eng_name == "") return(NA_character_)
words <- unlist(strsplit(eng_name, "\\s+"))
words <- gsub("[^A-Za-z]", "", words)
words <- words[words != ""]
if (length(words) == 0) return(NA_character_)
abbrev <- paste0(toupper(substr(words, 1, 1)), collapse = "")
substr(abbrev, 1, 4)
}
# ============================================================
# TẠO BẢNG BIẾN
# ============================================================
if (!exists("bctc")) stop("Chưa có dữ liệu 'bctc' — hãy load dữ liệu trước!")
cols_vi <- names(bctc)
skip_translate <- c("Kinh doanh", "Đầu tư", "Tài chính")
cols_en <- sapply(cols_vi, function(x) {
if (x %in% skip_translate) return(x)
translate_vi_en(x)
}, USE.NAMES = FALSE)
cols_short <- sapply(cols_en, make_abbrev)
cols_en[is.na(cols_en) | cols_en == ""] <- cols_vi
cols_short[is.na(cols_short) | cols_short == ""] <- cols_vi
vars_map <- data.frame(
Vietnamese = cols_vi,
English = cols_en,
ShortName = cols_short,
stringsAsFactors = FALSE
)
rownames(vars_map) <- seq_len(nrow(vars_map))
names(bctc) <- vars_map$ShortName # Đổi tên biến trong dataset
# ============================================================
# HIỂN THỊ BẢNG ĐẸP CHO PDF (FULL VIỀN, KHÔNG LỖI)
# ============================================================
vars_map %>%
select(Vietnamese, English, ShortName) %>% # KHÓA 3 CỘT RÕ RÀNG
kbl(
caption = "Danh sách biến, bản dịch tiếng Anh và viết tắt",
booktabs = TRUE,
align = "lll", # <<< CHỈ ĐỂ "lll", KHÔNG DÙNG c("l","l","c")
escape = TRUE, # <<< TRÁNH LỖI & % _ trong PDF
longtable = TRUE
) %>%
kable_styling(
latex_options = c("hold_position", "repeat_header", "bordered"),
font_size = 10,
position = "center"
) %>%
column_spec(1, width = "6cm") %>%
column_spec(2, width = "6cm") %>%
column_spec(3, width = "2.5cm", bold = TRUE)
| Vietnamese | English | ShortName |
|---|---|---|
| Year | Year | Y |
| Tổng nợ | Total debt | TD |
| VCSH | VCSH | V |
| Tổng tài sản | Total assets | TA |
| Cho vay khách hàng | Loans to customers | LTC |
| Tiền gửi khách hàng | Customer deposits | CD |
| Thu nhập lãi thuần | Net interest income | NII |
| Lãi thuần từ hoạt động dịch vụ | Net profit from service activities | NPFS |
| Lãi thuần từ kinh doanh ngoại hối và vàng | Net profit from foreign exchange and gold trading | NPFF |
| Lãi thuần từ mua bán chứng khoán kinh doanh, chứng khoán đầu tư và góp vốn đầu tư dài hạn | Net profit from trading of business securities, investment securities and long-term investment capital contributions | NPFT |
| Thu nhập thuần từ hoạt động khác | Net income from other activities | NIFO |
| Thu nhập từ góp vốn, mua cổ phần | Income from capital contribution and share purchase | IFCC |
| EPS | EPS | E |
| Lợi nhuận sau thuế | Profit after tax | PAT |
| Chi phí hoạt động | Operating expenses | OE |
| Kinh doanh | Kinh doanh | KD |
| Đầu tư | Đầu tư | UT |
| Tài chính | Tài chính | TC |
| Lưu chuyển tiền thuần trong năm | Net cash flow during the year | NCFD |
translate_vi_en() Dịch tiếng Việt sang tiếng Anh tự
động
make_abbrev() Tạo tên viết tắt từ tên tiếng Anh
# D/E (Tỷ lệ nợ / vốn chủ sở hữu)
bctc <- bctc %>%
mutate(DE = TD / V)
# ROE (Lợi nhuận sau thuế / VCSH)
bctc <- bctc %>%
mutate(ROE = PAT / V)
# ROA (Lợi nhuận sau thuế / Tổng tài sản)
bctc <- bctc %>%
mutate(ROA = PAT / V)
# NIM (Thu nhập lãi thuần / Cho vay khách hàng)
bctc <- bctc %>%
mutate(NIM = NII / LTC)
# Làm tròn 3 chữ số
bctc <- bctc %>%
mutate(across(c(DE, ROE, ROA, NIM), ~round(., 3)))
# Hiển thị kết quả
bctc %>%
select(Y,DE, ROE, ROA, NIM) %>%
knitr::kable(caption = "Bảng kết quả") %>%
kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed"))
| Y | DE | ROE | ROA | NIM |
|---|---|---|---|---|
| 2014 | 10.692 | 0.146 | 0.146 | 0.072 |
| 2015 | 8.535 | 0.108 | 0.108 | 0.061 |
| 2016 | 8.638 | 0.108 | 0.108 | 0.054 |
| 2017 | 9.604 | 0.118 | 0.118 | 0.062 |
| 2018 | 9.603 | 0.181 | 0.181 | 0.534 |
| 2019 | 84.531 | 0.202 | 0.202 | 0.008 |
| 2020 | 8.880 | 0.172 | 0.172 | 0.069 |
| 2021 | 8.716 | 0.212 | 0.212 | 0.074 |
| 2022 | 8.151 | 0.228 | 0.228 | 0.080 |
| 2023 | 8.771 | 0.218 | 0.218 | 0.065 |
| 2024 | 8.643 | 0.196 | 0.196 | 0.054 |
Phân tích xu hướng hiệu quả hoạt động và rủi ro tài chính của Ngân
hàng MB (MB Bank) giai đoạn 2013–2024 nhằm đánh giá tình hình phát triển
bền vững của ngân hàng qua thời gian.
Cụ thể, mục tiêu của phần này là:
# Mô tả thống kê cơ bản của chỉ số D/E
de_summary <- bctc %>%
summarise(
Min = min(DE),
Max = max(DE),
Mean = mean(DE),
Median = median(DE),
SD = sd(DE),
CV = sd(DE)/mean(DE)
)
# Hiển thị bảng kết quả
de_summary %>%
kable(caption = "Thống kê mô tả chỉ số D/E") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed"))
| Min | Max | Mean | Median | SD | CV |
|---|---|---|---|---|---|
| 8.151 | 84.531 | 15.88764 | 8.771 | 22.77725 | 1.433646 |
summarise() (hoặc summarize()): dùng để tính toán tổng hợp các giá trị theo cột, trả về 1 hàng chứa kết quả.
# Biểu đồ xu hướng D/E qua các năm
ggplot(bctc, aes(x = Y, y = DE)) +
geom_line(color = "#2E86C1", linewidth = 1.2) + # Layer 1
geom_point(color = "#E74C3C", size = 3) + # Layer 2
geom_smooth(method = "lm", se = FALSE, color = "gray40") +# Layer 3
geom_text(aes(label = round(DE,2)), vjust = -0.6, size=3) + # Layer 4
labs(title = "Xu hướng tỷ lệ D/E MB Bank (2014–2024)",
subtitle = "Đánh giá xu hướng đòn bẩy tài chính theo thời gian",
x = "Năm", y = "Tỷ lệ D/E") + # Layer 5
theme_minimal(base_size = 13) # Layer 6
Giải thích cấu trúc biểu đồ -
geom_line() đường nối xu hướng qua các năm.
- geom_point() điểm đánh dấu từng năm.
- geom_text() hiển thị nhãn giá trị D/E trên mỗi
điểm.
- geom_smooth() đường hồi quy mượt để nhận biết xu
hướng chung.
- theme_minimal() + labs() bố cục và tiêu đề giúp biểu
đồ rõ ràng, chuyên nghiệp
Nhận xét
- Tỷ lệ D/E của MB Bank duy trì ổn định quanh mức 8–10
lần trong hầu hết các năm, thể hiện cấu trúc vốn an toàn và
chính sách quản lý nợ hợp lý.
- Năm 2019, tỷ lệ D/E tăng đột biến lên 84,53 lần, cho
thấy ngân hàng có thể đã tăng vay nợ ngắn hạn hoặc thay đổi cơ cấu vốn
để đáp ứng nhu cầu vốn tạm thời — đây là điểm bất thường cần được kiểm
tra thêm.
- Sau năm 2019, tỷ lệ D/E giảm mạnh trở lại mức dưới 9 lần và ổn định
dần đến 2024, phản ánh xu hướng kiểm soát nợ tốt hơn, hướng đến an toàn
tài chính và giảm rủi ro đòn bẩy.
# Phân nhóm rủi ro theo mức D/E
bctc <- bctc %>%
mutate(Nhom_rui_ro = case_when(
DE < 5 ~ "Thấp",
DE >= 5 & DE < 10 ~ "Trung bình",
DE >= 10 ~ "Cao"
))
# Biểu đồ cột thể hiện phân nhóm rủi ro
ggplot(bctc, aes(x = factor(Y), fill = Nhom_rui_ro)) +
geom_bar() + # Layer 1
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.3) + # Layer 2
scale_fill_manual(values = c("#2ECC71", "#F5B041", "#E74C3C")) + # Layer 3
labs(title = "Phân nhóm rủi ro tài chính theo năm",
subtitle = "Dựa trên ngưỡng tỷ lệ D/E",
x = "Năm", y = "Số lượng năm thuộc nhóm") + # Layer 4
theme_minimal(base_size = 13) # Layer 5
Giải thích cấu trúc biểu đồ
- geom_bar(): tạo các cột thể hiện số lượng năm theo
từng nhóm rủi ro.
- geom_text(): hiển thị số lượng (count) trên đầu mỗi
cột.
- scale_fill_manual(): tùy chỉnh màu sắc cho 3 nhóm rủi
ro (xanh – thấp, vàng – trung bình, đỏ – cao).
- labs(): thêm tiêu đề và nhãn trục để biểu đồ rõ
ràng.
Nhận xét biểu đồ
Ba nhóm rủi ro được xác định như sau:
- Nhóm Thấp: D/E < 5
- Nhóm Trung bình: 5 ≤ D/E < 10
- Nhóm Cao: D/E ≥ 10
Diễn giải kết quả:
Giai đoạn 2014–2018: Tất cả các năm đều thuộc nhóm rủi ro trung
bình (màu vàng cam).
→ MB Bank duy trì được cơ cấu tài chính khá ổn định, đòn bẩy ở mức hợp
lý, đảm bảo khả năng sinh lời mà không quá phụ thuộc vào nợ vay.
Năm 2019:
Biểu đồ xuất hiện một cột màu xanh lá (nhóm rủi ro cao).
→ Đây là năm duy nhất có tỷ lệ D/E vượt ngưỡng 10, phản ánh rủi ro tài
chính tăng mạnh — có thể do gia tăng nợ hoặc thay đổi cơ cấu vốn đột
ngột.
Giai đoạn 2020–2024:
MB Bank quay trở lại nhóm trung bình (màu vàng cam), chứng tỏ ngân hàng
đã kịp thời điều chỉnh chiến lược tài chính, giúp giảm rủi ro và cân
bằng đòn bẩy.
Là một chỉ báo mô tả sự phân bố và xu hướng biến động của mức độ rủi ro
tài chính giữa các doanh nghiệp theo từng giai đoạn, qua đó giúp đánh
giá sự ổn định và bền vững trong cấu trúc tài chính.
# Tỷ trọng từng nhóm rủi ro qua thời gian
ty_trong <- bctc %>%
group_by(Nhom_rui_ro) %>%
summarise(Ty_trong = n() / nrow(bctc))
# Biểu đồ tròn thể hiện tỷ trọng nhóm rủi ro
ggplot(ty_trong, aes(x = "", y = Ty_trong, fill = Nhom_rui_ro)) +
geom_bar(stat = "identity", width = 1, color = "white") + # Layer 1 # Vẽ cột tỷ trọng (dạng bar) # Chuyển bar chart thành biểu đồ tròn
coord_polar("y", start = 0) + # Layer 2
geom_text(aes(label = scales::percent(Ty_trong)),
# Hiển thị nhãn phần trăm trên lát cắt
position = position_stack(vjust = 0.5)) + # Layer 3
scale_fill_manual(values = c("#2ECC71", "#F5B041", "#E74C3C")) + # Layer 4
labs(title = "Tỷ trọng các nhóm rủi ro tài chính (2014–2024)",
fill = "Mức rủi ro") + # Layer 5
theme_void(base_size = 13)
Giải thích cấu trúc biểu đồ
- geom_bar(): tạo các thanh biểu diễn tỷ trọng từng
nhóm rủi ro.
- coord_polar(“y”): chuyển biểu đồ thanh thành biểu đồ
tròn (pie chart).
- geom_text(): thêm nhãn phần trăm trực tiếp trên từng
phần của biểu đồ
- scale_fill_manual(): chọn màu sắc riêng cho từng nhóm
rủi ro (cao, trung bình, thấp).
Nhận xét biểu đồ
Kết quả biểu đồ cho thấy, trong giai đoạn 2013–2024,
nhóm rủi ro trung bình chiếm tỷ trọng áp đảo (82%), trong khi nhóm rủi
ro cao chỉ chiếm 18%.
-> Điều này phản ánh rằng phần lớn các doanh nghiệp duy trì cấu trúc
tài chính ở mức an toàn tương đối, không quá phụ thuộc vào đòn bẩy nợ.
Tuy nhiên, vẫn tồn tại một tỷ lệ nhất định các doanh nghiệp có rủi ro
cao, cho thấy sự phân hóa trong khả năng quản trị nợ và sử dụng vốn vay
giữa các đơn vị trong mẫu nghiên cứu.
Biểu đồ mật độ được sử dụng để quan sát phân bố xác suất của tỷ lệ nợ
trên vốn chủ sở hữu (D/E) — đại diện cho mức độ rủi ro tài chính hoặc
đòn bẩy của ngân hàng.
Mục tiêu chính:
- Giúp nhận diện hình dạng phân bố (chuẩn, lệch phải, lệch
trái,…).
- Đánh giá xem có tồn tại các giá trị cực đoan (outliers) trong cấu trúc
vốn hay không.
- Hỗ trợ nhà phân tích xác định mức D/E phổ biến nhất và vị trí trung
bình so với toàn bộ giai đoạn.
- Là cơ sở cho phân nhóm rủi ro tài chính trong bước tiếp theo.
# Phân bố D/E (Density Plot)
ggplot(bctc, aes(x = DE)) +
geom_density(fill = "#5DADE2", alpha = 0.6) + # Layer 1
geom_vline(aes(xintercept = mean(DE)), color = "red", linetype = "dashed") + # Layer 2
geom_rug(sides = "b", color = "gray30") + # Layer 3
labs(title = "Phân bố tỷ lệ D/E của MB Bank",
subtitle = "Phân tích mật độ xác suất của đòn bẩy tài chính",
x = "Tỷ lệ D/E", y = "Mật độ") + # Layer 4
annotate("text", x = mean(bctc$DE), y = 0.02,
label = paste0("Mean = ", round(mean(bctc$DE),2)), color = "red", vjust = -1) + # Layer 5
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- Ggeom_density(): vẽ đường cong mật độ thể hiện xác
suất phân bố của tỷ lệ D/E.
- geom_vline(): thêm đường dọc biểu thị giá trị trung
bình (Mean).
- geom_rug(): chèn các “vạch nhỏ” ở trục x để minh họa
từng điểm dữ liệu thực tế.
- annotate(): hiển thị nhãn “Mean = …” ngay trên biểu
đồ để người đọc dễ nhận biết.
Nhận xét biểu đồ
Phân bố tỷ lệ D/E của MB Bank cho thấy dữ liệu lệch phải mạnh
(right-skewed), tức có một số năm ghi nhận đòn bẩy tài chính rất cao so
với mặt bằng chung. Giá trị trung bình D/E ≈ 15.89,
trong khi phần lớn quan sát tập trung quanh 8–10 lần, phản ánh sự biến
động lớn trong cấu trúc vốn giữa các năm.
Điều này cho thấy MB Bank đã có giai đoạn tăng cường sử dụng nợ để mở
rộng hoạt động, nhưng nhìn chung mức trung bình vẫn trong phạm vi kiểm
soát.
# Tính thống kê mô tả cho ROE, ROA, NIM
desc_eff <- bctc %>%
summarise(across(c(ROE, ROA, NIM),
list(min = min, mean = mean, median = median,
max = max, sd = sd), .names = "{.col}_{.fn}"))
# Chuyển định dạng để có hàng là biến, cột là chỉ tiêu
desc_eff_clean <- desc_eff %>%
pivot_longer(everything(),
names_to = c("Biến", "Chỉ_tiêu"),
names_sep = "_") %>%
pivot_wider(names_from = Chỉ_tiêu, values_from = value)
# Hiển thị bảng đẹp
desc_eff_clean %>%
knitr::kable(caption = "Thống kê mô tả ROE, ROA, NIM (2014–2024)") %>%
kable_styling(full_width = FALSE,
bootstrap_options = c("striped", "hover"))
| Biến | min | mean | median | max | sd |
|---|---|---|---|---|---|
| ROE | 0.108 | 0.1717273 | 0.181 | 0.228 | 0.0449357 |
| ROA | 0.108 | 0.1717273 | 0.181 | 0.228 | 0.0449357 |
| NIM | 0.008 | 0.1030000 | 0.065 | 0.534 | 0.1442096 |
Giải thích
- summarise(across(…)): Tính các đặc trưng thống kê
(Min, Mean, Median, Max, SD) cho từng biến ROE, ROA, NIM.
- pivot_longer(): Biến bảng ngang thành dọc để trình
bày gọn.
- kable() + kable_styling(): Tạo bảng chuyên nghiệp
trong R Markdown.
Ý nghĩa phân tích
Bảng mô tả giúp đánh giá hiệu quả sinh lời (ROE, ROA) và biên lợi nhuận
(NIM) qua thời gian.
- ROE/ROA trung bình phản ánh hiệu quả vốn và tài sản.
- NIM thể hiện khả năng quản lý lãi suất đầu ra – đầu vào.
- Độ lệch chuẩn (SD) nhỏ cho thấy biến động thấp, ổn định tài chính
cao.
# So sánh trung bình trước và sau 2020
bctc$Y <- as.numeric(as.character(bctc$Y))
eff_compare <- bctc %>%
mutate(GiaiDoan = ifelse(Y < 2020, "Trước 2020", "Sau 2020")) %>%
group_by(GiaiDoan) %>%
summarise(across(c(ROE, ROA, NIM), mean, .names = "mean_{.col}"))
eff_compare_long <- eff_compare %>%
pivot_longer(cols = starts_with("mean"), names_to = "Chỉ_số", values_to = "Giá_trị")
ggplot(eff_compare_long, aes(x = GiaiDoan, y = Giá_trị, fill = Chỉ_số)) +
geom_col(position = "dodge", width = 0.6) + # Layer 1
geom_text(aes(label = round(Giá_trị,3)),
position = position_dodge(0.6), vjust = -0.5, size=3) + # Layer 2
scale_fill_brewer(palette = "Set2") + # Layer 3
labs(title = "So sánh hiệu quả hoạt động MB Bank trước và sau 2020",
subtitle = "Trung bình các chỉ số ROE, ROA, NIM",
x = "Giai đoạn", y = "Giá trị trung bình") + # Layer 4
theme_minimal(base_size = 13) # Layer 5
Giải thích cấu trúc biểu đồ
- Trục hoành (x – Giai đoạn): chia MB Bank thành 2 giai đoạn — Trước
2020 và Sau 2020.
- Trục tung (y – Giá trị trung bình): thể hiện giá trị trung bình của
từng chỉ số hiệu quả tài chính.
- Các cột (geom_col): chiều cao thể hiện mức trung bình của từng chỉ
tiêu trong mỗi giai đoạn.
- Nhãn (geom_text): hiển thị giá trị trung bình cụ thể trên mỗi
cột.
Nhận xét biểu đồ
- Biểu đồ giúp so sánh trực quan hiệu quả hoạt động của MB Bank trước và
sau năm 2020 — thời điểm bắt đầu chịu ảnh hưởng mạnh của COVID-19 và
giai đoạn đẩy mạnh chuyển đổi số.
- Nếu ROE và ROA sau 2020 tăng → MB Bank nâng cao hiệu quả sử dụng vốn
và tài sản, có thể do tối ưu chi phí và tăng trưởng tín dụng.
- Nếu NIM giảm sau 2020 → phản ánh áp lực giảm biên lợi nhuận do lãi
suất thị trường hoặc chính sách điều tiết.
-> Tổng thể, biểu đồ hỗ trợ đánh giá xu hướng cải thiện hay suy giảm
hiệu quả sinh lời giữa hai giai đoạn, giúp làm rõ tác động của môi
trường kinh tế và chiến lược nội bộ MB Bank.
# Tốc độ tăng trưởng hằng năm
# Tính tốc độ tăng trưởng theo năm
bctc_growth <- bctc %>%
arrange(Y) %>%
mutate(
ROE_g = (ROE / lag(ROE) - 1) * 100,
ROA_g = (ROA / lag(ROA) - 1) * 100,
NIM_g = (NIM / lag(NIM) - 1) * 100
)
# Thay NA (năm đầu tiên) bằng 0 để biểu đồ liền mạch
bctc_growth <- bctc_growth %>%
mutate(
ROE_g = replace_na(ROE_g, 0),
ROA_g = replace_na(ROA_g, 0),
NIM_g = replace_na(NIM_g, 0)
)
# Chuẩn hóa dữ liệu dạng dài (long format)
growth_long <- bctc_growth %>%
select(Y, ROE_g, ROA_g, NIM_g) %>%
pivot_longer(-Y, names_to = "Chỉ_số", values_to = "Tăng_trưởng")
# Hiển thị bảng tốc độ tăng trưởng
growth_long %>%
kbl(
caption = "Bảng: Tốc độ tăng trưởng hằng năm của ROE, ROA và NIM",
booktabs = TRUE,
align = c("c", "c", "r")
) %>%
kable_paper(full_width = FALSE, lightable_options = "striped") %>%
kable_styling(
latex_options = c("hold_position", "scale_down"),
font_size = 11
)
| Y | Chỉ_số | Tăng_trưởng |
|---|---|---|
| 2014 | ROE_g | 0.000000 |
| 2014 | ROA_g | 0.000000 |
| 2014 | NIM_g | 0.000000 |
| 2015 | ROE_g | -26.027397 |
| 2015 | ROA_g | -26.027397 |
| 2015 | NIM_g | -15.277778 |
| 2016 | ROE_g | 0.000000 |
| 2016 | ROA_g | 0.000000 |
| 2016 | NIM_g | -11.475410 |
| 2017 | ROE_g | 9.259259 |
| 2017 | ROA_g | 9.259259 |
| 2017 | NIM_g | 14.814815 |
| 2018 | ROE_g | 53.389831 |
| 2018 | ROA_g | 53.389831 |
| 2018 | NIM_g | 761.290323 |
| 2019 | ROE_g | 11.602210 |
| 2019 | ROA_g | 11.602210 |
| 2019 | NIM_g | -98.501873 |
| 2020 | ROE_g | -14.851485 |
| 2020 | ROA_g | -14.851485 |
| 2020 | NIM_g | 762.500000 |
| 2021 | ROE_g | 23.255814 |
| 2021 | ROA_g | 23.255814 |
| 2021 | NIM_g | 7.246377 |
| 2022 | ROE_g | 7.547170 |
| 2022 | ROA_g | 7.547170 |
| 2022 | NIM_g | 8.108108 |
| 2023 | ROE_g | -4.385965 |
| 2023 | ROA_g | -4.385965 |
| 2023 | NIM_g | -18.750000 |
| 2024 | ROE_g | -10.091743 |
| 2024 | ROA_g | -10.091743 |
| 2024 | NIM_g | -16.923077 |
Giải thích - lag(ROE) → giá trị ROE
của năm trước.
- (ROE / lag(ROE) - 1) * 100 → % tăng/giảm của ROE so
với năm trước.
- replace_na(…, 0) → thay NA bằng 0, để biểu đồ hoặc
bảng hiển thị liên tục, không đứt quãng.
- select(Y, ROE_g, ROA_g, NIM_g) → chọn 4 cột cần
dùng.
- pivot_longer(-Y, …) → chuyển dữ liệu từ dạng rộng
(wide) sang dài (long)
- kbl() → Tạo bảng LaTeX hoặc HTML từ dữ liệu (thuộc
knitr).
- kable_paper() → làm bảng đẹp hơn, dễ đọc, có hiệu ứng
“striped”.
- kable_styling() → điều chỉnh kiểu hiển thị cho phù
hợp khổ A4, cỡ chữ vừa phải.
# Vẽ biểu đồ tăng trưởng hằng năm
ggplot(growth_long, aes(x = Y, y = Tăng_trưởng, color = Chỉ_số)) +
geom_line(linewidth = 1.1, na.rm = TRUE) + # Layer 1
geom_point(size = 2.5, na.rm = TRUE) + # Layer 2
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") + # Layer 3
geom_text(
aes(label = round(Tăng_trưởng, 1)),
vjust = -0.6, size = 3, na.rm = TRUE
) + # Layer 4
labs(
title = "Tốc độ tăng trưởng hằng năm của ROE, ROA, NIM",
subtitle = "Tỷ lệ % thay đổi so với năm trước",
x = "Năm", y = "Tăng trưởng (%)",
caption = "Lưu ý: Năm đầu tiên được quy ước tăng trưởng = 0 do không có dữ liệu năm trước."
) + # Layer 5
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- Trục hoành (x – Năm): thể hiện các năm trong giai đoạn
2014–2024.
- Trục tung (y – Tăng trưởng %): biểu thị tốc độ tăng trưởng hằng năm
của từng chỉ số, được tính theo phần trăm thay đổi so với năm
trước.
- Đường đứt (y=0): đường tham chiếu — giá trị trên 0 thể hiện tăng
trưởng dương, dưới 0 là suy giảm.
- Các điểm dữ liệu & nhãn giá trị: cho thấy mức tăng/giảm cụ thể
từng năm, giúp dễ dàng nhận biết các biến động lớn.
Nhận xét biểu đồ
- Biểu đồ cho thấy tốc độ tăng trưởng của các chỉ số tài chính biến động
mạnh qua thời gian, đặc biệt là NIM có hai đột biến lớn (trên 700%) vào
các năm 2018 và 2020 — có thể do sự thay đổi chính sách tín dụng hoặc cơ
cấu tài sản sinh lãi.
- Trong khi đó, ROE và ROA duy trì mức biến động nhẹ, phản ánh hiệu quả
sinh lời của MB Bank ổn định hơn và ít chịu tác động ngắn hạn.
- Giai đoạn sau 2020, các chỉ số dần hội tụ về mức tăng trưởng thấp và
ổn định, cho thấy doanh nghiệp đã đi vào giai đoạn ổn định tài chính sau
giai đoạn điều chỉnh mạnh.
- Biểu đồ giúp làm rõ xu hướng biến động hiệu quả hoạt động tài chính
qua các năm, từ đó đánh giá tính bền vững và khả năng thích ứng của MB
Bank trước thay đổi kinh tế.
Biểu đồ này giúp nhận diện mức độ ổn định trong hiệu quả hoạt động của
MB Bank, cho thấy ROE và ROA duy trì ổn định hơn, trong khi NIM có mức
dao động lớn hơn qua các năm.
# So sánh biến động qua Boxplot
eff_long <- bctc %>%
select(Y, ROE, ROA, NIM) %>%
pivot_longer(-Y, names_to = "Chỉ_số", values_to = "Giá_trị")
ggplot(eff_long, aes(x = Chỉ_số, y = Giá_trị, fill = Chỉ_số)) +
geom_boxplot(alpha = 0.7, width = 0.5) + # Layer 1
geom_jitter(color = "gray40", width = 0.1, alpha = 0.8) + # Layer 2
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") + # Layer 3
labs(title = "Phân bố biến động của ROE, ROA, NIM (2013–2024)",
subtitle = "Mức dao động thể hiện sự ổn định của hiệu quả hoạt động",
x = "Chỉ số", y = "Giá trị") + # Layer 4
theme_minimal(base_size = 13) + # Layer 5
scale_fill_brewer(palette = "Set3")
Giải thích cấu trúc biểu đồ
- Trục hoành (x – Chỉ số): gồm ba chỉ tiêu thể hiện
hiệu quả hoạt động: ROE, ROA, NIM.
- Trục tung (y – Giá trị): biểu thị mức độ của từng chỉ
số trong giai đoạn 2013–2024.
- Các hộp (Boxplot): thể hiện phân bố dữ liệu của mỗi
chỉ số
- Đường giữa hộp là trung vị (median).
- Chiều cao của hộp (khoảng tứ phân vị) thể hiện mức độ
dao động của dữ liệu.
- Các chấm rờ là ngoại lệ (outliers) – giá trị bất
thường hoặc đột biến.
- Màu sắc: mỗi chỉ số có màu khác nhau giúp dễ phân
biệt mức ổn định tương đối.
Nhận xét biểu đồ
- ROE và ROA có phân bố khá tương đồng, với mức trung vị cao và biên dao
động hẹp, phản ánh hiệu quả sinh lời ổn định và ít biến động qua các
năm.
- NIM có trung vị thấp hơn và xuất hiện một số điểm ngoại lệ lớn, cho
thấy biên lãi ròng biến động mạnh hơn, có thể do tác động từ biến động
lãi suất hoặc chính sách tín dụng.
- Nhìn chung, MB Bank duy trì hiệu quả hoạt động ổn định, đặc biệt ở ROE
và ROA, trong khi NIM thể hiện tính nhạy cảm cao hơn với điều kiện thị
trường.
Độ ổn định phản ánh mức độ dao động của hiệu quả hoạt động qua thời
gian.
Chỉ số này thường được đo bằng hệ số biến thiên (CV = độ lệch chuẩn /
giá trị trung bình):
- CV càng nhỏ → hiệu quả hoạt động càng ổn định, ít biến động.
- CV càng lớn → hiệu quả biến động mạnh, rủi ro cao hơn.
# Độ ổn định (Hệ số biến thiên CV = sd/mean) ------------------------------
cv_eff <- bctc %>%
summarise(across(c(ROE, ROA, NIM),
list(CV = ~sd(.)/mean(.)), .names = "{.col}_{.fn}")) %>%
pivot_longer(everything(), names_to = "Chỉ_số", values_to = "CV")
ggplot(cv_eff, aes(x = Chỉ_số, y = CV, color = Chỉ_số)) +
geom_segment(aes(x = Chỉ_số, xend = Chỉ_số, y = 0, yend = CV),
linewidth = 1.1) + # Layer 1
geom_point(size = 4) + # Layer 2
geom_text(aes(label = round(CV,3)), vjust = -0.6, size = 3) + # Layer 3
scale_color_brewer(palette = "Dark2") + # Layer 4
labs(title = "Độ ổn định của các chỉ số hiệu quả hoạt động",
subtitle = "Hệ số biến thiên (CV = sd/mean) – CV càng nhỏ, ổn định càng cao",
x = "Chỉ số", y = "CV") + # Layer 5
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- geom_segment(): vẽ đường nối từ trục hoành đến điểm
giá trị CV của từng chỉ số. - geom_point(): đánh dấu
giá trị CV cụ thể cho mỗi chỉ số.
- geom_text(): hiển thị nhãn giá trị CV ngay trên điểm
dữ liệu.
- scale_color_brewer() + theme_minimal(): giúp biểu đồ
có màu sắc hài hòa và bố cục rõ ràng.
Nhận xét biểu đồ
- Trong ba chỉ số, ROE và ROA có CV nhỏ (~0.26) → thể hiện mức độ ổn
định cao, phản ánh khả năng duy trì hiệu quả sinh lời ổn định của ngân
hàng.
- NIM có CV rất lớn (≈ 1.4) → chứng tỏ biên lãi ròng biến động mạnh, có
thể chịu ảnh hưởng từ điều kiện thị trường hoặc chính sách lãi
suất.
-> Như vậy, về tổng thể, MB Bank duy trì hiệu quả sinh lời (ROE, ROA)
ổn định hơn so với biên lãi (NIM) trong giai đoạn 2013–2024.
# Phân tích tương quan
corr_data <- bctc %>%
select(DE, ROE, ROA, NIM)
corr_matrix <- round(cor(corr_data), 3)
ggcorrplot(corr_matrix,
hc.order = TRUE, type = "lower",
lab = TRUE, lab_size = 3,
colors = c("#E74C3C", "white", "#2ECC71"),
title = "Ma trận tương quan giữa D/E và các chỉ số hiệu quả",
ggtheme = theme_minimal())
Giải thích cấu trúc biểu đồ
- ggcorrplot(): hàm dùng để trực quan hóa ma trận tương
quan giữa các biến định lượng.
- type = “lower”: chỉ hiển thị nửa dưới của ma trận để
biểu đồ gọn hơn.
- colors = c(“đỏ”, “trắng”, “xanh lá”): biểu thị hướng
tương quan — đỏ là âm, xanh là dương, trắng là gần bằng 0.
- lab = TRUE: hiển thị hệ số tương quan ngay trong từng
ô để dễ đọc.
Nhận xét biểu đồ
- Hệ số tương quan giữa D/E và ROE, ROA đều dương nhẹ (~0.21) → cho thấy
khi đòn bẩy tài chính tăng, hiệu quả sinh lời có xu hướng tăng nhưng
không mạnh.
- Ngược lại, D/E và NIM có tương quan âm (-0.21) → gợi ý rằng đòn bẩy
cao có thể làm biên lãi ròng giảm, phản ánh chi phí lãi vay hoặc rủi ro
tài chính tăng.
-> Nhìn chung, mức tương quan thấp cho thấy tác động của đòn bẩy tài
chính đến hiệu quả hoạt động chưa rõ rệt, cần kiểm định sâu hơn bằng mô
hình hồi quy.
Mục tiêu: Kiểm định xem đòn bẩy tài chính (DE) có ảnh
hưởng đến khả năng sinh lời (ROE) hay không.
# Hồi qui đơn
# Xây dựng mô hình hồi quy
model1 <- lm(ROE ~ DE, data = bctc)
# Tóm tắt kết quả hồi quy thành bảng
tidy(model1) %>%
mutate(across(where(is.numeric), ~ round(.x, 5))) %>%
kable(
caption = "Kết quả hồi quy đơn: Mối quan hệ giữa ROE và DE",
align = "c"
)
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | 0.16502 | 0.01729 | 9.54685 | 0.00001 |
| DE | 0.00042 | 0.00064 | 0.65723 | 0.52748 |
# Trực quan hóa kết quả hồi quy
suppressMessages(
ggplot(bctc, aes(x = DE, y = ROE)) +
geom_point(aes(size = abs(DE)), color = "#2E86C1", alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE, color = "#E74C3C") +
geom_text(aes(label = Y), vjust = -0.6, size = 3) +
labs(
title = "Mối quan hệ giữa đòn bẩy tài chính (DE) và khả năng sinh lời (ROE)",
subtitle = "Mô hình hồi quy tuyến tính đơn: ROE ~ DE",
x = "Tỷ lệ D/E", y = "ROE (%)"
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none")
)
Mô hình hồi qui
ROE = 0.165 + 0.0002 × DE + ε
- Hệ số chặn (= 0.165): Khi tỷ lệ D/E = 0, ROE dự kiến khoảng 16.5%. Đây
là mức sinh lời cơ bản của ngân hàng khi không sử dụng nợ.
- Hệ số DE (0.00042): Khi D/E tăng thêm 1 đơn vị, ROE tăng nhẹ 0.042%.
Tuy nhiên, giá trị p = 0.527 > 0.05, nên ảnh hưởng này không có ý
nghĩa thống kê → đòn bẩy tài chính chưa chứng minh được làm tăng lợi
nhuận.
Giải thích cấu trúc biểu đồ
- ggplot(bctc, aes(x = DE, y = ROE)): tạo biểu đồ thể
hiện mối quan hệ giữa tỷ lệ nợ (DE) và khả năng sinh lời (ROE).
- geom_point(): vẽ các điểm dữ liệu, kích thước điểm tỉ
lệ với DE, màu xanh thể hiện sự ổn định.
- geom_smooth(method = “lm”, se = TRUE): thêm đường hồi
quy tuyến tính màu đỏ cùng vùng tin cậy 95%.
Nhận xét biểu đồ
Đường hồi quy có xu hướng tăng nhẹ → DE và ROE có quan hệ cùng chiều,
nhưng mức tác động yếu (p-value > 0.05).Điều này cho thấy đòn bẩy tài
chính chưa ảnh hưởng đáng kể đến hiệu quả sinh lời của doanh nghiệp
model2 <- lm(ROE ~ DE + NIM + ROA, data = bctc)
broom::tidy(model2) %>%
mutate(term = ifelse(term == "(Intercept)", "Hằng số", term)) %>%
mutate(Signif = case_when(
p.value < 0.001 ~ "***",
p.value < 0.01 ~ "**",
p.value < 0.05 ~ "*",
p.value < 0.1 ~ ".",
TRUE ~ ""
)) %>%
select(term, estimate, std.error, statistic, p.value, Signif) %>%
knitr::kable(
caption = "Kết quả hồi quy đa biến: ROE ~ DE + NIM + ROA",
digits = 4, align = "c", escape = TRUE
) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
| term | estimate | std.error | statistic | p.value | Signif |
|---|---|---|---|---|---|
| Hằng số | 0 | 0 | 2.2055 | 0.0632 | . |
| DE | 0 | 0 | 0.6519 | 0.5353 | |
| NIM | 0 | 0 | 0.3074 | 0.7675 | |
| ROA | 1 | 0 | 12299894817743720.0000 | 0.0000 | *** |
Giải thích cấu trúc - lm(ROE ~ DE + NIM + ROA): mô
hình hồi quy đa biến, trong đó ROE là biến phụ thuộc, còn DE, NIM, ROA
là các biến độc lập.
- estimate: hệ số ước lượng — cho biết mức thay đổi của ROE khi biến độc
lập tăng 1 đơn vị.
- p.value: giá trị kiểm định ý nghĩa thống kê.
- Signif: ký hiệu mức ý nghĩa (, , ) để dễ
nhận biết biến nào có ảnh hưởng đáng kể.
Kết quả
- ROA có hệ số ước lượng ≈ 1 và p-value = 0.0000 (* )**, chứng tỏ ảnh
hưởng mạnh và có ý nghĩa thống kê đến ROE.
- DE và NIM có p-value > 0.05, nghĩa là không có tác động đáng kể lên
ROE.
-> Mô hình cho thấy ROA là yếu tố quyết định chính của khả năng sinh
lời (ROE), trong khi đòn bẩy tài chính và biên lãi ròng không tạo ra
khác biệt rõ rệt.
glance(model2) %>%
select(r.squared, adj.r.squared, p.value) %>%
knitr::kable(caption = "Tóm tắt mô hình", digits = 4) %>%
kable_styling(full_width = FALSE)
| r.squared | adj.r.squared | p.value |
|---|---|---|
| 1 | 1 | 0 |
# Vẽ hệ số hồi quy
coef_df <- tidy(model2)
ggplot(coef_df, aes(x = reorder(term, estimate), y = estimate, fill = term)) +
geom_col(width = 0.6) + # Layer 1
geom_text(aes(label = round(estimate, 3)), vjust = -0.5) + # Layer 2
geom_hline(yintercept = 0, color = "gray40", linetype = "dashed") + # Layer 3
coord_flip() + # Layer 4
labs(title = "Hệ số hồi quy trong mô hình ROE ~ DE + NIM + ROA",
x = "Biến độc lập", y = "Hệ số ước lượng") + # Layer 5
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- geom_col(): tạo cột biểu diễn hệ số ước lượng của
từng biến trong mô hình hồi quy.
- geom_text(): hiển thị giá trị hệ số ngay trên đầu cột
giúp dễ so sánh.
- geom_hline(yintercept = 0): thêm đường gạch ngang tại
0 để phân biệt hệ số dương – âm.
- coord_flip(): xoay trục để biểu đồ dễ đọc hơn theo
chiều ngang.
- Màu sắc đại diện cho từng biến độc lập (DE, NIM, ROA).
Nhận xét biểu đồ
- ROA nổi bật với hệ số gần 1, cho thấy tác động mạnh và tích cực đến
ROE.
- DE và NIM có hệ số gần 0, nghĩa là ảnh hưởng không đáng kể đến khả
năng sinh lời.
- Biểu đồ trực quan khẳng định lại kết quả hồi quy: ROA là nhân tố chi
phối chính trong mô hình ROE ~ DE + NIM + ROA.
#Biểu đồ phân tán màu theo NIM (Bubble chart)
ggplot(bctc, aes(x = DE, y = ROE)) +
geom_point(aes(size = abs(NIM), color = NIM), alpha = 0.7) + # Layer 1-2
geom_smooth(method = "lm", se = FALSE, color = "gray30") + # Layer 3
scale_color_gradient(low = "#E74C3C", high = "#2ECC71") + # Layer 4
geom_text(aes(label = Y), vjust = -0.6, size = 3) + # Layer 5
labs(title = "Mối quan hệ giữa D/E và ROE (màu theo NIM)",
subtitle = "Kích thước bong bóng thể hiện mức NIM",
x = "Tỷ lệ D/E", y = "ROE (%)") +
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- geom_point(): vẽ các điểm dữ liệu (mỗi năm là một
điểm).
- size = abs(NIM) → kích thước bong bóng thể hiện biên
lợi nhuận NIM.
- color = NIM → màu sắc biểu thị mức NIM (đỏ = thấp,
xanh = cao).
- geom_smooth(method = “lm”): thêm đường xu hướng tuyến
tính giữa DE và ROE.
- scale_color_gradient(): thiết lập dải màu chuyển từ
đỏ → xanh để thể hiện mức độ NIM.
- geom_text(): gắn nhãn năm giúp nhận diện từng điểm dữ
liệu.
Nhận xét biểu đồ
- ROE có xu hướng tăng nhẹ khi DE tăng, nhưng độ dốc không lớn → mối
quan hệ tác động yếu.
- Các điểm có màu xanh đậm (NIM cao) thường nằm ở vùng ROE cao, cho thấy
biên lợi nhuận (NIM) hỗ trợ cải thiện hiệu quả sinh lời.
- NIM đóng vai trò bổ trợ tích cực, trong khi đòn bẩy tài chính (DE) chỉ
có ảnh hưởng hạn chế đến ROE.
#Biểu đồ chú thích hệ số hồi quy
ggplot(bctc, aes(x = DE, y = ROE)) +
geom_point(color = "#2E86C1", size = 3) +
geom_smooth(method = "lm", se = FALSE, color = "#E74C3C") +
annotate("text", x = max(bctc$DE)*0.8, y = max(bctc$ROE),
label = paste0("ROE = ",
round(coef(model1)[1],3), " + ",
round(coef(model1)[2],3),"×DE\n",
"R² = ", round(summary(model1)$r.squared,3)),
hjust = 1, size = 3.5, color = "gray20") +
labs(title = "Phương trình hồi quy ROE ~ DE",
x = "Tỷ lệ D/E", y = "ROE (%)") +
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- Trục hoành (X): Tỷ lệ D/E (Debt to Equity) — mức độ sử dụng đòn bẩy
tài chính.
- Trục tung (Y): ROE (%) — tỷ suất sinh lời trên vốn chủ sở hữu.
- Điểm xanh (geom_point): Đại diện cho giá trị thực tế của từng
năm/doanh nghiệp trong dữ liệu.
- Đường đỏ (geom_smooth, method = “lm”): Đường hồi quy tuyến tính thể
hiện xu hướng chung giữa D/E và ROE.
- Chú thích góc phải: Hiển thị phương trình hồi quy và hệ số R², mô tả
mối quan hệ định lượng giữa hai biến.
- Giao diện (theme_minimal): Tạo bố cục đơn giản, dễ nhìn, tập trung vào
dữ liệu.
Nhận xét biểu đồ
- Độ dốc đường hồi quy rất nhỏ → cho thấy tác động của D/E lên ROE gần
như không đáng kể.
- R² = 0.046 → mô hình chỉ giải thích được 4.6% biến thiên ROE, phần lớn
do các yếu tố khác.
- Điểm dữ liệu phân tán xa khỏi đường hồi quy → mối quan hệ giữa hai
biến yếu và không ổn định.
Kết luận: Doanh nghiệp có tỷ lệ nợ cao không nhất thiết
đạt R OE cao hơn → đòn bẩy tài chính chưa mang lại hiệu quả sinh lời rõ
rệt.
summary_stats <- bctc %>%
summarise(across(c(DE, ROE, ROA, NIM),
list(Min = min, Mean = mean, Max = max),
.names = "{.col}_{.fn}")) %>%
pivot_longer(cols = everything(),
names_to = c("Chỉ tiêu", ".value"),
names_sep = "_") %>%
mutate(across(c(Min, Mean, Max), ~ round(. , 3))) # Làm tròn đẹp
summary_stats %>%
kable(caption = "Bảng: Thống kê mô tả các chỉ tiêu tài chính (2014–2024)",
align = "c",
col.names = c("Chỉ tiêu", "Min", "Mean", "Max")) %>%
kable_styling(full_width = FALSE,
bootstrap_options = c("striped","hover","condensed")) %>%
column_spec(1, bold = TRUE) %>%
row_spec(0, bold = TRUE)
| Chỉ tiêu | Min | Mean | Max |
|---|---|---|---|
| DE | 8.151 | 15.888 | 84.531 |
| ROE | 0.108 | 0.172 | 0.228 |
| ROA | 0.108 | 0.172 | 0.228 |
| NIM | 0.008 | 0.103 | 0.534 |
# =====================================================
# Biểu đồ tốc độ tăng trưởng (%)
# =====================================================
bctc_growth <- bctc %>%
arrange(Y) %>%
mutate(ROE_g = (ROE/lag(ROE) - 1)*100,
ROA_g = (ROA/lag(ROA) - 1)*100,
NIM_g = (NIM/lag(NIM) - 1)*100)
growth_long <- bctc_growth %>%
select(Y, ROE_g, ROA_g, NIM_g) %>%
pivot_longer(-Y, names_to = "ChiSo", values_to = "TangTruong")
ggplot(growth_long, aes(x = Y, y = TangTruong, color = ChiSo)) +
geom_line(linewidth = 1.1) +
geom_point(size = 2.5) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray60") +
labs(title = "Tốc độ tăng trưởng hằng năm của ROE, ROA, NIM",
subtitle = "Tỷ lệ phần trăm thay đổi so với năm trước",
x = "Năm", y = "Tăng trưởng (%)") +
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- Trục X: Năm (Y).
- Trục Y: Tỷ lệ tăng trưởng (%) so với năm trước.
- Đường gạch ngang tại 0%: Mốc phân biệt giữa tăng trưởng dương và
âm.
- geom_line() + geom_point(): Hiển thị xu hướng và các
điểm biến động của từng chỉ tiêu qua thời gian.
Nhận xét biểu đồ
- NIM (biên lãi ròng) biến động mạnh, có các đỉnh tăng đột biến (≈ 2018,
2020) → phản ánh biến động mạnh về lợi nhuận lãi thuần, có thể do thay
đổi trong lãi suất thị trường hoặc cơ cấu cho vay.
- ROE và ROA có xu hướng ổn định hơn, dao động quanh mức tăng trưởng nhỏ
(±20%) → cho thấy hiệu quả sinh lời của doanh nghiệp tương đối bền
vững.
- Sau năm 2020, cả ba chỉ tiêu đều giảm và tiến gần 0, chứng tỏ hiệu quả
sinh lời chững lại trong giai đoạn gần đây.
-> Nhìn chung, mô hình tài chính có xu hướng tăng mạnh trong ngắn hạn
nhưng chưa ổn định dài hạn, đặc biệt chịu ảnh hưởng lớn từ NIM.
# Phân tích tương quan toàn diện (Pair Plot)
suppressMessages({
library(GGally)
bctc %>%
select(DE, ROE, ROA, NIM) %>%
ggpairs(
title = "Phân tích tương quan giữa D/E, ROE, ROA và NIM",
upper = list(continuous = wrap("cor", size = 4)),
lower = list(continuous = wrap("smooth", alpha = 0.4, size = 0.2)),
diag = list(continuous = wrap("densityDiag", alpha = 0.5))
) +
theme_bw(base_size = 11)
})
Giải thích cấu trúc biểu đồ
- Đường chéo (Diagonal): Thể hiện phân phối (density) của từng
biến.
- Phần trên (Upper): Hiển thị hệ số tương quan (Corr) giữa các cặp
biến.
- Hệ số gần 1 → tương quan dương mạnh.
- Gần -1 → tương quan âm mạnh.
- Gần 0 → không có tương quan đáng kể.
- Phần dưới (Lower): Các biểu đồ hồi quy tuyến tính mượt (smooth) thể
hiện xu hướng quan hệ giữa hai biến.
- Màu nền xám và vùng mờ: Biểu thị độ tin cậy của đường hồi quy.
Nhận xét biểu đồ
- ROE và ROA có tương quan dương rất mạnh (≈ 1.000***), cho thấy hai chỉ
số này gần như song hành — khi hiệu quả sinh lời tài sản tăng, hiệu quả
vốn chủ cũng tăng tương ứng.
- D/E (đòn bẩy tài chính) có tương quan dương yếu với ROE và ROA (≈
0.21) → mức nợ cao có thể giúp tăng sinh lời, nhưng chưa rõ rệt.
- NIM (biên lãi ròng) tương quan âm nhẹ với D/E (-0.21) và gần như không
tương quan với ROE, ROA (≈ 0.07) → chứng tỏ biến động lãi suất hoặc chi
phí huy động vốn không ảnh hưởng trực tiếp đến khả năng sinh lời tổng
thể.
Nhìn chung, ROA là biến giải thích chính cho ROE, trong khi D/E và NIM
chỉ đóng vai trò bổ trợ yếu trong mô hình tài chính này.
# Tổng hợp xu hướng 4 chỉ số (Facet Chart)
bctc_long <- bctc %>%
pivot_longer(cols = c(DE, ROE, ROA, NIM),
names_to = "ChiSo", values_to = "GiaTri")
ggplot(bctc_long, aes(x = Y, y = GiaTri, color = ChiSo)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
facet_wrap(~ChiSo, scales = "free_y", ncol = 2) +
labs(title = "Tổng hợp xu hướng chi tiết từng chỉ tiêu",
subtitle = "So sánh biến động D/E, ROE, ROA, NIM theo thời gian",
x = "Năm", y = "Giá trị") +
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- pivot_longer(): chuyển dữ liệu từ dạng rộng (mỗi cột
là một chỉ số) sang dạng dài (một cột “ChiSo”, một cột “GiaTri”).
- facet_wrap(~ChiSo): chia biểu đồ thành 4 ô nhỏ, mỗi ô
thể hiện biến động theo năm của D/E, NIM, ROA, ROE.
- geom_line() + geom_point(): biểu diễn xu hướng và
điểm dữ liệu cụ thể.
- scales = “free_y”: cho phép mỗi chỉ số có trục tung
riêng, giúp nhìn rõ biến động tương đối dù đơn vị khác nhau.
Nhận xét biểu đồ
- D/E có đột biến lớn vào năm 2019, cho thấy ngân hàng tăng cường sử
dụng đòn bẩy tài chính, sau đó giảm mạnh trở lại mức ổn định.
- NIM cũng tăng đột biến cùng năm, có thể do thay đổi chính sách lãi
suất hoặc cơ cấu cho vay – huy động.
- ROA và ROE thể hiện xu hướng đồng biến, cùng tăng mạnh giai đoạn
2018–2021, phản ánh hiệu quả sử dụng vốn và tài sản được cải
thiện.
Sau 2022, các chỉ tiêu đều có xu hướng giảm nhẹ và ổn định, cho thấy
doanh nghiệp bước vào giai đoạn tăng trưởng chậm nhưng bền vững.
Biểu đồ này giúp nhìn tổng quan chu kỳ tài chính, dễ nhận ra năm bất
thường (2019) và mối quan hệ giữa hiệu quả – rủi ro – lợi nhuận
MB Bank là một trong những ngân hàng có hiệu suất tài chính tốt, ít rủi ro, và hiệu quả tăng trưởng ổn định trong thập kỷ qua. Việc duy trì cấu trúc tài chính an toàn cùng khả năng sinh lời bền vững cho thấy năng lực quản trị tài chính và chiến lược phát triển hợp lý của ngân hàng.