Tôi xin trân trọng bày tỏ lòng biết ơn sâu sắc đến Thầy Trần Mạnh Tường, người đã trực tiếp hướng dẫn tôi hoàn thành bài tiểu luận này.
Sự chỉ dẫn tận tâm, cùng những góp ý chuyên sâu và kịp thời của thầy đã giúp tôi định hướng rõ ràng nội dung nghiên cứu, nắm vững phương pháp luận khoa học. Quá trình làm việc với thầy là cơ hội quý giá để tôi rèn luyện tư duy phân tích và củng cố kiến thức chuyên ngành.
Để hoàn thiện bài báo cáo, tôi đã luôn nỗ lực, song hành cùng sự quan tâm và chia sẻ từ thầy. Tuy nhiên, do những hạn chế nhất định về kiến thức và thời gian, bài báo cáo khó tránh khỏi những thiếu sót. Tôi rất mong nhận được những nhận xét, đóng góp của thầy để có thể hoàn thiện hơn trong các nghiên cứu sau này.
Lời cuối, tôi kính chúc thầy luôn mạnh khỏe, hạnh phúc và thành công trên con đường sự nghiệp.
Tôi xin cam đoan bài tiểu luận này là kết quả của quá trình học tập và nghiên cứu độc lập của cá nhân tôi, dưới sự hướng dẫn của Thầy Trần Mạnh Tường.
Tôi cam kết đã tuân thủ các nguyên tắc nghiên cứu khoa học. Tất cả nội dung, số liệu và các trích dẫn trong bài đều đảm bảo tính chính xác, trung thực và được ghi rõ nguồn gốc.
Tôi xin chịu hoàn toàn trách nhiệm về nội dung và tính trung thực của bài tiểu luận này.
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. 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.
df <- read.csv("C:/Users/Admin/Downloads/data.csv",header = TRUE)
Câu lệnh trên dùng để đọc tệp dữ liệu có đường dẫn cụ thể. Tham số header = TRUE cho R biết rằng hàng đầu tiên của tệp là tên các biến. Kết quả của hàm được gán cho biến df.
Ý nghĩa kĩ thuật và thống kê
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. 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.
dim(df)
## [1] 200000 25
Dòng code (1) dùng để xác định số hàng và số cột của bảng dữ liệu df nhằm nắm được quy mô của tập dữ liệu.
Kết quả dòng (1) trả về một vector gồm hai giá trị: 200.000 (số hàng) và 25 (số cột).
Ý nghĩa kĩ thuật và thống kê
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, thể hiện bộ dữ liệu lớn, đủ mạnh để thực hiện các mô hình phân tích phức tạp.
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.
head(df, 5)
Dòng code (1) lấy ra 5 dòng đầu tiên của dữ liệu df. Tham số 5 quy định số lượng dòng hiển thị, nếu không có tham số này, mặc định hàm sẽ hiển thị 6 dòng đầu.
Ý nghĩa kĩ thuật và thống kê
Kết quả cho thấy dữ liệu đã được ghi nhận đúng về kiểu bảng, có đầy đủ thông tin các trường chính như diện tích, giá, loại bất động sản,….
Dựa vào 5 quan sát đầu tiên, thấy đa dạng về price, property_size_sqft, customer_salary,… Điều này cho thấy dữ liệu đủ đại diện cho nhiều phân khúc thị trường bất động sản và có thể phục vụ cho các phân tích đa chiều về giá, vị trí hay đặc thù sản phẩm.
tail(df,5)
Dòng code (1) lấy ra toàn bộ trường của 5 dòng cuối cùng trong bảng df.
Ý nghĩa kĩ thuật và thống kê
Kết quả xác nhận dữ liệu đã đủ và đúng định dạng, không bị thiếu dòng cuối, các trường đều có giá trị hợp lệ như price, loan_amount, property_type,…phù hợp cho thao tác phân tích tiếp theo. Các dòng cuối cũng thể hiện rõ sự đa dạng về country, emi_to_income_ratio, decision,…củng cố tính đại diện và phân bố đều của bộ dữ liệu, từ đó tăng độ tin cậy và độ mở rộng khi áp dụng kết quả phân tích vào thực tiễn.
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"
Dòng code (1) trả về danh sách đúng thứ tự các tên biến/vector trong bảng df. Kết quả cho ra một vector chứa tên tất cả các biến, ví dụ như “property_id”, “country”, “city”, “property_type”, “furnishing_status”,… Các tên biến đều rõ ràng, không bị trùng và đã chuẩn hóa, thuận tiện cho quá trình thao tác, truy xuất hoặc thống kê từng biến cụ thể.
Ý nghĩa kĩ thuật và thống kê
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,…
Các cột dữ liệu đa dạng, bao quát nhiều yếu tố, điều này là nền tảng để xây dựng mô hình dự báo hoặc phân tích yếu tố ảnh hưởng đến quyết định kinh tế trong thị trường bất động sản.
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"
Dòng code (1) trả về một vector gồm tên biến và loại dữ liệu, cho phép nắm rõ đặc trưng dữ liệu từng biến. Kết quả của dòng (1) và (2): với 3 biến gồm property_id, country, city thì sẽ có kiểu dữ liệu lần lượt là interger, character, character. Tổng cộng có 21 biến kiểu số nguyên hoặc số thực (như property_id, price, property_sqft, customer_salary,…), 4 biến có kiểu ký tự (như country, city, property_type, furnishing_status).
Ý nghĩa kĩ thuật và thống kê
Việc xác định đúng kiểu biến giúp đảm bảo khi thao tác tiếp diễn ra thuận lợi, tránh lỗi khi chuyển kiểu hoặc tính toán toán học. Dữ liệu rõ ràng, không bị lẫn lộn kiểu giữa các biến, phù hợp cho phân tích, thống kê và mô hình hóa.
unique(df$country)
## [1] "France" "South Africa" "Germany" "Canada" "Brazil"
## [6] "UAE" "Australia" "UK" "USA" "China"
## [11] "Singapore" "India" "Japan"
Dòng code (1) trả về một vector gồm các tên quốc gia xuất hiện duy nhất trong dữ liệu: France, South Africa, Germany, Canada,… .
Kết quả cho dòng (1): tên các quốc gia được hiển thị như France, South Africa, Germany, Canada và Brazil. Tổng bộ dữ liệu gồm 13 quốc gia, mỗi giá trị là một tên quốc gia ghi nhận đúng kiểu chuỗi, không có quốc gia lặp lại, giá trị dữ liệu rõ ràng, không sai lỗi chính tả hoặc trùng tên. Phạm vi địa lý của bộ dữ liệu khá rộng, mang tính đại diện cho nhiều châu lục và thị trường bất động sản lớn.
Ý nghĩa kĩ thuật và thống kê Bộ dữ liệu đa quốc gia sẽ giúp phân tích được mức độ khác biệt về thị trường, giá, đặc điểm về bất động sản giữa các nước, tạo điều kiện tốt khi xây dựng nhận định so sánh hoặc ra quyết định đầu tư phân vùng theo quốc gia.
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
Dòng code (1) có mục đích tạo ra một bảng tần số cho biến country,thống kê số lượng quan sát ứ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.
Kết quả dòng (2) và (3) cho thấy ở Australia có 15.442 quan sát và Brazil là 15.401 quan sát,… Tổng quan 13 quốc gia đều có số lượng quan sát dao động khoảng 15.000, mức phân bổ khá đồng đều giữa các nhóm. Không có quốc gia nào bị thiếu hàng hay nhiều hơn hẳn so với nhóm còn lại, đảm bảo tính đại diện, hạn chế ảnh hưởng do dữ liệu lệch nhóm.
Ý nghĩa kĩ thuật và thống kê
Dữ liệu phân bố gần như bằng nhau giữa các quốc gia, tạo tiền đề tốt cho so sánh đặc điểm và xu hướng thị trường bất động sản giữa các nước. Kết quả này tăng độ tin cậy cho các mô hình ước lượng và dự báo, đồng thời hạn chế sai số do mất cân bằng mẫu giữa các nhóm quốc gia.
summary(df$price)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 56288 565990 1023429 1215365 1725557 4202732
Dòng code (1) 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 ta 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.
Kết quả cho thấy biến price có giá trị thấp nhất là 56.288, lớn nhất là 4.202.732. Giá trị trung bình đạt 1.215.365 và trung vị là 1.023.429. Điều này thể hiện khoảng chênh lệch rất lớn giữa các mức giá bất động sản trong tập dữ liệu, thể hiện độ phân tán cao.
Ý nghĩa kĩ thuật và thống kê
Độ lệch: Giá trị Trung bình (\(1.215.365\)) lớn hơn Trung vị (\(1.023.429\)), thể hiện dữ liệu có thể nghiêng về phía giá trị lớn, không hoàn toàn đối xứng. Giá bất động sản dao động trong biên độ rộng từ 56.288 USD đến 4.202.732 USD cho thấy thị trường rất đa dạng sản phẩm, có cả phân khúc giá rẻ lẫn cao cấp. Giá trung bình 1.215.365 USD và trung vị 1.023.429 USD giúp xác định đặc điểm thị trường tập trung vào khoảng giá trung bình khá cao, phục vụ tốt cho phân tích phân vùng giá, định giá và tư vấn chiến lược đầu tư.
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) |
Dòng code (1) đến (9): tạo vector tên 25 biến gốc khớp hoàn toàn với bảng dữ liệu đã đọc.
Dòng code (10) đến (17): tạo vector chứa 25 chú giải tiếng Việt chi tiết, hỗ trợ hiểu rõ nội hàm từng biến.
Dòng code (18) đến (21): ghép song song hai vector thành bảng có hai cột: tên biến và giải thích nghĩa.
Dòng code (22) chuyển bảng sang dạng trực quan, đẹp và dễ đọc khi trình bày báo cáo.
Bảng kết quả gồm đầy đủ 25 dòng, ví dụ property_id với giải thích “ID Bất động sản”, country với giải thích “Quốc gia”, price với giải thích “Giá bán Bất động sản”. Các chú giải rất rõ ràng, trực tiếp áp dụng cho báo cáo hoặc dùng khi giải thích các bước phân tích tiếp theo.
Ý nghĩa kĩ thuật và thống kê
Việc trình bày song song tên biến nguyên gốc và ý nghĩa giúp hạn chế nhầm lẫn khi thao tác qua lại giữa tên tiếng Anh và tiếng Việt, tra cứu biến nhanh, chuẩn hóa quy trình code và phân tích dữ liệu. Phần giải thích rõ ràng cho từng biến bảo đảm mọi bước xây dựng mô hình, kiểm tra hoặc báo cáo kết quả đều đồng nhất về mặt ý nghĩa, giúp người dùng (bao gồm cả độc giả không rành tiếng Anh chuyên ngành) hiểu rõ từng yếu tố tác động đến quyết định mua, giá bán và các kết quả kinh tế rút ra từ dữ liệu.
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
Dòng code (1) có mục đích kiểm tra số lượng giá trị khuyết thiếu 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) trước khi tiến hành mô hình hóa.
Kết quả cho thấy toàn bộ 25 biến không có bất kỳ giá trị NA nào (ví dụ property_id: 0, country: 0, price: 0,…). Điều này đảm bảo dữ liệu hoàn chỉnh, nhất quán, không có trường hợp thiếu dòng ở bất kỳ biến nào.
Ý nghĩa kĩ thuật và thống kê
0 giá trị thiếu ở tất cả các biến chứng tỏ chất lượng nguồn dữ liệu cao, không cần thao tác bổ sung, loại bỏ hay nội suy giá trị. Phân tích, lập mô hình sẽ thuận tiện, tránh lỗi do dữ liệu thiếu gây ra, đặc biệt đúng với các thuật toán nhạy với NA. Bộ dữ liệu không có giá trị thiếu tạo thuận lợi cho các phép thống kê mô tả, hồi quy, dự báo vì đảm bảo toàn bộ thông tin được sử dụng tối đa, giữ nguyên tính đại diện của mẫu. Việc không thiếu biến nào giúp tăng độ tin cậy và ý nghĩa của các kết quả phân tích kinh tế, nhất là khi áp dụng cho ra quyết định tài chính, tư vấn đầu tư.
sum(duplicated(df))
## [1] 0
Dòng code (1) 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). Dữ liệu trùng lặp có thể làm sai lệch các ước lượng thống kê.
Kết quả trả về là 0, nghĩa là trong toàn bộ dữ liệu không có dòng nào bị trùng hoàn toàn. Điều này xác nhận mỗi quan sát là duy nhất, không hề trùng về tất cả biến với bất kỳ dòng nào khác..
Ý nghĩa kĩ thuật và thống kê
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 . 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ê. Không có dòng trùng giúp loại trừ khả năng lặp lại thông tin khách hàng hay tài sản bất động sản, nâng cao giá trị suy luận và độ tin cậy cho các kết quả phân tích, dự báo kinh tế và ra quyết định đầu tư dựa trên dữ liệu này.
df <- df %>%
rename(
salary = customer_salary,
loan = loan_amount,
emi_ratio = emi_to_income_ratio)
df %>%
select(salary, loan, emi_ratio) %>%
head()
Khối lệnh này thực hiện thao tác đổi tên ba biến số định lượng trong dữ liệu. Mục đích là để đổi tên các biến liên quan tài chính sang tiếng Anh ngắn gọn hơn, dễ nhớ hơn (ví dụ: 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ê.
Dòng code (1)-(5): Đổi tên biến cho 3 biến tài chính trong bảng dữ liệu thành tên mới ngắn gọn, trực tiếp.
Dòng code (6), (7): Chọn ra đúng 3 biến đã đổi tên từ bảng dữ liệu df.
Bảng kết quả gồm 6 dòng đầu tiên với ba biến: lương (salary), khoản vay (loan), tỷ lệ trả nợ/thu nhập (emi_ratio). Ví dụ, dòng (1): khách hàng có lương 10.745 USD, khoản vay 193.949 USD, tỷ lệ emi là 0,16. Dòng (6): khách hàng có lương 95.520 USD, khoản vay 793.316 USD, tỷ lệ emi là 0,05. Các giá trị này hoàn toàn hợp lý, phản ánh đủ cả khách thu nhập thấp và cao, mức vay lớn nhỏ đa dạng.
Ý nghĩa kĩ thuật và thống kê
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. 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. Dữ liệu minh họa đầy đủ trường hợp khách hàng lương thấp (10.745) và cao (95.520), cũng như các khoản vay thực nhận từ 65.833 đến 793.316. Tỷ lệ emi/thu nhập dao động từ 0,03 đến 0,33 phản ánh mức rủi ro tín dụng phong phú—nền tảng tốt để phân tích khả năng trả nợ, sức mua và xây dựng mô hình đánh giá hồ sơ tài chính khách hàng.
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"
Khối lệnh 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.
Dòng code (1) đến (8): tạo lại bảng df với pipe (%>%), trong đó thực hiện mutate để chuyển đồng loạt 5 biến country, city, property_type, furnishing_status, decision về kiểu dữ liệu factor. Dòng code (9): xem kiểu dữ liệu của các biến đã đổi sang factor chưa.
Kết quả hiển thị trong bảng cho thấy cả 5 biến country, city, property_type, furnishing_status, decision đều đã được chuyển thành công sang kiểu factor.
Ý nghĩa kĩ thuật và thống kê
Tất cả 5 biến phân loại đã chuyển về kiểu yếu tố thành công, không lỗi, đảm bảo đúng cấu trúc cho các hàm phân loại, hồi quy sau này. Việc đồng bộ các biến về dạng nhóm giúp thuận tiện chia nhóm, so sánh từng quốc gia, từng loại nhà, từng quyết định mua, phục vụ cho phân tích xu hướng và đánh giá rủi ro thị trường đa chiều.
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 lệnh này có mục đích tạo biến mới price_group và phân loại giá thành 4 nhóm (Thấp, Trung bình, Cao, Rất cao) để kiểm tra phân phối giá, thống kê số lượng từng nhóm và tỉ lệ phần trăm dễ dàng so sánh mức độ chênh lệch các phân khúc thị trường.
Dòng code từ (2) đến (7) : dùng mutate để tạo biến price_group với phân loại giá dựa trên các điều kiện rõ ràng bằng case_when. Điều kiện từng nhóm không bị lặp hoặc thiếu, xử lý đúng các trường hợp phân tổ (price dưới 200.000 là “Thấp”, từ 200.000-500.000 là “Trung bình”, từ 500.000-1.000.000 là “Cao”, từ 1.000.000 trở lên là “Rất cao”).
Dòng code (9) : nhóm dữ liệu dựa trên biến price_group.
Dòng code (10) và (11): tạo biến Count, đếm số quan sát cho từng nhóm giá bất động sản.
Dòng code (13): thêm chỉ số phần trăm cho từng nhóm, làm tròn 2 chữ số sau dấu phẩy, giúp so sánh trực quan giữa các nhóm với tổng mẫu.
Dòng code (14): sắp xếp bảng kết quả theo số lượng giảm dần, hỗ trợ nhận diện nhóm chiếm ưu thế thị trường.
Kết quả: nhóm “Rất cao”: 102.236 quan sát, chiếm 51,12% tổng số quan sát.Nhóm “Cao”: 55.274 quan sát, chiếm 27,64% tổng số quan sát.Nhóm “Trung bình”: 33.615 quan sát, chiếm 16,81% tổng số quan sát.Nhóm “Thấp”: 8.875 quan sát, chiếm 4,44% tổng số quan sát.
Ý nghĩa kĩ thuật và thống kê
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 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ừ bước chia lớp đến tính phần trăm đều đầy đủ, chính xác, trả về số liệu đúng, bảng hiện rõ kết quả. Thị trường tập trung mạnh vào sản phẩm giá Rất cao (hơn 50% tổng mẫu), phân khúc Thấp chiếm rất nhỏ (dưới 5%), thể hiện xu hướng, cấu trúc phân khúc và sức mua chủ yếu vào bất động sản cao cấp, có ý nghĩa lớn khi đánh giá tiềm năng phát triển, ra quyết định đầu tư.
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()
Khối lệnh 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.
Dòng code (2)-(5): dùng mutate để tạo hai biến mới: garage_label và garden_label. Trong đó, ifelse kiểm tra biến gốc garage/garden: nếu garage/garden bằng 1 thì gán “Có”, nếu bằng 0 thì gán “Không”.
Kết quả của dòng (2): bất động sản này có garage và có vườn. Tương tự cho các dòng kết quả còn lại.
Ý nghĩa kĩ thuật và thống kê
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).
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”.
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()
Khối lệnh này thực hiện thao tác chuẩn hóa cho bốn 4 đị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.
Dòng code từ (2) đến (7): Tạo 4 biến mới đã chuẩn hóa. Các biến đều đã chuẩn hóa, giá trị dương nghĩa là lớn hơn trung bình, giá trị âm nghĩa là nhỏ hơn trung bình.
Kết quả dòng (1) price_scaled, salary_scaled, loan_scaled, expenses_scaled tương ứng với các giá trị chuẩn hoá: -0,974, -1,278, -1,0307, -0,7397.Tương tự cho các dòng kết quả còn lại.
Ý nghĩa kĩ thuật và thống kê
Các giá trị sau chuẩn hóa đều về trung bình 0, độ lệch chuẩn 1, đạt đúng mục tiêu xử lý z-score, dữ liệu đầu ra thuận tiện cho các phân tích thống kê.
Việc chuẩn hóa giúp loại bỏ ảnh hưởng chênh lệch đơn vị gốc giữa giá, lương, khoản vay và chi phí, bảo đảm mọi biến có ảnh hưởng ngang nhau khi sử dụng trong phân tích tương quan, hồi quy. Giá trị âm/dương giúp nhận diện các trường hợp bất động sản có đặc điểm vượt chuẩn hay thấp hơn chuẩn, trực tiếp hỗ trợ tìm hiểu nhóm khách hàng hoặc sản phẩm ngoại lệ trên thị trường.
df <- df %>%
mutate(loan_to_income = loan / salary)
df %>%
select(loan, salary,loan_to_income) %>%
head()
Khối 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).
Dòng code (2): tạo biến tỉ số khoản vay trên thu nhập, căn cứ so sánh chuẩn hóa mức gánh nợ của từng cá nhân trong tập hợp dữ liệu.
Kết quả dòng (1): loan = 193.949, salary = 10.745, loan_to_income = 18,05. Khách vay gấp hơn 18 lần thu nhập, rủi ro tín dụng lớn. Tương tự cho các dòng kết quả còn lại.
Ý nghĩa kĩ thuật và thống kê
Phép toán chia với hai biến số, không lỗi cú pháp hoặc NA, logic gán biến hợp lý, phù hợp thực tiễn xử lý biến tài chính.
Hệ số loan_to_income trong nhiều trường hợp quá cao (cụ thể dòng (4): 37,53), cảnh báo nguy cơ tín dụng xấu, đặc biệt với khách vay gấp nhiều lần thu nhập. Việc có biến này giúp phát hiện các trường hợp vượt chuẩn an toàn tài chính, hỗ trợ chặt chẽ cho kiểm tra, tư vấn hoặc xây dựng mô hình xác suất vỡ nợ.
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()
Khối lệnh này thực hiện thao tác kỹ thuật tạo đặc trưng. Mục đích là 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 và 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ũ”).
Dòng code từ (2) đến (8): dùng mutate để tạo biến house_age, đo tuổi đời từng bất động sản bằng phép toán trừ năm gốc giả định (2025) cho năm xây dựng. Tiếp tục dùng case_when phân loại rõ 3 nhóm: nếu house_age < 10 thì gán nhãn “Mới (<10 năm)”, nếu house_age >= 10 & house_age < 30 thì gán nhãn “Trung niên (10-30 năm)”, trường hợp còn lại house_age >= 30 thì gán “Cũ (>30 năm)”.
Kết quả dòng (1): France, 1989, “Cũ (>30 năm)”, nhà trên 30 năm tuổi, thuộc nhóm cũ. Dòng (3): South Africa, 2019, “Mới (<10 năm)”, nhà mới xây. Tương tự cho các dòng kết quả còn lại.
Ý nghĩa kĩ thuật và thống kê
Code chia nhóm rõ ràng, giá trị Age_Group khớp điều kiện, không có logic dư thừa hay rối nhánh, trích xuất lên bảng đúng chuẩn bài toán.
Đ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. Phân nhóm tuổi nhà giúp nhận diện đặc điểm nguồn cung bất động sản từng quốc gia, kiểm tra nhu cầu cải tạo, nâng cấp hoặc xác định sức hút thị trường của từng nhóm tuổi, phù hợp với nhu cầu đầu tư và phân tích giá trị sản phẩm.
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()
Khối 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ị .
Dòng code từ (2) đến (6): dùng mutate tạo biến salary_quantile với ntile, chia đều bộ data thành 4 nhóm lương đi từ thấp lên cao.
Kết quả dòng (1): salary_quantile = 1, Salary_Group = Thấp thể hiện khách thuộc nhóm lương thấp nhất. Dòng (3): salary_quantile = 2, Salary_Group = Trung bình thấp thể hiện khách có lương cao hơn nhóm đầu, thuộc nhóm thứ hai. Tương tự cho các dòng kết quả còn lại
Ý nghĩa kĩ thuật và thống kê
Tạo biến thứ tự nhóm và gán nhãn chính xác, không trùng lặp, dữ liệu phân phối đúng logic.
Việc phân nhóm lương này giúp nhận diện nhanh tỷ lệ khách hàng từng nhóm, hỗ trợ so sánh sức mua, xu hướng tiêu dùng và xác định tiềm năng từng nhóm đối tượng cho mục tiêu marketing, phát triển sản phẩm hoặc khuyến nghị đầu tư.
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()
Dòng code (1) 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): “Đầ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 đồ trở nên dễ đọc và chuyên nghiệp hơn đối với người xem.
Kết quả dòng (2): property_type = “Apartment”, furnishing_status = “Bán nội thất”. Nhà thuộc dạng căn hộ với trang bị nội thất mức bán phần.Tương tự cho các dòng kết quả còn lại.
Ý nghĩa kĩ thuật và thống kê
Các giá trị nhãn mới hiển thị trực tiếp trên bảng, đổi mức chuẩn cú pháp, thuận tiện trình bày kết quả hoặc làm báo cáo phân loại sản phẩm, không lỗi dữ liệu, không chồng lấn nhãn.
Việc đổi nhãn mức giúp phân tích nhanh tỷ lệ, đặc điểm bất động sản theo từng trạng thái nội thất, nhận diện nhóm sản phẩm được ưa chuộng, thuận tiện phục vụ phân khúc khách hàng, lên chiến lược bán hàng, kinh doanh và đánh giá tiềm năng từng loại hình nhà trên thị trường.
tab_decision <- table(df$decision)
tab_decision
##
## 0 1
## 153932 46068
prop.table(tab_decision)
##
## 0 1
## 0.76966 0.23034
Khối lệnh này có hai mục đích chính: thống kê tần số và thống kê tần suất.
Dòng code (1) bảng tần số dùng để đếm số lượng quan sát 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”)
Dòng code (1) bảng tần suất dùng để tính toán tỷ lệ phần trăm của mỗi cấp độ “0” và “1” so với tổng số quan sát.
Kết quả: dòng (2) và (3) của bảng tần số: số khách không mua (0): 153.932, số khách mua (1): 46.068. Kiểu dữ liệu hiển thị đúng bảng thống kê, đủ hai mức giá trị của biến quyết định mua. Kết quả dòng (2) và (3) của bảng tần suất: tỉ lệ khách không mua là 0,77; tỉ lệ khách có mua là 0,23. Tính toán đúng phần trăm trên tổng mẫu, không bị làm tròn thừa hoặc thiếu.
Ý nghĩa kĩ thuật và thống kê
Tổng tần số là \(153.932 + 46.068 = 200.000\) (khớp với dim(df)). Bảng số lượng và tỉ lệ trả về không lỗi cú pháp, số liệu trực quan. Rất thuận tiện cho bước phân tích tiếp theo và trình bày báo cáo xác suất mua hàng.
Tỉ lệ khách có quyết định mua bất động sản chỉ chiếm 23%, số không mua chiếm 77%. Điều này phản ánh rằng đa số khách hàng khảo sát chưa ra quyết định mua hoặc điều kiện chưa đủ hấp dẫn, thể hiện mức độ cạnh tranh và nhu cầu đầu tư thực tế trên thị trường bất động sản hiện tại.
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:tạo bảng tần số và tạo bảng tần suất.
Dòng code (1) của bảng tần số: dùng để đếm số lượng khách hàng ra quyết định *mua và không mua** cho từng phân khúc giá.
Dòng code (1) của bảng tần suất: dùng để tính toán tỷ lệ phần trăm của khách hàng ra quyết định “mua” và “không mua” trong từng phân khúc giá.
Kết quả dòng (4) của bảng tần số: phân khúc Rất cao có 79.472 khách hàng quyết định không mua và 22.809 khách hàng ra quyết định mua. Tương tự cho các phân khúc còn lại.
Kết quả dòng (3) của bảng tần suất: Phân khúc Cao có 77% khách hàng quyết định không mua và 23% còn lại quyết định mua. Tương tự cho các phân khúc còn lại.
Ý nghĩa kĩ thuật và thống kê
Kết quả đủ 4 dòng phân khúc, không trùng nhãn hoặc lỗi tính tỷ lệ, bảng kết quả trực quan. Phân khúc giá Thấp và Trung bình có tỷ lệ khách quyết định mua cao hơn (24,8%) so với nhóm Cao (23%) và Rất cao (22,3%). Tuy số lượng tuyệt đối nhóm Cao, Rất cao lớn hơn rất nhiều nhưng tỷ lệ ra quyết định mua vẫn thấp hơn nhóm giá thấp–trung bình.
Tỷ lệ quyết định mua ở các phân khúc giá thấp–trung bình nhỉnh hơn, phản ánh sức mua thực tế và rào cản của nhóm giá cao. Thị trường thực tế chủ yếu hấp dẫn khách ở nhóm giá không quá cao dù số lượng nguồn cung thuộc nhóm “Rất cao” chiếm vị trí 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
Khối 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.
Dòng code từ (3) đến (8): tính tổng số dòng của mỗi nhóm; giá trung bình, độ lệch chuẩn giá và giá trung vị. Dữ liệu đầu ra là một bảng tổng hợp.
Kết quả dòng (4): nhóm không mua có số lượng 153.932, giá trung bình là 1.233.212 USD, độ lệch chuẩn giá 836.705 USD, giá trung vị 1.033.279 USD.Dòng (5): nhóm mua có số lượng 46.068, giá trung bình 1.155.732 USD, độ lệch chuẩn giá 775.540 USD, giá trung vị 989.079 USD.
Ý nghĩa kĩ thuật và thống kê
Bảng tổng hợp đúng lệnh, giá trị trả về hợp lý, không dư hoặc thiếu nhóm. Dữ liệu sạch, dễ sử dụng tiếp cho các phân tích sâu.
Giá trung bình và trung vị của nhóm có mua thấp hơn nhóm không mua; tức là khách càng quyết định mua thì xu hướng chọn mức giá tốt, hợp túi tiền hơn. Độ lệch chuẩn giá ở nhóm mua cũng thấp hơn, phản ánh hành vi chọn lựa ổn định và ít rủi ro về giá trong nhóm khách quyết định mua bất động sản.
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
Khối lệnh này có mục đích thực hiện thống kê mô tả (trung bình và độ lệch chuẩn) so sánh cho các biến tài chính (salary, loan, emi_ratio).
Dòng code từ (2) đến (10): sử dụng group_by(decision) để chia nhỏ dữ liệu thành hai nhóm theo biến quyết định mua của từng khách hàng. Sau đó, summarise sẽ lần lượt tính các đại lượng đặc trưng: trung bình và độ lệch chuẩn cho các biến tài chính.
Kết quả dòng (4): nhóm khách hàng không mua có lương trung bình 45.126 USD, lệch chuẩn lương 28.066 USD. Khoản vay trung bình là 773.941 USD, lệch chuẩn khoản vay 560.421 USD. Tỷ lệ emi trung bình 0,214, lệch chuẩn 0,2422.
Ý nghĩa kĩ thuật và thống kê
Các giá trị trả về hợp lý, các con số chuẩn xác, mức độ biến động tài chính đều rõ ràng và logic với đặc điểm nhóm.
Những khách hàng quyết định mua nhà thường có mức lương trung bình cao hơn, khoản vay hơi thấp hơn so với nhóm không mua. Đáng chú ý tỷ lệ trả góp (emi) thấp hơn đáng kể – cho thấy nhóm này có sức tài chính tốt, áp lực trả nợ nhẹ hơn, khả năng tiếp cận tín dụng an toàn hơn. Độ lệch chuẩn lương và khoản vay đều cao, thể hiện mức độ phân tán lớn trong thị trường.
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
Dòng code (1): thực hiện kiểm định t-test hai mẫu độc lập cho giá (price) của bất động sản giữa nhóm khách không mua (decision = 0) và nhóm mua (decision = 1). Hàm t.test kiểm tra sự khác biệt về giá trung bình giữa hai nhóm, áp dụng Welch t-test để đảm bảo tính đúng khi phương sai hai nhóm khác nhau.
Dòng code (2): trả về bảng kết quả kiểm định.
Kết quả kiểm định: giá trung bình nhóm không mua: 1.233.212 USD, giá trung bình nhóm mua: 1.155.732 USD. Với t = 18.466, p-value < 2.2e-16 (cực kỳ nhỏ), cho thấy sự khác biệt giá giữa hai nhóm là có ý nghĩa thống kê. Khoảng tin cậy 95% hiệu số giá trung bình: từ 69.256 đến 85.703 USD — đảm bảo khoảng cách khác biệt giá luôn dương.
Ý nghĩa kĩ thuật và thống kê
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ê.
Giá trung bình bất động sản nhóm mua nhà thấp hơn nhóm không mua. Điều này cho thấy khách thực sự mua có xu hướng chọn sản phẩm ở mức giá vừa phải, chênh thấp so với mặt bằng chung, khẳng định hành vi lựa chọn hợp lý theo năng lực chi trả. Sự khác biệt giá này có ý nghĩa thống kê rất lớn, củng cố kết luận các yếu tố tài chính (giá cả) đóng vai trò quan trọng trong quyết định mua bất động sản trên thị trường hiện nay.
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
Dòng code (1) 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).
Kết quả của dòng (5) và (6): giá trị w lớn 3.704.107.167, p-value cực nhỏ (p-value <2.2e-16), khẳng định sự khác biệt rất rõ ràng về giá giữa hai nhóm. Khẳng định lại giả thuyết đối nghịch: trung vị của hai nhóm giá là khác nhau.
Ý nghĩa kĩ thuật và thống kê
Cụm code này chuẩn về cú pháp, thao tác đúng logic khi muốn kiểm định sự khác biệt phân phối (chủ yếu là trung vị) giữa hai nhóm biến phân loại với biến định lượng. Đặc biệt phù hợp với dữ liệu lớn, phân phối lệch hoặc nhiều ngoại lệ như giá bất động sản.
Nhóm khách có mua có xu hướng lựa chọn sản phẩm ở mức giá thấp hơn hoặc chênh lệch với mặt bằng giá chung, cho thấy yếu tố tài chính, sức chi trả quyết định rõ ràng đến hành vi tiêu dùng trên thị trường bất động sản.
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
Dòng code (1) để 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.
Kết quả dòng (5): X-squared = 108,15, df = 3, p-value < 2.2e-16. P-value cực nhỏ, bác bỏ giả thuyết độc lập — khẳng định biến price_group và decision liên quan chặt chẽ.
Ý nghĩa kĩ thuật và thống kê
Hàm kiểm định chi-squared tự động đối chiếu tần suất thực tế với tần suất kỳ vọng, cho ra x-squared, p-value và bậc tự do, rất thích hợp cho dữ liệu phân nhóm như này.
P-value cực nhỏ chứng minh biến price_group và decision liên quan có ý nghĩa rất mạnh. Nghĩa là tỉ lệ quyết định mua giữa các nhóm giá không giống nhau; phân khúc giá tác động lớn đến hành vi tiêu dùng bất động sản, nhóm giá thấp-tầm trung thường có tỉ lệ mua cao hơn so với nhóm giá cao, phản ánh đúng thực tế chi tiêu và nhu cầu thị trườ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
Dòng code (1),(2): chỉ định tập biến số định tính cần phân tích, từ bảng dữ liệu df. Tạo khung số liệu cho tính tương quan, logic chọn biến đầu vào hợp lý, đúng cú pháp.
Dòng code (3): tính ma trận tương quan các biến, với tuỳ chọn *pairwise.complete.obs giúp xử lý giá trị thiếu. Dòng code (4): Làm tròn giá trị ma trận tương quan về 3 chữ số thập phân.
Kết quả: các cặp biến tài chính như price - loan (0,938), price - property_size_sqft (0,745), loan - emi_ratio (0,424), loan_to_income - emi_ratio (0,957) cho hệ số cao; các biến cảm nhận như satisfaction_score, neighbourhood_rating, connectivity_score đều có hệ số gần 0 so với các biến tài chính.
Ý nghĩa kĩ thuật và thống kê*
Kết quả khối code trả về ma trận số hoàn chỉnh, không lỗi kỹ thuật. Giá trị ma trận dùng tốt cho kiểm tra đa cộng tuyến, lựa chọn biến mô hình, vẽ biểu đồ heatmap hoặc lấy kết quả cho các phép kiểm định tiếp theo.
Các biến giá, khoản vay, diện tích nhà, tỷ lệ tài chính liên thông rất mạnh với nhau, ngược lại nhóm đánh giá và cảm nhận gần như độc lập. Kết quả phù hợp thực tế rằng các yếu tố tài chính quyết định chính đến giá nhà, còn cảm nhận cá nhân có vai trò riêng độc lập với tài sản vật chất.
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
Khối lệnh này dùng để thực hiện phân tích tương quan điểm-nhị phân. Mục đích chính là để đo lường mối quan hệ tuyến tính và xếp hạng độ mạnh của mối quan hệ đó giữa mỗi biến định lượng với biến mục tiêu nhị phân (decision).
Dòng code (1): 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).
Dòng code (2): dùng sapply kết hợp cor để tính hệ số tương quan Pearson giữa mỗi biến số lượng trong num_vars và biến decision_num. lựa chọn pairwise.complete.obs xử lý hợp lý trường hợp thiếu dữ liệu.
Dòng code (3): sắp xếp theo thứ tự hệ số tương quan tuyệt đối giảm dần, giúp xác định biến nào liên quan mạnh/yếu với quyết định mua nhất.
Dòng code (4): In ra kết quả tương quan đầy đủ nhất (có cả dấu âm/ dương) sau khi đã tính toán.
Kết quả: biến satisfaction_score có tương quan với quyết định mua mạnh nhất (0,573). Các biến còn lại tương quan rất yếu với quyết định mua (đa số hệ số chỉ quanh 0,15 hoặc thấp hơn).
Ý nghĩa kĩ thuật và thống kê
Quá trình chuyển đổi biến kiểu phân loại sang số và tính tương quan hoàn toàn hợp lý trong phân tích thống kê, tránh lỗi khi thao tác với dữ liệu chuỗi; sapply tăng tốc độ xử lý so với vòng lặp thường. Kết quả trả về rõ ràng, hỗ trợ nhận diện nhanh biến liên quan quyết định mua nhất.
Biến satisfaction_score là yếu tố tác động lớn nhất đến quyết định mua bất động sản, cao hơn hẳn các yếu tố tài chính, những yếu tố này ảnh hưởng yếu hoặc không đáng kể đến quyết định mua trong mẫu thực tế này, điều này cho thấy ngoài vấn đề tài chính, đánh giá hài lòng chung (về sản phẩm, dịch vụ, vị trí, vv…) mới là yếu tố quyết định lớn cho hành vi mua bất động sản.
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
Khối lệnh này dùng để phân tích mối quan hệ giữa nhóm thu nhập và quyết định mua nhà bằng cách tạo ra hai bảng tổng hợp: bảng tần số và bảng tần suất.
Dòng code (1) của bảng tần số: tạo bảng phân phối số lượng khách chia theo nhóm lương (4 mức: thấp, trung bình thấp, trung bình cao, cao) và quyết định mua (0: không mua, 1: có mua).
Dòng code (1) của bảng tần suất: tính tỷ lệ phần trăm quyết định mua ở từng nhóm thu nhập, tổng trên mỗi dòng bằng 1.
Kết quả dòng (3) cả 2 bảng tần số và tần suất: nhóm “Thấp”: 41.578 khách hàng không mua, 8.413 khách hàng mua → tỷ lệ mua 16,8%. Tương tự cho các nhóm thu nhập còn lại.
Ý nghĩa kĩ thuật và thống kê
Khối code chạy đúng, phân tổ không lỗi nhãn, thống kê bảng và tỷ lệ rõ ràng, thuận tiện cho việc tổng hợp, xuất báo cáo hoặc làm trực quan hóa dữ liệu.
Tỷ lệ mua bất động sản tăng cùng với mức lương. Những khách hàng thuộc nhóm lương cao có khả năng ra quyết định mua cao hơn, phản ánh thực tiễn thu nhập là yếu tố tài chính quyết định lớn nhất tới sức mua trên thị trường, từ đó định hướng chiến lược sản phẩm và marketing phù hợp từng giai đoạn.
t_satis <- t.test(satisfaction_score ~ decision, data = df)
t_satis
##
## Welch Two Sample t-test
##
## data: satisfaction_score by decision
## t = -462.35, df = 177880, 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:
## -3.928119 -3.894956
## sample estimates:
## mean in group 0 mean in group 1
## 4.597667 8.509204
Khối lệnh này dùng để thực hiện Kiểm định T-test độc lập). Mục đích chính là để kiểm tra xem trung bình 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).
Dòng code (1): thực hiện kiểm định t-test so sánh trung bình satisfaction_score giữa hai nhóm khách không mua (0) và mua (1).
Dòng code (2): xuất bảng kết quả kiểm định: giá trị t, bậc tự do, p-value, hiệu trung bình giữa hai nhóm, khoảng tin cậy 95% và giá trị trung bình mẫu từng nhóm.
Kết quả: t = -462.35, kiểm định có khác biệt rất rõ, df = 177.880 cho thấy cỡ mẫu lớn. Với p-value < 2.2e-16, bác bỏ hoàn toàn giả thuyết hai nhóm giống nhau. Trung bình điểm hài lòng của nhóm không mua là 4,60 điểm và của nhóm mua là 8,51 điểm. Nhóm khách mua nhà có điểm hài lòng trung bình gấp đôi nhóm không mua, sự khác biệt rất mạnh và chắc chắn về mặt xác suất.
Ý nghĩa kĩ thuật và thống kê
Khối code chạy đúng, hàm trả đủ tham số kiểm định, kết luận không lỗi kỹ thuật, định dạng kết quả rõ ràng dễ tổng hợp báo cáo.
Điểm hài lòng có thể được xem là yếu tố quyết định lớn nhất tới hành vi mua bất động sản, khách mua nhà có mức hài lòng vượt trội so với nhóm còn lại, thể hiện vai trò trọng tâm của chất lượng dịch vụ, trải nghiệm và sản phẩm đối với tiêu dùng trên thị trường.
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
Dòng code (1): tính toán các phân vị, cụ thể là tứ phân vị (Quartiles: \(25\%\), \(50\%\) (trung vị), \(75\%\)) và các phân vị ngoại lai (\(1\%\), \(5\%\), \(95\%\), \(99\%\)).
Kết quả của dòng (1) và (2):
1%: 110.736 USD — 1% bất động sản có giá thấp hơn mức này, phản ánh đáy thị trường.
5%: 210.252 USD — 5% sản phẩm ở mức giá rất thấp.
25%: 565.990 USD — đại diện nhóm giá trung bình thấp.
50%: 1.023.429 USD — trung vị, 50% bất động sản giá thấp hơn, 50% giá cao hơn (mức phổ biến thị trường).
75%: 1.725.557 USD — đại diện nhóm giá trung bình-cao.
95%: 2.807.339 USD — mức giá cao vượt lên hầu hết sản phẩm.
99%: 3.671.005 USD — chỉ 1% bất động sản thị trường có giá trên mức này, thể hiện giá siêu cao/đặc biệt, ngoại lệ tuyệt đối.
Ý nghĩa kĩ thuật và thống kê
Khối code trả về 7 mốc giá đại diện, dễ dùng cho tổng hợp bảng, vẽ boxplot, phát hiện nhanh ngoại lệ và xác định ranh giới phân nhóm giá, không lỗi kỹ thuật, đúng chuẩn phân tích dữ liệu lớn.
Kết quả phản ánh phần lớn thị trường tập trung quanh vùng giá 500.000 - 1.700.000 USD, xuất hiện nhóm nhà giá rất thấp và một nhóm nhỏ nhà giá siêu cao. Việc xác định các mốc này giúp doanh nghiệp chọn chiến lược bán phù hợp hơn, phòng tránh rủi ro khi định giá và đánh giá hiệu quả tiếp cận từng phân khúc khách hàng cụ thể.
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
Dòng code (1): tạo ra một danh sách chỉ chứa tỷ lệ phần trăm khách hàng quyết định “Mua” (decision = 1) bên trong mỗi nhóm giá.
Kết quả của dòng (1), (2): nhóm giá Rất cao: 0,223 — tỷ lệ mua thấp nhất trong nhóm giá (22,3%). Nhóm giá Cao: 0,230 — cứ 100 khách ở nhóm giá cao thì có 23 người mua. Nhóm giá Trung bình: 0,248 — bằng với nhóm thấp, 24,8% quyết định mua. Nhóm giá Thấp: 0,248 — tỷ lệ mua ở nhóm giá thấp cao nhất (24,8%).
Ý nghĩa kĩ thuật và thống kê
Khối code gọn, tận dụng đúng chức năng bảng tần suất, giúp ta khai thác dữ liệu dễ tiếp cận mà không cần xử lý phức tạp.
Kết quả cho thấy khách thực sự mua tập trung chủ yếu ở nhóm giá thấp và trung bình; nhóm cao và rất cao sức mua giảm rõ. Điều này thể hiện phần lớn thị trường có khả năng chi trả ở mức vừa phải, là bằng chứng để ưu tiên chính sách sản phẩm phù hợp hộ gia đình và cá nhân phổ thông.
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
Dòng code (1): 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.
Kết quả của dòng (1) và (2): với các loại hình Farmhouse, Apartment, Studio và Villa thì tỷ lệ mua tương ứng cho từng loại lần lượt là : 0,232, 0,232, 0,231, 0,230.
Ý nghĩa kĩ thuật và thống kê
Khối code hiệu quả, tự động trả về bảng xác suất, dễ kết nối biểu đồ hoặc báo cáo, không cần xử lý thêm, phù hợp R hiện đại và chuẩn chỉnh cho phân tích dữ liệu lớn.
Các loại hình bất động sản có tỷ lệ khách quyết định mua khá sát nhau (dao động 22,8% – 23,2%). Farmhouse, Apartment và Studio có tỷ lệ mua cao nhất một chút, trong khi Independent house và Townhouse thấp hơn. Kết quả này cho thấy không có loại hình nào vượt trội hoàn toàn, nhưng ưu thế nghiêng về các lựa chọn căn hộ và Farmhouse – phù hợp xu hướng thị trường hiện đại (ưu tiên căn hộ, nhà nghỉ dưỡng), doanh nghiệp nên xem xét đẩy mạnh sản phẩm này trong danh mục; mặt khác, sự chênh lệch nhỏ hàm ý cạnh tranh thị phần chủ yếu nằm ở yếu tố ngoài loại hình, như vị trí, giá hoặc chính sách bán hàng.
df %>%
group_by(decision) %>%
summarise(
median_lti = median(loan_to_income, na.rm = TRUE),
IQR_lti = IQR(loan_to_income, na.rm = TRUE)
)
Khối lệnh trên có 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).
Dòng code (3) đến (5): chia bảng theo 2 nhóm quyết định mua (0, 1), tính trung vị và khoảng biến thiên loan_to_income từng nhóm, loại bỏ NA nếu có.
kết quả:
Nhóm không mua có trung vị của loan_to_income là 16,7; khoảng tứ phân vị là 23,4.
Nhóm mua có trung vị của loan_to_income là 13,9; khoảng tứ phân vị là 15,5.4
Ý nghĩa kĩ thuật và thống kê
Khối code tính đủ hai chỉ số thống kê (trung vị, khoảng tứ phân vị), loại đúng giá trị thiếu, tách nhóm chính xác theo decision, đảm bảo tính toán không lỗi và dễ áp dụng thực tế.
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.
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)
Đ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 của biến Giá (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.
Dòng code (1): tính giá trị phân vị 99% của biến price, xác định ngưỡng “siêu cao” theo thực tế dữ liệu.
Dòng code (2): đếm số lượng dòng trong bảng dữ liệu có giá cao hơn mốc 99%.
Dòng code: head(df %>% filter(price > p99) %>% select(property_id, country, price, price_group), 10) dùng để lọc dữ liệu để xem chi tiết những dòng bất động sản giá vượt ngưỡng p99, chọn ra các trường property_id, country, price, price_group, trả về 10 dòng đầu.
Kết quả: tất cả 10 bất động sản lọc ra đều thuộc Singapore, giá đều trên 3.700.000 USD và được gán nhóm “Rất cao”. Dữ liệu đúng chuẩn ngoại lệ theo logic đã xác định ở những dòng trước.
Ý nghĩa kĩ thuật và thống kê
Các bước nối với nhau mạch lạc, xử lý truy vấn nhanh, trả kết quả sạch và rõ nhóm, tránh lỗi logic, dễ mở rộng sang các phân tích cao cấp hơn hoặc trực quan hóa.
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.
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
Mục đích chính của khối lệ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).
Dòng code (1) và (2) của garage_label: tạo bảng tần số giao giữa biến định tính garage_label (có/không garage) và quyết định mua (0, 1). Sau đó tiến hành kiểm định chi bình phương với dữ liệu tình trạng vườn (garage_label). Tương tự cho dòng code của garden_label.
Hai kết quả kiểm định:
chisq.test(tab_garage): x-squared = 0,3344; df = 1; p-value = 0,5631. chisq.test(tab_garden): x-squared = 0,20286; df = 1; p-value = 0,6524.
Ở cả hai kiểm định, p-value đều lớn hơn rất nhiều so với ngưỡng 0,05, nghĩa là không có đủ bằng chứng để kết luận tỉ lệ mua khác biệt giữa các nhóm có/không garage hoặc vườn.
Ý nghĩa kĩ thuật và thống kê
Kết quả này cho thấy hai biến garage 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…).
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.
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)
broom::tidy(m2, exponentiate = TRUE, conf.int = TRUE)
broom::tidy(m3, exponentiate = TRUE, conf.int = TRUE)
Mục đích chính khối lệ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).
Dòng code (1): lập mô hình hồi quy logistic , với biến price biến giải thích. Kết quả trả về các hệ số hồi quy cho biến price tác động lên xác suất khách hàng mua nhà. Tương tự cho code (2) và (3).
Dòng code (4): trả về bảng tổng hợp kết quả mô hình: odds ratio (“exp(estimate)”), p-value, khoảng tin cậy, kiểm định tác động của biến giải thích price lên xác suất mua.
Kết quả:
m1: mô hình trả kết quả OR xấp xỉ = 1 cho price nhưng có hệ số âm ở statistic, p-value ≈ 0, CI lower/upper: 1,000. Nghĩa là price tăng thì odds mua giảm nhẹ, có ý nghĩa thống kê mạnh.
m2: mô hình trả OR (1,000), statistic dương, p-value ≈ 0, CI lower/upper: 1,000. Nghĩa là salary tăng thì odds mua tăng, cũng có ý nghĩa thống kê mạnh, tác động dương nhưng khá nhẹ.
m3: mô hình trả OR ≈ 2,16, statistic lớn (200), p-value ≈ 0, CI lower: 2,14, upper: 2,18. Hài lòng tăng 1 điểm thì odds mua tăng hơn gấp đôi, tác động cực mạnh và có ý nghĩa cao về thống kê.
Ý nghĩa kĩ thuật và thống kê
Khối code tuân thủ chuẩn hồi quy logistic với một biến giải thích, dùng broom::tidy để tổng hợp kết quả rõ ràng, dễ đọc, không lỗi về mặt xử lý mô hình, thích hợp khi so sánh sức mạnh các biến độc lập.
Mức lương và giá bất động sản dù có ý nghĩa thống kê nhưng hiệu ứng quá nhỏ (OR ≈ 1), không có ý nghĩa thực tiễn rõ rệt trong xác suất mua; ngược lại, sự hài lòng (satisfaction_score) là yếu tố duy nhất trong ba biến làm tăng mạnh xác suất khách hàng quyết định mua – mỗi điểm hài lòng thêm làm odds mua tăng hơn gấp đôi, củng cố nhận định trải nghiệm và cảm nhận thực tế mới là động lực chính thúc đẩy hành vi tiêu dùng bất động sản
ks.test(df$price[df$decision=="0"], df$price[df$decision=="1"])
##
## 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
Dòng code (1): 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.
Kết quả của dòng (5):
D = 0,036893: khoảng cách lớn nhất giữa hai hàm phân phối, số càng lớn thì khác biệt càng rõ.
p-value < 2.2e-16: cực nhỏ, bác bỏ giả thuyết hai phân phối giống nhau. Vậy phân phối giá giữa hai nhóm mua và không mua là khác biệt.
Ý nghĩa kĩ thuật và thống kê
Sử dụng ks.test là phương pháp nhanh, hiệu quả để so sánh hai phân phối, không đòi hỏi giả thiết chuẩn hay phương sai bằng nhau, code lọc dữ liệu đúng logic, không lỗi.
Khách hàng mua bất động sản có xu hướng chọn mức giá khác so với nhóm còn lại, phản ánh sự phân cực hoặc đặc thù hành vi mua theo phân khúc. Điều này giúp xác định chiến lược giá phù hợp và gia tăng hiệu quả tiếp cận khách hàng mục tiêu.
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
Chỉ số AUC là thước đo chuẩn mực nhất để đánh giá khả năng phân biệt 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).
Dòng code (1): lập mô hình hồi quy logistic với biến kết quả là quyết định mua (0 hoặc 1); hai biến giải thích đầu vào là giá bất động sản (price) và lương (salary).
Dòng code (2): tính xác suất dự báo cho từng dòng dữ liệu dựa trên mô hình đã xây dựng ở code (1), type = “response” đảm bảo đầu ra là giá trị xác suất (trong khoảng 0-1), tượng trưng cho khả năng từng khách sẽ ra quyết định mua.
Dòng code (3): xây dựng đường cong ROC , một tiêu chuẩn để kiểm tra khả năng phân biệt của mô hình nhị phân.
Dòng code: pROC::auc(roc_obj) : tính chỉ số diện tích dưới đường cong ROC. AUC dao động từ 0,5 (phân biệt ngẫu nhiên) đến gần 1 (mô hình càng tốt).
Kết quả: AUC xấp xỉ 0,5762, nghĩa là mô hình dựa trên hai biến price và salary chỉ phân biệt hơi nhỉnh hơn ngẫu nhiên, chưa đủ mạnh để dự đoán xác suất thực sự của quyết định mua.
Ý nghĩa kĩ thuật và thống kê
Toàn bộ quy trình code chặt chẽ, đúng với bài toán phân loại trong kinh tế-xã hộ, không lỗi khi chạy trên bộ dữ liệu tiêu chuẩn. Sử dụng ROC và AUC là bước kiểm tra hiệu quả thiết thực, phản ánh trực tiếp chất lượng phân loại của mô hình.
Chỉ số AUC thấp cho thấy giá và lương chưa đủ để giải thích hoặc phân biệt khách hàng có quyết định mua với không mua, cần xem xét bổ sung thêm các biến khác (về cảm nhận, vị trí, tiện ích, v.v.) hoặc mô hình hóa phức tạp hơn để cải thiện độ mạnh giải thích xác suất mua bất động sản thực tế.
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(plot.title = element_text(size = 12))+
theme(legend.position="right")
p1
Giải thích Dòng (1)-(4): xây dựng nền tảng biểu đồ, chọn đúng dữ liệu (decision), ép kiểu phân nhóm, chia màu cho nhóm mua/không mua, vẽ biểu đồ cột, tạo đường viền trắng và hiển thị nhãn số lượng lên đầu mỗi cột.
Dòng (5)-(9): đảm nhận phần hoàn thiện biểu đồ: làm trục y thoáng không che nhãn, đặt các nhãn giải thích rõ ràng (tiêu đề, tên trục), định dạng kiểu chữ TimesVN cho đẹp mắt, tăng cỡ tiêu đề, và ẩn chú giải màu vì không cần thiết.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cột thể hiện rõ ràng cơ cấu hai nhóm khách hàng: không mua (0) là 153.932 người, mua (1) là 46.068 người. Số liệu nhãn được gắn ngay trên từng cột, màu sắc phân biệt rõ, giúp tra cứu trực tiếp và so sánh nhanh tổ chức dữ liệu. Hình thức trực quan, rõ ràng, dễ nhìn, thuận tiện tổng hợp báo cáo.
Lực mua trên thị trường thấp, phần lớn khách tạm thời chưa ra quyết định mua bất động sản. Biểu đồ phản ánh thực trạng quá trình tiếp cận khách hàng còn nhiều rào cản, gợi ý doanh nghiệp nên phân tích sâu nguyên nhân chưa ra quyết định để tối ưu các hoạt động kinh doanh hoặc chính sách hỗ trợ.
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(axis.text.x=element_text(angle=45,hjust=1))
p2
Giải thích
Dòng code (1)-(3): tạo nền biểu đồ cột với trục x là từng phân khúc giá, fill là màu đại diện nhóm quyết định mua/không mua, geom_bar(position=“fill”) để mỗi cột phân chia tỷ lệ phần trăm (độ cao cột luôn là 100%), scale_y_continuous chuyển trục y sang phần trăm.
Dòng code (4)-(9): thêm số lượng khách hàng của từng nhóm ngay trên từng dải tỷ lệ trong cột, đồng thời tùy chỉnh màu để khách không mua và mua tách biệt bằng xanh nhạt và đỏ cà chua, cùng chú giải rõ ràng.
Dòng code (11)-(12): đặt tiêu đề, tên trục, chọn giao diện tối giản và xoay nhãn dưới trục x cho khớp, tránh che lấp.
Ý nghĩa kĩ thuật và thống kê
Tỷ lệ khách mua ở các nhóm giá “cao”, “rất cao”, “thấp” và “trung bình” khá tương đương nhau (dải đỏ chiếm khoảng 20–25% ở mỗi cột), cho thấy không có sự khác biệt lớn về tỷ lệ mua giữa các phân khúc giá. Lượng khách không mua đều áp đảo trong mọi phân khúc, phản ánh xu hướng chung là đa số khách chưa ra quyết định mua bất động sản ở mọi mức giá.
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(legend.position="none")
p3
Dòng (1)-(3): tạo biểu đồ boxplot nhóm theo phân khúc giá (price_group),
giá trị y là giá bất động sản. geom_boxplot vẽ ra dạng tóm tắt trung vị,
phần tư, dải biến động, bỏ qua outlier vì đã có lớp geom_jitter bên
ngoài giúp quan sát phân bố dữ liệu đầy đủ.
Dòng (4)-(5): định dạng giá trị trục y kiểu kế toán (có dấu phẩy), thêm tiêu đề và nhãn trục, giúp tăng tính chuyên nghiệp cho biểu đồ.
Dòng (6): Tùy chỉnh bố cục tối giản, loại bỏ chú giải màu vì màu fill đã trực tiếp thể hiện phân khúc.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ boxplot kết hợp điểm jitter minh họa rõ ràng sự phân bố giá trong từng phân khúc: “Thấp”, “Trung bình”, “Cao”, “Rất cao”.
Nhóm “Rất cao” có khoảng giá lớn, dải biến động rộng, tập trung nhiều giá trị ở mức từ 1.000.000 đến hơn 4.000.000 USD, phản ánh thị trường bất động sản cao cấp trải dài rõ rệt. Ba phân khúc còn lại (“Thấp”, “Trung bình”, “Cao”) có hộp boxplot nhỏ hơn, giá trị trung vị và biến thiên thấp hơn nhiều (đặc biệt phân khúc “Thấp” và “Trung bình” giá tập trung ở mức dưới 1.000.000 USD). ## 4.4. Trực quan hóa phân phối thu nhập khách hàng
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 (USD)", y="Tần số")
p4
Dòng (1)-(2): tạo histogram với dữ liệu là cột salary, chia 40 cột nhỏ
(bin), màu nền xanh đậm, đường viền trắng, giúp dễ nhận diện các mức mật
độ phân phối thu nhập trên toàn tập khách hàng.
Dòng (3)-(4): chèn thêm đường thẳng trung bình của salary (vạch đỏ nét đứt) để minh họa vị trí trung bình trên biểu đồ, geom_rug giúp thể hiện các giá trị số thực nằm sát trục x, tăng độ cảm nhận mật độ từng điểm lẻ.
Dòng (5)-(6): định dạng trục x thành kiểu kế toán, thêm tiêu đề, nhãn trục x/y giúp thống nhất nội dung báo cáo hoặc trình bày.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ histogram chia 40 cột, tự động hiện mật độ từng nhóm thu nhập; vạch đỏ nét đứt thể hiện mức thu nhập trung bình, giúp nhìn nhanh vị trí trung bình trên toàn phân bố, màu nền xanh đậm nổi bật, viền trắng rõ và dễ đọc.
Đa số khách hàng có thu nhập dưới 50.000 USD, tập trung cao nhất ở vùng từ 15.000 đến 35.000 USD, phân phối lệch phải, còn khá nhiều khách thu nhập cao nhưng thưa dần. Mức thu nhập trung bình nằm nghiêng về phía thấp, cho thấy thị trường vẫn chủ yếu là nhóm thu nhập vừa và thấp.
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 (USD)", y="Giá (USD)", color="Decision")
p5
## `geom_smooth()` using formula = 'y ~ x'
Dòng code (1)-(6): tạo biểu đồ scatter plot hai chiều phân màu theo nhóm
quyết định mua/không mua. Các điểm được vẽ với độ trong suốt cao, thể
hiện phân bố thực tế. Đường cong mờ và đường viền mật độ minh họa xu
hướng tuyến tính cũng như khu vực tập trung mật độ đồng thời.
Dòng code (7)-(11): tuỳ chỉnh màu cho hai nhóm quyết định: “darkorange” cho không mua, “deepskyblue4” cho mua; bảng chú giải rõ ràng, đặt tiêu đề, tên trục, nên dễ đọc và truy cứu phân bố.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ scatter plot với hai nhóm màu (cam: không mua, xanh: mua), mật độ điểm dày toàn bản đồ, giúp thể hiện số lượng lớn và phân bố thực tế. Đường contour và đường xu hướng hỗ trợ nhận diện khu vực tập trung dữ liệu nhanh và độ nghiêng giữa hai biến.
Xu hướng giá nhà tăng cùng thu nhập thể hiện rõ trên toàn mẫu. Vùng khách có quyết định mua thường nằm ở phía trên và phía phải (giá cao, thu nhập cao), còn nhóm không mua phủ kín toàn đồ thị.
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
Dòng (1)-(4): tạo biểu đồ tổng hợp: trục x là nhóm quyết định (không
mua/mua), y là điểm hài lòng. Lớp violin hiển thị phân bố mật độ,
boxplot hiển thị các mốc cơ bản (trung vị, phần tư), chấm đỏ đánh dấu
giá trị trung bình từng nhóm.
Dòng (5)-(15): nhóm này chỉnh màu viền cho từng nhóm quyết định và điền đầy đủ phần giải thích tiêu đề, phụ đề, tên trục. Theme tối giản giúp biểu đồ sáng, thẩm mỹ khi trình chiếu.
Dòng (16)-(21): Tập trung cân chỉnh lại vị trí, kích thước tiêu đề và chuyển chú giải xuống đáy, đồng thời loại bỏ hướng dẫn màu/shape không cần thiết để biểu đồ tối ưu khi trình bày.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ violin kết hợp boxplot cho hai nhóm quyết định: không mua (màu xanh) và mua (màu đỏ). Ô boxplot cho thấy phương sai, trung vị; chấm đỏ đánh dấu vị trí trung bình, phần mật độ giúp hình dung độ tập trung điểm số trong mỗi nhóm, biểu đồ minh họa đồng thời nhiều đặc trưng phân phối dữ liệu.
Nhóm khách không mua có điểm hài lòng tập trung ở dưới (điểm trung bình và trung vị thấp), vùng phân bố kéo dài và rộng. Nhóm khách mua có điểm hài lòng cao hơn rõ rệt, phân bố tập trung hẹp ở phía trên (trung bình, trung vị đều lớn hơn nhiều)
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 thu nhập") +
theme(plot.title = element_text(size = 10))
p7
Dòng (1),(2): Tạo biểu đồ mật độ, mỗi đường màu cho một nhóm lương. Biểu
đồ thể hiện mức phổ biến tỷ lệ vay/thu nhập của từng nhóm lương, giúp so
sánh hình dạng phân phối giữa nhóm.
Dòng (3),(4): Thêm các vạch nhỏ ngay trục x để minh họa mật độ điểm thực tế và đường thẳng đứng màu đỏ là giá trị trung bình toàn tập, là mốc tham chiếu chung.
Dòng (6),(7): Chỉnh trục x thành định dạng phần trăm, thêm tiêu đề/nhãn trục, tăng cỡ tiêu đề cho trực quan và dễ đọc.
Ý nghĩa kĩ thuật và phân tích
Biểu đồ mật độ nhiều lớp, chia rõ 4 nhóm lương (Thấp, Trung bình thấp, Trung bình cao, Cao), màu sắc phân biệt, đường viền trục x có geom_rug giúp nhìn thêm mật độ tập trung dữ liệu thực tế. Đường đỏ thẳng đứng là giá trị trung bình toàn bộ mẫu, hỗ trợ so sánh vị trí các phân phối.
Tất cả nhóm lương có tỷ lệ vay/thu nhập tập trung rất cao phía dưới 10% và giảm dần về phía phải. Nhóm lương cao (màu tím) có mật độ lớn nhất ở vùng thấp, dư nợ so thu nhập thấp hơn các nhóm còn lại, chứng tỏ khả năng chi trả và kiểm soát nợ tốt hơn. Các nhóm thu nhập thấp và trung bình trải dài hơn về phía phải . Điều này phản ánh khách lương cao vay ít hơn so với thu nhập, khách thu nhập thấp dễ rơi vào nguy cơ lo về khoản vay .
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 (USD)", y="Khoản vay (USD)") +
theme(plot.title = element_text(size = 12))
p8
## `geom_smooth()` using formula = 'y ~ x'
Dòng (1),(2): tạo biểu đồ scatter plot với trục x là thu nhập (salary),
trục y là khoản vay (loan), phân màu theo quyết định mua (decision). Lớp
geom_point(alpha=0.5) vẽ từng điểm dữ liệu với độ trong suốt vừa phải,
tránh bị che khuất khi dữ liệu dày.
Dòng (3),(4): thêm đường xu hướng tuyến tính toàn tập (dashed, màu đen) thể hiện quan hệ chung giữa thu nhập và khoản vay bằng hồi quy tuyến tính; geom_rug tạo thêm các vạch nhỏ sát trục y để nhận biết vùng tập trung mật độ giá trị thực tế trên hai biến này.
Dòng code (5)-(7): Định dạng trục y kiểu kế toán, đặt tiêu đề, nhãn trục x/y và chỉnh kích thước tiêu đề cho đồng nhất.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ scatter plot thể hiện dữ liệu hai chiều, chia làm hai màu nhóm quyết định mua (0: đỏ, 1: xanh). Phân bố điểm rất dày, các vạch nhỏ sát trục thể hiện phân bố thực tế, đường xu hướng nét đứt màu đen minh họa mối liên hệ tuyến tính tổng quát giữa hai biến trong tập dữ liệu.
Phần lớn khách đều có khoản vay trải từ thấp tới hơn 3.000.000 USD, khoản vay tăng dần theo mức thu nhập nhưng tốc độ tăng không quá mạnh. Sự chồng lấn hai nhóm (không mua/mua) thể hiện không có khác biệt rõ về khoản vay giữa nhóm quyết định, nhưng khách thu nhập cao thì khả năng vay lớn hơn. Tuy nhiên, nhiều khách không mua vẫn vay với giá trị khá lớn, cho thấy vay tiền chưa hẳn là yếu tố quyết định đi đến giao dịch cuối cùng.
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 Điểm khu vực & 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(plot.title = element_text(size = 12))
p9
## `geom_smooth()` using formula = 'y ~ x'
Dòng (1)-(3): tạo khung biểu đồ cơ bản, ánh xạ trục x là điểm khu vực
lân cận (neighbourhood_rating), trục y là điểm kết nối
giao thông (connectivity_score), màu theo nhóm
lương.
Dòng (4),(5): chia mỗi nhóm giá thành một biểu đồ con riêng để quan sát so sánh rõ hơn xu hướng từng khúc, scale_color_discrete đổi chú giải màu nhóm lương, giúp người đọc dễ nhận biết các đường xu hướng theo từng loại lương trong mỗi phân khúc giá.
Dòng (6)-(9): đặt tiêu đề tổng hợp, nhãn trục đầy đủ, điều chỉnh kích cỡ font tiêu đề giúp biểu đồ rõ ràng và chuyên nghiệp khi báo cáo hoặc trình chiếu.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ facet wrap tách thành bốn ô: cao, rất cao, thấp, trung bình — mỗi ô là một phân khúc giá. trong từng khung, mỗi đường màu biểu diễn xu hướng thay đổi điểm kết nối giao thông theo điểm khu vực lân cận, phân biệt rõ ràng theo bốn nhóm lương.
Ở đa số phân khúc giá, nhóm lương thấp (màu nâu, hồng nhạt) có xu hướng điểm kết nối giao thông giảm khi điểm khu vực lân cận tăng (hệ số xu hướng âm). Ngược lại, nhóm lương cao (nhất là màu tím ở khung “cao”) lại có xu hướng tăng rõ nét – điểm khu vực lân cận càng cao thì kết nối càng tốt lê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.5) +
scale_y_continuous(labels = scales::percent, 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", fill = "Tình trạng garage")
p10
Dòng (1)-(3): nhóm dữ liệu gốc theo biến garage_label, rồi tính xác suất
mua (tỷ lệ decision==“1”) cho từng trạng thái garage (có hoặc
không).
Dòng (4)-(6): xây dựng biểu đồ cột cho bảng garage_df, trục x là tình trạng garage, trục y là xác suất mua; fill tự động phân màu theo từng trạng thái, geom_col() dựng cột trực tiếp từ giá trị đã tính, geom_text thêm nhãn tỷ lệ phần trăm trên đầu mỗi cột giúp đọc nhanh.
Dòng (7),(8): Định dạng trục y thành phần trăm, mở rộng đầu trục giúp nhãn không bị cắt. Đặt tiêu đề, tên trục, chú giải màu đầy đủ cho biểu đồ.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cột hiển thị rõ hai nhóm: có garage (màu đỏ), không garage (màu xanh). Nhãn xác suất được gắn ngay cạnh đầu mỗi cột, định dạng phần trăm rõ ràng, giúp so sánh nhanh kết quả giữa hai nhóm. tỷ lệ khách mua ở nhóm có garage (23,09%) và nhóm không garage (22,98%) gần như bằng nhau, gần như không có sự khác biệt về xác suất mua giữa hai nhóm này.
Điều này cho thấy tình trạng garage không phải yếu tố phân biệt rõ rệt hay quyết định đến xác suất khách hàng mua bất động sản trong tập dữ liệu này.
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)
##
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
##
## smiths
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(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(plot.title = element_text(size = 12))
p11
Dòng (1)-(3): lấy dữ liệu các biến số cần phân tích, sau đó tính ma trận
tương quan pearson, làm tròn 2 chữ số để thuận mắt đọc kết quả.Gọi
reshape2 để chuẩn bị chuyển dữ liệu kiểu ma trận sang dạng bảng dài dùng
cho ggplot.
Dòng (4)-(7): dùng hàm melt để chuyển ma trận sang dạng bảng có cột (biến 1, biến 2, hệ số tương quan), ggplot sẽ vẽ các ô vuông ứng với từng cặp biến, màu sắc theo giá trị tương quan, nhãn hệ số được đính trực tiếp lên từng ô.
Dòng (8)-(11): tạo thang màu đi từ xanh (tương quan âm), trắng (không liên hệ), tới đỏ (tương quan dương), giúp nhìn nổi bật các cặp biến có mối liên hệ mạnh, thêm tiêu đề, xoay nhãn cột ngang cho khớp.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ heatmap thể hiện hệ số tương quan pearson giữa từng cặp biến số trong bộ dữ liệu.Màu đỏ là tương quan dương mạnh, xanh tím là âm; giá trị được gắn trực tiếp lên từng ô thuận tiện cho việc đọc nhanh và so sánh mức độ liên hệ.
Giá và khoản vay có tương quan dương cực mạnh (0,94), giá và tỷ lệ trả nợ cũng dương cao (0,38); khoản vay liên hệ rất lớn với tỷ lệ vay/thu nhập (0,44) và tỉ lệ trả nợ (0,42), satisfaction_score độc lập hoàn toàn với các biến tài chính, hệ số đều là 0, phản ánh điểm hài lòng không bị chi phối bởi giá, lương, khoản vay hoặc các tỷ lệ liên quan. Tương quan âm giữa lương và các tỷ lệ nợ (-0,47 với emi_ratio, -0,5 với loan_to_income), cho thấy khách lương cao thường có tỷ lệ vay và tỷ lệ trả nợ thấp hơn.
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á (USD)",
y = "Phân khúc giá") +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(legend.position = "none")
p12
## Picking joint bandwidth of 22000
Dòng (1),(2): tạo biểu đồ ridgeline với biến x là giá (price), trục y là
phân khúc giá (price_group), fill tự động theo nhóm giá. Lớp
geom_density_ridges tạo các đường cong mật độ lấp màu, giúp so sánh hình
dạng phân phối giá ở từng nhóm trên cùng một mặt phẳng.
Dòng (3)-(9): cụm này định dạng trục x có chia vạch đều từ 0 đến 3.000.000, thêm dấu phẩy kiểu kế toán giúp đọc số liệu to dễ dàng, bổ sung tiêu đề và tên trục rõ ràng.
Dòng (10),(11): xoay nhãn trục x cho gọn, tránh gối nhãn khi số lượng giá trị lớn; ẩn bảng chú giải màu vì tên nhóm đã thể hiện rõ trên trục y, giữ biểu đồ thoáng sạch.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ ridgeline gồm bốn lớp mật độ, mỗi lớp ứng với một phân khúc giá (thấp, trung bình, cao, rất cao), màu sắc phân biệt rõ cho từng nhóm, trục x chia giá trị đều và gắn số kiểu kế toán giúp đọc giá trực quan, layout thoáng gọn.
Nhóm thấp và trung bình có phân phối giá rất hẹp, đỉnh mật độ tập trung quanh dưới 500.000 USD, nhóm cao có dải giá lớn hơn rõ, mật độ kéo dài tới gần 1.100.000 USD rồi giảm nhanh, nhóm rất cao trải rộng từ 1.000.000 tới trên 3.000.000 USD, phân bố đều và không có đỉnh rõ.
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.5) +
labs(title="(P13) Quyết định mua theo tình trạng garden", x="Tình trạng garden",
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.16))) +
theme(
plot.margin = margin(10,10,30,10),
axis.title.x = element_text(margin = margin(t = 14))
)
## `summarise()` has grouped output by 'garden_label'. You can override using the
## `.groups` argument.
p13
Dòng (1)-(3): nhóm dữ liệu theo tình trạng garden và quyết định mua, sau
đó đếm số lượng khách hàng cho từng tổ hợp, tạo bảng tổng hợp tần số
phục vụ trực quan hóa dạng cột.
Dòng (4)-(6): xây dựng biểu đồ cột nhóm, hai cột cạnh nhau cho mỗi tình trạng garden, dùng fill để tự động màu cho từng quyết định (không mua/mua), geom_text gắn số lượng lên đầu từng cột để đọc nhanh mỗi nhóm.
Dòng (7)-(16): đặt tiêu đề, tên trục, điều chỉnh bảng màu cho hai nhóm quyết định và chỉnh khoảng mở rộng trục y để số nhãn không bị cắt, chỉnh khoảng lề, căn lại tên trục x cho đẹp mắt.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cột nhóm, hai cột cho mỗi trạng thái garden (có/không), màu cam cho nhóm không mua, xanh cho nhóm mua. Số lượng được gắn trực tiếp trên đầu từng cột, màu sắc dễ phân biệt, trục y hiển thị đúng số lượng.
Số lượng khách mua gần như bằng nhau giữa hai tình trạng garden (có: 23.001, không: 23.067), cũng tương tự với không mua (có: 77.042; không: 76.890), hai nhóm tỷ lệ rất cân bằng, không có sự chênh lệch rõ giữa nhóm có hoặc không có garden. Điều này cho thấy tình trạng garden không ảnh hưởng đáng kể tới quyết định mua bất động sản của khách hàng trong bộ dữ liệu này.
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á (USD)") +
scale_color_manual(
name = "Quyết định",
labels = c("0" = "Không mua", "1" = "Mua"),
values = c("0" = "darkorange", "1" = "deepskyblue4")
) +
theme(plot.title = element_text(size = 13))
p14
## `geom_smooth()` using formula = 'y ~ x'
Dòng (1),(2): tạo scatter plot, trục x là điểm hài lòng, y là giá nhà,
phân màu theo nhóm quyết định mua/không mua.
Dòng (3),(4): lớp geom_smooth vẽ đường hồi quy tuyến tính chung cho toàn tập, giúp nhận diện xu hướng thay đổi giá nhà theo điểm hài lòng. Trục y chuyển về định dạng kế toán, dễ đọc giá trị lớn.
Dòng (6)-(11): Đặt tiêu đề, tên trục, bảng màu cam/xanh cho hai nhóm quyết định, chỉnh kích thước tiêu đề. Ý nghĩa kĩ thuật và thống kê
Đám mây điểm nhóm “Không mua” tập trung tại vùng điểm hài lòng dưới 7, cho thấy threshold hài lòng thấp gần như loại trừ khả năng mua – phân phối 2 nhóm tách biệt, không đồng đều. Giá trị “Mua” trải rộng về mặt giá, nhưng chỉ tập trung ở phía cuối phân phối điểm hài lòng, minh chứng vai trò then chốt của chỉ số này trong quyết định mua nhà.
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(
plot.margin = margin(10,10,30,10),
axis.title.x = element_text(margin = margin(t = 14))
)
p15
Dòng (1),(2): tạo biểu đồ histogram với biến diện tích nhà, chia thành
40 cột nhỏ, màu nền lam nhạt, viền trắng.
Dòng (3)-(6): thêm đường thẳng đứng đỏ biểu diễn giá trị trung bình diện tích, geom_rug vẽ các vạch nhỏ sát trục x, giúp xác định mật độ các điểm số nguyên thực t, trục x được chia đều 1.000 ft, định dạng kiểu kế toán cho dễ đọc.
Dòng (7)-(13): bổ sung tiêu đề ngắn gọn, nhãn trục rõ ràng, tinh chỉnh lề để bố cục thoáng.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ histogram mặt bằng diện tích thiết kế tốt, 40 cột đều nhau, màu lam nhạt rõ nét; vạch trung bình đỏ chia biểu đồ theo đúng vị trí mean của diện tích, trục x chia đều từng 1.000 ft², bố cục hợp lý, tiêu đề đầy đủ cho đúng mục tiêu phân tích.
Phân phối diện tích gần như đồng đều toàn dải từ 500 tới 6.000 ft², không kiểu lệch trái/phải hay tập trung một vùng, giá trị trung bình nằm gần mốc 3.000 ft²,biểu đồ này xác nhận thị trường phân phối bất động sản trong bộ dữ liệu chia đều đủ loại diện tích, phục vụ mọi nhu cầu khách hàng.
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 = 4, fontface = "bold",
vjust = -8) +
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
Dòng (1)-(3): nhóm dữ liệu theo loại hình nhà, tính diện tích trung bình
cho từng loại tạo bảng tóm tắt. Khởi tạo ggplot, ánh xạ trục x là loại
hình, trục y là diện tích, fill tự động màu hóa từng loại nhà.
Dòng (5)-(11): hiển thị phân phối diện tích cho từng loại bằng lớp violin và boxplot đồng thời. Chấm đỏ biểu diễn trung bình của từng loại, nhãn số diện tích trung bình được gắn lên ngay chính xác ở vị trí mean.
Dòng (12)-(26): chuyển trục y sang thang logarit, tiện so sánh các loại diện tích có quy mô rất khác nhau mà không bị ép sát. Định dạng dấu phẩy theo kiểu kế toán, thêm tiêu đề, nhãn trục, bố trí theme tối giản.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ violin kết hợp boxplot, thể hiện phân phối diện tích từng loại nhà trên thang logarit, mỗi loại có màu riêng, nhãn giá trị trung bình được gắn trực tiếp lên chấm đỏ ở giữa boxplot giúp đọc nhanh, bố cục hài hòa, nhãn trục x nghiêng nhẹ đúng quy chuẩn.
Phân phối diện tích của từng loại kéo dài hơi rộng phía dưới, nhưng phần chính tập trung quanh khoảng 3.000 ft², cho thấy đa phần các loại hình nhà trong dữ liệu đều hướng về nhóm diện tích vừa phải, ít xuất hiện cực trị lớn hoặc nhỏ quá rõ rệt.
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=-2, size=4) +
scale_y_continuous(labels=percent) +
labs(title="(P17) Xác suất mua theo nhóm thu nhập",x="Nhóm thu nhập",y="Xác suất mua") +
scale_y_continuous(expand = expansion(mult = c(0, 0.16)))+
theme(
plot.margin = margin(10,10,30,10),
axis.title.x = element_text(margin = margin(t = 14))
)
## Scale for y is already present.
## Adding another scale for y, which will replace the existing scale.
p17
Dòng (1),(2): dòng đầu lấy dữ liệu đã gom theo
Salary_Group, tính xác suất mua trung bình cho từng
nhóm (prob_buy). Tiếp theo, khởi tạo biểu đồ ggplot ánh xạ trục x là
nhóm lương, y là xác suất mua, group=1 đảm bảo vẽ đúng một đường nối
giữa các điểm theo thứ tự nhóm lương.
Dòng (3)-(5): vẽ line plot cho xác suất mua ở từng nhóm lương, màu tomato nổi bật, kích thước đường vừa phải, geom_point hiển thị điểm rời trên từng vị trí nhóm thu nhập, geom_text gắn nhãn giá trị phần trăm ngay phía trên.
Dòng (6)-(11): định dạng trục y là phần trăm, bổ sung tiêu đề, tên trục cho rõ ràng. Mở rộng trục y một chút ngoài phần dữ liệu để nhãn phần trăm phía trên không bị cắt, chỉnh margin cho bố cục biểu đồ hợp lý.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ line plot thể hiện xác suất mua qua bốn nhóm thu nhập, đường nối màu đỏ tách biệt rõ, các điểm dữ liệu in đậm, nhãn số liệu phần trăm gắn trực tiếp trên mỗi điểm, giúp so sánh rất dễ giữa các nhóm.
xác suất mua tăng rõ khi đi từ nhóm thu nhập thấp (16,83%) lên nhóm thu nhập cao (26,98%), tức khách hàng thu nhập càng cao càng dễ ra quyết định mua bất động sản. Xu hướng xác suất mua ổn định ở nhóm cao, phản ánh sự gắn kết chặt giữa tiềm lực tài chính và quyết định tiêu dùng bất động sả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.8, size=6) +
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(legend.position = "none") +
theme(plot.title = element_text(size = 12))
p18
Dòng (1)-(5): nhóm dữ liệu theo loại hình bất động sản, tính toán điểm
hài lòng trung bình, xác định điểm nhỏ và lớn nhất rồi cộng/trừ biên để
đảm bảo biểu đồ không bị cắt nhãn.
Dòng (6)-(17): hoán vị trục bằng coord_flip giúp các loại hình bđs nằm bên trục y, dễ kiểm tra giá trị điểm hài lòng, geom_segment vẽ đường thẳng từ đáy lên vị trí điểm, geom_point biểu diễn từng loại bằng màu riêng, geom_text dán nhãn giá trị, thuận tiện đối chiếu trực tiếp số liệu.
Dòng (18)-(24): hoàn thiện tiêu đề, nhãn x/y rõ ràng, ẩn chú giải màu vì biểu đồ chỉ gán màu trực tiếp từng điểm trên trục nên không cần chú giải riêng.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ scatter dạng hoán vị trục, mỗi điểm gắn màu riêng, nhãn số liệu dán trên từng điểm giúp đọc giá trị trung bình của từng loại nhà dễ dàng. Các đường kẻ từ chân trục tới từng điểm tăng cảm giác đối chiếu trực quan, thứ tự loại nhà được sắp theo giá trị điểm hài lòng tăng dần, rất thuận tiện để so sánh thứ bậc.
Apartment đạt điểm hài lòng trung bình cao nhất (5,516), tiếp theo là villa (5,511), studio (5,498). các loại farmhouse, townhouse, independent house đều thấp hơn (5,486–5,492), khoảng chênh giữa các nhóm khá nhỏ, nhưng rõ ràng khách chọn apartment, villa hài lòng nhất trong tập số liệu, còn loại farmhouse ghi nhận hài lòng thấp nhất.
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() +
theme(
legend.position = "right",
legend.justification = c(0, 1),
legend.box.margin = margin(0, 30, 0, 0)
)
p19
Dòng (1)-(5): nhóm dữ liệu theo loại hình bất động sản, đếm số lượng
từng loại nhà, tính toán tỉ trọng từng loại so với tổng; sắp xếp lại
bảng theo thứ tự giảm dần để vị trí nhãn đặt chính xác.
Dòng (6)-(10): xây dựng biểu đồ tròn bằng cách vẽ bar chart một biến y dạng phần trăm theo nhóm fill (loại nhà), mỗi lát cắt (nhóm loại nhà) được phân màu riêng và đặt nhãn phần trăm ngay vị trí trung tâm lát, giúp đọc tỉ lệ nhanh gọn.
Nhóm (11)-(14): đặt tiêu đề rõ ràng, chú giải màu cho các loại hình, tắt trục x/y bằng x = NULL, y = NULL, theme_void tạo bố cục thoáng sạch hoàn toàn, đúng tiêu chuẩn minh họa pie chart.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ tròn (pie chart) sắc nét, chia đều 6 lát cho 6 loại bất động sản: Apartment, Farmhouse, Independent house, Studio, Townhouse, Villa, mỗi lát cắt được mã màu riêng rõ ràng và nhãn phần trăm in đậm ở giữa các phần của bánh, giúp việc so sánh tỷ lệ dễ dàng, chú giải màu đặt ngay bên phải với tên loại nhà song song từng màu lát bánh.
Tỷ lệ phân bổ các loại hình bất động sản cực kỳ cân bằng, 5 nhóm đều đạt 16,7%, nhóm “Farmhouse” nhỉnh hơn chút (16,8%) và “studio” thấp hơn nhẹ (16,5%), nhưng chênh lệch thực tế rất nhỏ. Phân bổ đồng đều này cho thấy dữ liệu đã được chọn mẫu hoặc gán nhãn cân bằng giữa các loại hình bất động sản.
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 (USD)",
y = "Giá (USD)"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(size = 12)) +
theme(legend.position = "bottom")
p20
## `geom_smooth()` using formula = 'y ~ x'
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”.
Dòng (6)-(13): tạo scatter plot nhưng chỉ vẽ đường hồi quy tuyến tính cho từng nhóm quyết định (không vẽ điểm), mỗi nhóm một màu riêng theo color_scheme, trục x/y chuẩn hóa định dạng kế toán, giúp đọc giá và khoản vay trực quan, không nhầm lẫn về đơn vị lớn nhỏ. đường hồi quy dùng method=“lm” (hồi quy tuyến tính), fill màu nhẹ thể hiện dải bối cảnh hai nhóm.
Ý nghĩa kĩ thuật và thống kê
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.
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. 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.
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
df_raw <- read_xlsx("C:/Users/Admin/Downloads/HVN_BCTC.xlsx",sheet = "Income Statement")
## New names:
## • `` -> `...1`
## • `` -> `...10`
Dòng code này đọc sheet “Income Statement” trong file excel, đường dẫn nằm trong ổ đĩa cá nhân. kết quả trả về là một bảng dữ liệu (dataframe) lưu vào biến df_raw.
Kết quả : df_raw có 25 dòng dữ liệu và 44 cột.
Ý nghĩa kĩ thuật và thống kê lệnh đọc đúng cú pháp, hợp lý cho việc nhập dữ liệu từ file excel, giữ nguyên cấu trúc bảng báo cáo tài chính từ sheet “income statement” vào biến df_raw dưới dạng dataframe.
Dữ liệu nhập chuẩn giúp ta phân tích trực tiếp các chỉ số tài chính, biến động doanh thu - lợi nhuận đúng bản chất bảng gốc, loại bỏ sai sót khi nhập tay và đảm bảo kết quả xử lý tiếp theo phản ánh trung thực số liệu thực tế doanh nghiệp.
dim(df_raw)
## [1] 25 44
Dòng code trên dùng xác định kích thước của đối tượng data.frame có tên là df_raw, trong đó giá trị đầu tiên là số hàng và giá trị thứ hai là số cột.
Kết quả: bảng dữ liệu có 25 hàng và 44 cột.
Ý nghĩa kĩ thuật và thống kê
Kết quả cho biết df_raw có 25 dòng và 44 cột, đúng cấu trúc rộng chuyên biệt cho dạng bảng báo cáo kết quả kinh doanh, xác nhận việc nhập dữ liệu từ file excel hoàn toàn chuẩn.
head(df_raw,5)
Dòng code trên thực hiện kiểm tra sơ bộ, xác minh trực quan rằng dữ liệu ban đầu đã được nạp chính xác.
Kết quả: 5 dòng đầu tiên của bảng df_raw sẽ được hiển thị đầy đủ với toàn bộ 44 cột dữ liệu, bao gồm các cột và thông tin đúng như dữ liệu gốc từ sheet “income statement”.
Ý nghĩa kĩ thuật và thống kê
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.
tail(df_raw,5)
Dòng code này có mục đích xem 5 hàng cuối cùng của dữ liệu (df_raw) nhằm 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.
Kết quả: 5 dòng cuối cùng của bảng df_raw hiển thị đủ 44 cột, phản ánh các chỉ tiêu tài chính cuối bảng báo cáo.
Ý nghĩa kĩ thuật và thống kê
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.
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 mốc thời gian, 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.
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"
Dòng code trên 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.
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…).
Ý nghĩa kĩ thuật và thống kê
Dữ liệu có đủ tên biến theo kiểu dòng thời gian, gồm cả các cột năm (“2017”, “2018”,…), quý từng năm (“Q1 2017”, “Q2 2017”,…), các quý đến tận “Q2 2025”.
cấu trúc biến theo hàng năm, từng quý và tổng hợp cho phép phân tích sâu sát hiệu quả kinh doanh từng giai đoạn cụ thể, nhìn được xu hướng tài chính từ năm, từng quý và nhiều chu kỳ gần nhau, cắt nghĩa chính xác biến động doanh thu, lợi nhuận, hoặc kiểm tra tăng trưởng qua từng kỳ tài chính dài hạn
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
Dòng code trên để 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.
Kết quả: các cột số năm và quý từ 2017 tới 2025 đều đủ 25 giá trị, riêng cột số 10 là cột trống hoàn toàn (0 giá trị).
Ý nghĩa kĩ thuật và thống kê
Câu lệnh chuẩn giúp rà dữ liệu nhanh, phát hiện cột trống để loại bỏ, đảm bảo bảng chỉ giữ biến thật sự có số liệu hữu ích cho phân tích tiếp theo.
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.
summary(df_raw[["2020"]])
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -1.240e+12 -8.073e+08 3.913e+03 1.464e+11 2.397e+11 1.689e+12
Dòng code này dùng để xem thống kê nhanh về giá năm năm 2020 của dataframe (df_raw).
Kết quả summary cho cột năm 2020:
Min. (-1.240e+12): Chỉ tiêu thấp nhất năm 2020 là -1.240 × 10¹² (âm rất lớn, có thể là lỗ/lỗ lũy kế/thua lỗ đặc biệt của một chỉ tiêu).
1st Qu. (-8.073e+08): 25% chỉ tiêu có giá trị nhỏ hơn -807 triệu.
Median (3.913e+03): Trung vị của các chỉ tiêu chỉ 3.913.
Mean (1.464e+11): Trung bình cộng bằng 146,4 tỷ (dương lớn, do một số chỉ tiêu rất lớn kéo lên).
3rd Qu. (2.397e+11): 75% chỉ tiêu nhỏ hơn 239,7 tỷ.
Max. (1.689e+12): Chỉ tiêu cao nhất năm 2020 là 1.689 × 10¹² (1.689 nghìn tỷ, rất lớn).
Ý nghĩa kĩ thuật và thống kê
Các chỉ tiêu tài chính năm 2020 thể hiện sự biến động rất mạnh giữa các mục, trong đó xuất hiện giá trị âm lớn (thua lỗ, chi phí bất thường) và giá trị dương cực đại. Trung vị thấp cho thấy đa số chỉ tiêu bình thường quanh mức nhỏ, còn giá trị trung bình bị kéo lên bởi một số outlier cực lớn. Mô tả này giúp xác định cần kiểm tra outlier, chuẩn hóa dữ liệu, hoặc phân nhóm trước khi dự báo hay kiểm định mẫu, đảm bảo kết quả phân tích đúng thực tế hoạt động tài chính doanh nghiệp.
names(df_raw) <- str_trim(names(df_raw))
names(df_raw) <- make.names(names(df_raw))
Dòng code (1) và (2) đã bỏ khoảng trắng đầu cuối và chuyển đổi toàn bộ về cú pháp chuẩn (không dấu cách, không ký hiệu lạ), gán tên hợp lệ để dùng lệnh trong R thuận tiện hơn. Vì hai dòng code này chỉ tác động ngầm và không in ra kết quả,
Ý nghĩa kĩ thuật và thống kê
Các dòng code hợp lý, làm sạch tên biến đảm bảo thao tác, truy xuất biến không bị lỗi do khoảng trắng, ký tự lạ. giúp dùng các hàm thao tác dữ liệu hoặc trích xuất cột chính xác, tránh bị lỗi cú pháp hoặc phải nhớ tên cột phức tạp. Việc chuẩn hóa tên giúp phân tích nhanh, giảm sai lệch khi gọi lệnh tổng hợp hoặc lọc biến, đảm bảo toàn bộ các phép tính, báo cáo tài chính về sau phản ánh đúng dữ liệu, không bị nhầm lẫn giữa các biến do tên không thống nhất.
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ó
Dòng code (1) và (2) 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 cho người dùng biết, thay vì để script bị lỗi.
Kết quả: trả về thông báo “Không tìm thấy cột Date— thử tạo từ Year & Month nếu có”“.
Ý nghĩa kĩ thuật và thống kê
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.
if ("Indicator" %in% names(df_raw)) {
n_distinct(df_raw$Indicator)
} else {
message("Không có cột Indicator!")
}
## Không có cột Indicator!
Khối code trê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 để đếm; nếu không, nó sẽ thông báo cho người dùng.
Kết quả: không tìm thấy cột Indicator trong bảng dữ liệu, câu lệnh hiển thị thông báo rõ cho người dùng.
ý nghĩa kĩ thuật và thống kê
Thông báo “Không tìm thấy cột Indicator” 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. Đ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. 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.
dt <- df_raw %>% as_tibble()
Dòng code trên chuyển đổi đối tượng df_raw từ kiểu data.fram sang kiểu tibble và gán kết quả vào một biến mới tên là dt.
Kết quả: lệnh này tạo ra dt là một bản sao hoàn toàn mới của df_raw, không chuyển đổi sang dạng khác, không thay đổi cấu trúc hay kiểu dữ liệu gốc.
Ý nghĩa kĩ thuật và thống kê Lệnh hợp lý khi bạn cần thao tác song song hoặc xử lý khác biệt trên hai bảng mà không ảnh hưởng dữ liệu gốc. Tạo bản sao giúp bạn thử nghiệm các thao tác, kiểm định hoặc biến đổi dữ liệu mà không sợ làm sai lệch hoặc ghi đè bảng gốc, đảm bảo kết quả phân tích so sánh được kiểm soát minh bạch, đối chiếu dễ dàng theo từng trường hợp.
names(dt)[1] <- "Indicator"
names(dt)[1:5]
## [1] "Indicator" "X2017" "X2018" "X2019" "X2020"
Dòng code (1) gán hoặc sửa 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 dt thành chuỗi ký tự “Indicator”.
Dòng code (2): xem tên của 5 cột đầu tiên Kết quả: tên cột đầu tiên của dt đã đổi thành Indicator.
Ý nghĩa kĩ thuật và thống kê
Lệnh đúng cú pháp, đổi tên trực tiếp vị trí cột đầu tiên về Indicator, thao tác gọn nhanh, tiện lợi khi không nhớ tên cột gốc hoặc bảng có cấu trúc cố định. Đặ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.
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).
pivot_longer(cols = -Indicator,names_to = “Period”,values_to = “Value”) dùng pivot_longer chuyển tất cả các cột trừ indicator thành hai cột mới là period và value, giúp gom giá trị của các kỳ (năm, quý) thành một cột, biến cấu trúc bảng rộng sang bảng dài gọn.
filter(!is.na(Value)): lọc bỏ các dòng không có giá trị value (bị thiếu), đảm bảo dữ liệu chỉ còn những quan sát đầy đủ, loại trừ lỗi do thiếu số.
mutate(Period = str_squish(Period), Indicator = str_squish(Indicator)): làm sạch khoảng trắng thừa trong Period và Indicator.
Ý nghĩa kĩ thuật và thống kê
Đ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).
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.
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.
*Ý nghĩa kĩ thuật và thống kê
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. Việc chuyển đổi, sinh biến (Year, Quarter, Month, Date) đã hoàn tất, định dạng thời gian đầy đủ. 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 code 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.
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.
Ý nghĩa kĩ thuật và thống kê
Đảm bảo dữ liệu đầu vào là tập các quan sát duy nhất, giúp mọi phép thống kê trả về kết quả chính xác, đồng thời tối ưu kích thước file. Ngăn chặn việc lặp lại cùng một thông tin nhiều. Đả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))
Dòng code trên 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.
Ý nghĩa kĩ thuật và thống kê
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ố.
dt2 <- dt2 %>%
mutate(LogValue = ifelse(Value > 0, log(Value), NA_real_))
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.
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; 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.
Ý nghĩa kĩ thuật và thống kê
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. 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.
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.
Ý nghĩa kĩ thuật và thống kê
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. 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.
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.
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.
Ý nghĩa kĩ thuật và thống kê
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.” 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.
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()
Đ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.
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ố đó.
Ý nghĩa kĩ thuật và thống kê
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. 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ô.
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()
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.
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).
Ý nghĩa kĩ thuật và thống kê
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ị.
dt2 <- dt2 %>%
group_by(Indicator) %>%
mutate(Value_z = as.numeric(scale(Value))) %>%
ungroup()
dt2 %>%
select(Value_z) %>%
head()
Đoạn mã dùng để chuẩn hoá biến Value theo z-score cho từng Indicator (chỉ tiêu).
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ị
Ý nghĩa kĩ thuật và thống kê
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. Ở 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.
dt2 <- dt2 %>%
mutate(DayOfYear = yday(Date))
dt2 %>%
select(Indicator, Date, DayOfYear) %>%
head()
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.
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
Ý nghĩa kĩ thuật và thống kê
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).
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ế.
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.
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.
Ý nghĩa kĩ thuật và thống kê
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.
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á.
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.
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.
Ý nghĩa kĩ thuật và thống kê
Biến LogValue đã được xử lý hoàn chỉnh: các giá trị NA đã được thay thế bằng giá trị trung vị (median) của LogValue trong từng nhóm Indicator.
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.
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 và YoY bằng giá trị trung vị 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.
group_by(Indicator): Nhóm toàn bộ dữ liệu theo từng giá trị trong cột Indicator. 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ị.
Ý nghĩa kĩ thuật và thống kê
Tất cả giá trị NA trong hai biến QoQ và YoY đều đã được thay thế bằng giá trị trung vị tính trong từng nhóm Indicator. Trường QoQ và YoY 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.
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.
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ê. 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.
Ý nghĩa kĩ thuật và thống kê
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. Giá trị Value_z đã đầy đủ cho phép so sánh mức độ “chênh lệch” 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.
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.
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.
Ý nghĩa kĩ thuật và thống kê
Đ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. 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.
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 , QoQ , YoY, Value_z 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.
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.
Ý nghĩa kĩ thuật và 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. 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.
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.
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.
Ý nghĩa kĩ thuật và thống kê
Mọi giá trị NA ở các trường logarit, biến động tăng trưởng, chuẩn hóa đề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). 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.
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.
is.na(dt2): Trả về một bảng logic dạng TRUE/FALSE báo NA ở từ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.
Ý nghĩa kĩ thuật và thống kê
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. 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 và YoY đề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ỳ.
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, số cột của dataframe dt2.
Dòng code (1): lấy ra số dòng và cột của bảng dt2 dưới dạng vector; dòng code (2): ghép số dòng và cột thành một chuỗi định dạng rõ ràng.
Ý nghĩa kĩ thuật và thống kê
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ố). 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.
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 |
Dòng code (1): lấy số lượng chỉ tiêu tài chính khác nhau trong dt2. dòng code (2): đếm số lần xuất hiện từng chỉ tiêu, sắp xếp giảm dần. dòng (3): hiển thị bảng 10 chỉ tiêu có nhiều quan sát nhất kèm chú thích tổng số Indicator. Ý nghĩa kĩ thuật và thống kê
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). 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.
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ã trên dùng để đếm tổng số giá trị thiếu (NA) ở từng cột trong bảng dữ liệu dt2.
Dòng code (1):tính tổng số giá trị NA trên mỗi cột trong bảng dt2.
Dòng code (2): in ra bảng tổng hợp số NA từng biến dưới dạng bảng, hỗ trợ phát hiện nhanh cột nào có vấn đề về thiếu dữ liệu. Ý nghĩa kĩ thuật và thống kê
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.
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 đủ.
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 |
Hàm summarise lấy số lượng dòng, tính trung bình, trung vị, độ lệch chuẩn, giá trị nhỏ nhất và lớn nhất của biến value trong dt2. Hàm kable: in ra bảng kết quả thống kê mô tả, hiển thị trực tiếp các chỉ số tổng quát cho toàn bộ biến value.
*Ý nghĩa kĩ thuật và thống kê
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.
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 |
Dòng code trên tổng hợp nhanh các chỉ số trung bình và độ lệch chuẩn cho ba biến: LogValue , YoY , QoQ .
Ý nghĩa kĩ thuật và thống kê
Giá trị trung bình logarit là 15.61, độ lệch chuẩn logarit 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 là 154.91 và độ lệch chuẩn 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 hoặc các kỳ khác nhau. Trung bình QoQ là 90.619,57 và độ lệch chuẩn 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
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 |
summarise(mean = mean(Value, na.rm=TRUE)): tính giá trị trung bình của value cho từng nhóm indicator, giúp xác định chỉ tiêu tài chính nào có trung bình cao nhất hoặc thấp nhất trên toàn dữ liệu.
summarise(sd = sd(Value, na.rm=TRUE)): tính độ lệch chuẩn của value từng nhóm indicator, phản ánh mức độ biến động dữ liệu quanh giá trị trung bình.
Ý nghĩa kĩ thuật và thống kê
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. Các chỉ tiêu chính như doanh thu, chi phí, giá vốn đều có xu hướng giá trị lớn và biến động nhiều, cho thấy quy mô hoạt động tài chính 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.
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.
Dòng code group_by(year): nhóm dữ liệu theo từng năm. Dòng code summarise: lấy số lượng dòng và trung bình value cho từng nhóm năm. Dòng code arrange(year): sắp xếp kết quả theo thứ tự năm tăng dần. Dòng kable: in bảng số dòng và giá trị trung bình cho từng năm.
Ý nghĩa kĩ thuật và thống kê
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 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.
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.
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ụ .
group_by(Quarter): Gom nhóm dữ liệu theo từng quý (Q1, Q2, Q3, Q4).
summarise(n = n(), mean_value = mean(Value, na.rm = TRUE)): đếm số dòng và tính trung bình value cho từng 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.
Ý nghĩa kĩ thuật và thống kê
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 obs. Giá trị trung bình của Value phân theo quý: Q1: 3,82e11, Q2: 4,46e11, Q3: 4,23e11, Q4: 4,65e11.
Trung bình Value tăng khá rõ từ Q1 đến 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.
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 |
Đ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.
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).
Ý nghĩa kĩ thuật và thống kê
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.
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.
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 (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.
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).
Ý nghĩa kĩ thuật và thống kê
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). Các chỉ tiêu có độ lệch mạnh thường là nhóm nhạy cảm nhất với biến động tài chính 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.
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. 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.
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.
Ý nghĩa kĩ thuật và thống kê
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ý. 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.
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ế.
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 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).
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.
Ý nghĩa kĩ thuật và thống kê
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. Tương quan âm ở lag 1 cho thấy hoạt động kinh doanh 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.
*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.
Ý nghĩa kĩ thuật và thống kê
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).
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ế.
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 và độ lệch chuẩn động 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.
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.
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.
Ý nghĩa kĩ thuật và thống kê
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ỳ.
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.
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 của biến Value theo thời gian cho top 5 chỉ tiêu có nhiều quan sát nhất.
top5inds: 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.
Ý nghĩa kĩ thuật và thống kê
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.
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ê.
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.
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.
Ý nghĩa kĩ thuật và thống kê
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).
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, kiểm định sự khác biệt trung bình Value giữa các nhóm Quarter(quý).
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.
Ý nghĩa kĩ thuật và thống kê
Chỉ tiêu Value được phân tích phương sai qua 4 nhóm quý, 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ý 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).
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. 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 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.
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)): tính số dòng và tỷ lệ quan sát YoY lớn hơn 0 của từng nhóm chỉ tiêu tài chính.
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.
Ý nghĩa kĩ thuật và thống kê
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 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.
group_by(Indicator): Gom nhóm dữ liệu theo từng Indicator để tính toán riêng biệt.
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.
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.
Ý nghĩa kĩ thuật và thống kê
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, giá trị trung bình và độ lệch chuẩn phân tán mạnh theo nhóm. 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.
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
Mục tiêu hàm code là chọn ra 8 nhóm chỉ tiêu 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.
dt2%>% count(Indicator, sort = TRUE): Đếm số lượng dòng 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.
Ý nghĩa kĩ thuật và thống kê
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ê.
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 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.
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.
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.
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ị.
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.
Ý nghĩa kĩ thuật và thống kê
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.
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.
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) +
geom_line(aes(group = 1), color = "steelblue", size = 1) +
geom_point(size = 1, color = "black") +
geom_smooth(method = "loess", se = FALSE, color = "darkred", size = 1.2) +
geom_text_repel(
aes(label = scales::comma(round(Total, 0))),
size = 3,
nudge_y = 5e11,
max.overlaps = 20
) +
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'
Giải thích
geom_line(aes(group = 1), color = “steelblue”, size = 1): vẽ đường nối các điểm value, thể hiện sự thay đổi tổng value qua từng năm để người xem dễ nhận diện xu hướng tăng/giảm liên tục.
geom_smooth(method = “loess”, se = FALSE, color = “darkred”, size = 1.2): vẽ đường cong loess phản ánh xu hướng tổng value qua các năm một cách mượt mà, dự báo được diễn biến tổng quát không bị lệch bởi biến động nhỏ lẻ, màu đỏ thẫm nổi bật.
labs(title = “(P1) Tổng giá trị theo năm”, x = “Năm”, y = “Tổng Value”): đặt tiêu đề, nhãn trục x và y đầy đủ, hỗ trợ thuyết minh và trình bày báo cáo rõ ràng, ý nghĩa.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ dùng cột màu skyblue, điểm đen, đường nối giá trị từng năm và đường xu hướng loess rõ ràng, nhãn số tự động tách giúp dễ đọc, tổng value từng năm thể hiện đầy đủ, không che khuất dữ liệu, bố cục trực quan hài hòa. Tổng value tăng đều đến năm 2023, đạt đỉnh, sau đó giảm mạnh năm 2024, đường loess cho thấy xu hướng tăng dài hạn rồi đảo chiều, phát hiện nhanh năm bất thường và nguy cơ tụt giá trị tài chính về các năm gần nhất.
summary_data <- dt2 %>%
group_by(Indicator) %>%
summarise(mean_val = mean(Value, na.rm = TRUE))
p2 <- ggplot(dt2 %>% 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 / 1e12, 2))),
nudge_y = 2e11, size = 3, color = "black", fontface = "bold") +
coord_flip(clip = "off") +
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),
axis.title.y = element_text(size = 9),
axis.text.x = element_text(size = 8),
axis.title.x = element_text(size = 9),
plot.margin = margin(1, 3, 1, 1, "cm"))
print(p2)
plots[[i]] <- p2; i <- i + 1
Giải thích
geom_boxplot(alpha = 0.6, outlier.shape = 21, outlier.size = 2, outlier.color = “red”, show.legend = FALSE): vẽ biểu đồ hộp cho từng nhóm chỉ tiêu, thể hiện phân bố value, thiết lập độ trong suốt 0.6 giúp nhìn lớp chồng, các điểm ngoại lai đánh dấu màu đỏ nổi bật với kích thước lớn hơn để nhận diện nhanh trường hợp bất thường.
geom_jitter(width = 0.15, alpha = 0.15, size = 0.8, show.legend = FALSE): bổ sung các điểm dữ liệu thực tế rải nhẹ sang ngang, tránh chồng lấn, tạo thành lớp phân tán trên các boxplot và giúp xem mật độ phân phối điểm từng nhóm rõ hơn.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cho thấy doanh thu bán hàng và cung cấp dịch vụ là chỉ tiêu vượt trội, áp đảo hoàn toàn các mục còn lại, xác lập vai trò nguồn thu cốt lõi của doanh nghiệp. Các chỉ tiêu chi phí như chi phí bán hàng, quản lý, tài chính… đều ở mức thấp hơn nhiều lần, thể hiện tỷ trọng chi phí kiểm soát tốt trong cơ cấu hoạt động chính.
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) +
stat_summary(fun = median, geom = "point", color = "white", fill = "black",
size = 1, shape = 21, stroke = 0.2) +
geom_vline(xintercept = median(dt2$Value, na.rm = TRUE),
linetype = "dashed", color = "gray50", size = 0.5) +
theme_style +
theme(legend.position = "none") +
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
Giải thích
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.
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.
Ý nghĩa kĩ thuật và thống kê
Phân phối giá trị giữa các chỉ tiêu chủ yếu tập trung thấp, hẹp và lệch phải; riêng doanh thu có phổ rộng kèm một số outlier nổi bật. Hầu hết chi phí bám quanh mức nhỏ, median thấp, biến động nhẹ—cho thấy cấu trúc dữ liệu có một nhóm rất lớn áp đảo và các nhóm còn lại cùng tập trung quanh giá trị thấp.
p4 <- ggplot(dt2 %>% filter(Indicator %in% top_inds[1:4]),
aes(x = Value / 1e12, fill = Indicator)) +
geom_histogram(aes(y = ..density..), bins = 25, alpha = 0.5, color = "white") +
geom_density(alpha = 0.3) +
geom_vline(aes(xintercept = mean(Value, na.rm = TRUE) / 1e12), color = "red") +
facet_wrap(~Indicator, scales = "free", ncol = 2) +
labs(title = "(P4) Phân bố Value (Histogram + Density)",
x = "Value (nghìn tỷ VND)", y = "Mật độ") +
theme_style + theme(legend.position = "none")
print(p4); plots[[i]] <- p4; i <- i + 1
Giải thích
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=25, alpha=0.5, color=“white”): Vẽ histogram với số bins vừa phải (25), 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.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ histogram và đường mật độ cho thấy các nhóm chỉ tiêu chi phí đều có giá trị tập trung ở khu vực gần 0 và chủ yếu trong biên hẹp; nổi bật là chi phí quản lý và lãi vay vẫn xuất hiện một số giá trị âm lớn, phản ánh các khoản biến động hoặc giảm giá bất thường. Chi phí bán hàng và chi phí khác đa số tích lũy ở mức dương thấp, cơ cấu tài chính ổn định, kiểm soát tốt chi phí vận hành.
p5 <- ggplot(dt2, aes(x = LogValue, y = Value, color = Year)) +
geom_point(alpha = 0.6, size = 2) +
geom_smooth(method = "loess", se = TRUE, color = "darkred", linewidth = 1) +
geom_smooth(method = "lm", se = FALSE, linetype = "dashed", color = "gray40", linewidth = 0.8) +
geom_text_repel(data = dt2 %>% slice_max(Value, n = 5),
aes(label = Indicator), size = 3, color = "black",
box.padding = 0.3, max.overlaps = 6) +
scale_y_continuous(labels = scales::label_number(scale_cut = scales::cut_short_scale())) +
scale_color_viridis_c(option = "plasma", end = 0.9) +
labs(title = "(P5) Mối quan hệ giữa Log(Value) và Value",
x = "Log(Value)", y = "Value (VND)", color = "Năm") +
theme_style
print(p5); plots[[i]] <- p5; i <- i + 1
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Giải thích
geom_smooth(method = “loess”, se = TRUE, color = “darkred”, linewidth = 1): vẽ đường xu hướng cong kiểu nội suy loess cho dữ liệu (logvalue và value), cho phép nhận diện xu thế tổng quát và biến động tự nhiên.
geom_smooth(method = “lm”, se = FALSE, linetype = “dashed”, color = “gray40”, linewidth = 0.8): vẽ đường hồi quy tuyến tính chuẩn kết nối giá trị logvalue và value, không vẽ vùng tin cậy (se = FALSE), đường được tô nét đứt (linetype = dashed) giúp so sánh nhanh xu hướng tổng thể với thực tế, từ đó đánh giá độ phù hợp (linear fit) giữa hai biến.
Ý nghĩa kĩ thuật và thống kê
Phân tích scatter và hai đường hồi quy cho thấy mối quan hệ dương giữa Value và log(Value): nhóm value lớn nhất (vài ngàn tỷ đến vài triệu tỷ) thuộc các năm sau, chủ yếu tạo thành đuôi phải của phân phối. Dữ liệu xuất hiện nhiều điểm tập trung quanh giá trị nhỏ, còn outlier tạo thành sự phân bố lệch phải. Mã màu các năm củng cố thêm xu thế ngân sách, doanh thu tăng dần về cuối giai đoạn, đồng thời cho phép nhận diện những năm bất thường hoặc đột biến.
p6 <- dt2 %>%
group_by(Indicator, Year) %>%
summarise(meanV = mean(Value, na.rm = TRUE), .groups = "drop") %>%
ggplot(aes(Year, reorder(Indicator, meanV), fill = meanV)) +
geom_tile(color = "white", height = 0.7) +
geom_text(aes(label = ifelse(meanV > 1e10, round(meanV/1e9, 1), "")),
size = 3, color = "black", fontface = "bold") +
scale_fill_viridis_c(option = "C", direction = -1) +
geom_vline(xintercept = 2020, lty = "dashed", color = "gray40") +
labs(title = "(P6) Heatmap giá trị trung bình theo năm",
x = "Year", y = NULL, fill = "Giá trị TB (VND)") +
theme_style +
theme(
axis.text.x = element_text(size = 9, angle = 45, hjust = 1),
axis.text.y = element_text(size = 8),
legend.position = "bottom",
panel.spacing.y = unit(0.6, "lines")
)
print(p6); plots[[i]] <- p6; i <- i + 1
Giải thích
geom_tile(color = “white”, height = 0.7): vẽ các ô màu (heatmap) để hiển thị mức trung bình value cho từng (indicator, year). mỗi ô thể hiện giá trị trung bình, giúp so sánh trực quan biến động giá trị giữa các chỉ tiêu qua từng năm.
geom_text(aes(label = ifelse(meanV > 1e10, round(meanV/1e9, 1), ““)), size = 3, color =”black”, fontface = “bold”): thêm nhãn số vào từng ô heatmap, chỉ hiện số khi giá trị lớn hơn 10.000.000.000.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ heatmap giá trị trung bình cho thấy doanh thu, lợi nhuận và một số chỉ tiêu trọng điểm giữ mức cao và ổn định trong hầu hết các năm. Một số mục chi phí, lỗ hoặc khoản mục khác chủ yếu duy trì giá trị nhỏ hoặc thấp hơn hẳn so với các chỉ tiêu chính. Năm 2020 là mốc đặc biệt với biến động rõ nét ở nhiều chỉ tiêu, điều này phản ánh tác động của biến động kinh tế vĩ mô hoặc sự kiện bất thường ảnh hưởng toàn ngành.
p7 <- ggplot(dt2 %>% filter(Indicator %in% top_inds),
aes(Date, Value, color=Indicator, fill=Indicator)) +
geom_line(linewidth=.8) + geom_point(size=1.2) +
geom_smooth(se=FALSE, lty="dotted", lw=.6, alpha=.7) +
labs(title="(P7) Xu hướng theo thời gian", x="Date", y="Value") +
theme_style + theme(legend.position="bottom",
legend.text=element_text(size=6),
legend.title=element_text(size=7),
legend.key.size=unit(.5,"lines")) +
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'
Giải thích
geom_point(size=1.2): 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.
labs(…): Tiêu đề, trục x/y rõ ràng, tiếng Việt nhất quán.
theme_style: Trình bày biểu đồ tối giản, nhất quán thẩm mỹ chuyên nghiệp.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ p7 cho thấy doanh thu luôn nổi bật vượt trội so với các chỉ tiêu còn lại, với các đỉnh lớn xuất hiện đều đặn hàng năm, phản ánh mùa vụ hoặc sự kiện tăng trưởng đột biến. Các chỉ tiêu chi phí (quản lý, bán hàng, vay, thuế…) duy trì ở mức ổn định hơn nhiều lần, nằm sát trục 0 và gần như không có tăng/giảm đột biến lớn xuyên suốt giai đoạn.
p8 <- dt2 %>%
group_by(Year) %>%
summarise(mean_YoY = mean(YoY, na.rm = TRUE)) %>%
ggplot(aes(x = Year, y = mean_YoY)) +
geom_line(color = "#2E86AB", size = 1) +
geom_point(color = "#E74C3C", size = 2) +
geom_smooth(method = "lm", se = FALSE, color = "gray50", linetype = "dashed") +
labs(
title = "(P8) Xu hướng trung bình YoY qua các năm",
x = "Năm", y = "YoY trung bình (%)"
) +
theme_style
print(p8)
## `geom_smooth()` using formula = 'y ~ x'
Giải thích
geom_line(color = “#2E86AB”, size = 1): vẽ đường nối thể hiện xu hướng thay đổi giá trị trung bình YoY qua các năm, giúp dễ dàng quan sát sự tăng giảm theo thời gian với màu sắc và độ dày phù hợp.
geom_point(color = “#E74C3C”, size = 2): thêm các điểm dữ liệu từng năm lên đường xu hướng, làm nổi bật từng giá trị năm cụ thể, giúp người xem nhận biết chi tiết từng năm.
geom_smooth(method = “lm”, se = FALSE, color = “gray50”, linetype = “dashed”): vẽ đường hồi quy tuyến tính tổng thể cho dữ liệu YoY theo năm, đường nét đứt màu xám giúp xác định xu hướng chung và so sánh nhanh với biến động thực tế.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ xu hướng trung bình YoY qua các năm cho thấy giai đoạn 2017–2020 xảy ra biến động rất mạnh với các năm tăng giảm đột biến (năm 2020 ghi nhận spike dương lớn). Sau các năm này, YoY trung bình trở về dao động quanh 0 và duy trì ổn định, không còn biến động lớn mà có xu hướng bám gần trục hoành. Đường hồi quy tuyến tính (xám) xác nhận toàn giai đoạn này sự phục hồi nhẹ của YoY trung bình, dù mức tăng còn thấp so với các năm đầu.
p9data <- dt2 %>%
group_by(Indicator) %>%
summarise(m = mean(Value, na.rm = TRUE), .groups = "drop")
p9 <- ggplot(p9data, aes(x = reorder(Indicator, m), y = m / 1e9)) +
geom_segment(aes(xend = Indicator, y = 0, yend = m / 1e9), color = "gray70") +
geom_point(size = 3, color = "tomato") +
geom_text(aes(label = round(m / 1e9, 1)),
hjust = -0.1, size = 3, color = "black", fontface = "bold") +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
coord_flip(clip = "off") +
labs(title = "(P9) Trung bình Value theo Indicator (Lollipop chart)",
x = NULL, y = "Giá trị trung bình (tỷ VND)") +
theme_style +
theme(
plot.margin = margin(10, 50, 10, 10), # tăng lề phải
axis.text.y = element_text(size = 8)
)
print(p9) ; plots[[i]] <- p9; i <- i + 1
Giải thích
ggplot(p9data, aes(x = reorder(Indicator, m), y = m)): Trục x là Indicator, 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.
Ý nghĩa kĩ thuật và thống kê
Phân bố giá trị trung bình giữa các chỉ tiêu cực kỳ lệch phải, với vài nhóm lớn kéo dài đuôi đồ thị lên cao, đa số các chỉ tiêu tập trung quanh mức nhỏ. Có hiện tượng outlier rõ rệt bên phía doanh thu, trong khi dữ liệu phần còn lại co cụm gần giá trị 0 hoặc âm. Điều này xác nhận rằng tổng thể báo cáo tài chính bị chi phối mạnh bởi một số hoạt động chính, còn phân phối giá trị bình quân cho các nhóm chi phí và khoản mục phụ thuộc ít và ổn định hơn trong dài hạn.
p10 <- dt2 %>%
filter(Indicator %in% top_inds) %>%
group_by(Year, Indicator) %>%
summarise(Total = sum(Value, na.rm = TRUE)) %>%
group_by(Year) %>%
mutate(Share = Total / sum(abs(Total)) * 100) %>%
ggplot(aes(x = factor(Year), y = Share, fill = Indicator)) +
geom_col(position = "fill", color = "white") +
labs(
title = "(P10) Tỷ trọng (%) Value theo Year và Indicator",
x = "Năm", y = "Tỷ trọng (%)"
) +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
scale_fill_brewer(palette = "Set2") +
theme_style +
theme(
legend.position = "bottom",
legend.title = element_blank(),
legend.text = element_text(size = 4.2),
legend.key.size = unit(0.25, "cm"),
legend.box = "horizontal",
legend.spacing.x = unit(0.1, "cm"),
axis.text.x = element_text(angle = 45, hjust = 1),
plot.margin = margin(5, 5, 30, 5)
) +
guides(fill = guide_legend(nrow = 2))
## `summarise()` has grouped output by 'Year'. You can override using the
## `.groups` argument.
print(p10)
Giải thích
geom_col(position = “fill”, color = “white”): vẽ biểu đồ cột tỷ trọng với chiều cao các cột luôn là 100%, mỗi cột chia thành phần trăm giá trị từng chỉ tiêu theo năm; border màu trắng giúp tách từng nhóm rõ ràng, dễ đọc tỉ lệ từng indicator.
scale_y_continuous(labels = scales::percent_format(scale = 1)): chuyển trục y của biểu đồ sang định dạng phần trăm, hiển thị đúng ý nghĩa tỷ trọng từng chỉ tiêu trên tổng giá trị theo năm, làm số trên cột dễ đọc, trực quan.
guides(fill = guide_legend(nrow = 2)): điều chỉnh chú giải màu fill cho indicator thành hai hàng, giúp biểu đồ nhiều nhóm vẫn gọn, dễ xem chú thích, không bị đè hay khuất khi xuất ra với nhiều chỉ tiêu.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ stacked bar tỷ trọng (%) giúp trực quan hóa sự chi phối tuyệt đối của doanh thu bán hàng & cung cấp dịch vụ, chiếm phần lớn trên tổng giá trị từng năm. Các nhóm chi phí và khoản mục phụ khác (bán hàng, quản lý, lãi vay, thuế…) chỉ chiếm tỷ trọng rất nhỏ, màu phân bổ mỏng, sát đáy biểu đồ. Biên độ tỷ trọng giữa các năm khá ổn định, doanh thu luôn dẫn đầu, chỉ một số năm xuất hiện nhích lên nhẹ trong tỷ trọng chi phí hoặc khoản giảm trừ.
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=1) +
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'
Giải thích
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=1) Đá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.
Ý nghĩa kĩ thuật và thống kê
Số liệu trung bình mỗi năm dao động nhưng dần dịch chuyển lên trên, thể hiện qua từng nhãn giá trị rõ ràng (từ khoảng 48 nghìn tỷ lên ~91 nghìn tỷ ở đỉnh cao nhất), với sai số và miền tin cậy thể hiện qua vùng ribbon xám. Đường lm cho thấy mối quan hệ dương mạnh giữa năm và giá trị trung bình – mỗi năm các biến động ngắn hạn xảy ra, nhưng tổng thể mức trung bình có xác suất tăng cao, phản ánh hiệu quả tăng trưởng ổn định và kỳ vọng lạc quan trong phân tích thống kê tài chính dài hạn.
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 = "Năm",
y = "Giá trị"
) +
theme_style
print(p13)
## `geom_smooth()` using formula = 'y ~ x'
plots[[i]] <- p13; i <- i + 1
Giải thích
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.
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.
Kết quả kĩ thuật
Biểu đồ rolling mean cho chỉ tiêu lớn nhất (likely “Doanh thu”) cho thấy xu hướng giảm dần trong những năm gần đây, với các giá trị thực (chấm xanh) biến động mạnh nhưng đường trung bình động 4 quý (đường đỏ) làm mượt biến động ngắn hạn, phản ánh rõ xu thế chung. Khoảng ribbon cho thấy vùng dao động ±5% quanh rolling mean, giúp nhận diện rủi ro biến động trong từng giai đoạn.
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'
Giải thích
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.
Ý nghĩa kĩ thuật thống kê
Phân bố các outlier theo thời gian cho thấy đây là hiện tượng không ngẫu nhiên mà có tính chu kỳ, có thể liên quan đến vụ mùa, điều chỉnh lớn hoặc các sự kiện kinh tế vĩ mô tác động. Số lượng outlier rải rác nhưng tập trung vào những thời điểm nhất định, hình thành các đoạn “đuôi dày” và mở rộng biên độ phân tích (variance), xác nhận cần kiểm soát rủi ro và đánh giá riêng các giá trị quá lớn/quá nhỏ trong phân tích chuyên sâu hoặc dự báo tài chính.
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.3) + geom_line(size=1) +
geom_point() + geom_text(aes(label=round(Cum/1e9,1)), vjust=-2, hjust = 0.65, size=3.5) +
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'
Giải thích
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.
Ý nghĩa kĩ thuật và thống kê
Đường hồi quy tuyến tính (màu đỏ) bám sát đường tổng thực tế và dốc mạnh, xác nhận mối quan hệ dương tuyệt đối giữa thời gian và giá trị tích lũy. Mỗi điểm giá trị cộng dồn đều cao hơn đáng kể so với năm liền trước, không xuất hiện điểm gãy hay đảo chiều—cho thấy phân phối dữ liệu tăng trưởng tích cực và ổn định, phù hợp với doanh nghiệp khỏe mạnh về dòng tiền và năng lực mở rộng sản xuất/kinh doanh qua các chu kỳ.
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'
Giải thích
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_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.
Kết quả kĩ thuật
Biểu đồ theo quý (P16) cho thấy các chỉ tiêu tài chính của doanh nghiệp đều tăng mạnh nhất ở các quý cuối mỗi năm, đặc biệt là Q1 và Q4 thường có giá trị cao hơn hẳn so với Q2 và Q3. Dạng cột phân chia theo năm và quý giúp làm rõ mùa vụ tài chính, sự bứt phá/doanh thu đột biến đúng vào các quý có nhiều hoạt động sản xuất-kinh doanh hoặc tổng kết dự án lớn.
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
Giải thích
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.
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.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cực tròn (polar chart) đã trực quan hóa so sánh nhanh giá trị trung bình của 12 chỉ tiêu tài chính lớn nhất. Hai chỉ tiêu doanh thu áp đảo hoàn toàn, thể hiện bằng chiều dài cực đại của cột và màu sắc nổi bật, phản ánh vai trò then chốt của doanh thu trong tổng cấu trúc tài chính. Nhìn quanh vòng biểu đồ, các chỉ tiêu còn lại như lợi nhuận, hoạt động kinh doanh, tài chính chỉ chiếm 1/3–1/4 giá trị, xác nhận đóng góp quan trọng nhưng không thể so sánh với các chỉ tiêu lõi.
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,7)) +
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")'
Giải thích
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)
Ý nghĩa kĩ thuật vầ thống kê
Bubble plot YoY vs QoQ (P18) thể hiện mức biến động/tăng trưởng cả theo quý (QoQ) và năm (YoY) của dữ liệu tài chính trên cùng một mặt phẳng, với kích thước bong bóng tương ứng giá trị tuyệt đối từng phiên/lần báo cáo. Các điểm giá trị lớn tập trung quanh gốc tọa độ, đa số các quan sát có biến động nhỏ, chỉ một vài bong bóng lớn xuất hiện ở biên extreme của biểu đồ — xác nhận chỉ một số sự kiện/nghiệp vụ tạo ra tăng/giảm vượt chuẩn.
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, fontface = "bold",
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
Giải thích
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õ.
Ý nghĩa kĩ thuật và thống kê
Dữ liệu phân phối quanh Z-Score = 0, tập trung dày ở tâm, xác nhận đa phần chỉ tiêu biến động bình thường, không có outlier. Những điểm có Z lớn kéo trung bình lệch rõ (đuôi dài về hai phía), dẫn đến phân phối phân mảnh và kéo dài — tác động lớn khi tính toán trung bình, phương sai hoặc mô hình hóa tài chính. Các nhãn chỉ tiêu giúp nhận diện nhanh những danh mục cần kiểm soát hoặc giải trình kỹ trong quá trình phân tích số liệu doanh nghiệp.
p20 <- ggplot(dt2 %>% filter(Indicator %in% top_inds),
aes(x = reorder(Indicator, YoY, FUN = median), y = YoY, fill = Indicator)) +
geom_boxplot(alpha = 0.6, outlier.color = "red", outlier.size = 1.5) +
geom_jitter(width = 0.2, alpha = 0.3, size = 1) +
labs(
title = "(P21) Phân bố YoY theo nhóm Indicator",
x = "Indicator",
y = "YoY (%)"
) +
scale_fill_brewer(palette = "Set2") +
theme_style +
theme(
legend.position = "none",
axis.text.x = element_text(size = 7, angle = 45, hjust = 1),
plot.title = element_text(face = "bold", size = 12)
)
print(p20)
plots[[i]] <- p20; i <- i + 1
Giải thích
geom_boxplot(alpha = 0.6, outlier.color = “red”, outlier.size = 1.5) vẽ biểu đồ hộp cho từng nhóm chỉ tiêu, thể hiện phân bố yoy với độ trong suốt 0.6, đánh dấu điểm ngoại lai bằng màu đỏ và size lớn để nổi bật các trường hợp bất thường.
geom_jitter(width = 0.2, alpha = 0.3, size = 1) thêm điểm dữ liệu thật rải nhẹ theo chiều ngang để tránh chồng lấn, độ trong suốt 0.3 cho dễ nhận diện mật độ và kích thước 1 để quan sát rõ điểm phân phối gốc trong nhóm indicator.
scale_fill_brewer(palette = “Set2”) thay đổi màu của các nhóm indicator bằng bảng màu set2, giúp phân biệt rõ các cột trên biểu đồ, dễ so sánh và xem trực quan nhiều nhóm một lúc.
Ý nghĩa kĩ thuật và thống kê
Biểu đồ boxplot YoY theo từng nhóm indicator cho thấy các chỉ tiêu tài chính đều có mức biến động mạnh quanh giá trị trung bình 0, với đa số dữ liệu tập trung ở vùng “hộp” giữa, nhưng vẫn tồn tại nhiều outlier tăng/giảm vượt bậc, minh họa rõ bằng các điểm đỏ ngoài whisker. Việc sắp xếp indicator theo median giúp nhận diện nhóm có tốc độ tăng trưởng bình quân cao nhất/tấp nhất một cách trực quan, thuận lợi cho việc so sánh và nhấn mạnh những chỉ tiêu nổi bật hoặc nhiều rủi ro.
p21 <- dt2 %>%
filter(Indicator %in% top_inds) %>%
group_by(Year, Indicator) %>%
summarise(Total = sum(Value, na.rm = TRUE), .groups = "drop") %>%
ggplot(aes(factor(Year), Total, fill = Indicator)) +
geom_col(alpha = 0.9) +
labs(title="(P21) Tổng Value theo Year & Indicator", x="Năm", y="Tổng Value") +
scale_fill_brewer(palette="Set2") +
theme_style +
theme(
legend.position="bottom",
legend.text=element_text(size=6),
legend.title=element_text(size=7),
legend.key.size=unit(0.4,"cm"),
axis.text.x=element_text(angle=45,hjust=1)
)
p21
Giải thích
geom_col(alpha = 0.9): vẽ cột biểu diễn tổng value từng năm, từng chỉ tiêu trong top_inds, độ trong suốt 0.9 giúp nhìn rõ chồng lên nhau khi có nhiều nhóm, dễ so sánh giá trị giữa các indicator theo thời gian.
scale_fill_brewer(palette = “Set2”): quy định bảng màu cho các cột indicator bằng palette set2, giúp từng nhóm chỉ tiêu có màu riêng biệt, dễ phân biệt và trực quan hơn trên biểu đồ, tránh bị trùng màu khi nhiều nhóm.
theme( legend.position=“bottom”, legend.text=element_text(size=6), legend.title=element_text(size=7), legend.key.size=unit(0.4,“cm”), axis.text.x=element_text(angle=45,hjust=1) ): hiển thị chú giải nằm dưới cùng, chỉnh cỡ chữ, tiêu đề và kích thước ô chú giải nhỏ gọn hơn, đồng thời xoay nhãn năm trên trục x cho dễ đọc, bảng dữ liệu nhiều nhóm/năm vẫn rõ ràng, không bị đè hay che khuất
Ý nghĩa kĩ thuật và thống kê
Biểu đồ cột nhóm (stacked bar) cho thấy doanh thu luôn vượt trội, chi phối tổng giá trị của tất cả các nhóm chỉ tiêu qua các năm. Các chi phí—bán hàng, quản lý, tài chính, lãi vay, các khoản giảm trừ…—chỉ đóng góp tỷ trọng rất nhỏ so với doanh thu và hầu như biến thiên nhẹ, tập trung đông ở vùng sát trục 0. Trong một số năm (2023, 2024), xuất hiện các giá trị âm rõ nét cho các nhóm chi phí, phản ánh giai đoạn tác động bất thường hoặc chi phí tăng mạnh, có thể do ảnh hưởng thị trường hoặc biến động kinh tế vĩ mô.