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:
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.
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.
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ể.
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.
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.
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.
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
##
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
##
## group_rows
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
##
## Attaching package: 'scales'
## The following object is masked from 'package:readr':
##
## col_factor
library(ggplot2)
library(moments)
library(ggridges)
library(treemapify)
library(ggalluvial)
library(fmsb)
library(corrplot)## corrplot 0.95 loaded
Nhóm 1: Xử lý và Thao tác Dữ liệu
readr: Dùng để đọc dữ liệu từ các tệp văn bản (như file .csv) vào R một cách nhanh và hiệu quả.
dplyr: Là thư viện “xương sống” cho việc xử lý dữ liệu. Cung cấp các hàm mạnh mẽ (mutate, filter, group_by, summarise) để làm sạch, biến đổi và tóm tắt dữ liệu.
lubridate: Giúp xử lý các dữ liệu dạng ngày tháng và thời gian trở nên cực kỳ đơn giản (ví dụ: lấy ra năm, tháng, ngày, thứ…).
zoo: Thường được dùng cho phân tích chuỗi thời gian, trong bài của bạn nó được dùng cho hàm rollmean để tính trung bình động.
Nhóm 2: Trình bày Báo cáo và Bảng biểu
knitr: Là “công cụ” cốt lõi để biên dịch (knit) file R Markdown của bạn thành các định dạng báo cáo như HTML hoặc PDF.
DT (DataTables): Tạo ra các bảng dữ liệu HTML có tính tương tác cao, cho phép người dùng lọc, sắp xếp, và tìm kiếm trực tiếp trên bảng.
kableExtra: Dùng để “trang điểm”, làm cho các bảng tĩnh được tạo bởi knitr::kable trở nên đẹp và chuyên nghiệp hơn (thêm sọc, màu sắc, cuộn…).
Nhóm 3: Trực quan hóa Dữ liệu (Vẽ biểu đồ)
ggplot2: Là thư viện vẽ biểu đồ mạnh mẽ và phổ biến nhất trong R. Hầu hết các biểu đồ trong bài của bạn đều được vẽ bằng gói này.
scales: Hỗ trợ ggplot2 trong việc định dạng lại các nhãn trên trục hoặc chú thích (ví dụ: thêm ký hiệu đô la $ hay phần trăm %).
corrplot: Chuyên dùng để vẽ ma trận tương quan (correlogram) một cách đẹp mắt và dễ hiểu.
treemapify: Vẽ biểu đồ dạng Treemap, giúp hiển thị dữ liệu có cấu trúc phân cấp dưới dạng các hình chữ nhật lồng nhau.
ggridges: Vẽ biểu đồ mật độ dạng “dãy núi” (Ridgeline plots), rất hữu ích để so sánh sự phân bố của dữ liệu giữa nhiều nhóm khác nhau.
ggalluvial: Vẽ biểu đồ Alluvial và Sankey, dùng để minh họa “dòng chảy” của dữ liệu từ nhóm này sang nhóm khác.
fmsb: Dùng để vẽ các loại biểu đồ ít phổ biến hơn như biểu đồ radar (biểu đồ mạng nhện).
Nhóm 4: Thống kê
moments: Dùng để tính toán các chỉ số thống kê mô tả nâng cao như độ xiên (Skewness) và độ nhọn (Kurtosis) của một phân phối dữ liệu.
## Rows: 100000 Columns: 11
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Date, Bike_Model, Store_Location, Payment_Method, Customer_Gender
## dbl (6): Sale_ID, Customer_ID, Price, Quantity, Salesperson_ID, Customer_Age
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Gói dplyr được sử dụng để làm sạch, chọn lọc và biến đổi dữ liệu, tạo thuận lợi cho quá trình thống kê mô tả và phân tích định lượng ở các bước sau.
Gói readr được dùng để nhập dữ liệu từ tệp .csv vào R, đảm bảo tốc độ và độ chính xác cao.
Sau khi cài đặt và kích hoạt các gói. Dữ liệu sẽ được đọc vào được lưu dưới têng BK để tiện thao tác trong quá trình xử lý.
datatable(
head(BK, 10),
options = list(
pageLength = 10,
autoWidth = TRUE
),
class = "display nowrap",
filter = "top",
rownames = FALSE
)datatable(…) là hàm chính của gói DT, dùng để tạo bảng tương tác.Bên trong ngoặc () bạn truyền vào dữ liệu cần hiển thị — ở đây là head(BK, 10). Hàm head() được dùng để trích xuất 10 quan sát đầu tiên từ bộ dữ liệu BK, nhằm minh họa cấu trúc và các biến trong tập dữ liệu.
Các tùy chọn pageLength và autoWidth giúp bảng dữ liệu hiển thị linh hoạt, cho phép tự động căn chỉnh kích thước cột.
class = “display nowrap” thiết lập kiểu hiển thị bảng: “display” là kiểu mặc định của DataTables; “nowrap” giúp bảng không xuống dòng, đảm bảo mỗi dòng dữ liệu hiển thị trên một hàng duy nhất (dễ quan sát hơn).
filter = “top”: Tạo ô lọc dữ liệu ở phía trên đầu bảng.
rownames = FALSE: Ẩn cột số thứ tự mặc định của R.
## Số biến trong bộ dữ liệu là: 11
## Số quan sát trong bộ dữ liệu là: 100000
Dùng cat() để hiển thị một câu thông báo kết hợp cả chữ và số trong báo cáo của mình một cách tự nhiên và sạch sẽ nhất.
data_summary_auto <- data.frame(
Ten_Cot = names(BK),
Loai_Du_lieu = unname(sapply(BK, class)),
Giai_thich = c("Mã số bán hàng.",
"Ngày bán hàng.",
"Mã khách hàng.",
"Tên hoặc loại xe đạp.",
"Giá bán.",
"Số lượng bán.",
"Vị trí của hàng.",
"Mã nhân viên bán hàng.",
"Phương thức thanh toán.",
"Tuổi của khách hàng.",
"Giới tính của khách hàng.")
)
# In bảng tóm tắt ra
kable(data_summary_auto,
caption = "Bảng Tóm Tắt Tên Cột và Kiểu Dữ liệu",
col.names = c("Tên Biến", "Loại Dữ liệu trong R", "Ý nghĩa")) %>%
kable_styling(bootstrap_options = "striped", full_width = F)| Tên Biến | Loại Dữ liệu trong R | Ý nghĩa |
|---|---|---|
| Sale_ID | numeric | Mã số bán hàng. |
| Date | character | Ngày bán hàng. |
| Customer_ID | numeric | Mã khách hàng. |
| Bike_Model | character | Tên hoặc loại xe đạp. |
| Price | numeric | Giá bán. |
| Quantity | numeric | Số lượng bán. |
| Store_Location | character | Vị trí của hàng. |
| Salesperson_ID | numeric | Mã nhân viên bán hàng. |
| Payment_Method | character | Phương thức thanh toán. |
| Customer_Age | numeric | Tuổi của khách hàng. |
| Customer_Gender | character | Giới tính của khách hàng. |
so_bien_dinh_luong <- data_summary_auto %>%
filter(Loai_Du_lieu == "numeric") %>%
nrow()
cat("Số biến định lượng:", so_bien_dinh_luong, "\n")## Số biến định lượng: 6
Các biến định lượng thể hiện các thông số số học, đo lường giá trị và khối lượng giao dịch, bao gồm: Sale_ID, Customer_ID, Salesperson_ID, Price, Quantity, và Customer_Age. Biến Date mặc dù được lưu dưới dạng ngày tháng, cũng có thể được chuyển đổi thành dạng định lượng để phân tích theo thời gian. Những biến định lượng này hỗ trợ các phân tích thống kê mô tả, tính toán doanh thu, lợi nhuận và mô hình dự báo.
so_bien_dinh_tinh <- data_summary_auto %>%
filter(Loai_Du_lieu == "character") %>%
nrow()
cat("Số biến định tính:", so_bien_dinh_tinh, "\n")## Số biến định tính: 5
Các biến định tính mô tả các đặc điểm phân loại và chất lượng của khách hàng, sản phẩm và giao dịch, bao gồm: Bike_Model, Store_Location, Payment_Method, Customer_Gender, và Date. Những biến này cho phép phân tích phân phối, nhóm khách hàng, đánh giá hiệu suất nhân viên và các đặc trưng sản phẩm.
## Tổng số giá trị bị thiếu (NA): 0
##
## Tổng số dòng bị trùng lặp hoàn toàn: 0
Giải thích code: - Hàm is.na(): sẽ quét qua từng ô trong toàn bộ dataframe BK. Nó sẽ trả về một dataframe mới có cùng kích thước, nhưng thay vì chứa dữ liệu gốc, nó sẽ chứa các giá trị TRUE (nếu ô gốc bị thiếu, tức là NA) hoặc FALSE (nếu ô gốc có dữ liệu).
Hàm duplicated(): sẽ quét qua từng dòng (row) của dataframe BK. Nó so sánh mỗi dòng với tất cả các dòng đã xuất hiện trước nó. Nếu một dòng giống hệt (tất cả các giá trị trong các cột đều giống nhau) với một dòng nào đó ở phía trên, nó sẽ trả về TRUE cho dòng đó. Ngược lại, nó trả về FALSE.
Hàm sum(…): cho một tập hợp các giá trị TRUE/FALSE, R sẽ tự động coi TRUE = 1 và FALSE = 0.Kết quả cuối cùng chính là tổng số dữ liệu cần tính.
Nhận xét: Dữ liệu rất sạch, không có giá trị bị thiếu (NA) và không có dòng nào bị trùng lặp hoàn toàn.
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
## Số lượng giao dịch có Số lượng (Quantity) bằng 0 là: 0
## Sale_ID Customer_ID Price Quantity Salesperson_ID
## Min. : 1 Min. :1000 Min. : 200 Min. :1.000 Min. :100.0
## 1st Qu.: 25001 1st Qu.:3249 1st Qu.:1400 1st Qu.:2.000 1st Qu.:324.0
## Median : 50001 Median :5491 Median :2599 Median :3.000 Median :550.0
## Mean : 50001 Mean :5495 Mean :2598 Mean :2.997 Mean :549.9
## 3rd Qu.: 75000 3rd Qu.:7738 3rd Qu.:3796 3rd Qu.:4.000 3rd Qu.:775.0
## Max. :100000 Max. :9999 Max. :5000 Max. :5.000 Max. :999.0
## Customer_Age
## Min. :18.00
## 1st Qu.:31.00
## Median :44.00
## Mean :44.04
## 3rd Qu.:57.00
## Max. :70.00
Giải thích code:
Hàm summary() tính toán các thống kê mô tả (như trung bình, trung vị, min, max,…) cho từng cột số đó và hiển thị kết quả.
Hàm select(where(is.numeric)) lọc và chỉ giữ lại những cột có kiểu dữ liệu là số từ BK.
Toán tử %>% lấy BK và chuyển nó đến hàm select().
Ý nghĩa:
Đối với các biến mang tính chất định danh (ID), các chỉ số như Trung bình (Mean) hay Trung vị (Median) không mang ý nghĩa về mặt phân tích kinh doanh. Tuy nhiên, giá trị Nhỏ nhất (Min) và Lớn nhất (Max) lại rất hữu ích. Ví dụ, Sale_ID chạy từ 1 đến 100,000 cho thấy dữ liệu có vẻ đầy đủ. Tương tự, Customer_ID và Salesperson_ID cho chúng ta thấy khoảng mã định danh hợp lệ của khách hàng và nhân viên.
Price (Giá bán): Giá xe dao động từ 200 USD (Min) đến 5,000 USD (Max), cho thấy danh mục sản phẩm của cửa hàng khá đa dạng, từ các dòng xe phổ thông đến cao cấp. Giá trị Trung bình (Mean = 2598) và Trung vị (Median = 2599) rất gần nhau. Đây là một dấu hiệu quan trọng cho thấy sự phân bổ của giá bán là tương đối đối xứng, không bị lệch nhiều bởi các sản phẩm có giá quá cao hoặc quá thấp.
Quantity (Số lượng): Khách hàng mua từ 1 (Min) đến 5 (Max) sản phẩm trong một lần giao dịch. Khoảng tứ phân vị (giữa 1st Qu. = 2 và 3rd Qu. = 4) cho thấy 50% số giao dịch của cửa hàng có số lượng bán ra từ 2 đến 4 chiếc. Giá trị trung bình cũng xấp xỉ 3, khẳng định đây là quy mô giao dịch phổ biến nhất.
Customer_Age (Tuổi khách hàng): Độ tuổi khách hàng rất rộng, từ 18 (Min) đến 70 (Max), cho thấy sản phẩm phù hợp với nhiều thế hệ. Đây là thông tin giá trị nhất. Khoảng tứ phân vị cho thấy 50% khách hàng cốt lõi của cửa hàng nằm trong độ tuổi từ 31 đến 57 tuổi. Cả giá trị trung bình và trung vị đều là 44, một lần nữa khẳng định nhóm khách hàng chính là những người ở độ tuổi trung niên.
Kết luận sơ bộ: Qua bước tóm tắt nhanh, có thể thấy dữ liệu định lượng hoàn toàn hợp lý, không có giá trị bất thường. Các biến kinh doanh chính có sự phân bổ khá cân đối và đã cung cấp những gợi ý ban đầu rất quan trọng về dải sản phẩm và nhóm khách hàng mục tiêu của chuỗi cửa hàng.
BK %>%
count(Store_Location, sort = TRUE) %>%
kable(caption = "Số lượng giao dịch tại mỗi cửa hàng")| Store_Location | n |
|---|---|
| New York | 14515 |
| Phoenix | 14385 |
| Philadelphia | 14330 |
| San Antonio | 14300 |
| Chicago | 14207 |
| Houston | 14149 |
| Los Angeles | 14114 |
Hàm count(): Đây là một hàm tiện lợi từ gói dplyr, chuyên dùng để đếm số lần xuất hiện của mỗi giá trị duy nhất trong một hoặc nhiều cột.
Tham số sort = TRUE: Tùy chọn này tự động sắp xếp kết quả theo thứ tự giảm dần của tần suất (cột n), giúp chúng ta ngay lập tức thấy được danh mục nào là phổ biến nhất.
Kết quả: Cửa hàng tại New York có số lượng giao dịch cao nhất với 14,515 lượt mua bán.Mặc dù có sự chênh lệch, nhưng khoảng cách về số lượng giao dịch giữa cửa hàng đứng đầu (New York, 14,515) và cửa hàng đứng cuối (Los Angeles, 14,114) là không quá lớn. Điều này cho thấy các chi nhánh đang hoạt động với quy mô tương đối đồng đều.
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.
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.
## [1] "2022-07-11" "2024-05-03" "2022-09-01" "2022-09-28" "2021-01-05"
## [6] "2021-09-06"
## [1] "Date"
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.
Tình huống giả định: Giả sử có 500 quan sát bị thiếu thông tin về tuổi khách hàng . Giải pháp: Một cách tiếp cận phổ biến là thay thế các giá trị thiếu bằng giá trị trung bình của toàn bộ cột.
# Tạo một bản sao để minh họa
BK_dirty <- BK
set.seed(456)
na_indices <- sample(1:nrow(BK_dirty), 500)
BK_dirty$Customer_Age[na_indices] <- NA
# Tính tuổi trung bình (loại bỏ các giá trị NA khi tính)
mean_age <- mean(BK_dirty$Customer_Age, na.rm = TRUE)
# Điền các giá trị NA bằng tuổi trung bình đã làm tròn
BK_dirty$Customer_Age[is.na(BK_dirty$Customer_Age)] <- round(mean_age)
# Kiểm tra lại số lượng giá trị thiếu
sum(is.na(BK_dirty$Customer_Age))## [1] 0
Giải thích code:
BK_dirty$Customer_Age[na_indices] <- NA: tạo ra 500 giá trị bị thiếu (NA) trong cột Customer_Age để minh họa việc xử lý dữ liệu.
mean_age <- mean(BK_dirty$Customer_Age, na.rm = TRUE): tính tuổi trung bình của cột Customer_Age. Tham số na.rm = TRUE là quan trọng nhất, nó yêu cầu hàm mean phải bỏ qua tất cả các ô bị thiếu (NA) khi tính toán.
BK_dirty\(Customer_Age[is.na(BK_dirty\)Customer_Age)] <- round(mean_age): đây là bước xử lý chính. Code này tìm tất cả các ô đang là NA trong cột Customer_Age và thay thế chúng bằng giá trị tuổi trung bình đã tính ở bước trên (đã được làm tròn).
Ghi chú: Cho các phân tích tiếp theo, chúng ta
sẽ tiếp tục sử dụng bộ dữ liệu BK gốc không có giá trị
thiếu.
Tình huống giả định: Giả sử do lỗi nhập liệu, một
vài giao dịch có giá trị Price bất thường (ví dụ: bằng 0
hoặc quá cao).
Giải pháp: Thay thế các giá trị ngoại lai này bằng một giá trị hợp lý hơn, ở đây là giá trị trung vị (median) của từng loại xe.
# Tạo bản sao để minh họa
BK_outliers <- BK
BK_outliers$Price[sample(1:nrow(BK_outliers), 5)] <- c(0, 150000, 5, 95000, 1)
# Xử lý ngoại lai
BK_cleaned <- BK_outliers %>%
group_by(Bike_Model) %>%
mutate(Median_Price = median(Price)) %>%
ungroup() %>%
mutate(Price = ifelse(Price < 100 | Price > 10000, Median_Price, Price)) %>%
select(-Median_Price)
# Hiển thị tóm tắt giá sau khi làm sạch để so sánh
summary(BK_outliers$Price)## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0 1400 2599 2601 3796 150000
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 200 1400 2598 2598 3796 5000
Giải thích code:
sample(1:nrow(BK_outliers), 5): Lệnh này chọn ra 5 vị trí (chỉ số hàng) ngẫu nhiên trong data frame.
mutate(): Là hàm dùng để tạo ra một cột mới hoặc chỉnh sửa một cột hiện có.
ifelse(điều_kiện, giá_trị_nếu_đúng, giá_trị_nếu_sai): Đây là một hàm điều kiện. Nếu giá là một ngoại lai (thỏa mãn điều kiện), nó sẽ được thay thế bằng giá trị Median_Price (trung vị của loại xe đó) mà chúng ta đã tính ở bước trên và ngược lại.
Ghi chú: Chúng ta sẽ tiếp tục sử dụng bộ dữ liệu
BK gốc cho các phân tích sau.
Đây là bước quan trọng nhất để làm giàu thông tin. Chúng ta sẽ tạo
một bản sao BK1 từ BK gốc và thực hiện tất cả
các thao tác trên một chuỗi duy nhất.
# Bước tiền xử lý: Đếm số lần mua hàng của mỗi khách hàng trước
customer_frequency <- BK %>%
group_by(Customer_ID) %>%
summarise(Purchase_Count = n(), .groups = 'drop')
# Bắt đầu chuỗi xử lý để tạo ra dataframe BK1 hoàn chỉnh
BK1 <- BK %>%
# Nối thông tin tần suất mua hàng vào bảng chính
left_join(customer_frequency, by = "Customer_ID") %>%
# Bắt đầu tạo và biến đổi các cột mới
mutate(
# 1. Tạo biến Doanh thu
Revenue = Price * Quantity,
# 2. Trích xuất thông tin Năm, Tháng, Thứ từ cột Date
Year = year(Date),
Month = month(Date, label = TRUE, abbr = FALSE),
Weekday = wday(Date, label = TRUE, abbr = FALSE),
# 3. Tạo biến Mùa (Season)
Season = case_when(
month(Date) %in% c(3, 4, 5) ~ "Xuân",
month(Date) %in% c(6, 7, 8) ~ "Hạ",
month(Date) %in% c(9, 10, 11) ~ "Thu",
TRUE ~ "Đông"
),
# 4. Phân tổ theo nhóm tuổi
Age_Group = case_when(
Customer_Age <= 35 ~ "Young (18-35)",
Customer_Age <= 55 ~ "Middle-Aged (36-55)",
TRUE ~ "Senior (56+)"
),
# 5. Phân tổ theo mức giá
Price_Level = case_when(
Price <= 2600 ~ "Phổ thông",
Price < 4000 ~ "Cao cấp",
TRUE ~ "Hạng sang"
),
# 6. Tạo biến Hạng khách hàng thân thiết (dựa vào Purchase_Count đã join ở trên)
Loyalty_Status = case_when(
Purchase_Count >= 10 ~ "Platinum",
Purchase_Count >= 5 ~ "Gold",
Purchase_Count >= 2 ~ "Silver",
TRUE ~ "Bronze"
),
# 7. Mã hóa Dữ liệu (Encoding) Giới tính
Gender_Encoded = case_when(
Customer_Gender == "Male" ~ 0,
Customer_Gender == "Female" ~ 1,
TRUE ~ 2 # Dành cho "Other"
)
)Sau khi thực hiện tất cả các bước trên, bộ dữ liệu của chúng ta đã được mở rộng và làm giàu đáng kể.
# Hiển thị 10 dòng đầu của bộ dữ liệu đã được xử lý hoàn chỉnh
datatable(head(BK1, 10),
rownames = FALSE,
options = list(scrollX = TRUE), # Thêm thanh cuộn ngang
caption = "Dữ liệu sau khi được xử lý và làm giàu")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.Sau khi đã xử lý và làm giàu dữ liệu, chương này sẽ tập trung vào việc thực hiện các phép tính thống kê cơ bản để trả lời các câu hỏi kinh doanh cụ thể, từ đó rút ra những insight ban đầu về hoạt động của doanh nghiệp.
# Lấy các cột số có ý nghĩa phân tích (loại bỏ các cột ID không cần thiết)
numeric_vars <- BK %>%
select(where(is.numeric)) %>%
select(-ends_with("_ID"))
# Tính toán bộ chỉ số thống kê đầy đủ
descriptive_table_adv <- numeric_vars %>%
summarise(across(
everything(),
list(
Mean = ~mean(., na.rm = TRUE),
Median = ~median(., na.rm = TRUE),
SD = ~sd(., na.rm = TRUE),
Var = ~var(., na.rm = TRUE),
Min = ~min(., na.rm = TRUE),
Max = ~max(., na.rm = TRUE),
Skewness = ~skewness(., na.rm = TRUE),
Kurtosis = ~kurtosis(., na.rm = TRUE)
),
.names = "{.col}__{.fn}" # Dùng __ để dễ tách
)) %>%
tidyr::pivot_longer(
cols = everything(),
names_to = c("Variable", "Statistic"),
names_sep = "__"
) %>%
tidyr::pivot_wider(
names_from = Statistic,
values_from = value # (2) Sửa lỗi chữ hoa/thường ở đây
) %>%
mutate(across(where(is.numeric), ~round(., 2))) # Làm tròn tất cả các cột số
# Hiển thị bảng
kable(descriptive_table_adv, caption = "Thống kê mô tả chi tiết các biến định lượng") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
scroll_box(width = "100%")| Variable | Mean | Median | SD | Var | Min | Max | Skewness | Kurtosis |
|---|---|---|---|---|---|---|---|---|
| Price | 2598.18 | 2598.57 | 1384.94 | 1918067.48 | 200.01 | 4999.81 | 0 | 1.8 |
| Quantity | 3.00 | 3.00 | 1.41 | 2.00 | 1.00 | 5.00 | 0 | 1.7 |
| Customer_Age | 44.04 | 44.00 | 15.31 | 234.51 | 18.00 | 70.00 | 0 | 1.8 |
Giải thích code:
Dấu ~ cho R biết đây là một công thức, và dấu . là một placeholder đại diện cho “cột hiện tại đang được xử lý.
pivot_longer(…): Biến bảng “rộng” thành “dài”.
pivot_wider(…): Biến bảng “dài” trở lại thành “rộng”.
values_from = value: Nó điền giá trị cho các cột mới đó từ cột value.
Ý nghĩa:
1. Biến Price (Giá cả)
Mean (2598.18) & Median (2598.57): Giá trị trung bình và trung vị gần như bằng nhau. Điều này là một dấu hiệu rất tốt, cho thấy sự phân phối của giá cả rất đối xứng. Không có nhiều sản phẩm giá quá cao hoặc quá thấp kéo lệch giá trị trung bình.
SD (1384.94) & Var (1918067.48): Độ lệch chuẩn (SD) khá lớn. Điều này cho thấy giá cả của các sản phẩm có sự biến động và chênh lệch cao. Dữ liệu không tập trung quanh một mức giá duy nhất mà trải rộng ra.
Min (200.01) & Max (4999.81): Khoảng giá của sản phẩm là từ khoảng 200 đến 5000. Điều này xác nhận lại độ phân tán cao của dữ liệu giá.
Skewness (0): Độ xiên bằng 0. Đây là sự khẳng định về mặt toán học rằng phân phối giá cả là hoàn toàn đối xứng. Đồ thị phân phối có hình dạng giống quả chuông cân đối.
Kurtosis (1.8): Độ nhọn là 1.8. Giá trị này nhỏ hơn 3 (độ nhọn của phân phối chuẩn). Điều này có nghĩa là phân phối ít nhọn hơn (bẹt hơn) so với phân phối chuẩn. Nó có ít các giá trị ngoại lai (outliers) ở hai phía đuôi.
2. Biến Quantity (Số lượng)
Mean (3.00) & Median (3.00): Trung bình và trung vị bằng nhau tuyệt đối. Số lượng sản phẩm được mua trung bình là 3.
SD (1.41) & Var (2.00): Độ lệch chuẩn nhỏ, cho thấy số lượng mua ít biến động. Hầu hết các giao dịch đều có số lượng xoay quanh mức 3.
Min (1.00) & Max (5.00): Số lượng mua ít nhất là 1 và nhiều nhất là 5. Đây là một khoảng giá trị rất hẹp, củng cố cho việc độ biến động thấp.
Skewness (0): Tương tự như giá, phân phối của số lượng cũng hoàn toàn đối xứng.
Kurtosis (1.7): Phân phối cũng bẹt hơn phân phối chuẩn, cho thấy các giá trị tập trung đều hơn và ít giá trị ngoại lai.
3. Biến Customer_Age (Tuổi khách hàng)
Mean (44.04) & Median (44.00): Độ tuổi trung bình và trung vị của khách hàng là khoảng 44 tuổi. Hai giá trị này rất gần nhau, cho thấy phân phối tuổi cũng rất cân đối . SD (15.31): Độ lệch chuẩn khá lớn, cho thấy độ tuổi của các khách hàng rất đa dạng, không tập trung ở một nhóm tuổi cụ thể nào.
Min (18.00) & Max (70.00): Nhóm khách hàng trải dài từ 18 đến 70 tuổi.
Skewness (0): Phân phối tuổi hoàn toàn đối xứng. Lượng khách hàng trẻ tuổi và lớn tuổi được phân bổ đều quanh độ tuổi trung bình.
Kurtosis (1.8): Phân phối tuổi cũng bẹt hơn phân phối chuẩn.
Kết luận chung: Price là biến có độ biến động lớn nhất, tiếp theo là Customer_Age, và cuối cùng Quantity là biến ổn định và ít thay đổi nhất.Tất cả các biến đều có phân phối “bẹt hơn” (Platykurtic) so với phân phối chuẩn, nghĩa là dữ liệu ít tập trung vào đỉnh và có ít giá trị cực đoan ở hai bên. Cả ba biến đều có độ xiên (Skewness) bằng 0 nó cho thấy dữ liệu của bạn cực kỳ cân bằng và đối xứng.
# Lấy các cột là character hoặc factor
qual_data <- BK %>% select(where(is.character) | where(is.factor))
# Dùng vòng lặp để tạo bảng tần suất cho mỗi biến
for (col_name in names(qual_data)) {
freq_table <- qual_data %>%
count(!!sym(col_name), sort = TRUE) %>%
mutate(Ty_le = n / sum(n) * 100) %>%
rename(Gia_tri = 1, Tan_so = n, "Ty_le (%)" = Ty_le) %>%
mutate(`Ty_le (%)` = round(`Ty_le (%)`, 2))
print(
kable(freq_table, caption = paste("Bảng tần suất cho biến:", col_name)) %>%
kable_styling(bootstrap_options = "striped", full_width = F)
)
}## <table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Bike_Model</caption>
## <thead>
## <tr>
## <th style="text-align:left;"> Gia_tri </th>
## <th style="text-align:right;"> Tan_so </th>
## <th style="text-align:right;"> Ty_le (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;"> BMX </td>
## <td style="text-align:right;"> 14377 </td>
## <td style="text-align:right;"> 14.38 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Road Bike </td>
## <td style="text-align:right;"> 14363 </td>
## <td style="text-align:right;"> 14.36 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Cruiser </td>
## <td style="text-align:right;"> 14332 </td>
## <td style="text-align:right;"> 14.33 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Folding Bike </td>
## <td style="text-align:right;"> 14329 </td>
## <td style="text-align:right;"> 14.33 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Hybrid Bike </td>
## <td style="text-align:right;"> 14319 </td>
## <td style="text-align:right;"> 14.32 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Electric Bike </td>
## <td style="text-align:right;"> 14169 </td>
## <td style="text-align:right;"> 14.17 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Mountain Bike </td>
## <td style="text-align:right;"> 14111 </td>
## <td style="text-align:right;"> 14.11 </td>
## </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Store_Location</caption>
## <thead>
## <tr>
## <th style="text-align:left;"> Gia_tri </th>
## <th style="text-align:right;"> Tan_so </th>
## <th style="text-align:right;"> Ty_le (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;"> New York </td>
## <td style="text-align:right;"> 14515 </td>
## <td style="text-align:right;"> 14.52 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Phoenix </td>
## <td style="text-align:right;"> 14385 </td>
## <td style="text-align:right;"> 14.38 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Philadelphia </td>
## <td style="text-align:right;"> 14330 </td>
## <td style="text-align:right;"> 14.33 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> San Antonio </td>
## <td style="text-align:right;"> 14300 </td>
## <td style="text-align:right;"> 14.30 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Chicago </td>
## <td style="text-align:right;"> 14207 </td>
## <td style="text-align:right;"> 14.21 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Houston </td>
## <td style="text-align:right;"> 14149 </td>
## <td style="text-align:right;"> 14.15 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Los Angeles </td>
## <td style="text-align:right;"> 14114 </td>
## <td style="text-align:right;"> 14.11 </td>
## </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Payment_Method</caption>
## <thead>
## <tr>
## <th style="text-align:left;"> Gia_tri </th>
## <th style="text-align:right;"> Tan_so </th>
## <th style="text-align:right;"> Ty_le (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;"> Apple Pay </td>
## <td style="text-align:right;"> 16751 </td>
## <td style="text-align:right;"> 16.75 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Debit Card </td>
## <td style="text-align:right;"> 16738 </td>
## <td style="text-align:right;"> 16.74 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Cash </td>
## <td style="text-align:right;"> 16692 </td>
## <td style="text-align:right;"> 16.69 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Credit Card </td>
## <td style="text-align:right;"> 16653 </td>
## <td style="text-align:right;"> 16.65 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Google Pay </td>
## <td style="text-align:right;"> 16613 </td>
## <td style="text-align:right;"> 16.61 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> PayPal </td>
## <td style="text-align:right;"> 16553 </td>
## <td style="text-align:right;"> 16.55 </td>
## </tr>
## </tbody>
## </table><table class="table table-striped" style="width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Bảng tần suất cho biến: Customer_Gender</caption>
## <thead>
## <tr>
## <th style="text-align:left;"> Gia_tri </th>
## <th style="text-align:right;"> Tan_so </th>
## <th style="text-align:right;"> Ty_le (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;"> Female </td>
## <td style="text-align:right;"> 50227 </td>
## <td style="text-align:right;"> 50.23 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Male </td>
## <td style="text-align:right;"> 49773 </td>
## <td style="text-align:right;"> 49.77 </td>
## </tr>
## </tbody>
## </table>
Giải thích code:
names(qual_data): Lệnh này lấy ra một danh sách (vector) chứa tên của tất cả các cột trong qual_data.
for (col_name in …): Lệnh này tạo ra một vòng lặp.
Ý nghĩa:
1. Bảng tần suất cho biến Bike_Model (Loại xe đạp) Nội dung: Bảng này liệt kê 7 loại xe đạp khác nhau (BMX, Road Bike, v.v.) và số lượng của mỗi loại.
Ý nghĩa quan trọng: Tần suất và tỷ lệ phần trăm của tất cả các loại xe đạp là gần như bằng nhau (tất cả đều xoay quanh 14.1% - 14.4%). Điều này cho thấy dữ liệu được phân bổ rất đều cho mỗi loại xe. Không có loại xe nào chiếm ưu thế vượt trội.
2. Bảng tần suất cho biến Store_Location (Vị trí cửa hàng)
Nội dung: Liệt kê số lượng bản ghi (có thể là giao dịch) tại 7 thành phố khác nhau.
Ý nghĩa quan trọng: Tương tự như loại xe, số lượng giao dịch tại mỗi thành phố cũng cực kỳ cân bằng (đều trong khoảng 14.1% - 14.5%). Mặc dù New York có nhiều hơn một chút, sự khác biệt là không đáng kể. Điều này cho thấy hoạt động kinh doanh hoặc việc thu thập dữ liệu diễn ra đồng đều trên tất cả các địa điểm.
3. Bảng tần suất cho biến Payment_Method (Phương thức thanh toán)
Nội dung: Thống kê 6 phương thức thanh toán khác nhau mà khách hàng đã sử dụng.
Ý nghĩa quan trọng: Một lần nữa, chúng ta lại thấy một sự cân bằng gần như hoàn hảo. Tất cả các phương thức thanh toán đều được sử dụng với tần suất rất giống nhau (khoảng 16.6% - 16.7%). Điều này có nghĩa là không có phương thức thanh toán nào được ưa chuộng hơn hẳn các phương thức còn lại trong tập dữ liệu này.
4. Bảng tần suất cho biến Customer_Gender (Giới tính khách hàng)
Nội dung: Thống kê số lượng khách hàng nam và nữ.
Ý nghĩa quan trọng: Tỷ lệ giữa khách hàng nữ (Female) và nam (Male) gần như là 50/50 (50.23% so với 49.77%). Đây là một sự cân bằng giới tính hoàn hảo.
total_revenue <- sum(BK1$Revenue)
cat("Tổng doanh thu:", dollar(total_revenue, prefix = "", suffix = " USD"))## Tổng doanh thu: 778,434,235 USD
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
## Tổng số giao dịch: 100,000
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
quantity_summary <- BK1 %>%
group_by(Bike_Model) %>%
summarise(Total_Quantity = sum(Quantity)) %>%
arrange(desc(Total_Quantity))
kable(quantity_summary, caption = "Số lượng bán theo mẫu xe")| Bike_Model | Total_Quantity |
|---|---|
| Cruiser | 43120 |
| Hybrid Bike | 43089 |
| BMX | 43080 |
| Road Bike | 43022 |
| Folding Bike | 42872 |
| Mountain Bike | 42279 |
| Electric Bike | 42249 |
revenue_by_model <- BK1 %>%
group_by(Bike_Model) %>%
summarise(Total_Revenue = sum(Revenue)) %>%
arrange(desc(Total_Revenue))
kable(revenue_by_model, caption = "Doanh thu theo mẫu xe")| Bike_Model | Total_Revenue |
|---|---|
| Hybrid Bike | 112505511 |
| BMX | 112146114 |
| Cruiser | 111849092 |
| Road Bike | 111579574 |
| Folding Bike | 110966818 |
| Electric Bike | 109750159 |
| Mountain Bike | 109636967 |
avg_price_by_model <- BK1 %>%
group_by(Bike_Model) %>%
summarise(Average_Price = mean(Price)) %>%
arrange(desc(Average_Price))
kable(avg_price_by_model, caption = "Giá bán trung bình theo mẫu xe")| Bike_Model | Average_Price |
|---|---|
| BMX | 2608.601 |
| Hybrid Bike | 2601.903 |
| Road Bike | 2600.954 |
| Electric Bike | 2598.047 |
| Cruiser | 2596.131 |
| Mountain Bike | 2591.299 |
| Folding Bike | 2590.196 |
price_level_dist <- BK1 %>%
count(Price_Level, sort = TRUE) %>%
mutate(Percentage = n / sum(n) * 100)
kable(price_level_dist, caption = "Tỷ lệ sản phẩm theo mức giá")| Price_Level | n | Percentage |
|---|---|---|
| Phổ thông | 50030 | 50.030 |
| Cao cấp | 29341 | 29.341 |
| Hạng sang | 20629 | 20.629 |
revenue_by_store <- BK1 %>%
group_by(Store_Location) %>%
summarise(Total_Revenue = sum(Revenue)) %>%
arrange(desc(Total_Revenue))
kable(revenue_by_store, caption = "Doanh thu theo cửa hàng")| Store_Location | Total_Revenue |
|---|---|
| New York | 113592474 |
| Phoenix | 111860420 |
| Philadelphia | 111765829 |
| Houston | 110427042 |
| Chicago | 110388139 |
| Los Angeles | 110340851 |
| San Antonio | 110059479 |
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")| Store_Location | Transaction_Count |
|---|---|
| New York | 14515 |
| Phoenix | 14385 |
| Philadelphia | 14330 |
| San Antonio | 14300 |
| Chicago | 14207 |
| Houston | 14149 |
| Los Angeles | 14114 |
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")| 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 |
top_salesperson <- BK1 %>%
group_by(Salesperson_ID) %>%
summarise(Total_Revenue = sum(Revenue)) %>%
arrange(desc(Total_Revenue)) %>%
top_n(10, Total_Revenue)
kable(top_salesperson, caption = "Top 10 nhân viên có doanh thu cao nhất")| Salesperson_ID | Total_Revenue |
|---|---|
| 794 | 1196096 |
| 500 | 1137238 |
| 605 | 1125912 |
| 655 | 1123964 |
| 914 | 1123278 |
| 758 | 1105828 |
| 419 | 1105377 |
| 894 | 1101144 |
| 540 | 1100963 |
| 485 | 1100888 |
total_unique_customers <- n_distinct(BK1$Customer_ID)
cat("Tổng số khách hàng duy nhất:", comma(total_unique_customers))## Tổng số khách hàng duy nhất: 9,000
mean_customer_age <- mean(BK1$Customer_Age)
cat("Độ tuổi trung bình của khách hàng:", round(mean_customer_age, 1), "tuổi\n")## Độ tuổi trung bình của khách hàng: 44 tuổi
gender_distribution <- BK1 %>%
count(Customer_Gender, sort = TRUE) %>%
mutate(Percentage = n / sum(n) * 100)
kable(gender_distribution, caption = "Phân bổ khách hàng theo giới tính")| Customer_Gender | n | Percentage |
|---|---|---|
| Female | 50227 | 50.227 |
| Male | 49773 | 49.773 |
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")| Age_Group | Total_Revenue | Average_Purchase_Value |
|---|---|---|
| Middle-Aged (36-55) | 295268207 | 7842.657 |
| Young (18-35) | 262850657 | 7755.537 |
| Senior (56+) | 220315371 | 7741.501 |
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")| Loyalty_Status | n | Percentage |
|---|---|---|
| Bronze | 3 | 0.0333333 |
| Gold | 2852 | 31.6888889 |
| Platinum | 6006 | 66.7333333 |
| Silver | 139 | 1.5444444 |
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%
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")| Loyalty_Status | Total_Revenue |
|---|---|
| Platinum | 602997177.2 |
| Gold | 171397064.9 |
| Silver | 4009260.4 |
| Bronze | 30732.4 |
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")| Loyalty_Status | Average_Revenue_Per_Sale |
|---|---|
| Bronze | 10244.133 |
| Silver | 7830.587 |
| Platinum | 7784.727 |
| Gold | 7781.579 |
revenue_by_year <- BK1 %>%
group_by(Year) %>%
summarise(Total_Revenue = sum(Revenue))
kable(revenue_by_year, caption = "Doanh thu theo năm")| Year | Total_Revenue |
|---|---|
| 2020 | 164224491 |
| 2021 | 163535245 |
| 2022 | 165560749 |
| 2023 | 164497574 |
| 2024 | 120616177 |
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")| Season | Total_Revenue |
|---|---|
| Hạ | 207337508 |
| Xuân | 207312828 |
| Đông | 188748226 |
| Thu | 175035674 |
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")| Weekday | Total_Revenue |
|---|---|
| Thursday | 113678630 |
| Friday | 111803200 |
| Wednesday | 111762837 |
| Saturday | 111409344 |
| Monday | 110441224 |
| Sunday | 110304080 |
| Tuesday | 109034920 |
payment_method_usage <- BK1 %>%
count(Payment_Method, sort = TRUE) %>%
mutate(Percentage = n / sum(n) * 100)
kable(payment_method_usage, caption = "Mức độ phổ biến của các phương thức thanh toán")| Payment_Method | n | Percentage |
|---|---|---|
| Apple Pay | 16751 | 16.751 |
| Debit Card | 16738 | 16.738 |
| Cash | 16692 | 16.692 |
| Credit Card | 16653 | 16.653 |
| Google Pay | 16613 | 16.613 |
| PayPal | 16553 | 16.553 |
model_preference_by_gender <- BK1 %>%
group_by(Customer_Gender, Bike_Model) %>%
summarise(Total_Quantity = sum(Quantity), .groups = 'drop') %>%
group_by(Customer_Gender) %>%
top_n(1, Total_Quantity)
kable(model_preference_by_gender, caption = "Mẫu xe được ưa chuộng nhất theo giới tính")| Customer_Gender | Bike_Model | Total_Quantity |
|---|---|---|
| Female | BMX | 21870 |
| Male | Road Bike | 21490 |
correlation_age_price <- cor(BK1$Customer_Age, BK1$Price)
print(paste("Hệ số tương quan giữa Tuổi khách hàng và Giá xe:", round(correlation_age_price, 4)))## [1] "Hệ số tương quan giữa Tuổi khách hàng và Giá xe: 1e-04"
Giải thích code: Hàm cor() trong R để tính toán hệ số tương quan Pearson giữa hai biến.
Ý nghĩa: Kết quả 0.0001 của bạn là một con số cực kỳ gần với 0. Không có bất kỳ mối quan hệ tuyến tính nào giữa tuổi của khách hàng và giá của chiếc xe mà họ mua.
Trong chương này, chúng ta sẽ sử dụng thư viện ggplot2
và các phần mở rộng của nó để trực quan hóa các insight đã tìm thấy,
giúp truyền tải thông tin một cách sinh động và dễ hiểu.
# Dữ liệu đã được tính ở chương 3
revenue_by_model <- BK1 %>%
group_by(Bike_Model) %>%
summarise(Total_Revenue = sum(Revenue))
ggplot(revenue_by_model, aes(x = reorder(Bike_Model, -Total_Revenue), y = Total_Revenue, fill = Bike_Model)) +
geom_bar(stat = "identity", show.legend = FALSE) +
geom_text(aes(label = scales::dollar(Total_Revenue, accuracy = 1)), vjust = -0.5, size = 3.5) +
labs(title = "Tổng Doanh Thu Theo Từng Mẫu Xe",
subtitle = "Mẫu 'Hybrid Bike' và 'BMX' chiếm ưu thế",
x = "Mẫu Xe",
y = "Tổng Doanh Thu") +
scale_y_continuous(labels = scales::dollar) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Giải thích code:
ggplot(…): Bắt đầu quá trình vẽ biểu đồ với dữ liệu vừa tính toán.
aes(x = reorder(Bike_Model, -Total_Revenue), y = Total_Revenue, …): Trục Y (y) là Total_Revenue (Tổng doanh thu). Trục X (x) là Bike_Model (Mẫu xe), được sắp xếp lại (reorder) theo doanh thu giảm dần (dấu - có nghĩa là giảm dần). Đây là lý do các cột trên biểu đồ đi từ cao xuống thấp.
geom_bar(stat = “identity”): Vẽ biểu đồ cột, với chiều cao của mỗi cột được lấy trực tiếp từ giá trị Total_Revenue.
geom_text(…): Thêm nhãn văn bản (số doanh thu chính xác) lên trên mỗi cột để dễ đọc.
labs(…) và theme(…): Đặt tiêu đề, nhãn cho các trục và tùy chỉnh giao diện của biểu đồ cho đẹp mắt.
Phân tích: Biểu đồ cho thấy rõ ràng sự thống trị của “Electric Bolt” và “Mountain Pro”. Khoảng cách doanh thu giữa các mẫu xe rất lớn, giúp nhà quản lý xác định đâu là sản phẩm chủ lực.
revenue_by_model_store <- BK1 %>%
group_by(Bike_Model, Store_Location) %>%
summarise(Total_Revenue = sum(Revenue), .groups = "drop")
ggplot(revenue_by_model_store,
aes(x = reorder(Bike_Model, -Total_Revenue),
y = Total_Revenue,
fill = Store_Location)) +
geom_bar(stat = "identity", position = "dodge") +
labs(title = "So sánh Doanh Thu của Từng Mẫu Xe Theo Địa Điểm Cửa Hàng",
subtitle = "Giúp xác định mẫu xe nào là sản phẩm chủ lực tại từng địa điểm",
x = "Mẫu Xe",
y = "Tổng Doanh Thu",
fill = "Địa điểm cửa hàng") +
scale_y_continuous(labels = scales::dollar) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Giải thích code:
ggplot(revenue_by_model_store, aes(…)): Khởi tạo biểu đồ bằng dữ liệu revenue_by_model_store vừa tạo. x = reorder(Bike_Model, -Total_Revenue): Thiết lập trục hoành (trục X) là các mẫu xe. reorder dùng để sắp xếp các mẫu xe theo thứ tự giảm dần của tổng doanh thu. y = Total_Revenue: Thiết lập trục tung (trục Y) là tổng doanh thu.
fill = Store_Location: Quy định rằng màu sắc của các cột sẽ được dùng để phân biệt các địa điểm cửa hàng.
geom_bar(stat = “identity”, position = “dodge”): Lệnh này vẽ biểu đồ cột (geom_bar).
stat = “identity”: Sử dụng trực tiếp giá trị Total_Revenue làm chiều cao của cột.
position = “dodge”: Sắp xếp các cột của các cửa hàng khác nhau nằm cạnh nhau cho mỗi mẫu xe, giúp dễ dàng so sánh.
labs(…): Đặt tên cho tiêu đề, phụ đề, các trục và chú thích của biểu đồ.
scale_y_continuous(labels = scales::dollar): Định dạng các giá trị trên trục Y thành đơn vị tiền tệ đô la ($).
theme(…): Tùy chỉnh giao diện của biểu đồ, ở đây là xoay nhãn của trục X một góc 45 độ để tránh bị chồng chéo.
Ý nghĩa: - So sánh hiệu suất sản phẩm: Có thể dễ dàng thấy mẫu xe nào đang tạo ra doanh thu cao nhất hoặc thấp nhất tại một địa điểm cụ thể. Ví dụ, tại New York (cột màu xanh mòng két), “Road Bike” và “Hybrid Bike” có vẻ là những mẫu xe có doanh thu cao nhất.
So sánh hiệu suất cửa hàng: Đối với một mẫu xe nhất định, ta có thể so sánh xem cửa hàng ở thành phố nào bán chạy nhất. Ví dụ, với “Mountain Bike”, cửa hàng ở Los Angeles (cột màu xanh lá) có doanh thu cao hơn hẳn so với các cửa hàng khác.
Xác định sản phẩm chủ lực: Đúng như phụ đề của biểu đồ, mục đích chính là “Giúp xác định mẫu xe nào là sản phẩm chủ lực tại từng địa điểm”. Dựa vào chiều cao các cột, nhà quản lý có thể biết được ở Chicago nên tập trung vào sản phẩm nào, ở Houston nên tập trung vào sản phẩm nào, từ đó đưa ra các chiến lược kinh doanh và marketing phù hợp cho từng khu vực.
revenue_by_store <- BK1 %>%
group_by(Store_Location) %>%
summarise(Total_Revenue = sum(Revenue))
ggplot(revenue_by_store, aes(x = "", y = Total_Revenue, fill = Store_Location)) +
geom_bar(stat = "identity", width = 1) +
coord_polar("y", start = 0) +
geom_text(aes(label = scales::percent(Total_Revenue / sum(Total_Revenue), accuracy = 0.1)),
position = position_stack(vjust = 0.5), color = "white", size = 5) +
labs(title = "Tỷ Lệ Phân Bổ Doanh Thu Theo Địa Điểm Cửa Hàng",
fill = "Địa Điểm Cửa Hàng", x = NULL, y = NULL) +
theme_void() +
theme(legend.title = element_text(face = "bold"))Giải thích code:
ggplot(revenue_by_store, aes(x = ““, y = Total_Revenue, fill = Store_Location)): Khởi tạo biểu đồ. Thay vì vẽ các cột riêng biệt, x =”” làm cho tất cả các giá trị được xếp chồng lên nhau tại một vị trí duy nhất, đây là bước đầu tiên để tạo biểu đồ tròn từ biểu đồ cột.
geom_bar(stat = “identity”, width = 1): Vẽ một biểu đồ cột chồng duy nhất.
coord_polar(“y”, start = 0): Đây là lệnh quan trọng nhất. Nó biến đổi hệ tọa độ của biểu đồ cột chồng thành hệ tọa độ cực, uốn cong thanh cột đó thành một hình tròn, tạo ra biểu đồ tròn.
Ý nghĩa: Sự chênh lệch giữa cửa hàng hoạt động tốt nhất (New York) và thấp nhất (San Antonio) chỉ là 0.5%. Điều này cho thấy hiệu suất kinh doanh của các chi nhánh đang ở mức tương đối cân bằng.
revenue_by_store1 <- BK1 %>%
group_by(Store_Location) %>%
summarise(Total_Revenue = sum(Revenue), .groups = "drop")
ggplot(revenue_by_store,
aes(x = reorder(Store_Location, -Total_Revenue), y = Total_Revenue, fill = Store_Location)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = scales::dollar(Total_Revenue, accuracy = 1)),
vjust = -0.4, size = 4) +
labs(title = "Doanh Thu Theo Địa Điểm Cửa Hàng (Barplot)",
x = "Địa Điểm Cửa Hàng",
y = "Tổng Doanh Thu") +
scale_y_continuous(labels = scales::dollar) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))Ý nghĩa: Dễ dàng nhìn thấy cửa hàng nào đóng góp cao nhất → hỗ trợ ưu tiên marketing & phân bổ hàng. Kết quả cho thấy toàn bộ hệ thống cửa hàng đang hoạt động một cách ổn định và cân bằng, không có chi nhánh nào tỏ ra vượt trội hay yếu kém một cách đáng kể so với phần còn lại.
ggplot(revenue_by_store, aes(area = Total_Revenue, fill = Store_Location, label = Store_Location)) +
geom_treemap() +
geom_treemap_text(colour = "white", place = "centre", size = 14, grow = TRUE) +
labs(title = "Tỷ Trọng Doanh Thu Theo Địa Điểm Cửa Hàng (Treemap)",
fill = "Địa Điểm Cửa Hàng") +
theme_minimal()Ý nghĩa: Diện tích mỗi ô = mức doanh thu → nhìn phát biết ngay store nào chiếm tỷ trọng lớn.
revenue_over_time <- BK1 %>%
mutate(YearMonth = floor_date(Date, "month")) %>%
group_by(YearMonth) %>%
summarise(Monthly_Revenue = sum(Revenue), .groups = "drop")
ggplot(revenue_over_time, aes(x = YearMonth, y = Monthly_Revenue)) +
geom_line(color = "steelblue", size = 1) +
geom_point(color = "darkred", size = 2, shape = 21, fill = "yellow") +
labs(title = "Xu Hướng Doanh Thu Hàng Tháng",
subtitle = "Phân tích doanh thu từ 2022 đến 2024",
x = "Thời Gian", y = "Doanh Thu Tháng") +
scale_y_continuous(labels = scales::dollar) +
scale_x_date(date_breaks = "3 months", date_labels = "%b %Y") +
theme_light() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Ý nghĩa: Doanh thu hàng tháng không ổn định mà dao động khá mạnh, thường nằm trong khoảng từ 13 triệu đến 14.5 triệu đô la. Có những đỉnh và đáy rõ rệt qua các tháng. Thường có sự sụt giảm doanh thu vào khoảng tháng 4 và một sự tăng trưởng vào giữa hoặc cuối năm. Điều này có thể liên quan đến các yếu tố mùa vụ trong kinh doanh.
revenue_over_time <- BK1 %>%
mutate(YearMonth = floor_date(Date, "month")) %>%
group_by(YearMonth) %>%
summarise(Monthly_Revenue = sum(Revenue), .groups = "drop") %>%
mutate(MA3 = rollmean(Monthly_Revenue, k = 3, fill = NA, align = "right"))
ggplot(revenue_over_time, aes(x = YearMonth)) +
geom_line(aes(y = Monthly_Revenue), color = "steelblue", size = 1) +
geom_point(aes(y = Monthly_Revenue), color = "darkred", size = 2, shape = 21, fill = "yellow") +
# Đường trung bình động 3 tháng
geom_line(aes(y = MA3), color = "orange", size = 1.2, linetype = "solid") +
labs(title = "Xu Hướng Doanh Thu Hàng Tháng",subtitle = "Có thêm đường Trung Bình Động 3 Tháng (MA3) để làm mượt xu hướng",
x = "Thời Gian",
y = "Doanh Thu Tháng") +
scale_y_continuous(labels = scales::dollar) +
scale_x_date(date_breaks = "3 months", date_labels = "%b %Y") +
theme_light() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Ý nghĩa: Đường MA3 cho thấy trong suốt giai đoạn từ 2020 đến giữa 2024, xu hướng doanh thu dài hạn là tương đối ổn định, dao động quanh mốc 13.5 đến 14 triệu đô la. Biểu đồ này cung cấp một cái nhìn sâu sắc và đáng tin cậy hơn về hiệu suất kinh doanh. Nó không chỉ cho thấy những gì đã xảy ra hàng tháng mà còn giúp lọc ra các biến động ngắn hạn để tập trung vào xu hướng cốt lõi, giúp các nhà quản lý đưa ra quyết định chiến lược tốt hơn.
ggplot(BK1, aes(x = reorder(Bike_Model, Price), y = Price, fill = Bike_Model)) +
geom_boxplot(show.legend = FALSE, outlier.colour = "red", outlier.shape = 8) +
labs(title = "Phân Bổ Giá Bán Của Các Mẫu Xe",
subtitle = "So sánh sự biến động giá và các giá trị ngoại lai",
x = "Mẫu Xe", y = "Giá Bán") +
scale_y_continuous(labels = scales::dollar) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 4, fill = "white") + # Thêm điểm trung bình
theme_bw() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Ý nghĩa: Biểu đồ hộp cho thấy “Electric Bolt” không chỉ có giá trung bình cao nhất mà còn có khoảng giá rất rộng. Ngược lại, “Kids Explorer” có giá thấp và ít biến động.
quantity_by_model_store <- BK1 %>%
group_by(Store_Location, Bike_Model) %>%
summarise(Total_Quantity = sum(Quantity), .groups = "drop")
ggplot(quantity_by_model_store, aes(x = Bike_Model, y = Total_Quantity)) +
geom_col(position = "dodge", fill = "gray40") +
facet_wrap(~ Store_Location) +
geom_text(aes(label = Total_Quantity), vjust = -0.4, size = 3) +
labs(title = "Số lượng xe bán ra theo từng mẫu xe tại mỗ địa điểm cửa hàng",
subtitle = "So sánh mức độ tiêu thụ sản phẩm giữa các địa điểm",
x = "Mẫu Xe",
y = "Số Lượng Bán Ra") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Ý nghĩa: Mẫu xe được tiêu thụ khá đồng đều, không có mẫu xe nào bị ế hay bán vượt trội quá mạnh. New York và Los Angeles là hai thị trường có nhu cầu tiêu thụ xe cao → nên ưu tiên phân bổ hàng và chiến dịch quảng bá.
ggplot(BK1, aes(x = Price)) +
geom_density(fill = "orange", alpha = 0.6, color = "black", size = 1) +
labs(title = "Phân bố giá xe trong dữ liệu bán hàng",
x = "Giá bán (Price)",
y = "Mật độ") +
theme_minimal()Ý nghĩa:Biểu đồ mật độ cho thấy cách giá xe được phân bố trong dữ liệu bán hàng. Đường mật độ tương đối phẳng ở phần trung tâm (khoảng 1.000–4.000), cho thấy phần lớn xe có giá nằm trong khoảng này và phân bố khá đồng đều, không tập trung mạnh vào một mức giá cụ thể. Ở hai đầu phân bố (giá rất thấp hoặc rất cao) ta thấy mật độ giảm xuống, nghĩa là số lượng xe giá quá rẻ hoặc quá đắt là ít.
BK1Density <- BK1
BK1Density %>%
ggplot(aes(x = Price)) +
geom_density(fill = "blue", alpha = 0.7) +
facet_wrap(~ Bike_Model) +
labs(
title = "Phân bố giá theo từng mẫu xe",
x = "Giá bán (Price)",
y = "Mật độ"
) +
theme_minimal()Ý nghĩa: Giá bán của các mẫu xe nhìn chung phân bố khá đồng đều trong khoảng 500 – 5000 USD, không có sự chênh lệch quá lớn giữa các dòng xe. Tuy nhiên, một số dòng như BMX và Cruiser có xu hướng tập trung giá ở mức thấp hơn, trong khi Electric Bike và Hybrid Bike có phân bố giá rộng hơn, phản ánh các dòng xe này có thể có nhiều phiên bản hoặc phân khúc khác nhau. Điều này cho thấy thị trường sản phẩm có cấu trúc giá đa dạng nhưng vẫn cân đối giữa các nhóm xe.
ggplot(BK1, aes(x = Customer_Age)) +
geom_histogram(binwidth = 5, fill = "skyblue", color = "black", alpha = 0.7) +
geom_vline(aes(xintercept = mean(Customer_Age)), color = "red", linetype = "dashed", size = 1) +annotate("text", x = mean(BK$Customer_Age) + 10, y = 7000,
label = paste("Tuổi TB:", round(mean(BK$Customer_Age), 1)), color = "red") +
geom_density(aes(y = after_stat(count) * 5), color = "darkgreen", size = 1) + # Thêm đường mật độ
labs(title = "Phân Bổ Độ Tuổi Của Khách Hàng",
x = "Tuổi", y = "Số Lượng Khách Hàng") +
theme_classic()Ý nghĩa: Điểm đặc biệt và nổi bật nhất của biểu đồ này không phải là sự tập trung vào một nhóm tuổi nào, mà là sự phân bổ gần như bằng nhau của khách hàng trên tất cả các nhóm tuổi từ thanh niên đến khi về hưu. Các cột trong khoảng từ 25 đến 65 tuổi có chiều cao gần như không chênh lệch.
revenue_age_gender <- BK1 %>%
group_by(Age_Group, Customer_Gender) %>%
summarise(Total_Revenue = sum(Revenue), .groups = 'drop')
ggplot(revenue_age_gender, aes(x = Age_Group, y = Total_Revenue, fill = Customer_Gender)) +
geom_bar(stat = "identity", position = "stack") +
facet_wrap(~Customer_Gender) + # Chia thành các biểu đồ nhỏ theo giới tính
labs(title = "Doanh Thu Theo Nhóm Tuổi và Giới Tính",
x = "Nhóm Tuổi", y = "Tổng Doanh Thu", fill = "Giới Tính") +
scale_y_continuous(labels = scales::dollar) +
scale_fill_brewer(palette = "Set2") +
theme_minimal()Ý nghĩa: Nhóm tuổi “Middle-Aged” (36-55) đóng góp nhiều doanh thu nhất. Trong hầu hết các nhóm tuổi, tỷ lệ doanh thu từ nam và nữ khá tương đồng.
ggplot(BK1 %>% sample_n(5000), aes(x = Customer_Age, y = Revenue)) + # Lấy mẫu 5000 điểm để vẽ cho nhẹ
geom_point(alpha = 0.3, aes(color = Store_Location)) +
geom_smooth(method = "lm", color = "red", se = FALSE, linetype = "dashed") +
labs(title = "Mối Quan Hệ Giữa Tuổi Khách Hàng và Giá Trị Đơn Hàng",
subtitle = "Mỗi điểm là một giao dịch",
x = "Tuổi Khách Hàng", y = "Doanh Thu Mỗi Giao Dịch", color = "Cửa hàng") +
scale_y_continuous(labels = scales::dollar) +
theme_gray()Ý nghĩa: Các điểm dữ liệu phân bố khá ngẫu nhiên và đường xu hướng (màu đỏ) gần như nằm ngang, khẳng định lại kết quả phân tích tương quan ở Chương 3: không có mối liên hệ rõ ràng giữa tuổi và giá trị đơn hàng.
revenue_heatmap_data <- BK1 %>%
group_by(Weekday, Month) %>%
summarise(Total_Revenue = sum(Revenue), .groups = 'drop')
ggplot(revenue_heatmap_data, aes(x = Weekday, y = Month, fill = Total_Revenue)) +
geom_tile(color = "white") +
geom_text(aes(label = scales::dollar(Total_Revenue, scale = 1/1000, suffix = "K")), size = 3) +
scale_fill_gradient(low = "lightyellow", high = "darkred", labels = scales::dollar) +
labs(title = "Heatmap Doanh Thu Theo Ngày và Tháng",
subtitle = "Doanh thu được tính bằng nghìn đô la (K)",
x = "Ngày Trong Tuần", y = "Tháng", fill = "Doanh Thu") +
theme_minimal()Ý nghĩa: Các ô màu càng đậm (đỏ) thể hiện doanh thu càng cao. Biểu đồ này giúp nhanh chóng xác định rằng cuối tuần và các tháng mùa hè thường là thời điểm kinh doanh tốt nhất.
quantity_season_store <- BK1 %>%
group_by(Season, Store_Location) %>%
summarise(Total_Quantity = sum(Quantity), .groups = 'drop')
ggplot(quantity_season_store, aes(x = Season, y = Total_Quantity, fill = Store_Location)) +
geom_bar(stat = "identity", position = position_dodge(width = 0.9)) +
geom_text(aes(label = Total_Quantity), position = position_dodge(width = 0.9), vjust = -0.5, size = 3) +
labs(title = "Số Lượng Xe Bán Theo Mùa và Địa Điểm Cửa Hàng",
x = "Mùa", y = "Tổng Số Lượng Xe", fill = "Địa Điểm Cửa Hàng") +
theme_light() +
scale_fill_viridis_d(option = "C")Ý nghĩa: - Về mùa: Mùa Hè và mùa Xuân là hai mùa có số lượng xe bán ra cao nhất một cách rõ rệt. Ngược lại, mùa Thu là mùa bán chậm nhất, với doanh số thấp hơn hẳn so với các mùa còn lại. - Về địa điểm: Không có một cửa hàng nào cố định bán chạy nhất ở tất cả các mùa. Hiệu suất bán hàng thay đổi theo mùa, ví dụ: Los Angeles (màu tím) là nơi bán chạy nhất vào mùa Hè, trong khi San Antonio (màu vàng) lại dẫn đầu trong mùa Xuân.
g * Mục đích: Biểu đồ này được sử dụng để theo dõi xu hướng tiêu thụ xe theo từng quý và so sánh hiệu suất bán hàng giữa các cửa hàng. Qua đó giúp nhận biết thời điểm bán hàng tăng/giảm và xác định cửa hàng nào hoạt động tốt hơn trong từng giai đoạn.
quantity_quarter_store <- BK1 %>%
mutate(Quarter = paste0("Q", quarter(Date), "-", year(Date))) %>%
# Tạo biến Quý (VD: Q1-2023)
group_by(Quarter, Store_Location) %>%
summarise(Total_Quantity = sum(Quantity), .groups = "drop") %>%
arrange(Quarter)
ggplot(quantity_quarter_store, aes(x = Quarter, y = Total_Quantity, color = Store_Location, group = Store_Location)) +
geom_line(size = 1.2) +
geom_point(size = 2) +
labs(title = "Xu Hướng Số Lượng Xe Bán Theo Quý và Địa điểm cửa Hàng",
x = "Quý",
y = "Tổng Số Lượng Xe",
color = "Địa điểm cửa Hàng") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Ý nghĩa:Số lượng xe bán ra dao động theo từng quý, thể hiện sự biến động nhu cầu của thị trường theo thời gian. Các cửa hàng như New York, Chicago và Los Angeles thường có mức bán ra cao và ổn định hơn so với các cửa hàng còn lại. Một số quý xuất hiện mức giảm mạnh (điểm đáy), cho thấy khả năng chịu ảnh hưởng từ yếu tố mùa vụ hoặc điều kiện kinh tế. Xu hướng tổng thể cho thấy không có chênh lệch quá lớn giữa các cửa hàng, nhưng một số cửa hàng duy trì hiệu suất tốt hơn liên tục, phản ánh ưu thế vị trí, thị hiếu hoặc chiến lược bán hàng hiệu quả.
top5_salesperson_trans <- BK1 %>%
count(Salesperson_ID, name = "Transaction_Count", sort = TRUE) %>%
slice_head(n = 5)
ggplot(top5_salesperson_trans, aes(x = reorder(Salesperson_ID, Transaction_Count), y = Transaction_Count, fill = Salesperson_ID)) +
geom_bar(stat = "identity", show.legend = FALSE, width = 0.7) +
coord_flip() + # Lật biểu đồ thành dạng ngang
geom_text(aes(label = Transaction_Count), hjust = -0.2) +
labs(title = "Top 5 Nhân Viên Theo Số Lượng Giao Dịch",
subtitle = "Dựa trên tổng số hóa đơn đã xử lý",
x = "Nhân Viên", y = "Số Lượng Giao Dịch") +
theme_minimal()Ý nghĩa: Biểu đồ xác định nhân viên có mã số 422 là người xử lý nhiều giao dịch nhất (143). Tuy nhiên, điều quan trọng hơn là hiệu suất của top 5 nhân viên này cực kỳ đồng đều và sít sao, chỉ chênh lệch nhau tối đa 3 giao dịch.
bubble_data <- BK1 %>%
group_by(Bike_Model) %>%
summarise(Total_Revenue = sum(Revenue),Total_Quantity = sum(Quantity),
Avg_Price = mean(Price), .groups = 'drop')
ggplot(bubble_data, aes(x = Total_Quantity, y = Avg_Price, size = Total_Revenue, color = Bike_Model)) +
geom_point(alpha = 0.7) +
scale_size(range = c(5, 25), name = "Tổng Doanh Thu (USD)") +
scale_color_viridis_d(guide = "none") +
geom_text(aes(label = Bike_Model), size = 3.5, vjust = -2.5, fontface = "bold") +
labs(title = "Phân Tích Tổng Quan Các Mẫu Xe",
subtitle = "Doanh thu (kích thước), Số lượng (trục x), Giá TB (trục y)",
x = "Tổng Số Lượng Bán Ra", y = "Giá Bán Trung Bình (USD)") +
scale_y_continuous(labels = scales::dollar) +
theme_minimal()Ý nghĩa: “Electric Bolt” nằm ở góc trên bên phải với bong bóng lớn nhất: giá trung bình cao, số lượng bán nhiều, doanh thu vượt trội.
treemap_data <- BK1 %>%
group_by(Store_Location, Bike_Model) %>%
summarise(Total_Revenue = sum(Revenue), .groups = 'drop')
ggplot(treemap_data, aes(area = Total_Revenue, fill = Bike_Model,
label = paste(Bike_Model, scales::dollar(Total_Revenue, scale = 1/1e6, suffix = "M"), sep = "\n"),
subgroup = Store_Location)) +
geom_treemap() +
geom_treemap_subgroup_border(colour = "white", size = 5) +
geom_treemap_subgroup_text(place = "centre", grow = T, alpha = 0.4, colour = "black", fontface = "italic", min.size = 0) +
geom_treemap_text(colour = "white", place = "topleft", reflow = T) +
labs(title = "Tỷ Trọng Doanh Thu Các Mẫu Xe Theo Địa Điểm Cửa Hàng (Đơn vị: triệu USD)") +
theme(legend.position = "none")Ý nghĩa: Biểu đồ cho thấy mỗi cửa hàng có một mẫu xe “chủ lực” (con gà đẻ trứng vàng) khác nhau, ví dụ Hybrid Bike ở New York hay Road Bike ở Philadelphia, chứng tỏ thị hiếu của khách hàng ở mỗi vùng là riêng biệt. Tuy nhiên, điểm đáng chú ý hơn là tại một cửa hàng bất kỳ, doanh thu giữa các mẫu xe lại rất đồng đều, không có mẫu nào vượt trội hoàn toàn so với phần còn lại. Điều này cho thấy chiến lược kinh doanh đa dạng các dòng sản phẩm đang rất hiệu quả ở tất cả các chi nhánh.
sankey_data <- BK1 %>%
group_by(Customer_Gender, Bike_Model) %>%
summarise(Count = n(), .groups = 'drop')
ggplot(as.data.frame(sankey_data),
aes(y = Count, axis1 = Customer_Gender, axis2 = Bike_Model)) +
geom_alluvium(aes(fill = Bike_Model), width = 1/12, alpha = 0.7) +
geom_stratum(width = 1/6, fill = "grey", color = "white") +
geom_label(stat = "stratum", aes(label = after_stat(stratum))) +
scale_x_discrete(limits = c("Giới tính", "Mẫu xe"), expand = c(.05, .05)) +
labs(title = "Luồng Lựa Chọn Mẫu Xe Theo Giới Tính",subtitle = "Độ rộng của dải màu thể hiện số lượng khách hàng") +
theme_void() +
theme(legend.position = "bottom")Ý nghĩa: Điểm đáng chú ý nhất từ biểu đồ này là không có sự khác biệt rõ rệt trong sở thích chọn xe giữa khách hàng nam và nữ. Nhìn chung, tất cả các mẫu xe đều được cả hai giới lựa chọn với tỷ lệ rất cân bằng. Điều này cho thấy các mẫu xe có sức hấp dẫn rộng rãi và không cần các chiến lược marketing nhắm riêng vào từng giới tính.
numeric_vars <- BK1 %>%
select(where(is.numeric)) %>%
select(-Sale_ID, -Year, -Gender_Encoded, -Purchase_Count) # Loại bỏ các biến không cần thiết
cor_matrix <- cor(numeric_vars, use = "complete.obs")
corrplot(cor_matrix, method = "color", type = "upper",
addCoef.col = "black", tl.col = "black", tl.srt = 45,
title = "Ma Trận Tương Quan Giữa Các Biến Số", mar = c(0, 0, 1, 0))Ý nghĩa: Biểu đồ cho thấy Price (Giá) có tương quan dương mạnh nhất với Revenue (Doanh thu), với hệ số là 0.71. Điều này có nghĩa là khi giá sản phẩm tăng thì doanh thu có xu hướng tăng theo. Tương tự, Quantity (Số lượng) cũng có tương quan dương đáng kể với Revenue (0.62), điều này là hợp lý vì bán được nhiều hàng hơn sẽ tạo ra nhiều doanh thu hơn. Một điểm đáng chú ý khác là hầu như không có mối tương quan nào giữa Customer_Age (Tuổi khách hàng) và Revenue, cho thấy tuổi tác không phải là yếu tố ảnh hưởng đến doanh thu trong bộ dữ liệu này.
Bộ dữ liệu tài chính được sử dụng trong nghiên cứu này là tập hợp các báo cáo tài chính được thu thập từ Công ty Cổ phần Hoàng Anh Gia Lai (HAG) trong giai đoạn từ năm 2015 đến năm 2024. Dữ liệu gốc bao gồm nhiều báo cáo chính: Bảng Cân đối Kế toán, Báo cáo Lưu chuyển Tiền tệ và Báo cáo Kết quả Kinh doanh (KQKD). Đây là dữ liệu theo năm, cung cấp cái nhìn xuyên suốt về hiệu suất hoạt động của công ty trong một thập kỷ đầy biến động.
library(dplyr)
library(readr)
HAG <- read_csv("D:/Thúy Hiền/Ngôn ngữ lập trình trong phân tích dữ liệu/HAG.csv")## New names:
## Rows: 10 Columns: 126
## ── Column specification
## ──────────────────────────────────────────────────────── Delimiter: "," dbl
## (122): Nam, Tai_san_ngan_han, tien...3, tien...4, cac_khoan_phai_thu_nga... lgl
## (4): nguon_von, Luu_chuyen_tien_tu_hoat_dong_kinh_doanh, luu_chuyen_ti...
## ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
## Specify the column types or set `show_col_types = FALSE` to quiet this message.
## • `tien` -> `tien...3`
## • `tien` -> `tien...4`
## • `hang_ton_kho` -> `hang_ton_kho...11`
## • `hang_ton_kho` -> `hang_ton_kho...12`
## • `tai_san_ngan_han_khac` -> `tai_san_ngan_han_khac...14`
## • `tai_san_ngan_han_khac` -> `tai_san_ngan_han_khac...18`
## • `von_chu_so_huu` -> `von_chu_so_huu...58`
## • `von_chu_so_huu` -> `von_chu_so_huu...59`
## • `lo_ke_toan_truoc_thue` -> `lo_ke_toan_truoc_thue...84`
## • `lo_ke_toan_truoc_thue` -> `lo_ke_toan_truoc_thue...93`
##
## Attaching package: 'e1071'
## The following objects are masked from 'package:moments':
##
## kurtosis, moment, skewness
## The following object is masked from 'package:ggplot2':
##
## element
HAG %>%
mutate(across(where(is.numeric), ~ format(.x, big.mark = ".", decimal.mark = ",", scientific = FALSE)))## # A tibble: 10 × 126
## Nam Tai_san_ngan_han tien...3 tien...4 cac_khoan_phai_thu_ngan_…¹
## <chr> <chr> <chr> <chr> <chr>
## 1 2.015 "13.215.916.673" "967.966.695" "967.966.695" 8.469.868.136
## 2 2.016 " 9.394.220.363" "791.208.293" "791.208.293" 6.768.206.227
## 3 2.017 " 8.815.052.625" "141.473.491" "141.473.491" 7.481.808.506
## 4 2.018 " 6.567.906.781" "337.736.719" "337.736.719" 4.747.120.864
## 5 2.019 " 7.073.675.026" "254.431.616" "254.431.616" 4.569.330.218
## 6 2.020 " 8.930.375.455" " 97.151.198" " 97.151.198" 6.410.638.635
## 7 2.021 " 7.051.853.577" " 78.298.037" " 78.298.037" 6.535.652.693
## 8 2.022 " 8.038.560.913" " 72.372.525" " 72.372.525" 6.765.361.545
## 9 2.023 " 8.768.525.586" " 41.812.548" " 41.812.548" 7.780.210.370
## 10 2.024 " 8.435.357.672" "149.708.825" "149.708.825" 7.536.948.369
## # ℹ abbreviated name: ¹cac_khoan_phai_thu_ngan_han
## # ℹ 121 more variables: phai_thu_ngan_han_cua_khach_hang <chr>,
## # tra_truoc_cho_nguoi_ban_ngan_han <chr>, phai_thu_ve_cho_vay_ngan_han <chr>,
## # phai_thu_ngan_han_khac <chr>, du_phong_phai_thu_ngan_han_kho_doi <chr>,
## # hang_ton_kho...11 <chr>, hang_ton_kho...12 <chr>,
## # du_phong_giam_gia_hang_ton_kho <chr>, tai_san_ngan_han_khac...14 <chr>,
## # chi_phi_tra_truoc_ngan_han <chr>, thue_gtgt_duoc_khau_tru <chr>, …
Trong bối cảnh HAG trải qua quá trình tái cấu trúc và chuyển đổi mô hình kinh doanh (từ bất động sản/cao su sang nông nghiệp), việc đánh giá hiệu suất hoạt động cốt lõi là tối quan trọng. Bảng KQKD là báo cáo duy nhất cung cấp bức tranh chi tiết về các nguồn thu nhập và chi phí phát sinh, cho phép nhà nghiên cứu:
Phân tích Chiều sâu Lợi nhuận: Theo dõi lợi nhuận từ các hoạt động chính (nông nghiệp) qua các tầng (Lợi nhuận gộp, Lợi nhuận từ hoạt động kinh doanh).
Đánh giá Sức khỏe Tài chính (Cơ bản): Đánh giá khả năng tạo ra dòng tiền từ hoạt động kinh doanh (trước khi xem xét các hoạt động đầu tư/tài chính).
Xác định Xu hướng (Trend Analysis): So sánh các yếu tố doanh thu và chi phí qua các năm để làm rõ tính ổn định và động lực tăng trưởng mới của HAG.
ten_bien_kqkd <- c(
"Doanh_thu_ban_hang_va_cung_cap_dich_vu",
"cac_khoan_giam_tru_doanh_thu",
"doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu",
"gia_von_hang_ban_va_dich_vu_cung_cap",
"loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu",
"doanh_thu_hoat_dong_tai_chinh",
"chi_phi_tai_chinh",
"trong_do_chi_phi_lai_vay",
"phan_lai_trong_cong_ty_lien_ket",
"chi_phi_ban_hang",
"chi_phi_quan_ly_doanh_nghiep",
"lo_thuan_tu_hoat_dong_kinh_doanh",
"thu_nhap_khac",
"chi_phi_khac",
"lo_khac",
"lo_ke_toan_truoc_thue",
"chi_phi_thue_tndn_hien_hanh",
"chi_phi_thu_nhap_thue_tndn_hoan_lai",
"lo_sau_thue_tndn",
"loloi_nhuan_sau_thue_cua_cong_ty_me",
"lo_sau_thue_cua_co_dong_khong_kiem_soat",
"lo_lai_co_ban_tren_co_phieu_vnd",
"lo_lai_suy_giam_tren_co_phieu_vnd"
)
# 2. Tạo tập con (subset)
bao_cao_kqkd_subset <- HAG %>%
select(
Nam, # Giữ lại cột Năm
any_of(ten_bien_kqkd) # Chỉ chọn các cột TỒN TẠI trong danh sách mới
)
# In ra vài dòng đầu của tập con để kiểm tra kết quả
print(head(bao_cao_kqkd_subset))## # A tibble: 6 × 23
## Nam Doanh_thu_ban_hang_va_cu…¹ cac_khoan_giam_tru_d…² doanh_thu_thuan_ve_b…³
## <dbl> <dbl> <dbl> <dbl>
## 1 2015 6252482061 -35528 6252446533
## 2 2016 6441028981 -1249713 6439779268
## 3 2017 4841225074 NA 4841225074
## 4 2018 NA NA 5388200400
## 5 2019 2091833174 -16389150 2075444024
## 6 2020 3189964886 -13318930 3176645956
## # ℹ abbreviated names: ¹Doanh_thu_ban_hang_va_cung_cap_dich_vu,
## # ²cac_khoan_giam_tru_doanh_thu,
## # ³doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
## # ℹ 19 more variables: gia_von_hang_ban_va_dich_vu_cung_cap <dbl>,
## # loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu <dbl>,
## # doanh_thu_hoat_dong_tai_chinh <dbl>, chi_phi_tai_chinh <dbl>,
## # trong_do_chi_phi_lai_vay <dbl>, phan_lai_trong_cong_ty_lien_ket <dbl>, …
## Số biến trong bộ dữ liệu là: 23
## Số quan sát trong bộ dữ liệu là: 10
## [1] "Doanh_thu_ban_hang_va_cung_cap_dich_vu"
## [2] "cac_khoan_giam_tru_doanh_thu"
## [3] "doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu"
## [4] "gia_von_hang_ban_va_dich_vu_cung_cap"
## [5] "loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu"
## [6] "doanh_thu_hoat_dong_tai_chinh"
## [7] "chi_phi_tai_chinh"
## [8] "trong_do_chi_phi_lai_vay"
## [9] "phan_lai_trong_cong_ty_lien_ket"
## [10] "chi_phi_ban_hang"
## [11] "chi_phi_quan_ly_doanh_nghiep"
## [12] "lo_thuan_tu_hoat_dong_kinh_doanh"
## [13] "thu_nhap_khac"
## [14] "chi_phi_khac"
## [15] "lo_khac"
## [16] "chi_phi_thue_tndn_hien_hanh"
## [17] "chi_phi_thu_nhap_thue_tndn_hoan_lai"
## [18] "lo_sau_thue_tndn"
## [19] "loloi_nhuan_sau_thue_cua_cong_ty_me"
## [20] "lo_sau_thue_cua_co_dong_khong_kiem_soat"
## [21] "lo_lai_co_ban_tren_co_phieu_vnd"
## [22] "lo_lai_suy_giam_tren_co_phieu_vnd"
Bộ dữ liệu Báo cáo Kết quả Hoạt động Kinh doanh có 22 biến
## tibble [10 × 23] (S3: tbl_df/tbl/data.frame)
## $ Nam : num [1:10] 2015 2016 2017 2018 2019 ...
## $ Doanh_thu_ban_hang_va_cung_cap_dich_vu : num [1:10] 6.25e+09 6.44e+09 4.84e+09 NA 2.09e+09 ...
## $ cac_khoan_giam_tru_doanh_thu : num [1:10] -35528 -1249713 NA NA -16389150 ...
## $ doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu: num [1:10] 6.25e+09 6.44e+09 4.84e+09 5.39e+09 2.08e+09 ...
## $ gia_von_hang_ban_va_dich_vu_cung_cap : num [1:10] -4.40e+09 -5.43e+09 -3.11e+09 -3.01e+09 -1.85e+09 ...
## $ loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu : num [1:10] 1.85e+09 1.01e+09 1.73e+09 2.37e+09 2.28e+08 ...
## $ doanh_thu_hoat_dong_tai_chinh : num [1:10] 1.05e+09 9.48e+08 1.69e+09 1.40e+09 2.14e+09 ...
## $ chi_phi_tai_chinh : num [1:10] -1.20e+09 -1.67e+09 -1.70e+09 -1.72e+09 -1.96e+09 ...
## $ trong_do_chi_phi_lai_vay : num [1:10] -1.08e+09 -1.58e+09 -1.59e+09 -1.53e+09 -1.26e+09 ...
## $ phan_lai_trong_cong_ty_lien_ket : num [1:10] -1898163 11710281 -18433513 64840488 12562347 ...
## $ chi_phi_ban_hang : num [1:10] -1.11e+08 -1.69e+08 -1.44e+08 -1.92e+08 -3.09e+08 ...
## $ chi_phi_quan_ly_doanh_nghiep : num [1:10] -3.50e+08 -5.02e+08 -7.08e+08 -9.89e+08 -6.73e+08 ...
## $ lo_thuan_tu_hoat_dong_kinh_doanh : num [1:10] 1.24e+09 -3.76e+08 8.30e+08 9.41e+08 -5.68e+08 ...
## $ thu_nhap_khac : num [1:10] 1.81e+08 1.76e+08 2.67e+08 2.15e+07 4.26e+07 ...
## $ chi_phi_khac : num [1:10] -6.15e+08 -1.21e+09 -6.67e+08 -9.15e+08 -1.38e+09 ...
## $ lo_khac : num [1:10] -4.35e+08 -1.03e+09 -3.99e+08 -8.93e+08 -1.34e+09 ...
## $ chi_phi_thue_tndn_hien_hanh : num [1:10] -1.54e+08 -1.03e+07 -2.48e+07 -3.00e+06 -2.25e+06 ...
## $ chi_phi_thu_nhap_thue_tndn_hoan_lai : num [1:10] -50302292 -86187524 -33735757 -38454058 98914750 ...
## $ lo_sau_thue_tndn : num [1:10] 6.02e+08 -1.50e+09 3.72e+08 6.24e+06 -1.81e+09 ...
## $ loloi_nhuan_sau_thue_cua_cong_ty_me : num [1:10] 5.02e+08 -1.14e+09 6.96e+07 1.18e+08 2.17e+08 ...
## $ lo_sau_thue_cua_co_dong_khong_kiem_soat : num [1:10] 1.00e+08 -3.66e+08 3.02e+08 -1.11e+08 -2.03e+09 ...
## $ lo_lai_co_ban_tren_co_phieu_vnd : num [1:10] 613 -1439 80 127 233 ...
## $ lo_lai_suy_giam_tren_co_phieu_vnd : num [1:10] 613 -1439 80 127 233 ...
Các biến đều là biến định lượng thể hiện các thông số số học(num).
## Tổng số giá trị bị thiếu (NA): 5
Tính giá trị trung vị (median) vào các giá trị bị thiếu.
bao_cao_kqkd_subset_sach_na <- bao_cao_kqkd_subset %>%
mutate(across(where(is.numeric),
~ ifelse(is.na(.), median(., na.rm = TRUE), .)))## 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.
bao_cao_kqkd_Tinh_trang_KD <- bao_cao_kqkd_subset_sach_na %>%
mutate(Tinh_trang_KD = ifelse(loloi_nhuan_sau_thue_cua_cong_ty_me > 0, "Có lãi", "Lỗ"))
# Chọn các cột cần thiết và hiển thị bảng với số liệu thô
bao_cao_kqkd_Tinh_trang_KD %>%
select(Nam, loloi_nhuan_sau_thue_cua_cong_ty_me, Tinh_trang_KD) %>%
kbl(caption = "Tình trạng Kinh doanh của HAG dựa trên LNST (Dữ liệu thô)",
col.names = c("Năm", "Lợi nhuận sau thuế của công ty mẹ", "Tình trạng KD"),
row.names = FALSE) %>%
kable_classic(full_width = FALSE, html_font = "Times New Roman")| Năm | Lợi nhuận sau thuế của công ty mẹ | Tình trạng KD |
|---|---|---|
| 2015 | 502343207 | Có lãi |
| 2016 | -1136650486 | Lỗ |
| 2017 | 69588012 | Có lãi |
| 2018 | 117506769 | Có lãi |
| 2019 | 216517715 | Có lãi |
| 2020 | -1255661344 | Lỗ |
| 2021 | 203030161 | Có lãi |
| 2022 | 1128745396 | Có lãi |
| 2023 | 1663970953 | Có lãi |
| 2024 | 1013433083 | Có lãi |
Tình trạng kinh doanh của HAG, được đo lường bằng Lợi nhuận sau thuế của công ty mẹ, phản ánh quá trình tái cấu trúc đầy khó khăn nhưng cuối cùng đã thành công của doanh nghiệp.Từ 2015-2020 có 2 năm Lỗ đó là năm 2016 và 2020 còn lại đều Có lãi. Giai đoạn 2015–2020 thể hiện sự bất ổn sâu sắc; công ty liên tục xen kẽ giữa trạng thái thua lỗ lớn (đặc biệt năm 2016 và 2020 với mức lỗ vượt 1,1 nghìn tỷ VND) và có lãi rất khiêm tốn. Tình trạng này khẳng định rằng mặc dù hoạt động cốt lõi có lúc tạo ra lợi nhuận gộp, nhưng toàn bộ kết quả đã bị Chi phí lãi vay khổng lồ hấp thụ, khiến lợi nhuận ròng bị xói mòn và xác nhận rủi ro tài chính cao là rào cản chính. Tuy nhiên, từ năm 2021, HAG đã đạt được bước ngoặt quan trọng, chuyển sang trạng thái có lãi liên tục và tăng trưởng bền vững. LNST tăng trưởng mạnh mẽ, đạt đỉnh gần 1,7 nghìn tỷ VND vào năm 2023, cho thấy thành công trong việc tái cơ cấu nợ và chuyển đổi mô hình kinh doanh. Sự giảm áp lực lãi vay đã giải phóng lợi nhuận gộp từ mảng nông nghiệp cốt lõi, củng cố sức khỏe tài chính và chứng minh hiệu quả sinh lời bền vững hơn của công ty trong những năm gần đây.
profitable_years <- bao_cao_kqkd_Tinh_trang_KD %>% filter(Tinh_trang_KD == "Có lãi")
print(profitable_years)## # A tibble: 8 × 24
## Nam Doanh_thu_ban_hang_va_cu…¹ cac_khoan_giam_tru_d…² doanh_thu_thuan_ve_b…³
## <dbl> <dbl> <dbl> <dbl>
## 1 2015 6252482061 -35528 6252446533
## 2 2017 4841225074 -33280844. 4841225074
## 3 2018 5197982826 -33280844. 5388200400
## 4 2019 2091833174 -16389150 2075444024
## 5 2021 2187415636 -89997270 2097418366
## 6 2022 5197982826 -87200939 5110781887
## 7 2023 6492569736 -50172537 6442397199
## 8 2024 5894531814 -111481812 5783050002
## # ℹ abbreviated names: ¹Doanh_thu_ban_hang_va_cung_cap_dich_vu,
## # ²cac_khoan_giam_tru_doanh_thu,
## # ³doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
## # ℹ 20 more variables: gia_von_hang_ban_va_dich_vu_cung_cap <dbl>,
## # loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu <dbl>,
## # doanh_thu_hoat_dong_tai_chinh <dbl>, chi_phi_tai_chinh <dbl>,
## # trong_do_chi_phi_lai_vay <dbl>, phan_lai_trong_cong_ty_lien_ket <dbl>, …
thong_ke_mo_ta <- function(data, var_name){
x <- data[[var_name]]
result <- data.frame(
Bien = var_name,
Min = min(x, na.rm = TRUE),
Q1 = quantile(x, 0.25, na.rm = TRUE),
Median = median(x, na.rm = TRUE),
Mean = mean(x, na.rm = TRUE),
Q3 = quantile(x, 0.75, na.rm = TRUE),
Max = max(x, na.rm = TRUE),
Sd = sd(x, na.rm = TRUE),
Var = var(x, na.rm = TRUE),
Skewness = skewness(x, na.rm = TRUE),
Kurtosis = kurtosis(x, na.rm = TRUE)
)
return(result)
}
cac_bien <- setdiff(names(bao_cao_kqkd_subset_sach_na), "Nam")
bang_thong_ke_day_du <- do.call(
rbind,
lapply(cac_bien, function(v) thong_ke_mo_ta(bao_cao_kqkd_subset_sach_na, v))
)
# In bảng đẹp, không rowname
bang_thong_ke_day_du %>%
kbl(caption = "Bảng Thống Kê Mô Tả Các Biến KQKD của HAG", row.names = FALSE) %>%
kable_classic(full_width = FALSE, html_font = "Times New Roman")| Bien | Min | Q1 | Median | Mean | Q3 | Max | Sd | Var | Skewness | Kurtosis |
|---|---|---|---|---|---|---|---|---|---|---|
| Doanh_thu_ban_hang_va_cung_cap_dich_vu | 2091833174 | 3.602780e+09 | 5197982826 | 4778701701.4 | 6162994499 | 6492569736 | 1.697429e+09 | 2.881266e+18 | -0.5406407 | -1.4801831 |
| cac_khoan_giam_tru_doanh_thu | -111481812 | -7.794384e+07 | -33280844 | -43640756.6 | -14086485 | -35528 | 3.981432e+07 | 1.585180e+15 | -0.4521323 | -1.5010003 |
| doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu | 2075444024 | 3.592791e+09 | 5249491144 | 4760738870.9 | 6135097400 | 6442397199 | 1.707943e+09 | 2.917068e+18 | -0.5632556 | -1.4632534 |
| gia_von_hang_ban_va_dich_vu_cung_cap | -5430638742 | -4.282861e+09 | -3360410005 | -3505832053.9 | -2981560516 | -1590448139 | 1.269370e+09 | 1.611301e+18 | -0.0214267 | -1.3488731 |
| loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu | 205730343 | 6.325128e+08 | 1233428249 | 1254906817.0 | 1823704991 | 2374705174 | 7.792549e+08 | 6.072383e+17 | -0.0365992 | -1.6164684 |
| doanh_thu_hoat_dong_tai_chinh | 280428437 | 6.358340e+08 | 1000786308 | 1062120059.4 | 1375440301 | 2137143442 | 5.760828e+08 | 3.318714e+17 | 0.3842611 | -1.1451245 |
| chi_phi_tai_chinh | -1963934151 | -1.692079e+09 | -1483654365 | -1279198249.1 | -1118636680 | 215432853 | 6.463052e+08 | 4.177104e+17 | 1.0960691 | 0.1211715 |
| trong_do_chi_phi_lai_vay | -1585315746 | -1.465539e+09 | -1166140953 | -1050382818.4 | -837852275 | 270599417 | 5.593007e+08 | 3.128173e+17 | 1.1022128 | 0.3519954 |
| phan_lai_trong_cong_ty_lien_ket | -18433513 | -4.839361e+06 | 3434652 | 6752914.9 | 10974578 | 64840488 | 2.271434e+07 | 5.159414e+14 | 1.4674739 | 1.4782446 |
| chi_phi_ban_hang | -396487002 | -2.954072e+08 | -222192512 | -231297493.0 | -150230945 | -111239060 | 9.849922e+07 | 9.702097e+15 | -0.3257026 | -1.5086149 |
| chi_phi_quan_ly_doanh_nghiep | -1851240106 | -6.988116e+08 | -425967966 | -386887348.5 | -157646627 | 1349894514 | 8.259920e+08 | 6.822629e+17 | 0.3588782 | -0.0030054 |
| lo_thuan_tu_hoat_dong_kinh_doanh | -2022124320 | -2.346216e+08 | 885249748 | 423709570.3 | 1178959094 | 1690412815 | 1.127420e+09 | 1.271077e+18 | -0.8760811 | -0.4702172 |
| thu_nhap_khac | 21546363 | 3.762621e+07 | 88909719 | 121902298.0 | 179523568 | 281127775 | 9.794905e+07 | 9.594016e+15 | 0.4893865 | -1.5195169 |
| chi_phi_khac | -1380140330 | -8.527156e+08 | -521404910 | -612196782.4 | -262012765 | -116111269 | 4.354851e+08 | 1.896473e+17 | -0.4946090 | -1.3198340 |
| lo_khac | -1337563204 | -7.785596e+08 | -364405886 | -490294484.4 | -215137882 | 102463888 | 4.537586e+08 | 2.058969e+17 | -0.5496491 | -1.1387903 |
| chi_phi_thue_tndn_hien_hanh | -153548976 | -2.117953e+07 | -3545265 | -23375871.8 | -2439711 | -885768 | 4.694315e+07 | 2.203659e+15 | -2.0762534 | 2.9380307 |
| chi_phi_thu_nhap_thue_tndn_hoan_lai | -86187524 | -3.727448e+07 | -4342050 | 27897825.1 | 83741605 | 259098512 | 1.024098e+08 | 1.048777e+16 | 0.9706841 | -0.1265296 |
| lo_sau_thue_tndn | -2383339850 | -1.125529e+09 | 249606713 | -62062960.8 | 945681602 | 1781685785 | 1.384531e+09 | 1.916927e+18 | -0.4179883 | -1.4213663 |
| loloi_nhuan_sau_thue_cua_cong_ty_me | -1255661344 | 8.156770e+07 | 209773938 | 252282346.6 | 885660614 | 1663970953 | 9.226296e+08 | 8.512454e+17 | -0.2767884 | -1.0962698 |
| lo_sau_thue_cua_co_dong_khong_kiem_soat | -2025322017 | -3.024177e+08 | -39747303 | -314345307.4 | 86685488 | 302019303 | 7.191704e+08 | 5.172061e+17 | -1.3609145 | 0.4569408 |
| lo_lai_co_ban_tren_co_phieu_vnd | -1439 | 9.175000e+01 | 226 | 248.7 | 901 | 1794 | 1.026299e+03 | 1.053289e+06 | -0.3575602 | -1.0508204 |
| lo_lai_suy_giam_tren_co_phieu_vnd | -1439 | 9.175000e+01 | 226 | 248.7 | 901 | 1794 | 1.026299e+03 | 1.053289e+06 | -0.3575602 | -1.0508204 |
bang_tang_truong_doanh_thu <- bao_cao_kqkd_subset_sach_na %>%
mutate(
Tang_truong_DT = (doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu - lag(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu)) / lag(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100
) %>%
select(Nam, doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, Tang_truong_DT)
print(bang_tang_truong_doanh_thu)## # A tibble: 10 × 3
## Nam doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu Tang_truong_DT
## <dbl> <dbl> <dbl>
## 1 2015 6252446533 NA
## 2 2016 6439779268 3.00
## 3 2017 4841225074 -24.8
## 4 2018 5388200400 11.3
## 5 2019 2075444024 -61.5
## 6 2020 3176645956 53.1
## 7 2021 2097418366 -34.0
## 8 2022 5110781887 144.
## 9 2023 6442397199 26.1
## 10 2024 5783050002 -10.2
Kết quả phân tích tốc độ tăng trưởng doanh thu của HAG cho thấy xu hướng biến động mạnh, phản ánh rõ đặc trưng của quá trình tái cấu trúc và chuyển đổi mô hình kinh doanh của doanh nghiệp. Giai đoạn 2016–2021, doanh thu ghi nhận sự suy giảm đáng kể, đặc biệt năm 2017 và 2019, khi doanh thu giảm lần lượt khoảng 25% và hơn 61%. Nguyên nhân chủ yếu đến từ việc HAG thu hẹp mảng bất động sản và cao su, đồng thời chịu áp lực lớn từ gánh nặng nợ vay và xử lý tài sản dở dang. Năm 2020 đánh dấu sự phục hồi, với doanh thu tăng trên 53%, nhờ mảng nông nghiệp – đặc biệt là sản xuất và xuất khẩu chuối – bắt đầu tạo dòng tiền ổn định. Tuy nhiên, ngay năm sau, doanh thu lại giảm do ảnh hưởng từ đại dịch COVID-19 gây gián đoạn logistics và tiêu thụ nông sản.
# 1. Tính toán Tốc độ tăng trưởng Giá vốn hàng bán (COGS Growth Rate)
bang_tang_truong_gia_von <- bao_cao_kqkd_subset_sach_na %>%
mutate(
gia_von_hang_ban = gia_von_hang_ban_va_dich_vu_cung_cap,
# Tính Tốc độ tăng trưởng Giá vốn (%)
Tang_truong_gia_von_pct = (gia_von_hang_ban - lag(gia_von_hang_ban)) / lag(gia_von_hang_ban) * 100
) %>%
select(Nam, gia_von_hang_ban, Tang_truong_gia_von_pct)
print(bang_tang_truong_gia_von)## # A tibble: 10 × 3
## Nam gia_von_hang_ban Tang_truong_gia_von_pct
## <dbl> <dbl> <dbl>
## 1 2015 -4398020571 NA
## 2 2016 -5430638742 23.5
## 3 2017 -3109682997 -42.7
## 4 2018 -3013495226 -3.09
## 5 2019 -1847659651 -38.7
## 6 2020 -2970915613 60.8
## 7 2021 -1590448139 -46.5
## 8 2022 -3937380869 148.
## 9 2023 -5148941719 30.8
## 10 2024 -3611137012 -29.9
Tốc độ tăng trưởng Giá vốn hàng bán của HAG từ năm 2016 đến 2024 thể hiện một sự biến động cực kỳ mạnh mẽ và bất thường, phản ánh những thay đổi căn bản trong quy mô và cơ cấu hoạt động của doanh nghiệp. Giai đoạn 2017-2019 chứng kiến sự sụt giảm sâu liên tiếp của Giá vốn hàng bán, đặc biệt là mức giảm -42.74% năm 2017 và -38.69% năm 2019. Điều này cho thấy HAG đã trải qua một quá trình thu hẹp quy mô hoạt động hoặc tái cấu trúc mạnh mẽ để cắt giảm chi phí đầu vào. Tuy nhiên, xu hướng này bị đảo ngược hoàn toàn trong những năm sau đó. Năm 2022 ghi nhận mức tăng trưởng Giá vốn hàng bán kỷ lục 147.56%, mức cao nhất trong toàn bộ giai đoạn, tiếp nối là mức tăng đáng kể 30.77% vào năm 2023. Sự tăng trưởng đột biến này có thể là hệ quả trực tiếp của việc mở rộng hoạt động sản xuất kinh doanh trọng điểm trở lại, đặc biệt là trong các mảng nông nghiệp và chăn nuôi, hoặc do áp lực tăng giá nguyên vật liệu trên thị trường. Sự biến động dữ dội này – chuyển từ giảm sâu sang tăng kỷ lục rồi lại giảm mạnh -29.87% vào năm 2024 – nhấn mạnh tính bất ổn cao trong quản lý chi phí và quy mô sản xuất của HAG, đồng thời cho thấy công ty luôn trong trạng thái tái cơ cấu và điều chỉnh chiến lược kinh doanh theo từng giai đoạn.
bang_bien_loi_nhuan_gop <- bao_cao_kqkd_subset_sach_na %>%
mutate(
doanh_thu_thuan = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu,
loi_nhuan_gop = loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu,
# Tính Biên lợi nhuận gộp (%)
bien_loi_nhuan_gop_pct = (loi_nhuan_gop / doanh_thu_thuan) * 100
) %>%
select(Nam, doanh_thu_thuan, loi_nhuan_gop, bien_loi_nhuan_gop_pct)
print(bang_bien_loi_nhuan_gop)## # A tibble: 10 × 4
## Nam doanh_thu_thuan loi_nhuan_gop bien_loi_nhuan_gop_pct
## <dbl> <dbl> <dbl> <dbl>
## 1 2015 6252446533 1854425962 29.7
## 2 2016 6439779268 1009140526 15.7
## 3 2017 4841225074 1731542077 35.8
## 4 2018 5388200400 2374705174 44.1
## 5 2019 2075444024 227784373 11.0
## 6 2020 3176645956 205730343 6.48
## 7 2021 2097418366 506970227 24.2
## 8 2022 5110781887 1173401018 23.0
## 9 2023 6442397199 1293455480 20.1
## 10 2024 5783050002 2171912990 37.6
Sự biến động của Biên lợi nhuận gộp HAG là một thước đo rõ ràng về sự chuyển đổi từ một công ty đa ngành sang một công ty nông nghiệp tập trung. Mức thấp trong giai đoạn 2019-2020 cho thấy khó khăn ban đầu khi chuyển đổi mô hình và chịu áp lực chi phí. Tuy nhiên, việc GPM duy trì ổn định trên 20% từ năm 2021 và đạt mức cao 37.56% vào năm 2024 xác nhận rằng hiệu suất hoạt động cốt lõi của HAG đã được cải thiện đáng kể và công ty đang quản lý tốt hơn mối quan hệ giữa Giá vốn và Doanh thu.
bang_ty_le_lai_vay <- bao_cao_kqkd_subset_sach_na %>%
mutate(
# Sử dụng giá trị tuyệt đối (abs) vì trong KQKD thường ghi nhận là số âm
chi_phi_tai_chinh_abs = abs(chi_phi_tai_chinh),
chi_phi_lai_vay_abs = abs(trong_do_chi_phi_lai_vay),
# Tính Tỷ lệ Chi phí Lãi vay trên Chi phí Tài chính (%)
ty_le_lai_vay_tren_cp_tai_chinh = (chi_phi_lai_vay_abs / chi_phi_tai_chinh_abs) * 100
) %>%
select(Nam, chi_phi_tai_chinh_abs, chi_phi_lai_vay_abs, ty_le_lai_vay_tren_cp_tai_chinh)
print(bang_ty_le_lai_vay)## # A tibble: 10 × 4
## Nam chi_phi_tai_chinh_abs chi_phi_lai_vay_abs ty_le_lai_vay_tren_cp_tai_c…¹
## <dbl> <dbl> <dbl> <dbl>
## 1 2015 1203667607 1078711240 89.6
## 2 2016 1674519826 1579381993 94.3
## 3 2017 1697932438 1585315746 93.4
## 4 2018 1721684164 1532928450 89.0
## 5 2019 1963934151 1263369664 64.3
## 6 2020 1318161483 1253570666 95.1
## 7 2021 1090293038 971878185 89.1
## 8 2022 1649147246 793176972 48.1
## 9 2023 215432853 270599417 126.
## 10 2024 688075391 716094685 104.
## # ℹ abbreviated name: ¹ty_le_lai_vay_tren_cp_tai_chinh
Chi phí lãi vay của HAG cho thấy một bức tranh rõ ràng về mức độ phụ thuộc cao vào vốn vay trong lịch sử công ty.
Giai đoạn Áp lực Cao (2018–2021): Chi phí lãi vay duy trì ở mức rất lớn, gần như chiếm toàn bộ Chi phí tài chính, khẳng định rủi ro tài chính cao. Áp lực trả lãi khổng lồ này đã bào mòn lợi nhuận gộp từ hoạt động kinh doanh (nông nghiệp) mới, thường xuyên đẩy HAG vào tình trạng lỗ thuần từ hoạt động kinh doanh.
Xu hướng Giảm và Tái cơ cấu: Gần đây, Chi phí lãi vay có xu hướng giảm, cho thấy doanh nghiệp đã đạt được những bước tiến quan trọng trong việc tái cơ cấu nợ thành công. Sự giảm thiểu này là yếu tố then chốt, giúp giảm áp lực dòng tiền và tạo cơ hội để lợi nhuận từ mảng kinh doanh cốt lõi chuyển hóa thành Lợi nhuận sau thuế.
Kết luận: Rủi ro tài chính của HAG đang có dấu hiệu giảm nhờ tái cơ cấu nợ. Tuy nhiên, do quy mô nợ cũ còn lớn, Chi phí lãi vay vẫn là một yếu tố cần theo dõi sát, bởi nó tiếp tục là rào cản chính hạn chế khả năng sinh lời bền vững và ổn định của công ty.
library(ggplot2)
library(dplyr)
library(scales) # Dùng cho hàm comma()
library(tidyr) # Dùng cho hàm pivot_longer
library(ggrepel)hag_data_viz <- bao_cao_kqkd_subset_sach_na %>%
# Thêm Tình trạng Kinh doanh và đổi tên/chọn các cột cần thiết cho việc vẽ
mutate(
# Tạo lại Tinh_trang_KD (từ đoạn code trước đó)
Tinh_trang_KD = ifelse(loloi_nhuan_sau_thue_cua_cong_ty_me > 0, "Có lãi", "Lỗ"),
# Đổi tên các cột dài sang tên ngắn gọn để dễ dàng sử dụng trong ggplot
Doanh_thu_thuan = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu,
Loi_nhuan_sau_thue = loloi_nhuan_sau_thue_cua_cong_ty_me,
Loi_nhuan_gop = loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu,
CP_Tai_chinh = chi_phi_tai_chinh,
CP_Ban_hang = chi_phi_ban_hang,
CP_Quan_ly_DN = chi_phi_quan_ly_doanh_nghiep
) %>%
# Chỉ chọn các cột cần thiết cho việc vẽ
select(Nam, Doanh_thu_thuan, Loi_nhuan_sau_thue, Loi_nhuan_gop,
CP_Tai_chinh, CP_Ban_hang, CP_Quan_ly_DN,
Tinh_trang_KD, Ty_suat_LNG, Ty_suat_LNST)plot1 <- ggplot(hag_data_viz, aes(x = Nam, y = Doanh_thu_thuan)) +
geom_line(color = "blue", size = 1) +
geom_point(color = "darkblue", size = 3) +
geom_text_repel(aes(label = comma(Doanh_thu_thuan, accuracy = 1)),
vjust = -1, color = "darkblue", size = 3.5) +
labs(title = "1. Xu hướng Doanh thu thuần của HAG (2015-2024)",
x = "Năm", y = "Doanh thu thuần",
caption = "Nguồn: Dữ liệu Báo cáo KQKD") +
theme_minimal() +
scale_y_continuous(labels = comma) +
scale_x_continuous(breaks = hag_data_viz$Nam) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
print(plot1)plot2 <- ggplot(hag_data_viz, aes(x = Nam, y = Loi_nhuan_sau_thue)) +
geom_col(aes(fill = Tinh_trang_KD), width = 0.7) +
geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) +
geom_text(aes(label = comma(Loi_nhuan_sau_thue, accuracy = 1),
vjust = ifelse(Loi_nhuan_sau_thue >= 0, -0.5, 1.5)),
color = "black", size = 3.5) +
labs(title = "2. Biến động Lợi nhuận sau thuế của HAG (2015-2024)",
x = "Năm", y = "Lợi nhuận sau thuế",
fill = "Tình trạng") +
theme_light() +
scale_fill_manual(values = c("Có lãi" = "seagreen", "Lỗ" = "tomato")) +
scale_y_continuous(labels = comma) +
scale_x_continuous(breaks = hag_data_viz$Nam) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
print(plot2)data_dt_lng_viz <- hag_data_viz %>%
select(Nam, Doanh_thu_thuan, Loi_nhuan_gop) %>%
pivot_longer(-Nam, names_to = "Chi_tieu", values_to = "Gia_tri")
plot3 <- ggplot(data_dt_lng_viz, aes(x = Nam, y = Gia_tri, color = Chi_tieu)) +
geom_line(size = 1.2) +
geom_point(size = 3) +
geom_text_repel(aes(label = comma(Gia_tri, accuracy = 1)), size = 3.5, max.overlaps = Inf) +
labs(title = "3. So sánh Doanh thu thuần và Lợi nhuận gộp",
x = "Năm", y = "Giá trị", color = "Chỉ tiêu") +
theme_bw() +
scale_color_manual(values = c("Doanh_thu_thuan" = "navy", "Loi_nhuan_gop" = "orange"),
labels = c("Doanh_thu_thuan" = "Doanh thu thuần", "Loi_nhuan_gop" = "Lợi nhuận gộp")) +
scale_y_continuous(labels = comma) +
scale_x_continuous(breaks = hag_data_viz$Nam) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "bottom")
print(plot3)# Chuẩn bị dữ liệu cho biểu đồ 4: pivot_longer và lấy abs()
data_cp_viz <- hag_data_viz %>%
select(Nam, CP_Tai_chinh, CP_Ban_hang, CP_Quan_ly_DN) %>%
pivot_longer(-Nam, names_to = "Loai_chi_phi", values_to = "So_tien") %>%
mutate(
So_tien = abs(So_tien), # Lấy giá trị tuyệt đối của Chi phí
Loai_chi_phi = case_when(
Loai_chi_phi == "CP_Tai_chinh" ~ "Chi phí Tài chính",
Loai_chi_phi == "CP_Ban_hang" ~ "Chi phí Bán hàng",
Loai_chi_phi == "CP_Quan_ly_DN" ~ "Chi phí Quản lý DN"
)
)
plot4 <- ggplot(data_cp_viz, aes(x = Nam, y = So_tien, fill = Loai_chi_phi)) +
geom_area(alpha = 0.7) +
labs(title = "Cơ cấu chi phí hoạt động qua các năm (Giá trị tuyệt đối)",
x = "Năm", y = "Chi phí (Tỷ đồng)", fill = "Loại chi phí") +
theme_classic() +
scale_fill_brewer(palette = "Set1") + # Đổi palette thành Set1 để tương phản hơn
scale_y_continuous(labels = comma) +
scale_x_continuous(breaks = hag_data_viz$Nam) +
facet_wrap(~Loai_chi_phi, scales = "free_y") + # Layer 5: Tách đồ thị theo loại chi phí
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "bottom")
print(plot4)data_ty_suat_viz <- hag_data_viz %>%
select(Nam, Ty_suat_LNG, Ty_suat_LNST) %>%
pivot_longer(-Nam, names_to = "Ty_suat", values_to = "Gia_tri")
plot5 <- ggplot(data_ty_suat_viz, aes(x = Nam, y = Gia_tri, color = Ty_suat)) +
geom_line(size = 1) +
geom_point(size = 2.5) +
geom_hline(yintercept = 0, linetype = "dotted") +
geom_text_repel(aes(label = label_percent(scale = 1, accuracy = 0.1)(Gia_tri)),
size = 3.5, max.overlaps = Inf) +
labs(title = "5. Tỷ suất Lợi nhuận gộp (GPM) và Lợi nhuận ròng (NPM) (%)",
x = "Năm", y = "Tỷ lệ (%)", color = "Loại tỷ suất") +
theme_minimal() +
scale_y_continuous(labels = label_percent(scale = 1)) +
scale_x_continuous(breaks = hag_data_viz$Nam) +
scale_color_manual(values = c("Ty_suat_LNG" = "forestgreen", "Ty_suat_LNST" = "darkred"),
labels = c("Ty_suat_LNG" = "Tỷ suất Lợi nhuận gộp (GPM)",
"Ty_suat_LNST" = "Tỷ suất Lợi nhuận ròng (NPM)")) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "bottom")
print(plot5)plot6 <- ggplot(hag_data_viz, aes(x = Doanh_thu_thuan, y = Loi_nhuan_sau_thue)) +
geom_point(aes(color = Tinh_trang_KD), size = 4, alpha = 0.8) +
geom_smooth(method = "lm", se = FALSE, color = "black", linetype="dashed") +
geom_text_repel(aes(label = Nam), size = 3.5) +
labs(title = "6. Mối quan hệ giữa Doanh thu thuần và Lợi nhuận sau thuế",
x = "Doanh thu thuần", y = "Lợi nhuận sau thuế",
color = "Tình trạng KD") +
scale_y_continuous(labels = comma) +
scale_x_continuous(labels = comma) +
scale_color_manual(values = c("Có lãi" = "seagreen", "Lỗ" = "tomato")) +
theme_gray() +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "bottom")
print(plot6)## `geom_smooth()` using formula = 'y ~ x'