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
)
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.
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, knitr và
kableExtra 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
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.
dim() và 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.
head() và
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*")
| 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*")
| 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.
Để 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")
| 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.
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.
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ô.
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ý.
Để 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.
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.
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.
saledateBiế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.
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")
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.
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).
age)Từ các cột year và saledate, 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ô
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.
price_segment) và biến đổi
logChú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”.
odometerSử 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”.
conditionTươ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”.
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ỹ.
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
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"
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")
| 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.
Sử dụng group_by() và 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")
| 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:
group_by(make_origin): Nhóm dữ liệu
theo cột make_origin (nguồn gốc hãng
xe).
summarise() tính cho mỗi nhóm:
so_luong_xe = n(): số lượng
xe.
gia_ban_trung_binh = mean(sellingprice):
giá bán trung bình.
odometer_trung_binh = mean(odometer):
km trung bình đã đi.
arrange(desc(gia_ban_trung_binh)):
sắp xếp bảng tổng hợp theo giá bán trung bình giảm dần.
kable() hiển thị bảng tổng hợp với
số liệu đã định dạng: không chữ số thập phân
(digits=0), dấu phân cách hàng nghìn là
dấu phẩy (big.mark=","), và tiêu đề bảng
là “Bảng tổng hợp theo nguồn gốc xe”.
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.
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)")
| 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 |
Ở 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.
Hãy tưởng tượng bạn là một họa sĩ vẽ tranh:
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.
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)?
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ử +.
Đầ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")
| 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:
numeric_vars <- df_clean |> select(sellingprice, age, odometer, mmr):
df_clean.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.
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ị.
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.
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.
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")
| 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.
# 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")
| 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.
# 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")
| 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”.
Đâ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ể.
Tính giá bán trung bình cho từng nhóm kết hợp giữa
make_origin và condition_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")
| 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.
# 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")
| 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”.
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.
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.
# 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á")
| 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”.
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.
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.
Để 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:
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.
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.
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.
Lớp nhãn (labs(...)):
Lớp giao diện (theme_light()):
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:
Lớp nền tảng
(ggplot(df_clean, aes(x = age))):
df_clean, ánh xạ biến
age (tuổi xe) lên trục X.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%).
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.
Lớp nhãn (labs(...)):
Lớp giao diện (theme_bw()):
Nhận xét:
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:
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.
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.
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:
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.
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).
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 đô.
Lớp nhãn (labs(...)):
Lớp giao diện
(theme_minimal()):
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.
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:
Lớp nền
(ggplot(df_clean, aes(x = age))):
age
(tuổi xe) lên trục X.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.
Lớp điểm dữ liệu
(geom_point(...)):
Lớp định dạng trục
(scale_y_continuous(labels = scales::dollar)):
Lớp nhãn (labs(...)):
Lớp chủ đề (theme_gray()):
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)
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).
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 mmr và sellingprice, 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 đồ 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.
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 đồ 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 đồ 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 đồ 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 đồ 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.
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.
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, Convertible và
SUV.
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ũ.
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.
# 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.
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"))
| 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
# 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
)
| 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)
| 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
)
| 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 |
# ============================================================
# 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")
| Vietnamese | English | ShortName |
|---|---|---|
| Year | Year | Y |
| Tổng nợ | Total debt | TD |
| VCSH | VCSH | V |
| Tổng tài sản | Total assets | TA |
| Cho vay khách hàng | Loans to customers | LTC |
| Tiền gửi khách hàng | Customer deposits | CD |
| Thu nhập lãi thuần | Net interest income | NII |
| Lãi thuần từ hoạt động dịch vụ | Net profit from service activities | NPFS |
| Lãi thuần từ kinh doanh ngoại hối và vàng | Net profit from foreign exchange and gold trading | NPFF |
| Lãi thuần từ mua bán chứng khoán kinh doanh, chứng khoán đầu tư và góp vốn đầu tư dài hạn | Net profit from trading of business securities, investment securities and long-term investment capital contributions | NPFT |
| Thu nhập thuần từ hoạt động khác | Net income from other activities | NIFO |
| Thu nhập từ góp vốn, mua cổ phần | Income from capital contribution and share purchase | IFCC |
| EPS | EPS | E |
| Lợi nhuận sau thuế | Profit after tax | PAT |
| Chi phí hoạt động | Operating expenses | OE |
| Kinh doanh | Kinh doanh | KD |
| Đầu tư | Đầu tư | UT |
| Tài chính | Tài chính | TC |
| Lưu chuyển tiền thuần trong năm | Net cash flow during the year | NCFD |
translate_vi_en() Dịch tiếng Việt sang tiếng Anh tự
động
make_abbrev() Tạo tên viết tắt từ tên tiếng Anh
# D/E (Tỷ lệ nợ / vốn chủ sở hữu)
bctc <- bctc %>%
mutate(DE = TD / V)
# ROE (Lợi nhuận sau thuế / VCSH)
bctc <- bctc %>%
mutate(ROE = PAT / V)
# ROA (Lợi nhuận sau thuế / Tổng tài sản)
bctc <- bctc %>%
mutate(ROA = PAT / V)
# NIM (Thu nhập lãi thuần / Cho vay khách hàng)
bctc <- bctc %>%
mutate(NIM = NII / LTC)
# Làm tròn 3 chữ số
bctc <- bctc %>%
mutate(across(c(DE, ROE, ROA, NIM), ~round(., 3)))
# Hiển thị kết quả
bctc %>%
select(Y,DE, ROE, ROA, NIM) %>%
knitr::kable(caption = "<b>Bảng kết quả</b>") %>%
kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed"))
| Y | DE | ROE | ROA | NIM |
|---|---|---|---|---|
| 2014 | 10.692 | 0.146 | 0.146 | 0.072 |
| 2015 | 8.535 | 0.108 | 0.108 | 0.061 |
| 2016 | 8.638 | 0.108 | 0.108 | 0.054 |
| 2017 | 9.604 | 0.118 | 0.118 | 0.062 |
| 2018 | 9.603 | 0.181 | 0.181 | 0.534 |
| 2019 | 84.531 | 0.202 | 0.202 | 0.008 |
| 2020 | 8.880 | 0.172 | 0.172 | 0.069 |
| 2021 | 8.716 | 0.212 | 0.212 | 0.074 |
| 2022 | 8.151 | 0.228 | 0.228 | 0.080 |
| 2023 | 8.771 | 0.218 | 0.218 | 0.065 |
| 2024 | 8.643 | 0.196 | 0.196 | 0.054 |
Phân tích xu hướng hiệu quả hoạt động và rủi ro tài chính của Ngân
hàng MB (MB Bank) giai đoạn 2013–2024 nhằm đánh giá tình hình phát triển
bền vững của ngân hàng qua thời gian.
Cụ thể, mục tiêu của phần này là:
Đá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ư:
Phân tích rủi ro tài chính thông qua chỉ tiêu:
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"))
| Min | Max | Mean | Median | SD | CV |
|---|---|---|---|---|---|
| 8.151 | 84.531 | 15.88764 | 8.771 | 22.77725 | 1.433646 |
summarise() (hoặc summarize()): dùng để tính toán tổng hợp các giá trị theo cột, trả về 1 hàng chứa kết quả.
# Biểu đồ xu hướng D/E qua các năm
ggplot(bctc, aes(x = Y, y = DE)) +
geom_line(color = "#2E86C1", linewidth = 1.2) + # Layer 1
geom_point(color = "#E74C3C", size = 3) + # Layer 2
geom_smooth(method = "lm", se = FALSE, color = "gray40") +# Layer 3
geom_text(aes(label = round(DE,2)), vjust = -0.6, size=3) + # Layer 4
labs(title = "Xu hướng tỷ lệ D/E MB Bank (2014–2024)",
subtitle = "Đánh giá xu hướng đòn bẩy tài chính theo thời gian",
x = "Năm", y = "Tỷ lệ D/E") + # Layer 5
theme_minimal(base_size = 13) # Layer 6
## `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.
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.
Là một chỉ báo mô tả sự phân bố và xu hướng biến động của mức độ rủi ro tài chính giữa các doanh nghiệp theo từng giai đoạn, qua đó giúp đánh giá sự ổn định và bền vững trong cấu trúc tài chính.
# Tỷ trọng từng nhóm rủi ro qua thời gian
ty_trong <- bctc %>%
group_by(Nhom_rui_ro) %>%
summarise(Ty_trong = n() / nrow(bctc))
# Biểu đồ tròn thể hiện tỷ trọng nhóm rủi ro
ggplot(ty_trong, aes(x = "", y = Ty_trong, fill = Nhom_rui_ro)) +
geom_bar(stat = "identity", width = 1, color = "white") + # Layer 1 # Vẽ cột tỷ trọng (dạng bar) # Chuyển bar chart thành biểu đồ tròn
coord_polar("y", start = 0) + # Layer 2
geom_text(aes(label = scales::percent(Ty_trong)),
# Hiển thị nhãn phần trăm trên lát cắt
position = position_stack(vjust = 0.5)) + # Layer 3
scale_fill_manual(values = c("#2ECC71", "#F5B041", "#E74C3C")) + # Layer 4
labs(title = "Tỷ trọng các nhóm rủi ro tài chính (2014–2024)",
fill = "Mức rủi ro") + # Layer 5
theme_void(base_size = 13)
Giải thích cấu trúc biểu đồ
- geom_bar(): tạo các thanh biểu diễn tỷ trọng từng
nhóm rủi ro.
coord_polar(“y”): chuyển biểu đồ thanh thành biểu đồ tròn (pie chart).
geom_text(): thêm nhãn phần trăm trực tiếp trên từng phần của biểu đồ
scale_fill_manual(): chọn màu sắc riêng cho từng
nhóm rủi ro (cao, trung bình, thấp).
Nhận xét biểu đồ
Kết quả biểu đồ cho thấy, trong giai đoạn 2013–2024,
nhóm rủi ro trung bình chiếm tỷ trọng áp đảo (82%), trong khi nhóm rủi
ro cao chỉ chiếm 18%.
-> Điều này phản ánh rằng phần lớn các doanh nghiệp duy trì cấu trúc
tài chính ở mức an toàn tương đối, không quá phụ thuộc vào đòn bẩy
nợ.
Tuy nhiên, vẫn tồn tại một tỷ lệ nhất định các doanh nghiệp có rủi ro
cao, cho thấy sự phân hóa trong khả năng quản trị nợ và sử dụng vốn vay
giữa các đơn vị trong mẫu nghiên cứu.
Biểu đồ mật độ được sử dụng để quan sát phân bố xác suất của tỷ lệ nợ
trên vốn chủ sở hữu (D/E) — đại diện cho mức độ rủi ro tài chính hoặc
đòn bẩy của ngân hàng. Mục tiêu chính:
- Giúp nhận diện hình dạng phân bố (chuẩn, lệch phải, lệch trái,…).
Đánh giá xem có tồn tại các giá trị cực đoan (outliers) trong cấu trúc vốn hay không.
Hỗ trợ nhà phân tích xác định mức D/E phổ biến nhất và vị trí trung bình so với toàn bộ giai đoạn.
Là cơ sở cho phân nhóm rủi ro tài chính trong bước tiếp theo.
# Phân bố D/E (Density Plot)
ggplot(bctc, aes(x = DE)) +
geom_density(fill = "#5DADE2", alpha = 0.6) + # Layer 1
geom_vline(aes(xintercept = mean(DE)), color = "red", linetype = "dashed") + # Layer 2
geom_rug(sides = "b", color = "gray30") + # Layer 3
labs(title = "Phân bố tỷ lệ D/E của MB Bank",
subtitle = "Phân tích mật độ xác suất của đòn bẩy tài chính",
x = "Tỷ lệ D/E", y = "Mật độ") + # Layer 4
annotate("text", x = mean(bctc$DE), y = 0.02,
label = paste0("Mean = ", round(mean(bctc$DE),2)), color = "red", vjust = -1) + # Layer 5
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- Ggeom_density(): vẽ đường cong mật độ thể hiện xác
suất phân bố của tỷ lệ D/E.
- geom_vline(): thêm đường dọc biểu thị giá trị trung
bình (Mean).
- geom_rug(): chèn các “vạch nhỏ” ở trục x để minh họa
từng điểm dữ liệu thực tế.
- annotate(): hiển thị nhãn “Mean = …” ngay trên biểu
đồ để người đọc dễ nhận biết.
Nhận xét biểu đồ
Phân bố tỷ lệ D/E của MB Bank cho thấy dữ liệu lệch phải mạnh
(right-skewed), tức có một số năm ghi nhận đòn bẩy tài chính rất cao so
với mặt bằng chung. Giá trị trung bình D/E ≈ 15.89,
trong khi phần lớn quan sát tập trung quanh 8–10 lần, phản ánh sự biến
động lớn trong cấu trúc vốn giữa các năm.
Điều này cho thấy MB Bank đã có giai đoạn tăng cường sử dụng nợ để mở
rộng hoạt động, nhưng nhìn chung mức trung bình vẫn trong phạm vi kiểm
soát. ### 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"))
| 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 trung bình trước và sau 2020
bctc$Y <- as.numeric(as.character(bctc$Y))
eff_compare <- bctc %>%
mutate(GiaiDoan = ifelse(Y < 2020, "Trước 2020", "Sau 2020")) %>%
group_by(GiaiDoan) %>%
summarise(across(c(ROE, ROA, NIM), mean, .names = "mean_{.col}"))
eff_compare_long <- eff_compare %>%
pivot_longer(cols = starts_with("mean"), names_to = "Chỉ_số", values_to = "Giá_trị")
ggplot(eff_compare_long, aes(x = GiaiDoan, y = Giá_trị, fill = Chỉ_số)) +
geom_col(position = "dodge", width = 0.6) + # Layer 1
geom_text(aes(label = round(Giá_trị,3)),
position = position_dodge(0.6), vjust = -0.5, size=3) + # Layer 2
scale_fill_brewer(palette = "Set2") + # Layer 3
labs(title = "So sánh hiệu quả hoạt động MB Bank trước và sau 2020",
subtitle = "Trung bình các chỉ số ROE, ROA, NIM",
x = "Giai đoạn", y = "Giá trị trung bình") + # Layer 4
theme_minimal(base_size = 13) # Layer 5
Giải thích cấu trúc biểu đồ
- Trục hoành (x – Giai đoạn): chia MB Bank thành 2 giai đoạn — Trước
2020 và Sau 2020.
Trục tung (y – Giá trị trung bình): thể hiện giá trị trung bình của từng chỉ số hiệu quả tài chính.
Các cột (geom_col): chiều cao thể hiện mức trung bình của từng chỉ tiêu trong mỗi giai đoạn.
Nhãn (geom_text): hiển thị giá trị trung bình cụ thể trên mỗi cột. Nhận xét biểu đồ
Biểu đồ giúp so sánh trực quan hiệu quả hoạt động của MB Bank trước và sau năm 2020 — thời điểm bắt đầu chịu ảnh hưởng mạnh của COVID-19 và giai đoạn đẩy mạnh chuyển đổi số.
Nếu ROE và ROA sau 2020 tăng → MB Bank nâng cao hiệu quả sử dụng vốn và tài sản, có thể do tối ưu chi phí và tăng trưởng tín dụng.
Nếu NIM giảm sau 2020 → phản ánh áp lực giảm biên lợi nhuận do lãi suất thị trường hoặc chính sách điều tiết.
-> Tổng thể, biểu đồ hỗ trợ đánh giá xu hướng cải thiện hay suy giảm hiệu quả sinh lời giữa hai giai đoạn, giúp làm rõ tác động của môi trường kinh tế và chiến lược nội bộ MB Bank.
# Tốc độ tăng trưởng hằng năm
# Tính tốc độ tăng trưởng theo năm
bctc_growth <- bctc %>%
arrange(Y) %>%
mutate(
ROE_g = (ROE / lag(ROE) - 1) * 100,
ROA_g = (ROA / lag(ROA) - 1) * 100,
NIM_g = (NIM / lag(NIM) - 1) * 100
)
# Thay NA (năm đầu tiên) bằng 0 để biểu đồ liền mạch
bctc_growth <- bctc_growth %>%
mutate(
ROE_g = replace_na(ROE_g, 0),
ROA_g = replace_na(ROA_g, 0),
NIM_g = replace_na(NIM_g, 0)
)
# Chuẩn hóa dữ liệu dạng dài (long format)
growth_long <- bctc_growth %>%
select(Y, ROE_g, ROA_g, NIM_g) %>%
pivot_longer(-Y, names_to = "Chỉ_số", values_to = "Tăng_trưởng")
# Hiển thị bảng tốc độ tăng trưởng
growth_long %>%
kbl(
caption = "Bảng: Tốc độ tăng trưởng hằng năm của ROE, ROA và NIM",
booktabs = TRUE,
align = c("c", "c", "r")
) %>%
kable_paper(full_width = FALSE, lightable_options = "striped") %>%
kable_styling(
latex_options = c("hold_position", "scale_down"),
font_size = 11
)
| Y | Chỉ_số | Tăng_trưởng |
|---|---|---|
| 2014 | ROE_g | 0.000000 |
| 2014 | ROA_g | 0.000000 |
| 2014 | NIM_g | 0.000000 |
| 2015 | ROE_g | -26.027397 |
| 2015 | ROA_g | -26.027397 |
| 2015 | NIM_g | -15.277778 |
| 2016 | ROE_g | 0.000000 |
| 2016 | ROA_g | 0.000000 |
| 2016 | NIM_g | -11.475410 |
| 2017 | ROE_g | 9.259259 |
| 2017 | ROA_g | 9.259259 |
| 2017 | NIM_g | 14.814815 |
| 2018 | ROE_g | 53.389831 |
| 2018 | ROA_g | 53.389831 |
| 2018 | NIM_g | 761.290323 |
| 2019 | ROE_g | 11.602210 |
| 2019 | ROA_g | 11.602210 |
| 2019 | NIM_g | -98.501873 |
| 2020 | ROE_g | -14.851485 |
| 2020 | ROA_g | -14.851485 |
| 2020 | NIM_g | 762.500000 |
| 2021 | ROE_g | 23.255814 |
| 2021 | ROA_g | 23.255814 |
| 2021 | NIM_g | 7.246377 |
| 2022 | ROE_g | 7.547170 |
| 2022 | ROA_g | 7.547170 |
| 2022 | NIM_g | 8.108108 |
| 2023 | ROE_g | -4.385965 |
| 2023 | ROA_g | -4.385965 |
| 2023 | NIM_g | -18.750000 |
| 2024 | ROE_g | -10.091743 |
| 2024 | ROA_g | -10.091743 |
| 2024 | NIM_g | -16.923077 |
Giải thích - lag(ROE) → giá trị ROE của năm trước.
(ROE / lag(ROE) - 1) * 100 → % tăng/giảm của ROE so với năm
trước.
replace_na(…, 0) → thay NA bằng 0, để biểu đồ hoặc bảng hiển thị liên tục, không đứt quãng.
select(Y, ROE_g, ROA_g, NIM_g) → chọn 4 cột cần dùng.
pivot_longer(-Y, …) → chuyển dữ liệu từ dạng rộng (wide) sang dài
(long)
kbl() → Tạo bảng LaTeX hoặc HTML từ dữ liệu (thuộc knitr).
kable_paper() → làm bảng đẹp hơn, dễ đọc, có hiệu ứng “striped”.
kable_styling() → 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ế.
# So sánh biến động qua Boxplot
eff_long <- bctc %>%
select(Y, ROE, ROA, NIM) %>%
pivot_longer(-Y, names_to = "Chỉ_số", values_to = "Giá_trị")
ggplot(eff_long, aes(x = Chỉ_số, y = Giá_trị, fill = Chỉ_số)) +
geom_boxplot(alpha = 0.7, width = 0.5) + # Layer 1
geom_jitter(color = "gray40", width = 0.1, alpha = 0.8) + # Layer 2
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") + # Layer 3
labs(title = "Phân bố biến động của ROE, ROA, NIM (2013–2024)",
subtitle = "Mức dao động thể hiện sự ổn định của hiệu quả hoạt động",
x = "Chỉ số", y = "Giá trị") + # Layer 4
theme_minimal(base_size = 13) + # Layer 5
scale_fill_brewer(palette = "Set3")
Giải thích cấu trúc biểu đồ
- Trục hoành (x – Chỉ số): gồm ba chỉ tiêu thể hiện
hiệu quả hoạt động: ROE, ROA, NIM.
Trục tung (y – Giá trị): biểu thị mức độ của từng chỉ số trong giai đoạn 2013–2024.
Các hộp (Boxplot): thể hiện phân bố dữ liệu của mỗi chỉ số:
Đường giữa hộp là trung vị (median).
Chiều cao của hộp (khoảng tứ phân vị) thể hiện mức độ dao động của dữ liệu.
Các chấm rờ là ngoại lệ (outliers) – giá trị bất thường hoặc đột biến.
Màu sắc: mỗi chỉ số có màu khác nhau giúp dễ
phân biệt mức ổn định tương đối. Nhận xét biểu đồ
ROE và ROA có phân bố khá tương đồng, với mức trung vị cao và biên dao động hẹp, phản ánh hiệu quả sinh lời ổn định và ít biến động qua các năm.
NIM có trung vị thấp hơn và xuất hiện một số điểm ngoại lệ lớn, cho thấy biên lãi ròng biến động mạnh hơn, có thể do tác động từ biến động lãi suất hoặc chính sách tín dụng.
Nhìn chung, MB Bank duy trì hiệu quả hoạt động ổn định, đặc biệt ở ROE và ROA, trong khi NIM thể hiện tính nhạy cảm cao hơn với điều kiện thị trường.
Độ ổn định phản ánh mức độ dao động của hiệu quả hoạt động qua thời
gian.
Chỉ số này thường được đo bằng hệ số biến thiên (CV = độ lệch chuẩn /
giá trị trung bình):
- CV càng nhỏ → hiệu quả hoạt động càng ổn định, ít biến động.
# Độ ổ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.
-> Như vậy, về tổng thể, MB Bank duy trì hiệu quả sinh lời (ROE, ROA) ổn định hơn so với biên lãi (NIM) trong giai đoạn 2013–2024.
# Phân tích tương quan
corr_data <- bctc %>%
select(DE, ROE, ROA, NIM)
corr_matrix <- round(cor(corr_data), 3)
ggcorrplot(corr_matrix,
hc.order = TRUE, type = "lower",
lab = TRUE, lab_size = 3,
colors = c("#E74C3C", "white", "#2ECC71"),
title = "Ma trận tương quan giữa D/E và các chỉ số hiệu quả",
ggtheme = theme_minimal())
## 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"
)
| 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
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
| 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
| 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.
#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.
#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"))
| 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 |
# =====================================================
# 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)
bctc_long <- bctc %>%
pivot_longer(cols = c(DE, ROE, ROA, NIM),
names_to = "ChiSo", values_to = "GiaTri")
ggplot(bctc_long, aes(x = Y, y = GiaTri, color = ChiSo)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
facet_wrap(~ChiSo, scales = "free_y", ncol = 2) +
labs(title = "Tổng hợp xu hướng chi tiết từng chỉ tiêu",
subtitle = "So sánh biến động D/E, ROE, ROA, NIM theo thời gian",
x = "Năm", y = "Giá trị") +
theme_minimal(base_size = 13)
Giải thích cấu trúc biểu đồ
- pivot_longer(): chuyển dữ liệu từ dạng rộng (mỗi cột là một chỉ số)
sang dạng dài (một cột “ChiSo”, một cột “GiaTri”).
facet_wrap(~ChiSo): chia biểu đồ thành 4 ô nhỏ, mỗi ô thể hiện biến động theo năm của D/E, NIM, ROA, ROE.
geom_line() + geom_point(): biểu diễn xu hướng và điểm dữ liệu cụ thể.
scales = “free_y”: cho phép mỗi chỉ số có trục tung riêng, giúp
nhìn rõ biến động tương đối dù đơn vị khác nhau.
Nhận xét biểu đồ
D/E có đột biến lớn vào năm 2019, cho thấy ngân hàng tăng cường sử dụng đòn bẩy tài chính, sau đó giảm mạnh trở lại mức ổn định.
NIM cũng tăng đột biến cùng năm, có thể do thay đổi chính sách lãi suất hoặc cơ cấu cho vay – huy động.
ROA và ROE thể hiện xu hướng đồng biến, cùng tăng mạnh giai đoạn 2018–2021, phản ánh hiệu quả sử dụng vốn và tài sản được cải thiện.
Sau 2022, các chỉ tiêu đều có xu hướng giảm nhẹ và ổn định, cho thấy doanh nghiệp bước vào giai đoạn tăng trưởng chậm nhưng bền vững.
Biểu đồ này giúp nhìn tổng quan chu kỳ tài chính, dễ nhận ra năm bất thường (2019) và mối quan hệ giữa hiệu quả – rủi ro – lợi nhuận
MB Bank là một trong những ngân hàng có hiệu suất tài chính tốt, ít rủi ro, và hiệu quả tăng trưởng ổn định trong thập kỷ qua. Việc duy trì cấu trúc tài chính an toàn cùng khả năng sinh lời bền vững cho thấy năng lực quản trị tài chính và chiến lược phát triển hợp lý của ngân hàng.