BỘ TÀI CHÍNH
TRƯỜNG ĐẠI HỌC TÀI CHÍNH – MARKETING
KHOA KHOA HỌC DỮ LIỆU





BÁO CÁO TIỂU LUẬN
NGÔN NGỮ LẬP TRÌNH TRONG PHÂN TÍCH DỮ LIỆU


Sinh viên thực hiện : Võ Thị Kiều My_2321000339
: Trần Huỳnh Ni Ka_2321000323
Mã lớp học phần : 2531101140001
Giảng viên phụ trách : ThS. Trần Mạnh Tường
Thời gian thực hiện : Học kỳ 3/2025





Thành phố Hồ Chí Minh, tháng 10 năm 2025

# Thiết lập toàn cục cho các chunk
knitr::opts_chunk$set(echo = TRUE)

# Hàm định dạng số Việt Nam
vn <- function(x) {
  scales::number(x, big.mark = ".", decimal.mark = ",", accuracy = 0.01)
}

# Tùy chọn cho cách tibble (tidyverse) được in ra
options(
  pillar.sigfig = 7
)
# Dòng lỗi đã được xóa

# Tùy chọn cho cách data.frame của base R được in ra trong R Markdown
knitr::opts_knit$set(
  rmarkdown.df.print = TRUE
)

CHƯƠNG 1 Giới thiệu và Khám phá Dữ liệu (EDA ban đầu)

1.1. Giới thiệu

Bộ dữ liệu Vehicle Sales Data là một bộ 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. Dữ liệu bao gồm thông tin về hãng xe, năm sản xuất, số km đã đi, tình trạng xe và giá bán, giúp đánh giá các yếu tố ảnh hưởng đến giá trị của xe trên thị trường.

1.2. Nạp các thư viện cần thiết

Phần code dưới đây sẽ nạp các thư viện cần thiết cho việc phân tích. tidyverse là bộ công cụ chính, knitrkableExtra dùng để tạo bảng đẹp, và janitor giúp làm sạch tên cột.

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.4.3
## Warning: package 'ggplot2' was built under R version 4.4.3
## Warning: package 'tidyr' was built under R version 4.4.3
## Warning: package 'readr' was built under R version 4.4.3
## Warning: package 'purrr' was built under R version 4.4.3
## Warning: package 'dplyr' was built under R version 4.4.3
## Warning: package 'stringr' was built under R version 4.4.3
## Warning: package 'forcats' was built under R version 4.4.3
## Warning: package 'lubridate' was built under R version 4.4.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.5.1
## ✔ ggplot2   4.0.0     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(knitr)
## Warning: package 'knitr' was built under R version 4.4.3
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.4.3
## 
## Attaching package: 'kableExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library(janitor)
## Warning: package 'janitor' was built under R version 4.4.3
## 
## Attaching package: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test

1.3. Đọc và Kiểm tra Dữ liệu ban đầu

Chúng ta sẽ sử dụng hàm read.csv() để đọc dữ liệu từ tệp car_prices.csv và lưu vào một biến (data frame) có tên là df.

df <- read.csv("car_prices.csv")

Tệp car_prices.csv được lưu xuống máy từ trang web Kaggle.

1.3.1. Kiểm tra Kích thước và Cấu trúc dữ liệu bằng dim()str()

Tiếp theo, ta sẽ kiểm tra kích thước và cấu trúc của bộ dữ liệu.

cat("Kích thước bộ dữ liệu (dòng, cột):\n")
## Kích thước bộ dữ liệu (dòng, cột):
dim(df)
## [1] 558837     16
cat("\nCấu trúc bộ dữ liệu:\n")
## 
## Cấu trúc bộ dữ liệu:
str(df)
## 'data.frame':    558837 obs. of  16 variables:
##  $ year        : int  2015 2015 2014 2015 2014 2015 2014 2014 2014 2014 ...
##  $ make        : chr  "Kia" "Kia" "BMW" "Volvo" ...
##  $ model       : chr  "Sorento" "Sorento" "3 Series" "S60" ...
##  $ trim        : chr  "LX" "LX" "328i SULEV" "T5" ...
##  $ body        : chr  "SUV" "SUV" "Sedan" "Sedan" ...
##  $ transmission: chr  "automatic" "automatic" "automatic" "automatic" ...
##  $ vin         : chr  "5xyktca69fg566472" "5xyktca69fg561319" "wba3c1c51ek116351" "yv1612tb4f1310987" ...
##  $ state       : chr  "ca" "ca" "ca" "ca" ...
##  $ condition   : int  5 5 45 41 43 1 34 2 42 3 ...
##  $ odometer    : int  16639 9393 1331 14282 2641 5554 14943 28617 9557 4809 ...
##  $ color       : chr  "white" "white" "gray" "white" ...
##  $ interior    : chr  "black" "beige" "black" "black" ...
##  $ seller      : chr  "kia motors america  inc" "kia motors america  inc" "financial services remarketing (lease)" "volvo na rep/world omni" ...
##  $ mmr         : int  20500 20800 31900 27500 66000 15350 69000 11900 32100 26300 ...
##  $ sellingprice: int  21500 21500 30000 27750 67000 10900 65000 9800 32250 17500 ...
##  $ saledate    : chr  "Tue Dec 16 2014 12:30:00 GMT-0800 (PST)" "Tue Dec 16 2014 12:30:00 GMT-0800 (PST)" "Thu Jan 15 2015 04:30:00 GMT-0800 (PST)" "Thu Jan 29 2015 04:30:00 GMT-0800 (PST)" ...

Giải thích câu lệnh:

dim(df)= “dimension” -> dùng để xem kích thước của bộ dữ liệu (data frame, matrix,…).

str() =“structure” -> hiển thị cấu trúc chi tiết của bộ dữ liệu:

  • Tên biến (tên cột),

  • Kiểu dữ liệu của từng biến (num , chr , factor , int , Date , …)

  • Một vài giá trị đầu tiên để mình họa nội dung của mỗi cột.

cat("\n...\n"): hàm cat() chỉ để in chú thích trước khi hiển thị cấu trúc và \n giúp tách dòng cho đẹp và dễ đọc hơn.

Kết quả: Bộ dữ liệu có 558.837 quan sát và 16 biến. Các biến chủ yếu thuộc kiểu integer (số nguyên) và character (ký tự), phù hợp với mô tả ban đầu.

1.3.2. Xem mẫu dữ liệu bằng head()tail()

Để đảm bảo dữ liệu được đọc đúng định dạng và không xảy ra lỗi nhập liệu, tác giả tiến hành kiểm tra ba dòng đầu tiên và ba dòng cuối cùng của bộ dữ liệu.

library(knitr)
head(df,3) |> 
  kable(caption = "*Bảng 1: Ba dòng đầu tiên của dữ liệu*")
Bảng 1: Ba dòng đầu tiên của dữ liệu
year make model trim body transmission vin state condition odometer color interior seller mmr sellingprice saledate
2015 Kia Sorento LX SUV automatic 5xyktca69fg566472 ca 5 16639 white black kia motors america inc 20500 21500 Tue Dec 16 2014 12:30:00 GMT-0800 (PST)
2015 Kia Sorento LX SUV automatic 5xyktca69fg561319 ca 5 9393 white beige kia motors america inc 20800 21500 Tue Dec 16 2014 12:30:00 GMT-0800 (PST)
2014 BMW 3 Series 328i SULEV Sedan automatic wba3c1c51ek116351 ca 45 1331 gray black financial services remarketing (lease) 31900 30000 Thu Jan 15 2015 04:30:00 GMT-0800 (PST)

Giải thích câu lệnh:

  • head(df): là 1 hàm cơ bản trong R, dùng để hiển thị vài dòng đầu tiên của data frame (mặc định là 6 dòng). Ta có thể thay đổi số dòng bằng cách: head(df,3)

  • Toán tử |> (pipe operator): là 1 toán tử ống (pipe) được thêm vào R từ phiên bản 4.1

    • Ý nghĩa: truyền kết quả của biểu thức trước -> làm đầu vào cho hàm sau.

    • Tương đương với %>% trong dplyr (tidyverse) nhưng là cú pháp chuẩn của R base.

  • kable() : là hàm trong gói knitr được thường dùng trong R Markdown. Nó chuyển data frame thành bảng định dạng đẹp trong báo cáo (HTML, Word, PDF,…). Có thể thêm:

    • caption -> tiêu đề của bảng

    • digits -> số chữ số sau dấu thập phân

    • align -> căn lề (c,l,r)

  • caption = "*Bảng 1: Ba dòng đầu tiên của dữ liệu*" : là chú thích tên bảng hiển thị dưới bảng. Dấu *...* trong Markdown dùng để in nguyên.( nếu muốn in đậm thì dùng: **...**)

tail(df,3) |>
kable(caption = "*Bảng 2: Ba dòng cuối cùng của dữ liệu*")
Bảng 2: Ba dòng cuối cùng của dữ liệu
year make model trim body transmission vin state condition odometer color interior seller mmr sellingprice saledate
558835 2012 BMW X5 xDrive35d SUV automatic 5uxzw0c58cl668465 ca 48 50561 black black financial services remarketing (lease) 29800 34000 Wed Jul 08 2015 09:30:00 GMT-0700 (PDT)
558836 2015 Nissan Altima 2.5 S sedan automatic 1n4al3ap0fc216050 ga 38 16658 white black enterprise vehicle exchange / tra / rental / tulsa 15100 11100 Thu Jul 09 2015 06:45:00 GMT-0700 (PDT)
558837 2014 Ford F-150 XLT SuperCrew automatic 1ftfw1et2eke87277 ca 34 15008 gray gray ford motor credit company llc pd 29600 26700 Thu May 28 2015 05:30:00 GMT-0700 (PDT)

Giải thích câu lệnh:

Tương tự như head(df,3) nhưng hàm tail(df,3) là kiểm tra cấu trúc dữ liệu 3 dòng cuối của dữ liệu.

=> Kết quả: cho thấy cấu trúc dữ liệu được ghi nhận đầy đủ, các biến có giá trị hợp lệ và không phát sinh giá trị thiếu ở đầu hoặc cuối tệp dữ liệu.

1.3.3. Mô tả các biến

Để hệ thống hóa thông tin, chúng ta tạo một bảng tóm tắt mô tả ý nghĩa và kiểu dữ liệu của từng biến.

variable_summary <- data.frame(
  Variable = names(df),
  Meaning = c("Năm sản xuất", "Hãng xe", "Dòng xe", "Phiên bản", "Kiểu dáng", "Hộp số", "Số VIN", "Bang", "Tình trạng (điểm)", "Số dặm đã đi", "Màu ngoại thất", "Màu nội thất", "Người bán", "Giá thị trường (MMR)", "Giá bán thực tế", "Ngày bán"),
  DataType = sapply(df, class)
)
knitr::kable(variable_summary, caption = "Bảng: Mô tả và kiểu dữ liệu của các biến")
Bảng: Mô tả và kiểu dữ liệu của các biến
Variable Meaning DataType
year year Năm sản xuất integer
make make Hãng xe character
model model Dòng xe character
trim trim Phiên bản character
body body Kiểu dáng character
transmission transmission Hộp số character
vin vin Số VIN character
state state Bang character
condition condition Tình trạng (điểm) integer
odometer odometer Số dặm đã đi integer
color color Màu ngoại thất character
interior interior Màu nội thất character
seller seller Người bán character
mmr mmr Giá thị trường (MMR) integer
sellingprice sellingprice Giá bán thực tế integer
saledate saledate Ngày bán character

Giải thích câu lệnh:

  • names(df): Trả về danh sách tên các biến (cột) của dataframe df.

  • Meaning: Một vector gồm giải thích ý nghĩa các biến, ví dụ “Năm sản xuất”, “Hãng xe”, “Dòng xe”….

  • sapply(df, class): Xác định kiểu dữ liệu từng biến (như numeric, character, factor).

  • Tất cả được gộp lại thành một dataframe là variable_summary, giúp bạn xem toàn bộ thông tin trên một bảng duy nhất.

  • Dùng knitr::kable để trình bày bảng trên định dạng đẹp trong báo cáo, dễ nhìn và chuyên nghiệp.

Kết quả: Bảng này cung cấp một danh mục tham khảo nhanh cho tất cả 16 biến, giúp việc phân tích sau này trở nên dễ dàng hơn.

1.3.4. Kiểm tra giá trị bị thiếu (NA)

colSums(is.na(df))
##         year         make        model         trim         body transmission 
##            0            0            0            0            0            0 
##          vin        state    condition     odometer        color     interior 
##            0            0        11820           94            0            0 
##       seller          mmr sellingprice     saledate 
##            0           38           12            0

Giải thích câu lệnh: Lệnh colSums(is.na(df)) sẽ đếm số lượng giá trị NA trong mỗi cột.

Kết quả: Bốn cột condition, odometer, mmr, và sellingprice được phát hiện có chứa giá trị thiếu.

1.3.5. Kiểm tra dữ liệu trùng lặp

Chúng ta cũng cần kiểm tra xem có dòng dữ liệu nào bị trùng lặp hoàn toàn hay không.

cat("Số dòng dữ liệu bị trùng lặp hoàn toàn:", sum(duplicated(df)))
## Số dòng dữ liệu bị trùng lặp hoàn toàn: 0

Giải thích câu lệnh: sum(duplicated(df) để đếm số dòng bị trùng lặp trong data frame df .

Kết quả: Không có dòng nào bị trùng lặp hoàn toàn trong bộ dữ liệu thô.

1.3.6. Thống kê mô tả ban đầu

Cuối cùng, hàm summary() cung cấp một bản tóm tắt thống kê nhanh cho toàn bộ dữ liệu, giúp phát hiện các giá trị bất thường hoặc đặc điểm phân bố ban đầu.

summary(df)
##       year          make              model               trim          
##  Min.   :1982   Length:558837      Length:558837      Length:558837     
##  1st Qu.:2007   Class :character   Class :character   Class :character  
##  Median :2012   Mode  :character   Mode  :character   Mode  :character  
##  Mean   :2010                                                           
##  3rd Qu.:2013                                                           
##  Max.   :2015                                                           
##                                                                         
##      body           transmission           vin               state          
##  Length:558837      Length:558837      Length:558837      Length:558837     
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##                                                                             
##    condition        odometer         color             interior        
##  Min.   : 1.00   Min.   :     1   Length:558837      Length:558837     
##  1st Qu.:23.00   1st Qu.: 28371   Class :character   Class :character  
##  Median :35.00   Median : 52254   Mode  :character   Mode  :character  
##  Mean   :30.67   Mean   : 68320                                        
##  3rd Qu.:42.00   3rd Qu.: 99109                                        
##  Max.   :49.00   Max.   :999999                                        
##  NA's   :11820   NA's   :94                                            
##     seller               mmr          sellingprice      saledate        
##  Length:558837      Min.   :    25   Min.   :     1   Length:558837     
##  Class :character   1st Qu.:  7100   1st Qu.:  6900   Class :character  
##  Mode  :character   Median : 12250   Median : 12100   Mode  :character  
##                     Mean   : 13769   Mean   : 13611                     
##                     3rd Qu.: 18300   3rd Qu.: 18200                     
##                     Max.   :182000   Max.   :230000                     
##                     NA's   :38       NA's   :12

Kết quả: Tóm tắt cho thấy một số điểm đáng chú ý, chẳng hạn như sellingprice có giá trị tối thiểu là 1 và odometer có giá trị tối đa là 999,999. Đây là những dấu hiệu của các giá trị ngoại lai cần được xử lý.


II: Làm sạch Dữ liệu (Data Cleaning)

2.1. Chuẩn hóa tên biến (String Manipulation)

Để tên biến nhất quán và dễ sử dụng, df <- df |> clean_names():

  • Dùng hàm clean_names() từ gói janitor (hoặc tương tự) để làm sạch tên biến trong dataframe df.

  • Hàm này chuẩn hóa tên biến bằng cách chuyển tất cả chữ sang dạng chữ thường, thay các dấu cách hoặc ký tự đặc biệt bằng dấu gạch dưới, tạo tên biến rõ ràng, dễ đọc và thuận tiện cho xử lý tiếp theo.

library(janitor)
df <- df |> clean_names()
cat("Tên biến sau khi được làm sạch:\n")
## Tên biến sau khi được làm sạch:
names(df)
##  [1] "year"         "make"         "model"        "trim"         "body"        
##  [6] "transmission" "vin"          "state"        "condition"    "odometer"    
## [11] "color"        "interior"     "seller"       "mmr"          "sellingprice"
## [16] "saledate"

Kết quả: Tên các biến đã được chuẩn hóa, giúp việc viết code sau này thuận tiện hơn.

2.2. Xử lý giá trị bị thiếu (Handling Missing Values)

Bước này xử lý các giá trị thiếu đã phát hiện ở Phần 1. Chúng ta sẽ loại bỏ các dòng có sellingprice bị thiếu (vì đây là biến mục tiêu) và điền giá trị trung vị cho các cột số còn lại để tránh ảnh hưởng của các giá trị ngoại lai.

df <- df |> filter(!is.na(sellingprice))
df <- df |>
  mutate(
    condition = ifelse(is.na(condition), median(condition, na.rm = TRUE), condition),
    odometer = ifelse(is.na(odometer), median(odometer, na.rm = TRUE), odometer),
    mmr = ifelse(is.na(mmr), median(mmr, na.rm = TRUE), mmr)
  )
cat("Số lượng giá trị thiếu sau khi xử lý:\n")
## Số lượng giá trị thiếu sau khi xử lý:
colSums(is.na(df))
##         year         make        model         trim         body transmission 
##            0            0            0            0            0            0 
##          vin        state    condition     odometer        color     interior 
##            0            0            0            0            0            0 
##       seller          mmr sellingprice     saledate 
##            0            0            0            0

Giải thích câu lệnh:

df<- df |> filter(!is.na(sellingprice)) :

  • Lọc các dòng trong df mà biến sellingprice không bị thiếu (tức khác NA).

  • Giữ lại những dòng có giá trị bán thực tế hợp lệ, loại bỏ những dòng không có giá bán để đảm bảo dữ liệu đầy đủ cho phân tích.

df<- df |> mutate(...) :

  • Sử dụng hàm mutate để thay thế các giá trị thiếu trong ba cột condition (tình trạng), odometer (số dặm đã đi), và mmr (giá thị trường) bằng giá trị trung vị (median) của từng cột đó.

  • Cụ thể, với mỗi biến:

    • ifelse(is.na(biến), median(biến, na.rm = TRUE), biến) nghĩa là nếu giá trị tại vị trí đó là NA (thiếu), thì thay bằng giá trị trung vị đã tính, còn nếu không thiếu thì giữ nguyên.

Kết quả: Bảng kết quả cho thấy tất cả các cột đều có 0 giá trị thiếu, chứng tỏ quá trình xử lý đã thành công.

2.3. Xử lý dữ liệu trùng lặp (Identifying and removing duplicates)

Mặc dù bước kiểm tra ban đầu không thấy dòng trùng lặp, việc chạy lệnh distinct() là một bước đảm bảo an toàn sau các bước xử lý khác để loại bỏ bất kỳ dòng nào có thể trở nên trùng lặp.

rows_before <- nrow(df)
df <- df |> distinct()
rows_after <- nrow(df)
cat("Số dòng đã bị loại bỏ do trùng lặp:", rows_before - rows_after, "\n")
## Số dòng đã bị loại bỏ do trùng lặp: 0

Giải thích câu lệnh:

rows_before <- nrow(df): Lấy số dòng ban đầu của dataframe df và lưu vào biến row_before để so sánh sau khi xử lý.

df<- df |> distinct() :

  • Hàm distinct() giữ lại các dòng duy nhất trong dataframe, loại bỏ các dòng bị trùng lặp hoàn toàn (các dòng có giá trị giống nhau ở tất cả các cột).

  • Kết quả là dataframe df chỉ còn các dòng riêng biệt, không bị lặp.

rows_after <- nrow(df): Lấy số dòng sau khi đã loại bỏ trùng lặp và lưu vào biến rows_after.

Kết quả: Cho thấy sau các bước xử lý trước thì không có dòng nào trùng lặp.

2.4. Chuyển đổi kiểu dữ liệu (Data Type Conversions)

2.4.1. Chuyển đổi biến saledate

Biến saledate cần được chuyển từ dạng ký tự (character) sang dạng ngày-giờ (datetime) để có thể thực hiện các phép tính liên quan đến thời gian.

df$saledate <- as.POSIXct(df$saledate, format = "%a %b %d %Y %H:%M:%S GMT%z", tz = "UTC")
head(df$saledate, 2)
## [1] "2014-12-16 20:30:00 UTC" "2014-12-16 20:30:00 UTC"

Giải thích câu lệnh:

  • as.POSIXct(...):

    • Hàm chuyển đổi chuỗi ký tự (character) thành kiểu dữ liệu dạng ngày giờ (POSIXct) trong R.

    • Kiểu POSIXct lưu trữ ngày giờ dưới dạng số nguyên tính bằng giây kể từ mốc thời gian gốc (1970-01-01).

  • Tham số format:

    • Là định dạng của chuỗi ngày giờ ban đầu cần chuyển đổi.

    • %a: Tên viết tắt ngày trong tuần (Mon, Tue, …)

    • %b: Tên viết tắt tháng (Jan, Feb, …)

    • %d: Ngày trong tháng (01-31)

    • %Y: Năm đầy đủ (4 chữ số)

    • %H:%M:%S: Giờ:phút:giây theo định dạng 24h

    • GMT%z: Múi giờ theo dạng GMT và offset số giờ/phút (ví dụ: GMT+0700)

  • tz = "UTC":

    • Chỉ định múi giờ chuẩn đầu ra là UTC (Coordinated Universal Time).

    • Giúp chuẩn hóa ngày giờ về một múi giờ chung để so sánh hoặc xử lý tiếp.

Kết quả: Chuỗi ký tự ban đầu của biến saledate đã được chuyển thành kiểu dữ liệu ngày-giờ chuẩn của R với múi giờ UTC.

2.4.2. Chuyển đổi các biến định tính sang factor

Để R hiểu các biến như make, body, transmission là biến phân loại (categorical), ta nên chuyển chúng sang kiểu factor. Điều này hữu ích cho việc vẽ đồ thị và xây dựng mô hình.

cols_to_factor <- c("make", "body", "transmission", "state", "color", "interior")
df <- df |>
  mutate(across(all_of(cols_to_factor), as.factor))
cat("Kiểu dữ liệu sau khi chuyển đổi:\n")
## Kiểu dữ liệu sau khi chuyển đổi:
str(df[, cols_to_factor])
## 'data.frame':    558825 obs. of  6 variables:
##  $ make        : Factor w/ 97 levels "","acura","Acura",..: 48 48 10 96 10 72 10 17 7 17 ...
##  $ body        : Factor w/ 88 levels "","access cab",..: 78 78 72 72 72 72 72 72 72 12 ...
##  $ transmission: Factor w/ 5 levels "","automatic",..: 2 2 2 2 2 2 2 2 2 2 ...
##  $ state       : Factor w/ 64 levels "3vwd17aj0fm227318",..: 30 30 30 30 30 30 30 30 30 30 ...
##  $ color       : Factor w/ 47 levels "","—","11034",..: 46 46 36 46 36 36 30 30 46 43 ...
##  $ interior    : Factor w/ 18 levels "","—","beige",..: 4 3 4 4 4 4 4 4 4 4 ...

Giải thích câu lệnh:

  • cols_to_factor <- c("make", "body", "transmission", "state", "color", "interior")

    • Tạo một vector chứa tên các biến cần chuyển đổi sang dạng factor, gồm: hãng xe, kiểu dáng, hộp số, bang, màu ngoại thất, màu nội thất.
  • df <- df |> mutate(across(all_of(cols_to_factor), as.factor))

    • Sử dụng mutate kết hợp với across để áp dụng hàm as.factor đồng thời lên tất cả các cột nằm trong vector cols_to_factor.

    • Kết quả: Các cột này trong dataframe sẽ chuyển từ dạng ký tự (character) hoặc số (numeric) sang dạng phân loại (factor). Điều này phục vụ cho các bài toán phân tích/thống kê/phân loại, ví dụ như vẽ biểu đồ bar hay dùng trong các model hồi quy tuyến tính, classification.

Kết quả: Kết quả hiển thị cấu trúc của 6 biến sau khi được chuyển sang kiểu factor cho thấy:

  • Dataframe có 558.825 dòng (obs.) và 6 biến (variables).

  • Mỗi biến đều là factor (kiểu phân loại), với số lượng “levels” (giá trị phân biệt) khác nhau:

    • make: 97 levels (hãng xe)

    • body: 88 levels (kiểu dáng)

    • transmission: 5 levels (hộp số)

    • state: 64 levels (bang)

    • color: 47 levels (màu ngoại thất)

    • interior: 18 levels (màu nội thất)

  • Đầu ra cũng hiện một số giá trị mẫu cũng như số lượng levels ở mỗi biến, xác nhận các biến này đã thực sự là kiểu factor sau khi chuyển đổi.

Điều này chứng tỏ các thao tác chuyển đổi dữ liệu phân loại sang factor đã thành công.

2.5. Sửa lỗi không nhất quán (Correcting Inconsistencies)

Biến transmission có thể có các giá trị khác nhau nhưng cùng ý nghĩa. Ta cần chuẩn hóa chúng bằng cách chuyển về chữ thường và gộp các giá trị tương tự.

df$transmission <- tolower(df$transmission)
df$transmission[df$transmission == "automated manual"] <- "automatic"
cat("Các giá trị của biến 'transmission' sau khi chuẩn hóa:\n")
## Các giá trị của biến 'transmission' sau khi chuẩn hóa:
table(df$transmission)
## 
##           automatic    manual     sedan 
##     65351    475904     17544        26

Giải thích câu lệnh :

df$transmission <- tolower(df$transmission) :

  • Hàm tolower() giúp chuyển tất cả ký tự trong chuỗi từ chữ hoa/thường về chữ thường.

  • Khi áp dụng lên df$transmission, tất cả giá trị như “Automatic”, “AUTOMATIC”, “automatic” sẽ trở thành “automatic”.

  • Việc này giúp đồng nhất dữ liệu, tránh trường hợp cùng một ý nghĩa nhưng khác kiểu chữ sẽ bị coi là hai nhóm khác nhau khi phân tích.

df$transmission[df$transmission == "automated manual"] <- "automatic":

  • Thay thế toàn bộ giá trị "automated manual" thành "automatic".

  • Mục đích là gom nhóm các kiểu hộp số tương đồng về cùng một giá trị, giúp phân tích thống nhất và chính xác hơn.

table(df$transmission):

  • Hiển thị bảng tần suất (dem các giá trị khác nhau và số lần xuất hiện của mỗi giá trị) của biến transmission sau chuẩn hóa.

  • Kết quả giúp bạn kiểm tra xem đã chuẩn hóa thành công và các nhóm giá trị của biến này đã đồng nhất hay chưa.

Kết quả: Kết quả bảng tần suất sau khi chuẩn hóa biến transmission cho thấy:

  • Biến transmission trong df hiện có 3 giá trị chính:

    • "automatic" xuất hiện 65.351 lần

    • "manual" xuất hiện 475.904 lần

    • "sedan" xuất hiện 17.544 lần

    • (và một giá trị hiếm "sedan" chỉ có 26 trường hợp).


III: Chuyển đổi Dữ liệu (Data Transformation)

3.1. Tạo biến mới (Creating New Variables / Feature Engineering)

3.1.1. Tạo biến tuổi xe (age)

Từ các cột yearsaledate, chúng ta sẽ tạo ra các biến mới hữu ích hơn cho phân tích: sale_year, sale_month và quan trọng nhất là age (tuổi của xe tại thời điểm bán).

df$sale_year <- as.numeric(format(df$saledate, "%Y"))
df$sale_month <- as.numeric(format(df$saledate, "%m"))
df$age <- df$sale_year - df$year + 1

Giải thích câu lệnh:

  • df$sale_year <- as.numeric(format(df$saledate, "%Y"))
    Lấy năm từ biến saledate đã ở dạng ngày-giờ, chuyển thành kiểu số và lưu vào biến mới sale_year. Bây giờ mỗi xe có giá trị năm bán riêng biệt.​

  • df$sale_month <- as.numeric(format(df$saledate, "%m"))
    Tách phần tháng ra khỏi biến ngày bán, đổi thành số và lưu vào biến mới sale_month. Biến này biểu thị tháng trong năm mà xe được bán.​

  • df$age <- df$sale_year - df$year + 1
    Tính tuổi của mỗi chiếc xe tại thời điểm bán. Công thức lấy năm bán trừ đi năm sản xuất, sau đó cộng thêm 1 (ví dụ: sản xuất năm 2011, bán năm 2014 thì tuổi = 4). Cách cộng thêm 1 giúp phản ánh đúng tuổi tính toán thông thường trong ngành ô tô

3.1.2. Phân nhóm các hãng xe hiếm (make_grouped)

Biến make có rất nhiều hãng xe hiếm. Để giảm nhiễu và giúp mô hình hoạt động hiệu quả hơn, ta sẽ nhóm các hãng xe xuất hiện ít hơn 500 lần vào một danh mục chung là ‘other’.

make_counts <- df |> count(make, sort = TRUE)
rare_makes <- make_counts |> filter(n < 500) |> pull(make)
df <- df |>
  mutate(make_grouped = ifelse(make %in% rare_makes, "other", as.character(make)))

Giải thích câu lệnh:

  • make_counts <- df |> count(make, sort = TRUE)

    • Đếm số lần xuất hiện của từng hãng xe trong dataframe df (biến make), sắp xếp giảm dần theo tần suất.

    • Kết quả là một bảng gồm hai cột: tên hãng (make) và số lần xuất hiện (n).​

  • rare_makes <- make_counts |> filter(n < 500) |> pull(make)

    • Lọc những hãng xe xuất hiện dưới 500 lần trong dữ liệu (hiếm gặp).

    • Dùng pull(make) để lấy danh sách tên các hãng hiếm vào một vector riêng, thuận tiện cho các thao tác sau.​

  • df <- df |> mutate(make_grouped = ifelse(make %in% rare_makes, "other", as.character(make)))

    • Tạo biến mới make_grouped trong dataframe:

      • Nếu hãng xe nằm trong danh sách hiếm (rare_makes), giá trị sẽ là "other".

      • Ngược lại, giữ nguyên tên hãng gốc.

Kết quả: Số lượng danh mục hãng xe đã giảm đáng kể, giúp mô hình hóa và trực quan hóa trở nên hiệu quả hơn.

3.1.3. Phân nhóm giá bán (price_segment) và biến đổi log

Chúng ta tạo ra biến price_segment để phân loại xe vào các phân khúc giá khác nhau, và biến log_price để xử lý độ lệch của phân phối giá.

df <- df |>
  mutate(
    price_segment = case_when(
      sellingprice < 5000   ~ "1. Giá rẻ (< $5k)",
      sellingprice < 15000  ~ "2. Phổ thông ($5k - $15k)",
      sellingprice < 30000  ~ "3. Cao cấp ($15k - $30k)",
      TRUE                  ~ "4. Hạng sang (>= $30k)"
    ),
    log_price = log(sellingprice)
  )
table(df$price_segment)
## 
##         1. Giá rẻ (< $5k) 2. Phổ thông ($5k - $15k)  3. Cao cấp ($15k - $30k) 
##                    102344                    253454                    172281 
##    4. Hạng sang (>= $30k) 
##                     30746

Giải thích câu lệnh:

  • mutate: Thêm biến mới vào dataframe.

  • case_when: Xếp loại biến sellingprice thành 4 phân khúc:

    • “< $5k”: Giá rẻ

    • “$5k - $15k”: Phổ thông

    • “$15k - $30k”: Cao cấp

    • “>= $30k”: Hạng sang

  • log_price = log(sellingprice): Tạo thêm cột giá trị lấy log tự nhiên của giá bán.

  • table(df$price_segment): Thống kê số lượng các dòng thuộc từng nhóm giá vừa gán.

Kết quả: Cho thấy phần lớn xe trong bộ dữ liệu thuộc phân khúc “Phổ thông” và “Cao cấp”.

3.1.4. Phân nhóm odometer

Sử dụng hàm cut để chia biến số odometer liên tục thành các khoảng (bin) rời rạc, giúp việc phân tích theo nhóm quãng đường dễ dàng hơn.

df <- df |>
  mutate(
    odometer_group = cut(odometer,
                         breaks = c(0, 30000, 60000, 100000, 150000, Inf),
                         labels = c("Rất mới", "Đi ít", "Trung bình", "Đi nhiều", "Đi rất nhiều"),
                         right = TRUE)
  )
table(df$odometer_group)
## 
##      Rất mới        Đi ít   Trung bình     Đi nhiều Đi rất nhiều 
##       151004       160248       110311        90419        46843

Giải thích câu lệnh: Câu lệnh này dùng để phân loại và thống kê số km đã chạy của xe:

  • mutate() thêm cột mới odometer_group vào dataframe df.

  • cut() chia biến odometer thành 5 nhóm theo các khoảng km trong breaks:

    • 0-30,000: “Rất mới”

    • 30,000-60,000: “Đi ít”

    • 60,000-100,000: “Trung bình”

    • 100,000-150,000: “Đi nhiều”

    • trên 150,000: “Đi rất nhiều”

  • right = TRUE: mỗi nhóm bao gồm giá trị bên phải của khoảng (ví dụ 30,000 thuộc nhóm đầu).

  • table(df$odometer_group) đếm số dòng thuộc từng nhóm odometer_group.

Mục đích là phân nhóm km đi xe để dễ phân tích, đồng thời biết được số lượng xe ở mỗi nhóm km chạy trong dữ liệu.

Kết quả: Số lượng xe tập trung nhiều nhất ở các nhóm “Rất mới” và “Đi ít”.

3.1.5. Phân nhóm condition

Tương tự, chúng ta phân nhóm điểm condition thành các nhãn có ý nghĩa hơn như “Tốt”, “Trung bình”, v.v.

df <- df |>
  mutate(
    condition_level = cut(condition,
                          breaks = c(0, 20, 30, 40, Inf),
                          labels = c("Cần sửa chữa", "Trung bình", "Tốt", "Rất tốt"),
                          right = FALSE)
  )
table(df$condition_level)
## 
## Cần sửa chữa   Trung bình          Tốt      Rất tốt 
##       113662       111695       173495       159973

Giải thích câu lệnh: Câu lệnh này làm tương tự như phân nhóm odometer, nhưng với biến condition (tình trạng xe):

  • mutate() thêm cột condition_level phân nhóm theo giá trị condition.

  • cut() chia condition thành 4 nhóm dựa vào breaks = c(0, 20, 30, 40, Inf):

    • 0 đến dưới 20: “Cần sửa chữa”

    • 20 đến dưới 30: “Trung bình”

    • 30 đến dưới 40: “Tốt”

    • 40 trở lên: “Rất tốt”

  • right = FALSE nghĩa mỗi khoảng sẽ bao gồm giá trị bên trái, tức khoảng là dạng [a, b).

  • table(df$condition_level) đếm số lượng xe trong từng nhóm tình trạng.

Kết quả: Phần lớn xe trong bộ dữ liệu được đánh giá ở tình trạng “Tốt” và “Rất tốt”.

3.1.6. Phân nhóm hãng xe theo nguồn gốc (make_origin)

Phân nhóm các hãng xe theo nguồn gốc (Mỹ, Nhật, Đức, Hàn) có thể là một đặc trưng mạnh vì nó thường liên quan đến định vị thương hiệu và giá cả.

american_makes <- c("ford", "chevrolet", "dodge", "gmc", "jeep", "chrysler", "cadillac", "buick", "ram", "lincoln")
japanese_makes <- c("nissan", "toyota", "honda", "infiniti", "mazda", "subaru", "lexus", "acura", "mitsubishi")
german_makes <- c("bmw", "mercedes-benz", "audi", "volkswagen", "porsche")
korean_makes <- c("hyundai", "kia")

df <- df |>
  mutate(
    make_origin = case_when(
      make %in% american_makes ~ "Mỹ",
      make %in% japanese_makes ~ "Nhật",
      make %in% german_makes ~ "Đức",
      make %in% korean_makes ~ "Hàn Quốc",
      TRUE ~ "Khác"
    )
  )
table(df$make_origin)
## 
##      Đức Hàn Quốc     Khác       Mỹ     Nhật 
##      125       27   556319     1576      778

Giải thích câu lệnh: Câu lệnh này phân loại nguồn gốc của hãng xe dựa trên tên hãng trong cột make, thông qua danh sách các hãng xe của từng quốc gia:

  • Các vector american_makes, japanese_makes, german_makes, korean_makes chứa tên các hãng xe phổ biến của từng quốc gia.

  • Trong mutate(), hàm case_when() kiểm tra:

    • Nếu tên hãng trong make nằm trong danh sách hãng của Mỹ, gán "Mỹ".

    • Tương tự với Nhật, Đức, Hàn Quốc.

    • Nếu không thuộc danh sách nào, gán "Khác".

  • table(df$make_origin) đếm số xe trong từng nhóm nguồn gốc, giúp thấy rõ phân bố xe theo quốc gia nguồn gốc của hãng.

Mục đích của câu lệnh này là giúp phân loại nguồn gốc xuất xứ xe một cách rõ ràng, phục vụ các phân tích theo khu vực xuất xứ.

Kết quả: Xe Mỹ chiếm ưu thế tuyệt đối về số lượng, cho thấy bộ dữ liệu có khả năng được thu thập tại thị trường Bắc Mỹ.

3.2. Lọc dữ liệu (Filtering Data)

3.2.1. Lọc các giá trị ngoại lai (Outliers)

Loại bỏ các quan sát có giá bán và số dặm không hợp lý để tăng độ tin cậy của phân tích.

original_rows <- nrow(df)
df <- df |>
  filter(sellingprice > 100,
         odometer > 100 & odometer < 500000)

cat("Số dòng đã bị loại bỏ do giá trị ngoại lai:", original_rows - nrow(df), "\n")
## Số dòng đã bị loại bỏ do giá trị ngoại lai: 1859

3.2.2. Lọc (chọn) các biến cần thiết cho phân tích

Tạo một data frame cuối cùng df_clean chỉ chứa các biến đã được xử lý và các biến mới hữu ích, đồng thời loại bỏ các cột gốc hoặc cột trung gian không cần thiết.

df_clean <- df |>
  select(
    sellingprice, log_price, age, odometer, condition, mmr, 
    make_grouped, make_origin, model, body, transmission, 
    price_segment, odometer_group, condition_level,
    sale_year, sale_month, state, color, interior, seller
  ) |>
  rename(make = make_grouped)

cat("Các biến trong bộ dữ liệu cuối cùng:\n")
## Các biến trong bộ dữ liệu cuối cùng:
names(df_clean)
##  [1] "sellingprice"    "log_price"       "age"             "odometer"       
##  [5] "condition"       "mmr"             "make"            "make_origin"    
##  [9] "model"           "body"            "transmission"    "price_segment"  
## [13] "odometer_group"  "condition_level" "sale_year"       "sale_month"     
## [17] "state"           "color"           "interior"        "seller"

3.3. Sắp xếp dữ liệu (Sorting Data)

Sử dụng arrange() để sắp xếp dữ liệu, ví dụ như để xem 5 chiếc xe có giá bán cao nhất trong bộ dữ liệu.

df_clean |>
  arrange(desc(sellingprice)) |>
  head(5) |>
  select(make, model, sale_year, age, odometer, sellingprice) |>
  kable(caption = "5 xe có giá bán cao nhất")
5 xe có giá bán cao nhất
make model sale_year age odometer sellingprice
Ford Escape 2015 2 27802 230000
other 458 Italia 2015 5 12116 183000
Mercedes-Benz S-Class 2015 1 5277 173000
other Ghost 2015 3 7852 171500
other Ghost 2015 4 14316 169500

Giải thích câu lệnh: Câu lệnh này thực hiện các bước sau trên dataframe df_clean:

  • arrange(desc(sellingprice)): Sắp xếp dữ liệu theo giá bán sellingprice giảm dần, tức từ cao đến thấp.​

  • head(5): Lấy 5 dòng đầu tiên của dữ liệu đã sắp xếp, tức 5 xe có giá bán cao nhất.

  • select(make, model, sale_year, age, odometer, sellingprice): Chỉ giữ lại các cột này để hiển thị, gồm hãng xe, mẫu xe, năm bán, tuổi xe, km đã đi, và giá bán.

  • kable(caption = "5 xe có giá bán cao nhất"): Hiển thị bảng kết quả rõ ràng, có chú thích là “5 xe có giá bán cao nhất”.

Mục đích của câu lệnh này là để trực quan so sánh, xem xét 5 xe có giá bán cao nhất trong dữ liệu, giúp dễ nhìn nhận các đặc điểm của các xe này.

IV. Tổng hợp dữ liệu (Aggregating Data)

Sử dụng group_by()summarise() để tạo một bảng tổng hợp thống kê, ví dụ như tính giá bán trung bình và số lượng xe theo từng nguồn gốc.

make_summary <- df_clean |>
  group_by(make_origin) |>
  summarise(
    so_luong_xe = n(),
    gia_ban_trung_binh = mean(sellingprice),
    odometer_trung_binh = mean(odometer)
  ) |>
  arrange(desc(gia_ban_trung_binh))

kable(make_summary, 
      digits = 0, 
      format.args = list(big.mark = ","),
      caption = "Bảng tổng hợp theo nguồn gốc xe")
Bảng tổng hợp theo nguồn gốc xe
make_origin so_luong_xe gia_ban_trung_binh odometer_trung_binh
Đức 125 24,979 70,791
Khác 554,476 13,662 68,159
Nhật 773 6,657 124,667
Mỹ 1,565 5,696 124,744
Hàn Quốc 27 3,593 133,027

Giải thích câu lệnh: Câu lệnh này thực hiện tổng hợp dữ liệu xe hơi theo nguồn gốc hãng xe trong df_clean:

Kết quả: Xe Đức có giá bán trung bình cao nhất, tiếp theo là xe Mỹ. Xe Hàn Quốc có giá trung bình thấp nhất trong các nhóm chính.

4.1. Hoàn thiện và Xem lại Dữ liệu Cuối cùng

Sau tất cả các bước làm sạch và chuyển đổi, chúng ta có được bộ dữ liệu df_clean sẵn sàng cho các bước phân tích thống kê và trực quan hóa tiếp theo.

head(df_clean) |> kable(caption = "Bảng: 6 dòng đầu của dữ liệu cuối cùng (df_clean)")
Bảng: 6 dòng đầu của dữ liệu cuối cùng (df_clean)
sellingprice log_price age odometer condition mmr make make_origin model body transmission price_segment odometer_group condition_level sale_year sale_month state color interior seller
21500 9.975808 0 16639 5 20500 Kia Khác Sorento SUV automatic 3. Cao cấp ($15k - $30k) Rất mới Cần sửa chữa 2014 12 ca white black kia motors america inc
21500 9.975808 0 9393 5 20800 Kia Khác Sorento SUV automatic 3. Cao cấp ($15k - $30k) Rất mới Cần sửa chữa 2014 12 ca white beige kia motors america inc
30000 10.308953 2 1331 45 31900 BMW Khác 3 Series Sedan automatic 4. Hạng sang (>= $30k) Rất mới Rất tốt 2015 1 ca gray black financial services remarketing (lease)
27750 10.230991 1 14282 41 27500 Volvo Khác S60 Sedan automatic 3. Cao cấp ($15k - $30k) Rất mới Rất tốt 2015 1 ca white black volvo na rep/world omni
67000 11.112448 1 2641 43 66000 BMW Khác 6 Series Gran Coupe Sedan automatic 4. Hạng sang (>= $30k) Rất mới Rất tốt 2014 12 ca gray black financial services remarketing (lease)
10900 9.296518 0 5554 1 15350 Nissan Khác Altima Sedan automatic 2. Phổ thông ($5k - $15k) Rất mới Cần sửa chữa 2014 12 ca gray black enterprise vehicle exchange / tra / rental / tulsa

V: Thống kê và Tổng hợp Dữ liệu (Data Aggregation & Summarization)

Ở phần này, chúng ta sẽ thực hiện các phép thống kê mô tả và tổng hợp dữ liệu để rút ra những thông tin chi tiết. Các thao tác sẽ tuân thủ theo sơ đồ tư duy “Data Aggregation and Summarization”, bao gồm tính toán thống kê, nhóm dữ liệu bằng dplyr và tạo các bảng tần suất, bảng chéo.

5.1. Ngữ pháp của Đồ họa (Grammar of Graphics)

Hãy tưởng tượng bạn là một họa sĩ vẽ tranh:

  1. Lớp Nền tảng (Foundation): Bạn bắt đầu với một tấm toan trắng và chọn bộ màu sẽ dùng.

    • Trong ggplot2, đây là lệnh ggplot(). Bạn chỉ định dữ liệu (data) nào sẽ được dùng (bộ màu) và cách ánh xạ (mapping) các biến trong dữ liệu vào các thuộc tính thẩm mỹ (aes()) như trục X, trục Y, màu sắc, kích thước (chọn màu nào cho chi tiết nào).
  2. Lớp Hình học (Geometric Layers - geom): Bây giờ bạn quyết định sẽ vẽ cái gì lên tấm toan. Bạn sẽ vẽ các điểm chấm (geom_point), các đường thẳng (geom_line), hay các cột (geom_col)?

    • Bạn có thể thêm nhiều lớp hình học chồng lên nhau. Ví dụ, vẽ các điểm chấm trước, sau đó vẽ một đường xu hướng đè lên trên. Mỗi geom_…() là một lớp mới được thêm vào bằng dấu +.
  3. Các Lớp Tùy chỉnh khác: Sau khi đã có hình vẽ cơ bản, bạn bắt đầu hoàn thiện bức tranh.

    • Lớp Thang đo (scale): Tùy chỉnh cách các trục và màu sắc được hiển thị (ví dụ: định dạng trục Y thành đơn vị tiền tệ, thay đổi bảng màu).

    • Lớp Nhãn (labs): Thêm tiêu đề, chú thích, và tên cho các trục.

    • Lớp Facet: Chia tấm toan thành nhiều bức tranh nhỏ hơn, mỗi bức cho một nhóm dữ liệu con.

    • Lớp Theme: Thay đổi “khung tranh” và “màu tường” phía sau – tức là các yếu tố không liên quan đến dữ liệu như màu nền, đường lưới, phông chữ.

Mỗi lớp được thêm vào biểu đồ bằng toán tử +.

5.2. Thống kê mô tả cho các biến định lượng

Đầu tiên, chúng ta sẽ tính các chỉ số thống kê cơ bản (trung bình, trung vị, độ lệch chuẩn, min, max) cho các biến số quan trọng như sellingprice, age, và odometer.

# Chọn các cột số cần phân tích
numeric_vars <- df_clean |> select(sellingprice, age, odometer, mmr)

# Tính toán các chỉ số thống kê
summary_stats <- numeric_vars |>
  summarise(
    # 1. Trung bình (Mean)
    `Giá bán trung bình` = mean(sellingprice),
    # 2. Trung vị (Median)
    `Giá bán trung vị` = median(sellingprice),
    # 3. Độ lệch chuẩn (Standard Deviation)
    `Độ lệch chuẩn giá bán` = sd(sellingprice),
    # 4. Giá bán nhỏ nhất
    `Giá bán nhỏ nhất` = min(sellingprice),
    # 5. Giá bán lớn nhất
    `Giá bán lớn nhất` = max(sellingprice),
    # 6. Tuổi xe trung bình
    `Tuổi xe trung bình` = mean(age),
    # 7. Odometer trung bình
    `Odometer trung bình` = mean(odometer)
  ) |>
  # Chuyển đổi bảng từ dạng rộng sang dạng dài để dễ đọc hơn
  pivot_longer(cols = everything(), names_to = "Chi_so_thong_ke", values_to = "Gia_tri")

# Hiển thị bảng kết quả
kable(summary_stats, 
      digits = 2, 
      format.args = list(big.mark = ","),
      caption = "Bảng: Các chỉ số thống kê mô tả chính")
Bảng: Các chỉ số thống kê mô tả chính
Chi_so_thong_ke Gia_tri
Giá bán trung bình 13,631.78
Giá bán trung vị 12,100.00
Độ lệch chuẩn giá bán 9,733.95
Giá bán nhỏ nhất 125.00
Giá bán lớn nhất 230,000.00
Tuổi xe trung bình 5.85
Odometer trung bình 68,400.37

Giải thích câu lệnh: Câu lệnh này thực hiện tính các chỉ số thống kê cơ bản của dữ liệu, giúp người mới bắt đầu dễ hiểu như sau:

  1. numeric_vars <- df_clean |> select(sellingprice, age, odometer, mmr):

    • Chọn các cột chứa dữ liệu số cần phân tích là: giá bán, tuổi xe, số km đã đi và giá thị trường tham khảo (mmr) từ bảng df_clean.
  2. summarise(...):

    • Tính từng chỉ số thống kê cho từng biến:

      • Trung bình (mean) giá bán, tuổi xe, số km.

      • Trung vị (median) giá bán: giá trị ở giữa khi sắp xếp dữ liệu.

      • Độ lệch chuẩn (sd): đo độ phân tán của giá bán.

      • Giá nhỏ nhất (min) và lớn nhất (max) của giá bán.

  3. pivot_longer(...):

    • Chuyển bảng kết quả hiện tại từ dạng ngang (mỗi chỉ số là một cột) sang dạng dọc (hai cột: tên chỉ số và giá trị).

    • Giúp dễ đọc, dễ hiển thị.

  4. kable(...):

    • Hiển thị bảng kết quả ra màn hình hoặc file với định dạng đẹp mắt.

    • digits = 2: làm tròn giá trị đến 2 chữ số thập phân.

    • format.args = list(big.mark = ","): đặt dấu phẩy ngăn cách hàng nghìn.

    • caption: tiêu đề bảng.

Kết quả: Bảng trên cung cấp cái nhìn tổng quan về các biến số chính. Ví dụ, giá bán trung bình của một chiếc xe trong bộ dữ liệu này là khoảng 13,639 USD, với tuổi đời trung bình là 5.65 năm.

5.3. Bảng tần suất cho các biến định tính

Bảng tần suất giúp chúng ta hiểu được sự phân bố của các biến phân loại. Chúng ta sẽ tạo bảng tần suất để xem các hãng xe, loại thân xe, và nguồn gốc xe phổ biến nhất.

5.3.1. Top 10 hãng xe phổ biến nhất

Sử dụng count() để đếm số lượng xe cho mỗi hãng và head(10) để lấy 10 hãng hàng đầu.

# 8. Đếm số lượng xe theo hãng, sắp xếp giảm dần
top_10_makes <- df_clean |>
  count(make, sort = TRUE, name = "So_luong_xe") |>
  head(10)

kable(top_10_makes, caption = "Top 10 hãng xe phổ biến nhất")
Top 10 hãng xe phổ biến nhất
make So_luong_xe
Ford 93353
Chevrolet 60032
Nissan 53777
Toyota 39777
Dodge 30589
Honda 27090
Hyundai 21719
BMW 20644
Kia 18046
Chrysler 17224

Giải thích câu lệnh:

  • count(make, sort = TRUE, name = "So_luong_xe"): Đếm số lượng xe theo từng hãng (make), đồng thời sắp xếp kết quả số lượng giảm dần.

  • head(10): Lấy 10 hãng xe có số lượng cao nhất.

  • kable(top_10_makes, caption = "Top 10 hãng xe phổ biến nhất"): Hiển thị bảng kết quả với tiêu đề là “Top 10 hãng xe phổ biến nhất”.

Kết quả: Ford, Chevrolet, và Nissan là ba hãng xe chiếm số lượng lớn nhất trong bộ dữ liệu.

5.3.2. Phân bố xe theo nguồn gốc

# 9. Đếm số lượng xe theo nguồn gốc
origin_counts <- df_clean |>
  count(make_origin, sort = TRUE, name = "So_luong_xe")

kable(origin_counts, caption = "Phân bố số lượng xe theo nguồn gốc")
Phân bố số lượng xe theo nguồn gốc
make_origin So_luong_xe
Khác 554476
Mỹ 1565
Nhật 773
Đức 125
Hàn Quốc 27

Giải thích câu lệnh:

  • count(make_origin, sort = TRUE, name = "So_luong_xe"): Đếm số lượng xe theo nguồn gốc (make_origin), đồng thời sắp xếp theo số lượng giảm dần.

  • kable(origin_counts, caption = "Phân bố số lượng xe theo nguồn gốc"): Hiển thị bảng kết quả với tiêu đề “Phân bố số lượng xe theo nguồn gốc”.

Kết quả: Xe Mỹ chiếm ưu thế tuyệt đối về số lượng, theo sau là xe Nhật.

5.3.3. Phân bố xe theo tình trạng

# 10. Đếm số lượng xe theo từng mức độ tình trạng
condition_counts <- df_clean |>
  count(condition_level, sort = TRUE, name = "So_luong_xe")

kable(condition_counts, caption = "Phân bố số lượng xe theo tình trạng")
Phân bố số lượng xe theo tình trạng
condition_level So_luong_xe
Tốt 173267
Rất tốt 159826
Cần sửa chữa 112411
Trung bình 111462

Giải thích câu lệnh:

  • count(condition_level, sort = TRUE, name = "So_luong_xe"): Đếm số lượng xe theo từng mức độ tình trạng (condition_level), đồng thời sắp xếp số lượng giảm dần.

  • kable(condition_counts, caption = "Phân bố số lượng xe theo tình trạng"): Hiển thị bảng kết quả với tiêu đề “Phân bố số lượng xe theo tình trạng”.

5.4. Phân nhóm và Tổng hợp dữ liệu (Grouping & Summarizing)

Đây là phần cốt lõi của việc tổng hợp dữ liệu, sử dụng group_by() kết hợp với summarise() để tính toán các chỉ số thống kê cho từng nhóm cụ thể.

5.4.1. Giá bán trung bình theo nguồn gốc và tình trạng xe

Tính giá bán trung bình cho từng nhóm kết hợp giữa make_origincondition_level.

# Nhóm theo 2 biến, tính giá trung bình, số lượng và sắp xếp
price_by_origin_condition <- df_clean |>
  group_by(make_origin, condition_level) |>
  summarise(
    So_luong_xe = n(),
    Gia_trung_binh = mean(sellingprice)
  ) |>
  arrange(make_origin, desc(Gia_trung_binh))
## `summarise()` has grouped output by 'make_origin'. You can override using the
## `.groups` argument.
kable(price_by_origin_condition, 
      digits = 0, format.args = list(big.mark = ","),
      caption = "Giá bán trung bình theo nguồn gốc và tình trạng")
Giá bán trung bình theo nguồn gốc và tình trạng
make_origin condition_level So_luong_xe Gia_trung_binh
Hàn Quốc Rất tốt 3 8,400
Hàn Quốc Tốt 12 3,529
Hàn Quốc Trung bình 5 3,460
Hàn Quốc Cần sửa chữa 7 1,736
Khác Rất tốt 159,567 19,440
Khác Tốt 172,367 13,479
Khác Cần sửa chữa 111,682 10,026
Khác Trung bình 110,860 9,293
Mỹ Rất tốt 161 16,097
Mỹ Tốt 524 5,419
Mỹ Trung bình 341 4,770
Mỹ Cần sửa chữa 539 3,443
Nhật Rất tốt 63 11,429
Nhật Tốt 309 7,418
Nhật Trung bình 229 6,435
Nhật Cần sửa chữa 172 3,839
Đức Rất tốt 32 37,052
Đức Tốt 55 23,155
Đức Cần sửa chữa 11 23,155
Đức Trung bình 27 15,131

Giải thích câu lệnh:

  • group_by(make_origin, condition_level): Nhóm dữ liệu cùng lúc theo hai biến là nguồn gốc xe (make_origin) và mức độ tình trạng xe (condition_level).

  • summarise() tính cho từng nhóm:

    • So_luong_xe = n(): đếm số lượng xe.

    • Gia_trung_binh = mean(sellingprice): tính giá bán trung bình của xe.

  • arrange(make_origin, desc(Gia_trung_binh)): sắp xếp kết quả theo từng nguồn gốc (make_origin), bên trong mỗi nguồn gốc sắp xếp giá trung bình giảm dần.

  • kable() hiển thị bảng kết quả với số nguyên, dấu phẩy phân tách hàng nghìn, tiêu đề “Giá bán trung bình theo nguồn gốc và tình trạng”.

Kết quả: Trong mọi nhóm nguồn gốc, xe có tình trạng “Rất tốt” luôn có giá bán trung bình cao nhất. Xe Đức duy trì mức giá cao nhất ở mọi cấp độ tình trạng so với các nhóm khác.

5.4.2. Top 5 bang có giá xe trung bình cao nhất

# Lọc các bang có hơn 1000 xe để đảm bảo tính đại diện
top_states_price <- df_clean |>
  group_by(state) |>
  summarise(
    So_luong_xe = n(),
    Gia_trung_binh = mean(sellingprice)
  ) |>
  filter(So_luong_xe > 1000) |>
  arrange(desc(Gia_trung_binh)) |>
  head(5)

kable(top_states_price, digits = 0, caption = "Top 5 bang có giá xe trung bình cao nhất")
Top 5 bang có giá xe trung bình cao nhất
state So_luong_xe Gia_trung_binh
on 3424 17808
tn 20838 17026
pa 53809 15974
co 7771 15879
nv 12625 15155

Giải thích câu lệnh:

  • group_by(state): Nhóm dữ liệu theo bang (state).

  • summarise() tính cho mỗi bang:

    • So_luong_xe = n(): số lượng xe.

    • Gia_trung_binh = mean(sellingprice): giá bán trung bình.

  • filter(So_luong_xe > 1000): lọc chỉ lấy các bang có hơn 1000 xe, để đảm bảo dữ liệu đại diện.

  • arrange(desc(Gia_trung_binh)): sắp xếp danh sách theo giá bán trung bình giảm dần.

  • head(5): lấy 5 bang có giá xe trung bình cao nhất.

  • kable() hiển thị bảng kết quả với số nguyên, tiêu đề “Top 5 bang có giá xe trung bình cao nhất”.

5.4.3. Tương quan giữa các biến số

Tính toán hệ số tương quan để đo lường mức độ quan hệ tuyến tính giữa các biến số.

# 17.Tương quan giữa giá bán và odometer
cor_price_odo <- cor(df_clean$sellingprice, df_clean$odometer)
cat("Hệ số tương quan giữa Giá bán và Odometer:", round(cor_price_odo, 2), "\n")
## Hệ số tương quan giữa Giá bán và Odometer: -0.6
# 18. Tương quan giữa giá bán và tuổi xe
cor_price_age <- cor(df_clean$sellingprice, df_clean$age)
cat("Hệ số tương quan giữa Giá bán và Tuổi xe:", round(cor_price_age, 2), "\n")
## Hệ số tương quan giữa Giá bán và Tuổi xe: -0.58

Giải thích câu lệnh:

  • cor(df_clean$sellingprice, df_clean$odometer): Tính hệ số tương quan giữa giá bán (sellingprice) và số km đã đi (odometer). Kết quả lưu vào cor_price_odo.

  • cat(...): Hiển thị kết quả dưới dạng văn bản, làm tròn 2 chữ số.

  • Tương tự, cor_price_age là hệ số tương quan giữa giá bán và tuổi xe (age).

Hệ số tương quan mô tả mức độ liên quan tuyến tính giữa hai biến:

  • Giá trị gần 1 hoặc -1 nghĩa là quan hệ rất chặt chẽ (dương hoặc âm).

  • Giá trị gần 0 nghĩa là không có quan hệ tuyến tính.

Kết quả thường là âm trong trường hợp này, cho thấy xe đi nhiều km hoặc có tuổi xe cao thì giá bán có xu hướng giảm.

Kết quả: Cả hai hệ số tương quan đều âm, cho thấy rằng khi odometer (số dặm đã đi) và age (tuổi xe) tăng, sellingprice (giá bán) có xu hướng giảm. Mối quan hệ này mạnh hơn với tuổi xe.

5.5. Bảng chéo và Bảng Pivot (Cross-tabulation & Pivot Tables)

Bảng chéo giúp ta xem xét mối quan hệ giữa hai biến định tính. Chúng ta sẽ sử dụng count() kết hợp với pivot_wider() để tạo ra một bảng pivot.

5.5.1. Bảng chéo giữa Nguồn gốc xe và Phân khúc giá

# 19, 20. Tạo bảng chéo và chuyển sang dạng pivot
origin_price_pivot <- df_clean |>
  # Đếm số lượng kết hợp giữa hai biến
  count(make_origin, price_segment) |>
  # Chuyển đổi bảng từ dạng dài sang dạng rộng (pivot)
  pivot_wider(names_from = price_segment, values_from = n, values_fill = 0)

kable(origin_price_pivot, caption = "Bảng chéo: Số lượng xe theo Nguồn gốc và Phân khúc giá")
Bảng chéo: Số lượng xe theo Nguồn gốc và Phân khúc giá
make_origin 1. Giá rẻ (< $5k) 2. Phổ thông ($5k - $15k) 3. Cao cấp ($15k - $30k) 4. Hạng sang (>= $30k)
Hàn Quốc 19 8 0 0
Khác 99721 252353 171847 30555
Mỹ 982 438 135 10
Nhật 327 405 41 0
Đức 17 28 24 56

Giải thích câu lệnh:

  • count(make_origin, price_segment): Đếm số lượng xe theo sự kết hợp của hai biến là nguồn gốc hãng xe (make_origin) và phân khúc giá (price_segment).

  • pivot_wider(names_from = price_segment, values_from = n, values_fill = 0): Chuyển bảng đếm ở dạng dài sang dạng rộng (bảng pivot), trong đó các cột là các phân khúc giá, giá trị là số lượng xe trong từng phân khúc tương ứng với từng nguồn gốc. Nếu không có kết hợp nào, sẽ điền giá trị 0.

  • kable(origin_price_pivot, caption = "Bảng chéo: Số lượng xe theo Nguồn gốc và Phân khúc giá"): Hiển thị bảng kết quả với tiêu đề rõ ràng.

Kết quả: Bảng pivot cho thấy rõ cơ cấu phân khúc của từng dòng xe. Ví dụ, xe Đức có tỷ lệ xe trong phân khúc “Cao cấp” và “Hạng sang” cao hơn đáng kể so với các nhóm khác. Xe Mỹ và Nhật lại tập trung chủ yếu ở phân khúc “Phổ thông”.


VI: Trực quan hóa Dữ liệu (Data Visualization)

Phần này sẽ xây dựng các biểu đồ để minh họa các kết quả phân tích, tuân thủ theo sơ đồ tư duy “Basic Plotting” và “Advanced Plotting with ggplot2”. Mỗi biểu đồ sẽ được xây dựng theo “Ngữ pháp đồ họa” (Grammar of Graphics) và có ít nhất 5 lớp (layers) để đảm bảo tính thẩm mỹ và thông tin.

6.1. Khám phá Phân bố của các Biến số

Biểu đồ 1: Phân bố Giá bán (Histogram)

ggplot(df_clean, aes(x = sellingprice)) +
  geom_histogram(bins = 50, fill = "steelblue", color = "white") +
  scale_x_continuous(labels = scales::dollar_format(), limits = c(0, 75000)) +
  labs(
    title = "Phân bố Giá bán xe",
    x = "Giá bán (USD)",
    y = "Số lượng",
    caption = "Phân bố lệch phải, đa số xe có giá dưới $30,000"
  ) +
  theme_minimal()
## Warning: Removed 672 rows containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).

Giải thích câu lệnh: Câu lệnh này tạo biểu đồ histogram thể hiện phân bố của giá bán xe (sellingprice) trong dữ liệu df_clean.

  • ggplot(df_clean, aes(x = sellingprice)): Khởi tạo biểu đồ, ánh xạ biến giá bán lên trục X.

  • geom_histogram(bins = 50, fill = "steelblue", color = "white"): Vẽ biểu đồ cột phân chia dữ liệu thành 50 khoảng (bins), với màu xanh thép cho cột và viền trắng.

  • scale_x_continuous(labels = scales::dollar_format(), limits = c(0, 75000)): Định dạng trục X hiển thị dưới dạng tiền đô và giới hạn từ 0 đến 75,000 USD.

  • labs(...): Thêm tiêu đề, nhãn trục X, trục Y, và chú thích cho biểu đồ.

  • theme_minimal(): Áp dụng giao diện tối giản, giúp biểu đồ thanh thoát, dễ đọc.

Nhận xét: Biểu đồ histogram cho thấy phân bố của giá bán bị lệch phải, nghĩa là hầu hết các xe có giá trị tương đối thấp, và có một số ít xe có giá trị rất cao.

Biểu đồ 2: Phân bố Logarit của Giá bán (Histogram)

Để giảm độ lệch, ta thường vẽ histogram của logarit giá bán.

ggplot(df_clean, aes(x = log_price)) +
  geom_histogram(bins = 50, fill = "seagreen", color = "white") +
  geom_vline(aes(xintercept = mean(log_price)), color = "red", linetype = "dashed", size = 1) +
  labs(
    title = "Phân bố Logarit của Giá bán",
    x = "Log(Giá bán)",
    y = "Số lượng",
    caption = "Phân bố gần với phân phối chuẩn hơn"
  ) +
  theme_light()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Giải thích câu lệnh:

  1. Lớp nền tảng (ggplot(df_clean, aes(x = log_price))):

    • Khởi tạo biểu đồ trên dữ liệu df_clean.

    • Ánh xạ biến log_price (logarit của giá bán) lên trục X.

  2. Lớp hình học Histogram (geom_histogram(bins = 50, fill = "seagreen", color = "white")):

    • Vẽ biểu đồ histogram với 50 cột.

    • Các cột có màu xanh lá cây biển (seagreen) với viền trắng.

  3. Lớp đường thẳng đứng trung bình (geom_vline(aes(xintercept = mean(log_price)), color = "red", linetype = "dashed", size = 1)):

    • Vẽ một đường thẳng đứng ở vị trí giá trị trung bình của log_price.

    • Đường đỏ, nét đứt, độ dày 1.

  4. Lớp nhãn (labs(...)):

    • Thêm tiêu đề, nhãn trục X, Y và chú thích phía dưới biểu đồ.
  5. Lớp giao diện (theme_light()):

    • Sử dụng giao diện nền sáng, giúp biểu đồ dễ nhìn, chuyên nghiệp.

Biểu đồ 3: Phân bố Tuổi xe (Density Plot)

ggplot(df_clean, aes(x = age)) +
  geom_density(fill = "orange", alpha = 0.7) +
  scale_x_continuous(limits = c(0, 20)) +
  labs(
    title = "Mật độ phân bố Tuổi của xe",
    x = "Tuổi xe (năm)",
    y = "Mật độ"
  ) +
  theme_bw()
## Warning: Removed 1444 rows containing non-finite outside the scale range
## (`stat_density()`).

Giải thích câu lệnh:

  1. Lớp nền tảng (ggplot(df_clean, aes(x = age))):

    • Khởi tạo biểu đồ trên dữ liệu df_clean, ánh xạ biến age (tuổi xe) lên trục X.
  2. Lớp hình học mật độ (geom_density(fill = "orange", alpha = 0.7)):

    • Vẽ đường mật độ phân bố biến age.

    • Tô màu cam (orange) phần dưới đường mật độ với độ trong suốt 0.7 (70%).

  3. Lớp thang đo trục X (scale_x_continuous(limits = c(0, 20))):

    • Giới hạn giá trị trục X từ 0 đến 20 năm.

    • Giúp biểu đồ tập trung vào vùng tuổi xe phổ biến nhất.

  4. Lớp nhãn (labs(...)):

    • Thêm tiêu đề biểu đồ, nhãn trục X và trục Y.
  5. Lớp giao diện (theme_bw()):

    • Sử dụng giao diện nền trắng (black & white) tạo cảm giác rõ ràng, chuyên nghiệp.

Nhận xét:

6.2. So sánh và Phân tích Tần suất

Biểu đồ 4: Số lượng xe theo Nguồn gốc (Bar Plot)

df_clean |>
  count(make_origin) |>
  ggplot(aes(x = reorder(make_origin, -n), y = n, fill = make_origin)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = scales::comma(n)), vjust = -0.5) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Số lượng xe theo Nguồn gốc",
    x = "Nguồn gốc",
    y = "Số lượng xe"
  ) +
  theme_classic()

Giải thích câu lệnh:

  • count(make_origin): Đếm số xe theo từng nhóm make_origin.

  • ggplot(aes(x = reorder(make_origin, -n), y = n, fill = make_origin)): Khởi tạo biểu đồ cột, xắp xếp nguồn gốc theo số lượng giảm dần, màu sắc theo nguồn gốc.

  • geom_col(show.legend = FALSE): Vẽ các cột, ẩn chú giải màu.

  • geom_text(aes(label = scales::comma(n)), vjust = -0.5): Thêm nhãn số lượng trên đỉnh cột, định dạng có dấu phẩy.

  • scale_y_continuous(labels = scales::comma): Định dạng trục y có dấu phẩy ngăn cách hàng nghìn.

  • labs(...): Thêm tiêu đề, nhãn trục x và y.

  • theme_classic(): Giao diện biểu đồ cổ điển, đơn giản, dễ nhìn.

Nhận xét:

Biểu đồ 5: Tỷ lệ các loại Hộp số (Pie Chart)

df_clean |>
  count(transmission) |>
  mutate(prop = n / sum(n)) |>
  ggplot(aes(x = "", y = prop, fill = transmission)) +
  geom_bar(stat = "identity", width = 1, color = "white") +
  coord_polar("y", start = 0) +
  geom_text(aes(label = paste0(round(prop * 100), "%")), position = position_stack(vjust = 0.5)) +
  labs(title = "Tỷ lệ các loại hộp số", fill = "Loại hộp số", x = NULL, y = NULL) +
  theme_void()

Giải thích câu lệnh:

  • count(transmission): Đếm số lượng xe theo biến transmission (loại hộp số).

  • mutate(prop = n / sum(n)): Tính tỷ lệ phần trăm của mỗi loại hộp số so với tổng.

  • ggplot(aes(x = "", y = prop, fill = transmission)): Khởi tạo biểu đồ, x trục rỗng (vì là biểu đồ tròn), y là tỷ lệ phần trăm, màu theo loại hộp số.

  • geom_bar(stat = "identity", width = 1, color = "white"): Vẽ cột theo tỷ lệ, viền trắng.

  • coord_polar("y", start = 0): Chuyển đổi biểu đồ cột sang biểu đồ tròn (pie chart).

  • geom_text(...): Thêm nhãn phần trăm trên từng phần, đặt vị trí ở giữa lát cắt.

  • labs(...): Thêm tiêu đề, tên legend cho loại hộp số, bỏ nhãn trục x, y.

  • theme_void(): Giao diện tối giản không có trục, lưới.

Nhận xét: Hộp số tự động (automatic) chiếm đa số tuyệt đối trên thị trường xe đã qua sử dụng này.

6.3. Phân tích Mối quan hệ giữa các Biến

Biểu đồ 6: Mối quan hệ giữa Odometer và Giá bán (Scatter Plot)

df_clean |>
  sample_n(5000) |> # Lấy mẫu ngẫu nhiên 5000 điểm để vẽ cho nhanh
  ggplot(aes(x = odometer, y = sellingprice)) +
  geom_point(alpha = 0.3, color = "darkblue") +
  geom_smooth(method = "loess", color = "red") +
  scale_x_continuous(labels = scales::comma) +
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Giá bán giảm khi Odometer tăng",
    x = "Số dặm đã đi (Odometer)",
    y = "Giá bán"
  ) +
  theme_light()
## `geom_smooth()` using formula = 'y ~ x'

Nhận xét: Biểu đồ tán xạ cho thấy một xu hướng giảm rõ rệt: xe càng đi nhiều, giá bán càng thấp.

Giải thích từng Lớp (Layer):

  • df_clean |> sample_n(5000) |>

    • Lệnh: sample_n(5000)

    • Giải thích: Đây là bước tiền xử lý, không phải là một lớp của ggplot. Nó lấy ngẫu nhiên 5000 dòng từ bộ dữ liệu df_clean. Mục đích là để biểu đồ chạy nhanh hơn và các điểm không bị quá dày đặc, dễ quan sát hơn.

  • Lớp 1: Nền tảng ggplot(aes(x = odometer, y = sellingprice))

    • Lệnh: ggplot()

    • Giải thích: Lệnh này khởi tạo biểu đồ. Nó giống như việc đặt một tấm toan trống lên giá vẽ.

      • Đối số đầu tiên (df_clean sau khi đã sample) là dữ liệu sẽ được sử dụng.

      • Đối số thứ hai, aes(), là ánh xạ thẩm mỹ (aesthetic mapping). Nó thiết lập một quy tắc chung cho tất cả các lớp sau: “Hãy lấy biến odometer trong dữ liệu để đặt lên trục X và lấy biến sellingprice để đặt lên trục Y”.

  • Lớp 2: Hình học geom_point(alpha = 0.3, color = “darkblue”)

    • Lệnh: geom_point()

    • Giải thích: Đây là lớp hình học đầu tiên. Nó yêu cầu R: “Dựa trên quy tắc ánh xạ ở Lớp 1, hãy vẽ một điểm (point) cho mỗi dòng dữ liệu”.

      • alpha = 0.3: Thuộc tính này làm cho các điểm trở nên trong suốt (độ mờ 30%). Điều này hữu ích khi có nhiều điểm chồng chéo lên nhau.

      • color = “darkblue”: Thuộc tính này tô màu cho tất cả các điểm thành màu xanh đậm. Lưu ý: vì color được đặt bên ngoài aes(), nó sẽ áp dụng một màu cố định cho tất cả các điểm. Nếu bạn đặt color = make_origin bên trong aes(), R sẽ tô màu cho các điểm dựa trên biến make_origin.

  • Lớp 3: Hình học geom_smooth(method = “loess”, color = “red”)

    • Lệnh: geom_smooth()

    • Giải thích: Đây là lớp hình học thứ hai, được vẽ chồng lên lớp điểm. Nó yêu cầu R: “Hãy vẽ một đường cong làm mượt (smoothing line) để thể hiện xu hướng chung của dữ liệu”.

      • method = “loess”: Là một thuật toán để vẽ đường xu hướng phi tuyến tính, phù hợp khi mối quan hệ không phải là một đường thẳng hoàn hảo.

      • color = “red”: Tô màu cho đường xu hướng thành màu đỏ.

  • Lớp 4 & 5: Thang đo scale_x_continuous(…) & scale_y_continuous(…)

    • Lệnh: scale_…_continuous()

    • Giải thích: Các lớp này tùy chỉnh cách hiển thị của các trục X và Y.

      • labels = scales::comma: Định dạng các nhãn trên trục X để có dấu phẩy ngăn cách hàng nghìn (ví dụ: 100,000).

      • labels = scales::dollar: Định dạng các nhãn trên trục Y để có ký hiệu đô la và dấu phẩy (ví dụ: $20,000).

  • Lớp 6: Nhãn labs(…)

    • Lệnh: labs()

    • Giải thích: Lớp này dùng để gán nhãn cho các thành phần của biểu đồ.

      • title: Tiêu đề chính của biểu đồ.

      • x, y: Nhãn cho các trục tương ứng.

  • Lớp 7: Giao diện theme_light()

    • Lệnh: theme_light()

    • Giải thích: Lớp này thay đổi toàn bộ giao diện không liên quan đến dữ liệu của biểu đồ. theme_light() là một giao diện có sẵn với nền trắng và các đường lưới màu xám nhạt, trông chuyên nghiệp và dễ đọc.

Biểu đồ 7: Phân bố Giá bán theo Tình trạng xe (Box Plot)

ggplot(df_clean, aes(x = condition_level, y = sellingprice, fill = condition_level)) +
  geom_boxplot(show.legend = FALSE) +
  scale_y_log10(labels = scales::dollar) + # Dùng trục log để dễ nhìn hơn
  labs(
    title = "Phân bố Giá bán theo Tình trạng xe",
    x = "Tình trạng xe",
    y = "Giá bán (Trục Log)"
  ) +
  theme_minimal()

Giải thích câu lệnh:

  1. Lớp nền tảng (ggplot(df_clean, aes(x = condition_level, y = sellingprice, fill = condition_level))):

    • Khởi tạo biểu đồ trên dữ liệu df_clean.

    • Ánh xạ biến condition_level lên trục X, sellingprice lên trục Y.

    • Màu sắc của các hộp được xác định theo biến condition_level.

  2. Lớp hình học hộp (Boxplot) (geom_boxplot(show.legend = FALSE)):

    • Vẽ biểu đồ hộp thể hiện phân bố giá bán theo từng nhóm tình trạng xe.

    • Ẩn chú giải màu (legend).

  3. Lớp thang đo trục Y (scale_y_log10(labels = scales::dollar)):

    • Sử dụng trục logarit cho trục Y để dễ quan sát dữ liệu có độ lệch lớn.

    • Định dạng nhãn trục Y dưới dạng tiền đô.

  4. Lớp nhãn (labs(...)):

    • Thêm tiêu đề, nhãn trục X, trục Y.
  5. Lớp giao diện (theme_minimal()):

    • Sử dụng giao diện tối giản để biểu đồ rõ ràng, dễ đọc.

Nhận xét: Biểu đồ hộp cho thấy rõ ràng rằng xe có tình trạng tốt hơn (Rất tốt, Tốt) có mức giá bán trung vị cao hơn và khoảng giá cũng rộng hơn so với các xe ở tình trạng thấp hơn.

Biểu đồ 8: Giá bán trung bình theo Tuổi xe (Line Plot)

df_clean |>
  group_by(age) |>
  summarise(avg_price = mean(sellingprice)) |>
  filter(age <= 20) |>
  ggplot(aes(x = age, y = avg_price)) +
  geom_line(color = "red", size = 1.2) +
  geom_point(color = "darkred", size = 3) +
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Giá bán trung bình giảm dần theo Tuổi xe",
    x = "Tuổi xe (năm)",
    y = "Giá bán trung bình"
  ) +
  theme_gray()

Giải thích câu lệnh: Các lớp (layers) trong biểu đồ ggplot này gồm:

  1. Lớp nền (ggplot(df_clean, aes(x = age))):

    • Khởi tạo biểu đồ, ánh xạ biến age (tuổi xe) lên trục X.
  2. Lớp đường biểu diễn (geom_line(...)):

    • Vẽ đường biểu diễn xu hướng giá trung bình theo tuổi xe.

    • Màu đỏ, độ dày 1.2.

  3. Lớp điểm dữ liệu (geom_point(...)):

    • Thể hiện các điểm dữ liệu cụ thể. Màu đậm đỏ, size 3.
  4. Lớp định dạng trục (scale_y_continuous(labels = scales::dollar)):

    • Định dạng trục Y với dấu đô la, để rõ ràng về trị giá tiền tệ.
  5. Lớp nhãn (labs(...)):

    • Thêm tiêu đề, nhãn trục X, trục Y.
  6. Lớp chủ đề (theme_gray()):

    • Giao diện nền xám rõ ràng, dễ nhìn, phù hợp hiển thị rõ xu hướng giảm giá trung bình theo tuổi xe.

6.4. Phân tích Nâng cao với Faceting và Biểu đồ Đa biến

Biểu đồ 9: Phân bố Giá bán theo từng Nguồn gốc xe (Faceting Plots)

ggplot(df_clean, aes(x = sellingprice, fill = make_origin)) +
  geom_histogram(bins = 40, show.legend = FALSE) +
  scale_x_continuous(labels = scales::dollar, limits = c(0, 80000)) +
  facet_wrap(~ make_origin, scales = "free_y") + # Tạo biểu đồ con cho mỗi nguồn gốc
  labs(
    title = "Phân bố Giá bán được chia theo từng Nguồn gốc xe",
    x = "Giá bán",
    y = "Số lượng"
  ) +
  theme_bw()
## Warning: Removed 471 rows containing non-finite outside the scale range
## (`stat_bin()`).
## Warning: Removed 10 rows containing missing values or values outside the scale range
## (`geom_bar()`).

Nhận xét: Kỹ thuật faceting cho phép so sánh trực tiếp phân bố giá của các nhóm. Ta thấy rõ đỉnh phân bố của xe Đức dịch chuyển về phía giá cao hơn so với các nhóm còn lại.

Giải thcihs từng lớp (Layer):

  • Lớp 1 (Nền tảng): aes(x = sellingprice, fill = make_origin)

    • Ở đây, ngoài trục X, chúng ta còn ánh xạ biến make_origin vào thuộc tính fill (màu tô). Điều này có nghĩa là R sẽ tự động chọn một màu tô khác nhau cho mỗi giá trị của make_origin.
  • Lớp 2 (Hình học): geom_histogram(…)

    • Thay vì geom_point, chúng ta dùng geom_histogram. Lệnh này yêu cầu R: “Hãy chia dữ liệu trên trục X thành các khoảng (bins) và vẽ các cột có chiều cao tương ứng với số lượng quan sát trong mỗi khoảng”.

    • show.legend = FALSE: Ẩn đi chú giải màu sắc vì thông tin này đã được thể hiện rõ trong tiêu đề của từng biểu đồ con.

  • Lớp 4 (Facet): facet_wrap(~ make_origin, scales = “free_y”)

    • Đây là một lớp cực kỳ mạnh mẽ. Nó yêu cầu R: “Hãy chia biểu đồ thành nhiều biểu đồ con (facets), mỗi biểu đồ con chỉ chứa dữ liệu cho một giá trị của biến make_origin”.

    • scales = “free_y”: Cho phép mỗi biểu đồ con có thang đo trục Y riêng. Điều này rất hữu ích khi số lượng xe trong các nhóm chênh lệch nhiều (ví dụ, xe Mỹ có số lượng lớn hơn nhiều so với xe Hàn).

Biểu đồ 10: Tương quan MMR và Giá bán, tô màu theo Nguồn gốc

df_clean |>
  sample_n(2000) |>
  ggplot(aes(x = mmr, y = sellingprice, color = make_origin)) +
  geom_point(alpha = 0.7) +
  geom_abline(intercept = 0, slope = 1, color = "black", linetype = "dashed") + # Đường y = x
  scale_x_continuous(labels = scales::dollar) +
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Tương quan giữa Giá thị trường (MMR) và Giá bán thực tế",
    x = "Giá thị trường tham khảo (MMR)",
    y = "Giá bán thực tế",
    color = "Nguồn gốc"
  ) +
  theme_linedraw()

Nhận xét: Có một mối tương quan tuyến tính rất mạnh giữa mmrsellingprice, thể hiện qua các điểm dữ liệu bám sát đường chéo y=x. Điều này cho thấy MMR là một chỉ số tham khảo giá rất tốt.

Biểu đồ 11: Mối quan hệ giữa Tuổi xe và Odometer (Scatter Plot với màu sắc)

Biểu đồ này giúp xem xét liệu các xe cũ hơn có xu hướng đi nhiều hơn không, và liệu có sự khác biệt giữa các nguồn gốc xe hay không.

df_clean |>
  sample_n(5000) |>
  ggplot(aes(x = age, y = odometer, color = make_origin)) +
  geom_point(alpha = 0.6) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Odometer tăng theo Tuổi của xe",
    x = "Tuổi xe (năm)",
    y = "Số dặm đã đi (Odometer)",
    color = "Nguồn gốc xe"
  ) +
  theme_minimal()

Nhận xét: Có một xu hướng tuyến tính rõ ràng: xe càng cũ thì số dặm đã đi càng cao. Các điểm màu của xe Nhật (màu xanh lá) có vẻ tập trung dày đặc ở phần dưới, cho thấy có thể xe Nhật có odometer trung bình thấp hơn ở cùng một độ tuổi.

Biểu đồ 12: Phân bố giá của 4 hãng xe phổ biến nhất (Violin Plot)

Violin plot là sự kết hợp giữa box plot và density plot, cho thấy cả các chỉ số thống kê chính và hình dạng phân bố của dữ liệu.

# Lấy tên 4 hãng xe phổ biến nhất
top_4_makes <- df_clean |>
  count(make, sort = TRUE) |>
  head(4) |>
  pull(make)

df_clean |>
  filter(make %in% top_4_makes) |>
  ggplot(aes(x = make, y = sellingprice, fill = make)) +
  geom_violin(trim = FALSE, show.legend = FALSE) +
  geom_boxplot(width = 0.1, fill = "white", alpha = 0.5) +
  scale_y_log10(labels = scales::dollar) +
  labs(
    title = "Phân bố giá của 4 hãng xe phổ biến nhất",
    x = "Hãng xe",
    y = "Giá bán (Trục Log)"
  ) +
  theme_light()

Nhận xét: Biểu đồ violin cho thấy sự khác biệt trong phân bố giá. Ví dụ, giá xe Ford có phần “đuôi” kéo dài lên mức giá cao, trong khi Nissan và Honda có mật độ tập trung dày đặc hơn ở phân khúc giá thấp hơn.

Biểu đồ 13: Giá bán trung bình theo Loại thân xe (Bar Plot)

Biểu đồ này so sánh giá trị trung bình của các loại thân xe khác nhau.

df_clean |>
  group_by(body) |>
  summarise(avg_price = mean(sellingprice), count = n()) |>
  filter(count > 1000) |> # Chỉ lấy các loại thân xe phổ biến
  ggplot(aes(x = reorder(body, avg_price), y = avg_price)) +
  geom_col(fill = "purple", alpha = 0.8) +
  coord_flip() + # Lật trục để dễ đọc tên
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Giá bán trung bình theo Loại thân xe",
    x = "Loại thân xe",
    y = "Giá bán trung bình"
  ) +
  theme_gray()

Nhận xét: Các loại xe như Convertible (mui trần) và Coupe (2 cửa) có giá bán trung bình cao hơn đáng kể so với các loại xe phổ thông như Sedan hay SUV.

Biểu đồ 14: Số lượng xe theo Hãng và Loại hộp số (Faceted Bar Plot)

Biểu đồ này giúp so sánh tỷ lệ hộp số tự động và số sàn giữa các hãng xe lớn.

# Lấy 9 hãng xe phổ biến
top_9_makes <- df_clean |>
  count(make, sort = TRUE) |>
  head(9) |>
  pull(make)

df_clean |>
  filter(make %in% top_9_makes) |>
  ggplot(aes(y = make, fill = transmission)) +
  geom_bar(position = "fill") + # position="fill" để hiển thị tỷ lệ %
  scale_x_continuous(labels = scales::percent) +
  labs(
    title = "Tỷ lệ loại hộp số theo từng hãng xe",
    x = "Tỷ lệ",
    y = "Hãng xe",
    fill = "Loại hộp số"
  ) +
  theme_minimal()

Nhận xét: Hộp số tự động (automatic) chiếm ưu thế tuyệt đối ở tất cả các hãng.

Biểu đồ 15: Odometer trung bình theo năm sản xuất (Line Plot)

Biểu đồ này cho thấy xu hướng về quãng đường đã đi của xe theo từng năm sản xuất.

df_clean |>
  group_by(sale_year) |>
  summarise(avg_odo = mean(odometer)) |>
  ggplot(aes(x = sale_year, y = avg_odo)) +
  geom_line(color = "darkcyan", size = 1.5) +
  geom_point(shape = 21, color = "black", fill = "darkcyan", size = 4) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    title = "Odometer trung bình của xe được bán theo từng năm",
    x = "Năm bán xe",
    y = "Odometer trung bình"
  ) +
  theme_bw()

Biểu đồ 16: So sánh Giá thị trường (MMR) và Giá bán thực tế theo Nguồn gốc (Box Plot)

Biểu đồ này so sánh sự chênh lệch giữa giá bán và giá tham chiếu (MMR) cho từng nguồn gốc xe.

df_clean <- df_clean |>
  mutate(price_diff_ratio = (sellingprice - mmr) / mmr)

ggplot(df_clean, aes(x = make_origin, y = price_diff_ratio, fill = make_origin)) +
  geom_boxplot(show.legend = FALSE, outlier.alpha = 0.1) +
  scale_y_continuous(labels = scales::percent, limits = c(-0.5, 0.5)) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  labs(
    title = "Tỷ lệ chênh lệch giữa Giá bán và Giá MMR",
    subtitle = "Giá trị dương nghĩa là bán được giá cao hơn MMR",
    x = "Nguồn gốc xe",
    y = "Tỷ lệ chênh lệch giá (%)"
  ) +
  theme_light()
## Warning: Removed 23272 rows containing non-finite outside the scale range
## (`stat_boxplot()`).

Nhận xét: Hầu hết các xe đều được bán với giá rất gần với giá tham chiếu MMR (đường trung vị của các hộp rất gần với 0%). Xe Đức và xe Mỹ có vẻ có khoảng biến động giá rộng hơn.

Biểu đồ 17: Phân bố số lượng xe theo Màu sắc (Bar Plot)

df_clean |>
  count(color) |>
  top_n(10, n) |>
  ggplot(aes(x = reorder(color, n), y = n, fill = color)) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  labs(
    title = "Top 10 màu xe phổ biến nhất",
    x = "Màu sắc",
    y = "Số lượng"
  ) +
  theme_classic()

Nhận xét: Các màu trung tính như Đen, Trắng, Bạc và Xám là những màu phổ biến nhất trên thị trường.

Biểu đồ 18: Heatmap về Giá bán trung bình theo Hãng và Loại thân xe

Heatmap là một cách hiệu quả để trực quan hóa giá trị trong một bảng hai chiều.

df_clean |>
  filter(make %in% top_9_makes) |> # Sử dụng lại top 9 hãng
  group_by(make, body) |>
  summarise(avg_price = mean(sellingprice), .groups = "drop") |>
  filter(!is.na(avg_price)) |>
  ggplot(aes(x = make, y = body, fill = avg_price)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(labels = scales::dollar) + # Sử dụng bảng màu thân thiện với người mù màu
  labs(
    title = "Giá bán trung bình theo Hãng và Loại thân xe",
    x = "Hãng xe",
    y = "Loại thân xe",
    fill = "Giá trung bình"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Nhận xét: Heatmap cho thấy các ô màu đậm (giá cao) thường tập trung ở các dòng xe BMW, Infiniti, và Mercedes-Benz, đặc biệt là ở các loại thân xe Coupe, ConvertibleSUV.

Biểu đồ 19: Sự thay đổi của giá trị thị trường (MMR) theo tuổi xe

df_clean |>
  group_by(age) |>
  summarise(avg_mmr = mean(mmr)) |>
  filter(age <= 20) |>
  ggplot(aes(x = age, y = avg_mmr)) +
  geom_area(fill = "lightblue", alpha = 0.5) +
  geom_line(color = "darkblue", size = 1) +
  scale_y_continuous(labels = scales::dollar) +
  labs(
    title = "Giá trị thị trường (MMR) giảm dần theo Tuổi xe",
    x = "Tuổi xe (năm)",
    y = "MMR trung bình"
  ) +
  theme_bw()

Nhận xét: Tương tự như giá bán, giá trị tham chiếu MMR cũng giảm mạnh trong những năm đầu và giảm chậm hơn khi xe đã cũ.

Biểu đồ 20: Biểu đồ Ridgeline về phân bố Odometer theo Phân khúc giá

Ridgeline plot là một cách trực quan hóa hấp dẫn để so sánh sự phân bố của một biến số giữa các nhóm khác nhau.

# Cần cài đặt và nạp thư viện ggridges
library(ggridges)

ggplot(df_clean, aes(x = odometer, y = price_segment, fill = price_segment)) +
  geom_density_ridges(alpha = 0.8, show.legend = FALSE) +
  scale_x_continuous(labels = scales::comma, limits = c(0, 250000)) +
  labs(
    title = "Phân bố Odometer theo từng Phân khúc giá",
    x = "Số dặm đã đi (Odometer)",
    y = "Phân khúc giá"
  ) +
  theme_ridges()

Nhận xét: Biểu đồ cho thấy một xu hướng rất rõ ràng: các xe ở phân khúc giá cao hơn (Hạng sang, Cao cấp) có xu hướng có số dặm đã đi thấp hơn, với đỉnh phân bố lệch về phía bên trái. Ngược lại, xe ở phân khúc “Giá rẻ” có phân bố trải rộng hơn về phía số dặm cao.

CHƯƠNG 2. Ngân hàng thương mại cổ phần Quân Đội (MBB)

2.1. Nạp thư viện

# Gọi thư viện
Sys.setlocale("LC_CTYPE", "en_US.UTF-8") # thiết lập ngôn ngữ tiếng việt
## [1] "en_US.UTF-8"
library(readxl)                          # đọc file exel  
library(dplyr)                           # xử lý và biến đổi dữ liệu  
library(kableExtra)                      # tạo và trình bày bảng đẹp
library(tidyr)           # Dọn dẹp dữ liệu” — chuyển đổi giữa dữ liệu dạng rộng và dài (pivot_longer, pivot_wider).
library(jsonlite)                        # Đọc và ghi file JSON, thường dùng khi dữ liệu lấy từ web hoặc API.
library(stringr)                         # Xử lý chuỗi ký tự dễ dàng hơn (cắt, nối, đổi chữ hoa/thường,...).
library(knitr)                           # Dùng để tạo báo cáo động
library(ggplot2)                         # Vẽ đồ thị và biểu đồ thống kê
library(ggcorrplot)                      #Vẽ biểu đồ ma trận tương quan
library(broom)                           # “Gọn gàng hóa” kết quả mô hình thống kê (biến output phức tạp thành bảng dữ liệu dễ đọc).
library(tidyverse)                       # Bộ công cụ tổng hợp gồm nhiều gói như ggplot2, dplyr, tidyr, readr, stringr,... giúp thao tác dữ liệu hiệu quả hơn.

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

2.2.1.Nhập sơ dữ liệu

Nạp dữ liệu

# Nạp dữ liệu
bctc <- read_excel("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
# 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.2.2. Kiểm tra dữ liệu

# 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.2.3.Tổng quan 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 <- data.frame(
  STT = 1:length(names(bctc)),
  Bien = names(bctc),
  Kieu_du_lieu = sapply(bctc, class),
  stringsAsFactors = FALSE
)
# In bảng ra, tối ưu cho khổ A4 dọc
bctc_types %>%
  kbl(
    caption = "Kiểu dữ liệu",
    booktabs = TRUE,
    longtable = FALSE,
    align = "lcl"
  ) %>%
  kable_paper(full_width = FALSE) %>%
  column_spec(2, width = "8cm", extra_css = "word-wrap:break-word; white-space:pre-wrap;") %>% # Giới hạn độ rộng cột tên biến
  column_spec(3, width = "3cm") %>%
  kable_styling(
    latex_options = c("hold_position", "scale_down"),
    font_size = 11
  )
Kiểu dữ liệu
STT Bien Kieu_du_lieu
Year 1 Year factor
Tổng nợ 2 Tổng nợ numeric
VCSH 3 VCSH numeric
Tổng tài sản 4 Tổng tài sản numeric
Cho vay khách hàng 5 Cho vay khách hàng numeric
Tiền gửi khách hàng 6 Tiền gửi khách hàng numeric
Thu nhập lãi thuần 7 Thu nhập lãi thuần numeric
Lãi thuần từ hoạt động dịch vụ 8 Lãi thuần từ hoạt động dịch vụ numeric
Lãi thuần từ kinh doanh ngoại hối và vàng 9 Lãi thuần từ kinh doanh ngoại hối và vàng numeric
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 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
Thu nhập thuần từ hoạt động khác 11 Thu nhập thuần từ hoạt động khác numeric
Thu nhập từ góp vốn, mua cổ phần 12 Thu nhập từ góp vốn, mua cổ phần numeric
EPS 13 EPS numeric
Lợi nhuận sau thuế 14 Lợi nhuận sau thuế numeric
Chi phí hoạt động 15 Chi phí hoạt động numeric
Kinh doanh 16 Kinh doanh numeric
Đầu tư 17 Đầu tư numeric
Tài chính 18 Tài chính numeric
Lưu chuyển tiền thuần trong năm 19 Lưu chuyển tiền thuần trong năm numeric
# Tạo bảng dữ liệu
goi_y_bien <- data.frame(
  Nhóm = c("Quy mô", "Hiệu quả hoạt động", "Dòng tiền", "Tín dụng (nếu là ngân hàng)"),
  `Biến đề xuất` = c(
    "Tổng tài sản, Tổng nợ, VCSH",
    "Lợi nhuận sau thuế, Chi phí hoạt động, EPS",
    "Lưu chuyển tiền thuần trong năm",
    "Cho vay khách hàng, Tiền gửi khách hàng"
  ),
  `Ý nghĩa` = c(
    "Phản ánh quy mô và cấu trúc tài chính",
    "Đánh giá khả năng sinh lời",
    "Thể hiện khả năng tạo dòng tiền",
    "Liên quan đến hoạt động cốt lõi"
  )
)

# Hiển thị bảng đẹp
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 = 12)
Ý 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
# Chọn vài biến quan trọng để thống kê
data_select <- bctc %>%
  select(`Tổng tài sản`, `Tổng nợ`, `VCSH`, 
         `Lợi nhuận sau thuế`, `Chi phí hoạt động`, 
         `Lưu chuyển tiền thuần trong năm`, `EPS`)
# Tính thống kê mô tả
thongke_mota <- data_select %>%
  summarise(across(
    everything(),
    list(
      `Nhỏ nhất` = ~min(., na.rm = TRUE),
      `Q1` = ~quantile(., 0.25, na.rm = TRUE),
      `Trung bình` = ~mean(., na.rm = TRUE),
      `Trung vị` = ~median(., na.rm = TRUE),
      `Q3` = ~quantile(., 0.75, na.rm = TRUE),
      `Lớn nhất` = ~max(., na.rm = TRUE),
      `Độ lệch chuẩn` = ~sd(., na.rm = TRUE)
    ),
    .names = "{.col}_{.fn}"
  )) %>%
  pivot_longer(
    cols = everything(),
    names_to = c("Biến", "Chỉ tiêu thống kê"),
    names_sep = "_",
    values_to = "Giá trị"
  ) %>%
  pivot_wider(names_from = "Chỉ tiêu thống kê", values_from = "Giá trị")

# Hiển thị bảng đẹp, vừa trang A4
thongke_mota %>%
  kbl(
    caption = " Thống kê mô tả một số biến tài chính quan trọng",
    digits = 2,
    booktabs = TRUE,
    longtable = FALSE,
    align = "lccccccc"
  ) %>%
  kable_paper(full_width = FALSE, lightable_options = "striped") %>%
  column_spec(1, width = "3.5cm", extra_css = "word-wrap:break-word; white-space:pre-wrap;") %>%  # Cột biến
  column_spec(2:8, width = "1.8cm") %>%  # Cột số liệu
  kable_styling(
    latex_options = c("hold_position", "scale_down"),
    position = "center",
    font_size = 11
  )
Thống kê mô tả một số biến tài chính quan trọng
Biến Nhỏ nhất Q1 Trung bình Trung vị Q3 Lớn nhất Độ lệch chuẩn
Tổng tài sản 200489173 338101445.0 606353617.00 494982162 836743007 1256258500 365180137.11
Tổng nợ 183340962 256973356.0 735759160.00 444882667 748580818 3371601761 915737632.92
VCSH 17148212 28094808.0 52413548.00 39885814 71049621 117059581 32807152.99
Lợi nhuận sau thuế 2502987 3186983.0 9966846.27 8068604 15688311 22951264 7695949.07
Chi phí hoạt động -17007250 -13596408.0 -9533019.09 -9723706 -5086954 -3114202 4918718.22
Lưu chuyển tiền thuần trong năm -32869438 -270898.0 9606048.91 5637401 11953145 80560405 27436501.74
EPS 1625 2044.5 2772.55 2993 3380 3856 797.98

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

2.2.1 Đặt lại tên các biến

# ============================================================
#  HÀM DỊCH TIẾNG VIỆT → TIẾNG ANH (Google Translate API)
# ============================================================
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(fromJSON(url), error = function(e) NULL)
    if (is.null(res)) return(NA_character_)
    if (!is.list(res) || length(res) < 1 || !is.list(res[[1]]) || length(res[[1]]) < 1)
      return(NA_character_)
    out <- res[[1]][[1]][[1]]
    if (length(out) > 1) out <- out[1]
    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)
}

# ============================================================
#  XỬ LÝ DỮ LIỆU – DỊCH VÀ TẠO BẢNG
# ============================================================
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")  # có thể thêm tên cột bạn muốn giữ nguyên

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)

# Nếu lỗi dịch → giữ nguyên tiếng Việt
cols_en[is.na(cols_en) | cols_en == ""] <- cols_vi[is.na(cols_en) | cols_en == ""]
cols_short[is.na(cols_short) | cols_short == ""] <- cols_vi[is.na(cols_short) | cols_short == ""]

# ============================================================
#  LÀM SẠCH & GỘP DỮ LIỆU
# ============================================================
min_len <- min(length(cols_vi), length(cols_en), length(cols_short))
cols_vi <- cols_vi[1:min_len]
cols_en <- cols_en[1:min_len]
cols_short <- cols_short[1:min_len]

valid_rows <- !(is.na(cols_vi) | is.na(cols_en) | is.na(cols_short))
cols_vi <- cols_vi[valid_rows]
cols_en <- cols_en[valid_rows]
cols_short <- cols_short[valid_rows]

# Gộp lại thành dataframe
vars_map <- data.frame(
  Vietnamese = cols_vi,
  English = cols_en,
  ShortName = cols_short,
  stringsAsFactors = FALSE
)
rownames(vars_map) <- seq_len(nrow(vars_map))

# ============================================================
# ĐỔI TÊN BIẾN TRONG DATA GỐC
# ============================================================
names(bctc) <- vars_map$ShortName

# ============================================================
# HIỂN THỊ BẢNG DỊCH (ĐẸP, VỪA KHỔ A4)
# ============================================================
vars_map %>%
  mutate(
    Vietnamese = str_wrap(Vietnamese, width = 40),
    English = str_wrap(English, width = 40),
    ShortName = str_trim(ShortName)
  ) %>%
  kbl(
    caption = "Bảng 3. Danh sách biến, bản dịch tiếng Anh và viết tắt",
    booktabs = TRUE,
    align = c("l", "l", "c"),
    escape = FALSE
  ) %>%
  kable_paper(full_width = FALSE, lightable_options = "striped") %>%
  kable_styling(
    bootstrap_options = c("hover", "condensed"),
    font_size = 11,
    position = "center",
    latex_options = c("hold_position", "scale_down")
  ) %>%
  column_spec(1, width = "7cm", extra_css = "word-wrap:break-word; white-space:pre-wrap;") %>%
  column_spec(2, width = "7cm", extra_css = "word-wrap:break-word; white-space:pre-wrap;") %>%
  column_spec(3, width = "2cm", bold = TRUE, background = "#F7F7F7")
Bảng 3. 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.2. 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>Bảng kết quả</b>") %>%
  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.3 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.3.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.3.2. Phân tích rủi ro tài chính (DE) ### 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ả.

Xu huướ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
## `geom_smooth()` using formula = 'y ~ x'
## `geom_line()`: Each group consists of only one observation.
## ℹ Do you need to adjust the group aesthetic?

Giải thích cấu trúc biểu đồ - geom_line() đường nối xu hướng qua các năm.

  • geom_point() điểm đánh dấu từng năm.

  • geom_text() hiển thị nhãn giá trị D/E trên mỗi điểm.

  • geom_smooth() đường hồi quy mượt để nhận biết xu hướng chung.

  • theme_minimal() + labs() bố cục và tiêu đề giúp biểu đồ rõ ràng, chuyên nghiệp
    Nhận xét

  • Tỷ lệ D/E của MB Bank duy trì ổn định quanh mức 8–10 lần trong hầu hết các năm, thể hiện cấu trúc vốn an toàn và chính sách quản lý nợ hợp lý.

  • Năm 2019, tỷ lệ D/E tăng đột biến lên 84,53 lần, cho thấy ngân hàng có thể đã tăng vay nợ ngắn hạn hoặc thay đổi cơ cấu vốn để đáp ứng nhu cầu vốn tạm thời — đây là điểm bất thường cần được kiểm tra thêm.

  • Sau năm 2019, tỷ lệ D/E giảm mạnh trở lại mức dưới 9 lần và ổn định dần đến 2024, phản ánh xu hướng kiểm soát nợ tốt hơn, hướng đến an toàn tài chính và giảm rủi ro đòn bẩy.

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

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

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.

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.3.3. Phân tích hiệu quả hoạt động ### Thống kê mô tả

#  Mô tả thống kê cơ bản ---------------------------------------------------
desc_eff <- bctc %>%
  summarise(across(c(ROE, ROA, NIM),
                   list(min = min, mean = mean, median = median,
                        max = max, sd = sd), .names = "{.col}_{.fn}"))

desc_eff %>%
  t() %>%
  as.data.frame() %>%
  rename(Giá_trị = V1) %>%
  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)
Giá_trị
ROE_min 0.1080000
ROE_mean 0.1717273
ROE_median 0.1810000
ROE_max 0.2280000
ROE_sd 0.0449357
ROA_min 0.1080000
ROA_mean 0.1717273
ROA_median 0.1810000
ROA_max 0.2280000
ROA_sd 0.0449357
NIM_min 0.0080000
NIM_mean 0.1030000
NIM_median 0.0650000
NIM_max 0.5340000
NIM_sd 0.1442096

Giải thích - summarise(across(…)): Tính các đặc trưng thống kê (Min, Mean, Median, Max, SD) cho từng biến ROE, ROA, NIM.

  • pivot_longer(): Biến bảng ngang thành dọc để trình bày gọn.

  • kable() + kable_styling(): Tạo bảng chuyên nghiệp trong R Markdown.
    Ý nghĩa phân tích
    Bảng mô tả giúp đánh giá hiệu quả sinh lời (ROE, ROA) và biên lợi nhuận (NIM) qua thời gian.

  • ROE/ROA trung bình phản ánh hiệu quả vốn và tài sản.

  • NIM thể hiện khả năng quản lý lãi suất đầu ra – đầu vào.

  • Độ lệch chuẩn (SD) nhỏ cho thấy biến động thấp, ổn định tài chính cao.

So sánh 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.

# 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() → tinh chỉnh kiểu hiển thị cho phù hợp khổ A4, cỡ chữ vừa phải.

# Vẽ biểu đồ tăng trưởng hằng năm
ggplot(growth_long, aes(x = Y, y = Tăng_trưởng, color = Chỉ_số)) +
  geom_line(linewidth = 1.1, na.rm = TRUE) +                        # Layer 1
  geom_point(size = 2.5, na.rm = TRUE) +                            # Layer 2
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") + # Layer 3
  geom_text(
    aes(label = round(Tăng_trưởng, 1)),
    vjust = -0.6, size = 3, na.rm = TRUE
  ) +                                                               # Layer 4
  labs(
    title = "Tốc độ tăng trưởng hằng năm của ROE, ROA, NIM",
    subtitle = "Tỷ lệ % thay đổi so với năm trước",
    x = "Năm", y = "Tăng trưởng (%)",
    caption = "Lưu ý: Năm đầu tiên được quy ước tăng trưởng = 0 do không có dữ liệu năm trước."
  ) +                                                               # Layer 5
  theme_minimal(base_size = 13)

Giải thích cấu trúc biểu đồ
- Trục hoành (x – Năm): thể hiện các năm trong giai đoạn 2014–2024.

  • Trục tung (y – Tăng trưởng %): biểu thị tốc độ tăng trưởng hằng năm của từng chỉ số, được tính theo phần trăm thay đổi so với năm trước.

  • Đường đứt (y=0): đường tham chiếu — giá trị trên 0 thể hiện tăng trưởng dương, dưới 0 là suy giảm.

  • Các điểm dữ liệu & nhãn giá trị: cho thấy mức tăng/giảm cụ thể từng năm, giúp dễ dàng nhận biết các biến động lớn.

Nhận xét biểu đồ
- Biểu đồ cho thấy tốc độ tăng trưởng của các chỉ số tài chính biến động mạnh qua thời gian, đặc biệt là NIM có hai đột biến lớn (trên 700%) vào các năm 2018 và 2020 — có thể do sự thay đổi chính sách tín dụng hoặc cơ cấu tài sản sinh lãi.

  • Trong khi đó, ROE và ROA duy trì mức biến động nhẹ, phản ánh hiệu quả sinh lời của MB Bank ổn định hơn và ít chịu tác động ngắn hạn.

  • Giai đoạn sau 2020, các chỉ số dần hội tụ về mức tăng trưởng thấp và ổn định, cho thấy doanh nghiệp đã đi vào giai đoạn ổn định tài chính sau giai đoạn điều chỉnh mạnh.

  • Biểu đồ giúp làm rõ xu hướng biến động hiệu quả hoạt động tài chính qua các năm, từ đó đánh giá tính bền vững và khả năng thích ứng của MB Bank trước thay đổi kinh tế.

Biểu đồ 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.

Độ ổ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.

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

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())
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the ggcorrplot package.
##   Please report the issue at <https://github.com/kassambara/ggcorrplot/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

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

# ⚙️ 1️⃣ Xây dựng mô hình hồi quy
model1 <- lm(ROE ~ DE, data = bctc)

# 🧮 2️⃣ 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 = "Bảng 1. Kết quả hồi quy đơn: Mối quan hệ giữa ROE và DE",
    align = "c"
  )
Bảng 1. 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
# 🎨 3️⃣ 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")
)
## `geom_smooth()` using formula = 'y ~ x'

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

Hồi quy đa biến

model2 <- lm(ROE ~ DE + NIM + ROA, data = bctc)

broom::tidy(model2) %>%
  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"
  ) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
## Warning in summary.lm(x): essentially perfect fit: summary may be unreliable
Kết quả hồi quy đa biến: ROE ~ DE + NIM + ROA
term estimate std.error statistic p.value Signif
(Intercept) 0 0 2.205500e+00 0.0632 .
DE 0 0 6.519000e-01 0.5353
NIM 0 0 3.074000e-01 0.7675
ROA 1 0 1.229989e+16 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)
## Warning in summary.lm(x): essentially perfect fit: summary may be unreliable
## Warning in summary.lm(x): essentially perfect fit: summary may be unreliable
## Warning in summary.lm(x): essentially perfect fit: summary may be unreliable
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)
## Warning in summary.lm(x): essentially perfect fit: summary may be unreliable
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.

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) +
  theme(legend.position = "right")
## `geom_smooth()` using formula = 'y ~ x'

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.

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)
## `geom_smooth()` using formula = 'y ~ x'

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 ROE cao hơn → đòn bẩy tài chính chưa mang lại hiệu quả sinh lời rõ rệt.

summary_stats <- bctc %>%
  summarise(across(c(DE, ROE, ROA, NIM),
                   list(Min = min, Mean = mean, Max = max),
                   .names = "{.col}_{.fn}"))

summary_stats %>%
  knitr::kable(caption = "Tóm tắt thống kê mô tả các chỉ tiêu tài chính (2014–2024)") %>%
  kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
Tóm tắt thống kê mô tả các chỉ tiêu tài chính (2014–2024)
DE_Min DE_Mean DE_Max ROE_Min ROE_Mean ROE_Max ROA_Min ROA_Mean ROA_Max NIM_Min NIM_Mean NIM_Max
8.151 15.88764 84.531 0.108 0.1717273 0.228 0.108 0.1717273 0.228 0.008 0.103 0.534

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)
## Warning: Removed 3 rows containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 3 rows containing missing values or values outside the scale range
## (`geom_point()`).

Giải thích cấu trúc biểu đồ
- Trục X: Năm (Y).

  • Trục Y: Tỷ lệ tăng trưởng (%) so với năm trước.

  • Đường gạch ngang tại 0%: Mốc phân biệt giữa tăng trưởng dương và âm.

  • geom_line() + geom_point(): Hiển thị xu hướng và các điểm biến động của từng chỉ tiêu qua thời gian.
    Nhận xét biểu đồ

  • NIM (biên lãi ròng) biến động mạnh, có các đỉnh tăng đột biến (≈ 2018, 2020) → phản ánh biến động mạnh về lợi nhuận lãi thuần, có thể do thay đổi trong lãi suất thị trường hoặc cơ cấu cho vay.

  • ROE và ROA có xu hướng ổn định hơn, dao động quanh mức tăng trưởng nhỏ (±20%) → cho thấy hiệu quả sinh lời của doanh nghiệp tương đối bền vững.

  • Sau năm 2020, cả ba chỉ tiêu đều giảm và tiến gần 0, chứng tỏ hiệu quả sinh lời chững lại trong giai đoạn gần đây.

-> Nhìn chung, mô hình tài chính có xu hướng tăng mạnh trong ngắn hạn nhưng chưa ổn định dài hạn, đặc biệt chịu ảnh hưởng lớn từ NIM.
### Phân tích tương quan toàn diện (Pair Plot)

#  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)
})
## Warning: package 'GGally' was built under R version 4.4.3

Giải thích cấu trúc biểu đồ
- Đường chéo (Diagonal): Thể hiện phân phối (density) của từng biến.

  • Phần trên (Upper): Hiển thị hệ số tương quan (Corr) giữa các cặp biến.

  • Hệ số gần 1 → tương quan dương mạnh.

  • Gần -1 → tương quan âm mạnh.

  • Gần 0 → không có tương quan đáng kể.

  • Phần dưới (Lower): Các biểu đồ hồi quy tuyến tính mượt (smooth) thể hiện xu hướng quan hệ giữa hai biến.

  • Màu nền xám và vùng mờ: Biểu thị độ tin cậy của đường hồi quy.
    Nhận xét biểu đồ

  • ROE và ROA có tương quan dương rất mạnh (≈ 1.000***), cho thấy hai chỉ số này gần như song hành — khi hiệu quả sinh lời tài sản tăng, hiệu quả vốn chủ cũng tăng tương ứng.

  • D/E (đòn bẩy tài chính) có tương quan dương yếu với ROE và ROA (≈ 0.21) → mức nợ cao có thể giúp tăng sinh lời, nhưng chưa rõ rệt.

  • NIM (biên lãi ròng) tương quan âm nhẹ với D/E (-0.21) và gần như không tương quan với ROE, ROA (≈ 0.07) → chứng tỏ biến động lãi suất hoặc chi phí huy động vốn không ảnh hưởng trực tiếp đến khả năng sinh lời tổng thể.

Nhìn chung, ROA là biến giải thích chính cho ROE, trong khi D/E và NIM chỉ đóng vai trò bổ trợ yếu trong mô hình tài chính này.

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

# 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

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.