LỜI CẢM ƠN

Em xin bày tỏ lòng cảm ơn chânh thành nhất đến Thầy Trần Mạnh Tường, người đã tận tâm đồng hành, định hướng và giải đáp mọi thắc mắc cho em trong suốt quá trình thực hiện bài tiểu luận này. Sự chỉ dẫn và những góp ý của Thầy là nền tảng quý báu giúp em hoàn thiện công trình này.

Bên cạnh đó, mặc dù em đã rất nỗ lực cố gắng để hoàn thành tốt nhất bài tiểu luận này, nhưng do kiến thức, kinh nghiệm và trải nghiệm còn hạn chế nên chắc chắn sẽ không thể tránh khỏi những sai sót trong quá trình thực hiện báo cáo. Kính mong nhận được những nhận xét, góp ý của các Thầy để em có thể rút kinh nghiệm cho các bài tiểu luận lần sau được tốt hơn.

LỜI CAM KẾT

Em xin cam đoan bài tiểu luận này là thành quả nghiên cứu của riêng mình, được hình thành và hoàn thiện dưới sự chỉ dẫn tận tâm của Thầy Trần Mạnh Tường.

Từng lập luận và phân tích trong bài là kết quả của quá trình tư duy độc lập, có tham khảo các nguồn học liệu và số liệu uy tín. Các kết quả nghiên cứu trong tiểu luận này là trung thực và chưa từng được sử dụng trong bài tiểu luận nào khác, cũng như không sao chép từ bất cứ nguồn nào.

CHƯƠNG 1: PHÂN TÍCH HOẠT ĐỘNG KINH DOANH CỦA MỘT CHUỖI CỬA HÀNG 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)
library(tidyr)
library(zoo)
library(lubridate)
library(knitr)
library(kableExtra)
library(ggplot2)
library(scales)
library(ggridges)
library(treemapify)
library(ggalluvial)
library(fmsb)
library(corrplot)
library(ggrepel)
library(e1071)
library(moments)

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

  • readr: Cung cấp các hàm để đọc và ghi dữ liệu dạng văn bản (như file .csv, .tsv) một cách nhanh chóng và hiệu quả, tối ưu hơn so với các hàm cơ bản của R.

  • dplyr: Là thư viện nền tảng cho việc thao tác dữ liệu theo triết lý “ngữ pháp của thao tác dữ liệu”. Nó cung cấp một bộ các hàm (động từ) nhất quán và hiệu suất cao như mutate(), select(), filter(), summarise(), và group_by() để thực hiện các tác vụ biến đổi và tóm tắt dữ liệu một cách logic.

  • tidyr: Chuyên về việc dọn dẹp và tái cấu trúc bố cục dữ liệu. Các hàm chính như pivot_longer() và pivot_wider() giúp chuyển đổi linh hoạt giữa định dạng dữ liệu dạng rộng (wide) và dạng dài (long), một bước tiền xử lý quan trọng cho nhiều loại phân tích và trực quan hóa.

  • lubridate: Giúp việc xử lý các đối tượng ngày tháng và thời gian (date/time) trở nên đơn giản và nhất quán. Thư viện này cho phép dễ dàng trích xuất các thành phần như năm, tháng, ngày, thứ, hoặc thực hiện các phép tính khoảng thời gian.

  • zoo: Là một thư viện mạnh mẽ cho việc làm việc với dữ liệu chuỗi thời gian. Trong bài tiểu luận, thư viện này được sử dụng cho hàm rollmean() để tính trung bình động, một kỹ thuật hữu ích để làm mượt và nhận diện xu hướng dài hạn của dữ liệu.

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 để tạo ra các báo cáo động từ R Markdown. Nó cho phép kết hợp mã R, kết quả thực thi (văn bản, bảng biểu, biểu đồ) và văn bản tường thuật vào một tài liệu duy nhất, sau đó biên dịch (knit) thành các định dạng đầu ra như PDF hoặc HTML.

  • kableExtra: Mở rộng khả năng của hàm knitr::kable(), cung cấp các công cụ để tạo ra các bảng biểu có tính thẩm mỹ cao và chuyên nghiệp. Nó cho phép tùy chỉnh định dạng nâng cao như tạo sọc, nhóm hàng/cột, định dạng có điều kiện và thêm thanh cuộn cho bảng lớn.

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

  • ggplot2: Là thư viện trực quan hóa dữ liệu mạnh mẽ và phổ biến nhất trong R, được xây dựng dựa trên triết lý “Ngữ pháp của Đồ thị”. Nó cho phép người dùng xây dựng các biểu đồ phức tạp từng lớp một cách linh hoạt.

  • scales: Cung cấp các công cụ để kiểm soát việc ánh xạ dữ liệu sang các thuộc tính thẩm mỹ. Thường được dùng cùng ggplot2 để định dạng lại các nhãn trên trục và chú giải (ví dụ: định dạng tiền tệ $, phần trăm %, hay ngày tháng).

  • corrplot: Chuyên dùng để trực quan hóa ma trận tương quan. Nó cung cấp nhiều phương pháp hiển thị hệ số tương quan (dưới dạng số, màu sắc, hình tròn,…) giúp dễ dàng nhận diện mối quan hệ giữa các biến số.

  • 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”, 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).

  • ggrepel: Là một phần mở rộng của ggplot2, cung cấp các hàm geom_text_repel() và geom_label_repel() để tự động điều chỉnh vị trí của các nhãn văn bản trên biểu đồ, tránh cho chúng bị chồng chéo lên nhau.

Nhóm 4: Thống kê

  • moments/e1071: Cả hai thư viện đều cung cấp các hàm để tính toán các chỉ số thống kê mô tả nâng cao, đặc biệt là các moments của một phân phối, như độ xiên (Skewness) và độ nhọn (Kurtosis), giúp đánh giá hình dạng phân phối của dữ liệu.

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

BK <- read_csv("D:/bike_sales_100k.csv")

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ên BK để tiện thao tác trong quá trình xử lý.

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

head(BK, 5) %>%
kable("latex", booktabs = TRUE, caption = "5 dòng đầu tiên của bộ dữ liệu") %>%
kable_styling(latex_options = c("hold_position", "striped", "scale_down"), position = "center",font_size = 9)

Giải thích code:

  • (1): Lệnh này có nghĩa là “Lấy ra 5 dòng đầu tiên của bộ dữ liệu có tên là BK”.

  • (2): Dòng này nhận 5 dòng dữ liệu từ bước trên và biến chúng thành một cái bảng cơ bản với tiêu đề là “10 dòng đầu tiên của bộ dữ liệu”.

  • (3): Dòng cuối cùng này có nhiệm vụ trang trí cho cái bảng đã tạo ở trên cho đẹp hơn bằng cách thêm hiệu ứng sọc, căn lề, và tự động điều chỉnh kích thước cho vừa trang giấy.

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

Giải thích code:

  • 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.

  • ncol(BK): Lệnh này có một nhiệm vụ duy nhất: Đếm xem có bao nhiêu cột trong bảng dữ liệu “BK” và trả về một con số.

  • nrow(BK): Lệnh này có nhiệm vụ là đếm xem có bao nhiêu hàng (dòng) trong bảng dữ liệu “BK” và trả về một con số.

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

  • Tạo bảng Tóm tắt:
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."))
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"),format = "latex",booktabs = TRUE) %>% kable_styling( latex_options = "striped", full_width = F, position = "center")

Giải thích code:

  • (1): Dòng này có nhiệm vụ tạo ra một bảng tóm tắt hoàn toàn mới tên là data_summary_auto để mô tả dữ liệu gốc BK. Bảng này gồm 3 cột:Ten_Cot, Loai_Du_Lieu, Giai_thich.

  • (2): Lấy bảng trên đặt lại tên các cột tiêu đề (col.names) và thêm tiêu đề chung cho bảng sau đó trang trí lại bảng cho dễ đọc.

  • Đếm số biến định lượng:

so_bien_dinh_luong <- data_summary_auto %>%      # Bắt đầu với bảng 'data_summary_auto'.
filter(Loai_Du_lieu == "numeric") %>%          # Dùng filter() để chỉ giữ lại các dòng có Loai_Du_lieu là "numeric".
nrow()                                         # Dùng nrow() để đếm số dòng còn lại.
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.

  • Đếm số biến định tính:
so_bien_dinh_tinh <- data_summary_auto %>% 
filter(Loai_Du_lieu == "character") %>%      # Dùng filter() để chỉ giữ lại các dòng có Loai_Du_lieu là "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(paste("Tổng số giá trị bị thiếu (NA):", sum(is.na(BK)), "\n"))
## Tổng số giá trị bị thiếu (NA): 0
cat(paste("Tổ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 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

Giải thích code:

  • (1), (2): Hai dòng này có nhiệm vụ rà soát toàn bộ cột Price và Quantity để đếm xem có bao nhiêu giá trị bằng 0.

Nhận xét:

Kết quả kiểm tra cho thấy không có bất kỳ giao dịch nào trong bộ dữ liệu có giá bán (Price) hoặc số lượng (Quantity) bằng 0. Đây là một dấu hiệu rất tích cực về chất lượng dữ liệu.

  • Về mặt logic kinh doanh: Mỗi giao dịch được ghi nhận đều là một giao dịch hợp lệ, có giá trị và có ít nhất một sản phẩm được bán ra.

  • Về mặt kỹ thuật: Dữ liệu sạch ở điểm này, giúp đảm bảo rằng các phép tính tài chính sau này, đặc biệt là tính toán doanh thu (Revenue = Price * Quantity), sẽ chính xác và không bị sai lệch bởi các giá trị zero bất thường.

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.

Nhận xét:

  • Đố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 đến 5,000 USD, 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 đến 5 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 đến 70, 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(Bike_Model, sort = TRUE) %>% kable(caption = "Số lượng giao dịch tại mỗi cửa hàng", col.names = c ("Mẫu xe", "Số lượng giao dịch"))
Số lượng giao dịch tại mỗi cửa hàng
Mẫu xe Số lượng giao dịch
BMX 14377
Road Bike 14363
Cruiser 14332
Folding Bike 14329
Hybrid Bike 14319
Electric Bike 14169
Mountain Bike 14111

Giải thích code: Dòng code này có nhiệm vụ đếm xem mỗi loại xe đạp (Bike_Model) xuất hiện bao nhiêu lần trong bộ dữ liệu, sau đó sắp xếp kết quả từ cao xuống thấp và cuối cùng là hiển thị kết quả dưới dạng một cái bảng gọn gàng có tiêu đề.

Kết quả: Mẫu xe “BMX” có số lượng giao dịch cao nhất với 14377 lượt, theo sau sát sao là “Road Bike” với 14363 lượt. Trong khi đó, “Mountain Bike” là mẫu xe có số lượng giao dịch thấp nhất, với 14111 lượt. Khoảng cách giữa mẫu xe bán chạy nhất và mẫu xe bán ít nhất không đáng kể. Điều này cho thấy danh mục sản phẩm của cửa hàng rất cân bằng, không có mẫu xe nào chiếm ưu thế vượt trội hay bị bỏ lại phía sau. Tất cả các dòng xe đều có sức hút tương đối đồng đều đối với khách hàng.

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"
class(BK$Date)
## [1] "Date"

Giải thích code:

  • (1): Chuyển đổi cột Date từ dạng chữ viết sang định dạng ngày tháng chuẩn.

  • (2): Hiển thị ra vài giá trị đầu tiên của cột Date sau khi đã chuyển đổi.

  • (1): Kiểm tra và cho biết kiểu dữ liệu của cột Date bây giờ là gì.

2.2. Xử lý các vấn dề 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

Tình huống giả định: Giả sử có 800 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.

BK_dirty <- BK 
set.seed(500) # -> set.seed() đảm bảo rằng mỗi lần chạy code, sẽ luôn nhận được CÙNG MỘT KẾT QUẢ "ngẫu nhiên"
na_indices <- sample(1:nrow(BK_dirty), 800)
BK_dirty$Customer_Age[na_indices] <- NA
mean_age <- mean(BK_dirty$Customer_Age, na.rm = TRUE)
BK_dirty$Customer_Age[is.na(BK_dirty$Customer_Age)] <- round(mean_age)
sum(is.na(BK_dirty$Customer_Age))
## [1] 0

Giải thích code:

  • (2): Đảm bảo rằng dù chạy code bao nhiêu lần, 800 vị trí “ngẫu nhiên” này sẽ luôn giống hệt nhau.

  • (4): Gán giá trị NA (bị thiếu) vào 800 vị trí đã chọn.

  • (5): 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.

  • (6): Đâ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).

  • (7): Đếm lại giá trị bị thiếu.

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.

BK_outliers <- BK
BK_outliers$Price[sample(1:nrow(BK_outliers), 10)] <- c(0, 120000, 10, 50000, 20, 70, 40, 80000, 5, 150000)
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)
summary(BK_outliers$Price)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##       0    1400    2599    2602    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:

  • (2): Chọn ngẫu nhiên 10 vị trí trong cột Price và thay thế chúng bằng những con số vô lý (ví dụ: 0, 5, 120000, 150000) để giả lập lỗi nhập liệu.

  • (5): Với mỗi nhóm xe, tính giá trung vị (median) - là mức giá phổ biến, ít bị ảnh hưởng bởi outlier - và lưu vào một cột tạm tên là Median_Price.

  • (7): Đâ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ơ bản

Đâ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.

customer_frequency <- BK %>% group_by(Customer_ID) %>% summarise(Purchase_Count = n(), .groups = 'drop')
BK1 <- BK %>%
left_join(customer_frequency, by = "Customer_ID") %>% 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 <= 30 ~ "Trẻ",
      Customer_Age <= 55 ~ "Trung niên",
      TRUE               ~ "Lớn tuổi"),
    # 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 >= 15 ~ "VIP",
      Purchase_Count >= 10  ~ "Thân thiết",
      TRUE                 ~ "Khách hàng mới"),
    # 7. Mã hóa dữ liệu (Encoding) giới tính
    Gender_Encoded = case_when(
      Customer_Gender == "Male"   ~ 0,
      Customer_Gender == "Female" ~ 1))

2.4 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ể.

head_data <- head(BK1, 5)
num_cols <- ncol(head_data)
mid_point <- ceiling(num_cols / 2)
head_data[, 1:mid_point] %>% kable(caption = "Dữ liệu sau xử lý (Phần 1).", booktabs = TRUE) %>% kable_styling(latex_options = c("striped", "hold_position"), font_size = 6)
Dữ liệu sau xử lý (Phần 1).
Sale_ID Date Customer_ID Bike_Model Price Quantity Store_Location Salesperson_ID Payment_Method Customer_Age Customer_Gender
1 2022-07-11 9390 Cruiser 318.32 1 Philadelphia 589 Apple Pay 70 Female
2 2024-05-03 3374 Hybrid Bike 3093.47 4 Chicago 390 Apple Pay 37 Male
3 2022-09-01 2689 Folding Bike 4247.99 3 San Antonio 338 PayPal 59 Female
4 2022-09-28 3797 Mountain Bike 1722.01 3 San Antonio 352 Apple Pay 19 Male
5 2021-01-05 1633 BMX 3941.44 3 Philadelphia 580 PayPal 67 Female
head_data[, (mid_point + 1):num_cols] %>% kable(caption = "Dữ liệu sau xử lý (Phần 2).", booktabs = TRUE) %>% kable_styling(latex_options = c("striped", "hold_position"), font_size = 7)
Dữ liệu sau xử lý (Phần 2).
Purchase_Count Revenue Year Month Weekday Season Age_Group Price_Level Loyalty_Status Gender_Encoded
15 318.32 2022 July Monday Hạ Lớn tuổi Phổ thông VIP 1
11 12373.88 2024 May Friday Xuân Trung niên Cao cấp Thân thiết 0
7 12743.97 2022 September Thursday Thu Lớn tuổi Hạng sang Khách hàng mới 1
17 5166.03 2022 September Wednesday Thu Trẻ Phổ thông VIP 0
18 11824.32 2021 January Tuesday Đông Lớn tuổi Cao cấp VIP 1

Giải thích code:

  • (3): Chia đôi số cột và làm tròn lên để đảm bảo không mất cột nào nếu tổng số cột là số lẻ. Kết quả được lưu vào mid_point.

  • (4): Lấy nửa đầu dữ liệu đó và định dạng nó thành một bảng đẹp có tiêu đề là “Dữ liệu sau xử lý (Phần 1)”.

  • (1): Lấy nửa sau dữ liệu đó và định dạng nó thành một bảng đẹp thứ hai có tiêu đề là “Dữ liệu sau xử lý (Phần 2)”.

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, mục 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

numeric_vars <- BK %>% 
select(where(is.numeric)) %>% select(-ends_with("_ID"))    
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}")) %>%
tidyr::pivot_longer(cols = everything(), names_to = c("Variable", "Statistic"), names_sep = "__" ) %>%
tidyr::pivot_wider( names_from = Statistic, values_from = value ) %>%
mutate(across(where(is.numeric), ~round(., 2)))
kable(descriptive_table_adv, caption = "Thống kê mô tả chi tiết các biến định lượng", digits = 2, booktabs = TRUE, format = "latex") %>%
kable_styling(latex_options = c("striped", "hold_position"), full_width = FALSE, position = "center")

Giải thích code:

  • (2): Giữ lại những cột là số và loại bỏ các cột định danh (ID) vì chúng không có ý nghĩa thống kê.

  • (4): Áp dụng một loạt các hàm thống kê (mean, median, sd,…) cho tất cả các cột.

  • (16): Lệnh này thực hiện một thao tác biến đổi cấu trúc quan trọng. Hãy tưởng tượng nó tháo dỡ cái bảng rộng ban đầu và xếp lại thành một danh sách dài, mỗi dòng là một cặp (Tên biến, Chỉ số thống kê, Giá trị).

  • (20): Xoay bảng từ dạng dài về lại dạng rộng. Kết quả là một bảng mà mỗi hàng là một biến số (Price, Quantity,…) và mỗi cột là một chỉ số thống kê (Mean, Median,…).

Nhận xét:

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 ở 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” 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ực kỳ cân bằng và đối xứng.

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

qual_data <- BK %>% select(where(is.character) | where(is.factor))
for (col_name in names(qual_data)) 
{freq_table <- qual_data %>% count(!!sym(col_name), sort = TRUE) %>% mutate(Ty_le = n / sum(n) * 100) %>% mutate(Ty_le = round(Ty_le, 2)) %>% rename(Gia_tri = 1,  Tan_so = n,  "Ty_le (\\%)" = Ty_le)
table_for_pdf <- kable(freq_table, caption = paste("Bảng tần suất cho biến:", gsub("_", "\\\\_", col_name)), booktabs = TRUE) %>%
kable_styling(latex_options = c("striped", "hold_position"),  full_width = FALSE, position = "center")
print(table_for_pdf)
cat("\n\n")}
Bảng tần suất cho biến: Bike_Model
Gia_tri Tan_so Ty_le (%)
BMX 14377 14.38
Road Bike 14363 14.36
Cruiser 14332 14.33
Folding Bike 14329 14.33
Hybrid Bike 14319 14.32
Electric Bike 14169 14.17
Mountain Bike 14111 14.11
Bảng tần suất cho biến: Store_Location
Gia_tri Tan_so Ty_le (%)
New York 14515 14.52
Phoenix 14385 14.38
Philadelphia 14330 14.33
San Antonio 14300 14.30
Chicago 14207 14.21
Houston 14149 14.15
Los Angeles 14114 14.11
Bảng tần suất cho biến: Payment_Method
Gia_tri Tan_so Ty_le (%)
Apple Pay 16751 16.75
Debit Card 16738 16.74
Cash 16692 16.69
Credit Card 16653 16.65
Google Pay 16613 16.61
PayPal 16553 16.55
Bảng tần suất cho biến: Customer_Gender
Gia_tri Tan_so Ty_le (%)
Female 50227 50.23
Male 49773 49.77

Giải thích code:

  • (2): Tạo ra một vòng lặp. Với mỗi tên cột (col_name) có trong bảng qual_data, nó sẽ thực hiện toàn bộ khối lệnh nằm bên trong cặp dấu {…}.

  • (3): Dòng này có nhiệm vụ tính toán bảng tần suất cho biến hiện tại đang được xét trong vòng lặp.

Nhận xét:

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

Giải thích code: sum(BK1$Revenue): Tính tổng tất cả các giá trị trong cột Revenue của bộ dữ liệu BK1.

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

Giải thích code: mean(BK1$Revenue): Tính doanh thu trung bình cho mỗi giao dịch.

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

Giải thích code: nrow(BK1): Đếm tổng số hàng trong bộ dữ liệu BK1.

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

Giải thích code: mean(BK1$Quantity): Tính Số lượng sản phẩm trung bình mỗi giao dịch.

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 mức giá

3.3.1 Tổng doanh thu theo mức giá:

revenue_by_price_level <- BK1 %>%
  group_by(Price_Level) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_price_level, caption = "Tổng doanh thu theo mức giá")
Tổng doanh thu theo mức giá
Price_Level Total_Revenue
Cao cấp 290411858
Hạng sang 277944860
Phổ thông 210077517

Nhận xét: Doanh thu cốt lõi của công ty đến từ các sản phẩm ở mức giá Cao cấp và Hạng sang (chiếm phần lớn nhất trong tổng doanh thu).

3.3.2 Tổng số lượng bán theo mức giá:

quantity_by_price_level <- BK1 %>%
  group_by(Price_Level) %>%
  summarise(Total_Quantity = sum(Quantity)) %>%
  arrange(desc(Total_Quantity))
kable(quantity_by_price_level, caption = "Tổng số lượng bán theo mức giá")
Tổng số lượng bán theo mức giá
Price_Level Total_Quantity
Phổ thông 150097
Cao cấp 87869
Hạng sang 61745

Nhận xét:

Phân khúc “Phổ thông” chiếm ưu thế vượt trội về số lượng bán: Với 150.097 sản phẩm được bán ra, số lượng bán của phân khúc Phổ thông gấp khoảng 1.7 lần so với phân khúc Cao cấp và gấp khoảng 2.4 lần so với phân khúc Hạng sang.

Xu hướng giảm dần rõ rệt theo mức giá: Có một mối quan hệ nghịch đảo rõ ràng: càng lên phân khúc giá cao hơn (“Cao cấp”, “Hạng sang”), tổng số lượng sản phẩm bán ra càng giảm.

3.3.3 Số lượng giao dịch theo mức giá:

transactions_by_price_level <- BK1 %>%
  count(Price_Level, name = "Transaction_Count", sort = TRUE)
kable(transactions_by_price_level, caption = "Số lượng giao dịch theo mức giá")
Số lượng giao dịch theo mức giá
Price_Level Transaction_Count
Phổ thông 50030
Cao cấp 29341
Hạng sang 20629

Nhận xét:

Đây là tổng số lần các giao dịch (đơn hàng) đã xảy ra trong mỗi Price_Level. Một giao dịch có thể bao gồm việc mua nhiều sản phẩm.

Phân khúc “Phổ thông” dẫn đầu về số lượt mua hàng: Với 50.030 giao dịch, các sản phẩm ở mức giá “Phổ thông” thu hút số lượng lượt mua lớn nhất.

Xu hướng giảm dần theo mức giá: Tương tự như tổng số lượng bán, số lượng giao dịch cũng có xu hướng giảm khi mức giá sản phẩm tăng lên. Điều này là dấu hiệu cho thấy phân khúc giá cao hơn có tệp khách hàng nhỏ hơn hoặc tần suất mua sắm ít thường xuyên hơn.

3.3.4 Số lượng giao dịch theo mức giá tại mỗi cửa hàng:

    transactions_by_price_level_and_store <- BK1 %>%
      group_by(Store_Location, Price_Level) %>%
      count(name = "Transaction_Count") %>%
      arrange(Store_Location, desc(Transaction_Count))
    kable(transactions_by_price_level_and_store, caption = "Số lượng giao dịch theo mức giá tại mỗi cửa hàng")
Số lượng giao dịch theo mức giá tại mỗi cửa hàng
Store_Location Price_Level Transaction_Count
Chicago Phổ thông 7082
Chicago Cao cấp 4213
Chicago Hạng sang 2912
Houston Phổ thông 7126
Houston Cao cấp 4093
Houston Hạng sang 2930
Los Angeles Phổ thông 7075
Los Angeles Cao cấp 4157
Los Angeles Hạng sang 2882
New York Phổ thông 7254
New York Cao cấp 4277
New York Hạng sang 2984
Philadelphia Phổ thông 7105
Philadelphia Cao cấp 4189
Philadelphia Hạng sang 3036
Phoenix Phổ thông 7179
Phoenix Cao cấp 4232
Phoenix Hạng sang 2974
San Antonio Phổ thông 7209
San Antonio Cao cấp 4180
San Antonio Hạng sang 2911

Nhận xét:

Mô hình Phân bổ Giao dịch Nhất quán: Thứ tự số lượng giao dịch theo mức giá là Phổ thông > Cao cấp > Hạng sang được lặp lại chính xác tại tất cả 7 thành phố. Điều này chỉ ra rằng thói quen tiêu dùng và cơ cấu nhu cầu sản phẩm là ổn định trên khắp các thị trường địa lý này.

Phổ thông là động lực khối lượng: Phân khúc Phổ thông luôn là động lực chính tạo ra khối lượng giao dịch (Volume) tại mọi cửa hàng, với số lượng vượt trội so với hai phân khúc còn lại.

Hiệu suất cửa hàng đồng đều: Sự chênh lệch về tổng số lượng giao dịch giữa các thành phố cho cùng một mức giá là rất nhỏ. Cửa hàng New York có số lượng giao dịch cao nhất ở cả ba phân khúc, khẳng định vị thế dẫn đầu về hiệu suất giao dịch tổng thể.

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

3.4.1 Hiệu suất kinh doanh theo từng địa điểm 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 địa điểm cửa hàng")
Doanh thu theo địa điểm 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 địa điểm 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 địa điểm 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 5 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(5, Total_Revenue)
kable(top_salesperson, caption = "Top 5 nhân viên có doanh thu cao nhất")
Top 5 nhân viên có doanh thu cao nhất
Salesperson_ID Total_Revenue
794 1196096
500 1137238
605 1125912
655 1123964
914 1123278

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 nhóm tuổi hoặc giới tính của khách hàng

3.5.1 Độ tuổi trung bình 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

3.5.2 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
Trung niên 367911187 7820.244
Lớn tuổi 220315371 7741.501
Trẻ 190207676 7765.163

Nhận xét:

Mức độ đóng góp doanh thu giảm dần từ Trung niên > Thanh niên > Cao tuổi.

Nhóm Trung niên (36-55) là khách hàng cốt lõi tạo ra phần lớn doanh thu, trong khi thói quen chi tiêu trung bình trên mỗi lần mua hàng là tương đối nhất quán giữa các nhóm tuổi.

3.5.3 Số lượng giao dịch theo nhóm tuổi:

transactions_by_age_group <- BK1 %>%
  count(Age_Group, name = "Transaction_Count", sort = TRUE)
kable(transactions_by_age_group, caption = "Số lượng giao dịch theo nhóm tuổi")
Số lượng giao dịch theo nhóm tuổi
Age_Group Transaction_Count
Trung niên 47046
Lớn tuổi 28459
Trẻ 24495

3.5.4 Mẫu xe được ưa chuộng nhất theo nhóm tuổi:

model_preference_by_age_group <- BK1 %>%
  group_by(Age_Group, Bike_Model) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
  group_by(Age_Group) %>%
  top_n(1, Total_Quantity) %>%
  arrange(Age_Group)
kable(model_preference_by_age_group, caption = "Mẫu xe được ưa chuộng nhất theo nhóm tuổi")
Mẫu xe được ưa chuộng nhất theo nhóm tuổi
Age_Group Bike_Model Total_Quantity
Lớn tuổi Cruiser 12446
Trung niên Folding Bike 20635
Trẻ Cruiser 10758

Nhận xét:

Phân khúc Trung niên có nhu cầu khác biệt: Nhóm Middle-Aged (36-55) có sở thích khác biệt rõ rệt, ưa chuộng Folding Bike (Xe đạp gấp) với số lượng bán cao nhất (16,601 chiếc). Điều này có thể phản ánh nhu cầu về sự tiện lợi, khả năng mang theo và tiết kiệm không gian cho nhóm tuổi thường xuyên di chuyển hoặc đi làm.

Mẫu Cruiser được ưa chuộng bởi nhóm trẻ và cao tuổi.

Nhóm Young mua Cruiser với số lượng cao hơn đáng kể (14,941 chiếc) so với nhóm Senior (12,446 chiếc).

3.5.5 Mức giá ưa chuộng theo nhóm tuổi:

price_level_preference_by_age <- BK1 %>%
  group_by(Age_Group, Price_Level) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
  group_by(Age_Group) %>%
  top_n(1, Total_Quantity) %>%
  arrange(Age_Group)
kable(price_level_preference_by_age, caption = "Mức giá ưa chuộng theo nhóm tuổi")
Mức giá ưa chuộng theo nhóm tuổi
Age_Group Price_Level Total_Quantity
Lớn tuổi Phổ thông 42452
Trung niên Phổ thông 70529
Trẻ Phổ thông 37116

Nhận xét: Khi xét về mức giá, phân khúc Phổ thông là lựa chọn số một phổ quát, với nhóm Trung niên dẫn đầu về khối lượng mua.

3.5.6 Phân tích doanh thu theo giới tính:

revenue_by_gender <- BK1 %>%
  group_by(Customer_Gender) %>%
  summarise(
    Total_Revenue = sum(Revenue),
    Average_Purchase_Value = mean(Revenue)
    ) %>%
  arrange(desc(Total_Revenue))
kable(revenue_by_gender, caption = "Doanh thu theo giới tính")
Doanh thu theo giới tính
Customer_Gender Total_Revenue Average_Purchase_Value
Female 392314349 7810.826
Male 386119886 7757.617

Nhận xét: Mức đóng góp doanh thu và thói quen chi tiêu trung bình trên mỗi lần mua hàng của hai giới là gần như ngang nhau.

3.5.7 Số lượng giao dịch theo giới tính:

transactions_by_gender <- BK1 %>%
  count(Customer_Gender, name = "Transaction_Count", sort = TRUE)
kable(transactions_by_gender, caption = "Số lượng giao dịch theo giới tính")
Số lượng giao dịch theo giới tính
Customer_Gender Transaction_Count
Female 50227
Male 49773

Nhận xét: Phân tích này giúp chúng ta hiểu rõ hơn về tần suất giao dịch của từng nhóm giới tính. Giới tính nào có số lượng giao dịch nhiều hơn có thể là nhóm khách hàng năng động hơn.

3.5.8 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) %>%
  arrange(Customer_Gender)
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

Nhận xét: Bảng này sẽ chỉ ra những mẫu xe đạp nào được ưa chuộng nhất bởi nam giới và nữ giới. Thông tin này rất hữu ích cho việc quản lý tồn kho, marketing và phát triển sản phẩm.

3.5.9 Mức giá ưa chuộng theo giới tính:

price_level_preference_by_gender <- BK1 %>%
  group_by(Customer_Gender, Price_Level) %>%
  summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
  group_by(Customer_Gender) %>%
  top_n(1, Total_Quantity) %>%
  arrange(Customer_Gender)
kable(price_level_preference_by_gender, caption = "Mức giá ưa chuộng theo giới tính")
Mức giá ưa chuộng theo giới tính
Customer_Gender Price_Level Total_Quantity
Female Phổ thông 75240
Male Phổ thông 74857

Nhận xét: Phổ thông là phân khúc chiến lược về mặt khối lượng. Hơn nữa, sự đồng đều này cho thấy chiến lược quản lý tồn kho và tiếp thị có thể được thiết kế để phục vụ cân bằng nhu cầu mua hàng số lượng lớn của cả khách hàng Nam và Nữ trong phân khúc giá thấp hơn.

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
Khách hàng mới 2994 33.26667
Thân thiết 4628 51.42222
VIP 1378 15.31111

Giải thích code:

    1. Bắt đầu Pipeline: Khởi tạo biến loyalty_distribution và bắt đầu chuỗi thao tác trên dataframe BK1.
    1. Lọc khách hàng duy nhất: Giữ lại chỉ một lần xuất hiện (một hàng duy nhất) cho mỗi Customer_ID (Mã khách hàng). Điều này đảm bảo rằng mỗi khách hàng được tính chính xác một lần, không bị trùng lặp do mua hàng nhiều lần.
    1. Đếm Tần suất: Đếm số lượng khách hàng (n) thuộc mỗi nhóm Loyalty_Status (hạng thân thiết) sau khi đã loại bỏ các giao dịch lặp lại.
    1. Tính Tỷ lệ Phần trăm: Tạo một cột mới tên là Percentage bằng cách chia số lượng khách hàng của mỗi hạng (n) cho tổng số khách hàng duy nhất (sum(n)) và nhân với 100.

Nhận xét:

  • Sự Tập trung vào Khách hàng VIP (Platinum & Gold):

Khách hàng Platinum là phân khúc lớn nhất, chiếm 66.73% (6,006 người).

Khách hàng Gold đứng thứ hai, chiếm 31.69% (2,852 người).

Tổng cộng, hai hạng này chiếm khoảng 98.42% tổng số khách hàng thân thiết.

  • Các hạng Silver và Bronze chiếm tỷ lệ rất nhỏ: Silver chỉ chiếm 1.54% (139 người), và Bronze gần như không đáng kể (0.03%, 3 người).

  • Ý nghĩa Chiến lược: Chương trình khách hàng thân thiết đang hoạt động hiệu quả trong việc thúc đẩy khách hàng đạt và duy trì các cấp độ cao, tạo ra một cơ sở khách hàng VIP rất mạnh mẽ và tập trung.

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

total_unique_customers <- n_distinct(BK1$Customer_ID)
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
Thân thiết 425059285
VIP 177937892
Khách hàng mới 175437058

Nhận xét: Khách hàng Platinum là nguồn doanh thu cốt lõi, tạo ra 603 triệu đơn vị tiền tệ. Doanh thu của hạng Platinum lớn hơn gấp hơn 3.5 lần so với hạng Gold (171 triệu). Hai hạng thấp nhất (Silver và Bronze) đóng góp tổng cộng rất ít vào doanh thu (chỉ khoảng 400.000 đơn vị). Sự khác biệt này củng cố rằng công ty nên tập trung tài nguyên và các chương trình duy trì vào các hạng Platinum và Gold, vì họ là nguồn giá trị tài chính duy nhất có ý nghĩa.

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
VIP 7805.321
Khách hàng mới 7783.020
Thân thiết 7776.139

Nhận xét: Đánh giá hiệu quả của chương trình khách hàng thân thiết. Khách hàng hạng VIP chi tiêu nhiều hơn và phần lớn doanh thu đến từ nhóm khách hàng này.

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", col.names = c ("Năm", "Tổng doanh thu"))
Doanh thu theo năm
Năm Tổng doanh thu
2020 164224491
2021 163535245
2022 165560749
2023 164497574
2024 120616177

Giải thích code:

  • (2): Nhóm tất cả các hàng trong data frame BK1 lại với nhau dựa trên giá trị của cột Year (Năm).

  • (3): Tính tổng của cột Revenue (Doanh thu) cho mỗi nhóm (tức là cho mỗi năm) và tạo một cột mới tên là Total_Revenue để lưu kết quả này.

Nhận xét:

  • Giai đoạn 2020-2023: Hiệu suất ổn định và mạnh mẽ. Doanh thu của công ty duy trì ở mức rất cao và ổn định, dao động quanh mốc 164 triệu USD mỗi năm. Điều này cho thấy hoạt động kinh doanh rất vững chắc và có thể dự báo được trong giai đoạn này. Doanh thu của năm 2024 (120.6 triệu USD) thấp hơn so với mức trung bình hàng năm (~164 triệu USD)

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", col.names = c("Mùa", "Tổng doanh thu"))
Doanh thu theo mùa
Mùa Tổng doanh thu
Hạ 207337508
Xuân 207312828
Đông 188748226
Thu 175035674

Giải thích code:

  • (2): Nhóm tất cả các hàng trong data frame BK1 lại với nhau dựa trên giá trị của cột Season (Mùa).

  • (3): Tính tổng của cột Revenue (Doanh thu) cho mỗi nhóm (tức là cho mỗi mùa) và tạo một cột mới tên là Total_Revenue để lưu kết quả này.

Nhận xét:

  • Mùa cao điểm: Mùa Hè và Mùa Xuân là hai mùa kinh doanh sôi động nhất, mang lại doanh thu cao và gần như tương đương nhau. Đây là giai đoạn “vàng” của công ty.

  • Mùa thấp điểm: Ngược lại, Mùa Thu là mùa có doanh thu thấp nhất, cho thấy nhu cầu mua sắm giảm đáng kể trong giai đoạn này, theo sau là Mùa Đông.

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", col.names = c("Ngày", "Tổng doanh thu"))
Doanh thu theo ngày trong tuần
Ngày Tổng doanh thu
Thursday 113678630
Friday 111803200
Wednesday 111762837
Saturday 111409344
Monday 110441224
Sunday 110304080
Tuesday 109034920

Giải thích code:

  • (2): Nhóm tất cả các hàng trong data frame BK1 lại với nhau dựa trên giá trị của cột Weekday (Ngày trong tuần).

  • (3): Tính tổng của cột Revenue (Doanh thu) cho mỗi nhóm (tức là cho mỗi ngày trong tuần) và sắp xếp các hàng theo thứ tự giảm dần của cột Total_Revenue, tức là ngày có doanh thu cao nhất sẽ được xếp lên đầu.

Nhận xét: Hành vi mua sắm của khách hàng không bị phụ thuộc nhiều vào các ngày nghỉ. Điều này cho thấy khách hàng có thể là những người có nhu cầu ổn định, mua sắm thường xuyên trong suốt cả tuần. Sự ổn định này giúp việc lên kế hoạch nhân sự và quản lý hàng tồn kho trở nên dễ dàng và hiệu quả hơn.

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

3.8.1 Mức độ ưa chuộng theo phân khúc giá:

price_level_popularity <- BK1 %>%
count(Price_Level, sort = TRUE) %>% 
mutate(Percentage = round(n / sum(n) * 100, 2))
kable(price_level_popularity, caption = "Mức độ phổ biến của các phân khúc giá", col.names = c("Phân khúc giá", "Số lượng giao dịch", "Tỷ lệ"))
Mức độ phổ biến của các phân khúc giá
Phân khúc giá Số lượng giao dịch Tỷ lệ
Phổ thông 50030 50.03
Cao cấp 29341 29.34
Hạng sang 20629 20.63

Giải thích code:

  • (2): Đếm số lần xuất hiện của mỗi giá trị duy nhất trong cột Price_Level (Phân khúc giá).

  • (3): Lấy số lượng giao dịch của từng phân khúc chia cho tổng số lượng rồi nhân 100 để ra tỷ lệ phần trăm và tạo một cột mới có tên là Percentage.

Nhận xét: Dòng sản phẩm “Phổ thông” với 50.03% tổng số lượng giao dịch, phục vụ cho số đông và là nguồn doanh số chính. Các phân khúc “Cao cấp” và “Hạng sang” tuy chiếm tỷ trọng nhỏ hơn nhưng vẫn đóng vai trò quan trọng trong việc đa dạng hóa danh mục, nâng cao hình ảnh thương hiệu và phục vụ các nhóm khách hàng có khả năng chi trả cao hơn.

3.8.2 Mẫu xe được ưa chuộng nhất theo địa điểm cửa hàng:

model_preference_by_location <- BK1 %>%
group_by(Store_Location, Bike_Model) %>%
summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
group_by(Store_Location) %>% top_n(1, Total_Quantity)
kable(model_preference_by_location, caption = "Mẫu xe được ưa chuộng nhất theo địa điểm", col.names = c("Địa điểm","Mẫu xe", "Tổng số lượng bán"))
Mẫu xe được ưa chuộng nhất theo địa điểm
Địa điểm Mẫu xe Tổng số lượng bán
Chicago Hybrid Bike 6318
Houston Road Bike 6243
Los Angeles Folding Bike 6251
New York Hybrid Bike 6422
Philadelphia BMX 6253
Phoenix Cruiser 6442
San Antonio Folding Bike 6248

Giải thích code:

  • (2): Nhóm data frame BK1 theo hai cột là Store_Location (Địa điểm cửa hàng) và Bike_Model (Mẫu xe).

  • (3): Đối với mỗi nhóm (tức là mỗi mẫu xe tại mỗi địa điểm), nó sẽ tính tổng của cột Quantity (Số lượng).

  • (4): Với mỗi nhóm Store_Location, hàm này sẽ lọc và chỉ giữ lại 1 hàng (n=1) có giá trị cao nhất trong cột Total_Quantity. Kết quả là nó sẽ tìm ra mẫu xe (Bike_Model) bán chạy nhất cho từng địa điểm cửa hàng.

Nhận xét: Phân tích cho thấy thị hiếu của khách hàng có sự khác biệt rõ rệt theo từng địa điểm. Mỗi thành phố lại có một mẫu xe bán chạy nhất riêng, ví dụ như Cruiser tại Phoenix hay BMX tại Philadelphia.

3.8.3 Tương quan giữa Doanh thu và Giá xe:

correlation_re_price <- cor(BK1$Revenue, BK1$Price)
print(paste("Hệ số tương quan giữa doanh thu và giá xe:", round(correlation_re_price, 4)))
## [1] "Hệ số tương quan giữa doanh thu và giá xe: 0.7055"

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.

Nhận xét: Kết quả 0.7055, một con số dương và khá gần với 1. Điều này cho thấy có một mối quan hệ tương quan dương mạnh giữa giá xe (Price) và doanh thu (Revenue) của một giao dịch. Nói một cách đơn giản: Khi giá của chiếc xe trong một giao dịch tăng lên, thì doanh thu của giao dịch đó cũng có xu hướng tăng theo. Mối quan hệ này là hợp lý và đúng với mong đợi trong kinh doanh.

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

Trong mục 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

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_col(show.legend = FALSE) +   # Vẽ cột, ẩn chú giải màu
  geom_text(aes(label = dollar(Total_Revenue, accuracy = 1)), 
            angle = 90,             # Xoay nhãn dọc
            vjust = 0.5,            # Căn giữa theo chiều cao cột
            hjust = 1.1,            # Đưa nhãn vào trong thân cột
            color = "white",        # Màu chữ trắng nổi bật
            size = 3.2, 
            fontface = "bold") +
  labs(title = "Tổng doanh thu theo từng mẫu xe",
       x = "Mẫu xe",
       y = "Tổng doanh thu") +
  scale_y_continuous(labels = dollar_format()) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích code:

    1. Bắt đầu quá trình vẽ biểu đồ với dữ liệu vừa tính toán.
    1. 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.
    1. 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.

Nhận xét: Biểu đồ cho thấy mẫu xe Hybrid Bike có tổng doanh thu cao nhất. Khoảng cách doanh thu giữa các mẫu xe khá nhỏ, kết quả giúp nhà quản lý xác định đâu là sản phẩm chủ lực.

4.1.2 Biểu đồ tròn: Tỷ lệ Doanh thu theo Địa điểm 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 = 0.8) +
coord_polar("y", start = 0) +
geom_text(aes(label = scales::percent(Total_Revenue / sum(Total_Revenue), accuracy = 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", x = NULL, y = NULL) +
theme_void() +
theme(legend.title = element_text(face = "bold"))

Giải thích code:

  • (2): Gom nhóm tất cả các giao dịch theo từng cửa hàng riêng biệt, với mỗi cửa hàng, tính tổng doanh thu mà nó tạo ra.

  • (3): Khởi tạo biểu đồ với x = “” để vẽ tất cả các cột chồng lên nhau tại một vị trí duy nhất và chiều cao của các đoạn trong cột chồng được quyết định bởi doanh thu.

  • (4): Vẽ một biểu đồ cột chồng duy nhất.

  • (5): 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 mà chúng ta thấy.

Nhận xét: Cửa hàng ở New York hoạt động tốt nhất và chỉ là cao hơn các địa điểm khác 1%. Đ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.3 Biểu đồ Barplot: 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")

# Vẽ biểu đồ
ggplot(revenue_by_store1, 
       aes(x = reorder(Store_Location, -Total_Revenue), 
           y = Total_Revenue, 
           fill = Store_Location)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = paste0("$", format(Total_Revenue, big.mark = ",", scientific = FALSE))),
            angle = 90,               # 🔹 Xoay chữ dọc
            vjust = 0.5,              # 🔹 Căn giữa theo chiều cao
            hjust = 1.1,              # 🔹 Đẩy chữ vào trong cột
            color = "white", 
            size = 3.2, 
            fontface = "bold") +
  labs(title = "Doanh thu theo địa điểm cửa hàng (Barplot)",
       subtitle = "Các địa điểm có doanh thu gần tương đương nhau",
       x = "Địa điểm cửa hàng",
       y = "Tổng doanh thu") +
  scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ",")) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích code:

    1. nhóm dữ liệu theo cột Store_Location (địa điểm cửa hàng).
    1. tính tổng doanh thu của mỗi nhóm; .groups = “drop”: loại bỏ thông tin nhóm sau khi tính (trả về data frame bình thường).
    1. khởi tạo biểu đồ với dữ liệu tổng hợp.
    1. mỗi cửa hàng có màu riêng biệt.

Nhận xét Dễ dàng nhìn thấy cửa hàng ở New York đóng góp cao nhất. 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.4 Biểu đồ Treemap: Tỷ trọng 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()

Giải thích code:

    1. Ánh xạ Thẩm mỹ: Thiết lập các ánh xạ (Aesthetics) cơ bản. Cụ thể: area = Total_Revenue chỉ định rằng diện tích của mỗi hình chữ nhật trong treemap sẽ tỷ lệ thuận với cột Total_Revenue.
    1. Ánh xạ Màu sắc: Chỉ định rằng màu sắc của mỗi hình chữ nhật sẽ được xác định theo tên của Store_Location.
    1. geom_treemap() Loại Hình Biểu đồ: Thêm lớp hình học geom_treemap (từ thư viện treemapify) để vẽ biểu đồ Treemap.
    1. Thêm Văn bản (Nhãn): Thêm lớp văn bản để hiển thị nhãn Store_Location lên treemap. colour = “white” đặt màu chữ là trắng. place = “centre” đặt nhãn ở trung tâm hình. size = 14 đặt cỡ chữ lớn. grow = TRUE cho phép kích thước chữ tăng theo diện tích hình chữ nhật.
    1. Thiết lập Chủ đề: Áp dụng chủ đề tối giản (minimal theme) cho biểu đồ, giúp các hình chữ nhật và dữ liệu nổi bật hơn.

Nhận xét Diện tích mỗi ô = mức doanh thu → dùng để kiểm tra store nào chiếm tỷ trọng lớn.

4.1.5 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:

    1. Khởi tạo biểu đồ bằng dữ liệu revenue_by_model_store vừa tạo.
    1. 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.
    1. Thiết lập trục tung (trục Y) là tổng doanh thu.
    1. 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.
    1. 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.
    1. Định dạng các giá trị trên trục Y thành đơn vị tiền tệ đô la ($).
    1. 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.

Nhận xét:

  • 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, “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.6 Biểu đồ đường: Xu hướng doanh thu qua các tháng

  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))

Giải thích code:

    1. Tạo cột thời gian Sử dụng hàm floor_date (từ gói lubridate) để làm tròn cột ngày (Date) xuống đầu tháng gần nhất, tạo ra cột YearMonth.
    1. Nhóm dữ liệu Nhóm dữ liệu theo cột YearMonth vừa tạo.
    1. Tổng hợp doanh thu Tính tổng cột Revenue cho mỗi nhóm (mỗi tháng), tạo ra cột Monthly_Revenue; .groups = “drop” Bỏ nhóm Bỏ cấu trúc nhóm sau khi tính toán xong.

Ý 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

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 = "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))

Giải thích code:

    1. Lấy cột Date và làm tròn xuống ngày đầu tiên của tháng. Ví dụ, 2025-10-15 và 2025-10-28 đều trở thành 2025-10-01. Điều này giúp nhóm tất cả các giao dịch trong cùng một tháng lại với nhau.
    1. Tạo một cột mới tên là MA3. Cột này chứa giá trị trung bình động 3 tháng (Moving Average) của cột Monthly_Revenue; align = “right” có nghĩa là giá trị trung bình động tại một tháng được tính bằng chính tháng đó và 2 tháng trước đó.
    1. Bắt đầu vẽ biểu đồ, sử dụng bộ dữ liệu revenue_over_time đã chuẩn bị, với trục X là cột YearMonth; Vẽ một đường màu xanh (steelblue) thể hiện doanh thu hàng tháng thực tế.
    1. Thêm các điểm (chấm tròn, viền đỏ, nền vàng) lên trên đường line để đánh dấu các điểm dữ liệu hàng tháng.
    1. Vẽ thêm một đường thứ hai màu cam (orange) thể hiện giá trị MA3. Đường này thường mượt hơn đường doanh thu thực tế, giúp nhìn rõ xu hướng chung.
    1. Định dạng các số trên trục Y (Doanh thu) thành dạng tiền tệ (ví dụ: $10,000).
    1. Định dạng trục X, yêu cầu hiển thị nhãn 3 tháng một lần (date_breaks) và theo định dạng “Tháng Năm” (ví dụ: “Nov 2025”).
    1. Xoay các nhãn trên trục X một góc 45 độ để chúng không bị chồng chéo lên nhau.

Ý 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.

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.2 Phân tích mức giá

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

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))

Giải thích code:

    1. Ánh xạ cột Bike_Model lên trục hoành (x). Hàm reorder() sắp xếp các mẫu xe trên trục x theo giá bán trung vị (median) của chúng (vì reorder() mặc định sử dụng median khi không chỉ định hàm tổng hợp).
    1. Tạo biểu đồ hộp: show.legend = FALSE ẩn chú giải màu vì màu được lặp lại trên trục x; outlier.colour = “red”, outlier.shape = 8: Đặt màu cho các điểm ngoại lai (Outliers) là đỏ và hình dạng là dấu sao (shape = 8).
    1. Định dạng giá trị trên trục tung (Giá Bán) theo định dạng tiền tệ (Dollar).
    1. Thêm một điểm đại diện cho giá trị trung bình (fun = mean) của giá bán cho mỗi mẫu xe. Điểm này có hình dạng hình thoi (shape = 23), cỡ 4, và màu nền trắng.
    1. Áp dụng chủ đề theme_bw (nền trắng, đường lưới đen mỏng).
    1. Xoay nhãn trục hoành 45 độ và căn lề phải (hjust = 1) để dễ đọc và tránh chồng lấn khi có nhiều mẫu xe.

Ý nghĩa: Biểu đồ hộp cho thấy các mẫu xe có giá trung bình và khoảng giá khá bằng nhau.

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

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()

Giải thích code:

    1. Khởi tạo đồ thị bằng ggplot, sử dụng khung dữ liệu BK1. Ánh xạ cột Price (Giá bán) lên trục hoành (x).
    1. Vẽ biểu đồ ước tính mật độ kernel, hiển thị hình dạng của sự phân bố dữ liệu giá bán; fill = “orange”, alpha = 0.6: Tô màu cam cho vùng dưới đường cong mật độ. alpha = 0.6 đặt độ mờ (độ trong suốt) là 60%; color = “black”, size = 1: Đặt màu đen cho đường viền của đường cong mật độ và độ dày đường viền là 1.

Ý 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.2.3 Biểu đồ mật độ (density): Biểu đồ mật độ phân bố giá theo từng mẫu xe

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()

Giải thích code:

    1. Bắt đầu tạo đồ thị bằng ggplot, ánh xạ cột Price (Giá bán) lên trục hoành (x).
    1. Vẽ đường cong ước tính mật độ kernel. Tô màu xanh dương (fill = “blue”) cho vùng dưới đường cong, với độ mờ (alpha) là 0.7 (70% không trong suốt).
    1. Chia biểu đồ chính thành nhiều biểu đồ nhỏ (facet_wrap), mỗi biểu đồ đại diện cho một Bike_Model riêng biệt. Điều này cho phép so sánh sự phân bố giá của từng mẫu xe một cách độc lập.

Ý 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.3 Phân tích theo nhóm tuổi hoặc giới tính của khách hàng

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

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()

Giải thích code:

    1. Vẽ biểu đồ tần suất. Chiều cao của mỗi cột biểu thị số lượng khách hàng trong một khoảng tuổi nhất định.
    1. binwidth = 5: Đặt độ rộng của mỗi cột (bin) là 5 tuổi. Ví dụ: các cột sẽ là [20, 25), [25, 30), v.v.
    1. fill = “skyblue”, color = “black”, alpha = 0.7: Tô màu nền xanh da trời (skyblue), viền đen, và độ mờ (alpha) là 0.7.
    1. Vẽ một đường thẳng đứng (vline) tại vị trí độ tuổi trung bình của khách hàng.
    1. annotate(“text”, …): Thêm một nhãn văn bản cố định để ghi rõ giá trị tuổi trung bình lên biểu đồ.
    1. Vẽ đường cong ước tính mật độ kernel.
    1. aes(y = after_stat(count) * 5): Nhân giá trị mật độ (mặc định) với độ rộng bin (5) để đặt đường mật độ trên cùng thang đo với trục y của histogram (Count), cho phép cả hai lớp đồ thị hiển thị cùng nhau một cách hợp lý.

Ý 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.3.2 Biểu đồ cột chồng: Doanh thu theo nhóm tuổi và giới tính

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()

Giải thích code:

    1. Tính tổng cột Revenue (Doanh Thu) cho mỗi nhóm, tạo ra cột mới là Total_Revenue (Tổng Doanh Thu). Sau đó, loại bỏ cấu trúc nhóm.
    1. Khởi tạo biểu đồ, sử dụng khung dữ liệu revenue_age_gender. Ánh xạ Age_Group lên trục x, Total_Revenue lên trục y, và sử dụng Customer_Gender để tô màu nền (fill) cho các cột.
    1. Vẽ biểu đồ cột. stat = “identity” yêu cầu chiều cao cột được lấy trực tiếp từ giá trị Total_Revenue trên trục y. position = “stack” chồng các cột theo giới tính lên nhau.
    1. Chia biểu đồ chính thành các biểu đồ nhỏ (facet_wrap), mỗi biểu đồ đại diện cho một Customer_Gender riêng biệt. Điều này giúp so sánh rõ hơn doanh thu theo nhóm tuổi giữa hai giới tính.
    1. Định dạng các giá trị trên trục tung (Tổng Doanh Thu) theo định dạng tiền tệ (Dollar).
    1. Áp dụng bảng màu “Set2” từ gói RColorBrewer cho màu nền của các cột, đây là một bảng màu phân loại thường được sử dụng.

Ý 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.3.3 Biểu đồ phân tán (Scatter Plot): Mối quan hệ giữa tuổi và giá trị đơn hà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()

Giải thích code:

    1. Khởi tạo biểu đồ. Sử dụng hàm sample_n(5000) để lấy ngẫu nhiên 5000 hàng từ BK1 để giảm tải tính toán và giúp biểu đồ dễ đọc hơn. Ánh xạ Customer_Age lên trục x và Revenue lên trục y.
    1. Thêm lớp điểm (Scatter Plot). Mỗi điểm đại diện cho một giao dịch. alpha = 0.3 đặt độ trong suốt của điểm là 30%, giúp nhìn thấy mật độ điểm. Màu sắc của mỗi điểm được xác định bởi Store_Location (Địa điểm Cửa hàng).
    1. Thêm đường hồi quy. method = “lm” chỉ định sử dụng hồi quy tuyến tính (Linear Model) để vẽ đường xu hướng. color = “red” đặt màu đỏ cho đường này. se = FALSE ẩn vùng sai số chuẩn (standard error) xung quanh đường. linetype = “dashed” làm đường xu hướng thành nét đứt.
    1. Định dạng các giá trị trên trục tung (Doanh Thu) theo định dạng tiền tệ (Dollar).

Ý 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.4 Phân tích Lòng trung thành của Khách hàng

4.4.1 Biểu đồ thanh ngang: Phân bổ khách hàng theo hạng thân thiết

ggplot(loyalty_distribution, aes(x = Percentage, y = reorder(Loyalty_Status, Percentage), fill = Loyalty_Status)) +
  geom_col() + # Sử dụng geom_col để vẽ biểu đồ thanh
  geom_text(aes(label = paste0(round(Percentage, 1), "%")),
            hjust = -0.2, # Điều chỉnh vị trí nhãn để nằm bên phải thanh
            size = 4, color = "black") +
  labs(title = "Phân bổ khách hàng theo hạng thân thiết",
       x = "Tỷ lệ (%)",
       y = "Hạng thân thiết") +
  scale_x_continuous(limits = c(0, max(loyalty_distribution$Percentage) * 1.2)) + # Mở rộng giới hạn trục X
  theme_minimal() +
  guides(fill = "none") # Bỏ chú giải màu vì màu được lặp lại ở trục Y

Giải thích code:

    1. Khởi tạo biểu đồ. Ánh xạ Percentage (Tỷ lệ %) lên trục hoành (x), sắp xếp các hạng thân thiết Loyalty_Status trên trục tung (y) dựa trên giá trị Percentage của chúng, và dùng Loyalty_Status để tô màu nền (fill) cho các thanh.
    1. Thêm lớp cột/thanh. Vì trục y là biến phân loại và trục x là giá trị, geom_col (viết tắt của geom_bar(stat=“identity”)) là thích hợp để vẽ biểu đồ thanh.
    1. Thêm nhãn văn bản vào cuối mỗi thanh. Nhãn hiển thị giá trị Percentage được làm tròn đến 1 chữ số thập phân và thêm ký hiệu “%”.
    1. scale_x_continuous(limits = c(0, max(loyalty_distribution$Percentage) * 1.2)): Tùy chỉnh trục hoành (x). Đặt giới hạn bắt đầu từ 0 và kết thúc bằng cách mở rộng giá trị phần trăm tối đa thêm 20% (* 1.2), để tạo không gian cho nhãn văn bản hiển thị bên ngoài thanh.
    1. Ẩn chú giải màu (fill) vì màu sắc đã được thể hiện rõ ràng trên trục tung (y) và không cần thiết.

Nhận xét:

Hạng Platinum Chiếm Đa Số Tuyệt Đối (66.7%): Hạng Platinum chiếm tỷ lệ lớn nhất, với 66.7% tổng số khách hàng. Điều này cho thấy phần lớn cơ sở khách hàng nằm ở phân khúc cao cấp nhất hoặc có giá trị nhất (tùy thuộc vào tiêu chí xếp hạng).

Hạng Gold Đáng Kể (31.7%): Hạng Gold chiếm tỷ lệ lớn thứ hai, với 31.7%. Kết hợp với Platinum, hai hạng này chiếm hơn 98% cơ sở khách hàng.

Các Hạng Thấp Chiếm Tỷ Lệ Rất Nhỏ (Dưới 2%):

Hạng Silver chỉ chiếm 1.5%.

Hạng Bronze không có khách hàng nào (0%).

4.4.2 Tổng doanh thu theo hạng khách hàng (Biểu đồ cột)

revenue_by_loyalty <- BK1 %>%
  group_by(Loyalty_Status) %>%
  summarise(Total_Revenue = sum(Revenue)) %>%
  ungroup()
ggplot(revenue_by_loyalty, aes(x = Loyalty_Status, y = Total_Revenue, fill = Loyalty_Status)) +
  geom_col() +
  labs(title = "Doanh thu theo Hạng Khách hàng",
       x = "Hạng khách hàng",
       y = "Tổng doanh thu") +
  theme_minimal() +
  theme(legend.position = "none")

Giải thích code:

    1. Nhóm tất cả các dòng dữ liệu dựa trên giá trị của cột Loyalty_Status. Ví dụ, tất cả khách hàng “Vàng” vào một nhóm, “Bạc” vào một nhóm, v.v.
    1. Sau khi đã nhóm, R sẽ tính tổng của cột Revenue cho từng nhóm (từng hạng khách hàng) và lưu kết quả vào một cột mới tên là Total_Revenue.
    1. Hủy nhóm để đưa dataframe trở lại trạng thái bình thường.
    1. ggplot(revenue_by_loyalty, …): Khởi tạo biểu đồ, sử dụng dataframe revenue_by_loyalty vừa tạo; aes(x = Loyalty_Status, y = Total_Revenue, fill = Loyalty_Status): Thiết lập “ánh xạ” (aesthetics): x = Loyalty_Status; y = Total_Revenue; fill = Loyalty_Status: Tô màu cho các cột dựa theo chính hạng khách hàng.
    1. Yêu cầu vẽ biểu đồ cột.
    1. Ẩn phần chú giải về màu sắc. Điều này là hợp lý vì các nhãn trên trục X (Loyalty_Status) đã giải thích ý nghĩa của các cột rồi.

Nhận xét:: Biểu đồ là bằng chứng cho thấy doanh nghiệp đang hoạt động hiệu quả với khách hàng hiện tại có giá trị cao, nhưng cần phải cân nhắc chiến lược để giảm thiểu rủi ro tập trung và mở rộng cơ sở doanh thu từ các phân khúc cấp thấp hơn.

4.4.3 Giá trị đơn hàng trung bình của mỗi hạng khách hàng (Biểu đồ cột)

ggplot(avg_revenue_by_loyalty, aes(x = reorder(Loyalty_Status, Average_Revenue_Per_Sale), y = Average_Revenue_Per_Sale, fill = Loyalty_Status)) +
  geom_col() +
  geom_text(aes(label = dollar(Average_Revenue_Per_Sale, prefix = "", suffix = " USD")), vjust = 0.5, size = 4) +
  labs(title = "Giá trị đơn hàng trung bình theo hạng khách hàng",
       x = "Hạng thân thiết",
       y = "Giá trị đơn hàng trung bình") +
  scale_y_continuous(labels = scales::dollar_format(prefix = "", suffix = " USD")) +
  theme_minimal() +
  guides(fill = "none")

Giải thích code:

    1. aes(x = reorder(Loyalty_Status, Average_Revenue_Per_Sale), y = Average_Revenue_Per_Sale, fill = Loyalty_Status): Ánh xạ: Trục x là Loyalty_Status, được sắp xếp theo giá trị Average_Revenue_Per_Sale (từ thấp đến cao mặc định). Trục y là Average_Revenue_Per_Sale. Màu nền (fill) cột theo Loyalty_Status.
    1. Thêm lớp cột. Chiều cao cột được lấy trực tiếp từ giá trị Average_Revenue_Per_Sale.
    1. Thêm nhãn văn bản lên trên đỉnh mỗi cột. Hàm dollar(…) (từ gói scales) được dùng để định dạng giá trị trung bình thành dạng tiền tệ, thêm ký hiệu ” USD” ở cuối.
    1. Tùy chỉnh trục tung (y). Định dạng các giá trị trên trục này theo định dạng tiền tệ với hậu tố ” USD”.

Nhận xét: Giá trị đơn hàng trung bình của hạng khách hàng Bronze là cao nhất vượt trội hơn hẳn so vớ i Gold, Platinum, Silver có giá trị khá ngang nhau.

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

4.5.1 Heatmap: Doanh thu theo ngày trong tháng và quý

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", linewidth = 0.5) +
  geom_text(aes(label = number(Total_Revenue / 1000, accuracy = 0.01)),
            size = 2.8, color = "black", fontface = "bold") +
  
  scale_fill_gradient(
    low = "#FFF5CC", high = "#7A0000",
    labels = label_dollar(scale = 1, suffix = "")
  ) +
  
  labs(
    title = "Heatmap doanh thu theo ngày và tháng",
    subtitle = "Doanh thu được tính bằng nghìn đô la Mỹ (K)",
    x = "Ngày trong tuần",
    y = "Tháng",
    fill = "Doanh thu"
  ) +
  
  theme_minimal(base_size = 11) +
  theme(
    plot.title = element_text(face = "bold", size = 13, hjust = 0.5),
    plot.subtitle = element_text(size = 10, hjust = 0.5),
    axis.text.x = element_text(angle = 45, hjust = 1),
    axis.text.y = element_text(size = 9),
    legend.title = element_text(face = "bold"),
    legend.text = element_text(size = 8),
    panel.grid = element_blank(),
    plot.margin = margin(10, 10, 10, 10)
  )

Giải thích code:

    1. Khởi tạo biểu đồ. Ánh xạ Weekday lên trục x, Month lên trục y, và giá trị Total_Revenue vào thuộc tính fill (màu nền của ô).
    1. Thêm lớp ô vuông (Tile) để tạo heatmap. color = “white” và linewidth = 0.5 tạo ra các đường viền trắng mỏng phân chia giữa các ô.
    1. Thêm nhãn văn bản vào giữa mỗi ô. Hiển thị Total_Revenue được chia cho 1000 và định dạng là tiền tệ với hậu tố “K” (nghìn), giúp nhãn gọn gàng hơn.
    1. Tùy chỉnh thang màu liên tục. Màu thấp (low) là vàng nhạt, màu cao (high) là đỏ đậm, tạo nên sự chuyển đổi màu sắc rõ ràng theo giá trị doanh thu.
    1. Định dạng nhãn cho chú giải màu (colorbar) dưới dạng tiền tệ.

Ý 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.5.2 Biểu đồ đường: Xu hướng số lượng xe bán theo quý và địa điểm cửa hàng

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))

Giải thích code:

    1. Khởi tạo biểu đồ, sử dụng dữ liệu đã tổng hợp.
    1. aes(x = Quarter, y = Total_Quantity, color = Store_Location, group = Store_Location): Ánh xạ: Quarter lên trục x, Total_Quantity lên trục y. Store_Location được dùng để tô màu cho các đường. group = Store_Location rất quan trọng để đảm bảo ggplot vẽ một đường riêng cho mỗi cửa hàng.
    1. Thêm lớp đường nối giữa các điểm dữ liệu. Độ dày đường là 1.2.
    1. Thêm lớp điểm tại mỗi điểm dữ liệu (mỗi quý). Cỡ điểm là 2.
    1. Xoay nhãn trục x 45 độ và căn lề phải để tránh nhãn Quý bị chồng chéo.

Ý 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.5.3 Biểu đồ thanh ngang: Top 5 nhân viên theo số lượng giao dịch

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() + 
  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()

Giải thích code:

    1. Khởi tạo biểu đồ, sử dụng dữ liệu của Top 5 nhân viên.
    1. aes(x = reorder(Salesperson_ID, Transaction_Count), y = Transaction_Count, fill = Salesperson_ID): Ánh xạ: Trục x là Salesperson_ID, được sắp xếp lại dựa trên Transaction_Count. Trục y là Transaction_Count. Màu nền (fill) cột theo Salesperson_ID.
    1. Thêm lớp thanh. stat = “identity” yêu cầu chiều cao thanh lấy trực tiếp từ Transaction_Count. show.legend = FALSE ẩn chú giải màu. Độ rộng thanh là 0.7.
    1. Lật biểu đồ để trục x (Số Lượng Giao Dịch) thành trục ngang và trục y (Nhân Viên) thành trục dọc, tạo thành biểu đồ thanh ngang.
    1. Thêm nhãn văn bản (số lượng giao dịch) ở cuối mỗi thanh. hjust = -0.2 dịch chuyển nhãn ra ngoài đầu thanh, giúp biểu đồ gọn gàng hơn.

Ý 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.5.4 Biểu đồ bong bóng: Tổng quan các mẫu xe

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(3, 15), 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()

Giải thích code:

    1. Khởi tạo biểu đồ, sử dụng dữ liệu đã tổng hợp. aes(x = Total_Quantity, y = Avg_Price, size = Total_Revenue, color = Bike_Model): Ánh xạ ba biến vào ba thuộc tính trực quan: Trục x là Total_Quantity, Trục y là Avg_Price, và Kích thước bong bóng là Total_Revenue. Màu sắc được dùng để phân biệt các Bike_Model.
    1. Thêm lớp điểm (bong bóng). alpha = 0.7 đặt độ trong suốt là 70%, giúp các bong bóng chồng lên nhau dễ nhìn hơn.
    1. Tùy chỉnh phạm vi kích thước của bong bóng, từ 3 đến 15, để tránh các bong bóng quá nhỏ hoặc quá lớn. Đặt tên chú giải kích thước là “Tổng Doanh Thu (USD)”.
    1. Áp dụng bảng màu phân loại viridis cho màu sắc của bong bóng. guide = “none” ẩn chú giải màu vì màu sắc chủ yếu được dùng để phân biệt và tên mẫu xe sẽ được ghi trực tiếp trên biểu đồ.
    1. Thêm nhãn văn bản (tên mẫu xe) cho mỗi bong bóng. vjust = -2.5 dịch chuyển nhãn lên trên bong bóng, và sử dụng chữ đậm (fontface = “bold”).

Nhận xét “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.5.5 Biểu đồ Treemap: Tỷ trọng doanh thu theo mẫu xe và địa điểm cửa hàng

ggplot(revenue_by_model, aes(
area = Total_Revenue, 
fill = Bike_Model, label = Bike_Model)) +
geom_treemap() +
geom_treemap_text(colour = "white", place = "centre", size = 14, grow = TRUE) +
labs(title = "Tỷ trọng doanh thu theo mẫu xe", fill = "Mẫu xe")

Giải thích code:

  • (1): Khởi tạo biểu đồ sử dụng bảng dữ liệu đã tóm tắt tổng doanh thu theo từng mẫu xe.

  • (2): Diện tích của mỗi hình chữ nhật trên biểu đồ sẽ tỷ lệ thuận với tổng doanh thu của mẫu xe đó. Doanh thu càng cao, hình chữ nhật càng lớn.

  • (4): Có nhiệm vụ vẽ các hình chữ nhật của biểu đồ Treemap dựa trên các ánh xạ đã thiết lập ở trên.

Nhận xét: Không có bất kỳ mẫu xe nào có diện tích vượt trội, cho thấy không có một sản phẩm nào đang thống trị hay gánh doanh thu cho toàn bộ công ty.

  • Giảm thiểu rủi ro: Công ty không bị phụ thuộc vào sự thành công của bất kỳ một sản phẩm riêng lẻ nào. Nếu một mẫu xe gặp vấn đề, các mẫu xe khác vẫn có thể duy trì hoạt động kinh doanh ổn định.

  • Tiếp cận thị trường rộng: Sự thành công đồng đều cho thấy công ty đã đáp ứng tốt nhu cầu của nhiều phân khúc khách hàng khác nhau.

4.5.6 Biểu đồ Sankey: Luồng khách hàng từ giới tính đến 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")

Giải thích code:

    1. Khởi tạo biểu đồ. y = Count thiết lập chiều cao của các luồng tỷ lệ với số lượng khách hàng. axis1 và axis2 định nghĩa thứ tự và tên của các trục (cột) trong biểu đồ.
    1. Thêm lớp luồng (Alluvium), là các dải màu nối các trục với nhau. fill = Bike_Model tô màu cho các luồng theo Mẫu xe. width = 1/12 và alpha = 0.7 thiết lập độ rộng và độ trong suốt của các dải.
    1. Thêm lớp cột (Strata), là các khối hình chữ nhật đại diện cho tổng số lượng ở mỗi cấp độ (Giới tính và Mẫu xe).
    1. Thêm nhãn cho mỗi khối Strata. after_stat(stratum) lấy tên của khối (ví dụ: “Male”, “Mountain-300”).
    1. Đặt tên nhãn cho các trục x (cột) là “Giới tính” và “Mẫu xe”.
    1. Áp dụng chủ đề trống (theme_void), loại bỏ tất cả các thành phần không cần thiết như trục, đường lưới, và nhãn trục mặc định.
    1. Di chuyển chú giải màu (Legend) xuống dưới cùng của biểu đồ.

Ý 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.

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

Công ty Cổ phần Hoàng Anh Gia Lai (HAG) là một trong những doanh nghiệp có lịch sử hoạt động đầy biến động và thu hút sự quan tâm lớn trên thị trường chứng khoán Việt Nam. Giai đoạn 2015-2024 đánh dấu một thập kỷ mang tính bước ngoặt, khi HAG thực hiện quá trình tái cấu trúc sâu sắc, chuyển đổi mô hình kinh doanh từ bất động sản và cao su sang tập trung vào nông nghiệp công nghệ cao, với các sản phẩm chủ lực là cây ăn trái (chuối) và chăn nuôi (heo).

Quá trình chuyển đổi này đặt ra một câu hỏi nghiên cứu cốt lõi: Liệu chiến lược tái cấu trúc và tập trung vào nông nghiệp có thực sự mang lại hiệu quả tài chính bền vững cho HAG hay không?

Để trả lời câu hỏi này, chương này sẽ thực hiện một phân tích định lượng chi tiết dựa trên dữ liệu tài chính của công ty. Trọng tâm của phân tích sẽ là Báo cáo Kết quả Hoạt động Kinh doanh (KQKD), vì đây là báo cáo phản ánh rõ nét nhất khả năng tạo ra doanh thu, quản lý chi phí và hiệu quả sinh lời từ hoạt động cốt lõi của doanh nghiệp. Thông qua việc phân tích sâu các chỉ tiêu trên Báo cáo KQKD trong suốt 10 năm, nghiên cứu này sẽ làm rõ các xu hướng, xác định các động lực tăng trưởng và đánh giá những thách thức mà HAG đã và đang đối mặt.

1. Tổng quan dữ liệu

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

Để thực hiện phân tích, chúng tôi sử dụng bộ dữ liệu tài chính của HAG được tổng hợp cho giai đoạn 10 năm, từ 2015 đến 2024.

Nguồn gốc và Phạm vi: Dữ liệu được thu thập từ các báo cáo tài chính chính thức của CTCP Hoàng Anh Gia Lai, bao gồm số liệu hàng năm trong khoảng thời gian đã nêu.

Cấu trúc dữ liệu gốc: Dữ liệu ban đầu được tổ chức dưới dạng bảng (dạng rộng - wide format). Trong đó, mỗi hàng đại diện cho một năm tài chính và mỗi cột là một chỉ tiêu tài chính cụ thể. Tệp dữ liệu gốc (BCTC) là một bộ dữ liệu lớn, tích hợp các chỉ tiêu từ ba báo cáo tài chính chính:

  • Bảng Cân đối Kế toán

  • Báo cáo Kết quả Hoạt động Kinh doanh

  • Báo cáo Lưu chuyển Tiền tệ

Trọng tâm phân tích: Để đáp ứng mục tiêu nghiên cứu đã đề ra trong phần giới thiệu, bài phân tích này sẽ thực hiện thao tác rút trích dữ liệu (data extraction). Cụ thể, chúng tôi sẽ tạo ra một tập con (subset) chỉ chứa các biến thuộc Báo cáo Kết quả Kinh doanh để tiến hành phân tích chuyên sâu.

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

library(dplyr)
library(readxl)
BCTC <- read_excel("D:/Thúy Hiền/Ngôn ngữ lập trình trong phân tích dữ liệu/BCTC.xlsx")

read_excel(…) Là hàm chính từ gói readxl, dùng để đọc dữ liệu từ tệp Excel.

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

library(knitr)
library(kableExtra)
library(e1071)
library(scales)
library(treemapify)
library(zoo)
library(ggalluvial)
library(corrplot)
library(lubridate)
library(dplyr)
library(ggplot2)
library(tidyr)
library(scales)
library(janitor)

1.4 Làm sạch tên biến

# Làm sạch tên biến:
BCTC <- BCTC %>%
  clean_names()    # chuyển tên biến thành dạng không dấu, có thể chạy trong R

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

Giải thích code:

    1. Lệnh này lấy 5 dòng đầu tiên từ data frame (bộ dữ liệu) có tên là BCTC.
    1. Hàm kable() (từ gói knitr) dùng để tạo bảng cơ bản.
    1. kable_styling(…) dùng để thêm các tùy chỉnh nâng cao cho bảng kable.

1.6 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(
  "x1_doanh_thu_ban_hang_va_cung_cap_dich_vu",
  "x2_cac_khoan_giam_tru_doanh_thu",
  "x3_doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu",
  "x4_gia_von_hang_ban_va_dich_vu_cung_cap",
  "x5_loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu",
  "x6_doanh_thu_hoat_dong_tai_chinh",
  "x7_chi_phi_tai_chinh",
  "trong_do_chi_phi_lai_vay",
  "x8_phan_lai_trong_cong_ty_lien_ket",
  "x9_chi_phi_ban_hang",
  "x10_chi_phi_quan_ly_doanh_nghiep",
  "x11_lo_thuan_tu_hoat_dong_kinh_doanh",
  "x12_thu_nhap_khac",
  "x13_chi_phi_khac",
  "x14_lo_khac",
  "x15_lo_ke_toan_truoc_thue",
  "x16_chi_phi_thue_tndn_hien_hanh",
  "x17_chi_phi_thu_nhap_thue_tndn_hoan_lai",
  "x18_lo_sau_thue_tndn",
  "x19_lo_loi_nhuan_sau_thue_cua_cong_ty_me",
  "x20_lo_sau_thue_cua_co_dong_khong_kiem_soat",
  "x21_lo_lai_co_ban_tren_co_phieu_vnd",
  "x22_lo_lai_suy_giam_tren_co_phieu_vnd"
)
# 2. Tạo tập con chỉ gồm bảng kết quả kinh doanh
KQKD <- BCTC %>%
  select(
    nam,                   # Giữ lại cột Năm
    any_of(ten_bien_kqkd)  # Chọn các cột KQKD tồn tại
  )

1.7 Khám phá dữ liệu

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

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

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

kieu_du_lieu <- sapply(KQKD, class)
data.frame(KieuDuLieu = kieu_du_lieu)

Nhận xét: Các biến đều là biến định lượng thể hiện các thông số số học(numeric).

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(KQKD)))
## 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.

KQKD_sach_na <- KQKD %>%
  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(KQKD_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(KQKD_sach_na)))
## 
## 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

2.2.1 Đổi tên các biến

KQKD_sach_na <- KQKD_sach_na %>%
  rename(
    doanh_thu = x1_doanh_thu_ban_hang_va_cung_cap_dich_vu,
    giam_tru_dt = x2_cac_khoan_giam_tru_doanh_thu,
    doanh_thu_thuan = x3_doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu,
    gia_von = x4_gia_von_hang_ban_va_dich_vu_cung_cap,
    loi_nhuan_gop = x5_loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu,
    doanh_thu_tc = x6_doanh_thu_hoat_dong_tai_chinh,
    chi_phi_tc = x7_chi_phi_tai_chinh,
    chi_phi_lai_vay = trong_do_chi_phi_lai_vay,
    lai_trong_lien_ket = x8_phan_lai_trong_cong_ty_lien_ket,
    chi_phi_bh = x9_chi_phi_ban_hang,
    chi_phi_ql = x10_chi_phi_quan_ly_doanh_nghiep,
    loi_nhuan_hd = x11_lo_thuan_tu_hoat_dong_kinh_doanh, 
    thu_nhap_khac = x12_thu_nhap_khac,
    chi_phi_khac = x13_chi_phi_khac,
    lo_khac = x14_lo_khac,
    lo_truoc_thue = x15_lo_ke_toan_truoc_thue,
    cp_thue_hien_hanh = x16_chi_phi_thue_tndn_hien_hanh,
    cp_thue_hoan_lai = x17_chi_phi_thu_nhap_thue_tndn_hoan_lai,
    lo_sau_thue = x18_lo_sau_thue_tndn,
    lnst_cong_ty_me = x19_lo_loi_nhuan_sau_thue_cua_cong_ty_me,
    lo_cd_khong_kiem_soat = x20_lo_sau_thue_cua_co_dong_khong_kiem_soat,
    lai_co_ban = x21_lo_lai_co_ban_tren_co_phieu_vnd, # EPS: Earnings Per Share
    lai_suy_giam = x22_lo_lai_suy_giam_tren_co_phieu_vnd
  )
names(KQKD_sach_na)
##  [1] "nam"                   "doanh_thu"             "giam_tru_dt"          
##  [4] "doanh_thu_thuan"       "gia_von"               "loi_nhuan_gop"        
##  [7] "doanh_thu_tc"          "chi_phi_tc"            "chi_phi_lai_vay"      
## [10] "lai_trong_lien_ket"    "chi_phi_bh"            "chi_phi_ql"           
## [13] "loi_nhuan_hd"          "thu_nhap_khac"         "chi_phi_khac"         
## [16] "lo_khac"               "lo_truoc_thue"         "cp_thue_hien_hanh"    
## [19] "cp_thue_hoan_lai"      "lo_sau_thue"           "lnst_cong_ty_me"      
## [22] "lo_cd_khong_kiem_soat" "lai_co_ban"            "lai_suy_giam"

Nhận xét: Vì tên các chỉ tiêu trong báo cáo tài chính quá dài nên đã đặt tên lại cho các chỉ tiêu để dễ dàng phân tích.

2.2.2 Mã hóa giai đoạn kinh doanh: Trước và sau cột mốc phục hồi 2020

KQKD1 <- KQKD_sach_na%>%
  mutate(
    Giai_doan = ifelse(
      nam <= 2019,
      "Tái cấu trúc", # Đổi tên nhóm 2015-2019
      "Phục hồi"     # Đổi tên nhóm 2020-2024
      )
  )

Nhận xét:

Từ năm 2015-2019 là giai đoạn tái cấu trúc.

Từ năm 2020-2024 là giai đoạn phục hồi.

2.2.3 Mã hóa Tình trạng kinh doanh dựa trên lợi nhuận

KQKD1 <- KQKD1 %>%
  mutate(Tinh_trang_KD = ifelse(lnst_cong_ty_me > 0, "Có lãi", "Lỗ"))
KQKD1 %>%
  select(nam, lnst_cong_ty_me, Tinh_trang_KD)

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.

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(KQKD_sach_na), "nam")
bang_thong_ke_day_du <- do.call(
  rbind,
  lapply(cac_bien, function(v) thong_ke_mo_ta(KQKD_sach_na, v))
)
# Bảng 1
bang_thong_ke_day_du %>%
  select(Bien, Min, Q1, Median, Mean, Q3, Max) %>%
  kbl(
    caption = "Bảng thống kê mô tả (Phần 1)", 
    row.names = FALSE, 
    digits = 2, 
    align = "lcccccc" 
  ) %>%
  kable_styling(
    latex_options = c("striped", "bordered", "hold_position"), # Tùy chọn cho PDF
    full_width = FALSE,
    position = "center"
  )
Bảng thống kê mô tả (Phần 1)
Bien Min Q1 Median Mean Q3 Max
doanh_thu 2091833174 3.602780e+09 5197982826 4778701701.4 6162994499 6492569736
giam_tru_dt -111481812 -7.794384e+07 -33280844 -43640756.6 -14086485 -35528
doanh_thu_thuan 2075444024 3.592791e+09 5249491144 4760738870.9 6135097400 6442397199
gia_von -5430638742 -4.282861e+09 -3360410005 -3505832053.9 -2981560516 -1590448139
loi_nhuan_gop 205730343 6.325128e+08 1233428249 1254906817.0 1823704991 2374705174
doanh_thu_tc 280428437 6.358340e+08 1000786308 1062120059.4 1375440301 2137143442
chi_phi_tc -1963934151 -1.692079e+09 -1483654365 -1279198249.1 -1118636680 215432853
chi_phi_lai_vay -1585315746 -1.465539e+09 -1166140953 -1050382818.4 -837852275 270599417
lai_trong_lien_ket -18433513 -4.839361e+06 3434652 6752914.9 10974578 64840488
chi_phi_bh -396487002 -2.954072e+08 -222192512 -231297493.0 -150230945 -111239060
chi_phi_ql -1851240106 -6.988116e+08 -425967966 -386887348.5 -157646627 1349894514
loi_nhuan_hd -2022124320 -2.346216e+08 885249748 423709570.3 1178959094 1690412815
thu_nhap_khac 21546363 3.762621e+07 88909719 121902298.0 179523568 281127775
chi_phi_khac -1380140330 -8.527156e+08 -521404910 -612196782.4 -262012765 -116111269
lo_khac -1337563204 -7.785596e+08 -364405886 -490294484.4 -215137882 102463888
lo_truoc_thue -2351460262 -1.087367e+09 238921066 -66584914.1 968669652 1792876703
cp_thue_hien_hanh -153548976 -2.117953e+07 -3545265 -23375871.8 -2439711 -885768
cp_thue_hoan_lai -86187524 -3.727448e+07 -4342050 27897825.1 83741605 259098512
lo_sau_thue -2383339850 -1.125529e+09 249606713 -62062960.8 945681602 1781685785
lnst_cong_ty_me -1255661344 8.156770e+07 209773938 252282346.6 885660614 1663970953
lo_cd_khong_kiem_soat -2025322017 -3.024177e+08 -39747303 -314345307.4 86685488 302019303
lai_co_ban -1439 9.175000e+01 226 248.7 901 1794
lai_suy_giam -1439 9.175000e+01 226 248.7 901 1794
# Bảng 2
bang_thong_ke_day_du %>%
  select(Bien, Sd, Var, Skewness, Kurtosis) %>%
  kbl(
    caption = "Bảng thống kê mô tả (Phần 2)", 
    row.names = FALSE, 
    digits = 2,
    align = "lcccc" # Căn lề
  ) %>%
  kable_styling(
    latex_options = c("striped", "bordered", "hold_position"), # Tùy chọn cho PDF
    full_width = FALSE,
    position = "center"
  )
Bảng thống kê mô tả (Phần 2)
Bien Sd Var Skewness Kurtosis
doanh_thu 1697429237.0 2.881266e+18 -0.63 1.88
giam_tru_dt 39814324.8 1.585180e+15 -0.53 1.85
doanh_thu_thuan 1707942693.2 2.917068e+18 -0.66 1.90
gia_von 1269370382.3 1.611301e+18 -0.03 2.04
loi_nhuan_gop 779254932.9 6.072383e+17 -0.04 1.71
doanh_thu_tc 576082822.3 3.318714e+17 0.45 2.29
chi_phi_tc 646305167.5 4.177104e+17 1.28 3.85
chi_phi_lai_vay 559300683.9 3.128173e+17 1.29 4.14
lai_trong_lien_ket 22714342.4 5.159414e+14 1.72 5.53
chi_phi_bh 98499222.1 9.702097e+15 -0.38 1.84
chi_phi_ql 825992043.9 6.822629e+17 0.42 3.70
loi_nhuan_hd 1127420460.2 1.271077e+18 -1.03 3.12
thu_nhap_khac 97949046.0 9.594016e+15 0.57 1.83
chi_phi_khac 435485086.5 1.896473e+17 -0.58 2.07
lo_khac 453758629.0 2.058969e+17 -0.64 2.30
lo_truoc_thue 1385063737.3 1.918402e+18 -0.47 1.92
cp_thue_hien_hanh 46943148.9 2.203659e+15 -2.43 7.33
cp_thue_hoan_lai 102409828.1 1.048777e+16 1.14 3.55
lo_sau_thue 1384531230.6 1.916927e+18 -0.49 1.95
lnst_cong_ty_me 922629589.0 8.512454e+17 -0.32 2.35
lo_cd_khong_kiem_soat 719170399.5 5.172061e+17 -1.59 4.27
lai_co_ban 1026.3 1.053289e+06 -0.42 2.41
lai_suy_giam 1026.3 1.053289e+06 -0.42 2.41

3.2 Tạo thêm biến

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

KQKD1 <- KQKD1 %>%
  mutate(Ty_suat_LNG = (loi_nhuan_gop/doanh_thu_thuan) * 100)

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

KQKD1 <- KQKD1 %>%
  mutate(Ty_suat_LNST = (lnst_cong_ty_me/doanh_thu_thuan) * 100)

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

KQKD1 <- KQKD1 %>%
  mutate(Tong_chi_phi_HD = gia_von+chi_phi_bh+chi_phi_ql)

3.2.4 Doanh thu từ hoạt động tài chính ròng

KQKD1 <- KQKD1 %>%
  mutate(
    Doanh_thu_TC_rong = doanh_thu_tc - abs(chi_phi_tc) # Chi phí TC thường là âm, dùng abs để tính toán đúng
  )

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

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

KQKD1 <- KQKD1 %>%
 mutate(bien_loi_nhuan_gop = (loi_nhuan_gop / doanh_thu_thuan) * 100
 ) 
KQKD1 %>%
 select(nam, doanh_thu_thuan, loi_nhuan_gop, bien_loi_nhuan_gop) %>%
   kable(
    caption = "Bảng tóm tắt kết quả kinh doanh qua các năm",
    col.names = c("Năm", "Doanh thu thuần", "Lợi nhuận gộp", "Biên lợi nhuận gộp (%)"),
    digits = 2) 
Bảng tóm tắt kết quả kinh doanh qua các năm
Năm Doanh thu thuần Lợi nhuận gộp Biên lợi nhuận gộp (%)
2015 6252446533 1854425962 29.66
2016 6439779268 1009140526 15.67
2017 4841225074 1731542077 35.77
2018 5388200400 2374705174 44.07
2019 2075444024 227784373 10.98
2020 3176645956 205730343 6.48
2021 2097418366 506970227 24.17
2022 5110781887 1173401018 22.96
2023 6442397199 1293455480 20.08
2024 5783050002 2171912990 37.56

Nhận xét: 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.2 Tính tỷ lệ lãi vay

KQKD1 <- KQKD1 %>%
  mutate(
    ty_le_lai_vay_tren_cp_tai_chinh = (chi_phi_lai_vay/ chi_phi_tc) * 100
  )
KQKD1 %>%
  select(nam, chi_phi_tc, chi_phi_lai_vay, ty_le_lai_vay_tren_cp_tai_chinh) %>%
kable(
    caption = "Bảng tỷ lệ lãi vay",
    col.names = c("Năm", "Chi phí tài chính", "Chi phí lãi vay", "Tỷ lệ lãi vay trên chi phí tài chính"),
    digits = 2)
Bảng tỷ lệ lãi vay
Năm Chi phí tài chính Chi phí lãi vay Tỷ lệ lãi vay trên chi phí tài chính
2015 -1203667607 -1078711240 89.62
2016 -1674519826 -1579381993 94.32
2017 -1697932438 -1585315746 93.37
2018 -1721684164 -1532928450 89.04
2019 -1963934151 -1263369664 64.33
2020 -1318161483 -1253570666 95.10
2021 -1090293038 -971878185 89.14
2022 -1649147246 -793176972 48.10
2023 215432853 270599417 125.61
2024 -688075391 -716094685 104.07

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.

3.3.3 Doanh thu từ hoạt động tài chính ròng

KQKD1 <- KQKD1 %>%
  mutate(
    Doanh_thu_TC_rong = doanh_thu_tc - abs(chi_phi_tc) # Chi phí TC thường là âm, dùng abs để tính toán đúng
  )

3.3.4 Lợi nhuận từ hoạt động kinh doanh (Operating Profit - OP)

KQKD1 <- KQKD1 %>%
  mutate(
    Loi_nhuan_HDKD = loi_nhuan_gop - chi_phi_bh - chi_phi_ql,
  )

3.3.5 Tỷ suất lợi nhuận từ hoạt động kinh doanh (Operating Profit Margin - OPM)

KQKD1 <- KQKD1 %>%
  mutate(
    Ty_suat_LNHDKD = (Loi_nhuan_HDKD / doanh_thu_thuan) * 100
  ) 

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

KQKD1 <- KQKD1 %>%
  mutate(
    Tang_truong_DT = (doanh_thu_thuan-lag(doanh_thu_thuan))/lag(doanh_thu_thuan) * 100
  )
KQKD1 %>%
  select(nam, doanh_thu_thuan, Tang_truong_DT) %>%
kable(
    caption = "Bảng tốc độ tăng trưởng doanh thu theo năm",
    col.names = c("Năm", "Doanh thu thuần", "Tăng trưởng doanh thu"),
    digits = 2)
Bảng tốc độ tăng trưởng doanh thu theo năm
Năm Doanh thu thuần Tăng trưởng doanh thu
2015 6252446533 NA
2016 6439779268 3.00
2017 4841225074 -24.82
2018 5388200400 11.30
2019 2075444024 -61.48
2020 3176645956 53.06
2021 2097418366 -33.97
2022 5110781887 143.67
2023 6442397199 26.06
2024 5783050002 -10.23

Kết luận: 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. 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.7 Tốc độ tăng trưởng của giá vốn hàng bán

KQKD1 <- KQKD1 %>%
  mutate(
    Tang_truong_gia_von=(gia_von-lag(gia_von))/lag(gia_von) * 100
 ) 
KQKD1 %>%
 select(nam, gia_von, Tang_truong_gia_von) %>%
  kable(
    caption = "Bảng tốc dộ tăng trưởng giá vốn hàng bán",
    col.names = c("Năm", "Giá vốn", "Tăng trưởng giá bán"),
    digits = 2)
Bảng tốc dộ tăng trưởng giá vốn hàng bán
Năm Giá vốn Tăng trưởng giá bán
2015 -4398020571 NA
2016 -5430638742 23.48
2017 -3109682997 -42.74
2018 -3013495226 -3.09
2019 -1847659651 -38.69
2020 -2970915613 60.79
2021 -1590448139 -46.47
2022 -3937380869 147.56
2023 -5148941719 30.77
2024 -3611137012 -29.87

Nhận xét: 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, 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.8 Tốc độ tăng trưởng doanh thu thuần

KQKD1 <- KQKD1 %>%
  mutate(
    Toc_do_tang_truong_DT = (doanh_thu_thuan / lag(doanh_thu_thuan) - 1) * 100)

3.3.9 Tốc độ tăng trưởng lợi nhuận hoạt động kinh doanh

KQKD1 <- KQKD1 %>%
  arrange(nam) %>%
  mutate(Toc_do_tang_truong_LNHDKD = (Loi_nhuan_HDKD / lag(Loi_nhuan_HDKD) - 1) * 100
  )

3.3.10 Tốc độ tăng trưởng lợi luận gộp

KQKD1 <- KQKD1 %>%
  arrange(nam) %>%
  mutate(
Toc_do_tang_truong_LNG = (loi_nhuan_gop / lag(loi_nhuan_gop) - 1) * 100
)

3.3.11 Tốc độ tăng trưởng lợi luận sau thuế

KQKD1 <- KQKD1 %>%
  arrange(nam) %>%
  mutate(
Toc_do_tang_truong_LNST = (lnst_cong_ty_me / lag(lnst_cong_ty_me) - 1) * 100
)

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

4.1 Thống kê

4.1.1 Tỷ trọng đóng góp của các năm vào tổng doanh thu tài chính ròng

KQKD_06 <- KQKD1 %>%
  mutate(val = abs(Doanh_thu_TC_rong),
         ty = val / sum(val, na.rm=TRUE))
ggplot(KQKD_06, aes(area = ty, fill = Doanh_thu_TC_rong > 0, label = paste(nam, "\n", round(ty*100,1),"%"))) +
  geom_treemap() +
  geom_treemap_text(place = "centre", grow = TRUE, reflow = TRUE) +
  scale_fill_manual(values=c("TRUE"="green","FALSE"="tomato"), guide=FALSE) +
  labs(title="Tỷ trọng đóng góp của doanh thu theo năm") +
  theme_void()

Giải thích code:

    1. Tính Tỷ trọng Tuyệt đối: Mã tạo hai cột mới: val: Giá trị tuyệt đối (abs()) của Doanh thut thuần; ty: Tỷ trọng (phần trăm) đóng góp của val theo từng năm so với tổng val của tất cả các năm.
    1. Kích thước ô (area = ty): Tỷ lệ thuận với tỷ trọng đóng góp của năm đó.
    1. Màu nền (fill = Doanh_thu_TC_rong > 0): Tô màu Xanh Lá (TRUE) nếu doanh thu dương và Đỏ (FALSE) nếu doanh thu âm.

Nhận xét: Đóng góp của “Tài chính ròng” rất tập trung vào chỉ một vài năm đột biến (đặc biệt là 2022) và biến động cao theo thời gian.

4.1.2 Ma trận tương quan các chỉ tiêu chính

vars_corr <- KQKD1 %>%
  select(doanh_thu_thuan, loi_nhuan_gop, Loi_nhuan_HDKD, lnst_cong_ty_me, 
         Doanh_thu_TC_rong) %>%
  na.omit()
corr_mat <- cor(vars_corr)
corrplot::corrplot(
  corr_mat, 
  method = "color",
  addCoef.col = "black",
  tl.col = "black",
  tl.srt = 45,       
  number.cex = 0.7,  # Giảm cỡ chữ của các con số trong ma trận
  tl.cex = 0.8,      # Giảm cỡ chữ của tên các biến
  number.digits = 2,
  title = "\nMa trận tương quan các chỉ tiêu chính",
  mar = c(0, 0, 1, 0) # Tăng lề trên và lề phải
)

Giải thích code:

    1. na.omit(): Loại bỏ bất kỳ hàng nào chứa giá trị thiếu (NA) trong các cột đã chọn, đảm bảo tính toán tương quan chính xác.
    1. Tính toán ma trận tương quan (cor) giữa tất cả các cặp biến trong vars_corr và lưu kết quả vào biến corr_mat.
    1. Vẽ ma trận tương quan đã tính.
    1. method = “color”: Sử dụng màu sắc (độ đậm nhạt) để thể hiện giá trị tương quan.
    1. addCoef.col = “black”: Thêm hệ số tương quan dưới dạng văn bản màu đen vào giữa mỗi ô.
  • (10),(11) tl.col = “black”, tl.srt = 45: Đặt màu nhãn trục là đen và xoay nhãn 45 độ để tránh chồng lấn.

    1. number.cex = 0.8: Điều chỉnh kích thước của số hệ số tương quan được in trong ô.

Nhận xét: Ma trận tương quan cho thấy hoạt động kinh doanh cốt lõi của công ty vận hành một cách hợp lý, thể hiện qua mối tương quan thuận mạnh mẽ giữa doanh_thu_thuan và loi_nhuan_gop (0.72), và giữa loi_nhuan_gop và Loi_nhuan_HDKD (0.62). Tuy nhiên, có một điểm bất thường và đáng lo ngại nhất là mối tương quan nghịch (-0.21) giữa Loi_nhuan_HDKD và LNST_cong_ty_me (Lợi nhuận sau thuế của công ty mẹ). Điều này cho thấy lợi nhuận kiếm được từ hoạt động kinh doanh chính không chuyển hóa thành lợi nhuận cuối cùng cho cổ đông, mà thậm chí còn đi ngược chiều. Ngoài ra, Doanh_thu_TC_rong (Doanh thu tài chính ròng) gần như không liên quan hoặc hơi ngược chiều với hoạt động kinh doanh chính (-0.19), cho thấy sự tách biệt của hai mảng này.

4.1.3 Diễn biến các chỉ tiêu tài chính cốt lõi qua các năm

data_long <- KQKD1 %>%
  select(nam, doanh_thu_thuan, lnst_cong_ty_me, loi_nhuan_gop, Loi_nhuan_HDKD) %>%
  pivot_longer(
    cols = -nam, 
    names_to = "Chi_tieu", 
    values_to = "Gia_tri" 
  ) %>%
  na.omit()

ggplot(data_long, aes(x = factor(nam), y = Gia_tri, group = Chi_tieu, color = Chi_tieu)) +
  geom_line(linewidth = 1.2) + # Tăng độ dày đường kẻ
  geom_point(size = 3) +       # Thêm điểm đánh dấu cho mỗi năm
  scale_y_continuous(
    labels = label_number(
      scale = 1e-9,
      suffix = " Tỷ"
    )
  ) +
  scale_color_brewer(palette = "Set1") + # Sử dụng bảng màu dễ phân biệt
  labs(
    title = "Diễn biến Doanh thu và Lợi nhuận qua các năm",
    subtitle = "Sự phân kỳ giữa Lợi nhuận HĐKD và Lợi nhuận Sau thuế",
    x = "Năm",
    y = "Giá trị (Tỷ VND)",
    color = "Chỉ tiêu" # Đặt tên cho phần chú giải
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 16),
    plot.subtitle = element_text(hjust = 0.5, size = 12),
    legend.position = "top",
    axis.text.x = element_text(angle = 45, hjust = 1) # Xoay nhãn trục X nếu cần
  )

Giải thích code:

    1. Xoay data frame:
    1. Tên của các cột (ví dụ: “doanh_thu_thuan”, “lnst_cong_ty_me”) sẽ được gộp vào một cột mới tên là Chi_tieu.
    1. Các giá trị tương ứng của chúng sẽ được gộp vào một cột mới tên là Gia_tri.
    1. Xóa bất kỳ dòng nào có giá trị NA (thiếu dữ liệu) để đảm bảo biểu đồ được vẽ liền mạch.
    1. Thêm lớp (layer) biểu đồ đường, với nét vẽ dày.
    1. Thêm các điểm tròn tại mỗi mốc dữ liệu.
    1. Tùy chỉnh trục Y.
    1. Sử dụng hàm label_number (từ thư viện scales) để định dạng các con số.

Nhận xét: Biểu đồ cho thấy Doanh thu thuần (đỏ) luôn ở mức cao (từ 2-6.5 tỷ tỷ), nhưng 3 đường lợi nhuận còn lại bị “ép” xuống mức rất thấp, cho thấy biên lợi nhuận mỏng. Đúng như tiêu đề phụ, có sự phân kỳ rõ rệt: Lợi nhuận HĐKD (tím) và Lợi nhuận gộp (xanh lá) luôn duy trì trên mức 0 (trừ 2022), nhưng Lợi nhuậnSau thuế (xanh dương) lại liên tục rơi xuống mức âm sâu (đặc biệt là 2016 và 2020), cho thấy gánh nặng chi phí (như tài chính) là rất lớn.

4.1.4 Phân phối doanh thu thuần

ggplot(KQKD1, aes(x = doanh_thu_thuan)) +
  geom_histogram(
    bins = 5, # Đặt số bin (thùng) để trực quan hóa phân phối
    fill = "#1f78b4", # Màu xanh đại diện cho doanh thu
    color = "white"   # Viền trắng giữa các cột
  ) +
  labs(
    title = "Phân phối Doanh thu thuần của HAG (2015-2024)",
    x = "Doanh thu thuần (VND)",
    y = "Số năm (Tần suất)"
  ) +
  scale_x_continuous(
    labels = label_number(
      big.mark = ".",
      decimal.mark = ",",
      scale = 1e-9, # Chuyển đơn vị sang Tỷ VND (chia cho 10^9)
      suffix = " Tỷ"
    )
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold"),
    axis.title.x = element_text(face = "bold"),
    axis.title.y = element_text(face = "bold")
  )

Giải thích code:

    1. Ánh xạ cột doanh_thu_thuan lên trục hoành (x).
    1. Tạo biểu đồ tần suất, chiều cao cột thể hiện số năm (tần suất) có doanh thu nằm trong khoảng tương ứng.
    1. bins = 5: Đặt biểu đồ có 5 cột (bin), mỗi cột đại diện cho một phạm vi giá trị doanh thu.
    1. Định dạng hiển thị các giá trị trên trục x (Doanh thu thuần).
  • (16),(17) Chia giá trị trục x cho \(10^9\) (1e-9) và thêm hậu tố ” Tỷ”, chuyển đơn vị hiển thị sang Tỷ VND.

  • (14),(15) Sử dụng dấu chấm (.) làm dấu phân cách hàng nghìn và dấu phẩy (,) làm dấu phân cách thập phân.

  • (20),(21) Áp dụng chủ đề tối giản và căn giữa tiêu đề chính (hjust = 0.5, face = “bold”), làm đậm nhãn trục.

Nhận xét:

Biểu đồ cho thấy sự phân cực mạnh mẽ trong kết quả kinh doanh của HAG. Công ty dường như hoạt động ở hai “trạng thái” riêng biệt trong giai đoạn này:

Trạng thái tốt: 7 năm kinh doanh với doanh thu rất cao (tập trung ở 5-6 Tỷ).

Trạng thái xấu: 3 năm kinh doanh với doanh thu thấp (tập trung ở 2-3 Tỷ).

Sự thiếu vắng hoàn toàn các giá trị ở mức trung bình (quanh 4 Tỷ) cho thấy HAG không có sự tăng trưởng hay suy giảm ổn định, mà là sự biến động đột ngột giữa hai trạng thái kinh doanh rất khác nhau.

4.1.5 Box plot cho Tỷ suất Lợi nhuận gộp (Ty_suat_LNG)

plot_box_gpm <- ggplot(KQKD1, aes(y = Ty_suat_LNG)) +
  geom_boxplot(fill = "lightblue", color = "darkblue", alpha = 0.7) +
  geom_point(aes(x = 0, color = factor(nam)), size = 3, position = position_jitter(width = 0.1)) + # Jitter plot để thấy từng điểm năm
  labs(
    title = "Phân bổ và biến động của tỷ suất lợi nhuận gộp (GPM)",
    subtitle = "Giá trị Trung vị, Tứ phân vị và các điểm ngoại lai",
    x = "", 
    y = "Tỷ suất Lợi nhuận gộp (%)",
    color = "Năm"
  ) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        axis.text.x = element_blank(), # Bỏ trục x vì đây là phân tích một chiều
        axis.ticks.x = element_blank(),
        legend.position = "right")

print(plot_box_gpm)

Giải thích code:

    1. Khởi tạo đồ thị, ánh xạ Tỷ suất Lợi nhuận Gộp lên trục tung (y) (vì đây là phân tích một biến số).
    1. Thêm lớp Biểu đồ Hộp (geom_boxplot). Biểu đồ này hiển thị giá trị Trung vị, Tứ phân vị (IQR), và các điểm ngoại lai.
    1. Thêm lớp Điểm (Jitter Plot) chồng lên Box Plot để hiển thị từng giá trị quan sát (mỗi năm là một điểm).
    1. aes(x = 0, color = factor(nam)): Tất cả các điểm được đặt trên trục x tại vị trí 0 (để nằm trong box plot) và được tô màu theo nam (biến được chuyển thành yếu tố factor).
    1. position = position_jitter(width = 0.1): Áp dụng hiệu ứng jitter (rải ngẫu nhiên theo chiều ngang) để các điểm không bị chồng lên nhau, giúp dễ dàng nhìn thấy từng năm riêng biệt.
    1. Định dạng nhãn trục y, thêm ký hiệu “%” vào sau giá trị tỷ suất.
  • (12),(13) Áp dụng chủ đề tối giản. Ẩn trục x và các dấu tích của trục x (axis.text.x = element_blank()) vì đây là biểu đồ một chiều (chỉ tập trung vào trục y).

Nhận xét: Tỷ suất lợi nhuận gộp của công ty rất không ổn định qua các năm. Các điểm dữ liệu phân tán rộng, trải dài từ mức rất thấp (khoảng 7% vào năm 2020) đến mức rất cao (hơn 40% vào năm 2018). Mức lợi nhuận gộp trung vị (đường kẻ đậm ở giữa hộp) là khoảng 23%. Điều này có nghĩa là một nửa số năm công ty đạt được GPM cao hơn mức này và một nửa còn lại thì thấp hơn. Năm 2018 có tỷ suất lợi nhuận gộp cao vượt trội so với các năm khác. Các năm 2019 và 2020 có tỷ suất lợi nhuận gộp rất thấp, nằm ở đáy của phân phối.

4.1.6 Biểu đồ mật độ hợp nhất

# Chuẩn bị dữ liệu: Tạo Ty_suat_LNG và Ty_suat_LNST, sau đó pivot
data_density_compare <- KQKD1 %>%
  mutate(
    Ty_suat_LNST = (lnst_cong_ty_me / doanh_thu_thuan) * 100
    # Giả định Ty_suat_LNG đã tồn tại
  ) %>%
  select(nam, Ty_suat_LNG, Ty_suat_LNST) %>%
  pivot_longer(cols = starts_with("Ty_suat"), names_to = "Ty_suat", values_to = "Gia_tri") %>%
  filter(!is.na(Gia_tri))

plot_density_overlay <- ggplot(data_density_compare, aes(x = Gia_tri, fill = Ty_suat)) +
  geom_density(alpha = 0.5, adjust = 1.5) +
  geom_vline(aes(xintercept = median(Gia_tri)), linetype = "dashed", color = "gray50") +
  
  labs(
    title = "So sánh phân bổ tỷ suất lợi nhuận gộp (GPM) và ròng (NPM)",
    x = "Tỷ lệ (%)", 
    y = "Mật độ", 
    fill = "Loại Tỷ suất"
  ) +
  
  scale_fill_manual(values = c("Ty_suat_LNG" = "#00BFC4", "Ty_suat_LNST" = "#F8766D"),
                    labels = c("Ty_suat_LNG" = "GPM (Lợi nhuận gộp)", "Ty_suat_LNST" = "NPM (Lợi nhuận ròng)")) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "top")

print(plot_density_overlay)

Giải thích code:

    1. Chuyển dữ liệu từ định dạng rộng sang dài. Hai cột tỷ suất (Ty_suat_LNG, Ty_suat_LNST) được gộp thành một cột tên là Ty_suat, và giá trị của chúng gộp thành cột Gia_tri.
    1. Lọc dữ liệu, loại bỏ các hàng có giá trị tỷ suất bị thiếu (NA) để biểu đồ không bị lỗi.
    1. Ánh xạ Giá trị tỷ suất lên trục hoành (x) và sử dụng Loại Tỷ suất để tô màu (fill) cho các đường cong.
    1. Tạo hai đường cong mật độ chồng lên nhau. alpha=0.5 làm cho các đường cong trong suốt để có thể nhìn thấy cả hai, và adjust=1.5 làm cho đường cong mượt mà hơn.
    1. Thêm một đường thẳng đứng tại giá trị trung vị tổng thể của tất cả các tỷ suất để tham chiếu.

Nhận xét:

Phân bổ của Lợi nhuận gộp hoàn toàn nằm ở phía bên phải của mốc 0%, cho thấy công ty luôn tạo ra lợi nhuận từ hoạt động bán hàng cơ bản.

Phân bổ của Lợi nhuận ròng bị lệch hẳn sang bên trái so với Lợi nhuận gộp. Đáng chú ý là một phần đáng kể của đường cong này nằm trong vùng âm, cho thấy công ty thường xuyên bị lỗ ròng sau khi trừ đi các chi phí hoạt động, lãi vay và thuế.

Biểu đồ cho thấy một vấn đề nghiêm trọng về quản lý chi phí. Mặc dù công ty kinh doanh có lãi ở cấp độ gộp, nhưng các chi phí phát sinh sau đó quá lớn, dẫn đến kết quả lợi nhuận cuối cùng rất kém hiệu quả và không ổn định..

4.2 Xu hướng Doanh thu và Lợi nhuân

4.2.1 Xu hướng Doanh thu thuần qua các năm (Biểu đồ Đường)

plot1 <- ggplot(KQKD1, aes(x = nam, y = doanh_thu_thuan)) +
  geom_line(color = "blue", size = 1) +
  geom_point(color = "darkblue", size = 3) +
  geom_label_repel(aes(label = comma(doanh_thu_thuan, accuracy = 1)),
            , color = "darkblue", size = 3.5,
                  box.padding = 0.5, point.padding = 0.5, 
                  max.overlaps = Inf) + 
  labs(title = "1. Xu hướng Doanh thu thuần của HAG (2015-2024)",
       x = "Năm", y = "Doanh thu thuần",
       ) +
  theme_minimal() +
  scale_y_continuous(labels = comma) +
  scale_x_continuous(breaks = unique(KQKD1$nam)) + 
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))
print(plot1)

Giải thích code:

    1. Tạo biểu đồ đường và điểm để thể hiện xu hướng theo thời gian, với màu xanh dương và xanh đậm.
    1. Thêm nhãn có hộp nền cho các điểm dữ liệu trên biểu đồ một cách thông minh, tự động đẩy các nhãn ra xa nhau và ra xa điểm dữ liệu để tránh bị chồng chéo, giúp biểu đồ dễ đọc hơn.
    1. Định dạng giá trị trên trục Y bằng dấu phân cách hàng nghìn (comma).
    1. Đảm bảo trục X chỉ hiển thị đúng các giá trị năm có trong dữ liệu, không hiển thị các số thập phân.
    1. Áp dụng chủ đề tối giản. Tiêu đề biểu đồ được căn giữa và in đậm (theme(plot.title = element_text(hjust = 0.5, face = “bold”))).

Nhận xét: Hoạt động kinh doanh của HAG mang tính chu kỳ rõ rệt, không có sự ổn định. Công ty có khả năng tạo ra doanh thu rất lớn nhưng cũng phải đối mặt với nguy cơ sụt giảm sâu. Giai đoạn 2021-2023 cho thấy khả năng phục hồi mạnh mẽ của công ty.

4.2.2 Xu hướng Lợi nhuận sau thuế qua các năm

plot2 <- ggplot(KQKD1, aes(x = nam, y = lnst_cong_ty_me)) +
 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(lnst_cong_ty_me / 1e9, accuracy = 0.01),
                  vjust = ifelse(lnst_cong_ty_me >= 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 = KQKD1$nam) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"))
print(plot2)

Giải thích code:

    1. Thêm lớp cột, chiều cao thể hiện giá trị LNST. Màu nền cột được phân biệt dựa trên cột Tinh_trang_KD (Có Lãi/Lỗ).
    1. Thêm một đường tham chiếu ngang đứt nét màu đỏ tại giá trị 0 để làm ranh giới giữa lợi nhuận và lỗ.
    1. Thêm nhãn giá trị (LNST) lên trên hoặc dưới mỗi cột.(Sử dụng hàm ifelse() để điều chỉnh vị trí nhãn (vjust): nhãn được đặt phía trên cột nếu giá trị > 0 và phía dưới cột nếu giá trị < 0.
    1. Định dạng giá trị trên trục Y (LNST) bằng dấu phân cách hàng nghìn.
    1. Đảm bảo trục X chỉ hiển thị đúng các giá trị năm có trong dữ liệu.
  • (10),(14) Áp dụng chủ đề sáng, đồng thời căn giữa và in đậm tiêu đề biểu đồ.

Nhận xét: HAG có lịch sử kinh doanh rất sóng gió với 2 lần lỗ nặng (2016, 2020). Tuy nhiên, giai đoạn 4 năm gần nhất (2021-2024) cho thấy một sự thay đổi cơ bản trong hoạt động kinh doanh, mang lại lợi nhuận cao và ổn định hơn nhiều so với trước đây.

4.2.3 So sánh Doanh thu thuần và Lợi nhuận gộp

KQKD_sach_na_lng <- KQKD_sach_na %>%
    select(nam, doanh_thu_thuan, loi_nhuan_gop) %>%
    pivot_longer(-nam, names_to = "Chi_tieu", values_to = "Gia_tri")
plot3 <- ggplot(KQKD_sach_na_lng, aes(x = nam, y = Gia_tri, color = Chi_tieu)) +
 geom_line(size = 1.2) +
 geom_point(size = 3) +
    geom_label_repel(aes(label = comma(Gia_tri / 1e9, accuracy = 0.01)), 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 = KQKD_sach_na$nam) +
 theme(plot.title = element_text(hjust = 0.5, face = "bold"),
 legend.position = "bottom")
print(plot3)

Giải thích code:

    1. Chuyển dữ liệu từ định dạng rộng sang dài (giữ cột nam cố định); Hai cột doanh_thu_thuan và loi_nhuan_gop được gộp lại: tên cột cũ chuyển thành giá trị trong cột Chi_tieu, và giá trị của chúng chuyển thành cột Gia_tri.
    1. Khởi tạo đồ thị. Ánh xạ nam lên trục X, Gia_tri lên trục Y. Màu sắc (color) được dùng để phân biệt hai Chi_tieu (Doanh thu và Lợi nhuận gộp).
  • (5),(6) Tạo hai đường (line) và các điểm (point) riêng biệt thể hiện xu hướng của mỗi chỉ tiêu.

    1. Đảm bảo trục X chỉ hiển thị đúng các giá trị năm.

Nhận xét: Cả hai chỉ tiêu đều cho thấy sự biến động lớn, không ổn định qua các năm. Đặc biệt, giai đoạn 2018 - 2022 chứng kiến những sự sụt giảm và phục hồi mạnh mẽ. Doanh thu thuần đạt mức cao nhất vào khoảng năm 2015, 2017-2018 và giai đoạn 2022-2023. Giai đoạn giảm sâu nhất là vào năm 2019 và 2021. Lợi nhuận gộp cũng có xu hướng tăng giảm tương ứng với doanh thu. Tuy nhiên, có những năm tỷ suất lợi nhuận gộp (khoảng cách giữa hai đường) bị thu hẹp đáng kể, ví dụ như năm 2019 và 2021, cho thấy chi phí giá vốn hàng bán tăng cao so với doanh thu.

4.3 Phân tích Cơ cấu chi phí và mối quan hệ Chi phí - Lợi nhuận

4.3.1 Cơ cấu chi phí hoạt động qua các năm

KQKD_sach_na_cp <- KQKD1 %>%
  select(nam, chi_phi_tc, chi_phi_bh, chi_phi_ql) %>%
  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 == "chi_phi_tc" ~ "CP Tài chính",
      Loai_chi_phi == "chi_phi_bh" ~ "CP Bán hàng",
      Loai_chi_phi == "chi_phi_ql" ~ "CP Quản lý DN")
  )
plot4 <- ggplot(KQKD_sach_na_cp, aes(x = nam, y = So_tien, fill = Loai_chi_phi)) +
  geom_area(alpha = 0.9) +
  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") + # Màu sắc tương phản hơn
  scale_y_continuous(labels = scales::comma) +
  
  scale_x_continuous(
    breaks = seq(min(KQKD_sach_na_cp$nam), max(KQKD_sach_na_cp$nam), by = 2)
  ) +
  facet_wrap(~Loai_chi_phi, scales = "free_y", nrow = 1) +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold"),
    legend.position = "bottom",
    axis.text.x = element_text(angle = 45, hjust = 1)
  )
print(plot4)

Giải thích code:

    1. Đây là bước quan trọng nhất. Nó tái cấu trúc dữ liệu từ dạng rộng (nhiều cột chi phí) sang dạng dài (một cột cho loại chi phí, một cột cho số tiền).
    1. Lấy giá trị tuyệt đối của So_tien. Vì trong báo cáo tài chính, chi phí thường được ghi là số âm.
    1. Đổi tên các giá trị trong cột Loai_chi_phi từ mã kỹ thuật (như “chi_phi_tc”) sang tên đầy đủ, dễ đọc (như “CP Tài chính”) để hiển thị trên biểu đồ.
    1. Chọn loại biểu đồ là biểu đồ miền (area chart). alpha = 0.9 làm cho màu tô gần như rõ nét (không quá trong suốt).
  • scale_y_continuous(labels = scales::comma): Định dạng thêm dấu phẩy hàng nghìn trên trục Y.

    1. Tùy chỉnh trục X. breaks = seq(…) yêu cầu R chỉ hiển thị các mốc (ticks) trên trục X cách nhau 2 năm (ví dụ: 2010, 2012, 2014…).
  • facet_wrap(~Loai_chi_phi, scales = “free_y”, nrow = 1): Chia biểu đồ thành nhiều biểu đồ con (facets), mỗi biểu đồ con ứng với một giá trị trong cột Loai_chi_phi; nrow = 1: Sắp xếp tất cả các biểu đồ con này trên một hàng duy nhất.

    1. Cho phép mỗi biểu đồ con có thang đo trục Y (tung) riêng biệt.
    1. Căn giữa (hjust = 0.5) và in đậm (face = “bold”) tiêu đề.
    1. Di chuyển phần chú giải (fill) xuống dưới cùng.
    1. Xoay nhãn trên trục X một góc 45 độ để tránh bị chồng chéo lên nhau.

Nhận xét:

Chi phí Tài chính và Chi phí Quản lý DN là hai gánh nặng chi phí chính, lớn hơn nhiều so với Chi phí Bán hàng.

Công ty giảm bớt các chi phí có quy mô lớn là Chi phí Tài chính và Chi phí Quản lý DN kể từ các đỉnh điểm của chúng vào 2018-2019. Ngược lại, công ty đang gia tăng đầu tư vào Chi phí Bán hàng, và chi phí này đang trong xu hướng tăng rõ rệt, đạt đỉnh vào năm 2023.

4.3.2 Tỷ lệ Chi phí trên Doanh thu

KQKD_ty_le_cp <- KQKD1 %>%
  mutate(
    # Sử dụng giá trị tuyệt đối cho Chi phí Tài chính
    Ty_le_TC = abs(chi_phi_tc) / doanh_thu_thuan * 100, 
    Ty_le_BH = abs(chi_phi_bh) / doanh_thu_thuan * 100,
    Ty_le_QL = abs(chi_phi_ql) / doanh_thu_thuan * 100
  ) %>%
  select(nam, Ty_le_TC, Ty_le_BH, Ty_le_QL) %>%
  pivot_longer(-nam, names_to = "Loai_chi_phi", values_to = "Ty_le")

plot_ty_le_cp <- ggplot(KQKD_ty_le_cp, aes(x = nam, y = Ty_le, color = Loai_chi_phi)) +
  geom_line(size = 1.2) +
  geom_point(size = 3) +
  labs(title = "Tỷ lệ các loại chi phí hoạt động trên oanh thu thuần",
       subtitle = "Hiệu quả kiểm soát chi phí so với quy mô doanh thu",
       x = "Năm", y = "Tỷ lệ Chi phí / Doanh thu (%)", color = "Loại Chi phí") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_x_continuous(breaks = unique(KQKD_ty_le_cp$nam)) +
  scale_color_manual(values = c("Ty_le_TC" = "firebrick", 
                                "Ty_le_BH" = "darkorange", 
                                "Ty_le_QL" = "darkgreen"),
                     labels = c("Ty_le_TC" = "Tài chính", 
                                "Ty_le_BH" = "Bán hàng", 
                                "Ty_le_QL" = "Quản lý DN")) +
  theme_bw() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "top")
print(plot_ty_le_cp)

Giải thích code:

    1. Chuyển dữ liệu sang dạng dài (Long Format). Các cột tỷ lệ được gộp thành cột Loai_chi_phi (tên chi phí) và Ty_le (giá trị tỷ lệ).
    1. Khởi tạo đồ thị. Ánh xạ nam lên trục X, Ty_le lên trục Y, và dùng Loai_chi_phi để phân biệt màu sắc đường.
  • (12),(13) Tạo các đường và điểm riêng biệt cho từng loại chi phí.

    1. Định dạng trục Y, thêm ký hiệu % vào sau giá trị.
    1. Gán màu sắc cụ thể và đặt tên nhãn chú giải rõ ràng (“Tài chính”, “Bán hàng”, “Quản lý DN”) cho từng đường.

Nhận xét: Biểu đồ này giúp so sánh sự thay đổi của hiệu quả kiểm soát chi phí (Chi phí trên 1 đồng Doanh thu) giữa ba loại chi phí qua các năm.

4.3.3 Mối quan hệ giữa Lợi nhuận gộp và Chi phí tài chính

plot_LNCP <- ggplot(KQKD1, aes(x = nam)) +
  geom_col(aes(y = abs(chi_phi_tc), fill = "Chi phí Tài chính"), width = 0.6, alpha = 0.8) +
  geom_line(aes(y = loi_nhuan_gop, color = "Lợi nhuận gộp"), size = 1.2) +
  geom_point(aes(y = loi_nhuan_gop, color = "Lợi nhuận gộp"), size = 3) +
  geom_hline(yintercept = 0) +
  
  labs(title = "Lợi nhuận gộp có đủ gánh Chi phí tài chính",
       subtitle = "Sự đảo chiều từ năm 2021 khi lợi nhuận gộp vượt xa Chi phí tài chính",
       x = "Năm", y = "Số tiền (tỷ VND)") +
  scale_y_continuous(labels = scales::comma) +
  scale_x_continuous(breaks = KQKD_sach_na$nam) +
  scale_fill_manual(name = "", values = c("Chi phí Tài chính" = "firebrick")) +
  scale_color_manual(name = "", values = c("Lợi nhuận gộp" = "steelblue")) +
  theme(legend.position = "top")
print(plot_LNCP)

Giải thích code:

    1. Khởi tạo đồ thị, ánh xạ Năm (nam) lên trục X.
    1. Vẽ Biểu đồ Cột cho Chi phí Tài chính.
  • (3),(4) Vẽ Biểu đồ Đường cho Lợi nhuận gộp.

    1. Thêm một đường tham chiếu ngang tại y=0.
    1. Định dạng trục Y (Số tiền) bằng dấu phân cách hàng nghìn.
    1. Đảm bảo trục X chỉ hiển thị đúng các giá trị năm.

Nhận xét: Từ năm 2023 trở đi, công ty đã thoát khỏi gánh nặng chi phí tài chính vốn đã đè nặng lên họ trong nhiều năm. Lợi nhuận gộp giờ đây không chỉ đủ “gánh” chi phí tài chính mà còn vượt xa, tạo ra một khoảng đệm an toàn rất lớn. Điều này cho thấy sức khỏe tài chính đã trở nên lành mạnh hơn rất nhiều.

4.4 Biểu đồ về Hiệu suất sinh lời

4.4.1 Tỷ suất Lợi nhuận gộp qua các năm

plot4LNG <- ggplot(KQKD1, aes(x = nam, y = Ty_suat_LNG)) +
  geom_line(color = "darkgreen", linewidth = 1.2) +
  geom_point(aes(color = Giai_doan), size = 4) +
  ggrepel::geom_label_repel(aes(label = paste0(round(Ty_suat_LNG, 1), "%")), size = 4) +
  scale_x_continuous(breaks = KQKD1$nam) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
 labs(title = "Tỷ suất Lợi nhuận gộp (2015-2024)",
      subtitle = "Hiệu quả hoạt động cốt lõi cải thiện rõ rệt trong giai đoạn Phục hồi",
      x = "Năm", y = "Tỷ suất Lợi nhuận gộp (%)", color = "Giai đoạn")
print(plot4LNG)

Giải thích code:

    1. Tạo đường nối thể hiện xu hướng của tỷ suất LNG qua các năm.
    1. Thêm điểm dữ liệu tại mỗi năm. Các điểm này được tô màu dựa trên cột Giai_doan (giả định là một cột phân loại giai đoạn hoạt động).
    1. Thêm nhãn giá trị cho mỗi điểm.
    1. label = paste0(round(Ty_suat_LNG, 1), “%”): Hiển thị giá trị tỷ suất, làm tròn 1 chữ số thập phân, kèm ký hiệu “%”.
    1. Sử dụng geom_text_repel để các nhãn tự động tránh chồng chéo lên nhau và lên các điểm.

Nhận xét: Sau giai đoạn khủng hoảng chạm đáy (2019-2020) do gánh nặng chi phí tài chính khổng lồ và hiệu quả kinh doanh cốt lõi sụp đổ, công ty đã đảo chiều ngoạn mục. Giai đoạn 2023-2024 chứng kiến gánh nặng nợ được giải quyết triệt để, trong khi hiệu quả kinh doanh cốt lõi (biên lợi nhuận gộp) phục hồi mạnh mẽ, tăng vọt lên 37.6%. Doanh nghiệp đã vượt qua giai đoạn khó khăn nhất và tái lập một nền tảng tài chính lành mạnh để tăng trưởng.

4.4.2 Tỷ suất Lợi nhuận sau thuế qua các năm

plotLNST <- ggplot(KQKD1, aes(x = nam, y = Ty_suat_LNST)) +
  geom_line(color = "darkgreen", linewidth = 1.2) +
  geom_point(aes(color = Giai_doan), size = 4) +
  ggrepel::geom_label_repel(aes(label = paste0(round(Ty_suat_LNST, 1), "%")), size = 4) +
  scale_x_continuous(breaks = KQKD1$nam) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
 labs(title = "Tỷ suất Lợi nhuận sau thuế (2015-2024)",
      subtitle = "Hiệu quả hoạt động cốt lõi cải thiện rõ rệt trong giai đoạn phục hồi",
      x = "Năm", y = "Tỷ suất Lợi nhuận sau thuế (%)", color = "Giai đoạn")
print(plotLNST)

Nhận xét: Sau giai đoạn 2015-2019 bất ổn, công ty đã chạm đáy khủng hoảng vào năm 2020 với tỷ suất lợi nhuận ròng (NPM) âm 39.5%. Ngay lập tức, công ty đã phục hồi “thần tốc” theo hình chữ V, và đạt mức hiệu quả sinh lời cao nhất lịch sử vào 2022-2023 (đỉnh 25.8%).

4.4.3 So sánh Tỷ suất lợi nhuận gộp và Lợi nhuận sau thuế

data_ty_suat <- KQKD1 %>%
  select(nam, Ty_suat_LNG, Ty_suat_LNST) %>%
  pivot_longer(-nam, names_to = "Ty_suat", values_to = "Gia_tri")
plot_ty_suat <- ggplot(data_ty_suat, aes(x = nam, y = Gia_tri, color = Ty_suat)) +
  geom_line(size = 1.2) +
  geom_point(size = 3) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  labs(title = "So sánh tỷ suất Lợi nhuân gộp và Lợi nhuận sau thuế",
       subtitle = "Khoảng cách giữa hai đường thể hiện mức độ ảnh hưởng của
       chi phí hoạt động và tài chính",
       x = "Năm", y = "Tỷ lệ (%)", color = "Loại tỷ suất") +
  scale_y_continuous(labels = scales::percent_format(scale = 1)) + 
  scale_x_continuous(breaks = KQKD1$nam) +
  scale_color_manual(values = c("Ty_suat_LNG" = "seagreen", "Ty_suat_LNST" = "firebrick"),
                     labels = c("Ty_suat_LNG" = "Tỷ suất Lợi nhuận gộp", "Ty_suat_LNST" = "Tỷ suất Lợi nhuận sau thuế")) +
  theme(legend.position = "bottom")
print (plot_ty_suat)

Giải thích code:

    1. Chuyển dữ liệu sang dạng dài. Hai cột tỷ suất được gộp lại thành cột Gia_tri. Mục đích là để vẽ hai đường trên cùng một trục.
    1. Khởi tạo đồ thị. Ánh xạ nam lên trục X, Gia_tri lên trục Y, và sử dụng Ty_suat để phân biệt màu sắc đường.
  • (5),(6) Tạo hai đường và các điểm riêng biệt cho GPM và NPM.geom_hline(yintercept = 0, …): Thêm đường tham chiếu ngang tại y=0 (hòa vốn).

    1. Định dạng trục Y, sử dụng scales::percent_format(scale = 1) để thêm ký hiệu % vào sau giá trị.

Nhận xét: Giai đoạn 2015-2020, khoảng cách lớn giữa Tỷ suất lợi nhuận gộp (GPM-luôn dương) và Tỷ suất lợi nhuận ròng (NPM-liên tục âm) cho thấy chi phí hoạt động và tài chính đã “ăn mòn” toàn bộ thành quả kinh doanh. Từ 2021 khoảng cách thu hẹp đột ngột, NPM dương trở lại và thậm chí giao cắt GPM (2022-2023). Điều này khẳng định công ty đã kiểm soát chi phí thành công và hiệu quả sinh lời tổng thể được cải thiện vượt bậc.

4.4.4 Xu hướng Lợi nhuận HĐKD và Lợi nhuận Sau thuế

XH_loi_nhuan <- ggplot(KQKD1, aes(x = nam)) +
  geom_line(aes(y = Loi_nhuan_HDKD, color = "Lợi nhuận HĐKD"), size = 1.2) +
  geom_line(aes(y = lnst_cong_ty_me, color = "Lợi nhuận Sau thuế"), size = 1.2, linetype = "dashed") +
  geom_point(aes(y = Loi_nhuan_HDKD), color = "seagreen", size = 3) +
  geom_point(aes(y = lnst_cong_ty_me), color = "firebrick", size = 3) +
  geom_hline(yintercept = 0, linetype = "dotted") +
  scale_y_continuous(labels = scales::comma) +
  scale_x_continuous(breaks = KQKD1$nam) +
  scale_color_manual(name = "Chỉ tiêu", values = c("Lợi nhuận HĐKD" = "seagreen", "Lợi nhuận sau thuế" = "firebrick")) +
  theme(legend.position = "top") +
  labs(title = "Lợi nhuận HĐKD so với Lợi nhuận sau thuế",
       subtitle = "Khoảng cách giữa hai đường thể hiện gánh nặng của chi phí tài chính",
       x = "Năm", y = "Giá trị (tỷ VND)")
print(XH_loi_nhuan)

Giải thích code:

    1. Thêm một đường tham chiếu ngang đứt quãng tại y=0 (điểm hòa vốn) để dễ dàng xác định các năm có lợi nhuận dương/âm.

Nhận xét: Giai đoạn 2015-2022: Lợi nhuận HĐKD (đường xanh) luôn dương, nhưng Lợi nhuận Sau thuế (đường đỏ) lại liên tục âm nặng, đặc biệt vào 2016 và 2020. Khoảng cách khổng lồ giữa hai đường xác nhận gánh nặng Chi phí Tài chính đã “ăn mòn” toàn bộ thành quả kinh doanh. Bước ngoặt xảy ra vào 2023-2024: khoảng cách này thu hẹp đột ngột, giúp Lợi nhuận Sau thuế lần đầu tiên có lãi dương trở lại và tăng trưởng, khẳng định công ty đã kiểm soát được chi phí tài chính hiệu quả.

4.5 Phân tích Tốc độ tăng trưởng

4.5.1 Tốc độ tăng trưởng Lợi nhuận sau thuế

KQKD_lnst <- KQKD1 %>%
  select(nam, Toc_do_tang_truong_LNST) %>% 
  filter(!is.na(Toc_do_tang_truong_LNST), !is.infinite(Toc_do_tang_truong_LNST))
plot_lnst_growth <- ggplot(KQKD_lnst, aes(x = nam, y = Toc_do_tang_truong_LNST, fill = Toc_do_tang_truong_LNST > 0)) +
  geom_col(width = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  geom_text(aes(label = paste0(round(Toc_do_tang_truong_LNST, 0), "%"),
                vjust = ifelse(Toc_do_tang_truong_LNST >= 0, -0.5, 1.5)), 
            size = 3.5) +
            
  labs(title = "Tốc độ tăng trưởng Lợi nhuận sau thuế (YoY)",
       x = "Năm", y = "Tỷ lệ tăng trưởng (%)", fill = "Tăng trưởng dương") +
       
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_x_continuous(breaks = unique(KQKD_lnst$nam)) +
  scale_fill_manual(values = c("TRUE" = "darkgreen", "FALSE" = "tomato")) +
  
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "none") 
        
print(plot_lnst_growth)

Giải thích code:

    1. Loại bỏ các hàng có giá trị NA (thiếu) hoặc Infinite (vô hạn), vì các giá trị này thường xảy ra khi tính tốc độ tăng trưởng từ giá trị 0 và không thể vẽ trên biểu đồ.
    1. Thêm đường tham chiếu ngang đứt nét tại \(y=0\) để làm ranh giới giữa tăng trưởng dương và âm.
    1. Thêm nhãn giá trị (tỷ lệ phần trăm đã làm tròn) lên trên/dưới mỗi cột.

Nhận xét: Giai đoạn 2016-2021 liên tục chứng kiến sụt giảm thảm hại, chạm đáy khủng hoảng với mức tăng trưởng âm 680% vào năm 2020. Ngay sau đó, công ty đã có cú “đại nhảy vọt” ngoạn mục, tăng trưởng bùng nổ 456% vào năm 2022, trước khi chững lại vào 2023-2024.

4.5.2 Tốc độ tăng trưởng Lợi nhuận gộp

KQKD_lng <- KQKD1 %>%
  select(nam, Toc_do_tang_truong_LNG) %>% 
  filter(!is.na(Toc_do_tang_truong_LNG), !is.infinite(Toc_do_tang_truong_LNG))
plot_lng_growth <- ggplot(KQKD_lng, aes(x = nam, y = Toc_do_tang_truong_LNG, fill = Toc_do_tang_truong_LNG > 0)) +
  geom_col(width = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "black") +
  geom_text(aes(label = paste0(round(Toc_do_tang_truong_LNG, 0), "%"),
                vjust = ifelse(Toc_do_tang_truong_LNG >= 0, -0.5, 1.5)), 
            size = 3.5) +
  labs(title = "Tốc độ tăng trưởng Lợi nhuận gộp",
       x = "Năm", y = "Tỷ lệ tăng trưởng (%)", fill = "Tăng trưởng Dương") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_x_continuous(breaks = unique(KQKD_lnst$nam)) +
  scale_fill_manual(values = c("TRUE" = "darkgreen", "FALSE" = "tomato")) +
  
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "none") 
print(plot_lng_growth)

Nhận xét: Giai đoạn 2016-2020 chứng kiến 3 năm tăng trưởng âm, đặc biệt là cú sụt giảm sâu -90% vào năm 2019. Doanh nghiệp đã bùng nổ ngoạn mục trong hai năm 2021 và 2022. Tốc độ tăng trưởng chững lại rõ rệt trong năm 2023 khi chỉ còn tăng 10%. Đến năm 2024, đà tăng trưởng đã phục hồi mạnh mẽ trở lại, đạt mức 68%.

4.5.3 Tốc độ Tăng trưởng của các Chi phí Hoạt động Chính

KQKD_cp_growth <- KQKD1 %>%
  mutate(
    # Lấy giá trị tuyệt đối của chi phí trước khi tính tăng trưởng
    cp_bh_abs = abs(chi_phi_bh),
    cp_ql_abs = abs(chi_phi_ql),
    # Tính tốc độ tăng trưởng chi phí Bán hàng và QLDN
    CP_BH_Growth = (cp_bh_abs / lag(cp_bh_abs) - 1) * 100,
    CP_QL_Growth = (cp_ql_abs / lag(cp_ql_abs) - 1) * 100
  ) %>%
  select(nam, CP_BH_Growth, CP_QL_Growth) %>%
  pivot_longer(-nam, names_to = "Loai_Chi_phi", values_to = "Ty_le_tang_truong") %>%
  filter(!is.na(Ty_le_tang_truong), !is.infinite(Ty_le_tang_truong))
plot_cp_growth <- ggplot(KQKD_cp_growth, aes(x = nam, y = Ty_le_tang_truong, color = Loai_Chi_phi)) +
  geom_line(size = 1.2) +
  geom_point(size = 3) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
  labs(title = "Tốc độ Tăng trưởng chi phí hoạt động (Bán hàng và QLDN)",
       subtitle = "So sánh với tốc độ tăng trưởng doanh thu để 
      đánh giá hiệu quả kiểm soát chi phí",
       x = "Năm", y = "Tỷ lệ tăng trưởng (%)", color = "Loại chi phí") +
  scale_color_manual(values = c("CP_BH_Growth" = "orange", "CP_QL_Growth" = "purple"),
                     labels = c("CP_BH_Growth" = "Chi phí Bán hàng", "CP_QL_Growth" = "Chi phí QLDN")) +
  scale_x_continuous(breaks = unique(KQKD_cp_growth$nam)) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  theme_bw() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "top")
print(plot_cp_growth)

Giải thích code:

    1. Chuyển dữ liệu sang dạng dài, gộp hai cột tốc độ tăng trưởng thành cột Loai_Chi_phi và Ty_le_tang_truong.
    1. Lọc dữ liệu, loại bỏ các giá trị bị thiếu hoặc vô hạn, thường xảy ra ở năm đầu tiên hoặc khi giá trị cơ sở là 0.
  • (14),(15) Tạo hai đường và điểm riêng biệt thể hiện xu hướng tăng trưởng của mỗi loại chi phí.

    1. Thêm đường tham chiếu ngang đứt nét tại \(y=0\), đánh dấu ranh giới giữa tăng trưởng và suy giảm.

Nhận xét: Đặc biệt năm 2022 có chi phí QLDN “bùng nổ”, với tốc độ tăng trưởng phi mã (lên đến hơn 500% vào 2022). Ngay sau đó, chi phí này được “ghìm” lại và tăng trưởng âm vào 2021 và 2023. Ngược lại, Chi phí Bán hàng (cam) tăng trưởng ổn định và dễ đoán hơn nhiều.

4.5.4 Tăng trưởng Doanh thu và Lợi nhuận

KQKD_ty_le <- KQKD1 %>%
  select(nam, Toc_do_tang_truong_DT, Toc_do_tang_truong_LNG) %>% 
  pivot_longer(-nam, names_to = "Chi_tieu", values_to = "Ty_le_tang_truong")

plot_growth_line <- ggplot(KQKD_ty_le, aes(x = nam, y = Ty_le_tang_truong, color = Chi_tieu)) +
  geom_line(aes(group = Chi_tieu), size = 1.2) +
  geom_point(size = 3) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  labs(
    title = "Tốc độ tăng trưởng Doanh thu và Lợi nhuận gộp",
    x = "Năm", y = "Tỷ lệ tăng trưởng (%)", color = "Chỉ tiêu"
  ) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_x_continuous(breaks = unique(KQKD_ty_le$nam)) +
  scale_color_manual(
    values = c("Toc_do_tang_truong_DT" = "darkblue", "Toc_do_tang_truong_LNG" = "orange"),
    labels = c("Toc_do_tang_truong_DT" = "Tốc độ tăng trưởng DT Thuần", "Toc_do_tang_truong_LNG" = "Tốc độ tăng trưởng LN Gộp")
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold"),
    legend.position = "top" # Di chuyển chú giải lên trên
  )
print(plot_growth_line)

Giải thích code:

Nhận xét: Tốc độ tăng trưởng của doanh nghiệp mang tính chu kỳ và bất ổn định rất cao. Cả doanh thu (xanh) và lợi nhuận gộp (cam) đều trải qua những cú sụt giảm sâu (như 2019, 2023) xen kẽ với các giai đoạn phục hồi bùng nổ (2017, 2021-2022). Đáng chú ý, tốc độ tăng trưởng lợi nhuận gộp luôn biến động với biên độ lớn hơn doanh thu. Điều này hàm ý rằng biên lợi nhuận gộp của công ty không ổn định, bị bào mòn rất mạnh khi doanh thu giảm và cũng được khuếch đại nhanh khi doanh thu phục hồi.

KẾT LUẬN CHUNG

Bài tiểu luận đã trình bày một cách toàn diện về năng lực và tính ứng dụng của ngôn ngữ lập trình R trong lĩnh vực phân tích dữ liệu kinh doanh thông qua hai nghiên cứu tình huống điển hình.

1. Tổng kết các kết quả chính:

2. Khẳng định vai trò của Ngôn ngữ R:

Qua hai ví dụ trên, có thể thấy R không chỉ là một công cụ tính toán mà còn là một hệ sinh thái mạnh mẽ cho phép thực hiện toàn bộ quy trình khoa học dữ liệu. Từ việc làm sạch, biến đổi dữ liệu (dplyr, tidyr) đến việc phân tích thống kê và trực quan hóa các insight phức tạp một cách sinh động (ggplot2), R đã chứng tỏ là một công cụ không thể thiếu cho các nhà phân tích kinh doanh hiện đại.

3. Hạn chế của bài tiểu luận:

Mặc dù đã đạt được các mục tiêu đề ra, bài tiểu luận vẫn còn một số hạn chế. Thứ nhất, bộ dữ liệu bán lẻ xe đạp là dữ liệu giả định, có thể không phản ánh hết sự phức tạp của thị trường thực tế. Thứ hai, phân tích tài chính HAG chỉ tập trung vào báo cáo kết quả kinh doanh và chưa kết hợp các yếu tố vĩ mô hay các báo cáo tài chính khác để có cái nhìn đa chiều hơn. Cuối cùng, các phân tích chủ yếu dừng lại ở mức độ mô tả mà chưa đi sâu vào các mô hình dự báo hay phân tích nhân quả.

4. Hướng phát triển trong tương lai:

Dựa trên nền tảng của bài tiểu luận này, các nghiên cứu tiếp theo có thể được phát triển theo hướng:

Tóm lại, bài tiểu luận đã hoàn thành mục tiêu ứng dụng R để khai phá những câu chuyện kinh doanh ẩn sau các con số, qua đó khẳng định giá trị to lớn của phân tích dữ liệu trong việc ra quyết định chiến lược