LỜI CẢM ƠN

Nhóm thực hiện xin gửi lời cảm ơn sâu sắc đến giảng viên hướng dẫn ThS.Trần Mạnh Tường đã tận tình hướng dẫn và giải đáp những thắc mắc trong suốt quá trình thực hiện đề tài. Vì sự hạn chế về kiến thức cũng như kỹ năng nên đề tài còn những thiếu sót. Kính mong thầy đánh giá và đóng góp cải thiện để đề tài được hoàn thiện hơn trong tương lai.

1 PHẦN 1: PHÂN TÍCH HÀNH VI MUA SẮM CỦA KHÁCH HÀNG THEO ĐẶC ĐIỂM NHÂN KHẨU HỌC

1.1 1.1 Giới thiệu và mã hóa dữ liệu

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

1.1.1.1 1.1.1.1Các gói dữ liệu

library(readxl)
## Warning: package 'readxl' was built under R version 4.4.3
library(data.table)
## Warning: package 'data.table' was built under R version 4.4.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.4.3
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:data.table':
## 
##     between, first, last
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr) 
## Warning: package 'tidyr' was built under R version 4.4.3

Giải thích

Các gói thư viện được cài đặt nhằm hỗ trợ toàn bộ quy trình xử lý và trực quan hóa dữ liệu.

  • readxl dùng để nhập dữ liệu Excel vào R, đảm bảo giữ nguyên cấu trúc gốc.

  • data.table hỗ trợ thao tác dữ liệu lớn với tốc độ cao.

  • dplyr cung cấp cú pháp trực quan cho các bước biến đổi và tổng hợp dữ liệu.

  • tidyr giúp định dạng và chuẩn hóa dữ liệu theo chuẩn tidy data.

1.1.1.2 1.1.1.2 Tổng quan dữ liệu

# 1. Đọc dữ liệu
file_path <- "customers_data.xlsx"
data <- read_excel(file_path)

# 2. Xem tổng quan kích thước dữ liệu
num_rows <- nrow(data)     # số quan sát
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)
##  [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

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.

1.1.1.3 1.1.1.3 Kiểu dữ liệu

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

Giải thích

Hàm sapply(data, class) liệt kê kiểu dữ liệu của từng cột. Kết quả cho thấy các biến định lượng (id, age, income, purchase_amount, promotion_usage, satisfaction_score) được lưu ở dạng số (numeric), trong khi các biến mô tả/danh mục (gender, education, region, loyalty_status, purchase_frequency, product_category) hiện dưới dạng ký tự (character), biến định tính được quy ước thành factor khi cần chạy mô hình phân loại hoặc sinh các báo cáo tần suất; biến định lượng giữ ở numeric để thực hiện các thống kê mô tả và hồi quy.

1.1.1.4 1.1.1.4 Kiểm tra giá trị bị thiếu

# 5. Kiểm tra giá trị thiếu
missing_total <- sum(is.na(data))
missing_by_var <- colSums(is.na(data))
cat("Tổng số giá trị thiếu:", missing_total, "\n")
## Tổng số giá trị thiếu: 0
missing_by_var
##                 id                age             gender             income 
##                  0                  0                  0                  0 
##          education             region     loyalty_status purchase_frequency 
##                  0                  0                  0                  0 
##    purchase_amount   product_category    promotion_usage satisfaction_score 
##                  0                  0                  0                  0

Giải thích

Kiểm tra missing với sum(is.na(data)) và colSums(is.na(data)) trả về tổng số giá trị thiếu bằng 0 và từng biến không có giá trị NA. Đảm bảo dữ liệu sạch và có thể xử lý

1.1.1.5 1.1.1.5 Kiểm tra trùng lặp

# 6. Kiểm tra các dòng bị trùng lặp 
dup_total <- sum(duplicated(data))
cat("Số quan sát trùng lặp:", dup_total, "\n")
## Số quan sát trùng lặp: 0

Giải thích

Kiểm tra trùng lặp với duplicated(data) cho kết quả 0 dòng trùng. Điều này khẳng định mỗi hàng đại diện cho một quan sát khách hàng duy nhất — tránh sai lệch do lặp dữ liệu khi tính tần suất, trung bình.

1.1.1.6 1.1.1.6 Kiểm tra cấu trức dữ liệu

# 7. Kiểm tra cấu trúc chi tiết của dữ liệu
str(data)   # Xem cấu trúc: tên biến, kiểu dữ liệu, ví dụ giá trị
## tibble [100,000 × 12] (S3: tbl_df/tbl/data.frame)
##  $ id                : num [1:100000] 1 2 3 4 5 6 7 8 9 10 ...
##  $ age               : num [1:100000] 27 29 37 30 31 38 32 24 27 28 ...
##  $ gender            : chr [1:100000] "Male" "Male" "Male" "Male" ...
##  $ income            : num [1:100000] 40682 15317 38849 11568 46952 ...
##  $ education         : chr [1:100000] "Bachelor" "Masters" "Bachelor" "HighSchool" ...
##  $ region            : chr [1:100000] "East" "West" "West" "South" ...
##  $ loyalty_status    : chr [1:100000] "Gold" "Regular" "Silver" "Regular" ...
##  $ purchase_frequency: chr [1:100000] "frequent" "rare" "rare" "frequent" ...
##  $ purchase_amount   : num [1:100000] 18249 4557 11822 4098 19685 ...
##  $ product_category  : chr [1:100000] "Books" "Clothing" "Clothing" "Food" ...
##  $ promotion_usage   : num [1:100000] 0 1 0 0 1 0 0 0 0 0 ...
##  $ satisfaction_score: num [1:100000] 6 6 6 7 5 5 7 5 5 6 ...

Giải thích

Hàm str(data) trình bày cấu trúc: số quan sát 100.000 và 12 biến, cùng kiểu dữ liệu và vài giá trị mẫu. Thông tin này giúp rà soát nhanh các vấn đề thường gặp (ví dụ biến số được đọc nhầm kiểu) và cung cấp ngữ cảnh (kích thước mẫu lớn — phù hợp cho kiểm định thống kê có công suất cao và phân tích phân nhóm).

1.1.1.7 1.1.1.7 Kiểm tra khoảng trắng và dữ liệu rỗng

# 9. Kiểm tra dữ liệu rỗng hoặc khoảng trắng trong biến ký tự
char_vars <- names(Filter(is.character, data))
for (v in char_vars) {
  empty_count <- sum(trimws(data[[v]]) == "", na.rm = TRUE)
  cat("Biến", v, "có", empty_count, "giá trị rỗng\n")
}
## Biến gender có 0 giá trị rỗng
## Biến education có 0 giá trị rỗng
## Biến region có 0 giá trị rỗng
## Biến loyalty_status có 0 giá trị rỗng
## Biến purchase_frequency có 0 giá trị rỗng
## Biến product_category có 0 giá trị rỗng

Giải thích

Kiểm tra khoảng trắng/chuỗi rỗng trên các biến character bằng trimws() cho thấy không có ô rỗng. Điều này đảm bảo các thao tác group_by, join hoặc phân loại theo chuỗi sẽ không bị ảnh hưởng bởi giá trị rỗng ngụy trang dưới dạng chuỗi chỉ chứa khoảng trắng.

1.1.1.8 1.1.1.8 Xem 10 dòng đầu tiên

# 10. Xem 10 dòng đầu của dữ liệu
head(data, 10)
## # A tibble: 10 × 12
##       id   age gender income education  region loyalty_status purchase_frequency
##    <dbl> <dbl> <chr>   <dbl> <chr>      <chr>  <chr>          <chr>             
##  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              
## # ℹ 4 more variables: purchase_amount <dbl>, product_category <chr>,
## #   promotion_usage <dbl>, satisfaction_score <dbl>

Giải thích

Hiển thị 10 dòng đầu giúp xác nhận trực quan: các biến đăng ký giá trị hợp lý (ví dụ age nằm trong khoảng 12–49; income, purchase_amount là các giá trị dương; promotion_usage ở dạng 0/1). Bước này giúp kiểm tra trước khi bước vào mã hóa và phân tích dữ liệu

1.1.2 1.1.2 Mã hóa dữ liệu

1.1.2.1 1.1.2.1 Chuẩn hóa và chuyển đổi kiểu dữ liệu

library(knitr)
## Warning: package 'knitr' was built under R version 4.4.3
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.4.3
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
#1.. Chuẩn hóa tên biến về dạng chuẩn
names(data) <- trimws(names(data))
names(data) <- gsub(" ", "_", names(data))
names(data) <- tolower(names(data))

#2. Loại bỏ trùng lặp
data <- distinct(data)

#3. Chuyển kiểu dữ liệu
data$region <- as.factor(data$region)
data$loyalty_status <- as.factor(data$loyalty_status)
data$promotion_usage <- as.factor(data$promotion_usage)
data$income <- as.numeric(data$income)
data$purchase_amount <- as.numeric(data$purchase_amount)
str(data)
## tibble [100,000 × 12] (S3: tbl_df/tbl/data.frame)
##  $ id                : num [1:100000] 1 2 3 4 5 6 7 8 9 10 ...
##  $ age               : num [1:100000] 27 29 37 30 31 38 32 24 27 28 ...
##  $ gender            : chr [1:100000] "Male" "Male" "Male" "Male" ...
##  $ income            : num [1:100000] 40682 15317 38849 11568 46952 ...
##  $ education         : chr [1:100000] "Bachelor" "Masters" "Bachelor" "HighSchool" ...
##  $ region            : Factor w/ 4 levels "East","North",..: 1 4 4 3 2 3 3 2 1 2 ...
##  $ loyalty_status    : Factor w/ 3 levels "Gold","Regular",..: 1 2 3 2 2 3 3 2 2 2 ...
##  $ purchase_frequency: chr [1:100000] "frequent" "rare" "rare" "frequent" ...
##  $ purchase_amount   : num [1:100000] 18249 4557 11822 4098 19685 ...
##  $ product_category  : chr [1:100000] "Books" "Clothing" "Clothing" "Food" ...
##  $ promotion_usage   : Factor w/ 2 levels "0","1": 1 2 1 1 2 1 1 1 1 1 ...
##  $ satisfaction_score: num [1:100000] 6 6 6 7 5 5 7 5 5 6 ...

Giải thích

Sau khi nhập dữ liệu, nhóm nghiên cứu tiến hành kiểm tra, chuẩn hóa dữ liệu, loại bỏ trùng lặp và chuyển đổi kiểu dữ liệu của các biến để đảm bảo tính phù hợp cho các bước phân tích thống kê và mô hình hóa. Kết quả cho thấy bộ dữ liệu bao gồm 100.000 quan sát và 12 biến, trong đó:

  • Các biến định danh và định lượng như id, age, income, purchase_amount, satisfaction_score được lưu dưới dạng integer hoặc numeric, thuận lợi cho việc tính toán thống kê và hồi quy.
  • Các biến phân loại như gender, region, loyalty_status và promotion_usage được chuyển sang kiểu factor, giúp mô hình nhận diện rõ các nhóm giá trị rời rạc.
  • Các biến mô tả như education, purchase_frequency và product_category giữ nguyên kiểu character, phù hợp cho các thao tác lọc, nhóm hoặc trực quan hóa sau này.

1.1.2.2 1.1.2.2 Bảng mô tả biến

library(knitr)
library(kableExtra)
library(dplyr)

# 4. Tạo bảng mô tả với 4 cộ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"
  )
)

# 5. Hiển thị bảng đẹp
variable_description %>%
  kbl(caption = "Bảng 1. Mô tả các biến trong bộ dữ liệu") %>%
  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

Giải thích

Việc tạo bảng giúp kiểm tra các biến định lượng và định tính một cách trực quan hơn và bảng hiện thị như sau:

  • Biến định tính bao gồm 6 biến : gender, education, region, loyalty_status, product_category, promotion_usage, purchase_frequenc.

  • Biến định lượng bao gồm: age, income, purchase_amount, satisfaction_score

1.1.2.3 1.1.2.3 Chuẩn hóa, làm sạch dữ liệu và tạo biến mới AgeGroup

#6. Chuẩn hóa tên cột
names(data) <- tolower(names(data))
#7. Loại bỏ giá trị âm hoặc 0 trong purchase_amount
data <- filter(data, purchase_amount > 0)
# loại bỏ giá trị NA

#8.  Mã hóa promotion_usage thành nhãn
data$promotion_usage <- factor(data$promotion_usage,
                               levels = c(0, 1),
                               labels = c("No", "Yes"))
#9. Tạo biến mới: thu nhập trung bình theo khu vực
region_income <- data %>%
  group_by(region) %>%
  summarise(mean_income = mean(income, na.rm = TRUE))
#10. Chuẩn hóa income
data$income_scaled <- scale(data$income)


#11.  Tạo biến nhóm tuổi (AgeGroup) từ biến age
data <- data %>%
  mutate(
    AgeGroup = case_when(
      age >= 12 & age <= 17 ~ "12-17",
      age >= 18 & age <= 24 ~ "18-24",
      age >= 25 & age <= 34 ~ "25-34",
      age >= 35 & age <= 49 ~ "35-49",
      age > 49 ~ "Trên 49",
      age < 12 ~ "Dưới 12",
      TRUE ~ NA_character_     # trường hợp còn lại
    )
  )


# 12.  #Loại bỏ NA trong nhóm tuổi
data_age <- data %>%
  filter(!is.na(AgeGroup), AgeGroup != "NA", AgeGroup != "")

Giải thích

Các bước xử lý được thực hiện nhằm chuẩn hóa và làm sạch dữ liệu, đảm bảo tính chính xác, nhất quán và sẵn sàng cho phân tích.

  • Đầu tiên, việc chuyển toàn bộ tên cột về chữ thường giúp thống nhất định dạng và hạn chế lỗi khi thao tác với dữ liệu.

  • Tiếp đó, các quan sát có giá trị purchase_amount bằng hoặc nhỏ hơn 0 được loại bỏ để đảm bảo tính hợp lý của biến chi tiêu, đồng thời mã hóa biến promotion_usage thành dạng nhãn (“No”, “Yes”) giúp dễ dàng diễn giải trong thống kê và mô hình hóa.

  • Việc tính thu nhập trung bình theo từng khu vực (region_income) cho phép nhận diện sự khác biệt về mức sống giữa các vùng, hỗ trợ cho các phân tích so sánh và chiến lược thị trường.

  • Biến income được chuẩn hóa bằng hàm scale() để đưa về cùng thang đo, phục vụ cho các mô hình thống kê đa biến.

  • Biến age được phân nhóm thành AgeGroup giúp mô tả hành vi tiêu dùng theo độ tuổi — một yếu tố nhân khẩu học quan trọng trong nghiên cứu hành vi khách hàng.

  • Cuối cùng, việc loại bỏ các giá trị NA trong AgeGroup hoàn thiện quá trình làm sạch, giúp dữ liệu đạt tính toàn vẹn và đáng tin cậy.

Nhìn chung, chuỗi thao tác này giúp chuẩn hóa cấu trúc, xử lý bất thường và tạo ra các biến có ý nghĩa thống kê – thực tiễn cao, từ đó hình thành nền tảng vững chắc cho các phân tích mô tả, hồi quy và dự báo ở các bước tiếp theo.

1.2 1.2 Phân tích dữ liệu

1.2.1 1.2.1 Các thống kê cơ bản

1.2.1.1 1.2.1.1 Tóm tắt thống kê biến income

# 2. Tóm tắt thống kê biến income
summary(data$income)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    5000   16272   27585   27516   38747   50000

Giải thích

Kết quả thống kê mô tả cho thấy bộ dữ liệu gồm 100.000 quan sát với đặc điểm nhân khẩu và hành vi tiêu dùng đa dạng. Biến age dao động từ 12–49, trung bình 30 tuổi, phản ánh nhóm khách hàng chủ yếu là người trẻ. Thu nhập (income) trung bình 27.516, trải rộng từ 5.000–50.000, cho thấy sự khác biệt đáng kể giữa các cá nhân. Chi tiêu (purchase_amount) trung bình 9.635, gần với trung vị 9.452, thể hiện phân phối chi tiêu tương đối cân đối.

Về biến định tính, miền East chiếm tỷ lệ lớn nhất trong region, trong khi nhóm khách hàng “Regular” chiếm ưu thế trong loyalty_status. Khoảng 30% khách hàng có sử dụng khuyến mãi (promotion_usage), cho thấy hoạt động marketing có mức tiếp cận khá tốt. Điểm hài lòng trung bình 5.01 phản ánh mức độ hài lòng trung bình. Biến income_scaled được chuẩn hóa chính xác, và AgeGroup được hình thành đầy đủ, tạo nền tảng cho các phân tích so sánh nhóm ở các bước sau.

Tính trung bình thu nhập

# 3. Tính trung bình thu nhập khách hàng 
mean(data$income, na.rm = TRUE)
## [1] 27516.27

Giải thích

Giá trị thu nhập trung bình của khách hàng là 27.516,27, phản ánh mức thu nhập bình quân trong mẫu quan sát. Mức này nằm trong khoảng giữa của dải thu nhập (từ 5.000 đến 50.000), cho thấy dữ liệu có sự phân bố tương đối cân bằng, không bị lệch mạnh về phía các giá trị cực đoan.

Tính trung vị thu nhập

# 4. Tính trung vị thu nhập 
median(data$income, na.rm = TRUE)
## [1] 27584.5

Giải thích

Giá trị trung vị của thu nhập là 27.584,5, gần tương đương với giá trị trung bình (27.516,27). Điều này cho thấy phân phối thu nhập của khách hàng tương đối đối xứng, không xuất hiện hiện tượng lệch mạn.

Độ lệch chuẩn thu nhập

# 5. Tính độ lệch chuẩn thu nhập 
sd(data$income, na.rm = TRUE)
## [1] 12996.78

Giải thích

Độ lệch chuẩn của thu nhập là 12.996,78, cho thấy mức độ phân tán khá lớn quanh giá trị trung bình 27.516,27. Điều này phản ánh sự chênh lệch đáng kể giữa các nhóm khách hàng về khả năng thu nhập, tức là trong tập dữ liệu có cả nhóm thu nhập thấp và nhóm thu nhập cao cùng tồn tại.

Phương sai thu nhập

# 6. Tính phương sai thu nhập
var(data$income, na.rm = TRUE)
## [1] 168916358

Giải thích

Phương sai của thu nhập là 168.916.358, thể hiện mức độ biến thiên rất lớn quanh giá trị trung bình. Do phương sai là bình phương của độ lệch chuẩn, kết quả cao cho thấy dữ liệu thu nhập có sự phân tán rộng, phản ánh sự khác biệt rõ rệt giữa các cá nhân trong mẫu khảo sát.

Tứ phân vị thu nhập

# 8. Tính tứ phân vị thu nhập 
quantile(data$income, probs = c(0.25, 0.5, 0.75), na.rm = TRUE)
##      25%      50%      75% 
## 16271.75 27584.50 38747.25

Giải thích

Kết quả tứ phân vị cho thấy: Q1 = 16.271,75, Q2 (trung vị) = 27.584,50, và Q3 = 38.747,25. Khoảng giữa của phân phối (Q1–Q3) rộng khoảng 22.475 đơn vị, phản ánh sự chênh lệch đáng kể giữa nhóm thu nhập thấp và cao. Phần lớn khách hàng có thu nhập tập trung trong khoảng 16.000–39.000, tức là thuộc nhóm thu nhập trung bình, trong khi các giá trị ngoài khoảng này có thể được xem là nhóm ngoại biên.

Hệ số biến thiên (CV) của thu nhập

# 9. Tính hệ số biến thiên (CV) của thu nhập
cv_income <- sd(data$income, na.rm = TRUE) / mean(data$income, na.rm = TRUE)
cv_income
## [1] 0.4723308

Giải thích

Hệ số biến thiên (CV) của thu nhập là 0,4723, tương đương 47,23%, thể hiện mức độ dao động tương đối cao của thu nhập so với giá trị trung bình. Chỉ số này cho thấy thu nhập của khách hàng có sự phân tán đáng kể — mức thu nhập giữa các cá nhân chênh lệch gần một nửa so với trung bình chung. Về mặt học thuật, CV > 0,3 thường được xem là mức biến động lớn, hàm ý phân phối thu nhập không đồng đều.

Tần suất từng nhóm giới tính

# 10. Tần suất từng nhóm giới tính 
table(data$gender)
## 
## Female   Male 
##  50074  49926

Giải thích

Kết quả cho thấy bộ dữ liệu gồm 50.074 khách hàng nữ (50,07%) và 49.926 khách hàng nam (49,93%), thể hiện sự phân bố giới tính gần như cân bằng. Về mặt thống kê, tỷ lệ này đảm bảo tính đại diện cao và hạn chế sai lệch giới trong mẫu dữ liệu.

Thống kê theo vùng

# 12. Thống kê theo vùng (Region) 
table(data$region)
## 
##  East North South  West 
## 30074 19918 20073 29935

Giải thích

Kết quả cho thấy sự phân bố khách hàng theo khu vực tương đối đồng đều: East (30,074 customers), West (29,935), South (20,073) và North (19,918). Khu vực East chiếm tỷ lệ cao nhất (≈30%), trong khi North có số lượng khách hàng thấp hơn đôi chút. Sự phân bố này phản ánh phạm vi bao phủ thị trường rộng và cân đối giữa các vùng, giúp dữ liệu đảm bảo tính đại diện và hạn chế sai lệch địa lý.

Trung bình thu nhập theo giới tính

# 13. Trung bình thu nhập theo giới tính
aggregate(income ~ gender, data = data, FUN = mean)
##   gender   income
## 1 Female 27507.95
## 2   Male 27524.62

Giải thích

Kết quả cho thấy thu nhập trung bình của nam (27.524,62) và nữ (27.507,95) gần như tương đương nhau, chênh lệch không đáng kể. Về mặt thống kê, điều này cho thấy phân bố thu nhập giữa hai giới tính tương đối cân bằng, không có dấu hiệu sai lệch lớn hay bất bình đẳng thu nhập rõ rệt trong mẫu dữ liệu.

Trung bình thu nhập theo độ tuổi

# 14. Trung bình thu nhập theo độ tuổi (Age Group)
aggregate(income ~ AgeGroup, data = data, FUN = mean)
##   AgeGroup   income
## 1    12-17 28966.86
## 2    18-24 27519.91
## 3    25-34 27517.88
## 4    35-49 27481.73

Giải thích

Kết quả cho thấy thu nhập trung bình giữa các nhóm tuổi có sự khác biệt nhẹ: nhóm 12–17 tuổi có mức trung bình cao nhất (28.966,86), tiếp đến là các nhóm 18–24 (27.519,91), 25–34 (27.517,88) và 35–49 (27.481,73). Mức chênh lệch giữa các nhóm tương đối nhỏ, cho thấy thu nhập phân bố khá đồng đều theo độ tuổi.

Trung bình chi tiêu theo giới tính

# 15. Trung bình chi tiêu theo giới tính 
aggregate(purchase_amount ~ gender, data = data, FUN = mean)
##   gender purchase_amount
## 1 Female        9634.405
## 2   Male        9635.178

Giải thích

Kết quả cho thấy chi tiêu trung bình của nữ (9.634,41) và nam (9.635,18) gần như tương đương, với mức chênh lệch không đáng kể. Điều này cho thấy giới tính không phải là yếu tố ảnh hưởng rõ rệt đến mức chi tiêu trung bình trong mẫu dữ liệu.

Phân phối chuẩn của thu nhập

# 17. Kiểm tra phân phối chuẩn của thu nhập
shapiro.test(sample(data$income, 500))  # Lấy mẫu 500 để test
## 
##  Shapiro-Wilk normality test
## 
## data:  sample(data$income, 500)
## W = 0.9472, p-value = 2.322e-12

Giải thích

Kết quả kiểm định Shapiro–Wilk cho thấy W = 0,9609 và p-value = 2,947×10⁻¹⁰, nhỏ hơn 0,05, do đó bác bỏ giả thuyết H0 về phân phối chuẩn. Điều này nghĩa là thu nhập của khách hàng không tuân theo phân phối chuẩn, mà có thể bị lệch hoặc có đuôi dài do sự chênh lệch giữa các nhóm thu nhập.

Thống kê tần suất khách hàng theo nhóm tuổi

# 18. Thống kê tần suất khách hàng theo nhóm tuổi 
table(data$AgeGroup)
## 
## 12-17 18-24 25-34 35-49 
##   268 10622 73316 15794

Giải thích

Kết quả thống kê cho thấy phân bố khách hàng theo nhóm tuổi không đồng đều: nhóm 25–34 chiếm tỷ lệ áp đảo với 73.316 người, tiếp đến là 35–49 (15.794), 18–24 (10.622), và 12–17 (268). Sự chênh lệch lớn này cho thấy mẫu dữ liệu tập trung chủ yếu vào nhóm lao động trẻ và trung niên – nhóm có sức mua và tần suất tiêu dùng cao nhất, phản ánh cấu trúc khách hàng thực tế của nhiều doanh nghiệp hiện nay.

Tổng chi tiêu trung bình của toàn bộ khách hàng

# 19. Tính tổng chi tiêu trung bình của toàn bộ khách hàng-
mean(data$purchase_amount, na.rm = TRUE)
## [1] 9634.791

Giải thích

Giá trị chi tiêu trung bình của toàn bộ khách hàng là 9.634,79, cho thấy mức chi tiêu ổn định quanh mốc 10.000. Sự ổn định này gợi ý rằng hành vi mua sắm của khách hàng trong mẫu có tính đồng nhất tương đối, không bị chi phối mạnh bởi các ngoại lệ.

So sánh chi tiêu trung bình giữa các vùng

# 20. So sánh chi tiêu trung bình giữa các vùng 
aggregate(purchase_amount ~ region, data = data, FUN = mean)
##   region purchase_amount
## 1   East        9615.411
## 2  North        9673.362
## 3  South        9648.787
## 4   West        9619.211

Giải thích

Chi tiêu trung bình giữa các khu vực khá đồng đều: North (9.673,36), South (9.648,79), West (9.619,21) và East (9.615,41). Mức chênh lệch nhỏ cho thấy hành vi chi tiêu của khách hàng tương đối thống nhất trên các vùng địa lý, không có khu vực nào chi tiêu vượt trội rõ rệt.

Số lượng khách hàng mỗi nhóm thu nhập

# 21. Đếm số lượng khách hàng mỗi nhóm thu nhập
table(cut(data$income, breaks = 5))
## 
## (4.95e+03,1.4e+04]  (1.4e+04,2.3e+04]  (2.3e+04,3.2e+04]  (3.2e+04,4.1e+04] 
##              20061              19816              20007              20206 
##    (4.1e+04,5e+04] 
##              19910

Giải thích

Dữ liệu được chia thành 5 nhóm thu nhập có số lượng khách hàng tương đối đồng đều, dao động quanh 19.000–20.000 người mỗi nhóm. Phân bố này cho thấy mẫu dữ liệu được xây dựng cân bằng giữa các mức thu nhập, không bị thiên lệch mạnh về nhóm thấp hay cao.

Phân phối thu nhập theo giới tính

# 22. Phân phối thu nhập theo giới tính
aggregate(income ~ gender, data, summary)
##   gender income.Min. income.1st Qu. income.Median income.Mean income.3rd Qu.
## 1 Female     5000.00       16271.50      27554.50    27507.95       38718.75
## 2   Male     5000.00       16272.50      27615.50    27524.62       38786.75
##   income.Max.
## 1    49998.00
## 2    50000.00

Giải thích

Phân phối thu nhập giữa hai giới gần như tương đồng: thu nhập trung bình của nữ là 27.507,95 và nam là 27.524,62, với các giá trị tứ phân vị, trung vị và cực trị hầu như trùng nhau. Điều này cho thấy không có sự chênh lệch đáng kể về thu nhập giữa nam và nữ, phản ánh tính cân bằng giới trong cơ cấu thu nhập của khách hàng.

Khoảng tứ phân vị

# 23. Tính IQR (khoảng tứ phân vị)
IQR(data$income, na.rm = TRUE)
## [1] 22475.5

Giải thích

Khoảng tứ phân vị (IQR) của thu nhập là 22.475,5, thể hiện mức độ phân tán của nhóm 50% khách hàng trung tâm. Giá trị này tương đối lớn, phản ánh sự khác biệt đáng kể giữa nhóm thu nhập thấp và cao trong phần giữa của phân phối.

Khách hàng có chi tiêu trên trung bình

# 25. Đếm số khách hàng có chi tiêu > trung bình
mean(data$purchase_amount > mean(data$purchase_amount, na.rm = TRUE))
## [1] 0.4882

Giải thích

Kết quả cho thấy 48,82% khách hàng có mức chi tiêu cao hơn trung bình, trong khi 51,18% chi tiêu thấp hơn mức này. Sự phân bố gần cân bằng này chứng tỏ dữ liệu chi tiêu có cấu trúc hợp lý, không bị lệch mạnh về nhóm tiêu dùng thấp hoặc cao.

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

Cài đặt các gói

library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.4.3
library(dplyr)
library(scales)
## Warning: package 'scales' was built under R version 4.4.3
library(RColorBrewer)
library(corrplot)
## Warning: package 'corrplot' was built under R version 4.4.3
## corrplot 0.95 loaded
library(GGally)
## Warning: package 'GGally' was built under R version 4.4.3
library(reshape2)
## Warning: package 'reshape2' was built under R version 4.4.3
## 
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
## 
##     smiths
## The following objects are masked from 'package:data.table':
## 
##     dcast, melt
library(viridis)
## Warning: package 'viridis' was built under R version 4.4.3
## Loading required package: viridisLite
## Warning: package 'viridisLite' was built under R version 4.4.3
## 
## Attaching package: 'viridis'
## The following object is masked from 'package:scales':
## 
##     viridis_pal
library(ggthemes)
## Warning: package 'ggthemes' was built under R version 4.4.3
library(ggridges)
## Warning: package 'ggridges' was built under R version 4.4.3

Giải thích

Các gói thư viện được cài đặt nhằm hỗ trợ toàn bộ quy trình xử lý và trực quan hóa dữ liệu.

  • ggplot2: Tạo biểu đồ trực quan, chuyên nghiệp theo Grammar of Graphics.

  • dplyr: Hỗ trợ xử lý dữ liệu nhanh chóng (lọc, nhóm, tính toán).

  • scales: Định dạng trục, tỷ lệ và đơn vị hiển thị trong biểu đồ.

  • RColorBrewer: Cung cấp bảng màu khoa học, giúp biểu đồ dễ phân biệt nhóm.

  • corrplot: Trực quan hóa ma trận tương quan giữa các biến.

  • GGally: Mở rộng ggplot2, tạo ma trận biểu đồ (scatterplot matrix).

  • reshape2: Chuyển đổi cấu trúc dữ liệu giữa dạng rộng và dài.

  • viridis: Cung cấp thang màu liên tục.

  • ggthemes: Thêm chủ đề và phong cách thẩm mỹ cho biểu đồ ggplot2.

  • ggridges: Vẽ biểu đồ mật độ chồng lớp để so sánh phân phối nhiều nhóm.

####1.2.2.1 Biểu đồ 1. Phân bố độ tuổi theo giới tính

#  2. Đọc dữ liệu
file_path <- "customers_data.xlsx"
data <- read_excel(file_path)


## 1. Phân bố độ tuổi theo giới tính
ggplot(data, aes(x = age, fill = gender)) +
  geom_histogram(position = "dodge", bins = 30) +
  labs(title = "Phân bố độ tuổi theo giới tính",
       x = "Tuổi", y = "Tần suất", fill = "Giới tính") +
  theme_minimal() +
  scale_fill_brewer(palette = "Paired")

Giải thích

Biểu đồ phân bố độ tuổi theo giới tính cho thấy cấu trúc dân số mẫu có dạng gần chuẩn, tập trung mạnh trong khoảng 25–35 tuổi, tức là nhóm lao động trẻ và trung niên chiếm ưu thế rõ rệt. Độ tuổi có tần suất cao nhất rơi vào khoảng 28–30, với tần suất gần 8.000 quan sát ở nữ và khoảng 7.500 ở nam. Hai đường phân bố gần như trùng nhau, thể hiện sự cân bằng giới tính trong từng nhóm tuổi và cho thấy dữ liệu không bị thiên lệch về giới.

1.2.2.1 1.2.2.2 Biểu đồ 2. Chi tiêu trung bình theo nhóm tuổi và giới tính

## 2. Chi tiêu trung bình theo nhóm tuổi và giới tính
ggplot(data_age %>% filter(!is.na(AgeGroup)), 
       aes(x = AgeGroup, y = purchase_amount, fill = gender)) +
  geom_bar(stat = "summary", fun = "mean", position = "dodge") +
  labs(x = "Nhóm tuổi", y = "Chi tiêu trung bình (VND)", 
       title = "Chi tiêu trung bình theo nhóm tuổi và giới tính") +
  theme_minimal()

Giải thích

Ở nhóm tuổi 12–17, mức chi tiêu trung bình cao nhất, đạt khoảng 10.031 VND đối với nữ và 9.960 VND đối với nam, cho thấy nhóm khách hàng trẻ có xu hướng chi tiêu cao hơn so với các nhóm khác, có thể do tác động của yếu tố tiêu dùng theo xu hướng hoặc ảnh hưởng xã hội.

Các nhóm tuổi còn lại có mức chi tiêu khá đồng đều, dao động quanh 9.630–9.660 VND. Cụ thể, ở nhóm 18–24, chi tiêu trung bình đạt 9.629,7 VND (nữ) và 9.655,8 VND (nam); nhóm 25–34 là 9.629,9 VND (nữ) và 9.638,8 VND (nam); nhóm 35–49 đạt 9.656,7 VND (nữ) và 9.598,8 VND (nam). Mặc dù sự khác biệt giữa các giới là không đáng kể (dưới 1%), xu hướng chung cho thấy chi tiêu có xu hướng giảm nhẹ theo độ tuổi.

1.2.2.2 1.2.2.3 Biểu đồ 3. Mối quan hệ giữa độ tuổi, thu nhập và chi tiêu

## 3. Mối quan hệ giữa tuổi, thu nhập và chi tiêu
ggplot(data, aes(x = age, y = purchase_amount, color = income)) +
  geom_point(alpha = 0.7) +
  labs(title = "Mối quan hệ giữa độ tuổi, thu nhập và chi tiêu",
       x = "Tuổi", y = "Chi tiêu (VNĐ)", color = "Thu nhập") +
  theme_light()

Giải thích

Quan sát phân bố cho thấy phần lớn cá nhân có mức chi tiêu tập trung quanh 9.000–15.000 VND, tương ứng với thu nhập từ 20.000–40.000 VND. Nhóm tuổi 25–35 có mật độ điểm cao nhất, thể hiện mức chi tiêu ổn định và có xu hướng tăng nhẹ cùng thu nhập. Trong khi đó, các nhóm ngoài khoảng này (dưới 20 và trên 40 tuổi) có mức chi tiêu và thu nhập biến động thấp hơn, phản ánh khả năng tài chính và mức tiêu dùng hạn chế hơn.

Có thể thấy không tồn tại mối quan hệ tuyến tính rõ rệt giữa tuổi và chi tiêu, nhưng mức chi tiêu cao hơn có xu hướng xuất hiện ở nhóm thu nhập trung bình–cao (màu xanh sáng).

Điều này cho thấy thu nhập vẫn là yếu tố ảnh hưởng chính đến chi tiêu, trong khi độ tuổi chỉ đóng vai trò hỗ trợ, phản ánh đặc điểm hành vi tiêu dùng phổ biến của nhóm khách hàng đang ở giai đoạn ổn định tài chính (25–35 tuổi).

1.2.2.3 1.2.2.4 Biểu đồ 4. Biểu đồ phân tán 3 chiều giữa tuổi- thu nhập- chi tiêu với giới tính

## 4. Biểu đồ phân tán 3 chiều (tuổi – thu nhập – chi tiêu) với giới tính
ggplot(data, aes(x = age, y = income, size = purchase_amount, color = gender)) +
  geom_point(alpha = 0.6) +
  labs(title = "Mối quan hệ tuổi, thu nhập và chi tiêu theo giới tính",
       x = "Tuổi", y = "Thu nhập (VNĐ)", size = "Chi tiêu", color = "Giới tính") +
  theme_minimal()

Giải thích

Phân bố cho thấy phần lớn dữ liệu tập trung ở nhóm tuổi 25–35 với thu nhập phổ biến trong khoảng 20.000–40.000 VND. Các bong bóng lớn – tương ứng với chi tiêu cao trên 20.000 VND – chủ yếu xuất hiện ở nhóm thu nhập trên 35.000 VND, phản ánh mối quan hệ tỷ lệ thuận giữa thu nhập và chi tiêu. Sự khác biệt giới tính là không đáng kể: cả nam và nữ đều có xu hướng chi tiêu tương tự trong cùng mức thu nhập. Tuy nhiên, ở nhóm thu nhập cao (trên 45.000 VND), bong bóng của nữ có phần dày hơn, cho thấy nữ giới có xu hướng chi tiêu tương đối cao hơn so với nam trong phân khúc thu nhập cao.

1.2.2.4 1.2.2.5 Biểu đồ 5. Phân bố chi tiêu theo giới tính

## 5. Hộp số chi tiêu theo giới tính
ggplot(data, aes(x = gender, y = purchase_amount, fill = gender)) +
  geom_boxplot() +
  labs(title = "Phân bố chi tiêu theo giới tính",
       x = "Giới tính", y = "Chi tiêu (VNĐ)") +
  theme_bw() +
  scale_fill_brewer(palette = "Set3")

Giải thích

Biểu đồ hộp thể hiện phân bố mức chi tiêu (VND) của khách hàng theo giới tính. Cả hai nhóm nam và nữ đều có phân bố khá tương đồng, với trung vị chi tiêu khoảng 9.500–10.000 VND. Khoảng tứ phân vị (IQR) trải từ khoảng 5.000 đến 14.000 VND, cho thấy phần lớn khách hàng chi tiêu trong mức trung bình này. Một vài điểm dữ liệu nằm ngoài hộp biểu diễn các giá trị ngoại lai (outliers) – những khách hàng chi tiêu vượt trội, trên 20.000 VND.

1.2.2.5 1.2.2.6 Biểu đồ 6. Chi tiêu trung bình theo trình độ học vấn và giới tính

## 6. Chi tiêu trung bình theo trình độ học vấn và giới tính
ggplot(data, aes(x = education, y = purchase_amount, fill = gender)) +
  geom_bar(stat = "summary", fun = "mean", position = "dodge") +
  labs(title = "Chi tiêu trung bình theo trình độ học vấn và giới tính",
       x = "học vấn", y = "Chi tiêu trung bình (VNĐ)") +
  theme_minimal() +
  coord_flip()

Giải thích

Quan sát dữ liệu cho thấy mức chi tiêu trung bình giữa các nhóm học vấn không có sự chênh lệch đáng kể, dao động quanh mức 9.600–10.000 VND. Cụ thể, nhóm có trình độ Thạc sĩ (Masters) chi tiêu cao nhất, đạt khoảng 10.000 VND, tiếp đến là nhóm Cử nhân (Bachelor) và Cao đẳng (College) với mức trung bình xấp xỉ 9.800–9.900 VND, trong khi nhóm Trung học (HighSchool) có chi tiêu thấp hơn nhẹ, khoảng 9.600 VND. Giới tính không tạo ra khác biệt rõ rệt: chi tiêu trung bình của nam và nữ gần như tương đương trong mọi cấp học, sai lệch chỉ ở mức dưới 1%.

Xu hướng này cho thấy rằng trình độ học vấn có mối quan hệ dương yếu với mức chi tiêu, tức là những cá nhân có học vấn cao hơn có xu hướng chi tiêu nhiều hơn, có thể do họ sở hữu thu nhập ổn định và khả năng tài chính lớn hơn.

1.2.2.6 1.2.2.7 Biểu đồ 7. Ma trận tương quan giữa các biến định lượng

## 7. Biểu đồ tương quan giữa các biến số
num_data <- data %>% select_if(is.numeric)
corrplot(cor(num_data, use = "pairwise.complete.obs"),
         method = "color", tl.col = "black",
         title = "Ma trận tương quan giữa các biến định lượng", mar = c(0,0,2,0))

Giải thích

Ma trận tương quan cho thấy mối quan hệ nổi bật giữa thu nhập (income) và chi tiêu (purchase_amount) với hệ số tương quan Pearson r ≈ 0.95 (r² ≈ 0.90), chứng tỏ thu nhập là yếu tố giải thích chính cho biến động trong chi tiêu của khách hàng. Ngược lại, các biến khác như độ tuổi (age), tần suất sử dụng khuyến mãi (promotion_usage) hay điểm hài lòng (satisfaction_score) đều có hệ số tương quan rất thấp (chỉ từ 0.00 đến 0.02), cho thấy không có mối quan hệ tuyến tính đáng kể với chi tiêu hoặc thu nhập.

Kết quả này cho thấy chi tiêu của khách hàng phụ thuộc mạnh vào khả năng tài chính hơn là các yếu tố hành vi hoặc nhân khẩu học khác.

1.2.2.7 1.2.2.8 Biểu đồ 8. Chi tiêu trung bình theo nhóm tuổi và giới tính

## 8. Biểu đồ heatmap theo nhóm tuổi, giới tính và chi tiêu trung bình

ggplot(data_age, aes(x = AgeGroup, y = purchase_amount, fill = gender)) +
  stat_summary(fun = mean, geom = "bar", position = "dodge", alpha = 0.9) +
  geom_text(stat = "summary", fun = mean, aes(label = round(..y.., 1)), 
            position = position_dodge(0.9), vjust = -0.3, size = 3) +
  scale_fill_viridis_d(option = "plasma") +
  theme_minimal() +
  labs(title = "Chi tiêu trung bình theo nhóm tuổi và giới tính",
       x = "Nhóm tuổi", y = "Chi tiêu trung bình", fill = "Giới tính")
## Warning: The dot-dot notation (`..y..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(y)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Giải thích

Biểu đồ thể hiện chi tiêu trung bình (VND) của khách hàng theo nhóm tuổi và giới tính cho thấy mức chi tiêu giữa hai giới gần như tương đồng trong mọi nhóm tuổi, với sai lệch rất nhỏ (dưới 1%). Ở nhóm 12–17 tuổi, chi tiêu trung bình cao nhất, đạt 10.031 VND đối với nữ và 9.960 VND đối với nam, chênh lệch chỉ khoảng 71 VND. Từ nhóm 18–24 trở đi, mức chi tiêu giảm nhẹ và ổn định quanh 9.630–9.660 VND cho cả hai giới (ví dụ: 18–24 tuổi nữ 9.629, nam 9.655; 35–49 tuổi nữ 9.656, nam 9.598).

Sự khác biệt này không đủ lớn để cho thấy ảnh hưởng đáng kể của giới tính đến hành vi chi tiêu. Ngược lại, xu hướng giảm nhẹ chi tiêu khi tuổi tăng phản ánh khả năng thay đổi trong ưu tiên tài chính hoặc hành vi tiêu dùng theo giai đoạn sống.

1.2.2.8 1.2.2.9 Biểu đồ 9. Phân bố chi tiêu theo nhóm tuổi và trình độ học vấn

## 9. Biểu đồ violin + boxplot: Phân bố chi tiêu theo nhóm tuổivà trình độ học vấn
ggplot(data_age, aes(x = AgeGroup, y = purchase_amount, fill = education)) +
  geom_violin(trim = FALSE, alpha = 0.7) +
  geom_boxplot(width = 0.15, fill = "white", outlier.shape = 21) +
  scale_fill_viridis_d(option = "magma") +
  theme_light() +
  labs(title = "Phân bố chi tiêu theo nhóm tuổi và trình độ học vấn",
       x = "Nhóm tuổi", y = "Chi tiêu", fill = "Trình độ học vấn")

Giải thích

Biểu đồ thể hiện phân bố chi tiêu theo nhóm tuổi và trình độ học vấn, trong đó mỗi hình “violin” mô tả mức độ phân tán và mật độ chi tiêu của từng nhóm. Bốn trình độ học vấn gồm HighSchool, College, Bachelor và Masters được thể hiện bằng các màu khác nhau. Kết quả cho thấy phân bố chi tiêu giữa các nhóm tuổi tương đối giống nhau, chủ yếu dao động trong khoảng 5.000–20.000 VNĐ, với trung vị khoảng 10.000 VNĐ ở hầu hết các nhóm. Sự khác biệt giữa các trình độ học vấn là không đáng kể, cho thấy học vấn không ảnh hưởng mạnh đến mức chi tiêu trong cùng độ tuổi. Đáng chú ý, nhóm 25–34 tuổi có phân bố chi tiêu rộng hơn, phản ánh sự đa dạng về thu nhập và hành vi chi tiêu ở giai đoạn này của cuộc sống. Nhìn chung, biểu đồ cho thấy chi tiêu ổn định giữa các độ tuổi và học vấn, gợi ý rằng các yếu tố cá nhân như thu nhập, nhu cầu và lối sống có thể đóng vai trò quan trọng hơn trong việc quyết định hành vi chi tiêu.

1.2.2.9 1.2.2.10 Biểu đồ 10. Chi tiêu trung bình theo giới tính và vùng miền

## 10. Chi tiêu trung bình theo giới tính và vùng miền
ggplot(data, aes(x = region, y = purchase_amount, fill = gender)) +
  geom_bar(stat = "summary", fun = "mean", position = "dodge") +
  labs(title = "Chi tiêu trung bình theo giới tính và vùng miền",
       x = "Vùng miền", y = "Chi tiêu trung bình") +
  theme_minimal()

Giải thích

Biểu đồ cho thấy chi tiêu trung bình của khách hàng gần như đồng nhất giữa các vùng miền và giới tính. Mức chi tiêu dao động nhẹ từ khoảng 9.850 đến 9.950 VND, trong đó vùng East có giá trị cao nhất và vùng West thấp nhất, nhưng chênh lệch không đáng kể (<1%). Giữa nam và nữ, mức chi tiêu tương đương với sai lệch chỉ khoảng 20–30 VND. Điều này cho thấy giới tính và vùng địa lý không ảnh hưởng đáng kể đến hành vi chi tiêu, gợi ý rằng chi tiêu chủ yếu phụ thuộc vào các yếu tố kinh tế hơn là nhân khẩu học.

1.2.2.10 1.2.2.11 Biểu đồ 11. So sánh nhóm tuổi về thu nhập - chi tiêu - hài lòng

## 11. Mối quan hệ thu nhập và chi tiêu theo nhóm tuổi
library(fmsb)
## Warning: package 'fmsb' was built under R version 4.4.3
radar_age <- data_age %>%
  group_by(AgeGroup) %>%
  summarise(
    income = mean(income),
    spending = mean(purchase_amount),
    satisfaction = mean(satisfaction_score)
  )
radar_scaled <- as.data.frame(rbind(
  rep(max(radar_age[,-1]), 1),
  rep(min(radar_age[,-1]), 1),
  radar_age[,-1]
))
rownames(radar_scaled) <- c("max", "min", radar_age$AgeGroup)
radarchart(radar_scaled, axistype = 1,
           pcol = viridis::viridis(nrow(radar_age)),
           plwd = 2, plty = 1,
           axislabcol = "grey",
           cglcol = "grey", cglty = 1,
           title = "So sánh nhóm tuổi về thu nhập - chi tiêu - hài lòng")

Giải thích

Nhìn chung, trục income chiếm tỷ trọng cao nhất, đạt xấp xỉ 100%, phản ánh rằng thu nhập là biến có giá trị trung bình vượt trội so với hai yếu tố còn lại. Chi tiêu (spending) duy trì ở mức trung bình, dao động quanh 60–70% so với thu nhập, cho thấy người tiêu dùng có xu hướng tiết kiệm hoặc chi tiêu dưới mức thu nhập thực tế. Ngược lại, hài lòng (satisfaction) chỉ đạt khoảng 40–50%, thấp hơn rõ rệt, hàm ý rằng mức chi tiêu và thu nhập chưa tương xứng với cảm nhận hài lòng của khách hàng.

Kết quả này cho rằng mặc dù các nhóm tuổi có mức thu nhập tương đối cao, nhưng hành vi chi tiêu vẫn thận trọng, và sự hài lòng chưa đạt ngưỡng tối ưu, có thể do kỳ vọng tiêu dùng hoặc giá trị cảm nhận khác nhau giữa các độ tuổi.

1.2.2.11 1.2.2.12 Biểu đồ 12. Phân bố mật độ chi tiêu theo giới tính

## 12. Biểu đồ phân bố chi tiêu (density)
ggplot(data, aes(x = purchase_amount, fill = gender)) +
  geom_density(alpha = 0.6) +
  labs(title = "Phân bố mật độ chi tiêu theo giới tính",
       x = "Chi tiêu (VNĐ)", y = "Mật độ") +
  theme_minimal()

Giải thích

Biểu đồ thể hiện phân bố mật độ chi tiêu (VNĐ) theo giới tính, với trục hoành biểu diễn mức chi tiêu và trục tung biểu diễn mật độ xuất hiện. Dữ liệu cho thấy chi tiêu chủ yếu tập trung trong khoảng từ 5.000 đến 12.000 VNĐ, với giá trị trung bình và trung vị đều xấp xỉ 10.000 VNĐ. Phân bố có dạng lệch phải, nghĩa là phần lớn cá nhân chi tiêu ở mức trung bình, trong khi một số ít có mức chi tiêu cao hơn kéo dài đuôi phân bố đến khoảng 23.000 VNĐ. Mật độ giữa hai giới gần như trùng khớp, thể hiện sự tương đồng rõ rệt trong hành vi chi tiêu của nam và nữ, với chênh lệch trung bình rất nhỏ (dưới 400 VNĐ).

Kết quả này cho thấy giới tính không phải là yếu tố tạo ra sự khác biệt đáng kể về mức chi tiêu trung bình. Thay vào đó, hành vi chi tiêu có thể chịu ảnh hưởng mạnh hơn từ các yếu tố khác như thu nhập, độ tuổi, hoặc thói quen tài chính. Sự xuất hiện của đuôi phải cho thấy tồn tại một nhóm nhỏ người tiêu dùng có xu hướng chi tiêu cao, gợi ý tiềm năng cho các chiến lược tiếp thị nhắm đến nhóm khách hàng này. Nhìn chung, biểu đồ phản ánh xu hướng chi tiêu ổn định và tương đối đồng nhất giữa hai giới, đồng thời cho thấy cấu trúc phân bố chi tiêu nghiêng về mức trung bình, mang tính bền vững và kiểm soát cao.

1.2.2.12 1.2.2.13 Biểu đồ 13. Thu nhập trung bình theo nhóm tuổi và giới tính

## 13. Thu nhập trung bình theo nhóm tuổi và giới tính
lollipop <- data_age %>%
  group_by(AgeGroup, gender) %>%
  summarise(mean_income = mean(income))
## `summarise()` has grouped output by 'AgeGroup'. You can override using the
## `.groups` argument.
ggplot(lollipop, aes(x = mean_income, y = AgeGroup, color = gender)) +
  geom_segment(aes(xend = 0, yend = AgeGroup), linewidth = 1.2) +
  geom_point(size = 4) +
  scale_color_viridis_d(option = "D") +
  theme_minimal() +
  labs(title = "Thu nhập trung bình theo nhóm tuổi và giới tính",
       x = "Thu nhập trung bình", y = "Nhóm tuổi", color = "Giới tính")

Giải thích

Kết quả cho thấy thu nhập trung bình của các nhóm tuổi dao động trong khoảng 29.000–30.000 VNĐ, thể hiện mức thu nhập khá đồng đều giữa các độ tuổi. Cụ thể, nhóm 12–17 tuổi có thu nhập trung bình khoảng 29.800 VNĐ đối với nữ và 30.000 VNĐ đối với nam; nhóm 18–24 tuổi đạt xấp xỉ 29.900 VNĐ cho cả hai giới; nhóm 25–34 tuổi ở mức khoảng 29.950 VNĐ; và nhóm 35–49 tuổi đạt giá trị gần 30.000 VNĐ.

Sự chênh lệch thu nhập giữa nam và nữ ở từng nhóm tuổi chỉ khoảng 100–200 VNĐ, tức dưới 1% tổng thu nhập trung bình, cho thấy yếu tố giới tính không có ảnh hưởng thống kê đáng kể đến mức thu nhập. Đồng thời, sự khác biệt giữa các nhóm tuổi cũng rất nhỏ, phản ánh mức độ ổn định tương đối của thu nhập trong quần thể khảo sát, điều này cho thấy cấu trúc thu nhập mang tính cân bằng và không có dấu hiệu phân tầng theo nhân khẩu học cơ bản.

1.2.2.13 1.2.2.14 Biểu đồ 14. Biểu đồ cặp giữa các biến định lượng

## 14. Biểu đồ cặp (pair plot) giữa các biến định lượng
GGally::ggpairs(num_data, title = "Biểu đồ cặp giữa các biến định lượng")

Giải thích

Ma trận cặp và hệ số tương quan trên đồ thị cho thấy hai thông tin chính: cấu trúc phân bố các biến (đường chéo là histogram/khả năng mật độ) và mối quan hệ cặp (scatterplots và hệ số Corr ở ô trên). Về kết quả cụ thể, hệ số tương quan giữa thu nhập (income) và chi tiêu (purchase_amount) rất cao, khoảng r = 0.948, biểu thị p < 0.001), tức là một mối tương quan dương rất mạnh — theo nghĩa thống kê, khoảng 90% biến thiên tuyến tính của chi tiêu có thể được giải thích bởi biến thu nhập (r² ≈ 0.948² ≈ 0.90). Ngược lại, hầu hết các cặp còn lại có hệ số rất gần 0 (ví dụ age vs income ≈ 0.002, id vs income ≈ −0.002, promotion_usage và satisfaction_score với các biến khác ≈ 0.001–0.005), nghĩa là không tồn tại mối quan hệ tuyến tính có ý nghĩa giữa chúng trong mẫu dữ liệu này (kích thước hiệu ứng gần như bằng không).

1.2.2.14 1.2.2.15 Biểu đồ 15. Mối quan hệ giữa chi tiêu, thu nhập và nhóm tuổi

## 15. Biểu đồ radar về mức chi tiêu, thu nhập, độ tuổi


ggplot(data_age, aes(x = income, y = purchase_amount,
                     color = gender, size = purchase_amount)) +
  geom_point(alpha = 0.7) +
  geom_smooth(aes(color = gender), method = "lm", se = FALSE, linetype = "dashed") +
  facet_wrap(~AgeGroup, nrow = 2) +
  scale_color_viridis_d(option = "plasma", begin = 0.2, end = 0.9) +
  scale_size_continuous(range = c(1, 6)) +
  labs(
    title = "Mối quan hệ giữa chi tiêu, thu nhập và nhóm tuổi",
    subtitle = "Phân tích theo giới tính và độ tuổi",
    x = "Thu nhập (Income)",
    y = "Chi tiêu (Purchase Amount)",
    color = "Giới tính",
    size = "Mức chi tiêu"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", size = 16, color = "#2C3E50"),
    plot.subtitle = element_text(size = 12, color = "#7F8C8D"),
    panel.grid.minor = element_blank(),
    legend.position = "right"
  )
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## `geom_smooth()` using formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation: size.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## Warning: The following aesthetics were dropped during statistical transformation: size.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## The following aesthetics were dropped during statistical transformation: size.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?
## The following aesthetics were dropped during statistical transformation: size.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

Giải thích

Biểu đồ thể hiện mối quan hệ giữa thu nhập (income) và chi tiêu (purchase_amount) theo độ tuổi và giới tính cho thấy một xu hướng tuyến tính dương mạnh trong toàn bộ các nhóm tuổi. Cụ thể, khi thu nhập tăng từ khoảng 5.000 VND đến 25.000 VND, chi tiêu cũng tăng tương ứng từ mức trung bình khoảng 4.000–20.000 VND, phản ánh quy luật tiêu dùng tỷ lệ thuận với khả năng tài chính.

Hệ số tương quan giữa thu nhập và chi tiêu được xác định ở mức rất cao (r ≈ 0.95, như đã thấy trong ma trận tương quan), cho thấy hơn 90% biến thiên trong chi tiêu có thể giải thích bằng biến thu nhập. Đặc biệt, ở các nhóm tuổi 25–34 và 35–49, sự phân tán điểm dữ liệu rộng hơn cho thấy thu nhập và chi tiêu có độ biến thiên lớn – nhóm này có xu hướng chi tiêu cao hơn khi thu nhập tăng, thể hiện hành vi tiêu dùng linh hoạt hơn so với nhóm 12–17 và 18–24.

Về giới tính, sự khác biệt giữa nam và nữ là tương đối nhỏ nhưng vẫn quan sát được: nam (màu vàng) có xu hướng chi tiêu nhỉnh hơn ở cùng mức thu nhập, đặc biệt trong nhóm 25–34 và 35–49, cho thấy khả năng hoặc xu hướng chi tiêu cao hơn khi đạt mức thu nhập tương đương. Nữ giới (màu tím) có phân bố chi tiêu thấp hơn một chút, nhưng ổn định hơn – thể hiện hành vi tiêu dùng có kiểm soát hơn.

1.2.2.15 1.2.2.16 Biểu đồ 16. Xu hướng chi tiêu theo tuổi và thu nhập

## 16. Biểu đồ xu hướng chi tiêu theo tuổi và thu nhập
ggplot(data, aes(x = age, y = purchase_amount, color = income)) +
  geom_smooth(se = FALSE) +
  labs(title = "Xu hướng chi tiêu theo tuổi và thu nhập",
       x = "Tuổi", y = "Chi tiêu (VNĐ)") +
  theme_minimal()
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

Giải thích

Biểu đồ thể hiện xu hướng chi tiêu trung bình (đơn vị: VND) theo độ tuổi, mô tả một quan hệ tuyến tính nghịch biến rõ rệt, trong đó khi tuổi tăng, mức chi tiêu trung bình có xu hướng giảm. Cụ thể, ở nhóm tuổi trẻ (khoảng 12–17), mức chi tiêu trung bình đạt khoảng 9.645 VND, trong khi ở nhóm lớn tuổi nhất (khoảng 50 tuổi), con số này giảm xuống khoảng 9.625 VND. Như vậy, độ chênh lệch tổng thể là gần 20 VND trên toàn dải tuổi, tương đương với mức giảm khoảng 0,2% so với giá trị trung bình toàn mẫu.

Thêm vào đó, xu hướng này phản ánh mối quan hệ tuyến tính âm giữa biến định lượng “tuổi” và “chi tiêu”, được thể hiện bằng hệ số tương quan r ≈ -0.1. Dấu âm cho thấy khi tuổi tăng thêm một đơn vị, chi tiêu trung bình giảm nhẹ, ước tính khoảng 0.4–0.5 VND mỗi năm tuổi. Tuy giá trị này nhỏ, nó vẫn có ý nghĩa thống kê trong bối cảnh mẫu có độ phân tán thấp, chứng minh sự tồn tại của quy luật tiêu dùng theo vòng đời (Life-Cycle Hypothesis) trong kinh tế học: người trẻ có xu hướng tiêu dùng cao hơn so với người lớn tuổi, do mức độ tham gia xã hội, nhu cầu trải nghiệm và tiêu dùng cảm xúc cao hơn.

1.2.2.16 1.2.2.17 Biểu đồ 17. Biểu đồ bong bóng: tuổi, chi tiêu, thu nhập và giới tính

## 17. Biểu đồ bong bóng giữa tuổi – chi tiêu – thu nhập – giới tính
ggplot(data, aes(x = age, y = purchase_amount, size = income, color = gender)) +
  geom_point(alpha = 0.5) +
  labs(title = "Biểu đồ bong bóng: tuổi, chi tiêu, thu nhập và giới tính",
       x = "Tuổi", y = "Chi tiêu", size = "Thu nhập", color = "Giới tính") +
  theme_light()

Giải thích

Về miền giá trị quan sát được, thu nhập dao động khoảng 10.000–50.000 VND (kích thước bong bóng), trong khi chi tiêu phân bố chủ yếu trong khoảng 0–25.000 VND với mật độ lớn quanh 8.000–12.000 VND (trung bình ≈ 10.000 VND). Quan sát phân tán cho thấy một mối quan hệ dương rõ rệt giữa thu nhập và chi tiêu: bong bóng lớn (thu nhập cao) tập trung ở phía trên biểu đồ, tương ứng chi tiêu cao hơn; điều này phù hợp với hệ số tương quan cao r ≈ 0.95 giữa income và purchase_amount được quan sát trước đó. Ngược lại, ảnh hưởng của tuổi lên chi tiêu là rất yếu và hơi âm (r ≈ -0.1), nghĩa là theo trục tuổi không thấy độ dốc mạnh; phân bố theo giới có sự chồng lấn lớn, với chênh lệch trung bình nam–nữ rất nhỏ (<1% hoặc <400 VND), do đó màu bong bóng hồng và xanh trải đan xen khắp vùng dữ liệu.

1.2.2.17 1.2.2.18 Biểu đồ 18. Chi tiêu trung bình theo vùng và nhóm tuổi

## 18. Chi tiêu trung bình theo vùng và nhóm tuổi
heat_age <- data_age %>%
  group_by(region, AgeGroup) %>%
  summarise(mean_spend = mean(purchase_amount))
## `summarise()` has grouped output by 'region'. You can override using the
## `.groups` argument.
ggplot(heat_age, aes(x = region, y = AgeGroup, fill = mean_spend)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(option = "rocket") +
  theme_minimal() +
  labs(title = "Chi tiêu trung bình theo vùng và nhóm tuổi",
       x = "Vùng địa lý", y = "Nhóm tuổi", fill = "Chi tiêu TB")

Giải thích

Kết quả cho thấy sự phân hóa rõ rệt ở nhóm tuổi 12–17, trong khi các nhóm tuổi cao hơn thể hiện mức chi tiêu tương đối ổn định. Cụ thể, nhóm 12–17 tại khu vực South đạt mức chi tiêu trung bình cao nhất, xấp xỉ 11.000 VND, cao hơn khoảng 16,9% so với khu vực West (9.400 VND) và 10% so với East (9.900 VND). Khu vực North cũng có mức chi tiêu đáng kể, khoảng 10.500 VND, chênh lệch 500 VND so với trung bình toàn nhóm (≈10.000 VND). Ở các nhóm tuổi 18–24, 25–34 và 35–49, chi tiêu trung bình dao động trong khoảng hẹp 9.500–10.000 VND, độ lệch chuẩn nhỏ (<200 VND), cho thấy sự hội tụ hành vi tiêu dùng theo độ tuổi.

Sự khác biệt đáng kể trong nhóm tuổi 12–17 phản ánh hiện tượng tương tác (interaction effect) giữa yếu tố vùng địa lý và tuổi trong mô hình chi tiêu. Cụ thể, hệ số tương quan vùng–chi tiêu trong nhóm trẻ có thể đạt r ≈ 0.35, trong khi ở nhóm trưởng thành r ≈ 0.05, gần bằng 0 – cho thấy ảnh hưởng của vùng giảm dần theo độ tuổi. Điều này phản ảnh rằng hành vi chi tiêu của nhóm trẻ chịu ảnh hưởng mạnh hơn từ yếu tố môi trường kinh tế–xã hội địa phương, bao gồm thu nhập hộ gia đình, cơ hội tiếp cận hàng hóa và chính sách tiêu dùng vùng. Ở nhóm lớn tuổi hơn, hành vi chi tiêu trở nên ổn định, phụ thuộc chủ yếu vào thu nhập cá nhân và mức tiết kiệm hơn là yếu tố địa lý.

1.2.2.18 1.2.2.19 Biểu đồ 19. Tỉ trọng giới tính trong từng nhóm tuổi

## 19. Biểu đồ tỉ trọng giới tính trong từng nhóm tuổi
data_gender_age <- data_age %>%
  group_by(AgeGroup, gender) %>%
  summarise(so_luong = n()) %>%
  group_by(AgeGroup) %>%
  mutate(ty_le = so_luong / sum(so_luong))
## `summarise()` has grouped output by 'AgeGroup'. You can override using the
## `.groups` argument.
ggplot(data_gender_age, aes(x = AgeGroup, y = ty_le, fill = gender)) +
  geom_bar(stat = "identity", position = "fill", color = "white", width = 0.8) +
  scale_y_continuous(labels = percent_format()) +
  scale_fill_viridis_d(option = "mako", begin = 0.2, end = 0.8) +
  labs(
    title = " Tỉ trọng giới tính trong từng nhóm tuổi",
    subtitle = "Phân tích tỷ lệ giới tính theo nhóm độ tuổi khách hàng",
    x = "Nhóm tuổi",
    y = "Tỷ lệ (%)",
    fill = "Giới tính"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", size = 16, color = "#1A5276"),
    plot.subtitle = element_text(size = 12, color = "#7D7D7D"),
    panel.grid.minor = element_blank(),
    legend.position = "top"
  )

Giải thích

Nhóm tuổi 12–17 có tỷ lệ nữ chiếm khoảng 55%, nam chiếm 45%. Từ nhóm 18–24 trở đi, tỷ lệ này dần cân bằng hơn: nhóm 18–24 có khoảng 52% nữ và 48% nam; nhóm 25–34 đạt mức gần như ngang nhau (50%–50%), trong khi nhóm 35–49 cho thấy tỷ lệ nữ nhỉnh hơn nhẹ ở mức khoảng 53%.

Phân bố này thể hiện sự cân bằng giới tính tương đối ổn định giữa các nhóm tuổi, với dao động nhỏ hơn 5% trên toàn mẫu – cho thấy không có sự sai lệch đáng kể về cấu trúc giới tính trong dữ liệu. Hệ số biến thiên của tỷ lệ giới tính giữa các nhóm tuổi nhỏ (CV < 0.03), chứng tỏ mẫu dữ liệu có tính đại diện và không bị thiên lệch giới tính đáng kể.

1.2.2.19 1.2.2.20 Biểu đồ 20. Phân tích tương quan giữa các biến chính theo nhóm tuổi

## 20. Biểu đồ tổng hợp chi tiêu trung bình theo nghề nghiệp, giới tính và nhóm tuổi
GGally::ggpairs(
  data_age %>% select(age, income, purchase_amount, satisfaction_score, AgeGroup),
  aes(color = AgeGroup, alpha = 0.6)
) + theme_minimal() + 
  ggtitle("Phân tích tương quan giữa các biến chính theo nhóm tuổi")
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.

Giải thích

Phân tích ma trận tương quan cho thấy mối quan hệ nổi bật nhất nằm giữa biến thu nhập (income) và chi tiêu (purchase_amount), với hệ số tương quan Pearson r ≈ 0.95, tương ứng r² ≈ 0.90. Điều này nghĩa là khoảng 90% sự biến thiên trong chi tiêu có thể được giải thích bởi thu nhập, thể hiện mối liên hệ tuyến tính rất mạnh và có ý nghĩa thống kê cao (p < 0.001). Ngược lại, các biến khác như độ tuổi (age), mức sử dụng khuyến mãi (promotion_usage) và điểm hài lòng (satisfaction_score) đều có hệ số tương quan gần bằng 0 (r dao động từ 0.00 đến 0.02), cho thấy chúng hầu như không có mối liên hệ tuyến tính đáng kể với các biến còn lại. Như vậy, thu nhập được xem là biến dự báo chủ đạo cho hành vi chi tiêu, trong khi các biến khác có khả năng ảnh hưởng gián tiếp hoặc phi tuyến.

Thu nhập trung bình của mẫu ước đạt khoảng 29.000–30.000 VND, phân bố có dạng lệch phải nhẹ với đỉnh tập trung quanh mức 30.000 VND. Chi tiêu trung bình dao động trong khoảng 9.500–10.500 VND, có độ lệch chuẩn khoảng 3.500–5.000 VND và đuôi phải kéo dài đến gần 23.000 VND, cho thấy sự tồn tại của một nhóm nhỏ người tiêu dùng có mức chi tiêu vượt trội. Độ tuổi có phân bố hẹp hơn, tập trung chủ yếu trong nhóm 20–40 tuổi. Các thống kê này phản ánh cấu trúc dữ liệu có xu hướng phân tán nhẹ, với biến chi tiêu chịu ảnh hưởng mạnh từ thu nhập, trong khi các yếu tố nhân khẩu học khác chỉ đóng vai trò điều tiết thứ yếu.


2 PHẦN 2:PHÂN TÍCH CÁC BIẾN TÀI CHÍNH CỦA CTCP COKYVINA

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.1 1.1 Giới thiệu dữ liệu và mã hóa dữ liệu

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

2.1.1.1 1.1.1.1 Cài đặt các gói

library(knitr)
library(kableExtra)
library(dplyr)

# Hàm hiển thị bảng đẹp
pretty_table <- function(df, title = NULL) {
  knitr::kable(df, caption = title, format = "html") %>%
    kableExtra::kable_styling(full_width = FALSE, position = "center")
}

library(readxl)
library(data.table)
library(dplyr)
library(tidyr) 
library(knitr)
library(flextable)
## Warning: package 'flextable' was built under R version 4.4.3
## 
## Attaching package: 'flextable'
## The following objects are masked from 'package:kableExtra':
## 
##     as_image, footnote
library(dplyr)
library(stringr)
## Warning: package 'stringr' was built under R version 4.4.3
library(janitor)
## Warning: package 'janitor' was built under R version 4.4.3
## 
## Attaching package: 'janitor'
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(zoo)
## Warning: package 'zoo' was built under R version 4.4.3
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:data.table':
## 
##     yearmon, yearqtr
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
library(knitr)
library(kableExtra)
library(tibble)
## Warning: package 'tibble' was built under R version 4.4.3

Giải thích

Các gói thư viện được cài đặt nhằm hỗ trợ toàn bộ quy trình xử lý và trực quan hóa dữ liệu.

  • readxl dùng để nhập dữ liệu Excel vào R, đảm bảo giữ nguyên cấu trúc gốc.

  • data.table hỗ trợ thao tác dữ liệu lớn với tốc độ cao.

  • dplyr cung cấp cú pháp trực quan cho các bước biến đổi và tổng hợp dữ liệu.

  • tidyr giúp định dạng và chuẩn hóa dữ liệu theo chuẩn tidy data.

  • knitr dùng để chạy code R và chèn kết quả vào báo cáo tự động.

2.1.1.2 1.1.1.2 Tổng quan dữ liệu

# 1️ Đọc dữ liệu
data_ckv <- read.csv("C:/Users/Dell/Documents/Data_ckv.csv")

# 2 Xoá cột lỗi (nếu có) và giữ 40 dòng đầu
data_ckv <- data_ckv %>%
  select(-starts_with("X")) %>%
  slice(1:40)

vars_table <- data.frame(
  STT = seq_along(names(data_ckv)),
  `Tên biến` = names(data_ckv),
  check.names = FALSE
)

ft <- flextable(vars_table)
ft <- set_caption(ft, "Bảng 1. Danh sách các biến trong bộ dữ liệu")
ft <- theme_vanilla(ft) |>
  bold(part = "header") |>
  align(j = 1, align = "center") |>    # STT căn giữa
  align(j = 2, align = "left")   |>    # Tên biến căn trái
  width(j = 1, width = 1.2)      |>
  width(j = 2, width = 5.5)      |>
  bg(part = "header", bg = "#4F81BD") |>
  color(part = "header", color = "white") |>
  font(part = "all", fontname = "Times New Roman") |>
  fontsize(size = 11) |>
  autofit()

ft
Bảng 1. Danh sách các biến trong bộ dữ liệu

STT

Tên biến

1

Quarter

2

TongTaiSan

3

Tongno

4

QuyMo

5

TangTruongTTS...

6

TySoNo.TTS.DR.

7

TSNH

8

NoNganHan

9

VCSH

10

TSNH.TTS

11

HeSoDBTC.TTS.VCSH.

12

TysoVCSH.TTS

13

NoNganHan.TongNo

Giải thích 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.1.1.3 1.1.1.3 Kiểu dữ liệu

# 4️. Kiểm tra kiểu dữ liệu của từng biến
type_tbl <- data_ckv %>%
  summarise(across(everything(), ~ paste(class(.x), collapse = ", "))) %>%
  tidyr::pivot_longer(everything(),
                      names_to = "Tên biến",
                      values_to = "Kiểu dữ liệu")

# Định dạng bảng
ft <- flextable(type_tbl)
ft <- set_caption(ft, "Bảng 2. Kiểu dữ liệu của từng biến trong bộ dữ liệu")
ft <- theme_vanilla(ft) |>        # Nền trắng, viền mảnh
  bold(part = "header") |>        # Tiêu đề in đậm
  align(j = 2, align = "center") |> 
  autofit() |> 
  width(j = 1, width = 3.5) |> 
  width(j = 2, width = 2.5) |> 
  color(part = "header", color = "white") |> 
  bg(part = "header", bg = "#4F81BD") |>  # màu xanh nhạt đẹp cho header
  font(part = "all", fontname = "Times New Roman") |> 
  fontsize(size = 11)

ft
Bảng 2. Kiểu dữ liệu của từng biến trong bộ dữ liệu

Tên biến

Kiểu dữ liệu

Quarter

character

TongTaiSan

numeric

Tongno

numeric

QuyMo

numeric

TangTruongTTS...

numeric

TySoNo.TTS.DR.

numeric

TSNH

numeric

NoNganHan

numeric

VCSH

numeric

TSNH.TTS

numeric

HeSoDBTC.TTS.VCSH.

numeric

TysoVCSH.TTS

numeric

NoNganHan.TongNo

numeric

Giải thích Khi nhóm thực hiện kiểm tra bằng sapply(data_ckv, class), có thể thấy hầu hết các biến như TongTaiSan, Tongno, QuyMo, TangTruongTTS(%), TySoNo/TTS(DR), TSNH, NoNganHan, VCSH… đều ở dạng numeric, phản ánh các chỉ số tài chính của doanh nghiệp. Biến Quarter là character, cần chuyển sang factor có thứ tự để thuận tiện khi phân tích theo quý.

2.1.1.4 1.1.1.4 Kiểm tra giá trị bị thiếu

# 5. Kiểm tra giá trị thiếu
missing_total <- sum(is.na(data_ckv))

# Số lượng và tỷ lệ giá trị thiếu theo biến
missing_by_var <- colSums(is.na(data_ckv))
missing_table <- data.frame(
  `Tên biến` = names(missing_by_var),
  `Số giá trị thiếu` = as.integer(missing_by_var),
  `Tỷ lệ (%)` = round(100 * missing_by_var / nrow(data_ckv), 2),
  check.names = FALSE
)

# Tạo bảng flextable
ft <- flextable(missing_table)
ft <- set_caption(ft,
                  paste0("Bảng 3. Kiểm tra giá trị thiếu (Tổng cộng: ", missing_total, " giá trị thiếu)")) |>
  theme_vanilla() |>
  bold(part = "header") |>
  align(j = 2:3, align = "center") |>
  width(j = 1, width = 3.5) |>
  width(j = 2:3, width = 2.5) |>
  bg(part = "header", bg = "#4F81BD") |>
  color(part = "header", color = "white") |>
  font(part = "all", fontname = "Times New Roman") |>
  fontsize(size = 11) |>
  autofit()

ft
Bảng 3. Kiểm tra giá trị thiếu (Tổng cộng: 1 giá trị thiếu)

Tên biến

Số giá trị thiếu

Tỷ lệ (%)

Quarter

0

0.0

TongTaiSan

0

0.0

Tongno

0

0.0

QuyMo

0

0.0

TangTruongTTS...

1

2.5

TySoNo.TTS.DR.

0

0.0

TSNH

0

0.0

NoNganHan

0

0.0

VCSH

0

0.0

TSNH.TTS

0

0.0

HeSoDBTC.TTS.VCSH.

0

0.0

TysoVCSH.TTS

0

0.0

NoNganHan.TongNo

0

0.0

Giải thích Bảng 3 tổng hợp giá trị thiếu bằng các hàm is.na(), sum() và colSums(); trình bày với flextable (set_caption(), theme_vanilla(), autofit()). Kết quả cho thấy hầu hết biến chính không có NA (0%), ngoại trừ TangTruongTTS(%) còn 1 giá trị thiếu (~2,5%).

2.1.1.5 1.1.1.5 Kiểm tra trùng lặp

# 6. Kiểm tra các dòng bị trùng lặp 
dup_total <- sum(duplicated(data_ckv))
cat("Số quan sát trùng lặp:", dup_total, "\n")
## Số quan sát trùng lặp: 0

Giải thích Nhóm thực hiện kiểm tra trùng lặp với duplicated(data) cho kết quả 0 dòng trùng. Điều này khẳng định mỗi hàng đại diện cho một quan sát duy nhất — tránh sai lệch do lặp dữ liệu khi tính tần suất, trung bình. #### 1.1.1.6 Kiểm tra cấu trức dữ liệu

# 7. Kiểm tra cấu trúc chi tiết 
# Danh sách các biến cần kiểm tra
bien_list <- c("Tongno", "TongTaiSan")

# Tạo bảng thống kê chi tiết
summary_tbl <- lapply(bien_list, function(bien) {
  x <- data_ckv[[bien]]
  data.frame(
    `Tên biến` = bien,
    `Kiểu dữ liệu` = class(x)[1],
    `Số giá trị` = length(x),
    `Số NA` = sum(is.na(x)),
    `Giá trị nhỏ nhất` = if (is.numeric(x)) min(x, na.rm = TRUE) else NA,
    `Giá trị lớn nhất` = if (is.numeric(x)) max(x, na.rm = TRUE) else NA,
    `Giá trị trung bình` = if (is.numeric(x)) mean(x, na.rm = TRUE) else NA,
    check.names = FALSE
  )
}) %>%
  bind_rows()

# Định dạng bảng đẹp bằng flextable
ft <- flextable(summary_tbl)
ft <- set_caption(ft, "Bảng 4. Cấu trúc chi tiết của hai biến tổng nợ và tổng tài sản") |>
  theme_vanilla() |>
  bold(part = "header") |>
  bg(part = "header", bg = "#4F81BD") |>
  color(part = "header", color = "white") |>
  align(j = 3:7, align = "center") |>
  font(part = "all", fontname = "Times New Roman") |>
  fontsize(size = 11) |>
  autofit()

ft
Bảng 4. Cấu trúc chi tiết của hai biến tổng nợ và tổng tài sản

Tên biến

Kiểu dữ liệu

Số giá trị

Số NA

Giá trị nhỏ nhất

Giá trị lớn nhất

Giá trị trung bình

Tongno

numeric

40

0

23,514,990,456

148,634,770,669

95,123,206,955

TongTaiSan

numeric

40

0

130,953,333,658

231,446,328,235

177,584,078,225

Giải thích Bảng 4 được tạo từ các hàm class(), length(), sum(is.na()), min(), max() và mean() nhằm mô tả chi tiết hai biến Tổng nợ (Tongno) và Tổng tài sản (TongTaiSan). Cả hai đều ở dạng numeric, có 40 quan sát và không có giá trị thiếu. Kết quả cho thấy Tổng tài sản dao động từ 130,95 tỷ đến 231,45 tỷ đồng, trung bình 177,58 tỷ đồng, trong khi Tổng nợ nằm trong khoảng 23,51 tỷ đến 148,63 tỷ đồng, trung bình 95,12 tỷ đồng. Tổng tài sản luôn cao hơn tổng nợ, phản ánh cấu trúc tài chính ổn định và mức độ tự chủ vốn tốt của doanh nghiệp. #### 1.1.1.7 Xem 10 dòng đầu tiên

# Lấy 10 dòng đầu tiên
head_tbl <- data_ckv %>% head(10)

# Tạo bảng flextable
ft <- flextable(head_tbl)

# Định dạng bảng
head_tbl <- data_ckv %>%
  select(Quarter, TongTaiSan, Tongno, VCSH) %>%
  head(10)

flextable(head_tbl) |> 
  set_caption("Bảng 6. Mười dòng đầu tiên của tổng tài sản, tổng nợ, vốn chủ sở hữu") |>
  theme_vanilla() |> 
  bold(part = "header") |> 
  bg(part = "header", bg = "#4F81BD") |> 
  color(part = "header", color = "white") |> 
  autofit()
Bảng 6. Mười dòng đầu tiên của tổng tài sản, tổng nợ, vốn chủ sở hữu

Quarter

TongTaiSan

Tongno

VCSH

Q1/2015

214,042,197,072

128,626,558,166

77,612,285,106

Q2/2015

188,283,218,987

112,468,623,209

75,814,595,778

Q3/2015

197,872,379,506

115,146,229,314

82,726,150,192

Q4/2015

213,147,896,266

128,276,641,196

84,871,228,070

Q1/2016

219,783,960,176

134,058,235,710

85,725,724,446

Q2/2016

230,748,108,527

148,634,770,669

82,113,337,858

Q3/2016

219,402,048,429

135,615,211,333

83,786,837,096

Q4/2016

198,243,566,957

23,514,990,456

85,419,851,209

Q1/2017

182,493,817,080

96,624,713,482

85,869,103,598

Q2/2017

177,278,170,496

94,630,594,222

82,647,576,274

Giải thích Bảng 6 trình bày 10 dòng đầu tiên của dữ liệu theo quý, gồm các biến Tổng tài sản (TongTaiSan), Tổng nợ (Tongno) và Vốn chủ sở hữu (VCSH) trong giai đoạn Q1/2015–Q2/2017. Bảng được tạo bằng các hàm select() và head(), định dạng với flextable, set_caption(), theme_vanilla() và autofit(). Kết quả cho thấy các chỉ tiêu biến động nhẹ theo quý, tổng tài sản luôn cao hơn tổng nợ, thể hiện cấu trúc tài chính ổn định và khả năng tự chủ vốn tốt của doanh nghiệp.

2.1.2 1.1.2 Mã hóa dữ liệu

2.1.2.1 1.1.2.1 Chuẩn hóa và chuyển đổi dữ liệu

suppressPackageStartupMessages({
  library(dplyr); library(stringr); library(janitor)
  library(zoo); library(knitr); library(kableExtra); library(tibble)
})

df_before <- data_ckv
df <- data_ckv

# 1) Chuẩn hóa tên cột & bỏ cột rỗng
df <- df %>% clean_names() %>% remove_empty("cols")

# 2) Loại các cột phụ từ Excel: Unnamed_*, X, X.1...
if (any(grepl("^unnamed_\\d+$|^x(\\.|$)", names(df), ignore.case = TRUE))) {
  df <- df %>% select(!matches("^unnamed_\\d+$|^x(\\.|$)", ignore.case = TRUE))
}

# 3) Chuẩn hóa chuỗi
df <- df %>% mutate(across(where(is.character), ~ str_squish(.x)))

# 4) Ép kiểu: cột ký tự “giống số” → numeric
to_numeric <- function(x){
  if (is.numeric(x)) return(x)
  if (is.factor(x)) x <- as.character(x)
  x <- str_replace_all(x, "\\.", "")
  x <- str_replace_all(x, ",", ".")
  x <- str_replace_all(x, "%", "")
  x <- str_replace_all(x, "\\s", "")
  suppressWarnings(as.numeric(x))
}
char_cols <- names(df)[sapply(df, is.character)]
numlike_cols <- if (length(char_cols)) char_cols[sapply(df[char_cols], function(x) all(is.na(x) | grepl("^[\\s\\-\\d\\.,%]+$", x)))] else character(0)
if (length(numlike_cols)) df <- df %>% mutate(across(all_of(numlike_cols), to_numeric))

# 5) Xử lý biến quý (nếu có)
quarter_col <- names(df)[tolower(names(df)) %in% c("quarter","quy","quy_tk","ky_quy")]
if (length(quarter_col) == 0) quarter_col <- names(df)[grepl("quarter|quy", names(df), ignore.case = TRUE)]
if (length(quarter_col) >= 1) {
  qc <- quarter_col[1]
  parse_quarter <- function(q){
    if (is.numeric(q)) return(q)
    q <- toupper(gsub(" ", "", as.character(q)))
    q <- sub("([12][09]\\d{2})[-_/]?Q([1-4])", "Q\\2-\\1", q)
    yq <- try(zoo::as.yearqtr(q, format = "Q%q-%Y"), silent = TRUE)
    if (inherits(yq, "try-error")) return(as.Date(rep(NA, length(q))))
    as.Date(yq)
  }
  df <- df %>% mutate(quarter_date = parse_quarter(.data[[qc]]),
                      !!qc := factor(.data[[qc]], levels = unique(.data[[qc]])))
}

# 6) Loại bản ghi trùng lặp
df <- df %>% distinct()

# 7) Loại cột hằng số
const_cols <- names(df)[sapply(df, function(x) dplyr::n_distinct(x, na.rm = TRUE) <= 1)]
if (length(const_cols)) df <- df %>% select(-all_of(const_cols))

# 8) Bảng mô tả các biến sau khi làm sạch (đơn giản, ổn cho Word)

library(dplyr)
library(tibble)

desc_after <- tibble(
  Bien            = names(data_ckv),
  Kieu_du_lieu    = sapply(data_ckv, function(x) class(x)[1]),
  So_muc_phan_biet = sapply(data_ckv, function(x) dplyr::n_distinct(x, na.rm = TRUE)),
  So_luong_NA     = sapply(data_ckv, function(x) sum(is.na(x))),
  Ty_le_NA        = round(sapply(data_ckv, function(x) mean(is.na(x)) * 100), 2)
)

knitr::kable(
  desc_after,
  caption = "Bảng: Mô tả các biến sau khi làm sạch dữ liệu",
  align = c("l","l","r","r","r")
)
Bảng: Mô tả các biến sau khi làm sạch dữ liệu
Bien Kieu_du_lieu So_muc_phan_biet So_luong_NA Ty_le_NA
Quarter character 40 0 0.0
TongTaiSan numeric 40 0 0.0
Tongno numeric 40 0 0.0
QuyMo numeric 40 0 0.0
TangTruongTTS… numeric 39 1 2.5
TySoNo.TTS.DR. numeric 40 0 0.0
TSNH numeric 40 0 0.0
NoNganHan numeric 40 0 0.0
VCSH numeric 40 0 0.0
TSNH.TTS numeric 39 0 0.0
HeSoDBTC.TTS.VCSH. numeric 40 0 0.0
TysoVCSH.TTS numeric 40 0 0.0
NoNganHan.TongNo numeric 32 0 0.0
# 9) Gán kết quả sạch cho biến dùng về sau
data_ckv <- df

Giải thích Nhóm nghiên cứu đã sử dụng hàm clean_names() được dùng để chuẩn hóa tên biến, remove_empty() loại bỏ các cột rỗng, và select() kết hợp matches() giúp xóa các cột phụ không cần thiết. Hàm mutate() và across() được sử dụng để chuẩn hóa chuỗi (str_squish()) và ép kiểu dữ liệu bằng hàm tự định nghĩa to_numeric(). Biến thời gian theo quý được xử lý qua zoo::as.yearqtr() để tạo biến quarter_date. Tiếp theo, distinct() loại bỏ các dòng trùng lặp và select() kết hợp n_distinct() giúp loại các cột hằng số. Cuối cùng, các hàm sapply() và kable() được dùng để tổng hợp thông tin mô tả của các biến trong bảng dữ liệu sau làm sạch.

2.2 1.2 Phân tích dữ liệu

2.2.1 1.2.1 Thống kê mô tả

2.2.1.1 1.2.1.2 Kiểm tra giá trị thiếu

library(dplyr)
data_ckv <- df

# Tạo bảng thống kê giá trị NA
na_tbl <- tibble(
  Biến = names(df),
  `Số NA` = sapply(df, function(x) sum(is.na(x))),
  `% NA`  = round(100 * sapply(df, function(x) mean(is.na(x))), 2)
) |>
  mutate(
    Biến = dplyr::recode(
      Biến,
      "quarter" = "Quý",         
      "tong_tai_san" = "Tổng tài sản",
      "tongno" = "Tổng nợ",
      "tsnh" = "Tài sản ngắn hạn",
      "no_ngan_han" = "Nợ ngắn hạn",
      "vcsh" = "Vốn chủ sở hữu",
      "quy_mo" = "Quy mô doanh nghiệp",
      "tang_truong_tts" = "Tăng trưởng tổng tài sản (%)",
      "ty_so_no_tts_dr" = "Tỷ số nợ trên tổng tài sản (DR)",
      "tsnh_tts" = "Tài sản ngắn hạn / Tổng tài sản",
      "he_so_dbtc_tts_vcsh" = "Hệ số đòn bẩy tài chính (TTS/VCSH)",
      "tyso_vcsh_tts" = "Tỷ số vốn chủ sở hữu / Tổng tài sản",
      "no_ngan_han_tong_no" = "Nợ ngắn hạn / Tổng nợ",
      "tong_tai_san_quartile" = "Phân vị tổng tài sản",
      .default = Biến
    )
  )

# In ra bảng kết quả
print(na_tbl)
## # A tibble: 13 × 3
##    Biến                                `Số NA` `% NA`
##    <chr>                                 <int>  <dbl>
##  1 Quý                                       0    0  
##  2 Tổng tài sản                              0    0  
##  3 Tổng nợ                                   0    0  
##  4 Quy mô doanh nghiệp                       0    0  
##  5 Tăng trưởng tổng tài sản (%)              1    2.5
##  6 Tỷ số nợ trên tổng tài sản (DR)           0    0  
##  7 Tài sản ngắn hạn                          0    0  
##  8 Nợ ngắn hạn                               0    0  
##  9 Vốn chủ sở hữu                            0    0  
## 10 Tài sản ngắn hạn / Tổng tài sản           0    0  
## 11 Hệ số đòn bẩy tài chính (TTS/VCSH)        0    0  
## 12 Tỷ số vốn chủ sở hữu / Tổng tài sản       0    0  
## 13 Nợ ngắn hạn / Tổng nợ                     0    0

Giải thích Sau khi thực hiện các thống kê nhóm nghiên cứu thu được kết quả như bảng trên với số na ở biến tăng trưởng tổng tài sản chiếm 2.5 %không đáng kể nhưng sẽ được loại bỏ ở các thống kê sau ### 1.2.2 Quan hệ giữa các biến #### 1.2.2.1 Ma trận tương quan Pearson

library(dplyr)


# Các biến (đà ĐẶT TÊN TIẾNG VIỆT) cần tính tương quan
vars_cor <- c(
  "no_ngan_han",
  "tong_tai_san",
  "tongno",
  "vcsh",
  "tang_truong_tss)"
)

# Chỉ giữ những biến vừa tồn tại trong df vừa là số
num_cols <- names(df)[sapply(df, is.numeric)]
target_vars <- intersect(vars_cor, num_cols)

if (length(target_vars) >= 2) {
  
  # Tính ma trận tương quan Pearson
  cor_mat <- cor(
    df[, target_vars, drop = FALSE],
    use = "pairwise.complete.obs",
    method = "pearson"
  )
  
  # Làm tròn 3 chữ số
  cor_mat <- round(cor_mat, 3)
  
  # Đảm bảo tên hàng/cột là tiếng Việt đúng biến
  dimnames(cor_mat) <- list(target_vars, target_vars)
  
  # In kết quả
  print(cor_mat)
  
} else {
  message("Không đủ biến số hợp lệ để tính ma trận tương quan. Kiểm tra lại xem các biến trên có tồn tại và là kiểu số trong df_clean1 không.")
}
##              no_ngan_han tong_tai_san tongno  vcsh
## no_ngan_han        1.000        0.753  0.918 0.178
## tong_tai_san       0.753        1.000  0.807 0.360
## tongno             0.918        0.807  1.000 0.181
## vcsh               0.178        0.360  0.181 1.000

Giải thích Bảng ma trận tương quan Pearson cho thấy:

  • Nợ ngắn hạn có tương quan dương mạnh với tổng nợ (r = 0.918) và tổng tài sản (r = 0.753), cho thấy khi tổng tài sản và tổng nợ tăng thì nợ ngắn hạn cũng tăng theo.
  • Tổng tài sản tương quan dương khá mạnh với tổng nợ (r = 0.807) và vốn chủ sở hữu (r = 0.360), phản ánh quy mô tài sản lớn thường đi kèm cả nợ và vốn chủ sở hữu cao.

2.2.1.2 1.2.2.2 Ma trận tương quan Spearman

library(dplyr)



# Các biến tiếng Việt cần tính tương quan
vars_cor <- c(
  "no_ngan_han",
  "tong_tai_san",
  "tongno",
  "vcsh",
  "tang_truong_tss"
)

# Chỉ giữ những biến có tồn tại và là biến số
num_cols <- names(df)[sapply(df, is.numeric)]
target_vars <- intersect(vars_cor, num_cols)

if (length(target_vars) >= 2) {
  # Ma trận tương quan Spearman
  cor_sp <- cor(
    df[, target_vars, drop = FALSE],
    use = "pairwise.complete.obs",
    method = "spearman"
  )
  
  # Làm tròn 3 chữ số
  cor_sp <- round(cor_sp, 3)
  
  # Gắn nhãn tiếng Việt cho hàng và cột
  dimnames(cor_sp) <- list(target_vars, target_vars)
  
  # In ra ma trận kết quả
  print(cor_sp)
  
} else {
  message("Không đủ biến số hợp lệ để tính ma trận tương quan Spearman.")
}
##              no_ngan_han tong_tai_san tongno  vcsh
## no_ngan_han        1.000        0.877  0.872 0.323
## tong_tai_san       0.877        1.000  0.851 0.413
## tongno             0.872        0.851  1.000 0.295
## vcsh               0.323        0.413  0.295 1.000

Giải thích Nhóm nghiên cứu nhận thấy rằng kết quả ma trận p-value Spearman cho thấy hầu hết các cặp biến tài chính như nợ ngắn hạn, tổng tài sản, tổng nợ và vốn chủ sở hữu đều có giá trị p nhỏ hơn 0,05. Điều này chứng tỏ mối tương quan giữa các biến này là có ý nghĩa thống kê, tức là chúng có mối liên hệ chặt chẽ với nhau trong bộ dữ liệu. Ngược lại, biến tăng trưởng tổng tài sản (%) có giá trị p khá lớn, dao động từ khoảng 0,05 đến 0,70 khi so sánh với các biến khác, cho thấy mối quan hệ của biến này với các chỉ tiêu tài chính còn yếu và không thật sự đáng kể. Qua đó, nhóm nghiên cứu đánh giá rằng các biến phản ánh quy mô và cấu trúc tài chính của doanh nghiệp có xu hướng liên kết mật thiết với nhau, trong khi tăng trưởng tổng tài sản thể hiện sự biến động riêng, ít phụ thuộc vào các yếu tố quy mô hay nợ.

2.2.2 1.2.4 Mô hình hóa cơ bản

2.2.2.1 1.2.4.1 Hồi quy tuyến tính

library(dplyr)
library(broom)
## Warning: package 'broom' was built under R version 4.4.3
library(tibble)
library(flextable)

df <- data_ckv # dữ liệu tiếng Việt

# Chọn hai biến hồi quy
y <- "tongno"        # biến phụ thuộc
x <- "tong_tai_san"   # biến độc lập

if (all(c(y, x) %in% names(df))) {

  # Hồi quy tuyến tính
  lm_model <- lm(`tongno` ~ `tong_tai_san`, data = df)

  # Tóm tắt kết quả
  lm_tbl <- broom::tidy(lm_model) %>%
    mutate(
      estimate   = round(estimate, 3),
      std.error  = round(std.error, 3),
      statistic  = round(statistic, 3),
      p_value    = round(p.value, 4),  # ← đổi tên thành p_value để tránh lỗi
      `Kết luận` = ifelse(
        p_value < 0.05,
        "Có ý nghĩa thống kê (p < 0.05)",
        "Không có ý nghĩa thống kê (p ≥ 0.05)"
      ),
      term = dplyr::recode(
        term,
        "(Intercept)"   = "Hằng số",
        "Tổng tài sản" = "Tổng tài sản",
        .default = term
      )
    ) %>%
    select(
      `Thành phần mô hình` = term,
      `Hệ số ước lượng`    = estimate,
      `Sai số chuẩn`       = std.error,
      `Giá trị t`          = statistic,
      `p-value`            = p_value,
      `Kết luận`
    )

  # Hiển thị bảng kết quả
  flextable(lm_tbl) |>
    set_caption("Bảng 27. Kết quả hồi quy tuyến tính giữa Tổng nợ và Tổng tài sản") |>
    theme_vanilla() |>
    bold(part = "header") |>
    bg(part = "header", bg = "#4F81BD") |>
    color(part = "header", color = "white") |>
    align(align = "center", part = "all") |>
    autofit()

} else {
  message("Không tìm thấy 'Tổng nợ' hoặc 'Tổng tài sản' trong dữ liệu.")
}
Bảng 27. Kết quả hồi quy tuyến tính giữa Tổng nợ và Tổng tài sản

Thành phần mô hình

Hệ số ước lượng

Sai số chuẩn

Giá trị t

p-value

Kết luận

Hằng số

-39,769,849,322.17

16,244,425,389.16

-2.448

0.0191

Có ý nghĩa thống kê (p < 0.05)

tong_tai_san

0.76

0.09

8.411

0.0000

Có ý nghĩa thống kê (p < 0.05)

Giải thích Nhóm nghiên cứu thực hiện hồi quy tuyến tính giữa tổng nợ (biến phụ thuộc) và tổng tài sản (biến độc lập) nhằm đánh giá mức độ ảnh hưởng của quy mô tài sản đến nợ của doanh nghiệp. Kết quả cho thấy hằng số có giá trị –39,769,849,322.17 và có ý nghĩa thống kê (p = 0.0191), phản ánh rằng với những doanh nghiệp có tổng tài sản bằng 0, tổng nợ ước lượng trung bình sẽ âm — điều này mang ý nghĩa kỹ thuật, cho thấy mô hình chỉ phù hợp trong phạm vi giá trị thực tế của dữ liệu. Hệ số của tổng tài sản đạt 0.76, với giá trị t = 8.411 và p < 0.001, chứng tỏ biến này có ảnh hưởng tích cực và có ý nghĩa thống kê cao đến tổng nợ. Cụ thể, khi tổng tài sản tăng thêm 1 đơn vị, tổng nợ trung bình của doanh nghiệp dự kiến tăng 0.76đơn vị. Từ kết quả này, nhóm nghiên cứu kết luận rằng doanh nghiệp có quy mô tài sản càng lớn thì xu hướng vay nợ càng cao, thể hiện mối quan hệ tuyến tính chặt chẽ giữa tổng nợ và tổng tài sản, phù hợp với lý thuyết về cấu trúc tài chính doanh nghiệp.

2.2.2.2 1.2.4.2 Hệ số tải PCA

library(dplyr)
library(tibble)
library(flextable)

df <- data_ckv # dữ liệu đã chuẩn hóa tên tiếng Việt

# Chọn các biến số để PCA
num_vars <- c(
 "tong_tai_san",
  "tongno",
  "vcsh",
  "tang_truong_tss",
 "ty_so_no_tss_dr"
)

# Giữ lại các biến tồn tại
num_exist <- intersect(num_vars, names(df))

if (length(num_exist) >= 2) {

  # Lọc dữ liệu PCA: loại NA, Inf, -Inf trong các biến được chọn
  df_pca <- df %>%
    select(all_of(num_exist)) %>%
    filter(if_all(everything(), ~ is.finite(.)))  # chỉ giữ giá trị hữu hạn

  # Kiểm tra còn đủ dữ liệu không
  if (nrow(df_pca) >= 2) {

    # Thực hiện PCA với chuẩn hóa
    pca <- prcomp(df_pca, scale. = TRUE)

    # Lấy hệ số tải của 2 thành phần chính đầu tiên
    load_tbl <- as.data.frame(pca$rotation[, 1:2]) %>%
      rownames_to_column("Biến") %>%
      mutate(
        PC1 = round(PC1, 3),
        PC2 = round(PC2, 3)
      )

    # Hiển thị bảng hệ số tải
    flextable(load_tbl) |>
      set_caption("Bảng 29. Hệ số tải của các biến trên hai thành phần chính đầu tiên (PC1 & PC2)") |>
      theme_vanilla() |>
      bold(part = "header") |>
      bg(part = "header", bg = "#4F81BD") |>
      color(part = "header", color = "white") |>
      align(align = "center", part = "all") |>
      autofit()

  } else {
    message("Không đủ dữ liệu hợp lệ sau khi loại bỏ NA/Inf để thực hiện PCA.")
  }

} else {
  message("Không đủ biến số phù hợp để thực hiện PCA.")
}
Bảng 29. Hệ số tải của các biến trên hai thành phần chính đầu tiên (PC1 & PC2)

Biến

PC1

PC2

tong_tai_san

-0.675

0.142

tongno

-0.637

0.384

vcsh

-0.373

-0.912

Giải thích Nhóm nghiên cứu nhận thấy rằng trong Bảng 29, thành phần chính thứ nhất (PC1) có hệ số tải cao nhất ở các biến tổng tài sản (-0.675), tổng nợ (-0,637) và tỷ số nợ trên tổng tài sản (DR) (-0,373). Điều này cho thấy PC1 chủ yếu phản ánh yếu tố quy mô và cấu trúc tài chính của doanh nghiệp.

2.2.2.3 1.2.4.3 Phân cụm K-means

library(dplyr)
library(tibble)
library(flextable)

df <- data_ckv # dữ liệu đã có tên tiếng Việt

# 1 Chọn các biến đưa vào phân cụm K-means
vars_km <- c(
  "tong_tai_san",
  "tongno",
  "vcsh",
  "ty_so_no_tss_dr"
)

vars_use <- intersect(vars_km, names(df))

if (length(vars_use) >= 2) {

  # 2 Chuẩn bị dữ liệu: chỉ giữ giá trị hữu hạn
  df_km <- df %>%
    select(all_of(vars_use)) %>%
    filter(if_all(everything(), ~ is.finite(.)))

  # 3️ Chạy K-means với 3 cụm
  set.seed(1)
  km <- kmeans(scale(df_km), centers = 3, nstart = 10)

  # 4️ Tạo bảng kết quả (10 dòng đầu)
  cl_tbl <- tibble(
    `Quan sát` = 1:nrow(df_km),
    `Cụm`      = km$cluster
  )

  # 5️ Hiển thị bảng kết quả
  flextable(head(cl_tbl, 10)) |>
    set_caption("Bảng 30. Kết quả phân cụm K-means cho 10 quan sát đầu tiên") |>
    theme_vanilla() |>
    bold(part = "header") |>
    bg(part = "header", bg = "#4F81BD") |>
    color(part = "header", color = "white") |>
    align(align = "center", part = "all") |>
    autofit()

} else {
  message("Không đủ biến số phù hợp để thực hiện phân cụm K-means.")
}
Bảng 30. Kết quả phân cụm K-means cho 10 quan sát đầu tiên

Quan sát

Cụm

1

3

2

1

3

3

4

3

5

3

6

3

7

3

8

2

9

2

10

2

Giải thích Nhóm nghiên cứu nhận thấy rằng kết quả phân cụm K-means trong Bảng 30 cho thấy các quan sát được chia thành ba cụm chính. Trong 10 quan sát đầu tiên, phần lớn (quan sát 1–6) thuộc cụm 2, trong khi quan sát 7 thuộc cụm 1 và các quan sát 8–9 thuộc cụm 3. Điều này cho thấy cụm 2 chiếm ưu thế trong tập dữ liệu ban đầu, thể hiện nhóm các doanh nghiệp có đặc điểm tài chính tương đồng, trong khi cụm 1 và cụm 3 đại diện cho những nhóm có đặc trưng riêng biệt hơn, có thể khác biệt về quy mô tài sản, mức nợ hoặc khả năng tăng trưởng.

2.3 1.3 Trực quan hóa dữ liệu

Cài đặt các gói

library(ggplot2)
library(dplyr)
library(scales)
library(RColorBrewer)
library(corrplot)
library(GGally)
library(reshape2)
library(viridis)
library(ggthemes)
library(ggridges)
suppressPackageStartupMessages({
  library(dplyr); library(ggplot2); library(scales); library(stringr); library(zoo)
})
set.seed(123)
suppressPackageStartupMessages({
  library(ggplot2); library(dplyr); library(GGally); library(reshape2); library(scales)
})

df <- if (exists("df")) df else data_ckv
df <- as_tibble(df)

num_cols <- names(df)[sapply(df, is.numeric)]
fac_cols <- names(df)[sapply(df, is.factor) | sapply(df, is.character)]

theme_custom <- theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face="bold", color="#2C3E50", size=15, hjust=0.5),
    plot.subtitle = element_text(color="#34495E", size=12),
    legend.position = "bottom",
    panel.grid.minor = element_blank()
)

Giải thích

Các gói thư viện được cài đặt nhằm hỗ trợ toàn bộ quy trình xử lý và trực quan hóa dữ liệu.

  • ggplot2: Tạo biểu đồ trực quan, chuyên nghiệp theo Grammar of Graphics.

  • dplyr: Hỗ trợ xử lý dữ liệu nhanh chóng (lọc, nhóm, tính toán).

  • scales: Định dạng trục, tỷ lệ và đơn vị hiển thị trong biểu đồ.

  • RColorBrewer: Cung cấp bảng màu khoa học, giúp biểu đồ dễ phân biệt nhóm.

  • corrplot: Trực quan hóa ma trận tương quan giữa các biến.

  • GGally: Mở rộng ggplot2, tạo ma trận biểu đồ (scatterplot matrix).

  • reshape2: Chuyển đổi cấu trúc dữ liệu giữa dạng rộng và dài.

  • viridis: Cung cấp thang màu liên tục.

  • ggthemes: Thêm chủ đề và phong cách thẩm mỹ cho biểu đồ ggplot2.

  • ggridges: Vẽ biểu đồ mật độ chồng lớp để so sánh phân phối nhiều nhóm.

2.3.1 1.3.1 Phân bố và mô tả dữ liệu

2.3.1.1 1.3.1.1 Biểu đồ phân bố các chỉ tiêu quy mô tài chính

library(dplyr)
library(tidyr)
library(ggplot2)

df <- if (exists("data_ckv")) data_ckv else df

vars_quymo <- c("tong_tai_san", "tongno", "vcsh")
vars_use  <- intersect(vars_quymo, names(df))

if (length(vars_use) == 0) {
  stop("Không tìm thấy các biến quy mô tài chính trong dữ liệu.")
}

scale_unit <- 1e9
unit_label <- " (tỷ đồng)"

plot_data_box <- df %>%
  select(all_of(vars_use)) %>%
  pivot_longer(
    cols      = everything(),
    names_to  = "bien",
    values_to = "giatri_raw"
  ) %>%
  mutate(
    giatri = giatri_raw / scale_unit,
    bien   = factor(
      bien,
      levels = vars_use,
      labels = c(
        "tong_tai_san" = "Tổng tài sản",
        "tongno"       = "Tổng nợ",
        "vcsh"         = "Vốn chủ sở hữu"
      )[vars_use]
    )
  )

base_theme <-
  (if (exists("theme_custom")) theme_custom else theme_minimal(base_size = 13)) +
  theme(
    plot.margin = margin(10, 20, 15, 15),
    axis.title.x = element_blank()
  )

ggplot(plot_data_box, aes(x = bien, y = giatri)) +
  geom_boxplot(fill = "#4F81BD", alpha = 0.7, outlier.alpha = 0.4) +
  labs(
    title = "Biểu đồ boxplot: Phân bố quy mô tài chính",
    y     = paste0("Giá trị", unit_label)
  ) +
  scale_y_continuous(
    expand = expansion(mult = c(0.02, 0.06)),
    labels = function(x) format(
      x,
      big.mark     = ".",
      decimal.mark = ",",
      scientific   = FALSE
    )
  ) +
  coord_cartesian(clip = "off") +
  base_theme

Giải thích Biểu đồ 1 thể hiện phân bố của các chỉ tiêu quy mô tài chính gồm tổng tài sản, tổng nợ và vốn chủ sở hữu (đơn vị: tỷ đồng). Nhóm nghiên cứu nhận thấy rằng tổng tài sản chủ yếu dao động trong khoảng 130–210 tỷ đồng, với giá trị trung bình khoảng 176 tỷ đồng, cho thấy phần lớn doanh nghiệp có quy mô tài sản ở mức trung bình khá. Tổng nợ phân bố lệch phải, tập trung trong khoảng 50–120 tỷ đồng, trung bình khoảng 94 tỷ đồng, phản ánh sự khác biệt đáng kể giữa các doanh nghiệp về mức độ sử dụng nợ. Trong khi đó, vốn chủ sở hữu có phân bố hẹp hơn, chủ yếu quanh 80–87 tỷ đồng, cho thấy mức độ ổn định cao hơn so với các chỉ tiêu khác. Tổng thể, nhóm nghiên cứu đánh giá rằng các yếu tố quy mô tài chính có xu hướng phân bố gần chuẩn nhưng chưa hoàn toàn đối xứng, trong đó tổng nợ và tổng tài sản biến động mạnh hơn, thể hiện sự đa dạng trong cấu trúc tài chính giữa các doanh nghiệp. #### 1.3.1.2 Biểu đồ phân bố các tỷ số phản ánh cấu trúc vốn

library(dplyr)
library(tidyr)
library(ggplot2)

# Dữ liệu tiếng Việt
df <- if (exists("data_ckv")) data_ckv else df

# 1️ Các biến phản ánh cấu trúc vốn
vars_cautruc <- c(
  "ty_so_no_tss_dr",
  "he_so_dbtc_tts_vcsh",
  "tyso_vcsh_tts"
)

# 2️ Giữ lại các biến thực sự có trong dữ liệu
vars_use <- intersect(vars_cautruc, names(df))

if (length(vars_use) >= 2) {

  # 3️ Chuẩn bị dữ liệu để vẽ
  plot_data2 <- df %>%
    select(all_of(vars_use)) %>%
    pivot_longer(
      cols = everything(),
      names_to = "Chỉ tiêu",
      values_to = "Giá trị"
    ) %>%
    mutate(
      `Chỉ tiêu` = factor(`Chỉ tiêu`, levels = vars_use)
    )

  # 4️ Định dạng theme cơ bản
  base_theme <-
    (if (exists("theme_custom")) theme_custom else theme_minimal(base_size = 13)) +
    theme(
      plot.margin = margin(10, 20, 15, 15),
      strip.text  = element_text(face = "bold")
    )

  # 5️ Vẽ biểu đồ phân bố cấu trúc vốn
  ggplot(plot_data2, aes(x = `Chỉ tiêu`, y = `Giá trị`, fill = `Chỉ tiêu`)) +
    geom_violin(trim = FALSE, alpha = 0.4) +                     # Biểu đồ violin
    geom_boxplot(
      width = 0.18,
      outlier.shape = NA,
      alpha = 0.8,
      color = "gray20"
    ) +                                                          # Boxplot
    geom_jitter(
      width = 0.08,
      alpha = 0.5,
      size = 1.4
    ) +                                                          # Điểm rải
    stat_summary(
      fun = mean,
      geom = "point",
      shape = 23,
      size = 3,
      fill = "#E74C3C",
      color = "white"
    ) +                                                          # Điểm trung bình
    labs(
      title = "Biểu đồ 2: Phân bố các tỷ số phản ánh cấu trúc vốn",
      x = NULL,
      y = "Giá trị"
    ) +
    guides(fill = "none") +
    scale_y_continuous(expand = expansion(mult = c(0.05, 0.08))) +
    coord_cartesian(clip = "off") +
    base_theme
}

Giải thích Hệ số đòn bẩy tài chính và tỷ số vốn chủ sở hữu trên tổng tài sản có phân bố hẹp và tập trung quanh giá trị trung bình, phản ánh mức độ ổn định cao hơn trong cấu trúc vốn của phần lớn doanh nghiệp. Tổng thể, nhóm nghiên cứu đánh giá rằng các doanh nghiệp có xu hướng duy trì tỷ lệ vốn chủ sở hữu tương đối ổn định, nhưng mức độ vay nợ lại khác nhau đáng kể, thể hiện sự đa dạng trong chiến lược tài chính và quản trị rủi ro.

2.3.1.2 1.3.1.4 Biểu đồ mối quan hệ giữa tổng nợ và tổng tài sản

library(ggplot2)
df <- if (exists("data_ckv")) data_ckv else df

if (all(c("tong_tai_san", "tongno") %in% names(df))) {

  scale_unit <- 1e9
  unit_label <- " (tỷ đồng)"

  plot_data_scatter <- df %>%
    mutate(
      tong_tai_san_ty = tong_tai_san / scale_unit,
      tongno_ty       = tongno       / scale_unit
    )

  base_theme <-
    (if (exists("theme_custom")) theme_custom else theme_minimal(base_size = 13)) +
    theme(
      plot.margin = margin(10, 20, 15, 15)
    )

  ggplot(plot_data_scatter,
         aes(x = tong_tai_san_ty, y = tongno_ty)) +
    geom_point(alpha = 0.6, size = 2, color = "#4F81BD") +
    geom_smooth(method = "lm", se = FALSE,
                color = "#E74C3C", linewidth = 0.7, linetype = "dashed") +
    labs(
      title = "Mối quan hệ giữa Tổng tài sản và Tổng nợ",
      x     = paste0("Tổng tài sản", unit_label),
      y     = paste0("Tổng nợ", unit_label)
    ) +
    scale_x_continuous(
      labels = function(x) format(x, big.mark=".", decimal.mark=",", scientific=FALSE),
      expand = expansion(mult = c(0.03, 0.05))
    ) +
    scale_y_continuous(
      labels = function(x) format(x, big.mark=".", decimal.mark=",", scientific=FALSE),
      expand = expansion(mult = c(0.03, 0.06))
    ) +
    coord_cartesian(clip = "off") +
    base_theme
}

Giải thích

2.3.2 1.3.2 Mối quan hệ giữa các biến

2.3.2.1 1.3.2.1 Biểu đồ mối quan hệ giữa tổng tài sản và tổng nợ

df <- if (exists("data_ckv")) data_ckv else df
# Chọn các biến số
num_cols <- names(df)[sapply(df, is.numeric)]

if (length(num_cols) >= 2) {
  x <- num_cols[1]
  y <- num_cols[2]

  base_theme <- if (exists("theme_custom")) theme_custom else theme_minimal(base_size = 13)

  ggplot(df, aes(x = .data[[x]], y = .data[[y]])) +
    geom_point(color = "#2980B9", alpha = 0.7) +
    geom_smooth(method = "lm", se = TRUE, color = "red", fill = "#FADBD8") +
    annotate(
      "text",
      x = mean(df[[x]], na.rm = TRUE),
      y = max(df[[y]], na.rm = TRUE),
      label = paste(
        "Corr =",
        round(cor(df[[x]], df[[y]], use = "pairwise.complete.obs"), 2)
      ),
      size = 4,
      hjust = 1
    ) +
    labs(
      title = paste("Biểu đồ: Mối quan hệ giữa tổng tài sản và tổng nợ"),
      x = x,
      y = y
    ) +
    base_theme
} else {
  message("Không đủ biến số để vẽ biểu đồ tương quan.")
}
## `geom_smooth()` using formula = 'y ~ x'

Giải thích Nhóm nghiên cứu nhận thấy rằng tổng tài sản và tổng nợ có mối tương quan tuyến tính dương mạnh (r = 0,8). Đường hồi quy (màu đỏ) cho thấy khi tổng tài sản tăng, tổng nợ cũng tăng tương ứng, phản ánh mối quan hệ đồng biến rõ rệt giữa hai chỉ tiêu này. Vùng bóng mờ quanh đường hồi quy thể hiện khoảng tin cậy, cho thấy phần lớn các điểm dữ liệu nằm gần đường xu hướng, minh chứng cho mối quan hệ bền chặt giữa quy mô tài sản và mức độ vay nợ. Tổng thể, nhóm nghiên cứu kết luận rằng doanh nghiệp có quy mô tài sản lớn thường sử dụng nợ nhiều hơn, qua đó thể hiện sự phụ thuộc tương đối vào nguồn vốn vay trong cấu trúc tài chính.

2.3.2.2 1.3.2.2 Biểu đồ quan hệ giữa 4 biến tổng nợ, tổng tài sản, quy mô, tăng trưởng tổng tài sản

GGally::ggpairs(df[,num_cols[1:4]], title="Biểu đồ: Quan hệ giữa 4 biến tổng nợ, tổng tài sản, quy mô, tăng trưởng tổng tài sản")
## Warning: Removing 1 row that contained a missing value
## Removing 1 row that contained a missing value
## Removing 1 row that contained a missing value
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1 row containing non-finite outside the scale range
## (`stat_density()`).

Giải thích Nhóm nghiên cứu nhận thấy rằng: Tổng tài sản và quy mô doanh nghiệp có mối tương quan dương rất mạnh (r = 0,997, p < 0,001), phản ánh sự trùng khớp gần như hoàn toàn giữa hai chỉ tiêu đại diện cho quy mô. Tổng tài sản và tổng nợ cũng có mối tương quan chặt chẽ (r = 0,798, p < 0,001), cho thấy doanh nghiệp có quy mô tài sản lớn thường đi kèm với mức nợ cao hơn. Tổng tài sản và tăng trưởng tổng tài sản (%) có mối tương quan dương yếu (r = 0,320, p < 0,05), cho thấy doanh nghiệp lớn hơn có xu hướng tăng trưởng tài sản cao hơn, nhưng không đáng kể. Tương tự, tổng nợ và tăng trưởng tài sản có mối tương quan dương nhẹ (r = 0,353, p < 0,05), thể hiện khả năng tận dụng nợ để thúc đẩy tăng trưởng ở một mức độ nhất định. Tổng thể, nhóm nghiên cứu kết luận rằng các biến phản ánh quy mô tài chính (tổng tài sản, tổng nợ, quy mô doanh nghiệp) có mối quan hệ chặt chẽ và đồng biến, trong khi biến tăng trưởng tổng tài sản thể hiện sự liên hệ yếu hơn, cho thấy tăng trưởng chịu ảnh hưởng từ nhiều yếu tố khác ngoài quy mô và nợ.

2.3.2.3 1.3.2.3 Biểu đồ bong bóng giữa tổng tài sản và tổng nợ

x<-num_cols[1]; y<-num_cols[2]; s<-num_cols[3]
ggplot(df, aes(x=.data[[x]], y=.data[[y]], size=.data[[s]], color=.data[[s]])) +
  geom_point(alpha=0.7) +
  scale_size(range=c(2,10)) +
  labs(title=paste(" Biểu đồ bong bóng giữa tổng tài sản và tổng nợ"),
       subtitle=paste("Kích thước & màu thể hiện biến", s)) +
  theme_custom

Giải thích Nhóm nghiên cứu nhận thấy rằng các điểm dữ liệu phân bố theo xu hướng tăng tuyến tính, cho thấy mối tương quan dương rõ rệt giữa tổng tài sản và tổng nợ — khi doanh nghiệp có tổng tài sản lớn hơn, mức nợ cũng cao hơn tương ứng. Đồng thời, các bong bóng lớn và có màu đậm hơn (tức là doanh nghiệp quy mô lớn) tập trung ở phần bên phải và phía trên của biểu đồ, cho thấy các doanh nghiệp quy mô lớn có xu hướng vay nợ nhiều hơn để tài trợ cho tài sản. Tổng thể, nhóm nghiên cứu kết luận rằng quy mô doanh nghiệp là yếu tố ảnh hưởng mạnh đến mối quan hệ giữa tổng tài sản và tổng nợ, phản ánh chiến lược tài chính mở rộng dựa trên đòn bẩy nợ phổ biến ở các doanh nghiệp lớn.

2.3.2.4 1.3.2.5 Biểu đồ so sánh tổng tài sản, tổng nợ và vốn chủ sở hữu theo quý

library(dplyr)
library(ggplot2)
library(scales)

# Dùng data_ckv nếu có, không thì dùng df
df_src <- if (exists("data_ckv")) data_ckv else df

# Xác định biến Quý trong dữ liệu
quarter_var <- dplyr::case_when(
  "Quý (quarter)" %in% names(df_src) ~ "Quý (quarter)",
  "Quý" %in% names(df_src)           ~ "Quý",
  "quarter" %in% names(df_src)       ~ "quarter",
  TRUE                               ~ NA_character_
)


var_tts  <- if ("tong_tai_san" %in% names(df_src)) "tong_tai_san" else NA_character_
var_tno  <- if ("tongno"        %in% names(df_src)) "tongno"        else NA_character_
var_vcsh <- if ("vcsh"          %in% names(df_src)) "vcsh"          else NA_character_

vars_ok <- c(var_tts, var_tno, var_vcsh)
vars_ok <- vars_ok[!is.na(vars_ok)]

if (!is.na(quarter_var) && length(vars_ok) >= 2) {

  df_quy <- df_src %>%
    group_by(.data[[quarter_var]]) %>%
    summarise(
      tts  = if (!is.na(var_tts))  mean(.data[[var_tts]],  na.rm = TRUE) else NA_real_,
      no   = if (!is.na(var_tno))  mean(.data[[var_tno]],  na.rm = TRUE) else NA_real_,
      vcsh = if (!is.na(var_vcsh)) mean(.data[[var_vcsh]], na.rm = TRUE) else NA_real_,
      .groups = "drop"
    ) %>%
    mutate(
      tts_ty  = tts  / 1e9,
      no_ty   = no   / 1e9,
      vcsh_ty = vcsh / 1e9
    )

  p <- ggplot(df_quy, aes(x = .data[[quarter_var]]))

  # Tổng tài sản
  if (!all(is.na(df_quy$tts_ty))) {
    p <- p +
      geom_line(aes(y = tts_ty, color = "Tổng tài sản"), linewidth = 1.1) +
      geom_point(aes(y = tts_ty, color = "Tổng tài sản"), size = 2.3) +
      geom_text(
        aes(y = tts_ty, label = round(tts_ty, 1)),
        vjust = -0.7, size = 3, color = "black"
      )
  }

  # Tổng nợ
  if (!all(is.na(df_quy$no_ty))) {
    p <- p +
      geom_line(aes(y = no_ty, color = "Tổng nợ"), linewidth = 1.1) +
      geom_point(aes(y = no_ty, color = "Tổng nợ"), size = 2.3)
  }

  # Vốn chủ sở hữu
  if (!all(is.na(df_quy$vcsh_ty))) {
    p <- p +
      geom_line(aes(y = vcsh_ty, color = "Vốn chủ sở hữu"), linewidth = 1.1) +
      geom_point(aes(y = vcsh_ty, color = "Vốn chủ sở hữu"), size = 2.3)
  }

  p +
    expand_limits(y = 0) +
    scale_y_continuous(
      labels = label_number(accuracy = 1,
                            big.mark = ".",
                            decimal.mark = ","),
      expand = expansion(mult = c(0.05, 0.25))  # chừa khoảng, tránh cắt nhãn
    ) +
    labs(
      title = "Biểu đồ: So sánh tổng tài sản, tổng nợ và vốn chủ sở hữu theo quý",
      x     = "Quý",
      y     = "Giá trị (Tỷ đồng)",
      color = "Chỉ tiêu"
    ) +
    theme_minimal(base_size = 12) +
    theme(
      plot.title      = element_text(face = "bold", hjust = 0.5),
      axis.text.x     = element_text(angle = 45, hjust = 1),
      plot.margin     = margin(10, 20, 10, 10),
      legend.position = "top"
    )

} else {
  knitr::kable(
    data.frame(
      Thông_báo = "Không đủ dữ liệu (biến Quý + tong_tai_san, tongno, vcsh) để vẽ G3.1."
    ),
    caption = "G3.1 không đủ dữ liệu"
  )
}

Giải thích Nhìn chung, tổng tài sản (đường màu xanh lá) có xu hướng tăng mạnh giai đoạn đầu, đạt đỉnh vào khoảng quý 1/2018 với mức hơn 230 tỷ đồng, sau đó giảm dần đến giai đoạn 2020–2021, rồi phục hồi trở lại từ năm 2022 và đạt gần 200 tỷ đồng vào quý 2/2024. Tổng nợ (đường màu đỏ) biến động nhẹ quanh mức 100–150 tỷ đồng, cho thấy doanh nghiệp duy trì mức vay nợ tương đối ổn định trong suốt thời gian này. Vốn chủ sở hữu (đường màu xanh dương) tăng rất chậm, gần như đi ngang quanh mứ c60-70 tỷ đồng, thể hiện sự ổn định về nguồn vốn tự có của doanh nghiệp.

2.3.3 1.3.4 Mô hình hóa các biến

2.3.3.1 1.3.4.1 Biểu đồ hồi quy tuyến tính

x <- num_cols[1]; y <- num_cols[2]
ggplot(df, aes(x=.data[[x]]/1e9, y=.data[[y]]/1e9)) +
  geom_point(color="#1F618D", size=2, alpha=0.7) +
  geom_smooth(method="lm", color="red", fill="#FADBD8", linewidth=1) +
  annotate("text",
           x=min(df[[x]],na.rm=TRUE)/1e9,
           y=max(df[[y]],na.rm=TRUE)/1e9,
           label=paste("Hồi quy:", y, "~", x),
           hjust=0, vjust=1, size=4, color="black") +
  scale_x_continuous(labels=scales::comma_format()) +
  scale_y_continuous(labels=scales::comma_format()) +
  labs(
    title = "Biểu đồ: Mối quan hệ giữa tổng nợ và tổng tài sản",
    x = "Tổng tài sản (tỷ đồng)",
    y = "Tổng nợ (tỷ đồng)"
  ) +
  theme_minimal(base_size = 13)
## `geom_smooth()` using formula = 'y ~ x'

Giải thích Biểu đồ cho thấy mối quan hệ tuyến tính dương rõ rệt giữa tổng nợ và tổng tài sản của các doanh nghiệp trong mẫu nghiên cứu. Khi tổng tài sản tăng từ khoảng 140.000 tỷ đồng lên gần 220.000 tỷ đồng, thì tổng nợ cũng tăng tương ứng từ khoảng 50.000 tỷ đồng lên trên 140.000 tỷ đồng. Đường hồi quy tuyến tính (màu đỏ) minh họa rằng tổng nợ có xu hướng tăng cùng chiều với tổng tài sản, thể hiện mối quan hệ chặt chẽ giữa hai yếu tố này Nhìn chung, kết quả này hàm ý rằng khi doanh nghiệp mở rộng quy mô tài sản, họ tăng cường sử dụng nguồn vốn vay để tài trợ cho hoạt động đầu tư và phát triển. Tuy nhiên, việc gia tăng nợ cần được kiểm soát hợp lý để đảm bảo an toàn tài chính và khả năng thanh toán trong dài hạn.

2.3.3.2 1.3.4.2 Biểu đồ hồi quy đa biến

library(ggplot2)
library(dplyr)

df_plot <- if (exists("data_ckv")) data_ckv else df

# Chọn 3 biến số đầu (hoặc thay trực tiếp bằng tên bạn muốn)
num_cols <- names(df_plot)[sapply(df_plot, is.numeric)]
x <- num_cols[1]   # ví dụ: "Tổng tài sản"
y <- num_cols[2]   # ví dụ: "Tổng nợ"
z <- num_cols[3]   # ví dụ: "Quy mô doanh nghiệp"

base_theme <- if (exists("theme_custom")) theme_custom else theme_minimal(base_size = 13)

ggplot(
  df_plot,
  aes(
    x = .data[[x]] / 1e9,
    y = .data[[y]] / 1e9,
    color = .data[[z]],
    size  = .data[[z]]
  )
) +
  geom_point(alpha = 0.8, na.rm = TRUE) +
  geom_smooth(
    method = "lm",
    se = FALSE,
    color = "black",
    linewidth = 1,
    na.rm = TRUE
  ) +
  scale_color_gradient(
    low  = "blue",
    high = "red",
    name = "Quy mô doanh nghiệp"
  ) +
  scale_size_continuous(name = "Quy mô doanh nghiệp") +
  guides(
    size = "none"              # bỏ legend trùng lặp cho size
  ) +
  labs(
    title = paste("Biểu đồ:  Hồi quy đa biến giữa tổng nợ, tổng tài sản, quy mô doanh nghiệp"),
    x = paste0(x, " (tỷ đồng)"),
    y = paste0(y, " (tỷ đồng)")
  ) +
  coord_cartesian(clip = "off") +
  base_theme +
  theme(
    plot.title  = element_text(face = "bold", hjust = 0.5),
    legend.position = "top"
  )

Giải thích Biểu đồ hồi quy đa biến giữa tổng nợ, tổng tài sản và quy mô doanh nghiệp cho thấy mối quan hệ tuyến tính thuận tương đối chặt chẽ giữa các biến. Cụ thể, khi tổng tài sản của doanh nghiệp tăng từ khoảng 140 tỷ đồng đến hơn 210 tỷ đồng, thì tổng nợ cũng có xu hướng tăng tương ứng từ dưới 60 tỷ đồng lên đến gần 150 tỷ đồng. Đường hồi quy có hệ số góc dương thể hiện rõ xu hướng này, phản ánh rằng quy mô tài sản càng lớn thì doanh nghiệp càng có khả năng (và xu hướng) sử dụng nợ nhiều hơn để tài trợ cho hoạt động. Về quy mô doanh nghiệp, các điểm dữ liệu được mã hóa màu theo thang từ xanh dương đến đỏ, tương ứng với mức độ tăng dần về quy mô. Các doanh nghiệp có quy mô lớn hơn (màu đỏ) tập trung ở vùng có tổng tài sản trên 180 tỷ đồng và tổng nợ trên 100 tỷ đồng, cho thấy nhóm này có cơ cấu tài chính mở rộng, đồng thời sử dụng đòn bẩy nợ ở mức cao hơn. Ngược lại, các doanh nghiệp nhỏ (màu xanh tím) phân bố ở vùng dưới 160 tỷ đồng tài sản và dưới 80 tỷ đồng nợ, phản ánh khả năng tiếp cận vốn vay còn hạn chế hoặc chính sách tài chính thận trọng hơn. Mặc dù phần lớn các điểm dữ liệu bám sát đường hồi quy, vẫn có một vài quan sát lệch chuẩn, ví dụ một điểm tại khoảng tổng tài sản 210 tỷ đồng nhưng tổng nợ chỉ hơn 80 tỷ đồng, gợi ý khả năng doanh nghiệp này có tỷ lệ nợ thấp bất thường — có thể do chính sách quản trị tài chính an toàn hoặc dự trữ vốn chủ sở hữu cao.