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.

CHƯƠNG 1. PHÂN TÍCH MÔ TẢ VÀ KHÁM PHÁ DỮ LIỆU GIÁ XE Ô TÔ CŨ TRÊN THỊ TRƯỜNG QUỐC TÊ

1.1. Giới thiệu dữ liệu và nạp các thư viện cần thiết

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

1.1.1. Đọc và Kiểm tra dữ liệu


Đọ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")
Danh sách biến và kiểu dữ liệu trong bộ dữ liệu
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.

    • Biến số (numeric) → dùng thống kê mô tả, vẽ biểu đồ phân phối
    • Biến phân loại (character) → cần mã hóa, kiểm tra tần suất, v.v.
  • 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")
Các biến có giá trị thiếu và số lượng thiếu
Biến Số giá trị thiếu
condition 11820
odometer 94
mmr 38
sellingprice 12
  • na_table <- df %>% summarise(across(everything(), ~sum(is.na(.))))
    Kỹ thuật:Với dplyr::summarise + across, tính tổng NA cho từng cột (biến).
    Ý Nghĩa:Xác định biến nào có nhiều thiếu nhất — hữu ích để ưu tiên xử lý hoặc xem nguyên nhân thiếu.
  • %>% pivot_longer(cols = everything(), names_to = “Biến”, values_to = “Số NA”)
    Kỹ thuật:Chuyển từ dạng một hàng nhiều cột sang dạng dài (cột Biến / Số NA) bằng pivot_longer.
    Ý Nghĩa:Làm bảng dễ đọc/so sánh giữa các biến, thuận tiện cho báo cáo và lọc.
  • %>% filter(Số NA > 0)
    Kỹ thuật:Lọc chỉ giữ các biến có số NA lớn hơn 0
    Ý Nghĩa:Loại bỏ các biến hoàn chỉnh để tập trung vào biến thực sự cần xử lý.
  • %>% arrange(desc(Số NA))
    Kỹ thuật: Sắp xếp bảng giảm dần theo số NA.
    Ý Nghĩa: Ưu tiên những biến có thiếu nhiều nhất — hướng xử lý hiệu quả (imputation, xóa, thu thập dữ liệu).
  • na_table %>% kable(caption = “…”, col.names = c(“Biến”, “Số giá trị thiếu”))
    Kỹ thuật: Dùng kable() để render bảng HTML/LaTeX với caption và tên cột.
    Ý Nghĩa:Trình bày kết quả rõ ràng cho báo cáo; giúp người đọc nhanh nắm biến cần chú ý.
  • %>% kable_styling(full_width = FALSE, position = “center”)
    Kỹ thuật:Tùy chỉnh kiểu hiển thị bảng (không full width, căn giữa).
    Ý Nghĩa:Cải thiện trực quan khi trình bày báo cáo; không ảnh hưởng tính toán nhưng tăng tính chuyên nghiệp.

1.1.2.Mô tả thống kê sơ bộ

Bảng thống kê mô tả các biến số xe ô tô
Biến Min Mean Median Max
year 1982 2010.039 2012 2015
odometer 1 68320.018 52254 999999
sellingprice 1 13611.359 12100 230000
  • Giải thích câu lệnh
    Trong đoạn mã trên, df là data frame chứa dữ liệu xe. Hàm select() chọn ba biến cần thống kê: year (năm sản xuất), odometer (số km đã đi)sellingprice (giá bán xe). Tiếp theo, summarise(across(…)) được dùng để tính các giá trị Min, Mean, Median và Max cho từng biến, với na.rm = TRUE để bỏ qua giá trị thiếu.
    Hàm pivot_longer() chuyển bảng từ dạng rộng sang dạng dài, giúp trình bày dễ đọc hơn. Tham số names_to = c(“Biến”, “.value”) tách phần tên biến và loại thống kê, còn **names_sep = “_“** xác định dấu gạch dưới làm ký tự phân tách.
    Kết quả là bảng thống kê mô tả gọn gàng, thể hiện các chỉ tiêu Min, Mean, Median và Max cho từng biến trong dữ liệu.

    ### 1.1.3. Kiểm tra số hãng xe
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)

1.2. Xử lý và mã hóa dữ liệu

1.2.1. Loại bỏ dòng có giá trị bị thiếu

# 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

1.2.2. Thêm cột tuổi xe

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
  )

1.2.3. Mã hóa hãng xe thành chữ hoa

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)

1.2.5. Loại bỏ xe chạy trên 400000km

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

1.2.6. Loại bỏ các hàng có ký tự đặc biệt

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

1.2.7. Chuẩn hóa dữ liệu số

df <- df %>%
  mutate(across(c(condition, odometer, sellingprice, mmr), as.numeric))

1.2.8. Chuẩn hóa chữ thường cho cột model

df$model <- tolower(trimws(df$model))

Ví dụ “Civic” và “CIVIC” được coi là một

1.3. Thống kê cơ bản

1.3.1. Thống kê 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
  • summarise(across(all_of(num_vars), list(…)))
    Kỹ thuật:
    • across() áp một danh sách hàm lên mọi biến được liệt kê trong num_vars.
    • Mỗi hàm được định nghĩa dưới dạng công thức ~ func(., na.rm = TRUE) để đảm bảo NA không gây lỗi.
    • Kết quả của summarise() ở đây là một một hàng chứa các cột có tên dạng variable_stat (ví dụ gdp_mean, fdi_sd).
      Ý nghĩa thống kê
    • Đây là bước thu thập tất cả thống kê mô tả cơ bản cho mỗi biến: vị trí (min, median, max), trung tâm (mean, median), và phân tán (sd).
  • pivot_longer(everything(), names_to = “stat”, values_to = “value”)
    Kỹ thuật:
    • Chuyển dữ liệu từ nhiều cột một hàng (wide single-row result) thành dạng dài, mỗi dòng chứa tên cột gốc (stat) và giá trị tương ứng (value).
    • Tạo cấu trúc dễ xử lý để tách tên biến và loại thống kê.
      Ý nghĩa thống kê
    • Dạng dài giúp thao tác chuỗi (vd. tách tên biến và tên thống kê) và chuyển đổi dạng (wide/long) linh hoạt cho báo cáo, in ấn, hoặc xuất sang Excel.
  • **separate(stat, into = c(“variable”, “stat”), sep = “_“)**
    Kỹ thuật:
    • Tách chuỗi variable_stat (ví dụ gdp_mean) thành hai cột: variable và stat, dùng dấu gạch dưới _ làm điểm tách.
    • Yêu cầu tên cột ban đầu phải theo định dạng variable_stat.
      Ý nghĩa thống kê
    • Cho phép nhóm/so sánh thống kê theo biến (vd. tất cả các thống kê cho gdp), hoặc so sánh theo thống kê (vd. mean của tất cả biến).
    • Chuẩn hoá cấu trúc dữ liệu cho báo cáo (mỗi hàng tương ứng 1 biến).
  • pivot_wider(names_from = stat, values_from = value)
    Kỹ thuật:
    • Xoay lại dữ liệu từ dạng dài thành dạng rộng, nhưng lần này mỗi hàng là một biến và mỗi cột là một thống kê (min, median, mean, max, sd).
    • Kết quả dễ đọc cho mục đích báo cáo và so sánh ngang giữa các biến.
      Ý nghĩa thống kê
    • Bảng này là dạng tiêu chuẩn cho “báo cáo thống kê mô tả”: nhanh chóng thấy trung vị, trung bình và độ lệch chuẩn để đánh giá phân bố và độ biến thiên giữa các biến.
      GIẢI THÍCH KẾT QUẢ
      Thống kê mô tả thể hiện các chỉ tiêu cơ bản như giá trị nhỏ nhất (Min), trung bình (Mean), trung vị (Median) và lớn nhất (Max) của các biến định lượng.
    • Biến year cho thấy năm sản xuất của xe dao động trong khoảng từ 1982 đến 2015
    • Biến odometer mô tả quãng đường đã đi, có mức trung bình khoảng 68184.12793 km.
    • Biến sellingprice thể hiện mức giá bán trung bình 13463.13753 USD, phản ánh giá trị phổ biến của xe cũ trên thị trường

1.3.2. Top 20 số lượng mỗi hãng xe phổ biến

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.

1.3.3. Tính correlation matrix cho biến số

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ố")
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
  • num_df <- df %>% select(all_of(num_vars))
    Kỹ thuật: Lấy ra chỉ các biến số (numeric variables) trong dataframe df. num_vars là một vector chứa tên biến số đã xác định trước.
    Ý nghĩa thống kê: Đảm bảo ma trận tương quan chỉ tính trên các biến định lượng (numeric). Tương quan không áp dụng cho biến định tính (categorical).
  • %>% drop_na()
    Kỹ thuật: Loại bỏ các dòng chứa giá trị NA trong bất kỳ biến số nào.
    Ý nghĩa thống kê: Đảm bảo dữ liệu hoàn chỉnh trước khi tính hệ số tương quan. Nếu còn NA, kết quả tương quan có thể bị sai hoặc trả về NA.
  • cor_mat <- cor(num_df, use=“pairwise.complete.obs”)
    Kỹ thuật: Hàm cor() tính ma trận tương quan Pearson giữa các cặp biến. Tham số “pairwise.complete.obs” cho phép tính tương quan theo từng cặp biến dựa trên dữ liệu không bị thiếu (pairwise non-missing).
    Ý nghĩa thống kê: Tạo ra ma trận hệ số tương quan (Correlation Matrix) → thể hiện mức độ và chiều hướng liên hệ tuyến tính giữa từng cặp biến.

1.3.4. Tóm tắt theo nhóm: giá trung bình theo hãng (top 10 theo vol)

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")
  )
Top 10 hãng xe có số lượng lớn nhất cùng giá trung bình và giá trung vị
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
  • group_by(make)
    Kỹ thuật: Gom nhóm theo tên hãng xe.
    Ý nghĩa: Chuẩn bị để tính toán thống kê theo từng hãng xe.
  • summarise(n = n(), …)
    Kỹ thuật: n() đếm số dòng = số lượng xe của hãng.
    Ý nghĩa: Biến n = số lượng xe phản ánh mức độ phổ biến (market presence).
  • mean(sellingprice, na.rm = TRUE)
    Kỹ thuật: Tính giá trung bình bán xe của hãng, bỏ qua NA.
    Ý nghĩa: Cho biết mức giá trung bình – đại diện xu hướng chung của giá
  • median(sellingprice, na.rm = TRUE)
    Kỹ thuật: Tính giá trung vị (median)
    Ý nghĩa: Trung vị ít bị ảnh hưởng bởi giá quá cao/thấp → chỉ báo tốt khi phân phối lệch
  • arrange(desc(n))
    Kỹ thuật: Sắp xếp theo số lượng giảm dần
    Ý nghĩa: Xác định hãng xe phổ biến nhất (dựa trên dữ liệu mẫu)
  • slice_head(n = 10)
    Kỹ thuật: Lấy Top 10 hãng có số lượng xe nhiều nhất
    Ý nghĩa: Giới hạn phân tích vào nhóm hãng có ý nghĩa (có nhiều dữ liệu)
  • mutate(n = comma(n))
    Kỹ thuật: Thêm dấu phẩy (1,000 → 10,000) giúp dễ đọc
    Ý nghĩa: Đọc bảng dễ hơn phù hợp báo cáo / luận văn
  • mean_price = comma(mean_price, accuracy = 1)
    Kỹ thuật: Làm tròn & định dạng giá (ví dụ: 12,540 → 12,540.0)
    Ý nghĩa: Chuẩn hoá báo cáo dễ so sánh giá giữa các hãng
  • rename(…)
    Kỹ thuật: Đổi tên cột sang tiếng Việt friendly
    Ý nghĩa: Phục vụ trình bày bảng báo cáo

1.3.5. Thống kê thời gian: số xe bán theo year_saledate

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")
    )

}  
Số lượng và giá bán trung bình của xe theo từng năm
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
  • if (“year_saledate” %in% names(df))
    Kỹ thuật: Kiểm tra xem biến year_saledate có tồn tại trong dataframe hay không. Nếu có thì mới chạy phân tích.
    Ý nghĩa: Tránh lỗi khi chạy code trên dataset khác không có biến này. Bước lập trình an toàn (defensive programming).
  • group_by(year_saledate)
    Kỹ thuật: Gom nhóm dữ liệu theo từng năm bán xe.
    Ý nghĩa: Mục tiêu là phân tích xu hướng theo thời gian (time-series descriptive).
  • summarise(n = n(), mean_price = mean(sellingprice, na.rm = TRUE))
    Kỹ thuật:n() đếm số lượng xe bán trong năm. mean_price tính giá bán trung bình.
    Ý nghĩa:Cho thấy: năm nào có doanh số cao nhất và giá xe trung bình thay đổi thế nào theo thời gian.

1.3.6. Thống kê số xe theo loại hộp số

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")
Phân bố xe theo loại hộp số
transmission So_luong Ti_le
automatic 416995 85.18
57246 11.69
manual 15317 3.13
  • df %>%
    Kỹ thuật: Bắt đầu pipeline xử lý dữ liệu dựa trên data frame df.
    Ý nghĩa: Cho phép thực hiện nhiều thao tác liên tiếp để tạo bảng tổng hợp.
  • group_by(transmission)
    Kỹ thuật: Gom nhóm dữ liệu theo biến transmission (hộp số).
    Ý nghĩa: Phân chia toàn bộ bộ dữ liệu theo từng loại hộp số (ví dụ: Automatic, Manual, CVT…).
  • summarise(So_luong = n(), Ti_le = round(100 * n() / nrow(df), 2))
    Kỹ thuật: n() đếm số dòng trong mỗi nhóm → số xe theo từng loại hộp số. Ti_le = tỷ lệ phần trăm của loại đó trên toàn bộ dataset, làm tròn 2 chữ số sau dấu phẩy.
    Ý nghĩa: Cung cấp tần suất (frequency) và tỷ lệ phần trăm (percentage) — đây là 2 chỉ số thống kê mô tả quan trọng khi phân tích biến phân loại (categorical variable).
  • arrange(desc(So_luong))
    Kỹ thuật: Sắp xếp kết quả theo số lượng xe giảm dần.
    Ý nghĩa: Giúp dễ đọc, ưu tiên loại hộp số phổ biến nhất lên đầu bảng.

1.4. Trực quan hóa dữ liệu

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)

1.4.1. Biểu đồ số lượng của mỗi hãng xe

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.

1.4.2. Biểu đồ quan hệ quãng đường — giá.

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.

1.4.3. So sánh xu hướng theo năm giữa các hãng.

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

1.4.4. Mô tả phân phối giá bán

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.

1.4.5.So sánh mức giá trung bình giữa xe số tự động và số sàn

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).

1.4.6. Mối quan hệ giữa quãng đường và giá trong từng nhóm giá

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).

1.4.7. Xác định hãng xe phổ biến nhất và giá trung bình tương ứng

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á.

1.4.8. Trực quan hóa mức độ tương quan giữa các biến định lượng

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.

1.4.9. So sánh phân phối giá giữa các nhóm tuổi xe

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?”.

1.4.10. Kiểm tra mức tương quan giữa giá thực tế và giá thị trường (MMR)

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”

1.4.11. So sánh quãng đường trung bình giữa các nhóm giá

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.

1.4.12. Phân tích xu hướng lượng xe bán ra theo thời gian

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ê.

1.4.13. Ảnh hưởng của tình trạng xe (condition) tới giá bán

  • Biểu đồ Violin + Boxplot theo nhóm tình trạng xe: So sánh phân phối giá bán (sellingprice) giữa các nhóm tình trạng xe (condition_group), nhằm kiểm tra xem tình trạng xe có ảnh hưởng đến giá bán hay không, và mức độ ảnh hưởng như thế nào.
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.

  • upper = list(continuous = GGally::wrap(“cor”, size = 3))
    Kỹ thuật:Ở phần trên đường chéo, hiển thị hệ số tương quan Pearson giữa các biến liên tục, chữ cỡ 3.
    Ý nghĩa thống kê:Cho biết mức độ tương quan (r) giữa từng cặp biến.
  • lower = list(continuous = “smooth”)
    Kỹ thuật:Dưới đường chéo hiển thị biểu đồ scatter kèm đường xu hướng mượt (loess).
    Ý nghĩa thống kê:Giúp nhìn rõ hướng tương quan (tăng/giảm) và tính tuyến tính của quan hệ giữa các biến.
  • diag = list(continuous = “barDiag”)
    Kỹ thuật:Ở đường chéo chính, hiển thị histogram của từng biến.
    Ý nghĩa thống kê:Minh họa phân phối đơn biến, xem biến có lệch phải/trái hay tập trung quanh trung bình.

1.4.15. Tỷ trọng từng nhóm giá theo loại hộp số

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).

1.4.16. Phân tích quan hệ giữa năm sản xuất và quãng đường chạy trong từng nhóm giá.

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

1.4.18. Phân bố phần dư của mô hình hồi quy

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.

1.4.19.Trực quan hóa tỷ lệ tích lũy của giá bá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.

1.4.20. Tổng hợp nhiều góc nhìn

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.

CHƯƠNG 2. PHÂN TÍCH CÁC CHỈ SỐ TÀI CHÍNH CỦA NGÂN HÀNG TMCP QUÂN ĐỘI (MBB)

2.1. Giới thiệu bộ dữ liệu

2.1.1.Nhập sơ dữ 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"))
Danh sách các biến trong dữ liệu
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.

2.1.2. Kiểm tra dữ liệu

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

2.1.3.Kiểu dữ liệu

# --- 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
  )
Kiểu dữ liệu trong bộ biến nghiên cứu
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









2.1.4. Ý nghĩa các biến

# 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;")
Ý nghĩa các biến
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

2.1.5. Viết tắt các tên biến

# ============================================================
#  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)
Danh sách biến, bản dịch tiếng Anh và viết tắt
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

2.1.6. Tính các chỉ số

# 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"))
Bảng kết quả
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

2.2. Phân tích xu hướng hiệu quả hoạt động và rủi ro tài chính của MB Bank (2013–2024)

2.2.1 Mục tiêu và ý nghĩa phân tích

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à:

  • Đánh giá xu hướng hiệu quả hoạt động thông qua các chỉ tiêu phản ánh khả năng sinh lời như:
    • ROE (Return on Equity) – Tỷ suất lợi nhuận trên vốn chủ sở hữu
    • ROA (Return on Assets) – Tỷ suất lợi nhuận trên tổng tài sản
    • NIM (Net Interest Margin) – Biên lợi nhuận lãi thuần
  • Phân tích rủi ro tài chính thông qua chỉ tiêu:
    • D/E (Debt-to-Equity Ratio) – Tỷ lệ nợ trên vốn chủ sở hữu, phản ánh mức độ sử dụng đòn bẩy tài chính của ngân hàng.
  • Kết hợp đánh giá dòng tiền từ ba hoạt động chính (kinh doanh, đầu tư, tài chính) nhằm xem xét sự bền vững trong khả năng tạo ra tiền và mức độ phụ thuộc vào nguồn vốn vay. Việc theo dõi biến động các chỉ tiêu này giúp:
    • Nhận diện xu hướng tài chính dài hạn của MB Bank.
    • Đánh giá mức độ an toàn tài chính và hiệu quả sử dụng vốn.
    • Cung cấp cơ sở cho nhà đầu tư, cổ đông và ban lãnh đạo trong việc ra quyết định tài chính và chiến lược phát triển.
      Ngoài ra, sự kết hợp giữa phân tích tỷ lệ và dòng tiền giúp làm rõ mối quan hệ giữa hiệu quả sinh lời và khả năng tạo tiền mặt thực tế, qua đó phản ánh chất lượng lợi nhuận và tính bền vững trong hoạt động của ngân hàng.

2.2.2. Phân tích rủi ro tài chính (DE)

a. Mô tả thống kê


# 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"))
Thống kê mô tả chỉ số D/E
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ả.

b. Biểu đồ xu hương D/E qua các năm


# 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.

c. Phân nhóm rủi ro theo mức D/E

  • Là cách đánh giá mức độ rủi ro tài chính của doanh nghiệp dựa trên tỷ lệ nợ trên vốn chủ sở hữu (Debt-to-Equity ratio – D/E).
    Ý nghĩa:
    Tỷ lệ D/E phản ánh mức độ sử dụng nợ vay để tài trợ cho tài sản của doanh nghiệp.
    • D/E cao → doanh nghiệp phụ thuộc nhiều vào nợ vay, rủi ro tài chính cao.
    • D/E thấp → cơ cấu vốn an toàn hơn, rủi ro thấp
#  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.

d. Tỷ trọng từng nhóm rủi ro qua thời gian


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.

e. Biểu đồ mật độ


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.

2.2.3. Phân tích hiệu quả hoạt động

a. Thống kê mô 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"))
Thống kê mô tả ROE, ROA, NIM (2014–2024)
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.

b. So sánh hiệu quả hoạt động của MB Bank trước và sau 2020


# 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.

c. Tính tốc độ tăng trưởng hàng năm và vẽ biểu đồ tăng trưởng


# 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
  )
Bảng: Tốc độ tăng trưởng hằng năm của ROE, ROA và NIM
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ế.

d. Biểu đồ boxplot


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.

e. Độ ổn định của các chỉ số hiệu quả hoạt độ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.

2.2.4. Quan hệ giữa rủi ro và hiệu quả

a. Phân tích tương quan


# 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.

b. Hồi quy đơn


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"
  )
Kết quả hồi quy đơn: Mối quan hệ giữa ROE và DE
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

c.Hồi quy đa biến


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"))
Kết quả hồi quy đa biến: ROE ~ DE + NIM + ROA
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)
Tóm tắt mô hình
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.

d. Mối quan hệ giữa D/E và ROE


#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.

e. Chú thích hệ số hồi quy


#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.

f. Thống kê mô tả các chỉ tiêu tài chính (2014–2024)


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)
Bảng: Thống kê mô tả các chỉ tiêu tài chính (2014–2024)
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

g. Tốc độ tăng trưởng


# =====================================================
# 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.

2.2.5. Phân tích tương quan toàn diện (Pair Plot)

#  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.

2.2.6. Tổng hợp xu hướng 4 chỉ số (Facet Chart)

# 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

2.2.7. Kết luậ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.