Chương 1: Phân tích Hoạt động Kinh doanh trong Lĩnh vực Bán lẻ Xe đạp

Giới thiệu

Trong bối cảnh thị trường cạnh tranh ngày càng gay gắt, việc phân tích dữ liệu kinh doanh đóng vai trò then chốt giúp doanh nghiệp thấu hiểu khách hàng, tối ưu hóa hoạt động và đưa ra các quyết định chiến lược đúng đắn. Bài tiểu luận này sẽ thực hiện một quy trình phân tích toàn diện trên bộ dữ liệu giả định về hoạt động bán xe đạp của một chuỗi cửa hàng trong giai đoạn từ 2022 đến 2024. Cấu trúc bài tiểu luận bao gồm 4 phần chính:

  1. Giới thiệu bộ dữ liệu: Khám phá cấu trúc và các đặc điểm ban đầu của dữ liệu.

  2. Xử lý và làm sạch dữ liệu: Chuẩn bị dữ liệu cho quá trình phân tích, bao gồm việc tạo các biến mới, xử lý các vấn đề tiềm ẩn và mã hóa dữ liệu.

  3. Thống kê cơ bản: Thực hiện các phép tính thống kê mô tả để trả lời các câu hỏi kinh doanh cụ thể.

  4. Trực quan hóa dữ liệu: Sử dụng các biểu đồ đa dạng để minh họa các kết quả phân tích một cách sinh động và dễ hiểu.

Toàn bộ quá trình phân tích sẽ được thực hiện bằng ngôn ngữ lập trình R, một công cụ mạnh mẽ trong lĩnh vực khoa học dữ liệu.

1. Tổng quan dữ liệu

Bước đầu tiên trong mọi dự án phân tích là làm quen với bộ dữ liệu. Chúng ta cần hiểu rõ về cấu trúc, ý nghĩa của từng cột và các đặc điểm cơ bản của dữ liệu.

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

Bộ dữ liệu “Bike Sales 100K” được nhóm tác giả thu thập từ nền tảng Kaggle. Đây là bộ dữ liệu mô phỏng hoạt động kinh doanh trong lĩnh vực bán lẻ xe đạp, được thiết kế nhằm phục vụ cho mục đích học tập, nghiên cứu và thực hành phân tích dữ liệu.

Dữ liệu bao gồm khoảng 100.000 quan sát và 11 biến, mỗi quan sát tương ứng với một giao dịch mua xe đạp của khách hàng. Các biến có thể được phân loại thành biến định tính và biến định lượng nhằm phục vụ cho các phân tích thống kê và mô hình hóa.

1.2 Nạp các gói thư viện cần thiết

library(readr)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(DT)
library(knitr)
library(kableExtra)
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library(zoo)
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
library(lubridate)
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
library(scales)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:readr':
## 
##     col_factor
library(ggplot2)
library(moments)
library(ggridges)
library(treemapify)
library(ggalluvial)
library(fmsb)
library(corrplot)
## corrplot 0.95 loaded

Nhóm 1: Xử lý và Thao tác Dữ liệu

readr: Dùng để đọc dữ liệu từ các tệp văn bản (như file .csv) vào R một cách nhanh và hiệu quả.

dplyr: Là thư viện “xương sống” cho việc xử lý dữ liệu. Cung cấp các hàm mạnh mẽ (mutate, filter, group_by, summarise) để làm sạch, biến đổi và tóm tắt dữ liệu.

lubridate: Giúp xử lý các dữ liệu dạng ngày tháng và thời gian trở nên cực kỳ đơn giản (ví dụ: lấy ra năm, tháng, ngày, thứ…).

zoo: Thường được dùng cho phân tích chuỗi thời gian, trong bài của bạn nó được dùng cho hàm rollmean để tính trung bình động.

Nhóm 2: Trình bày Báo cáo và Bảng biểu

knitr: Là “công cụ” cốt lõi để biên dịch (knit) file R Markdown của bạn thành các định dạng báo cáo như HTML hoặc PDF.

DT (DataTables): Tạo ra các bảng dữ liệu HTML có tính tương tác cao, cho phép người dùng lọc, sắp xếp, và tìm kiếm trực tiếp trên bảng.

kableExtra: Dùng để “trang điểm”, làm cho các bảng tĩnh được tạo bởi knitr::kable trở nên đẹp và chuyên nghiệp hơn (thêm sọc, màu sắc, cuộn…).

Nhóm 3: Trực quan hóa Dữ liệu (Vẽ biểu đồ)

ggplot2: Là thư viện vẽ biểu đồ mạnh mẽ và phổ biến nhất trong R. Hầu hết các biểu đồ trong bài của bạn đều được vẽ bằng gói này.

scales: Hỗ trợ ggplot2 trong việc định dạng lại các nhãn trên trục hoặc chú thích (ví dụ: thêm ký hiệu đô la $ hay phần trăm %).

corrplot: Chuyên dùng để vẽ ma trận tương quan (correlogram) một cách đẹp mắt và dễ hiểu.

treemapify: Vẽ biểu đồ dạng Treemap, giúp hiển thị dữ liệu có cấu trúc phân cấp dưới dạng các hình chữ nhật lồng nhau.

ggridges: Vẽ biểu đồ mật độ dạng “dãy núi” (Ridgeline plots), rất hữu ích để so sánh sự phân bố của dữ liệu giữa nhiều nhóm khác nhau.

ggalluvial: Vẽ biểu đồ Alluvial và Sankey, dùng để minh họa “dòng chảy” của dữ liệu từ nhóm này sang nhóm khác.

fmsb: Dùng để vẽ các loại biểu đồ ít phổ biến hơn như biểu đồ radar (biểu đồ mạng nhện).

Nhóm 4: Thống kê

moments: Dùng để tính toán các chỉ số thống kê mô tả nâng cao như độ xiên (Skewness) và độ nhọn (Kurtosis) của một phân phối dữ liệu.

1.3 Nạp dữ liệu vào R

BK <- read_csv("D:/Thúy Hiền/bike_sales_100k.csv")
## Rows: 100000 Columns: 11
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Date, Bike_Model, Store_Location, Payment_Method, Customer_Gender
## dbl (6): Sale_ID, Customer_ID, Price, Quantity, Salesperson_ID, Customer_Age
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Gói dplyr được sử dụng để làm sạch, chọn lọc và biến đổi dữ liệu, tạo thuận lợi cho quá trình thống kê mô tả và phân tích định lượng ở các bước sau.

Gói readr được dùng để nhập dữ liệu từ tệp .csv vào R, đảm bảo tốc độ và độ chính xác cao.

Sau khi cài đặt và kích hoạt các gói. Dữ liệu sẽ được đọc vào được lưu dưới têng BK để tiện thao tác trong quá trình xử lý.

1.4 Hiển thị 10 dòng đầu tiên

datatable(
  head(BK, 10),                
  options = list(
    pageLength = 10,        
    autoWidth = TRUE
  ),
  class = "display nowrap",       
  filter = "top",                
  rownames = FALSE
)

datatable(…) là hàm chính của gói DT, dùng để tạo bảng tương tác.Bên trong ngoặc () bạn truyền vào dữ liệu cần hiển thị — ở đây là head(BK, 10). Hàm head() được dùng để trích xuất 10 quan sát đầu tiên từ bộ dữ liệu BK, nhằm minh họa cấu trúc và các biến trong tập dữ liệu.

Các tùy chọn pageLength và autoWidth giúp bảng dữ liệu hiển thị linh hoạt, cho phép tự động căn chỉnh kích thước cột.

class = “display nowrap” thiết lập kiểu hiển thị bảng: “display” là kiểu mặc định của DataTables; “nowrap” giúp bảng không xuống dòng, đảm bảo mỗi dòng dữ liệu hiển thị trên một hàng duy nhất (dễ quan sát hơn).

filter = “top”: Tạo ô lọc dữ liệu ở phía trên đầu bảng.

rownames = FALSE: Ẩn cột số thứ tự mặc định của R.

1.5 Khám phá dữ liệu

1.5.1 Số quan sát và số biến

cat("Số biến trong bộ dữ liệu là:", ncol(BK))
## Số biến trong bộ dữ liệu là: 11
cat("Số quan sát trong bộ dữ liệu là:", nrow(BK))
## Số quan sát trong bộ dữ liệu là: 100000

Dùng cat() để hiển thị một câu thông báo kết hợp cả chữ và số trong báo cáo của mình một cách tự nhiên và sạch sẽ nhất.

1.5.2 Cấu trúc và kiểu dữ liệu của các biến

data_summary_auto <- data.frame(
  Ten_Cot = names(BK),
  Loai_Du_lieu = unname(sapply(BK, class)),
  Giai_thich = c("Mã số bán hàng.", 
                 "Ngày bán hàng.",
                 "Mã khách hàng.", 
                 "Tên hoặc loại xe đạp.", 
                 "Giá bán.",
                 "Số lượng bán.", 
                 "Vị trí của hàng.", 
                 "Mã nhân viên bán hàng.",
                 "Phương thức thanh toán.", 
                 "Tuổi của khách hàng.",
                 "Giới tính của khách hàng.")
)

# In bảng tóm tắt ra
kable(data_summary_auto, 
      caption = "Bảng Tóm Tắt Tên Cột và Kiểu Dữ liệu",
      col.names = c("Tên Biến", "Loại Dữ liệu trong R", "Ý nghĩa")) %>%
  kable_styling(bootstrap_options = "striped", full_width = F)
Bảng Tóm Tắt Tên Cột và Kiểu Dữ liệu
Tên Biến Loại Dữ liệu trong R Ý nghĩa
Sale_ID numeric Mã số bán hàng.
Date character Ngày bán hàng.
Customer_ID numeric Mã khách hàng.
Bike_Model character Tên hoặc loại xe đạp.
Price numeric Giá bán.
Quantity numeric Số lượng bán.
Store_Location character Vị trí của hàng.
Salesperson_ID numeric Mã nhân viên bán hàng.
Payment_Method character Phương thức thanh toán.
Customer_Age numeric Tuổi của khách hàng.
Customer_Gender character Giới tính của khách hàng.
so_bien_dinh_luong <- data_summary_auto %>% 
  filter(Loai_Du_lieu == "numeric") %>% 
  nrow()
cat("Số biến định lượng:", so_bien_dinh_luong, "\n")
## Số biến định lượng: 6

Các biến định lượng thể hiện các thông số số học, đo lường giá trị và khối lượng giao dịch, bao gồm: Sale_ID, Customer_ID, Salesperson_ID, Price, Quantity, và Customer_Age. Biến Date mặc dù được lưu dưới dạng ngày tháng, cũng có thể được chuyển đổi thành dạng định lượng để phân tích theo thời gian. Những biến định lượng này hỗ trợ các phân tích thống kê mô tả, tính toán doanh thu, lợi nhuận và mô hình dự báo.

so_bien_dinh_tinh <- data_summary_auto %>% 
  filter(Loai_Du_lieu == "character") %>% 
  nrow()
cat("Số biến định tính:", so_bien_dinh_tinh, "\n")
## Số biến định tính: 5

Các biến định tính mô tả các đặc điểm phân loại và chất lượng của khách hàng, sản phẩm và giao dịch, bao gồm: Bike_Model, Store_Location, Payment_Method, Customer_Gender, và Date. Những biến này cho phép phân tích phân phối, nhóm khách hàng, đánh giá hiệu suất nhân viên và các đặc trưng sản phẩm.

1.5.3 Kiểm tra chất lượng dữ liệu

cat("Tổng số giá trị bị thiếu (NA):", sum(is.na(BK)))
## Tổng số giá trị bị thiếu (NA): 0
cat("\nTổng số dòng bị trùng lặp hoàn toàn:", sum(duplicated(BK)))
## 
## Tổng số dòng bị trùng lặp hoàn toàn: 0

Giải thích code: - Hàm is.na(): sẽ quét qua từng ô trong toàn bộ dataframe BK. Nó sẽ trả về một dataframe mới có cùng kích thước, nhưng thay vì chứa dữ liệu gốc, nó sẽ chứa các giá trị TRUE (nếu ô gốc bị thiếu, tức là NA) hoặc FALSE (nếu ô gốc có dữ liệu).

  • Hàm duplicated(): sẽ quét qua từng dòng (row) của dataframe BK. Nó so sánh mỗi dòng với tất cả các dòng đã xuất hiện trước nó. Nếu một dòng giống hệt (tất cả các giá trị trong các cột đều giống nhau) với một dòng nào đó ở phía trên, nó sẽ trả về TRUE cho dòng đó. Ngược lại, nó trả về FALSE.

  • Hàm sum(…): cho một tập hợp các giá trị TRUE/FALSE, R sẽ tự động coi TRUE = 1 và FALSE = 0.Kết quả cuối cùng chính là tổng số dữ liệu cần tính.

Nhận xét: Dữ liệu rất sạch, không có giá trị bị thiếu (NA) và không có dòng nào bị trùng lặp hoàn toàn.

1.6 Kiểm tra các giá trị Zero (0) bất thường

price_zero_count <- sum(BK$Price == 0, na.rm = TRUE)
quantity_zero_count <- sum(BK$Quantity == 0, na.rm = TRUE)

cat(sprintf("Số lượng giao dịch có Giá bán (Price) bằng 0 là: %d\n", price_zero_count))
## Số lượng giao dịch có Giá bán (Price) bằng 0 là: 0
cat(sprintf("Số lượng giao dịch có Số lượng (Quantity) bằng 0 là: %d\n", quantity_zero_count))
## Số lượng giao dịch có Số lượng (Quantity) bằng 0 là: 0

1.7 Thống kê tóm tắt sơ bộ

1.7.1 Tóm tắt nhanh các biến định lượng

summary(BK %>% select(where(is.numeric)))
##     Sale_ID        Customer_ID       Price         Quantity     Salesperson_ID 
##  Min.   :     1   Min.   :1000   Min.   : 200   Min.   :1.000   Min.   :100.0  
##  1st Qu.: 25001   1st Qu.:3249   1st Qu.:1400   1st Qu.:2.000   1st Qu.:324.0  
##  Median : 50001   Median :5491   Median :2599   Median :3.000   Median :550.0  
##  Mean   : 50001   Mean   :5495   Mean   :2598   Mean   :2.997   Mean   :549.9  
##  3rd Qu.: 75000   3rd Qu.:7738   3rd Qu.:3796   3rd Qu.:4.000   3rd Qu.:775.0  
##  Max.   :100000   Max.   :9999   Max.   :5000   Max.   :5.000   Max.   :999.0  
##   Customer_Age  
##  Min.   :18.00  
##  1st Qu.:31.00  
##  Median :44.00  
##  Mean   :44.04  
##  3rd Qu.:57.00  
##  Max.   :70.00

Giải thích code:

  • Hàm summary() tính toán các thống kê mô tả (như trung bình, trung vị, min, max,…) cho từng cột số đó và hiển thị kết quả.

  • Hàm select(where(is.numeric)) lọc và chỉ giữ lại những cột có kiểu dữ liệu là số từ BK.

  • Toán tử %>% lấy BK và chuyển nó đến hàm select().

Ý nghĩa:

  • Đối với các biến mang tính chất định danh (ID), các chỉ số như Trung bình (Mean) hay Trung vị (Median) không mang ý nghĩa về mặt phân tích kinh doanh. Tuy nhiên, giá trị Nhỏ nhất (Min) và Lớn nhất (Max) lại rất hữu ích. Ví dụ, Sale_ID chạy từ 1 đến 100,000 cho thấy dữ liệu có vẻ đầy đủ. Tương tự, Customer_ID và Salesperson_ID cho chúng ta thấy khoảng mã định danh hợp lệ của khách hàng và nhân viên.

  • Price (Giá bán): Giá xe dao động từ 200 USD (Min) đến 5,000 USD (Max), cho thấy danh mục sản phẩm của cửa hàng khá đa dạng, từ các dòng xe phổ thông đến cao cấp. Giá trị Trung bình (Mean = 2598) và Trung vị (Median = 2599) rất gần nhau. Đây là một dấu hiệu quan trọng cho thấy sự phân bổ của giá bán là tương đối đối xứng, không bị lệch nhiều bởi các sản phẩm có giá quá cao hoặc quá thấp.

  • Quantity (Số lượng): Khách hàng mua từ 1 (Min) đến 5 (Max) sản phẩm trong một lần giao dịch. Khoảng tứ phân vị (giữa 1st Qu. = 2 và 3rd Qu. = 4) cho thấy 50% số giao dịch của cửa hàng có số lượng bán ra từ 2 đến 4 chiếc. Giá trị trung bình cũng xấp xỉ 3, khẳng định đây là quy mô giao dịch phổ biến nhất.

  • Customer_Age (Tuổi khách hàng): Độ tuổi khách hàng rất rộng, từ 18 (Min) đến 70 (Max), cho thấy sản phẩm phù hợp với nhiều thế hệ. Đây là thông tin giá trị nhất. Khoảng tứ phân vị cho thấy 50% khách hàng cốt lõi của cửa hàng nằm trong độ tuổi từ 31 đến 57 tuổi. Cả giá trị trung bình và trung vị đều là 44, một lần nữa khẳng định nhóm khách hàng chính là những người ở độ tuổi trung niên.

Kết luận sơ bộ: Qua bước tóm tắt nhanh, có thể thấy dữ liệu định lượng hoàn toàn hợp lý, không có giá trị bất thường. Các biến kinh doanh chính có sự phân bổ khá cân đối và đã cung cấp những gợi ý ban đầu rất quan trọng về dải sản phẩm và nhóm khách hàng mục tiêu của chuỗi cửa hàng.

1.7.2 Đếm tần suất sơ bộ một biến định tính

BK %>% 
  count(Store_Location, sort = TRUE) %>%
  kable(caption = "Số lượng giao dịch tại mỗi cửa hàng")
Số lượng giao dịch tại mỗi cửa hàng
Store_Location n
New York 14515
Phoenix 14385
Philadelphia 14330
San Antonio 14300
Chicago 14207
Houston 14149
Los Angeles 14114

Hàm count(): Đây là một hàm tiện lợi từ gói dplyr, chuyên dùng để đếm số lần xuất hiện của mỗi giá trị duy nhất trong một hoặc nhiều cột.

Tham số sort = TRUE: Tùy chọn này tự động sắp xếp kết quả theo thứ tự giảm dần của tần suất (cột n), giúp chúng ta ngay lập tức thấy được danh mục nào là phổ biến nhất.

Kết quả: Cửa hàng tại New York có số lượng giao dịch cao nhất với 14,515 lượt mua bán.Mặc dù có sự chênh lệch, nhưng khoảng cách về số lượng giao dịch giữa cửa hàng đứng đầu (New York, 14,515) và cửa hàng đứng cuối (Los Angeles, 14,114) là không quá lớn. Điều này cho thấy các chi nhánh đang hoạt động với quy mô tương đối đồng đều.

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

Trong mục này, chúng ta sẽ thực hiện các bước quan trọng để làm sạch, chuẩn hóa và làm giàu bộ dữ liệu. Mục tiêu là chuyển đổi dữ liệu thô thành một dạng có cấu trúc, đầy đủ thông tin và sẵn sàng cho các bước phân tích sâu hơn.

2.1. Chuẩn Hóa Kiểu Dữ Liệu

Bước đầu tiên là đảm bảo các biến được lưu trữ dưới đúng định dạng mà R có thể hiểu và thao tác.

Biến Date đang ở dạng chữ (character). Chúng ta cần chuyển nó sang định dạng Date để có thể thực hiện các phân tích theo thời gian.

BK$Date <- as.Date(BK$Date, format = "%d-%m-%Y")
head(BK$Date)
## [1] "2022-07-11" "2024-05-03" "2022-09-01" "2022-09-28" "2021-01-05"
## [6] "2021-09-06"
# Kiểm tra lại kiểu dữ liệu của cột Date
class(BK$Date)
## [1] "Date"

2.2. Xử Lý Các Vấn Đề Dữ Liệu (Minh Họa)

Một bộ dữ liệu thực tế thường chứa các giá trị bị thiếu (NA) hoặc giá trị ngoại lai (outliers). Mặc dù bộ dữ liệu hiện tại của chúng ta khá sạch, chúng ta sẽ minh họa cách xử lý các vấn đề này.

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

Tình huống giả định: Giả sử có 500 quan sát bị thiếu thông tin về tuổi khách hàng . Giải pháp: Một cách tiếp cận phổ biến là thay thế các giá trị thiếu bằng giá trị trung bình của toàn bộ cột.

# Tạo một bản sao để minh họa
BK_dirty <- BK 
set.seed(456)
na_indices <- sample(1:nrow(BK_dirty), 500)
BK_dirty$Customer_Age[na_indices] <- NA

# Tính tuổi trung bình (loại bỏ các giá trị NA khi tính)
mean_age <- mean(BK_dirty$Customer_Age, na.rm = TRUE)

# Điền các giá trị NA bằng tuổi trung bình đã làm tròn
BK_dirty$Customer_Age[is.na(BK_dirty$Customer_Age)] <- round(mean_age)

# Kiểm tra lại số lượng giá trị thiếu
sum(is.na(BK_dirty$Customer_Age))
## [1] 0

Giải thích code:

  • BK_dirty$Customer_Age[na_indices] <- NA: tạo ra 500 giá trị bị thiếu (NA) trong cột Customer_Age để minh họa việc xử lý dữ liệu.

  • mean_age <- mean(BK_dirty$Customer_Age, na.rm = TRUE): tính tuổi trung bình của cột Customer_Age. Tham số na.rm = TRUE là quan trọng nhất, nó yêu cầu hàm mean phải bỏ qua tất cả các ô bị thiếu (NA) khi tính toán.

  • BK_dirty\(Customer_Age[is.na(BK_dirty\)Customer_Age)] <- round(mean_age): đây là bước xử lý chính. Code này tìm tất cả các ô đang là NA trong cột Customer_Age và thay thế chúng bằng giá trị tuổi trung bình đã tính ở bước trên (đã được làm tròn).

  • Ghi chú: Cho các phân tích tiếp theo, chúng ta sẽ tiếp tục sử dụng bộ dữ liệu BK gốc không có giá trị thiếu.

2.2.2. Xử lý giá trị ngoại lai (Outliers)

Tình huống giả định: Giả sử do lỗi nhập liệu, một vài giao dịch có giá trị Price bất thường (ví dụ: bằng 0 hoặc quá cao).

Giải pháp: Thay thế các giá trị ngoại lai này bằng một giá trị hợp lý hơn, ở đây là giá trị trung vị (median) của từng loại xe.

# Tạo bản sao để minh họa
BK_outliers <- BK
BK_outliers$Price[sample(1:nrow(BK_outliers), 5)] <- c(0, 150000, 5, 95000, 1)

# Xử lý ngoại lai
BK_cleaned <- BK_outliers %>%
  group_by(Bike_Model) %>%
  mutate(Median_Price = median(Price)) %>%
  ungroup() %>%
  mutate(Price = ifelse(Price < 100 | Price > 10000, Median_Price, Price)) %>%
  select(-Median_Price)

# Hiển thị tóm tắt giá sau khi làm sạch để so sánh
summary(BK_outliers$Price)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##       0    1400    2599    2601    3796  150000
summary(BK_cleaned$Price)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     200    1400    2598    2598    3796    5000

Giải thích code:

  • sample(1:nrow(BK_outliers), 5): Lệnh này chọn ra 5 vị trí (chỉ số hàng) ngẫu nhiên trong data frame.

  • mutate(): Là hàm dùng để tạo ra một cột mới hoặc chỉnh sửa một cột hiện có.

  • ifelse(điều_kiện, giá_trị_nếu_đúng, giá_trị_nếu_sai): Đây là một hàm điều kiện. Nếu giá là một ngoại lai (thỏa mãn điều kiện), nó sẽ được thay thế bằng giá trị Median_Price (trung vị của loại xe đó) mà chúng ta đã tính ở bước trên và ngược lại.

  • Ghi chú: Chúng ta sẽ tiếp tục sử dụng bộ dữ liệu BK gốc cho các phân tích sau.

2.3. Kỹ thuật tạo đặc trưng (Feature Engineering)

Đây là bước quan trọng nhất để làm giàu thông tin. Chúng ta sẽ tạo một bản sao BK1 từ BK gốc và thực hiện tất cả các thao tác trên một chuỗi duy nhất.

# Bước tiền xử lý: Đếm số lần mua hàng của mỗi khách hàng trước
customer_frequency <- BK %>%
  group_by(Customer_ID) %>%
  summarise(Purchase_Count = n(), .groups = 'drop')

# Bắt đầu chuỗi xử lý để tạo ra dataframe BK1 hoàn chỉnh
BK1 <- BK %>%
  # Nối thông tin tần suất mua hàng vào bảng chính
  left_join(customer_frequency, by = "Customer_ID") %>%
  
  # Bắt đầu tạo và biến đổi các cột mới
  mutate(
    # 1. Tạo biến Doanh thu
    Revenue = Price * Quantity,
    
    # 2. Trích xuất thông tin Năm, Tháng, Thứ từ cột Date
    Year = year(Date),
    Month = month(Date, label = TRUE, abbr = FALSE),
    Weekday = wday(Date, label = TRUE, abbr = FALSE),
    
    # 3. Tạo biến Mùa (Season)
    Season = case_when(
      month(Date) %in% c(3, 4, 5)   ~ "Xuân",
      month(Date) %in% c(6, 7, 8)   ~ "Hạ",
      month(Date) %in% c(9, 10, 11) ~ "Thu",
      TRUE                         ~ "Đông"
    ),
    
    # 4. Phân tổ theo nhóm tuổi
    Age_Group = case_when(
      Customer_Age <= 35 ~ "Young (18-35)",
      Customer_Age <= 55 ~ "Middle-Aged (36-55)",
      TRUE               ~ "Senior (56+)"
    ),
    
    # 5. Phân tổ theo mức giá
    Price_Level = case_when(
      Price <= 2600      ~ "Phổ thông",
      Price < 4000       ~ "Cao cấp",
      TRUE               ~ "Hạng sang"
    ),
    
    # 6. Tạo biến Hạng khách hàng thân thiết (dựa vào Purchase_Count đã join ở trên)
    Loyalty_Status = case_when(
      Purchase_Count >= 10 ~ "Platinum",
      Purchase_Count >= 5  ~ "Gold",
      Purchase_Count >= 2  ~ "Silver",
      TRUE                 ~ "Bronze"
    ),
    
    # 7. Mã hóa Dữ liệu (Encoding) Giới tính
    Gender_Encoded = case_when(
      Customer_Gender == "Male"   ~ 0,
      Customer_Gender == "Female" ~ 1,
      TRUE                        ~ 2 # Dành cho "Other"
    )
  )

2.5. Tổng kết và xem lại dữ liệu sau xử lý

Sau khi thực hiện tất cả các bước trên, bộ dữ liệu của chúng ta đã được mở rộng và làm giàu đáng kể.

# Hiển thị 10 dòng đầu của bộ dữ liệu đã được xử lý hoàn chỉnh
datatable(head(BK1, 10), 
          rownames = FALSE,
          options = list(scrollX = TRUE), # Thêm thanh cuộn ngang
          caption = "Dữ liệu sau khi được xử lý và làm giàu")
  • Kết quả: Bộ dữ liệu BK1 giờ đây đã có thêm nhiều cột mới (Revenue, Year, Month, Weekday, Season, Age_Group, Price_Level, Loyalty_Status, Gender_Encoded), sẵn sàng cho các phân tích sâu sắc ở các chương tiếp theo.

3. Thống kê và Phân tích Mô tả

Sau khi đã xử lý và làm giàu dữ liệu, chương này sẽ tập trung vào việc thực hiện các phép tính thống kê cơ bản để trả lời các câu hỏi kinh doanh cụ thể, từ đó rút ra những insight ban đầu về hoạt động của doanh nghiệp.

3.1 Phân tích thống kê mô tả chi tiết

3.1.1 Thống kê Mô tả biến Định lượng

# Lấy các cột số có ý nghĩa phân tích (loại bỏ các cột ID không cần thiết)
numeric_vars <- BK %>% 
  select(where(is.numeric)) %>%
  select(-ends_with("_ID"))

# Tính toán bộ chỉ số thống kê đầy đủ
descriptive_table_adv <- numeric_vars %>%
  summarise(across(
    everything(),
    list(
      Mean     = ~mean(., na.rm = TRUE),
      Median   = ~median(., na.rm = TRUE),
      SD       = ~sd(., na.rm = TRUE),
      Var      = ~var(., na.rm = TRUE),
      Min      = ~min(., na.rm = TRUE),
      Max      = ~max(., na.rm = TRUE),
      Skewness = ~skewness(., na.rm = TRUE),
      Kurtosis = ~kurtosis(., na.rm = TRUE)
    ),
    .names = "{.col}__{.fn}" # Dùng __ để dễ tách
  )) %>%
  tidyr::pivot_longer(
    cols = everything(),
    names_to = c("Variable", "Statistic"),
    names_sep = "__"
  ) %>%
  tidyr::pivot_wider(
    names_from = Statistic,
    values_from = value # (2) Sửa lỗi chữ hoa/thường ở đây
  ) %>%
  mutate(across(where(is.numeric), ~round(., 2))) # Làm tròn tất cả các cột số

# Hiển thị bảng
kable(descriptive_table_adv, caption = "Thống kê mô tả chi tiết các biến định lượng") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  scroll_box(width = "100%")
Thống kê mô tả chi tiết các biến định lượng
Variable Mean Median SD Var Min Max Skewness Kurtosis
Price 2598.18 2598.57 1384.94 1918067.48 200.01 4999.81 0 1.8
Quantity 3.00 3.00 1.41 2.00 1.00 5.00 0 1.7
Customer_Age 44.04 44.00 15.31 234.51 18.00 70.00 0 1.8

Giải thích code:

  • Dấu ~ cho R biết đây là một công thức, và dấu . là một placeholder đại diện cho “cột hiện tại đang được xử lý.

  • pivot_longer(…): Biến bảng “rộng” thành “dài”.

  • pivot_wider(…): Biến bảng “dài” trở lại thành “rộng”.

  • values_from = value: Nó điền giá trị cho các cột mới đó từ cột value.

Ý nghĩa:

1. Biến Price (Giá cả)

Mean (2598.18) & Median (2598.57): Giá trị trung bình và trung vị gần như bằng nhau. Điều này là một dấu hiệu rất tốt, cho thấy sự phân phối của giá cả rất đối xứng. Không có nhiều sản phẩm giá quá cao hoặc quá thấp kéo lệch giá trị trung bình.

SD (1384.94) & Var (1918067.48): Độ lệch chuẩn (SD) khá lớn. Điều này cho thấy giá cả của các sản phẩm có sự biến động và chênh lệch cao. Dữ liệu không tập trung quanh một mức giá duy nhất mà trải rộng ra.

Min (200.01) & Max (4999.81): Khoảng giá của sản phẩm là từ khoảng 200 đến 5000. Điều này xác nhận lại độ phân tán cao của dữ liệu giá.

Skewness (0): Độ xiên bằng 0. Đây là sự khẳng định về mặt toán học rằng phân phối giá cả là hoàn toàn đối xứng. Đồ thị phân phối có hình dạng giống quả chuông cân đối.

Kurtosis (1.8): Độ nhọn là 1.8. Giá trị này nhỏ hơn 3 (độ nhọn của phân phối chuẩn). Điều này có nghĩa là phân phối ít nhọn hơn (bẹt hơn) so với phân phối chuẩn. Nó có ít các giá trị ngoại lai (outliers) ở hai phía đuôi.

2. Biến Quantity (Số lượng)

Mean (3.00) & Median (3.00): Trung bình và trung vị bằng nhau tuyệt đối. Số lượng sản phẩm được mua trung bình là 3.

SD (1.41) & Var (2.00): Độ lệch chuẩn nhỏ, cho thấy số lượng mua ít biến động. Hầu hết các giao dịch đều có số lượng xoay quanh mức 3.

Min (1.00) & Max (5.00): Số lượng mua ít nhất là 1 và nhiều nhất là 5. Đây là một khoảng giá trị rất hẹp, củng cố cho việc độ biến động thấp.

Skewness (0): Tương tự như giá, phân phối của số lượng cũng hoàn toàn đối xứng.

Kurtosis (1.7): Phân phối cũng bẹt hơn phân phối chuẩn, cho thấy các giá trị tập trung đều hơn và ít giá trị ngoại lai.

3. Biến Customer_Age (Tuổi khách hàng)

Mean (44.04) & Median (44.00): Độ tuổi trung bình và trung vị của khách hàng là khoảng 44 tuổi. Hai giá trị này rất gần nhau, cho thấy phân phối tuổi cũng rất cân đối . SD (15.31): Độ lệch chuẩn khá lớn, cho thấy độ tuổi của các khách hàng rất đa dạng, không tập trung ở một nhóm tuổi cụ thể nào.

Min (18.00) & Max (70.00): Nhóm khách hàng trải dài từ 18 đến 70 tuổi.

Skewness (0): Phân phối tuổi hoàn toàn đối xứng. Lượng khách hàng trẻ tuổi và lớn tuổi được phân bổ đều quanh độ tuổi trung bình.

Kurtosis (1.8): Phân phối tuổi cũng bẹt hơn phân phối chuẩn.

Kết luận chung: Price là biến có độ biến động lớn nhất, tiếp theo là Customer_Age, và cuối cùng Quantity là biến ổn định và ít thay đổi nhất.Tất cả các biến đều có phân phối “bẹt hơn” (Platykurtic) so với phân phối chuẩn, nghĩa là dữ liệu ít tập trung vào đỉnh và có ít giá trị cực đoan ở hai bên. Cả ba biến đều có độ xiên (Skewness) bằng 0 nó cho thấy dữ liệu của bạn cực kỳ cân bằng và đối xứng.

3.1.2 Thống kê Mô tả Biến Định tính

# Lấy các cột là character hoặc factor
qual_data <- BK %>% select(where(is.character) | where(is.factor))

# Dùng vòng lặp để tạo bảng tần suất cho mỗi biến
for (col_name in names(qual_data)) {
  
  freq_table <- qual_data %>%
    count(!!sym(col_name), sort = TRUE) %>%
    mutate(Ty_le = n / sum(n) * 100) %>%
    rename(Gia_tri = 1, Tan_so = n, "Ty_le (%)" = Ty_le) %>%
    mutate(`Ty_le (%)` = round(`Ty_le (%)`, 2))

  print(
    kable(freq_table, caption = paste("Bảng tần suất cho biến:", col_name)) %>%
    kable_styling(bootstrap_options = "striped", full_width = F)
  )
}
## <table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Bike_Model</caption>
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Gia_tri </th>
##    <th style="text-align:right;"> Tan_so </th>
##    <th style="text-align:right;"> Ty_le (%) </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> BMX </td>
##    <td style="text-align:right;"> 14377 </td>
##    <td style="text-align:right;"> 14.38 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Road Bike </td>
##    <td style="text-align:right;"> 14363 </td>
##    <td style="text-align:right;"> 14.36 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Cruiser </td>
##    <td style="text-align:right;"> 14332 </td>
##    <td style="text-align:right;"> 14.33 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Folding Bike </td>
##    <td style="text-align:right;"> 14329 </td>
##    <td style="text-align:right;"> 14.33 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Hybrid Bike </td>
##    <td style="text-align:right;"> 14319 </td>
##    <td style="text-align:right;"> 14.32 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Electric Bike </td>
##    <td style="text-align:right;"> 14169 </td>
##    <td style="text-align:right;"> 14.17 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Mountain Bike </td>
##    <td style="text-align:right;"> 14111 </td>
##    <td style="text-align:right;"> 14.11 </td>
##   </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Store_Location</caption>
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Gia_tri </th>
##    <th style="text-align:right;"> Tan_so </th>
##    <th style="text-align:right;"> Ty_le (%) </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> New York </td>
##    <td style="text-align:right;"> 14515 </td>
##    <td style="text-align:right;"> 14.52 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Phoenix </td>
##    <td style="text-align:right;"> 14385 </td>
##    <td style="text-align:right;"> 14.38 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Philadelphia </td>
##    <td style="text-align:right;"> 14330 </td>
##    <td style="text-align:right;"> 14.33 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> San Antonio </td>
##    <td style="text-align:right;"> 14300 </td>
##    <td style="text-align:right;"> 14.30 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Chicago </td>
##    <td style="text-align:right;"> 14207 </td>
##    <td style="text-align:right;"> 14.21 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Houston </td>
##    <td style="text-align:right;"> 14149 </td>
##    <td style="text-align:right;"> 14.15 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Los Angeles </td>
##    <td style="text-align:right;"> 14114 </td>
##    <td style="text-align:right;"> 14.11 </td>
##   </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Payment_Method</caption>
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Gia_tri </th>
##    <th style="text-align:right;"> Tan_so </th>
##    <th style="text-align:right;"> Ty_le (%) </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> Apple Pay </td>
##    <td style="text-align:right;"> 16751 </td>
##    <td style="text-align:right;"> 16.75 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Debit Card </td>
##    <td style="text-align:right;"> 16738 </td>
##    <td style="text-align:right;"> 16.74 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Cash </td>
##    <td style="text-align:right;"> 16692 </td>
##    <td style="text-align:right;"> 16.69 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Credit Card </td>
##    <td style="text-align:right;"> 16653 </td>
##    <td style="text-align:right;"> 16.65 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Google Pay </td>
##    <td style="text-align:right;"> 16613 </td>
##    <td style="text-align:right;"> 16.61 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> PayPal </td>
##    <td style="text-align:right;"> 16553 </td>
##    <td style="text-align:right;"> 16.55 </td>
##   </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Customer_Gender</caption>
##  <thead>
##   <tr>
##    <th style="text-align:left;"> Gia_tri </th>
##    <th style="text-align:right;"> Tan_so </th>
##    <th style="text-align:right;"> Ty_le (%) </th>
##   </tr>
##  </thead>
## <tbody>
##   <tr>
##    <td style="text-align:left;"> Female </td>
##    <td style="text-align:right;"> 50227 </td>
##    <td style="text-align:right;"> 50.23 </td>
##   </tr>
##   <tr>
##    <td style="text-align:left;"> Male </td>
##    <td style="text-align:right;"> 49773 </td>
##    <td style="text-align:right;"> 49.77 </td>
##   </tr>
## </tbody>
## </table>

Giải thích code:

  • names(qual_data): Lệnh này lấy ra một danh sách (vector) chứa tên của tất cả các cột trong qual_data.

  • for (col_name in …): Lệnh này tạo ra một vòng lặp.

Ý nghĩa:

1. Bảng tần suất cho biến Bike_Model (Loại xe đạp) Nội dung: Bảng này liệt kê 7 loại xe đạp khác nhau (BMX, Road Bike, v.v.) và số lượng của mỗi loại.

Ý nghĩa quan trọng: Tần suất và tỷ lệ phần trăm của tất cả các loại xe đạp là gần như bằng nhau (tất cả đều xoay quanh 14.1% - 14.4%). Điều này cho thấy dữ liệu được phân bổ rất đều cho mỗi loại xe. Không có loại xe nào chiếm ưu thế vượt trội.

2. Bảng tần suất cho biến Store_Location (Vị trí cửa hàng)

Nội dung: Liệt kê số lượng bản ghi (có thể là giao dịch) tại 7 thành phố khác nhau.

Ý nghĩa quan trọng: Tương tự như loại xe, số lượng giao dịch tại mỗi thành phố cũng cực kỳ cân bằng (đều trong khoảng 14.1% - 14.5%). Mặc dù New York có nhiều hơn một chút, sự khác biệt là không đáng kể. Điều này cho thấy hoạt động kinh doanh hoặc việc thu thập dữ liệu diễn ra đồng đều trên tất cả các địa điểm.

3. Bảng tần suất cho biến Payment_Method (Phương thức thanh toán)

Nội dung: Thống kê 6 phương thức thanh toán khác nhau mà khách hàng đã sử dụng.

Ý nghĩa quan trọng: Một lần nữa, chúng ta lại thấy một sự cân bằng gần như hoàn hảo. Tất cả các phương thức thanh toán đều được sử dụng với tần suất rất giống nhau (khoảng 16.6% - 16.7%). Điều này có nghĩa là không có phương thức thanh toán nào được ưa chuộng hơn hẳn các phương thức còn lại trong tập dữ liệu này.

4. Bảng tần suất cho biến Customer_Gender (Giới tính khách hàng)

Nội dung: Thống kê số lượng khách hàng nam và nữ.

Ý nghĩa quan trọng: Tỷ lệ giữa khách hàng nữ (Female) và nam (Male) gần như là 50/50 (50.23% so với 49.77%). Đây là một sự cân bằng giới tính hoàn hảo.

3.2 Phân tích Tổng quan về Giao dịch

3.2.1 Tổng doanh thu:

total_revenue <- sum(BK1$Revenue)
cat("Tổng doanh thu:", dollar(total_revenue, prefix = "", suffix = " USD"))
## Tổng doanh thu: 778,434,235 USD

3.2.2 Doanh thu trung bình trên mỗi giao dịch:

avg_revenue_per_sale <- mean(BK1$Revenue)
cat("Doanh thu trung bình mỗi giao dịch:", dollar(avg_revenue_per_sale, prefix = "", suffix = " USD"))
## Doanh thu trung bình mỗi giao dịch: 7,784.34 USD

3.2.3 Tổng số giao dịch đã thực hiện:

total_transactions <- nrow(BK1)
cat("Tổng số giao dịch:", comma(total_transactions))
## Tổng số giao dịch: 100,000

3.2.4 Số lượng sản phẩm trung bình mỗi giao dịch:

avg_quantity_per_sale <- mean(BK1$Quantity)
cat("Số lượng xe trung bình mỗi giao dịch:", round(avg_quantity_per_sale, 2))
## Số lượng xe trung bình mỗi giao dịch: 3
  • Nhận xét: Các chỉ số tổng quan cho thấy quy mô kinh doanh, giá trị trung bình mỗi đơn hàng và thói quen mua sắm (số lượng) của khách hàng.

3.3 Phân tích theo Sản phẩm

3.3.1 Tổng số lượng bán theo từng mẫu xe:

quantity_summary <- BK1 %>%
  group_by(Bike_Model) %>%
  summarise(Total_Quantity = sum(Quantity)) %>%
  arrange(desc(Total_Quantity))
kable(quantity_summary, caption = "Số lượng bán theo mẫu xe")
Số lượng bán theo mẫu xe
Bike_Model Total_Quantity
Cruiser 43120
Hybrid Bike 43089
BMX 43080
Road Bike 43022
Folding Bike 42872
Mountain Bike 42279
Electric Bike 42249

3.3.2 Tổng doanh thu theo từng mẫu xe:

revenue_by_model <- BK1 %>%
  group_by(Bike_Model) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_model, caption = "Doanh thu theo mẫu xe")
Doanh thu theo mẫu xe
Bike_Model Total_Revenue
Hybrid Bike 112505511
BMX 112146114
Cruiser 111849092
Road Bike 111579574
Folding Bike 110966818
Electric Bike 109750159
Mountain Bike 109636967

3.3.3 Giá bán trung bình của từng mẫu xe:

avg_price_by_model <- BK1 %>%
  group_by(Bike_Model) %>%
  summarise(Average_Price = mean(Price)) %>%
  arrange(desc(Average_Price))
kable(avg_price_by_model, caption = "Giá bán trung bình theo mẫu xe")
Giá bán trung bình theo mẫu xe
Bike_Model Average_Price
BMX 2608.601
Hybrid Bike 2601.903
Road Bike 2600.954
Electric Bike 2598.047
Cruiser 2596.131
Mountain Bike 2591.299
Folding Bike 2590.196

3.3.4 Phân bổ sản phẩm theo các mức giá đã phân tổ:

price_level_dist <- BK1 %>%
  count(Price_Level, sort = TRUE) %>%
  mutate(Percentage = n / sum(n) * 100)
kable(price_level_dist, caption = "Tỷ lệ sản phẩm theo mức giá")
Tỷ lệ sản phẩm theo mức giá
Price_Level n Percentage
Phổ thông 50030 50.030
Cao cấp 29341 29.341
Hạng sang 20629 20.629
  • Nhận xét: Phân tích theo sản phẩm giúp xác định đâu là dòng xe “chủ lực” (best-seller), đâu là dòng xe cao cấp (premium), và phân khúc giá nào đang chiếm ưu thế trên thị trường.

3.4. Phân tích theo Địa điểm và Nhân viên

3.4.1 Hiệu suất kinh doanh theo từng cửa hàng (Doanh thu):

revenue_by_store <- BK1 %>%
  group_by(Store_Location) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_store, caption = "Doanh thu theo cửa hàng")
Doanh thu theo cửa hàng
Store_Location Total_Revenue
New York 113592474
Phoenix 111860420
Philadelphia 111765829
Houston 110427042
Chicago 110388139
Los Angeles 110340851
San Antonio 110059479

3.4.2 Số lượng giao dịch theo từng cửa hàng:

transactions_by_store <- BK1 %>%
  count(Store_Location, name = "Transaction_Count", sort = TRUE)
kable(transactions_by_store, caption = "Số lượng giao dịch theo cửa hàng")
Số lượng giao dịch theo cửa hàng
Store_Location Transaction_Count
New York 14515
Phoenix 14385
Philadelphia 14330
San Antonio 14300
Chicago 14207
Houston 14149
Los Angeles 14114

3.4.3 Giá trị giao dịch trung bình tại mỗi cửa hàng:

avg_revenue_by_store <- BK1 %>%
  group_by(Store_Location) %>%
  summarise(Average_Revenue_Per_Sale = mean(Revenue)) %>%
  arrange(desc(Average_Revenue_Per_Sale))
kable(avg_revenue_by_store, caption = "Giá trị giao dịch trung bình tại mỗi cửa hàng")
Giá trị giao dịch trung bình tại mỗi cửa hàng
Store_Location Average_Revenue_Per_Sale
New York 7825.868
Los Angeles 7817.830
Houston 7804.583
Philadelphia 7799.430
Phoenix 7776.185
Chicago 7769.982
San Antonio 7696.467

3.4.4 Top 10 nhân viên bán hàng xuất sắc nhất:

top_salesperson <- BK1 %>%
  group_by(Salesperson_ID) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue)) %>%
  top_n(10, Total_Revenue)
kable(top_salesperson, caption = "Top 10 nhân viên có doanh thu cao nhất")
Top 10 nhân viên có doanh thu cao nhất
Salesperson_ID Total_Revenue
794 1196096
500 1137238
605 1125912
655 1123964
914 1123278
758 1105828
419 1105377
894 1101144
540 1100963
485 1100888
  • Nhận xét: Các thống kê này giúp đánh giá hiệu quả hoạt động của từng chi nhánh và hiệu suất làm việc của cá nhân, từ đó có chính sách khen thưởng hoặc cải thiện phù hợp.

3.5 Phân tích theo Khách hàng

3.5.1 Tổng số khách hàng duy nhất:

total_unique_customers <- n_distinct(BK1$Customer_ID)
cat("Tổng số khách hàng duy nhất:", comma(total_unique_customers))
## Tổng số khách hàng duy nhất: 9,000

3.5.2 Đặc điểm nhân khẩu học của khách hàng:

mean_customer_age <- mean(BK1$Customer_Age)
cat("Độ tuổi trung bình của khách hàng:", round(mean_customer_age, 1), "tuổi\n")
## Độ tuổi trung bình của khách hàng: 44 tuổi
gender_distribution <- BK1 %>%
  count(Customer_Gender, sort = TRUE) %>%
  mutate(Percentage = n / sum(n) * 100)
kable(gender_distribution, caption = "Phân bổ khách hàng theo giới tính")
Phân bổ khách hàng theo giới tính
Customer_Gender n Percentage
Female 50227 50.227
Male 49773 49.773

3.5.3 Phân tích doanh thu theo các nhóm tuổi đã phân tổ:

revenue_by_age_group <- BK1 %>%
  group_by(Age_Group) %>%
  summarise(
    Total_Revenue = sum(Revenue),
    Average_Purchase_Value = mean(Revenue)
    ) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_age_group, caption = "Doanh thu theo nhóm tuổi")
Doanh thu theo nhóm tuổi
Age_Group Total_Revenue Average_Purchase_Value
Middle-Aged (36-55) 295268207 7842.657
Young (18-35) 262850657 7755.537
Senior (56+) 220315371 7741.501
  • Nhận xét: Giúp phác họa chân dung khách hàng mục tiêu và xác định nhóm tuổi nào đang là nguồn doanh thu chính.

3.6 Phân tích Lòng trung thành của Khách hàng

3.6.1 Phân bổ khách hàng theo hạng thân thiết:

loyalty_distribution <- BK1 %>%
  distinct(Customer_ID, .keep_all = TRUE) %>%
  count(Loyalty_Status) %>%
  mutate(Percentage = n / sum(n) * 100)
kable(loyalty_distribution, caption = "Phân bổ khách hàng theo hạng thân thiết")
Phân bổ khách hàng theo hạng thân thiết
Loyalty_Status n Percentage
Bronze 3 0.0333333
Gold 2852 31.6888889
Platinum 6006 66.7333333
Silver 139 1.5444444

3.6.2 Tỷ lệ khách hàng quay lại mua hàng:

returning_customers <- customer_frequency %>% filter(Purchase_Count > 1) %>% nrow()
cat("Tỷ lệ khách hàng quay lại:", percent(returning_customers / total_unique_customers, accuracy = 0.1))
## Tỷ lệ khách hàng quay lại: 100.0%

3.6.3 Tổng doanh thu đến từ mỗi hạng khách hàng:

revenue_by_loyalty <- BK1 %>%
  group_by(Loyalty_Status) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_loyalty, caption = "Tổng doanh thu theo hạng khách hàng")
Tổng doanh thu theo hạng khách hàng
Loyalty_Status Total_Revenue
Platinum 602997177.2
Gold 171397064.9
Silver 4009260.4
Bronze 30732.4

3.6.4 Giá trị đơn hàng trung bình của mỗi hạng khách hàng:

avg_revenue_by_loyalty <- BK1 %>%
  group_by(Loyalty_Status) %>%
  summarise(Average_Revenue_Per_Sale = mean(Revenue)) %>%
  arrange(desc(Average_Revenue_Per_Sale))
kable(avg_revenue_by_loyalty, caption = "Giá trị đơn hàng trung bình theo hạng khách hàng")
Giá trị đơn hàng trung bình theo hạng khách hàng
Loyalty_Status Average_Revenue_Per_Sale
Bronze 10244.133
Silver 7830.587
Platinum 7784.727
Gold 7781.579
  • Nhận xét: Đánh giá sức khỏe của chương trình khách hàng thân thiết. Liệu khách hàng hạng cao có thực sự chi tiêu nhiều hơn không? Phần lớn doanh thu đến từ nhóm khách hàng nào?

3.7 Phân tích theo Thời gian

3.7.1 Doanh thu theo từng năm:

revenue_by_year <- BK1 %>%
  group_by(Year) %>%
  summarise(Total_Revenue = sum(Revenue))
kable(revenue_by_year, caption = "Doanh thu theo năm")
Doanh thu theo năm
Year Total_Revenue
2020 164224491
2021 163535245
2022 165560749
2023 164497574
2024 120616177

3.7.2 Doanh thu theo từng mùa:

revenue_by_season <- BK1 %>%
  group_by(Season) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_season, caption = "Doanh thu theo mùa")
Doanh thu theo mùa
Season Total_Revenue
Hạ 207337508
Xuân 207312828
Đông 188748226
Thu 175035674

3.7.3 Doanh thu theo từng ngày trong tuần:

revenue_by_weekday <- BK1 %>%
  group_by(Weekday) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_weekday, caption = "Doanh thu theo ngày trong tuần")
Doanh thu theo ngày trong tuần
Weekday Total_Revenue
Thursday 113678630
Friday 111803200
Wednesday 111762837
Saturday 111409344
Monday 110441224
Sunday 110304080
Tuesday 109034920
  • Nhận xét: Giúp phát hiện các xu hướng mùa vụ, xác định các thời điểm kinh doanh cao điểm để lên kế hoạch marketing, nhân sự và tồn kho hiệu quả.

3.8 Phân tích Hành vi và Tương quan

3.8.1 Mức độ ưa chuộng các phương thức thanh toán:

payment_method_usage <- BK1 %>%
  count(Payment_Method, sort = TRUE) %>%
  mutate(Percentage = n / sum(n) * 100)
kable(payment_method_usage, caption = "Mức độ phổ biến của các phương thức thanh toán")
Mức độ phổ biến của các phương thức thanh toán
Payment_Method n Percentage
Apple Pay 16751 16.751
Debit Card 16738 16.738
Cash 16692 16.692
Credit Card 16653 16.653
Google Pay 16613 16.613
PayPal 16553 16.553

3.8.2 Mẫu xe được ưa chuộng nhất theo giới tính:

model_preference_by_gender <- BK1 %>%
  group_by(Customer_Gender, Bike_Model) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
  group_by(Customer_Gender) %>%
  top_n(1, Total_Quantity)
kable(model_preference_by_gender, caption = "Mẫu xe được ưa chuộng nhất theo giới tính")
Mẫu xe được ưa chuộng nhất theo giới tính
Customer_Gender Bike_Model Total_Quantity
Female BMX 21870
Male Road Bike 21490

3.8.3 Tương quan giữa Tuổi và Giá xe:

correlation_age_price <- cor(BK1$Customer_Age, BK1$Price)
print(paste("Hệ số tương quan giữa Tuổi khách hàng và Giá xe:", round(correlation_age_price, 4)))
## [1] "Hệ số tương quan giữa Tuổi khách hàng và Giá xe: 1e-04"

Giải thích code: Hàm cor() trong R để tính toán hệ số tương quan Pearson giữa hai biến.

Ý nghĩa: Kết quả 0.0001 của bạn là một con số cực kỳ gần với 0. Không có bất kỳ mối quan hệ tuyến tính nào giữa tuổi của khách hàng và giá của chiếc xe mà họ mua.

  • Nhận xét: Các phân tích này đi sâu vào thói quen của khách hàng và tìm kiếm các mối liên hệ ẩn giữa các biến trong bộ dữ liệu.

4. Trực Quan Hóa Dữ Liệu

Trong chương này, chúng ta sẽ sử dụng thư viện ggplot2 và các phần mở rộng của nó để trực quan hóa các insight đã tìm thấy, giúp truyền tải thông tin một cách sinh động và dễ hiểu.

4.1 Phân tích Doanh thu và Sản phẩm

4.1.1 Biểu đồ cột: Doanh thu theo từng mẫu xe

  • Mục đích: So sánh tổng doanh thu của các mẫu xe.
# Dữ liệu đã được tính ở chương 3
revenue_by_model <- BK1 %>%
  group_by(Bike_Model) %>%
  summarise(Total_Revenue = sum(Revenue))

ggplot(revenue_by_model, aes(x = reorder(Bike_Model, -Total_Revenue), y = Total_Revenue, fill = Bike_Model)) +
  geom_bar(stat = "identity", show.legend = FALSE) +
  geom_text(aes(label = scales::dollar(Total_Revenue, accuracy = 1)), vjust = -0.5, size = 3.5) +
  labs(title = "Tổng Doanh Thu Theo Từng Mẫu Xe",
       subtitle = "Mẫu 'Hybrid Bike' và 'BMX' chiếm ưu thế",
       x = "Mẫu Xe",
       y = "Tổng Doanh Thu") +
  scale_y_continuous(labels = scales::dollar) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích code:

  • ggplot(…): Bắt đầu quá trình vẽ biểu đồ với dữ liệu vừa tính toán.

  • aes(x = reorder(Bike_Model, -Total_Revenue), y = Total_Revenue, …): Trục Y (y) là Total_Revenue (Tổng doanh thu). Trục X (x) là Bike_Model (Mẫu xe), được sắp xếp lại (reorder) theo doanh thu giảm dần (dấu - có nghĩa là giảm dần). Đây là lý do các cột trên biểu đồ đi từ cao xuống thấp.

  • geom_bar(stat = “identity”): Vẽ biểu đồ cột, với chiều cao của mỗi cột được lấy trực tiếp từ giá trị Total_Revenue.

  • geom_text(…): Thêm nhãn văn bản (số doanh thu chính xác) lên trên mỗi cột để dễ đọc.

  • labs(…) và theme(…): Đặt tiêu đề, nhãn cho các trục và tùy chỉnh giao diện của biểu đồ cho đẹp mắt.

  • Phân tích: Biểu đồ cho thấy rõ ràng sự thống trị của “Electric Bolt” và “Mountain Pro”. Khoảng cách doanh thu giữa các mẫu xe rất lớn, giúp nhà quản lý xác định đâu là sản phẩm chủ lực.

4.1.2 Biểu đồ cột: Doanh thu theo từng mẫu xe theo địa điểm cửa hàng

revenue_by_model_store <- BK1 %>%
  group_by(Bike_Model, Store_Location) %>%
  summarise(Total_Revenue = sum(Revenue), .groups = "drop")

ggplot(revenue_by_model_store, 
       aes(x = reorder(Bike_Model, -Total_Revenue), 
           y = Total_Revenue, 
           fill = Store_Location)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "So sánh Doanh Thu của Từng Mẫu Xe Theo Địa Điểm Cửa Hàng",
       subtitle = "Giúp xác định mẫu xe nào là sản phẩm chủ lực tại từng địa điểm",
       x = "Mẫu Xe",
       y = "Tổng Doanh Thu",
       fill = "Địa điểm cửa hàng") +
  scale_y_continuous(labels = scales::dollar) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích code:

  • ggplot(revenue_by_model_store, aes(…)): Khởi tạo biểu đồ bằng dữ liệu revenue_by_model_store vừa tạo. x = reorder(Bike_Model, -Total_Revenue): Thiết lập trục hoành (trục X) là các mẫu xe. reorder dùng để sắp xếp các mẫu xe theo thứ tự giảm dần của tổng doanh thu. y = Total_Revenue: Thiết lập trục tung (trục Y) là tổng doanh thu.

  • fill = Store_Location: Quy định rằng màu sắc của các cột sẽ được dùng để phân biệt các địa điểm cửa hàng.

  • geom_bar(stat = “identity”, position = “dodge”): Lệnh này vẽ biểu đồ cột (geom_bar).

  • stat = “identity”: Sử dụng trực tiếp giá trị Total_Revenue làm chiều cao của cột.

  • position = “dodge”: Sắp xếp các cột của các cửa hàng khác nhau nằm cạnh nhau cho mỗi mẫu xe, giúp dễ dàng so sánh.

  • labs(…): Đặt tên cho tiêu đề, phụ đề, các trục và chú thích của biểu đồ.

  • scale_y_continuous(labels = scales::dollar): Định dạng các giá trị trên trục Y thành đơn vị tiền tệ đô la ($).

  • theme(…): Tùy chỉnh giao diện của biểu đồ, ở đây là xoay nhãn của trục X một góc 45 độ để tránh bị chồng chéo.

Ý nghĩa: - So sánh hiệu suất sản phẩm: Có thể dễ dàng thấy mẫu xe nào đang tạo ra doanh thu cao nhất hoặc thấp nhất tại một địa điểm cụ thể. Ví dụ, tại New York (cột màu xanh mòng két), “Road Bike” và “Hybrid Bike” có vẻ là những mẫu xe có doanh thu cao nhất.

  • So sánh hiệu suất cửa hàng: Đối với một mẫu xe nhất định, ta có thể so sánh xem cửa hàng ở thành phố nào bán chạy nhất. Ví dụ, với “Mountain Bike”, cửa hàng ở Los Angeles (cột màu xanh lá) có doanh thu cao hơn hẳn so với các cửa hàng khác.

  • Xác định sản phẩm chủ lực: Đúng như phụ đề của biểu đồ, mục đích chính là “Giúp xác định mẫu xe nào là sản phẩm chủ lực tại từng địa điểm”. Dựa vào chiều cao các cột, nhà quản lý có thể biết được ở Chicago nên tập trung vào sản phẩm nào, ở Houston nên tập trung vào sản phẩm nào, từ đó đưa ra các chiến lược kinh doanh và marketing phù hợp cho từng khu vực.

4.1.3 Biểu đồ tròn: Tỷ lệ doanh thu theo địa điểm cửa hàng

  • Mục đích: Hiển thị tỷ trọng đóng góp doanh thu của mỗi cửa hàng.
revenue_by_store <- BK1 %>%
  group_by(Store_Location) %>%
  summarise(Total_Revenue = sum(Revenue))

ggplot(revenue_by_store, aes(x = "", y = Total_Revenue, fill = Store_Location)) +
  geom_bar(stat = "identity", width = 1) +
  coord_polar("y", start = 0) +
  geom_text(aes(label = scales::percent(Total_Revenue / sum(Total_Revenue), accuracy = 0.1)),
            position = position_stack(vjust = 0.5), color = "white", size = 5) +
  labs(title = "Tỷ Lệ Phân Bổ Doanh Thu Theo Địa Điểm Cửa Hàng",
       fill = "Địa Điểm Cửa Hàng", x = NULL, y = NULL) +
  theme_void() +
  theme(legend.title = element_text(face = "bold"))

Giải thích code:

  • ggplot(revenue_by_store, aes(x = ““, y = Total_Revenue, fill = Store_Location)): Khởi tạo biểu đồ. Thay vì vẽ các cột riêng biệt, x =”” làm cho tất cả các giá trị được xếp chồng lên nhau tại một vị trí duy nhất, đây là bước đầu tiên để tạo biểu đồ tròn từ biểu đồ cột.

  • geom_bar(stat = “identity”, width = 1): Vẽ một biểu đồ cột chồng duy nhất.

  • coord_polar(“y”, start = 0): Đây là lệnh quan trọng nhất. Nó biến đổi hệ tọa độ của biểu đồ cột chồng thành hệ tọa độ cực, uốn cong thanh cột đó thành một hình tròn, tạo ra biểu đồ tròn.

Ý nghĩa: Sự chênh lệch giữa cửa hàng hoạt động tốt nhất (New York) và thấp nhất (San Antonio) chỉ là 0.5%. Điều này cho thấy hiệu suất kinh doanh của các chi nhánh đang ở mức tương đối cân bằng.

4.1.4 Biểu đồ Barplot: Tỷ lệ doanh thu theo địa điểm cửa hàng

revenue_by_store1 <- BK1 %>%
  group_by(Store_Location) %>%
  summarise(Total_Revenue = sum(Revenue), .groups = "drop")

ggplot(revenue_by_store, 
       aes(x = reorder(Store_Location, -Total_Revenue), y = Total_Revenue, fill = Store_Location)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = scales::dollar(Total_Revenue, accuracy = 1)),
            vjust = -0.4, size = 4) +
  labs(title = "Doanh Thu Theo Địa Điểm Cửa Hàng (Barplot)",
       x = "Địa Điểm Cửa Hàng",
       y = "Tổng Doanh Thu") +
  scale_y_continuous(labels = scales::dollar) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

Ý nghĩa: Dễ dàng nhìn thấy cửa hàng nào đóng góp cao nhất → hỗ trợ ưu tiên marketing & phân bổ hàng. Kết quả cho thấy toàn bộ hệ thống cửa hàng đang hoạt động một cách ổn định và cân bằng, không có chi nhánh nào tỏ ra vượt trội hay yếu kém một cách đáng kể so với phần còn lại.

4.1.5 Biểu đồ Treemap: Tỷ lệ doanh thu theo địa điểm cửa hàng

ggplot(revenue_by_store, aes(area = Total_Revenue, fill = Store_Location, label = Store_Location)) +
  geom_treemap() +
  geom_treemap_text(colour = "white", place = "centre", size = 14, grow = TRUE) +
  labs(title = "Tỷ Trọng Doanh Thu Theo Địa Điểm Cửa Hàng (Treemap)",
       fill = "Địa Điểm Cửa Hàng") +
  theme_minimal()

Ý nghĩa: Diện tích mỗi ô = mức doanh thu → nhìn phát biết ngay store nào chiếm tỷ trọng lớn.

4.1.6 Biểu đồ đường: Xu hướng doanh thu qua các tháng

  • Mục đích: Theo dõi sự biến động doanh thu theo thời gian.
  revenue_over_time <- BK1 %>%
  mutate(YearMonth = floor_date(Date, "month")) %>%
  group_by(YearMonth) %>%
  summarise(Monthly_Revenue = sum(Revenue), .groups = "drop")
ggplot(revenue_over_time, aes(x = YearMonth, y = Monthly_Revenue)) +
  geom_line(color = "steelblue", size = 1) +
  geom_point(color = "darkred", size = 2, shape = 21, fill = "yellow") +
  labs(title = "Xu Hướng Doanh Thu Hàng Tháng",
       subtitle = "Phân tích doanh thu từ 2022 đến 2024",
       x = "Thời Gian", y = "Doanh Thu Tháng") +
  scale_y_continuous(labels = scales::dollar) +
  scale_x_date(date_breaks = "3 months", date_labels = "%b %Y") +
  theme_light() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
## 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.

Ý nghĩa: Doanh thu hàng tháng không ổn định mà dao động khá mạnh, thường nằm trong khoảng từ 13 triệu đến 14.5 triệu đô la. Có những đỉnh và đáy rõ rệt qua các tháng. Thường có sự sụt giảm doanh thu vào khoảng tháng 4 và một sự tăng trưởng vào giữa hoặc cuối năm. Điều này có thể liên quan đến các yếu tố mùa vụ trong kinh doanh.

4.1.7 Biểu đồ đường: Xu hướng doanh thu qua các tháng có thêm đường trung bình động 3 tháng

  • Mục đích:Theo dõi động thái và đánh giá hiệu suất kinh doanh thông qua dữ liệu Doanh Thu Hàng Tháng trong giai đoạn 2020 - 2024. Dùng để phân tích xu hướng dài hạn (sử dụng đường Trung Bình Động 3 Tháng - MA3) và nhận diện các biến động ngắn hạn (doanh thu thực tế) nhằm hỗ trợ quyết định kinh doanh và dự báo.
revenue_over_time <- BK1 %>%
  mutate(YearMonth = floor_date(Date, "month")) %>%
  group_by(YearMonth) %>%
  summarise(Monthly_Revenue = sum(Revenue), .groups = "drop") %>%
  mutate(MA3 = rollmean(Monthly_Revenue, k = 3, fill = NA, align = "right"))

ggplot(revenue_over_time, aes(x = YearMonth)) +
  geom_line(aes(y = Monthly_Revenue), color = "steelblue", size = 1) +
  geom_point(aes(y = Monthly_Revenue), color = "darkred", size = 2, shape = 21, fill = "yellow") +
  
  # Đường trung bình động 3 tháng
  geom_line(aes(y = MA3), color = "orange", size = 1.2, linetype = "solid") +
  
  labs(title = "Xu Hướng Doanh Thu Hàng Tháng",subtitle = "Có thêm đường Trung Bình Động 3 Tháng (MA3) để làm mượt xu hướng",
       x = "Thời Gian", 
       y = "Doanh Thu Tháng") +
  scale_y_continuous(labels = scales::dollar) +
  scale_x_date(date_breaks = "3 months", date_labels = "%b %Y") +
  theme_light() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Ý nghĩa: Đường MA3 cho thấy trong suốt giai đoạn từ 2020 đến giữa 2024, xu hướng doanh thu dài hạn là tương đối ổn định, dao động quanh mốc 13.5 đến 14 triệu đô la. Biểu đồ này cung cấp một cái nhìn sâu sắc và đáng tin cậy hơn về hiệu suất kinh doanh. Nó không chỉ cho thấy những gì đã xảy ra hàng tháng mà còn giúp lọc ra các biến động ngắn hạn để tập trung vào xu hướng cốt lõi, giúp các nhà quản lý đưa ra quyết định chiến lược tốt hơn.

4.1.8 Biểu đồ hộp (Boxplot): Phân bổ giá xe theo từng mẫu

  • Mục đích: So sánh sự phân bổ giá (từ thấp nhất đến cao nhất, trung vị, các giá trị ngoại lai) của các mẫu xe.
ggplot(BK1, aes(x = reorder(Bike_Model, Price), y = Price, fill = Bike_Model)) +
  geom_boxplot(show.legend = FALSE, outlier.colour = "red", outlier.shape = 8) +
  labs(title = "Phân Bổ Giá Bán Của Các Mẫu Xe",
       subtitle = "So sánh sự biến động giá và các giá trị ngoại lai",
       x = "Mẫu Xe", y = "Giá Bán") +
  scale_y_continuous(labels = scales::dollar) +
  stat_summary(fun = mean, geom = "point", shape = 23, size = 4, fill = "white") + # Thêm điểm trung bình
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Ý nghĩa: Biểu đồ hộp cho thấy “Electric Bolt” không chỉ có giá trung bình cao nhất mà còn có khoảng giá rất rộng. Ngược lại, “Kids Explorer” có giá thấp và ít biến động.

4.1.9 Biểu đồ cột Facet: Số lượng xe bán ra theo từng mẫu xe và từng địa điểm cửa hàn

  • Mục đích: Biểu đồ nhằm so sánh số lượng xe bán ra của từng mẫu xe tại mỗi cửa hàng. Việc hiển thị theo dạng facet giúp nhìn rõ sự khác biệt về xu hướng tiêu thụ sản phẩm theo từng địa điểm.
quantity_by_model_store <- BK1 %>%
  group_by(Store_Location, Bike_Model) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = "drop")

ggplot(quantity_by_model_store, aes(x = Bike_Model, y = Total_Quantity)) +
  geom_col(position = "dodge", fill = "gray40") +
  facet_wrap(~ Store_Location) +
  geom_text(aes(label = Total_Quantity), vjust = -0.4, size = 3) +
  labs(title = "Số lượng xe bán ra theo từng mẫu xe tại mỗ địa điểm cửa hàng",
       subtitle = "So sánh mức độ tiêu thụ sản phẩm giữa các địa điểm",
       x = "Mẫu Xe",
       y = "Số Lượng Bán Ra") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Ý nghĩa: Mẫu xe được tiêu thụ khá đồng đều, không có mẫu xe nào bị ế hay bán vượt trội quá mạnh. New York và Los Angeles là hai thị trường có nhu cầu tiêu thụ xe cao → nên ưu tiên phân bổ hàng và chiến dịch quảng bá.

4.1.10. Biểu đồ mật độ (density plot): Quan sát mật độ phân bố giá xe (Price).

  • Mục đích:Biểu đồ nhằm xác định cách phân bố mức giá bán của các mẫu xe trong dữ liệu. Qua đó, giúp nhận biết khoảng giá phổ biến, mức độ tập trung giá, và xem liệu có xuất hiện các dòng xe giá cao làm kéo đuôi phân bố hay không.
ggplot(BK1, aes(x = Price)) +
geom_density(fill = "orange", alpha = 0.6, color = "black", size = 1) +
labs(title = "Phân bố giá xe trong dữ liệu bán hàng",
x = "Giá bán (Price)",
y = "Mật độ") +
theme_minimal()

Ý nghĩa:Biểu đồ mật độ cho thấy cách giá xe được phân bố trong dữ liệu bán hàng. Đường mật độ tương đối phẳng ở phần trung tâm (khoảng 1.000–4.000), cho thấy phần lớn xe có giá nằm trong khoảng này và phân bố khá đồng đều, không tập trung mạnh vào một mức giá cụ thể. Ở hai đầu phân bố (giá rất thấp hoặc rất cao) ta thấy mật độ giảm xuống, nghĩa là số lượng xe giá quá rẻ hoặc quá đắt là ít.

4.1.11 Biểu đồ mật độ (density): Biểu đồ mật độ phân bố giá theo từng mẫu xe

  • Mục đích:Biểu đồ mật độ phân bố giá theo từng mẫu xe nhằm so sánh mức giá phổ biến giữa các dòng xe khác nhau. Qua đó giúp nhận diện dải giá đặc trưng của từng mẫu xe trong danh mục sản phẩm.
BK1Density <- BK1
BK1Density %>% 
  ggplot(aes(x = Price)) +
  geom_density(fill = "blue", alpha = 0.7) +
  facet_wrap(~ Bike_Model) +
  labs(
    title = "Phân bố giá theo từng mẫu xe",
    x = "Giá bán (Price)",
    y = "Mật độ"
  ) +
  theme_minimal()

Ý nghĩa: Giá bán của các mẫu xe nhìn chung phân bố khá đồng đều trong khoảng 500 – 5000 USD, không có sự chênh lệch quá lớn giữa các dòng xe. Tuy nhiên, một số dòng như BMX và Cruiser có xu hướng tập trung giá ở mức thấp hơn, trong khi Electric Bike và Hybrid Bike có phân bố giá rộng hơn, phản ánh các dòng xe này có thể có nhiều phiên bản hoặc phân khúc khác nhau. Điều này cho thấy thị trường sản phẩm có cấu trúc giá đa dạng nhưng vẫn cân đối giữa các nhóm xe.

4.2. Phân tích Khách hàng

4.2.1 Biểu đồ tần suất (Histogram): Phân bổ độ tuổi của khách hàng

  • Mục đích: Xem xét sự phân bổ của khách hàng theo các nhóm tuổi.
ggplot(BK1, aes(x = Customer_Age)) +
  geom_histogram(binwidth = 5, fill = "skyblue", color = "black", alpha = 0.7) +
  geom_vline(aes(xintercept = mean(Customer_Age)), color = "red", linetype = "dashed", size = 1) +annotate("text", x = mean(BK$Customer_Age) + 10, y = 7000, 
           label = paste("Tuổi TB:", round(mean(BK$Customer_Age), 1)), color = "red") +
  geom_density(aes(y = after_stat(count) * 5), color = "darkgreen", size = 1) + # Thêm đường mật độ
  labs(title = "Phân Bổ Độ Tuổi Của Khách Hàng",
       x = "Tuổi", y = "Số Lượng Khách Hàng") +
  theme_classic()

Ý nghĩa: Điểm đặc biệt và nổi bật nhất của biểu đồ này không phải là sự tập trung vào một nhóm tuổi nào, mà là sự phân bổ gần như bằng nhau của khách hàng trên tất cả các nhóm tuổi từ thanh niên đến khi về hưu. Các cột trong khoảng từ 25 đến 65 tuổi có chiều cao gần như không chênh lệch.

4.2.2 Biểu đồ cột chồng: Doanh thu theo nhóm tuổi và giới tính

  • Mục đích: So sánh doanh thu giữa các nhóm tuổi và xem xét tỷ lệ đóng góp của nam và nữ trong mỗi nhóm.
revenue_age_gender <- BK1 %>%
  group_by(Age_Group, Customer_Gender) %>%
  summarise(Total_Revenue = sum(Revenue), .groups = 'drop')

ggplot(revenue_age_gender, aes(x = Age_Group, y = Total_Revenue, fill = Customer_Gender)) +
  geom_bar(stat = "identity", position = "stack") +
  facet_wrap(~Customer_Gender) + # Chia thành các biểu đồ nhỏ theo giới tính
  labs(title = "Doanh Thu Theo Nhóm Tuổi và Giới Tính",
       x = "Nhóm Tuổi", y = "Tổng Doanh Thu", fill = "Giới Tính") +
  scale_y_continuous(labels = scales::dollar) +
  scale_fill_brewer(palette = "Set2") +
  theme_minimal()

Ý nghĩa: Nhóm tuổi “Middle-Aged” (36-55) đóng góp nhiều doanh thu nhất. Trong hầu hết các nhóm tuổi, tỷ lệ doanh thu từ nam và nữ khá tương đồng.

4.2.3 Biểu đồ phân tán (Scatter Plot): Mối quan hệ giữa tuổi và giá trị đơn hàng

  • Mục đích: Kiểm tra xem có mối liên hệ nào giữa tuổi của khách hàng và số tiền họ chi tiêu cho mỗi giao dịch hay không.
ggplot(BK1 %>% sample_n(5000), aes(x = Customer_Age, y = Revenue)) + # Lấy mẫu 5000 điểm để vẽ cho nhẹ
  geom_point(alpha = 0.3, aes(color = Store_Location)) +
  geom_smooth(method = "lm", color = "red", se = FALSE, linetype = "dashed") +
  labs(title = "Mối Quan Hệ Giữa Tuổi Khách Hàng và Giá Trị Đơn Hàng",
       subtitle = "Mỗi điểm là một giao dịch",
       x = "Tuổi Khách Hàng", y = "Doanh Thu Mỗi Giao Dịch", color = "Cửa hàng") +
  scale_y_continuous(labels = scales::dollar) +
  theme_gray()

Ý nghĩa: Các điểm dữ liệu phân bố khá ngẫu nhiên và đường xu hướng (màu đỏ) gần như nằm ngang, khẳng định lại kết quả phân tích tương quan ở Chương 3: không có mối liên hệ rõ ràng giữa tuổi và giá trị đơn hàng.

4.3. Phân tích Đa chiều và Chuyên sâu

4.3.1 Heatmap: Doanh thu theo ngày trong tuần và tháng

  • Mục đích: Tìm ra những “điểm nóng” về thời gian, tức là những tháng và ngày nào trong tuần có doanh thu cao nhất.
revenue_heatmap_data <- BK1 %>%
  group_by(Weekday, Month) %>%
  summarise(Total_Revenue = sum(Revenue), .groups = 'drop')

ggplot(revenue_heatmap_data, aes(x = Weekday, y = Month, fill = Total_Revenue)) +
  geom_tile(color = "white") +
  geom_text(aes(label = scales::dollar(Total_Revenue, scale = 1/1000, suffix = "K")), size = 3) +
  scale_fill_gradient(low = "lightyellow", high = "darkred", labels = scales::dollar) +
  labs(title = "Heatmap Doanh Thu Theo Ngày và Tháng",
       subtitle = "Doanh thu được tính bằng nghìn đô la (K)",
       x = "Ngày Trong Tuần", y = "Tháng", fill = "Doanh Thu") +
  theme_minimal()

Ý nghĩa: Các ô màu càng đậm (đỏ) thể hiện doanh thu càng cao. Biểu đồ này giúp nhanh chóng xác định rằng cuối tuần và các tháng mùa hè thường là thời điểm kinh doanh tốt nhất.

4.3.2 Biểu đồ cột phân nhóm: Số lượng xe bán theo mùa và địa điểm cửa hàng

  • Mục đích: So sánh hiệu suất bán hàng (về số lượng) của các cửa hàng trong các mùa khác nhau.
quantity_season_store <- BK1 %>%
  group_by(Season, Store_Location) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = 'drop')

ggplot(quantity_season_store, aes(x = Season, y = Total_Quantity, fill = Store_Location)) +
  geom_bar(stat = "identity", position = position_dodge(width = 0.9)) +
  geom_text(aes(label = Total_Quantity), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) +
  labs(title = "Số Lượng Xe Bán Theo Mùa và Địa Điểm Cửa Hàng",
       x = "Mùa", y = "Tổng Số Lượng Xe", fill = "Địa Điểm Cửa Hàng") +
  theme_light() +
  scale_fill_viridis_d(option = "C")

Ý nghĩa: - Về mùa: Mùa Hè và mùa Xuân là hai mùa có số lượng xe bán ra cao nhất một cách rõ rệt. Ngược lại, mùa Thu là mùa bán chậm nhất, với doanh số thấp hơn hẳn so với các mùa còn lại. - Về địa điểm: Không có một cửa hàng nào cố định bán chạy nhất ở tất cả các mùa. Hiệu suất bán hàng thay đổi theo mùa, ví dụ: Los Angeles (màu tím) là nơi bán chạy nhất vào mùa Hè, trong khi San Antonio (màu vàng) lại dẫn đầu trong mùa Xuân.

4.3.3 Biểu đồ đường: Xu hướng số lượng xe bán theo quý và địa điểm cửa hàn

g * Mục đích: Biểu đồ này được sử dụng để theo dõi xu hướng tiêu thụ xe theo từng quý và so sánh hiệu suất bán hàng giữa các cửa hàng. Qua đó giúp nhận biết thời điểm bán hàng tăng/giảm và xác định cửa hàng nào hoạt động tốt hơn trong từng giai đoạn.

quantity_quarter_store <- BK1 %>%
  mutate(Quarter = paste0("Q", quarter(Date), "-", year(Date))) %>%   
  # Tạo biến Quý (VD: Q1-2023)
  group_by(Quarter, Store_Location) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = "drop") %>%
  arrange(Quarter)

ggplot(quantity_quarter_store, aes(x = Quarter, y = Total_Quantity, color = Store_Location, group = Store_Location)) +
  geom_line(size = 1.2) +
  geom_point(size = 2) +
  labs(title = "Xu Hướng Số Lượng Xe Bán Theo Quý và Địa điểm cửa Hàng",
       x = "Quý",
       y = "Tổng Số Lượng Xe",
       color = "Địa điểm cửa Hàng") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Ý nghĩa:Số lượng xe bán ra dao động theo từng quý, thể hiện sự biến động nhu cầu của thị trường theo thời gian. Các cửa hàng như New York, Chicago và Los Angeles thường có mức bán ra cao và ổn định hơn so với các cửa hàng còn lại. Một số quý xuất hiện mức giảm mạnh (điểm đáy), cho thấy khả năng chịu ảnh hưởng từ yếu tố mùa vụ hoặc điều kiện kinh tế. Xu hướng tổng thể cho thấy không có chênh lệch quá lớn giữa các cửa hàng, nhưng một số cửa hàng duy trì hiệu suất tốt hơn liên tục, phản ánh ưu thế vị trí, thị hiếu hoặc chiến lược bán hàng hiệu quả.

4.3.4 Biểu đồ thanh ngang: Top 5 nhân viên theo số lượng giao dịch

  • Mục đích: Vinh danh các nhân viên tích cực nhất dựa trên số lượng giao dịch họ thực hiện.
top5_salesperson_trans <- BK1 %>%
  count(Salesperson_ID, name = "Transaction_Count", sort = TRUE) %>%
  slice_head(n = 5)

ggplot(top5_salesperson_trans, aes(x = reorder(Salesperson_ID, Transaction_Count), y = Transaction_Count, fill = Salesperson_ID)) +
  geom_bar(stat = "identity", show.legend = FALSE, width = 0.7) +
  coord_flip() + # Lật biểu đồ thành dạng ngang
  geom_text(aes(label = Transaction_Count), hjust = -0.2) +
  labs(title = "Top 5 Nhân Viên Theo Số Lượng Giao Dịch",
       subtitle = "Dựa trên tổng số hóa đơn đã xử lý",
       x = "Nhân Viên", y = "Số Lượng Giao Dịch") +
  theme_minimal()

Ý nghĩa: Biểu đồ xác định nhân viên có mã số 422 là người xử lý nhiều giao dịch nhất (143). Tuy nhiên, điều quan trọng hơn là hiệu suất của top 5 nhân viên này cực kỳ đồng đều và sít sao, chỉ chênh lệch nhau tối đa 3 giao dịch.

4.3.5 Biểu đồ bong bóng: Tổng quan các mẫu xe

  • Mục đích: Hiển thị 3 chiều thông tin: Số lượng bán (trục x), Giá trung bình (trục y), và Doanh thu (kích thước bong bóng).
bubble_data <- BK1 %>%
  group_by(Bike_Model) %>%
  summarise(Total_Revenue = sum(Revenue),Total_Quantity = sum(Quantity),
            Avg_Price = mean(Price), .groups = 'drop')

ggplot(bubble_data, aes(x = Total_Quantity, y = Avg_Price, size = Total_Revenue, color = Bike_Model)) +
  geom_point(alpha = 0.7) +
  scale_size(range = c(5, 25), name = "Tổng Doanh Thu (USD)") +
  scale_color_viridis_d(guide = "none") +
  geom_text(aes(label = Bike_Model), size = 3.5, vjust = -2.5, fontface = "bold") +
  labs(title = "Phân Tích Tổng Quan Các Mẫu Xe",
       subtitle = "Doanh thu (kích thước), Số lượng (trục x), Giá TB (trục y)",
       x = "Tổng Số Lượng Bán Ra", y = "Giá Bán Trung Bình (USD)") +
  scale_y_continuous(labels = scales::dollar) +
  theme_minimal()

Ý nghĩa: “Electric Bolt” nằm ở góc trên bên phải với bong bóng lớn nhất: giá trung bình cao, số lượng bán nhiều, doanh thu vượt trội.

4.3.6 Biểu đồ Treemap: Tỷ trọng doanh thu theo mẫu xe và địa điểm cửa hàng

  • Mục đích: Hiển thị cấu trúc doanh thu lồng nhau: mỗi cửa hàng là một nhóm lớn, bên trong là doanh thu của từng mẫu xe.
treemap_data <- BK1 %>%
  group_by(Store_Location, Bike_Model) %>%
  summarise(Total_Revenue = sum(Revenue), .groups = 'drop')

ggplot(treemap_data, aes(area = Total_Revenue, fill = Bike_Model, 
                         label = paste(Bike_Model, scales::dollar(Total_Revenue, scale = 1/1e6, suffix = "M"), sep = "\n"), 
                         subgroup = Store_Location)) +
  geom_treemap() +
  geom_treemap_subgroup_border(colour = "white", size = 5) +
  geom_treemap_subgroup_text(place = "centre", grow = T, alpha = 0.4, colour = "black", fontface = "italic", min.size = 0) +
  geom_treemap_text(colour = "white", place = "topleft", reflow = T) +
  labs(title = "Tỷ Trọng Doanh Thu Các Mẫu Xe Theo Địa Điểm Cửa Hàng (Đơn vị: triệu USD)") +
  theme(legend.position = "none")

Ý nghĩa: Biểu đồ cho thấy mỗi cửa hàng có một mẫu xe “chủ lực” (con gà đẻ trứng vàng) khác nhau, ví dụ Hybrid Bike ở New York hay Road Bike ở Philadelphia, chứng tỏ thị hiếu của khách hàng ở mỗi vùng là riêng biệt. Tuy nhiên, điểm đáng chú ý hơn là tại một cửa hàng bất kỳ, doanh thu giữa các mẫu xe lại rất đồng đều, không có mẫu nào vượt trội hoàn toàn so với phần còn lại. Điều này cho thấy chiến lược kinh doanh đa dạng các dòng sản phẩm đang rất hiệu quả ở tất cả các chi nhánh.

4.3.7 Biểu đồ Sankey: Luồng khách hàng từ giới tính đến mẫu xe

  • Mục đích: Minh họa “dòng chảy” của khách hàng từ một nhóm (Giới tính) đến một nhóm khác (Mẫu xe).
sankey_data <- BK1 %>%
  group_by(Customer_Gender, Bike_Model) %>%
  summarise(Count = n(), .groups = 'drop')

ggplot(as.data.frame(sankey_data),
       aes(y = Count, axis1 = Customer_Gender, axis2 = Bike_Model)) +
  geom_alluvium(aes(fill = Bike_Model), width = 1/12, alpha = 0.7) +
  geom_stratum(width = 1/6, fill = "grey", color = "white") +
  geom_label(stat = "stratum", aes(label = after_stat(stratum))) +
  scale_x_discrete(limits = c("Giới tính", "Mẫu xe"), expand = c(.05, .05)) +
  labs(title = "Luồng Lựa Chọn Mẫu Xe Theo Giới Tính",subtitle = "Độ rộng của dải màu thể hiện số lượng khách hàng") +
  theme_void() +
  theme(legend.position = "bottom")

Ý nghĩa: Điểm đáng chú ý nhất từ biểu đồ này là không có sự khác biệt rõ rệt trong sở thích chọn xe giữa khách hàng nam và nữ. Nhìn chung, tất cả các mẫu xe đều được cả hai giới lựa chọn với tỷ lệ rất cân bằng. Điều này cho thấy các mẫu xe có sức hấp dẫn rộng rãi và không cần các chiến lược marketing nhắm riêng vào từng giới tính.

4.3.8 Biểu đồ tương quan (Correlogram)

  • Mục đích: Hiển thị ma trận tương quan giữa các biến số.
numeric_vars <- BK1 %>%
  select(where(is.numeric)) %>%
  select(-Sale_ID, -Year, -Gender_Encoded, -Purchase_Count) # Loại bỏ các biến không cần thiết

cor_matrix <- cor(numeric_vars, use = "complete.obs")
corrplot(cor_matrix, method = "color", type = "upper",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Ma Trận Tương Quan Giữa Các Biến Số", mar = c(0, 0, 1, 0))

Ý nghĩa: Biểu đồ cho thấy Price (Giá) có tương quan dương mạnh nhất với Revenue (Doanh thu), với hệ số là 0.71. Điều này có nghĩa là khi giá sản phẩm tăng thì doanh thu có xu hướng tăng theo. Tương tự, Quantity (Số lượng) cũng có tương quan dương đáng kể với Revenue (0.62), điều này là hợp lý vì bán được nhiều hàng hơn sẽ tạo ra nhiều doanh thu hơn. Một điểm đáng chú ý khác là hầu như không có mối tương quan nào giữa Customer_Age (Tuổi khách hàng) và Revenue, cho thấy tuổi tác không phải là yếu tố ảnh hưởng đến doanh thu trong bộ dữ liệu này.

Chương 2: Phân tích đánh giá kết quả hoạt động kinh doanh của CTCP Hoàng Anh Gia Lai (HAG)

Giới thiệu

Bộ dữ liệu tài chính được sử dụng trong nghiên cứu này là tập hợp các báo cáo tài chính được thu thập từ Công ty Cổ phần Hoàng Anh Gia Lai (HAG) trong giai đoạn từ năm 2015 đến năm 2024. Dữ liệu gốc bao gồm nhiều báo cáo chính: Bảng Cân đối Kế toán, Báo cáo Lưu chuyển Tiền tệ và Báo cáo Kết quả Kinh doanh (KQKD). Đây là dữ liệu theo năm, cung cấp cái nhìn xuyên suốt về hiệu suất hoạt động của công ty trong một thập kỷ đầy biến động.

1. Tổng quan dữ liệu

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

1.2 Nạp dữ liệu vào R

library(dplyr)
library(readr)
HAG <- read_csv("D:/Thúy Hiền/Ngôn ngữ lập trình trong phân tích dữ liệu/HAG.csv")
## New names:
## Rows: 10 Columns: 126
## ── Column specification
## ──────────────────────────────────────────────────────── Delimiter: "," dbl
## (122): Nam, Tai_san_ngan_han, tien...3, tien...4, cac_khoan_phai_thu_nga... lgl
## (4): nguon_von, Luu_chuyen_tien_tu_hoat_dong_kinh_doanh, luu_chuyen_ti...
## ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
## Specify the column types or set `show_col_types = FALSE` to quiet this message.
## • `tien` -> `tien...3`
## • `tien` -> `tien...4`
## • `hang_ton_kho` -> `hang_ton_kho...11`
## • `hang_ton_kho` -> `hang_ton_kho...12`
## • `tai_san_ngan_han_khac` -> `tai_san_ngan_han_khac...14`
## • `tai_san_ngan_han_khac` -> `tai_san_ngan_han_khac...18`
## • `von_chu_so_huu` -> `von_chu_so_huu...58`
## • `von_chu_so_huu` -> `von_chu_so_huu...59`
## • `lo_ke_toan_truoc_thue` -> `lo_ke_toan_truoc_thue...84`
## • `lo_ke_toan_truoc_thue` -> `lo_ke_toan_truoc_thue...93`

1.3 Nạp thư viện cần thiết

library(knitr)
library(kableExtra)
library(e1071)
## 
## Attaching package: 'e1071'
## The following objects are masked from 'package:moments':
## 
##     kurtosis, moment, skewness
## The following object is masked from 'package:ggplot2':
## 
##     element
library(scales)

1.4 Hiển thị 10 dòng đầu tiên của bộ dữ biệu BCTC

HAG %>% 
  mutate(across(where(is.numeric), ~ format(.x, big.mark = ".", decimal.mark = ",", scientific = FALSE)))
## # A tibble: 10 × 126
##    Nam   Tai_san_ngan_han tien...3      tien...4      cac_khoan_phai_thu_ngan_…¹
##    <chr> <chr>            <chr>         <chr>         <chr>                     
##  1 2.015 "13.215.916.673" "967.966.695" "967.966.695" 8.469.868.136             
##  2 2.016 " 9.394.220.363" "791.208.293" "791.208.293" 6.768.206.227             
##  3 2.017 " 8.815.052.625" "141.473.491" "141.473.491" 7.481.808.506             
##  4 2.018 " 6.567.906.781" "337.736.719" "337.736.719" 4.747.120.864             
##  5 2.019 " 7.073.675.026" "254.431.616" "254.431.616" 4.569.330.218             
##  6 2.020 " 8.930.375.455" " 97.151.198" " 97.151.198" 6.410.638.635             
##  7 2.021 " 7.051.853.577" " 78.298.037" " 78.298.037" 6.535.652.693             
##  8 2.022 " 8.038.560.913" " 72.372.525" " 72.372.525" 6.765.361.545             
##  9 2.023 " 8.768.525.586" " 41.812.548" " 41.812.548" 7.780.210.370             
## 10 2.024 " 8.435.357.672" "149.708.825" "149.708.825" 7.536.948.369             
## # ℹ abbreviated name: ¹​cac_khoan_phai_thu_ngan_han
## # ℹ 121 more variables: phai_thu_ngan_han_cua_khach_hang <chr>,
## #   tra_truoc_cho_nguoi_ban_ngan_han <chr>, phai_thu_ve_cho_vay_ngan_han <chr>,
## #   phai_thu_ngan_han_khac <chr>, du_phong_phai_thu_ngan_han_kho_doi <chr>,
## #   hang_ton_kho...11 <chr>, hang_ton_kho...12 <chr>,
## #   du_phong_giam_gia_hang_ton_kho <chr>, tai_san_ngan_han_khac...14 <chr>,
## #   chi_phi_tra_truoc_ngan_han <chr>, thue_gtgt_duoc_khau_tru <chr>, …

1.5 Chọn phân tích Bảng Kết quả Kinh doanh

Trong bối cảnh HAG trải qua quá trình tái cấu trúc và chuyển đổi mô hình kinh doanh (từ bất động sản/cao su sang nông nghiệp), việc đánh giá hiệu suất hoạt động cốt lõi là tối quan trọng. Bảng KQKD là báo cáo duy nhất cung cấp bức tranh chi tiết về các nguồn thu nhập và chi phí phát sinh, cho phép nhà nghiên cứu:

Phân tích Chiều sâu Lợi nhuận: Theo dõi lợi nhuận từ các hoạt động chính (nông nghiệp) qua các tầng (Lợi nhuận gộp, Lợi nhuận từ hoạt động kinh doanh).

Đánh giá Sức khỏe Tài chính (Cơ bản): Đánh giá khả năng tạo ra dòng tiền từ hoạt động kinh doanh (trước khi xem xét các hoạt động đầu tư/tài chính).

Xác định Xu hướng (Trend Analysis): So sánh các yếu tố doanh thu và chi phí qua các năm để làm rõ tính ổn định và động lực tăng trưởng mới của HAG.

ten_bien_kqkd <- c(
  "Doanh_thu_ban_hang_va_cung_cap_dich_vu",
  "cac_khoan_giam_tru_doanh_thu",
  "doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu",
  "gia_von_hang_ban_va_dich_vu_cung_cap",
  "loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu",
  "doanh_thu_hoat_dong_tai_chinh",
  "chi_phi_tai_chinh",
  "trong_do_chi_phi_lai_vay",
  "phan_lai_trong_cong_ty_lien_ket",
  "chi_phi_ban_hang",
  "chi_phi_quan_ly_doanh_nghiep",
  "lo_thuan_tu_hoat_dong_kinh_doanh",
  "thu_nhap_khac",
  "chi_phi_khac",
  "lo_khac",
  "lo_ke_toan_truoc_thue",
  "chi_phi_thue_tndn_hien_hanh",
  "chi_phi_thu_nhap_thue_tndn_hoan_lai",
  "lo_sau_thue_tndn",
  "loloi_nhuan_sau_thue_cua_cong_ty_me",
  "lo_sau_thue_cua_co_dong_khong_kiem_soat",
  "lo_lai_co_ban_tren_co_phieu_vnd",
  "lo_lai_suy_giam_tren_co_phieu_vnd"
)

# 2. Tạo tập con (subset)
bao_cao_kqkd_subset <- HAG %>%
  select(
    Nam,                   # Giữ lại cột Năm
    any_of(ten_bien_kqkd)  # Chỉ chọn các cột TỒN TẠI trong danh sách mới
  )

# In ra vài dòng đầu của tập con để kiểm tra kết quả
print(head(bao_cao_kqkd_subset))
## # A tibble: 6 × 23
##     Nam Doanh_thu_ban_hang_va_cu…¹ cac_khoan_giam_tru_d…² doanh_thu_thuan_ve_b…³
##   <dbl>                      <dbl>                  <dbl>                  <dbl>
## 1  2015                 6252482061                 -35528             6252446533
## 2  2016                 6441028981               -1249713             6439779268
## 3  2017                 4841225074                     NA             4841225074
## 4  2018                         NA                     NA             5388200400
## 5  2019                 2091833174              -16389150             2075444024
## 6  2020                 3189964886              -13318930             3176645956
## # ℹ abbreviated names: ¹​Doanh_thu_ban_hang_va_cung_cap_dich_vu,
## #   ²​cac_khoan_giam_tru_doanh_thu,
## #   ³​doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
## # ℹ 19 more variables: gia_von_hang_ban_va_dich_vu_cung_cap <dbl>,
## #   loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu <dbl>,
## #   doanh_thu_hoat_dong_tai_chinh <dbl>, chi_phi_tai_chinh <dbl>,
## #   trong_do_chi_phi_lai_vay <dbl>, phan_lai_trong_cong_ty_lien_ket <dbl>, …

1.6 Khám phá dữ liệu

1.6.1 Số quan sát và số biến

cat("Số biến trong bộ dữ liệu là:", ncol(bao_cao_kqkd_subset))
## Số biến trong bộ dữ liệu là: 23
cat("Số quan sát trong bộ dữ liệu là:", nrow(bao_cao_kqkd_subset))
## Số quan sát trong bộ dữ liệu là: 10

1.6.2 Cấu trúc và kiểu dữ liệu của các biến

Liệt kê tên các biến

cac_bien_kqkd <- setdiff(colnames(bao_cao_kqkd_subset), "Nam")
print(cac_bien_kqkd)
##  [1] "Doanh_thu_ban_hang_va_cung_cap_dich_vu"         
##  [2] "cac_khoan_giam_tru_doanh_thu"                   
##  [3] "doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu"
##  [4] "gia_von_hang_ban_va_dich_vu_cung_cap"           
##  [5] "loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu"  
##  [6] "doanh_thu_hoat_dong_tai_chinh"                  
##  [7] "chi_phi_tai_chinh"                              
##  [8] "trong_do_chi_phi_lai_vay"                       
##  [9] "phan_lai_trong_cong_ty_lien_ket"                
## [10] "chi_phi_ban_hang"                               
## [11] "chi_phi_quan_ly_doanh_nghiep"                   
## [12] "lo_thuan_tu_hoat_dong_kinh_doanh"               
## [13] "thu_nhap_khac"                                  
## [14] "chi_phi_khac"                                   
## [15] "lo_khac"                                        
## [16] "chi_phi_thue_tndn_hien_hanh"                    
## [17] "chi_phi_thu_nhap_thue_tndn_hoan_lai"            
## [18] "lo_sau_thue_tndn"                               
## [19] "loloi_nhuan_sau_thue_cua_cong_ty_me"            
## [20] "lo_sau_thue_cua_co_dong_khong_kiem_soat"        
## [21] "lo_lai_co_ban_tren_co_phieu_vnd"                
## [22] "lo_lai_suy_giam_tren_co_phieu_vnd"

Bộ dữ liệu Báo cáo Kết quả Hoạt động Kinh doanh có 22 biến

Kiểu dữ liệu các biến

str(bao_cao_kqkd_subset)
## tibble [10 × 23] (S3: tbl_df/tbl/data.frame)
##  $ Nam                                            : num [1:10] 2015 2016 2017 2018 2019 ...
##  $ Doanh_thu_ban_hang_va_cung_cap_dich_vu         : num [1:10] 6.25e+09 6.44e+09 4.84e+09 NA 2.09e+09 ...
##  $ cac_khoan_giam_tru_doanh_thu                   : num [1:10] -35528 -1249713 NA NA -16389150 ...
##  $ doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu: num [1:10] 6.25e+09 6.44e+09 4.84e+09 5.39e+09 2.08e+09 ...
##  $ gia_von_hang_ban_va_dich_vu_cung_cap           : num [1:10] -4.40e+09 -5.43e+09 -3.11e+09 -3.01e+09 -1.85e+09 ...
##  $ loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu  : num [1:10] 1.85e+09 1.01e+09 1.73e+09 2.37e+09 2.28e+08 ...
##  $ doanh_thu_hoat_dong_tai_chinh                  : num [1:10] 1.05e+09 9.48e+08 1.69e+09 1.40e+09 2.14e+09 ...
##  $ chi_phi_tai_chinh                              : num [1:10] -1.20e+09 -1.67e+09 -1.70e+09 -1.72e+09 -1.96e+09 ...
##  $ trong_do_chi_phi_lai_vay                       : num [1:10] -1.08e+09 -1.58e+09 -1.59e+09 -1.53e+09 -1.26e+09 ...
##  $ phan_lai_trong_cong_ty_lien_ket                : num [1:10] -1898163 11710281 -18433513 64840488 12562347 ...
##  $ chi_phi_ban_hang                               : num [1:10] -1.11e+08 -1.69e+08 -1.44e+08 -1.92e+08 -3.09e+08 ...
##  $ chi_phi_quan_ly_doanh_nghiep                   : num [1:10] -3.50e+08 -5.02e+08 -7.08e+08 -9.89e+08 -6.73e+08 ...
##  $ lo_thuan_tu_hoat_dong_kinh_doanh               : num [1:10] 1.24e+09 -3.76e+08 8.30e+08 9.41e+08 -5.68e+08 ...
##  $ thu_nhap_khac                                  : num [1:10] 1.81e+08 1.76e+08 2.67e+08 2.15e+07 4.26e+07 ...
##  $ chi_phi_khac                                   : num [1:10] -6.15e+08 -1.21e+09 -6.67e+08 -9.15e+08 -1.38e+09 ...
##  $ lo_khac                                        : num [1:10] -4.35e+08 -1.03e+09 -3.99e+08 -8.93e+08 -1.34e+09 ...
##  $ chi_phi_thue_tndn_hien_hanh                    : num [1:10] -1.54e+08 -1.03e+07 -2.48e+07 -3.00e+06 -2.25e+06 ...
##  $ chi_phi_thu_nhap_thue_tndn_hoan_lai            : num [1:10] -50302292 -86187524 -33735757 -38454058 98914750 ...
##  $ lo_sau_thue_tndn                               : num [1:10] 6.02e+08 -1.50e+09 3.72e+08 6.24e+06 -1.81e+09 ...
##  $ loloi_nhuan_sau_thue_cua_cong_ty_me            : num [1:10] 5.02e+08 -1.14e+09 6.96e+07 1.18e+08 2.17e+08 ...
##  $ lo_sau_thue_cua_co_dong_khong_kiem_soat        : num [1:10] 1.00e+08 -3.66e+08 3.02e+08 -1.11e+08 -2.03e+09 ...
##  $ lo_lai_co_ban_tren_co_phieu_vnd                : num [1:10] 613 -1439 80 127 233 ...
##  $ lo_lai_suy_giam_tren_co_phieu_vnd              : num [1:10] 613 -1439 80 127 233 ...

Các biến đều là biến định lượng thể hiện các thông số số học(num).

2. Xử lý dữ liệu thô và mã hoá dữ liệu

2.1 Kiểm tra chất lượng dữ liệu

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

cat("Tổng số giá trị bị thiếu (NA):", sum(is.na(bao_cao_kqkd_subset)))
## Tổng số giá trị bị thiếu (NA): 5

2.1.2 Xử lý giá trị bị thiếu

Tính giá trị trung vị (median) vào các giá trị bị thiếu.

bao_cao_kqkd_subset_sach_na <- bao_cao_kqkd_subset %>%
  mutate(across(where(is.numeric),
                ~ ifelse(is.na(.), median(., na.rm = TRUE), .)))
cat("Tổng số giá trị bị thiếu (NA):", sum(is.na(bao_cao_kqkd_subset_sach_na)))
## Tổng số giá trị bị thiếu (NA): 0

Nhận xét:Sau khi xử lý giá trị bị thiếu ta kiểm tra lại và thấy dữ liệu đã sạch.

2.1.3 Kiểm tra số dòng bị trùng lặp hoàn toàn

cat("\nTổng số dòng bị trùng lặp hoàn toàn:", sum(duplicated(bao_cao_kqkd_subset)))
## 
## Tổng số dòng bị trùng lặp hoàn toàn: 0

Nhận xét: Dữ liệu không có dòng (quan sát) nào bị trùng lặp hoàn toàn.

2.2 Mã hoá dữ liệu

Tạo cột “Tinh_trang_KD” (Tình trạng kinh doanh) dựa trên LNST

bao_cao_kqkd_Tinh_trang_KD <- bao_cao_kqkd_subset_sach_na %>%
 mutate(Tinh_trang_KD = ifelse(loloi_nhuan_sau_thue_cua_cong_ty_me > 0, "Có lãi", "Lỗ"))

# Chọn các cột cần thiết và hiển thị bảng với số liệu thô
bao_cao_kqkd_Tinh_trang_KD %>%
 select(Nam, loloi_nhuan_sau_thue_cua_cong_ty_me, Tinh_trang_KD) %>%
 kbl(caption = "Tình trạng Kinh doanh của HAG dựa trên LNST (Dữ liệu thô)",
 col.names = c("Năm", "Lợi nhuận sau thuế của công ty mẹ", "Tình trạng KD"),
 row.names = FALSE) %>%
 kable_classic(full_width = FALSE, html_font = "Times New Roman")
Tình trạng Kinh doanh của HAG dựa trên LNST (Dữ liệu thô)
Năm Lợi nhuận sau thuế của công ty mẹ Tình trạng KD
2015 502343207 Có lãi
2016 -1136650486 Lỗ
2017 69588012 Có lãi
2018 117506769 Có lãi
2019 216517715 Có lãi
2020 -1255661344 Lỗ
2021 203030161 Có lãi
2022 1128745396 Có lãi
2023 1663970953 Có lãi
2024 1013433083 Có lãi

Tình trạng kinh doanh của HAG, được đo lường bằng Lợi nhuận sau thuế của công ty mẹ, phản ánh quá trình tái cấu trúc đầy khó khăn nhưng cuối cùng đã thành công của doanh nghiệp.Từ 2015-2020 có 2 năm Lỗ đó là năm 2016 và 2020 còn lại đều Có lãi. Giai đoạn 2015–2020 thể hiện sự bất ổn sâu sắc; công ty liên tục xen kẽ giữa trạng thái thua lỗ lớn (đặc biệt năm 2016 và 2020 với mức lỗ vượt 1,1 nghìn tỷ VND) và có lãi rất khiêm tốn. Tình trạng này khẳng định rằng mặc dù hoạt động cốt lõi có lúc tạo ra lợi nhuận gộp, nhưng toàn bộ kết quả đã bị Chi phí lãi vay khổng lồ hấp thụ, khiến lợi nhuận ròng bị xói mòn và xác nhận rủi ro tài chính cao là rào cản chính. Tuy nhiên, từ năm 2021, HAG đã đạt được bước ngoặt quan trọng, chuyển sang trạng thái có lãi liên tục và tăng trưởng bền vững. LNST tăng trưởng mạnh mẽ, đạt đỉnh gần 1,7 nghìn tỷ VND vào năm 2023, cho thấy thành công trong việc tái cơ cấu nợ và chuyển đổi mô hình kinh doanh. Sự giảm áp lực lãi vay đã giải phóng lợi nhuận gộp từ mảng nông nghiệp cốt lõi, củng cố sức khỏe tài chính và chứng minh hiệu quả sinh lời bền vững hơn của công ty trong những năm gần đây.

Lọc ra những năm kinh doanh có lãi

profitable_years <- bao_cao_kqkd_Tinh_trang_KD %>% filter(Tinh_trang_KD == "Có lãi")
print(profitable_years)
## # A tibble: 8 × 24
##     Nam Doanh_thu_ban_hang_va_cu…¹ cac_khoan_giam_tru_d…² doanh_thu_thuan_ve_b…³
##   <dbl>                      <dbl>                  <dbl>                  <dbl>
## 1  2015                 6252482061                -35528              6252446533
## 2  2017                 4841225074             -33280844.             4841225074
## 3  2018                 5197982826             -33280844.             5388200400
## 4  2019                 2091833174             -16389150              2075444024
## 5  2021                 2187415636             -89997270              2097418366
## 6  2022                 5197982826             -87200939              5110781887
## 7  2023                 6492569736             -50172537              6442397199
## 8  2024                 5894531814            -111481812              5783050002
## # ℹ abbreviated names: ¹​Doanh_thu_ban_hang_va_cung_cap_dich_vu,
## #   ²​cac_khoan_giam_tru_doanh_thu,
## #   ³​doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
## # ℹ 20 more variables: gia_von_hang_ban_va_dich_vu_cung_cap <dbl>,
## #   loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu <dbl>,
## #   doanh_thu_hoat_dong_tai_chinh <dbl>, chi_phi_tai_chinh <dbl>,
## #   trong_do_chi_phi_lai_vay <dbl>, phan_lai_trong_cong_ty_lien_ket <dbl>, …

3. Thông kê cơ bản

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

thong_ke_mo_ta <- function(data, var_name){
  x <- data[[var_name]]
  result <- data.frame(
    Bien = var_name,
    Min = min(x, na.rm = TRUE),
    Q1 = quantile(x, 0.25, na.rm = TRUE),
    Median = median(x, na.rm = TRUE),
    Mean = mean(x, na.rm = TRUE),
    Q3 = quantile(x, 0.75, na.rm = TRUE),
    Max = max(x, na.rm = TRUE),
    Sd = sd(x, na.rm = TRUE),
    Var = var(x, na.rm = TRUE),
    Skewness = skewness(x, na.rm = TRUE),
    Kurtosis = kurtosis(x, na.rm = TRUE)
  )
  return(result)
}
cac_bien <- setdiff(names(bao_cao_kqkd_subset_sach_na), "Nam")

bang_thong_ke_day_du <- do.call(
  rbind,
  lapply(cac_bien, function(v) thong_ke_mo_ta(bao_cao_kqkd_subset_sach_na, v))
)


# In bảng đẹp, không rowname
bang_thong_ke_day_du %>%
  kbl(caption = "Bảng Thống Kê Mô Tả Các Biến KQKD của HAG", row.names = FALSE) %>%
  kable_classic(full_width = FALSE, html_font = "Times New Roman")
Bảng Thống Kê Mô Tả Các Biến KQKD của HAG
Bien Min Q1 Median Mean Q3 Max Sd Var Skewness Kurtosis
Doanh_thu_ban_hang_va_cung_cap_dich_vu 2091833174 3.602780e+09 5197982826 4778701701.4 6162994499 6492569736 1.697429e+09 2.881266e+18 -0.5406407 -1.4801831
cac_khoan_giam_tru_doanh_thu -111481812 -7.794384e+07 -33280844 -43640756.6 -14086485 -35528 3.981432e+07 1.585180e+15 -0.4521323 -1.5010003
doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu 2075444024 3.592791e+09 5249491144 4760738870.9 6135097400 6442397199 1.707943e+09 2.917068e+18 -0.5632556 -1.4632534
gia_von_hang_ban_va_dich_vu_cung_cap -5430638742 -4.282861e+09 -3360410005 -3505832053.9 -2981560516 -1590448139 1.269370e+09 1.611301e+18 -0.0214267 -1.3488731
loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu 205730343 6.325128e+08 1233428249 1254906817.0 1823704991 2374705174 7.792549e+08 6.072383e+17 -0.0365992 -1.6164684
doanh_thu_hoat_dong_tai_chinh 280428437 6.358340e+08 1000786308 1062120059.4 1375440301 2137143442 5.760828e+08 3.318714e+17 0.3842611 -1.1451245
chi_phi_tai_chinh -1963934151 -1.692079e+09 -1483654365 -1279198249.1 -1118636680 215432853 6.463052e+08 4.177104e+17 1.0960691 0.1211715
trong_do_chi_phi_lai_vay -1585315746 -1.465539e+09 -1166140953 -1050382818.4 -837852275 270599417 5.593007e+08 3.128173e+17 1.1022128 0.3519954
phan_lai_trong_cong_ty_lien_ket -18433513 -4.839361e+06 3434652 6752914.9 10974578 64840488 2.271434e+07 5.159414e+14 1.4674739 1.4782446
chi_phi_ban_hang -396487002 -2.954072e+08 -222192512 -231297493.0 -150230945 -111239060 9.849922e+07 9.702097e+15 -0.3257026 -1.5086149
chi_phi_quan_ly_doanh_nghiep -1851240106 -6.988116e+08 -425967966 -386887348.5 -157646627 1349894514 8.259920e+08 6.822629e+17 0.3588782 -0.0030054
lo_thuan_tu_hoat_dong_kinh_doanh -2022124320 -2.346216e+08 885249748 423709570.3 1178959094 1690412815 1.127420e+09 1.271077e+18 -0.8760811 -0.4702172
thu_nhap_khac 21546363 3.762621e+07 88909719 121902298.0 179523568 281127775 9.794905e+07 9.594016e+15 0.4893865 -1.5195169
chi_phi_khac -1380140330 -8.527156e+08 -521404910 -612196782.4 -262012765 -116111269 4.354851e+08 1.896473e+17 -0.4946090 -1.3198340
lo_khac -1337563204 -7.785596e+08 -364405886 -490294484.4 -215137882 102463888 4.537586e+08 2.058969e+17 -0.5496491 -1.1387903
chi_phi_thue_tndn_hien_hanh -153548976 -2.117953e+07 -3545265 -23375871.8 -2439711 -885768 4.694315e+07 2.203659e+15 -2.0762534 2.9380307
chi_phi_thu_nhap_thue_tndn_hoan_lai -86187524 -3.727448e+07 -4342050 27897825.1 83741605 259098512 1.024098e+08 1.048777e+16 0.9706841 -0.1265296
lo_sau_thue_tndn -2383339850 -1.125529e+09 249606713 -62062960.8 945681602 1781685785 1.384531e+09 1.916927e+18 -0.4179883 -1.4213663
loloi_nhuan_sau_thue_cua_cong_ty_me -1255661344 8.156770e+07 209773938 252282346.6 885660614 1663970953 9.226296e+08 8.512454e+17 -0.2767884 -1.0962698
lo_sau_thue_cua_co_dong_khong_kiem_soat -2025322017 -3.024177e+08 -39747303 -314345307.4 86685488 302019303 7.191704e+08 5.172061e+17 -1.3609145 0.4569408
lo_lai_co_ban_tren_co_phieu_vnd -1439 9.175000e+01 226 248.7 901 1794 1.026299e+03 1.053289e+06 -0.3575602 -1.0508204
lo_lai_suy_giam_tren_co_phieu_vnd -1439 9.175000e+01 226 248.7 901 1794 1.026299e+03 1.053289e+06 -0.3575602 -1.0508204

3.2 Tạo thêm biến

3.2.1 Tỷ suất lợi nhuận gộp (Gross Profit Margin)

bao_cao_kqkd_subset_sach_na <- bao_cao_kqkd_subset_sach_na %>%
  mutate(Ty_suat_LNG = (loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu
 / Doanh_thu_ban_hang_va_cung_cap_dich_vu
) * 100)

3.2.2 Tỷ suất lợi nhuận ròng (Net Profit Margin)

bao_cao_kqkd_subset_sach_na <- bao_cao_kqkd_subset_sach_na %>%
  mutate(Ty_suat_LNST = (loloi_nhuan_sau_thue_cua_cong_ty_me
 / Doanh_thu_ban_hang_va_cung_cap_dich_vu) * 100)

3.2.3 Tổng chi phí hoạt động

bao_cao_kqkd_subset_sach_na <- bao_cao_kqkd_subset_sach_na %>%
  mutate(Tong_chi_phi_HD = gia_von_hang_ban_va_dich_vu_cung_cap
 + chi_phi_ban_hang + chi_phi_quan_ly_doanh_nghiep)

3.3. Phân tích Kết quả Kinh doanh của HAG

3.3.1 Tốc độ tăng trưởng doanh thu theo năm

bang_tang_truong_doanh_thu <- bao_cao_kqkd_subset_sach_na %>%
  mutate(
    Tang_truong_DT = (doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu - lag(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu)) / lag(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100
  ) %>%
  select(Nam, doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, Tang_truong_DT)
print(bang_tang_truong_doanh_thu)
## # A tibble: 10 × 3
##      Nam doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu Tang_truong_DT
##    <dbl>                                           <dbl>          <dbl>
##  1  2015                                      6252446533          NA   
##  2  2016                                      6439779268           3.00
##  3  2017                                      4841225074         -24.8 
##  4  2018                                      5388200400          11.3 
##  5  2019                                      2075444024         -61.5 
##  6  2020                                      3176645956          53.1 
##  7  2021                                      2097418366         -34.0 
##  8  2022                                      5110781887         144.  
##  9  2023                                      6442397199          26.1 
## 10  2024                                      5783050002         -10.2

Kết quả phân tích tốc độ tăng trưởng doanh thu của HAG cho thấy xu hướng biến động mạnh, phản ánh rõ đặc trưng của quá trình tái cấu trúc và chuyển đổi mô hình kinh doanh của doanh nghiệp. Giai đoạn 2016–2021, doanh thu ghi nhận sự suy giảm đáng kể, đặc biệt năm 2017 và 2019, khi doanh thu giảm lần lượt khoảng 25% và hơn 61%. Nguyên nhân chủ yếu đến từ việc HAG thu hẹp mảng bất động sản và cao su, đồng thời chịu áp lực lớn từ gánh nặng nợ vay và xử lý tài sản dở dang. Năm 2020 đánh dấu sự phục hồi, với doanh thu tăng trên 53%, nhờ mảng nông nghiệp – đặc biệt là sản xuất và xuất khẩu chuối – bắt đầu tạo dòng tiền ổn định. Tuy nhiên, ngay năm sau, doanh thu lại giảm do ảnh hưởng từ đại dịch COVID-19 gây gián đoạn logistics và tiêu thụ nông sản.

3.3.2 Tốc độ tăng trưởng của giá vốn hàng bán

# 1. Tính toán Tốc độ tăng trưởng Giá vốn hàng bán (COGS Growth Rate)
bang_tang_truong_gia_von <- bao_cao_kqkd_subset_sach_na %>%
  mutate(
    gia_von_hang_ban = gia_von_hang_ban_va_dich_vu_cung_cap,
    # Tính Tốc độ tăng trưởng Giá vốn (%)
    Tang_truong_gia_von_pct = (gia_von_hang_ban - lag(gia_von_hang_ban)) / lag(gia_von_hang_ban) * 100
 ) %>%
 select(Nam, gia_von_hang_ban, Tang_truong_gia_von_pct)
print(bang_tang_truong_gia_von)
## # A tibble: 10 × 3
##      Nam gia_von_hang_ban Tang_truong_gia_von_pct
##    <dbl>            <dbl>                   <dbl>
##  1  2015      -4398020571                   NA   
##  2  2016      -5430638742                   23.5 
##  3  2017      -3109682997                  -42.7 
##  4  2018      -3013495226                   -3.09
##  5  2019      -1847659651                  -38.7 
##  6  2020      -2970915613                   60.8 
##  7  2021      -1590448139                  -46.5 
##  8  2022      -3937380869                  148.  
##  9  2023      -5148941719                   30.8 
## 10  2024      -3611137012                  -29.9

Tốc độ tăng trưởng Giá vốn hàng bán của HAG từ năm 2016 đến 2024 thể hiện một sự biến động cực kỳ mạnh mẽ và bất thường, phản ánh những thay đổi căn bản trong quy mô và cơ cấu hoạt động của doanh nghiệp. Giai đoạn 2017-2019 chứng kiến sự sụt giảm sâu liên tiếp của Giá vốn hàng bán, đặc biệt là mức giảm -42.74% năm 2017 và -38.69% năm 2019. Điều này cho thấy HAG đã trải qua một quá trình thu hẹp quy mô hoạt động hoặc tái cấu trúc mạnh mẽ để cắt giảm chi phí đầu vào. Tuy nhiên, xu hướng này bị đảo ngược hoàn toàn trong những năm sau đó. Năm 2022 ghi nhận mức tăng trưởng Giá vốn hàng bán kỷ lục 147.56%, mức cao nhất trong toàn bộ giai đoạn, tiếp nối là mức tăng đáng kể 30.77% vào năm 2023. Sự tăng trưởng đột biến này có thể là hệ quả trực tiếp của việc mở rộng hoạt động sản xuất kinh doanh trọng điểm trở lại, đặc biệt là trong các mảng nông nghiệp và chăn nuôi, hoặc do áp lực tăng giá nguyên vật liệu trên thị trường. Sự biến động dữ dội này – chuyển từ giảm sâu sang tăng kỷ lục rồi lại giảm mạnh -29.87% vào năm 2024 – nhấn mạnh tính bất ổn cao trong quản lý chi phí và quy mô sản xuất của HAG, đồng thời cho thấy công ty luôn trong trạng thái tái cơ cấu và điều chỉnh chiến lược kinh doanh theo từng giai đoạn.

3.3.3. Tính Biên lợi nhuận gộp

bang_bien_loi_nhuan_gop <- bao_cao_kqkd_subset_sach_na %>%
 mutate(
 doanh_thu_thuan = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu,
 loi_nhuan_gop = loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu,
 # Tính Biên lợi nhuận gộp (%)
 bien_loi_nhuan_gop_pct = (loi_nhuan_gop / doanh_thu_thuan) * 100
 ) %>%
 select(Nam, doanh_thu_thuan, loi_nhuan_gop, bien_loi_nhuan_gop_pct)
print(bang_bien_loi_nhuan_gop)
## # A tibble: 10 × 4
##      Nam doanh_thu_thuan loi_nhuan_gop bien_loi_nhuan_gop_pct
##    <dbl>           <dbl>         <dbl>                  <dbl>
##  1  2015      6252446533    1854425962                  29.7 
##  2  2016      6439779268    1009140526                  15.7 
##  3  2017      4841225074    1731542077                  35.8 
##  4  2018      5388200400    2374705174                  44.1 
##  5  2019      2075444024     227784373                  11.0 
##  6  2020      3176645956     205730343                   6.48
##  7  2021      2097418366     506970227                  24.2 
##  8  2022      5110781887    1173401018                  23.0 
##  9  2023      6442397199    1293455480                  20.1 
## 10  2024      5783050002    2171912990                  37.6

Sự biến động của Biên lợi nhuận gộp HAG là một thước đo rõ ràng về sự chuyển đổi từ một công ty đa ngành sang một công ty nông nghiệp tập trung. Mức thấp trong giai đoạn 2019-2020 cho thấy khó khăn ban đầu khi chuyển đổi mô hình và chịu áp lực chi phí. Tuy nhiên, việc GPM duy trì ổn định trên 20% từ năm 2021 và đạt mức cao 37.56% vào năm 2024 xác nhận rằng hiệu suất hoạt động cốt lõi của HAG đã được cải thiện đáng kể và công ty đang quản lý tốt hơn mối quan hệ giữa Giá vốn và Doanh thu.

3.3.4 Tính tỷ lệ lãi vay

bang_ty_le_lai_vay <- bao_cao_kqkd_subset_sach_na %>%
  mutate(
    # Sử dụng giá trị tuyệt đối (abs) vì trong KQKD thường ghi nhận là số âm
    chi_phi_tai_chinh_abs = abs(chi_phi_tai_chinh),
    chi_phi_lai_vay_abs = abs(trong_do_chi_phi_lai_vay),
    # Tính Tỷ lệ Chi phí Lãi vay trên Chi phí Tài chính (%)
    ty_le_lai_vay_tren_cp_tai_chinh = (chi_phi_lai_vay_abs / chi_phi_tai_chinh_abs) * 100
  ) %>%
  select(Nam, chi_phi_tai_chinh_abs, chi_phi_lai_vay_abs, ty_le_lai_vay_tren_cp_tai_chinh)
print(bang_ty_le_lai_vay)
## # A tibble: 10 × 4
##      Nam chi_phi_tai_chinh_abs chi_phi_lai_vay_abs ty_le_lai_vay_tren_cp_tai_c…¹
##    <dbl>                 <dbl>               <dbl>                         <dbl>
##  1  2015            1203667607          1078711240                          89.6
##  2  2016            1674519826          1579381993                          94.3
##  3  2017            1697932438          1585315746                          93.4
##  4  2018            1721684164          1532928450                          89.0
##  5  2019            1963934151          1263369664                          64.3
##  6  2020            1318161483          1253570666                          95.1
##  7  2021            1090293038           971878185                          89.1
##  8  2022            1649147246           793176972                          48.1
##  9  2023             215432853           270599417                         126. 
## 10  2024             688075391           716094685                         104. 
## # ℹ abbreviated name: ¹​ty_le_lai_vay_tren_cp_tai_chinh

Chi phí lãi vay của HAG cho thấy một bức tranh rõ ràng về mức độ phụ thuộc cao vào vốn vay trong lịch sử công ty.

Giai đoạn Áp lực Cao (2018–2021): Chi phí lãi vay duy trì ở mức rất lớn, gần như chiếm toàn bộ Chi phí tài chính, khẳng định rủi ro tài chính cao. Áp lực trả lãi khổng lồ này đã bào mòn lợi nhuận gộp từ hoạt động kinh doanh (nông nghiệp) mới, thường xuyên đẩy HAG vào tình trạng lỗ thuần từ hoạt động kinh doanh.

Xu hướng Giảm và Tái cơ cấu: Gần đây, Chi phí lãi vay có xu hướng giảm, cho thấy doanh nghiệp đã đạt được những bước tiến quan trọng trong việc tái cơ cấu nợ thành công. Sự giảm thiểu này là yếu tố then chốt, giúp giảm áp lực dòng tiền và tạo cơ hội để lợi nhuận từ mảng kinh doanh cốt lõi chuyển hóa thành Lợi nhuận sau thuế.

Kết luận: Rủi ro tài chính của HAG đang có dấu hiệu giảm nhờ tái cơ cấu nợ. Tuy nhiên, do quy mô nợ cũ còn lớn, Chi phí lãi vay vẫn là một yếu tố cần theo dõi sát, bởi nó tiếp tục là rào cản chính hạn chế khả năng sinh lời bền vững và ổn định của công ty.

4. Trực quan hoá dữ liệu

library(ggplot2)
library(dplyr)
library(scales) # Dùng cho hàm comma()
library(tidyr) # Dùng cho hàm pivot_longer
library(ggrepel)

Tạo tập dữ liệu cuối cùng cho việc vẽ biểu đồ

hag_data_viz <- bao_cao_kqkd_subset_sach_na %>%
    # Thêm Tình trạng Kinh doanh và đổi tên/chọn các cột cần thiết cho việc vẽ
    mutate(
        # Tạo lại Tinh_trang_KD (từ đoạn code trước đó)
        Tinh_trang_KD = ifelse(loloi_nhuan_sau_thue_cua_cong_ty_me > 0, "Có lãi", "Lỗ"),
        # Đổi tên các cột dài sang tên ngắn gọn để dễ dàng sử dụng trong ggplot
        Doanh_thu_thuan = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu,
        Loi_nhuan_sau_thue = loloi_nhuan_sau_thue_cua_cong_ty_me,
        Loi_nhuan_gop = loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu,
        CP_Tai_chinh = chi_phi_tai_chinh,
        CP_Ban_hang = chi_phi_ban_hang,
        CP_Quan_ly_DN = chi_phi_quan_ly_doanh_nghiep
    ) %>%
    # Chỉ chọn các cột cần thiết cho việc vẽ
    select(Nam, Doanh_thu_thuan, Loi_nhuan_sau_thue, Loi_nhuan_gop, 
           CP_Tai_chinh, CP_Ban_hang, CP_Quan_ly_DN, 
           Tinh_trang_KD, Ty_suat_LNG, Ty_suat_LNST)

4.1 Đồ thị 1: Xu hướng Doanh thu thuần qua các năm (Biểu đồ Đường)

plot1 <- ggplot(hag_data_viz, aes(x = Nam, y = Doanh_thu_thuan)) +
 geom_line(color = "blue", size = 1) +
 geom_point(color = "darkblue", size = 3) +
    geom_text_repel(aes(label = comma(Doanh_thu_thuan, accuracy = 1)), 
              vjust = -1, color = "darkblue", size = 3.5) +
 labs(title = "1. Xu hướng Doanh thu thuần của HAG (2015-2024)",
x = "Năm", y = "Doanh thu thuần",
caption = "Nguồn: Dữ liệu Báo cáo KQKD") +
 theme_minimal() +
 scale_y_continuous(labels = comma) +
 scale_x_continuous(breaks = hag_data_viz$Nam) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"))

print(plot1)

4.2 Đồ thị 2: Xu hướng Lợi nhuận sau thuế qua các năm

plot2 <- ggplot(hag_data_viz, aes(x = Nam, y = Loi_nhuan_sau_thue)) +
 geom_col(aes(fill = Tinh_trang_KD), width = 0.7) +
 geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) +
    geom_text(aes(label = comma(Loi_nhuan_sau_thue, accuracy = 1), 
                  vjust = ifelse(Loi_nhuan_sau_thue >= 0, -0.5, 1.5)), 
              color = "black", size = 3.5) +
 labs(title = "2. Biến động Lợi nhuận sau thuế của HAG (2015-2024)",
x = "Năm", y = "Lợi nhuận sau thuế",
fill = "Tình trạng") +
 theme_light() +
 scale_fill_manual(values = c("Có lãi" = "seagreen", "Lỗ" = "tomato")) +
 scale_y_continuous(labels = comma) +
 scale_x_continuous(breaks = hag_data_viz$Nam) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"))

print(plot2)

4.3 Đồ thị 3: So sánh Doanh thu thuần và Lợi nhuận gộp

data_dt_lng_viz <- hag_data_viz %>%
    select(Nam, Doanh_thu_thuan, Loi_nhuan_gop) %>%
    pivot_longer(-Nam, names_to = "Chi_tieu", values_to = "Gia_tri")

plot3 <- ggplot(data_dt_lng_viz, aes(x = Nam, y = Gia_tri, color = Chi_tieu)) +
 geom_line(size = 1.2) +
 geom_point(size = 3) +
    geom_text_repel(aes(label = comma(Gia_tri, accuracy = 1)), size = 3.5, max.overlaps = Inf) +
 labs(title = "3. So sánh Doanh thu thuần và Lợi nhuận gộp",
x = "Năm", y = "Giá trị", color = "Chỉ tiêu") +
 theme_bw() +
 scale_color_manual(values = c("Doanh_thu_thuan" = "navy", "Loi_nhuan_gop" = "orange"),
                     labels = c("Doanh_thu_thuan" = "Doanh thu thuần", "Loi_nhuan_gop" = "Lợi nhuận gộp")) +
 scale_y_continuous(labels = comma) +
 scale_x_continuous(breaks = hag_data_viz$Nam) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"),
 legend.position = "bottom")

print(plot3)

4.4 Đồ thị 4: Biểu đồ các loại chi phí chính (Cơ cấu chi phí)

# Chuẩn bị dữ liệu cho biểu đồ 4: pivot_longer và lấy abs()
data_cp_viz <- hag_data_viz %>%
  select(Nam, CP_Tai_chinh, CP_Ban_hang, CP_Quan_ly_DN) %>%
  pivot_longer(-Nam, names_to = "Loai_chi_phi", values_to = "So_tien") %>%
  mutate(
    So_tien = abs(So_tien), # Lấy giá trị tuyệt đối của Chi phí
    Loai_chi_phi = case_when(
      Loai_chi_phi == "CP_Tai_chinh" ~ "Chi phí Tài chính",
      Loai_chi_phi == "CP_Ban_hang" ~ "Chi phí Bán hàng",
      Loai_chi_phi == "CP_Quan_ly_DN" ~ "Chi phí Quản lý DN"
    )
  )
plot4 <- ggplot(data_cp_viz, aes(x = Nam, y = So_tien, fill = Loai_chi_phi)) +
  geom_area(alpha = 0.7) +
  labs(title = "Cơ cấu chi phí hoạt động qua các năm (Giá trị tuyệt đối)",
       x = "Năm", y = "Chi phí (Tỷ đồng)", fill = "Loại chi phí") +
  theme_classic() +
  scale_fill_brewer(palette = "Set1") + # Đổi palette thành Set1 để tương phản hơn
  scale_y_continuous(labels = comma) +
  scale_x_continuous(breaks = hag_data_viz$Nam) +
  facet_wrap(~Loai_chi_phi, scales = "free_y") + # Layer 5: Tách đồ thị theo loại chi phí
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "bottom")

print(plot4)

4.5 Đồ thị 5: Tỷ suất Lợi nhuận gộp và Lợi nhuận ròng (%)

data_ty_suat_viz <- hag_data_viz %>%
 select(Nam, Ty_suat_LNG, Ty_suat_LNST) %>%
 pivot_longer(-Nam, names_to = "Ty_suat", values_to = "Gia_tri")

plot5 <- ggplot(data_ty_suat_viz, aes(x = Nam, y = Gia_tri, color = Ty_suat)) +
 geom_line(size = 1) +
 geom_point(size = 2.5) +
 geom_hline(yintercept = 0, linetype = "dotted") +
    geom_text_repel(aes(label = label_percent(scale = 1, accuracy = 0.1)(Gia_tri)), 
                    size = 3.5, max.overlaps = Inf) +
 labs(title = "5. Tỷ suất Lợi nhuận gộp (GPM) và Lợi nhuận ròng (NPM) (%)",
x = "Năm", y = "Tỷ lệ (%)", color = "Loại tỷ suất") +
 theme_minimal() +
 scale_y_continuous(labels = label_percent(scale = 1)) + 
 scale_x_continuous(breaks = hag_data_viz$Nam) +
 scale_color_manual(values = c("Ty_suat_LNG" = "forestgreen", "Ty_suat_LNST" = "darkred"),
labels = c("Ty_suat_LNG" = "Tỷ suất Lợi nhuận gộp (GPM)",
 "Ty_suat_LNST" = "Tỷ suất Lợi nhuận ròng (NPM)")) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"),
 legend.position = "bottom")
print(plot5)

4.6 Đồ thị 6: Tương quan giữa Doanh thu và LNST

plot6 <- ggplot(hag_data_viz, aes(x = Doanh_thu_thuan, y = Loi_nhuan_sau_thue)) +
 geom_point(aes(color = Tinh_trang_KD), size = 4, alpha = 0.8) +
 geom_smooth(method = "lm", se = FALSE, color = "black", linetype="dashed") +
 geom_text_repel(aes(label = Nam), size = 3.5) + 
 labs(title = "6. Mối quan hệ giữa Doanh thu thuần và Lợi nhuận sau thuế",
x = "Doanh thu thuần", y = "Lợi nhuận sau thuế",
color = "Tình trạng KD") +
 scale_y_continuous(labels = comma) +
 scale_x_continuous(labels = comma) +
 scale_color_manual(values = c("Có lãi" = "seagreen", "Lỗ" = "tomato")) +
 theme_gray() +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"),
 legend.position = "bottom")

print(plot6)
## `geom_smooth()` using formula = 'y ~ x'