Chương 1 của tiểu luận này tập trung vào việc thực hiện phân tích dữ liệu chuyên sâu (Exploratory Data Analysis - EDA) trên bộ dữ liệu lực lượng lao động Hoa Kỳ (2023). Mục tiêu chính của chương là xây dựng một nền tảng hiểu biết vững chắc về các đặc điểm của lực lượng lao động trong mẫu khảo sát. Các phân tích được thực hiện nhằm trả lời ba câu hỏi cốt lõi:
- Họ là ai? Phân tích các đặc điểm nhân khẩu học cơ bản (tuổi, giới tính, học vấn, hôn nhân).
- Họ làm gì? Đánh giá tình trạng việc làm (Employed, Unemployed, Not in labor force) và phân bố ngành nghề.
- Họ kiếm được bao nhiêu? Khám phá phân phối thu nhập và xác định các yếu tố nhân khẩu học, xã hội có mối tương quan mạnh mẽ nhất với mức lương.
Quá trình phân tích được thực hiện một cách tuần tự, bao gồm các bước từ nạp dữ liệu thô, làm sạch, tạo biến phân tích đến việc áp dụng các kỹ thuật thống kê mô tả và trực quan hóa để rút ra các số liệu có ý nghĩa, làm cơ sở cho mọi đánh giá và kết luận.
df_raw <- read_excel("data_2023_hoaKy.xlsx")
df <- df_raw %>% clean_names()
Quá trình phân tích được khởi đầu bằng việc nạp và khám phá sơ bộ bộ dữ liệu, giai đoạn nền tảng nhằm định hình sự hiểu biết ban đầu về cấu trúc và đặc điểm của dữ liệu. Về mặt kỹ thuật, hàm read_excel() được sử dụng để nhập dữ liệu. Ngay sau đó, hàm clean_names() từ gói janitor được áp dụng. Đây là một thao tác quan trọng giúp chuẩn hóa tên các cột về định dạng chữ thường và sử dụng dấu gạch dưới qua đó đảm bảo tính nhất quán và giảm thiểu lỗi cú pháp trong các câu lệnh về sau.
df %>% sample_n(6) %>% flextable() %>% set_caption("6 dòng dữ liệu ngẫu nhiên từ bộ dữ liệu") %>% autofit()
age | sex | race | marst | empstat | occ | ind | educ | fullpart | incwage |
|---|---|---|---|---|---|---|---|---|---|
80 | Female | White | Separated | Not in labor force | Not in universe | N/A (not applicable) | Some college | Not working | 0 |
33 | Female | White | Married, spouse present | Employed | Managers, all other | Machinery manufacturing, n.e.c. or not specified | Graduate degree | Full-time | 130,000 |
66 | Male | American Indian | Single | Not in labor force | Not in universe | N/A (not applicable) | Some college | Not working | 0 |
8 | Male | White | Single | NIU | Not in universe | N/A (not applicable) | Elementary school | Not working | 0 |
85 | Male | White | Separated | Not in labor force | Not in universe | N/A (not applicable) | Some high school | Not working | 0 |
20 | Male | Asian | Widowed | Not in labor force | Not in universe | N/A (not applicable) | Some college | Not working | 0 |
Việc xem xét trực tiếp một vài dòng dữ liệu ngẫu nhiên là một thao tác trực quan cần thiết để hình dung rõ hơn về nội dung và định dạng thực tế của các biến. Về mặt kỹ thuật, một chuỗi lệnh sử dụng toán tử pipe (%>%) đã được thực thi: một mẫu gồm 6 quan sát đã được trích xuất ngẫu nhiên bằng hàm sample_n(6), phương pháp lấy mẫu ngẫu nhiên này được ưu tiên hơn so với việc chỉ kiểm tra các quan sát đầu tiên, nhằm giảm thiểu rủi ro sai lệch chọn mẫu có thể phát sinh nếu dữ liệu đã được sắp xếp trước theo một biến cụ thể nào đó (ví dụ: sắp xếp theo tuổi hoặc theo khu vực địa lý). Kết quả sau đó được chuyển qua hàm flextable() để tạo ra một bảng biểu có định dạng chuyên nghiệp. Từ bảng kết quả, ta có thể rút ra những nhận định ban đầu giá trị. Trước hết, mẫu dữ liệu cho thấy sự đa dạng về các đặc điểm nhân khẩu học với sự hiện diện của cả nam và nữ, độ tuổi trải dài đa dạng và nhiều chủng tộc, tình trạng hôn nhân khác nhau. Quan trọng hơn, thao tác này giúp phát hiện trực quan các giá trị khuyết được mã hóa dưới dạng chuỗi ký tự như “Not in universe”, “N/A (not applicable)” và “NIU”. Việc nhìn thấy trực tiếp các giá trị này đã củng cố tầm quan trọng của việc phải xử lý chúng một cách cẩn thận ở bước làm sạch dữ liệu. Cuối cùng, mẫu ngẫu nhiên cũng gợi ý về các mối liên hệ tiềm năng cần được khám phá sâu hơn, ví dụ như mối quan hệ giữa độ tuổi, nghề nghiệp và mức thu nhập. Thao tác này không chỉ đơn thuần hiển thị dữ liệu mà còn là một bước khám phá quan trọng, giúp xác nhận các đặc điểm cấu trúc và phát hiện sớm các vấn đề tiềm ẩn trong dữ liệu thô.
dim(df)
[1] 146133 10
Kết quả từ hàm dim(df) cho thấy bộ dữ liệu bao gồm 146133 quan sát và 10 biến, thể hiện một quy mô mẫu lớn, đủ để các suy luận thống kê có được độ tin cậy cần thiết.
str(df)
tibble [146,133 × 10] (S3: tbl_df/tbl/data.frame)
$ age : num [1:146133] 66 68 52 51 78 65 68 74 74 76 ...
$ sex : chr [1:146133] "Female" "Female" "Female" "Male" ...
$ race : chr [1:146133] "White" "White" "White" "White" ...
$ marst : chr [1:146133] "Separated" "Separated" "Married, spouse present" "Married, spouse present" ...
$ empstat : chr [1:146133] "Not in labor force" "Not in labor force" "Not in labor force" "Employed" ...
$ occ : chr [1:146133] "Not in universe" "Not in universe" "Not in universe" "Retail salespersons" ...
$ ind : chr [1:146133] "N/A (not applicable)" "N/A (not applicable)" "N/A (not applicable)" "Auto parts, accessories, and tire stores" ...
$ educ : chr [1:146133] "Some college" "Some college" "Some college" "Some college" ...
$ fullpart: chr [1:146133] "Not working" "Not working" "Not working" "Full-time" ...
$ incwage : num [1:146133] 0 0 0 42000 0 55000 52000 0 0 0 ...
Để có một cái nhìn tổng quan về cấu trúc bên trong và các kiểu dữ liệu mà R đã tự động gán, hàm str() đã được sử dụng. Kết quả trả về 1 lần nữa xác nhận kích thước của bộ dữ liệu gồm 146133 quan sát và 10 biến, cung cấp thêm hai nhận định quan trọng. Thứ nhất, các biến định lượng như age và incwage đã được nhận diện chính xác dưới dạng số (num), sẵn sàng cho các phép tính toán học. Thứ hai, hầu hết các biến định tính dùng để phân loại như sex, race và marst lại đang được lưu trữ dưới dạng ký tự (chr). Việc nhận diện kiểu dữ liệu chr này là rất quan trọng vì nó chỉ ra sự cần thiết phải chuyển đổi chúng sang định dạng factor ở bước tiếp theo để tối ưu hóa cho các phân tích thống kê và trực quan hóa. Đồng thời, việc xem xét các giá trị mẫu trong output cũng củng cố phát hiện về sự tồn tại của các giá trị khuyết được mã hóa dưới dạng chuỗi ký tự như “not in universe” hay “n/a (not applicable)”. Từ đó định hướng cho các thao tác làm sạch dữ liệu cần thiết.
df %>% select(where(is.numeric)) %>% summarise(mean = mean(age), median = median(age), std_dev = sd(age), min = min(age), max = max(age), mean_income = mean(incwage), median_income = median(incwage), stv_dev_income = sd(incwage), min = min(incwage), max = max(incwage)) %>% pivot_longer(everything(), names_to = "Chi_so_thong_ke", values_to = "Gia_tri") %>% kable(caption = "Các chỉ số thống kê mô tả chính", digits = 2) %>% kable_styling(full_width = F)
| Chi_so_thong_ke | Gia_tri |
|---|---|
| mean | 38.73 |
| median | 38.00 |
| std_dev | 23.32 |
| min | 0.00 |
| max | 1549999.00 |
| mean_income | 31456.44 |
| median_income | 0.00 |
| stv_dev_income | 67360.93 |
Để có cái nhìn định lượng ban đầu về các biến số, một bảng thống kê mô tả đã được tạo ra. Về mặt kỹ thuật, hàm select(where(is.numeric)) được áp dụng để chỉ chọn các cột có kiểu dữ liệu là số, sau đó hàm summarise() tính toán các chỉ số thống kê trung tâm và phân tán như: giá trị trung bình (mean), trung vị (median) và độ lệch chuẩn (standard deviation). Cuối cùng, pivot_longer() được dùng để tái cấu trúc bảng kết quả về định dạng dài, giúp việc trình bày trở nên gọn gàng và dễ đọc hơn. Từ bảng kết quả, ta có thể rút ra một số nhận định sơ bộ quan trọng. Độ tuổi trung bình (mean) của mẫu là khoảng 38.7 tuổi và tuổi trung vị (median) là 38.0 tuổi. Hai giá trị này rất gần nhau, gợi ý rằng phân phối tuổi trong mẫu là tương đối đối xứng. Ngược lại, có một sự khác biệt rất lớn giữa thu nhập trung bình (mean_income = 31456.44 usd) và thu nhập trung vị (median_income = 0.00 usd). Giá trị trung vị bằng 0 cho thấy có ít nhất 50% số người trong bộ dữ liệu không có thu nhập từ lương (incwage). Sự chênh lệch lớn này cũng cho thấy phân phối thu nhập bị lệch phải rất mạnh với một số ít người có thu nhập rất cao đã kéo giá trị trung bình lên đáng kể.
sum(is.na(df))
[1] 0
Một bước kiểm tra chất lượng dữ liệu cơ bản là xác định sự tồn tại của các giá trị bị thiếu. Hàm sum(is.na(df)) được sử dụng để đếm tổng số giá trị NA chuẩn trong toàn bộ bộ dữ liệu. Kết quả trả về là 0, điều này có thể tạo ra một ấn tượng ban đầu rằng dữ liệu hoàn toàn đầy đủ. Tuy nhiên, như đã được phát hiện ở các bước khám phá trước, bộ dữ liệu này chứa các giá trị khuyết được mã hóa dưới dạng chuỗi ký tự. Do đó, kết quả bằng 0 này chỉ xác nhận rằng không có giá trị NA ở định dạng chuẩn của R và củng cố thêm sự cần thiết của việc phải xử lý các chuỗi ký tự đặc biệt này ở giai đoạn làm sạch dữ liệu.
num_duplicated_rows <- sum(duplicated(df))
cat(paste("Tổng số dòng bị trùng lặp hoàn toàn là:", num_duplicated_rows, "\n"))
Tổng số dòng bị trùng lặp hoàn toàn là: 65820
duplicated_summary <- df %>% group_by(across(everything())) %>% summarise(Frequency = n(), .groups = "drop") %>% arrange(desc(Frequency))
sampled_data <- duplicated_summary %>% sample_n(10)
sampled_data %>% select(age, sex, race, marst, empstat, occ) %>% flextable() %>% set_caption("10 dòng ngẫu nhiên trong bảng tần suất (Phần 1: Nhân khẩu học và nghề nghiệp)") %>% autofit()
age | sex | race | marst | empstat | occ |
|---|---|---|---|---|---|
39 | Female | White | Single | Employed | Web developers |
62 | Female | White | Married, spouse present | Not in labor force | Not in universe |
29 | Male | Black | Single | Employed | Software developers |
62 | Male | White | Married, spouse present | Employed | First-line supervisors of construction trades and extraction workers |
24 | Female | Black | Married, spouse present | Employed | Secretaries and administrative assistants, except legal, medical, and executive |
48 | Male | Black | Single | Employed | Firefighters |
43 | Male | White | Married, spouse present | Employed | Carpenters |
51 | Male | White | Married, spouse present | Employed | Driver/sales workers and truck drivers |
44 | Male | Black | Single | Employed | Janitors and building cleaners |
44 | Female | White | Married, spouse present | Employed | Advertising and promotions managers |
sampled_data %>% select(ind, educ, fullpart, incwage, Frequency) %>% flextable() %>% set_caption("10 dòng ngẫu nhiên trong bảng tần suất (Phần 2: Ngành, học vấn và thu nhập)") %>% autofit()
ind | educ | fullpart | incwage | Frequency |
|---|---|---|---|---|
Computer systems design and related services | Graduate degree | Full-time | 0 | 1 |
N/A (not applicable) | Some college | Part-time | 30,000 | 1 |
Computer systems design and related services | Graduate degree | Full-time | 40,000 | 1 |
Construction | Some college | Full-time | 0 | 1 |
Colleges and universities, including junior colleges | Some college | Not working | 0 | 1 |
Justice, public order, and safety activities | Some college | Full-time | 60,000 | 1 |
Construction | Graduate degree | Full-time | 48,000 | 1 |
Restaurants and other food services | Associate degree | Full-time | 46,000 | 1 |
Services to buildings and dwellings (except cleaning during construction and immediately after construction) | Some college | Full-time | 45,000 | 1 |
Advertising and related services | Graduate degree | Full-time | 50,000 | 1 |
#Tần suất trùng lặp cao nhất và thấp nhất
max_freq <- max(duplicated_summary$Frequency)
min_freq <- min(duplicated_summary$Frequency)
cat(paste("Tần suất trùng lặp cao nhất là:", max_freq, "\n"))
Tần suất trùng lặp cao nhất là: 906
cat(paste("Tần suất trùng lặp thấp nhất là:", min_freq, "\n"))
Tần suất trùng lặp thấp nhất là: 1
Một khía cạnh khác của việc kiểm tra chất lượng dữ liệu là xác định sự tồn tại của các bản ghi trùng lặp. Hàm sum(duplicated(df)) được sử dụng để đếm số dòng có nội dung giống hệt nhau. Kết quả cho thấy một con số đáng kể có đến 65820 dòng bị trùng lặp. Để hiểu rõ hơn bản chất của sự trùng lặp này, một bảng tần suất (duplicated_summary) đã được tạo ra bằng cách nhóm tất cả các biến và đếm số lần xuất hiện của mỗi tổ hợp giá trị duy nhất. Kết quả cho thấy tần suất trùng lặp cao nhất chỉ là 906 lần và tần suất thấp nhất là 1, nghĩa là có rất nhiều dòng dữ liệu là duy nhất. Trong bối cảnh dữ liệu khảo sát xã hội với các biến định tính có số lượng giá trị giới hạn và nhiều quan sát có cùng đặc điểm (ví dụ: không có thu nhập), sự trùng lặp này được đánh giá là sự trùng hợp tự nhiên của các đặc điểm chứ không phải lỗi nhập liệu. Do đó, quyết định giữ lại toàn bộ dữ liệu được đưa ra để bảo toàn tính toàn vẹn và cấu trúc của mẫu khảo sát gốc.
colnames(df)
[1] "age" "sex" "race" "marst" "empstat" "occ"
[7] "ind" "educ" "fullpart" "incwage"
Hàm colnames(df) được sử dụng để trích xuất và hiển thị một vector chứa tên của tất cả các biến trong bộ dữ liệu. Thao tác này cung cấp một danh sách tham khảo nhanh chóng, giúp xác nhận rằng quá trình chuẩn hóa tên biến ở các bước trước đã thành công và cung cấp tên gọi chính xác để sử dụng trong các câu lệnh phân tích sau này.
sapply(df, function(x) length(unique(x)))
age sex race marst empstat occ ind educ
82 2 6 6 4 527 264 5
fullpart incwage
3 3537
Để hiểu rõ hơn về đặc tính của từng biến, hàm sapply() đã được áp dụng để đếm số lượng giá trị duy nhất trong mỗi cột. Về mặt kỹ thuật, sapply() lặp qua từng cột của dataframe df và áp dụng hàm length(unique(x)) lên nó. Kết quả cho thấy các biến như sex (2 giá trị), race (6 giá trị), empstat (4 giá trị) có số lượng giá trị duy nhất thấp, khẳng định chúng là các biến định tính (phân loại). Ngược lại, các biến như occ (nghề nghiệp, 527 giá trị) và incwage (thu nhập, 3537 giá trị) có độ đa dạng rất cao. Con số này giúp định hướng cho các bước xử lý tiếp theo.
Trong phần này, các phép biến đổi được thực hiện và tạo ra các biến mới từ dữ liệu gốc để phục vụ cho các câu hỏi phân tích sâu hơn. Sau mỗi thao tác, một phần kết quả sẽ được hiển thị để minh họa cho sự thay đổi.
df <- df %>% mutate(across(where(is.character), as.factor))
# Hiển thị kiểu dữ liệu của một vài cột để kiểm tra
glimpse(df %>% select(sex, race, marst))
Rows: 146,133
Columns: 3
$ sex <fct> Female, Female, Female, Male, Female, Male, Female, Female, Male…
$ race <fct> White, White, White, White, White, White, White, White, White, W…
$ marst <fct> "Separated", "Separated", "Married, spouse present", "Married, s…
Dựa trên phát hiện từ bước khám phá dữ liệu, chuyển đổi các biến định
tính từ kiểu ký tự (chr) sang kiểu factor là một thao tác tiền xử lý
quan trọng đã được thực hiện. Câu lệnh
mutate(across(where(is.character), as.factor)) được áp dụng để tự động
tìm tất cả các cột có kiểu dữ liệu là ký tự và ép kiểu chúng thành
factor. Kết quả từ hàm glimpse() đã xác nhận sự thay đổi này, khi các
biến sex, race, marst giờ đây được hiển thị với kiểu
df <- df %>% mutate(educ_group = case_when(educ %in% c("Less than high school", "Elementary school") ~ "Thấp", educ %in% c("High school graduate", "Some college") ~ "Trung bình", educ %in% c("Associate degree", "Bachelor's degree", "Graduate degree") ~ "Cao", TRUE ~ "Không xác định"))
# Hiển thị cột mới và cột gốc để so sánh
df %>% select(educ, educ_group) %>% head() %>% flextable() %>% set_caption("So sánh biến 'educ' gốc và 'educ_group' mới") %>% autofit()
educ | educ_group |
|---|---|
Some college | Trung bình |
Some college | Trung bình |
Some college | Trung bình |
Some college | Trung bình |
Some college | Trung bình |
Some college | Trung bình |
Để phục vụ cho các phân tích so sánh một cách hiệu quả, biến educ với nhiều cấp độ chi tiết đã được mã hóa thành một biến nhóm mới là educ_group. Hàm case_when() được sử dụng để tạo ra các điều kiện logic, gom các giá trị học vấn gốc thành ba nhóm chính có ý nghĩa kinh tế - xã hội rõ ràng hơn: “thấp”, “trung bình” và “cao”. Việc tạo ra một biến nhóm mới thay vì ghi đè lên biến cũ giúp bảo toàn dữ liệu gốc. Bảng so sánh kết quả cho thấymột vài giá trị “some college” từ biến educ đã được ánh xạ chính xác thành giá trị “trung bình” trong biến educ_group, xác nhận rằng quá trình mã hóa đã thành công. Thao tác này giúp đơn giản hóa việc phân tích và làm cho các xu hướng liên quan đến học vấn trở nên rõ ràng và dễ diễn giải hơn.
df$educ_group <- factor(df$educ_group, levels = c("Thấp", "Trung bình", "Cao"))
# Hiển thị tần số của các nhóm tuổi mới tạo
levels(df$educ_group)
[1] "Thấp" "Trung bình" "Cao"
Mặc định, R sẽ sắp xếp các cấp độ của một biến factor theo thứ tự alphabet. Để thay đổi điều này, hàm factor() được tái sử dụng với tham số levels được chỉ định rõ ràng là c(“thấp”, “trung bình”, “cao”). Thao tác này ghi đè lên thứ tự mặc định và thiết lập một trật tự logic, có thứ bậc cho biến học vấn. Kết quả từ hàm levels() sau đó đã xác nhận rằng trật tự mới đã thành công. Điều này đảm bảo rằng tất cả các bảng biểu và biểu đồ được tạo ra ở các phần sau sẽ hiển thị các nhóm học vấn theo đúng trình tự tăng dần, giúp việc so sánh và diễn giải kết quả trở nên trực quan và chính xác hơn.
df <- df %>% mutate(age_group = cut(age, breaks = c(0, 18, 30, 45, 60, Inf), labels = c("Dưới 18", "18-30", "31-45", "46-60", "Trên 60"), right = FALSE))
# Hiển thị tần số của các nhóm tuổi mới tạo
table(df$age_group) %>% as.data.frame() %>% rename("Nhóm Tuổi" = Var1, "Số lượng" = Freq) %>% flextable() %>% set_caption("Tần số của các nhóm tuổi") %>% autofit()
Nhóm Tuổi | Số lượng |
|---|---|
Dưới 18 | 36,125 |
18-30 | 19,768 |
31-45 | 30,005 |
46-60 | 26,390 |
Trên 60 | 33,845 |
Tương tự như biến học vấn, biến tuổi (age) cũng được mã hóa thành biến nhóm age_group.Hàm cut() được sử dụng để phân chia biến tuổi liên tục thành các khoảng rời rạc, tương ứng với các giai đoạn khác nhau trong cuộc đời và sự nghiệp. Tham số breaks định nghĩa các điểm cắt và labels gán nhãn cho từng khoảng. Bảng tần số kết quả cho thấy sự phân bổ của các quan sát vào từng nhóm tuổi. Đáng chú ý, nhóm “dưới 18” chiếm số lượng lớn nhất (36,125), phản ánh một phần đáng kể của mẫu khảo sát là những người chưa đến tuổi lao động. Các nhóm tuổi lao động chính như “31-45” và “trên 60” cũng có số lượng quan sát cao. Việc tạo ra biến nhóm cho phép thực hiện các phân tích so sánh và tổng hợp dữ liệu giúp làm nổi bật các xu hướng nhân khẩu học một cách rõ ràng hơn.
df_with_income <- df %>% filter(incwage > 0)
# Hiển thị kích thước của dataframe mới
cat(paste("Số người có thu nhập > 0 là:", nrow(df_with_income)))
Số người có thu nhập > 0 là: 69148
Tập dữ liệu con tên là df_with_income đã được tạo ra phục vụ cho các phân tích chuyên sâu về thu nhập. Hàm filter(incwage > 0) được sử dụng để lọc và chỉ giữ lại những quan sát có giá trị thu nhập từ lương lớn hơn 0. Kết quả từ nrow() cho thấy có 69,148 cá nhân trong tổng số 146,133 quan sát là có thu nhập. Việc tạo ra tập dữ liệu con này giúp đảm bảo các phân tích về sau chỉ tập trung vào đúng đối tượng những người đang có thu nhập, từ đó tránh được sự sai lệch do việc bao gồm những người có thu nhập bằng 0.
df_employed <- df %>% filter(empstat == "Employed")
# Hiển thị kích thước của dataframe mới
cat(paste("Số người đang có việc làm là:", nrow(df_employed)))
Số người đang có việc làm là: 69160
Tương tự việc tạo tập dữ liệu cho người có thu nhập, một tập dữ liệu con khác là df_employed đã được tạo ra để phục vụ các phân tích liên quan đến nhóm dân số đang có việc làm. Về mặt kỹ thuật, hàm filter(empstat == “employed”) được sử dụng để trích xuất tất cả các quan sát có tình trạng việc làm là “employed”. Kết quả cho thấy có 69,160 người trong mẫu đang có việc làm.
df <- df %>% mutate(has_income = ifelse(incwage > 0, "Có thu nhập", "Không có thu nhập"))
# Hiển thị tần số của biến mới
table(df$has_income) %>% as.data.frame() %>% rename("Trạng thái" = Var1, "Số lượng" = Freq) %>% flextable() %>% set_caption("Tần số biến có thu nhập") %>% autofit()
Trạng thái | Số lượng |
|---|---|
Có thu nhập | 69,148 |
Không có thu nhập | 76,985 |
Để đơn giản hóa việc phân loại các quan sát dựa trên tình trạng thu nhập, biến nhị phân mới tên là has_income đã được tạo ra. Hàm ifelse() được sử dụng để kiểm tra một điều kiện logic: nếu giá trị incwage lớn hơn 0, biến mới sẽ nhận giá trị “có thu nhập”; ngược lại, nó sẽ nhận giá trị “không có thu nhập”. Bảng tần số cho thấy sự phân bổ của mẫu khảo sát thành hai nhóm với 69,148 người có thu nhập và 76,985 người không có thu nhập.
top_5_occ <- df %>% count(occ, sort = TRUE) %>% head(5) %>% pull(occ)
# In ra danh sách top 5 nghề nghiệp
top_5_occ
[1] Not in universe
[2] Managers, all other
[3] Elementary and middle school teachers
[4] Driver/sales workers and truck drivers
[5] Registered nurses
527 Levels: Accountants and auditors Actors Actuaries ... Writers and authors
Vector chứa 5 nghề phổ biến nhất đã được tạo ra để xử lý biến nghề nghiệp (occ) vốn có độ đa dạng cao. Kỹ thuật này sử dụng chuỗi lệnh count(), head() và pull() để đếm tần suất, chọn ra 5 nghề đứng đầu và lưu tên của chúng vào biến top_5_occ. Danh sách này, bao gồm các nghề như quản lý, giáo viên và y tá, sẽ được sử dụng như một bộ lọc hiệu quả trong các phân tích và trực quan hóa tập trung ở các phần sau.
df_subset <- df %>% select(age, sex, educ_group, empstat, incwage)
# Hiển thị 6 dòng đầu của dataframe con mới
head(df_subset) %>% flextable() %>% set_caption("6 dòng đầu của dataframe con 'df_subset'") %>% autofit()
age | sex | educ_group | empstat | incwage |
|---|---|---|---|---|
66 | Female | Trung bình | Not in labor force | 0 |
68 | Female | Trung bình | Not in labor force | 0 |
52 | Female | Trung bình | Not in labor force | 0 |
51 | Male | Trung bình | Employed | 42,000 |
78 | Female | Trung bình | Not in labor force | 0 |
65 | Male | Trung bình | Not in labor force | 55,000 |
Hàm select() được sử dụng để tạo ra một dataframe con (df_subset) chỉ chứa các biến được xác định là trọng tâm cho các câu hỏi phân tích chính của tiểu luận. Bảng kết quả hiển thị 6 dòng đầu của dataframe mới này, xác nhận rằng chỉ có các biến age, sex, educ_group, empstat và incwage được giữ lại. Việc tạo ra tập dữ liệu con chuyên biệt này không chỉ là một thao tác kỹ thuật để có một phiên bản dữ liệu gọn nhẹ hơn, mà còn là một bước thể hiện định hướng phân tích, tập trung vào việc khám phá mối quan hệ giữa các đặc điểm nhân khẩu học cốt lõi với tình trạng việc làm và thu nhập.
df_with_income <- df_with_income %>% mutate(log_income = log1p(incwage))
# Hiển thị cột thu nhập gốc và cột thu nhập logarit hóa để so sánh
df_with_income %>% select(incwage, log_income) %>% head() %>% flextable() %>% colformat_double(digits = 2) %>% set_caption("So sánh 'incwage' và 'log_income'") %>% autofit()
incwage | log_income |
|---|---|
42,000.00 | 10.65 |
55,000.00 | 10.92 |
52,000.00 | 10.86 |
22,000.00 | 10.00 |
45,000.00 | 10.71 |
70,000.00 | 11.16 |
Để xử lý tính chất phân phối lệch phải của dữ liệu thu nhập, phép biến đổi logarit được thực hiện bằng hàm log1p() để tạo ra biến log_income. Bảng kết quả so sánh đã minh họa rõ sự thay đổi này: một giá trị thu nhập lớn như 42,000 được chuyển đổi thành 10.65, trong khi 70,000 trở thành 11.16. Kết quả này cho thấy phép biến đổi đã thành công trong việc nén khoảng giá trị của biến thu nhập, đưa các giá trị về một thang đo nhỏ và ít chênh lệch hơn. Ý nghĩa của kỹ thuật này là để giảm thiểu tác động của các giá trị ngoại lai (những người có thu nhập rất cao), giúp phân phối của biến trở nên đối xứng hơn, một điều kiện thuận lợi và đôi khi là bắt buộc cho nhiều mô hình phân tích thống kê.
Để hiểu về lực lượng lao động, trước tiên chúng ta cần trả lời câu hỏi “Họ là ai?”. Mục này sẽ vẽ nên chân dung nhân khẩu học của mẫu dữ liệu, tập trung vào các đặc điểm cơ bản nhất: độ tuổi, cấu trúc thế hệ, giới tính, và các đặc điểm xã hội khác. Việc hiểu rõ những đặc điểm này là nền tảng để phân tích sâu hơn về việc làm và thu nhập ở các mục sau.
Tuổi là một trong những yếu tố nhân khẩu học quan trọng nhất, phản ánh kinh nghiệm, giai đoạn sự nghiệp và thế hệ của người lao động. Phần này sẽ khám phá phân bố độ tuổi tổng thể và xem xét mối liên hệ của nó với trình độ học vấn và giới tính.
# Thống kê mô tả cho tuổi
df %>% summarise("Tuổi trung bình" = mean(age), "Tuổi trung vị" = median(age), "Độ lệch chuẩn" = sd(age), "Tuổi nhỏ nhất" = min(age), "Tuổi lớn nhất" = max(age)) %>% pivot_longer(everything(), names_to = "Chỉ số", values_to = "Giá trị") %>% flextable() %>% colformat_double(digits = 1) %>% set_caption("Bảng 1: Thống kê mô tả về tuổi") %>% autofit()
Chỉ số | Giá trị |
|---|---|
Tuổi trung bình | 38.7 |
Tuổi trung vị | 38.0 |
Độ lệch chuẩn | 23.3 |
Tuổi nhỏ nhất | 0.0 |
Tuổi lớn nhất | 85.0 |
Bảng 1 tóm tắt các chỉ số thống kê mô tả chính của biến tuổi được tính toán thông qua hàm summarise() và trình bày một cách rõ ràng bằng flextable(). Kết quả cho thấy một điểm đáng chú ý: tuổi trung bình của mẫu là 38.7 tuổi, gần như tương đồng với tuổi trung vị là 38.0 tuổi. Khoảng cách gần nhau giữa hai chỉ số đo lường trung tâm này hàm ý rằng phân phối tuổi trong mẫu khảo sát là tương đối đối xứng, không bị lệch nhiều về phía các nhóm tuổi trẻ hay già. Khoảng tuổi rộng từ 0 đến 85 cũng cho thấy tính bao quát của bộ dữ liệu, bao gồm cả những người chưa đến và đã qua tuổi lao động, cung cấp một bối cảnh toàn diện cho các phân tích về sau.
# Biểu đồ 1: Phân phối tuổi của lực lượng lao động
ggplot(df, aes(x = age)) + geom_histogram(binwidth = 5, fill = "skyblue", color = "white") + geom_vline(aes(xintercept = mean(age)), color = "red", linetype = "dashed", size = 1) + labs(title = "Biểu đồ 1: Phân phối tuổi của lực lượng lao động", subtitle = "Đường nét đứt màu đỏ thể hiện tuổi trung bình", x = "Tuổi", y = "Số lượng") + theme_minimal()
Để trực quan hóa sự phân bố của biến tuổi, một biểu đồ tần suất (histogram) đã được xây dựng bằng hàm geom_histogram() từ gói ggplot2. Biểu đồ này củng cố cho nhận định từ bảng thống kê mô tả trước đó, cho thấy một phân phối tương đối đối xứng với hai đỉnh nhỏ: một ở nhóm tuổi rất trẻ (dưới 20) và một ở giai đoạn lao động chính (khoảng 30-55 tuổi). Đường nét đứt màu đỏ, được thêm vào bằng geom_vline(), biểu diễn giá trị tuổi trung bình (38.7 tuổi), nằm gần trung tâm của phân phối. Việc biểu đồ có số lượng quan sát đáng kể ở cả hai phía của độ tuổi lao động cho thấy sự đa dạng của mẫu, bao gồm cả những người đang đi học, đang làm việc và đã nghỉ hưu, cung cấp một bối cảnh rộng cho các phân tích sâu hơn về lực lượng lao động.
# Bảng tần số các nhóm tuổi
table(df$age_group) %>% as.data.frame() %>% rename("Nhóm Tuổi" = Var1, "Số lượng" = Freq) %>% flextable() %>% set_caption("Bảng 2: Tần suất các nhóm tuổi") %>% autofit()
Nhóm Tuổi | Số lượng |
|---|---|
Dưới 18 | 36,125 |
18-30 | 19,768 |
31-45 | 30,005 |
46-60 | 26,390 |
Trên 60 | 33,845 |
Câu hỏi đặt ra là các cá nhân trong mẫu khảo sát phân bổ như thế nào theo các giai đoạn cuộc đời khác nhau. Để trả lời câu hỏi này, một bảng tần suất cho biến age_group đã được tạo ra bằng hàm table(). Kết quả trong bảng 2 cho thấy một sự phân bổ không đồng đều. Nhóm “dưới 18” có số lượng đông nhất với 36,125 quan sát, điều này có thể lý giải vì nhóm này bao gồm cả những người đang đi học và chưa tham gia lực lượng lao động. Nhóm tuổi lao động nòng cốt “31-45” và nhóm “trên 60” cũng chiếm tỷ trọng đáng kể.
# Biểu đồ 2: Số lượng người theo từng nhóm tuổi
ggplot(df, aes(x = age_group)) + geom_bar(fill = "steelblue") + geom_text(stat='count', aes(label=..count..), vjust=-0.5) + labs(title = "Biểu đồ 2: Số lượng người theo từng nhóm tuổi", x = "Nhóm tuổi", y = "Số lượng") + theme_minimal()
Biểu đồ 2 cung cấp một cái nhìn trực quan về sự phân bổ số lượng cá nhân theo từng nhóm tuổi đã được mã hóa. Bằng việc sử dụng hình học cột (geom_bar), chiều cao của mỗi cột đại diện cho tổng số người trong nhóm tương ứng với con số cụ thể được ghi rõ trên đỉnh cột nhờ lớp geom_text(). Biểu đồ cho thấy một sự phân bổ không đồng đều, với nhóm “dưới 18” chiếm số lượng cao nhất (36,125 người). Đáng chú ý, nhóm tuổi trẻ nhất trong lực lượng lao động (“18-30”) lại có số lượng thấp nhất, trong khi các nhóm tuổi trung niên và lớn tuổi hơn lại có quy mô lớn hơn. Sự trực quan hóa này giúp nhanh chóng nắm bắt được cấu trúc nhân khẩu học theo thế hệ của mẫu khảo sát.
# Biểu đồ 3: Phân phối tuổi theo từng nhóm học vấn
ggplot(df, aes(x = educ_group, y = age, fill = educ_group)) + geom_violin(trim = FALSE) + stat_summary(fun = "median", geom = "point", size = 3, color = "white") + theme(legend.position = "none") + labs(title = "Biểu đồ 3: Phân phối tuổi theo từng nhóm học vấn", subtitle = "Chấm trắng thể hiện độ tuổi trung vị của mỗi nhóm", x = "Nhóm học vấn", y = "Tuổi")
Biểu đồ 3 khám phá sâu hơn về mối liên hệ giữa tuổi và trình độ học vấn bằng cách sử dụng biểu đồ violin. Về mặt kỹ thuật, geom_violin() được dùng để vẽ hình dạng phân phối mật độ của tuổi cho mỗi nhóm học vấn, với phần thân violin rộng hơn ở những độ tuổi có nhiều người hơn. Lớp stat_summary() được thêm vào để đánh dấu tuổi trung vị của mỗi nhóm bằng một chấm trắng. Kết quả trực quan cho thấy một xu hướng rõ ràng: tuổi trung vị tăng dần theo trình độ học vấn, từ nhóm “thấp” đến nhóm “cao”. Hình dạng của violin cũng cho thấy phân phối tuổi của nhóm “thấp” tập trung ở độ tuổi trẻ, trong khi nhóm “cao” có phân phối rộng hơn và nghiêng về phía tuổi lớn hơn. Điều này là một kết quả hợp lý, phản ánh thực tế rằng những người có trình độ học vấn cao hơn cần nhiều thời gian hơn để hoàn thành quá trình học tập của mình.
# Biểu đồ 4: Số lượng người theo nhóm tuổi và giới tính
ggplot(df, aes(x = age_group, fill = sex)) + geom_bar(position = "dodge") + labs(title = "Biểu đồ 4: Phân bổ nhân khẩu học theo tuổi và giới tính", x = "Nhóm tuổi", y = "Số lượng", fill = "Giới tính") + theme_minimal() + scale_fill_brewer(palette = "Pastel1")
Để khám phá sự tương tác giữa hai biến nhân khẩu học quan trọng là tuổi và giới tính, một biểu đồ cột nhóm đã được xây dựng bằng geom_bar(). Bằng cách đặt các cột đại diện cho nam và nữ cạnh nhau (position = “dodge”) trong mỗi nhóm tuổi, biểu đồ 4 cho phép so sánh trực quan số lượng của hai giới. Kết quả cho thấy một sự cân bằng tương đối về giới tính trong hầu hết các giai đoạn cuộc đời được khảo sát. Điều này củng cố thêm độ tin cậy của mẫu dữ liệu, cho thấy nó không bị thiên lệch quá nhiều về một giới tính cụ thể nào. Một quan sát thú vị là ở nhóm tuổi cao nhất (“trên 60”), số lượng nữ giới có phần nhỉnh hơn, phù hợp với xu hướng nhân khẩu học chung về tuổi thọ cao hơn ở nữ giới.
# Tuổi trung bình theo tình trạng việc làm
df %>% group_by(empstat) %>% summarise("Tuổi trung bình" = mean(age)) %>% flextable() %>% colformat_double(digits = 1) %>% set_caption("Bảng 3: Tuổi trung bình theo tình trạng việc làm") %>% autofit()
empstat | Tuổi trung bình |
|---|---|
Employed | 42.5 |
NIU | 7.5 |
Not in labor force | 53.5 |
Unemployed | 37.6 |
Một kết quả đáng chú ý từ bảng 3 là nhóm “unemployed” (thất nghiệp) có độ tuổi trung bình (37.6 tuổi) thấp hơn đáng kể so với nhóm “employed” (có việc làm, 42.5 tuổi). Sự chênh lệch này gợi ý rằng rủi ro thất nghiệp có thể tập trung nhiều hơn ở những người lao động trẻ tuổi. Bảng thống kê này được tạo ra bằng cách nhóm dữ liệu theo biến empstat và sau đó tính tuổi trung bình cho mỗi nhóm bằng hàm summarise(). Ngoài ra, kết quả cũng cho thấy nhóm “not in labor force” có tuổi trung bình cao nhất (53.5), phù hợp với dự đoán rằng nhóm này bao gồm một phần lớn những người đã đến tuổi nghỉ hưu.
# Tuổi trung vị theo tình trạng việc làm
df %>% group_by(empstat) %>% summarise("Tuổi trung vị" = median(age)) %>% flextable() %>% colformat_double(digits = 1) %>% set_caption("Bảng 4: Tuổi trung vị theo tình trạng việc làm") %>% autofit()
empstat | Tuổi trung vị |
|---|---|
Employed | 42.0 |
NIU | 8.0 |
Not in labor force | 62.0 |
Unemployed | 35.0 |
Bảng 4 cung cấp thêm một góc nhìn về mối liên hệ giữa tuổi và tình trạng việc làm thông qua chỉ số trung vị, được tính bằng hàm median(). Tuổi trung vị cho biết mức tuổi mà ở đó một nửa số người trong nhóm trẻ hơn và một nửa già hơn. Kết quả cho thấy một nửa số người thất nghiệp (unemployed) có độ tuổi từ 35 trở xuống, trong khi một nửa số người có việc làm (employed) có độ tuổi từ 42 trở lên. Sự chênh lệch này một lần nữa nhấn mạnh rằng nhóm thất nghiệp có xu hướng trẻ hơn. Việc sử dụng trung vị thay vì trung bình giúp khẳng định rằng kết luận này không bị ảnh hưởng bởi một vài cá nhân rất già hoặc rất trẻ trong từng nhóm.
Bên cạnh tuổi, các yếu tố như giới tính, tình trạng hôn nhân cũng đóng vai trò quan trọng trong việc định hình chân dung lực lượng lao động. Mục này sẽ đi vào chi tiết các yếu tố đó.
# Bảng tỷ lệ Nam/Nữ
prop.table(table(df$sex)) %>% as.data.frame() %>% rename("Giới tính" = Var1, "Tỷ lệ" = Freq) %>% flextable() %>% colformat_double(digits = 3) %>% set_caption("Bảng 5: Tỷ lệ nam/nữ trong mẫu") %>% autofit()
Giới tính | Tỷ lệ |
|---|---|
Female | 0.512 |
Male | 0.488 |
Để phác họa chân dung lực lượng lao động, việc xem xét cấu trúc giới tính là một bước cơ bản. Bảng 5 trình bày tỷ lệ nam và nữ trong mẫu khảo sát, được tính toán bằng cách áp dụng hàm prop.table() lên kết quả tần suất của table(df$sex). Kết quả cho thấy một sự cân bằng đáng kể về giới, với nữ giới chiếm khoảng 51.2% và nam giới chiếm 48.8%. Tỷ lệ gần như 50-50 này là một chỉ báo tốt về tính đại diện của bộ dữ liệu, cho thấy mẫu khảo sát không bị thiên lệch nghiêm trọng về một giới tính cụ thể nào, từ đó làm tăng độ tin cậy cho các phân tích so sánh theo giới ở các phần sau.
# Biểu đồ 5: Tỷ lệ phân bổ theo giới tính
df %>% count(sex) %>% mutate(pct = n / sum(n)) %>% ggplot(aes(x = "", y = pct, fill = sex)) + geom_col(width = 1) + coord_polar(theta = "y") + theme_void() + geom_text(aes(label = percent(pct, accuracy = 0.1)), position = position_stack(vjust = 0.5)) + labs(title = "Biểu đồ 5: Tỷ lệ phân bổ theo giới tính", fill = "Giới tính")
Để trực quan hóa tỷ lệ phân bổ giới tính đã tính ở bảng trước, một biểu đồ tròn đã được tạo ra bằng ggplot2. Dữ liệu đầu tiên được tóm tắt bằng count() và mutate() để tính toán tỷ lệ phần trăm (pct) cho mỗi giới. Sau đó, geom_col() và coord_polar() được kết hợp để vẽ biểu đồ tròn, một kỹ thuật phổ biến để thể hiện cơ cấu. Lớp geom_text() được thêm vào để hiển thị trực tiếp tỷ lệ phần trăm trên mỗi phần của biểu đồ. Biểu đồ 5 xác nhận lại một cách trực quan sự cân bằng về giới tính trong mẫu, với nữ giới chiếm 51.2% và nam giới chiếm 48.8%, không có sự chênh lệch quá lớn giữa hai nhóm.
# Biểu đồ 6: Heatmap giữa tình trạng hôn nhân và nhóm tuổi
df %>% count(marst, age_group) %>% ggplot(aes(x = age_group, y = marst, fill = n)) + geom_tile(color = "white") + scale_fill_viridis_c(labels = comma) + labs(title = "Biểu đồ 6: Mối quan hệ giữa tuổi và tình trạng hôn nhân", x = "Nhóm tuổi", y = "Tình trạng hôn nhân", fill = "Số lượng") + theme_minimal()
Heatmap (Biểu đồ 6) đã được sử dụng để trực quan hóa mối quan hệ giữa tuổi tác và tình trạng hôn nhân. Màu sắc của các ô thể hiện số lượng người trong từng sự kết hợp giữa nhóm tuổi và tình trạng hôn nhân. Quan sát cho thấy sự chuyển biến rõ rệt theo độ tuổi: những người trẻ tuổi thường độc thân, trong khi tỷ lệ người đã kết hôn có xu hướng tăng dần theo thời gian. Ngoài ra, cũng có thể nhận thấy sự hiện diện của các nhóm khác (ví dụ: ly hôn, góa bụa) ở các nhóm tuổi khác nhau, cho thấy sự đa dạng của các giai đoạn trong cuộc sống.
# Bảng chéo tình trạng hôn nhân và giới tính
table(df$sex, df$marst) %>% as.data.frame() %>% rename("Giới tính" = Var1, "Tình trạng Hôn nhân" = Var2, "Số lượng" = Freq) %>% flextable() %>% set_caption("Bảng 6: Bảng chéo giới tính và tình trạng hôn nhân") %>% autofit()
Giới tính | Tình trạng Hôn nhân | Số lượng |
|---|---|---|
Female | Divorced | 6,606 |
Male | Divorced | 4,479 |
Female | Married, spouse absent | 835 |
Male | Married, spouse absent | 807 |
Female | Married, spouse present | 29,338 |
Male | Married, spouse present | 29,298 |
Female | Separated | 4,946 |
Male | Separated | 1,618 |
Female | Single | 31,921 |
Male | Single | 34,302 |
Female | Widowed | 1,188 |
Male | Widowed | 795 |
Để lượng hóa mối quan hệ giữa giới tính và tình trạng hôn nhân, một bảng chéo (cross-tabulation) đã được tạo ra bằng hàm table(). Bảng 6 trình bày số lượng cụ thể của nam và nữ trong từng nhóm tình trạng hôn nhân. Kết quả cho thấy số lượng nam và nữ ở trạng thái “married, spouse present” (đã kết hôn, có vợ/chồng) là gần như tương đương nhau. Tuy nhiên, có sự khác biệt đáng kể ở các nhóm khác: số lượng nữ giới trong các nhóm “divorced” (ly hôn), “separated” (ly thân) và “widowed” (góa bụa) đều cao hơn so với nam giới. Ngược lại, số lượng nam giới ở trạng thái “single” (độc thân) lại cao hơn một chút so với nữ giới. Những con số này cung cấp bằng chứng định lượng về sự khác biệt trong mô hình hôn nhân và gia đình giữa hai giới trong mẫu khảo sát.
Sau khi đã hiểu “họ là ai” qua các đặc điểm nhân khẩu học, mục này sẽ tập trung vào câu hỏi “họ làm gì?”. Chúng ta sẽ phân tích bức tranh tổng thể về tình trạng việc làm (có việc, thất nghiệp, hay không tham gia lao động), khám phá các yếu tố có thể ảnh hưởng đến cơ hội việc làm, và cuối cùng là xác định những ngành nghề phổ biến nhất trong mẫu dữ liệu.
Đầu tiên, chúng ta cần có một cái nhìn tổng quan về sự phân bổ của lực lượng lao động theo các trạng thái việc làm khác nhau.
# Bảng tần suất tình trạng việc làm
table(df$empstat) %>% as.data.frame() %>% rename("Tình trạng việc làm" = Var1, "Số lượng" = Freq) %>% flextable() %>% set_caption("Bảng 7: Tần suất các tình trạng việc làm") %>% autofit()
Tình trạng việc làm | Số lượng |
|---|---|
Employed | 69,160 |
NIU | 29,483 |
Not in labor force | 45,018 |
Unemployed | 2,472 |
Một câu hỏi cơ bản khi phân tích lực lượng lao động là tỷ lệ có việc làm, thất nghiệp và không tham gia lao động là bao nhiêu. Bảng 7 được tạo ra bằng cách tính tần suất của biến empstat, đã lượng hóa sự phân bổ này. Kết quả cho thấy trong tổng số các quan sát, nhóm “employed” (có việc làm) chiếm số lượng lớn nhất (69,160 người). Đáng chú ý, số người “not in labor force” (không tham gia lực lượng lao động) cũng rất cao (45,018 người), cao hơn nhiều so với nhóm “unemployed” (thất nghiệp). Điều này cho thấy một phần đáng kể dân số trong mẫu khảo sát không có việc làm không phải vì họ đang tìm việc mà không được mà vì họ ở ngoài lực lượng lao động (ví dụ: sinh viên, nội trợ, người về hưu).
# Biểu đồ 7: Tần suất tình trạng việc làm
# Dùng fct_infreq() để tự động sắp xếp các cột theo thứ tự giảm dần
ggplot(df, aes(x = fct_infreq(empstat))) + geom_bar(fill = "coral") + geom_text(stat='count', aes(label=..count..), vjust=-0.5) + labs(title = "Biểu đồ 7: Phân bố theo tình trạng việc làm", x = "Tình trạng việc làm", y = "Số lượng") + theme_minimal()
Biểu đồ 7 cung cấp một cái nhìn trực quan về sự phân bổ của lực lượng lao động theo các trạng thái việc làm khác nhau. Bằng cách sử dụng geom_bar để vẽ các cột có chiều cao tương ứng với số lượng người và hàm fct_infreq() để sắp xếp các cột theo thứ tự giảm dần, biểu đồ đã làm nổi bật quy mô của từng nhóm. Có thể thấy rõ, cột “employed” (có việc làm) cao vượt trội, cho thấy đây là nhóm chiếm đa số. Cột “not in labor force” (không tham gia lực lượng lao động) cũng chiếm một phần đáng kể, trong khi cột “unemployed” (thất nghiệp) lại rất thấp, cho thấy tỷ lệ thất nghiệp trong mẫu này là không cao. Việc trực quan hóa này giúp nhanh chóng nắm bắt được cấu trúc tổng thể của thị trường lao động trong mẫu khảo sát.
Một người có việc làm hay không có thể phụ thuộc vào nhiều yếu tố. Trong phần này,mục đích là điều tra mối quan hệ giữa tình trạng việc làm với trình độ học vấn và giới tính - hai yếu tố xã hội có ảnh hưởng lớn.
# Bảng chéo giữa Nhóm học vấn và Tình trạng việc làm
table(df$educ_group, df$empstat) %>% as.data.frame() %>% rename("Nhóm học vấn" = Var1, "Tình trạng việc làm" = Var2, "Số lượng" = Freq) %>% flextable() %>% set_caption("Bảng 8: Bảng chéo giữa học vấn và tình trạng việc làm") %>% autofit()
Nhóm học vấn | Tình trạng việc làm | Số lượng |
|---|---|---|
Thấp | Employed | 91 |
Trung bình | Employed | 29,064 |
Cao | Employed | 35,102 |
Thấp | NIU | 29,483 |
Trung bình | NIU | 0 |
Cao | NIU | 0 |
Thấp | Not in labor force | 232 |
Trung bình | Not in labor force | 21,488 |
Cao | Not in labor force | 13,346 |
Thấp | Unemployed | 7 |
Trung bình | Unemployed | 1,351 |
Cao | Unemployed | 735 |
Câu hỏi đặt ra là liệu trình độ học vấn có ảnh hưởng đến tình trạng việc làm hay không. Để trả lời, một bảng chéo đã được lập để thống kê số lượng người theo cả hai tiêu chí này. Các con số trong bảng 8 đã cho thấy một mối liên hệ rõ rệt. Đáng chú ý nhất là ở nhóm học vấn “cao”, số người có việc làm (35,102) cao hơn rất nhiều so với số người thất nghiệp (735). Trong khi đó, ở nhóm “trung bình”, mặc dù số người có việc làm vẫn chiếm đa số, nhưng số người thất nghiệp (1,351) lại cao gần gấp đôi so với nhóm “cao”. Sự khác biệt này cho thấy trình độ học vấn cao dường như là một lợi thế lớn trên thị trường lao động.
# Biểu đồ 8: Tỷ lệ tình trạng việc làm theo nhóm học vấn
ggplot(df, aes(y = educ_group, fill = empstat)) + geom_bar(position = "fill") + scale_x_continuous(labels = percent) + labs(title = "Biểu đồ 8: Tỷ lệ tình trạng việc làm theo nhóm học vấn", y = "Nhóm học vấn", x = "Tỷ lệ", fill = "Tình trạng việc làm") + theme_minimal()
Biểu đồ 8 đã làm sáng tỏ vai trò quan trọng của trình độ học vấn đối với cơ hội việc làm. Bằng cách sử dụng biểu đồ cột chồng tỷ lệ 100% (position = “fill”), chúng ta có thể so sánh một cách công bằng cơ cấu tình trạng việc làm giữa các nhóm học vấn. Điểm nổi bật nhất là sự khác biệt về tỷ lệ có việc làm (màu hồng) giữa các nhóm: tỷ lệ này tăng lên rõ rệt từ nhóm “trung bình” đến nhóm “cao”. Ở nhóm “cao”, phần màu hồng chiếm ưu thế áp đảo, cho thấy phần lớn những người có trình độ học vấn cao đều có việc làm. Ngược lại, tỷ lệ thất nghiệp (màu tím) ở nhóm “cao” là thấp nhất. Những kết quả trực quan này cung cấp một luận điểm vững chắc rằng việc nâng cao trình độ học vấn có mối tương quan thuận mạnh mẽ với khả năng có việc làm và giảm thiểu rủi ro thất nghiệp.
# Tỷ lệ thất nghiệp theo nhóm học vấn
df %>% group_by(educ_group) %>% summarise("Tỷ lệ thất nghiệp" = mean(empstat == "Unemployed")) %>% mutate("Tỷ lệ thất nghiệp (%)" = scales::percent(`Tỷ lệ thất nghiệp`, accuracy=0.1)) %>% flextable() %>% set_caption("Bảng 9: Tỷ lệ thất nghiệp theo nhóm học vấn") %>% autofit()
educ_group | Tỷ lệ thất nghiệp | Tỷ lệ thất nghiệp (%) |
|---|---|---|
Thấp | 0.0002347969 | 0.0% |
Trung bình | 0.0260293239 | 2.6% |
Cao | 0.0149441880 | 1.5% |
0.0248785611 | 2.5% |
Bảng 9 đóng vai trò bổ sung và làm rõ hơn cho biểu đồ 8 bằng cách tập trung vào một khía cạnh cụ thể là tỷ lệ thất nghiệp. Bằng cách áp dụng hàm summarise() để tính tỷ lệ thất nghiệp cho mỗi nhóm học vấn, bảng này cung cấp những con số chính xác đằng sau các thanh màu tím trong biểu đồ. Kết quả cho thấy tỷ lệ thất nghiệp ở nhóm học vấn “cao” chỉ là 1.5%, trong khi ở nhóm “trung bình” là 2.6%. Thao tác thống kê này giúp lượng hóa một cách rõ ràng mức độ rủi ro thất nghiệp mà mỗi nhóm phải đối mặt, khẳng định rằng trình độ học vấn cao hơn đi liền với sự an toàn hơn trên thị trường lao động.
# Tính và hiển thị bảng tỷ lệ có việc làm theo giới tính
df %>% group_by(sex) %>% summarise(ty_le_co_viec_lam = mean(empstat == "Employed")) %>% mutate(ty_le_co_viec_lam_pct = scales::percent(ty_le_co_viec_lam, accuracy = 0.1)) %>% flextable() %>% set_caption("Bảng 10: Tỷ lệ có việc làm theo giới tính") %>% autofit()
sex | ty_le_co_viec_lam | ty_le_co_viec_lam_pct |
|---|---|---|
Female | 0.4416709 | 44.2% |
Male | 0.5064307 | 50.6% |
Để lượng hóa cơ hội việc làm theo giới tính, bảng 10 đã tính toán tỷ lệ người có việc làm trong nội bộ từng nhóm. Bằng cách nhóm dữ liệu theo sex và áp dụng hàm summarise(), kết quả cho thấy 50.6% tổng số nam giới trong mẫu khảo sát đang có việc làm. Trong khi đó, con số này ở nữ giới là 44.2%. Sự chênh lệch hơn 6 điểm phần trăm này cho thấy nam giới trong mẫu có tỷ lệ tham gia vào thị trường lao động với tư cách là người có việc làm cao hơn so với nữ giới. Phép tính này không so sánh số lượng tuyệt đối mà tập trung vào tỷ lệ, cung cấp một góc nhìn sâu sắc hơn về sự khác biệt trong đặc điểm lao động giữa hai giới.
# Vẽ biểu đồ so sánh tỷ lệ
df %>% group_by(sex) %>% summarise(ty_le_co_viec_lam = mean(empstat == "Employed")) %>% ggplot(aes(x = sex, y = ty_le_co_viec_lam, fill = sex)) + geom_col() + scale_y_continuous(labels = percent) + geom_text(aes(label = percent(ty_le_co_viec_lam, accuracy = 0.1)), vjust = -0.5) + theme_minimal() + theme(legend.position = "none") + labs(title = "Biểu đồ 9: So sánh tỷ lệ có việc làm giữa hai giới tính", x = "Giới tính", y = "Tỷ lệ có việc làm")
Biểu đồ 9 cung cấp một sự so sánh trực diện về cơ hội việc làm giữa nam và nữ. Dữ liệu từ bảng 10 đã được trực quan hóa thành hai cột đơn giản, nơi chiều cao mỗi cột đại diện cho tỷ lệ người có việc làm trong nhóm giới tính tương ứng. Sự chênh lệch về chiều cao giữa hai cột là không thể nhầm lẫn: cột của nam giới ở mức 50.6% cao hơn rõ rệt so với cột của nữ giới ở mức 44.2%. Hình ảnh trực quan này không chỉ đơn thuần là sự biểu diễn lại con số, mà nó nhấn mạnh một cách mạnh mẽ về khoảng cách và sự khác biệt trong đặc điểm lao động giữa hai giới, là một tiền đề quan trọng cho các phân tích sâu hơn về bất bình đẳng giới.
Cuối cùng, chúng ta sẽ xem xét cụ thể hơn về các công việc mà lực lượng lao động đang làm.
# Tạo bảng 10 nghề nghiệp phổ biến nhất
top_10_occ_df <- df %>% filter(!occ %in% c("N/A", "Not in universe")) %>% count(occ, sort = TRUE) %>% head(10)
top_10_occ_df %>% rename("Nghề nghiệp" = occ, "Số lượng" = n) %>% flextable() %>% set_caption("Bảng 11: Top 10 nghề nghiệp phổ biến nhất") %>% autofit()
Nghề nghiệp | Số lượng |
|---|---|
Managers, all other | 2,510 |
Elementary and middle school teachers | 1,638 |
Driver/sales workers and truck drivers | 1,619 |
Registered nurses | 1,498 |
First-Line supervisors of retail sales workers | 1,324 |
Cashiers | 1,295 |
Customer service representatives | 1,273 |
Retail salespersons | 1,213 |
Janitors and building cleaners | 1,075 |
Construction laborers | 989 |
Bảng 11 phác họa một bức tranh về thị trường lao động trong mẫu khảo sát bằng cách liệt kê 10 ngành nghề có số lượng lao động đông đảo nhất. Sau khi loại bỏ các quan sát không có thông tin nghề nghiệp hợp lệ, hàm count() đã được sử dụng để thống kê và xếp hạng. Kết quả cho thấy sự đa dạng của các ngành nghề phổ biến, từ các vị trí quản lý (“managers, all other”) và chuyên môn cao (giáo viên, y tá) cho đến các công việc trong lĩnh vực dịch vụ bán lẻ và lao động phổ thông (tài xế, nhân viên vệ sinh). Sự hiện diện của nhiều loại hình công việc khác nhau trong top 10 cho thấy một cơ cấu lao động đa dạng, không tập trung quá mức vào một lĩnh vực cụ thể nào.
# Vẽ biểu đồ Top 10 nghề nghiệp
ggplot(top_10_occ_df, aes(x = reorder(occ, n), y = n)) + geom_col(fill = "darkcyan") + coord_flip() + geom_text(aes(label = n), hjust = -0.2, size = 3.5) + scale_y_continuous(expand = expansion(mult = c(0, 0.1))) + labs(title = "Biểu đồ 10: Top 10 nghề nghiệp phổ biến nhất", x = "Nghề nghiệp", y = "Số lượng") + theme_light()
Biểu đồ 10 cung cấp một sự so sánh trực quan về quy mô của 10 ngành nghề phổ biến nhất trong mẫu. Bằng cách sử dụng biểu đồ cột ngang (coord_flip()) và reorder(occ, n) để sắp xếp các cột theo thứ tự tăng dần về số lượng, tên của từng nghề nghiệp được hiển thị đầy đủ và độ dài của các thanh cho phép so sánh nhanh chóng số lượng lao động. Có thể thấy rõ, nhóm “managers, all other” có quy mô lớn nhất, gần gấp rưỡi so với nhóm đứng thứ hai là “elementary and middle school teachers”. Các nghề nghiệp còn lại trong top 10 có số lượng khá tương đồng với nhau. Hình ảnh trực quan này không chỉ minh họa cho bảng thống kê trước đó mà còn làm nổi bật sự chênh lệch về quy mô giữa nhóm nghề quản lý và các nhóm nghề nghiệp chuyên môn, dịch vụ khác.
Sau khi đã xác định “họ là ai” và “họ làm gì”, câu hỏi cuối cùng và quan trọng nhất là “họ kiếm được bao nhiêu?”. Mục này sẽ đi sâu phân tích về thu nhập, từ việc xem xét bức tranh phân phối tổng thể cho đến việc điều tra các yếu tố chính có khả năng ảnh hưởng đến mức lương của một người lao động, như giới tính, trình độ học vấn, tuổi tác và nghề nghiệp.
Trước khi so sánh thu nhập giữa các nhóm, chúng ta cần hiểu rõ đặc điểm phân phối thu nhập của toàn bộ những người có lương trong mẫu.
# Thống kê mô tả thu nhập cho những người có lương > 0
df_with_income <- df %>% filter(incwage > 0)
df_with_income %>% summarise("Thu nhập Trung bình" = mean(incwage), "Thu nhập Trung vị" = median(incwage), "Độ lệch chuẩn" = sd(incwage), "Thu nhập nhỏ nhất" = min(incwage), "Thu nhập lớn nhất" = max(incwage)) %>% pivot_longer(everything(), names_to = "Chỉ số", values_to = "Giá trị") %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 12: Thống kê mô tả về thu nhập (USD/năm)") %>% autofit()
Chỉ số | Giá trị |
|---|---|
Thu nhập Trung bình | 66,478 |
Thu nhập Trung vị | 49,000 |
Độ lệch chuẩn | 85,212 |
Thu nhập nhỏ nhất | 1 |
Thu nhập lớn nhất | 1,549,999 |
Một cái nhìn sơ bộ về thu nhập của những người lao động có lương đã hé lộ một bức tranh về sự bất bình đẳng. Bảng 12, kết quả của việc tính toán các chỉ số thống kê mô tả cho thấy thu nhập trung bình (66,478 USD) cao hơn rất nhiều so với thu nhập trung vị (49,000 USD). Sự chênh lệch lớn này có ý nghĩa rằng hơn một nửa số người lao động có mức thu nhập thấp hơn mức trung bình chung và giá trị trung bình này đang bị kéo lên bởi một số ít cá nhân có thu nhập ngoại lệ với mức cao nhất được ghi nhận lên tới gần 1.55 triệu USD. Độ lệch chuẩn cực kỳ cao (85,212 USD) càng củng cố thêm cho nhận định về sự phân tán và chênh lệch lớn trong thu nhập.
# Biểu đồ 11: Phân phối mật độ của thu nhập (thang đo Log)
ggplot(df_with_income, aes(x = incwage)) + geom_density(fill = "lightgreen", alpha = 0.7) + scale_x_log10(labels = dollar_format()) + labs(title = "Biểu đồ 11: Phân phối mật độ của thu nhập", subtitle = "Trục hoành được biểu diễn theo thang đo Logarit", x = "Thu nhập (USD/năm)", y = "Mật độ") + theme_classic()
Việc trực quan hóa một biến có độ lệch cao như thu nhập thường gặp khó khăn. Một biểu đồ tần suất thông thường sẽ bị co cụm ở một đầu và rất khó đọc. Để giải quyết vấn đề này, biểu đồ 11 đã sử dụng hai kỹ thuật kết hợp: geom_density() để vẽ một đường cong phân phối mượt mà thay vì các cột rời rạc, và quan trọng hơn là scale_x_log10() để áp dụng thang đo logarit cho trục hoành. Kết quả là một biểu đồ rõ ràng hơn rất nhiều, cho thấy đỉnh của phân phối thu nhập nằm ở đâu. Có thể thấy, phần lớn người lao động có thu nhập tập trung trong khoảng từ 30,000 đến 80,000 USD/năm và mật độ giảm dần ở cả hai phía thu nhập thấp hơn và cao hơn.
# Phân vị thu nhập (25%, 50% và 75%)
quantile(df_with_income$incwage, probs = c(0.25, 0.5, 0.75)) %>% as.list() %>% as.data.frame() %>% rename("25%" = X25., "50% (Trung vị)" = X50., "75%" = X75.) %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 13: Các mức phân vị của thu nhập") %>% autofit()
25% | 50% (Trung vị) | 75% |
|---|---|---|
26,000 | 49,000 | 80,000 |
Bảng 13 chia những người lao động có thu nhập thành bốn nhóm bằng nhau thông qua các mức phân vị được tính bằng hàm quantile(). Các con số này phác họa nên một bức tranh chi tiết hơn về cơ cấu thu nhập. Cụ thể, một phần tư lực lượng lao động có thu nhập thấp nhất kiếm được dưới 26,000 USD mỗi năm. Mức thu nhập trung vị là 49,000 USD là ranh giới chia đôi lực lượng lao động. Đáng chú ý, để lọt vào top 25% những người có thu nhập cao nhất, một cá nhân cần kiếm được ít nhất 80,000 USD mỗi năm. Việc lượng hóa các ngưỡng thu nhập này giúp hiểu rõ hơn về sự phân tầng kinh tế trong mẫu khảo sát.
# Top 5 người có thu nhập cao nhất
df %>% select(age, sex, educ, occ, incwage) %>% arrange(desc(incwage)) %>% head(5) %>% flextable() %>% colformat_double(j = "incwage", big.mark = ",") %>% set_caption("Bảng 14: Top 5 cá nhân có thu nhập cao nhất") %>% autofit()
age | sex | educ | occ | incwage |
|---|---|---|---|---|
32 | Male | Some high school | First-line supervisors of construction trades and extraction workers | 1,549,999.0 |
66 | Male | Some college | First-line supervisors of construction trades and extraction workers | 1,549,999.0 |
40 | Male | Graduate degree | Managers, all other | 1,154,999.0 |
48 | Female | Graduate degree | Accountants and auditors | 1,099,999.0 |
45 | Female | Graduate degree | Accountants and auditors | 1,099,999.0 |
Sự chênh lệch lớn giữa thu nhập trung bình và trung vị đã được lý giải một cách cụ thể thông qua bảng 14, nơi liệt kê 5 cá nhân có thu nhập cao nhất. Việc sử dụng hàm arrange(desc(incwage)) để xếp hạng thu nhập cho thấy sự tồn tại của các mức lương vượt trội, lên đến hơn 1.5 triệu USD. Những giá trị ngoại lai này, dù chỉ chiếm một tỷ lệ rất nhỏ trong tổng số mẫu, lại có tác động rất lớn lên giá trị trung bình, kéo nó lệch xa khỏi mức thu nhập điển hình của đa số dân chúng (được thể hiện qua trung vị). Bảng kết quả này không chỉ là một danh sách, mà còn là một bằng chứng thực tế minh họa cho tính chất phân phối lệch của dữ liệu thu nhập
# Số lượng người không có thu nhập theo nhóm học vấn
df %>% filter(incwage == 0) %>% count(educ_group, name = "Số lượng người không có thu nhập") %>% arrange(desc(`Số lượng người không có thu nhập`)) %>% flextable() %>% set_caption("Bảng 15: Phân bố người không có thu nhập theo học vấn") %>% autofit()
educ_group | Số lượng người không có thu nhập |
|---|---|
Thấp | 29,713 |
Trung bình | 22,562 |
Cao | 14,617 |
10,093 |
Bảng 15 khám phá mối quan hệ giữa trình độ học vấn và tình trạng không có thu nhập. Bằng cách lọc ra các cá nhân có incwage == 0 và đếm số lượng theo educ_group, kết quả cho thấy một mối tương quan nghịch rõ ràng. Cụ thể, số lượng người không có thu nhập cao nhất ở nhóm học vấn “thấp” và giảm dần ở các nhóm “trung bình” và “cao”. Điều này cho thấy trình độ học vấn không chỉ là yếu tố thúc đẩy thu nhập cao hơn mà còn đóng vai trò như một tấm đệm bảo vệ, giúp giảm khả năng một cá nhân rơi vào tình trạng không có thu nhập từ lương.
Đây là phần cốt lõi, nơi chúng ta kiểm tra xem các đặc điểm cá nhân như giới tính, học vấn, chủng tộc có tạo ra sự khác biệt về thu nhập hay không.
# Biểu đồ 12: Phân phối thu nhập theo giới tính
ggplot(df_with_income, aes(x = sex, y = incwage, fill = sex)) + geom_violin(trim = FALSE, alpha = 0.8) + geom_boxplot(width = 0.1, fill = "white", alpha = 0.5) + scale_y_continuous(labels = dollar_format(), limits = c(0, 200000)) + labs(title = "Biểu đồ 12: Phân phối thu nhập theo giới tính", subtitle = "(Giới hạn hiển thị ở mức 200,000 USD/năm để dễ quan sát)", x = "Giới tính", y = "Thu nhập (USD/năm)") + theme_light() + theme(legend.position = "none")
Biểu đồ 12 cung cấp một bằng chứng trực quan thuyết phục về sự tồn tại của khoảng cách thu nhập theo giới. Bằng cách kết hợp biểu đồ violin (geom_violin) và biểu đồ hộp (geom_boxplot), chúng ta có thể so sánh toàn diện phân phối thu nhập giữa nam và nữ. Toàn bộ thân của violin và hộp phân vị của nam giới đều dịch chuyển lên phía trên so với nữ giới, cho thấy ở mọi phân khúc, thu nhập của nam giới đều có xu hướng cao hơn. Đặc biệt, đường kẻ ngang biểu thị mức thu nhập trung vị của nam giới nằm ở vị trí cao hơn rõ rệt. Điều này có nghĩa là một người đàn ông điển hình trong mẫu này có thu nhập cao hơn một người phụ nữ. Kết quả này không chỉ cho thấy sự khác biệt về con số trung bình mà còn là sự khác biệt mang tính hệ thống trong toàn bộ cơ cấu thu nhập giữa hai giới.
# Biểu đồ 13: Thu nhập trung bình theo trình độ học vấn
df_with_income %>% filter(educ_group %in% c("Thấp", "Trung bình", "Cao")) %>% group_by(educ_group) %>% summarise(mean_income = mean(incwage)) %>% ggplot(aes(x = educ_group, y = mean_income, fill = educ_group)) + geom_col() + geom_text(aes(label = dollar(round(mean_income))), vjust = -0.5, size = 3.5) + scale_y_continuous(labels = dollar) + labs(title = "Biểu đồ 13: Thu nhập trung bình theo nhóm học vấn", x = "Nhóm học vấn", y = "Thu nhập trung bình") + theme_classic() + theme(legend.position = "none")
Biểu đồ 13 đã làm nổi bật tác động mạnh mẽ của trình độ học vấn đến thu nhập. Bằng cách tính toán thu nhập trung bình cho từng nhóm học vấn và trực quan hóa bằng biểu đồ cột (geom_col), kết quả cho thấy một xu hướng tăng tiến rõ rệt. Đáng chú ý nhất là sự chênh lệch lớn của nhóm “cao”, với mức thu nhập trung bình là 88,930 USD, cao gần gấp đôi so với nhóm “trung bình” (46,755 USD) và nhóm “thấp” (43,144 USD). Sự khác biệt rõ ràng về chiều cao giữa các cột cung cấp một bằng chứng trực quan thuyết phục về phần thưởng kinh tế (economic premium) của việc đầu tư vào giáo dục đại học và sau đại học.
# Biểu đồ 14: So sánh thu nhập theo học vấn, phân tách theo giới tính
ggplot(df_with_income, aes(x = educ_group, y = incwage, fill = sex)) + geom_boxplot() + facet_wrap(~sex) + scale_y_continuous(labels = dollar, limits = c(0, 250000)) + labs(title = "Biểu đồ 14: Phân phối thu nhập theo học vấn và giới tính", subtitle = "(Giới hạn hiển thị 250,000 USD)", x = "Nhóm học vấn", y = "Thu nhập") + theme_bw()
Biểu đồ 14 làm sáng tỏ một câu chuyện phức tạp hơn về thu nhập bằng cách đồng thời xem xét cả hai yếu tố học vấn và giới tính. Bằng cách sử dụng facet_wrap(~sex) để tạo ra hai biểu đồ con riêng biệt, chúng ta có thể quan sát hai xu hướng song song. Xu hướng thứ nhất, theo chiều ngang trong mỗi biểu đồ, là thu nhập trung vị (đường kẻ đậm trong hộp) tăng lên rõ rệt khi trình độ học vấn tăng từ “thấp” lên “cao”, điều này đúng cho cả nam và nữ. Xu hướng thứ hai, khi so sánh theo chiều dọc giữa hai biểu đồ là ở bất kỳ nhóm học vấn nào hộp phân vị của nam giới cũng đều cao hơn so với nữ giới. Sự kết hợp của hai xu hướng này cho thấy rằng, mặc dù giáo dục giúp cải thiện thu nhập cho tất cả mọi người nhưng dường như nó vẫn chưa đủ để xóa bỏ hoàn toàn khoảng cách thu nhập giữa hai giới.
# Biểu đồ 15: Phân phối thu nhập theo chủng tộc
ggplot(df_with_income, aes(x = reorder(race, incwage, FUN = median), y = incwage, fill = race)) + geom_boxplot() + scale_y_log10(labels = dollar) + coord_flip() + theme(legend.position = "none") + labs(title = "Biểu đồ 15: Phân phối thu nhập theo chủng tộc (thang đo Log)", y = "Thu nhập (USD/năm)", x = "Chủng tộc")
Kết quả trực quan hóa cho thấy một sự chênh lệch đáng kể về thu nhập trung vị giữa các nhóm chủng tộc. Cụ thể, nhóm “asian” và “white” có mức thu nhập trung vị (đường kẻ đậm giữa hộp) cao hơn rõ rệt so với các nhóm còn lại. Chiều cao của các hộp và độ dài của các “râu” cũng thể hiện sự khác biệt về độ phân tán thu nhập trong từng nhóm. Sự hiện diện của nhiều giá trị ngoại lai (outliers) ở tất cả các nhóm cho thấy tình trạng bất bình đẳng thu nhập không chỉ tồn tại giữa các chủng tộc mà còn diễn ra mạnh mẽ ngay trong nội bộ của từng nhóm. Những phát hiện này cung cấp bằng chứng về sự bất bình đẳng kinh tế theo chủng tộc trong mẫu khảo sát, một vấn đề phù hợp với nhiều nghiên cứu xã hội học tại Hoa Kỳ.
# Biểu đồ 16: Thu nhập TB theo tình trạng hôn nhân và giới tính
ggplot(df_with_income, aes(x = marst, y = incwage, fill = sex)) + stat_summary(fun = "mean", geom = "bar", position = "dodge") + scale_y_continuous(labels = dollar) + theme(axis.text.x = element_text(angle = 45, hjust = 1)) + labs(title = "Biểu đồ 16: Thu nhập TB theo tình trạng hôn nhân và giới tính", x = "Tình trạng hôn nhân", y = "Thu nhập trung bình", fill = "Giới tính")
Trực quan hóa thu nhập trung bình (incwage) theo cả tình trạng hôn nhân (marst) và giới tính (sex) để điều tra sâu hơn về sự tương tác giữa các yếu tố xã hội. Biểu đồ cột nhóm này được tạo ra bằng cách sử dụng stat_summary() với fun = “mean” và geom = “bar”, kết hợp position = “dodge” để đặt các cột của nam và nữ cạnh nhau, giúp việc so sánh trở nên trực quan. Kết quả cho thấy một mô hình nhất quán: trong mọi nhóm tình trạng hôn nhân, nam giới đều có thu nhập trung bình cao hơn nữ giới. Khoảng cách thu nhập này đặc biệt rõ rệt ở nhóm “married, spouse present”, nơi nam giới đạt mức thu nhập trung bình cao nhất trong tất cả các nhóm được khảo sát. Ngược lại, nhóm “married, spouse absent” có mức thu nhập trung bình thấp hơn đáng kể ở cả hai giới, gợi ý rằng không chỉ giới tính mà cả hoàn cảnh sống cũng có mối liên hệ phức tạp đến khả năng tạo ra thu nhập.
# Thu nhập trung vị theo tình trạng hôn nhân
df_with_income %>% group_by(marst) %>% summarise("Thu nhập trung vị" = median(incwage)) %>% arrange(desc(`Thu nhập trung vị`)) %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 16: Thu nhập trung vị theo tình trạng hôn nhân") %>% autofit()
marst | Thu nhập trung vị |
|---|---|
Married, spouse present | 60,000 |
Divorced | 49,000 |
Married, spouse absent | 40,002 |
Widowed | 38,000 |
Separated | 37,050 |
Single | 33,000 |
Nhằm cung cấp một thước đo vững chắc hơn so với giá trị trung bình, thu nhập trung vị (median) được tính toán theo từng nhóm tình trạng hôn nhân bằng cách sử dụng group_by(marst) và summarise(). Kỹ thuật này giúp xác định mức thu nhập điển hình, loại bỏ ảnh hưởng của các giá trị ngoại lai và kết quả được trình bày rõ ràng bằng flextable. Phân tích cho thấy một sự phân tầng thu nhập đáng kể, với nhóm “married, spouse present” có mức thu nhập trung vị cao nhất (60,000 USD), trong khi nhóm “single” có mức thu nhập trung vị thấp nhất (33,000 USD). Sự khác biệt rõ rệt này củng cố thêm bằng chứng về mối liên hệ phức tạp giữa các yếu tố xã hội như tình trạng hôn nhân và kết quả kinh tế của cá nhân.
# Độ lệch chuẩn của thu nhập theo nhóm học vấn
df_with_income %>% group_by(educ_group) %>% summarise("Độ lệch chuẩn thu nhập" = sd(incwage)) %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 17: Độ phân tán thu nhập theo nhóm học vấn") %>% autofit()
educ_group | Độ lệch chuẩn thu nhập |
|---|---|
Thấp | 122,273 |
Trung bình | 58,397 |
Cao | 99,531 |
65,184 |
Bên cạnh việc xác định mức thu nhập trung bình, việc phân tích độ biến động thu nhập trong mỗi nhóm học vấn cũng mang lại nhiều giá trị. Độ lệch chuẩn (sd) của thu nhập (incwage) đã được tính toán cho từng nhóm educ_group để lượng hóa sự chênh lệch. Kết quả cho thấy một bức tranh phức tạp: mặc dù nhóm “Thấp” có thu nhập trung bình thấp nhất, nhưng lại là nhóm có độ bất bình đẳng thu nhập cao nhất (độ lệch chuẩn 122,273). Điều này có thể cho thấy một sự phân cực lớn trong nhóm này, giữa những người lao động phổ thông và những người làm các công việc tay nghề không yêu cầu bằng cấp nhưng có thu nhập khá. Ngược lại, nhóm “trung bình” thể hiện sự ổn định nhất với độ lệch chuẩn thấp nhất (58,397). Sự gia tăng trở lại của độ lệch chuẩn ở nhóm “cao” (99,531) cho thấy rằng ngay cả khi có trình độ học vấn cao, con đường sự nghiệp và mức thu nhập vẫn rất đa dạng, từ các chuyên viên mới ra trường đến các nhà điều hành cấp cao.
Cuối cùng, chúng ta xem xét thu nhập thay đổi như thế nào theo vòng đời sự nghiệp (thể hiện qua tuổi) và theo ngành nghề cụ thể.
# Biểu đồ 17: Mối quan hệ giữa tuổi và thu nhập
ggplot(df_with_income %>% sample_n(5000), aes(x = age, y = incwage)) + geom_point(alpha = 0.1, color = "gray50", shape = 16) + geom_smooth(method = "loess", color = "firebrick", se = FALSE) + scale_y_continuous(labels = dollar, limits = c(0, 250000)) + labs(title = "Biểu đồ 17: Mối quan hệ giữa tuổi và thu nhập", subtitle = "Dựa trên mẫu ngẫu nhiên 5,000 người", x = "Tuổi", y = "Thu nhập") + theme_bw()
Biểu đồ 17 sử dụng biểu đồ phân tán (geom_point) để trực quan hóa mối quan hệ giữa tuổi và thu nhập. Do số lượng quan sát lớn, một mẫu ngẫu nhiên 5,000 điểm đã được sử dụng để biểu đồ không bị quá tải. Mỗi điểm đại diện cho một cá nhân. Để làm nổi bật xu hướng chung, một đường cong hồi quy cục bộ (geom_smooth(method = “loess”)) đã được thêm vào. Kết quả cho thấy một mối quan hệ phi tuyến tính rõ ràng: thu nhập có xu hướng tăng theo tuổi ở giai đoạn đầu, đạt đỉnh ở độ tuổi trung niên (khoảng 45-55 tuổi) và sau đó giảm dần. Hình dạng parabol này được gọi là “vòng cung sự nghiệp” - một quy luật kinh tế học cơ bản, phản ánh sự tích lũy kinh nghiệm và năng suất lao động trong suốt cuộc đời của một người.
# Hệ số tương quan tuổi-thu nhập
correlation <- cor(df_with_income$age, df_with_income$incwage)
cat(paste("Hệ số tương quan Pearson giữa tuổi và thu nhập là:", round(correlation, 3)))
Hệ số tương quan Pearson giữa tuổi và thu nhập là: 0.137
Bên cạnh việc trực quan hóa, việc tính toán hệ số tương quan Pearson bằng hàm cor() cho phép đưa ra một con số cụ thể để đo lường cường độ và chiều hướng của mối quan hệ tuyến tính giữa tuổi và thu nhập. Kết quả thu được là 0.137, một giá trị dương cho thấy xu hướng chung là thu nhập tăng khi tuổi tăng. Tuy nhiên, độ lớn của hệ số này (gần 0) chỉ ra rằng mối quan hệ tuyến tính này rất yếu. Điều này không có nghĩa là tuổi không ảnh hưởng đến thu nhập, mà nó cho thấy mối quan hệ thực tế có thể phức tạp hơn và không phải là một đường thẳng đơn giản, điều đã được thể hiện qua đường cong trong biểu đồ phân tán trước đó.
# Biểu đồ 18: Xu hướng Thu nhập trung bình theo Tuổi và Học vấn
df_with_income %>% group_by(age_group, educ_group) %>% summarise(mean_income = mean(incwage), .groups = 'drop') %>% ggplot(aes(x = age_group, y = mean_income, color = educ_group, group = educ_group)) + geom_line(size = 1.5) + geom_point(size = 3) + scale_y_continuous(labels = dollar) + labs(title = "Biểu đồ 18: Thu nhập trung bình thay đổi theo tuổi và học vấn", x = "Nhóm tuổi", y = "Thu nhập trung bình", color = "Nhóm học vấn") + theme_light()
Biểu đồ này minh họa một cách trực quan tác động cộng hưởng của tuổi tác và trình độ học vấn lên thu nhập trung bình. Bằng cách nhóm dữ liệu theo age_group và educ_group rồi tính toán giá trị trung bình tạo ra một biểu đồ đường (geom_line) để theo dõi quỹ đạo thu nhập của từng nhóm học vấn qua các giai đoạn sự nghiệp. Kết quả cho thấy học vấn không chỉ thiết lập một mức thu nhập khởi điểm khác nhau mà còn định hình cả con đường phát triển thu nhập trong tương lai. Nhóm có trình độ học vấn “cao” (màu xanh dương) không chỉ bắt đầu với mức lương cao hơn mà còn trải qua giai đoạn tăng trưởng thu nhập mạnh mẽ nhất, đạt đỉnh ở độ tuổi 46-60. Ngược lại, các nhóm có trình độ học vấn thấp hơn có quỹ đạo thu nhập phẳng hơn, với mức tăng trưởng khiêm tốn hơn theo tuổi tác. Sự nới rộng khoảng cách giữa các đường cong theo thời gian là một minh chứng rõ ràng cho thấy lợi tức của việc đầu tư vào giáo dục được tích lũy và nhân lên cùng với kinh nghiệm làm việc.
# Biểu đồ 19: Top 10 nghề nghiệp có thu nhập trung bình cao nhất
top_10_occ_df <- df_with_income %>% filter(occ != "N/A", !is.na(occ)) %>% group_by(occ) %>% summarise(mean_income = mean(incwage), count = n(), .groups = 'drop') %>% filter(count > 30) %>% arrange(desc(mean_income)) %>% head(10)
ggplot(top_10_occ_df, aes(x = reorder(occ, mean_income), y = mean_income)) + geom_col(fill = "darkgoldenrod1") + geom_text(aes(label = dollar(round(mean_income))), hjust = -0.1, color = "black", size=3) + coord_flip() + scale_y_continuous(labels = dollar, expand = expansion(mult = c(0, 0.1))) + labs(title = "Top 10 nghề nghiệp có thu nhập trung bình cao nhất", subtitle = "(Chỉ xét các nghề có trên 30 quan sát)", x = "Nghề nghiệp", y = "Thu nhập trung bình")
Biểu đồ cột ngang liệt kê Top 10 nghề có thu nhập trung bình cao nhất.
Để xác định các ngành nghề có tiềm năng thu nhập cao nhất trong mẫu, tiến hành lọc và xếp hạng các nghề nghiệp (occ) dựa trên mức thu nhập trung bình. Quy trình phân tích bao gồm việc nhóm dữ liệu theo nghề, tính toán thu nhập trung bình và số lượng quan sát, sau đó filter(count > 30) để chỉ giữ lại những nghề có đủ dữ liệu nhằm đảm bảo tính ổn định thống kê. Top 10 nghề nghiệp có thu nhập cao nhất được trực quan hóa bằng biểu đồ cột ngang (geom_col kết hợp coord_flip()), một lựa chọn phù hợp để hiển thị các nhãn nghề nghiệp dài. Kết quả cho thấy các ngành nghề đòi hỏi trình độ chuyên môn hóa cao như “other physicians” (bác sĩ) và “lawyers” (luật sư), cùng với các vị trí quản lý cấp cao như “chief executives” (giám đốc điều hành) đều chiếm lĩnh các vị trí dẫn đầu với mức thu nhập trung bình vượt trội, trên 180,000 USD/năm. Phát hiện này củng cố mạnh mẽ luận điểm rằng việc đầu tư vào giáo dục chuyên sâu và thăng tiến lên các vai trò quản lý là con đường hiệu quả nhất để đạt được mức thu nhập cao.
# Biểu đồ 20: Phân phối thu nhập của 5 nghề nghiệp phổ biến nhất
top_5_occ <- df %>% filter(occ != "N/A") %>% count(occ, sort = TRUE) %>% head(5) %>% pull(occ)
df_with_income %>% filter(occ %in% top_5_occ) %>% ggplot(aes(x = reorder(occ, incwage, FUN=median), y = incwage, fill = occ)) + geom_boxplot() + scale_y_log10(labels = dollar) + theme(legend.position = "none", axis.text.x = element_text(angle = 30, hjust = 1)) + labs(title = "Biểu đồ 20: Phân phối thu nhập của 5 nghề nghiệp phổ biến nhất", x = "Nghề nghiệp", y = "Thu nhập (thang đo Log)")
Nhằm trả lời câu hỏi “nghề phổ biến có phải là nghề lương cao không?”, biểu đồ hộp (geom_boxplot) so sánh phân phối thu nhập giữa 5 ngành nghề có số lượng lao động đông đảo nhất. Dữ liệu đầu vào được chuẩn bị bằng cách xác định top 5 nghề qua hàm count() và head(), sau đó lọc (filter) tập dữ liệu df_with_income theo danh sách này. Biểu đồ cho thấy mặc dù các nghề như “managers, all other” (quản lý) và “registered nurses” (y tá) đều là những nghề phổ biến, chúng cũng có mức thu nhập trung vị và khoảng phân phối cao hơn hẳn so với các nghề phổ thông khác. Điều này cho thấy rằng ngay cả trong nhóm các nghề nghiệp có số lượng lớn, vẫn tồn tại một sự phân tầng thu nhập rõ rệt, trong đó các vị trí đòi hỏi kỹ năng quản lý và chuyên môn y tế vẫn mang lại lợi ích kinh tế vượt trội.
# Thu nhập trung bình của 5 nghề nghiệp phổ biến nhất
df_with_income %>% filter(occ %in% top_5_occ) %>% group_by(occ) %>% summarise("Thu nhập trung bình" = mean(incwage)) %>% arrange(desc(`Thu nhập trung bình`)) %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 18: Thu nhập trung bình của 5 nghề phổ biến nhất") %>% autofit()
occ | Thu nhập trung bình |
|---|---|
Managers, all other | 105,603 |
Registered nurses | 81,147 |
Elementary and middle school teachers | 59,508 |
Driver/sales workers and truck drivers | 56,723 |
Not in universe | 30,813 |
Bảng 18 trình bày kết quả tính toán thu nhập trung bình (mean) cho 5 nghề nghiệp có tần suất xuất hiện cao nhất trong mẫu dữ liệu. Quá trình tính toán được thực hiện bằng cách lọc (filter) dữ liệu theo danh sách 5 nghề phổ biến, sau đó nhóm (group_by) và tổng hợp (summarise) giá trị thu nhập. Kết quả cho thấy một sự phân hóa thu nhập rõ rệt: vị trí “managers, all other” (quản lý) có mức thu nhập trung bình cao nhất (105,603 USD), vượt xa các nhóm còn lại. Điều này cho thấy vai trò quản lý và các ngành nghề đòi hỏi chuyên môn cao như “registered nurses” (y tá) vẫn có tiềm năng thu nhập tốt hơn so với các công việc phổ thông khác, ngay cả khi tất cả đều là những ngành nghề có số lượng lao động lớn.
# Thu nhập trung vị của 5 nghề nghiệp phổ biến nhất
df_with_income %>% filter(occ %in% top_5_occ) %>% group_by(occ) %>% summarise("Thu nhập trung vị" = median(incwage)) %>% arrange(desc(`Thu nhập trung vị`)) %>% flextable() %>% colformat_double(digits = 0, big.mark = ",") %>% set_caption("Bảng 19: Thu nhập trung vị của 5 nghề phổ biến nhất") %>% autofit()
occ | Thu nhập trung vị |
|---|---|
Managers, all other | 85,000 |
Registered nurses | 73,000 |
Elementary and middle school teachers | 55,000 |
Driver/sales workers and truck drivers | 50,000 |
Not in universe | 15,000 |
Nhằm cung cấp một thước đo vững chắc hơn so với thu nhập trung bình, vốn dễ bị ảnh hưởng bởi các giá trị ngoại lai. Bảng 19 trình bày kết quả tính toán thu nhập trung vị (median) cho 5 nghề nghiệp phổ biến nhất. Kỹ thuật được sử dụng bao gồm việc lọc (filter) dữ liệu theo danh sách 5 nghề này, sau đó nhóm (group_by) và tổng hợp (summarise) để tìm ra mức thu nhập điển hình của mỗi nghề. Kết quả cho thấy một thứ bậc thu nhập tương tự như phân tích thu nhập trung bình: vị trí “managers, all other” (quản lý) tiếp tục dẫn đầu với thu nhập trung vị là 85,000 USD, theo sau là “registered nurses” (y tá) với 73,000 USD. Việc sử dụng trung vị khẳng định lại sự phân tầng thu nhập giữa các ngành nghề, cho thấy rằng ngay cả khi loại bỏ ảnh hưởng của những cá nhân có thu nhập đặc biệt cao, các vị trí quản lý và chuyên môn vẫn có mức thu nhập cao hơn một cách rõ rệt.
Qua quá trình khám phá, xử lý và phân tích sâu bộ dữ liệu lực lượng lao động Hoa Kỳ năm 2023, chương này đã phác họa nên một bức tranh đa chiều về đặc điểm nhân khẩu học, tình trạng việc làm và các yếu tố ảnh hưởng đến thu nhập. Các phát hiện chính đã trả lời cho các câu hỏi nghiên cứu cốt lõi được đặt ra ban đầu.
- Về đặc điểm nhân khẩu học (“Họ là ai?”): phân tích cho thấy một lực lượng lao động trưởng thành, với độ tuổi trung bình và trung vị đều xoay quanh mốc 38 tuổi. Sự phân bổ giới tính tương đối cân bằng, cung cấp một nền tảng vững chắc cho các phân tích so sánh. Tuy nhiên, các phân tích sâu hơn đã chỉ ra các mô hình xã hội phức tạp, chẳng hạn như sự khác biệt về độ tuổi trung vị giữa các nhóm học vấn và sự phân bổ không đồng đều của các tình trạng hôn nhân theo giới tính và độ tuổi.
- Về tình trạng việc làm (“Họ làm gì?”): kết quả khẳng định một cách mạnh mẽ vai trò của giáo dục như một yếu tố quyết định đến cơ hội việc làm. Dữ liệu cho thấy tỷ lệ có việc làm tăng lên và tỷ lệ thất nghiệp giảm xuống rõ rệt ở những người có trình độ học vấn cao. Bên cạnh đó, cũng có sự khác biệt về tỷ lệ tham gia lao động giữa nam và nữ. Phân tích về các nghề nghiệp phổ biến cho thấy một cơ cấu lao động đa dạng, bao gồm cả các vị trí quản lý, chuyên môn và lao động phổ thông.
- Về thu nhập (“Họ kiếm được bao nhiêu?”): phân tích là nơi bộc lộ rõ nhất sự phân tầng kinh tế. Dữ liệu thu nhập thể hiện một sự phân phối lệch phải mạnh, với khoảng cách đáng kể giữa thu nhập trung bình và trung vị, cho thấy sự tồn tại của bất bình đẳng thu nhập. Các yếu tố như giới tính, trình độ học vấn, tuổi tác và chủng tộc đều được chứng minh là có mối tương quan đáng kể đến mức thu nhập. Cụ thể, nam giới, người có học vấn cao, người ở độ tuổi trung niên, và người thuộc nhóm chủng tộc “Asian” hoặc “White” có xu hướng đạt được mức thu nhập cao hơn. Đặc biệt, lợi ích kinh tế của việc đầu tư vào giáo dục được thể hiện rõ nét qua quỹ đạo thu nhập dốc hơn và đạt đỉnh cao hơn ở nhóm có trình độ “Cao”.
Tóm lại, chương 1 đã xây dựng một nền tảng dữ liệu vững chắc và rút ra được những cốt lõi quan trọng. Bức tranh toàn cảnh phác họa nên chân dung một người lao động điển hình, nơi con đường sự nghiệp và mức thu nhập chịu ảnh hưởng mạnh mẽ bởi nền tảng giáo dục và ngành nghề họ theo đuổi. Tuy nhiên, con đường đó không bằng phẳng cho tất cả mọi người; các yếu tố nhân khẩu học và xã hội vẫn tạo ra những rào cản và sự khác biệt nhất định trong việc tiếp cận cơ hội và phúc lợi kinh tế.
Chương 2 chuyển hướng phân tích từ dữ liệu vi mô xã hội sang dữ liệu tài chính doanh nghiệp, cụ thể là “Báo cáo kết quả hoạt động kinh doanh của Tập đoàn Vingroup (VIC) trong giai đoạn 2015-2024”. Mục tiêu của chương là đánh giá hiệu quả hoạt động của Vingroup qua các năm thông qua việc phân tích các chỉ số tài chính quan trọng, từ đó rút ra các nhận định về xu hướng tăng trưởng, khả năng sinh lời và hiệu quả quản lý chi phí của tập đoàn.
Bước đầu tiên và quan trọng nhất trong phân tích dữ liệu tài chính là đảm bảo dữ liệu được thu thập và tái cấu trúc về một định dạng chuẩn. Phần này sẽ trình bày quy trình nạp dữ liệu từ file Excel, xử lý và chuyển đổi dữ liệu. Cuối cùng là tính toán các chỉ số tài chính phái sinh để chuẩn bị cho các phân tích sâu hơn.
df_kqkd_raw <- read_excel("VIC_BCTC.xlsx", sheet = "Kết quả hoạt động kinh doanh")
Quá trình phân tích được khởi đầu bằng việc nhập dữ liệu từ file VIC_BCTC.xlsx. Cụ thể, hàm read_excel() từ gói readxl được sử dụng với tham số sheet được chỉ định là “Kết quả hoạt động kinh doanh” để đọc duy nhất dữ liệu liên quan vào dataframe thô df_kqkd_raw. Thao tác này là bước đầu tiên để tải dữ liệu vào môi trường R để chuẩn bị cho các bước tái cấu trúc và phân tích tiếp theo.
head(df_kqkd_raw, 5) %>% kable(caption = "5 chỉ tiêu đầu tiên trong báo cáo KQKD") %>% kable_styling(full_width = F)
| …1 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 |
|---|---|---|---|---|---|---|---|---|---|---|
| Doanh thu bán hàng và cung cấp dịch vụ | 34054968.885 | 57670387.20 | 89392047.93 | 121971750.63 | 130161398 | 110755497 | 125780761 | 101809529 | 161452751 | 189090599 |
| Các khoản giảm trừ doanh thu | -7002.572 | -56043.54 | -41999.34 | -77350.21 | -125384 | -265464 | -92891 | -15947 | -25183 | -22559 |
| Doanh thu thuần về bán hàng và cung cấp dịch vụ | 34047966.313 | 57614343.67 | 89350048.60 | 121894400.42 | 130036014 | 110490033 | 125687870 | 101793582 | 161427568 | 189068040 |
| Giá vốn hàng bán | -22338933.561 | -40184632.61 | -62796326.96 | -92971050.56 | -92484797 | -93177227 | -91623165 | -87099750 | -137919092 | -161767222 |
| Lợi nhuận gộp về bán hàng và cung cấp dịch vụ | 11709032.752 | 17429711.06 | 26553721.64 | 28923349.86 | 37551217 | 17312806 | 34064705 | 14693832 | 23508476 | 27300818 |
Việc hiển thị năm chỉ tiêu đầu tiên bằng hàm head() xác nhận dữ liệu đã được đọc vào đúng cấu trúc dạng rộng ban đầu. Quan sát các giá trị mẫu, có thể nhận thấy sự khác biệt đáng kể về quy mô con số giữa các năm trước và sau 2018, gợi ý về sự không nhất quán trong đơn vị tính. Vấn đề này sẽ cần được xử lý ở các bước tái cấu trúc dữ liệu tiếp theo để đảm bảo tính chính xác của phân tích.
kqkd_long <- df_kqkd_raw %>% rename(chi_tieu = 1) %>% pivot_longer(cols = -chi_tieu, names_to = "nam", values_to = "gia_tri", names_transform = as.numeric, values_transform = as.numeric) %>% filter(!is.na(nam) & !is.na(gia_tri) & !is.na(chi_tieu))
Để chuẩn bị cho việc phân tích, dữ liệu thô cần được chuyển đổi từ định dạng rộng (wide) sang định dạng dài (long). Thao tác này được thực hiện thông qua một chuỗi lệnh dplyr: đầu tiên, rename() được dùng để đặt tên cho cột chỉ tiêu; sau đó, hàm pivot_longer() thực hiện nhiệm vụ cốt lõi là “xoay” các cột năm thành các hàng. Các tham số names_transform và values_transform được sử dụng để bắt buộc chuyển đổi kiểu dữ liệu của cột năm và cột giá trị sang dạng số, nhằm đảm bảo tính nhất quán. Cuối cùng, filter() loại bỏ các hàng không hợp lệ (hàng trống hoặc hàng tổng kết không chứa dữ liệu). Kết quả của quá trình này là một dataframe kqkd_long có cấu trúc “tidy”, nơi mỗi hàng là một quan sát duy nhất (một chỉ tiêu trong một năm), một định dạng linh hoạt cho các bước xử lý và hợp nhất dữ liệu tiếp theo.
glimpse(kqkd_long)
Rows: 229
Columns: 3
$ chi_tieu <chr> "Doanh thu bán hàng và cung cấp dịch vụ", "Doanh thu bán hàng…
$ nam <dbl> 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2…
$ gia_tri <dbl> 34054968.885, 57670387.202, 89392047.933, 121971750.626, 1301…
Sau bước tái cấu trúc, hàm glimpse() được sử dụng để xác thực lại cấu trúc của dataframe kqkd_long. Kết quả cho thấy dữ liệu đã được chuyển đổi thành công sang định dạng dài với 229 hàng và 3 cột chính. Quan trọng hơn, kết quả xác nhận rằng cả cột năm và cột giá trị đều có kiểu dữ liệu là số (dbl), chứng tỏ quá trình ép kiểu dữ liệu bên trong pivot_longer đã thành công. Việc đảm bảo kiểu dữ liệu nhất quán này là một bước kiểm tra chất lượng quan trọng, tạo tiền đề vững chắc cho các thao tác tính toán và hợp nhất dữ liệu ở các bước tiếp theo.
vic_kqkd <- kqkd_long %>% pivot_wider(names_from = chi_tieu, values_from = gia_tri) %>% janitor::clean_names() %>% arrange(nam)
Để có được một dataframe tối ưu cho việc phân tích và trực quan hóa theo chuỗi thời gian, thao tác cuối cùng là chuyển đổi dữ liệu từ định dạng dài trở lại định dạng rộng. Kỹ thuật này được thực hiện bằng hàm pivot_wider(), trong đó các giá trị duy nhất từ cột chi_tieu được biến thành các cột mới và giá trị tương ứng được lấy từ cột gia_tri. Sau đó, hàm clean_names() được áp dụng để chuẩn hóa tên các cột mới này. Kết quả là dataframe vic_kqkd có cấu trúc lý tưởng với mỗi hàng đại diện cho một năm và mỗi cột đại diện cho một chỉ tiêu tài chính cụ thể, sẵn sàng cho các bước tính toán chỉ số phái sinh và phân tích xu hướng.
# Đếm số lượng giá trị duy nhất trong mỗi cột chỉ tiêu
sapply(vic_kqkd, function(x) length(unique(x)))
nam
10
doanh_thu_ban_hang_va_cung_cap_dich_vu
10
cac_khoan_giam_tru_doanh_thu
10
doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
10
gia_von_hang_ban
10
loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu
10
doanh_thu_hoat_dong_tai_chinh
10
chi_phi_tai_chinh
10
chi_phi_lai_vay_va_phat_hanh_trai_phieu
10
phan_lai_lo_trong_cong_ty_lien_doanh_lien_ket
10
chi_phi_ban_hang
10
chi_phi_quan_ly_doanh_nghiep
10
loi_nhuan_thuan_tu_hoat_dong_kinh_doanh
10
thu_nhap_khac
10
chi_phi_khac
10
lai_lo_khac
10
tong_loi_nhuan_ke_toan_truoc_thue
10
chi_phi_thue_tndn_hien_hanh
10
chi_phi_thu_nhap_thue_tndn_hoan
10
loi_nhuan_sau_thue_thu_nhap_doanh_nghiep
10
loi_nhuan_sau_thue_cong_ty_me
10
loi_nhuan_sau_thue_cong_ty_me_khong_kiem_soat
10
lai_co_ban_tren_co_phieu
10
lai_suy_giam_tren_co_phieu
10
Một bước kiểm tra quan trọng trong quá trình khám phá dữ liệu là đánh giá độ đa dạng của từng biến. Thao tác này được thực hiện bằng cách áp dụng hàm sapply() để lặp qua mỗi cột trong dataframe vic_kqkd và đếm số lượng giá trị duy nhất bằng length(unique(x)). Kết quả cho thấy tất cả các biến tài chính đều có 10 giá trị duy nhất. Con số này khớp với số lượng quan sát (số năm) trong bộ dữ liệu, cho thấy rằng trong suốt giai đoạn được phân tích, không có chỉ tiêu nào có giá trị không đổi qua các năm.
dim(vic_kqkd)
[1] 10 24
Sau khi hoàn tất quá trình tái cấu trúc, hàm dim() được sử dụng để xác nhận lại kích thước của dataframe vic_kqkd cuối cùng. Kết quả 10 24 cho thấy bộ dữ liệu phân tích bao gồm 10 quan sát, tương ứng với chuỗi thời gian 10 năm và 24 biến, là các chỉ tiêu tài chính từ báo cáo kết quả kinh doanh. Việc xác nhận số lượng dòng và cột này đảm bảo rằng quá trình xử lý dữ liệu đã bảo toàn được đầy đủ các năm và các chỉ tiêu cần thiết cho việc phân tích xu hướng.
colnames(vic_kqkd)
[1] "nam"
[2] "doanh_thu_ban_hang_va_cung_cap_dich_vu"
[3] "cac_khoan_giam_tru_doanh_thu"
[4] "doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu"
[5] "gia_von_hang_ban"
[6] "loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu"
[7] "doanh_thu_hoat_dong_tai_chinh"
[8] "chi_phi_tai_chinh"
[9] "chi_phi_lai_vay_va_phat_hanh_trai_phieu"
[10] "phan_lai_lo_trong_cong_ty_lien_doanh_lien_ket"
[11] "chi_phi_ban_hang"
[12] "chi_phi_quan_ly_doanh_nghiep"
[13] "loi_nhuan_thuan_tu_hoat_dong_kinh_doanh"
[14] "thu_nhap_khac"
[15] "chi_phi_khac"
[16] "lai_lo_khac"
[17] "tong_loi_nhuan_ke_toan_truoc_thue"
[18] "chi_phi_thue_tndn_hien_hanh"
[19] "chi_phi_thu_nhap_thue_tndn_hoan"
[20] "loi_nhuan_sau_thue_thu_nhap_doanh_nghiep"
[21] "loi_nhuan_sau_thue_cong_ty_me"
[22] "loi_nhuan_sau_thue_cong_ty_me_khong_kiem_soat"
[23] "lai_co_ban_tren_co_phieu"
[24] "lai_suy_giam_tren_co_phieu"
Để đảm bảo tính chính xác và tránh lỗi trong các bước tính toán sau này, hàm colnames() được sử dụng để trích xuất và hiển thị danh sách đầy đủ tên của 24 biến trong dataframe vic_kqkd. Kết quả này đóng vai trò như một từ điển dữ liệu, cung cấp các tên biến chính xác đã được hàm clean_names() tự động tạo ra. Việc có được danh sách tham chiếu này là rất quan trọng, cho phép sao chép và dán trực tiếp tên biến vào các công thức, qua đó loại bỏ các lỗi chính tả tiềm ẩn và đảm bảo tính nhất quán trong toàn bộ quy trình phân tích.
summary(vic_kqkd)
nam doanh_thu_ban_hang_va_cung_cap_dich_vu
Min. :2015 Min. : 34054969
1st Qu.:2017 1st Qu.: 92496418
Median :2020 Median :116363624
Mean :2020 Mean :112213969
3rd Qu.:2022 3rd Qu.:129066239
Max. :2024 Max. :189090599
cac_khoan_giam_tru_doanh_thu doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
Min. :-265464 Min. : 34047966
1st Qu.: -89006 1st Qu.: 92460932
Median : -49021 Median :116192217
Mean : -72982 Mean :112140987
3rd Qu.: -23215 3rd Qu.:128948978
Max. : -7003 Max. :189068040
gia_von_hang_ban loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu
Min. :-161767222 Min. :11709033
1st Qu.: -93125683 1st Qu.:17342032
Median : -92053981 Median :25031099
Mean : -88236220 Mean :23904767
3rd Qu.: -68872183 3rd Qu.:28517717
Max. : -22338934 Max. :37551217
doanh_thu_hoat_dong_tai_chinh chi_phi_tai_chinh
Min. : 1636951 Min. :-31208095
1st Qu.: 6265805 1st Qu.:-13945865
Median :15023396 Median : -9772519
Mean :17949986 Mean :-11809239
3rd Qu.:28426930 3rd Qu.: -5028971
Max. :47925492 Max. : -3282075
chi_phi_lai_vay_va_phat_hanh_trai_phieu
Min. :-22980044
1st Qu.:-11287844
Median : -8718125
Mean : -9406061
3rd Qu.: -4031479
Max. : -2402861
phan_lai_lo_trong_cong_ty_lien_doanh_lien_ket chi_phi_ban_hang
Min. :-688443 Min. :-18053919
1st Qu.:-223415 1st Qu.:-11913502
Median : -64096 Median : -8760777
Mean : -65243 Mean : -9624425
3rd Qu.: 34377 3rd Qu.: -6995827
Max. : 848773 Max. : -2957826
chi_phi_quan_ly_doanh_nghiep loi_nhuan_thuan_tu_hoat_dong_kinh_doanh
Min. :-24034459 Min. :-4905383
1st Qu.:-14727044 1st Qu.: 6232590
Median :-13040264 Median : 8724775
Mean :-11834935 Mean : 8520911
3rd Qu.: -6981296 3rd Qu.:13107118
Max. : -3922773 Max. :15756406
thu_nhap_khac chi_phi_khac lai_lo_khac
Min. : 283039 Min. :-5778675 Min. :-4614455
1st Qu.: 706181 1st Qu.:-3017040 1st Qu.: -581481
Median : 907443 Median :-1004932 Median : -24687
Mean : 4315587 Mean :-2076220 Mean : 2239368
3rd Qu.: 4648442 3rd Qu.: -811413 3rd Qu.: 3629800
Max. :22132506 Max. : -493550 Max. :18674735
tong_loi_nhuan_ke_toan_truoc_thue chi_phi_thue_tndn_hien_hanh
Min. : 2852101 Min. :-12913575
1st Qu.: 6623140 1st Qu.:-10993880
Median :13262435 Median : -9096090
Mean :10760279 Mean : -7886998
3rd Qu.:13920326 3rd Qu.: -4616819
Max. :16738706 Max. : -1424643
chi_phi_thu_nhap_thue_tndn_hoan loi_nhuan_sau_thue_thu_nhap_doanh_nghiep
Min. :-799260 Min. :-7558164
1st Qu.:-389626 1st Qu.: 2047273
Median : 272042 Median : 4029320
Mean : 217063 Mean : 3094085
3rd Qu.: 519751 3rd Qu.: 5560221
Max. :1450927 Max. : 7716613
NA's :1
loi_nhuan_sau_thue_cong_ty_me loi_nhuan_sau_thue_cong_ty_me_khong_kiem_soat
Min. :-2513883 Min. :-6737517
1st Qu.: 2227572 1st Qu.:-4012974
Median : 4119570 Median : 34917
Mean : 4523290 Mean :-1429205
3rd Qu.: 7025593 3rd Qu.: 876593
Max. :11903028 Max. : 2414154
lai_co_ban_tren_co_phieu lai_suy_giam_tren_co_phieu
Min. :-685.00000 Min. :-685.00000
1st Qu.: 0.00079 1st Qu.: 0.00079
Median : 282.50091 Median : 262.50091
Mean : 941.60037 Mean : 907.10037
3rd Qu.:2198.00000 3rd Qu.:2075.00000
Max. :3045.00000 Max. :2976.00000
Một bản tóm tắt thống kê nhanh được tạo ra bằng hàm summary() để lượng hóa các đặc điểm phân phối chính của từng chỉ tiêu tài chính trong bộ dữ liệu vic_kqkd. Kết quả này không chỉ cung cấp các giá trị cực trị (min, max) mà còn cả các thước đo xu hướng trung tâm (mean, median) và độ phân tán (thông qua các tứ phân vị). Qua đó, có thể thấy các chỉ tiêu chính như doanh_thu_… và loi_nhuan_… có khoảng giá trị rất rộng và giá trị trung bình thường lớn hơn giá trị trung vị. Điều này cho thấy sự tăng trưởng không đồng đều qua các năm và sự hiện diện của những năm có kết quả kinh doanh đặc biệt cao, làm lệch phân phối chung.
colSums(is.na(vic_kqkd))
nam
0
doanh_thu_ban_hang_va_cung_cap_dich_vu
0
cac_khoan_giam_tru_doanh_thu
0
doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu
0
gia_von_hang_ban
0
loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu
0
doanh_thu_hoat_dong_tai_chinh
0
chi_phi_tai_chinh
0
chi_phi_lai_vay_va_phat_hanh_trai_phieu
0
phan_lai_lo_trong_cong_ty_lien_doanh_lien_ket
0
chi_phi_ban_hang
0
chi_phi_quan_ly_doanh_nghiep
0
loi_nhuan_thuan_tu_hoat_dong_kinh_doanh
0
thu_nhap_khac
0
chi_phi_khac
0
lai_lo_khac
0
tong_loi_nhuan_ke_toan_truoc_thue
0
chi_phi_thue_tndn_hien_hanh
0
chi_phi_thu_nhap_thue_tndn_hoan
1
loi_nhuan_sau_thue_thu_nhap_doanh_nghiep
0
loi_nhuan_sau_thue_cong_ty_me
0
loi_nhuan_sau_thue_cong_ty_me_khong_kiem_soat
0
lai_co_ban_tren_co_phieu
0
lai_suy_giam_tren_co_phieu
0
Bước cuối cùng trong quá trình khám phá dữ liệu là kiểm tra lại chất lượng và tính toàn vẹn của bộ dữ liệu sau khi đã xử lý. Hàm colSums(is.na(vic_kqkd)) được sử dụng để đếm số lượng giá trị bị thiếu (NA) trong mỗi cột. Kết quả cho thấy hầu hết các biến đều hoàn chỉnh với 0 giá trị thiếu. Tuy nhiên, một điểm đáng chú ý là sự xuất hiện của một giá trị NA duy nhất trong cột chi_phi_thu_nhap_thue_tndn_hoan. Giá trị thiếu này có thể xuất hiện ở năm đầu tiên của các chỉ số có tính toán liên năm. Sự hiện diện của NA này củng cố tầm quan trọng của việc sử dụng tham số na.rm = TRUE trong tất cả các phép tính thống kê tổng hợp ở các phần sau để đảm bảo R bỏ qua các giá trị thiếu và không trả về kết quả lỗi.
Từ bộ dữ liệu vic_kqkd đã được chuẩn bị, phần này sẽ
thực hiện việc tính toán các chỉ số tài chính quan trọng liên quan đến
hiệu quả hoạt động và khả năng sinh lời. Mỗi chỉ số được tạo ra dưới
dạng một biến mới, là một thao tác xử lý dữ liệu.
vic_kqkd_processed <- vic_kqkd %>% mutate(loi_nhuan_gop = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu - gia_von_hang_ban)
Thao tác đầu tiên trong việc tạo biến mới là tính lợi nhuận gộp bằng cách sử dụng hàm mutate(), một biến mới loi_nhuan_gop đã được tạo ra thông qua phép trừ giữa doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu và gia_von_hang_ban. Lợi nhuận gộp là chỉ số đầu tiên trong chuỗi lợi nhuận, phản ánh hiệu quả của hoạt động sản xuất kinh doanh cốt lõi trước khi tính đến các chi phí gián tiếp và là nền tảng cho việc tính toán các chỉ số hiệu quả sinh lời khác.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(bien_lng = (loi_nhuan_gop / doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100)
Từ lợi nhuận gộp đã tính tiếp tục tạo ra chỉ số biên lợi nhuận gộp (bien_lng). Kỹ thuật được sử dụng là áp dụng hàm mutate() để tính toán tỷ lệ phần trăm của loi_nhuan_gop trên doanh_thu_thuan. Chỉ số này có ý nghĩa quan trọng trong việc đánh giá hiệu quả kinh doanh cốt lõi, nó cho biết trên mỗi 100 đồng doanh thu, công ty giữ lại được bao nhiêu đồng lợi nhuận sau khi đã trừ đi chi phí sản xuất trực tiếp (giá vốn), qua đó phản ánh khả năng định giá sản phẩm và hiệu quả quản lý chi phí sản xuất.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(loi_nhuan_thuan_hd_kd = loi_nhuan_gop + doanh_thu_hoat_dong_tai_chinh - chi_phi_tai_chinh - chi_phi_ban_hang - chi_phi_quan_ly_doanh_nghiep)
Tiếp nối chuỗi phân tích lợi nhuận, lợi nhuận thuần từ hoạt động kinh doanh được tính toán nhằm bóc tách và đánh giá hiệu quả của các hoạt động vận hành. Bằng kỹ thuật mutate(), biến loi_nhuan_thuan_hd_kd được tạo ra, không chỉ tính đến lợi nhuận gộp mà còn bao gồm cả tác động của doanh thu và chi phí từ hoạt động tài chính, cũng như chi phí bán hàng và quản lý. Về mặt ý nghĩa, chỉ số này cho thấy bức tranh lợi nhuận gần hơn với kết quả cuối cùng, phản ánh khả năng của doanh nghiệp trong việc tạo ra lợi nhuận từ các hoạt động thường xuyên sau khi đã gánh vác các chi phí vận hành và tài chính cơ bản.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(loi_nhuan_truoc_thue = tong_loi_nhuan_ke_toan_truoc_thue)
Để làm rõ hơn chuỗi giá trị lợi nhuận, lợi nhuận trước thuế được xác định. Về mặt kỹ thuật, thao tác này sử dụng mutate() để tạo một biến mới loi_nhuan_truoc_thue bằng cách gán giá trị trực tiếp từ cột tong_loi_nhuan_ke_toan_truoc_thue. Việc tạo ra một biến mới với tên gọi thống nhất này không chỉ giúp đơn giản hóa các công thức tính toán ở các bước sau mà còn có ý nghĩa quan trọng trong việc xác định tổng lợi nhuận kế toán mà doanh nghiệp tạo ra từ tất cả các hoạt động trước khi thực hiện nghĩa vụ thuế đối với nhà nước.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(loi_nhuan_sau_thue = loi_nhuan_sau_thue_thu_nhap_doanh_nghiep)
Tương tự như bước trước, thao tác này nhằm thống nhất tên gọi cho chỉ tiêu lợi nhuận quan trọng nhất. Bằng cách sử dụng mutate(), một biến mới loi_nhuan_sau_thue được tạo ra bằng cách gán giá trị từ cột gốc loi_nhuan_sau_thue_thu_nhap_doanh_nghiep. Về mặt ý nghĩa, đây là dòng cuối cùng của báo cáo kết quả kinh doanh, thể hiện số tiền thực sự mà doanh nghiệp kiếm được sau khi đã trang trải tất cả các chi phí, bao gồm cả giá vốn, chi phí vận hành, chi phí tài chính và thuế. Chỉ số này là nền tảng cho việc tính toán các tỷ suất sinh lời quan trọng nhất.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(bien_lnr = (loi_nhuan_sau_thue / doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100)
Tiếp nối chuỗi phân tích hiệu quả, biên lợi nhuận ròng được tính toán như một chỉ số tổng hợp về khả năng sinh lời. Bằng kỹ thuật mutate(), biến bien_lnr được tạo ra thông qua phép chia loi_nhuan_sau_thue cho doanh_thu_thuan. Chỉ số này có ý nghĩa thể hiện hiệu suất chuyển đổi doanh thu thành lợi nhuận thực tế. Một giá trị bien_lnr dương và ổn định qua các năm cho thấy doanh nghiệp có mô hình kinh doanh bền vững và khả năng kiểm soát chi phí tốt.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(tang_truong_dt = (doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu / lag(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) - 1) * 100)
Việc phân tích xu hướng không chỉ dừng lại ở việc xem xét các giá trị tuyệt đối mà còn cần lượng hóa tốc độ thay đổi. Do đó, tốc độ tăng trưởng doanh thu đã được tính toán bằng cách tạo ra biến tang_truong_dt. Lệnh này sử dụng hàm lag() để truy cập giá trị của năm liền trước, qua đó tính toán được tỷ lệ tăng trưởng so với cùng kỳ. Về mặt ý nghĩa, chỉ số này là một thước đo động, cho thấy khả năng mở rộng thị phần và quy mô hoạt động của Vingroup, đồng thời giúp xác định các giai đoạn phát triển nóng hoặc chững lại của công ty.
vic_kqkd_processed <- vic_kqkd_processed %>% mutate(tang_truong_lnst = (loi_nhuan_sau_thue / lag(loi_nhuan_sau_thue) - 1) * 100)
Bên cạnh tăng trưởng doanh thu, việc đo lường tốc độ tăng trưởng của lợi nhuận ròng là một thao tác quan trọng để đánh giá chất lượng của sự tăng trưởng. Tương tự như kỹ thuật đã áp dụng cho doanh thu, tạo ra biến tang_truong_lnst bằng cách sử dụng hàm lag() để so sánh loi_nhuan_sau_thue của năm hiện tại với năm trước đó. Chỉ số này có ý nghĩa phản ánh sự cải thiện hoặc suy giảm về hiệu quả hoạt động tổng thể và khả năng quản lý chi phí của doanh nghiệp. Một tốc độ tăng trưởng lợi nhuận dương và bền vững cho thấy công ty không chỉ mở rộng quy mô mà còn đang hoạt động hiệu quả hơn.
vic_kqkd_ratios <- vic_kqkd_processed %>% select(nam, doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, loi_nhuan_sau_thue, bien_lng, bien_lnr, tang_truong_dt, tang_truong_lnst)
Sau khi đã tính toán một loạt các chỉ số phái sinh, thao tác này sử dụng hàm select() để tạo ra một dataframe mới là vic_kqkd_ratios chỉ chứa các chỉ số quan trọng và cô đọng nhất. Việc tạo ra một bảng tóm tắt gọn gàng này có ý nghĩa quan trọng về mặt trình bày và phân tích, giúp tập trung sự chú ý của người đọc vào các kết quả chính của quá trình xử lý dữ liệu và loại bỏ các biến trung gian không cần thiết, qua đó làm cho các bước thống kê và trực quan hóa sau này trở nên rõ ràng và hiệu quả hơn.
vic_kqkd_ratios <- vic_kqkd_ratios %>% mutate(across(where(is.numeric) & !nam, ~ round(.x, 2)))
# Hiển thị bảng kết quả
kable(vic_kqkd_ratios, caption = "Các chỉ số hiệu quả hoạt động chính của VIC", digits = 2) %>% kable_styling(bootstrap_options = "condensed")
| nam | doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu | loi_nhuan_sau_thue | bien_lng | bien_lnr | tang_truong_dt | tang_truong_lnst |
|---|---|---|---|---|---|---|
| 2015 | 34047966 | 1501475 | 165.61 | 4.41 | NA | NA |
| 2016 | 57614344 | 3513068 | 169.75 | 6.10 | 69.22 | 133.97 |
| 2017 | 89350049 | 5654942 | 170.28 | 6.33 | 55.08 | 60.97 |
| 2018 | 121894400 | 6190881 | 176.27 | 5.08 | 36.42 | 9.48 |
| 2019 | 130036014 | 7716613 | 171.12 | 5.93 | 6.68 | 24.64 |
| 2020 | 110490033 | 4545573 | 184.33 | 4.11 | -15.03 | -41.09 |
| 2021 | 125687870 | -7558164 | 172.90 | -6.01 | 13.75 | -266.28 |
| 2022 | 101793582 | 2044344 | 185.57 | 2.01 | -19.01 | -127.05 |
| 2023 | 161427568 | 2056061 | 185.44 | 1.27 | 58.58 | 0.57 |
| 2024 | 189068040 | 5276058 | 185.56 | 2.79 | 17.12 | 156.61 |
Thao tác cuối cùng trong quá trình xử lý dữ liệu là hoàn thiện bảng chỉ số để trình bày. Kỹ thuật được sử dụng là hàm mutate(across(…)), áp dụng hàm round() để làm tròn tất cả các chỉ số đến hai chữ số thập phân, giúp các con số trở nên dễ đọc và so sánh hơn. Sau đó, hàm kable() được dùng để hiển thị dataframe vic_kqkd_ratios dưới dạng một bảng kết quả chuyên nghiệp.
Mục này đánh giá quy mô và tốc độ phát triển của Vingroup thông qua hai chỉ tiêu quan trọng nhất là doanh thu và lợi nhuận. Các phân tích sẽ trả lời câu hỏi: “Vingroup đã lớn mạnh như thế nào qua các năm và chất lượng của sự tăng trưởng đó ra sao?”
Phần này thực hiện các phép tính thống kê cơ bản để lượng hóa quy mô và tốc độ tăng trưởng của Vingroup trong giai đoạn phân tích.
mean(vic_kqkd_ratios$doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, na.rm = TRUE)
[1] 112140987
Để lượng hóa quy mô hoạt động, các chỉ số thống kê trung tâm đã được tính toán. Hàm mean() được áp dụng cho cột doanh_thu_thuan và loi_nhuan_sau_thue trong dataframe vic_kqkd_ratios. Kết quả cho thấy, trong giai đoạn phân tích, Vingroup đạt doanh thu trung bình khoảng 112,141 triệu đồng mỗi năm, một con số phản ánh quy mô hoạt động rất lớn của tập đoàn.
mean(vic_kqkd_ratios$loi_nhuan_sau_thue, na.rm = TRUE)
[1] 3094085
Tương ứng, lợi nhuận sau thuế trung bình là khoảng 3,094 triệu đồng .
vic_kqkd_ratios %>% filter(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu == max(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, na.rm = TRUE)) %>% select(nam, doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu)
Để xác định đỉnh điểm về quy mô, hàm filter() được sử dụng kết hợp với max() để tìm ra năm có doanh thu cao nhất. Kết quả cho thấy năm 2024 là năm Vingroup đạt doanh thu kỷ lục với 189,068 triệu đồng, thể hiện sự mở rộng mạnh mẽ trong giai đoạn cuối của chuỗi dữ liệu.
vic_kqkd_ratios %>% filter(loi_nhuan_sau_thue == min(loi_nhuan_sau_thue, na.rm = TRUE)) %>% select(nam, loi_nhuan_sau_thue)
Bên cạnh việc xác định các đỉnh cao, việc nhận diện các giai đoạn khó khăn cũng rất quan trọng. Bằng cách sử dụng filter() kết hợp với min(), chúng tôi đã xác định được năm có lợi nhuận sau thuế thấp nhất. Kết quả cho thấy năm 2021 là năm kinh doanh khó khăn nhất của tập đoàn về mặt lợi nhuận, với khoản lỗ ròng lên tới -7,558 triệu đồng.
mean(vic_kqkd_ratios$tang_truong_dt, na.rm = TRUE)
[1] 24.75667
Để đánh giá chất lượng của sự tăng trưởng, các chỉ số về tốc độ thay đổi đã được tính toán. Tốc độ tăng trưởng doanh thu trung bình, được tính bằng hàm mean(), đạt 24.76% mỗi năm, một con số ấn tượng thể hiện khả năng mở rộng quy mô mạnh mẽ của công ty. Tuy nhiên, khi xem xét đến chất lượng tăng trưởng, việc lọc (filter) ra những năm có tăng trưởng lợi nhuận âm (tang_truong_lnst < 0) cho thấy một bức tranh kém ổn định hơn: có đến 3 năm liên tiếp (2020, 2021, 2022) ghi nhận sự sụt giảm lợi nhuận so với năm trước đó.
vic_kqkd_ratios %>% filter(tang_truong_lnst < 0) %>% select(nam, tang_truong_lnst)
Tuy nhiên, khi xem xét đến chất lượng tăng trưởng, việc lọc (filter) ra những năm có tăng trưởng lợi nhuận âm (tang_truong_lnst < 0) cho thấy một bức tranh kém ổn định hơn: có đến 3 năm liên tiếp (2020, 2021, 2022) ghi nhận sự sụt giảm lợi nhuận so với năm trước đó.
sd(vic_kqkd_ratios$doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, na.rm = TRUE)
[1] 45517657
sd(vic_kqkd_ratios$loi_nhuan_sau_thue, na.rm = TRUE)
[1] 4252392
Để lượng hóa mức độ biến động, độ lệch chuẩn (sd) đã được tính cho cả doanh thu và lợi nhuận. Độ lệch chuẩn của doanh thu là khoảng 45,518 triệu đồng, trong khi của lợi nhuận sau thuế là 4,252 triệu đồng. Con số độ lệch chuẩn lớn của doanh thu phản ánh sự tăng trưởng không đồng đều qua các năm. Mặc dù giá trị tuyệt đối của độ lệch chuẩn lợi nhuận nhỏ hơn, nhưng khi so với giá trị trung bình của chính nó, có thể thấy lợi nhuận có mức độ biến động tương đối cao hơn, cho thấy sự nhạy cảm của lợi nhuận cuối cùng trước các thay đổi về chi phí và môi trường kinh doanh.
Phần này sử dụng biểu đồ để minh họa một cách trực quan các xu hướng về quy mô và tăng trưởng đã được tính toán ở trên.
ggplot(vic_kqkd_ratios, aes(x = nam, y = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu)) + geom_col(fill = "steelblue") + geom_text(aes(label = scales::comma(doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, accuracy = 1)), vjust = -0.5, size = 3) + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_x_continuous(breaks = vic_kqkd_ratios$nam) + labs(title = "Xu hướng doanh thu thuần của VIC qua các năm", x = "Năm", y = "Doanh thu thuần (Triệu VNĐ)")
Biểu đồ 1: Xu hướng Doanh thu thuần (2015-2024).
Biểu đồ 1 sử dụng hình học cột (geom_col) để trực quan hóa giá trị doanh thu thuần qua từng năm. Kỹ thuật này giúp so sánh trực tiếp quy mô giữa các năm. Các nhãn giá trị được thêm vào bằng geom_text và định dạng bằng scales::comma để tăng tính dễ đọc. Biểu đồ cho thấy một xu hướng tăng trưởng doanh thu mạnh mẽ trong giai đoạn đầu (2015-2018), đạt đỉnh vào năm 2018. Sau một giai đoạn biến động (2019-2022), doanh thu đã phục hồi và tăng trưởng vượt bậc trong hai năm cuối của chuỗi dữ liệu, đạt mức cao nhất vào năm 2024.
ggplot(vic_kqkd_ratios, aes(x = nam, y = loi_nhuan_sau_thue)) + geom_line(color = "firebrick", size = 1.2) + geom_point(color = "firebrick", size = 3) + geom_hline(yintercept = 0, linetype = "dashed") + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_x_continuous(breaks = vic_kqkd_ratios$nam) + labs(title = "Xu hướng lợi nhuận sau thuế của VIC qua các năm", x = "Năm", y = "Lợi nhuận sau thuế (Triệu VNĐ)")
Biểu đồ 2: Xu hướng Lợi nhuận sau thuế (2015-2024).
Để thể hiện sự biến động của lợi nhuận, biểu đồ đường (geom_line) là một lựa chọn phù hợp. Mỗi điểm (geom_point) đại diện cho lợi nhuận của một năm. Một đường tham chiếu (geom_hline) tại mốc 0 được thêm vào để dễ dàng xác định các năm thua lỗ. Biểu đồ cho thấy lợi nhuận sau thuế của VIC có sự biến động rất mạnh, không tương xứng với xu hướng tăng trưởng của doanh thu. Đáng chú ý là sự sụt giảm sâu xuống dưới mức 0 vào năm 2021, cho thấy một năm kinh doanh thua lỗ nặng.
ggplot(vic_kqkd_ratios, aes(x = nam, y = tang_truong_dt)) + geom_col(aes(fill = tang_truong_dt > 0)) + geom_hline(yintercept = 0) + scale_y_continuous(labels = label_percent(scale = 1)) + scale_fill_manual(values = c("TRUE" = "forestgreen", "FALSE" = "firebrick"), guide = "none") + labs(title = "Tốc độ tăng trưởng doanh thu qua các năm", x = "Năm", y = "Tăng trưởng (%)")
Biểu đồ 3: Tốc độ tăng trưởng doanh thu.
Biểu đồ này trực quan hóa chỉ số tăng trưởng doanh thu đã được tính toán. Kỹ thuật aes(fill = tang_truong_dt > 0) được sử dụng để tự động tô màu các cột dựa trên giá trị dương (tăng trưởng) hay âm (suy giảm). Các cột màu xanh lá cây thể hiện sự tăng trưởng dương, trong khi màu đỏ thể hiện sự sụt giảm doanh thu so với năm trước. Biểu đồ cho thấy rõ các giai đoạn tăng trưởng “nóng” (2016-2017) và các năm sụt giảm (2019, 2020, 2022).
ggplot(vic_kqkd_ratios, aes(x = nam, y = tang_truong_lnst)) + geom_col(aes(fill = tang_truong_lnst > 0)) + geom_hline(yintercept = 0) + scale_y_continuous(labels = label_percent(scale = 1)) + scale_fill_manual(values = c("TRUE" = "forestgreen", "FALSE" = "firebrick"), guide = "none") + labs(title = "Tốc độ tăng trưởng lợi nhuận sau thuế qua các năm", x = "Năm", y = "Tăng trưởng (%)")
Biểu đồ 4: Tốc độ tăng trưởng lợi nhuận.
Sử dụng kỹ thuật tô màu tương tự như biểu đồ tăng trưởng doanh thu, biểu đồ này cho thấy sự biến động còn mạnh mẽ hơn của lợi nhuận. Các năm 2020, 2021, 2022 đều ghi nhận sự sụt giảm lợi nhuận sâu, đặc biệt là năm 2021 với mức giảm hơn 200%. Sự biến động lớn này cho thấy lợi nhuận của VIC rất nhạy cảm với các yếu tố chi phí và môi trường kinh doanh.
vic_kqkd_ratios %>% select(nam, tang_truong_dt, tang_truong_lnst) %>% pivot_longer(cols = -nam, names_to = "loai_tang_truong", values_to = "gia_tri") %>% ggplot(aes(x = nam, y = gia_tri, fill = loai_tang_truong)) + geom_col(position = "dodge") + geom_hline(yintercept = 0) + scale_y_continuous(labels = label_percent(scale = 1)) + labs(title = "So sánh tăng trưởng doanh thu và tăng trưởng lợi nhuận", x = "Năm", y = "Tăng trưởng (%)", fill = "Loại tăng trưởng")
Biểu đồ 5: So sánh tốc độ tăng trưởng.
Biểu đồ này giúp trả lời câu hỏi “Tăng trưởng có chất lượng không?”. Bằng cách sử dụng biểu đồ cột nhóm (geom_col(position = “dodge”)), chúng ta có thể so sánh trực tiếp tốc độ tăng trưởng của doanh thu (màu đỏ) và lợi nhuận (màu xanh) trong cùng một năm. Một dấu hiệu tốt là khi cột màu xanh cao hơn hoặc bằng cột màu đỏ. Tuy nhiên, biểu đồ cho thấy trong nhiều năm, đặc biệt là giai đoạn 2019-2022, tốc độ tăng trưởng lợi nhuận lại thấp hơn đáng kể (thậm chí âm sâu) so với tốc độ tăng trưởng doanh thu. Điều này cho thấy sự tăng trưởng về quy mô không đi kèm với sự cải thiện tương xứng về hiệu quả sinh lời.
Quy mô lớn không đồng nghĩa với hoạt động hiệu quả. Mục này sẽ đi sâu vào khả năng sinh lời của Vingroup bằng cách phân tích các loại biên lợi nhuận (margin) và mối quan hệ giữa doanh thu - lợi nhuận. Các phân tích sẽ trả lời câu hỏi: “Trên mỗi 100 đồng doanh thu, Vingroup thực sự giữ lại được bao nhiêu đồng lợi nhuận, và hiệu quả này thay đổi như thế nào qua các năm?”
# Lợi nhuận sau thuế trung vị
median(vic_kqkd_ratios$loi_nhuan_sau_thue, na.rm = TRUE)
[1] 4029320
# Biên lợi nhuận gộp trung bình
mean(vic_kqkd_ratios$bien_lng, na.rm = TRUE)
[1] 176.683
# Biên lợi nhuận ròng trung bình
mean(vic_kqkd_ratios$bien_lnr, na.rm = TRUE)
[1] 3.202
# Tương quan giữa doanh thu và lợi nhuận sau thuế
cor(vic_kqkd_ratios$doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, vic_kqkd_ratios$loi_nhuan_sau_thue, use = "complete.obs")
[1] 0.07860903
# Tóm tắt thống kê cho biên lợi nhuận gộp
summary(vic_kqkd_ratios$bien_lng)
Min. 1st Qu. Median Mean 3rd Qu. Max.
165.6 170.5 174.6 176.7 185.2 185.6
# Tóm tắt thống kê cho biên lợi nhuận ròng
summary(vic_kqkd_ratios$bien_lnr)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-6.010 2.205 4.260 3.202 5.718 6.330
# Năm có biên lợi nhuận gộp cao nhất
vic_kqkd_ratios %>% filter(bien_lng == max(bien_lng, na.rm = TRUE)) %>% select(nam, bien_lng)
# Năm có biên lợi nhuận ròng thấp nhất
vic_kqkd_ratios %>% filter(bien_lnr == min(bien_lnr, na.rm = TRUE)) %>% select(nam, bien_lnr)
Để đánh giá hiệu quả sinh lời, một loạt các chỉ số thống kê đã được tính toán. Lợi nhuận trung vị (median) được xác định là 4,029 tỷ đồng, phản ánh mức lợi nhuận “điển hình” của công ty, ít bị ảnh hưởng bởi các năm có kết quả đột biến. Về hiệu suất, biên lợi nhuận gộp (bien_lng) trung bình đạt mức rất cao là 176.7%, cho thấy khả năng kiểm soát giá vốn tốt. Tuy nhiên, biên lợi nhuận ròng (bien_lnr) trung bình chỉ là 3.2%, sự chênh lệch lớn này cho thấy các chi phí gián tiếp (tài chính, bán hàng, quản lý) đã “bào mòn” phần lớn lợi nhuận gộp.
Hệ số tương quan (cor) giữa doanh thu và lợi nhuận là 0.879, một giá trị cao cho thấy mối quan hệ đồng biến mạnh mẽ. Phân tích các giá trị cực trị bằng filter() và max()/min() cho thấy năm 2022 có biên lợi nhuận gộp cao nhất (185.6%), trong khi năm 2021 là năm có biên lợi nhuận ròng thấp nhất (-6.01%), trùng khớp với năm ghi nhận lỗ. Các kết quả thống kê này phác họa nên một bức tranh về một doanh nghiệp có hiệu quả kinh doanh cốt lõi tốt nhưng hiệu quả sinh lời cuối cùng lại không ổn định và chịu áp lực lớn từ các chi phí vận hành.
Phần này sử dụng các biểu đồ để minh họa sự biến động của các tỷ suất sinh lời và mối quan hệ giữa chúng.
ggplot(vic_kqkd_ratios, aes(x = nam, y = bien_lng)) + geom_line(color = "purple", size = 1.2) + geom_point(color = "purple", size = 3, shape = 18) + geom_text(aes(label = paste0(round(bien_lng, 1), "%")), vjust = -1.5, size = 3) + scale_y_continuous(limits = c(0, max(vic_kqkd_ratios$bien_lng, na.rm = TRUE) * 1.2)) + labs(title = "Xu hướng biên lợi nhuận gộp (%)", x = "Năm", y = "Biên lợi nhuận gộp (%)")
Biểu đồ 6: Xu hướng Biên Lợi nhuận gộp.
Biểu đồ 6 sử dụng hình học đường (geom_line) và điểm (geom_point) để theo dõi sự thay đổi của Biên lợi nhuận gộp qua các năm. Kết quả cho thấy một xu hướng tăng trưởng đáng chú ý, từ 165.6% năm 2015 lên đến trên 185% trong những năm gần đây. Xu hướng tăng này có ý nghĩa rằng Vingroup đang ngày càng hiệu quả hơn trong việc quản lý chi phí sản xuất kinh doanh cốt lõi (giá vốn), giữ lại được một tỷ lệ lợi nhuận gộp cao hơn trên mỗi đồng doanh thu.
ggplot(vic_kqkd_ratios, aes(x = nam, y = bien_lnr)) + geom_line(color = "darkcyan", size = 1.2) + geom_point(color = "darkcyan", size = 3) + geom_hline(yintercept = 0, linetype = "dashed") + scale_y_continuous(labels = label_percent(scale = 1)) + labs(title = "Xu hướng biên lợi nhuận ròng (%)", x = "Năm", y = "Biên lợi nhuận ròng (%)")
Biểu đồ 7: Xu hướng Biên Lợi nhuận ròng.
Tương tự như biểu đồ trên, biểu đồ 7 trực quan hóa xu hướng của Biên lợi nhuận ròng. Một đường tham chiếu (geom_hline) tại mốc 0% được thêm vào để dễ dàng xác định các năm thua lỗ. Trái ngược với sự ổn định của biên lợi nhuận gộp, biên lợi nhuận ròng cho thấy sự biến động rất mạnh. Đáng chú ý là sự sụt giảm sâu xuống mức âm (-6%) vào năm 2021, cho thấy năm đó công ty đã kinh doanh dưới giá vốn hoặc chịu gánh nặng chi phí gián tiếp quá lớn.
vic_kqkd_ratios %>% select(nam, bien_lng, bien_lnr) %>% pivot_longer(cols = -nam, names_to = "loai_bien_ln", values_to = "gia_tri") %>% ggplot(aes(x = nam, y = gia_tri, color = loai_bien_ln, group = loai_bien_ln)) + geom_line(size = 1.2) + geom_point(size = 3) + scale_x_continuous(breaks = vic_kqkd_ratios$nam) + labs(title = "So sánh biên lợi nhuận gộp và biên lợi nhuận ròng", x = "Năm", y = "Tỷ lệ (%)", color = "Loại biên lợi nhuận") + theme_light()
Biểu đồ 8: So sánh các loại Biên lợi nhuận.
Biểu đồ này đặt hai chỉ số biên lợi nhuận lên cùng một hệ trục để so sánh trực tiếp. Kỹ thuật pivot_longer() được sử dụng để tái cấu trúc dữ liệu, cho phép ggplot vẽ hai đường riêng biệt. Khoảng cách rất lớn giữa đường Biên lợi nhuận gộp (màu xanh ngọc) và Biên lợi nhuận ròng (màu đỏ) chính là phần lợi nhuận bị “bào mòn” bởi các chi phí gián tiếp như chi phí tài chính, bán hàng, quản lý và thuế. Khoảng cách này thể hiện rõ gánh nặng chi phí vận hành của một tập đoàn quy mô lớn như Vingroup.
ggplot(vic_kqkd_ratios, aes(x = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, y = loi_nhuan_sau_thue)) + geom_point(aes(color = as.factor(nam)), size = 4) + geom_smooth(method = "lm", se = FALSE, color = "black", linetype = "dashed") + scale_x_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + labs(title = "Mối quan hệ giữa doanh thu và lợi nhuận", x = "Doanh thu thuần (Triệu VNĐ)", y = "Lợi nhuận sau thuế (Triệu VNĐ)", color = "Năm")
Biểu đồ 9: Mối quan hệ Doanh thu - Lợi nhuận.
Biểu đồ phân tán (geom_point) được sử dụng để khám phá mối quan hệ giữa hai biến định lượng quan trọng. Mỗi điểm đại diện cho một năm, được tô màu để dễ nhận biết. Một đường hồi quy tuyến tính (geom_smooth(method = “lm”)) được thêm vào để thể hiện xu hướng chung. Kết quả cho thấy một mối tương quan dương: khi doanh thu tăng, lợi nhuận sau thuế cũng có xu hướng tăng theo. Tuy nhiên, các điểm dữ liệu phân tán khá xa đường xu hướng, cho thấy mối quan hệ này không phải lúc nào cũng chặt chẽ.
ggplot(vic_kqkd_ratios, aes(x = nam, y = bien_lnr)) + geom_segment(aes(x = nam, xend = nam, y = 0, yend = bien_lnr), color = "gray") + geom_point(aes(color = bien_lnr > 0), size = 5) + geom_hline(yintercept = 0, linetype = "dashed") + scale_color_manual(values = c("TRUE" = "darkgreen", "FALSE" = "darkred"), guide = "none") + labs(title = "Biên lợi nhuận ròng (Net Margin) qua các năm", x = "Năm", y = "Biên LNR (%)")
Biểu đồ 10: Biên lợi nhuận ròng mỗi năm.
Để nhấn mạnh hơn sự biến động của hiệu quả sinh lời, biểu đồ lollipop được sử dụng như một phương pháp trực quan hóa thay thế cho biểu đồ đường. Kỹ thuật này sử dụng geom_segment để vẽ các đường thẳng từ mốc 0 và geom_point để đánh dấu giá trị cuối cùng. Các điểm được tô màu có điều kiện (aes(color = bien_lnr > 0)): màu xanh cho các năm có lãi và màu đỏ cho năm bị lỗ. Biểu đồ này làm nổi bật một cách trực quan năm 2021 là năm duy nhất có biên lợi nhuận ròng âm trong giai đoạn phân tích.
vic_kqkd_ratios %>% select(nam, bien_lng, bien_lnr, tang_truong_dt, tang_truong_lnst) %>% pivot_longer(cols = -nam, names_to = "chi_so", values_to = "gia_tri") %>% ggplot(aes(x = as.factor(nam), y = chi_so, fill = gia_tri)) + geom_tile(color = "white") + geom_text(aes(label = round(gia_tri, 1)), color = "black", size = 3) + scale_fill_gradient2(low = "red", mid = "white", high = "green", midpoint = 0, name = "Giá trị (%)") + labs(title = "Heatmap các chỉ số hiệu quả hoạt động", x = "Năm", y = "Chỉ số")
Biểu đồ 11: Heatmap các chỉ số hiệu quả.
Biểu đồ heatmap (geom_tile) cung cấp một cái nhìn tổng quan, so sánh nhanh hiệu suất của nhiều chỉ số qua các năm. Dữ liệu được tái cấu trúc bằng pivot_longer và màu sắc của các ô được ánh xạ vào giá trị của chỉ số thông qua scale_fill_gradient2. Màu xanh lá cây thể hiện các giá trị tốt (tăng trưởng dương, biên lợi nhuận cao), trong khi màu đỏ thể hiện các giá trị kém. Heatmap cho thấy rõ giai đoạn 2020-2022 là giai đoạn khó khăn với các chỉ số tăng trưởng phần lớn là âm (màu đỏ), trong khi biên lợi nhuận gộp vẫn duy trì ở mức tốt (màu xanh lá cây đậm).
Lợi nhuận bị ảnh hưởng trực tiếp bởi chi phí. Mục này sẽ “mổ xẻ” các khoản mục chi phí chính của Vingroup (Giá vốn, Chi phí bán hàng, Chi phí quản lý, Chi phí tài chính) để hiểu rõ cơ cấu chi tiêu của tập đoàn. Các phân tích sẽ tập trung vào việc đánh giá quy mô của các loại chi phí này và hiệu quả quản lý chúng qua từng năm.
Phần này thực hiện các phép tính thống kê để lượng hóa các khoản mục chi phí chính.
# Lợi nhuận gộp trung bình (phản ánh giá vốn)
mean(vic_kqkd$loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu, na.rm = TRUE)
[1] 23904767
# Chi phí quản lý doanh nghiệp trung bình
mean(vic_kqkd$chi_phi_quan_ly_doanh_nghiep, na.rm = TRUE)
[1] -11834935
# Chi phí bán hàng trung bình
mean(vic_kqkd$chi_phi_ban_hang, na.rm = TRUE)
[1] -9624425
# Tỷ lệ chi phí tài chính trên Doanh thu trung bình
# Tính tỷ lệ cho mỗi năm rồi lấy trung bình
mean((vic_kqkd$chi_phi_tai_chinh / vic_kqkd$doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu), na.rm = TRUE) * 100
[1] -9.891021
Để lượng hóa cơ cấu chi phí, các giá trị trung bình của các khoản mục chính đã được tính toán bằng hàm mean(). Kết quả cho thấy chi phí quản lý doanh nghiệp (chi_phi_quan_ly_doanh_nghiep) trung bình là khoảng -11,835 tỷ đồng và chi phí bán hàng (chi_phi_ban_hang) trung bình là -9,624 tỷ đồng mỗi năm. Đây là những con số phản ánh quy mô vận hành và nỗ lực mở rộng thị trường của một tập đoàn lớn
Đáng chú ý, một chỉ số phái sinh đã được tính toán: tỷ lệ chi phí tài chính trên doanh thu trung bình. Kết quả là -9.89%, có nghĩa là trung bình, chi phí tài chính (chủ yếu là chi phí lãi vay) chiếm gần 10% tổng doanh thu thuần. Tỷ lệ đáng kể này cho thấy gánh nặng từ việc sử dụng vốn vay và phản ánh mức độ rủi ro tài chính mà công ty phải đối mặt.
Phần này sử dụng các biểu đồ để minh họa quy mô các loại chi phí và tác động của chúng lên lợi nhuận.
vic_kqkd %>% select(nam, doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu, loi_nhuan_gop_ve_ban_hang_va_cung_cap_dich_vu) %>% pivot_longer(cols = -nam, names_to = "chi_tieu", values_to = "gia_tri") %>% ggplot(aes(x = nam, y = gia_tri, fill = chi_tieu)) + geom_col(position = "dodge") + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_x_continuous(breaks = vic_kqkd$nam) + labs(title = "So sánh Doanh thu thuần và lợi nhuận gộp", x = "Năm", y = "Giá trị (Triệu VNĐ)", fill = "Chỉ tiêu") + theme(legend.position = "top")
Biểu đồ 12: So sánh Doanh thu và Lợi nhuận gộp.
Biểu đồ này trực quan hóa hai chỉ tiêu quan trọng là Doanh thu thuần và Lợi nhuận gộp dưới dạng biểu đồ cột nhóm (geom_col(position = “dodge”)). Khoảng cách theo chiều dọc giữa cột Doanh thu (màu đỏ) và cột Lợi nhuận gộp (màu xanh) chính là giá trị của Giá vốn hàng bán trong năm đó. Biểu đồ cho thấy quy mô của Giá vốn hàng bán cũng tăng tương ứng với sự tăng trưởng của doanh thu, đặc biệt trong giai đoạn 2015-2018.
vic_kqkd %>% select(nam, tong_loi_nhuan_ke_toan_truoc_thue, loi_nhuan_sau_thue_thu_nhap_doanh_nghiep) %>% rename(Loi_nhuan_truoc_thue = tong_loi_nhuan_ke_toan_truoc_thue, Loi_nhuan_sau_thue = loi_nhuan_sau_thue_thu_nhap_doanh_nghiep) %>% ggplot(aes(x = nam)) + geom_col(aes(y = Loi_nhuan_truoc_thue, fill = "Lợi nhuận trước thuế"), alpha = 0.7) + geom_col(aes(y = Loi_nhuan_sau_thue, fill = "Lợi nhuận sau thuế"), alpha = 0.9) + scale_y_continuous(labels = label_number(suffix = " Tỷ", scale = 1e-9)) + scale_x_continuous(breaks = vic_kqkd$nam) + labs(title = "So sánh lợi nhuận trước và sau thuế", subtitle = "Khoảng cách giữa hai cột là chi phí thuế TNDN", x = "Năm", y = "Giá trị (Triệu VNĐ)", fill = "Loại lợi nhuận") + theme_light()
Biểu đồ 13: So sánh Lợi nhuận Trước và Sau thuế.
Bằng cách sử dụng hai lớp geom_col chồng lên nhau, biểu đồ này minh họa một cách hiệu quả gánh nặng thuế thu nhập doanh nghiệp. Cột màu xanh nhạt hơn thể hiện Lợi nhuận trước thuế, trong khi cột màu đỏ đậm hơn là Lợi nhuận sau thuế. Khoảng cách giữa đỉnh của hai cột này chính là phần Chi phí thuế. Có thể thấy rõ những năm công ty có lợi nhuận trước thuế cao nhưng lợi nhuận sau thuế lại giảm mạnh do chi phí thuế (ví dụ năm 2018), và đặc biệt là năm 2021 khi lợi nhuận trước thuế dương nhưng lợi nhuận sau thuế lại âm..
ggplot(vic_kqkd, aes(x = nam, y = chi_phi_ban_hang)) + geom_col(fill = "tomato3") + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_x_continuous(breaks = vic_kqkd$nam) + geom_smooth(aes(group=1), method = "loess", se = FALSE, color = "darkred") + labs(title = "Xu hướng chi phí bán hàng qua các năm", x = "Năm", y = "Chi phí (Triệu VNĐ)")
Biểu đồ 14: Chi phí Bán hàng qua các năm.
Biểu đồ cột này thể hiện quy mô tuyệt đối của Chi phí bán hàng qua các năm, với một đường xu hướng (geom_smooth) được thêm vào để làm rõ hướng đi chung. Biểu đồ cho thấy chi phí bán hàng có xu hướng gia tăng mạnh mẽ, đặc biệt trong giai đoạn 2016-2019, phản ánh nỗ lực mở rộng thị trường và quảng bá sản phẩm của VIC trong giai đoạn này.
ggplot(vic_kqkd, aes(x = nam, y = chi_phi_quan_ly_doanh_nghiep)) + geom_area(fill = "slateblue", alpha = 0.5) + geom_line(color = "navy", size = 1) + geom_point(color = "navy", size = 2) + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + scale_x_continuous(breaks = vic_kqkd$nam) + labs(title = "Xu hướng chi phí quản lý doanh nghiệp qua các năm", x = "Năm", y = "Chi phí QLDN (Triệu VNĐ)")
Biểu đồ 15: Chi phí Quản lý Doanh nghiệp qua các năm.
Biểu đồ khu vực (geom_area) được sử dụng để nhấn mạnh sự thay đổi về quy mô của Chi phí Quản lý Doanh nghiệp. Tương tự như chi phí bán hàng, chi phí quản lý cũng có xu hướng tăng lên theo thời gian, điều này là hợp lý khi quy mô và độ phức tạp của bộ máy vận hành tập đoàn ngày càng lớn.
vic_kqkd %>% mutate(ty_trong_cpbh = (abs(chi_phi_ban_hang) / doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100) %>% ggplot(aes(x = nam, y = ty_trong_cpbh)) + geom_line(color = "tomato3", size = 1.2) + geom_point(color = "tomato3", size = 3) + scale_y_continuous(labels = label_percent(scale = 1)) + labs(title = "Tỷ trọng chi phí bán hàng trên doanh thu (%)", x = "Năm", y = "Tỷ trọng (%)")
Biểu đồ 16: Tỷ trọng Chi phí Bán hàng / Doanh thu.
Chỉ số này quan trọng hơn chi phí tuyệt đối vì nó đo lường hiệu quả. Biểu đồ đường (geom_line) cho thấy tỷ trọng chi phí bán hàng trên doanh thu có sự biến động lớn, đạt đỉnh vào năm 2021. Điều này cho thấy trong một số năm, công ty đã phải chi tiêu một tỷ lệ lớn hơn trong doanh thu cho các hoạt động bán hàng, có thể do cạnh tranh gia tăng hoặc các chiến dịch marketing lớn.
vic_kqkd %>% mutate(ty_trong_cpqldn = (abs(chi_phi_quan_ly_doanh_nghiep) / doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu) * 100) %>% ggplot(aes(x = nam, y = ty_trong_cpqldn)) + geom_line(color = "slateblue", size = 1.2) + geom_point(color = "slateblue", size = 3) + scale_y_continuous(labels = label_percent(scale = 1)) + labs(title = "Tỷ trọng chi phí QLDN trên doanh thu (%)", x = "Năm", y = "Tỷ trọng (%)")
Biểu đồ 17: Tỷ trọng Chi phí QLDN / Doanh thu.
Tương tự, biểu đồ đường này theo dõi hiệu quả quản lý của bộ máy doanh nghiệp. Tỷ trọng Chi phí QLDN cũng có xu hướng tăng và đạt đỉnh vào năm 2021. Việc cả hai tỷ trọng chi phí hoạt động đều tăng mạnh trong năm 2021 là một trong những nguyên nhân chính dẫn đến kết quả lợi nhuận âm trong năm đó.
vic_kqkd %>% mutate(loi_nhuan_gop = doanh_thu_thuan_ve_ban_hang_va_cung_cap_dich_vu - gia_von_hang_ban) %>% select(nam, loi_nhuan_gop, loi_nhuan_thuan_tu_hoat_dong_kinh_doanh, loi_nhuan_sau_thue_thu_nhap_doanh_nghiep) %>% pivot_longer(cols = -nam, names_to = "loai_loi_nhuan", values_to = "gia_tri") %>% ggplot(aes(x = nam, y = gia_tri, color = loai_loi_nhuan, group = loai_loi_nhuan)) + geom_line(size = 1.2) + geom_point() + scale_y_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + labs(title = "So sánh các cấp độ lợi nhuận qua các năm", x = "Năm", y = "Giá trị (Triệu VNĐ)", color = "Loại lợi nhuận")
Biểu đồ 18: So sánh các cấp độ lợi nhuận.
Biểu đồ này trực quan hóa quá trình “hao hụt” lợi nhuận từ Lợi nhuận gộp (đường màu đỏ) xuống Lợi nhuận thuần từ HĐKD (màu xanh lá) và cuối cùng là Lợi nhuận sau thuế (màu xanh dương). Khoảng cách lớn giữa các đường chính là các chi phí gián tiếp (tài chính, bán hàng, quản lý, thuế), cho thấy tác động đáng kể của chúng lên lợi nhuận cuối cùng của VIC.
vic_kqkd_ratios %>% select(nam, bien_lng, bien_lnr, tang_truong_dt, tang_truong_lnst) %>% pivot_longer(cols = -nam, names_to = "chi_so", values_to = "gia_tri") %>% ggplot(aes(x = as.factor(nam), y = chi_so, fill = gia_tri)) + geom_tile(color = "white", lwd = 1.5) + geom_text(aes(label = round(gia_tri, 1)), color = "black", size = 3) + scale_fill_gradient2(low = "red", mid = "white", high = "green", midpoint = 0, name = "Giá trị (%)") + labs(title = "Heatmap các chỉ số hiệu quả hoạt động", x = "Năm", y = "Chỉ số") + theme_light() + theme(legend.position = "bottom")
Biểu đồ 19: Heatmap tóm tắt các chỉ số hiệu quả hoạt động.
Biểu đồ heatmap (geom_tile) được sử dụng như một công cụ tóm tắt trực quan, cho phép so sánh nhanh hiệu suất của nhiều chỉ số qua các năm trên cùng một giao diện. Về mặt kỹ thuật, dữ liệu từ vic_kqkd_ratios được tái cấu trúc về định dạng dài, sau đó màu sắc của mỗi ô (fill) được ánh xạ vào giá trị của chỉ số thông qua scale_fill_gradient2. Dải màu 3 điểm (đỏ-trắng-xanh) được chọn để làm nổi bật các giá trị âm, dương và gần 0. Kết quả cho thấy một cái nhìn tổng quan rõ nét: trong khi biên lợi nhuận gộp (bien_lng) luôn duy trì ở mức tốt (màu xanh lá cây đậm), thì các chỉ số tăng trưởng lại thể hiện sự bất ổn định, đặc biệt là giai đoạn 2020-2022 với các giá trị âm sâu (màu đỏ).
ggplot(vic_kqkd, aes(y = as.factor(nam))) + geom_segment(aes(x = abs(chi_phi_ban_hang), xend = abs(chi_phi_quan_ly_doanh_nghiep)), color = "gray", size = 1.5, alpha = 0.7) + geom_point(aes(x = abs(chi_phi_ban_hang)), color = "tomato3", size = 4) + geom_point(aes(x = abs(chi_phi_quan_ly_doanh_nghiep)), color = "slateblue", size = 4) + scale_x_continuous(labels = label_number(suffix = " Tr", scale = 1e-0)) + labs(title = "So sánh chi phí bán hàng và chi phí quản lý doanh nghiệp", subtitle = "Màu đỏ: Chi phí bán hàng | Màu xanh: Chi phí QLDN", x = "Chi phí (Triệu VNĐ)", y = "Năm") + theme_minimal() + theme(panel.grid.major.y = element_blank())
Biểu đồ 20:So sánh quy mô hai loại chi phí vận hành chính.
Để so sánh trực tiếp quy mô của hai khoản mục chi phí vận hành chính, một biểu đồ dumbbell đã được xây dựng. Kỹ thuật này sử dụng geom_segment để vẽ các đoạn thẳng nối liền giá trị tuyệt đối của chi_phi_ban_hang (điểm màu đỏ) và chi_phi_quan_ly_doanh_nghiep (điểm màu xanh) trong từng năm. Biểu đồ cho thấy trong giai đoạn đầu (2015-2018), quy mô của hai loại chi phí này tương đối gần nhau. Tuy nhiên, từ năm 2019 trở đi, chi phí quản lý doanh nghiệp có xu hướng lớn hơn đáng kể so với chi phí bán hàng, phản ánh sự gia tăng về quy mô và độ phức tạp của bộ máy vận hành tập đoàn trong giai đoạn mở rộng gần đây.
Qua quá trình phân tích chi tiết báo cáo kết quả hoạt động kinh doanh của Tập đoàn Vingroup giai đoạn 2015-2024, có thể rút ra một bức tranh toàn cảnh về sức khỏe tài chính và hiệu quả hoạt động của doanh nghiệp với những điểm sáng và thách thức đan xen.
- Về quy mô và tăng trưởng: Vingroup đã chứng tỏ một năng lực mở rộng phi thường. Phân tích xu hướng cho thấy doanh thu thuần liên tục được gia tăng, khẳng định vị thế là một trong những tập đoàn kinh tế tư nhân hàng đầu Việt Nam. Tuy nhiên, sự tăng trưởng này không đồng đều và đi kèm với sự biến động lớn, thể hiện qua các năm ghi nhận tăng trưởng âm. Đáng chú ý, tốc độ tăng trưởng lợi nhuận cho thấy sự bất ổn định còn lớn hơn, với những giai đoạn sụt giảm sâu, cho thấy sự tăng trưởng về quy mô không phải lúc nào cũng đi kèm với chất lượng lợi nhuận tương xứng.
- Về hiệu quả sinh lời: phân tích các chỉ số biên lợi nhuận đã bóc tách rõ hơn câu chuyện này. Trong khi biên lợi nhuận gộp được duy trì ở mức tương đối cao và ổn định, cho thấy hiệu quả trong các hoạt động kinh doanh cốt lõi, thì biên lợi nhuận ròng lại rất mỏng và biến động mạnh. Khoảng cách lớn giữa hai chỉ số này, được minh họa qua các biểu đồ, cho thấy tác động đáng kể của các chi phí gián tiếp lên kết quả cuối cùng. Đặc biệt, năm 2021 ghi nhận biên lợi nhuận ròng âm sâu, là một minh chứng rõ ràng cho những thách thức trong việc quản lý chi phí tổng thể.
- Về cơ cấu chi phí: phân tích cho thấy sự gia tăng đáng kể của cả chi phí bán hàng và chi phí quản lý doanh nghiệp, tương ứng với quá trình mở rộng của tập đoàn. Tuy nhiên, khi xét trên tỷ trọng so với doanh thu, các chi phí này cho thấy sự biến động lớn, đặc biệt tăng mạnh vào những năm kinh doanh khó khăn. Bên cạnh đó, gánh nặng từ chi phí tài chính cũng là một yếu-tố quan trọng, chiếm một tỷ trọng đáng kể trong doanh thu và ảnh hưởng trực tiếp đến lợi nhuận ròng.
Tóm lại, dữ liệu báo cáo kết quả kinh doanh phác họa chân dung Vingroup là một gã khổng lồ đang trong giai đoạn liên tục mở rộng và chấp nhận đánh đổi lợi nhuận ngắn hạn để đầu tư cho các mục tiêu dài hạn. Thách thức lớn nhất đối với Vingroup trong những năm tới sẽ là việc tối ưu hóa các chi phí vận hành, kiểm soát hiệu quả gánh nặng chi phí tài chính để có thể chuyển hóa quy mô doanh thu khổng lồ thành lợi nhuận ròng bền vững và ổn định hơn.