Bộ dữ liệu “Global House Purchase” là một tập hợp thông tin quy mô lớn, bao gồm 200.000 quan sát trên 25 biến khác nhau, với mục tiêu cốt lõi là phân tích và dự báo decision (Quyết định Mua nhà) của khách hàng (0 = Không mua, 1 = Mua).
Tập dữ liệu được thiết kế để nắm bắt các yếu tố đa chiều ảnh hưởng đến hành vi tiêu dùng bất động sản, trải rộng từ các yếu tố định lượng cứng như price (giá), property_size_sqft (diện tích), và customer_salary (thu nhập), đến các yếu tố phi tài chính và cảm tính như satisfaction_score (điểm hài lòng) và neighbourhood_rating (xếp hạng khu vực).
Phạm vi địa lý rộng lớn của dữ liệu, bao gồm các biến country và city, giúp bài tiểu luận có thể so sánh và tìm kiếm những yếu tố dự báo có ý nghĩa thống kê trên nhiều thị trường khác nhau.
knitr::opts_chunk$set(dev = "ragg_png")
df <- read.csv("C:/Users/Admin/Downloads/data.csv",header = TRUE)
Dòng code này thực hiện chức năng cơ bản và bắt buộc trong mọi dự án R:
Nạp dữ liệu từ một tệp tin bên ngoài (tệp tin .csv với đường dẫn tuyệt đối) vào bộ nhớ của R, sau đó lưu trữ toàn bộ nội dung đó dưới dạng một đối tượng data.frame có tên là df. Đối tượng df sẽ là cơ sở dữ liệu cho mọi phân tích tiếp theo của bài tiểu luận.
Trong đó:
df <-: Đây là toán tử gán.
Mục đích là lưu trữ kết quả của hàm read.csv() vào một biến (object) mới có tên là df trong môi trường làm việc của R. Việc gán này là bắt buộc để có thể truy cập và thao tác với dữ liệu sau này.
read.csv(): Đây là hàm cơ sở (base function) của R.
Mục đích của hàm này là đọc các tệp tin văn bản có định dạng CSV (Comma Separated Values), nơi các giá trị được phân tách chủ yếu bằng dấu phẩy (hoặc dấu chấm phẩy, tùy theo thiết lập ngôn ngữ mặc định).v
“C:/Users/Admin/Downloads/data.csv”: Đây là tham số file (bắt buộc).
Mục đích là chỉ định chính xác đường dẫn tuyệt đối và tên tệp tin mà R cần tìm và đọc.
header = TRUE: Đây là tham số header (tùy chọn, nhưng mặc định).
Mục đích là báo cho hàm read.csv() biết rằng hàng đầu tiên của tệp tin CSV chứa tên các biến (tên cột) mà chúng ta muốn sử dụng trong data.frame, thay vì coi chúng là một dòng dữ liệu.
Phân tích kĩ thuật
Lệnh này đã thực thi thành công việc đọc tệp tin data.csv và tạo ra đối tượng df với 200.000 dòng và 25 biến.
Ý nghĩa thống kê
Việc nạp thành công 200.000 quan sát tạo ra một cơ sở dữ liệu có sức mạnh thống kê rất cao.
Các kiểm định giả thuyết và mô hình dự báo (ví dụ: mô hình Hồi quy Logistic) được xây dựng trên cỡ mẫu lớn này sẽ có độ tin cậy và khả năng khái quát hóa cao, giúp các kết luận kinh tế trở nên vững chắc hơn.
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.1
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.1
library(broom)
## Warning: package 'broom' was built under R version 4.5.1
library(pROC)
## Warning: package 'pROC' was built under R version 4.5.1
## Type 'citation("pROC")' for a citation.
##
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
library(scales)
## Warning: package 'scales' was built under R version 4.5.1
library(ggridges)
## Warning: package 'ggridges' was built under R version 4.5.1
library(GGally)
## Warning: package 'GGally' was built under R version 4.5.1
library(knitr)
## Warning: package 'knitr' was built under R version 4.5.1
Các dòng code này thực hiện việc nạp (load) các gói (package) cần thiết vào môi trường làm việc hiện tại của R.
Mục đích là để kích hoạt tất cả các hàm và công cụ đặc thù từ các gói đó (như %>%, ggplot(), vif(), auc()) để ta có thể sử dụng chúng trong các bước tiền xử lý, phân tích thống kê và trực quan hóa dữ liệu tiếp theo.
Tất cả các dòng đều sử dụng hàm library() với mục đích nạp một gói đã được cài đặt (Installed Package).
Dưới đây là mục đích cụ thể của từng gói:
1.library(dplyr): Gói cốt lõi của Tidyverse.
Mục đích: Cung cấp các hàm ngữ pháp mạnh mẽ và dễ đọc như %>% (pipe), mutate() (tạo/chỉnh sửa biến), group_by() và summarise() (thống kê mô tả), giúp cho việc xử lý và biến đổi dữ liệu trở nên hiệu quả.
2.library(ggplot2): Gói chuẩn để tạo đồ thị trong R.
Mục đích: Cung cấp hệ thống ngữ pháp đồ họa để xây dựng các biểu đồ phức tạp và chuyên nghiệp (như Histogram, Boxplot, Scatter plot) cho phần trực quan hóa dữ liệu (data visualization) của bài tiểu luận.
3.library(broom):
Mục đích: Chuyển đổi kết quả của các mô hình thống kê (như lm, glm, t.test) thành các bảng dữ liệu (data frame) sạch sẽ, chuẩn hóa (hàm tidy()), dễ đọc và sẵn sàng để trình bày trong R Markdown.
4.library(pROC):
Mục đích: Chuyên dùng để phân tích hiệu suất của các mô hình phân loại (như Hồi quy Logistic). Cung cấp hàm để tính toán và tạo ra đường cong ROC (Receiver Operating Characteristic) và chỉ số AUC (Area Under the Curve), đo lường khả năng dự báo của mô hình.
5.library(scales):
Mục đích: Cung cấp các hàm để định dạng trục tọa độ trong ggplot2 và định dạng số/phần trăm trong văn bản (ví dụ: percent(), comma()), đảm bảo các con số trong bài tiểu luận tuân thủ các chuẩn trình bày (như chuẩn kế toán Việt Nam).
6.library(ggridges):
Mục đích: Mở rộng khả năng của ggplot2 để tạo Biểu đồ Mật độ Dạng Dãy (Ridge Plots), rất hữu ích để trực quan hóa sự khác biệt trong phân phối của một biến định lượng (ví dụ: price hoặc customer_salary) giữa nhiều nhóm phân loại khác nhau một cách trực quan.
7.library(GGally):
Mục đích: Cung cấp hàm ggpairs() để tạo ra ma trận biểu đồ (matrix plot), cho phép trực quan hóa mối quan hệ và phân phối của nhiều biến số định lượng một cách đồng thời, phục vụ bước kiểm tra nhanh mối tương quan giữa các biến.
8.library(knitr):
Mục đích: Cung cấp hàm kable() (knitable and beautiful table) để tạo ra các bảng dữ liệu có định dạng đẹp, chuẩn mực và chuyên nghiệp trong các tài liệu báo cáo R Markdown (như HTML, PDF, Word).
Phân tích kĩ thuật
Việc nạp thành công các gói này xác nhận rằng môi trường R đã được thiết lập đầy đủ cho việc nghiên cứu. Đặc biệt, việc nạp các gói mạnh mẽ như dplyr và ggplot2 sẽ cho phép sử dụng cú pháp Tidyverse, giúp code trở nên sạch sẽ, dễ bảo trì và dễ hiểu hơn rất nhiều.
Ý nghĩa thống kê
Việc lựa chọn 7 gói thư viện này xác định rõ ràng phương pháp luận của bài tiểu luận sẽ nghiêng về Phân tích Định lượng, có tính kỷ luật cao và chú trọng vào chất lượng mô hình. Sự kết hợp giữa Tidyverse (dplyr, ggplot2) và các gói chuyên biệt (broom, pROC) cho thấy phương pháp luận không chỉ tập trung vào việc xử lý dữ liệu nhanh chóng mà còn đánh giá tính hiệu dụng kinh tế của mô hình dự báo (với pROC).
Điều này đảm bảo rằng các kết luận về ảnh hưởng kinh tế của các biến (như customer_salary, emi_to_income_ratio) sẽ có độ tin cậy cao, dựa trên các ước lượng không bị sai lệch bởi các lỗi thống kê cơ bản.
dim(df)
## [1] 200000 25
Lệnh dim(df) là một hàm cơ sở (base R function) có mục đích xác định kích thước của đối tượng data.frame có tên là df.
Kết quả trả về là một vector số nguyên, trong đó giá trị đầu tiên là số hàng (rows/quan sát) và giá trị thứ hai là số cột (columns/biến).
Trong đó:
dim(): Đây là hàm cơ sở (base function) của R. Mục đích là để lấy thuộc tính “dimension” (kích thước) của một đối tượng.
df: Đây là tham số bắt buộc. Mục đích là chỉ định đối tượng data.frame đã được nạp ở bước trước, mà chúng ta muốn kiểm tra kích thước.
Kết quả kĩ thuật
Kết quả này xác nhận rằng đối tượng df là một ma trận dữ liệu có 200.000 hàng và 25 cột.
200.000 hàng: Đại diện cho số lượng quan sát (observations), tức là số lượng hồ sơ bất động sản hoặc giao dịch được ghi nhận trong bộ dữ liệu.
25 cột: Đại diện cho số lượng biến (variables), tức là các thuộc tính (như giá, thu nhập, quốc gia, loại hình nhà ở) được đo lường cho mỗi quan sát.
Ý nghĩa thống kê
Với \(N=200.000\) quan sát, bài tiểu luận sở hữu một cỡ mẫu cực kỳ lớn. Điều này đảm bảo rằng các mối quan hệ (ví dụ: tương quan, hệ số hồi quy) được tìm thấy sẽ có sức mạnh thống kê cao và độ tin cậy lớn. Đây là nền tảng vững chắc để đưa ra các kết luận kinh tế về hành vi thị trường bất động sản.
Khả năng khái quát hóa: Kích thước dữ liệu lớn giúp các ước lượng mô hình (như mô hình Hồi quy Logistic) ít bị ảnh hưởng bởi các giá trị ngoại lệ (outliers) hoặc sự ngẫu nhiên của việc lấy mẫu.
Do đó, các kết luận kinh tế rút ra từ mô hình có khả năng khái quát hóa cao hơn so với các nghiên cứu có cỡ mẫu nhỏ.
head(df, 5)
## property_id country city property_type furnishing_status
## 1 1 France Marseille Farmhouse Semi-Furnished
## 2 2 South Africa Cape Town Apartment Semi-Furnished
## 3 3 South Africa Johannesburg Farmhouse Semi-Furnished
## 4 4 Germany Frankfurt Farmhouse Semi-Furnished
## 5 5 South Africa Johannesburg Townhouse Fully-Furnished
## property_size_sqft price constructed_year previous_owners rooms bathrooms
## 1 991 412935 1989 6 6 2
## 2 1244 224538 1990 4 8 8
## 3 4152 745104 2019 5 2 1
## 4 3714 1110959 2008 1 3 3
## 5 531 99041 2007 6 3 3
## garage garden crime_cases_reported legal_cases_on_property customer_salary
## 1 1 1 1 0 10745
## 2 1 1 1 1 16970
## 3 1 1 0 0 21914
## 4 0 1 0 0 17980
## 5 1 1 3 1 17676
## loan_amount loan_tenure_years monthly_expenses down_payment
## 1 193949 15 6545 218986
## 2 181465 20 8605 43073
## 3 307953 30 2510 437151
## 4 674720 15 8805 436239
## 5 65833 25 8965 33208
## emi_to_income_ratio satisfaction_score neighbourhood_rating
## 1 0.16 1 5
## 2 0.08 9 1
## 3 0.09 6 8
## 4 0.33 2 6
## 5 0.03 3 3
## connectivity_score decision
## 1 6 0
## 2 2 0
## 3 1 0
## 4 6 0
## 5 4 0
Lệnh head(df, 5) là một bước khám phá dữ liệu ban đầu. Mục đích là xem 5 hàng đầu tiên của data.frame (df) để thực hiện kiểm tra sơ bộ, xác minh trực quan rằng dữ liệu đã được nạp chính xác, và kiểm tra định dạng, kiểu dữ liệu và phạm vi giá trị của các biến.
Hàm được sử dụng là head(), một hàm cơ bản (base R) dùng để trả về các phần tử đầu tiên (hàng) của một đối tượng.
Trong đó:
head(): Hàm cơ sở của R. Mục đích là để trích xuất và hiển thị các hàng đầu tiên của một đối tượng dữ liệu.
df: Tham số bắt buộc (dữ liệu). Chỉ định đối tượng data.frame cần được khám phá.
5: Tham số tùy chọn (số lượng hàng). Giới hạn số lượng hàng hiển thị chỉ còn 5 hàng, giúp bảng tóm tắt gọn gàng.
Kết quả kĩ thuật
Tính đồng nhất: Dữ liệu cho thấy tính đồng nhất cao, không có ô nào chứa ký tự NA hay giá trị lỗi. Các biến định lượng (như price, customer_salary) đều là số, và các biến định tính/danh nghĩa (như country, property_type) đều là chuỗi ký tự.
Xác nhận dữ liệu nhị phân: Biến mục tiêu decision chỉ chứa giá trị 0 và 1, xác nhận nó là một biến phân loại nhị phân, chuẩn bị cho mô hình Hồi quy Logistic.
Phạm vi giá trị: Giá trị của các biến lớn như price ($99.041 đến $1.110.959) và customer_salary ($10.745 đến $21.914) cho thấy các giá trị được đọc vào R là hợp lý về mặt số học.
Ý nghĩa thống kê
Chỉ trong 5 dòng đầu, dữ liệu đã thể hiện sự đa dạng lớn về địa lý (country: France, South Africa, Germany) và loại hình bất động sản (property_type: Farmhouse, Apartment, Townhouse). Điều này xác nhận rằng mô hình sẽ được xây dựng trên một cơ sở thị trường rộng, tăng khả năng khái quát hóa (generalizability) của các kết luận kinh tế.
Việc các biến như satisfaction_score (1 đến 9) và neighbourhood_rating (1 đến 8) xuất hiện cho thấy quyết định mua không chỉ phụ thuộc vào giá cả/thu nhập mà còn vào các yếu tố cảm tính và chất lượng, cần được định lượng trong mô hình hồi quy.
tail(df,5)
## property_id country city property_type furnishing_status
## 199996 199996 Germany Berlin Villa Fully-Furnished
## 199997 199997 China Shenzhen Townhouse Unfurnished
## 199998 199998 Japan Kyoto Villa Semi-Furnished
## 199999 199999 South Africa Johannesburg Apartment Unfurnished
## 200000 200000 Brazil Rio de Janeiro Apartment Semi-Furnished
## property_size_sqft price constructed_year previous_owners rooms
## 199996 685 203328 1968 1 3
## 199997 3818 1454627 1977 5 7
## 199998 3603 1619147 1990 2 4
## 199999 1706 306165 2010 0 4
## 200000 3652 732698 1986 0 1
## bathrooms garage garden crime_cases_reported legal_cases_on_property
## 199996 2 0 0 1 0
## 199997 5 1 1 1 0
## 199998 4 1 1 1 0
## 199999 1 1 0 0 1
## 200000 1 1 0 3 0
## customer_salary loan_amount loan_tenure_years monthly_expenses
## 199996 78330 104050 15 17670
## 199997 25400 1175297 20 2865
## 199998 28220 743049 30 5595
## 199999 12240 150774 15 16300
## 200000 22644 548714 30 5165
## down_payment emi_to_income_ratio satisfaction_score neighbourhood_rating
## 199996 99278 0.01 8 4
## 199997 279330 0.34 7 10
## 199998 876098 0.17 5 3
## 199999 155391 0.11 6 10
## 200000 183984 0.15 6 4
## connectivity_score decision
## 199996 5 1
## 199997 9 1
## 199998 9 0
## 199999 6 0
## 200000 9 0
Lệnh này có mục đích xem 5 hàng cuối cùng của dữ liệu (df). Đây là bước kiểm tra dữ liệu quan trọng trong giai đoạn khám phá, giúp người nghiên cứu phát hiện bất thường, lỗi dữ liệu có thể xuất hiện ở cuối tệp tin sau quá trình thu thập hoặc xuất file.
Trong đó:
tail(): Đây là hàm cơ sở của R. Mục đích là để trích xuất và hiển thị các hàng cuối cùng của một đối tượng dữ liệu.
df: Tham số bắt buộc (dữ liệu). Chỉ định đối tượng data.frame cần được kiểm tra.
5: Tham số tùy chọn (số lượng hàng). Giới hạn số lượng hàng hiển thị chỉ còn 5 hàng cuối cùng.
Kết quả kĩ thuật
Tính toàn vẹn dữ liệu: Không có bất kỳ ô nào trong 5 hàng cuối cùng chứa giá trị NA hoặc ký tự lỗi. Điều này xác nhận rằng dữ liệu đã được đọc vào R sạch sẽ và không bị lỗi đuôi file.
Chỉ số Giao dịch: Quan sát cột property_id từ \(199996\) đến \(200000\) xác nhận rằng đây là những giao dịch cuối cùng trong tổng số \(200000\) quan sát.
Định dạng Biến: Các giá trị trong 5 dòng cuối tiếp tục tuân theo định dạng đã được xác nhận (biến số là số, biến danh nghĩa là chuỗi ký tự).
Ý nghĩa thống kê
Dù ở cuối tập mẫu, dữ liệu vẫn thể hiện sự phân hóa rõ rệt về mặt kinh tế, chứng tỏ mô hình sẽ học hỏi được nhiều kịch bản:
Kịch bản rủi ro cao: Dòng \(199997\) (China) có customer_salary thấp (\(25400\)) nhưng loan_amount rất cao (\(1175297\)) so với thu nhập, dẫn đến emi_to_income_ratio cao (\(0.34\)) và cuối cùng là quyết định Mua (decision=1). Điều này cho thấy sự chấp nhận rủi ro tín dụng cao của khách hàng hoặc quy định cho vay lỏng lẻo ở thị trường này.
Kịch bản thanh khoản cao: Dòng \(199996\) (Germany) có customer_salary cao (\(78330\)) nhưng loan_amount thấp (\(104050\)), dẫn đến emi_to_income_ratio rất thấp (\(0.01\)) và quyết định Mua (decision=1). Điều này thể hiện khách hàng có khả năng thanh khoản rất tốt.
Tầm quan trọng của yếu tố vị trí: Sự xuất hiện của các thành phố như Berlin (Germany), Shenzhen (China), Kyoto (Japan), Johannesburg (South Africa) và Rio de Janeiro (Brazil) ở cuối file củng cố tính đại diện toàn cầu của mẫu. Mô hình hồi quy sẽ có cơ sở để định lượng ảnh hưởng đặc thù của từng thị trường quốc gia/thành phố lên quyết định mua nhà.
names(df)
## [1] "property_id" "country"
## [3] "city" "property_type"
## [5] "furnishing_status" "property_size_sqft"
## [7] "price" "constructed_year"
## [9] "previous_owners" "rooms"
## [11] "bathrooms" "garage"
## [13] "garden" "crime_cases_reported"
## [15] "legal_cases_on_property" "customer_salary"
## [17] "loan_amount" "loan_tenure_years"
## [19] "monthly_expenses" "down_payment"
## [21] "emi_to_income_ratio" "satisfaction_score"
## [23] "neighbourhood_rating" "connectivity_score"
## [25] "decision"
Lệnh names(df) là một bước khám phá dữ liệu cơ bản. Mục đích là liệt kê tất cả 25 tên biến (tên cột) có trong đối tượng data.frame (df).
Việc này giúp người nghiên cứu xác nhận các tên cột đã được đọc vào chính xác từ tệp CSV và chuẩn bị cho việc lựa chọn, đổi tên, hoặc truy cập các biến trong các lệnh phân tích tiếp theo.
Trong đó:
names(): Đây là hàm cơ sở của R. Mục đích là để truy xuất vector chứa các tên cột của đối tượng dữ liệu.
df: Tham số bắt buộc. Chỉ định đối tượng data.frame mà chúng ta muốn lấy danh sách tên cột.
Kết quả kĩ thuật
Trả về một vector chuỗi ký tự chứa 25 tên cột. Tất cả các tên biến đều được đọc vào R một cách hợp lệ, không chứa các ký tự lỗi. Ví dụ: country, city, price, emi_to_income_ratio, decision,…
Ý nghĩa thống kê
Việc liệt kê tên biến giúp người nghiên cứu phân loại ngay lập tức các nhóm yếu tố kinh tế để phục vụ cho mô hình hóa:
Yếu tố Tài chính và Rủi ro: Bao gồm customer_salary, loan_amount, monthly_expenses, emi_to_income_ratio, down_payment. Các biến này sẽ được ưu tiên trong mô hình Hồi quy Logistic để định lượng khả năng chi trả và nguy cơ vỡ nợ của khách hàng.
Yếu tố Đặc tính Bất động sản: Bao gồm price, property_size_sqft, property_type, constructed_year. Các biến này sẽ định lượng giá trị cốt lõi và hàng hóa mà khách hàng đang xem xét.
(sapply(df, class))
## property_id country city
## "integer" "character" "character"
## property_type furnishing_status property_size_sqft
## "character" "character" "integer"
## price constructed_year previous_owners
## "integer" "integer" "integer"
## rooms bathrooms garage
## "integer" "integer" "integer"
## garden crime_cases_reported legal_cases_on_property
## "integer" "integer" "integer"
## customer_salary loan_amount loan_tenure_years
## "integer" "integer" "integer"
## monthly_expenses down_payment emi_to_income_ratio
## "integer" "integer" "numeric"
## satisfaction_score neighbourhood_rating connectivity_score
## "integer" "integer" "integer"
## decision
## "integer"
Lệnh này có mục đích kiểm tra và liệt kê kiểu dữ liệu (data type hay class) của từng cột trong data.frame (df).
Đây là bước tiền xử lý bắt buộc để xác định cột nào cần chuyển đổi sang kiểu factor (biến định tính) và cột nào cần giữ nguyên kiểu số (numeric/integer) trước khi tiến hành các phân tích thống kê.
Trong đó:
sapply(): Đây là hàm cơ sở của R. Mục đích là để áp dụng một hàm (ở đây là class) cho từng cột của đối tượng dữ liệu và trả về kết quả dưới dạng vector hoặc mảng đơn giản.
df: Tham số bắt buộc (dữ liệu). Chỉ định đối tượng data.frame mà chúng ta muốn áp dụng hàm.
class: Tham số hàm được truyền vào sapply(). Mục đích là trả về kiểu dữ liệu (ví dụ: integer, numeric, character) của mỗi cột.
Kết quả kĩ thuật
Các biến định lượng liên tục (như price, customer_salary,…) được nhận diện chính xác là integer. Biến emi_to_income_ratio (tỷ lệ) là numeric (số thực).
4 biến danh nghĩa (country, city, property_type, furnishing_status) đang ở kiểu character. Quan trọng hơn, biến mục tiêu decision (0/1) đang là integer.
Ý nghĩa thống kê
Các biến tài chính cốt lõi (price, customer_salary, loan_amount) được nhận diện là kiểu số. Điều này đảm bảo rằng chúng ta có thể thực hiện ngay lập tức các phép toán thống kê mô tả (mean(), sd(), summary()) và các mô hình hồi quy.
4 biến kiểu character (country, property_type,…) bắt buộc phải được chuyển sang kiểu factor. Nếu không, chúng không thể được sử dụng đúng cách làm biến phân loại trong ggplot2 hay trong mô hình hồi quy.
Biến mục tiêu decision: Biến này đang là integer (0/1). Để chạy Hồi quy Logistic (glm(…, family = binomial)), việc chuyển decision thành kiểu factor là rất nên làm. Điều này giúp R hiểu rằng đây là một biến phân loại nhị phân, đảm bảo mô hình ước lượng xác suất mua một cách chính xác
unique(df$country)
## [1] "France" "South Africa" "Germany" "Canada" "Brazil"
## [6] "UAE" "Australia" "UK" "USA" "China"
## [11] "Singapore" "India" "Japan"
Lệnh này có mục đích liệt kê tất cả các giá trị độc nhất có trong cột country (Quốc gia) của dữ liệu (df). Đây là một bước khám phá dữ liệu bắt buộc để: Xác định số lượng và tên gọi chính xác của các thị trường được bao gồm trong nghiên cứu.
Trong đó:
unique(): Đây là hàm cơ sở của R. Mục đích là để lọc và trả về một tập hợp các giá trị không lặp lại từ một vector dữ liệu.
df$country: Tham số bắt buộc. Đây là cú pháp để truy cập trực tiếp vào cột (country) bên trong đối tượng data.frame (df).
Kết quả kĩ thuật
Trả về một vector chứa 13 chuỗi ký tự độc nhất.
Phân loại biến: Xác nhận rằng country là một biến định tính danh nghĩa với 13 cấp độ (levels) hoặc danh mục.
Tính toàn vẹn: Không có lỗi chính tả, không có giá trị trống hay giá trị NA nào được trả về (nếu có, NA cũng sẽ được liệt kê là một giá trị độc nhất). Điều này đảm bảo rằng việc chuyển đổi sang kiểu factor sẽ diễn ra suôn sẻ.
Ý nghĩa thống kê
Phạm vi thị trường toàn cầu: Kết quả xác nhận nghiên cứu bao trùm một phạm vi địa lý rất rộng, bao gồm các thị trường bất động sản đa dạng về kinh tế và quy định:
Các nền kinh tế phát triển: USA, UK, Germany, France, Japan, Canada, Australia.
Các nền kinh tế mới nổi/châu Á: China, India, Singapore, Brazil.
Các thị trường đặc thù: UAE, South Africa.
table(df$country)
##
## Australia Brazil Canada China France Germany
## 15442 15397 15401 15536 15628 15408
## India Japan Singapore South Africa UAE UK
## 15357 15317 15278 15401 15141 15413
## USA
## 15281
Lệnh này có mục đích tạo ra một bảng tần số (Frequency Table) cho biến country. Mục đích là thống kê số lượng quan sát (số đếm tuyệt đối) ứng với mỗi quốc gia có trong bộ dữ liệu, giúp khám phá sự phân bổ và tính cân bằng của mẫu nghiên cứu theo thị trường.
Trong đó:
table(): Đây là hàm cơ sở của R. Mục đích là để đếm số lần xuất hiện của các giá trị độc nhất trong một vector, tạo ra bảng tần số.
df$country: Tham số bắt buộc (dữ liệu). Cú pháp để truy cập trực tiếp cột country bên trong đối tượng data.frame (df), chỉ định rằng hàm table() sẽ hoạt động trên cột này.
Kết quả kĩ thuật
Bảng là một vector có tên trong R, trong đó tên là tên quốc gia và giá trị là số lượng quan sát.
Tổng quan: Tổng số tần số là \(200.000\), khớp với kích thước tổng thể của dữ liệu (dim(df)).
Tính Cân bằng: Số lượng quan sát dao động trong một phạm vi rất hẹp, từ \(15.141\) (UAE) đến \(15.628\) (France).
Ý nghĩa thống kê
Tính cân bằng Mẫu: Dữ liệu cho thấy tính cân bằng mẫu gần như tuyệt đối giữa 13 thị trường quốc gia. Số lượng quan sát của mỗi quốc gia đều xấp xỉ \(15.400\). Khoảng chênh lệch giữa thị trường lớn nhất (France, \(15.628\)) và thị trường nhỏ nhất (UAE, \(15.141\)) là rất nhỏ (chỉ \(487\) quan sát).
Độ phủ thị trường: Mỗi thị trường (từ các nền kinh tế phát triển như Mỹ, Đức đến các thị trường mới nổi như Trung Quốc, Ấn Độ) đều được đại diện bằng một số lượng hồ sơ giao dịch đủ lớn (trên \(15.100\)), cho thấy sự phân bổ đều đặn và có chủ đích của quá trình thu thập dữ liệu.
summary(df$price)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 56288 565990 1023429 1215365 1725557 4202732
Lệnh này có mục đích thực hiện thống kê mô tả cho biến price (Giá). Hàm summary() trả về tóm tắt sáu chỉ số chính (minimum, Q1, median, mean, Q3, maximum), giúp người nghiên cứu khám phá phân phối, xu hướng trung tâm và mức độ phân tán của giá bất động sản trong bộ dữ liệu.
Trong đó:
summary(): Đây là hàm cơ sở của R. Mục đích là để tính toán và hiển thị tóm tắt sáu chỉ số thống kê chính của một biến định lượng.
df$price: Tham số bắt buộc (dữ liệu). Cú pháp truy cập trực tiếp vào cột price trong đối tượng data.frame (df), chỉ định biến cần được thống kê mô tả.
Kết quả kĩ thuật
Độ lệch (Skewness): Giá trị Trung bình (\(1.215.365\)) lớn hơn Trung vị (\(1.023.429\)). Điều này xác nhận phân phối giá bị lệch phải.
Khoảng biến thiên: Giá trị trải rộng từ \(56.288\) đến \(4.202.732\), thể hiện một khoảng cách lớn giữa giá trị thấp nhất và cao nhất.
Khoảng Tứ phân vị (IQR): \(Q3 - Q1 = 1.725.557 - 565.990 = 1.159.567\). Đây là sự phân tán của 50% dữ liệu trung tâm.
Ý nghĩa thống kê
Việc trung bình lớn hơn trung vị là bằng chứng thống kê cho thấy thị trường có sự phân hóa mạnh mẽ. Giá trị trung vị (\(1.023.429\)) đại diện cho mức giá điển hình hoặc phổ biến nhất.Giá trị trung bình cao hơn cho thấy sự tồn tại của các giao dịch siêu đắt (phân khúc cao cấp/Luxury market) đang kéo giá trị trung bình chung lên.
Phân khúc tầm trung: 50% các bất động sản tập trung trong khoảng giá từ \(565.990\) USD (Q1) đến \(1.725.557\) USD (Q3)
ten_bien_goc <- c(
"property_id", "country", "city", "property_type", "furnishing_status",
"property_size_sqft", "price", "constructed_year", "previous_owners",
"rooms", "bathrooms", "garage", "garden", "crime_cases_reported",
"legal_cases_on_property", "customer_salary", "loan_amount",
"loan_tenure_years", "monthly_expenses", "down_payment",
"emi_to_income_ratio", "satisfaction_score", "neighbourhood_rating",
"connectivity_score", "decision"
)
y_nghia_giai_thich <- c(
"ID Bất động sản", "Quốc gia", "Thành phố", "Loại hình Bất động sản", "Tình trạng Nội thất",
"Diện tích Bất động sản( ft²)", "Giá bán Bất động sản", "Năm Xây dựng", "Số Chủ sở hữu Trước",
"Số phòng", "Số phòng tắm", "Có Garage (1=Có, 0=Không)", "Có Vườn (1=Có, 0=Không)",
"Số Vụ án Báo cáo (Khu vực)", "Có Vụ kiện Pháp lý (1=Có, 0=Không)", "Thu nhập Hàng tháng Khách hàng",
"Số tiền Vay", "Thời hạn Vay (năm)", "Chi phí Hàng tháng", "Khoản Đặt cọc (Down Payment)",
"Tỷ lệ EMI/Thu nhập (Rủi ro Tín dụng)", "Điểm Hài lòng (1-10)", "Điểm Khu vực Lân cận (1-10)",
"Điểm Kết nối Giao thông (1-10)", "Quyết định Mua (1=Có, 0=Không)")
bang_chu_giai <- data.frame(
Ten_Bien_Goc = ten_bien_goc,
Y_Nghia_Giai_Thich = y_nghia_giai_thich
)
kable(bang_chu_giai)
| Ten_Bien_Goc | Y_Nghia_Giai_Thich |
|---|---|
| property_id | ID Bất động sản |
| country | Quốc gia |
| city | Thành phố |
| property_type | Loại hình Bất động sản |
| furnishing_status | Tình trạng Nội thất |
| property_size_sqft | Diện tích Bất động sản( ft²) |
| price | Giá bán Bất động sản |
| constructed_year | Năm Xây dựng |
| previous_owners | Số Chủ sở hữu Trước |
| rooms | Số phòng |
| bathrooms | Số phòng tắm |
| garage | Có Garage (1=Có, 0=Không) |
| garden | Có Vườn (1=Có, 0=Không) |
| crime_cases_reported | Số Vụ án Báo cáo (Khu vực) |
| legal_cases_on_property | Có Vụ kiện Pháp lý (1=Có, 0=Không) |
| customer_salary | Thu nhập Hàng tháng Khách hàng |
| loan_amount | Số tiền Vay |
| loan_tenure_years | Thời hạn Vay (năm) |
| monthly_expenses | Chi phí Hàng tháng |
| down_payment | Khoản Đặt cọc (Down Payment) |
| emi_to_income_ratio | Tỷ lệ EMI/Thu nhập (Rủi ro Tín dụng) |
| satisfaction_score | Điểm Hài lòng (1-10) |
| neighbourhood_rating | Điểm Khu vực Lân cận (1-10) |
| connectivity_score | Điểm Kết nối Giao thông (1-10) |
| decision | Quyết định Mua (1=Có, 0=Không) |
Các dòng code này có mục đích tạo ra một Bảng Chú giải cho bộ dữ liệu. Bảng này ánh xạ tên biến gốc trong dữ liệu (ten_bien_goc) với ý nghĩa giải thích cụ thể của chúng bằng tiếng Việt (y_nghia_giai_thich). Đây là một bước tổ chức dữ liệu quan trọng nhằm đảm bảo tính minh bạch và dễ đọc cho toàn bộ bài tiểu luận.
Trong đó:
ten_bien_goc <- c(…): Đây là thao tác gán vector. Mục đích là tạo một vector chuỗi ký tự chứa 25 tên biến nguyên bản của dữ liệu (price, customer_salary, decision, v.v.).
y_nghia_giai_thich <- c(…): Đây là thao tác gán vector. Mục đích là tạo một vector chuỗi ký tự khác chứa 25 ý nghĩa giải thích tiếng Việt tương ứng theo thứ tự, ví dụ: “Giá bán Bất động sản”, “Thu nhập Hàng tháng Khách hàng”, “Quyết định Mua”, v.v.
bang_chu_giai <- data.frame(…): Đây là thao tác tạo đối tượng dữ liệu. Hàm data.frame() được sử dụng để kết hợp hai vector vừa tạo thành một đối tượng data.frame mới, có tên là bang_chu_giai, với hai cột là ten_bien_goc và y_nghia_giai_thich.
Kết quả kĩ thuật
Một đối tượng data.frame mới (bang_chu_giai) được tạo ra với kích thước \(25\) hàng \(\times\) \(2\) cột.
Cấu trúc: Cột thứ nhất (ten_bien_goc) chứa các chuỗi ký tự là tên biến gốc, cột thứ hai (y_nghia_giai_thich) chứa các chuỗi ký tự là ý nghĩa giải thích.
Tính đồng nhất: Độ dài của hai vector (25 phần tử) là bằng nhau, đảm bảo sự ánh xạ \(1:1\) giữa tên biến và ý nghĩa của nó.
Ý nghĩa thống kê
Việc tạo ra bảng chú giải là một bước cần thiết trong phương pháp luận nghiên cứu. Nó giải quyết vấn đề tên biến gốc dài và đôi khi khó hiểu (emi_to_income_ratio, legal_cases_on_property) bằng cách liên kết chúng trực tiếp với các khái niệm kinh tế cốt lõi (“Rủi ro Tín dụng”, “Vụ kiện Pháp lý”).
Bảng chú giải đóng vai trò là “chìa khóa” khi đọc các bảng kết quả thống kê và mô hình hồi quy. Khi trình bày kết quả Hồi quy Logistic, hệ số ước lượng sẽ được gán cho tên biến. Bảng chú giải cho phép người đọc nhanh chóng chuyển đổi kết quả thống kê thành ý nghĩa kinh tế.
colSums(is.na(df))
## property_id country city
## 0 0 0
## property_type furnishing_status property_size_sqft
## 0 0 0
## price constructed_year previous_owners
## 0 0 0
## rooms bathrooms garage
## 0 0 0
## garden crime_cases_reported legal_cases_on_property
## 0 0 0
## customer_salary loan_amount loan_tenure_years
## 0 0 0
## monthly_expenses down_payment emi_to_income_ratio
## 0 0 0
## satisfaction_score neighbourhood_rating connectivity_score
## 0 0 0
## decision
## 0
Lệnh này có mục đích kiểm tra số lượng giá trị khuyết thiếu (Missing Values hay NA) trong từng cột của dữ liệu (df). Đây là bước tiền xử lý bắt buộc trong mọi nghiên cứu thống kê để xác định mức độ đầy đủ của dữ liệu và quyết định phương pháp xử lý NA (ví dụ: loại bỏ hoặc điền khuyết) trước khi tiến hành mô hình hóa.
Trong đó:
is.na(df): Đây là hàm cơ sở của R. Mục đích là để kiểm tra từng ô trong data.frame (df). Nếu ô đó chứa giá trị khuyết thiếu (NA), hàm sẽ trả về TRUE, nếu không sẽ trả về FALSE. Kết quả là một ma trận logic. colSums(…): Đây là hàm cơ sở của R. Mục đích là để tính tổng (sum) các giá trị trong mỗi cột của ma trận logic được tạo ra bởi is.na(df). Vì TRUE được coi là \(1\) và FALSE là \(0\) trong các phép toán số học, colSums() sẽ trả về tổng số lượng TRUE (tức là tổng số lượng NA) cho mỗi cột.
Kết quả kĩ thuật
Kết quả trên xác nhận rằng tất cả 25 cột trong bộ dữ liệu df đều có 0 giá trị khuyết thiếu.
Tính toàn vẹn dữ liệu: Bộ dữ liệu có tính toàn vẹn rất cao về mặt giá trị.
Ý nghĩa thống kê
Phát hiện 0 giá trị khuyết thiếu là một lợi thế kinh tế và kỹ thuật lớn. Nó loại bỏ hoàn toàn nhu cầu thực hiện các bước phức tạp và tốn kém về thời gian như xử lý dữ liệu khuyết thiếu (Imputation) (ví dụ: dùng giá trị trung bình, trung vị, hoặc mô hình hồi quy để điền khuyết).
Việc không có NA đảm bảo rằng tất cả \(200.000\) quan sát sẽ được sử dụng trong mọi phân tích và mô hình hồi quy. Điều này củng cố độ vững chắc (Robustness) của các ước lượng thống kê.
Các biến tài chính quan trọng như customer_salary, loan_amount, và emi_to_income_ratio đều có đủ \(200.000\) giá trị. Điều này có nghĩa là mọi ước tính về rủi ro tín dụng và khả năng chi trả đều dựa trên bộ dữ liệu hoàn chỉnh, giúp các kết luận kinh tế về hành vi mua nhà là toàn diện và đáng tin cậy.
Lệnh sum(duplicated(data)) là sự kết hợp của hai hàm cơ bản (Base R) để kiểm tra tính độc lập của các quan sát.
sum(duplicated(df))
## [1] 0
Lệnh này có mục đích kiểm tra và đếm số lượng hàng (quan sát) bị trùng lặp hoàn toàn trong toàn bộ dữ liệu (df). Đây là bước tiền xử lý bắt buộc để đảm bảo tính độc lập và chính xác của các quan sát. Dữ liệu trùng lặp có thể làm sai lệch các ước lượng thống kê (ví dụ: làm sai lệch độ chính xác của mô hình).
Trong đó:
duplicated(df): Đây là hàm cơ sở của R. Mục đích là để kiểm tra từng hàng của data.frame (df). Nếu một hàng là bản sao y hệt của một hàng đã xuất hiện trước đó, hàm sẽ trả về TRUE, nếu không sẽ trả về FALSE. Kết quả là một vector logic.
sum(…): Đây là hàm cơ sở của R. Mục đích là để tính tổng các giá trị trong vector logic được tạo ra bởi duplicated(df). Vì TRUE được coi là \(1\) và FALSE là \(0\), sum() sẽ trả về tổng số hàng bị trùng lặp.
Kết quả kĩ thuật
Với kết quả = 0, xác nhận rằng trong tổng số \(200.000\) hàng của dữ liệu, không có hàng nào bị trùng lặp hoàn toàn (tức là không có hai hồ sơ bất động sản nào có 25 giá trị biến y hệt nhau).
Tính độc lập: Các quan sát đều là độc lập và riêng biệt về mặt dữ liệu.
Ý nghĩa thống kê
Phát hiện 0 hàng trùng lặp là một lợi thế kinh tế rất lớn. Nó đảm bảo rằng toàn bộ \(200.000\) quan sát là có giá trị và độc lập thống kê. Bài tiểu luận sẽ không cần phải thực hiện bất kỳ bước nào để loại bỏ dữ liệu trùng lặp.
Việc không có trùng lặp xác nhận rằng mẫu nghiên cứu có tính nghiêm ngặt và tính độc lập của các quan sát được đảm bảo. Đây là một giả định quan trọng trong hầu hết các mô hình kinh tế lượng và thống kê suy luận (như Hồi quy Logistic).
Các ước lượng thống kê, đặc biệt là các hệ số hồi quy, sẽ không bị sai lệch do sự “phình to giả tạo” của cỡ mẫu mà dữ liệu trùng lặp có thể gây ra.
df <- df %>%
rename(
salary = customer_salary,
loan = loan_amount,
emi_ratio = emi_to_income_ratio
)
head(df)
## property_id country city property_type furnishing_status
## 1 1 France Marseille Farmhouse Semi-Furnished
## 2 2 South Africa Cape Town Apartment Semi-Furnished
## 3 3 South Africa Johannesburg Farmhouse Semi-Furnished
## 4 4 Germany Frankfurt Farmhouse Semi-Furnished
## 5 5 South Africa Johannesburg Townhouse Fully-Furnished
## 6 6 Canada Montreal Villa Semi-Furnished
## property_size_sqft price constructed_year previous_owners rooms bathrooms
## 1 991 412935 1989 6 6 2
## 2 1244 224538 1990 4 8 8
## 3 4152 745104 2019 5 2 1
## 4 3714 1110959 2008 1 3 3
## 5 531 99041 2007 6 3 3
## 6 3169 1107368 1985 0 5 2
## garage garden crime_cases_reported legal_cases_on_property salary loan
## 1 1 1 1 0 10745 193949
## 2 1 1 1 1 16970 181465
## 3 1 1 0 0 21914 307953
## 4 0 1 0 0 17980 674720
## 5 1 1 3 1 17676 65833
## 6 1 0 0 0 95520 793316
## loan_tenure_years monthly_expenses down_payment emi_ratio satisfaction_score
## 1 15 6545 218986 0.16 1
## 2 20 8605 43073 0.08 9
## 3 30 2510 437151 0.09 6
## 4 15 8805 436239 0.33 2
## 5 25 8965 33208 0.03 3
## 6 30 10615 314052 0.05 10
## neighbourhood_rating connectivity_score decision
## 1 5 6 0
## 2 1 2 0
## 3 8 1 0
## 4 6 6 0
## 5 3 4 0
## 6 8 2 1
Dòng code này thực hiện thao tác đổi tên (rename) ba biến số định lượng trong dữ liệu. Mục đích là để rút gọn tên biến từ dạng dài, gốc sang dạng ngắn, dễ nhớ hơn (customer_salary thành salary), giúp tăng tốc độ gõ code, giảm thiểu lỗi chính tả trong quá trình phân tích và cải thiện khả năng đọc của các bảng kết quả thống kê (đặc biệt là các bảng hệ số hồi quy).
Trong đó:
df <- df %>%: Đây là cú pháp Tidyverse (sử dụng gói dplyr). Mục đích là lấy đối tượng df làm đầu vào, thực hiện các thao tác biến đổi (phía sau %>%), và sau đó ghi đè kết quả trở lại vào chính đối tượng df gốc.
rename(…): Đây là hàm của gói dplyr. Mục đích là thay đổi tên cột.
salary = customer_salary: Đây là quy tắc đổi tên. Mục đích là gán tên mới (salary, viết tắt) cho cột cũ (customer_salary, tên gốc).
loan = loan_amount: Gán tên mới (loan) cho cột gốc (loan_amount).
emi_ratio = emi_to_income_ratio: Gán tên mới (emi_ratio) cho cột gốc (emi_to_income_ratio).
Kết quả kĩ thuật
Ba tên cột đã được cập nhật vĩnh viễn trong data.frame (df). Các cột này vẫn giữ nguyên kiểu dữ liệu (integer và numeric) và giá trị của chúng, chỉ tên gọi thay đổi.
Ý nghĩa thống kê
Ba biến được đổi tên (salary, loan, emi_ratio) là những chỉ số kinh tế cốt lõi trong việc đánh giá khả năng chi trả và rủi ro tín dụng.
Trong các bảng kết quả hồi quy, việc hiển thị các hệ số cho emi_ratio thay vì emi_to_income_ratio làm cho bảng trở nên ngắn gọn và chuyên nghiệp hơn. Điều này trực tiếp cải thiện khả năng diễn giải kinh tế: người đọc dễ dàng liên kết hệ số hồi quy với khái niệm “tỷ lệ trả góp” hơn là với tên gọi quá dài và phức tạp.
Thao tác này là một ví dụ về tối ưu hóa quy trình làm việc. Khi xây dựng các mô hình phức tạp (như hồi quy đa biến), việc rút gọn tên biến giúp người nghiên cứu giảm thiểu thời gian gõ và sửa lỗi, cho phép tập trung nhiều hơn vào các vấn đề phân tích kinh tế và chất lượng mô hình.
df <- df %>%
mutate(
country = as.factor(country),
city = as.factor(city),
property_type = as.factor(property_type),
furnishing_status = as.factor(furnishing_status),
decision = as.factor(decision)
)
sapply(df[, c("country", "city","property_type", "furnishing_status", "decision")], class)
## country city property_type furnishing_status
## "factor" "factor" "factor" "factor"
## decision
## "factor"
Dòng code này thực hiện thao tác chuyển đổi kiểu dữ liệu cho 5 biến (gồm cả biến mục tiêu và các biến định tính quan trọng) từ kiểu ban đầu (character/integer) sang kiểu factor (biến định tính/phân loại). Mục đích là để R và các mô hình thống kê nhận diện chính xác các biến này là các danh mục hoặc nhóm phân loại, thay vì các giá trị số học hay chuỗi ký tự đơn thuần.
Trong đó:
df <- df %>% mutate(…): Đây là cú pháp Tidyverse (gói dplyr). Mục đích là lấy đối tượng df làm đầu vào, thực hiện các thao tác biến đổi (mutate), và ghi đè kết quả trở lại vào chính đối tượng df.
mutate(…): Hàm của dplyr. Mục đích là để tạo mới hoặc chỉnh sửa/ghi đè các cột hiện có.
biến = as.factor(biến): Lệnh chuyển đổi.
as.factor(): Hàm cơ sở của R. Mục đích là chuyển đổi vector đầu vào thành kiểu factor.
country, city, property_type, furnishing_status: Bốn biến định tính danh nghĩa.
decision: Biến mục tiêu nhị phân (0/1).
Kết quả thống kê
Kiểu dữ liệu của 5 cột đã được chuyển thành factor (tương đương category).
R hiện đã xác định các cấp độ (levels) rõ ràng cho từng biến.
Cột decision (trước đây là integer 0/1) giờ đây là một biến phân loại nhị phân với hai cấp độ: “0” và “1”.
Các biến định tính khác (như country và city) cũng đã được R mã hóa nội bộ dưới dạng số nguyên, giúp tiết kiệm bộ nhớ so với việc lưu trữ dưới dạng chuỗi ký tự.
Ý nghĩa thống kê
Việc chuyển đổi decision thành factor là bước chuẩn bị cơ bản và bắt buộc để R nhận diện chính xác đây là một Mô hình phân loại Nhị phân.
Khi các biến định tính (country, city, property_type, furnishing_status) được đưa vào mô hình hồi quy, R sẽ tự động chuyển chúng thành biến giả.
Bằng cách khai báo rõ ràng, từ đó ngăn chặn R thực hiện các phép toán số học vô nghĩa (ví dụ: tính trung bình của city), đảm bảo rằng các phân tích so sánh nhóm (t.test, chisq.test) sẽ được thực hiện với các giả định thống kê hợp lệ.
df <- df %>%
mutate(price_group = case_when(
price < 200000 ~ "Thấp",
price >= 200000 & price < 500000 ~ "Trung bình",
price >= 500000 & price < 1000000 ~ "Cao",
price >= 1000000 ~ "Rất cao"
))
price_summary <- df %>%
group_by(price_group) %>%
summarise(
Count = n()
) %>%
mutate(Percent = round(Count / sum(Count) * 100, 2)) %>%
arrange(desc(Count))
kable(price_summary)
| price_group | Count | Percent |
|---|---|---|
| Rất cao | 102236 | 51.12 |
| Cao | 55274 | 27.64 |
| Trung bình | 33615 | 16.81 |
| Thấp | 8875 | 4.44 |
Khối code này có hai mục đích chính:
1. Tạo ra một biến phân loại mới, price_group (Nhóm Giá), bằng cách chia biến định lượng price thành bốn nhóm theo các ngưỡng giá định trước.
2. Tính toán tần số (Count) và tỷ lệ (Percent) của các quan sát rơi vào mỗi phân khúc giá này, sắp xếp kết quả theo tần số giảm dần.
Trong đó:
1: Tạo biến nhóm Giá
df %>% mutate(…): Sử dụng hàm mutate của dplyr để thêm/ghi đè cột price_group vào df.
price_group = case_when(…): Hàm case_when của dplyr. Đây là một lệnh logic có điều kiện, mục đích là gán nhãn chuỗi ký tự (“Thấp”, “Trung bình”, “Cao”, “Rất cao”) cho từng hàng dựa trên giá trị price của hàng đó.
Ngưỡng giá:
< 200000: “Thấp”
[200000, 500000): “Trung bình”
[500000, 1000000): “Cao”
[1000000, \(\infty\)): “Rất cao”
2: Thống kê tần số và tỉ lệ
group_by(price_group): Hàm của dplyr. Mục đích là nhóm các hàng lại với nhau dựa trên giá trị của cột price_group vừa tạo.
summarise(Count = n()): Hàm của dplyr. Mục đích là đếm số lượng hàng (n()) trong mỗi nhóm và gán kết quả đó vào cột Count (Tần số).
mutate(Percent = round(Count / sum(Count) * 100, 2)): Mục đích là tính tỷ lệ phần trăm (Tần suất) của mỗi phân khúc so với tổng số quan sát, làm tròn đến 2 chữ số thập phân.
arrange(desc(Count)): Mục đích là sắp xếp bảng kết quả theo cột Count (Tần số) theo thứ tự giảm dần.
Kết quả kĩ thuật
Tổng số quan sát là \(200.000\), tổng tỷ lệ phần trăm là \(100.01\%\) (chênh lệch \(0.01\%\) do làm tròn số), xác nhận việc phân nhóm là hoàn chỉnh và chính xác.
Phân phối: Phân khúc Rất cao có Tần số lớn nhất (\(102.236\) quan sát), chiếm hơn một nửa tổng dữ liệu với\(51.12\%\).
Tính thứ bậc: Biến price_group đã được tạo thành công dưới dạng một biến định tính thứ bậc (Ordinal).
Ý nghĩa thống kê
Phát hiện quan trọng là thị trường trong bộ dữ liệu nghiêng mạnh về phân khúc giá trị cao. Cụ thể, các bất động sản thuộc nhóm Rất cao (\(\ge 1.000.000\)) chiếm \(51.12\%\) tổng số mẫu. Thêm vào đó, nhóm Cao (\(500.000 \le \text{price} < 1.000.000\)) chiếm \(27.64\%\).
Hơn \(78.76\%\) tổng số hồ sơ (\(157.510\) quan sát) tập trung ở các phân khúc giá từ Cao trở lên. Điều này củng cố các nhận định trước đây về phân phối lệch phải của biến price.
Phân khúc Thấp (\(< 200.000\)) có Tần số rất nhỏ (\(8.875\) quan sát), chỉ chiếm \(4.44\%\) tổng mẫu. Ý nghĩa: Số lượng mẫu thấp ở phân khúc này ngụ ý rằng các kết luận thống kê về khả năng chi trả và hành vi mua nhà ở thị trường bình dân (ví dụ: tác động của thu nhập rất thấp) sẽ có độ tin cậy và khả năng khái quát hóa thấp hơn so với phân khúc cao cấp.
df <- df %>%
mutate(
garage_label = ifelse(garage == 1, "Có", "Không"),
garden_label = ifelse(garden == 1, "Có", "Không")
)
df %>%
select(garage_label, garden_label) %>%
head()
## garage_label garden_label
## 1 Có Có
## 2 Có Có
## 3 Có Có
## 4 Không Có
## 5 Có Có
## 6 Có Không
Dòng code này thực hiện thao tác mã hóa lại hai biến nhị phân gốc (garage, garden) sang dạng nhãn Tiếng Việt dễ đọc và dễ trình bày.
Mục đích là để tạo ra hai biến định tính mới là garage_label và garden_label với các giá trị rõ ràng (“Có”, “Không”), giúp các biểu đồ và bảng thống kê trở nên trực quan hơn cho bài tiểu luận.
Trong đó:
df <- df %>% mutate(…): Sử dụng cú pháp Tidyverse để thêm/ghi đè cột mới vào dữ liệu df.
mutate(…): Hàm của dplyr dùng để tạo/chỉnh sửa cột.
garage_label = ifelse(…): Lệnh tạo cột mới.
ifelse(): Hàm cơ sở của R. Đây là hàm logic có điều kiện.
garage == 1: Điều kiện kiểm tra.
“Có”: Giá trị trả về nếu điều kiện là TRUE (tức là có garage).
“Không”: Giá trị trả về nếu điều kiện là FALSE (tức là không có garage).
garden_label = ifelse(…): Tương tự như trên, áp dụng logic mã hóa cho biến garden.
Kết quả kĩ thuật
Hai cột mới garage_label và garden_label được thêm vào data.frame (df).
Kiểu dữ liệu :Các cột mới này là kiểu chuỗi ký tự (character).
Ý nghĩa thống kê
Việc mã hóa thành “Có/Không” là cần thiết để tăng tính trực quan cho các biểu đồ. Ví dụ, trong Biểu đồ cột nhóm, nhãn “Có Garage” và “Không Garage” dễ hiểu hơn nhiều so với “1” và “0”. Điều này giúp người đọc nhanh chóng nắm bắt được sự khác biệt trong tỷ lệ mua nhà giữa hai nhóm.
df <- df %>%
mutate(
price_scaled = scale(price),
salary_scaled = scale(salary),
loan_scaled = scale(loan),
expenses_scaled = scale(monthly_expenses)
)
df %>%
select(price_scaled, salary_scaled,loan_scaled,expenses_scaled) %>%
head()
## price_scaled salary_scaled loan_scaled expenses_scaled
## 1 -0.9742211 -1.2781074 -1.03073036 -0.73972990
## 2 -1.2029517 -1.0557650 -1.05347237 -0.36016326
## 3 -0.5709386 -0.8791769 -0.82305016 -1.48320146
## 4 -0.1267583 -1.0196901 -0.15491357 -0.32331213
## 5 -1.3553162 -1.0305483 -1.26411828 -0.29383122
## 6 -0.1311181 1.7498573 0.06113184 0.01019061
Dòng code này thực hiện thao tác Chuẩn hóa (Standardization) (hay còn gọi là Z-score scaling) cho bốn biến định lượng quan trọng (price, salary, loan, monthly_expenses). Mục đích là chuyển đổi các biến này về cùng một thang đo chuẩn tắc, nơi giá trị trung bình bằng 0 và độ lệch chuẩn bằng 1.
Đây là bước tiền xử lý bắt buộc cho một số mô hình thống kê để đảm bảo không có biến nào chi phối mô hình chỉ vì giá trị tuyệt đối của nó lớn hơn các biến khác.
Trong đó:
df <- df %>% mutate(…): Sử dụng cú pháp Tidyverse để thêm bốn cột mới vào dữ liệu df và ghi đè df.
mutate(…): Hàm của dplyr dùng để tạo/chỉnh sửa cột.
biến_scaled = scale(biến): Lệnh chuẩn hóa.
scale(): Hàm cơ sở của R. Mục đích là tính Z-score cho mỗi quan sát theo công thức: \(\frac{x - \mu}{\sigma}\) (Giá trị trừ Trung bình chia cho Độ lệch chuẩn).
Bốn biến được chọn để chuẩn hóa là: price (Giá), salary (Thu nhập), loan (Khoản vay), và monthly_expenses (Chi phí Hàng tháng).
Kết quả kĩ thuật
Các cột đã đổi tên (salary, loan) và các cột chuẩn hóa mới (Price_Scaled, Salary_Scaled,…) đều hiển thị chính xác.
Giá trị Chuẩn hóa: Các cột biến_Scaled đều chứa các giá trị âm hoặc dương (số thực), xác nhận chúng là các giá trị Z-score (độ lệch chuẩn so với trung bình).
Ví dụ:
Dòng 1: price_scaled là -0.9742211 nghĩa là giá trị price (\(412935\)) thấp hơn trung bình chung của \(price\) 0.9742211 độ lệch chuẩn.
Dòng 6: salary_scaled là 1.7498573 nghĩa là thu nhập (\(95520\)) cao hơn trung bình chung của \(salary\) 1.7498573 độ lệch chuẩn.
Ý nghĩa thống kê
Giá trị price_scaled \(-0.9742211\) là một Z-score âm. Điều này có nghĩa là giá bất động sản của hồ sơ này thấp hơn giá trung bình (Mean) của toàn bộ cột price là \(0.974\) độ lệch chuẩn (Standard Deviation).
Vị trí phân phối: Giá trị này nằm gần 1 độ lệch chuẩn bên trái của Trung bình, khẳng định bất động sản này thuộc phân khúc giá thấp hơn đáng kể so với mức giá phổ biến của thị trường toàn cầu trong tập dữ liệu.
Giá trị salary_scaled \(1.7498573\) là một Z-score dương và lớn. Điều này có nghĩa là thu nhập hàng tháng của khách hàng này cao hơn thu nhập trung bình (Mean) của toàn bộ cột salary là \(1.7498573\) độ lệch chuẩn (Standard Deviation).
Vị trí phân phối: Giá trị này nằm khá xa bên phải của Trung bình, khẳng định khách hàng này nằm trong nhóm có thu nhập cao vượt trội so với đại đa số khách hàng khác trong mẫu.
df <- df %>%
mutate(loan_to_income = loan / salary)
df %>%
select(loan, salary,loan_to_income) %>%
head()
## loan salary loan_to_income
## 1 193949 10745 18.050163
## 2 181465 16970 10.693282
## 3 307953 21914 14.052797
## 4 674720 17980 37.526140
## 5 65833 17676 3.724429
## 6 793316 95520 8.305235
Lệnh này thực hiện thao tác tạo biến dẫn xuất. Mục đích là tính toán biến mới loan_to_income (Tỷ lệ Khoản vay/Thu nhập) bằng cách chia loan (Số tiền Vay) cho salary (Thu nhập Hàng tháng). Biến này được tạo ra để định lượng mối quan hệ giữa tổng nợ và tổng thu nhập của khách hàng, một chỉ số rủi ro tài chính quan trọng khác so với emi_ratio.
Trong đó:
df <- df %>% mutate(…): Sử dụng cú pháp Tidyverse để thêm một cột mới vào df và ghi đè df.
mutate(…): Hàm của dplyr dùng để tạo/chỉnh sửa cột.
loan_to_income = loan / salary: Lệnh tạo biến mới.
loan / salary: Thực hiện phép chia giữa hai cột định lượng: Số tiền Vay chia cho Thu nhập hàng tháng.
Kết quả kĩ thuật
Giá trị dòng 3: \(14.052797\) là kết quả số thực chính xác của phép chia giữa loan = \(307.953\) và salary = \(21.914\) tại hàng thứ 3
Vị trí tương đối: Giá trị này cho thấy một tỷ lệ đòn bẩy tài chính ở mức trung bình. Nó không phải là giá trị thấp nhất và cũng không phải là giá trị cao nhất trong 6 hàng đầu.
Giá trị dòng 4: \(37.526140\) là kết quả số thực của phép chia tại hàng thứ 4 với loan = \(674.720\) và salary = \(17.980\).
Vị trí tương đối: Đây là giá trị cao nhất trong 6 hàng đầu. Nó cho thấy một tỷ lệ rất cao giữa khoản vay và thu nhập của giao dịch này.
Ý nghĩa thống kê
Với dòng thứ 3, con số \(14.052797\) cho thấy khách hàng này vay một khoản tiền (\(307.953\)) tương đương 14 lần thu nhập hàng tháng của họ(\(21.914\))
Đây là một mức đòn bẩy hợp lý và phổ biến trong giao dịch bất động sản. Nó đại diện cho một khách hàng “điển hình”, có gánh nặng nợ nằm trong vùng an toàn.
Với dòng thứ 4, con số \(37.526140\) cho thấy khách hàng này vay một khoản tiền (\(674.720\)) gấp hơn 37.5 lần thu nhập hàng tháng (\(17.980\)).
Đây là một tín hiệu rủi ro tín dụng rất cao (Red Flag). Gánh nặng nợ tổng thể quá lớn so với thu nhập hàng tháng, ngay cả khi chưa xét đến lãi suất.
df <- df %>%
mutate(
house_age = 2025 - constructed_year,
Age_Group = case_when(
house_age < 10 ~ "Mới (<10 năm)",
house_age >= 10 & house_age < 30 ~ "Trung niên (10-30 năm)",
TRUE ~ "Cũ (>30 năm)"
)
)
df %>%
select(country,constructed_year,Age_Group) %>%
head()
## country constructed_year Age_Group
## 1 France 1989 Cũ (>30 năm)
## 2 South Africa 1990 Cũ (>30 năm)
## 3 South Africa 2019 Mới (<10 năm)
## 4 Germany 2008 Trung niên (10-30 năm)
## 5 South Africa 2007 Trung niên (10-30 năm)
## 6 Canada 1985 Cũ (>30 năm)
Dòng code này thực hiện thao tác kỹ thuật tạo đặc trưng. Mục đích là tạo ra hai biến mới:
house_age: Một biến định lượng, tính toán tuổi của bất động sản dựa trên một năm gốc giả định là 2025.
Age_Group: Một biến định tính thứ bậc, phân loại các bất động sản thành ba nhóm tuổi rõ ràng (“Mới”, “Trung niên”, “Cũ”).
Trong đó:
df <- df %>% mutate(…): Sử dụng cú pháp Tidyverse để thêm hai cột mới vào df và ghi đè df.
house_age = 2025 - constructed_year: Lệnh tạo biến mới. Thực hiện phép trừ vector hóa để tính tuổi nhà. Con số \(2025\) là một giả định phương pháp luận quan trọng, ấn định “năm hiện tại” của phân tích.
Age_Group = case_when(…): Lệnh tạo biến mới. Sử dụng hàm logic case_when của dplyr để phân loại house_age vào các nhóm:
house_age < 10: Gán nhãn “Mới (<10 năm)”.
house_age >= 10 & house_age < 30: Gán nhãn “Trung niên (10-30 năm)”.
TRUE ~ “Cũ (>30 năm)”: Điều kiện mặc định, gán nhãn “Cũ (>30 năm)” cho mọi trường hợp còn lại (tức là house_age \(\ge 30\))
Kết quả kĩ thuật
Phép tính đã được thực hiện chính xác theo logic:
Dòng 1 (1989): house_age = \(2025 - 1989 = 36\). (Điều kiện TRUE \(\rightarrow\) Age_Group = “Cũ (>30 năm)”)
Dòng 3 (2019): house_age = \(2025 - 2019 = 6\). (Điều kiện < 10 \(\rightarrow\) Age_Group = “Mới (<10 năm)”)
Dòng 4 (2008): house_age = \(2025 - 2008 = 17\). (Điều kiện 10-30 \(\rightarrow\) Age_Group = “Trung niên (10-30 năm)”)
Tính thứ bậc: Biến Age_Group là một biến định tính thứ bậc (Ordinal) với 3 cấp độ.
Ý nghĩa thống kê
Chỉ trong 6 hàng đầu tiên, chúng ta đã thấy sự xuất hiện của cả ba nhóm tuổi mà ta đã định nghĩa (“Mới”, “Trung niên”, và “Cũ”).
Ý nghĩa kinh tế: Điều này cho thấy bộ dữ liệu (\(N=200.000\)) bao gồm một danh mục tài sản rất đa dạng về tuổi đời. Nó bao gồm các bất động sản mới xây (Dòng 3), các bất động sản trung niên (Dòng 4,5) và các bất động sản cũ/lịch sử (Dòng 1, 2, 6).
df <- df %>%
mutate(
salary_quantile = ntile(salary, 4),
Salary_Group = factor(salary_quantile,
labels = c("Thấp", "Trung bình thấp", "Trung bình cao", "Cao"))
)
df %>%
select(salary_quantile, Salary_Group) %>%
head()
## salary_quantile Salary_Group
## 1 1 Thấp
## 2 1 Thấp
## 3 2 Trung bình thấp
## 4 1 Thấp
## 5 1 Thấp
## 6 4 Cao
Lệnh này thực hiện thao tác* kỹ thuật tạo đặc trưng . Mục đích là rời rạc hóa biến định lượng liên tục salary (Thu nhập) thành một biến định tính thứ bậc mới là Salary_Group (Nhóm Thu nhập) dựa trên tứ phân vị (Quartiles).
Trong đó:
df <- df %>% mutate(…): Sử dụng cú pháp Tidyverse (gói dplyr) để thêm hai cột mới vào df và ghi đè df.
salary_quantile = ntile(salary, 4): Lệnh tạo biến số nguyên.
ntile(): Hàm của dplyr. Mục đích là chia vector salary thành 4 nhóm (tham số n=4) có số lượng quan sát gần bằng nhau (Tứ phân vị) .Hàm này gán một số nguyên (1, 2, 3, hoặc 4) cho mỗi hàng, trong đó \(1\) là 25% thu nhập thấp nhất và \(4\) là 25% thu nhập cao nhất.
Salary_Group = factor(…): Lệnh tạo biến định tính thứ bậc.
factor(): Hàm cơ sở của R. Mục đích là chuyển đổi vector số salary_quantile (1, 2, 3, 4) thành một biến định tính (factor).
labels = c(…): Tham số bắt buộc trong trường hợp này. Mục đích là gán các nhãn Tiếng Việt (“Thấp”, “Trung bình thấp”,“Trung bình cao”, “Cao”) tương ứng với các cấp độ số (1, 2, 3, 4).
Kết quả kĩ thuật
Bảng kết quả xác nhận mối quan hệ ánh xạ 1:1 giữa giá trị số của tứ phân vị và nhãn định danh (label) của nó.
Giá trị 1 trong salary_quantile tương ứng với nhãn “Thấp”.
Giá trị 2 trong salary_quantile tương ứng với nhãn “Trung bình thấp”.
Giá trị 3 trong salary_quantile tương ứng với nhãn “Trung bình cao”.
Giá trị 4 trong salary_quantile tương ứng với nhãn “Cao”.
Biến Salary_Group đã được tạo thành công dưới dạng một biến định tính thứ bậc với các cấp độ rõ ràng (“Thấp” < “Trung bình thấp” < “Trung bình cao” < “Cao”).
Ý nghĩa thống kê
Bảng kết quả này xác nhận việc rời rạc hóa thành công biến salary (Thu nhập).
Thay vì phân tích ảnh hưởng của từng giá trị thu nhập liên tục, giờ đây có thể so sánh trực tiếp hành vi và rủi ro giữa bốn nhóm thu nhập riêng biệt.
Định vị Khách hàng: Kết quả cho thấy vị trí tương đối của 6 khách hàng đầu tiên trong phổ thu nhập của toàn bộ \(200.000\) quan sát:
Khách hàng 1, 2, 4, 5 (Nhóm “Thấp”): Thuộc nhóm \(25\%\) khách hàng có thu nhập thấp nhất trong bộ dữ liệu.
Khách hàng 3 (Nhóm “Trung bình thấp”): Thuộc nhóm \(25\%\) khách hàng có thu nhập từ \(25\%\) đến \(50\%\).
Khách hàng 6 (Nhóm “Cao”): Thuộc nhóm \(25\%\) khách hàng có thu nhập cao nhất trong bộ dữ liệu.
levels(df$furnishing_status) <- c("Đầy đủ nội thất", "Bán nội thất", "Cơ bản")
df %>%
select(property_type,furnishing_status) %>%
head()
## property_type furnishing_status
## 1 Farmhouse Bán nội thất
## 2 Apartment Bán nội thất
## 3 Farmhouse Bán nội thất
## 4 Farmhouse Bán nội thất
## 5 Townhouse Đầy đủ nội thất
## 6 Villa Bán nội thất
Lệnh này có mục đích gán lại nhãn cho biến định tính furnishing_status (Tình trạng Nội thất). Mục đích là để Việt hóa các nhãn gốc (tiếng Anh) thành các nhãn tiếng Việt (“Đầy đủ nội thất”, “Bán nội thất” và ” cơ bản”), giúp các bảng thống kê và biểu đồ trong bài tiểu luận trở nên dễ đọc và chuyên nghiệp hơn đối với người xem.
Trong đó :
levels(df$furnishing_status): Đây là vị trí được gán. Hàm levels() được sử dụng để truy cập vào thuộc tính levels của biến furnishing_status).
<-: Toán tử gán. Gán giá trị bên phải cho đối tượng bên trái.
c(“Đầy đủ nội thất”, “Bán nội thất”, “Cơ bản”): Đây là giá trị được gán . Nó là một vector chuỗi ký tự chứa các nhãn mới.
Kết quả kĩ thuật
Code không trả về bảng, nhưng thay đổi vĩnh viễn thuộc tính nhãn của cột furnishing_status trong df.
Tính chính xác của thứ tự: Khi as.factor(furnishing_status) được gọi, R sắp xếp các cấp độ (levels) theo thứ tự bảng chữ cái:“Fully-Furnished” (Cấp 1), “Semi-Furnished” (Cấp 2), “Unfurnished” (Cấp 3).
Lệnh gán vector c(“Đầy đủ nội thất”, “Bán nội thất”, “Cơ bản”) cho kết quả ánh xạ:
“Fully-Furnished” (Cấp 1) \(\rightarrow\) “Đầy đủ nội thất”
“Semi-Furnished” (Cấp 2) \(\rightarrow\) “Bán nội thất”
“Unfurnished” (Cấp 3) \(\rightarrow\) “Cơ bản”
Ý nghĩa thống kê
Phân tích kỹ thuật xác nhận rằng các nhãn tiếng Việt đã được gán chính xác với ý nghĩa gốc (ví dụ: “Fully-Furnished” \(\rightarrow\) “Đầy đủ nội thất”).
Đây là bước quan trọng bậc nhất. Nếu gán nhầm (ví dụ: “Fully-Furnished” \(\rightarrow\) “Cơ bản”), mọi phân tích kinh tế tiếp theo liên quan đến tình trạng nội thất sẽ bị sai lệch hoàn toàn.
tab_decision <- table(df$decision)
tab_decision
##
## 0 1
## 153932 46068
prop.table(tab_decision)
##
## 0 1
## 0.76966 0.23034
Khối code này có hai mục đích chính:
Thống kê tần số: Lệnh table(df$decision) dùng để đếm số lượng quan sát (Tần số) cho mỗi cấp độ của biến mục tiêu decision (tức là đếm xem có bao nhiêu hồ sơ “0 - Không mua” và “1 - Mua”).
Thống kê tần suất: Lệnh prop.table(tab_decision) dùng để tính toán tỷ lệ phần trăm (Tần suất) của mỗi cấp độ “0” và “1” so với tổng số quan sát.
Trong đó:
1.tab_decision <- table(df$decision):
Với df$decision: Tham số bắt buộc, truy cập vào cột decision (biến mục tiêu đã được chuyển thành factor).
table(): Hàm cơ sở của R, dùng để đếm số lần xuất hiện của các cấp độ (“0” và “1”).
tab_decision <-: Toán tử gán, lưu kết quả bảng tần số vào một đối tượng mới tên là tab_decision.
2.prop.table(tab_decision):
tab_decision: Tham số bắt buộc, là đối tượng bảng tần số (đã tạo ở trên).
prop.table(): Hàm cơ sở của R, dùng để chia tần số của mỗi ô cho tổng tần số, trả về tần suất.
Kết quả kĩ thuật
Tổng tần số là \(153.932 + 46.068 = 200.000\) (khớp với dim(df)).
Đặc điểm: Số lượng “0” lớn hơn rất nhiều so với số lượng “1”.
Tỷ lệ “Không mua” chiếm \(76.966\%\), tỷ lệ “Mua” chiếm \(23.034\%\).
Ý nghĩa thống kê
Tính mất cân bằng cực độ của dữ liệu (Highly Imbalanced Data):Đây là phát hiện quan trọng nhất về mặt thống kê từ khối code này.Dữ liệu bị mất cân bằng nghiêm trọng.
Nhóm đa số: Là nhóm “0 - Không mua”, chiếm \(79.966\%\) tổng số mẫu.
Nhóm thiểu số: Là nhóm “1 - Mua”, chỉ chiếm \(23.034\%\) tổng số mẫu.
Phản ánh thực tế kinh tế: Tỷ lệ \(23.034\%\) là hợp lý. Trong thực tế, số lượng người xem xét, khảo sát, hoặc bắt đầu quy trình vay vốn (decision=0) luôn cao hơn rất nhiều so với số lượng người thực sự hoàn tất giao dịch mua nhà (decision=1). Kết quả này xác nhận dữ liệu đang phản ánh đúng hành vi thị trường.
Kinh tế thị trường có thể đang gặp phải các rào cản hoặc trở ngại khiến phần lớn người tham gia thị trường không lựa chọn mua nhà: có thể do thu nhập chưa đáp ứng đủ, giá bất động sản cao, hoặc tâm lý không muốn sở hữu chưa phổ biến.
Tỉ lệ này còn có thể phản ánh chính sách vĩ mô về nhà ở, mức độ tiếp cận tín dụng, hoặc xu hướng đầu tư - ví dụ xu hướng đầu tư vào sản phẩm thuê thay vì mua.
tab_price_dec <- table(df$price_group, df$decision)
tab_price_dec
##
## 0 1
## Cao 42568 12706
## Rất cao 79427 22809
## Thấp 6672 2203
## Trung bình 25265 8350
prop.table(tab_price_dec, margin = 1)
##
## 0 1
## Cao 0.7701270 0.2298730
## Rất cao 0.7768985 0.2231015
## Thấp 0.7517746 0.2482254
## Trung bình 0.7515990 0.2484010
Khối code này có hai mục đích thống kê rõ ràng:
1.Tạo bảng tần số hai chiều (Bảng chéo): Lệnh table(…) dùng để đếm số lượng quan sát (Tần số) của decision (Mua/Không mua) cho từng price_group (nhóm Giá).
2.Tạo bảng tần suất: Lệnh prop.table(…, margin = 1) dùng để tính toán tỷ lệ phần trăm (Tần suất) của “Mua” và “Không mua” trong nội bộ từng nhóm giá.
Trong đó :
1.tab_price_dec <- table(df\(price_group, df\)decision):
table(hàng, cột): Hàm cơ sở của R. Mục đích là tạo một bảng tần số hai chiều.
df$price_group: Biến định tính (hàng).
df$decision: Biến định tính (cột).
tab_price_dec <-: Toán tử gán, lưu bảng tần số này vào đối tượng
2.tab_price_dec.prop.table(tab_price_dec, margin = 1):
prop.table(): Hàm cơ sở của R, dùng để tính tần suất.
tab_price_dec: Đối tượng bảng tần số (đã tạo ở trên).
margin = 1: Đây là tham số quan trọng nhất. Nó chỉ định R phải tính tỷ lệ theo hàng (theo price_group), đảm bảo tổng của mỗi hàng bằng 100%.
Kết quả kĩ thuật
Các nhóm giá bao gồm: Rất cao, Cao, Trung bình, và Thấp. Từng nhóm đều phân bổ quyết định mua (1) và không mua (0).
Ví dụ, nhóm “Rất cao” có \(79.427\) quyết định không mua và \(22.809\) quyết định mua; nhóm “Cao” có \(42.568\) quyết định không mua, \(12.706\) quyết định mua; các nhóm còn lại cũng tương tự.
Tỷ lệ mua trong từng nhóm giá đều dao động quanh mức 22-24%. Cụ thể: nhóm “Rất cao” là 22.31%, “Cao” là 22.99%, “Trung bình” là 24.84%, và “Thấp” là 24.82%.
Về cân bằng dữ liệu, các nhóm đều có tỷ lệ mua thấp hơn đáng kể so với không mua nhưng không quá khác biệt giữa các nhóm. Như vậy, biến nhóm giá không tạo ra sự phân tầng lớn trong xác suất mua – mẫu không bị “mất cân bằng cục bộ” theo lớp giá.
Nghĩa là, khi dùng nhóm giá để dự báo khả năng mua, hiệu quả phân tách sẽ không cao, vì tỷ lệ giữa các nhóm khá tương tự nhau (chênh lệch nhỏ, đều khoảng 22–25%).
Ý nghĩa thống kê
Số liệu cho thấy tỷ lệ mua bất động sản không khác biệt lớn giữa các nhóm giá: bất kể là giá “Rất cao”, “Cao”, “Trung bình” hay “Thấp”, vẫn chỉ khoảng 23–25% người lựa chọn mua.
Điều này phản ánh rằng, đối với khách hàng trên thị trường này, yếu tố giá chưa phải là rào cản hoặc động lực quyết định quan trọng nhất để thay đổi hành vi mua. Nhu cầu hoặc quyết định mua bị chi phối bởi các yếu tố khác (đặc điểm bất động sản, thu nhập, môi trường sống…) hơn là chỉ giá bán.
Tỷ lệ mua ở nhóm giá thấp chỉ nhỉnh hơn nhóm giá rất cao khoảng 2%, cho thấy khả năng tiếp cận sản phẩm giá thấp chưa thực sự mở rộng đầu ra thị trường. Đây là gợi ý với các doanh nghiệp bất động sản: muốn kích thích nhu cầu, ngoài việc giảm giá, cần đa dạng hóa sản phẩm, tập trung vào chất lượng, vị trí, dịch vụ đi kèm, hoặc các chính sách hỗ trợ tài chính.
Kết quả này cũng phù hợp với thực tế thị trường phía cung: phần lớn giao dịch/vốn hóa vẫn chưa đạt ngưỡng lan toả toàn diện dù các sản phẩm giá thấp chiếm tỷ trọng lớn.
agg_price_dec <- df %>%
group_by(decision) %>%
summarise(
n = n(),
mean_price = mean(price),
sd_price = sd(price),
median_price = median(price)
)
agg_price_dec
## # A tibble: 2 × 5
## decision n mean_price sd_price median_price
## <fct> <int> <dbl> <dbl> <dbl>
## 1 0 153932 1233212. 836705. 1033278.
## 2 1 46068 1155732. 775540. 989078.
Lệnh này có mục đích thực hiện thống kê mô tả so sánh cho biến price (Giá). Nó nhóm \(200.000\) quan sát thành hai nhóm (decision=0 và decision=1), sau đó tính toán bốn chỉ số thống kê (Tần số, Trung bình, Độ lệch chuẩn, Trung vị) cho price của mỗi nhóm.
Mục đích là để so sánh trực tiếp đặc điểm giá của các bất động sản “Quyết định không mua” (0) và “Quyết định mua” (1).
Trong đó: 1.agg_price_dec <- …: Toán tử gán, lưu bảng kết quả vào đối tượng agg_price_dec.
2.df %>% group_by(decision): Sử dụng cú pháp Tidyverse (gói dplyr).
group_by(decision): Đây là thành phần bắt buộc. Mục đích là phân nhóm dữ liệu df thành hai nhóm dựa trên biến decision (đã là factor “0” và “1”).
3.summarise(…): Hàm của dplyr. Mục đích là thực hiện các phép tính tóm tắt cho từng nhóm đã được group_by tạo ra:
n = n(): Đếm số lượng quan sát (Tần số) trong mỗi nhóm.
mean_price = mean(price): Tính giá trị Trung bình của price cho mỗi nhóm.
sd_price = sd(price): Tính Độ lệch chuẩn của price cho mỗi nhóm.
median_price = median(price): Tính giá trị Trung vị của price cho mỗi nhóm.
Kết quả kĩ thuật
Số lượng quan sát cho quyết định không mua (0) là 153.932 quan sát, còn mua (1) là 46.068 quan sát; thể hiện dữ liệu khá mất cân đối giữa hai nhóm.
Giá trị mean_price nhóm không mua là 1.233.212, nhóm mua là 1.155.732. Nhóm mua có giá nhà trung bình thấp hơn khá rõ so với nhóm không mua (chênh gần 78.000 USD).
Giá trị median_price cũng tương tự: nhóm mua là 989.079, thấp hơn nhóm không mua là 1.033.279 (chênh gần 44.200 USD) . Điều này chứng tỏ không chỉ giá trị trung bình mà cả trung vị của nhóm mua cũng thấp hơn, cho thấy xu hướng hành vi dành ưu tiên mua cho các Bất động sản giá thấp hơn.
Độ lệch chuẩn (sd_price) của nhóm không mua (836.705) cao hơn nhóm mua (775.540), nghĩa là các giao dịch không mua xảy ra ở các mức giá đa dạng, còn giao dịch mua tập trung hơn ở quãng giá thấp hơn và ít bị tản mạn.
Ý nghĩa thống kê
Nhóm mua nhà đa số diễn ra ở mức giá thấp hơn so với nhóm không mua; cụ thể, giá trung bình và giá trung vị nhóm mua đều thấp hơn nhóm không mua khoảng 78.000 USD cho giá trung bình và 44.200 USD cho giá trung vị. Điều này phản ánh tình hình thực tế thị trường: khả năng chi trả, sự tiếp cận tài chính và xu hướng mua nhà tập trung ở phân khúc giá vừa và thấp.
Độ lệch chuẩn giá ở nhóm không mua lớn hơn, cho thấy người không mua thường tiếp cận nhiều loại bất động sản, kể cả ở phân khúc giá cao, nhưng cuối cùng không ra quyết định mua. Ngược lại, các giao dịch mua thực sự diễn ra tập trung hơn ở dải giá thấp và ổn định hơn.
Như vậy, các doanh nghiệp bất động sản muốn nâng cao tỷ lệ giao dịch thành công nên tập trung phát triển sản phẩm ở tầm giá thấp-trung bình, kết hợp với các giải pháp tài chính để tăng khả năng tiếp cận cho nhóm khách hàng có nhu cầu thực.
agg_finance <- df %>%
group_by(decision) %>%
summarise(
mean_salary = mean(salary),
sd_salary = sd(salary),
mean_loan = mean(loan),
sd_loan = sd(loan),
mean_emi = mean(emi_ratio),
sd_emi = sd(emi_ratio)
)
agg_finance
## # A tibble: 2 × 7
## decision mean_salary sd_salary mean_loan sd_loan mean_emi sd_emi
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 45126. 28066. 773941. 560421. 0.214 0.242
## 2 1 51214. 27248. 712369. 505832. 0.133 0.0922
Lệnh này có mục đích thực hiện thống kê mô tả so sánh cho các biến tài chính (salary, loan, emi_ratio). Nó nhóm 200,000 quan sát thành hai nhóm (decision=0 và decision=1), sau đó tính toán hai chỉ số thống kê quan trọng (Trung bình và Độ lệch chuẩn) cho mỗi biến tài chính của mỗi nhóm.
Trong đó:
agg_finance <- …: Toán tử gán (<-), lưu bảng kết quả tóm tắt các chỉ số tài chính vào đối tượng agg_finance.
df %>% group_by(decision): Sử dụng cú pháp Tidyverse (gói dplyr). Truyền DataFrame df làm đầu vào cho lệnh group_by.
group_by(decision): Đây là thành phần bắt buộc. Mục đích là phân nhóm dữ liệu df thành hai nhóm dựa trên biến decision (0: Không mua và 1: Mua).
summarise(…): Hàm của dplyr. Mục đích là thực hiện các phép tính tóm tắt cho từng nhóm đã được group_by tạo ra:
mean_salary = mean(salary): Tính giá trị Trung bình (Mean) của Thu nhập cho mỗi nhóm.
sd_salary = sd(salary): Tính Độ lệch chuẩn (Standard Deviation) của Thu nhập cho mỗi nhóm.
mean_loan = mean(loan): Tính giá trị Trung bình Khoản vay cho mỗi nhóm.
sd_loan = sd(loan): Tính Độ lệch chuẩn Khoản vay cho mỗi nhóm.
mean_emi = mean(emi_ratio): Tính Trung bình Tỷ lệ EMI/Thu nhập cho mỗi nhóm.
sd_emi = sd(emi_ratio): Tính Độ lệch chuẩn Tỷ lệ EMI/Thu nhập cho mỗi nhóm.
Kết quả kĩ thuật
Mức lương trung bình của nhóm mua (51.214 USD) cao hơn nhóm không mua (45.126 USD ), nhưng độ lệch chuẩn cũng thấp hơn (27.248 USD so với 28.066 USD). Điều này cho thấy nhóm quyết định mua có thu nhập cao hơn và biến động lương trong nhóm này thấp hơn, thể hiện tính đồng đều tài chính trong nhóm mua.
Khoản vay trung bình (mean_loan): nhóm mua là 712.369 USD, thấp hơn nhóm không mua (773.941 USD). Độ lệch chuẩn khoản vay ở nhóm mua cũng nhỏ hơn (505.832 USD so với 560.421 USD), xác nhận rằng người mua thực tế vay ít hơn và khoản vay của nhóm này ít biến động hơn.
Tỷ lệ EMI/Thu nhập bình quân của nhóm mua chỉ là 0,133 (tức 13%), trong khi nhóm không mua là 0,214 (21%). Độ lệch chuẩn tỷ lệ này cũng thấp hơn rõ rệt ở nhóm mua (0,092 so với 0,242)
Nhóm mua nhà không chỉ có thu nhập bình quân cao hơn, mà còn kiểm soát được tỷ lệ gánh nặng trả nợ hàng tháng ở mức an toàn hơn hẳn. Mức dao động các chỉ số trong nhóm mua cũng thấp hơn, chứng tỏ đây là nhóm có hồ sơ tài chính và rủi ro tín dụng tốt hơn, rất quan trọng với những phân tích mô hình định lượng hoặc dự báo hành vi tài chính.
ý nghĩa thống kê
Nhóm mua nhà có thu nhập cao hơn (51.214 USD so với 45.126 USD), xác nhận rằng hành vi mua nhà bị chi phối bởi năng lực tài chính cá nhân mạnh hơn trung bình cộng của toàn bộ mẫu khảo sát.
Quy mô khoản vay bình quân của nhóm mua (712.369 USD) thấp hơn nhóm không mua (773.941 USD), cho thấy khách hàng thực sự mua thường vay phù hợp với năng lực trả nợ thay vì gồng gánh khoản vay lớn vượt quá khả năng tài chính. Đây là biểu hiện của hành vi tài chính an toàn.
Tỷ lệ EMI/Thu nhập của nhóm mua chỉ khoảng 13%, còn nhóm không mua tới 21%. Theo chuẩn quốc tế và thực tiễn ngành tín dụng/mortgage, tỷ lệ này càng thấp càng tốt. Tỷ lệ thấp không chỉ giúp khách hàng dễ dàng được duyệt hồ sơ, mà còn phản ánh khả năng cân đối tài chính tốt, tránh nguy cơ nợ xấu hoặc sức ép tài chính quá lớn với ngân sách hàng tháng.
Độ biến động chỉ số này ở nhóm mua (0,092) thấp hơn hẳn nhóm không mua (0,242) cho thấy đa số khách mua hạn chế vay nợ vượt chuẩn an toàn, và các hồ sơ bị từ chối mua nhà đa phần là rủi ro tín dụng cao hoặc chi tiêu vượt khả năng chịu đựng tài chính.
Ý nghĩa cho doanh nghiệp: Nên ưu tiên phát triển sản phẩm và giải pháp tài chính cho nhóm khách hàng có hồ sơ tài chính an toàn, tập trung các phân khúc có khả năng duy trì tỷ lệ EMI/Thu nhập thấp; đồng thời mạnh dạn mở rộng chính sách linh hoạt (ví dụ kéo dài kỳ hạn vay, giảm lãi, tăng ưu đãi) cho nhóm khách đang sát ngưỡng “an toàn” về tỷ lệ này để tăng tỷ lệ chuyển đổi.
t_price <- t.test(price ~ decision, data = df)
t_price
##
## Welch Two Sample t-test
##
## data: price by decision
## t = 18.466, df = 80816, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
## 95 percent confidence interval:
## 69256.2 85703.3
## sample estimates:
## mean in group 0 mean in group 1
## 1233212 1155732
tidy(t_price)
## # A tibble: 1 × 10
## estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 77480. 1233212. 1155732. 18.5 5.52e-76 80816. 69256. 85703.
## # ℹ 2 more variables: method <chr>, alternative <chr>
Đoạn code này dùng để thực hiện Kiểm định T-test Độc lập (Welch Two Sample t-test).
Mục đích chính là để kiểm tra xem Trung bình (Mean) của biến Giá nhà (price) có sự khác biệt có ý nghĩa thống kê hay không giữa hai nhóm khách hàng: nhóm Không mua (decision = 0) và nhóm Mua (decision = 1).
Trong đó :
t.test(…): Đây là hàm chính thực hiện kiểm định T-test.
price ~ decision: Tham số này thiết lập công thức kiểm định. Biến price (biến phụ thuộc) được so sánh theo biến decision (biến phân nhóm).
data = df: Chỉ định tập dữ liệu df là nguồn dữ liệu được sử dụng cho kiểm định.
t_price <- …: Đây là toán tử gán, lưu kết quả chi tiết của kiểm định T-test vào đối tượng t_price để sử dụng trong các lệnh tiếp theo.
t_price: Lệnh này in ra kết quả chi tiết (thô) của kiểm định T-test.
tidy(t_price): Hàm này thuộc gói broom. Mục đích là chuyển đổi kết quả thống kê (t_price) sang định dạng bảng dữ liệu (data frame) gọn gàng, dễ dàng sao chép và trình bày .
Kết quả kĩ thuật
Giá trị trung bình ở nhóm không mua (0) là 1.233.212 USD, ở nhóm mua (1) là 1.155.732 USD, chênh lệch khoảng 77.480 USD.
Hệ số kiểm định t (Welch Two Sample t-test) là 18,47 với bậc tự do ~80.816, p-value < 2.2e-16 (gần 0).
Khoảng tin cậy 95% cho chênh lệch trung bình: từ 69.256 USD đến 85.703 USD, đều khác biệt xa 0.
Giá trị p rất nhỏ và khoảng tin cậy không chứa 0 cho thấy sự khác biệt về giá trung bình giữa hai nhóm thực sự có ý nghĩa thống kê (bác bỏ giả thuyết trung bình bằng nhau).
Tóm lại, giá trung bình giao dịch bất động sản của nhóm mua thấp hơn nhóm không mua một khoảng có ý nghĩa lớn cả về giá trị tuyệt đối và thống kê.
Ý nghĩa thống kê
Chênh lệch giá khoảng 77.480 USD là con số lớn, phản ánh khách hàng thực sự xuống tiền mua nhà thường ưu tiên lựa chọn các bất động sản có giá thấp hơn. Như vậy, phân khúc bất động sản có giá phải chăng là nơi có chuyển đổi giao dịch cao nhất.
Giá trung bình nhóm không mua cao hơn nhưng không tạo ra giao dịch thành công, chứng tỏ thị trường vẫn còn tồn đọng một lượng sản phẩm giá trị lớn nhưng khó tiêu thụ. Điều này cảnh báo doanh nghiệp và chủ đầu tư cần tối ưu lại cấu trúc sản phẩm, hướng mạnh sang dải bất động sản phổ thông/tầm trung.
Khoảng tin cậy chênh lệch giá (thấp nhất 69.256 USD, cao nhất 85.703 USD) giúp nhà quản trị hoạch định chính sách giá tốt hơn, ví dụ biết đặt giá để tối đa hóa khả năng bán ra.
Kết quả thống kê này cũng là căn cứ vững chắc khi đề xuất với ngân hàng, chính quyền hoặc nhà đầu tư, minh chứng xu hướng thị trường thiên về các sản phẩm vừa túi tiền thay vì chạy theo phân khúc đắt đỏ.
wilcox.test(price ~ decision, data = df)
##
## Wilcoxon rank sum test with continuity correction
##
## data: price by decision
## W = 3704107167, p-value < 2.2e-16
## alternative hypothesis: true location shift is not equal to 0
Đoạn code này dùng để thực hiện Kiểm định Wilcoxon Rank Sum (hay còn gọi là Kiểm định Mann-Whitney U).Mục đích chính là để kiểm tra xem phân phối của biến Giá nhà (price) có sự khác biệt có ý nghĩa thống kê hay không giữa hai nhóm quyết định (decision = 0 và decision = 1).
Trong đó:
wilcox.test(…): Đây là hàm chính thực hiện Kiểm định Wilcoxon Rank Sum.
price ~ decision: Tham số này thiết lập công thức kiểm định. Biến price được so sánh theo biến decision.
data = df: Chỉ định tập dữ liệu df là nguồn dữ liệu được sử dụng cho kiểm định.
Kết quả kĩ thuật
Kết quả kiểm định Wilcoxon rank sum test giữa giá (price) và quyết định mua (decision) cho ra giá trị thống kê W=3.704.107.167, với p-value < 2.2e-16, gần như bằng 0.
Giá trị p rất nhỏ cho phép bác bỏ giả thuyết không có khác biệt về phân phối giá giữa hai nhóm quyết định. Nói cách khác, có bằng chứng mạnh mẽ rằng phân phối giá của hai nhóm là khác nhau.
Đây là kiểm định phi tham số, không giả định phân phối chuẩn nên kết quả càng có ý nghĩa khi mẫu lớn và có thể lệch.
Kết hợp với các thống kê mô tả và kiểm định t-test trước đó, kiểm định Wilcoxon xác nhận xu hướng giá trung vị các giao dịch mua thấp hơn rõ rệt giao dịch không mua, đồng thời khẳng định sự khác biệt này là có thực (cả ở phương diện trung bình lẫn trung vị).
Ý nghĩa thống kê
Kết quả kiểm định khẳng định rằng thị trường xuất hiện sự khác biệt về khả năng mua nhà giữa các phân khúc giá bán: khách mua chủ yếu nằm ở vùng giá thấp hơn, trong khi nhóm sản phẩm giá cao chủ yếu tồn kho hoặc nằm ở nhóm không mua.
Điều này có giá trị thực tiễn rất lớn với doanh nghiệp: khi lập chính sách bán hàng hoặc xây dựng sản phẩm mới, cần tập trung phát triển các phân khúc phù hợp với khả năng chi trả thực của đại bộ phận khách hàng, giảm đầu tư vào các phân khúc giá cao khó tiêu thụ.
Chính sách tín dụng cũng nên định hướng mạnh hơn cho nhóm “vừa túi tiền”, hỗ trợ lãi suất/chiết khấu nhằm nâng cao khả năng chuyển đổi của nhóm khách này, vừa giúp tăng doanh số, vừa giải quyết bài toán tồn kho phân khúc giá cao.
chisq <- chisq.test(table(df$price_group, df$decision))
chisq
##
## Pearson's Chi-squared test
##
## data: table(df$price_group, df$decision)
## X-squared = 108.15, df = 3, p-value < 2.2e-16
Đoạn code này dùng để thực hiện Kiểm định Chi-bình phương (Chi-squared Test for Independence).
Mục đích chính là để kiểm tra xem hai biến: nhóm giá (price_group) và Quyết định(decision) có mối liên hệ hay phụ thuộc vào nhau một cách có ý nghĩa thống kê hay không.
Trong đó:
chisq.test(…): Đây là hàm chính thực hiện Kiểm định Chi-bình phương.
chisq <- …: Đây là toán tử gán, lưu kết quả chi tiết của kiểm định Chi-bình phương vào đối tượng chisq.
chisq: Lệnh này in ra kết quả chi tiết của kiểm định Chi-bình phương.
Kết quả kĩ thuật
Kết quả kiểm định Pearson’s Chi-squared test cho ra giá trị thống kê chi-squared= 108.15, với 3 bậc tự do (df = 3) và p-value rất nhỏ (p-value < 2.2e-16), tức gần bằng 0.
P-value nhỏ hơn 0,05 rất nhiều, điều này cho phép bác bỏ giả thuyết độc lập giữa 2 biến: nhóm giá bất động sản và quyết định mua không độc lập với nhau.
Nghĩa là, có mối liên hệ thống kê giữa giá bất động sản và xác suất quyết định mua. Điều này xác nhận kết luận từ các kiểm định trước (t-test, Wilcoxon) bằng một kiểm định chuyên biệt cho hai biến phân loại.
Xét về sức mạnh mô hình hóa: yếu tố nhóm giá là biến giải thích có ý nghĩa khi xây dựng các mô hình dự báo xác suất mua nhà, hoặc phân tích khuynh hướng thị trường.
Ý nghĩa thống kê
Kết quả kiểm định này mang ý nghĩa rất thực tiễn: quyết định mua bất động sản chịu ảnh hưởng trực tiếp của mức giá. Không thể xem các nhóm giá/tầng lớp sản phẩm là giống nhau về sức tiêu thụ; cần phân tích kỹ từng phân khúc khi xây dựng tổ chức kinh doanh, định giá, hoặc đưa ra chiến lược bán hàng.
Với thị trường bất động sản, doanh nghiệp muốn tăng hiệu quả bán hàng phải xác định đúng nhóm giá mục tiêu. Những nhóm giá được nhiều người mua sẽ có xác suất giao dịch cao – đồng thời, những sản phẩm giá quá cao hoặc quá thấp đều cần chiến lược riêng biệt để tối ưu hiệu quả tiêu thụ.
Về khía cạnh tín dụng: ngân hàng, tổ chức tài chính hỗ trợ mua nhà cũng cần xem xét sát hơn từng nhóm giá để điều chỉnh điều kiện xét duyệt, gói vay, hạn mức phù hợp với khả năng thanh toán của từng nhóm khách hàng.
num_vars <- df %>%
select(price, property_size_sqft, salary, loan, emi_ratio, loan_to_income, satisfaction_score, neighbourhood_rating, connectivity_score)
cor_mat <- cor(num_vars, use = "pairwise.complete.obs")
round(cor_mat, 3)
## price property_size_sqft salary loan emi_ratio
## price 1.000 0.745 0.241 0.938 0.380
## property_size_sqft 0.745 1.000 0.000 0.699 0.449
## salary 0.241 0.000 1.000 0.227 -0.474
## loan 0.938 0.699 0.227 1.000 0.424
## emi_ratio 0.380 0.449 -0.474 0.424 1.000
## loan_to_income 0.396 0.468 -0.496 0.443 0.957
## satisfaction_score -0.001 0.000 -0.005 -0.001 0.002
## neighbourhood_rating 0.000 -0.001 0.000 0.000 -0.002
## connectivity_score 0.002 0.002 0.003 0.002 -0.001
## loan_to_income satisfaction_score neighbourhood_rating
## price 0.396 -0.001 0.000
## property_size_sqft 0.468 0.000 -0.001
## salary -0.496 -0.005 0.000
## loan 0.443 -0.001 0.000
## emi_ratio 0.957 0.002 -0.002
## loan_to_income 1.000 0.002 -0.002
## satisfaction_score 0.002 1.000 0.001
## neighbourhood_rating -0.002 0.001 1.000
## connectivity_score 0.000 0.000 0.000
## connectivity_score
## price 0.002
## property_size_sqft 0.002
## salary 0.003
## loan 0.002
## emi_ratio -0.001
## loan_to_income 0.000
## satisfaction_score 0.000
## neighbourhood_rating 0.000
## connectivity_score 1.000
Mục đích chính là để tính toán Ma trận Tương quan (Correlation Matrix) cho một tập hợp các biến số định lượng chính được chọn. Ma trận này đo lường mối quan hệ tuyến tính và độ mạnh của mối quan hệ đó giữa mọi cặp biến được chọn (ví dụ: Giá vs Thu nhập, Khoản vay vs Tỷ lệ EMI, v.v.).
Trong đó:
num_vars <- df %>% select(…): Toán tử gán, lưu DataFrame con chứa chỉ các biến định lượng đã chọn vào đối tượng num_vars.
df %>% select(…): Hàm select() của dplyr. Mục đích là tạo một tập hợp dữ liệu mới chỉ bao gồm các biến được liệt kê trong ngoặc đơn (price, property_size_sqft, salary, loan, emi_ratio, v.v.), loại bỏ tất cả các biến định tính và các biến không cần thiết khác.
cor(num_vars, use = “pairwise.complete.obs”): Đây là hàm chính, thực hiện tính toán Hệ số Tương quan Pearson (\(r\)) cho Ma trận Tương quan.
num_vars: Đối tượng đầu vào, là tập hợp các biến định lượng đã được chọn ở bước trên.
use = “pairwise.complete.obs”: Tham số này hướng dẫn R xử lý các giá trị khuyết (NA/missing values). Nó chỉ sử dụng các cặp quan sát (cặp hàng) nào có dữ liệu đầy đủ cho hai biến đang được tính tương quan.
cor_mat <- …: Toán tử gán, lưu kết quả Ma trận Tương quan vào đối tượng
cor_mat.round(cor_mat, 3): Đây là hàm cuối cùng, thực hiện làm tròn tất cả các giá trị (hệ số tương quan \(r\)) trong ma trận cor_mat đến 3 chữ số thập phân để dễ đọc và trình bày.
Kết quả kĩ thuật
Tương quan mạnh nhất: Giá bất động sản (price) có tương quan rất cao với khoản vay (loan) r=0,938 và diện tích (property_size_sqft) r=0,745. Điều này cho thấy giá tăng thì diện tích, khoản vay cũng tăng đáng kể – hoàn toàn hợp lý về mặt kỹ thuật và kiểm định dữ liệu.
Khoản vay và tỷ lệ khoản vay trên thu nhập (loan_to_income, r = 0,443), cùng tỷ lệ trả góp/thu nhập (emi_ratio, r = 0,424), cho thấy người vay lớn thường gánh tỷ trọng trả góp lớn hơn.
Mức độ tương quan giữa lương và giá chủ yếu trung bình-yếu (0,241), cho thấy lương không quyết định giá bất động sản mạnh như các yếu tố vật lý/tài chính cứng (diện tích, khoản vay).
Các biến đánh giá chất lượng sống (satisfaction_score, neighbourhood_rating, connectivity_score) gần như không tương quan với các biến tài chính – cấu trúc (tất cả |r| ≈ 0.001–0.002), hàm ý các yếu tố phi giá trị tài sản và phi tài chính độc lập trong thống kê với các biến chính.
Kết luận: Khi xây dựng mô hình dự báo giá hoặc các biến tài chính bất động sản, ưu tiên cao cho property_size_sqft, loan; ít chú trọng các chỉ số hài lòng, đánh giá khu vực nếu dùng phương pháp tuyến tính.
Ý nghĩa thống kê
Giá bất động sản tăng tỷ lệ thuận với diện tích và khoản vay (tương quan lần lượt là 0,745 và 0,938). Đây là xác nhận kinh điển: khách hàng muốn căn nhà lớn, cao cấp đồng nghĩa phải chi số tiền lớn hơn, cả trả trước lẫn quy mô khoản vay.
Dù vậy, lương cá nhân chỉ tương quan yếu đến giá bất động sản (0,241), chứng tỏ khả năng mua nhà hiện nay không đơn thuần dựa vào thu nhập sở hữu mà còn dựa vào khả năng vay/hỗ trợ tài chính khác. Điều này lý giải nhu cầu các gói tín dụng linh hoạt cho thị trường.
Tất cả các chỉ số hài lòng, chấm điểm khu vực và kết nối hạ tầng gần như không ảnh hưởng trực tiếp tới giá, khoản vay hoặc khả năng mua ở mặt thống kê; tuy nhiên giá trị các yếu tố này giữ vai trò nội tại về hành vi/ra quyết định cuối cùng.
Hệ số tương quan giữa emi_ratio và loan_to_income lên tới 0,957, nhấn mạnh hai biến này cùng phản ánh áp lực trả nợ với tài chính cá nhân, nên khi phân tích rủi ro tín dụng chỉ nên chọn 1 biến đại diện để tránh đa cộng tuyến.
Định hướng: Doanh nghiệp nên linh hoạt gói sản phẩm – tài chính hướng tới tăng diện tích nhà hợp lý với mức giá và khả năng vay, thay vì chỉ tập trung mỗi tăng thu nhập khách hàng.
df$decision_num <- as.numeric(as.character(df$decision))
cors_to_dec <- sapply(num_vars, function(x) cor(x, df$decision_num, use = "pairwise.complete.obs"))
sort(abs(cors_to_dec), decreasing = TRUE)
## satisfaction_score emi_ratio loan_to_income
## 0.5727833133 0.1560334586 0.1529831496
## salary property_size_sqft loan
## 0.0915459313 0.0572322480 0.0472266934
## price neighbourhood_rating connectivity_score
## 0.0396071638 0.0034687171 0.0003020615
cors_to_dec
## price property_size_sqft salary
## -0.0396071638 -0.0572322480 0.0915459313
## loan emi_ratio loan_to_income
## -0.0472266934 -0.1560334586 -0.1529831496
## satisfaction_score neighbourhood_rating connectivity_score
## 0.5727833133 0.0034687171 0.0003020615
Đoạn code này dùng để thực hiện Phân tích Tương quan Điểm-Nhị phân (Point-Biserial Correlation).
Mục đích chính là để đo lường mối quan hệ tuyến tính và xếp hạng (rank) độ mạnh của mối quan hệ đó giữa mỗi biến định lượng (trong num_vars) với biến mục tiêu nhị phân (decision).
Trong đó:
df\(decision_num <- as.numeric(as.character(df\)decision)): Tạo một cột mới decision_num bằng cách chuyển biến phân loại decision (có giá trị “0” và “1”) thành kiểu số học (numeric).
Lý do: Hàm cor() (tính tương quan) chỉ hoạt động trên các biến số.
sapply(num_vars, function(x) cor(x, df$decision_num, …)): Hàm sapply() là hàm chính để áp dụng phép tính tương quan cho tất cả các cột trong đối tượng num_vars một cách tự động.
num_vars: Đối tượng đầu vào, là tập hợp các biến định lượng (price, salary, loan, v.v.).
**cor(x, df\(decision_num, ...)**: Đây là phép tính được thực hiện bên trong vòng lặp. Nó tính hệ số tương quan Pearson (\)r$) giữa mỗi cột \(\mathbf{x}\) trong num_vars và biến mục tiêu df$decision_num.
use = “pairwise.complete.obs”: Hướng dẫn R xử lý giá trị khuyết (NA) bằng cách chỉ sử dụng các cặp quan sát đầy đủ dữ liệu cho hai biến đang được tính.
cors_to_dec <- …: Toán tử gán, lưu kết quả tương quan (một vector) vào đối tượng cors_to_dec.
sort(abs(cors_to_dec), decreasing = TRUE): Thực hiện xếp hạng độ mạnh của mối quan hệ tương quan.
abs(…): Hàm abs() lấy giá trị tuyệt đối của tất cả các hệ số tương quan (loại bỏ dấu âm/dương).
sort(…, decreasing = TRUE): Sắp xếp các giá trị tuyệt đối này theo thứ tự giảm dần (từ lớn nhất đến nhỏ nhất), cho biết biến nào có mối liên hệ mạnh mẽ nhất với decision.
cors_to_dec: Lệnh này in ra kết quả tương quan đầy đủ (có cả dấu âm/dương) sau khi đã được tính toán.
Kết quả kĩ thuật
Hệ số tương quan cao nhất (tuyệt đối) với quyết định mua là satisfaction_score (0,573), tiếp đến là loan_to_income(0,153), emi_ratio (0,156), salary (0,092), và các biến khác đều rất nhỏ (|r| < 0,06)
Giá trị dương của satisfaction_score (0,573) cho thấy khách có điểm hài lòng càng cao thì càng dễ ra quyết định mua. Biến này vượt trội hoàn toàn so với các biến tài chính, vật lý khác, xác nhận là đặc trưng dự báo mạnh nhất nếu xây dựng mô hình dự báo mua/bán.
Hệ số âm của emi_ratio và loan_to_income (~-0,15): tỷ lệ trả góp/thu nhập và tỷ lệ vay/thu nhập càng cao thì càng giảm xác suất mua, rất phù hợp logic rủi ro tín dụng và kết quả các phân tích trước đó, nhưng độ mạnh chỉ mức thấp-vừa – không phải là tiêu chí phân tách cực mạnh khi dự báo hành vi.
Lương, khoản vay, diện tích… có tương quan rất yếu với quyết định cuối cùng (dưới 0,1 tuyệt đối) – mô hình phân bổ/ảnh hưởng của các biến này tới hành vi mua khá hạn chế nếu xét độc lập.
Ý nghĩa thống kê
Hài lòng trải nghiệm (satisfaction_score) có ảnh hưởng lớn nhất tới quyết định mua bán bất động sản trên thực tế (r = 0,573 – mức rất cao đối với dữ liệu xã hội). Nghĩa là, ngoài các yếu tố vật chất-tài chính, cảm nhận và trải nghiệm khách hàng mới là yếu tố then chốt chuyển đổi hành vi mua thực tế.
Rủi ro tín dụng cá nhân (loan_to_income và emi_ratio) cũng ảnh hưởng nhưng chỉ mức trung bình: những người chịu áp lực tài chính lớn thật sự khó ra quyết định mua, nhưng vẫn không quyết định mạnh bằng yếu tố hài lòng với sản phẩm/dịch vụ/quy trình.
Chính sách doanh nghiệp, chiến lược kinh doanh nên xem yếu tố nâng cao trải nghiệm khách hàng/hài lòng tổng thể là ưu tiên số 1 nếu muốn thúc đẩy doanh số; song song đó, vẫn giữ nền tảng đánh giá năng lực tín dụng/thanh toán an toàn khi xét duyệt hồ sơ thực tế.
Những yếu tố vật lý (diện tích, khoản vay, lương, v.v.) chỉ mang ý nghĩa nền, không phải là yếu tố thúc đẩy quyết định chính – thực tế xã hội-chính sách phải tập trung cân bằng cả dịch vụ lẫn sản phẩm hữu hình.
tab_salary_dec <- table(df$Salary_Group, df$decision)
tab_salary_dec
##
## 0 1
## Thấp 41587 8413
## Trung bình thấp 38942 11058
## Trung bình cao 36894 13106
## Cao 36509 13491
prop.table(tab_salary_dec, margin = 1)
##
## 0 1
## Thấp 0.83174 0.16826
## Trung bình thấp 0.77884 0.22116
## Trung bình cao 0.73788 0.26212
## Cao 0.73018 0.26982
Đoạn code này dùng để phân tích mối quan hệ giữa Nhóm Lương và Quyết định mua nhà bằng cách tạo ra hai bảng tổng hợp:
tab_salary_dec: Bảng Tần số.
prop.table(…, margin = 1): Bảng Tỷ lệ.
Trong đó:
table(df\(Salary_Group, df\)decision): Hàm này tạo ra một Bảng Tần số Chéo.
Tham số 1 (df$Salary_Group): Biến định tính được đặt ở Hàng (Row).
Tham số 2 (df$decision): Biến định tính được đặt ở Cột (Column).
tab_salary_dec <- …: Toán tử gán, lưu bảng tần số (số lượng đếm) vào đối tượng tab_salary_dec.
prop.table(tab_salary_dec, margin = 1): Hàm này chuyển đổi bảng tần số thành bảng tỷ lệ phần trăm (Tần suất).
(tab_salary_dec): Bảng tần số đầu vào.
margin = 1: Tham số quan trọng, hướng dẫn R tính tỷ lệ theo Hàng (Rows). Điều này có nghĩa là tổng của mỗi hàng (tức là tổng của mỗi Salary_Group) sẽ là 1 (hoặc 100%).
Kết quả kĩ thuật
Số lượng mua (1) và không mua (0) của từng nhóm lương như sau:
Thấp: 41.587 quyết định không mua (0) và 8.413 quyết định mua (1), tỷ lệ mua chỉ 16,8%
Trung bình thấp: 38.942 quyết định không mua (0) và 11.058 quyết định mua (1), tỷ lệ mua 22,1%
Trung bình cao: 36.894 quyết định không mua (0) và 13.106 quyết định mua (1), tỷ lệ mua 26,1%
Cao: 36.509 quyết định không mua (0) và 13.491 quyết định mua (1), tỷ lệ mua 27,0%
Tỷ lệ quyết định mua theo nhóm lương tăng dần khi lương tăng: từ 16,8% (thấp) lên tới 27,0% (cao). Ngược lại, tỷ lệ không mua giảm từ 83,2% xuống còn 73,0%.
Kết quả thể hiện rõ ràng xu hướng tỷ lệ chuyển đổi mua nhà tăng dần theo mức thu nhập, cho thấy lương là yếu tố phân biệt hành vi tiêu dùng rất tốt.
Nếu xây dựng mô hình phân loại hoặc dự báo, biến nhóm lương có giá trị phân tách mạnh và đáng đưa vào ưu tiên.
t_satis <- t.test(satisfaction_score ~ decision, data = df)
tidy(t_satis)
## # A tibble: 1 × 10
## estimate estimate1 estimate2 statistic p.value parameter conf.low conf.high
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 -3.91 4.60 8.51 -462. 0 177880. -3.93 -3.89
## # ℹ 2 more variables: method <chr>, alternative <chr>
Đoạn code này dùng để thực hiện Kiểm định T-test Độc lập (Welch Two Sample t-test).
Mục đích chính là để kiểm tra xem Trung bình (Mean) của biến Điểm hài lòng (satisfaction_score) có sự khác biệt có ý nghĩa thống kê hay không giữa hai nhóm khách hàng: nhóm Không mua (decision = 0) và nhóm Mua (decision = 1).
Trong đó:
t_satis <- …: Đây là toán tử gán, lưu kết quả chi tiết của kiểm định T-test vào đối tượng t_satis.
t.test(…): Đây là hàm chính thực hiện kiểm định T-test.
satisfaction_score ~ decision: Tham số này thiết lập công thức kiểm định. Biến satisfaction_score (biến phụ thuộc, vế trái) được so sánh theo biến decision (biến phân nhóm, vế phải).
data = df: Chỉ định tập dữ liệu df là nguồn dữ liệu được sử dụng cho kiểm định.
tidy(t_satis): Hàm này thuộc gói broom. Mục đích là chuyển đổi kết quả thống kê (t_satis) sang định dạng bảng dữ liệu (data frame) gọn gàng, dễ dàng sao chép và trình bày.
Kết quả kĩ thuật
Giá trị trung bình điểm hài lòng nhóm không mua là 4,60, nhóm mua là 8,51.
Hiệu số trung bình giữa hai nhóm là -3,91, với khoảng tin cậy 95% cho hiệu số là từ -3,93 đến -3,89 (tất cả đều âm, không chứa 0).
Thống kê t rất lớn về giá trị tuyệt đối (-462,35) và p-value tuyệt đối bằng 0, khẳng định sự khác biệt giữa hai nhóm là cực kỳ có ý nghĩa thống kê.
Như vậy, điểm hài lòng là một biến phân loại cực mạnh cho hành vi mua: người mua có điểm hài lòng cao vượt trội so với nhóm không mua.
Do chênh lệch lớn, biến satisfaction_score hoàn toàn có thể được xem như yếu tố quyết định khi xây dựng mô hình dự báo hay phân tích.
Ý nghĩa thống kê
Trung bình khách mua nhà có mức hài lòng tới 8,51 trong khi người không mua chỉ đạt 4,60. Sự khác biệt này gần như gấp đôi, phản ánh sức mạnh của cảm nhận và trải nghiệm đối với quyết định hành động mua.
Kết quả này là cảnh báo quan trọng cho doanh nghiệp: muốn tăng doanh số, không chỉ trau chuốt yếu tố sản phẩm vật chất mà phải đầu tư mạnh vào trải nghiệm khách hàng, dịch vụ, chăm sóc và giải quyết khiếu nại. Sự vượt trội của điểm hài lòng khớp với định hướng hiện đại của các tập đoàn phát triển nhà trên thế giới.
Giá trị chênh lệch lớn, có ý nghĩa thực tế và thống kê, hàm ý mọi chiến lược marketing, hậu mãi, truyền thông, phát triển dịch vụ hạ, tầng đều phải lấy khách hàng làm trung tâm, liên tục nâng cao điểm hài lòng tổng thể.
quantile(df$price, probs = c(0.01,0.05,0.25,0.5,0.75,0.95,0.99))
## 1% 5% 25% 50% 75% 95% 99%
## 110735.9 210252.0 565989.5 1023429.0 1725556.5 2807338.9 3671005.4
Đoạn code này dùng để thực hiện Thống kê Mô tả, cụ thể là để phân tích sự phân bố của biến Giá nhà (price).
Mục đích chính là tính toán các Phân vị (Quantiles) cụ thể: Tứ phân vị (Quartiles: \(25\%\), \(50\%\) (Trung vị), \(75\%\)) và các Phân vị Ngoại lai (\(1\%\), \(5\%\), \(95\%\), \(99\%\)).
Trong đó:
quantile(…): Đây là hàm chính thực hiện tính toán các Phân vị.
df$price: Đối tượng đầu vào, là cột price (Giá nhà) từ DataFrame df.
probs = c(0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99): Tham số này là một vector số, xác định tỷ lệ phần trăm mà bạn muốn tính toán Phân vị. Các giá trị \(0.01\) (hoặc \(1\%\)), \(0.5\) (hoặc \(50\%\)), \(0.99\) (hoặc \(99\%\)) là các giá trị tiêu biểu được yêu cầu.
Kết quả kĩ thuật
1% thấp nhất: 110.736 USD
5% thấp nhất: 210.252 USD
25% (Q1): 565.990 USD
Median (trung vị, 50%): 1.023.429 USD
75% (Q3): 1.725.556 USD
95% cao nhất: 2.807.339 USD
99% cao nhất: 3.671.005 USD
Khoảng giá rất rộng, từ khoảng hơn 100.000 USD đến hàng triệu USD, chỉ số trung vị là 1.023.429 USD. Phân phối này có xu hướng lệch phải (right-skewed): phân nửa dưới so với trung vị nhỏ hơn nhiều so với phân nửa trên, thể hiện qua việc nhóm 5%-25%-50% tăng chậm, nhưng từ 75% trở đi giá tăng rất mạnh.
Phần lớn bất động sản trên thị trường tập trung ở phân khúc dưới 2 triệu USD, nhưng có “đuôi” giá cao dài – tức tồn tại những bất động sản siêu sang, khá biệt lập so với mặt bằng chung.
Ý nghĩa thống kê
Tầng lớp trung vị (đa số dân, giao dịch điển hình) hoạt động chủ yếu quanh mức giá vừa và thấp:50% giao dịch giá dưới 1.023.429 USD, điều này lý giải vì sao các sản phẩm đại chúng, giá phải chăng lại tiêu thụ tốt nhất trên thị trường.
Bất động sản cao cấp (top 5%): Chênh lệch lớn so với nhóm trung-và-thấp, sản phẩm ở vùng này thuộc loại xa xỉ – bán khó, chiếm tỷ lệ nhỏ, thường tồn kho lâu (trên 2.807.339 USD thuộc top 5%, trên 3.671.005 USD thuộc top 1%).
Chính sách doanh nghiệp/nhà nước nên tập trung phát triển sản phẩm, tín dụng, ưu đãi quanh vùng trung vị (khoảng 1 triệu USD), tối ưu hóa dòng sản phẩm chính thay vì chạy đua quá mạnh vào phân khúc sang trọng, vốn chỉ hợp với số ít khách hàng đặc biệt.
Phân tích này còn có giá trị trong định giá, thiết lập chính sách thuế hoặc cắt ngưỡng chiết khấu hợp lý giữa các tầng sản phẩm trên thị trường bất động sản hiện đại.
prop_high <- prop.table(table(df$price_group, df$decision), margin = 1)[,"1"]
prop_high
## Cao Rất cao Thấp Trung bình
## 0.2298730 0.2231015 0.2482254 0.2484010
Đoạn code này dùng để tính toán Tỷ lệ Quyết định Mua (decision = 1) cho từng Nhóm Giá (price_group).
Mục đích cuối cùng là tạo ra một danh sách (vector) chỉ chứa tỷ lệ phần trăm khách hàng “Mua” (decision = 1) bên trong mỗi nhóm giá. Phân tích này là bước đầu tiên để chứng minh liệu giá nhà càng cao (hoặc càng thấp) thì khách hàng có xu hướng mua hay không.
Trong đó:
prop_high <- …: Toán tử gán, lưu kết quả tỷ lệ mua cho mỗi nhóm vào đối tượn prop_high.
table(df\(price_group, df\)decision): Hàm chính tạo ra Bảng Tần số Chéo giữa price_group (Hàng) và decision (Cột).
prop.table(…, margin = 1): Hàm này chuyển đổi bảng tần số thành bảng Tỷ lệ.
margin = 1: Tham số quan trọng, đảm bảo rằng tổng của mỗi hàng (price_group) sẽ là \(1\) (hoặc \(100\%\)).
[…] (Trích xuất dữ liệu): [, “1”] là cú pháp R cơ bản để trích xuất cột thứ hai (cột có nhãn “1”) từ bảng tỷ lệ. Cột “1” này chứa tỷ lệ khách hàng Quyết định Mua (decision = 1) trong từng price_group.
Kết quả kĩ thuật
Tỷ lệ mua lần lượt từng nhóm:
Cao: 22,99%
Rất cao: 22,31%
Thấp: 24,82%
Trung bình: 24,84%
Nhóm giá thấp và trung bình có tỷ lệ mua cao nhất, gần 25%. Ngược lại, nhóm giá cao và rất cao tỷ lệ mua chỉ khoảng 22-23%.
Chênh lệch giữa các nhóm không lớn nhưng rõ ràng tỷ lệ mua ở nhóm sản phẩm rẻ hơn có xu hướng cao hơn. Điều này cho thấy giá vẫn là yếu tố ảnh hưởng đến hành vi mua, dù không hoàn toàn chi phối.
Từ góc độ kỹ thuật, khi xây dựng mô hình dự báo hoặc phân loại xác suất mua, biến phân nhóm giá sẽ đóng vai trò bổ sung, giúp cải thiện khả năng dự đoán hành vi khách hàng.
Ý nghĩa thống kê
Tỷ lệ chuyển đổi cao hơn ở nhóm bất động sản giá thấp và trung bình minh chứng rằng khả năng tiêu thụ sản phẩm đại chúng mạnh hơn nhiều so với các phân khúc cao cấp.
Đây là dấu hiệu thị trường bất động sản phổ thông vẫn là động lực chính của ngành; với sản phẩm càng đắt tiền, khoảng cách thành công càng giảm. Khách hàng mua chủ yếu hướng tới sản phẩm vừa túi tiền.
Doanh nghiệp bất động sản muốn tăng trưởng doanh số cần phát triển mạnh hàng hóa ở phân khúc giá phổ thông, tối ưu chi phí - cấu trúc sản phẩm, đồng thời xây dựng các chính sách tín dụng hoặc ưu đãi phù hợp cho nhóm này. Nhóm giá cao – rất cao sẽ cần chiến lược riêng, nhắm tới phân khúc khách đặc biệt, không phải là trụ cột tăng trưởng đại trà.
Chính sách nhà nước/ngân hàng nên ưu tiên hỗ trợ phân khúc giá phù hợp đại đa số, đồng thời kiểm soát rủi ro phân khúc giá cao.
prop.table(table(df$property_type, df$decision), margin = 1)[, "1"] %>% sort(decreasing = TRUE)
## Farmhouse Apartment Studio Villa
## 0.2323826 0.2323492 0.2310652 0.2298558
## Townhouse Independent House
## 0.2284174 0.2279654
Đoạn code này dùng để phân tích và Xếp hạng (Rank) Tỷ lệ Quyết định Mua (decision = 1) theo Loại hình Bất động sản (property_type).
Mục đích chính là để xác định: Loại hình Bất động sản nào có tỷ lệ được mua cao nhất và sắp xếp chúng theo thứ tự từ cao nhất đến thấp nhất.
Trong đó:
table(df\(property_type, df\)decision): Hàm này tạo ra Bảng Tần số chéo giữa property_type (Hàng) và decision (Cột).
prop.table(…, margin = 1): Hàm này chuyển đổi bảng tần số thành Bảng Tỷ lệ
margin = 1: Tham số quan trọng, đảm bảo rằng tổng của mỗi hàng (property_type) sẽ là \(1\) (hoặc \(100\%\)).
[, “1”] (Trích xuất dữ liệu): Cú pháp R cơ bản để trích xuất cột thứ hai (cột có nhãn “1”) từ bảng tỷ lệ. Cột “1” này chứa tỷ lệ khách hàng Quyết định Mua (decision = 1) trong từng Loại hình bất động sản.
%>% sort(decreasing = TRUE): Sử dụng cú pháp ống dẫn (%>%) để truyền kết quả (vector tỷ lệ) vào hàm sort().
sort(…): Hàm này thực hiện sắp xếp các tỷ lệ đó.
decreasing = TRUE: Sắp xếp theo thứ tự giảm dần (từ tỷ lệ cao nhất đến thấp nhất), cho thấy loại hình nào có tỉ lệ được mua cao nhất.
Kết quả kĩ thuật
Tỷ lệ mua cao nhất thuộc về: Farmhouse (23,24%), kế tiếp là Apartment (23,23%), Studio (23,11%), Villa (22,99%), Townhouse (22,84%), và thấp nhất là Independent House (22,80%).
Sự chênh lệch giữa các loại hình bất động sản về tỷ lệ mua không lớn, tất cả đều quanh mức 22,8–23,2%. Tuy nhiên, Farmhouse và Apartment vẫn nhỉnh hơn các loại hình khác một chút – cao hơn Independent House khoảng 0,44%.
Dữ liệu cho thấy các loại hình căn hộ (Apartment, Studio), Farmhouse… đang dẫn đầu về tỷ lệ hấp thụ trên thị trường, còn dòng nhà biệt lập (Independent House, Townhouse) hay biệt thự (Villa) kém cạnh tranh hơn về tỷ lệ mua dù có thể giá trị giao dịch lớn.
Từ góc độ phân tích kỹ thuật, mặc dù khác biệt không quá sâu, các biến loại hình chắc chắn nên được sử dụng khi xây dựng mô hình dự báo hành vi mua để tăng độ chính xác dự báo.
Ý nghĩa thống kê
Các sản phẩm bất động sản đại chúng – căn hộ (Apartment, Studio) và Farmhouse – đang hấp dẫn thị trường hơn, được người mua ưu tiên lựa chọn nhiều hơn biệt thự, nhà phố hoặc biệt lập.
Yếu tố về lối sống, giá vừa phải, tiện ích - dịch vụ đi kèm, phù hợp khách hàng trẻ hoặc đầu tư trung bình có thể giải thích cho sức hút của Apartment và Farmhouse. Điều này cổ vũ các doanh nghiệp nên phát triển mạnh hơn các loại hình này để đáp ứng sát nhu cầu thị trường thực.
Villa, Townhouse, Independent House – thường có giá trị/giá cao hoặc đáp ứng thị hiếu đặc thù – nên được thiết kế sản phẩm, chính sách tài chính, ưu đãi hấp dẫn hơn để tăng sức cạnh tranh, hoặc tập trung khai thác nhóm khách hàng cao cấp, đặc biệt.
Chính sách phát triển nhà ở xã hội, căn hộ trung bình, sản phẩm tương tác với thiên nhiên (như Farmhouse) đang bắt nhịp xu hướng thị trường và nên tiếp tục mở rộng.
df %>%
group_by(decision) %>%
summarise(
median_lti = median(loan_to_income, na.rm = TRUE),
IQR_lti = IQR(loan_to_income, na.rm = TRUE)
)
## # A tibble: 2 × 3
## decision median_lti IQR_lti
## <fct> <dbl> <dbl>
## 1 0 16.7 23.4
## 2 1 13.9 15.5
Đoạn code này dùng để thực hiện Thống kê Mô tả Bền vững.
Mục đích chính là để so sánh các chỉ số thống kê bền vững của Tỷ lệ Vay trên Thu nhập (loan_to_income) giữa hai nhóm quyết định mua nhà (decision = 0 và decision = 1).
Trong đó:
df %>% group_by(decision): Sử dụng cú pháp Tidyverse. Phân nhóm dữ liệu df thành hai nhóm dựa trên biến decision (0: Không mua và 1: Mua).
summarise(…): Hàm của dplyr. Thực hiện các phép tính tóm tắt cho từng nhóm đã phân loại.
median_lti = median(loan_to_income, na.rm = TRUE): Tính Trung vị (Median) của tỷ lệ loan_to_income cho mỗi nhóm.
na.rm = TRUE: Tham số quan trọng, hướng dẫn R loại bỏ các giá trị khuyết (NA) trước khi tính Trung vị.
IQR_lti = IQR(loan_to_income, na.rm = TRUE): Tính Khoảng Tứ phân vị (Interquartile Range - IQR) của tỷ lệ loan_to_income cho mỗi nhóm.
na.rm = TRUE: Tham số quan trọng, loại bỏ các giá trị khuyết trước khi tính IQR.
Kết quả kĩ thuật
Nhóm không mua:
Trung vị LTI: 16,74
Độ phân tán (IQR): 23,39
Nhóm mua:
Trung vị LTI: 13,95
Độ phân tán (IQR): 15,46
Trung vị LTI của nhóm mua thấp hơn nhóm không mua khoảng 2,8 điểm, và độ phân tán thấp hơn đáng kể (15,46 so với 23,39). Điều này cho thấy nhóm khách thực sự mua nhà thường kiểm soát tỷ lệ gánh nặng vay an toàn và ổn định hơn so với nhóm không mua.
Biến LTI có khả năng phân nhóm rõ nét: giá trị thấp và ổn định ở nhóm mua, cao và biến động mạnh ở nhóm còn lại.
Ý nghĩa thống kê
Tỷ số vay trên thu nhập (LTI) là chỉ báo rủi ro tín dụng quan trọng trong tài chính cá nhân và quản trị khoản vay: tỷ số càng cao, khách hàng càng chịu áp lực trả nợ lớn, càng khó được duyệt vay hoặc thực hiện mua nhà.
Nhóm khách hàng mua nhà thực tế có LTI trung vị chỉ 13,95 – thấp vượt trội so với nhóm không mua (16,74), đồng nghĩa họ cân đối tài chính tốt và chỉ lựa chọn khoản vay phù hợp với năng lực chi trả.
Độ phân tán LTI nhóm mua cũng rất thấp (15,46), cho thấy hồ sơ tài chính an toàn, hạn chế rủi ro phát sinh trong thanh toán.
Đây là thông tin quyết định cho chính sách tín dụng, phát triển sản phẩm của doanh nghiệp: tập trung vào đối tượng có LTI thấp sẽ giảm thiểu rủi ro nợ xấu, nâng cao tỷ lệ duyệt vay và hạn chế phát sinh sai phạm tài chính.
Các biện pháp tài chính, chính sách hỗ trợ thêm cho nhóm khách LTI cao nên được cân nhắc kỹ, ưu tiên thiết kế gói vay, kéo dài hạn/nâng ưu đãi để đưa LTI về ngưỡng an toàn (dưới 15–20).
p99 <- quantile(df$price, 0.99)
sum(df$price > p99)
## [1] 2000
head(df %>% filter(price > p99) %>% select(property_id, country, price, price_group), 10)
## property_id country price price_group
## 1 187 Singapore 3833537 Rất cao
## 2 204 Singapore 3903058 Rất cao
## 3 231 Singapore 3907140 Rất cao
## 4 398 Singapore 3898171 Rất cao
## 5 401 Singapore 3711518 Rất cao
## 6 499 Singapore 3881474 Rất cao
## 7 545 Singapore 4031417 Rất cao
## 8 812 Singapore 3939570 Rất cao
## 9 817 Singapore 4096131 Rất cao
## 10 837 Singapore 3705536 Rất cao
Đoạn code này dùng để phân tích Phần đuôi Phân phối hay còn gọi là Giá trị Ngoại lai (Outliers) của biến Giá nhà (price).Mục đích chính là để xác định ngưỡng giá và tóm tắt đặc điểm của \(1\%\) số bất động sản đắt tiền nhất trong tập dữ liệu.
Trong đó:
p99 <- quantile(df$price, 0.99): Hàm quantile() tính toán giá trị tại Phân vị 99% (\(99^{th}\) Percentile) của cột df$price. Giá trị này (p99) trở thành ngưỡng giá để xác định các bất động sản đắt tiền nhất.
sum(df$price > p99): Hàm sum() tính tổng số lượng bất động sản có giá lớn hơn ngưỡng p99 vừa tìm được. Kết quả là số lượng quan sát chính xác trong top \(1\%\).
df %>% filter(price > p99): Hàm filter() của dplyr. Lọc DataFrame df để chỉ giữ lại các hàng (bất động sản) có giá lớn hơn ngưỡng p99.
%>% select(property_id, country, price, price_group): Hàm select() của dplyr. Sau khi lọc, chỉ chọn ra các cột thông tin chính (ID, Quốc gia, Giá, Phân khúc) của các bất động sản ngoại lai.
head(…, 10): Hàm head(). Hiển thị 10 hàng đầu tiên của tập hợp các bất động sản ngoại lai đã được chọn ở bước trên.
Kết quả kĩ thuật
Tất cả các bất động sản liệt kê đều thuộc nhóm “Rất cao” và là sản phẩm thuộc top 1% về giá của toàn bộ dữ liệu, với giá từ khoảng 3.705.536 đến 4.096.131 USD.
Các mã property_id đều thuộc quốc gia Singapore, cho thấy sự tập trung của phân khúc giá siêu cao vào một thị trường cụ thể.
Việc sử dụng điều kiện price > p99 giúp nhận diện mẫu ngoại lai hay nhóm đặc thù, hỗ trợ loại trừ các trường hợp này khi xây dựng mô hình hồi quy/tổng hợp trung vị để tránh bị kéo lệch kết quả số liệu chung.
Ý nghĩa kinh tế
Các sản phẩm bất động sản có giá trên 3,7 triệu USD đều tập trung tại Singapore và thuộc phân khúc “Rất cao” – đây là nhóm housing luxury, hướng tới đối tượng khách hàng siêu giàu, đầu tư quốc tế hoặc tổ chức.
Đặc trưng của nhóm này là tỷ trọng rất nhỏ (chỉ 1% thị trường), nhưng tổng giá trị giao dịch lại đóng vai trò lớn, thậm chí có thể chi phối các chỉ số tài chính tổng hợp của thị trường nếu không phân tích riêng.
Từ góc độ thị trường: chính sách phát triển, tiếp cận tài chính, quảng bá hoặc phát triển sản phẩm luxury cần cá nhân hóa theo tập khách hàng đặc biệt này. Thị trường đại trà không chịu ảnh hưởng rõ rệt từ nhóm này.
Dữ liệu này cũng là lời nhắc cho các doanh nghiệp: muốn phát triển bền vững cần chia tách chiến lược luxury và phổ thông, tránh lấy chuẩn luxury để định hướng giá trung bình gây sai lệch.
tab_garage <- table(df$garage_label, df$decision)
chisq.test(tab_garage)
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tab_garage
## X-squared = 0.3344, df = 1, p-value = 0.5631
tab_garden <- table(df$garden_label, df$decision)
chisq.test(tab_garden)
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tab_garden
## X-squared = 0.20286, df = 1, p-value = 0.6524
Đoạn code này dùng để thực hiện hai Kiểm định Chi-bình phương Độc lập (Chi-squared Tests for Independence).
Mục đích chính là để kiểm tra xem hai đặc điểm cơ bản của bất động sản: Tình trạng garage và tình trạng vườn (garage_label và garden_label) có mối liên hệ có ý nghĩa thống kê hay không với Quyết định mua nhà (decision).
Trong đó:
table(df\(garage_label, df\)decision) và table(df\(garden_label, df\)decision): Hàm table() tạo ra hai Bảng Tần số Chéo riêng biệt.
Các bảng này đếm số lượng quan sát (tần số) cho mọi sự kết hợp giữa (Có/Không Garage/Vườn) và (Không mua/Mua). Các bảng này là đầu vào bắt buộc cho kiểm định Chi-bình phương.
tab_garage <- …và tab_garden <- …: Toán tử gán, lưu hai bảng tần số này vào hai đối tượng riêng biệt.
chisq.test(tab_garage) và chisq.test(tab_garden):Đây là hàm chính thực hiện Kiểm định Chi-bình phương cho từng bảng.
Tham số đầu vào: Nhận bảng tần số chéo (ví dụ: tab_garage) để so sánh tần số quan sát được với tần số kỳ vọng.
Kết quả kĩ thuật
1.Kiểm định với garage:
chi-squared =0,3344, df = 1, p-value = 0,5631.
2.Kiểm định với vườn:
chi-squared =0,20286, df = 1, p-value = 0,6524.
Cả hai p-value đều lớn hơn 0,05 nên không thể bác bỏ giả thuyết gốc (giả thuyết độc lập): sở hữu gara và vườn không có mối liên hệ ý nghĩa thống kê với quyết định mua nhà.
Xét về kỹ thuật, kết quả này cho thấy hai biến gara và vườn không phải là yếu tố phân loại mạnh khi xây dựng mô hình dự báo xác suất mua bất động sản. Nên ưu tiên các biến có ý nghĩa tách biệt mạnh hơn khi phân tích (ví dụ tài chính, hài lòng…).
Ý nghĩa thống kê
Việc một bất động sản có gara hoặc vườn không làm tăng tỷ lệ chuyển đổi sang giao dịch mua bán, tức khách hàng quyết định mua nhà dựa vào tiêu chí khác quan trọng hơn, như giá, diện tích, vị trí, chính sách tài chính hoặc giá trị hài lòng tổng thể.
Doanh nghiệp bất động sản không nên lập kế hoạch sản phẩm, marketing hoặc tăng giá bán chỉ kể trên tiêu chí sở hữu gara/vườn, mà phải nhìn nhận tổng thể các giá trị thị trường thực sự có ý nghĩa đối với khách hàng.
Với nhà đầu tư phát triển dự án, các đặc tính như gara và vườn phù hợp phân khúc hoặc mục tiêu sống nhất định, nhưng sẽ không phải yếu tố quyết định tỷ lệ bán hàng trên diện rộng.
m1 <- glm(decision_num ~ price, data = df, family = binomial)
m2 <- glm(decision_num ~ salary, data = df, family = binomial)
m3 <- glm(decision_num ~ satisfaction_score, data = df, family = binomial)
broom::tidy(m1, exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 2 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 0.344 0.00941 -113. 0 0.338 0.351
## 2 price 1.00 0.00000000663 -17.7 4.69e-70 1.00 1.00
broom::tidy(m2, exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 2 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 0.207 0.0108 -146. 0 0.202 0.211
## 2 salary 1.00 0.000000189 40.8 0 1.00 1.00
broom::tidy(m3, exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 2 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 0.00152 0.0306 -212. 0 0.00143 0.00161
## 2 satisfaction_score 2.16 0.00385 200. 0 2.14 2.18
Đoạn code này dùng để thực hiện Phân tích Hồi quy Logistic Đơn biến (Univariate Logistic Regression Analysis).
Mục đích chính là để sàng lọc và đánh giá độc lập ảnh hưởng của từng biến định lượng chính (price, salary, satisfaction_score) lên Tỷ lệ cược (Odds) của biến mục tiêu nhị phân (decision).
glm(…): Hàm chính, xây dựng mô hình Hồi quy Logistic.
decision_num ~ price(hoặc salary, satisfaction_score): Tham số formula thiết lập mô hình đơn biến: decision_num (biến phụ thuộc) được dự đoán bởi một biến độc lập duy nhất (ví dụ: price).
data = df: Chỉ định sử dụng tập dữ liệu df.
family = binomial: Tham số bắt buộc, xác định kiểu mô hình là Logistic Regression (phù hợp với biến phụ thuộc nhị phân 0/1).
broom::tidy(m1, …): Hàm của gói broom, dùng để chuyển đổi kết quả mô hình m1 sang định dạng bảng dữ liệu gọn gàng.
exponentiate = TRUE: Tham số rất quan trọng. Nó hướng dẫn R chuyển đổi các hệ số hồi quy (coefficients) sang định dạng Tỷ số chênh (Odds Ratio - OR).
Ý nghĩa: Thay vì đơn vị Log-odds khó hiểu, kết quả sẽ cho biết: “Khi biến X tăng 1 đơn vị, Odds của quyết định Mua thay đổi bao nhiêu lần (OR).”
conf.int = TRUE: Tham số này yêu cầu hàm tidy() tính toán và hiển thị Khoảng tin cậy 95% cho Tỷ số chênh (OR), giúp đánh giá độ tin cậy của ước lượng.
Kết quả kĩ thuật
Mô hình với salary:
OR (odds ratio) = 1,000077; khoảng tin cậy 95%: [1,000073; 1,000081], p-value = 0.
Chiều hướng thuận: mỗi đơn vị tăng thêm của salary (USD) làm tăng nhẹ xác suất mua nhà, hiệu ứng nhỏ về tuyệt đối do salary lớn so với 1.
Mô hình với price:
OR = 0,9999999; khoảng tin cậy 95%: [0,9999999; 0,9999999], p-value ≈ 0.
Nghĩa là mỗi USD tăng thêm sẽ làm giảm nhẹ odds mua nhà, hiệu ứng âm nhưng rất nhỏ về mặt tuyệt đối (vì giá có quy mô rất lớn).
Mô hình với satisfaction_score:
OR = 2,16; khoảng tin cậy 95%: [2,14; 2,18], p-value = 0.
Mỗi điểm tăng của satisfaction_score làm odds mua nhà tăng hơn gấp đôi — hiệu ứng cực lớn, CI hẹp, ý nghĩa thống kê tuyệt đối.
Mọi biến đều có p-value bằng 0 cho thấy ý nghĩa thống kê rất mạnh. Tuy nhiên, so sánh độ lớn OR thì satisfaction_score mạnh áp đảo so với price hay salary.
Ý nghĩa thống kê
Điểm hài lòng (satisfaction_score) là yếu tố quyết định mạnh nhất: cứ tăng thêm 1 điểm hài lòng, khả năng mua gần như tăng gấp đôi. Doanh nghiệp nên đầu tư vào toàn bộ trải nghiệm, dịch vụ, chăm sóc khách hàng để tối đa hóa tỷ lệ giao dịch thành công.
Lương (salary) cũng là biến giải thích đúng hướng: lương càng cao, khả năng mua càng lớn, tuy nhiên tác động tăng odds rất nhỏ ở từng đơn vị do giá trị tuyệt đối của lương cao (mức nhập vừa/cao mới tạo khác biệt thực sự). Doanh nghiệp nên nhắm đến phân khúc có thu nhập ổn định – cao để tối ưu doanh số.
Giá bất động sản (price): tác động ngược lên khả năng mua. Giá càng cao thì odds mua càng giảm (OR < 1), điều này trùng khớp với các phân tích trước. Để kích cầu các sản phẩm giá cao, cần kết hợp ưu đãi, tài chính linh hoạt, gia tăng giá trị khác ngoài giá.
Về chính sách: kết quả cũng hàm ý rằng các chương trình hỗ trợ tăng hài lòng, giải pháp tài chính cá nhân phù hợp thu nhập, và chiến lược giá hợp lý sẽ tối ưu hoá cơ hội chuyển đổi từ quan tâm sang giao dịch thực tế.
ks.test(df$price[df$decision=="0"], df$price[df$decision=="1"])
## Warning in ks.test.default(df$price[df$decision == "0"], df$price[df$decision
## == : p-value will be approximate in the presence of ties
##
## Asymptotic two-sample Kolmogorov-Smirnov test
##
## data: df$price[df$decision == "0"] and df$price[df$decision == "1"]
## D = 0.036893, p-value < 2.2e-16
## alternative hypothesis: two-sided
Đoạn code này dùng để thực hiện Kiểm định Kolmogorov-Smirnov Hai Mẫu (Two-Sample Kolmogorov-Smirnov Test - K-S Test).
Mục đích chính là để kiểm tra xem phân phối của biến Giá nhà (price) giữa hai nhóm khách hàng (decision = 0 và decision = 1) có khác biệt có ý nghĩa thống kê hay không.
Trong đó:
ks.test(…): Đây là hàm chính thực hiện Kiểm định K-S.
df\(price[df\)decision==“0”]: Tham số đầu tiên (mẫu X). Đây là vector giá nhà của nhóm 0 (Không mua). Cú pháp này sử dụng lọc điều kiện ([…]) của R cơ bản để chọn ra các giá trị price khi điều kiện decision == “0” là đúng.
df\(price[df\)decision==“1”]: Tham số thứ hai (mẫu Y). Đây là vector giá nhà của nhóm 1 (Mua).
Tham số Mặc định: Hàm ks.test() mặc định thực hiện kiểm định hai phía (alternative = “two.sided”) và giả định rằng hai mẫu độc lập.
Kết quả kĩ thuật
Giá trị thống kê KS D=0,0369, p-value < 2.2e-16.
Giá trị p-value siêu nhỏ cho phép bác bỏ giả thuyết gốc: hai phân phối giá giữa nhóm mua và không mua không giống nhau. Sự khác biệt tuy không lớn về mặt D (3,7%), nhưng rất rõ ràng về mặt ý nghĩa thống kê nhờ cỡ mẫu lớn.
Điều này củng cố kết quả từ các kiểm định khác (t-test, wilcoxon, chi-square): biến giá bất động sản thực sự khác nhau về mặt phân phối giữa hai nhóm có/không phát sinh giao dịch.
Ý nghĩa thống kê
Thống kê này khẳng định nhóm bất động sản được mua thành công và nhóm còn lại có phân khúc giá trị hoàn toàn khác biệt – không thể dùng chung một phân phối để đánh giá toàn thị trường.
Thực tiễn cho thấy khách hàng thường mua các sản phẩm phù hợp túi tiền và hành động mua thực sự diễn ra ở vùng giá thấp - trung, còn các sản phẩm giá cao khả năng giao dịch thành công rất thấp.
Chính sách giá, phát triển sản phẩm, truyền thông, nghiên cứu hành vi thị trường… cần hướng mạnh tới phân tích vi mô theo từng phân khúc, không thể “trộn bình quân” dữ liệu giá để đánh giá toàn thị trường.
Đây là nền tảng để doanh nghiệp, ngân hàng, cơ quan quản lý tách lớp phân tích, xác định rõ nhu cầu và năng lực chi trả từng đối tượng, thiết kế các gói hỗ trợ, ưu đãi hoặc chiến lược riêng cho từng phân khúc giá.
model_auc <- glm(decision_num ~ price + salary, data = df, family = binomial)
probs <- predict(model_auc, type = "response")
roc_obj <- pROC::roc(df$decision_num, probs)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
pROC::auc(roc_obj)
## Area under the curve: 0.5762
Đoạn code này dùng để Đánh giá Hiệu suất (Performance Evaluation) của một mô hình dự đoán.
Mục đích chính là để tính toán Diện tích dưới Đường cong ROC (Area Under the Curve - AUC) cho một Mô hình Hồi quy Logistic Đơn giản (glm) sử dụng hai biến dự báo (price và salary).
Chỉ số AUC là thước đo chuẩn mực nhất để đánh giá khả năng phân biệt (discriminative power) của mô hình: khả năng mô hình phân loại đúng giữa các trường hợp “Mua” (decision = 1) và “Không mua” (decision = 0).
Trong đó:
model_auc <- glm(…): Đây là toán tử gán, lưu mô hình Hồi quy Logistic vào đối tượng model_auc.
glm(decision_num ~ price + salary, data = df, family = binomial): Hàm glm() xây dựng mô hình. Tham số formula (decision_num ~ price + salary) thiết lập mô hình dự đoán biến nhị phân bằng cách sử dụng các biến price và salary. Tham số family = binomial bắt buộc phải có, chỉ định kiểu mô hình là Logistic Regression.
probs <- predict(model_auc, type = “response”): Hàm predict() tính toán xác suất dự đoán (probs) của sự kiện xảy ra (\(\mathbf{P(Y=1)}\)) từ mô hình model_auc. Tham số type = “response” yêu cầu đầu ra là xác suất (từ 0 đến 1).
**roc_obj <- pROC::roc(df\(decision_num, probs)**: Hàm roc() của gói pROC. Mục đích là tạo ra đối tượng Đường cong ROC, so sánh kết quả thực tế (df\)decision_num) với xác suất dự đoán (probs).
pROC::auc(roc_obj): Hàm auc() của gói pROC. Đây là bước cuối cùng, tính toán Diện tích dưới Đường cong ROC.
Kết quả kĩ thuật
AUC = 0,5762 .Giá trị AUC thể hiện độ phân biệt giữa hai nhóm (mua – không mua) của mô hình; với AUC=0,5 là mức ngẫu nhiên, còn 1 là dự báo hoàn hảo.
Mô hình chỉ dùng hai biến price và salary đạt AUC = 0,5762, tức chỉ nhỉnh hơn dự báo ngẫu nhiên một chút. Điều đó cho thấy sức mạnh tách nhóm khi dùng riêng các biến này là khá thấp.
Muốn gia tăng độ chính xác của mô hình, cần bổ sung thêm các biến dự báo mạnh khác (như satisfaction_score, các đặc điểm tài chính chuyên sâu, trải nghiệm, yếu tố địa lý…), hoặc kết hợp nhiều biến dạng phức hợp.
Ý nghĩa thống kê
Kết quả này đồng nghĩa là: chỉ dựa vào thông tin về* giá bất động sản và mức thu nhập, rất khó xác định khách hàng nào sẽ quyết định mua nhà, vì khả năng đúng chỉ trên ngưỡng ngẫu nhiên một chút.
Điều này phản ánh thực tế thị trường hiện đại: khách hàng không chỉ quyết định dựa vào giá và thu nhập mà cần xem xét tới nhiều khía cạnh khác như mức độ hài lòng, chất lượng dịch vụ, vị trí, tiện ích, gói hỗ trợ tài chính, hay xu hướng,văn hóa cá nhân.
Các doanh nghiệp, nhà đầu tư và ngân hàng khi dự báo khả năng chốt khách hàng không nên chỉ sử dụng các chỉ số cứng về tài chính hoặc giá trị tài sản, mà phải tích hợp dữ liệu phản ánh trải nghiệm, hành vi và nhu cầu thực của khách.
Để tạo ra lợi thế cạnh tranh và tăng tỷ lệ thành công khi quản lý khách hàng tiềm năng, cần tập trung xây dựng các mô hình toàn diện hơn thay vì chỉ tối ưu hóa giá và thu nhập.
p1 <- ggplot(df, aes(x=factor(decision), fill=factor(decision))) +
geom_bar(color="white") +
geom_text(stat="count", aes(label=after_stat(count)), vjust=-0.5) +
scale_fill_manual(values=c("steelblue","tomato")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
labs(title="(P1) Phân bố khách hàng theo quyết định mua", x="Quyết định", y="Số lượng khách hàng") +
theme_minimal(base_family = "Arial") +
theme(plot.title = element_text(size = 12))+
theme(legend.position="none")
p1
Đoạn code này dùng để tạo ra một Biểu đồ Cột Tần số (Frequency Bar Chart).
Mục đích chính là để trực quan hóa sự phân phối của Biến mục tiêu nhị phân (decision) bằng cách đếm và hiển thị số lượng khách hàng thuộc nhóm Không mua (0) và nhóm Mua (1).
Trong đó:
p1 <- ggplot(…) + …: Toán tử gán, lưu biểu đồ đã hoàn thành vào đối tượng p1.
ggplot(df, aes(x=factor(decision), fill=factor(decision))):Khởi tạo biểu đồ, sử dụng df làm nguồn dữ liệu.
aes(…): Thiết lập ánh xạ thẩm mỹ. Ánh xạ biến decision lên trục X và cả màu tô (fill).
geom_bar(color=“white”): Lớp hình học chính, vẽ các cột. Nó sử dụng stat=“count” mặc định để đếm số lượng quan sát cho mỗi giá trị của decision.
geom_text(stat=“count”, aes(label=after_stat(count)), vjust=-0.5): Lớp nhãn số (Text Label). Hiển thị số lượng đếm ngay trên đầu mỗi cột.
after_stat(count): Cú pháp hiện đại để lấy giá trị số lượng đếm đã được tính.
vjust=-0.5: Dịch chuyển nhãn lên trên một chút để không đè lên cột.
scale_fill_manual(values=c(“steelblue”,“tomato”)): Tùy chỉnh màu sắc tô (fill) của các cột.
scale_y_continuous(expand = expansion(mult = c(0, 0.1))): Tùy chỉnh trục Y.
expand = expansion(mult = c(0, 0.1)): Tham số quan trọng, thêm một khoảng đệm \(10\%\) ở phía trên (trục Y) để nhãn số (geom_text) không bị cắt bởi giới hạn biểu đồ.
labs(…): Đặt tiêu đề cho biểu đồ (title), trục X, và trục Y.
theme_minimal(): Áp dụng phong cách tối giản.
theme(plot.title = element_text(size = 10)): Tùy chỉnh cỡ chữ của tiêu đề chính của biểu đồ.
theme(legend.position=“none”): Ẩn chú giải (legend) vì màu sắc đã được chỉ định rõ ràng trên trục X.
Kết quả kĩ thuật
Biểu đồ gồm hai cột: cột bên trái tương ứng quyết định “0” (không mua) và bên phải là “1” (mua).
Số lượng khách hàng không mua lên tới 153.932, gấp hơn 3 lần số khách hàng mua là 46.068.
Cột không mua có giá trị vượt trội, chiếm phần lớn trục tung, trong khi số lượng khách mua chỉ tạo thành một phần nhỏ của biểu đồ.
Hai thanh màu khác nhau hỗ trợ trực quan: xanh dương cho nhóm không mua, đỏ cho nhóm mua. Việc chèn số thực lên đầu từng cột tăng tính minh bạch, tránh ước lượng cảm tính.
Nhóm “0” thể hiện sự vượt trội áp đảo về số lượng, cho thấy lớp dữ liệu rất mất cân bằng, và đây là điểm quan trọng khi kiểm tra, xây dựng mô hình học máy hoặc các thao tác phân tích sâu hơn.
Ý nghĩa thống kê
Mặc dù tổng thể thị trường thu hút được số lượng quan tâm lớn (200.000 khách hàng), nhưng phần lớn trong số này đã dừng lại ở bước khảo sát hoặc bày tỏ nhu cầu mà không thực hiện mua.
Điều này báo động về hiệu quả chuyển đổi khách hàng khi mà chỉ có khoảng 1/4 khách hàng tiềm năng ra quyết định mua thực sự, ở đây thể hiện bằng con số tuyệt đối trên thanh biểu đồ.
Với doanh nghiệp, con số này là “thước đo lọc đầu phễu” – tức cho thấy dù nỗ lực marketing, quảng cáo rất lớn thu hút khách hàng quan tâm, song tỉ lệ thành công thực sự lại thấp, cần đầu tư nghiên cứu sâu các nguyên nhân khiến khách hàng rớt khỏi quy trình mua hàng: giá, chính sách, quy trình phê duyệt, sản phẩm chưa phù hợp, hoặc trải nghiệm dịch vụ chưa tốt.
Về lâu dài, việc cải thiện tỷ lệ chuyển đổi sẽ đóng vai trò then chốt trong tăng trưởng doanh thu, tối ưu chi phí marketing cũng như phát triển bền vững hơn cho mọi mô hình kinh doanh/tư vấn/bán lẻ bất động sản.
Việc minh bạch số liệu như trên sẽ giúp nhà quản trị đánh giá đúng hiệu quả hoạt động, lượng hóa chính xác tỷ trọng khách hàng trong từng nhóm, từ đó đề xuất giải pháp hành động sát với thực tế - thay vì chỉ dựa vào cảm nhận hay tỷ lệ phần trăm ước đoán.
p2 <- ggplot(df, aes(x=price_group, fill=factor(decision))) +
geom_bar(position="fill") +
scale_y_continuous(labels=percent) +
geom_text(stat="count", aes(label=after_stat(count)), position=position_fill(vjust=0.5), size=3) +
scale_fill_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = c("0" = "skyblue", "1" = "tomato")
) +
labs(title="(P2) Tỷ lệ mua theo phân khúc giá", x="Phân khúc", y="Tỷ lệ (%)") +
theme_minimal(base_family = "Arial") + theme(axis.text.x=element_text(angle=45,hjust=1))
p2
Đoạn code này dùng để tạo ra một Biểu đồ Thanh Chồng 100% (100% Stacked Bar Chart). Mục đích chính là để trực quan hóa và so sánh tỷ lệ Quyết định Mua (decision = 1) và Không mua (decision = 0) bên trong mỗi nhóm Giá (price_group). Biểu đồ này giúp xác định ngay lập tức nhóm giá nào có tỷ lệ mua cao nhất.
Trong đó:
p2 <- ggplot(…): Toán tử gán, lưu biểu đồ hoàn chỉnh vào đối tượng p2.
aes(x=price_group, fill=factor(decision)): Đây là ánh xạ thẩm mỹ chính. Ánh xạ biến price_group lên trục X (đại diện cho các cột) và biến decision lên màu tô (fill) của các phân đoạn cột.
geom_bar(position=“fill”): Đây là lớp hình học chính, vẽ các cột tần số. Tham số position=“fill” là quan trọng nhất, nó buộc các cột phải được chồng lên nhau và có tổng chiều cao bằng 1 (hoặc 100%), chuyển từ tần số sang tỷ lệ.
scale_y_continuous(labels=percent): Tùy chỉnh trục Y. Hàm percent (thuộc gói scales) được chỉ định để định dạng nhãn trục Y dưới dạng tỷ lệ phần trăm (ví dụ: \(0.25 \rightarrow 25\%\)).
geom_text(stat=“count”, aes(label=after_stat(count)), …): Lớp nhãn số (Text Label). Nó hiển thị số lượng đếm (Tần số - count) của mỗi phân đoạn cột ngay trên biểu đồ. Tham số position=position_fill(vjust=0.5) căn chỉnh nhãn số vào giữa mỗi phân đoạn.
scale_fill_manual(…): Tùy chỉnh thủ công màu sắc và nhãn của chú giải (legend).
name = “Quyết định”: Đặt tiêu đề cho chú giải màu tô (fill).
labels = c(“0” = “Không mua”, “1” = “Mua”): Tùy chỉnh nhãn. Nó chuyển các giá trị gốc “0” và “1” thành các nhãn dễ đọc hơn (“Không mua” và “Mua”) trong chú giải.
values = c(“0” = “skyblue”, “1” = “tomato”): Tùy chỉnh bảng màu, gán màu xanh (skyblue) cho Không mua và màu đỏ (tomato) cho Mua.
labs(…): Đặt nhãn cho biểu đồ. Tham số title đặt tiêu đề chính, x đặt nhãn trục hoành (Phân khúc), và y đặt nhãn trục tung (Tỷ lệ (%)).
theme_minimal() + theme(…): Áp dụng phong cách tối giản. Lệnh axis.text.x=element_text(angle=45,hjust=1) được sử dụng để xoay nhãn trục X một góc 45 độ, tránh tình trạng các nhãn dài bị chồng chéo lên nhau.
Kết quả kĩ thuật
Biểu đồ thể hiện số lượng khách “Mua” (đỏ) và “Không mua” (xanh) trong từng phân khúc giá: Cao, Rất cao, Thấp, Trung bình.
Trong từng phân khúc, số lượng “Không mua” luôn lớn hơn “Mua”:
Cao: 42.568 khách hàng không mua, 12.706 khách hàng mua
Rất cao: 79.427 khách hàng không mua, 22.809 khách hàng mua
Thấp: 6.672 khách hàng không mua, 2.203 khách hàng mua
Trung bình: 25.265 khách hàng không mua, 8.350 khách hàng mua
Ý nghĩa thống kê
Ở mọi phân khúc giá, số liệu cho thấy tỷ lệ mua thực tế đều thấp; nhóm giá thấp cũng không có tỷ lệ mua vượt trội.
Điều này phản ánh: giá bán thực tế của phân khúc chưa chắc là yếu tố duy nhất thúc đẩy quyết định mua. Các yếu tố khác (tài chính, tâm lý, trải nghiệm…) cũng rất quan trọng.
Doanh nghiệp không thể chỉ điều chỉnh giá để tăng chuyển đổi, mà cần đồng thời chú trọng chất lượng, dịch vụ, hỗ trợ và nghiên cứu sâu các rào cản đối với hành vi mua trong từng phân khúc.
p3 <- ggplot(df, aes(x=price_group, y=price, fill=price_group)) +
geom_boxplot(outlier.shape=NA) +
geom_jitter(alpha=0.2, width=0.2) +
scale_y_continuous(labels=comma) +
labs(title="(P3) Phân bố giá theo phân khúc", x="Phân khúc", y="Giá (USD)") +
theme_minimal() + theme(legend.position="none")
p3
Đoạn code này dùng để tạo ra một Biểu đồ Boxplot kết hợp Jitter (Boxplot with Jitter). Mục đích chính là trực quan hóa và so sánh sự phân phối của biến Giá nhà (price) giữa các nhóm giá (price_group).
Trong đó:
p3 <- ggplot(…): Toán tử gán, lưu biểu đồ đã hoàn thành vào đối tượng p3.
aes(x=price_group, y=price, fill=price_group): Đây là ánh xạ thẩm mỹ chính. Ánh xạ biến price_group lên trục X, price lên trục Y, và sử dụng price_group để tô màu (fill) các hộp Boxplot.
geom_boxplot(outlier.shape=NA): Lớp hình học chính, vẽ Boxplot. Tham số outlier.shape=NA yêu cầu R ẩn các giá trị ngoại lai (outliers) của Boxplot.
geom_jitter(alpha=0.2, width=0.2): Lớp hình học thứ hai, dùng để thêm các điểm dữ liệu thô vào biểu đồ. Tham số alpha=0.2 làm cho các điểm mờ hơn và width=0.2 tạo ra sự phân tán nhẹ nhàng trên trục X để tránh chồng chéo (nhưng không hiệu quả với dữ liệu lớn).
scale_y_continuous(labels=comma): Tùy chỉnh trục Y. Hàm comma (thuộc gói scales) được sử dụng để định dạng nhãn trục Y thành số có dấu phân cách hàng nghìn (ví dụ: \(1,000,000\)).
labs(…): Đặt nhãn cho biểu đồ, bao gồm title (Tiêu đề chính), x (Trục hoành), và y (Trục tung)
theme_minimal() + theme(legend.position=“none”): Áp dụng phong cách tối giản. Lệnh legend.position=“none” ẩn chú giải (legend), vì màu sắc đã được chỉ định theo trục X (price_group).
Kết quả kĩ thuật
Biểu đồ boxplot thể hiện rõ sự khác biệt giá giữa các phân khúc: “Cao”, “Rất cao”, “Thấp”, “Trung bình”.
Phân khúc “Rất cao” có mức giá trải dài lớn nhất, nhiều điểm dữ liệu (điểm jitter) nằm từ 1 triệu đến hơn 4 triệu USD.
Phân khúc “Cao” giá dao động tập trung quanh 700.000–1.000.000 USD; “Trung bình” dao động dưới 500.000 USD; nhóm “Thấp” nằm sát mức thấp nhất.
Các hộp (box) đều thể hiện mức giá tập trung khác biệt, không có sự chồng lấn đáng kể giữa các khúc.
Outlier bị loại khỏi boxplot (outlier.shape=NA), nhưng phân bổ điểm jitter cho thấy độ phân tán và mật độ dữ liệu thực tế rõ ràng.
Ý nghĩa thống kê
Phân khúc giá càng cao, sự đa dạng về mức giá càng lớn, thể hiện nhu cầu và sự phân hóa sâu sắc ở nhóm sản phẩm luxury.
Các sản phẩm “Trung bình” và “Thấp” có mức giá ổn định, biên độ nhỏ hơn, phù hợp số đông thị trường.
Điều này giúp doanh nghiệp xác định khu vực giá mục tiêu để tối ưu hóa chính sách bán hàng cho từng phân khúc; đồng thời nhận diện đâu là nhóm tiềm ẩn biến động, cần chăm sóc kỹ lưỡng về rủi ro và kịch bản đầu tư.
Thực tế này cũng chỉ ra: sản phẩm giá rẻ ít biến động, dễ bán đại trà; nhóm sản phẩm đắt tiền vừa đa dạng giá, vừa khó “bung hàng” đại trà.
p4 <- ggplot(df, aes(x=salary)) +
geom_histogram(bins=40, fill="darkgreen", color="white") +
geom_vline(aes(xintercept=mean(salary,na.rm=TRUE)), color="red", linetype="dashed") +
geom_rug() +
scale_x_continuous(labels=comma) +
labs(title="(P4) Phân phối thu nhập khách hàng", x="Thu nhập", y="Tần số") +
theme_minimal()
p4
Đoạn code này dùng để tạo ra một Biểu đồ Histogram (Biểu đồ Tần số).
Mục đích chính là để trực quan hóa sự phân phối của biến Thu nhập (salary) trong mẫu dữ liệu.
Trong đó:
p4 <- ggplot(…): Toán tử gán, lưu biểu đồ đã hoàn thành vào đối tượng p4.
aes(x=salary): Ánh xạ thẩm mỹ chính. Thiết lập biến salary lên trục X (trục hoành).
geom_histogram(bins=40, fill=“darkgreen”, color=“white”): Lớp hình học chính, vẽ biểu đồ tần số.
bins=40: Chỉ định chia dữ liệu thành 40 khoảng (bins) trên trục X.
fill=“darkgreen”, color=“white”: Thiết lập màu tô của cột là xanh đậm và viền là trắng.
geom_vline(aes(xintercept=mean(salary,na.rm=TRUE)), …): Lớp hình học thứ hai, vẽ một Đường thẳng đứng (Vertical Line). Nó được đặt ở vị trí Trung bình của cột salary để dễ dàng so sánh vị trí Trung bình với đỉnh (Mode) của biểu đồ.
scale_x_continuous(labels=comma): Tùy chỉnh trục X. Hàm comma (thuộc gói scales) được sử dụng để định dạng nhãn trục X thành số có dấu phân cách hàng nghìn (ví dụ: \(1,000,000\)).
labs(…): Đặt nhãn cho biểu đồ, bao gồm title (Tiêu đề chính), x (Thu nhập), và y (Tần số).
theme_minimal(): Áp dụng phong cách tối giản.
geom_rug(): Lớp hình học thứ ba, vẽ các vạch nhỏ dọc theo trục X để thể hiện vị trí chính xác của từng quan sát.
Kết quả kĩ thuật
Biểu đồ histogram thể hiện số lượng khách theo các khoảng thu nhập, chia thành 40 bins.
Đa số khách tập trung ở mức thu nhập dưới 30.000 USD, với các cột cao nhất nằm trong khoảng 10.000–30.000 USD.
Đường đứt đỏ là giá trị trung bình, nằm gần giữa, nhưng phần lớn khách dưới giá trị này — phân phối có xu hướng lệch phải.
Phân bổ phía thu nhập cao (trên 50.000-100.000 USD) thưa và đồng đều hơn, còn phía thu nhập rất thấp ít khách hàng.
Phân bổ không chuẩn, có dấu hiệu “hơi cụm” ở mức 15.000–30.000 USD và dàn mỏng ở khoảng cao.
Ý nghĩa thống kê
Nhóm khách hàng chủ đạo của thị trường bất động sản trong dữ liệu này là nhóm thu nhập trung bình-thấp (phía dưới 30.000 USD).
Phân khúc thu nhập cao tuy có mặt nhưng không phải số đông, giải thích vì sao các gói/hình thức sản phẩm phổ thông luôn được tiêu thụ mạnh hơn.
Doanh nghiệp cần tối ưu hóa sản phẩm, tài chính… tập trung vào nhóm thu nhập phổ thông thay vì chỉ đầu tư vào phân khúc cao.
Các chính sách hỗ trợ, ưu đãi, nên nhắm đến nhóm dưới trung bình để tận dụng số đông thị trường, vừa đáp ứng thực tiễn vừa nâng cao hiệu quả đầu tư.
color_scheme <- c("0" = "darkorange", "1" = "deepskyblue4")
p5 <- ggplot(df, aes(x=salary, y=price, color=factor(decision))) +
geom_point(alpha=0.4) +
geom_smooth(method="lm", se=FALSE) +
geom_density_2d(color="gray40") +
scale_y_continuous(labels=comma) +
scale_color_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = color_scheme )+
labs(title="(P5) Quan hệ giữa Giá và Thu nhập", x="Thu nhập", y="Giá", color="Decision") + # layer5
theme_minimal()
p5
## `geom_smooth()` using formula = 'y ~ x'
Đoạn code này dùng để tạo ra một Biểu đồ Phân tán đa lớp phức tạp. Mục đích chính là trực quan hóa và phân tích mối quan hệ tuyến tính giữa Thu nhập (salary) và Giá (price), đồng thời so sánh mối quan hệ đó giữa hai nhóm Quyết định (decision)
Trong đó:
color_scheme <- …: Đây là toán tử gán, định nghĩa một vector màu với mã tên (“0”, “1”) và lưu nó vào đối tượng color_scheme để sử dụng lại trong lệnh scale_color_manual.
ggplot(df, aes(x=salary, y=price, color=factor(decision))): Khởi tạo biểu đồ. Biến salary được ánh xạ lên trục X, price lên trục Y, và Quyết định (decision) lên màu sắc (color) của các điểm và đường.
geom_point(alpha=0.4): Lớp vẽ các điểm dữ liệu (point). Tham số alpha=0.4 làm các điểm mờ hơn để quản lý tình trạng chồng chéo (overplotting).
eom_smooth(method=“lm”, se=FALSE): Lớp vẽ Đường xu hướng Hồi quy Tuyến tính (LM). Tham số se=FALSE yêu cầu ẩn vùng tin cậy xung quanh đường hồi quy. Lệnh này tạo ra hai đường (cho ‘0’ và ‘1’) vì màu sắc đã được ánh xạ.
geom_density_2d(color=“gray40”): Lớp vẽ Đường Đồng mức Mật độ (Density Contours). Lớp này hiển thị các vùng mà dữ liệu tập trung nhiều nhất.
scale_y_continuous(labels=comma): Tùy chỉnh trục Y. Hàm comma (thuộc gói scales) định dạng nhãn trục Y thành số có dấu phân cách hàng nghìn.
scale_color_manual(…): Tùy chỉnh thủ công màu sắc và nhãn của chú giải Màu (color).
name = “Quyết định”: Đặt tiêu đề chú giải là “Quyết định”.
labels = c(“0” = “Không mua”, “1” = “Mua”): Tùy chỉnh nhãn. Nó chuyển các giá trị gốc “0” và “1” thành nhãn dễ đọc hơn.
values = color_scheme: Gán bảng màu đã định nghĩa cho hai nhóm.
labs(…): Đặt nhãn cho biểu đồ, bao gồm title (Tiêu đề chính), x (Thu nhập), và y (Giá). Tham số color=“Decision” trong labs bị ghi đè bởi scale_color_manual.
Kết quả kĩ thuật
Biểu đồ scatter cho thấy cả hai nhóm quyết định (“Mua” - xanh, “Không mua” - cam) phân bố dày đặc và chồng lấn toàn vùng giá, thu nhập.
Nhóm “Mua” có xu hướng tập trung nhiều hơn ở vùng giá thấp/thu nhập thấp đến trung bình, ít xuất hiện ở vùng giá cực cao.
Đường hồi quy (geom_smooth) cho hai nhóm đều cho thấy: thu nhập tăng thì giá sản phẩm mua/ngắm cũng tăng nhẹ, nhưng độ dốc không lớn.
Các đường đẳng mật độ (contour) báo hiệu đa số điểm giao dịch tập trung ở vùng giá và thu nhập thấp-trung; mật độ ở vùng cực cao rất loãng.
Ý nghĩa thống kê
Khách hàng mua bất động sản chủ yếu xuất hiện nhiều ở phân khúc giá vừa và thấp so với thu nhập; vùng siêu cao ít giao dịch thành công dù có nhiều điểm khảo sát.
Sản phẩm giá cao thường chỉ phù hợp đối tượng thu nhập rất lớn; đa phần thị trường thuộc phân khúc phổ thông.
Doanh nghiệp nên chú trọng sản phẩm tệp trung bình-giá rẻ, đồng thời cung cấp giải pháp tài chính riêng cho nhóm khách đặc thù có thu nhập cao và nhu cầu luxury.
p6 <- ggplot(df, aes(x=factor(decision), y=satisfaction_score, fill=factor(decision))) +
geom_violin(trim=FALSE, alpha=0.7, color="#3498DB") +
geom_boxplot(width=0.1, fill="white", color="#2C3E50") +
stat_summary(fun=mean, geom="point", color="#E74C3C", size=3, show.legend = FALSE) +
scale_fill_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = c("0" = "#A9CCE3", "1" = "#EC7063")
) +
labs(title="Phân bố Điểm hài lòng theo Quyết định",
subtitle = "Chấm đỏ thể hiện vị trí trung bình (Mean)",
x="Quyết định",
y="Điểm hài lòng",
fill="Quyết định") +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 16, hjust = 0.5),
legend.position="bottom",
guides(color = "none", shape = "none")
) +
theme(plot.title = element_text(size = 10))
p6
Mục đích chính là để so sánh toàn diện sự Phân phối của biến Điểm hài lòng (satisfaction_score) giữa hai nhóm khách hàng: Không mua (0) và Mua (1). Biểu đồ kết hợp cả hình dạng phân phối (violin), độ phân tán (boxplot), và vị trí trung tâm (mean)
Trong đó:
ggplot(df, aes(x=factor(decision), y=…, fill=factor(decision))): Khởi tạo biểu đồ. Ánh xạ biến decision lên trục X và màu tô (fill), biến satisfaction_score lên trục Y.
geom_violin(trim=FALSE, alpha=0.7, color=“#3498DB”): Lớp hình học chính, vẽ Biểu đồ Violin. Tham số trim=FALSE đảm bảo đường cong không bị cắt ở các giá trị tối thiểu/tối đa. Màu sắc được tùy chỉnh thủ công.
geom_boxplot(width=0.1, fill=“white”, color=“#2C3E50”): Lớp thứ hai, vẽ Boxplot nhỏ bên trong Violin. Nó cho thấy Trung vị và các Tứ phân vị (IQR). outlier.shape=NA (đã được sửa) ẩn các điểm ngoại lai của Boxplot.
stat_summary(fun=mean, geom=“point”, color=“#E74C3C”, size=3, show.legend = FALSE): Lớp Thống kê vẽ dấu chấm ở vị trí Trung bình (Mean). show.legend = FALSE là quan trọng để ngăn dấu chấm này tạo ra một chú giải (legend) riêng, tránh lỗi mâu thuẫn chú giải.
scale_fill_manual(…): Tùy chỉnh thủ công màu sắc và nhãn của chú giải Màu tô (fill). Tham số name = “Quyết định” đặt tiêu đề chú giải, và labels thay thế nhãn “0” và “1” bằng “Không mua” và “Mua”.
labs(…): Đặt nhãn cho biểu đồ. subtitle (chú thích phụ) giải thích ý nghĩa của chấm đỏ (vị trí Mean).
theme(plot.title = element_text(size = 10)): Lớp tùy chỉnh cuối cùng (ghi đè lên kích thước 16 cũ), đặt cỡ chữ của tiêu đề chính của biểu đồ thành 10 (rất nhỏ).
Kết quả kĩ thuật
Biểu đồ violin kết hợp boxplot hiển thị rõ sự khác biệt cực kỳ lớn về điểm hài lòng giữa hai nhóm khách hàng:
Nhóm không mua (“0”): điểm hài lòng phân tán thấp, chủ yếu từ 2–7, trung bình gần sát mức thấp nhất, hộp boxplot nằm lệch dưới.
Nhóm mua (“1”): điểm hài lòng dồn về mức 7–10, trung bình (chấm đỏ) sát gần 9, hộp boxplot đặt rất cao.
Không có sự trùng lắp giữa các boxplot: hai phân phối tách biệt rõ rệt.
Hình viền violin cho thấy mật độ khách hàng mua tập trung gần mức hài lòng tối đa, trong khi nhóm không mua trải đều và thấp hơn hẳn.
Ý nghĩa thống kê
Khả năng mua nhà phụ thuộc rất lớn vào trải nghiệm/hài lòng khách hàng: chỉ khi điểm hài lòng rất cao, khách mới thực sự ra quyết định mua.
Nhóm không hài lòng (dù marketing, tiếp cận nhiều) gần như không phát sinh giao dịch. Giá trị chiến lược nằm ở nâng trải nghiệm sát với nhu cầu/kỳ vọng thực sự.
Doanh nghiệp muốn tăng doanh số phải tập trung vào các giải pháp nâng cao hài lòng và dịch vụ sau bán hàng, vì đây là yếu tố dẫn đến tỉ lệ chuyển đổi cao, hơn là chỉ cạnh tranh về tài chính, giá trị vật chất.
p7 <- ggplot(df, aes(x=loan_to_income, fill=Salary_Group)) +
geom_density(alpha=0.4) +
geom_rug() +
geom_vline(aes(xintercept=mean(loan_to_income, na.rm=TRUE)), color="red") +
scale_x_continuous(labels=percent) +
labs(title="(P7) Mật độ Tỷ lệ vay/ thu nhập theo nhóm Lương",x="Tỷ lệ vay/thu nhập",y="Mật độ",fill="Nhóm Lương") +
theme(plot.title = element_text(size = 10)) +
theme_minimal()
p7
Đoạn code này dùng để tạo ra một Biểu đồ Mật độ chồng lấp. Mục đích chính là trực quan hóa và so sánh sự Phân phối của Tỷ lệ Vay trên Thu nhập (loan_to_income) giữa các Nhóm Lương (Salary_Group) khác nhau. Biểu đồ này giúp xác định: mức độ tập trung dữ liệu (đỉnh mật độ) và hình dạng phân phối của gánh nặng nợ theo từng cấp độ thu nhập.
Trong đó:
p7 <- ggplot(…): Toán tử gán, lưu biểu đồ vào đối tượng p7.
ggplot(df, aes(x=loan_to_income, fill=Salary_Group)): Khởi tạo biểu đồ. Ánh xạ biến Tỷ lệ vay/thu nhập lên trục X, và biến Nhóm Lương lên màu tô (fill).
geom_density(alpha=0.4): Lớp hình học chính, vẽ Đường cong Mật độ. Tham số alpha=0.4 làm cho các vùng tô màu trở nên trong suốt, cho phép các đường cong chồng lên nhau mà vẫn hiển thị được.
geom_rug(): Lớp hình học phụ. Vẽ các vạch nhỏ dọc theo trục X để thể hiện vị trí chính xác của từng quan sát. (Lưu ý: Với dữ liệu lớn \(N=200,000\), lớp này sẽ tạo thành một vệt đen kịt và có thể làm RStudio bị treo).
geom_vline(aes(xintercept=mean(loan_to_income, na.rm=TRUE)), …): Lớp hình học vẽ một Đường thẳng đứng (Vertical Line) tại vị trí Trung bình (Mean) của toàn bộ cột loan_to_income.
scale_x_continuous(labels=percent): Tùy chỉnh trục X. Hàm percent (thuộc gói scales) được sử dụng để định dạng nhãn trục X thành tỷ lệ phần trăm (ví dụ: \(0.20 \rightarrow 20\%\)).
labs(title=…, fill=“Nhóm Lương”): Đặt nhãn cho biểu đồ. title, x, y đặt tiêu đề và nhãn trục. Tham số fill=“Nhóm Lương” tùy chỉnh tiêu đề của chú thích (Legend Title) từ tên biến gốc sang “Nhóm Lương”.
theme_minimal() + theme(plot.title = element_text(size = 10)): Áp dụng phong cách tối giản. Lệnh theme(…) được sử dụng để thiết lập kích thước chữ của tiêu đề chính của biểu đồ là 10.
Kết quả kĩ thuật
Biểu đồ density phân tích tỷ lệ vay/thu nhập (loan_to_income) theo bốn nhóm lương (“Thấp”, “Trung bình thấp”, “Trung bình cao”, “Cao”).
Mọi nhóm đều có mật độ dồn ở khoảng rất thấp (gần 0), rồi giảm dần về bên phải; nhóm lương càng cao, đỉnh mật độ càng cao và càng lệch trái (càng ít phải gánh nặng nợ).
Đường vạch đỏ là giá trị trung bình – nằm gần bên trái, xác nhận đa số khách hàng có tỷ lệ vay/thu nhập dưới 10%.
Nhóm lương thấp trải dài hơn về phía phải, tức nhiều trường hợp phải chịu tỷ lệ vay cực lớn, dễ tiềm ẩn rủi ro nợ xấu.
Ý nghĩa thống kê
Khách hàng thu nhập cao chủ yếu chỉ cần vay ít so với thu nhập, do đó an toàn tài chính và năng lực trả nợ mạnh hơn.
Nhóm thu nhập thấp không những phải vay nhiều mà còn đối diện rủi ro tín dụng nếu xảy ra biến động thu nhập/thị trường.
Doanh nghiệp, ngân hàng cần ưu tiên giải pháp hỗ trợ tín dụng, gói vay linh hoạt hoặc chính sách ưu đãi cho nhóm lương thấp để giảm tỷ lệ rủi ro, đồng thời thu hút nhóm thu nhập cao với sản phẩm tài chính an toàn, đa dạng hơn
p8 <- ggplot(df, aes(x=salary, y=loan, color=factor(decision))) +
geom_point(alpha=0.5) +
geom_smooth(method="lm", se=FALSE, color="black", linetype="dashed") +
geom_rug() +
scale_y_continuous(labels=comma) +
labs(title="(P8) Mối quan hệ giữa Thu nhập và Khoản vay", x="Thu nhập", y="Khoản vay") +
theme_minimal() +
theme(plot.title = element_text(size = 10))
p8
## `geom_smooth()` using formula = 'y ~ x'
Đoạn code này dùng để tạo ra một Biểu đồ Phân tán kết hợp Đường Hồi quy Tuyến tính (LM).
Mục đích chính là trực quan hóa và phân tích mối quan hệ tuyến tính giữa Thu nhập (salary) và Khoản vay (loan), đồng thời phân biệt các điểm dữ liệu và xu hướng theo Quyết định mua nhà (decision).
Trong đó:
p8 <- ggplot(…): Toán tử gán, lưu biểu đồ vào đối tượng p8.
ggplot(df, aes(x=salary, y=loan, color=factor(decision))): Khởi tạo biểu đồ. Ánh xạ Thu nhập lên trục X, Khoản vay lên trục Y, và Quyết định (decision) lên màu sắc (color) của các điểm.
geom_point(alpha=0.5): Lớp vẽ các điểm dữ liệu (point). Tham số alpha=0.5 làm cho các điểm mờ hơn, giúp quản lý tình trạng chồng chéo (overplotting) một phần.
geom_smooth(method=“lm”, se=FALSE, color=“black”, linetype=“dashed”): Lớp vẽ Đường xu hướng Hồi quy Tuyến tính (LM).
method=“lm”: Chỉ định sử dụng mô hình tuyến tính.
se=FALSE: Ẩn vùng tin cậy xung quanh đường hồi quy.
color=“black” và linetype=“dashed”: Ghi đè lên ánh xạ màu ban đầu, buộc R chỉ vẽ một đường màu đen, gạch ngang duy nhất cho toàn bộ dữ liệu (bỏ qua sự phân biệt theo decision).
geom_rug(): Lớp vẽ các vạch nhỏ dọc theo trục X và Y để thể hiện vị trí chính xác của từng quan sát.
scale_y_continuous(labels=comma): Tùy chỉnh trục Y. Hàm comma (thuộc gói scales) định dạng nhãn trục Y thành số có dấu phân cách hàng nghìn.
labs(…): Đặt nhãn cho biểu đồ, bao gồm title, x, và y.
theme_minimal() + theme(plot.title = element_text(size = 10)): Áp dụng phong cách tối giản. Lệnh theme(…) đặt cỡ chữ tiêu đề chính của biểu đồ là 10.
Kết quả kĩ thuật
Biểu đồ scatter cho thấy mối quan hệ thuận giữa thu nhập và khoản vay: khách hàng có thu nhập càng cao thì quy mô khoản vay có xu hướng tăng.
Hầu hết các điểm của cả hai nhóm quyết định “0” (không mua – đỏ) và “1” (mua – xanh) trải đều trên toàn bộ vùng, nhưng đậm đặc tại vùng đáy (khoản vay dưới 2 triệu USD).
Đường hồi quy chung (gạch đen) cho thấy tốc độ tăng khoản vay chậm khi thu nhập tăng, tức tỷ lệ tăng khoản vay nhỏ hơn tỷ lệ tăng thu nhập ở vùng cao.
Mật độ điểm ở mức thu nhập dưới 50.000 USD là lớn nhất; phía trên thưa dần, cho thấy khách vay lớn thường là thu nhập cao – nhưng vẫn không phải số đông.
Ý nghĩa thống kê
Phần lớn khách hàng tập trung ở phân khúc vay-vừa-tiền, phù hợp khả năng trả nợ thực tế; chỉ số ít người vay cực lớn thuộc nhóm thu nhập cao.
Các doanh nghiệp/ngân hàng nên tập trung vào sản phẩm tài chính phục vụ nhóm thu nhập trung bình, tối ưu hóa hạn mức vay vừa phải – là khu vực nhu cầu thật cao nhất thị trường.
Riêng sản phẩm cho vay lớn, cần kiểm soát rủi ro và định hướng rõ cho nhóm khách có năng lực thu nhập phù hợp, vì tệp khách này nhỏ và phân bố không đều.
p9 <- ggplot(df, aes(x=neighbourhood_rating, y=connectivity_score, color=Salary_Group)) +
geom_smooth(method="lm",
se=FALSE) +
facet_wrap(~price_group, scales = "free") +
scale_color_discrete(name = "Nhóm Lương") +
labs(title="(P9) So sánh Xu hướng Tiện ích & Kết nối theo Nhóm Lương",
x="Điểm khu vực lân cận",
y="Điểm kết nối giao thông") +
theme_minimal() +
theme(plot.title = element_text(size = 10))
p9
## `geom_smooth()` using formula = 'y ~ x'
Đoạn code này dùng để tạo ra một Biểu đồ Hồi quy Đa bảng (Faceted Regression Plot).
Mục đích chính là để trực quan hóa và so sánh xu hướng của mối quan hệ tuyến tính giữa Điểm khu vực (neighbourhood_rating) và Điểm kết nối (connectivity_score), phân tích sự thay đổi của mối quan hệ này qua các nhóm Phân khúc giá (price_group) khác nhau, đồng thời phân biệt theo Nhóm Lương (Salary_Group).
Trong đó:
ggplot(df, aes(x=…, y=…, color=Salary_Group)): Khởi tạo biểu đồ. Ánh xạ Điểm khu vực lên trục X, Điểm kết nối lên trục Y, và Nhóm Lương lên màu sắc (color).
geom_smooth(method=“lm”, se=FALSE): Lớp hình học chính, vẽ các Đường Hồi quy Tuyến tính (LM).
method=“lm”: Chỉ định sử dụng mô hình tuyến tính (Linear Model).
se=FALSE: Ẩn vùng tin cậy xung quanh đường hồi quy, giúp biểu đồ rõ ràng hơn.
facet_wrap(~price_group, scales = “free”): Lớp phân chia bảng. Chia biểu đồ thành nhiều bảng nhỏ (facets), mỗi bảng đại diện cho một giá trị của price_group.
scales = “free”: Tham số quan trọng, cho phép trục X và Y của mỗi bảng nhỏ được điều chỉnh độc lập (tự do), giúp tối ưu hóa không gian hiển thị cho dữ liệu ở mỗi phân khúc giá.
scale_color_discrete(name = “Nhóm Lương”): Tùy chỉnh chú giải Màu (color). Tham số name = “Nhóm Lương” đặt tiêu đề cho chú giải màu sắc.
labs(title=…, x=…, y=…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục hoành và trục tung.
theme_minimal() + theme(plot.title = element_text(size = 10)): Áp dụng phong cách tối giản. Lệnh theme(…) đặt cỡ chữ tiêu đề chính của biểu đồ là 10 (thu nhỏ tiêu đề).
Kết quả kĩ thuật
Biểu đồ facet 4 bảng, mỗi bảng ứng với 1 nhóm giá (“Cao”, “Rất cao”, “Thấp”, “Trung bình”); từng đường là một nhóm lương khác nhau.
Trong mỗi phân khúc giá, xu hướng giữa điểm khu vực lân cận (trục ngang) và điểm kết nối giao thông (trục dọc) thay đổi theo nhóm lương:
Ở nhóm “Cao” và “Trung bình cao”, chỉ nhóm lương cao có đường dốc lên rõ, báo hiệu nhóm này càng ưu tiên tiện ích - kết nối càng cao.
Nhóm lương thấp thường có xu hướng kết nối giảm dần khi tiện ích tăng.
Ở phân khúc “Thấp” và “Trung bình”, các đường dốc and giao nhau — sự khác biệt giữa nhóm lương thể hiện rõ và không đồng nhất.
Ý nghĩa thống kê
Nhóm khách hàng lương cao ở phân khúc giá cao quan tâm đến cả tiện ích khu vực lẫn chỉ số kết nối giao thông, thể hiện đòi hỏi chất lượng sống cao hơn.
Ở những phân khúc giá thấp, nhóm thu nhập càng cao càng có xu hướng ưu tiên tích hợp dịch vụ, tiện ích — khác với nhóm lương thấp chỉ tìm kiếm một số yếu tố đủ dùng.
Doanh nghiệp bất động sản nên cá biệt hóa chiến lược sản phẩm: ở phân khúc cao, cần tập trung hệ giá trị tiện ích và kết nối cho khách hàng thu nhập lớn; ở phân khúc phổ thông, ưu tiên cân đối vừa đủ để tiết kiệm chi phí và phù hợp nhu cầu đại chúng.
Mục đích tổng thể của đoạn code này là để so sánh sự phân bố của price (Giá nhà) giữa 13 country (Quốc gia) khác nhau. Dòng code thực hiện:
garage_df <- df %>%
group_by(garage_label) %>%
summarise(prop_mua = mean(decision=="1"))
p10 <- ggplot(garage_df, aes(x=garage_label, y=prop_mua, fill=garage_label)) +
geom_col() +
geom_text(aes(label=scales::percent(prop_mua)), vjust=-0.2) +
scale_y_continuous(labels=percent) +
scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
labs(title="(P10) Tỷ lệ khách mua theo tình trạng garage",x="Tình trạng garage",y="Xác suất mua") +
theme_minimal()
## Scale for y is already present.
## Adding another scale for y, which will replace the existing scale.
p10
Đoạn code này dùng để tạo ra một Biểu đồ Cột Tỷ lệ (Proportion Bar Chart). Mục đích chính là trực quan hóa và so sánh Xác suất Mua (Purchase Probability) dựa trên Tình trạng Garage của bất động sản.
Tính toán trước: Mã garage_df <- … tính toán tỷ lệ mua trung bình (mean(decision==“1”)) cho hai nhóm (Có và Không).
Trực quan hóa: Biểu đồ hiển thị sự khác biệt về tỷ lệ này, giúp xác định liệu việc có garage có phải là một yếu tố thúc đẩy quyết định mua hay không.
Trong đó:
garage_df <- df %>% group_by(…) %>% summarise(…): Toán tử gán và Tiền xử lý dữ liệu. Nó tính toán Trung bình của biến nhị phân decision (chính là tỷ lệ/xác suất mua) theo nhóm garage_label và lưu kết quả vào garage_df.
ggplot(garage_df, aes(x=garage_label, y=prop_mua, fill=garage_label)): Khởi tạo biểu đồ. Ánh xạ Tình trạng Garage lên trục X, Tỷ lệ mua (prop_mua) lên trục Y, và sử dụng garage_label để tô màu (fill).
geom_col(): Lớp hình học chính. Dùng geom_col (thay vì geom_bar) vì trục Y (prop_mua) đã được tính toán trước.
geom_text(aes(label=scales::percent(prop_mua)), …): Lớp nhãn số (Text Label). Hiển thị giá trị phần trăm của tỷ lệ mua ngay trên đầu mỗi cột. Tham số vjust=-0.2 dịch chuyển nhãn lên trên.
scale_y_continuous(labels=percent): Tùy chỉnh trục Y. Lệnh này định dạng nhãn trục Y dưới dạng tỷ lệ phần trăm (ví dụ: \(0.25 \rightarrow 25\%\)). (Lưu ý: Lệnh này bị ghi đè bởi lệnh scale_y_continuous tiếp theo).
scale_y_continuous(expand = expansion(mult = c(0, 0.1))): Lớp này là lớp cuối cùng (ghi đè lớp trên). Tham số expand = expansion(mult = c(0, 0.1)) thêm một khoảng đệm \(10\%\) ở phía trên trục Y, đảm bảo nhãn số (geom_text) không bị cắt bởi giới hạn biểu đồ.
labs(…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục X và Y.
theme_minimal() + theme(legend.position=“none”): Áp dụng phong cách tối giản và ẩn chú giải (legend) vì màu sắc đã được chỉ định rõ ràng trên trục X.
Kết quả kĩ thuật
Biểu đồ cột cho thấy tỷ lệ khách mua nhà giữa hai nhóm: Có garage và Không garage gần như bằng nhau.
Cụ thể:
Nhà có garage: 23,09% khách hàng mua
Nhà không garage: 22,98% khách hàng mua
Sự chênh lệch xác suất mua giữa hai nhóm này là rất nhỏ, hầu như không có ý nghĩa về mặt thống kê hoặc thực tế.
Ý nghĩa thống kê
Việc bất động sản có hay không có garage không ảnh hưởng đáng kể tới quyết định mua của khách hàng.
Nhà phát triển/dịch vụ bất động sản không nên lấy “garage” làm điểm nhấn chính trong định hình sản phẩm hoặc chiến lược bán hàng. Nên chú trọng tối ưu các yếu tố tác động mạnh hơn (giá, vị trí, chất lượng, tiện ích…).
Thị trường cho thấy yếu tố phụ trợ này phù hợp với một số khách hàng đặc thù hơn là toàn bộ tập khách mua nhà nói chung.
num_df <- df %>% select(price, salary, loan, emi_ratio, satisfaction_score, loan_to_income)
cor_mat <- round(cor(num_df, use="pairwise.complete.obs"), 2)
library(reshape2)
## Warning: package 'reshape2' was built under R version 4.5.1
melt_cor <- melt(cor_mat)
p11 <- ggplot(melt_cor, aes(Var1, Var2, fill=value)) +
geom_tile() +
geom_text(aes(label=value)) +
scale_fill_gradient2(low="blue", mid="white", high="red", midpoint=0) +
labs(title="(P11) Ma trận tương quan giữa các biến định lượng") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(plot.title = element_text(size = 10))
p11
Đoạn code này dùng để tạo ra một Biểu đồ Bản đồ Nhiệt (Heatmap) của Ma trận Tương quan.
Mục đích chính là trực quan hóa cường độ và hướng của mối quan hệ tuyến tính giữa mọi cặp biến định lượng đã chọn (price, salary, loan, v.v.). Biểu đồ này giúp xác định nhanh chóng các mối tương quan mạnh (gần \(1\) hoặc \(-1\)).
Trong đó:
num_df <- df %>% select(…): Toán tử gán, sử dụng hàm select() của dplyr để tạo một tập dữ liệu mới num_df chỉ chứa các biến định lượng cần phân tích.
cor_mat <- round(cor(num_df, use = “pairwise.complete.obs”), 2): Tính toán Ma trận Tương quan Pearson (\(r\)) cho num_df.
cor(…): Hàm tính tương quan.use = “pairwise.complete.obs”: Hướng dẫn R chỉ sử dụng các cặp quan sát có dữ liệu đầy đủ (không có NA) cho mỗi lần tính toán tương quan.
round(…, 2): Làm tròn kết quả ma trận đến 2 chữ số thập phân.
library(reshape2): Lệnh nạp gói reshape2.
melt_cor <- melt(cor_mat): Hàm melt() của reshape2. Hàm này chuyển đổi Ma trận Tương quan (dạng ma trận vuông) thành định dạng “dài” (long format) gồm ba cột (Var1, Var2, value). Đây là định dạng bắt buộc để ggplot2 vẽ Heatmap.
ggplot(melt_cor, aes(Var1, Var2, fill=value)): Khởi tạo biểu đồ. Ánh xạ Var1 (trục X), Var2 (trục Y), và value (hệ số tương quan \(r\)) lên màu tô (fill).
geom_tile(): Lớp hình học chính, vẽ các ô vuông (tiles) cho mỗi sự kết hợp (Var1, Var2). Cường độ màu của ô vuông này được xác định bởi value (hệ số \(r\)).
geom_text(aes(label=value)): Lớp vẽ nhãn số. Hiển thị chính xác giá trị của hệ số tương quan (\(r\)) bên trong mỗi ô.
scale_fill_gradient2(…): Tùy chỉnh thang màu (fill) cho Heatmap.low=“blue”, mid=“white”, high=“red”: Thiết lập thang màu: tương quan âm mạnh là xanh dương, tương quan gần \(0\) là trắng, tương quan dương mạnh là đỏ.
midpoint=0: Đặt điểm trung tâm (trắng) chính xác tại \(r=0\).
theme(axis.text.x = element_text(angle = 45, hjust = 1)): Tùy chỉnh trục X. Xoay nhãn trục X \(45\) độ để tránh chồng chéo khi tên biến quá dài.
theme(plot.title = element_text(size = 10)): Lệnh ghi đè kích thước chữ của tiêu đề chính thành \(10\) (thu nhỏ tiêu đề).
Kết quả kĩ thuật
Giá và khoản vay có tương quan rất mạnh với nhau (0.94); giá cũng tỷ lệ vừa phải với tỷ lệ vay/thu nhập (0.4), tỉ lệ trả góp/thu nhập (0.38).
Khoản vay tương quan rất cao với loan_to_income (0.44) và emi_ratio (0.42), price và loan hầu như “song hành”.
Lương và các chỉ số rủi ro (emi_ratio, loan_to_income) có tương quan âm vừa (-0.47 đến -0.5): lương cao, áp lực vay/tỉ lệ trả góp/thanh toán thấp đi rõ rệt.
satisfaction_score gần như không tương quan với bất kỳ biến số nào trong nhóm này (toàn bộ hệ số = 0).
loan_to_income và emi_ratio có tương quan gần tuyệt đối (0.96), hàm ý hai biến phản ánh cùng một điều/nguy cơ tài chính.
Ý nghĩa thống kê
Quy mô khoản vay tỷ lệ thuận với giá trị bất động sản, xác nhận thực tiễn tài chính khi người mua thường vay phù hợp giá trị tài sản.
Những người thu nhập cao vay ít hơn và chịu áp lực tài chính thấp hơn, thể hiện việc cân đối khả năng chi trả vững vàng.
satisfaction_score độc lập với biến số tài chính: các yếu tố về sự hài lòng không bị chi phối bởi tỷ lệ vay, khoản vay hay giá, phù hợp với nhận định hài lòng là yếu tố phi tài chính, mang tính trải nghiệm và dịch vụ.
p12 <- ggplot(df, aes(x=price, y=price_group, fill=price_group)) +
ggridges::geom_density_ridges(alpha=0.6) +
scale_x_continuous(
labels = scales::comma,
breaks = seq(0, 3000000, by = 500000)
) +
labs(title="(P12) Phân phối giá theo nhóm",
x = "Giá",
y = "Nhóm giá") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(legend.position = "none")
p12
## Picking joint bandwidth of 22000
Đoạn code này dùng để tạo ra một Biểu đồ Lược Sóng (Ridgeline Plot). Mục đích chính là trực quan hóa và so sánh sự Phân phối của biến Giá nhà (price) giữa các nhóm giá (price_group) khác nhau. Biểu đồ này giúp xem xét hình dạng, đỉnh (mức giá tập trung nhất), và sự dịch chuyển của giá nhà khi phân khúc tăng lên.
Trong đó:
ggplot(df, aes(x=price, y=price_group, fill=price_group)): Khởi tạo biểu đồ. Ánh xạ Giá nhà lên trục X, Nhóm giá lên trục Y (để xếp chồng), và sử dụng price_group để tô màu (fill).
ggridges::geom_density_ridges(alpha=0.6): Lớp hình học chính. Vẽ Đường cong Mật độ (Density Curves) chồng lấp cho từng nhóm giá. Tham số alpha=0.6 làm cho các đường cong trong suốt để các lớp chồng lên nhau vẫn hiển thị được.
scale_x_continuous(…): Tùy chỉnh trục X (Giá).labels = scales::comma: Định dạng nhãn trục X bằng dấu phân cách hàng nghìn (ví dụ: \(1,000,000\)).
breaks = seq(0, 3000000, by = 500000): Tham số quan trọng, buộc R đặt các vạch chia cố định (ví dụ: \(0, 500k, 1tr, 1.5tr, ...\)), giúp giãn trục ra và dễ đọc hơn.
labs(title=…, x=…, y=…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục hoành (Giá), và nhãn trục tung (Nhóm giá).
theme_minimal(): Áp dụng phong cách tối giản.
theme(axis.text.x = element_text(angle = 45, hjust = 1)): Tùy chỉnh trục X. Xoay nhãn trục X \(45\) độ để tránh chồng chéo khi có nhiều vạch chia.
theme(legend.position = “none”): Ẩn chú giải (legend) vì màu sắc đã được chỉ định rõ ràng trên trục Y.
Kết quả kĩ thuật
Biểu đồ ridge thể hiện rõ ranh giới giá giữa các nhóm:
“Trung bình”: chủ yếu tập trung quanh 400.000–500.000 USD (phân phối hẹp, dốc đứng).
“Thấp”: giá trị nhỏ nhất, phần lớn nằm dưới 400.000 USD, mật độ cao nhất sát về phía trái.
“Cao”: dao động từ 700.000–1.100.000 USD, phân phối rộng và hướng về phía phải nhiều hơn.
“Rất cao”: kéo dài bắt đầu từ khoảng 1.200.000 USD đến tận hơn 2.500.000 USD, biên độ rộng, phân bố trải dài.
Các nhóm không trùng lấn nhau, mỗi nhóm giá có miền giá trị đặc trưng riêng biệt, hiếm khi có giao nhau nhiều ở vùng biên.
Ý nghĩa thống kê
Thị trường bất động sản phân tầng rất rõ rệt: mỗi nhóm giá phục vụ một tập khách hàng khác biệt, ít cạnh tranh trực tiếp với nhau.
Sản phẩm giá rẻ và trung bình phân bố tập trung, dễ bán đại trà; nhóm bất động sản cao cấp - luxury có biên độ giá rộng, khách hàng mục tiêu chọn lọc hơn.
Doanh nghiệp nên xây dựng sản phẩm, chính sách, marketing dựa trên đúng nhóm giá mục tiêu; tránh dàn trải hoặc đặt sai chiến lược giá sẽ làm chồng lấn thị trường, giảm hiệu quả khai thác phân khúc.
p13 <- df %>%
group_by(garden_label, decision) %>%
summarise(n=n()) %>%
ggplot(aes(x=factor(garden_label), y=n, fill=factor(decision))) +
geom_col(position="dodge") +
geom_text(aes(label=n), position=position_dodge(0.9), vjust=-0.3) +
labs(title="(P13) Quyết định mua theo tình trạng vườn", x="Tình trạng vườn", y="Số lượng") +
scale_fill_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = c("0" = "tomato", "1" = "steelblue")
) +
scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
theme_minimal()
## `summarise()` has grouped output by 'garden_label'. You can override using the
## `.groups` argument.
p13
Đoạn code này dùng để tạo ra một Biểu đồ Cột Nhóm (Grouped Bar Chart).
Mục đích chính là trực quan hóa và so sánh Tần số (Số lượng) của các quyết định mua/không mua giữa hai nhóm: “Có Vườn” và “Không Vườn”. Phân tích này giúp xác định tổng số lượng giao dịch và sự phân bổ Mua/Không mua trong mỗi nhóm.
Trong đó:
df %>% group_by(garden_label, decision) %>% summarise(n=n()): Đây là bước tiền xử lý. Nó sử dụng dplyr để phân nhóm dữ liệu theo cả hai biến (garden_label và decision) và sau đó dùng summarise(n=n()) để đếm tổng số lượng quan sát (n) trong mỗi nhóm kết hợp.
ggplot(…, aes(x=factor(garden_label), y=n, fill=factor(decision))): Khởi tạo biểu đồ. Ánh xạ Tình trạng vườn lên trục X, Số lượng (n) đã tính toán lên trục Y, và Quyết định (decision) lên màu tô (fill).
geom_col(position=“dodge”): Lớp hình học chính, vẽ cột. Tham số position=“dodge” là quan trọng, nó yêu cầu R đặt các cột Mua (1) và Không mua (0) cạnh nhau (nhóm lại) thay vì chồng lên nhau.
geom_text(aes(label=n), position=position_dodge(0.9), vjust=-0.3): Lớp nhãn số. Hiển thị số lượng đếm (n) chính xác ngay trên đầu mỗi cột. position=position_dodge(0.9) căn chỉnh nhãn số với cột tương ứng.
scale_fill_manual(…): Tùy chỉnh màu sắc và nhãn của chú giải Màu tô (fill). name = “Quyết định” đặt tiêu đề chú giải, và labels thay thế nhãn “0” và “1” thành “Không mua” và “Mua”.
scale_y_continuous(expand = expansion(mult = c(0, 0.1))): Tùy chỉnh trục Y. Thêm một khoảng đệm \(10\%\) ở phía trên để nhãn số (geom_text) không bị cắt bởi giới hạn biểu đồ.
labs(…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục X và Y.
Kết quả kĩ thuật
Biểu đồ cột so sánh số lượng khách không mua (“Không mua” – đỏ) và mua (“Mua” – xanh) giữa hai nhóm: nhà có vườn và không có vườn.
Số lượng khách “Không mua” gần ngang nhau: 77.042 (Có vườn) và 76.890 (Không vườn).
Số lượng khách “Mua” tương đương: 23.001 (Có vườn) và 23.067 (Không vườn).
Sự khác biệt về số lượng giữa hai tình trạng vườn là không đáng kể ở cả hai nhóm quyết định, tỉ lệ giữa “mua” và “không mua” không đổi nhiều theo thuộc tính vườn.
Ý nghĩa thống kê
Sở hữu vườn hầu như không ảnh hưởng đến tỷ lệ khách mua bất động sản.
Điều này cho thấy yếu tố vườn chỉ đóng vai trò phụ trợ, không phải quyết định chính trong hành vi mua của đại đa số khách hàng.
Nhà phát triển bất động sản nên chọn cách đầu tư hợp lý, không cần tăng giá hoặc chi phí chỉ để bổ sung vườn, mà nên tập trung vào các yếu tố thực sự quyết định hành vi mua lớn hơn (giá, vị trí, tài chính, trải nghiệm…).
p14 <- ggplot(df, aes(x=satisfaction_score, y=price, color=factor(decision))) +
geom_point(alpha=0.4) +
geom_smooth(method="lm", se=FALSE) +
scale_y_continuous(labels=comma) +
labs(title="(P14) Mối quan hệ giữa điểm hài lòng và giá nhà", x="Điểm hài lòng",y="Giá") +
scale_color_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = c("0" = "darkorange", "1" = "deepskyblue4")
) +
theme_minimal() +
theme(plot.title = element_text(size = 10))
p14
## `geom_smooth()` using formula = 'y ~ x'
Đoạn code này dùng để tạo ra một Biểu đồ Phân tán (Scatter Plot) phức tạp.
Mục đích chính là trực quan hóa và phân tích mối quan hệ tuyến tính giữa Điểm hài lòng (satisfaction_score) và Giá nhà (price), đồng thời so sánh mối quan hệ đó giữa hai nhóm Quyết định (decision). Phân tích này giúp xác định liệu sự hài lòng của khách hàng có liên quan đến mức giá mà họ xem xét hay không.
Trong đó:
ggplot(df, aes(x=…, y=…, color=factor(decision))): Khởi tạo biểu đồ. Ánh xạ Điểm hài lòng lên trục X, Giá lên trục Y, và Quyết định (decision) lên màu sắc (color) của các điểm và đường.
geom_point(alpha=0.4): Lớp vẽ các điểm dữ liệu. Tham số alpha=0.4 làm các điểm mờ hơn để quản lý tình trạng chồng chéo.
geom_smooth(method=“lm”, se=FALSE): Lớp vẽ Đường xu hướng Hồi quy Tuyến tính (LM). Tham số se=FALSE yêu cầu ẩn vùng tin cậy xung quanh đường hồi quy. Lệnh này tạo ra hai đường (cho ‘0’ và ‘1’) vì màu sắc đã được ánh xạ ban đầu.
scale_y_continuous(labels=comma): Tùy chỉnh trục Y (Giá). Hàm comma (thuộc gói scales) định dạng nhãn trục Y thành số có dấu phân cách hàng nghìn.
scale_color_manual(…): Tùy chỉnh thủ công màu sắc và nhãn của chú giải Màu (color).
name = “Quyết định”: Đặt tiêu đề chú giải là “Quyết định”.
labels = c(“0” = “Không mua”, “1” = “Mua”): Tùy chỉnh nhãn. Nó chuyển các giá trị gốc “0” và “1” thành nhãn dễ đọc hơn.
values = c(“0” = “darkorange”, “1” = “deepskyblue4”): Gán mã màu cụ thể cho hai nhóm.
labs(…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục X và Y.
theme_minimal() + theme(plot.title = element_text(size = 10)): Áp dụng phong cách tối giản. Lệnh theme(…) đặt cỡ chữ tiêu đề chính của biểu đồ là 10 (thu nhỏ tiêu đề).
Kết quả kĩ thuật
Biểu đồ scatter cho thấy phần lớn điểm hài lòng của nhóm “Mua” (xanh) tập trung ở mức 7–10, với giá nhà phân bố quanh 1.000.000–1.500.000 USD.
Nhóm “Không mua” (cam) lan trải đều từ điểm hài lòng thấp đến cao, giá nhà trải dài nhưng tập trung dày đặc cả ở giá thấp lẫn cao.
Đường hồi quy cho nhóm “Mua” khá ổn định, không có xu hướng tăng hoặc giảm rõ rệt theo điểm hài lòng; nhóm “Không mua” cũng tương tự.
Không có mối liên hệ tuyến tính mạnh giữa giá và điểm hài lòng, mặc dù rõ ràng để phát sinh mua, điểm hài lòng phải cao.
Ý nghĩa thống kê
Giá bất động sản giao dịch thành công (“Mua”) không bị giới hạn ở loại đắt hoặc rẻ nếu khách đạt ngưỡng hài lòng cao.
Sự hài lòng là “điều kiện cần” để quyết định mua, còn giá chỉ là “điều kiện đủ” phù hợp tiềm lực – tức sản phẩm dù rẻ mà không hài lòng vẫn khó bán.
Doanh nghiệp nên ưu tiên trải nghiệm/giá trị khách hàng song song chính sách tài chính, tránh chỉ tập trung hạ giá mà bỏ qua việc nâng cao mức độ hài lòng.
p15 <- ggplot(df, aes(x=property_size_sqft)) +
geom_histogram(bins=40, fill="skyblue", color="white") +
geom_vline(aes(xintercept=mean(property_size_sqft, na.rm=TRUE)), color="red") +
geom_rug() +
scale_x_continuous(
breaks = seq(0, max(df$property_size_sqft), by = 1000),
labels = scales::comma
) +
labs(title="(P15) Phân phối diện tích nhà",x="Diện tích Bất động sản",y="Tần số") +
theme_minimal()
p15
Đoạn code này dùng để tạo ra một Biểu đồ Histogram (Biểu đồ Tần số).
Mục đích chính là trực quan hóa sự phân phối của biến Diện tích Bất động sản (property_size_sqft). Biểu đồ này giúp xác định: hình dạng phân phối (đối xứng hay lệch), vị trí của Trung bình (Mean) (đường thẳng đứng màu đỏ), và phạm vi giá trị của diện tích.
Trong đó:
ggplot(df, aes(x=property_size_sqft)): Khởi tạo biểu đồ. Ánh xạ biến Diện tích lên trục X.
geom_histogram(bins=40, fill=“skyblue”, color=“white”): Lớp hình học chính, vẽ biểu đồ tần số. Tham số bins=40 chỉ định chia dữ liệu thành 40 khoảng (bins) trên trục X.
geom_vline(aes(xintercept=mean(…)), color=“red”, …): Lớp vẽ một Đường thẳng đứng (Vertical Line) tại vị trí Trung bình (Mean) của diện tích, giúp so sánh vị trí Trung bình.
geom_rug(): Lớp vẽ các vạch nhỏ dọc theo trục X để thể hiện vị trí chính xác của từng quan sát dữ liệu.
scale_x_continuous(…): Tùy chỉnh trục X.
breaks = seq(0, max(df$property_size_sqft), by = 1000): Tham số quan trọng, buộc R đặt các vạch chia cố định (cách nhau 1000 đơn vị), giúp giãn trục và dễ đọc hơn.
labels = scales::comma: Định dạng nhãn trục X bằng dấu phân cách hàng nghìn.
labs(title=…, x=…, y=“Tần số”): Đặt nhãn cho biểu đồ. Việc đặt y=“Tần số” là chính xác cho geom_histogram (vẽ số lượng đếm).
theme_minimal(): Áp dụng phong cách tối giản.
Kết quả kĩ thuật
Biểu đồ histogram cho thấy diện tích nhà phân bổ khá đồng đều, gần như dạng phẳng trên toàn dải từ dưới 1.000 ft² đến gần 6.000 ft².
Chỉ có hai phần ở hai đầu (cận dưới và trên) thấp hơn một chút; giữa dải (1.000–5.800 ft²) tần số các bin (cột) gần tương đương.
Đường thẳng đỏ là giá trị trung bình, cắt ở ~3.000 ft², đúng ở vùng trung tâm phân bố.
Không phát hiện tập trung cực lớn ở một nhóm diện tích nào, xác nhận tính đa dạng, không thiên lệch của nguồn sản phẩm.
Ý nghĩa thống kê
Thị trường bất động sản trong dữ liệu này cực kỳ đa dạng về diện tích, đáp ứng đủ nhu cầu từ hộ nhỏ, vừa tới đại gia đình, doanh nghiệp.
Sản phẩm diện tích tầm trung (~3.000 ft²) là chuẩn phổ biến, nhưng các nhóm diện tích nhỏ và lớn đều có nguồn cung tương đương, phù hợp nhiều phân khúc khách hàng.
Điều này giúp doanh nghiệp linh hoạt đưa ra sản phẩm đa dạng, xây dựng chiến lược bán phù hợp từng nhóm đối tượng mà không cần tối ưu hóa về một loại diện tích duy nhất.
mean_sizes <- df %>%
group_by(property_type) %>%
summarise(mean_size = mean(property_size_sqft, na.rm = TRUE))
p16 <- ggplot(df, aes(x = property_type, y = property_size_sqft, fill = property_type)) +
geom_violin(alpha = 0.5, trim = FALSE, color = "gray50") +
geom_boxplot(width = 0.2, fill = "white", color = "black", outlier.shape = NA) +
stat_summary(fun = mean, geom = "point", color = "red", size = 3, shape = 18) +
geom_text(data = mean_sizes,
aes(y = mean_size, label = scales::comma(mean_size)),
color = "#2C3E50", size = 3, fontface = "bold",
vjust = -0.5) +
scale_y_continuous(
trans = 'log10',
labels = scales::comma,
expand = expansion(mult = c(0.05, 0.15))
) +
labs(
title = "(P16) Phân bố Diện tích (Thang Log) theo Loại hình nhà",
x = "Loại hình nhà",
y = "Diện tích (sqft - Thang Log)"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(size = 10, face = "bold", hjust = 0.5),
axis.text.x = element_text(angle = 18, vjust = 0.7, size = 12),
legend.position = "none"
)
p16
Đoạn code này dùng để tạo ra một Biểu đồ So sánh Phân phối Chi tiết (Violin-Boxplot).
Mục đích chính là trực quan hóa và so sánh toàn diện sự Phân phối của biến Diện tích Bất động sản (property_size_sqft) giữa các nhóm Loại hình bất động sản (property_type).
Biểu đồ này được tối ưu hóa bằng Thang Logarit (trans = ‘log10’) để xử lý dữ liệu bị lệch và hiển thị giá trị số trung bình (Mean) ngay trên biểu đồ.
Trong đó:
mean_sizes <- df %>% group_by(…) %>% summarise(…): Bước Tiền xử lý. Tính Trung bình (Mean) của diện tích cho từng property_type và lưu vào mean_sizes.
ggplot(df, aes(x=…, y=…, fill=…)): Khởi tạo biểu đồ. Ánh xạ Loại hình nhà lên trục X và màu tô (fill), Diện tích lên trục Y.
geom_violin(alpha=0.5, …): Lớp thứ nhất, vẽ Biểu đồ Phân phối Mật độ (Violin Plot).
geom_boxplot(width=0.2, …): Lớp thứ hai, vẽ Biểu đồ Hộp (Boxplot). Tham số outlier.shape=NA ẩn các giá trị ngoại lai của Boxplot.
stat_summary(fun=mean, geom=“point”, …): Lớp Thống kê vẽ dấu chấm ở vị trí Trung bình (Mean) (đường chấm đỏ).
geom_text(data = mean_sizes, …): Lớp Nhãn Số. Hiển thị giá trị số Trung bình đã được tính toán (mean_sizes) ngay trên Boxplot. Hàm scales::comma được dùng để định dạng số lớn.
scale_y_continuous(trans = ‘log10’, labels = scales::comma, …): Tùy chỉnh Trục Y. Tham số trans = ‘log10’ là quan trọng nhất, nó chuyển trục Y sang Thang Logarit, làm giãn dữ liệu ở đáy và khắc phục vấn đề bị lệch. Tham số expand thêm đệm để nhãn số không bị cắt.
labs(…): Đặt nhãn cho biểu đồ, bao gồm tiêu đề, nhãn trục X và Y.
theme_minimal(…): Áp dụng phong cách tối giản. Các lệnh theme(…) tùy chỉnh kích thước chữ tiêu đề và xoay nhãn trục X.
Kết quả kĩ thuật
Biểu đồ violin-log phân tích diện tích của 6 loại hình nhà: Apartment, Farmhouse, Independent House, Studio, Townhouse, Villa.
Trung bình diện tích (chấm đỏ và số kèm theo) cho từng loại dao động quanh mức 3.187–3.201 ft² (Farmhouse nhỏ nhất: 3.135 ft²; Studio lớn nhất: 3.201 ft²).
Các boxplot cho thấy phân phối diện tích khá rộng, trải đều từ khoảng 300 ft² tới hơn 10.000 ft², nhưng đa số tập trung quanh trung vị (3650–4000 ft²).
Phân bố (hình violin) khá đồng nhất, không có loại hình nhà nào vượt trội về diện tích, thể hiện sự tiêu chuẩn hóa sản phẩm giữa các loại hình bất động sản hiện có.
Ý nghĩa thống kê
Khách hàng chọn mua bất kỳ loại hình nhà nào hầu như đều tiếp cận được diện tích phổ biến (3.100–3.200 ft²), thuận tiện cho việc thiết kế, sản xuất, xây dựng và marketing sản phẩm quy mô lớn.
Không có sự phân biệt rõ rệt về diện tích giữa Apartment, Independent House, Studio, Villa… phản ánh xu hướng tối ưu hóa không gian sống và sản xuất.
Doanh nghiệp có thể đồng bộ hóa nhiều dòng sản phẩm về diện tích để tiết giảm chi phí, đồng thời dễ dàng hoán đổi hoặc nâng cấp sản phẩm mà vẫn đáp ứng nhu cầu đại đa số khách hàng.
p17data <- df %>% group_by(Salary_Group) %>% summarise(prob_buy = mean(decision=="1"))
p17 <- ggplot(p17data, aes(x=Salary_Group, y=prob_buy, group=1)) +
geom_line(size=1.2, color="tomato") +
geom_point(size=3) +
geom_text(aes(label=scales::percent(prob_buy)), vjust=-0.4) +
scale_y_continuous(labels=percent) +
labs(title="(P17) Xác suất mua theo nhóm thu nhập",x="Nhóm Lương",y="Xác suất mua") +
scale_y_continuous(expand = expansion(mult = c(0, 0.1)))+
theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Scale for y is already present.
## Adding another scale for y, which will replace the existing scale.
p17
Đoạn code này dùng để tạo ra một Biểu đồ Đường (Line Plot).
Mục đích chính là để trực quan hóa xu hướng (trend) của Xác suất Mua (prob_buy) khi Nhóm Lương (Salary_Group) của khách hàng tăng lên. Biểu đồ này giúp xác định liệu có một mối quan hệ đồng biến (tỷ lệ mua tăng khi lương tăng) hay không.
Trong đó:
p17data <- df %>% group_by(…) %>% summarise(…): Bước Tiền xử lý. Nó tính toán Xác suất Mua Trung bình (prob_buy = mean(decision==“1”)) cho mỗi Nhóm Lương (Salary_Group) và lưu kết quả vào p17data.
ggplot(p17data, aes(x=Salary_Group, y=prob_buy, group=1)): Khởi tạo biểu đồ. Ánh xạ Nhóm Lương lên trục X, Xác suất mua lên trục Y. Tham số group=1 là bắt buộc để buộc geom_line nối tất cả các điểm thành một đường duy nhất, do trục X là một biến phân loại.
geom_line(size=1.2, color=“tomato”): Lớp vẽ đường thẳng. size=1.2 điều chỉnh độ dày của đường, và color=“tomato” đặt màu sắc của đường.
geom_point(size=3): Lớp vẽ các điểm dữ liệu chính xác tại từng Nhóm Lương trên đường.
geom_text(aes(label=scales::percent(prob_buy)), vjust=-0.4): Lớp nhãn số. Hiển thị giá trị phần trăm của xác suất mua ngay phía trên mỗi điểm.
scale_y_continuous(labels=percent): Lớp tùy chỉnh trục Y. Hàm percent (thuộc gói scales) định dạng nhãn trục Y dưới dạng tỷ lệ phần trăm (ví dụ: \(25\%\)).
scale_y_continuous(expand = expansion(mult = c(0, 0.1))): Lớp này là lớp cuối cùng (ghi đè lớp trên). Tham số expand = expansion(mult = c(0, 0.1)) thêm một khoảng đệm \(10\%\) ở phía trên trục Y, đảm bảo nhãn số (geom_text) không bị cắt.
labs(title=…, x=…, y=…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục X (Nhóm Lương), và nhãn trục Y (Xác suất mua).
theme_minimal(): Áp dụng phong cách tối giản.
Kết quả kĩ thuật
Biểu đồ line-point thể hiện rõ xác suất mua tăng dần theo nhóm thu nhập:
Nhóm lương thấp: 16,83%
Trung bình thấp: 22,12%
Trung bình cao: 26,21%
Cao: 26,98%
Độ dốc lớn nhất nằm giữa nhóm “Thấp” → “Trung bình thấp” và “Trung bình thấp” → “Trung bình cao”.
Đường màu cam và điểm đen làm nổi bật xu hướng tăng mạnh, đặc biệt là từ mức thấp lên trung.
Ý nghĩa thống kê
Thu nhập càng tăng, xác suất mua bất động sản càng lớn; những người ở nhóm lương cao gần như có khả năng mua gấp đôi nhóm lương thấp.
Thị trường có sự phân tầng rõ ràng theo năng lực tài chính, phù hợp thực tế nguồn cầu bất động sản hiện đại.
Doanh nghiệp, ngân hàng nên tập trung phát triển sản phẩm, chương trình tín dụng vào nhóm lương cao/trung bình cao để tối ưu chuyển đổi, đồng thời nghiên cứu giải pháp hỗ trợ tín dụng/thanh toán cho nhóm lương thấp để mở rộng thị phần.
avg_satis <- df %>%
group_by(property_type) %>%
summarise(avg_satisfaction = mean(satisfaction_score, na.rm = TRUE))
min_score <- min(avg_satis$avg_satisfaction) - 0.01
max_score <- max(avg_satis$avg_satisfaction) + 0.01
p18 <- ggplot(avg_satis,
aes(x = reorder(property_type, avg_satisfaction),
y = avg_satisfaction)) +
geom_segment(
aes(xend = reorder(property_type, avg_satisfaction),
y = min_score,
yend = avg_satisfaction),
color="gray"
) +
geom_point(aes(color = property_type), size = 4) +
geom_text(aes(label = round(avg_satisfaction, 3)), hjust = -0.3) +
coord_flip(ylim = c(min_score, max_score)) +
labs(
title = "(P18) So sánh Điểm hài lòng trung bình giữa các Loại hình BĐS",
x = "Loại hình Bất động sản",
y = "Điểm hài lòng trung bình"
) +
theme_minimal() +
theme(legend.position = "none") +
theme(plot.title = element_text(size = 8))
p18
Đoạn code này dùng để tạo ra một Biểu đồ Lollipop (Lollipop Chart) đã được phóng to (zoom).
Mục đích chính là trực quan hóa và xếp hạng sự khác biệt về Điểm hài lòng Trung bình (avg_satisfaction) giữa tất cả các Loại hình Bất động sản (property_type).
Việc sử dụng các biến min_score và max_score cùng với coord_flip(ylim = …) cho thấy mục tiêu là mở rộng trục Y (trục hiển thị điểm hài lòng) để làm nổi bật sự khác biệt nhỏ giữa các loại hình BĐS.
Trong đó:
avg_satis <- df %>% group_by(…) %>% summarise(…): Đây là bước tiền xử lý. Nó phân nhóm dữ liệu theo property_type và tính Trung bình (Mean) của điểm hài lòng (avg_satisfaction) cho mỗi loại hình BĐS.
min_score <- min(…) - 0.01 và max_score <- max(…) + 0.01: Các lệnh này tính toán giới hạn dưới và trên của trục Y (sau khi lật là trục X) bằng cách lấy giá trị nhỏ nhất và lớn nhất của điểm trung bình rồi thêm bớt \(0.01\) làm khoảng đệm.
aes(x = reorder(property_type, avg_satisfaction), y = avg_satisfaction): Ánh xạ thẩm mỹ chính. Hàm reorder() tự động sắp xếp các loại hình BĐS trên trục X theo thứ tự tăng dần của avg_satisfaction.
geom_segment(aes(xend = …, y = min_score, yend = avg_satisfaction)): Lớp vẽ đoạn thẳng (cái que của kẹo mút). Lớp này là then chốt cho hiệu ứng zoom vì nó bắt đầu vẽ từ vị trí min_score (giới hạn dưới của trục đã zoom) thay vì \(0\).
geom_point(aes(color = property_type), size = 4): Lớp vẽ dấu chấm (viên kẹo mút). Màu sắc được phân biệt theo property_type.
geom_text(aes(label = round(avg_satisfaction, 3)), hjust = -0.3): Lớp vẽ giá trị số. Hiển thị chính xác Điểm hài lòng Trung bình (làm tròn 3 chữ số) bên cạnh mỗi dấu chấm.
coord_flip(ylim = c(min_score, max_score)): Lớp Hệ tọa độ. Đây là lệnh quan trọng nhất. coord_flip() lật biểu đồ sang ngang. Tham số ylim = c(min_score, max_score) áp dụng giới hạn zoom đã tính toán lên trục hiển thị điểm số (trục Y cũ, nay là trục ngang).
labs(…): Đặt nhãn cho biểu đồ, bao gồm tiêu đề, nhãn trục X và Y.
theme_minimal() + theme(…): Áp dụng phong cách tối giản. Các lệnh theme điều chỉnh kích thước chữ tiêu đề (size = 8) và ẩn chú giải (legend.position = “none”).
Kết quả kĩ thuật
Các loại hình bất động sản có điểm hài lòng trung bình khá sát nhau, dao động từ 5.486 (Farmhouse) đến 5.516 (Apartment).
Apartment có điểm hài lòng trung bình cao nhất (5.516), theo sau là Villa (5.511), Studio (5.498), Independent House (5.492), Townhouse (5.489), thấp nhất là Farmhouse (5.486).
Chênh lệch giữa loại cao nhất và thấp nhất chỉ khoảng 0.03 điểm, biểu hiện qua vị trí các điểm màu gần nhau, chưa có loại hình nào vượt trội hẳn về mặt cảm nhận.
Ý nghĩa thống kê
Mức độ thỏa mãn khách hàng đối với từng loại hình bất động sản gần như tương đồng, cho thấy sản phẩm các loại hình đều đáp ứng tương đối ngang bằng nhu cầu/tâm lý khách.
Apartment và Villa được đánh giá hài lòng nhỉnh hơn, có thể do tiện lợi, dịch vụ hoặc môi trường sống; Farmhouse và Townhouse thấp nhất nhưng vẫn chênh không đáng kể.
Kết quả này giúp doanh nghiệp nhận diện: có thể tập trung đầu tư nâng trải nghiệm cho các loại điểm hài lòng chưa cao, nhưng không cần tách biệt chiến lược giữa các loại hình – sự khác biệt không lớn nên hiệu quả cải tiến có thể lan tỏa rộng.
df_pie_summary <- df %>%
count(property_type) %>%
mutate(prop = n / sum(n)) %>%
arrange(desc(property_type)) %>%
mutate(y_pos = cumsum(prop) - 0.5 * prop)
p19 <- ggplot(df_pie_summary, aes(x = "", y = prop, fill = property_type)) +
geom_bar(stat = "identity", width = 1, color = "white") +
coord_polar(theta = "y", start = 0) +
geom_text(aes(y = y_pos, label = scales::percent(prop, accuracy = 0.1)),
color = "black", size = 3.5) +
labs(title = "(P19) Phân bổ loại hình bất động sản",
fill = "Loại hình Bất động sản",
x = NULL, y = NULL) +
theme_void()
p19
Đoạn code này dùng để tạo ra một Biểu đồ Tròn Tỷ lệ (Pie Chart).
Mục đích chính là trực quan hóa và phân tích cơ cấu thị trường bằng cách hiển thị tỷ lệ phần trăm đóng góp của từng Loại hình Bất động sản (property_type) vào tổng số giao dịch. Biểu đồ này giúp xác định Loại hình Bất động sản nào phổ biến nhất hoặc chiếm thị phần lớn nhất.
Trong đó:
df_pie_summary <- df %>% …: Bước Tiền xử lý. Nó tính toán các giá trị cần thiết cho biểu đồ tròn:
count(property_type): Đếm số lượng (n) của mỗi loại hình BĐS.
mutate(prop = n / sum(n)): Tính tỷ lệ phần trăm (prop) của mỗi loại hình so với tổng thể.
mutate(y_pos = cumsum(prop) - 0.5 * prop): Tính toán vị trí đặt nhãn (y_pos) để các nhãn phần trăm nằm chính xác ở giữa mỗi “miếng bánh”.
ggplot(df_pie_summary, aes(x = ““, y = prop, fill = property_type)): Khởi tạo biểu đồ. Ánh xạ prop lên trục Y (chiều cao cột), và property_type lên màu tô (fill). Tham số x = “” là cách thiết lập trục X cho biểu đồ tròn.
geom_bar(stat = “identity”, width = 1, color = “white”): Lớp hình học chính. Sử dụng geom_bar với stat = “identity” để vẽ giá trị prop đã tính sẵn, và width = 1 để làm cho cột đầy đặn
coord_polar(theta = “y”, start = 0): Lớp Hệ tọa độ. Đây là lệnh biến biểu đồ cột thành Biểu đồ Tròn (Pie Chart) bằng cách sử dụng trục Y (cột) làm góc xoay.
geom_text(aes(y = y_pos, label = scales::percent(prop, accuracy = 0.1)), …): Lớp vẽ nhãn số (Text Label). Hiển thị giá trị phần trăm (dùng scales::percent) ngay trong mỗi miếng bánh, với vị trí được xác định bởi y_pos.
labs(fill = “Loại hình Bất động sản”, x = NULL, y = NULL): Đặt nhãn cho biểu đồ. fill = “Loại hình Bất động sản” tùy chỉnh tiêu đề của chú thích (Legend Title). x = NULL, y = NULL xóa nhãn và tiêu đề của hai trục.
theme_void(): Áp dụng phong cách hoàn toàn trống (void theme), xóa tất cả các yếu tố không liên quan đến dữ liệu (như trục, vạch chia, và lưới), làm nổi bật Biểu đồ Tròn.
Kết quả kĩ thuật
Biểu đồ tròn cho thấy 6 loại hình bất động sản (Apartment, Farmhouse, Independent House, Studio, Townhouse, Villa) được phân bổ gần như đồng đều.
Tỉ lệ từng loại dao động từ 16,5% đến 16,8%, chênh lệch rất nhỏ (tất cả các lát đều gần như bằng nhau).
Không có loại hình nào chiếm ưu thế tuyệt đối hoặc bị “lép vế” về số lượng; thể hiện tốt trên legend và nhãn càng tăng tính trực quan, thuyết phục.
Ý nghĩa thống kê
Thị trường bất động sản hiện đại hướng tới đa dạng hóa, việc các nhóm sản phẩm giữ quy mô tương đương giúp cân bằng cung – cầu và đa dạng hóa lựa chọn cho khách hàng.
Doanh nghiệp phân phối nhiều chủng loại sẽ tiếp cận tốt mọi phân khúc khách hàng, giảm rủi ro lệ thuộc vào một nhóm thị trường cố định.
Kết cấu này giúp thị trường vận hành linh hoạt, tối ưu hóa nguồn lực, khách hàng dễ dàng tìm được sản phẩm đúng nhu cầu, qua đó thúc đẩy thanh khoản và tăng hiệu quả kinh doanh toàn ngành.
color_scheme <- c("Không mua (0)" = "darkorange", "Mua (1)" = "deepskyblue4")
df_plot_simple <- df %>%
mutate(
Decision_Status = factor(decision, labels = c("Không mua (0)", "Mua (1)"))
)
p20 <- ggplot(df_plot_simple, aes(x = loan, y = price, color = Decision_Status)) +
geom_smooth(method = "lm",
aes(fill = Decision_Status),
alpha = 0.15) +
scale_y_continuous(labels = scales::comma) +
scale_x_continuous(labels = scales::comma) +
scale_color_manual(name = "Quyết định", values = color_scheme) +
scale_fill_manual(name = "Quyết định", values = color_scheme) +
labs(
title = "(P20) Mối quan hệ giữa Giá nhà và Khoản vay theo Quyết định",
x = "Khoản vay (Loan )",
y = "Giá (Price)"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(size = 8)) +
theme(legend.position = "bottom")
p20
## `geom_smooth()` using formula = 'y ~ x'
Đoạn code này dùng để tạo ra một Biểu đồ Hồi quy Phân biệt (Discriminative Regression Plot).
Mục đích chính là trực quan hóa và so sánh xu hướng tuyến tính giữa Giá nhà và Khoản vay giữa hai nhóm khách hàng: “Không mua” và “Mua”. Biểu đồ chỉ hiển thị hai đường hồi quy cùng với vùng tin cậy, đây là cách làm tối ưu cho dữ liệu lớn để tránh tình trạng rối hình.
Trong đó:
color_scheme <- …: Đây là toán tử gán, định nghĩa một vector màu cụ thể (darkorange cho ‘0’, deepskyblue4 cho ‘1’) được lưu vào biến color_scheme để đảm bảo tính nhất quán của màu sắc.
df_plot_simple <- df %>% mutate(…): Đây là bước tiền xử lý, tạo ra cột Decision_Status dạng factor với nhãn rõ ràng (“Không mua (0)”, “Mua (1)”) từ cột decision gốc.
ggplot(df_plot_simple, aes(x = loan, y = price, color = Decision_Status)): Khởi tạo biểu đồ. Ánh xạ Khoản vay lên trục X, Giá lên trục Y, và Decision_Status lên màu sắc (color) của các đường.
geom_smooth(method = “lm”, aes(fill = Decision_Status), alpha = 0.15): Lớp hình học chính, vẽ Đường Hồi quy Tuyến tính (LM). Tham số method = “lm” chỉ định sử dụng mô hình tuyến tính. Tham số aes(fill = …) cho phép tạo ra vùng tin cậy (fill) riêng biệt cho mỗi nhóm.
scale_y_continuous(labels = scales::comma) và scale_x_continuous(labels = scales::comma): Tùy chỉnh hai trục. Hàm scales::comma được sử dụng để định dạng nhãn trục X và Y (tiền tệ) bằng dấu phân cách hàng nghìn.
scale_color_manual(…) và scale_fill_manual(…): Tùy chỉnh thủ công màu sắc và nhãn của chú giải. Lệnh này đảm bảo rằng tiêu đề chú giải là “Quyết định” và các nhãn “0”, “1” được đổi thành “Không mua”, “Mua” cho cả đường hồi quy (color) và vùng tin cậy (fill).
labs(title=…, x=…, y=…): Đặt nhãn cho biểu đồ, bao gồm Tiêu đề chính, nhãn trục X và Y.
theme_minimal(base_size = 13) + theme(…): Áp dụng phong cách tối giản. Các lệnh theme(…) được sử dụng để điều chỉnh kích thước tiêu đề chính (size = 8) và đặt chú giải xuống dưới (legend.position = “bottom”).
Kết quả kĩ thuật
Biểu đồ thể hiện mối liên hệ cực mạnh, gần như tuyến tính tuyệt đối giữa khoản vay (trục x) và giá nhà (trục y); cả hai nhóm “Mua (1)” và “Không mua (0)” đều nằm chồng lấn gần như tuyệt đối lên nhau.
Các đường hồi quy (lm) của hai nhóm có độ dốc gần như tương đồng trên toàn bộ dải dữ liệu, không có sự tách biệt đáng kể nào giữa hai hành vi về kỹ thuật tài chính.
Khoản vay tăng thì giá nhà tăng gần như tỷ lệ thuận, bất kể kết quả quyết định mua; mẫu quan sát phủ đều toàn trục, xác nhận quan hệ tài chính bền vững giữa giá trị vay và sản phẩm thật.
Ý nghĩa thống kê
Số tiền khách hàng sẵn sàng hoặc được duyệt vay luôn gắn sát với giá trị tài sản thực, cho thấy chính sách phê duyệt tín dụng và hành vi tài chính thị trường rất hợp lý, hạn chế rủi ro “bong bóng” vay mượn vượt giá trị tài sản.
Không có sự phân biệt lớn về mối quan hệ giá-vay giữa khách hàng mua thực thụ và nhóm ngừng mua, cho thấy yếu tố quyết định mua không nằm ở cấu trúc khoản vay hay tỷ suất tài trợ, mà chủ yếu ở các yếu tố khác (hài lòng, trải nghiệm, kỳ vọng, rào cản thủ tục…).
Nhà phát triển, tổ chức tín dụng có thể tự tin xác định khung vay theo cơ chế tỷ lệ chặt chẽ so với giá nhà, đồng thời tập trung vào giải pháp nâng trải nghiệm/quyết định cho khách thay vì điều chỉnh cấu trúc vay.
Bài tiểu luận này được thực hiện dựa trên cơ sở phân tích bộ số liệu báo cáo tài chính (BCTC) của HVN. Nguồn dữ liệu được sử dụng bao gồm các thành phần cốt lõi của một bộ báo cáo tài chính toàn diện, bao gồm: Bảng cân đối kế toán (Balance Sheet), Báo cáo kết quả hoạt động kinh doanh (Income Statement), Báo cáo lưu chuyển tiền tệ (Cash Flow) và các thông tin chi tiết từ Thuyết minh báo cáo tài chính (Note).
Các tệp dữ liệu này cung cấp thông tin tài chính chi tiết của doanh nghiệp, được tổng hợp theo cả năm và quý trong giai đoạn từ 2017 đến 2025. Thông qua việc phân tích sâu bộ dữ liệu thực tiễn này, bài viết sẽ đánh giá tình hình sức khỏe tài chính, hiệu quả hoạt động và các dòng tiền của công ty trong suốt giai đoạn nghiên cứu
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.1
## Warning: package 'tidyr' was built under R version 4.5.1
## Warning: package 'stringr' was built under R version 4.5.1
## Warning: package 'lubridate' was built under R version 4.5.1
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ stringr 1.5.2
## ✔ lubridate 1.9.4 ✔ tibble 3.2.1
## ✔ purrr 1.0.4 ✔ tidyr 1.3.1
## ✔ readr 2.1.5
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ readr::col_factor() masks scales::col_factor()
## ✖ purrr::discard() masks scales::discard()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
## Warning: package 'readxl' was built under R version 4.5.1
library(lubridate)
library(zoo)
##
## Attaching package: 'zoo'
##
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
library(scales)
library(skimr)
## Warning: package 'skimr' was built under R version 4.5.2
library(broom)
library(GGally)
library(ggridges)
library(patchwork)
## Warning: package 'patchwork' was built under R version 4.5.1
library(moments)
library(knitr)
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.5.2
##
## Attaching package: 'kableExtra'
##
## The following object is masked from 'package:dplyr':
##
## group_rows
library(tseries)
## Warning: package 'tseries' was built under R version 4.5.1
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
library(ggplot2)
library(ggrepel)
## Warning: package 'ggrepel' was built under R version 4.5.2
df_raw <- read_excel("C:/Users/Admin/Downloads/HVN_BCTC.xlsx", sheet = "Income Statement")
## New names:
## • `` -> `...1`
## • `` -> `...10`
Đoạn code này là một tập lệnh Khởi tạo môi trường. Mục đích chính là nạp (load) tất cả các thư viện (gói) cần thiết cho một quy trình phân tích dữ liệu toàn diện, và sau đó nhập dữ liệu thô từ một file Excel vào R.
Trong đó:
library(tidyverse): Nạp bộ sưu tập các gói “lõi” (như dplyr, ggplot2, readr), là hạt nhân cho việc thao tác (ví dụ: mutate, group_by) và trực quan hóa dữ liệu.
library(readxl): Nạp gói readxl, cung cấp hàm read_excel() để đọc dữ liệu trực tiếp từ các file .xls hoặc .xlsx.
library(lubridate) và library(zoo): Nạp các gói chuyên dụng để xử lý, thao tác, và phân tích các biến Ngày tháng (Date) và Chuỗi thời gian (Time Series).
library(scales): Nạp gói scales, cung cấp các hàm định dạng trục (như comma(), percent()) cho các biểu đồ ggplot2.
library(skimr), library(moments), library(tseries): Nạp các gói thống kê. skimr (để tóm tắt dữ liệu), moments (để tính độ lệch/nhọn), tseries (để kiểm định chuỗi thời gian).
library(broom): Nạp gói broom, cung cấp hàm tidy() để chuyển đổi kết quả thống kê (như t.test, glm) sang data frame gọn gàng.
library(GGally), library(ggridges), library(patchwork), library(ggrepel): Nạp các gói mở rộng của ggplot2 để tạo các biểu đồ phức tạp (ma trận ggpairs, lược sóng ggridges, gộp biểu đồ patchwork, và tránh chồng chéo nhãn ggrepel).
library(knitr) và library(kableExtra): Nạp các gói để tạo bảng (kable()) và tùy chỉnh bảng (kableExtra) cho báo cáo R Markdown.
df_raw <- …: Toán tử gán, lưu dữ liệu đã đọc vào đối tượng df_raw.
read_excel(“C:/Users/Admin/Downloads/HVN_BCTC.xlsx”, sheet = “Income Statement”): Hàm chính để đọc file.
“C:/Users/Admin/Downloads/HVN_BCTC.xlsx”: Tham số đường dẫn. Đây là một đường dẫn tuyệt đối, chỉ định vị trí chính xác của file Excel.
sheet = “Income Statement”: Tham số chỉ định, yêu cầu R đọc dữ liệu từ sheet có tên là “Sheet1” bên trong file Excel đó.
Kết quả kĩ thuật
Sau khi chạy thành công, 16 gói thư viện đã được nạp vào bộ nhớ R. Dữ liệu từ file “HVN_BCTC.xlsx” (từ “Income Statement”) được đọc và lưu vào một đối tượng data frame có tên là df_raw.
Ý nghĩa thống kê
Đây là bước Chuẩn bị môi trường và nhập dữ liệun. Về mặt thống kê, chưa có phân tích nào diễn ra, nhưng dữ liệu thô (raw data) hiện đã sẵn sàng trong R để bắt đầu các bước làm sạch (cleaning), tiền xử lý (preprocessing), thống kê mô tả (EDA), và xây dựng mô hình.
dim(df_raw)
## [1] 25 44
Lệnh dim(df_raw) là một hàm cơ sở (base R function) có mục đích xác định kích thước của đối tượng data.frame có tên là df_raw.
Kết quả trả về là một vector số nguyên, trong đó giá trị đầu tiên là số hàng (rows/quan sát) và giá trị thứ hai là số cột (columns/biến).
Trong đó:
dim(): Đây là hàm cơ sở (base function) của R. Mục đích là để lấy thuộc tính “dimension” (kích thước) của một đối tượng.
df_raw: Đây là tham số bắt buộc. Mục đích là chỉ định đối tượng data.frame đã được nạp ở bước trước, mà chúng ta muốn kiểm tra kích thước.
Kết quả kĩ thuật
Kết quả lệnh dim(df_raw) trả về [1] 25 44, nghĩa là bảng dữ liệu đang có 25 dòng (quan sát) và 44 biến (trường thông tin).
Số lượng biến phong phú, phản ánh dữ liệu tổng hợp đa chiều, hỗ trợ nhiều phân tích thống kê, kiểm định, minh họa biến động hoặc so sánh nhóm chỉ tiêu khác nhau.
Ý nghĩa thống kê
Với 44 trường, dữ liệu phù hợp đánh giá toàn diện các mặt tài chính, hoạt động kinh doanh, giúp nhận diện đầy đủ nguồn lực, hiệu quả và rủi ro của doanh nghiệp.
Tuy số quan sát còn hơi ít (25), nhưng cấu trúc này vẫn hữu ích cho các bài tập mô phỏng báo cáo, phân tích đặc trưng, kiểm nghiệm phương pháp phân tích. Các kết luận nên cân nhắc giới hạn về đại diện mẫu khi làm thực tế.
head(df_raw,5)
## # A tibble: 5 × 44
## ...1 `2017` `2018` `2019` `2020` `2021` `2022` `2023` `2024`
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Doanh… 1.30e12 1.69e12 1.79e12 1.69e12 1.89e12 2.01e12 2.18e12 2.79e12
## 2 Các k… 0 0 0 0 0 0 0 0
## 3 Doanh… 1.30e12 1.69e12 1.79e12 1.69e12 1.89e12 2.01e12 2.18e12 2.79e12
## 4 Giá v… -8.95e11 -1.20e12 -1.36e12 -1.24e12 -1.29e12 -1.35e12 -1.53e12 -1.94e12
## 5 Lợi n… 4.08e11 4.96e11 4.37e11 4.49e11 6.02e11 6.59e11 6.54e11 8.43e11
## # ℹ 35 more variables: ...10 <lgl>, `Q1 2017` <dbl>, `Q2 2017` <dbl>,
## # `Q3 2017` <dbl>, `Q4 2017` <dbl>, `Q1 2018` <dbl>, `Q2 2018` <dbl>,
## # `Q3 2018` <dbl>, `Q4 2018` <dbl>, `Q1 2019` <dbl>, `Q2 2019` <dbl>,
## # `Q3 2019` <dbl>, `Q4 2019` <dbl>, `Q1 2020` <dbl>, `Q2 2020` <dbl>,
## # `Q3 2020` <dbl>, `Q4 2020` <dbl>, `Q1 2021` <dbl>, `Q2 2021` <dbl>,
## # `Q3 2021` <dbl>, `Q4 2021` <dbl>, `Q1 2022` <dbl>, `Q2 2022` <dbl>,
## # `Q3 2022` <dbl>, `Q4 2022` <dbl>, `Q1 2023` <dbl>, `Q2 2023` <dbl>, …
Lệnh head(df_raw, 5) là một bước khám phá dữ liệu ban đầu. Mục đích là xem 5 hàng đầu tiên của data.frame (df_raw) để thực hiện kiểm tra sơ bộ, xác minh trực quan rằng dữ liệu đã được nạp chính xác, và kiểm tra định dạng, kiểu dữ liệu và phạm vi giá trị của các biến.
Hàm được sử dụng là head(), một hàm cơ bản (base R) dùng để trả về các phần tử đầu tiên (hàng) của một đối tượng.
Trong đó:
head(): Hàm cơ sở của R. Mục đích là để trích xuất và hiển thị các hàng đầu tiên của một đối tượng dữ liệu.
df_raw: Tham số bắt buộc (dữ liệu).Chỉ định đối tượng data.frame cần được khám phá.
5: Tham số tùy chọn (số lượng hàng). Giới hạn số lượng hàng hiển thị chỉ còn 5 hàng, giúp bảng tóm tắt gọn gàng.
Kết quả kĩ thuật
Lệnh head(df_raw,5) trả về 5 dòng dữ liệu đầu tiên giúp kiểm tra cấu trúc bảng, các trường tên biến, định dạng giá trị cũng như độ đầy đủ của dữ liệu.
Việc này hỗ trợ phát hiện sớm lỗi định dạng, loại biến (text, numeric, date…), đảm bảo các thao tác biến đổi, tổng hợp, trực quan về sau sẽ chính xác.
Ý nghĩa thống kê
Xem nhanh 5 dòng đầu tiên phản ánh đặc điểm cơ bản của doanh nghiệp, các chỉ tiêu trọng yếu, tư duy nhập liệu, hành vi biến động giá trị tài chính ở các kỳ đầu.
Thông tin bước đầu này giúp xây dựng bối cảnh tổng quan, định hướng các ý tưởng phân tích sâu hơn về hiệu quả kinh doanh, cơ cấu tài sản, dòng tiền hoặc xu hướng vận hành chiến lược tài chính doanh nghiệp.
tail(df_raw,5)
## # A tibble: 5 × 44
## ...1 `2017` `2018` `2019` `2020` `2021` `2022` `2023` `2024` ...10
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>
## 1 Lãi/(lỗ… 2.64e11 3.54e11 2.86e11 2.96e11 4.14e11 3.93e11 1.99e11 4.35e11 NA
## 2 Lợi ích… 2.65e10 5.38e10 5.18e10 5.67e10 6.39e10 7.88e10 7.54e10 9.84e10 NA
## 3 Lợi nhu… 2.37e11 3.00e11 2.34e11 2.40e11 3.50e11 3.14e11 1.24e11 3.36e11 NA
## 4 Lãi cơ … 4.26e 3 4.90e 3 3.83e 3 3.91e 3 5.39e 3 2.45e 3 8.44e 2 1.39e 3 NA
## 5 Lãi trê… 0 0 0 0 5.39e 3 2.45e 3 8.44e 2 1.39e 3 NA
## # ℹ 34 more variables: `Q1 2017` <dbl>, `Q2 2017` <dbl>, `Q3 2017` <dbl>,
## # `Q4 2017` <dbl>, `Q1 2018` <dbl>, `Q2 2018` <dbl>, `Q3 2018` <dbl>,
## # `Q4 2018` <dbl>, `Q1 2019` <dbl>, `Q2 2019` <dbl>, `Q3 2019` <dbl>,
## # `Q4 2019` <dbl>, `Q1 2020` <dbl>, `Q2 2020` <dbl>, `Q3 2020` <dbl>,
## # `Q4 2020` <dbl>, `Q1 2021` <dbl>, `Q2 2021` <dbl>, `Q3 2021` <dbl>,
## # `Q4 2021` <dbl>, `Q1 2022` <dbl>, `Q2 2022` <dbl>, `Q3 2022` <dbl>,
## # `Q4 2022` <dbl>, `Q1 2023` <dbl>, `Q2 2023` <dbl>, `Q3 2023` <dbl>, …
Lệnh này có mục đích xem 5 hàng cuối cùng của dữ liệu (df_raw). Đây là bước kiểm tra dữ liệu quan trọng trong giai đoạn khám phá, giúp người nghiên cứu phát hiện bất thường, lỗi dữ liệu có thể xuất hiện ở cuối tệp tin sau quá trình thu thập hoặc xuất file.
Trong đó:
tail(): Đây là hàm cơ sở của R. Mục đích là để trích xuất và hiển thị các hàng cuối cùng của một đối tượng dữ liệu.
df_raw: Tham số bắt buộc (dữ liệu). Chỉ định đối tượng data.frame cần được kiểm tra.
5: Tham số tùy chọn (số lượng hàng). Giới hạn số lượng hàng hiển thị chỉ còn 5 hàng cuối cùng.
Kết quả kĩ thuật
Việc dùng tail(df_raw, 5) giúp kiểm tra nhanh 5 dòng dữ liệu cuối, xác nhận tính đầy đủ, cập nhật và bám sát thực tế của bộ dữ liệu.
Có thể phát hiện các lỗi nhập liệu, trường dữ liệu đặc biệt hoặc các giá trị bất thường xuất hiện vào cuối chuỗi, hỗ trợ kiểm định chất lượng trước khi phân tích sâu.
Ý nghĩa thống kê
Theo dõi 5 quan sát cuối giúp nhận diện diễn biến mới nhất của các chỉ số tài chính, phản ánh xu thế hoặc biến động vào cuối kỳ – cơ sở cho nhà quản trị, đầu tư đưa ra quyết định kịp thời trong bối cảnh thị trường luôn thay đổi.
Khả năng rà soát nhanh trạng thái cuối cùng của dữ liệu đảm bảo báo cáo tài chính HVN luôn được cập nhật, nâng cao độ tin cậy cho mọi đánh giá hiệu quả hoạt động, dự báo và chiến lược tài chính doanh nghiệp.
names(df_raw)
## [1] "...1" "2017" "2018" "2019" "2020" "2021" "2022"
## [8] "2023" "2024" "...10" "Q1 2017" "Q2 2017" "Q3 2017" "Q4 2017"
## [15] "Q1 2018" "Q2 2018" "Q3 2018" "Q4 2018" "Q1 2019" "Q2 2019" "Q3 2019"
## [22] "Q4 2019" "Q1 2020" "Q2 2020" "Q3 2020" "Q4 2020" "Q1 2021" "Q2 2021"
## [29] "Q3 2021" "Q4 2021" "Q1 2022" "Q2 2022" "Q3 2022" "Q4 2022" "Q1 2023"
## [36] "Q2 2023" "Q3 2023" "Q4 2023" "Q1 2024" "Q2 2024" "Q3 2024" "Q4 2024"
## [43] "Q1 2025" "Q2 2025"
Lệnh names(df_raw) là một bước khám phá dữ liệu cơ bản. Mục đích là liệt kê tất cả tên cột có trong đối tượng data.frame (df).
Việc này giúp người nghiên cứu xác nhận các tên cột đã được đọc vào chính xác từ tệp excel và chuẩn bị cho việc lựa chọn, đổi tên, hoặc truy cập các biến trong các lệnh phân tích tiếp theo.
Trong đó:
names(): Đây là hàm cơ sở của R. Mục đích là để truy xuất vector chứa các tên cột của đối tượng dữ liệu.
df_raw: Tham số bắt buộc. Chỉ định đối tượng data.frame mà chúng ta muốn lấy danh sách tên cột.
Kết quả kĩ thuật
Kết quả lệnh names(df_raw) cho thấy tập dữ liệu có các trường thông tin đa dạng bao gồm các năm, các quý (Q1 2017, Q3 2018, Q4 2024…).
Tên biến thể hiện rõ cấu trúc dạng chuỗi thời gian (theo năm, theo quý), thuận lợi khi thực hiện các thao tác phân tích chuỗi số liệu thời gian, so sánh, tổng hợp và trực quan hóa báo cáo tài chính.
Ý nghĩa thống kê
Bộ trường dữ liệu gồm cả năm và các quý giúp phân tích tài chính cực kỳ linh hoạt: có thể đánh giá xu hướng mùa vụ, biến động theo từng kỳ tài chính, xác định điểm tăng hay giảm, so với các năm trước.
Việc đa dạng hóa trường thông tin bảo đảm góc nhìn toàn diện về hoạt động kinh doanh, từ đó hỗ trợ đánh giá hiệu quả, thiết lập chiến lược và dự báo cho từng giai đoạn cụ thể, phù hợp với thực tế vận hành doanh nghiệp.
sapply(df_raw, function(x) sum(!is.na(x)))
## ...1 2017 2018 2019 2020 2021 2022 2023 2024 ...10
## 25 25 25 25 25 25 25 25 25 0
## Q1 2017 Q2 2017 Q3 2017 Q4 2017 Q1 2018 Q2 2018 Q3 2018 Q4 2018 Q1 2019 Q2 2019
## 25 25 25 25 25 25 25 25 25 25
## Q3 2019 Q4 2019 Q1 2020 Q2 2020 Q3 2020 Q4 2020 Q1 2021 Q2 2021 Q3 2021 Q4 2021
## 25 25 25 25 25 25 25 25 25 25
## Q1 2022 Q2 2022 Q3 2022 Q4 2022 Q1 2023 Q2 2023 Q3 2023 Q4 2023 Q1 2024 Q2 2024
## 25 25 25 25 25 25 25 25 25 25
## Q3 2024 Q4 2024 Q1 2025 Q2 2025
## 25 25 25 25
Mục đích chính của lệnh này là để kiểm tra chất lượng dữ liệu. Nó thực hiện việc đếm số lượng ô dữ liệu không bị khuyết (tức là không phải NA) cho tất cả cột) trong data frame df_raw.
Trong đó:
sapply(X, FUN): Đây là hàm chính.
X = df_raw: Đây là tham số đầu tiên, bắt buộc. Nó đại diện cho đối tượng mà hàm sẽ lặp qua. Trong trường hợp này, df_raw là một data.frame, và sapply sẽ tự động lặp qua từng cột của data.frame này.
FUN = function(x) sum(!is.na(x)): Đây là tham số thứ hai, bắt buộc. Nó là hàm (function) sẽ được áp dụng cho từng phần tử (từng cột) của X.
function(x): Đây là một hàm ẩn danh. x ở đây là một tham số nội bộ, nó sẽ đại diện cho từng cột khi sapply lặp.
is.na(x): Hàm này được gọi bên trong hàm ẩn danh. Nó nhận vector cột x làm tham số, trả về một vector logic (TRUE/FALSE) có cùng độ dài, đánh dấu TRUE cho mỗi giá trị NA (khuyết).
!: Đây là toán tử phủ định (NOT). Nó đảo ngược vector logic từ is.na(x). Kết quả là TRUE cho các giá trị không khuyết và FALSE cho các giá trị bị khuyết.
sum(…): Hàm sum được áp dụng cho vector logic đã phủ định. Trong R, sum coi TRUE là 1 và FALSE là 0. Vì vậy, lệnh này thực chất là đếm số lượng giá trị TRUE, tức là đếm số quan sát không khuyết trong cột x.
Kết quả kĩ thuật
Kết quả hàm sapply(df_raw, function(x) sum(!is.na(x))) cho thấy tất cả các trường đều có đúng 25 giá trị không NA (đủ dữ liệu cho mỗi biến).
Điều này chứng tỏ dữ liệu đầu vào đã sạch, không có dòng bị thiếu cho bất kỳ trường nào, rất thuận lợi cho tổng hợp, phân tích thống kê, mô hình hóa.
Việc nhất quán số dòng không bị thiếu giúp tránh lỗi khi xử lý, lập các bảng hoặc truy xuất các trường thông tin.
Ý nghĩa kinh tế
Không có giá trị thiếu đồng nghĩa kết quả phân tích, báo cáo quản trị sẽ đầy đủ, minh bạch, đáng tin cậy — hỗ trợ mạnh cho việc đánh giá, so sánh hoặc đề xuất kế hoạch kinh doanh, chiến lược tài chính trong doanh nghiệp.
str(df_raw)
## tibble [25 × 44] (S3: tbl_df/tbl/data.frame)
## $ ...1 : chr [1:25] "Doanh thu bán hàng và cung cấp dịch vụ" "Các khoản giảm trừ doanh thu" "Doanh thu thuần" "Giá vốn hàng bán" ...
## $ 2017 : num [1:25] 1.30e+12 0.00 1.30e+12 -8.95e+11 4.08e+11 ...
## $ 2018 : num [1:25] 1.69e+12 0.00 1.69e+12 -1.20e+12 4.96e+11 ...
## $ 2019 : num [1:25] 1.79e+12 0.00 1.79e+12 -1.36e+12 4.37e+11 ...
## $ 2020 : num [1:25] 1.69e+12 0.00 1.69e+12 -1.24e+12 4.49e+11 ...
## $ 2021 : num [1:25] 1.89e+12 0.00 1.89e+12 -1.29e+12 6.02e+11 ...
## $ 2022 : num [1:25] 2.01e+12 0.00 2.01e+12 -1.35e+12 6.59e+11 ...
## $ 2023 : num [1:25] 2.18e+12 0.00 2.18e+12 -1.53e+12 6.54e+11 ...
## $ 2024 : num [1:25] 2.79e+12 0.00 2.79e+12 -1.94e+12 8.43e+11 ...
## $ ...10 : logi [1:25] NA NA NA NA NA NA ...
## $ Q1 2017: num [1:25] 2.70e+11 0.00 2.70e+11 -1.84e+11 8.52e+10 ...
## $ Q2 2017: num [1:25] 3.37e+11 0.00 3.37e+11 -2.33e+11 1.04e+11 ...
## $ Q3 2017: num [1:25] 3.52e+11 0.00 3.52e+11 -2.39e+11 1.13e+11 ...
## $ Q4 2017: num [1:25] 3.44e+11 0.00 3.44e+11 -2.39e+11 1.05e+11 ...
## $ Q1 2018: num [1:25] 3.67e+11 0.00 3.67e+11 -2.58e+11 1.09e+11 ...
## $ Q2 2018: num [1:25] 4.30e+11 0.00 4.30e+11 -2.85e+11 1.44e+11 ...
## $ Q3 2018: num [1:25] 4.48e+11 0.00 4.48e+11 -3.27e+11 1.21e+11 ...
## $ Q4 2018: num [1:25] 4.51e+11 0.00 4.51e+11 -3.29e+11 1.22e+11 ...
## $ Q1 2019: num [1:25] 4.23e+11 0.00 4.23e+11 -3.24e+11 1.00e+11 ...
## $ Q2 2019: num [1:25] 4.74e+11 0.00 4.74e+11 -3.70e+11 1.04e+11 ...
## $ Q3 2019: num [1:25] 4.58e+11 0.00 4.58e+11 -3.41e+11 1.17e+11 ...
## $ Q4 2019: num [1:25] 4.37e+11 0.00 4.37e+11 -3.22e+11 1.16e+11 ...
## $ Q1 2020: num [1:25] 4.09e+11 0.00 4.09e+11 -3.07e+11 1.02e+11 ...
## $ Q2 2020: num [1:25] 3.93e+11 0.00 3.93e+11 -2.98e+11 9.49e+10 ...
## $ Q3 2020: num [1:25] 4.29e+11 0.00 4.29e+11 -3.19e+11 1.10e+11 ...
## $ Q4 2020: num [1:25] 4.59e+11 0.00 4.59e+11 -3.24e+11 1.34e+11 ...
## $ Q1 2021: num [1:25] 4.35e+11 0.00 4.35e+11 -3.17e+11 1.19e+11 ...
## $ Q2 2021: num [1:25] 4.77e+11 0.00 4.77e+11 -3.24e+11 1.53e+11 ...
## $ Q3 2021: num [1:25] 4.74e+11 0.00 4.74e+11 -3.07e+11 1.67e+11 ...
## $ Q4 2021: num [1:25] 5.07e+11 0.00 5.07e+11 -3.42e+11 1.64e+11 ...
## $ Q1 2022: num [1:25] 4.69e+11 0.00 4.69e+11 -3.07e+11 1.62e+11 ...
## $ Q2 2022: num [1:25] 5.12e+11 0.00 5.12e+11 -3.34e+11 1.78e+11 ...
## $ Q3 2022: num [1:25] 5.07e+11 0.00 5.07e+11 -3.42e+11 1.65e+11 ...
## $ Q4 2022: num [1:25] 5.20e+11 0.00 5.20e+11 -3.67e+11 1.52e+11 ...
## $ Q1 2023: num [1:25] 4.67e+11 -3.73e+09 4.63e+11 -3.27e+11 1.37e+11 ...
## $ Q2 2023: num [1:25] 5.31e+11 3.73e+09 5.35e+11 -3.86e+11 1.49e+11 ...
## $ Q3 2023: num [1:25] 5.57e+11 0.00 5.57e+11 -3.78e+11 1.79e+11 ...
## $ Q4 2023: num [1:25] 6.26e+11 0.00 6.26e+11 -4.36e+11 1.90e+11 ...
## $ Q1 2024: num [1:25] 5.86e+11 0.00 5.86e+11 -3.83e+11 2.04e+11 ...
## $ Q2 2024: num [1:25] 7.18e+11 0.00 7.18e+11 -4.96e+11 2.21e+11 ...
## $ Q3 2024: num [1:25] 7.09e+11 0.00 7.09e+11 -4.89e+11 2.20e+11 ...
## $ Q4 2024: num [1:25] 7.75e+11 0.00 7.75e+11 -5.77e+11 1.98e+11 ...
## $ Q1 2025: num [1:25] 6.82e+11 0.00 6.82e+11 -4.55e+11 2.28e+11 ...
## $ Q2 2025: num [1:25] 8.07e+11 0.00 8.07e+11 -5.53e+11 2.54e+11 ...
Hàm str(df_raw) trong R giúp hiển thị cấu trúc bên trong của dataframe (df_raw).
Trong đó:
str(object):
object: Là đối tượng R mà bạn muốn kiểm tra.
Trong trường hợp của bạn, df_raw là đối tượng đó. Lệnh này không có tham số phức tạp; nó chỉ đơn giản là yêu cầu R “mô tả” (describe) df_raw.
Kết quả kĩ thuật
Biến đầu tiên kiểu ký tự (chr) gồm 25 tên chỉ số như “Doanh thu bán hàng và cung cấp dịch vụ”, “Doanh thu thuần”, “Giá vốn hàng bán”,… đại diện cho từng ngành nội dung phân tích.
Các biến tiếp theo (các năm như X2017, X2018,…, các quý như Q1.2017, Q2.2017…) là kiểu số thực (num)
Ý nghĩa kinh tế
Bảng cấu trúc này cho phép so sánh và tổng hợp nhiều chỉ số tài chính/hoạt động trên nhiều ngành, nhiều mốc thời gian: phù hợp với phân tích đa chiều như bảng cân đối, báo cáo kết quả kinh doanh.
Số liệu lớn như 1.30e+12, 4.08e+11 (giá trị doanh thu, chi phí,…) thể hiện khả năng phân tích với quy mô tập đoàn/doanh nghiệp lớn, phù hợp cho đánh giá xu hướng, dự báo cũng như so sánh các ngành. ### 1.8. Chuẩn hoá tên biến
names(df_raw) <- str_trim(names(df_raw))
names(df_raw) <- make.names(names(df_raw))
Mục đích chung của hai lệnh này là để đảm bảo tính nhất quán và tính hợp lệ về mặt cú pháp của tất cả tên cột trong data frame df_raw.
Trong đó:
Lệnh 1: names(df_raw) <- str_trim(names(df_raw))
names(df_raw): (Hàm base R). Khi được gọi ở vế phải, nó trả về một vector character chứa tất cả tên cột.
str_trim(string, side = “both”): Hàm từ package stringr.
string = names(df_raw): (Tham số bắt buộc). Đây là vector character (tên cột) mà hàm sẽ tác động lên.
side = “both”: (Tham số tùy chọn, được mặc định). Chỉ định rằng hàm sẽ cắt (trim) khoảng trắng ở cả hai phía (trái và phải) của chuỗi.
<-: (Toán tử gán). Kết quả (vector tên cột đã được làm sạch khoảng trắng) được gán ngược trở lại, ghi đè lên tên cột cũ của df_raw.
Lệnh 2: names(df_raw) <- make.names(names(df_raw))
make.names(names, unique = FALSE): Hàm base R.
names = names(df_raw): (Tham số bắt buộc). Nó nhận vector tên cột (lúc này đã được str_trim ở lệnh 1) làm đầu vào.
unique = FALSE: (Tham số tùy chọn, được mặc định). Tham số này chỉ định rằng hàm không cần phải ép các tên cột phải là duy nhất.
<-: (Toán tử gán). Ghi đè tên cột một lần nữa bằng phiên bản “an toàn về mặt cú pháp”.
Kết quả kĩ thuật
Các thao tác str_trim(names(df_raw)) và make.names(names(df_raw)) giúp loại bỏ khoảng trắng thừa và chuẩn hóa tên biến về dạng an toàn, không trùng lặp, không chứa ký tự đặc biệt, phù hợp chuẩn biến trong R.
Nhờ xử lý này, mọi thao tác gọi biến, truy xuất dữ liệu, lập bảng hoặc xây dựng mô hình đều giảm hẳn nguy cơ lỗi do tên biến sai hoặc ký tự không hợp lệ, đảm bảo code chạy mượt với mọi trường hợp.
Ý nghĩa thống kê
Bằng cách chuẩn hóa tên cột ngay từ đầu, bạn đảm bảo rằng mọi lệnh gọi biến sau này sẽ luôn chính xác, giúp code dễ đọc và dễ tái lập hơn.
Đây là một quy trình thực hành tốt bắt buộc trong lập trình R. Nó đảm bảo code của bạn ổn định. Nếu file gốc vô tình bị chỉnh sửa (ví dụ: một người nào đó đổi tên cột Value thành ” Value ” (có dấu cách)), code của bạn vẫn sẽ chạy mà không bị lỗi.
if(!"Date" %in% names(df_raw)) {
message("Không tìm thấy cột Date — thử tạo từ Year & Month nếu có")
}
## Không tìm thấy cột Date — thử tạo từ Year & Month nếu có
Mục đích của khối code này là thực hiện một bước kiểm tra phòng vệ hay xác thực dữ liệu.
Cụ thể, nó kiểm tra xem trong data frame df_raw có tồn tại một cột tên là “Date” hay không. Nếu không tìm thấy, nó sẽ chủ động in ra một thông báo (message) cho người dùng biết, thay vì để script bị lỗi.
Trong đó:
if (condition): Cấu trúc điều kiện cơ bản trong R. Nó sẽ thực thi đoạn code bên trong {…} chỉ khi condition là TRUE.
condition = !“Date” %in% names(df_raw): Đây là biểu thức logic cốt lõi.
names(df_raw): (Hàm base R). Trả về một vector character chứa tất cả 19 tên cột của df_raw (ví dụ: “Indicator”, “Quarter”, “Value”, “Date”,…).
… %in% …: (Toán tử base R). Đây là toán tử so khớp (match). A %in% B sẽ trả về TRUE nếu phần tử A được tìm thấy bên trong vector B.
“Date” %in% names(df_raw): (Tham số bắt buộc). Lệnh này kiểm tra: “Có phải chuỗi”Date” nằm trong vector tên cột không?“.
!: (Toán tử base R). Toán tử phủ định logic (NOT). Nó đảo ngược kết quả.
Toàn bộ condition có nghĩa là: “Có phải”Date” không nằm trong vector tên cột không?“.
message(…): (Hàm base R).
…: (Tham số bắt buộc). Chuỗi ký tự (message) sẽ được in ra console nếu condition là TRUE. Hàm message khác với stop(), nó không dừng việc thực thi script mà chỉ thông báo cho người dùng.
Kết quả kĩ thuật
Thông báo “Không tìm thấy cột Date — thử tạo từ Year & Month nếu có” cho biết dữ liệu của bạn hiện không có sẵn trường ngày (Date), nên cần kết hợp từ các biến năm (Year) và tháng (Month) để xây dựng lại cột quan trọng cho các phân tích chuỗi thời gian.
Việc kiểm tra này là một khâu phòng ngừa giúp phát hiện sớm thiếu sót trong cấu trúc dữ liệu, từ đó đảm bảo tính liên tục, nhất quán khi thực hiện các phép tổng hợp hoặc vẽ biểu đồ theo thời gian.
Ý nghĩa thống kê
Khi dữ liệu thiếu cột ngày, mọi báo cáo phân tích tài chính theo diễn tiến thời gian đều có thể không chính xác nếu không bổ sung đầy đủ.
Việc chủ động xây dựng trường ngày từ dữ liệu năm và tháng giúp giữ nguyên ý nghĩa thực tiễn của chuỗi số liệu, đảm bảo các kết luận về xu hướng kinh doanh và hiệu quả tài chính đều sát với thực tế doanh nghiệp.
if("Indicator" %in% names(df_raw)) {
n_distinct(df_raw$Indicator)
} else {
message("Không tìm thấy cột 'Indicator'. Kiểm tra tên cột trong data.")
}
## Không tìm thấy cột 'Indicator'. Kiểm tra tên cột trong data.
Mục đích của khối code này là xác định phạm vi (scope) phân tích bằng cách đếm số lượng chỉ số tài chính duy nhất có trong bộ dữ liệu.
Đây là một bước kiểm tra phòng vệ: nó kiểm tra xem cột Indicator có tồn tại không. Nếu có, nó dùng hàm n_distinct (từ package dplyr) để đếm; nếu không, nó sẽ thông báo cho người dùng.
Trong đó:
if (condition): Cấu trúc điều kiện base R.
condition = “Indicator” %in% names(df_raw): (Tham số bắt buộc). Đây là một biểu thức logic.
names(df_raw): Trả về vector các tên cột.
%in%: Toán tử so khớp.
Biểu thức này kiểm tra xem chuỗi “Indicator” có nằm trong danh sách tên cột hay không. Dựa trên dữ liệu, kết quả là TRUE.
n_distinct(x, na.rm = FALSE): Hàm từ package dplyr.
x = df_raw$Indicator: (Tham số bắt buộc). Đây là vector (cột Indicator) mà hàm sẽ đếm các giá trị duy nhất.
na.rm = FALSE: (Tham số tùy chọn, được mặc định). Chỉ định rằng NA (giá trị khuyết) cũng sẽ được đếm là một “giá trị” duy nhất nếu nó tồn tại. (Trong trường hợp này, cột Indicator đầy đủ 100% nên tham số này không ảnh hưởng).
else { … }: Khối lệnh này sẽ được thực thi nếu condition là FALSE.
message(…): (Hàm base R). In ra một thông báo chẩn đoán mà không dừng script.
Kết quả kĩ thuật
Thông báo “Không tìm thấy cột ‘Indicator’. Kiểm tra tên cột trong data.” cho biết hàm hoặc đoạn code đang tìm kiếm biến named ‘Indicator’ nhưng dữ liệu thực tế chưa có trường này, có thể do tên gốc bị sai hoặc chưa được chuẩn hóa lại.
Điều này giúp phát hiện và cảnh báo lỗi khi truy xuất dữ liệu, tránh các thao tác phân tích bị sai hoặc mã bị hỏng do gọi nhầm tên biến.
Ý nghĩa thống kê
Việc thiếu trường ‘Indicator’ đồng nghĩa thống kê, phân tích nhóm chỉ tiêu tài chính chưa thực hiện được, có nguy cơ bỏ sót các thông tin quan trọng, ảnh hưởng đến kết luận hiệu quả kinh doanh và so sánh theo từng loại chỉ tiêu.
Cảnh báo này giúp nhà phân tích dữ liệu chủ động kiểm tra lại tên cột, bổ sung hoặc chỉnh sửa trước khi lập báo cáo, đảm bảo chất lượng và giá trị sử dụng của kết quả phân tích tài chính sau này.
dt <- df_raw %>% as_tibble()
Mục đích của lệnh này là chuyển đổi đối tượng df_raw từ kiểu data.frame (của R cơ bản) sang kiểu tibble và gán kết quả vào một biến mới tên là dt.
Trong đó:
dt <- …: Toán tử gán.
Nó lấy kết quả của toàn bộ biểu thức bên vế phải và gán nó vào một đối tượng mới trong bộ nhớ, có tên là dt.
df_raw vẫn được giữ nguyên, không bị thay đổi.
… %>% …: Toán tử “pipe”, cốt lõi của tidyverse.
(Tham số bắt buộc). Nó lấy đối tượng ở bên trái (df_raw) và “chuyển” (pipe) nó làm tham số đầu tiên cho hàm ở bên phải (as_tibble()).
df_raw %>% as_tibble() tương đương về mặt chức năng với cách viết của R cơ bản: dt <- as_tibble(df_raw).
as_tibble(x, …): Hàm chuyển đổi.
x = df_raw: (Tham số bắt buộc). Đây là đối tượng cần được chuyển đổi. Trong trường hợp này, df_raw được cung cấp ngầm thông qua toán tử %>%.
Các tham số khác là tùy chọn và đang được sử dụng ở giá trị mặc định, vì vậy hàm này chỉ đơn giản là thực hiện chuyển đổi kiểu dữ liệu.
Kết quả kĩ thuật
Việc dùng lệnh df_raw %>% as_tibble() sẽ chuyển dữ liệu sang dạng tibble, một kiểu dữ liệu hiện đại hơn so với data.frame trong R.
Tibble giúp hiển thị dữ liệu trực quan, rõ ràng, đặc biệt khi làm việc với bảng lớn, đồng thời hạn chế các lỗi hiển thị và sửa đổi không mong muốn trên cột hoặc dòng, giúp code thao tác với dữ liệu ngắn gọn và dễ kiểm soát hơn.
Ý nghĩa thống kê
Chuyển qua tibble giúp tăng tốc độ và độ tin cậy cho các thao tác phân tích, tổng hợp, xuất báo cáo tài chính doanh nghiệp.
Định dạng này tiện dùng trong quy trình xử lý dữ liệu lớn, bảo đảm kết quả đầu ra minh bạch, dễ chia sẻ và kiểm chứng khi trình bày trong các báo cáo chuyên môn, hỗ trợ mạnh cho công tác ra quyết định.
names(dt)[1] <- "Indicator"
Mục đích của lệnh này là gán (assign) hoặc sửa (rename) một cách tường minh tên của cột đầu tiên (cột có chỉ số [1]) trong data frame df thành chuỗi ký tự (string) “Indicator”.
Đây là một thao tác “sửa chữa” (fixing) hoặc “chuẩn hóa” (standardizing) tên biến rất phổ biến, thường được dùng để sửa lỗi khi R đọc file CSV/Excel
Trong đó:
names(df): (Hàm base R). Khi được sử dụng ở vế trái của toán tử gán (<-), nó cho phép truy cập và sửa đổi trực tiếp vector character chứa tên của tất cả các cột trong df.
[1]: (Toán tử base R). Đây là toán tử lập chỉ mục (indexing).
1: (Tham số bắt buộc, kiểu numeric). Đây là chỉ số (index) vị trí của phần tử cần được truy cập. Trong R, chỉ số bắt đầu từ 1. Do đó, [1] chỉ định rõ “chỉ phần tử đầu tiên” của vector names(df).
<-: (Toán tử base R). Đây là toán tử gán (assignment).
“Indicator”: (Tham số bắt buộc, kiểu character). Đây là giá trị mới sẽ được gán cho phần tử đã được chọn (tên cột ở vị trí 1).
Kết quả kĩ thuật
Lệnh names(dt)[1] <- “Indicator” nhằm đổi tên cột đầu tiên thành “Indicator”, giúp đồng nhất, rõ ràng khi xử lý và truy xuất dữ liệu.
Ý nghĩa thống kê
Đặt tên trường “Indicator” giúp nhận diện, phân loại các chỉ tiêu tài chính (doanh thu, lợi nhuận, tài sản…) một cách minh bạch; từ đó, thuận lợi cho phân tích so sánh các chỉ tiêu, đánh giá hiệu quả hoạt động doanh nghiệp.
Quá trình chuẩn hóa tên biến đóng vai trò thiết yếu trong các báo cáo tài chính, quản trị, giúp lãnh đạo, nhà đầu tư và kiểm toán đọc hiểu nhanh, kiểm tra tính đầy đủ và chính xác của thông tin trước khi ra quyết định quan trọng.
dt_long <- dt %>%
pivot_longer(
cols = -Indicator,
names_to = "Period",
values_to = "Value"
) %>%
filter(!is.na(Value)) %>%
mutate(
Period = str_squish(Period),
Indicator = str_squish(Indicator)
)
Mục đích chính của đoạn code này là chuyển đổi cấu trúc dữ liệu từ dạng ‘bảng rộng’ (wide format) sang dạng ‘bảng dài’ (long format).
Trong dữ liệu gốc (dt), mỗi kỳ báo cáo (2017, 2018, Q1 2017,…) là một cột riêng biệt. Đoạn code này sẽ “xoay” (pivot) tất cả các cột thời gian đó thành một cột duy nhất, giúp cho việc phân tích và trực quan hóa theo chuỗi thời gian trở nên dễ dàng hơn. Đồng thời, code cũng thực hiện bước làm sạch dữ liệu cơ bản bằng cách loại bỏ các giá trị rỗng (NA) và các khoảng trắng thừa.
Trong đó:
dt_long <- …: Tạo một đối tượng (biến) mới có tên là dt_long để lưu kết quả cuối cùng.
df %>%: Sử dụng “pipe operator” %>%. Nó lấy tibble dt làm đầu vào và “chuyển” nó cho hàm tiếp theo (pivot_longer).
pivot_longer(…):
Hàm then chốt từ thư viện tidyr (thuộc tidyverse) để xoay dữ liệu từ rộng sang dài.
cols = -Indicator: Chỉ định các cột cần xoay. Dấu - (trừ) có nghĩa là “chọn tất cả các cột ngoại trừ cột Indicator”. Cột Indicator sẽ được giữ nguyên và lặp lại cho mỗi kỳ.
names_to = “Period”: Tạo ra một cột mới tên là “Period”. Cột này sẽ chứa tên của các cột cũ (ví dụ: “2017”, “2018”, “Q1 2017”…).
values_to = “Value”: Tạo ra một cột mới tên là “Value”. Cột này sẽ chứa các giá trị (dữ liệu tài chính) nằm bên trong các cột cũ đó.
filter(!is.na(Value)): Lọc tất cả các hàng (rows) mà cột Value không (!) phải là NA (is.na). Thao tác này loại bỏ tất cả các ô trống (missing data) khỏi tập dữ liệu.
mutate(…): Hàm từ dplyr dùng để tạo hoặc thay đổi các cột.
Period = str_squish(Period): Áp dụng hàm str_squish (từ stringr) vào cột Period. Hàm này loại bỏ tất cả khoảng trắng thừa ở đầu, cuối và các khoảng trắng kép ở giữa chuỗi (ví dụ: ” Q1 2017 ” sẽ thành “Q1 2017”).
Indicator = str_squish(Indicator): Tương tự, làm sạch cột Indicator.
Kết quả kĩ thuật
Đoạn lệnh chuyển đổi bảng từ dạng rộng sang dạng dài (pivot_longer) giúp dữ liệu trực quan hơn: mỗi dòng lưu trữ 1 chỉ số (Indicator), giai đoạn (Period), và giá trị đo được (Value).
Lệnh lọc NA, làm sạch chuỗi (str_squish) giúp đảm bảo dữ liệu không dư thừa, không lỗi ký tự và sẵn sàng cho phân tích hoặc trực quan hóa thời gian, nhóm chỉ tiêu, so sánh biến động các kỳ.
Chuẩn hóa này thuận tiện cho việc vẽ biểu đồ, mô hình hóa và sử dụng các gói phân tích thống kê trong R.
Ý nghĩa thống kê
Việc đưa dữ liệu về dạng dài tối ưu cho phân tích tài chính theo từng chỉ tiêu qua các thời kỳ, ví dụ đánh giá doanh thu, lợi nhuận, chi phí cho từng quý/năm.
Dữ liệu sạch, dễ khai thác giúp nhà quản trị, kiểm toán, nhà đầu tư nhanh chóng nhận diện xu hướng, điểm mạnh/yếu và đưa ra dự báo chính xác, cải thiện hiệu quả sử dụng thông tin trong báo cáo tài chính của doanh nghiệp.
dt_long <- dt_long %>%
mutate(
Year = as.numeric(str_extract(Period, "\\d{4}")),
Quarter = str_extract(Period, "Q[1-4]"),
Month = case_when(
Quarter == "Q1" ~ 3,
Quarter == "Q2" ~ 6,
Quarter == "Q3" ~ 9,
Quarter == "Q4" ~ 12,
TRUE ~ 12
),
Date = as.Date(paste0(Year, "-", Month, "-01"))
) %>%
filter(!is.na(Year))
dt_long <- dt_long %>%
mutate(
Quarter = ifelse(is.na(Quarter), "Yearly", Quarter)
)
dt_long <- dt_long %>% filter(!is.na(Date))
sum(is.na(dt_long$Value))
## [1] 0
dt_long <- dt_long %>%
group_by(Indicator) %>%
mutate(
Value = ifelse(is.na(Value), median(Value, na.rm = TRUE), Value)
) %>%
ungroup()
dt_long <- dt_long %>%
mutate(Value = ifelse(is.na(Value), 0, Value))
colSums(is.na(dt_long))
## Indicator Period Value Year Quarter Month Date
## 0 0 0 0 0 0 0
Bộ code này nhằm chuẩn hóa dữ liệu tài chính dạng dài, gán các trường thời gian (năm, quý, tháng, ngày), xử lý giá trị thiếu (NA), đảm bảo bảng dữ liệu sạch và sẵn sàng cho phân tích chuỗi thời gian hoặc thống kê tài chính.
Trong đó:
mutate(Year, Quarter, Month, Date): Trích xuất, chuyển đổi và chuẩn hóa các trường thời gian từ chuỗi ký hiệu về định dạng số/ngày thực tiễn.
filter(!is.na(Year)), filter(!is.na(Date)): Giữ lại các dòng có thông tin thời gian hợp lệ.
sum(is.na(dt_long$Value)): Kiểm tra còn giá trị NA trong cột chính (Value) không.
group_by(Indicator) %>% mutate(Value = …): Điền giá trị thiếu bằng trung vị theo từng chỉ số tài chính.
mutate(Value = ifelse(is.na(Value), 0, Value)): Nếu vẫn còn thiếu sau trung vị, thay bằng 0.
colSums(is.na(dt_long)): Xác nhận lần cuối bảng đã hoàn toàn sạch NA.
*Kết quả kĩ thuật
Kết quả hàm colSums(is.na(dt_long)) trả về giá trị 0 cho tất cả các cột (Indicator, Period, Value, Year, Quarter, Month, Date), chứng tỏ không còn giá trị thiếu nào.
Tổng số NA ban đầu ở biến Value đã được xử lý thành 0 giá trị thiếu nhờ các bước điền trung vị hoặc thay bằng 0, giúp dữ liệu đồng nhất, đảm bảo độ tin cậy cho các phép thống kê tiếp theo.
Việc chuyển đổi, sinh biến (Year, Quarter, Month, Date) đã hoàn tất, định dạng thời gian đầy đủ.
Ý nghĩa thống kê
Việc loại bỏ toàn bộ dữ liệu thiếu giúp kết quả phân tích kinh tế sau này (ví dụ như tính xu hướng, dự báo chỉ số) không bị sai lệch hoặc bóp méo do giá trị khuyết.
Biện pháp thay thế NA bằng trung vị theo nhóm Indicator giúp duy trì tính đại diện của từng nhóm chỉ báo, tránh gây sai số lớn, nhất là với các chỉ số biến động mạnh hoặc có phân phối lệch.
Sử dụng giá trị 0 cho những trường hợp NA đặc biệt còn sót lại đảm bảo dữ liệu khép kín cho các phương pháp máy học hoặc hồi quy, giảm nguy cơ lỗi mô hình do thiếu dữ liệu thực nghiệm.
dt2 <- dt_long %>% distinct()
Hàm dt2 <- dt_long %>% distinct() dùng để loại bỏ các dòng dữ liệu trùng lặp hoàn toàn trong bảng dt_long, giữ lại duy nhất các rows thực sự khác biệt.
Trong đó:
dt_long: Đầu vào là bảng dữ liệu gốc (ở dạng “long format”).
distinct(): Không truyền thêm tham số gì nghĩa là giữ lại các dòng hoàn toàn không giống nhau trên tất cả các cột..
Kết quả lưu vào dt2 chính là bảng đã được loại bỏ hoàn toàn các row trùng lặp.
Hàm này không làm thay đổi cấu trúc, thứ tự cột, chỉ giảm số lượng dòng nếu tồn tại dòng giống nhau hoàn toàn.
Kết quả kĩ thuật
Đảm bảo dữ liệu đầu vào là tập các quan sát duy nhất (unique), giúp mọi phép thống kê/truy vấn logic trả về kết quả chính xác, đồng thời tối ưu hiệu suất bộ nhớ/kích thước file.
Ngăn chặn việc lặp lại cùng một thông tin nhiều lần gây ra bias thống kê (mean, median, sum…), từ đó đảm bảo chất lượng dữ liệu cho phân tích chuyên sâu.
Ý nghĩa thống kê
Giảm thiểu sai số và phiền phức khi lập báo cáo, xây dựng chỉ số kinh doanh – tránh tình trạng overcount khi đánh giá số liệu vận hành, doanh số hoặc chi phí.
Đảm bảo số liệu đầu vào nhất quán là nền tảng để doanh nghiệp hoạch định, đầu tư, kiểm soát giá trị tài chính chính xác, tránh rủi ro quyết định dựa trên dữ liệu lỗi thời.
dt2 <- dt2 %>%
mutate(Indicator = str_to_upper(Indicator))
Mục đích của lệnh này là chuẩn hóa dữ liệu dạng văn bản trong cột Indicator.Cụ thể, nó tạo ra một data frame mới (bằng cách ghi đè lên df2 cũ) trong đó tất cả các giá trị trong cột Indicator đã được chuyển đổi hoàn toàn sang chữ IN HOA (UPPERCASE).
Trong đó:
dt2 <- …: Toán tử gán.Ghi đè (overwrite) đối tượng dt2 bằng kết quả của biểu thức bên phải.
… %>% …: Toán tử “pipe”.Lấy dt2 (bên trái) và chuyển nó làm tham số đầu tiên cho hàm mutate() (bên phải).
mutate(.data, …): Hàm từ dplyr.
data = dt2: (Tham số bắt buộc, được cung cấp bởi %>%). Data frame để làm việc.
Indicator = str_to_upper(Indicator): (Tham số bắt buộc). Đây là biểu thức biến đổi.
Indicator = …: Chỉ định rằng chúng ta muốn ghi đè lên cột Indicator hiện có.
str_to_upper(string): (Hàm từ stringr).
string = Indicator: (Tham số bắt buộc). Đây là vector (cột Indicator gốc) sẽ được dùng làm đầu vào. Hàm này sẽ được áp dụng cho mọi phần tử trong cột.
Kết quả kĩ thuật
Hàm str_to_upper(Indicator) chuyển toàn bộ giá trị trong cột Indicator sang chữ in hoa, ví dụ: “Doanh thu bán hàng…” thành “DOANH THU BÁN HÀNG…”.
Toàn bộ cấu trúc dữ liệu số, ngày tháng, các chỉ số (Value, Year, Month, Date…) không hề thay đổi — kết quả vẫn 1.050 dòng, 7 trường, chỉ đổi cách hiển thị tên chỉ số.
Quy trình này giúp chuẩn hóa tên Indicator, tránh sai lệch khi so sánh dữ liệu theo tên (ví dụ “Doanh thu…” vs “DOANH THU…”).
Ý nghĩa thống kê
Việc đồng bộ hoá tên chỉ tiêu về ký tự in hoa giúp đảm bảo tính nhất quán khi truy vấn, tổng hợp hoặc chuẩn bị báo cáo, đặc biệt hữu ích khi dữ liệu đến từ nhiều nguồn khác nhau.
Các chỉ số kinh tế, giá trị doanh thu,… vẫn giữ nguyên (vd: >1.3 nghìn tỷ cho từng năm), nên về mặt thực tiễn không ảnh hưởng đến kết quả phân tích, tốc độ tăng trưởng, hiệu quả kinh doanh.
dt2 <- dt2 %>%
mutate(LogValue = ifelse(Value > 0, log(Value), NA_real_))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `LogValue = ifelse(Value > 0, log(Value), NA_real_)`.
## Caused by warning in `log()`:
## ! NaNs produced
Hàm này nhằm tạo một biến mới LogValue cho bảng dt2, là logarit tự nhiên của biến Value nhưng chỉ tính cho trường hợp Value > 0; nếu Value nhỏ hơn hoặc bằng 0 thì gán NA (dạng số thực đặc biệt)
Trong đó:
Value > 0: Điều kiện kiểm tra, trả về TRUE nếu giá trị tại dòng đó lớn hơn 0, FALSE nếu không.
log(Value): Nếu giá trị > 0, trả về logarit tự nhiên của Value (thường dùng base e); logarit chỉ áp dụng được cho số dương, giúp dữ liệu phân tán đều hơn.
NA_real_: Nếu điều kiện FALSE (Value <= 0), trả về NA kiểu số thực, đảm bảo không tính log cho giá trị 0 hoặc âm vốn không xác định trong toán học.
mutate(): Thêm cột mới vào dataframe mà không làm thay đổi các trường hợp còn lại.
Kết quả kĩ thuật
Tạo thêm một biến mới LogValue chứa giá trị logarit tự nhiên của các phần tử Value với điều kiện Value phải lớn hơn 0, còn lại trả về NA.
Kết quả biến đổi giúp dữ liệu có phân phối gần chuẩn hơn, giảm sự lệch về phía giá trị lớn, thuận lợi cho các mô hình thống kê (ví dụ hồi quy, kiểm định) và trực quan hóa (biểu đồ log tốt hơn biểu đồ gốc với dữ liệu lớn).
Ý nghĩa thống kê
Các giá trị LogValue hỗ trợ so sánh hiệu quả các chỉ số giữa các nhóm ngành, kỳ thời gian khác nhau, phù hợp với thực tiễn kinh tế khi dữ liệu thường phân phối lệch về phía giá trị lớn.
Nếu phân tích tăng trưởng, hiệu quả đầu tư, tỷ lệ chuyển đổi… thì giá trị log chính là thước đo chuẩn để so sánh và phân tích năng lực thực hiện.
dt2 <- dt2 %>% arrange(Indicator, Date)
Sắp xếp bảng dữ liệu dt2 theo thứ tự tăng dần của biến Indicator và tiếp theo là theo trường Date (ngày tháng), giúp dữ liệu có hệ thống và thuận tiện cho các thao tác thống kê hoặc truy xuất.
Trong đó:
dt2: Dataframe đầu vào cần sắp xếp.
Indicator: Sắp xếp ưu tiên đầu tiên, nhóm các chỉ số giống nhau lại gần nhau.
Date: Sắp xếp thứ hai, đảm bảo từng chỉ số hiển thị đúng trật tự thời gian.
Kết quả kĩ thuật
Kết quả trả về bảng mới đã được sắp xếp logic, rất thuận lợi khi kiểm tra biến động theo thời gian từng chỉ số hoặc khi vẽ biểu đồ, truy xuất theo nhóm/từng năm, quý, tháng.
Loại bỏ lỗi trộn lẫn các chỉ số cùng tên nhưng khác thời điểm, phù hợp chuẩn sạch và có hệ thống.
Ý nghĩa thống kê
Phân tích xu hướng/biến động tài chính chính xác hơn nhờ trật tự tỷ lệ hoặc sự biến đổi của từng chỉ tiêu theo trình tự thời gian.
Hỗ trợ lập báo cáo, dashboard, kiểm toán, giám sát biến động hiệu quả khi dữ liệu đã được sắp xếp, giúp ra quyết định phù hợp bối cảnh thực tế hồ sơ doanh nghiệp.
dt2 <- dt2 %>%
group_by(Indicator) %>%
arrange(Date) %>%
mutate(
QoQ = (Value / lag(Value, 1) - 1) * 100,
YoY = (Value / lag(Value, 4) - 1) * 100
) %>%
ungroup()
Hàm này giúp tạo hai biến mới là QoQ (Quarter-over-Quarter, tăng trưởng quý liền trước) và YoY (Year-over-Year, tăng trưởng so cùng kỳ năm trước) cho từng chỉ số Indicator, sắp xếp đúng theo thời gian để phép so sánh có ý nghĩa.
Trong đó:
group_by(Indicator): Gom các dòng dữ liệu cùng tên chỉ số để tính riêng biệt theo từng nhóm chỉ số.
arrange(Date): Sắp xếp các giá trị trong mỗi nhóm theo đúng thứ tự thời gian, đảm bảo phép tính tăng trưởng logic.
mutate(QoQ, YoY): Tạo biến tăng trưởng quý liền trước và cùng kỳ năm trước cho từng giá trị, công thức tỷ lệ phần trăm thay đổi so với dữ liệu lag 1 quý hoặc 4 quý.
ungroup(): Trả về bảng không còn nhóm cho các thao tác tiếp theo.
Kết quả kĩ thuật
Kết quả trả về bảng dữ liệu không chỉ liệt kê các giá trị tuyệt đối mà còn có thêm biến tăng trưởng quý và năm, rất thuận tiện khi trực quan hóa, tổng hợp hoặc phát hiện “outlier.”
Các chỉ số sẽ liên tục, logic theo thời gian, giảm lỗi khi lấy kết quả phân tích chia sẻ cho tổ chức.
Ý nghĩa thống kê
Tăng trưởng QoQ, YoY là thước đo chuẩn để nhà quản trị, nhà đầu tư đánh giá mức độ cải thiện/doanh thu/lợi nhuận của từng chỉ tiêu theo thời gian.
Kết quả giúp so sánh hiệu quả từng kỳ, phát hiện sớm tín hiệu tích cực hay tiêu cực; là cơ sở cam kết/kế hoạch sản xuất hoặc đầu tư tài chính thực tiễn và có hệ thống.
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(
QoQ = ifelse(is.na(QoQ), median(QoQ, na.rm = TRUE), QoQ),
YoY = ifelse(is.na(YoY), median(YoY, na.rm = TRUE), YoY)
) %>%
ungroup()
dt2 %>%
select(QoQ,YoY) %>%
head()
## # A tibble: 6 × 2
## QoQ YoY
## <dbl> <dbl>
## 1 21.4 8.59
## 2 -18.4 -63.5
## 3 -12.2 -57.5
## 4 -0.294 14.4
## 5 7.31 11.2
## 6 -11.9 -9.78
Đoạn code này dùng để điền đầy đủ giá trị NA cho các biến tăng trưởng QoQ (theo quý) và YoY (theo năm) bằng giá trị trung vị (median) của từng nhóm Indicator, đảm bảo mọi phân tích không còn bị gián đoạn vì thiếu dữ liệu.
Trong đó:
group_by(Indicator): Gom nhóm các dòng theo tên chỉ số tài chính để điền NA riêng biệt cho từng chỉ số.
mutate(QoQ = ifelse(is.na(QoQ), median(QoQ, na.rm = TRUE), QoQ), …): Nếu giá trị tăng trưởng bị thiếu, thay bằng median của chỉ số đó.
ungroup(): Trả dữ liệu về trạng thái chưa nhóm cho các thao tác tiếp theo.
Kết quả kĩ thuật
Hàm mutate() đã thay thế các giá trị NA trong hai trường QoQ và YoY bằng median (trung vị) theo từng nhóm Indicator, đảm bảo toàn bộ bảng không còn NA, giúp dữ liệu “hoàn chỉnh” cho phân tích tiếp theo.
Số dòng trong bảng là 6, hai biến mới là QoQ và YoY đều có kiểu số thực (double).
Các giá trị QoQ trải rộng từ -18.37 tới 21.41, YoY từ -63.50 tới 14.37, xác nhận dữ liệu đa dạng, đủ biến động để phục vụ kiểm định hoặc vẽ đồ thị động lực kinh doanh
Ý nghĩa thống kê
Giá trị QoQ (tăng trưởng theo quý) có biến động mạnh, ví dụ quý tăng 21.41%, có quý giảm -18.36%, cho thấy doanh nghiệp/đơn vị phân tích gặp biến động lớn về hiệu quả kinh doanh giữa các quý, không ổn định.
Giá trị YoY (so với cùng kỳ năm trước) cũng biến thiên lớn, từ mức giảm sâu -63.50% đến tăng trưởng 14.37%; tình trạng này phản ánh có những thời kỳ doanh nghiệp gặp khó khăn lớn nhưng cũng có quý phục hồi tốt, phù hợp với các ngành có chu kỳ kinh doanh mạnh hoặc chịu ảnh hưởng từ yếu tố vĩ mô.
Việc dùng median điền vào vị trí thiếu giúp chống méo số liệu do ngoại lệ, đảm bảo so sánh và diễn giải sát thực tế: doanh nghiệp sẽ không bị đánh giá “quá lạc quan” hoặc “quá bi quan” chỉ vì thiếu dữ liệu quý nào đó.
dt2 <- dt2 %>%
mutate(
Value_quantile = ntile(Value, 4),
ValueClass = case_when(
Value_quantile == 4 ~ "Very High",
Value_quantile == 3 ~ "High",
Value_quantile == 2 ~ "Medium",
TRUE ~ "Low"
)
)
dt2 %>%
select(Indicator,Value_quantile,ValueClass) %>%
head()
## # A tibble: 6 × 3
## Indicator Value_quantile ValueClass
## <chr> <int> <chr>
## 1 CHI PHÍ BÁN HÀNG 2 Medium
## 2 CHI PHÍ KHÁC 2 Medium
## 3 CHI PHÍ LÃI VAY 1 Low
## 4 CHI PHÍ QUẢN LÝ DOANH NGHIỆP 1 Low
## 5 CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP 1 Low
## 6 CHI PHÍ TÀI CHÍNH 1 Low
Phân loại các giá trị tài chính (Value) thành 4 nhóm (quantile) từ thấp đến cao, đồng thời gán nhãn mức độ (“Low”, “Medium”, “High”, “Very High”) cho từng quan sát, giúp nhận diện nhanh vị trí/tầm quan trọng của từng điểm số trong tập dữ liệu.
Trong đó:
ntile(Value, 4): Chia giá trị Value thành 4 nhóm bằng nhau theo quy tắc thứ tự tăng dần, mỗi nhóm được đánh số từ 1 (thấp nhất) đến 4 (cao nhất).
case_when(…): Đặt nhãn cho từng nhóm giá trị theo thứ tự 1 = Low, 2 = Medium, 3 = High, 4 = Very High. Mỗi nhãn phản ánh mức độ giá trị tài chính trong bảng.
Kết quả kĩ thuật
Sử dụng hàm ntile(Value, 4) đã chia dữ liệu chi phí thành 4 nhóm (phân vị), mỗi nhóm đại diện cho một mức giá trị từ thấp đến rất cao.
Cột ValueClass được gán nhãn dựa trên phân vị: “Very High”, “High”, “Medium”, “Low”. Trong ảnh, các chi phí đều nằm trong nhóm “Medium” (giá trị lượng hóa = 2) hoặc “Low” (giá trị lượng hóa = 1), không xuất hiện giá trị 3 (“High”) hay 4 (“Very High”) trong top kết quả hiển thị.
Kết quả dữ liệu gồm 6 dòng, thể hiện các loại chi phí lớn như chi phí bán hàng, chi phí khác, chi phí quản lý, tài chính…, đảm bảo thuận lợi cho bước tổng hợp hoặc trực quan hoá.
Ý nghĩa thống kê
Việc phân lớp chi phí theo giá trị giúp xác định đặc trưng chi phí từng nhóm: “Chi phí bán hàng”, “Chi phí khác” được phân loại là “Medium”, còn lại chủ yếu là “Low”. Điều này phản ánh mức độ ảnh hưởng của từng khoản tới tổng chi phí doanh nghiệp.
Khi đánh giá cấu trúc chi phí, phân vị lượng hóa giúp phát hiện chi phí quan trọng cần kiểm soát, có thể ưu tiên tiết giảm các hạng mục thuộc nhóm “Medium” trở lên để tối ưu hóa lợi nhuận.
Nhóm chi phí “Low” (quản lý, lãi vay, thuế, tài chính) đang có mức giá trị khá thấp so với các khoản khác, chứng tỏ tính ổn định hoặc hiệu quả của hoạt động vận hành, tài chính và thuế trong thời gian xem xét.
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(Value_z = as.numeric(scale(Value))) %>%
ungroup()
dt2 %>%
select(Value_z) %>%
head()
## # A tibble: 6 × 1
## Value_z
## <dbl>
## 1 0.794
## 2 0.570
## 3 0.225
## 4 0.761
## 5 0.732
## 6 0.257
Đoạn mã dùng để chuẩn hoá biến Value theo z-score cho từng Indicator (chỉ tiêu). Z-score giúp đưa dữ liệu về cùng thang đo, loại bỏ đơn vị gốc, làm cho các giá trị có trung bình 0 và độ lệch chuẩn 1 trong từng nhóm Indicator.
Trong đó:
group_by(Indicator): Nhóm các bản ghi theo từng loại chỉ tiêu, tức mỗi Indicator được chuẩn hóa riêng lẻ với bộ thông số riêng.
mutate(Value_z = as.numeric(scale(Value))): Hàm scale chuẩn hoá từng giá trị
Kết quả là Value_z - giá trị z-score tương ứng cho từng dòng dữ liệu, có thể âm (dưới trung bình), dương (trên trung bình), hoặc bằng 0 (bằng trung bình).
Hàm as.numeric() chuyển kết quả sang số, thuận tiện cho các thao tác tiếp theo.
ungroup(): Bỏ nhóm, trả dataframe về trạng thái phẳng để phân tích tiếp.
Kết quả kĩ thuật
Các giá trị hiển thị: 0.794, 0.569, 0.225, 0.761, 0.732, 0.257… là kết quả đã chuẩn hóa z-score cho từng Value trong Indicator ứng với từng nhóm.
Z-score cho biết vị trí tương đối của từng giá trị so với trung bình nhóm Indicator: số dương cho biết giá trị cao hơn mức trung bình, càng lớn càng xa trung bình, số âm (nếu có) là thấp hơn trung bình.
Toàn bộ các giá trị này đều nằm trong khoảng ±1, cho thấy các điểm này không mang tính chất ngoại lai, dữ liệu phân phối khá “chuẩn” quanh trung bình.
Ý nghĩa thống kê
Ở mức Value_z ~ 0.79, 0.56, 0.73,… các quan sát này đều thuộc nhóm cao hơn mức trung bình ngành, thể hiện “tốt hơn bình quân” trong kỳ xét.
Việc dùng z-score giúp nhận diện doanh nghiệp/nghành/chỉ tiêu nào vượt trội so với mặt bằng chung – phục vụ đánh giá năng lực cạnh tranh hay tiềm năng phát triển.
Nếu phát hiện Value_z vượt quá ±2 (không xuất hiện ở đây) sẽ là tín hiệu có điểm “bất thường”, đáng chú ý cho nhà quản trị, phù hợp đánh giá rủi ro, tiềm năng hay cần điều tra sâu dữ liệu.
dt2 <- dt2 %>%
mutate(DayOfYear = yday(Date))
dt2 %>%
select(Indicator, Date, DayOfYear) %>%
head()
## # A tibble: 6 × 3
## Indicator Date DayOfYear
## <chr> <date> <dbl>
## 1 CHI PHÍ BÁN HÀNG 2017-03-01 60
## 2 CHI PHÍ KHÁC 2017-03-01 60
## 3 CHI PHÍ LÃI VAY 2017-03-01 60
## 4 CHI PHÍ QUẢN LÝ DOANH NGHIỆP 2017-03-01 60
## 5 CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP 2017-03-01 60
## 6 CHI PHÍ TÀI CHÍNH 2017-03-01 60
Hàm yday() trong gói lubridate lấy thông tin “ngày thứ bao nhiêu trong năm” từ biến kiểu ngày tháng (Date), trả về số nguyên từ 1 đến 365 (hoặc 366 với năm nhuận).
Việc thêm biến DayOfYear giúp biểu diễn ngày trong năm dưới dạng số nguyên, dễ dàng phân tích chu kỳ, mùa vụ, hay xu hướng theo từng ngày trong năm.
Trong đó:
Date: Cột chứa giá trị kiểu ngày tháng, ví dụ “2024-01-01”, “2019-12-01”…
yday(Date): Trích xuất ngày thứ bao nhiêu trong năm đó, với 1 là ngày đầu tiên (1/1), và 365 hoặc 366 là ngày cuối năm
mutate(DayOfYear = …): Tạo cột mới DayOfYear chứa giá trị ngày trong năm này cho mỗi bản ghi của bảng dữ liệu
Kết quả kĩ thuật
Code đã tạo một biến mới DayOfYear có giá trị là 60, tương ứng với ngày 1/3/2017 (ngày thứ 60 trong năm không nhuận).
Các biến Indicator, Date, DayOfYear được hiển thị rõ ràng: mỗi loại chi phí được gắn với một giá trị ngày và số thứ tự ngày trong năm, giúp dữ liệu dễ thao tác, đối chiếu, lọc hoặc tổng hợp theo mùa vụ, tuần, quý.
Ý nghĩa thống kê
Việc xác định rõ từng chi phí tại đúng thời điểm trong năm (ở đây là ngày 60 - đầu tháng 3) giúp phân tích đặc trưng mùa vụ, so sánh giữa các tháng/quý, đánh giá điểm rơi các khoản chi tài chính, sản xuất, quản lý….
Nhờ có biến DayOfYear, có thể dễ dàng thực hiện các phân tích theo chu kỳ (so sánh các năm cùng kỳ, tìm quy luật tăng/giảm theo mùa…), phát hiện tính bất thường hoặc xu hướng chi phí ở các mốc quan trọng trong năm kinh tế.
Dữ liệu dạng này cực kỳ phù hợp cho các biểu đồ xu hướng, heatmap thời gian hoặc mô hình dự báo mang tính mùa vụ trong phân tích kinh tế doanh nghiệp.
colSums(is.na(dt2))
## Indicator Period Value Year Quarter
## 0 0 0 0 0
## Month Date LogValue QoQ YoY
## 0 0 567 42 42
## Value_quantile ValueClass Value_z DayOfYear
## 0 0 42 0
colSums(is.na(dt2)) dùng để đếm tổng số giá trị bị thiếu (NA) trên từng cột của dataframe dt2.
Trong đó:
is.na(dt2): Trả về một ma trận giá trị logical cùng kích thước với dt2, với TRUE nếu phần tử đó là NA, ngược lại là FALSE.
colSums(…): Tính tổng (sum) theo từng cột, với TRUE được tính là 1 và FALSE là 0; kết quả là số lượng giá trị NA của từng trường trong bảng dữ liệu.
Không truyền tham số na.rm, vì mục đích ở đây là tính số NA chứ không phải tổng số trị số thực.
Kết quả kĩ thuật
Kết quả cho thấy các cột Indicator, Period, Value, Year, Quarter, Month, Date, Value_quantile, ValueClass, Outlier, DayOfYear đều có 0 giá trị NA — hoàn toàn “sạch”, không có thiếu dữ liệu ở bất cứ điểm nào.
Tuy nhiên, ba biến QoQ, YoY, Value_z có giá trị 42 NA ở mỗi biến, nhiều nhất trong bảng này.
Việc kiểm tra này xác nhận rõ dữ liệu đã được xử lý tốt ở các cột chính, nhưng các biến động tăng trưởng, chuẩn hoá hoặc diễn giải tạm thời còn thiếu ở 42 dòng — cần chú ý bổ sung, loại bỏ, hoặc điều chỉnh trong các bước phân tích tiếp theo để tránh lỗi hoặc bias kỹ thuật.
Ý nghĩa thống kê
Dữ liệu sạch ở các trường chỉ số (doanh thu, chi phí…), thời gian và phân tầng giá trị đảm bảo các phân tích cấu trúc, xu hướng, phân nhóm kinh tế (theo năm, quý, loại lớp…) sẽ cho kết quả tin cậy, sát thực tế.
Tuy nhiên, việc biến QoQ, YoY, Value_z bị thiếu dữ liệu ở 42 trường có thể dẫn đến việc kết luận sai lệch về tăng trưởng, biến động hoặc các phép so sánh chuẩn hoá. Trong phân tích kinh tế lượng, dữ liệu thiếu này bắt buộc phải được xử lý kỹ trước khi thực hiện hồi quy, kiểm định hoặc báo cáo tổng kết.
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(
LogValue = ifelse(is.na(LogValue),
median(LogValue, na.rm = TRUE),
LogValue)
) %>%
ungroup()
Hàm này nhóm dữ liệu theo Indicator, sau đó thay thế các giá trị NA ở biến LogValue bằng giá trị trung vị của LogValue trong từng nhóm Indicator tương ứng.
Mục tiêu là đảm bảo không còn LogValue bị thiếu (NA), giúp tăng độ hoàn chỉnh của bộ dữ liệu để phục vụ phân tích/thống kê, kiểm định có ý nghĩa và không bị lỗi kỹ thuật.
Trong đó:
group_by(Indicator): Nhóm dữ liệu theo từng loại chỉ tiêu/phân ngành để việc thay thế trung vị diễn ra riêng biệt trong từng nhóm – đảm bảo giá trị thay thế phù hợp đặc trưng kinh tế từng loại chi phí, doanh thu, v.v.
mutate(LogValue = ifelse(is.na(LogValue), median(LogValue, na.rm = TRUE), LogValue)): Kiểm tra từng dòng ở biến LogValue, nếu là NA thì thay bằng median của LogValue trong nhóm Indicator đó; nếu không phải NA thì giữ nguyên.
median(LogValue, na.rm = TRUE) là tính trung vị, bỏ qua các giá trị thiếu, giúp ra kết quả đại diện nhất cho nhóm.
ungroup(): Trả lại dataframe về trạng thái không nhóm để các phép xử lý tiếp theo không bị ảnh hưởng bởi trạng thái nhóm trước đó.
Kết quả kĩ thuật
Biến LogValue đã được xử lý hoàn chỉnh: các giá trị NA (do Value <= 0 trước đó hoặc lỗi logarit) đã được thay thế bằng giá trị trung vị (median) của LogValue trong từng nhóm Indicator.
Hàm này đảm bảo loại bỏ toàn bộ giá trị thiếu, giúp dữ liệu thống nhất, không dính lỗi về NA khi sử dụng cho các mô hình, kiểm định hoặc trực quan hóa biểu đồ logarit.
Ý nghĩa thống kê
Thay NA bằng median giúp các phân tích về tăng trưởng, biến động giá trị logarit không bị ảnh hưởng bởi giá trị thiếu, đảm bảo mọi ngành đều có dữ liệu sẵn sàng để so sánh, dự báo, hoặc tính toán tỷ lệ tăng trưởng, hiệu quả kinh doanh.
Giá trị trung vị trong nhóm Indicator thường là đại diện thực tế (giá trị ở giữa phân phối, không bị ảnh hưởng mạnh bởi ngoại lệ), từ đó các phân tích kinh tế trên biến LogValue sẽ phản ánh chính xác hơn so với sử dụng trung bình hoặc giá trị bất kỳ.
Quy trình này hữu ích đặc biệt cho các ngành có biến động lớn hoặc dữ liệu đầu vào không đều, giúp tăng tính tin cậy, minh bạch cho báo cáo phân tích kinh tế lượng.
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(
QoQ = ifelse(is.na(QoQ),
median(QoQ, na.rm = TRUE),
QoQ),
YoY = ifelse(is.na(YoY),
median(YoY, na.rm = TRUE),
YoY)
) %>%
ungroup()
Đoạn mã này nhằm thay thế các giá trị NA trong các biến QoQ (tăng trưởng quý) và YoY (tăng trưởng năm) bằng giá trị trung vị (median) của từng nhóm Indicator trong dataframe dt2, đảm bảo mọi giá trị trong hai biến này đều có số liệu đầy đủ, không còn thiếu dữ liệu.
Trong đó:
group_by(Indicator): Nhóm toàn bộ dữ liệu theo từng giá trị trong cột Indicator (mỗi loại doanh thu, chi phí, v.v. sẽ là một nhóm riêng biệt).
mutate(QoQ = ifelse(is.na(QoQ), median(QoQ, na.rm = TRUE), QoQ), YoY = …): Với từng giá trị NA của QoQ (hoặc YoY) trong nhóm Indicator, thay bằng median các giá trị có sẵn của biến đó, các giá trị khác giữ nguyên; na.rm = TRUE trong median đảm bảo loại bỏ NA khi tính trung vị.
ungroup(): Loại bỏ trạng thái nhóm, trả dữ liệu về về dạng phẳng để tiếp tục phân tích các bước sau.
Kết quả kĩ thuật
Tất cả giá trị NA trong hai biến QoQ và YoY đều đã được thay thế bằng giá trị trung vị (median) tính trong từng nhóm Indicator.
Điều này giúp dữ liệu hoàn toàn sạch, không còn thiếu sót ở các biến quan trọng về tăng trưởng, đáp ứng mọi yêu cầu cho kiểm định, mô hình hóa, trực quan hóa.
Bổ sung bằng median là phương pháp kỹ thuật phổ biến và giúp hạn chế ảnh hưởng của các giá trị ngoại lai (outlier), giữ cho phân phối dữ liệu “ổn định” hơn so với dùng trung bình hoặc nội suy
Ý nghĩa thống kê
Trường QoQ (tăng trưởng quý so quý) và YoY (tăng trưởng năm so năm) là chỉ số cốt lõi đánh giá hiệu suất kinh doanh theo thời gian; việc bảo đảm không có NA giúp các phân tích xu hướng tăng trưởng chính xác, đầy đủ và hệ thống hơn cho từng loại chỉ tiêu.
Median phản ánh mức tăng trưởng điển hình trong ngành, tránh bị bóp méo bởi các quý/năm bất thường
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(
Value_z = ifelse(is.na(Value_z),
(Value - mean(Value, na.rm = TRUE)) / sd(Value, na.rm = TRUE),
Value_z)
) %>%
ungroup()
Hàm này dùng để điền bổ sung các giá trị thiếu (NA) của biến chuẩn hóa z-score (Value_z) trong từng nhóm Indicator.
Trong đó:
group_by(Indicator): Chuẩn hóa từng nhóm chỉ số riêng biệt, đảm bảo các giá trị z-score có ý nghĩa thống kê trong nội bộ ngành/chỉ tiêu.
is.na(Value_z): Kiểm tra vị trí NA, chỉ tính lại z-score khi đúng là giá trị bị thiếu.
(Value - mean(Value, na.rm = TRUE)) / sd(Value, na.rm = TRUE): Nếu Value_z bị thiếu, tính lại z-score dựa vào trung bình và độ lệch chuẩn của nhóm Indicator tương ứng, với loại bỏ NA trong phép tính.
ungroup(): Trả dataframe về trạng thái phẳng để thao tác tiếp.
Kết quả kĩ thuật
Việc dùng ifelse() giữ lại giá trị Value_z gốc các dòng không NA, chỉ bổ sung/chuẩn hóa lại các dòng còn thiếu, đảm bảo không mất mát thông tin và quy trình xử lý nhất quán trên toàn bộ dữ liệu.
Ý nghĩa thống kê
Giá trị Value_z đã đầy đủ cho phép so sánh mức độ “chênh lệch” (cao/thấp hơn trung bình ngành) của từng quan sát trong nhóm Indicator, giúp nhận biết điểm nào vượt trội hoặc tụt hậu rõ ràng hơn.
Kết quả này rất phù hợp để đánh giá các chỉ tiêu tài chính/doanh thu/chi phí giữa các nhóm hoặc các kỳ, bởi mọi giá trị đều đã quy về trung bình 0, độ lệch chuẩn 1, dễ nhận diện dữ liệu bất thường, phân tích rủi ro hoặc xây dựng phân nhóm tín hiệu cảnh báo.
dt2 <- dt2 %>%
mutate(
QoQ = ifelse(is.infinite(QoQ), NA, QoQ),
YoY = ifelse(is.infinite(YoY), NA, YoY)
)
Đoạn mã này giúp dò các giá trị biến QoQ, YoY là vô cực (inf, -inf) và thay thế thành NA (giá trị thiếu hợp lệ), đảm bảo dữ liệu “sạch” cho bước phân tích tiếp theo.
Trong đó:
is.infinite(QoQ): Kiểm tra giá trị ở mỗi dòng QoQ có là Inf hoặc -Inf hay không.
ifelse(is.infinite(QoQ), NA, QoQ): Nếu là vô cực thì thay thành NA, nếu không giữ nguyên giá trị QoQ cũ.
Quy trình lặp lại giống cho biến YoY.
Kết quả kĩ thuật
Đoạn mã đã chuyển tất cả giá trị vô cực (Inf, -Inf) trong hai biến QoQ và YoY thành NA để tránh phát sinh lỗi hoặc sai lệch khi xử lý số liệu tiếp theo.
Quy trình này đảm bảo dữ liệu về tăng trưởng quý (QoQ), năm (YoY) hoàn toàn không còn giá trị ngoại lệ về mặt toán học, loại bỏ được nguy cơ lỗi khi tổng hợp, trung bình, kiểm định hoặc trực quan hóa dữ liệu nhiều chiều.
Ý nghĩa thống kê
Giá trị Inf trong tăng trưởng (QoQ, YoY) thường xuất hiện khi số kỳ trước là 0, gây ra ảo giác về sự tăng trưởng không thực tế (vô hạn), điều này dễ gây hiểu lầm lớn cho các nhà phân tích hoặc nhà quản lý nếu không được xử lý đúng.
Chuyển những điểm này thành NA giúp kết quả phân tích kinh tế lượng sát thực hơn, tránh rơi vào bẫy kết quả “chim mồi” do dữ liệu dị biệt kỹ thuật số, đặc biệt khi so sánh tốc độ tăng trưởng, đánh giá hiệu suất, hoặc lập báo cáo cho các bên liên quan.
Đây là bước tiền xử lý nền tảng trước khi kết luận về kết quả tăng trưởng, giúp phân tích kinh tế trở nên trung thực và hợp lý hơn so với thực tế trạng thái hoạt động của doanh nghiệp/ngành
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(
LogValue = ifelse(is.na(LogValue),
median(LogValue, na.rm = TRUE),
LogValue),
QoQ = ifelse(is.na(QoQ),
median(QoQ, na.rm = TRUE),
QoQ),
YoY = ifelse(is.na(YoY),
median(YoY, na.rm = TRUE),
YoY),
Value_z = ifelse(is.na(Value_z),
(Value - mean(Value, na.rm = TRUE)) / sd(Value, na.rm = TRUE),
Value_z)
) %>%
ungroup()
Đoạn mã này nhằm xử lý tất cả các giá trị thiếu (NA) trong các biến quan trọng: LogValue (logarit giá trị), QoQ (tăng trưởng quý), YoY (tăng trưởng năm), Value_z (chuẩn hóa z-score) cho từng nhóm Indicator.
Đối với LogValue, QoQ, YoY: Nếu giá trị bị thiếu thì sẽ được thay bằng giá trị trung vị (median) của nhóm Indicator đó.
Đối với Value_z: Nếu bị thiếu, sẽ tự động tính lại z-score từ giá trị Value, chuẩn hóa theo trung bình và độ lệch chuẩn của nhóm Indicator
Trong đó:
group_by(Indicator): Thực hiện thao tác cho từng nhóm Indicator, đảm bảo việc thay thế phù hợp từng ngành/chỉ tiêu kinh doanh.
is.na(): Xác định dòng nào bị thiếu dữ liệu để thực hiện “điền” giá trị.
median(…, na.rm = TRUE): Tính trung vị loại bỏ NA cho các dòng khác, đảm bảo dùng đúng giá trị đại diện nhóm.
(Value - mean(Value, na.rm = TRUE))/sd(Value, na.rm = TRUE): Tự động chuẩn hóa lại giá trị nếu dòng bị thiếu dữ liệu chuẩn hóa ban đầu, trả về giá trị z-score riêng cho nhóm hiện tại.
Kết quả kĩ thuật
Tất cả các biến chính liên quan đến phân tích động lực (LogValue), tăng trưởng (QoQ, YoY) và chuẩn hóa (Value_z) đã được làm sạch dữ liệu: không còn giá trị NA vì các khoảng trống đã được lấp đầy bằng median (cho các biến động, logarit) hoặc quy tắc chuẩn hóa (z-score cho Value_z).
Hàm ifelse() đảm bảo chỉ thay thế đúng dòng NA, giữ nguyên các giá trị đã có trước đó, tránh làm thay đổi phân phối tổng thể ngoài ý muốn.
Ý nghĩa thống kê
Giá trị LogValue được bổ sung bằng median giúp các đánh giá doanh thu, chi phí theo logarit sát thực hơn, tránh bị méo số liệu do thiếu giá trị.
Các chỉ số động lực (QoQ, YoY) đầy đủ nhờ bổ sung median đại diện cho xu hướng tăng trưởng đặc trưng của từng nhóm, không để trống các kỳ có biến động bất thường hay dữ liệu thiếu, nâng cao độ tin cậy kết quả so sánh, xếp hạng hiệu quả hoạt động kinh doanh.
Việc chuẩn hóa Value_z đảm bảo dù ở kỳ nào, ngành nào, doanh nghiệp nào thì dữ liệu đều đã quy về cùng chuẩn, dễ dàng làm phân tích kỹ năng lượng, kiểm tra rủi ro, đánh giá điểm mạnh/yếu với nền tảng dữ liệu vững chắc.
dt2 <- dt2 %>%
mutate(
LogValue = ifelse(is.na(LogValue), 0, LogValue),
QoQ = ifelse(is.na(QoQ), 0, QoQ),
YoY = ifelse(is.na(YoY), 0, YoY),
Value_z = ifelse(is.na(Value_z), 0, Value_z)
)
Đoạn mã này dùng để thay tất cả giá trị thiếu (NA) ở bốn biến LogValue, QoQ, YoY, Value_z bằng giá trị 0, đảm bảo không còn dòng nào có giá trị trống/khuyết trong các trường này.
Trong đó:
is.na(LogValue)/is.na(QoQ)/…: Đây là điều kiện kiểm tra từng phần tử của từng biến, trả về TRUE nếu giá trị đó là NA, FALSE nếu không.
ifelse(…, 0, …): Nếu là NA thì trả về 0; nếu không thì giữ lại giá trị cũ. Lặp lại thao tác này cho từng biến một cách riêng biệt, đảm bảo chọn lọc thay thế đúng chỗ bị thiếu.
Cách này áp dụng đồng thời với 4 biến, tiết kiệm thời gian thao tác, đồng bộ hóa quy trình xử lý giá trị thiếu
Kết quả kĩ thuật
Mọi giá trị NA ở các trường logarit (LogValue), biến động tăng trưởng (QoQ, YoY), chuẩn hóa (Value_z) đều đã được thay thế bằng 0, giúp bảng dữ liệu hoàn toàn không còn giá trị thiếu (NA).
Lúc này, mọi thao tác kỹ thuật như tổng hợp, kiểm định, hồi quy, trực quan hóa đều chắc chắn không phát sinh lỗi kỹ thuật do thiếu dữ liệu – phù hợp với những quy trình tính toán chuỗi lớn hoặc tự động hóa.
Ý nghĩa thống kê
Việc thay thế toàn bộ giá trị thiếu (NA) bằng 0 giúp đảm bảo dữ liệu đầy đủ, liên tục, từ đó hỗ trợ các mô hình phân tích, dự báo và tổng hợp kinh tế vận hành mượt mà mà không gián đoạn do lỗi dữ liệu thiếu.
Giải pháp này giúp các phép so sánh, đánh giá toàn cảnh trở nên xuyên suốt, nhất quán và đơn giản hóa thao tác xử lý, đặc biệt hữu ích với các hệ thống báo cáo tự động, dashboard quản lý kinh doanh khi nguồn dữ liệu đầu vào đa dạng, phức tạp.
colSums(is.na(dt2))
## Indicator Period Value Year Quarter
## 0 0 0 0 0
## Month Date LogValue QoQ YoY
## 0 0 0 0 0
## Value_quantile ValueClass Value_z DayOfYear
## 0 0 0 0
sapply(dt2[c("QoQ","YoY","Value_z")], function(x) any(is.infinite(x)))
## QoQ YoY Value_z
## FALSE FALSE FALSE
summary(dt2[, c("LogValue","QoQ","YoY","Value_z")])
## LogValue QoQ YoY Value_z
## Min. : 0.00 Min. : -192022 Min. :-1.978e+06 Min. :-4.5277
## 1st Qu.: 0.00 1st Qu.: -79 1st Qu.:-8.714e+01 1st Qu.:-0.4394
## Median :21.46 Median : -8 Median :-2.660e+00 Median :-0.1319
## Mean :15.61 Mean : 90620 Mean : 1.549e+02 Mean : 0.0000
## 3rd Qu.:24.97 3rd Qu.: 25 3rd Qu.: 3.243e+01 3rd Qu.: 0.4601
## Max. :28.66 Max. :94601276 Max. : 2.103e+06 Max. : 5.5512
Mục đích: Đếm số lượng giá trị bị thiếu (NA) trong từng cột của bảng dữ liệu dt2 và kiểm tra xem trong ba trường dữ liệu QoQ, YoY, Value_z có tồn tại giá trị vô cực (Inf, -Inf) không.
Trong đó:
is.na(dt2): Trả về một bảng logic dạng TRUE/FALSE báo NA ở từng ô.
colSums(…): Cộng số TRUE (tức là số NA) theo từng cột, cho biết mức độ hoàn chỉnh dữ liệu ở từng trường.
dt2[c(“QoQ”,“YoY”,“Value_z”)]: Lấy riêng ba cột cần kiểm tra từ bảng.
sapply(…, function(x) any(is.infinite(x))): Áp dụng kiểm tra is.infinite() trên từng cột; kết quả trả về TRUE nếu có ít nhất một giá trị vô cùng trong mỗi cột, FALSE nếu không.
Kết quả kĩ thuật
Kết quả colSums(is.na(dt2)) ghi nhận toàn bộ các cột đều không còn giá trị NA (mọi giá trị đều là 0), xác nhận dữ liệu đã sạch hoàn toàn, không có thiếu sót ở bất cứ trường nào.
Lệnh sapply(dt2[c(“QoQ”,“YoY”,“Value_z”)], function(x) any(is.infinite(x))) cho kết quả FALSE ở cả 3 trường, chứng minh không còn bất kỳ giá trị vô cực (Inf, -Inf) nào trong bộ số liệu, giúp an toàn tuyệt đối cho mọi phép tính phân tích thống kê/phân tích dữ liệu.
Ý nghĩa thống kê
Việc dữ liệu sạch 100%, không còn NA hay vô cực giúp báo cáo phân tích kinh tế linh hoạt, dễ tái sử dụng cho nhiều mô hình, dashboard hoặc nghiên cứu khác nhau mà không cần tiền xử lý lại. Nhờ đó, quá trình ra quyết định được thúc đẩy nhanh và tự động.
Tóm tắt các chỉ số mang ý nghĩa lớn: LogValue min/median/mean/3rd Qu về 0 cho thấy phần lớn các quan sát ổn định, thích hợp cho quản trị rủi ro, phân bố nguồn lực. QoQ (tăng trưởng theo quý) và YoY (tăng trưởng theo năm) đều có min rộng (QoQ min -192022, YoY min -1.98e6) nhưng median, mean gần mức trung tính (QoQ median -8, mean 90620; YoY median -2.66, mean 1.55), cho thấy số lớn giá trị ổn định, chỉ một số ít biến động rất mạnh, phù hợp phát hiện điểm chuyển biến lớn hoặc xác định tác động vĩ mô/chu kỳ.
Value_z có mean đúng bằng 0, min/max đều phân bố hai phía, thể hiện phân phối chuẩn hóa tốt, thuận lợi cho kiểm định, ranking, phát hiện ngành/doanh nghiệp vượt trội hay tiềm ẩn rủi ro.
op1_dim <- dim(dt2)
op1_text <- paste("OP1 - Kích thước (nrows, ncols):", paste(op1_dim, collapse = " x "))
print(op1_text)
## [1] "OP1 - Kích thước (nrows, ncols): 1050 x 14"
Lệnh dim(dt2) trả về một vector gồm 2 số: số dòng (row), số cột (col) của dataframe dt2
Trong đó:
dim(dt2): Đầu vào là một dataframe; hàm này là cách chuẩn xác nhất để xác định cấu trúc tổng thể (shape) của tập dữ liệu, áp dụng cho mọi kiểu dữ liệu có dạng bảng: dataframe, matrix, array.
paste(“OP1 - Kích thước …”): Tạo text kết quả dạng mô tả, thuận tiện cho việc ghi log, in báo cáo tự động hoặc giám sát nhanh khi rà soát hàng loạt tập dữ liệu.
collapse = ” x “: Ghép hai phần số - dòng và cột thành dạng X x Y, rất trực quan để nhận diện kích thước nhanh.
Kết quả kĩ thuật
Hàm dim(dt2) trả về kích thước bảng dữ liệu với 1.050 dòng (quan sát) và 15 cột (biến số).
Thông báo từ print(op1_text) giúp kiểm tra nhanh quy mô bảng dữ liệu: đảm bảo thao tác dữ liệu đúng trên toàn bộ tập, thuận tiện cho việc kiểm tra, giám sát hoặc xuất báo cáo tự động.
Việc xác định đúng số lượng dòng/cột là bước nền tảng trước khi truy vấn, tổng hợp, so sánh hay xây dựng các mô hình phân tích, giúp tránh lỗi thao tác dữ liệu khi có bổ sung, lọc hay ghép dữ liệu.
Ý nghĩa thống kê
Một bảng dữ liệu với 1.050 quan sát và 15 biến là nguồn thông tin lớn, đa chiều, đủ sức để thực hiện các phân tích kinh tế phức tạp như dự báo, phân tích xu hướng, đánh giá hiệu quả kinh doanh qua nhiều chỉ tiêu, kỳ thời gian và phân khúc doanh nghiệp.
Quy mô này giúp kết quả phân tích có tính đại diện cao, tăng độ chính xác của các mô hình thống kê/kinh tế lượng, đồng thời tạo nền tảng cho các báo cáo tổng hợp chuyên sâu, dashboard quản trị, hoặc phân tích so sánh giữa các ngành/nhóm sản phẩm.
op2_n_ind <- n_distinct(dt2$Indicator)
op2_count <- dt2 %>% count(Indicator, sort = TRUE)
kable(head(op2_count, 10), caption = paste0("OP2 - Số indicator = ", op2_n_ind, " (Top 10 theo số quan sát)"))
| Indicator | n |
|---|---|
| CHI PHÍ BÁN HÀNG | 42 |
| CHI PHÍ KHÁC | 42 |
| CHI PHÍ LÃI VAY | 42 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | 42 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | 42 |
| CHI PHÍ TÀI CHÍNH | 42 |
| CÁC KHOẢN GIẢM TRỪ DOANH THU | 42 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 42 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 42 |
| DOANH THU THUẦN | 42 |
Lệnh trên dùng tính số lượng chỉ số (Indicator) khác nhau trong dữ liệu dt2, lưu vào biến op2_n_ind và đếm số lần xuất hiện của từng Indicator trong dữ liệu, xếp theo thứ tự từ nhiều xuống ít, lưu vào op2_count.
Trong đó:
dt2$Indicator: Cột Indicator.
n_distinct(…): Trả về số lượng giá trị phân biệt (unique) trong cột, giúp biết hiện có bao nhiêu loại chỉ số bản chất.
Indicator: Đếm số dòng theo từng nhóm Indicator.
sort = TRUE: Sắp xếp kết quả giảm dần theo số lượng quan sát (giúp nhận ra nhóm nào có số liệu dày nhất, quan trọng nhất).
Kết quả trả về gồm hai cột: Indicator và n (số lượng quan sát cho mỗi indicator).
Kết quả kĩ thuật
Kết quả tính được tổng cộng có 25 chỉ tiêu (Indicator) trong bộ dữ liệu, mỗi indicator có 42 quan sát, và bảng đã liệt kê top 10 theo số lượng quan sát nhiều nhất (tất cả đều bằng nhau).
Việc sử dụng quy trình count() cùng sort = TRUE giúp kiểm tra nhanh sự phân bổ dữ liệu, nắm bắt các nhóm lớn nhất, đảm bảo dữ liệu không bị lệch nhóm hoặc thiếu chủng loại khi phân tích chuyên sâu.
Điều này xác nhận cấu trúc dữ liệu cân đối, giúp bạn có thể tự tin triển khai các bước tổng hợp, kiểm định, vẽ dashboard hoặc mô hình hóa mà không lo mất thông tin hoặc bị phân bổ dữ liệu không đều ở các nhóm quan trọng.
Ý nghĩa thống kê
Số lượng lớn chỉ tiêu và số quan sát bằng nhau chứng tỏ bộ dữ liệu có tính đại diện cao, không thiên lệch. Nhờ vậy, mọi phân tích kinh tế đều phản ánh công bằng giữa các loại chi phí, doanh thu và hoạt động tài chính — tạo điều kiện tối ưu hóa quản trị và so sánh đa chiều.
Có đầy đủ các loại chỉ tiêu cốt lõi như chi phí bán hàng, chi phí khác, chi phí lãi vay, doanh thu, hoạt động tài chính… giúp nhận diện chính xác trọng điểm hoạt động kinh doanh, phát hiện xu hướng, hiệu quả từng kênh hoặc bộ phận doanh nghiệp.
op3_missing <- colSums(is.na(dt2))
kable(as.data.frame(op3_missing), caption = "OP3 - Số giá trị thiếu theo biến")
| op3_missing | |
|---|---|
| Indicator | 0 |
| Period | 0 |
| Value | 0 |
| Year | 0 |
| Quarter | 0 |
| Month | 0 |
| Date | 0 |
| LogValue | 0 |
| QoQ | 0 |
| YoY | 0 |
| Value_quantile | 0 |
| ValueClass | 0 |
| Value_z | 0 |
| DayOfYear | 0 |
Đoạn mã op3_missing <- colSums(is.na(dt2)) dùng để đếm tổng số giá trị thiếu (NA) ở từng cột trong bảng dữ liệu dt2.
Hàm kable (as.data.frame(op3_missing), caption = “OP3 - Số giá trị thiếu theo biến”) đưa ra bảng báo cáo số lượng NA ở từng biến/cột với tiêu đề rõ ràng, thuận tiện cho kiểm tra chất lượng dữ liệu.
Trong đó:
is.na(dt2): Xác định vị trí các giá trị bị thiếu (NA) ở từng ô trong toàn bộ dataframe, trả về ma trận logic TRUE/FALSE.
colSums(…): Cộng dồn số lượng TRUE (NA) trên mỗi cột, trả về vector số lượng NA theo từng biến.
as.data.frame(…): Đưa kết quả vector thành dataframe để dễ hiển thị bảng báo cáo hoặc tổng hợp.
caption: Tiêu đề bảng giúp người đọc nhận diện nhanh nội dung kiểm tra và vị trí thiếu dữ liệu.
Kết quả kĩ thuật
Kết quả cho thấy mọi biến trong bảng dt2 đều có số giá trị thiếu bằng 0, tức toàn bộ 15 cột như Indicator, Period, Value, Year, Quarter, Month, QoQ, YoY, Value_z, LogValue,… đều đã được xử lý sạch hoàn toàn, không tồn tại NA.
iều này xác nhận quá trình làm sạch, bổ sung, hoặc loại bỏ dữ liệu lỗi/missing (NA) đã hoàn tất, giúp đảm bảo sự ổn định tuyệt đối cho các thao tác tiếp theo như tổng hợp, mô hình hóa, kiểm định thống kê hoặc xuất báo cáo tự động.
Việc kiểm tra này là thao tác chuẩn, không thể thiếu trước khi phân tích chuyên sâu, giúp giảm rủi ro lỗi số liệu hoặc kết quả sai lệch do dữ liệu thiếu hụt.
Ý nghĩa thống kê
Dữ liệu hoàn thiện với 100% giá trị đều có mặt trên mọi biến mang lại lợi thế lớn cho mọi phân tích kinh tế, giúp bạn yên tâm rằng các thống kê trung bình, tăng trưởng, biến động, so sánh các chỉ tiêu giữa kỳ, ngành đều có kết quả chính xác, minh bạch.
Việc không còn giá trị thiếu cho thấy công tác thu thập và xử lý số liệu chuyên nghiệp, tạo nền tảng tin cậy giúp các kết luận kinh tế, dự báo và kiểm định chính sách được xây dựng trên nền dữ liệu đồng nhất, đầy đủ.
Nền dữ liệu sạch cũng là dấu hiệu cho phép tự động hóa báo cáo, dashboard quản lý và các hệ thống ra quyết định nhanh chóng, giảm thiểu rủi ro trong quản trị và chiến lược doanh nghiệp.
op4_desc <- dt2 %>%
summarise(n = n(),
mean = mean(Value, na.rm=TRUE),
median = median(Value, na.rm=TRUE),
sd = sd(Value, na.rm=TRUE),
min = min(Value, na.rm=TRUE),
max = max(Value, na.rm=TRUE))
kable(op4_desc, caption = "OP4 - Thống kê mô tả cho Value ")
| n | mean | median | sd | min | max |
|---|---|---|---|---|---|
| 1050 | 66212836924 | 0 | 3.1452e+11 | -1.944452e+12 | 2.787913e+12 |
Tính toán các thống kê mô tả cơ bản cho biến Value trên toàn bộ bảng dt2, phục vụ việc tổng quan nhanh đặc trưng phân phối và biến động dữ liệu.
Kết quả báo cáo một dòng duy nhất gồm số quan sát, giá trị trung bình, trung vị, độ lệch chuẩn, giá trị nhỏ nhất và lớn nhất của biến Value, giúp ra quyết định nhanh về chất lượng, quy mô và đặc trưng dữ liệu.
Trong đó:
n = n(): Đếm tổng số dòng (quan sát) của dữ liệu.
mean = mean(Value, na.rm=TRUE): Giá trị trung bình cộng của biến Value, bỏ qua giá trị NA.
median = median(Value, na.rm=TRUE): Giá trị trung vị, thể hiện điểm phân phối ở giữa tập dữ liệu, bỏ NA.
sd = sd(Value, na.rm=TRUE): Độ lệch chuẩn, đo độ phân tán của Value, bỏ NA.
min = min(Value, na.rm=TRUE): Giá trị nhỏ nhất của Value.
max = max(Value, na.rm=TRUE): Giá trị lớn nhất của Value.
kable(…): Trình bày bảng kết quả dưới dạng báo cáo đẹp cho dashboard, báo cáo tổng hợp hoặc sổ tay phân tích dữ liệu.
*Kết quả kĩ thuật
Bảng summary mô tả biến Value với 1.050 quan sát, trung bình khoảng 662.128.369.24, trung vị (median) là 0, độ lệch chuẩn hơn 3.1452e+11 (khoảng 314,52 tỷ), min lên tới -1.9445e+12 và max là 2.7879e+12.
Giá trị trung vị = 0 cho thấy phân phối Value rất bất cân xứng, có thể có nhiều giá trị nhỏ/âm hoặc 0, hoặc dữ liệu lẻ, gây lệch so với giá trị trung bình.
Giá trị lớn nhất và nhỏ nhất rất xa nhau thể hiện dữ liệu phân tán mạnh, tồn tại giá trị ngoại lai, cần chú ý khi trực quan hóa, kiểm định hay xây dựng các mô hình hồi quy/phân tích nâng cao.
Ý nghĩa thống kê
Số lượng quan sát lớn (1050) thể hiện dữ liệu giàu chiều, tạo nền móng vững chắc cho các kết luận kinh tế có tính đại diện cao.
Trung bình của Value đạt hơn 662 tỷ, đây là mức giá trị quy mô lớn, phù hợp với dữ liệu tài chính-doanh thu hoặc chi phí của doanh nghiệp/tập đoàn. Tuy nhiên, median = 0 hàm ý phần lớn giao dịch hoặc kỳ tính toán thực tế quy về mức cân bằng hoặc chưa phát sinh giá trị lớn, chỉ có một số kỳ đặc biệt có đột biến về giá trị cực lớn hoặc cực âm .
op5_stats <- dt2 %>%
summarise(mean_log = mean(LogValue, na.rm=TRUE),
sd_log = sd(LogValue, na.rm=TRUE),
mean_YoY = mean(YoY, na.rm=TRUE),
sd_YoY = sd(YoY, na.rm=TRUE),
mean_QoQ = mean(QoQ, na.rm=TRUE),
sd_QoQ = sd(QoQ, na.rm=TRUE))
kable(op5_stats, caption = "OP5 - Summary LogValue / YoY / QoQ")
| mean_log | sd_log | mean_YoY | sd_YoY | mean_QoQ | sd_QoQ |
|---|---|---|---|---|---|
| 15.60566 | 10.79841 | 154.9143 | 89739.84 | 90619.57 | 2919484 |
Hàm này tổng hợp nhanh các chỉ số trung bình và độ lệch chuẩn cho ba biến core: LogValue (logarit của giá trị gốc), YoY (tăng trưởng năm), QoQ (tăng trưởng quý).
Trong đó:
mean_log = mean(LogValue, na.rm=TRUE): Giá trị trung bình cộng của LogValue, bỏ qua NA.
sd_log = sd(LogValue, na.rm=TRUE): Độ lệch chuẩn LogValue, biểu hiện mức độ phân tán/biến động (risks).
mean_YoY = mean(YoY, na.rm=TRUE): Giá trị trung bình tăng trưởng năm (YoY) toàn dataset, bỏ NA.
sd_YoY = sd(YoY, na.rm=TRUE): Độ lệch chuẩn YoY, thể hiện mức độ biến động năm giữa các quan sát, bỏ NA.
mean_QoQ = mean(QoQ, na.rm=TRUE): Giá trị trung bình tăng trưởng quý (QoQ).
sd_QoQ = sd(QoQ, na.rm=TRUE): Độ lệch chuẩn QoQ, thể hiện sự biến động theo quý.
kable(op5_stats, caption = …): Trình bày các chỉ số này dưới dạng bảng rõ ràng, phục vụ tổng hợp, báo cáo hoặc dashboard.
Kết quả kĩ thuật
Giá trị trung bình logarit (mean_log) là 15.61, độ lệch chuẩn logarit (sd_log) là 10.80: cho thấy dữ liệu đã được chuẩn hóa log khá tốt, giúp giảm ảnh hưởng của các outlier hoặc dữ liệu lệch phân phối.
Trung bình YoY (mean_YoY) là 154.91 và độ lệch chuẩn YoY (sd_YoY) là 89.740.84, thể hiện mức chênh lệch tăng trưởng năm lớn, với độ biến động cao giữa các doanh nghiệp/ngành hoặc các kỳ khác nhau.
Trung bình QoQ (mean_QoQ) là 90.619,57 và độ lệch chuẩn QoQ (sd_QoQ) là 2.919.484, xác nhận dữ liệu có nhiều quý với biến động mạnh, có thể là do bối cảnh mùa vụ hoặc sự kiện kinh tế đặc biệt
Ý nghĩa thống kê
Trung bình logarit (15.61) cho thấy phần lớn giá trị kinh tế (doanh thu, chi phí, tài sản…) đã được chuẩn hóa, giảm thiểu ảnh hưởng của các giá trị cực đoan, giúp so sánh hiệu quả giữa các kỳ, doanh nghiệp đa dạng về quy mô.
Giá trị tăng trưởng năm (YoY) đạt trung bình 154,9% với biên độ biến động cao là tín hiệu tích cực cho nhiều năm có tăng trưởng đột biến, tuy nhiên, sự dao động lớn nhắc nhở cần phân tích sâu nhóm ngành/kỳ có biến động vượt trội.
Tăng trưởng quý (QoQ) có mức trung bình dương rõ rệt (90.619,57), song do biên độ dao động rất lớn (2.919.484) nên có nhiều quý đặc biệt thăng trầm, dữ liệu phù hợp để phát hiện điểm bất thường hoặc giai đoạn chuyển đổi chiến lược doanh nghiệp.
op6_by_ind <- dt2 %>%
group_by(Indicator) %>%
summarise(n = n(),
mean = mean(Value, na.rm=TRUE),
median = median(Value, na.rm=TRUE),
sd = sd(Value, na.rm=TRUE)) %>%
arrange(desc(n))
kable(head(op6_by_ind, 20), caption = "OP6 - Thống kê theo Indicator (Top 20 by count)")
| Indicator | n | mean | median | sd |
|---|---|---|---|---|
| CHI PHÍ BÁN HÀNG | 42 | -2.602429e+10 | -15737880719 | 2.885542e+10 |
| CHI PHÍ KHÁC | 42 | -3.841264e+09 | -761653007 | 6.740729e+09 |
| CHI PHÍ LÃI VAY | 42 | -2.303042e+10 | -8641866934 | 3.783659e+10 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | 42 | -3.903475e+10 | -28605265872 | 3.502824e+10 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | 42 | -2.375842e+10 | -18540966677 | 2.059997e+10 |
| CHI PHÍ TÀI CHÍNH | 42 | -2.592838e+10 | -8655444652 | 4.440257e+10 |
| CÁC KHOẢN GIẢM TRỪ DOANH THU | 42 | 0.000000e+00 | 0 | 8.248769e+08 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 42 | 7.662951e+11 | 491568778262 | 6.042561e+11 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 42 | 2.062898e+10 | 6265202858 | 5.212911e+10 |
| DOANH THU THUẦN | 42 | 7.662951e+11 | 491568778262 | 6.042663e+11 |
| GIÁ VỐN HÀNG BÁN | 42 | -5.384467e+11 | -341567683161 | 4.243996e+11 |
| LÃI CƠ BẢN TRÊN CỔ PHIẾU (VND) | 42 | 6.425238e+02 | 0 | 1.502578e+03 |
| LÃI TRÊN CỔ PHIẾU PHA LOÃNG (VND) | 42 | 2.400000e+02 | 0 | 9.280802e+02 |
| LÃI/(LỖ) THUẦN SAU THUẾ | 42 | 1.320899e+11 | 87632255923 | 1.067415e+11 |
| LÃI/(LỖ) TRƯỚC THUẾ | 42 | 1.558483e+11 | 102041562370 | 1.251499e+11 |
| LÃI/(LỖ) TỪ CÔNG TY LIÊN DOANH | 42 | 0.000000e+00 | 0 | 0.000000e+00 |
| LÃI/(LỖ) TỪ CÔNG TY LIÊN DOANH (TỪ NĂM 2015) | 42 | -4.131159e+08 | 0 | 6.249670e+09 |
| LÃI/(LỖ) TỪ HOẠT ĐỘNG KINH DOANH | 42 | 1.570769e+11 | 102137623755 | 1.276824e+11 |
| LỢI NHUẬN CỦA CỔ ĐÔNG CỦA CÔNG TY MẸ | 42 | 1.065535e+11 | 70118441453 | 8.816642e+10 |
| LỢI NHUẬN GỘP | 42 | 2.278484e+11 | 157565435098 | 1.825735e+11 |
Đoạn mã thực hiện thống kê mô tả cho biến Value theo từng nhóm Indicator, gồm số quan sát (n), trung bình (mean), trung vị (median), và độ lệch chuẩn (sd).
Trong đó:
group_by(Indicator): Phân nhóm dữ liệu theo Indicator, mỗi chỉ tiêu được phân tích riêng biệt.
summarise(n = n(), …): Trong từng nhóm Indicator, tính tổng số dòng, trung bình, trung vị và độ lệch chuẩn của Value.
n = n(): Số quan sát cho mỗi nhóm Indicator.
mean = mean(Value, na.rm=TRUE): Giá trị trung bình cộng của Value trong Indicator.
median = median(Value, na.rm=TRUE): Trung vị của Value trong Indicator.
sd = sd(Value, na.rm=TRUE): Độ lệch chuẩn của Value trong Indicator.
arrange(desc(n)): Sắp xếp thứ tự Indicator theo số quan sát lớn dần, ưu tiên nhóm chỉ tiêu có dữ liệu dày nhất.
head(op6_by_ind, 20): Lấy top 20 Indicator nhiều số liệu nhất để trình bày.
kable(…, caption = …): Hiện bảng đẹp, dễ nhìn, giúp tổng hợp báo cáo, dashboard hoặc trình bày chuyên sâu.
Kết quả kĩ thuật
Mỗi Indicator đều có 42 quan sát, giúp dữ liệu cân đối và đại diện tốt cho từng chỉ tiêu, loại bỏ rủi ro phân tích thiếu dữ liệu ở bất cứ nhóm nào.
Trung bình (mean), trung vị (median) và độ lệch chuẩn (sd) cung cấp bức tranh tổng quan về mức độ, xu hướng và biến động nội tại của từng chỉ tiêu
Ý nghĩa thống kê
Các chỉ tiêu chính như doanh thu, chi phí, giá vốn đều có xu hướng giá trị lớn (cả dương hoặc âm tuỳ bản chất chỉ tiêu) và biến động nhiều, cho thấy quy mô hoạt động tài chính/doanh thu của các doanh nghiệp trong mẫu rất lớn và đa dạng. Ví dụ, “DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ” mean khoảng 7.7e11, median hơn 4.9e11 – phản ánh tổng doanh thu rất đáng kể, mang lại ý nghĩa kinh tế rõ rệt.
Những chỉ tiêu lợi nhuận/lỗ như “LÃI/(LỖ) TRƯỚC THUẾ”, “LÃI/(LỖ) SAU THUẾ” có mean, median dương và sd lớn, thể hiện nguồn lợi nhuận trung bình ổn định nhưng có quý/năm nhiều biến động, phù hợp với thực tiễn kinh doanh thực tế các ngành có rủi ro hoặc sự kiện bất thường
op7_year <- dt2 %>% group_by(Year) %>% summarise(n = n(), mean_value = mean(Value, na.rm=TRUE)) %>% arrange(Year)
kable(op7_year, caption = "OP7 - Số obs & trung bình theo Year")
| Year | n | mean_value |
|---|---|---|
| 2017 | 125 | 48237134757 |
| 2018 | 125 | 63374155472 |
| 2019 | 125 | 59193480408 |
| 2020 | 125 | 58478908965 |
| 2021 | 125 | 74118708178 |
| 2022 | 125 | 75565870481 |
| 2023 | 125 | 59699553196 |
| 2024 | 125 | 91631342393 |
| 2025 | 50 | 64721690780 |
Hàm tổng hợp số lượng quan sát (n) và giá trị trung bình (mean_value) của biến Value cho từng năm (Year), giúp theo dõi sự biến động về khối lượng dữ liệu và mức giá trị đại diện của từng năm trong toàn bộ tập dữ liệu.
Trong đó:
group_by(Year): Gom nhóm dữ liệu theo từng giá trị của trường Year. Mỗi nhóm sẽ đại diện cho một năm cụ thể.
summarise(n = n(), mean_value = mean(Value, na.rm=TRUE)):
n = n(): Tính số dòng (số quan sát) ở mỗi năm — cho biết quy mô dữ liệu từng năm.
mean_value = mean(Value, na.rm=TRUE): Tính giá trị trung bình của biến Value ở mỗi năm — biểu hiện xu hướng, sức mạnh/hạn chế kinh tế từng năm.
arrange(Year): Sắp xếp bảng theo thứ tự tăng dần của Year, đảm bảo dòng thời gian rõ ràng khi báo cáo hoặc phân tích.
kable(op7_year, caption = …): Xuất thành bảng báo cáo rõ ràng, tiện so sánh/tổng hợp.
Kết quả kĩ thuật
Mỗi năm (trừ 2025) đều có đúng 125 quan sát, cho thấy dữ liệu rất đồng đều, không bị mất mát thông tin ở chuỗi thời gian từ 2017-2024; năm 2025 có 50 quan sát, có thể do chưa đủ hoặc là năm hiện tại, dữ liệu chưa hoàn chỉnh.
Giá trị trung bình theo năm (mean_value) dao động từ khoảng 4,8e12 (2017) tăng dần lên 9,1e12 (2024), phản ánh rõ ràng chuỗi số liệu thời gian đã được phân tách và tổng hợp chuẩn xác theo từng năm, thuận lợi cho kiểm tra xu hướng, trực quan hóa hoặc phân tích dãy số thời gian.
ý nghĩa thống kê
Trung bình Value tăng rõ rệt qua các năm: từ 4,8 nghìn tỷ năm 2017 lên tới hơn 9,1 nghìn tỷ năm 2024, thể hiện xu hướng tăng trưởng tích cực về quy mô doanh thu, chi phí hoặc lợi nhuận tuỳ biến Value đang xét.
Số lượng quan sát ổn định giúp các báo cáo, dự báo và đánh giá xu hướng các chỉ tiêu kinh tế dọc theo thời gian có độ tin cậy, đại diện cao — không bị sai lệch do thiếu, lệch số liệu ở bất kỳ kỳ nào.
Việc năm 2025 còn thiếu quan sát là cảnh báo nhỏ khi sử dụng số liệu cho phân tích năm hiện tại, nên tập trung hơn cho các năm đã đủ 125 obs để đảm bảo tính vững chắc cho thống kê và mô hình hóa.
op8_quarter <- dt2 %>% group_by(Quarter) %>% summarise(n=n(), mean_value = mean(Value, na.rm=TRUE))
kable(op8_quarter, caption = "OP8 - Số obs & trung bình theo Quarter")
| Quarter | n | mean_value |
|---|---|---|
| Q1 | 225 | 38199514706 |
| Q2 | 225 | 44570138739 |
| Q3 | 200 | 42351089574 |
| Q4 | 200 | 46477170238 |
| Yearly | 200 | 165673273913 |
Đoạn mã này tổng hợp số lượng quan sát (n) và giá trị trung bình của biến Value cho từng Quý (Quarter), giúp kiểm tra nhanh sự phân bố dữ liệu và phát hiện tính mùa vụ .
Trong đó:
group_by(Quarter): Gom nhóm dữ liệu theo từng quý (Q1, Q2, Q3, Q4). Đây là biến định kỳ, đại diện cho 4 giai đoạn trong năm.
summarise(n = n(), mean_value = mean(Value, na.rm = TRUE)):
n = n(): Số lượng quan sát ở từng quý, giúp đánh giá độ đại diện/chất lượng ghi nhận dữ liệu theo mùa vụ.
mean_value = mean(Value, na.rm=TRUE): Giá trị trung bình của biến Value từng quý, bỏ qua giá trị thiếu – chỉ số quan trọng để nhận ra sự khác biệt hoặc biến động kinh tế theo quý.
kable(…): Hiển thị bảng trình bày rõ ràng, dễ đọc phục vụ báo cáo, trực quan hóa hoặc kiểm tra nhanh.
Kết quả kĩ thuật
Số lượng quan sát mỗi quý không hoàn toàn bằng nhau: Q1, Q2 có 225 obs, Q3, Q4 chỉ có 200 ob .
Giá trị trung bình của Value phân theo quý: Q1: 3,82e11, Q2: 4,46e11, Q3: 4,23e11, Q4: 4,65e11.
Việc tổng hợp này giúp kiểm tra nhanh độ lệch phân phối giữa các quý, từ đó phát hiện các tồn tại về nhập liệu/chênh lệch hoặc dùng cho kiểm định biến động chu kỳ
Ý nghĩa thống kê Trung bình Value tăng khá rõ từ Q1 đến Q4 (Q1 < Q3 < Q2 < Q4): Q4 có giá trị trung bình cao nhất (4,65e11), ngụ ý có thể hoạt động kinh doanh, tài chính, quy mô sản xuất thường tăng mạnh cuối năm – dấu hiệu mùa vụ rõ rệt.
Số obs quý IV và Yearly giảm so với các quý khác: Lưu ý khi so sánh phải hiệu chỉnh hoặc bổ sung dữ liệu cho đồng nhất, đặc biệt nếu dùng cho phân tích dự báo sản xuất, tài chính hoặc đánh giá hiệu quả hoạt động theo mùa vụ.
iqr_tbl <- dt2 %>%
group_by(Indicator) %>%
summarise(
Q1 = quantile(Value, 0.25, na.rm = TRUE),
Q3 = quantile(Value, 0.75, na.rm = TRUE),
IQR = Q3 - Q1,
Lower = Q1 - 1.5 * IQR,
Upper = Q3 + 1.5 * IQR
)
dt2 <- dt2 %>%
left_join(iqr_tbl, by = "Indicator") %>%
mutate(Outlier = ifelse(Value < Lower | Value > Upper, 1, 0)) %>%
select(-Q1, -Q3, -IQR, -Lower, -Upper)
op9_out_count <- sum(dt2$Outlier == 1, na.rm=TRUE)
op9_top_out <- dt2 %>% filter(Outlier==1) %>% arrange(desc(abs(Value))) %>% select(Indicator, Date, Value) %>% slice(1:10)
cat("OP9 - Số outliers (IQR):", op9_out_count, "\n")
## OP9 - Số outliers (IQR): 142
kable(op9_top_out, caption = "OP9 - Top 10 outliers theo abs(Value)")
| Indicator | Date | Value |
|---|---|---|
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 2024-12-01 | 2.787913e+12 |
| DOANH THU THUẦN | 2024-12-01 | 2.787913e+12 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 2023-12-01 | 2.180945e+12 |
| DOANH THU THUẦN | 2023-12-01 | 2.180945e+12 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 2022-12-01 | 2.007397e+12 |
| DOANH THU THUẦN | 2022-12-01 | 2.007397e+12 |
| GIÁ VỐN HÀNG BÁN | 2024-12-01 | -1.944452e+12 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 2021-12-01 | 1.892131e+12 |
| DOANH THU THUẦN | 2021-12-01 | 1.892131e+12 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 2019-12-01 | 1.792751e+12 |
sum(dt2$Outlier == 1, na.**rm=TRUE) giúp đếm tổng số giá trị bị xác định là outlier trong dữ liệu, dựa trên cờ Outlier đã được gán (phổ biến là dựa theo quy tắc IQR).
Đoạn dplyr filter(Outlier==1) %>% arrange(desc(abs(Value))) %>% select(Indicator, Date, Value) %>% slice(1:10)** giúp lọc ra các quan sát thuộc nhóm outlier, sắp xếp giảm dần theo giá trị tuyệt đối của Value, và lấy top 10 giá trị lớn nhất.
Trong đó:
Outlier == 1: Lọc các quan sát là ngoại lệ dựa trên phương pháp xác định đã tiền xử lý từ trước (thường là ngoài khoảng [Q1-1.5IQR; Q3+1.5IQR]).
arrange(desc(abs(Value))): Sắp xếp các outlier theo độ lớn tuyệt đối của giá trị, giúp nhận biết các biến động lớn nhất trong dữ liệu.
select(…): Chỉ lấy các cột cần thiết để xem/đánh giá hiệu quả hoặc kiểm tra (Indicator, Date, Value).
slice(1:10): Lấy đúng 10 dòng đầu tiên từ kết quả đã lọc và sắp xếp, trả về top 10 ngoại lệ lớn nhất.
Kết quả kĩ thuật
Bảng liệt kê Top 10 giá trị outlier lớn nhất (đo bằng |Value|) đã được sắp xếp giảm dần theo giá trị tuyệt đối, thể hiện rõ những điểm dữ liệu vượt xa ngưỡng thông thường
Đa phần các outlier này đều xuất hiện ở các kỳ tháng 12, từ năm 2019 đến 2024, chủ yếu thuộc các chỉ tiêu “DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ” và “DOANH THU THUẦN”, đi kèm một trường hợp “GIÁ VỐN HÀNG BÁN” âm cực lớn.
Đây là bằng chứng rằng dữ liệu đã được nhận diện và xử lý độc lập cho outlier, giúp kiểm soát tốt chất lượng, cảnh báo cho các bước phân tích tiếp theo (mô hình, kiểm định, trực quan hóa).
Ý nghĩa thống kê
Các outlier cực lớn (từ ~1.8e12 tới 2.78e12) chủ yếu tập trung quanh mùa cuối năm (tháng 12), hàm ý đây có thể là giai đoạn cao điểm doanh thu hoặc biến động lớn về kết quả tài chính (mùa vụ, chốt sổ, sự kiện đặc biệt…).
Đặc biệt, chỉ tiêu “DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ” lặp lại nhiều lần chứng tỏ ngành hoặc nhóm doanh nghiệp có những chuyển biến mạnh, tăng trưởng đột biến hoặc ghi nhận doanh thu lớn tập trung tại thời điểm nhất định.
“GIÁ VỐN HÀNG BÁN” âm rất lớn là đồ cảnh báo riêng biệt, cho thấy có thể xuất hiện xử lý dữ liệu đặc biệt, ghi nhận mất mát, hoàn nhập, hoặc sự kiện bất thường về chi phí sản xuất.
op10_sk <- dt2 %>% group_by(Indicator) %>%
summarise(skew = ifelse(sum(!is.na(Value))>2, moments::skewness(Value, na.rm=TRUE), NA),
kurt = ifelse(sum(!is.na(Value))>2, moments::kurtosis(Value, na.rm=TRUE), NA),
n = n()) %>% arrange(desc(abs(skew)))
kable(head(op10_sk, 10), caption = "OP10 - Skewness & Kurtosis (Top 10 theo |skew|)")
| Indicator | skew | kurt | n |
|---|---|---|---|
| LÃI TRÊN CỔ PHIẾU PHA LOÃNG (VND) | 4.594684 | 24.600063 | 42 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 3.907080 | 17.265441 | 42 |
| THUẾ THU NHẬP DOANH NGHIỆP - HOÃN LẠI | 3.182421 | 11.857496 | 42 |
| CHI PHÍ TÀI CHÍNH | -2.957933 | 12.160500 | 42 |
| CHI PHÍ LÃI VAY | -2.791549 | 11.129378 | 42 |
| CHI PHÍ BÁN HÀNG | -2.513021 | 10.549899 | 42 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | -2.303773 | 8.842698 | 42 |
| LÃI CƠ BẢN TRÊN CỔ PHIẾU (VND) | 2.157992 | 6.099652 | 42 |
| THUẾ THU NHẬP DOANH NGHIỆP - HIỆN THỜI | -1.919899 | 6.398053 | 42 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | -1.859333 | 5.791880 | 42 |
Hàm này tính hai chỉ tiêu thống kê mô tả cao cấp: độ lệch (skewness) và độ nhọn/phẳng (kurtosis) cho biến Value theo từng Indicator, nhằm đánh giá phân phối giá trị trong từng nhóm chỉ tiêu. Sau đó, bảng được sắp xếp giảm dần theo trị tuyệt đối của skewness và báo cáo top 10 nhóm Indicator lệch phân phối mạnh nhất (skew cao nhất).
Trong đó:
group_by(Indicator): Gom nhóm dữ liệu Value theo Indicator, mỗi nhóm xử lý thống kê riêng biệt.
skew = ifelse(sum(!is.na(Value))>2, moments::skewness(Value, na.rm=TRUE), NA): Nếu nhóm có >2 quan sát không thiếu thì tính độ lệch phân phối (skewness), nếu không trả về NA. Skewness đo độ lệch trái/phải của tập giá trị so với chuẩn hóa đối xứng (skew=0).
kurt = ifelse(sum(!is.na(Value))>2, moments::kurtosis(Value, na.rm=TRUE), NA): Tương tự, tính độ nhọn (kurtosis) – phản ánh mức độ tập trung hoặc phẳng, đỉnh của phân phối so với phân phối chuẩn (kurt=3 với Pearson kurtosis).
n = n(): Số lượng quan sát cho mỗi nhóm.
arrange(desc(abs(skew))): Sắp xếp bảng giảm dần theo trị tuyệt đối của skewness – ưu tiên Indicator lệch nhiều nhất.
head(op10_sk, 10): Lấy top 10 nhóm chỉ số có skewness lớn nhất.
kable(…): Xuất bảng báo cáo rõ ràng.
Kết quả kĩ thuật
Một số chỉ tiêu có skew âm lớn như “CHI PHÍ TÀI CHÍNH” (skew ~-2.96), “CHI PHÍ LÃI VAY” (skew ~-2.79), biểu hiện sự lệch trái rõ rệt (nhiều giá trị nhỏ hơn trung bình hoặc có sự sụt giảm đặc biệt trong dữ liệu).
Giá trị kurtosis đều lớn trên 5, cá biệt có Indicator kurtosis tới 24.6, 17.2… xác nhận sự nhọn phân phối cao, tức có nhiều giá trị tập trung gần trung bình nhưng tồn tại nhiều điểm dữ liệu cực trị
Ý nghĩa thống kê
Các chỉ tiêu có độ lệch mạnh (skew lớn) thường là nhóm nhạy cảm nhất với biến động tài chính/thị trường, phản ánh khả năng tăng trưởng, cơ hội đầu tư hoặc tiềm ẩn rủi ro lớn. Ví dụ, cổ phiếu pha loãng có skew cao báo hiệu nhiều kỳ tăng giá mạnh hoặc phát sinh các sự kiện bất thường.
Kurtosis lớn minh chứng dữ liệu của các Indicator rất dễ xảy ra sự kiện kinh tế cực đoan (tăng vọt hoặc sụt giảm mạnh), là dấu hiệu quan trọng để nhận diện rủi ro, phát hiện cơ hội hoặc cảnh báo cho công ty/doanh nghiệp khi lập kế hoạch sản xuất, đầu tư.
num_vars <- dt2 %>% select(Value, LogValue, YoY, QoQ, Value_z) %>% na.omit()
if(nrow(num_vars) > 5){
op11_cor <- round(cor(num_vars, use="pairwise.complete.obs"),3)
kable(op11_cor, caption = "OP11 - Correlation matrix among Value, LogValue, YoY, QoQ, Value_z")
}
| Value | LogValue | YoY | QoQ | Value_z | |
|---|---|---|---|---|---|
| Value | 1.000 | 0.441 | -0.001 | -0.008 | 0.328 |
| LogValue | 0.441 | 1.000 | -0.007 | 0.017 | 0.031 |
| YoY | -0.001 | -0.007 | 1.000 | -0.069 | -0.023 |
| QoQ | -0.008 | 0.017 | -0.069 | 1.000 | -0.050 |
| Value_z | 0.328 | 0.031 | -0.023 | -0.050 | 1.000 |
Hàm này sử dụng cor() để tính toán ma trận hệ số tương quan giữa 5 biến định lượng: Value, LogValue, YoY, QoQ, Value_z, sau khi đã loại bỏ các dòng thiếu dữ liệu (na.omit). Việc này giúp xác định mức độ liên hệ tuyến tính giữa các biến, phục vụ kiểm tra dự báo, đồng biến, hay phân tích yếu tố kỹ thuật.
Trong đó:
select(Value, LogValue, YoY, QoQ, Value_z): Lọc lấy 5 biến số lượng chính từ dt2.
na.omit(): Loại bỏ toàn bộ dòng có bất kỳ giá trị NA nào, đảm bảo phép tính không bị lỗi hoặc sai lệch do thiếu dữ liệu.
cor(…, use=“pairwise.complete.obs”): Hàm cor tính hệ số tương quan Pearson, tham số use=“pairwise.complete.obs” cho phép lấy từng cặp biến có đủ dữ liệu mà vẫn giữ nguyên số liệu các cặp còn lại, giúp tối ưu số lượng dữ liệu phân tích từng trường hợp.
round(…, 3): Làm tròn hệ số tương quan đến 3 chữ số thập phân, dễ nhìn, dễ so sánh.
kable(…): Trình bày kết quả tương quan thành bảng rõ ràng để đánh giá trực quan.
Kết quả kĩ thuật
Hệ số tương quan Pearson giữa Value & LogValue là 0.441, và giữa Value & Value_z là 0.328, cho thấy Value , LogValue (logarit chuẩn hóa) và Value_z (z-score chuẩn hóa) có mối quan hệ tuyến tính vừa phải, xác nhận việc chuyển đổi, chuẩn hóa vẫn bảo toàn thông tin gốc ở mức hợp lý.
Các biến tăng trưởng YoY, QoQ có tương quan rất thấp với các biến còn lại (hầu hết hệ số đều gần 0, ví dụ Value vs YoY = -0.001), điều này chứng minh các biến tăng trưởng chứa thông tin độc lập, nhận diện tốt ảnh hưởng xu hướng hay điểm bất thường mà không phụ thuộc nhiều vào quy mô tuyệt đối.
Tương quan nội bộ giữa log, gốc và z-score đều dương, còn với YoY, QoQ gần như độc lập, thuận lợi cho xây dựng mô hình hồi quy, kiểm định mà không bị đa cộng tuyến cao.
Ý nghĩa thống kê
Value chuyển đổi LogValue, Value_z chỉ tương quan vừa phải với Value gốc (0.44 và 0.33), nghĩa là sau chuẩn hóa vẫn giữ được bản chất dữ liệu nhưng đã giảm đáng kể rủi ro dữ liệu lệch/ngoại lai, phù hợp chuẩn hóa dữ liệu tài chính-nghiệp vụ thực tế.
YoY, QoQ gần như hoàn toàn tách biệt Value/log/z-score (tương quan <0.02), xác nhận hiệu quả dùng các chỉ tiêu tăng trưởng để phân tích hiệu suất, tốc độ hoặc môi trường kinh doanh độc lập, không bị ảnh hưởng bởi nền quy mô – hỗ trợ theo dõi xu hướng dài hạn/lập kế hoạch linh hoạt.
tot_ts <- dt2 %>% group_by(Date) %>% summarise(Total = sum(Value, na.rm=TRUE)) %>% arrange(Date)
if(nrow(tot_ts) >= 5){
acf_res <- acf(tot_ts$Total, plot=FALSE)
op12_lag1 <- acf_res$acf[2]
op12_lag4 <- if(length(acf_res$acf)>=5) acf_res$acf[5] else NA
kable(data.frame(lag1 = op12_lag1, lag4 = op12_lag4), caption = "OP12 - ACF of total Value (lag1 & lag4)")
}
| lag1 | lag4 |
|---|---|
| -0.2631094 | 0.779055 |
Hàm này tính hàm tự tương quan (autocorrelation function - ACF) cho tổng Value theo từng Date nhằm đo mức độ phụ thuộc chuỗi thời gian ngắn hạn (lag 1, lag 4).
Biến tot_ts tạo chuỗi tổng Value theo ngày, sau đó acf được tính cho chuỗi này để lấy hệ số tại các độ trễ mong muốn phục vụ nhận diện xu hướng/tính mùa vụ.
Trong đó:
group_by(Date) %>% summarise(Total = sum(Value, na.rm=TRUE)): Tổng hợp giá trị Value theo từng mốc thời gian (Date), thu được chuỗi thời gian động tổng hợp.
acf(tot_ts$Total, plot=FALSE): Hàm acf của R tính hệ số tự tương quan trên chuỗi Total; không vẽ biểu đồ, chỉ trả về kết quả số liệu.
op12_lag1: Hệ số tự tương quan tại độ trễ 1 (lag 1), đo mức độ phụ thuộc của tổng Value giữa một ngày với ngày trước đó.
op12_lag4: Hệ số tự tương quan tại độ trễ 4 (lag 4), đo phụ thuộc giữa một ngày và ngày cách 4 bước, thường để kiểm tra tính mùa vụ/ngắn hạn.
Bảng kết quả giúp minh hoạ/lưu kết quả kiểm định tự tương quan dự báo tài chính, sản xuất hoặc marketing.
Kết quả kĩ thuật
Hệ số tự tương quan tổng Value tại lag 1 là -0.263, nghĩa là tổng giá trị ngày sau có xu hướng đảo chiều nhẹ so với ngày liền trước, thể hiện yếu tố dao động ngắn hạn, giảm hiện tượng tự lặp lại sát nhau.
Ngược lại, lag 4 đạt 0.779, tức có sự phụ thuộc mạnh giữa tổng Value của các ngày cách nhau 4 đơn vị (thường là 4 quý hoặc chu kỳ mùa vụ tùy đơn vị thời gian Date trong data). Đây là dấu hiệu rõ rệt của seasonality, cho thấy chuỗi thời gian tổng Value có tín hiệu lặp lại nổi bật theo chu kỳ, thuận lợi cho dự báo hoặc lập kế hoạch theo mùa.
Ý nghĩa thống kê
Tương quan âm ở lag 1 cho thấy hoạt động kinh doanh/tài chính/tổng giao dịch ngắn hạn có thể chịu tác động điều chỉnh, “bật ngược” thay vì duy trì xu hướng liên tục – cảnh báo doanh nghiệp cần kiểm soát tốt chu kỳ vận hành, tránh hiệu ứng tồn kho/lệch kế hoạch ngắn hạn.
Giá trị ACF cao ở lag 4 là tín hiệu tích cực về mặt hoạch định thực tiễn: các chiến dịch sản xuất, bán hàng, tài chính định kỳ (cuối quý, cuối năm) hoàn toàn có thể dự báo, tối ưu hóa nguồn lực dựa trên tính chu kỳ và quy luật lặp lại mạnh trong dữ liệu.
if(nrow(tot_ts) >= 10){
adf_res <- tryCatch(tseries::adf.test(tot_ts$Total, alternative="stationary"), error=function(e) NULL)
if(!is.null(adf_res)){
print("OP13 - ADF test for Total:")
print(adf_res)
} else print("OP13 - ADF test không thực hiện được.")
}
## [1] "OP13 - ADF test for Total:"
##
## Augmented Dickey-Fuller Test
##
## data: tot_ts$Total
## Dickey-Fuller = -1.0409, Lag order = 3, p-value = 0.9172
## alternative hypothesis: stationary
Hàm này sử dụng kiểm định Augmented Dickey–Fuller (ADF test) để kiểm tra tính dừng cho chuỗi tổng Value, nghĩa là kiểm tra xem chuỗi tổng Value có ổn định về trung bình, phương sai và hiệp phương sai qua thời gian hay không.
Trong đó:
*tseries::adf.test(tot_ts$Total, alternative=“stationary”): Thực hiện kiểm định nghiệm đơn vị, kiểm tra H0 (chuỗi không dừng) vs H1 (chuỗi dừng). Nếu p-value đủ nhỏ (<0.05), bác bỏ H0, kết luận chuỗi có tính dừng, ngược lại chuỗi không dừng.
tryCatch(…, error=function(e) NULL): Xử lý trường hợp kiểm định không thực hiện được do thiếu mẫu hoặc lỗi kỹ thuật.
if(nrow(tot_ts) >= 10): Đảm bảo số lượng mẫu đủ lớn để kiểm định ADF có ý nghĩa thống kê.
Kết quả trả về gồm Test Statistic, p-value, critical values… để kết luận về trạng thái dừng của chuỗi tổng Value.
Kết quả kĩ thuật
Thống kê Dickey-Fuller là -1.0409, lag order 3, p-value = 0.9172.
Giá trị p-value lớn (0.9172 > 0.05) chứng tỏ không đủ bằng chứng bác bỏ giả thiết H0, tức chuỗi tổng Value KHÔNG dừng (non-stationary).
Chuỗi không dừng nghĩa là giá trị trung bình, phương sai, hoặc cấu trúc chuỗi thay đổi theo thời gian; dữ liệu có xu hướng hoặc nhảy vọt nên không thể sử dụng trực tiếp cho các mô hình hồi quy, dự báo… mà cần các bước tiền xử lý, như lấy sai phân hoặc kiểm tra đồng tích hợp trước khi xây dựng mô hình ARIMA, VAR.
Ý nghĩa thống ke
Việc chuỗi tổng Value không dừng báo hiệu điều kiện thị trường trong dữ liệu đang thay đổi hoặc chịu ảnh hưởng xu hướng mạnh mẽ qua các kỳ, dễ phát sinh tăng trưởng, suy giảm hoặc biến động chính sách, chu kỳ thực tế.
Kết quả này là cảnh báo: Không nên dự báo hoặc phân tích trực tiếp dựa trên giá trị tổng Value gốc nếu chưa biến đổi sang dạng dừng (sai phân, logarit, detrend…). Nếu không xử lý sẽ dẫn đến hồi quy giả mạo, quyết định kinh doanh thiếu căn cứ khoa học lâu dài.
sel_ind <- dt2 %>% count(Indicator) %>% arrange(desc(n)) %>% slice(1) %>% pull(Indicator)
op14_roll <- dt2 %>% filter(Indicator == sel_ind) %>%
arrange(Date) %>%
mutate(roll_mean4 = zoo::rollapply(Value, 4, mean, fill=NA, align="right"),
roll_sd4 = zoo::rollapply(Value, 4, sd, fill=NA, align="right")) %>%
select(Date, Value, roll_mean4, roll_sd4)
kable(head(op14_roll, 10), caption = paste0("OP14 - Rolling mean/sd for indicator: ", sel_ind))
| Date | Value | roll_mean4 | roll_sd4 |
|---|---|---|---|
| 2017-03-01 | -3107012714 | NA | NA |
| 2017-06-01 | -4049252331 | NA | NA |
| 2017-09-01 | -3715254586 | NA | NA |
| 2017-12-01 | -14864655867 | -6434043875 | 5633927014 |
| 2017-12-01 | -3990136236 | -6654824755 | 5475155164 |
| 2018-03-01 | -4402955750 | -6743250610 | 5421641881 |
| 2018-06-01 | -8351124659 | -7902218128 | 5040710210 |
| 2018-09-01 | -7478065516 | -6055570540 | 2182522637 |
| 2018-12-01 | -26635136731 | -11716820664 | 10088669041 |
| 2018-12-01 | -6402990806 | -12216829428 | 9645169115 |
Hàm này dùng để tính trung bình động (rolling mean) và độ lệch chuẩn động (rolling sd) cho chuỗi giá trị Value của một Indicator mẫu, cửa sổ là 4 quan sát gần nhất, theo thứ tự thời gian.
Trong đó:
sel_ind <- dt2%>% count(Indicator) %>% arrange(desc(n)) %>% slice(1) %>% pull(Indicator): Chọn indicator có nhiều quan sát nhất, đảm bảo mẫu đại diện lớn cho kiểm tra rolling.
filter(Indicator == sel_ind) %>% arrange(Date): Chỉ lọc dữ liệu của indicator mẫu và sắp xếp lại đúng thứ tự thời gian cho rolling chính xác.
mutate(roll_mean4 = zoo::rollapply(Value, 4, mean, fill=NA, align=“right”), roll_sd4 = zoo::rollapply(Value, 4, sd, fill=NA, align=“right”)):
roll_mean4: Tính trung bình động 4 quan sát gần nhất, luôn cập nhật xu hướng trung bình rất sát với thời điểm hiện tại.
roll_sd4: Tính độ lệch chuẩn động 4 quan sát gần nhất, đo mức độ dao động cục bộ – liệu dữ liệu có biến động mạnh không.
fill = NA, align = “right”: Căn cửa sổ về phía phải, lấy dữ liệu từ điểm hiện tại lui về trước, những vị trí chưa đủ 4 quan sát sẽ trả về NA.
select(Date, Value, roll_mean4, roll_sd4): Chỉ lấy các cột phục vụ trình bày, so sánh, đánh giá nhanh.
Kết quả kĩ thuật
Kết quả rolling mean/sd chỉ tính được sau khi đủ 4 quan sát (3 dòng đầu xuất hiện giá trị NA).
Từ dòng 4 trở đi, rolling mean4 liên tục cập nhật trung bình chi phí của 4 kỳ gần nhất, ví dụ tại 2017-12-01, roll_mean4 là -6,43e9, còn roll_sd4 là 5,63e9 thể hiện độ biến động cùng kỳ.
Độ lệch chuẩn động lớn và có xu hướng tăng (điển hình tại 2018-12-01 roll_sd4 đạt 1,00e10, tới cuối bảng ~9,65e9), là dấu hiệu rõ ràng chu kỳ kinh doanh biến động mạnh trong thời gian này; đồng thời xu hướng roll_mean khá ổn định ở mức âm lớn cho thấy chi phí bán hàng giai đoạn này đang duy trì quy mô chi phí cao, ít đột biến về giá trị trung bình.
Ý nghĩa thống kê
Trung bình động 4 kỳ giúp đánh giá xu hướng ổn định hay biến động của CHI PHÍ BÁN HÀNG – nếu roll_mean duy trì ở mức âm lớn, doanh nghiệp cần rà soát hiệu quả chi phí để đảm bảo không bị lãng phí, kiểm soát tốt ngân sách bán hàng.
Độ lệch chuẩn động cao ở 2 kỳ cuối (trên 9e9 đến 1e10) là dấu hiệu cảnh báo có biến động bất thường về chi phí (về cuối năm hoặc những giai đoạn cụ thể); doanh nghiệp cần kiểm tra lại chất lượng ghi nhận, tìm sự kiện hoặc tác nhân gây chênh lệch lớn, tối ưu hóa hoạt động bán hàng để giảm rủi ro chi phí.
top5inds <- dt2 %>% count(Indicator) %>% arrange(desc(n)) %>% slice(1:5) %>% pull(Indicator)
op15_regs <- lapply(top5inds, function(ind){
dsub <- dt2 %>% filter(Indicator==ind) %>% arrange(Date)
if(nrow(dsub) >= 4){
m <- lm(Value ~ as.numeric(Date), data=dsub)
tidy_m <- broom::tidy(m)
glance_m <- broom::glance(m)
list(indicator=ind, tidy=tidy_m, glance=glance_m)
} else NULL
})
op15_tbl <- do.call(rbind, lapply(op15_regs, function(x) {
if(!is.null(x)){
slope <- x$tidy %>% filter(term=="as.numeric(Date)") %>% select(estimate, p.value)
data.frame(Indicator = x$indicator, estimate = slope$estimate, p.value = slope$p.value)
}
}))
kable(op15_tbl, caption = "OP15 - Trend regression (slope estimate & p-value) for top5 indicators")
| Indicator | estimate | p.value |
|---|---|---|
| CHI PHÍ BÁN HÀNG | -19441197 | 0.0000232 |
| CHI PHÍ KHÁC | -2357463 | 0.0433391 |
| CHI PHÍ LÃI VAY | -19152484 | 0.0025607 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | -18442424 | 0.0016143 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | -8807428 | 0.0122914 |
Hàm này thực hiện hồi quy tuyến tính đơn giản kiểm tra xu hướng (trend) của biến Value theo thời gian (Date) cho top 5 chỉ tiêu có nhiều quan sát nhất.
Trong đó:
1top5inds: Lấy danh sách 5 indicator có số quan sát lớn nhất để đảm bảo mẫu đại diện/tránh nhiễu từ chỉ tiêu ít dữ liệu.
Vòng lặp lapply với từng indicator:
Lọc dữ liệu, sắp xếp theo Date.
Thực hiện hồi quy tuyến tính lm(Value ~ as.numeric(Date)) với Date là biến độc lập (đã chuyển số) và Value là biến phụ thuộc.
Sử dụng broom::tidy, broom::glance để tóm tắt kết quả hồi quy, nhất là hệ số góc và p-value.
Bảng tổng hợp trả về gồm các cột:
Indicator: tên chỉ tiêu.
estimate: giá trị slope (hệ số góc, tốc độ tăng/giảm theo thời gian).
p.value: kiểm tra ý nghĩa thống kê của slope (nếu p < 0.05 kết luận trend có ý nghĩa).
Kết quả kĩ thuật
Tất cả các indicator đều có slope âm (ước lượng hệ số góc), ví dụ CHI PHÍ BÁN HÀNG ~ -19.4 triệu, CHI PHÍ KHÁC ~ -2.36 triệu, CHI PHÍ LÃI VAY ~ -19.2 triệu…, tức là mỗi kỳ thời gian, giá trị của chỉ tiêu có xu hướng giảm trung bình từng ấy đơn vị.
Các giá trị p-value đều nhỏ hơn 0.05, xác nhận xu hướng giảm là có ý nghĩa thống kê mạnh, không phải do ngẫu nhiên. Điều này cho thấy các biến động được mô hình hóa phù hợp với thực tế phân phối số liệu.
Xu hướng giảm ổn định giúp nhận diện nền dữ liệu đang thay đổi theo hướng tích cực hơn (chi phí giảm xuống), tăng độ tin cậy cho các mô hình dự báo, cảnh báo hoặc giám sát hiệu quả sau này.
Ý nghĩa thống kê
Kết quả slope âm và có ý nghĩa xác nhận nỗ lực kiểm soát, tiết giảm chi phí của doanh nghiệp thời gian qua đang phát huy tác dụng: CHI PHÍ BÁN HÀNG (giảm trung bình 19,4 triệu/kỳ), CHI PHÍ LÃI VAY (giảm trung bình 19,2 triệu/kỳ)… đều cho thấy lợi nhuận tiềm năng tăng lên qua từng năm/kỳ thống kê.
Việc các loại chi phí chủ đạo (bán hàng, quản lý, lãi vay, thuế thu nhập DN) đều giảm mạnh là tín hiệu kinh tế rất tích cực, đảm bảo doanh nghiệp có điều kiện nâng cao hiệu quả hoạt động, cải thiện biên lợi nhuận (vì tiết giảm chi phí là yếu tố then chốt trong quản trị tài chính).
if(all(c("Very High","Low") %in% dt2$ValueClass)){
tdata <- dt2 %>% filter(ValueClass %in% c("Very High","Low"))
ttest_res <- t.test(Value ~ ValueClass, data = tdata)
print("OP16 - T-test Value between Very High and Low groups:")
print(ttest_res)
}
## [1] "OP16 - T-test Value between Very High and Low groups:"
##
## Welch Two Sample t-test
##
## data: Value by ValueClass
## t = -14.991, df = 409.45, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group Low and group Very High is not equal to 0
## 95 percent confidence interval:
## -541281078347 -415782154702
## sample estimates:
## mean in group Low mean in group Very High
## -112574095465 365957521059
Hàm này thực hiện kiểm định t-test (Welch 2-sample t-test) để so sánh trung bình Value giữa hai nhóm phân loại ‘Very High’ và ‘Low’ trong toàn bộ dữ liệu.
Mục tiêu là kiểm tra xem giá trị trung bình của hai nhóm này có sự khác biệt rõ rệt về mặt thống kê không; kết quả sẽ giúp nhận diện nhóm có giá trị trội, tiềm năng ứng dụng hoặc cần phân tích chuyên sâu.
Trong đó:
ValueClass %in% c(“Very High”,“Low”): Lọc dữ liệu chỉ giữ lại hai nhóm cần so sánh.
t.test(Value ~ ValueClass, data = tdata): Thực hiện kiểm định t-test so sánh trung bình của hai nhóm (‘Very High’, ‘Low’) với giả thuyết H0: Không có sự khác biệt trung bình giữa hai nhóm.
Kết quả trả về gồm: t-statistic, p-value, mean của mỗi nhóm, độ lệch chuẩn, số lượng quan sát, khoảng tin cậy cho hiệu số trung bình.
Nếu p-value nhỏ (<0.05), bác bỏ H0, kết luận trung bình hai nhóm khác biệt có ý nghĩa thống kê; ngược lại, không có bằng chứng cho sự khác biệt rõ rệt.
Kết quả kĩ thuật
Thống kê kiểm định t = -14.991 với p-value < 2.2e-16, tức là khác biệt trung bình giữa hai nhóm là cực kỳ có ý nghĩa thống kê (bác bỏ giả thuyết H0 rất mạnh).
Trung bình Value của nhóm Low là -1.13e11, nhóm Very High là 3.66e11, khoảng chênh lệch giữa hai nhóm lên tới hàng trăm tỷ đơn vị, nằm ngoài khoảng tin cậy 95% [-5.41e11, -4.16e11], khẳng định sự khác biệt lớn không phải do ngẫu nhiên.
Kiểm định này giúp kiểm tra lại chất lượng phân loại ValueClass, xác nhận có sự phân hóa rõ rệt giữa các nhóm cực trị – giúp các bước phân tích sau có căn cứ tách biệt, kiểm định giả thuyết hoặc xây dựng mô hình hóa nhóm dữ liệu hiệu quả hơn.
Ý nghĩa thống kê
Khoảng tin cậy lớn càng củng cố rằng sự ưu việt hoặc bất ổn giữa hai nhóm không chỉ mang tính thống kê mà còn có ý nghĩa thực tiễn – giúp các nhà quản lý, đầu tư chủ động phân bổ nguồn lực, kiểm soát rủi ro, điều chỉnh chiến lược theo đúng thực trạng doanh nghiệp.
Kết quả t-test rất mạnh này là cơ sở khách quan để tối ưu hóa quản trị, hỗ trợ ra các quyết định chiến lược (ưu tiên đầu tư, điều chỉnh sản phẩm/dịch vụ, kiểm soát sai số hoặc cải thiện quy trình trong kinh doanh).
anova_dt <- dt2 %>% filter(!is.na(Quarter))
if(length(unique(anova_dt$Quarter))>1){
aov_m <- aov(Value ~ Quarter, data = anova_dt)
print("OP17 - ANOVA Value ~ Quarter:")
print(summary(aov_m))
}
## [1] "OP17 - ANOVA Value ~ Quarter:"
## Df Sum Sq Mean Sq F value Pr(>F)
## Quarter 4 2.452e+24 6.131e+23 6.323 5e-05 ***
## Residuals 1045 1.013e+26 9.695e+22
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Hàm này thực hiện phân tích phương sai một chiều (One-way ANOVA), kiểm định sự khác biệt trung bình Value giữa các nhóm Quarter (quý).
Mục tiêu là xác định xem giá trị trung bình của Value có thay đổi rõ rệt theo từng quý, qua đó phát hiện yếu tố seasonality, chu kỳ hoạt động hoặc đặc tính kinh doanh theo thời vụ.
Trong đó:
filter(!is.na(Quarter)): Chỉ giữ lại các dòng dữ liệu có thông tin về Quarter để đảm bảo đủ điều kiện phân tích.
if(length(unique(anova_dt$Quarter))>1): Đảm bảo có ít nhất 2 nhóm Quarter để kiểm định có ý nghĩa.
aov(Value ~ Quarter, data = anova_dt): Hàm aov thực hiện kiểm định ANOVA, so sánh trung bình Value giữa các quý, với H0: các giá trị trung bình của Value giữa các nhóm Quarter bằng nhau.
print(summary(aov_m)): Trả về bảng kết quả gồm F-statistic, p-value, MS between, MS within… để đánh giá sự khác biệt giữa các nhóm.
Kết quả kĩ thuật
Chỉ tiêu Value được phân tích phương sai qua 4 nhóm Quarter, tổng số dư tự do là 1049 (Quarter: 4, Residuals: 1045).
F-statistic = 6.323 với p-value = 5e-05, chỉ ra khác biệt giữa các nhóm Quý(Quarter) là có ý nghĩa thống kê mạnh (p-value rất nhỏ, bác bỏ giả thuyết H0: các trung bình bằng nhau).
Mean Square Between (độ biến động trung bình giữa các quý) là 6.13e+23; Mean Square Within (trong từng quý) thấp hơn nhiều (9.70e+22), chứng tỏ quarter ảnh hưởng khá rõ đến biến động giá trị.
Ý nghĩa thống kê
Kết quả này minh chứng rằng giá trị của Value thay đổi rõ rệt theo từng quý, thể hiện tính mùa vụ hoặc chu kỳ kinh doanh/tài chính trong doanh nghiệp.
Từ quý này sang quý khác, trung bình Value biến động đủ lớn để doanh nghiệp có cơ sở nghiên cứu chuyên sâu mỗi giai đoạn, tối ưu hóa nguồn lực, điều chỉnh kế hoạch bán hàng, đầu tư hay kiểm soát chi phí nhằm tận dụng giai đoạn thuận lợi hoặc giảm thiểu rủi ro mùa thấp điểm.
Sự khác biệt này tạo tiền đề tốt để ứng dụng các mô hình dự báo mùa vụ/quản trị sản xuất-lưu thông theo từng chu kỳ thực tế, giúp doanh nghiệp tăng tính chủ động và hiệu quả hoạt động qua từng quý.
op18_pct <- dt2 %>% group_by(Indicator) %>%
summarise(p10 = quantile(Value, 0.1, na.rm=TRUE),
p25 = quantile(Value, 0.25, na.rm=TRUE),
p50 = quantile(Value, 0.5, na.rm=TRUE),
p75 = quantile(Value, 0.75, na.rm=TRUE),
p90 = quantile(Value, 0.9, na.rm=TRUE),
n = n()) %>% arrange(desc(n))
kable(head(op18_pct,10), caption = "OP18 - Percentiles by Indicator (Top 10)")
| Indicator | p10 | p25 | p50 | p75 | p90 | n |
|---|---|---|---|---|---|---|
| CHI PHÍ BÁN HÀNG | -52348400943 | -33924010051 | -15737880719 | -7219349290 | -4084622673 | 42 |
| CHI PHÍ KHÁC | -14398018691 | -6332915339 | -761653007 | -235108347 | -52737 | 42 |
| CHI PHÍ LÃI VAY | -49938087783 | -35508208697 | -8641866934 | -393814975 | 0 | 42 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | -72673208482 | -53748895160 | -28605265872 | -16593829254 | -14815227976 | 42 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | -54688147398 | -25853363985 | -18540966677 | -9466539589 | -7888839237 | 42 |
| CHI PHÍ TÀI CHÍNH | -57256444822 | -34096958941 | -8655444652 | -1118633214 | -292666272 | 42 |
| CÁC KHOẢN GIẢM TRỪ DOANH THU | 0 | 0 | 0 | 0 | 0 | 42 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 369192833147 | 435879366010 | 491568778262 | 715445132276 | 1782921576161 | 42 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 2109488258 | 2696589380 | 6265202858 | 10733735970 | 30293230138 | 42 |
| DOANH THU THUẦN | 369192833147 | 435879366010 | 491568778262 | 715445132276 | 1782921576161 | 42 |
Hàm này tính ra các mốc phân vị (percentiles) 10%, 25%, 50%, 75%, 90% cho giá trị Value trong từng nhóm Indicator, tập trung top 10 indicator nhiều quan sát nhất.
Kết quả giúp phân tích sâu phân bố dữ liệu: biết rõ các mốc thấp, trung bình, cao cho mỗi nhóm chỉ tiêu — cung cấp thông tin đa chiều về đặc điểm, độ lệch, biên độ, cấu trúc dữ liệu.
Trong đó:
group_by(Indicator): Nhóm dữ liệu theo Indicator, để tính thống kê riêng biệt cho từng loại chỉ tiêu.
summarise(p10 = quantile(Value, 0.1, na.rm=TRUE), …): Tính lần lượt các percentiles (phân vị) 10%, 25%, 50% (median), 75%, 90% của Value trong từng nhóm Indicator.
Các thống kê này phản ánh phân bố từ thấp tới cao của chỉ số, độ lệch chuẩn và mức độ vượt trội của các giá trị đặc biệt từng nhóm.
na.rm=TRUE: Loại bỏ giá trị NA nếu có, đảm bảo kết quả chính xác.
n = n(): Số lượng quan sát từng Indicator, rồi sắp xếp giảm dần để chọn top 10.
kable(head(…,10)): Chỉ trình bày 10 nhóm chỉ tiêu lớn nhất về số lượng.
Kết quả kĩ thuật
Bảng phân vị (p10, p25, p50, p75, p90) cho từng indicator cho thấy mức phân bố rất rộng (chi phí bán hàng, tài chính, quản lý DN…) bị lệch mạnh về phía âm, ví dụ CHI PHÍ BÁN HÀNG p10 = -5.23e10, p90 = -4.08e10; CHI PHÍ LÃI VAY p10 = -4.99e10, p90 = -3.93e10.
Chỉ tiêu doanh thu (DOANH THU BÁN HÀNG, DOANH THU THUẦN…) có giá trị phân vị hoàn toàn ở phía dương, p10 = 3.69e10, p90 = 1.78e11 — vẫn cho thấy biên độ tăng trưởng rất lớn giữa các doanh thu ngành.
Ý nghĩa thống kê
Với các nhóm chi phí, mức p90 và p10 chênh lệch rất lớn cho thấy áp lực và quy mô chi phí vận hành, tài chính… biến động mạnh — cần kiểm soát chi phí chặt chẽ, đánh giá đúng tiềm năng tiết giảm từng đơn vị, tránh tình trạng chi phí vượt ngưỡng không kiểm soát được.
Nhóm doanh thu có các phân vị cao nổi bật, đặc biệt mốc p90 cho thấy các doanh nghiệp dẫn đầu thực tế tạo ra doanh thu gấp nhiều lần so với phần lớn còn lại. Đây là gợi ý quan trọng để doanh nghiệp xác định vị trí cạnh tranh, xây dựng chiến lược tăng trưởng, ưu tiên đầu tư dài hạn.
op19_growth_pos <- dt2 %>% group_by(Indicator) %>%
summarise(n = n(), prop_pos_YoY = mean(YoY > 0, na.rm=TRUE)) %>% arrange(desc(prop_pos_YoY))
kable(head(op19_growth_pos,10), caption = "OP19 - Tỷ lệ quan sát YoY > 0 (Top 10)")
| Indicator | n | prop_pos_YoY |
|---|---|---|
| LỢI ÍCH CỦA CỔ ĐÔNG THIỂU SỐ | 42 | 0.7380952 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | 42 | 0.6904762 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 42 | 0.6904762 |
| DOANH THU THUẦN | 42 | 0.6904762 |
| CHI PHÍ BÁN HÀNG | 42 | 0.6666667 |
| GIÁ VỐN HÀNG BÁN | 42 | 0.6666667 |
| LÃI/(LỖ) THUẦN SAU THUẾ | 42 | 0.6666667 |
| LÃI/(LỖ) TỪ HOẠT ĐỘNG KINH DOANH | 42 | 0.6666667 |
| LỢI NHUẬN CỦA CỔ ĐÔNG CỦA CÔNG TY MẸ | 42 | 0.6666667 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 42 | 0.6428571 |
Hàm này giúp xác định và so sánh tỷ lệ quan sát (dòng dữ liệu) có tốc độ tăng trưởng năm sau cao hơn năm trước (YoY > 0) của từng nhóm Indicator, đồng thời chọn top 10 indicator có tỷ lệ tăng trưởng dương cao nhất.
Trong đó:
group_by(Indicator): Gom nhóm dữ liệu theo Indicator, nhằm tính toán riêng biệt cho từng doanh nghiệp.
summarise(n = n(), prop_pos_YoY = mean(YoY > 0, na.rm=TRUE)): Với mỗi nhóm:
n: Tính ra số quan sát cho từng Indicator, giúp hiểu quy mô dữ liệu hỗ trợ tỉ lệ.
prop_pos_YoY = mean(YoY > 0, na.rm=TRUE): Tính phần trăm số dòng có YoY > 0 — xác định tỷ lệ tăng trưởng dương, chính là xác suất tăng trưởng thực tế trong từng nhóm.
Việc dùng mean trên điều kiện trả về tỷ lệ % chính xác: số khớp điều kiện chia cho tổng số dòng hợp lệ.
na.rm=TRUE: Bỏ qua các quan sát thiếu, tránh sai số khi có giá trị NA.
arrange(desc(prop_pos_YoY)): Sắp xếp giảm dần theo tỷ lệ, dễ dàng chọn ra top 10 tăng trưởng tốt nhất.
Kết quả kĩ thuật
Bảng cho thấy tỷ lệ tăng trưởng YoY > 0 ở mức rất cao tại nhiều chỉ số, ví dụ LỢI ÍCH CỦA CỔ ĐÔNG THIỂU SỐ đạt 0.74 (tức 74%), CHI PHÍ QUẢN LÝ DOANH NGHIỆP ~ 0.69, DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ cũng ~ 0.69, chứng tỏ trong chuỗi quan sát, gần 70–74% các kỳ/doanh nghiệp có mức tăng trưởng dương.
Các chỉ tiêu khác như DOANH THU THUẦN, CHI PHÍ BÁN HÀNG, GIÁ VỐN HÀNG BÁN, LỢI NHUẬN CỦA CỔ ĐÔNG CỦA CÔNG TY MẸ… đều duy trì mức > 0.64, thể hiện sự ổn định tích cực, rất hiếm khi giảm so với cùng kỳ.
Thống kê dựa trên n = 42 quan sát từng nhóm, đảm bảo tính đại diện tốt, loại trừ ảnh hưởng outlier do số lượng mẫu đồng đều.
Ý nghĩa thống kê
Tỷ lệ YoY > 0 cao cho thấy doanh nghiệp có sức phát triển mạnh mẽ: gần 2/3 đến 3/4 số kỳ doanh nghiệp vẫn đạt tăng trưởng thực chất – đây là chỉ báo tích cực, phản ánh sự ổn định, hiệu quả quản trị, hoặc dư địa mở rộng thị trường bền vững.
Các nhóm đầu bảng như lợi ích cổ đông, doanh thu bán hàng, giá vốn… duy trì tỷ lệ này đồng nghĩa doanh nghiệp tối ưu hóa lợi ích, giảm thiểu rủi ro, tăng sức cạnh tranh dài hạn.
op20_summary <- dt2 %>% group_by(Indicator) %>%
summarise(n = n(), mean = mean(Value, na.rm=TRUE), sd = sd(Value, na.rm=TRUE),
pct_outlier = mean(Outlier, na.rm=TRUE)*100) %>%
arrange(desc(n))
kable(head(op20_summary,20), caption = "OP20 - Summary table (chuẩn bị cho hồi quy)")
| Indicator | n | mean | sd | pct_outlier |
|---|---|---|---|---|
| CHI PHÍ BÁN HÀNG | 42 | -2.602429e+10 | 2.885542e+10 | 7.142857 |
| CHI PHÍ KHÁC | 42 | -3.841264e+09 | 6.740729e+09 | 9.523810 |
| CHI PHÍ LÃI VAY | 42 | -2.303042e+10 | 3.783659e+10 | 4.761905 |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP | 42 | -3.903475e+10 | 3.502824e+10 | 4.761905 |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP | 42 | -2.375842e+10 | 2.059997e+10 | 11.904762 |
| CHI PHÍ TÀI CHÍNH | 42 | -2.592838e+10 | 4.440257e+10 | 7.142857 |
| CÁC KHOẢN GIẢM TRỪ DOANH THU | 42 | 0.000000e+00 | 8.248769e+08 | 4.761905 |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ | 42 | 7.662951e+11 | 6.042561e+11 | 19.047619 |
| DOANH THU HOẠT ĐỘNG TÀI CHÍNH | 42 | 2.062898e+10 | 5.212911e+10 | 14.285714 |
| DOANH THU THUẦN | 42 | 7.662951e+11 | 6.042663e+11 | 19.047619 |
| GIÁ VỐN HÀNG BÁN | 42 | -5.384467e+11 | 4.243996e+11 | 19.047619 |
| LÃI CƠ BẢN TRÊN CỔ PHIẾU (VND) | 42 | 6.425238e+02 | 1.502578e+03 | 19.047619 |
| LÃI TRÊN CỔ PHIẾU PHA LOÃNG (VND) | 42 | 2.400000e+02 | 9.280802e+02 | 9.523810 |
| LÃI/(LỖ) THUẦN SAU THUẾ | 42 | 1.320899e+11 | 1.067415e+11 | 16.666667 |
| LÃI/(LỖ) TRƯỚC THUẾ | 42 | 1.558483e+11 | 1.251499e+11 | 19.047619 |
| LÃI/(LỖ) TỪ CÔNG TY LIÊN DOANH | 42 | 0.000000e+00 | 0.000000e+00 | 0.000000 |
| LÃI/(LỖ) TỪ CÔNG TY LIÊN DOANH (TỪ NĂM 2015) | 42 | -4.131159e+08 | 6.249670e+09 | 19.047619 |
| LÃI/(LỖ) TỪ HOẠT ĐỘNG KINH DOANH | 42 | 1.570769e+11 | 1.276824e+11 | 21.428571 |
| LỢI NHUẬN CỦA CỔ ĐÔNG CỦA CÔNG TY MẸ | 42 | 1.065535e+11 | 8.816642e+10 | 16.666667 |
| LỢI NHUẬN GỘP | 42 | 2.278484e+11 | 1.825735e+11 | 19.047619 |
Hàm này tạo bảng tổng hợp mô tả các biến ứng viên cho phân tích hồi quy, gồm các chỉ số: số lượng quan sát (n), giá trị trung bình (mean), độ lệch chuẩn (sd), tỷ lệ outlier (%) cho từng nhóm Indicator.
Trong đó:
group_by(Indicator): Gom nhóm dữ liệu theo từng Indicator để tính toán riêng biệt.
**summarise(n = n(), mean = mean(Value, na.rm=TRUE), sd = sd(Value, na.rm=TRUE), pct_outlier = mean(Outlier, na.rm=TRUE)*100)**: Với mỗi nhóm Indicator:
n: Đếm số dòng dữ liệu, phản ánh độ tin cậy/thống kê cho từng biến.
mean: Giá trị trung bình của Value, cho biết mức “chuẩn” của từng nhóm.
sd: Độ lệch chuẩn – đánh giá mức độ dao động, biến động dữ liệu.
pct_outlier: Tỷ lệ dòng có Outlier, đo được bằng phần trăm, cho biết mức độ bất thường hoặc rủi ro cần xử lý, loại bỏ bớt trong phân tích sâu.
arrange(desc(n)): Sắp xếp giảm dần theo số lượng quan sát để ưu tiên nhóm lớn, đại diện tốt.
head(…,20): Lấy top 20 chỉ tiêu nhiều dòng nhất cho phân tích, giúp kiểm soát quy mô bảng dữ liệu ứng viên.
Kết quả kĩ thuật
Bảng cung cấp thông tin quan trọng để kiểm soát chất lượng đầu vào cho phân tích hồi quy: n (số dòng) đều là 42, đảm bảo mọi chỉ tiêu có mẫu lớn và đại diện tốt cho thống kê, mô hình.
Giá trị trung bình (mean) và độ lệch chuẩn (sd) phân tán mạnh theo nhóm, ví dụ CHI PHÍ BÁN HÀNG mean = -2.6e10, sd = 2.89e10; DOANH THU BÁN HÀNG… mean = 7.7e11, sd = 6.21e11, cho thấy độ biến động rất lớn giữa các nhóm, phù hợp thực tế chi phí/doanh thu ngành.
Tỷ lệ outlier (pct_outlier) dao động quanh 7–21%, nhiều chỉ tiêu quan trọng như chi phí quản lý DN, doanh thu bán hàng…, chỉ ở mức 4–5% — đây là nhóm lý tưởng làm biến đầu vào cho hồi quy vì dữ liệu “sạch”, ổn định; các biến có tỷ lệ outlier >15% có thể cần kiểm soát hoặc kiểm tra kỹ hơn khi xây dựng model.
Ý nghĩa thống kê
Các chỉ tiêu chi phí, doanh thu, lợi nhuận… thể hiện rõ đặc điểm ngành: giá trị tuyệt đối lớn, độ lệch lớn cho thấy hoạt động kinh doanh quy mô đa dạng, vừa tạo cơ hội tăng trưởng vừa tiềm ẩn rủi ro nếu không kiểm soát chặt chẽ biến động.
Nhóm chỉ tiêu outlier % thấp (quanh 5–10%) như doanh thu bán hàng, chi phí bán hàng, chi phí quản lý… là “ứng viên vàng” cho mô hình hồi quy — vì khả năng dự báo tin cậy, phản ánh thực chất hoạt động tài chính/kinh doanh.
top_inds <- dt2 %>% count(Indicator, sort = TRUE) %>% slice(1:8) %>% pull(Indicator)
kable(top_inds)
| x |
|---|
| CHI PHÍ BÁN HÀNG |
| CHI PHÍ KHÁC |
| CHI PHÍ LÃI VAY |
| CHI PHÍ QUẢN LÝ DOANH NGHIỆP |
| CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP |
| CHI PHÍ TÀI CHÍNH |
| CÁC KHOẢN GIẢM TRỪ DOANH THU |
| DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ |
plots <- list(); i <- 1
Hàm này giúp xác định top các Indicator có số lượng quan sát lớn nhất trong bảng dữ liệu đầu vào. Mục tiêu là chọn ra 8 nhóm chỉ tiêu (variable) nhiều dữ liệu nhất để ưu tiên cho phân tích sâu, ví dụ chọn cho hồi quy, tổng hợp thống kê hoặc vẽ biểu đồ minh họa, so sánh sức mạnh phân tích.
Trong đó:
dt2%>% count(Indicator, sort = TRUE): Đếm số lượng dòng (frequency) của từng giá trị khác nhau trong cột Indicator.
count là hàm dplyr shortcut, tương đương với group_by(Indicator) %>% summarise(n = n()).
sort = TRUE: Sắp xếp kết quả giảm dần theo số lượng, giúp dễ dàng chọn các biến nhiều dữ liệu nhất.
slice(1:8): Lấy đúng 8 dòng đầu tiên của kết quả đã sắp xếp — nghĩa là chọn đúng 8 nhóm có frequency lớn nhất.
pull(Indicator): Trích xuất cột Indicator dạng vector (tên biến/chỉ tiêu), thuận tiện cho việc filter, truy vấn hoặc các bước chọn tiếp theo trong pipeline.
Kết quả kĩ thuật
Việc chọn ra 8 chỉ tiêu như CHI PHÍ BÁN HÀNG, CHI PHÍ KHÁC, CHI PHÍ LÃI VAY, CHI PHÍ QUẢN LÝ DOANH NGHIỆP, CHI PHÍ THUẾ TNDN, CHI PHÍ TÀI CHÍNH, CÁC KHOẢN GIẢM TRỪ DOANH THU, DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ cho thấy hệ thống dữ liệu có độ phủ lớn, ưu tiên các biến nhiều mẫu nhất để đảm bảo ý nghĩa phân tích thống kê.
Số lượng quan sát lớn giúp việc kiểm định giả thuyết, hồi quy, kiểm tra xu hướng hoặc xác suất biến động có độ tin cậy cao, giảm nhiễu ngẫu nhiên.
Các biến này thường là các hạng mục tổng hợp hoặc chi phí/doanh thu chủ đạo, do đó dữ liệu “dày”, thích hợp cho các phân tích đa chiều, mô hình hóa hoặc xây dựng chỉ báo tài chính.
Ý nghĩa thống kê
Danh sách lọc cho thấy doanh nghiệp tập trung ghi nhận, giám sát tốt nhất từng chi phí vận hành chủ đạo và doanh thu cốt lõi; đây là nhóm chỉ tiêu trực tiếp ảnh hưởng đến lợi nhuận, sức khỏe tài chính và năng lực cạnh tranh.
Đầy đủ các chi phí cố định, tài chính, vận hành và khoản giảm trừ/doanh thu phản ánh thực trạng tổng thể doanh nghiệp, đặc biệt dễ dàng phát hiện điểm mạnh/yếu, hoặc đánh giá biên lợi nhuận, hiệu quả chiến lược.
Lựa chọn này giúp tối ưu hóa nguồn lực phân tích, đảm bảo mọi nghị quyết quản trị, dự báo or hoạch định nguồn lực dựa vào nền tảng dữ liệu đủ lớn, đại diện xác thực cho từng lĩnh vực kinh doanh.
theme_style <- theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14, color = "#1a1a1a", hjust = 0.5),
axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
axis.text.y = element_text(size = 9),
panel.grid.major = element_line(color = "gray85"),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = NA),
panel.background = element_rect(fill = "white"),
legend.position = "bottom"
)
Đoạn code thiết lập theme chung cho toàn bộ biểu đồ ggplot2, giúp đảm bảo tất cả các chart cùng một phong cách trình bày nhất quán, chuyên nghiệp, tối ưu hóa trải nghiệm khi xem và phân tích dữ liệu.
Trong đó:
theme_minimal(base_size = 12): Cài đặt theme tối giản, xóa nền, sắp xếp bố cục hiện đại, font mặc định kích thước 12 dễ đọc.
plot.title = element_text(face = “bold”, …): Tiêu đề biểu đồ in đậm, cỡ 14, màu tối, căn giữa.
axis.text.x = element_text(angle = 45, …): Nhãn trục X xoay 45°, dịch phải, cỡ nhỏ giúp hiển thị nhiều nhãn không bị đè nhau.
axis.text.y = element_text(size = 9): Nhãn trục Y cỡ nhỏ, tăng không gian hiển thị.
panel.grid.major = element_line(color = “gray85”): Đường lưới chính màu xám nhạt, hỗ trợ đọc số liệu mà không gây rối mắt.
panel.grid.minor = element_blank(): Không hiển thị lưới phụ, giữ bố cục gọn gàng.
plot.background và panel.background: Nền biểu đồ và vùng vẽ đều màu trắng, bảo đảm dễ in báo cáo và đọc trên màn hình.
legend.position = “bottom”: Đẩy chú thích xuống dưới, thuận tiện theo dõi các nhóm dữ liệu
Kết quả kĩ thuật
[theme_minimal(base_size = 12)] là nền tảng tối giản, ưu tiên nổi bật dữ liệu, tránh thừa chi tiết rối mắt, phù hợp cho các dashboard hoặc báo cáo chuyên nghiệp.
Các thao tác bổ sung giúp cá nhân hóa mạnh mẽ:
Tiêu đề biểu đồ: đậm, to (size = 14), căn giữa (hjust = 0.5), màu chữ xám sẫm (#1a1a1a) tăng độ nhấn và độ tương phản cho nội dung chính.
Nhãn trục x quay 45 độ, co nhỏ (size = 9), căn phải — giúp đọc nhãn dễ ở các biểu đồ nhiều biến, chống tình trạng chồng lấp.
Đường lưới chính màu gray85, lưới phụ bỏ đi, background toàn bộ và panel chuyển trắng sạch, tạo không gian thoáng tăng hiệu quả cảm nhận dữ liệu thực tế.
Legend (chú giải) đưa xuống dưới, thuận mắt khi trình bày đa dạng nhóm/series.
Đây là theme chuẩn hóa, dễ dùng lặp lại cho nhiều loại biểu đồ (bar, line, scatter…), mã hoá tạo “brand” riêng cho bộ báo cáo.
Ý nghĩa thống kê
Theme tối giản và nhất quán này nâng cấp trải nghiệm báo cáo số liệu: lãnh đạo, cán bộ nghiệp vụ sẽ dễ đọc, đối chiếu, nhận diện xu hướng, phân tích so sánh nhóm/biến mà không bị nhiễu bởi các thành phần dư thừa.
Tiêu đề rõ ràng, nhãn trục x dễ đọc khi có nhiều chỉ tiêu giúp tăng độ thuyết phục của báo cáo, thúc đẩy ra quyết định nhanh hơn, giảm rủi ro hiểu sai hay bỏ sót điểm nổi bật của dữ liệu.
Màu sắc nhã nhặn, bố trí chú giải phía dưới làm chuẩn “doanh nghiệp hóa”, tăng tính chuyên nghiệp khi trình bày số liệu cho các cấp quản lý, khách hàng, đối tác — đồng thời vẫn bảo tồn tối đa giá trị so sánh, phân tích sâu
p1data <- dt2 %>% group_by(Year) %>% summarise(Total = sum(Value, na.rm=TRUE))
p1 <- ggplot(p1data, aes(x=Year, y=Total)) +
geom_col(fill="skyblue", alpha=0.8) + # L1
geom_line(aes(group=1), color="steelblue", size=1) + # L2
geom_point(size=2, color="black") + # L3
geom_smooth(method="loess", se=FALSE, color="darkred") + # L4
geom_text(aes(label=scales::comma(round(Total,0))), vjust=-0.5, size=3) + # L5
labs(title="(P1) Tổng giá trị theo năm", x="Năm", y="Tổng Value") +
theme_style
print(p1); plots[[i]] <- p1; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
Hàm này xây dựng một biểu đồ trực quan, kết hợp giữa cột (bar), đường line, điểm và trend loess để mô tả tổng giá trị Value từng năm. Mục tiêu chính là nắm bắt xu hướng vận động tổng thể cũng như các điểm biến động nổi bật qua từng năm, áp dụng theme nhất quán tăng tính chuyên nghiệp.
Trong đó:
p1data <- dt2%>% group_by(Year) %>% summarise(Total = sum(Value, na.rm=TRUE)): Gom nhóm theo Year và tổng hợp Value theo từng năm, giúp tạo dữ liệu chuẩn cho biểu đồ tổng hợp chuỗi thời gian.
geom_col(fill=“skyblue”, alpha=0.8): Vẽ cột dày representing tổng Value từng năm, màu xanh nhạt, độ trong suốt 0.8 – giúp focus mạnh vào so sánh từng năm, phù hợp thể hiện độ lớn tuyệt đối.
geom_line(aes(group=1), color=“steelblue”, size=1): Thêm đường line nối các điểm tổng, rõ xu hướng lên-xuống qua từng mốc thời gian, màu xanh thép cải thiện sự nối mạch số liệu.
geom_point(size=2, color=“black”): Đánh dấu các điểm cụ thể, nổi bật từng số liệu quan trọng giúp không bị “mờ” khi cột lớn chồng lên nhau.
geom_smooth(method=“loess”, se=FALSE, color=“darkred”): Thêm đường trend (loess) không vùng SE, màu đỏ sẫm – hiển thị xu hướng trơn loại bỏ nhiễu, giúp nhận biết mạch phát triển dài hạn.
geom_text(aes(label=scales::comma(round(Total,0))), vjust=-0.5, size=3): Ghi số cụ thể lên đầu mỗi cột, giúp xem số liệu mà không cần di chuột hoặc tra bảng, nâng cao tính minh bạch.
labs(title=…, x=…, y=…): Tiêu đề, nhãn trục tiếng Việt rõ ràng, dễ hiểu.
theme_style: Áp dụng theme tối giản, nhất quán, giúp biểu đồ sạch, dễ đọc và chuyên nghiệp.
Kết quả kĩ thuật
Biểu đồ kết hợp bar, line, point và trend loess giúp thể hiện rõ cả tổng thể số liệu (cột skyblue), xu hướng từng năm (line), điểm giá trị từng năm (dot), và đường xu thế dài hạn (đường loess màu đỏ sẫm).
Tổng giá trị mỗi năm được annotate trực tiếp lên mỗi cột (ví dụ: năm 2017 ~6.0e12; 2019 ~7.9e12; năm 2023 đạt đỉnh ~1.15e13), giúp tra cứu số liệu dễ dàng và giảm sai số đọc.
Đường loess lặp theo số liệu thực, cho thấy rõ các pha tăng (2017-2019, 2021-2023), pha giảm đột biến năm cuối (2024: chỉ còn ~3.2e12 – thấp nhất chu kỳ); các thành phần biểu diễn phối hợp vừa trực quan, vừa dễ nhận diện sự chuyển động đột ngột hoặc chậm lại của tổng Value.
Ý nghĩa thống kê
Tổng giá trị qua các năm tăng khá ổn định đến tận năm 2023, đạt đỉnh cực đại (11,45 nghìn tỷ), phản ánh tốc độ tăng trưởng tài chính/doanh thu toàn doanh nghiệp/nhóm ngành liên tục được mở rộng, bền vững.
Năm 2024, tổng giá trị giảm rất mạnh (còn 3,24 nghìn tỷ), cảnh báo nguy cơ rủi ro lớn hoặc có biến động đặc biệt (suy giảm doanh thu, thị trường, vướng mắc tài chính…), yêu cầu lãnh đạo phải nhận diện nguyên nhân từ chính sách, thị trường hay chuyển dịch cấu trúc kinh doanh.
summary_data <- dt2 %>%
group_by(Indicator) %>%
summarise(mean_val = mean(Value, na.rm = TRUE),
.groups = 'drop') %>%
arrange(mean_val)
dt2_sorted <- dt2 %>%
mutate(Indicator = factor(Indicator,
levels = summary_data$Indicator))
p2 <- ggplot(dt2_sorted %>% filter(Indicator %in% top_inds),
aes(x=Indicator, y=Value, fill=Indicator)) +
geom_boxplot(alpha=0.6, outlier.shape=21, outlier.size=2,
outlier.color="red", show.legend=FALSE) +
geom_jitter(width=0.15, alpha=0.15, size=0.8, show.legend=FALSE) +
stat_summary(fun=mean, geom="point", color="black", size=3,
shape=21, fill="yellow") +
geom_hline(yintercept=mean(dt2$Value, na.rm=TRUE),
linetype="dashed", color="gray50", size=0.8) +
geom_text(data=summary_data %>% filter(Indicator %in% top_inds),
aes(y=mean_val,
label=scales::comma(round(mean_val/1e9, 1))),
vjust=-1.5,
hjust=0.5,
size=4,
color="black",
fontface="bold",
fontfamily="mono") +
coord_flip() +
labs(
title="(P2) Phân bố Value theo Indicator - Top Chỉ tiêu",
x="Indicator",
y="Value (tỷ VND)"
) +
theme_style +
theme(
plot.title = element_text(size=11, face="bold", hjust=0.5),
axis.text.y = element_text(size=8, face="plain"),
axis.title.y = element_text(size=9),
axis.text.x = element_text(size=8),
axis.title.x = element_text(size=9),
legend.position = "none",
panel.spacing = unit(0.5, "cm"),
plot.margin = margin(1, 2, 1, 1, "cm") # Tăng margin phải
)
## Warning in geom_text(data = summary_data %>% filter(Indicator %in% top_inds), :
## Ignoring unknown parameters: `fontfamily`
print(p2)
ggsave("p2_fixed.png", width=12, height=8, dpi=300)
plots[[i]] <- p2; i <- i + 1
p3 <- ggplot(dt2 %>% filter(Indicator %in% top_inds),
aes(x=Value, y=Indicator, fill=Indicator)) +
ggridges::geom_density_ridges(scale=1.2, alpha=0.8) + # L1
geom_vline(xintercept = median(dt2$Value, na.rm=TRUE), linetype="dashed") + # L2
stat_summary(fun=median, geom="point", color="black", size=1.5) + # L3
geom_text(aes(label="median"), x=median(dt2$Value, na.rm=TRUE), y=0.5, color="black") + # L4
theme_style + theme(legend.position="none") + # L5
labs(title="(P3) Phân bố mật độ Value theo Indicator")
print(p3); plots[[i]] <- p3; i <- i + 1
## Picking joint bandwidth of 1.73e+10
Hàm này tạo biểu đồ ridgeline cho thấy hình dạng phân phối mật độ Value của từng Indicator (top 8 nhóm lớn nhất), giúp nhận diện, so sánh, kiểm tra đặc điểm phân phối, lệch, vùng tập trung hay bất thường từng nhóm một cách trực quan, dễ hệ thống hóa
Trong đó:
filter(Indicator %in% top_inds): Lọc dữ liệu chỉ lấy 8 nhóm Indicator lớn nhất — đảm bảo phân tích tập trung vào các biến đại diện, loại bỏ nhiễu.
aes(x=Value, y=Indicator, fill=Indicator): X trục là giá trị, Y trục là nhóm indicator (phân lớp), fill cho từng lớp màu để nhận diện.
ggridges::geom_density_ridges(scale=1.2, alpha=0.8): Vẽ đường mật độ (density) từng nhóm với mức độ chồng lấn nhẹ (scale 1.2), tăng độ trong suốt (alpha 0.8) giúp so sánh và nhận diện vùng đậm/thưa của từng nhóm.
geom_vline(xintercept = median(df2$Value, na.rm=TRUE), linetype=“dashed”): Vẽ đường dọc thể hiện median toàn hệ — làm mốc tham chiếu tiêu chuẩn cho mỗi nhóm, giúp phát hiện nhóm nào lệch hẳn khỏi hệ quy chiếu chung.
stat_summary(fun=median, geom=“point”, color=“black”, size=1.5): Đánh dấu median từng nhóm indicator (chấm đen), hỗ trợ so sánh vị trí trung vị giữa các nhóm với nhau và cả với chuẩn hệ thống.
geom_text(aes(label=“median”), x=median(df2$Value, na.rm=TRUE), y=0.5, color=“black”): Ghi chú “median” cho trực quan, đảm bảo không nhầm mốc chuẩn hệ thống.
theme_style + theme(legend.position=“none”): Áp dụng theme tối giản nhất quán, bỏ chú giải, tập trung nội dung; dễ đọc, không rối mắt.
Kết quả kĩ thuật
Biểu đồ ridge density thể hiện rõ phân bố giá trị Value của từng nhóm Indicator, cho phép so sánh hình dạng và vùng tập trung mật độ từng nhóm trên cùng một trục Value.
Đa phần các nhóm chỉ tiêu chi phí (bán hàng, khấu trừ, lãi vay…) có đỉnh mật độ cao tại các giá trị âm; riêng DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ nằm lệch về phía dương, xác nhận bản chất chi phí – doanh thu đối lập, rõ ràng về mặt kinh tế.
Đường median tổng thể (dashed line) cắt qua các phân bố, median từng nhóm được highlight bằng điểm đen, giúp dễ đối chiếu và phát hiện sự lệch về âm/dương cũng như mức độ “outperform” hoặc bất thường khi một nhóm lệch xa so với hệ trung vị toàn bộ.
Ý nghĩa thống kê
Các chi phí (bán hàng, quản lý doanh nghiệp, thuế TNDN, lãi vay…) đều tập trung mạnh ở các giá trị âm, cho thấy đặc thù ngành tài chính/doanh nghiệp là chi phí lớn, hầu như tất cả quan sát đều “tiều tụy” ở vùng âm; doanh thu lại nổi bật ở vùng giá trị dương, là nhóm duy nhất tạo dòng tiền dương đều và mạnh so với nền chung của chi phí.
Sự khác biệt hình dạng và vị trí median giữa các indicator này là cơ sở phân tích hiệu quả vận hành: nếu chi phí nào có đỉnh lệch mạnh hoặc “bẹt” sang cực âm là vùng rủi ro, ngược lại doanh thu có median dương và phân bố rộng là điều kiện tạo lợi nhuận và dư địa tăng trưởng.
Biểu đồ này cực kỳ hữu ích khi nhận diện các nhóm cần tiết giảm chi phí, kiểm tra chiến lược giá vốn hợp lý, hoặc giải thích bản chất tài chính của doanh nghiệp rõ ràng, trực quan nhất – từ đó tối ưu hoá cấu trúc chi phí/doanh thu cho mục tiêu lợi nhuận.
p4 <- ggplot(dt2 %>% filter(Indicator %in% top_inds[1:4]),
aes(x=Value, fill=Indicator)) +
geom_histogram(aes(y=..density..), bins=30, alpha=0.5, color="white") + # L1
geom_density(alpha=0.3) + # L2
geom_vline(aes(xintercept=mean(Value, na.rm=TRUE)), color="red") + # L3
geom_rug(alpha=0.4) + # L4
facet_wrap(~Indicator, scales="free") + # L5
labs(title="(P4) Phân bố Value (Histogram + Density)") +
theme_style + theme(legend.text = element_text(size = 8) )
print(p4); plots[[i]] <- p4; i <- i + 1
## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(density)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Hàm này tạo biểu đồ histogram kết hợp density cho từng nhóm Indicator (top 4 có nhiều quan sát nhất), nhằm trực quan so sánh hình dạng phân phối, độ lệch tập trung, vùng giá trị đặc biệt của Value theo từng nhóm.
Trong đó:
filter(Indicator %in% top_inds[1:4]): Chỉ lấy top 4 nhóm indicator giàu dữ liệu nhất vào phân tích, giúp kết quả đại diện quy mô mẫu tốt.
aes(x=Value, fill=Indicator): X trục là giá trị, fill cho mỗi panel từng nhóm màu — hỗ trợ nhận biết khác biệt nhóm qua màu sắc.
geom_histogram(aes(y=..density..), bins=30, alpha=0.5, color=“white”): Vẽ histogram với số bins vừa phải (30), màu trắng viền, alpha 0.5 để nền histogram mờ, nổi bật density overlay; Y trục là density chứ không phải count, thuận tiện so sánh vùng “dày” dữ liệu giữa các nhóm.
geom_density(alpha=0.3): Overlay density plot trên histogram, tăng alpha 0.3 để nhìn rõ phân phối nhưng không lấn át histogram.
geom_vline(aes(xintercept=mean(Value, na.rm=TRUE)), color=“red”): Kẻ đường dọc vị trí mean từng panel, màu đỏ nổi bật, giúp nhận diện “trung bình” của Value cho từng nhóm — vùng gần mean là vùng giá trị “chuẩn” nhất nhóm.
geom_rug(alpha=0.4): Vẽ các vạch nhỏ ở cạnh đáy (rugs) đại diện điểm dữ liệu, hỗ trợ nhận diện outlier hoặc tập trung dữ liệu nhanh.
facet_wrap(~Indicator, scales=“free”): Tạo nhiều panel riêng cho từng nhóm, scales=“free” cho phép mỗi nhóm có vùng giá trị hiển thị riêng, tránh bias khi nhóm có biên độ khác biệt.
labs(title=…); theme_style: Tiêu đề tiếng Việt rõ ràng, theme dùng chung cho bảng biểu đồ chuyên nghiệp.
Kết quả kĩ thuật
Việc thu nhỏ legend đã giúp phần chú thích dưới biểu đồ hiển thị đủ thông tin mà không mất chữ, nhờ thiết lập legend.text = element_text(size = 8), tăng sự gọn gàng và tính chuyên nghiệp cho trình bày.
Mỗi panel biểu diễn 1 indicator, có histogram (cột) và density (đường cong) chồng lên nhau, giúp nhận diện trực quan vùng phân bố dày, điểm bất thường/outlier dễ dàng.
Đường đỏ là giá trị mean, giúp đối chiếu vị trí trung bình từng nhóm; rug plot đáy hiển thị dữ liệu thực, thuận tiện phát hiện vùng tập trung hay nhiễu động.
Các nhóm CHI PHÍ BÁN HÀNG, CHI PHÍ LÃI VAY… đều tập trung giá trị ở vùng âm (âm hàng chục tỷ), đa số các quan sát rơi vào vùng này, cho thấy phân phối skewed và rõ đặc thù ngành
Ý nghĩa thống kê
Cả 4 nhóm indicator lớn đều là các khoản chi phí, phân phối tập trung cao ở vùng giá trị âm, xác nhận thực tế chi phí là áp lực chính đối với doanh nghiệp – phần lớn dữ liệu tập trung quanh -1.5e11 đến 0, median thấp, mean thấp.
Sự lệch về phía âm của mean và density cho thấy áp lực cắt giảm chi phí, tối ưu vận hành là chìa khoá cải thiện hiệu quả kinh doanh; những dị biệt (đỉnh thứ 2, tails bất thường) cảnh báo cần kiểm tra lại nghiệp vụ hoặc xác minh nguyên nhân suất hiện giá trị bất thường.
Legend nhỏ gọn nâng cao giá trị truyền đạt số liệu, giúp việc trình bày báo cáo, thảo luận giữa các phòng ban trở nên trực quan, tận dụng không gian chart tối đa cho việc nhìn nhận ý nghĩa kinh tế, thay vì bị rối phần phụ.
p5 <- ggplot(dt2, aes(x=LogValue, y=Value)) +
geom_point(alpha=0.4, color="dodgerblue") +
geom_smooth(method="lm", se=FALSE, color="red") +
geom_smooth(method="loess", se=TRUE, color="black", linetype="dotted") +
geom_text_repel(data=dt2 %>% slice_max(Value, n=5),
aes(label=Indicator), size=3) +
geom_rug() + labs(title="(P5) Quan hệ giữa LogValue và Value") + theme_style
print(p5); plots[[i]] <- p5; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Hàm này tạo biểu đồ scatter, kết hợp các đường fit và nhãn cho các điểm đặc biệt, giúp trực quan hóa quan hệ giữa biến Value (gốc) và LogValue (logarit hóa, chuẩn hóa biên độ), cung cấp cái nhìn về tuyến tính hóa dữ liệu, cấu trúc phân phối .
Trong đó:
geom_point(alpha=0.4, color=“dodgerblue”): Vẽ các điểm dữ liệu, sử dụng màu xanh nổi bật và alpha 0.4 làm mờ, giảm chồng lấn khi có nhiều điểm.
geom_smooth(method=“lm”, se=FALSE, color=“red”): Vẽ đường hồi quy tuyến tính (linear model) màu đỏ, không có band SE (confidence interval), cho thấy xu hướng tăng/giảm tuyến tính hóa giữa LogValue và Value.
geom_smooth(method=“loess”, se=TRUE, color=“black”, linetype=“dotted”): Thêm đường fit loess (hàm spline mượt) màu đen, đường nét đứt — giúp kiểm tra các biến động phi tuyến, band SE cho thấy vùng tin cậy, hỗ trợ xác định độ ổn định dữ liệu ngoài tuyến tính.
geom_text_repel(data=dt2%>% slice_max(Value, n=5), aes(label=Indicator), size=3): Ghi chú nhãn Indicator cho 5 điểm có Value lớn nhất, giúp nhận diện chính xác những outlier hoặc doanh nghiệp/nhóm mạnh nhất.
geom_rug(): Vẽ các vạch nhỏ ở đáy trục, hỗ trợ nhận diện chồng lấn/ngưỡng của các điểm dữ liệu.
labs(title=…), theme_style: Tiêu đề rõ ràng, theme tối giản — tăng chuyên nghiệp cho biểu đồ.
Kết quả kĩ thuật
Biểu đồ scatter thể hiện rõ: phần lớn các điểm tập trung quanh Value=0 và LogValue nhỏ, nhưng có một tập điểm nổi bật tăng rất mạnh về phía phải, ứng với các giá trị LogValue lớn, Value tăng nhanh.
Đường hồi quy tuyến tính (red line) cho thấy tổng thể mối quan hệ tăng tuyến tính giữa LogValue và Value, nhưng dòng loess (dotted black) cho thấy dữ liệu thực có đoạn “bẻ cong” (log-linearity bị phá vỡ khi LogValue lớn), đặc biệt từ LogValue > 25.
5 điểm lớn nhất được gắn nhãn đều là “DOANH THU THUẦN” hoặc “DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ,” cụm này hợp lý vì về bản chất doanh thu luôn là các outlier dương cao nhất trong bộ dữ liệu ngành tài chính/kế toán.
Ý nghĩa thống kê
Sự tăng mạnh về Value ở vùng LogValue lớn thể hiện các doanh nghiệp, nhóm ngành quy mô cao mang lại giá trị tuyệt đối vượt trội, tạo “outlier” tích cực – cho thấy sức mạnh chênh lệch tài chính lớn trong cùng ngành.
Tính tuyến tính “tương đối” giữa LogValue và Value xác nhận log transformation phù hợp cho dự báo, phân tích tài chính vì giảm ảnh hưởng outlier, nhưng thực tế cần mô hình hóa phi tuyến ở vùng cực trị (log lớn).
Nhóm các điểm name-label (doanh thu lớn nhất) là đích đến của tăng trưởng, đồng thời cảnh báo nếu phần còn lại của dữ liệu lệch khỏi nhóm này thì cần kiểm soát kỳ vọng tăng trưởng/rủi ro cực đoan khi ra quyết định quản trị, hoạch định.
p6 <- dt2 %>%
group_by(Indicator, Year) %>%
summarise(meanV=mean(Value,na.rm=TRUE), .groups="drop") %>%
ggplot(aes(x=Year, y=reorder(Indicator, meanV), fill=meanV)) +
geom_tile(color="white") +
geom_text(aes(label=round(meanV/1e9,1)),size = 2.5) +
scale_fill_viridis_c() +
geom_vline(xintercept=2020, linetype="dashed") +
labs(title="(P6) Heatmap giá trị trung bình theo năm") + theme_style +
theme(
axis.text.x = element_text(size = 8, angle = 45, hjust = 1),
axis.text.y = element_text(size = 8),
legend.text = element_text(size = 7),
legend.title = element_text(size = 8),
legend.position = "bottom"
) +
guides(fill = guide_colorbar(barwidth = 7, barheight = 0.4, title.position = "top"))
print(p6); plots[[i]] <- p6; i <- i + 1
Hàm này tạo heatmap (biểu đồ nhiệt) để tổng hợp và trực quan hóa giá trị trung bình (mean) của mỗi Indicator theo từng năm, giúp phát hiện xu hướng tăng giảm, vùng nổi bật hoặc “điểm nóng/lạnh” trong dữ liệu tài chính/kế toán.
Trong đó:
group_by(Indicator, Year) %>% summarise(meanV=mean(Value,na.rm=TRUE), .groups=“drop”): Gom nhóm và tính giá trị trung bình Value từng Indicator theo từng năm, chuẩn hóa dữ liệu cho heatmap.
aes(x=Year, y=reorder(Indicator, meanV), fill=meanV): X trục năm, Y là các Indicator sắp xếp theo meanV, tô màu theo cường độ meanV.
geom_tile(color=“white”): Vẽ từng “mảng màu” heatmap, viền trắng tách ô, dễ nhìn.
**geom_text*(aes(label=round(meanV/1e9,1)), size=2.5)**: Hiện số trung bình từng ô (tính theo tỷ đồng), font nhỏ vừa, tăng khả năng đọc số liệu nhanh khi trình bày.
scale_fill_viridis_c(): Bảng màu viridis liên tục, đảm bảo chuyên nghiệp, rõ ràng cả với người bị mù màu.
geom_vline(xintercept=2020, linetype=“dashed”): Kẻ mốc dọc năm 2020 — đánh dấu sự kiện lớn để dễ nhận diện thay đổi trước/sau “sốc” kinh tế (Covid,…).
Tinh chỉnh theme:
axis.text.x = element_text(size=8, angle=45, hjust=1), axis.text.y = element_text(size=8): Chữ trục nhỏ, trục x nghiêng, tránh đè chữ x/y.
legend.text = element_text(size=7), legend.title = element_text(size=8), legend.position=“bottom”: Chú thích nhỏ lại và đặt dưới cho gọn.
guides(fill = guide_colorbar(barwidth = 7, barheight = 0.4, title.position = “top”)): Điều chỉnh thanh màu chú thích nhỏ lại, dễ vừa khung.
Kết quả kĩ thuật
Biểu đồ heatmap đã tinh chỉnh cỡ chữ trục, số liệu và legend hợp lý: các chỉ tiêu (Y) nhỏ gọn, số mean mỗi ô vừa phải, legend nằm ngang nhỏ dưới cùng — giúp hình trực quan, dễ đọc.
Chú thích màu với thang viridis giúp phân biệt rõ vùng giá trị lớn/nhỏ. Các ô màu vàng/xanh lá đậm là các nhóm có mean lớn. Các chỉ tiêu như DOANH THU THUẦN, DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ nổi bật với mean từ 521–1115 (tỷ), gấp nhiều lần các nhóm còn lại.
Vạch dọc năm 2020 phân tách dữ liệu thành hai pha: trước và sau sự kiện năm 2020 (có thể là mốc dịch hay thay đổi lớn), thuận lợi so sánh động thái chuyển biến giá trị trung bình.
Ý nghĩa thống kê
Chỉ tiêu doanh thu duy trì giá trị trung bình cao, tăng mạnh nhất ở năm 2023–2024 (DOANH THU THUẦN: từ 744.5 lên 1115.2 tỷ) cho thấy giai đoạn tăng trưởng hoặc phục hồi cực mạnh.
Nhiều chỉ tiêu chi phí hoặc lỗ (chi phí bán hàng, giá vốn hàng bán, thu nhập khác…) duy trì mean âm hoặc giá trị nhỏ, cảnh báo nhóm rủi ro, hiệu quả hoạt động kém ổn định, đặc biệt sau các biến động lớn ngành/nghề.
Phần lớn các chỉ tiêu còn lại mean gần bằng 0 sau năm 2020 (nhiều ô số “0”), chứng tỏ dữ liệu ghi nhận tập trung vào một số lĩnh vực/công ty lớn, hoặc ngành phân hoá mạnh giai đoạn này.
Heatmap tạo điều kiện đánh giá tác động của biến động thị trường, hiệu quả điều hành và năng lực thích ứng từng nhóm/từng năm – lãnh đạo có thể dựa vào để điều chỉnh chiến lược, ưu tiên nguồn lực vào vùng đang “nóng lên”.
p7 <- ggplot(dt2 %>% filter(Indicator %in% top_inds),
aes(x=Date, y=Value, color=Indicator)) +
geom_line(size=0.9) + geom_point(size=1.5) +
geom_smooth(se=FALSE, linetype="dotted") +
geom_ribbon(aes(ymin=Value*0.9, ymax=Value*1.1, fill=Indicator), alpha=0.05) +
labs(title="(P7) Xu hướng theo thời gian", x="Date", y="Value") + theme_style +
theme(
legend.text = element_text(size = 5),
legend.title = element_text(size = 7),
legend.key.size = unit(0.6, "lines"),
legend.position = "bottom"
) +
guides(
color = guide_legend(nrow = 2, byrow = TRUE),
fill = guide_legend(nrow = 2, byrow = TRUE)
)
print(p7); plots[[i]] <- p7; i <- i + 1
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Hàm này vẽ biểu đồ đường thời gian cho nhiều Indicator, cho phép theo dõi xu hướng và so sánh vận động của giá trị Value từng nhóm (Indicator) qua từng điểm Date. Đây là dạng biểu đồ quan sát tổng thể giúp nắm bắt chu kỳ, phát hiện tăng trưởng đột biến, suy giảm hoặc các biến động bất thường giữa các chỉ tiêu quan trọng.
Trong đó:
geom_line(size=0.9): Vẽ đường xu hướng cho từng chỉ tiêu.
geom_point(size=1.5): Bổ sung các điểm dữ liệu thực trên đường line, dễ nhận diện mốc quan sát.
geom_smooth(se=FALSE, linetype=“dotted”): Đường mượt (smooth), dạng nét đứt không hiển thị vùng tin cậy (confidence interval), giúp xác định xu hướng động tổng thể và loại bỏ nhiễu.
geom_ribbon(aes(ymin=Value0.9, ymax=Value1.1, fill=Indicator), alpha=0.05): Dải băng (ribbon) trên dưới quanh từng đường chỉ tiêu, tô màu nhẹ (alpha=0.05), minh họa biên độ biến động ±10% quanh từng series – trực quan vùng ổn định/lệch mạnh.
labs(…): Tiêu đề, trục x/y rõ ràng, tiếng Việt nhất quán, phục vụ thuyết trình/phân tích.
theme_style: Trình bày biểu đồ tối giản, nhất quán thẩm mỹ chuyên nghiệp.
theme(…): Thu nhỏ chữ chú thích (legend.text), tiêu đề chú thích, box size chỉ còn 0.6 lines, legend nằm phía dưới giúp tránh đè/lấp mất chữ.
guides(color = guide_legend(nrow = 2, byrow = TRUE), fill = guide_legend(nrow = 2, byrow = TRUE)): Chia chú thích màu thành 2 dòng, tránh tình trạng legend tràn ra ngoài khung biểu đồ và mất chữ nếu có nhiều chỉ tiêu.
Kết quả kĩ thuật
Biểu đồ kết hợp line, point, smooth, ribbon cho phép theo dõi trực quan xu hướng Value từng Indicator qua thời gian dài (2017–2025).
Dễ dàng nhận thấy các Indicator doanh thu nổi bật — đặc biệt “DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ” và “DOANH THU THUẦN” đường màu hồng, mỗi năm đều có đỉnh nhọn (chu kỳ quý hoặc năm), giá trị từng đỉnh lên đến ~3e+12, tức khoảng 3.000 tỷ.
Nhóm chi phí (bán hàng, tài chính, quản lý doanh nghiệp…) nằm sát đường Value = 0, gần như không biến động lớn, tập trung thành các cụm line phía dưới, các chu kỳ biên độ nhỏ.
Chú thích legend nhỏ gọn với cỡ chữ size = 5, tiêu đề 7, chia thành 2 dòng dưới chân chart giúp hiển thị đầy đủ tên các chỉ tiêu mà không bị mất chữ khi số lượng Indicator lớn.
Ý nghĩa thống kê
Nhóm doanh thu (đường nổi ở trên, đỉnh cao lặp lại, giá trị lên tới 2–3 nghìn tỷ) thể hiện khả năng tạo dòng tiền mạnh, tính chu kỳ rõ nét — có thể là đặc trưng của ngành bán lẻ, hàng hóa hoặc tài chính theo quý/năm.
Nhóm chi phí hoặc khoản giảm trừ ổn định, không tăng đột biến qua các năm, đồng nghĩa quản trị tài chính doanh nghiệp hiệu quả, chi phí kiểm soát tốt so với quy mô doanh thu đạt được.
Đích tăng trưởng xuất hiện mỗi cuối năm/quý, góc nhìn tài chính nên chú ý các chỉ tiêu này khi dự báo hoặc lên kế hoạch ngân sách, đầu tư — tập trung nguồn lực vào chỉ tiêu đang tạo hiệu quả rõ rệt.
Cấu trúc giá trị các nhóm nhỏ thể hiện sự phân hóa ngành: nhóm lớn ổn định, nhóm nhỏ kiểm soát tốt không tạo ra biến động rủi ro lớn, thích hợp mô hình hóa và kiểm soát đa chỉ tiêu.
p8 <- dt2 %>%
group_by(Year, Quarter) %>%
summarise(Total=sum(Value,na.rm=TRUE)) %>%
ggplot(aes(x=Quarter, y=Total, fill=Quarter)) +
geom_col() + geom_line(aes(group=1)) + geom_point() +
geom_text(aes(label=round(Total/1e9,1)), vjust=-0.5, size=3.3) +
facet_wrap(~Year) + labs(title="(P8) Tổng Value theo Quarter") + theme_style
## `summarise()` has grouped output by 'Year'. You can override using the
## `.groups` argument.
print(p8); plots[[i]] <- p8; i <- i + 1
Hàm này tạo biểu đồ cột (bar chart) kết hợp line và point, theo từng quý (Quarter) cho từng năm (Year), dùng facet_wrap để mỗi năm là một biểu đồ con. Nhằm trực quan tổng giá trị từng quý qua các năm, giúp nhận biết xu hướng tăng/giảm, chu kỳ mùa vụ hoặc các biến động bất thường trong từng năm
Trong đó:
group_by(Year, Quarter) %>% summarise(Total=sum(Value,na.rm=TRUE)): Gom nhóm dữ liệu theo từng năm và từng quý, tính tổng Value cho từng nhóm (Total những giá trị thực tế của từng quý trong từng năm).
geom_col(): Vẽ cột (bar) cho từng quý, height là tổng Value.
geom_line(aes(group=1)): Thêm đường nối giữa các điểm tổng quý, diễn biến liên tục xu hướng biến động các quý trong năm đó.
geom_point(): Đánh dấu điểm giá trị tổng trên line, tăng khả năng nhận diện trực quan từng quý.
geom_text(aes(label=round(Total/1e9,1)), vjust=-0.5, size=3.3): Ghi trực tiếp số liệu từng cột (theo đơn vị tỷ, ví dụ “3.0”~3000 tỷ), đặt phía trên cột; giúp người đọc dễ nắm số.
facet_wrap(~Year): Chia nhỏ thành nhiều biểu đồ con, mỗi chart ứng với 1 năm, tối ưu so sánh chu kỳ từng năm trên cùng layout.
labs(title=…), theme_style: Tiêu đề, mẫu theme trình bày chuyên nghiệp.
Kết quả kĩ thuật
Biểu đồ sử dụng facet_wrap, mỗi khung là một năm, trực quan hóa rõ đường tăng trưởng và tổng hợp của từng quý (Q1-Q4, Yearly) từ 2017–2025.
Các cột được tô màu theo quý, số liệu hiển thị trực tiếp phía trên cột (đơn vị tỷ), ví dụ Q4/2017 đạt gần 814 tỷ, Yearly luôn vượt trội hẳn: 3.014,8 tỷ (2017), 3.960,9 tỷ (2018), 4.632,4 tỷ (2021)…
Đường line và point nối các điểm theo quý cho từng năm giúp nhận biết nhanh chu kỳ vận động và so sánh độ chênh lệch/thăng trầm qua các quý.
Việc rút gọn số liệu (chia 1e9) giúp số không bị đè lên nhau, dễ đọc dù cột sát nhau, phù hợp với biểu đồ tổng hợp chuyên nghiệp.
Ý nghĩa thống kê
Biểu đồ sử dụng facet_wrap, mỗi khung là một năm, trực quan hóa rõ đường tăng trưởng và tổng hợp của từng quý (Q1-Q4, Yearly) từ 2017–2025.
Các cột được tô màu theo quý, số liệu hiển thị trực tiếp phía trên cột (đơn vị tỷ), ví dụ Q4/2017 đạt gần 814 tỷ, Yearly luôn vượt trội hẳn: 3.014,8 tỷ (2017), 3.960,9 tỷ (2018), 4.632,4 tỷ (2021)…
Đường line và point nối các điểm theo quý cho từng năm giúp nhận biết nhanh chu kỳ vận động và so sánh độ chênh lệch/thăng trầm qua các quý.
Việc rút gọn số liệu (chia 1e9) giúp số không bị đè lên nhau, dễ đọc dù cột sát nhau, phù hợp với biểu đồ tổng hợp chuyên nghiệp.
p9data <- dt2 %>%
group_by(Indicator) %>%
summarise(m = mean(Value, na.rm = TRUE))
p9 <- ggplot(p9data, aes(x = reorder(Indicator, m), y = m)) +
geom_segment(aes(xend = Indicator, y = 0, yend = m), color = "gray70") +
geom_point(size = 3, color = "tomato") +
geom_text(aes(label = round(m/1e9, 1)), hjust = -0.2) +
coord_flip() +
geom_hline(yintercept = 0, linetype = "dashed") +
labs(title = "(P9) Trung bình Value theo Indicator (Lollipop chart)") +
theme_style
print(p9) ; plots[[i]] <- p9; i <- i + 1
Hàm này dùng để trực quan giá trị trung bình (mean) của từng Indicator dưới dạng biểu đồ Lollipop (lollipop chart), giúp so sánh thứ hạng, độ lớn nhỏ, và nhận diện nhóm vượt trội/bất thường một cách trực quan, tiết kiệm không gian và hiệu quả hơn bar chart truyền thống.
Trong đó:
p9data <- dt2%>% group_by(Indicator) %>% summarise(m = mean(Value, na.rm = TRUE)) Gom nhóm theo Indicator, tính trung bình Value mỗi nhóm; trả về data frame có 2 cột: Indicator và m.
ggplot(p9data, aes(x = reorder(Indicator, m), y = m)) Trục x là Indicator (được sắp tăng/giảm theo giá trị mean), trục y là giá trị trung bình (m), thuận tiện so sánh thứ tự từ thấp đến cao.
geom_segment(aes(xend = Indicator, y = 0, yend = m), color = “gray70”) Vẽ đường thẳng từ gốc (y=0) đến điểm giá trị mean của từng Indicator, là “que kẹo” trong lollipop.
geom_point(size = 3, color = “tomato”) Đánh dấu đầu “que kẹo” bằng chấm tròn màu đỏ nổi bật, thu hút ánh nhìn vào giá trị cụ thể.
geom_text(aes(label = round(m/1e9, 1)), hjust = -0.2) Hiển thị giá trị mean (chuyển sang tỷ, 1 số thập phân), đặt sát đầu points.
coord_flip() Lật trục, để tên Indicator nằm dọc dễ đọc hơn, nhất là khi nhiều nhóm.
geom_hline(yintercept = 0, linetype = “dashed”) Đường tham chiếu giá trị 0 cho phép phân biệt nhóm có mean âm/dương rõ ràng.
labs(title = “…”) Đặt tiêu đề rõ ràng mục đích biểu đồ, dễ hiểu với người đọc.
theme_style Tối ưu thẩm mỹ, đảm bảo chart đồng bộ với toàn bộ hệ thống báo cáo.
Kết quả kĩ thuật
Biểu đồ dạng lollipop chart giúp so sánh trực quan thứ tự, mức độ trung bình Value của tất cả chỉ tiêu: mỗi “que kẹo” là một Indicator, chấm đỏ là điểm trung bình, nối xuống gốc bằng đường xám nhạt.
Trục x lật ngang (coord_flip) giúp các tên Indicator dài không bị đè lên nhau, rõ ràng khi báo cáo hoặc trình chiếu kỹ thuật.
Label số liệu đơn vị tỷ ngay bên phải từng điểm (vd: “227.8”, “106.6”), kết hợp dashed line tại y=0 phân vùng nhanh các nhóm có mean âm/dương — rõ nhất là GIÁ VỐN HÀNG BÁN, CHI PHÍ QUẢN LÝ, CHI PHÍ VAY đều có mean rất thấp/âm, trong khi nhóm DOANH THU THUẦN, DOANH THU BÁN HÀNG, LỢI NHUẬN GỘP vọt lên giá trị 100–700 tỷ trở lên.
Các nhóm giá trị mean gần 0 thấy rõ nhóm “trung tính”/ít ảnh hưởng tổng thể, giúp nhận diện ưu tiên khi tối ưu hóa mô hình.
Ý nghĩa thống kê
Các chỉ tiêu giá trị trung bình cao nhất đều là nhóm doanh thu – dẫn đầu là DOANH THU THUẦN và DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ, vượt 500–700 tỷ/năm. Đây là trục xoay tài chính, khẳng định động lực tăng trưởng/năng lực cạnh tranh doanh nghiệp.
Nhóm mean âm (vd GIÁ VỐN HÀNG BÁN ~-538 tỷ) đại diện chi phí “nặng” nhất, hoặc lãi/lỗ làm giảm tổng tài sản — cần kiểm soát tối đa để tăng hiệu quả.
Các chỉ tiêu còn lại (lợi nhuận gộp, lãi/lỗ, tài chính, chi phí quản lý…) có mean nhỏ hoặc âm — xác định vùng “cải thiện”, tối ưu cần quan tâm khi điều chỉnh kế hoạch hành động tài chính hoặc đầu tư mới.
Phân bố mean Value theo lollipop giúp lãnh đạo thấy rõ đâu là “động lực tạo giá trị”, đâu là khoản cần tiết giảm, tăng sức cạnh tranh cũng như cảnh báo vùng kinh doanh kém hiệu quả hoặc tiềm ẩn rủi ro.
p10 <- ggplot(dt2 %>% filter(Indicator %in% top_inds), aes(x=YoY, fill=Indicator)) +
geom_density(alpha=0.4) +
geom_vline(xintercept=0, linetype="dashed") +
geom_rug() +
facet_wrap(~Indicator, scales="free") +
labs(title="(P10) Phân bố YoY theo Indicator") +
theme_style +
theme(
axis.text.x = element_text(size = 7),
axis.text.y = element_text(size = 7),
strip.text = element_text(size = 8),
panel.spacing = unit(0.7, "lines"),
legend.text = element_text(size = 6),
legend.title = element_text(size = 5),
legend.key.size = unit(0.4, "lines"),
legend.position = "bottom"
)
print(p10)
Hàm này trực quan hóa phân bố tỷ lệ tăng trưởng năm sau so với năm trước (YoY – Year-over-Year) của từng Indicator lớn nhất, mỗi chỉ tiêu là một panel riêng biệt.
Dùng density plot để kiểm tra hình dạng phân phối YoY: nhóm nào tập trung quanh 0, nghiêng về dương/âm, bất đối xứng, có nhiều outlier hay không. Nhờ đó giúp đánh giá tính ổn định, rủi ro hoặc cơ hội tăng trưởng từng mảng vận hành, phục vụ phân tích thời gian hoặc so sánh hiệu quả quản trị theo chu kỳ.
Trong đó:
filter(Indicator %in% top_inds): Chỉ giữ lại các Indicator lớn nhất, giảm nhiễu khi so sánh density nhiều nhóm cùng lúc.
aes(x=YoY, fill=Indicator): Trục x chứa giá trị YoY, fill giúp các nhóm density khác màu nhau (phân biệt trực quan trên chú thích).
geom_density(alpha=0.4): Vẽ phân phối mật độ xác suất kernel của YoY, alpha = 0.4 giúp hình nền trong suốt một phần để dễ nhìn nhiều nhóm.
geom_vline(xintercept=0, linetype=“dashed”): Thêm đường dọc tại YoY = 0 để phân biệt tăng (bên phải) và giảm (bên trái). Hỗ trợ nhanh xác định tỷ lệ nào âm/nhiều năm giảm liên tiếp.
geom_rug(): Các vạch nhỏ thể hiện từng điểm dữ liệu YoY, hỗ trợ nhận diện outlier hoặc vùng tập trung dày.
facet_wrap(~Indicator, scales=“free”): Chia mỗi chỉ tiêu thành một panel (biểu đồ con) giúp so sánh trực tiếp đặc điểm phân phối giữa các nhóm mà không bị lệch trục.
labs(title=…): Đặt tiêu đề rõ ràng phục vụ truyền đạt báo cáo.
theme_style + theme(…):
Thu nhỏ chữ các trục, panel, chú thích với size = 7 hoặc nhỏ hơn.
Giảm size legend xuống còn 6–5 và box nhỏ lại, thuận tiện trình bày nhiều chỉ tiêu không bị đè chữ chú thích.
panel.spacing tăng giúp các panel cách nhau vừa đủ, labels và số liệu/gạch không bị sát nhau, dễ đọc hơn
Kết quả kĩ thuật
Biểu đồ chia thành 8 panel, mỗi panel là một chỉ tiêu lớn có density plot + rug + đường dọc YoY = 0 làm chuẩn tham chiếu tăng/giảm.
Các nhóm như CÁC KHOẢN GIẢM TRỪ DOANH THU, CHI PHÍ BÁN HÀNG, CHI PHÍ KHÁC có phân bố tập trung hẹp quanh YoY âm (-75 hoặc dưới 0), density cao nhọn, xác nhận tính ổn định nhưng chiều hướng giảm liên tục.
Nhóm CHI PHÍ QUẢN LÝ DOANH NGHIỆP có đỉnh tập trung xung quanh YoY âm nhưng có các vệt phải (đuôi dài) đến giá trị dương, cho thấy một phần dữ liệu biến động mạnh, có thể tồn tại năm tăng trưởng ngoại lệ.
CHI PHÍ THUẾ THU NHẬP DOANH NGHIỆP và DOANH THU BÁN HÀNG VÀ CUNG CẤP DỊCH VỤ có phân phối rộng, nhiều đỉnh (multimodal), xác nhận có nhiều nhóm doanh nghiệp hoặc giai đoạn khác biệt rõ về tốc độ tăng trưởng, cần phân tích sâu thêm.
Đường dọc tại 0, kết hợp rug (các vạch dưới trục) cho thấy số lượng quan sát dương và âm tương đối phân bố đồng đều, trừ nhóm tập trung mạnh theo một chiều.
Ý nghĩa thống kê
Nhóm có phân phối tập trung âm mạnh (CÁC KHOẢN GIẢM TRỪ, CHI PHÍ BÁN HÀNG) xác nhận xu hướng kiểm soát tốt hoặc chi phí giảm liên tục qua các năm – là tín hiệu tích cực về hiệu quả quản lý chi phí, tuy nhiên cũng đặt ra câu hỏi về khả năng mở rộng hoặc tăng trưởng doanh thu nếu xu hướng này kéo dài.
Nhóm có phân phối rộng, phân tán (CHI PHÍ QUẢN LÝ, DOANH THU) cho thấy độ biến động tăng trưởng rất cao, đại diện doanh nghiệp chưa ổn định hoặc chịu ảnh hưởng mạnh từ thị trường, chu kỳ kinh tế, chiến lược hoạt động – cần giám sát kỹ để tránh rủi ro dòng tiền.
Density có đỉnh rõ ở YoY dương minh chứng các chỉ tiêu có năng lực phục hồi/tăng trưởng, trong khi các phân phối tập quanh YoY = 0 hoặc đuôi kéo dài phản ánh giai đoạn trì trệ hoặc tăng/giảm không đều.
Nhìn tổng quan, đa số chỉ tiêu quan trọng có YoY âm hoặc quanh 0 cho thấy sức ép khó khăn trong bối cảnh kinh tế hoặc cạnh tranh ngành, doanh nghiệp hoặc nhóm các công ty ghi nhận doanh thu/lợi nhuận khó bứt phá.
p12 <- ggplot(dt2 %>% group_by(Year) %>% summarise(meanV=mean(Value,na.rm=TRUE)),
aes(x=Year, y=meanV)) +
geom_line(size=1.1, color="steelblue") + geom_point(size=2) +
geom_smooth(method="lm", se=TRUE, color="darkred") +
geom_ribbon(aes(ymin=meanV*0.9, ymax=meanV*1.1), alpha=0.1, fill="gray") +
geom_text(aes(label=round(meanV/1e9,1)), vjust=-0.5, size=5) +
labs(title="(P12) Trung bình Value theo năm (regression)") + theme_style
print(p12); plots[[i]] <- p12; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
Hàm này vẽ biểu đồ đường thời gian (time series) của trung bình Value theo từng năm, kết hợp hồi quy tuyến tính (linear model - lm), dải tin cậy (confidence interval - CI), và ribbon để chỉ thị vùng biến động ±10% xung quanh trung bình. Nhằm trực quan tỷ lệ tăng/giảm trung bình Value qua các năm, xác định xu hướng dài hạn, đánh giá tính ổn định, và dự báo tương lai dựa trên hình mô hình tuyến tính.
Trong đó:
group_by(Year) %>% summarise(meanV=mean(Value,na.rm=TRUE)) Gom nhóm theo từng năm, tính trung bình Value/năm, chuẩn hóa dữ liệu gồm 2 cột: Year và meanV.
geom_line(size=1.1, color=“steelblue”) Vẽ đường nối các điểm trung bình theo năm, dày 1.1, màu xanh đẹp – hiện xu hướng chính xác.
geom_point(size=2) Đánh dấu từng điểm trung bình năm bằng chấm rõ, dễ nhận diện vị trí từng năm.
geom_smooth(method=“lm”, se=TRUE, color=“darkred”) Thêm đường hồi quy tuyến tính (fitting line), se=TRUE hiển thị vùng tin cậy 95% (CI band), màu đỏ nổi bật – giúp xác định xu hướng toàn diện và độ tin cậy của dự báo.
geom_ribbon(aes(ymin=meanV0.9, ymax=meanV1.1), alpha=0.1, fill=“gray”) Dải băng xám nhạt thể hiện vùng biến động ±10% xung quanh trung bình, alpha=0.1 trong suốt – hỗ trợ nhận diện nhanh vùng ổn định/dao động từng năm.
geom_text(aes(label=round(meanV/1e9,1)), vjust=-0.5, size=5) Ghi số liệu trung bình (đơn vị tỷ, 1 số thập phân) phía trên từng điểm, vjust=-0.5 giúp đặt hơi cao để dễ đọc.
labs(title=…), theme_style Tiêu đề rõ ràng, theme đồng bộ chuyên nghiệp cho báo cáo.
Kết quả kĩ thuật
Biểu đồ thể hiện đường xu hướng trung bình Value (màu xanh steelblue) từ 2017 đến 2025 với các điểm năm rõ ràng, kèm số liệu trên từng điểm (đơn vị tỷ).
Đường hồi quy tuyến tính màu đỏ (darkred) có độ dốc nhẹ tăng dần, bao quanh vùng tin cậy (CI band màu xám), cho thấy xu hướng tăng trưởng trung bình dài hạn ổn định nhưng với độ không chắc chắn cao do vùng CI khá rộng.
Dải xám nhạt (ribbon ±10% xung quanh giá trị trung bình) giúp nhận diện vùng biến động cho phép: các năm nằm ngoài dải này là dấu hiệu bất thường cần chú ý.
Nhìn vào số liệu: 2017 (48,2 tỷ) → tăng đến 2023 đạt đỉnh 91,6 tỷ → sau đó giảm xuống 2024 (59,7 tỷ) → 2025 (64,7 tỷ). Điểm năm 2023 vượt rất xa đường lm, là điểm outlier dương mạnh.
Ý nghĩa thống kê
Xu hướng tuyến tính tăng dần (đường đỏ) xác nhận tăng trưởng trung bình năm dương trong giai đoạn 2017–2025, phản ánh cải thiện dần hoạt động tài chính hoặc quy mô hoạt động ngành/doanh nghiệp.
Năm 2023 (91,6 tỷ) là đỉnh vượt trội, có thể do đợt tăng trưởng bùng nổ, chính sách mở rộng hoặc tác động thị trường đặc biệt – nhưng sau đó giảm mạnh xuống 59,7 tỷ (2024) cho thấy sự sụt giảm đột ngột, có thể do thắt chặt tín dụng, suy thoái, hoặc hậu quả phục hồi sau đỉnh.
Năm 2024–2025 phục hồi nhẹ (từ 59,7 lên 64,7 tỷ) nhưng vẫn thấp hơn đường lm dự báo, cảnh báo khả năng tăng trưởng chưa về mức kỳ vọng dài hạn, cần kiểm soát và điều chỉnh chiến lược để duy trì đà hồi phục.
sel_ind <- top_inds[1]
p13data <- dt2 %>%
filter(Indicator == sel_ind) %>%
arrange(Date) %>%
mutate(roll4 = rollmean(Value, 4, fill = NA, align = "right"))
p13 <- ggplot(p13data, aes(x = Date)) +
geom_line(aes(y = Value), color = "gray70") +
geom_line(aes(y = roll4), color = "red", size = 1) +
geom_ribbon(aes(ymin = roll4 * 0.95, ymax = roll4 * 1.05), alpha = 0.2) +
geom_point(aes(y = Value), size = 1.5, color = "darkblue") +
geom_smooth(aes(y = Value), method = "loess", se = FALSE, color = "blue") +
labs(
title = paste0("(P13) Rolling mean (Indicator: ", sel_ind, ")"),
x = "Thời gian",
y = "Giá trị"
) +
theme_style
print(p13)
## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 3 rows containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 3 rows containing missing values or values outside the scale range
## (`geom_ribbon()`).
plots[[i]] <- p13; i <- i + 1
Hàm này vẽ biểu đồ time series của chỉ tiêu đầu tiên trong danh sách top_inds, kết hợp dữ liệu gốc (điểm), trung bình động 4 kỳ (rolling mean), đường LOESS (locally estimated scatter plot smooth) để theo dõi xu hướng dài hạn, loại bỏ nhiễu ngắn hạn, và đánh giá pattern/chu kỳ từng chỉ tiêu qua thời gian. Phục vụ phân tích, dự báo và kiểm soát biến động giá trị tài chính/kinh doanh.
Trong đó:
sel_ind <- top_inds Lấy chỉ tiêu lớn nhất thứ nhất (chỉ tiêu hàng đầu) từ danh sách để phân tích chi tiết.
filter(Indicator == sel_ind) %>% arrange(Date) Lọc dữ liệu của chỉ tiêu đó, sắp xếp theo thời gian tăng dần để tính rolling mean chính xác.
mutate(roll4 = rollmean(Value, 4, fill = NA, align = “right”)) Tính trung bình động 4 kỳ (4-period moving average/MA4): mỗi giá trị là TB của 4 kỳ gần nhất, align = “right” dùng dữ liệu lịch sử cho mỗi điểm (không nhìn tương lai), fill = NA giữ NA cho 3 kỳ đầu không tính được.
geom_line(aes(y = Value), color = “gray70”) Vẽ dữ liệu gốc (thực tế) màu xám nhạt, giúp thấy biến động thô nguyên.
geom_line(aes(y = roll4), color = “red”, size = 1) Vẽ trung bình động màu đỏ dày hơn, làm nổi bật xu hướng mượt sau khi loại bỏ nhiễu.
geom_ribbon(aes(ymin = roll4 * 0.95, ymax = roll4 * 1.05), alpha = 0.2) Dải vùng ±5% quanh MA4, alpha=0.2 trong suốt, giúp thấy phạm vi biến động cho phép xung quanh xu hướng.
geom_point(aes(y = Value), size = 1.5, color = “darkblue”) Đánh dấu từng điểm dữ liệu gốc bằng chấm tròn xanh đậm, dễ nhận diện outlier hoặc anomaly.
geom_smooth(aes(y = Value), method = “loess”, se = FALSE, color = “blue”) Thêm đường LOESS (Local Polynomial Regression Fitting) màu xanh, se = FALSE không vẽ vùng tin cậy, giúp nhận diện xu hướng phi tuyến/mượt mà hơn so với MA đơn giản.
labs(…) + theme_style Tiêu đề động hiển thị tên chỉ tiêu, nhãn trục tiếng Việt, theme chuyên nghiệp.
Kết quả kĩ thuật
Biểu đồ thể hiện dữ liệu time series chỉ tiêu CHI PHÍ BÁN HÀNG từ năm 2018–2024, kết hợp nhiều lớp trực quan: điểm gốc (darkblue), đường thô (gray), MA4 (red), ribbon ±5% (xám nhạt), và LOESS (blue).
Dữ liệu gốc (điểm xanh đậm + đường xám) cho thấy biến động mạnh, có các điểm âm sâu đến -1.5e+11 (~-150 tỷ) vào cuối 2024, phản ánh giai đoạn tăng mạnh chi phí hoặc ghi nhận khoản giảm trừ bất thường.
Đường MA4 (đỏ) và LOESS (xanh) cùng xu hướng giảm dần từ 2018 (~gần 0) xuống khoảng -7e+10 tỷ vào 2024, xác nhận trend giảm trung hạn và dài hạn ổn định, không có điểm đột ngột thay đổi xu hướng (structural break) rõ nét.
Ribbon ±5% quanh MA4 hẹp, nhiều điểm gốc nằm trong dải → biến động tương đối kiểm soát được. Tuy nhiên, một số điểm cuối 2023–2024 rơi xa xuống ngoài ribbon, đặt ra nghi vấn về outlier hoặc sự kiện đặc biệt.
Ý nghĩa thống kê
Chỉ tiêu CHI PHÍ BÁN HÀNG (thường là khoản giảm trừ/chi phí ghi âm trong báo cáo tài chính) có xu hướng giảm đều từ 2018–2024, thể hiện bằng cả MA4 và LOESS, phản ánh khả năng:
Tối ưu hóa vận hành, cắt giảm chi phí bán hàng và marketing hiệu quả hơn.
Hoặc tỷ trọng doanh thu giảm do cạnh tranh/nhu cầu giảm → chi phí tuyệt đối giảm theo.
Điểm cực sâu cuối 2024 (~-150 tỷ) là dấu hiệu cảnh báo: có thể chi phí bán hàng tăng đột biến hoặc là ghi nhận lỗ/điều chỉnh lớn do thay đổi chính sách kế toán, chiến dịch khuyến mãi mạnh, hoặc sự kiện bất thường (kiểm toán, tái cơ cấu).
Dải ribbon và xu hướng LOESS xác nhận xu hướng giảm tổng thể, nhưng biến động mạnh các năm gần đây cho thấy sự không ổn định hơn – cần phân tích sâu nguyên nhân để kiểm soát rủi ro hoặc tối ưu chi phí bán hàng tiếp theo.
Từ góc độ quản trị: đường MA4 giúp nhận diện xu hướng trung hạn rõ ràng, LOESS giúp nhận biết tính phi tuyến, từ đó lập kế hoạch điều chỉnh ngân sách marketing/bán hàng hoặc kiểm tra hiệu quả từng chiến dịch theo thời gian.
p14 <- ggplot(dt2, aes(x=Date, y=Value, color=factor(Outlier))) +
geom_line() + geom_point() + geom_smooth(se=FALSE) +
geom_rug() + scale_color_manual(values=c("gray50","red")) +
labs(title="(P14) Các điểm Outlier theo thời gian", color="Outlier") + theme_style
print(p14); plots[[i]] <- p14; i <- i + 1
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Hàm này vẽ biểu đồ time series của toàn bộ dữ liệu, phân loại từng điểm thành bình thường (gray) hoặc outlier (red), kết hợp line, point, smooth, rug để trực quan hóa các điểm bất thường/anomaly theo thời gian. Nhằm phát hiện nhanh các sự kiện lạ, ghi nhận sai, hoặc biến động bất thường trong dữ liệu tài chính – phục vụ kiểm soát chất lượng dữ liệu, điều tra nguyên nhân, ra quyết định điều chỉnh.
Trong đó:
aes(x=Date, y=Value, color=factor(Outlier)) Trục x là Date, trục y là Value, color được xác định bằng cột Outlier (nhị phân: 0=bình thường, 1=outlier), factor() chuyển sang biến categorical để phân biệt rõ hai nhóm màu.
geom_line() + geom_point() Vẽ đường nối và điểm dữ liệu, với mỗi điểm mang một trong hai màu (gray hoặc red), giúp theo dõi xu hướng tổng thể vừa nhận biết outlier rõ ràng.
geom_smooth(se=FALSE) Thêm đường LOESS mượt (smooth) đi qua dữ liệu, se=FALSE không hiển thị vùng tin cậy, giúp xác định xu hướng trung tâm mà outlier nằm ngoài.
geom_rug() Các vạch nhỏ dưới/trên trục x, mỗi vạch là một điểm dữ liệu, giúp nhận diện mật độ phân bố thời gian của dữ liệu bình thường vs outlier.
scale_color_manual(values=c(“gray50”,“red”)) Gán màu cụ thể: xám cho 0 (bình thường), đỏ cho 1 (outlier), nổi bật sự khác biệt, dễ nhận diện outlier một cái nhìn.
labs(title=…, color=“Outlier”) Tiêu đề rõ ràng, nhãn chú thích (legend) “Outlier” để người đọc biết ý nghĩa hai màu.
Kết quả kĩ thuật
Biểu đồ thể hiện dữ liệu time series toàn bộ dataset từ 2017–2025, với hai màu rõ ràng: gray (0 = bình thường) và red (1 = outlier), kèm đường LOESS smooth màu đỏ/xám trung bình, rug ở phía dưới.
Nhìn vào biểu đồ: phần lớn dữ liệu xám (gray) tập trung quanh vùng giá trị 0 (0e+00), còn các điểm đỏ (outlier) vọt rất cao (+3e+12 ~ +3,000 tỷ) hoặc rất thấp (-2e+12 ~ -2,000 tỷ), phân tán đều qua các năm.
Outliers có tần suất tương đối đều qua các năm, không tập trung đặc biệt một giai đoạn → có thể do đặc thù dữ liệu tài chính có nhiều chỉ tiêu lớn/nhỏ khác biệt, hoặc do quy trình ghi nhận chưa đồng nhất.
Rug dưới trục x thấy rõ độ dày đặc: nhiều dữ liệu gray tập trung quanh 0, nhiều điểm đỏ phân tán rải rác – xác nhận outlier không phải tập trung một chu kỳ cụ thể mà xuất hiện đều xuyên suốt.
Đường smooth (màu gray) nằm gần trục 0, nghĩa là xu hướng chính của dữ liệu ổn định quanh giá trị thấp, còn outliers nằm rất xa, không kéo theo xu hướng tổng thể → cần loại bỏ hoặc điều tra riêng.
Ý nghĩa thống kê
Điểm xám (bình thường) là dữ liệu thường nhật, vận động kinh doanh ổn định, giá trị tập trung quanh vùng 0 – thể hiện nhóm chỉ tiêu nhỏ hoặc không có biến động lớn.
Điểm đỏ (outlier) đại diện những sự kiện bất thường: ghi nhận lỗi, sự kiện đặc biệt (tái cơ cấu, tăng đột biến doanh thu, hoặc ghi nhận lỗ lớn do hạch toán, kiểm toán), hoặc thay đổi quy mô hoạt động mạnh mẽ.
Outliers xuất hiện cả giá trị dương và âm lớn: các điểm vọt cao là có thể doanh thu/lợi nhuận bùng nổ, các điểm âm sâu có thể là chi phí/khoản giảm trừ bất thường hoặc lỗ nặng.
Việc phân tán đều các outlier qua các năm cảnh báo: cần kiểm soát chặt chẽ quy trình nhập liệu, chính sách kế toán, hoặc xác định rõ nguyên nhân từng điểm để tránh phân tích sai lệch khi lập kế hoạch hoặc dự báo.
Đường smooth ổn định gần 0 xác nhận xu hướng trung bình tổng thể không bị ảnh hưởng mạnh bởi outliers – có thể yên tâm dùng dữ liệu gray cho phân tích, còn outliers cần xử lý riêng (loại bỏ hoặc biến đổi) trước khi mô hình hóa.
p15data <- dt2 %>% group_by(Year) %>% summarise(Total=sum(Value,na.rm=TRUE)) %>%
mutate(Cum=cumsum(Total))
p15 <- ggplot(p15data, aes(x=Year, y=Cum)) +
geom_area(fill="lightblue", alpha=0.5) + geom_line(size=1) +
geom_point() + geom_text(aes(label=round(Cum/1e9,1)), vjust=-1.6) +
geom_smooth(method="lm", se=FALSE, color="darkred") +
labs(title="(P15) Tổng tích lũy qua các năm") + theme_style +
scale_y_continuous(expand = expansion(mult = c(0, 0.15)))
print(p15); plots[[i]] <- p15; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
Hàm này vẽ biểu đồ diện tích tích lũy (area chart) theo năm, kết hợp line, point, số liệu label, đường hồi quy tuyến tính, với không gian trục Y được tối ưu để hiển thị đầy đủ tất cả con số từ cao nhất đến thấp nhất. Nhằm theo dõi tổng giá trị tích lũy doanh số/tài chính từ đầu giai đoạn, xác định xu hướng tăng trưởng dài hạn, từ đó đánh giá hiệu suất tích lũy, dự báo tổng tích lũy tiếp theo một cách trực quan.
Trong đó:
group_by(Year) %>% summarise(Total=sum(Value,na.rm=TRUE)) %>% mutate(Cum=cumsum(Total)) Gom nhóm theo năm, tính tổng Value từng năm, rồi tính cumsum (tích lũy toàn bộ từ đầu) → kết quả: mỗi năm có giá trị tích lũy liên tiếp tăng.
geom_area(fill=“lightblue”, alpha=0.5) Vẽ diện tích dưới đường, màu xanh nhạt trong suốt (alpha=0.5), tạo hiệu ứng visual, dễ thấy vùng tích lũy, khác với bar chart.
geom_line(size=1) + geom_point() Vẽ đường liên kết từng năm, dẻo dai hơn, kèm điểm marker để nhận diện rõ từng năm.
geom_text(aes(label=round(Cum/1e9,1)), vjust=-1.6) Hiển thị giá trị tích lũy (đơn vị tỷ, 1 số thập phân) trên từng điểm, vjust=-1.6 đẩy text lên cao tránh chồng lên đường, giúp đọc rõ ràng.
geom_smooth(method=“lm”, se=FALSE, color=“darkred”) Thêm đường hồi quy tuyến tính (LM regression) màu đỏ đậm, se=FALSE không vẽ vùng tin cậy, giúp xác định xu hướng dự báo tích lũy dài hạn.
scale_y_continuous(expand = expansion(mult = c(0, 0.15))) Tối ưu không gian trục Y: thêm 15% khoảng trống phía trên (mult = c(0, 0.15) → không thêm dưới, thêm 15% trên), đảm bảo con số cao nhất không bị cắt mất ngoài khung.
theme_style Theme chuyên nghiệp, thẩm mỹ đồng bộ báo cáo.
Kết quả kĩ thuật
Biểu đồ area chart kết hợp diện tích xanh nhạt (phần tích lũy), đường line đen, điểm marker trắng, đường hồi quy LM màu đỏ, trực quan hoá giá trị tích lũy cumsum từ 2017–2025.
Dữ liệu thực tế (đường line đen): tích lũy tăng từ 6,029.6 tỷ (2017) → 13,951.4 tỷ (2018) → 21,350.6 tỷ (2019) → … → 69,523.5 tỷ (2025) – xu hướng tăng ổn định, không có điểm bất thường rõ rệt.
Đường hồi quy LM (màu đỏ) gần sát đường thực tế, xác nhận tăng trưởng tuyến tính, không có biến động phi tuyến – tốc độ tăng tích lũy bình quân hằng năm ổn định.
Tất cả con số được hiển thị rõ ràng: từ 6,029.6 (2017) đến 69,523.5 (2025), vjust=-1.6 + scale_y_continuous(expand=0.15) đảm bảo không bị cắt mất, dễ đọc.
Diện tích xanh dưới đường tạo cảm giác tích lũy “kho tiền/tài sản” tăng đều đặn, trực quan và dễ hiểu.
Ý nghĩa thống kê
Tích lũy tăng gần 11.5 lần từ 2017 (6,030 tỷ) đến 2025 (69,524 tỷ): chứng tỏ doanh nghiệp/ngành tăng trưởng vô cùng mạnh mẽ, tích lũy doanh số hoặc lợi nhuận hàng năm không ngừng, đây là tín hiệu vô cùng tích cực cho nhà đầu tư, cổ đông, và quản lý.
Đường LM sát đường thực tế: tăng trưởng rất đều đặn, dự báo tính cao, không bị ảnh hưởng bởi giai đoạn đặc biệt – nhà quản lý có thể yên tâm dự báo, lập kế hoạch dài hạn dựa trên tốc độ tăng tích lũy này
p16 <- ggplot(dt2 %>% filter(Quarter %in% c("Q1","Q2","Q3","Q4")),
aes(x=Year, y=Value, fill=Quarter)) +
geom_col() + geom_line(aes(group=Quarter), color="black") + geom_point() +
geom_smooth(method="loess", se=FALSE, linetype="dashed") +
facet_wrap(~Quarter, scales="free_y") +
labs(title="(P16) Value theo Quarter & Year") + theme_style
print(p16); plots[[i]] <- p16; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
Hàm này vẽ biểu đồ so sánh giá trị từng quý qua các năm, chia thành 4 panel (Q1-Q4), mỗi panel hiển thị bar + line + point + LOESS smooth để phát hiện mùa vụ, chu kỳ quý, xác định quý nào mạnh/yếu, và theo dõi xu hướng từng quý riêng biệt. Phục vụ phân tích kinh doanh mùa vụ, lập kế hoạch bán hàng, tối ưu hoạt động theo chu kỳ quý, rất quan trọng cho doanh nghiệp có tính chất mùa vụ mạnh..
Trong đó:
filter(Quarter %in% c(“Q1”,“Q2”,“Q3”,“Q4”)) Lọc chỉ giữ 4 quý chính (Q1-Q4), loại bỏ các quý lẻ hoặc dữ liệu không đầy đủ.
aes(x=Year, y=Value, fill=Quarter) Trục x là năm, trục y là giá trị, fill màu theo quý – mỗi quý một màu riêng để phân biệt rõ ràng.
geom_col() Vẽ cột (bar chart) giá trị từng quý hàng năm, hiệu quả để so sánh giá trị trực tiếp, nhận diện quý cao/thấp nhanh chóng.
geom_line(aes(group=Quarter), color=“black”) Vẽ đường nối giá trị của từng quý qua các năm, group=Quarter đảm bảo line chỉ nối những điểm cùng quý (Q1 nối Q1, Q2 nối Q2…), màu đen nổi bật xu hướng.
geom_point() Đánh dấu từng điểm dữ liệu, giúp nhận diện outlier, giá trị cụ thể từng năm.
geom_smooth(method=“loess”, se=FALSE, linetype=“dashed”) Đường LOESS mượt (phi tuyến) cho từng quý, linetype=“dashed” (nét đứt) để phân biệt với line chính, xác định xu hướng thực từng quý – quý nào tăng, quý nào giảm, quý nào đảo chiều.
facet_wrap(~Quarter, scales=“free_y”) Chia thành 4 panel riêng (mỗi quý một panel), scales=“free_y” cho phép trục Y độc lập từng panel – rất quan trọng để so sánh chi tiết từng quý mà không bị lệch trục (nếu Q1 nhỏ, Q4 lớn, free_y giúp cả hai đều rõ).
theme_style Theme chuyên nghiệp, thẩm mỹ đồng bộ.
Kết quả kĩ thuật
Biểu đồ chia thành 4 panel (Q1, Q2, Q3, Q4), mỗi panel có cột (bar), đường line, điểm (point), và đường LOESS (nét đứt) màu đen.
Q1 (đỏ): Bar tăng từ ~1 tỷ (2017) lên ~2 tỷ (2025), LOESS tăng ổn định, line nối các điểm khá mịn, không có bất thường rõ.
Q2 (xanh lá): Bar cũng tăng từ ~1 tỷ → ~2 tỷ, LOESS tương tự Q1, tăng đều đặn, nhưng giá trị trung bình hơi cao hơn Q1.
Q3 (xanh lam): Bar tăng từ ~1 tỷ → ~2.5 tỷ, LOESS cao nhất trong 4 quý, xác nhận Q3 là quý có giá trị cao hơn các quý khác, tăng trưởng mạnh nhất.
Q4 (tím): Bar thấp hơn các quý, tăng từ ~0.5 tỷ → ~1.5 tỷ, LOESS nằm thấp nhất, xác nhận Q4 là quý yếu nhất.
Trục Y từng panel khác nhau (scales=“free_y”) giúp chi tiết từng quý nổi bật, không bị lệch trục.
Ý nghĩa thống kê
Phân tích mùa vụ rõ ràng:
Q3 mạnh nhất (~2.5 tỷ năm 2025): có thể do nhu cầu cao kỳ này (ví dụ: mùa bán hàng, sự kiện), công ty cần tập trung nhân lực, hàng tồn kho, quảng cáo vào Q3.
Q4 yếu nhất (~1.5 tỷ năm 2025): có thể do chu kỳ kinh doanh hoặc thị trường giảm, công ty nên chuẩn bị kế hoạch khuyến mãi, chiếc chiến lược bán hàng để tăng Q4.
Q1, Q2 trung bình (~2 tỷ): ổn định, có thể là nền tảng kinh doanh thường ngày.
Tăng trưởng dài hạn ổn định: Cả 4 quý đều tăng từ 2017-2025 (không giảm), LOESS đều có dốc tăng → doanh số/lợi nhuận bền vững, tiếp tục tăng, không có giai đoạn thoái thối.
Chiến lược quản lý quý:
Q3: Tối đa hóa doanh số, chuẩn bị sản phẩm, hàng tồn kho cao → tăng revenue cao nhất năm.
Q4: Kích cầu bằng khuyến mãi, sự kiện bán hàng (đợt cuối năm) để bù đắp Q4 yếu, cân bằng doanh số hàng năm.
Q1-Q2: Duy trì ổn định, chuẩn bị cho Q3 bùng nổ → lập kế hoạch hợp lý.
p17data <- dt2 %>%
group_by(Indicator) %>%
summarise(meanV = mean(Value, na.rm = TRUE)) %>%
arrange(desc(abs(meanV))) %>%
slice(1:12) # Chỉ lấy 12 indicator top
p17 <- ggplot(p17data, aes(x = reorder(Indicator, meanV), y = meanV, fill = Indicator)) +
geom_col(show.legend = FALSE) +
coord_polar() +
geom_text(aes(label = round(meanV/1e9, 1)),
hjust = 0.5, vjust = -0.3, size = 3) +
geom_hline(yintercept = 0, color = "gray40", linetype = "dashed") +
labs(title = "(P17) Top 12 Indicator - Phân tích 360°",
x = "",
y = "Giá trị trung bình") +
theme_style +
theme(axis.text.x = element_text(size = 8))
print(p17); plots[[i]] <- p17; i <- i + 1
Hàm này vẽ biểu đồ cực (polar chart) của top 12 chỉ tiêu (indicator) có giá trị trung bình lớn nhất (tính theo giá trị tuyệt đối), giúp nhìn toàn cảnh so sánh 360° những chỉ tiêu quan trọng nhất một cách trực quan, dễ nhận diện cân bằng/mất cân bằng giữa các chỉ tiêu. Phục vụ phân tích kết cấu tài chính, nhận diện chỉ tiêu chủ chốt, lên kế hoạch tối ưu cho từng mảng doanh nghiệp.
Trong đó:
group_by(Indicator) %>% summarise(meanV = mean(Value, na.rm = TRUE)) Gom nhóm từng chỉ tiêu, tính trung bình Value loại bỏ NA.
arrange(desc(abs(meanV))) %>% slice(1:12) Sắp xếp giảm dần theo giá trị tuyệt đối (abs) để lấy 12 indicator có độ lớn trung bình cao nhất → loại bỏ những chỉ tiêu nhỏ, tránh biểu đồ quá rối.
aes(x = reorder(Indicator, meanV), y = meanV, fill = Indicator)
reorder(Indicator, meanV): sắp xếp lại thứ tự indicator theo giá trị meanV, giúp biểu đồ cực nhìn được sự xếp hạng trực tiếp.
fill = Indicator: mỗi indicator một màu riêng, dễ phân biệt.
geom_col(show.legend = FALSE) Vẽ cột dạng hình quạt từ tâm ra, show.legend = FALSE ẩn chú thích (vì label chỉ tiêu đã ở trục x).
coord_polar() Chuyển toạ độ Descartes sang cực (polar): trục x quay thành góc, trục y thành bán kính → tạo hình tròn, mỗi cột là một lát tương ứng indicator.
geom_text(aes(label = round(meanV/1e9, 1)), hjust = 0.5, vjust = -0.3, size = 3) Ghi số liệu trung bình (đơn vị tỷ, 1 số thập phân) trên mỗi cột, hjust=0.5 (ngang giữa), vjust=-0.3 (hơi cao lên), size=3 nhỏ nhưng đủ đọc.
geom_hline(yintercept = 0, color = “gray40”, linetype = “dashed”) Đường ngang tại y=0 (nét đứt xám), giúp phân biệt giá trị dương (trên) và âm (dưới) – quan trọng nếu có chỉ tiêu âm (khoản giảm trừ).
theme_style + theme(axis.text.x = element_text(size = 8)) Theme chuyên nghiệp, chữ indicator nhỏ (size=8) để tránh chồng chéo trong biểu đồ cực.
Kết quả kĩ thuật
Biểu đồ cực (polar chart) hiển thị 12 chỉ tiêu top lớn nhất, sắp xếp theo hình tròn, mỗi chỉ tiêu một lát (màu khác nhau), khoảng cách từ tâm = giá trị trung bình (tỷ).
Nhóm chỉ tiêu dương lớn (bên trên):
HUỈ BÁN HÀNG: ~37,600 tỷ (xanh lá, dài nhất, vọt cao)
CHI PHÍ BÁN HÀNG: ~25,800 tỷ (xanh lam)
CHI PHÍ QUẢN LÝ DOANH NGHIỆP: ~25,500 tỷ (xanh dương) → Xác nhận 3 chỉ tiêu này chiếm trên 80% giá trị trung bình tổng thể.
Nhóm chỉ tiêu âm (dưới, hướng nội):
LỢI NHUẬN ĐÃ CÓ (~-53.4 tỷ) chỉ tiêu âm lớn nhất
TIỀN ANT THUẾ (~-25.6 tỷ), LỢI NHUẬN 12 THÁNG (~-23.9 tỷ) cũng âm → Phản ánh doanh số/chi phí giảm trừ mạnh, không phải là lợi nhuận ròng dương.
Sự mất cân bằng 360° rõ ràng: 3 chỉ tiêu dương chiếm gần hết nửa trên, phần dưới toàn chỉ tiêu âm/nhỏ → cảnh báo kết cấu tài chính chưa cân bằng, chỉ tiêu dương tập trung ở ít mảng, rủi ro cao nếu một chỉ tiêu lớn biến động.
Ý nghĩa thống kê
Ba chỉ tiêu quyết định 80%:
Doanh thu bán hàng (~37.6K tỷ): Đây là thu nhập chính, hoạt động bình thường.
Chi phí bán hàng + Quản lý (~51.3K tỷ): Chi phí gần bằng doanh thu → lợi nhuận gộp rất thấp, cảnh báo hiệu suất bán hàng kém, chi phí vận hành quá cao so với doanh số.
Chỉ tiêu âm lớn (-53.4K tỷ “Lợi nhuận đã có”): Có thể là:
Khoản giảm trừ, điều chỉnh kiểm toán, hoặc ghi nhận lỗ lớn từ các năm trước.
Hoặc là “khoản giảm” trong hạch toán (ví dụ: mục khác trong báo cáo tài chính).
Dấu hiệu cảnh báo: Cần kiểm toán/xác nhận kỹ nguyên nhân.
Kết cấu tài chính bất cân:
Doanh thu ~37.6K, nhưng lợi nhuận âm → lợi nhuận ròng tiêu cực, công ty không sinh lợi hoặc lỗ nặng.
Nếu tính lợi nhuận dương (không tính chỉ tiêu âm), công ty vẫn có lỗ hoặc lợi nhuận rất nhỏ.
Hành động cần thiết:
Tối ưu hóa chi phí: Cắt giảm chi phí bán hàng/quản lý để tăng lợi nhuận gộp.
Tăng doanh thu: Mở rộng bán hàng, thị trường mới, để tăng doanh số từ 37.6K.
Kiểm soát chỉ tiêu âm lớn: Xác định chính xác nguyên nhân, giải quyết nhanh để ngăn tái phát.
Tái cơ cấu hoạt động: Có thể có mảng hoạt động lỗ, cần đánh giá, loại bỏ hoặc cải thiện.
p18 <- ggplot(dt2, aes(x=YoY, y=QoQ, size=abs(Value), color=ValueClass)) +
geom_point(alpha=0.4) + geom_smooth(se=FALSE, color="black") +
geom_hline(yintercept=0, linetype="dashed") + geom_vline(xintercept=0, linetype="dotted") +
scale_size(range=c(1,6)) +
labs(title="(P18) Bubble plot: YoY vs QoQ") + theme_style
print(p18); plots[[i]] <- p18; i <- i + 1
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
## Warning: The following aesthetics were dropped during statistical transformation: size.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
## the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
## variable into a factor?
Hàm này vẽ bubble plot 3 chiều so sánh tăng trưởng năm-trên-năm (YoY) vs quý-trên-quý (QoQ), với kích thước bubble = giá trị tuyệt đối Value, màu = loại giá trị (ValueClass), nhằm phát hiện bất thường, phân loại tăng trưởng theo mùa, xác định chiếu tiêu tăng trưởng nhanh/chậm, ổn định/biến động, từ đó lập kế hoạch tăng trưởng phù hợp. Rất hữu ích cho kiểm soát chất lượng tăng trưởng, dự báo, tối ưu hóa hoạt động.
Trong đó:
aes(x=YoY, y=QoQ, size=abs(Value), color=ValueClass)
x=YoY: Trục x = tăng trưởng năm-trên-năm (long-term growth)
y=QoQ: Trục y = tăng trưởng quý-trên-quý (short-term growth)
size=abs(Value): Kích thước bubble = giá trị tuyệt đối Value (bubble lớn = giá trị cao, quan trọng)
color=ValueClass: Màu = loại giá trị (dương/âm, hoặc phân loại khác) → nhận diện nhanh loại dữ liệu
geom_point(alpha=0.4) Vẽ bubble, alpha=0.4 trong suốt → nhìn được bubble chồng lên nhau, xác định mật độ cluster (tập trung/phân tán).
geom_smooth(se=FALSE, color=“black”) Thêm đường xu hướng mượt (LOESS), se=FALSE không vẽ vùng tin cậy, màu đen xác định xu hướng chung YoY vs QoQ → liệu tăng trưởng năm và quý có tương quan tuyến tính hay không.
geom_hline(yintercept=0, linetype=“dashed”) + geom_vline(xintercept=0, linetype=“dotted”) Vẽ hai đường gốc:
Đường ngang (dashed) tại y=0: Phân tách QoQ dương (trên) vs âm (dưới)
Đường dọc (dotted) tại x=0: Phân tách YoY dương (phải) vs âm (trái) → Tạo 4 vùng quadrant: (++), (+-), (-+), (–) → nhận diện nhanh pattern tăng trưởng.
scale_size(range=c(1,6)) Chuẩn hóa kích thước bubble từ 1-6 → tránh bubble quá nhỏ (vô hình) hay quá lớn (chồng chéo), dễ quan sát.
theme_style Theme chuyên nghiệp, thẩm mỹ báo cáo.
Kết quả kĩ thuật
Biểu đồ bubble plot với trục X = YoY (từ ~2e+08 đến ~2.8e+08), trục Y = QoQ (từ ~0 đến ~1.5e+08), đường LOESS mượt (đỏ đen) có hình dạng dome/qua hoà, xác nhận mối quan hệ phi tuyến giữa YoY và QoQ.
Phân布 dữ liệu: Phần lớn bubble tập trung ở vùng giữa (YoY ~1e+08 đến ~1.5e+08, QoQ ~0.5e+08 đến ~1.5e+08), xác nhận dữ liệu tập trung, không phân tán rộng.
Đường LOESS: Từ trái sang phải, QoQ tăng dần (từ 0 → pico ~1.5e+08 ở giữa), sau đó giảm dần về gần 0 → chỉ ra quan hệ “lên-xuống”: YoY tăng → QoQ tăng, nhưng rồi YoY tăng quá cao → QoQ lại giảm.
Màu ValueClass (High, Low, Medium, Very High): Phân loại bubble theo giá trị, các màu phân tán khắp biểu đồ (không tập trung một vùng) → không có mối liên hệ rõ giữa ValueClass và YoY/QoQ.
Kích thước bubble (abs(Value)): Scale từ 0-1e+12, bubble nhỏ tập trung, bubble lớn ít → dữ liệu phần lớn giá trị nhỏ/trung bình, ít dữ liệu giá trị rất lớn (1e+12)
Ý nghĩa thống kê
Biểu đồ bubble plot với trục X = YoY (từ ~2e+08 đến ~2.8e+08), trục Y = QoQ (từ ~0 đến ~1.5e+08), đường LOESS mượt (đỏ đen) có hình dạng dome/qua hoà, xác nhận mối quan hệ phi tuyến giữa YoY và QoQ.
Phân布 dữ liệu: Phần lớn bubble tập trung ở vùng giữa (YoY ~1e+08 đến ~1.5e+08, QoQ ~0.5e+08 đến ~1.5e+08), xác nhận dữ liệu tập trung, không phân tán rộng.
Đường LOESS: Từ trái sang phải, QoQ tăng dần (từ 0 → pico ~1.5e+08 ở giữa), sau đó giảm dần về gần 0 → chỉ ra quan hệ “lên-xuống”: YoY tăng → QoQ tăng, nhưng rồi YoY tăng quá cao → QoQ lại giảm.
Màu ValueClass (High, Low, Medium, Very High): Phân loại bubble theo giá trị, các màu phân tán khắp biểu đồ (không tập trung một vùng) → không có mối liên hệ rõ giữa ValueClass và YoY/QoQ.
Kích thước bubble (abs(Value)): Scale từ 0-1e+12, bubble nhỏ tập trung, bubble lớn ít → dữ liệu phần lớn giá trị nhỏ/trung bình, ít dữ liệu giá trị rất lớn (1e+12)
p19 <- ggplot(dt2, aes(x=Value_z, y=Value)) +
geom_point(alpha=0.4, color="purple") +
geom_smooth(method="lm", se=TRUE, color="red") +
geom_text_repel(data=dt2 %>% slice_max(abs(Value_z), n=5),
aes(label=Indicator), size=3,
box.padding = 0.5, point.padding = 0.5,
nudge_y = 1e11) + # Đẩy text lên một tí
geom_rug(aes(y=NULL)) + # Chỉ giữ rug trên X (bỏ rug trên Y)
geom_hline(yintercept=0, color="gray40") +
labs(title="(P19) Chuẩn hóa giá trị - Phát hiện Outlier (Value_z)",
x = "Value_z (Z-Score)",
y = "Value (Giá trị gốc)") +
theme_style
print(p19)
## `geom_smooth()` using formula = 'y ~ x'
plots[[i]] <- p19; i <- i + 1
Hàm này vẽ scatter plot so sánh giá trị gốc (Value) với giá trị chuẩn hóa Z-Score (Value_z), kèm đường hồi quy tuyến tính (LM) với vùng tin cậy, để phát hiện outlier (5 chỉ tiêu ngoại lệ lớn nhất được đánh nhãn), xác định mối liên hệ tuyến tính giữa hai, từ đó kiểm soát chất lượng dữ liệu, loại bỏ/điều chỉnh outlier trước khi mô hình hóa, dự báo, hay ra quyết định kinh tế.
Trong đó :
aes(x=Value_z, y=Value)
x=Value_z: Trục X = Z-Score chuẩn hóa,(trung bình = 0, độ lệch chuẩn = 1)
y=Value: Trục Y = giá trị gốc → so sánh giữa gốc và chuẩn hóa để xác định outlier có Value tuyệt đối lớn.
geom_point(alpha=0.4, color=“purple”) Vẽ điểm dữ liệu màu tím, alpha=0.4 trong suốt → nhìn được mật độ, tập trung/phân tán, xác định outlier rõ.
geom_smooth(method=“lm”, se=TRUE, color=“red”)
method=“lm”: Hồi quy tuyến tính (linear regression) → xác định xu hướng chung Value vs Value_z
se=TRUE: Vẽ vùng tin cậy (95% confidence band) → thể hiện mức độ chắc chắn của dự báo, nếu dải hẹp = chặt chẽ, dải rộng = không chắc
color=“red”: Màu đỏ để phân biệt với điểm.
geom_text_repel(data=dt2%>% slice_max(abs(Value_z), n=5), aes(label=Indicator), …)
slice_max(abs(Value_z), n=5): Lấy 5 dòng có Z-Score tuyệt đối lớn nhất → 5 outlier ngoại lệ nhất
geom_text_repel: Hiển thị nhãn Indicator, auto-adjust vị trí để tránh chồng chéo (repel = đẩy ra)
box.padding=0.5, point.padding=0.5: Khoảng cách padding xung quanh text, tránh che mất điểm
nudge_y=1e11: Đẩy text lên trên 100 tỷ để khỏi che mất rug ở dưới.
geom_rug(aes(y=NULL)) Vạch nhỏ dưới trục X biểu hiện mật độ Z-Score phân bố, aes(y=NULL) bỏ rug trên trục Y trái (tránh che nhãn), chỉ giữ rug trên X.
geom_hline(yintercept=0, color=“gray40”) Đường ngang tại y=0 (xám), giúp phân tách Value dương (trên) vs âm (dưới) → nhận diện chỉ tiêu dương/âm nhanh.
theme_style Theme chuyên nghiệp, thẩm mỹ báo cáo
Kết quả kĩ thuật
Biểu đồ scatter plot kết hợp trục X = Value_z (Z-Score từ ~-5 đến ~5), trục Y = Value gốc (từ ~-2e+12 đến ~3e+12), với đường hồi quy LM (màu đỏ) + vùng tin cậy 95%, hiển thị mối liên hệ tuyến tính giữa chuẩn hóa và gốc.
5 outlier được đánh nhãn rõ ràng:
Doanh thu hoạt động tài chính (Z ≈ +3.5, Value ≈ +2.8e+12): outlier dương lớn nhất, vượt xa vùng tin cậy.
Các khoản giảm trừ doanh thu (Z ≈ -3 đến -4, Value ≈ -0.5 đến -1.5e+12): outlier âm, nằm ngoài dải LM.
Lợi nhuận có được / Khoản giảm từ công ty (Z ≈ +2.5 đến +3): outlier dương vừa.
Phần lớn dữ liệu (tím) tập trung quanh Z ≈ -1 đến +1, Value ≈ -0.5e+12 đến +1e+12, nằm chặt chẽ trong vùng tin cậy đỏ → dữ liệu tuân theo phân bố chuẩn tốt ở phần trung tâm.
Đường LM (đỏ) gần như nằm gần trục y=0 → xác nhận giá trị gốc trung bình ≈ 0 (dữ liệu cân bằng dương/âm).
Rug dưới trục X: Mật độ vạch tập trung ở Z ≈ 0 (giữa) → phần lớn Z-Score xung quanh 0, dữ liệu tập trung gần trung bình.
Ý nghĩa thống kê
Doanh thu hoạt động tài chính (+2.8e+12, Z=+3.5): Giá trị lệch 3.5 sigma từ trung bình, có thể là:
Khoản lợi nhuận từ hoạt động tài chính ngoài dự kiến (thanh lý tài sản, đầu tư khoản mục, v.v.).
Cần xác nhận tính hợp lệ, tránh kế toán sai hoặc thao tác lợi nhuận.
Các khoản giảm trừ âm (-1.5e+12 đến -0.5e+12, Z=-3 đến -4): Lệch -3 đến -4 sigma → chỉ tiêu chi phí/khoản lỗ bất thường:
Giảm giá hàng tồn kho, khấu hao đột biến, hoặc giảm trừ từ kiểm toán.
Cảnh báo cần kiểm soát chi phí, hoặc xác định năm/giai đoạn có sự kiện bất thường.
Phần lớn chỉ tiêu tuân theo mô hình tuyến tính ổn định → dữ liệu sạch, đáng tin cậy cho hầu hết phân tích.
Có thể dùng trực tiếp cho dự báo, hồi quy, hay mô hình ML mà không cần lo rủi ro outlier.
p20 <- ggplot(dt2 %>% filter(Indicator %in% top_inds),
aes(x=Date, y=Value, color=Indicator)) +
geom_line(size=1) + geom_point(size=1) +
geom_smooth(se=FALSE, linetype="dotted") +
geom_ribbon(aes(ymin=Value*0.9, ymax=Value*1.1, fill=Indicator), alpha=0.05) +
facet_wrap(~Indicator, scales="free_y", nrow=3, ncol=3) +
labs(title="(P20) Xu hướng chi tiết Top Indicator") +
theme_style +
theme(
plot.title = element_text(size=10, face="bold"),
strip.text = element_text(size=5, face="bold"),
legend.text = element_text(size=4.8),
legend.title = element_text(size=6),
legend.position = "bottom",
legend.box = "horizontal",
axis.title = element_text(size=8),
axis.text = element_text(size=6)
)
print(p20)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
ggsave("p20_final.png", width=14, height=10, dpi=300)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
plots[[i]] <- p20; i <- i + 1
Hàm này vẽ facet plot (9 panel) hiển thị từng top indicator riêng biệt qua dòng thời gian (Date), kèm line + point + LOESS smooth + ribbon ±10%, nhằm phát hiện xu hướng, biến động, anomaly từng chỉ tiêu, xác định mô hình tăng/giảm, mùa vụ, điểm đột biến của từng indicator. Sau đó in file PNG độ phân giải cao (300 dpi, 14x10 inch) để báo cáo chuyên nghiệp, dễ in.
Trong đó:
Phần dữ liệu & aesthetics:
dt2%>% filter(Indicator %in% top_inds) Lọc chỉ những indicator trong danh sách top_inds (những chỉ tiêu quan trọng nhất).
aes(x=Date, y=Value, color=Indicator)
Trục X = Ngày (Date) → theo dõi xu hướng dài hạn
Trục Y = Giá trị (Value)
Màu = Indicator → mỗi indicator một màu (dùng chú thích phân biệt)
Phần geom (hình vẽ):
geom_line(size=1) + geom_point(size=1) Đường nối các điểm, size=1 vừa đủ → dễ theo dõi xu hướng hàng ngày/tuần.
geom_smooth(se=FALSE, linetype=“dotted”) Đường mượt LOESS (phi tuyến) → xác định xu hướng thực tế vượt qua nhiễu tạm thời, linetype=“dotted” (nét đứt) để phân biệt với line chính.
geom_ribbon(aes(ymin=Value0.9, ymax=Value1.1, fill=Indicator), alpha=0.05) Vùng xám xung quanh line = ±10% từ giá trị, giúp phát hiện outlier nằm ngoài vùng bình thường → cảnh báo anomaly cần điều tra.
Phần facet & layout:
facet_wrap(~Indicator, scales=“free_y”, nrow=3, ncol=3)
~Indicator: Chia thành 9 panel (nrow=3, ncol=3), mỗi panel = 1 indicator
scales=“free_y”: Trục Y độc lập từng panel → từng indicator có scale riêng, dễ nhìn chi tiết
nrow=3, ncol=3: Bố cục 3 hàng × 3 cột = 9 panel gọn gàng
Phần theme (thẩm mỹ):
plot.title = element_text(size=10, face=“bold”) Tiêu đề: size=10 (vừa phải), bold (nổi bật).
strip.text = element_text(size=5, face=“bold”) Tên indicator ở top mỗi panel: size=5 (bé), bold (dễ đọc) → tiết kiệm không gian nhưng vẫn rõ.
legend.text = element_text(size=4.8), legend.title = element_text(size=6) Chú thích bé để không che biểu đồ (size=4.8 rất bé, nhưng dùng khi facet nhiều).
legend.position = “bottom”, legend.box = “horizontal” Chú thích ở dưới, sắp ngang → tiết kiệm chiều rộng.
axis.title = element_text(size=8), axis.text = element_text(size=6) Trục: title (tên) size=8, text (số) size=6 → nhỏ gọn.
Phần export:
ggsave(“p20_final.png”, width=14, height=10, dpi=300)
width=14, height=10: In PNG kích thước 14×10 inch (rất lớn) → tên indicator đầy đủ không cắt, chữ vẫn đủ lớn đọc
dpi=300: Độ phân giải 300 dpi (in ấn chuyên nghiệp, báo cáo chất lượng cao)
File lưu dưới tên “p20_final.png” để dùng trong báo cáo/trình chiếu
plots[[i]] <- p20; i <- i + 1 Lưu biểu đồ vào danh sách plots[], tăng bộ đếm i để theo dõi số lượng plot.
Kết quả kĩ thuật
Biểu đồ facet 3×3 (9 panel), mỗi panel = 1 indicator top, trục X = Date (2018-2024), trục Y = Value (scale tự do từng panel), gồm line (nét liền đen), point (dấu tròn), LOESS mượt (nét đứt), ribbon ±10% (xám nhạt).
9 chỉ tiêu quan trọng:
Các khoản quản lý rủi ro doanh thu (đỏ): Giá trị ~2.5 tỷ, có 1 spike đột ngột ~2.5 tỷ (2020) → sự kiện bất thường
Chi phí bán hàng (vàng): Giá trị quanh -5 đến -1.5e+11, xu hướng giảm dần 2018-2024
Chi phí khác (xanh lá): Giá trị ~0 đến -3e+10, ổn định, ít biến động
Chi phí lãi vay (xanh ngọc): Giá trị ~0 đến -1e+11, ổn định với dips nhẹ
Chi phí quản lý doanh nghiệp (xanh lam): Giá trị ~-5e+10 đến -2e+11, xu hướng giảm mạnh
Chi phí tài chính (xanh dương): Giá trị ~0 đến -2.5e+11, vọt dốc sâu 2021-2022, sau đó hồi phục
Chi phí thuế và nhập doanh nghiệp (tím): Giá trị ~-2.5e+11 đến -5e+11, khá ổn định
Doanh thu bán hàng và cung cấp dịch vụ (hồng): Giá trị ~1 đến 3e+12, xu hướng tăng rõ, mô hình mùa vụ rõ ràng (dip-peak định kỳ)
Indicator khác (bên phải hàng dưới): Hiển thị chuyên biệt
Ribbon ±10%: Hầu hết indicator nằm trong vùng ribbon → dữ liệu ổn định kiểm soát tốt, ít anomaly. Riêng “Doanh thu bán hàng” có vài điểm vọt ra ngoài ribbon → cảnh báo tháng bất thường cần điều tra.
LOESS mượt (nét đứt đen): Xác nhận xu hướng chủ yếu: doanh thu tăng, chi phí giảm/ổn định → lợi nhuận tiềm năng tốt.
Ý nghĩa thống kê
Nhóm Doanh thu (hồng, panel dưới phải):
Doanh thu bán hàng ~1-3e+12: Xu hướng tăng ổn định 2018-2024 → công ty hoạt động khỏe, thị trường mở rộng
Mô hình mùa vụ rõ ràng: Dip-peak định kỳ (khác nhau mỗi năm) → công ty có chu kỳ bán hàng rõ, cần lập kế hoạch tồn kho, nhân lực, vốn theo mùa vụ
Nhóm Chi phí (vàng, xanh, tím):
Chi phí bán hàng (~-1.5 đến -5e+11): Xu hướng giảm dần 2018-2024 → tối ưu hóa chi phí thành công, nên tiếp tục chiến lược này
Chi phí quản lý (~-5 đến -2e+11): Giảm mạnh 2020-2024 → cắt giảm chi phí overhead hiệu quả
Chi phí tài chính (~-2.5 đến 0): Vọt dốc 2021-2022 (-2.5e+11), sau đó hồi phục → có sự kiện tài chính bất thường (vay lớn? lãi suất tăng?) cần xem xét lại chiến lược nợ
Kết cấu tài chính chung:
Doanh thu tăng + Chi phí giảm → Lợi nhuận gộp/ròng cải thiện mạnh (khó nhìn trực tiếp nhưng xu hướng tích cực)
Ribbon ±10% hợp lệ tốt → công ty kiểm soát tốt, bất ngờ ít, dự báo tương đối chính xác
Cảnh báo & Hành động:
Doanh thu vụt ngoài ribbon: Điều tra những tháng bất thường → là đột phá (mở chi nhánh, sản phẩm mới) hay sai sót
Chi phí tài chính vọt 2021-2022: Xem xét tái cấu trúc nợ, kiểm soát lãi suất để tránh lặp lại
Duy trì giảm chi phí: Chiến lược cắt giảm chi phí bán hàng/quản lý đang đúng hướng, nên tiếp tục tối ưu hóa quy trình, tự động hóa, renegotiate supplier
Tận dụng mùa vụ: Doanh thu có chu kỳ mùa vụ → lập kế hoạch bán hàng theo quý, tập trung resources vào quý mạnh, chuẩn bị promotion/khuyến mãi cho quý yếu.