Lời đầu tiên, em xin gửi lời cảm ơn chân thành và sâu sắc nhất đến thầy Trần Mạnh Tường, giảng viên bộ môn Ngôn ngữ lập trình trong phân tích dữ liệu.
Trong suốt quá trình học tập và thực hiện bài tiểu luận này, em đã nhận được sự hướng dẫn, chỉ bảo tận tình và những góp ý chuyên môn quý báu từ Thầy. Thầy không chỉ truyền đạt cho em những kiến thức nền tảng vững chắc mà còn khơi dậy trong em niềm đam mê và tư duy phân tích khoa học. Sự nhiệt tình và tâm huyết của Thầy chính là động lực lớn giúp em có thể hoàn thành tốt bài tiểu luận này.
Mặc dù đã rất nỗ lực, nhưng do kiến thức và kinh nghiệm còn hạn chế, bài tiểu luận chắc chắn không thể tránh khỏi những thiếu sót. Em rất mong nhận được những ý kiến đóng góp của Thầy để bài làm của em được hoàn thiện hơn.
Một lần nữa, em xin chân thành cảm ơn Thầy!
library(readxl)
library(dplyr)
library(ggplot2)
library(data.table)
library(tidyr)
library(knitr)
library(kableExtra)
library(viridis)
library(ggrepel)
library(scales)
library(moments)
library(corrplot)
library(treemapify)
library(ggridges)
library(tibble)
library(showtext)
library(patchwork)
Trong quá trình thực hiện đề tài, một số gói thư viện trong R được sử dụng nhằm hỗ trợ xử lý, phân tích và trực quan hóa dữ liệu một cách hiệu quả:
data <- read.csv(file.choose(), header = TRUE)
Ở dòng lệnh này, tác giả đã chọn file transactions_data.csv để thêm file csv vào RStudio. Sau đó tác giả đã gán data.frame này vào biến data để thuận lợi phân tích.
setDT(data)
kable(head(data, 5),
caption = "5 dòng dữ liệu đầu tiên của bộ dữ liệu.",
format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options = c("striped", "scale_down", "hold_position"),
font_size = 9,
position = "center")
Giải thích
Lệnh kable(head(data, 5) hiển thị 5 dòng đầu tiên của bộ dữ liệu dưới dạng bảng trong báo cáo PDF, kết hợp với kable_styling() để định dạng bảng có đường kẻ, căn giữa và thu gọn kích thước font. Mục đích nhằm kiểm tra nhanh cấu trúc dữ liệu.
Nhận xét
kable(tail(data, 5),
caption = "5 dòng dữ liệu cuối cùng của bộ dữ liệu.",
format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options = c("striped", "scale_down", "hold_position"),
font_size = 9,
position = "center")
Giải thích
Lệnh kable(tail(data, 5),…) được sử dụng để hiển thị 5 dòng dữ liệu cuối cùng trong bộ dữ liệu. Mục đích của đoạn mã là kiểm tra tính toàn vẹn của dữ liệu ở phần cuối, xác định xem dữ liệu có bị thiếu, lỗi hoặc ngắt quãng trước khi đưa vào phân tích.
Nhận xét
str(data)
## Classes 'data.table' and 'data.frame': 13305915 obs. of 12 variables:
## $ id : int 7475327 7475328 7475329 7475331 7475332 7475333 7475334 7475335 7475336 7475337 ...
## $ date : chr "2010-01-01 00:01:00" "2010-01-01 00:02:00" "2010-01-01 00:02:00" "2010-01-01 00:05:00" ...
## $ client_id : int 1556 561 1129 430 848 1807 1556 1684 335 351 ...
## $ card_id : int 2972 4575 102 2860 3915 165 2972 2140 5131 1112 ...
## $ amount : chr "$-77.00" "$14.57" "$80.00" "$200.00" ...
## $ use_chip : chr "Swipe Transaction" "Swipe Transaction" "Swipe Transaction" "Swipe Transaction" ...
## $ merchant_id : int 59935 67570 27092 27092 13051 20519 59935 39021 50292 3864 ...
## $ merchant_city : chr "Beulah" "Bettendorf" "Vista" "Crown Point" ...
## $ merchant_state: chr "ND" "IA" "CA" "IN" ...
## $ zip : num 58523 52722 92084 46307 20776 ...
## $ mcc : int 5499 5311 4829 4829 5813 5942 5499 4784 7801 5813 ...
## $ errors : chr "" "" "" "" ...
## - attr(*, ".internal.selfref")=<externalptr>
Hàm str(data) trong R được dùng để mô tả cấu trúc tổng thể của đối tượng dữ liệu, giúp nắm nhanh loại dữ liệu, số lượng quan sát, số biến, cũng như kiểu dữ liệu của từng biến. Trong trường hợp này, data là một đối tượng thuộc lớp data.table và data.frame, gồm 13.305.915 quan sát và 21 biến. Các biến bao gồm: - Kiểu số (num) — thường dùng cho định lượng. - Kiểu chuỗi (chr) — mô tả thuộc tính dạng văn bản. - Kiểu nhân tố (Factor) — đại diện cho dữ liệu phân loại. - Kiểu logic (logi) — biểu thị giá trị đúng/sai.
unique_use_chip <- data[,.N, by =.(`Phương thức` = use_chip)][order(-N)]
kable(unique_use_chip,
caption = "Thống kê số lượng theo phương thức thanh toán.",
format = "latex", booktabs = TRUE,
col.names = c("Phương thức thanh toán", "Số lượng giao dịch")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn mã trên sử dụng cú pháp của data.table để thống kê tần suất xuất hiện của từng phương thức thanh toán trong biến use_chip.
Nhận xét
Kết quả cho thấy phương thức “Swipe Transaction” chiếm tỷ trọng lớn nhất với 6.967.185 giao dịch, tiếp theo là “Chip Transaction” với 4.780.818 và “Online Transaction” với 1.557.912 giao dịch.
dim(data)
## [1] 13305915 12
Lệnh dim(data) trong R được dùng để kiểm tra kích thước của bộ dữ liệu, bao gồm số dòng (quan sát) và số cột (biến). Kết quả cho biết bộ dữ liệu có 13.305.915 quan sát và 12 biến.
names(data)
## [1] "id" "date" "client_id" "card_id"
## [5] "amount" "use_chip" "merchant_id" "merchant_city"
## [9] "merchant_state" "zip" "mcc" "errors"
unique_cat_counts <- data[, lapply(.SD, uniqueN),.SDcols = c(
"use_chip", "merchant_city", "merchant_state", "mcc")]
unique_cat_df <- data.frame(
`Biến` = names(unique_cat_counts),
`Số lượng duy nhất` = as.integer(unique_cat_counts)
)
kable(unique_cat_df,
caption = "Số lượng giá trị duy nhất cho các biến định tính.",
format = "latex", booktabs = TRUE,
col.names = c("Biến", "Số lượng duy nhất")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn lệnh sử dụng kết hợp các hàm trong data.table để đếm số lượng giá trị duy nhất (unique) của các biến định tính. Lệnh lapply(.SD, uniqueN) được áp dụng cho bốn biến: use_chip, merchant_city, merchant_state, và mcc. Kết quả được lưu sau đó chuyển thành data frame để trình bày bằng kable.
Nhận xét
Kết quả cho thấy:
na_counts <- colSums(is.na(data))
na_df <- data.frame(
`Biến` = names(na_counts),
`Số lượng NA` = na_counts
)
if(sum(na_counts) > 0) {
kable(na_df[na_counts > 0, ],
caption = "Số lượng giá trị thiếu theo từng cột.",
format = "latex", booktabs = TRUE, row.names = FALSE) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
} else {
print("Bộ dữ liệu không có giá trị NA.")
}
Giải thích
Lệnh is.na(data) tạo một ma trận logic xác định vị trí các giá trị bị thiếu, sau đó colSums() tính tổng số lượng NA cho mỗi cột. Kết quả được lưu vào na_counts và trình bày lại trong na_df.
Cấu trúc if(sum(na_counts) > 0) giúp tự động phát hiện xem dữ liệu có bị thiếu hay không (nếu có), hiển thị bảng kết quả; nếu không, in ra thông báo rằng dữ liệu đầy đủ.
Nhận xét
Kết quả cho thấy có ba biến chứa giá trị thiếu:
if ("zip" %in% names(data)) {
data[, zip := NULL]
}
print(paste("Số biến còn lại:", ncol(data)))
## [1] "Số biến còn lại: 11"
Giải thích
Lệnh if (“zip” %in% names(data)) kiểm tra xem biến zip có tồn tại trong bộ dữ liệu hay không. Nếu có, hàm data[, zip := NULL] sẽ xóa hoàn toàn cột đó ra khỏi data. Sau cùng, lệnh print(paste(“Số biến còn lại:”, ncol(data))) in ra số lượng biến còn lại nhằm xác nhận kết quả sau khi xử lý.
Nhận xét
Sau khi loại bỏ biến zip. Việc loại bỏ biến này là hợp lý vì zip là mã bưu điện, không mang ý nghĩa thống kê đáng kể trong phân tích hành vi giao dịch hoặc dự báo tài chính. Bước xử lý này giúp tối ưu hóa bộ dữ liệu, giảm độ phức tạp và tập trung hơn vào các biến có giá trị phân tích thực tiễn.
dup_count <- data[,.N, by = names(data)][N > 1,.N]
print(paste("Số hàng bị trùng lặp hoàn toàn:", dup_count))
## [1] "Số hàng bị trùng lặp hoàn toàn: 0"
Giải thích
Lệnh data[,.N, by = names(data)] nhóm toàn bộ các biến trong bảng, đếm số lần xuất hiện của từng hàng bằng .N. Sau đó, điều kiện [N > 1,.N] lọc ra các hàng có số lần xuất hiện lớn hơn 1, tức là những hàng bị lặp lại, và đếm tổng số lượng trùng lặp này.
Nhận xét
Kết quả cho thấy không có bản ghi nào bị trùng lặp hoàn toàn trong bộ dữ liệu. Điều này chứng tỏ dữ liệu được thu thập và lưu trữ có tính toàn vẹn cao, không xảy ra lỗi nhập liệu hoặc sao chép thông tin
data[, merchant_city := toupper(merchant_city)]
kable(head(data[,.N, by = merchant_city][order(-N)], 5),
caption = "Top 5 thành phố có khối lượng giao dịch cao nhất.",
format = "latex", booktabs = TRUE, col.names = c("Thành phố", "Số lượng"))%>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Lệnh data[, merchant_city := toupper(merchant_city)] được sử dụng để chuẩn hóa tên thành phố bằng cách chuyển toàn bộ ký tự sang chữ in hoa, giúp tránh sai lệch khi phân tích do khác biệt định dạng. Tiếp đó, data[,.N, by = merchant_city][order(-N)] tính tần suất xuất hiện của từng thành phố, sau đó sắp xếp giảm dần theo số lượng giao dịch.
Nhận xét
Kết quả cho thấy “ONLINE” là danh mục có số lượng giao dịch cao nhất, đạt 1.563.700 giao dịch, phản ánh xu hướng thanh toán trực tuyến đang chiếm ưu thế mạnh mẽ. Về mặt thống kê – kinh tế, kết quả này cho thấy hành vi tiêu dùng hiện đại đang chuyển dịch mạnh về thanh toán số, đồng thời phản ánh sức mua lớn tại các trung tâm kinh tế trọng điểm.
if (class(data$amount) != "numeric") {
data[, amount := as.numeric(gsub("\\$", "", amount))]
}
print(paste("Kiểu dữ liệu mới của amount:", class(data$amount)))
## [1] "Kiểu dữ liệu mới của amount: numeric"
Giải thích
Câu lệnh if (class(data\(amount) != "numeric")* kiểm tra xem cột này có phải kiểu số học hay chưa. Nếu chưa, phần *data[, amount := as.numeric(gsub("\\\)“,”“, amount))] sẽ loại bỏ ký hiệu $ khỏi dữ liệu rồi chuyển toàn bộ sang dạng số (numeric) để phục vụ cho việc tính toán, thống kê và vẽ biểu đồ.
Nhận xét
Kết quả cho thấy biến amount đã được chuyển về dạng số, giúp đảm bảo tính tương thích trong các phép toán và mô hình phân tích định lượng. Về mặt thống kê – kinh tế, việc chuẩn hóa này là cần thiết để các giá trị giao dịch có thể được so sánh, tổng hợp và phân tích xu hướng chi tiêu một cách chính xác.
if (!inherits(data$date, "POSIXct")) {
data[, date := as.POSIXct(date)]
}
data[, year := year(date)]
data[, month := month(date)]
data[, hour := hour(date)]
Giải thích
Điều kiện if() để kiểm tra xem cột date có đang ở kiểu ngày giờ chuẩn POSIXct hay chưa. Nếu chưa, lệnh POSIXct(date) sẽ chuyển toàn bộ dữ liệu trong cột này sang định dạng POSIXct – định dạng đặc trưng trong R để xử lý dữ liệu ngày và giờ.
Nhận xét
Kết quả hiển thị cho thấy biến date đã ở dạng POSIXct, đảm bảo khả năng xử lý, lọc và trích xuất thông tin thời gian chính xác như năm, tháng, ngày, hoặc khung giờ. Về mặt kinh tế, việc chuẩn hóa này giúp phân tích xu hướng giao dịch theo thời gian, từ đó hỗ trợ cho các bước phân tổ và trực quan hóa dữ liệu.
data[, payment_type := gsub(" Transaction", "", use_chip)]
kable(head(data[,.(use_chip, payment_type)]),
caption = "Mã hóa biến phương thức thanh toán",
format = "latex", booktabs = TRUE, col.names = c("use_chip","payment_type")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Lệnh data[, payment_type := gsub(” Transaction”, ““, use_chip)] sẽ tạo biến mới payment_type bằng cách loại bỏ chuỗi “ Transaction” trong cột use_chip, giúp tên phương thức trở nên ngắn gọn và dễ đọc hơn.
Nhận xét
Kết quả cho thấy biến use_chip ban đầu như “Swipe Transaction” hay “Chip Transaction” đã được rút gọn thành “Swipe” và “Chip”, giúp bảng dữ liệu trở nên sạch và dễ xử lý hơn. Về mặt thống kê – kinh tế, việc mã hóa này giúp quá trình phân tổ và trực quan hóa dữ liệu theo từng hình thức thanh toán thuận tiện hơn.
data[, hour := hour(date)]
kable(head(data[,.(date, hour)]),
caption = "Mã hóa biến giờ",
format = "latex", booktabs = TRUE, col.names = c("Ngày giờ gốc", "Giờ")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Lệnh data[, hour := hour(date)] sử dụng hàm hour() trong gói lubridate để tạo biến mới hour, chứa giá trị giờ (từ 0 đến 23) tương ứng với mỗi giao dịch.
Nhận xét
Kết quả cho thấy các giao dịch được thực hiện vào thời điểm đầu ngày (giờ 0) được trích xuất chính xác. Việc mã hóa này rất hữu ích cho phân tích hành vi giao dịch theo khung giờ, chẳng hạn xác định khung thời gian cao điểm trong ngày hoặc thói quen tiêu dùng theo thời gian.
data[, wday_num := as.POSIXlt(date)$wday]
vietnamese_days <- c("Chủ nhật", "Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm",
"Thứ sáu", "Thứ bảy")
data[, weekday := vietnamese_days[wday_num + 1]]
display_levels <- c("Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm", "Thứ sáu",
"Thứ bảy", "Chủ nhật")
data[, weekday := factor(weekday, levels = display_levels)]
data[, wday_num := NULL]
kable(head(data[,.(date, weekday)]),
caption = "Mã hóa biến thứ trong tuần",
format = "latex", booktabs = TRUE, col.names = c("Ngày giờ gốc", "Thứ"))%>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Hàm as.POSIXlt(date)$wday trích xuất giá trị số tương ứng với các ngày trong tuần (0 = Chủ nhật, 6 = Thứ bảy) và lưu vào biến tạm wday_num. Tiếp đó, vector vietnamese_days được tạo nhằm chuyển đổi các giá trị số thành tên thứ bằng tiếng Việt. Lệnh data[, weekday := vietnamese_days[wday_num + 1]] gán tên thứ phù hợp cho mỗi dòng dữ liệu. Sau đó, weekday được định dạng thành biến nhân tố (factor) với thứ tự hiển thị logic trong tuần (display_levels), giúp dễ dàng trong việc sắp xếp hoặc trực quan hóa. Cuối cùng, biến tạm wday_num được xóa để tránh dư thừa dữ liệu.
Nhận xét
Kết quả cho thấy các bản ghi ngày 01/01/2010 đều được xác định là “Thứ sáu”, chứng tỏ quá trình mã hóa ngày trong tuần được thực hiện chính xác. Việc chuyển đổi này giúp dữ liệu trở nên dễ phân tổ, đặc biệt khi phân tích xu hướng giao dịch theo ngày trong tuần. Nhờ đó, tác giả có thể phát hiện xu hướng giao dịch chẳng hạn giao dịch nhiều hơn vào cuối tuần hoặc giảm mạnh vào đầu tuần,
kable(head(data[,.(date, year, month)]),
caption = "Mã hóa biến tháng và năm",
format = "latex", booktabs = TRUE, col.names = c("Ngày giờ gốc", "Năm", "Tháng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn mã trên hiển thị một phần dữ liệu sau khi mã hóa hai biến thời gian mới là “năm” và “tháng” từ biến gốc date. Hàm year và month được dùng để tách riêng phần năm và tháng từ biến ngày giờ dạng POSIXct.
Nhận xét
Kết quả cho thấy các giá trị năm và tháng được trích xuất chính xác từ dữ liệu thời gian gốc, với các giao dịch đầu tiên đều rơi vào tháng 1 năm 2010.Việc tạo thêm hai biến này rất hữu ích cho các bước phân tổ giúp phát hiện những xu hướng dài hạn.
data[, khung_gio := cut(hour,
breaks = c(-1, 5, 11, 17, 24),
labels = c("Đêm", "Sáng", "Chiều", "Tối"))]
kable(head(data[,.(hour, khung_gio)]),
caption = "Mã hóa biến khung giờ",
format = "latex", booktabs = TRUE, col.names = c("Giờ", "Khung giờ")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Hàm cut() chia biến hour (giờ trong ngày) thành 4 khoản:“Đêm”, “Sáng”, “Chiều”, “Tối. Việc đặt breaks = c(-1, 5, 11, 17, 24) giúp bao phủ toàn bộ dải giá trị từ 0 đến 23, tránh lỗi bỏ sót giá trị biên.
Nhận xét
Kết quả minh họa cho thấy các giao dịch có “hour = 0” được xếp chính xác vào khung giờ “Đêm”, thể hiện logic mã hóa hợp lý. Biến này hỗ trợ các bước phân tích hành vi giao dịch theo thời gian, chẳng hạn như: Phát hiện khung giờ có rủi ro giao dịch cao, và hỗ trợ trực quan hóa dữ liệu bằng biểu đồ tần suất hoặc biểu đồ mật độ theo khung giờ.
data[, cuoi_tuan := ifelse(weekday %in% c("Thứ bảy", "Chủ nhật"), TRUE, FALSE)]
kable(tail(data[,.(weekday, cuoi_tuan)]),
caption = "Mã hóa biến cuối tuần'.",
format = "latex", booktabs = TRUE, col.names = c("Thứ", "Cuối tuần")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn mã trên tạo biến logic mới tên “cuoi_tuan” nhằm xác định liệu một giao dịch có diễn ra vào cuối tuần hay không.Đầu tiên tác giả sử dụng hàm ifelse() để gán giá trị: TRUE nếu biến weekday thuộc “Thứ bảy” hoặc “Chủ nhật”, FALSE cho các ngày còn lại trong tuần.
Nhận xét
Kết quả mẫu cho thấy các giao dịch rơi vào “Thứ năm” được gán giá trị FALSE, phản ánh chính xác rằng đây không phải cuối tuần. Biến này sẽ hỗ trợ cho việc phân tích hành vi giao dịch theo thời gian chẳng hạn như: so sánh khối lượng và giá trị giao dịch giữa ngày thường và cuối tuần, nhận diện xu hướng rủi ro giao dịch tăng giảm theo từng giai đoạn trong tuần.
data[, co_loi := ifelse(errors == "" | is.na(errors), FALSE, TRUE)]
kable(data[,.N, by = co_loi],
caption = "Mã hóa biến có lỗi",
format = "latex", booktabs = TRUE, col.names = c("Có lỗi", "Số lượng"))%>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn mã trên thực hiện mã hóa biến logic mới co_loi để phản ánh tình trạng lỗi của từng giao dịch trong tập dữ liệu. Sử dụng hàm ifelse() nhằm gán: FALSE cho các dòng có giá trị errors trống (““) hoặc thiếu (NA), tức là không phát sinh lỗi, TRUE nếu biến errors có nội dung khác rỗng, nghĩa là giao dịch có lỗi được ghi nhận.
Nhận xét
Kết quả cho thấy trong tổng số hơn 13,3 triệu giao dịch có: 13.094.522 giao dịch không gặp lỗi, 211.393 giao dịch có lỗi phát sinh. Tỷ lệ lỗi thấp chứng tỏ dữ liệu tương đối sạch và hệ thống ghi nhận ổn định, tuy nhiên nhóm giao dịch có lỗi vẫn cần được xem xét riêng biệt để phân tích nguyên nhân.
data_duong <- data[amount > 0]
print(paste("Số lượng giao dịch thành công:", scales::comma(nrow(
data_duong), accuracy=1,big.mark = ".", decimal.mark = ",")))
## [1] "Số lượng giao dịch thành công: 12.635.227"
Giải thích
Đoạn mã trên lọc ra các giao dịch có giá trị amount lớn hơn 0, tức là các giao dịch giảm giá trị và hợp lệ về mặt tài chính. Sử dụng cú pháp của data.table: data[amount > 0] để tạo tập con data_duong. Hàm nrow() đếm tổng số giao dịch thỏa điều kiện. Hàm scales::comma() định dạng số lượng có dấu phẩy phân tách hàng nghìn, giúp kết quả dễ đọc hơn.
Nhận xét
Kết quả cho thấy có 12.635.227 giao dịch giảm giá trị, chiếm phần lớn trong tổng số 13.305.915 bản ghi của tập dữ liệu ban đầu. Điều này chứng tỏ tỷ lệ giao dịch hoàn tiền (giá trị âm) là rất nhỏ, cho thấy dữ liệu thích hợp để tiếp tục khai thác cho các bước phân tích thống kê và mô hình hóa.
data_am <- data[amount < 0]
data_abs <- copy(data_am)
data_abs[, amount := abs(amount)]
print(paste("Số lượng giao dịch hoàn tiền:", scales::comma(nrow(data_abs),
accuracy=1,big.mark = ".", decimal.mark = ",")))
## [1] "Số lượng giao dịch hoàn tiền: 660.049"
Giải thích
Đoạn mã trên lọc ra các giao dịch có giá trị amount âm, tức là các giao dịch hoàn tiền hoặc hủy giao dịch.Lệnh data[amount < 0] tạo tập con data_am chứa các bản ghi có giá trị âm. Lệnh copy() được dùng để tránh thay đổi dữ liệu gốc khi xử lý. Trong data_abs, giá trị amount được chuyển sang trị tuyệt đối (abs(amount)) để tiện cho việc so sánh hoặc trực quan hóa sau này. Lệnh nrow() đếm tổng số giao dịch, scales::comma() định dạng hiển thị dẩu phẩy phân cách các dữ liệu số giúp dễ đọc hơn.
Nhận xét
Có 660.049 giao dịch hoàn tiền, chiếm khoảng 5% tổng số giao dịch. Tỷ lệ này ở mức chấp nhận được, phản ánh rằng hệ thống thanh toán hoạt động khá ổn định, chỉ có một phần nhỏ các giao dịch bị hoàn lại — có thể do lỗi kỹ thuật, hủy đơn hàng hoặc yêu cầu hoàn trả từ khách hàng. Việc tách nhóm này riêng giúp phân tích hành vi rủi ro hoặc hiệu suất xử lý thanh toán trong các phần sau được chính xác hơn.
data_am <- data[amount < 0]
data_abs <- copy(data_am)
data_abs[, amount := abs(amount)]
data_abs[, nhom_gia_tri := cut(amount, breaks = c(0, 50, 250, 1000, Inf),
labels = c("Rất nhỏ (0-50)", "Nhỏ (50-250)",
"Trung bình (250-1k)", "Lớn (>1k)"),
right = FALSE)]
kable(head(data_abs[,.(amount, nhom_gia_tri)]),
caption = "Mã hóa biến nhóm giá trị (Giao dịch Hoàn tiền)",
format = "latex", booktabs = TRUE, col.names = c("Giá trị", "Nhóm")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Đoạn mã trên thực hiện phân loại biến định lượng “amount” thành các nhóm định tính “nhom_gia_tri” nhằm thuận tiện cho việc mô tả và phân tích thống kê. Sử dụng hàm cut() để chia amount thành 4 khoảng giá trị theo tiêu chí:
Đặt tham số right = FALSE để cận trái bao gồm trong khoảng, giúp xác định ranh giới nhóm rõ ràng.
Nhận xét
Việc mã hóa này giúp biến giá trị giao dịch từ dạng liên tục sang dạng phân loại, hỗ trợ cho việc so sánh phân bố giao dịch giữa các mức giá trị khác nhau, xác định các nhóm giao dịch có tần suất cao (ví dụ: nhóm nhỏ hoặc trung bình).
pt_method <- data_abs[,.N, by = payment_type][order(-N)]
kable(pt_method,
caption = "Số lượng giao dịch giảm giá trị theo phương thức thanh toán",
format = "latex", booktabs = TRUE, col.names = c("Phương thức", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Từ kết quả trên cho thấy các giao dịch giảm giá trị tập trung chủ yếu ở các kênh thanh toán vật lý. Phương thức Swipe chiếm số lượng cao nhất (389.176), cao hơn nhiều so với Chip (264.190), điều này phản ánh rủi ro vận hành hoặc tỷ lệ tranh chấp cao hơn từ công nghệ thẻ từ cũ. Ngoài ra, kênh Online có số lượng giao dịch giảm với giá trị 6.683 thấp bất thường, điều này ngụ ý rằng các quy trình điều chỉnh cho kênh này có thể được xử lý riêng biệt và không được ghi lại trong tập dữ liệu.
pt_hour <- data_abs[,.N, by = khung_gio][order(-N)]
kable(pt_hour, caption = "Số lượng giao dịch giảm giá trị theo khung giờ",
format = "latex", booktabs = TRUE, col.names = c("Khung giờ", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Kết quả này cho thấy các giao dịch giảm giá trị xảy ra chủ yếu vào ban ngày, tập trung cao nhất vào chiều (292.491) và sáng (235.684). Hình thức này tương đồng với phân bố của các giao dịch thành công, cho thấy các hoạt động như hoàn trả, sửa lỗi, hoặc điều chỉnh giao dịch chủ yếu được xử lý trong giờ làm việc và hoạt động kinh doanh cao điểm. Ngoài ra số lượng giảm mạnh vào tối và đặc biệt là đêm (25.992) khẳng định rằng đây là các nghiệp vụ gắn liền với hoạt động tại cửa hàng vật lý.
pt_weekday <- data_abs[,.N, by = weekday][order(factor(weekday, levels = display_levels))]
kable(pt_weekday, caption = "Số lượng giao dịch giảm giá trị theo thứ trong tuần",
format = "latex", booktabs = TRUE, col.names = c("Thứ", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Từ kết quả trên cho thấy sự phân bố đồng đều của các giao dịch giảm giá trị trên tất cả các ngày trong tuần. Số lượng giao dịch dao động rất hẹp (từ 92.651 đến 95.678), không có sự chênh lệch đáng kể giữa ngày thường và cuối tuần.
data[, year := year(date)]
data[, month := month(date)]
data_am <- data[amount < 0]
pt_year <- data_am[,.N, by = year][order(year)]
kable(pt_year, caption = "Số lượng giao dịch giảm giá trị theo năm",
format = "latex", booktabs = TRUE, col.names = c("Năm", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Từ 2010 đến 2017, số lượng giao dịch giảm giá trị tăng đều đặn (từ 64.044 lên 69.043), cùng với sự tăng trưởng chung của tổng khối lượng giao dịch thành công. Ngoài ra, có sự sụt giảm rõ rệt vào năm 2018 và 2019 (chỉ còn 56.900). Sự sụt giảm này có thể phản ánh tác động tích cực của việc áp dụng rộng rãi công nghệ Chip hoặc sự cải thiện chung trong quy trình nghiệp vụ, giúp giảm tỷ lệ lỗi và điều chỉnh giao dịch.
pt_value <- data_abs[,.N, by = nhom_gia_tri][order(-N)]
kable(pt_value,
caption = "Số lượng giao dịch giảm giá trị theo nhóm giá trị giao dịch",
format = "latex", booktabs = TRUE, col.names = c("Nhóm giá trị", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Từ kết quả này cho thấy sự tập trung bất thường, với 92% (609.640) giao dịch giảm giá trị nằm trong nhóm nhỏ (50-250 USD)“. Ngược lại, các nhóm rất nhỏ (182) và lớn gần như không tồn tại. Điều này ngụ ý rằng việc hoàn trả sản phẩm thường rơi vào khoảng giá trị này, trong khi các giao dịch giảm giá trị rất nhỏ hoặc rất lớn có thể bị xử lý theo quy trình riêng.
pt_city <- data_abs[,.N, by = merchant_city][order(-N)][1:10]
kable(pt_city,
caption = "Top 10 thành phố có số lượng giao dịch giảm giá trị cao nhất",
format = "latex", booktabs = TRUE, col.names = c("Thành phố", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Kết quả này cho thấy các giao dịch giảm giá trị tập trung chủ yếu ở các trung tâm đô thị, với “HOUSTON” (10.906) dẫn đầu. Ngoài ra, sự thay đổi vị trí của “ONLINE”: trong khi nó dẫn đầu tuyệt đối ở các giao dịch thành công (1,55 triệu), thì ở đây nó chỉ chiếm 9.576 giao dịch, tương đương các thành phố lớn khác. Điều này củng cố luận điểm rằng bộ dữ liệu 660.000 giao dịch giảm giá trị này gần như chỉ phản ánh các nghiệp vụ tại điểm bán, còn các quy trình điều chỉnh hoặc hoàn tiền cho thương mại điện tử gần như chắc chắn được xử lý qua một kênh khác và không bị ghi nhận là giá trị âm trong bộ dữ liệu này.
pt_state <- data_abs[,.N, by = merchant_state][order(-N)][1:10]
kable(pt_state, caption = "Top 10 bang có số lượng giao dịch giảm giá trị cao nhất",
format = "latex", booktabs = TRUE, col.names = c("Bang", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Tương tự như phân tích theo thành phố, bảng xếp hạng các bang có giao dịch giảm giá trị cao nhất (dẫn đầu là CA, TX, FL, NY) gần như trùng khớp hoàn toàn với bảng xếp hạng các bang có giao dịch thành công cao nhất. Điều này cho thấy số lượng giao dịch giảm giá trị như điều chỉnh hay hoàn trả là một nghiệp vụ có tỷ lệ thuận trực tiếp với tổng khối lượng giao dịch tại điểm bán. Mặt khác, là sự vắng mặt của “NA” (kênh Online) trong top 10. Điều này củng cố luận điểm rằng quy trình điều chỉnh hoặc hoàn tiền cho kênh thương mại điện tử được xử lý riêng biệt và không nằm trong bộ dữ liệu 660.000 giao dịch này.
pt_weekend <- data_abs[,.N, by = cuoi_tuan]
kable(pt_weekend, caption = "Số lượng giao dịch giảm giá trị theo loại ngày",
format = "latex", booktabs = TRUE, col.names = c("Cuối tuần", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Bảng thống kê này cho thấy số lượng giao dịch giảm giá trị vào ngày thường (FALSE: 471.979) cao hơn khoảng 2,5 lần so với cuối tuần (TRUE: 188.070). Tuy nhiên, sự chênh lệch này chủ yếu là do có 5 ngày thường so với 2 ngày cuối tuần. Nếu tính trung bình theo ngày, số lượng giao dịch gần như hoàn toàn giống nhau (khoảng 94.000 giao dịch/ngày). Điều này khẳng định mạnh mẽ giả thuyết rằng các giao dịch giảm giá trị này không phải là hoạt động do khách hàng khởi xướng theo thời gian thực mà là các nghiệp vụ xử lý tự động của hệ thống, được lên lịch chạy hàng ngày với một khối lượng công việc nhất quán.
pt_year_method <- data_abs[,.N, by = c("year", "payment_type")]
setorder(pt_year_method, "year", "payment_type")
kable(head(pt_year_method, 10),
caption = "Số lượng giao dịch giảm giá trị theo năm và phương thức",
format = "latex", booktabs = TRUE, col.names = c("Năm", "Phương thức", "Số lượng")) %>%
kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Bảng thống kê này cho thấy quẹt thẻ từ là nguồn chính tuyệt đối của các giao dịch giảm giá trị (chiếm khoảng 63k đến 66k/năm), trong khi kênh Online (chỉ khoảng từ 900 đến 1000/năm). Điều này cho thấy rằng bộ dữ liệu 660.000 giao dịch này chủ yếu phản ánh các nghiệp vụ điều chỉnh tại điểm bán. Ngoài ra, sự tăng nhẹ của các giao dịch Swipe từ 2010-2013 cũng tương ứng với sự tăng trưởng chung của việc sử dụng thẻ từ trong giai đoạn đó, cho thấy một tỷ lệ lỗi hay điều chỉnh ổn định của công nghệ này.
pt_year_hour <- data_abs[,.N, by =.(year, khung_gio)][order(year)]
kable(head(pt_year_hour, 10),
caption = "Số lượng giao dịch giảm giá trị theo năm và khung giờ",
format = "latex", booktabs = TRUE, col.names = c(
"Năm", "Khung giờ", "Số lượng")) %>% kable_styling(font_size = 11, position = "center",latex_options = "hold_position")
Giải thích
Nhận xét
Bảng phân tích này tiếp tục củng cố luận điểm về nghiệp vụ xử lý tại điểm bán. Trong đó, các giao dịch giảm giá trị tập trung cao nhất vào chiều và sáng. Đây là giờ hoạt động kinh doanh cao điểm và gần như rất ít vào đêm. Hình thức này lặp lại ổn định qua các năm 2010, 2011, 2012. Từ những điều đó cho thấy các nghiệp vụ vận hành như hoàn trả, điều chỉnh được thực hiện bởi nhân viên trong giờ làm việc, chứ không phải các sự kiện tự động, và quy mô của chúng tăng trưởng ổn định cùng với tổng khối lượng giao dịch chung.
Ở phần này, em sẽ tiến hành dùng các câu lệnh tính các đặc trưng thống kê, sau đó lập các bảng thống kê mô tả, cụ thể như sau:
summary_abs_method <- data_abs[,.(
`Số lượng` =.N,`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(`Phương thức` = payment_type)]
cols_to_round <- c("Trung bình","Độ lệch chuẩn","Phương sai","Q1","Trung vị","Q3","Độ xiên","Độ nhọn")
summary_abs_method[, (cols_to_round) := round(.SD, 2),.SDcols = cols_to_round]
kable(summary_abs_method, caption="Thống kê mô tả số lượng giao dịch theo phương thức
thanh toán", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9,
position="center")
Nhận xét
Từ bảng trên cho thấy Swipe và Chip chiếm phần lớn giao dịch (tổng 633.566 trên 640.249 giao dịch), trong đó Swipe dẫn đầu với 369.176 giao dịch với trung bình 101.75 USD, cao hơn nhiều so với trung vị 78 USD, do phân bố lệch phải mạnh (độ xiên 3.08). Điều này cho thấy đa số giao dịch nhỏ khoảng 78 giao dịch, nhưng có đuôi dài gồm các khoản điều chỉnh lớn kéo trung bình lên. Hơn thế nữa, Chip cũng tương tự, với độ xiên 3.35 thậm chí lệch hơn Swipe. Trong khi đó, Online chỉ có 6.683 giao dịch, nhưng lại rất đặc biệt với trung bình (297.56) gần bằng trung vị (296), độ xiên gần như bằng0. Điều này chứng tỏ đây là các giao dịch có giá trị ổn định, có thể là quy trình tự động hóa như hoàn tiền cố định hoặc bù trừ hệ thống và không mang tính ngẫu nhiên như hai kênh còn lại. Chính khác biệt rõ rệt này cần được xem xét riêng trong quản lý rủi ro và báo cáo tài chính.
summary_abs_hour <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),`Độ xiên` = skewness(
amount, na.rm=TRUE),`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(`Khung giờ`=khung_gio)]
summary_abs_hour[, (cols_to_round) := round(.SD,2),.SDcols=cols_to_round]
kable(summary_abs_hour, caption="Thống kê mô tả số lượng giao dịch giảm giá trị theo
khung giờ giao dịch", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9,
position="center")
Nhận xét
Từ bảng thống kê mô tả cho thấy giao dịch giảm giá trị tập trung nhiều nhất vào đêm (25.992 giao dịch, trung bình 177.07), có độ lệch chuẩn cao (136.78), do đó biến động lớn. Buổi sáng có số lượng giao dịch thấp nhất nhưng độ nhọn cao nhất (19.61), điều này phản ánh dữ liệu phân bố dồn về trung tâm. Buổi chiều và tối có mức trung bình lần lượt là 94 và 130, nhưng chiều có độ lệch chuẩn nhỏ hơn do đó ổn định hơn. Nhìn chung, đêm là khung giờ giao dịch mạnh nhất, trong khi sáng ít giao dịch nhưng ổn định hơn.
summary_abs_weekday <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(Thứ=weekday)]
summary_abs_weekday[, (cols_to_round):=round(.SD,2),.SDcols=cols_to_round]
kable(summary_abs_weekday, caption="Thống kê mô tả số lượng giao dịch giảm giá trị theo
thứ trong tuần", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Từ bảng thống kê cho thấy số lượng giao dịch giảm giá trị dao động theo ngày trong tuần, với thứ hai là cao nhất với (95.678 giao dịch, trung bình 106.21), tiếp theo là thứ tư và thứ năm (khoảng 95.000 giao dịch). Ngoài ra, thứ sáu có trung bình cao nhất (105.19) nhưng độ lệch chuẩn lớn (88.62, do đó biến động mạnh. Mặt khác, chủ nhật có độ nhọn cao nhất (12.88) do đó phân bố dồn về trung tâm, trong khi thứ bảy có độ nhọn thấp nhất (10.51) nên dữ liệu trải rộng hơn. Nhìn chung, tất cả các ngày đều có min = 0.01–0.08 và max = 500 nên tập giá trị số lương giao dịch giảm giá trị cố định.
summary_abs_year <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(Năm=year)]
summary_abs_year[, (cols_to_round):=round(.SD,2),.SDcols=cols_to_round]
kable(summary_abs_year, caption="Thống kê mô tả số lượng giao dịch giảm giá trị theo
năm", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Từ bảng thống kê mô tả trên cho thấy các giao dịch giảm giá trị xảy ra nhiều nhất vào giờ kinh doanh cao điểm (chiều và sáng). Tuy nhiên, có sự khác biệt rõ rệt về bản chất: các giao dịch ban ngày có giá trị trung bình thấp (từ 91 đến 94 giao dịch) và lệch phải mạnh với độ xiên là 4, cho thấy đây là các khoản điều chỉnh nhỏ. Ngược lại, đêm và tối có khối lượng thấp hơn nhưng giá trị trung bình lần lượt là 177 và 130 cao hơn hẳn ngày và chiều. Chính vì thế, các nghiệp vụ điều chỉnh giá trị cao, có thể là các lô xử lý đặc thù hoặc rủi ro, được thực hiện ngoài giờ hành chính.
summary_abs_value <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(`Nhóm giá trị`=nhom_gia_tri)]
summary_abs_value[, (cols_to_round):=round(.SD,2),.SDcols=cols_to_round]
kable(summary_abs_value, caption="Thống kê mô tả số lượng giao dịch giảm giá trị theo
nhóm giá trị giao dịch", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Từ bảng thống kê mô tả trên cho thấy 609.640 giao dịch giảm giá trị tập trung vào nhóm nhỏ (50-250), với phân bố lệch phải (độ xiên 3.08), từ đó cho thấy đây là các nghiệp vụ phổ biến nhưng có rủi ro cao. Ngoài ra, ở nhóm trung bình (250-1k) thì phân bố đối xứng với độ xiên 0.00 và trung bình (374.65 giao dịch) gần trùng khớp trung vị (375.00 giao dịch). Điều này cho thấy 50.227 giao dịch này rất có thể là các bút toán điều chỉnh tự động, có giá trị cố định (khoảng 375 giao dịch), chứ không phải các khoản hoàn tiền ngẫu nhiên. Nhóm rất nhỏ (182 giao dịch) điều này cho thấy một ngưỡng tối thiểu cho các giao dịch điều chỉnh.
summary_abs_city <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(`Thành phố`=merchant_city)][order(-`Số lượng`)][1:10]
summary_abs_city[, (cols_to_round[1:3]):=round(.SD,2),.SDcols=cols_to_round[1:3]]
kable(summary_abs_city, caption="Thống kê mô tả top 10 thành phố có lượng giao dịch
cao nhất",format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Bảng thống kê này cho thấy hai hình thức giảm giá trị hoàn toàn riêng biệt:
Hình thức 1: Điều chỉnh vận hành (đa số các thành phố): Các thành phố như “HOUSTON”, “OLYMPIA”, “FARMINGTON” có phân bố lệch phải mạnh với độ xiên từ 3 đến 8. Ngoài ra giá trị trung bình từ 75 đến 82 giao dịch cao hơn Trung vị từ 75 đến 76 giao dịch. Điều này phản ánh các nghiệp vụphần lớn là các giao dịch điều chỉnh hoặchoàn tiền nhỏ và một số ít các giao dịch giá trị lớn kéo trung bình lên.
Hình thức 2: Nghiệp vụ cố định (các ngoại lệ): “ONLINE”, “OAKLAND”, và “LAS VEGAS” là các ngoại lệ. Phân bố của chúng hoàn toàn đối xứng (độ xiên xấp xỉ 0), với trung bình gần bằng trung vị (ví dụ: ONLINE 298.7 giao dịch với 298 giao dịch). Từ đó, ta có thể khẳng định không phải là các khoản hoàn tiền ngẫu nhiên, mà là các quy trình điều chỉnh tự động, có giá trị cố định.
summary_abs_state <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(Bang=merchant_state)][order(-`Số lượng`)][1:10]
summary_abs_state[, (cols_to_round[1:3]):=round(.SD,2),.SDcols=cols_to_round[1:3]]
kable(summary_abs_state, caption="Thống kê mô tả top 10 bang có lượng giao dịch cao
nhất", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Bảng thống kê mô tả này cho thấy hai hình thức rủi ro khác nhau giữa các bang:
Hình thức 1: Đa số các bang hàng đầu (như CA, TX, FL, NY, IL) đều cho thấy một mô hình nhất quán: các giao dịch giảm giá trị có phân bố lệch phải mạnh (Độ xiên 3.0 - 4.0). Giá trị Trung vị (77-79 giao dịch) thấp hơn đáng kể so với giá trị trung bình (91-102 giao dịch). Về mặt kinh tế, điều này có nghĩa là rủi ro ở các bang này chủ yếu bao gồm các nghiệp vụ điều chỉnh/hoàn tiền nhỏ, phổ biến, nhưng vẫn tồn tại một “đuôi rủi ro” gồm các giao dịch giá trị rất lớn kéo trung bình lên.
Hình thức 2: “MI” (Michigan) là một ngoại lệ đáng chú ý. Bang này có giá trị trung bình (132.85 giao dịch) cao hơn hẳn so với các bang khác, và phân bố ít lệch hơn (độ xiên 1.8). Điều này cho thấy các giao dịch giảm giá trị ở Michigan có xu hướng xảy ra ở mức giá trị cao hơn, điều này gợi ý về một cơ cấu ngành hàng hoặc rủi ro vận hành khác biệt so với các bang còn lại.
summary_abs_weekend <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(`Cuối tuần`=cuoi_tuan)]
summary_abs_weekend[, (cols_to_round):=round(.SD,2),.SDcols=cols_to_round]
kable(summary_abs_weekend, caption="Thống kê mô tả số lượng giao dịch giảm giá trị theo
loại ngày", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Từ bảng trên cho thấy sự giống hệt nhau trong cấu trúc phân bố giao dịch giảm giá trị giữa “Ngày thường” (FALSE) và “Cuối tuần” (TRUE). Mặc dù có 5 ngày thường và 2 ngày cuối tuần, các chỉ số cốt lõi như giá trị trung bình ($102.16 và $102.64), trung vị ($78 và $79), và độ xiên (3.05 và 3.03) là gần như y hệt. Về mặt kinh tế, điều này bác bỏ giả thuyết về hoạt động do khách hàng khởi xướng như trả hàng. Thay vào đó, có thể khẳng định rằng đây là các nghiệp vụ xử lý tự động theo lô, được hệ thống lên lịch chạy hàng ngày với một quy trình nghiệp vụ và cấu trúc rủi ro nhất quán, bất kể là ngày làm việc hay ngày nghỉ.
summary_abs_year_method <- data_abs[,.(
`Số lượng` =.N,
`Trung bình` = mean(amount, na.rm=TRUE),
`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),
`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),
Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(Năm=year, `Phương thức`=payment_type)][order(Năm,`Phương thức`)]
summary_abs_year_method[, (cols_to_round[1:3]):=round(.SD,2),.SDcols=cols_to_round[1:3]]
kable(head(summary_abs_year_method,10), caption="Thống kê mô tả số lượng giao dịch
giảm giá trị theo năm và phương thức", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Bảng thống kê chi tiết này củng cố mạnh mẽ kết luận rằng “Swipe” và “Online” là hai quy trình nghiệp vụ hoàn toàn khác biệt.
Kênh “Swipe”: Phân bố của “Swipe” lệch phải mạnh với độ xiên ~3.2 và cực kỳ ổn định qua 5 năm. Ngoài ra trung vị luôn là $78, trong khi trung bình lại là $99. Về mặt kinh tế, điều này cho thấy các nghiệp vụ giảm giá trị tại điểm bán có một quy trình vận hành nhất quán: phần lớn là các điều chỉnh nhỏ (quanh $78) và một đoạn rủi ro gồm các giao dịch lớn (lên đến $5000).
Kênh “Online”: Kênh “Online” có phân bố đối xứng tuyệt đối với độ xiên \(\approx 0\), với trung bình ($295.46) gần như trùng khớp hoàn toàn với trung vị ($295.00). Hình thức này lặp lại y hệt mỗi năm, bác bỏ giả thuyết đây là các khoản hoàn tiền hoặc tranh chấp ngẫu nhiên, mà khẳng định đây là một quy trình tự động, có giá trị cố định (khoảng $295 đến $$299) của hệ thống.
summary_abs_year_hour <- data_abs[,.(`Số lượng` =.N,`Trung bình` = mean(
amount, na.rm=TRUE),`Độ lệch chuẩn` = sd(amount, na.rm=TRUE),`Phương sai` = var(amount, na.rm=TRUE),
Min = min(amount, na.rm=TRUE),Q1 = quantile(amount, 0.25, na.rm=TRUE),
`Trung vị` = median(amount, na.rm=TRUE),
Q3 = quantile(amount, 0.75, na.rm=TRUE),
Max = max(amount, na.rm=TRUE),
`Độ xiên` = skewness(amount, na.rm=TRUE),
`Độ nhọn` = kurtosis(amount, na.rm=TRUE)
), by=.(Năm=year, `Khung giờ`=khung_gio)][order(Năm,`Khung giờ`)]
summary_abs_year_hour[, (cols_to_round[1:3]):=round(.SD,2),.SDcols=cols_to_round[1:3]]
kable(head(summary_abs_year_hour,10), caption="Thống kê mô tả số lượng giao dịch
giảm giá trị theo năm và khung giờ", format = "latex", booktabs = TRUE) %>%
kable_styling(latex_options=c("scale_down","hold_position"), font_size=9, position="center")
Nhận xét
Bảng thống kê cho thấy một mô thức nghiệp vụ kép, rất ổn định qua các năm. Các giao dịch giảm giá trị trong giờ hành chính (sáng và chiều) chiếm số lượng lớn nhất, nhưng có giá trị trung bình thấp (91-94 giao dịch) và phân bố lệch phải mạnh (độ xiên khoảng 4.0), cho thấy đây là các nghiệp vụ điều chỉnh hoặc hoàn tiền nhỏ, phổ biến tại điểm bán. Ngược lại, các giao dịch tối và đêm có khối lượng thấp hơn nhiều nhưng giá trị trung bình (130-181 giao dịch) và trung vị (83-98 giao dịch) cao hơn hẳn. Điều này ngụ ý rằng các nghiệp vụ điều chỉnh giá trị cao, có thể là các lô xử lý tự động hoặc rủi ro, được thực hiện ngoài giờ kinh doanh cao điểm.
ggplot(pt_method, aes(x = reorder(payment_type, -N), y = N, fill = payment_type)) +
geom_col(width = 0.65, alpha = 0.9, show.legend = FALSE) +
geom_text(aes(
label = scales::comma(N,big.mark = ".", decimal.mark = ",")),
vjust = -0.4, size = 3.2, fontface = "bold") +
geom_hline(yintercept = mean(
pt_method$N), color = "red", linetype = "dashed", size = 1) + annotate(
"text",x = Inf, y = mean(pt_method$N) * 1.05,
label = paste("Trung bình:",
scales::comma(mean(pt_method$N), accuracy = 1,big.mark = ".", decimal.mark = ",")),
color = "red", fontface = "italic", size = 3.5, hjust = 1.05, vjust = -0.5) +
scale_fill_brewer(palette = "Set2") + scale_y_continuous(labels = scales::comma) +
labs(title = "Số lượng giao dịch giảm giá trị theo phương thức thanh toán",
x = "Phương thức", y = "Số lượng giao dịch") + theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5))
Giải thích
Nhận xét
Biểu đồ cho thấy giao dịch giảm giá trị chủ yếu tập trung ở kênh vật lý. Phương thức quẹt thẻ từ chiếm tỷ trọng lớn nhất với 389.176 giao dịch, cao hơn so với chip (264.190). Điều này phản ánh rủi ro vận hành hoặc tỷ lệ tranh chấp cao hơn từ công nghệ thẻ từ cũ. Kênh Online có số lượng rất thấp (chỉ 6.683), gợi ngụ ý rằng các quy trình điều chỉnh cho kênh này có thể được xử lý riêng biệt và không được ghi lại trong bộ dữ liệu.
pt_method <- data_abs[,.N, by = payment_type][order(-N)]
pt_method <- pt_method %>%
mutate(perc = N / sum(N) * 100,
payment_type = factor(payment_type,levels = c("Swipe", "Chip", "Online"),
labels = c("Swipe (Quẹt thẻ từ)","Chip (Thẻ gắn chip)",
"Online (Thanh toán trực tuyến)")))
ggplot(pt_method, aes(x = 2, y = perc, fill = payment_type)) +
geom_bar(stat = "identity", width = 1, color = "white") +
coord_polar(theta = "y", start = 0) +
geom_text(aes(label = paste0(round(perc, 1), "%")),
position = position_stack(vjust = 0.5),
size = 3.5, color = "black", fontface = "bold") +
scale_fill_brewer(palette = "Pastel1", name = "Phương thức thanh toán") +
xlim(0.5, 2.5) + labs( title = "Tỷ trọng giao dịch giảm giá trị theo phương thức") +
theme_void(base_size = 12) +
theme(legend.position = "right", legend.title = element_text(face = "bold"),
plot.title = element_text(face = "bold", hjust = 0.5, size = 14),
plot.subtitle = element_text(face = "italic", hjust = 0.5, size = 10))
Giải thích
Nhận xét
Biểu đồ donut cho thấy Swipe chiếm 59% giao dịch giảm giá trị, là kênh chủ đạo, tiếp theo là Chip (40%), trong khi Online chỉ 1% rất ít so với tổng thể. Điều này cho thấy phần lớn điều chỉnh hoặc hoàn tiền xảy ra tại điểm bán vật lý (quẹt thẻ hoặc chip), trong khi kênh trực tuyến hiếm khi phát sinh giao dịch giảm giá trị. Sự chênh lệch rõ rệt giữa các kênh gợi ý cần tập trung kiểm soát rủi ro ở Swipe và Chip, đồng thời xem xét lại quy trình Online để xác định nguyên nhân tỷ trọng quá thấp có thể do hệ thống tự động hóa hoặc hạn chế chính sách.
ggplot(data_abs, aes(x = khung_gio, fill = khung_gio)) +
geom_bar(width = 0.7, alpha = 0.8, color = "white") +
scale_fill_brewer(palette = "Pastel1") +
labs(title = "Phân bố giao dịch giảm giá trị theo khung giờ trong ngày",
x = "Khung giờ", y = "Số lượng giao dịch") +
geom_text(stat = "count", aes(label = scales::comma(
after_stat(count)),big.mark = ".", decimal.mark = ","),
vjust = -0.5, size = 3.2) + theme_minimal() +
theme(plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
plot.subtitle = element_text(face = "italic", size = 10, hjust = 0.5),
legend.position = "none"
)
Giải thích
Nhận xét
Biểu đồ cột cho thấy giao dịch giảm giá trị tập trung mạnh vào ban ngày, đặc biệt là khung giờ chiều (292.491) và sáng (235.684), chiếm đa số tổng khối lượng. Đây là các khung giờ hoạt động kinh doanh cao điểm, nơi các nghiệp vụ như hoàn trả hoặc sửa lỗi được xử lý. Số lượng giao dịch giảm mạnh ở tối và đặc biệt thấp ở đêm (chỉ 25.992). Từ đó cho thấy đây là các nghiệp vụ gắn liền với hoạt động tại cửa hàng vật lý, không phải giao dịch trực tuyến tự động.
Giải thích
Nhận xét
Biểu đồ cột chồng này cho thấy sự thay đổi rõ rệt trong cơ cấu giao dịch giảm giá trị theo phương thức thanh toán qua các năm. Từ 2010 đến 2014, kênh Swipe chiếm gần như tuyệt đối với hơn 63.000 giao dịch mỗi năm. Tuy nhiên, từ 2015, Chip đột ngột trở thành kênh chính, đánh dấu sự chuyển dịch công nghệ. Kênh Online luôn ở mức rất thấp, khẳng định đây là một nghiệp vụ riêng biệt, không nằm trong dòng chảy chính của dữ liệu. Sự sụt giảm mạnh vào 2019 cho thấy hệ thống đã được cải tiến, giảm thiểu các giao dịch cần điều chỉnh.
pt_year <- data_abs[,.N, by = year][order(year)]
nudge_y_values <- ifelse(
pt_year$year %in% c(2013, 2014, 2015, 2016, 2017, 2018), 30000,-30000)
nudge_x_values <- ifelse( pt_year$year == 2010, 0.2,
ifelse(pt_year$year == 2019, -0.2, 0))
ggplot(pt_year, aes(x = year, y = N)) +
geom_line(aes(color = "Số lượng thực tế"), size = 1.2) +
geom_point(size = 2.5, color = "#E41A1C") +
geom_smooth(aes(color = "Xu hướng"), method = "lm", se = FALSE,
linetype = "dashed", size = 1) +
ggrepel::geom_label_repel( aes(label = scales::comma(
N, accuracy = 1),big.mark = ".", decimal.mark = ","), size = 3.2,
color = "black", fill = "white", nudge_y = nudge_y_values,
nudge_x = nudge_x_values,
segment.linetype = "dashed",segment.size = 0.3, segment.color = "gray50",
box.padding = 0.1) +
scale_color_manual( name = "Chú thích:",
values = c("Số lượng thực tế" = "#0072B2", "Xu hướng" = "gray40")
) +
scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0.1, 0.15))) +
scale_x_continuous(breaks = sort(unique(pt_year$year))) +
labs(title = "Xu hướng tăng trưởng giao dịch giảm giá trị theo năm",
x = "Năm", y = "Số lượng giao dịch") + theme_minimal(base_size = 12) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, face = "italic"),
legend.position = "bottom")
Giải thích
Nhận xét
Biểu đồ thể hiện xu hướng tăng trưởng số lượng giao dịch giảm giá trị từ 2010–2019. Từ năm 2010 (64.044) đến 2017 (69.043), con số tăng nhẹ, ổn định quanh mốc 67.000–69.000. Tuy nhiên, năm 2018 bắt đầu giảm (67.534) và rơi mạnh vào 2019 xuống còn 56.900, đây là mức thấp nhất trong giai đoạn. Tuy nhiên, sau chu kỳ tăng trưởng kéo dài, thị trường đã bước vào giai đoạn chững lại hoặc suy giảm rõ rệt từ cuối 2018.
amount_trim <- data_abs[amount <= quantile(amount, 0.99, na.rm = TRUE)]$amount
mean_val <- mean(amount_trim, na.rm = TRUE)
ggplot(data.frame(amount = amount_trim), aes(x = amount)) +
geom_histogram(aes(y =..density..),
bin= 20, fill = "#56B4E9", color = "white", alpha = 0.7) +
geom_density(color = "red", size = 1) +
geom_vline(xintercept = mean_val, color = "black", linetype = "dashed", size = 1.1) +
annotate("text", x = mean_val + 10, y = 0.015,
label = paste0("Trung bình = ", round(mean_val, 1)),
color = "black", hjust = 0) +
labs(title = "Phân phối giá trị giao dịch giảm giá trị",
x = "Giá trị giao dịch ($)", y = "Mật độ") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5))
Giải thích
Nhận xét
Biểu đồ mật độ cho thấy phần lớn giao dịch giảm giá trị tập trung ở mức thấp, với đỉnh mật độ cao nhất quanh khoảng 70–100 USD, phù hợp với giá trị trung bình 98.6 USD được đánh dấu. Phân bố lệch phải rõ rệt: phần lớn giao dịch nhỏ, nhưng có “đuôi dài” kéo đến 500 USD, phản ánh các khoản điều chỉnh hoặc hoàn tiền lớn hiếm gặp nhưng ảnh hưởng mạnh đến trung bình. Dạng phân phối này cho thấy tính chất bất đối xứng, thế nên cần thận trọng khi dùng trung bình làm thước đo đại diện; nên kết hợp trung vị để đánh giá chính xác hơn.
pt_weekday_perc <- pt_weekday %>%
mutate( perc = N / sum(N), label_text = paste0(scales::percent(
perc, accuracy = 0.1)))
ggplot(pt_weekday_perc, aes(x = "", y = perc, fill = weekday)) +
geom_col(color = "white", width = 1) +
coord_polar(theta = "y", start = 0) +
ggrepel::geom_text_repel(aes(label = label_text),
position = position_stack(vjust = 0.5),
size = 3.5, color = "black", fontface = "bold",
segment.color = "gray50", show.legend = FALSE) +
scale_fill_brewer(palette = "Pastel2", name = "Thứ:") + labs(
title = "Tỷ trọng số lượng giao dịch giảm giá trị theo thứ trong tuần") +
theme_void(base_size = 12) +
theme( plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "right"
)
Giải thích
Nhận xét
Biểu đồ tròn này cho thấy tỷ trọng của các giao dịch giảm giá trị được phân bố đồng đều gần như tuyệt đối qua tất cả các ngày trong tuần, với mỗi ngày chiếm xấp xỉ 14.0% - 14.5% (tương đương 1/7). Sự đồng đều này, đặc biệt là việc không có sự khác biệt giữa ngày thường và cuối tuần. Do đó đây không phải là các nghiệp vụ do khách hàng khởi xướng như trả hàng. Thay vào đó, đây là bằng chứng cho thấy các giao dịch này là quy trình xử lý theo lô tự động của hệ thống, được lên lịch chạy hàng ngày với một khối lượng công việc nhất quán.
ggplot(pt_city, aes(area = N, fill = merchant_city, label = paste0(
merchant_city, "\n", scales::comma(
N,big.mark = ".", decimal.mark = ",")))) + geom_treemap(color = "white", size = 1) +
geom_treemap_text( color = "black", place = "centre", grow = TRUE, reflow = TRUE,
min.size = 6, size = 10 ) +
scale_fill_brewer(palette = "Paired") +
labs(title = "Top 10 thành phố có số lượng giao dịch giảm giá trị cao nhất")+
theme_void() +
theme( plot.title = element_text(hjust = 0.5, face = "bold", size = 16),
plot.subtitle = element_text(hjust = 0.5, face = "italic"),
legend.position = "none")
Giải thích
Nhận xét
Biểu đồ này cho thấy sự phân bổ của các giao dịch giảm giá trị cân bằng hơn nhiều so với giao dịch thành công. Khác với trước đây, “ONLINE” (9.576) không còn thống trị mà chỉ ngang hàng với các thành phố vật lý lớn khác, trong khi “HOUSTON” (10.906) lạị dẫn đầu. Điều này cho thấy bộ dữ liệu 660.000 giao dịch này chủ yếu phản ánh các nghiệp vụ điều chỉnh tại điểm bán vật lý, và số lượng ONLINE thấp tiếp tục ngụ ý rằng quy trình điều chỉnh hoặc hoàn tiền cho thương mại điện tử được xử lý riêng biệt và không nằm trong bộ dữ liệu này.
pt_state <- data_abs[,.N, by = merchant_state][order(-N)][1:10]
ggplot(pt_state, aes(x = reorder(merchant_state, N), y = N, fill = merchant_state)) +
geom_col(alpha = 0.9, color = "black") +
coord_flip() +
geom_text(aes(label = scales::comma(N,big.mark = ".", decimal.mark = ",")),
hjust = 1.1, color = "black", size = 4) +
scale_y_continuous(labels = scales::comma) +
scale_fill_manual(values = c("#A6CEE3", "#B2DF8A", "#FDBF6F", "#CAB2D6",
"#FB9A99", "#8DD3C7", "#FFFFB3", "#BEBADA",
"#80B1D3", "#FFED6F")) +
labs(title = "So sánh 10 bang có số lượng giao dịch giảm giá trị cao nhất",
x = "Bang", y = "Số lượng giao dịch") +
theme_minimal() + theme(legend.position = "none",
plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, face = "italic"))
Giải thích
Nhận xét
Biểu đồ cột ngang cho thấy sự phân bố giao dịch giảm giá trị theo bang, với California (CA) dẫn đầu tuyệt đối (84.479 giao dịch), gấp đôi Texas (TX). Điều này phản ánh quy mô kinh tế và lưu lượng giao dịch tại các bang lớn. Tuy nhiên, phát hiện quan trọng là sự vắng mặt của kênh Online trong top 10, cho nên các nghiệp vụ điều chỉnh hoặc hoàn tiền cho thương mại điện tử được xử lý riêng biệt và không nằm trong bộ dữ liệu này.
pt_weekend <- data_abs[,.N, by = cuoi_tuan]
pt_weekend[, perc := round(N / sum(N) * 100, 1)]
ggplot(pt_weekend,aes(x = factor(cuoi_tuan, labels = c("Ngày thường", "Cuối tuần")),
y = N, fill = cuoi_tuan)) +geom_col(
width = 0.6, alpha = 0.9, color = "gray20", show.legend = FALSE) +
geom_text(aes(label = scales::comma(N,big.mark = ".", decimal.mark = ",")),
vjust = -1.2, size = 3.5, color = "gray20", fontface = "bold") +
geom_text(aes(y = N / 2, label = paste0(perc, "%")),
vjust = 0.5, size = 3.8, color = "white", fontface = "bold") +
scale_fill_manual(values = c("#80cbc4", "#4db6ac")) +
labs(title = "So sánh giao dịch giảm giá trị: Ngày thường và Cuối tuần",
x = "Loại ngày", y = "Số lượng giao dịch") +
scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.1))) +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", hjust = 0.5, size = 14),
plot.subtitle = element_text(hjust = 0.5, size = 11, color = "gray30"),
axis.title.x = element_text(face = "bold"),
axis.title.y = element_text(face = "bold"),axis.text = element_text(size = 10))
Giải thích
Nhận xét
Biểu đồ cột này so sánh khối lượng giao dịch giảm giá trị giữa ngày thường (471.979) và cuối tuầ (188.070). Dù số lượng chênh lệch lớn, tỷ trọng cho thấy 71.5% giao dịch xảy ra vào ngày thường, trong khi cuối tuần chỉ chiếm 28.5%. Tuy nhiên, nếu tính trung bình theo ngày (5 ngày thường vs 2 ngày cuối tuần), thì mật độ giao dịch gần như bằng nhau (khoảng 94.000 giao dịch/ngày). Điều này khẳng định đây là các nghiệp vụ xử lý tự động của hệ thống, được lên lịch chạy hàng ngày với một khối lượng công việc nhất quán, chứ không phải do hành vi khách hàng.
avg_year <- data_abs[,.(mean_amount = mean(
amount, na.rm = TRUE)), by = year][order(year)]
nudge_y_values <- ifelse(avg_year$year %in% c(2010, 2013, 2016, 2018, 2019),
0.18, -0.18)
nudge_x_values <- ifelse(avg_year$year == 2010, 0.2,
ifelse(avg_year$year == 2019, -0.2, 0))
ggplot(avg_year, aes(x = year, y = mean_amount)) +
geom_line(aes(color = "Giá trị Trung bình"), size = 1.2) +
geom_point(color = "#0d47a1", size = 3) +
geom_smooth(aes(color = "Xu hướng"), method = "lm", se = FALSE,
linetype = "dashed", size = 1) +
ggrepel::geom_label_repel( aes(
label = round(mean_amount, 1)), size = 3.2, color = "black",
fill = "white", nudge_y = nudge_y_values,
nudge_x = nudge_x_values, segment.linetype = "dashed",
segment.size = 0.3, segment.color = "gray50", box.padding = 0.1) +
scale_color_manual( name = "Chú thích:",
values = c("Giá trị Trung bình" = "#1e88e5", "Xu hướng" = "gray40")) +
scale_x_continuous(breaks = seq(min(avg_year$year), max(avg_year$year), by = 1),
labels = function(x) format(round(x), scientific = FALSE)) +
scale_y_continuous(expand = expansion(mult = c(0.15, 0.15))) +
labs(title = "Xu hướng giá trị trung bình giao dịch giảm giá trị theo năm",
x = "Năm", y = "Giá trị trung bình ($)") +
theme_minimal(base_size = 12) +
theme( plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5),legend.position = "bottom")
Giải thích
Nhận xét
Biểu đồ này cho thấy giá trị trung bình của một giao dịch giảm giá trị luôn ở mức cao và ổn định (dao động quanh 102-103 USD), cao gần gấp đôi so vớii giá trị trung bình của một giao dịch thành công (khoảng 51 USD). Điều này cho thấy đường xu hướngg gần như bằng phẳng, cho thấy mức giá trị trung bình này không hề thay đổi trong suốt 10 năm. Điều này cho thấy đây là các giao dịch hoàn tiền ngẫu nhiên của khách hàng, mà khẳng định đây là các nghiệp vụ điều chỉnh có quy chuẩn, mang tính thủ tục với một mức giá trị cố định, không thay đổi theo thời gian.
pt_hour_method <- data_abs[,.N, by =.(khung_gio, payment_type)]
pt_hour_method[, Total_N := sum(N), by =.(khung_gio)]
pt_hour_method[, Percentage := N / Total_N]
ggplot(pt_hour_method, aes(x = khung_gio, y = Percentage, fill = payment_type)) +
geom_col(position = "dodge", color = "gray30") +
geom_text(aes(label = scales::percent(Percentage, accuracy = 0.1)),
position = position_dodge(width = 0.9),
vjust = -0.5, size = 3) +
scale_fill_brewer(palette = "Pastel1") +
labs(title = "Cơ cấu giao dịch giảm giá trị theo khung giờ và phương thức",
x = "Khung giờ", y = "Tỷ lệ giao dịch (%)", fill = "Phương thức") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5))
Giải thích
Nhận xét
Biểu đồ này khẳng định rằng nghiệp vụ giảm giá trị gần như chỉ xảy ra ở kênh thanh toán vật lý. Kênh Online chiếm tỷ lệ rất nhỏ (0.7% - 4.2%) trong mọi khung giờ, do đó quy trình điều chỉnh hoặc hoàn tiền cho thương mại điện tử được xử lý riêng biệt. Trong số các kênh vật lý, Swipe luôn chiếm đa số (57-60%) trong các khung giờ, cho thấy công nghệ cũ này vẫn là nguồn phát sinh các nghiệp vụ điều chỉnh chính, ngay cả khi chip đã được áp dụng.
pt_state_day <- data_abs[,.(mean_amount = mean(amount, na.rm = TRUE)),
by =.(merchant_state, cuoi_tuan)]
top_states <- pt_state_day[,.(total = sum(
mean_amount)), by = merchant_state][order(-total)][1:10]$merchant_state
pt_state_day <- pt_state_day[merchant_state %in% top_states]
ggplot(pt_state_day, aes(x = reorder(merchant_state, mean_amount),
y = mean_amount, fill = factor(cuoi_tuan, labels = c(
"Ngày thường", "Cuối tuần")))) +
geom_col(position = position_dodge(width = 0.9), color = "white", alpha = 0.9) +
geom_text(aes(label = round(mean_amount, 1)), position = position_dodge(width = 0.8),
hjust = 1.1, vjust = 0.5, size = 2.8) +
coord_flip() +
scale_fill_brewer(palette = "Pastel1", name = "Loại ngày") +
labs(title = "Giá trị trung bình giao dịch giảm giá trị theo bang và loại ngày",
x = "Bang", y = "Giá trị trung bình ($)") +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
Giải thích
Nhận xét
Biểu đồ trên so sánh giá trị trung bình giao dịch giảm giá trị theo từng bang và loại ngày (ngày thường với cuối tuần). Đa số các quốc gia như Maldives, Egypt, Malaysia hay New Zealand có giá trị trung bình ở mức 300-350 USD, với sự chênh lệch nhỏ giữa hai loại ngày. Tuy nhiên, có hai ngoại lệ đáng chú ý đó là Barbados và Morocco cho thấy giá trị trung bình cao hơn vào cuối tuần (448 và 438 USD), trong khi Oman lại có giá trị trung bình cực cao vào ngày thường (402.7 USD). Điều này cho thấy các giao dịch giảm giá trị có giá trị lớn (trên 400 USD) dường như gắn liền với các ngành dịch vụ hoặc du lịch, nơi các khoản thanh toán lớn vào cuối tuần dễ phát sinh tranh chấp hoặc điều chỉnh nhất. Ngược lại, Oman có thể phản ánh một cơ cấu ngành hàng khác biệt.
pt_heat <- data_abs[,.N, by =.(weekday, khung_gio)]
ggplot(pt_heat, aes(x = khung_gio, y = weekday, fill = N)) +
geom_tile(color = "white") +
geom_text(aes(label = scales::comma(
N,big.mark = ".", decimal.mark = ",")), color = "black", size = 3) +
scale_fill_gradient(low = "#cfe2f3", high = "#084594", name = "Số lượng") +
labs(title = "Số lượng giao dịch giảm giá trị theo khung giờ và thứ trong tuần",
x = "Khung giờ", y = "Thứ") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
Giải thích
Nhận xét
Biểu đồ nhiệt này cho thấy sự phân bố số lượng giao dịch giảm giá trị theo cả hai chiều khung giờ trong ngày và thứ trong tuần với cách hoạt động cực kỳ nhất quán: các giao dịch giảm giá trị tập trung cao nhất vào giờ kinh doanh cao điểm, cụ thể là chiều (40.980 - 42.547) và sáng (33.109 - 34.041), đồng thời giảm mạnh vào tối (14.565 - 15.658) và đặc biệt thấp vào đêm (3.662 - 3.772). Điều này cho thấy các nghiệp vụ vận hành hoặc xử lý lô tự động, được thực hiện bởi nhân viên trong giờ làm việc, chứ không phải các sự kiện ngẫu nhiên do khách hàng.
Quan trọng hơn, mô thức này lặp lại y hệt ở tất cả 7 ngày trong tuần, kể cả thứ bảy và chủ nhật. Sự đồng đều tuyệt đối này bác bỏ giả thuyết về hoạt động do khách hàng khởi xướng như trả hàng, mà củng cố mạnh mẽ rằng đây là quy trình hệ thống được lên lịch chạy hàng ngày với một khối lượng công việc nhất quán, bất kể là ngày thường hay cuối tuần.
pt_city_year <- data_abs[,.N, by =.(merchant_city, year)][order(-N)][1:50]
pt_city_year[, year := as.integer(year)]
ggplot(pt_city_year, aes(x = factor(year), y = reorder(
merchant_city, N), size = N, color = year)) +
geom_point(alpha = 0.75) +
scale_size_continuous(range = c(3, 10), name = "Số lượng") +
scale_color_viridis_c(option = "plasma", name = "Năm", breaks = c(2010, 2013, 2016, 2019)) +
labs(title = "Tần suất giao dịch giảm giá trị của top 50 thành phố theo năm",
subtitle = "Mỗi điểm biểu thị tần suất giao dịch giảm giá trị tại một thành phố trong năm tương ứng",x = "Năm", y = "Thành phố") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
plot.subtitle = element_text(hjust = 0.5, size = 10, color = "gray40"),
axis.title = element_text(face = "bold"),panel.grid.minor = element_blank(),
legend.position = "right",legend.title = element_text(face = "bold"))
Giải thích
Nhận xét
Biểu đồ này cho thấy một xu hướng kinh tế rất tích cực: tần suất các giao dịch giảm giá trị đã giảm đi đáng kể tại hầu hết các thành phố hàng đầu trong suốt thập kỷ. Kích thước của các bong bóng có xu hướng thu hẹp lại khi di chuyển từ năm 2010 (màu tím) đến 2019 (màu vàng). Điều này thể hiện rõ nhất ở “HOUSTON” và “ONLINE”, những nơi có bong bóng lớn nhất vào năm 2010 nhưng đã co lại đáng kể vào năm 2019. Về mặt kinh tế, sự sụt giảm này là một dấu hiệu của sự trưởng thành và cải tiến quy trình. Nó cho thấy hệ thống thanh toán ngày càng hiệu quả hơn, có thể là do việc áp dụng công nghệ “Chip” an toàn hơn, dẫn đến ít giao dịch lỗi, ít tranh chấp và ít cần điều chỉnh hơn.
avg_by_hour_method <- data_abs[, .(avg_amount = mean(amount, na.rm = TRUE)),
by = .(khung_gio, payment_type)]
avg_by_hour_method[, khung_gio := factor(khung_gio,
levels = c("Đêm", "Sáng", "Chiều", "Tối"))]
ggplot(avg_by_hour_method, aes(x = payment_type, y = khung_gio, fill = avg_amount)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = scales::dollar(round(avg_amount, 0))),
color = "black", size = 4, fontface = "bold") +
scale_fill_gradient2(low = "#E63946", mid = "#FF9E6D", high = "#FFD166",
midpoint = median(avg_by_hour_method$avg_amount),name = "Giá trị trung bình" ) +
labs(
title = "Giá trị trung bình giao dịch giảm giá trị theo khung giờ và phương thức",
subtitle = "Đơn vị: USD | Nguồn: Transactiondataset",
x = "Phương thức thanh toán",y = "Khung giờ") +
theme_minimal(base_size = 12) + theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
plot.subtitle = element_text(hjust = 0.5, size = 10, color = "gray40"),
axis.text = element_text(size = 11),panel.grid = element_blank())
Giải thích
Nhận xét
Biểu đồ nhiệt cho thấy giá trị trung bình giao dịch giảm giá trị có sự khác biệt rõ rệt giữa các phương thức và khung giờ. Kênh Online luôn có giá trị cao nhất ( khoảng ($293–$302), cho thấy đây là một quy trình nghiệp vụ tự động, cố định, không liên quan đến hoạt động tại điểm bán. Trong khi đó, kênh Swipe có giá trị thấp hơn (khoảng $91–$175), phản ánh rủi ro vận hành hoặc tỷ lệ tranh chấp cao hơn từ công nghệ cũ. Khung giờ chiều và sáng là thời điểm tập trung của các giao dịch điều chỉnh, phù hợp với giờ làm việc.
global_mean_amount <- mean(data_abs$amount, na.rm = TRUE)
year_summary <- data_abs[, .(mean_amount = mean(amount, na.rm = TRUE)),
by = year][order(year)]
p1 <- ggplot(year_summary, aes(x = factor(year), y = mean_amount)) +
geom_line(group = 1, color = "steelblue", linewidth = 1.2) +
geom_point(color = "steelblue", size = 2) +
geom_hline(yintercept = global_mean_amount, linetype = "dashed",
color = "red", linewidth = 0.6) +
annotate("text", x = Inf, y = global_mean_amount,
label = "Trung bình", color = "red", hjust = 2, vjust = -0.4,
fontface = "italic", size = 2.8) +
labs(title = "Theo Năm", x = "Năm", y = "Giá trị trung bình") +
theme_minimal(base_size = 10) +theme(plot.title = element_text(hjust = 0.5))
month_summary <- data_abs[, .(mean_amount = mean(
amount, na.rm = TRUE)), by = month][order(month)]
p2 <- ggplot(month_summary, aes(x = factor(month), y = mean_amount)) +
geom_line(group = 1, color = "#FF9E6D", linewidth = 1.2) +
geom_point(color = "#FF9E6D", size = 2) +
geom_hline(yintercept = global_mean_amount, linetype = "dashed",
color = "red", linewidth = 0.6) +
annotate("text", x = Inf, y = global_mean_amount,
label = "Trung bình", color = "red", hjust = 1.05, vjust = -0.4,
fontface = "italic", size = 2.8) +
labs(title = "Theo Tháng", x = "Tháng", y = NULL) + theme_minimal(base_size = 10) +
theme(plot.title = element_text(hjust = 0.5))
weekday_summary <- data_abs[, .(mean_amount = mean(
amount, na.rm = TRUE)), by = weekday][
order(factor(weekday, levels = c(
"Thứ hai", "Thứ ba", "Thứ tư", "Thứ năm", "Thứ sáu", "Thứ bảy", "Chủ nhật")))]
p3 <- ggplot(weekday_summary, aes(x = weekday, y = mean_amount)) +
geom_line(group = 1, color = "#FFD166", linewidth = 1.2) +
geom_point(color = "#FFD166", size = 2) +
geom_hline(yintercept = global_mean_amount, linetype = "dashed",
color = "red", linewidth = 0.6) +
annotate("text", x = Inf, y = global_mean_amount,
label = "Trung bình", color = "red", hjust = 1.0, vjust = -0.4,
fontface = "italic", size = 2.8) +
labs(title = "Theo Thứ", x = "Thứ", y = NULL) +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_text(angle = 30, hjust = 1))
hour_summary <- data_abs[, .(mean_amount = mean(amount, na.rm = TRUE)), by = hour][order(hour)]
p4 <- ggplot(hour_summary, aes(x = factor(hour), y = mean_amount)) +
geom_line(group = 1, color = "#6D597A", linewidth = 1.2) +
geom_point(color = "#6D597A", size = 1.5) +
geom_hline(yintercept = global_mean_amount, linetype = "dashed",
color = "red", linewidth = 0.6) +
annotate("text", x = Inf, y = global_mean_amount,
label = "Trung bình", color = "red", hjust = 1.05, vjust = -0.4,
fontface = "italic", size = 2.8) +
labs(title = "Theo Giờ", x = "Giờ", y = NULL) +theme_minimal(base_size = 10) +
theme(plot.title = element_text(hjust = 0.5))
final_plot <- (p1 + p2) / (p3 + p4) +
plot_annotation(
title = "Giá trị trung bình giao dịch giảm giá trị theo các yếu tố thời gian",
theme = theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14)))
final_plot
Ý nghĩa
Nhận xét
Biểu đồ trên thể hiện giá trị trung bình giao dịch giảm giá trị theo 4 yếu tố thời gian: năm, tháng, thứ và giờ. Dữ liệu cho thấy giá trị này cực kỳ ổn định trong suốt 10 năm (2010-2019), dao động quanh mức 102-103 giao dịch, với đường xu hướng gần như bằng phẳng. Điều này bác bỏ giả thuyết đây là các giao dịch hoàn tiền ngẫu nhiên của khách hàng, mà khẳng định chúng là các nghiệp vụ điều chỉnh có quy chuẩn, mang tính thủ tục với một mức giá trị cố định. Điều này tương tự lặp lại ở cấp độ tháng và ngày trong tuần, cho thấy sự nhất quán về mặt quy trình. Riêng theo khung giờ, giá trị trung bình thấp vào ban ngày (khoảng 100-105 giao dịch) và tăng vọt vào buổi tối (lên đến 250 giao dịch), điều này nghĩa là các nghiệp vụ điều chỉnh các dịch giá trị cao thường được xử lý ngoài giờ hành chính, có thể là các lô xử lý tự động hoặc rủi ro đặc thù.
Bộ dữ liệu được sử dụng trong phần này là báo cáo tài chính hợp nhất của Công ty Cổ phần Camimex Group (mã chứng khoán: CMX), cụ thể là trong bảng cân đối kế toán của doanh nghiệp được thu thập theo quý trong giai đoạn từ quý 1 năm 2015 đến quý 4 năm 2024. Nguồn dữ liệu được trích xuất từ báo cáo tài chính công bố công khai của doanh nghiệp trên Sở Giao dịch Chứng khoán TP. Hồ Chí Minh (HOSE). Bộ dữ liệu bao gồm thông tin về cơ cấu tài sản và nguồn vốn của công ty, với trọng tâm là hai nhóm chính:
Bộ dữ liệu này là cơ sở cho việc phân tích biến động cơ cấu tài sản và nguồn vốn, cũng như đánh giá hiệu quả hoạt động tài chính của doanh nghiệp theo từng quý.
library(readxl)
library(dplyr)
library(ggplot2)
library(tibble)
library(moments)
library(tidyr)
library(ggrepel)
library(scales)
library(corrplot)
library(patchwork)
library(gridExtra)
library(reshape2)
library(ggdist)
library(knitr)
library(kableExtra)
Ở đoạn lệnh này, em đã sử dụng hàm library với cú pháp như sau library(package) với package là một gói được nạp vào Rstudio để thực hiện các mục đích liên quan đến tải dữ liệu, phân tích, thống kê, trực quan hóa… Ngoài ra, em còn tải các gói cần thiết phục vụ cho nội dung phân tích bên dưới. Cụ thể như sau:
data <- read_excel(file.choose())
Ở dòng lệnh này, em dùng lệnh read_excel để tải file dữ liệu vào R. Cú pháp như sau read_excel(path) với path là tham số chính dùng để chọn file cần tải vào R. Ngoài ra em còn sử dụng lệnh file.choose() để chọn được file CMX11.xlsx - file dữ liệu mà em mong muốn. Sau khi đã tải được file CMX11.xlsx vào R, em gán bộ dữ liệu này vào biến data để thuận thiện phân tích.
class(data)
## [1] "tbl_df" "tbl" "data.frame"
Ở dòng lệnh này, em dùng hàm class để kiểm tra bộ dữ liệu có kiểu dữ liệu là gì. Cú pháp như sau: class(x) với x là một dữ liệu bất kỳ.
Kết quả cho thấy bộ dữ liệu mà nhóm em đang nghiên cứu là dataframe.
dim(data)
## [1] 119 41
Ở dòng lệnh này, em dùng lệnh dim để xác định số biến và số quan sát. Cú pháp sau như sau dim(x), với x là matrix, array hoặc là data frame. Kết quả cho thấy bộ dữ liệu có 119 dòng và 41 cột.
colnames(data)
## [1] "Tiêu chí" "2015_Q1" "2015_Q2" "2015_Q3" "2015_Q4" "2016_Q1"
## [7] "2016_Q2" "2016_Q3" "2016_Q4" "2017_Q1" "2017_Q2" "2017_Q3"
## [13] "2017_Q4" "2018_Q1" "2018_Q2" "2018_Q3" "2018_Q4" "2019_Q1"
## [19] "2019_Q2" "2019_Q3" "2019_Q4" "2020_Q1" "2020_Q2" "2020_Q3"
## [25] "2020_Q4" "2021_Q1" "2021_Q2" "2021_Q3" "2021_Q4" "2022_Q1"
## [31] "2022_Q2" "2022_Q3" "2022_Q4" "2023_Q1" "2023_Q2" "2023_Q3"
## [37] "2023_Q4" "2024_Q1" "2024_Q2" "2024_Q3" "2024_Q4"
Ở dòng lệnh này, em dùng lệnh colnames để xác định các biến của từng cột. Cú pháp như sau colnames(x) với x là dataframe, matrix hoặc array. Kết quả cho thấy có 41 biến là “Tiêu chí” và các quý từ quý 1 năm 2015 đến quý 4 năm 2024 (gồm 40 biến)
Tuy nhiên từ đây mà khẳng định dựa trên số cột khẳng định số biến và số quan sát từ số cột thì chưa chính xác. Bởi vì dữ liệu của tụi em được trình bày theo Bảng cân đối kế toán, nên các hàng là giá trị của các biến như Tài sản, Tài sản ngắn hạn, Tài sản dài hạn, Nguồn vốn… còn ở các cột là các quý. Để khắc phục vấn đề này, em sẽ xử lý thông qua phần 2:Xử lý dữ liệu thô, mã hóa dữ liệu
kable(data[1:6, 1:6], booktabs = FALSE,linesep = "",align = "c",escape = FALSE) %>%
kable_styling(latex_options = "striped",font_size = 7)
| Tiêu chí | 2015_Q1 | 2015_Q2 | 2015_Q3 | 2015_Q4 | 2016_Q1 |
|---|---|---|---|---|---|
| TÀI SẢN | NA | NA | NA | NA | NA |
| A- TÀI SẢN NGẮN HẠN | 457530508816 | 455477073220 | 496604609235 | 505064327772 | 531545294814 |
| I. Tiền và các khoản tương đương tiền | 5402139458 | 7363850792 | 8178935850 | 4319012577 | 9260939793 |
|
5402139458 | 7363850792 | 8178935850 | 4319012577 | 9260939793 |
|
0 | NA | NA | NA | NA |
|
0 | NA | NA | NA | NA |
Ở dòng lệnh này, em sử dụng lệnh kable kết hợp với hàm kable_styling để kiểm tra cấu trúc của 6 dòng đầu và 6 cột đầu của bộ dữ liệu.
any(is.na(data))
## [1] TRUE
Ở câu lệnh này, em dùng hàm any kết hợp với hàm is.na để xác định số dữ liệu bị thiếu của từng cột. Cú pháp như sau:
Kết quả cho thấy bộ dữ liệu có ít nhất một giá trị bị thiếu.
colSums(is.na(data))
## Tiêu chí 2015_Q1 2015_Q2 2015_Q3 2015_Q4 2016_Q1 2016_Q2 2016_Q3
## 0 60 63 66 69 66 65 67
## 2016_Q4 2017_Q1 2017_Q2 2017_Q3 2017_Q4 2018_Q1 2018_Q2 2018_Q3
## 67 66 66 66 66 65 64 66
## 2018_Q4 2019_Q1 2019_Q2 2019_Q3 2019_Q4 2020_Q1 2020_Q2 2020_Q3
## 66 63 57 55 54 54 53 52
## 2020_Q4 2021_Q1 2021_Q2 2021_Q3 2021_Q4 2022_Q1 2022_Q2 2022_Q3
## 53 53 54 56 55 56 54 53
## 2022_Q4 2023_Q1 2023_Q2 2023_Q3 2023_Q4 2024_Q1 2024_Q2 2024_Q3
## 53 54 54 56 57 57 56 54
## 2024_Q4
## 55
Ở câu lệnh này, em dùng hàm colSums kết hợp với hàm is.na để xác định số dữ liệu bị thiếu của từng cột. Cú pháp như sau:
Kết quả cho thấy, ở các cột theo quý số lượng giá trị bị thiếu dao động từ 52 - 69. Điều này được lý giải vì có nhiều hàng bị bỏ trống (giá trị tại đó bằng 0), nhưng trong quá trình thu thập dữ liệu dẫn đến sai sót này. Ở vấn đề này em sẽ khắc phục ở phần 2:Xử lý dữ liệu thô, mã hóa dữ liệu.
head(unique(data$`Tiêu chí`), n =10)
## [1] "TÀI SẢN"
## [2] "A- TÀI SẢN NGẮN HẠN"
## [3] "I. Tiền và các khoản tương đương tiền"
## [4] "1. Tiền"
## [5] "2. Các khoản tương đương tiền"
## [6] "II. Các khoản đầu tư tài chính ngắn hạn"
## [7] "1. Chứng khoán kinh doanh"
## [8] "2. Dự phòng giảm giá chứng khoán kinh doanh"
## [9] "3. Đầu tư nắm giữ đến ngày đáo hạn"
## [10] "III. Các khoản phải thu ngắn hạn"
Ở dòng lệnh này, em dùng hàm head kết hợp hàm unique để xem thông tin 10 biến của cột Tiêu chí, chỉ xuất hiện những phần tử duy nhất, không hiển thị phần tử trùng lặp. Cú pháp như sau unique(x) với x là vecto, matrix, dataframe.
any(duplicated(data))
## [1] FALSE
Ở câu lệnh này, em dùng hàm any kết hợp với hàm duplicated để xác định bộ dữ liệu có dòng nào bị trùng lặp theo hàng hay không. Cú pháp như sau:
Kết quả cho thấy không có hàng nào bị trùng lắp hoàn toàn. Bởi vì dữ liệu của em được trình bày như Bảng cân đối kế toán, do đó trùng lặp theo hàng là không thể xảy ra.
Như nãy em đã đề cập, dữ liệu của tụi em được trình bày theo Bảng cân đối kế toán, nên các hàng là giá trị của các biến như Tài sản, Tài sản ngắn hạn, Tài sản dài hạn, Nguồn vốn… còn ở các cột là các quý. Do đó để thuận lợi phân tích, em sẽ chuyển hàng thành cột.
data1 <- as.matrix(data)
data1 <- t(data1)
data <- as.data.frame(data1)
colnames(data) <- data[1,]
data <- data[-1,]
data[] <- lapply(data, function(x) as.numeric(x))
Ở câu lệnh này em sử dụng hàm as.matrix ,t ,colnames , function, as.numeric và lapply để chuyển hàng thành cột, đồng thời dữ liệu về dạng số. Cú pháp như sau:
Bằng cách lợi dụng tính chất chuyển vị của ma trận, em đã chuyển đổi hàng thành cột. Sau đó chuyển matrix thành dataframe. Tuy nhiên sau khi chuyển đổi thì tên các cột lại để trống nên em đã chuyển dòng 1 của dữ liệu thành tên các cột, đồng thời xóa dòng 1 này để được định dạng dữ liệu như nhóm em mong muốn. Mặt khác khi tụi em cố đưa data về dạng matrix thì em đã vô tình đưa kiểu dữ liệu của bộ dữ liệu về dạng character. Vì vậy tụi em dùng hàm lappy kết hợp hàm as.numeric để đưa dữ liệu trong biến data về dạng số, thuận tiện để phân tích.
Vì có một số biến không được nhập hoặc do chính sách của công ty nên những biến đó có giá trị bằng 0. Do vậy xảy ra trong giai đoạn nghiên cứu, giá trị của các biến đó đều là . Để thuận tiện phân tích, nhóm em đã xác định lại các cột có dữ liệu đều là NA và loại bỏ các cột này đi.
d1 <- colSums(is.na(data))
head(d1)
## TÀI SẢN A- TÀI SẢN NGẮN HẠN
## 40 0
## I. Tiền và các khoản tương đương tiền 1. Tiền
## 0 0
## 2. Các khoản tương đương tiền II. Các khoản đầu tư tài chính ngắn hạn
## 35 23
Ở câu lệnh này, em dùng hàm colSums kết hợp với hàm is.na để xác định số dữ liệu bị thiếu của từng cột. Cú pháp như sau:
Kết quả cho thấy có các cột sau có số lượng giá trị bị thiếu nhiều như bằng 40 (tương ứng với toàn bộ giá trị theo quý của biến này đều bỏ trống) hoặc lớn hơn 20, ví dụ như TÀI SẢN,Các khoản tương đương tiền … Ngoài ra có một số biến không có giá trị bị thiếu, ví dụ như TIền và các khoản tương đương tiền, Tiền,…
Đối với các biến này, em quyết định loại bỏ chúng ra khỏi bộ dữ liệu.
data2 <- data[,d1 < 21]
Ở dòng lệnh này gán biến data2, em đã lọc, chỉ giữ lại các biến có số lượng giá trị bị thiếu nhỏ hơn 9, nghĩa các biến có các giá trị bị thiếu từ 9 đến 10 thì em sẽ bỏ đi.
Đối với các biến này, em quyết định sẽ lấy giá trị trung bình của các giá trị còn lại và thay vào vị trí các giá trị bị thiếu.
data3 <- data2
for (i in names(data3)) {
data3[[i]][is.na(data3[[i]])] <- mean(data3[[i]], na.rm = TRUE)}
kable(data3[1:6, 1:4], booktabs = FALSE,linesep = "",align = "c",escape = FALSE) %>%
kable_styling(latex_options = c("striped", "hold_position"),font_size = 8,position = "center")
| A- TÀI SẢN NGẮN HẠN | I. Tiền và các khoản tương đương tiền |
|
|
|
|---|---|---|---|---|
| 2015_Q1 | 457530508816 | 5402139458 | 5402139458 | 71695285341 |
| 2015_Q2 | 455477073220 | 7363850792 | 7363850792 | 111614058523 |
| 2015_Q3 | 496604609235 | 8178935850 | 8178935850 | 127034850435 |
| 2015_Q4 | 505064327772 | 4319012577 | 4319012577 | 158932625019 |
| 2016_Q1 | 531545294814 | 9260939793 | 9260939793 | 167309074680 |
| 2016_Q2 | 517672484471 | 6280471175 | 6280471175 | 106470036547 |
Ở dòng lệnh này em sử dụng hàm for kết hợp hàm names, is.na và hàm mean. Sau đó em dùng hàm kable để xuất 6 dòng và cột của dữ liệu thành dạng bảng. Cú pháp như sau:
Vòng lặp này có ý nghĩa rằng lấy từng cột của data3, kiểm tra xem trong từng cột này có giá trị nào bị thiếu hay không, nếu không có thì bỏ qua, nếu có thì sẽ lấy trung bình của cột đó (tính luôn vị trí các giá trị bị thiếu) sau đó thay vào vị trí các giá trị bị thiếu này.
Trước khi tiến hành các bước xử lý và phân tích dữ liệu, nhóm em cần thực hiện thao tác chuẩn hóa cấu trúc bảng dữ liệu. Trong tập dữ liệu ban đầu, các thời điểm quan sát (theo quý và năm) được lưu dưới dạng tên hàng) thay vì là một cột thông thường. Điều này gây khó khăn cho việc trích lọc, tổng hợp và trực quan hóa dữ liệu theo thời gian. Do đó, nhóm em tiến hành chuyển phần tên hàng thành một cột mới có tên là Thoi_gian, sau đó tách riêng phần năm và phần quý để thuận tiện cho việc thống kê, so sánh và biểu diễn xu hướng theo từng giai đoạn.
data3 <- data3 %>%
rownames_to_column(var = "Thoi_gian") %>%
mutate(Nam = sub("_Q\\d", "", Thoi_gian),Quy = sub(".*_Q", "", Thoi_gian))
Ở câu lệnh này, em sử dụng hàm rownames_to_column() từ gói tibble để chuyển phần tên hàng (rownames) của bảng dữ liệu data3 thành một cột dữ liệu mới có tên là Thoi_gian sau đó em dùng hàm mutate kết hợp hàm sub để tạo thêm hai cột mới Quy và Nam từ cột Thoi_gian. Cú pháp như sau:
Kết quả ở cột Nam là giá trị phần năm, ví dụ 2019_Q3 được chuyển thành 2019 và kết quả ở cột Quy thu được là phần quý, ví dụ 2019_Q3 được chuyển thành 3.
Sau khi đã xử lý các giá trị bị thiếu, em sẽ tiến hành mã hóa dữ liệu theo khả năng thanh toán hiện thời (CR), khả năng thanh toán tức thời (QR), tỷ lệ nợ (DR), tỷ lệ nợ trên vốn chủ sở hữu (D/E), tỷ lệ vốn chủ sở hữu (ER), tỷ lệ nợ dài hạn (LDR), vốn lưu động ròng (NWC), tỷ lệ tài sản cố định (FAR), tỷ lệ hàng tồn kho (IR) và tỷ suất sinh lời trên tài sản (ROA). Đây là các chỉ số quan trọng với doanh nghiệp nông 2015 đến 2024.
data3$QR <- (data3$`A- TÀI SẢN NGẮN HẠN`-data3$`IV. Hàng tồn kho`)/data3$`I. Nợ ngắn hạn`
data3$CR <- data3$`A- TÀI SẢN NGẮN HẠN`/data3$`I. Nợ ngắn hạn`
data3$DR <- data3$`C. NỢ PHẢI TRẢ`/data3$`TỔNG CỘNG TÀI SẢN`
data3$DE <- data3$`C. NỢ PHẢI TRẢ`/data3$`D.VỐN CHỦ SỞ HỮU`
data3$ER <- data3$`D.VỐN CHỦ SỞ HỮU`/data3$`TỔNG CỘNG TÀI SẢN`
data3$LDR <- data3$`II. Nợ dài hạn`/data3$`TỔNG CỘNG NGUỒN VỐN`
data3$NWC <- data3$`I. Nợ ngắn hạn`-data3$`II. Nợ dài hạn`
data3$FAR <- data3$`II.Tài sản cố định`/data3$`TỔNG CỘNG TÀI SẢN`
data3$IR <- data3$`IV. Hàng tồn kho`/data3$`TỔNG CỘNG TÀI SẢN`
data3$ROA <- data3$`11. Lợi nhuận sau thuế chưa phân phối`/data3$`TỔNG CỘNG TÀI SẢN`
Ở các dòng lệnh trên, em đã tạo các cột mới liên quan đến các biến mà em muốn phân tích ở phần sau. Sau đó thực hiện công thức tính toán như sau:
data3$Muc_do_CR <- ifelse (data3$CR >= 0.8,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ CR theo các mức mà tụi em mong muốn. Em đang phân tổ CR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_QR <- ifelse (data3$QR >= 0.5,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ QR theo các mức mà tụi em mong muốn. Em đang phân tổ QR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_DR <- ifelse (data3$DR <= 0.7,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ DR theo các mức mà tụi em mong muốn. Em đang phân tổ DR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_DE <- ifelse (data3$"DE" <= 2.5,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ D/E theo các mức mà tụi em mong muốn. Em đang phân tổ DR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_ER <- ifelse (data3$ER >= 0.3,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ ER theo các mức mà tụi em mong muốn. Em đang phân tổ ER thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_LDR <- ifelse (data3$LDR <= 0.1,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ LDR theo các mức mà tụi em mong muốn. Em đang phân tổ LDR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_NWC <- ifelse (data3$NWC >= 0,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ NWC theo các mức mà tụi em mong muốn. Em đang phân tổ NWC thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_FAR <- ifelse (data3$FAR <= 0.6,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ FAR theo các mức mà tụi em mong muốn. Em đang phân tổ FAR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_IR <- ifelse (data3$IR <= 0.45,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ IR theo các mức mà tụi em mong muốn. Em đang phân tổ IR thành 2 mức 1 và 0 tương ứng với:
data3$Muc_do_ROA <- ifelse (data3$ROA >= 0.04,1,0)
Ở dòng lệnh này em sử dụng hàm ifelse với cú pháp: ifelse(điều kiện, giá trị đúng, giá trị sai), với mục đích là phân tổ ROA theo các mức mà tụi em mong muốn. Em đang phân tổ IR thành 2 mức 1 và 0 tương ứng với:
data_CR0 <- data3 %>%filter(Muc_do_CR == 0) %>%select(Muc_do_CR, `Quy`)
data_CR1 <- data3 %>%filter(Muc_do_CR == 1) %>%select(Muc_do_CR, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số CR thành hai nhóm: nhóm 0 (CR < 0.8) và nhóm 1 (CR ≥ 0.8), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được
data_QR0 <- data3 %>%filter(Muc_do_QR == 0) %>%select(Muc_do_QR, `Quy`)
data_QR1 <- data3 %>%filter(Muc_do_QR == 1) %>%select(Muc_do_QR, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số QR thành hai nhóm: nhóm 0 (QR < 0.5) và nhóm 1 (QR ≥ 0.5), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_DR0 <- data3 %>%filter(Muc_do_DR == 0) %>%select(Muc_do_DR, `Quy`)
data_DR1 <- data3 %>%filter(Muc_do_DR == 1) %>%select(Muc_do_DR, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số DR thành hai nhóm: nhóm 0 (DR ≥ 2) và nhóm 1 (DR < 2), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_DE0 <- data3 %>%filter(Muc_do_DE == 0) %>%select(Muc_do_DE, `Quy`)
data_DE1 <- data3 %>%filter(Muc_do_DE == 1) %>%select(Muc_do_DE, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số DE thành hai nhóm: nhóm 0 (DE ≥ 2) và nhóm 1 (DE < 2), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_ER0 <- data3 %>%filter(Muc_do_ER == 0) %>%select(Muc_do_ER, `Quy`)
data_ER1 <- data3 %>%filter(Muc_do_ER == 1) %>%select(Muc_do_ER, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số ER thành hai nhóm: nhóm 0 (ER < 1) và nhóm 1 (ER ≥ 1), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_LDR0 <- data3 %>%filter(Muc_do_LDR == 0) %>%select(Muc_do_LDR, Quy)
data_LDR1 <- data3 %>%filter(Muc_do_LDR == 1) %>%select(Muc_do_LDR, Quy)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số LDR thành hai nhóm: nhóm 0 (LDR > 0.8) và nhóm 1 (LDR ≤ 0.8), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_NWC0 <- data3 %>%filter(Muc_do_NWC == 0) %>%select(Muc_do_NWC, `Quy`)
data_NWC1 <- data3 %>%filter(Muc_do_NWC == 1) %>%select(Muc_do_NWC, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số NWC thành hai nhóm: nhóm 0 (NWC < 0) và nhóm 1 (NWC ≥ 0), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_FAR0 <- data3 %>%filter(Muc_do_FAR == 0) %>%select(Muc_do_FAR, `Quy`)
data_FAR1 <- data3 %>%filter(Muc_do_FAR == 1) %>%select(Muc_do_FAR, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số FAR thành hai nhóm: nhóm 0 (FAR > 0.6) và nhóm 1 (FAR ≤ 0.6), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_IR0 <- data3 %>%filter(Muc_do_IR == 0) %>%select(Muc_do_IR, `Quy`)
data_IR1 <- data3 %>%filter(Muc_do_IR == 1) %>%select(Muc_do_IR, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số IR thành hai nhóm: nhóm 0 (IR < 1) và nhóm 1 (IR ≥ 1), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
data_ROA0 <- data3 %>%filter(Muc_do_ROA == 0) %>%select(Muc_do_ROA, `Quy`)
data_ROA1 <- data3 %>%filter(Muc_do_ROA == 1) %>%select(Muc_do_ROA, `Quy`)
Ở đoạn lệnh này, em phân tách dữ liệu theo mức độ chỉ số ROA thành hai nhóm: nhóm 0 (ROA < 0.05) và nhóm 1 (ROA ≥ 0.05), đồng thời chỉ giữ các cột cần thiết để quan sát theo quý. Cụ pháp như sau:
Kết quả thu được:
thongke_CR <- data3 %>%
summarise(
Trung_binh = mean(CR, na.rm = TRUE),
Do_lech_chuan = sd(CR, na.rm = TRUE),
Phuong_sai = var(CR, na.rm = TRUE),
Q1 = quantile(CR, 0.25, na.rm = TRUE),
Trung_vi = median(CR, na.rm = TRUE),
Q3 = quantile(CR, 0.75, na.rm = TRUE),
Do_lech = skewness(CR, na.rm = TRUE),
Do_nhon = kurtosis(CR, na.rm = TRUE))
thongke_CR
Ở các câu lệnh này, em sử dụng Câu lệnh 2 để tính các chỉ tiêu thống kê cơ bản cho biến CR. Cụ thể như sau:
Như vậy, sau khi chạy đoạn code trên, em thu được một bảng thongke_CR chỉ gồm một hàng chứa tất cả các chỉ tiêu thống kê cơ bản của CR. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_QR <- data3 %>%
summarise(
Trung_binh = mean(QR, na.rm = TRUE),
Do_lech_chuan = sd(QR, na.rm = TRUE),
Phuong_sai = var(QR, na.rm = TRUE),
Q1 = quantile(QR, 0.25, na.rm = TRUE),
Trung_vi = median(QR, na.rm = TRUE),
Q3 = quantile(QR, 0.75, na.rm = TRUE),
Do_lech = skewness(QR, na.rm = TRUE),
Do_nhon = kurtosis(QR, na.rm = TRUE))
thongke_QR
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_DR <- data3 %>%
summarise(
Trung_binh = mean(DR, na.rm = TRUE),
Do_lech_chuan = sd(DR, na.rm = TRUE),
Phuong_sai = var(DR, na.rm = TRUE),
Q1 = quantile(DR, 0.25, na.rm = TRUE),
Trung_vi = median(DR, na.rm = TRUE),
Q3 = quantile(DR, 0.75, na.rm = TRUE),
Do_lech = skewness(DR, na.rm = TRUE),
Do_nhon = kurtosis(DR, na.rm = TRUE)
)
thongke_DR
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_DE <- data3 %>%
summarise(
Trung_binh = mean(DE, na.rm = TRUE),
Do_lech_chuan = sd(DE, na.rm = TRUE),
Phuong_sai = var(DE, na.rm = TRUE),
Q1 = quantile(DE, 0.25, na.rm = TRUE),
Trung_vi = median(DE, na.rm = TRUE),
Q3 = quantile(DE, 0.75, na.rm = TRUE),
Do_lech = skewness(DE, na.rm = TRUE),
Do_nhon = kurtosis(DE, na.rm = TRUE)
)
thongke_DE
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_ER <- data3 %>%
summarise(
Trung_binh = mean(ER, na.rm = TRUE),
Do_lech_chuan = sd(ER, na.rm = TRUE),
Phuong_sai = var(ER, na.rm = TRUE),
Q1 = quantile(ER, 0.25, na.rm = TRUE),
Trung_vi = median(ER, na.rm = TRUE),
Q3 = quantile(ER, 0.75, na.rm = TRUE),
Do_lech = skewness(ER, na.rm = TRUE),
Do_nhon = kurtosis(ER, na.rm = TRUE))
thongke_ER
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_LDR <- data3 %>%
summarise(
Trung_binh = mean(LDR, na.rm = TRUE),
Do_lech_chuan = sd(LDR, na.rm = TRUE),
Phuong_sai = var(LDR, na.rm = TRUE),
Q1 = quantile(LDR, 0.25, na.rm = TRUE),
Trung_vi = median(LDR, na.rm = TRUE),
Q3 = quantile(LDR, 0.75, na.rm = TRUE),
Do_lech = skewness(LDR, na.rm = TRUE),
Do_nhon = kurtosis(LDR, na.rm = TRUE))
thongke_LDR
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_FAR <- data3 %>%
summarise(
Trung_binh = mean(FAR, na.rm = TRUE),
Do_lech_chuan = sd(FAR, na.rm = TRUE),
Phuong_sai = var(FAR, na.rm = TRUE),
Q1 = quantile(FAR, 0.25, na.rm = TRUE),
Trung_vi = median(FAR, na.rm = TRUE),
Q3 = quantile(FAR, 0.75, na.rm = TRUE),
Do_lech = skewness(FAR, na.rm = TRUE),
Do_nhon = kurtosis(FAR, na.rm = TRUE))
thongke_FAR
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_IR <- data3 %>%
summarise(
Trung_binh = mean(IR, na.rm = TRUE),
Do_lech_chuan = sd(IR, na.rm = TRUE),
Phuong_sai = var(IR, na.rm = TRUE),
Q1 = quantile(IR, 0.25, na.rm = TRUE),
Trung_vi = median(IR, na.rm = TRUE),
Q3 = quantile(IR, 0.75, na.rm = TRUE),
Do_lech = skewness(IR, na.rm = TRUE),
Do_nhon = kurtosis(IR, na.rm = TRUE))
thongke_IR
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét. Từ kết quả trên, em rút ra các nhận xét sau:
thongke_ROA <- data3 %>%
summarise(
Trung_binh = mean(ROA, na.rm = TRUE),
Do_lech_chuan = sd(ROA, na.rm = TRUE),
Phuong_sai = var(ROA, na.rm = TRUE),
Q1 = quantile(ROA, 0.25, na.rm = TRUE),
Trung_vi = median(ROA, na.rm = TRUE),
Q3 = quantile(ROA, 0.75, na.rm = TRUE),
Do_lech = skewness(ROA, na.rm = TRUE),
Do_nhon = kurtosis(ROA, na.rm = TRUE))
thongke_ROA
Tương tự như phần trên, em dùng lệnh summarise để tính các đại lượng thống kê đại diện cho bộ dữ liệu em đang xét.Từ kết quả trên, em rút ra các nhận xét sau:
roa_year <- data3 %>%
group_by(Nam) %>% summarise(ROA = mean(ROA, na.rm = TRUE)) %>%
mutate(Nam = as.numeric(Nam)) %>% drop_na(Nam, ROA) %>%
mutate(nudge_x = ifelse(Nam %in% c(2018, 2019), -0.55,
ifelse(Nam %in% c(2021, 2022, 2023), 0,
-0.3)),nudge_y = ifelse(Nam %in% c(2018, 2019), 0.006,
ifelse(Nam %in% c(2021, 2022, 2023), 0.015,0.001)))
mean_roa <- mean(data3$ROA, na.rm = TRUE)
p_roa <- ggplot(roa_year, aes(Nam, ROA)) +
geom_line(color = "#1f77b4", linewidth = 1) +geom_point(
size = 1.5, color = "#1f77b4") + geom_text(aes(label = round(ROA, 3),
nudge_x = nudge_x, nudge_y = nudge_y), vjust = 1.4,size = 3,
fontface = "bold",check_overlap = TRUE) +
geom_smooth(se = FALSE, color = "#2ca02c", linetype = "dashed", linewidth = 0.7) +
geom_hline(
yintercept = mean_roa, color = "red", linetype = "dashed", linewidth = 0.8) +
annotate("text",x = max(roa_year$Nam) - 0.5, y = mean_roa + 0.005,label = paste0(
"ROA trung bình = ", round(mean_roa, 3)),
color = "red", fontface = "bold", size = 2.5,hjust = 3, vjust = 0) +
geom_rug(alpha = 0.2) + labs(
title = "Tỷ lệ sinh lời trung bình", x = "Năm", y = "ROA") + scale_x_continuous(
breaks = unique(roa_year$Nam)) +theme_minimal() + theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
axis.ticks.y = element_blank(), panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank())
far_year <- data3 %>% group_by(Nam) %>% summarise(FAR = mean(FAR, na.rm = TRUE)) %>%
mutate(Nam = as.numeric(Nam)) %>% drop_na(Nam, FAR) %>%
mutate(nudge_x = ifelse(Nam %in% c(2016, 2017,2018, 2020), -0.41,
ifelse(Nam %in% c(2021, 2024), 0, 0)),nudge_y = ifelse(
Nam %in% c(2016, 2017,2018, 2020), 0,
ifelse(Nam %in% c(2021, 2024), 0.003,-0.001)))
mean_far <- mean(data3$FAR, na.rm = TRUE)
p_far <- ggplot(far_year, aes(Nam, FAR)) +
geom_line(color = "#1f77b4", linewidth = 1) +
geom_point(size = 1.5, color = "#1f77b4") +
geom_text(aes(label = round(FAR, 3),nudge_x = nudge_x,nudge_y = nudge_y),
vjust = 1,size = 3,fontface = "bold",check_overlap = TRUE) +
geom_smooth(se = FALSE, color = "#2ca02c", linetype = "dashed", linewidth = 0.7) +
geom_hline(
yintercept = mean_far, color = "red", linetype = "dashed", linewidth = 0.8) +
annotate("text", x = max(far_year$Nam) - 0.5, y = mean_far + 0.001,
label = paste0("FAR trung bình = ", round(mean_far, 3)),
color = "red", fontface = "bold", size = 2.5,hjust = 0.7, vjust = 0) +
geom_rug(alpha = 0.2) + labs(
title = "Tỷ lệ tài sản cố định trung bình", x = "Năm", y = "FAR") +
scale_x_continuous(breaks = unique(far_year$Nam)) +
theme_minimal() + theme(plot.title = element_text(
hjust = 0.5, face = "bold", size = 14),
axis.ticks.y = element_blank(), panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank())
p_combined <- p_roa + p_far + plot_layout(ncol = 2) + plot_annotation(
title = "Biểu đồ các chỉ số tài chính (ROA, FAR) trung bình theo năm",
theme = theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 14)))
p_combined
Giải thích
Nhận xét
Giai đoạn 2015–2024 cho thấy biến động mạnh mẽ trong hiệu quả hoạt động của doanh nghiệp CMX. Từ chỉ số ROA phản ánh sự thua lỗ nghiêm trọng (đỉnh điểm -0,127 năm 2017) trước khi chuyển sang giai đoạn phục hồi ấn tượng và đạt đỉnh 0,054 vào năm 2023. Tuy nhiên, sự sụt giảm mạnh của ROA xuống còn 0,01 trong năm 2024 đã làm gián đoạn đà cải thiện hiệu suất và là một tín hiệu cảnh báo về khả năng duy trì lợi nhuận trong tương lai của doanh nghiệp Camimex Group.
Về cơ cấu tài sản, CMX duy trì tỷ lệ tài sản cố định (FAR) linh hoạt (trung bình 15,2%). Việc FAR giảm trong giai đoạn 2022–2023, trùng với lúc ROA đạt đỉnh, cho thấy doanh nghiệp đã tối ưu hóa tài sản cố định để nâng cao hiệu suất. Tuy nhiên, sự gia tăng nhẹ của FAR năm 2024 mà không đi kèm với cải thiện ROA cho thấy nguy cơ về sự kém hiệu quả của các khoản đầu tư mới. Do vậy, doanh nghiệp cần rà soát nguyên nhân sụt giảm lợi nhuận và đánh giá lại các quyết định tài sản gần đây để đảm bảo chúng đóng góp tích cực vào khả năng sinh lời trong tương lai.
p1 <- ggplot(data3, aes(x = CR)) +geom_histogram(
aes(y = ..density..), fill = "lightblue", color = "black", bins = 7, alpha = 0.6) +
geom_density(color = "red", linewidth = 1.2) +
labs(title = "CR",x = "Giá trị CR", y = "Mật độ")+theme_minimal(base_size = 11)+
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11))
p2 <- ggplot(data3, aes(x = QR)) + geom_histogram(
aes(y = ..density..),fill = "lightblue",color = "black",bins = 7,alpha = 0.6) +
geom_density(color = "red", linewidth = 1.2) +
labs(title = "QR",x = "Giá trị QR",y = "Mật độ") +theme_minimal(base_size = 11)+
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11))
p3 <- ggplot(data3, aes(x = DR)) + geom_histogram(
aes(y = ..density..),fill = "lightblue",color = "black",bins = 7,alpha = 0.6) +
geom_density(color = "red", linewidth = 1.2) +
labs(title = "DR",x = "Giá trị DR",y = "Mật độ") +theme_minimal(base_size = 11)+
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11))
p4 <- ggplot(data3, aes(x = DE)) + geom_histogram(
aes(y = ..density..),fill = "lightblue",color = "black",bins = 7,alpha = 0.6) +
geom_density(color = "red", linewidth = 1.2) +
labs(title = "DE",x = "Giá trị DE",y = "Mật độ") +theme_minimal(base_size = 11)+
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11))
p_his <- (p1 + p2) / (p3 + p4) +
plot_annotation(
title = "Biểu đồ Histogram và đường mật độ của CR, QR, DR và DE theo thời gian",
theme = theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 14)))
p_his
Giải thích
Nhận xét
Từ biểu đồ mật độ của các chỉ số tài chính cho thấy cấu trúc vốn của CMX linh hoạt nhưng không ổn định. CR tập trung quanh mức 1,0 điều này thể hiện thanh khoản hiện hành ở mức vừa phải. Ngược lại, QR dồn về 0,3, báo hiệu rủi ro thanh khoản nhanh cao do khả năng thanh toán ngắn hạn bị hạn chế khi loại bỏ hàng tồn kho. Mặt khác, DR có sự phân bố hai đỉnh rõ rệt tại 0,6 và 0,9, phản ánh sự biến động lớn về mức độ đòn bẩy và tiềm ẩn rủi ro tài chính cao trong các giai đoạn phụ thuộc nhiều vào nợ. Mặc dù DE thường giữ dưới 10 nhưng doanh nghiệp cần kiểm soát chặt chẽ tỷ lệ nợ cao và cải thiện QR để tăng cường tính an toàn và ổn định tài chính trong dài hạn.
plot_violin_box <- function(var_name) {
df <- data3[, var_name, drop = FALSE]
colnames(df) <- "value"
ggplot(df, aes(x = "", y = value)) +
geom_violin(
fill = "lightblue", alpha = 0.7, color = "lightblue", linewidth = 0.02) +
geom_boxplot(width = 0.3, fill = "#ECEFF1", color = "black",
outlier.color = "red", outlier.size = 1.5) +
labs(title = var_name, x = NULL, y = "Giá trị") + theme_minimal(base_size = 10) +
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11),
axis.text = element_text(size = 9), axis.title.y = element_text(size = 10),
axis.ticks.x = element_blank(), panel.grid.major.x = element_blank())}
p_CR <- plot_violin_box("CR")
p_QR <- plot_violin_box("QR")
p_DR <- plot_violin_box("DR")
p_DE <- plot_violin_box("DE")
final_plot <- (p_CR + p_QR) / (p_DR + p_DE) +
plot_annotation(
title = "Phân phối các chỉ tiêu tài chính: Violin plot kết hợp Box plot",
theme = theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 13)))
final_plot
Giải thích
Nhận xét
Từ biểu đồ Violin kết hợp Box plot cho thấy CR và QR của CMX dao động ở mức trung bình (với trung vị lần lượt khoảng 1,0 và 0,4), phản ánh khả năng thanh toán ngắn hạn ổn định nhưng chưa thực sự dồi dào, đặc biệt khi loại bỏ hàng tồn kho. Ngược lại, DR và DE cho thấy mức độ đòn bẩy tài chính cao, với trung vị DR khoảng 0,7 và sự xuất hiện của hai điểm ngoại lai ở DE (lên tới 40), cho thấy những giai đoạn doanh nghiệp phụ thuộc mạnh vào nợ vay. Điều này tiềm ẩn rủi ro tài chính nếu dòng tiền bị gián đoạn hoặc chi phí vốn gia tăng, đòi hỏidoanh nghiệp cần tối ưu cơ cấu nguồn vốn và tăng cường quản trị thanh khoản trong dài hạn.
Như ở phần mã hóa dữ liệu em đã trình bày, em đã phân loại các chỉ tiêu tài chính them một ngưỡng. Với mức 1 là mức Đạt, và mức 0 là mức không đạt.
data3$Quy <- as.numeric(data3$Quy)
muc_do_cols <- c("Muc_do_CR", "Muc_do_QR", "Muc_do_DR", "Muc_do_DE")
plot_pie_for_col <- function(col_name) {
df <- data3 %>%filter(!is.na(!!sym(col_name)), !!sym(col_name) == 1) %>%
count(Quy, .drop = FALSE) %>%mutate(
Quy = factor(Quy, levels = 1:4, labels = c("Quý 1", "Quý 2", "Quý 3", "Quý 4")),
pct = n / sum(n) * 100,label = sprintf("%.1f%%", pct))
all_quy <- data.frame(
Quy = factor(c("Quý 1", "Quý 2", "Quý 3", "Quý 4"),
levels = c("Quý 1", "Quý 2", "Quý 3", "Quý 4")))
df <- all_quy %>% left_join(df, by = "Quy") %>%
mutate(n = replace_na(n, 0),pct = replace_na(pct, 0),
label = ifelse(n == 0, "0.0%", sprintf("%.1f%%", pct)))
ggplot(df, aes(x = "", y = n, fill = Quy)) +geom_col(width = 1, color = "white") +
geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 3) +
coord_polar("y", start = 0) +scale_fill_manual(values = c("Quý 1" = "#FF9999",
"Quý 2" = "#66B2FF","Quý 3" = "#99FF99", "Quý 4" = "#FFCC99"),name = "Quý") +
labs(title = gsub("Muc_do_", "", col_name)) +theme_void() +theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 10),
legend.position = "none")}
p_CR <- plot_pie_for_col("Muc_do_CR")
p_QR <- plot_pie_for_col("Muc_do_QR")
p_DR <- plot_pie_for_col("Muc_do_DR")
p_DE <- plot_pie_for_col("Muc_do_DE")
final_plot <- (p_CR + p_QR) / (p_DR + p_DE) + plot_layout(guides = "collect") &
theme(legend.position = "right", legend.text = element_text(
size = 8), legend.title = element_text(size = 9))
final_plot + plot_annotation(title = "Tỷ lệ an toàn tài chính theo quý",
theme = theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 12)))
Giải thích
Nhận xét
Biểu đồ tỷ lệ an toàn tài chính theo quý cho thấy Camimex Group duy trì sự ổn định tương đối trong cơ cấu thanh khoản và đòn bẩy. Trong đó, CR và QR có sự biến động nhẹ giữa các quý, phản ánh khả năng thanh toán ngắn hạn được kiểm soát linh hoạt nhưng chưa đồng đều. Trong khi đó, DR và DE phân bổ đều ở mức 25% mỗi quý, cho thấy doanh nghiệp áp dụng chiến lược tài chính cân bằng, không tập trung rủi ro vào một quý cụ thể. Tuy nhiên, việc thiếu sự khác biệt rõ rệt giữa các quý cho thấy khả năng quản trị tài chính còn mang tính chu kỳ hoặc thiếu điều chỉnh chủ động theo biến động thị trường. Chính vì vậy, doanh nghiệp cần tăng cường phân tích theo mùa vụ để tối ưu hóa dòng tiền và giảm thiểu rủi ro tài chính.
chi_tieu_list <- c("IR", "ER", "ROA", "FAR")
data_corr <- data3[, chi_tieu_list]
M <- cor(data_corr, use = "complete.obs")
M_long <- melt(M)
ggplot(M_long, aes(x = Var1, y = Var2, fill = value)) +geom_tile() +
scale_fill_gradient2( low = "#E74C3C",high = "#1B9E77",mid = "white",midpoint = 0,
limits = c(-1, 1),space = "Lab",name = "Hệ số\ntương quan") +
geom_text(aes(label = round(value, 2)), size = 4, color = "black") +
theme_minimal() + theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 11),
axis.text.y = element_text(size = 11),
plot.title = element_text(hjust = 0.5, face = "bold", size = 13),
panel.grid = element_blank(),axis.ticks = element_blank(),
legend.title = element_text(size = 11),legend.text = element_text(size = 10)) +
labs(x = NULL,y = NULL,title = "Ma trận tương quan giữa các chỉ tiêu tài chính") +
coord_fixed()
Giải thích
Nhận xét
Từ ma trận tương quan trên, cho thấy ER có mối tương quan thuật rất mạnh với ROA là 0,81 điều này cho thấy hiệu quả sinh lời của CMX tăng khi doanh nghiệp dựa nhiều hơn vào vốn chủ sở hữu thay vì nợ. Từ đó, ta có thể thấy cấu trúc tài chính của doanh nghiệp này rất tốt. Ngược lại, IRvà ROA tương quan âm với nhau với hệ số tương quan là –0,53, điều này ngụ ý rằng các giai đoạn đầu tư mở rộng thường đi kèm suy giảm lợi nhuận ngắn hạn. Ngoài ra, FAR cũng có tương quan âm với cả ROA và ER, cho thấy việc gia tăng tài sản cố định có thể làm giảm hiệu suất sử dụng vốn và gia tăng đòn bẩy. Từ kết quả này, ta có thể nhấn mạnh nhu cầu cân đối giữa đầu tư, cơ cấu vốn và hiệu quả sinh lời trong chiến lược tài chính dài hạn của doanh nghiệp Camimex Group.
muc_do_vars <- c("Muc_do_IR", "Muc_do_ER", "Muc_do_ROA", "Muc_do_FAR")
data_long <- data3 %>%select(all_of(muc_do_vars)) %>%pivot_longer(
cols = everything(),names_to = "Chi_tieu",values_to = "Muc_do") %>%
group_by(Chi_tieu, Muc_do) %>%summarise(So_luong = n(), .groups = "drop") %>%
mutate(Chi_tieu = recode(Chi_tieu,"Muc_do_ROA" = "ROA","Muc_do_IR" = "IR",
"Muc_do_FAR" = "FAR","Muc_do_ER" = "ER")) %>%
complete(Chi_tieu, Muc_do = c(0, 1), fill = list(So_luong = 0))
ggplot(data_long, aes(x = Chi_tieu, y = So_luong, fill = factor(Muc_do))) +
geom_col(width = 0.9, position = position_dodge(width = 0.9),
alpha = 0.9, color = "black") + coord_flip() +
geom_text(aes(label = ifelse(So_luong > 0, So_luong, "")),
position = position_dodge(width = 0.9),
hjust = -0.2, size = 3, color = "black") +
scale_fill_manual(values = c("0" = "#E74C3C", "1" = "#2ECC71"),
name = "Mức độ",labels = c("Không đạt", "Đạt")) +
labs(title = "Số lần đạt và không đạt ngưỡng của các chỉ tiêu tài chính",
x = "Chỉ tiêu",y = "Số lượng quan sát") +
theme_minimal() +theme(legend.position = "right",
plot.title = element_text(hjust = 0.5, face = "bold", size = 13),
axis.text = element_text(size = 10),axis.title = element_text(size = 10))
Giải thích
Nhận xét
Từ biểu đồ cột đôi trên, ta thấy số lần đạt và không đạt ngưỡng của FAR dẫn đầu với 40 lần đạt, điều này cho thấy doanh nghiệp duy trì ổn định tỷ lệ tài sản cố định. Trong khi đó, ROA và IR có số lần không đạt cao hơn đạt (23 và 17; 19 và 21), phản ánh hiệu quả sinh lời và đầu tư chưa đồng đều qua thời gian. Mặt khác, ER cân bằng giữa số lần đạt và không đạt (20/20), cho thấy cơ cấu vốn chủ sở hữu được kiểm soát ổn định. Điều này cho thấy Camimex Group có nền tảng tài sản vững chắc. Do đó, doanh nghiệp này cần cải thiện hiệu suất hoạt động và chiến lược đầu tư để nâng cao khả năng sinh lời trong tương lai.
indicators <- c("IR", "ER", "ROA", "FAR")
dat_long <- data3 %>%select(all_of(indicators), Quy) %>%pivot_longer(
cols = all_of(indicators),names_to = "Indicator",values_to = "Value") %>%
mutate(Quy = factor(
Quy, levels = c(1, 2, 3, 4), labels = c("Quý 1", "Quý 2", "Quý 3", "Quý 4")))
plot_density_for_indicator <- function(ind) {
df_sub <- dat_long %>% filter(Indicator == ind)
ggplot(df_sub, aes(x = Value, color = Quy, fill = Quy)) +geom_density(
alpha = 0.6, linewidth = 0.5) +scale_color_manual(
values = c("Quý 1" = "#FF9999", "Quý 2" = "#66B2FF",
"Quý 3" = "#99FF99", "Quý 4" = "#FFCC99"),name = "Quý") +
scale_fill_manual(values = c("Quý 1" = "#FF9999", "Quý 2" = "#66B2FF",
"Quý 3" = "#99FF99", "Quý 4" = "#FFCC99"),name = "Quý") +
labs(title = ind, x = "Giá trị", y = "Mật độ") +theme_minimal(base_size = 10) +
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 11),
legend.position = "none", axis.text = element_text(size = 9),
axis.title = element_text(size = 10))}
p_IR <- plot_density_for_indicator("IR")
p_ER <- plot_density_for_indicator("ER")
p_ROA <- plot_density_for_indicator("ROA")
p_FAR <- plot_density_for_indicator("FAR")
final_plot <- (p_IR + p_ER) / (p_ROA + p_FAR) + plot_layout(guides = "collect") +
plot_annotation(title = "Phân bố mật độ các chỉ tiêu tài chính theo quý") &
theme(legend.position = "right",legend.title = element_text(size = 9),
legend.text = element_text(size = 8),
plot.title = element_text(hjust = 0.5, face = "bold", size = 13))
final_plot
Giải thích
Nhận xét
Từ đồ thị phân bố mật độ theo quý, ta có thể thấy IR và ROA có sự biến động rõ rệt giữa các quý, đặc biệt tập trung cao ở quý 3, điều này phản ánh khả năng sinh lời và hiệu quả đầu tư của CMX đạt đỉnh vào giai đoạn này. Ngược lại, ER duy trì phân bố ổn định qua các quý, cho thấy chiến lược tài chính thận trọng với cơ cấu vốn chủ sở hữu được kiểm soát bền vững.Mặt khác, FAR thường dao động trong khoảng từ 0.1 đến 0.2, với đỉnh mật độ nổi bật ở quý 1, điều này ngụ ý rằng doanh nghiệp thường triển khai đầu tư tài sản cố định vào đầu năm. Sự không đồng đều trong phân bố theo quý giữa các chỉ số cho thấy CMX cần chủ động hơn trong việc điều chỉnh kế hoạch tài chính theo chu kỳ mùa vụ, đồng thời tăng cường giám sát hiệu suất ở các quý có hoạt động đầu tư cao để đảm bảo cân bằng giữa tăng trưởng và ổn định tài chính dài hạn.