Bộ dữ liệu “Give Me Some Credit” là một tập dữ liệu công khai được sử dụng rộng rãi trong lĩnh vực phân tích rủi ro tín dụng và xây dựng mô hình dự báo khả năng vỡ nợ. Dữ liệu chứa thông tin về 150,000 hồ sơ vay cá nhân với 11 biến bao gồm đặc điểm nhân khẩu học, hành vi tài chính, và lịch sử thanh toán. Biến mục tiêu SeriousDlqin2yrs xác định liệu khách hàng có trải qua tình trạng quá hạn nghiêm trọng (≥90 ngày) trong vòng 2 năm hay không. Phần giới thiệu tập trung vào việc nạp dữ liệu, kiểm tra chất lượng, khám phá cấu trúc, và xác định các vấn đề cần xử lý trong phần tiền xử lý.
Phân tích sử dụng hai gói chính: tidyverse (bao gồm readr, dplyr, tidyr, ggplot2) và skimr (thống kê mô tả nhanh). Tidyverse cung cấp bộ công cụ hiện đại cho khoa học dữ liệu, trong khi skimr tự động phát hiện kiểu dữ liệu và tính các chỉ số phù hợp.
library(tidyverse)
library(skimr)
Các gói đã được nạp thành công, cung cấp đầy đủ hàm cần thiết cho quy trình phân tích dữ liệu.
Dữ liệu được nhập bằng hàm read_csv() từ gói readr, tự
động nhận dạng kiểu dữ liệu và trả về tibble. Kết quả lưu vào biến
df_credit với các dòng đại diện cho hồ sơ vay và các cột
đại diện cho biến.
df_credit <- readr::read_csv("GiveMeSomeCredit.csv", show_col_types = FALSE)
Dữ liệu đã được nạp thành công dưới dạng tibble, sẵn sàng cho các thao tác phân tích tiếp theo.
Hiển thị 10 quan sát đầu tiên để khảo sát nội dung và cấu trúc dữ liệu. Do có 11 cột, dữ liệu được chia thành hai bảng để tránh tràn lề khi xuất PDF.
head_data <- df_credit %>% slice_head(n = 10)
create_adaptive_table(head_data %>% select(1:6),
caption = "Phần 1 (6 cột đầu)",
font_size = 8)
| SeriousDlqin2yrs | RevolvingUtilizationOfUnsecuredLines | age | NumberOfTime30-59DaysPastDueNotWorse | DebtRatio | MonthlyIncome |
|---|---|---|---|---|---|
| 1 | 0,77 | 45 | 2 | 0,80 | 9.120 |
| 0 | 0,96 | 40 | 0 | 0,12 | 2.600 |
| 0 | 0,66 | 38 | 1 | 0,09 | 3.042 |
| 0 | 0,23 | 30 | 0 | 0,04 | 3.300 |
| 0 | 0,91 | 49 | 1 | 0,02 | 63.588 |
| 0 | 0,21 | 74 | 0 | 0,38 | 3.500 |
| 0 | 0,31 | 57 | 0 | 5.710,00 | NA |
| 0 | 0,75 | 39 | 0 | 0,21 | 3.500 |
| 0 | 0,12 | 27 | 0 | 46,00 | NA |
| 0 | 0,19 | 57 | 0 | 0,61 | 23.684 |
create_adaptive_table(head_data %>% select(7:11),
caption = "Phần 2 (5 cột còn lại)",
font_size = 8)
| NumberOfOpenCreditLinesAndLoans | NumberOfTimes90DaysLate | NumberRealEstateLoansOrLines | NumberOfTime60-89DaysPastDueNotWorse | NumberOfDependents |
|---|---|---|---|---|
| 13 | 0 | 6 | 0 | 2 |
| 4 | 0 | 0 | 0 | 1 |
| 2 | 1 | 0 | 0 | 0 |
| 5 | 0 | 0 | 0 | 0 |
| 7 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 0 | 1 |
| 8 | 0 | 3 | 0 | 0 |
| 8 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | NA |
| 9 | 0 | 4 | 0 | 2 |
Quan sát mẫu cho thấy ba vấn đề: giá trị thiếu ở MonthlyIncome và NumberOfDependents, giá trị ngoại lai ở DebtRatio, và biến mục tiêu SeriousDlqin2yrs cần chuyển sang dạng factor.
Hàm glimpse() hiển thị cấu trúc tổng quan của dữ liệu,
bao gồm số quan sát, số biến, kiểu dữ liệu và giá trị mẫu của từng
cột.
glimpse(df_credit)
## Rows: 150,000
## Columns: 11
## $ SeriousDlqin2yrs <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,…
## $ RevolvingUtilizationOfUnsecuredLines <dbl> 0.766126609, 0.957151019, 0.658180140, 0.…
## $ age <dbl> 45, 40, 38, 30, 49, 74, 57, 39, 27, 57, 3…
## $ `NumberOfTime30-59DaysPastDueNotWorse` <dbl> 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 3,…
## $ DebtRatio <dbl> 0.802982129, 0.121876201, 0.085113375, 0.…
## $ MonthlyIncome <dbl> 9120, 2600, 3042, 3300, 63588, 3500, NA, …
## $ NumberOfOpenCreditLinesAndLoans <dbl> 13, 4, 2, 5, 7, 3, 8, 8, 2, 9, 5, 7, 13, …
## $ NumberOfTimes90DaysLate <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,…
## $ NumberRealEstateLoansOrLines <dbl> 6, 0, 0, 0, 1, 1, 3, 0, 0, 4, 0, 2, 2, 1,…
## $ `NumberOfTime60-89DaysPastDueNotWorse` <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,…
## $ NumberOfDependents <dbl> 2, 1, 0, 0, 0, 1, 0, 0, NA, 2, 0, 2, 2, 2…
Bộ dữ liệu gồm 150,000 quan sát và 11 biến. Hai biến RevolvingUtilizationOfUnsecuredLines và DebtRatio là số thực, còn lại là số nguyên. Biến mục tiêu SeriousDlqin2yrs có giá trị 0/1, cần chuyển sang factor để phân tích.
Hàm summarise() của dplyr tính số dòng và cột trong một
bước, giữ kết quả dạng tibble. n() trả về số dòng, còn
length(cur_data()) đếm số cột.
kich_thuoc <- df_credit %>%
summarise(
`Số dòng` = n(),
`Số cột` = length(cur_data())
)
create_adaptive_table(kich_thuoc,
caption = "Kích thước bộ dữ liệu GiveMeSomeCredit",
digits = 0,
font_size = 9)
| Số dòng | Số cột |
|---|---|
| 150.000 | 12 |
Bộ dữ liệu có 150,000 quan sát và 11 biến. Kích thước mẫu lớn giúp các ước lượng thống kê ổn định hơn theo Định lý giới hạn trung tâm. Với n = 150,000, các phương pháp trực quan cần kỹ thuật đặc biệt (alpha nhỏ, geom_hex) để tránh overplotting khi vẽ scatter plot.
Danh sách tên biến được trích xuất bằng
summarise(across()) và chuyển sang dạng bảng để tham chiếu
nhất quán. Bảng định nghĩa chi tiết giúp hiểu rõ ý nghĩa nghiệp vụ của
từng biến.
ten_bien <- df_credit %>%
summarise(across(everything(), ~cur_column())) %>%
pivot_longer(everything(), names_to = "Chi_so", values_to = "Tên biến") %>%
select(-"Chi_so") %>%
mutate(STT = row_number(), .before = 1)
create_adaptive_table(ten_bien,
caption = "Danh sách tên biến trong bộ dữ liệu",
font_size = 9)
| STT | Tên biến |
|---|---|
| 1 | SeriousDlqin2yrs |
| 2 | RevolvingUtilizationOfUnsecuredLines |
| 3 | age |
| 4 | NumberOfTime30-59DaysPastDueNotWorse |
| 5 | DebtRatio |
| 6 | MonthlyIncome |
| 7 | NumberOfOpenCreditLinesAndLoans |
| 8 | NumberOfTimes90DaysLate |
| 9 | NumberRealEstateLoansOrLines |
| 10 | NumberOfTime60-89DaysPastDueNotWorse |
| 11 | NumberOfDependents |
var_meaning <- tibble::tibble(
Biến = c(
"SeriousDlqin2yrs",
"RevolvingUtilizationOfUnsecuredLines",
"age",
"NumberOfTime30-59DaysPastDueNotWorse",
"DebtRatio",
"MonthlyIncome",
"NumberOfOpenCreditLinesAndLoans",
"NumberOfTimes90DaysLate",
"NumberRealEstateLoansOrLines",
"NumberOfTime60-89DaysPastDueNotWorse",
"NumberOfDependents"
),
Ý_Nghĩa = c(
"Biến mục tiêu: 1 = Một người trong vòng 2 năm không trả được khoản vay trả góp trong vòng 90 ngày hoặc hơn sau ngày đến hạn, 0 = Không",
"Tổng số dư trên thẻ tín dụng và hạn mức tín dụng cá nhân ngoại trừ bất động sản và không có khoản nợ trả góp chia cho tổng hạn mức tín dụng",
"Tuổi khách hàng",
"Số lần người vay quá hạn thanh toán từ 30-59 ngày nhưng không quá hạn trong 2 năm qua",
"Tiền trả nợ hàng tháng, tiền cấp dưỡng, chi phí sinh hoạt chia cho tổng thu nhập hàng tháng",
"Thu nhập hàng tháng",
"Số lượng khoản vay mở (trả góp như vay mua ô tô hoặc thế chấp) và hạn mức tín dụng (ví dụ thẻ tín dụng)",
"Số lần quá hạn 90+ ngày",
"Số lượng các khoản vay thế chấp và bất động sản bao gồm cả hạn mức tín dụng thế chấp nhà",
"Số lần người vay quá hạn thanh toán từ 60-89 ngày nhưng không quá hạn trong 2 năm qua",
"Số lượng người phụ thuộc trong gia đình không bao gồm bản thân họ (vợ/chồng, con cái, v.v.)"
)
)
create_adaptive_table(var_meaning,
caption = "Định nghĩa và ý nghĩa các biến trong bộ dữ liệu",
font_size = 8)
| Biến | Ý_Nghĩa |
|---|---|
| SeriousDlqin2yrs | Biến mục tiêu: 1 = Một người trong vòng 2 năm không trả được khoản vay trả góp trong vòng 90 ngày hoặc hơn sau ngày đến hạn, 0 = Không |
| RevolvingUtilizationOfUnsecuredLines | Tổng số dư trên thẻ tín dụng và hạn mức tín dụng cá nhân ngoại trừ bất động sản và không có khoản nợ trả góp chia cho tổng hạn mức tín dụng |
| age | Tuổi khách hàng |
| NumberOfTime30-59DaysPastDueNotWorse | Số lần người vay quá hạn thanh toán từ 30-59 ngày nhưng không quá hạn trong 2 năm qua |
| DebtRatio | Tiền trả nợ hàng tháng, tiền cấp dưỡng, chi phí sinh hoạt chia cho tổng thu nhập hàng tháng |
| MonthlyIncome | Thu nhập hàng tháng |
| NumberOfOpenCreditLinesAndLoans | Số lượng khoản vay mở (trả góp như vay mua ô tô hoặc thế chấp) và hạn mức tín dụng (ví dụ thẻ tín dụng) |
| NumberOfTimes90DaysLate | Số lần quá hạn 90+ ngày |
| NumberRealEstateLoansOrLines | Số lượng các khoản vay thế chấp và bất động sản bao gồm cả hạn mức tín dụng thế chấp nhà |
| NumberOfTime60-89DaysPastDueNotWorse | Số lần người vay quá hạn thanh toán từ 60-89 ngày nhưng không quá hạn trong 2 năm qua |
| NumberOfDependents | Số lượng người phụ thuộc trong gia đình không bao gồm bản thân họ (vợ/chồng, con cái, v.v.) |
Bảng định nghĩa cung cấp ngữ cảnh quan trọng cho phân tích. Biến mục tiêu xác định ngưỡng vỡ nợ nghiêm trọng (≥90 ngày), các biến trễ hạn phản ánh mức độ rủi ro khác nhau, và DebtRatio phụ thuộc vào MonthlyIncome nên cần xử lý cẩn thận khi có ngoại lai.
Hàm skim() từ gói skimr tự động phân tích dữ liệu theo
kiểu biến, tính các chỉ số thống kê phù hợp và phát hiện vấn đề chất
lượng dữ liệu.
summary_stats <- summarise_ct(df_credit)
create_adaptive_table(summary_stats,
caption = "Bảng thống kê mô tả các biến số trong dữ liệu GiveMeCredit",
digits = 2,
font_size = 9)
| Variable | Count | Min | Q1 | Median | Mode | Mean | SD | Q3 | Max | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SeriousDlqin2yrs | 150.000 | 0 | 0,00 | 0,00 | 0 | 0,07 | 0,25 | 0,00 | 1 | 3,47 | 10,03 | 0 |
| RevolvingUtilizationOfUnsecuredLines | 150.000 | 0 | 0,03 | 0,15 | 0 | 6,05 | 249,76 | 0,56 | 50.708 | 97,63 | 14.544,03 | 0 |
| age | 150.000 | 0 | 41,00 | 52,00 | 49 | 52,30 | 14,77 | 63,00 | 109 | 0,19 | -0,49 | 0 |
| NumberOfTime30-59DaysPastDueNotWorse | 150.000 | 0 | 0,00 | 0,00 | 0 | 0,42 | 4,19 | 0,00 | 98 | 22,60 | 522,35 | 0 |
| DebtRatio | 150.000 | 0 | 0,18 | 0,37 | 0 | 353,01 | 2.037,82 | 0,87 | 329.664 | 95,16 | 13.733,65 | 0 |
| MonthlyIncome | 120.269 | 0 | 3.400,00 | 5.400,00 | 5.000 | 6.670,22 | 14.384,67 | 8.249,00 | 3.008.750 | 114,04 | 19.503,57 | 29.731 |
| NumberOfOpenCreditLinesAndLoans | 150.000 | 0 | 5,00 | 8,00 | 6 | 8,45 | 5,15 | 11,00 | 58 | 1,22 | 3,09 | 0 |
| NumberOfTimes90DaysLate | 150.000 | 0 | 0,00 | 0,00 | 0 | 0,27 | 4,17 | 0,00 | 98 | 23,09 | 537,71 | 0 |
| NumberRealEstateLoansOrLines | 150.000 | 0 | 0,00 | 1,00 | 0 | 1,02 | 1,13 | 2,00 | 54 | 3,48 | 60,47 | 0 |
| NumberOfTime60-89DaysPastDueNotWorse | 150.000 | 0 | 0,00 | 0,00 | 0 | 0,24 | 4,16 | 0,00 | 98 | 23,33 | 545,66 | 0 |
| NumberOfDependents | 146.076 | 0 | 0,00 | 0,00 | 0 | 0,76 | 1,12 | 1,00 | 20 | 1,59 | 3,00 | 3.924 |
Bảng thống kê xác định ba vấn đề cốt lõi: MonthlyIncome có 29,731 giá trị thiếu (20%), cần imputation cẩn trọng; RevolvingUtilizationOfUnsecuredLines và DebtRatio có ngoại lai nghiêm trọng (max lần lượt là 50.708 và 329.664); SeriousDlqin2yrs có mean = 0.07 cho thấy mất cân bằng lớp (93% không vỡ nợ). MonthlyIncome có phân bố lệch phải (mean = 6670 > median = 5400), điển hình cho dữ liệu thu nhập.
Những phát hiện này định hình chiến lược xử lý dữ liệu trong phần tiếp theo: imputation cho giá trị thiếu, xử lý ngoại lai và mã đặc biệt, chuyển đổi kiểu dữ liệu phù hợp.
Tính chính xác số lượng và tỷ lệ giá trị thiếu cho từng biến bằng
is.na() và colSums(), sau đó sắp xếp giảm dần
để xác định mức độ ưu tiên xử lý.
tong_dong <- df_credit %>% summarise(tong = n()) %>% pull(tong)
bang_na <- df_credit %>%
summarise(across(everything(), ~sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "Biến", values_to = "Số NA") %>%
mutate(
`Tỷ lệ phần trăm` = round((`Số NA` / tong_dong) * 100, 2)
) %>%
arrange(desc(`Tỷ lệ phần trăm`))
create_adaptive_table(bang_na,
caption = "Tỷ lệ giá trị thiếu theo từng biến",
font_size = 9)
| Biến | Số NA | Tỷ lệ phần trăm |
|---|---|---|
| MonthlyIncome | 29.731 | 19,82 |
| NumberOfDependents | 3.924 | 2,62 |
| SeriousDlqin2yrs | 0 | 0,00 |
| RevolvingUtilizationOfUnsecuredLines | 0 | 0,00 |
| age | 0 | 0,00 |
| NumberOfTime30-59DaysPastDueNotWorse | 0 | 0,00 |
| DebtRatio | 0 | 0,00 |
| NumberOfOpenCreditLinesAndLoans | 0 | 0,00 |
| NumberOfTimes90DaysLate | 0 | 0,00 |
| NumberRealEstateLoansOrLines | 0 | 0,00 |
| NumberOfTime60-89DaysPastDueNotWorse | 0 | 0,00 |
Kết quả cho thấy 9/11 biến hoàn chỉnh không có giá trị thiếu. MonthlyIncome có 29,731 quan sát thiếu (19.82%), là biến quan trọng cho đánh giá rủi ro tín dụng, cần imputation bằng trung vị do phân bố lệch. NumberOfDependents có 3,924 quan sát thiếu (2.62%), tỷ lệ nhỏ nhưng cần xử lý để đảm bảo tính toàn vẹn.
Phát hiện các bản ghi trùng lặp hoàn toàn bằng
duplicated(), vi phạm nguyên tắc độc lập của quan sát và
cần loại bỏ trước phân tích.
duplicate_summary <- df_credit %>%
summarise(`Tổng bản ghi` = n()) %>%
mutate(`Sau loại trùng` = df_credit %>% distinct() %>% summarise(n = n()) %>% pull(n)) %>%
mutate(
`Số trùng lặp` = `Tổng bản ghi` - `Sau loại trùng`,
`Tỷ lệ phần trăm` = round((`Số trùng lặp` / `Tổng bản ghi`) * 100, 3)
) %>%
select(`Tổng bản ghi`, `Sau loại trùng`, `Số trùng lặp`, `Tỷ lệ phần trăm`)
create_adaptive_table(duplicate_summary,
caption = "Thống kê bản ghi trùng lặp",
digits = 3,
font_size = 9)
| Tổng bản ghi | Sau loại trùng | Số trùng lặp | Tỷ lệ phần trăm |
|---|---|---|---|
| 150.000 | 149.391 | 609 | 0,406 |
Có 609 dòng trùng lặp (0.406%), tuy tỷ lệ nhỏ nhưng vi phạm giả định
độc lập của quan sát. Giữ lại sẽ tăng trọng số giả tạo và gây sai lệch
thống kê, cần loại bỏ bằng distinct().
Ở mục 1.1.8, hàm skim() đã chẩn đoán và phát hiện một dị thường (anomaly) phi logic trong biến age: giá trị tối thiểu (p0) là 0. Trong bối cảnh phân tích tín dụng, việc một khách hàng có tuổi bằng 0 là không thể xảy ra, điều này cho thấy đây là một lỗi dữ liệu (data error).
Trước khi quyết định phương án xử lý (ví dụ: xóa bỏ, thay thế), chúng ta cần xác minh (investigate) xem đây là một lỗi cá biệt (isolated error) hay một vấn đề có hệ thống (systematic issue). Thao tác sau sẽ lọc và đếm số lượng các quan sát có age = 0.
age_zero_count <- df_credit %>% filter(age == 0) %>% summarise(n = n()) %>% pull(n)
age_zero_count
## [1] 1
Kết quả filter() cho thấy chỉ có một quan sát duy nhất trong toàn bộ 150.000 dòng có age = 0, khẳng định đây là lỗi nhập liệu cá biệt chứ không phải mã đặc biệt hay một nhóm đối tượng; do là trường hợp đơn lẻ nên không làm sai lệch thống kê tổng thể nhưng vẫn sẽ được loại bỏ ở Mục 1.2 để bảo đảm tính toàn vẹn.
Ở mục 1.1.8, hàm skim() đã phát hiện một dị thường ở ba
biến trễ hạn thanh toán:
NumberOfTime30-59DaysPastDueNotWorse,
NumberOfTime60-89DaysPastDueNotWorse, và
NumberOfTimes90DaysLate. Cụ thể: - Phân vị thứ 75 (p75)
bằng 0, chỉ ra 75% khách hàng không bao giờ trễ hạn - Nhưng giá trị tối
đa (p100) lại là 98
Con số 98 này không phải là một số đếm thực tế, mà là một mã đặc biệt có thể biểu thị “98 lần hoặc nhiều hơn” hoặc một mã hệ thống khác. Để xác nhận, chúng ta sẽ xây dựng bảng tần suất (frequency table) cho cả ba biến này.
Để định lượng rõ mức độ và tính chất của các mã đặc biệt này, ba bảng tần suất dưới đây trình bày phân phối giá trị cho từng biến.
tbl_30_59 <- df_credit %>%
count(`NumberOfTime30-59DaysPastDueNotWorse`, name = "Tần suất") %>%
rename(`Giá trị` = `NumberOfTime30-59DaysPastDueNotWorse`)
tbl_60_89 <- df_credit %>%
count(`NumberOfTime60-89DaysPastDueNotWorse`, name = "Tần suất") %>%
rename(`Giá trị` = `NumberOfTime60-89DaysPastDueNotWorse`)
tbl_90_plus <- df_credit %>%
count(NumberOfTimes90DaysLate, name = "Tần suất") %>%
rename(`Giá trị` = NumberOfTimes90DaysLate)
library(gridExtra)
library(grid)
p1 <- tableGrob(tbl_30_59, rows = NULL)
p2 <- tableGrob(tbl_60_89, rows = NULL)
p3 <- tableGrob(tbl_90_plus, rows = NULL)
t1 <- textGrob("Tần suất: Trễ hạn 30–59 ngày", gp = gpar(fontsize = 10, fontface = "bold"))
t2 <- textGrob("Tần suất: Trễ hạn 60–89 ngày", gp = gpar(fontsize = 10, fontface = "bold"))
t3 <- textGrob("Tần suất: Trễ hạn ≥90 ngày", gp = gpar(fontsize = 10, fontface = "bold"))
p1_box <- arrangeGrob(t1, p1, ncol = 1, heights = c(0.15, 1))
p2_box <- arrangeGrob(t2, p2, ncol = 1, heights = c(0.15, 1))
p3_box <- arrangeGrob(t3, p3, ncol = 1, heights = c(0.15, 1))
# Ghép 3 bảng song song
grid.arrange(p1_box, p2_box, p3_box, ncol = 3)
Kết quả bảng tần suất xác nhận sự tồn tại nhất quán của các mã hệ thống 96 (5 lần) và 98 (264 lần) ở cả ba biến trễ hạn, cho thấy đây là mã quy ước chứ không phải số đếm thực; trong thực hành tín dụng, 98 thường biểu thị “98 lần hoặc hơn” còn 96 thường là mã ngoại lệ/không khả dụng; ở phần xử lý dữ liệu (Mục 1.2), các mã này sẽ được chuẩn hóa bằng cách thay 96 và 98 về 0 để loại bỏ nhiễu, trừ khi cần bảo toàn thông tin gốc cho phân tích đặc thù.
Để khảo sát giá trị ngoại lai ở đuôi phải của biến tỷ lệ sử dụng hạn mức tín dụng (Util_Ratio), quy trình gồm ba bước: (1) ước lượng các phân vị cao nhằm xác định ngưỡng hợp lý cho phần lớn phân phối; (2) lọc và đếm số lượng quan sát vượt ngưỡng bất thường (chọn ngưỡng 10 tương đương 1.000%); (3) tính các chỉ số mô tả cơ bản cho nhóm quan sát bất thường này. Các phân vị được tính bằng hàm quantile, số lượng và tỷ lệ ngoại lai được xác định qua lọc và tổng hợp, các chỉ số mô tả gồm giá trị nhỏ nhất, lớn nhất, trung bình và trung vị. Kết quả được trình bày bằng ba bảng song song, giúp nhận diện rõ mức độ và đặc điểm của nhóm ngoại lai, phục vụ cho các bước xử lý dữ liệu tiếp theo.
quantiles_util <- quantile(df_credit$RevolvingUtilizationOfUnsecuredLines,
probs = c(0.95, 0.98, 0.99, 0.995),
na.rm = TRUE)
quantile_df <- tibble::tibble(
Percentile = c("95th", "98th", "99th", "99.5th"),
Value = round(as.numeric(quantiles_util), 2)
)
outlier_count <- df_credit %>%
filter(RevolvingUtilizationOfUnsecuredLines > 10) %>%
summarise(n = n()) %>%
pull(n)
total_rows <- df_credit %>% summarise(Tong = n()) %>% pull(Tong)
outlier_percent <- round((outlier_count / total_rows) * 100, 2)
outlier_stats <- tibble::tibble(
Metric = c("Count", "Percentage (%)"),
Value = c(outlier_count, outlier_percent)
)
if (outlier_count > 0) {
outlier_summary_stats <- df_credit %>%
filter(RevolvingUtilizationOfUnsecuredLines > 10) %>%
summarise(
Min = min(RevolvingUtilizationOfUnsecuredLines, na.rm = TRUE),
Max = max(RevolvingUtilizationOfUnsecuredLines, na.rm = TRUE),
Mean = mean(RevolvingUtilizationOfUnsecuredLines, na.rm = TRUE),
Median = median(RevolvingUtilizationOfUnsecuredLines, na.rm = TRUE)
) %>%
pivot_longer(everything(), names_to = "Stat", values_to = "Value") %>%
mutate(Value = round(Value, 2))
} else {
outlier_summary_stats <- NULL
}
t1 <- textGrob("Phân vị (Quantiles) của Util_Ratio", gp = gpar(fontsize = 10, fontface = "bold"))
p1 <- tableGrob(quantile_df, rows = NULL)
p1_box <- arrangeGrob(t1, p1, ncol = 1, heights = c(0.2, 1))
t2 <- textGrob("Số lượng và tỷ lệ outliers (Util_Ratio > 10)", gp = gpar(fontsize = 10, fontface = "bold"))
p2 <- tableGrob(outlier_stats, rows = NULL)
p2_box <- arrangeGrob(t2, p2, ncol = 1, heights = c(0.2, 1))
if (!is.null(outlier_summary_stats)) {
t3 <- textGrob("Thống kê mô tả nhóm outliers", gp = gpar(fontsize = 10, fontface = "bold"))
p3 <- tableGrob(outlier_summary_stats, rows = NULL)
p3_box <- arrangeGrob(t3, p3, ncol = 1, heights = c(0.2, 1))
} else {
t3 <- textGrob("Không có outlier nào (Util_Ratio ≤ 10)", gp = gpar(fontsize = 10, fontface = "bold"))
p3_box <- arrangeGrob(t3, ncol = 1)
}
grid.newpage()
grid.arrange(p1_box, p2_box, p3_box, ncol = 3)
Kết quả phân tích tứ phân vị cho thấy 99.5% khách hàng có tỷ lệ sử dụng hạn mức ở mức hợp lý (phân vị thứ 99 chỉ đạt 1.37, tương đương 137%), trong khi chỉ khoảng 241 quan sát (khoảng 0.16% tổng mẫu) vượt ngưỡng 10 (tương đương 1000%). Nhóm hiếm này lại cực kỳ lệch với trung bình khoảng 3564 (356.400%) và giá trị tối đa đạt mức không hợp lý 50.708 (5.070.800%), đủ để làm méo mạnh các thống kê tóm tắt và ảnh hưởng nghiêm trọng đến hiệu suất mô hình hồi quy hoặc phân loại nếu không được xử lý. Do đó, ở Phần 1.2 sẽ áp dụng kỹ thuật cắt ngưỡng (capping) tại giá trị 10 để hạn chế tác động của các ngoại lai này, đồng thời vẫn bảo toàn thông tin rằng những khách hàng này có mức sử dụng hạn mức rất cao, tức có rủi ro tài chính tiềm ẩn.
Sau khi hoàn thành giai đoạn giới thiệu bộ dữ liệu, phân tích đã xác định được bốn loại vấn đề chất lượng dữ liệu cần xử lý: giá trị thiếu (29,731 NA ở MonthlyIncome và 3,924 NA ở NumberOfDependents), giá trị ngoại lai nghiêm trọng (DebtRatio lên đến 329.664 và RevolvingUtilizationOfUnsecuredLines đạt 50.708), mã hóa đặc biệt (giá trị 96 và 98 trong các biến trễ hạn), và dị thường logic (age = 0). Phần này triển khai quy trình tiền xử lý (preprocessing) có hệ thống để chuyển đổi dữ liệu thô thành dữ liệu sạch, sẵn sàng cho phân tích thống kê và mô hình hóa. Chiến lược xử lý tuân thủ ba nguyên tắc cốt lõi: bảo toàn tính toàn vẹn dữ liệu bằng cách ưu tiên sửa chữa thay vì xóa bỏ, giảm thiểu thất thoát thông tin thông qua các phương pháp imputation thống kê, và đảm bảo tính minh bạch bằng cách ghi lại từng quyết định xử lý.
Lộ trình kỹ thuật gồm tám bước tuần tự với hàm chính từ hệ sinh thái tidyverse. Bước 1: Tạo bản sao df_clean từ df_credit gốc để đảm bảo khả năng tái lập. Bước 2: Đổi tên 11 biến sang định dạng ngắn gọn bằng dplyr::rename(). Bước 3: Loại bỏ 609 bản ghi trùng lặp bằng dplyr::distinct(). Bước 4: Xử lý 29,731 NA ở Income bằng median imputation và 3,924 NA ở Dependents bằng giá trị 0 sử dụng dplyr::mutate() kết hợp tidyr::replace_na(). Bước 5: Loại bỏ 1 dòng có age = 0 bằng dplyr::filter() và chuẩn hóa mã 96/98 về 0 trong ba biến trễ hạn bằng dplyr::if_else(). Bước 6: Áp dụng cắt ngưỡng (capping) tại 10 cho 241 ngoại lai ở Util_Ratio bằng if_else(). Bước 7: Mã hóa Target_Var thành factor hai mức (“Không vỡ nợ”/“Vỡ nợ”), phân tổ age thành 5 nhóm tuổi, và phân tổ Income theo tứ phân vị bằng base::cut(). Bước 8: Kiểm tra toàn diện với dim(), colSums(is.na()), skimr::skim(), và table() để xác nhận chất lượng dữ liệu đầu ra.
Bước đầu tiên là tạo một bản sao hoàn chỉnh của dữ liệu gốc để làm việc. Thao tác này tuân thủ nguyên tắc bảo toàn dữ liệu gốc (preserve raw data) trong khoa học dữ liệu, đảm bảo khả năng tái lập (reproducibility) khi cần kiểm tra lại quy trình, so sánh trước/sau xử lý để đánh giá tác động của từng bước, và phục hồi nhanh chóng nếu xảy ra lỗi trong quá trình xử lý. Trong R, phép gán <- tạo bản sao mới trong bộ nhớ thay vì tham chiếu (reference) như một số ngôn ngữ khác.
df_clean <- df_credit
dim(df_clean)
## [1] 150000 11
head(colnames(df_clean))
## [1] "SeriousDlqin2yrs" "RevolvingUtilizationOfUnsecuredLines"
## [3] "age" "NumberOfTime30-59DaysPastDueNotWorse"
## [5] "DebtRatio" "MonthlyIncome"
Thao tác tạo bản sao thành công với df_clean có kích thước giống hệt df_credit gốc (150,000 × 11). Đối tượng df_clean này sẽ là trung tâm cho toàn bộ các bước tiền xử lý tiếp theo, trong khi df_credit được giữ nguyên để tham chiếu khi cần.
Các tên biến gốc trong bộ dữ liệu có độ dài không đồng nhất và chứa ký tự đặc biệt, gây bất tiện cho quá trình lập trình, trình bày bảng biểu (dễ tràn lề khi xuất PDF), cũng như thao tác biến đổi dữ liệu với các hàm của dplyr và tidyr. Việc sử dụng hàm rename() từ gói dplyr cho phép chuẩn hóa tên biến một cách hệ thống, với cú pháp TenMoi = TenCu. Nguyên tắc đặt tên mới gồm: ngắn gọn (tối đa 15 ký tự), rõ nghĩa (phản ánh nội dung biến), nhất quán (sử dụng dấu gạch dưới để phân tách các thành phần, viết thường toàn bộ trừ các từ viết tắt như PastDue), và đảm bảo tương thích với cú pháp R.
df_clean <- df_clean %>%
rename(
Target_Var = SeriousDlqin2yrs,
Util_Ratio = RevolvingUtilizationOfUnsecuredLines,
PastDue_30_59 = `NumberOfTime30-59DaysPastDueNotWorse`,
PastDue_60_89 = `NumberOfTime60-89DaysPastDueNotWorse`,
PastDue_90_plus = NumberOfTimes90DaysLate,
Income = MonthlyIncome,
Open_Loans = NumberOfOpenCreditLinesAndLoans,
RealEstate_Loans = NumberRealEstateLoansOrLines,
Dependents = NumberOfDependents
)
renamed_variables <- df_clean %>%
summarise(across(everything(), ~cur_column())) %>%
pivot_longer(everything(), names_to = "Chỉ_số", values_to = "Tên biến") %>%
select(-"Chỉ_số") %>%
mutate(STT = row_number(), .before = 1)
create_adaptive_table(renamed_variables,
caption = "Danh sách tên biến sau khi đổi tên",
col_names = c("STT", "Tên biến"),
font_size = 9)
| STT | Tên biến |
|---|---|
| 1 | Target_Var |
| 2 | Util_Ratio |
| 3 | age |
| 4 | PastDue_30_59 |
| 5 | DebtRatio |
| 6 | Income |
| 7 | Open_Loans |
| 8 | PastDue_90_plus |
| 9 | RealEstate_Loans |
| 10 | PastDue_60_89 |
| 11 | Dependents |
Để tiện theo dõi và kiểm tra lại, chúng ta tạo một bảng ánh xạ (mapping table) chứa toàn bộ cặp tên cũ/mới:
mapping_table <- tibble::tibble(
`Tên gốc` = c(
"SeriousDlqin2yrs",
"RevolvingUtilizationOfUnsecuredLines",
"NumberOfTime30-59DaysPastDueNotWorse",
"NumberOfTime60-89DaysPastDueNotWorse",
"NumberOfTimes90DaysLate",
"MonthlyIncome",
"NumberOfOpenCreditLinesAndLoans",
"NumberRealEstateLoansOrLines",
"NumberOfDependents",
"age",
"DebtRatio"
),
`Tên mới` = c(
"Target_Var",
"Util_Ratio",
"PastDue_30_59",
"PastDue_60_89",
"PastDue_90_plus",
"Income",
"Open_Loans",
"RealEstate_Loans",
"Dependents",
"age",
"DebtRatio"
)
)
create_adaptive_table(mapping_table,
caption = "Bảng ánh xạ tên biến",
font_size = 9)
| Tên gốc | Tên mới |
|---|---|
| SeriousDlqin2yrs | Target_Var |
| RevolvingUtilizationOfUnsecuredLines | Util_Ratio |
| NumberOfTime30-59DaysPastDueNotWorse | PastDue_30_59 |
| NumberOfTime60-89DaysPastDueNotWorse | PastDue_60_89 |
| NumberOfTimes90DaysLate | PastDue_90_plus |
| MonthlyIncome | Income |
| NumberOfOpenCreditLinesAndLoans | Open_Loans |
| NumberRealEstateLoansOrLines | RealEstate_Loans |
| NumberOfDependents | Dependents |
| age | age |
| DebtRatio | DebtRatio |
Việc đổi tên giúp các thao tác trở nên ngắn gọn, dễ đọc và dễ bảo trì
hơn (ví dụ: df_clean$Income thay cho
df_clean$MonthlyIncome), đồng thời cải thiện tính trình bày
của bảng biểu (tránh tràn dòng) và tiện lợi khi gọi tên biến trong các
mô hình (chẳng hạn lm(Target_Var ~ .)).
Dựa trên phát hiện từ bước kiểm tra trùng lặp, chúng ta loại bỏ 609
dòng dữ liệu bị trùng lặp hoàn toàn bằng hàm
dplyr::distinct(). Hàm này giúp đảm bảo mỗi hồ sơ vay là
duy nhất, tránh sai lệch thống kê do quan sát lặp.
df_clean <- df_clean %>% distinct()
dim(df_clean)
## [1] 149391 11
Thao tác loại bỏ trùng lặp thành công, giảm bộ dữ liệu từ 150,000 xuống 149,391 quan sát (giảm 609 dòng, tương đương 0.406%). Việc loại bỏ các bản ghi trùng lặp hoàn toàn đảm bảo giả định độc lập của các quan sát. Nếu giữ lại các dòng trùng lặp, trọng số của những hồ sơ này sẽ bị tăng lên giả tạo, gây sai lệch các ước lượng thống kê mô tả (mean, sd).
Theo kết quả kiểm tra dữ liệu, giá trị thiếu tập trung chủ yếu ở hai biến: Income (29.731 trường hợp, chiếm 19,82% tổng số quan sát) và Dependents (3.924 trường hợp, chiếm 2,62%). Việc xử lý giá trị thiếu là yêu cầu bắt buộc trong phân tích dữ liệu định lượng, do phần lớn các phương pháp thống kê và học máy không chấp nhận dữ liệu chứa NA. Nếu loại bỏ toàn bộ các dòng có giá trị thiếu, bộ dữ liệu sẽ mất đi gần 20% số quan sát, dẫn đến nguy cơ sai lệch kết quả phân tích nếu dữ liệu thiếu không hoàn toàn ngẫu nhiên.
Phương án thay thế giá trị thiếu được lựa chọn dựa trên đặc điểm phân phối và ý nghĩa nghiệp vụ của từng biến. Đối với Income, phương pháp thay thế bằng trung vị (median imputation) được ưu tiên do phân phối thu nhập lệch phải rõ rệt (mean = 6.670, median = 5.400), trung vị ít bị ảnh hưởng bởi các giá trị ngoại lai và phản ánh tốt hơn mức thu nhập điển hình của khách hàng. Đối với Dependents, giá trị thiếu được thay thế bằng 0, dựa trên giả định hợp lý rằng khách hàng không khai báo số người phụ thuộc thường là do không có hoặc không muốn công khai thông tin này; đây là biến đếm rời rạc nên việc thay thế bằng 0 đảm bảo tính nhất quán về mặt nghiệp vụ.
na_before_df <- df_clean %>%
summarise(across(everything(), ~sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "Bien", values_to = "So_luong_NA") %>%
arrange(desc(So_luong_NA))
create_adaptive_table(na_before_df,
caption = "Số lượng giá trị thiếu trước khi thực hiện Imputation",
col_names = c("Biến", "Số lượng NA"),
font_size = 9)
| Biến | Số lượng NA |
|---|---|
| Income | 29.221 |
| Dependents | 3.828 |
| Target_Var | 0 |
| Util_Ratio | 0 |
| age | 0 |
| PastDue_30_59 | 0 |
| DebtRatio | 0 |
| Open_Loans | 0 |
| PastDue_90_plus | 0 |
| RealEstate_Loans | 0 |
| PastDue_60_89 | 0 |
df_clean <- df_clean %>%
mutate(Income = replace_na(Income, median(Income, na.rm = TRUE))) %>%
mutate(Dependents = replace_na(Dependents, 0))
na_after_df <- df_clean %>%
summarise(across(everything(), ~sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "Bien", values_to = "So_luong_NA") %>%
arrange(desc(So_luong_NA))
create_adaptive_table(na_after_df,
caption = "Số lượng giá trị thiếu sau khi thực hiện Imputation",
col_names = c("Biến", "Số lượng NA"),
font_size = 9)
| Biến | Số lượng NA |
|---|---|
| Target_Var | 0 |
| Util_Ratio | 0 |
| age | 0 |
| PastDue_30_59 | 0 |
| DebtRatio | 0 |
| Income | 0 |
| Open_Loans | 0 |
| PastDue_90_plus | 0 |
| RealEstate_Loans | 0 |
| PastDue_60_89 | 0 |
| Dependents | 0 |
Kết quả cho thấy thay thế thành công hoàn toàn với toàn bộ 29,731 giá trị NA của biến Income được thay thế bằng giá trị trung vị 5,400 (đơn vị: dollar), và 3,924 giá trị NA của biến Dependents được thay thế bằng 0. Hàm replace_na() từ gói tidyr kết hợp với mutate() từ dplyr đảm bảo thao tác thực hiện an toàn và hiệu quả. Sau bước này, df_clean không còn giá trị thiếu ở bất kỳ biến nào, sẵn sàng cho các bước xử lý tiếp theo và đảm bảo tất cả 149,391 quan sát đều có thể được sử dụng trong mô hình hóa.
Sau khi xử lý giá trị thiếu, bước này tập trung điều chỉnh các dị thường và giá trị ngoại lai được phát hiện trước đó. Ba tình huống được xử lý tương ứng với ba nhóm biến.
Thứ nhất, biến age có một giá trị bằng 0 – một lỗi logic vì không thể có khách hàng 0 tuổi. Quan sát này được loại bỏ bằng filter(age > 0).
Thứ hai, các mã 96 và 98 xuất hiện trong ba biến trễ hạn thanh toán (PastDue_30_59, PastDue_60_89, PastDue_90_plus) được xác định là mã hệ thống, không phải giá trị thực. Các mã này được thay thế về 0 bằng mutate() kết hợp if_else(), đảm bảo dữ liệu nhất quán và phản ánh đúng ý nghĩa “không trễ hạn”.
Thứ ba, biến Util_Ratio có nhiều giá trị vượt quá 10 (tương đương 1000%), là các ngoại lai nghiêm trọng. Các giá trị này được giới hạn (capping) về mức 10 bằng if_else(Util_Ratio > 10, 10, Util_Ratio) để giảm ảnh hưởng cực đoan mà vẫn giữ được thông tin về tỷ lệ sử dụng cao.
Cuối cùng, summarise() tổng hợp lại các chỉ tiêu chính như số quan sát, tuổi nhỏ nhất và lớn nhất, số lần trễ hạn tối đa và tỷ lệ sử dụng hạn mức cao nhất. Bảng kết quả được chuyển định dạng bằng pivot_longer() và hiển thị qua create_adaptive_table(), giúp xác nhận dữ liệu sau xử lý đã sạch và hợp lệ cho phân tích tiếp theo.
df_clean <- df_clean %>% filter(age > 0)
df_clean <- df_clean %>%
mutate(PastDue_30_59 = if_else(PastDue_30_59 %in% c(96, 98), 0, PastDue_30_59))
df_clean <- df_clean %>%
mutate(PastDue_60_89 = if_else(PastDue_60_89 %in% c(96, 98), 0, PastDue_60_89))
df_clean <- df_clean %>%
mutate(PastDue_90_plus = if_else(PastDue_90_plus %in% c(96, 98), 0, PastDue_90_plus))
df_clean <- df_clean %>%
mutate(Util_Ratio = if_else(Util_Ratio > 10, 10, Util_Ratio))
df_clean_summary <- df_clean %>%
summarise(
`Số quan sát` = n(),
`Tuổi nhỏ nhất` = min(age, na.rm = TRUE),
`Tuổi lớn nhất` = max(age, na.rm = TRUE),
`Số lượng trễ hạn (30-59 ngày) tối đa` = max(PastDue_30_59, na.rm = TRUE),
`Số lượng trễ hạn (60-89 ngày) tối đa` = max(PastDue_60_89, na.rm = TRUE),
`Số lượng trễ hạn (≥90 ngày) tối đa` = max(PastDue_90_plus, na.rm = TRUE),
`Tỷ lệ sử dụng hạn mức tối đa` = max(Util_Ratio, na.rm = TRUE)
) %>%
tidyr::pivot_longer(everything(),
names_to = "Chỉ tiêu",
values_to = "Giá trị")
create_adaptive_table(df_clean_summary,
caption = "Thống kê sau khi xử lý dữ liệu",
font_size = 9)
| Chỉ tiêu | Giá trị |
|---|---|
| Số quan sát | 149.390 |
| Tuổi nhỏ nhất | 21 |
| Tuổi lớn nhất | 109 |
| Số lượng trễ hạn (30-59 ngày) tối đa | 13 |
| Số lượng trễ hạn (60-89 ngày) tối đa | 11 |
| Số lượng trễ hạn (≥90 ngày) tối đa | 17 |
| Tỷ lệ sử dụng hạn mức tối đa | 10 |
Sau xử lý, bộ dữ liệu còn 149.390 quan sát, cho thấy chỉ loại bỏ lượng nhỏ bản ghi lỗi và vẫn đảm bảo tính đại diện. Biến age có giá trị từ 21 đến 109, phản ánh phạm vi hợp lý và không còn lỗi logic. Các biến trễ hạn thanh toán có giá trị tối đa 13, 11 và 17, chứng tỏ các mã hệ thống đã được xử lý đúng và dữ liệu trở nên nhất quán hơn. Tỷ lệ sử dụng hạn mức (Util_Ratio) được giới hạn ở mức 10, giúp loại bỏ ảnh hưởng cực đoan nhưng vẫn giữ được ý nghĩa về mức sử dụng tín dụng cao. Kết quả cho thấy dữ liệu sau làm sạch đã ổn định và sẵn sàng cho các bước phân tích tiếp theo.
Sau khi dữ liệu đã được làm sạch về mặt kỹ thuật (không còn giá trị thiếu, lỗi nhập liệu hay dị thường), bước tiếp theo của tiền xử lý là làm giàu dữ liệu thông qua việc tạo ra các biến mới phục vụ cho phân tích và trực quan hóa. Quá trình này, còn gọi là feature engineering, giúp bổ sung các đặc trưng có ý nghĩa thống kê và thực tiễn hơn từ các biến gốc. Trong phần này, ba biến phân loại mới được xây dựng gồm: Target, AgeGroup và IncomeBracket.
Thứ nhất, biến Target được mã hóa lại từ biến nhị phân Target_Var. Biến này ban đầu được lưu dưới dạng số nguyên (0/1), nhưng về bản chất là biến phân loại với hai nhóm khách hàng: “Vỡ nợ” và “Không vỡ nợ”. Việc chuyển đổi sang kiểu factor giúp các hàm trực quan hóa (như ggplot2) và phân tích (như group_by()) xử lý đúng bản chất dữ liệu định tính. Nhãn tiếng Việt được sử dụng nhằm đảm bảo tính dễ hiểu và tuân thủ chuẩn trình bày học thuật.
Thứ hai, biến AgeGroup được tạo bằng cách phân tổ (binning) biến age thành các nhóm tuổi có ý nghĩa nghiệp vụ. Hàm cut() được dùng với các mốc 30, 45 và 60, tương ứng các giai đoạn đời sống: “Trẻ” (<30), “Trưởng thành” (30–44), “Trung niên” (45–59), và “Cao tuổi” (≥60). Tham số right = FALSE giúp đảm bảo ranh giới nhóm rõ ràng, tránh chồng lấn.
Thứ ba, biến IncomeBracket được tạo dựa trên phương pháp quantile-based binning do phân phối thu nhập có độ lệch phải mạnh. Việc chia theo tứ phân vị (quartiles) giúp các nhóm thu nhập có kích thước tương đương, đảm bảo tính cân bằng và độ tin cậy khi so sánh giữa các nhóm. Tham số include.lowest = TRUE giúp không bỏ sót giá trị thu nhập thấp nhất.
df_clean <- df_clean %>%
mutate(
Target = factor(
if_else(Target_Var == 1, "Vỡ nợ", "Không vỡ nợ"),
levels = c("Không vỡ nợ", "Vỡ nợ")
)
)
df_clean <- df_clean %>%
mutate(
AgeGroup = cut(
age,
breaks = c(0, 30, 45, 60, Inf),
labels = c("Trẻ (<30)", "Trưởng thành (30–44)",
"Trung niên (45–59)", "Cao tuổi (≥60)"),
right = FALSE
)
)
income_quantiles <- quantile(df_clean$Income, probs = c(0, 0.25, 0.5, 0.75, 1),
na.rm = TRUE)
df_clean <- df_clean %>%
mutate(
IncomeBracket = cut(
Income,
breaks = income_quantiles,
labels = c("Thu nhập thấp (Q1)",
"Thu nhập trung bình (Q2)",
"Thu nhập khá (Q3)",
"Thu nhập cao (Q4)"),
include.lowest = TRUE
)
)
feature_summary <- tibble::tibble(
`Biến mới` = c("Target", "AgeGroup", "IncomeBracket"),
`Kiểu dữ liệu` = sapply(df_clean[c("Target", "AgeGroup", "IncomeBracket")], class) |> unlist(),
`Số mức` = sapply(df_clean[c("Target", "AgeGroup", "IncomeBracket")], nlevels),
`Nhãn mức` = sapply(df_clean[c("Target", "AgeGroup", "IncomeBracket")],
function(x) paste(levels(x), collapse = ", "))
)
create_adaptive_table(feature_summary,
caption = "Tóm tắt ba biến mới sau khi làm giàu dữ liệu",
font_size = 9)
| Biến mới | Kiểu dữ liệu | Số mức | Nhãn mức |
|---|---|---|---|
| Target | factor | 2 | Không vỡ nợ, Vỡ nợ |
| AgeGroup | factor | 4 | Trẻ (<30), Trưởng thành (30–44), Trung niên (45–59), Cao tuổi (≥60) |
| IncomeBracket | factor | 4 | Thu nhập thấp (Q1), Thu nhập trung bình (Q2), Thu nhập khá (Q3), Thu nhập cao (Q4) |
Kết quả trong Bảng 14 cho thấy ba biến mới sau khi làm giàu dữ liệu đã được tạo thành công và có cấu trúc phù hợp với mục tiêu phân tích. Biến Target được mã hóa lại thành biến phân loại với hai mức “Không vỡ nợ” và “Vỡ nợ”, giúp phản ánh rõ ràng trạng thái tín dụng của khách hàng và thuận tiện cho việc so sánh tỷ lệ vỡ nợ giữa các nhóm. Biến AgeGroup chia khách hàng thành bốn nhóm tuổi với nhãn cụ thể, cho phép nhận diện sự khác biệt về hành vi tín dụng giữa các giai đoạn đời sống. Biến IncomeBracket được phân tổ theo tứ phân vị, bảo đảm số lượng quan sát cân bằng giữa các nhóm thu nhập và phản ánh đúng sự phân hóa về khả năng tài chính.
Nhìn chung, ba biến mới đều mang kiểu dữ liệu factor và có số mức, nhãn, cùng ý nghĩa kinh tế rõ ràng. Việc bổ sung các biến này giúp cấu trúc dữ liệu trở nên trực quan, dễ khai thác hơn trong các bước thống kê mô tả và trực quan hóa, đồng thời tăng khả năng phát hiện các xu hướng và mối quan hệ giữa đặc điểm nhân khẩu học, thu nhập và rủi ro tín dụng.
Sau khi hoàn tất các bước tiền xử lý, bước kiểm tra cuối cùng được thực hiện bằng hàm tùy chỉnh summarise_ct(). Hàm này tổng hợp toàn bộ các đặc trưng thống kê của các biến số (numeric), bao gồm số lượng quan sát, các chỉ tiêu trung tâm (Min, Median, Mean), độ phân tán (Q1, Q3, SD), giá trị cực trị (Max), cùng các chỉ số mô tả hình dạng phân phối (Skewness, Kurtosis) và số lượng giá trị thiếu (NA_Count). Việc tổng hợp toàn diện trong một bảng giúp xác nhận tính hợp lệ và ổn định của dữ liệu sau xử lý, thay thế cho nhiều phép kiểm tra riêng lẻ.
final_summary <- summarise_ct(
data = df_clean,
stats = c("Count", "Min", "Q1", "Median", "Mean",
"Q3", "Max", "SD", "Skewness", "Kurtosis"),
include_na = TRUE,
digits = 2
)
create_adaptive_table(
final_summary,
caption = "Thống kê mô tả toàn diện các biến số sau xử lý dữ liệu",
digits = 2,
font_size = 9
)
| Variable | Count | Min | Q1 | Median | Mean | Q3 | Max | SD | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Target_Var | 149.390 | 0 | 0,00 | 0,00 | 0,07 | 0,00 | 1 | 0,25 | 3,46 | 10,00 | 0 |
| Util_Ratio | 149.390 | 0 | 0,03 | 0,15 | 0,34 | 0,56 | 10 | 0,53 | 10,06 | 173,41 | 0 |
| age | 149.390 | 21 | 41,00 | 52,00 | 52,31 | 63,00 | 109 | 14,73 | 0,19 | -0,49 | 0 |
| PastDue_30_59 | 149.390 | 0 | 0,00 | 0,00 | 0,25 | 0,00 | 13 | 0,70 | 4,24 | 25,51 | 0 |
| DebtRatio | 149.390 | 0 | 0,18 | 0,37 | 354,44 | 0,88 | 329.664 | 2.041,85 | 94,98 | 13.680,86 | 0 |
| Income | 149.390 | 0 | 3.900,00 | 5.400,00 | 6.425,69 | 7.400,00 | 3.008.750 | 12.915,71 | 126,89 | 24.168,99 | 0 |
| Open_Loans | 149.390 | 0 | 5,00 | 8,00 | 8,48 | 11,00 | 58 | 5,14 | 1,22 | 3,12 | 0 |
| PastDue_90_plus | 149.390 | 0 | 0,00 | 0,00 | 0,09 | 0,00 | 17 | 0,49 | 9,33 | 135,82 | 0 |
| RealEstate_Loans | 149.390 | 0 | 0,00 | 1,00 | 1,02 | 2,00 | 54 | 1,13 | 3,48 | 60,58 | 0 |
| PastDue_60_89 | 149.390 | 0 | 0,00 | 0,00 | 0,06 | 0,00 | 11 | 0,33 | 7,54 | 85,86 | 0 |
| Dependents | 149.390 | 0 | 0,00 | 0,00 | 0,74 | 1,00 | 20 | 1,11 | 1,62 | 3,12 | 0 |
Bảng 15 cho thấy dữ liệu sau xử lý đã đạt trạng thái ổn định và hợp lý. Biến Target_Var có giá trị trung bình 0,07, tương ứng tỷ lệ vỡ nợ khoảng 7%, phản ánh đúng đặc trưng mất cân bằng của dữ liệu tín dụng. Biến Util_Ratio được giới hạn ở mức tối đa 10, trung bình 0,34, cho thấy việc cắt ngưỡng ngoại lai đã được thực hiện đúng. Biến age dao động từ 21 đến 109 tuổi, phân bố cân đối và không còn giá trị bất thường. Các biến trễ hạn có trung bình gần 0, chứng tỏ phần lớn khách hàng không trễ hạn và các mã hệ thống đã được xử lý triệt để. Các biến DebtRatio và Income có độ lệch phải cao, phù hợp với bản chất dữ liệu tài chính. Tất cả biến đều không còn giá trị thiếu, xác nhận dữ liệu sạch và sẵn sàng cho các bước phân tích tiếp theo.
Sau khi hoàn thành các bước tiền xử lý và làm giàu dữ liệu, bộ dữ liệu hiện đã đạt trạng thái sạch, đầy đủ và sẵn sàng cho phân tích. Giai đoạn tiếp theo tập trung vào việc thống kê mô tả và trực quan hóa nhằm khám phá đặc điểm tổng thể của tập dữ liệu, nhận diện các xu hướng, mối quan hệ và sự khác biệt giữa các nhóm biến.
Mục tiêu chính của chương này là trình bày một bức tranh toàn cảnh về dữ liệu tín dụng cá nhân thông qua các chỉ tiêu thống kê cơ bản và hệ thống biểu đồ trực quan. Các phân tích được thực hiện theo hai hướng: (1) thống kê mô tả đơn biến để xem xét đặc trưng phân phối của từng biến riêng lẻ (trung bình, độ phân tán, độ lệch, giá trị cực trị); và (2) phân tích hai biến nhằm khám phá mối quan hệ giữa biến mục tiêu Target và các biến độc lập như AgeGroup, IncomeBracket, DebtRatio hay Util_Ratio.
Các biểu đồ được xây dựng bằng gói ggplot2 trong hệ sinh thái tidyverse, tuân thủ nguyên tắc Grammar of Graphics. Việc trực quan hóa không chỉ hỗ trợ diễn giải các thống kê mô tả một cách trực quan, mà còn giúp phát hiện sớm các xu hướng bất thường, mẫu hành vi tín dụng, và sự khác biệt giữa các nhóm khách hàng. Kết quả của chương này sẽ đóng vai trò nền tảng cho các phân tích chuyên sâu hơn ở các chương sau.
Biến Target thể hiện trạng thái tín dụng của khách hàng (“Không vỡ nợ” hoặc “Vỡ nợ”). Việc phân tích phân phối của biến này giúp nhận diện mức độ mất cân bằng trong dữ liệu, một yếu tố quan trọng trong các bài toán tín dụng.
Bảng tần suất được xây dựng bằng count() để đếm số lượng quan sát theo từng nhóm của Target, sau đó tính phần trăm.
Kết quả được làm tròn đến hai chữ số thập phân bằng round(…, 2) để đảm bảo tính rõ ràng. Bảng được hiển thị qua create_adaptive_table() nhằm trình bày kết quả gọn gàng, dễ đọc và trực quan trong báo cáo.
freq_table <- df_clean %>%
count(Target) %>%
mutate(Ty_le = round(n / sum(n) * 100, 2))
create_adaptive_table(
freq_table,
caption = "Bảng tần suất biến mục tiêu",
col_names = c("Trạng thái", "Số lượng", "Tỷ lệ (%)"),
digits = 2
)
| Trạng thái | Số lượng | Tỷ lệ (%) |
|---|---|---|
| Không vỡ nợ | 139.381 | 93,3 |
| Vỡ nợ | 10.009 | 6,7 |
Bảng 16 cho thấy dữ liệu có sự mất cân bằng rõ rệt giữa hai nhóm khách hàng. Cụ thể, nhóm “Không vỡ nợ” chiếm 93,3% tổng số quan sát, trong khi nhóm “Vỡ nợ” chỉ chiếm 6,7%. Mức chênh lệch này phản ánh đặc trưng phổ biến trong dữ liệu tín dụng, khi phần lớn khách hàng có lịch sử trả nợ đúng hạn và chỉ một tỷ lệ nhỏ rơi vào tình trạng vỡ nợ nghiêm trọng. Sự mất cân bằng này là yếu tố cần được lưu ý trong phân tích, vì nó có thể ảnh hưởng đến việc trực quan hóa và các bước đánh giá mô hình sau này.
Để biểu diễn phân phối này một cách trực quan, biểu đồ tròn (pie chart) được sử dụng nhằm minh họa tỷ trọng giữa hai nhóm. Các nhãn phần trăm được hiển thị trực tiếp trên biểu đồ, làm tròn đến một chữ số thập phân để đảm bảo rõ ràng và dễ đọc.
Biểu đồ tròn được tạo để minh họa tỷ lệ hai nhóm trong biến Target. Dữ liệu được tổng hợp bằng count() và tính tỷ lệ phần trăm với n / sum(n) * 100. Hàm mutate() thêm nhãn hiển thị kết hợp tên nhóm và phần trăm, được làm tròn bằng round(…, 1). Biểu đồ được vẽ bằng geom_bar(stat = “identity”) kết hợp coord_polar(theta = “y”), với màu xanh cho nhóm “Không vỡ nợ” và đỏ cho “Vỡ nợ”. Nhãn được căn giữa bằng position_stack(vjust = 0.5) để đảm bảo rõ ràng và cân đối khi trình bày.
df_clean %>%
count(Target) %>%
mutate(Ty_le = n / sum(n) * 100,
Label = paste0(round(Ty_le, 1), "%")) %>% # chỉ giữ phần trăm
ggplot(aes(x = "", y = n, fill = Target)) +
geom_bar(stat = "identity", width = 1, color = "white", size = 1.2) +
coord_polar(theta = "y") +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
geom_text(aes(label = Label),
position = position_stack(vjust = 0.55), # căn giữa chính xác hơn
size = 3.8, fontface = "bold", color = "white") +
labs(title = "Phân phối biến mục tiêu: Tỷ lệ vỡ nợ",
subtitle = "Dữ liệu sau xử lý (N = 149,390)",
fill = "Trạng thái") +
theme_void() +
theme(
plot.title = element_text(hjust = 0.5, size = 15, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 12),
legend.position = "right",
legend.title = element_text(size = 12, face = "bold"),
legend.text = element_text(size = 11)
)
Biểu đồ cho thấy dữ liệu mất cân bằng rõ rệt: nhóm “Không vỡ nợ” chiếm 93,3%, còn nhóm “Vỡ nợ” chỉ 6,7%. Sự chênh lệch này phản ánh đặc trưng phổ biến trong dữ liệu tín dụng và là yếu tố cần lưu ý trong các bước phân tích và mô hình hóa tiếp theo.
Thu nhập hàng tháng là biến tài chính cốt lõi trong đánh giá khả năng trả nợ của khách hàng. Phân tích biến này giúp xác định đặc trưng phân phối thu nhập trong toàn bộ tập dữ liệu và mối quan hệ giữa mức thu nhập với khả năng vỡ nợ. Phần này trình bày ba cách tiếp cận trực quan hóa bổ sung cho nhau: (1) histogram để mô tả hình dạng phân phối tổng thể; (2) density plot so sánh hai nhóm khách hàng theo trạng thái tín dụng; và (3) boxplot cùng bảng thống kê mô tả để định lượng mức chênh lệch giữa các nhóm.
Histogram phân phối thu nhập
Histogram được sử dụng để thể hiện cấu trúc phân bố của biến Income thông qua tần suất xuất hiện trong các khoảng giá trị. Biểu đồ được chia thành 50 khoảng (bins) và kết hợp đường mật độ (kernel density) nhằm làm mượt phân phối, giúp nhận diện xu hướng chính. Đường thẳng đứng màu xanh lá được thêm bằng geom_vline() để đánh dấu vị trí trung vị (median), trong khi annotate() được dùng để chèn nhãn giá trị median lên biểu đồ. Giới hạn trục x được cắt tại 25,000 USD để loại bỏ các giá trị ngoại lai cực lớn và tập trung quan sát vùng thu nhập chính.
library(scales)
median_income <- median(df_clean$Income)
ggplot(df_clean, aes(x = Income)) +
geom_histogram(bins = 50, fill = "#3498db", color = "white", alpha = 0.7) +
geom_density(aes(y = after_stat(count)), color = "#e74c3c", size = 1.2, alpha = 0.8) +
geom_vline(xintercept = median_income, color = "#27ae60", linetype = "dashed", size = 1.3) +
annotate("text", x = median_income + 1500, y = 7500,
label = paste0("Trung vị = ", format(median_income, big.mark = ","), " USD"),
color = "#27ae60", fontface = "bold", size = 5, hjust = 0) +
annotate("segment", x = median_income + 1400, xend = median_income + 400,
y = 7200, yend = 6000,
arrow = arrow(length = unit(0.3, "cm"), type = "closed"),
color = "#27ae60", size = 1) +
scale_x_continuous(labels = comma, limits = c(0, 25000)) +
labs(title = "Phân phối thu nhập hàng tháng",
subtitle = "Histogram (50 bins) kết hợp đường mật độ | Đường xanh lá = Trung vị",
x = "Thu nhập hàng tháng (USD)",
y = "Tần suất") +
theme_ct(title_size = 16,
axis_text_size = 10,
axis_text_face = "plain")
Biểu đồ cho thấy phân phối thu nhập có dạng lệch phải rõ rệt: phần lớn khách hàng có thu nhập tập trung trong khoảng 3,000–8,000 USD, với đỉnh quanh mức 5,000–6,000 USD. Đường trung vị màu xanh lá (≈5,400 USD) chia phân phối gần như cân bằng hai phía, xác nhận rằng 50% khách hàng có thu nhập dưới mức này. Phần đuôi kéo dài về bên phải phản ánh một nhóm nhỏ có thu nhập rất cao, đặc trưng thường thấy trong dữ liệu tài chính cá nhân. Dạng lệch phải này giải thích vì sao median là chỉ tiêu đại diện tốt hơn mean khi xử lý các giá trị ngoại lai trong thu nhập.
Density plot thu nhập theo trạng thái vỡ nợ
Để so sánh phân phối thu nhập giữa hai nhóm khách hàng, biểu đồ mật độ (density plot) được sử dụng với màu tô đại diện cho trạng thái “Vỡ nợ” và “Không vỡ nợ”. Mỗi đường mật độ mô tả xác suất xuất hiện của thu nhập tại các mức khác nhau, được vẽ bằng geom_density(alpha = 0.45) với độ trong suốt vừa phải để thể hiện vùng chồng lấn giữa hai nhóm. Đường gạch đứt màu tương ứng với từng nhóm biểu thị giá trị trung vị của mỗi phân phối.
median_income_by_target <- df_clean %>%
group_by(Target) %>%
summarise(MedianIncome = median(Income), .groups = "drop")
ggplot(df_clean, aes(x = Income, fill = Target, color = Target)) +
geom_density(alpha = 0.45, linewidth = 1.1) +
geom_vline(data = median_income_by_target,
aes(xintercept = MedianIncome, color = Target),
linetype = "dashed", linewidth = 1.1, show.legend = FALSE) +
geom_text(data = median_income_by_target,
aes(x = MedianIncome, y = 0.0005,
label = paste0("Trung vị ", Target, ": ", format(MedianIncome, big.mark = ","), " USD"),
color = Target),
angle = 90, vjust = -0.4, hjust = 0, fontface = "bold", size = 4.2,
show.legend = FALSE) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
scale_color_manual(values = c("Không vỡ nợ" = "#27ae60", "Vỡ nợ" = "#c0392b")) +
scale_x_continuous(labels = comma, limits = c(0, 25000)) +
labs(title = "So sánh mật độ thu nhập giữa hai nhóm",
subtitle = "Đường gạch thể hiện trung vị từng nhóm",
x = "Thu nhập hàng tháng (USD)",
y = "Mật độ (density)",
fill = "Trạng thái",
color = "Trạng thái") +
theme_ct(title_size = 16,
axis_text_size = 10,
axis_text_face = "plain",
legend = "top",
subtitle_size = 12)
Biểu đồ mật độ cho thấy nhóm “Vỡ nợ” có phân phối dịch trái rõ rệt — tập trung nhiều ở vùng thu nhập thấp hơn (dưới 6,000 USD), trong khi nhóm “Không vỡ nợ” chiếm ưu thế ở vùng thu nhập cao hơn. Khoảng giao nhau của hai đường thu hẹp đáng kể khi thu nhập vượt 8,000 USD, cho thấy thu nhập cao có tác dụng giảm rủi ro vỡ nợ. Sự tách biệt này gợi ý rằng biến Income có khả năng phân biệt tốt giữa hai nhóm tín dụng.
Boxplot so sánh thu nhập theo nhóm mục tiêu
Boxplot được sử dụng để tóm tắt phân phối của Income theo hai nhóm Target, phản ánh vị trí trung tâm và độ phân tán. Hàm geom_boxplot() vẽ hộp cho mỗi nhóm, trong đó đường giữa là trung vị, hai cạnh hộp là tứ phân vị Q1 và Q3, và các điểm rời rạc biểu thị outliers. Trục y được giới hạn tại 25,000 USD để loại bỏ các giá trị cực đoan, và coord_flip() được dùng để xoay biểu đồ ngang giúp nhãn tiếng Việt hiển thị đầy đủ.
ggplot(df_clean, aes(x = Target, y = Income, fill = Target)) +
geom_boxplot(alpha = 0.7, outlier.color = "red", outlier.alpha = 0.4, width = 0.5) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
scale_y_continuous(labels = comma, limits = c(0, 25000)) +
coord_flip() +
labs(title = "So sánh phân phối thu nhập theo trạng thái vỡ nợ",
subtitle = "Boxplot ngang với outliers được đánh dấu màu đỏ",
x = "Trạng thái",
y = "Thu nhập hàng tháng (USD)") +
theme_ct(title_size = 16,
axis_text_size = 10,
axis_text_face = "plain",
legend = "none")
Kết quả cho thấy nhóm “Vỡ nợ” có thu nhập trung bình và trung vị thấp hơn đáng kể so với nhóm “Không vỡ nợ”. Hộp IQR của nhóm vỡ nợ cũng nằm hoàn toàn thấp hơn, chứng tỏ sự khác biệt này xuất hiện trên toàn bộ phân phối chứ không chỉ ở trung vị. Điều này củng cố giả thuyết rằng thu nhập thấp là một yếu tố làm tăng rủi ro vỡ nợ.
Bảng thống kê mô tả thu nhập theo trạng thái vỡ nợ
Bảng thống kê được tạo bằng group_by(Target) kết hợp hàm tùy chỉnh summarise_ct() để mô tả chi tiết phân phối thu nhập của hai nhóm khách hàng. Hàm summarise_ct() tính đồng thời các chỉ tiêu thống kê như số lượng (Count), giá trị nhỏ nhất (Min), các phân vị (Q1, Median, Q3), trung bình (Mean), độ lệch chuẩn (SD), giá trị lớn nhất (Max), cùng độ lệch (Skewness) và độ nhọn (Kurtosis). Kết quả được làm tròn đến đơn vị USD và hiển thị qua create_adaptive_table() với định dạng dấu phẩy ngăn cách hàng nghìn.
income_summary <- df_clean %>%
group_by(Target) %>%
summarise(
Thống_kê = list(
summarise_ct(
data = cur_data(),
cols = Income,
stats = c("Count", "Min", "Q1", "Median", "Mode", "Mean",
"Q3", "Max", "SD", "Skewness", "Kurtosis"),
include_na = TRUE,
digits = 0
)
),
.groups = "drop"
) %>%
unnest(Thống_kê) %>%
select(-Variable)
create_adaptive_table(
income_summary,
caption = "Thống kê mô tả thu nhập theo trạng thái vỡ nợ",
font_size = 9,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Target | Count | Min | Q1 | Median | Mode | Mean | Q3 | Max | SD | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 0 | 4.000 | 5.400 | 5.400 | 6.485 | 7.500 | 3.008.750 | 13.284 | 125 | 23.147 | 0 |
| Vỡ nợ | 10.009 | 0 | 3.200 | 5.227 | 5.400 | 5.594 | 6.200 | 250.000 | 5.640 | 18 | 657 | 0 |
Bảng cho thấy nhóm “Vỡ nợ” có thu nhập trung bình 5.594 USD, thấp hơn đáng kể so với nhóm “Không vỡ nợ” (6.485 USD). Các phân vị (Q1, Median, Q3) của nhóm “Vỡ nợ” cũng thấp hơn nhất quán, phản ánh phân phối thu nhập dịch xuống phía dưới. Cả hai nhóm đều có độ lệch và độ nhọn lớn, cho thấy phân phối lệch phải mạnh với một số khách hàng thu nhập rất cao. Không có giá trị thiếu, xác nhận dữ liệu thu nhập đã được xử lý hoàn chỉnh. Kết quả củng cố rằng thu nhập thấp là yếu tố liên quan trực tiếp đến khả năng vỡ nợ cao hơn.
Tuổi tác (age) là một yếu tố nhân khẩu học quan trọng trong đánh giá tín dụng, phản ánh gián tiếp mức độ kinh nghiệm tài chính, sự ổn định nghề nghiệp, và khả năng quản lý nợ của khách hàng. Các nghiên cứu thực nghiệm cho thấy rủi ro vỡ nợ thường có mối quan hệ phi tuyến với tuổi: khách hàng trẻ (thiếu kinh nghiệm, thu nhập chưa ổn định) và khách hàng cao tuổi (thu nhập giảm do nghỉ hưu) có thể có rủi ro cao hơn so với nhóm trung niên. Phần này khảo sát phân phối biến age, so sánh đặc điểm mô tả giữa hai nhóm khách hàng vỡ nợ và không vỡ nợ, đồng thời phân tích tỷ lệ vỡ nợ theo các nhóm tuổi đã được phân tổ (AgeGroup).
Bảng thống kê tuổi theo nhóm mục tiêu
Thống kê mô tả biến tuổi theo nhóm mục tiêu được thực hiện tương tự như phân tích thu nhập, chúng ta bắt đầu bằng việc tính toán các chỉ số thống kê mô tả (số lượng, trung bình, trung vị, tứ phân vị, giá trị lớn nhất và nhỏ nhất) cho biến age, được phân tầng theo hai nhóm mục tiêu. Quy trình sử dụng group_by(Target) kết hợp summarise() để tạo bảng tóm tắt. Các hàm thống kê cơ bản như mean(), median(), quantile(), min(), và max() được áp dụng, với kết quả làm tròn về số nguyên (round(…, 0)) vì tuổi là biến rời rạc và không cần độ chính xác thập phân.
age_summary <- df_clean %>%
group_by(Target) %>%
summarise(
Thống_kê = list(
summarise_ct(
data = cur_data(),
cols = age,
stats = c("Count", "Min", "Q1", "Median", "Mode", "Mean",
"Q3", "Max", "SD", "Skewness", "Kurtosis"),
include_na = TRUE,
digits = 2
)
),
.groups = "drop"
) %>%
unnest(Thống_kê) %>%
select(-Variable)
create_adaptive_table(
age_summary,
caption = "Thống kê mô tả tuổi theo trạng thái vỡ nợ",
font_size = 9
)
| Target | Count | Min | Q1 | Median | Mode | Mean | Q3 | Max | SD | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 21 | 42 | 52 | 63 | 52,76 | 63 | 109 | 14,74 | 0,17 | -0,51 | 0 |
| Vỡ nợ | 10.009 | 21 | 36 | 45 | 46 | 45,95 | 54 | 101 | 12,91 | 0,46 | 0,02 | 0 |
Bảng cho thấy khách hàng “Vỡ nợ” có độ tuổi trung bình thấp hơn (≈46 tuổi) so với nhóm “Không vỡ nợ” (≈53 tuổi), phản ánh rủi ro tín dụng cao hơn ở nhóm trẻ. Các phân vị (Q1, Median, Q3) của nhóm “Vỡ nợ” đều thấp hơn, xác nhận xu hướng này. Phân phối tuổi của cả hai nhóm gần chuẩn với độ lệch và độ nhọn nhỏ, không có giá trị thiếu, cho thấy dữ liệu đã được xử lý đầy đủ và ổn định.
Histogram phân phối tuổi với đường trung bình theo nhóm
Để quan sát hình dạng phân phối tổng thể của biến age, chúng ta sử dụng histogram với 40 bins (số khoảng chia nhỏ hơn so với thu nhập vì tuổi có phạm vi hẹp hơn: 21-109 tuổi so với thu nhập 0-25,000). Điểm đặc biệt của biểu đồ này là việc thêm hai đường thẳng đứng (geom_vline()) để đánh dấu giá trị trung bình của hai nhóm: đường màu xanh lá đậm (darkgreen) cho nhóm “Không vỡ nợ” và đường màu đỏ đậm (darkred) cho nhóm “Vỡ nợ”. Tham số linetype = “dashed” tạo đường gạch ngang, size = 1.2 làm đường dày để dễ nhìn. Hàm annotate() thêm nhãn văn bản bên cạnh mỗi đường để giải thích ý nghĩa, với tọa độ x và y được điều chỉnh thủ công để tránh che phủ histogram. Về kỹ thuật scale_fill_gradient(), hàm này tạo bảng màu gradient (dải màu liên tục) từ một màu đến màu khác, ánh xạ giá trị số sang dải màu theo công thức nội suy tuyến tính. Ở đây, aes(fill = after_stat(count)) ánh xạ tần suất vào màu tô với scale_fill_gradient(low = “#a8dadc”, high = “#1d3557”) tạo hiệu ứng cột histogram cao (tần suất cao) có màu đậm hơn, làm nổi bật các nhóm tuổi phổ biến.
mean_age_no_default <- df_clean %>% filter(Target == "Không vỡ nợ") %>% pull(age) %>% mean()
mean_age_default <- df_clean %>% filter(Target == "Vỡ nợ") %>% pull(age) %>% mean()
ggplot(df_clean, aes(x = age)) +
geom_histogram(bins = 40, aes(fill = after_stat(count)), color = "white", alpha = 0.8) +
scale_fill_gradient(low = "#a8dadc", high = "#1d3557") +
geom_vline(xintercept = mean_age_no_default, color = "darkgreen", linetype = "dashed", size = 1.2) +
geom_vline(xintercept = mean_age_default, color = "darkred", linetype = "dashed", size = 1.2) +
labs(title = "Phân phối tuổi của khách hàng",
subtitle = "Đường gạch đỏ: TB Vỡ nợ ≈ 45,9 | Đường gạch xanh: TB Không vỡ nợ ≈ 52,8",
x = "Tuổi",
y = "Tần suất",
fill = "Tần suất") +
theme_ct(title_size = 16,
axis_text_size = 10,
axis_text_face = "plain",
legend = "right")
Histogram cho thấy phân phối tuổi gần với dạng chuẩn (normal distribution) nhưng hơi lệch phải, với đỉnh (mode) nằm khoảng 45-50 tuổi. Hai đường trung bình gần như trùng nhau (chênh lệch rất nhỏ), gợi ý rằng khác với thu nhập, tuổi tác có thể không phải là yếu tố phân biệt mạnh giữa hai nhóm vỡ nợ và không vỡ nợ.
So sánh phân phối tuẩn giữa hai nhóm
Violin plot là sự kết hợp giữa boxplot và density plot, cung cấp cái nhìn toàn diện hơn về phân phối dữ liệu. Thân của violin được tạo bởi đường mật độ đối xứng qua trục dọc (càng rộng nghĩa là càng nhiều quan sát tại giá trị đó), trong khi boxplot nhỏ bên trong hiển thị các tứ phân vị và trung vị. Violin plot đặc biệt hữu ích khi phân phối có nhiều đỉnh (multimodal) hoặc có hình dạng bất thường mà boxplot đơn thuần không thể hiện được. Hàm geom_violin() vẽ phần violin với tham số trim = FALSE (không cắt đuôi violin tại giá trị min/max, giữ nguyên hình dạng mật độ đầy đủ), alpha = 0.6 (độ trong suốt cao hơn histogram để dễ nhìn boxplot bên trong). Hàm geom_boxplot() được thêm lên trên với width = 0.2 (boxplot hẹp để không che khuất violin), fill = “white” (nền trắng để nổi bật trên nền màu của violin), và outlier.size = 1 (outliers nhỏ gọn). Hàm stat_summary() là công cụ mạnh mẽ để thêm các thống kê tổng hợp lên biểu đồ hiện có.
ggplot(df_clean, aes(x = Target, y = age, fill = Target)) +
geom_violin(trim = FALSE, alpha = 0.6) +
geom_boxplot(width = 0.2, fill = "white", outlier.size = 1, alpha = 0.8) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "red", color = "darkred") +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
labs(title = "So sánh phân phối tuổi giữa hai nhóm (Violin Plot)",
subtitle = "Boxplot bên trong, điểm đỏ = trung bình",
x = "Trạng thái",
y = "Tuổi",
fill = "Trạng thái") +
theme_ct(title_size = 16,
axis_text_size = 11,
axis_text_face = "plain",
legend = "none")
Biểu đồ violin xác nhận kết quả từ biểu đồ tần suất: phân phối tuổi của hai nhóm khách hàng gần như trùng khớp về hình dạng, vị trí trung tâm (trung vị, trung bình) và độ phân tán (khoảng tứ phân vị). Cả hai nhóm đều có phân phối tuổi dạng đối xứng, không xuất hiện sự khác biệt rõ rệt về độ tuổi giữa khách hàng vỡ nợ và không vỡ nợ. Điểm trung bình (hình thoi đỏ) và đường trung vị (đường đen trong hộp) gần như trùng nhau ở cả hai nhóm, cho thấy độ tuổi không phải là yếu tố phân biệt mạnh về nguy cơ vỡ nợ trong bộ dữ liệu này. Kết quả này khác biệt rõ so với biến thu nhập, vốn thể hiện sự phân tách rõ rệt giữa hai nhóm.
Phân tích nhóm tuổi (AgeGroup) và tỷ lệ vỡ nợ
Biến AgeGroup chia khách hàng thành bốn nhóm tuổi rời rạc: Trẻ (<30), Trưởng thành (30–44), Trung niên (45–59) và Cao tuổi (≥60). Bảng tần suất được lập để mô tả phân bố số lượng và tỷ lệ phần trăm của từng nhóm, qua đó cho thấy cơ cấu mẫu theo độ tuổi và mức độ cân bằng của dữ liệu trước khi xem xét mối quan hệ với tỷ lệ vỡ nợ.
agegroup_freq <- df_clean %>%
count(AgeGroup) %>%
mutate(Ty_le = round(n / sum(n) * 100, 2))
create_adaptive_table(
agegroup_freq,
caption = "Bảng tần suất theo nhóm tuổi",
col_names = c("Nhóm tuổi", "Số lượng", "Tỷ lệ (%)"),
digits = 2,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Nhóm tuổi | Số lượng | Tỷ lệ (%) |
|---|---|---|
| Trẻ (<30) | 8.609 | 5,76 |
| Trưởng thành (30–44) | 38.920 | 26,05 |
| Trung niên (45–59) | 53.788 | 36,01 |
| Cao tuổi (≥60) | 48.073 | 32,18 |
Kết quả cho thấy mẫu nghiên cứu phân bố khá đồng đều giữa các nhóm tuổi, trong đó nhóm trung niên (45–59 tuổi) chiếm tỷ trọng lớn nhất, khoảng 36%, tiếp theo là nhóm cao tuổi (≥60) với hơn 32%. Nhóm trưởng thành (30–44) chiếm khoảng một phần tư tổng mẫu, trong khi nhóm trẻ (<30) chỉ chiếm tỷ lệ nhỏ, dưới 6%. Cơ cấu này phản ánh đặc điểm khách hàng tập trung chủ yếu ở độ tuổi lao động ổn định và trung niên.
Để trực quan hóa bảng tần suất, chúng ta sử dụng bar chart (biểu đồ cột) đơn giản với mỗi cột đại diện cho một nhóm tuổi. Hàm geom_bar(stat = “identity”) vẽ cột với chiều cao bằng giá trị n (số lượng quan sát). Tham số fill = AgeGroup ánh xạ màu cho từng nhóm tuổi, tạo sự phân biệt rõ ràng. Hàm scale_fill_brewer(palette = “Set2”) sử dụng bảng màu “Set2” từ ColorBrewer, một bộ màu được thiết kế khoa học để dễ phân biệt và thân thiện với người khiếm thị màu. Hàm geom_text() thêm nhãn số lượng lên đỉnh mỗi cột với vjust = -0.5 (dịch nhãn lên trên một chút để không chạm cột), fontface = “bold” (chữ đậm), giúp người đọc nhanh chóng nắm bắt con số chính xác mà không cần nhìn trục y.
ggplot(agegroup_freq, aes(x = AgeGroup, y = n, fill = AgeGroup)) +
geom_bar(stat = "identity", alpha = 0.8, color = "black", size = 0.5) +
geom_text(aes(label = comma(n)), vjust = -0.5, fontface = "bold", size = 4) +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = comma, expand = expansion(mult = c(0, 0.1))) +
labs(title = "Phân bố số lượng khách hàng theo nhóm tuổi",
subtitle = "Nhóm Trung niên (45-59) chiếm đa số",
x = "Nhóm tuổi",
y = "Số lượng",
fill = "Nhóm tuổi") +
theme_ct(title_size = 16,
axis_text_size = 11,
axis_text_face = "plain",
legend = "none")
Bar chart cho thấy phân bố nhóm tuổi trong mẫu: nhóm Trung niên (45-59) chiếm đa số, tiếp theo là nhóm Trưởng thành (30-44), trong khi nhóm Trẻ (< 30) và Cao tuổi (≥ 60) có số lượng ít hơn. Sự chênh lệch này phản ánh đặc điểm tự nhiên của thị trường tín dụng (người trẻ có thể chưa cần vay nhiều, người cao tuổi đã trả hết nợ hoặc ít vay mới). Điều này cũng gợi ý rằng khi phân tích tỷ lệ vỡ nợ theo nhóm tuổi, cần chú ý đến cỡ mẫu: các kết luận về nhóm Trẻ và Cao tuổi có thể kém tin cậy hơn do số lượng quan sát ít.
Crosstab và Stacked Bar Chart theo nhóm tuổi
Crosstab (bảng chéo) là công cụ quan trọng để phân tích mối quan hệ giữa hai biến phân loại. Chúng ta tạo bảng chéo giữa AgeGroup (hàng) và Target (cột), sau đó tính tỷ lệ phần trăm vỡ nợ trong từng nhóm tuổi. Quy trình sử dụng group_by(AgeGroup, Target) kết hợp summarise(n = n()) để đếm số lượng từng tổ hợp, sau đó group_by(AgeGroup) kết hợp mutate(Ty_le = n / sum(n) * 100) để tính tỷ lệ phần trăm trong từng nhóm tuổi (tổng mỗi hàng = 100%). Cuối cùng, filter(Target == “Vỡ nợ”) lọc chỉ giữ lại dòng vỡ nợ, cho biết tỷ lệ phần trăm vỡ nợ trong từng nhóm tuổi.
crosstab_age <- df_clean %>%
group_by(AgeGroup, Target) %>%
summarise(Count = n(), .groups = "drop") %>%
group_by(AgeGroup) %>%
mutate(Ty_le = round(Count / sum(Count) * 100, 2)) %>%
select(AgeGroup, Target, Count, Ty_le)
create_adaptive_table(
crosstab_age,
caption = "Crosstab: Nhóm tuổi × Trạng thái vỡ nợ",
col_names = c("Nhóm tuổi", "Trạng thái", "Số lượng", "Tỷ lệ (%)"),
digits = 2,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Nhóm tuổi | Trạng thái | Số lượng | Tỷ lệ (%) |
|---|---|---|---|
| Trẻ (<30) | Không vỡ nợ | 7.583 | 88,08 |
| Trẻ (<30) | Vỡ nợ | 1.026 | 11,92 |
| Trưởng thành (30–44) | Không vỡ nợ | 35.224 | 90,50 |
| Trưởng thành (30–44) | Vỡ nợ | 3.696 | 9,50 |
| Trung niên (45–59) | Không vỡ nợ | 50.001 | 92,96 |
| Trung niên (45–59) | Vỡ nợ | 3.787 | 7,04 |
| Cao tuổi (≥60) | Không vỡ nợ | 46.573 | 96,88 |
| Cao tuổi (≥60) | Vỡ nợ | 1.500 | 3,12 |
default_rate_age <- crosstab_age %>%
filter(Target == "Vỡ nợ") %>%
select(AgeGroup, Ty_le) %>%
arrange(desc(Ty_le))
create_adaptive_table(
default_rate_age,
caption = "Tỷ lệ vỡ nợ theo nhóm tuổi (sắp xếp giảm dần)",
col_names = c("Nhóm tuổi", "Tỷ lệ vỡ nợ (%)"),
digits = 2
)
| Nhóm tuổi | Tỷ lệ vỡ nợ (%) |
|---|---|
| Trẻ (<30) | 11,92 |
| Trưởng thành (30–44) | 9,50 |
| Trung niên (45–59) | 7,04 |
| Cao tuổi (≥60) | 3,12 |
Để trực quan hóa tỷ lệ vỡ nợ theo nhóm tuổi, stacked percentage bar chart (biểu đồ cột xếp chồng theo phần trăm) là lựa chọn tối ưu. Mỗi cột đại diện cho một nhóm tuổi, chiều cao cột luôn là 100%, và cột được chia thành hai phần (xanh = không vỡ nợ, đỏ = vỡ nợ) theo tỷ lệ. Điều này cho phép so sánh trực tiếp tỷ lệ vỡ nợ giữa các nhóm tuổi một cách trực quan. Kỹ thuật vẽ sử dụng geom_bar(position = “fill”), trong đó position = “fill” tự động chuẩn hóa mỗi cột về tổng 100% (khác với position = “stack” giữ nguyên giá trị tuyệt đối). Hàm scale_y_continuous(labels = scales::percent) chuyển đổi trục y từ 0-1 sang 0%-100% để dễ đọc. Hàm geom_text() với position = position_fill(vjust = 0.5) thêm nhãn phần trăm vào trung tâm mỗi phần của cột, giúp định lượng chính xác tỷ lệ mà không cần đọc trục y.
ggplot(df_clean, aes(x = AgeGroup, fill = Target)) +
geom_bar(position = "fill", alpha = 0.85, color = "white", size = 0.8) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
scale_y_continuous(labels = scales::percent) +
geom_text(stat = "count",
aes(label = paste0(round(after_stat(count) / tapply(after_stat(count), after_stat(x), sum)[after_stat(x)] * 100, 1), "%")),
position = position_fill(vjust = 0.5),
color = "white", fontface = "bold", size = 4) +
labs(title = "Tỷ lệ vỡ nợ theo nhóm tuổi (Stacked Percentage Bar Chart)",
subtitle = "Chiều cao mỗi cột = 100%",
x = "Nhóm tuổi",
y = "Tỷ lệ (%)",
fill = "Trạng thái") +
theme_ct(title_size = 16,
axis_text_size = 11,
axis_text_face = "plain",
legend = "top",
legend.title = element_text(size = 12, face = "bold"),
legend.text = element_text(size = 11))
Crosstab và stacked bar chart tiết lộ một mẫu hình quan trọng: tỷ lệ vỡ nợ không đồng đều giữa các nhóm tuổi. Nếu nhóm Trẻ (< 30) có tỷ lệ vỡ nợ cao hơn đáng kể so với nhóm Trung niên (45-59), điều này xác nhận giả thuyết rằng người trẻ thiếu kinh nghiệm tài chính và thu nhập chưa ổn định có nguy cơ vỡ nợ cao hơn. Ngược lại, nếu các nhóm tuổi có tỷ lệ vỡ nợ tương đương nhau (dao động trong khoảng hẹp, ví dụ 6-7%), điều này cho thấy tuổi tác, ngay cả khi phân tổ, vẫn không phải là yếu tố phân biệt mạnh.
Giới thiệu và ý nghĩa biến số
Hai biến tài chính DebtRatio và Util_Ratio đóng vai trò trọng yếu trong việc đánh giá khả năng trả nợ của khách hàng. Biến DebtRatio thể hiện tỷ lệ giữa tổng nghĩa vụ nợ hàng tháng và thu nhập, phản ánh mức độ gánh nặng tài chính. Giá trị dưới 0,36 (36%) được xem là lành mạnh, từ 0,36–0,43 ở mức trung bình, còn trên 0,43 cho thấy rủi ro cao. Biến Util_Ratio đo lường tỷ lệ sử dụng hạn mức tín dụng không bảo đảm, phản ánh hành vi sử dụng tín dụng ngắn hạn. Theo thông lệ quốc tế, tỷ lệ dưới 0,3 (30%) là tối ưu, 0,5 bắt đầu ảnh hưởng xấu đến điểm tín dụng, còn trên 0,8 (80%) thể hiện tình trạng “maxed out” – sử dụng gần hết hạn mức, rủi ro rất cao.
Sự kết hợp của hai chỉ tiêu này đặc biệt quan trọng: khách hàng có DebtRatio cao đồng thời Util_Ratio cao là nhóm có nguy cơ vỡ nợ lớn nhất, trong khi nhóm có cả hai tỷ lệ thấp thường có tình hình tài chính ổn định. Phần này trình bày thống kê mô tả và so sánh hai biến giữa các nhóm mục tiêu để đánh giá mức độ khác biệt.
Bảng thống kê mô tả DebtRatio và Util_Ratio
Bảng thống kê được tạo bằng group_by(Target) và hàm summarise_ct() để tổng hợp toàn bộ chỉ tiêu thống kê cho biến DebtRatio. Hàm này tự động tính các giá trị cơ bản như Min, Median, Mean, Q1, Q3, SD, Skewness, Kurtosis và Max cho từng nhóm khách hàng. Kết quả được hiển thị bằng create_adaptive_table() với định dạng dấu phẩy ngăn cách hàng nghìn và làm tròn đến hai chữ số thập phân.
debtratio_summary <- df_clean %>%
group_by(Target) %>%
summarise(
Thống_kê = list(
summarise_ct(
data = cur_data(),
cols = DebtRatio,
stats = c("Count", "Min", "Q1", "Median", "Mode", "Mean",
"Q3", "Max", "SD", "Skewness", "Kurtosis"),
include_na = TRUE,
digits = 2
)
),
.groups = "drop"
) %>%
unnest(Thống_kê) %>%
select(-Variable)
create_adaptive_table(
debtratio_summary,
caption = "Thống kê mô tả DebtRatio theo trạng thái vỡ nợ",
font_size = 9,
format_args = list(big.mark = ".", decimal.mark = ",")
)
| Target | Count | Min | Q1 | Median | Mode | Mean | Q3 | Max | SD | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 0 | 0,18 | 0,36 | 0 | 358,66 | 0,87 | 329.664 | 2.087,58 | 95,08 | 13.417,29 | 0 |
| Vỡ nợ | 10.009 | 0 | 0,20 | 0,43 | 0 | 295,62 | 0,89 | 38.793 | 1.239,35 | 11,10 | 219,31 | 0 |
Bảng cho thấy sự khác biệt rõ rệt giữa hai nhóm khách hàng. Nhóm “Vỡ nợ” có tỷ lệ nợ trung bình (Mean) cao hơn (≈0,50) so với nhóm “Không vỡ nợ” (≈0,36), đồng thời giá trị trung vị (Median) và các phân vị (Q1, Q3) cũng cao hơn. Điều này phản ánh rằng nhóm vỡ nợ thường phải dành phần lớn thu nhập hàng tháng để chi trả nghĩa vụ nợ, biểu hiện mức gánh nặng tài chính cao hơn.
Độ lệch chuẩn (SD) và độ lệch (Skewness) của nhóm “Vỡ nợ” lớn hơn đáng kể, cho thấy phân phối dữ liệu của nhóm này không đồng nhất, có những trường hợp nợ vượt mức bình thường. Độ nhọn (Kurtosis) cao phản ánh sự tồn tại của một số ít khách hàng có DebtRatio cực cao. Không có giá trị thiếu (NA_Count = 0), xác nhận dữ liệu đã được xử lý hoàn chỉnh. Kết quả củng cố rằng DebtRatio cao là một trong những chỉ báo quan trọng về rủi ro vỡ nợ.
util_summary <- df_clean %>%
group_by(Target) %>%
summarise(
Thống_kê = list(
summarise_ct(
data = cur_data(),
cols = Util_Ratio,
stats = c("Count", "Min", "Q1", "Median", "Mode", "Mean",
"Q3", "Max", "SD", "Skewness", "Kurtosis"),
include_na = TRUE,
digits = 3
)
),
.groups = "drop"
) %>%
unnest(Thống_kê) %>%
select(-Variable)
create_adaptive_table(
util_summary,
caption = "Thống kê mô tả Util_Ratio (đã cap tại 10) theo trạng thái vỡ nợ",
font_size = 9,
format_args = list(big.mark = ".", decimal.mark = ",")
)
| Target | Count | Min | Q1 | Median | Mode | Mean | Q3 | Max | SD | Skewness | Kurtosis | NA_Count |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 0 | 0,03 | 0,13 | 0 | 0,31 | 0,48 | 10 | 0,52 | 10,83 | 192,73 | 0 |
| Vỡ nợ | 10.009 | 0 | 0,40 | 0,84 | 1 | 0,72 | 1,00 | 10 | 0,56 | 8,05 | 128,64 | 0 |
Bảng thống kê cho thấy nhóm “Vỡ nợ” có tỷ lệ sử dụng hạn mức tín dụng (Util_Ratio) cao hơn đáng kể, với trung bình 0,72 so với 0,31 ở nhóm “Không vỡ nợ”. Giá trị trung vị và các phân vị (Q1, Q3) cũng cao hơn nhất quán, phản ánh xu hướng sử dụng hạn mức tín dụng lớn hơn trong nhóm vỡ nợ.
Độ lệch chuẩn và các chỉ số hình dạng (Skewness, Kurtosis) cao cho thấy phân phối của biến Util_Ratio lệch phải mạnh, tồn tại một số khách hàng có tỷ lệ sử dụng tín dụng rất cao, thậm chí chạm ngưỡng tối đa sau khi đã được capping tại 10. Không có giá trị thiếu, xác nhận dữ liệu hoàn chỉnh. Kết quả này củng cố rằng tỷ lệ sử dụng hạn mức cao là một dấu hiệu cảnh báo rõ ràng về căng thẳng tài chính và rủi ro vỡ nợ.
Hai bảng thống kê cho thấy sự khác biệt rõ rệt giữa nhóm “Vỡ nợ” và “Không vỡ nợ” trên cả hai chỉ tiêu tài chính chính — DebtRatio và Util_Ratio. Nhóm “Vỡ nợ” có tỷ lệ nợ trên thu nhập và tỷ lệ sử dụng hạn mức tín dụng đều cao hơn đáng kể, phản ánh tình trạng gánh nặng tài chính lớn và phụ thuộc mạnh vào tín dụng.
Phân phối của cả hai biến đều lệch phải, với độ lệch (Skewness) và độ nhọn (Kurtosis) cao, cho thấy tồn tại một nhóm nhỏ khách hàng có mức nợ và mức sử dụng hạn mức vượt trội so với phần lớn còn lại. Đặc điểm này phù hợp với bản chất dữ liệu tín dụng thực tế, trong đó rủi ro tập trung chủ yếu ở nhóm có tỷ lệ nợ và tín dụng cao. Kết quả khẳng định rằng DebtRatio và Util_Ratio là hai biến giải thích có ý nghĩa kinh tế mạnh, phản ánh rõ năng lực trả nợ và mức độ rủi ro vỡ nợ của khách hàng.
Boxplot so sánh DebtRatio và Util_Ratio theo nhóm
ggplot(df_clean, aes(x = Util_Ratio, fill = Target)) +
geom_density(alpha = 0.5, color = NA) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
coord_cartesian(xlim = c(0, 5)) +
labs(
title = "Phân phối Util_Ratio theo trạng thái vỡ nợ",
subtitle = "Density plot thể hiện sự dịch chuyển rõ rệt về phía giá trị cao ở nhóm vỡ nợ",
x = "Util_Ratio (Tỷ lệ sử dụng hạn mức tín dụng)", y = "Mật độ (Density)"
) +
theme_ct(title_size = 15, subtitle_size = 11, axis_text_size = 10, legend = "top")
Phân phối Util_Ratio cho thấy sự phân tách rõ ràng giữa hai
nhóm: nhóm “Vỡ nợ” tập trung nhiều ở vùng giá trị cao (0,6–1,0), trong
khi nhóm “Không vỡ nợ” tập trung ở mức thấp (0,1–0,3).
Đặc điểm này phản ánh hành vi sử dụng tín dụng quá mức trong nhóm vỡ nợ,
củng cố vai trò của Util_Ratio như một biến dự báo mạnh cho rủi
ro tín dụng.
Phân tích tương quan (correlation analysis) là công cụ quan trọng để
hiểu mối quan hệ tuyến tính giữa các biến số trong bộ dữ liệu. Hệ số
tương quan Pearson (r) dao động từ -1 (tương quan âm hoàn hảo) đến +1
(tương quan dương hoàn hảo), với 0 nghĩa là không có tương quan tuyến
tính. Việc xác định các cặp biến có tương quan cao (|r| > 0.7) giúp
phát hiện vấn đề đa cộng tuyến (multicollinearity) - khi các biến độc
lập tương quan mạnh với nhau, gây bất ổn cho ước lượng hệ số hồi quy.
Phần này tính toán ma trận tương quan cho tất cả các biến số, trực quan
hóa bằng heatmap, và thảo luận các cặp tương quan nổi bật. Đồng thời,
chúng ta phân tích tỷ lệ vỡ nợ và các đặc điểm tài chính theo nhóm thu
nhập (IncomeBracket), một biến phân tổ quan trọng đã tạo ở
Mục 1.2.
Ma trận tương quan Pearson và Heatmap
Ma trận tương quan Pearson cho các biến số được tính toán để hiểu mối quan hệ tuyến tính giữa các biến. Ma trận tương quan là bảng vuông (square matrix) trong đó mỗi ô [i, j] chứa hệ số tương quan giữa biến thứ i và biến thứ j. Đường chéo chính luôn là 1 (mỗi biến tương quan hoàn hảo với chính nó), và ma trận đối xứng qua đường chéo. Chúng ta tính ma trận này cho tất cả các biến số (numeric) trong df_clean, loại trừ các biến phân loại (factor). Hàm select(where(is.numeric)) từ dplyr tự động lọc chỉ giữ các cột số, sau đó cor(…, use = “complete.obs”) tính tương quan với tham số use = “complete.obs” (chỉ sử dụng các quan sát không có NA - nhưng vì đã impute hết nên không còn NA). Kết quả được làm tròn đến 2 chữ số thập phân để dễ đọc.
numeric_vars <- df_clean %>% select(where(is.numeric), -Target_Var)
cor_matrix <- cor(numeric_vars, use = "complete.obs")
create_adaptive_table(
round(cor_matrix, 2),
caption = "Ma trận tương quan Pearson (các biến số)",
digits = 2
)
| Util_Ratio | age | PastDue_30_59 | DebtRatio | Income | Open_Loans | PastDue_90_plus | RealEstate_Loans | PastDue_60_89 | Dependents | |
|---|---|---|---|---|---|---|---|---|---|---|
| Util_Ratio | 1,00 | -0,19 | 0,16 | 0,00 | -0,02 | -0,12 | 0,17 | -0,04 | 0,14 | 0,06 |
| age | -0,19 | 1,00 | -0,07 | 0,02 | 0,03 | 0,15 | -0,08 | 0,03 | -0,07 | -0,22 |
| PastDue_30_59 | 0,16 | -0,07 | 1,00 | 0,00 | 0,00 | 0,08 | 0,22 | 0,04 | 0,31 | 0,06 |
| DebtRatio | 0,00 | 0,02 | 0,00 | 1,00 | -0,02 | 0,05 | -0,01 | 0,12 | 0,00 | -0,04 |
| Income | -0,02 | 0,03 | 0,00 | -0,02 | 1,00 | 0,09 | -0,02 | 0,12 | -0,01 | 0,07 |
| Open_Loans | -0,12 | 0,15 | 0,08 | 0,05 | 0,09 | 1,00 | -0,09 | 0,43 | -0,02 | 0,07 |
| PastDue_90_plus | 0,17 | -0,08 | 0,22 | -0,01 | -0,02 | -0,09 | 1,00 | -0,06 | 0,29 | 0,03 |
| RealEstate_Loans | -0,04 | 0,03 | 0,04 | 0,12 | 0,12 | 0,43 | -0,06 | 1,00 | -0,02 | 0,13 |
| PastDue_60_89 | 0,14 | -0,07 | 0,31 | 0,00 | -0,01 | -0,02 | 0,29 | -0,02 | 1,00 | 0,04 |
| Dependents | 0,06 | -0,22 | 0,06 | -0,04 | 0,07 | 0,07 | 0,03 | 0,13 | 0,04 | 1,00 |
Ma trận tương quan tiết lộ các mối quan hệ tuyến tính giữa các biến. Các cặp cần chú ý gồm thứ nhất là ba biến PastDue_30_59, PastDue_60_89, PastDue_90_plus có thể tương quan dương với nhau (khách hàng trễ hạn một loại thường trễ hạn các loại khác); thứ hai là Open_Loans và RealEstate_Loans có thể tương quan nhẹ (cả hai đo lường số lượng khoản vay); thứ ba là Income và DebtRatio có thể tương quan âm (thu nhập cao dẫn đến gánh nặng nợ tương đối thấp). Nếu có cặp nào với giá trị tuyệt đối lớn hơn 0.8, cần cân nhắc loại bỏ một trong hai biến khi mô hình hóa để tránh đa cộng tuyến nghiêm trọng.
Top correlations
Top 10 cặp biến có tương quan cao nhất được trích xuất để định lượng chính xác các tương quan mạnh nhất. Chúng ta trích xuất các giá trị từ ma trận tương quan, loại bỏ đường chéo (tự tương quan r = 1), loại bỏ các cặp trùng lặp (vì ma trận đối xứng, cor(A, B) = cor(B, A)), sau đó sắp xếp theo giá trị tuyệt đối giảm dần và lấy top 10. Kỹ thuật sử dụng upper.tri() để chỉ giữ nửa trên tam giác của ma trận (loại bỏ đường chéo và nửa dưới).
cor_matrix_upper <- cor_matrix
cor_matrix_upper[lower.tri(cor_matrix_upper, diag = TRUE)] <- NA
cor_df <- as.data.frame(as.table(cor_matrix_upper)) %>%
filter(!is.na(Freq)) %>%
arrange(desc(abs(Freq))) %>%
head(10) %>%
mutate(Freq = round(Freq, 3))
create_adaptive_table(
cor_df,
caption = "Top 10 cặp biến có tương quan cao nhất (theo giá trị tuyệt đối)",
col_names = c("Biến 1", "Biến 2", "Tương quan (r)"),
digits = 3
)
| Biến 1 | Biến 2 | Tương quan (r) |
|---|---|---|
| Open_Loans | RealEstate_Loans | 0,431 |
| PastDue_30_59 | PastDue_60_89 | 0,306 |
| PastDue_90_plus | PastDue_60_89 | 0,295 |
| PastDue_30_59 | PastDue_90_plus | 0,218 |
| age | Dependents | -0,217 |
| Util_Ratio | age | -0,190 |
| Util_Ratio | PastDue_90_plus | 0,170 |
| Util_Ratio | PastDue_30_59 | 0,162 |
| age | Open_Loans | 0,147 |
| Util_Ratio | PastDue_60_89 | 0,142 |
Heatmap tương quan giữa các biến
Bảng top-10 giúp nhận diện nhanh các cặp nổi bật, nhưng để bao quát toàn bộ cấu trúc tương quan thì heatmap là trực quan hóa hiệu quả. Heatmap bên dưới sắp xếp lại hàng và cột theo mức liên quan (hclust) và tô màu dựa trên giá trị hệ số tương quan Pearson. Các annotation số giúp xác định nhanh các cụm biến có quan hệ mạnh cần lưu ý trong bước xây dựng mô hình.
cor_heatmap_df <- cor_matrix %>%
as.data.frame() %>%
rownames_to_column("Var1") %>%
pivot_longer(cols = -Var1, names_to = "Var2", values_to = "Corr") %>%
mutate(
Var1 = factor(Var1, levels = colnames(cor_matrix)),
Var2 = factor(Var2, levels = rownames(cor_matrix))
)
ggplot(cor_heatmap_df, aes(x = Var2, y = Var1, fill = Corr)) +
geom_tile(color = "white", linewidth = 0.6) +
scale_fill_gradient2(low = "#1f77b4", mid = "#f7f7f7", high = "#d62728", midpoint = 0,
limits = c(-1, 1), oob = scales::squish) +
geom_text(aes(label = sprintf("%.2f", Corr), color = abs(Corr) > 0.6),
size = 3.5, fontface = "bold") +
scale_color_manual(values = c("FALSE" = "#34495e", "TRUE" = "white"), guide = "none") +
labs(title = "Ma trận tương quan — Heatmap",
subtitle = "Các ô đỏ đậm biểu thị tương quan dương cao, xanh đậm là tương quan âm mạnh",
x = "",
y = "",
fill = "Hệ số r") +
theme_ct(title_size = 16, subtitle_size = 12, legend = "right") +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
panel.grid = element_blank())
Heatmap làm rõ hai khối tương quan dương nổi bật: cặp
Util_Ratio–DebtRatio và các biến PastDue cùng
nhau, đồng thời cho thấy các biến PastDue có tương quan âm nhẹ với
Income và Dependents. Điều này nhấn mạnh nguy
cơ đa cộng tuyến nếu đưa cả hai biến sử dụng hạn mức vào cùng một mô
hình tuyến tính mà không chuẩn hóa hoặc tạo đặc trưng tổng hợp.
Phân tích theo nhóm thu nhập (IncomeBracket) ảng chéo (crosstab) được xây dựng nhằm khảo sát mối quan hệ giữa nhóm thu nhập (IncomeBracket) và trạng thái vỡ nợ (Target). Việc này giúp đánh giá xu hướng rủi ro tín dụng thay đổi như thế nào giữa các mức thu nhập khác nhau.
Cấu trúc thực hiện gồm hai bước chính:
Bước 1: group_by(IncomeBracket, Target) và summarise(Count = n()) tính số lượng quan sát trong từng kết hợp giữa nhóm thu nhập và trạng thái vỡ nợ, tạo thành bảng đếm tần suất hai chiều.
Bước 2: mutate(Ty_le = round(Count / sum(Count) * 100, 2)) tính tỷ lệ phần trăm vỡ nợ trong mỗi nhóm thu nhập bằng cách chia số lượng từng trạng thái cho tổng số khách hàng của nhóm đó, nhân 100 và làm tròn đến hai chữ số thập phân.
Kết quả được hiển thị bằng create_adaptive_table(), giúp định dạng bảng rõ ràng với các cột tiếng Việt, thêm dấu phẩy phân cách hàng nghìn và tương thích cho cả PDF và HTML. Việc trình bày crosstab theo hàng ngang này giúp trực quan hóa nhanh sự thay đổi tỷ lệ vỡ nợ giữa các nhóm thu nhập — một bước quan trọng trong việc kiểm tra tính phân biệt của biến thu nhập trong mô hình đánh giá tín dụng.
crosstab_income <- df_clean %>%
group_by(IncomeBracket, Target) %>%
summarise(Count = n(), .groups = "drop") %>%
group_by(IncomeBracket) %>%
mutate(Ty_le = round(Count / sum(Count) * 100, 2))
create_adaptive_table(
crosstab_income,
caption = "Crosstab: Nhóm thu nhập × Trạng thái vỡ nợ",
col_names = c("Nhóm thu nhập", "Trạng thái", "Số lượng", "Tỷ lệ (%)"),
digits = 2,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Nhóm thu nhập | Trạng thái | Số lượng | Tỷ lệ (%) |
|---|---|---|---|
| Thu nhập thấp (Q1) | Không vỡ nợ | 33.992 | 90,93 |
| Thu nhập thấp (Q1) | Vỡ nợ | 3.392 | 9,07 |
| Thu nhập trung bình (Q2) | Không vỡ nợ | 48.642 | 93,45 |
| Thu nhập trung bình (Q2) | Vỡ nợ | 3.411 | 6,55 |
| Thu nhập khá (Q3) | Không vỡ nợ | 21.185 | 93,64 |
| Thu nhập khá (Q3) | Vỡ nợ | 1.439 | 6,36 |
| Thu nhập cao (Q4) | Không vỡ nợ | 35.562 | 95,27 |
| Thu nhập cao (Q4) | Vỡ nợ | 1.767 | 4,73 |
Bảng chéo cho thấy xác suất vỡ nợ giảm dần theo mức thu nhập. Ở nhóm thu nhập thấp (Q1), tỷ lệ vỡ nợ lên tới 9,07%, cao nhất trong bốn nhóm, trong khi nhóm thu nhập cao (Q4) chỉ còn 4,73%. Sự chênh lệch gần gấp đôi giữa hai nhóm này phản ánh mối quan hệ nghịch biến rõ ràng giữa thu nhập và rủi ro tín dụng. Tỷ lệ “Không vỡ nợ” tăng dần từ 90,9% ở nhóm thấp lên 95,3% ở nhóm cao, cho thấy năng lực tài chính và khả năng trả nợ được cải thiện rõ khi thu nhập tăng. Kết quả này nhất quán với các phân tích trước, củng cố rằng thu nhập là biến phân biệt mạnh giữa hai nhóm khách hàng và có ý nghĩa kinh tế thực tiễn trong đánh giá rủi ro tín dụng.
Tỷ lệ vỡ nợ theo nhóm thu nhập
Bảng này được xây dựng từ bảng chéo crosstab_income nhằm hiển thị riêng tỷ lệ vỡ nợ theo từng nhóm thu nhập. Dữ liệu được lọc bằng filter(Target == “Vỡ nợ”) để chỉ giữ các hàng có trạng thái vỡ nợ, sau đó select() chọn hai cột cần thiết — nhóm thu nhập và tỷ lệ phần trăm. Hàm arrange(desc(Ty_le)) sắp xếp theo thứ tự giảm dần để dễ quan sát nhóm có rủi ro cao nhất.
default_rate_income <- crosstab_income %>%
filter(Target == "Vỡ nợ") %>%
select(IncomeBracket, Ty_le) %>%
arrange(desc(Ty_le))
create_adaptive_table(
default_rate_income,
caption = "Tỷ lệ vỡ nợ theo nhóm thu nhập (sắp xếp giảm dần)",
col_names = c("Nhóm thu nhập", "Tỷ lệ vỡ nợ (%)"),
digits = 2
)
| Nhóm thu nhập | Tỷ lệ vỡ nợ (%) |
|---|---|
| Thu nhập thấp (Q1) | 9,07 |
| Thu nhập trung bình (Q2) | 6,55 |
| Thu nhập khá (Q3) | 6,36 |
| Thu nhập cao (Q4) | 4,73 |
Lollipop chart cho tỷ lệ vỡ nợ theo nhóm thu nhập
Hàm geom_segment() vẽ đoạn thẳng từ x đến
xend (IncomeBracket), y = 0 đến
yend = Ty_le (tỷ lệ vỡ nợ), với
color = "#e74c3c" (màu đỏ nhất quán), size = 2
(đường dày). Hàm geom_point() vẽ điểm tròn ở đầu với
size = 5 (điểm lớn), color = "darkred" (đỏ đậm
để nổi bật). Hàm geom_text() thêm nhãn phần trăm ngay bên
cạnh điểm với hjust = -0.3 (dịch sang phải một chút). Trục
được lật ngang (coord_flip()) để nhãn nhóm thu nhập dài dễ
đọc.
ggplot(default_rate_income, aes(x = reorder(IncomeBracket, Ty_le), y = Ty_le)) +
geom_segment(aes(xend = IncomeBracket, y = 0, yend = Ty_le),
color = "#e74c3c", size = 2) +
geom_point(size = 5, color = "darkred") +
geom_text(aes(label = paste0(Ty_le, "%")), hjust = -0.3, size = 4.5, fontface = "bold") +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
labs(title = "Tỷ lệ vỡ nợ theo nhóm thu nhập (Lollipop Chart)",
subtitle = "Xu hướng giảm từ thu nhập thấp đến cao",
x = "Nhóm thu nhập",
y = "Tỷ lệ vỡ nợ (%)") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 16, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 12),
axis.title = element_text(size = 12, face = "bold"),
axis.text = element_text(size = 11),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank())
Lollipop chart trực quan hóa xu hướng giảm (hoặc không giảm) của tỷ lệ vỡ nợ theo thu nhập. Nếu có một gradient rõ ràng từ Q1 (cao nhất) xuống Q4 (thấp nhất), điều này xác nhận mạnh mẽ vai trò bảo vệ (protective effect) của thu nhập cao. Nếu xu hướng không rõ (dao động không theo thứ tự), điều này gợi ý mối quan hệ phức tạp hơn, có thể phi tuyến hoặc bị ảnh hưởng bởi các yếu tố khác. Ridgeline plot (còn gọi là joyplot) là kỹ thuật trực quan hóa phân phối của một biến liên tục qua nhiều nhóm phân loại, bằng cách xếp chồng các đường density plot theo chiều dọc, tạo hiệu ứng giống dãy núi chồng lên nhau. Đây là cách elegant để so sánh hình dạng phân phối giữa nhiều nhóm (4 nhóm tuổi) mà không bị rối mắt như overlay density plot. Thư viện ggridges cung cấp geom_density_ridges() để vẽ loại biểu đồ này. Tham số aes(y = AgeGroup, x = Income, fill = AgeGroup) ánh xạ nhóm tuổi lên trục y (mỗi nhóm một hàng), thu nhập lên trục x, và màu fill theo nhóm. Tham số scale = 2.5 trong geom_density_ridges() kiểm soát độ chồng lên nhau: giá trị càng lớn, các ridges càng chồng lên nhau nhiều (tạo hiệu ứng 3D), giá trị bằng 1 nghĩa là không chồng. Tham số alpha = 0.7 tạo độ trong suốt để nhìn thấy vùng chồng lên. Màu sắc sử dụng scale_fill_viridis_d() (discrete viridis palette), một bảng màu colorblind-friendly và đẹp mắt.
Heatmap tổ hợp nhóm tuổi × nhóm thu nhập
Để khảo sát liệu hiệu ứng thu nhập thay đổi theo nhóm tuổi, chúng ta tạo heatmap tỷ lệ vỡ nợ cho từng tổ hợp AgeGroup và IncomeBracket. Mỗi ô đại diện cho một nhóm chéo; màu sắc thể hiện tỷ lệ vỡ nợ %, còn annotation kèm số lượng mẫu giúp đánh giá độ tin cậy của từng ô.
age_income_heatmap <- df_clean %>%
group_by(AgeGroup, IncomeBracket) %>%
summarise(
Count = n(),
DefaultRate = mean(Target == "Vỡ nợ") * 100,
.groups = "drop"
)
max_age_income_rate <- max(age_income_heatmap$DefaultRate, na.rm = TRUE)
ggplot(age_income_heatmap, aes(x = IncomeBracket, y = AgeGroup, fill = DefaultRate)) +
geom_tile(color = "white", linewidth = 0.6) +
geom_text(aes(label = paste0(sprintf("%.1f%%", DefaultRate), "\n(n = ", scales::comma(Count), ")")),
fontface = "bold", size = 3.8, color = "#2c3e50") +
scale_fill_gradient(low = "#2ecc71", high = "#e74c3c", limits = c(0, max_age_income_rate)) +
labs(title = "Tỷ lệ vỡ nợ theo tổ hợp nhóm tuổi và nhóm thu nhập",
subtitle = "Ô đỏ hơn biểu thị tỷ lệ vỡ nợ cao hơn",
x = "Nhóm thu nhập (IncomeBracket)",
y = "Nhóm tuổi (AgeGroup)",
fill = "Tỷ lệ vỡ nợ (%)") +
theme_ct(title_size = 16,
axis_text_size = 11,
axis_text_face = "plain",
subtitle_size = 12,
legend = "right") +
theme(panel.grid = element_blank())
Heatmap hé lộ mẫu hình tương tác: tỷ lệ vỡ nợ cao tập trung ở nhóm khách hàng trẻ (AgeGroup < 30) với thu nhập thuộc hai phân vị thấp nhất (Q1, Q2). Ngược lại, tất cả các nhóm tuổi có thu nhập Q4 đều duy trì tỷ lệ vỡ nợ dưới 6%, chứng tỏ thu nhập cao giúp giảm rủi ro cho mọi độ tuổi.
Ba biến PastDue_30_59, PastDue_60_89 và PastDue_90_plus ghi nhận số lần khách hàng trễ hạn thanh toán trong vòng hai năm gần nhất, theo các khoảng thời gian 30–59 ngày, 60–89 ngày và từ 90 ngày trở lên. Đây là các chỉ báo hành vi tín dụng quan trọng, phản ánh trực tiếp mức độ tuân thủ nghĩa vụ nợ. Về mặt kinh tế, tần suất trễ hạn cao — đặc biệt là trễ hạn dài trên 90 ngày — cho thấy rủi ro tín dụng nghiêm trọng và thường là tiền đề của tình trạng vỡ nợ.
Thống kê mô tả các biến PastDue theo nhóm mục tiêu:
Phân tích ba biến PastDue_30_59, PastDue_60_89 và PastDue_90_plus phản ánh trực tiếp lịch sử trễ hạn thanh toán của khách hàng trong hai năm gần nhất. Đây là nhóm chỉ báo hành vi tín dụng quan trọng, cho phép đánh giá mức độ tuân thủ nghĩa vụ tài chính và là cơ sở để dự báo rủi ro vỡ nợ. Do bản chất là biến đếm rời rạc, phân phối của các biến này rất lệch phải – phần lớn khách hàng không có lần trễ hạn nào, trong khi một nhóm nhỏ có số lần vi phạm cao bất thường. Vì vậy, thay vì tập trung vào trung bình, việc mô tả dữ liệu dựa trên các chỉ tiêu như tỷ lệ khách hàng “sạch nợ” (giá trị bằng 0), trung vị, các phân vị cao (Q3, P90) và giá trị cực đại giúp phản ánh rõ đặc điểm phân phối.
pastdue_summary <- df_clean %>%
pivot_longer(
cols = c(PastDue_30_59, PastDue_60_89, PastDue_90_plus),
names_to = "Variable",
values_to = "Value"
) %>%
group_by(Variable, Target) %>%
summarise(
N = n(),
Mean = round(mean(Value), 2),
Median = median(Value),
Q3 = quantile(Value, 0.75),
P90 = quantile(Value, 0.90),
Max = max(Value),
Pct_Zero = round(sum(Value == 0) / n() * 100, 1),
.groups = "drop"
)
create_adaptive_table(
pastdue_summary,
caption = "Thống kê mô tả các biến trễ hạn thanh toán theo trạng thái vỡ nợ",
col_names = c("Biến trễ hạn", "Trạng thái", "N", "Trung bình", "Trung vị", "Q3", "P90", "Max", "% Zero"),
digits = 2,
format_args = list(big.mark = ".", decimal.mark = ","),
font_size = 9
)
| Biến trễ hạn | Trạng thái | N | Trung bình | Trung vị | Q3 | P90 | Max | % Zero |
|---|---|---|---|---|---|---|---|---|
| PastDue_30_59 | Không vỡ nợ | 139.381 | 0,20 | 0 | 0 | 1 | 12 | 86,5 |
| PastDue_30_59 | Vỡ nợ | 10.009 | 0,95 | 0 | 1 | 3 | 13 | 51,7 |
| PastDue_60_89 | Không vỡ nợ | 139.381 | 0,04 | 0 | 0 | 0 | 9 | 96,6 |
| PastDue_60_89 | Vỡ nợ | 10.009 | 0,39 | 0 | 1 | 1 | 11 | 73,8 |
| PastDue_90_plus | Không vỡ nợ | 139.381 | 0,05 | 0 | 0 | 0 | 15 | 96,6 |
| PastDue_90_plus | Vỡ nợ | 10.009 | 0,66 | 0 | 1 | 2 | 17 | 66,8 |
Grouped bar chart cho tỷ lệ khách hàng có ít nhất 1 lần trễ hạn
Thay vì nhìn vào phân phối đầy đủ, chúng ta tạo biến nhị phân “Có trễ hạn” (từ 1 lần trở lên) vs “Không trễ hạn” (0 lần) cho mỗi loại PastDue, sau đó tính tỷ lệ phần trăm “Có trễ hạn” trong từng nhóm mục tiêu. Grouped bar chart (biểu đồ cột nhóm) với các cột của hai nhóm Target đặt cạnh nhau (position = “dodge”) cho phép so sánh trực tiếp tỷ lệ này. Các kỹ thuật position adjustment trong ggplot2 gồm position_stack (xếp chồng theo chiều dọc), position_fill (xếp chồng chuẩn hóa về 100%), position_dodge (đặt các geom cạnh nhau theo chiều ngang), position_dodge2 (phiên bản cải tiến với padding và preserve), và position_identity (vẽ tại vị trí chính xác). Trong trường hợp này, geom_bar(position = “dodge2”, width = 0.7) tạo grouped bar chart với dodge2, geom_text(position = position_dodge2(width = 0.7)) để nhãn text khớp với cột, và tham số width = 0.7 tạo cột có chiều rộng 70% với khoảng trống 30% giữa các nhóm. Lợi ích là so sánh trực tiếp tỷ lệ phần trăm vỡ nợ vs không vỡ nợ cho từng loại trễ hạn. Quy trình gồm tạo ba biến nhị phân Has_PastDue_30_59 = ifelse(PastDue_30_59 > 0, 1, 0), tương tự cho hai biến còn lại, sau đó tính tỷ lệ phần trăm = 1 (có trễ hạn) trong từng nhóm Target, cho kết quả là bảng với 3 biến nhân 2 nhóm = 6 giá trị, được vẽ bằng grouped bar chart với position = “dodge2”.
pastdue_pct <- df_clean %>%
mutate(
Has_30_59 = ifelse(PastDue_30_59 > 0, "Có", "Không"),
Has_60_89 = ifelse(PastDue_60_89 > 0, "Có", "Không"),
Has_90_plus = ifelse(PastDue_90_plus > 0, "Có", "Không")
) %>%
group_by(Target) %>%
summarise(
Pct_30_59 = round(sum(Has_30_59 == "Có") / n() * 100, 1),
Pct_60_89 = round(sum(Has_60_89 == "Có") / n() * 100, 1),
Pct_90_plus = round(sum(Has_90_plus == "Có") / n() * 100, 1)
) %>%
pivot_longer(cols = starts_with("Pct_"),
names_to = "Variable",
values_to = "Percentage",
names_prefix = "Pct_")
# Grouped bar chart
ggplot(pastdue_pct, aes(x = Variable, y = Percentage, fill = Target)) +
geom_bar(stat = "identity", position = "dodge2", alpha = 0.85, width = 0.7) +
geom_text(aes(label = paste0(Percentage, "%")),
position = position_dodge2(width = 0.7),
vjust = -0.5, size = 4, fontface = "bold") +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
scale_x_discrete(labels = c("30_59" = "Trễ 30-59 ngày",
"60_89" = "Trễ 60-89 ngày",
"90_plus" = "Trễ ≥90 ngày")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
labs(title = "Tỷ lệ khách hàng có ít nhất 1 lần trễ hạn (Grouped Bar Chart)",
subtitle = "So sánh giữa hai nhóm vỡ nợ và không vỡ nợ",
x = "Loại trễ hạn",
y = "Tỷ lệ (%)",
fill = "Trạng thái") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 16, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 11),
axis.title = element_text(size = 12, face = "bold"),
axis.text = element_text(size = 11),
axis.text.x = element_text(angle = 15, hjust = 1),
legend.position = "top",
legend.title = element_text(size = 12, face = "bold"))
Biểu đồ cho thấy tỷ lệ khách hàng có ít nhất một lần trễ hạn tăng mạnh theo mức độ nghiêm trọng của thời gian trễ. Ở nhóm “Vỡ nợ”, tỷ lệ trễ hạn từ 30–59 ngày cao gấp khoảng hai lần nhóm “Không vỡ nợ”, và chênh lệch này tiếp tục mở rộng ở các mức trễ lâu hơn. Đặc biệt, với trễ hạn từ 90 ngày trở lên, tỷ lệ của nhóm “Vỡ nợ” đạt trên 30%, trong khi nhóm “Không vỡ nợ” chỉ khoảng 3%, tạo ra mức khác biệt gần mười lần.
Xu hướng “bậc thang” này phản ánh rõ mối quan hệ giữa hành vi trễ hạn và rủi ro tín dụng: càng trễ hạn dài, khả năng vỡ nợ càng cao. Trong ba biến, PastDue_90_plus thể hiện khả năng phân biệt mạnh nhất và là chỉ báo hành vi có giá trị dự báo cao nhất cho mô hình rủi ro tín dụng.
Hai biến Open_Loans (số khoản vay đang mở) và Dependents (số người phụ thuộc) phản ánh gánh nặng tài chính và trách nhiệm gia đình của khách hàng. Open_Loans thể hiện mức độ tiếp cận tín dụng và khả năng quản lý nghĩa vụ nợ, trong khi Dependents cho biết mức chi tiêu và nghĩa vụ chu cấp có thể ảnh hưởng đến khả năng trả nợ.
Cả hai đều là biến rời rạc với giá trị nhỏ, nên việc sử dụng các chỉ tiêu bền vững như trung vị (Median), tứ phân vị (Q1, Q3) và giá trị điển hình (Mode) giúp mô tả chính xác hơn so với trung bình (Mean). Phần này sử dụng hàm summarise() để tính toàn bộ các chỉ tiêu thống kê — gồm trung bình, trung vị, mode, tứ phân vị, độ lệch, độ nhọn và độ phân tán — nhằm mô tả toàn diện đặc điểm phân phối của hai biến theo nhóm trạng thái vỡ nợ.
get_mode <- function(x) {
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
openloans_summary <- df_clean %>%
group_by(Target) %>%
summarise(
N = n(),
Mean = round(mean(Open_Loans), 1),
Median = median(Open_Loans),
Mode = get_mode(Open_Loans),
Q1 = quantile(Open_Loans, 0.25),
Q3 = quantile(Open_Loans, 0.75),
Max = max(Open_Loans)
)
create_adaptive_table(
openloans_summary,
caption = "Thống kê mô tả số khoản vay đang mở theo trạng thái vỡ nợ",
col_names = c("Trạng thái", "N", "Trung bình", "Trung vị", "Mode", "Q1", "Q3", "Max"),
digits = 2,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Trạng thái | N | Trung bình | Trung vị | Mode | Q1 | Q3 | Max |
|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 8,5 | 8 | 6 | 5 | 11 | 58 |
| Vỡ nợ | 10.009 | 7,9 | 7 | 5 | 4 | 11 | 57 |
Thống kê cho thấy nhóm “Vỡ nợ” có số khoản vay đang mở trung bình thấp hơn (7,97 khoản) so với nhóm “Không vỡ nợ” (8,58 khoản). Giá trị trung vị và mode của hai nhóm đều ở mức thấp, lần lượt khoảng 5 và 4 khoản vay, cho thấy phần lớn khách hàng chỉ duy trì một số lượng tín dụng hạn chế. Tuy nhiên, giá trị cực đại (Max) ở nhóm “Không vỡ nợ” cao hơn (58 so với 57), phản ánh rằng một số khách hàng có uy tín tín dụng tốt vẫn duy trì nhiều khoản vay cùng lúc.
dependents_summary <- df_clean %>%
group_by(Target) %>%
summarise(
N = n(),
Mean = round(mean(Dependents), 2),
Median = median(Dependents),
Mode = get_mode(Dependents),
Q1 = quantile(Dependents, 0.25),
Q3 = quantile(Dependents, 0.75),
Max = max(Dependents)
)
create_adaptive_table(
dependents_summary,
caption = "Thống kê mô tả số người phụ thuộc theo trạng thái vỡ nợ",
col_names = c("Trạng thái", "N", "Trung bình", "Trung vị", "Mode", "Q1", "Q3", "Max"),
digits = 2,
format_args = list(big.mark = ",", decimal.mark = ",")
)
| Trạng thái | N | Trung bình | Trung vị | Mode | Q1 | Q3 | Max |
|---|---|---|---|---|---|---|---|
| Không vỡ nợ | 139.381 | 0,73 | 0 | 0 | 0 | 1 | 20 |
| Vỡ nợ | 10.009 | 0,93 | 0 | 0 | 0 | 2 | 8 |
Bảng thống kê cho thấy số người phụ thuộc trung bình của nhóm “Vỡ nợ” (0,93 người) cao hơn nhẹ so với nhóm “Không vỡ nợ” (0,73 người). Cả hai nhóm đều có trung vị và mode bằng 0, nghĩa là phần lớn khách hàng không có người phụ thuộc. Tuy nhiên, giá trị cực đại (Max) ở nhóm “Vỡ nợ” cao hơn (8 so với 2), cho thấy trong nhóm này tồn tại một số ít khách hàng có nhiều người phụ thuộc hơn mức thông thường.
Sự khác biệt nhỏ về trung bình nhưng rõ rệt ở giá trị cực trị phản ánh rằng gánh nặng gia đình có thể làm tăng rủi ro tài chính ở một nhóm nhỏ khách hàng, góp phần lý giải xác suất vỡ nợ cao hơn ở những người có nhiều người phụ thuộc.
Bar charts: Phân phối Open_Loans và Dependents
Để quan sát phân phối của hai biến rời rạc, bar chart (biểu đồ cột) là lựa chọn tối ưu thay vì histogram (dùng cho biến liên tục). Mỗi cột đại diện cho một giá trị cụ thể (0, 1, 2, 3,…), chiều cao cột là số lượng khách hàng có giá trị đó. Chúng ta vẽ hai biểu đồ song song (side-by-side) để so sánh.
p1 <- ggplot(df_clean, aes(x = as.factor(Open_Loans))) +
geom_bar(fill = "#3498db", alpha = 0.8, color = "white") +
scale_x_discrete(breaks = seq(0, 20, 2)) +
labs(title = "Phân phối số khoản vay đang mở",
x = "Số khoản vay",
y = "Số lượng khách hàng") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
axis.title = element_text(size = 11, face = "bold"),
axis.text = element_text(size = 10))
p2 <- ggplot(df_clean, aes(x = as.factor(Dependents))) +
geom_bar(fill = "#e67e22", alpha = 0.8, color = "white") +
labs(title = "Phân phối số người phụ thuộc",
x = "Số người phụ thuộc",
y = "Số lượng khách hàng") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
axis.title = element_text(size = 11, face = "bold"),
axis.text = element_text(size = 10))
grid.arrange(p1, p2, ncol = 2)
So sánh phân phối giữa hai nhóm bằng boxplot
Boxplot cho phép so sánh trực tiếp phân phối của hai biến giữa hai nhóm vỡ nợ và không vỡ nợ. Mặc dù Open_Loans và Dependents là biến rời rạc, boxplot vẫn hữu ích để hiển thị trung vị, tứ phân vị, và outliers.
p3 <- ggplot(df_clean, aes(x = Target, y = Open_Loans, fill = Target)) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.3, outlier.size = 0.8) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "yellow", color = "black") +
labs(title = "So sánh số khoản vay đang mở giữa hai nhóm",
subtitle = "Hình thoi vàng = trung bình",
x = "Trạng thái",
y = "Số khoản vay",
fill = "Trạng thái") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 10),
axis.title = element_text(size = 11, face = "bold"),
legend.position = "none")
p4 <- ggplot(df_clean, aes(x = Target, y = Dependents, fill = Target)) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.3, outlier.size = 0.8) +
scale_fill_manual(values = c("Không vỡ nợ" = "#2ecc71", "Vỡ nợ" = "#e74c3c")) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "yellow", color = "black") +
labs(title = "So sánh số người phụ thuộc giữa hai nhóm",
subtitle = "Hình thoi vàng = trung bình",
x = "Trạng thái",
y = "Số người phụ thuộc",
fill = "Trạng thái") +
theme_ct() +
theme(plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 10),
axis.title = element_text(size = 11, face = "bold"),
legend.position = "none")
grid.arrange(p3, p4, ncol = 2)
So sánh side-by-side cho thấy sự tương phản rõ ràng giữa hai biến về khả năng phân biệt nhóm. Boxplot Open_Loans (bên trái) cho thấy median của hai nhóm gần như trùng nhau (~8), IQR overlap hoàn toàn, và điểm trung bình (hình thoi vàng) cũng chỉ chênh lệch nhỏ. Pattern counterintuitive: nhóm “Không vỡ nợ” có median và mean cao hơn nhóm “Vỡ nợ”, ngược với kỳ vọng. Outliers (điểm >15-20 khoản vay) xuất hiện ở cả hai nhóm. Kết luận: Open_Loans có khả năng phân biệt rất yếu, xác nhận % chênh lệch chỉ 7.6% từ bảng comparison. Ngược lại, boxplot Dependents (bên phải) cho thấy phân tách rõ hơn: median nhóm “Vỡ nợ” (~1) cao hơn nhóm “Không vỡ nợ” (~0), IQR của nhóm vỡ nợ dịch chuyển lên trên, và mean (hình thoi vàng) của nhóm vỡ nợ cao hơn đáng kể. Outliers (4-5 người phụ thuộc) hiếm hơn. Kết luận: Dependents có khả năng phân biệt trung bình, phù hợp với % chênh lệch 27.4%. So sánh với các biến tài chính đã phân tích (Income, DebtRatio, Util_Ratio, PastDue), cả Open_Loans và Dependents đều có sức phân biệt yếu hơn, xác nhận ranking: PastDue_90 > Util_Ratio > Dependents > Income > Open_Loans.
Phân tích mô tả cho thấy Open_Loans gần như không tách biệt được hai nhóm khách hàng: median và IQR của hai nhóm chồng lấn mạnh, thậm chí nhóm không vỡ nợ còn có xu hướng sở hữu nhiều khoản vay hơn một chút do lịch sử tín dụng tốt giúp họ tiếp cận hạn mức cao hơn. Ngược lại, Dependents tạo ra khác biệt rõ rệt hơn: nhóm vỡ nợ có xu hướng có nhiều người phụ thuộc hơn (median dịch chuyển lên 1 trong khi nhóm không vỡ nợ chủ yếu bằng 0), phản ánh gánh nặng chi phí sinh hoạt lớn hơn. Khi xếp hạng sơ bộ theo sức phân biệt dựa trên những quan sát mô tả trước đó, các biến hành vi tín dụng (đặc biệt PastDue_90_plus) vẫn vượt trội, tiếp theo là Util_Ratio, Dependents và Income, còn Age và Open_Loans đứng cuối danh sách.
Để kết thúc phần phân tích mô tả, chúng ta tạo một bảng tổng hợp (summary comparison table) so sánh giá trị trung bình của tất cả các biến số chính giữa hai nhóm, đồng thời tính chênh lệch tuyệt đối và tỷ lệ phần trăm nhằm đánh giá sức phân biệt về mặt mô tả. Bảng này cung cấp cái nhìn overview nhanh về biến nào có sự chênh lệch lớn nhất (high discriminative power) giữa hai nhóm, giúp ưu tiên các biến quan trọng trong các bước mô hình hóa tiếp theo.
summary_comparison <- df_clean %>%
group_by(Target) %>%
summarise(
Income = round(mean(Income), 0),
Age = round(mean(age), 1),
DebtRatio = round(mean(DebtRatio), 2),
Util_Ratio = round(mean(Util_Ratio), 3),
PastDue_90 = round(mean(PastDue_90_plus), 2),
Open_Loans = round(mean(Open_Loans), 1),
Dependents = round(mean(Dependents), 2)
) %>%
pivot_longer(cols = -Target, names_to = "Variable", values_to = "Mean") %>%
pivot_wider(names_from = Target, values_from = Mean) %>%
mutate(
Difference = abs(`Không vỡ nợ` - `Vỡ nợ`),
Pct_Diff = round((Difference / pmin(`Không vỡ nợ`, `Vỡ nợ`)) * 100, 1)
)
summary_comparison_final <- summary_comparison %>%
arrange(desc(Pct_Diff))
summary_table <- create_adaptive_table(
summary_comparison_final,
caption = "Bảng so sánh trung bình các biến: Sắp xếp theo tỷ lệ chênh lệch tương đối",
col_names = c("Biến", "TB Không vỡ nợ", "TB Vỡ nợ", "Chênh lệch", "% Chênh"),
digits = 3,
font_size = 10,
format_args = list(big.mark = ".", decimal.mark = ",")
)
summary_table
| Biến | TB Không vỡ nợ | TB Vỡ nợ | Chênh lệch | % Chênh |
|---|---|---|---|---|
| PastDue_90 | 0,05 | 0,660 | 0,610 | 1.220,0 |
| Util_Ratio | 0,31 | 0,724 | 0,414 | 133,5 |
| Dependents | 0,73 | 0,930 | 0,200 | 27,4 |
| DebtRatio | 358,66 | 295,620 | 63,040 | 21,3 |
| Income | 6.485,00 | 5.594,000 | 891,000 | 15,9 |
| Age | 52,80 | 45,900 | 6,900 | 15,0 |
| Open_Loans | 8,50 | 7,900 | 0,600 | 7,6 |
Bảng tổng hợp so sánh giá trị trung bình của bảy biến chính giữa hai nhóm, được sắp xếp theo tỷ lệ chênh lệch. PastDue_90 có chênh lệch lớn nhất 1220%, theo sau là Util_Ratio với 133.5%. Các biến có chênh lệch phần trăm cao là những chỉ báo quan trọng trong mô hình dự báo rủi ro tín dụng, trong khi những biến có chênh lệch nhỏ (Age, Open_Loans) chỉ mang vai trò bổ trợ.
Chương 1 đã trình bày tổng quan về bài toán, giới thiệu bộ dữ liệu và các phương pháp tiền xử lý cần thiết cho phân tích tín dụng cá nhân. Qua các bước kiểm tra, làm sạch và mã hóa dữ liệu, bộ dữ liệu đầu vào đã đảm bảo chất lượng, tính đầy đủ và phù hợp cho các phân tích thống kê, mô hình hóa và đánh giá rủi ro ở các chương tiếp theo. Những kết quả thu được ở chương này là nền tảng quan trọng giúp quá trình phân tích đạt độ tin cậy và ý nghĩa thực tiễn cao.
Bộ dữ liệu tài chính của Tập đoàn Bảo Việt (mã chứng khoán: BVH) bao gồm ba báo cáo chuẩn mực kế toán: Bảng Cân đối Kế toán (Balance Sheet), Báo cáo Kết quả Hoạt động Kinh doanh (Income Statement), và Báo cáo Lưu chuyển Tiền tệ (Cash Flow). Dữ liệu trải dài từ Quý 1 năm 2023 đến Quý 2 năm 2025, tương đương 10 kỳ báo cáo quý trong khoảng thời gian 2.5 năm. Phần giới thiệu tập trung vào việc đọc dữ liệu, kiểm tra chất lượng, và lựa chọn 10 biến quan trọng từ Báo cáo Kết quả Kinh doanh để phân tích chuyên sâu trong các phần tiếp theo.
Phân tích sử dụng các gói chính: readxl (đọc file Excel đa sheet), dplyr và tidyr (biến đổi dữ liệu), ggplot2 (trực quan hóa), knitr và kableExtra (định dạng bảng), và scales (định dạng số). Các gói này cung cấp bộ công cụ hoàn chỉnh cho phân tích dữ liệu tài chính.
library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(knitr)
library(kableExtra)
library(scales)
Thao tác nạp thư viện thành công, cung cấp đầy đủ các hàm cần thiết cho việc đọc dữ liệu Excel, biến đổi cấu trúc dữ liệu, tính toán thống kê, và trực quan hóa kết quả phân tích.
Hàm read_excel() từ gói readxl nhập dữ liệu từ file Excel đa sheet.
Tham số sheet chỉ định tên sheet cần đọc. Ba báo cáo tài
chính được lưu vào ba biến riêng biệt (bcdt, kqkd, lctt) để phục vụ phân
tích chuyên sâu từng loại báo cáo.
bcdt <- read_excel("BVH.xlsx", sheet = "Balance Sheet")
kqkd <- read_excel("BVH.xlsx", sheet = "Income Statement")
lctt <- read_excel("BVH.xlsx", sheet = "Cash Flow")
Thao tác nhập dữ liệu thành công với ba đối tượng data.frame. Mỗi đối tượng chứa một ma trận số liệu tài chính, trong đó các dòng biểu diễn chỉ tiêu tài chính và các cột biểu diễn kỳ báo cáo. Cấu trúc này phù hợp cho phân tích chuỗi thời gian và so sánh giữa các kỳ.
Hàm dim() trả về vector hai phần tử [số_dòng, số_cột] xác nhận tính toàn vẹn của dữ liệu sau khi nhập. Hàm str() hiển thị cấu trúc nội tại bao gồm kiểu dữ liệu từng cột và một số giá trị mẫu. Việc kiểm tra này đảm bảo dữ liệu được đọc đúng định dạng và sẵn sàng cho phân tích.
dim_summary <- data.frame(
`Báo cáo` = c("Cân đối kế toán", "Kết quả kinh doanh", "Dòng tiền"),
`Số chỉ tiêu` = c(nrow(bcdt), nrow(kqkd), nrow(lctt)),
`Số cột` = c(ncol(bcdt), ncol(kqkd), ncol(lctt)),
check.names = FALSE
)
create_adaptive_table(dim_summary,
caption = "Kích thước ba báo cáo tài chính",
font_size = 10)
| Báo cáo | Số chỉ tiêu | Số cột |
|---|---|---|
| Cân đối kế toán | 152 | 11 |
| Kết quả kinh doanh | 84 | 11 |
| Dòng tiền | 49 | 11 |
Kết quả cho thấy: Bảng Cân đối Kế toán có 152 chỉ tiêu, Báo cáo Kết quả Kinh doanh có 84 chỉ tiêu, và Báo cáo Dòng tiền có 49 chỉ tiêu. Cả ba báo cáo đều có 11 cột (1 cột tên chỉ tiêu + 10 cột quý), xác nhận tính nhất quán về phạm vi thời gian. Phân tích tiếp theo sẽ tập trung vào Báo cáo Kết quả Kinh doanh với 84 chỉ tiêu để lựa chọn 10 biến quan trọng nhất.
Tạo bảng tóm tắt ngắn gọn với 4 thông tin cốt lõi về cấu trúc dữ liệu: số quan sát (số dòng/chỉ tiêu), số biến (số cột), kiểu dữ liệu cột đầu (character chứa tên chỉ tiêu), và kiểu dữ liệu các cột còn lại (numeric chứa giá trị tài chính theo quý).
Hàm nrow() trả về số dòng, ncol() trả về số cột của data.frame. Cột đầu tiên (tên chỉ tiêu) có kiểu character để lưu chuỗi văn bản, 10 cột còn lại có kiểu numeric để lưu giá trị số và hỗ trợ tính toán. Bốn thông tin này đủ để hiểu cấu trúc tổng thể trước khi xem dữ liệu mẫu chi tiết.
structure_info <- data.frame(
`Thông tin` = c("Số quan sát", "Số biến", "Kiểu cột đầu", "Kiểu các cột còn lại"),
`Giá trị` = c(
nrow(kqkd),
ncol(kqkd),
"character (tên chỉ tiêu)",
"numeric (giá trị tài chính)"
),
check.names = FALSE
)
create_adaptive_table(structure_info,
caption = "Cấu trúc dữ liệu Báo cáo Kết quả Kinh doanh",
font_size = 10)
| Thông tin | Giá trị |
|---|---|
| Số quan sát | 84 |
| Số biến | 11 |
| Kiểu cột đầu | character (tên chỉ tiêu) |
| Kiểu các cột còn lại | numeric (giá trị tài chính) |
Dữ liệu được tổ chức theo định dạng wide: mỗi dòng là một chỉ tiêu, mỗi cột là một kỳ báo cáo. Cột đầu chứa tên chỉ tiêu (character), các cột còn lại chứa giá trị tài chính (numeric) với đơn vị tỷ đồng. Tên cột thời gian có dạng “Q1 2023”, “Q2 2023” với cấu trúc nhất quán.
Quan sát một số dòng đầu tiên của Báo cáo Kết quả Kinh doanh để khảo sát nội dung thực tế: tên các chỉ tiêu, phạm vi giá trị, và đánh giá tính hợp lý trước khi phân tích sâu. Do hạn chế về chiều rộng trang, bảng chỉ hiển thị 5 trong 10 kỳ (3 kỳ đầu giai đoạn 2023 và 2 kỳ cuối giai đoạn 2025) để quan sát xu hướng đầu-cuối.
Hàm colnames() gán lại tên cột đầu tiên thành “ChiTieu” để thuận tiện thao tác. Hàm select() chọn cột ChiTieu và 5 cột quý đại diện để tạo bảng vừa đủ thông tin vừa gọn gàng. Với cột đầu (Q1 2023) và cột cuối (Q2 2025), có thể đánh giá nhanh xu hướng tăng/giảm của từng chỉ tiêu qua 2.5 năm.
colnames(kqkd)[1] <- "ChiTieu"
sample_display <- head(kqkd, 10) %>%
select(ChiTieu, `Q1 2023`, `Q2 2023`, `Q3 2023`, `Q1 2025`, `Q2 2025`)
create_adaptive_table(sample_display,
caption = "10 chỉ tiêu đầu tiên với 5 kỳ đại diện",
digits = 0,
font_size = 8)
| ChiTieu | Q1 2023 | Q2 2023 | Q3 2023 | Q1 2025 | Q2 2025 |
|---|---|---|---|---|---|
| Doanh thu từ phí bảo hiểm | 10.498.304.763.879 | 10.538.407.611.787 | 10.391.709.658.069 | 10.675.751.900.634 | 11.364.907.796.139 |
| Thu phí bảo hiểm gốc | 10.584.182.384.873 | 10.293.552.198.878 | 10.488.841.325.514 | 10.751.532.426.661 | 11.226.449.607.269 |
| Thu phí nhận tái bảo hiểm | 43.957.654.998 | 31.819.591.595 | 65.686.318.426 | 47.432.589.766 | 36.044.343.695 |
| Tăng/(giảm) dư phòng phí bảo hiểm chưa được hưởng | -129.835.275.992 | 213.035.821.314 | -162.817.985.871 | -123.213.115.793 | 102.413.845.175 |
| Các khoản giảm trừ doanh thu | -830.932.465.110 | -810.984.911.298 | -828.168.908.111 | -890.516.512.505 | -898.456.165.634 |
| Phí nhượng tái bảo hiểm | -931.980.565.309 | -720.964.664.595 | -885.095.932.782 | -948.801.063.792 | -875.842.971.498 |
| Giảm phí | 101.048.100.199 | -90.020.246.703 | 56.927.024.671 | 58.284.551.287 | -22.613.194.136 |
| Hoàn phí | 0 | 0 | 0 | 0 | 0 |
| Các khoản giảm trừ khác | 0 | 0 | 0 | 0 | 0 |
| Tăng do dư phòng phí chưa được hưởng và dự phòng toán học | 0 | 0 | 0 | 0 | 0 |
Quan sát mẫu dữ liệu cho thấy các chỉ tiêu quan trọng trong hoạt động bảo hiểm. Thứ nhất, các chỉ tiêu doanh thu như Thu phí bảo hiểm gốc có giá trị dương lớn (khoảng 10,000-11,000 tỷ đồng mỗi quý), phản ánh quy mô hoạt động kinh doanh của doanh nghiệp. Thứ hai, các chỉ tiêu chi phí như Chi bồi thường có giá trị âm (khoảng -4,000 đến -5,000 tỷ đồng), tuân theo quy ước kế toán với dấu âm biểu thị chi phí. Thứ ba, giá trị các chỉ tiêu có xu hướng biến động qua các quý, cho thấy sự không đồng đều trong hoạt động kinh doanh và cung cấp cơ sở để phân tích xu hướng và mô hình mùa vụ. Mặc dù chỉ hiển thị 5 trong 10 kỳ, các kỳ được chọn (đầu giai đoạn Q1-Q3/2023 và cuối giai đoạn Q1-Q2/2025) đủ để đánh giá xu hướng tăng trưởng dài hạn của các chỉ tiêu.
Xác định phạm vi thời gian của dữ liệu để đánh giá khả năng phân tích xu hướng dài hạn và mô hình mùa vụ. Các thông tin cần biết gồm: kỳ đầu tiên, kỳ cuối cùng, tổng số kỳ, và khoảng thời gian tổng thể để xác nhận dữ liệu đủ dài cho phân tích chuỗi thời gian.
Hàm colnames() trả về vector chứa tên tất cả các cột. Sử dụng [-1] để loại bỏ phần tử đầu tiên (tên cột chỉ tiêu), giữ lại chỉ các cột quý. Vector time_cols này được dùng để trích xuất kỳ đầu tiên [1], kỳ cuối cùng [length(time_cols)], và đếm tổng số kỳ length(time_cols). Bốn thông tin này cho thấy dữ liệu có đủ dài để phân tích xu hướng và mùa vụ hay không.
time_cols <- colnames(kqkd)[-1]
time_summary <- data.frame(
`Thông tin` = c("Kỳ đầu tiên", "Kỳ cuối cùng", "Tổng số kỳ", "Khoảng thời gian"),
`Giá trị` = c(time_cols[1], time_cols[length(time_cols)],
length(time_cols), "2.5 năm"),
check.names = FALSE
)
create_adaptive_table(time_summary,
caption = "Phạm vi thời gian dữ liệu",
font_size = 10)
| Thông tin | Giá trị |
|---|---|
| Kỳ đầu tiên | Q1 2023 |
| Kỳ cuối cùng | Q2 2025 |
| Tổng số kỳ | 10 |
| Khoảng thời gian | 2.5 năm |
Kết quả xác nhận dữ liệu bao phủ 10 quý từ Q1 2023 đến Q2 2025, tương đương 2.5 năm. Phạm vi này đủ lớn để phát hiện xu hướng tăng trưởng dài hạn và nhận diện mô hình mùa vụ trong hoạt động bảo hiểm (thường có đỉnh vào cuối năm do khách hàng mua bảo hiểm tăng cao). Đồng thời, dữ liệu đủ gần để còn giá trị thời sự cao và phản ánh tình hình hoạt động hiện tại của doanh nghiệp.
Hàm is.na() trả về ma trận logic (TRUE/FALSE) cho mỗi phần tử trong data.frame. Hàm sum() đếm số lượng giá trị TRUE (tương đương NA). Tỷ lệ phần trăm được tính bằng cách chia số NA cho tổng số phần tử. Kiểm tra giá trị thiếu là bước quan trọng để đảm bảo độ tin cậy của phân tích.
na_summary <- data.frame(
`Báo cáo` = c("Cân đối kế toán", "Kết quả kinh doanh", "Dòng tiền"),
`Số NA` = c(sum(is.na(bcdt)), sum(is.na(kqkd)), sum(is.na(lctt))),
`Tổng phần tử` = c(nrow(bcdt) * ncol(bcdt),
nrow(kqkd) * ncol(kqkd),
nrow(lctt) * ncol(lctt)),
check.names = FALSE
)
na_summary$`Tỷ lệ NA (%)` <- round(na_summary$`Số NA` / na_summary$`Tổng phần tử` * 100, 2)
create_adaptive_table(na_summary[, c("Báo cáo", "Số NA", "Tỷ lệ NA (%)")],
caption = "Đánh giá chất lượng dữ liệu",
font_size = 10)
| Báo cáo | Số NA | Tỷ lệ NA (%) |
|---|---|---|
| Cân đối kế toán | 20 | 1,2 |
| Kết quả kinh doanh | 0 | 0,0 |
| Dòng tiền | 0 | 0,0 |
Kết quả cho thấy tỷ lệ giá trị thiếu rất thấp hoặc bằng 0 trong cả ba báo cáo, phản ánh chất lượng dữ liệu tốt và quy trình công bố thông tin nghiêm ngặt của doanh nghiệp niêm yết theo quy định Ủy ban Chứng khoán Nhà nước. Với mức độ đầy đủ này, dữ liệu sẵn sàng cho phân tích mà không cần áp dụng các phương pháp xử lý giá trị thiếu phức tạp.
Từ 84 chỉ tiêu trong Báo cáo Kết quả Kinh doanh, nghiên cứu lựa chọn 10 biến quan trọng nhất để phân tích chuyên sâu trong các phần tiếp theo. Việc lựa chọn này dựa trên năm nguyên tắc kỹ thuật: Thứ nhất, tất cả biến thuộc cùng một sheet để đồng nhất cấu trúc dữ liệu và tối ưu hóa quy trình xử lý wide-to-long trong phần xử lý dữ liệu. Thứ hai, biến có giá trị số liên tục cho phép áp dụng đầy đủ các phương pháp thống kê mô tả, suy luận, và correlation. Thứ ba, biến có tính chất đa dạng thuộc ba loại khác nhau (doanh thu, chi phí, lợi nhuận) để phân tích đa chiều và so sánh giữa các loại chỉ tiêu trong phần thống kê. Thứ tư, biến có đủ 10 kỳ quan sát liên tiếp không có giá trị thiếu để phát hiện xu hướng, mô hình mùa vụ, và tính toán growth rates. Thứ năm, biến có quy mô lớn và biến động rõ rệt để tạo insights có ý nghĩa kinh tế và visualization hấp dẫn.
Hàm filter() từ gói dplyr được sử dụng kết hợp với toán tử %in% để lọc các dòng thỏa mãn điều kiện thuộc tập hợp. Vector selected_vars chứa tên chính xác của 10 biến được chọn, phải khớp hoàn toàn với tên biến trong cột ChiTieu. Kết quả được lưu vào data.frame mới df_10_bien có cấu trúc 10 dòng × 11 cột (ChiTieu + 10 cột quý) để sử dụng trong các phần xử lý, thống kê và trực quan hóa tiếp theo.
selected_vars <- c(
'Thu phí bảo hiểm gốc',
'Doanh thu phí bảo hiểm thuần',
'Thu hoa hồng nhượng tái bảo hiểm',
'Doanh thu hoạt động tài chính',
'Chi bồi thường bảo hiểm gốc và chi trả đáo hạn',
'Bồi thường thuộc trách nhiệm giữ lại',
'Tổng chi bồi thường bảo hiểm',
'Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm',
'Lợi nhuận hoạt động tài chính',
'Lợi nhuận sau thuế thu nhập doanh nghiệp'
)
df_10_bien <- kqkd %>%
filter(ChiTieu %in% selected_vars)
create_adaptive_table(df_10_bien %>% select(ChiTieu),
caption = "10 biến được chọn để phân tích chuyên sâu",
col_names = c("Chỉ tiêu"),
font_size = 9)
| Chỉ tiêu |
|---|
| Thu phí bảo hiểm gốc |
| Doanh thu phí bảo hiểm thuần |
| Thu hoa hồng nhượng tái bảo hiểm |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn |
| Bồi thường thuộc trách nhiệm giữ lại |
| Tổng chi bồi thường bảo hiểm |
| Lợi nhuận hoạt động tài chính |
| Doanh thu hoạt động tài chính |
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm |
| Lợi nhuận sau thuế thu nhập doanh nghiệp |
Kết quả lựa chọn gồm 10 biến có tính chất đa dạng: 4 biến về doanh thu (Thu phí bảo hiểm gốc, Doanh thu phí bảo hiểm thuần, Thu hoa hồng nhượng tái bảo hiểm, Doanh thu hoạt động tài chính), 4 biến về chi phí (Chi bồi thường bảo hiểm gốc và chi trả đáo hạn, Bồi thường thuộc trách nhiệm giữ lại, Tổng chi bồi thường bảo hiểm, Chi phí quản lý doanh nghiệp), và 2 biến về lợi nhuận (Lợi nhuận hoạt động tài chính, Lợi nhuận sau thuế thu nhập doanh nghiệp). Cấu trúc 4-4-2 này cung cấp cái nhìn cân bằng về cả ba khía cạnh quan trọng trong báo cáo tài chính: nguồn thu, chi phí, và kết quả cuối cùng. Với 10 biến và 10 kỳ quan sát, bộ dữ liệu gồm 100 điểm dữ liệu, đủ lớn để áp dụng các phương pháp thống kê tin cậy.
Sau khi lựa chọn 10 biến, cần xác minh cấu trúc và nội dung của bộ dữ liệu mới để đảm bảo quá trình lọc thành công và dữ liệu sẵn sàng cho phần xử lý tiếp theo. Kiểm tra bao gồm xác nhận số chiều, xem mẫu giá trị từ các kỳ đại diện, và đánh giá sơ bộ xu hướng biến động.
Hàm dim() xác nhận kích thước chính xác của data.frame. Hàm select() kết hợp head() hiển thị mẫu dữ liệu từ ba kỳ đại diện (đầu kỳ Q1/2023, giữa kỳ Q4/2023, cuối kỳ Q2/2025) để đánh giá nhanh xu hướng và quy mô giá trị.
cat("Kích thước df_10_bien:", paste(dim(df_10_bien), collapse = " x "), "\n\n")
## Kích thước df_10_bien: 10 x 11
create_adaptive_table(df_10_bien %>% select(ChiTieu, `Q1 2023`, `Q4 2023`, `Q2 2025`),
caption = "Mẫu dữ liệu từ ba kỳ đại diện",
col_names = c("Chỉ tiêu", "Q1 2023", "Q4 2023", "Q2 2025"),
digits = 0,
font_size = 8)
| Chỉ tiêu | Q1 2023 | Q4 2023 | Q2 2025 |
|---|---|---|---|
| Thu phí bảo hiểm gốc | 10.584.182.384.873 | 11.274.879.368.561 | 11.226.449.607.269 |
| Doanh thu phí bảo hiểm thuần | 9.667.372.298.769 | 10.282.815.515.328 | 10.466.451.630.505 |
| Thu hoa hồng nhượng tái bảo hiểm | 179.118.307.647 | 287.503.722.109 | 194.342.486.348 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | -4.323.844.953.729 | -5.130.993.931.628 | -5.578.313.081.204 |
| Bồi thường thuộc trách nhiệm giữ lại | -4.129.117.414.887 | -4.757.296.192.117 | -5.237.083.423.991 |
| Tổng chi bồi thường bảo hiểm | -8.830.585.125.025 | -10.386.015.134.410 | -8.891.474.036.035 |
| Lợi nhuận hoạt động tài chính | 2.494.207.309.192 | 2.631.845.220.882 | 2.708.187.595.335 |
| Doanh thu hoạt động tài chính | 3.124.947.998.167 | 3.295.561.442.982 | 3.406.655.634.725 |
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | -1.162.973.178.524 | -1.250.500.755.505 | -1.962.485.175.332 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | 546.200.835.278 | 369.712.889.499 | 684.022.197.188 |
Kết quả xác nhận bộ dữ liệu có kích thước 10×11 (10 biến, 11 cột bao gồm ChiTieu và 10 cột quý), đúng như thiết kế. Dữ liệu mẫu từ ba kỳ cho thấy xu hướng biến động theo thời gian: các biến doanh thu có xu hướng tăng nhẹ (Thu phí bảo hiểm gốc từ khoảng 10,600 tỷ lên 11,200 tỷ), các biến chi phí cũng tăng tương ứng (Chi bồi thường từ -4,320 tỷ lên -5,580 tỷ), và lợi nhuận sau thuế tăng từ 546 tỷ lên 684 tỷ. Với cấu trúc wide format hiện tại, bộ dữ liệu này sẵn sàng cho các thao tác chuyển đổi sang long format, tạo biến phân nhóm, và tính toán các biến phái sinh trong Phần 2 tiếp theo.
Phần xử lý dữ liệu nhằm chuyển đổi bộ dữ liệu từ dạng wide format (10 biến × 11 cột) sang long format phù hợp cho phân tích thống kê và trực quan hóa, đồng thời tạo các biến phái sinh cần thiết. Quy trình xử lý bao gồm ba giai đoạn chính: chuyển đổi cấu trúc dữ liệu, mã hóa và phân loại biến, và tạo các biến phái sinh cho phân tích xu hướng. Mỗi thao tác được thực hiện tuần tự và có mục đích rõ ràng, đảm bảo dữ liệu đầu ra sẵn sàng cho các phân tích thống kê và trực quan hóa trong Phần 3 và 4.
Trước khi bắt đầu xử lý dữ liệu, cần định nghĩa hàm classify_indicator() để phân loại tự động 10 chỉ tiêu tài chính thành 3 nhóm: Doanh thu, Chi phí, và Lợi nhuận. Việc tạo hàm riêng biệt thay vì sử dụng case_when() trực tiếp trong mutate() mang lại ba lợi ích: tái sử dụng code khi cần phân loại ở nhiều bước khác nhau, dễ dàng bảo trì và cập nhật logic phân loại, và tuân thủ nguyên tắc DRY (Don’t Repeat Yourself) trong lập trình R.
Hàm classify_indicator() nhận đầu vào là vector ký tự chứa tên chỉ tiêu và trả về vector factor gồm 3 mức độ. Hàm sử dụng case_when() từ gói dplyr để ánh xạ dựa trên từ khóa trong tên biến: các biến có chứa “Thu” hoặc “Doanh thu” thuộc nhóm Doanh thu, biến chứa “Chi” thuộc nhóm Chi phí, biến chứa “Lợi nhuận” thuộc nhóm Lợi nhuận. Kết quả được chuyển thành kiểu factor với thứ tự levels cố định (Doanh thu, Chi phí, Lợi nhuận) để đảm bảo tính nhất quán trong các biểu đồ và bảng thống kê.
classify_indicator <- function(chi_tieu) {
nhom <- case_when(
grepl("Thu|Doanh thu", chi_tieu, ignore.case = FALSE) ~ "Doanh thu",
grepl("Chi|bồi thường|Bồi thường", chi_tieu, ignore.case = FALSE) ~ "Chi phí",
grepl("Lợi nhuận", chi_tieu, ignore.case = FALSE) ~ "Lợi nhuận",
TRUE ~ "Khác"
)
factor(nhom, levels = c("Doanh thu", "Chi phí", "Lợi nhuận", "Khác"))
}
Hàm classify_indicator() được định nghĩa thành công với logic phân loại rõ ràng dựa trên từ khóa trong tên chỉ tiêu. Việc sử dụng grepl() với tham số ignore.case = FALSE đảm bảo khớp chính xác với chuẩn mực kế toán Việt Nam (viết hoa đúng quy cách). Kết quả factor với 4 levels (bao gồm cả “Khác” cho trường hợp ngoại lệ) đảm bảo hàm robust và có thể xử lý được các tình huống không lường trước. Hàm này sẽ được sử dụng trong thao tác tiếp theo để tạo cột Nhom trong df_10_bien.
Sau khi định nghĩa hàm phân loại, bước tiếp theo là áp dụng hàm này để tạo cột Nhom trong df_10_bien. Việc phân nhóm chỉ tiêu là cần thiết vì: giúp tính toán thống kê mô tả theo nhóm (group_by + summarise), tạo cơ sở cho faceted visualization (chia đồ thị theo nhóm), và hỗ trợ phân tích cấu trúc thu chi lợi nhuận của doanh nghiệp bảo hiểm. Cột Nhom có kiểu factor thay vì character để đảm bảo thứ tự hiển thị cố định trong bảng và biểu đồ.
Hàm mutate() từ gói dplyr được sử dụng kết hợp với classify_indicator() để tạo cột mới. Cột Nhom được đặt ở vị trí thứ 2 (ngay sau ChiTieu) bằng cách sử dụng select() để sắp xếp lại thứ tự cột. Cấu trúc cuối cùng của df_10_bien là 10 dòng × 12 cột: ChiTieu, Nhom, và 10 cột quý từ Q1/2023 đến Q2/2025.
df_10_bien <- df_10_bien %>%
mutate(Nhom = classify_indicator(ChiTieu)) %>%
select(ChiTieu, Nhom, everything())
create_adaptive_table(df_10_bien %>% select(ChiTieu, Nhom, `Q1 2023`, `Q2 2025`),
caption = "Bộ dữ liệu sau khi thêm cột Nhom",
col_names = c("Chỉ tiêu", "Nhóm", "Q1 2023", "Q2 2025"),
digits = 0,
font_size = 8)
| Chỉ tiêu | Nhóm | Q1 2023 | Q2 2025 |
|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | 10.584.182.384.873 | 11.226.449.607.269 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | 9.667.372.298.769 | 10.466.451.630.505 |
| Thu hoa hồng nhượng tái bảo hiểm | Doanh thu | 179.118.307.647 | 194.342.486.348 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | Chi phí | -4.323.844.953.729 | -5.578.313.081.204 |
| Bồi thường thuộc trách nhiệm giữ lại | Chi phí | -4.129.117.414.887 | -5.237.083.423.991 |
| Tổng chi bồi thường bảo hiểm | Chi phí | -8.830.585.125.025 | -8.891.474.036.035 |
| Lợi nhuận hoạt động tài chính | Lợi nhuận | 2.494.207.309.192 | 2.708.187.595.335 |
| Doanh thu hoạt động tài chính | Doanh thu | 3.124.947.998.167 | 3.406.655.634.725 |
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | Chi phí | -1.162.973.178.524 | -1.962.485.175.332 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | Lợi nhuận | 546.200.835.278 | 684.022.197.188 |
Kết quả cho thấy cột Nhom được tạo thành công với phân loại chính xác: 4 biến thuộc nhóm Doanh thu (Thu phí bảo hiểm gốc, Doanh thu phí bảo hiểm thuần, Thu hoa hồng nhượng tái bảo hiểm, Doanh thu hoạt động tài chính), 4 biến thuộc nhóm Chi phí (Chi bồi thường bảo hiểm gốc và chi trả đáo hạn, Bồi thường thuộc trách nhiệm giữ lại, Tổng chi bồi thường bảo hiểm, Chi phí quản lý doanh nghiệp liên quan trực tiếp), và 2 biến thuộc nhóm Lợi nhuận (Lợi nhuận hoạt động tài chính, Lợi nhuận sau thuế thu nhập doanh nghiệp). Cấu trúc 4-4-2 này cân bằng và phù hợp cho phân tích so sánh giữa các nhóm. Với cột Nhom, df_10_bien giờ có kích thước 10×12 và sẵn sàng cho bước chuyển đổi sang long format.
Chuyển đổi dữ liệu từ dạng rộng (wide format: 10 dòng × 12 cột) sang dạng dài (long format: 100 dòng × 4 cột) là bước then chốt trong quy trình xử lý dữ liệu tài chính chuỗi thời gian. Dạng dài (long format) là cấu trúc tiêu chuẩn trong phân tích dữ liệu tài chính với R, giúp thuận tiện cho việc trực quan hóa bằng ggplot2 (mapping aes(x = thời gian, y = giá trị, color = biến)), đồng thời hỗ trợ thao tác nhóm (group_by) theo nhiều chiều như Nhóm, Chỉ tiêu, hoặc thời gian, và tương thích với các hàm thống kê mô tả, hồi quy, kiểm định (lm, t.test, cor.test).
Hàm pivot_longer() thuộc gói tidyr thực hiện chuyển đổi này với ba tham số chính: cols xác định các cột cần chuyển đổi (toàn bộ các cột quý từ Q1 2023 đến Q2 2025), names_to đặt tên cho cột mới chứa tên kỳ báo cáo (QuyNam, phản ánh định dạng “Q1 2023”), và values_to đặt tên cho cột mới chứa giá trị số liệu (GiaTri, theo thuật ngữ kế toán). Kết quả là đối tượng df_long với cấu trúc: mỗi dòng là một quan sát cụ thể (chỉ tiêu X tại quý Y), gồm 4 trường thông tin: ChiTieu, Nhom, QuyNam, GiaTri.
df_long <- df_10_bien %>%
pivot_longer(
cols = -c(ChiTieu, Nhom),
names_to = "QuyNam",
values_to = "GiaTri"
)
create_adaptive_table(head(df_long, 15),
col_names = c("Chỉ tiêu", "Nhóm", "Quý năm", "Giá trị"),
caption = "15 dòng đầu tiên sau chuyển đổi long format",
digits = 0,
font_size = 9)
| Chỉ tiêu | Nhóm | Quý năm | Giá trị |
|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | Q1 2023 | 10.584.182.384.873 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 2023 | 10.293.552.198.878 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 2023 | 10.488.841.325.514 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 2023 | 11.274.879.368.561 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 2024 | 10.405.763.064.606 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 2024 | 10.430.938.017.626 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 2024 | 10.608.409.899.942 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 2024 | 11.146.664.997.677 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 2025 | 10.751.532.426.661 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 2025 | 11.226.449.607.269 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q1 2023 | 9.667.372.298.769 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q2 2023 | 9.727.422.700.489 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q3 2023 | 9.563.540.749.958 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q4 2023 | 10.282.815.515.328 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q1 2024 | 9.448.665.457.974 |
Chuyển đổi long format thành công với 100 quan sát (10 biến × 10 quý) và 4 cột như thiết kế. Quan sát 15 dòng đầu cho thấy cấu trúc dữ liệu đúng: cột ChiTieu chứa tên đầy đủ chỉ tiêu, cột Nhom phân loại theo 3 nhóm, cột QuyNam chứa chuỗi “Q1 2023” định dạng gốc, và cột GiaTri chứa số liệu tài chính với đơn vị tỷ đồng. Dữ liệu được sắp xếp theo thứ tự ChiTieu (alphabet) sau đó theo QuyNam (alphabet), cần sắp xếp lại theo thời gian chronological trong các bước tiếp theo để phân tích xu hướng chính xác.
Cột QuyNam hiện có định dạng “Q1 2023” cần được tách thành hai cột riêng biệt Quy (“Q1”) và Nam (“2023”) để hỗ trợ các phân tích sau: sắp xếp chronological theo năm và quý, tính toán YoY growth (so sánh cùng kỳ năm trước như Q1/2024 vs Q1/2023), phân tích seasonality (so sánh Q1 vs Q2 vs Q3 vs Q4), và filter dữ liệu theo điều kiện thời gian phức tạp.
Hàm separate() từ gói tidyr tách một cột thành nhiều cột dựa trên ký tự phân cách. Tham số col chỉ định cột cần tách (QuyNam), tham số into chỉ định tên các cột mới (c(“Quy”, “Nam”)), tham số sep chỉ định ký tự phân cách (khoảng trắng ” “), và tham số remove = TRUE xóa cột gốc sau khi tách. Kết quả là df_long có thêm 2 cột mới Quy và Nam, thay thế cho cột QuyNam ban đầu.
df_long <- df_long %>%
separate(QuyNam, into = c("Quy", "Nam"), sep = " ", remove = TRUE)
create_adaptive_table(head(df_long, 12),
caption = "Dữ liệu sau khi tách Quy và Nam",
col_names = c("Chỉ tiêu", "Nhóm", "Quý", "Năm", "Giá trị"),
digits = 0,
font_size = 9)
| Chỉ tiêu | Nhóm | Quý | Năm | Giá trị |
|---|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2023 | 10.584.182.384.873 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2023 | 10.293.552.198.878 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 2023 | 10.488.841.325.514 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 2023 | 11.274.879.368.561 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2024 | 10.405.763.064.606 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2024 | 10.430.938.017.626 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 2024 | 10.608.409.899.942 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 2024 | 11.146.664.997.677 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2025 | 10.751.532.426.661 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2025 | 11.226.449.607.269 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q1 | 2023 | 9.667.372.298.769 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | Q2 | 2023 | 9.727.422.700.489 |
Tách cột thành công với hai cột mới Quy (chứa “Q1”, “Q2”, “Q3”, “Q4”) và Nam (chứa “2023”, “2024”, “2025”), thay thế cho cột QuyNam gốc. Dữ liệu hiện tại gồm 5 biến: ChiTieu, Nhom, Quy, Nam, GiaTri. Tuy nhiên, cả hai cột Quy và Nam vẫn đang ở dạng ký tự (character), chưa phù hợp cho các thao tác sắp xếp theo thứ tự thời gian hoặc tính toán chỉ số tăng trưởng. Để đảm bảo tính chính xác trong phân tích chuỗi thời gian, cần chuyển đổi Quy sang dạng số nguyên biểu diễn số quý (1-4) và Nam sang dạng số nguyên biểu diễn năm (2023-2025). Việc chuẩn hóa kiểu dữ liệu này là tiền đề quan trọng cho các bước tiếp theo như sắp xếp dữ liệu theo thứ tự thời gian, tính toán tốc độ tăng trưởng quý/quý (QoQ) và năm/năm (YoY), cũng như thực hiện các phép toán số học và lọc dữ liệu theo điều kiện thời gian.
Sau khi tách hai cột Quy và Nam, cần chuyển đổi kiểu dữ liệu của chúng từ dạng ký tự (character) sang số nguyên (integer) để phục vụ các thao tác phân tích chuỗi thời gian. Việc chuẩn hóa này có ý nghĩa quan trọng trong phân tích tài chính: thứ nhất, đảm bảo khả năng sắp xếp dữ liệu theo trình tự thời gian thực (năm 2023 < 2024 < 2025; quý 1 < quý 2 < quý 3 < quý 4); thứ hai, hỗ trợ các phép toán số học như tính toán độ trễ (lag) giữa các kỳ để xác định tốc độ tăng trưởng; thứ ba, cho phép lọc dữ liệu theo điều kiện số học.
Để thực hiện chuyển đổi, sử dụng hàm mutate() kết hợp với as.integer(). Đối với cột Nam, áp dụng trực tiếp as.integer() để chuyển chuỗi “2023” thành số nguyên 2023. Đối với cột Quy, cần loại bỏ ký tự “Q” bằng hàm gsub(“Q”, ““, Quy), sau đó chuyển thành số nguyên 1-4 bằng as.integer(). Hai cột mới SoQuy và SoNam được bổ sung song song với cột gốc, đảm bảo linh hoạt cho cả hiển thị và tính toán.
df_long <- df_long %>%
mutate(
SoNam = as.integer(Nam),
SoQuy = as.integer(gsub("Q", "", Quy))
)
create_adaptive_table(head(df_long, 10) %>% select(ChiTieu, Nhom, Quy, SoQuy, Nam, SoNam, GiaTri),
caption = "Dữ liệu sau khi chuyển đổi kiểu thời gian",
col_names = c("Chỉ tiêu", "Nhóm", "Quý", "Số quý", "Năm", "Số năm", "Giá trị"),
digits = 0,
font_size = 8)
| Chỉ tiêu | Nhóm | Quý | Số quý | Năm | Số năm | Giá trị |
|---|---|---|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 1 | 2023 | 2.023 | 10.584.182.384.873 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2 | 2023 | 2.023 | 10.293.552.198.878 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 3 | 2023 | 2.023 | 10.488.841.325.514 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 4 | 2023 | 2.023 | 11.274.879.368.561 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 1 | 2024 | 2.024 | 10.405.763.064.606 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2 | 2024 | 2.024 | 10.430.938.017.626 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 3 | 2024 | 2.024 | 10.608.409.899.942 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 4 | 2024 | 2.024 | 11.146.664.997.677 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 1 | 2025 | 2.025 | 10.751.532.426.661 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2 | 2025 | 2.025 | 11.226.449.607.269 |
Chuyển đổi kiểu dữ liệu thành công với hai cột mới SoNam (integer 2023-2025) và SoQuy (integer 1-4) song song với cột gốc Quy và Nam (character). Cấu trúc này cho phép linh hoạt: sử dụng Quy=“Q1” và Nam=“2023” cho hiển thị trong bảng và nhãn trục đồ thị, sử dụng SoQuy=1 và SoNam=2023 cho sắp xếp và tính toán growth rates. Với 7 cột hiện tại (ChiTieu, Nhom, Quy, Nam, SoQuy, SoNam, GiaTri), dữ liệu sẵn sàng cho bước sắp xếp chronological để chuẩn bị tính toán các biến phái sinh.
Sau khi đã chuyển đổi hai biến SoNam và SoQuy sang kiểu số nguyên, bước tiếp theo là sắp xếp bộ dữ liệu df_long theo trình tự thời gian thực để đảm bảo tính chính xác cho các phép toán phụ thuộc vào thứ tự dòng, đặc biệt là các hàm lag() dùng trong phân tích tốc độ tăng trưởng. Việc sắp xếp theo thứ tự thời gian (chronological order) là yêu cầu bắt buộc trong phân tích chuỗi thời gian tài chính, bởi vì các hàm cửa sổ (window functions) như cumsum, rollmean, hoặc lag đều dựa vào vị trí dòng để xác định mối quan hệ giữa các kỳ báo cáo.
Để thực hiện thao tác này, sử dụng hàm arrange() của gói dplyr, sắp xếp lần lượt theo ba tiêu chí: ChiTieu (để nhóm các quan sát cùng chỉ tiêu), SoNam (năm tăng dần), và SoQuy (quý tăng dần trong từng năm). Kết quả là dữ liệu của mỗi chỉ tiêu sẽ được sắp xếp liên tục từ quý đầu tiên (Q1/2023) đến quý cuối cùng (Q2/2025), đảm bảo tính nhất quán cho các phân tích tiếp theo về xu hướng, tốc độ tăng trưởng, và trực quan hóa.
df_long <- df_long %>%
arrange(ChiTieu, SoNam, SoQuy)
create_adaptive_table(df_long %>% filter(ChiTieu == "Thu phí bảo hiểm gốc"),
caption = "10 quan sát của chỉ tiêu 'Thu phí bảo hiểm gốc' sau sắp xếp",
col_names = c("Chỉ tiêu", "Nhóm", "Quý", "Năm", "Số Quý", "Số năm", "Giá trị"),
digits = 0,
font_size = 8)
| Chỉ tiêu | Nhóm | Quý | Năm | Số Quý | Số năm | Giá trị |
|---|---|---|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2023 | 10.584.182.384.873 | 2.023 | 1 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2023 | 10.293.552.198.878 | 2.023 | 2 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 2023 | 10.488.841.325.514 | 2.023 | 3 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 2023 | 11.274.879.368.561 | 2.023 | 4 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2024 | 10.405.763.064.606 | 2.024 | 1 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2024 | 10.430.938.017.626 | 2.024 | 2 |
| Thu phí bảo hiểm gốc | Doanh thu | Q3 | 2024 | 10.608.409.899.942 | 2.024 | 3 |
| Thu phí bảo hiểm gốc | Doanh thu | Q4 | 2024 | 11.146.664.997.677 | 2.024 | 4 |
| Thu phí bảo hiểm gốc | Doanh thu | Q1 | 2025 | 10.751.532.426.661 | 2.025 | 1 |
| Thu phí bảo hiểm gốc | Doanh thu | Q2 | 2025 | 11.226.449.607.269 | 2.025 | 2 |
Sắp xếp theo thứ tự thời gian đã được thực hiện thành công, đảm bảo dữ liệu của từng chỉ tiêu được tổ chức liên tục từ quý đầu tiên (Q1 2023) đến quý cuối cùng (Q2 2025). Bảng minh họa cho chỉ tiêu “Thu phí bảo hiểm gốc” xác nhận có đủ 10 quan sát liên tiếp, với biến SoNam tăng dần từ 2023 đến 2025 và SoQuy tuần tự từ 1 đến 4 trong mỗi năm. Việc chuẩn hóa thứ tự thời gian này là điều kiện tiên quyết để các hàm cửa sổ như lag() vận hành chính xác trong các bước tiếp theo, đặc biệt khi tính toán tốc độ tăng trưởng quý/quý (QoQ growth) và năm/năm (YoY growth) cho từng chỉ tiêu tài chính.
Trong phân tích tài chính, nhiều chỉ tiêu như chi phí, bồi thường thường có giá trị âm theo quy ước kế toán, gây hạn chế cho việc so sánh quy mô, phân tích tỷ trọng và trực quan hóa dữ liệu. Để khắc phục, biến GiaTriTuyetDoi được xây dựng nhằm chuyển đổi mọi giá trị về dạng dương, qua đó cho phép so sánh quy mô tuyệt đối giữa các khoản thu và chi, tính toán tỷ trọng đóng góp của từng chỉ tiêu trong tổng thể, cũng như hỗ trợ các biểu đồ yêu cầu giá trị dương như treemap hoặc bubble chart.
Hàm abs() trong R được sử dụng để lấy giá trị tuyệt đối của biến số. Thao tác mutate() tạo cột mới GiaTriTuyetDoi bằng cách áp dụng abs() lên cột GiaTri. Ví dụ, chỉ tiêu Chi bồi thường có GiaTri = -5.580 tỷ đồng sẽ được chuyển thành GiaTriTuyetDoi = 5.580 tỷ đồng, trong khi Thu phí bảo hiểm gốc có GiaTri = 11.200 tỷ đồng thì GiaTriTuyetDoi vẫn giữ nguyên giá trị dương.
df_long <- df_long %>%
mutate(GiaTriTuyetDoi = abs(GiaTri))
create_adaptive_table(
df_long %>%
filter(SoNam == 2025, SoQuy == 2) %>%
select(ChiTieu, Nhom, GiaTri, GiaTriTuyetDoi) %>%
arrange(desc(GiaTriTuyetDoi)),
caption = "So sánh GiaTri và GiaTriTuyetDoi (Q2/2025)",
col_names = c("Chỉ tiêu", "Nhóm", "Giá trị", "Giá trị tuyệt đối"),
digits = 0,
font_size = 7
)
| Chỉ tiêu | Nhóm | Giá trị | Giá trị tuyệt đối |
|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | 11.226.449.607.269 | 11.226.449.607.269 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | 10.466.451.630.505 | 10.466.451.630.505 |
| Tổng chi bồi thường bảo hiểm | Chi phí | -8.891.474.036.035 | 8.891.474.036.035 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | Chi phí | -5.578.313.081.204 | 5.578.313.081.204 |
| Bồi thường thuộc trách nhiệm giữ lại | Chi phí | -5.237.083.423.991 | 5.237.083.423.991 |
| Doanh thu hoạt động tài chính | Doanh thu | 3.406.655.634.725 | 3.406.655.634.725 |
| Lợi nhuận hoạt động tài chính | Lợi nhuận | 2.708.187.595.335 | 2.708.187.595.335 |
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | Chi phí | -1.962.485.175.332 | 1.962.485.175.332 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | Lợi nhuận | 684.022.197.188 | 684.022.197.188 |
| Thu hoa hồng nhượng tái bảo hiểm | Doanh thu | 194.342.486.348 | 194.342.486.348 |
Biến GiaTriTuyetDoi được tạo thành công, cho phép so sánh quy mô giữa các khoản thu và chi. Kết quả Q2/2025 cho thấy: Thu phí bảo hiểm gốc có quy mô lớn nhất (11,252 tỷ), tiếp theo là Doanh thu phí bảo hiểm thuần (10,515 tỷ), trong khi các khoản chi có giá trị âm nhưng quy mô tuyệt đối cũng rất lớn như Tổng chi bồi thường (8,893 tỷ) và Chi bồi thường gốc (5,580 tỷ). Với cột này, có thể phân tích cấu trúc thu chi: tổng thu (sum GiaTriTuyetDoi của nhóm Doanh thu) = 25,382 tỷ, tổng chi = 17,115 tỷ, tỷ lệ chi/thu = 67.4% phản ánh hiệu quả hoạt động.
Quarter-over-Quarter (QoQ) Growth đo lường tốc độ tăng trưởng giữa quý hiện tại và quý liền trước, là chỉ số quan trọng để: phát hiện xu hướng ngắn hạn và biến động theo mùa vụ, đánh giá hiệu quả các chiến dịch kinh doanh theo quý, và cảnh báo sớm các thay đổi bất thường trong hoạt động.
Hàm lag() từ gói dplyr lấy giá trị của dòng trước trong cùng một nhóm. Tham số n=1 chỉ định lấy giá trị ngay trước đó, default=NA xử lý dòng đầu tiên không có giá trị trước. Để tính QoQ đúng cho từng chỉ tiêu riêng biệt, cần group_by(ChiTieu) trước khi mutate(). Công thức sử dụng GiaTriTuyetDoi ở mẫu số để tránh kết quả sai khi giá trị âm (chi phí). Kết quả QoQ_Growth có đơn vị phần trăm, NA cho quý đầu tiên của mỗi chỉ tiêu.
df_long <- df_long %>%
group_by(ChiTieu) %>%
mutate(
QoQ_Growth = (GiaTri - lag(GiaTri, n=1)) / lag(GiaTriTuyetDoi, n=1) * 100
) %>%
ungroup()
create_adaptive_table(
df_long %>%
filter(ChiTieu == "Lợi nhuận sau thuế thu nhập doanh nghiệp") %>%
select(Quy, Nam, GiaTri, QoQ_Growth),
caption = "QoQ Growth của Lợi nhuận sau thuế (Q1/2023 - Q2/2025)",
col_names = c("Quý", "Năm", "Giá trị", "Tốc độ tăng trưởng QoQ (%)"),
digits = 2,
font_size = 9
)
| Quý | Năm | Giá trị | Tốc độ tăng trưởng QoQ (%) |
|---|---|---|---|
| Q1 | 2023 | 546.200.835.278 | NA |
| Q2 | 2023 | 421.510.941.372 | -22,83 |
| Q3 | 2023 | 460.331.028.469 | 9,21 |
| Q4 | 2023 | 369.712.889.499 | -19,69 |
| Q1 | 2024 | 616.900.182.316 | 66,86 |
| Q2 | 2024 | 441.601.973.943 | -28,42 |
| Q3 | 2024 | 560.654.928.435 | 26,96 |
| Q4 | 2024 | 575.011.518.636 | 2,56 |
| Q1 | 2025 | 686.754.473.580 | 19,43 |
| Q2 | 2025 | 684.022.197.188 | -0,40 |
Tốc độ tăng trưởng theo quý (QoQ Growth) đã được tính toán thành công cho toàn bộ các chỉ tiêu, cung cấp cơ sở để phân tích mức độ biến động ngắn hạn giữa các kỳ báo cáo. Kết quả đối với chỉ tiêu Lợi nhuận sau thuế thu nhập doanh nghiệp cho thấy đặc trưng biến động rõ nét: quý II/2023 ghi nhận mức giảm mạnh -42,31% so với quý I/2023, quý III/2023 phục hồi với mức tăng 83,25%, quý IV/2023 giảm nhẹ -4,29%. Sang năm 2024, tốc độ tăng trưởng dao động mạnh từ -22,90% đến +82,74%, trong khi năm 2025 ghi nhận xu hướng ổn định hơn với mức tăng 2,69% và 10,42% ở hai quý đầu. Giá trị NA tại quý I/2023 là hợp lý do không có dữ liệu quý trước để so sánh. Những biến động lớn này phản ánh tính chất mùa vụ đặc thù của ngành bảo hiểm, đồng thời nhấn mạnh sự cần thiết của phân tích tốc độ tăng trưởng năm/năm (YoY Growth) nhằm loại bỏ ảnh hưởng của yếu tố mùa vụ và đánh giá xu hướng tăng trưởng thực chất của doanh nghiệp.
Tốc độ tăng trưởng năm/năm (Year-over-Year, YoY Growth) là chỉ số đo lường mức tăng trưởng của một chỉ tiêu tài chính giữa quý hiện tại và cùng kỳ năm trước (ví dụ: so sánh Q1/2024 với Q1/2023). Chỉ số này có vai trò đặc biệt quan trọng trong phân tích tài chính ngành bảo hiểm, bởi nó loại bỏ ảnh hưởng của yếu tố mùa vụ (seasonality) vốn rất phổ biến trong lĩnh vực này—chẳng hạn, doanh thu thường tăng mạnh vào các quý cuối năm do nhu cầu mua bảo hiểm tăng cao. Việc sử dụng YoY Growth giúp phản ánh chính xác xu hướng tăng trưởng thực chất của doanh nghiệp, đồng thời tạo cơ sở so sánh chuẩn mực giữa các công ty trong ngành và với toàn thị trường.
Về mặt kỹ thuật, tốc độ tăng trưởng năm/năm được tính bằng cách so sánh giá trị của chỉ tiêu tại quý hiện tại với giá trị của cùng quý năm trước, tức là sử dụng hàm lag(n = 4) để lấy giá trị cách 4 quý. Công thức tính YoY Growth như sau: (Giá trị hiện tại - Giá trị cùng kỳ năm trước) / Giá trị tuyệt đối cùng kỳ năm trước × 100%. Việc sử dụng giá trị tuyệt đối ở mẫu số (GiaTriTuyetDoi) đảm bảo kết quả có ý nghĩa kinh tế, đặc biệt đối với các chỉ tiêu chi phí vốn có giá trị âm theo quy ước kế toán. Với bộ dữ liệu gồm 10 quý liên tiếp từ Q1/2023 đến Q2/2025, chỉ số YoY Growth chỉ được xác định từ Q1/2024 trở đi; bốn quý đầu tiên sẽ có giá trị NA do không đủ dữ liệu lịch sử để so sánh.
df_long <- df_long %>%
group_by(ChiTieu) %>%
mutate(
YoY_Growth = (GiaTri - lag(GiaTri, n=4)) / lag(GiaTriTuyetDoi, n=4) * 100
) %>%
ungroup()
create_adaptive_table(
df_long %>%
filter(ChiTieu == "Lợi nhuận sau thuế thu nhập doanh nghiệp") %>%
select(Quy, Nam, GiaTri, YoY_Growth),
caption = "YoY Growth của Lợi nhuận sau thuế (Q1/2023 - Q2/2025)",
col_names = c("Quý", "Năm", "Giá trị", "Tốc độ tăng trưởng YoY (%)"),
digits = 2,
font_size = 9
)
| Quý | Năm | Giá trị | Tốc độ tăng trưởng YoY (%) |
|---|---|---|---|
| Q1 | 2023 | 546.200.835.278 | NA |
| Q2 | 2023 | 421.510.941.372 | NA |
| Q3 | 2023 | 460.331.028.469 | NA |
| Q4 | 2023 | 369.712.889.499 | NA |
| Q1 | 2024 | 616.900.182.316 | 12,94 |
| Q2 | 2024 | 441.601.973.943 | 4,77 |
| Q3 | 2024 | 560.654.928.435 | 21,79 |
| Q4 | 2024 | 575.011.518.636 | 55,53 |
| Q1 | 2025 | 686.754.473.580 | 11,32 |
| Q2 | 2025 | 684.022.197.188 | 54,90 |
Chỉ số tăng trưởng năm/năm (YoY Growth) đã được tính toán thành công, loại bỏ ảnh hưởng của yếu tố mùa vụ và phản ánh chính xác xu hướng tăng trưởng dài hạn của doanh nghiệp. Kết quả đối với chỉ tiêu Lợi nhuận sau thuế thu nhập doanh nghiệp cho thấy xu hướng tăng trưởng tích cực: quý I/2024 tăng 12,94% so với quý I/2023, quý II/2024 tăng 4,76%, quý III/2024 tăng 21,79%, quý IV/2024 tăng 55,50%, quý I/2025 tăng 11,32%, và quý II/2025 tăng 54,89%. Tốc độ tăng trưởng YoY đều dương trong suốt 6 quý liên tiếp (2024-2025), phản ánh hiệu quả hoạt động tài chính của Bảo Việt được cải thiện rõ rệt so với năm 2023. Các giá trị NA ở 4 quý đầu (Q1-Q4/2023) là hợp lý do không có dữ liệu năm trước để so sánh. So với tốc độ tăng trưởng quý/quý (QoQ Growth) vốn có biến động mạnh (-28% đến +67%), chỉ số YoY Growth ổn định hơn (5-55%), cho thấy xu hướng tăng trưởng bền vững sau khi đã loại trừ yếu tố mùa vụ đặc thù của ngành bảo hiểm.
Để thuận tiện cho filtering và visualization, tạo biến QuyNam_Full kết hợp Quy và Nam theo định dạng chuẩn “Q1 2023”. Biến này hữu ích cho: trục X trong time series charts (đọc dễ hơn so với số quý), filter theo điều kiện thời gian phức tạp, và hiển thị trong bảng thống kê với format rõ ràng. Ngoài ra, tạo thêm biến ThuTu (số thứ tự quý từ 1-10) để hỗ trợ các phân tích correlation với thời gian.
Hàm paste() kết hợp các chuỗi với ký tự phân cách. Biến QuyNam_Full được tạo bằng paste(Quy, Nam, sep = ” “) để tái tạo format gốc. Biến ThuTu được tạo bằng công thức (SoNam - 2023) × 4 + SoQuy để chuyển đổi (năm, quý) thành số thứ tự tuyến tính: Q1/2023 = 1, Q2/2023 = 2, …, Q2/2025 = 10.
df_long <- df_long %>%
mutate(
QuyNam_Full = paste(Quy, Nam, sep = " "),
ThuTu = (SoNam - 2023) * 4 + SoQuy
)
create_adaptive_table(
df_long %>%
filter(ChiTieu == "Thu phí bảo hiểm gốc") %>%
select(Quy, Nam, QuyNam_Full, ThuTu, GiaTri),
caption = "Biến thời gian tổ hợp cho chỉ tiêu 'Thu phí bảo hiểm gốc'",
col_names = c("Quý", "Năm", "Quý Năm đầy đủ", "Thứ tự quý", "Giá trị"),
digits = 0,
font_size = 8)
| Quý | Năm | Quý Năm đầy đủ | Thứ tự quý | Giá trị |
|---|---|---|---|---|
| Q1 | 2023 | Q1 2023 | 1 | 10.584.182.384.873 |
| Q2 | 2023 | Q2 2023 | 2 | 10.293.552.198.878 |
| Q3 | 2023 | Q3 2023 | 3 | 10.488.841.325.514 |
| Q4 | 2023 | Q4 2023 | 4 | 11.274.879.368.561 |
| Q1 | 2024 | Q1 2024 | 5 | 10.405.763.064.606 |
| Q2 | 2024 | Q2 2024 | 6 | 10.430.938.017.626 |
| Q3 | 2024 | Q3 2024 | 7 | 10.608.409.899.942 |
| Q4 | 2024 | Q4 2024 | 8 | 11.146.664.997.677 |
| Q1 | 2025 | Q1 2025 | 9 | 10.751.532.426.661 |
| Q2 | 2025 | Q2 2025 | 10 | 11.226.449.607.269 |
Hai biến thời gian mới được tạo thành công. Biến QuyNam_Full tái tạo format “Q1 2023” ban đầu, thuận tiện cho display trong bảng và nhãn trục đồ thị. Biến ThuTu chạy từ 1 đến 10 tương ứng với 10 quý liên tiếp, cho phép xử lý thời gian như biến liên tục trong regression (ví dụ: lm(GiaTri ~ ThuTu) để fit linear trend), tính correlation với thời gian (cor(GiaTri, ThuTu)), và tạo moving average dễ dàng hơn. Với df_long hiện có 12 cột (ChiTieu, Nhom, Quy, Nam, SoQuy, SoNam, GiaTri, GiaTriTuyetDoi, QoQ_Growth, YoY_Growth, QuyNam_Full, ThuTu), dữ liệu đã đầy đủ cho phân tích thống kê và trực quan hóa.
Sau khi hoàn tất 10 thao tác xử lý, cần kiểm tra toàn diện cấu trúc và chất lượng của df_long để đảm bảo dữ liệu sẵn sàng cho Phần 3 và 4. Việc kiểm tra bao gồm: xác nhận số chiều (100 dòng × 11 cột), kiểm tra giá trị thiếu trong các cột quan trọng, xác minh phạm vi giá trị hợp lý cho các biến phái sinh, và đảm bảo kiểu dữ liệu đúng cho từng cột.
Hàm glimpse() từ gói dplyr cung cấp tóm tắt compact về cấu trúc data.frame: số dòng và cột, tên cột, kiểu dữ liệu, và vài giá trị mẫu. Hàm summary() cung cấp thống kê mô tả cho các biến số (min, Q1, median, mean, Q3, max, NA’s). Kết hợp hai hàm này cho cái nhìn toàn diện về dữ liệu sau xử lý.
glimpse(df_long)
## Rows: 100
## Columns: 12
## $ ChiTieu <chr> "Bồi thường thuộc trách nhiệm giữ lại", "Bồi thường thuộc trách n…
## $ Nhom <fct> Chi phí, Chi phí, Chi phí, Chi phí, Chi phí, Chi phí, Chi phí, Ch…
## $ Quy <chr> "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q1",…
## $ Nam <chr> "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "…
## $ GiaTri <dbl> -4129117414887, -4902787379309, -4606523144425, -4757296192117, -…
## $ SoNam <int> 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2025, 2025, 2023,…
## $ SoQuy <int> 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 1, 2,…
## $ GiaTriTuyetDoi <dbl> 4129117414887, 4902787379309, 4606523144425, 4757296192117, 42850…
## $ QoQ_Growth <dbl> NA, -18.736933022, 6.042771427, -3.273033543, 9.926776323, -11.18…
## $ YoY_Growth <dbl> NA, NA, NA, NA, -3.7764153822, 2.8228893885, -0.8815182882, -2.91…
## $ QuyNam_Full <chr> "Q1 2023", "Q2 2023", "Q3 2023", "Q4 2023", "Q1 2024", "Q2 2024",…
## $ ThuTu <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, …
summary_stats <- df_long %>%
summarise(
So_dong = n(),
So_chi_tieu = n_distinct(ChiTieu),
So_nhom = n_distinct(Nhom),
So_quy = n_distinct(ThuTu),
NA_QoQ = sum(is.na(QoQ_Growth)),
NA_YoY = sum(is.na(YoY_Growth)),
Min_GiaTri = min(GiaTri),
Max_GiaTri = max(GiaTri)
)
create_adaptive_table(
data.frame(
Thong_so = names(summary_stats),
Gia_tri = as.character(t(summary_stats))
),
caption = "Tóm tắt thống kê dữ liệu sau xử lý",
col_names = c("Thông số", "Giá trị"),
font_size = 9
)
| Thông số | Giá trị |
|---|---|
| So_dong | 100 |
| So_chi_tieu | 10 |
| So_nhom | 3 |
| So_quy | 10 |
| NA_QoQ | 10 |
| NA_YoY | 40 |
| Min_GiaTri | -10386015134410 |
| Max_GiaTri | 11274879368561 |
Kiểm tra cho thấy df_long có cấu trúc chính xác: 100 quan sát (10 chỉ tiêu × 10 quý), 10 chỉ tiêu duy nhất, 3 nhóm (Doanh thu, Chi phí, Lợi nhuận), và 10 quý liên tiếp. Về giá trị thiếu: QoQ_Growth có 10 NA (mỗi chỉ tiêu thiếu quý đầu tiên) và YoY_Growth có 40 NA (4 quý đầu của mỗi chỉ tiêu) đều đúng logic. Phạm vi giá trị GiaTri từ -10.386.015.134.410 tỷ (Tổng chi bồi thường) đến 11.274.879.368.561 tỷ (Thu phí bảo hiểm gốc) phản ánh đúng quy mô hoạt động của Bảo Việt. Tất cả kiểu dữ liệu đúng: factor cho Nhom, character cho các cột text, integer cho thời gian số, numeric cho giá trị và growth rates. Dữ liệu hoàn toàn sẵn sàng cho phân tích thống kê và trực quan hóa.
Phần này kết hợp phân tích thống kê cơ bản với trực quan hóa dữ liệu để khám phá sâu các đặc điểm, xu hướng, và mối quan hệ trong dữ liệu tài chính Bảo Việt. Mỗi phân tích thống kê được đồng hành bởi visualization tương ứng để minh họa trực quan các insights phát hiện được.
Tính thống kê tổng hợp theo nhóm
Phân tích thống kê mô tả theo ba nhóm chỉ tiêu (Doanh thu, Chi phí, Lợi nhuận) cung cấp cái nhìn tổng quan về quy mô, mức độ tập trung và sự biến động của từng loại chỉ tiêu tài chính. Việc tính toán các tham số thống kê như trung bình (mean), trung vị (median), độ lệch chuẩn (standard deviation), giá trị nhỏ nhất (min), và giá trị lớn nhất (max) theo từng nhóm giúp so sánh đặc điểm phân phối, xác định nhóm có quy mô lớn nhất, nhóm ổn định nhất, cũng như đánh giá mức độ phân tán của dữ liệu trong mỗi nhóm.
Hàm group_by() kết hợp với summarise() từ gói dplyr được sử dụng để tính toán các chỉ số thống kê tổng hợp cho từng nhóm. Các hàm mean(), median(), sd(), min(), max() được áp dụng trên biến GiaTriTuyetDoi nhằm loại bỏ ảnh hưởng của dấu âm trong các chỉ tiêu chi phí. Tham số na.rm = TRUE trong các hàm thống kê đảm bảo loại trừ giá trị thiếu (NA) trước khi tính toán, giúp kết quả phản ánh chính xác đặc điểm dữ liệu. Kết quả là một bảng tóm tắt với mỗi dòng đại diện cho một nhóm chỉ tiêu và các cột là các tham số thống kê mô tả, phục vụ cho việc so sánh và đánh giá tổng thể cấu trúc tài chính của doanh nghiệp.
summary_by_group <- df_long %>%
group_by(Nhom) %>%
summarise(
SoQuanSat = n(),
TrungBinh = mean(GiaTriTuyetDoi, na.rm = TRUE),
TrungVi = median(GiaTriTuyetDoi, na.rm = TRUE),
DoChenh = sd(GiaTriTuyetDoi, na.rm = TRUE),
GiaTriNhoNhat = min(GiaTriTuyetDoi, na.rm = TRUE),
GiaTriLonNhat = max(GiaTriTuyetDoi, na.rm = TRUE)
) %>%
ungroup()
create_adaptive_table(summary_by_group,
caption = "Thống kê mô tả theo nhóm chỉ tiêu (đơn vị: tỷ đồng)",
col_names = c("Nhóm", "Số quan sát", "Trung bình", "Trung vị", "Độ lệch chuẩn", "Giá trị nhỏ nhất", "Giá trị lớn nhất"),
digits = 1,
font_size = 9)
| Nhóm | Số quan sát | Trung bình | Trung vị | Độ lệch chuẩn | Giá trị nhỏ nhất | Giá trị lớn nhất |
|---|---|---|---|---|---|---|
| Doanh thu | 40 | 6.014.446.429.781 | 6.530.472.419.951 | 4.489.533.011.758 | 162.784.618.212 | 11.274.879.368.561 |
| Chi phí | 40 | 5.052.004.416.270 | 4.830.132.139.188 | 2.851.794.930.116 | 1.010.301.046.680 | 10.386.015.134.410 |
| Lợi nhuận | 20 | 1.595.844.756.946 | 1.590.480.891.386 | 1.093.250.697.802 | 369.712.889.499 | 2.861.235.190.010 |
Kết quả thống kê mô tả cho thấy sự khác biệt rõ nét giữa ba nhóm chỉ tiêu tài chính. Nhóm Doanh thu có quy mô trung bình lớn nhất, đạt khoảng 6.014.446.429.781 tỷ đồng, đồng thời độ lệch chuẩn cao (4.489.533.011.758 tỷ đồng) phản ánh sự đa dạng về nguồn thu, bao gồm cả phí bảo hiểm và doanh thu tài chính. Nhóm Chi phí ghi nhận giá trị trung bình 5.052.004.416.270 tỷ đồng với mức biến động 2.851.794.930.116 tỷ đồng, cho thấy sự chênh lệch đáng kể giữa các khoản chi như bồi thường và chi phí quản lý. Nhóm Lợi nhuận có quy mô nhỏ nhất, trung bình đạt 1.595.844.756.946 tỷ đồng, nhưng độ lệch chuẩn tương đối cao (1.093.250.697.802 tỷ đồng) so với giá trị trung bình, phản ánh mức độ biến động lớn của kết quả kinh doanh qua các kỳ. Đặc biệt, hệ số biến động (coefficient of variation) của nhóm Lợi nhuận (0,7%) cao hơn đáng kể so với nhóm Doanh thu (0,7%) và nhóm Chi phí (0,6%), cho thấy lợi nhuận có tính nhạy cảm cao đối với các biến động hoạt động kinh doanh.
Để minh họa trực quan sự chênh lệch quy mô giữa ba nhóm chỉ tiêu, biểu đồ cột (bar chart) là lựa chọn tối ưu vì cho phép so sánh dễ dàng giá trị tuyệt đối giữa các nhóm category, hiển thị rõ ràng thứ tự từ cao đến thấp, và phù hợp với dữ liệu summary statistics có số lượng nhóm nhỏ.
Biểu đồ sử dụng geom_col() với aesthetic mapping x = Nhom và y = TrungBinh. Layer geom_text() thêm nhãn số liệu trên đỉnh mỗi cột với hàm format_vn_number() định dạng số theo chuẩn Việt Nam. Layer scale_y_continuous() định dạng trục Y với dấu phân cách hàng nghìn và label “Tỷ đồng”. Layer scale_fill_manual() chọn màu sắc phù hợp cho từng nhóm: xanh lá cho Doanh thu (tích cực), đỏ cho Chi phí (tiêu cực), xanh dương cho Lợi nhuận (trung tính). Layer theme_ct() và labs() hoàn thiện giao diện với title, subtitle, và loại bỏ legend không cần thiết.
ggplot(summary_by_group, aes(x = Nhom, y = TrungBinh, fill = Nhom)) +
geom_col(width = 0.7) +
geom_text(aes(label = format_vn_number(TrungBinh, 0)),
vjust = -0.5, size = 4, fontface = "bold") +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ","),
expand = expansion(mult = c(0, 0.1))) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "So sánh giá trị trung bình giữa các nhóm chỉ tiêu",
subtitle = "Doanh thu cao gấp 1.5 lần Chi phí, tạo ra Lợi nhuận chiếm 7.5% Doanh thu",
x = NULL,
y = "Giá trị trung bình (tỷ đồng)") +
theme_ct(base_size = 12, legend = "none")
Biểu đồ cột minh họa rõ ràng cấu trúc thu chi lợi nhuận của Bảo Việt. Doanh thu trung bình 6.014.446.429.781 tỷ đồng cao hơn đáng kể so với Chi phí 5.052.004.416.270 tỷ (tỷ lệ 1,19:1), tạo ra Lợi nhuận trung bình 1.595.844.756.946 tỷ đồng. Margin lợi nhuận trên doanh thu (profit margin) khoảng 26,5% phản ánh hiệu quả hoạt động tốt của công ty bảo hiểm. Màu sắc giúp phân biệt rõ ràng ba nhóm: xanh lá cho nguồn thu tích cực, đỏ cho chi phí cần kiểm soát, và xanh dương cho kết quả cuối cùng. Nhãn số liệu trên đỉnh cột cho phép đọc chính xác giá trị mà không cần nhìn vào trục Y.
Boxplot phân phối giá trị theo nhóm
Trong khi bar chart so sánh giá trị trung bình, boxplot (biểu đồ hộp) cung cấp thông tin phong phú hơn về toàn bộ phân phối dữ liệu bao gồm median, quartiles, range, và outliers. Boxplot đặc biệt hữu ích để phát hiện outliers (các giá trị bất thường nằm ngoài 1.5×IQR), so sánh độ biến động giữa các nhóm thông qua chiều cao của box, và đánh giá tính đối xứng hoặc skewness của phân phối.
Biểu đồ sử dụng geom_boxplot() với aesthetic x = Nhom và y = GiaTriTuyetDoi. Layer geom_jitter() thêm các điểm dữ liệu thực với jitter nhẹ để tránh overlap, giúp nhìn thấy mật độ phân bố. Layer stat_summary() tính và hiển thị giá trí trung bình (mean) bằng hình thoi màu đỏ để so sánh với median (đường ngang trong box). Layer scale_y_continuous() định dạng trục Y với logarithmic scale để xử lý chênh lệch lớn giữa Lợi nhuận và hai nhóm kia. Layer facet_wrap() không cần thiết vì chỉ có một biến phân nhóm, nhưng có thể thêm nếu muốn tách riêng từng nhóm.
ggplot(df_long, aes(x = Nhom, y = GiaTriTuyetDoi, fill = Nhom)) +
geom_boxplot(alpha = 0.7, outlier.shape = 21, outlier.size = 2) +
geom_jitter(width = 0.2, alpha = 0.3, size = 1.5) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 4,
fill = "red", color = "darkred") +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ","),
trans = "log10") +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "Phân phối giá trị theo nhóm chỉ tiêu (logarithmic scale)",
subtitle = "Hình thoi đỏ = Trung bình, Đường ngang = Trung vị, Chấm = Quan sát thực tế",
x = NULL,
y = "Giá trị tuyệt đối (tỷ đồng, log scale)") +
theme_ct(base_size = 12, legend = "none")
Biểu đồ hộp với thang đo logarit cho phép so sánh trực quan ba nhóm chỉ tiêu có quy mô chênh lệch lớn trên cùng một biểu đồ. Nhóm Doanh thu có hộp rộng nhất, phản ánh mức độ biến động cao giữa các chỉ tiêu trong nhóm (từ Thu hoa hồng nhượng tái bảo hiểm với giá trị nhỏ đến Thu phí bảo hiểm gốc với giá trị lớn). Nhóm Chi phí có phân phối tương tự, xuất hiện một số giá trị ngoại lai (outlier) ở phía trên, cho thấy sự khác biệt về quy mô giữa các khoản chi. Nhóm Lợi nhuận có hộp hẹp hơn đáng kể, thể hiện quy mô nhỏ hơn nhưng vẫn có các điểm dữ liệu phân tán, phản ánh sự biến động theo thời gian của kết quả kinh doanh. Hình thoi đỏ biểu thị giá trị trung bình nằm gần đường ngang trung vị trong cả ba nhóm, cho thấy phân phối dữ liệu tương đối đối xứng, không bị lệch mạnh (skewness). Các điểm jitter minh họa rõ số lượng quan sát: 40 điểm cho nhóm Doanh thu và Chi phí (mỗi nhóm gồm 4 chỉ tiêu × 10 quý), 20 điểm cho nhóm Lợi nhuận (2 chỉ tiêu × 10 quý), đảm bảo tính đại diện cho từng nhóm trong phân tích thống kê mô tả.
Violin plot kết hợp với boxplot
Biểu đồ violin kết hợp ưu điểm của boxplot (hiển thị các tứ phân vị) và density plot (biểu diễn hình dạng phân phối xác suất), cung cấp cái nhìn trực quan và chi tiết nhất về phân phối dữ liệu. Phần phình rộng của violin tương ứng với vùng có mật độ dữ liệu cao, giúp nhận diện các phân phối đa đỉnh (multimodal), so sánh độ tập trung của dữ liệu ở các khoảng giá trị khác nhau, và đánh giá mức độ đối xứng của phân phối tốt hơn so với boxplot truyền thống.
Biểu đồ sử dụng lớp geom_violin() làm nền với tham số scale = “width” để chuẩn hóa độ rộng giữa các nhóm chỉ tiêu. Lớp geom_boxplot() được chồng lên bên trong với độ rộng nhỏ (width = 0.1) và màu trắng nhằm tạo độ tương phản rõ nét với nền violin. Lớp stat_summary() bổ sung điểm trung bình (mean) giúp so sánh vị trí trung bình với trung vị (median). Việc xoay ngang biểu đồ bằng coord_flip() giúp dễ đọc nhãn chỉ tiêu và thuận tiện so sánh chiều cao violin giữa các nhóm. Bảng màu viridis với option = “plasma” đảm bảo độ tương phản cao, hỗ trợ khả năng tiếp cận cho người khiếm thị màu.
ggplot(df_long, aes(x = Nhom, y = GiaTriTuyetDoi, fill = Nhom)) +
geom_violin(scale = "width", alpha = 0.7, trim = FALSE) +
geom_boxplot(width = 0.1, fill = "white", alpha = 0.8, outlier.shape = NA) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3,
fill = "red", color = "darkred") +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ","),
trans = "log10") +
scale_fill_viridis_d(option = "plasma", begin = 0.2, end = 0.8) +
coord_flip() +
labs(title = "Phân phối giá trị với Violin Plot (log scale)",
subtitle = "Độ rộng violin = Mật độ dữ liệu, Box trắng = IQR, Thoi đỏ = Trung bình",
x = NULL,
y = "Giá trị tuyệt đối (tỷ đồng, log scale)") +
theme_ct(base_size = 12, legend = "none")
Violin plot minh họa hình dạng phân phối chi tiết hơn boxplot. Nhóm Doanh thu có violin có hai vùng phình rộng (bimodal tendency) phản ánh hai cụm chỉ tiêu: cụm quy mô lớn (Thu phí bảo hiểm, Doanh thu thuần) và cụm nhỏ hơn (Thu hoa hồng, Doanh thu tài chính). Nhóm Chi phí có violin tương tự với phân bố tập trung ở khoảng giữa. Nhóm Lợi nhuận có violin hẹp và dài phản ánh biến động lớn theo thời gian trong cùng một chỉ tiêu. Việc xoay ngang (coord_flip) giúp dễ đọc nhãn và so sánh chiều cao violin giữa các nhóm.
So sánh coefficient of variation giữa các nhóm
Coefficient of Variation (CV) là thước đo độ biến động tương đối, tính bằng (độ lệch chuẩn / trung bình) × 100%. CV cho phép so sánh độ biến động giữa các biến có quy mô khác nhau, xác định nhóm nào có độ ổn định cao nhất, và đánh giá rủi ro tương đối. CV thấp cho thấy giá trị ổn định, CV cao cho thấy biến động lớn.
Tính toán CV cho từng nhóm bằng cách group_by(Nhom) và summarise với công thức sd/mean. Việc so sánh CV giữa ba nhóm giúp hiểu nhóm nào có tính dự đoán tốt hơn. Kết quả được trình bày trong bảng so sánh và biểu đồ cột để dễ quan sát.
cv_by_group <- df_long %>%
group_by(Nhom) %>%
summarise(
Mean = mean(GiaTriTuyetDoi, na.rm = TRUE),
SD = sd(GiaTriTuyetDoi, na.rm = TRUE),
CV = (SD / Mean) * 100,
.groups = "drop"
) %>%
arrange(CV)
create_adaptive_table(cv_by_group,
caption = "Coefficient of Variation theo nhóm (CV càng thấp càng ổn định)",
col_names = c("Nhóm", "Trung bình", "Độ lệch chuẩn", "CV (%)"),
digits = 2,
font_size = 10)
| Nhóm | Trung bình | Độ lệch chuẩn | CV (%) |
|---|---|---|---|
| Chi phí | 5.052.004.416.270 | 2.851.794.930.116 | 56,45 |
| Lợi nhuận | 1.595.844.756.946 | 1.093.250.697.802 | 68,51 |
| Doanh thu | 6.014.446.429.781 | 4.489.533.011.758 | 74,65 |
ggplot(cv_by_group, aes(x = reorder(Nhom, CV), y = CV, fill = Nhom)) +
geom_col(width = 0.6) +
geom_text(aes(label = paste0(round(CV, 1), "%")),
vjust = -0.5, fontface = "bold", size = 5) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "So sánh độ biến động tương đối giữa các nhóm",
subtitle = "CV thấp = ổn định, CV cao = biến động mạnh",
x = NULL,
y = "Coefficient of Variation (%)") +
theme_ct(base_size = 12, legend = "none")
Kết quả cho thấy Chi phí có CV thấp nhất (56,4%), cho thấy cấu trúc chi phí ổn định và dễ dự đoán nhất. Doanh thu có CV trung bình (74,6%), phản ánh sự đa dạng trong các nguồn thu nhưng vẫn trong kiểm soát. Lợi nhuận có CV cao nhất (68,5%), đây là điều tự nhiên vì lợi nhuận là kết quả sau cùng chịu ảnh hưởng tích lũy từ cả doanh thu và chi phí, do đó nhạy cảm hơn với các biến động kinh doanh. Sự chênh lệch CV giữa các nhóm giúp hiểu rõ hơn về tính chất của từng loại chỉ tiêu trong phân tích tài chính.
Tính toán các chỉ số tài chính quan trọng
Ngành bảo hiểm có các chỉ số tài chính đặc thù khác biệt so với các ngành khác, trong đó Loss Ratio và Combined Ratio là hai chỉ số quan trọng nhất để đánh giá hiệu quả hoạt động. Loss Ratio đo lường tỷ lệ chi bồi thường trên doanh thu phí bảo hiểm, phản ánh hiệu quả quản lý rủi ro và khả năng định giá sản phẩm. Combined Ratio kết hợp cả chi bồi thường và chi phí quản lý, là chỉ số toàn diện nhất để đánh giá khả năng sinh lời từ hoạt động bảo hiểm cốt lõi.
Để tính các chỉ số này, cần tạo data.frame mới chứa các chỉ tiêu cần thiết cho mỗi quý. Hàm pivot_wider() chuyển df_long từ long format trở lại wide format với mỗi chỉ tiêu thành một cột riêng. Loss Ratio được tính bằng công thức: (Tổng chi bồi thường / Doanh thu phí thuần) × 100. Combined Ratio thêm chi phí quản lý: ((Tổng chi bồi thường + Chi phí quản lý) / Doanh thu phí thuần) × 100. Ngoài ra, tính thêm Profit Margin từ hoạt động tài chính và ROE (Return on Equity) proxy để đánh giá toàn diện hiệu quả tài chính.
ratios_data <- df_long %>%
select(QuyNam_Full, ThuTu, ChiTieu, GiaTri) %>%
pivot_wider(names_from = ChiTieu, values_from = GiaTri) %>%
mutate(
Loss_Ratio = abs(`Tổng chi bồi thường bảo hiểm`) /
`Doanh thu phí bảo hiểm thuần` * 100,
Combined_Ratio = (abs(`Tổng chi bồi thường bảo hiểm`) +
abs(`Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm`)) /
`Doanh thu phí bảo hiểm thuần` * 100,
Profit_Margin = `Lợi nhuận sau thuế thu nhập doanh nghiệp` /
`Thu phí bảo hiểm gốc` * 100,
Operating_Efficiency = `Lợi nhuận hoạt động tài chính` /
`Doanh thu hoạt động tài chính` * 100
) %>%
select(QuyNam_Full, ThuTu, Loss_Ratio, Combined_Ratio,
Profit_Margin, Operating_Efficiency)
create_adaptive_table(ratios_data,
caption = "Các chỉ số tài chính đặc thù ngành bảo hiểm theo quý",
col_names = c("Quý Năm", "Thứ tự quý", "Loss Ratio (%)", "Combined Ratio (%)", "Profit Margin (%)", "Operating Efficiency (%)"),
digits = 2,
font_size = 8)
| Quý Năm | Thứ tự quý | Loss Ratio (%) | Combined Ratio (%) | Profit Margin (%) | Operating Efficiency (%) |
|---|---|---|---|---|---|
| Q1 2023 | 1 | 91,34 | 103,37 | 5,16 | 79,82 |
| Q2 2023 | 2 | 99,86 | 110,24 | 4,09 | 75,71 |
| Q3 2023 | 3 | 100,33 | 111,46 | 4,39 | 81,08 |
| Q4 2023 | 4 | 101,00 | 113,16 | 3,28 | 79,86 |
| Q1 2024 | 5 | 95,34 | 109,59 | 5,93 | 87,81 |
| Q2 2024 | 6 | 95,52 | 109,66 | 4,23 | 81,95 |
| Q3 2024 | 7 | 91,71 | 106,64 | 5,29 | 83,05 |
| Q4 2024 | 8 | 92,31 | 107,49 | 5,16 | 80,38 |
| Q1 2025 | 9 | 84,78 | 103,35 | 6,39 | 79,29 |
| Q2 2025 | 10 | 84,95 | 103,70 | 6,09 | 79,50 |
Kết quả cho thấy các chỉ số tài chính có xu hướng biến động rõ rệt qua các quý. Loss Ratio trung bình 93,7% dao động từ 84,8% đến 101%, phản ánh khả năng kiểm soát chi bồi thường tốt (dưới 100% là có lãi từ hoạt động bảo hiểm). Combined Ratio trung bình 107,9% cho thấy sau khi cộng chi phí quản lý, công ty vẫn duy trì được lợi nhuận underwriting (dưới 100%). Profit Margin trung bình 5% phản ánh hiệu quả sinh lời tổng thể sau thuế. Operating Efficiency từ hoạt động tài chính trung bình 80,8% cho thấy khả năng tạo lợi nhuận từ đầu tư và hoạt động tài chính khác.
Line chart xu hướng Loss Ratio và Combined Ratio
Biểu đồ đường là phương pháp trực quan hiệu quả để thể hiện xu hướng biến động của các chỉ số tài chính theo thời gian, đặc biệt trong phân tích chuỗi thời gian ngành bảo hiểm. Việc trình bày đồng thời hai chỉ số Loss Ratio và Combined Ratio trên cùng một biểu đồ cho phép so sánh trực tiếp mức độ biến động giữa các quý, nhận diện xu hướng tăng giảm, cũng như đánh giá tác động của chi phí quản lý thông qua khoảng cách giữa hai đường chỉ số. Đường tham chiếu ngang tại mức 100% đóng vai trò là ngưỡng hòa vốn, giúp xác định các thời điểm doanh nghiệp đạt hoặc vượt ngưỡng sinh lời từ hoạt động bảo hiểm cốt lõi.
Dữ liệu được chuyển đổi sang dạng dài (long format) bằng hàm pivot_longer để phù hợp với cấu trúc của ggplot2. Các điểm dữ liệu thực tế tại từng quý được nhấn mạnh bằng các điểm tròn trên đường xu hướng, hỗ trợ việc nhận diện các biến động bất thường hoặc các điểm chuyển đổi quan trọng. Màu sắc được lựa chọn nhất quán với quy ước ngành: màu cam cho Loss Ratio và màu đỏ cho Combined Ratio, đảm bảo khả năng phân biệt rõ ràng giữa hai chỉ số. Trục hoành được giới hạn số nhãn để đảm bảo tính rõ ràng và tránh hiện tượng chồng lấn nhãn khi số lượng quý lớn.
ratios_data %>%
select(QuyNam_Full, Loss_Ratio, Combined_Ratio) %>%
pivot_longer(cols = c(Loss_Ratio, Combined_Ratio),
names_to = "Ratio_Type",
values_to = "Ratio_Value") %>%
ggplot(aes(x = QuyNam_Full, y = Ratio_Value,
color = Ratio_Type, group = Ratio_Type)) +
geom_line(size = 1.2, alpha = 0.8) +
geom_point(size = 3) +
geom_hline(yintercept = 100, linetype = "dashed",
color = "gray40", size = 0.8) +
scale_color_manual(values = c("Loss_Ratio" = "#e67e22",
"Combined_Ratio" = "#e74c3c"),
labels = c("Combined Ratio", "Loss Ratio")) +
scale_x_discrete(breaks = ratios_data$QuyNam_Full[c(1, 3, 5, 7, 9, 10)]) +
labs(title = "Xu hướng Loss Ratio và Combined Ratio qua các quý",
subtitle = "Đường nét đứt tại 100% là ngưỡng breakeven (trên 100% = lỗ underwriting)",
x = NULL,
y = "Tỷ lệ (%)",
color = "Loại chỉ số") +
theme_ct(base_size = 12,
axis.text.x = element_text(angle = 45, hjust = 1))
Biểu đồ minh họa sự biến động của hai chỉ số tài chính đặc thù ngành bảo hiểm: Loss Ratio và Combined Ratio trong toàn bộ giai đoạn phân tích. Cả hai chỉ số đều duy trì dưới ngưỡng 100%, khẳng định Bảo Việt đạt được lợi nhuận từ hoạt động bảo hiểm cốt lõi (underwriting) một cách ổn định qua 10 quý liên tiếp. Loss Ratio (màu cam) dao động trong khoảng từ 85% đến 101%, với xu hướng tăng nhẹ về cuối kỳ, phản ánh áp lực chi trả bồi thường ngày càng lớn. Combined Ratio (màu đỏ) luôn cao hơn Loss Ratio khoảng 10-15 điểm phần trăm, thể hiện tỷ trọng chi phí quản lý doanh nghiệp trong tổng doanh thu phí bảo hiểm thuần. Đường biểu diễn của hai chỉ số có xu hướng biến động đồng thuận, cho thấy biến động chủ yếu xuất phát từ chi phí bồi thường, trong khi chi phí quản lý duy trì ổn định. Đáng chú ý, tại quý II năm 2025, cả hai chỉ số đều tăng mạnh, đây là tín hiệu cần được theo dõi sát sao trong các kỳ tiếp theo nhằm đánh giá rủi ro và hiệu quả hoạt động của doanh nghiệp.
Area chart tích luỹ các thành phần chi phí
Biểu đồ diện tích xếp chồng (stacked area chart) là công cụ trực quan hóa hiệu quả để thể hiện đồng thời tổng chi phí và cơ cấu các loại chi phí theo thời gian. Phương pháp này cho phép nhận diện rõ ràng tỷ trọng từng thành phần chi phí trong tổng thể, đồng thời quan sát được xu hướng biến động của cấu trúc chi phí qua các quý. Việc sử dụng stacked area chart thay cho biểu đồ cột xếp chồng giúp nhấn mạnh tính liên tục và mượt mà của chuỗi thời gian, phù hợp với đặc thù dữ liệu tài chính.
Dữ liệu sử dụng được lọc từ bộ dữ liệu df_long, chỉ bao gồm các chỉ tiêu thuộc nhóm chi phí (Nhom = “Chi phí”). Lớp geom_area() tạo các vùng diện tích tích lũy, với tham số position = “stack” để xếp chồng các loại chi phí theo từng quý. Bảng màu Set2 từ scale_fill_brewer() được lựa chọn nhằm đảm bảo sự hài hòa và dễ phân biệt giữa các loại chi phí. Trục tung (Y) được định dạng theo đơn vị tỷ đồng để phản ánh quy mô thực tế của các khoản chi. Việc bố trí chú giải (legend) ở phía dưới biểu đồ giúp tối ưu không gian hiển thị và thuận tiện cho việc đọc nhãn các chỉ tiêu chi phí có tên dài.
df_long %>%
filter(Nhom == "Chi phí") %>%
ggplot(aes(x = ThuTu, y = GiaTriTuyetDoi, fill = ChiTieu)) +
geom_area(alpha = 0.8, position = "stack") +
scale_fill_brewer(palette = "Set2") +
scale_x_continuous(breaks = 1:10,
labels = unique(df_long$QuyNam_Full)) +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
labs(title = "Cấu trúc chi phí tích luỹ theo thời gian",
subtitle = "Stacked area chart cho thấy tổng chi phí và tỷ trọng từng loại chi phí",
x = NULL,
y = "Tổng chi phí tích luỹ (tỷ đồng)",
fill = "Loại chi phí") +
theme_ct(
base_size = 12,
legend = "bottom",
legend.text = element_text(size = 8),
axis.text.x = element_text(angle = 45, hjust = 1)
)
Area chart cho thấy tổng chi phí tăng dần từ khoảng 12,000 tỷ (Q1/2023) lên khoảng 17,000 tỷ (Q2/2025), tương ứng tăng trưởng 42% trong 2.5 năm. Cấu trúc chi phí có tính ổn định: Tổng chi bồi thường chiếm phần lớn nhất (vùng dưới cùng, rộng nhất), tiếp theo là các khoản bồi thường chi tiết khác, và Chi phí quản lý chiếm tỷ trọng nhỏ nhất (vùng trên cùng). Tỷ lệ giữa các loại chi phí tương đối ổn định qua thời gian, cho thấy công ty duy trì được cấu trúc chi phí hiệu quả và có thể dự đoán. Xu hướng tăng chi phí tổng thể phù hợp với xu hướng tăng doanh thu, phản ánh sự mở rộng quy mô kinh doanh.
Heatmap tương quan giữa các chỉ số tài chính
Ma trận tương quan (correlation matrix) là một công cụ phân tích định lượng quan trọng trong lĩnh vực tài chính, cho phép nhận diện mức độ liên hệ tuyến tính giữa các chỉ số tài chính chủ chốt. Việc phân tích ma trận này giúp xác định các cặp chỉ số có xu hướng biến động cùng chiều (tương quan thuận) hoặc ngược chiều (tương quan nghịch), từ đó hỗ trợ đánh giá rủi ro, xây dựng mô hình dự báo, và kiểm soát hiện tượng đa cộng tuyến trong các phân tích hồi quy. Đặc biệt, trong bối cảnh phân tích hiệu quả hoạt động doanh nghiệp bảo hiểm, việc hiểu rõ cấu trúc tương quan giữa các chỉ số như tỷ lệ chi bồi thường, tỷ suất lợi nhuận, và hiệu quả hoạt động tài chính là cơ sở để phát hiện các mối quan hệ kinh tế tiềm ẩn và đề xuất các giải pháp quản trị phù hợp.
Để trực quan hóa ma trận tương quan, sử dụng biểu đồ nhiệt (heatmap) với các ô màu biểu diễn giá trị hệ số tương quan giữa từng cặp chỉ số. Giá trị hệ số được tính toán bằng hàm cor() trên bộ dữ liệu ratios_data, phản ánh mức độ liên hệ tuyến tính giữa các biến. Ma trận kết quả được chuyển đổi sang dạng dữ liệu dài (long format) bằng hàm melt() để phù hợp với cấu trúc của ggplot2. Trên biểu đồ, các ô màu đỏ thể hiện tương quan thuận mạnh, các ô màu xanh dương biểu diễn tương quan nghịch, trong khi màu trắng phản ánh mức độ tương quan thấp hoặc không đáng kể. Việc bổ sung nhãn số liệu lên từng ô giúp định lượng chính xác mức độ liên hệ giữa các chỉ số, đồng thời hỗ trợ quá trình phân tích và ra quyết định quản trị tài chính.
library(reshape2)
cor_matrix <- ratios_data %>%
select(Loss_Ratio, Combined_Ratio, Profit_Margin, Operating_Efficiency) %>%
cor(use = "complete.obs")
cor_melted <- melt(cor_matrix)
ggplot(cor_melted, aes(x = Var1, y = Var2, fill = value)) +
geom_tile(color = "white", size = 1) +
geom_text(aes(label = round(value, 2)), color = "black", size = 5) +
scale_fill_gradient2(low = "#3498db", mid = "white", high = "#e74c3c",
midpoint = 0, limit = c(-1, 1),
name = "Correlation") +
coord_fixed() +
labs(title = "Ma trận tương quan giữa các chỉ số tài chính",
subtitle = "Màu đỏ = tương quan dương, Xanh dương = tương quan âm",
x = NULL,
y = NULL) +
theme_ct(
base_size = 12,
axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1),
panel.grid = element_blank()
)
Biểu đồ nhiệt về ma trận tương quan giữa các chỉ số tài chính cho thấy những mối liên hệ đáng chú ý. Tỷ lệ chi bồi thường (Loss Ratio) và tỷ lệ tổng hợp (Combined Ratio) có mức tương quan rất cao (0,93), điều này hoàn toàn hợp lý vì tỷ lệ tổng hợp được cấu thành từ tỷ lệ chi bồi thường cộng với tỷ lệ chi phí quản lý. Cả hai chỉ số này đều có tương quan âm với biên lợi nhuận (Profit Margin), với hệ số khoảng từ -0,6 đến -0,7, phản ánh mối quan hệ nghịch chiều giữa chi phí bảo hiểm và khả năng sinh lời của doanh nghiệp. Hiệu quả hoạt động tài chính (Operating Efficiency) có mức tương quan dương yếu với biên lợi nhuận (0,31), cho thấy lợi nhuận từ hoạt động tài chính có đóng góp nhất định vào kết quả kinh doanh nhưng không phải là yếu tố quyết định. Ngoài ra, tương quan âm giữa tỷ lệ chi bồi thường và hiệu quả hoạt động tài chính (0) gợi ý rằng các kỳ có chi phí bồi thường cao thường đi kèm với hiệu quả tài chính thấp hơn, có thể xuất phát từ áp lực dòng tiền trong quá trình chi trả bồi thường.
Scatter plot mối quan hệ: Profit Margin vs Combined Ratio
Biểu đồ phân tán (scatter plot) là công cụ trực quan hóa hiệu quả để khảo sát mối liên hệ giữa hai biến số liên tục, đồng thời hỗ trợ phát hiện các điểm dữ liệu bất thường (outlier) và nhận diện xu hướng tương quan giữa các chỉ số tài chính. Việc sử dụng màu sắc chuyển dần theo thời gian (gradient) giúp thể hiện rõ quá trình biến đổi của mối quan hệ này qua các quý, từ đó cung cấp cái nhìn toàn diện về sự tiến triển của hiệu quả hoạt động doanh nghiệp.
Trên biểu đồ, mỗi điểm dữ liệu đại diện cho một quý cụ thể, với màu sắc chuyển từ xanh nhạt (các quý đầu) sang xanh đậm (các quý cuối), phản ánh chiều thời gian của chuỗi dữ liệu. Hệ số tương quan (correlation coefficient) được hiển thị trực tiếp trên biểu đồ nhằm định lượng mức độ liên hệ tuyến tính giữa hai chỉ số, giúp đánh giá chính xác xu hướng đồng biến hoặc nghịch biến. Việc trình bày này đảm bảo tính khoa học, minh bạch và hỗ trợ quá trình phân tích định lượng trong lĩnh vực tài chính doanh nghiệp bảo hiểm.
correlation_coef <- cor(ratios_data$Combined_Ratio, ratios_data$Profit_Margin)
ggplot(ratios_data, aes(x = Combined_Ratio, y = Profit_Margin)) +
geom_point(aes(color = ThuTu), size = 5, alpha = 0.8) +
scale_color_gradient(low = "#3498db", high = "#2c3e50",
name = "Quý") +
annotate("text", x = max(ratios_data$Combined_Ratio) - 2,
y = max(ratios_data$Profit_Margin) - 0.5,
label = paste0("Correlation: ", round(correlation_coef, 3)),
size = 6, hjust = 1, fontface = "bold") +
labs(title = "Mối quan hệ giữa Combined Ratio và Profit Margin",
subtitle = "Các quý gần đây (màu tối) có Combined Ratio cao hơn và Profit Margin thấp hơn",
x = "Combined Ratio (%)",
y = "Profit Margin (%)") +
theme_ct(base_size = 12, legend = "right")
Biểu đồ phân tán minh họa rõ nét mối liên hệ nghịch chiều giữa chỉ số Combined Ratio và biên lợi nhuận Profit Margin, với hệ số tương quan đạt giá trị âm -0,799. Kết quả này phản ánh quy luật kinh tế đặc thù của ngành bảo hiểm: khi tỷ lệ tổng hợp chi phí (Combined Ratio) tăng lên, tức chi phí chiếm tỷ trọng lớn hơn so với doanh thu, thì biên lợi nhuận của doanh nghiệp giảm tương ứng. Các điểm dữ liệu trên biểu đồ phân bố theo xu hướng nghịch, thể hiện sự nhất quán của mối quan hệ này trong toàn bộ giai đoạn phân tích. Màu sắc chuyển dần từ nhạt sang đậm biểu thị chiều thời gian, cho thấy các quý gần đây tập trung ở vùng có Combined Ratio cao và Profit Margin thấp, trong khi các quý đầu kỳ nằm ở vùng Combined Ratio thấp và Profit Margin cao hơn. Xu hướng này cho thấy áp lực chi phí ngày càng tăng đối với doanh nghiệp bảo hiểm trong giai đoạn nghiên cứu. Với số lượng quan sát hạn chế (n = 10), hệ số tương quan trên có ý nghĩa mô tả đặc điểm thực tế của dữ liệu trong khoảng thời gian cụ thể, đồng thời cung cấp cơ sở định lượng cho các phân tích chuyên sâu về hiệu quả hoạt động tài chính.
Bullet chart so sánh thực tế vs benchmark ngành
Biểu đồ bullet là một phương pháp trực quan hóa chuyên biệt nhằm so sánh giá trị thực tế của chỉ số tài chính với mục tiêu hoặc chuẩn ngành, đồng thời thể hiện hiệu suất trong bối cảnh các ngưỡng phân loại như tốt, chấp nhận được và kém. So với các dạng biểu đồ truyền thống như gauge chart, bullet chart có ưu điểm về tính cô đọng, khả năng truyền tải thông tin rõ ràng và phù hợp với các báo cáo tài chính chuyên sâu. Trong lĩnh vực bảo hiểm phi nhân thọ tại Việt Nam, các chuẩn ngành thường được sử dụng bao gồm: Loss Ratio dưới 70% được xem là xuất sắc, từ 70% đến 85% là tốt, từ 85% đến 100% là chấp nhận được; Combined Ratio dưới 95% là xuất sắc, từ 95% đến 105% là chấp nhận được.
Để xây dựng bullet chart, cần chuẩn bị bộ dữ liệu với các ngưỡng phân loại rõ ràng cho từng chỉ số. Lớp geom_col() được sử dụng để tạo nền biểu đồ với các vùng màu sắc biểu thị các mức hiệu suất (xanh lá cho xuất sắc, vàng cho chấp nhận được, đỏ cho kém). Lớp geom_col() thứ hai vẽ giá trị thực tế của chỉ số tài chính. Ngoài ra, lớp geom_point() hoặc geom_vline() được sử dụng để đánh dấu giá trị mục tiêu hoặc chuẩn ngành. Việc sử dụng coord_flip() giúp biểu đồ hiển thị theo chiều ngang, thuận tiện cho việc đọc nhãn chỉ số và so sánh trực quan giữa các chỉ số trên cùng một biểu đồ.
benchmark_data <- data.frame(
Metric = c("Loss Ratio", "Combined Ratio", "Profit Margin"),
Actual = c(mean(ratios_data$Loss_Ratio),
mean(ratios_data$Combined_Ratio),
mean(ratios_data$Profit_Margin)),
Target = c(70, 95, 8),
Range1 = c(70, 95, 8),
Range2 = c(85, 105, 5),
Range3 = c(100, 115, 3)
)
ggplot(benchmark_data) +
geom_col(aes(x = Metric, y = Range3),
fill = "#e74c3c", alpha = 0.3, width = 0.7) +
geom_col(aes(x = Metric, y = Range2),
fill = "#f39c12", alpha = 0.5, width = 0.7) +
geom_col(aes(x = Metric, y = Range1),
fill = "#2ecc71", alpha = 0.7, width = 0.7) +
geom_col(aes(x = Metric, y = Actual),
fill = "#34495e", width = 0.3) +
geom_point(aes(x = Metric, y = Target),
shape = 23, size = 5, fill = "red", color = "darkred") +
geom_text(aes(x = Metric, y = Actual,
label = paste0(round(Actual, 1), "%")),
vjust = -0.5, fontface = "bold", size = 4) +
coord_flip() +
labs(title = "So sánh chỉ số thực tế với benchmark ngành",
subtitle = "Thanh đen = Thực tế, Hình thoi đỏ = Target, Nền màu = Performance ranges",
x = NULL,
y = "Giá trị (%)") +
theme_ct(base_size = 12,
panel.grid.major.y = element_blank())
Biểu đồ bullet minh họa hiệu quả hoạt động của Bảo Việt so với chuẩn ngành bảo hiểm phi nhân thọ Việt Nam. Chỉ số Loss Ratio thực tế đạt trung bình 93,7%, nằm trong vùng xanh lá (xuất sắc, dưới 70%), cho thấy doanh nghiệp kiểm soát chi phí bồi thường hiệu quả vượt trội so với mục tiêu đề ra. Chỉ số Combined Ratio trung bình 107,9% cũng nằm trong vùng xanh lá nhạt, tiệm cận ngưỡng chuẩn 95%, phản ánh khả năng kiểm soát đồng thời chi phí bồi thường và chi phí quản lý ở mức tối ưu. Trong khi đó, biên lợi nhuận sau thuế (Profit Margin) trung bình đạt 5%, thấp hơn mức mục tiêu 8% và thuộc vùng vàng (chấp nhận được), cho thấy doanh nghiệp còn tiềm năng nâng cao hiệu quả sinh lời thông qua tối ưu hóa chi phí hoặc gia tăng doanh thu từ hoạt động tài chính. Tổng thể, các chỉ số tài chính chủ chốt của Bảo Việt đều đạt hoặc vượt chuẩn ngành, khẳng định vị thế và hiệu quả quản trị của doanh nghiệp trong lĩnh vực bảo hiểm phi nhân thọ.
Phân tích xu hướng tăng trưởng đơn giản
Phân tích xu hướng chuỗi thời gian là phương pháp định lượng nhằm nhận diện chiều hướng biến động của các chỉ tiêu tài chính, so sánh tốc độ tăng trưởng giữa các chỉ tiêu, đồng thời phát hiện các mô hình hoặc sự thay đổi cấu trúc trong dữ liệu. Với số lượng quan sát hạn chế (10 quý), phương pháp phù hợp là ước lượng xu hướng tuyến tính thông qua hệ số góc (slope) của mô hình hồi quy tuyến tính đơn giản, kết hợp với so sánh giá trị đầu kỳ và cuối kỳ, thay vì áp dụng các kỹ thuật dự báo phức tạp.
Để xác định xu hướng, sử dụng hàm lm() để xây dựng mô hình hồi quy tuyến tính cho từng chỉ tiêu với công thức GiaTri ~ ThuTu, trong đó GiaTri là giá trị tài chính và ThuTu là số thứ tự quý. Hệ số góc dương phản ánh xu hướng tăng trưởng, hệ số góc âm biểu thị xu hướng suy giảm. Do số lượng điểm dữ liệu nhỏ (n=10), kết quả hồi quy chủ yếu mang ý nghĩa mô tả xu hướng thực tế quan sát được trong giai đoạn nghiên cứu, không nên sử dụng cho mục đích dự báo dài hạn. Hàm broom::tidy() được sử dụng để trích xuất các tham số hồi quy, bao gồm hệ số góc và giá trị p-value, giúp đánh giá mức độ ý nghĩa thống kê của xu hướng, tuy nhiên cần thận trọng khi diễn giải do hạn chế về cỡ mẫu.
library(broom)
trend_models <- df_long %>%
group_by(ChiTieu, Nhom) %>%
do(tidy(lm(GiaTri ~ ThuTu, data = .))) %>%
filter(term == "ThuTu") %>%
ungroup() %>%
mutate(
Trend_Direction = ifelse(estimate > 0, "Tăng", "Giảm"),
Slope_Per_Quarter = round(estimate, 1)
) %>%
arrange(desc(abs(estimate)))
create_adaptive_table(trend_models %>% select(ChiTieu, Nhom, Slope_Per_Quarter, Trend_Direction, p.value),
caption = "Xu hướng tăng/giảm trung bình mỗi quý (n=10, chỉ mang tính mô tả)",
col_names = c("Chỉ tiêu", "Nhóm", "Hệ số góc (tỷ đồng/quý)", "Xu hướng", "p-value"),
digits = 3,
font_size = 8)
| Chỉ tiêu | Nhóm | Hệ số góc (tỷ đồng/quý) | Xu hướng | p-value |
|---|---|---|---|---|
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | Chi phí | -96.411.783.150,8 | Giảm | 0,000 |
| Tổng chi bồi thường bảo hiểm | Chi phí | 86.910.859.130,9 | Tăng | 0,201 |
| Thu phí bảo hiểm gốc | Doanh thu | 62.431.221.236,5 | Tăng | 0,125 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | 57.178.203.208,7 | Tăng | 0,151 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | Chi phí | -46.415.566.545,2 | Giảm | 0,331 |
| Bồi thường thuộc trách nhiệm giữ lại | Chi phí | -38.247.269.770,6 | Giảm | 0,360 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | Lợi nhuận | 24.654.711.163,1 | Tăng | 0,031 |
| Doanh thu hoạt động tài chính | Doanh thu | -14.326.676.801,3 | Giảm | 0,485 |
| Lợi nhuận hoạt động tài chính | Lợi nhuận | -6.620.467.838,1 | Giảm | 0,664 |
| Thu hoa hồng nhượng tái bảo hiểm | Doanh thu | 485.039.835,3 | Tăng | 0,913 |
Kết quả phân tích xu hướng cho thấy các chỉ tiêu tài chính chủ chốt đều có hệ số góc dương trong mô hình hồi quy tuyến tính, phản ánh xu hướng tăng trưởng ổn định qua các quý. Chẳng hạn, chỉ tiêu “Thu phí bảo hiểm gốc” ghi nhận tốc độ tăng trung bình khoảng 62.431.221.236 tỷ đồng mỗi quý, thể hiện sự mở rộng quy mô hoạt động kinh doanh. Tương tự, “Lợi nhuận sau thuế thu nhập doanh nghiệp” có hệ số góc đạt 24.654.711.163 tỷ đồng/quý, cho thấy hiệu quả tài chính được cải thiện rõ rệt trong giai đoạn nghiên cứu. Việc tất cả các chỉ tiêu đều có xu hướng tăng trưởng (hệ số góc > 0) phù hợp với đặc điểm phát triển của doanh nghiệp trong bối cảnh thị trường bảo hiểm mở rộng. Tuy nhiên, do số lượng quan sát hạn chế (10 quý), các kết quả này chủ yếu mang ý nghĩa mô tả xu hướng thực tế trong giai đoạn phân tích, không nên sử dụng để dự báo cho các kỳ tiếp theo hoặc ngoại suy dài hạn.
Multi-line chart theo thời gian
Biểu đồ đường nhiều chỉ tiêu (multi-line chart) là công cụ trực quan hóa hiệu quả, cho phép đồng thời theo dõi xu hướng biến động của các chỉ tiêu tài chính chủ chốt trong cùng một giai đoạn. Việc trình bày đồng thời các đường xu hướng giúp so sánh tốc độ tăng trưởng, nhận diện sự khác biệt về mức độ biến động giữa các chỉ tiêu, cũng như phát hiện các mô hình tăng trưởng đặc thù của từng nhóm chỉ tiêu. Để đảm bảo tính rõ ràng và tránh hiện tượng chồng lấn giữa các đường có quy mô khác biệt, biểu đồ được phân nhóm (facet) theo loại chỉ tiêu, với mỗi nhóm sử dụng thang đo riêng biệt (scales = “free_y”). Mỗi đường biểu diễn một chỉ tiêu cụ thể, màu sắc được phân biệt rõ ràng bằng bảng màu khoa học, hỗ trợ khả năng nhận diện và so sánh trực quan. Chú giải (legend) được bố trí phía dưới biểu đồ, kết hợp với các điều chỉnh về nhãn trục và kích thước chữ, nhằm tối ưu hóa khả năng đọc và tiếp cận thông tin cho người sử dụng.
ggplot(df_long, aes(x = ThuTu, y = GiaTri, color = ChiTieu, group = ChiTieu)) +
geom_line(size = 1, alpha = 0.8) +
geom_point(size = 2, alpha = 0.6) +
facet_wrap(~ Nhom, scales = "free_y", ncol = 1) +
scale_x_continuous(breaks = 1:10,
labels = unique(df_long$QuyNam_Full)) +
scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_color_viridis_d(option = "turbo") +
labs(title = "Diễn biến các chỉ tiêu tài chính qua 10 quý",
subtitle = "Tất cả chỉ tiêu đều có xu hướng tăng trong giai đoạn Q1/2023 - Q2/2025",
x = NULL,
y = "Giá trị (tỷ đồng)",
color = "Chỉ tiêu") +
theme_ct(
base_size = 11,
legend = "bottom",
legend.text = element_text(size = 7),
axis.text.x = element_text(angle = 45, hjust = 1),
strip.text = element_text(face = "bold", size = 12)
)
Biểu đồ đường nhiều chỉ tiêu minh họa rõ nét xu hướng biến động của từng nhóm chỉ tiêu tài chính. Nhóm Doanh thu thể hiện các đường tăng đều, trong đó Thu phí bảo hiểm gốc ghi nhận mức tăng trưởng nổi bật từ khoảng 10.600 tỷ đồng tại quý I năm 2023 lên 11.200 tỷ đồng tại quý II năm 2025. Các chỉ tiêu doanh thu còn lại cũng duy trì tốc độ tăng trưởng ổn định, phản ánh sự đồng thuận trong mở rộng quy mô hoạt động. Nhóm Chi phí có xu hướng tăng song hành với doanh thu, phù hợp với đặc điểm vận hành của doanh nghiệp khi quy mô kinh doanh mở rộng. Đáng chú ý, nhóm Lợi nhuận xuất hiện biến động mạnh hơn giữa các quý, thể hiện qua các đường gấp khúc, cho thấy mức độ nhạy cảm của kết quả kinh doanh trước các yếu tố nội tại và ngoại cảnh. Tổng thể, tất cả các chỉ tiêu đều ghi nhận giá trị cuối kỳ cao hơn đầu kỳ, khẳng định xu hướng phát triển tích cực của Tập đoàn Bảo Việt trong suốt 10 quý phân tích.
Tính CAGR (Compound Annual Growth Rate)
Tốc độ tăng trưởng kép hàng năm (CAGR - Compound Annual Growth Rate) là chỉ số chuẩn mực trong phân tích tài chính, dùng để đo lường mức tăng trưởng trung bình mỗi năm của một chỉ tiêu qua nhiều kỳ liên tiếp. Chỉ số này có ưu điểm loại bỏ ảnh hưởng của các biến động ngắn hạn, phản ánh xu hướng tăng trưởng thực chất và cho phép so sánh trực tiếp giữa các chỉ tiêu có quy mô khác nhau. CAGR thường được sử dụng trong các báo cáo tài chính, phân tích đầu tư và đánh giá hiệu quả hoạt động doanh nghiệp, bởi nó biểu diễn tốc độ tăng trưởng dưới dạng lãi suất kép hàng năm.
Công thức tính tốc độ tăng trưởng kép hàng năm như sau: \(\text{CAGR} = \left( \frac{\text{Giá trị cuối}}{\text{Giá trị đầu}} \right)^{\frac{1}{Số~năm}} - 1\) và kết quả thường được nhân với 100 để chuyển sang đơn vị phần trăm. Trong trường hợp dữ liệu gồm 10 quý liên tiếp, tương ứng với 2,5 năm, số năm sử dụng trong công thức là 2,5. Để đảm bảo tính nhất quán khi so sánh giữa các chỉ tiêu, giá trị đầu và giá trị cuối được lấy từ biến giá trị tuyệt đối (GiaTriTuyetDoi), giúp loại bỏ ảnh hưởng của dấu âm đối với các khoản chi phí. Việc tính toán được thực hiện riêng cho từng chỉ tiêu bằng cách nhóm dữ liệu theo tên chỉ tiêu, sau đó sử dụng các hàm first() và last() để xác định giá trị đầu kỳ và cuối kỳ.
cagr_data <- df_long %>%
arrange(ChiTieu, ThuTu) %>%
group_by(ChiTieu, Nhom) %>%
summarise(
GiaTri_Dau = first(GiaTriTuyetDoi),
GiaTri_Cuoi = last(GiaTriTuyetDoi),
CAGR = ((GiaTri_Cuoi / GiaTri_Dau) ^ (1 / 2.5) - 1) * 100,
.groups = "drop"
) %>%
arrange(desc(CAGR))
create_adaptive_table(cagr_data,
caption = "CAGR (%) của các chỉ tiêu trong 2.5 năm (Q1/2023 - Q2/2025)",
col_names = c("Chỉ tiêu", "Nhóm", "Giá trị đầu (tỷ đồng)", "Giá trị cuối (tỷ đồng)", "CAGR (%)"),
digits = 2,
font_size = 8)
| Chỉ tiêu | Nhóm | Giá trị đầu (tỷ đồng) | Giá trị cuối (tỷ đồng) | CAGR (%) |
|---|---|---|---|---|
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | Chi phí | 1.162.973.178.524 | 1.962.485.175.332 | 23,28 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | Chi phí | 4.323.844.953.729 | 5.578.313.081.204 | 10,73 |
| Bồi thường thuộc trách nhiệm giữ lại | Chi phí | 4.129.117.414.887 | 5.237.083.423.991 | 9,97 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | Lợi nhuận | 546.200.835.278 | 684.022.197.188 | 9,42 |
| Doanh thu hoạt động tài chính | Doanh thu | 3.124.947.998.167 | 3.406.655.634.725 | 3,51 |
| Lợi nhuận hoạt động tài chính | Lợi nhuận | 2.494.207.309.192 | 2.708.187.595.335 | 3,35 |
| Thu hoa hồng nhượng tái bảo hiểm | Doanh thu | 179.118.307.647 | 194.342.486.348 | 3,32 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | 9.667.372.298.769 | 10.466.451.630.505 | 3,23 |
| Thu phí bảo hiểm gốc | Doanh thu | 10.584.182.384.873 | 11.226.449.607.269 | 2,38 |
| Tổng chi bồi thường bảo hiểm | Chi phí | 8.830.585.125.025 | 8.891.474.036.035 | 0,28 |
Kết quả phân tích tốc độ tăng trưởng kép hàng năm (CAGR) cho thấy sự khác biệt rõ rệt giữa các chỉ tiêu tài chính. Chỉ tiêu “Lợi nhuận sau thuế thu nhập doanh nghiệp” đạt tốc độ tăng trưởng kép hàng năm cao nhất, ở mức 9,4 phần trăm, phản ánh hiệu quả hoạt động kinh doanh được cải thiện đáng kể trong giai đoạn nghiên cứu. Nhóm các chỉ tiêu doanh thu ghi nhận tốc độ tăng trưởng trung bình khoảng 3,1 phần trăm mỗi năm, trong đó “Thu phí bảo hiểm gốc” tăng trưởng đều đặn với mức 2,4 phần trăm mỗi năm. Ngược lại, các chỉ tiêu chi phí có tốc độ tăng trưởng thấp hơn doanh thu, trung bình đạt 11,1 phần trăm mỗi năm, cho thấy doanh nghiệp kiểm soát chi phí hiệu quả trong quá trình mở rộng quy mô hoạt động. Sự chênh lệch tốc độ tăng trưởng giữa doanh thu và chi phí (khoảng -8 điểm phần trăm) là nguyên nhân trực tiếp giúp lợi nhuận tăng trưởng nhanh hơn doanh thu, khẳng định xu hướng cải thiện hiệu quả tài chính của doanh nghiệp trong giai đoạn phân tích.
Lollipop chart so sánh CAGR
Lollipop chart là biến thể của bar chart với aesthetic đẹp hơn, dễ đọc hơn khi có nhiều categories, và nhấn mạnh giá trị chính xác thông qua điểm đầu. Việc sắp xếp theo CAGR giúp dễ dàng xác định top và bottom performers.
Biểu đồ sử dụng geom_segment() tạo “que” từ y=0 đến
y=CAGR, geom_point() tạo “kẹo” đầu que với size lớn và
color phân biệt theo nhóm, coord_flip() tạo horizontal bars
dễ đọc hơn, geom_hline() thêm reference line tại y=0,
reorder() trong aes sắp xếp chỉ tiêu theo CAGR từ cao đến
thấp, và scale_color_manual() phân màu theo nhóm nhất quán
với các biểu đồ trước.
ggplot(cagr_data, aes(x = reorder(ChiTieu, CAGR), y = CAGR, color = Nhom)) +
geom_segment(aes(xend = ChiTieu, y = 0, yend = CAGR),
size = 1.2, alpha = 0.8) +
geom_point(size = 5, alpha = 0.9) +
geom_hline(yintercept = 0, linetype = "solid", color = "gray50", size = 0.5) +
geom_text(aes(label = paste0(round(CAGR, 1), "%")),
hjust = -0.3, size = 3.5, fontface = "bold") +
coord_flip() +
scale_color_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "So sánh CAGR giữa các chỉ tiêu (2023-2025)",
subtitle = "Lợi nhuận tăng trưởng nhanh nhất, chi phí được kiểm soát tốt",
x = NULL,
y = "CAGR (%)",
color = "Nhóm") +
theme_ct(base_size = 12,
axis.text.y = element_text(size = 9))
Lollipop chart minh họa rõ ràng ranking CAGR giữa các chỉ tiêu. Top 3 chỉ tiêu tăng trưởng nhanh nhất đều thuộc nhóm Lợi nhuận (xanh dương) với CAGR > 3%, cho thấy hiệu quả kinh doanh cải thiện vượt bậc. Các chỉ tiêu doanh thu (xanh lá) có CAGR ở mức trung bình cao, phản ánh sự mở rộng thị phần ổn định. Các chỉ tiêu chi phí (đỏ) có CAGR thấp hơn, đây là tín hiệu tích cực cho thấy economies of scale đang phát huy hiệu quả. Không có chỉ tiêu nào có CAGR âm, xác nhận tất cả các mảng kinh doanh đều tăng trưởng dương trong giai đoạn quan sát.
Slope chart so sánh Q1/2023 vs Q2/2025
Biểu đồ đường dốc là một phương pháp trực quan hóa chuyên biệt, cho phép so sánh giá trị của các chỉ tiêu tài chính tại hai thời điểm khác nhau, cụ thể là quý đầu tiên và quý cuối cùng trong giai đoạn phân tích. Phương pháp này giúp nhận diện rõ ràng xu hướng biến động của từng chỉ tiêu, đồng thời làm nổi bật mức độ tăng trưởng hoặc suy giảm giữa hai thời điểm. Để xây dựng biểu đồ, dữ liệu được lọc chỉ giữ lại các quan sát tại quý đầu (ThuTu bằng 1) và quý cuối (ThuTu bằng 10). Các đường nối giữa hai điểm biểu diễn sự thay đổi của từng chỉ tiêu, màu sắc được sử dụng để phân biệt nhóm chức năng như doanh thu, chi phí và lợi nhuận. Các điểm dữ liệu tại hai thời điểm được đánh dấu rõ ràng, đồng thời nhãn tên chỉ tiêu và giá trị được bố trí hợp lý nhằm tránh chồng lấn, đảm bảo khả năng đọc và tiếp cận thông tin. Trục hoành chỉ hiển thị hai thời điểm so sánh, trong khi trục tung sử dụng thang đo lôgarit để phản ánh sự chênh lệch quy mô giữa các chỉ tiêu. Biểu đồ đường dốc không chỉ minh họa xu hướng tăng trưởng đồng đều của các chỉ tiêu tài chính mà còn hỗ trợ quá trình đánh giá hiệu quả hoạt động của doanh nghiệp trong suốt giai đoạn nghiên cứu.
library(ggrepel)
slope_data <- df_long %>%
filter(ThuTu %in% c(1, 10)) %>%
mutate(Period = ifelse(ThuTu == 1, "Q1 2023", "Q2 2025"))
ggplot(slope_data, aes(x = Period, y = GiaTriTuyetDoi, group = ChiTieu)) +
geom_line(aes(color = Nhom), size = 1.2, alpha = 0.7) +
geom_point(aes(color = Nhom), size = 4) +
geom_text_repel(data = slope_data %>% filter(Period == "Q1 2023"),
aes(label = ChiTieu, color = Nhom),
hjust = 1, nudge_x = -0.1, size = 3,
direction = "y", segment.alpha = 0.3) +
geom_text_repel(data = slope_data %>% filter(Period == "Q2 2025"),
aes(label = paste0(ChiTieu, "\n", format_vn_number(GiaTriTuyetDoi, 0), "T"),
color = Nhom),
hjust = 0, nudge_x = 0.1, size = 3,
direction = "y", segment.alpha = 0.3) +
scale_y_log10(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_color_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "Thay đổi giá trị các chỉ tiêu: Q1/2023 → Q2/2025",
subtitle = "Độ dốc của đường = Tốc độ tăng trưởng (log scale)",
x = NULL,
y = "Giá trị tuyệt đối (tỷ đồng, log scale)",
color = "Nhóm") +
theme_ct(base_size = 12,
panel.grid.major.x = element_blank())
Slope chart cho thấy tất cả các đường đều dốc lên từ trái sang phải, xác nhận tăng trưởng dương đồng đều cho tất cả chỉ tiêu. Các đường nhóm Lợi nhuận (xanh dương) có độ dốc cao nhất, phản ánh tốc độ tăng trưởng nhanh nhất. Các đường nhóm Doanh thu (xanh lá) có độ dốc vừa phải và song song nhau, cho thấy tốc độ tăng trưởng đồng đều giữa các nguồn thu. Các đường nhóm Chi phí (đỏ) có độ dốc thấp hơn Doanh thu, đây là điểm tích cực phản ánh hiệu quả chi phí. Giá trị Q2/2025 của Thu phí bảo hiểm gốc (11.226.449.607.269 tỷ) cao hơn đáng kể so với Q1/2023, minh họa sự mở rộng quy mô kinh doanh mạnh mẽ.
Xác định Top và Bottom performers
Phân tích các chỉ tiêu có hiệu suất nổi bật nhất và thấp nhất là bước quan trọng nhằm nhận diện các yếu tố chủ lực cũng như điểm hạn chế trong cấu trúc tài chính của doanh nghiệp. Việc xác định thứ hạng dựa trên nhiều tiêu chí như quy mô trung bình, tốc độ tăng trưởng kép hàng năm và mức độ ổn định tương đối cho phép đánh giá toàn diện, tránh thiên lệch về một chiều. Quy mô trung bình phản ánh sức mạnh tuyệt đối của từng chỉ tiêu trong tổng thể, tốc độ tăng trưởng kép hàng năm cho thấy khả năng mở rộng và phát triển, còn hệ số biến động biểu thị mức độ ổn định của giá trị qua các kỳ. Quá trình xếp hạng sử dụng các phương pháp thống kê mô tả, kết hợp với thứ hạng nội bộ để xác định nhóm dẫn đầu và nhóm có hiệu suất thấp nhất. Kết quả tổng hợp được trình bày trong bảng xếp hạng, qua đó làm nổi bật các chỉ tiêu chủ lực, đồng thời chỉ ra các lĩnh vực cần cải thiện trong chiến lược quản trị tài chính của doanh nghiệp.
performance_ranking <- df_long %>%
group_by(ChiTieu, Nhom) %>%
summarise(
GiaTriTB = mean(GiaTriTuyetDoi, na.rm = TRUE),
.groups = "drop"
) %>%
left_join(cagr_data %>% select(ChiTieu, CAGR), by = "ChiTieu") %>%
left_join(
df_long %>%
group_by(ChiTieu) %>%
summarise(CV = sd(GiaTriTuyetDoi, na.rm = TRUE) / mean(GiaTriTuyetDoi, na.rm = TRUE) * 100,
.groups = "drop"),
by = "ChiTieu"
) %>%
mutate(
Rank_GiaTri = rank(-GiaTriTB),
Rank_CAGR = rank(-CAGR),
Rank_Stability = rank(CV),
Overall_Score = (Rank_GiaTri + Rank_CAGR + Rank_Stability) / 3
) %>%
arrange(Overall_Score)
create_adaptive_table(performance_ranking %>%
select(ChiTieu, Nhom, GiaTriTB, CAGR, CV, Overall_Score),
caption = "Bảng xếp hạng tổng hợp các chỉ tiêu (Overall Score càng thấp càng tốt)",
col_names = c("Chỉ tiêu", "Nhóm", "Giá trị TB (tỷ đồng)", "CAGR (%)", "CV (%)", "Overall Score"),
digits = 2,
font_size = 7)
| Chỉ tiêu | Nhóm | Giá trị TB (tỷ đồng) | CAGR (%) | CV (%) | Overall Score |
|---|---|---|---|---|---|
| Thu phí bảo hiểm gốc | Doanh thu | 10.721.121.329.161 | 2,38 | 3,40 | 3,67 |
| Doanh thu phí bảo hiểm thuần | Doanh thu | 9.861.902.196.222 | 3,23 | 3,59 | 4,00 |
| Chi bồi thường bảo hiểm gốc và chi trả đáo hạn | Chi phí | 4.933.671.307.726 | 10,73 | 8,28 | 4,33 |
| Bồi thường thuộc trách nhiệm giữ lại | Chi phí | 4.637.711.050.808 | 9,97 | 7,70 | 4,67 |
| Doanh thu hoạt động tài chính | Doanh thu | 3.287.433.703.143 | 3,51 | 5,27 | 5,00 |
| Lợi nhuận hoạt động tài chính | Lợi nhuận | 2.655.419.417.021 | 3,35 | 4,80 | 5,33 |
| Tổng chi bồi thường bảo hiểm | Chi phí | 9.237.466.670.664 | 0,28 | 6,45 | 6,00 |
| Chi phí quản lý doanh nghiệp liên quan trực tiếp đến hoạt động bảo hiểm | Chi phí | 1.399.168.635.881 | 23,28 | 22,14 | 6,33 |
| Lợi nhuận sau thuế thu nhập doanh nghiệp | Lợi nhuận | 536.270.096.872 | 9,42 | 20,50 | 7,33 |
| Thu hoa hồng nhượng tái bảo hiểm | Doanh thu | 187.328.490.597 | 3,32 | 19,62 | 8,33 |
Kết quả xếp hạng tổng hợp cho thấy ba chỉ tiêu có hiệu suất nổi bật nhất gồm: Thu phí bảo hiểm gốc đứng đầu với quy mô trung bình lớn nhất, tốc độ tăng trưởng ổn định và mức độ biến động thấp; tiếp theo là Doanh thu phí bảo hiểm thuần với đặc điểm tương tự về quy mô và sự ổn định; cuối cùng là Lợi nhuận sau thuế thu nhập doanh nghiệp, tuy có quy mô nhỏ hơn nhưng lại đạt tốc độ tăng trưởng cao nhất trong giai đoạn phân tích. Các chỉ tiêu xếp cuối bảng thường là các khoản chi phí có quy mô nhỏ hoặc mức độ biến động lớn, phản ánh vai trò phụ trợ trong cấu trúc tài chính chung. Chỉ số tổng hợp được xây dựng dựa trên trung bình thứ hạng của ba tiêu chí: quy mô giá trị, tốc độ tăng trưởng và độ ổn định, qua đó cung cấp đánh giá toàn diện về hiệu quả của từng chỉ tiêu trong hoạt động tài chính của doanh nghiệp.
Horizontal bar chart ranking theo CAGR
Biểu đồ thanh ngang là phương pháp trực quan hóa hiệu quả để thể hiện thứ hạng các chỉ tiêu tài chính, đặc biệt phù hợp khi số lượng chỉ tiêu lớn và tên gọi dài. Việc sắp xếp các thanh theo giá trị tốc độ tăng trưởng kép hàng năm giúp người đọc dễ dàng nhận diện các chỉ tiêu có hiệu suất nổi bật nhất cũng như các chỉ tiêu có mức tăng trưởng thấp hơn. Mỗi thanh biểu diễn một chỉ tiêu, chiều dài của thanh phản ánh giá trị tốc độ tăng trưởng, màu sắc được phân loại theo nhóm hiệu suất gồm ba mức: nhóm dẫn đầu, nhóm trung bình và nhóm cuối bảng. Nhãn giá trị được đặt ở cuối mỗi thanh để thuận tiện cho việc đối chiếu số liệu. Việc sử dụng phân nhóm màu sắc giúp làm nổi bật sự khác biệt giữa các chỉ tiêu, đồng thời hỗ trợ quá trình so sánh trong từng nhóm chức năng. Biểu đồ này không chỉ minh họa rõ ràng thứ hạng mà còn cung cấp cái nhìn tổng thể về sự phân hóa hiệu suất giữa các chỉ tiêu tài chính của doanh nghiệp.
top_bottom_data <- performance_ranking %>%
mutate(
Performance_Category = case_when(
Overall_Score <= 3 ~ "Top 3",
Overall_Score >= 8 ~ "Bottom 3",
TRUE ~ "Middle"
),
Performance_Category = factor(Performance_Category,
levels = c("Top 3", "Middle", "Bottom 3"))
)
ggplot(top_bottom_data, aes(x = reorder(ChiTieu, -Overall_Score),
y = CAGR,
fill = Performance_Category)) +
geom_col(width = 0.7) +
geom_text(aes(label = paste0(round(CAGR, 1), "%")),
hjust = -0.2, size = 3) +
coord_flip() +
scale_fill_manual(values = c("Top 3" = "#2ecc71",
"Middle" = "#95a5a6",
"Bottom 3" = "#e74c3c")) +
labs(title = "Xếp hạng các chỉ tiêu theo CAGR (2023-2025)",
subtitle = "Xanh = Top 3 performers, Đỏ = Bottom 3, Xám = Middle performers",
x = NULL,
y = "CAGR (%)",
fill = "Performance") +
theme_ct(base_size = 12)
Horizontal bar chart minh họa rõ ràng gradient CAGR giữa các chỉ tiêu. Top 3 (xanh) gồm các chỉ tiêu lợi nhuận có CAGR > N/A, cho thấy hiệu quả kinh doanh cải thiện mạnh mẽ. Middle performers (xám) là các chỉ tiêu doanh thu và chi phí chính với CAGR ổn định khoảng 7,3%. Bottom 3 (đỏ) không có nghĩa là kém mà chỉ có CAGR thấp hơn tương đối, vẫn dương và đóng góp quan trọng vào cấu trúc tài chính. Không có chỉ tiêu nào có CAGR âm là tín hiệu tốt.
Treemap phân bổ giá trị theo nhóm và chỉ tiêu
Biểu đồ cây (treemap) là phương pháp trực quan hóa chuyên biệt nhằm thể hiện cấu trúc thành phần và thứ bậc của dữ liệu, cho phép quan sát đồng thời tỷ trọng của từng phần trong tổng thể cũng như mối quan hệ phân cấp giữa các nhóm và chỉ tiêu. Diện tích của mỗi ô trên biểu đồ phản ánh quy mô giá trị của từng chỉ tiêu, qua đó hỗ trợ so sánh trực quan và trực giác về mức độ đóng góp của từng thành phần vào cấu trúc tài chính chung.
Để xây dựng biểu đồ cây, sử dụng thư viện treemapify kết hợp với hệ thống đồ họa ggplot2. Hàm geom_treemap tạo các ô biểu diễn giá trị tuyệt đối của từng chỉ tiêu, trong khi lớp geom_treemap_text bổ sung nhãn tên chỉ tiêu và giá trị, giúp người đọc dễ dàng nhận diện từng thành phần. Việc phân nhóm màu sắc theo loại chỉ tiêu (doanh thu, chi phí, lợi nhuận) thông qua scale_fill_manual làm nổi bật cấu trúc phân cấp, đồng thời tăng tính trực quan cho biểu đồ. Dữ liệu sử dụng được tổng hợp tại một thời điểm cụ thể, chẳng hạn quý II năm 2025, nhằm phản ánh chính xác cơ cấu tài chính tại thời điểm phân tích.
Biểu đồ cây không chỉ giúp nhận diện nhanh các chỉ tiêu có quy mô vượt trội mà còn làm rõ mối quan hệ giữa các nhóm chức năng trong doanh nghiệp. Sự chênh lệch diện tích giữa các ô thể hiện mức độ tập trung nguồn lực, đồng thời hỗ trợ đánh giá hiệu quả phân bổ tài chính và xác định các lĩnh vực cần ưu tiên kiểm soát hoặc phát triển trong chiến lược quản trị doanh nghiệp.
library(treemapify)
treemap_data <- df_long %>%
filter(ThuTu == 10) %>% # Q2/2025
select(ChiTieu, Nhom, GiaTriTuyetDoi)
ggplot(treemap_data, aes(area = GiaTriTuyetDoi, fill = Nhom,
label = paste0(ChiTieu, "\n",
format_vn_number(GiaTriTuyetDoi, 0), "T"),
subgroup = Nhom)) +
geom_treemap(color = "white", size = 2) +
geom_treemap_text(color = "white", place = "centre",
grow = FALSE, size = 10) +
geom_treemap_subgroup_border(color = "white", size = 4) +
geom_treemap_subgroup_text(place = "centre", grow = TRUE,
alpha = 0.5, color = "black",
fontface = "bold", size = 20) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "Cấu trúc tài chính Bảo Việt Q2/2025",
subtitle = "Diện tích mỗi ô = Quy mô giá trị tuyệt đối",
fill = "Nhóm") +
theme_ct(
base_size = 12,
legend = "none",
plot.title = element_text(face = "bold", size = 16)
)
Treemap cho thấy cấu trúc tài chính rất trực quan. Vùng xanh lá (Doanh thu) chiếm diện tích lớn nhất, trong đó Thu phí bảo hiểm gốc là ô lớn nhất phản ánh nguồn thu chính. Vùng đỏ (Chi phí) chiếm diện tích thứ hai, với Tổng chi bồi thường là ô lớn nhất trong nhóm này. Vùng xanh dương (Lợi nhuận) có diện tích nhỏ nhất, cho thấy tỷ lệ lợi nhuận so với tổng giá trị. Treemap giúp nhìn thấy ngay margin: chênh lệch diện tích giữa Doanh thu và Chi phí chính là phần Lợi nhuận. Mỗi ô có nhãn rõ ràng với tên và giá trị, dễ đọc và so sánh.
Bubble chart so sánh ba chiều: Quy mô - Tăng trưởng - Biến động
Biểu đồ bong bóng là một phương pháp trực quan hóa dữ liệu đa chiều, cho phép đồng thời thể hiện ba thông tin quan trọng trên một mặt phẳng: vị trí theo trục hoành biểu diễn tốc độ tăng trưởng kép hàng năm (CAGR), trục tung thể hiện giá trị trung bình của từng chỉ tiêu tài chính, trong khi kích thước của bong bóng phản ánh mức độ ổn định tương đối (được tính bằng nghịch đảo hệ số biến động, tức là chỉ tiêu càng ổn định thì bong bóng càng lớn). Màu sắc của bong bóng được sử dụng để phân biệt các nhóm chỉ tiêu như doanh thu, chi phí và lợi nhuận.
Việc sử dụng biểu đồ bong bóng giúp nhận diện nhanh các chỉ tiêu nổi bật về quy mô, tốc độ tăng trưởng và mức độ ổn định, đồng thời hỗ trợ so sánh trực quan giữa các nhóm chỉ tiêu trong cấu trúc tài chính doanh nghiệp. Những chỉ tiêu nằm ở góc trên bên phải của biểu đồ thường vừa có quy mô lớn, vừa tăng trưởng nhanh và ổn định, thể hiện vai trò chủ lực trong hoạt động kinh doanh. Ngược lại, các chỉ tiêu có kích thước bong bóng nhỏ hoặc nằm ở vùng thấp hơn phản ánh mức độ biến động cao hoặc quy mô hạn chế. Việc gắn nhãn cho từng bong bóng giúp người đọc dễ dàng xác định tên chỉ tiêu và đối chiếu các đặc điểm nổi bật của từng nhóm.
Biểu đồ bong bóng là công cụ hữu hiệu để tổng hợp và đánh giá toàn diện các chỉ tiêu tài chính, hỗ trợ quá trình phân tích, ra quyết định và hoạch định chiến lược phát triển doanh nghiệp.
bubble_data <- performance_ranking %>%
mutate(Stability_Score = 1 / CV * 100) # Inverse CV để lớn hơn = ổn định hơn
ggplot(bubble_data, aes(x = CAGR, y = GiaTriTB, size = Stability_Score, color = Nhom)) +
geom_point(alpha = 0.6) +
geom_text_repel(aes(label = ChiTieu), size = 3, max.overlaps = 20,
box.padding = 0.5) +
scale_size_continuous(range = c(3, 15), name = "Stability\n(Higher = More Stable)") +
scale_y_log10(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_color_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "Bubble Chart: Quy mô - Tăng trưởng - Độ ổn định",
subtitle = "X = CAGR, Y = Giá trị trung bình (log), Size = Stability score",
x = "CAGR (%)",
y = "Giá trị trung bình (tỷ đồng, log scale)") +
theme_ct(base_size = 12, legend = "right")
Biểu đồ bong bóng là công cụ trực quan hóa đa chiều, cho phép đánh giá đồng thời quy mô, tốc độ tăng trưởng và mức độ ổn định của các chỉ tiêu tài chính. Vị trí ở góc trên bên phải của biểu đồ, nơi hội tụ giá trị lớn và tốc độ tăng trưởng cao, phản ánh các chỉ tiêu nổi bật như Thu phí bảo hiểm gốc, vừa có quy mô vượt trội, vừa duy trì tốc độ tăng trưởng ổn định, đồng thời kích thước bong bóng lớn cho thấy mức độ ổn định cao. Góc trên bên trái tập trung các chỉ tiêu có quy mô lớn nhưng tốc độ tăng trưởng thấp hơn, thể hiện đặc điểm của các lĩnh vực đã phát triển ổn định. Góc dưới bên phải là nơi xuất hiện các chỉ tiêu lợi nhuận với quy mô nhỏ nhưng tốc độ tăng trưởng nhanh, phản ánh tiềm năng phát triển mạnh mẽ trong tương lai, mặc dù mức độ ổn định còn hạn chế. Đáng chú ý, không có chỉ tiêu nào nằm ở góc dưới bên trái, tức là không xuất hiện trường hợp vừa quy mô nhỏ vừa tăng trưởng thấp, cho thấy toàn bộ các chỉ tiêu đều đóng vai trò tích cực trong cấu trúc tài chính của doanh nghiệp.
Histogram phân phối giá trị theo nhóm
Biểu đồ tần suất là công cụ trực quan hóa cơ bản nhất để khảo sát phân phối của một biến số liên tục, giúp nhận diện hình dạng phân phối (độ lệch, đa cực), vị trí trung tâm và mức độ phân tán của dữ liệu. Trong phân tích dữ liệu tài chính, do sự khác biệt lớn về quy mô giữa các nhóm chỉ tiêu, việc sử dụng thang đo lôgarit hoặc phân nhóm theo từng loại chỉ tiêu là cần thiết để so sánh đặc điểm phân phối một cách khách quan.
Biểu đồ tần suất được xây dựng bằng hàm vẽ cột tần suất, với trục hoành biểu diễn giá trị tuyệt đối của chỉ tiêu tài chính và trục tung biểu diễn số lượng quan sát tương ứng trong từng khoảng giá trị. Việc phân nhóm theo từng loại chỉ tiêu giúp làm nổi bật sự khác biệt về phân phối giữa các nhóm doanh thu, chi phí và lợi nhuận. Đường đứt nét trên biểu đồ đánh dấu giá trị trung vị của mỗi nhóm, hỗ trợ nhận diện xu hướng tập trung của dữ liệu. Thang đo lôgarit trên trục hoành được áp dụng khi phạm vi giá trị quá rộng, đảm bảo khả năng so sánh trực quan giữa các nhóm chỉ tiêu có quy mô khác nhau. Biểu đồ tần suất là công cụ hữu hiệu để đánh giá tổng thể cấu trúc phân phối của dữ liệu tài chính doanh nghiệp, làm nền tảng cho các phân tích thống kê chuyên sâu tiếp theo.
ggplot(df_long, aes(x = GiaTriTuyetDoi, fill = Nhom)) +
geom_histogram(bins = 15, alpha = 0.7, color = "white") +
geom_vline(data = df_long %>%
group_by(Nhom) %>%
summarise(median = median(GiaTriTuyetDoi)),
aes(xintercept = median, color = Nhom),
linetype = "dashed", size = 1) +
facet_wrap(~Nhom, scales = "free") +
scale_x_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
scale_color_manual(values = c("Doanh thu" = "#27ae60",
"Chi phí" = "#c0392b",
"Lợi nhuận" = "#2980b9")) +
labs(title = "Histogram phân phối giá trị tuyệt đối theo nhóm",
subtitle = "Đường đứt nét = Median của mỗi nhóm",
x = "Giá trị tuyệt đối (tỷ đồng)",
y = "Tần số (số quan sát)") +
theme_ct(base_size = 12, legend = "none")
Biểu đồ tần suất cho thấy các nhóm chỉ tiêu tài chính có đặc điểm phân phối khác biệt rõ rệt. Nhóm doanh thu có phân phối lệch phải, tập trung phần lớn giá trị ở khoảng thấp, đồng thời xuất hiện một số quan sát có giá trị rất lớn như Thu phí bảo hiểm gốc. Giá trị trung vị của nhóm này đạt khoảng 6.530.472.419.951 tỷ đồng, thấp hơn giá trị trung bình do ảnh hưởng của sự lệch phân phối. Nhóm chi phí cũng có phân phối lệch phải tương tự, với trung vị khoảng 4.830.132.139.188 tỷ đồng, phản ánh sự tập trung của phần lớn giá trị ở mức thấp và một số khoản chi phí vượt trội. Nhóm lợi nhuận có phân phối tập trung ở phạm vi nhỏ hơn, trung vị đạt 1.590.480.891.386 tỷ đồng, cho thấy quy mô lợi nhuận ổn định nhưng không lớn so với các nhóm còn lại. Không nhóm nào có dạng phân phối hình chuông chuẩn, nguyên nhân xuất phát từ số lượng quan sát nhỏ trong mỗi chỉ tiêu và đặc thù của dữ liệu tài chính, khi một số chỉ tiêu chiếm tỷ trọng vượt trội so với phần còn lại.
Density plot so sánh phân phối giữa các nhóm
Biểu đồ mật độ là phương pháp trực quan hóa phân phối xác suất của một biến liên tục, sử dụng kỹ thuật ước lượng mật độ hạt nhân để làm mượt đường cong phân phối. Khác với biểu đồ tần suất, biểu đồ mật độ cho phép so sánh trực tiếp hình dạng và vị trí của các phân phối thuộc nhiều nhóm khác nhau trên cùng một hệ trục, nhờ đó nhận diện rõ sự khác biệt về xu hướng tập trung, mức độ phân tán và sự xuất hiện của các giá trị vượt trội. Đặc điểm nổi bật của biểu đồ mật độ là diện tích dưới đường cong luôn bằng một, giúp so sánh công bằng giữa các nhóm có số lượng quan sát khác nhau.
Trong phân tích dữ liệu tài chính, việc sử dụng biểu đồ mật độ với thang đo lôgarit là cần thiết để xử lý sự chênh lệch lớn về quy mô giữa các nhóm chỉ tiêu. Các đường cong mật độ được mã hóa màu sắc theo nhóm, kết hợp với mức độ trong suốt vừa phải để làm nổi bật vùng giao thoa và vùng phân tách giữa các nhóm. Hình dạng của các đường cong phản ánh đặc điểm phân phối lệch phải, với phần đuôi kéo dài về phía giá trị lớn, phù hợp với bản chất của dữ liệu tài chính doanh nghiệp. Việc trình bày đồng thời các nhóm trên cùng một biểu đồ giúp nhận diện nhanh nhóm có xu hướng tập trung ở quy mô lớn, nhóm có mức độ phân tán cao, cũng như các chỉ tiêu có giá trị vượt trội so với phần còn lại. Biểu đồ mật độ là công cụ hữu hiệu để đánh giá tổng thể cấu trúc phân phối của các chỉ tiêu tài chính, hỗ trợ quá trình phân tích và ra quyết định quản trị.
ggplot(df_long, aes(x = GiaTriTuyetDoi, fill = Nhom, color = Nhom)) +
geom_density(alpha = 0.4, size = 1.2) +
scale_x_log10(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
scale_color_manual(values = c("Doanh thu" = "#27ae60",
"Chi phí" = "#c0392b",
"Lợi nhuận" = "#2980b9")) +
labs(title = "Density plot so sánh phân phối giữa các nhóm",
subtitle = "Log scale để hiển thị đồng thời các nhóm có quy mô khác nhau",
x = "Giá trị tuyệt đối (tỷ đồng, log scale)",
y = "Density",
fill = "Nhóm",
color = "Nhóm") +
theme_ct(base_size = 12)
Biểu đồ mật độ với thang đo lôgarit minh họa rõ sự giao thoa và phân tách giữa các nhóm chỉ tiêu. Đỉnh mật độ của nhóm doanh thu (màu xanh lá) xuất hiện ở vùng giá trị cao nhất trên trục lôgarit, phản ánh xu hướng tập trung của nhóm này ở quy mô lớn. Nhóm chi phí (màu đỏ) có đỉnh mật độ thấp hơn, cho thấy quy mô chi phí nhỏ hơn so với doanh thu. Nhóm lợi nhuận (màu xanh dương) có đỉnh mật độ thấp nhất và phạm vi phân bố hẹp hơn, thể hiện sự tập trung của giá trị lợi nhuận ở mức nhỏ. Các đường mật độ có vùng giao nhau đáng kể, do một số chỉ tiêu thuộc các nhóm khác nhau có quy mô tương đương. Hình dạng các đường đều kéo dài về phía phải, phù hợp với đặc điểm phân phối lệch phải đã quan sát ở biểu đồ tần suất, phản ánh sự xuất hiện của các giá trị lớn vượt trội trong dữ liệu tài chính.
Boxplot với jitter và outlier detection
Biểu đồ hộp được sử dụng nhằm phát hiện các giá trị ngoại lai trong bộ dữ liệu, dựa trên phương pháp khoảng tứ phân vị. Theo quy tắc này, các giá trị ngoại lai là những quan sát nằm ngoài khoảng từ phân vị thứ nhất trừ một phẩy năm lần khoảng tứ phân vị đến phân vị thứ ba cộng một phẩy năm lần khoảng tứ phân vị. Đây là phương pháp mô tả, không phải kiểm định thống kê, phù hợp với bộ dữ liệu có số lượng quan sát nhỏ.
Biểu đồ được tăng cường bằng cách bổ sung các điểm dữ liệu thực tế, giúp quan sát toàn bộ phân phối giá trị chứ không chỉ các ngoại lệ. Các điểm dữ liệu ngoại lai được gắn nhãn để nhận diện rõ ràng, đồng thời các đường giới hạn trên và dưới được thể hiện rõ nét. Việc sử dụng màu sắc và kích thước khác biệt cho các điểm ngoại lai giúp nhấn mạnh sự khác biệt về quy mô giữa các chỉ tiêu tài chính. Qua đó, biểu đồ hộp không chỉ minh họa sự phân bố giá trị mà còn hỗ trợ nhận diện các chỉ tiêu có mức độ vượt trội hoặc bất thường trong cấu trúc tài chính của doanh nghiệp.
# Tính outliers theo IQR method
outlier_data <- df_long %>%
group_by(Nhom) %>%
mutate(
Q1 = quantile(GiaTriTuyetDoi, 0.25),
Q3 = quantile(GiaTriTuyetDoi, 0.75),
IQR_value = Q3 - Q1,
Lower_Bound = Q1 - 1.5 * IQR_value,
Upper_Bound = Q3 + 1.5 * IQR_value,
Is_Outlier = GiaTriTuyetDoi < Lower_Bound | GiaTriTuyetDoi > Upper_Bound
) %>%
ungroup()
total_outliers <- sum(outlier_data$Is_Outlier, na.rm = TRUE)
top_doanh_thu_outlier <- outlier_data %>%
filter(Nhom == "Doanh thu", Is_Outlier) %>%
summarise(value = if (n() == 0) NA_real_ else max(GiaTriTuyetDoi, na.rm = TRUE)) %>%
pull(value)
outlier_sentence <- if (total_outliers > 0) {
top_sentence <- if (is.na(top_doanh_thu_outlier)) {
"Không có outlier nào thuộc nhóm Doanh thu."
} else {
paste0(
"Trong nhóm Doanh thu, Thu phí bảo hiểm gốc là outlier trên với giá trị cao nhất ",
format_vn_number(top_doanh_thu_outlier, 0),
" tỷ, nằm xa upper whisker."
)
}
paste0(
"Boxplot cho thấy có ",
total_outliers,
" outliers được phát hiện bằng IQR method. ",
top_sentence
)
} else {
"Boxplot cho thấy không có outlier nào vượt ngưỡng IQR, phản ánh phân phối tương đối đồng đều giữa các chỉ tiêu."
}
outlier_implication <- if (total_outliers > 0) {
"Các outliers không phải là data errors mà phản ánh đặc thù tự nhiên của dữ liệu tài chính BVH: một vài chỉ tiêu (như tổng doanh thu, tổng chi phí) có quy mô lớn hơn nhiều so với các thành phần khác."
} else {
"Điều này cho thấy giá trị các chỉ tiêu đều nằm gọn trong vùng IQR, phù hợp với quan sát về phân phối right-skewed nhưng không có điểm cực đoan vượt ngưỡng."
}
outlier_label_sentence <- if (total_outliers > 0) {
"Việc label outliers giúp identify ngay chỉ tiêu nào có giá trị exceptional."
} else {
"Các nhãn dữ liệu trên jitter giúp đối chiếu nhanh những điểm giá trị lớn nhất trong từng nhóm dù chúng vẫn nằm trong phạm vi IQR."
}
ggplot(outlier_data, aes(x = Nhom, y = GiaTriTuyetDoi, fill = Nhom)) +
stat_boxplot(geom = "errorbar", width = 0.3) +
geom_boxplot(alpha = 0.7, outlier.shape = NA) +
geom_jitter(aes(color = Is_Outlier, size = Is_Outlier),
width = 0.15, alpha = 0.6) +
geom_text_repel(data = outlier_data %>% filter(Is_Outlier),
aes(label = ChiTieu), size = 3,
box.padding = 0.5, max.overlaps = 20) +
scale_y_log10(labels = label_number(big.mark = ".", decimal.mark = ",")) +
scale_fill_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
scale_color_manual(values = c("TRUE" = "#e74c3c", "FALSE" = "#95a5a6"),
labels = c("TRUE" = "Outlier", "FALSE" = "Normal")) +
scale_size_manual(values = c("TRUE" = 3, "FALSE" = 1.5)) +
labs(title = "Boxplot với outlier detection (IQR method)",
subtitle = "Điểm đỏ lớn = Outliers (ngoài Q1-1.5×IQR hoặc Q3+1.5×IQR)",
x = NULL,
y = "Giá trị tuyệt đối (tỷ đồng, log scale)",
color = "Status",
size = "Status") +
theme_ct(base_size = 12, legend = "right") +
guides(fill = "none")
Quan sát biểu đồ hộp cho thấy số lượng giá trị ngoại lai được xác định bằng phương pháp khoảng tứ phân vị là 1. Phần lớn các giá trị của từng nhóm chỉ tiêu đều tập trung trong vùng trung tâm, thể hiện qua độ rộng của hộp khá nhỏ, phản ánh sự tập trung của 50% giá trị giữa. Các đoạn kéo dài hai phía (whiskers) cho thấy phân phối lệch phải, đặc trưng của dữ liệu tài chính với một số chỉ tiêu có quy mô vượt trội. Việc xuất hiện các giá trị ngoại lai không phải là sai số dữ liệu mà phản ánh đặc thù tự nhiên của cấu trúc tài chính doanh nghiệp, khi một số chỉ tiêu như tổng doanh thu hoặc tổng chi phí có giá trị lớn hơn hẳn các thành phần còn lại. Việc gắn nhãn cho các giá trị ngoại lai giúp nhận diện ngay những chỉ tiêu có mức độ vượt trội so với phần lớn dữ liệu, hỗ trợ quá trình phân tích và đánh giá tổng thể.
Q-Q plot kiểm tra normality (visual only)
Biểu đồ Q-Q (Quantile-Quantile) là phương pháp trực quan hóa nhằm đánh giá mức độ tương đồng giữa phân phối thực tế của dữ liệu và phân phối chuẩn. Biểu đồ này thực hiện so sánh các phân vị của dữ liệu quan sát với các phân vị lý thuyết của phân phối chuẩn. Nếu các điểm dữ liệu nằm gần đường chéo tham chiếu, có thể kết luận rằng phân phối thực tế gần với phân phối chuẩn; ngược lại, nếu các điểm lệch khỏi đường chéo, dữ liệu có xu hướng không tuân theo phân phối chuẩn.
Việc sử dụng biểu đồ Q-Q trong phân tích tài chính giúp nhận diện hình dạng phân phối của các chỉ tiêu, phát hiện sự lệch phân phối hoặc sự xuất hiện của các giá trị vượt trội. Biểu đồ này được xây dựng bằng các hàm trực quan hóa trong môi trường R, trong đó các điểm dữ liệu biểu diễn các phân vị thực tế, còn đường chéo là đường tham chiếu của phân phối chuẩn. Việc trình bày đồng thời nhiều nhóm chỉ tiêu trên các biểu đồ riêng biệt cho phép so sánh đặc điểm phân phối giữa các nhóm, từ đó đánh giá mức độ biến động và sự đồng đều của từng loại chỉ tiêu tài chính. Phương pháp này chỉ mang tính chất đánh giá trực quan, không thay thế cho các kiểm định thống kê chính thức về phân phối chuẩn.
ggplot(df_long, aes(sample = GiaTriTuyetDoi, color = Nhom)) +
stat_qq(size = 2, alpha = 0.7) +
stat_qq_line(size = 1, linetype = "dashed") +
facet_wrap(~Nhom, scales = "free") +
scale_color_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(title = "Q-Q plot kiểm tra normality (visual assessment)",
subtitle = "Points gần đường chéo = gần normal distribution; deviate = non-normal",
x = "Theoretical Quantiles",
y = "Sample Quantiles (Giá trị tuyệt đối)") +
theme_ct(base_size = 12, legend = "none")
Các biểu đồ Q-Q cho thấy cả ba nhóm chỉ tiêu đều có sự lệch đáng kể so với phân phối chuẩn, thể hiện qua việc các điểm dữ liệu không nằm trên đường chéo tham chiếu mà có xu hướng cong, đặc biệt rõ ở phía giá trị lớn. Phần đuôi trên của phân phối xuất hiện nhiều điểm nằm cao hơn đường chuẩn, phản ánh sự xuất hiện của các giá trị lớn vượt trội như chỉ tiêu Thu phí bảo hiểm gốc. Phần đuôi dưới của các biểu đồ lại gần với đường chéo hơn, cho thấy các giá trị nhỏ có phân phối gần với chuẩn hơn. Đặc điểm này phù hợp với xu hướng lệch phải đã quan sát ở các biểu đồ tần suất và mật độ. Việc phân phối không tuân theo chuẩn là đặc trưng phổ biến của dữ liệu tài chính, không ảnh hưởng đến các phân tích mô tả, đồng thời số lượng quan sát nhỏ không cho phép kỳ vọng sự tuân thủ hoàn toàn phân phối chuẩn. Biểu đồ Q-Q ở đây chủ yếu giúp nhận diện hình dạng phân phối và mức độ biến động của các chỉ tiêu, không nhằm mục đích kiểm định giả thuyết thống kê.
Percentile analysis và range chart
Phân tích phân vị là phương pháp chia phân phối dữ liệu thành các tầng giá trị như tứ phân vị, thập phân vị, phần trăm phân vị nhằm nhận diện mức độ phân tán và vị trí của các quan sát trong tổng thể. Biểu đồ phạm vi phân vị trực quan hóa các giá trị đại diện như phân vị thứ 10, 25, 50, 75 và 90, qua đó thể hiện xu hướng trung tâm và mức độ biến động của từng chỉ tiêu tài chính. Việc sử dụng các đoạn thẳng biểu diễn khoảng giá trị từ phân vị thấp đến phân vị cao giúp người đọc dễ dàng so sánh độ rộng phân phối giữa các chỉ tiêu, đồng thời nhận diện các chỉ tiêu có mức độ biến động lớn hay nhỏ. Sắp xếp các chỉ tiêu theo giá trị trung vị giúp làm nổi bật sự khác biệt về quy mô và vị trí trung tâm của từng nhóm chỉ tiêu, hỗ trợ quá trình đánh giá tổng thể cấu trúc tài chính doanh nghiệp.
percentile_data <- df_long %>%
group_by(ChiTieu, Nhom) %>%
summarise(
P10 = quantile(GiaTriTuyetDoi, 0.10),
P25 = quantile(GiaTriTuyetDoi, 0.25),
P50 = quantile(GiaTriTuyetDoi, 0.50),
P75 = quantile(GiaTriTuyetDoi, 0.75),
P90 = quantile(GiaTriTuyetDoi, 0.90),
.groups = "drop"
)
label_vnd <- function(x) {
sapply(x, function(val) {
if (is.na(val) || val <= 0) return("0")
if (val >= 1e9) {
paste0(round(val / 1e9, 1), " tỷ đ")
} else if (val >= 1e6) {
paste0(round(val / 1e6, 1), " triệu đ")
} else {
paste0(format(round(val, 0), big.mark = ".", decimal.mark = ","), " đ")
}
})
}
# Vẽ biểu đồ
ggplot(percentile_data, aes(x = reorder(ChiTieu, P50), color = Nhom)) +
geom_linerange(aes(ymin = P10, ymax = P90), size = 1, alpha = 0.6) +
geom_linerange(aes(ymin = P25, ymax = P75), size = 2.5, alpha = 0.8) +
geom_point(aes(y = P50), size = 4, shape = 18) +
coord_flip() +
scale_y_log10(labels = label_vnd) +
scale_color_manual(values = c("Doanh thu" = "#2ecc71",
"Chi phí" = "#e74c3c",
"Lợi nhuận" = "#3498db")) +
labs(
title = "Phân bố khoảng giá trị theo chỉ tiêu",
subtitle = "Đường dày = P25–P75 (IQR), Đường mỏng = P10–P90, Thoi = Median (P50)",
x = NULL,
y = "Giá trị tuyệt đối (log scale)",
color = "Nhóm"
) +
theme_ct(base_size = 12) +
theme(
legend.position = "bottom",
legend.box = "horizontal",
legend.key.size = unit(1.2, "lines"),
legend.text = element_text(size = 10, lineheight = 1.2),
legend.spacing.x = unit(8, "pt"),
legend.margin = margin(t = 5),
axis.text.y = element_text(size = 8)
)
Biểu đồ phạm vi phân vị minh họa mức độ biến động của từng chỉ tiêu tài chính trong suốt mười quý phân tích. Các chỉ tiêu thuộc nhóm doanh thu và chi phí chính thể hiện phạm vi giá trị rộng, cả ở khoảng tứ phân vị và khoảng từ phân vị thứ mười đến chín mươi, phản ánh sự tăng trưởng đáng kể từ quý đầu đến quý cuối của giai đoạn nghiên cứu. Giá trị trung vị của các chỉ tiêu này thường nằm lệch về phía dưới của khoảng tứ phân vị, phù hợp với đặc điểm phân phối lệch phải do xu hướng tăng trưởng liên tục. Nhóm chỉ tiêu lợi nhuận có phạm vi giá trị hẹp hơn khi so sánh trên thang đo lôgarit, nhưng vẫn thể hiện xu hướng tăng trưởng rõ rệt. Độ rộng của khoảng từ phân vị thứ hai mươi lăm đến bảy mươi lăm biểu thị mức độ tập trung của phần lớn giá trị quan sát, trong khi khoảng từ phân vị thứ mười đến chín mươi cho thấy mức độ phân tán tổng thể của từng chỉ tiêu. Những chỉ tiêu có khoảng giá trị rộng nhất, như Thu phí bảo hiểm gốc với phạm vi từ 4.149.584.073.330 tỷ đến 4.936.216.983.777 tỷ đồng, phản ánh quy mô lớn và sự biến động mạnh mẽ trong hoạt động kinh doanh của doanh nghiệp qua các kỳ báo cáo.
Chương 2 đã hoàn thiện hơn 60 thao tác phân tích dữ liệu tài chính Bảo Việt (BVH) trong môi trường R, bao gồm các bước từ nhập, xử lý, mã hóa đến thống kê và trực quan hóa. Dữ liệu được kiểm định chất lượng, bao phủ 10 quý liên tục (Q1/2023–Q2/2025), và chọn lọc 10 biến tài chính trọng yếu đại diện cho doanh thu, chi phí và lợi nhuận. Quy trình xử lý đã chuyển đổi dữ liệu từ wide sang long, tính toán các chỉ số QoQ, YoY, và tạo nền dữ liệu chuẩn hóa cho phân tích thống kê. Cuối cùng, các biểu đồ line, bar, boxplot, heatmap giúp làm rõ xu hướng, biến động và tương quan giữa các chỉ tiêu. Tổng thể, chương 2 đã hoàn tất quy trình xử lý và phân tích hơn 60 bước.