Cách dùng file này: 1. Mở trong RStudio — chạy từng chunk bằng nút ▶ bên phải mỗi khối code. 2. Đọc phần giải thích trước khi chạy, đọc phần Đọc kết quả sau khi chạy. 3. Điền số thật vào các ô ___ trong phần Ghi nhận kết quả. 4. Bấm Knit → Knit to HTML để xuất toàn bộ báo cáo.


1 Cài đặt & nạp gói (Packages)

1.1 Tại sao cần làm bước này trước?

Gói (package) trong R giống như ứng dụng trên điện thoại — R chỉ có tính năng cơ bản sẵn, mọi phân tích nâng cao đều cần cài thêm. Bạn chỉ cài một lần (install.packages), nhưng phải nạp mỗi phiên (library).

# --- Cài gói (chỉ làm 1 lần, xong thì để dấu # trước mỗi dòng) --------------
# install.packages(c("tidyverse","readxl","psych","GPArotation",
#                    "lavaan","semTools","car","corrplot","modelsummary"))

# --- Nạp gói (làm MỖI LẦN mở RStudio) ---------------------------------------
library(tidyverse)    # xử lý dữ liệu + ggplot2
library(readxl)       # đọc file Excel
library(psych)        # mô tả, Cronbach's alpha, EFA
library(GPArotation)  # thuật toán xoay nhân tố cho EFA
library(lavaan)       # CFA + SEM
library(semTools)     # CR, AVE, bổ trợ lavaan
library(car)          # VIF, kiểm định hồi quy
library(corrplot)     # vẽ ma trận tương quan
library(modelsummary) # xuất bảng hồi quy

Kiểm tra: Sau khi chạy chunk trên, bạn thấy dòng chữ trong Console nhưng không có chữ Error → thành công.


2 Nhập dữ liệu (Import)

2.1 Tại sao phải đặt đúng thư mục làm việc?

R cần biết file nằm ở đâu trên máy. Nếu dùng R Project (khuyến nghị), thư mục tự động là thư mục chứa file .Rproj — không cần setwd(). Nếu chưa dùng Project, hãy chạy dòng setwd() ở dưới.

# Nếu CHƯA dùng R Project, bỏ dấu # và sửa đường dẫn:
# setwd("D:/Research/draft/dataset_r")

# Đọc file Excel, lấy sheet "responses"
df_raw <- read_excel("AI_appliance_survey_DATA.xlsx",
                     sheet = "responses")

# Xem nhanh cấu trúc
dim(df_raw)       # số hàng × số cột
[1] 382  41
names(df_raw)     # tên các cột
 [1] "Timestamp" "Consent"   "Age18"     "BuyerRole" "HeardOf"   "UsageExp" 
 [7] "Devices"   "SQ1"       "SQ2"       "SQ3"       "SQ4"       "SQ5"      
[13] "PP1"       "PP2"       "PP3"       "PP4"       "PP5"       "TR1"      
[19] "TR2"       "TR3"       "PU1"       "PU2"       "PU3"       "PU4"      
[25] "PEOU1"     "PEOU2"     "PEOU3"     "PEOU4"     "BT1"       "BT2"      
[31] "BT3"       "BT4"       "BI1"       "BI2"       "BI3"       "Gender"   
[37] "City"      "Age"       "Education" "Job"       "Income"   

⚠️ Khi có dữ liệu thật từ Google Forms: Google Forms xuất tiêu đề câu hỏi làm tên cột — rất dài. Bước đầu tiên là đổi về mã ngắn:

df_raw <- df_raw %>%
  rename(
    SQ1 = `SQ1: Tôi sử dụng các thiết bị...vì chúng rất hữu ích`,
    SQ2 = `SQ2: Mặc dù tôi sử dụng...`,
    # ... tiếp tục cho đến BI3
  )
# Định nghĩa nhóm item — dùng lại ở mọi bước phía sau
SQ_i   <- paste0("SQ",   1:5)   # AI System Quality
PP_i   <- paste0("PP",   1:5)   # Perceived Personalization
TR_i   <- paste0("TR",   1:3)   # AI Transparency
PU_i   <- paste0("PU",   1:4)   # Perceived Usefulness
PEOU_i <- paste0("PEOU", 1:4)   # Perceived Ease of Use
BT_i   <- paste0("BT",   1:4)   # Brand Trust (biến trung gian)
BI_i   <- paste0("BI",   1:3)   # Behavioral Intention (biến phụ thuộc)
items  <- c(SQ_i, PP_i, TR_i, PU_i, PEOU_i, BT_i, BI_i)  # tất cả 28 item

3 Làm sạch dữ liệu (Data Cleaning)

3.1 Tại sao cần làm sạch?

Dữ liệu khảo sát giống rau vừa mới hái — phải nhặt sạch trước khi nấu. Với Google Forms, “sạn” lớn nhất không phải giá trị lỗi (Forms đã ép chọn 1–5), mà là người không đủ điều kiện vẫn lọt vào, và người trả lời cẩu thả.

Có 4 việc cần làm theo thứ tự:

Bước Việc làm Vì sao
1 Lọc sàng lọc Giữ đúng đối tượng nghiên cứu
2 Xóa Likert trống Người bị rẽ nhánh kết thúc sớm
3 Xóa trùng lặp 1 người bấm gửi 2 lần
4 Xóa straight-liner Trả lời tất cả 1 giá trị, nghi cẩu thả
# Bước 1 + 2: Lọc người hợp lệ
df_q <- df_raw %>%
  filter(
    Consent   == "Tôi đồng ý và tự nguyện tham gia khảo sát",
    Age18     == "Có",
    HeardOf   == "Có",
    UsageExp  != "Tôi chưa từng nghe đến",
    BuyerRole != "Không tham gia vào việc quyết định mua"
  ) %>%
  drop_na(all_of(items))   # bỏ hàng còn Likert trống

cat("Sau lọc sàng lọc:", nrow(df_q), "người\n")
Sau lọc sàng lọc: 350 người
# Bước 3: Xóa trùng lặp
truoc_dedup <- nrow(df_q)
df_q <- df_q[!duplicated(df_q[items]), ]
cat("Đã xóa", truoc_dedup - nrow(df_q), "dòng trùng lặp\n")
Đã xóa 1 dòng trùng lặp
# Bước 4: Xóa straight-liner (SD hàng < 0.30)
row_sd <- apply(df_q[items], 1, sd)
truoc_sl <- nrow(df_q)
df_q <- df_q[row_sd > 0.30, ]
cat("Đã xóa", truoc_sl - nrow(df_q), "người straight-line\n")
Đã xóa 2 người straight-line
cat("\n✅ Cỡ mẫu cuối (df_clean):", nrow(df_q), "người\n")

✅ Cỡ mẫu cuối (df_clean): 347 người
df_clean <- df_q
# Lưu dữ liệu sạch để các bước sau dùng lại (không cần chạy lại từ đầu)
# saveRDS(df_clean, "data/processed/df_clean.rds")
# df_clean <- readRDS("data/processed/df_clean.rds")  # đọc lại ở session khác

📝 Ghi nhận kết quả:

  • Tổng dữ liệu thô: ___ hàng
  • Sau lọc sàng lọc: ___ người
  • Sau xóa trùng + straight-liner: ___ người (= N cuối cùng)

⚠️ Lưu ý: Form của bạn KHÔNG có câu hỏi ngược (reverse item) — mọi phát biểu đều cùng chiều dương → không cần bước 6 - x.


4 Thống kê mô tả (Descriptive Statistics)

4.1 Tại sao cần làm trước khi phân tích sâu?

Trước khi “khám bệnh” cho mô hình, hãy “đo thân nhiệt” dữ liệu. Thống kê mô tả cho bạn biết: mẫu của tôi trông như thế nào? Đây là nội dung đầu tiên của Chương 4 trong bài luận.

# --- Nhân khẩu học ---
cat("=== GIỚI TÍNH ===\n")
=== GIỚI TÍNH ===
print(round(prop.table(table(df_clean$Gender)) * 100, 1))

Khác  Nam   Nữ 
 0.6 48.1 51.3 
cat("\n=== NHÓM TUỔI ===\n")

=== NHÓM TUỔI ===
print(round(prop.table(table(df_clean$Age)) * 100, 1))

Từ 18 - dưới 25 tuổi Từ 25 - dưới 35 tuổi Từ 35 - dưới 45 tuổi 
                45.8                 30.0                 11.8 
Từ 45 - dưới 55 tuổi   Từ 55 tuổi trở lên 
                 7.2                  5.2 
cat("\n=== HỌC VẤN ===\n")

=== HỌC VẤN ===
print(round(prop.table(table(df_clean$Education)) * 100, 1))

           Cao đẳng             Đại học         Sau đại học Trung học phổ thông 
               23.6                51.3                 8.9                16.1 
cat("\n=== NGHỀ NGHIỆP ===\n")

=== NGHỀ NGHIỆP ===
print(round(prop.table(table(df_clean$Job)) * 100, 1))

Chuyên viên (bác sĩ, giáo viên, kỹ sư…)                    Học sinh / Sinh viên 
                                   17.6                                    34.9 
             Kinh doanh / tự kinh doanh                          Lao động tự do 
                                   11.2                                     5.5 
                    Nhân viên văn phòng                                 Nội trợ 
                                   28.0                                     2.9 
cat("\n=== THU NHẬP ===\n")

=== THU NHẬP ===
print(round(prop.table(table(df_clean$Income)) * 100, 1))

        Dưới 10 triệu VNĐ        Không muốn trả lời         Trên 60 triệu VNĐ 
                     17.0                       8.6                       4.0 
Từ 10 – dưới 20 triệu VNĐ Từ 20 – dưới 30 triệu VNĐ Từ 30 – dưới 40 triệu VNĐ 
                     33.7                      21.6                       9.5 
Từ 40 – dưới 60 triệu VNĐ 
                      5.5 
# --- Mô tả từng item (mean, sd, skew, kurtosis) ---
desc <- describe(df_clean[items])
round(desc[, c("mean", "sd", "skew", "kurtosis", "min", "max")], 2)
      mean   sd  skew kurtosis min max
SQ1   3.41 0.87  0.01    -0.47   1   5
SQ2   3.40 0.85 -0.06    -0.41   1   5
SQ3   3.41 0.91  0.05    -0.53   1   5
SQ4   3.42 0.89 -0.30    -0.32   1   5
SQ5   3.38 0.89 -0.02    -0.38   1   5
PP1   3.37 0.90 -0.28    -0.16   1   5
PP2   3.41 0.91 -0.22    -0.11   1   5
PP3   3.41 0.87 -0.23    -0.11   1   5
PP4   3.39 0.88 -0.10    -0.26   1   5
PP5   3.41 0.86 -0.13    -0.25   1   5
TR1   3.39 0.88 -0.21    -0.18   1   5
TR2   3.38 0.88 -0.16    -0.26   1   5
TR3   3.40 0.90 -0.12    -0.37   1   5
PU1   3.41 0.88 -0.09    -0.31   1   5
PU2   3.39 0.88 -0.06    -0.35   1   5
PU3   3.39 0.89 -0.19    -0.44   1   5
PU4   3.39 0.87  0.02    -0.59   1   5
PEOU1 3.41 0.88 -0.02    -0.64   1   5
PEOU2 3.43 0.89 -0.22    -0.18   1   5
PEOU3 3.40 0.87 -0.01    -0.48   1   5
PEOU4 3.40 0.88 -0.11    -0.22   1   5
BT1   3.41 0.88  0.06    -0.61   1   5
BT2   3.42 0.87  0.02    -0.36   1   5
BT3   3.41 0.88 -0.18     0.01   1   5
BT4   3.42 0.86 -0.05    -0.20   1   5
BI1   3.39 0.87 -0.01    -0.28   1   5
BI2   3.39 0.89 -0.15    -0.23   1   5
BI3   3.44 0.88 -0.04    -0.50   1   5

4.1.1 Cách đọc kết quả:

Chỉ số Ý nghĩa Ngưỡng chú ý
mean Mức đồng ý trung bình Thang 1–5; > 3.5 = xu hướng đồng ý
sd Mức phân tán ý kiến SD lớn = ý kiến chia rẽ
skew Độ lệch phân phối Cần ❙skew❙ < 3 để dùng ML estimator
kurtosis Độ nhọn phân phối Cần ❙kurt❙ < 8
min / max Giá trị nhỏ/lớn nhất Phải trong 1–5
# --- Tính điểm tổng hợp (mean composite) cho từng construct ---
# Dùng để chạy tương quan và hồi quy ở bước sau
df_clean <- df_clean %>% mutate(
  SQ   = rowMeans(across(all_of(SQ_i))),
  PP   = rowMeans(across(all_of(PP_i))),
  TR   = rowMeans(across(all_of(TR_i))),
  PU   = rowMeans(across(all_of(PU_i))),
  PEOU = rowMeans(across(all_of(PEOU_i))),
  BT   = rowMeans(across(all_of(BT_i))),
  BI   = rowMeans(across(all_of(BI_i)))
)

constructs <- c("SQ","PP","TR","PU","PEOU","BT","BI")
round(describe(df_clean[constructs])[, c("mean","sd","min","max")], 2)
     mean   sd  min max
SQ   3.41 0.65 1.60   5
PP   3.40 0.69 1.00   5
TR   3.39 0.67 1.67   5
PU   3.40 0.70 1.50   5
PEOU 3.41 0.71 1.25   5
BT   3.42 0.71 1.50   5
BI   3.40 0.74 1.33   5

📝 Ghi nhận kết quả: (điền vào bảng trong bài)

Construct có mean cao nhất: ___ (M = ) Construct có SD lớn nhất: (SD = ) Item nào có skew hoặc kurtosis đáng lo?


5 Kiểm định độ tin cậy — Cronbach’s Alpha

5.1 Tại sao cần làm trước EFA/CFA?

4 item của Brand Trust giống 4 nhân chứng cùng kể một sự việc. Nếu họ kể khớp nhau → thang đo đáng tin (alpha cao). Một người kể ngược hoặc lan man (item kém) → alpha giảm → cân nhắc loại.

Alpha đo lường: tính nhất quán nội tại (internal consistency).

Mức alpha Đánh giá
≥ 0.90 Xuất sắc (coi chừng item trùng ý)
0.80 – 0.89 Tốt
0.70 – 0.79 Chấp nhận được
< 0.70 Cần xem xét lại thang đo

Hai cột cần chú ý thêm:

  • r.drop (corrected item-total correlation): item “ăn khớp” thang đo cỡ nào. Nếu r.drop < 0.30 → item yếu → cân nhắc loại.
  • alpha if dropped: alpha sẽ là bao nhiêu nếu bỏ item đó. Nếu con số này lớn hơn alpha hiện tại → nên bỏ item đó.
cat("=== SQ: AI System Quality (5 items) ===\n")
=== SQ: AI System Quality (5 items) ===
psych::alpha(df_clean[SQ_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[SQ_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean   sd median_r
      0.78      0.78    0.75      0.42 3.6 0.018  3.4 0.65     0.42

    95% confidence boundaries 
         lower alpha upper
Feldt     0.75  0.78  0.82
Duhachek  0.75  0.78  0.82

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r S/N alpha se  var.r med.r
SQ1      0.77      0.77    0.72      0.46 3.4    0.020 0.0023  0.45
SQ2      0.72      0.72    0.67      0.39 2.6    0.025 0.0055  0.41
SQ3      0.72      0.72    0.67      0.40 2.6    0.024 0.0067  0.42
SQ4      0.74      0.74    0.69      0.42 2.9    0.023 0.0046  0.42
SQ5      0.76      0.76    0.71      0.44 3.2    0.021 0.0065  0.46

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
SQ1 347  0.67  0.67  0.54   0.47  3.4 0.87
SQ2 347  0.78  0.78  0.72   0.64  3.4 0.85
SQ3 347  0.78  0.78  0.70   0.62  3.4 0.91
SQ4 347  0.74  0.74  0.65   0.57  3.4 0.89
SQ5 347  0.70  0.70  0.57   0.51  3.4 0.89

Non missing response frequency for each item
       1    2    3    4    5 miss
SQ1 0.01 0.13 0.41 0.35 0.11    0
SQ2 0.01 0.13 0.40 0.37 0.09    0
SQ3 0.01 0.14 0.41 0.31 0.13    0
SQ4 0.01 0.14 0.34 0.42 0.09    0
SQ5 0.01 0.14 0.41 0.33 0.11    0
# Chú ý SQ1: wording "vì chúng rất hữu ích" có thể bị cross-load với PU
cat("=== PP: Perceived Personalization (5 items) ===\n")
=== PP: Perceived Personalization (5 items) ===
psych::alpha(df_clean[PP_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[PP_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean   sd median_r
      0.84      0.84    0.81      0.51 5.3 0.014  3.4 0.69     0.52

    95% confidence boundaries 
         lower alpha upper
Feldt     0.81  0.84  0.87
Duhachek  0.81  0.84  0.87

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r S/N alpha se  var.r med.r
PP1      0.81      0.81    0.76      0.51 4.1    0.017 0.0024  0.52
PP2      0.80      0.80    0.76      0.50 4.1    0.017 0.0022  0.50
PP3      0.79      0.79    0.74      0.49 3.8    0.018 0.0014  0.48
PP4      0.82      0.82    0.77      0.53 4.4    0.016 0.0015  0.53
PP5      0.82      0.82    0.78      0.53 4.6    0.016 0.0011  0.54

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
PP1 347  0.79  0.79  0.71   0.65  3.4 0.90
PP2 347  0.80  0.79  0.72   0.66  3.4 0.91
PP3 347  0.82  0.82  0.76   0.70  3.4 0.87
PP4 347  0.76  0.76  0.67   0.61  3.4 0.88
PP5 347  0.74  0.75  0.65   0.59  3.4 0.86

Non missing response frequency for each item
       1    2    3    4    5 miss
PP1 0.02 0.13 0.38 0.38 0.09    0
PP2 0.02 0.11 0.40 0.35 0.11    0
PP3 0.02 0.12 0.39 0.38 0.09    0
PP4 0.01 0.13 0.41 0.35 0.10    0
PP5 0.01 0.12 0.40 0.37 0.09    0
cat("=== TR: AI Transparency (3 items) ===\n")
=== TR: AI Transparency (3 items) ===
psych::alpha(df_clean[TR_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[TR_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean   sd median_r
      0.62      0.62    0.55      0.35 1.6 0.035  3.4 0.67     0.27

    95% confidence boundaries 
         lower alpha upper
Feldt     0.55  0.62  0.69
Duhachek  0.55  0.62  0.69

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r  S/N alpha se var.r med.r
TR1      0.70      0.70    0.54      0.54 2.36    0.032    NA  0.54
TR2      0.43      0.43    0.27      0.27 0.75    0.061    NA  0.27
TR3      0.40      0.40    0.25      0.25 0.65    0.065    NA  0.25

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
TR1 347  0.67  0.67  0.36   0.30  3.4 0.88
TR2 347  0.79  0.79  0.65   0.50  3.4 0.88
TR3 347  0.81  0.80  0.67   0.52  3.4 0.90

Non missing response frequency for each item
       1    2    3    4    5 miss
TR1 0.02 0.13 0.39 0.37 0.09    0
TR2 0.01 0.13 0.40 0.37 0.09    0
TR3 0.01 0.14 0.39 0.35 0.11    0
# Chú ý TR1: "Tôi MONG MUỐN...minh bạch" — đo kỳ vọng, không đo cảm nhận
# → r.drop của TR1 có thể thấp
cat("=== PU: Perceived Usefulness (4 items) ===\n")
=== PU: Perceived Usefulness (4 items) ===
psych::alpha(df_clean[PU_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[PU_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean  sd median_r
       0.8       0.8    0.76      0.51 4.1 0.017  3.4 0.7     0.51

    95% confidence boundaries 
         lower alpha upper
Feldt     0.77   0.8  0.84
Duhachek  0.77   0.8  0.84

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r S/N alpha se   var.r med.r
PU1      0.73      0.73    0.65      0.48 2.7    0.025 0.00084  0.48
PU2      0.77      0.77    0.69      0.52 3.3    0.022 0.00206  0.53
PU3      0.77      0.77    0.69      0.53 3.4    0.021 0.00117  0.51
PU4      0.75      0.75    0.67      0.50 3.0    0.024 0.00173  0.51

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
PU1 347  0.82  0.82  0.74   0.66  3.4 0.88
PU2 347  0.78  0.78  0.66   0.59  3.4 0.88
PU3 347  0.78  0.77  0.65   0.59  3.4 0.89
PU4 347  0.80  0.80  0.71   0.63  3.4 0.87

Non missing response frequency for each item
       1    2    3    4    5 miss
PU1 0.01 0.13 0.41 0.35 0.10    0
PU2 0.01 0.13 0.41 0.34 0.10    0
PU3 0.01 0.15 0.36 0.39 0.09    0
PU4 0.00 0.15 0.39 0.35 0.10    0
cat("=== PEOU: Perceived Ease of Use (4 items) ===\n")
=== PEOU: Perceived Ease of Use (4 items) ===
psych::alpha(df_clean[PEOU_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[PEOU_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean   sd median_r
      0.83      0.83    0.79      0.55 4.8 0.015  3.4 0.71     0.55

    95% confidence boundaries 
         lower alpha upper
Feldt      0.8  0.83  0.86
Duhachek   0.8  0.83  0.86

 Reliability if an item is dropped:
      raw_alpha std.alpha G6(smc) average_r S/N alpha se   var.r med.r
PEOU1      0.78      0.78    0.70      0.54 3.6    0.020 0.00038  0.54
PEOU2      0.77      0.77    0.69      0.52 3.3    0.022 0.00180  0.53
PEOU3      0.78      0.78    0.70      0.54 3.5    0.021 0.00348  0.54
PEOU4      0.80      0.80    0.73      0.58 4.1    0.018 0.00038  0.57

 Item statistics 
        n raw.r std.r r.cor r.drop mean   sd
PEOU1 347  0.81  0.81  0.73   0.66  3.4 0.88
PEOU2 347  0.83  0.83  0.76   0.69  3.4 0.89
PEOU3 347  0.82  0.82  0.73   0.66  3.4 0.87
PEOU4 347  0.78  0.78  0.67   0.61  3.4 0.88

Non missing response frequency for each item
         1    2    3    4   5 miss
PEOU1 0.00 0.15 0.37 0.37 0.1    0
PEOU2 0.02 0.12 0.38 0.38 0.1    0
PEOU3 0.01 0.14 0.40 0.35 0.1    0
PEOU4 0.01 0.12 0.41 0.35 0.1    0
cat("=== BT: Brand Trust — TRUNG GIAN (4 items) ===\n")
=== BT: Brand Trust — TRUNG GIAN (4 items) ===
psych::alpha(df_clean[BT_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[BT_i])

  raw_alpha std.alpha G6(smc) average_r S/N   ase mean   sd median_r
      0.82      0.82    0.78      0.54 4.7 0.015  3.4 0.71     0.54

    95% confidence boundaries 
         lower alpha upper
Feldt     0.79  0.82  0.85
Duhachek  0.79  0.82  0.85

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r S/N alpha se   var.r med.r
BT1      0.77      0.77    0.69      0.53 3.4    0.021 0.00026  0.53
BT2      0.77      0.77    0.69      0.53 3.4    0.021 0.00034  0.53
BT3      0.78      0.78    0.70      0.54 3.6    0.020 0.00038  0.53
BT4      0.79      0.79    0.71      0.55 3.7    0.020 0.00014  0.55

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
BT1 347  0.82  0.82  0.73   0.66  3.4 0.88
BT2 347  0.82  0.82  0.73   0.66  3.4 0.87
BT3 347  0.81  0.80  0.71   0.64  3.4 0.88
BT4 347  0.79  0.80  0.69   0.63  3.4 0.86

Non missing response frequency for each item
       1    2    3    4    5 miss
BT1 0.00 0.14 0.40 0.34 0.11    0
BT2 0.01 0.12 0.43 0.33 0.11    0
BT3 0.02 0.10 0.43 0.35 0.10    0
BT4 0.01 0.11 0.43 0.35 0.10    0
cat("=== BI: Behavioral Intention — PHỤ THUỘC (3 items) ===\n")
=== BI: Behavioral Intention — PHỤ THUỘC (3 items) ===
psych::alpha(df_clean[BI_i])

Reliability analysis   
Call: psych::alpha(x = df_clean[BI_i])

  raw_alpha std.alpha G6(smc) average_r S/N  ase mean   sd median_r
      0.78      0.78    0.71      0.55 3.6 0.02  3.4 0.74     0.55

    95% confidence boundaries 
         lower alpha upper
Feldt     0.74  0.78  0.82
Duhachek  0.74  0.78  0.82

 Reliability if an item is dropped:
    raw_alpha std.alpha G6(smc) average_r S/N alpha se var.r med.r
BI1      0.68      0.68    0.51      0.51 2.1    0.034    NA  0.51
BI2      0.71      0.71    0.55      0.55 2.4    0.031    NA  0.55
BI3      0.73      0.73    0.58      0.58 2.7    0.029    NA  0.58

 Item statistics 
      n raw.r std.r r.cor r.drop mean   sd
BI1 347  0.85  0.85  0.73   0.65  3.4 0.87
BI2 347  0.84  0.83  0.70   0.62  3.4 0.89
BI3 347  0.82  0.82  0.68   0.60  3.4 0.88

Non missing response frequency for each item
       1    2    3    4    5 miss
BI1 0.01 0.12 0.43 0.33 0.10    0
BI2 0.02 0.13 0.40 0.35 0.10    0
BI3 0.01 0.13 0.39 0.36 0.11    0
# --- Bảng tổng hợp alpha cho cả 7 construct ---
get_alpha <- function(data, label) {
  a <- psych::alpha(data)
  data.frame(
    Construct  = label,
    N_items    = ncol(data),
    Alpha      = round(a$total$raw_alpha, 3),
    Dat_yeu_cau = ifelse(a$total$raw_alpha >= 0.70, "✅ Đạt", "❌ Chưa đạt")
  )
}

alpha_table <- bind_rows(
  get_alpha(df_clean[SQ_i],   "SQ – AI System Quality"),
  get_alpha(df_clean[PP_i],   "PP – Personalization"),
  get_alpha(df_clean[TR_i],   "TR – Transparency"),
  get_alpha(df_clean[PU_i],   "PU – Perceived Usefulness"),
  get_alpha(df_clean[PEOU_i], "PEOU – Ease of Use"),
  get_alpha(df_clean[BT_i],   "BT – Brand Trust"),
  get_alpha(df_clean[BI_i],   "BI – Behavioral Intention")
)
knitr::kable(alpha_table, caption = "Bảng tổng hợp Cronbach's Alpha")
Bảng tổng hợp Cronbach’s Alpha
Construct N_items Alpha Dat_yeu_cau
SQ – AI System Quality 5 0.784 ✅ Đạt
PP – Personalization 5 0.840 ✅ Đạt
TR – Transparency 3 0.622 ❌ Chưa đạt
PU – Perceived Usefulness 4 0.804 ✅ Đạt
PEOU – Ease of Use 4 0.828 ✅ Đạt
BT – Brand Trust 4 0.824 ✅ Đạt
BI – Behavioral Intention 3 0.784 ✅ Đạt

5.1.1 Viết kết quả vào bài luận (mẫu câu):

Kết quả kiểm định độ tin cậy cho thấy tất cả các thang đo đều đạt yêu cầu, với hệ số Cronbach’s Alpha dao động từ ___ (___ items) đến ___ (___ items), đều vượt ngưỡng khuyến nghị 0.70 (Nunnally & Bernstein, 1994). Hệ số tương quan biến–tổng hiệu chỉnh của mọi biến quan sát đều lớn hơn 0.30.


6 Phân tích nhân tố khám phá — EFA

6.1 Tại sao cần EFA trước CFA?

  • EFA: hỏi “các item tự nhóm thành mấy nhân tố?” — khám phá.
  • CFA: hỏi “cấu trúc 7 nhân tố mình thiết kế có khớp không?” — khẳng định.

EFA là bước “kiểm tra sức khỏe sơ bộ” của thang đo trước khi CFA xác nhận chính thức.

6.1.1 Điều kiện tiên quyết (kiểm tra trước khi chạy EFA):

  • KMO ≥ 0.70: dữ liệu đủ “chín” để phân tích nhân tố.
  • Bartlett p < 0.05: các item có đủ tương quan để gom nhóm.
cat("=== KIỂM TRA ĐIỀU KIỆN TIÊN QUYẾT ===\n\n")
=== KIỂM TRA ĐIỀU KIỆN TIÊN QUYẾT ===
# KMO
kmo_result <- KMO(df_clean[items])
cat("KMO Overall MSA =", round(kmo_result$MSA, 3), "\n")
KMO Overall MSA = 0.915 
cat("Đánh giá:", ifelse(kmo_result$MSA >= 0.80, "✅ Tốt (≥ 0.80)",
              ifelse(kmo_result$MSA >= 0.70, "✅ Chấp nhận được (≥ 0.70)",
                     "❌ Chưa đạt (< 0.70)")), "\n\n")
Đánh giá: ✅ Tốt (≥ 0.80) 
# Bartlett's Test
bart <- cortest.bartlett(df_clean[items])
cat("Bartlett's Test: Chi-square =", round(bart$chisq, 1),
    ", p =", format(bart$p.value, scientific = TRUE), "\n")
Bartlett's Test: Chi-square = 3852 , p = 0e+00 
cat("Đánh giá:", ifelse(bart$p.value < 0.05, "✅ Đạt (p < 0.05)", "❌ Chưa đạt"), "\n")
Đánh giá: ✅ Đạt (p < 0.05) 
# --- Xác định số nhân tố bằng Parallel Analysis ---
# Parallel analysis so sánh eigenvalue thực với eigenvalue ngẫu nhiên
# → số nhân tố = số điểm TRÊN đường chấm (random data line)
fa.parallel(df_clean[items],
            fa     = "fa",
            fm     = "pa",
            main   = "Parallel Analysis — Xác định số nhân tố")

Parallel analysis suggests that the number of factors =  6  and the number of components =  NA 

Cách đọc Scree Plot: Đếm số điểm nằm trên đường đứt (Simulated Data) → đó là số nhân tố nên dùng. Kỳ vọng cho bài này: 7 nhân tố (khớp 7 construct trong mô hình).

# --- Chạy EFA ---
# fm = "pa": Principal Axis (tốt cho dữ liệu không chuẩn)
# rotate = "promax": xoay xiên — phù hợp khi các construct CÓ tương quan với nhau
efa <- fa(df_clean[items],
          nfactors = 7,
          fm       = "pa",
          rotate   = "promax")

# In bảng factor loadings (chỉ hiển thị loading >= 0.30)
print(efa$loadings, cutoff = 0.30, sort = TRUE)

Loadings:
      PA2    PA5    PA4    PA3    PA1    PA6    PA7   
PP1    0.748                                          
PP2    0.758                                          
PP3    0.740                                          
PP4    0.599                                          
PP5    0.661                                          
BT1           0.670                                   
BT2           0.678                                   
BT3           0.676                                   
BT4           0.804                                   
PU1                  0.792                            
PU2                  0.707                            
PU3                  0.615                            
PU4                  0.737                            
PEOU1                       0.681                     
PEOU2                       0.770                     
PEOU3                       0.697                     
PEOU4                       0.720                     
SQ2                                0.660              
SQ3                                0.725              
SQ4                                0.727              
SQ5                                0.560              
BI1                                       0.710       
BI2                                       0.751       
BI3                                       0.683       
TR2                                              0.639
TR3                                              0.701
SQ1                                0.352              
TR1                                              0.394

                 PA2   PA5   PA4   PA3   PA1   PA6   PA7
SS loadings    2.528 2.212 2.210 2.131 2.005 1.630 1.109
Proportion Var 0.090 0.079 0.079 0.076 0.072 0.058 0.040
Cumulative Var 0.090 0.169 0.248 0.324 0.396 0.454 0.494
# --- Phương sai giải thích ---
cat("\n=== PHƯƠNG SAI GIẢI THÍCH ===\n")

=== PHƯƠNG SAI GIẢI THÍCH ===
var_table <- round(efa$Vaccounted, 3)
print(var_table)
                        PA2   PA5   PA4   PA3   PA1   PA6   PA7
SS loadings           2.597 2.388 2.245 2.218 2.058 1.701 1.181
Proportion Var        0.093 0.085 0.080 0.079 0.073 0.061 0.042
Cumulative Var        0.093 0.178 0.258 0.337 0.411 0.472 0.514
Proportion Explained  0.181 0.166 0.156 0.154 0.143 0.118 0.082
Cumulative Proportion 0.181 0.346 0.503 0.657 0.800 0.918 1.000
cat("\nTổng phương sai giải thích:",
    round(sum(efa$Vaccounted["Proportion Var", ]) * 100, 1), "%\n")

Tổng phương sai giải thích: 51.4 %

6.1.2 Cách đọc bảng Factor Loadings:

Giá trị loading Đánh giá
≥ 0.70 Rất mạnh ✅
0.50 – 0.69 Tốt ✅
0.30 – 0.49 Chấp nhận (xem xét)
Xuất hiện ở 2+ nhân tố (cross-loading) ❌ Cần loại item

⚠️ Hai điểm cần chú ý kỹ:

  • SQ1 (“Tôi sử dụng…vì chúng rất hữu ích”) — wording đo sự hữu ích, có thể tải lên cả nhân tố SQ lẫn PU → nếu cross-loading > 0.30 thì loại.
  • TR1 (“Tôi mong muốn…minh bạch”) — đo kỳ vọng, không đo cảm nhận, có thể tải yếu → nếu loading < 0.50 và TR chỉ còn 2 item → báo GVHD.

📝 Ghi nhận kết quả:

  • KMO = ___
  • Bartlett p = ___
  • Số nhân tố đề xuất từ parallel analysis: ___
  • Tổng phương sai giải thích: ___%
  • Item cần xem xét loại: ___

6.1.3 Viết kết quả vào bài luận (mẫu câu):

Phân tích nhân tố khám phá được tiến hành với phép trích Principal Axis Factoring và phép xoay Promax. Hệ số KMO đạt ___ (> 0.70) và kiểm định Bartlett có ý nghĩa thống kê (χ² = , p < 0.001), khẳng định dữ liệu phù hợp để phân tích nhân tố. Kết quả trích được 7 nhân tố với tổng phương sai trích đạt %, các hệ số tải nhân tố đều lớn hơn 0.50.


7 Phân tích nhân tố khẳng định — CFA

7.1 EFA vs CFA: đâu là điểm khác biệt?

EFA giống bạn đổ một rổ đồ chơi lẫn lộn và xem chúng tự gom thành mấy đống. CFA giống bạn áp một bản thiết kế đã vẽ sẵn (7 hộp mô hình) vào dữ liệu và hỏi: “Dữ liệu có vừa khớp với bản thiết kế này không?”

Nếu CFA “vừa khít” → mô hình đo lường của bạn đáng tin → mới được chạy SEM.

7.1.1 Ngưỡng chấp nhận của các chỉ số fit:

Chỉ số Chấp nhận Tốt Ý nghĩa nôm na
CFI ≥ 0.90 ≥ 0.95 Càng gần 1 càng khớp
TLI ≥ 0.90 ≥ 0.95 Như CFI, phạt mô hình phức tạp
RMSEA ≤ 0.08 ≤ 0.06 Sai số xấp xỉ, càng nhỏ càng tốt
SRMR ≤ 0.08 ≤ 0.05 Phần dư chuẩn hóa trung bình
χ²/df ≤ 3 ≤ 2 Nhạy cảm với cỡ mẫu lớn
# --- Định nghĩa mô hình đo lường ---
# "=~" đọc là "được đo bởi"
cfa_model <- '
  SQ   =~ SQ1 + SQ2 + SQ3 + SQ4 + SQ5
  PP   =~ PP1 + PP2 + PP3 + PP4 + PP5
  TR   =~ TR1 + TR2 + TR3
  PU   =~ PU1 + PU2 + PU3 + PU4
  PEOU =~ PEOU1 + PEOU2 + PEOU3 + PEOU4
  BT   =~ BT1 + BT2 + BT3 + BT4
  BI   =~ BI1 + BI2 + BI3
'

# Chạy CFA với MLR (Robust Maximum Likelihood)
# std.lv = TRUE: neo thang đo bằng cách cố định variance = 1 (cách chuẩn)
fit_cfa <- cfa(cfa_model,
               data      = df_clean,
               estimator = "MLR",
               std.lv    = TRUE)
# --- Chỉ số phù hợp ---
cat("=== CHỈ SỐ PHÙ HỢP CFA ===\n")
=== CHỈ SỐ PHÙ HỢP CFA ===
fit_idx <- fitMeasures(fit_cfa, c("chisq.scaled","df.scaled","pvalue.scaled",
                                   "cfi.robust","tli.robust",
                                   "rmsea.robust","srmr"))
print(round(fit_idx, 3))
 chisq.scaled     df.scaled pvalue.scaled    cfi.robust    tli.robust 
      352.626       329.000         0.177         0.993         0.992 
 rmsea.robust          srmr 
        0.014         0.038 
cat("\nChiSq/df =", round(fit_idx["chisq.scaled"]/fit_idx["df.scaled"], 2), "\n")

ChiSq/df = 1.07 
# --- Standardized factor loadings ---
cat("\n=== STANDARDIZED FACTOR LOADINGS ===\n")

=== STANDARDIZED FACTOR LOADINGS ===
summary(fit_cfa, standardized = TRUE)
lavaan 0.6-21 ended normally after 27 iterations

  Estimator                                         ML
  Optimization method                           NLMINB
  Number of model parameters                        77

  Number of observations                           347

Model Test User Model:
                                              Standard      Scaled
  Test Statistic                               352.284     352.626
  Degrees of freedom                               329         329
  P-value (Chi-square)                           0.181       0.177
  Scaling correction factor                                  0.999
    Yuan-Bentler correction (Mplus variant)                       

Parameter Estimates:

  Standard errors                             Sandwich
  Information bread                           Observed
  Observed information based on                Hessian

Latent Variables:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  SQ =~                                                                 
    SQ1               0.512    0.045   11.321    0.000    0.512    0.589
    SQ2               0.635    0.038   16.724    0.000    0.635    0.752
    SQ3               0.640    0.042   15.225    0.000    0.640    0.702
    SQ4               0.562    0.046   12.118    0.000    0.562    0.634
    SQ5               0.518    0.045   11.504    0.000    0.518    0.581
  PP =~                                                                 
    PP1               0.647    0.044   14.765    0.000    0.647    0.719
    PP2               0.662    0.046   14.261    0.000    0.662    0.729
    PP3               0.686    0.042   16.280    0.000    0.686    0.787
    PP4               0.613    0.043   14.160    0.000    0.613    0.694
    PP5               0.562    0.044   12.915    0.000    0.562    0.652
  TR =~                                                                 
    TR1               0.293    0.053    5.501    0.000    0.293    0.333
    TR2               0.676    0.053   12.702    0.000    0.676    0.772
    TR3               0.640    0.051   12.515    0.000    0.640    0.708
  PU =~                                                                 
    PU1               0.670    0.041   16.323    0.000    0.670    0.765
    PU2               0.596    0.042   14.189    0.000    0.596    0.674
    PU3               0.606    0.044   13.895    0.000    0.606    0.684
    PU4               0.631    0.041   15.524    0.000    0.631    0.728
  PEOU =~                                                               
    PEOU1             0.666    0.037   17.766    0.000    0.666    0.757
    PEOU2             0.689    0.044   15.611    0.000    0.689    0.773
    PEOU3             0.657    0.040   16.399    0.000    0.657    0.757
    PEOU4             0.585    0.045   12.965    0.000    0.585    0.667
  BT =~                                                                 
    BT1               0.665    0.040   16.575    0.000    0.665    0.756
    BT2               0.667    0.039   17.208    0.000    0.667    0.764
    BT3               0.633    0.041   15.354    0.000    0.633    0.723
    BT4               0.595    0.043   13.716    0.000    0.595    0.692
  BI =~                                                                 
    BI1               0.689    0.041   16.824    0.000    0.689    0.789
    BI2               0.645    0.045   14.323    0.000    0.645    0.723
    BI3               0.620    0.043   14.458    0.000    0.620    0.709

Covariances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  SQ ~~                                                                 
    PP                0.264    0.058    4.527    0.000    0.264    0.264
    TR                0.367    0.070    5.254    0.000    0.367    0.367
    PU                0.541    0.047   11.531    0.000    0.541    0.541
    PEOU              0.556    0.049   11.333    0.000    0.556    0.556
    BT                0.615    0.045   13.760    0.000    0.615    0.615
    BI                0.556    0.047   11.735    0.000    0.556    0.556
  PP ~~                                                                 
    TR                0.364    0.066    5.540    0.000    0.364    0.364
    PU                0.459    0.054    8.562    0.000    0.459    0.459
    PEOU              0.470    0.050    9.464    0.000    0.470    0.470
    BT                0.577    0.054   10.787    0.000    0.577    0.577
    BI                0.390    0.054    7.207    0.000    0.390    0.390
  TR ~~                                                                 
    PU                0.523    0.054    9.651    0.000    0.523    0.523
    PEOU              0.538    0.054   10.055    0.000    0.538    0.538
    BT                0.602    0.052   11.581    0.000    0.602    0.602
    BI                0.351    0.065    5.442    0.000    0.351    0.351
  PU ~~                                                                 
    PEOU              0.579    0.047   12.251    0.000    0.579    0.579
    BT                0.578    0.047   12.206    0.000    0.578    0.578
    BI                0.561    0.051   10.967    0.000    0.561    0.561
  PEOU ~~                                                               
    BT                0.574    0.046   12.510    0.000    0.574    0.574
    BI                0.492    0.049    9.960    0.000    0.492    0.492
  BT ~~                                                                 
    BI                0.597    0.048   12.448    0.000    0.597    0.597

Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .SQ1               0.494    0.040   12.479    0.000    0.494    0.654
   .SQ2               0.310    0.030   10.195    0.000    0.310    0.435
   .SQ3               0.421    0.041   10.278    0.000    0.421    0.507
   .SQ4               0.469    0.036   12.980    0.000    0.469    0.597
   .SQ5               0.526    0.044   11.968    0.000    0.526    0.662
   .PP1               0.391    0.036   10.931    0.000    0.391    0.483
   .PP2               0.386    0.035   11.044    0.000    0.386    0.468
   .PP3               0.289    0.026   10.930    0.000    0.289    0.380
   .PP4               0.405    0.036   11.334    0.000    0.405    0.518
   .PP5               0.427    0.033   12.952    0.000    0.427    0.575
   .TR1               0.688    0.052   13.199    0.000    0.688    0.889
   .TR2               0.310    0.057    5.478    0.000    0.310    0.404
   .TR3               0.407    0.051    7.949    0.000    0.407    0.499
   .PU1               0.317    0.032   10.023    0.000    0.317    0.415
   .PU2               0.426    0.037   11.465    0.000    0.426    0.546
   .PU3               0.418    0.036   11.609    0.000    0.418    0.532
   .PU4               0.353    0.035   10.044    0.000    0.353    0.470
   .PEOU1             0.330    0.032   10.306    0.000    0.330    0.427
   .PEOU2             0.319    0.031   10.156    0.000    0.319    0.402
   .PEOU3             0.322    0.032   10.194    0.000    0.322    0.427
   .PEOU4             0.428    0.034   12.690    0.000    0.428    0.555
   .BT1               0.331    0.033   10.028    0.000    0.331    0.428
   .BT2               0.317    0.031   10.175    0.000    0.317    0.416
   .BT3               0.366    0.036   10.094    0.000    0.366    0.478
   .BT4               0.385    0.036   10.797    0.000    0.385    0.521
   .BI1               0.288    0.038    7.560    0.000    0.288    0.377
   .BI2               0.381    0.039    9.809    0.000    0.381    0.478
   .BI3               0.381    0.038   10.077    0.000    0.381    0.498
    SQ                1.000                               1.000    1.000
    PP                1.000                               1.000    1.000
    TR                1.000                               1.000    1.000
    PU                1.000                               1.000    1.000
    PEOU              1.000                               1.000    1.000
    BT                1.000                               1.000    1.000
    BI                1.000                               1.000    1.000
# --- CR (Composite Reliability) và AVE (Average Variance Extracted) ---
# CR  >= 0.70 → độ tin cậy tổng hợp đạt
# AVE >= 0.50 → giá trị hội tụ đạt
cat("=== CR & AVE (Convergent Validity) ===\n")
=== CR & AVE (Convergent Validity) ===
semTools::reliability(fit_cfa)
              SQ        PP        TR        PU      PEOU        BT        BI
alpha  0.7842197 0.8403041 0.6224480 0.8039503 0.8277972 0.8241253 0.7837275
omega  0.7873230 0.8412556 0.6479433 0.8052053 0.8281954 0.8240350 0.7843818
omega2 0.7873230 0.8412556 0.6479433 0.8052053 0.8281954 0.8240350 0.7843818
omega3 0.7894978 0.8412027 0.6420471 0.8062660 0.8272487 0.8229644 0.7842700
avevar 0.4276690 0.5156942 0.4037050 0.5087328 0.5473896 0.5398436 0.5485217
# --- Giá trị phân biệt — Tiêu chí Fornell-Larcker ---
# Điều kiện: sqrt(AVE) của mỗi construct > tương quan với mọi construct khác
cat("=== TƯƠNG QUAN GIỮA CÁC CONSTRUCT (CFA) ===\n")
=== TƯƠNG QUAN GIỮA CÁC CONSTRUCT (CFA) ===
lavInspect(fit_cfa, "cor.lv")
        SQ    PP    TR    PU  PEOU    BT    BI
SQ   1.000                                    
PP   0.264 1.000                              
TR   0.367 0.364 1.000                        
PU   0.541 0.459 0.523 1.000                  
PEOU 0.556 0.470 0.538 0.579 1.000            
BT   0.615 0.577 0.602 0.578 0.574 1.000      
BI   0.556 0.390 0.351 0.561 0.492 0.597 1.000
# So sánh thủ công: sqrt(AVE) của từng construct phải lớn hơn
# giá trị trong cùng hàng/cột của bảng tương quan trên.

7.1.2 Nếu fit chưa đạt → Xem Modification Indices

# Chạy dòng này khi RMSEA > 0.08 hoặc CFI < 0.90
mi <- modificationIndices(fit_cfa, sort. = TRUE, minimum.value = 10)
head(mi, 15)
# MI lớn gợi ý có thể thêm đường (covariance hoặc cross-loading)
# Chỉ thêm khi có LÝ THUYẾT biện minh, không thêm bừa để fit đẹp.

📝 Ghi nhận kết quả:

Chỉ số Kết quả Đạt/Chưa
CFI ___ ___
TLI ___ ___
RMSEA ___ ___
SRMR ___ ___
χ²/df ___ ___
CR thấp nhất ___ (construct: ___) ___
AVE thấp nhất ___ (construct: ___) ___

7.1.3 Viết kết quả vào bài luận (mẫu câu):

Mô hình đo lường được kiểm định bằng phân tích nhân tố khẳng định với ước lượng MLR. Các chỉ số phù hợp đạt mức tốt (CFI = ; TLI = ; RMSEA = ; SRMR = ), cho thấy mô hình tương thích với dữ liệu. Độ tin cậy tổng hợp (CR) của các khái niệm dao động từ ___ đến ___ và phương sai trích bình quân (AVE) đều vượt ngưỡng 0.50, khẳng định giá trị hội tụ. Giá trị phân biệt được thỏa mãn theo tiêu chí Fornell-Larcker khi căn bậc hai của AVE lớn hơn các hệ số tương quan giữa các khái niệm.


8 Phân tích tương quan

8.1 Tại sao cần tương quan trước khi kiểm định giả thuyết?

Tương quan là bức tranh sơ bộ — xem các construct có “đi cùng chiều” không trước khi khẳng định quan hệ nhân quả bằng SEM.

⚠️ Quan trọng: Tương quan ≠ nhân quả. Chỉ SEM mới nói được “X gây ra Y”.

corr_mat <- cor(df_clean[constructs], use = "pairwise.complete.obs")

# Bảng số
cat("=== MA TRẬN TƯƠNG QUAN ===\n")
=== MA TRẬN TƯƠNG QUAN ===
print(round(corr_mat, 2))
       SQ   PP   TR   PU PEOU   BT   BI
SQ   1.00 0.21 0.27 0.44 0.45 0.50 0.44
PP   0.21 1.00 0.27 0.38 0.39 0.48 0.31
TR   0.27 0.27 1.00 0.38 0.39 0.42 0.25
PU   0.44 0.38 0.38 1.00 0.47 0.47 0.44
PEOU 0.45 0.39 0.39 0.47 1.00 0.47 0.40
BT   0.50 0.48 0.42 0.47 0.47 1.00 0.48
BI   0.44 0.31 0.25 0.44 0.40 0.48 1.00
# Biểu đồ nhiệt tương quan
corrplot(corr_mat,
         method      = "color",
         type        = "lower",
         addCoef.col = "black",
         number.cex  = 0.75,
         tl.col      = "black",
         tl.srt      = 45,
         col         = colorRampPalette(c("#d73027","white","#1a9850"))(200),
         title       = "Ma trận tương quan giữa các construct",
         mar         = c(0,0,1,0))

Cách đọc: Màu xanh đậm = tương quan dương mạnh. Màu đỏ = tương quan âm. Số trong ô = hệ số r (từ −1 đến +1).

📝 Ghi nhận: Tương quan mạnh nhất: ___ và ___ (r = ) Tương quan BT–BI: r = (kỳ vọng dương, đây là cặp mediator–DV)


9 Hồi quy bội (Quick regression check)

9.1 Tại sao chạy hồi quy nếu đã có SEM?

Hồi quy dùng điểm tổng hợp (mean composite) nên chạy nhanh và dễ đọc. Đây là bước kiểm tra nhanh trước SEM, và cũng là cách kiểm tra giả định (VIF, phần dư) mà lavaan không cung cấp sẵn.

SEM dùng biến tiềm ẩn (latent variable) → kết quả chuẩn hơn nhưng phức tạp hơn.

# Hồi quy: BI phụ thuộc vào PU, PEOU và BT
reg <- lm(BI ~ PU + PEOU + BT, data = df_clean)
summary(reg)

Call:
lm(formula = BI ~ PU + PEOU + BT, data = df_clean)

Residuals:
    Min      1Q  Median      3Q     Max 
-1.6681 -0.4148  0.0203  0.4293  1.6345 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.97787    0.20135   4.856 1.82e-06 ***
PU           0.24278    0.05682   4.272 2.51e-05 ***
PEOU         0.15653    0.05539   2.826  0.00499 ** 
BT           0.31257    0.05601   5.581 4.87e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.6158 on 343 degrees of freedom
Multiple R-squared:  0.3067,    Adjusted R-squared:  0.3006 
F-statistic: 50.58 on 3 and 343 DF,  p-value: < 2.2e-16
# --- Kiểm định giả định ---
cat("=== VIF (đa cộng tuyến) ===\n")
=== VIF (đa cộng tuyến) ===
print(round(vif(reg), 2))
  PU PEOU   BT 
1.43 1.43 1.43 
cat("→ VIF < 3: lý tưởng | VIF < 10: chấp nhận\n")
→ VIF < 3: lý tưởng | VIF < 10: chấp nhận
cat("\n=== CONFIDENCE INTERVALS (95%) ===\n")

=== CONFIDENCE INTERVALS (95%) ===
print(round(confint(reg), 3))
            2.5 % 97.5 %
(Intercept) 0.582  1.374
PU          0.131  0.355
PEOU        0.048  0.265
BT          0.202  0.423
# --- Biểu đồ kiểm định giả định phần dư ---
par(mfrow = c(2,2))
plot(reg)

par(mfrow = c(1,1))

9.1.1 Cách đọc biểu đồ giả định:

Biểu đồ Kiểm tra Kỳ vọng
Residuals vs Fitted Tuyến tính Điểm phân tán đều, không có mẫu hình
Q-Q Plot Phân phối chuẩn Điểm bám sát đường chéo
Scale-Location Phương sai đồng nhất Đường nằm ngang
Cook’s Distance Điểm ảnh hưởng lớn Không có điểm vượt ngưỡng 0.5

📝 Ghi nhận:

  • R² = ___ (mô hình giải thích ___% phương sai BI)
  • VIF lớn nhất = ___ (construct: ___)
  • Hệ số có ý nghĩa nhất: ___ (β = , p = )

10 Mô hình cấu trúc — SEM & Kiểm định trung gian

10.1 Tại sao SEM thay vì chỉ dùng hồi quy?

Hồi quy SEM
Loại biến Composite score Latent variable (chuẩn hơn)
Mô hình 1 phương trình Nhiều phương trình đồng thời
Sai số đo lường Không tính đến Tính đến (ít thiên lệch hơn)
Kiểm định trung gian Cần chạy nhiều bước Tích hợp trong 1 lần chạy

10.2 Brand Trust là trung gian — kiểm định thế nào?

PU → BT → BI
    [a] [b]

Hiệu ứng gián tiếp (indirect effect) = a × b
Kiểm định: dùng BOOTSTRAP → nếu khoảng tin cậy 95% KHÔNG chứa 0 → có trung gian
  • Trung gian hoàn toàn (full): thêm BT vào, đường PU→BI mất ý nghĩa thống kê.
  • Trung gian một phần (partial): đường PU→BI vẫn có ý nghĩa nhưng yếu đi.
sem_model <- '
  # ===================== MEASUREMENT MODEL ===================================
  SQ   =~ SQ1 + SQ2 + SQ3 + SQ4 + SQ5
  PP   =~ PP1 + PP2 + PP3 + PP4 + PP5
  TR   =~ TR1 + TR2 + TR3
  PU   =~ PU1 + PU2 + PU3 + PU4
  PEOU =~ PEOU1 + PEOU2 + PEOU3 + PEOU4
  BT   =~ BT1 + BT2 + BT3 + BT4
  BI   =~ BI1 + BI2 + BI3

  # ===================== STRUCTURAL MODEL ====================================
  # Biến nhận thức TAM (PEOU, PU)
  PEOU ~ SQ + PP + TR
  PU   ~ PEOU + SQ + PP + TR

  # Brand Trust nhận tác động từ cả 5 tiền đề
  BT ~ a1*PU + a2*PEOU + SQ + PP + TR

  # BI: biến phụ thuộc
  BI ~ b*BT + cp1*PU + cp2*PEOU

  # ===================== INDIRECT EFFECTS (MEDIATION) ========================
  # Đặt nhãn cho từng đường để tính indirect effect
  ind_PU   := a1 * b      # PU   -> BT -> BI
  ind_PEOU := a2 * b      # PEOU -> BT -> BI
  total_PU := cp1 + a1*b  # tổng tác động của PU lên BI
'

fit_sem <- sem(sem_model,
               data      = df_clean,
               estimator = "MLR",
               std.lv    = TRUE)
cat("=== CHỈ SỐ PHÙ HỢP SEM ===\n")
=== CHỈ SỐ PHÙ HỢP SEM ===
fit_sem_idx <- fitMeasures(fit_sem, c("chisq.scaled","df.scaled",
                                       "cfi.robust","tli.robust",
                                       "rmsea.robust","srmr"))
print(round(fit_sem_idx, 3))
chisq.scaled    df.scaled   cfi.robust   tli.robust rmsea.robust         srmr 
     361.201      332.000        0.992        0.991        0.016        0.039 
cat("Chi-sq/df =", round(fit_sem_idx["chisq.scaled"]/fit_sem_idx["df.scaled"], 2))
Chi-sq/df = 1.09
cat("\n=== HỆ SỐ ĐƯỜNG DẪN (STANDARDIZED) ===\n")

=== HỆ SỐ ĐƯỜNG DẪN (STANDARDIZED) ===
params <- parameterEstimates(fit_sem,
                              standardized = TRUE,
                              rsquare      = TRUE)
# Chỉ lấy các đường cấu trúc (loại đường đo lường)
paths <- params %>%
  filter(op %in% c("~", ":=")) %>%
  select(lhs, op, rhs, est, se, pvalue, std.all) %>%
  mutate(across(c(est,se,pvalue,std.all), round, 3),
         Sig = case_when(pvalue < 0.001 ~ "***",
                         pvalue < 0.01  ~ "**",
                         pvalue < 0.05  ~ "*",
                         TRUE           ~ "ns"))
print(paths)
        lhs op      rhs   est    se pvalue std.all Sig
1      PEOU  ~       SQ 0.536 0.096  0.000   0.380 ***
2      PEOU  ~       PP 0.365 0.079  0.000   0.259 ***
3      PEOU  ~       TR 0.427 0.098  0.000   0.303 ***
4        PU  ~     PEOU 0.187 0.087  0.031   0.189   *
5        PU  ~       SQ 0.429 0.098  0.000   0.306 ***
6        PU  ~       PP 0.288 0.090  0.001   0.206  **
7        PU  ~       TR 0.320 0.105  0.002   0.228  **
8        BT  ~       PU 0.055 0.091  0.550   0.045  ns
9        BT  ~     PEOU 0.006 0.089  0.945   0.005  ns
10       BT  ~       SQ 0.676 0.134  0.000   0.399 ***
11       BT  ~       PP 0.574 0.117  0.000   0.339 ***
12       BT  ~       TR 0.507 0.125  0.000   0.299 ***
13       BI  ~       BT 0.295 0.069  0.000   0.374 ***
14       BI  ~       PU 0.268 0.086  0.002   0.280  **
15       BI  ~     PEOU 0.112 0.078  0.147   0.118  ns
16   ind_PU :=     a1*b 0.016 0.027  0.557   0.017  ns
17 ind_PEOU :=     a2*b 0.002 0.026  0.945   0.002  ns
18 total_PU := cp1+a1*b 0.284 0.084  0.001   0.297  **
cat("\n=== R-SQUARED (phương sai giải thích) ===\n")

=== R-SQUARED (phương sai giải thích) ===
r2 <- params %>% filter(op == "r2") %>% select(lhs, est)
print(r2)
     lhs   est
1    SQ1 0.350
2    SQ2 0.564
3    SQ3 0.495
4    SQ4 0.403
5    SQ5 0.332
6    PP1 0.517
7    PP2 0.532
8    PP3 0.620
9    PP4 0.481
10   PP5 0.425
11   TR1 0.112
12   TR2 0.588
13   TR3 0.508
14   PU1 0.585
15   PU2 0.454
16   PU3 0.467
17   PU4 0.530
18 PEOU1 0.573
19 PEOU2 0.597
20 PEOU3 0.573
21 PEOU4 0.445
22   BT1 0.571
23   BT2 0.582
24   BT3 0.524
25   BT4 0.477
26   BI1 0.621
27   BI2 0.521
28   BI3 0.505
29    PU 0.490
30  PEOU 0.496
31    BT 0.652
32    BI 0.442
# --- Bảng kiểm định giả thuyết tổng hợp ---
# Điền tên giả thuyết, đường, và kết quả sau khi chạy

hyp_table <- data.frame(
  Gia_thuyet = c("H1a","H1b","H1c",
                 "H2a","H2b","H2c",
                 "H3",
                 "H4a","H4b","H4c",
                 "H5","H6",
                 "H7","H8","H9",
                 "H10a","H10b"),
  Duong = c("SQ→PEOU","PP→PEOU","TR→PEOU",
             "SQ→PU","PP→PU","TR→PU",
             "PEOU→PU",
             "SQ→BT","PP→BT","TR→BT",
             "PU→BT","PEOU→BT",
             "BT→BI","PU→BI","PEOU→BI",
             "PU→BT→BI (indirect)","PEOU→BT→BI (indirect)"),
  Beta = rep("___", 17),
  p    = rep("___", 17),
  KQ   = rep("Chờ kết quả", 17)
)
knitr::kable(hyp_table, caption = "Bảng kiểm định giả thuyết (điền sau khi chạy)")
Bảng kiểm định giả thuyết (điền sau khi chạy)
Gia_thuyet Duong Beta p KQ
H1a SQ→PEOU ___ ___ Chờ kết quả
H1b PP→PEOU ___ ___ Chờ kết quả
H1c TR→PEOU ___ ___ Chờ kết quả
H2a SQ→PU ___ ___ Chờ kết quả
H2b PP→PU ___ ___ Chờ kết quả
H2c TR→PU ___ ___ Chờ kết quả
H3 PEOU→PU ___ ___ Chờ kết quả
H4a SQ→BT ___ ___ Chờ kết quả
H4b PP→BT ___ ___ Chờ kết quả
H4c TR→BT ___ ___ Chờ kết quả
H5 PU→BT ___ ___ Chờ kết quả
H6 PEOU→BT ___ ___ Chờ kết quả
H7 BT→BI ___ ___ Chờ kết quả
H8 PU→BI ___ ___ Chờ kết quả
H9 PEOU→BI ___ ___ Chờ kết quả
H10a PU→BT→BI (indirect) ___ ___ Chờ kết quả
H10b PEOU→BT→BI (indirect) ___ ___ Chờ kết quả

10.2.1 Viết kết quả vào bài luận (mẫu câu):

Mô hình cấu trúc đạt độ phù hợp tốt (CFI = ; RMSEA = ). Kết quả cho thấy niềm tin thương hiệu có tác động dương và mạnh nhất đến ý định hành vi (β = ___; p < 0.001), ủng hộ giả thuyết H7.

Kết quả kiểm định bằng phương pháp bootstrap (1.000 mẫu lặp) xác nhận vai trò trung gian của niềm tin thương hiệu trong mối quan hệ giữa nhận thức sự hữu ích và ý định hành vi. Hiệu ứng gián tiếp có ý nghĩa thống kê (β = ___; CI 95% [; ], không chứa 0), ủng hộ giả thuyết H10a.


11 Kiểm định trung gian bằng Bootstrap (tùy chọn — chuẩn nhất)

# Bootstrap mất ~5-10 phút — chỉ chạy khi đã chạy xong SEM thường ở trên.
# Đặt eval=TRUE để kích hoạt chunk này khi Knit.
set.seed(2026)
fit_boot <- sem(sem_model,
                data      = df_clean,
                se        = "bootstrap",
                bootstrap = 1000)

# Xem khoảng tin cậy cho indirect effects
boot_ci <- parameterEstimates(fit_boot,
                               boot.ci.type = "perc",
                               level        = 0.95) %>%
  filter(label %in% c("ind_PU","ind_PEOU","total_PU")) %>%
  select(label, est, ci.lower, ci.upper, pvalue)
print(boot_ci)
# Nếu ci.lower và ci.upper đều cùng dấu (cả 2 > 0 hoặc < 0) → có trung gian

Đọc kết quả Bootstrap: ind_PU: est=0.12, CI=[0.06, 0.19] → CI không chứa 0 → có trung gian → H10a ủng hộ ✅


12 Tóm tắt quy trình phân tích

Lộ trình phân tích từ đầu đến cuối
Buoc Phan_tich Cong_cu Viet_vao_chuong
1 Import & Screening read_excel, filter, drop_na Chương 3: Phương pháp
2 Descriptive Statistics describe(), table() Chương 4.1: Thực trạng mẫu
3 Cronbach’s Alpha psych::alpha() Chương 4.2: Đo lường
4 EFA KMO, fa.parallel, fa() Chương 4.2: Đo lường
5 CFA cfa(), fitMeasures, reliability() Chương 4.2: Đo lường
6 Correlation cor(), corrplot() Chương 4.2: Phân tích
7 Regression lm(), vif() Chương 4.2 (tham khảo)
8 SEM sem(), parameterEstimates() Chương 4.2: Kiểm định H
9 Bootstrap Mediation sem(se=‘bootstrap’) Chương 4.2: Trung gian

📌 Nhắc nhở cuối:

  1. Luôn đi theo thứ tự: Cronbach → EFA → CFA (đo lường trước) → SEM (cấu trúc sau).
  2. Nếu CFA chưa đạt → không chạy SEM để kiểm định giả thuyết.
  3. Khi có dữ liệu thật → chỉ thay file ở bước Import, mọi code chạy lại y hệt.
  4. Bấm Knit → Knit to HTML để có báo cáo đẹp gửi GVHD.

File tạo bằng R Markdown | Dữ liệu: AI_appliance_survey_DATA.xlsx Mô hình: SQ, PP, TR → PEOU, PU → Brand Trust (BT) → Behavioral Intention (BI)