# ---- Cấu hình chung ----
options(bitmapType = "cairo")

knitr::opts_chunk$set(
  echo = TRUE,        # code hiển thị
  warning = FALSE,
  message = FALSE,
  fig.align = "center",
  fig.width = 8,
  fig.height = 5,
  dev = "cairo_pdf",
  tidy.opts = list(width.cutoff = 80),
  tidy = TRUE,
  comment = NA
)
# ---- Thư viện thường dùng ----
library(readxl)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(ggplot2)
library(car)
## Loading required package: carData
## 
## Attaching package: 'car'
## The following object is masked from 'package:dplyr':
## 
##     recode
library(knitr)
library(kableExtra)
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library(broom)
library(lmtest)
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
library(sandwich)
library(scales)
library(lubridate)
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
library(tidyr)
library(forcats)
library(stringr)
library(corrplot)
## corrplot 0.95 loaded
library(ggthemes)
library(gridExtra)
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
library(plotly)
## 
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout
library(viridis)
## Loading required package: viridisLite
## 
## Attaching package: 'viridis'
## The following object is masked from 'package:scales':
## 
##     viridis_pal
library(Cairo)

1 PHẦN 1. PHÂN TÍCH BỘ DỮ LIỆU HÀNH VI TIÊU DÙNG CỦA KHÁCH HÀNG

1.1 GIỚI THIỆU và MÃ HÓA BỘ DỮ LIỆU

1.1.1 Giới thiệu dữ liệu

Đọc dữ liệu

# 1. Đọc dữ liệu
data <- read.csv("C:/Users/Admin/Documents/DataR/customers_data.csv") #(1)
# 2. Xem tổng quan kích thước dữ liệu
num_rows <- nrow(data)     # số quan sát #(2)
num_cols <- ncol(data)     # số biến
cat("Số quan sát:", num_rows, "\n")
Số quan sát: 100000 
cat("Số biến:", num_cols, "\n")
Số biến: 12 
# 3. Tên các biến trong bộ dữ liệu
names(data) #(3)
 [1] "id"                 "age"                "gender"            
 [4] "income"             "education"          "region"            
 [7] "loyalty_status"     "purchase_frequency" "purchase_amount"   
[10] "product_category"   "promotion_usage"    "satisfaction_score"

Giải thích

  • (1),(2),(3): các lệnh được sử dùng nhằm mục đích đọc dữ liệu từ file excel, xem tổng số biến và quan sát và cuối cùng là các tên của biến có trong bộ dữ liệu.
  • Bộ dữ liệu được sử dụng trong nghiên cứu có tên là “Customer Purchases Behaviour Dataset”, được công bố trên nền tảng Kaggle bởi tác giả Sanyam Goyal. Đây là một bộ dữ liệu mô phỏng (simulated dataset) phản ánh hành vi mua sắm của khách hàng trong lĩnh vực thương mại điện tử, được thiết kế với mục tiêu phục vụ cho mục đích nghiên cứu, phân tích dữ liệu và xây dựng mô hình dự báo trong marketing.
  • Cụ thể, bộ dữ liệu gồm 100.000 quan sát (rows) và 12 biến (columns), mỗi quan sát tương ứng với một khách hàng giả định. Các biến bao phủ cả đặc điểm nhân khẩu học và hành vi mua hàng bao gồm: id, age, gender, income, education, region, loyalty_status, purchase_frequency, purchase_amount, product_category, promotion_usage, satisfaction_score.

Kiểu dữ liệu

# 4. Kiểm tra kiểu dữ liệu của từng biến
sapply(data, class) #(4)
                id                age             gender             income 
         "integer"          "integer"        "character"          "integer" 
         education             region     loyalty_status purchase_frequency 
       "character"        "character"        "character"        "character" 
   purchase_amount   product_category    promotion_usage satisfaction_score 
         "integer"        "character"          "integer"          "integer" 

Giải thích:

  • (4): sử dụng để xem kiểu dữ liệu từng biến
  • Tập dữ liệu này bao gồm 12 biến, được chia thành hai nhóm kiểu dữ liệu chính:
    • Số nguyên (Integer): id, age, income, purchase_amount, promotion_usage, satisfaction_score
    • Ký tự(Character): gender, education, region, loyalty_status, purchase_frequency, product_category

Xem 10 dòng đầu của dữ liệu

# 5. Xem dữ liệu 10 dòng đầu
head(data,10)
   id age gender income  education region loyalty_status purchase_frequency
1   1  27   Male  40682   Bachelor   East           Gold           frequent
2   2  29   Male  15317    Masters   West        Regular               rare
3   3  37   Male  38849   Bachelor   West         Silver               rare
4   4  30   Male  11568 HighSchool  South        Regular           frequent
5   5  31 Female  46952    College  North        Regular         occasional
6   6  38   Male   7347   Bachelor  South         Silver         occasional
7   7  32 Female   8265   Bachelor  South         Silver           frequent
8   8  24 Female  47773 HighSchool  North        Regular               rare
9   9  27   Male  19154    College   East        Regular         occasional
10 10  28 Female  24666 HighSchool  North        Regular               rare
   purchase_amount product_category promotion_usage satisfaction_score
1            18249            Books               0                  6
2             4557         Clothing               1                  6
3            11822         Clothing               0                  6
4             4098             Food               0                  7
5            19685         Clothing               1                  5
6             2822      Electronics               0                  5
7             3293         Clothing               0                  7
8            21794            Books               0                  5
9             5819         Clothing               0                  5
10            8779             Food               0                  6
# 6. Hiển thị vài dòng cuối để kiểm tra
tail(data,10)
           id age gender income  education region loyalty_status
99991   99991  34 Female  40601   Bachelor   West        Regular
99992   99992  37   Male  24100   Bachelor   East        Regular
99993   99993  31 Female  34119 HighSchool   West         Silver
99994   99994  37 Female  37380    College  South        Regular
99995   99995  30   Male  30740 HighSchool  South        Regular
99996   99996  31 Female  19691    College   West        Regular
99997   99997  36   Male  17428 HighSchool  South        Regular
99998   99998  29   Male  13222    College   West        Regular
99999   99999  31 Female  40093   Bachelor   West        Regular
100000 100000  35 Female  22249    College   West         Silver
       purchase_frequency purchase_amount product_category promotion_usage
99991          occasional           13850         Clothing               0
99992            frequent           10307         Clothing               0
99993                rare           11241           Health               0
99994                rare           13238             Food               0
99995            frequent           12385         Clothing               0
99996          occasional            7075           Health               0
99997                rare            6873           Health               0
99998            frequent            5152         Clothing               0
99999            frequent           16312           Health               1
100000               rare            9426           Health               0
       satisfaction_score
99991                   2
99992                   4
99993                   5
99994                   5
99995                   5
99996                   7
99997                   5
99998                   5
99999                   5
100000                  6

Tổng quan về bộ dữ liệu

str(data) #(6)
'data.frame':   100000 obs. of  12 variables:
 $ id                : int  1 2 3 4 5 6 7 8 9 10 ...
 $ age               : int  27 29 37 30 31 38 32 24 27 28 ...
 $ gender            : chr  "Male" "Male" "Male" "Male" ...
 $ income            : int  40682 15317 38849 11568 46952 7347 8265 47773 19154 24666 ...
 $ education         : chr  "Bachelor" "Masters" "Bachelor" "HighSchool" ...
 $ region            : chr  "East" "West" "West" "South" ...
 $ loyalty_status    : chr  "Gold" "Regular" "Silver" "Regular" ...
 $ purchase_frequency: chr  "frequent" "rare" "rare" "frequent" ...
 $ purchase_amount   : int  18249 4557 11822 4098 19685 2822 3293 21794 5819 8779 ...
 $ product_category  : chr  "Books" "Clothing" "Clothing" "Food" ...
 $ promotion_usage   : int  0 1 0 0 1 0 0 0 0 0 ...
 $ satisfaction_score: int  6 6 6 7 5 5 7 5 5 6 ...

-Lệnh số (6): sử dụng để xem cấu trúc tổng quan bộ dữ liệu

Đếm số lượng giá trị duy nhất trong cột education

# --- 1. Liệt kê CÁC GIÁ TRỊ CỤ THỂ ---
unique_levels <- unique(data$education) #(7)

cat("2. CÁC CẤP ĐỘ HỌC VẤN CỤ THỂ:\n")
2. CÁC CẤP ĐỘ HỌC VẤN CỤ THỂ:
print(unique_levels)
[1] "Bachelor"   "Masters"    "HighSchool" "College"   
cat("----------------------------------------------\n")
----------------------------------------------
  • Nhóm sử dụng lệnh số (7) nhằm xem giá trị cụ thể của biến “Education” Lọc ra tất cả các hàng có income có giá trị thấp nhất
# Lọc ra tất cả các hàng có income có giá trị thấp nhất
minincomevalue <- min(data$income, na.rm = TRUE) #(8)
minincomerow <- data %>%
  filter(income == minincomevalue)

print(minincomerow)
     id age gender income education region loyalty_status purchase_frequency
1  7026  32   Male   5000   College   East        Regular               rare
2 66721  28   Male   5000   College   West        Regular         occasional
3 71169  27 Female   5000   Masters   East        Regular               rare
  purchase_amount product_category promotion_usage satisfaction_score
1            1834         Clothing               1                  7
2            1620      Electronics               0                  5
3            1757           Health               0                  7
  • Lệnh số (8) sử dụng lọc ra các giá trị thấp nhất trong biến “Income”

Đếm số lượng khách hàng có mức chi tiêu là Gold

goldfrequentcount <- data %>% # 9
  filter(loyalty_status == "Gold",
         purchase_frequency == "frequent") %>%
  summarise(`Số lượng khách hàng Gold` = n()) %>%
  tibble::as_tibble()

kable(goldfrequentcount, align = "c") %>%
  kable_styling(full_width = FALSE, position = "center")
Số lượng khách hàng Gold
1992
  • Lệnh số (9) dùng để đếm số lượng khách hàng có thẻ chi tiêu là Gold, kết quả là có 1992 khách hàng.

1.1.2 Xử lý dữ liệu

Đổi tên cột purchase_amount

# Thao tác 11: Đổi tên cột
data1 <- data %>%
  rename(total_spent = purchase_amount) # (10)

# Kiểm tra kết quả (in ra tên cột để xác nhận)
print(names(data))
 [1] "id"                 "age"                "gender"            
 [4] "income"             "education"          "region"            
 [7] "loyalty_status"     "purchase_frequency" "purchase_amount"   
[10] "product_category"   "promotion_usage"    "satisfaction_score"
  • Nhóm dùng lệnh số (10) để đổi tên cột từ purchase_amount thành total_spent và lưu ở bảng mới tên “data1”

Tạo bảng mới loại bỏ cột id

# Thao tác 12: Loại bỏ cột
data_subset <- data %>%
  select(-id) # (11)

# Kiểm tra kết quả
print(names(data_subset))
 [1] "age"                "gender"             "income"            
 [4] "education"          "region"             "loyalty_status"    
 [7] "purchase_frequency" "purchase_amount"    "product_category"  
[10] "promotion_usage"    "satisfaction_score"
  • Câu lệnh số (11) dùng để xóa bỏ cột và cụ thể nhóm chúng em xóa cột “ID”.

Chuẩn hóa Z-score

# Thao tác 13: Chuẩn hóa Z-score
data <- data %>%
  mutate(
    # Sử dụng hàm scale() cơ bản của R để tính Z-score, 
    # hàm này trả về matrix nên cần dùng as.numeric()
    age_zscore = as.numeric(scale(age)) 
  )
# (12)
# Kiểm tra kết quả (xem trung bình và độ lệch chuẩn của age_zscore)
 data %>% summarise(mean_z = mean(age_zscore), sd_z = sd(age_zscore))
         mean_z sd_z
1 -2.136176e-16    1
 head(data$age_zscore, n = 10)
 [1] -0.6702905527 -0.2239152454  1.5615859837 -0.0007275918  0.2224600619
 [6]  1.7847736374  0.4456477155 -1.3398535136 -0.6702905527 -0.4471028990
 # (13)
  • Lệnh thứ (12),(13) dùng để chuẩn hóa biến age thành biến age_zscore bằng công thức Z-score. Kết quả kiểm tra cho thấy giá trị trung bình (mean_z) của biến mới về mặt thực tế gần bằng 0 và độ lệch chuẩn (sd_z) chính xác bằng 1.

Phân tổ biến income

# (14) Phân tổ
income_classification <- data %>% 
  select(id, income) %>%
  mutate(
    income_category = case_when(
      income <= 15000 ~ "Low",
      income > 15000 & income <= 40000 ~ "Medium",
      income > 40000 ~ "High",
      TRUE ~ "Undefined" # Dự phòng giá trị ngoại lệ
    )
  )

# Hiển thị 10 hàng đầu tiên để kiểm tra
income_classification %>%
  head(10) %>%
  kable(
    caption = "Bảng 1. Kết quả sau khi phân loại thu nhập",
    align = "c"
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    full_width = FALSE,
    position = "center"
  )
Bảng 1. Kết quả sau khi phân loại thu nhập
id income income_category
1 40682 High
2 15317 Medium
3 38849 Medium
4 11568 Low
5 46952 High
6 7347 Low
7 8265 Low
8 47773 High
9 19154 Medium
10 24666 Medium
  • Nhóm sử dụng lệnh số (14) nhằm mục đích phân tổ biến income và lưu thành cột mới tên income_classification.

Thay thế chuỗi “rare” thành “infrequent”

# (15) Thay thế chuỗi "rare" thành "infrequent"
data <- data %>%
  mutate(
    purchase_frequency = if_else(
      purchase_frequency == "rare",
      "infrequent",
      purchase_frequency
    )
  )

# Tạo bảng tóm tắt kết quả sau khi thay thế
freq_summary <- data %>% #(16)
  count(purchase_frequency, sort = TRUE) 

# Hiển thị bảng kết quả gọn đẹp trong PDF
freq_summary %>%
  kable(
    caption = "Bảng 3. Phân phối tần suất mua hàng sau xử lý",
    col.names = c("Tần suất mua hàng", "Số lượng"),
    align = "c"
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    full_width = FALSE,
    position = "center"
  )
Bảng 3. Phân phối tần suất mua hàng sau xử lý
Tần suất mua hàng Số lượng
infrequent 50019
occasional 29886
frequent 20095
  • Ở thao tác (15) nhóm thay thế chuỗi “rare” thành “infrequent”. và tính tần suất mua hàng cho bằng lệnh “count” ở thao tác (16).

Tạo biến dummy (0/1) cho nhóm “Gold”

# (17) Tạo biến dummy (0/1) cho nhóm "Gold"
data_dummy <- data %>%
  mutate(
    is_gold = if_else(loyalty_status == "Gold", 1, 0)
  )
# Hiển thị 10 dòng đầu để kiểm tra kết quả
data_dummy %>%
  select(loyalty_status, is_gold) %>%
  head(5) %>%
  kable(
    caption = "Bảng 4. Biến dummy",
    col.names = c("Trạng thái khách hàng", "is_gold"),
    align = "c"
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    full_width = FALSE,
    position = "center"
  )
Bảng 4. Biến dummy
Trạng thái khách hàng is_gold
Gold 1
Regular 0
Silver 0
Regular 0
Regular 0
  • Thao tác (17) này đã tạo ra một biến mới tên là “is_gold” trong tập dữ liệu data của bạn. Đây là một biến nhị phân hay còn gọi là biến giả (dummy variable) có tác dụng mã hóa mức độ trung thành của khách hàng.

Điền giá trị thiếu (NA) của satisfaction_score bằng Mode

# Giả sử biến satisfaction_score có giá trị thiếu (NA)
# (18) Tạo hàm tính Mode (giá trị xuất hiện nhiều nhất)
get_mode <- function(x) {
  freq <- table(x)
  names(freq)[which.max(freq)]
}

# Tính Mode của satisfaction_score
mode_satis <- get_mode(na.omit(data$satisfaction_score)) # (19)

# Thay thế NA bằng Mode vừa tìm được
data <- data %>%
  mutate(
    satisfaction_score = if_else(
      is.na(satisfaction_score),
      as.numeric(mode_satis),
      satisfaction_score
    )
  )

# Kiểm tra số lượng NA còn lại sau khi xử lý
cat("Số giá trị thiếu còn lại sau khi thay thế:", 
    sum(is.na(data$satisfaction_score)), "\n")
Số giá trị thiếu còn lại sau khi thay thế: 0 
  • Giả sử biến “satisfaction_score” có biến Na, nhóm em sử dụng thao tác (18), (19) để tính Mode và thay thế giá trị NA bằng giá trị Mode vừa tìm. Tuy nhiên, thực tế bộ dữ liệu không có dữ liệu thiếu nên số lượng giá trị NA bị thay thế là 0.

Chuyển biến ‘education’ thành Ordered Factor theo thứ tự học vấn

# (20) Chuyển biến 'education' thành Ordered Factor theo thứ tự học vấn
data <- data %>%
  mutate(
    education = factor(
      education,
      levels = c("HighSchool", "Bachelor", "Masters", "College"),
      ordered = TRUE
    )
  )

# Xác nhận kết quả
cat("Thứ tự levels của biến education:", levels(data$education), "\n")
Thứ tự levels của biến education: HighSchool Bachelor Masters College 
cat("Kiểu dữ liệu sau khi chuyển:", class(data$education), "\n")
Kiểu dữ liệu sau khi chuyển: ordered factor 
  • Nhóm chuyển kiểu dữ liệu education thành dạng ordered factor.

Lọc dữ liệu phụ

# Thao tác 21: Lọc dữ liệu phụ
promoted_users <- data %>%
  filter(promotion_usage == 1)

# Kiểm tra kết quả (xem kích thước của promoted_users)
 dim(promoted_users)
[1] 30080    13
  • Để lọc dữ liệu nhóm khách hàng có sử dụng khuyến mãi không nhóm sử dụng thao tác thứ (21) là lệnh fillter.

Tạo biến phân loại phức tạp: service_tier (VIP / Standard)

# (22) Tạo biến phân loại: service_tier (VIP / Standard)
data1 <- data1 %>%
  mutate(
    service_tier = case_when(
      total_spent >= 10000 & satisfaction_score >= 7 ~ "VIP",
      TRUE ~ "Standard"
    )
  )

# Hiển thị kết quả tổng hợp
cat("Phân loại khách hàng theo service_tier:\n")
Phân loại khách hàng theo service_tier:
count(data1, service_tier) %>%
  rename("Hạng dịch vụ" = service_tier,
         "Số lượng khách hàng" = n) %>%
  knitr::kable(align = "lc", caption = "Bảng phân loại khách hàng theo hạng dịch vụ")
Bảng phân loại khách hàng theo hạng dịch vụ
Hạng dịch vụ Số lượng khách hàng
Standard 96822
VIP 3178
  • Ở thao tác thứ 22 (case_when), mục đích phân loại dịch vụ khách hàng. Trong đó, khách hàng có mức chi tiêu > 10000 sẽ thuộc tệp khách “Vip”, ngược lại thì thuộc nhóm “standard”.

Bảng mô tả các biến

# 2# 1. Tạo bảng mô tả
variable_description <- data.frame(
  Biến = c("id", "age", "gender", "income", "education", "region",
           "loyalty_status", "purchase_frequency", "purchase_amount", 
           "product_category", "promotion_usage", "satisfaction_score"),
  Ý_nghĩa = c(
    "Mã định danh khách hàng",
    "Tuổi của khách hàng",
    "Giới tính của khách hàng (Nam/Nữ)",
    "Thu nhập hằng năm (USD)",
    "Trình độ học vấn (HighSchool/College/Bachelor/Masters)",
    "Khu vực sinh sống",
    "Mức độ trung thành (Bronze/Silver/Gold/Platinum)",
    "Tần suất mua hàng trong kỳ",
    "Số tiền chi tiêu trong một lần mua (USD)",
    "Loại sản phẩm đã mua",
    "Có sử dụng khuyến mãi hay không (Yes/No)",
    "Mức độ hài lòng (1–10)"
  ),
  Kiểu_dữ_liệu = c(
    "integer", "integer", "character", "integer", "character", "character",
    "character", "character", "integer", "character", "integer", "integer"
  ),
  Loại_biến = c(
    "Định danh", "Định lượng", "Định tính", "Định lượng", "Định tính",
    "Định tính", "Định tính", "Định tính", "Định lượng", "Định tính",
    "Định lượng", "Định lượng"
  )
)

# 2. Hiển thị bảng đẹp, giữ nguyên dấu tiếng Việt
variable_description %>%
  kbl(
    col.names = c("Biến", "Ý nghĩa", "Kiểu dữ liệu", "Loại biến"),
    caption = "Bảng 1. Mô tả các biến trong bộ dữ liệu",
    booktabs = TRUE,
    escape = FALSE # giữ nguyên tiếng Việt, không thêm dấu chấm
  ) %>%
  kable_styling(
    full_width = FALSE,
    position = "center",
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    font_size = 13,
    fixed_thead = TRUE
  ) %>%
  column_spec(1, bold = TRUE, color = "white", background = "#4F81BD") %>%
  column_spec(2, width = "10cm") %>%
  column_spec(3, width = "2.5cm", background = "#EAF2F8") %>%
  column_spec(4, width = "2.5cm", background = "#F9EBEA") %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86C1") %>%
  add_header_above(c(" " = 1, "Thông tin mô tả" = 3),
                   bold = TRUE, background = "#21618C", color = "white") %>%
  kable_paper("hover", full_width = FALSE)
Bảng 1. Mô tả các biến trong bộ dữ liệu
Thông tin mô tả
Biến Ý nghĩa Kiểu dữ liệu Loại biến
id Mã định danh khách hàng integer Định danh
age Tuổi của khách hàng integer Định lượng
gender Giới tính của khách hàng (Nam/Nữ) character Định tính
income Thu nhập hằng năm (USD) integer Định lượng
education Trình độ học vấn (HighSchool/College/Bachelor/Masters) character Định tính
region Khu vực sinh sống character Định tính
loyalty_status Mức độ trung thành (Bronze/Silver/Gold/Platinum) character Định tính
purchase_frequency Tần suất mua hàng trong kỳ character Định tính
purchase_amount Số tiền chi tiêu trong một lần mua (USD) integer Định lượng
product_category Loại sản phẩm đã mua character Định tính
promotion_usage Có sử dụng khuyến mãi hay không (Yes/No) integer Định lượng
satisfaction_score Mức độ hài lòng (1–10) integer Định lượng

1.2 Phân tích thống kê mô tả và trực quan hóa

1.2.1 Giới thiệu chung

Ba biến được tập trung phân tích trong chương này:

  • gender: giới tính khách hàng.
  • education: trình độ học vấn.
  • purchase_amount: mức độ chi tiêu.

Biến định lượng purchase_amount

# Tóm tắt phân phối biến định lượng purchase_amount

summary_purchase_amount <- data %>%
  summarise(
    Min     = min(purchase_amount, na.rm = TRUE),
    Q1      = quantile(purchase_amount, 0.25, na.rm = TRUE),
    Median  = median(purchase_amount, na.rm = TRUE),
    Mean    = mean(purchase_amount, na.rm = TRUE),
    Q3      = quantile(purchase_amount, 0.75, na.rm = TRUE),
    Max     = max(purchase_amount, na.rm = TRUE),
    SD      = sd(purchase_amount, na.rm = TRUE)
  )
summary_purchase_amount %>%
  kable(
    caption = "Bảng 1. Kết quả thống kê mô tả cho biến purchase_amount",
    align = "c"
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    position = "center",
    full_width = FALSE
  )
Bảng 1. Kết quả thống kê mô tả cho biến purchase_amount
Min Q1 Median Mean Q3 Max SD
1118 5583 9452 9634.791 13350 26204 4799.339
  • Đoạn code trên sử dụng hàm summarise() trong R để tính các chỉ tiêu thống kê mô tả cho biến purchase_amount, bao gồm giá trị nhỏ nhất (Min), các tứ phân vị (Q1, Median, Q3), giá trị trung bình (Mean), lớn nhất (Max) và độ lệch chuẩn (SD).
  • Kết quả cho thấy giá trị mua hàng dao động từ 1.118 đến 26.204, với mức trung bình khoảng 9.635 và trung vị là 9.452, cho thấy phân phối tương đối cân bằng. Khoảng giữa các tứ phân vị (Q1 = 5.583, Q3 = 13.350) thể hiện phần lớn khách hàng có giá trị mua hàng trong khoảng này. Độ lệch chuẩn 4.799,339 cho thấy mức độ phân tán vừa phải quanh giá trị trung bình.

Phân loại mức độ chi tiêu

# Phân loại mức độ chi tiêu
data$spending_level <- cut(
  data$purchase_amount,
  breaks = c(-Inf, 5583, 13350, Inf),
  labels = c("Low", "Medium", "High"),
  right = TRUE
)

# Kiểm tra kết quả
table(data$spending_level)

   Low Medium   High 
 25002  49999  24999 
prop.table(table(data$spending_level))

    Low  Medium    High 
0.25002 0.49999 0.24999 
  • Để phân loại mức độ chi tiêu, nhóm chúng em sử dụng hàm cut() trong R để chia biến purchase_amount thành ba nhóm dựa trên các ngưỡng giá trị tứ phân vị giúp xác định và đếm số lượng khách hàng trong từng nhóm chi tiêu.
  • Kết quả thu được: Low: 25.002 (25%), Medium: 49.999 (50%), High: 24.999 (25%). Điều này cho thấy phần lớn khách hàng thuộc nhóm chi tiêu trung bình, còn hai nhóm còn lại có quy mô tương đương nhau, phản ánh sự phân bổ tương đối cân đối giữa các mức chi tiêu.

Biểu đồ 1. Phân bổ mức độ chi tiêu

# Tạo bảng tóm tắt tần suất và tần số
freq_table <- data %>%
  group_by(spending_level) %>%
  summarise(
    count = n()
  ) %>%
  mutate(
    percent = count / sum(count) * 100
  )

# Vẽ biểu đồ kết hợp
ggplot(freq_table, aes(x = spending_level)) +
  geom_col(aes(y = count, fill = spending_level), alpha = 0.7) +
  geom_line(aes(y = percent * max(count) / 100, group = 1), color = "red", size = 1.2) +
  geom_point(aes(y = percent * max(count) / 100), color = "red", size = 3) +
  scale_y_continuous(
    name = "Số lượng khách hàng",
    sec.axis = sec_axis(~ . / max(freq_table$count) * 100, name = "Tỷ lệ (%)")
  ) +
  labs(
    title = "Biểu đồ tần suất và tần số của mức độ chi tiêu",
    x = "Nhóm chi tiêu",
    fill = "Mức chi tiêu"
  ) +
  theme_minimal(base_size = 13)
  • Đoạn code trên tạo bảng tóm tắt tần suất và tần số của biến spending_level bằng lệnh group_by() và summarise() rồi vẽ biểu đồ kết hợp giữa cột và đường bằng ggplot().
  • Quan sát biểu đồ cho thấy nhóm khách hàng có mức chi tiêu trung bình (Medium) chiếm tỷ trọng lớn nhất, với số lượng khoảng 50.000 khách hàng, tương ứng 50% tổng mẫu. Trong khi đó, hai nhóm Low và High có quy mô tương đương nhau, mỗi nhóm chiếm khoảng 25% và có số lượng gần 25.000 khách hàng.

Bảng tần số biến gender

# Thao tác: Tính tỷ lệ phần trăm phân phối của biến gender

gender_distribution <- data %>%
  count(gender) %>%
  mutate(Percent = round(100 * n / sum(n), 1))

gender_distribution %>%
  kable(
    caption = "Bảng 2. Phân phối giới tính của khách hàng",
    align = "c",
    col.names = c("Giới tính", "Số lượng", "Tỷ lệ (%)")
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    position = "center",
    full_width = FALSE
  )
Bảng 2. Phân phối giới tính của khách hàng
Giới tính Số lượng Tỷ lệ (%)
Female 50074 50.1
Male 49926 49.9
  • Đoạn code trên sử dụng hàm count() để đếm số lượng khách hàng theo giới tính (gender), sau đó dùng mutate() để tính thêm cột tỷ lệ phần trăm (%).
  • Kết quả trong Bảng 2 cho thấy phân bố giới tính của khách hàng khá cân bằng, với nữ chiếm 50,1% (50.074 người) và nam chiếm 49,9% (49.926 người). Sự chênh lệch này rất nhỏ, chứng tỏ dữ liệu có sự phân bổ giới tính gần như đồng đều giữa hai nhóm.

Biểu đồ 2. Biểu đồ thể hiện tỷ trọng giới tính khách hàng

gender_long <- gender_distribution %>%
  tidyr::pivot_longer(cols = c(n, Percent), names_to = "type", values_to = "value")

ggplot(gender_long, aes(x = gender, y = value, fill = gender)) +
  geom_col(width = 0.6) +
  facet_wrap(~type, scales = "free_y", ncol = 1) +
  labs(
    title = "Biểu đồ phân phối số lượng và phần trăm giới tính của khách hàng",
    x = "Giới tính", y = ""
  ) +
  theme_minimal(base_size = 13)
  • Đoạn code trên chuyển đổi bảng gender_distribution từ dạng rộng sang dạng dài bằng hàm pivot_longer() để thuận tiện cho việc vẽ biểu đồ số lượng và tỷ lệ trong cùng một biểu đồ. Sau đó, hàm ggplot() được sử dụng để vẽ biểu đồ cột thể hiện phân phối giới tính của khách hàng theo hai khía cạnh: số lượng tuyệt đối (n) và tỷ lệ phần trăm (Percent). Lệnh facet_wrap() chia biểu đồ thành hai phần riêng biệt, giúp so sánh trực quan hơn

Bảng tần số biến education

education_distribution <- data %>%
  mutate(
    education = as.character(education),
    education = factor(education, levels = c("HighSchool", "Bachelor", "Masters", "College"))
  ) %>%
  count(education, name = "Số_lượng") %>%
  mutate(
    `Tỷ_lệ (%)` = round(100 * Số_lượng / sum(Số_lượng), 1)
  )

education_distribution %>%
  rename(`Trình độ học vấn` = education) %>%
  kable(
    caption = "Bảng 3. Phân phối trình độ học vấn của khách hàng",
    align = "c",
    col.names = c("Trình độ học vấn", "Số lượng", "Tỷ lệ (%)")
  ) %>%
  kable_styling(
    latex_options = c("striped", "hold_position"),
    position = "center",
    full_width = FALSE
  )
Bảng 3. Phân phối trình độ học vấn của khách hàng
Trình độ học vấn Số lượng Tỷ lệ (%)
HighSchool 20031 20.0
Bachelor 30279 30.3
Masters 9816 9.8
College 39874 39.9
  • Đoạn code trên sử dụng các hàm của dplyr để xử lý và mô tả biến education. Cụ thể, bước mutate() chuyển giá trị NA hoặc ô trống thành “Unknown” nhằm đảm bảo không bị mất dữ liệu, đồng thời chuyển biến education thành dạng nhân tố với các mức theo thứ tự: HighSchool, Bachelor, Masters và Unknown. Sau đó, count() được dùng để đếm số lượng khách hàng ở từng trình độ học vấn, và mutate() tiếp tục tính tỷ lệ phần trăm (%) tương ứng.
  • Kết quả cho thấy nhóm khách hàng có trình độ Bachelor chiếm tỷ lệ lớn nhất với 30,3% (30.279 người), tiếp đến là nhóm HighSchool chiếm 20,0% (20.031 người) và nhóm Masters chiếm 9,8% (9.816 người). Nhóm College chiếm khoảng 39,9% (39.874 người).

Biểu đồ 3. Biểu đồ thể hiện tỷ lệ khách hàng theo trình độ học vấn

library(ggplot2)

ggplot(education_distribution, aes(x = education, y = Số_lượng, fill = education)) +
  geom_col(alpha = 0.8, width = 0.6) +
  geom_text(aes(label = Số_lượng), vjust = -0.1) +
  labs(x = "Trình độ học vấn", y = "Số lượng", fill = "Education") +
  theme_minimal()

1.2.2 Phân tích đa biến

Bảng tần số giới tính theo từng trình độ học vấn

gender_edu_freq <- data %>%
  mutate(
    education = factor(education, levels = c("HighSchool", "Bachelor", "Masters", "College"))
  ) %>%
  group_by(education, gender) %>%
  summarise(Count = n(), .groups = "drop") %>%
  group_by(education) %>%
  mutate(
    Total = sum(Count),
    Percent = round(100 * Count / Total, 1)
  ) %>%
  ungroup()

# Chuyển sang dạng wide - hiển thị Số lượng
gender_edu_wide_count <- gender_edu_freq %>%
  select(education, gender, Count) %>%
  pivot_wider(
    names_from = education,
    values_from = Count,
    values_fill = 0
  )
kable(
  gender_edu_wide_count,
  caption = "Số lượng giới tính theo từng trình độ học vấn",
  align = "c"
) %>%
  kable_styling(full_width = FALSE, position = "center", bootstrap_options = c("striped", "hover"))
Số lượng giới tính theo từng trình độ học vấn
gender HighSchool Bachelor Masters College
Female 10034 15064 4945 20031
Male 9997 15215 4871 19843
# Chuyển sang dạng wide - hiển thị Tỷ lệ (%)
gender_edu_wide_percent <- gender_edu_freq %>%
  select(education, gender, Percent) %>%
  pivot_wider(
    names_from = education,
    values_from = Percent,
    values_fill = 0
  )
kable(
  gender_edu_wide_percent,
  caption = "Tỷ lệ (%) giới tính theo từng trình độ học vấn",
  align = "c"
) %>%
  kable_styling(full_width = FALSE, position = "center", bootstrap_options = c("striped", "hover"))
Tỷ lệ (%) giới tính theo từng trình độ học vấn
gender HighSchool Bachelor Masters College
Female 50.1 49.8 50.4 50.2
Male 49.9 50.2 49.6 49.8
  • Tương tự các tính toán trên, nhóm sử dụng select() chọn các cột cần thiết (education, gender, Count), sau đó pivot_wider() biến các giá trị của cột education thành cột mới, điền giá trị tương ứng từ cột Count. Tham số values_fill = 0 đảm bảo rằng nếu một giới tính không có trong một trình độ nào, ô sẽ được điền bằng 0 thay vì NA.

So sánh tỷ lệ giới tính theo từng trình độ học vấn

# ===   So sánh tỷ lệ giới tính theo từng trình độ học vấn ===
edu_gender_compare <- data.frame(
  Education = c("Bachelor", "HighSchool", "Masters", "College"),
  Female = c(49.8, 50.1, 50.4, 50.2),
  Male = c(50.2, 49.9, 49.6, 49.8),
  Difference = c(-0.4, 0.2, 0.8, 0.4)
)

# Thêm cột diễn giải với mũi tên
edu_gender_compare <- edu_gender_compare %>%
  mutate(
    Interpretation = case_when(
      Difference > 0 ~ "↑ Nữ chiếm ưu thế",
      Difference < 0 ~ "↑ Nam chiếm ưu thế",
      TRUE ~ "Cân bằng giới tính"
    )
  )

# In bảng đẹp hơn
kable(edu_gender_compare,
      caption = "So sánh tỷ lệ giới tính theo từng trình độ học vấn",
      align = "c",
      digits = 1,
      col.names = c("Trình độ học vấn", "Nữ (%)", "Nam (%)", "Chênh lệch", "Diễn giải")) %>%
  kable_styling(full_width = FALSE, position = "center", bootstrap_options = c("striped", "hover")) %>%
  column_spec(4, color = ifelse(edu_gender_compare$Difference > 0, "blue", "red")) %>%
  collapse_rows(columns = 1, valign = "middle")
So sánh tỷ lệ giới tính theo từng trình độ học vấn
Trình độ học vấn Nữ (%) Nam (%) Chênh lệch Diễn giải
Bachelor 49.8 50.2 -0.4 ↑ Nam chiếm ưu thế
HighSchool 50.1 49.9 0.2 ↑ Nữ chiếm ưu thế
Masters 50.4 49.6 0.8 ↑ Nữ chiếm ưu thế
College 50.2 49.8 0.4 ↑ Nữ chiếm ưu thế
  • Đoạn code trên tạo một bảng so sánh tỷ lệ giới tính theo từng trình độ học vấn. Sau đó, mutate() kết hợp với case_when() thêm cột Interpretation để diễn giải sự chênh lệch: nếu Difference > 0 thì “↑ Nữ chiếm ưu thế”, nếu < 0 thì “↑ Nam chiếm ưu thế”, bằng 0 thì “Cân bằng giới tính”.
  • Diễn giải kết quả:
    • Ở trình độ Bachelor, nam chiếm ưu thế nhẹ (50.2% so với 49.8%).
    • Ở các trình độ HighSchool, Masters, College, nữ chiếm ưu thế nhẹ, nhưng tỷ lệ chênh lệch đều dưới 1%, cho thấy sự phân bố giới tính gần như cân bằng trong tất cả các trình độ học vấn.

Biểu đồ 4. So sánh tỷ lệ giới tính theo trình độ học vấn

# 1️⃣ Chuyển định dạng dữ liệu
edu_gender_long <- edu_gender_compare %>%
  select(Education, Female, Male) %>%
  pivot_longer(cols = c(Female, Male),
               names_to = "Gender",
               values_to = "Percent")

# 2️⃣ Vẽ biểu đồ
ggplot(edu_gender_long, aes(x = Education, y = Percent, fill = Gender)) +
  # (Layer 1) Cột
  geom_bar(stat = "identity", position = position_dodge(width = 0.7), width = 0.6) +
  # (Layer 2) Nhãn phần trăm
  geom_text(aes(label = round(Percent, 1)),
            position = position_dodge(width = 0.7),
            vjust = -0.4, size = 4, family = "Times New Roman") +
  # (Layer 3) Đường trung bình 50%
  geom_hline(yintercept = 50, linetype = "dashed", color = "black", linewidth = 0.6) +
  # (Layer 4) Màu sắc và theme
  scale_fill_manual(values = c("Male" = "#1F77B4", "Female" = "#FF69B4")) +
  theme_minimal(base_family = "Times New Roman") +
  # (Layer 5) Tiêu đề, nhãn, giới hạn
  labs(
    title = "Biểu đồ so sánh tỷ lệ giới tính theo trình độ học vấn",
    x = "Trình độ học vấn",
    y = "Tỷ lệ (%)",
    fill = "Giới tính"
  ) +
  ylim(0, 55) +
  theme(
    plot.title = element_text(size = 13, face = "bold", hjust = 0.5),
    axis.text = element_text(size = 12),
    axis.title = element_text(size = 13, face = "bold"),
    legend.title = element_text(size = 12)
  )

Bảng tần suất nhóm chi tiêu theo giới tính

# =====  Bảng tần suất và tần suất (%) nhóm chi tiêu theo giới tính =====

# Tạo bảng chéo: tần số
table_freq <- table(data$gender, data$spending_level)

# Tính tần suất theo hàng (tỷ lệ % trong từng giới tính)
table_prop <- round(prop.table(table_freq, margin = 1) * 100, 1)

table_combined <- matrix(
  paste0(table_freq, " (", table_prop, "%)"),
  nrow = nrow(table_freq),
  dimnames = dimnames(table_freq)
)

# Đưa về dạng data frame để knit ra PDF đẹp hơn
spend_gender_table <- as.data.frame.matrix(table_combined)

# Hiển thị kết quả
cat("Bảng: Phân bố nhóm chi tiêu theo giới tính\n")
Bảng: Phân bố nhóm chi tiêu theo giới tính
spend_gender_table
                 Low        Medium          High
Female 12573 (25.1%) 25011 (49.9%) 12490 (24.9%)
Male   12429 (24.9%) 24988 (50.1%) 12509 (25.1%)
  • Đoạn code trên được sử dụng để xây dựng bảng tần suất và tần suất phần trăm của nhóm chi tiêu theo giới tính bằng các hàm sau: hàm table(); prop.table(); paste0(); Số lượng (Tỷ lệ%); as.data.frame.matrix().
  • Kết quả thu được cho thấy phân bố nhóm chi tiêu giữa nam và nữ khá cân bằng. Nhóm chi tiêu Medium chiếm tỷ lệ lớn nhất, khoảng 50% ở cả hai giới tính, trong khi các mức Low và High chiếm tỷ lệ tương đương, khoảng 25%. Điều này phản ánh rằng trong mẫu nghiên cứu, sự khác biệt về chi tiêu giữa nam và nữ là rất nhỏ, và phần lớn khách hàng tập trung ở mức chi tiêu trung bình.

Biểu đồ 5. Phân bố tần số và tỷ lệ khách hàng theo nhóm chi tiêu và giới tính

# Tạo bảng chéo tần số và tần suất (%)
table_freq <- table(data$gender, data$spending_level)
table_prop <- round(prop.table(table_freq, margin = 1) * 100, 1)

# Chuyển thành data frame
df_combined <- as.data.frame(table_freq)
colnames(df_combined) <- c("Gender", "spending_level", "Frequency")

df_prop <- as.data.frame(table_prop)
colnames(df_prop) <- c("Gender", "spending_level", "Percent")

# Gộp 2 bảng
df_plot <- merge(df_combined, df_prop, by = c("Gender", "spending_level"))
# === Vẽ biểu đồ kết hợp ===
ggplot(df_plot, aes(x = spending_level, group = Gender)) +
  # Cột: tần số (số lượng khách hàng)
  geom_bar(aes(y = Frequency, fill = Gender),
           stat = "identity", position = position_dodge(width = 0.8), width = 0.6) +
  geom_line(aes(y = Percent * max(Frequency) / 100, color = Gender),
            size = 1.1, position = position_dodge(width = 0.8)) +
  geom_point(aes(y = Percent * max(Frequency) / 100, color = Gender),
             size = 3, position = position_dodge(width = 0.8)) +
  geom_text(aes(y = Frequency, label = Frequency),
            position = position_dodge(width = 0.8), vjust = -0.6, size = 3.5) +
  geom_text(aes(y = Percent * max(Frequency) / 100, label = paste0(Percent, "%")),
            position = position_dodge(width = 0.8), vjust = -1.2, size = 3.3, color = "black") +
  # Màu sắc
  scale_fill_brewer(palette = "Set2") +
  scale_color_brewer(palette = "Dark2") +
  # Trục tung phụ cho tỷ lệ %
  scale_y_continuous(
    name = "Số lượng khách hàng",
    sec.axis = sec_axis(~ . / max(df_plot$Frequency) * 100,
                        name = "Tỷ lệ (%)")
  ) +
  labs(
    title = "Phân bố khách hàng theo nhóm chi tiêu và giới tính",
    x = "Nhóm mức độ chi tiêu",
    fill = "Giới tính",
    color = "Giới tính"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.title.y.right = element_text(color = "gray30"),
    axis.title.y.left = element_text(color = "gray30"),
    legend.position = "top"
  )
  • Đoạn code trên được sử dụng để chuẩn bị dữ liệu cho biểu đồ kết hợp tần số và tỷ lệ phần trăm nhóm chi tiêu theo giới tính. Cụ thể, trước hết, table() và prop.table() tạo bảng tần số (Frequency) và tần suất phần trăm (Percent) theo từng giới tính trong mỗi nhóm chi tiêu. Sau đó, hai bảng này được chuyển sang data frame riêng (df_combined cho số lượng, df_prop cho tỷ lệ %). Cuối cùng, merge() kết hợp hai bảng theo các cột chung Gender và spending_level, tạo ra bảng df_plot để dùng trong ggplot.

Thống kê mô tả giới mức chi tiêu của nữ

# Thống kê mô tả mức chi tiêu của nữ
# 1) Lọc dữ liệu chỉ lấy nữ
female_data <- subset(data, gender == "Female")
# 2) Chọn biến purchase_amount
female_purchase <- female_data$purchase_amount
# 3) Thống kê mô tả cơ bản
female_summary <- data.frame(
  Count = length(female_purchase),
  Mean = mean(female_purchase, na.rm=TRUE),
  Median = median(female_purchase, na.rm=TRUE),
  SD = sd(female_purchase, na.rm=TRUE),
  Variance = var(female_purchase, na.rm=TRUE),
  Min = min(female_purchase, na.rm=TRUE),
  Max = max(female_purchase, na.rm=TRUE),
  Q1 = quantile(female_purchase, 0.25, na.rm=TRUE),
  Q3 = quantile(female_purchase, 0.75, na.rm=TRUE)
)
female_summary
    Count     Mean Median      SD Variance  Min   Max      Q1       Q3
25% 50074 9634.405   9449 4798.81 23028579 1146 25406 5569.25 13345.75
  • Để tính toán, nhóm chúng em đã lọc dữ liệu chỉ lấy nữ, chọn biến purchase_amount, rồi tính các chỉ số thống kê cơ bản như Count, Mean, Median, SD, Variance, Min, Max, Q1, Q3.
  • Kết quả:
    • Trung bình mức chi tiêu của nữ là khoảng 9.634, với độ lệch chuẩn gần 4.799, cho thấy có sự phân tán khá lớn trong chi tiêu.
    • Giá trị trung vị 9.449 gần bằng trung bình, phản ánh phân bố tương đối cân bằng nhưng có sự xuất hiện của các giá trị chi tiêu cao kéo dài (Max = 25.406).
    • Khoảng tứ phân từ Q1 = 5.569 đến Q3 = 13.346 cho thấy 50% nữ có mức chi tiêu nằm trong khoảng này.

Thống kê mô tả giới mức chi tiêu của nam

# Thống kê mô tả giới mức chi tiêu của nam
# Lọc dữ liệu nam
male_data <- subset(data, gender == "Male")
#  Chọn biến purchase_amount
male_purchase <- male_data$purchase_amount
# 3) Thống kê mô tả cơ bản
male_summary <- data.frame(
  Count = length(male_purchase),
  Mean = mean(male_purchase, na.rm=TRUE),
  Median = median(male_purchase, na.rm=TRUE),
  SD = sd(male_purchase, na.rm=TRUE),
  Variance = var(male_purchase, na.rm=TRUE),
  Min = min(male_purchase, na.rm=TRUE),
  Max = max(male_purchase, na.rm=TRUE),
  Q1 = quantile(male_purchase, 0.25, na.rm=TRUE),
  Q3 = quantile(male_purchase, 0.75, na.rm=TRUE)
)
male_summary
    Count     Mean Median       SD Variance  Min   Max   Q1    Q3
25% 49926 9635.178   9458 4799.918 23039216 1118 26204 5600 13356
  • Tương tự ở phần trên, nhóm sử dụng và tính toán các chỉ số thống kê mô tả để phần tích mức chi tiêu ở nam giới. Trong đó:
    • Trung bình mức chi tiêu của nam là khoảng 9.635, gần bằng nữ, với độ lệch chuẩn 4.800, cho thấy sự phân tán tương tự như nữ.
    • Giá trị trung vị 9.458 gần bằng trung bình, phản ánh phân bố chi tiêu cân bằng, nhưng có xuất hiện các giá trị cực đoan (Max = 26.204).
    • Khoảng tứ phân từ Q1 = 5.600 đến Q3 = 13.356 cho thấy 50% nam có mức chi tiêu nằm trong khoảng này.
  • So sánh với nữ: phân bố chi tiêu giữa nam và nữ rất tương đồng, cả về trung bình, trung vị và độ biến thiên.

Biểu đồ 6 và 7. So sánh chi tiêu từng giới tính theo giá trị Mean ± SD và Tứ phân vị

library(ggplot2)
library(patchwork)
gender_spending_summary <- data %>%
  group_by(gender) %>%
  summarise(
    Mean = mean(purchase_amount, na.rm = TRUE),
    SD = sd(purchase_amount, na.rm = TRUE),
    .groups = "drop"
  )
# Biểu đồ Mean ± SD
b1 <- ggplot(gender_spending_summary, aes(x = gender, y = Mean, fill = gender)) +
  geom_col(width = 0.5, alpha = 0.8, color = "black") +
  geom_errorbar(aes(ymin = Mean - SD, ymax = Mean + SD), width = 0.2, size = 1) +
  geom_text(aes(label = round(Mean, 1)), vjust = -0.8, size = 2) +
  scale_fill_manual(values = c("Male" = "#4B9CD3", "Female" = "#E69F00")) +
  labs(
    title = "Biểu đồ 6. Mean+SD",
    x = "Giới tính",
    y = "Chi tiêu trung bình (USD)"
  ) +
  theme_minimal(base_size = 10) +  # Giảm base_size
  theme(
    plot.title = element_text(face = "bold", size = 12, hjust = 0.4),
    plot.subtitle = element_text(hjust = 0.5),
    legend.position = "none"
  )

# Biểu đồ tứ phân vị
b2 <- ggplot(data, aes(x = gender, y = purchase_amount, fill = gender)) +
  geom_boxplot(width = 0.4, outlier.color = "black", color = "black", alpha = 0.7) +
  stat_summary(fun = "mean", geom = "point", shape = 18, color = "red", size = 2) +
  scale_fill_manual(values = c("Male" = "#4B9CD3", "Female" = "#E69F00")) +
  labs(
    title = "Biểu đồ 7. Tứ phân vị",
    x = "Giới tính",
    y = "Chi tiêu (USD)"
  ) +
  theme_minimal(base_size = 10) +  # Giảm base_size
  theme(
    plot.title = element_text(face = "bold", size = 12, hjust = 0.4),
    plot.subtitle = element_text(hjust = 0.4),
    legend.position = "none"
  )

# Ghép 2 biểu đồ
(b1 | b2) + plot_layout(ncol = 2) +
  plot_annotation(
    title = "So sánh chi tiêu từng giới tính theo giá trị Mean ± SD và Tứ phân vị",
    theme = theme(plot.title = element_text(size = 10, face = "bold"))  # giảm size tổng thể
  )
  • Biểu đồ b1 sử dụng ggplot() + geom_col() + geom_errorbar() + geom_text() để thể hiện chi tiêu trung bình của từng giới tính (Mean) cùng độ biến thiên (SD), với giá trị trung bình hiển thị trên cột.
  • Biểu đồ b2 sử dụng ggplot() + geom_boxplot() + stat_summary() để thể hiện phân bố chi tiêu theo tứ phân vị, đồng thời đánh dấu trung bình bằng chấm đỏ. Hai biểu đồ giúp so sánh chi tiêu giữa nam và nữ về cả trung bình và phân bố.
  • Kết quả: mức chi tiêu trung bình cả hai giới tính khá tương đồng nhau.

Kiểm định t-test mức độ chi tiêu giữa các nhóm giới tính

# ==== CỤM 8: KIỂM ĐỊNH SỰ KHÁC BIỆT CHI TIÊU GIỮA NAM VÀ NỮ ====

# --- 1. Kiểm định t-test hai nhóm ---
# Giả thuyết:
#   H0: Không có sự khác biệt về chi tiêu giữa Nam và Nữ
#   H1: Có sự khác biệt về chi tiêu giữa Nam và Nữ

t_test_result <- t.test(purchase_amount ~ gender, data = data, var.equal = FALSE)

# Hiển thị kết quả tóm tắt
t_test_summary <- data.frame(
  Thống_kê_t = round(t_test_result$statistic, 3),
  bậc_tự_do = round(t_test_result$parameter, 2),
  Giá_trị_p = round(t_test_result$p.value, 4),
  Trung_bình_Nam = round(mean(data$purchase_amount[data$gender == "Male"], na.rm = TRUE), 2),
  Trung_bình_Nữ = round(mean(data$purchase_amount[data$gender == "Female"], na.rm = TRUE), 2)
)

kable(
  t_test_summary,
  caption = "Bảng. Kết quả kiểm định t-test sự khác biệt chi tiêu giữa Nam và Nữ",
  align = "c"
) %>%
  kable_styling(full_width = FALSE, position = "center", latex_options = c("striped", "hold_position"))
Bảng. Kết quả kiểm định t-test sự khác biệt chi tiêu giữa Nam và Nữ
Thống_kê_t bậc_tự_do Giá_trị_p Trung_bình_Nam Trung_bình_Nữ
t -0.025 99996.98 0.9797 9635.18 9634.4
# --- 2. Diễn giải kết quả ---
if (t_test_result$p.value < 0.05) {
  cat("Kết luận: Có sự khác biệt có ý nghĩa thống kê (p < 0.05) giữa mức chi tiêu trung bình của Nam và Nữ.\n")
} else {
  cat("Kết luận: Không có sự khác biệt có ý nghĩa thống kê giữa mức chi tiêu trung bình của Nam và Nữ.\n")
}
Kết luận: Không có sự khác biệt có ý nghĩa thống kê giữa mức chi tiêu trung bình của Nam và Nữ.
  • Kiểm định t-test bằng hàm t.test() để so sánh mức chi tiêu trung bình giữa nam và nữ. Kết quả đạt được:
    • Thống_kê_t = -0.025 → giá trị thống kê t, gần bằng 0, cho thấy sự khác biệt trung bình rất nhỏ.
    • bậc_tự_do = 99996.98 → số bậc tự do của kiểm định.
    • Giá_trị_p = 0.9797 → p-value lớn hơn 0.05, không có bằng chứng bác bỏ giả thuyết H0, nghĩa là trung bình chi tiêu giữa nam và nữ không khác biệt đáng kể.
    • Trung_bình_Nam = 9635.18, Trung_bình_Nữ = 9634.4 → mức chi tiêu trung bình gần như bằng nhau.

Biểu đồ 8. Phân bố mức chi tiêu theo giới tính

# --- 3. Biểu đồ Violin plot thể hiện sự khác biệt mức chi tiêu giữa Nam và Nữ ---
library(ggplot2)

ggplot(data, aes(x = gender, y = purchase_amount, fill = gender)) +
  geom_violin(trim = FALSE, alpha = 0.7, color = "black") +
  geom_boxplot(width = 0.15, fill = "white", color = "black", outlier.shape = NA) +
  stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "red") +
  labs(
    title = "Hình 8. Phân bố mức chi tiêu theo giới tính",
    x = "Giới tính",
    y = "Chi tiêu (VNĐ)"
  ) +
  theme_minimal(base_family = "vietfont") +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5, size = 14),
    legend.position = "none"
  )

Bảng thống kê mô tả mức chi tiêu theo từng trình độ học vấn

# 2. Tạo bảng thống kê mô tả
edu_summary <- data %>%
  group_by(education) %>%
  summarise(
    Count = n(),
    Mean = mean(purchase_amount, na.rm = TRUE),
    Median = median(purchase_amount, na.rm = TRUE),
    SD = sd(purchase_amount, na.rm = TRUE),
    Min = min(purchase_amount, na.rm = TRUE),
    Q1 = quantile(purchase_amount, 0.25, na.rm = TRUE),
    Q3 = quantile(purchase_amount, 0.75, na.rm = TRUE),
    Max = max(purchase_amount, na.rm = TRUE)
  ) %>%
  arrange(desc(Mean)) %>%
  mutate(across(where(is.numeric), ~ round(., 2)))

# 3. Hiển thị bảng đẹp
edu_summary %>%
  kbl(
    caption = "Bảng 2. Thống kê mô tả mức chi tiêu theo trình độ học vấn",
    col.names = c("Trình độ học vấn", "Số KH", "Trung bình", "Trung vị", 
                  "Độ lệch chuẩn", "Thấp nhất", "Q1", "Q3", "Cao nhất"),
    booktabs = TRUE
  ) %>%
  kable_styling(
    full_width = FALSE,
    position = "center",
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    font_size = 13
  ) %>%
  column_spec(1, bold = TRUE, color = "white", background = "#4F81BD") %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86C1") %>%
  kable_paper("hover", full_width = FALSE)
Bảng 2. Thống kê mô tả mức chi tiêu theo trình độ học vấn
Trình độ học vấn Số KH Trung bình Trung vị Độ lệch chuẩn Thấp nhất Q1 Q3 Cao nhất
Bachelor 30279 9656.73 9489 4786.43 1146 5626.00 13354.5 26204
College 39874 9647.31 9470 4815.00 1118 5587.00 13377.0 25406
Masters 9816 9614.40 9450 4773.95 1304 5532.25 13365.0 23708
HighSchool 20031 9586.71 9368 4800.01 1233 5539.50 13290.0 24408
  • Đoạn code trên sử dụng dplyr để tạo bảng thống kê mô tả mức chi tiêu theo từng trình độ học vấn. Cụ thể:
    • group_by(education) gom dữ liệu theo từng trình độ học vấn.
    • summarise() tính các chỉ số cơ bản: số khách hàng (Count).
    • trung bình (Mean), trung vị (Median), độ lệch chuẩn (SD), giá trị thấp nhất (Min), tứ phân vị thứ nhất và ba (Q1, Q3) và giá trị cao nhất (Max).
    • arrange(desc(Mean)) sắp xếp bảng theo giá trị trung bình giảm dần.
    • mutate(across(…, round(.,2))) làm tròn các giá trị số để bảng dễ đọc.
  • Diễn giải kết quả: Nhìn chung, mức chi tiêu trung bình giữa các trình độ học vấn không chênh lệch nhiều (từ khoảng 9.587 đến 9.657).
    • Độ lệch chuẩn từ 4.77–4.82 cho thấy sự phân tán chi tiêu tương đối đồng đều giữa các nhóm.
    • Khoảng tứ phân từ Q1 đến Q3 và giá trị Min–Max cho thấy 50% khách hàng chủ yếu chi tiêu trong khoảng 5.500–13.400, nhưng vẫn có một số giá trị cực đoan cao (Max >23000).

Biểu đồ 9, 10 và 11: So sánh mức chi tiêu theo trình độ học vấn

data2 <- data %>%
  mutate(
    hocvan_rutgon = case_when(
      education %in% c("High School", "HighSchool") ~ "HS",
      education == "College" ~ "CL",
      education == "Bachelor" ~ "BC",
      education %in% c("Master", "Masters") ~ "MT",
      TRUE ~ NA_character_
    )
  )

# 1️⃣ Boxplot
p1 <- ggplot(data2, aes(x = hocvan_rutgon, y = purchase_amount)) +
  geom_boxplot(fill = "gray90", color = "black", width = 0.6) +
  stat_summary(fun = mean, geom = "point", shape = 21, fill = "black", size = 2) +
  labs(title = "Boxplot", x = "Trình độ học vấn", y = "Chi tiêu") +
  theme_minimal(base_size = 10) +
  theme(plot.title = element_text(size = 11, face = "bold"))

# 2️⃣ Violin (log-scale)
p2 <- ggplot(data2, aes(x = hocvan_rutgon, y = purchase_amount)) +
  geom_violin(fill = "gray85", color = "black", trim = FALSE, width = 0.8) +
  stat_summary(fun = mean, geom = "point", shape = 21, fill = "black", size = 2) +
  scale_y_continuous(trans = "log10") +
  labs(title = "Violin (log-scale)", x = "Trình độ học vấn", y = "Chi tiêu (log10)") +
  theme_minimal(base_size = 10) +
  theme(plot.title = element_text(size = 11, face = "bold"))

# 3️⃣ Mean ± SD
data_summary <- data2 %>%
  group_by(hocvan_rutgon) %>%
  summarise(
    mean_spent = mean(purchase_amount, na.rm = TRUE),
    sd_spent = sd(purchase_amount, na.rm = TRUE),
    .groups = "drop"
  )

p3 <- ggplot(data_summary, aes(x = hocvan_rutgon, y = mean_spent)) +
  geom_point(size = 2, color = "black") +
  geom_errorbar(aes(ymin = mean_spent - sd_spent, ymax = mean_spent + sd_spent),
                width = 0.2, color = "black") +
  geom_line(aes(group = 1), color = "gray40", linetype = "dashed") +
  labs(title = "Trung bình ± SD", x = "Trình độ học vấn", y = "Chi tiêu trung bình") +
  theme_minimal(base_size = 10) +
  theme(plot.title = element_text(size = 11, face = "bold"))

# 4️⃣ Ghép 3 biểu đồ
combined_plot <- (p1 | p2 | p3) +
  plot_annotation(
    title = "Biểu đồ 13. So sánh mức chi tiêu theo trình độ học vấn",
    subtitle = "Boxplot – Violin (log-scale) – Trung bình ± SD",
    caption = "Chú thích: HS = High School; CL = College; BC = Bachelor; MT = Master",
    theme = theme(
      plot.title = element_text(size = 12, face = "bold"),
      plot.subtitle = element_text(size = 10),
      plot.caption = element_text(size = 9, face = "italic")
    )
  )

combined_plot

Bảng tần số chéo

# 1. Tạo bảng tần số chéo
cross_table <- table(data1$education, data$spending_level)

# 2. Thêm hàng và cột tổng
cross_table_total <- addmargins(cross_table)

# 3. Chuyển sang dạng phần trăm hàng (để xem trong mỗi trình độ học vấn, tỷ lệ chi tiêu thế nào)
cross_table_rowpct <- prop.table(cross_table, 1) * 100
cross_table_rowpct <- round(cross_table_rowpct, 2)

# 4. Chuyển sang data frame cho đẹp
cross_df <- as.data.frame.matrix(cross_table_total)

# 5. Hiển thị bảng tần suất
kbl(
  cross_df,
  caption = "Bảng tần số chéo giữa trình độ học vấn và mức độ chi tiêu",
  booktabs = TRUE
) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86C1") %>%
  column_spec(1, bold = TRUE, color = "white", background = "#4F81BD")
Bảng tần số chéo giữa trình độ học vấn và mức độ chi tiêu
Low Medium High Sum
Bachelor 7491 15211 7577 30279
College 9958 19886 10030 39874
HighSchool 5067 10033 4931 20031
Masters 2486 4869 2461 9816
Sum 25002 49999 24999 100000
# 6. Nếu muốn in thêm bảng tỷ lệ hàng:
kbl(
  as.data.frame.matrix(cross_table_rowpct),
  caption = "Bảng 4. Tỷ lệ phần trăm chi tiêu theo trình độ học vấn (%)",
  booktabs = TRUE
) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#1F618D") %>%
  column_spec(1, bold = TRUE, color = "white", background = "#2471A3")
Bảng 4. Tỷ lệ phần trăm chi tiêu theo trình độ học vấn (%)
Low Medium High
Bachelor 24.74 50.24 25.02
College 24.97 49.87 25.15
HighSchool 25.30 50.09 24.62
Masters 25.33 49.60 25.07
  • Kết quả thống kê chung: Bảng tần số chéo và bảng tỷ lệ phần trăm cho thấy mức chi tiêu của khách hàng phân bố tương đối đồng đều giữa các trình độ học vấn. Nhóm College có quy mô lớn nhất (39.874 khách hàng), tiếp theo là Bachelor (30.279), HighSchool (20.031) và Masters (9.816). Trong tất cả các nhóm, số khách chi tiêu trung bình luôn chiếm khoảng 50%, trong khi chi tiêu thấp và cao chiếm khoảng 25% mỗi mức.
  • So sánh giữa nhóm Bachelor và HighSchool:
    • Số lượng khách chi tiêu trung bình ở Bachelor là 15.211, cao hơn 5.178 khách so với 10.033 ở HighSchool, tức Bachelor nhiều hơn 1,5 lần.
    • Tỷ lệ chi tiêu cao ở Bachelor là 25,02%, nhỉnh hơn 0,4 điểm phần trăm so với HighSchool (24,62%), tương đương cao hơn 1,02 lần.

Biểu đồ 13. Tổng hợp mức chi tiêu theo trình độ học vấn

merged_data <- data %>%
  mutate(spending_level = data$spending_level)

# Tính count và proportion (%)
spending_summary <- merged_data %>%
  group_by(education, spending_level) %>%
  summarise(count = n(),
            mean_spending = mean(purchase_amount, na.rm = TRUE),
            .groups = "drop") %>%
  group_by(education) %>%
  mutate(proportion = count / sum(count) * 100) %>%
  ungroup()

# Biểu đồ: cột = count, nhãn = %
ggplot(spending_summary, aes(x = spending_level, y = count, fill = education)) +
  geom_col(position = "dodge", alpha = 0.8) +
  geom_text(aes(label = paste0(round(proportion, 1), "%")),
            position = position_dodge(width = 0.8),
            vjust = -0.5, size = 2.5, color = "black") +
  labs(
    title = "Biểu đồ 13. Mức chi tiêu theo trình độ học vấn",
    subtitle = "Cột thể hiện số lượng khách hàng, nhãn hiển thị tỷ lệ (%)",
    x = "Nhóm chi tiêu",
    y = "Số lượng khách hàng",
    fill = "Trình độ học vấn"
  ) +
  theme_minimal(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold", size = 11, hjust = 0.5),
    plot.subtitle = element_text(size = 9, hjust = 0.5),
    axis.title = element_text(size = 9),
    axis.text = element_text(size = 8),
    legend.title = element_text(size = 9),
    legend.text = element_text(size = 8),
    axis.text.x = element_text(angle = 15, vjust = 0.8)
  )

So sánh mức chi tiêu trung bình theo học vấn (nhóm tham chiếu: Bachelor)

# Chọn nhóm tham chiếu
baseline <- "Bachelor"

# 1. Tính trung bình chi tiêu theo học vấn
edu_summary <- data %>%
  group_by(education) %>%
  summarise(mean_spending = mean(purchase_amount, na.rm = TRUE)) %>%
  ungroup()

# 2. Tính chênh lệch so với nhóm tham chiếu
baseline_value <- edu_summary$mean_spending[edu_summary$education == baseline]
edu_summary <- edu_summary %>%
  mutate(diff_vs_baseline = mean_spending - baseline_value)

# 3. In bảng kết quả
edu_summary %>%
  kbl(caption = paste0("Bảng 5. So sánh mức chi tiêu trung bình theo học vấn (tham chiếu: ", baseline, ")")) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86C1")
Bảng 5. So sánh mức chi tiêu trung bình theo học vấn (tham chiếu: Bachelor)
education mean_spending diff_vs_baseline
HighSchool 9586.709 -70.019164
Bachelor 9656.728 0.000000
Masters 9614.403 -42.324956
College 9647.306 -9.421351
  • Dùng group_by(education) + summarise(mean_spending = mean(…)) tính trung bình chi tiêu của từng nhóm học vấn. Sau đó,lấy giá trị trung bình của nhóm tham chiếu (baseline_value) và tính diff_vs_baseline = chênh lệch trung bình của các nhóm so với Bachelor.
  • Kết quả: Khi đối chiếu với nhóm Bachelor, nhóm HighSchool có chi tiêu trung bình thấp nhất, thấp hơn khoảng 70 USD, tức khoảng 0,7% so với nhóm tham chiếu. Nhóm Masters thấp hơn 42 USD (khoảng 0,4%), trong khi College gần như bằng với Bachelor, chỉ thấp hơn 9 USD (khoảng 0,09%). Như vậy, khách hàng có trình độ học vấn cao hơn hoặc tương đương với Bachelor có xu hướng chi tiêu nhỉnh hơn chút ít so với HighSchool, mặc dù sự khác biệt không đáng kể.

Biểu đồ 14. Mức độ chênh lệch chi tiêu trung bình các nhóm so với nhóm Bachelor

# 4. Biểu đồ thể hiện sự chênh lệch
ggplot(edu_summary, aes(x = reorder(education, diff_vs_baseline), y = diff_vs_baseline, fill = education)) +
  geom_col() +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  labs(
    title = paste0("Biểu đồ 14. Mức chênh lệch chi tiêu trung bình so với nhóm ", baseline),
    x = "Trình độ học vấn",
    y = "Chênh lệch chi tiêu (USD)"
  ) +
  theme_minimal(base_size = 9) + 
  theme(
    plot.title = element_text(face = "bold", size = 10, hjust = 0.5),
    axis.title = element_text(size = 8),
    axis.text = element_text(size = 8),
    legend.position = "none"
  )

Tỷ lệ chi tiêu cao, chi tiêu thấp và mức chênh lệch theo trình độ học vấn

# Tính toán và đổi tên cột rõ ràng tiếng Việt
bang_tyle_chitieu <- merged_data %>%
  group_by(education, spending_level) %>%
  summarise(Số_lượng = n(), .groups = "drop") %>%
  group_by(education) %>%
  mutate(Tổng = sum(Số_lượng),
         Tỷ_lệ = 100 * Số_lượng / Tổng) %>%
  ungroup() %>%
  filter(spending_level %in% c("Low", "High")) %>%
  select(Trình_độ_học_vấn = education,
         Nhóm_chi_tiêu = spending_level,
         Tỷ_lệ) %>%
  pivot_wider(names_from = Nhóm_chi_tiêu, values_from = Tỷ_lệ) %>%
  mutate(`Mức chênh lệch (%)` = round(High - Low, 2)) %>%
  rename(`Tỷ lệ chi tiêu cao (%)` = High,
         `Tỷ lệ chi tiêu thấp (%)` = Low)

# Hiển thị bảng rõ tiếng Việt, format đẹp
bang_tyle_chitieu %>%
  kbl(
    caption = "Bảng. Tỷ lệ chi tiêu cao, thấp và mức chênh lệch theo trình độ học vấn",
    digits = 2,
    align = "c",
    booktabs = TRUE
  ) %>%
  kable_styling(
    full_width = FALSE,
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    position = "center",
    font_size = 13
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2E86C1") %>%
  column_spec(1, bold = TRUE, color = "black", background = "#EAF2F8")
Bảng. Tỷ lệ chi tiêu cao, thấp và mức chênh lệch theo trình độ học vấn
Trình_độ_học_vấn Tỷ lệ chi tiêu thấp (%) Tỷ lệ chi tiêu cao (%) Mức chênh lệch (%)
HighSchool 25.30 24.62 -0.68
Bachelor 24.74 25.02 0.28
Masters 25.33 25.07 -0.25
College 24.97 25.15 0.18
  • Nhóm Bachelor có tỷ lệ chi tiêu cao hơn so với chi tiêu thấp là 0,28%, tương ứng với mức cân bằng gần như hoàn hảo. Nhóm College cao hơn chi tiêu thấp là 0,18%, vẫn cân bằng. Ngược lại, HighSchool có tỷ lệ chi tiêu thấp vượt cao 0,68 điểm %, nghĩa là khách hàng HighSchool có xu hướng chi tiêu thấp hơn đáng kể so với các nhóm còn lại. Nhóm Masters cũng có chi tiêu thấp nhỉnh hơn cao một chút (-0,25%), nhưng mức chênh lệch ít hơn HighSchool.

Biểu đồ 15&16&17: So sánh chi tiêu cao – thấp theo trình độ học vấn

library(patchwork)

# 1️⃣ Biểu đồ tỷ lệ chi tiêu cao
p1 <- ggplot(bang_tyle_chitieu, aes(x = reorder(Trình_độ_học_vấn, `Tỷ lệ chi tiêu cao (%)`),
                                    y = `Tỷ lệ chi tiêu cao (%)`)) +
  geom_col(fill = "gray30", color = "black", width = 0.6) +
  geom_text(aes(label = paste0(round(`Tỷ lệ chi tiêu cao (%)`, 1), "%")),
            vjust = -0.5, color = "black", size = 3.2) +
  labs(title = "Tỷ lệ chi tiêu cao", x = "Trình độ học vấn", y = "Tỷ lệ (%)") +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 15, vjust = 0.8, size = 9), # ↓ giảm size
    axis.text.y = element_text(size = 9)
  )

# 2️⃣ Biểu đồ tỷ lệ chi tiêu thấp
p2 <- ggplot(bang_tyle_chitieu, aes(x = reorder(Trình_độ_học_vấn, `Tỷ lệ chi tiêu thấp (%)`),
                                    y = `Tỷ lệ chi tiêu thấp (%)`)) +
  geom_col(fill = "gray60", color = "black", width = 0.6) +
  geom_text(aes(label = paste0(round(`Tỷ lệ chi tiêu thấp (%)`, 1), "%")),
            vjust = -0.5, color = "black", size = 3.2) +
  labs(title = "Tỷ lệ chi tiêu thấp", x = "Trình độ học vấn", y = "Tỷ lệ (%)") +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 15, vjust = 0.8, size = 9),
    axis.text.y = element_text(size = 9)
  )

# 3️⃣ Biểu đồ mức chênh lệch
p3 <- ggplot(bang_tyle_chitieu, aes(x = reorder(Trình_độ_học_vấn, `Mức chênh lệch (%)`),
                                    y = `Mức chênh lệch (%)`)) +
  geom_col(fill = "white", color = "black", width = 0.6) +
  geom_text(aes(label = paste0(round(`Mức chênh lệch (%)`, 1), "%")),
            vjust = -0.5, color = "black", size = 3.2) +
  labs(title = "Mức chênh lệch", x = "Trình độ học vấn", y = "Chênh lệch (%)") +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    axis.text.x = element_text(angle = 15, vjust = 0.8, size = 9),
    axis.text.y = element_text(size = 9)
  )

# 4️⃣ Ghép 3 biểu đồ thành 1 khung (3 cột)
(p1 | p2 | p3) +
  plot_annotation(
    title = "Biểu đồ: So sánh chi tiêu cao – thấp theo trình độ học vấn",
    subtitle = "Ba khung biểu đồ thể hiện: Chi tiêu cao | Chi tiêu thấp | Mức chênh lệch (%)",
    theme = theme(
      plot.title = element_text(face = "bold", hjust = 0.5, size = 14),
      plot.subtitle = element_text(hjust = 0.5, size = 11)
    )
  )

Thống kê mô tả chi tiêu của giới tính nữ theo từng trình độ học vấn

# 1️⃣ Lọc dữ liệu NỮ
female_data <- subset(data, gender == "Female")

# 2️⃣ Tính thống kê mô tả chi tiêu theo từng trình độ học vấn
female_summary_by_edu <- female_data %>%
  group_by(education) %>%
  summarise(
    Số_lượng = n(),
    Trung_bình = mean(purchase_amount, na.rm = TRUE),
    Trung_vị = median(purchase_amount, na.rm = TRUE),
    Độ_lệch_chuẩn = sd(purchase_amount, na.rm = TRUE),
    Nhỏ_nhất = min(purchase_amount, na.rm = TRUE),
    Lớn_nhất = max(purchase_amount, na.rm = TRUE),
    Q1 = quantile(purchase_amount, 0.25, na.rm = TRUE),
    Q3 = quantile(purchase_amount, 0.75, na.rm = TRUE)
  ) %>%
  arrange(desc(Trung_bình))

# 3️⃣ Hiển thị kết quả
female_summary_by_edu
# A tibble: 4 × 9
  education  Số_lượng Trung_bình Trung_vị Độ_lệch_chuẩn Nhỏ_nhất Lớn_nhất    Q1
  <ord>         <int>      <dbl>    <dbl>         <dbl>    <int>    <int> <dbl>
1 College       20031      9675.    9498          4821.     1166    25406 5593 
2 Bachelor      15064      9628.    9476.         4775.     1146    24967 5550.
3 Masters        4945      9601.    9405          4787.     1361    23708 5521 
4 HighSchool    10034      9580.    9310.         4796.     1233    24050 5560.
# ℹ 1 more variable: Q3 <dbl>
  • Giới thiệu code: Đoạn code sử dụng dplyr để lọc dữ liệu chỉ lấy nữ (subset(gender == “Female”)) và tính thống kê mô tả chi tiêu (purchase_amount) theo từng trình độ học vấn. Các chỉ số gồm số lượng, trung bình, trung vị, độ lệch chuẩn, giá trị nhỏ nhất, lớn nhất, Q1 và Q3, đồng thời sắp xếp theo trung bình chi tiêu giảm dần.
  • Diễn giải kết quả:
    • Nhóm College có số lượng lớn nhất (20.031 người) và mức chi tiêu trung bình cao nhất 9.674 USD, trung vị 9.498 USD.
    • Nhóm Bachelor có mức chi tiêu trung bình 9.628 USD, thấp hơn College khoảng 46 USD, trung vị 9.476 USD, khá gần nhau.
    • Nhóm Masters chi tiêu trung bình 9.601 USD, thấp hơn College khoảng 73 USD, trung vị 9.405 USD.
    • Nhóm HighSchool có mức chi tiêu thấp nhất 9.580 USD, trung vị 9.310 USD, thấp hơn nhóm College khoảng 94 USD.
  • Nhìn chung, chi tiêu trung bình của nữ tăng dần theo trình độ học vấn, College và Bachelor nhỉnh hơn Masters và HighSchool, nhưng sự khác biệt không quá lớn, dao động trong khoảng 40–100 USD.

Biểu đồ 18. So sánh mức chi tiêu của giới tính nữ ở trình độ College và Highschool

# Lọc dữ liệu nữ HighSchool và College
female_subset <- data %>%
  filter(gender == "Female", education %in% c("HighSchool", "College"))

# Vẽ boxplot so sánh
ggplot(female_subset, aes(x = education, y = purchase_amount, fill = education)) +
  geom_boxplot(width = 0.5, alpha = 0.7, outlier.color = "black") +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 3, color = "red") +
  labs(
    title = "So sánh mức chi tiêu của nữ theo trình độ HighSchool và College",
    x = "Trình độ học vấn",
    y = "Chi tiêu (USD)"
  ) +
  scale_fill_manual(values = c("HighSchool" = "#4B9CD3", "College" = "#E69F00")) +
  theme_minimal(base_size = 10) +
  theme(
    plot.title = element_text(face = "bold", size = 11, hjust = 0.5),
    axis.title = element_text(size = 9),
    axis.text = element_text(size = 8),
    legend.position = "none"
  )
  • Mức chi tiêu trung bình: Nữ nhóm College có mức chi tiêu trung bình khoảng 9.674 USD, nhỉnh hơn nữ nhóm HighSchool khoảng 95 USD (HighSchool trung bình ~9.580 USD). Điểm trung bình được đánh dấu bằng chấm đỏ trên biểu đồ, thể hiện College luôn cao hơn HighSchool.
  • Phân bố chi tiêu: Cả hai nhóm có phân bố tương tự, với Q1–Q3 xấp xỉ 5.500–13.300 USD. Tuy nhiên, HighSchool có mức chi tiêu tối đa cao hơn một chút (~25.406 USD so với ~25.406 USD), nhưng trung bình vẫn thấp hơn. Ngoài ra, cả hai nhóm đều có outlier, nhưng College tập trung quanh trung bình cao hơn.
  • Kết luận tổng quát: Nữ có trình độ College chi tiêu trung bình cao hơn nữ HighSchool, khoảng 1% so với HighSchool, đồng thời phân bố chi tiêu tương tự nhau. Như vậy, trình độ học vấn có ảnh hưởng nhẹ đến mức chi tiêu, College nhỉnh hơn HighSchool nhưng không quá chênh lệch lớn.

Thống kê mô tả chi tiêu của giới tính nam theo từng trình độ học vấn

# 1️⃣ Lọc dữ liệu NAM
male_data <- subset(data, gender == "Male")

# 2️⃣ Tính thống kê mô tả chi tiêu theo từng trình độ học vấn
male_summary_by_edu <- male_data %>%
  group_by(education) %>%
  summarise(
    Số_lượng = n(),
    Trung_bình = mean(purchase_amount, na.rm = TRUE),
    Trung_vị = median(purchase_amount, na.rm = TRUE),
    Độ_lệch_chuẩn = sd(purchase_amount, na.rm = TRUE),
    Nhỏ_nhất = min(purchase_amount, na.rm = TRUE),
    Lớn_nhất = max(purchase_amount, na.rm = TRUE),
    Q1 = quantile(purchase_amount, 0.25, na.rm = TRUE),
    Q3 = quantile(purchase_amount, 0.75, na.rm = TRUE)
  ) %>%
  arrange(desc(Trung_bình))

# 3️⃣ Hiển thị kết quả
male_summary_by_edu
# A tibble: 4 × 9
  education  Số_lượng Trung_bình Trung_vị Độ_lệch_chuẩn Nhỏ_nhất Lớn_nhất    Q1
  <ord>         <int>      <dbl>    <int>         <dbl>    <int>    <int> <dbl>
1 Bachelor      15215      9685.     9499         4798.     1223    26204 5694.
2 Masters        4871      9628.     9509         4761.     1304    23484 5541 
3 College       19843      9620.     9441         4809.     1118    24314 5582.
4 HighSchool     9997      9593.     9423         4804.     1239    24408 5510 
# ℹ 1 more variable: Q3 <dbl>
  • Đoạn code trên lọc dữ liệu nam từ bộ dữ liệu gốc (subset) và tính các thống kê mô tả về chi tiêu (purchase_amount) theo trình độ học vấn sử dụng group_by() và summarise() của dplyr. Các hàm chính: mean(), median(), sd(), min(), max(), quantile() để tính trung bình, trung vị, độ lệch chuẩn, giá trị nhỏ nhất, lớn nhất và các phần vị (Q1, Q3). Kết quả sắp xếp giảm dần theo giá trị trung bình.
  • Dựa trên bảng thống kê chi tiêu của nam theo trình độ học vấn, ta có:
    • Trung bình chi tiêu: Nam trình độ Bachelor chi tiêu trung bình 9685.01, cao hơn College (9619.79), Masters (9627.79) và HighSchool (9593.49). Mức chênh lệch so với HighSchool là khoảng 92–92 USD, so với College khoảng 65 USD, tức là Bachelor có chi tiêu nhỉnh hơn các nhóm khác, nhưng không quá khác biệt lớn.
    • Trung vị chi tiêu: Trung vị cho thấy 50% nam chi tiêu dưới mức này. Bachelor có trung vị 9499, College 9441, Masters 9509, HighSchool 9423. Nhìn chung, trung vị khá gần nhau, chứng tỏ chi tiêu điển hình của các nhóm khá tương đồng.
    • Độ lệch chuẩn: Các nhóm đều có độ lệch chuẩn ~4800–4809, cho thấy mức chi tiêu cá nhân phân tán rộng. Nam College có độ lệch chuẩn cao nhất (4808.86), nghĩa là trong nhóm này có nhiều người chi tiêu rất cao hoặc rất thấp so với trung bình.
    • Giá trị nhỏ nhất và lớn nhất: Nhóm Bachelor có giá trị chi tiêu cao nhất (26204), cao hơn hẳn các nhóm khác (College 24314, HighSchool 24408, Masters 23484), cho thấy trong nhóm này có những cá nhân chi tiêu cực kỳ lớn, kéo trung bình lên cao. Giá trị nhỏ nhất dao động từ 1118–1304, khá tương đồng giữa các nhóm.
    • Q1 và Q3 (25% và 75%): Khoảng chi tiêu trung bình của 50% nam nằm giữa Q1 và Q3, nhóm Bachelor Q1=5694.5, Q3=13396.5, nhỉnh hơn College (5582.5–13371) và HighSchool (5510–13281), chứng tỏ chi tiêu ở nhóm Bachelor cao hơn hầu hết các nhóm khác trong phân vị giữa.

Biểu đồ 19. So sánh mức chi tiêu của giới tính nam ở trình độ College và Bachelor

# Lọc dữ liệu nam trình độ Bachelor và College
male_bc_data <- male_data %>%
  filter(education %in% c("Bachelor", "College"))

# Vẽ boxplot so sánh chi tiêu
ggplot(male_bc_data, aes(x = education, y = purchase_amount, fill = education)) +
  geom_boxplot() +
  labs(
    title = "So sánh chi tiêu của nam theo trình độ Bachelor và College",
    x = "Trình độ học vấn",
    y = "Chi tiêu (purchase_amount)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")
  • Nam trình độ Bachelor nhìn chung chi tiêu cao hơn College, cả về trung vị và trung bình, đồng thời có mức chi tiêu cực đại lớn hơn, tạo sự khác biệt rõ rệt ở các giá trị cao. College có mức chi tiêu điển hình gần bằng nhưng ít có cá nhân chi tiêu cực đại.

Biểu đồ 20. So sánh chi tiêu của giới tính theo từng trình độ học vấn

# Kết hợp dữ liệu thống kê trung bình của nam và nữ
compare_gender_edu <- rbind(
  female_summary_by_edu %>% mutate(Giới_tính = "Nữ"),
  male_summary_by_edu %>% mutate(Giới_tính = "Nam")
)

# Vẽ biểu đồ so sánh
ggplot(compare_gender_edu, aes(x = education, y = Trung_bình, fill = Giới_tính)) +
  geom_col(position = position_dodge(width = 0.7), color = "black", width = 0.6) +
  geom_text(aes(label = round(Trung_bình, 0)),
            position = position_dodge(width = 0.7),
            vjust = -0.5, size = 3, color = "black") +
  labs(
    title = "Biểu đồ. So sánh mức chi tiêu trung bình theo giới tính và trình độ học vấn",
    x = "Trình độ học vấn",
    y = "Chi tiêu trung bình (USD)",
    fill = "Giới tính"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5, size = 14),
    axis.text.x = element_text(angle = 15, vjust = 0.8, size = 9),
    legend.position = "bottom"
  )
  • Biểu đồ So sánh mức chi tiêu trung bình theo giới tính và trình độ học vấn cho thấy mức chi tiêu của khách hàng có xu hướng rất cao và đồng đều, tập trung quanh ngưỡng 9,600 USD cho tất cả các nhóm nhân khẩu học được phân tích. Phát hiện nổi bật nhất là sự thiếu vắng khác biệt đáng kể về mức chi tiêu trung bình khi so sánh giữa khách hàng Nam và Nữ trong cùng một cấp độ học vấn. Điều này chỉ ra rằng, đối với bộ dữ liệu này, giới tính không phải là yếu tố phân khúc chính để dự đoán hoặc giải thích sự khác biệt trong hành vi chi tiêu.
  • Khi đi sâu vào chi tiết, mức chênh lệch tuyệt đối về chi tiêu giữa hai giới là không đáng kể, chỉ dao động tối đa khoảng $50 đến $60. Cụ thể, nhóm có mức chi tiêu trung bình cao nhất là Nam giới có trình độ Bachelor (9,685 USD), trong khi nhóm có mức chi tiêu thấp nhất là Nữ giới có trình độ HighSchool (9,580 USD). Tuy nhiên, ngay cả sự chênh lệch nhỏ này cũng không mang ý nghĩa kinh tế để điều chỉnh chiến lược tiếp thị. Mức chi tiêu cao nhất giữa hai giới chỉ chênh nhau $105 ($9,685 vs $9,580) trên toàn bộ phổ học vấn. Đáng chú ý, Nữ giới có mức chi tiêu trung bình cao hơn một chút so với Nam giới trong nhóm College (chênh lệch +55 USD), là điểm khác biệt lớn nhất theo hướng Nữ.

2 PHẦN 2: BỘ DỮ LIỆU TỪ MÃ CHỨNG KHOÁN CKV

2.1 TỔNG QUAN BỘ DỮ LIỆU

Công ty Cổ phần CokyVina (CKV) là một tổ chức có lịch sử phát triển từ năm 1990, khởi điểm là một doanh nghiệp nhà nước trực thuộc Tổng cục Bưu điện. Sau quá trình chuyển đổi mô hình hoạt động, CKV đã được tái cấu trúc thành công ty cổ phần, trong đó vốn Nhà nước chiếm 49%. Lĩnh vực hoạt động chính của CKV bao gồm: viễn thông (phát triển và quản lý hạ tầng mạng, truyền dẫn), công nghệ thông tin (cung cấp các dịch vụ như EKYC, xác thực khuôn mặt) và hoạt động xuất nhập khẩu vật tư, thiết bị chuyên ngành.

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

a. Chi tiết bộ dữ liệu

# --- Đọc và xử lý dữ liệu CKV ---
# 1. Đọc file
data_ckv <- read.csv("C:/Users/Admin/Documents/DataR/Data_ckv.csv")

# 2. Xóa các cột lỗi (từ X đến X.7)
data_ckv <- data_ckv %>%
  select(-starts_with("X"))

# 3. Giữ lại 40 dòng đầu tiên
data_ckv <- data_ckv[1:40, ]

# 4. Thông tin khái quát
so_dong <- nrow(data_ckv)
so_cot <- ncol(data_ckv)
cat(sprintf("Số dòng: %s, số cột: %s.\n",
            format(so_dong, big.mark = ".", decimal.mark = ",", scientific = FALSE),
            format(so_cot, big.mark = ".", decimal.mark = ",", scientific = FALSE)))
Số dòng: 40, số cột: 13.
# --- CHUẨN HÓA DỮ LIỆU ---

# 5. Giữ nguyên biến thời gian, chuyển các biến khác về numeric
data_ckv <- data_ckv %>%
  mutate(across(
    where(is.character) & !matches("Quarter"),
    ~ as.numeric(gsub(",", ".", gsub("\\.", "", trimws(.))))
  ))

# 6. Kiểm tra cấu trúc dữ liệu
str(data_ckv)
'data.frame':   40 obs. of  13 variables:
 $ Quarter           : chr  "Q1/2015" "Q2/2015" "Q3/2015" "Q4/2015" ...
 $ TongTaiSan        : num  2.14e+11 1.88e+11 1.98e+11 2.13e+11 2.20e+11 ...
 $ Tongno            : num  1.29e+11 1.12e+11 1.15e+11 1.28e+11 1.34e+11 ...
 $ QuyMo             : num  26.1 26 26 26.1 26.1 ...
 $ TangTruongTTS...  : num  NA -0.1203 0.0509 0.0772 0.0311 ...
 $ TySoNo.TTS.DR.    : num  0.601 0.597 0.582 0.602 0.61 ...
 $ TSNH              : num  1.33e+11 1.33e+11 1.52e+11 1.69e+11 1.27e+11 ...
 $ NoNganHan         : num  1.00e+11 1.10e+11 1.14e+11 1.26e+11 1.31e+11 ...
 $ VCSH              : num  7.76e+10 7.58e+10 8.27e+10 8.49e+10 8.57e+10 ...
 $ TSNH.TTS          : num  0.62 0.706 0.769 0.792 0.579 ...
 $ HeSoDBTC.TTS.VCSH.: num  2.76 2.48 2.39 2.51 2.56 ...
 $ TysoVCSH.TTS      : num  0.363 0.403 0.418 0.398 0.39 ...
 $ NoNganHan.TongNo  : num  0.778 0.981 0.988 0.98 0.981 ...
# --- HIỂN THỊ THEO CHUẨN KẾ TOÁN VIỆT NAM ---
# (Khi cần in ra, trình bày, hoặc knit ra PDF, chứ không thay đổi kiểu dữ liệu thật)

# Ví dụ in thử 5 dòng đầu
head(
  data_ckv %>%
    mutate(across(where(is.numeric),
                  ~ format(.x, big.mark = ".", decimal.mark = ",", scientific = FALSE))),
  5
)
  Quarter      TongTaiSan          Tongno   QuyMo TangTruongTTS...
1 Q1/2015 214.042.000.000 128.627.000.000 26,0894               NA
2 Q2/2015 188.283.000.000 112.469.000.000 25,9612          -0,1203
3 Q3/2015 197.872.000.000 115.146.000.000 26,0109           0,0509
4 Q4/2015 213.148.000.000 128.277.000.000 26,0853           0,0772
5 Q1/2016 219.784.000.000 134.058.000.000 26,1159           0,0311
  TySoNo.TTS.DR.            TSNH       NoNganHan           VCSH TSNH.TTS
1         0,6009 132.727.000.000 100.055.000.000 77.612.285.106   0,6201
2         0,5973 132.824.000.000 110.369.000.000 75.814.595.778   0,7055
3         0,5819 152.110.000.000 113.761.000.000 82.726.150.192   0,7687
4         0,6018 168.814.000.000 125.704.000.000 84.871.228.070   0,7920
5         0,6100 127.348.000.000 131.454.000.000 85.725.724.446   0,5794
  HeSoDBTC.TTS.VCSH. TysoVCSH.TTS NoNganHan.TongNo
1             2,7578       0,3626           0,7779
2             2,4835       0,4027           0,9813
3             2,3919       0,4181           0,9880
4             2,5114       0,3982           0,9799
5             2,5638       0,3900           0,9806

Giới thiệu code: Đoạn code trên thực hiện các bước tiền xử lý dữ liệu cơ bản trước khi tiến hành phân tích:

  • Dòng read.csv() dùng để đọc dữ liệu từ file CSV “Data_ckv.csv” được lưu trong máy tính cá nhân.
  • Lệnh select(-starts_with(“X”)) trong gói dplyr có chức năng loại bỏ các cột có tên bắt đầu bằng ký tự “X”, cụ thể là từ “X tới X7” — đây là các cột phát sinh do lỗi trong quá trình xuất file từ Excel, chứa toàn giá trị NA.
  • Dòng data_ckv <- data_ckv[1:40, ] có chức năng giữ lại 40 dòng đầu tiên, loại bỏ các quan sát bị khuyết từ hàng 41 trở đi.
  • Hai lệnh nrow(data_ckv), “”) và ncol(data_ckv), “” là số lượng dòng và cột hiện tại sau khi xử lý. Trong đó, Hàm nrow() đếm số dòng (quan sát), ncol() đếm số cột (biến) trong bảng data_ckv
  • Hàm mutate(across(…)) được dùng để chuyển đổi định dạng dữ liệu: giữ nguyên biến thời gian (Quarter), đồng thời chuyển các biến khác từ kiểu character sang numeric.
  • Hàm str(data_ckv) cho phép kiểm tra cấu trúc tổng quát của bảng dữ liệu.
  • sử dụng head() in thử 5 dòng đầu tiên của bảng dữ liệu.

Giới thiệu bộ dữ liệu: Bộ dữ liệu được sử dụng trong phân tích này được thu thập từ Báo cáo tài chính theo quý của Công ty Cổ phần CokyVina trong giai đoạn từ quý I năm 2015 đến quý IV năm 2024, gồm 40 quan sát tương ứng với 40 quý và 13 biến số tài chính chủ yếu. Trong đó, các biến phản ánh các khía cạnh khác nhau của tình hình tài chính doanh nghiệp như sau:

  • Tổng tài sản (TongTaiSan), Tổng nợ (Tongno) và Vốn chủ sở hữu (VCSH) thể hiện quy mô và cơ cấu tài chính của doanh nghiệp.
  • Tăng trưởng tổng tài sản (TangTruongTTS…) giúp theo dõi mức độ mở rộng hoạt động và hiệu quả sử dụng vốn qua thời gian.
  • Các tỷ số tài chính như Tỷ số nợ/Tổng tài sản (TySoNo.TTS.DR.), Tỷ lệ tài sản ngắn hạn/Tổng tài sản (TSNH.TTS), hay Tỷ lệ vốn chủ sở hữu/Tổng tài sản (TysoVCSH.TTS) phản ánh mức độ an toàn tài chính và khả năng thanh khoản.

2.2.1 Xử lý dữ liệu

b. Kiểm tra dữ liệu khuyết NA

# --- KIỂM TRA GIÁ TRỊ NA  ---
colSums(
  data_ckv == "" | data_ckv == "NA" | data_ckv == "     NA" | is.na(data_ckv)
)
           Quarter         TongTaiSan             Tongno              QuyMo 
                 0                  0                  0                  0 
  TangTruongTTS...     TySoNo.TTS.DR.               TSNH          NoNganHan 
                 1                  0                  0                  0 
              VCSH           TSNH.TTS HeSoDBTC.TTS.VCSH.       TysoVCSH.TTS 
                 0                  0                  0                  0 
  NoNganHan.TongNo 
                 0 
sum_na <- sum(is.na(data_ckv))
cat("🔹 Tổng số giá trị thiếu trong bộ dữ liệu là:", sum_na, "\n")
🔹 Tổng số giá trị thiếu trong bộ dữ liệu là: 1 

Giải thích code: Để kiểm tra các giá trị khuyết, nhóm chúng em đã thực hiện lệnh sau:

  • Hàm is.na(data_ckv) tạo ra một ma trận logic có cùng kích thước với bộ dữ liệu “data_ckv”. Trong đó, mỗi ô có giá trị “TRUE” nếu phần tử đó bị thiếu dữ liệu (NA), và FALSE nếu có giá trị hợp lệ.
  • Hàm colSums() sau đó được sử dụng để tính tổng số giá trị TRUE (tức là số lượng NA) trong từng cột. Cụ thể, colSums() cộng tất cả các giá trị trong mỗi cột, với quy ước TRUE = 1, FALSE = 0.
  • Kết quả trả về là một vector, trong đó tên mỗi phần tử là tên biến, và giá trị tương ứng là số lượng NA của biến đó.

Kết quả: Từ bảng kết quả trên, ta thấy chỉ có một giá trị bị thiếu (NA) ở biến TangTruongTTS do biến “tăng trưởng tổng tài sản” được tính theo tỷ lệ thay đổi qua các quý, nên quý đầu tiên (Q1/2015) không có dữ liệu của quý trước để so sánh từ đó dẫn đến giá trị bị thiếu.

b. Xử lý dữ liệu bị khuyết(NA)

# 4️⃣ Thay thế giá trị NA của cột 'TangTruongTTS...' bằng trung bình cộng
mean_tts <- mean(data_ckv$TangTruongTTS..., na.rm = TRUE)
data_ckv$TangTruongTTS...[is.na(data_ckv$TangTruongTTS...)] <- mean_tts

# 5️⃣ In 10 dòng đầu để kiểm tra, hiển thị chuẩn kế toán Việt Nam
data_ckv_display <- head(data_ckv, 10)

kable(
  format(
    data_ckv_display,
    big.mark = ".", decimal.mark = ",", scientific = FALSE
  ),
  caption = "10 dòng đầu của bộ dữ liệu sau khi thay thế giá trị NA bằng trung bình cộng",
  align = "c"
) %>%
  kable_styling(latex_options = c("hold_position"))
10 dòng đầu của bộ dữ liệu sau khi thay thế giá trị NA bằng trung bình cộng
Quarter TongTaiSan Tongno QuyMo TangTruongTTS… TySoNo.TTS.DR. TSNH NoNganHan VCSH TSNH.TTS HeSoDBTC.TTS.VCSH. TysoVCSH.TTS NoNganHan.TongNo
Q1/2015 214.042.000.000 128.627.000.000 26,0894 0,003487179 0,6009 132.727.000.000 100.055.000.000 77.612.285.106 0,6201 2,7578 0,3626 0,7779
Q2/2015 188.283.000.000 112.469.000.000 25,9612 -0,120300000 0,5973 132.824.000.000 110.369.000.000 75.814.595.778 0,7055 2,4835 0,4027 0,9813
Q3/2015 197.872.000.000 115.146.000.000 26,0109 0,050900000 0,5819 152.110.000.000 113.761.000.000 82.726.150.192 0,7687 2,3919 0,4181 0,9880
Q4/2015 213.148.000.000 128.277.000.000 26,0853 0,077200000 0,6018 168.814.000.000 125.704.000.000 84.871.228.070 0,7920 2,5114 0,3982 0,9799
Q1/2016 219.784.000.000 134.058.000.000 26,1159 0,031100000 0,6100 127.348.000.000 131.454.000.000 85.725.724.446 0,5794 2,5638 0,3900 0,9806
Q2/2016 230.748.000.000 148.635.000.000 26,1646 0,049900000 0,6441 127.171.000.000 146.030.000.000 82.113.337.858 0,5511 2,8101 0,3559 0,9825
Q3/2016 219.402.000.000 135.615.000.000 26,1142 -0,049200000 0,6181 118.252.000.000 93.995.786.810 83.786.837.096 0,5390 2,6186 0,3819 0,6931
Q4/2016 198.244.000.000 23.514.990.456 26,0128 -0,096400000 0,1186 111.123.000.000 303.132.423 85.419.851.209 0,5605 2,3208 0,4309 0,0129
Q1/2017 182.494.000.000 96.624.713.482 25,9300 -0,079400000 0,5295 95.533.409.248 73.412.855.449 85.869.103.598 0,5235 2,1253 0,4705 0,7598
Q2/2017 177.278.000.000 94.630.594.222 25,9010 -0,028600000 0,5338 92.598.075.484 72.923.736.189 82.647.576.274 0,5223 2,1450 0,4662 0,7706

Giải thích code: - Hàm mean() được sử dụng để tính giá trị trung bình cộng của cột TangTruongTTS…, với tham số na.rm = TRUE giúp bỏ qua các giá trị NA khi tính toán. - Sau đó, biểu thức data_ckv\(TangTruongTTS...[is.na(data_ckv\)TangTruongTTS…)] <- mean_tts có chức năng thay thế toàn bộ giá trị NA trong cột bằng giá trị trung bình vừa tính được. - Cách tiếp cận này giúp đảm bảo rằng biến TangTruongTTS… không còn chứa giá trị thiếu, thuận tiện cho việc mô hình hóa và trực quan hóa dữ liệu ở các bước sau.

Kết quả:

  • Biến Tăng trưởng tổng tài sản (%) có duy nhất một giá trị bị khuyết tại quý 1/2015. Do đây là quý đầu tiên trong chuỗi thời gian, giá trị NA được xử lý bằng phương pháp thay thế trung bình cộng, với giá trị thay thế là 0.003487179. Sau xử lý, toàn bộ bộ dữ liệu không còn giá trị bị khuyết.

c. Kiểm tra trùng lặp

# --- KIỂM TRA TRÙNG LẶP ---
duplicated_rows <- sum(duplicated(data_ckv))
cat("Số dòng trùng lặp:", duplicated_rows, "\n")
Số dòng trùng lặp: 0 

Nhóm chúng em sử dụng lệnh “sum(duplicated(data_ckv))” để kiểm tra dữ liệu trùng lặp và kết quả đạt được là bộ dữ liệu không có xuất hiện trùng lặp.

d. PHÁT HIỆN & XỬ LÝ NGOẠI LAI (OUTLIERS) THEO IQR

# 1. Hàm xác định ngoại lai (theo IQR)
detect_outlier <- function(x) {
  if (is.numeric(x)) {
    Q1 <- quantile(x, 0.25, na.rm = TRUE)
    Q3 <- quantile(x, 0.75, na.rm = TRUE)
    IQR_val <- Q3 - Q1
    lower <- Q1 - 1.5 * IQR_val
    upper <- Q3 + 1.5 * IQR_val
    return(ifelse(x < lower | x > upper, 1, 0))
  } else {
    return(0)
  }
}

# 2. Áp dụng gắn cờ cho toàn bộ cột numeric
data_ckv_flag <- data_ckv %>%
  mutate(across(where(is.numeric),
                ~ detect_outlier(.x),
                .names = "Flag_{.col}"))

# 3. Kiểm tra và trình bày số lượng giá trị ngoại lai dễ hiểu hơn ----

# Tổng hợp số lượng giá trị bị gắn cờ
outlier_summary <- data_ckv_flag %>%
  select(starts_with("Flag_")) %>%
  summarise(across(everything(), sum)) %>%
  pivot_longer(cols = everything(),
               names_to = "Biến",
               values_to = "Số_ngoại_lai") %>%
  mutate(
    Biến = gsub("Flag_", "", Biến),
    Trạng_thái = ifelse(Số_ngoại_lai > 0, "Có ngoại lai", "Không có")
  )

# Hiển thị kết quả gọn gàng, dễ hiểu
kable(outlier_summary,
      caption = "Tổng hợp số lượng giá trị ngoại lai theo từng biến") %>%
  kable_styling(latex_options = c("hold_position")) %>%
  column_spec(1, bold = TRUE) %>%
  column_spec(3, color = ifelse(outlier_summary$Trạng_thái == "Có ngoại lai", "red", "black"))
Tổng hợp số lượng giá trị ngoại lai theo từng biến
Biến Số_ngoại_lai Trạng_thái
TongTaiSan 0 Không có
Tongno 0 Không có
QuyMo 0 Không có
TangTruongTTS… 1 Có ngoại lai
TySoNo.TTS.DR. 1 Có ngoại lai
TSNH 0 Không có
NoNganHan 1 Có ngoại lai
VCSH 0 Không có
TSNH.TTS 0 Không có
HeSoDBTC.TTS.VCSH. 0 Không có
TysoVCSH.TTS 0 Không có
NoNganHan.TongNo 2 Có ngoại lai

Giải thích code:

  • Lệnh names(data_ckv) hiển thị danh sách tên biến gốc trong bộ dữ liệu.
  • Lệnh gsub(“[1]+”, “”, names(data_ckv)) dùng hàm thay thế ký tự đặc biệt. Trong đó, ”[2]+” nghĩa là mọi ký tự không phải chữ cái hoặc số (ví dụ: dấu chấm, khoảng trắng, ký tự đặc biệt) sẽ được thay thế bằng dấu gạch dưới “” để giúp tên biến dễ đọc hơn và phù hợp với chuẩn R.
  • Lệnh tolower(names(data_ckv)) chuyển toàn bộ tên biến sang chữ thường, giúp đồng nhất định dạng, tránh lỗi khi gọi tên biến.

Kết quả: Kết quả cho thấy tên biến đã được chuẩn hóa thành dạng nhất quán ví dụ như:

  • Biến “TongTaiSan” đổi thành “tongtaisan”
  • Biến “TySoNo.TTS.DR.” đổi thành “tysono_tts_dr_”
  • Biến “HeSoDBTC.TTS.VCSH.” đổi thành “hesodbtc_tts_vcsh_”

Tạo thêm 1 biến mới: Tỷ lệ nợ trên vốn chủ sở hữu

# 1. Chuẩn hóa tên biến về dạng snake_case (dễ code, dễ đọc)
names(data_ckv) <- tolower(gsub("[\\.\\s]+", "_", names(data_ckv)))

# 2. Tạo thêm 1 biến mới: Tỷ lệ nợ trên vốn chủ sở hữu (debt_to_equity)
data_ckv <- data_ckv %>%
  mutate(debt_to_equity = tongno / vcsh)

# 3. Hiển thị 10 dòng đầu để kiểm tra
kable(head(data_ckv, 10),
      caption = "10 dòng đầu sau khi chuẩn hóa tên biến và tạo thêm biến debt_to_equity") %>%
  kable_styling(latex_options = c("hold_position"))
10 dòng đầu sau khi chuẩn hóa tên biến và tạo thêm biến debt_to_equity
quarter tongtaisan tongno quymo tangtruongtts_ tysono_tts_dr_ tsnh nonganhan vcsh tsnh_tts hesodbtc_tts_vcsh_ ty_ovcsh_tts nonganhan_tongno debt_to_equity
Q1/2015 2.14042e+11 128627000000 26.0894 0.0034872 0.6009 132727000000 100055000000 77612285106 0.6201 2.7578 0.3626 0.7779 1.6573021
Q2/2015 1.88283e+11 112469000000 25.9612 -0.1203000 0.5973 132824000000 110369000000 75814595778 0.7055 2.4835 0.4027 0.9813 1.4834742
Q3/2015 1.97872e+11 115146000000 26.0109 0.0509000 0.5819 152110000000 113761000000 82726150192 0.7687 2.3919 0.4181 0.9880 1.3918936
Q4/2015 2.13148e+11 128277000000 26.0853 0.0772000 0.6018 168814000000 125704000000 84871228070 0.7920 2.5114 0.3982 0.9799 1.5114309
Q1/2016 2.19784e+11 134058000000 26.1159 0.0311000 0.6100 127348000000 131454000000 85725724446 0.5794 2.5638 0.3900 0.9806 1.5638013
Q2/2016 2.30748e+11 148635000000 26.1646 0.0499000 0.6441 127171000000 146030000000 82113337858 0.5511 2.8101 0.3559 0.9825 1.8101201
Q3/2016 2.19402e+11 135615000000 26.1142 -0.0492000 0.6181 118252000000 93995786810 83786837096 0.5390 2.6186 0.3819 0.6931 1.6185717
Q4/2016 1.98244e+11 23514990456 26.0128 -0.0964000 0.1186 111123000000 303132423 85419851209 0.5605 2.3208 0.4309 0.0129 0.2752872
Q1/2017 1.82494e+11 96624713482 25.9300 -0.0794000 0.5295 95533409248 73412855449 85869103598 0.5235 2.1253 0.4705 0.7598 1.1252559
Q2/2017 1.77278e+11 94630594222 25.9010 -0.0286000 0.5338 92598075484 72923736189 82647576274 0.5223 2.1450 0.4662 0.7706 1.1449893
  • Hàm mutate() thuộc gói dplyr được sử dụng để tạo và gán thêm biến mới vào bảng dữ liệu hiện có (data_ckv) cụ thể nhóm chúng em tạo biến debt_to_equity.

e.Xử lý biến Quarter

# 1. Đảm bảo các biến số là numeric
data_ckv <- data_ckv %>%
  mutate(across(-quarter, ~ as.numeric(.)))
unique(data_ckv$quarter)
 [1] "Q1/2015" "Q2/2015" "Q3/2015" "Q4/2015" "Q1/2016" "Q2/2016" "Q3/2016"
 [8] "Q4/2016" "Q1/2017" "Q2/2017" "Q3/2017" "Q4/2017" "Q1/2018" "Q2/2018"
[15] "Q3/2018" "Q4/2018" "Q1/2019" "Q2/2019" "Q3/2019" "Q4/2019" "Q1/2020"
[22] "Q2/2020" "Q3/2020" "Q4/2020" "Q1/2021" "Q2/2021" "Q3/2021" "Q4/2021"
[29] "Q1/2022" "Q2/2022" "Q3/2022" "Q4/2022" "Q1/2023" "Q2/2023" "Q3/2023"
[36] "Q4/2023" "Q1/2024" "Q2/2024" "Q3/2024" "Q4/2024"

Để xem dữ liệu cột “Quarter”, nhóm chúng em dùng lệnh “unique(data_ckv$quarter)” để liệt kê chi tiết dữ liệu lấy từ cột “Quarter” ở trong bảng có tên “data_ckv”.

2.3 Tổng kết

# --- BẢNG MÔ TẢ CÁC BIẾN TRONG BỘ DỮ LIỆU CKV ----------------------------------

library(dplyr)
library(kableExtra)

mo_ta_bien <- data.frame(
  Bien = c("quarter", "tongtaisan", "tongno", "quymo", 
           "tangtruongtts_", "tysono_tts_dr_", "tsnh", "nonganhan", 
           "vcsh", "tsnh_tts", "hesodbtc_tts_vcsh_", 
           "ty_ovcsh_tts", "nonganhan_tongno", "debt_to_equity"),
  
  Kieu_Du_Lieu = c("character", rep("numeric", 12), "numeric"),
  
  Y_Nghia = c(
    "Quý quan sát (dạng thời gian: Q1, Q2, Q3, Q4 theo từng năm)",
    "Tổng tài sản của doanh nghiệp trong kỳ (Total Assets)",
    "Tổng nợ phải trả trong kỳ (Total Liabilities)",
    "Quy mô doanh nghiệp (ước tính thông qua tổng tài sản, log hoặc giá trị gốc)",
    "Tốc độ tăng trưởng tổng tài sản so với kỳ trước (%)",
    "Tỷ số nợ trên tổng tài sản (Debt Ratio = Nợ / Tổng tài sản)",
    "Tài sản ngắn hạn (Current Assets)",
    "Nợ ngắn hạn (Current Liabilities, phải trả trong 1 năm)",
    "Vốn chủ sở hữu (Equity)",
    "Tỷ lệ tài sản ngắn hạn trên tổng tài sản (Current Assets Ratio)",
    "Hệ số đảm bảo tài chính giữa tổng tài sản và vốn chủ sở hữu (TTS/VCSH)",
    "Tỷ số vốn chủ sở hữu trên tổng tài sản (Equity Ratio)",
    "Tỷ lệ nợ ngắn hạn trên tổng nợ (Short-term Debt Ratio)",
    "Tỷ lệ Nợ phải trả trên Vốn chủ sở hữu (Debt-to-Equity Ratio)"
  )
)

# Hiển thị bảng đẹp
mo_ta_bien %>%
  kbl(caption = "Bảng 1.1: Mô tả các biến trong bộ dữ liệu CKV", 
      align = c('c', 'c', 'l'),
      col.names = c("Biến", "Kiểu dữ liệu", "Ý nghĩa")) %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  column_spec(1, bold = TRUE, width = "6em") %>%
  column_spec(2, width = "7em") %>%
  column_spec(3, width = "30em") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 1.1: Mô tả các biến trong bộ dữ liệu CKV
Biến Kiểu dữ liệu Ý nghĩa
quarter character Quý quan sát (dạng thời gian: Q1, Q2, Q3, Q4 theo từng năm)
tongtaisan numeric Tổng tài sản của doanh nghiệp trong kỳ (Total Assets)
tongno numeric Tổng nợ phải trả trong kỳ (Total Liabilities)
quymo numeric Quy mô doanh nghiệp (ước tính thông qua tổng tài sản, log hoặc giá trị gốc)
tangtruongtts_ numeric Tốc độ tăng trưởng tổng tài sản so với kỳ trước (%)
tysono_tts_dr_ numeric Tỷ số nợ trên tổng tài sản (Debt Ratio = Nợ / Tổng tài sản)
tsnh numeric Tài sản ngắn hạn (Current Assets)
nonganhan numeric Nợ ngắn hạn (Current Liabilities, phải trả trong 1 năm)
vcsh numeric Vốn chủ sở hữu (Equity)
tsnh_tts numeric Tỷ lệ tài sản ngắn hạn trên tổng tài sản (Current Assets Ratio)
hesodbtc_tts_vcsh_ numeric Hệ số đảm bảo tài chính giữa tổng tài sản và vốn chủ sở hữu (TTS/VCSH)
ty_ovcsh_tts numeric Tỷ số vốn chủ sở hữu trên tổng tài sản (Equity Ratio)
nonganhan_tongno numeric Tỷ lệ nợ ngắn hạn trên tổng nợ (Short-term Debt Ratio)
debt_to_equity numeric Tỷ lệ Nợ phải trả trên Vốn chủ sở hữu (Debt-to-Equity Ratio)

Sau khi tiền xử lý dữ liệu, chúng em tạo bảng tổng hợp kết quả và mô tả ý nghĩa các biến, cụ thể:

  • Chúng em sử dụng data.frame() để liệt kê tên biến (Bien), kiểu dữ liệu (Kieu_Du_Lieu) và ý nghĩa từng biến (Y_Nghia).
    • Thư viện kableExtra được dùng để trình bày bảng đẹp mắt: kbl() tạo bảng cơ bản với tiêu đề và căn chỉnh cột, kable_paper() thêm style dạng “striped”, column_spec() điều chỉnh độ rộng và in đậm cột, row_spec() định dạng hàng tiêu đề với màu nền và chữ đậm.
    • Kết quả là một bảng tổng hợp trực quan, giúp người đọc dễ dàng nắm được cấu trúc và nội dung các biến trong bộ dữ liệu CKV.

2.4 PHÂN TÍCH DỮ LIỆU

2.4.1 Thống kê mô tả

Mục tiêu: Phần này nhằm cung cấp cái nhìn tổng quan về các đặc trưng thống kê cơ bản của từng biến trong bộ dữ liệu CKV, bao gồm trung bình, trung vị, độ lệch chuẩn, phương sai, giá trị nhỏ nhất và lớn nhất, cũng như các đặc trưng hình dạng phân bố như độ lệch (skewness) và độ nhọn (kurtosis).

a. Thống kê mô tả từng biến

# Hàm định dạng hiển thị chuẩn kế toán Việt Nam (chỉ dùng khi in)
fmt_vn <- function(x, digits = 0) {
  if (is.numeric(x)) {
    format(round(x, digits), big.mark = ".", decimal.mark = ",", scientific = FALSE, trim = TRUE)
  } else {
    as.character(x)
  }
}
# Chuẩn: đảm bảo dữ liệu đã sắp theo thời gian
data_ckv <- data_ckv %>% arrange(quarter)

Biến Tổng tài sản (tongtaisan)

# --- (1) Tính tốc độ tăng trưởng trung bình theo năm (bảng) ---
data_ckv <- data_ckv %>% mutate(year = substr(quarter, 4, 7))
tangtruong_nam <- data_ckv %>%
  group_by(year) %>%
  summarise(
    first_tts = first(tongtaisan),
    last_tts = last(tongtaisan),
    growth_pct = (last_tts - first_tts) / first_tts * 100
  )

tangtruong_nam_display <- tangtruong_nam %>%
  mutate(across(c(first_tts, last_tts), ~ fmt_vn(.x))) %>%
  mutate(growth_pct = paste0(round(growth_pct, 2), "%"))

tangtruong_nam_display %>%
  kbl(caption = "Bảng 3.1: Tốc độ tăng trưởng tổng tài sản theo năm", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.1: Tốc độ tăng trưởng tổng tài sản theo năm
year first_tts last_tts growth_pct
2015 214.042.000.000 213.148.000.000 -0.42%
2016 219.784.000.000 198.244.000.000 -9.8%
2017 182.494.000.000 205.729.000.000 12.73%
2018 231.446.000.000 212.164.000.000 -8.33%
2019 178.206.000.000 189.817.000.000 6.52%
2020 172.352.000.000 158.478.000.000 -8.05%
2021 145.838.000.000 170.434.000.000 16.87%
2022 151.459.000.000 154.404.000.000 1.94%
2023 144.493.000.000 152.050.000.000 5.23%
2024 158.888.000.000 199.116.000.000 25.32%
  • Đoạn mã dưới đây sử dụng các hàm trong gói dplyr như mutate(), group_by(), summarise() và phép tính toán học để tính tốc độ tăng trưởng tổng tài sản theo năm. Dữ liệu được xử lý, nhóm và tổng hợp theo biến year, sau đó tốc độ tăng trưởng được tính bằng công thức phần trăm thay đổi giữa giá trị đầu và cuối năm. Kết quả giúp nhận diện các giai đoạn tăng trưởng, suy giảm hoặc biến động mạnh trong quy mô tài sản của doanh nghiệp.
  • Tốc độ tăng trưởng tổng tài sản của doanh nghiệp có sự biến động rõ rệt qua các năm, phản ánh quá trình điều chỉnh quy mô hoạt động và cơ cấu tài chính trong giai đoạn 2015–2024.Cụ thể,
    • Giai đoạn 2016–2017 ghi nhận mức tăng trưởng đột biến +12,73%, cho thấy doanh nghiệp đã đảo chiều xu hướng suy giảm của năm 2016 và mở rộng đáng kể quy mô tài sản. Tuy nhiên, năm 2018 lại chứng kiến sự sụt giảm mạnh -8,33%, phản ánh biến động lớn trong cấu trúc tài sản, có thể do điều chỉnh danh mục đầu tư hoặc cắt giảm nguồn vốn ngắn hạn.
    • Đến năm 2021, tổng tài sản tăng 16,87%, cho thấy dấu hiệu phục hồi mạnh mẽ sau giai đoạn suy giảm năm 2020. Đặc biệt, năm 2024 là mốc tăng trưởng nổi bật nhất trong toàn bộ chuỗi thời gian, với mức tăng +25,32%, thể hiện sự mở rộng quy mô hoạt động và năng lực tài chính đáng kể của doanh nghiệp so với năm trước đó.

Tính tốc độ tăng trưởng kép hàng năm (CAGR)

# --- (2) Tính tốc độ tăng trưởng kép hàng năm (CAGR) ---
# Lấy giá trị đầu kỳ và cuối kỳ của tổng tài sản
gia_tri_dau <- first(data_ckv$tongtaisan)
gia_tri_cuoi <- last(data_ckv$tongtaisan)

# Xác định số năm (dựa trên khoảng thời gian trong dữ liệu)
so_nam <- as.numeric(last(data_ckv$year)) - as.numeric(first(data_ckv$year))

# Tính CAGR theo công thức:
# CAGR = (Giá trị cuối / Giá trị đầu)^(1 / số năm) - 1
cagr <- ((gia_tri_cuoi / gia_tri_dau)^(1 / so_nam) - 1) * 100

# --- (3) Hiển thị kết quả dưới dạng bảng ---
cagr_table <- data.frame(
  Chi_tieu = c("Giá trị khởi đầu", "Giá trị kết thúc", "Số năm (n)", "CAGR (%)"),
  Gia_tri = c(
    format(gia_tri_dau, big.mark = ".", decimal.mark = ",", scientific = FALSE),
    format(gia_tri_cuoi, big.mark = ".", decimal.mark = ",", scientific = FALSE),
    so_nam,
    paste0(round(cagr, 2), "%")
  )
)

kbl(cagr_table, caption = "Bảng 3.2: CAGR toàn kỳ của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.2: CAGR toàn kỳ của Tổng tài sản
Chi_tieu Gia_tri
Giá trị khởi đầu 214.042.000.000
Giá trị kết thúc 199.116.000.000
Số năm (n) 9
CAGR (%) -0.8%
  • Các hàm được sử dụng gồm:
    • first() và last() để xác định giá trị đầu kỳ và cuối kỳ của biến tổng tài sản.
    • length(unique()) để xác định số năm phân tích trong chuỗi dữ liệu.
    • Công thức ((end_value / start_value)^(1 / n_years) - 1) * 100 được áp dụng để tính CAGR, phản ánh mức tăng trưởng bình quân hàng năm.
  • Kết quả: Trong giai đoạn 2015–2024, tốc độ tăng trưởng kép hàng năm (CAGR) của tổng tài sản đạt -0,8%, cho thấy quy mô tài sản có xu hướng giảm nhẹ bình quân mỗi năm. Điều này phản ánh hiệu quả tăng trưởng chưa bền vững trong dài hạn, khi giá trị tài sản cuối kỳ (năm 2024) vẫn thấp hơn mức khởi đầu (năm 2015). Mức CAGR âm cho thấy doanh nghiệp chưa duy trì được đà tăng trưởng ổn định, mặc dù trong chuỗi kỳ có xuất hiện các giai đoạn hồi phục mạnh ở các năm 2017, 2021 và 2024. Số quan sát hợp lệ cho tongtaisan
# ----- (3) Số lượng quan sát hợp lệ (non-NA) -----
n_obs <- sum(!is.na(data_ckv$tongtaisan))
data.frame(Chi_tieu = "Số quan sát hợp lệ (non-NA)", Gia_tri = n_obs) %>%
  kbl(caption = "Bảng 3.3: Số quan sát hợp lệ cho tongtaisan", align = "c") %>%
  kable_paper(full_width = F) %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.3: Số quan sát hợp lệ cho tongtaisan
Chi_tieu Gia_tri
Số quan sát hợp lệ (non-NA) 40
  • Nhóm sử dụng hàm sum() để tính tổng số quan sát hợp lệ cho tongtaisan

Thống kê mô tả cho biến Tổng tài sản

# ----- (4) Trung bình ----- 
mean_tts <- mean(data_ckv$tongtaisan, na.rm = TRUE)

# ----- (5) Trung vị (median) -----
median_tts <- median(data_ckv$tongtaisan, na.rm = TRUE)

# ----- (6) Độ lệch chuẩn (SD) -----
sd_tts <- sd(data_ckv$tongtaisan, na.rm = TRUE)

# ----- (7) Phương sai (variance) -----
var_tts <- var(data_ckv$tongtaisan, na.rm = TRUE)

# ----- (8) Giá trị nhỏ nhất và lớn nhất (min & max) -----
min_tts <- min(data_ckv$tongtaisan, na.rm = TRUE)
max_tts <- max(data_ckv$tongtaisan, na.rm = TRUE)
# ----- (9) Khoảng (range = max - min) -----
range_tts <- max_tts - min_tts

# ----- (10) IQR (Q3 - Q1) -----
q1 <- quantile(data_ckv$tongtaisan, 0.25, na.rm = TRUE)
q3 <- quantile(data_ckv$tongtaisan, 0.75, na.rm = TRUE)
iqr_tts <- q3 - q1

# Tạo bảng 4 (tập hợp các chỉ tiêu cơ bản 4–10) và hiển thị
basic_table <- data.frame(
  Chi_tieu = c("Trung bình", "Trung vị", "Độ lệch chuẩn", "Phương sai",
               "Nhỏ nhất (Min)", "Lớn nhất (Max)", "Range", "IQR (Q3-Q1)"),
  Gia_tri = c(mean_tts, median_tts, sd_tts, var_tts,
              min_tts, max_tts, range_tts, iqr_tts)
)

basic_table %>%
  mutate(Gia_tri = fmt_vn(Gia_tri)) %>%
  kbl(caption = "Bảng 3.4: Các chỉ tiêu cơ bản của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.4: Các chỉ tiêu cơ bản của Tổng tài sản
Chi_tieu Gia_tri
Trung bình 177.584.100.000
Trung vị 174.960.000.000
Độ lệch chuẩn 28.907.405.651
Phương sai 835.638.101.476.923.015.108
Nhỏ nhất (Min) 130.953.000.000
Lớn nhất (Max) 231.446.000.000
Range 100.493.000.000
IQR (Q3-Q1) 48.867.000.000
  • Hàm mean() và median() được sử dụng để xác định giá trị trung bình và trung vị, phản ánh xu hướng tập trung của dữ liệu.
  • Các hàm sd() và var() cho biết mức độ phân tán của dữ liệu thông qua độ lệch chuẩn và phương sai.
  • Hàm min(), max(), range và quantile() được dùng để xác định các giá trị biên và khoảng phân vị, hỗ trợ đánh giá mức độ dao động và phân bố của tổng tài sản.

Tính Phân vị (Q1, Q2, Q3) của biến Tổng tài sản

# ----- (11) Phân vị (Q1, Q2, Q3) -----
quartile_table <- data.frame(
  Phan_vi = c("Q1 (25%)", "Q2 (50%) - Median", "Q3 (75%)"),
  Gia_tri = c(q1, median_tts, q3)
) %>%
  mutate(Gia_tri = fmt_vn(Gia_tri))

quartile_table %>%
  kbl(caption = "Bảng 3.5: Phân vị của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.5: Phân vị của Tổng tài sản
Phan_vi Gia_tri
25% Q1 (25%) 151.902.250.000
Q2 (50%) - Median 174.960.000.000
75% Q3 (75%) 200.769.250.000
  • Hàm quantile() được dùng để tính phân vị thứ nhất (Q1) và phân vị thứ ba (Q3), tương ứng với mốc 25% và 75% của phân bố dữ liệu.
  • Hàm median() đại diện cho phân vị thứ hai (Q2), hay còn gọi là trung vị.
  • Kết quả cho thấy 25% giá trị thấp nhất của tổng tài sản nằm dưới mức 151,90 nghìn tỷ đồng, trong khi 25% giá trị cao nhất vượt trên 200,77 nghìn tỷ đồng. Trung vị đạt 174,96 nghìn tỷ đồng, nằm gần giữa khoảng phân vị, phản ánh phân bố dữ liệu khá cân đối.
  • Khoảng chênh lệch giữa Q1 và Q3 đạt gần 48,87 nghìn tỷ đồng, cho thấy sự phân tán đáng kể trong quy mô tổng tài sản giữa các kỳ quan sát, dù vẫn nằm trong giới hạn hợp lý của biến động tài chính doanh nghiệp.

Tính (12) Skewness (độ lệch) & (13) Kurtosis (độ nhọn)

library(e1071)

# ----- (12) Skewness (độ lệch) & (13) Kurtosis (độ nhọn) -----
skew_tts <- skewness(data_ckv$tongtaisan, na.rm = TRUE)
kurt_tts <- kurtosis(data_ckv$tongtaisan, na.rm = TRUE)

data.frame(
  Chi_tieu = c("Skewness (Độ lệch)", "Kurtosis (Độ nhọn)"),
  Gia_tri = c(round(skew_tts, 3), round(kurt_tts, 3))
) %>%
  kbl(caption = "Bảng 3.6: Skewness & Kurtosis của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.6: Skewness & Kurtosis của Tổng tài sản
Chi_tieu Gia_tri
Skewness (Độ lệch) 0.212
Kurtosis (Độ nhọn) -1.270
  • Hàm skewness() đo lường mức độ đối xứng của phân bố dữ liệu so với giá trị trung bình.
  • Hàm kurtosis() đánh giá độ tập trung hoặc độ nhọn của phân bố dữ liệu.
  • Giá trị Skewness = 0,212 mang dấu dương, cho thấy phân bố tổng tài sản có xu hướng lệch nhẹ về bên phải, nghĩa là vẫn tồn tại một số kỳ có giá trị tài sản cao hơn trung bình nhưng không quá cực đoan.
  • Trong khi đó, Kurtosis = -1,270 có giá trị âm thể hiện phân bố tương đối phẳng hơn so với phân phối chuẩn (platykurtic), tức dữ liệu ít tập trung quanh trung bình và trải rộng hơn.
  • Nhìn chung, phân bố Tổng tài sản có mức độ cân đối khá tốt, không xuất hiện hiện tượng lệch hay tập trung cực đoan, cho phép sử dụng các phương pháp thống kê tham số trong các bước phân tích tiếp theo.

(14) Hệ số biến thiên (CV = sd/mean) của biến Tổng tài sản

# ----- (14) Hệ số biến thiên (CV = sd/mean) -----
cv_tts <- ifelse(mean_tts != 0, sd_tts / mean_tts, NA)

data.frame(
  Chi_tieu = "Hệ số biến thiên (CV)",
  Gia_tri = round(cv_tts, 3)
) %>%
  kbl(caption = "Bảng 3.7: Hệ số biến thiên của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.7: Hệ số biến thiên của Tổng tài sản
Chi_tieu Gia_tri
Hệ số biến thiên (CV) 0.163
  • sd() để tính độ lệch chuẩn – thể hiện mức độ dao động tuyệt đối của dữ liệu,
  • mean() để tính giá trị trung bình – biểu thị quy mô trung tâm của biến.
  • Hệ số biến thiên được tính bằng công thức CV = sd_tts / mean_tts, phản ánh mức độ biến động tương đối của Tổng tài sản so với giá trị trung bình. Câu lệnh ifelse() được dùng nhằm tránh lỗi chia cho 0 trong trường hợp trung bình bằng 0. -Giá trị CV = 0,163 (tương đương 16,3%) cho thấy mức độ biến động tương đối thấp của Tổng tài sản trong toàn bộ giai đoạn quan sát.
  • Điều này nghĩa là quy mô tài sản của doanh nghiệp ổn định và ít dao động so với trung bình chung, phản ánh sự kiểm soát tốt về cân đối tài chính và quản trị nguồn vốn.
  • Mức CV dưới 20% thường được xem là ổn định, qua đó cho thấy cấu trúc tài sản của doanh nghiệp không biến động mạnh theo chu kỳ kinh doanh, tạo nền tảng vững chắc cho các hoạt động mở rộng trong tương lai.

Trimmed mean

# ----- (15) Trimmed mean (bỏ 10% 2 đầu) -----
trim_mean_tts <- mean(data_ckv$tongtaisan, trim = 0.1, na.rm = TRUE)
data.frame(Chi_tieu = "Trimmed mean (10% hai đầu)", Gia_tri = fmt_vn(trim_mean_tts)) %>%
  kbl(caption = "Bảng 3.8: Trimmed mean của Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F) %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.8: Trimmed mean của Tổng tài sản
Chi_tieu Gia_tri
Trimmed mean (10% hai đầu) 176.669.687.500
  • Đoạn mã sử dụng hàm mean() với đối số trim = 0.1 để tính giá trị trung bình cắt tỉa (Trimmed Mean) của biến Tổng tài sản. Tham số trim = 0.1 nghĩa là loại bỏ 10% giá trị nhỏ nhất và 10% giá trị lớn nhất trong mẫu dữ liệu trước khi tính trung bình. Cách tính này giúp giảm ảnh hưởng của các giá trị ngoại lai (outliers), phản ánh xu hướng trung tâm ổn định hơn so với trung bình thông thường.
  • Giá trị Trimmed Mean đạt 176,67 nghìn tỷ đồng, thấp hơn nhẹ so với trung bình tổng thể (177,58 nghìn tỷ đồng). Điều này cho thấy một số giá trị cực đại của tổng tài sản đã kéo trung bình chung lên cao hơn thực tế, và sau khi loại bỏ 10% hai đầu, xu hướng trung tâm trở nên ổn định và phản ánh chính xác hơn quy mô tài sản thực tế của doanh nghiệp. Kết quả này khẳng định dữ liệu Tổng tài sản không bị chi phối mạnh bởi các ngoại lệ, và phân bố có tính cân đối tương đối tốt trong toàn bộ giai đoạn quan sát.

(16) Sai số chuẩn của trung bình (Standard Error)

# ----- (16) Sai số chuẩn của trung bình (Standard Error) -----
se_tts <- sd_tts / sqrt(n_obs)
data.frame(Chi_tieu = "Sai số chuẩn (SE) của trung bình", Gia_tri = fmt_vn(se_tts, digits = 4)) %>%
  kbl(caption = "Bảng 3.9: Sai số chuẩn của trung bình", align = "c") %>%
  kable_paper(full_width = F) %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.9: Sai số chuẩn của trung bình
Chi_tieu Gia_tri
Sai số chuẩn (SE) của trung bình 4.570.662.155
  • sd_tts là độ lệch chuẩn của biến Tổng tài sản, được tính bằng hàm sd().
  • n_obs là số lượng quan sát hợp lệ, thường xác định bằng sum(!is.na(data_ckv$tongtaisan)).
  • Công thức se_tts <- sd_tts / sqrt(n_obs) giúp đo lường độ chính xác của trung bình mẫu khi ước lượng cho tổng thể
  • Giá trị SE = 4,57 nghìn tỷ đồng cho thấy trung bình tổng tài sản có sai số nhỏ khi ước lượng cho toàn bộ tập dữ liệu. Điều này phản ánh độ tin cậy cao của trung bình mẫu, tức dữ liệu Tổng tài sản được phân bố khá ổn định, không bị biến động mạnh giữa các quan sát.
  • Nói cách khác, sai số chuẩn thấp chứng minh rằng giá trị trung bình 177,58 nghìn tỷ đồng được tính toán là đại diện tốt cho toàn bộ giai đoạn, tạo cơ sở vững chắc cho các phân tích hồi quy và so sánh xu hướng ở các phần tiếp theo.

Khoảng tin cậy 95% cho trung bình Tổng tài sản

# ----- (17) Khoảng tin cậy 95% cho trung bình (CI) -----
ci_lower <- mean_tts - 1.96 * se_tts
ci_upper <- mean_tts + 1.96 * se_tts
data.frame(
  Chi_tieu = c("CI 95% - Lower", "CI 95% - Upper"),
  Gia_tri = c(fmt_vn(ci_lower), fmt_vn(ci_upper))
) %>%
  kbl(caption = "Bảng 3.10: Khoảng tin cậy 95% cho trung bình Tổng tài sản", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.10: Khoảng tin cậy 95% cho trung bình Tổng tài sản
Chi_tieu Gia_tri
CI 95% - Lower 168.625.602.176
CI 95% - Upper 186.542.597.824
  • mean_tts là giá trị trung bình của biến Tổng tài sản.
  • se_tts là sai số chuẩn của trung bình (đã tính ở bước trước).
  • Hệ số 1.96 tương ứng với ngưỡng tin cậy 95% trong phân phối chuẩn.
  • Khoảng tin cậy 95% cho trung bình Tổng tài sản nằm trong khoảng từ 168,63 nghìn tỷ đến 186,54 nghìn tỷ đồng. Điều này có nghĩa là, với xác suất 95%, giá trị trung bình thực của tổng tài sản trong toàn bộ giai đoạn nằm trong khoảng trên.
  • Khoảng tin cậy hẹp cho thấy mức độ biến động thấp và độ tin cậy cao của ước lượng trung bình, phản ánh sự ổn định tương đối của quy mô tổng tài sản qua các năm. (18) Tính tăng trưởng theo năm
# ----- (18) Tính tăng trưởng theo quý -----
data_ckv <- data_ckv %>%
  arrange(quarter) %>%
  mutate(tangtruong_quy_pct = (tongtaisan - lag(tongtaisan)) / lag(tongtaisan) * 100)

# Hiển thị 10 dòng có cột tăng trưởng năm (dạng hiển thị)
data_ckv %>%
  select(quarter, tongtaisan, tangtruong_quy_pct) %>%
  mutate(tongtaisan = fmt_vn(tongtaisan),
         tangtruong_quy_pct = ifelse(is.na(tangtruong_quy_pct), "", paste0(round(tangtruong_quy_pct, 2), "%"))) %>%
  head(10) %>%
  kbl(caption = "Bảng 3.11: Tăng trưởng theo năm", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped")
Bảng 3.11: Tăng trưởng theo năm
quarter tongtaisan tangtruong_quy_pct
Q1/2015 214.042.000.000
Q1/2016 219.784.000.000 2.68%
Q1/2017 182.494.000.000 -16.97%
Q1/2018 231.446.000.000 26.82%
Q1/2019 178.206.000.000 -23%
Q1/2020 172.352.000.000 -3.28%
Q1/2021 145.838.000.000 -15.38%
Q1/2022 151.459.000.000 3.85%
Q1/2023 144.493.000.000 -4.6%
Q1/2024 158.888.000.000 9.96%
  • Hàm lag() giúp truy xuất giá trị Tổng tài sản của quý trước, nhờ đó dễ dàng so sánh biến động giữa các kỳ.
  • Qua bảng trên, Tổng tài sản của CKV có sự biến động mạnh qua các năm, thể hiện rõ tính chu kỳ trong hoạt động kinh doanh:
    • Các năm 2018 và 2024 ghi nhận tăng trưởng mạnh nhất (26,82% và 9,96%), cho thấy khả năng mở rộng quy mô tài sản hiệu quả.
    • Ngược lại, các năm 2017, 2019, và 2021 chứng kiến suy giảm đáng kể (giảm lần lượt 16,97%, 23,00%, và 15,38%), phản ánh những giai đoạn điều chỉnh hoặc khó khăn tài chính.
    • Trung bình giai đoạn 2015–2024, biên độ dao động tăng trưởng khá lớn, chứng tỏ tính ổn định chưa cao trong quản lý tổng tài sản. Xác định các quý có cú sốc: giảm mạnh nhất & tăng mạnh nhất
# ----- (19) Xác định các quý có cú sốc: giảm mạnh nhất & tăng mạnh nhất -----
shock_quy <- data_ckv %>%
  filter(!is.na(tangtruong_quy_pct)) %>%
  summarise(
    quy_giam_manh = quarter[which.min(tangtruong_quy_pct)],
    giam_manh_pct = min(tangtruong_quy_pct, na.rm = TRUE),
    quy_tang_manh = quarter[which.max(tangtruong_quy_pct)],
    tang_manh_pct = max(tangtruong_quy_pct, na.rm = TRUE)
  )

shock_table <- data.frame(
  Loai = c("Cú sốc giảm mạnh nhất", "Cú sốc tăng mạnh nhất"),
  Quy = c(shock_quy$quy_giam_manh, shock_quy$quy_tang_manh),
  Tang_truong = c(paste0(round(shock_quy$giam_manh_pct, 2), "%"), paste0(round(shock_quy$tang_manh_pct, 2), "%"))
)

shock_table %>%
  kbl(caption = "Bảng 3.12: Các quý có biến động mạnh nhất (giảm & tăng)", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.12: Các quý có biến động mạnh nhất (giảm & tăng)
Loai Quy Tang_truong
Cú sốc giảm mạnh nhất Q2/2017 -23.17%
Cú sốc tăng mạnh nhất Q4/2015 40.02%

Cụ thể, các hàm chính gồm: - filter(!is.na()): loại bỏ các quan sát trống (NA). - which.min() và which.max(): xác định vị trí của giá trị nhỏ nhất và lớn nhất trong chuỗi tốc độ tăng trưởng (tangtruong_quy_pct). - summarise() kết hợp với min() và max(): trích xuất ra quý có biến động cực trị cùng với tỷ lệ phần trăm tương ứng. Kết quả cho thấy biến động lớn nhất của Tổng tài sản trong giai đoạn 2015–2024 diễn ra ở hai thời điểm quan trọng: - Cú sốc giảm mạnh nhất xảy ra vào quý II/2017, khi Tổng tài sản giảm 23,17% so với quý trước đó. → Đây là giai đoạn suy giảm sâu nhất trong toàn bộ chuỗi thời gian, phản ánh khả năng thu hẹp quy mô hoạt động hoặc rút vốn đầu tư đáng kể của doanh nghiệp. - Cú sốc tăng mạnh nhất ghi nhận tại quý IV/2015, với mức tăng 40,02% so với quý III cùng năm. → Sự gia tăng đột biến này cho thấy sự mở rộng tài sản mạnh mẽ, có thể đến từ đầu tư bổ sung hoặc tái cấu trúc nguồn vốn nhằm nâng cao năng lực hoạt động.

# 1️⃣ Tính hệ số tương quan giữa các biến định lượng
cor_matrix <- data_ckv %>%
  select(tongtaisan, tongno, vcsh, quymo, tsnh, debt_to_equity) %>%
  cor(use = "pairwise.complete.obs")

# 2️⃣ Chuyển ma trận thành bảng gọn để hiển thị
cor_table <- as.data.frame(round(cor_matrix, 3))
colnames(cor_table) <- c("Tổng tài sản", "Tổng nợ", "Vốn CSH", "Quy mô", "TS ngắn hạn", "Nợ/VCSH")

# 3️⃣ Hiển thị bảng kết quả theo chuẩn kế toán Việt Nam
cor_table %>%
  kbl(caption = "Bảng 3.14: Ma trận hệ số tương quan giữa Tổng tài sản và các biến liên quan",
      align = "c", digits = 3) %>%
  kable_paper(full_width = FALSE, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.14: Ma trận hệ số tương quan giữa Tổng tài sản và các biến liên quan
Tổng tài sản Tổng nợ Vốn CSH Quy mô TS ngắn hạn Nợ/VCSH
tongtaisan 1.000 0.807 0.360 0.998 0.839 0.771
tongno 0.807 1.000 0.181 0.790 0.734 0.988
vcsh 0.360 0.181 1.000 0.372 0.121 0.029
quymo 0.998 0.790 0.372 1.000 0.839 0.753
tsnh 0.839 0.734 0.121 0.839 1.000 0.729
debt_to_equity 0.771 0.988 0.029 0.753 0.729 1.000
  • Đoạn mã sử dụng các hàm select() và cor() trong R để tính ma trận hệ số tương quan Pearson giữa biến trung tâm Tổng tài sản và các biến tài chính liên quan.
  • Hàm cor() đo lường mức độ và chiều hướng mối quan hệ tuyến tính giữa các cặp biến -Kết quả cho thấy Tổng tài sản (TTS) có mối tương quan rất chặt chẽ với Quy mô hoạt động (r = 0,998) và Tổng nợ (r = 0,807), phản ánh quy luật tăng đồng biến giữa quy mô tài sản và các khoản nợ. Ngoài ra, TTS tương quan cao với Tài sản ngắn hạn (r = 0,839), cho thấy phần lớn tài sản của doanh nghiệp nằm ở nhóm có tính thanh khoản cao.
  • Ngược lại, Vốn chủ sở hữu (r = 0,360) có tương quan yếu với Tổng tài sản, hàm ý rằng sự gia tăng quy mô tài sản chủ yếu đến từ đòn bẩy nợ hơn là vốn tự có.
  • Biến Tỷ lệ Nợ/VCSH (r = 0,771) cũng có mối liên hệ dương khá mạnh với Tổng tài sản, củng cố nhận định rằng doanh nghiệp sử dụng cấu trúc vốn thiên về nợ để mở rộng tài sản.

Ma trận hệ số tương quan giữa Tổng tài sản và các biến khác

# Tính ma trận tương quan cho 3 biến trọng tâm
cor_matrix <- data_ckv %>%
  select(tongtaisan, tongno, vcsh) %>%
  cor(use = "pairwise.complete.obs")

# Làm tròn và chuyển sang data frame
cor_table <- as.data.frame(round(cor_matrix, 3))

# Xuất bảng định dạng chuẩn
kbl(
  cor_table,
  caption = "Bảng 3.14: Ma trận hệ số tương quan giữa Tổng tài sản và các biến liên quan",
  align = "c",
  digits = 3,
  booktabs = TRUE
) %>%
  kable_paper(full_width = FALSE, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6", color = "black") %>%
  kable_styling(
    font_size = 12,
    bootstrap_options = c("hover", "condensed", "responsive"),
    position = "center",
    full_width = FALSE
  ) %>%
  column_spec(1, bold = TRUE, width = "5cm") %>%
  add_header_above(c(" " = 1, "Hệ số tương quan" = 3)) %>%  # Tổng cộng 4 cột (1 + 3)
  footnote(
    general = "Nguồn: Tính toán của tác giả từ dữ liệu CKV, làm tròn đến 3 chữ số thập phân.",
    general_title = "",
    threeparttable = TRUE
  )
Bảng 3.14: Ma trận hệ số tương quan giữa Tổng tài sản và các biến liên quan
Hệ số tương quan
tongtaisan tongno vcsh
tongtaisan 1.000 0.807 0.360
tongno 0.807 1.000 0.181
vcsh 0.360 0.181 1.000
Nguồn: Tính toán của tác giả từ dữ liệu CKV, làm tròn đến 3 chữ số thập phân.
  • Đoạn mã sau sử dụng hàm cor() trong R để tính ma trận hệ số tương quan Pearson giữa biến tổng tài sản (tongtaisan) và hai biến tài chính liên quan gồm tổng nợ (tongno) và vốn chủ sở hữu (vcsh). Kết quả được trình bày bằng bảng định dạng kable() và hiển thị theo chuẩn kế toán Việt Nam (dấu “.” phân cách hàng nghìn, dấu “,” cho phần thập phân).
  • Bảng 3.14 thể hiện ma trận hệ số tương quan Pearson giữa tổng tài sản và hai biến tài chính quan trọng khác gồm tổng nợ và vốn chủ sở hữu. Kết quả cho thấy:
    • Tổng tài sản và tổng nợ có hệ số tương quan r = 0.807, thể hiện mối tương quan thuận rất mạnh. Điều này hàm ý rằng khi tổng nợ tăng, quy mô tổng tài sản của doanh nghiệp cũng tăng theo, phản ánh việc sử dụng đòn bẩy tài chính đóng vai trò quan trọng trong việc mở rộng tài sản của CKV.
    • Tổng tài sản và vốn chủ sở hữu có hệ số tương quan r = 0.360, cho thấy mối tương quan thuận nhưng yếu hơn so với tổng nợ. Nghĩa là nguồn vốn tự có của doanh nghiệp có đóng góp tích cực vào quy mô tài sản, nhưng mức độ ảnh hưởng không mạnh bằng nợ vay.
    • Tổng nợ và vốn chủ sở hữu có tương quan thấp (r = 0.181), hàm ý rằng hai cấu phần vốn này biến động khá độc lập, thể hiện cấu trúc tài chính của CKV chủ yếu dựa vào nợ vay hơn là vốn góp từ chủ sở hữu.
# --- (5) Phân tích nhóm theo năm ---
group_year <- data_ckv %>%
  group_by(year) %>%
  summarise(
    Mean_TTS = mean(tongtaisan, na.rm = TRUE),
    Mean_Debt = mean(tongno, na.rm = TRUE),
    Mean_Equity = mean(vcsh, na.rm = TRUE),
  ) %>%
  mutate(across(everything(), fmt_vn))

group_year %>%
  kbl(caption = "Bảng 3.15: Trung bình các chỉ tiêu chính theo năm", align = "c") %>%
  kable_paper(full_width = F, lightable_options = "striped") %>%
  row_spec(0, bold = TRUE, background = "#BFD3E6")
Bảng 3.15: Trung bình các chỉ tiêu chính theo năm
year Mean_TTS Mean_Debt Mean_Equity
2015 203.336.250.000 121.129.750.000 80.256.064.786
2016 217.044.500.000 110.455.747.614 84.261.437.652
2017 187.756.500.000 102.744.826.926 85.020.705.340
2018 209.518.500.000 123.599.500.000 85.896.258.082
2019 187.943.250.000 100.934.234.701 87.008.768.258
2020 162.650.000.000 81.355.611.634 85.952.363.882
2021 153.277.000.000 74.160.417.348 82.370.405.875
2022 143.554.750.000 82.778.422.916 81.256.602.247
2023 144.185.250.000 64.572.213.149 79.613.003.180
2024 166.575.000.000 89.501.688.839 77.823.287.238
  • Diễn giải kết quả
    • Kết quả cho thấy, trong giai đoạn 2015–2024, tổng tài sản bình quân của CKV có xu hướng giảm dần, đặc biệt rõ từ năm 2019 đến 2022, phản ánh quá trình thu hẹp quy mô hoạt động hoặc tái cơ cấu danh mục tài sản.
    • Ngược lại, vốn chủ sở hữu duy trì mức tương đối ổn định quanh 80.000 tỷ đồng, trong khi tổng nợ bình quân giảm đáng kể từ hơn 121.000 tỷ (2015) xuống khoảng 65.000 tỷ (2023), thể hiện xu hướng giảm đòn bẩy tài chính và tăng tính tự chủ vốn của doanh nghiệp.
    • Đến năm 2024, tổng tài sản bình quân phục hồi nhẹ (tăng 15,5% so với 2023), cho thấy dấu hiệu mở rộng trở lại sau giai đoạn suy giảm.

2.4.2 Trức quan hóa biểu đ

# Tạo biến giai đoạn dịch
data_ckv <- data_ckv %>%
  mutate(
    giai_doan = ifelse(year <= 2019, "Trước dịch (≤2019)", "Sau dịch (≥2020)"),
    tongtaisan_ty = tongtaisan / 1e9,
    tongno_ty = tongno / 1e9,
    vonchusohuu_ty = vcsh / 1e9
  )
data_plot <- data_ckv %>%
  mutate(
    tongtaisan_ty = tongtaisan / 1e9,
    group = ifelse(year <= 2019, "Trước dịch", "Sau dịch") 
  )
# Hàm định dạng giá trị VN (tỷ VND)
fmt_ty <- function(x) paste0(comma(round(x, 0)))
theme_ckv <- theme_minimal(base_size = 13) +
  theme(panel.grid.minor = element_blank(),
        panel.grid.major.x = element_blank(),
        legend.position = "bottom",
        plot.title = element_text(face = "bold"))
ggplot(data_ckv, aes(x = year, y = tangtruongtts_, group = 1)) +
  geom_line(color = "purple", linewidth = 1) +
  geom_point(color = "darkorange") +
  labs(title = "Biểu đồ Tốc độ tăng trưởng Tổng tài sản (%)",
       x = "Quý", y = "Tăng trưởng (%)") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Quan sát biểu đồ cho thấy:

  • Giai đoạn 2015–2018, tốc độ tăng trưởng tổng tài sản của CKV duy trì xu hướng dương và khá ổn định, dao động quanh mức 0.05–0.15%, cho thấy doanh nghiệp vẫn trong thời kỳ mở rộng quy mô.
  • Năm 2019, tốc độ tăng trưởng giảm mạnh, thậm chí rơi xuống mức âm (≈ -0.2%), phản ánh giai đoạn khó khăn trước thời kỳ dịch bệnh, có thể do hoạt động đầu tư và tín dụng bị chững lại.
  • Từ năm 2020–2022, tốc độ tăng trưởng tổng tài sản dao động mạnh, xen kẽ giữa các năm tăng và giảm, phản ánh tác động bất ổn của đại dịch COVID-19 đến hoạt động kinh doanh.
  • Giai đoạn 2023–2024, tốc độ tăng trưởng hồi phục rõ rệt, đạt mức cao nhất khoảng 0.3% vào năm 2024, cho thấy CKV đã phục hồi quy mô tài sản và củng cố năng lực tài chính sau dịch.
ggplot(data_ckv, aes(x = year, y = tongno, group = 1)) +
  geom_line(color = "orange", linewidth = 1) +
  geom_point(color = "brown", size = 2) +
  labs(
    title = "Biểu đồ biến động Tổng nợ theo năm (2015–2024)",
    x = "Thời gian (Quý)",
    y = "Tổng nợ (VND)"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
  • Quan sát dữ liệu cho thấy:

    • Giai đoạn 2015–2017, tổng nợ tăng mạnh, đặc biệt trong năm 2016 và 2017, đạt đỉnh gần 1.5×10¹¹ VND, cho thấy CKV đẩy mạnh vay nợ để mở rộng hoạt động kinh doanh hoặc đầu tư tài sản cố định.
    • Từ năm 2018–2020, tổng nợ giảm dần, cho thấy doanh nghiệp bắt đầu kiểm soát đòn bẩy tài chính, hạn chế tăng trưởng nợ mới và có thể đã tiến hành tái cơ cấu nguồn vốn trong bối cảnh thị trường biến động.
    • Giai đoạn 2020–2022, tổng nợ ổn định quanh mức 1.0×10¹¹ VND, phản ánh sự thận trọng trong huy động vốn trong thời kỳ dịch COVID-19, khi rủi ro thị trường và chi phí vốn tăng cao.
    • Đến năm 2023–2024, tổng nợ có dấu hiệu tăng nhẹ trở lại, thể hiện nỗ lực phục hồi hoạt động đầu tư và mở rộng quy mô tài sản sau dịch.
ggplot(data_ckv, aes(x = year)) +
  geom_line(aes(y = tongtaisan, color = "Tổng tài sản"), linewidth = 1) +
  geom_line(aes(y = tongno, color = "Tổng nợ"), linewidth = 1, linetype = "dashed") +
  labs(
    title = "Biến động Tổng tài sản và Tổng nợ (2015–2024)",
    x = "Thời gian (Quý)",
    y = "Giá trị (VND)",
    color = "Chỉ tiêu"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 25, hjust = 1))

Quan sát cho thấy:

  • Tổng tài sản (màu xanh) luôn cao hơn đáng kể so với Tổng nợ (màu đỏ) trong toàn bộ giai đoạn, cho thấy CKV duy trì được khả năng thanh toán dài hạn ổn định, và vốn chủ sở hữu chiếm tỷ trọng đáng kể trong tổng nguồn vốn.
  • Từ năm 2015 đến 2017, cả tổng tài sản và tổng nợ đều tăng, cho thấy giai đoạn mở rộng hoạt động đầu tư, trong đó nguồn vốn vay đóng vai trò hỗ trợ tăng trưởng tài sản.
  • Giai đoạn 2018–2020, hai đường biểu diễn đồng loạt giảm, phản ánh xu hướng thu hẹp quy mô đầu tư hoặc tái cơ cấu tài sản. Điều này có thể gắn liền với chính sách giảm nợ để cải thiện khả năng thanh toán.
  • Từ 2021–2024, tổng tài sản ổn định quanh mức 1.8×10¹¹ – 2.0×10¹¹ VND, trong khi tổng nợ duy trì ở mức thấp hơn rõ rệt (~1.0×10¹¹ VND), thể hiện xu hướng kiểm soát rủi ro tài chính và tối ưu hóa cấu trúc vốn.
library(ggrepel)
ggplot(data_ckv, aes(x = tongtaisan, y = tongno)) +
  geom_point(color = "#1F77B4", size = 3, alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE, color = "red", linewidth = 1) +
  geom_text_repel(aes(label = year), size = 3, color = "black") +
  labs(
    title = "Biểu đồ tương quan giữa TTS và TN",
    subtitle = "Giai đoạn 20215-2024",
    x = "Tổng tài sản (tỷ VND)",
    y = "Tổng nợ (tỷ VND)",
    caption = "Nguồn: Dữ liệu CKV"
  ) +
  theme_minimal(base_size = 12)

Kết quả cho thấy:

  • Hai biến Tổng tài sản và Tổng nợ có mối tương quan thuận rất chặt chẽ, thể hiện qua độ dốc lớn và sự phân bố gần sát quanh đường hồi quy. Điều này chứng tỏ mỗi khi quy mô tài sản tăng, doanh nghiệp cũng đồng thời mở rộng quy mô vay nợ để tài trợ cho hoạt động đầu tư.
  • Các năm 2016 và 2023 là hai điểm lệch nhẹ khỏi xu hướng chung, gợi ý rằng trong những năm này CKV có thể đã điều chỉnh cơ cấu vốn, chẳng hạn giảm sử dụng đòn bẩy tài chính hoặc tăng vốn chủ sở hữu.
  • Giai đoạn 2015–2018, các điểm dữ liệu nằm ở phía trên đường hồi quy, cho thấy tỷ trọng nợ trên tài sản cao hơn trung bình — đây là giai đoạn CKV đẩy mạnh huy động vốn vay.
  • Ngược lại, từ 2019–2024, các điểm dần dịch xuống dưới đường hồi quy, thể hiện xu hướng giảm dần tỷ lệ nợ, hàm ý doanh nghiệp tăng cường tự chủ tài chính và tối ưu hóa cơ cấu nguồn vốn.

  1. :alnum:↩︎

  2. :alnum:↩︎