#CHƯƠNG 1: PHÂN TÍCH KHÁM PHÁ BỘ DỮ LIỆU ÂM NHẠC SPOTIFY (1921-2020).
#1.1. Bối cảnh và giới thiệu bộ dữ liệu.
Trong bối cảnh ngành công nghiệp âm nhạc kỹ thuật số đang phát triển với tốc độ vũ bão, dữ liệu thu thập từ các nền tảng streaming trực tuyến như Spotify đã trở thành một tài sản vô giá. Nguồn dữ liệu này mở ra cơ hội cho các nhà phân tích khám phá và lượng hóa các xu hướng âm nhạc, đặc điểm âm thanh (audio features), và hành vi nghe nhạc của công chúng qua nhiều thập kỷ.
Bộ dữ liệu được sử dụng trong chương này—với tên gọi “Spotify Dataset 1921-2020”—là một bộ sưu tập dữ liệu quy mô lớn, bao gồm thông tin chi tiết của hơn 170,000 bản ghi (tracks). Dữ liệu không chỉ giới hạn ở các thông tin cơ bản như tên bài hát, nghệ sĩ và năm phát hành, mà còn đi sâu vào các thuộc tính âm nhạc chuyên biệt (ví dụ: danceability, energy, acousticness), cung cấp một bức tranh toàn diện về đặc tính của từng bài hát.
Mục tiêu của chương này là thực hiện các bước nền tảng của quy trình Phân tích dữ liệu Khám phá (Exploratory Data Analysis - EDA). Quy trình này bắt đầu bằng việc tải (import) và làm sạch (clean) bộ dữ liệu thô vào môi trường R. Tiếp theo, sẽ tiến hành kiểm tra sơ bộ cấu trúc, kiểu dữ liệu và phát hiện các điểm bất thường. Giai đoạn này có vai trò then chốt, đảm bảo tính toàn vẹn và chất lượng của dữ liệu, từ đó tạo tiền đề vững chắc cho các phân tích thống kê và trực quan hóa phức tạp hơn ở các phần sau của bài tiểu luận.
#1.2. Tải và khám phá sơ bộ dữ liệu (EDA).
Sau khi đã thiết lập bối cảnh, bước tiếp theo là tải bộ dữ liệu data.csv vào môi trường R. Đồng thời, sẽ thực hiện ngay các bước kiểm tra cơ bản để xác thực tính toàn vẹn và cấu trúc của dữ liệu vừa tải.
#1.2.1. Tải dữ liệu.
Sử dụng hàm read_csv() từ gói readr (thuộc hệ sinh thái tidyverse) để tải dữ liệu. Hàm này được ưu tiên hơn read.csv() của R cơ bản vì tốc độ xử lý nhanh hơn đối với các tệp lớn và khả năng tự động nhận diện kiểu dữ liệu (data types) một cách thông minh, giúp hạn chế lỗi.
# Thao tác 1: Đọc dữ liệu
# Đọc dữ liệu từ file CSV vào biến (data frame) 'spotify_data'
spotify_data <- read_csv("data.csv")
#1.2.2. Kiểm tra kích thước.
Bước đầu tiên sau khi tải dữ liệu là kiểm tra kích thước (dimensions) của bộ dữ liệu bằng hàm dim(). Thao tác này trả về một vector gồm hai giá trị: số lượng quan sát (hàng) và số lượng thuộc tính (cột).Bước đầu tiên sau khi tải dữ liệu là kiểm tra kích thước (dimensions) của bộ dữ liệu bằng hàm dim(). Thao tác này trả về một vector gồm hai giá trị: số lượng quan sát (hàng) và số lượng thuộc tính (cột).
# Thao tác 2: Kiểm tra kích thước
dim(spotify_data)
## [1] 170653 19
Giải thích:
Hàm dim() là một hàm R cơ bản dùng để lấy hoặc thiết lập kích thước (số hàng và số cột) của một đối tượng dữ liệu (như data frame hoặc matrix).
Kết quả: Hàm này trả về một vector số nguyên (numeric vector): [1] 170653 19. Số đầu tiên là 170,653: số lượng quan sát (hàng). Số thứ hai là 19: số lượng thuộc tính (cột).
Ý nghĩa: Con số này xác nhận đây là một bộ dữ liệu có kích thước lớn, cung cấp một nguồn thông tin dồi dào cho việc phân tích. Mỗi hàng (170,653) đại diện cho một bài hát, và mỗi cột (19) là một đặc điểm của bài hát đó.
#1.2.3. Kiểm tra cấu trúc và kiểu dữ liệu.
Sau khi biết được kích thước, bước tiếp theo là kiểm tra cấu trúc bên trong của 19 cột. Sử dụng hàm glimpse() (thuộc gói dplyr) là một phương pháp hiệu quả để “kiểm tra sức khỏe” (sanity check) của dữ liệu.
Hàm này cung cấp một cái nhìn tổng quan theo chiều dọc, hiển thị:
Tên của từng cột (ví dụ: artists).
Kiểu dữ liệu mà R đã tự động gán cho cột đó (ví dụ:
Một vài giá trị mẫu đầu tiên của mỗi cột.
Đây là thao tác xác thực quan trọng để đảm bảo R đã diễn giải
chính xác các kiểu dữ liệu, ví dụ như year (năm) phải là kiểu số (
# Thao tác 3: Xem lướt cấu trúc (Glimpse)
glimpse(spotify_data)
## Rows: 170,653
## Columns: 19
## $ valence <dbl> 0.0594, 0.9630, 0.0394, 0.1650, 0.2530, 0.1960, 0.406…
## $ year <dbl> 1921, 1921, 1921, 1921, 1921, 1921, 1921, 1921, 1921,…
## $ acousticness <dbl> 0.9820, 0.7320, 0.9610, 0.9670, 0.9570, 0.5790, 0.996…
## $ artists <chr> "['Sergei Rachmaninoff', 'James Levine', 'Berliner Ph…
## $ danceability <dbl> 0.279, 0.819, 0.328, 0.275, 0.418, 0.697, 0.518, 0.38…
## $ duration_ms <dbl> 831667, 180533, 500062, 210000, 166693, 395076, 15950…
## $ energy <dbl> 0.2110, 0.3410, 0.1660, 0.3090, 0.1930, 0.3460, 0.203…
## $ explicit <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,…
## $ id <chr> "4BJqT0PrAfrxzMOxytFOIz", "7xPhfUan2yNtyFG0cUWkt8", "…
## $ instrumentalness <dbl> 8.78e-01, 0.00e+00, 9.13e-01, 2.77e-05, 1.68e-06, 1.6…
## $ key <dbl> 10, 7, 3, 5, 3, 2, 0, 1, 5, 8, 9, 10, 5, 8, 7, 7, 2, …
## $ liveness <dbl> 0.6650, 0.1600, 0.1010, 0.3810, 0.2290, 0.1300, 0.115…
## $ loudness <dbl> -20.096, -12.441, -14.850, -9.316, -10.096, -12.506, …
## $ mode <dbl> 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1,…
## $ name <chr> "Piano Concerto No. 3 in D Minor, Op. 30: III. Finale…
## $ popularity <dbl> 4, 5, 5, 3, 2, 6, 4, 2, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0,…
## $ release_date <chr> "1921", "1921", "1921", "1921", "1921", "1921", "1921…
## $ speechiness <dbl> 0.0366, 0.4150, 0.0339, 0.0354, 0.0380, 0.0700, 0.061…
## $ tempo <dbl> 80.954, 60.936, 110.339, 100.109, 101.665, 119.824, 6…
Giải thích: Hàm glimpse() cho phép “liếc” nhanh toàn bộ cấu trúc của
bảng. Ưu điểm vượt trội của nó so với hàm str() (R cơ bản) là glimpse()
trình bày thông tin theo chiều dọc (mỗi cột một dòng). Khi làm việc với
19 cột, cách hiển thị này giúp dễ dàng quan sát toàn bộ bảng mà không bị
xuống dòng hay che khuất. Hàm này in ra danh sách 19 cột, hiển thị tên
cột (ví dụ: $ year), kiểu dữ liệu R đã gán (ví dụ:
#1.2.4. Định nghĩa bảng tra cứu (Metadata).
Bộ dữ liệu chứa 19 cột, nhiều cột trong số đó có tên kỹ thuật (ví dụ: valence, speechiness) không thể tự giải thích. Để đảm bảo tính rõ ràng và giúp người đọc dễ dàng theo dõi bài phân tích, một bảng tra cứu (metadata) sẽ được tạo ra.
Bảng này định nghĩa rõ ràng từng biến, kiểu dữ liệu và mô tả ý nghĩa của nó. Hàm tribble() (từ gói tibble, đã được tải cùng tidyverse) được sử dụng để tạo bảng này một cách trực quan.
# Thao tác 4: Tạo bảng tra cứu (Metadata)
library(tibble) #1
spotify_metadata <- tribble( #2
~Variable, ~Data_Type, ~Description,
"valence", "double", "Mức độ tích cực trong âm nhạc (0 = buồn, 1 = vui)",
"year", "double", "Năm phát hành của bài hát",
"acousticness", "double", "Khả năng bài hát là acoustic (âm thanh mộc)",
"artists", "character","Danh sách nghệ sĩ thể hiện bài hát",
"danceability", "double", "Mức độ dễ nhảy theo bài hát (0 đến 1)",
"duration_ms", "double", "Thời lượng bài hát (miligiây)",
"energy", "double", "Mức năng lượng của bài hát",
"explicit", "double", "Bài hát có nội dung nhạy cảm không (0 = không, 1 = có)",
"id", "character","Mã định danh bài hát trên Spotify",
"instrumentalness", "double", "Xác suất bài hát là nhạc không lời",
"key", "double", "Tông nhạc chính của bài hát (0 = C, 1 = C#, ..., 11 = B)",
"liveness", "double", "Khả năng bài hát được thu âm trực tiếp",
"loudness", "double", "Mức âm lượng trung bình (đơn vị decibel)",
"mode", "double", "Thang âm chính (1) hoặc phụ (0)",
"name", "character","Tên bài hát",
"popularity", "double", "Mức độ phổ biến (0-100)",
"release_date", "character","Ngày phát hành bài hát",
"speechiness", "double", "Mức độ chứa lời thoại trong bài hát",
"tempo", "double", "Tốc độ của bài hát (nhịp/phút - BPM)"
)
print(spotify_metadata, n = Inf) #3
## # A tibble: 19 × 3
## Variable Data_Type Description
## <chr> <chr> <chr>
## 1 valence double Mức độ tích cực trong âm nhạc (0 = buồn, 1 = vui)
## 2 year double Năm phát hành của bài hát
## 3 acousticness double Khả năng bài hát là acoustic (âm thanh mộc)
## 4 artists character Danh sách nghệ sĩ thể hiện bài hát
## 5 danceability double Mức độ dễ nhảy theo bài hát (0 đến 1)
## 6 duration_ms double Thời lượng bài hát (miligiây)
## 7 energy double Mức năng lượng của bài hát
## 8 explicit double Bài hát có nội dung nhạy cảm không (0 = không, 1 …
## 9 id character Mã định danh bài hát trên Spotify
## 10 instrumentalness double Xác suất bài hát là nhạc không lời
## 11 key double Tông nhạc chính của bài hát (0 = C, 1 = C#, ..., …
## 12 liveness double Khả năng bài hát được thu âm trực tiếp
## 13 loudness double Mức âm lượng trung bình (đơn vị decibel)
## 14 mode double Thang âm chính (1) hoặc phụ (0)
## 15 name character Tên bài hát
## 16 popularity double Mức độ phổ biến (0-100)
## 17 release_date character Ngày phát hành bài hát
## 18 speechiness double Mức độ chứa lời thoại trong bài hát
## 19 tempo double Tốc độ của bài hát (nhịp/phút - BPM)
Giải thích: - Câu lệnh #1: library(tibble) tải gói tibble (thuộc tidyverse) để sử dụng hàm tribble().
Câu lệnh #2: spotify_metadata <- tribble(…)
Ý nghĩa: tribble() (viết tắt của “transposed tibble”) là một hàm đặc biệt để khởi tạo một data frame (tibble). Khác với hàm data.frame() thông thường, tribble() cho phép định nghĩa dữ liệu theo cách rất dễ đọc (giống như đang gõ vào một bảng tính), với các tên cột (bắt đầu bằng ~) ở hàng đầu tiên.
Câu lệnh #3: print(spotify_metadata, n = Inf). Lệnh này in toàn bộ nội dung của bảng spotify_metadata. Tham số n = Inf (infinite - vô hạn) là rất quan trọng. Mặc định, R (hoặc tibble) chỉ in 10 hàng đầu tiên. Việc dùng n = Inf buộc R phải hiển thị tất cả 19 hàng, đảm bảo bảng tra cứu được trình bày đầy đủ.
Thao tác này tạo ra một “từ điển dữ liệu” (data dictionary). Bảng tra cứu này sẽ đóng vai trò như một tài liệu tham khảo quan trọng xuyên suốt bài tiểu luận, đảm bảo rằng mọi thuật ngữ phân tích (như valence, acousticness) đều được hiểu một cách chính xác.
#1.2.5. Kiểm tra mẫu dữ liệu đầu và cuối.
Trong khi glimpse() cho thấy cấu trúc cột, hàm head() (6 dòng đầu) và tail() (6 dòng cuối) cho phép kiểm tra trực quan dữ liệu theo hàng, tương tự như xem trong bảng tính.
Mục đích của việc này là:
head(): Cung cấp cái nhìn trực quan về 6 bản ghi đầu tiên, giúp hiểu rõ hơn về cách dữ liệu được điền vào.
tail(): Cực kỳ quan trọng để phát hiện dữ liệu “rác” (junk data) ở cuối tệp. Nhiều tệp CSV có thể chứa các dòng tổng kết (ví dụ: “Total:…”, “Data exported on…”) hoặc các hàng trống, điều này có thể gây lỗi khi phân tích.
# Thao tác 5: Kiểm tra 6 dòng đầu
head(spotify_data)
## # A tibble: 6 × 19
## valence year acousticness artists danceability duration_ms energy explicit
## <dbl> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 0.0594 1921 0.982 ['Sergei … 0.279 831667 0.211 0
## 2 0.963 1921 0.732 ['Dennis … 0.819 180533 0.341 0
## 3 0.0394 1921 0.961 ['KHP Kri… 0.328 500062 0.166 0
## 4 0.165 1921 0.967 ['Frank P… 0.275 210000 0.309 0
## 5 0.253 1921 0.957 ['Phil Re… 0.418 166693 0.193 0
## 6 0.196 1921 0.579 ['KHP Kri… 0.697 395076 0.346 0
## # ℹ 11 more variables: id <chr>, instrumentalness <dbl>, key <dbl>,
## # liveness <dbl>, loudness <dbl>, mode <dbl>, name <chr>, popularity <dbl>,
## # release_date <chr>, speechiness <dbl>, tempo <dbl>
Giải thích: Hàm head() được dùng để xem 6 dòng đầu tiên của data frame.
# Thao tác 6: Kiểm tra 6 dòng cuối
tail(spotify_data)
## # A tibble: 6 × 19
## valence year acousticness artists danceability duration_ms energy explicit
## <dbl> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 0.0838 2020 0.974 ['Najma … 0.175 133500 0.00759 0
## 2 0.608 2020 0.0846 ['Anuel … 0.786 301714 0.808 0
## 3 0.734 2020 0.206 ['Ashnik… 0.717 150654 0.753 0
## 4 0.637 2020 0.101 ['MAMAMO… 0.634 211280 0.858 0
## 5 0.195 2020 0.00998 ['Eminem… 0.671 337147 0.623 1
## 6 0.642 2020 0.132 ['KEVVO'… 0.856 189507 0.721 1
## # ℹ 11 more variables: id <chr>, instrumentalness <dbl>, key <dbl>,
## # liveness <dbl>, loudness <dbl>, mode <dbl>, name <chr>, popularity <dbl>,
## # release_date <chr>, speechiness <dbl>, tempo <dbl>
Giải thích: Hàm tail() được dùng để xem 6 dòng cuối cùng của data frame.
Kết quả này xác nhận rằng không có hàng tổng kết hay dữ liệu “rác” nào ở cuối tệp. Bộ dữ liệu kết thúc một cách “sạch sẽ”, đảm bảo tính toàn vẹn cho phân tích.
#1.2.6. Trích xuất tên biến.
Hàm colnames() trích xuất tên của tất cả các biến thành một vector ký tự (một danh sách đơn). Việc này hữu ích khi cần tham chiếu hoặc sao chép nhanh danh sách các cột để lựa chọn (subsetting) ở các bước sau.
# Thao tác 7: Liệt kê tên các biến
colnames(spotify_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"
Giải thích: Hàm colnames() là hàm R cơ bản dùng để trích xuất tên của tất cả các cột từ một đối tượng dữ liệu). Hàm này trả về một vector ký tự (character vector), chứa chính xác 19 tên cột của bộ dữ liệu. Thao tác này rất hữu ích trong thực tế. Nó cung cấp một danh sách “sạch” chỉ chứa tên các cột. Danh sách này có thể được sao chép để sử dụng trong các thao tác lựa chọn biến hoặc khi cần áp dụng một hàm cho hàng loạt cột ở các bước sau.
#1.2.7. Kiểm tra dữ liệu thiếu (Missing Values - NA).
Đây là một trong những bước kiểm tra quan trọng nhất trong quy trình làm sạch dữ liệu. Dữ liệu thiếu (biểu thị bằng giá trị NA trong R) có thể gây ra lỗi tính toán (ví dụ: mean() sẽ trả về NA) hoặc làm sai lệch nghiêm trọng kết quả phân tích.
Phương pháp kiểm tra là kết hợp hai hàm: is.na() sẽ kiểm tra từng ô trong toàn bộ data frame (trả về TRUE nếu là NA), và sum() sẽ cộng tất cả các giá trị TRUE lại (vì R coi TRUE = 1 và FALSE = 0).
# Thao tác 8: Đếm tổng số giá trị thiếu (NA)
total_na <- sum(is.na(spotify_data)) #1
print(paste("Tổng số giá trị thiếu (NA) trong dữ liệu:", total_na)) #2
## [1] "Tổng số giá trị thiếu (NA) trong dữ liệu: 0"
Giải thích:
Câu lệnh #1: total_na <- sum(is.na(spotify_data))
is.na(spotify_data): Hàm này thực hiện kiểm tra logic trên từng ô (cell) trong toàn bộ data frame. Nó trả về TRUE nếu ô đó là giá trị NA (bị thiếu) và FALSE nếu ô đó có dữ liệu.
sum(…): Hàm sum() sau đó cộng tất cả các giá trị TRUE lại với nhau (vì R coi TRUE = 1 và FALSE = 0). Kết quả là tổng số lượng ô bị thiếu trong toàn bộ bảng.
Câu lệnh #2: print(paste(…)) => In kết quả total_na ra màn hình, kèm theo một chuỗi văn bản mô tả (“Tong so gia tri thieu…”) để báo cáo dễ đọc hơn.
Kết quả trả về là 0. Con số này xác nhận bộ dữ liệu spotify_data rất “sạch” và không chứa bất kỳ giá trị thiếu (NA) chuẩn nào. Điều này giúp đơn giản hóa đáng kể giai đoạn tiền xử lý, vì không cần phải áp dụng các kỹ thuật xử lý NA (như xóa hàng hoặc gán giá trị thay thế - imputation).
#1.2.8. Kiểm tra quan sát trùng lặp (Duplicated Rows).
Một vấn đề phổ biến khác là các quan sát (hàng) bị trùng lặp hoàn toàn. Nếu một bài hát bị nhập liệu nhiều lần giống hệt nhau, nó sẽ làm sai lệch kết quả thống kê (ví dụ: tăng giả tạo số lượng bài hát của một nghệ sĩ, hoặc làm lệch giá trị trung bình của các thuộc tính).
Hàm duplicated() sẽ kiểm tra từng hàng, trả về TRUE nếu hàng đó giống hệt một hàng đã xuất hiện trước đó. Hàm sum() được dùng để đếm tổng số hàng trùng lặp.
# Thao tác 9: Kiểm tra số lượng quan sát trùng lặp
duplicated_rows <- sum(duplicated(spotify_data)) #1
print(paste("Số lượng quan sát trùng lặp là:", duplicated_rows)) #2
## [1] "Số lượng quan sát trùng lặp là: 0"
Giải thích:
Câu lệnh #1: duplicated_rows <- sum(duplicated(spotify_data))
duplicated(spotify_data): Hàm R cơ bản này kiểm tra từng hàng. Nó trả về TRUE nếu hàng đó giống hệt (100%) một hàng đã xuất hiện trước đó trong bảng, và FALSE nếu hàng đó là duy nhất.
sum(…): Tương tự như bước kiểm tra NA, hàm sum() đếm tổng số giá trị TRUE (vì TRUE = 1, FALSE = 0). Kết quả là tổng số hàng bị trùng lặp.
Câu lệnh #2: print(paste(…)) => In kết quả đếm được ra màn hình kèm theo mô tả.
Kết quả: 0. Con số này xác nhận rằng không có bất kỳ hàng nào bị trùng lặp 100% trong bộ dữ liệu. Mọi bản ghi (bài hát) đều là duy nhất. Điều này đảm bảo tính toàn vẹn của các phép đếm và thống kê sau này.
#1.2.9. Kiểm tra phạm vi dữ liệu (Biến year).
Cuối cùng, sẽ thực hiện một bước kiểm tra logic đơn giản đối với biến year (Năm). Hàm range() sẽ trả về giá trị nhỏ nhất (Min) và lớn nhất (Max) của cột này.
Mục đích là để xác nhận phạm vi thời gian của dữ liệu có khớp với mô tả (“1921-2020”) hay không, đồng thời phát hiện các giá trị năm bất thường hoặc vô lý (ví dụ: năm 0 hoặc năm 3000).
# Thao tác 10: Kiểm tra giá trị nhỏ nhất và lớn nhất của cột 'year'
range(spotify_data$year)
## [1] 1921 2020
Giải thích: - Câu lệnh: range(spotify_data$year)
spotify_data$year: Sử dụng ký hiệu $ để truy cập cụ thể vào cột year từ bảng spotify_data.
range(…): Hàm R cơ bản này nhận vào một vector số và trả về một vector mới chứa 2 giá trị: giá trị tối thiểu (Min) và tối đa (Max).
Kết quả: Hàm trả về [1] 1921 2020. Kết quả xác nhận phạm vi dữ liệu là từ năm 1921 đến 2020, hoàn toàn khớp với mô tả của bộ dữ liệu (“Spotify Dataset 1921-2020”). Điều này cũng khẳng định không có dữ liệu năm bất thường (ví dụ: năm 0 hoặc năm 3000) bị lẫn vào.
Kết luận phần 1.2:
Kết thúc giai đoạn khám phá sơ bộ, có thể kết luận rằng bộ dữ liệu spotify_data đã được tải thành công.
Các bước kiểm tra đã xác nhận:
Kích thước: Dữ liệu có quy mô lớn (170,653 hàng, 19 cột).
Cấu trúc: Kiểu dữ liệu của 19 cột đã được R đọc chính xác (ví dụ: year là số, artists là văn bản).
Tính toàn vẹn: Dữ liệu không chứa bất kỳ giá trị thiếu (NA) hay quan sát trùng lặp (duplicated rows) nào.
Phạm vi: Dữ liệu năm nằm trong đúng khoảng thời gian (1921-2020).
Nhận xét: Bộ dữ liệu này được đánh giá là rất “sạch”, có cấu trúc tốt và sẵn sàng cho giai đoạn xử lý và làm giàu dữ liệu (Feature Engineering) tiếp theo.
#1.3. Tiền xử lý và làm giàu dữ liệu (Data Preprocessing and Feature Engineering).
Mục tiêu: Sau khi đã khám phá và xác nhận tính toàn vẹn của dữ liệu ở phần 1.2, giai đoạn này sẽ tập trung vào việc “làm giàu” (enrich) bộ dữ liệu. Dữ liệu thô, dù sạch, nhưng thường chưa ở định dạng tối ưu cho việc phân tích.
Phần này sẽ thực hiện 10 thao tác, bao gồm chuyển đổi đơn vị, loại bỏ các thuộc tính không liên quan, và quan trọng nhất là tạo ra các biến mới (feature engineering) thông qua việc mã hóa và phân nhóm (binning). Mục đích là để chuyển đổi các biến số liên tục thành các biến phân loại (categorical) có ý nghĩa hơn, phục vụ cho việc thống kê theo nhóm và trực quan hóa ở các phần sau.
Đồng thời, phần này cũng sẽ trình bày các kỹ thuật xử lý dữ liệu trong “tình huống giả định” (lọc ngoại lai, xử lý NA, chuẩn hóa) để thể hiện năng lực xử lý các vấno đề dữ liệu phức tạp trong thực tế, ngay cả khi bộ dữ liệu hiện tại không yêu cầu.
#1.3.1. Tạo bản sao dữ liệu.
Một nguyên tắc cơ bản trong phân tích dữ liệu là không bao giờ thao tác trực tiếp trên bộ dữ liệu gốc (spotify_data). Việc này nhằm đảm bảo tính toàn vẹn của dữ liệu ban đầu, phòng trường hợp các bước xử lý sau này xảy ra lỗi hoặc cần đối chiếu lại.
Thao tác này tạo một bản sao (spotify_processed) để thực hiện tất cả các thay đổi.
# Thao tác 1: Tạo bản sao dữ liệu
spotify_processed <- spotify_data
Giải thích: - Câu lệnh: spotify_processed <- spotify_data
#1.3.2. Chuyển đổi đơn vị (Feature Engineering).
Cột duration_ms (thời lượng) đang được lưu dưới dạng mili giây, một đơn vị khó diễn giải trong thực tế. Ví dụ, giá trị 180000 mili giây không trực quan bằng 3 phút.
Thao tác này sẽ tạo một cột mới duration_min bằng cách chia duration_ms cho 60,000 (vì 1 phút = 60 giây * 1000 mili giây). Cột mới này sẽ trực quan hơn và dễ sử dụng hơn trong các phân tích và biểu đồ sau này.
# Thao tác 2: Chuyển đổi đơn vị
spotify_processed <- spotify_processed %>% #1
mutate(duration_min = duration_ms / 60000) #2
head(spotify_processed) #3
## # A tibble: 6 × 20
## valence year acousticness artists danceability duration_ms energy explicit
## <dbl> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 0.0594 1921 0.982 ['Sergei … 0.279 831667 0.211 0
## 2 0.963 1921 0.732 ['Dennis … 0.819 180533 0.341 0
## 3 0.0394 1921 0.961 ['KHP Kri… 0.328 500062 0.166 0
## 4 0.165 1921 0.967 ['Frank P… 0.275 210000 0.309 0
## 5 0.253 1921 0.957 ['Phil Re… 0.418 166693 0.193 0
## 6 0.196 1921 0.579 ['KHP Kri… 0.697 395076 0.346 0
## # ℹ 12 more variables: id <chr>, instrumentalness <dbl>, key <dbl>,
## # liveness <dbl>, loudness <dbl>, mode <dbl>, name <chr>, popularity <dbl>,
## # release_date <chr>, speechiness <dbl>, tempo <dbl>, duration_min <dbl>
Giải thích: - Câu lệnh #1: spotify_processed <- spotify_processed %>%
%>% (Toán tử “Pipe”): Đây là toán tử “đường ống” (pipe) từ gói dplyr. Nó lấy kết quả của vế bên trái (spotify_processed) và “chuyển” (pipe) nó làm đầu vào (input) cho hàm ở vế bên phải (hàm mutate).
spotify_processed <- …: Phép gán (<-) có nghĩa là sau khi thực hiện các thao tác (ở Dòng 2), kết quả cuối cùng sẽ được gán ngược trở lại, cập nhật chính biến spotify_processed.
Câu lệnh #2: mutate(duration_min = duration_ms / 60000)
mutate(…): Đây là hàm của dplyr dùng để thêm một cột mới hoặc thay đổi một cột hiện có.
duration_min = duration_ms / 60000: Logic chính. Hàm này tạo một cột mới tên là duration_min, với giá trị được tính bằng cách lấy giá trị của cột duration_ms chia cho 60,000.
Câu lệnh #3: head(spotify_processed). Sau khi cột mới đã được tạo, lệnh head() được gọi để xem 6 dòng đầu tiên của bảng spotify_processed đã được cập nhật.
Kết quả: Kết quả kiểm tra cho thấy cột duration_min đã xuất hiện ở cuối data frame với các giá trị có thể hiểu được (ví dụ: 3.0, 4.2 phút)..
#1.3.3. Lựa chọn thuộc tính (Feature Selection).
Sau khi đã tạo duration_min, cột duration_ms gốc trở nên dư thừa. Tương tự, các cột như id (mã định danh duy nhất của Spotify) và release_date (ngày phát hành chi tiết) không cần thiết cho các phân tích xu hướng tổng hợp (vì đã có cột year).
Thao tác này giúp “làm gọn” data frame bằng cách loại bỏ các cột không cần thiết, giúp tập trung vào các thuộc tính liên quan đến phân tích.
# Thao tác 3: Loại bỏ các cột không cần thiết
spotify_processed <- spotify_processed %>% #1
select(-duration_ms, -id, -release_date) #2
dim(spotify_processed) #3
## [1] 170653 17
Giải thích:
Câu lệnh #1: spotify_processed <- spotify_processed %>%
Toán tử pipe (%>%) tiếp tục chuỗi thao tác, lấy bảng spotify_processed làm đầu vào cho hàm select().
Câu lệnh #2: select(-duration_ms, -id, -release_date)
select(…): Đây là hàm của dplyr dùng để chọn các cột.
Cú pháp - (dấu trừ): Khi đặt dấu trừ trước tên cột trong hàm select(), nó mang ý nghĩa là “loại bỏ” cột đó.
Ý nghĩa: Lệnh này chỉ định R loại bỏ 3 cột (duration_ms, id, release_date) khỏi bảng và giữ lại tất cả các cột còn lại.
Dòng 3: dim(spotify_processed)
Nhận xét: Thao tác này giúp data frame trở nên “gọn” hơn, loại bỏ các thông tin không cần thiết và chỉ tập trung vào các thuộc tính sẽ được sử dụng cho phân tích..
#1.3.4. Mã hóa biến phân loại (Encoding).
Biến explicit (nội dung nhạy cảm) hiện là một biến số (
Mục đích của thao tác này là chuyển đổi kiểu dữ liệu của nó sang Factor (biến phân loại). Nó sẽ gán các nhãn văn bản có ý nghĩa (Không nhạy cảm”, Có nhạy cảm”) cho các giá trị số tương ứng. Điều này làm cho các bảng thống kê và biểu đồ (ví dụ ggplot2) trở nên rõ ràng và chuyên nghiệp hơn.
# Thao tác 4: Mã hóa biến 'explicit' thành Factor
spotify_processed <- spotify_processed %>% #1
mutate(explicit_label = factor(explicit, #2
levels = c(0, 1), #3
labels = c("Không nhạy cảm", "Có nhạy cảm"))) #4
glimpse(spotify_processed) #5
## Rows: 170,653
## Columns: 18
## $ valence <dbl> 0.0594, 0.9630, 0.0394, 0.1650, 0.2530, 0.1960, 0.406…
## $ year <dbl> 1921, 1921, 1921, 1921, 1921, 1921, 1921, 1921, 1921,…
## $ acousticness <dbl> 0.9820, 0.7320, 0.9610, 0.9670, 0.9570, 0.5790, 0.996…
## $ artists <chr> "['Sergei Rachmaninoff', 'James Levine', 'Berliner Ph…
## $ danceability <dbl> 0.279, 0.819, 0.328, 0.275, 0.418, 0.697, 0.518, 0.38…
## $ energy <dbl> 0.2110, 0.3410, 0.1660, 0.3090, 0.1930, 0.3460, 0.203…
## $ explicit <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,…
## $ instrumentalness <dbl> 8.78e-01, 0.00e+00, 9.13e-01, 2.77e-05, 1.68e-06, 1.6…
## $ key <dbl> 10, 7, 3, 5, 3, 2, 0, 1, 5, 8, 9, 10, 5, 8, 7, 7, 2, …
## $ liveness <dbl> 0.6650, 0.1600, 0.1010, 0.3810, 0.2290, 0.1300, 0.115…
## $ loudness <dbl> -20.096, -12.441, -14.850, -9.316, -10.096, -12.506, …
## $ mode <dbl> 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1,…
## $ name <chr> "Piano Concerto No. 3 in D Minor, Op. 30: III. Finale…
## $ popularity <dbl> 4, 5, 5, 3, 2, 6, 4, 2, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0,…
## $ speechiness <dbl> 0.0366, 0.4150, 0.0339, 0.0354, 0.0380, 0.0700, 0.061…
## $ tempo <dbl> 80.954, 60.936, 110.339, 100.109, 101.665, 119.824, 6…
## $ duration_min <dbl> 13.861117, 3.008883, 8.334367, 3.500000, 2.778217, 6.…
## $ explicit_label <fct> Không nhạy cảm, Không nhạy cảm, Không nhạy cảm, Không…
Giải thích:
Câu lệnh #1: spotify_processed <- spotify_processed %>%
Toán tử pipe (%>%) tiếp tục chuỗi thao tác, lấy bảng spotify_processed làm đầu vào cho hàm mutate().
Câu lệnh #2: mutate(explicit_label = factor(explicit, …))
mutate(…) tạo một cột mới có tên là explicit_label.
Giá trị của cột mới này được tạo bởi hàm factor().
Câu lệnh #3: levels = c(0, 1). Đây là một tham số của hàm factor(). Nó chỉ định các giá trị gốc (hiện có) trong cột explicit mà chúng ta muốn mã hóa.
Câu lệnh #4: labels = c(“Không nhạy cảm”, “Có nhạy cảm”). Tham số này chỉ định các nhãn mới (dạng văn bản) tương ứng. R sẽ tìm giá trị 0 (trong levels) và gán cho nó nhãn “Không nhạy cảm”. Tương tự, nó tìm giá trị 1 và gán nhãn “Có nhạy cảm”.
Câu lệnh #5: glimpse(spotify_processed). Lệnh kiểm tra này cho
thấy cột mới explicit_label đã xuất hiện. Quan trọng hơn, kiểu dữ liệu
của nó là
#1.3.5. Rời rạc hóa (Binning) biến popularity.
Biến popularity (độ phổ biến) là một biến liên tục chạy từ 0 đến 100. Sẽ rất khó để trả lời câu hỏi “Các bài hát nổi tiếng có đặc điểm gì?” nếu chỉ dựa trên thang đo này.
Kỹ thuật “Binning” (Phân nhóm, hay Rời rạc hóa) được áp dụng để “cắt” thang đo liên tục này thành 4 nhóm (thùng) có ý nghĩa: “Thap”, “Trung binh”, “Cao”, và “Rat cao”. Việc này cho phép thực hiện các phân tích thống kê theo nhóm (ví dụ: so sánh energy trung bình của nhóm “Rat cao” so với nhóm “Thap”).
# Thao tác 5: Mã hóa (Binning) biến 'popularity'
spotify_processed <- spotify_processed %>% #1
mutate(
popularity_level = case_when( #2
popularity <= 20 ~ "Thấp", #3
popularity <= 50 ~ "Trung bình", #4
popularity <= 80 ~ "Cao", #5
TRUE ~ "Rất cao" #6
)
)
table(spotify_processed$popularity_level) #7
##
## Cao Rất cao Thấp Trung bình
## 35737 549 54404 79963
Giải thích:
Câu lệnh #1 và #2: Sử dụng hàm mutate() để tạo một cột mới tên là popularity_level. Giá trị của cột này được quyết định bởi hàm case_when().
case_when(…): Đây là một hàm if-else mạnh mẽ và dễ đọc của dplyr. Nó sẽ kiểm tra các điều kiện logic từ trên xuống dưới (Dòng 3 đến 6) và gán nhãn tương ứng ngay khi gặp điều kiện ĐÚNG đầu tiên.
Câu lệnh #3, #4, #5: Đây là các “điểm cắt” (cut points):
#3: Nếu popularity từ 0-20, gán nhãn “Thấp”.
#4: Nếu không (tức là > 20) VÀ popularity <= 50 (tức là 21-50), gán nhãn “Trung bình”.
#5: Nếu không (tức là > 50) VÀ popularity <= 80 (tức là 51-80), gán nhãn “Cao”.
Câu lệnh #6: TRUE ~ “Rất cao”
Ý nghĩa: TRUE ở đây đóng vai trò như một mệnh đề else (mặc định). Nếu tất cả các điều kiện bên trên đều SAI (tức là popularity > 80), điều kiện TRUE sẽ được kích hoạt, và gán nhãn “Rất cao”.
Câu lệnh #7: table(spotify_processed$popularity_level).
Ý nghĩa: Sau khi tạo cột mới, hàm table() được dùng để đếm nhanh số lượng bài hát rơi vào mỗi nhóm (“Thấp”, “Trung bình”,…).
Kết quả: Bảng tần suất này cho thấy sự phân bổ của các bài hát vào 4 nhóm mới, giúp kiểm tra logic “cắt” của chúng ta có hợp lý không.
#1.3.6. Rời rạc hóa (Binning) biến energy.
Tương tự như popularity, biến energy (năng lượng) là một biến liên tục từ 0.0 đến 1.0. Để đơn giản hóa việc so sánh và thống kê, nó sẽ được “cắt” thành 3 nhóm: “Thấp”, “Trung bình”, và “Cao”, dựa trên việc chia đều thang đo (0-0.33, 0.33-0.66, 0.66-1.0).
# Thao tác 6: Mã hóa (Binning) biến 'energy'
spotify_processed <- spotify_processed %>% #1
mutate(energy_level = case_when( #2
energy <= 0.33 ~ "Thấp", #3
energy <= 0.66 ~ "Trung bình", #4
TRUE ~ "Cao" #5
))
table(spotify_processed$energy_level) #6
##
## Cao Thấp Trung bình
## 50437 58543 61673
Giải thích:
Câu lệnh #1 và #2: Sử dụng toán tử pipe (%>%) và hàm mutate() để tạo một cột mới tên là energy_level. Giá trị của cột này được quyết định bởi hàm case_when().
case_when(…): Hàm này kiểm tra các điều kiện logic từ trên xuống dưới.
Câu lệnh #3: Nếu energy từ 0 - 0.33, gán nhãn “Thấp”.
Câu lệnh #4: Nếu không (tức là > 0.33) và energy <= 0.66 (tức là 0.34 - 0.66), gán nhãn “Trung bình”.
Câu lệnh #5: TRUE ~ “Cao”: Đây là mệnh đề else (mặc định). Nếu cả hai điều kiện trên đều SAI (tức là energy > 0.66), nó sẽ gán nhãn “Cao”.
Câu lệnh #6: table(spotify_processed$energy_level). Hàm table() được dùng để đếm nhanh số lượng bài hát rơi vào mỗi nhóm (“Thấp”, “Trung bình”, “Cao”).
Nhận xét: Biến energy_level mới này cho phép phân tích chéo, ví dụ: “Có bao nhiêu bài hát ‘Năng lượng Cao’ thuộc nhóm ‘Phổ biến Rất cao’?”.
#1.3.7. Rời rạc hóa (Binning) biến year (Thập kỷ).
Biến year (năm) hiện tại quá chi tiết. Việc phân tích xu hướng âm nhạc bằng cách so sánh từng năm riêng lẻ (ví dụ: 1971 vs 1972) sẽ rất “nhiễu” (noisy).
Một cách tiếp cận hiệu quả hơn là nhóm các năm lại theo thập kỷ (decade). Thao tác này tạo ra một cột decade mới bằng cách làm tròn năm xuống (ví dụ: 1975 sẽ được làm tròn xuống 1970). Điều này cho phép so sánh các xu hướng âm nhạc theo các thời kỳ rõ rệt (ví dụ: “âm nhạc thập niên 70” so với “âm nhạc thập niên 80”).
# Thao tác 7: Mã hóa (Binning) biến 'year' thành 'decade'
spotify_processed <- spotify_processed %>% #1
mutate(decade = floor(year / 10) * 10) #2
table(spotify_processed$decade) #3
##
## 1920 1930 1940 1950 1960 1970 1980 1990 2000 2010 2020
## 5126 9549 15378 19850 19549 20000 19850 19901 19646 19774 2030
Giải thích:
Câu lệnh #1: Sử dụng toán tử pipe (%>%) để lấy data frame spotify_processed làm đầu vào cho hàm mutate().
Câu lệnh #2 mutate(decade = floor(year / 10) * 10): Tạo một cột mới tên là decade.
Đây là một mẹo toán học để làm tròn năm xuống hàng chục gần nhất. Ví dụ với year = 1975:; year / 10 = 197.5; floor(197.5) (làm tròn xuống số nguyên gần nhất) = 197; 197 * 10 = 1970
Kết quả: tất cả các năm từ 1970 đến 1979 sẽ được gán chung vào một nhóm là 1970.
Câu lệnh #3 table(spotify_processed$decade): Hàm table() được dùng để đếm nhanh số lượng bài hát rơi vào mỗi nhóm thập kỷ mới được tạo (1920, 1930, …, 2020).
Nhận xét: Biến decade mới này là một công cụ phân loại rất mạnh mẽ, cho phép chúng ta tổng hợp và so sánh các xu hướng âm nhạc qua các thời kỳ (thập kỷ) thay vì từng năm riêng lẻ, giúp xu hướng trở nên rõ nét hơn..
#1.3.8. Tình huống giả định: Lọc bỏ dữ liệu ngoại lai (Outliers).
Trong thực tế, dữ liệu hiếm khi hoàn hảo như bộ dữ liệu này. Chúng thường chứa các giá trị ngoại lai (outliers) - những giá trị vô lý (ví dụ: bài hát dài 0.1 giây, hoặc dài 50 phút). Những giá trị này có thể làm hỏn g các phân tích thống kê (ví dụ: kéo giá trị trung bình lên cao hoặc thấp một cách giả tạo) và làm méo mó thang đo của biểu đồ.
Thao tác này giả định rằng các bài hát quá ngắn (< 30 giây) hoặc quá dài (> 20 phút) là dữ liệu lỗi và lọc bỏ chúng. Đây là một kỹ thuật làm sạch dữ liệu quan trọng.
# Thao tác 8: Lọc bỏ dữ liệu ngoại lai (Giả định)
nrow_before <- nrow(spotify_processed) #1
spotify_processed <- spotify_processed %>% #2
filter(duration_min > 0.5 & duration_min < 20) %>% #3
filter(decade >= 1920) #4
nrow_after <- nrow(spotify_processed) #5
print(paste("Đã lọc bỏ (giả định):", nrow_before - nrow_after, "quan sát.")) #6
## [1] "Đã lọc bỏ (giả định): 489 quan sát."
Giải thích:
Câu lệnh #1 nrow_before <- nrow(spotify_processed). Sử dụng hàm nrow() để đếm tổng số hàng hiện có của spotify_processed trước khi thực hiện thao tác lọc. Kết quả được lưu vào biến nrow_before để đối chiếu sau.
Câu lệnh #2 spotify_processed <- spotify_processed %>%. Ý nghĩa: Bắt đầu một chuỗi thao tác pipe (%>%) trên bảng spotify_processed và gán kết quả cuối cùng ngược trở lại chính nó.
Câu lệnh #3 filter(duration_min > 0.5 & duration_min < 20) %>%. Ý nghĩa: Sử dụng hàm filter() của dplyr để lọc (giữ lại) các hàng. Chỉ giữ lại những hàng thỏa mãn đồng thời (&) hai điều kiện: duration_min (thời lượng, phút) lớn hơn 0.5 (30 giây) và duration_min nhỏ hơn 20 (20 phút). Những bài hát quá ngắn hoặc quá dài (được coi là ngoại lai) sẽ bị loại bỏ.
Câu lệnh #4 filter(decade >= 1920). Đây là thao tác lọc thứ hai, áp dụng trên kết quả của thao tác lọc thứ nhất. Chỉ giữ lại những hàng có giá trị decade (thập kỷ) lớn hơn hoặc bằng 1920. Thao tác này giúp đồng bộ dữ liệu với biến decade đã tạo (vốn bắt đầu từ 1920).
Câu lệnh #5 nrow_after <- nrow(spotify_processed): Đếm lại tổng số hàng của spotify_processed sau khi đã lọc xong và lưu vào biến nrow_after.
Câu lệnh #6 print(paste(“Đã lọc bỏ (giả định):”, nrow_before - nrow_after, “quan sát.”)): In ra một thông báo, thực hiện phép trừ nrow_before - nrow_after để báo cáo chính xác có bao nhiêu hàng (quan sát) đã bị loại bỏ khỏi phân tích.
Nhận xét: Thao tác này minh họa một kỹ thuật làm sạch dữ liệu quan trọng trong thực tế. Việc giả định và lọc bỏ các giá trị ngoại lai là cần thiết để đảm bảo tính ổn định của các phép tính thống kê (như tính trung bình) và tính thẩm mỹ của biểu đồ (tránh bị méo mó thang đo). Phép tính nrow_before - nrow_after giúp theo dõi được quy trình làm sạch đã tác động đến dữ liệu nhiều như thế nào.
#1.3.9. Tình huống giả định: Xử lý dữ liệu thiếu (Imputation).
Như đã phát hiện ở phần 1.2.7, bộ dữ liệu này không có giá trị thiếu (NA). Tuy nhiên, trong thực tế, NA rất phổ biến. Việc xóa tất cả các hàng có NA (na.omit()) thường là một giải pháp tồi, vì nó có thể loại bỏ một lượng lớn dữ liệu quý giá ở các cột khác.
Một kỹ thuật phổ biến là Imputation (Gán giá trị thay thế). Thao tác này sẽ cố tình tạo ra 100 giá trị NA giả định trong cột tempo và sửa chữa chúng bằng cách lấp đầy các ô trống đó bằng giá trị trung bình của cả cột.
# Thao tác 9: Xử lý NA giả định
# --- GIAI ĐOẠN 1: TẠO 100 GIÁ TRỊ NA GIẢ ĐỊNH ---
set.seed(123) # 1
na_indices <- sample(1:nrow(spotify_processed), 100) # 2
spotify_processed$tempo[na_indices] <- NA # 3
print(paste("Số lượng NA vừa tạo ra trong cột 'tempo':", sum(is.na(spotify_processed$tempo)))) # 4
## [1] "Số lượng NA vừa tạo ra trong cột 'tempo': 100"
# --- GIAI ĐOẠN 2: XỬ LÝ (MEAN IMPUTATION - GÁN GIÁ TRỊ TRUNG BÌNH) ---
mean_tempo <- mean(spotify_processed$tempo, na.rm = TRUE) # 5
spotify_processed <- spotify_processed %>% # 6
mutate(tempo = ifelse(is.na(tempo), mean_tempo, tempo)) # 7
print(paste("Số lượng NA còn lại trong cột 'tempo' sau khi xử lý:", sum(is.na(spotify_processed$tempo)))) # 8
## [1] "Số lượng NA còn lại trong cột 'tempo' sau khi xử lý: 0"
Giải thích:
Câu lệnh #1 set.seed(123): Khởi tạo “hạt giống” (seed) cho trình tạo số ngẫu nhiên của R. Việc này đảm bảo tính tái lập (reproducibility): bất kỳ ai chạy lại mã này cũng sẽ chọn được chính xác 100 vị trí ngẫu nhiên giống nhau để tạo NA.
Câu lệnh #2 na_indices <- sample(1:nrow(spotify_processed), 100): Tạo một vector na_indices chứa 100 số nguyên, được chọn ngẫu nhiên (sample) từ dãy số từ 1 đến tổng số hàng (nrow(spotify_processed)). Đây chính là 100 chỉ số hàng sẽ bị làm “bẩn”.
Câu lệnh #3 spotify_processed\(tempo[na_indices] <- NA: Truy cập vào cột tempo (\)tempo), sử dụng 100 chỉ số hàng ngẫu nhiên ([na_indices]) và gán (<-) cho chúng giá trị NA (Missing Value).
Câu lệnh #4 print(paste(… sum(is.na(spotify_processed$tempo)))): Kiểm tra giai đoạn 1. Hàm sum(is.na(…)) đếm tổng số NA trong cột tempo. Kết quả in ra phải là 100.
Câu lệnh #5 mean_tempo <- mean(spotify_processed$tempo, na.rm = TRUE): Tính giá trị trung bình của cột tempo và lưu vào biến mean_tempo.
na.rm = TRUE: Đây là tham số bắt buộc. Nó ra lệnh cho hàm mean() hãy loại bỏ (rm - remove) các giá trị NA (mà chúng ta vừa tạo) trước khi tính trung bình. Nếu không có tham số này, kết quả của phép mean() sẽ là NA.
Câu lệnh #6 và #7 … %>% mutate(tempo = ifelse(is.na(tempo), mean_tempo, tempo)): Đây là lệnh xử lý cốt lõi. Hàm mutate() cập nhật lại cột tempo.
ifelse(…): Hàm ifelse kiểm tra logic trên từng hàng của cột tempo:
is.na(tempo): “Nếu (if) giá trị tempo của hàng này là NA…”.
mean_tempo: “…thì hãy thay thế nó bằng giá trị mean_tempo (đã tính ở bước 5).”.
tempo: “…ngược lại (else), hãy giữ nguyên giá trị tempo cũ của nó.”.
Câu lệnh #8 print(paste(… sum(is.na(spotify_processed$tempo)))): Kiểm tra giai đoạn 2. Đếm lại số NA còn lại trong cột tempo. Kết quả in ra phải là 0, xác nhận 100 giá trị NA đã được xử lý thành công.
Nhận xét: Thao tác này minh họa kỹ thuật “gán giá trị thay thế bằng trung bình” (mean imputation), một phương pháp cơ bản và phổ biến để xử lý dữ liệu thiếu trong thực tế, giúp giữ lại các hàng dữ liệu thay vì phải xóa bỏ chúng.
#1.3.10. Tình huống giả định: Chuẩn hóa biến (Standardization)
Các biến số thường có các thang đo (scale) rất khác nhau. Ví dụ, popularity chạy từ 0-100, trong khi loudness (âm lượng) chạy từ -60 đến 0.
Trong nhiều mô hình phân tích nâng cao (như Machine Learning, ví dụ: K-Means Clustering, PCA), sự chênh lệch này có thể làm cho mô hình bị sai lệch (ví dụ: mô hình sẽ coi popularity “quan trọng” hơn loudness chỉ vì thang đo của nó lớn hơn).
Thao tác “Standardization” (Chuẩn hóa) sẽ đưa tất cả các biến về một thang đo chung (thường là Z-score, với trung bình = 0 và độ lệch chuẩn = 1). Thao tác này giả định chúng ta cần chuẩn bị dữ liệu cho một mô hình nâng cao.
# Thao tác 10: Chuẩn hóa biến 'loudness' (Giả định)
spotify_processed <- spotify_processed %>% # 1
mutate(loudness_scaled = scale(loudness)) # 2
head(spotify_processed[c("loudness", "loudness_scaled")]) # 3
## # A tibble: 6 × 2
## loudness loudness_scaled[,1]
## <dbl> <dbl>
## 1 -20.1 -1.52
## 2 -12.4 -0.174
## 3 -14.8 -0.599
## 4 -9.32 0.376
## 5 -10.1 0.239
## 6 -12.5 -0.186
Giải thích:
Câu lệnh #1 spotify_processed <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả cuối cùng ngược trở lại spotify_processed.
Câu lệnh #2 mutate(loudness_scaled = scale(loudness)): Sử dụng mutate() để tạo một cột mới tên là loudness_scaled.
scale(loudness): Đây là lệnh R cơ bản dùng để thực hiện chuẩn hóa Z-score.
Hàm scale() sẽ tự động: Tính giá trị trung bình (mean) và độ lệch chuẩn (standard deviation) của cột loudness. Đối với mỗi giá trị loudness, nó sẽ lấy giá trị đó trừ đi trung bình, rồi chia cho độ lệch chuẩn.
Câu lệnh #3 head(spotify_processed[c(“loudness”, “loudness_scaled”)]): Lệnh này dùng để kiểm tra kết quả.
[c(“loudness”, “loudness_scaled”)]: Cú pháp này chọn (subset) chỉ 2 cột từ data frame: cột loudness (gốc) và cột loudness_scaled (mới).
head(…): In ra 6 dòng đầu tiên của 2 cột này, giúp so sánh trực tiếp giá trị “trước” và “sau” khi chuẩn hóa.
Nhận xét: Kết quả head() cho thấy các giá trị loudness gốc (ví dụ: -20.096, -12.441) đã được chuyển đổi thành các giá trị Z-score (ví dụ: -1.63, -0.36) xoay quanh 0. Việc chuẩn hóa này đảm bảo các biến có thang đo khác nhau sẽ có “trọng số” ngang bằng nhau khi đưa vào các mô hình phân tích nâng cao.
#1.4. Phân tích thống kê mô tả.
Mục tiêu: Sau khi dữ liệu đã được làm sạch, tiền xử lý và làm giàu ở phần 1.3, giai đoạn này sẽ thực hiện các phép tính thống kê mô tả. Mục đích là để lượng hóa các đặc điểm của dữ liệu, trả lời các câu hỏi như “Sự phân bổ của biến này như thế nào?”, “Giá trị trung tâm là gì?”, “Có bao nhiêu quan sát trong mỗi nhóm?”.
Phần này sẽ sử dụng data frame spotify_processed để thực hiện 24 thao tác, tập trung vào việc tính toán các bản tóm tắt tổng quan, thống kê tần suất (cho các biến phân loại) và các bảng chéo (crosstabs). Quan trọng hơn, phần này sẽ tính toán các thống kê mô tả theo nhóm (ví dụ: giá trị trung bình theo thập kỷ) và phân tích tương quan. Các kết quả (bảng, số liệu) thu được trong phần này chính là nền tảng dữ liệu thô (data source) để trực quan hóa và rút ra kết luận ở phần 1.5.
#1.4.1. Thống kê tổng quan.
Bước đầu tiên là xem xét một bản tóm tắt thống kê 5 số (Min, Q1, Median, Q3, Max) cùng với giá trị trung bình (Mean) cho tất cả các biến số (numeric) trong bộ dữ liệu.
# Thao tác 1: Tóm tắt 5 số liệu (và trung bình)
summary(spotify_processed)
## valence year acousticness artists
## Min. :0.0000 Min. :1921 Min. :0.0000 Length:170164
## 1st Qu.:0.3180 1st Qu.:1956 1st Qu.:0.1010 Class :character
## Median :0.5400 Median :1978 Median :0.5140 Mode :character
## Mean :0.5291 Mean :1977 Mean :0.5013
## 3rd Qu.:0.7470 3rd Qu.:1999 3rd Qu.:0.8920
## Max. :1.0000 Max. :2020 Max. :0.9960
## danceability energy explicit instrumentalness
## Min. :0.0000 Min. :0.0000 Min. :0.00000 Min. :0.000000
## 1st Qu.:0.4160 1st Qu.:0.2550 1st Qu.:0.00000 1st Qu.:0.000000
## Median :0.5480 Median :0.4720 Median :0.00000 Median :0.000212
## Mean :0.5378 Mean :0.4829 Mean :0.08479 Mean :0.166416
## 3rd Qu.:0.6680 3rd Qu.:0.7040 3rd Qu.:0.00000 3rd Qu.:0.099500
## Max. :0.9880 Max. :1.0000 Max. :1.00000 Max. :1.000000
## key liveness loudness mode
## Min. : 0.0 Min. :0.0000 Min. :-60.000 Min. :0.000
## 1st Qu.: 2.0 1st Qu.:0.0987 1st Qu.:-14.593 1st Qu.:0.000
## Median : 5.0 Median :0.1350 Median :-10.568 Median :1.000
## Mean : 5.2 Mean :0.2054 Mean :-11.451 Mean :0.707
## 3rd Qu.: 8.0 3rd Qu.:0.2610 3rd Qu.: -7.175 3rd Qu.:1.000
## Max. :11.0 Max. :1.0000 Max. : 3.855 Max. :1.000
## name popularity speechiness tempo
## Length:170164 Min. : 0.00 Min. :0.00000 Min. : 0.00
## Class :character 1st Qu.: 11.00 1st Qu.:0.03490 1st Qu.: 93.49
## Mode :character Median : 34.00 Median :0.04500 Median :114.79
## Mean : 31.49 Mean :0.09819 Mean :116.90
## 3rd Qu.: 48.00 3rd Qu.:0.07560 3rd Qu.:135.56
## Max. :100.00 Max. :0.97000 Max. :243.51
## duration_min explicit_label popularity_level
## Min. : 0.5007 Không nhạy cảm:155735 Length:170164
## 1st Qu.: 2.8320 Có nhạy cảm : 14429 Class :character
## Median : 3.4567 Mode :character
## Mean : 3.8074
## 3rd Qu.: 4.3685
## Max. :19.9831
## energy_level decade loudness_scaled.V1
## Length:170164 Min. :1920 Min. :-8.550667168750
## Class :character 1st Qu.:1950 1st Qu.:-0.553482496285
## Mode :character Median :1970 Median : 0.155457641533
## Mean :1972 Mean : 0.000000000000
## 3rd Qu.:1990 3rd Qu.: 0.753043850493
## Max. :2020 Max. : 2.695683368570
Giải thích:
Câu lệnh summary(spotify_processed): summary() là một hàm R cơ bản, cung cấp một cái nhìn tổng quan và cực kỳ nhanh chóng về phân phối của mọi cột trong data frame.
Kết quả: Hàm này trả về một bảng tóm tắt:
Đối với biến số (numeric) (như danceability, energy): Nó cung cấp Tứ phân vị (Min, 1st Qu., Median, 3rd Qu., Max) và giá trị Trung bình (Mean).
Đối với biến phân loại (factor) (như explicit_label đã tạo): Nó tự động thực hiện một phép đếm tần suất (frequency count), cho biết có bao nhiêu quan sát trong mỗi nhóm.
Nhận xét: Thao tác này rất quan trọng để phát hiện nhanh các giá trị ngoại lai (outliers) hoặc các vấn đề về thang đo (ví dụ: nếu cột popularity có giá trị Max là 1000 thay vì 100, chúng ta sẽ thấy ngay). Nó cũng cho ta cái nhìn đầu tiên về xu hướng trung tâm (Median - Trung vị) và độ phân tán của từng thuộc tính âm nhạc.
#1.4.2. Thống kê tần suất (biến phân loại).
Phần này tập trung vào việc đếm số lượng quan sát (bài hát) rơi vào mỗi nhóm (level) của các biến phân loại đã được mã hóa ở phần 1.3.
#1.4.2.1. Bảng tần suất các biến đã mã hóa. Thao tác này đếm số lượng bài hát trong từng nhóm popularity_level, energy_level, và explicit_label.
# Thao tác 2: Tần suất Mức độ Phổ biến
print("Tần suất Mức độ Phổ biến:") # 1
## [1] "Tần suất Mức độ Phổ biến:"
table(spotify_processed$popularity_level) # 2
##
## Cao Rất cao Thấp Trung bình
## 35727 549 54045 79843
# Thao tác 3: Tần suất Mức độ Năng lượng
print("Tần suất Mức độ Năng lượng:") # 3
## [1] "Tần suất Mức độ Năng lượng:"
table(spotify_processed$energy_level) # 4
##
## Cao Thấp Trung bình
## 50386 58260 61518
# Thao tác 4: Tần suất Nội dung Nhạy cảm
print("Tần suất Nội dung Nhạy cảm:") # 5
## [1] "Tần suất Nội dung Nhạy cảm:"
table(spotify_processed$explicit_label) # 6
##
## Không nhạy cảm Có nhạy cảm
## 155735 14429
Giải thích:
Câu lệnh #1, #3, #5 print(…): Sử dụng hàm print() để in ra một chuỗi văn bản (tiêu đề mô tả) trước mỗi kết quả. Việc này giúp báo cáo đầu ra (output) dễ đọc và rõ ràng hơn.
Câu lệnh #2, #4, #6 table(spotify_processed$…): Hàm table() là hàm chính để tính toán bảng tần suất (frequency table) cho một biến phân loại (biến đã được mã hóa ở phần 1.3). Hàm này đếm số lần xuất hiện (số lượng bài hát) cho mỗi nhóm (level) duy nhất trong cột được chỉ định (ví dụ: “Thấp”, “Trung bình”, “Cao”…).
Nhận xét: Kết quả của 3 thao tác này cho thấy sự phân bổ tổng quan của các bài hát theo các nhóm đã định nghĩa. Từ các bảng này, có thể thấy ngay nhóm phổ biến nào chiếm đa số, sự phân chia của các mức năng lượng, hoặc tỷ lệ bài hát có nội dung nhạy cảm là bao nhiêu..
#1.4.2.2.Thống kê tần suất (iến gốc và biến mã hóa). Phần này sẽ đếm số lượng bài hát trong mỗi Thập kỷ (biến decade), mỗi Tông nhạc (biến key), và mỗi Thang âm (biến mode) để hiểu sự phân bổ của các đặc tính âm nhạc cơ bản.
# Thao tác 5: 10 thập kỷ có nhiều bài hát nhất
print("Top 10 thập kỷ có nhiều bài hát nhất:") # 1
## [1] "Top 10 thập kỷ có nhiều bài hát nhất:"
spotify_processed %>% # 2
count(decade, sort = TRUE) %>% #3
head(10) %>% #4
print() #5
## # A tibble: 10 × 2
## decade n
## <dbl> <int>
## 1 1970 19951
## 2 1990 19892
## 3 1980 19835
## 4 2010 19765
## 5 1950 19757
## 6 2000 19633
## 7 1960 19500
## 8 1940 15221
## 9 1930 9463
## 10 1920 5117
# Thao tác 6: 10 tông nhạc (Key) phổ biến nhất
print("Top 10 tông nhạc (Key) phổ biến nhất:") # 6
## [1] "Top 10 tông nhạc (Key) phổ biến nhất:"
spotify_processed %>% #7
count(key, sort = TRUE) %>% #8
head(10) %>% #9
print() #10
## # A tibble: 10 × 2
## key n
## <dbl> <int>
## 1 0 21547
## 2 7 20759
## 3 2 18745
## 4 9 17543
## 5 5 16381
## 6 4 12899
## 7 1 12847
## 8 10 12094
## 9 8 10709
## 10 11 10641
# Thao tác 7: Thống kê thang âm (Mode)
print("Thống kê Thang âm (1=Trưởng, 0=Thứ):") # 11
## [1] "Thống kê Thang âm (1=Trưởng, 0=Thứ):"
table(spotify_processed$mode) # 12
##
## 0 1
## 49861 120303
Giải thích:
Câu lệnh #1, #6, #11 print(…): In ra các tiêu đề mô tả để giúp phân tách và làm rõ các bảng kết quả trong output.
Câu lệnh #2, #3, #4, #5:
Câu lệnh #2, #3: spotify_processed %>% count(decade, sort = TRUE)
count(decade, sort = TRUE): Đây là một hàm của dplyr (thuộc tidyverse). Nó thực hiện 3 việc: Đếm số lượng bài hát (n) cho mỗi nhóm decade (thập kỷ), tự động sắp xếp (sort = TRUE) kết quả theo thứ tự giảm dần (từ cao xuống thấp), trả về một data frame (tibble) chứa kết quả. Hàm count() này hiệu quả hơn hàm table() (R cơ bản) vì nó trả về một data frame có cấu trúc và cho phép sắp xếp ngay lập tức.
Câu lệnh #4: head(10): Sau khi đã sắp xếp, hàm head() được dùng để trích xuất và chỉ giữ lại 10 hàng đầu tiên (tức 10 thập kỷ phổ biến nhất).
Câu lệnh #5: print(): In data frame kết quả (Top 10) ra màn hình.
Câu lệnh #7, #8, #9, #10: Cụm lệnh này có logic tương tự thao tác 5, nhưng áp dụng cho cột key (tông nhạc) để tìm ra 10 tông nhạc được sử dụng nhiều nhất.
Câu lệnh #12 table(spotify_processed$mode): Sử dụng hàm table() (R cơ bản) để đếm tần suất cho cột mode (thang âm). Tại sao dùng table()? Vì cột mode chỉ có 2 giá trị (0 = Thứ, 1 = Trưởng), chúng ta không cần sắp xếp hay lấy Top 10. Do đó, dùng table() là cách đơn giản và nhanh gọn nhất để xem sự phân bổ giữa hai nhóm này.
Nhận xét: Các thao tác này cho thấy sự phân bổ của các đặc tính âm nhạc. Việc sử dụng count() cho decade và key giúp nhanh chóng xác định các nhóm phổ biến nhất, trong khi table() phù hợp cho biến nhị phân (binary) như mode.
#1.4.3. Thống kê bảng chéo (Crosstabs).
Phần này nâng cao hơn thống kê tần suất bằng cách xem xét mối quan hệ giữa hai biến phân loại cùng một lúc. Bảng chéo (crosstab) sẽ đếm số lượng quan sát (bài hát) dựa trên sự kết hợp của hai nhóm.
#1.4.3.1. Bảng chéo Phổ biến vs Năng lượng.
Tạo một bảng tóm tắt đếm số lượng bài hát cho mỗi sự kết hợp giữa popularity_level và energy_level. Bảng này là nguồn dữ liệu để vẽ biểu đồ cột nhóm ở phần 1.5.
# Thao tác 8: Bảng chéo Phổ biến vs. Năng lượng
pop_energy_summary <- spotify_processed %>% # 1
group_by(popularity_level, energy_level) %>% # 2
summarise(So_bai_hat = n(), .groups = "drop") # 3
print(pop_energy_summary) # 4
## # A tibble: 12 × 3
## popularity_level energy_level So_bai_hat
## <chr> <chr> <int>
## 1 Cao Cao 17029
## 2 Cao Thấp 4378
## 3 Cao Trung bình 14320
## 4 Rất cao Cao 229
## 5 Rất cao Thấp 32
## 6 Rất cao Trung bình 288
## 7 Thấp Cao 3344
## 8 Thấp Thấp 35398
## 9 Thấp Trung bình 15303
## 10 Trung bình Cao 29784
## 11 Trung bình Thấp 18452
## 12 Trung bình Trung bình 31607
Giải thích:
Câu lệnh #1 pop_energy_summary <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) trên bảng spotify_processed và gán kết quả cuối cùng vào một biến mới tên là pop_energy_summary.
Câu lệnh #2 group_by(popularity_level, energy_level) %>%: Đây là một hàm của dplyr. Nó “chia” bộ dữ liệu (vẫn còn hơn 170,000 hàng) thành các nhóm nhỏ. Một nhóm được tạo ra cho mỗi sự kết hợp duy nhất giữa hai cột. Ví dụ: (“Cao”, “Thấp”), (“Cao”, “Trung bình”), (“Cao”, “Cao”), (“Trung bình”, “Thấp”), v.v.
Câu lệnh #3 summarise(So_bai_hat = n(), .groups = “drop”): Hàm summarise() (hoặc summarize) của dplyr được dùng để tính toán thống kê tóm tắt cho mỗi nhóm đã được tạo ở Câu lệnh #2.
So_bai_hat = n(): Đây là logic đếm cốt lõi. n() là một hàm đặc biệt của dplyr (chỉ dùng bên trong summarise), nó đơn giản là đếm số lượng hàng (bài hát) có trong nhóm hiện tại. Kết quả đếm này được lưu vào một cột mới tên là So_bai_hat.
.groups = “drop”: Đây là một tùy chọn kỹ thuật. Sau khi summarise xong, nó gỡ bỏ cấu trúc nhóm, giúp data frame pop_energy_summary trở lại bình thường.
Câu lệnh #4 print(pop_energy_summary): In bảng kết quả pop_energy_summary ra màn hình.
Nhận xét: Kết quả của thao tác này là một bảng tóm tắt mới (ví dụ: chỉ có 12 hàng = 4 nhóm phổ biến * 3 nhóm năng lượng). Bảng này không còn chứa 170,000 bài hát nữa, mà thay vào đó chứa số lượng đếm của chúng. Đây chính là bảng dữ liệu nguồn (data source) sẽ được sử dụng để vẽ biểu đồ cột nhóm (Dodged Bar Chart) ở phần 1.5.
#1.4.3.2. Bảng chéo Phổ biến vs Nhạy cảm.
Thao tác này đếm xem có bao nhiêu bài hát “Không Nhạy Cảm” và “Có Nhạy Cảm” trong từng nhóm phổ biến. Dữ liệu này dùng để kiểm tra giả thuyết: “Liệu các bài hát nổi tiếng có xu hướng chứa nội dung nhạy cảm nhiều hơn không?”
# Thao tác 9: Bảng chéo Phổ biến vs Nhạy cảm
pop_explicit_summary <- spotify_processed %>% # 1
group_by(popularity_level, explicit_label) %>% # 2
summarise(So_bai_hat = n(), .groups = "drop") # 3
print(pop_explicit_summary) # 4
## # A tibble: 8 × 3
## popularity_level explicit_label So_bai_hat
## <chr> <fct> <int>
## 1 Cao Không nhạy cảm 28615
## 2 Cao Có nhạy cảm 7112
## 3 Rất cao Không nhạy cảm 353
## 4 Rất cao Có nhạy cảm 196
## 5 Thấp Không nhạy cảm 51471
## 6 Thấp Có nhạy cảm 2574
## 7 Trung bình Không nhạy cảm 75296
## 8 Trung bình Có nhạy cảm 4547
Giải thích:
Câu lệnh #1 pop_explicit_summary <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả tóm tắt cuối cùng vào một biến mới tên là pop_explicit_summary.
Câu lệnh #2 group_by(popularity_level, explicit_label) %>%: Sử dụng hàm group_by() của dplyr để “chia” bộ dữ liệu thành các nhóm. Một nhóm được tạo ra cho mỗi sự kết hợp duy nhất giữa hai cột này. Ví dụ: (“Cao”, “Không Nhạy Cảm”), (“Cao”, “Có Nhạy Cảm”), (“Thấp”, “Không Nhạy Cảm”), v.v.
Câu lệnh #3 summarise(So_bai_hat = n(), .groups = “drop”): Hàm summarise() (tóm tắt) được thực thi trên mỗi nhóm đã tạo.
So_bai_hat = n(): Hàm n() đếm số lượng hàng (bài hát) trong mỗi nhóm kết hợp đó và lưu kết quả vào cột mới So_bai_hat.
.groups = “drop”: Gỡ bỏ cấu trúc nhóm sau khi tóm tắt xong, trả data frame về trạng thái bình thường.
Câu lệnh #4 print(pop_explicit_summary): In bảng tóm tắt kết quả ra màn hình.
Nhận xét: Logic của thao tác này tương tự thao tác 8. Kết quả là một bảng tóm tắt mới (ví dụ: 8 hàng = 4 nhóm phổ biến * 2 nhóm nhạy cảm). Bảng này là cơ sở dữ liệu nguồn để vẽ biểu đồ cột chồng 100% (100% Stacked Bar Chart) ở Phần 1.5, nhằm so sánh tỷ lệ nội dung nhạy cảm giữa các nhóm phổ biến.
#1.4.3.3. Bảng chéo Thập kỉ vs Nhạy cảm.
Thao tác này đếm xem có bao nhiêu bài hát “Không Nhạy Cảm” và “Có Nhạy Cảm” trong từng thập kỷ. Dữ liệu này dùng để kiểm tra giả thuyết: “Liệu các bài hát có nội dung nhạy cảm chỉ mới xuất hiện gần đây hay không?”
# Thao tác 10: Bảng chéo Thập kỷ vs Nhạy cảm
decade_explicit_summary <- spotify_processed %>% # 1
group_by(decade, explicit_label) %>% # 2
summarise(So_bai_hat = n(), .groups = "drop") # 3
print(decade_explicit_summary) # 4
## # A tibble: 22 × 3
## decade explicit_label So_bai_hat
## <dbl> <fct> <int>
## 1 1920 Không nhạy cảm 4432
## 2 1920 Có nhạy cảm 685
## 3 1930 Không nhạy cảm 8872
## 4 1930 Có nhạy cảm 591
## 5 1940 Không nhạy cảm 14128
## 6 1940 Có nhạy cảm 1093
## 7 1950 Không nhạy cảm 19603
## 8 1950 Có nhạy cảm 154
## 9 1960 Không nhạy cảm 19489
## 10 1960 Có nhạy cảm 11
## # ℹ 12 more rows
Giải thích:
Câu lệnh #1 decade_explicit_summary <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả tóm tắt cuối cùng vào một biến mới tên là decade_explicit_summary.
Câu lệnh #2 group_by(decade, explicit_label) %>%: Sử dụng group_by() của dplyr để “chia” bộ dữ liệu thành các nhóm, dựa trên sự kết hợp của 2 cột: decade (ví dụ: 1990) và explicit_label (ví dụ: “Có Nhạy Cảm”).
Câu lệnh #3 summarise(So_bai_hat = n(), .groups = “drop”): Hàm summarise() được thực thi trên mỗi nhóm.
So_bai_hat = n(): Hàm n() đếm số lượng hàng (bài hát) trong mỗi nhóm kết hợp đó (ví dụ: đếm số bài hát “Có Nhạy Cảm” trong thập kỷ 1990) và lưu kết quả vào cột So_bai_hat.
Câu lệnh #4 print(decade_explicit_summary): In bảng tóm tắt kết quả ra màn hình.
Nhận xét: Bảng tóm tắt này là dữ liệu nguồn cho phép chúng ta trực quan hóa và xem xét sự thay đổi (nếu có) của nội dung nhạy cảm trong âm nhạc qua các thời kỳ.
#1.4.3.4. Bảng chéo Phổ biến vs Tông nhạc (Key):
Đây là một phân tích chi tiết hơn, nhằm kiểm tra giả thuyết: “Liệu có ‘tông nhạc vàng’ (golden key) nào thường xuất hiện trong các bài hát ‘Rất cao’ về độ phổ biến không?”.
# Thao tác 11: Bảng chéo Phổ biến vs. Tông nhạc (Key)
pop_key_summary <- spotify_processed %>% # 1
group_by(popularity_level, key) %>% # 2
summarise(So_bai_hat = n(), .groups = "drop") # 3
print(pop_key_summary) # 4
## # A tibble: 48 × 3
## popularity_level key So_bai_hat
## <chr> <dbl> <int>
## 1 Cao 0 4128
## 2 Cao 1 3633
## 3 Cao 2 3697
## 4 Cao 3 1146
## 5 Cao 4 2742
## 6 Cao 5 3033
## 7 Cao 6 2346
## 8 Cao 7 3978
## 9 Cao 8 2463
## 10 Cao 9 3548
## # ℹ 38 more rows
Giải thích:
Câu lệnh #1 pop_key_summary <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả tóm tắt cuối cùng vào một biến mới tên là pop_key_summary.
Câu lệnh #2 group_by(popularity_level, key) %>%: Sử dụng group_by() của dplyr để “chia” bộ dữ liệu thành các nhóm, dựa trên sự kết hợp của 2 cột: popularity_level (ví dụ: “Rất cao”) và key (ví dụ: 0, 1, 2,… 11).
Câu lệnh #3 summarise(So_bai_hat = n(), .groups = “drop”): Hàm summarise() (tóm tắt) được thực thi trên mỗi nhóm.
So_bai_hat = n(): Hàm n() đếm số lượng hàng (bài hát) trong mỗi nhóm kết hợp đó (ví dụ: đếm số bài hát có key = 7 thuộc nhóm popularity_level = “Rất cao”) và lưu kết quả vào cột So_bai_hat.
Câu lệnh #4 print(pop_key_summary): In bảng tóm tắt kết quả ra màn hình.
Nhận xét: Thao tác này tạo ra một bảng tóm tắt chi tiết, đếm số lượng bài hát cho từng tông nhạc (0-11) bên trong mỗi nhóm phổ biến (“Thấp”, “Trung bình”, “Cao”, “Rất cao”).
#1.4.4. Thống kê mô tả theo nhóm (Grouped Summaries).
Thay vì chỉ đếm (như table hay count), phần này sẽ tính toán các chỉ số thống kê (như mean - trung bình) cho các biến số, nhưng được nhóm theo các biến phân loại.
#1.4.4.1. Xu hướng âm nhạc theo Thập kỉ.
Thao tác này sẽ tính toán các giá trị trung bình của các thuộc tính âm nhạc chính (ví dụ: acousticness, energy, valence, danceability) cho từng thập kỷ (decade).
# Thao tác 12-15: Tính toán các giá trị trung bình theo thập kỷ
trends_over_time <- spotify_processed %>% # 1
group_by(decade) %>% # 2
summarise( # 3
mean_acousticness = mean(acousticness), # 4
mean_energy = mean(energy), # 5
mean_valence = mean(valence), # 6
mean_danceability = mean(danceability) # 7
)
print("Thống kê xu hướng âm nhạc trung bình theo thập kỷ:") # 8
## [1] "Thống kê xu hướng âm nhạc trung bình theo thập kỷ:"
print(trends_over_time) # 9
## # A tibble: 11 × 5
## decade mean_acousticness mean_energy mean_valence mean_danceability
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1920 0.801 0.235 0.543 0.587
## 2 1930 0.868 0.286 0.568 0.542
## 3 1940 0.871 0.255 0.490 0.475
## 4 1950 0.840 0.287 0.479 0.477
## 5 1960 0.624 0.415 0.552 0.495
## 6 1970 0.400 0.534 0.586 0.525
## 7 1980 0.298 0.595 0.565 0.546
## 8 1990 0.307 0.586 0.545 0.566
## 9 2000 0.270 0.652 0.531 0.574
## 10 2010 0.264 0.629 0.457 0.598
## 11 2020 0.220 0.631 0.501 0.693
Giải thích:
Câu lệnh #1 trends_over_time <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) trên bảng spotify_processed và gán kết quả tóm tắt cuối cùng vào một biến mới tên là trends_over_time.
Câu lệnh #2 group_by(decade) %>%: Đây là một hàm của dplyr. Nó “chia” bộ dữ liệu (vẫn còn hơn 170,000 hàng) thành các nhóm dựa trên cột decade. Tất cả các bài hát thuộc thập kỷ 1920 sẽ được gom vào một nhóm, tất cả các bài hát thuộc 1930 vào một nhóm khác, v.v. (tổng cộng khoảng 10-11 nhóm).
Câu lệnh #3 summarise(…): Hàm summarise() (tóm tắt) của dplyr được thực thi trên mỗi nhóm (mỗi thập kỷ) đã được tạo ở câu lệnh #2.
Câu lệnh #4, #5, #6, #7 mean_acousticness = mean(acousticness), …: Đây là các phép tính bên trong summarise. Đối với mỗi nhóm thập kỷ, R sẽ tính giá trị trung bình (mean()) của các cột acousticness, energy, valence, và danceability cho tất cả các bài hát chỉ thuộc nhóm đó. Kết quả được gán vào các cột mới (ví dụ: mean_acousticness).
Câu lệnh #8 & #9 print(…): In tiêu đề và bảng trends_over_time (bảng tóm tắt) ra màn hình.
Nhận xét: Kết quả của thao tác này là một bảng tóm tắt mới (trends_over_time) rất nhỏ và quan trọng (chỉ khoảng 10-11 hàng, mỗi hàng là một thập kỷ). Bảng này không còn chứa 170,000 bài hát nữa, mà thay vào đó chứa các giá trị trung bình của từng thuộc tính theo thời gian. Đây chính là bảng dữ liệu nguồn (data source) cốt lõi, sẽ được sử dụng để vẽ biểu đồ đường (Line Chart hay time-series) ở Phần 1.5, nhằm xem các xu hướng âm nhạc này thay đổi thế nào qua 100 năm.
#1.4.4.2. Phân tích thuộc tính theo mức độ phổ biến.
Tương tự như trên, nhưng thay vì nhóm theo Thập kỷ, thao tác này sẽ nhóm theo popularity_level (“Thap”, “Trung binh”, “Cao”, “Rat cao”). Mục đích là để tìm ra “công thức” của một bài hát nổi tiếng: “Các bài hát ‘Rất cao’ về độ phổ biến thì có thuộc tính trung bình là bao nhiêu?”.
# Thao tác 16-19: Tính toán các giá trị trung bình theo Mức độ Phổ biến
stats_by_popularity <- spotify_processed %>% # 1
group_by(popularity_level) %>% # 2
summarise( # 3
mean_loudness = mean(loudness), # 4
mean_energy = mean(energy), # 5
mean_acousticness = mean(acousticness), # 6
mean_danceability = mean(danceability) # 7
)
print("Thống kê trung bình các thuộc tính theo Mức độ Phổ biến:") # 8
## [1] "Thống kê trung bình các thuộc tính theo Mức độ Phổ biến:"
print(stats_by_popularity) # 9
## # A tibble: 4 × 5
## popularity_level mean_loudness mean_energy mean_acousticness mean_danceability
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 Cao -8.11 0.619 0.280 0.593
## 2 Rất cao -6.54 0.612 0.251 0.678
## 3 Thấp -14.7 0.295 0.817 0.501
## 4 Trung bình -10.8 0.548 0.388 0.537
Giải thích:
Câu lệnh #1 stats_by_popularity <- spotify_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) trên bảng spotify_processed và gán kết quả tóm tắt cuối cùng vào một biến mới tên là stats_by_popularity.
Câu lệnh #2 group_by(popularity_level) %>%: Sử dụng group_by() của dplyr để “chia” bộ dữ liệu (hơn 170,000 hàng) thành 4 nhóm dựa trên cột popularity_level (“Thấp”, “Trung bình”, “Cao”, “Rất cao”).
Câu lệnh #3 summarise(…): Hàm summarise() (tóm tắt) của dplyr được thực thi trên mỗi nhóm (mỗi mức độ phổ biến) đã được tạo ở câu lệnh #2.
Câu lệnh #4, #5, #6, #7 mean_loudness = mean(loudness), …: Đây là các phép tính bên trong summarise. Đối với mỗi nhóm phổ biến (ví dụ: nhóm “Rất cao”), R sẽ tính giá trị trung bình (mean()) của các cột loudness, energy, acousticness, và danceability cho tất cả các bài hát chỉ thuộc nhóm đó.
Câu lệnh #8 & #9 print(…): In tiêu đề và bảng stats_by_popularity (bảng tóm tắt) ra màn hình.
Nhận xét: Bảng stats_by_popularity này chính là “công thức tạo hit” dưới dạng số liệu. Nó tóm tắt các đặc điểm âm thanh trung bình cho từng mức độ phổ biến. Bảng này là cơ sở dữ liệu nguồn, rất quan trọng để vẽ các biểu đồ so sánh (ví dụ: biểu đồ cột, biểu đồ hộp) ở Phần 1.5 nhằm trả lời câu hỏi “Bài hát nổi tiếng thì có đặc điểm gì?”.5.
#1.4.5. Thống kê tương quan.
Phần này sẽ tính toán hệ số tương quan (Correlation Coefficient) - một con số từ -1 đến 1 - để đo lường mức độ liên hệ tuyến tính giữa các cặp biến số. > +1: Tương quan thuận hoàn hảo (biến A tăng thì biến B tăng). > -1: Tương quan nghịch hoàn hảo (biến A tăng thì biến B giảm). > 0: Không có liên hệ tuyến tính.
#1.4.5.1. Tương quan theo cặp riêng lẻ.
# Thao tác 20: Tương quan Năng lượng vs Độ lớn
print("Tương quan giữa Energy và Loudness:") # 1
## [1] "Tương quan giữa Energy và Loudness:"
cor(spotify_processed$energy, spotify_processed$loudness) # 2
## [1] 0.7833619
# Thao tác 21: Tương quan Phổ biến vs Độ lớn
print("Tương quan giữa Popularity và Loudness:") # 3
## [1] "Tương quan giữa Popularity và Loudness:"
cor(spotify_processed$popularity, spotify_processed$loudness) # 4
## [1] 0.4567484
# Thao tác 22: Tương quan Phổ biến vs Độ mộc
print("Tương quan giữa Popularity và Acousticness:") # 5
## [1] "Tương quan giữa Popularity và Acousticness:"
cor(spotify_processed$popularity, spotify_processed$acousticness) # 6
## [1] -0.5725282
# Thao tác 23: Tương quan Phổ biến vs Năng lượng
print("Tương quan giữa Popularity và Energy:") # 7
## [1] "Tương quan giữa Popularity và Energy:"
cor(spotify_processed$popularity, spotify_processed$energy) # 8
## [1] 0.4846094
Giải thích:
Câu lệnh #1, #3, #5, #7 print(…): In ra một chuỗi văn bản mô tả (tiêu đề) cho mỗi kết quả tương quan, giúp báo cáo đầu ra (output) dễ đọc.
Câu lệnh #2, #4, #6, #8 cor(bien_A, bien_B): Đây là hàm cor() (R cơ bản) dùng để tính toán hệ số tương quan Pearson (mặc định) giữa hai vector số (hai cột). Ví dụ, cor(spotify_processed\(energy, spotify_processed\)loudness) sẽ tính toán mức độ liên hệ tuyến tính giữa cột energy và cột loudness. Kết quả: Mỗi lệnh cor() trả về một giá trị số duy nhất nằm trong khoảng từ -1 đến +1.
Nhận xét: Các con số này là những phát hiện thống kê quan-trọng. Ví dụ, kết quả cho Thao tác 20 (energy vs loudness) cho thấy một hệ số tương quan thuận mạnh (ví dụ: ~0.77), cho thấy hai thuộc tính này gần như di chuyển cùng nhau. Các thao tác 21-23 giúp lượng hóa mối liên hệ giữa độ phổ biến (popularity) và các đặc tính âm thanh khác.
#1.4.5.2. Ma trận tương quan toàn bộ.
Thay vì tính từng cặp riêng lẻ, thao tác này sẽ tính toán và hiển thị một bảng tóm tắt (ma trận) toàn bộ các mối quan hệ tương quan giữa 10 biến số chính. Bảng ma trận này là nền tảng cho biểu đồ nhiệt (Heatmap) ở phần 1.5.
# Thao tác 24: Tính toán na trận tương quan
numeric_vars <- spotify_processed %>% # 1
select(popularity, danceability, energy, loudness, speechiness, # 2
acousticness, instrumentalness, liveness, valence, tempo)
cor_matrix <- correlate(numeric_vars) # 3
print("Ma trận Tương quan:") # 4
## [1] "Ma trận Tương quan:"
print(cor_matrix) # 5
## # A tibble: 10 × 11
## term popularity danceability energy loudness speechiness acousticness
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 popularity NA 0.199 0.485 0.457 -0.171 -0.573
## 2 danceabili… 0.199 NA 0.221 0.284 0.235 -0.267
## 3 energy 0.485 0.221 NA 0.783 -0.0701 -0.750
## 4 loudness 0.457 0.284 0.783 NA -0.139 -0.563
## 5 speechiness -0.171 0.235 -0.0701 -0.139 NA -0.0456
## 6 acousticne… -0.573 -0.267 -0.750 -0.563 -0.0456 NA
## 7 instrument… -0.296 -0.277 -0.281 -0.409 -0.122 0.330
## 8 liveness -0.0748 -0.0992 0.127 0.0571 0.135 -0.0266
## 9 valence 0.0129 0.558 0.354 0.312 0.0463 -0.184
## 10 tempo 0.132 -0.000808 0.251 0.209 -0.0112 -0.207
## # ℹ 4 more variables: instrumentalness <dbl>, liveness <dbl>, valence <dbl>,
## # tempo <dbl>
Giải thích:
Câu lệnh #1 & #2 numeric_vars <- spotify_processed %>%
select(…): Sử dụng hàm select() của dplyr để chọn (subset) ra 10 cột
biến số (numeric) mà chúng ta muốn phân tích tương quan. Kết quả được
lưu vào một data frame mới tên là numeric_vars. Hàm tính tương quan (như
cor() hay correlate()) chỉ hoạt động trên các biến số (numeric). Bước
này là bắt buộc để loại bỏ các biến không phải là số (như biến ký tự
Câu lệnh #3 cor_matrix <- correlate(numeric_vars): Sử dụng hàm correlate() (từ gói corrr, đã được tải ở khối setup-all) để tính toán ma trận tương quan cho 10 cột trong bảng numeric_vars. Hàm correlate() này ưu việt hơn hàm cor() (R cơ bản) vì nó trả về kết quả dưới dạng một tibble (bảng) gọn gàng, “ngăn nắp” (tidy format), dễ đọc và cũng dễ dàng sử dụng để vẽ biểu đồ (rplot) sau này.
Câu lệnh #4 & #5 print(…): In tiêu đề và toàn bộ bảng ma trận tương quan cor_matrix ra màn hình.
Nhận xét: Bảng ma trận tương quan này là một kết quả thống kê quan trọng. Nó cho phép chúng ta quét nhanh toàn bộ 10 biến và xác định các cặp có mối quan hệ tuyến tính mạnh (ví dụ: hệ số gần +1 hoặc -1) hoặc yếu (hệ số gần 0). Đây là dữ liệu nguồn (data source) sẽ được dùng để trực quan hóa thành biểu đồ nhiệt (Heatmap) ở Phần 1.5.
#1.5. Trực quan hóa dữ liệu (Data Visualization).
Sau khi đã thực hiện các phân tích thống kê chi tiết và thu được các bảng dữ liệu tóm tắt ở phần 1.4, giai đoạn này sẽ sử dụng thư viện ggplot2 (thuộc tidyverse) để trực quan hóa các kết quả này. Trực quan hóa là một bước then chốt, giúp biến các con số và bảng biểu khô khan thành các biểu đồ có ý nghĩa, làm nổi bật các xu hướng, mẫu (patterns) và các điểm bất thường mà có thể khó phát hiện khi chỉ đọc số liệu.
Phần này sẽ trình bày 24 biểu đồ, được chia thành 5 nhóm chính:
Phân phối (1 biến): Sử dụng histogram để xem hình dạng phân bổ của các biến số.
Tần suất (biến phân loại): Sử dụng biểu đồ cột/tròn để xem số lượng trong các nhóm.
Quan hệ (Số vs Phân loại): Dùng biểu đồ hộp/violin để so sánh phân phối của một biến số giữa các nhóm.
Quan hệ (Số vs Số): Dùng biểu đồ tán xạ để xem mối tương quan tuyến tính.
Quan hệ (Nhiều biến): Dùng biểu đồ phức hợp (cột nhóm, cột chồng, heatmap, facet) để kể những câu chuyện dữ liệu đa chiều.
Mỗi biểu đồ được xây dựng dựa trên “Ngữ pháp Đồ thị” (Grammar of Graphics), bao gồm nhiều lớp (layers) được cộng lại với nhau.
#1.5.1. Trực quan hóa phân phối (1 biến).
Mục đích của nhóm biểu đồ này là sử dụng Histogram (biểu đồ tần suất) để kiểm tra hình dạng (shape), xu hướng trung tâm và độ phân tán của các biến số quan trọng.
#1.5.1.1. Đồ thị 1: Phân phối mức độ “dễ nhảy” (Danceability).
Thao tác này vẽ biểu đồ histogram cho biến danceability để trả lời câu hỏi: “Phần lớn các bài hát có xu hướng ‘dễ nhảy’ hay ‘khó nhảy’?”
# Đồ thị 1: Phân phối 'danceability'
ggplot(spotify_processed, aes(x = danceability)) + # 1
geom_histogram(bins = 40, fill = "darkgreen", color = "white", alpha = 0.7) + # 2
geom_vline(aes(xintercept = mean(danceability)), # 3
color = "red", linetype = "dashed", size = 1) +
labs(title = "Phân phối mức độ 'dễ nhảy' (Danceability)", # 4
subtitle = "Đường kẻ đỏ là giá trị trung bình",
x = "Mức độ dễ nhảy (0 = Khó, 1 = Dễ)",
y = "Số lượng bài hát") +
theme_minimal() + # 5
scale_y_continuous(labels = scales::comma) # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = danceability)) +: Khởi tạo một đối tượng ggplot.
spotify_processed: Chỉ định đây là data frame nguồn.
aes(x = danceability): “Ánh xạ” (aes - aesthetics) cột danceability vào trục x của biểu đồ. Dấu + dùng để “cộng” thêm các lớp (layers) tiếp theo.
Câu lệnh #2 geom_histogram(…) +: Thêm một lớp hình học (geom) là biểu đồ tần suất (histogram). Hàm này tự động “cắt” trục x thành các “thùng” (bins) và đếm số lượng bài hát rơi vào mỗi thùng, sau đó vẽ chiều cao cột tương ứng.
bins = 40: Tăng số lượng thùng lên 40 (thay vì 30 mặc định) để biểu đồ chi tiết, mượt mà hơn.
Câu lệnh #3 geom_vline(…) +: Thêm một lớp thống kê: một đường kẻ dọc (vline - vertical line).
aes(xintercept = mean(danceability)): Chỉ định vị trí (xintercept) của đường kẻ này chính là giá trị trung bình (mean) của cột danceability. Đường này giúp so sánh trực quan giữa phân phối và giá trị trung tâm.
Câu lệnh #4 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, giúp biểu đồ dễ hiểu hơn.
Câu lệnh #5 theme_minimal() +: Áp dụng một “giao diện” (theme) có sẵn. theme_minimal() loại bỏ nền xám mặc định, tạo một giao diện sạch sẽ, chuyên nghiệp.
Câu lệnh #6 scale_y_continuous(labels = scales::comma): Tùy chỉnh “thang đo” (scale) của trục y (là trục liên tục - continuous).
labels = scales::comma: Sử dụng hàm comma từ gói scales để tự động định dạng các số trên trục y có dấu phẩy hàng ngàn (ví dụ: 10000 -> 10,000), giúp đọc số liệu dễ dàng hơn.
Nhận xét: Biểu đồ histogram cho thấy phân phối của danceability có xu hướng lệch trái (left-skewed), với phần lớn các bài hát tập trung ở nửa bên phải của thang đo (từ 0.5 đến 0.8). Đường trung bình (màu đỏ) cũng nằm ở mức tương đối cao. Điều này cho thấy về tổng thể, các bài hát trong bộ dữ liệu có xu hướng “dễ nhảy” nhiều hơn là “khó nhảy”.
#1.5.1.2. Đồ thị 2: Phân phối mức độ “vui vẻ” (Valence).
Thao tác này vẽ biểu đồ histogram cho biến valence để trả lời câu hỏi: “Âm nhạc trong bộ dữ liệu có xu hướng ‘vui’ hay ‘buồn’? Hay là cân bằng?”
# Đồ thị 2: Phân phối 'valence'
ggplot(spotify_processed, aes(x = valence)) + # 1
geom_histogram(bins = 40, fill = "orange", color = "white", alpha = 0.7) + # 2
geom_vline(aes(xintercept = mean(valence)), # 3
color = "black", linetype = "dashed", size = 1) +
geom_density(aes(y = ..count.. * 100), color = "blue", size = 1) + #
labs(title = "Phân phối mức độ 'vui vẻ' (Valence)", # 5
subtitle = "Đường kẻ đen là giá trị trung bình, đường xanh là đường mật độ",
x = "Mức độ tích cực (0 = Buồn, 1 = Vui)",
y = "Số lượng bài hát") +
theme_minimal() # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = valence)) +: Khởi tạo đối tượng ggplot, ánh xạ (aes) cột valence vào trục x.
Câu lệnh #2 geom_histogram(…) +: Thêm lớp biểu đồ tần suất (histogram) để đếm số lượng bài hát trong mỗi khoảng giá trị valence.
Câu lệnh #3 geom_vline(…) +: Thêm một lớp đường kẻ dọc (vline) tại vị trí giá trị trung bình (mean) của valence, giúp xác định xu hướng trung tâm.
Câu lệnh #4 geom_density(…) +: Thêm một lớp đường cong mật độ (density plot). Đường cong này giúp làm mịn hình dạng của histogram, cho phép quan sát xu hướng phân phối một cách rõ ràng hơn (ví dụ: xác định biểu đồ có 1 đỉnh hay 2 đỉnh).
aes(y = ..count.. * 100): Đây là một kỹ thuật điều chỉnh. ..count.. là một biến nội bộ của ggplot, nó lấy số đếm của histogram. Việc nhân với 100 (một con số tùy chỉnh) là để điều chỉnh thang đo của đường geom_density (vốn mặc định là tỷ lệ 0-1) cho phù hợp với thang đo của geom_histogram (vốn là số đếm thực tế).
Câu lệnh #5 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, giúp biểu đồ dễ hiểu.
Câu lệnh #6 theme_minimal(): Áp dụng giao diện theme_minimal() để loại bỏ nền xám, giúp biểu đồ sạch sẽ hơn.
Nhận xét: Biểu đồ cho thấy một sự phân phối khá đồng đều trên toàn thang đo, nhưng hơi lệch về bên trái (buồn). Có thể thấy hai đỉnh nhỏ (bimodal) ở khoảng 0.2 và 0.8, nhưng nhìn chung, không có sự chênh lệch quá lớn giữa số lượng bài hát vui và buồn. Đường trung bình (kẻ đen) nằm gần 0.5, củng cố cho nhận định về sự cân bằng tương đối này.
#1.5.1.3. Đồ thị 3: Phân phối mức độ “năng lượng” (Energy).
Thao tác này vẽ biểu đồ histogram cho biến energy để trả lời câu hỏi: “Phần lớn các bài hát có xu hướng ‘nhiều năng lượng’ (sôi động) hay ‘ít năng lượng’ (thư giãn)?”
# Đồ thị 3: Phân phối 'energy'
ggplot(spotify_processed, aes(x = energy)) + # 1
geom_histogram(bins = 40, fill = "red", color = "white", alpha = 0.7) + # 2
geom_vline(aes(xintercept = mean(energy)), # 3
color = "black", linetype = "dashed", size = 1) +
labs(title = "Phân phối mức độ 'năng lượng' (Energy)", # 4
subtitle = "Đường kẻ đen là giá trị trung bình",
x = "Mức độ năng lượng (0 = Thấp, 1 = Cao)",
y = "Số lượng bài hát") +
theme_minimal() + # 5
scale_y_continuous(labels = scales::comma) # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = energy)) +: Khởi tạo đối tượng ggplot, ánh xạ (aes) cột energy vào trục x.
Câu lệnh #2 geom_histogram(…) +: Thêm lớp biểu đồ tần suất (histogram). Hàm này tự động đếm số lượng bài hát trong mỗi khoảng giá trị energy (với bins = 40 để tăng độ chi tiết).
Câu lệnh #3 geom_vline(…) +: Thêm một lớp đường kẻ dọc (vline) tại vị trí giá trị trung bình (mean) của energy, giúp xác định xu hướng trung tâm của phân phối.
Câu lệnh #4 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, giúp biểu đồ dễ hiểu.
Câu lệnh #5 theme_minimal() +: Áp dụng giao diện theme_minimal() để loại bỏ nền xám, giúp biểu đồ sạch sẽ hơn.
Câu lệnh #6 scale_y_continuous(labels = scales::comma): Tùy chỉnh thang đo trục y (trục số lượng), sử dụng hàm scales::comma để thêm dấu phẩy hàng ngàn (ví dụ: 10,000), giúp đọc số liệu dễ dàng hơn.
Nhận xét: Cấu trúc biểu đồ này tương tự Đồ thị 1. Kết quả cho thấy phân phối của energy cũng lệch trái (left-skewed), tương tự như danceability. Phần lớn các bài hát tập trung ở mức năng lượng cao (từ 0.5 trở lên). Điều này cho thấy xu hướng chung của âm nhạc trong bộ dữ liệu là các bài hát sôi động, có tiết tấu nhanh và mạnh mẽ.
#1.5.1.4. Đồ thị 4: Phân phối mức độ “mộc” (Acousticness).
Biến acousticness có đặc điểm là dữ liệu bị lệch phải nghiêm trọng (highly right-skewed). Hầu hết các bài hát hiện đại có giá trị này rất gần 0, trong khi một số ít bài hát (nhạc cổ điển, acoustic) có giá trị gần 1. Vẽ histogram bình thường sẽ chỉ thấy một cột khổng lồ ở số 0.
Mục đích của thao tác này là sử dụng thang đo Logarit (log scale) trên trục X để “kéo dãn” vùng dữ liệu gần 0, giúp quan sát được sự phân phối thực sự của nó.
# Đồ thị 4: Phân phối 'acousticness'
ggplot(spotify_processed, aes(x = acousticness)) + # 1
geom_histogram(bins = 40, fill = "brown", color = "white", alpha = 0.7) + # 2
labs(title = "Phân phối mức độ 'mộc' (Acousticness)", # 3
subtitle = "Lưu ý: Trục X là thang đo Logarit (Log scale) để quan sát",
x = "Mức độ mộc (Log scale)",
y = "Số lượng bài hát") +
scale_x_log10(labels = scales::number_format(accuracy = 0.01)) + # 4
theme_minimal() + # 5
scale_y_continuous(labels = scales::comma) # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = acousticness)) +: Khởi tạo đối tượng ggplot, ánh xạ (aes) cột acousticness vào trục x.
Câu lệnh #2 geom_histogram(…) +: Thêm lớp biểu đồ tần suất (histogram) để đếm số lượng bài hát.
Câu lệnh #3 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, đặc biệt là subtitle để lưu ý cho người đọc về thang đo logarit.
Câu lệnh #4: scale_x_log10(labels = scales::number_format(accuracy = 0.01)) +: Đây là lệnh quan trọng nhất của biểu đồ này. scale_x_log10() biến đổi thang đo của trục x thành thang đo Logarit cơ số 10. Thay vì chia đều (0.1, 0.2, 0.3…), thang đo sẽ hiển thị theo các lũy thừa của 10 (ví dụ: 0.001, 0.01, 0.1, 1). Điều này “kéo dãn” vùng dữ liệu bị dồn nén ở gần 0, cho phép chúng ta thấy rõ sự phân bổ.
labels = scales::number_format(accuracy = 0.01): Tùy chỉnh này giúp hiển thị các nhãn trục là số thập phân (ví dụ: 0.01) thay vì ký hiệu khoa học (ví dụ: 1e-2), giúp biểu đồ dễ đọc hơn.
Câu lệnh #5 theme_minimal() +: Áp dụng giao diện theme_minimal() để loại bỏ nền xám.
Câu lệnh #6 scale_y_continuous(labels = scales::comma): Tùy chỉnh thang đo trục y (số lượng), sử dụng scales::comma để thêm dấu phẩy hàng ngàn.
Nhận xét: Biểu đồ sau khi chuyển đổi thang đo cho thấy một phân phối nhị thức (bimodal) rõ rệt. Có một cụm lớn các bài hát có acousticness rất thấp (nhạc điện tử, hiện đại) và một cụm lớn khác có acousticness rất cao (nhạc mộc, cổ điển). Rất ít bài hát nằm ở khoảng giữa. Nếu không dùng thang đo logarit, chúng ta sẽ không thể thấy được cấu trúc hai đỉnh này.
#1.5.1.5. Đồ thị 5: Phân phối mức độ “không lời” (Instrumentalness). Tương tự như Đồ thị 4, hầu hết các bài hát đều có lời (giá trị instrumentalness rất gần 0). Thao tác này cũng sử dụng thang đo Logarit (log scale) trên trục X để “kéo dãn” vùng dữ liệu gần 0, giúp chúng ta quan sát được sự phân phối.
# Đồ thị 5: Phân phối 'instrumentalness'
ggplot(spotify_processed, aes(x = instrumentalness)) + # 1
geom_histogram(bins = 40, fill = "purple", color = "white", alpha = 0.7) + # 2
labs(title = "Phân phối mức độ 'không lời' (Instrumentalness)", # 3
subtitle = "Lưu ý: Trục X là thang đo Logarit (Log scale) để quan sát",
x = "Mức độ không lời (Log scale)",
y = "Số lượng bài hát") +
scale_x_log10(labels = scales::number_format(accuracy = 0.001)) + # 4
theme_minimal() + # 5
scale_y_continuous(labels = scales::comma) # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = instrumentalness)) +: Khởi tạo đối tượng ggplot, ánh xạ (aes) cột instrumentalness vào trục x.
Câu lệnh #2 geom_histogram(…) +: Thêm lớp biểu đồ tần suất (histogram) để đếm số lượng bài hát.
Câu lệnh #3 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, đặc biệt là subtitle để lưu ý cho người đọc về thang đo logarit.
Câu lệnh #4 scale_x_log10(labels = scales::number_format(accuracy = 0.001)) +: Đây là lệnh quan trọng nhất, tương tự Đồ thị 4. scale_x_log10() biến đổi thang đo của trục x thành thang đo Logarit cơ số 10. Thao tác này “kéo dãn” vùng dữ liệu bị dồn nén ở gần 0, cho phép chúng ta thấy rõ cấu trúc phân phối vốn bị che khuất.
labels = scales::number_format(accuracy = 0.001): Tùy chỉnh này giúp hiển thị các nhãn trục là số thập phân (ví dụ: 0.001) thay vì ký hiệu khoa học.
Câu lệnh #5 theme_minimal() +: Áp dụng giao diện theme_minimal() để loại bỏ nền xám.
Câu lệnh #6: scale_y_continuous(labels = scales::comma): Tùy chỉnh thang đo trục y (số lượng), sử dụng scales::comma để thêm dấu phẩy hàng ngàn.
Nhận xét: Biểu đồ này cũng cho thấy một phân phối nhị thức (bimodal) rõ rệt, tương tự như acousticness. Có một cụm khổng lồ các bài hát có instrumentalness cực kỳ thấp (gần 0, tức là nhạc có lời) và một cụm nhỏ hơn nhưng đáng kể ở các giá trị cao (nhạc không lời).
#1.5.2. Trực quan hóa tần suất (biến phân loại).
Phần này sẽ vẽ các biểu đồ cột (geom_bar hoặc geom_col) và biểu đồ tròn để xem số lượng (tần suất) trong các nhóm (level) của các biến phân loại.
#1.5.2.1. Đồ thị 6: Tần suất mức độ phổ biến.
Biểu đồ này trực quan hóa bảng tần suất của popularity_level (đã tính ở Thao tác 2, Phần 1.4) để trả lời câu hỏi: “Nhóm phổ biến nào (‘Thap’, ‘Trung binh’…) chiếm đa số?”
# Đồ thị 6: Tần suất mức độ phổ biến
Bang_tan_suat_pop <- spotify_processed %>% # 1
group_by(popularity_level) %>% # 2
summarise(So_bai_hat = n(), .groups = "drop") %>% # 3
mutate(Ty_le = So_bai_hat / sum(So_bai_hat)) # 4
ggplot(Bang_tan_suat_pop, aes(x = popularity_level, y = So_bai_hat, fill = popularity_level)) + # 5
geom_col(show.legend = FALSE) + # 6
geom_text(aes(label = scales::percent(Ty_le, accuracy = 0.1)), vjust = -0.5) + # 7
scale_y_continuous(labels = scales::comma) + # 8
labs(title = "Tần suất mức độ phổ biến bài hát", # 9
x = "Mức độ phổ biến",
y = "Số lượng bài hát") +
theme_minimal() # 10
Giải thích:
Câu lệnh #1 - #4: Chuẩn bị dữ liệu nguồn cho biểu đồ.
Câu lệnh #2 group_by(popularity_level): Nhóm tất cả các bài hát theo 4 mức độ phổ biến đã tạo.
Câu lệnh #3 summarise(So_bai_hat = n(), .groups = “drop”): Đếm (n()) số lượng bài hát trong mỗi nhóm và lưu vào cột So_bai_hat.
Câu lệnh #4 mutate(Ty_le = So_bai_hat / sum(So_bai_hat)): Tạo một cột Ty_le (Tỷ lệ) mới. Giá trị của nó được tính bằng cách lấy số bài hát của nhóm (So_bai_hat) chia cho tổng số bài hát (sum(So_bai_hat)).
Câu lệnh #5 ggplot(Bang_tan_suat_pop, …): Khởi tạo ggplot, nhưng lần này sử dụng bảng tóm tắt Bang_tan_suat_pop (chỉ có 4 hàng) làm dữ liệu nguồn.
aes(x = …, y = …, fill = …): Ánh xạ trục x (popularity_level), trục y (So_bai_hat) và màu tô (fill).
Câu lệnh #6 geom_col(show.legend = FALSE) +: Thêm lớp biểu đồ cột geom_col(). Chúng ta dùng geom_col() thay vì geom_bar() vì dữ liệu đã được đếm sẵn (chúng ta cung cấp cả x và y). geom_bar() được dùng khi chúng ta chỉ cung cấp x và muốn ggplot tự đếm.
show.legend = FALSE: Ẩn chú giải (legend) về màu sắc vì nó bị trùng lặp, không cần thiết (trục x đã có thông tin đó).
Câu lệnh #7 geom_text(…) +: Thêm một lớp văn bản (text label) lên trên biểu đồ.
aes(label = scales::percent(Ty_le, …)): Chỉ định “nhãn” chính là cột Ty_le (Tỷ lệ) đã tính ở Bước 1, và dùng hàm scales::percent để định dạng nó thành % (ví dụ: 0.68 -> 68.0%).
vjust = -0.5: Căn chỉnh theo chiều dọc (vjust), đẩy nhãn lên trên (-0.5) một chút so với đỉnh cột.
Câu lệnh #8 scale_y_continuous(labels = scales::comma) +: Định dạng trục y (số lượng), sử dụng scales::comma để thêm dấu phẩy hàng ngàn.
Câu lệnh #9 & #10 labs(…) và theme_minimal(): Tùy chỉnh tiêu đề, nhãn trục và áp dụng giao diện sạch sẽ.
Nhận xét: Biểu đồ này sử dụng geom_col() vì dữ liệu đã được tính toán trước (Đếm ở Bước 1). Biểu đồ cho thấy rõ ràng rằng phần lớn các bài hát trong bộ dữ liệu (gần 70%, như được hiển thị bởi nhãn geom_text) thuộc nhóm “Trung bình”. Các bài hát “Rất cao” về độ phổ biến chỉ chiếm một tỷ lệ nhỏ, điều này là hợp lý.
#1.5.2.2. Tần suất bài hát qua các thập kỷ.
Biểu đồ này trực quan hóa số lượng bài hát từ mỗi thập kỷ để xem “Sự bùng nổ trong sản xuất âm nhạc diễn ra ở các thập kỷ nào?”.
# Đồ thị 7: Tần suất bài hát qua các thập kỷ
ggplot(spotify_processed, aes(x = factor(decade), fill = factor(decade))) + # 1
geom_bar(show.legend = FALSE) + # 2
scale_y_continuous(labels = scales::comma) + # 3
labs(title = "Số lượng bài hát qua các thập kỷ", # 4
x = "Thập kỷ",
y = "Số lượng bài hát") +
theme_minimal() + # 5
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # 6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = factor(decade), fill = factor(decade))) +: Khởi tạo đối tượng ggplot từ data frame spotify_processed.
aes(x = factor(decade), …): Đây là một điểm quan trọng. Cột
decade (ví dụ: 1970, 1980) vốn là kiểu số (
fill = factor(decade): Ánh xạ biến decade (đã factor-hóa) vào thuộc tính fill (màu tô), khiến mỗi cột thập kỷ có một màu khác nhau.
Câu lệnh #2 geom_bar(show.legend = FALSE) +: Thêm lớp biểu đồ cột geom_bar(). Chúng ta dùng geom_bar() vì dữ liệu chưa được đếm. geom_bar() sẽ tự động thực hiện thao tác đếm (count) số lượng hàng (bài hát) cho mỗi nhóm x (mỗi thập kỷ) và vẽ cột có chiều cao tương ứng. (Điều này khác với geom_col() ở Đồ thị 6, vốn yêu cầu dữ liệu đã đếm sẵn).
show.legend = FALSE: Ẩn chú giải (legend) về màu sắc vì nó không cần thiết (trục x đã có thông tin đó).
Câu lệnh #3 scale_y_continuous(labels = scales::comma) +: Định dạng trục y (số lượng), sử dụng scales::comma để thêm dấu phẩy hàng ngàn (ví dụ: 15,000), giúp đọc số liệu dễ dàng hơn.
Câu lệnh #4 labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ.
Câu lệnh #5 theme_minimal() +: Áp dụng giao diện theme_minimal() để loại bỏ nền xám.
Câu lệnh #6 theme(axis.text.x = element_text(angle = 45, hjust = 1)): Đây là một tùy chỉnh giao diện (theme) đặc biệt.
axis.text.x = …: Chỉ định rằng chúng ta muốn thay đổi văn bản của trục x.
element_text(angle = 45, hjust = 1): Yêu cầu ggplot xoay các nhãn trên trục x (tức là 1920, 1930…) một góc 45 độ. Thao tác này là cần thiết để các nhãn không bị dính vào nhau (overlapping) và giúp chúng dễ đọc hơn.
Nhận xét: Biểu đồ cho thấy một sự tăng trưởng bùng nổ về số lượng bài hát được ghi lại (hoặc ít nhất là được đưa lên Spotify). Số lượng bài hát tăng trưởng chậm ở các thập kỷ đầu và bắt đầu tăng mạnh từ thập niên 1950, sau đó đạt đỉnh vào những năm 2010.
#1.5.2.3. Đồ thị 8: 15 nghệ sĩ có nhiều bài hát nhất.
Thao tác này sẽ (1) Đếm số lượng bài hát của mỗi nghệ sĩ, (2) Lọc ra 15 nghệ sĩ hàng đầu, và (3) Vẽ biểu đồ cột ngang để hiển thị kết quả.
# Đồ thị 8: Top 15 nghệ sĩ
top_artists <- spotify_processed %>% #1
count(artists, sort = TRUE) %>% #2
head(15) #3
ggplot(top_artists, aes(x = reorder(artists, n), y = n, fill = artists)) + #4
geom_col(show.legend = FALSE) + #5
coord_flip() + #6
labs(title = "15 Nghệ sĩ có nhiều bài hát nhất trong dữ liệu", #7
x = "Nghệ sĩ",
y = "Số lượng bài hát") +
theme_minimal() #8
Giải thích:
Câu lệnh #1 - #3: Chuẩn bị dữ liệu nguồn cho biểu đồ.
Câu lệnh #1: Lấy data frame spotify_processed làm đầu vào.
Câu lệnh #2: count(artists, sort = TRUE): Đây là hàm của dplyr, thực hiện 2 việc: 1) Đếm số lượng bài hát (n) cho mỗi nghệ sĩ (artists), 2) Tự động sắp xếp (sort = TRUE) kết quả theo thứ tự giảm dần.
Câu lệnh #3: head(15): Chỉ giữ lại 15 hàng đầu tiên (Top 15 nghệ sĩ).
Câu lệnh #4 - #8: Vẽ biểu đồ từ bảng top_artists đã tóm tắt.
Câu lệnh #4: ggplot(top_artists, aes(x = reorder(artists, n), y = n, …)) +
aes(x = reorder(artists, n), …): Đây là một kỹ thuật quan trọng. reorder(artists, n) là một hàm đặc biệt, nó tự động sắp xếp lại các nghệ sĩ (artists) trên trục x dựa trên giá trị n (số lượng). Điều này đảm bảo các cột trên biểu đồ sẽ được sắp xếp theo thứ tự độ cao.
Câu lệnh #5 geom_col(show.legend = FALSE) +: Sử dụng geom_col() vì dữ liệu đã được đếm sẵn ở Bước 1. show.legend = FALSE ẩn chú giải màu sắc không cần thiết.
Câu lệnh #6 coord_flip() +: Đây là lệnh lật ngang biểu đồ, hoán đổi trục x và y. Khi các nhãn (tên nghệ sĩ) quá dài, việc đặt chúng trên trục x sẽ gây chồng chéo. Lật ngang biểu đồ sẽ đưa các nhãn này sang trục y, giúp chúng hiển thị rõ ràng và dễ đọc.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề, nhãn trục và áp dụng giao diện sạch sẽ.
Nhận xét: Biểu đồ cho thấy sự thống trị của các nghệ sĩ nhạc cổ điển (ví dụ: ‘Frédéric Chopin’) trong bộ dữ liệu này về số lượng bản ghi. Kỹ thuật reorder() kết hợp với coord_flip() rất hữu ích để tạo ra một biểu đồ cột ngang đã được sắp xếp.
#1.5.2.4. Đồ thị 9: Tỷ lệ nội dung nhạy cảm.
Biểu đồ tròn là cách trực quan nhất để thể hiện sự chênh lệch “một phần trong tổng thể” (part-to-whole) của biến explicit_label.
# Đồ thị 9: Tỷ lệ nội dung nhạy cảm
explicit_summary_pie <- spotify_processed %>% #1
count(explicit_label) %>% #2
mutate(percentage = n / sum(n)) #3
ggplot(explicit_summary_pie, aes(x = "", y = percentage, fill = explicit_label)) + #4
geom_col(width = 1, color = "white") + # 5
coord_polar(theta = "y") + # 6
geom_text(aes(label = scales::percent(percentage, accuracy = 0.1)), #7
position = position_stack(vjust = 0.5)) +
labs(title = "Tỷ lệ bài hát có nội dung nhạy cảm", #8
fill = "Loại nội dung") +
theme_void() #9
Giải thích:
Câu lệnh #1 - #3: Chuẩn bị dữ liệu.
Câu lệnh #2: count(explicit_label): Đếm số lượng bài hát cho 2 nhóm (“Không Nhạy Cảm”, “Có Nhạy Cảm”).
Câu lệnh #3: mutate(percentage = n / sum(n)): Tạo cột tỷ lệ % (ví dụ: 0.95 và 0.05).
Câu lệnh #4 - #9: Vẽ biểu đồ. Đây là một kỹ thuật “hack” để vẽ biểu đồ tròn trong ggplot2.
Câu lệnh #4: ggplot(explicit_summary_pie, aes(x = ““, y = percentage, fill = explicit_label)) +
aes(x = ““): Đây là mẹo mấu chốt. Bằng cách gán x cho một giá trị rỗng (”“), chúng ta yêu cầu ggplot tạo ra một cột duy nhất.
Câu lệnh #5 geom_col(…) +: Vẽ một biểu đồ cột chồng (stacked bar chart) trên cột rỗng đó. Kết quả là một cột duy nhất, cao 100%, được chia thành 2 phần màu (theo fill = explicit_label).
Câu lệnh #6 coord_polar(theta = “y”) +: Đây là lệnh “biến hình”. coord_polar() lấy biểu đồ cột chồng (vốn ở hệ tọa độ Descartes x, y) và “uốn cong” nó thành hệ tọa độ cực (polar).
theta = “y”: Chỉ định rằng giá trị y (tỷ lệ %) sẽ được ánh xạ vào góc (theta), tạo ra các “miếng bánh” (pie slices).
position = position_stack(vjust = 0.5): Một kỹ thuật căn chỉnh quan trọng, đảm bảo nhãn % được đặt vào chính giữa (vjust = 0.5) của mỗi “miếng bánh”.
Nhận xét: Biểu đồ cho thấy rõ ràng rằng các bài hát “Có Nhạy Cảm” chỉ chiếm một tỷ lệ rất nhỏ (khoảng 5-6%) trong toàn bộ bộ dữ liệu.
#1.5.2.5. Đồ thị 10: Tần suất các tông nhạc (key).
Biểu đồ này giúp trả lời câu hỏi: “Liệu có tông nhạc (key) nào được các nghệ sĩ ưa chuộng hơn hẳn các tông khác hay không?”.
# Đồ thị 10: Tần suất các tông nhạc (Key)
ggplot(spotify_processed, aes(x = factor(key), fill = factor(key))) + # 1
geom_bar(show.legend = FALSE) + # 2
scale_y_continuous(labels = scales::comma) + # 3
labs(title = "Tần suất của các tông nhạc (Key)", # 4
x = "Key (0=C, 1=C#, ..., 11=B)",
y = "Số lượng bài hát") +
theme_minimal() # 5
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = factor(key), fill = factor(key))) +: Khởi tạo ggplot từ dữ liệu thô spotify_processed.
aes(x = factor(key), …): Tương tự Đồ thị 7, key là một biến số (0-11) nhưng đại diện cho các nhóm (tông C, C#, D…). Do đó, phải dùng factor(key) để ggplot coi nó là biến phân loại và vẽ các cột riêng biệt.
Câu lệnh #2 geom_bar(show.legend = FALSE) +: Sử dụng geom_bar() để ggplot tự động đếm số lượng bài hát cho mỗi key và vẽ cột tương ứng.
Câu lệnh #3 scale_y_continuous(labels = scales::comma) +
Ý nghĩa: Định dạng trục y (số lượng) với dấu phẩy hàng ngàn.
Câu lệnh #4 labs(…) +: Tùy chỉnh tiêu đề và nhãn trục. Nhãn trục x được chú thích rõ (0=C, 1=C#…) để người đọc hiểu ý nghĩa của các con số.
-Câu lệnh #5 theme_minimal(): Áp dụng giao diện sạch sẽ.
#1.5.3. Trực quan hóa quan hệ (Số vs Phân loại).
Phần này sẽ dùng các biểu đồ phức tạp hơn (như Biểu đồ Hộp (Box Plot) và Biểu đồ Violin (Violin Plot)) để so sánh phân phối của một biến số (như valence) giữa các nhóm (như popularity_level).
1.5.3.1. Đồ thị 11: Mức độ “vui vẻ” (Valence) vs Mức độ phổ biến
Mục đích là để trả lời câu hỏi: “Bài hát Nổi tiếng thì ‘Vui’ hay ‘Buồn’ hơn?” bằng cách so sánh phân phối của valence giữa 4 nhóm popularity_level.
# Đồ thị 11: Biểu đồ hộp (Box Plot) - Valence vs Popularity
ggplot(spotify_processed, aes(x = popularity_level, y = valence, fill = popularity_level)) + #1
geom_boxplot(show.legend = FALSE) + #2
labs(title = "Bài hát nổi tiếng thì 'Vui' hay 'Buồn' hơn?", #3
x = "Mức độ Phổ biến",
y = "Mức độ Tích cực (Vui/Buồn)") +
theme_minimal() + #4
scale_x_discrete(limits = c("Thấp", "Trung bình", "Cao", "Rất cao")) #5
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = popularity_level, y = valence, …)) +: Khởi tạo ggplot. Ánh xạ (aes) popularity_level (biến phân loại, nhóm) vào trục x và valence (biến số, giá trị) vào trục y.
Câu lệnh #2 geom_boxplot(show.legend = FALSE) +: Thêm lớp hình học biểu đồ hộp (geom_boxplot). ggplot sẽ tự động tính toán và vẽ tóm tắt 5 số liệu (Min, Tứ phân vị 1, Trung vị, Tứ phân vị 3, Max) cho phân phối valence của từng nhóm trên trục x.
Câu lệnh #3 và #4: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #5 scale_x_discrete(limits = c(“Thấp”, “Trung bình”, “Cao”, “Rất cao”)): Đây là một lệnh tùy chỉnh thang đo rất quan trọng.
Vấn đề: Mặc định, ggplot sắp xếp các nhãn trên trục x (biến discrete - rời rạc) theo thứ tự alphabet (nghĩa là: “Cao”, “Rất cao”, “Thấp”, “Trung bình”).
Giải pháp: scale_x_discrete(limits = …) cho phép chúng ta ghi đè thứ tự mặc định và cung cấp một vector (c(…)) chứa thứ tự logic mà chúng ta mong muốn (“Thấp” -> “Trung bình” -> “Cao” -> “Rất cao”).
Nhận xét: Biểu đồ hộp cho thấy một xu hướng tăng nhẹ. Đường trung vị (vạch ngang đậm giữa hộp) của nhóm “Rất cao” cao hơn một chút so với các nhóm còn lại. Điều này cho thấy các bài hát nổi tiếng có xu hướng “vui vẻ” (tích cực) hơn một chút.
#1.5.3.2. Đồ thị 12: Mức độ “năng lượng” (Energy) vs Mức độ phổ biến.
Tương tự Đồ thị 11, nhưng mục đích là để trả lời câu hỏi: “Bài hát Nổi tiếng có xu hướng ‘nhiều năng lượng’ (sôi động) hơn không?”
# Đồ thị 12: Biểu đồ Hộp (Box Plot) - Energy vs Popularity
ggplot(spotify_processed, aes(x = popularity_level, y = energy, fill = popularity_level)) + #1
geom_boxplot(show.legend = FALSE) + #2
labs(title = "Bài hát nổi tiếng có nhiều 'năng lượng' hơn?", #3
x = "Mức độ phổ biến",
y = "Mức độ năng lượng") +
theme_minimal() + #4
scale_x_discrete(limits = c("Thấp", "Trung bình", "Cao", "Rất cao")) #5
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = popularity_level, y = energy, …)) +: Khởi tạo ggplot, ánh xạ popularity_level (biến nhóm) vào trục x và energy (biến giá trị) vào trục y.
Câu lệnh #2 geom_boxplot(show.legend = FALSE) +: Thêm lớp biểu đồ hộp (geom_boxplot), tự động vẽ phân phối 5 số của energy cho từng nhóm popularity_level.
Câu lệnh #3 và #4 labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #5 scale_x_discrete(limits = c(“Thấp”, “Trung bình”, “Cao”, “Rất cao”)): Tương tự Đồ thị 11, lệnh này bắt buộc ggplot phải sắp xếp trục x theo thứ tự logic mong muốn, thay vì thứ tự alphabet mặc định.
Nhận xét: Xu hướng ở đây rõ rệt hơn Đồ thị 11. Đường trung vị (vạch ngang đậm) tăng dần rõ rệt khi đi từ “Thấp” đến “Rất cao”. Điều này cho thấy các bài hát càng nổi tiếng thì có xu hướng năng lượng trung bình càng cao.
#1.5.3.3. Đồ thị 13: Mức độ “dễ nhảy” (Danceability) vs Thập kỷ.
Mục đích là để trả lời câu hỏi: “Khả năng ‘dễ nhảy’ của âm nhạc đã thay đổi như thế nào qua 100 năm?”.
# Đồ thị 13: Biểu đồ Hộp (Box Plot) - Danceability vs Decade
ggplot(spotify_processed, aes(x = factor(decade), y = danceability, fill = factor(decade))) + #1
geom_boxplot(show.legend = FALSE) + #2
labs(title = "Độ 'Dễ nhảy' thay đổi như thế nào qua thời gian?", #3
x = "Thập kỷ",
y = "Mức độ dễ nhảy") +
theme_minimal() + #4
theme(axis.text.x = element_text(angle = 45, hjust = 1)) #5
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = factor(decade), y = danceability, …)) +: Khởi tạo ggplot. Ánh xạ decade (biến nhóm) vào trục x và danceability (biến giá trị) vào trục y.
factor(decade): Tương tự Đồ thị 7 và 10, lệnh factor() là bắt buộc để ggplot coi decade (vốn là số) như một biến phân loại (nhóm).
Câu lệnh #2 geom_boxplot(show.legend = FALSE) +: Thêm lớp biểu đồ hộp (geom_boxplot), vẽ phân phối 5 số của danceability cho từng decade.
Câu lệnh #3 và #4: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #5 theme(axis.text.x = element_text(angle = 45, hjust = 1): Tương tự Đồ thị 7, lệnh này xoay các nhãn trên trục x (1920, 1930…) một góc 45 độ để tránh chồng chéo và giúp dễ đọc hơn. Nhận xét: Biểu đồ hộp cho thấy mức độ “dễ nhảy” trung bình (trung vị - vạch đậm giữa hộp) tăng dần từ những năm 1920, đạt đỉnh trong các thời kỳ Disco/Pop (1970-1980) và tiếp tục duy trì ở mức cao trong những năm 2000-2010.
#1.5.3.4. Đồ thị 14: Mức độ “Độ lớn” (Loudness) vs Mức độ phổ biến.
Sử dụng Biểu đồ Violin (Violin Plot) thay vì Biểu đồ Hộp. Biểu đồ violin là sự kết hợp của biểu đồ hộp (bên trong) và biểu đồ mật độ (hình dạng “violin” bên ngoài), cho thấy cả tóm tắt 5 số và hình dạng phân phối.
# Đồ thị 14: Biểu đồ Violin (Violin Plot)
ggplot(spotify_processed, aes(x = popularity_level, y = loudness, fill = popularity_level)) + #1
geom_violin(show.legend = FALSE) + #2
geom_boxplot(width = 0.1, fill = "white", alpha = 0.5, show.legend = FALSE) + #3
labs(title = "Bài hát Nổi tiếng có 'To' hơn không?", #4
subtitle = "Sử dụng Biểu đồ Violin để xem phân phối",
x = "Mức độ Phổ biến",
y = "Độ lớn (Loudness)") +
theme_minimal() + #5
scale_x_discrete(limits = c("Thấp", "Trung bình", "Cao", "Rất cao")) #6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = popularity_level, y = loudness, …)) +: Khởi tạo ggplot, ánh xạ popularity_level (biến nhóm) vào trục x và loudness (biến giá trị) vào trục y.
Câu lệnh #2 geom_violin(show.legend = FALSE) +: Thêm lớp hình học biểu đồ violin (geom_violin). geom_violin vẽ một biểu đồ mật độ (density plot) đã được “nhân đôi” và xoay dọc (giống hình cây đàn violin). “Độ phình” của violin cho thấy mật độ tập trung của dữ liệu ở mức giá trị đó.
Câu lệnh #3 geom_boxplot(width = 0.1, fill = “white”, alpha = 0.5, …): Đây là một kỹ thuật nâng cao. Lệnh này “vẽ lồng” một biểu đồ hộp (geom_boxplot) rất hẹp (width = 0.1) vào bên trong biểu đồ violin. Kỹ thuật này kết hợp ưu điểm của cả hai: geom_violin cho thấy hình dạng phân phối (độ phình), trong khi geom_boxplot bên trong cho thấy rõ các giá trị Tứ phân vị và Trung vị.
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #6 scale_x_discrete(limits = c(“Thấp”, “Trung bình”, “Cao”, “Rất cao”)): Tương tự Đồ thị 11 & 12, lệnh này bắt buộc ggplot phải sắp xếp trục x theo thứ tự logic mong muốn.
Nhận xét: Biểu đồ cho thấy rõ ràng: các bài hát càng nổi tiếng thì càng “to” (loudness) hơn (phân phối dịch chuyển lên trên). Hơn nữa, violin của nhóm “Rất cao” “phình” nhất ở một khoảng hẹp và cao, cho thấy các bài hit vừa “to” hơn, vừa có mức độ “to” đồng đều hơn (ít biến động hơn).
#1.5.3.5. Đồ thị 15: Thời lượng (Duration) vs Thập kỷ.
Mục đích là để trả lời câu hỏi: “Độ dài trung bình của các bài hát đã thay đổi như thế nào qua 100 năm?”.
# Đồ thị 15: Biểu đồ Violin (Violin Plot) - Duration vs Decade.
ggplot(spotify_processed, aes(x = factor(decade), y = duration_min, fill = factor(decade))) + #1
geom_violin(show.legend = FALSE) + #2
coord_cartesian(ylim = c(0, 10)) + #3
labs(title = "Thời lượng bài hát thay đổi qua thời gian", #4
subtitle = "Zoom vào khoảng 0-10 phút",
x = "Thập kỷ",
y = "Thời lượng (phút)") +
theme_minimal() + #5
theme(axis.text.x = element_text(angle = 45, hjust = 1)) #6
Giải thích:
Câu lệnh #1 ggplot(spotify_processed, aes(x = factor(decade), y = duration_min, …)) +: Khởi tạo ggplot. Ánh xạ decade (biến nhóm, đã factor-hóa) vào trục x và duration_min (biến giá trị) vào trục y.
Câu lệnh #2 geom_violin(show.legend = FALSE) +:Thêm lớp biểu đồ violin (geom_violin) để vẽ phân phối của thời lượng bài hát cho từng thập kỷ.
Câu lệnh #3 coord_cartesian(ylim = c(0, 10)): Đây là lệnh quan trọng nhất của biểu đồ này. Nó dùng để “zoom” vào biểu đồ.
Vấn đề: Cột duration_min có nhiều giá trị ngoại lai (outliers) rất lớn (ví dụ: bài hát 20 phút), khiến cho phần thân chính của violin (nơi hầu hết dữ liệu tập trung, 2-5 phút) bị “bóp méo”, “dồn nén” xuống đáy biểu đồ, rất khó xem.
Giải pháp: coord_cartesian(ylim = c(0, 10)) hoạt động như một “kính lúp”, nó chỉ hiển thị vùng trục y từ 0 đến 10.
Quan trọng: Không giống như ylim(), coord_cartesian không xóa các điểm dữ liệu bên ngoài (ví dụ: điểm 20 phút). Nó vẫn sử dụng chúng để tính toán hình dạng violin, sau đó mới “zoom” vào. Điều này đảm bảo hình dạng phân phối vẫn chính xác.
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn (lưu ý subtitle giải thích việc zoom) và áp dụng giao diện sạch sẽ. Câu lệnh #6 theme(axis.text.x = element_text(angle = 45, hjust = 1)): Tương tự Đồ thị 13, xoay các nhãn trên trục x (1920, 1930…) một góc 45 độ để tránh chồng chéo.
Nhận xét: Biểu đồ cho thấy thời lượng bài hát trung bình tăng dần từ những năm 1920 và đạt đỉnh vào những năm 1970-1980 (thời kỳ nhạc Rock) trước khi giảm nhẹ và ổn định trở lại ở mức khoảng 3-4 phút trong các thập kỷ gần đây.
#1.5.4. Trực quan hóa quan hệ (Số vs Số).
Phần này sẽ dùng Biểu đồ Tán xạ (Scatter Plot) để xem hai biến số (như energy và loudness) có di chuyển cùng nhau hay không (tức là kiểm tra tương quan tuyến tính).
Vấn đề: Bộ dữ liệu có hơn 170,000 điểm. Vẽ tất cả lên một biểu đồ tán xạ sẽ (1) rất chậm và (2) các điểm sẽ bị chồng đè lên nhau (overplotting), khiến biểu đồ trở thành một mảng đen đặc, không thể quan sát.
Giải pháp: Tạo một mẫu ngẫu nhiên (sample) gồm 10,000 bài hát để vẽ.
set.seed(42) #1
spotify_sample <- sample_n(spotify_processed, 10000) #2
Giải thích:
Câu lệnh #1 set.seed(42): Khởi tạo “hạt giống” (seed) cho trình tạo số ngẫu nhiên của R. Con số 42 là tùy ý. Lệnh này đảm bảo tính tái lập (reproducibility). Nếu không có nó, mỗi lần chạy lại mã, sample_n sẽ chọn 10,000 bài hát khác nhau, dẫn đến biểu đồ hơi khác nhau. set.seed(42) đảm bảo rằng luôn luôn 10,000 bài hát giống hệt nhau sẽ được chọn, giúp kết quả phân tích ổn định.
Câu lệnh #2 spotify_sample <- sample_n(spotify_processed, 10000: Sử dụng hàm sample_n() của dplyr để chọn ngẫu nhiên 10,000 hàng (bài hát) từ spotify_processed và lưu vào data frame mới là spotify_sample.
#1.5.4.1. Đồ thị 16: Năng lượng (Energy) vs Độ lớn (Loudness).
Mục đích là để trả lời câu hỏi: “Có phải các bài hát có ‘năng lượng’ cao cũng là các bài hát ‘to’ hơn không?”.
# Đồ thị 16: Biểu đồ Tán xạ (Scatter Plot) - Energy vs Loudness
ggplot(spotify_sample, aes(x = energy, y = loudness)) + #1
geom_point(alpha = 0.3, color = "purple") + #2
geom_smooth(method = "lm", color = "red", size = 1) + #3
labs(title = "Mối quan hệ giữa Năng lượng và Độ lớn", #4
subtitle = "Mỗi chấm là một bài hát (lấy từ mẫu 10,000 bài)",
x = "Năng lượng",
y = "Độ lớn (dB)") +
theme_minimal() #5
Giải thích:
Câu lệnh #1 ggplot(spotify_sample, aes(x = energy, y = loudness)) +: Khởi tạo ggplot, sử dụng data frame mẫu spotify_sample (chỉ 10,000 điểm). Ánh xạ energy vào trục x và loudness vào trục y.
Câu lệnh #2 geom_point(alpha = 0.3, color = “purple”) +: Thêm lớp hình học điểm (geom_point), tạo ra biểu đồ tán xạ.
alpha = 0.3: Đây là tham số quan trọng. Nó làm cho các điểm trong suốt 30% (70% mờ). Khi nhiều điểm chồng lên nhau, khu vực đó sẽ trở nên đậm màu hơn. Kỹ thuật này giúp giải quyết vấn đề “overplotting” (chồng đè), cho phép chúng ta nhìn thấy mật độ của dữ liệu.
Câu lệnh #3: geom_smooth(method = “lm”, color = “red”, size = 1) +: Thêm một lớp thống kê đường xu hướng (geom_smooth).
method = “lm”: Chỉ định phương pháp là “lm” (Linear Model - Mô hình Tuyến tính). ggplot sẽ tự động tính toán và vẽ một đường thẳng hồi quy tuyến tính (đường xu hướng) thể hiện mối quan hệ giữa x và y.
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ. Nhận xét: Biểu đồ cho thấy một mối tương quan thuận mạnh (strong positive correlation). Các điểm tập trung thành một đường chéo đi lên, và đường hồi quy (màu đỏ) xác nhận điều này. Nhạc càng nhiều năng lượng thì càng to. (Điều này khớp với kết quả cor = ~0.77 ở Phần 1.4.5).
#1.5.4.2. Đồ thị 17: Độ mộc (Acousticness) vs Năng lượng (Energy). Mục đích là để trả lời câu hỏi: “Có phải các bài hát càng ‘mộc’ (acoustic) thì ‘năng lượng’ càng thấp không?”.
# Đồ thị 17: Biểu đồ Tán xạ (Scatter Plot) - Acousticness vs Energy.
ggplot(spotify_sample, aes(x = acousticness, y = energy)) + #1
geom_point(alpha = 0.2, color = "brown") + #2
geom_smooth(method = "lm", color = "blue", size = 1) + #3
labs(title = "Mối quan hệ giữa Độ mộc và Năng lượng", #4
subtitle = "Mỗi chấm là một bài hát (lấy từ mẫu 10,000 bài)",
x = "Độ mộc (Acousticness)",
y = "Năng lượng (Energy)") +
theme_minimal() #5
Giải thích:
Câu lệnh #1 ggplot(spotify_sample, aes(x = acousticness, y = energy)) +: Khởi tạo ggplot từ mẫu 10,000 điểm. Ánh xạ acousticness vào trục x và energy vào trục y.
Câu lệnh #2 geom_point(alpha = 0.2, …): Vẽ các điểm tán xạ với độ trong suốt 20% (alpha = 0.2) để xem mật độ.
Câu lệnh #3 geom_smooth(method = “lm”, …): Thêm một đường xu hướng tuyến tính (hồi quy) lm để xem mối quan hệ tổng thể.
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Nhận xét: Biểu đồ cho thấy một mối tương quan nghịch mạnh (strong negative correlation). Các điểm tập trung ở hai góc (mộc nhiều/năng lượng thấp và mộc ít/năng lượng cao). Đường xu hướng màu xanh đi dốc xuống, cho thấy hai thuộc tính này trái ngược nhau.
#1.5.4.3. Đồ thị 18: Độ vui vẻ (Valence) vs Độ dễ nhảy (Danceability).
Mục đích là để trả lời câu hỏi: “Có phải các bài hát ‘vui vẻ’ cũng là các bài hát ‘dễ nhảy’ không?”.
# Đồ thị 18: Biểu đồ Tán xạ (Scatter Plot) - Valence vs Danceability.
ggplot(spotify_sample, aes(x = danceability, y = valence)) + #1
geom_point(alpha = 0.2, color = "darkgreen") + #2
geom_smooth(method = "lm", color = "red", size = 1) + #3
labs(title = "Mối quan hệ giữa Độ dễ nhảy và Độ vui vẻ", #4
subtitle = "Mỗi chấm là một bài hát (lấy từ mẫu 10,000 bài)",
x = "Độ dễ nhảy (Danceability)",
y = "Độ vui vẻ (Valence)") +
theme_minimal() #5
Giải thích:
Câu lệnh #1 ggplot(spotify_sample, aes(x = danceability, y = valence)) +: Khởi tạo ggplot từ mẫu. Ánh xạ danceability vào trục x và valence vào trục y.
Câu lệnh #2 geom_point(alpha = 0.2, …): Vẽ các điểm tán xạ với độ trong suốt 20% (alpha = 0.2).
Câu lệnh #3 geom_smooth(method = “lm”, …): Thêm một đường xu hướng tuyến tính (lm).
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Nhận xét: Biểu đồ cho thấy các điểm phân bổ khá rộng, nhưng đường xu hướng màu đỏ dốc lên, cho thấy một mối tương quan thuận nhẹ (weak positive correlation). Điều này có nghĩa là các bài hát dễ nhảy có xu hướng vui vẻ hơn một chút, nhưng mối quan hệ này không mạnh.
#1.5.4.4. Đồ thị 19: Độ nổi tiếng (Popularity) vs Độ lớn (Loudness).
Mục đích là để trả lời câu hỏi: “Độ lớn (âm thanh to, rõ) có ảnh hưởng đến Độ nổi tiếng không?”.
# Đồ thị 19: Biểu đồ Tán xạ (Scatter Plot) - Popularity vs Loudness
ggplot(spotify_sample, aes(x = loudness, y = popularity)) + #1
geom_point(alpha = 0.2, color = "red") + #2
geom_smooth(method = "lm", color = "black", size = 1) + #3
labs(title = "Độ lớn có ảnh hưởng đến Độ nổi tiếng?", #4
subtitle = "Mỗi chấm là một bài hát (lấy từ mẫu 10,000 bài)",
x = "Độ lớn (Loudness, dB)",
y = "Độ nổi tiếng (Popularity)") +
theme_minimal() #5
theme_minimal()
## List of 136
## $ line :List of 6
## ..$ colour : chr "black"
## ..$ linewidth : num 0.5
## ..$ linetype : num 1
## ..$ lineend : chr "butt"
## ..$ arrow : logi FALSE
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_line" "element"
## $ rect :List of 5
## ..$ fill : chr "white"
## ..$ colour : chr "black"
## ..$ linewidth : num 0.5
## ..$ linetype : num 1
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_rect" "element"
## $ text :List of 11
## ..$ family : chr ""
## ..$ face : chr "plain"
## ..$ colour : chr "black"
## ..$ size : num 11
## ..$ hjust : num 0.5
## ..$ vjust : num 0.5
## ..$ angle : num 0
## ..$ lineheight : num 0.9
## ..$ margin : 'margin' num [1:4] 0points 0points 0points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : logi FALSE
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ title : NULL
## $ aspect.ratio : NULL
## $ axis.title : NULL
## $ axis.title.x :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 1
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 2.75points 0points 0points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.title.x.top :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 0
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 0points 2.75points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.title.x.bottom : NULL
## $ axis.title.y :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 1
## ..$ angle : num 90
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 2.75points 0points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.title.y.left : NULL
## $ axis.title.y.right :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 1
## ..$ angle : num -90
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 0points 0points 2.75points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : chr "grey30"
## ..$ size : 'rel' num 0.8
## ..$ hjust : NULL
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : NULL
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text.x :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 1
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 2.2points 0points 0points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text.x.top :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : NULL
## ..$ vjust : num 0
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 0points 2.2points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text.x.bottom : NULL
## $ axis.text.y :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : num 1
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 2.2points 0points 0points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text.y.left : NULL
## $ axis.text.y.right :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : num 0
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 0points 0points 2.2points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.text.theta : NULL
## $ axis.text.r :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : num 0.5
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : 'margin' num [1:4] 0points 2.2points 0points 2.2points
## .. ..- attr(*, "unit")= int 8
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ axis.ticks : list()
## ..- attr(*, "class")= chr [1:2] "element_blank" "element"
## $ axis.ticks.x : NULL
## $ axis.ticks.x.top : NULL
## $ axis.ticks.x.bottom : NULL
## $ axis.ticks.y : NULL
## $ axis.ticks.y.left : NULL
## $ axis.ticks.y.right : NULL
## $ axis.ticks.theta : NULL
## $ axis.ticks.r : NULL
## $ axis.minor.ticks.x.top : NULL
## $ axis.minor.ticks.x.bottom : NULL
## $ axis.minor.ticks.y.left : NULL
## $ axis.minor.ticks.y.right : NULL
## $ axis.minor.ticks.theta : NULL
## $ axis.minor.ticks.r : NULL
## $ axis.ticks.length : 'simpleUnit' num 2.75points
## ..- attr(*, "unit")= int 8
## $ axis.ticks.length.x : NULL
## $ axis.ticks.length.x.top : NULL
## $ axis.ticks.length.x.bottom : NULL
## $ axis.ticks.length.y : NULL
## $ axis.ticks.length.y.left : NULL
## $ axis.ticks.length.y.right : NULL
## $ axis.ticks.length.theta : NULL
## $ axis.ticks.length.r : NULL
## $ axis.minor.ticks.length : 'rel' num 0.75
## $ axis.minor.ticks.length.x : NULL
## $ axis.minor.ticks.length.x.top : NULL
## $ axis.minor.ticks.length.x.bottom: NULL
## $ axis.minor.ticks.length.y : NULL
## $ axis.minor.ticks.length.y.left : NULL
## $ axis.minor.ticks.length.y.right : NULL
## $ axis.minor.ticks.length.theta : NULL
## $ axis.minor.ticks.length.r : NULL
## $ axis.line : list()
## ..- attr(*, "class")= chr [1:2] "element_blank" "element"
## $ axis.line.x : NULL
## $ axis.line.x.top : NULL
## $ axis.line.x.bottom : NULL
## $ axis.line.y : NULL
## $ axis.line.y.left : NULL
## $ axis.line.y.right : NULL
## $ axis.line.theta : NULL
## $ axis.line.r : NULL
## $ legend.background : list()
## ..- attr(*, "class")= chr [1:2] "element_blank" "element"
## $ legend.margin : 'margin' num [1:4] 5.5points 5.5points 5.5points 5.5points
## ..- attr(*, "unit")= int 8
## $ legend.spacing : 'simpleUnit' num 11points
## ..- attr(*, "unit")= int 8
## $ legend.spacing.x : NULL
## $ legend.spacing.y : NULL
## $ legend.key : list()
## ..- attr(*, "class")= chr [1:2] "element_blank" "element"
## $ legend.key.size : 'simpleUnit' num 1.2lines
## ..- attr(*, "unit")= int 3
## $ legend.key.height : NULL
## $ legend.key.width : NULL
## $ legend.key.spacing : 'simpleUnit' num 5.5points
## ..- attr(*, "unit")= int 8
## $ legend.key.spacing.x : NULL
## $ legend.key.spacing.y : NULL
## $ legend.frame : NULL
## $ legend.ticks : NULL
## $ legend.ticks.length : 'rel' num 0.2
## $ legend.axis.line : NULL
## $ legend.text :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : 'rel' num 0.8
## ..$ hjust : NULL
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : NULL
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ legend.text.position : NULL
## $ legend.title :List of 11
## ..$ family : NULL
## ..$ face : NULL
## ..$ colour : NULL
## ..$ size : NULL
## ..$ hjust : num 0
## ..$ vjust : NULL
## ..$ angle : NULL
## ..$ lineheight : NULL
## ..$ margin : NULL
## ..$ debug : NULL
## ..$ inherit.blank: logi TRUE
## ..- attr(*, "class")= chr [1:2] "element_text" "element"
## $ legend.title.position : NULL
## $ legend.position : chr "right"
## $ legend.position.inside : NULL
## $ legend.direction : NULL
## $ legend.byrow : NULL
## $ legend.justification : chr "center"
## $ legend.justification.top : NULL
## $ legend.justification.bottom : NULL
## $ legend.justification.left : NULL
## $ legend.justification.right : NULL
## $ legend.justification.inside : NULL
## $ legend.location : NULL
## $ legend.box : NULL
## $ legend.box.just : NULL
## $ legend.box.margin : 'margin' num [1:4] 0cm 0cm 0cm 0cm
## ..- attr(*, "unit")= int 1
## $ legend.box.background : list()
## ..- attr(*, "class")= chr [1:2] "element_blank" "element"
## $ legend.box.spacing : 'simpleUnit' num 11points
## ..- attr(*, "unit")= int 8
## [list output truncated]
## - attr(*, "class")= chr [1:2] "theme" "gg"
## - attr(*, "complete")= logi TRUE
## - attr(*, "validate")= logi TRUE
Giải thích:
Câu lệnh #1 ggplot(spotify_sample, aes(x = loudness, y = popularity)) +: Khởi tạo ggplot từ mẫu. Ánh xạ loudness vào trục x và popularity vào trục y.
Câu lệnh #2 geom_point(alpha = 0.2, …): Vẽ các điểm tán xạ với độ trong suốt 20% (alpha = 0.2).
Câu lệnh #3 geom_smooth(method = “lm”, …): Thêm một đường xu hướng tuyến tính (lm).
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Nhận xét: Biểu đồ cho thấy một xu hướng dốc lên rõ rệt. Các bài hát có loudness cao hơn (gần 0 dB) có xu hướng đạt được popularity cao hơn. (Điều này khớp với kết quả cor = ~0.46 ở phần 1.4.5).
#1.5.4.5. Đồ thị 20: Độ nổi tiếng (Popularity) vs Độ mộc (Acousticness).
Mục đích là để trả lời câu hỏi: “Độ ‘mộc’ của bài hát có ảnh hưởng đến Độ nổi tiếng không?”.
# Đồ thị 20: Biểu đồ Tán xạ (Scatter Plot) - Popularity vs Acousticness
ggplot(spotify_sample, aes(x = acousticness, y = popularity)) + #1
geom_point(alpha = 0.2, color = "blue") + #2
geom_smooth(method = "lm", color = "red", size = 1) + #3
labs(title = "Độ mộc có ảnh hưởng đến Độ nổi tiếng?", #4
subtitle = "Mỗi chấm là một bài hát (lấy từ mẫu 10,000 bài)",
x = "Độ mộc (Acousticness)",
y = "Độ nổi tiếng (Popularity)") +
theme_minimal() #5
Giải thích:
Câu lệnh #1 ggplot(spotify_sample, aes(x = acousticness, y = popularity)) +: Khởi tạo ggplot từ mẫu. Ánh xạ acousticness vào trục x và popularity vào trục y.
Câu lệnh #2 geom_point(alpha = 0.2, …): Vẽ các điểm tán xạ với độ trong suốt 20% (alpha = 0.2).
Câu lệnh #3 geom_smooth(method = “lm”, …): Thêm một đường xu hướng tuyến tính (lm).
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Nhận xét: Đám mây điểm tập trung ở góc trên bên trái (mộc ít, nổi tiếng cao) và góc dưới bên phải (mộc nhiều, nổi tiếng thấp). Đường xu hướng màu đỏ dốc xuống, cho thấy một mối tương quan nghịch rõ rệt (clear negative correlation). Nhạc càng mộc thì càng ít nổi tiếng. (Điều này khớp với kết quả cor = ~-0.57 ở phần 1.4.5).
#1.5.5. Trực quan hóa quan hệ (nhiều biến).
Phần này sẽ vẽ các biểu đồ “tổng hợp” để kể những câu chuyện phức tạp hơn, kết hợp 3 biến trở lên (ví dụ: Phổ biến vs Năng lượng, Phổ biến vs Explicit).
#1.5.5.1. Đồ thị 21: Mối quan hệ Năng lượng & Phổ biến (Biểu đồ cột nhóm).
Trực quan hóa bảng pop_energy_summary (từ Thao tác 8) để so sánh số lượng bài hát có mức năng lượng (“Thap”, “Trung binh”, “Cao”) bên trong mỗi nhóm phổ biến.
# Đồ thị 21: Biểu đồ cột nhóm (Dodged Bar Chart).
ggplot(pop_energy_summary, aes(x = popularity_level, y = So_bai_hat, fill = energy_level)) + #1
geom_col(position = "dodge") + #2
labs(title = "Số lượng bài hát theo Mức độ Phổ biến và Năng lượng", #3
x = "Mức độ Phổ biến",
y = "Số lượng bài hát",
fill = "Mức Năng lượng") + # Đổi tên chú giải (legend)
theme_minimal() + #4
scale_y_continuous(labels = scales::comma) + #5
scale_x_discrete(limits = c("Thấp", "Trung bình", "Cao", "Rất cao")) #6
Giải thích:
Câu lệnh #1 ggplot(pop_energy_summary, aes(x = popularity_level, y = So_bai_hat, fill = energy_level)) +: Khởi tạo ggplot, sử dụng bảng tóm tắt pop_energy_summary (từ Thao tác 8) làm dữ liệu nguồn.
aes(…): Đây là ánh xạ 3 biến. Trục x là popularity_level (nhóm chính), y là So_bai_hat (chiều cao cột), và fill là energy_level (nhóm phụ, sẽ được tô màu).
Câu lệnh #2 geom_col(position = “dodge”) +: Sử dụng geom_col() (vì dữ liệu đã được đếm sẵn, có cả x và y).
position = “dodge”: Đây là lệnh mấu chốt. Nó chỉ định ggplot phải đặt các cột của nhóm phụ (fill) đứng cạnh nhau (dodge) tại mỗi vị trí x, thay vì “chồng lên nhau” (mặc định).
Câu lệnh #3 và #4: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn (lưu ý fill = “…” đổi tên chú giải) và áp dụng giao diện sạch sẽ.
Câu lệnh #5 scale_y_continuous(labels = scales::comma) +: Định dạng trục y (số lượng) với dấu phẩy hàng ngàn.
Câu lệnh #6: scale_x_discrete(limits = c(“Thấp”, “Trung bình”, “Cao”, “Rất cao”)): Tương tự Đồ thị 11 & 12, bắt buộc ggplot phải sắp xếp trục x theo thứ tự logic mong muốn.
Nhận xét: Biểu đồ cho thấy rõ ràng rằng ở nhóm “Cao” và “Rất cao”, các cột “Năng lượng Trung bình” và “Năng lượng Cao” chiếm ưu thế vượt trội. Ngược lại, ở nhóm “Thấp”, cột “Năng lượng Thấp” lại chiếm đáng kể.
#1.5.5.2. Đồ thị 22: Tỷ lệ Bài hát “Explicit” (Biểu đồ cột chồng 100%).
Trực quan hóa bảng pop_explicit_summary (từ Thao tác 9) để trả lời câu hỏi: “Tỷ lệ bài hát có nội dung nhạy cảm có tăng lên theo mức độ phổ biến không?”.
# Đồ thị 22: Biểu đồ cột chồng 100%
ggplot(pop_explicit_summary, aes(x = popularity_level, y = So_bai_hat, fill = explicit_label)) + #1
geom_col(position = "fill") + #2
scale_y_continuous(labels = scales::percent_format()) + #3
labs(title = "Tỷ lệ bài hát 'Explicit' theo Mức độ Phổ biến", #4
x = "Mức độ Phổ biến",
y = "Tỷ lệ phần trăm",
fill = "Nội dung") + # Đổi tên chú giải
theme_minimal() + #5
scale_x_discrete(limits = c("Thấp", "Trung bình", "Cao", "Rất cao")) #6
Giải thích:
Câu lệnh #1 ggplot(pop_explicit_summary, aes(x = popularity_level, y = So_bai_hat, fill = explicit_label)) +: Khởi tạo ggplot, sử dụng bảng tóm tắt pop_explicit_summary (từ Thao tác 9).
aes(…): Ánh xạ popularity_level (nhóm chính) vào x, So_bai_hat (số lượng) vào y, và explicit_label (nhóm phụ) vào fill.
Câu lệnh #2 geom_col(position = “fill”) +: Sử dụng geom_col() (vì dữ liệu đã được đếm sẵn).
position = “fill”: Đây là lệnh mấu chốt. Nó chỉ định ggplot làm 2 việc: 1) “Chồng” (stack) các nhóm phụ (fill) lên nhau, 2) Tự động chuẩn hóa (normalize) chiều cao của mỗi cột (mỗi nhóm x) về 100% (thang đo từ 0 đến 1). Kỹ thuật này hoàn hảo cho việc so sánh tỷ lệ (proportion) giữa các nhóm, thay vì so sánh số lượng tuyệt đối.
Câu lệnh #3 scale_y_continuous(labels = scales::percent_format()) +: Tùy chỉnh trục y. Vì position = “fill” đã đổi thang đo trục y thành (0-1), lệnh này sử dụng hàm scales::percent_format() để định dạng lại các nhãn (0, 0.25, 0.5…) thành dạng % (0%, 25%, 50%…), giúp biểu đồ dễ đọc.
Câu lệnh #4 và #5: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #6 scale_x_discrete(limits = c(“Thấp”, “Trung bình”, “Cao”, “Rất cao”)): Bắt buộc ggplot phải sắp xếp trục x theo thứ tự logic mong muốn. Nhận xét: Biểu đồ cho thấy một xu hướng rất rõ ràng: Tỷ lệ (diện tích màu) của các bài hát “Có Nhạy Cảm” tăng lên đáng kể khi mức độ phổ biến tăng, từ gần như không có ở nhóm “Thấp” lên đến chiếm một phần đáng kể ở nhóm “Rất cao”.
#1.5.5.3. Đồ thị 23: Ma trận Tương quan (Heatmap).
Trực quan hóa cor_matrix (từ Thao tác 24) thành một bản đồ nhiệt (Heatmap). Mục đích là biến bảng số khô khan thành một hình ảnh trực quan, nơi mắt người có thể nhận diện các mối quan hệ mạnh/yếu qua màu sắc.
# Đồ thị 23: Biểu đồ nhiệt (Heatmap) Ma trận tương quan
rplot(cor_matrix, #1
legend = TRUE, #2
print_cor = TRUE) + #3
labs(title = "Ma trận Tương quan giữa các Thuộc tính Âm nhạc") + #4
theme(axis.text.x = element_text(angle = 45, hjust = 1)) #5
Giải thích:
Câu lệnh #1 rplot(cor_matrix, …): rplot() là một hàm vẽ đặc biệt từ gói corrr (đã tải ở setup-all), được thiết kế chuyên để trực quan hóa một đối tượng ma trận tương quan (như cor_matrix đã tạo ở Thao tác 24). Hàm này tự động sử dụng ggplot2 bên dưới, ánh xạ giá trị tương quan vào màu sắc và vẽ các ô (geom_tile) để tạo ra một biểu đồ nhiệt (heatmap).
Câu lệnh #2 legend = TRUE: Một tham số của rplot(), yêu cầu hiển thị thanh chú giải (thang màu), giúp người đọc hiểu ý nghĩa của các màu (ví dụ: xanh = tương quan thuận, đỏ = tương quan nghịch).
Câu lệnh #3 print_cor = TRUE: Một tham số rất hữu ích của rplot(). Nó yêu cầu hàm này in giá trị số của hệ số tương quan (ví dụ: “0.77”) lên trên mỗi ô màu tương ứng, giúp việc đọc biểu đồ chính xác hơn.
Câu lệnh #4: labs(…) +: Tùy chỉnh tiêu đề cho biểu đồ (dấu + hoạt động vì rplot trả về một đối tượng ggplot).
Câu lệnh #5 theme(axis.text.x = element_text(angle = 45, hjust = 1)): Tùy chỉnh giao diện, xoay các nhãn trên trục x một góc 45 độ để tránh chồng chéo, giúp dễ đọc.
Nhận xét: Biểu đồ nhiệt cho phép quét nhanh các mối quan hệ: - Tương quan thuận mạnh (màu xanh đậm): energy và loudness. - Tương quan nghịch mạnh (màu đỏ đậm): acousticness và energy; acousticness và loudness. - Tương quan đáng chú ý: popularity tương quan thuận với loudness và energy, tương quan nghịch với acousticness.
#1.5.5.4. Đồ thị 24: Xu hướng Âm nhạc theo Thời gian (biểu đồ nhiều đường).
Trực quan hóa bảng trends_over_time (từ Thao tác 12-15) để so sánh 3 xu hướng (acousticness, energy, valence) trên cùng một biểu đồ. Kỹ thuật facet_wrap sẽ được dùng để tạo ra một “bảng điều khiển” (dashboard) gồm 3 biểu đồ con.
# Đồ thị 24: Biểu đồ nhiều đường với Facet
trends_tidy <- trends_over_time %>% #1
select(decade, mean_acousticness, mean_energy, mean_valence) %>% #2
gather(key = "metric", value = "value", -decade) #3
ggplot(trends_tidy, aes(x = decade, y = value, color = metric)) + #4
geom_line(size = 1.2, show.legend = FALSE) + #5
geom_point(size = 2, show.legend = FALSE) + #6
facet_wrap(~ metric, scales = "free_y") + #7
labs(title = "Các Xu hướng Âm nhạc chính theo Thập kỷ", #8
x = "Thập kỷ",
y = "Giá trị trung bình") +
theme_minimal() #9
Giải thích:
Câu lệnh #1 - #3: Chuẩn bị dữ liệu (Tidy Data).
Vấn đề: Bảng trends_over_time đang ở định dạng “rộng” (wide), với mỗi chỉ số (metric) là một cột riêng (mean_acousticness, mean_energy…). ggplot hoạt động tốt nhất với dữ liệu “dài” (long).
Câu lệnh #2: select(…): Chọn các cột cần thiết.
Câu lệnh #3 gather(key = “metric”, value = “value”, -decade): Đây là một hàm của tidyr. Nó “xoay” (hay “làm tan chảy” - melt) bảng dữ liệu. Nó lấy 3 cột (mean_acousticness, mean_energy, mean_valence), “gom” tên của chúng vào một cột mới tên là metric, và “gom” giá trị của chúng vào một cột mới tên là value. Cột decade được giữ nguyên (-decade).Kết quả: Bảng trends_tidy mới sẽ “dài” hơn (nhiều hàng hơn) nhưng “hẹp” hơn (ít cột hơn), đây là định dạng “tidy” (ngăn nắp) lý tưởng cho ggplot.
Câu lệnh #4 - #9: Vẽ biểu đồ từ bảng trends_tidy.
Câu lệnh #4 ggplot(trends_tidy, aes(x = decade, y = value, color = metric)) +: Khởi tạo ggplot từ bảng “dài” trends_tidy. Ánh xạ decade vào x, value vào y.
Câu lệnh #5 và #6: geom_line(…) và geom_point(…): Vẽ các đường (geom_line) và các điểm (geom_point) cho xu hướng.
show.legend = FALSE: Ẩn chú giải màu sắc, vì chúng ta sẽ dùng facet để thay thế.
scales = “free_y”: Đây là tham số rất quan trọng. Mặc định, ggplot sẽ buộc cả 3 biểu đồ con dùng chung một thang đo trục y. Vì 3 chỉ số này có thang đo khác nhau, scales = “free_y” cho phép mỗi biểu đồ con có thang đo trục y tự do (riêng biệt), giúp hiển thị xu hướng của từng chỉ số một cách rõ ràng.
Nhận xét: Bảng điều khiển 3 biểu đồ này tóm tắt 3 câu chuyện lớn về lịch sử âm nhạc 100 năm qua: - mean_acousticness (độ mộc): Giảm mạnh và liên tục, cho thấy sự chuyển dịch từ nhạc cụ mộc sang nhạc cụ điện tử. - mean_energy (năng lượng): Tăng mạnh và liên tục, cho thấy âm nhạc ngày càng “sôi động” hơn. - mean_valence (độ vui vẻ): Giảm nhẹ và liên tục, cho thấy xu hướng âm nhạc ngày càng “buồn” hoặc “sâu lắng” hơn.
#1.6. Kết luận chương 1.
Chương 1 đã thực hiện một quy trình Phân tích Dữ liệu Khám phá (EDA) toàn diện trên bộ dữ liệu Spotify 1921-2020. Mục tiêu của chương là tải, làm sạch, xử lý và phân tích bộ dữ liệu để rút ra các hiểu biết (insights) sâu sắc về đặc điểm và xu hướng âm nhạc qua 100 năm.
Quá trình này đã được thực hiện thành công qua các giai đoạn:
Tải và Khám phá (Phần 1.2): Đã xác thực được bộ dữ liệu có quy mô lớn (hơn 170,000 bản ghi) và có chất lượng rất cao. Dữ liệu hoàn toàn “sạch”, không chứa bất kỳ giá trị thiếu (NA) hay quan sát trùng lặp (duplicated) nào. Phạm vi dữ liệu (1921-2020) cũng được xác nhận là chính xác, tạo một nền tảng đáng tin cậy cho phân tích.
Tiền xử lý và Làm giàu (Phần 1.3): Dữ liệu thô đã được làm giàu thông qua các kỹ thuật feature engineering. Các biến mới có ý nghĩa phân tích cao đã được tạo ra, bao gồm duration_min (thời lượng bằng phút), decade (thập kỷ), và các biến phân loại như popularity_level, energy_level, và explicit_label. Các kỹ thuật xử lý dữ liệu giả định (lọc ngoại lai, imputation, chuẩn hóa) cũng được trình bày để thể hiện năng lực xử lý các bộ dữ liệu phức tạp trong thực tế.
Thống kê và Trực quan hóa (Phần 1.4 & 1.5): Đây là phần trọng tâm của chương. Thông qua 24 thao tác thống kê và 24 biểu đồ trực quan hóa, nhiều xu hướng và mối quan hệ quan trọng đã được phát hiện.
Các phát hiện chính của chương này có thể được tóm tắt thành ba câu chuyện lớn:
Sự trỗi dậy của âm thanh điện tử: Phân tích xu hướng theo thập kỷ (Đồ thị 24) cho thấy một phát hiện rõ nét nhất: acousticness (độ mộc) đã giảm mạnh và liên tục từ 1920 đến nay. Ngược lại, energy (năng lượng) và loudness (độ lớn) tăng đều đặn. Điều này phản ánh rõ ràng sự chuyển dịch của ngành công nghiệp âm nhạc từ các bản ghi âm mộc, cổ điển sang âm nhạc điện tử, được sản xuất và khuếch đại.
Xu hướng về tiết tấu và cảm xúc: Mức độ “dễ nhảy” (danceability) và thời lượng bài hát (duration_min) trung bình đều đạt đỉnh vào khoảng thập niên 1970-1980 (Đồ thị 13, 15). Đáng chú ý, valence (độ vui vẻ) cho thấy xu hướng giảm nhẹ qua thời gian, cho thấy âm nhạc hiện đại có xu hướng “buồn” hoặc “sâu lắng” hơn.
Sự bùng nổ của sản xuất: Số lượng bài hát được ghi lại tăng trưởng theo cấp số nhân, bùng nổ mạnh mẽ từ sau năm 1950 (Đồ thị 7).
Âm thanh của sự nổi tiếng: Phân tích tương quan (Đồ thị 19, 20) và phân tích theo nhóm (Đồ thị 12, 14) đều đi đến một kết luận chung: Các bài hát nổi tiếng (popularity cao) có một “dấu vân tay” âm thanh rõ rệt. Chúng có xu hướng to hơn (loudness cao), nhiều năng lượng hơn (energy cao), và ít mộc hơn (acousticness thấp).
Nội dung và sự phổ biến: Một trong những phát hiện đáng ngạc nhiên nhất là mối liên hệ giữa nội dung nhạy cảm và độ phổ biến (Đồ thị 22). Mặc dù các bài hát “explicit” chỉ chiếm một phần nhỏ trong tổng thể, tỷ lệ của chúng tăng vọt trong nhóm các bài hát có độ phổ biến “Cao” và “Rất cao”.
Các thuộc tính đồng hành: Phân tích tương quan (Đồ thị 16, 23) xác nhận mối quan hệ tương quan thuận cực kỳ mạnh mẽ giữa energy và loudness. Hai thuộc tính này gần như đồng nghĩa với nhau trong âm nhạc.
Các thuộc tính đối nghịch: Tương tự, acousticness được xác nhận là yếu tố đối nghịch cơ bản với âm nhạc hiện đại, thể hiện qua mối tương quan nghịch rất mạnh với cả energy và loudness (Đồ thị 17, 23).
Tóm lại, Chương 1 đã hoàn thành xuất sắc mục tiêu đề ra. Bằng cách kết hợp các kỹ thuật tiền xử lý của dplyr và khả năng trực quan hóa của ggplot2, bộ dữ liệu Spotify đã được “mở khóa”, cung cấp những hiểu biết có giá trị, dựa trên bằng chứng về lịch sử và đặc điểm của âm nhạc. Các kết quả phân tích này đã tạo một nền tảng vững chắc, kết thúc phần phân tích khám phá thứ nhất của bài tiểu luận.
#CHƯƠNG 2: PHÂN TÍCH DỮ LIỆU TÀI CHÍNH MÃ CHỨNG KHOÁN AGR.
#2.1. Giới thiệu và khám phá sơ bộ dữ liệu (EDA).
Chuyển sang chương thứ hai của bài tiểu luận, phần này sẽ tập trung vào một bộ dữ liệu hoàn toàn khác về bản chất: dữ liệu tài chính theo chuỗi thời gian của Công ty Cổ phần Chứng khoán Agribank (mã: AGR). Khác với bộ dữ liệu Spotify (lớn về số lượng quan sát), bộ dữ liệu này nhỏ gọn về số hàng (10 năm) nhưng phức tạp về cấu trúc dữ liệu thô.
Mục tiêu của phần này là thực hiện các bước khám phá sơ bộ (EDA) để xác định các vấn đề về chất lượng dữ liệu. Trọng tâm chính là phát hiện và chẩn đoán các kiểu dữ liệu bị đọc sai (ví dụ: số liệu tài chính bị đọc thành văn bản), vốn là một thách thức phổ biến khi làm việc với các tệp CSV xuất từ các nguồn bên ngoài.
#2.1.1. Tải dữ liệu.
Bước đầu tiên là tải tệp mckarg.csv vào môi trường R. Một tham số quan trọng (locale) được bổ sung ngay tại bước này.
# Thao tác 1: Đọc Dữ liệu
agr_data <- read_csv("mckarg.csv", locale = locale(encoding = "UTF-8")) #1
Giải thích:
Câu lệnh #1: agr_data <- read_csv(“mckarg.csv”, locale = locale(encoding = “UTF-8”))
agr_data <- read_csv(…): Tương tự như Chương 1, hàm read_csv (thuộc gói readr của tidyverse) được sử dụng để đọc tệp mckarg.csv và lưu vào biến agr_data.
locale = locale(encoding = “UTF-8”): Đây là một tham số bắt buộc trong tình huống này. Do các tên cột trong tệp CSV (ví dụ: “TỔNG TÀI SẢN”, “NỢ PHẢI TRẢ”) là tiếng Việt có dấu, việc chỉ định mã hóa (encoding) là UTF-8 đảm bảo R đọc và hiển thị các ký tự này một cách chính xác, tránh các lỗi hiển thị (ví dụ: “Tá»”NG TÀI SẢN”).
Nhận xét: Dữ liệu đã được tải vào biến agr_data. Bước tiếp theo là kiểm tra xem R đã hiểu dữ liệu này như thế nào.
#2.1.2. Kiểm tra kích thước.
Thao tác này xác định chính xác số lượng hàng (quan sát) và số lượng cột (biến) của bộ dữ liệu vừa tải, cung cấp cái nhìn tổng quan về phạm vi của dữ liệu.
# Thao tác 2: Kiểm tra Kích thước
dim(agr_data)
## [1] 10 22
Giải thích: Hàm dim() được dùng để lấy kích thước (dimensions) của một đối tượng dữ liệu (như data frame). Hàm này trả về một vector số nguyên: [1] 10 22. Số đầu tiên (10) là số lượng hàng (quan sát), số thứ hai (22) là số lượng cột (biến).
Nhận xét: Kết quả cho thấy có 10 hàng và 22 cột. Điều này xác nhận đây là một bộ dữ liệu chuỗi thời gian (time-series) nhỏ gọn, bao gồm 10 quan sát (tương ứng 10 năm) và 22 chỉ tiêu tài chính (biến) cho mỗi năm.
#2.1.3. Xem lướt cấu trúc (Glimpse).
Đây là bước “chẩn đoán” quan trọng nhất. Trong khi dim() chỉ cho biết
số lượng cột, glimpse() cho phép kiểm tra nội dung và kiểu dữ liệu
(
# Thao tác 3: Xem lướt Cấu trúc (Glimpse)
glimpse(agr_data) # 1
## Rows: 10
## Columns: 22
## $ Năm <dbl> 2015, 2016,…
## $ `TỔNG TÀI SẢN` <dbl> 2.242172e+1…
## $ `NỢ PHẢI TRẢ` <dbl> 1.881087e+1…
## $ `VỐN CHỦ SỞ HỮU` <dbl> 2.054063e+1…
## $ `Tiền gửi của khách hàng` <dbl> 1.635051e+1…
## $ `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl> 16350508305…
## $ `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl> 2110165790,…
## $ `Cộng doanh thu hoạt động` <dbl> 16878651161…
## $ `Cộng chi phí hoạt động` <dbl> -5021368305…
## $ `Cộng doanh thu hoạt động tài chính` <chr> "Không có",…
## $ `Cộng chi phí tài chính` <chr> "Không có",…
## $ `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl> -3312897178…
## $ `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl> -2127160302…
## $ `CHI PHÍ THUẾ TNDN` <dbl> 26010087063…
## $ `LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` <dbl> -1867059431…
## $ `TỔNG LỖ TOÀN DIỆN KHÁC SAU THUẾ TNDN` <chr> "Không có",…
## $ `Lãi cơ bản trên cổ phiếu (VND/cổ phiếu)` <dbl> -884, -1919…
## $ `Lưu chuyển tiền thuần từ hoạt động kinh doanh` <dbl> 39526400977…
## $ `Lưu chuyển tiền thuần từ hoạt động đầu tư` <dbl> -3508377915…
## $ `Lưu chuyển tiền thuần từ hoạt động tài chính` <dbl> -1813666610…
## $ `Tiền và các khoản tương đương tiền đầu năm` <dbl> 35289449606…
## $ `Tiền và các khoản tương đương tiền cuối năm` <dbl> 56328346692…
Giải thích:
Hàm glimpse() (thuộc gói dplyr) được sử dụng để “liếc” nhanh cấu trúc của data frame. Với 22 cột, glimpse() trình bày theo chiều dọc, giúp dễ dàng xem tất cả các cột, kiểu dữ liệu và một vài giá trị mẫu.
Kết quả glimpse() là phát hiện quan trọng nhất của phần này. Nó
cho thấy, ngoại trừ cột Năm (có thể là
Nguyên nhân: Dữ liệu thô trong các cột này chứa các ký tự phi số học, bao gồm dấu phẩy phân cách hàng ngàn (ví dụ: “2,242,171,802,587”) và các giá trị văn bản (ví dụ: “Không có”).
Hệ quả: Vì R thấy các ký tự này, nó không thể coi chúng là số và buộc phải đọc toàn bộ cột dưới dạng văn bản.
Nhận xét: Phát hiện này có nghĩa là bộ dữ liệu đang ở trạng thái “bẩn” (dirty data). Chúng ta không thể thực hiện bất kỳ phép tính toán (tổng, trung bình) hay vẽ biểu đồ xu hướng nào. Đây chính là vấn đề cốt lõi cần được giải quyết ở phần 2.2.
#2.1.4. Tạo bảng tra cứu (Metadata).
Do bộ dữ liệu có 22 biến với tên tiếng Việt đầy đủ, việc tạo một bảng
tra cứu (metadata) là vô cùng cần thiết để quản lý. Bảng này liệt kê rõ
ràng tên biến, kiểu dữ liệu gốc (vừa phát hiện là
# Thao tác 4: Tạo bảng tra cứu (Metadata)
agr_metadata <- tribble( # 1
~Variable, ~Data_Type_Raw, ~Description,
"Năm", "double", "Năm hạch toán (2015-2024)",
"TỔNG TÀI SẢN", "character", "Tổng tài sản của công ty",
"NỢ PHẢI TRẢ", "character", "Tổng nợ phải trả",
"VỐN CHỦ SỞ HỮU", "character", "Vốn chủ sở hữu của công ty",
"Tiền gửi của khách hàng", "character", "Tiền gửi của khách hàng tại công ty",
"Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...", "character", "Tiền gửi giao dịch của nhà đầu tư",
"Phải trả cổ tức, gốc và lãi trái phiếu", "character", "Các khoản phải trả ngắn hạn",
"Cộng doanh thu hoạt động", "character", "Tổng doanh thu từ hoạt động kinh doanh",
"Cộng chi phí hoạt động", "character", "Tổng chi phí hoạt động kinh doanh",
"Cộng doanh thu hoạt động tài chính", "character", "Doanh thu từ hoạt động tài chính",
"Cộng chi phí tài chính", "character", "Chi phí tài chính",
"CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN", "character", "Chi phí quản lý doanh nghiệp",
"TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ", "character", "Lợi nhuận trước thuế (EBT)",
"CHI PHÍ THUẾ TNDN", "character", "Chi phí thuế thu nhập doanh nghiệp",
"LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN", "character", "Lợi nhuận sau thuế (EAT/Net Income)",
"TỔNG LỖ TOÀN DIỆN KHÁC SAU THUẾ TNDN", "character", "Thu nhập/Lỗ toàn diện khác",
"Lãi cơ bản trên cổ phiếu (VND/cổ phiếu)", "character", "Earnings Per Share (EPS)",
"Lưu chuyển tiền thuần từ hoạt động kinh doanh", "character", "Cash Flow from Operations (CFO)",
"Lưu chuyển tiền thuần từ hoạt động đầu tư", "character", "Cash Flow from Investing (CFI)",
"Lưu chuyển tiền thuần từ hoạt động tài chính", "character", "Cash Flow from Financing (CFF)",
"Tiền và các khoản tương đương tiền đầu năm", "character", "Tiền mặt đầu kỳ",
"Tiền và các khoản tương đương tiền cuối năm", "character", "Tiền mặt cuối kỳ"
)
print(agr_metadata, n = Inf)
## # A tibble: 22 × 3
## Variable Data_Type_Raw Description
## <chr> <chr> <chr>
## 1 Năm double Năm hạch t…
## 2 TỔNG TÀI SẢN character Tổng tài s…
## 3 NỢ PHẢI TRẢ character Tổng nợ ph…
## 4 VỐN CHỦ SỞ HỮU character Vốn chủ sở…
## 5 Tiền gửi của khách hàng character Tiền gửi c…
## 6 Phải trả Nhà đầu tư về tiền gửi giao dịch chứng kh… character Tiền gửi g…
## 7 Phải trả cổ tức, gốc và lãi trái phiếu character Các khoản …
## 8 Cộng doanh thu hoạt động character Tổng doanh…
## 9 Cộng chi phí hoạt động character Tổng chi p…
## 10 Cộng doanh thu hoạt động tài chính character Doanh thu …
## 11 Cộng chi phí tài chính character Chi phí tà…
## 12 CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN character Chi phí qu…
## 13 TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ character Lợi nhuận …
## 14 CHI PHÍ THUẾ TNDN character Chi phí th…
## 15 LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN character Lợi nhuận …
## 16 TỔNG LỖ TOÀN DIỆN KHÁC SAU THUẾ TNDN character Thu nhập/L…
## 17 Lãi cơ bản trên cổ phiếu (VND/cổ phiếu) character Earnings P…
## 18 Lưu chuyển tiền thuần từ hoạt động kinh doanh character Cash Flow …
## 19 Lưu chuyển tiền thuần từ hoạt động đầu tư character Cash Flow …
## 20 Lưu chuyển tiền thuần từ hoạt động tài chính character Cash Flow …
## 21 Tiền và các khoản tương đương tiền đầu năm character Tiền mặt đ…
## 22 Tiền và các khoản tương đương tiền cuối năm character Tiền mặt c…
Giải thích:
Câu lệnh #1 agr_metadata <- tribble(…): Hàm tribble() (thuộc
gói tibble) được dùng để tạo một data frame (tibble) mới tên là
agr_metadata một cách trực quan, đóng vai trò là “từ điển dữ liệu”. Cột
Data_Type_Raw: Cột này được tạo ra để ghi lại hiện trạng kiểu dữ liệu
gốc (
Câu lệnh #2 print(agr_metadata, n = Inf): In toàn bộ nội dung của bảng agr_metadata. Tham số n = Inf (vô hạn) là bắt buộc để R in ra tất cả 22 hàng (thay vì 10 hàng mặc định).
Nhận xét: Bảng tra cứu này đóng vai trò quan trọng như một “từ điển”
dữ liệu. Nó ghi lại hiện trạng kiểu dữ liệu (
#2.1.5. Kiểm tra 6 dòng đầu.
Trong khi glimpse() cho thấy cấu trúc cột, head() cung cấp một cái nhìn trực quan theo hàng, giống như xem trong bảng tính. Đây là bước xác nhận bằng mắt thường các vấn đề đã chẩn đoán ở thao tác 3.
# Thao tác 5: Kiểm tra 6 dòng đầu
head(agr_data)
## # A tibble: 6 × 22
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 17 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <chr>, `Cộng chi phí tài chính` <chr>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích: Hàm head() được dùng để xem 6 dòng đầu tiên của data frame. Kết quả cho thấy rõ ràng các giá trị “bẩn”: các số được bao trong dấu ngoặc kép (ví dụ: “2,242,171,802,587”) và chứa đầy dấu phẩy.
Nhận xét: Thao tác này trực quan hóa lý do tại sao R đọc chúng là
#2.1.6. Kiểm tra 6 dòng cuối.
Tương tự head(), thao tác tail() kiểm tra 6 dòng cuối cùng. Mục đích là để phát hiện các dữ liệu “rác” (junk data) thường xuất hiện ở cuối các tệp CSV tài chính (ví dụ: “Nguồn:…”, “Tổng cộng:”, hoặc các hàng trống).
# Thao tác 6: Kiểm tra 6 dòng cuối
tail(agr_data)
## # A tibble: 6 × 22
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2019 2111308956461 144414199839 1966894756622 149612555145
## 2 2020 2286420537073 189791546527 2096628990546 418977823548
## 3 2021 2739269214518 288227817971 2451041396547 645697274287
## 4 2022 2805191288242 307969917729 2497221370513 487771892263
## 5 2023 3062598168848 567859059590 2494739109258 1410273320776
## 6 2024 3472227107539 1029539399155 2442687708384 528048523971
## # ℹ 17 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <chr>, `Cộng chi phí tài chính` <chr>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích: Hàm tail() được dùng để xem 6 dòng cuối cùng của data frame. Kết quả hiển thị dữ liệu của các năm gần đây nhất.
Nhận xét: Việc không thấy các dòng “rác” hay dòng tổng kết ở 6 dòng cuối xác nhận rằng bộ dữ liệu kết thúc một cách “sạch sẽ”, chỉ chứa dữ liệu hạch toán 10 năm..
#2.1.7. Liệt kê tên các biến.
Hàm colnames() trích xuất tất cả 22 tên cột thành một vector ký tự.
# Thao tác 7: Liệt kê tên các biến
colnames(agr_data)
## [1] "Năm"
## [2] "TỔNG TÀI SẢN"
## [3] "NỢ PHẢI TRẢ"
## [4] "VỐN CHỦ SỞ HỮU"
## [5] "Tiền gửi của khách hàng"
## [6] "Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán..."
## [7] "Phải trả cổ tức, gốc và lãi trái phiếu"
## [8] "Cộng doanh thu hoạt động"
## [9] "Cộng chi phí hoạt động"
## [10] "Cộng doanh thu hoạt động tài chính"
## [11] "Cộng chi phí tài chính"
## [12] "CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN"
## [13] "TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ"
## [14] "CHI PHÍ THUẾ TNDN"
## [15] "LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN"
## [16] "TỔNG LỖ TOÀN DIỆN KHÁC SAU THUẾ TNDN"
## [17] "Lãi cơ bản trên cổ phiếu (VND/cổ phiếu)"
## [18] "Lưu chuyển tiền thuần từ hoạt động kinh doanh"
## [19] "Lưu chuyển tiền thuần từ hoạt động đầu tư"
## [20] "Lưu chuyển tiền thuần từ hoạt động tài chính"
## [21] "Tiền và các khoản tương đương tiền đầu năm"
## [22] "Tiền và các khoản tương đương tiền cuối năm"
Giải thích: Hàm colnames() trích xuất tên của tất cả các cột từ data frame agr_data. Kết quả trả về một vector ký tự (character vector) chứa 22 tên cột.
Nhận xét: Thao tác này rất hữu ích trong thực tế. Nó cung cấp một danh sách “sạch” chỉ chứa tên các cột. Danh sách này có thể được sao chép (copy-paste) để sử dụng trong các thao tác làm sạch ở phần 2.2, đặc biệt là khi cần áp dụng một hàm (như mutate(across(…))) cho hàng loạt cột.
#2.1.8. Kiểm tra dữ liệu thiếu (NA chuẩn).
Đây là một trong những bước quan trọng nhất. Chúng ta cần biết liệu có “ô trống” (giá trị NA chuẩn của R) nào trong toàn bộ bộ dữ liệu hay không.
# Thao tác 8: Đếm TỔNG số giá trị thiếu (NA) chuẩn
sum(is.na(agr_data))
## [1] 0
Giải thích:
Câu lệnh: sum(is.na(agr_data))
is.na(agr_data): Hàm này kiểm tra từng ô trong data frame, trả về TRUE nếu ô đó là NA (thiếu) và FALSE nếu không.
sum(…): Hàm sum() (R cơ bản) đếm tổng số giá trị TRUE (vì R coi TRUE = 1).
Kết quả: Kết quả là 0.
Nhận xét: Kết quả bằng 0 cho thấy không có giá trị NA chuẩn nào. Điều này quan trọng vì nó có nghĩa là mọi “ô trống” hoặc “dữ liệu thiếu” trong bộ dữ liệu này đang được biểu thị bằng một hình thức khác (phi chuẩn), mà chúng ta đã thấy ở thao tác 3.
#2.1.9. Kiểm tra dữ liệu thiếu (Tùy chỉnh - “Không có”).
Sau khi xác nhận không có NA chuẩn, bước này sẽ “săn” giá trị thiếu phi chuẩn mà chúng ta đã thấy: chuỗi văn bản “Không có”.
# Thao tác 9: Đếm tổng số giá trị "Không có"
sum(agr_data == "Không có", na.rm = TRUE)
## [1] 4
Giải thích:
Câu lệnh: sum(agr_data == “Không có”, na.rm = TRUE)
agr_data == “Không có”: Đây là một phép so sánh logic vector hóa. R sẽ kiểm tra từng ô trong toàn bộ data frame, nếu ô đó chứa chính xác chuỗi “Không có”, nó trả về TRUE.
sum(…): Đếm tổng số lần TRUE xuất hiện.
na.rm = TRUE: Là một tham số an toàn (mặc dù ở Thao tác 8 ta biết không có NA) để bỏ qua các giá trị NA (nếu có) khi tính tổng.
Nhận xét: Kết quả này (ví dụ: 10) cho biết chính xác có bao nhiêu ô dữ liệu “bẩn” (đang là văn bản “Không có”) cần được xử lý và chuyển đổi thành NA (hoặc số 0) ở phần 2.2.
#2.1.10. Kiểm tra quan sát trùng lặp.
Thao tác này kiểm tra xem có bất kỳ dòng nào (tức là “năm”) nào bị trùng lặp 100% hay không.
# Thao tác 10: Kiểm tra số lượng quan sát trùng lặp
duplicated_rows_agr <- sum(duplicated(agr_data)) # 1
print(paste("Số lượng quan sát trùng lặp là:", duplicated_rows_agr)) # 2
## [1] "Số lượng quan sát trùng lặp là: 0"
Giải thích:
Câu lệnh #1: duplicated_rows_agr <- sum(duplicated(agr_data))
duplicated(agr_data): Hàm duplicated() (R cơ bản) kiểm tra từng dòng, trả về TRUE nếu dòng đó giống hệt một dòng đã xuất hiện trước đó.
sum(…): Đếm tổng số giá trị TRUE (tổng số hàng trùng lặp).
Câu lệnh #2 print(paste(…)): In kết quả đếm được ra màn hình kèm theo mô tả.
Kết quả: Kết quả là 0.
Nhận xét: Đối với dữ liệu tài chính theo chuỗi thời gian, mỗi năm chỉ được xuất hiện một lần. Kết quả là 0 xác nhận tính toàn vẹn của chuỗi thời gian (mỗi năm là duy nhất), không có năm nào bị lặp lại gây sai lệch phân tích.
Kết luận Phần 2.1: Kết thúc giai đoạn khám phá, chân dung của bộ dữ
liệu đã rõ ràng: đây là một bộ dữ liệu chuỗi thời gian “bẩn”. Mặc dù
không có NA chuẩn hay trùng lặp, nhưng 21/22 cột tài chính đang bị R
hiểu sai là văn bản (
#2.2. Tiền xử lý và làm giàu dữ liệu (Data Preprocessing and Feature Engineering).
Mục tiêu: Phần 2.1 đã chẩn đoán chính xác vấn đề: 21 trong 22 cột dữ
liệu đang bị R hiểu sai là văn bản (
Phần 2.2 này sẽ thực hiện quy trình “giải cứu” dữ liệu thông qua 10 thao tác. Mục tiêu là chuyển đổi bộ dữ liệu thô, “bẩn” thành một bộ dữ liệu “sạch”, có cấu trúc, sẵn sàng cho phân tích. Quá trình này bao gồm:
Làm sạch (Cleaning): Loại bỏ các ký tự phi số học.
Chuyển đổi kiểu (Type Casting): Ép kiểu dữ liệu từ văn bản sang số.
Làm giàu (Feature Engineering): Tạo ra các tỷ số tài chính quan trọng (ROA, ROE) từ các dữ liệu gốc.
Tái cấu trúc (Restructuring): Chuẩn hóa tên cột để dễ dàng truy vấn ở phần sau.
#2.2.1. Tạo bản sao dữ liệu.
Một nguyên tắc cơ bản trong phân tích dữ liệu là không bao giờ thao tác trực tiếp trên bộ dữ liệu gốc (agr_data). Việc này nhằm đảm bảo tính toàn vẹn của dữ liệu ban đầu, phòng trường hợp các bước xử lý sau này xảy ra lỗi hoặc cần đối chiếu lại. Thao tác này tạo một bản sao (agr_processed) để thực hiện tất cả các thay đổi.
# Thao tác 1: Tạo bản sao dữ liệu
agr_processed <- agr_data
Giải thích:
Nhận xét: Đây là một bước thực hành an toàn. Mọi thao tác làm sạch, thay đổi, thêm/xóa cột sau này sẽ được thực hiện trên bản sao agr_processed. Dữ liệu gốc agr_data được giữ nguyên vẹn để đối chiếu khi cần.
#2.2.2. Làm sạch dữ liệu (Xử lý “Không có” và Dấu phẩy).
Mục tiêu của thao tác này là “dọn dẹp” 21 cột dữ liệu văn bản (
Thay thế tất cả giá trị “Không có” (đã phát hiện ở 2.1.9) bằng số “0” (vẫn ở dạng văn bản).
Xóa tất cả các dấu phẩy (,) phân cách hàng ngàn ra khỏi các con số.
Sẽ sử dụng hàm mutate() kết hợp với across() để áp dụng hai thay đổi này cho tất cả các cột, ngoại trừ cột Năm (vì cột Năm đã sạch).
# Thao tác 2: Làm sạch cột (Xóa dấu phẩy và "Không có")
library(stringr) # 1
agr_processed <- agr_processed %>% # 2
mutate(across(-Năm, ~ str_replace_all(., "Không có", "0"))) %>% # 3
mutate(across(-Năm, ~ str_replace_all(., ",", ""))) # 4
head(agr_processed) # 5
## # A tibble: 6 × 22
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <chr> <chr> <chr> <chr>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 17 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <chr>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <chr>,
## # `Cộng doanh thu hoạt động` <chr>, `Cộng chi phí hoạt động` <chr>,
## # `Cộng doanh thu hoạt động tài chính` <chr>, `Cộng chi phí tài chính` <chr>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <chr>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <chr>, `CHI PHÍ THUẾ TNDN` <chr>, …
Giải thích:
Câu lệnh #1 library(stringr): Tải thư viện stringr (thuộc tidyverse) để có thể sử dụng hàm str_replace_all, một hàm mạnh mẽ để tìm kiếm và thay thế văn bản.
Câu lệnh #2 agr_processed <- agr_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả cuối cùng ngược trở lại agr_processed.
Câu lệnh #3: mutate(across(-Năm, ~ str_replace_all(., “Không có”, “0”))) %>%
mutate(across(…)): Cú pháp này vô cùng hiệu quả. across() cho phép áp dụng cùng một hàm (ký hiệu là ~) lên nhiều cột cùng lúc.
-Năm: Cú pháp này có nghĩa là “áp dụng cho tất cả các cột, TRỪ cột Năm”.
~ str_replace_all(., “Không có”, “0”): Hàm được áp dụng. Dấu . đại diện cho “cột hiện tại”. Hàm này tìm tất cả chuỗi “Không có” và thay thế bằng chuỗi “0”.
Câu lệnh #4 mutate(across(-Năm, ~ str_replace_all(., “,”, ““))): Tương tự Câu lệnh #3, nhưng lần này áp dụng hàm str_replace_all() để tìm tất cả dấu phẩy (,) và thay thế chúng bằng chuỗi rỗng (”“), tức là xóa bỏ chúng.
Câu lệnh #5 head(agr_processed): In 6 dòng đầu tiên ra để kiểm tra.
Nhận xét: Kết quả head() cho thấy các cột tài chính đã mất hết dấu
phẩy và chữ “Không có”. Tuy nhiên, chúng vẫn là kiểu
#2.2.3. Chuyển đổi kiểu dữ liệu (sang Số).
Sau khi đã “dọn dẹp” văn bản ở Thao tác 2, các cột của chúng ta
(ngoại trừ cột Năm) giờ là các chuỗi ký tự “sạch” (ví dụ:
“2242171802587”, “0”, “-50213683051”). Thao tác này sẽ dùng hàm
as.numeric() để “ép kiểu”, biến các chuỗi ký tự này thành các con số
thực (
# Thao tác 3: Chuyển đổi kiểu dữ liệu sang Số (numeric)
agr_processed <- agr_processed %>% # 1
mutate(across(-Năm, ~ as.numeric(.))) # 2
glimpse(agr_processed) # 3
## Rows: 10
## Columns: 22
## $ Năm <dbl> 2015, 2016,…
## $ `TỔNG TÀI SẢN` <dbl> 2.242172e+1…
## $ `NỢ PHẢI TRẢ` <dbl> 1.881087e+1…
## $ `VỐN CHỦ SỞ HỮU` <dbl> 2.054063e+1…
## $ `Tiền gửi của khách hàng` <dbl> 1.635051e+1…
## $ `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl> 16350508305…
## $ `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl> 2110165790,…
## $ `Cộng doanh thu hoạt động` <dbl> 16878651161…
## $ `Cộng chi phí hoạt động` <dbl> -5021368305…
## $ `Cộng doanh thu hoạt động tài chính` <dbl> 0, 17388103…
## $ `Cộng chi phí tài chính` <dbl> 0, -4712500…
## $ `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl> -3312897178…
## $ `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl> -2127160302…
## $ `CHI PHÍ THUẾ TNDN` <dbl> 26010087063…
## $ `LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` <dbl> -1867059431…
## $ `TỔNG LỖ TOÀN DIỆN KHÁC SAU THUẾ TNDN` <dbl> 0, 0, 50246…
## $ `Lãi cơ bản trên cổ phiếu (VND/cổ phiếu)` <dbl> -884, -1919…
## $ `Lưu chuyển tiền thuần từ hoạt động kinh doanh` <dbl> 39526400977…
## $ `Lưu chuyển tiền thuần từ hoạt động đầu tư` <dbl> -3508377915…
## $ `Lưu chuyển tiền thuần từ hoạt động tài chính` <dbl> -1813666610…
## $ `Tiền và các khoản tương đương tiền đầu năm` <dbl> 35289449606…
## $ `Tiền và các khoản tương đương tiền cuối năm` <dbl> 56328346692…
Giải thích:
Câu lệnh #1 agr_processed <- agr_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả cuối cùng ngược trở lại agr_processed.
Câu lệnh #2 mutate(across(-Năm, ~ as.numeric(.))): Dùng mutate(across(…)) để áp dụng một hàm cho nhiều cột (trừ cột Năm).
~ as.numeric(.): Hàm được áp dụng là as.numeric(). Hàm R cơ bản
này nhận . (cột hiện tại, đang là
Câu lệnh #3 glimpse(agr_processed): Dùng glimpse() để kiểm tra lại cấu trúc của data frame.
Nhận xét: Kết quả glimpse() (Thao tác 3) so với glimpse() (Thao tác
2.1.3) cho thấy một sự thay đổi lớn: tất cả 21 cột tài chính (trừ Năm)
bây giờ đã chuyển từ
#2.2.4. Tạo biến mới (Feature Engineering): ROA.
Từ dữ liệu gốc, các chỉ số tỷ suất (ratios) quan trọng sẽ được tính toán để đánh giá hiệu quả hoạt động. Chỉ số đầu tiên là ROA (Return on Assets) - Tỷ suất lợi nhuận trên tổng tài sản.
Công thức: ROA = Lợi nhuận sau thuế / Tổng tài sản.
# Thao tác 4: Tạo biến mới 'ROA'
agr_processed <- agr_processed %>% # 1
mutate(ROA = `LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` / `TỔNG TÀI SẢN`) # 2
head(agr_processed) # 3
## # A tibble: 6 × 23
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 18 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <dbl>, `Cộng chi phí tài chính` <dbl>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích:
Câu lệnh #1 agr_processed <- agr_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%) và gán kết quả cuối cùng ngược trở lại agr_processed.
Câu lệnh #2: mutate(ROA = … / …).
mutate(ROA = …): Sử dụng mutate() của dplyr để tạo một cột mới tên là ROA.
Dấu (huyền): Dấu huyền là bắt buộc phải sử dụng để
bao bọc các tên cột có chứa dấu cách hoặc ký tự tiếng Việt (ví dụ:
LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN).
Phép chia /: Phép chia này đã có thể thực hiện được vì cả hai cột
(LỢI NHUẬN… và TỔNG TÀI SẢN) đều đã được chuyển sang kiểu số (
Câu lệnh #3 head(agr_processed): In 6 dòng đầu ra để kiểm tra.
Nhận xét: Kết quả head() cho thấy một cột mới ROA (với các giá trị thập phân, ví dụ: 0.05 hoặc -0.02) xuất hiện ở cuối bảng. Đây là chỉ số “làm giàu” (feature engineering) đầu tiên của chúng ta..
#2.2.5. Tạo biến mới (Feature Engineering): ROE.
Tương tự như ROA, ROE (Return on Equity - Tỷ suất lợi nhuận trên vốn chủ sở hữu) là một chỉ số quan trọng bậc nhất để đánh giá hiệu quả sử dụng vốn.
Thao tác này sẽ tạo ra một cột mới tên là ROE.
Công thức: ROE = Lợi nhuận sau thuế / Vốn chủ sở hữu.
# Thao tác 5: Tạo biến mới 'ROE'
agr_processed <- agr_processed %>% # 1
mutate(ROE = `LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` / `VỐN CHỦ SỞ HỮU`) # 2
head(agr_processed) # 3
## # A tibble: 6 × 24
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 19 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <dbl>, `Cộng chi phí tài chính` <dbl>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích:
Câu lệnh #1 agr_processed <- agr_processed %>%: Tiếp tục chuỗi thao tác pipe (%>%) trên agr_processed.
Câu lệnh #2 mutate(ROE = … / …): Sử dụng mutate() để tạo một cột
mới tên là ROE. Lấy giá trị từ cột
LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN chia cho giá trị từ cột
VỐN CHỦ SỞ HỮU. Dấu huyền ( ) là bắt buộc do
tên cột có tiếng Việt và dấu cách.
Câu lệnh #3 head(agr_processed): In 6 dòng đầu ra để kiểm tra.
Nhận xét: Kết quả head() sẽ thấy cột ROE mới xuất hiện bên cạnh cột ROA ở cuối bảng.
#2.2.6. Tạo biến mới (Feature Engineering): Biên lợi nhuận ròng.
Chỉ số này (còn gọi là Net Profit Margin) cho biết công ty kiếm được bao nhiêu đồng lợi nhuận từ mỗi đồng doanh thu.
Thao tác này sẽ tạo ra một cột mới tên là Bien_Loi_Nhuan_Rong.
Công thức: Biên lợi nhuận ròng = Lợi nhuận sau thuế / Cộng doanh thu hoạt động.
# Thao tác 6: Tạo biến mới 'Bien_Loi_Nhuan_Rong'
agr_processed <- agr_processed %>% # 1
mutate(Bien_Loi_Nhuan_Rong = `LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` / `Cộng doanh thu hoạt động`) # 2
head(agr_processed) # 3
## # A tibble: 6 × 25
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 20 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <dbl>, `Cộng chi phí tài chính` <dbl>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích:
Câu lệnh #1 agr_processed <- agr_processed %>%: Tiếp tục chuỗi thao tác pipe (%>%) trên agr_processed.
Câu lệnh #2 mutate(Bien_Loi_Nhuan_Rong = … / …): Sử dụng mutate()
để tạo một cột mới tên là Bien_Loi_Nhuan_Rong. Lấy giá trị từ cột
LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN chia cho giá trị từ cột
Cộng doanh thu hoạt động.
Câu lệnh #3 head(agr_processed): In 6 dòng đầu ra để kiểm tra.
Nhận xét: Cột Bien_Loi_Nhuan_Rong mới xuất hiện ở cuối bảng.
#2.2.7. Tạo biến mới (Feature Engineering): Tỷ số Nợ trên Vốn chủ sở hữu (D/E).
Đây là một chỉ số đòn bẩy tài chính quan trọng, cho biết mức độ công ty sử dụng nợ để tài trợ cho tài sản so với vốn chủ sở hữu.
Thao tác này sẽ tạo ra một cột mới tên là DE_Ratio (Debt-to-Equity Ratio).
Công thức: Tỷ số D/E = Nợ phải trả / Vốn chủ sở hữu.
# Thao tác 7: Tạo biến mới 'DE_Ratio'
agr_processed <- agr_processed %>% # 1
mutate(DE_Ratio = `NỢ PHẢI TRẢ` / `VỐN CHỦ SỞ HỮU`) # 2
head(agr_processed) # 3
## # A tibble: 6 × 26
## Năm `TỔNG TÀI SẢN` `NỢ PHẢI TRẢ` `VỐN CHỦ SỞ HỮU` `Tiền gửi của khách hàng`
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 163505083052
## 2 2016 1660379497164 12044370105 1648335127059 185241610291
## 3 2017 1776671310931 12812979026 1763858331905 273566621452
## 4 2018 1917072477344 46650397331 1870422080013 285970535936
## 5 2019 2111308956461 144414199839 1966894756622 149612555145
## 6 2020 2286420537073 189791546527 2096628990546 418977823548
## # ℹ 21 more variables:
## # `Phải trả Nhà đầu tư về tiền gửi giao dịch chứng khoán...` <dbl>,
## # `Phải trả cổ tức, gốc và lãi trái phiếu` <dbl>,
## # `Cộng doanh thu hoạt động` <dbl>, `Cộng chi phí hoạt động` <dbl>,
## # `Cộng doanh thu hoạt động tài chính` <dbl>, `Cộng chi phí tài chính` <dbl>,
## # `CHI PHÍ QUẢN LÝ CÔNG TY CHỨNG KHOÁN` <dbl>,
## # `TỔNG LỢI NHUẬN KẾ TOÁN TRƯỚC THUẾ` <dbl>, `CHI PHÍ THUẾ TNDN` <dbl>, …
Giải thích:
Câu lệnh #1 agr_processed <- agr_processed %>%: Tiếp tục chuỗi thao tác pipe (%>%) trên agr_processed.
Câu lệnh #2: mutate(DE_Ratio = … / …): Sử dụng mutate() để tạo
một cột mới tên là DE_Ratio (Debt-to-Equity Ratio). Lấy giá trị từ cột
NỢ PHẢI TRẢ chia cho giá trị từ cột
VỐN CHỦ SỞ HỮU.
Câu lệnh #3 head(agr_processed): In 6 dòng đầu ra để kiểm tra.
Nhận xét: Cột DE_Ratio mới xuất hiện ở cuối bảng, hoàn thành việc tạo ra các chỉ số tài chính phái sinh cơ bản.
#2.2.8. Tình huống giả định: Tạo biến phân loại (Binning) Lợi nhuận.
Hiện tại, các cột lợi nhuận là những con số tuyệt đối (ví dụ: 390,130,202,453 hoặc -186,705,943,166). Sẽ rất khó để thống kê xem “có bao nhiêu năm lãi” và “bao nhiêu năm lỗ”.
Mục đích của thao tác này (gọi là Binning - chia vào thùng) là tạo một cột mới (Loi_Nhuan_Status) bằng cách “cắt” cột Lợi nhuận sau thuế thành 2 nhóm (thùng) mà chúng ta tự định nghĩa: “Lãi” và “Lỗ”.
# Thao tác 8: Mã hóa (Binning) biến 'Loi_Nhuan_Status'
agr_processed <- agr_processed %>% # 1
mutate(Loi_Nhuan_Status = case_when( # 2
`LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` > 0 ~ "Lãi", # 3
`LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN` <= 0 ~ "Lỗ", # 4
TRUE ~ "Không xác định" # 5
))
table(agr_processed$Loi_Nhuan_Status) # 6
##
## Lãi Lỗ
## 8 2
Giải thích:
Câu lệnh #1 & #2 … %>% mutate(Loi_Nhuan_Status = case_when(…)): Sử dụng mutate() để tạo một cột mới tên là Loi_Nhuan_Status. Giá trị của cột này được quyết định bởi hàm case_when() (hàm if-else mạnh mẽ của dplyr).
Câu lệnh #3 LỢI NHUẬN... > 0 ~ “Lãi”: Nếu (if)
lợi nhuận sau thuế lớn hơn 0, gán nhãn là “Lãi”.
Câu lệnh #4 LỢI NHUẬN... <= 0 ~ “Lỗ”: Nếu không
(else if), lợi nhuận sau thuế nhỏ hơn hoặc bằng 0, gán nhãn là
“Lỗ”.
Câu lệnh #5 TRUE ~ “Không xác định”: Mệnh đề else cuối cùng, bắt các trường hợp khác (ví dụ: NA).
Câu lệnh #6 table(agr_processed$Loi_Nhuan_Status): Dùng lệnh table() (R cơ bản) để đếm nhanh xem có bao nhiêu năm “Lãi” và bao nhiêu năm “Lỗ”, giúp kiểm tra logic của case_when() có chạy đúng không.
Nhận xét: Thao tác này tạo ra một biến phân loại (
#2.2.9. Tình huống giả định: Làm sạch tên cột (Clean Column Names).
Ở các thao tác 4, 5, 6, 7, 8, việc gõ các tên cột tiếng Việt dài và có dấu (như LỢI NHUẬN KẾ TOÁN SAU THUẾ TNDN) rất vất vả và dễ gây lỗi (phải dùng dấu `` ).
Thao tác này sẽ sử dụng gói janitor để tự động “làm sạch” tất cả 27 tên cột của chúng ta, biến chúng thành dạng snake_case (ví dụ:loi_nhuan_ke_toan_sau_thue_tndn) - tức là viết thường, không dấu, và dùng dấu _ để nối các từ. Điều này sẽ giúp việc gọi tên cột ở Phần 3 (Thống kê) dễ dàng hơn rất nhiều.
# Thao tác 9: Làm sạch Tên cột
library(janitor) # 1
agr_processed <- agr_processed %>% # 2
clean_names() # 3
glimpse(agr_processed) # 4
## Rows: 10
## Columns: 27
## $ nam <dbl> 2015, 2016, 2017…
## $ tong_tai_san <dbl> 2.242172e+12, 1.…
## $ no_phai_tra <dbl> 1.881087e+11, 1.…
## $ von_chu_so_huu <dbl> 2.054063e+12, 1.…
## $ tien_gui_cua_khach_hang <dbl> 1.635051e+11, 1.…
## $ phai_tra_nha_dau_tu_ve_tien_gui_giao_dich_chung_khoan <dbl> 163505083052, 18…
## $ phai_tra_co_tuc_goc_va_lai_trai_phieu <dbl> 2110165790, 3470…
## $ cong_doanh_thu_hoat_dong <dbl> 168786511613, 13…
## $ cong_chi_phi_hoat_dong <dbl> -50213683051, -4…
## $ cong_doanh_thu_hoat_dong_tai_chinh <dbl> 0, 1738810369, 1…
## $ cong_chi_phi_tai_chinh <dbl> 0, -471250000, -…
## $ chi_phi_quan_ly_cong_ty_chung_khoan <dbl> -331289717849, -…
## $ tong_loi_nhuan_ke_toan_truoc_thue <dbl> -212716030229, -…
## $ chi_phi_thue_tndn <dbl> 26010087063, -39…
## $ loi_nhuan_ke_toan_sau_thue_tndn <dbl> -186705943166, -…
## $ tong_lo_toan_dien_khac_sau_thue_tndn <dbl> 0, 0, 5024653000…
## $ lai_co_ban_tren_co_phieu_vnd_co_phieu <dbl> -884, -1919, 309…
## $ luu_chuyen_tien_thuan_tu_hoat_dong_kinh_doanh <dbl> 395264009771, -2…
## $ luu_chuyen_tien_thuan_tu_hoat_dong_dau_tu <dbl> -3508377915, -19…
## $ luu_chuyen_tien_thuan_tu_hoat_dong_tai_chinh <dbl> -181366661000, 0…
## $ tien_va_cac_khoan_tuong_duong_tien_dau_nam <dbl> 352894496064, 39…
## $ tien_va_cac_khoan_tuong_duong_tien_cuoi_nam <dbl> 563283466920, 19…
## $ roa <dbl> -0.08327013, -0.…
## $ roe <dbl> -0.09089592, -0.…
## $ bien_loi_nhuan_rong <dbl> -1.1061663, -3.0…
## $ de_ratio <dbl> 0.091578855, 0.0…
## $ loi_nhuan_status <chr> "Lỗ", "Lỗ", "Lãi…
Giải thích:
Câu lệnh #1 library(janitor): Tải thư viện janitor, thư viện này chứa các hàm tiện ích để dọn dẹp dữ liệu.
Câu lệnh #2 agr_processed <- agr_processed %>%: Bắt đầu chuỗi thao tác pipe (%>%).
Câu lệnh #3 clean_names(): Đây là hàm cốt lõi của gói janitor. Nó tự động chuẩn hóa tất cả tên cột trong data frame. Hàm này tự động chuyển tên cột sang chữ thường, bỏ dấu tiếng Việt (ví dụ: Năm -> nam, VỐN CHỦ SỞ HỮU -> von_chu_so_huu), và thay thế dấu cách/ký tự đặc biệt bằng dấu gạch dưới (_).
Câu lệnh #4 glimpse(agr_processed): Dùng glimpse() để kiểm tra lại.
Nhận xét: Kết quả glimpse() xác nhận tất cả tên cột (bao gồm cả ROA,
ROE… đã được chuẩn hóa thành roa, roe…) (ví dụ: tong_tai_san,
no_phai_tra, von_chu_so_huu…). Điều này giúp việc truy vấn ở Phần 2.3 dễ
dàng hơn rất nhiều (không cần dấu nữa).
#2.2.10. Sắp xếp & Đổi tên các cột chính.
Thao tác cuối cùng này sẽ “dọn dẹp” bảng dữ liệu của chúng ta lần cuối trước khi phân tích. Chúng ta sẽ: (1) Đổi tên một số cột chính cho ngắn gọn hơn (ví dụ: loi_nhuan_ke_toan_sau_thue_tndnthànhloi_nhuan_sau_thue). (2) Sắp xếp lại thứ tự các cột (dùng select()) để đưa các chỉ số quan trọng (ROA, ROE…) lên gần các cột tài chính gốc, giúp việc đọc bảng dễ dàng hơn.
# Thao tác 10: Sắp xếp lại & Đổi tên cột
agr_processed <- agr_processed %>% # 1
select( # 2
nam,
tong_tai_san,
no_phai_tra,
von_chu_so_huu,
doanh_thu = cong_doanh_thu_hoat_dong, # 3
loi_nhuan_truoc_thue = tong_loi_nhuan_ke_toan_truoc_thue, # 4
loi_nhuan_sau_thue = loi_nhuan_ke_toan_sau_thue_tndn, # 5
roa,
roe,
de_ratio,
bien_loi_nhuan_rong,
loi_nhuan_status,
everything() # 6
)
head(agr_processed) # 7
## # A tibble: 6 × 27
## nam tong_tai_san no_phai_tra von_chu_so_huu doanh_thu loi_nhuan_truoc_thue
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2015 2242171802587 188108742589 2054063059998 1.69e11 -212716030229
## 2 2016 1660379497164 12044370105 1648335127059 1.34e11 -365821398106
## 3 2017 1776671310931 12812979026 1763858331905 1.85e11 81594093557
## 4 2018 1917072477344 46650397331 1870422080013 1.81e11 84821567306
## 5 2019 2111308956461 144414199839 1966894756622 2.04e11 85321106490
## 6 2020 2286420537073 189791546527 2096628990546 2.25e11 120991612113
## # ℹ 21 more variables: loi_nhuan_sau_thue <dbl>, roa <dbl>, roe <dbl>,
## # de_ratio <dbl>, bien_loi_nhuan_rong <dbl>, loi_nhuan_status <chr>,
## # tien_gui_cua_khach_hang <dbl>,
## # phai_tra_nha_dau_tu_ve_tien_gui_giao_dich_chung_khoan <dbl>,
## # phai_tra_co_tuc_goc_va_lai_trai_phieu <dbl>, cong_chi_phi_hoat_dong <dbl>,
## # cong_doanh_thu_hoat_dong_tai_chinh <dbl>, cong_chi_phi_tai_chinh <dbl>,
## # chi_phi_quan_ly_cong_ty_chung_khoan <dbl>, chi_phi_thue_tndn <dbl>, …
Giải thích:
Câu lệnh #1 & #2 … %>% select(…): Sử dụng hàm select() của dplyr để tái cấu trúc toàn bộ data frame theo thứ tự đã chỉ định.
Câu lệnh #3, #4, #5 doanh_thu = cong_doanh_thu_hoat_dong, …: Đây là cú pháp đổi tên trong select. Cột cong_doanh_thu_hoat_dong (đã được janitor làm sạch) sẽ được giữ lại nhưng với tên mới là doanh_thu.
Câu lệnh #6 everything(): Đây là một hàm trợ giúp của dplyr (dùng bên trong select). Nó có nghĩa là “chọn tất cả các cột còn lại chưa được liệt kê ở trên và đặt chúng ở vị trí này (tức là ở cuối)”. Điều này đảm bảo chúng ta không vô tình làm mất cột nào.
Câu lệnh #7 head(agr_processed): In 6 dòng đầu của bảng dữ liệu cuối cùng (đã được làm sạch, làm giàu, và sắp xếp lại).
Nhận xét: Kết quả head() cho thấy một bảng dữ liệu đã được tổ chức lại một cách logic, với các cột quan trọng nhất (đã được đổi tên ngắn gọn) được ưu tiên hiển thị trước. Data frame agr_processed đã hoàn toàn sẵn sàng cho Phần 2.3 (Phân tích thống kê).
#2.3. Phân tích thống kê cơ bản.
Mục tiêu: Sau khi dữ liệu đã được làm sạch, chuyển đổi và “làm giàu” ở phần 2.2, giai đoạn này sẽ thực hiện các phân tích thống kê mô tả chi tiết. Mục đích là để lượng hóa hiệu quả hoạt động, sức khỏe tài chính và khả năng sinh lời của Công ty Cổ phần Chứng khoán Agribank (AGR) trong giai đoạn 10 năm.
Để đảm bảo tính tập trung của phân tích, một bộ 12 biến cốt lõi sẽ được chọn lọc. Bộ 12 biến này bao gồm 7 chỉ tiêu tài chính gốc (như Tổng tài sản, Doanh thu, Lợi nhuận…) và 5 chỉ số phái sinh quan trọng (như ROA, ROE, D/E…) đã được tạo ra. Phần này sẽ thực hiện 20 thao tác thống kê để khám phá các xu hướng trung tâm (Mean, Median), độ phân tán (Min, Max) và các đặc điểm phân bổ của 12 chỉ số này.
Để tập trung vào các chỉ số quan trọng nhất và làm cho các thao tác sau này gọn gàng hơn, thao tác này sẽ tạo một bảng dữ liệu mới tên là agr_analysis chỉ chứa 12 biến đã chọn lọc từ agr_processed.
# Thao tác 1 (Phần 3): Chọn lọc 12 biến phân tích cốt lõi
agr_analysis <- agr_processed %>% # 1
select( # 2
nam,
tong_tai_san,
no_phai_tra,
von_chu_so_huu,
doanh_thu,
loi_nhuan_truoc_thue,
loi_nhuan_sau_thue,
roa,
roe,
de_ratio,
bien_loi_nhuan_rong,
loi_nhuan_status
)
glimpse(agr_analysis) # 3
## Rows: 10
## Columns: 12
## $ nam <dbl> 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2…
## $ tong_tai_san <dbl> 2.242172e+12, 1.660379e+12, 1.776671e+12, 1.91707…
## $ no_phai_tra <dbl> 1.881087e+11, 1.204437e+10, 1.281298e+10, 4.66504…
## $ von_chu_so_huu <dbl> 2.054063e+12, 1.648335e+12, 1.763858e+12, 1.87042…
## $ doanh_thu <dbl> 168786511613, 133600086761, 184849879214, 1812151…
## $ loi_nhuan_truoc_thue <dbl> -212716030229, -365821398106, 81594093557, 848215…
## $ loi_nhuan_sau_thue <dbl> -186705943166, -405397932938, 65275274846, 678152…
## $ roa <dbl> -0.08327013, -0.24415980, 0.03674021, 0.03537437,…
## $ roe <dbl> -0.09089592, -0.24594388, 0.03700710, 0.03625665,…
## $ de_ratio <dbl> 0.091578855, 0.007306991, 0.007264177, 0.02494110…
## $ bien_loi_nhuan_rong <dbl> -1.1061663, -3.0344137, 0.3531259, 0.3742249, 0.3…
## $ loi_nhuan_status <chr> "Lỗ", "Lỗ", "Lãi", "Lãi", "Lãi", "Lãi", "Lãi", "L…
Giải thích:
Câu lệnh #1 & #2 agr_analysis <- agr_processed %>% select(…): Sử dụng hàm select() của dplyr để chọn (subset) chính xác 12 cột quan tâm từ bảng agr_processed. Kết quả được gán vào một data frame con mới tên là agr_analysis.
Câu lệnh #3 glimpse(agr_analysis): Dùng glimpse() để kiểm tra bảng agr_analysis mới, đảm bảo nó chỉ chứa 10 hàng và 12 cột đã chọn.
Nhận xét: Việc này giúp cô lập không gian làm việc. Từ giờ, mọi phân tích ở Phần 2.3 và trực quan hóa ở Phần 2.4 sẽ chỉ tập trung vào 12 chỉ số cốt lõi này trong bảng agr_analysis, làm tăng tính rõ ràng của bài luận.
#2.3.1. Thống kê tổng quan (trên 12 biến đã chọn).
#2.3.1.1. Tóm tắt thống kê 5-số (Summary Statistics).
Đây là thao tác tổng quan nhất, cho cái nhìn toàn cảnh về 12 biến trong bộ dữ liệu đã chọn lọc.
# Thao tác 2: Tóm tắt 5 số liệu (của 12 biến)
library(scales) # 1
summary(agr_analysis) # 2
## nam tong_tai_san no_phai_tra von_chu_so_huu
## 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
## doanh_thu loi_nhuan_truoc_thue loi_nhuan_sau_thue
## Min. :1.336e+11 Min. :-3.658e+11 Min. :-4.054e+11
## 1st Qu.:1.821e+11 1st Qu.: 8.240e+10 1st Qu.: 6.591e+10
## Median :2.144e+11 Median : 1.032e+11 Median : 8.253e+10
## Mean :2.633e+11 Mean : 7.574e+10 Mean : 5.240e+10
## 3rd Qu.:3.659e+11 3rd Qu.: 1.774e+11 3rd Qu.: 1.435e+11
## Max. :4.129e+11 Max. : 4.320e+11 Max. : 3.901e+11
## roa roe de_ratio bien_loi_nhuan_rong
## Min. :-0.24416 Min. :-0.24594 Min. :0.007264 Min. :-3.03441
## 1st Qu.: 0.03309 1st Qu.: 0.03509 1st Qu.:0.037061 1st Qu.: 0.32911
## Median : 0.03783 Median : 0.04159 Median :0.091051 Median : 0.36368
## Mean : 0.01006 Mean : 0.01490 Mean :0.118506 Mean :-0.05279
## 3rd Qu.: 0.04641 3rd Qu.: 0.05778 3rd Qu.:0.121892 3rd Qu.: 0.40324
## Max. : 0.14242 Max. : 0.15917 Max. :0.421478 Max. : 0.98975
## loi_nhuan_status
## Length:10
## Class :character
## Mode :character
##
##
##
Giải thích:
Câu lệnh #1 library(scales): Tải thư viện scales, thư viện này chứa các hàm tiện ích để định dạng số (ví dụ: thêm dấu phẩy, thêm ký hiệu %).
Câu lệnh #2 summary(agr_analysis): Hàm summary() (R cơ bản) tính toán các giá trị thống kê tóm tắt cho 12 cột trong agr_analysis.
Kết quả:
Đối với 11 cột số (ví dụ: tong_tai_san, roa): Tính Tối thiểu (Min), Tối đa (Max), Trung vị (Median), Trung bình (Mean), và Tứ phân vị (1st Qu., 3rd Qu.).
Đối với 1 cột ký tự (loi_nhuan_status): Báo cáo thông tin cơ bản (Số lượng, Lớp, Kiểu).
Nhận xét: Thao tác này giúp nhanh chóng nắm bắt được phạm vi (Min/Max) và xu hướng trung tâm (Mean/Median) của 12 chỉ tiêu tài chính cốt lõi trong 10 năm qua.
#2.3.2. Thống kê Khả năng sinh lời (lợi nhuận, ROA, ROE).
#2.3.2.1. Thống kê xu hướng trung tâm (lợi nhuận sau thuế).
Tính toán và trình bày riêng lẻ các chỉ số trung tâm cho các biến quan trọng nhất.
# Thao tác 3: Tính Trung bình & Trung vị (Lợi nhuận sau thuế)
mean_lnst <- mean(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 1
median_lnst <- median(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 2
print(paste("Lợi nhuận sau thuế TRUNG BÌNH (10 năm):", scales::comma(mean_lnst, accuracy = 1))) # 3
## [1] "Lợi nhuận sau thuế TRUNG BÌNH (10 năm): 52,397,092,931"
print(paste("Lợi nhuận sau thuế TRUNG VỊ (10 năm):", scales::comma(median_lnst, accuracy = 1))) # 4
## [1] "Lợi nhuận sau thuế TRUNG VỊ (10 năm): 82,525,087,441"
Giải thích:
Câu lệnh #1 mean_lnst <- mean(…, na.rm = TRUE): Tính giá trị trung bình (mean) của cột loi_nhuan_sau_thue (truy cập bằng $).
na.rm = TRUE: Tham số an toàn để loại bỏ (rm) bất kỳ giá trị NA nào (nếu có) trước khi tính toán.
Câu lệnh #2 median_lnst <- median(…, na.rm = TRUE): Tính giá trị trung vị (median - giá trị đứng giữa) của cột.
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In kết quả ra màn hình.
scales::comma(…, accuracy = 1): Sử dụng hàm comma() từ gói scales để định dạng con số (ví dụ: 123456789.0) thành dạng dễ đọc (ví dụ: 123,456,789.0), giúp đọc kết quả dễ dàng hơn.
Nhận xét: Hai chỉ số này cho thấy xu hướng trung tâm của lợi nhuận. Trung vị (Median) thường đáng tin cậy hơn Trung bình (Mean) nếu có các năm lỗ nặng hoặc lãi đột biến (outliers).
#2.3.2.2. Thống kê phạm vi (lợi nhuận sau thuế).
Thao tác này cho thấy năm lãi cao nhất và năm lỗ nặng nhất trong 10 năm.
# Thao tác 4: Tính Min & Max (Lợi nhuận sau thuế)
min_lnst <- min(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 1
max_lnst <- max(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 2
print(paste("Lợi nhuận sau thuế THẤP NHẤT (Lỗ):", scales::comma(min_lnst, accuracy = 1))) # 3
## [1] "Lợi nhuận sau thuế THẤP NHẤT (Lỗ): -405,397,932,938"
print(paste("Lợi nhuận sau thuế CAO NHẤT (Lãi):", scales::comma(max_lnst, accuracy = 1))) # 4
## [1] "Lợi nhuận sau thuế CAO NHẤT (Lãi): 390,130,202,453"
Giải thích:
Câu lệnh #1 min_lnst <- min(…, na.rm = TRUE): Tìm giá trị nhỏ nhất (min) của cột loi_nhuan_sau_thue (tương ứng năm lỗ nặng nhất).
Câu lệnh #2 max_lnst <- max(…, na.rm = TRUE): Tìm giá trị lớn nhất (max) của cột loi_nhuan_sau_thue (tương ứng năm lãi cao nhất).
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In các giá trị Min, Max đã tìm được ra màn hình, có định dạng scales::comma cho dễ đọc.
Nhận xét: Thao tác này cho thấy phạm vi (range) biến động lợi nhuận của công ty.
#2.3.2.3. Thống kê xu hướng trung tâm (ROA).
Thực hiện tương tự cho chỉ số ROA (Tỷ suất lợi nhuận trên tài sản).
# Thao tác 5: Tính Trung bình & Trung vị (ROA)
mean_roa <- mean(agr_analysis$roa, na.rm = TRUE) # 1
median_roa <- median(agr_analysis$roa, na.rm = TRUE) # 2
print(paste("ROA TRUNG BÌNH (10 năm):", scales::percent(mean_roa, accuracy = 0.1))) # 3
## [1] "ROA TRUNG BÌNH (10 năm): 1.0%"
print(paste("ROA TRUNG VỊ (10 năm):", scales::percent(median_roa, accuracy = 0.1))) # 4
## [1] "ROA TRUNG VỊ (10 năm): 3.8%"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị cho cột roa.
Câu lệnh #3 & #4 print(paste(…, scales::percent(…))): In kết quả ra màn hình.
scales::percent(…, accuracy = 0.1): Sử dụng hàm percent() từ gói scales để định dạng tỷ lệ ROA (ví dụ: 0.053) thành dạng phần trăm dễ đọc (ví dụ: 5.3%), với 1 chữ số thập phân.
#2.3.2.4. Thống kê xu hướng trung tâm (ROE).
Thực hiện tương tự cho chỉ số ROE (Tỷ suất lợi nhuận trên vốn chủ sở hữu).
# Thao tác 6 (Phần 3): Tính Trung bình & Trung vị (ROE)
mean_roe <- mean(agr_analysis$roe, na.rm = TRUE) # 1
median_roe <- median(agr_analysis$roe, na.rm = TRUE) # 2
print(paste("ROE TRUNG BÌNH (10 năm):", scales::percent(mean_roe, accuracy = 0.1))) # 3
## [1] "ROE TRUNG BÌNH (10 năm): 1.5%"
print(paste("ROE TRUNG VỊ (10 năm):", scales::percent(median_roe, accuracy = 0.1))) # 4
## [1] "ROE TRUNG VỊ (10 năm): 4.2%"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị cho cột roe.
Câu lệnh #3 & #4 print(paste(…, scales::percent(…))): In kết quả ra màn hình, sử dụng scales::percent() để định dạng % (ví dụ: 10.1%).
Nhận xét: Các chỉ số này tóm tắt hiệu quả sử dụng vốn chủ sở hữu trung bình trong 10 năm.
#2.3.3. Thống kê quy mô và cấu trúc vốn.
#2.3.3.1. Thống kê xu hướng trung tâm (tổng tài sản).
Thao tác này cho biết quy mô tài sản trung bình của AGR trong 10 năm.
# Thao tác 7: Tính trung bình & trung vị (tổng tài sản)
mean_tts <- mean(agr_analysis$tong_tai_san, na.rm = TRUE) # 1
median_tts <- median(agr_analysis$tong_tai_san, na.rm = TRUE) # 2
print(paste("Tổng tài sản TRUNG BÌNH (10 năm):", scales::comma(mean_tts, accuracy = 1))) # 3
## [1] "Tổng tài sản TRUNG BÌNH (10 năm): 2,407,331,036,071"
print(paste("Tổng tài sản TRUNG VỊ (10 năm):", scales::comma(median_tts, accuracy = 1))) # 4
## [1] "Tổng tài sản TRUNG VỊ (10 năm): 2,264,296,169,830"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị cho cột tong_tai_san.
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In kết quả ra màn hình, sử dụng scales::comma() để định dạng các con số hàng nghìn tỷ cho dễ đọc.
Nhận xét: Hai chỉ số này cho thấy quy mô tài sản trung tâm của công ty trong thập kỷ qua.
#2.3.3.2. Thống kê phạm vi (tổng tài sản).
Thao tác này cho thấy sự biến động về quy mô tài sản (năm thấp nhất và cao nhất).
# Thao tác 8: Tính Min & Max (tổng tài sản)
min_tts <- min(agr_analysis$tong_tai_san, na.rm = TRUE) # 1
max_tts <- max(agr_analysis$tong_tai_san, na.rm = TRUE) # 2
print(paste("Tổng tài sản THẤP NHẤT:", scales::comma(min_tts, accuracy = 1))) # 3
## [1] "Tổng tài sản THẤP NHẤT: 1,660,379,497,164"
print(paste("Tổng tài sản CAO NHẤT:", scales::comma(max_tts, accuracy = 1))) # 4
## [1] "Tổng tài sản CAO NHẤT: 3,472,227,107,539"
Giải thích:
Câu lệnh #1 min_tts <- min(…): Tìm giá trị nhỏ nhất (min) của cột tong_tai_san.
Câu lệnh #2: max_tts <- max(…): Tìm giá trị lớn nhất (max) của cột tong_tai_san.
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In các giá trị Min, Max đã tìm được ra màn hình, có định dạng scales::comma cho dễ đọc.
Nhận xét: Thao tác này cho thấy phạm vi (range) tăng trưởng về quy mô tài sản của công ty.
#2.3.3.3. Thống kê xu hướng trung tâm (vốn chủ sở hữu).
Xem xét quy mô Vốn chủ sở hữu (VCSH) trung bình.
# Thao tác 9: Tính trung bình & trung vị (vốn chủ sở hữu)
mean_vcsh <- mean(agr_analysis$von_chu_so_huu, na.rm = TRUE) # 1
median_vcsh <- median(agr_analysis$von_chu_so_huu, na.rm = TRUE) # 2
print(paste("Vốn chủ sở hữu TRUNG BÌNH (10 năm):", scales::comma(mean_vcsh, accuracy = 1))) # 3
## [1] "Vốn chủ sở hữu TRUNG BÌNH (10 năm): 2,128,589,193,084"
print(paste("Vốn chủ sở hữu TRUNG VỊ (10 năm):", scales::comma(median_vcsh, accuracy = 1))) # 4
## [1] "Vốn chủ sở hữu TRUNG VỊ (10 năm): 2,075,346,025,272"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị của cột von_chu_so_huu (tên đã được janitor làm sạch).
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In kết quả ra màn hình, sử dụng scales::comma() để định dạng.
Nhận xét: Hai chỉ số này cho thấy quy mô vốn chủ sở hữu trung tâm của công ty.
#2.3.4. Thống kê đòn bẩy và hiệu quả hoạt động.
#2.3.4.1. Thống kê xu hướng trung tâm (Tỷ số nợ trên vốn chủ sở hữu - D/E). Phân tích chỉ số đòn bẩy de_ratio (Nợ/Vốn chủ sở hữu) đã tạo.
# Thao tác 10: Tính trung bình & trung vị (DE Ratio)
mean_de <- mean(agr_analysis$de_ratio, na.rm = TRUE) # 1
median_de <- median(agr_analysis$de_ratio, na.rm = TRUE) # 2
print(paste("Tỷ số D/E TRUNG BÌNH (10 năm):", scales::number(mean_de, accuracy = 0.01))) # 3
## [1] "Tỷ số D/E TRUNG BÌNH (10 năm): 0.12"
print(paste("Tỷ số D/E TRUNG VỊ (10 năm):", scales::number(median_de, accuracy = 0.01))) # 4
## [1] "Tỷ số D/E TRUNG VỊ (10 năm): 0.09"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị cho cột de_ratio.
Câu lệnh #3 & #4: print(paste(…, scales::number(…))): In kết quả ra màn hình.
scales::number(…, accuracy = 0.01): Sử dụng hàm number() từ gói scales để định dạng tỷ lệ này thành số thập phân (lấy 2 chữ số), giúp đọc tỷ lệ dễ dàng (ví dụ: 1.20 nghĩa là 1 đồng vốn chủ sở hữu gánh 1.2 đồng nợ).
Nhận xét: Các chỉ số này cho thấy mức độ sử dụng đòn bẩy tài chính (nợ) trung bình của công ty.
#2.3.4.2. Thống kê xu hướng trung tâm (Biên lợi nhuận ròng). Phân tích chỉ số bien_loi_nhuan_rong để xem trung bình 100 đồng doanh thu thì thu về bao nhiêu đồng lợi nhuận.
# Thao tác 11: Tính trung bình & trung vị (biên lợi nhuận ròng)
mean_margin <- mean(agr_analysis$bien_loi_nhuan_rong, na.rm = TRUE) # 1
median_margin <- median(agr_analysis$bien_loi_nhuan_rong, na.rm = TRUE) # 2
print(paste("Biên lợi nhuận ròng TRUNG BÌNH (10 năm):", scales::percent(mean_margin, accuracy = 0.1))) # 3
## [1] "Biên lợi nhuận ròng TRUNG BÌNH (10 năm): -5.3%"
print(paste("Biên lợi nhuận ròng TRUNG VỊ (10 năm):", scales::percent(median_margin, accuracy = 0.1))) # 4
## [1] "Biên lợi nhuận ròng TRUNG VỊ (10 năm): 36.4%"
Giải thích:
Câu lệnh #1 & #2 mean(…) và median(…): Tính giá trị trung bình và trung vị của cột bien_loi_nhuan_rong.
Câu lệnh #3 & #4 print(paste(…, scales::percent(…))): In kết quả ra màn hình, sử dụng scales::percent() để định dạng % (ví dụ: 25.5%).
Nhận xét: Các chỉ số này cho thấy hiệu quả chuyển đổi doanh thu thành lợi nhuận trung bình của công ty.
#2.3.4.3. Thống kê xu hướng trung tâm (doanh thu). Xem xét quy mô doanh thu trung bình của công ty.
# Thao tác 12 : Tính trung bình & trung vị (Doanh thu)
mean_dt <- mean(agr_analysis$doanh_thu, na.rm = TRUE) # 1
median_dt <- median(agr_analysis$doanh_thu, na.rm = TRUE) # 2
print(paste("Doanh thu TRUNG BÌNH (10 năm):", scales::comma(mean_dt, accuracy = 1))) # 3
## [1] "Doanh thu TRUNG BÌNH (10 năm): 263,314,866,896"
print(paste("Doanh thu TRUNG VỊ (10 năm):", scales::comma(median_dt, accuracy = 1))) # 4
## [1] "Doanh thu TRUNG VỊ (10 năm): 214,438,417,887"
Giải thích:
Câu lệnh #1 mean(agr_analysis$doanh_thu, na.rm = TRUE): Tính doanh thu trung bình (sử dụng cột doanh_thu đã đổi tên).
Câu lệnh #2 median(agr_analysis$doanh_thu, na.rm = TRUE): Tính doanh thu trung vị.
Câu lệnh #3 & #4 print(paste(…, scales::comma(…))): In kết quả ra màn hình, dùng scales::comma() để định dạng số lớn cho dễ đọc.
#2.3.4.4. Thống kê phạm vi (doanh thu). Thao tác này cho thấy doanh thu năm cao nhất và năm thấp nhất.
# Thao tác 13: Tính Min & Max (Doanh thu)
min_dt <- min(agr_analysis$doanh_thu, na.rm = TRUE) # 1
max_dt <- max(agr_analysis$doanh_thu, na.rm = TRUE) # 2
print(paste("Doanh thu THẤP NHẤT:", scales::comma(min_dt, accuracy = 1))) # 3
## [1] "Doanh thu THẤP NHẤT: 133,600,086,761"
print(paste("Doanh thu CAO NHẤT:", scales::comma(max_dt, accuracy = 1))) # 4
## [1] "Doanh thu CAO NHẤT: 412,896,892,995"
Giải thích:
Câu lệnh #1 min(agr_analysis$doanh_thu, na.rm = TRUE): Tìm giá trị nhỏ nhất (min) của doanh_thu.
Câu lệnh #2 max(agr_analysis$doanh_thu, na.rm = TRUE): Tìm giá trị lớn nhất (max) của doanh_thu.
Câu lệnh #3 & #4: print(paste(…, scales::comma(…))): In kết quả Min, Max ra màn hình, có định dạng scales::comma.
#2.3.5. Thống kê tần suất và tương quan. #2.3.5.1. Thống kê bảng tần suất (Lãi/Lỗ). Thực hiện lại thao tác 8 (phần 2.2) một cách chính thức, và bổ sung thêm tính toán tỷ lệ phần trăm (%). Thao tác này sử dụng hàm sprintf() của R cơ bản để định dạng %, thay vì dùng gói scales.
# Thao tác 14: Bảng tần suất (Frequency Table)
status_table <- table(agr_analysis$loi_nhuan_status) # 1
status_prop_table <- prop.table(status_table) # 2
status_percent_formatted <- sprintf("%.1f%%", status_prop_table * 100) # 3
names(status_percent_formatted) <- names(status_table) # 4
print("Bảng tần suất Số năm Lãi/Lỗ:") # 5
## [1] "Bảng tần suất Số năm Lãi/Lỗ:"
print(status_table)
##
## Lãi Lỗ
## 8 2
print("Bảng tỷ lệ phần trăm (%):")
## [1] "Bảng tỷ lệ phần trăm (%):"
print(status_percent_formatted)
## Lãi Lỗ
## "80.0%" "20.0%"
Giải thích:
Câu lệnh #1 status_table <- table(…): Dùng hàm table() (R cơ bản) để đếm số năm thuộc mỗi nhóm loi_nhuan_status (Lãi, Lỗ).
Câu lệnh #2 status_prop_table <- prop.table(status_table): Dùng hàm prop.table() để tính tỷ lệ (proportion) cho bảng tần suất vừa tạo (ví dụ: Lãi = 0.8, Lỗ = 0.2).
Câu lệnh #3 status_percent_formatted <- sprintf(“%.1f%%”, status_prop_table * 100): Dùng hàm sprintf() (R cơ bản) để định dạng chuỗi.
“%.1f%%”: Cú pháp này có nghĩa là: “Định dạng một con số (f) với 1 chữ số sau dấu phẩy (.1f), sau đó in ra ký tự % (%%)”.
… * 100: Nhân tỷ lệ (0.8) với 100 để có số phần trăm (80.0).
Câu lệnh #4 names(status_percent_formatted) <- names(status_table): Gán lại tên (“Lãi”, “Lỗ”) cho vector (vì sprintf làm mất tên).
Câu lệnh #5: print(…): In cả hai bảng (số lượng đếm và tỷ lệ %) ra màn hình.
#2.3.5.2. Thống kê tương quan (Correlation) - Các chỉ số sinh lời. Tính toán hệ số tương quan Pearson (từ -1 đến 1) để đo lường mối quan hệ tuyến tính giữa các chỉ số sinh lời.
# Thao tác 15: Tương quan giữa ROA, ROE, và Biên Lợi Nhuận
cor_roa_roe <- cor(agr_analysis$roa, agr_analysis$roe, use = "complete.obs") # 1
cor_roa_margin <- cor(agr_analysis$roa, agr_analysis$bien_loi_nhuan_rong, use = "complete.obs") # 2
print(paste("Hệ số tương quan giữa ROA và ROE:", sprintf("%.2f", cor_roa_roe))) # 3
## [1] "Hệ số tương quan giữa ROA và ROE: 1.00"
print(paste("Hệ số tương quan giữa ROA và Biên Lợi Nhuận:", sprintf("%.2f", cor_roa_margin))) # 4
## [1] "Hệ số tương quan giữa ROA và Biên Lợi Nhuận: 0.99"
Giải thích:
Câu lệnh #1 & #2 cor(…, use = “complete.obs”): Dùng hàm cor() (R cơ bản) để tính hệ số tương quan Pearson.
use = “complete.obs”: Tham số an toàn, chỉ định R chỉ sử dụng các hàng (quan sát) không có giá trị NA ở cả hai cột đang so sánh.
Câu lệnh #3 & #4 print(paste(…, sprintf(“%.2f”, …))): In kết quả ra màn hình.
#2.5.3.3. Thống kê tương quan - Lợi nhuận vs Quy mô. Kiểm tra xem lợi nhuận có xu hướng tăng cùng với quy mô tổng tài sản hay không.
# Thao tác 16: Tương quan giữa Lợi nhuận và Tổng tài sản
cor_ln_tts <- cor(agr_analysis$loi_nhuan_sau_thue, agr_analysis$tong_tai_san, use = "complete.obs") # 1
print(paste("Hệ số tương quan giữa Lợi nhuận sau thuế và Tổng tài sản:", sprintf("%.2f", cor_ln_tts))) # 2
## [1] "Hệ số tương quan giữa Lợi nhuận sau thuế và Tổng tài sản: 0.58"
Giải thích:
Câu lệnh #1 cor(…): Tính hệ số tương quan giữa loi_nhuan_sau_thue và tong_tai_san.
Câu lệnh #2: print(paste(…, sprintf(“%.2f”, …))): In kết quả đã định dạng (lấy 2 chữ số thập phân) ra màn hình.
#2.3.6. Thống kê theo nhóm và Thống kê Đặc biệt. #2.3.6.1. Thống kê
tương quan - Lợi nhuận vs Đòn bẩy (D/E). Kiểm tra giả thuyết: Liệu việc
sử dụng đòn bẩy cao (nợ nhiều) có liên quan đến lợi nhuận cao hơn (hoặc
thấp hơn) không?
# Thao tác 17: Tương quan giữa Lợi nhuận và Tỷ số D/E
cor_ln_de <- cor(agr_analysis$loi_nhuan_sau_thue, agr_analysis$de_ratio, use = "complete.obs") # 1
print(paste("Hệ số tương quan giữa Lợi nhuận sau thuế và Tỷ số D/E:", sprintf("%.2f", cor_ln_de))) # 2
## [1] "Hệ số tương quan giữa Lợi nhuận sau thuế và Tỷ số D/E: 0.37"
Giải thích:
Câu lệnh #1 cor(agr_analysis\(loi_nhuan_sau_thue, agr_analysis\)de_ratio, …): Tính hệ số tương quan giữa lợi nhuận và tỷ số nợ trên vốn chủ sở hữu (de_ratio).
Câu lệnh #2 print(paste(…, sprintf(“%.2f”, …))): In kết quả đã định dạng ra màn hình.
#2.3.6.2. Bảng thống kê tóm tắt theo nhóm (Lãi vs Lỗ). Đây là một thao tác thống kê mạnh mẽ. Nó nhóm 10 năm dữ liệu thành 2 nhóm (“Lãi” và “Lỗ”) và tính toán các chỉ số trung bình cho từng nhóm. Điều này giúp trả lời câu hỏi: “Các năm ‘Lãi’ thì khác gì các năm ‘Lỗ’ về mặt chỉ số (ROE, D/E, Doanh thu)?”
# Thao tác 18: Thống kê tóm tắt theo nhóm (Lãi/Lỗ)
summary_by_status <- agr_analysis %>% # 1
group_by(loi_nhuan_status) %>% # 2
summarise( # 3
So_nam = n(), # 4
ROE_Trung_binh = mean(roe[is.finite(roe)], na.rm = TRUE), # 5
DE_Ratio_Trung_binh = mean(de_ratio[is.finite(de_ratio)], na.rm = TRUE), # 6
Doanh_thu_Trung_binh = mean(doanh_thu[is.finite(doanh_thu)], na.rm = TRUE) # 7
)
print("Bảng tóm tắt các chỉ số trung bình theo trạng thái Lãi/Lỗ:")
## [1] "Bảng tóm tắt các chỉ số trung bình theo trạng thái Lãi/Lỗ:"
print(summary_by_status) # 8
## # A tibble: 2 × 5
## loi_nhuan_status So_nam ROE_Trung_binh DE_Ratio_Trung_binh
## <chr> <int> <dbl> <dbl>
## 1 Lãi 8 0.0607 0.136
## 2 Lỗ 2 -0.168 0.0494
## # ℹ 1 more variable: Doanh_thu_Trung_binh <dbl>
Giải thích:
Câu lệnh #1 & #2 … %>% group_by(loi_nhuan_status): Nhóm 10 hàng (năm) thành 2 nhóm (Lãi, Lỗ) dựa trên cột loi_nhuan_status.
Câu lệnh #3: summarise(…): Tính toán các giá trị tóm tắt cho mỗi nhóm.
Câu lệnh #4: So_nam = n(): Đếm (n()) số năm trong mỗi nhóm.
Câu lệnh #5, #6, #7: … = mean(…[is.finite(…)], na.rm = TRUE): Đây là một kỹ thuật xử lý lỗi quan trọng.
is.finite(…): Hàm này kiểm tra và chỉ giữ lại các số hữu hạn (finite), tức là nó sẽ loại bỏ cả NA và Inf (Vô cực - Infinity). Nếu von_chu_so_huu bằng 0 trong một năm nào đó, phép tính ROE (Lợi nhuận / Vốn chủ) ở Phần 2.2 sẽ tạo ra giá trị Inf. Hàm is.finite() ngăn ngừa các giá trị Inf này làm hỏng phép tính mean().
Câu lệnh #8 print(summary_by_status): In bảng tóm tắt theo nhóm (Lãi/Lỗ) ra màn hình.
#2.3.6.3. Xác định năm có lợi nhuận cao nhất (tốt nhất). Tìm ra năm cụ thể có lợi nhuận sau thuế cao nhất.
# Thao tác 19: Tìm năm có Lợi nhuận sau thuế CAO NHẤT
library(tidyverse) # 1
max_profit <- max(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 2
best_year_data <- agr_analysis %>% # 3
filter(loi_nhuan_sau_thue == max_profit) # 4
print("Năm tài chính có lợi nhuận CAO NHẤT:")
## [1] "Năm tài chính có lợi nhuận CAO NHẤT:"
print(best_year_data) # 5
## # A tibble: 1 × 12
## nam tong_tai_san no_phai_tra von_chu_so_huu doanh_thu loi_nhuan_truoc_thue
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2021 2739269214518 288227817971 2451041396547 3.94e11 431972320134
## # ℹ 6 more variables: loi_nhuan_sau_thue <dbl>, roa <dbl>, roe <dbl>,
## # de_ratio <dbl>, bien_loi_nhuan_rong <dbl>, loi_nhuan_status <chr>
Giải thích:
Câu lệnh #1 library(tidyverse): Được gọi lại như một biện pháp “an toàn” để đảm bảo các hàm dplyr như filter() và %>% hoạt động chính xác (đặc biệt khi chạy từng chunk riêng lẻ).
Câu lệnh #2 max_profit <- max(…): Tìm giá trị lợi nhuận lớn nhất (max) trong cột loi_nhuan_sau_thue và lưu vào biến max_profit.
Câu lệnh #3 & #4 best_year_data <- agr_analysis %>% filter(…): Sử dụng hàm filter() của dplyr để “lọc” bảng agr_analysis. filter(loi_nhuan_sau_thue == max_profit) chỉ giữ lại (các) dòng nào có giá trị loi_nhuan_sau_thue bằng (==) đúng với giá trị max_profit đã tìm.
Câu lệnh #5 print(best_year_data): In ra toàn bộ thông tin của năm tốt nhất..
#2.3.6.4. Xác định năm có lợi nhuận thấp nhất (kém nhất).
Tương tự, tìm ra năm có lợi nhuận sau thuế thấp nhất (tức là năm lỗ nặng nhất).
# Thao tác 20: Tìm năm có Lợi nhuận sau thuế THẤP NHẤT
library(tidyverse) # 1
min_profit <- min(agr_analysis$loi_nhuan_sau_thue, na.rm = TRUE) # 2
worst_year_data <- agr_analysis %>% # 3
filter(loi_nhuan_sau_thue == min_profit) # 4
print("Năm tài chính có lợi nhuận THẤP NHẤT (Lỗ nặng nhất):")
## [1] "Năm tài chính có lợi nhuận THẤP NHẤT (Lỗ nặng nhất):"
print(worst_year_data) # 5
## # A tibble: 1 × 12
## nam tong_tai_san no_phai_tra von_chu_so_huu doanh_thu loi_nhuan_truoc_thue
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2016 1660379497164 12044370105 1648335127059 1.34e11 -365821398106
## # ℹ 6 more variables: loi_nhuan_sau_thue <dbl>, roa <dbl>, roe <dbl>,
## # de_ratio <dbl>, bien_loi_nhuan_rong <dbl>, loi_nhuan_status <chr>
Giải thích:
Câu lệnh #1 library(tidyverse): Đảm bảo các hàm dplyr có sẵn.
Câu lệnh #2 min_profit <- min(…): Tìm giá trị lợi nhuận nhỏ nhất (min) (sẽ là một số âm) và lưu vào biến min_profit.
Câu lệnh #3 & #4 worst_year_data <- agr_analysis %>% filter(…): Sử dụng filter() của dplyr. filter(loi_nhuan_sau_thue == min_profit) chỉ giữ lại (các) dòng nào có giá trị loi_nhuan_sau_thue bằng (==) đúng với giá trị min_profit (năm lỗ nặng nhất).
Câu lệnh #5: print(worst_year_data): In ra toàn bộ thông tin của năm kém nhất.
#2.4. Trực quan hóa dữ liệu
Mục tiêu: Sau khi đã thực hiện các phân tích thống kê chi tiết và thu được các số liệu ở phần 2.3, giai đoạn này sẽ sử dụng thư viện ggplot2 (thuộc tidyverse) để trực quan hóa các kết quả và xu hướng. Trực quan hóa giúp biến các con số thống kê thành các biểu đồ có ý nghĩa, làm nổi bật các xu hướng theo thời gian, các mối quan-hệ và sự khác biệt giữa các nhóm.
Tất cả các biểu đồ sẽ được vẽ từ bộ dữ liệu đã chọn lọc agr_analysis (chỉ chứa 12 biến cốt lõi). Phần này sẽ bao gồm 20 biểu đồ, được chia thành các nhóm chính:
Xu hướng theo thời gian (Time Series).
Tần suất và Phân loại (Frequency).
Mối quan hệ (Scatter Plots).
Phân phối (Distribution).
So sánh theo nhóm (Grouped Comparisons).
#2.4.1. Trực quan hóa xu hướng theo Thời gian (Time Series).
Nhóm biểu đồ đầu tiên này sử dụng biểu đồ đường (geom_line) để theo dõi sự biến động của các chỉ số tài chính quan trọng qua 10 năm (2015-2024).
#2.4.1.1. Đồ thị 1: Xu hướng Lợi nhuận sau thuế (2015-2024). Đây là biểu đồ quan trọng nhất, cho thấy hiệu suất sinh lời ròng của AGR qua từng năm.
# Đồ thị 1: Xu hướng Lợi nhuận sau thuế
library(tidyverse) #1
ggplot(agr_analysis, aes(x = nam, y = loi_nhuan_sau_thue)) + #2
geom_line(color = "blue", size = 1.2) + #3
geom_point(color = "blue", size = 3) + #4
geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) + #5
labs(title = "Xu hướng Lợi nhuận sau thuế của AGR (2015-2024)", #6
subtitle = "Đường màu đỏ là ngưỡng hòa vốn (Lợi nhuận = 0)",
x = "Năm",
y = "Lợi nhuận sau thuế (VND)") +
theme_minimal() + #7
scale_y_continuous(labels = function(x) { sprintf("%.0f", x) }) + #8
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 9
Giải thích:
Câu lệnh #1: library(tidyverse): Tải thư viện tidyverse, bao gồm ggplot2 để vẽ biểu đồ và dplyr để xử lý dữ liệu.
Câu lệnh #2 ggplot(agr_analysis, aes(x = nam, y = loi_nhuan_sau_thue)) +: Khởi tạo một đối tượng ggplot. agr_analysis là data frame nguồn.
aes(x = nam, y = loi_nhuan_sau_thue): “Ánh xạ” (aes - aesthetics) cột nam vào trục x và cột loi_nhuan_sau_thue vào trục y. Dấu + dùng để “cộng” thêm các lớp (layers) tiếp theo.
Câu lệnh #3: geom_line(…) +: Thêm lớp hình học đường kẻ (geom_line) để nối các điểm dữ liệu, thể hiện xu hướng.
Câu lệnh #4: geom_point(…) +: Thêm lớp hình học điểm (geom_point) để đánh dấu giá trị cụ thể của từng năm.
Câu lệnh #5 geom_hline(…) +: Thêm một lớp đường kẻ ngang (hline - horizontal line) tham chiếu.
yintercept = 0: Đặt đường kẻ này ngay tại giá trị y = 0. Đây là ranh giới Lãi/Lỗ, giúp người xem nhận diện ngay lập tức các năm kinh doanh không hiệu quả (nằm dưới vạch đỏ).
Câu lệnh #6: labs(…) +: Tùy chỉnh các nhãn (labels) và tiêu đề của biểu đồ, giúp biểu đồ dễ hiểu hơn.
Câu lệnh #7: theme_minimal() +: Áp dụng một “giao diện” (theme) sạch sẽ, loại bỏ nền xám mặc định.
Câu lệnh #8 scale_y_continuous(labels = function(x) { sprintf(“%.0f”, x) }) +: Tùy chỉnh thang đo trục y. Thay vì dùng scales::comma (có thể gây lỗi trong một số môi trường R Markdown), một hàm tùy chỉnh sử dụng sprintf (R cơ bản) được dùng.
function(x) { sprintf(“%.0f”, x) }: Hàm này định dạng số (f) không có phần thập phân (.0), giúp hiển thị các số lớn (ví dụ: 200000000000) thay vì ký hiệu khoa học (ví dụ: 2e+11).
Câu lệnh #9 scale_x_continuous(breaks = seq(min(agr_analysis\(nam), max(agr_analysis\)nam), by = 1)): Tùy chỉnh thang đo trục x. Lệnh này buộc ggplot phải hiển thị nhãn cho tất cả các năm (từ năm nhỏ nhất đến lớn nhất, bước nhảy by = 1), giúp trục thời gian rõ ràng, không bị thiếu năm.
#2.4.1.2. Đồ thị 2: Xu hướng tỷ suất lợi nhuận trên tài sản (ROA).
Thao tác này vẽ xu hướng của chỉ số ROA (đã tạo ở 2.2.4) để xem hiệu quả sử dụng tài sản của AGR thay đổi thế nào.
# Đồ thị 2: Xu hướng Tỷ suất lợi nhuận trên tài sản (ROA).
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { #2
paste(sprintf("%.1f", x * 100), "%")
}
ggplot(agr_analysis, aes(x = nam, y = roa)) + #3
geom_line(color = "darkgreen", size = 1.2) + #4
geom_point(color = "darkgreen", size = 3) + #5
geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) + #6
labs(title = "Xu hướng Tỷ suất lợi nhuận trên tài sản (ROA) (2015-2024)", #7
subtitle = "ROA = Lợi nhuận sau thuế / Tổng tài sản",
x = "Năm",
y = "Tỷ suất ROA (%)") +
theme_minimal() + #8
scale_y_continuous(labels = dinh_dang_phan_tram) + #9
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) #10
Giải thích:
Câu lệnh #2 dinh_dang_phan_tram <- function(x) { … }: Một hàm tùy chỉnh được tạo bằng sprintf (R cơ bản) để thay thế scales::percent(). Hàm này nhận một số thập phân (ví dụ: 0.053), nhân với 100 và định dạng nó thành chuỗi “5.3%”.
Câu lệnh #3: ggplot(agr_analysis, aes(x = nam, y = roa)) +: Khởi tạo ggplot, ánh xạ nam vào x và roa vào y. Câu lệnh #4, #5, #6: Thêm các lớp geom_line, geom_point (tương tự Đồ thị 1) và geom_hline tại mốc 0 để phân biệt Lãi/Lỗ.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề và áp dụng giao diện sạch sẽ.
Câu lệnh #9 scale_y_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm tùy chỉnh dinh_dang_phan_tram (đã tạo ở Câu lệnh #2) để định dạng các nhãn trên trục y, giúp các tỷ lệ ROA dễ đọc hơn (ví dụ: 0.05 -> 5.0%).
Câu lệnh #10: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.1.3. Đồ thị 3: Xu hướng Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE).
Thao tác này vẽ xu hướng của chỉ số ROE (đã tạo ở 2.2.5) để xem hiệu quả sử dụng vốn chủ sở hữu.
# Đồ thị 3: Xu hướng tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE)
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { #2
paste(sprintf("%.1f", x * 100), "%")
}
ggplot(agr_analysis, aes(x = nam, y = roe)) + #3
geom_line(color = "darkred", size = 1.2) + #4
geom_point(color = "darkred", size = 3) + #5
geom_hline(yintercept = 0, linetype = "dashed", color = "black", size = 1) + #6
labs(title = "Xu hướng Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE) (2015-2024)", #7
subtitle = "ROE = Lợi nhuận sau thuế / Vốn chủ sở hữu",
x = "Năm",
y = "Tỷ suất ROE (%)") +
theme_minimal() + #8
scale_y_continuous(labels = dinh_dang_phan_tram) + #9
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) #10
Giải thích:
Câu lệnh #2: Hàm dinh_dang_phan_tram được định nghĩa lại (cho an toàn, dù đã có ở Đồ thị 2) để định dạng % bằng sprintf.
Câu lệnh #3: ggplot(agr_analysis, aes(x = nam, y = roe)) +: Khởi tạo ggplot, ánh xạ nam vào x và roe vào y.
Câu lệnh #4, #5, #6: Thêm các lớp geom_line, geom_point và geom_hline (đường tham chiếu 0%).
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #9: scale_y_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % tùy chỉnh cho trục y.
Câu lệnh #10: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.1.4. Đồ thị 4: Xu hướng Tổng tài sản (quy mô công ty).
Thao tác này sẽ vẽ xu hướng của cột tong_tai_san (đã làm sạch) để cho thấy quy mô của AGR đã tăng trưởng (hay thu hẹp) như thế nào trong 10 năm qua.
# Đồ thị 4: Xu hướng tổng tài sản (2015-2024)
library(tidyverse) #1
dinh_dang_ty_vnd <- function(x) { #2
paste(sprintf("%.0f", x / 1e9), "tỷ")
}
ggplot(agr_analysis, aes(x = nam, y = tong_tai_san)) + #3
geom_line(color = "purple", size = 1.2) + #4
geom_point(color = "purple", size = 3) + #5
geom_smooth(method = "lm", color = "orange", linetype = "dashed", se = FALSE) + #6
labs(title = "Xu hướng Tăng trưởng Tổng tài sản của AGR (2015-2024)", #7
subtitle = "Đường màu cam là đường xu hướng tuyến tính (tăng trưởng)",
x = "Năm",
y = "Tổng tài sản (Tỷ VND)") +
theme_minimal() + #8
scale_y_continuous(labels = dinh_dang_ty_vnd) + #9
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) #10
Giải thích:
Câu lệnh #2 dinh_dang_ty_vnd <- function(x) { … }: Tạo một hàm tùy chỉnh để định dạng trục y. Hàm này nhận giá trị gốc (ví dụ: 2200000000000), chia cho 1 tỷ (x / 1e9), rồi định dạng số nguyên (%.0f) và thêm chuỗi ” tỷ” vào cuối (Kết quả: “2200 tỷ”).
Câu lệnh #3: ggplot(agr_analysis, aes(x = nam, y = tong_tai_san)) +: Khởi tạo ggplot, ánh xạ nam vào x và giá trị tong_tai_san (chưa chia) vào y.
Câu lệnh #4, #5: Thêm các lớp geom_line và geom_point.
Câu lệnh #6 geom_smooth(method = “lm”, …, se = FALSE) +: Thêm một đường xu hướng (hồi quy tuyến tính lm) màu cam để làm nổi bật xu hướng tăng trưởng tổng thể của tài sản.
se = FALSE: Ẩn “dải lỗi” (confidence interval) xung quanh đường xu hướng.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #9 scale_y_continuous(labels = dinh_dang_ty_vnd): Áp dụng hàm tùy chỉnh dinh_dang_ty_vnd (từ Câu lệnh #2) cho trục y. ggplot sẽ tự động truyền các giá trị trục y (số lớn) vào hàm này để định dạng.
Câu lệnh #10: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.1.5. Đồ thị 5: Xu hướng Tỷ số Nợ trên Vốn chủ sở hữu (D/E Ratio).
Thao tác này sẽ vẽ xu hướng của chỉ số de_ratio (mà chúng ta đã tạo). Đây là một chỉ số đòn bẩy tài chính quan trọng, cho thấy mức độ công ty sử dụng nợ so với vốn chủ sở hữu.
# Đồ thị 5: Xu hướng tỷ số nợ trên vốn chủ sở hữu (D/E)
library(tidyverse) #1
dinh_dang_so <- function(x) { #2
sprintf("%.2f", x)
}
agr_analysis %>%
filter(is.finite(de_ratio)) %>% #3
ggplot(aes(x = nam, y = de_ratio)) + #4
geom_line(color = "orange", size = 1.2) + #5
geom_point(color = "orange", size = 3) + #6
geom_hline(yintercept = 1.0, linetype = "dashed", color = "blue", size = 1) + #7
labs(title = "Xu hướng Đòn bẩy tài chính (Tỷ số D/E) (2015-2024)", #8
subtitle = "Đường màu xanh là ngưỡng D/E = 1.0 (Nợ = Vốn chủ)",
x = "Năm",
y = "Tỷ số D/E") +
theme_minimal() + #9
scale_y_continuous(labels = dinh_dang_so) + #10
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) #11
Giải thích:
Câu lệnh #2 dinh_dang_so <- function(x) { … }: Tạo một hàm tùy chỉnh dinh_dang_so (dùng sprintf R cơ bản) để định dạng số, lấy 2 chữ số thập phân (ví dụ: 0.92).
Câu lệnh #3 filter(is.finite(de_ratio)) %>%: Đây là bước tiền xử lý bắt buộc trước khi vẽ.
Vấn đề: Nếu có năm VỐN CHỦ SỞ HỮU bằng 0, phép tính DE_Ratio (Nợ / Vốn chủ) ở phần 2.2 sẽ tạo ra giá trị Inf (Vô cực).
Giải pháp: filter(is.finite(de_ratio)) chỉ giữ lại các hàng có giá trị de_ratio là số hữu hạn (lọc bỏ Inf và NA), giúp ggplot không bị lỗi.
Câu lệnh #4: ggplot(aes(x = nam, y = de_ratio)) +: Khởi tạo ggplot (trên dữ liệu đã lọc), ánh xạ nam vào x và de_ratio vào y.
Câu lệnh #5, #6: Thêm các lớp geom_line và geom_point.
Câu lệnh #7 geom_hline(yintercept = 1.0, …): Thêm một đường tham chiếu ngang tại y = 1.0. Đây là mốc chuẩn quan trọng trong tài chính, thể hiện Nợ (D) bằng Vốn chủ sở hữu (E).
Câu lệnh #8, #9: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #10: scale_y_continuous(labels = dinh_dang_so): Áp dụng hàm định dạng số tùy chỉnh (từ Câu lệnh #2) cho trục y.
Câu lệnh #11: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.2. Trực quan hóa Tần suất và Phân loại.
Sau khi xem xét các xu hướng theo thời gian, bây giờ chúng ta sẽ trực quan hóa tần suất của các biến phân loại. Biểu đồ đầu tiên sẽ là biểu đồ cột cho thấy số năm Lãi và Lỗ. 2.4.2.1. Đồ thị 6: Tần suất số năm Lãi và Lỗ.
Thao tác này sẽ vẽ một biểu đồ cột (Bar Chart) để trực quan hóa kết quả Thao tác 14 (Phần 3), cho thấy 8 năm lãi và 2 năm lỗ một cách trực quan.
# Đồ thị 6: Tần suất số năm Lãi và Lỗ
library(tidyverse) #1
ggplot(agr_analysis, aes(x = loi_nhuan_status, fill = loi_nhuan_status)) + #2
geom_bar(show.legend = FALSE) + #3
geom_text(stat = 'count', aes(label = ..count..), vjust = -0.5, size = 4) + #4
scale_fill_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) + #5
labs(title = "Số lượng năm Đạt Lãi và Bị Lỗ (2015-2024)", #6
subtitle = "Tổng số 10 năm quan sát",
x = "Trạng thái Lợi nhuận",
y = "Số năm") +
theme_minimal() + #7
scale_y_continuous(breaks = seq(0, 10, by = 2), limits = c(0, 10)) #8
Giải thích:
Câu lệnh #1: library(tidyverse): Tải thư viện (bao gồm ggplot2).
Câu lệnh #2 ggplot(agr_analysis, aes(x = loi_nhuan_status, fill = loi_nhuan_status)) +: Khởi tạo ggplot. Ánh xạ (aes) loi_nhuan_status (biến nhóm) vào trục x và vào fill (màu tô) để R tự động tô màu khác nhau cho “Lãi” và “Lỗ”.
Câu lệnh #3 geom_bar(show.legend = FALSE) +: Thêm lớp biểu đồ cột geom_bar(). Chúng ta dùng geom_bar() vì dữ liệu chưa được đếm. geom_bar() sẽ tự động đếm số lượng hàng (năm) cho mỗi nhóm x. (Khác với geom_col() vốn yêu cầu dữ liệu đã đếm sẵn).
show.legend = FALSE: Ẩn chú giải màu sắc vì không cần thiết (trục x đã có thông tin).
Câu lệnh #4 geom_text(stat = ‘count’, aes(label = ..count..), …): Thêm nhãn văn bản (số lượng) lên trên đầu mỗi cột.
stat = ‘count’ và aes(label = ..count..): Đây là một kỹ thuật để lấy số lượng mà geom_bar (ở Câu lệnh #3) vừa đếm và in nó lên trên đầu cột.
vjust = -0.5: Đẩy nhãn lên trên một chút so với đỉnh cột.
Câu lệnh #5 scale_fill_manual(values = …): Tự định nghĩa (manual) màu sắc cho các nhóm. Chúng ta gán “Lãi” = màu xanh (#28a745) và “Lỗ” = màu đỏ (#dc3545) để biểu đồ rõ ràng hơn.
Câu lệnh #6, #7: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #8 scale_y_continuous(…): Tinh chỉnh trục y để đảm bảo nó hiển thị các mốc chẵn (0, 2, 4…) và có giới hạn tối đa là 10.
#2.4.3. Trực quan hóa mối quan hệ (Scatter Plots).
Bây giờ, chúng ta sẽ chuyển sang các biểu đồ tán xạ (Scatter Plot) để xem xét mối quan hệ (tương quan) giữa các cặp biến số với nhau.
#2.4.3.1. Đồ thị 7: Mối quan hệ giữa Tổng tài sản và Lợi nhuận.
Thao tác này sẽ vẽ một biểu đồ tán xạ. Mỗi điểm là một năm. Trục X là Tổng tài sản và Trục Y là Lợi nhuận sau thuế. Chúng ta muốn xem liệu có phải những năm có quy mô tài sản lớn hơn (trục X) thì cũng có lợi nhuận cao hơn (trục Y) hay không.
# Đồ thị 7: Mối quan hệ giữa tổng tài sản và lợi nhuận sau thuế
library(tidyverse) #1
dinh_dang_ty_vnd <- function(x) { #2
paste(sprintf("%.0f", x), "tỷ") # Định dạng số nguyên, thêm " tỷ"
}
ggplot(agr_analysis, aes(x = tong_tai_san / 1e9, y = loi_nhuan_sau_thue / 1e9)) + #3
geom_point(color = "red", size = 3) + #4
geom_smooth(method = "lm", color = "blue", linetype = "dashed", se = FALSE) + #5
geom_text(aes(label = nam), vjust = -1, hjust = 0.5, size = 3) + #6
labs(title = "Mối quan hệ giữa Tổng tài sản và Lợi nhuận sau thuế", #7
subtitle = "Mỗi điểm là một năm (2015-2024)",
x = "Tổng tài sản (Tỷ VND)",
y = "Lợi nhuận sau thuế (Tỷ VND)") +
theme_minimal() + #8
scale_y_continuous(labels = dinh_dang_ty_vnd) + #9
scale_x_continuous(labels = dinh_dang_ty_vnd) #10
Giải thích:
Câu lệnh #2 dinh_dang_ty_vnd <- function(x) { … }: Tạo hàm tùy chỉnh để định dạng trục. Hàm này nhận giá trị (đã chia cho 1 tỷ ở Câu lệnh #3) và thêm hậu tố ” tỷ” (ví dụ: 2200 -> “2200 tỷ”).
Câu lệnh #3 ggplot(agr_analysis, aes(x = tong_tai_san / 1e9, y = loi_nhuan_sau_thue / 1e9)) +: Khởi tạo ggplot. Để các con số trên trục dễ đọc, chúng ta chia cả hai biến cho 1 tỷ (1e9) ngay trong lệnh aes(). Trục x là Tổng tài sản (tỷ VND), y là Lợi nhuận (tỷ VND).
Câu lệnh #4: geom_point(…) +: Thêm lớp điểm (geom_point) (mỗi điểm là 1 năm).
Câu lệnh #5 geom_smooth(method = “lm”, …, se = FALSE) +: Thêm một đường xu hướng tuyến tính (method = “lm”) màu xanh để cho thấy mối quan hệ tổng thể. se = FALSE ẩn dải lỗi.
Câu lệnh #6 geom_text(aes(label = nam), …): Đây là một lớp rất hữu ích. Nó lấy cột nam và in giá trị (ví dụ: “2015”, “2016”…) ngay bên trên mỗi điểm (vjust = -1), giúp xác định từng năm.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #9, #10 scale_y_continuous(labels = dinh_dang_ty_vnd) và scale_x_continuous(labels = dinh_dang_ty_vnd): Áp dụng hàm tùy chỉnh dinh_dang_ty_vnd (từ Câu lệnh #2) cho cả hai trục x và y.
#2.4.3.2. Đồ thị 8: Mối quan hệ giữa ROA và ROE.
Thao tác này sẽ vẽ một biểu đồ tán xạ để xem xét mối quan hệ (tương quan) giữa hai chỉ số sinh lời quan trọng nhất mà chúng ta đã tạo: Tỷ suất lợi nhuận trên tài sản (ROA) và Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE).
Chúng ta kỳ vọng sẽ thấy một mối tương quan thuận mạnh (các điểm đi theo một đường dốc lên), vì cả hai đều được tính toán từ Lợi nhuận sau thuế. Biểu đồ này sẽ trực quan hóa kết quả của Thao tác 15 (Phần 3).
# Đồ thị 8: Mối quan hệ giữa ROA và ROE
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { #2
paste(sprintf("%.1f", x * 100), "%")
}
ggplot(agr_analysis, aes(x = roa, y = roe)) + #3
geom_point(aes(color = loi_nhuan_status), size = 3) + #4
geom_smooth(method = "lm", color = "black", linetype = "dashed", se = FALSE) + #5
geom_text(aes(label = nam), vjust = -1, size = 3) + #6
labs(title = "Mối quan hệ giữa ROA và ROE (2015-2024)", #7
subtitle = "Mỗi điểm là một năm. Màu sắc phân biệt Lãi/Lỗ",
x = "Tỷ suất lợi nhuận trên tài sản (ROA)",
y = "Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE)",
color = "Trạng thái") + # Đổi tên chú giải (legend)
theme_minimal() + #8
scale_y_continuous(labels = dinh_dang_phan_tram) + #9
scale_x_continuous(labels = dinh_dang_phan_tram) + #10
scale_color_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) #11
Giải thích:
Câu lệnh #2: Tạo hàm dinh_dang_phan_tram (dùng sprintf) để định dạng %.
Câu lệnh #3: ggplot(agr_analysis, aes(x = roa, y = roe)) +: Khởi tạo ggplot, ánh xạ roa vào x và roe vào y.
Câu lệnh #4 geom_point(aes(color = loi_nhuan_status), …): Vẽ các điểm (geom_point). Lần này, chúng ta thêm một ánh xạ (aes) bên trong geom_point: color = loi_nhuan_status. Lệnh này yêu cầu ggplot tô màu các điểm dựa trên việc năm đó “Lãi” hay “Lỗ”.
Câu lệnh #5: geom_smooth(method = “lm”, …): Thêm đường xu hướng tuyến tính màu đen.
Câu lệnh #6: geom_text(aes(label = nam), …): Thêm nhãn nam cho từng điểm.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn (lưu ý color = “Trạng thái” để đổi tên chú giải) và áp dụng giao diện.
Câu lệnh #9, #10: scale_…_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % (từ Câu lệnh #2) cho cả hai trục.
Câu lệnh #11 scale_color_manual(values = …): Tự định nghĩa (manual) màu sắc cho ánh xạ color (đã tạo ở Câu lệnh #4), để đồng bộ với Đồ thị 6 (“Lãi” = xanh, “Lỗ” = đỏ).
#2.4.3.3. Đồ thị 9: Mối quan hệ giữa đòn bẩy (D/E Ratio) và Khả năng sinh lời (ROE).
Thao tác này sẽ vẽ một biểu đồ tán xạ để kiểm tra một trong những lý thuyết tài chính quan trọng nhất: “Liệu đòn bẩy tài chính (sử dụng nợ) có khuếch đại khả năng sinh lời (ROE) hay không?”.
Chúng ta sẽ đặt Tỷ số D/E (đòn bẩy) lên trục X và ROE (lợi nhuận) lên trục Y. Chúng ta cũng sẽ lọc bỏ các giá trị Inf (Vô cực) khỏi cả hai cột để đảm bảo biểu đồ vẽ được.
# Đồ thị 9: Mối quan hệ giữa đòn bẩy (D/E) và lợi nhuận (ROE)
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { #2
paste(sprintf("%.1f", x * 100), "%")
}
dinh_dang_so <- function(x) { #3
sprintf("%.2f", x)
}
agr_analysis %>%
filter(is.finite(de_ratio) & is.finite(roe)) %>% #4
ggplot(aes(x = de_ratio, y = roe)) + #5
geom_point(aes(color = loi_nhuan_status), size = 3) + #6
geom_smooth(method = "lm", color = "black", linetype = "dashed", se = FALSE) + #7
geom_text(aes(label = nam), vjust = -1, size = 3) + #8
labs(title = "Mối quan hệ giữa Đòn bẩy (D/E) và Lợi nhuận (ROE)", #9
subtitle = "Mỗi điểm là một năm. Màu sắc phân biệt Lãi/Lỗ",
x = "Tỷ số Đòn bẩy (D/E Ratio)",
y = "Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE)",
color = "Trạng thái") +
theme_minimal() + #10
scale_y_continuous(labels = dinh_dang_phan_tram) + #11
scale_x_continuous(labels = dinh_dang_so) + #12
scale_color_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) #13
Giải thích:
Câu lệnh #2, #3: Tạo các hàm tùy chỉnh dinh_dang_phan_tram (để định dạng %) và dinh_dang_so (để định dạng số thập phân) bằng sprintf.
Câu lệnh #4 filter(is.finite(de_ratio) & is.finite(roe)) %>%: Đây là bước tiền xử lý bắt buộc. Lọc bỏ bất kỳ hàng nào có giá trị Inf (Vô cực) ở một trong hai cột de_ratio HOẶC roe để đảm bảo biểu đồ vẽ được.
Câu lệnh #5: ggplot(aes(x = de_ratio, y = roe)) +: Khởi tạo ggplot (trên dữ liệu đã lọc), ánh xạ de_ratio (đòn bẩy) vào x và roe (lợi nhuận) vào y.
Câu lệnh #6: geom_point(aes(color = loi_nhuan_status), …): Vẽ các điểm và tô màu theo “Lãi” (xanh) hoặc “Lỗ” (đỏ).
Câu lệnh #7: geom_smooth(method = “lm”, …): Thêm đường xu hướng tuyến tính.
Câu lệnh #8: geom_text(aes(label = nam), …): Thêm nhãn nam cho từng điểm.
Câu lệnh #9, #10, #13: labs(…), theme_minimal(), scale_color_manual(…): Tùy chỉnh tiêu đề, giao diện, và màu sắc.
Câu lệnh #11: scale_y_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % (từ Câu lệnh #2) cho trục y.
Câu lệnh #12: scale_x_continuous(labels = dinh_dang_so): Áp dụng hàm định dạng số (từ Câu lệnh #3) cho trục x.
#2.4.4. Trực quan hóa phân phối (Distribution).
Sau khi xem xét các mối quan hệ (scatter plots), bây giờ chúng ta sẽ xem xét sự phân phối của một trong các chỉ số quan trọng.
#2.4.4.1. Đồ thị 10: Phân phối của Biên lợi nhuận ròng.
Thao tác này sẽ vẽ một biểu đồ tần suất (Histogram) cho biến bien_loi_nhuan_rong (mà chúng ta đã tạo).
Mục đích là để xem xét hình dạng (shape) của dữ liệu. Chúng ta muốn trả lời câu hỏi: “Trong 10 năm qua, phần lớn các năm có biên lợi nhuận cao hay thấp? Hay là phân bổ rải rác?”
# Đồ thị 10: Phân phối của Biên lợi nhuận ròng
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { # 2
paste(sprintf("%.0f", x * 100), "%")
}
mean_margin <- mean(agr_analysis$bien_loi_nhuan_rong, na.rm = TRUE) # 3
ggplot(agr_analysis, aes(x = bien_loi_nhuan_rong)) + # 4
geom_histogram(bins = 8, fill = "lightblue", color = "black", alpha = 0.8) + #5
geom_vline(aes(xintercept = mean_margin),
color = "red", linetype = "dashed", size = 1) + #6
geom_text(aes(x = mean_margin, y = 2.5, label = paste("Trung bình:", dinh_dang_phan_tram(mean_margin))),
color = "red", vjust = -0.5, hjust = -0.1) + # 7
labs(title = "Phân phối Tần suất của Biên lợi nhuận ròng (2015-2024)", # 8
subtitle = "Đường màu đỏ là giá trị trung bình",
x = "Biên lợi nhuận ròng (%)",
y = "Số năm (Tần suất)") +
theme_minimal() + #9
scale_x_continuous(labels = dinh_dang_phan_tram) #10
Giải thích:
Câu lệnh #1: library(tidyverse): Tải thư viện (bao gồm ggplot2).
Câu lệnh #2 dinh_dang_phan_tram <- function(x) { … }: Tạo một hàm tùy chỉnh (dùng sprintf R cơ bản) để định dạng các số thành %. Ví dụ, 0.25 -> “25%”.
Câu lệnh #3 mean_margin <- mean(…): Tính giá trị trung bình của bien_loi_nhuan_rong (lưu ý na.rm = TRUE để bỏ qua NA nếu có) và lưu vào biến mean_margin.
Câu lệnh #4 ggplot(agr_analysis, aes(x = bien_loi_nhuan_rong)) +: Khởi tạo ggplot, ánh xạ bien_loi_nhuan_rong vào trục x.
Câu lệnh #5 geom_histogram(bins = 8, …): Vẽ biểu đồ tần suất. Chọn bins = 8 (8 “thùng”) vì chúng ta chỉ có 10 điểm dữ liệu (10 năm), giúp biểu đồ không bị quá thưa.
Câu lệnh #6 geom_vline(aes(xintercept = mean_margin), …): Thêm một đường gạch đứt màu đỏ tại vị trí mean_margin (biên lợi nhuận trung bình).
Câu lệnh #7 geom_text(…): Thêm một nhãn văn bản (“Trung bình: …%”) bên cạnh đường màu đỏ để giải thích ý nghĩa của nó. y = 2.5 và hjust được chọn để định vị nhãn.
Câu lệnh #8, #9: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện sạch sẽ.
Câu lệnh #10 scale_x_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % (từ Câu lệnh #2) cho các nhãn trên trục x.
#2.4.5. Trực quan hóa So sánh theo Nhóm.
Chúng ta đã xem xét xu hướng theo thời gian, tần suất, và các mối quan hệ. Bây giờ, chúng ta sẽ sử dụng các biểu đồ thống kê (như Biểu đồ Hộp) để so sánh sự khác biệt của các chỉ số giữa hai nhóm: 8 năm “Lãi” và 2 năm “Lỗ”.
#2.4.5.1. Đồ thị 11: So sánh ROE giữa các năm Lãi và Lỗ.
Thao tác này sẽ vẽ hai Biểu đồ Hộp (Box Plot) song song. Một biểu đồ cho 8 năm “Lãi” và một biểu đồ cho 2 năm “Lỗ”.
Mục đích là để so sánh sự phân phối (distribution) của chỉ số ROE giữa hai nhóm này. Chúng ta muốn xem: “ROE của các năm Lãi khác biệt như thế nào so với các năm Lỗ?”
Lưu ý: Chúng ta sẽ lọc bỏ các giá trị Inf(Vô cực) khỏi cộtroe trước khi vẽ để đảm bảo biểu đồ hoạt động.
# Đồ thị 11: So sánh ROE giữa năm Lãi và Lỗ (Biểu đồ Hộp)
library(tidyverse) #1
dinh_dang_phan_tram <- function(x) { #2
paste(sprintf("%.1f", x * 100), "%")
}
agr_analysis %>%
filter(is.finite(roe)) %>% #3
ggplot(aes(x = loi_nhuan_status, y = roe, fill = loi_nhuan_status)) + #4
geom_boxplot(show.legend = FALSE) + #5
geom_jitter(width = 0.1, alpha = 0.7, color = "black") + #6
scale_fill_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) + #7
labs(title = "So sánh Phân phối ROE giữa các năm Lãi và Lỗ", #8
subtitle = "Mỗi điểm đen là một năm thực tế",
x = "Trạng thái Lợi nhuận",
y = "Tỷ suất lợi nhuận trên vốn chủ sở hữu (ROE)") +
theme_minimal() + #9
scale_y_continuous(labels = dinh_dang_phan_tram) #10
Giải thích:
Câu lệnh #2: Tạo hàm dinh_dang_phan_tram (dùng sprintf) để định dạng %.
Câu lệnh #3 filter(is.finite(roe)) %>%: Đây là bước tiền xử lý bắt buộc. Lọc bỏ bất kỳ hàng nào có giá trị roe là Inf (Vô cực) hoặc NA trước khi chuyển dữ liệu vào ggplot.
Câu lệnh #4 ggplot(aes(x = loi_nhuan_status, y = roe, fill = loi_nhuan_status)) +: Khởi tạo ggplot (trên dữ liệu đã lọc). Ánh xạ loi_nhuan_status (biến nhóm) vào trục x và roe (biến giá trị) vào trục y. fill cũng được gán để tô màu cho các hộp.
Câu lệnh #5 geom_boxplot(…) +: Yêu cầu R vẽ biểu đồ hộp. Biểu đồ này sẽ tóm tắt 50% dữ liệu (phần thân hộp) và đường trung vị (Median) cho mỗi nhóm (“Lãi” và “Lỗ”).
Câu lệnh #6 geom_jitter(…) +: Đây là một lớp bổ sung rất hữu ích. Vì chúng ta chỉ có 10 điểm dữ liệu, việc vẽ các điểm thực tế (geom_jitter - các điểm được “rung” nhẹ để không bị chồng lên nhau) lên trên biểu đồ hộp giúp người xem thấy rõ dữ liệu gốc (đặc biệt là 2 điểm của nhóm “Lỗ”).
Câu lệnh #7 scale_fill_manual(…): Đồng bộ màu “Lãi” (xanh) và “Lỗ” (đỏ) giống như Đồ thị 6.
Câu lệnh #8, #9: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #10: scale_y_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % (từ Câu lệnh #2) cho trục y.
#2.4.5.2. Đồ thị 12: So sánh Đòn bẩy (D/E Ratio) giữa các năm Lãi và Lỗ.
Thao tác này sẽ vẽ hai Biểu đồ Hộp (Box Plot) song song, tương tự như Đồ thị 11. Nhưng lần này, chúng ta sẽ so sánh sự phân phối của chỉ số de_ratio (đòn bẩy tài chính) giữa hai nhóm “Lãi” và “Lỗ”.
Mục đích là để trả lời câu hỏi: “Các năm ‘Lỗ’ có xu hướng sử dụng đòn bẩy (nợ) cao hơn hay thấp hơn so với các năm ‘Lãi’?”
Lưu ý: Chúng ta sẽ lọc bỏ các giá trị Inf(Vô cực) khỏi cộtde_ratio trước khi vẽ.
# Đồ thị 12: So sánh Đòn bẩy (D/E) giữa năm Lãi và Lỗ (Biểu đồ Hộp)
library(tidyverse) #1
dinh_dang_so <- function(x) { #2
sprintf("%.2f", x)
}
agr_analysis %>%
filter(is.finite(de_ratio)) %>% #3
ggplot(aes(x = loi_nhuan_status, y = de_ratio, fill = loi_nhuan_status)) + #4
geom_boxplot(show.legend = FALSE) + #5
geom_jitter(width = 0.1, alpha = 0.7, color = "black") + #6
scale_fill_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) + #7
labs(title = "So sánh Đòn bẩy tài chính (D/E) giữa các năm Lãi và Lỗ", #8
subtitle = "Mỗi điểm đen là một năm thực tế",
x = "Trạng thái Lợi nhuận",
y = "Tỷ số Đòn bẩy (D/E Ratio)") +
theme_minimal() + #9
scale_y_continuous(labels = dinh_dang_so) #10
Giải thích:
Câu lệnh #2: dinh_dang_so <- function(x) { … }: Tạo hàm tùy chỉnh (dùng sprintf) để định dạng số thập phân (lấy 2 chữ số).
Câu lệnh #3 filter(is.finite(de_ratio)) %>%: Đây là bước tiền xử lý bắt buộc, lọc bỏ các giá trị Inf (Vô cực) hoặc NA từ cột de_ratio trước khi vẽ.
Câu lệnh #4 ggplot(aes(x = loi_nhuan_status, y = de_ratio, …)) +: Khởi tạo ggplot, ánh xạ loi_nhuan_status (nhóm) vào x và de_ratio (giá trị) vào y.
Câu lệnh #5: geom_boxplot(…) +: Thêm lớp biểu đồ hộp để vẽ phân phối 5 số.
Câu lệnh #6: geom_jitter(…) +: Thêm các điểm dữ liệu thực tế (10 năm) lên trên biểu đồ hộp để thấy rõ sự phân bổ, đặc biệt là của 2 năm “Lỗ”.
Câu lệnh #7: scale_fill_manual(…): Đồng bộ màu “Lãi” (xanh) và “Lỗ” (đỏ).
Câu lệnh #8, #9: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #10: scale_y_continuous(labels = dinh_dang_so): Áp dụng hàm định dạng số (từ Câu lệnh #2) cho trục y.
#2.4.5.3. Đồ thị 13: Xu hướng Doanh thu (2015-2024).
Thao tác này sẽ vẽ xu hướng của cột doanh_thu (đã đổi tên) để cho thấy quy mô doanh thu của AGR đã biến động như thế nào trong 10 năm qua, bổ sung cho biểu đồ xu hướng lợi nhuận.
# Đồ thị 13: Xu hướng Doanh thu (2015-2024)
library(tidyverse) # 1
dinh_dang_ty_vnd <- function(x) { # 2
paste(sprintf("%.0f", x / 1e9), "tỷ")
}
ggplot(agr_analysis, aes(x = nam, y = doanh_thu)) + # 3
geom_line(color = "#007bff", size = 1.2) + # 4
geom_point(color = "#007bff", size = 3) + # 5
geom_smooth(method = "lm", color = "red", linetype = "dashed", se = FALSE) + # 6
labs(title = "Xu hướng Doanh thu của AGR (2015-2024)", # 7
subtitle = "Đường màu đỏ là đường xu hướng tuyến tính",
x = "Năm",
y = "Doanh thu (Tỷ VND)") +
theme_minimal() + # 8
scale_y_continuous(labels = dinh_dang_ty_vnd) + # 9
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 10
Giải thích:
Câu lệnh #2 dinh_dang_ty_vnd <- function(x) { … }: Tạo hàm tùy chỉnh để định dạng trục y. Hàm này nhận giá trị gốc (ví dụ: 300000000000), chia cho 1 tỷ (x / 1e9), rồi định dạng số nguyên (%.0f) và thêm chuỗi ” tỷ” (Kết quả: “300 tỷ”).
Câu lệnh #3: ggplot(agr_analysis, aes(x = nam, y = doanh_thu)) +: Khởi tạo ggplot, ánh xạ nam vào x và doanh_thu (chưa chia) vào y.
Câu lệnh #4, #5: Thêm các lớp geom_line và geom_point.
Câu lệnh #6: geom_smooth(method = “lm”, …): Thêm một đường xu hướng tuyến tính (màu đỏ) để làm nổi bật xu hướng tăng trưởng tổng thể của doanh thu.
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #9: scale_y_continuous(labels = dinh_dang_ty_vnd): Áp dụng hàm tùy chỉnh dinh_dang_ty_vnd (từ Câu lệnh #2) cho trục y.
Câu lệnh #10: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.5.4. Đồ thị 14: Xu hướng Lợi nhuận trước thuế (2015-2024).
Thao tác này sẽ vẽ xu hướng của cột loi_nhuan_truoc_thue (đã đổi tên). Biểu đồ này sẽ bổ sung cho Đồ thị 1 (Lợi nhuận sau thuế), cho thấy khả năng sinh lời của công ty trước khi tính chi phí thuế TNDN.
# Đồ thị 14: Xu hướng Lợi nhuận trước thuế (2015-2024)
library(tidyverse) # 1
dinh_dang_ty_vnd <- function(x) { # 2
paste(sprintf("%.0f", x / 1e9), "tỷ")
}
ggplot(agr_analysis, aes(x = nam, y = loi_nhuan_truoc_thue)) + # 3
geom_line(color = "#17a2b8", size = 1.2) + # 4
geom_point(color = "#17a2b8", size = 3) + # 5
geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) + # 6
labs(title = "Xu hướng Lợi nhuận trước thuế của AGR (2015-2024)", # 7
subtitle = "Đường màu đỏ là ngưỡng hòa vốn (Lợi nhuận = 0)",
x = "Năm",
y = "Lợi nhuận trước thuế (Tỷ VND)") +
theme_minimal() + # 8
scale_y_continuous(labels = dinh_dang_ty_vnd) + # 9
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 10
Giải thích:
Câu lệnh #2: dinh_dang_ty_vnd <- function(x) { … }: Tái sử dụng hàm tùy chỉnh để định dạng trục y sang “tỷ VND”.
Câu lệnh #3: ggplot(agr_analysis, aes(x = nam, y = loi_nhuan_truoc_thue)) +: Khởi tạo ggplot, ánh xạ nam vào x và loi_nhuan_truoc_thue vào y.
Câu lệnh #4, #5, #6: Thêm các lớp geom_line, geom_point và geom_hline (đường tham chiếu 0).
Câu lệnh #7, #8: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #9: scale_y_continuous(labels = dinh_dang_ty_vnd): Áp dụng hàm định dạng “tỷ VND” (từ Câu lệnh #2) cho trục y.
Câu lệnh #10: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.5.5. Đồ thị 15: Xu hướng Biên lợi nhuận ròng (2015-2024).
Thao tác này sẽ vẽ xu hướng của chỉ số bien_loi_nhuan_rong (mà chúng ta đã tạo). Biểu đồ này cho thấy hiệu quả chuyển đổi doanh thu thành lợi nhuận của AGR qua các năm.
Lưu ý: Chúng ta sẽ lọc bỏ các giá trị Inf (Vô cực) nếu có khỏi cột này trước khi vẽ.
# Đồ thị 15: Xu hướng Biên lợi nhuận ròng
library(tidyverse) # 1
dinh_dang_phan_tram <- function(x) { # 2
paste(sprintf("%.1f", x * 100), "%")
}
agr_analysis %>%
filter(is.finite(bien_loi_nhuan_rong)) %>% # 3
ggplot(aes(x = nam, y = bien_loi_nhuan_rong)) + # 4
geom_line(color = "#6f42c1", size = 1.2) + # 5
geom_point(color = "#6f42c1", size = 3) + # 6
geom_hline(yintercept = 0, linetype = "dashed", color = "red", size = 1) + # 7
labs(title = "Xu hướng Biên lợi nhuận ròng của AGR (2015-2024)", # 8
subtitle = "Biên lợi nhuận ròng = Lợi nhuận sau thuế / Doanh thu",
x = "Năm",
y = "Biên lợi nhuận ròng (%)") +
theme_minimal() + # 9
scale_y_continuous(labels = dinh_dang_phan_tram) + # 10
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 11
Giải thích:
Câu lệnh #2: dinh_dang_phan_tram <- function(x) { … }: Tạo hàm tùy chỉnh để định dạng %.
Câu lệnh #3 filter(is.finite(bien_loi_nhuan_rong)) %>%: (An toàn) Lọc bỏ các giá trị Inf (Vô cực) hoặc NA nếu có khỏi cột bien_loi_nhuan_rong trước khi vẽ.
Câu lệnh #4: ggplot(aes(x = nam, y = bien_loi_nhuan_rong)) +: Khởi tạo ggplot, ánh xạ nam vào x và bien_loi_nhuan_rong vào y.
Câu lệnh #5, #6, #7: Thêm các lớp geom_line, geom_point và geom_hline (đường tham chiếu 0%).
Câu lệnh #8, #9: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #10: scale_y_continuous(labels = dinh_dang_phan_tram): Áp dụng hàm định dạng % (từ Câu lệnh #2) cho trục y.
Câu lệnh #11: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.5.6. Đồ thị 16: Mối quan hệ giữa Doanh thu và Lợi nhuận sau thuế.
Thao tác này sẽ vẽ một biểu đồ tán xạ. Mỗi điểm là một năm. Trục X là Doanh thu và Trục Y là Lợi nhuận sau thuế. Chúng ta muốn xem liệu có phải những năm có doanh thu cao hơn (trục X) thì cũng có lợi nhuận cao hơn (trục Y) hay không.
Lưu ý: Chúng ta sẽ lọc bỏ các giá trị Inf (Vô cực) nếu có khỏi cả hai cột trước khi vẽ.
# Đồ thị 16: Mối quan hệ giữa Doanh thu và Lợi nhuận sau thuế
library(tidyverse) # 1
dinh_dang_ty_vnd <- function(x) { # 2
paste(sprintf("%.0f", x), "tỷ") # Định dạng số nguyên, thêm " tỷ"
}
agr_analysis %>%
filter(is.finite(doanh_thu) & is.finite(loi_nhuan_sau_thue)) %>% # 3
ggplot(aes(x = doanh_thu / 1e9, y = loi_nhuan_sau_thue / 1e9)) + # 4
geom_point(aes(color = loi_nhuan_status), size = 3) + # 5
geom_smooth(method = "lm", color = "black", linetype = "dashed", se = FALSE) + # 6
geom_text(aes(label = nam), vjust = -1, size = 3) + # 7
labs(title = "Mối quan hệ giữa Doanh thu và Lợi nhuận sau thuế", # 8
subtitle = "Mỗi điểm là một năm. Màu sắc phân biệt Lãi/Lỗ",
x = "Doanh thu (Tỷ VND)",
y = "Lợi nhuận sau thuế (Tỷ VND)",
color = "Trạng thái") + # Đổi tên chú giải
theme_minimal() + # 9
scale_y_continuous(labels = dinh_dang_ty_vnd) + # 10
scale_x_continuous(labels = dinh_dang_ty_vnd) + # 11
scale_color_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) # 12
Giải thích:
Câu lệnh #2: Tạo hàm dinh_dang_ty_vnd (dùng sprintf) để định dạng “tỷ VND”.
Câu lệnh #3 filter(is.finite(doanh_thu) & is.finite(loi_nhuan_sau_thue)) %>%: (An toàn) Lọc bỏ bất kỳ hàng nào có giá trị Inf hoặc NA ở một trong hai cột này.
Câu lệnh #4 ggplot(aes(x = doanh_thu / 1e9, y = loi_nhuan_sau_thue / 1e9)) +: Khởi tạo ggplot. Chia cả hai trục cho 1 tỷ (1e9) để có đơn vị “Tỷ VND”.
Câu lệnh #5 geom_point(aes(color = loi_nhuan_status), …): Vẽ các điểm và tô màu theo “Lãi” (xanh) hoặc “Lỗ” (đỏ).
Câu lệnh #6 geom_smooth(method = “lm”, …): Thêm đường xu hướng tuyến tính (màu đen).
Câu lệnh #7 geom_text(aes(label = nam), …): Thêm nhãn nam cho từng điểm.
Câu lệnh #8, #9, #12: labs(…), theme_minimal(), scale_color_manual(…): Tùy chỉnh tiêu đề, giao diện, và màu sắc.
Câu lệnh #10, #11: scale_…_continuous(labels = dinh_dang_ty_vnd): Áp dụng hàm định dạng “tỷ VND” (từ Câu lệnh #2) cho cả hai trục.
#2.4.5.7. Đồ thị 17: So sánh xu hướng Nợ phải trả và Vốn chủ sở hữu.
Thao tác này sẽ vẽ một biểu đồ nhiều đường (Multi-line Plot) trên cùng một trục. Biểu đồ này sẽ cho thấy sự thay đổi tương quan giữa hai thành phần chính của cấu trúc vốn: no_phai_tra(Nợ) vàvon_chu_so_huu (Vốn chủ).
Để vẽ hai đường này, chúng ta cần một bước chuẩn bị: “tidy” (dọn dẹp) dữ liệu. Chúng ta sẽ chuyển đổi 2 cột (no_phai_tra, von_chu_so_huu) thành 1 cột Chi_tieu(Chỉ tiêu) và 1 cộtGia_tri(Giá trị) bằng cách sử dụng hàmpivot_longer()(thuộctidyverse).
# Đồ thị 17: Xu hướng Nợ phải trả vs Vốn chủ sở hữu
library(tidyverse) # 1
dinh_dang_ty_vnd <- function(x) { # 2
paste(sprintf("%.0f", x), "tỷ") # Định dạng số nguyên, thêm " tỷ"
}
data_long_von <- agr_analysis %>%
filter(is.finite(no_phai_tra) & is.finite(von_chu_so_huu)) %>% # 3
select(nam, `Nợ phải trả` = no_phai_tra, `Vốn chủ sở hữu` = von_chu_so_huu) %>% # 4
pivot_longer(cols = c(`Nợ phải trả`, `Vốn chủ sở hữu`), # 5
names_to = "Chi_tieu",
values_to = "Gia_tri")
ggplot(data_long_von, aes(x = nam, y = Gia_tri / 1e9, color = Chi_tieu)) + # 6
geom_line(size = 1.2) + # 7
geom_point(size = 3) + # 8
scale_color_manual(values = c("Nợ phải trả" = "#dc3545", "Vốn chủ sở hữu" = "#007bff")) + # 9
labs(title = "Xu hướng Cơ cấu Vốn: Nợ phải trả vs. Vốn chủ sở hữu", # 10
subtitle = "So sánh sự biến động của Nợ và Vốn chủ qua 10 năm",
x = "Năm",
y = "Giá trị (Tỷ VND)",
color = "Chỉ tiêu Tài chính") + # Đổi tên chú giải
theme_minimal() + # 11
scale_y_continuous(labels = function(x) { sprintf("%.0f", x) }) + # 12
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 13
Giải thích:
Câu lệnh #3 - #5: Chuẩn bị dữ liệu.
Câu lệnh #4: select(nam, Nợ phải trả = no_phai_tra, …): Chọn các cột cần vẽ. Lưu ý, chúng ta đổi tên no_phai_tra thành Nợ phải trả (viết hoa, có dấu cách) để nhãn trên chú giải (legend) đẹp hơn.
Câu lệnh #5: pivot_longer(…): Đây là hàm “tidy” quan trọng (thuộc tidyr). Nó lấy 2 cột (Nợ phải trả, Vốn chủ sở hữu) và “xoay” chúng thành 2 cột mới: 1) Chi_tieu (chứa tên: “Nợ phải trả”, “Vốn chủ sở hữu”) và 2) Gia_tri (chứa giá trị tương ứng). Bảng data_long_von mới này sẵn sàng để vẽ biểu đồ nhiều đường.
Câu lệnh #6 - #13: Vẽ biểu đồ.
Câu lệnh #6 ggplot(data_long_von, aes(x = nam, y = Gia_tri / 1e9, color = Chi_tieu)) +: Vẽ từ bảng data_long_von mới. Ánh xạ Gia_tri (đã chia 1 tỷ) vào y và Chi_tieu vào color. ggplot sẽ tự động vẽ 2 đường với 2 màu khác nhau.
Câu lệnh #7, #8: Thêm các lớp geom_line và geom_point.
Câu lệnh #9: scale_color_manual(…): Tự định nghĩa màu cho 2 đường: “Nợ phải trả” (đỏ) và “Vốn chủ sở hữu” (xanh).
Câu lệnh #10, #11: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #12: scale_y_continuous(labels = …): Sử dụng hàm sprintf để định dạng trục y (tỷ VND) (lưu ý: hàm dinh_dang_ty_vnd ở trên đã chia cho 1e9, nên ở đây sprintf chỉ cần định dạng số nguyên).
Câu lệnh #13: scale_x_continuous(…): Đảm bảo trục x hiển thị nhãn cho tất cả 10 năm.
#2.4.5.8. Đồ thị 18: So sánh xu hướng Doanh thu và Lợi nhuận trước thuế.
Thao tác này sẽ vẽ một biểu đồ nhiều đường (Multi-line Plot) thứ hai. Biểu đồ này sẽ đặt doanh_thuvàloi_nhuan_truoc_thue trên cùng một trục, cho phép chúng ta so sánh trực tiếp quy mô và sự biến động của chúng qua các năm.
Giống như Đồ thị 17, chúng ta cần “tidy” (dọn dẹp) dữ liệu bằng hàm pivot_longer() trước khi vẽ.
# Đồ thị 18: Xu hướng Doanh thu vs Lợi nhuận trước thuế
library(tidyverse) # 1
dinh_dang_ty_vnd <- function(x) { # 2
paste(sprintf("%.0f", x), "tỷ") # Định dạng số nguyên, thêm " tỷ"
}
data_long_loinhuan <- agr_analysis %>%
filter(is.finite(doanh_thu) & is.finite(loi_nhuan_truoc_thue)) %>% # 3
select(nam, `Doanh thu` = doanh_thu, `Lợi nhuận trước thuế` = loi_nhuan_truoc_thue) %>% # 4
pivot_longer(cols = c(`Doanh thu`, `Lợi nhuận trước thuế`), # 5
names_to = "Chi_tieu",
values_to = "Gia_tri")
ggplot(data_long_loinhuan, aes(x = nam, y = Gia_tri / 1e9, color = Chi_tieu)) + # 6
geom_line(size = 1.2) + # 7
geom_point(size = 3) + # 8
scale_color_manual(values = c("Doanh thu" = "#007bff", "Lợi nhuận trước thuế" = "#17a2b8")) + # 9
labs(title = "Xu hướng Doanh thu vs. Lợi nhuận trước thuế", # 10
subtitle = "So sánh sự biến động của Doanh thu và Lợi nhuận qua 10 năm",
x = "Năm",
y = "Giá trị (Tỷ VND)",
color = "Chỉ tiêu Tài chính") + # Đổi tên chú giải
theme_minimal() + # 11
scale_y_continuous(labels = function(x) { sprintf("%.0f", x) }) + # 12
scale_x_continuous(breaks = seq(min(agr_analysis$nam), max(agr_analysis$nam), by = 1)) # 13
Giải thích:
Câu lệnh #3 - #5: Chuẩn bị dữ liệu.
Câu lệnh #4: select(…): Chọn cột nam, doanh_thu (đổi tên thành Doanh thu), và loi_nhuan_truoc_thue (đổi tên thành Lợi nhuận trước thuế).
Câu lệnh #5: pivot_longer(…): “Xoay” 2 cột (Doanh thu, Lợi nhuận trước thuế) thành 2 cột (Chi_tieu, Gia_tri) để tạo bảng “dài” data_long_loinhuan.
Câu lệnh #6 - #13: Vẽ biểu đồ.
Câu lệnh #6 ggplot(data_long_loinhuan, aes(x = nam, y = Gia_tri / 1e9, color = Chi_tieu)) +: Vẽ từ bảng “dài” mới. Ánh xạ Gia_tri (đã chia 1 tỷ) vào y và Chi_tieu vào color. ggplot sẽ tự động vẽ 2 đường.
Câu lệnh #7, #8: Thêm các lớp geom_line và geom_point.
Câu lệnh #9: scale_color_manual(…): Tự định nghĩa màu cho 2 đường: “Doanh thu” (xanh) và “Lợi nhuận trước thuế” (xanh cyan).
Câu lệnh #10, #11: labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #12, #13: scale_…_continuous(…): Định dạng trục y (tỷ VND) và đảm bảo trục x hiển thị tất cả các năm.
#2.4.5.9. Đồ thị 19: So sánh các chỉ số trung bình giữa năm Lãi và Lỗ.
Thao tác này sẽ trực quan hóa Bảng tóm tắt (Thao tác 18, Phần 3) mà chúng ta đã tạo. Chúng ta sẽ dùng Biểu đồ cột nhóm (Dodged Bar Chart) để so sánh các chỉ số trung bình (ROE, D/E, Doanh thu) của 8 năm “Lãi” so với 2 năm “Lỗ”.
Biểu đồ này rất quan trọng để “nhìn thấy” sự khác biệt cốt lõi giữa các năm hoạt động hiệu quả và không hiệu quả.
Lưu ý: Chúng ta sẽ cần “tidy” (dọn dẹp) bảng summary_by_status trước khi vẽ.
# Đồ thị 19: Biểu đồ cột so sánh nhóm Lãi vs Lỗ
library(tidyverse) # 1
summary_by_status <- agr_analysis %>% # 2
filter(is.finite(roe) & is.finite(de_ratio) & is.finite(doanh_thu)) %>%
group_by(loi_nhuan_status) %>%
summarise(
So_nam = n(),
ROE_Trung_binh = mean(roe, na.rm = TRUE),
DE_Ratio_Trung_binh = mean(de_ratio, na.rm = TRUE),
Doanh_thu_Trung_binh = mean(doanh_thu / 1e9, na.rm = TRUE) # Chia 1 tỷ
)
data_long_summary <- summary_by_status %>%
select(loi_nhuan_status, `ROE (%)` = ROE_Trung_binh, `D/E Ratio` = DE_Ratio_Trung_binh) %>% # 3
pivot_longer(cols = c(`ROE (%)`, `D/E Ratio`), # 4
names_to = "Chi_so",
values_to = "Gia_tri") %>%
mutate(Gia_tri_ve = ifelse(Chi_so == "ROE (%)", Gia_tri * 100, Gia_tri)) # 5
ggplot(data_long_summary, aes(x = Chi_so, y = Gia_tri_ve, fill = loi_nhuan_status)) + # 6
geom_col(position = "dodge") + # 7
geom_text(aes(label = sprintf("%.2f", Gia_tri_ve)), # 8
position = position_dodge(width = 0.9), vjust = -0.5) +
scale_fill_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545")) + # 9
labs(title = "So sánh Chỉ số Trung bình giữa năm Lãi và Lỗ", # 10
subtitle = "So sánh ROE (%) và D/E Ratio (lần)",
x = "Chỉ số Tài chính",
y = "Giá trị Trung bình",
fill = "Trạng thái") + # Đổi tên chú giải
theme_minimal() # 11
Giải thích:
Câu lệnh #2: Chạy lại Thao tác 18 (Phần 2.3) để đảm bảo có dữ liệu summary_by_status.
Câu lệnh #3 - #5: Chuẩn bị dữ liệu (Tidy Data).
Câu lệnh #3: select(…): Chọn chỉ 2 chỉ số có thể so sánh chung (ROE, D/E).
Câu lệnh #4: pivot_longer(…): “Xoay” bảng, chuẩn bị cho geom_col(position = “dodge”).
Câu lệnh #5: mutate(Gia_tri_ve = …): Tạo cột Gia_tri_ve (Giá trị vẽ), trong đó ROE được nhân 100 (để thể hiện %).
Câu lệnh #6 - #11: Vẽ biểu đồ.
Câu lệnh #6 ggplot(aes(x = Chi_so, y = Gia_tri_ve, fill = loi_nhuan_status)) +: Ánh xạ Chi_so (ROE, D/E) vào x, Gia_tri_ve (giá trị) vào y, và loi_nhuan_status (Lãi/Lỗ) vào fill.
Câu lệnh #7 geom_col(position = “dodge”) +: Sử dụng geom_col() (vì dữ liệu đã tóm tắt).
position = “dodge”: Yêu cầu ggplot tạo các cặp cột (Lãi/Lỗ) “đứng cạnh nhau” tại mỗi vị trí x (mỗi chỉ số).
position = position_dodge(width = 0.9): Đảm bảo các nhãn cũng “né” nhau (dodge) giống như các cột.
#2.4.5.10. Đồ thị 20: Xếp hạng ROE theo từng năm (Biểu đồ Lollipop).
Thao tác này sẽ vẽ một biểu đồ Lollipop (Kẹo mút). Đây là một dạng biểu đồ cột (Bar Chart) được cách điệu, rất tốt để so sánh các giá trị. Chúng ta sẽ sử dụng nó để xếp hạng 10 năm dựa trên hiệu suất ROE (Tỷ suất lợi nhuận trên vốn chủ sở hữu), từ cao nhất đến thấp nhất.
Lưu ý: Cần lọc bỏ các giá trị Inf(Vô cực) khỏi cộtroe trước khi vẽ.
# Đồ thị 20: Xếp hạng ROE theo năm (Lollipop Chart)
library(tidyverse) # 1
dinh_dang_phan_tram <- function(x) { # 2
paste(sprintf("%.1f", x * 100), "%")
}
agr_analysis %>%
filter(is.finite(roe)) %>% # 3
mutate(nam_fct = reorder(as.factor(nam), roe)) %>% # 4
ggplot(aes(x = nam_fct, y = roe, color = loi_nhuan_status)) + # 5
geom_segment(aes(x = nam_fct, xend = nam_fct, y = 0, yend = roe), # 6
size = 1) +
geom_point(size = 4) + # 7
geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # 8
coord_flip() + # 9
scale_color_manual(values = c("Lãi" = "#28a745", "Lỗ" = "#dc3545"), # 10
guide = "none") + # Ẩn chú giải (legend)
labs(title = "Xếp hạng Hiệu quả Sinh lời (ROE) theo Năm", # 11
subtitle = "Xếp hạng 10 năm dựa trên Tỷ suất lợi nhuận trên vốn chủ sở hữu",
x = "Năm",
y = "Tỷ suất ROE (%)") +
theme_minimal() + # 12
scale_y_continuous(labels = dinh_dang_phan_tram) # 13
Giải thích:
Câu lệnh #3: filter(is.finite(roe)) %>%: (An toàn) Lọc bỏ các giá trị Inf (Vô cực) trước khi vẽ.
Câu lệnh #4 mutate(nam_fct = reorder(as.factor(nam), roe)) %>%: Đây là lệnh rất quan trọng để sắp xếp biểu đồ.
as.factor(nam): Biến nam (số) thành nam_fct (nhóm/văn bản).
reorder(nam_fct, roe): Sắp xếp lại thứ tự của các “Năm” (nam_fct) dựa trên giá trị roe tương ứng của chúng (từ thấp đến cao).
Câu lệnh #5 ggplot(aes(x = nam_fct, y = roe, color = loi_nhuan_status)) +: Khởi tạo ggplot. Ánh xạ nam_fct (các năm đã sắp xếp) vào x, roe (giá trị) vào y, và loi_nhuan_status vào color.
Câu lệnh #6 geom_segment(…) +: Vẽ các đường thẳng đứng (thanh kẹo) từ y=0 đến y=roe cho mỗi x (mỗi năm).
Câu lệnh #7 geom_point(…) +: Vẽ các điểm tròn (viên kẹo) ở cuối mỗi thanh (y=roe).
Câu lệnh #8 geom_hline(…) +: Thêm đường tham chiếu 0% (ranh giới Lãi/Lỗ).
Câu lệnh #9 coord_flip() +: Lật ngang biểu đồ (hoán đổi trục x và y). Kết quả là các thanh kẹo sẽ nằm ngang, giúp các nhãn “Năm” (giờ nằm trên trục y) dễ đọc hơn.
Câu lệnh #10 scale_color_manual(…, guide = “none”) +: Tô màu các “viên kẹo” theo “Lãi” (xanh) hoặc “Lỗ” (đỏ). guide = “none” dùng để ẩn chú giải (legend) vì đã quá rõ ràng.
Câu lệnh #11, #12 labs(…) và theme_minimal(): Tùy chỉnh tiêu đề/nhãn và áp dụng giao diện.
Câu lệnh #13 scale_y_continuous(labels = dinh_dang_phan_tram): Sử dụng hàm định dạng % (từ Câu lệnh #2) để định dạng trục y (nay là trục ngang) thành %.
#2.5. Kết luận chương 2.
Chương 2 đã hoàn thành mục tiêu phân tích bộ dữ liệu tài chính 10 năm của mã chứng khoán AGR (Công ty Cổ phần Chứng khoán Agribank). Khác biệt cơ bản so với Chương 1, thách thức trọng tâm của chương này không nằm ở quy mô dữ liệu, mà nằm ở tính phức tạp của dữ liệu thô (dirty data), đòi hỏi các kỹ thuật tiền xử lý chuyên sâu trước khi có thể tiến hành phân tích.
Quá trình phân tích đã trải qua ba giai đoạn then chốt:
Chẩn đoán dữ liệu (Phần 2.1): Giai đoạn khám phá (EDA) đã nhanh
chóng xác định vấn đề cốt lõi: 21 trong 22 cột tài chính bị R đọc sai
thành kiểu văn bản (
Làm sạch và Làm giàu (Phần 2.2): Đây là giai đoạn xử lý kỹ thuật quan trọng nhất của chương.
Làm sạch (Cleaning): Bằng cách sử dụng kết hợp dplyr::across và stringr::str_replace_all, tất cả các ký tự “bẩn” (dấu phẩy và chuỗi “Không có”) đã được xử lý một cách hệ thống và hiệu quả.
Chuyển đổi kiểu (Type Casting): 21 cột dữ liệu đã được ép kiểu
thành công từ
Làm giàu (Feature Engineering): Giá trị của bộ dữ liệu được nâng cao đáng kể thông qua việc tạo ra 5 chỉ số tài chính phái sinh quan trọng (ROA, ROE, DE_Ratio, Bien_Loi_Nhuan_Rong) và 1 biến phân loại (Loi_Nhuan_Status). Các tên cột cũng được chuẩn hóa bằng janitor::clean_names để tối ưu hóa cho các truy vấn sau này.
Về khả năng sinh lời: Đã xác định được bức tranh tổng quan 10 năm với 8 năm báo “Lãi” và 2 năm báo “Lỗ” (Thao tác 14). Các xu hướng trung tâm (Mean, Median) của Lợi nhuận sau thuế, ROA, và ROE đã được tính toán, cung cấp các thước đo cơ sở về hiệu quả sử dụng tài sản và vốn chủ sở hữu.
Về phạm vi hoạt động: Phân tích đã chỉ rõ năm tài chính tốt nhất (lợi nhuận cao nhất, Thao tác 19) và năm kém nhất (lỗ nặng nhất, Thao tác 20) trong giai đoạn khảo sát, cung cấp các mốc tham chiếu quan trọng về hiệu suất.
Về cấu trúc tài chính và rủi ro: Đã lượng hóa được quy mô tài sản/vốn chủ sở hữu trung bình và mức độ sử dụng đòn bẩy (D/E Ratio) trung bình (Thao tác 10). Phân tích tương quan (Thao tác 17) cũng đã được thực hiện để kiểm tra mối liên hệ giữa lợi nhuận và việc sử dụng nợ.
Phân tích theo nhóm: Bảng tóm tắt theo trạng thái Lãi/Lỗ (Thao tác 18) đã cung cấp một cái nhìn sâu sắc, so sánh các chỉ số trung bình (như ROE, Doanh thu) giữa các năm hoạt động hiệu quả và không hiệu quả, cho thấy sự khác biệt rõ rệt về hiệu quả hoạt động.
Tóm lại, Chương 2 đã minh họa thành công một quy trình phân tích dữ liệu tài chính thực tế, từ việc “giải cứu” dữ liệu bẩn (một kỹ năng thực tế quan trọng) đến việc tạo ra các chỉ số có ý nghĩa và rút ra các kết luận thống kê định lượng. Các bảng, số liệu và kết quả tương quan thu được đã vẽ nên một bức tranh rõ nét về sức khỏe tài chính của AGR trong thập kỷ qua. Những kết quả này đã hoàn thiện phần phân tích thứ hai, kết thúc nội dung cốt lõi của bài tiểu luận.