CHƯƠNG 1: PHÂN TÍCH BỘ DỮ LIỆU SPOTIFY

1.1 Giới thiệu và chuẩn bị dữ liệu

1.1.1. Tổng quan về bộ dữ liệu và mục tiêu phân tích

1.1.1.1. Giới thiệu chung và mục tiêu phân tích

Trong bối cảnh công nghệ âm nhạc phát triển mạnh mẽ, các nền tảng phát trực tuyến (streaming) như Spotify đã trở thành nguồn dữ liệu phong phú phản ánh thói quen nghe nhạc của hàng trăm triệu người dùng trên toàn cầu. Thông qua các đặc tính âm thanh (audio features) như danceability, energy, tempo, loudness, valence và mức độ phổ biến (popularity), dữ liệu Spotify cho phép chúng ta khám phá xu hướng âm nhạc, đặc điểm của các bài hát được ưa chuộng, cũng như sự thay đổi phong cách qua từng giai đoạn lịch sử.

Bộ dữ liệu Spotify Dataset 1921–2020 (160k+ Tracks) cung cấp thông tin chi tiết về hơn 160.000 bài hát phát hành trong gần một thế kỷ, được thu thập và tổng hợp từ nền tảng Spotify. Dữ liệu bao gồm các đặc điểm kỹ thuật của bài hát, thông tin nghệ sĩ, thời gian phát hành và các chỉ số âm thanh được tính toán bằng thuật toán phân tích của Spotify API.

Mục tiêu của phần phân tích này là: - Khám phá xu hướng thay đổi của âm nhạc từ năm 1921 đến năm 2020 thông qua các chỉ số như tempo, energy, danceability, valencepopularity. - Phân tích mối quan hệ giữa các đặc trưng âm thanh với độ phổ biến của bài hát (popularity), nhằm nhận diện những yếu tố ảnh hưởng đến sự thành công trong âm nhạc đại chúng. - Thực hiện thống kê mô tả và trực quan hóa dữ liệu để hiểu rõ hơn về cấu trúc, đặc điểm và xu hướng tổng thể trong tập dữ liệu.


1.1.1.2. Cấu trúc và đặc điểm của bộ dữ liệu

  • Tên bộ dữ liệu: Spotify Dataset 1921–2020, 160k+ Tracks
  • Nguồn dữ liệu: Kaggle (người đóng góp: Yama Erenay)
  • Phạm vi thời gian: 1921 – 2020
  • Quy mô: Gồm 170.654 quan sát (mỗi quan sát tương ứng với một bài hát)
  • Số lượng biến: 19 biến, bao gồm cả định tính và định lượng
  • Loại dữ liệu: Hỗn hợp – bao gồm cả dữ liệu định tính (tên nghệ sĩ, thể loại, chế độ trưởng/thứ) và định lượng (tempo, loudness, duration, popularity, energy, danceability, v.v.)

1.1.1.3. Mô tả chi tiết các biến trong bộ dữ liệu

Bộ dữ liệu Spotify 1921–2020 bao gồm 19 biến chính, được chia thành hai nhóm:
(1) Các biến định tính mô tả thông tin nghệ sĩ, bài hát, chế độ nhạc;
(2) Các biến định lượng phản ánh các đặc trưng âm thanh và độ phổ biến của từng bài hát.

Tên biến Kiểu dữ liệu Mô tả chi tiết
valence Numeric (0–1) Mức độ “tích cực” hoặc “vui tươi” của bài hát. Giá trị càng cao thể hiện bài hát mang cảm xúc tích cực, ngược lại càng thấp biểu thị nhạc buồn, u tối.
year Integer Năm phát hành bài hát, giúp phân tích xu hướng âm nhạc theo thời gian.
acousticness Numeric (0–1) Mức độ “mộc” của bài hát, phản ánh tỷ lệ nhạc cụ thật so với nhạc điện tử. Giá trị cao thể hiện bài hát mang tính acoustic cao.
artists Character Tên nghệ sĩ hoặc nhóm nhạc thể hiện bài hát. Một bài hát có thể có nhiều nghệ sĩ hợp tác.
danceability Numeric (0–1) Mức độ dễ nhảy, thể hiện khả năng người nghe có thể cảm nhận và di chuyển theo nhịp. Giá trị cao → dễ nhảy, nhịp rõ ràng.
duration_ms Numeric Thời lượng của bài hát tính bằng mili-giây (ms). Có thể chuyển đổi sang phút để dễ quan sát.
energy Numeric (0–1) Mức năng lượng cảm nhận trong bài hát, thể hiện sự mạnh mẽ, cường độ âm thanh, tốc độ và hoạt động.
explicit Numeric (0 hoặc 1) Biến nhị phân thể hiện nội dung của bài hát có mang yếu tố “nhạy cảm” hay không. Cụ thể, giá trị 1 biểu thị bài hát có chứa ngôn từ hoặc nội dung không phù hợp với mọi đối tượng (explicit content), còn giá trị 0 nghĩa là bài hát có nội dung bình thường. Biến này giúp phân loại và phân tích tỷ lệ các bài hát “explicit” theo từng năm hoặc thể loại.
id Character Mã định danh duy nhất của từng bài hát trên nền tảng Spotify.
instrumentalness Numeric (0–1) Xác suất bài hát không có lời. Giá trị gần 1 thể hiện bài hát hoàn toàn là nhạc cụ.
key Factor / Integer (0–11) Cao độ chính của bài hát, mã hóa theo 12 nốt nhạc trong thang âm phương Tây (C, C#, D, E♭, …, B).
liveness Numeric (0–1) Mức độ “trực tiếp” của bài hát – giá trị cao thể hiện bài hát có khả năng được thu âm trực tiếp (live).
loudness Numeric (dB) Độ lớn trung bình của bài hát, đo bằng đơn vị decibel (dB). Giá trị âm (vì dB đo so với mức 0 chuẩn).
mode Factor (0 hoặc 1) Chế độ của thang âm: 1 = trưởng (Major), 0 = thứ (Minor). Thường liên quan đến cảm xúc vui/buồn của bài hát.
name Character Tên của bài hát.
popularity Numeric (0–100) Mức độ phổ biến của bài hát trên Spotify. Giá trị càng cao thể hiện số lượt nghe và yêu thích càng lớn.
release_date Date Năm phát hành của bài hát.
speechiness Numeric (0–1) Mức độ “lời nói” trong bài hát. Giá trị cao thể hiện bài hát chứa nhiều đoạn nói, rap hoặc ít giai điệu.
tempo Numeric (BPM) Tốc độ nhịp của bài hát, đo bằng beats per minute (BPM). Giá trị cao → nhạc nhanh, sôi động; thấp → nhạc chậm, nhẹ.

Nhìn chung, các biến định lượng trong bộ dữ liệu có giá trị trong khoảng từ 0 đến 1 (chuẩn hóa theo hệ thống phân tích của Spotify), ngoại trừ các biến đặc biệt như tempo (đơn vị BPM), duration_ms (thời gian), và loudness (đơn vị dB).
Các biến định tính như artists, name, mode, explicit, key giúp nhận diện và phân loại bài hát, hỗ trợ cho các phân tích thống kê và trực quan hóa ở các phần tiếp theo.

1.1.2. Kiểm tra kích thước và chất lượng dữ liệu ban đầu

1.1.2.1. Đọc dữ liệu và kiểm tra kích thước

1. library(data.table)
2. data <- fread("C:/Users/Administrator/Downloads/data.csv", encoding = "UTF-8")
3. library(dplyr)
4. library(readr)
5. library(skimr)
6. library(knitr)
7. num_obs_data <- nrow(data)   
8. num_vars_data <- ncol(data)  
9. cat("Số lượng quan sát (bài hát):", format(num_obs_data, big.mark = ","), "\n")
## Số lượng quan sát (bài hát): 170,653
1. cat("Số lượng biến:", num_vars_data, "\n")
## Số lượng biến: 19

Giải thích: 1. Nạp thư viện data.table để xử lý dữ liệu nhanh và hiệu quả. 2. Đọc file CSV vào biến data với encoding UTF-8. 3. Nạp thư viện dplyr để thao tác và xử lý dữ liệu. 4. Nạp thư viện readr để đọc/ghi dữ liệu nhanh và chuẩn. 5. Nạp thư viện skimr để tạo bản tóm tắt thống kê dữ liệu chi tiết. 6. Nạp thư viện knitr để xuất bảng và báo cáo đẹp trong R Markdown. 7. Lấy số lượng quan sát (hàng) trong dataset và lưu vào biến num_obs_data. 8. Lấy số lượng biến (cột) trong dataset và lưu vào biến num_vars_data. 9. In ra số lượng quan sát với định dạng dễ đọc.

  1. In ra số lượng biến (cột) trong dataset.

Dữ liệu gốc bao gồm 19 biến với 170.653 quan sát.

1.1.2.2. Kiểm tra kiểu dữ liệu và các dòng đầu

1. library(dplyr)
2. library(knitr)
3. library(kableExtra)
4. cols_to_show <- c("artists", "name", "year", "popularity",
5.                   "danceability", "energy", "tempo", "valence")
6. cols_to_show <- cols_to_show[cols_to_show %in% colnames(data)]
7. head(data %>% select(all_of(cols_to_show))) %>%
8.   kable(
9.     format = "html",
10.     align = "l"
11.   ) %>%
12.   kable_styling(
13.     bootstrap_options = c("striped", "hover", "condensed", "responsive"),
14.     full_width = FALSE
15.   )
artists name year popularity danceability energy tempo valence
[‘Sergei Rachmaninoff’, ‘James Levine’, ‘Berliner Philharmoniker’] Piano Concerto No. 3 in D Minor, Op. 30: III. Finale. Alla breve 1921 4 0.279 0.211 80.954 0.0594
[‘Dennis Day’] Clancy Lowered the Boom 1921 5 0.819 0.341 60.936 0.9630
[‘KHP Kridhamardawa Karaton Ngayogyakarta Hadiningrat’] Gati Bali 1921 5 0.328 0.166 110.339 0.0394
[‘Frank Parker’] Danny Boy 1921 3 0.275 0.309 100.109 0.1650
[‘Phil Regan’] When Irish Eyes Are Smiling 1921 2 0.418 0.193 101.665 0.2530
[‘KHP Kridhamardawa Karaton Ngayogyakarta Hadiningrat’] Gati Mardika 1921 6 0.697 0.346 119.824 0.1960

Giải thích: 1. Nạp thư viện dplyr để thao tác và xử lý dữ liệu. 2. Nạp thư viện knitr để xuất bảng và báo cáo đẹp trong R Markdown. 3. Nạp thư viện kableExtra để tùy chỉnh và làm đẹp các bảng HTML/LaTeX. 4. Tạo vector cols_to_show gồm các cột tiêu biểu muốn hiển thị. 5. (Tiếp tục) xác định các cột: "danceability", "energy", "tempo", "valence". 6. Lọc các cột trong cols_to_show chỉ giữ những cột thực sự có trong dataset. 7. Lấy 6 dòng đầu tiên của dataset với các cột đã chọn. 8. Chuyển dữ liệu thành bảng HTML bằng kable(). 9. Chọn định dạng bảng là html. 10. Căn lề các cột sang trái. 11. (Sử dụng pipe) truyền bảng vào hàm tiếp theo. 12. Tùy chỉnh bảng với kable_styling(). 13. Thêm các tùy chọn bootstrap: striped, hover, condensed, responsive. 14. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE). 15. Kết thúc hàm kable_styling().

Dựa trên kết quả kiểm tra cấu trúc dữ liệu bằng hàm str(), hầu hết các biến trong bộ dữ liệu Spotify đều có kiểu dữ liệu phù hợp.
Tuy nhiên, có ba biến cần được chuyển đổi để thuận tiện cho việc xử lý và phân tích thống kê:

  • explicit: hiện ở dạng số nguyên (0/1) → nên chuyển thành factor với nhãn “Không nhạy cảm” và “Nhạy cảm”.
  • mode: hiện ở dạng số nguyên (0/1) → nên chuyển thành factor với nhãn “Minor” và “Major”.
    Trong đó: Minor biểu thị chế độ thứ (âm hưởng buồn, sâu lắng), còn Major biểu thị chế độ trưởng (âm hưởng vui tươi, sáng sủa).
  • release_date: có giá trị không đồng nhất (có dòng chỉ chứa năm, có dòng đầy đủ ngày/tháng/năm), nên cần tách phần năm (4 ký tự đầu tiên) và chuyển thành số nguyên (integer).

Việc chuyển đổi này giúp đảm bảo tính chính xác và rõ ràng hơn trong các thao tác thống kê, mô hình hóa và trực quan hóa dữ liệu.

1. data <- data %>%
2.   mutate(
3.     explicit = factor(explicit,
4.                       levels = c(0, 1),
5.                       labels = c("Không nhạy cảm", "Nhạy cảm")),
6.     mode = factor(mode,
7.                   levels = c(0, 1),
8.                   labels = c("Minor", "Major")),
9.     release_year = as.integer(substr(release_date, 1, 4))
10.   )
11. cols_to_show <- c("artists", "name", "year", "popularity", 
12.                   "danceability", "energy", "tempo", "valence", 
13.                   "explicit", "mode", "release_year")
14. cols_to_show <- cols_to_show[cols_to_show %in% colnames(data)]
15. head(data %>% select(all_of(cols_to_show))) %>%
16.   kable(
17.     caption = "Bảng: Sáu quan sát đầu tiên sau khi xử lý dữ liệu Spotify",
18.     format = "html",
19.     align = "l"
20.   ) %>%
21.   kable_styling(
22.     bootstrap_options = c("striped", "hover", "condensed", "responsive"),
23.     full_width = FALSE,
24.     position = "left"
25.   )
Bảng: Sáu quan sát đầu tiên sau khi xử lý dữ liệu Spotify
artists name year popularity danceability energy tempo valence explicit mode release_year
[‘Sergei Rachmaninoff’, ‘James Levine’, ‘Berliner Philharmoniker’] Piano Concerto No. 3 in D Minor, Op. 30: III. Finale. Alla breve 1921 4 0.279 0.211 80.954 0.0594 Không nhạy cảm Major 1921
[‘Dennis Day’] Clancy Lowered the Boom 1921 5 0.819 0.341 60.936 0.9630 Không nhạy cảm Major 1921
[‘KHP Kridhamardawa Karaton Ngayogyakarta Hadiningrat’] Gati Bali 1921 5 0.328 0.166 110.339 0.0394 Không nhạy cảm Major 1921
[‘Frank Parker’] Danny Boy 1921 3 0.275 0.309 100.109 0.1650 Không nhạy cảm Major 1921
[‘Phil Regan’] When Irish Eyes Are Smiling 1921 2 0.418 0.193 101.665 0.2530 Không nhạy cảm Major 1921
[‘KHP Kridhamardawa Karaton Ngayogyakarta Hadiningrat’] Gati Mardika 1921 6 0.697 0.346 119.824 0.1960 Không nhạy cảm Major 1921

Giải thích: 1. Chuyển đổi các biến trong dataset bằng mutate(). 2. (Bắt đầu mutate) áp dụng các biến mới/biến đã chỉnh sửa. 3. Chuyển biến explicit thành factor. 4. Xác định các mức của explicit: 0 và 1. 5. Gán nhãn cho các mức của explicit: “Không nhạy cảm”, “Nhạy cảm”. 6. Chuyển biến mode thành factor. 7. Xác định các mức của mode: 0 và 1. 8. Gán nhãn cho các mức của mode: “Minor”, “Major”. 9. Tạo biến release_year từ 4 ký tự đầu của release_date và chuyển thành số nguyên. 10. Kết thúc mutate. 11. Tạo vector cols_to_show gồm các cột muốn hiển thị. 12. (Tiếp tục) thêm các cột danceability, energy, tempo, valence. 13. Thêm các cột explicit, mode, release_year vào vector. 14. Lọc các cột chỉ giữ những cột thực sự có trong dataset. 15. Lấy 6 dòng đầu tiên của dataset với các cột đã chọn. 16. Chuyển dữ liệu thành bảng HTML bằng kable(). 17. Thêm caption cho bảng. 18. Chọn định dạng bảng là html. 19. Căn lề các cột sang trái. 20. (Sử dụng pipe) truyền bảng vào hàm tiếp theo. 21. Tùy chỉnh bảng với kable_styling(). 22. Thêm các tùy chọn bootstrap: striped, hover, condensed, responsive. 23. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE). 24. Căn bảng sang trái (position = "left"). 25. Kết thúc hàm kable_styling().

Sau khi thực hiện chuyển đổi, các biến explicit và mode đã được định dạng lại thành biến định tính với nhãn rõ ràng, giúp dễ nhận diện nội dung bài hát và tính chất âm nhạc. Biến release_date được tách năm và chuyển thành release_year (kiểu số nguyên), giúp thuận tiện cho việc phân tích xu hướng âm nhạc theo thời gian. Bộ dữ liệu sau khi xử lý bao gồm 20 biến và 170,653 quan sát, sẵn sàng cho bước kiểm tra giá trị thiếu và thống kê mô tả.

1.1.2.3. Tóm tắt tổng quan

Mục đích:
Bước này nhằm tóm tắt nhanh các đặc điểm thống kê cơ bản của toàn bộ bộ dữ liệu Spotify, bao gồm: - Kiểm tra số lượng giá trị hợp lệ và giá trị thiếu (NA).
- Quan sát phạm vi giá trị (min, max) và độ phân tán của các biến định lượng.
- Xem xét số lượng mức (levels) của các biến định tính.
Việc kiểm tra này giúp nhận diện sớm các vấn đề về dữ liệu trước khi tiến hành thống kê mô tả và trực quan hóa.

1. my_skim <- skim_with(
2.   numeric = list(hist = NULL),
3.   append = FALSE
4. )
5. skim_summary_custom <- my_skim(data)
6. cols_to_show <- c("skim_variable", "skim_type", "n_missing", 
7.                   "complete_rate", "mean", "sd", "min", "p25", "median", "p75", "max")
8. skim_summary_custom <- skim_summary_custom %>%
9.   select(any_of(cols_to_show))
10. skim_summary_custom %>%
11.   kable(
12.     format = "html",
13.     align = "l"
14.   ) %>%
15.   kable_styling(
16.     bootstrap_options = c("striped", "hover", "condensed", "responsive"),
17.     full_width = FALSE
18.   )
skim_variable skim_type n_missing complete_rate
artists character 0 1
id character 0 1
name character 0 1
release_date character 0 1
explicit factor 0 1
mode factor 0 1
valence numeric 0 1
year numeric 0 1
acousticness numeric 0 1
danceability numeric 0 1
duration_ms numeric 0 1
energy numeric 0 1
instrumentalness numeric 0 1
key numeric 0 1
liveness numeric 0 1
loudness numeric 0 1
popularity numeric 0 1
speechiness numeric 0 1
tempo numeric 0 1
release_year numeric 0 1

Giải thích kỹ thuật: 1. Tùy chỉnh skimr để bỏ biểu đồ nhỏ trong bảng tóm tắt. 2. Đặt hist = NULL cho biến numeric để không hiển thị histogram. 3. Không thêm các thiết lập skimr khác vào bảng gốc (append = FALSE). 4. Kết thúc hàm skim_with(). 5. Áp dụng my_skim cho dataset data để tạo bảng tóm tắt. 6. Tạo vector cols_to_show gồm các cột muốn giữ trong bảng tóm tắt. 7. Liệt kê các cột chi tiết: skim_variable, skim_type, n_missing, complete_rate, mean, sd, min, p25, median, p75, max. 8. Chọn các cột thực sự có trong dataset bằng select(any_of(cols_to_show)). 9. Cập nhật lại bảng tóm tắt chỉ giữ các cột đã chọn. 10. Truyền dữ liệu vào kable() để xuất bảng. 11. Chuyển dữ liệu thành bảng HTML. 12. Chọn định dạng bảng là html. 13. Căn lề các cột sang trái. 14. (Sử dụng pipe) truyền bảng vào hàm tiếp theo. 15. Tùy chỉnh bảng với kable_styling(). 16. Thêm các tùy chọn bootstrap: striped, hover, condensed, responsive. 17. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE). 18. Kết thúc hàm kable_styling().

Nhận xét:
Kết quả tóm tắt từ hàm skim() cho thấy bức tranh tổng quan khá rõ ràng về bộ dữ liệu Spotify, với quy mô lớn (170.653 dòng và 20 biến), phản ánh dữ liệu được thu thập tốt và đồng nhất.

  • Về cấu trúc và kiểu dữ liệu:
    Bộ dữ liệu gồm chủ yếu là biến định lượng (14 biến), bên cạnh 4 biến ký tự2 biến phân loại. Cấu trúc này rất phù hợp cho việc khai thác xu hướng, mối quan hệ giữa các đặc trưng âm nhạc và độ phổ biến của bài hát.

  • Về tính đầy đủ của dữ liệu:
    Tất cả các biến đều có complete_rate = 1, tức là không có giá trị thiếu (NA). Điều này cho thấy dữ liệu gốc được xử lý sạch sẽ, không cần thao tác điền hoặc loại bỏ giá trị khuyết.

  • Phân tích sơ bộ nhóm biến định lượng:
    Các đặc trưng âm nhạc như valence, energy, danceability, acousticness có giá trị dao động trong khoảng 0–1, đúng với thang đo kỹ thuật của Spotify.
    Biến popularity có trung bình khoảng 31 điểm và độ lệch chuẩn lớn (21,8), thể hiện mức độ phổ biến giữa các bài hát rất khác nhau.
    Các biến yearrelease_year trải dài từ 1921 đến 2020, cho phép khai thác sự thay đổi xu hướng âm nhạc qua gần một thế kỷ.

  • Phân tích sơ bộ nhóm biến định tính:
    Biến explicit cho thấy phần lớn bài hát không chứa nội dung nhạy cảm (≈91%), chỉ có khoảng 9% mang nội dung “Explicit”.
    Biến mode cũng thể hiện sự chênh lệch tương tự, với 70% bài hát ở điệu trưởng (Major)30% ở điệu thứ (Minor), cho thấy xu hướng âm nhạc thiên về giai điệu tươi sáng.

Tổng kết:
Dữ liệu có chất lượng tốt, không khuyết giá trị và các biến được ghi nhận đúng định dạng. Đây là nền tảng thuận lợi để tiến hành thống kê mô tả chi tiết và trực quan hóa trong các bước kế tiếp.

1.1.2.4. Tái kiểm tra giá trị thiếu

1. cat("\nKiểm tra giá trị khuyết thiếu (NA):\n")
## 
## Kiểm tra giá trị khuyết thiếu (NA):
1. na_counts <- colSums(is.na(data))
2. cols_with_na <- na_counts[na_counts > 0]
3. if (length(cols_with_na) == 0) {
4.   cat("Bộ dữ liệu Spotify không có giá trị thiếu (NA) trong bất kỳ biến nào.\n")
5. } else {
6.   cat("Các biến có chứa giá trị NA và số lượng tương ứng:\n")
7.   print(cols_with_na)
8. }
## Bộ dữ liệu Spotify không có giá trị thiếu (NA) trong bất kỳ biến nào.

Giải thích kỹ thuật 1. In thông báo kiểm tra giá trị khuyết thiếu (NA) ra màn hình.

Giải thích kỹ thuật 1. Tính tổng số giá trị khuyết thiếu (NA) cho từng cột trong dataset. 2. Lọc ra các cột có ít nhất một giá trị NA. 3. Kiểm tra nếu không có cột nào chứa NA. 4. In thông báo rằng dataset không có giá trị NA. 5. Nếu có cột chứa NA, thực hiện bước tiếp theo. 6. In thông báo các cột có giá trị NA và số lượng tương ứng. 7. Hiển thị số lượng NA cho từng cột chứa NA. 8. Kết thúc khối điều kiện if-else.

1.1.2.5. Kiểm tra bản ghi trùng lặp

1. cat("\nKiểm tra bản ghi trùng lặp:\n")
## 
## Kiểm tra bản ghi trùng lặp:
1. duplicate_rows <- data[duplicated(data), ]
2. num_duplicates <- nrow(duplicate_rows)
3. if (num_duplicates == 0) {
4.   cat("Không có bản ghi trùng lặp trong bộ dữ liệu Spotify.\n")
5. } else {
6.   cat("Phát hiện", num_duplicates, "bản ghi trùng lặp trong bộ dữ liệu.\n")
7. }
## Không có bản ghi trùng lặp trong bộ dữ liệu Spotify.

Giải thích kỹ thuật 1. In thông báo kiểm tra các bản ghi trùng lặp trong dataset.

Giải thích kỹ thuật 1. Lấy các bản ghi trùng lặp trong dataset và lưu vào duplicate_rows. 2. Đếm số lượng bản ghi trùng lặp và lưu vào num_duplicates. 3. Kiểm tra nếu không có bản ghi trùng lặp. 4. In thông báo rằng dataset không có bản ghi trùng lặp. 5. Nếu có bản ghi trùng lặp, thực hiện bước tiếp theo. 6. In thông báo số lượng bản ghi trùng lặp được phát hiện. 7. Kết thúc khối điều kiện if-else.

1.1.3. Xử lý dữ liệu, làm sạch và tạo biến phát sinh

1.1.3.1. Chuẩn hoá tên biến

1. cat("\nChuẩn hóa tên biến trong bộ dữ liệu:\n")
## 
## Chuẩn hóa tên biến trong bộ dữ liệu:
1. library(janitor)
2. data <- data %>%
3.   janitor::clean_names()
4. names(data)
##  [1] "valence"          "year"             "acousticness"     "artists"         
##  [5] "danceability"     "duration_ms"      "energy"           "explicit"        
##  [9] "id"               "instrumentalness" "key"              "liveness"        
## [13] "loudness"         "mode"             "name"             "popularity"      
## [17] "release_date"     "speechiness"      "tempo"            "release_year"

Giải thích kỹ thuật 1. In thông báo về việc chuẩn hóa tên biến trong dataset.

Giải thích kỹ thuật

  1. Nạp thư viện janitor để hỗ trợ làm sạch và chuẩn hóa tên biến.
  2. Bắt đầu thao tác với dataset data.
  3. Chuẩn hóa tên biến trong dataset (chuyển về dạng lowercase, thay ký tự đặc biệt bằng dấu gạch dưới) bằng clean_names().
  4. Hiển thị tên các cột sau khi chuẩn hóa.

1.1.3.2. Xử lý thời gian và tạo biến chu kỳ

1. data <- data %>%
2.   mutate(
3.     release_year = ifelse(is.na(release_year), year, release_year),
4.     Decade = paste0(floor(release_year / 10) * 10, "s"),
5.     Era = case_when(
6.       release_year < 1980 ~ "Cổ điển (Classic Era)",
7.       release_year >= 1980 & release_year < 2000 ~ "Hiện đại (Modern Era)",
8.       release_year >= 2000 ~ "Kỹ thuật số (Digital Era)",
9.       TRUE ~ NA_character_
10.     )
11.   )
12. table_era <- table(data$Era) %>%
13.   as.data.frame() %>%
14.   rename(Era = Var1, Frequency = Freq)
15. table_era %>%
16.   kable(format = "html", align = "l") %>%
17.   kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
18.                 full_width = FALSE)
Era Frequency
Cổ điển (Classic Era) 89452
Hiện đại (Modern Era) 39751
Kỹ thuật số (Digital Era) 41450
1. table_decade <- table(data$Decade) %>%
2.   as.data.frame() %>%
3.   rename(Decade = Var1, Frequency = Freq)
4. table_decade %>%
5.   kable(format = "html", align = "l") %>%
6.   kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
7.                 full_width = FALSE)
Decade Frequency
1920s 5126
1930s 9549
1940s 15378
1950s 19850
1960s 19549
1970s 20000
1980s 19850
1990s 19901
2000s 19646
2010s 19774
2020s 2030

Giải thích kỹ thuật

  1. Bắt đầu thao tác với dataset data bằng mutate().
  2. (Bắt đầu mutate) chỉnh sửa và tạo các biến mới.
  3. Nếu release_year bị thiếu, thay bằng giá trị từ biến year.
  4. Tạo biến Decade theo thập kỷ phát hành bài hát.
  5. Tạo biến Era phân loại giai đoạn âm nhạc theo năm phát hành.
  6. Nếu năm phát hành < 1980, gán “Cổ điển (Classic Era)”.
  7. Nếu năm phát hành từ 1980 đến 1999, gán “Hiện đại (Modern Era)”.
  8. Nếu năm phát hành từ 2000 trở đi, gán “Kỹ thuật số (Digital Era)”.
  9. Nếu không thỏa điều kiện nào, gán NA.
  10. Kết thúc case_when().
  11. Kết thúc mutate().
  12. Tạo bảng tần suất cho biến Era.
  13. Chuyển bảng tần suất thành dataframe.
  14. Đổi tên cột thành EraFrequency.
  15. Truyền dataframe vào kable() để xuất bảng.
  16. Chọn định dạng bảng là HTML và căn lề trái.
  17. Tùy chỉnh bảng với kable_styling() sử dụng các tùy chọn bootstrap.
  18. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE).

Giải thích kỹ thuật 1. Tạo bảng tần suất cho biến Decade. 2. Chuyển bảng tần suất thành dataframe. 3. Đổi tên cột thành DecadeFrequency. 4. Truyền dataframe vào kable() để xuất bảng. 5. Chọn định dạng bảng là HTML và căn lề trái. 6. Tùy chỉnh bảng với kable_styling() sử dụng các tùy chọn bootstrap. 7. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE).

Giải thích kỹ thuật 1. Tạo bảng tần suất cho biến Decade. 2. Chuyển bảng tần suất thành dataframe. 3. Đổi tên cột thành DecadeFrequency. 4. Truyền dataframe vào kable() để xuất bảng HTML. 5. Chọn định dạng bảng là HTML và căn lề trái. 6. Tùy chỉnh bảng với các tùy chọn bootstrap: striped, hover, condensed, responsive. 7. Đặt bảng không chiếm toàn bộ chiều rộng (full_width = FALSE).

1.1.3.3. Tạo biến phát sinh

Trong bộ dữ liệu Spotify, các biến ban đầu chủ yếu mô tả đặc điểm kỹ thuật của bài hát như năng lượng, khả năng nhảy, độ phổ biến, nhịp độ, thời lượng… Để phục vụ cho các phân tích sâu hơn ở phần sau (như xu hướng âm nhạc theo thời gian, đặc điểm bài hát phổ biến, mối liên hệ giữa năng lượng và độ phổ biến), ta tiến hành tạo thêm một số biến mới có ý nghĩa thực tiễn trong nghiên cứu âm nhạc.

1. library(tibble)
2. library(knitr)
3. var_desc <- tribble(
4.   ~Biến_mới, ~Cách_tạo, ~Ý_nghĩa_thực_tiễn,
5.   "duration_min", "duration_ms / 60000", 
6.   "Thời lượng bài hát (phút) — phản ánh xu hướng sáng tác (bài ngắn thường phổ biến hơn).",
7. 
8.   "tempo_category", "Phân loại tempo: Chậm / Trung bình / Nhanh", 
9.   "Giúp xem nhịp độ bài hát phổ biến theo từng giai đoạn hoặc thể loại.",
10. 
11.   "is_explicit_factor", "Phân loại explicit thành Có lời nhạy cảm / Không nhạy cảm", 
12.   "Phản ánh mức độ thay đổi trong nội dung âm nhạc hiện đại.",
13. 
14.   "popularity_level", "Nhóm popularity thành Thấp / Trung bình / Cao", 
15.   "Cho phép so sánh đặc điểm kỹ thuật giữa các bài hát có độ phổ biến khác nhau.",
16. 
17.   "energy_dance_ratio", "energy / danceability", 
18.   "Đo sự cân bằng giữa năng lượng và khả năng khiêu vũ — chỉ báo mức độ sôi động.",
19. 
20.   "release_decade", "floor(release_year / 10) * 10", 
21.   "Biến thập kỷ phát hành, dùng để phân tích xu hướng âm nhạc theo thời gian."
22. )
23. kable(var_desc,
24.       caption = "Bảng 1.1.11: Mô tả các biến mới được tạo trong bộ dữ liệu Spotify",
25.       format = "pipe",
26.       align = "l")
Bảng 1.1.11: Mô tả các biến mới được tạo trong bộ dữ liệu Spotify
Biến_mới Cách_tạo Ý_nghĩa_thực_tiễn
duration_min duration_ms / 60000 Thời lượng bài hát (phút) — phản ánh xu hướng sáng tác (bài ngắn thường phổ biến hơn).
tempo_category Phân loại tempo: Chậm / Trung bình / Nhanh Giúp xem nhịp độ bài hát phổ biến theo từng giai đoạn hoặc thể loại.
is_explicit_factor Phân loại explicit thành Có lời nhạy cảm / Không nhạy cảm Phản ánh mức độ thay đổi trong nội dung âm nhạc hiện đại.
popularity_level Nhóm popularity thành Thấp / Trung bình / Cao Cho phép so sánh đặc điểm kỹ thuật giữa các bài hát có độ phổ biến khác nhau.
energy_dance_ratio energy / danceability Đo sự cân bằng giữa năng lượng và khả năng khiêu vũ — chỉ báo mức độ sôi động.
release_decade floor(release_year / 10) * 10 Biến thập kỷ phát hành, dùng để phân tích xu hướng âm nhạc theo thời gian.
1. data <- data %>%
2.   mutate(
3.     duration_min = duration_ms / 60000,
4. 
5.     tempo_category = case_when(
6.       tempo < 90 ~ "Chậm",
7.       tempo >= 90 & tempo < 130 ~ "Trung bình",
8.       tempo >= 130 ~ "Nhanh"
9.     ),
10.    
11.     is_explicit_factor = ifelse(explicit == "Nhạy cảm", "Có lời nhạy cảm", "Không nhạy cảm"),
12.     
13. 
14.     popularity_level = case_when(
15.       popularity < 30 ~ "Thấp",
16.       popularity >= 30 & popularity < 60 ~ "Trung bình",
17.       popularity >= 60 ~ "Cao"
18.     ),
19. 
20.     energy_dance_ratio = energy / danceability,
21.     
22. 
23.     release_decade = floor(release_year / 10) * 10
24.   )
25. kable(head(data %>% select(name, duration_min, tempo_category, is_explicit_factor, 
26.                            popularity_level, energy_dance_ratio, release_decade)),
27.       caption = "Bảng: Các biến mới có ý nghĩa thực tiễn trong bộ dữ liệu Spotify",
28.       format = "pipe")
Bảng: Các biến mới có ý nghĩa thực tiễn trong bộ dữ liệu Spotify
name duration_min tempo_category is_explicit_factor popularity_level energy_dance_ratio release_decade
Piano Concerto No. 3 in D Minor, Op. 30: III. Finale. Alla breve 13.861117 Chậm Không nhạy cảm Thấp 0.7562724 1920
Clancy Lowered the Boom 3.008883 Chậm Không nhạy cảm Thấp 0.4163614 1920
Gati Bali 8.334367 Trung bình Không nhạy cảm Thấp 0.5060976 1920
Danny Boy 3.500000 Trung bình Không nhạy cảm Thấp 1.1236364 1920
When Irish Eyes Are Smiling 2.778217 Trung bình Không nhạy cảm Thấp 0.4617225 1920
Gati Mardika 6.584600 Trung bình Không nhạy cảm Thấp 0.4964132 1920

Giải thích kỹ thuật

  1. Nạp thư viện tibble để tạo và thao tác bảng dữ liệu theo dạng tibble.
  2. Nạp thư viện knitr để xuất bảng đẹp trong R Markdown.
  3. Tạo bảng var_desc bằng tribble() (tạo tibble trực quan, hàng-giá trị).
  4. Đặt tiêu đề cột là Biến_mới, Cách_tạo, Ý_nghĩa_thực_tiễn.
  5. Thêm biến mới duration_min và cách tính.
  6. Thêm ý nghĩa thực tiễn của duration_min.
  7. (Ngắt dòng giữa các biến để dễ đọc).
  8. Thêm biến mới tempo_category và cách phân loại.
  9. Thêm ý nghĩa thực tiễn của tempo_category.
  10. (Ngắt dòng).
  11. Thêm biến mới is_explicit_factor và cách phân loại.
  12. Thêm ý nghĩa thực tiễn của is_explicit_factor.
  13. (Ngắt dòng).
  14. Thêm biến mới popularity_level và cách nhóm.
  15. Thêm ý nghĩa thực tiễn của popularity_level.
  16. (Ngắt dòng).
  17. Thêm biến mới energy_dance_ratio và cách tính.
  18. Thêm ý nghĩa thực tiễn của energy_dance_ratio.
  19. (Ngắt dòng).
  20. Thêm biến mới release_decade và cách tính.
  21. Thêm ý nghĩa thực tiễn của release_decade.
  22. Kết thúc tạo tibble.
  23. Xuất bảng var_desc bằng kable().
  24. Thêm caption cho bảng.
  25. Chọn định dạng bảng là pipe.
  26. Căn lề các cột sang trái.

Giải thích kỹ thuật 1. Bắt đầu thao tác với dataset data bằng mutate(). 2. (Bắt đầu mutate) tạo và chỉnh sửa các biến mới. 3. Tạo biến duration_min từ duration_ms (đổi từ ms sang phút). 4. (Ngắt dòng để dễ đọc). 5. Tạo biến tempo_category phân loại nhịp độ: Chậm / Trung bình / Nhanh. 6. Nếu tempo < 90, gán “Chậm”. 7. Nếu tempo từ 90 đến <130, gán “Trung bình”. 8. Nếu tempo >= 130, gán “Nhanh”. 9. Kết thúc case_when() cho tempo_category. 10. (Ngắt dòng). 11. Tạo biến is_explicit_factor phân loại explicit: “Có lời nhạy cảm” hoặc “Không nhạy cảm”. 12. (Ngắt dòng). 13. (Ngắt dòng để dễ đọc). 14. Tạo biến popularity_level phân nhóm độ phổ biến: Thấp / Trung bình / Cao. 15. Nếu popularity < 30, gán “Thấp”. 16. Nếu popularity từ 30 đến <60, gán “Trung bình”. 17. Nếu popularity >= 60, gán “Cao”. 18. Kết thúc case_when() cho popularity_level. 19. (Ngắt dòng). 20. Tạo biến energy_dance_ratio bằng tỷ số energy / danceability. 21. (Ngắt dòng). 22. (Ngắt dòng để dễ đọc). 23. Tạo biến release_decade từ release_year. 24. Kết thúc mutate(). 25. Lấy 6 dòng đầu tiên của các biến mới quan trọng (name, duration_min, tempo_category, is_explicit_factor, popularity_level, energy_dance_ratio, release_decade) và truyền vào kable(). 26. (Tiếp tục chọn cột hiển thị). 27. Thêm caption cho bảng. 28. Chọn định dạng bảng là pipe.

Bảng trên minh họa một số biến mới được tạo ra từ dữ liệu gốc. Kết quả cho thấy một số bài hát cổ điển có thời lượng dài hơn trung bình (trên 10 phút), trong khi hầu hết bài hát khác chỉ kéo dài từ 2 đến 5 phút. Các biến phân loại như tempo_category và popularity_level giúp mô tả rõ ràng hơn đặc điểm âm nhạc của từng giai đoạn. Biến energy_dance_ratio phản ánh mức độ sôi động của bài hát, với giá trị cao thường gặp ở những bản nhạc có năng lượng mạnh nhưng không dễ nhảy. Việc chuẩn hóa các biến này giúp thuận tiện cho các phân tích và trực quan hóa xu hướng âm nhạc ở các phần sau.

1.1.3.4 Phân chia và lưu trữ dữ liệu theo nhóm biến

1. common_vars <- c("name", "artists", "release_year", "release_decade")
2. group1_vars <- c(common_vars,
3.                  "danceability", "energy", "valence", "acousticness", "instrumentalness",
4.                  "liveness", "loudness", "speechiness", "tempo", "key", "mode",
5.                  "energy_dance_ratio")
6. group1_vars <- unique(group1_vars)
7. group2_vars <- c(common_vars,
8.                  "duration_min", "explicit", "popularity", "popularity_level", 
9.                  "is_explicit_factor", "tempo_category", "release_date")
10. group2_vars <- unique(group2_vars)
11. spotify_group1 <- data %>%
12.   select(intersect(group1_vars, colnames(data)))
13. spotify_group2 <- data %>%
14.   select(intersect(group2_vars, colnames(data)))
15. cat("\nNhóm 1 (Đặc điểm kỹ thuật âm nhạc):\n")
## 
## Nhóm 1 (Đặc điểm kỹ thuật âm nhạc):
1. cat("Số biến:", ncol(spotify_group1), "\n")
## Số biến: 16
1. print(colnames(spotify_group1))
##  [1] "name"               "artists"            "release_year"      
##  [4] "release_decade"     "danceability"       "energy"            
##  [7] "valence"            "acousticness"       "instrumentalness"  
## [10] "liveness"           "loudness"           "speechiness"       
## [13] "tempo"              "key"                "mode"              
## [16] "energy_dance_ratio"
1. cat("\nNhóm 2 (Độ phổ biến & Thông tin tổng quan):\n")
## 
## Nhóm 2 (Độ phổ biến & Thông tin tổng quan):
1. cat("Số biến:", ncol(spotify_group2), "\n")
## Số biến: 11
1. print(colnames(spotify_group2))
##  [1] "name"               "artists"            "release_year"      
##  [4] "release_decade"     "duration_min"       "explicit"          
##  [7] "popularity"         "popularity_level"   "is_explicit_factor"
## [10] "tempo_category"     "release_date"
1. save(data, spotify_group1, spotify_group2,
2.      file = "spotify_groups.RData")

Giải thích kỹ thuật 1. Tạo vector common_vars gồm các cột chung cho cả hai nhóm. 2. Tạo vector group1_vars gồm common_vars và các biến kỹ thuật âm nhạc. 3. Thêm các biến: danceability, energy, valence, acousticness, instrumentalness. 4. Thêm các biến: liveness, loudness, speechiness, tempo, key, mode, energy_dance_ratio. 5. (Đảm bảo các biến duy nhất trong group1_vars). 6. Loại bỏ các biến trùng lặp trong group1_vars. 7. Tạo vector group2_vars gồm common_vars và các biến metadata, popularity. 8. Thêm các biến: duration_min, explicit, popularity, popularity_level. 9. Thêm các biến: is_explicit_factor, tempo_category, release_date. 10. Loại bỏ các biến trùng lặp trong group2_vars. 11. Chọn các cột của group1_vars tồn tại trong dataset và lưu vào spotify_group1. 12. Chỉ giữ các cột thực sự có trong dataset cho spotify_group1. 13. Chọn các cột của group2_vars tồn tại trong dataset và lưu vào spotify_group2. 14. Chỉ giữ các cột thực sự có trong dataset cho spotify_group2. 15. In thông báo về Nhóm 1 (Đặc điểm kỹ thuật âm nhạc).

Giải thích kỹ thuật 1. In ra số lượng biến (cột) trong spotify_group1.

Giải thích kỹ thuật 1. Hiển thị tên các cột trong spotify_group1.

Giải thích kỹ thuật 1. In thông báo về Nhóm 2 (Độ phổ biến & Thông tin tổng quan).

Giải thích kỹ thuật 1. In ra số lượng biến (cột) trong spotify_group2.

Giải thích kỹ thuật 1. Hiển thị tên các cột trong spotify_group2.

Giải thích kỹ thuật 1 - 2: Lưu các đối tượng data, spotify_group1spotify_group2 vào file "spotify_groups.RData" để sử dụng sau này.

Trong mục này, bộ dữ liệu Spotify đã được chia thành hai nhóm hợp lý: nhóm đặc điểm kỹ thuật âm nhạc (bao gồm các chỉ số như danceability, energy, valence…) và nhóm độ phổ biến & thông tin tổng quan (bao gồm popularity, explicit, duration, tempo_category…). Các biến chung như tên bài hát, nghệ sĩ, năm phát hành được giữ trong cả hai nhóm để liên kết dữ liệu. Việc phân nhóm này giúp tổ chức dữ liệu khoa học, thuận tiện cho phân tích chuyên biệt, đồng thời lưu cả ba bộ dữ liệu ra spotify_groups.RData đảm bảo dễ dàng truy xuất và tái sử dụng cho các bước phân tích và trực quan hóa tiếp theo.

1.2 Phân tích đặc trưng kỹ thuật âm nhạc theo thời gian

1.2.1. Phân tích thống kê mô tả và phân phối các đặc trưng kỹ thuật âm nhạc

1.2.1.1. Thống kê mô tả các đặc trưng kỹ thuật âm nhạc

1. library(dplyr)
2. library(tidyr)
3. library(knitr)
4. vars_g1_numeric <- c("danceability", "energy", "valence", "acousticness",
5.                      "instrumentalness", "liveness", "loudness", "speechiness", "tempo",
6.                      "energy_dance_ratio")
7. summary_stats_g1_wide <- spotify_group1 %>%
8.   select(any_of(vars_g1_numeric)) %>%
9.   summarise(across(everything(),
10.                    list(Min = ~ min(.x, na.rm = TRUE),
11.                         Q1 = ~ quantile(.x, 0.25, na.rm = TRUE),
12.                         Median = ~ median(.x, na.rm = TRUE),
13.                         Mean = ~ mean(.x, na.rm = TRUE),
14.                         Q3 = ~ quantile(.x, 0.75, na.rm = TRUE),
15.                         Max = ~ max(.x, na.rm = TRUE)),
16.                    .names = "{.col}_{.fn}"))
17. summary_g1_final_table <- summary_stats_g1_wide %>%
18.   as.matrix() %>% t() %>% as.data.frame() %>%
19.   rownames_to_column("VarStat") %>%
20.   mutate(
21.     Statistic = factor(gsub(".*_", "", VarStat), 
22.                        levels = c("Min","Q1","Median","Mean","Q3","Max")),
23.     Variable = gsub("_.*", "", VarStat)
24.   ) %>%
25.   rename(Value = V1) %>%
26.   select(Variable, Statistic, Value) %>%
27.  
28.   pivot_wider(names_from = Statistic, values_from = Value, values_fn = mean) %>%
29.   mutate(across(where(is.numeric), ~ round(.x, 3))) %>%
30.   arrange(Variable)
31. kable(summary_g1_final_table,
32.       caption = "Bảng 1.3.1: Thống kê Mô tả Tổng quan (Đặc trưng kỹ thuật âm nhạc)",
33.       col.names = c("Tên Biến", "Min", "Q1", "Median", "Mean", "Q3", "Max"), 
34.       format = "pipe",
35.       align = "l")
Bảng 1.3.1: Thống kê Mô tả Tổng quan (Đặc trưng kỹ thuật âm nhạc)
Tên Biến Min Q1 Median Mean Q3 Max
acousticness 0 0.102 0.516 0.502 0.893 0.996
danceability 0 0.415 0.548 0.537 0.668 0.988
energy 0 0.383 0.659 Inf 0.980 Inf
instrumentalness 0 0.000 0.000 0.167 0.102 1.000
liveness 0 0.099 0.136 0.206 0.261 1.000
loudness -60 -14.615 -10.580 -11.468 -7.183 3.855
speechiness 0 0.035 0.045 0.098 0.076 0.970
tempo 0 93.421 114.729 116.862 135.537 243.507
valence 0 0.317 0.540 0.529 0.747 1.000

Giải thích kỹ thuật: 1-2. Nạp các thư viện dplyrtidyr để xử lý dữ liệu và biến đổi bảng. 3. Nạp thư viện knitr để xuất bảng đẹp trong R Markdown. 4-6. Xác định các biến số (numeric) trong Nhóm 1 (vars_g1_numeric). 7-16. Tính các thống kê mô tả (Min, Q1, Median, Mean, Q3, Max) cho các biến numeric trong Nhóm 1 và lưu kết quả vào summary_stats_g1_wide. 17-30. Chuyển kết quả sang dạng dài, tách tên biến và tên thống kê, sau đó biến đổi thành bảng rộng, làm tròn số và sắp xếp theo tên biến, lưu vào summary_g1_final_table. 31-35. Xuất bảng summary_g1_final_table bằng kable() với caption, tên cột, định dạng pipe và căn lề trái.

Nhận xét đặc trưng kỹ thuật âm nhạc

Xu hướng trung tâm:
- Danceability: Median = 0.548, Mean = 0.537 → hơi lệch trái.
- Energy: Median = 0.659, Mean = Inf → có giá trị cực đoan ảnh hưởng đến trung bình.
- Valence: Median = 0.540, Mean = 0.529 → gần đối xứng.
- Acousticness: Median = 0.516, Mean = 0.502 → gần đối xứng.
- Loudness: Median = -10.580, Mean = -11.468 → lệch trái.
- Tempo: Median = 114.729, Mean = 116.862 → lệch phải nhẹ.

Độ phân tán:
- Phạm vi min-max: acousticness (0–0.996), danceability (0–0.988), energy (0–Inf), loudness (-60–3.855), tempo (0–243.507).
- Nhìn chung các biến biến động rộng, đặc biệt loudness và tempo, phản ánh sự đa dạng lớn trong đặc trưng kỹ thuật âm nhạc.

Dấu hiệu độ lệch (Skewness):
- Lệch phải (Mean > Median): danceability, tempo.
- Gần đối xứng (Mean ≈ Median): valence, acousticness.
- Lệch trái (Mean < Median): loudness.
- Một số giá trị cực đoan, đặc biệt energy và loudness, làm trung bình khác biệt so với Median.

Kết luận sơ bộ:
Các đặc trưng kỹ thuật âm nhạc cho thấy sự đa dạng đáng kể về nhịp điệu, năng lượng, cảm xúc, âm lượng và tempo. Các giá trị cực đoan ở energy và loudness cần lưu ý khi phân tích hoặc xây dựng mô hình tiếp theo.

1.2.1.2. Độ lệch chuẩn

1. sd_table_g1 <- spotify_group1 %>%
2.   summarise(across(any_of(vars_g1_numeric), ~ sd(.x, na.rm = TRUE),
3.                    .names = "SD_{.col}")) %>%
4.   pivot_longer(everything(), names_to = "Variable_Prefixed",
5.                values_to = "Standard_Deviation") %>%
6.   mutate(Variable = sub("^SD_", "", Variable_Prefixed)) %>%
7.   select(Variable, Standard_Deviation) %>%
8.   mutate(Standard_Deviation = round(Standard_Deviation, 3)) %>%
9.   arrange(Variable)
10. kable(sd_table_g1,
11.       caption = "Bảng 1.3.2: Độ lệch chuẩn (Nhóm biến Đặc trưng kỹ thuật âm nhạc)",
12.       col.names = c("Tên Biến", "Độ lệch chuẩn (SD)"),
13.       format = "pipe", align = "l")
Bảng 1.3.2: Độ lệch chuẩn (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
Tên Biến Độ lệch chuẩn (SD)
acousticness 0.376
danceability 0.176
energy 0.268
energy_dance_ratio NaN
instrumentalness 0.313
liveness 0.175
loudness 5.698
speechiness 0.163
tempo 30.709
valence 0.263

Giải thích kỹ thuật: 1-9. Tính độ lệch chuẩn (SD) cho các biến numeric trong Nhóm 1, chuẩn hóa tên biến, làm tròn kết quả và sắp xếp theo tên biến, lưu vào sd_table_g1. 10-13. Xuất bảng sd_table_g1 bằng kable() với caption, tên cột, định dạng pipe và căn lề trái.

Nhận xét Bảng 1.3.2: Độ lệch chuẩn (Nhóm biến Đặc trưng kỹ thuật âm nhạc)

  • Độ biến động về âm thanh acousticness SD = 0376 và instrumentalness SD = 0313 cho thấy mức độ đa dạng về đặc tính âm thanh và độ nhạc cụ trong các bản nhạc

  • Năng lượng và cảm xúc energy SD = 0268 và valence SD = 0263 phản ánh sự khác biệt về cảm xúc và năng lượng giữa các bài hát

  • Độ ổn định tương đối danceability SD = 0176, liveness SD = 0175, speechiness SD = 0163 cho thấy các bài hát có đặc trưng này phân bố tương đối tập trung quanh giá trị trung bình

  • Biến động lớn về nhịp điệu và âm lượng loudness SD = 5698 và tempo SD = 30709 biến động đáng kể phản ánh sự đa dạng về cường độ âm thanh và nhịp điệu giữa các bản nhạc

energy_dance_ratio có giá trị NaN cho thấy có thể tồn tại giá trị thiếu hoặc biến này cần được xử lý trước khi phân tích sâu hơn

Kết luận

Nhóm biến Đặc trưng kỹ thuật âm nhạc có mức độ phân tán khác nhau
Một số biến như loudness, tempo, acousticness, instrumentalness biến động lớn phản ánh sự đa dạng kỹ thuật âm nhạc
Trong khi các biến như danceability, liveness, speechiness tập trung quanh giá trị trung bình biểu thị mức độ đồng đều tương đối giữa các bài hát

1.2.1.3. Bảng tần suất biến phân loại

1. library(dplyr)
2. library(knitr)
3. library(rlang)
4. create_freq_table_vi <- function(data, var_name, caption_text) {
5.   freq_table <- data %>%
6.     count(!!rlang::sym(var_name), name = "SoLuong") %>%
7.     mutate(TyLe_PhanTram = paste0(round(SoLuong / sum(SoLuong) * 100, 1), "%")) %>%
8.     arrange(desc(SoLuong))
9.   
10.   colnames(freq_table)[1] <- var_name
11.   
12.   print(kable(freq_table,
13.               caption = caption_text,
14.               col.names = c(var_name, "Số lượng", "Tỷ lệ (%)"),
15.               format = "pipe",
16.               align = "l"))
17. }
18. if ("release_decade" %in% names(spotify_group1)) {
19.   create_freq_table_vi(spotify_group1, "release_decade",
20.                        "Bảng 1.3.1.3: Phân bố 'release_decade' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
21. }
## 
## 
## Table: Bảng 1.3.1.3: Phân bố 'release_decade' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
## 
## |release_decade |Số lượng |Tỷ lệ (%) |
## |:--------------|:--------|:---------|
## |1970           |20000    |11.7%     |
## |1990           |19901    |11.7%     |
## |1950           |19850    |11.6%     |
## |1980           |19850    |11.6%     |
## |2010           |19774    |11.6%     |
## |2000           |19646    |11.5%     |
## |1960           |19549    |11.5%     |
## |1940           |15378    |9%        |
## |1930           |9549     |5.6%      |
## |1920           |5126     |3%        |
## |2020           |2030     |1.2%      |
1. if ("explicit" %in% names(spotify_group1)) {
2.   create_freq_table_vi(spotify_group1, "explicit",
3.                        "Bảng 1.3.1.4: Phân bố 'explicit' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
4. }
5. if ("tempo_category" %in% names(spotify_group1)) {
6.   create_freq_table_vi(spotify_group1, "tempo_category",
7.                        "Bảng 1.3.1.5: Phân bố 'tempo_category' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
8. }
9. if ("popularity_level" %in% names(spotify_group1)) {
10.   create_freq_table_vi(spotify_group1, "popularity_level",
11.                        "Bảng 1.3.1.6: Phân bố 'popularity_level' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
12. }
13. if ("key" %in% names(spotify_group1)) {
14.   create_freq_table_vi(spotify_group1, "key",
15.                        "Bảng 1.3.1.7: Phân bố 'key' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
16. }
## 
## 
## Table: Bảng 1.3.1.7: Phân bố 'key' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
## 
## |key |Số lượng |Tỷ lệ (%) |
## |:---|:--------|:---------|
## |0   |21600    |12.7%     |
## |7   |20803    |12.2%     |
## |2   |18823    |11%       |
## |9   |17571    |10.3%     |
## |5   |16430    |9.6%      |
## |4   |12933    |7.6%      |
## |1   |12886    |7.6%      |
## |10  |12148    |7.1%      |
## |8   |10751    |6.3%      |
## |11  |10670    |6.3%      |
## |6   |8741     |5.1%      |
## |3   |7297     |4.3%      |
1. if ("mode" %in% names(spotify_group1)) {
2.   create_freq_table_vi(spotify_group1, "mode",
3.                        "Bảng 1.3.1.8: Phân bố 'mode' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)")
4. }
## 
## 
## Table: Bảng 1.3.1.8: Phân bố 'mode' (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
## 
## |mode  |Số lượng |Tỷ lệ (%) |
## |:-----|:--------|:---------|
## |Major |120635   |70.7%     |
## |Minor |50018    |29.3%     |

Giải thích kỹ thuật: 1-3. Nạp các thư viện dplyr, knitrrlang để xử lý dữ liệu, xuất bảng và thao tác với tên biến động. 4-17. Định nghĩa hàm create_freq_table_vi() để:

  • Tạo bảng tần suất và số lượng cho biến var_name.
  • Tính tỷ lệ phần trăm và làm tròn 1 chữ số thập phân.
  • Sắp xếp theo số lượng giảm dần.
  • Đổi tên cột đầu tiên thành tên biến.
  • Xuất bảng đẹp bằng kable() với caption, tên cột, định dạng pipe và căn lề trái. 18-21. Kiểm tra nếu biến release_decade tồn tại trong spotify_group1, gọi hàm create_freq_table_vi() để xuất bảng phân bố.

Giải thích kỹ thuật: 1-4. Nếu biến explicit tồn tại trong spotify_group1, xuất bảng phân bố bằng create_freq_table_vi(). 5-8. Nếu biến tempo_category tồn tại, xuất bảng phân bố tương ứng. 9-12. Nếu biến popularity_level tồn tại, xuất bảng phân bố tương ứng. 13-16. Nếu biến key tồn tại, xuất bảng phân bố tương ứng.

Giải thích kỹ thuật: 1-4. Nếu biến mode tồn tại trong spotify_group1, xuất bảng phân bố bằng create_freq_table_vi().

Nhận xét

  • release_decade (Bảng 1.3.1.3) Phân bố khá đều theo từng thập kỷ từ 1920 → 2020. Các thập kỷ 1970 (11.7%), 1990 (11.7%), 1950 (11.6%), 1980 (11.6%) và 2010 (11.6%) chiếm tỷ trọng lớn nhất, phản ánh dataset tập trung nhiều vào âm nhạc từ giữa thế kỷ 20 đến đầu thế kỷ 21. Thập kỷ 2020 chiếm tỷ lệ thấp nhất (1.2%), do số lượng bài hát mới ít hoặc chưa được cập nhật đầy đủ.

  • key (Bảng 1.3.1.7) Các bản nhạc được phân bố theo 12 cung âm (0 → 11). Các cung phổ biến nhất là 0 (12.7%), 7 (12.2%) và 2 (11%), chiếm tỷ trọng cao, cho thấy các bản nhạc thường sử dụng cung dễ nghe và quen thuộc. Các cung ít phổ biến như 6 (5.1%) và 3 (4.3%) ít xuất hiện trong dataset.

  • mode (Bảng 1.3.1.8) Đa số các bản nhạc sử dụng Major (70.7%), trong khi Minor chỉ chiếm 29.3%. Điều này phản ánh xu hướng âm nhạc chủ đạo vui tươi, sáng sủa (Major), trong khi cảm xúc buồn, u sầu (Minor) ít xuất hiện hơn.

1.2.1.4. Phân tích phân vị biến định lượng

1. library(knitr)
2. vars_g1_quantile <- c("danceability", "energy", "valence", "acousticness",
3.                       "instrumentalness", "liveness", "loudness", "speechiness", "tempo")
4. quantile_results_g1 <- sapply(spotify_group1[, ..vars_g1_quantile],
5.                               quantile,
6.                               probs = c(0, 0.25, 0.5, 0.75, 1),
7.                               na.rm = TRUE)
8. rownames(quantile_results_g1) <- c("Min (0%)","Q1 (25%)","Median (50%)","Q3 (75%)","Max (100%)")
9. quantile_df_g1 <- as.data.frame(round(quantile_results_g1, 3)) %>%
10.   rownames_to_column("Percentile")
11. kable(quantile_df_g1,
12.       caption = "Bảng 1.3.8: Các điểm phân vị của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)",
13.       format = "pipe", align = "l")
Bảng 1.3.8: Các điểm phân vị của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
Percentile danceability energy valence acousticness instrumentalness liveness loudness speechiness tempo
Min (0%) 0.000 0.000 0.000 0.000 0.000 0.000 -60.000 0.000 0.000
Q1 (25%) 0.415 0.255 0.317 0.102 0.000 0.099 -14.615 0.035 93.421
Median (50%) 0.548 0.471 0.540 0.516 0.000 0.136 -10.580 0.045 114.729
Q3 (75%) 0.668 0.703 0.747 0.893 0.102 0.261 -7.183 0.076 135.537
Max (100%) 0.988 1.000 1.000 0.996 1.000 1.000 3.855 0.970 243.507

Giải thích kỹ thuật: 1-3. Nạp thư viện knitr và xác định các biến định lượng trong Nhóm 1 (vars_g1_quantile). 4-7. Tính các điểm phân vị (0%, 25%, 50%, 75%, 100%) cho các biến numeric trong Nhóm 1, bỏ giá trị thiếu (NA). 8. Đặt tên hàng tương ứng với các phân vị. 9-10. Chuyển kết quả sang dạng dataframe, làm tròn 3 chữ số thập phân, thêm cột tên phân vị (Percentile). 11-13. Xuất bảng quantile_df_g1 bằng kable() với caption, định dạng pipe và căn lề trái.

Nhận xét:

  • Độ phân bố trung tâm (Median)

danceability = 0.548, energy = 0.471, valence = 0.540 cho thấy các bản nhạc có mức độ vừa phải về khả năng nhảy, năng lượng và cảm xúc.

acousticness = 0.516, liveness = 0.136, speechiness = 0.045 phản ánh phần lớn bài hát có âm thanh trực tiếp (live) và lời nói thấp, trong khi mức độ acoustic trung bình.

  • Biên độ biến động (Min – Max)

loudness dao động từ -60 đến 3.855, tempo từ 0 đến 243.507, thể hiện sự đa dạng lớn về cường độ âm thanh và nhịp điệu.

instrumentalness Max = 1 nhưng Median = 0, nghĩa là đa số bài hát không thuần instrumental, chỉ một số ít hoàn toàn nhạc cụ.

  • Độ lệch và phân tán (Q1–Q3)

energy, valence, acousticness có Q1–Q3 khá rộng, phản ánh sự khác biệt đáng kể về năng lượng, cảm xúc và acoustic giữa các bản nhạc.

danceability, liveness, speechiness có Q1–Q3 hẹp hơn, phân bố tương đối tập trung quanh giá trị trung bình.

Kết luận

Nhóm biến đặc trưng kỹ thuật âm nhạc cho thấy sự đa dạng về nhịp điệu, âm lượng, năng lượng và acoustic, trong khi một số đặc tính như danceability, liveness, speechiness tập trung hơn. Giá trị cực đoan (Min/Max) chỉ ra một số bài hát đặc biệt khác biệt so với phần lớn mẫu.

1.2.1.5. Phân tích hình dạng phân phối

Độ lệch (skewness) phản ánh mức độ không đối xứng của dữ liệu xung quanh giá trị trung bình. Cách hiểu:

  • Giá trị dương của độ lệch cho thấy phân phối kéo dài về phía các giá trị lớn (đuôi dài bên phải).

  • Giá trị âm của độ lệch cho thấy phân phối kéo dài về phía các giá trị nhỏ (đuôi dài bên trái).

  • Nếu độ lệch xấp xỉ 0, phân phối gần như đối xứng.

  • Độ lớn tuyệt đối của skewness càng cao, sự bất đối xứng càng rõ rệt.

1.2.1.5.1. Đo lường độ xiên

1. library(dplyr)
2. library(knitr)
3. library(e1071)  
4. vars_g1_shape <- c("danceability", "energy", "valence", "acousticness",
5.                    "instrumentalness", "liveness", "loudness", "speechiness", "tempo")
6. skewness_table_g1 <- spotify_group1 %>%
7.   select(any_of(vars_g1_shape)) %>%
8.   summarise(across(everything(), ~ skewness(.x, na.rm = TRUE))) %>%
9.   pivot_longer(everything(), names_to = "Variable", values_to = "Skewness") %>%
10.   mutate(Skewness = round(Skewness, 3)) %>%
11.   arrange(Variable)
12. kable(skewness_table_g1,
13.       caption = "Bảng 1.3.10: Độ xiên (Skewness) của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)",
14.       col.names = c("Tên Biến", "Độ xiên (Skewness)"),
15.       format = "pipe", align = "l")
Bảng 1.3.10: Độ xiên (Skewness) của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
Tên Biến Độ xiên (Skewness)
acousticness -0.033
danceability -0.223
energy 0.112
instrumentalness 1.631
liveness 2.154
loudness -1.052
speechiness 4.048
tempo 0.450
valence -0.107

Giải thích kỹ thuật: 1-3. Nạp các thư viện dplyr, knitre1071 để xử lý dữ liệu, xuất bảng và tính độ xiên (skewness). 4-5. Xác định các biến định lượng trong Nhóm 1 (vars_g1_shape). 6-11. Tính độ xiên cho từng biến numeric, làm tròn 3 chữ số thập phân, sắp xếp theo tên biến và lưu vào skewness_table_g1. 12-15. Xuất bảng skewness_table_g1 bằng kable() với caption, tên cột, định dạng pipe và căn lề trái.

Nhận xét

Các biến acousticness (-0.033), danceability (-0.223), energy (0.112) và valence (-0.107) có độ xiên gần 0, cho thấy phân phối gần đối xứng quanh giá trị trung bình. Điều này nghĩa là phần lớn bài hát tập trung quanh mức nhịp điệu, năng lượng và cảm xúc trung bình, không có nhiều giá trị cực đoan.

Biến tempo (0.450) lệch phải nhẹ, tức phần lớn bài hát có nhịp độ vừa phải, nhưng có một số bài hát với nhịp độ cao kéo dài đuôi phân phối. Ngược lại, loudness (-1.052) lệch trái, cho thấy đa số bài hát có âm lượng thấp, chỉ một số ít bài hát có âm lượng cao tạo đuôi dài về phía giá trị nhỏ.

Các biến instrumentalness (1.631), liveness (2.154) và speechiness (4.048) lệch phải đáng kể, phản ánh rằng phần lớn bài hát có giá trị thấp về nhạc cụ, độ sống động và speechiness, trong khi số ít bài hát có giá trị cao kéo dài đuôi phải. Đây là các giá trị cực đoan cần lưu ý vì có thể ảnh hưởng đến trung bình và kết quả phân tích.

Tóm lại, các đặc trưng cơ bản về cảm xúc và nhịp điệu gần đối xứng, còn các đặc trưng về nhạc cụ, độ sống động và speechiness có phân bố lệch phải đáng kể, cho thấy sự đa dạng kỹ thuật âm nhạc trong bộ dữ liệu.

1.2.1.5.2. Đo lường độ nhọn

Độ nhọn phản ánh mức độ tập trung của dữ liệu quanh trung tâm, đồng thời cho biết độ “dày” hay “mỏng” của phần đuôi so với phân phối chuẩn. Trong R, hàm kurtosis() thường trả giá trị Excess Kurtosis (kurtosis trừ đi 3).

  • Khi Excess Kurtosis = 0, phân phối có độ nhọn tương đương phân phối chuẩn (Mesokurtic).

  • Khi Excess Kurtosis > 0, dữ liệu có đỉnh nhọn hơn và đuôi dày hơn chuẩn (Leptokurtic), nghĩa là khả năng xuất hiện giá trị ngoại lai (outlier) cao hơn.

  • Khi Excess Kurtosis < 0, phân phối bằng phẳng hơn, đuôi mỏng hơn so với chuẩn (Platykurtic), tức dữ liệu phân tán đều hơn quanh trung tâm.

1. vars_g1_shape <- c("danceability", "energy", "valence", "acousticness",
2.                    "instrumentalness", "liveness", "loudness", "speechiness", "tempo")
3. kurtosis_table_g1 <- sapply(spotify_group1[, ..vars_g1_shape],
4.                             kurtosis, na.rm = TRUE) %>%
5.   round(3) %>% as.data.frame() %>%
6.   rownames_to_column("Variable") %>%
7.   rename(Excess_Kurtosis = ".") %>%
8.   arrange(Variable)
9. kable(kurtosis_table_g1,
10.       caption = "Bảng 1.3.11: Độ nhọn (Excess Kurtosis = Kurtosis − 3) của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)",
11.       col.names = c("Tên Biến", "Độ nhọn (Kurtosis−3)"),
12.       format = "pipe", align = "l")
Bảng 1.3.11: Độ nhọn (Excess Kurtosis = Kurtosis − 3) của các biến định lượng (Nhóm biến Đặc trưng kỹ thuật âm nhạc)
Tên Biến Độ nhọn (Kurtosis−3)
acousticness -1.609
danceability -0.443
energy -1.100
instrumentalness 0.942
liveness 5.001
loudness 1.847
speechiness 17.000
tempo -0.078
valence -1.062

Giải thích kỹ thuật: 1-2. Xác định các biến định lượng trong Nhóm 1 (vars_g1_shape). 3-8. Tính độ nhọn (Excess Kurtosis) cho từng biến numeric, làm tròn 3 chữ số thập phân, chuyển sang dataframe, thêm cột tên biến và sắp xếp theo tên biến, lưu vào kurtosis_table_g1. 9-12. Xuất bảng kurtosis_table_g1 bằng kable() với caption, tên cột, định dạng pipe và căn lề trái.

Các đặc trưng kỹ thuật âm nhạc trong dataset thể hiện sự phân bố khác nhau về độ lệch và độ nhọn. Danceability có độ xiên nhẹ trái (Skewness = -0.223) và độ nhọn hơi bẹt (Kurtosis = -0.443), cho thấy các giá trị hơi tập trung xung quanh trung tâm nhưng không quá cực đoan. Energy gần đối xứng (Skewness = 0.112) nhưng có độ nhọn thấp (Kurtosis = -1.100), nghĩa là phân phối rộng hơn phân phối chuẩn, ít bị ảnh hưởng bởi giá trị cực đoan.

Các biến instrumentalness (Skewness = 1.631, Kurtosis = 0.942) và liveness (Skewness = 2.154, Kurtosis = 5.001) lệch phải đáng kể và có đuôi dài, phản ánh rằng hầu hết bài hát có giá trị thấp nhưng một số bài có giá trị cao, tạo ra outlier. Speechiness nổi bật với Skewness = 4.048 và Kurtosis = 17.000, cho thấy dữ liệu cực kỳ lệch phải với các giá trị cực đoan rất cao, là những outlier có ảnh hưởng lớn đến trung bình và độ nhọn của phân phối.

Các biến như loudness (Skewness = -1.052, Kurtosis = 1.847) lệch trái, nghĩa là nhiều bài hát có âm lượng gần trung bình nhưng một số giá trị cực thấp làm “kéo” trung bình xuống. Tempo và valence gần đối xứng với Skewness lần lượt là 0.450 và -0.107, độ nhọn tương đối bình thường, phản ánh sự phân bố đều hơn quanh trung tâm.

Nhìn chung, nhóm biến đặc trưng kỹ thuật âm nhạc cho thấy sự đa dạng về nhịp điệu, cảm xúc, độ nhạc cụ và độ “sôi động” của bài hát. Một số biến như speechiness, liveness, instrumentalness có outlier rõ rệt, cần lưu ý khi phân tích sâu hoặc xây dựng mô hình dự báo.

1.2.1.5.3. Kiểm định tính phân phối chuẩn

Kiểm định Shapiro-Wilk được sử dụng để xác định xem dữ liệu có tuân theo phân phối chuẩn hay không. Giả thuyết gốc (H0) của kiểm định là dữ liệu có phân phối chuẩn. Khi giá trị p > 0.05, H0 không bị bác bỏ, tức dữ liệu có xu hướng phân phối chuẩn; ngược lại, p ≤ 0.05 cho thấy dữ liệu không tuân theo phân phối chuẩn. Tuy nhiên, kiểm định này rất nhạy cảm với kích thước mẫu lớn: khi số quan sát vượt quá khoảng vài nghìn, kiểm định dễ phát hiện sự khác biệt rất nhỏ với phân phối chuẩn và thường báo dữ liệu không chuẩn ngay cả khi phân phối gần chuẩn. Do đó, với bộ dữ liệu lớn, nên thực hiện kiểm định trên một mẫu con nhỏ để có đánh giá sơ bộ hợp lý, đồng thời kết hợp quan sát đồ thị (histogram, Q-Q plot) để kiểm tra trực quan.

1. library(purrr)
2. library(dplyr)
3. library(knitr)
4. library(tibble)
5. library(stats) # Chứa shapiro.test
6. shapiro_on_sample <- function(x, max_n = 5000) {
7.   x <- na.omit(x)
8.   n_use <- min(length(x), max_n)
9.   if (n_use < 3) return(tibble(N = length(x), W = NA_real_, p_value = NA_real_))
10.   x_sample <- sample(x, n_use)
11.   res <- shapiro.test(x_sample)
12.   tibble(N = n_use, W = unname(res$statistic), p_value = unname(res$p.value))
13. }
14. vars_shapiro <- c("danceability","energy","valence","acousticness",
15.                   "instrumentalness","liveness","loudness","speechiness","tempo")
16. shapiro_df <- map_dfr(vars_shapiro, ~ {
17.   tmp <- spotify_group1[[.x]]
18.   out <- shapiro_on_sample(tmp)
19.   out$Variable <- .x
20.   out
21. }) %>% relocate(Variable)
22. shapiro_df <- shapiro_df %>%
23.   mutate(p_value_fmt = format.pval(p_value, digits = 3, eps = 0.001),
24.          Kết_luận = ifelse(!is.na(p_value) & p_value < 0.05,
25.                            "Bác bỏ phân phối chuẩn (p<0.05)",
26.                            "Không bác bỏ (p≥0.05)"))
27. kable(shapiro_df %>% select(Variable, N, W, p_value_fmt, Kết_luận),
28.       caption = "Bảng: Kiểm định Shapiro–Wilk cho các đặc trưng kỹ thuật âm nhạc (N≤5000)",
29.       col.names = c("Biến","Cỡ mẫu","W","P-value","Kết luận"),
30.       format = "pipe", align = "l")
Bảng: Kiểm định Shapiro–Wilk cho các đặc trưng kỹ thuật âm nhạc (N≤5000)
Biến Cỡ mẫu W P-value Kết luận
danceability 5000 0.9910439 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
energy 5000 0.9646551 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
valence 5000 0.9671218 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
acousticness 5000 0.8687693 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
instrumentalness 5000 0.5855677 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
liveness 5000 0.7491300 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
loudness 5000 0.9457929 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
speechiness 5000 0.4221000 <0.001 Bác bỏ phân phối chuẩn (p<0.05)
tempo 5000 0.9800432 <0.001 Bác bỏ phân phối chuẩn (p<0.05)

Giải thích kỹ thuật: 1-5. Nạp các thư viện purrr, dplyr, knitr, tibblestats để xử lý dữ liệu, xuất bảng và thực hiện kiểm định Shapiro–Wilk. 6-13. Định nghĩa hàm shapiro_on_sample() để:

  • Loại bỏ giá trị thiếu (NA) và lấy mẫu tối đa 5000 quan sát.
  • Nếu mẫu quá nhỏ (<3), trả về NA.
  • Thực hiện kiểm định Shapiro–Wilk trên mẫu và trả về tibble gồm cỡ mẫu, W và p-value. 14-15. Xác định các biến numeric cần kiểm định (vars_shapiro). 16-21. Áp dụng hàm Shapiro cho từng biến, lưu kết quả vào shapiro_df và thêm cột tên biến. 22-26. Thêm cột hiển thị p-value làm tròn và kết luận kiểm định: bác bỏ hoặc không bác bỏ giả thuyết phân phối chuẩn. 27-30. Xuất bảng kết quả kiểm định bằng kable() với caption, tên cột, định dạng pipe và căn lề trái.

Dựa trên kết quả kiểm định Shapiro–Wilk cho các biến định lượng nhóm đặc trưng kỹ thuật âm nhạc:

Các giá trị W đều thấp hơn 1, và p-value < 0.001 cho tất cả các biến, cho thấy rằng tất cả các biến đều không tuân theo phân phối chuẩn trên mẫu con 5000 quan sát.

Cụ thể:

  • Biến danceability (W = 0.993) và valence (W = 0.969) có giá trị W gần 1 hơn các biến khác, tức là phân phối của chúng tương đối gần với chuẩn, nhưng vẫn bị bác bỏ giả thuyết phân phối chuẩn do p-value rất nhỏ.

  • Các biến instrumentalness (W = 0.561), speechiness (W = 0.441) và liveness (W = 0.761) có giá trị W thấp hơn đáng kể, cho thấy phân phối bị lệch mạnh và có khả năng chứa nhiều giá trị ngoại lai (outlier).

  • Biến acousticness (W = 0.867), energy (W = 0.965), loudness (W = 0.945) và tempo (W = 0.979) cũng đều bị bác bỏ phân phối chuẩn, mặc dù mức độ lệch không mạnh bằng instrumentalness hay speechiness.

Nhận xét tổng thể:

Tất cả các đặc trưng kỹ thuật âm nhạc đều phân phối không chuẩn, điều này cần được lưu ý khi áp dụng các phân tích thống kê yêu cầu giả định phân phối chuẩn, như hồi quy tuyến tính cổ điển. Trong thực hành, bạn có thể cân nhắc biến đổi dữ liệu (log, sqrt, Box-Cox) hoặc sử dụng các phương pháp phi tham số.

1.2.1.6. Phân tích giá trị ngoại lai

Phân tích trước đó về độ xiên (Skewness) và độ nhọn (Excess Kurtosis) trong Mục 1.3.1.5 đã cho thấy một số biến trong Nhóm 1 có phân phối lệch hoặc nhọn bất thường, cho thấy sự hiện diện của các giá trị cực đoan (outliers). Để hiểu rõ hơn về mức độ và tần suất của các giá trị này, chúng tôi sẽ áp dụng phương pháp IQR (Interquartile Range) để xác định các outlier. Phân tích này sẽ tập trung vào các biến có độ lệch cao nhất, giúp nhận diện các bản ghi đặc biệt, đánh giá tính đa dạng của dữ liệu âm nhạc và đảm bảo các bước phân tích thống kê tiếp theo không bị ảnh hưởng bởi những giá trị ngoại lai.

1.2.1.6.1. Xác định ngưỡng ngoại lai bằng phương pháp IQR

Trong phương pháp IQR, các giá trị được xem là ngoại lai nếu chúng nằm ngoài khoảng từ Q1 − 1,5 × IQR đến Q3 + 1,5 × IQR, trong đó Q1 và Q3 lần lượt là phần tử thứ nhất và thứ ba của tứ phân vị, còn IQR là khoảng tứ phân vị (Q3 − Q1).

1. identify_outliers_iqr <- function(df, var) {
2.   x <- df[[var]]
3.   x <- na.omit(x) # loại NA
4.   Q1 <- quantile(x, 0.25)
5.   Q3 <- quantile(x, 0.75)
6.   IQR_val <- Q3 - Q1
7.   Lower_Bound <- Q1 - 1.5 * IQR_val
8.   Upper_Bound <- Q3 + 1.5 * IQR_val
9.   list(
10.     var_name = var,
11.     Q1 = Q1,
12.     Q3 = Q3,
13.     IQR = IQR_val,
14.     Lower_Bound = Lower_Bound,
15.     Upper_Bound = Upper_Bound
16.   )
17. }
18. vars_g1_outlier <- c("danceability", "energy", "valence", "acousticness",
19.                      "instrumentalness", "liveness", "loudness", "speechiness", "tempo")
20. outlier_results_list_g1 <- lapply(vars_g1_outlier, function(var) {
21.   identify_outliers_iqr(spotify_group1, var)
22. })
23. outlier_threshold_df_g1 <- do.call(rbind, lapply(outlier_results_list_g1, function(res) {
24.   data.frame(
25.     Variable = res$var_name,
26.     Q1 = round(res$Q1, 3), Q3 = round(res$Q3, 3), IQR = round(res$IQR, 3),
27.     Lower_Bound = round(res$Lower_Bound, 3), Upper_Bound = round(res$Upper_Bound, 3)
28.   )
29. }))
30. library(knitr)
31. kable(outlier_threshold_df_g1,
32.       caption = "Bảng 1.3.12: Ngưỡng xác định giá trị ngoại lai theo phương pháp IQR (Nhóm Đặc trưng kỹ thuật âm nhạc)",
33.       col.names = c("Tên Biến", "Q1", "Q3", "IQR", "Ngưỡng Dưới", "Ngưỡng Trên"),
34.       format = "pipe", align = "l")
Bảng 1.3.12: Ngưỡng xác định giá trị ngoại lai theo phương pháp IQR (Nhóm Đặc trưng kỹ thuật âm nhạc)
Tên Biến Q1 Q3 IQR Ngưỡng Dưới Ngưỡng Trên
25% danceability 0.415 0.668 0.253 0.036 1.047
25%1 energy 0.255 0.703 0.448 -0.417 1.375
25%2 valence 0.317 0.747 0.430 -0.328 1.392
25%3 acousticness 0.102 0.893 0.791 -1.084 2.080
25%4 instrumentalness 0.000 0.102 0.102 -0.153 0.255
25%5 liveness 0.099 0.261 0.162 -0.145 0.504
25%6 loudness -14.615 -7.183 7.432 -25.763 3.965
25%7 speechiness 0.035 0.076 0.041 -0.026 0.137
25%8 tempo 93.421 135.537 42.116 30.247 198.711

Giải thích kỹ thuật:

1-17. Định nghĩa hàm identify_outliers_iqr() để:

  • Lấy biến cần kiểm tra từ dataframe, loại bỏ giá trị NA.
  • Tính Q1, Q3, IQR, ngưỡng dưới và ngưỡng trên theo quy tắc IQR (1.5 × IQR).
  • Trả về danh sách các giá trị này kèm tên biến.

18-19. Xác định danh sách các biến numeric nhóm đặc trưng kỹ thuật âm nhạc cần kiểm tra ngoại lai.

20-22. Áp dụng hàm identify_outliers_iqr() cho từng biến và lưu kết quả vào danh sách outlier_results_list_g1.

23-29. Chuyển danh sách kết quả thành dataframe outlier_threshold_df_g1 với các cột: Variable, Q1, Q3, IQR, Lower_Bound, Upper_Bound; làm tròn 3 chữ số thập phân.

30-34. Dùng kable() xuất bảng ngưỡng ngoại lai, đặt caption, tên cột và định dạng pipe, căn lề trái.

Các kết quả bảng ngưỡng ngoại lai cho Nhóm biến đặc trưng kỹ thuật âm nhạc cho thấy sự phân bố dữ liệu và mức độ tiềm ẩn outlier ở từng biến:

Biến danceability có Q1 = 0.415, Q3 = 0.668, IQR = 0.253, với ngưỡng dưới 0.036 và ngưỡng trên 1.047. Điều này cho thấy dữ liệu tập trung trong khoảng trung bình, nhưng vẫn có thể có một số quan sát rất thấp hoặc cao hơn 1.047 được xem là ngoại lai.

Biến energy có IQR lớn hơn (0.448) và ngưỡng dưới -0.417, ngưỡng trên 1.375, phản ánh dữ liệu có phạm vi rộng hơn, khả năng xuất hiện các giá trị cực thấp hoặc cực cao cũng cao hơn so với danceability.

Valence có ngưỡng từ -0.328 đến 1.392, cho thấy phân bố cũng khá rộng, một số giá trị cực đoan có thể xuất hiện, nhưng tập trung vẫn chủ yếu trong khoảng trung tâm.

Biến acousticness có IQR = 0.791 và ngưỡng trên 2.080, ngưỡng dưới -1.084. Phạm vi rộng này phản ánh dữ liệu rất phân tán, đồng thời khả năng xuất hiện outlier cao, đặc biệt là các giá trị cực lớn.

Instrumentalness có IQR nhỏ (0.102), ngưỡng dưới -0.153 và ngưỡng trên 0.255, cho thấy hầu hết dữ liệu tập trung rất thấp, các giá trị vượt quá 0.255 có thể được coi là ngoại lai.

Biến liveness có ngưỡng trên 0.504, ngưỡng dưới -0.145, với IQR 0.162, dữ liệu tập trung chủ yếu quanh Q1-Q3 nhưng vẫn có khả năng xuất hiện giá trị cao bất thường.

Loudness có ngưỡng dưới -25.763 và ngưỡng trên 3.965, IQR = 7.432. Đây là biến có phạm vi rộng và tiềm năng xuất hiện outlier lớn, do giá trị âm sâu bất thường có thể xuất hiện.

Speechiness với IQR = 0.041, ngưỡng trên 0.137, ngưỡng dưới -0.026, cho thấy phần lớn dữ liệu tập trung thấp, nhưng các giá trị vượt ngưỡng trên là cực đoan rõ rệt, phù hợp với nhận xét trước đó về outlier cao trong biến này.

Cuối cùng, tempo có IQR = 42.116, ngưỡng từ 30.247 đến 198.711, phản ánh sự phân tán đáng kể, với khả năng xuất hiện giá trị cực thấp hoặc cực cao khá cao so với phần còn lại của dữ liệu.

Nhìn chung, các biến như speechiness, loudness, acousticness có khả năng xuất hiện các giá trị cực đoan cao nhất, điều này phù hợp với kết quả phân tích skewness và kurtosis trước đó. Các biến còn lại vẫn có một số outlier nhưng ở mức vừa phải.

1.2.1.6.2. Định lượng tỷ lệ phần trăm ngoại lai

1. outlier_percentage_df_g1 <- lapply(outlier_results_list_g1, function(res) {
2.   x <- spotify_group1[[res$var_name]] %>% na.omit()
3.   Num_Outliers_Low <- sum(x < res$Lower_Bound)
4.   Num_Outliers_High <- sum(x > res$Upper_Bound)
5.   Total_Outliers <- Num_Outliers_Low + Num_Outliers_High
6.   Total_NonNA <- length(x)
7.   Percentage_Outliers <- round((Total_Outliers / Total_NonNA) * 100, 2)
8.   
9.   data.frame(
10.     Variable = res$var_name,
11.     Num_Outliers_Low = Num_Outliers_Low,
12.     Num_Outliers_High = Num_Outliers_High,
13.     Total_Outliers = Total_Outliers,
14.     Total_NonNA = Total_NonNA,
15.     Percentage_Outliers = Percentage_Outliers
16.   )
17. }) %>% do.call(rbind, .)
18. kable(outlier_percentage_df_g1,
19.       caption = "Bảng: Số lượng và tỷ lệ phần trăm giá trị ngoại lai (Nhóm Đặc trưng kỹ thuật âm nhạc)",
20.       col.names = c("Tên Biến", "Outlier Dưới", "Outlier Trên", "Tổng Outlier", "Tổng QS (Không NA)", "Tỷ lệ Outlier (%)"),
21.       format = "pipe", align = "l")
Bảng: Số lượng và tỷ lệ phần trăm giá trị ngoại lai (Nhóm Đặc trưng kỹ thuật âm nhạc)
Tên Biến Outlier Dưới Outlier Trên Tổng Outlier Tổng QS (Không NA) Tỷ lệ Outlier (%)
danceability 143 0 143 170653 0.08
energy 0 0 0 170653 0.00
valence 0 0 0 170653 0.00
acousticness 0 0 0 170653 0.00
instrumentalness 0 36105 36105 170653 21.16
liveness 0 11808 11808 170653 6.92
loudness 3501 0 3501 170653 2.05
speechiness 0 23937 23937 170653 14.03
tempo 143 1502 1645 170653 0.96

Giải thích kỹ thuật: 1-7. Với mỗi biến trong nhóm đặc trưng kỹ thuật âm nhạc:

  • Lọc bỏ NA.
  • Đếm số giá trị ngoại lai dưới ngưỡng (Num_Outliers_Low) và trên ngưỡng (Num_Outliers_High).
  • Tính tổng ngoại lai (Total_Outliers) và tổng quan sát hợp lệ (Total_NonNA).
  • Tính tỷ lệ phần trăm ngoại lai (Percentage_Outliers).

8-17. Lưu các giá trị trên thành dataframe cho từng biến và ghép tất cả thành outlier_percentage_df_g1.

18-21. Dùng kable() xuất bảng hiển thị số lượng và tỷ lệ phần trăm ngoại lai, đặt caption, tên cột và căn lề trái.

Dựa trên bảng số liệu về giá trị ngoại lai (outlier) trong Nhóm biến Đặc trưng kỹ thuật âm nhạc, có thể nhận xét như sau:

Biến instrumentalness là biến có tỷ lệ outlier cao nhất, với 36.105 giá trị ngoại lai, chiếm 21.16% tổng số quan sát. Tất cả outlier đều nằm trên ngưỡng trên, cho thấy sự tồn tại của nhiều bản ghi có instrumentalness cực cao so với phần còn lại của dữ liệu. Điều này có thể phản ánh rằng một số bản nhạc hoàn toàn mang tính instrumental, khác biệt rõ rệt với phần lớn các bản nhạc còn lại.

Biến speechiness cũng có tỷ lệ outlier đáng chú ý, với 23.937 giá trị ngoại lai, chiếm 14.03% tổng quan sát, tất cả nằm trên ngưỡng trên. Điều này chỉ ra rằng một số bản nhạc có mức độ “nói” (speechiness) cực cao, khác biệt với phần lớn các bản nhạc.

Biến liveness có 11.808 outlier, chiếm 6.92%, cũng chủ yếu nằm trên ngưỡng trên, cho thấy có một số bản nhạc có tính “sống” (liveness) rất cao, có thể là bản thu trực tiếp hoặc có âm thanh khán giả rõ rệt.

Các biến như loudness có 3.501 outlier dưới, chiếm 2.05%, cho thấy một số bản nhạc khá “nhẹ” so với phần lớn dữ liệu. Trong khi đó, các biến danceability, tempo có một số ít outlier (0.08% và 0.96%), còn các biến energy, valence, acousticness hầu như không có outlier.

Nhìn chung, instrumentalness, speechiness và liveness là những biến có mức độ biến động và sự tồn tại outlier đáng kể nhất trong nhóm, phản ánh sự khác biệt đặc trưng của một số bản nhạc so với phần còn lại của tập dữ liệu. Các biến khác như energy, valence, acousticness tương đối ổn định, gần như không có outlier.

1.2.1.6.3. Phân tích giá trị ngoại lai của biến instrumentalness

1. instr_outlier_info <- identify_outliers_iqr(spotify_group1, "instrumentalness")
2. instr_lower_bound <- instr_outlier_info$Lower_Bound
3. instr_upper_bound <- instr_outlier_info$Upper_Bound
4. cat(paste("Ngưỡng Outlier cho instrumentalness: Dưới", round(instr_lower_bound, 3),
5.           "và Trên", round(instr_upper_bound, 3), "\n"))
## Ngưỡng Outlier cho instrumentalness: Dưới -0.153 và Trên 0.255
1. instr_outliers <- spotify_group1 %>%
2.   filter(instrumentalness < instr_lower_bound | instrumentalness > instr_upper_bound)
3. outlier_by_year <- instr_outliers %>%
4.   count(release_year, name = "SoLuongOutlier") %>%                 # Đếm số outlier theo năm
5.   mutate(TyLeTrongOutlier = round((SoLuongOutlier / sum(SoLuongOutlier)) * 100, 2)) %>%  # Tính tỷ lệ %
6.   arrange(release_year)  # Sắp xếp theo năm tăng dần
7. kable(outlier_by_year,
8.       caption = "Bảng: Phân bổ Outlier của Instrumentalness theo Năm phát hành",
9.       col.names = c("Năm Phát Hành", "Số lượng Outlier", "Tỷ lệ trong Tổng Outlier (%)"),
10.       format = "pipe", align = "l")
Bảng: Phân bổ Outlier của Instrumentalness theo Năm phát hành
Năm Phát Hành Số lượng Outlier Tỷ lệ trong Tổng Outlier (%)
1921 64 0.18
1922 37 0.10
1923 82 0.23
1924 155 0.43
1925 138 0.38
1926 547 1.52
1927 309 0.86
1928 748 2.07
1929 280 0.78
1930 872 2.42
1931 263 0.73
1932 151 0.42
1933 165 0.46
1934 201 0.56
1935 440 1.22
1936 358 0.99
1937 269 0.75
1938 309 0.86
1939 364 1.01
1940 806 2.23
1941 514 1.42
1942 825 2.29
1943 367 1.02
1944 419 1.16
1945 698 1.93
1946 1019 2.82
1947 737 2.04
1948 860 2.38
1949 747 2.07
1950 612 1.70
1951 755 2.09
1952 704 1.95
1953 767 2.12
1954 735 2.04
1955 551 1.53
1956 580 1.61
1957 472 1.31
1958 466 1.29
1959 465 1.29
1960 430 1.19
1961 633 1.75
1962 411 1.14
1963 461 1.28
1964 387 1.07
1965 282 0.78
1966 290 0.80
1967 312 0.86
1968 306 0.85
1969 419 1.16
1970 343 0.95
1971 328 0.91
1972 296 0.82
1973 259 0.72
1974 299 0.83
1975 296 0.82
1976 312 0.86
1977 333 0.92
1978 260 0.72
1979 305 0.84
1980 330 0.91
1981 371 1.03
1982 259 0.72
1983 385 1.07
1984 369 1.02
1985 294 0.81
1986 289 0.80
1987 267 0.74
1988 298 0.83
1989 298 0.83
1990 332 0.92
1991 291 0.81
1992 265 0.73
1993 309 0.86
1994 301 0.83
1995 325 0.90
1996 249 0.69
1997 276 0.76
1998 283 0.78
1999 239 0.66
2000 255 0.71
2001 268 0.74
2002 222 0.61
2003 202 0.56
2004 196 0.54
2005 227 0.63
2006 195 0.54
2007 183 0.51
2008 156 0.43
2009 184 0.51
2010 207 0.57
2011 259 0.72
2012 210 0.58
2013 245 0.68
2014 195 0.54
2015 261 0.72
2016 204 0.57
2017 229 0.63
2018 143 0.40
2019 179 0.50
2020 42 0.12

Giải thích kỹ thuật: 1-5. Xác định ngưỡng giá trị ngoại lai (IQR) cho biến instrumentalness:

  • Dòng 1: Gọi hàm identify_outliers_iqr() để tính Q1, Q3, IQR, ngưỡng dưới và ngưỡng trên.
  • Dòng 2-3: Lấy giá trị ngưỡng dưới (Lower_Bound) và ngưỡng trên (Upper_Bound).
  • Dòng 4-5: In ra thông báo các ngưỡng ngoại lai, làm tròn 3 chữ số thập phân.

Giải thích kỹ thuật: 1-10. Phân tích giá trị ngoại lai của biến instrumentalness theo năm:

  • Dòng 1-2: Lọc các bản ghi ngoại lai dựa trên ngưỡng dưới và trên đã xác định.
  • Dòng 3-6: Tính số lượng ngoại lai theo từng năm, đồng thời tính tỷ lệ phần trăm trong tổng số ngoại lai, rồi sắp xếp theo năm.
  • Dòng 7-10: Hiển thị kết quả dưới dạng bảng đẹp với tiêu đề, tên cột và căn lề chuẩn.

Biến instrumentalness (mức độ nhạc cụ, đo lường khả năng một bản nhạc không có lời hát) trong bộ dữ liệu có một số giá trị ngoại lai, với ngưỡng dưới là -0.153 và ngưỡng trên là 0.255. Tổng cộng có 7,131 outlier, chiếm 19.72% tổng số quan sát hợp lệ.

Xét theo năm phát hành, outlier phân bố không đều: các năm 1946, 1948, 1953 và các năm đầu thập niên 1930-1950 có số lượng outlier cao, trong khi các năm gần đây (2018-2020) có rất ít outlier. Điều này có thể phản ánh xu hướng thay đổi đặc trưng instrumentalness của các bản nhạc qua các thập kỷ.

1.2.1.6.4. Đánh giá ảnh hưởng của ngoại lai lên instrumentalness

1. mean_instr_orig <- mean(spotify_group1$instrumentalness, na.rm = TRUE)
2. sd_instr_orig   <- sd(spotify_group1$instrumentalness, na.rm = TRUE)
3. instr_no_outliers <- spotify_group1 %>%
4.   filter(!is.na(instrumentalness) & 
5.          instrumentalness >= instr_lower_bound & 
6.          instrumentalness <= instr_upper_bound)
7. mean_instr_no_out <- mean(instr_no_outliers$instrumentalness, na.rm = TRUE)
8. sd_instr_no_out   <- sd(instr_no_outliers$instrumentalness, na.rm = TRUE)
9. impact_instr_df <- data.frame(
10.   Metric = c("Mean (Trung bình)", "SD (Độ lệch chuẩn)"),
11.   Original = round(c(mean_instr_orig, sd_instr_orig), 3),
12.   Without_Outliers = round(c(mean_instr_no_out, sd_instr_no_out), 3)
13. )
14. kable(impact_instr_df,
15.       caption = "Bảng: Ảnh hưởng của Outlier lên Mean và SD biến Instrumentalness",
16.       col.names = c("Chỉ số Thống kê", "Giá trị Gốc", "Giá trị Sau khi Loại bỏ Outlier"),
17.       format = "pipe", align = "l")
Bảng: Ảnh hưởng của Outlier lên Mean và SD biến Instrumentalness
Chỉ số Thống kê Giá trị Gốc Giá trị Sau khi Loại bỏ Outlier
Mean (Trung bình) 0.167 0.013
SD (Độ lệch chuẩn) 0.313 0.040

Giải thích kỹ thuật:

1-17. So sánh ảnh hưởng của giá trị ngoại lai lên meanSD của biến instrumentalness:

  • Dòng 1-2: Tính trung bình và độ lệch chuẩn gốc, bao gồm tất cả giá trị.
  • Dòng 3-6: Lọc các quan sát không phải ngoại lai.
  • Dòng 7-8: Tính trung bình và độ lệch chuẩn sau khi loại bỏ ngoại lai.
  • Dòng 9-13: Tạo bảng so sánh các chỉ số trước và sau khi loại bỏ ngoại lai.
  • Dòng 14-17: Hiển thị bảng đẹp với caption, tên cột và căn lề chuẩn.

Nhận xét :

Mean (Trung bình) giảm mạnh từ 0.167 xuống 0.013 sau khi loại bỏ outlier, cho thấy các giá trị ngoại lai có ảnh hưởng lớn đến mức trung bình của instrumentalness.

SD (Độ lệch chuẩn) giảm từ 0.313 xuống 0.040, chứng tỏ các outlier cực đoan làm tăng đáng kể độ biến thiên của dữ liệu.

=> instrumentalness có nhiều giá trị ngoại lai ảnh hưởng mạnh đến các chỉ số thống kê cơ bản, nên việc loại bỏ outlier là cần thiết trước khi phân tích sâu hơn hoặc xây dựng mô hình.

1.2.2. Phân tích tương quan và mối quan hệ của các đặc trưng kỹ thuật âm nhạc

Sau khi nắm rõ phân bố của từng biến, mục này nghiên cứu mối quan hệ tuyến tính giữa các đặc trưng kỹ thuật âm nhạc định lượng. Hệ số tương quan Pearson được sử dụng để đánh giá mức độ và chiều hướng liên kết, từ đó xác định các xu hướng hoặc sự tương tác giữa các thuộc tính như mức độ nhạc cụ (instrumentalness), cảm xúc âm nhạc (valence), năng lượng (energy), độ sôi động (danceability) hay độ vang âm (loudness).

1.2.2.1 Ma trận tương quan

1. cor_vars_instr <- c("danceability", "energy", "valence", "acousticness", 
2.                     "instrumentalness", "liveness", "loudness", "speechiness", 
3.                     "tempo", "energy_dance_ratio")
4. cor_data_instr <- spotify_group1 %>%
5.   select(all_of(cor_vars_instr)) %>%
6.   na.omit()  # loại bỏ NA
7. str(cor_data_instr)
## Classes 'data.table' and 'data.frame':   170644 obs. of  10 variables:
##  $ danceability      : num  0.279 0.819 0.328 0.275 0.418 0.697 0.518 0.389 0.485 0.684 ...
##  $ energy            : num  0.211 0.341 0.166 0.309 0.193 0.346 0.203 0.088 0.13 0.257 ...
##  $ valence           : num  0.0594 0.963 0.0394 0.165 0.253 0.196 0.406 0.0731 0.721 0.771 ...
##  $ acousticness      : num  0.982 0.732 0.961 0.967 0.957 0.579 0.996 0.993 0.996 0.982 ...
##  $ instrumentalness  : num  8.78e-01 0.00 9.13e-01 2.77e-05 1.68e-06 1.68e-01 0.00 5.27e-01 1.51e-01 0.00 ...
##  $ liveness          : num  0.665 0.16 0.101 0.381 0.229 0.13 0.115 0.363 0.104 0.504 ...
##  $ loudness          : num  -20.1 -12.44 -14.85 -9.32 -10.1 ...
##  $ speechiness       : num  0.0366 0.415 0.0339 0.0354 0.038 0.07 0.0615 0.0456 0.0483 0.399 ...
##  $ tempo             : num  81 60.9 110.3 100.1 101.7 ...
##  $ energy_dance_ratio: num  0.756 0.416 0.506 1.124 0.462 ...
##  - attr(*, ".internal.selfref")=<externalptr>
1. cor_matrix_instr <- cor(cor_data_instr, method = "pearson")
2. cor_matrix_instr
##                    danceability      energy      valence acousticness
## danceability        1.000000000  0.22175035  0.558819907  -0.26714512
## energy              0.221750349  1.00000000  0.353752989  -0.74961939
## valence             0.558819907  0.35375299  1.000000000  -0.18427075
## acousticness       -0.267145117 -0.74961939 -0.184270752   1.00000000
## instrumentalness   -0.278219613 -0.28117827 -0.198580019   0.32979978
## liveness           -0.100410796  0.12609525  0.003708148  -0.02456746
## loudness            0.284300165  0.78311890  0.313242446  -0.56340157
## speechiness         0.235454197 -0.07061908  0.046322306  -0.04402539
## tempo               0.001188986  0.25062072  0.171369152  -0.20747670
## energy_dance_ratio          NaN         NaN          NaN          NaN
##                    instrumentalness     liveness    loudness speechiness
## danceability            -0.27821961 -0.100410796  0.28430016  0.23545420
## energy                  -0.28117827  0.126095246  0.78311890 -0.07061908
## valence                 -0.19858002  0.003708148  0.31324245  0.04632231
## acousticness             0.32979978 -0.024567464 -0.56340157 -0.04402539
## instrumentalness         1.00000000 -0.047227815 -0.40963810 -0.12171891
## liveness                -0.04722781  1.000000000  0.05600204  0.13463610
## loudness                -0.40963810  0.056002044  1.00000000 -0.13983643
## speechiness             -0.12171891  0.134636102 -0.13983643  1.00000000
## tempo                   -0.10550941  0.007480293  0.20854378 -0.01164909
## energy_dance_ratio              NaN          NaN         NaN         NaN
##                           tempo energy_dance_ratio
## danceability        0.001188986                NaN
## energy              0.250620720                NaN
## valence             0.171369152                NaN
## acousticness       -0.207476701                NaN
## instrumentalness   -0.105509410                NaN
## liveness            0.007480293                NaN
## loudness            0.208543784                NaN
## speechiness        -0.011649091                NaN
## tempo               1.000000000                NaN
## energy_dance_ratio          NaN                  1

Giải thích kỹ thuật: 1-7. Chuẩn bị dữ liệu để phân tích tương quan nhóm biến kỹ thuật âm nhạc:

  • Dòng 1-3: Xác định các biến số cần tính hệ số tương quan.
  • Dòng 4-6: Lấy các biến đó từ spotify_group1 và loại bỏ các giá trị thiếu (NA).
  • Dòng 7: Kiểm tra cấu trúc dữ liệu sau khi lọc (số quan sát, số biến, kiểu dữ liệu).

Giải thích kỹ thuật: 1-2. Tính ma trận tương quan Pearson cho các biến kỹ thuật âm nhạc:

  • Dòng 1: Dùng hàm cor() với phương pháp Pearson trên dữ liệu đã loại bỏ NA.
  • Dòng 2: Hiển thị ma trận kết quả, thể hiện mức độ tương quan tuyến tính giữa các biến.

1.2.2.2 Xác định các cặp tương quan mạnh nhất

1. library(reshape2)
2. library(dplyr)
3. library(knitr)
4. melted_cor_instr <- melt(cor_matrix_instr, na.rm = TRUE)
5. melted_cor_unique_instr <- melted_cor_instr %>%
6.   filter(as.character(Var1) < as.character(Var2))
7. strongest_cor_instr <- melted_cor_unique_instr %>%
8.   arrange(desc(abs(value)))
9. top_5_cor_instr <- head(strongest_cor_instr, 5) %>%
10.                     mutate(value = round(value, 3))
11. kable(top_5_cor_instr,
12.       caption = "Bảng: Top 5 cặp tương quan tuyến tính mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc",
13.       col.names = c("Biến 1", "Biến 2", "Hệ số Tương quan (r)"),
14.       format = "pipe", align = "l")
Bảng: Top 5 cặp tương quan tuyến tính mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc
Biến 1 Biến 2 Hệ số Tương quan (r)
energy loudness 0.783
acousticness energy -0.750
acousticness loudness -0.563
danceability valence 0.559
instrumentalness loudness -0.410
1. top_3_pos_cor_instr <- strongest_cor_instr %>%
2.                         filter(value > 0 & value < 1) %>%
3.                         head(3) %>%
4.                         mutate(value = round(value, 3))
5. kable(top_3_pos_cor_instr,
6.       caption = "Bảng: Top 3 cặp tương quan dương mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc",
7.       col.names = c("Biến 1", "Biến 2", "Hệ số Tương quan (r)"),
8.       format = "pipe", align = "l")
Bảng: Top 3 cặp tương quan dương mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc
Biến 1 Biến 2 Hệ số Tương quan (r)
energy loudness 0.783
danceability valence 0.559
energy valence 0.354
1. top_3_neg_cor_instr <- strongest_cor_instr %>%
2.                         filter(value < 0) %>%
3.                         arrange(value) %>%
4.                         head(3) %>%
5.                         mutate(value = round(value, 3))
6. if(nrow(top_3_neg_cor_instr) > 0) {
7.   kable(top_3_neg_cor_instr,
8.         caption = "Bảng: Top 3 cặp tương quan âm mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc",
9.         col.names = c("Biến 1", "Biến 2", "Hệ số Tương quan (r)"),
10.         format = "pipe", align = "l")
11. }
Bảng: Top 3 cặp tương quan âm mạnh nhất của Instrumentalness với các đặc trưng kỹ thuật âm nhạc
Biến 1 Biến 2 Hệ số Tương quan (r)
acousticness energy -0.750
acousticness loudness -0.563
instrumentalness loudness -0.410

Giải thích kỹ thuật: 1-14. Xác định 5 cặp biến có tương quan mạnh nhất trong nhóm đặc trưng kỹ thuật âm nhạc:

  • Dòng 1-3: Nạp thư viện cần thiết (reshape2 để melt ma trận, dplyr thao tác dữ liệu, knitr xuất bảng).
  • Dòng 4: Chuyển ma trận tương quan thành dạng “dài” để dễ xử lý.
  • Dòng 5-6: Lọc các cặp biến duy nhất (tránh lặp lại và loại bỏ đường chéo).
  • Dòng 7-8: Sắp xếp theo giá trị tuyệt đối của hệ số tương quan để tìm cặp mạnh nhất.
  • Dòng 9-10: Lấy 5 cặp tương quan mạnh nhất và làm tròn giá trị.
  • Dòng 11-14: Hiển thị bảng với tên biến và hệ số tương quan (r).

Giải thích kỹ thuật: 1-8. Xác định 3 cặp tương quan dương mạnh nhất trong nhóm đặc trưng kỹ thuật âm nhạc:

  • Dòng 1-4: Lọc các cặp tương quan dương (0 < r < 1) từ danh sách cặp biến đã sắp xếp strongest_cor_instr, lấy 3 cặp mạnh nhất và làm tròn hệ số tương quan.
  • Dòng 5-8: Hiển thị bảng với tên hai biến và hệ số tương quan (r) bằng kable.

Giải thích kỹ thuật: 1-11. Xác định 3 cặp tương quan âm mạnh nhất trong nhóm đặc trưng kỹ thuật âm nhạc:

  • Dòng 1-5: Lọc các cặp tương quan âm (r < 0) từ strongest_cor_instr, sắp xếp theo giá trị tăng dần, lấy 3 cặp mạnh nhất và làm tròn hệ số tương quan.
  • Dòng 6-11: Nếu tồn tại ít nhất 1 cặp, hiển thị bảng với tên hai biến và hệ số tương quan bằng kable.

1.2.2.3 Kiểm định ý nghĩa thống kê của các tương quan chính

1. library(purrr)
2. library(broom)
3. library(dplyr)
4. library(knitr)
5. pairs_to_test_instr <- list(
6.   c("instrumentalness", "danceability"),
7.   c("instrumentalness", "energy"),
8.   c("instrumentalness", "loudness"),
9.   c("instrumentalness", "valence"),
10.   c("instrumentalness", "tempo")
11. )
12. cor_test_results_instr <- map_dfr(pairs_to_test_instr, function(pair) {
13.   var1 <- pair[1]
14.   var2 <- pair[2]
15.   pair_label <- paste(var1, "vs", var2)
16. 
17.   
18.   pair_data <- spotify_group1 %>%
19.     select(all_of(c(var1, var2))) %>%
20.     filter(across(everything(), ~ !is.na(.) & !is.infinite(.))) %>%
21.     mutate(across(everything(), as.numeric))
22. 
23.   if(nrow(pair_data) >= 3) {
24.     test_result <- tryCatch({
25.       cor.test(pair_data[[var1]], pair_data[[var2]], method = "pearson") %>%
26.         tidy() %>%
27.         select(Correlation = estimate, P_Value = p.value, DF = parameter) %>%
28.         mutate(across(where(is.numeric), ~ round(.x, 3)))
29.     }, error = function(e) {
30.       tibble(Correlation = NA_real_, P_Value = NA_real_, DF = NA_integer_)
31.     })
32.   } else {
33.     test_result <- tibble(Correlation = NA_real_, P_Value = NA_real_, DF = NA_integer_)
34.   }
35. 
36.   test_result %>% add_column(Pair = pair_label, .before = 1)
37. })
38. cor_test_final_instr <- cor_test_results_instr %>%
39.   mutate(P_Value_Formatted = format.pval(P_Value, digits = 3, eps = 0.001),
40.          Significance = case_when(
41.            is.na(P_Value) ~ "Lỗi/NA",
42.            P_Value < 0.05 ~ "Có ý nghĩa (p < 0.05)",
43.            TRUE ~ "Không ý nghĩa (p >= 0.05)"
44.          ))
45. kable(cor_test_final_instr %>% select(Pair, Correlation, P_Value_Formatted, Significance, DF),
46.       caption = "Bảng: Kết quả kiểm định ý nghĩa thống kê cho các tương quan Instrumentalness",
47.       col.names = c("Cặp Biến", "Hệ số r", "P-value", "Kết luận", "Bậc tự do (df)"),
48.       format = "pipe", align = "l")
Bảng: Kết quả kiểm định ý nghĩa thống kê cho các tương quan Instrumentalness
Cặp Biến Hệ số r P-value Kết luận Bậc tự do (df)
instrumentalness vs danceability -0.278 <0.001 Có ý nghĩa (p < 0.05) 170651
instrumentalness vs energy -0.281 <0.001 Có ý nghĩa (p < 0.05) 170651
instrumentalness vs loudness -0.409 <0.001 Có ý nghĩa (p < 0.05) 170651
instrumentalness vs valence -0.199 <0.001 Có ý nghĩa (p < 0.05) 170651
instrumentalness vs tempo -0.105 <0.001 Có ý nghĩa (p < 0.05) 170651

Giải thích kĩ thuật:

1-48. Thực hiện kiểm định ý nghĩa thống kê (Pearson correlation test) cho các cặp biến liên quan đến instrumentalness:

  • Dòng 5-11: Xác định danh sách các cặp biến cần kiểm định (instrumentalness vs danceability, energy, loudness, valence, tempo).

  • Dòng 12-37: Với mỗi cặp:

    • Lấy dữ liệu không NA và không vô hạn, chuyển sang dạng số.
    • Nếu đủ quan sát (≥3), thực hiện kiểm định cor.test theo Pearson.
    • Bắt lỗi để tránh sự cố khi dữ liệu không phù hợp.
    • Kết quả lưu trữ bao gồm cặp biến, hệ số tương quan, p-value và bậc tự do.
  • Dòng 38-44: Làm tròn số liệu, định dạng p-value và phân loại có ý nghĩa hoặc không ý nghĩa dựa trên ngưỡng 0.05.

  • Dòng 45-48: Hiển thị bảng kết quả kiểm định bằng kable.

Mục 1.3.2. phân tích mối quan hệ giữa các đặc trưng kỹ thuật âm nhạc để làm rõ cách các thuộc tính tương tác và ảnh hưởng lẫn nhau. Việc sử dụng hệ số tương quan Pearson giúp xác định cường độ và chiều hướng liên kết giữa các biến numeric, từ đó nhận diện các quy luật hay xu hướng âm nhạc chung.

Các cặp tương quan dương mạnh nhất phản ánh các thuộc tính thường đi kèm nhau. Cụ thể, energy và loudness có mối tương quan dương rất mạnh (r = 0.783), cho thấy các bài hát năng lượng cao thường có âm lượng lớn. Danceability và valence cũng có tương quan dương đáng kể (r = 0.559), điều này nhấn mạnh rằng những bài hát dễ nhảy thường mang cảm xúc tích cực. Ngoài ra, energy và valence cũng có tương quan vừa phải (r = 0.354), gợi ý rằng các bài hát năng động thường có cảm giác vui vẻ hoặc lạc quan hơn.

Các tương quan âm mạnh chỉ ra các thuộc tính có xu hướng đối lập. Acousticness và energy có tương quan âm cao (-0.750), tương tự acousticness với loudness (-0.563), chứng tỏ các bài hát mang tính acoustic thường ít năng lượng và âm lượng vừa phải. Instrumentalness cũng có tương quan âm với loudness (-0.410), energy (-0.281) và danceability (-0.278), phản ánh nhạc nhiều nhạc cụ thường nhịp độ nhẹ nhàng và ít sôi động. Những mối quan hệ âm này giúp phân biệt các nhóm nhạc cụ, phong cách acoustic và nhạc điện tử hoặc nhạc sôi động.

Các tương quan yếu hơn vẫn có ý nghĩa thống kê, ví dụ: tempo gần như không liên quan nhiều với hầu hết các biến khác, liveness và speechiness có tương quan nhẹ với energy, loudness và danceability. Điều này gợi ý rằng các đặc trưng về độ sống động của buổi biểu diễn và mức độ lời nói không ảnh hưởng mạnh đến nhịp điệu hay năng lượng tổng thể của bài hát.

Kiểm định ý nghĩa thống kê cho thấy tất cả các cặp tương quan chính đều có p-value < 0.001, cho thấy các mối quan hệ này là đáng tin cậy và không xảy ra do ngẫu nhiên. Nhờ đó, các kết luận về mối tương quan âm-dương, mức độ mạnh-yếu của từng cặp biến có cơ sở thống kê vững chắc.

Nhìn chung, kết quả cho thấy các đặc trưng kỹ thuật âm nhạc không hoạt động độc lập mà thường tương tác lẫn nhau: năng lượng, âm lượng, valence và danceability có xu hướng đi cùng nhau, trong khi acousticness và instrumentalness thường đi ngược lại với các yếu tố sôi động và nhịp điệu mạnh. Nhận xét này quan trọng cho việc phân loại bài hát, mô hình đề xuất nhạc, hoặc phân tích hành vi nghe nhạc dựa trên đặc trưng kỹ thuật.

Các thông tin chi tiết về các cặp tương quan dương/âm mạnh nhất, cũng như kiểm định ý nghĩa, giúp minh họa rõ ràng rằng không chỉ một vài biến quyết định đặc trưng âm nhạc mà là sự kết hợp của nhiều thuộc tính. Việc nắm được mối quan hệ giữa các biến này hỗ trợ cả việc phân tích dữ liệu âm nhạc lẫn ứng dụng thực tiễn trong hệ thống gợi ý, playlist tự động, hoặc nghiên cứu hành vi người nghe.

1.2.3. Phân tổ biến định lượng theo biến định tính (mục thêm để đúng yêu cầu thầy)

1.2.3.1. Biến valence theo mode

1. valence_mode_summary <- data %>%
2.   group_by(mode) %>%
3.   summarise(
4.     Mean = mean(valence, na.rm = TRUE),
5.     Min = min(valence, na.rm = TRUE),
6.     Q1 = quantile(valence, 0.25, na.rm = TRUE),
7.     Median = median(valence, na.rm = TRUE),
8.     Q3 = quantile(valence, 0.75, na.rm = TRUE),
9.     Max = max(valence, na.rm = TRUE)
10.   )
11. kable(valence_mode_summary, digits = 3, caption = "Thống kê valence theo mode")
Thống kê valence theo mode
mode Mean Min Q1 Median Q3 Max
Minor 0.522 0 0.314 0.538 0.735 1
Major 0.531 0 0.318 0.540 0.753 1

Giải thích kĩ thuật:

1-11. Thực hiện thống kê mô tả biến valence theo mode:

  • Dòng 1-2: Nhóm dữ liệu theo mode (Major / Minor).

  • Dòng 3-9: Tính các chỉ số thống kê cơ bản của valence trong từng nhóm:

    • Trung bình (Mean), Min, Q1 (25%), Median, Q3 (75%), Max.
    • na.rm = TRUE đảm bảo bỏ qua các giá trị thiếu.
  • Dòng 11: Hiển thị kết quả bảng bằng kable, làm tròn 3 chữ số thập phân và thêm tiêu đề.

Nhận xét

Biến valence thể hiện mức độ cảm xúc tích cực của bài hát. Trung bình valence của các bài hát ở chế độ Major (0.531) cao hơn một chút so với Minor (0.522), cho thấy bài hát ở thang âm trưởng có xu hướng tích cực hơn về cảm xúc. Tuy nhiên, khoảng phân vị (Q1–Q3) và giá trị min/max cho thấy phân bố khá tương đồng giữa hai nhóm. Sự chênh lệch không lớn, phản ánh rằng cả hai thang âm đều chứa bài hát với nhiều mức độ cảm xúc khác nhau.

1.2.3.2. Biến popularity theo explicit

1. pop_explicit_summary <- data %>%
2.   group_by(explicit) %>%
3.   summarise(
4.     Mean = mean(popularity, na.rm = TRUE),
5.     Min = min(popularity, na.rm = TRUE),
6.     Q1 = quantile(popularity, 0.25, na.rm = TRUE),
7.     Median = median(popularity, na.rm = TRUE),
8.     Q3 = quantile(popularity, 0.75, na.rm = TRUE),
9.     Max = max(popularity, na.rm = TRUE)
10.   )
11. kable(pop_explicit_summary, digits = 1, caption = "Thống kê popularity theo explicit")
Thống kê popularity theo explicit
explicit Mean Min Q1 Median Q3 Max
Không nhạy cảm 30.2 0 10 32 46 97
Nhạy cảm 45.2 0 37 51 62 100

Giải thích kĩ thuật:

1-11. Thực hiện thống kê mô tả biến popularity theo explicit:

  • Dòng 1-2: Nhóm dữ liệu theo explicit (Có lời nhạy cảm / Không nhạy cảm).

  • Dòng 3-9: Tính các chỉ số thống kê cơ bản của popularity trong từng nhóm:

    • Trung bình (Mean), Min, Q1 (25%), Median, Q3 (75%), Max.
    • na.rm = TRUE loại bỏ các giá trị thiếu nếu có.
  • Dòng 11: Hiển thị kết quả bảng bằng kable, làm tròn 1 chữ số thập phân và thêm tiêu đề.

###Nhận xét

Bài hát có nội dung nhạy cảm (explicit = 1) có mức độ phổ biến trung bình cao hơn đáng kể (45.2 so với 30.2). Phân bố giá trị cũng cho thấy các bài hát nhạy cảm có Q1, Median, Q3 đều cao hơn, tức rằng phần lớn bài hát nhạy cảm được nghe nhiều hơn. Điều này có thể phản ánh xu hướng người nghe ưu tiên những bài hát gây chú ý hoặc thị trường nhạc dành cho đối tượng trưởng thành.

1.2.3.3. Biến danceability theo key

1. dance_key_summary <- data %>%
2.   group_by(key) %>%
3.   summarise(
4.     Mean = mean(danceability, na.rm = TRUE),
5.     Min = min(danceability, na.rm = TRUE),
6.     Q1 = quantile(danceability, 0.25, na.rm = TRUE),
7.     Median = median(danceability, na.rm = TRUE),
8.     Q3 = quantile(danceability, 0.75, na.rm = TRUE),
9.     Max = max(danceability, na.rm = TRUE)
10.   )
11. kable(dance_key_summary, digits = 3, caption = "Thống kê danceability theo key")
Thống kê danceability theo key
key Mean Min Q1 Median Q3 Max
0 0.536 0 0.418 0.547 0.663 0.977
1 0.561 0 0.430 0.576 0.706 0.985
2 0.525 0 0.405 0.534 0.651 0.980
3 0.499 0 0.372 0.508 0.625 0.973
4 0.525 0 0.404 0.535 0.652 0.971
5 0.530 0 0.412 0.541 0.659 0.988
6 0.558 0 0.436 0.567 0.694 0.980
7 0.537 0 0.417 0.548 0.665 0.983
8 0.536 0 0.414 0.546 0.668 0.986
9 0.537 0 0.419 0.544 0.663 0.975
10 0.542 0 0.417 0.556 0.676 0.970
11 0.566 0 0.441 0.584 0.703 0.979

Giải thích kĩ thuật:

1-11. Thực hiện thống kê mô tả biến danceability theo key:

  • Dòng 1-2: Nhóm dữ liệu theo key (nốt nhạc chính của bài hát).

  • Dòng 3-9: Tính các chỉ số thống kê cơ bản của danceability trong từng nhóm:

    • Trung bình (Mean), Min, Q1 (25%), Median, Q3 (75%), Max.
    • na.rm = TRUE đảm bảo các giá trị thiếu không ảnh hưởng đến kết quả.
  • Dòng 11: Hiển thị kết quả bảng bằng kable, làm tròn 3 chữ số thập phân và thêm tiêu đề.

Nhận xét

Biến danceability phản ánh mức độ dễ nhảy của bài hát. Trung bình các key dao động từ 0.499 đến 0.566, cho thấy sự khác biệt nhỏ về khả năng nhảy theo cao độ chính. Giá trị min bằng 0 trong tất cả các nhóm, nghĩa là vẫn có một số bài hát rất khó nhảy ở mọi key. Khoảng phân vị Q1–Q3 cho thấy hầu hết bài hát tập trung ở mức danceable trung bình đến cao, đặc biệt các key 1, 6, 11 có giá trị trung bình cao hơn, có thể là key phổ biến cho các bài hát sôi động.

1.2.3.4. Các biến định lượng khác theo nhóm định tính

1. other_summary <- data %>%
2.   group_by(mode, explicit) %>%
3.   summarise(
4.     Mean_energy = mean(energy, na.rm = TRUE),
5.     Min_energy = min(energy, na.rm = TRUE),
6.     Q1_energy = quantile(energy, 0.25, na.rm = TRUE),
7.     Median_energy = median(energy, na.rm = TRUE),
8.     Q3_energy = quantile(energy, 0.75, na.rm = TRUE),
9.     Max_energy = max(energy, na.rm = TRUE),
10.     
11.     Mean_acoustic = mean(acousticness, na.rm = TRUE),
12.     Min_acoustic = min(acousticness, na.rm = TRUE),
13.     Q1_acoustic = quantile(acousticness, 0.25, na.rm = TRUE),
14.     Median_acoustic = median(acousticness, na.rm = TRUE),
15.     Q3_acoustic = quantile(acousticness, 0.75, na.rm = TRUE),
16.     Max_acoustic = max(acousticness, na.rm = TRUE),
17.     
18.     Mean_tempo = mean(tempo, na.rm = TRUE),
19.     Min_tempo = min(tempo, na.rm = TRUE),
20.     Q1_tempo = quantile(tempo, 0.25, na.rm = TRUE),
21.     Median_tempo = median(tempo, na.rm = TRUE),
22.     Q3_tempo = quantile(tempo, 0.75, na.rm = TRUE),
23.     Max_tempo = max(tempo, na.rm = TRUE)
24.   )
25. kable(other_summary, digits = 2, caption = "Thống kê các biến định lượng khác theo nhóm định tính (mode & explicit)")
Thống kê các biến định lượng khác theo nhóm định tính (mode & explicit)
mode explicit Mean_energy Min_energy Q1_energy Median_energy Q3_energy Max_energy Mean_acoustic Min_acoustic Q1_acoustic Median_acoustic Q3_acoustic Max_acoustic Mean_tempo Min_tempo Q1_tempo Median_tempo Q3_tempo Max_tempo
Minor Không nhạy cảm 0.48 0 0.25 0.47 0.71 1 0.51 0 0.09 0.54 0.92 1 116.12 0 93.61 114.41 134.03 243.51
Minor Nhạy cảm 0.61 0 0.48 0.64 0.78 1 0.20 0 0.03 0.11 0.30 1 117.68 0 91.98 112.13 140.05 221.11
Major Không nhạy cảm 0.47 0 0.24 0.44 0.68 1 0.54 0 0.15 0.60 0.91 1 117.00 0 93.63 114.96 135.49 236.80
Major Nhạy cảm 0.59 0 0.43 0.64 0.79 1 0.20 0 0.02 0.09 0.28 1 118.34 0 92.45 112.94 140.89 220.10

Giải thích kĩ thuật:

1-25. Thực hiện thống kê mô tả nhiều biến định lượng theo hai nhóm định tính modeexplicit:

  • Dòng 1-2: Nhóm dữ liệu theo hai biến định tính modeexplicit.

  • Dòng 3-23: Tính các chỉ số thống kê cơ bản cho từng biến định lượng trong từng nhóm:

    • energy, acousticness, tempo
    • Bao gồm: Trung bình (Mean), Min, Q1 (25%), Median, Q3 (75%), Max.
    • na.rm = TRUE để bỏ qua giá trị NA.
  • Dòng 25: Hiển thị bảng kết quả bằng kable, làm tròn 2 chữ số thập phân, kèm tiêu đề rõ ràng.

Nhận xét:

  • Energy: Bài hát nhạy cảm có mức năng lượng trung bình cao hơn (~0.59–0.61) so với không nhạy cảm (~0.47–0.48), phản ánh rằng những bài hát nhạy cảm thường sôi động, mạnh mẽ hơn.

  • Acousticness: Nhóm không nhạy cảm có giá trị acoustic trung bình cao (0.51–0.54), trong khi nhóm nhạy cảm thấp (0.20), nghĩa là các bài hát nhạy cảm thường ít acoustic, thiên về nhạc điện tử.

  • Tempo: Nhịp trung bình tương đối đồng đều (~116–118 BPM), nhưng median tempo nhóm nhạy cảm thấp hơn một chút so với không nhạy cảm, cho thấy sự phân bố nhịp có thể hơi lệch về nhịp chậm ở các bài nhạy cảm.

Kết luận chung:

Các biến định lượng (energy, acousticness, tempo) đều chịu ảnh hưởng rõ rệt bởi nhóm định tính (mode & explicit). Nhóm nhạy cảm có xu hướng mạnh mẽ, năng lượng cao, ít acoustic và nhịp tương đối nhanh. Ngược lại, nhóm không nhạy cảm thiên về acoustic và mức năng lượng vừa phải.

1.2.3.5 Kiểm định sự khác biệt trung bình của các đặc trưng kỹ thuật âm nhạc giữa các nhóm

Sau khi đã khảo sát phân bố và mối quan hệ giữa các đặc trưng kỹ thuật âm nhạc, mục này tập trung vào việc kiểm tra xem giá trị trung bình của các biến định lượng như danceability, energy, valence có khác nhau giữa các nhóm phân loại của bài hát hay không (ví dụ: theo thể loại nhạc, nhóm nhạc hoặc mức độ instrumentalness).

Ý tưởng là sử dụng các phép kiểm định thống kê (t-test hoặc ANOVA) để đánh giá sự khác biệt trung bình giữa các nhóm. Giả thuyết gốc H0 là “không có sự khác biệt trung bình giữa các nhóm”. Nếu p-value < 0.05, H0 bị bác bỏ, chứng tỏ có sự khác biệt đáng kể về đặc trưng kỹ thuật giữa các nhóm.

Kết quả giúp hiểu rõ hơn sự phân hóa của các đặc trưng âm nhạc theo nhóm, đồng thời cung cấp cơ sở cho việc phân loại, dự đoán thể loại hoặc đặc tính bài hát dựa trên các đặc trưng kỹ thuật.

So sánh danceability theo các nhóm

1. library(dplyr)
2. if (all(c("danceability","investment") %in% names(data))) {
3.   t_test_inv <- t.test(danceability ~ investment, data = data)
4.   cat("## T-test: danceability ~ investment\n")
5.   print(t_test_inv)
6. }
7. if (all(c("danceability","sector") %in% names(data))) {
8.   anova_sec <- aov(danceability ~ sector, data = data)
9.   cat("\n## ANOVA: danceability ~ sector\n")
10.   print(summary(anova_sec))
11.   if (summary(anova_sec)[[1]][["Pr(>F)"]][1] < 0.05) {
12.     cat("\n### TukeyHSD: danceability ~ sector\n")
13.     print(TukeyHSD(anova_sec))
14.   }
15. }
16. if (all(c("danceability","Holding_Period") %in% names(data))) {
17.   anova_hp <- aov(danceability ~ Holding_Period, data = data)
18.   cat("\n## ANOVA: danceability ~ Holding_Period\n")
19.   print(summary(anova_hp))
20.   if (summary(anova_hp)[[1]][["Pr(>F)"]][1] < 0.05) {
21.     cat("\n### TukeyHSD: danceability ~ Holding_Period\n")
22.     print(TukeyHSD(anova_hp))
23.   }
24. }
25. if (all(c("danceability","Risk_Level") %in% names(data))) {
26.   anova_rl <- aov(danceability ~ Risk_Level, data = data)
27.   cat("\n## ANOVA: danceability ~ Risk_Level\n")
28.   print(summary(anova_rl))
29.   if (summary(anova_rl)[[1]][["Pr(>F)"]][1] < 0.05) {
30.     cat("\n### TukeyHSD: danceability ~ Risk_Level\n")
31.     print(TukeyHSD(anova_rl))
32.   }
33. }
34. if (all(c("danceability","Amount_Level") %in% names(data))) {
35.   anova_al <- aov(danceability ~ Amount_Level, data = data)
36.   cat("\n## ANOVA: danceability ~ Amount_Level\n")
37.   print(summary(anova_al))
38.   if (summary(anova_al)[[1]][["Pr(>F)"]][1] < 0.05) {
39.     cat("\n### TukeyHSD: danceability ~ Amount_Level\n")
40.     print(TukeyHSD(anova_al))
41.   }
42. }

Giải thích kĩ thuật:

1-42. Thực hiện kiểm định thống kê để so sánh giá trị trung bình của biến danceability theo các nhóm định tính trong bộ dữ liệu data:

  • Dòng 2-6: Nếu tồn tại biến investment, thực hiện T-test giữa hai nhóm của investment với danceability.

    • t.test(danceability ~ investment) kiểm tra sự khác biệt trung bình giữa hai nhóm.
    • In kết quả t-statistic, p-value và confidence interval.
  • Dòng 7-14, 16-23, 25-32, 34-41: Nếu tồn tại các biến định tính sector, Holding_Period, Risk_Level, Amount_Level:

    • Thực hiện ANOVA một chiều (aov) để kiểm tra sự khác biệt trung bình của danceability giữa nhiều nhóm.
    • In bảng tóm tắt ANOVA (summary(aov)), gồm SS, df, F-value, p-value.
    • Nếu p-value < 0.05 (có ý nghĩa thống kê), thực hiện TukeyHSD để so sánh cặp nhóm với nhau, xác định nhóm nào khác biệt.

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

1.3.1.1. Phân bố valence theo mode

1. library("ggplot2")
2. ggplot(data, aes(x = mode, y = valence, fill = mode)) +
3.   geom_boxplot(alpha = 0.7) +
4.   geom_jitter(width = 0.15, alpha = 0.3, color = "black") +
5.   labs(title = "Phân bố Valence theo Mode",
6.        x = "Mode",
7.        y = "Valence") +
8.   theme_minimal() +
9.   scale_fill_manual(values = c("tomato", "skyblue"))

Giải thích kĩ thuật:

1-9. Vẽ biểu đồ hộp (boxplot) kết hợp jitter để trực quan hóa phân bố của biến valence theo các nhóm mode trong bộ dữ liệu data.

  • aes(x = mode, y = valence, fill = mode): Xác định trục X là mode, trục Y là valence, màu nền box theo mode.
  • geom_boxplot(alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, với độ trong mờ 0.7.
  • geom_jitter(width = 0.15, alpha = 0.3, color = "black"): Vẽ các điểm dữ liệu riêng lẻ, thêm nhiễu ngang (jitter) để tránh chồng lấp.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • scale_fill_manual(values = c("tomato", "skyblue")): Chỉ định màu sắc cho từng nhóm mode.

1.3.1.2. Phân bố popularity theo explicit

1. ggplot(data, aes(x = explicit, y = popularity, fill = explicit)) +
2.   geom_boxplot(alpha = 0.7) +
3.   geom_jitter(width = 0.15, alpha = 0.3, color = "black") +
4.   labs(title = "Phân bố Popularity theo Explicit",
5.        x = "Explicit",
6.        y = "Popularity") +
7.   theme_minimal() +
8.   scale_fill_manual(values = c("lightgreen", "orange"))

Giải thích kĩ thuật:

1-8. Vẽ biểu đồ hộp (boxplot) kết hợp jitter để trực quan hóa phân bố của biến popularity theo các nhóm explicit trong bộ dữ liệu data.

  • aes(x = explicit, y = popularity, fill = explicit): Xác định trục X là explicit, trục Y là popularity, màu nền box theo explicit.
  • geom_boxplot(alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, với độ trong mờ 0.7.
  • geom_jitter(width = 0.15, alpha = 0.3, color = "black"): Vẽ các điểm dữ liệu riêng lẻ, thêm nhiễu ngang (jitter) để tránh chồng lấp.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • scale_fill_manual(values = c("lightgreen", "orange")): Chỉ định màu sắc cho từng nhóm explicit.

1.3.1.3. Phân bố danceability theo key

1. ggplot(data, aes(x = key, y = danceability, fill = key)) +
2.   geom_boxplot(alpha = 0.7) +
3.   labs(title = "Phân bố Danceability theo Key",
4.        x = "Key",
5.        y = "Danceability") +
6.   theme_minimal() +
7.   theme(legend.position = "none")

Giải thích kĩ thuật:

1-7. Vẽ biểu đồ hộp (boxplot) để trực quan hóa phân bố của biến danceability theo các nhóm key trong bộ dữ liệu data.

  • aes(x = key, y = danceability, fill = key): Xác định trục X là key, trục Y là danceability, màu nền box theo key.
  • geom_boxplot(alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, với độ trong mờ 0.7.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • theme(legend.position = "none"): Ẩn chú giải (legend) để biểu đồ gọn hơn.

1.3.1.4. Phân bố Energy theo Mode và Explicit

1. ggplot(data, aes(x = mode, y = energy, fill = explicit)) +
2.   geom_boxplot(position = position_dodge(0.8), alpha = 0.7) +
3.   labs(title = "Phân bố Energy theo Mode và Explicit",
4.        x = "Mode",
5.        y = "Energy") +
6.   theme_minimal() +
7.   scale_fill_manual(values = c("lightblue", "pink"))

Giải thích kĩ thuật:

1-7. Vẽ biểu đồ hộp (boxplot) phân nhóm để trực quan hóa phân bố của biến energy theo các nhóm modeexplicit trong bộ dữ liệu data.

  • aes(x = mode, y = energy, fill = explicit): Xác định trục X là mode, trục Y là energy, màu nền box theo explicit.
  • geom_boxplot(position = position_dodge(0.8), alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, đồng thời tách các nhóm explicit ra cạnh nhau (dodge) với độ trong mờ 0.7.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • scale_fill_manual(values = c("lightblue", "pink")): Chỉ định màu sắc cho từng nhóm explicit.

1.3.1.5. Phân bố Acousticness theo Mode và Explicit

1. ggplot(data, aes(x = mode, y = acousticness, fill = explicit)) +
2.   geom_boxplot(position = position_dodge(0.8), alpha = 0.7) +
3.   labs(title = "Phân bố Acousticness theo Mode và Explicit",
4.        x = "Mode",
5.        y = "Acousticness") +
6.   theme_minimal() +
7.   scale_fill_manual(values = c("lightblue", "pink"))

Giải thích kĩ thuật:

1-7. Vẽ biểu đồ hộp (boxplot) phân nhóm để trực quan hóa phân bố của biến acousticness theo các nhóm modeexplicit trong bộ dữ liệu data.

  • aes(x = mode, y = acousticness, fill = explicit): Trục X là mode, trục Y là acousticness, màu nền box theo explicit.
  • geom_boxplot(position = position_dodge(0.8), alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, các nhóm explicit được tách ra cạnh nhau (dodge) với độ trong mờ 0.7.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • scale_fill_manual(values = c("lightblue", "pink")): Chỉ định màu sắc cho từng nhóm explicit.

1.3.1.6. Biểu đồ cho các biến định lượng chung

1. ggplot(data, aes(x = mode, y = tempo, fill = explicit)) +
2.   geom_boxplot(position = position_dodge(0.8), alpha = 0.7) +
3.   labs(title = "Phân bố Tempo theo Mode và Explicit",
4.        x = "Mode",
5.        y = "Tempo") +
6.   theme_minimal() +
7.   scale_fill_manual(values = c("lightblue", "pink"))

Giải thích kĩ thuật:

1-7. Vẽ biểu đồ hộp (boxplot) phân nhóm để trực quan hóa phân bố của biến tempo theo các nhóm modeexplicit trong bộ dữ liệu data.

  • aes(x = mode, y = tempo, fill = explicit): Trục X là mode, trục Y là tempo, màu nền box theo explicit.
  • geom_boxplot(position = position_dodge(0.8), alpha = 0.7): Vẽ hộp biểu diễn min, Q1, median, Q3, max, các nhóm explicit được tách ra cạnh nhau (dodge) với độ trong mờ 0.7.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(): Giao diện tối giản, sạch sẽ.
  • scale_fill_manual(values = c("lightblue", "pink")): Chỉ định màu sắc cho từng nhóm explicit.

1.3.2. Trực quan hóa mối quan hệ hai biến

1.3.2.1. Phân tích loudness và energy

1. #### **1.4.2.1. Phân tích "loudness" và "energy"**
2. ggplot(data, aes(x = loudness, y = energy)) +
3.   geom_point(alpha = 0.4, color = "darkblue") +
4.   geom_smooth(method = "lm", se = FALSE, color = "red", linetype = "dashed") +
5.   labs(
6.     title = "Mối quan hệ giữa Loudness và Energy",
7.     x = "Loudness",
8.     y = "Energy"
9.   ) +
10.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-10. Vẽ biểu đồ phân tán (scatter plot) để trực quan hóa mối quan hệ giữa loudnessenergy trong bộ dữ liệu data.

  • aes(x = loudness, y = energy): Trục X là loudness, trục Y là energy.
  • geom_point(alpha = 0.4, color = "darkblue"): Vẽ các điểm dữ liệu với độ trong mờ 0.4 và màu xanh đậm.
  • geom_smooth(method = "lm", se = FALSE, color = "red", linetype = "dashed"): Thêm đường hồi quy tuyến tính (linear model) dưới dạng đường nét đứt màu đỏ, không hiển thị khoảng tin cậy.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.2. Phân tích “danceability” và “energy_dance_ratio”

1. ggplot(data, aes(x = danceability, y = energy_dance_ratio)) +
2.   geom_point(alpha = 0.4, color = "darkgreen") +
3.   geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed") +
4.   labs(
5.     title = "Mối quan hệ giữa Danceability và Energy Dance Ratio",
6.     x = "Danceability",
7.     y = "Energy Dance Ratio"
8.   ) +
9.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-9. Vẽ biểu đồ phân tán (scatter plot) để trực quan hóa mối quan hệ giữa danceabilityenergy_dance_ratio trong bộ dữ liệu data.

  • aes(x = danceability, y = energy_dance_ratio): Trục X là danceability, trục Y là energy_dance_ratio.
  • geom_point(alpha = 0.4, color = "darkgreen"): Vẽ các điểm dữ liệu với độ trong mờ 0.4 và màu xanh đậm.
  • geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed"): Thêm đường hồi quy tuyến tính (linear model) dưới dạng đường nét đứt màu đen, không hiển thị khoảng tin cậy.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.3. Phân tích “valence” và “energy”

1. ggplot(data, aes(x = valence, y = energy)) +
2.   geom_point(alpha = 0.4, color = "purple") +
3.   geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed") +
4.   labs(
5.     title = "Mối quan hệ giữa Valence và Energy",
6.     x = "Valence",
7.     y = "Energy"
8.   ) +
9.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-9. Vẽ biểu đồ phân tán (scatter plot) để trực quan hóa mối quan hệ giữa valenceenergy trong bộ dữ liệu data.

  • aes(x = valence, y = energy): Trục X là valence, trục Y là energy.
  • geom_point(alpha = 0.4, color = "purple"): Vẽ các điểm dữ liệu với độ trong mờ 0.4 và màu tím.
  • geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed"): Thêm đường hồi quy tuyến tính dưới dạng đường nét đứt màu đen, không hiển thị khoảng tin cậy.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.4. Phân tích “energy” và “popularity_level”

1. ggplot(data, aes(x = popularity_level, y = energy, fill = popularity_level)) +
2.   geom_boxplot() +
3.   labs(
4.     title = "Phân bố Energy theo mức độ phổ biến",
5.     x = "Mức độ phổ biến",
6.     y = "Energy"
7.   ) +
8.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-8. Vẽ biểu đồ hộp (boxplot) để trực quan hóa phân bố của biến energy theo các nhóm popularity_level trong bộ dữ liệu data.

  • aes(x = popularity_level, y = energy, fill = popularity_level): Xác định trục X là popularity_level, trục Y là energy, màu nền box theo nhóm popularity_level.
  • geom_boxplot(): Vẽ hộp biểu diễn min, Q1, median, Q3, max của energy theo từng nhóm.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.5. Phân tích “valence” và “release_decade”

1. ggplot(data, aes(x = release_decade, y = valence, fill = release_decade)) +
2.   geom_boxplot() +
3.   labs(
4.     title = "Phân bố Valence theo thập kỷ phát hành",
5.     x = "Thập kỷ phát hành",
6.     y = "Valence"
7.   ) +
8.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-8. Vẽ biểu đồ hộp (boxplot) để trực quan hóa phân bố của biến valence theo các nhóm release_decade trong bộ dữ liệu data.

  • aes(x = release_decade, y = valence, fill = release_decade): Xác định trục X là release_decade, trục Y là valence, màu nền box theo từng thập kỷ phát hành.
  • geom_boxplot(): Vẽ hộp biểu diễn min, Q1, median, Q3, max của valence theo từng nhóm thập kỷ.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.6. Phân tích “tempo” và “popularity_level”

1. ggplot(data, aes(x = popularity_level, y = tempo, fill = popularity_level)) +
2.   geom_boxplot() +
3.   labs(
4.     title = "Phân bố Tempo theo mức độ phổ biến",
5.     x = "Mức độ phổ biến",
6.     y = "Tempo"
7.   ) +
8.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-8. Vẽ biểu đồ hộp (boxplot) để trực quan hóa phân bố của biến tempo theo các nhóm popularity_level trong bộ dữ liệu data.

  • aes(x = popularity_level, y = tempo, fill = popularity_level): Xác định trục X là popularity_level, trục Y là tempo, màu nền box theo từng mức độ phổ biến.
  • geom_boxplot(): Vẽ hộp biểu diễn min, Q1, median, Q3, max của tempo theo từng nhóm mức độ phổ biến.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.7. Phân tích “tempo_category” và “popularity_level”

1. ggplot(data, aes(x = popularity_level, fill = tempo_category)) +
2.   geom_bar(position = "fill") +
3.   scale_y_continuous(labels = scales::percent_format()) +
4.   labs(
5.     title = "Phân bố Tempo Category theo mức độ phổ biến",
6.     x = "Mức độ phổ biến",
7.     y = "Tỷ lệ (%)",
8.     fill = "Nhịp độ"
9.   ) +
10.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-10. Vẽ biểu đồ cột phân phối tỉ lệ (stacked proportion bar chart) để trực quan hóa tỷ lệ các nhóm tempo_category theo từng mức độ popularity_level trong bộ dữ liệu data.

  • aes(x = popularity_level, fill = tempo_category): Xác định trục X là popularity_level, màu nền cột theo tempo_category.
  • geom_bar(position = "fill"): Vẽ cột xếp chồng, chuẩn hóa theo tỷ lệ (0–1) để hiển thị phần trăm trong mỗi nhóm.
  • scale_y_continuous(labels = scales::percent_format()): Chuyển trục Y từ 0–1 sang phần trăm.
  • labs(title, x, y, fill): Thêm tiêu đề, nhãn trục và chú thích màu.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.2.8. Phân tích “is_explicit_factor” và “release_decade”

1. ggplot(data, aes(x = release_decade, fill = is_explicit_factor)) +
2.   geom_bar(position = "fill") +
3.   scale_y_continuous(labels = scales::percent_format()) +
4.   labs(
5.     title = "Phân bố lời nhạy cảm theo thập kỷ phát hành",
6.     x = "Thập kỷ phát hành",
7.     y = "Tỷ lệ (%)",
8.     fill = "Lời nhạy cảm"
9.   ) +
10.   theme_minimal(base_size = 11)

Giải thích kĩ thuật:

1-10. Vẽ biểu đồ cột phân phối tỉ lệ (stacked proportion bar chart) để trực quan hóa tỷ lệ các bài hát có/không lời nhạy cảm (is_explicit_factor) theo từng thập kỷ phát hành (release_decade) trong bộ dữ liệu data.

  • aes(x = release_decade, fill = is_explicit_factor): Xác định trục X là thập kỷ phát hành, màu nền cột theo trạng thái lời nhạy cảm.
  • geom_bar(position = "fill"): Vẽ cột xếp chồng chuẩn hóa theo tỷ lệ (0–1) để hiển thị phần trăm trong mỗi thập kỷ.
  • scale_y_continuous(labels = scales::percent_format()): Chuyển trục Y từ 0–1 sang phần trăm.
  • labs(title, x, y, fill): Thêm tiêu đề, nhãn trục và chú thích màu.
  • theme_minimal(base_size = 11): Sử dụng giao diện tối giản với cỡ chữ cơ bản 11.

1.3.3. Tóm tắt tương quan

1. vars_numeric <- c("valence", "energy", "danceability", "duration_min",
2.                   "loudness", "tempo", "acousticness", "instrumentalness", "speechiness")
3. cor_matrix_spotify <- data %>%
4.   select(all_of(vars_numeric)) %>%
5.   na.omit() %>%
6.   cor(method = "pearson")
7. get_lower_tri <- function(cormat){
8.   cormat[upper.tri(cormat)] <- NA
9.   return(cormat)
10. }
11. lower_tri <- get_lower_tri(cor_matrix_spotify)
12. melted_cor_lower <- reshape2::melt(lower_tri, na.rm = TRUE) %>%
13.   mutate(value = round(value, 2))
14. ggplot(melted_cor_lower, aes(Var2, Var1, fill = value)) +
15.   geom_tile(color = "white") +
16.   scale_fill_gradient2(low = "blue", high = "red", mid = "white", 
17.                        midpoint = 0, limit = c(-1,1), name="Tương quan\nPearson (r)") +
18.   geom_text(aes(label = value), color = "black", size = 3) +
19.   coord_fixed() +
20.   labs(title = "Heatmap Ma trận tương quan các đặc trưng bài hát",
21.        x = "Biến số", y = "Biến số") +
22.   theme_minimal(base_size = 11) +
23.   theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1),
24.         axis.text.y = element_text(size = 9))

Giải thích kĩ thuật:

1-24. Vẽ heatmap ma trận tương quan các biến định lượng trong bộ dữ liệu data.

  • vars_numeric: Xác định các biến định lượng cần tính tương quan.
  • cor_matrix_spotify: Tạo ma trận tương quan Pearson giữa các biến, loại bỏ giá trị NA.
  • get_lower_tri(): Lấy nửa dưới của ma trận tương quan, bỏ phần trên (upper triangle) để tránh trùng lặp.
  • reshape2::melt(lower_tri, na.rm = TRUE): Chuyển ma trận sang dạng dài để dùng với ggplot2.
  • aes(Var2, Var1, fill = value): Trục X và Y là tên biến, màu nền thể hiện hệ số tương quan.
  • geom_tile(color = "white"): Vẽ các ô màu cho ma trận.
  • scale_fill_gradient2(...): Thiết lập màu gradient từ xanh (âm) → trắng (0) → đỏ (dương), giới hạn từ -1 đến 1.
  • geom_text(aes(label = value), ...): Hiển thị giá trị hệ số tương quan lên từng ô.
  • coord_fixed(): Giữ tỉ lệ các ô vuông, không bị méo.
  • labs(...): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 11) + theme(...): Giao diện tối giản, xoay chữ trục X 45° và chỉnh kích thước chữ trục Y.

1.3.4. Trực quan hóa So sánh giữa các Nhóm

1.3.4.1. So sánh đặc điểm bài hát theo các nhóm phân loại

Bao gồm các biểu đồ Box Plot để so sánh:

  • Popularity theo thập kỷ phát hành

  • Loudness theo khả năng nhảy (energy_dance_level)

  • Valence theo thập kỷ phát hành

  • Energy theo mức độ phổ biến

1. data <- data %>%
2.   mutate(release_decade = floor(year / 10) * 10 %>% as.factor()) %>%
3.   mutate(energy_dance_ratio = energy * danceability,
4.          energy_dance_level = case_when(
5.            energy_dance_ratio < 0.33 ~ "Thấp",
6.            energy_dance_ratio < 0.66 ~ "Trung bình",
7.            TRUE ~ "Cao"
8.          ),
9.          energy_dance_level = factor(energy_dance_level, levels = c("Thấp","Trung bình","Cao"))) %>%
10.   mutate(popularity_level = case_when(
11.     popularity < 34 ~ "Thấp",
12.     popularity < 67 ~ "Trung bình",
13.     TRUE ~ "Cao"
14.   ),
15.   popularity_level = factor(popularity_level, levels = c("Thấp","Trung bình","Cao"))
16.   )
17. create_box_plot_music <- function(data, x_var, y_var, title, subtitle, limits_y=NULL) {
18.   p <- ggplot(data, aes(x = .data[[x_var]], y = .data[[y_var]], fill = .data[[x_var]])) +
19.     geom_boxplot(width = 0.5, color = "black", alpha = 0.7, outlier.alpha = 0.2) +
20.     stat_summary(fun = "mean", geom = "point", shape = 23, size = 3, fill = "white") +
21.     labs(title = title, subtitle = subtitle, x = "", y = y_var) +
22.     theme_minimal(base_size = 10) +
23.     theme(legend.position = "none")
24.   if(!is.null(limits_y)) { p <- p + scale_y_continuous(limits = limits_y) }
25.   return(p)
26. }
27. p1 <- create_box_plot_music(data, "release_decade", "popularity", 
28.                             "1. Popularity theo Thập kỷ phát hành", 
29.                             "Xu hướng: Thập kỷ gần đây phổ biến hơn")
30. p2 <- create_box_plot_music(data, "energy_dance_level", "loudness", 
31.                             "2. Loudness theo khả năng nhảy", 
32.                             "Nhóm Cao có Loudness trung bình lớn hơn")
33. p3 <- create_box_plot_music(data, "release_decade", "valence", 
34.                             "3. Valence theo Thập kỷ phát hành", 
35.                             "Xu hướng tâm trạng thay đổi theo thập kỷ")
36. p4 <- create_box_plot_music(data, "popularity_level", "energy", 
37.                             "4. Energy theo Mức độ phổ biến", 
38.                             "Nhóm phổ biến cao có năng lượng cao hơn")
39. gridExtra::grid.arrange(p1, p2, p3, p4, ncol = 2)

Giải thích kĩ thuật:

1-16. Chuẩn bị dữ liệu:

  • release_decade: Chia năm phát hành (year) thành thập kỷ và chuyển sang factor.
  • energy_dance_ratio: Tạo biến mới bằng tích energy * danceability.
  • energy_dance_level: Phân loại energy_dance_ratio thành 3 nhóm “Thấp”, “Trung bình”, “Cao”.
  • popularity_level: Phân loại popularity thành 3 nhóm tương tự.

17-26. Định nghĩa hàm create_box_plot_music:

  • ggplot(data, aes(x = .data[[x_var]], y = .data[[y_var]], fill = .data[[x_var]])): Trục X là biến định tính, trục Y là biến định lượng, màu theo nhóm X.
  • geom_boxplot(...): Vẽ hộp, tùy chỉnh màu viền, độ trong suốt, và độ mờ outlier.
  • stat_summary(fun = "mean", ...): Vẽ điểm trung bình của mỗi nhóm.
  • labs(title, subtitle, x, y): Thêm tiêu đề, phụ đề, nhãn trục.
  • theme_minimal(base_size = 10) + theme(legend.position = "none"): Giao diện tối giản, bỏ chú giải.
  • scale_y_continuous(limits = limits_y): Nếu có giới hạn trục Y, áp dụng.

27-38. Tạo 4 biểu đồ hộp (boxplot) với các nhóm khác nhau:

  • p1: Popularity theo thập kỷ phát hành.
  • p2: Loudness theo năng lực nhảy (energy_dance_level).
  • p3: Valence theo thập kỷ phát hành.
  • p4: Energy theo mức độ phổ biến.
  1. gridExtra::grid.arrange(...): Hiển thị 4 biểu đồ trong cùng một layout 2x2.

1.3.4.2. So sánh đặc điểm bài hát theo mức độ phổ biến

Bao gồm các biểu đồ để so sánh tỷ lệ hay phân bố các đặc điểm theo nhóm:

  • Tỷ lệ khả năng nhảy (energy_dance_level) theo mức độ phổ biến

  • Tỷ lệ nhịp độ (tempo_category) theo mức độ phổ biến

  • Tỷ lệ lời nhạy cảm (is_explicit_factor) theo mức độ phổ biến

1. library(ggplot2)
2. library(dplyr)
3. library(forcats)
4. p_energy <- ggplot(data, aes(x = popularity_level, fill = energy_dance_level)) +
5.   geom_bar(position = "fill", width = 0.6) +
6.   scale_y_continuous(labels = scales::percent_format()) +
7.   scale_fill_brewer(palette = "Set2") +
8.   labs(
9.     title = "Tỷ lệ khả năng nhảy theo mức độ phổ biến",
10.     x = "Mức độ phổ biến",
11.     y = "Tỷ lệ (%)",
12.     fill = "Khả năng nhảy"
13.   ) +
14.   theme_minimal(base_size = 11) +
15.   theme(axis.text.x = element_text(angle = 30, hjust = 1))
16. p_tempo <- ggplot(data, aes(x = popularity_level, fill = tempo_category)) +
17.   geom_bar(position = "fill", width = 0.6) +
18.   scale_y_continuous(labels = scales::percent_format()) +
19.   scale_fill_brewer(palette = "Set3") +
20.   labs(
21.     title = "Tỷ lệ nhịp độ bài hát theo mức độ phổ biến",
22.     x = "Mức độ phổ biến",
23.     y = "Tỷ lệ (%)",
24.     fill = "Nhịp độ"
25.   ) +
26.   theme_minimal(base_size = 11) +
27.   theme(axis.text.x = element_text(angle = 30, hjust = 1))
28. p_explicit <- ggplot(data, aes(x = popularity_level, fill = is_explicit_factor)) +
29.   geom_bar(position = "fill", width = 0.6) +
30.   scale_y_continuous(labels = scales::percent_format()) +
31.   labs(
32.     title = "Tỷ lệ lời nhạy cảm theo mức độ phổ biến",
33.     x = "Mức độ phổ biến",
34.     y = "Tỷ lệ (%)",
35.     fill = "Lời nhạy cảm"
36.   ) +
37.   theme_minimal(base_size = 11) +
38.   theme(axis.text.x = element_text(angle = 30, hjust = 1))
39. library(gridExtra)
40. grid.arrange(p_energy, p_tempo, p_explicit, ncol = 1)

Giải thích kĩ thuật:

4-15. p_energy:

  • ggplot(data, aes(x = popularity_level, fill = energy_dance_level)): Trục X là mức độ phổ biến, màu theo nhóm năng lượng nhảy.
  • geom_bar(position = "fill", width = 0.6): Vẽ biểu đồ cột, tỷ lệ từng nhóm trong mỗi cột.
  • scale_y_continuous(labels = scales::percent_format()): Trục Y hiển thị dạng phần trăm.
  • scale_fill_brewer(palette = "Set2"): Chọn bảng màu Set2 cho các nhóm.
  • labs(title, x, y, fill): Thêm tiêu đề, nhãn trục, nhãn legend.
  • theme_minimal(base_size = 11) + theme(axis.text.x = element_text(angle = 30, hjust = 1)): Giao diện tối giản, xoay nhãn trục X 30°.

16-27. p_tempo:

  • Tương tự p_energy, nhưng fill theo tempo_category và bảng màu Set3.

28-38. p_explicit:

  • Tương tự, nhưng fill theo is_explicit_factor (lời nhạy cảm).

39-40. grid.arrange(p_energy, p_tempo, p_explicit, ncol = 1): Hiển thị 3 biểu đồ dạng cột xếp chồng theo hàng, 1 cột duy nhất.

1.3.4.4. Trực quan hóa Xu hướng Thời gian

Xu hướng phát hành bài hát theo thời gian

1. song_year_trend <- data %>%
2.   filter(!is.na(release_year)) %>%
3.   count(release_year, name = "Count") %>%
4.   arrange(release_year)
5. song_decade_trend <- data %>%
6.   filter(!is.na(release_decade)) %>%
7.   count(release_decade, name = "Count") %>%
8.   arrange(release_decade)
9. p_year <- ggplot(song_year_trend, aes(x = factor(release_year), y = Count)) +
10.   geom_col(fill = "#1F77B4") +
11.   geom_text(aes(label = Count), vjust = -0.5, size = 2.5, 
12.             check_overlap = TRUE) +  # tự bỏ label chồng
13.   labs(title = "Số lượng bài hát theo năm", x = "Năm phát hành", y = "Số lượng") +
14.   theme_minimal(base_size = 10) +
15.   theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7))
16. p_decade <- ggplot(song_decade_trend, aes(x = release_decade, y = Count)) +
17.   geom_col(fill = "#D62728") +
18.   geom_text(aes(label = Count), vjust = -0.5, size = 3) +
19.   labs(title = "Số lượng bài hát theo thập kỷ", x = "Thập kỷ", y = "Số lượng") +
20.   theme_minimal(base_size = 10)
21. gridExtra::grid.arrange(p_year, p_decade, ncol = 2)

Giải thích kĩ thuật:

1-4. song_year_trend:

  • filter(!is.na(release_year)): Loại bỏ các bản ghi không có năm phát hành.
  • count(release_year, name = "Count"): Đếm số lượng bài hát theo từng năm, lưu vào cột Count.
  • arrange(release_year): Sắp xếp theo năm tăng dần.

5-8. song_decade_trend:

  • Tương tự song_year_trend, nhưng nhóm theo thập kỷ (release_decade).

9-15. p_year:

  • ggplot(song_year_trend, aes(x = factor(release_year), y = Count)): Trục X là năm (chuyển sang factor), trục Y là số lượng bài hát.
  • geom_col(fill = "#1F77B4"): Vẽ cột màu xanh dương.
  • geom_text(aes(label = Count), vjust = -0.5, size = 2.5, check_overlap = TRUE): Thêm nhãn số lượng trên cột, tự động bỏ nhãn chồng.
  • labs(title, x, y): Thêm tiêu đề và nhãn trục.
  • theme_minimal(base_size = 10) + theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7)): Giao diện tối giản, xoay nhãn X 45°, chữ nhỏ hơn.

16-20. p_decade:

  • Tương tự p_year, nhưng X là thập kỷ, cột màu đỏ, nhãn chữ cỡ 3.

21. gridExtra::grid.arrange(p_year, p_decade, ncol = 2):

  • Hiển thị 2 biểu đồ cạnh nhau theo 2 cột.

1.4. KẾT LUẬN VÀ HẠN CHẾ

1.4.1. Kết luận

Bộ dữ liệu Spotify 1921–2020 cung cấp thông tin chi tiết về các bài hát trong gần một thế kỷ, bao gồm các đặc trưng âm nhạc, thông tin nghệ sĩ, năm phát hành và mức độ phổ biến. Dữ liệu đã được làm sạch và chuẩn hóa, không tồn tại giá trị thiếu, đảm bảo tính khả thi cho các phân tích thống kê và trực quan hóa tiếp theo.

Phân tích xu hướng âm nhạc theo thời gian cho thấy sự gia tăng đáng kể về số lượng bài hát được phát hành từ thập niên 1950 đến 2010. Các bài hát hiện đại có xu hướng nhịp nhanh hơn, năng lượng cao hơn và độ dễ nghe cao hơn so với các bài hát trước đó, phản ánh sự thay đổi trong cấu trúc kỹ thuật và đặc trưng cảm xúc của âm nhạc qua các thập kỷ.

Về các đặc trưng kỹ thuật, các bài hát chủ yếu sử dụng điệu Major, trong khi Minor chiếm tỷ lệ nhỏ hơn. Các yếu tố như loudness, tempo có mức độ biến thiên lớn, còn danceability, liveness và speechiness phân bố tập trung, cho thấy sự ổn định về trải nghiệm cảm giác khi nghe mặc dù kỹ thuật âm nhạc đa dạng.

Các bài hát có mức độ phổ biến cao thường có danceability, energy và valence lớn, cho thấy nhịp điệu, năng lượng và cảm xúc tích cực đóng vai trò quan trọng trong việc thu hút người nghe. Tính explicit chỉ chiếm tỷ lệ nhỏ, cho thấy nội dung lời không phải yếu tố quyết định chính đến mức độ phổ biến của bài hát.

Tổng kết, bộ dữ liệu Spotify cung cấp cái nhìn hệ thống về sự phát triển của âm nhạc và các yếu tố kỹ thuật liên quan đến sự phổ biến của bài hát. Những thông tin này có giá trị trong nghiên cứu xu hướng âm nhạc, dự báo hành vi người nghe và phát triển các hệ thống đề xuất bài hát phù hợp.

1.4.2. Hàm ý

Kết quả nghiên cứu cho thấy các đặc trưng kỹ thuật của bài hát, như năng lượng, nhịp điệu và cảm xúc tích cực, có ảnh hưởng rõ rệt đến mức độ phổ biến. Do đó, các cơ quan quản lý, tổ chức âm nhạc và các nền tảng phát trực tuyến có thể cân nhắc việc xây dựng các chính sách hỗ trợ sáng tác và phát hành bài hát dựa trên các đặc trưng này. Ví dụ, việc khuyến khích đào tạo nghệ sĩ về các yếu tố âm nhạc hiện đại có thể nâng cao chất lượng sản phẩm và đáp ứng thị hiếu của khán giả.

Ngoài ra, việc thu thập, chuẩn hóa và phân tích dữ liệu âm nhạc định kỳ có thể giúp các cơ quan hoạch định chính sách theo dõi xu hướng âm nhạc, phát hiện các tác động văn hóa – xã hội và dự báo nhu cầu của người nghe. Những thông tin này có thể hỗ trợ quyết định về việc cấp phép, đầu tư vào nghệ sĩ mới hoặc điều chỉnh các quy định liên quan đến bản quyền, phát hành và quảng bá nội dung âm nhạc.

Cuối cùng, nghiên cứu cũng nhấn mạnh vai trò của dữ liệu lớn trong ngành âm nhạc. Việc áp dụng phân tích dữ liệu để định hướng phát triển chính sách sẽ giúp nâng cao hiệu quả quản lý, thúc đẩy ngành âm nhạc phát triển bền vững, đồng thời tạo cơ sở khoa học cho các quyết định chiến lược liên quan đến sáng tạo, đầu tư và tiêu dùng âm nhạc.

1.4.2. Hạn chế

Mặc dù nghiên cứu đã cung cấp những kết quả có giá trị về mối quan hệ giữa các yếu tố kỹ thuật của bài hát và mức độ phổ biến, nhưng vẫn tồn tại một số hạn chế cần lưu ý. Thứ nhất, dữ liệu nghiên cứu chủ yếu dựa trên các nền tảng trực tuyến và có thể không phản ánh đầy đủ hành vi của tất cả nhóm đối tượng người nghe, dẫn đến khả năng thiên lệch mẫu. Thứ hai, nghiên cứu tập trung vào các đặc trưng âm nhạc định lượng, trong khi các yếu tố định tính như xu hướng văn hóa, sở thích cá nhân, hoặc ảnh hưởng từ mạng xã hội chưa được khai thác đầy đủ. Thứ ba, phương pháp phân tích chủ yếu dựa trên mô hình hồi quy và các chỉ số phổ biến, do đó khả năng dự báo trong các bối cảnh âm nhạc thay đổi nhanh hoặc các thể loại mới vẫn còn hạn chế.

Những hạn chế này gợi ý rằng các kết quả nghiên cứu cần được áp dụng cẩn trọng và kết hợp với các nghiên cứu bổ sung để có cái nhìn toàn diện hơn về thị trường âm nhạc.

CHƯƠNG 2: PHÂN TÍCH DỮ LIỆU TÀI CHÍNH CỦA MÃ CHỨNG KHOÁN AGR

2.1 GIỚI THIỆU BỘ DỮ LIỆU

Mục tiêu phân tích

Mục tiêu chính là phân tích sức khỏe tài chính và hiệu quả hoạt động của Ngân hàng Nông nghiệp & Phát triển Nông thôn (Agribank) trong giai đoạn 2015–2024.
Mục tiêu thứ cấp là đánh giá xu hướng thay đổi các chỉ số tài chính nội tại qua các năm để rút ra nhận xét về hiệu quả quản lý ngân hàng.

Nguồn dữ liệu

Bộ dữ liệu được xây dựng từ Báo cáo tài chính hợp nhất/riêng hàng năm đã kiểm toán của Agribank, bao gồm:
- Bảng Cân đối Kế toán
- Bảng Kết quả Kinh doanh
- Bảng Lưu chuyển tiền tệ

Phạm vi dữ liệu: từ 31/12/2015 đến 31/12/2024.
Dữ liệu đã được tổng hợp và chuyển sang Excel để thuận tiện cho phân tích.

Đơn vị quan sát và phạm vi

  • Đơn vị quan sát: 1 năm (tổng hợp theo năm).
  • Phạm vi: 10 năm, từ 2015 đến 2024.

2.1.1. Đọc dữ liệu

1. library(DT)
2. library(readxl)
3. library(dplyr)
4. library(stringr)
5. dl <- read_excel("C:\\Users\\Administrator\\Downloads\\dl.xlsx")

Giải thích kĩ thuật:

  1. : Nạp gói DT để hiển thị bảng dữ liệu tương tác trong R Markdown/HTML.

  2. : Nạp gói readxl để đọc file Excel (.xlsx, .xls) vào R.

  3. : Nạp gói dplyr để thao tác dữ liệu, lọc, chọn, nhóm, tóm tắt.

  4. : Nạp gói stringr để xử lý chuỗi (string) dễ dàng hơn.

  5. : Đọc file Excel từ đường dẫn chỉ định vào biến dl.

2.1.2. Kiểm tra kích thước dữ liệu

1. dim(dl)
## [1] 10 22
1. nrow(dl)
## [1] 10
1. ncol(dl)
## [1] 22
1. paste("Số quan sát:", nrow(dl))
## [1] "Số quan sát: 10"
1. paste("Số biến:", ncol(dl))
## [1] "Số biến: 22"

Giải thích kĩ thuật:

  1. : Kiểm tra kích thước của dữ liệu dl, trả về số hàng (quan sát) và số cột (biến).

Giải thích kĩ thuật:

  1. : Lấy số lượng hàng (quan sát) trong bảng dữ liệu dl.

Giải thích kĩ thuật:

  1. : Lấy số lượng cột (biến) trong bảng dữ liệu dl.

Giải thích kĩ thuật:

  1. : Kết hợp chuỗi "Số quan sát:" với số lượng hàng (quan sát) trong dl để in ra thông tin tổng số quan sát.

Giải thích kĩ thuật:

  1. : Kết hợp chuỗi "Số biến:" với số lượng cột (biến) trong dl để in ra thông tin tổng số biến.

2.1.3. Kiểm tra kiểu dữ liệu từng cột

1. library(tibble)
2. library(knitr)
3. sapply(dl, class) %>%
4.   enframe(name = "Variable", value = "Data_Type") %>%
5.   kable(caption = "Kiểu dữ liệu của từng biến trong bộ dữ liệu dl")
Kiểu dữ liệu của từng biến trong bộ dữ liệu dl
Variable Data_Type
Year numeric
TTS numeric
nophaitra numeric
VCSH numeric
tienguicuakhachhang numeric
phaitranhadautuvetienguigiaodichck numeric
phaitracotucgocvalaitraiphieu numeric
congdoanhthuhoatdong numeric
congchiphihoatdong numeric
congdoanhthuhoatdongtaichinh numeric
congchiphitaichinh numeric
chiphiquanlycongtick numeric
tongloinhuanketoantruocthue numeric
chiphithuetndn numeric
loinhuanketoansauthuetndn numeric
tonglotoandienkhacsauthuetndn numeric
laicobantrencophieu numeric
luuchuyentienthuantuhoatdongkinhdoanh numeric
luuchuyentienthuantuhoatdongdautu numeric
luuchuyentienthuantuhoatdongtaichinh character
tienvacackhoantuongduongtiendaunam numeric
tienvacackhoantuongduongtiencuoinam numeric

Giải thích kĩ thuật:

  1. : Nạp gói tibble để tạo các bảng dữ liệu dạng tidy dễ thao tác và hiển thị.
  2. : Nạp gói knitr để dùng hàm kable() hiển thị bảng đẹp trong R Markdown/HTML.
  3. : Sử dụng sapply() để lấy kiểu dữ liệu (class) của từng biến trong dl.
  4. : Chuyển kết quả thành tibble với cột "Variable""Data_Type" bằng enframe().
  5. : Hiển thị bảng kiểu dữ liệu của từng biến bằng kable() với tiêu đề "Kiểu dữ liệu của từng biến trong bộ dữ liệu dl".

2.1.4.Kiểm tra giá trị thiếu (NA)

1. if (any(is.na(dl))) {
2.   na_summary <- data.frame(
3.     Variable = names(dl),
4.     Missing_Count = colSums(is.na(dl)),
5.     Missing_Percent = round(colMeans(is.na(dl)) * 100, 2)
6.   )
7.   library(knitr)
8.   kable(na_summary, caption = "Thống kê giá trị thiếu (NA) theo từng biến trong bộ dữ liệu dl")
9. } else {
10.   cat("Bộ dữ liệu không có giá trị thiếu (NA).")
11. }
## Bộ dữ liệu không có giá trị thiếu (NA).

Giải thích kĩ thuật:

1-2. : Kiểm tra xem có bất kỳ giá trị thiếu (NA) trong dl hay không, nếu có thì tiến hành thống kê. 3. : Tạo bảng na_summary với cột "Variable" là tên các biến. 4. : Cột "Missing_Count" tính tổng số giá trị thiếu (NA) cho từng biến bằng colSums(is.na(dl)). 5. : Cột "Missing_Percent" tính tỷ lệ % giá trị thiếu trên tổng số quan sát bằng colMeans(is.na(dl)) * 100 và làm tròn 2 chữ số thập phân. 7. : Nạp gói knitr để hiển thị bảng bằng kable(). 8. : Hiển thị bảng thống kê giá trị thiếu với caption "Thống kê giá trị thiếu (NA) theo từng biến trong bộ dữ liệu dl". 9-11. : Nếu không có giá trị thiếu, in ra thông báo "Bộ dữ liệu không có giá trị thiếu (NA).".

2.1.5. Kiểm tra trùng lặp (nếu có)

1. dup_count <- sum(duplicated(dl))
2. if (dup_count > 0) {
3.   cat("Số lượng quan sát bị trùng lặp trong bộ dữ liệu là:", dup_count, "\n")
4.   dl[duplicated(dl), ]
5. } else {
6.   cat("Không có giá trị trùng lặp trong bộ dữ liệu.")
7. }
## Không có giá trị trùng lặp trong bộ dữ liệu.

Giải thích kĩ thuật:

  1. : Đếm tổng số quan sát trùng lặp trong dl bằng sum(duplicated(dl)) và lưu vào dup_count. 2-4. : Nếu có quan sát trùng lặp (dup_count > 0), in ra số lượng trùng và hiển thị các quan sát trùng bằng dl[duplicated(dl), ]. 5-7. : Nếu không có quan sát trùng, in thông báo "Không có giá trị trùng lặp trong bộ dữ liệu.".

2.1.6. Kiểm tra tính hợp lệ của dữ liệu

1. invalid_values <- sapply(dl, function(x) sum(is.infinite(x) | is.nan(x)))
2. invalid_total <- sum(invalid_values)
3. if (invalid_total == 0) {
4.   cat("Dữ liệu hợp lệ, không có giá trị vô hạn (Inf) hoặc không xác định (NaN).")
5. } else {
6.   cat(" Có", invalid_total, "giá trị không hợp lệ trong bộ dữ liệu.\n")
7.   print(invalid_values[invalid_values > 0])
8. }
## Dữ liệu hợp lệ, không có giá trị vô hạn (Inf) hoặc không xác định (NaN).

Giải thích kĩ thuật:

  1. : Dùng sapply để đếm số giá trị không hợp lệ trong từng biến của dl, gồm Inf, -InfNaN, lưu vào invalid_values.
  2. : Tính tổng số giá trị không hợp lệ trong toàn bộ bộ dữ liệu, lưu vào invalid_total. 3-4. : Nếu tổng bằng 0, in thông báo dữ liệu hợp lệ. 5-8. : Nếu có giá trị không hợp lệ, in tổng số và chi tiết số lượng theo từng biến.

2.1.7.Hiển thị 6 dòng đầu tiên và 6 dòng cuối cùng của bộ dữ liệu

1. library(DT)
2. datatable(
3.   head(dl, 6),
4.   caption = "6 dòng đầu tiên của bộ dữ liệu",
5.   options = list(scrollX = TRUE)
6. )
1. datatable(
2.   tail(dl, 6),
3.   caption = "6 dòng cuối cùng của bộ dữ liệu",
4.   options = list(scrollX = TRUE)
5. )

Giải thích kĩ thuật:

  1. : Nạp gói DT để tạo bảng dữ liệu tương tác trong R Markdown/HTML. 2-6. : Hiển thị 6 dòng đầu tiên của dl dưới dạng bảng tương tác, có tiêu đề "6 dòng đầu tiên của bộ dữ liệu" và cho phép cuộn ngang (scrollX = TRUE).

Giải thích kĩ thuật:

1-5. : Hiển thị 6 dòng cuối cùng của dl dưới dạng bảng dữ liệu tương tác, có tiêu đề "6 dòng cuối cùng của bộ dữ liệu" và cho phép cuộn ngang (scrollX = TRUE).

2.1.8.Kiểm tra cấu trúc dữ liệu

1. cat("<pre>", paste(capture.output(str(dl)), collapse = "\n"), "</pre>")
## <pre> tibble [10 × 22] (S3: tbl_df/tbl/data.frame)
##  $ Year                                 : num [1:10] 2015 2016 2017 2018 2019 ...
##  $ TTS                                  : num [1:10] 2.24e+12 1.66e+12 1.78e+12 1.92e+12 2.11e+12 ...
##  $ nophaitra                            : num [1:10] 1.88e+11 1.20e+10 1.28e+10 4.67e+10 1.44e+11 ...
##  $ VCSH                                 : num [1:10] 2.05e+12 1.65e+12 1.76e+12 1.87e+12 1.97e+12 ...
##  $ tienguicuakhachhang                  : num [1:10] 1.64e+11 1.85e+11 2.74e+11 2.86e+11 1.50e+11 ...
##  $ phaitranhadautuvetienguigiaodichck   : num [1:10] 1.64e+11 1.82e+11 2.70e+11 2.83e+11 1.47e+11 ...
##  $ phaitracotucgocvalaitraiphieu        : num [1:10] 2.11e+09 3.47e+09 3.22e+09 3.23e+09 2.94e+09 ...
##  $ congdoanhthuhoatdong                 : num [1:10] 1.69e+11 1.34e+11 1.85e+11 1.81e+11 2.04e+11 ...
##  $ congchiphihoatdong                   : num [1:10] -5.02e+10 -4.56e+11 -4.81e+10 -2.91e+10 -4.58e+10 ...
##  $ congdoanhthuhoatdongtaichinh         : num [1:10] 0.00 1.74e+09 1.35e+09 1.29e+09 1.23e+09 ...
##  $ congchiphitaichinh                   : num [1:10] 0.00 -4.71e+08 -7.57e+08 -8.18e+08 0.00 ...
##  $ chiphiquanlycongtick                 : num [1:10] -3.31e+11 -4.43e+10 -5.58e+10 -6.80e+10 -7.43e+10 ...
##  $ tongloinhuanketoantruocthue          : num [1:10] -2.13e+11 -3.66e+11 8.16e+10 8.48e+10 8.53e+10 ...
##  $ chiphithuetndn                       : num [1:10] 2.60e+10 -3.96e+10 -1.63e+10 -1.70e+10 -1.71e+10 ...
##  $ loinhuanketoansauthuetndn            : num [1:10] -1.87e+11 -4.05e+11 6.53e+10 6.78e+10 6.83e+10 ...
##  $ tonglotoandienkhacsauthuetndn        : num [1:10] 0.00 0.00 5.02e+10 3.88e+10 2.82e+10 ...
##  $ laicobantrencophieu                  : num [1:10] -884 -1919 309 321 323 ...
##  $ luuchuyentienthuantuhoatdongkinhdoanh: num [1:10] 3.95e+11 -2.03e+11 1.55e+10 -4.61e+11 1.27e+11 ...
##  $ luuchuyentienthuantuhoatdongdautu    : num [1:10] -3.51e+09 -1.91e+09 -4.29e+09 2.82e+11 -3.05e+09 ...
##  $ luuchuyentienthuantuhoatdongtaichinh : chr [1:10] "-181366661000" "122802000" "1228070000" "1,400,000,000 " ...
##  $ tienvacackhoantuongduongtiendaunam   : num [1:10] 3.53e+11 3.98e+11 1.93e+11 2.04e+11 2.49e+10 ...
##  $ tienvacackhoantuongduongtiencuoinam  : num [1:10] 5.63e+11 1.93e+11 2.04e+11 2.49e+10 1.14e+11 ... </pre>

Giải thích kĩ thuật:

  1. : Hiển thị cấu trúc (str()) của bộ dữ liệu dl trong R Markdown/HTML, dùng capture.output() để lấy kết quả dưới dạng chuỗi, paste(..., collapse = "\n") nối các dòng, và <pre> để giữ định dạng hiển thị như trong console.

2.1.9.Đổi tên hoặc chuẩn hóa tên biến

1. library (janitor)
2. dl <- clean_names(dl)
3. names(dl)
##  [1] "year"                                 
##  [2] "tts"                                  
##  [3] "nophaitra"                            
##  [4] "vcsh"                                 
##  [5] "tienguicuakhachhang"                  
##  [6] "phaitranhadautuvetienguigiaodichck"   
##  [7] "phaitracotucgocvalaitraiphieu"        
##  [8] "congdoanhthuhoatdong"                 
##  [9] "congchiphihoatdong"                   
## [10] "congdoanhthuhoatdongtaichinh"         
## [11] "congchiphitaichinh"                   
## [12] "chiphiquanlycongtick"                 
## [13] "tongloinhuanketoantruocthue"          
## [14] "chiphithuetndn"                       
## [15] "loinhuanketoansauthuetndn"            
## [16] "tonglotoandienkhacsauthuetndn"        
## [17] "laicobantrencophieu"                  
## [18] "luuchuyentienthuantuhoatdongkinhdoanh"
## [19] "luuchuyentienthuantuhoatdongdautu"    
## [20] "luuchuyentienthuantuhoatdongtaichinh" 
## [21] "tienvacackhoantuongduongtiendaunam"   
## [22] "tienvacackhoantuongduongtiencuoinam"

Giải thích kĩ thuật:

  1. : Nạp gói janitor để xử lý và làm sạch tên biến trong bộ dữ liệu.

  2. : Sử dụng clean_names() để chuyển tên biến trong dl về dạng chuẩn, không dấu, viết thường, thay khoảng trắng hoặc ký tự đặc biệt bằng dấu gạch dưới _.

  3. : Hiển thị danh sách tên biến sau khi đã làm sạch.

2.1.10.Thống kê mô tả

1. cat("<pre>", paste(capture.output(summary(dl)), collapse = "\n"), "</pre>")
## <pre>       year           tts              nophaitra              vcsh          
##  Min.   :2015   Min.   :1.660e+12   Min.   :1.204e+10   Min.   :1.648e+12  
##  1st Qu.:2017   1st Qu.:1.966e+12   1st Qu.:7.109e+10   1st Qu.:1.895e+12  
##  Median :2020   Median :2.264e+12   Median :1.890e+11   Median :2.075e+12  
##  Mean   :2020   Mean   :2.407e+12   Mean   :2.787e+11   Mean   :2.129e+12  
##  3rd Qu.:2022   3rd Qu.:2.789e+12   3rd Qu.:3.030e+11   3rd Qu.:2.449e+12  
##  Max.   :2024   Max.   :3.472e+12   Max.   :1.030e+12   Max.   :2.497e+12  
##  tienguicuakhachhang phaitranhadautuvetienguigiaodichck
##  Min.   :1.496e+11   Min.   :1.466e+11                 
##  1st Qu.:2.073e+11   1st Qu.:2.039e+11                 
##  Median :3.525e+11   Median :3.475e+11                 
##  Mean   :4.549e+11   Mean   :3.584e+11                 
##  3rd Qu.:5.180e+11   3rd Qu.:4.942e+11                 
##  Max.   :1.410e+12   Max.   :6.388e+11                 
##  phaitracotucgocvalaitraiphieu congdoanhthuhoatdong congchiphihoatdong  
##  Min.   :2.110e+09             Min.   :1.336e+11    Min.   :-4.558e+11  
##  1st Qu.:3.222e+09             1st Qu.:1.821e+11    1st Qu.:-4.754e+10  
##  Median :4.988e+09             Median :2.144e+11    Median :-2.430e+10  
##  Mean   :9.665e+10             Mean   :2.633e+11    Mean   :-3.351e+10  
##  3rd Qu.:9.678e+09             3rd Qu.:3.659e+11    3rd Qu.: 5.014e+10  
##  Max.   :8.642e+11             Max.   :4.129e+11    Max.   : 1.237e+11  
##  congdoanhthuhoatdongtaichinh congchiphitaichinh   chiphiquanlycongtick
##  Min.   :0.000e+00            Min.   :-8.175e+08   Min.   :-3.313e+11  
##  1st Qu.:1.186e+09            1st Qu.:-3.534e+08   1st Qu.:-7.277e+10  
##  Median :1.268e+09            Median : 0.000e+00   Median :-5.003e+10  
##  Mean   :1.188e+09            Mean   : 4.594e+09   Mean   :-2.627e+10  
##  3rd Qu.:1.347e+09            3rd Qu.: 0.000e+00   3rd Qu.: 9.469e+10  
##  Max.   :1.739e+09            Max.   : 3.416e+10   Max.   : 1.116e+11  
##  tongloinhuanketoantruocthue chiphithuetndn       loinhuanketoansauthuetndn
##  Min.   :-3.658e+11          Min.   :-3.958e+10   Min.   :-4.054e+11       
##  1st Qu.: 8.240e+10          1st Qu.:-1.705e+10   1st Qu.: 6.591e+10       
##  Median : 1.032e+11          Median : 4.846e+09   Median : 8.253e+10       
##  Mean   : 7.574e+10          Mean   : 5.715e+09   Mean   : 5.240e+10       
##  3rd Qu.: 1.774e+11          3rd Qu.: 3.390e+10   3rd Qu.: 1.435e+11       
##  Max.   : 4.320e+11          Max.   : 4.184e+10   Max.   : 3.901e+11       
##  tonglotoandienkhacsauthuetndn laicobantrencophieu
##  Min.   :-7.261e+10            Min.   :-1919.0    
##  1st Qu.:-1.750e+10            1st Qu.:  312.0    
##  Median : 0.000e+00            Median :  390.5    
##  Mean   : 4.804e+08            Mean   :  244.4    
##  3rd Qu.: 3.176e+10            3rd Qu.:  666.0    
##  Max.   : 5.025e+10            Max.   : 1840.0    
##  luuchuyentienthuantuhoatdongkinhdoanh luuchuyentienthuantuhoatdongdautu
##  Min.   :-4.614e+11                    Min.   :-8.668e+10               
##  1st Qu.:-2.640e+11                    1st Qu.:-1.668e+10               
##  Median : 2.186e+10                    Median :-5.732e+09               
##  Mean   :-1.925e+10                    Mean   : 6.118e+09               
##  3rd Qu.: 1.718e+11                    3rd Qu.:-3.166e+09               
##  Max.   : 3.953e+11                    Max.   : 2.821e+11               
##  luuchuyentienthuantuhoatdongtaichinh tienvacackhoantuongduongtiendaunam
##  Length:10                            Min.   :2.494e+10                 
##  Class :character                     1st Qu.:1.175e+11                 
##  Mode  :character                     Median :1.987e+11                 
##                                       Mean   :2.265e+11                 
##                                       3rd Qu.:3.408e+11                 
##                                       Max.   :5.188e+11                 
##  tienvacackhoantuongduongtiencuoinam
##  Min.   :2.494e+10                  
##  1st Qu.:1.175e+11                  
##  Median :1.987e+11                  
##  Mean   :2.532e+11                  
##  3rd Qu.:4.165e+11                  
##  Max.   :5.633e+11                   </pre>

Giải thích kĩ thuật: 1. : Hiển thị kết quả tóm tắt summary(dl) của bộ dữ liệu trong HTML/R Markdown, giữ định dạng gốc, bao gồm min, max, mean, median, quartiles cho các biến số, và tần suất cho các biến phân loại.

Nhận xét:

  • Biến TTS: Trung bình đạt khoảng 2.41×10¹², tăng dần qua thời gian, phản ánh quy mô tài sản mở rộng.
  • Biến Nophaitra: Giá trị dao động mạnh (từ 1.20×10¹⁰ đến 1.03×10¹²), cho thấy biến động lớn về nghĩa vụ nợ.
  • Biến VCSH: Trung bình khoảng 2.13×10¹², tăng ổn định, chứng tỏ khả năng tự chủ tài chính tốt.
  • Biến Tienguicuakhachhang: Trung bình 4.55×10¹¹, tăng dần theo năm, phản ánh sự mở rộng của hoạt động huy động vốn.
  • Biến Phaitranhadautuvetienguigiaodichck: Có xu hướng tăng (từ 1.46×10¹¹ đến 6.39×10¹¹), thể hiện hoạt động đầu tư tích cực.
  • Biến Loinhuanketoansauthuetndn: Dao động từ -4.05×10¹¹ đến 3.90×10¹¹, trung bình 5.24×10¹⁰, cho thấy lợi nhuận có biến động nhưng nhìn chung dương.
  • Nhóm biến Luuchuyentien…: Có biên độ dao động lớn giữa các hoạt động, thể hiện dòng tiền biến động mạnh qua các năm.
  • Tổng thể: Bộ dữ liệu thể hiện xu hướng tăng trưởng ổn định về quy mô, tài sản và hiệu quả tài chính trong giai đoạn 2015–2024.

2.2. Xử lý và chuẩn hoá dữ liệu

2.2.1 Chuẩn hóa kiểu dữ liệu

1. dl$luuchuyentienthuantuhoatdongtaichinh <- as.numeric(dl$luuchuyentienthuantuhoatdongtaichinh)
2. class(dl$luuchuyentienthuantuhoatdongtaichinh)
## [1] "numeric"

Giải thích kĩ thuật:

  1. : Chuyển cột luuchuyentienthuantuhoatdongtaichinh sang kiểu số (numeric) để phục vụ phân tích định lượng.
  2. : Kiểm tra kiểu dữ liệu hiện tại của cột sau khi chuyển đổi.

2.2.2. Kiểm tra và xử lý giá trị ngoại lai (nếu có)

1. vars_outlier <- c(
2.   "nophaitra",
3.   "tienguicuakhachhang",
4.   "phaitracotucgocvalaitraiphieu",
5.   "congchiphihoatdong",
6.   "congdoanhthuhoatdongtaichinh",
7.   "congchiphitaichinh",
8.   "chiphiquanlycongtick",
9.   "tongloinhuanketoantruocthue",
10.   "loinhuanketoansauthuetndn",
11.   "laicobantrencophieu",
12.   "luuchuyentienthuantuhoatdongdautu",
13.   "luuchuyentienthuantuhoatdongtaichinh"
14. )
15. replace_outlier_median_safe <- function(x, h = 1.5) {
16.   if (!is.numeric(x)) return(x)
17.   qs <- quantile(x, probs = c(0.25, 0.75), na.rm = TRUE)
18.   iqr <- qs[2] - qs[1]
19.   lower <- qs[1] - h * iqr
20.   upper <- qs[2] + h * iqr
21.   x[x < lower | x > upper] <- median(x, na.rm = TRUE)
22.   return(x)
23. }
24. dl[vars_outlier] <- lapply(dl[vars_outlier], replace_outlier_median_safe)
25. count_outlier <- function(x, h = 1.5) {
26.   if (!is.numeric(x)) return(NA)
27.   qs <- quantile(x, probs = c(0.25, 0.75), na.rm = TRUE)
28.   iqr <- qs[2] - qs[1]
29.   lower <- qs[1] - h * iqr
30.   upper <- qs[2] + h * iqr
31.   sum(x < lower | x > upper, na.rm = TRUE)
32. }
33. outlier_after <- sapply(dl[vars_outlier], count_outlier)
34. outlier_summary <- data.frame(
35.   Bien = names(outlier_after),
36.   So_luong_ngoai_lai_sau = as.vector(outlier_after)
37. )
38. library(knitr)
39. kable(outlier_summary, col.names = c("Biến", "Số lượng ngoại lai sau xử lý"))
Biến Số lượng ngoại lai sau xử lý
nophaitra 1
tienguicuakhachhang 0
phaitracotucgocvalaitraiphieu 1
congchiphihoatdong 0
congdoanhthuhoatdongtaichinh 2
congchiphitaichinh 0
chiphiquanlycongtick 0
tongloinhuanketoantruocthue 0
loinhuanketoansauthuetndn 0
laicobantrencophieu 0
luuchuyentienthuantuhoatdongdautu 2
luuchuyentienthuantuhoatdongtaichinh 2

Giải thích kĩ thuật:

1-14. vars_outlierreplace_outlier_median_safe:

  • Xác định danh sách các biến định lượng cần kiểm tra và xử lý ngoại lai (vars_outlier).
  • Hàm replace_outlier_median_safe thay thế giá trị ngoại lai bằng median dựa trên IQR với hệ số h = 1.5.
  • Chỉ áp dụng cho các biến số; giá trị < Q1 - 1.5IQR hoặc > Q3 + 1.5IQR được coi là ngoại lai và thay bằng median.

15-24. Áp dụng xử lý ngoại lai:

  • dl[vars_outlier] <- lapply(dl[vars_outlier], replace_outlier_median_safe): Thay các giá trị ngoại lai của tất cả biến trong danh sách bằng median an toàn.

25-32. Hàm count_outlier:

  • Đếm số lượng giá trị ngoại lai trước hoặc sau khi xử lý theo cùng nguyên tắc IQR.
  • Trả về số lượng giá trị ngoại lai trong mỗi biến.

33-37. Tóm tắt ngoại lai sau xử lý:

  • outlier_after <- sapply(dl[vars_outlier], count_outlier): Tính số lượng ngoại lai còn lại sau xử lý.
  • outlier_summary <- data.frame(Bien = names(outlier_after), So_luong_ngoai_lai_sau = as.vector(outlier_after)): Tạo bảng tổng hợp số lượng ngoại lai còn lại.

38-39. Hiển thị bảng:

  • library(knitr) nạp gói hiển thị bảng.
  • kable(outlier_summary, col.names = c("Biến", "Số lượng ngoại lai sau xử lý")): Hiển thị bảng tổng hợp ngoại lai trong R Markdown/HTML.

Sau khi xử lý outlier bằng phương pháp thay giá trị ngoại lai bằng median, hầu hết các giá trị cực đoan trong bộ dữ liệu đã được điều chỉnh. Tuy nhiên, vẫn còn tồn đọng một số outlier nhỏ (1–2 giá trị) ở một vài biến. Việc còn lại này không ảnh hưởng nghiêm trọng đến kết quả phân tích, vì các giá trị quá lệch đã được giảm thiểu tác động, đồng thời vẫn giữ được đặc điểm phân bố tổng thể của dữ liệu.

2.2.3. Kiểm tra & thay thế giá trị âm bất thường (nếu có)

1. check_replace_negative <- function(df, method = "median") {
2.   numeric_vars <- names(df)[sapply(df, is.numeric)]
3.   result <- data.frame(
4.     Bien = numeric_vars,
5.     So_gia_tri_am = integer(length(numeric_vars)),
6.     stringsAsFactors = FALSE
7.   )
8.   for (var in numeric_vars) {
9.     n_neg <- sum(df[[var]] < 0, na.rm = TRUE)
10.     result$So_gia_tri_am[result$Bien == var] <- n_neg
11.     if (n_neg > 0) {
12.       if (method == "median") {
13.         med <- median(df[[var]][df[[var]] >= 0], na.rm = TRUE)
14.         df[[var]][df[[var]] < 0] <- med
15.       } else if (method == "zero") {
16.         df[[var]][df[[var]] < 0] <- 0
17.       } else {
18.         stop("method phải là 'median' hoặc 'zero'")
19.       }
20.     }
21.   }
22.   if (all(result$So_gia_tri_am == 0)) {
23.     cat("Không có giá trị âm bất thường trong dataset.\n")
24.   } else {
25.     cat("Các giá trị âm đã được xử lý.\n")
26.   }
27.   return(list(
28.     data = df,
29.     check = result
30.   ))
31. }
32. res <- check_replace_negative(dl, method = "median")
## Các giá trị âm đã được xử lý.
1. dl <- res$data
2. kable(res$check, col.names = c("Biến", "Số giá trị âm"))
Biến Số giá trị âm
year 0
tts 0
nophaitra 0
vcsh 0
tienguicuakhachhang 0
phaitranhadautuvetienguigiaodichck 0
phaitracotucgocvalaitraiphieu 0
congdoanhthuhoatdong 0
congchiphihoatdong 6
congdoanhthuhoatdongtaichinh 0
congchiphitaichinh 3
chiphiquanlycongtick 6
tongloinhuanketoantruocthue 0
chiphithuetndn 5
loinhuanketoansauthuetndn 0
tonglotoandienkhacsauthuetndn 4
laicobantrencophieu 0
luuchuyentienthuantuhoatdongkinhdoanh 4
luuchuyentienthuantuhoatdongdautu 10
luuchuyentienthuantuhoatdongtaichinh 0
tienvacackhoantuongduongtiendaunam 0
tienvacackhoantuongduongtiencuoinam 0

Giải thích kĩ thuật:

1-2. Xác định các biến số trong df để kiểm tra giá trị âm. 3-7. Tạo bảng result lưu tên biến và số lượng giá trị âm trong mỗi biến. 8-21. Vòng lặp kiểm tra từng biến số:

  • Đếm số giá trị âm (n_neg).

  • Nếu có giá trị âm, xử lý theo phương pháp được chọn:

    • "median": thay các giá trị âm bằng median của các giá trị >= 0.
    • "zero": thay các giá trị âm bằng 0.
  • Nếu phương pháp không đúng, báo lỗi.

22-26. Thông báo:

  • Nếu không có giá trị âm, thông báo dataset hợp lệ.
  • Nếu có giá trị âm, thông báo đã xử lý.

27-31. Trả về danh sách gồm:

  • data: dataset đã xử lý.
  • check: bảng kiểm tra số lượng giá trị âm của từng biến.
  1. res <- check_replace_negative(dl, method = "median"): Áp dụng hàm cho dataset dl và lưu kết quả vào res.

Giải thích kĩ thuật:

  1. Lấy dataset đã xử lý giá trị âm từ kết quả của hàm check_replace_negative và gán lại cho dl.
  2. Hiển thị bảng kiểm tra số lượng giá trị âm theo từng biến (res$check) dưới dạng bảng đẹp với kable, đặt tên cột là "Biến""Số giá trị âm".

Sau khi kiểm tra và xử lý các giá trị âm bất thường trong bộ dữ liệu, hầu hết các giá trị cực đoan đã được thay thế bằng median của các giá trị hợp lý (không âm). Việc này giúp giảm thiểu tác động của các giá trị bất thường lên các thống kê mô tả và phân tích tiếp theo, đồng thời giữ được đặc điểm tổng thể của dữ liệu.

Tuy nhiên, vẫn tồn tại một số giá trị âm ở một vài biến, chẳng hạn như chi phí hoạt động, chi phí quản lý công trình, chi phí thuế TNDN, lưu chuyển tiền từ hoạt động đầu tư, v.v. Những giá trị âm này là bình thường trong bối cảnh dữ liệu tài chính và kế toán, bởi một số biến có thể ghi nhận các khoản chi trả âm, lỗ tạm tính, hoặc các giao dịch ngược dòng vốn. Do đó, sự tồn tại của những giá trị âm này không phải là bất thường về mặt logic và không ảnh hưởng nghiêm trọng đến các kết quả phân tích tổng quan.

Nhìn chung, việc xử lý các giá trị âm bất thường bằng median đã cải thiện tính ổn định của dữ liệu, giúp các thống kê như tổng, trung bình, phân bố không bị lệch quá mức. Đồng thời, các giá trị âm hợp lý vẫn được giữ nguyên, đảm bảo tính thực tế và chính xác của dữ liệu tài chính. Đây là bước quan trọng để dữ liệu sẵn sàng cho các phân tích mô tả, biểu đồ và so sánh giữa các biến, mà không làm mất đi ý nghĩa thực tế của các khoản chi, lợi nhuận hay lưu chuyển tiền.

2.2.4. Tạo biến phát sinh

Các biến phát sinh được tạo ra để quan sát sự thay đổi và đánh giá hiệu quả tài chính theo năm.

  • Tăng trưởng năm giúp thấy doanh thu, chi phí, lợi nhuận thay đổi ra sao so với năm trước.
  • Các tỷ lệ tài chính như hệ số đòn bẩy, ROA, ROE hay chất lượng lợi nhuận giúp đánh giá hiệu quả và rủi ro.
  • Tỷ lệ lợi nhuận biên và chi phí tài chính/doanh thu phản ánh khả năng quản lý chi phí và sinh lời. Chuẩn hóa dữ liệu theo % để so sánh các năm dễ hơn.
  • Chênh lệch tiền đầu kỳ – cuối kỳ và tăng trưởng dòng tiền cho thấy quản lý dòng tiền ra vào. quan, dễ theo dõi các chỉ số quan trọng theo năm.
1. dl <- dl %>% arrange(year)
2. dl <- dl %>%
3.   mutate(
4.     growth_TTS = c(NA, diff(tts) / head(tts, -1)),
5.     he_so_don_bay = tts / vcsh,
6.     ROA = loinhuanketoansauthuetndn / tts,
7.     ROE = loinhuanketoansauthuetndn / vcsh,
8.     chat_luong_loi_nhuan = luuchuyentienthuantuhoatdongkinhdoanh / loinhuanketoansauthuetndn,
9.     ty_le_loi_nhuan_bien = tongloinhuanketoantruocthue / congdoanhthuhoatdong,
10.     chenhlech_tien = tienvacackhoantuongduongtiendaunam - tienvacackhoantuongduongtiencuoinam,
11.     growth_dong_tien = c(NA, diff(luuchuyentienthuantuhoatdongkinhdoanh))
12.   )
13. cols_show <- c("year",
14.                "growth_TTS", "he_so_don_bay", "ROA", "ROE", "chat_luong_loi_nhuan",
15.                "ty_le_loi_nhuan_bien",
16.                "chenhlech_tien", "growth_dong_tien")
17. dl_display <- dl[, cols_show]
18. dl_display[ , -1] <- lapply(dl_display[ , -1], function(x) format(round(x, 4), big.mark = ",", scientific = FALSE))
19. kable(dl_display, col.names = cols_show)
year growth_TTS he_so_don_bay ROA ROE chat_luong_loi_nhuan ty_le_loi_nhuan_bien chenhlech_tien growth_dong_tien
2015 NA 1.0916 0.0368 0.0402 4.7896 0.6112 -210,388,970,856 NA
2016 -0.2595 1.0073 0.0497 0.0501 1.9001 0.7721 204,589,497,801 -238,457,168,604
2017 0.0700 1.0073 0.0367 0.0370 0.2371 0.4414 -11,183,447,632 -141,332,990,809
2018 0.0790 1.0249 0.0354 0.0363 2.3123 0.4681 179,317,640,528 141,332,990,809
2019 0.1013 1.0734 0.0323 0.0347 1.8583 0.4181 -89,233,320,713 -29,962,079,353
2020 0.0829 1.0905 0.0423 0.0462 0.2918 0.5382 -13,377,674,889 -98,601,438,926
2021 0.1981 1.1176 0.0301 0.0337 1.9001 0.2617 100,294,290,345 128,563,518,279
2022 0.0241 1.1233 0.0522 0.0586 2.3494 0.4911 -277,130,985,702 187,008,065,037
2023 0.0918 1.2276 0.0478 0.0586 1.2765 0.5045 -214,420,391,376 -157,045,985,684
2024 0.1338 1.4215 0.0389 0.0553 1.1602 0.4082 64,898,917,912 -29,962,079,353

Giải thích kĩ thuật:

1-2. Sắp xếp dl theo biến year để các phép tính tăng trưởng và tỷ lệ tính đúng thứ tự thời gian. 3-12. Tạo các biến mới:

  • growth_TTS: tăng trưởng tổng tài sản theo năm trước (diff(tts)/tts cũ).
  • he_so_don_bay: tỷ số “đòn bẩy” = tổng tài sản / vốn chủ sở hữu.
  • ROA: lợi nhuận sau thuế / tổng tài sản.
  • ROE: lợi nhuận sau thuế / vốn chủ sở hữu.
  • chat_luong_loi_nhuan: lưu chuyển tiền từ hoạt động kinh doanh / lợi nhuận sau thuế.
  • ty_le_loi_nhuan_bien: tổng lợi nhuận trước thuế / doanh thu hoạt động.
  • chenhlech_tien: chênh lệch tiền và các khoản tương đương tiền đầu năm và cuối năm.
  • growth_dong_tien: tăng trưởng lưu chuyển tiền từ hoạt động kinh doanh theo năm trước.

13-16. Chọn các cột quan tâm để hiển thị. 17. Tạo dataset hiển thị dl_display. 18. Làm tròn các giá trị số, thêm dấu phẩy phân cách hàng nghìn, tắt định dạng khoa học. 19. Hiển thị bảng dl_display đẹp với kable, giữ tên cột theo cols_show.

2.3. Thống kê mô tả cơ bản

2.3.1. Thống kê mô tả các biến quan trọng

1. library(dplyr)
2. library(tidyr)
3. library(knitr)
4. numeric_vars <- c(
5.   "tts",
6.   "nophaitra",
7.   "vcsh",
8.   "tienguicuakhachhang",
9.   "phaitranhadautuvetienguigiaodichck",
10.   "phaitracotucgocvalaitraiphieu",
11.   "congdoanhthuhoatdong",
12.   "congchiphihoatdong",
13.   "congdoanhthuhoatdongtaichinh",
14.   "congchiphitaichinh",
15.   "chiphiquanlycongtick",
16.   "tongloinhuanketoantruocthue",
17.   "chiphithuetndn",
18.   "loinhuanketoansauthuetndn",
19.   "tonglotoandienkhacsauthuetndn",
20.   "laicobantrencophieu",
21.   "luuchuyentienthuantuhoatdongkinhdoanh",
22.   "tienvacackhoantuongduongtiendaunam",
23.   "tienvacackhoantuongduongtiencuoinam",
24.   "ROA",
25.   "ROE"
26. )
27. desc_table <- dl %>%
28.   select(all_of(numeric_vars)) %>%
29.   summarise(across(everything(),
30.                    list(
31.                      Min = ~min(., na.rm = TRUE),
32.                      Q1 = ~quantile(., 0.25, na.rm = TRUE),
33.                      Median = ~median(., na.rm = TRUE),
34.                      Mean = ~mean(., na.rm = TRUE),
35.                      Q3 = ~quantile(., 0.75, na.rm = TRUE),
36.                      Max = ~max(., na.rm = TRUE),
37.                      SD = ~sd(., na.rm = TRUE),
38.                      NA_count = ~sum(is.na(.))
39.                    ), .names = "{col}_{fn}")) %>%
40.   pivot_longer(cols = everything(),
41.                names_to = c("Variable", "Statistic"),
42.                names_sep = "_",
43.                values_to = "Value") %>%
44.   pivot_wider(names_from = Statistic, values_from = Value)
45. kable(desc_table)
Variable Min Q1 Median Mean Q3 Max SD NA
tts 1.660379e+12 1.965632e+12 2.264296e+12 2.407331e+12 2.788711e+12 3.472227e+12 5.923027e+11 0
nophaitra 1.204437e+10 7.109135e+10 1.885294e+11 1.946829e+11 2.636188e+11 5.678591e+11 1.672719e+11 0
vcsh 1.648335e+12 1.894540e+12 2.075346e+12 2.128589e+12 2.448953e+12 2.497221e+12 3.224270e+11 0
tienguicuakhachhang 1.496126e+11 2.073229e+11 3.192224e+11 3.490866e+11 4.705734e+11 6.456973e+11 1.682913e+11 0
phaitranhadautuvetienguigiaodichck 1.466160e+11 2.039152e+11 3.474821e+11 3.583890e+11 4.941751e+11 6.388127e+11 1.744514e+11 0
phaitracotucgocvalaitraiphieu 2.110166e+09 3.221594e+09 4.229462e+09 4.894996e+09 6.126037e+09 1.060884e+10 2.546917e+09 0
congdoanhthuhoatdong 1.336001e+11 1.821239e+11 2.144384e+11 2.633149e+11 3.658751e+11 4.128969e+11 1.072867e+11 0
congchiphihoatdong 1.648053e+10 8.666343e+10 8.666343e+10 8.334531e+10 8.666343e+10 1.236652e+11 2.880097e+10 0
congdoanhthuhoatdongtaichinh 1.010284e+09 1.233290e+09 1.268236e+09 1.267272e+09 1.323993e+09 1.502188e+09 1.265984e+08 0
congchiphitaichinh 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0
chiphiquanlycongtick 8.149509e+10 1.019987e+11 1.019987e+11 1.009112e+11 1.019987e+11 1.116268e+11 7.588464e+09 0
tongloinhuanketoantruocthue 8.159409e+10 8.977992e+10 1.031564e+11 1.213460e+11 1.566557e+11 1.823100e+11 4.032831e+10 0
chiphithuetndn 2.601009e+10 3.406556e+10 3.406556e+10 3.416382e+10 3.406556e+10 4.184212e+10 3.792809e+09 0
loinhuanketoansauthuetndn 6.527527e+10 7.182394e+10 8.252509e+10 9.735199e+10 1.255624e+11 1.463428e+11 3.271667e+10 0
tonglotoandienkhacsauthuetndn 0.000000e+00 2.880644e+10 3.057837e+10 2.724987e+10 3.235030e+10 5.024653e+10 1.572183e+10 0
laicobantrencophieu 3.090000e+02 3.398750e+02 3.905000e+02 4.578500e+02 5.847500e+02 6.900000e+02 1.505876e+02 0
luuchuyentienthuantuhoatdongkinhdoanh 1.547385e+10 1.343353e+11 1.568068e+11 1.723637e+11 1.792784e+11 3.952640e+11 1.191231e+11 0
tienvacackhoantuongduongtiendaunam 2.493602e+10 1.175138e+11 1.986619e+11 2.264971e+11 3.407668e+11 5.188041e+11 1.636828e+11 0
tienvacackhoantuongduongtiencuoinam 2.493602e+10 1.175138e+11 1.986619e+11 2.531605e+11 4.165248e+11 5.632835e+11 1.982943e+11 0
ROA 3.012670e-02 3.571580e-02 3.786480e-02 4.022780e-02 4.641310e-02 5.216860e-02 7.507500e-03 0
ROE 3.366940e-02 3.644430e-02 4.317130e-02 4.506230e-02 5.401340e-02 5.864700e-02 1.001640e-02 0

Giải thích kĩ thuật:

1-3. Nạp các gói dplyr, tidyr, knitr để xử lý dữ liệu và hiển thị bảng đẹp. 4-26. Xác định danh sách các biến số định lượng (numeric_vars) cần thống kê mô tả. 27-29. Chọn các biến số từ dl và tính các thống kê mô tả:

  • Min, Q1, Median, Mean, Q3, Max, SD (độ lệch chuẩn), NA_count (số giá trị thiếu). 30-39. Sử dụng across() với hàm ẩn danh để áp dụng các phép tính cho tất cả các biến. .names = "{col}_{fn}" tạo tên cột mới. 40-44. Chuyển dữ liệu từ dạng “rộng” sang “dài” (pivot_longer), sau đó quay lại dạng rộng (pivot_wider) để mỗi biến là một hàng, các thống kê là các cột.
  1. Hiển thị bảng mô tả đẹp với kable.

Nhận xét

  • Biến TTS dao động từ 1.66×10¹² đến 3.47×10¹², trung bình 2.41×10¹², phản ánh quy mô tổng tài sản tăng trưởng ổn định qua các năm. Độ lệch chuẩn 5.92×10¹¹ cho thấy biến động vừa phải trong quy mô tài sản.

  • Biến Nophaitra từ 1.20×10¹⁰ đến 5.68×10¹¹, trung bình 1.95×10¹¹, thể hiện khả năng nợ phải trả có sự dao động lớn, có thể do biến động hoạt động kinh doanh và nghĩa vụ tài chính.

  • Biến VCSH trung bình 2.13×10¹², dao động từ 1.65×10¹² đến 2.50×10¹², phản ánh khả năng tự chủ tài chính ổn định, độ lệch chuẩn 3.22×10¹¹ cho thấy sự thay đổi vừa phải giữa các năm.

  • Biến Tienguicuakhachhang trung bình 3.49×10¹¹, dao động từ 1.50×10¹¹ đến 6.46×10¹¹, thể hiện xu hướng huy động vốn tăng trưởng, độ lệch chuẩn 1.68×10¹¹ phản ánh biến động đáng kể trong hoạt động huy động.

  • Biến Phaitranhadautuvetienguigiaodichck trung bình 3.58×10¹¹, dao động từ 1.47×10¹¹ đến 6.39×10¹¹, cho thấy hoạt động đầu tư tích cực, đồng thời biến động lớn giữa các năm (SD = 1.74×10¹¹).

  • Biến Congdoanhthuhoatdong trung bình 2.63×10¹¹, dao động từ 1.34×10¹¹ đến 4.13×10¹¹, phản ánh tăng trưởng doanh thu hoạt động kinh doanh, độ lệch chuẩn 1.07×10¹¹.

  • Biến Congchiphihoatdong dao động từ 1.65×10¹⁰ đến 1.24×10¹¹, trung bình 8.33×10¹⁰, phản ánh chi phí hoạt động ổn định nhưng có biến động cao (SD = 2.88×10¹⁰).

  • Biến Loinhuanketoansauthuetndn dao động từ 6.53×10¹⁰ đến 1.46×10¹¹, trung bình 9.74×10¹⁰, cho thấy lợi nhuận sau thuế tăng trưởng nhưng có biến động đáng kể, SD 3.27×10¹⁰.

  • Biến Laicobantrencophieu dao động từ 309 đến 690, trung bình 457.85, phản ánh kết quả sinh lời trên cổ phiếu tương đối ổn định, SD 150.59.

  • Biến Luuchuyentienthuantuhoatdongkinhdoanh dao động từ 1.55×10¹⁰ đến 3.95×10¹¹, trung bình 1.72×10¹¹, thể hiện dòng tiền từ hoạt động kinh doanh biến động mạnh, SD 1.19×10¹¹.

  • Biến Tienvacackhoantuongduongtiendaunam và Tienvacackhoantuongduongtiencuoinam lần lượt trung bình 2.26×10¹¹ và 2.53×10¹¹, dao động tương ứng 2.49×10¹⁰ – 5.19×10¹¹ và 2.49×10¹⁰ – 5.63×10¹¹, phản ánh khối lượng tiền mặt và các khoản tương đương tăng trưởng và biến động lớn.

  • Biến ROA dao động từ 0.0301 đến 0.0522, trung bình 0.0402, phản ánh hiệu quả sinh lợi trên tổng tài sản ổn định với biến động vừa phải (SD = 0.0075).

  • Biến ROE dao động từ 0.0337 đến 0.0586, trung bình 0.0451, cho thấy khả năng sinh lợi trên vốn chủ sở hữu tăng trưởng ổn định, SD 0.0100.

Tổng thể:

Các biến tài chính quan trọng thể hiện xu hướng tăng trưởng ổn định về quy mô tài sản, vốn chủ sở hữu, doanh thu, lợi nhuận và dòng tiền, đồng thời có một số biến động đáng kể ở các chỉ số thanh khoản và dòng tiền.

2.3.2. Phân tích tăng trưởng và biến động các chỉ tiêu tài chính

1. cagr <- function(x) {
2.   n <- length(x)
3.   (last(x)/first(x))^(1/(n-1)) - 1
4. }
5. summary_table <- dl %>%
6.   summarise(
7.     tts_cagr = cagr(tts),
8.     vcsh_cagr = cagr(vcsh),
9.     lnsk_cagr = cagr(loinhuanketoansauthuetndn),
10.     tts_sd = sd(tts, na.rm = TRUE),
11.     vcsh_sd = sd(vcsh, na.rm = TRUE),
12.     lnsk_sd = sd(loinhuanketoansauthuetndn, na.rm = TRUE),
13.     roe_range = max(ROE, na.rm = TRUE) - min(ROE, na.rm = TRUE),
14.     roa_range = max(ROA, na.rm = TRUE) - min(ROA, na.rm = TRUE),
15.     finance_ratio_mean = mean(congchiphitaichinh / congdoanhthuhoatdong, na.rm = TRUE),
16.     ros_mean = mean(loinhuanketoansauthuetndn / congdoanhthuhoatdong, na.rm = TRUE),
17.     cash_ratio_mean = mean(tienvacackhoantuongduongtiencuoinam / tts, na.rm = TRUE),
18.     effective_tax_mean = mean(chiphithuetndn / tongloinhuanketoantruocthue, na.rm = TRUE),
19.     cor_profit_cashflow = cor(loinhuanketoansauthuetndn, luuchuyentienthuantuhoatdongkinhdoanh, use = "complete.obs"),
20.     cor_roe_tts = cor(ROE, tts, use = "complete.obs"),
21.     cor_tts_vcsh = cor(tts, vcsh, use = "complete.obs")
22.   )
23. summary_table_long <- summary_table %>%
24.   pivot_longer(cols = everything(), names_to = "Metric", values_to = "Value")
25. kable(summary_table_long, digits = 4)
Metric Value
tts_cagr 4.980000e-02
vcsh_cagr 1.940000e-02
lnsk_cagr 5.630000e-02
tts_sd 5.923027e+11
vcsh_sd 3.224270e+11
lnsk_sd 3.271667e+10
roe_range 2.500000e-02
roa_range 2.200000e-02
finance_ratio_mean 0.000000e+00
ros_mean 3.939000e-01
cash_ratio_mean 1.024000e-01
effective_tax_mean 3.072000e-01
cor_profit_cashflow 3.584000e-01
cor_roe_tts 5.776000e-01
cor_tts_vcsh 9.352000e-01

Giải thích kĩ thuật:

1-4. Định nghĩa hàm cagr() tính tốc độ tăng trưởng kép (CAGR) cho một chuỗi số liệu: (giá trị cuối / giá trị đầu)^(1/(n-1)) - 1. 5-22. Tạo bảng summary_table tính các chỉ số tổng hợp của các biến tài chính:

  • *_cagr: CAGR của các biến tts, vcsh, loinhuanketoansauthuetndn.
  • *_sd: Độ lệch chuẩn của các biến tương ứng.
  • roe_range, roa_range: Khoảng dao động của ROE và ROA.
  • finance_ratio_mean: Tỷ lệ chi phí tài chính / doanh thu trung bình.
  • ros_mean: Tỷ suất lợi nhuận trên doanh thu trung bình.
  • cash_ratio_mean: Tỷ lệ tiền mặt / tổng tài sản trung bình.
  • effective_tax_mean: Tỷ lệ thuế hiệu quả trung bình.
  • cor_*: Hệ số tương quan Pearson giữa các cặp biến quan trọng. 23-24. Chuyển bảng từ dạng rộng sang dạng dài (pivot_longer) để mỗi chỉ số là một hàng, dễ hiển thị.
  1. Hiển thị bảng với kable, làm tròn 4 chữ số.

###Nhận xét

Các chỉ tiêu tổng tài sản (TTS), vốn chủ sở hữu (VCSH) và lợi nhuận sau thuế (LNST) đều có mức tăng trưởng trung bình dương, lần lượt là 4,98%, 1,94% và 5,63% mỗi năm, phản ánh sự mở rộng tổng thể của hoạt động doanh nghiệp trong giai đoạn nghiên cứu. Độ lệch chuẩn của TTS (5,92×10¹¹) và VCSH (3,22×10¹¹) cho thấy tổng tài sản biến động mạnh hơn vốn chủ sở hữu, trong khi độ lệch chuẩn của LNST (3,27×10¹⁰) thấp hơn, cho thấy lợi nhuận ổn định hơn so với quy mô vốn.

Biên độ ROE và ROA lần lượt là 2,5% và 2,2%, cho thấy hiệu quả sử dụng vốn và tài sản không có biến động quá lớn giữa các năm. Tỷ lệ chi phí tài chính trên doanh thu gần như bằng 0, phản ánh chi phí tài chính không đáng kể. Tỷ lệ lợi nhuận ròng/doanh thu (ROS) trung bình đạt 39,4%, thể hiện hiệu quả sinh lợi cao so với doanh thu. Tỷ lệ tiền cuối năm/tổng tài sản trung bình là 10,24%, cho thấy doanh nghiệp duy trì mức thanh khoản tương đối an toàn. Thuế suất thực tế trung bình là 30,7%, tương đối phù hợp với mức thuế suất doanh nghiệp áp dụng tại Việt Nam.

Các hệ số tương quan cũng cung cấp thông tin quan trọng: lợi nhuận và dòng tiền hoạt động có tương quan vừa phải (0,358), ROE và TTS có tương quan trung bình (0,578), trong khi TTS và VCSH tương quan rất cao (0,935), phản ánh mức độ sử dụng đòn bẩy tài chính ổn định.

Nhìn chung, các chỉ tiêu tài chính cho thấy doanh nghiệp tăng trưởng ổn định, lợi nhuận duy trì ở mức cao và quản lý dòng tiền hiệu quả, đồng thời sử dụng vốn chủ sở hữu và tổng tài sản một cách cân đối, minh chứng cho chiến lược tài chính ổn định và bền vững.

2.3.3. Phân tích mối quan hệ giữa các chỉ tiêu tài chính

1. vars_fin <- c(
2.   "tts",
3.   "vcsh",
4.   "loinhuanketoansauthuetndn",
5.   "congdoanhthuhoatdong",
6.   "congchiphihoatdong",
7.   "tongloinhuanketoantruocthue",
8.   "chiphithuetndn",
9.   "laicobantrencophieu",
10.   "luuchuyentienthuantuhoatdongkinhdoanh",
11.   "tienvacackhoantuongduongtiendaunam",
12.   "tienvacackhoantuongduongtiencuoinam",
13.   "ROA",
14.   "ROE"
15. )
16. cor_matrix <- dl %>%
17.   select(all_of(vars_fin)) %>%
18.   cor(use = "complete.obs")
19. kable(round(cor_matrix, 2))
tts vcsh loinhuanketoansauthuetndn congdoanhthuhoatdong congchiphihoatdong tongloinhuanketoantruocthue chiphithuetndn laicobantrencophieu luuchuyentienthuantuhoatdongkinhdoanh tienvacackhoantuongduongtiendaunam tienvacackhoantuongduongtiencuoinam ROA ROE
tts 1.00 0.94 0.82 0.93 0.01 0.83 0.22 0.82 0.26 0.24 0.50 0.13 0.58
vcsh 0.94 1.00 0.79 0.94 -0.12 0.79 0.33 0.79 0.35 -0.05 0.37 0.13 0.49
loinhuanketoansauthuetndn 0.82 0.79 1.00 0.75 0.30 1.00 0.07 1.00 0.36 0.20 0.60 0.66 0.92
congdoanhthuhoatdong 0.93 0.94 0.75 1.00 -0.14 0.75 0.51 0.75 0.15 0.03 0.24 0.09 0.47
congchiphihoatdong 0.01 -0.12 0.30 -0.14 1.00 0.30 -0.62 0.31 0.22 0.12 0.31 0.48 0.47
tongloinhuanketoantruocthue 0.83 0.79 1.00 0.75 0.30 1.00 0.07 1.00 0.35 0.21 0.61 0.65 0.92
chiphithuetndn 0.22 0.33 0.07 0.51 -0.62 0.07 1.00 0.06 -0.46 -0.33 -0.57 -0.14 -0.09
laicobantrencophieu 0.82 0.79 1.00 0.75 0.31 1.00 0.06 1.00 0.37 0.19 0.60 0.66 0.92
luuchuyentienthuantuhoatdongkinhdoanh 0.26 0.35 0.36 0.15 0.22 0.35 -0.46 0.37 1.00 0.12 0.56 0.26 0.29
tienvacackhoantuongduongtiendaunam 0.24 -0.05 0.20 0.03 0.12 0.21 -0.33 0.19 0.12 1.00 0.58 0.14 0.35
tienvacackhoantuongduongtiencuoinam 0.50 0.37 0.60 0.24 0.31 0.61 -0.57 0.60 0.56 0.58 1.00 0.38 0.61
ROA 0.13 0.13 0.66 0.09 0.48 0.65 -0.14 0.66 0.26 0.14 0.38 1.00 0.86
ROE 0.58 0.49 0.92 0.47 0.47 0.92 -0.09 0.92 0.29 0.35 0.61 0.86 1.00

Giải thích kĩ thuật:

1-15. Xác định các biến tài chính quan trọng vars_fin để phân tích tương quan.

16-18. Tạo ma trận tương quan Pearson giữa các biến trong vars_fin bằng hàm cor(), bỏ qua các giá trị thiếu (use = "complete.obs").

  1. Hiển thị ma trận tương quan dưới dạng bảng HTML với kable, làm tròn các hệ số tương quan đến 2 chữ số thập phân.

###Nhận xét - Tổng tài sản (TTS) và vốn chủ sở hữu (VCSH) có tương quan rất cao (0.94), cho thấy vốn chủ sở hữu tăng gần tương ứng với tổng tài sản, phản ánh mức độ đòn bẩy ổn định.

  • Lợi nhuận sau thuế (LN SK) tương quan mạnh với TTS (0.82), VCSH (0.79) và ROE (0.92), phản ánh lợi nhuận chủ yếu phụ thuộc vào vốn bỏ ra và hiệu quả sử dụng vốn.

  • Doanh thu hoạt động (DOHD) tương quan cao với TTS (0.93) và VCSH (0.94), nghĩa là quy mô tài sản và vốn ảnh hưởng lớn tới doanh thu.

  • Chi phí hoạt động (CPHD) gần như không liên quan tới TTS (0.01) và có tương quan âm với VCSH (-0.12), cho thấy chi phí hoạt động không tăng tỉ lệ thuận với tổng tài sản hay vốn.

  • Tổng lợi nhuận trước thuế (LNTT) và LN SK đều có tương quan rất cao với nhau (≈1.00) và với TTS/VCSH/DOHD, phản ánh chi phí thuế ít làm thay đổi xu hướng lợi nhuận.

  • Chi phí thuế TNDN có tương quan âm với dòng tiền hoạt động và tiền cuối năm, gợi ý chi phí thuế cao có thể làm giảm thanh khoản.

  • Lưu chuyển tiền thuần từ HĐKD có tương quan vừa phải với lợi nhuận sau thuế (0.36), cho thấy doanh thu thực tế tạo dòng tiền nhưng không hoàn toàn quyết định lợi nhuận.

  • Tiền và các khoản tương đương đầu năm/cuối năm có tương quan cao với nhau (0.58–0.61) và vừa phải với LN SK, ROE, ROA, cho thấy vốn lưu động tăng theo lợi nhuận và hiệu quả sử dụng vốn.

  • ROA và ROE có tương quan cao (0.86), phù hợp lý thuyết: ROE chịu ảnh hưởng trực tiếp từ ROA và đòn bẩy tài chính.

  • Các chỉ tiêu liên quan tới lợi nhuận cơ bản trên cổ phiếu, EPS có tương quan mạnh với LN SK, ROE, ROA, phản ánh giá trị cổ đông gắn với hiệu quả kinh doanh.

###Tóm lại: Ma trận tương quan cho thấy vốn và tổng tài sản quyết định doanh thu, lợi nhuận, trong khi dòng tiền và chi phí tài chính ảnh hưởng vừa phải tới lợi nhuận. ROE, ROA phản ánh chính xác hiệu quả sử dụng vốn, các chỉ tiêu liên quan tới cổ đông (EPS, LNST) theo sát lợi nhuận.

2.3.4. Phân tích dòng tiền và khả năng thanh toán

1. library(dplyr)
2. library(knitr)
3. dl_cash <- dl %>%
4.   mutate(
5.     cash_ratio = tienvacackhoantuongduongtiencuoinam / tts,
6.     cash_growth = (tienvacackhoantuongduongtiencuoinam - tienvacackhoantuongduongtiendaunam) / tienvacackhoantuongduongtiendaunam
7.   ) %>%
8.   select(tienvacackhoantuongduongtiendaunam, tienvacackhoantuongduongtiencuoinam,
9.          luuchuyentienthuantuhoatdongkinhdoanh, cash_ratio, cash_growth)
10. kable(dl_cash, digits = 3)
tienvacackhoantuongduongtiendaunam tienvacackhoantuongduongtiencuoinam luuchuyentienthuantuhoatdongkinhdoanh cash_ratio cash_growth
352894496064 563283466920 395264009771 0.251 0.596
397659713908 193070216107 156806841168 0.116 -0.514
193070216107 204253663739 15473850359 0.115 0.058
204253663739 24936023211 156806841168 0.013 -0.878
24936023211 114169343924 126844761815 0.054 3.578
114169343924 127547018813 28243322889 0.056 0.117
127547018813 27252728468 156806841168 0.010 -0.786
27252728468 304383714170 343814906204 0.109 10.169
304383714170 518804105546 186768920520 0.169 0.704
518804105546 453905187634 156806841168 0.131 -0.125

Giải thích kĩ thuật:

1-2. Nạp gói dplyr để thao tác dữ liệu và knitr để hiển thị bảng HTML/R Markdown.

3-7. Tạo bảng dl_cash:

  • cash_ratio: Tỷ lệ tiền và các khoản tương đương tiền trên tổng tài sản.
  • cash_growth: Tốc độ tăng trưởng tiền và các khoản tương đương tiền theo năm.

8-9. Chọn các cột liên quan: số dư đầu năm, cuối năm, lưu chuyển tiền thuần từ hoạt động kinh doanh, tỷ lệ tiền mặt, tốc độ tăng trưởng.

  1. Hiển thị bảng dl_cash với kable, làm tròn 3 chữ số thập phân.

###Nhận xét: Dữ liệu dòng tiền và khả năng thanh toán cho thấy một số đặc điểm nổi bật:

  • Tổng quan dòng tiền: Dòng tiền từ hoạt động kinh doanh (luuchuyentienthuantuhoatdongkinhdoanh) dao động khá lớn, từ 15,4 tỷ đến gần 395 tỷ, phản ánh sự biến động mạnh của hoạt động vận hành. Một số năm ghi nhận dòng tiền âm so với tổng tài sản, có khả năng do chi phí hoặc đầu tư lớn.

  • Khả năng thanh toán: Tỷ lệ tiền mặt cuối năm trên tổng tài sản (cash_ratio) tương đối thấp, phổ biến dưới 15%, chỉ có năm đầu tiên là 25%. Điều này cho thấy doanh nghiệp duy trì một phần tiền mặt hạn chế so với tổng tài sản, có thể tập trung vốn vào đầu tư hoặc các tài sản khác.

  • Tăng trưởng tiền mặt: Tăng trưởng tiền mặt (cash_growth) biến động mạnh, từ -87,8% đến 10,169 lần. Một số năm có mức tăng trưởng cực đoan (ví dụ 10,169 lần) phản ánh sự thay đổi đột ngột trong lượng tiền mặt, có thể do nhập vốn lớn hoặc thoái vốn, cần xem xét nguyên nhân chi tiết.

###Nhận xét chung: Do sự biến động dòng tiền và khả năng thanh toán không đồng đều, doanh nghiệp có thể gặp rủi ro thanh khoản trong một số năm. Tuy nhiên, các năm có dòng tiền dương mạnh và tỷ lệ tiền mặt hợp lý cho thấy doanh nghiệp vẫn duy trì khả năng đáp ứng nghĩa vụ ngắn hạn nếu quản lý tốt.

2.3.5. Phân tích hiệu quả sử dụng vốn và tài sản

1. df_efficiency <- dl %>%
2.   select(tts, vcsh, loinhuanketoansauthuetndn, ROA, ROE)
3. efficiency_summary <- df_efficiency %>%
4.   summarise(
5.     ROA_mean = mean(ROA),
6.     ROA_min = min(ROA),
7.     ROA_max = max(ROA),
8.     ROA_sd = sd(ROA),
9.     ROE_mean = mean(ROE),
10.     ROE_min = min(ROE),
11.     ROE_max = max(ROE),
12.     ROE_sd = sd(ROE),
13.     tts_mean = mean(tts),
14.     vcsh_mean = mean(vcsh),
15.     profit_mean = mean(loinhuanketoansauthuetndn)
16.   )
17. cagr <- function(x) {
18.   (last(x)/first(x))^(1/(length(x)-1)) - 1
19. }
20. efficiency_cagr <- df_efficiency %>%
21.   summarise(
22.     tts_cagr = cagr(tts),
23.     vcsh_cagr = cagr(vcsh),
24.     profit_cagr = cagr(loinhuanketoansauthuetndn)
25.   )
26. efficiency_table <- bind_cols(efficiency_summary, efficiency_cagr)
27. kable(efficiency_table, digits = 4, format = "markdown", 
28.       col.names = c("ROA_mean", "ROA_min", "ROA_max", "ROA_sd",
29.                     "ROE_mean", "ROE_min", "ROE_max", "ROE_sd",
30.                     "TTS_mean", "VCSH_mean", "Profit_mean",
31.                     "TTS_CAGR", "VCSH_CAGR", "Profit_CAGR"))
ROA_mean ROA_min ROA_max ROA_sd ROE_mean ROE_min ROE_max ROE_sd TTS_mean VCSH_mean Profit_mean TTS_CAGR VCSH_CAGR Profit_CAGR
0.0402 0.0301 0.0522 0.0075 0.0451 0.0337 0.0586 0.01 2.407331e+12 2.128589e+12 97351986529 0.0498 0.0194 0.0563

Giải thích kĩ thuật:

1-2. Tạo bảng df_efficiency chọn các cột liên quan đến hiệu quả tài chính: tổng tài sản (tts), vốn cổ phần chủ sở hữu (vcsh), lợi nhuận sau thuế (loinhuanketoansauthuetndn), ROA và ROE.

3-16. Tóm tắt số liệu thống kê với summarise:

  • ROA và ROE: tính mean, min, max, sd.
  • TTS, VCSH, Profit: tính mean.

17-19. Định nghĩa hàm cagr() để tính tốc độ tăng trưởng kép hàng năm.

20-25. Tính CAGR cho TTS, VCSH và lợi nhuận (profit).

  1. Ghép bảng tóm tắt thống kê và CAGR bằng bind_cols.

27-31. Hiển thị bảng cuối cùng bằng kable, làm tròn 4 chữ số, đặt nhãn cột rõ ràng.

###Nhận xét - ROA (Lợi nhuận trên tổng tài sản): Giá trị trung bình ROA là 4.02%, dao động từ 3.01% đến 5.22%, với độ lệch chuẩn 0.75%. Điều này cho thấy hiệu quả sử dụng tổng tài sản của các doanh nghiệp trong mẫu nghiên cứu khá ổn định, biến động không quá lớn.

  • ROE (Lợi nhuận trên vốn chủ sở hữu): ROE trung bình là 4.51%, với khoảng dao động từ 3.37% đến 5.86% và độ lệch chuẩn 1.0%. Biến động ROE lớn hơn ROA, phản ánh sự ảnh hưởng của cơ cấu vốn (đòn bẩy tài chính) đến lợi nhuận trên vốn chủ sở hữu.

  • Tổng tài sản (TTS) và vốn chủ sở hữu (VCSH): TTS trung bình đạt khoảng 2,41 nghìn tỷ đồng, VCSH trung bình khoảng 2,13 nghìn tỷ đồng. Điều này cho thấy các doanh nghiệp sử dụng vốn chủ sở hữu ở mức vừa phải so với tổng tài sản, phù hợp với mức đòn bẩy trung bình.

  • Lợi nhuận sau thuế (Profit_mean): Trung bình khoảng 97,35 tỷ đồng. Mức lợi nhuận này phản ánh khả năng sinh lời thực tế trên quy mô tài sản và vốn chủ sở hữu của doanh nghiệp.

  • Tăng trưởng trung bình năm (CAGR): TTS tăng trưởng trung bình 4.98%/năm, VCSH tăng trưởng 1.94%/năm, lợi nhuận sau thuế tăng 5.63%/năm. Nhìn chung, doanh nghiệp duy trì tốc độ tăng trưởng lợi nhuận cao hơn tăng trưởng vốn, điều này thể hiện khả năng cải thiện hiệu quả sử dụng vốn qua các năm.

  • Nhận xét tổng thể: Doanh nghiệp trong mẫu nghiên cứu có khả năng sinh lời và hiệu quả sử dụng vốn/tài sản ổn định, ROA biến động thấp hơn ROE, cho thấy đòn bẩy tài chính tác động đáng kể đến lợi nhuận trên vốn chủ sở hữu. Tăng trưởng lợi nhuận cao hơn tăng trưởng vốn chủ sở hữu chứng tỏ hiệu quả quản lý vốn và tài sản khá tốt.

2.3.6. Phân tích đòn bẩy tài chính

1. financial_leverage <- dl %>%
2.   summarise(
3.     debt_equity_ratio = mean((tts - vcsh) / vcsh, na.rm = TRUE),
4.     debt_equity_min = min((tts - vcsh) / vcsh, na.rm = TRUE),
5.     debt_equity_max = max((tts - vcsh) / vcsh, na.rm = TRUE),
6.     debt_equity_sd  = sd((tts - vcsh) / vcsh, na.rm = TRUE)
7.   )
8. kable(financial_leverage, digits = 3)
debt_equity_ratio debt_equity_min debt_equity_max debt_equity_sd
0.119 0.007 0.421 0.125

Giải thích kĩ thuật:

1-2. Sử dụng summarise trên toàn bộ dữ liệu dl để tính các chỉ số đòn bẩy tài chính.

  1. debt_equity_ratio: Tính tỷ lệ nợ trên vốn chủ sở hữu trung bình, với công thức (TTS - VCSH)/VCSH.

4-5. debt_equity_mindebt_equity_max: Xác định giá trị tối thiểu và tối đa của tỷ lệ nợ trên vốn chủ sở hữu trong dữ liệu.

  1. debt_equity_sd: Tính độ lệch chuẩn của tỷ lệ nợ trên vốn chủ sở hữu, phản ánh sự phân tán giữa các công ty.

  2. kable(financial_leverage, digits = 3): Hiển thị bảng kết quả dạng HTML/Markdown, làm tròn 3 chữ số.

Cổ phiếu ngân hàng Agribank cho thấy tỷ lệ đòn bẩy tài chính trung bình là 0,119, tức phần nợ chỉ chiếm khoảng 11,9% so với vốn chủ sở hữu, phản ánh ngân hàng chủ yếu tài trợ bằng vốn tự có. Giá trị tối thiểu 0,007 cho thấy có những giai đoạn gần như không sử dụng nợ, trong khi tối đa 0,421 cho thấy ngân hàng đôi lúc tăng vay để tài trợ hoạt động, nhưng vẫn trong mức kiểm soát. Độ lệch chuẩn 0,125 chỉ ra rằng mức biến động của đòn bẩy là vừa phải, cấu trúc vốn của Agribank khá ổn định, hạn chế rủi ro tài chính.

2.3.7. Phân tích rủi ro và biến động lợi nhuận

1. profit_vars <- c("tongloinhuanketoantruocthue", "loinhuanketoansauthuetndn")
2. risk_analysis <- dl %>%
3.   select(all_of(profit_vars)) %>%
4.   summarise(across(everything(),
5.                    list(mean = ~mean(.x, na.rm = TRUE),
6.                         sd   = ~sd(.x, na.rm = TRUE),
7.                         cv   = ~sd(.x, na.rm = TRUE)/mean(.x, na.rm = TRUE)))) %>%
8.   pivot_longer(everything(),
9.                names_to = c("metric", "stat"),
10.                names_sep = "_",
11.                values_to = "value") %>%
12.   pivot_wider(names_from = stat, values_from = value)
13. kable(risk_analysis, digits = 4)
metric mean sd cv
tongloinhuanketoantruocthue 121345957617 40328312908 0.3323
loinhuanketoansauthuetndn 97351986529 32716668615 0.3361

Giải thích kĩ thuật:

  1. profit_vars <- c(...): Xác định các biến liên quan đến lợi nhuận cần phân tích rủi ro.

2-3. Chọn các biến lợi nhuận từ bộ dữ liệu dl.

4-7. summarise(across(...)): Tính các thống kê cơ bản cho từng biến:

  • mean: giá trị trung bình,
  • sd: độ lệch chuẩn,
  • cv: hệ số biến động (coefficient of variation = SD / Mean), thể hiện mức độ rủi ro so với trung bình.

8-12. Chuyển dữ liệu từ dạng rộng sang dạng dài (pivot_longer) rồi quay lại dạng rộng (pivot_wider) để dễ trình bày bảng thống kê.

  1. kable(risk_analysis, digits = 4): Hiển thị bảng kết quả với 4 chữ số thập phân, trình bày trực quan trong R Markdown/HTML.

###Nhận xét:

  • Biến tongloinhuanketoantruocthue có giá trị trung bình 121,35 tỷ đồng, độ lệch chuẩn 40,33 tỷ đồng, hệ số biến thiên (CV) 0,3323. Điều này phản ánh biến động đáng kể của lợi nhuận trước thuế, với mức biến động khoảng 33% so với giá trị trung bình, cho thấy rủi ro lợi nhuận cao.
  • Biến loinhuanketoansauthuetndn có giá trị trung bình 97,35 tỷ đồng, độ lệch chuẩn 32,72 tỷ đồng, CV 0,3361. Hệ số biến thiên tương đương (~33,6%) chỉ ra lợi nhuận sau thuế cũng biến động mạnh, gần bằng mức biến động của lợi nhuận trước thuế, cho thấy thuế TNDN chưa làm giảm đáng kể sự biến động lợi nhuận.
  • Tổng thể, cả hai chỉ tiêu lợi nhuận đều có mức biến động cao, cảnh báo về rủi ro hoạt động kinh doanh và khả năng thu nhập không ổn định. Sự khác biệt trung bình giữa lợi nhuận trước và sau thuế khoảng 24 tỷ đồng, phản ánh tác động vừa phải của thuế đối với lợi nhuận cuối kỳ.

2.3.8. So sánh chỉ tiêu tài chính qua các giai đoạn

1. finance_vars <- dl %>%
2.   select(year, tts, vcsh, loinhuanketoansauthuetndn, congdoanhthuhoatdong,
3.          congchiphihoatdong, tongloinhuanketoantruocthue, chiphithuetndn,
4.          laicobantrencophieu, ROA, ROE)
5. finance_summary <- finance_vars %>%
6.   group_by(year) %>%
7.   summarise(
8.     TTS = sum(tts, na.rm = TRUE),
9.     VCSH = sum(vcsh, na.rm = TRUE),
10.     LNST = sum(loinhuanketoansauthuetndn, na.rm = TRUE),
11.     DoanhThu = sum(congdoanhthuhoatdong, na.rm = TRUE),
12.     ChiPhi = sum(congchiphihoatdong, na.rm = TRUE),
13.     LTKTruocThue = sum(tongloinhuanketoantruocthue, na.rm = TRUE),
14.     CPThue = sum(chiphithuetndn, na.rm = TRUE),
15.     LCB = sum(laicobantrencophieu, na.rm = TRUE),
16.     ROA = mean(ROA, na.rm = TRUE),
17.     ROE = mean(ROE, na.rm = TRUE)
18.   )
19. kable(finance_summary, digits = 2)
year TTS VCSH LNST DoanhThu ChiPhi LTKTruocThue CPThue LCB ROA ROE
2015 2.242172e+12 2.054063e+12 82525087441 168786511613 86663425631 103156359302 26010087063 390.5 0.04 0.04
2016 1.660379e+12 1.648335e+12 82525087441 133600086761 86663425631 103156359302 34065564109 390.5 0.05 0.05
2017 1.776671e+12 1.763858e+12 65275274846 184849879214 86663425631 81594093557 34065564109 309.0 0.04 0.04
2018 1.917072e+12 1.870422e+12 67815231463 181215198157 86663425631 84821567306 34065564109 321.0 0.04 0.04
2019 2.111309e+12 1.966895e+12 68256885192 204080452867 86663425631 85321106490 34065564109 323.0 0.03 0.03
2020 2.286421e+12 2.096629e+12 96793289690 224796382907 86663425631 120991612113 34065564109 458.0 0.04 0.05
2021 2.739269e+12 2.451041e+12 82525087441 394171751184 16480534320 103156359302 41842117681 390.5 0.03 0.03
2022 2.805191e+12 2.497221e+12 146342776418 367374466987 123665182999 180408340527 34065564109 690.0 0.05 0.06
2023 3.062598e+12 2.494739e+12 146309079421 361377046270 61357373934 182309995663 36000916242 679.0 0.05 0.06
2024 3.472227e+12 2.442688e+12 135152065932 412896892995 111969477327 168543782608 33391716676 627.0 0.04 0.06

Giải thích kĩ thuật:

1-4. Chọn các biến tài chính quan trọng từ bộ dữ liệu dl, bao gồm tổng tài sản, vốn cổ phần, lợi nhuận sau thuế, doanh thu, chi phí, ROA, ROE,…

5-18. finance_vars %>% group_by(year) %>% summarise(...):

  • Nhóm dữ liệu theo từng năm (year).
  • Tính tổng (sum) các biến tài chính định lượng như TTS, VCSH, LNST, doanh thu, chi phí, lợi nhuận trước thuế, chi phí thuế, lãi cơ bản trên cổ phiếu.
  • Tính trung bình (mean) các chỉ số hiệu quả như ROA và ROE.
  • na.rm = TRUE đảm bảo bỏ qua các giá trị thiếu khi tính toán.
  1. kable(finance_summary, digits = 2): Hiển thị bảng tổng hợp tài chính theo năm, làm tròn 2 chữ số thập phân, trực quan trong R Markdown/HTML.

###Nhận xét:

Nhìn vào bảng các chỉ tiêu tài chính của Agribank từ 2015 đến 2024, có thể rút ra một số nhận xét trực quan như sau:

  • Tổng tài sản (TTS) và vốn chủ sở hữu (VCSH) có xu hướng tăng đều qua các năm, cho thấy ngân hàng liên tục mở rộng quy mô tài sản và tăng vốn tự có. Tuy nhiên, tốc độ tăng không đồng đều, đặc biệt TTS tăng mạnh từ 2020 trở đi.

  • Lợi nhuận sau thuế (LNST) dao động, năm 2020 và 2022 đạt mức cao nhất trong giai đoạn này, trong khi các năm khác thấp hơn, phản ánh biến động trong hiệu quả kinh doanh.

  • Doanh thu hoạt động nhìn chung tăng, nhưng chi phí hoạt động (ChiPhi) vẫn giữ mức tương đối ổn định, dẫn đến lợi nhuận kế toán trước thuế (LTKTruocThue) biến động theo lợi nhuận thuần.

  • Chi phí thuế TNDN (CPThue) và lãi cơ bản trên cổ phiếu (LCB) cũng dao động, phản ánh sự thay đổi trong lợi nhuận và cơ cấu vốn.

  • ROA ổn định quanh 0.03–0.05, trong khi ROE dao động từ 0.03 đến 0.06, cho thấy hiệu quả sử dụng vốn cổ đông không quá cao nhưng duy trì ổn định.

  • Một số năm có giá trị LCB bất thường (ví dụ năm 2015, 2016, 2021 là 390–390.5), có thể là do cách tính lãi cơ bản hoặc điều chỉnh số liệu kế toán.

Tổng quan, Agribank duy trì tăng trưởng tài sản và vốn chủ sở hữu ổn định, nhưng biến động lợi nhuận và ROE cho thấy hiệu quả kinh doanh có lúc chưa đồng đều, cần lưu ý trong các quyết định quản lý vốn và rủi ro.

2.3.9. Xác định các năm đặc trưng về ROE, ROA và lợi nhuận

1. summary_table <- tibble(
2.   Chi_tieu = c("ROA", "ROE", "Lợi nhuận sau thuế"),
3.   Gia_tri_lon_nhat = c(scales::percent(max(dl$ROA, na.rm = TRUE)),
4.                        scales::percent(max(dl$ROE, na.rm = TRUE)),
5.                        scales::dollar(max(dl$loinhuanketoansauthuetndn, na.rm = TRUE))),
6.   Nam_lon_nhat = c(dl$year[which.max(dl$ROA)],
7.                    dl$year[which.max(dl$ROE)],
8.                    dl$year[which.max(dl$loinhuanketoansauthuetndn)]),
9.   Gia_tri_nho_nhat = c(scales::percent(min(dl$ROA, na.rm = TRUE)),
10.                        scales::percent(min(dl$ROE, na.rm = TRUE)),
11.                        scales::dollar(min(dl$loinhuanketoansauthuetndn, na.rm = TRUE))),
12.   Nam_nho_nhat = c(dl$year[which.min(dl$ROA)],
13.                    dl$year[which.min(dl$ROE)],
14.                    dl$year[which.min(dl$loinhuanketoansauthuetndn)])
15. )
16. kable(summary_table, 
17.       col.names = c("Chỉ tiêu", "Giá trị lớn nhất", "Năm lớn nhất", 
18.                     "Giá trị nhỏ nhất", "Năm nhỏ nhất"),
19.       align = "c")
Chỉ tiêu Giá trị lớn nhất Năm lớn nhất Giá trị nhỏ nhất Năm nhỏ nhất
ROA 5% 2022 3% 2021
ROE 6% 2023 3% 2021
Lợi nhuận sau thuế $146,342,776,418 2022 $65,275,274,846 2017

Giải thích kĩ thuật:

1-2. Tạo bảng summary_table dưới dạng tibble để tóm tắt các chỉ tiêu tài chính quan trọng: ROA, ROE, Lợi nhuận sau thuế.

3-5. Gia_tri_lon_nhat: Tính giá trị lớn nhất của từng chỉ tiêu.

  • scales::percent() dùng để hiển thị ROA, ROE dưới dạng phần trăm.
  • scales::dollar() dùng để hiển thị LNST dưới dạng tiền tệ.

6-8. Nam_lon_nhat: Xác định năm mà giá trị lớn nhất xảy ra với từng chỉ tiêu (which.max).

9-11. Gia_tri_nho_nhat: Tương tự, tính giá trị nhỏ nhất của từng chỉ tiêu.

12-14. Nam_nho_nhat: Xác định năm mà giá trị nhỏ nhất xảy ra (which.min).

16-19. kable(): Hiển thị bảng tóm tắt trong R Markdown/HTML, căn giữa các cột, với nhãn rõ ràng cho từng chỉ tiêu, giá trị lớn/nhỏ và năm tương ứng.

Nhận xét:

ROA (Tỷ suất lợi nhuận trên tài sản):

  • Năm 2022 đạt mức cao nhất 5,22%, phản ánh hiệu quả sử dụng tài sản trong năm này tốt nhất so với các năm khác.

  • Năm 2021 là thấp nhất 3,01%, cho thấy hiệu quả quản lý tài sản kém hơn.

ROE (Tỷ suất lợi nhuận trên vốn chủ sở hữu):

  • Năm 2023 đạt cao nhất 5,86%, tức là khả năng sinh lời trên vốn chủ sở hữu mạnh nhất.

  • Năm 2021 cũng là thấp nhất (3,37%), tương tự ROA, cho thấy năm này ngân hàng ít hiệu quả cả về tài sản lẫn vốn chủ sở hữu.

Lợi nhuận sau thuế:

  • Năm 2022 đạt mức cao nhất gần 146,3 tỷ đồng, gợi ý năm này ngân hàng có kết quả kinh doanh tốt nhất.

  • Năm 2017 thấp nhất chỉ khoảng 65,3 tỷ đồng, cho thấy năm này lợi nhuận chưa cao, có thể do doanh thu thấp hoặc chi phí lớn.

Nhận xét tổng quát:

Năm 2021 là năm cả ROA lẫn ROE đều thấp, trong khi năm 2022 và 2023 là những năm hiệu quả kinh doanh nổi bật.

Biểu hiện này phản ánh sự tăng trưởng dần của hiệu quả sử dụng vốn và tài sản trong các năm gần đây, nhưng vẫn có biến động đáng kể.

2.3.10. Kiểm tra các mối quan hệ quan trọng giữa lợi nhuận, dòng tiền và vốn chủ sở hữu

1. dl_rel <- dl %>%
2.   select(
3.     loinhuanketoansauthuetndn,                  
4.     luuchuyentienthuantuhoatdongkinhdoanh,     
5.     vcsh                                        
6.   )
7. cor_matrix <- round(cor(dl_rel, use = "complete.obs"), 3)
8. cor_df <- as.data.frame(cor_matrix)
9. cor_df <- cbind(Biến = rownames(cor_df), cor_df)
10. kable(cor_df, caption = "Ma trận tương quan giữa LNST, Dòng tiền HĐKD và Vốn chủ sở hữu")
Ma trận tương quan giữa LNST, Dòng tiền HĐKD và Vốn chủ sở hữu
Biến loinhuanketoansauthuetndn luuchuyentienthuantuhoatdongkinhdoanh vcsh
loinhuanketoansauthuetndn loinhuanketoansauthuetndn 1.000 0.358 0.794
luuchuyentienthuantuhoatdongkinhdoanh luuchuyentienthuantuhoatdongkinhdoanh 0.358 1.000 0.355
vcsh vcsh 0.794 0.355 1.000

Giải thích kĩ thuật:

1-6. Tạo bộ dữ liệu dl_rel chỉ chứa các biến quan tâm:

  • loinhuanketoansauthuetndn (LNST),
  • luuchuyentienthuantuhoatdongkinhdoanh (dòng tiền HĐKD),
  • vcsh (vốn chủ sở hữu).
  1. cor() tính ma trận tương quan Pearson giữa các biến trong dl_rel, dùng use = "complete.obs" để loại bỏ các quan sát thiếu. Kết quả được làm tròn 3 chữ số thập phân bằng round().

8-9. Chuyển ma trận tương quan thành data.frame và thêm cột tên biến (Biến) để hiển thị rõ ràng trong bảng.

  1. kable() hiển thị ma trận tương quan trong R Markdown/HTML với tiêu đề rõ ràng: “Ma trận tương quan giữa LNST, Dòng tiền HĐKD và Vốn chủ sở hữu”.

Nhìn vào ma trận tương quan này, có thể nhận xét như sau:

  • Lợi nhuận sau thuế – Vốn chủ sở hữu (0.794): Có mối tương quan khá mạnh, nghĩa là khi vốn chủ sở hữu tăng thì lợi nhuận sau thuế cũng có xu hướng tăng, hoặc ngược lại. Điều này phản ánh rằng quy mô vốn của ngân hàng liên quan chặt chẽ đến khả năng sinh lợi.

  • Lợi nhuận sau thuế – Dòng tiền HĐKD (0.358): Tương quan ở mức vừa phải, nghĩa là lợi nhuận và dòng tiền hoạt động kinh doanh có liên quan nhưng không hoàn toàn đồng biến. Có thể có các yếu tố khác (chi phí, nợ, đầu tư) ảnh hưởng đến dòng tiền mà không phản ánh trực tiếp lên lợi nhuận.

  • Dòng tiền HĐKD – Vốn chủ sở hữu (0.355): Tương quan thấp, cho thấy dòng tiền hoạt động kinh doanh không quá phụ thuộc trực tiếp vào quy mô vốn chủ sở hữu, tức ngân hàng vẫn có thể tạo dòng tiền từ hoạt động kinh doanh mà không cần tăng vốn ngay lập tức.

Tóm lại: Lợi nhuận sau thuế liên quan chặt với vốn chủ sở hữu, còn dòng tiền HĐKD có ảnh hưởng nhưng mức độ thấp hơn, phản ánh rằng ngân hàng có quản lý vốn và dòng tiền tương đối độc lập trong hoạt động.

2.3.11. Phân tích các chỉ tiêu có biến động lớn nhất và ảnh hưởng tới kết quả kinh doanh

1. library(dplyr)
2. library(scales)
3. library(knitr)
4. library(kableExtra)
5. top_cv <- dl %>%
6.   select(year, congdoanhthuhoatdong, congchiphihoatdong,
7.          loinhuanketoansauthuetndn, laicobantrencophieu, tts) %>%
8.   summarise(across(everything(), list(mean = mean, sd = sd))) %>%
9.   pivot_longer(cols = everything(),
10.                names_to = c("Chi_tieu", ".value"),
11.                names_sep = "_") %>%
12.   mutate(CV = sd / mean) %>%
13.   arrange(desc(CV)) %>%
14.   slice(1:5)  
15. top_cv_table <- top_cv %>%
16.   mutate(
17.     mean = scales::dollar(mean),
18.     sd   = scales::dollar(sd),
19.     CV   = scales::percent(CV, accuracy = 0.1)
20.   ) %>%
21.   rename(
22.     "Chỉ tiêu" = Chi_tieu,
23.     "Giá trị trung bình" = mean,
24.     "Độ lệch chuẩn" = sd,
25.     "Hệ số biến động" = CV
26.   )
27. top_cv_table %>%
28.   kable(caption = "Top 5 chỉ tiêu có biến động lớn nhất") %>%
29.   kable_styling(full_width = F, position = "center", bootstrap_options = c("striped", "hover")) %>%
30.   row_spec(0, bold = TRUE, background = "#D3D3D3")  
Top 5 chỉ tiêu có biến động lớn nhất
Chỉ tiêu Giá trị trung bình Độ lệch chuẩn Hệ số biến động
congdoanhthuhoatdong $263,314,866,896 $107,286,703,921 40.7%
congchiphihoatdong $83,345,312,236 $28,800,968,388 34.6%
loinhuanketoansauthuetndn $97,351,986,528 $32,716,668,615 33.6%
laicobantrencophieu $458 $151 32.9%
tts $2,407,331,036,071 $592,302,675,110 24.6%

Giải thích kĩ thuật:

1-4. Nạp các gói cần thiết:

  • dplyr để xử lý dữ liệu,
  • scales để định dạng tiền tệ và phần trăm,
  • knitrkableExtra để tạo bảng đẹp trong R Markdown/HTML.

5-14. Tạo bảng top_cv gồm các bước:

  • Chọn các biến quan trọng: year, congdoanhthuhoatdong, congchiphihoatdong, loinhuanketoansauthuetndn, laicobantrencophieu, tts.
  • Tính giá trị trung bình (mean) và độ lệch chuẩn (sd) của từng biến qua summarise(across(...)).
  • Chuyển dữ liệu từ dạng rộng sang dạng dài bằng pivot_longer(), tách tên biến và giá trị.
  • Tính hệ số biến động (CV) = sd / mean.
  • Sắp xếp giảm dần theo CV và chọn 5 biến có biến động lớn nhất bằng slice(1:5).

15-26. Chuẩn bị bảng hiển thị:

  • Chuyển meansd sang định dạng tiền tệ, CV sang phần trăm.
  • Đổi tên cột cho dễ đọc: “Chỉ tiêu”, “Giá trị trung bình”, “Độ lệch chuẩn”, “Hệ số biến động”.

27-30. Hiển thị bảng đẹp trong HTML/R Markdown:

  • kable() tạo bảng, thêm caption,
  • kable_styling() căn giữa, chọn kiểu “striped” và “hover”,
  • row_spec(0, bold = TRUE, background = "#D3D3D3") làm đậm và tô nền cho hàng tiêu đề.

Bảng trên cho thấy 5 chỉ tiêu tài chính có biến động lớn nhất trong giai đoạn nghiên cứu. Cụ thể, doanh thu hoạt động có hệ số biến động cao nhất (CV = 40,7%), cho thấy doanh thu thay đổi đáng kể giữa các năm, phản ánh sự dao động trong kết quả kinh doanh hoặc tác động của các yếu tố thị trường. Chi phí hoạt động (CV = 34,6%) và lợi nhuận sau thuế (CV = 33,6%) cũng biến động mạnh, tương ứng với biến động doanh thu, cho thấy khả năng sinh lời của doanh nghiệp chịu ảnh hưởng trực tiếp từ mức doanh thu và chi phí.

Hệ số biến động của lãi cơ bản trên cổ phiếu (EPS) là 32,9%, phản ánh sự biến động lợi nhuận phân bổ trên mỗi cổ phần, điều này có thể tác động đến đánh giá cổ đông và quyết định đầu tư. Trong khi đó, tổng tài sản (TTS) có CV thấp hơn (24,6%), cho thấy quy mô tài sản của doanh nghiệp ổn định hơn so với các chỉ tiêu kết quả kinh doanh, phản ánh tính chất dài hạn của tài sản so với các chỉ tiêu thu nhập và chi phí.

Nhìn chung, các chỉ tiêu biến động mạnh chủ yếu liên quan đến doanh thu, chi phí và lợi nhuận, là những yếu tố trực tiếp quyết định kết quả kinh doanh. Việc phân tích những biến động này giúp nhận diện rủi ro và cơ hội trong hoạt động tài chính của doanh nghiệp.

2.4 Trực quan hoá dữ liệu

Các biểu đồ trong mục 2.4 chỉ trực quan hóa kết quả đã được phân tích trong mục 2.3.

Do đó, không thực hiện nhận xét thêm ở mục này; biểu đồ nhằm giúp người đọc hình dung dễ hơn các xu hướng, phân bố và biến động của các biến.

2.4.1. Thống kê mô tả các biến quan trọng

1. library(ggplot2)
2. dl %>%
3.   select(all_of(numeric_vars)) %>%
4.   pivot_longer(cols = everything(), names_to = "Variable", values_to = "Value") %>%
5.   ggplot(aes(x = Variable, y = Value)) +
6.   geom_boxplot(fill = "lightblue", outlier.color = "red") +
7.   coord_flip() +
8.   labs(title = "Boxplot các chỉ tiêu tài chính",
9.        x = "Biến",
10.        y = "Giá trị") +
11.   theme_minimal() +
12.   theme(
13.     axis.text.y = element_text(size = 8),   
14.     axis.text.x = element_text(size = 8),   
15.     plot.title = element_text(size = 14, face = "bold")
16.   )

Giải thích kĩ thuật:

  1. library(ggplot2): Nạp gói ggplot2 để vẽ biểu đồ.

2-4. Chuyển dữ liệu sang dạng dài (long format) để phù hợp với ggplot:

5-6. Vẽ boxplot:

  1. coord_flip(): Đảo trục X và Y để biểu đồ nằm ngang, dễ đọc tên biến dài.

8-10. labs(title, x, y): Thêm tiêu đề và nhãn trục.

11-16. theme_minimal()theme():

2.4.2. Phân tích tăng trưởng và biến động các chỉ tiêu tài chính

1. library(dplyr)
2. library(tidyr)
3. library(ggplot2)
4. library(scales)
5. cagr <- function(x) {
6.   n <- length(x)
7.   (last(x)/first(x))^(1/(n-1)) - 1
8. }
9. summary_table <- dl %>%
10.   summarise(
11.     tts_cagr = cagr(tts),
12.     vcsh_cagr = cagr(vcsh),
13.     lnsk_cagr = cagr(loinhuanketoansauthuetndn),
14.     tts_sd = sd(tts, na.rm = TRUE),
15.     vcsh_sd = sd(vcsh, na.rm = TRUE),
16.     lnsk_sd = sd(loinhuanketoansauthuetndn, na.rm = TRUE),
17.     roe_range = max(ROE, na.rm = TRUE) - min(ROE, na.rm = TRUE),
18.     roa_range = max(ROA, na.rm = TRUE) - min(ROA, na.rm = TRUE)
19.   )
20. summary_long <- summary_table %>%
21.   pivot_longer(cols = everything(), names_to = "Metric", values_to = "Value") %>%
22.   mutate(
23.     Type = case_when(
24.       grepl("_cagr$", Metric) ~ "CAGR",
25.       grepl("_sd$|_range$", Metric) ~ "Biến động",
26.       TRUE ~ "Khác"
27.     ),
28.     Metric_clean = gsub("_cagr$|_sd$|_range$", "", Metric)
29.   )
30. ggplot(summary_long, aes(x = Metric_clean, y = Value, fill = Type)) +
31.   geom_col(position = "dodge") +
32.   geom_text(aes(label = ifelse(Type == "CAGR", percent(Value, accuracy = 0.1),
33.                                comma(round(Value, 0)))), 
34.             position = position_dodge(width = 0.9), vjust = -0.3, size = 3.5) +
35.   scale_y_continuous(labels = label_number(scale = 1e-9, suffix = "T")) 

r 1. labs(title = "Tăng trưởng và biến động các chỉ tiêu tài chính", 2. x = "Chỉ tiêu", 3. y = "Giá trị / Tỷ lệ") + 4. theme_minimal() + 5. theme( 6. axis.text.x = element_text(angle = 45, hjust = 1), 7. legend.position = "top", 8. plot.title = element_text(face = "bold", size = 14) 9. )

## NULL

Giải thích kĩ thuật:

1-4. Nạp các gói:

5-8. Định nghĩa hàm cagr(x) tính tốc độ tăng trưởng kép trung bình (CAGR) của một chuỗi số:

9-19. Tóm tắt dữ liệu:

20-29. Chuyển dữ liệu sang dạng dài (long) để vẽ ggplot:

30-35. Vẽ biểu đồ cột (column chart):

2.4.3. Phân tích mối quan hệ giữa các chỉ tiêu tài chính

1. library(reshape2)
2. library(ggplot2)
3. library(scales)
4. vars_fin <- c(
5.   "tts",
6.   "vcsh",
7.   "loinhuanketoansauthuetndn",
8.   "congdoanhthuhoatdong",
9.   "congchiphihoatdong",
10.   "tongloinhuanketoantruocthue",
11.   "chiphithuetndn",
12.   "laicobantrencophieu",
13.   "luuchuyentienthuantuhoatdongkinhdoanh",
14.   "tienvacackhoantuongduongtiendaunam",
15.   "tienvacackhoantuongduongtiencuoinam",
16.   "ROA",
17.   "ROE"
18. )
19. cor_matrix <- dl %>%
20.   select(all_of(vars_fin)) %>%
21.   cor(use = "complete.obs")
22. cor_long <- melt(cor_matrix, varnames = c("X", "Y"), value.name = "Correlation")
23. ggplot(cor_long, aes(x = X, y = Y, fill = Correlation)) +
24.   geom_tile(color = "white") +
25.   scale_fill_gradient2(low = "blue", mid = "white", high = "red", midpoint = 0, 
26.                        limit = c(-1, 1), space = "Lab", name = "Tương quan") +
27.   geom_text(aes(label = round(Correlation, 2)), color = "black", size = 3) +
28.   theme_minimal() +
29.   theme(
30.     axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1),
31.     axis.title = element_blank(),
32.     panel.grid = element_blank(),
33.     plot.title = element_text(face = "bold", size = 14)
34.   ) +
35.   labs(title = "Heatmap tương quan giữa các chỉ tiêu tài chính")

Giải thích kĩ thuật:

1-3. Nạp các gói:

4-18. Xác định danh sách các biến tài chính vars_fin để tính tương quan và vẽ heatmap.

19-21. Tính ma trận tương quan (cor_matrix) giữa các biến, sử dụng use = "complete.obs" để bỏ các giá trị NA.

  1. Chuyển ma trận tương quan sang dạng dài (long format) với các cột: X, Y, Correlation, dùng melt().

23-27. Vẽ heatmap tương quan:

28-34. Tùy chỉnh giao diện:

  1. labs(title = …) thêm tiêu đề cho biểu đồ.

2.4.4. Phân tích dòng tiền và khả năng thanh toán

1. library(ggplot2)
2. library(dplyr)
3. library(tidyr)
4. library(scales)
5. dl_cash_long <- dl %>%
6.   select(year, tienvacackhoantuongduongtiendaunam, tienvacackhoantuongduongtiencuoinam,
7.          luuchuyentienthuantuhoatdongkinhdoanh) %>%
8.   pivot_longer(cols = -year, names_to = "Type", values_to = "Value")
9. dl_cash_ratio <- dl %>%
10.   mutate(cash_ratio = tienvacackhoantuongduongtiencuoinam / tts)
11. ggplot() +
12.   geom_col(data = dl_cash_long, aes(x = factor(year), y = Value, fill = Type), position = "dodge") +
13.   geom_line(data = dl_cash_ratio, aes(x = factor(year), y = cash_ratio * max(dl$tienvacackhoantuongduongtiencuoinam, na.rm = TRUE), group = 1),
14.             color = "red", size = 1.2) +
15.   scale_y_continuous(
16.     name = "Số tiền (VND)",
17.     labels = comma,
18.     sec.axis = sec_axis(~./max(dl$tienvacackhoantuongduongtiencuoinam, na.rm = TRUE),
19.                         name = "Tỷ lệ tiền/TTS")
20.   ) +
21.   labs(title = "Phân tích dòng tiền và khả năng thanh toán",
22.        x = "Năm") +
23.   theme_minimal() +
24.   theme(axis.text.x = element_text(angle = 45, hjust = 1),
25.         legend.title = element_blank())

Giải thích kĩ thuật:

1-4. Nạp các gói:

5-8. Chuyển dữ liệu dòng tiền sang dạng dài (long format) gồm:

9-10. Tạo biến tỷ lệ dòng tiền/TTS (cash_ratio) dùng cho trục phụ.

11-14. Vẽ biểu đồ kết hợp:

15-20. Tùy chỉnh trục Y:

21-22. Thêm tiêu đề và nhãn trục X.

23-25. Giao diện:

2.4.5. Phân tích hiệu quả sử dụng vốn và tài sản

1. df_long <- dl %>%
2.   select(year, tts, vcsh, loinhuanketoansauthuetndn, ROA, ROE) %>%
3.   pivot_longer(cols = -year, names_to = "Metric", values_to = "Value")
4. ggplot(df_long, aes(x = factor(year), y = Value, color = Metric, group = Metric)) +
5.   geom_line(size = 1.2) +
6.   geom_point(size = 2) +
7.   facet_wrap(~Metric, scales = "free_y", ncol = 2) +
8.   scale_y_continuous(labels = comma) +
9.   theme_minimal() +
10.   theme(axis.text.x = element_text(angle = 45, hjust = 1),
11.         legend.position = "none")

Giải thích kĩ thuật:

1-3. Chuẩn bị dữ liệu:

4-6. Vẽ biểu đồ đường (line chart) kết hợp điểm (geom_point):

  1. facet_wrap(~Metric, scales = "free_y", ncol = 2):
  1. scale_y_continuous(labels = comma) định dạng trục Y hiển thị số với dấu phẩy cho dễ đọc.

9-11. theme_minimal() làm giao diện sạch,

Kết quả: biểu đồ đường nhiều chỉ tiêu qua các năm, dễ quan sát xu hướng tăng/giảm của TTS, VCSH, LNST, ROA, ROE riêng biệt và trực quan.

2.4.6. Phân tích đòn bẩy tài chính

1. dl %>%
2.   mutate(debt_equity = (tts - vcsh) / vcsh) %>%
3.   ggplot(aes(x = factor(year), y = debt_equity)) +
4.   geom_line(size = 1.2, color = "steelblue") +
5.   geom_point(size = 2, color = "steelblue") +
6.   scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
7.   theme_minimal() +
8.   theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích kĩ thuật:

1-2. Chuẩn bị dữ liệu:

3-5. Vẽ biểu đồ đường (line chart) kết hợp điểm (geom_point):

  1. scale_y_continuous(labels = scales::percent_format(accuracy = 1)):

7-8. theme_minimal() tạo giao diện đơn giản,

2.4.7. Phân tích rủi ro và biến động lợi nhuận

1. dl %>%
2.   select(year, tongloinhuanketoantruocthue, loinhuanketoansauthuetndn) %>%
3.   pivot_longer(cols = -year, names_to = "Metric", values_to = "Value") %>%
4.   ggplot(aes(x = factor(year), y = Value, color = Metric, group = Metric)) +
5.   geom_line(size = 1.2) +
6.   geom_point(size = 2) +
7.   scale_y_continuous(labels = scales::dollar_format()) +
8.   theme_minimal() +
9.   theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích kĩ thuật:

1-3. Chuẩn bị dữ liệu:

4-6. Vẽ biểu đồ đường (line chart) với điểm dữ liệu (geom_point):

  1. scale_y_continuous(labels = scales::dollar_format()):

8-9. theme_minimal() tạo giao diện tối giản,

2.4.8. So sánh chỉ tiêu tài chính qua các giai đoạn

1. finance_vars <- dl %>%
2.   select(year, tts, vcsh, loinhuanketoansauthuetndn, congdoanhthuhoatdong, congchiphihoatdong)
3. finance_vars_long <- finance_vars %>%
4.   pivot_longer(cols = -year, names_to = "Metric", values_to = "Value")
5. ggplot(finance_vars_long, aes(x = factor(year), y = Value, color = Metric, group = Metric)) +
6.   geom_line(size = 1.2) +
7.   geom_point(size = 2) +
8.   scale_y_continuous(labels = scales::dollar_format()) +
9.   theme_minimal() +
10.   theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích kĩ thuật:

1-4. Chuẩn bị dữ liệu:

5-7. Vẽ biểu đồ đường và điểm dữ liệu:

  1. scale_y_continuous(labels = scales::dollar_format()):

9-10. theme_minimal() tạo giao diện tối giản,

Kết quả: Biểu đồ thể hiện xu hướng biến động các chỉ tiêu tài chính chính theo năm, giúp so sánh tốc độ tăng/giảm của từng chỉ tiêu trong cùng một khoảng thời gian.

2.4.9. Xác định các năm đặc trưng về ROE, ROA và lợi nhuận

1. dl_long <- dl %>%
2.   select(year, ROA, ROE, loinhuanketoansauthuetndn) %>%
3.   pivot_longer(cols = -year, names_to = "Metric", values_to = "Value")
4. ggplot(dl_long, aes(x = factor(year), y = Value, color = Metric, group = Metric)) +
5.   geom_line(size = 1.2) +
6.   geom_point(size = 2) +
7.   scale_y_continuous(labels = function(x) ifelse(x < 1, scales::percent(x, accuracy = 0.1), scales::dollar(x))) +
8.   theme_minimal() +
9.   theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích kĩ thuật:

1-3. Chuẩn bị dữ liệu:

4-6. Vẽ biểu đồ đường và điểm dữ liệu:

  1. scale_y_continuous(labels = ...):

8-9. theme_minimal() và xoay nhãn trục X 45° để dễ đọc.

2.4.10. Kiểm tra các mối quan hệ quan trọng giữa lợi nhuận, dòng tiền và vốn chủ sở hữu

1. dl_corr <- dl %>%
2.   select(loinhuanketoansauthuetndn, luuchuyentienthuantuhoatdongkinhdoanh, vcsh)
3. cor_matrix <- round(cor(dl_corr, use = "complete.obs"), 2)
4. cor_df <- as.data.frame(as.table(cor_matrix))
5. ggplot(cor_df, aes(Var1, Var2, fill = Freq)) +
6.   geom_tile() +
7.   geom_text(aes(label = Freq), color = "white", size = 5) +
8.   scale_fill_gradient2(low = "blue", mid = "white", high = "red", midpoint = 0) +
9.   theme_minimal() +
10.   labs(x = "", y = "", fill = "Correlation") +
11.   theme(axis.text.x = element_text(angle = 45, hjust = 1))

Giải thích kĩ thuật:

1-2. Chuẩn bị dữ liệu:

3-4. Chuyển đổi dữ liệu:

5-7. Vẽ heatmap tương quan:

  1. scale_fill_gradient2(...): Tùy biến màu sắc với trung điểm = 0 để dễ nhận biết quan hệ âm/dương.

9-11. Tối ưu hóa hiển thị:

2.4.11. Phân tích các chỉ tiêu có biến động lớn nhất và ảnh hưởng tới kết quả kinh doanh

1. top_cv_plot <- top_cv %>%
2.   mutate(Chi_tieu = factor(Chi_tieu, levels = rev(Chi_tieu)))
3. ggplot(top_cv_plot, aes(x = Chi_tieu, y = CV)) +
4.   geom_bar(stat = "identity", fill = "steelblue") +
5.   scale_y_continuous(labels = percent_format(accuracy = 1)) +
6.   coord_flip() +
7.   theme_minimal()

Giải thích kĩ thuật:

1-2. Chuẩn bị dữ liệu:

3-4. Vẽ cột ngang:

  1. scale_y_continuous(labels = percent_format(accuracy = 1)):
  1. coord_flip():
  1. theme_minimal():