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

Ý 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ị:

# 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ụ: , ), và một vài giá trị mẫu. Kết quả glimpse() xác nhận rằng R đã đọc chính xác các kiểu dữ liệu (ví dụ: year là kiểu số, artists là kiểu ký tự), cho thấy dữ liệu đã sẵn sàng cho phân tích.

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

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

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

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

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

Kết luận phần 1.2:

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

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

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ố () với giá trị 0 hoặc 1. Nếu sử dụng trực tiếp trong biểu đồ, các nhãn trên trục sẽ là “0” và “1”, gây khó hiểu cho người đọc.

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:

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

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

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:

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:

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:

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:

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:

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:

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:

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

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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.

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:

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.

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:

Ý nghĩa: Định dạng trục y (số lượng) với dấu phẩy hàng ngàn.

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

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:

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:

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

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:

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:

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

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

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:

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:

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:

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:

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:

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

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:

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:

  1. Câu chuyện về sự thay đổi của âm nhạc qua 100 năm:

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

  1. Câu chuyện về “Công thức” của một bài hát nổi tiếng:

Â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”.

  1. Câu chuyện về các mối quan hệ kỹ thuật:

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:

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 (, ) mà R đã tự động gán cho 22 cột đó.

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

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à ) và mô tả ngắn gọn..

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

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 () và cung cấp một danh sách rõ ràng các chỉ tiêu tài chính sẽ được làm sạch và phân tích.Câu lệnh #1: agr_metadata <- tribble(…)

#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à (văn bản) và xác nhận sự cần thiết của việc làm sạch dữ liệu ở phần 2.2.

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

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:

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:

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 () do chứa dấu phẩy và chuỗi “Không có”. Toàn bộ phần 2.2 tiếp theo sẽ tập trung vào việc “giải cứu” và chuyển đổi các cột này về đúng định dạng số (numeric) để có thể phân tích.

#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 () do chứa dấu phẩy (,) và chuỗi “Không có”. Điều này khiến việc phân tích là không thể.

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:

#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 () để chuẩn bị cho việc chuyển đổi sang dạng số. Quá trình này bao gồm hai bước:

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:

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 (ví dụ: “2242171802587”). Bước tiếp theo là chuyển chúng sang kiểu số.

#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 () mà R có thể dùng để tính toán.

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

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ừ (văn bản) thành (số). Bộ dữ liệu đã “sạch” và sẵn sàng để tính toán.

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

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:

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:

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:

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:

Nhận xét: Thao tác này tạo ra một biến phân loại ( hoặc ) mới, rất hữu ích cho việc thống kê theo nhóm (ví dụ: “So sánh ROE trung bình của các năm Lãi so với các năm Lỗ?”).

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

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:

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:

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:

Đố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:

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:

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:

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

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:

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:

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:

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:

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:

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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:

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

  1. 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 (). Nguyên nhân là do sự tồn tại của các ký tự phi số học (dấu phẩy phân cách hàng ngàn) và các giá trị thiếu phi chuẩn (chuỗi văn bản “Không có”).

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

  1. Phân tích Thống kê (Phần 2.3): Giai đoạn cuối cùng tập trung vào việc lượng hóa hiệu quả hoạt động của AGR, dựa trên 12 biến cốt lõi đã được chọn lọc. Các phát hiện thống kê chính bao gồm:

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.