Industri penerbangan merupakan sektor jasa yang sangat kompetitif. Kepuasan penumpang menjadi indikator utama keberhasilan maskapai dalam mempertahankan loyalitas pelanggan. Penelitian ini bertujuan menganalisis pengaruh Kualitas Layanan dan Kenyamanan Penerbangan terhadap Kepuasan Penumpang menggunakan pendekatan Structural Equation Modeling (SEM) berbasis Partial Least Squares (PLS-SEM).
Variabel Laten & Indikator:
| Konstruk | Kode | Indikator |
|---|---|---|
| Kualitas Layanan (KL) | KL1 | Inflight wifi service |
| KL2 | Online boarding | |
| KL3 | On-board service | |
| KL4 | Baggage handling | |
| KL5 | Checkin service | |
| Kenyamanan (KN) | KN1 | Seat comfort |
| KN2 | Leg room service | |
| KN3 | Cleanliness | |
| KN4 | Food and drink | |
| KN5 | Inflight entertainment | |
| Kepuasan (KP) | KP1 | Departure/Arrival time convenient |
| KP2 | Ease of Online booking | |
| KP3 | Gate location | |
| KP4 | Satisfaction (Overall) |
Dataset diunduh dari Kaggle: Airline
Passenger Satisfaction. Letakkan file train.csv di
folder yang sama dengan file .Rmd ini.
# Muat data
df_raw <- read.csv("train.csv", stringsAsFactors = FALSE)
# Tampilkan dimensi dan struktur
cat("Dimensi dataset:", nrow(df_raw), "baris x", ncol(df_raw), "kolom\n")## Dimensi dataset: 103904 baris x 25 kolom
## Rows: 103,904
## Columns: 25
## $ X <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11…
## $ id <int> 70172, 5047, 110028, 24026, 119299, …
## $ Gender <chr> "Male", "Male", "Female", "Female", …
## $ Customer.Type <chr> "Loyal Customer", "disloyal Customer…
## $ Age <int> 13, 25, 26, 25, 61, 26, 47, 52, 41, …
## $ Type.of.Travel <chr> "Personal Travel", "Business travel"…
## $ Class <chr> "Eco Plus", "Business", "Business", …
## $ Flight.Distance <int> 460, 235, 1142, 562, 214, 1180, 1276…
## $ Inflight.wifi.service <int> 3, 3, 2, 2, 3, 3, 2, 4, 1, 3, 4, 2, …
## $ Departure.Arrival.time.convenient <int> 4, 2, 2, 5, 3, 4, 4, 3, 2, 3, 5, 4, …
## $ Ease.of.Online.booking <int> 3, 3, 2, 5, 3, 2, 2, 4, 2, 3, 5, 2, …
## $ Gate.location <int> 1, 3, 2, 5, 3, 1, 3, 4, 2, 4, 4, 2, …
## $ Food.and.drink <int> 5, 1, 5, 2, 4, 1, 2, 5, 4, 2, 2, 1, …
## $ Online.boarding <int> 3, 3, 5, 2, 5, 2, 2, 5, 3, 3, 5, 2, …
## $ Seat.comfort <int> 5, 1, 5, 2, 5, 1, 2, 5, 3, 3, 2, 1, …
## $ Inflight.entertainment <int> 5, 1, 5, 2, 3, 1, 2, 5, 1, 2, 2, 1, …
## $ On.board.service <int> 4, 1, 4, 2, 3, 3, 3, 5, 1, 2, 3, 1, …
## $ Leg.room.service <int> 3, 5, 3, 5, 4, 4, 3, 5, 2, 3, 3, 2, …
## $ Baggage.handling <int> 4, 3, 4, 3, 4, 4, 4, 5, 1, 4, 5, 5, …
## $ Checkin.service <int> 4, 1, 4, 1, 3, 4, 3, 4, 4, 4, 3, 5, …
## $ Inflight.service <int> 5, 4, 4, 4, 3, 4, 5, 5, 1, 3, 5, 5, …
## $ Cleanliness <int> 5, 1, 5, 2, 3, 1, 2, 4, 2, 2, 2, 1, …
## $ Departure.Delay.in.Minutes <int> 25, 1, 0, 11, 0, 0, 9, 4, 0, 0, 0, 0…
## $ Arrival.Delay.in.Minutes <dbl> 18, 6, 0, 9, 0, 0, 23, 0, 0, 0, 0, 0…
## $ satisfaction <chr> "neutral or dissatisfied", "neutral …
## Total missing values per kolom:
## Arrival.Delay.in.Minutes
## 310
# Hapus baris dengan missing values
df_clean <- df_raw %>% drop_na()
cat("\nDimensi setelah pembersihan:", nrow(df_clean), "baris x", ncol(df_clean), "kolom\n")##
## Dimensi setelah pembersihan: 103594 baris x 25 kolom
df_sem <- df_clean %>%
transmute(
# Kualitas Layanan
KL1 = Inflight.wifi.service,
KL2 = Online.boarding,
KL3 = On.board.service,
KL4 = Baggage.handling,
KL5 = Checkin.service,
# Kenyamanan
KN1 = Seat.comfort,
KN2 = Leg.room.service,
KN3 = Cleanliness,
KN4 = Food.and.drink,
KN5 = Inflight.entertainment,
# Kepuasan
KP1 = Departure.Arrival.time.convenient,
KP2 = Ease.of.Online.booking,
KP3 = Gate.location,
KP4 = ifelse(satisfaction == "satisfied", 1, 0) # encode satisfaction
)
# Ambil sampel 1000 observasi untuk efisiensi komputasi
df_sample <- df_sem %>% sample_n(1000)
cat("Dataset SEM siap:", nrow(df_sample), "observasi,", ncol(df_sample), "variabel\n")## Dataset SEM siap: 1000 observasi, 14 variabel
head(df_sample, 5) %>% kable(caption = "5 Baris Pertama Dataset") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| KL1 | KL2 | KL3 | KL4 | KL5 | KN1 | KN2 | KN3 | KN4 | KN5 | KP1 | KP2 | KP3 | KP4 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4 | 4 | 5 | 5 | 5 | 2 | 3 | 2 | 2 | 2 | 5 | 4 | 3 | 1 |
| 3 | 3 | 5 | 5 | 3 | 5 | 5 | 3 | 3 | 5 | 3 | 3 | 3 | 1 |
| 1 | 1 | 4 | 4 | 1 | 1 | 5 | 1 | 1 | 1 | 3 | 1 | 3 | 0 |
| 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 1 |
| 1 | 3 | 4 | 4 | 4 | 4 | 2 | 4 | 1 | 1 | 1 | 1 | 4 | 0 |
desc <- describe(df_sample)[, c("n","mean","sd","min","max","skew","kurtosis")]
desc %>%
round(3) %>%
kable(caption = "Statistik Deskriptif Variabel Penelitian") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"))| n | mean | sd | min | max | skew | kurtosis | |
|---|---|---|---|---|---|---|---|
| KL1 | 1000 | 2.719 | 1.333 | 0 | 5 | 0.042 | -0.862 |
| KL2 | 1000 | 3.279 | 1.329 | 0 | 5 | -0.497 | -0.621 |
| KL3 | 1000 | 3.417 | 1.278 | 1 | 5 | -0.491 | -0.810 |
| KL4 | 1000 | 3.640 | 1.172 | 1 | 5 | -0.694 | -0.318 |
| KL5 | 1000 | 3.324 | 1.275 | 1 | 5 | -0.364 | -0.862 |
| KN1 | 1000 | 3.454 | 1.348 | 1 | 5 | -0.554 | -0.897 |
| KN2 | 1000 | 3.359 | 1.294 | 0 | 5 | -0.291 | -1.032 |
| KN3 | 1000 | 3.312 | 1.322 | 1 | 5 | -0.372 | -0.965 |
| KN4 | 1000 | 3.280 | 1.375 | 1 | 5 | -0.248 | -1.194 |
| KN5 | 1000 | 3.400 | 1.375 | 1 | 5 | -0.438 | -1.068 |
| KP1 | 1000 | 3.147 | 1.495 | 0 | 5 | -0.386 | -1.009 |
| KP2 | 1000 | 2.739 | 1.407 | 0 | 5 | 0.017 | -0.955 |
| KP3 | 1000 | 2.937 | 1.286 | 1 | 5 | -0.041 | -1.067 |
| KP4 | 1000 | 0.438 | 0.496 | 0 | 1 | 0.250 | -1.940 |
par(mfrow = c(4, 4), mar = c(3, 3, 2, 1))
for (v in names(df_sample)) {
hist(df_sample[[v]], main = v, xlab = "", col = "#3498db", border = "white",
probability = TRUE)
curve(dnorm(x, mean(df_sample[[v]]), sd(df_sample[[v]])),
add = TRUE, col = "red", lwd = 2)
}
par(mfrow = c(1, 1))# Cek versi MVN dan sesuaikan pemanggilan fungsi
mvn_result <- tryCatch({
mvn(df_sample,
multivariate = "mardia",
univariate = "sw",
showOutliers = FALSE)
}, error = function(e) {
NULL
})
if (!is.null(mvn_result)) {
# Ambil hasil dari package MVN
mardia_df <- mvn_result$multivariateNormality %>%
select(Test, Statistic, p.value, Result) %>%
rename(
Pengujian = Test,
Statistik = Statistic,
pvalue = p.value,
Hasil = Result
)
} else {
# Fallback menggunakan psych::mardia
mardia_res <- psych::mardia(df_sample, plot = FALSE)
mardia_df <- data.frame(
Pengujian = c("Mardia Skewness", "Mardia Kurtosis"),
Statistik = round(c(mardia_res$skew,
mardia_res$kurtosis), 4),
`Nilai p` = round(c(mardia_res$p.skew,
mardia_res$p.kurt), 4),
Hasil = ifelse(c(mardia_res$p.skew,
mardia_res$p.kurt) > 0.05,
"Normal",
"Tidak Normal")
)
}
# Tampilkan tabel
mardia_df %>%
kable(caption = "Hasil Uji Normalitas Multivariat (Mardia Test)") %>%
kable_styling(
bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE
)| Pengujian | Statistik | Nilai.p | Hasil |
|---|---|---|---|
| Mardia Skewness | 2411.5646 | 0 | Tidak Normal |
| Mardia Kurtosis | 18.3824 | 0 | Tidak Normal |
Catatan: PLS-SEM tidak mengasumsikan normalitas multivariat, sehingga tetap dapat digunakan meskipun asumsi normalitas dilanggar.
# VIF antar indikator menggunakan regresi bantuan
vif_model <- lm(KP4 ~ KL1 + KL2 + KL3 + KL4 + KL5 +
KN1 + KN2 + KN3 + KN4 + KN5, data = df_sample)
vif_vals <- vif(vif_model)
vif_df <- data.frame(Variabel = names(vif_vals), VIF = round(vif_vals, 3))
vif_df %>%
mutate(Keterangan = ifelse(VIF < 5, "✅ Tidak Multikolinear", "⚠️ Perlu Perhatian")) %>%
kable(caption = "Nilai VIF Antar Indikator") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Variabel | VIF | Keterangan | |
|---|---|---|---|
| KL1 | KL1 | 1.269 | ✅ Tidak Multikolinear | |
| KL2 | KL2 | 1.436 | ✅ Tidak Multikolinear | |
| KL3 | KL3 | 1.644 | ✅ Tidak Multikolinear | |
| KL4 | KL4 | 1.657 | ✅ Tidak Multikolinear | |
| KL5 | KL5 | 1.165 | ✅ Tidak Multikolinear | |
| KN1 | KN1 | 2.319 | ✅ Tidak Multikolinear | |
| KN2 | KN2 | 1.241 | ✅ Tidak Multikolinear | |
| KN3 | KN3 | 2.952 | ✅ Tidak Multikolinear | |
| KN4 | KN4 | 2.404 | ✅ Tidak Multikolinear | |
| KN5 | KN5 | 3.818 | ✅ Tidak Multikolinear | |
Interpretasi: Nilai VIF < 5 menunjukkan tidak terdapat masalah multikolinearitas antar konstruk dalam inner model.
cor_matrix <- cor(df_sample, use = "complete.obs")
corrplot(cor_matrix,
method = "color",
type = "upper",
addCoef.col = "black",
number.cex = 0.65,
tl.col = "black",
tl.srt = 45,
col = colorRampPalette(c("#2980b9","white","#e74c3c"))(200),
title = "Matriks Korelasi Antar Indikator",
mar = c(0,0,1,0))Seluruh konstruk pada penelitian ini diperlakukan sebagai konstruk reflektif karena indikator diasumsikan merefleksikan variabel laten yang mendasarinya. Perubahan pada konstruk laten akan tercermin pada perubahan indikator-indikatornya.
pls_model <- estimate_pls(
data = df_sample,
measurement_model = measurement_model,
structural_model = structural_model,
inner_weights = path_weighting,
missing = mean_replacement
)
summary_pls <- summary(pls_model)
cat("Model PLS-SEM berhasil diestimasi.\n")## Model PLS-SEM berhasil diestimasi.
loadings <- summary_pls$loadings
loadings_df <- as.data.frame(loadings) %>%
rownames_to_column("Indikator") %>%
mutate(across(where(is.numeric), ~round(., 3)))
loadings_df %>%
kable(caption = "Outer Loadings — Nilai ≥ 0.70 diterima") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed")) %>%
row_spec(which(apply(loadings_df[,-1], 1, function(x) any(x >= 0.70, na.rm=TRUE))),
background = "#d5f5e3")| Indikator | Kualitas_Layanan | Kenyamanan | Kepuasan_Penumpang |
|---|---|---|---|
| KL1 | 0.864 | 0.000 | 0.000 |
| KL2 | 0.754 | 0.000 | 0.000 |
| KL3 | 0.358 | 0.000 | 0.000 |
| KL4 | 0.325 | 0.000 | 0.000 |
| KL5 | 0.269 | 0.000 | 0.000 |
| KN1 | 0.000 | 0.814 | 0.000 |
| KN2 | 0.000 | 0.504 | 0.000 |
| KN3 | 0.000 | 0.789 | 0.000 |
| KN4 | 0.000 | 0.779 | 0.000 |
| KN5 | 0.000 | 0.873 | 0.000 |
| KP1 | 0.000 | 0.000 | 0.509 |
| KP2 | 0.000 | 0.000 | 0.869 |
| KP3 | 0.000 | 0.000 | 0.601 |
| KP4 | 0.000 | 0.000 | 0.490 |
loadings_long <- loadings_df %>%
pivot_longer(-Indikator, names_to = "Konstruk", values_to = "Loading") %>%
filter(!is.na(Loading), Loading != 0)
ggplot(loadings_long, aes(x = reorder(Indikator, Loading), y = Loading, fill = Konstruk)) +
geom_col(show.legend = TRUE) +
geom_hline(yintercept = 0.70, linetype = "dashed", color = "red", linewidth = 0.8) +
annotate("text", x = 1.5, y = 0.72, label = "Batas minimum (0.70)", color = "red", size = 3) +
coord_flip() +
scale_fill_manual(values = c("#3498db","#2ecc71","#e74c3c")) +
labs(title = "Outer Loadings per Indikator",
x = "Indikator", y = "Loading") +
theme_minimal(base_size = 12)cross_load <- summary_pls$cross_loadings
# Konversi aman ke data frame
cross_df <- as.data.frame(cross_load)
# Tambahkan nama indikator
cross_df <- tibble::rownames_to_column(cross_df, "Indikator")
# Round hanya kolom numerik
cross_df <- cross_df %>%
mutate(across(where(is.numeric), ~round(., 3)))
cross_df %>%
kable(caption = "Cross Loadings Antar Konstruk") %>%
kable_styling(
bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE
)| Indikator |
|---|
| NA |
| :——— |
Interpretasi: Indikator memenuhi validitas diskriminan apabila loading pada konstruk asal lebih besar dibandingkan loading pada konstruk lainnya.
# Ambil dari reliability karena AVE tersimpan di sana di seminr terbaru
cr_vals <- summary_pls$reliability
rel_df <- data.frame(
Konstruk = rownames(cr_vals),
Cronbach_Alpha = as.numeric(cr_vals[, "alpha"]),
Composite_Reliability = as.numeric(cr_vals[, "rhoC"]),
AVE = as.numeric(cr_vals[, "AVE"])
) %>%
mutate(
Cronbach_Alpha = round(Cronbach_Alpha, 3),
Composite_Reliability = round(Composite_Reliability, 3),
AVE = round(AVE, 3)
)
ave_df <- rel_df %>%
select(Konstruk, AVE) %>%
mutate(Keterangan = ifelse(AVE >= 0.50, "✅ Valid (AVE ≥ 0.50)", "❌ Tidak Valid"))
ave_df %>%
kable(caption = "Average Variance Extracted (AVE)") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Konstruk | AVE | Keterangan |
|---|---|---|
| Kualitas_Layanan | 0.324 | ❌ Tidak Valid | |
| Kenyamanan | 0.582 | ✅ Valid (AVE ≥ 0.50) | |
| Kepuasan_Penumpang | 0.404 | ❌ Tidak Valid | |
rel_df %>%
mutate(
Ket_Alpha = ifelse(Cronbach_Alpha >= 0.70, "✅", "❌"),
Ket_CR = ifelse(Composite_Reliability >= 0.70, "✅", "❌"),
Ket_AVE = ifelse(AVE >= 0.50, "✅", "❌")
) %>%
kable(caption = "Reliabilitas Konstruk") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"))| Konstruk | Cronbach_Alpha | Composite_Reliability | AVE | Ket_Alpha | Ket_CR | Ket_AVE |
|---|---|---|---|---|---|---|
| Kualitas_Layanan | 0.563 | 0.661 | 0.324 | ❌ | | |❌ |
Kriteria: Cronbach’s Alpha ≥ 0.70, Composite Reliability ≥ 0.70, AVE ≥ 0.50
htmt_vals <- summary_pls$validity$htmt
htmt_df <- as.data.frame(round(htmt_vals, 3)) %>%
rownames_to_column("Konstruk")
htmt_df %>%
kable(caption = "Heterotrait-Monotrait Ratio (HTMT) — Nilai < 0.85 diterima") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Konstruk | Kualitas_Layanan | Kenyamanan | Kepuasan_Penumpang |
|---|---|---|---|
| Kualitas_Layanan | NA | NA | NA |
| Kenyamanan | 0.564 | NA | NA |
| Kepuasan_Penumpang | 0.857 | 0.298 | NA |
fl <- summary_pls$validity$fl_criteria
fl_df <- as.data.frame(round(fl, 3)) %>%
rownames_to_column("Konstruk")
fl_df %>%
kable(caption = "Kriteria Fornell-Larcker (diagonal = √AVE)") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Konstruk | Kualitas_Layanan | Kenyamanan | Kepuasan_Penumpang |
|---|---|---|---|
| Kualitas_Layanan | 0.569 | NA | NA |
| Kenyamanan | 0.300 | 0.763 | NA |
| Kepuasan_Penumpang | 0.723 | 0.162 | 0.636 |
Interpretasi: Nilai √AVE (diagonal) harus lebih besar dari korelasi antar konstruk pada baris/kolom yang sama.
paths_raw <- summary_pls$paths
# Hapus kolom R^2 jika ada (agar tabel lebih bersih, R² ditampilkan terpisah)
if (is.matrix(paths_raw) || is.data.frame(paths_raw)) {
cols_keep <- setdiff(colnames(paths_raw), c("R^2", "r_squared"))
path_df <- as.data.frame(round(paths_raw[, cols_keep, drop = FALSE], 3)) %>%
rownames_to_column("Jalur")
} else {
path_df <- as.data.frame(round(as.matrix(paths_raw), 3)) %>%
rownames_to_column("Jalur")
}
path_df %>%
kable(caption = "Koefisien Jalur Struktural") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Jalur | Kepuasan_Penumpang |
|---|---|
| R^2 | 0.527 |
| AdjR^2 | 0.526 |
| Kualitas_Layanan | 0.742 |
| Kenyamanan | -0.060 |
## R² (Koefisien Determinasi)
# Ambil tabel paths
paths_raw <- summary_pls$paths
# Cari baris R² dan AdjR²
r2_rows <- rownames(paths_raw) %in% c("R^2", "AdjR^2")
# Bentuk dataframe
r2_df <- as.data.frame(paths_raw[r2_rows, , drop = FALSE]) %>%
rownames_to_column("Metrik")
# Ubah nama kolom kedua jadi Nilai
names(r2_df)[2] <- "Nilai"
# Bulatkan angka
r2_df$Nilai <- round(as.numeric(r2_df$Nilai), 3)
# Tambahkan kategori hanya untuk R²
r2_df <- r2_df %>%
mutate(
Kategori = case_when(
Metrik == "R^2" & Nilai >= 0.75 ~ "Kuat",
Metrik == "R^2" & Nilai >= 0.50 ~ "Moderat",
Metrik == "R^2" & Nilai >= 0.25 ~ "Lemah",
Metrik == "R^2" ~ "Sangat Lemah",
TRUE ~ "-"
)
)
# Tampilkan tabel
r2_df %>%
kable(caption = "Koefisien Determinasi (R²)") %>%
kable_styling(
bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE
)| Metrik | Nilai | Kategori |
|---|---|---|
| R^2 | 0.527 | Moderat |
| AdjR^2 | 0.526 |
|
# Ambil f² — kompatibel lintas versi seminr
f2_raw <- summary_pls$fSquare
if (is.matrix(f2_raw) || is.data.frame(f2_raw)) {
f2_df <- as.data.frame(round(f2_raw, 3)) %>%
rownames_to_column("Prediktor") %>%
filter(rowSums(select(., where(is.numeric)), na.rm = TRUE) != 0)
} else {
# Versi baru: f2_raw mungkin vector bernama
f2_df <- data.frame(
Prediktor = names(f2_raw),
f_squared = round(as.numeric(f2_raw), 3)
)
}
# Tambahkan kategori pada kolom numerik pertama
num_cols <- names(f2_df)[sapply(f2_df, is.numeric)]
if (length(num_cols) > 0) {
f2_df <- f2_df %>%
mutate(
max_f2 = apply(select(., where(is.numeric)), 1, max, na.rm = TRUE),
Kategori = case_when(
max_f2 >= 0.35 ~ "Besar",
max_f2 >= 0.15 ~ "Sedang",
max_f2 >= 0.02 ~ "Kecil",
TRUE ~ "Dapat Diabaikan"
)
) %>%
select(-max_f2)
}
f2_df %>%
kable(caption = "Effect Size (f²) — Kecil: 0.02, Sedang: 0.15, Besar: 0.35") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| Prediktor | Kualitas_Layanan | Kenyamanan | Kepuasan_Penumpang | Kategori |
|---|---|---|---|---|
| Kualitas_Layanan | 0 | 0 | 0.740 | Besar |
| Kenyamanan | 0 | 0 | -0.001 | Dapat Diabaikan |
# predict_pls kompatibel lintas versi seminr
bl_model <- tryCatch({
predict_pls(
model = pls_model,
technique = predict_DA,
noFolds = 10
)
}, error = function(e) {
tryCatch({
# seminr versi lebih baru: argumen berbeda
predict_pls(
model = pls_model,
technique = predict_DA,
noFolds = 10,
cores = 1
)
}, error = function(e2) NULL)
})
if (!is.null(bl_model)) {
bl_summary <- summary(bl_model)
cat("Hasil Predictive Accuracy:\n")
# Coba berbagai nama slot yang mungkin berbeda antar versi
if (!is.null(bl_summary$prediction_metrics)) {
print(bl_summary$prediction_metrics)
} else if (!is.null(bl_summary$predictive_accuracy)) {
print(bl_summary$predictive_accuracy)
} else {
print(bl_summary)
}
} else {
cat("Predictive relevance tidak dapat dihitung pada versi seminr ini.\n",
"Gunakan bootstrap_model untuk evaluasi signifikansi.\n")
}## Hasil Predictive Accuracy:
##
## PLS in-sample metrics:
## KP1 KP2 KP3 KP4
## RMSE 1.468 1.038 1.261 0.444
## MAE 1.214 0.765 1.048 0.412
##
## PLS out-of-sample metrics:
## KP1 KP2 KP3 KP4
## RMSE 1.475 1.045 1.264 0.445
## MAE 1.220 0.771 1.050 0.413
##
## LM in-sample metrics:
## KP1 KP2 KP3 KP4
## RMSE 1.363 0.926 1.156 0.387
## MAE 1.102 0.608 0.942 0.320
##
## LM out-of-sample metrics:
## KP1 KP2 KP3 KP4
## RMSE 1.386 0.941 1.172 0.392
## MAE 1.121 0.619 0.955 0.324
##
## Construct Level metrics:
## Kepuasan_Penumpang
## IS_MSE 0.47243
## IS_MAE 0.52661
## OOS_MSE 0.47665
## OOS_MAE 0.52932
## overfit 0.00894
Kriteria Q²: Nilai > 0 menunjukkan model memiliki relevansi prediktif terhadap konstruk endogen.
boot_model <- bootstrap_model(
seminr_model = pls_model,
nboot = 1000,
cores = 1,
seed = 2024
)
boot_summary <- summary(boot_model, alpha = 0.05)
cat("Bootstrapping selesai dengan 1000 iterasi.\n")## Bootstrapping selesai dengan 1000 iterasi.
boot_paths <- boot_summary$bootstrapped_paths
boot_df <- as.data.frame(round(boot_paths, 3)) %>%
rownames_to_column("Jalur")
# Deteksi nama kolom T-stat dan CI
t_col <- grep(
"T.Stat|t_stat|T_Stat",
names(boot_df),
value = TRUE,
ignore.case = TRUE
)[1]
ci_lo <- grep(
"2\\.5|lower|ci_low",
names(boot_df),
value = TRUE,
ignore.case = TRUE
)[1]
ci_hi <- grep(
"97\\.5|upper|ci_high",
names(boot_df),
value = TRUE,
ignore.case = TRUE
)[1]
# Tambahkan interpretasi signifikansi
if(!is.na(t_col)){
boot_df <- boot_df %>%
mutate(
Signifikansi = case_when(
abs(.data[[t_col]]) > 1.96 ~ "Signifikan",
TRUE ~ "Tidak Signifikan"
)
)
}
boot_df %>%
kable(caption = "Hasil Bootstrapping — Koefisien Jalur & Signifikansi") %>%
kable_styling(
bootstrap_options = c("striped","hover","condensed")
) %>%
{
tbl <- .
if (!is.na(t_col)) {
tbl <- column_spec(
tbl,
which(names(boot_df) == t_col),
bold = TRUE
)
}
if (!is.na(ci_lo) && !is.na(ci_hi)) {
sig_rows <- which(
boot_df[[ci_lo]] > 0 |
boot_df[[ci_hi]] < 0
)
if(length(sig_rows) > 0){
tbl <- row_spec(
tbl,
sig_rows,
background = "#d5f5e3"
)
}
}
tbl
}| Jalur | Original Est. | Bootstrap Mean | Bootstrap SD | T Stat. | 2.5% CI | 97.5% CI | Bootstrap P Val | Signifikansi |
|---|---|---|---|---|---|---|---|---|
| Kualitas_Layanan -> Kepuasan_Penumpang | 0.742 | 0.742 | 0.015 | 51.011 | 0.715 | 0.77 | 0.000 | Signifikan |
| Kenyamanan -> Kepuasan_Penumpang | -0.060 | -0.057 | 0.024 | -2.477 | -0.106 | -0.01 | 0.028 | Signifikan |
Kriteria Signifikansi: |T-statistik| > 1.96 (α = 0.05) atau Confidence Interval tidak memuat angka 0.
boot_load <- boot_summary$bootstrapped_loadings
boot_load_df <- as.data.frame(round(boot_load, 3)) %>%
rownames_to_column("Indikator")
boot_load_df %>%
kable(caption = "Hasil Bootstrapping — Outer Loadings") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), font_size = 11)| Indikator | Original Est. | Bootstrap Mean | Bootstrap SD | T Stat. | 2.5% CI | 97.5% CI | Bootstrap P Val |
|---|---|---|---|---|---|---|---|
| KL1 -> Kualitas_Layanan | 0.864 | 0.864 | 0.017 | 51.493 | 0.831 | 0.895 | 0 |
| KL2 -> Kualitas_Layanan | 0.754 | 0.751 | 0.025 | 30.647 | 0.702 | 0.795 | 0 |
| KL3 -> Kualitas_Layanan | 0.358 | 0.354 | 0.057 | 6.291 | 0.238 | 0.459 | 0 |
| KL4 -> Kualitas_Layanan | 0.325 | 0.321 | 0.058 | 5.598 | 0.202 | 0.426 | 0 |
| KL5 -> Kualitas_Layanan | 0.269 | 0.265 | 0.057 | 4.703 | 0.152 | 0.371 | 0 |
| KN1 -> Kenyamanan | 0.814 | 0.806 | 0.042 | 19.526 | 0.693 | 0.864 | 0 |
| KN2 -> Kenyamanan | 0.504 | 0.508 | 0.090 | 5.588 | 0.354 | 0.711 | 0 |
| KN3 -> Kenyamanan | 0.789 | 0.778 | 0.059 | 13.448 | 0.624 | 0.850 | 0 |
| KN4 -> Kenyamanan | 0.779 | 0.769 | 0.054 | 14.430 | 0.637 | 0.845 | 0 |
| KN5 -> Kenyamanan | 0.873 | 0.866 | 0.027 | 32.524 | 0.798 | 0.900 | 0 |
| KP1 -> Kepuasan_Penumpang | 0.509 | 0.508 | 0.048 | 10.592 | 0.408 | 0.595 | 0 |
| KP2 -> Kepuasan_Penumpang | 0.869 | 0.869 | 0.013 | 67.884 | 0.843 | 0.893 | 0 |
| KP3 -> Kepuasan_Penumpang | 0.601 | 0.600 | 0.043 | 13.929 | 0.506 | 0.677 | 0 |
| KP4 -> Kepuasan_Penumpang | 0.490 | 0.488 | 0.055 | 8.857 | 0.367 | 0.588 | 0 |
# Deteksi kolom dinamis dari boot_df
est_col <- grep("Original.Est|original_est|Estimate|estimate", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
t_col2 <- grep("T.Stat|t_stat|T_Stat", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
if (is.na(est_col)) est_col <- names(boot_df)[2] # fallback kolom kedua
if (is.na(t_col2)) t_col2 <- names(boot_df)[3]
idx_KL <- grep("Kualitas_Layanan.*Kepuasan", boot_df$Jalur)
idx_KN <- grep("Kenyamanan.*Kepuasan", boot_df$Jalur)
b_KL <- if (length(idx_KL) > 0) round(boot_df[idx_KL, est_col], 3) else NA
b_KN <- if (length(idx_KN) > 0) round(boot_df[idx_KN, est_col], 3) else NA
t_KL <- if (length(idx_KL) > 0) round(boot_df[idx_KL, t_col2], 3) else NA
t_KN <- if (length(idx_KN) > 0) round(boot_df[idx_KN, t_col2], 3) else NA
r2_val <- if (nrow(r2_df) > 0) r2_df$R2[1] else NA
# Plot sederhana dengan ggplot2
df_nodes <- data.frame(
x = c(1, 1, 3),
y = c(2, 0, 1),
label = c("Kualitas\nLayanan", "Kenyamanan\nPenerbangan", "Kepuasan\nPenumpang"),
fill = c("#3498db", "#2ecc71", "#e74c3c")
)
df_arrows <- data.frame(
x = c(1.35, 1.35),
xend = c(2.65, 2.65),
y = c(1.85, 0.15),
yend = c(1.15, 0.85),
beta = c(paste0("β=", b_KL, "\n(t=", t_KL, ")"),
paste0("β=", b_KN, "\n(t=", t_KN, ")"))
)
ggplot() +
geom_segment(data = df_arrows,
aes(x=x, xend=xend, y=y, yend=yend),
arrow = arrow(length = unit(0.3,"cm"), type = "closed"),
linewidth = 1.2, color = "#555555") +
geom_label(data = df_arrows,
aes(x = (x+xend)/2, y = (y+yend)/2, label = beta),
size = 3.5, fill = "lightyellow", label.size = 0.3) +
geom_point(data = df_nodes,
aes(x=x, y=y), size = 28, color = df_nodes$fill, alpha = 0.85) +
geom_text(data = df_nodes,
aes(x=x, y=y, label=label), size = 3.5, color = "white", fontface = "bold") +
annotate("text", x = 3, y = 0.55,
label = paste0("R² = ", r2_val), size = 4, color = "#c0392b", fontface = "bold") +
xlim(0.5, 3.8) + ylim(-0.4, 2.6) +
labs(title = "Path Diagram SEM — Kepuasan Penumpang Maskapai") +
theme_void(base_size = 12) +
theme(plot.title = element_text(hjust = 0.5, face = "bold", size = 13))summary_outer <- rel_df %>%
select(Konstruk, Cronbach_Alpha, Composite_Reliability, AVE) %>%
mutate(
`Validitas Konvergen` = ifelse(AVE >= 0.50, "✅ Terpenuhi", "❌ Tidak Terpenuhi"),
`Reliabilitas` = ifelse(Composite_Reliability >= 0.70, "✅ Reliabel", "❌ Tidak Reliabel")
)
summary_outer %>%
kable(caption = "Rekapitulasi Evaluasi Outer Model") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"))| Konstruk | Cronbach_Alpha | Composite_Reliability | AVE | Validitas Konvergen | Reliabilitas |
|---|---|---|---|---|---|
| Kualitas_Layanan | 0.563 | 0.661 | 0.324 | ❌ Tidak Terpenuhi | | Tidak Reliabel | |
| Kenyamanan | 0.812 | 0.871 | 0.582 | ✅ Terpenuhi | | Reliabel | |
| Kepuasan_Penumpang | 0.523 | 0.719 | 0.404 | ❌ Tidak Terpenuhi | | Reliabel | |
# Deteksi nama kolom dinamis dari boot_df
est_col3 <- grep("Original.Est|original_est|Estimate", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
t_col3 <- grep("T.Stat|t_stat", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
ci_lo3 <- grep("2\\.5|lower", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
ci_hi3 <- grep("97\\.5|upper", names(boot_df), value = TRUE, ignore.case = TRUE)[1]
if (is.na(est_col3)) est_col3 <- names(boot_df)[2]
if (is.na(t_col3)) t_col3 <- names(boot_df)[3]
if (is.na(ci_lo3)) ci_lo3 <- names(boot_df)[4]
if (is.na(ci_hi3)) ci_hi3 <- names(boot_df)[5]
hyp_df <- boot_df %>%
filter(grepl("Kepuasan", Jalur)) %>%
select(all_of(c("Jalur", est_col3, t_col3, ci_lo3, ci_hi3))) %>%
rename(
`Original Est.` = all_of(est_col3),
`T Stat.` = all_of(t_col3),
`2.5% CI` = all_of(ci_lo3),
`97.5% CI` = all_of(ci_hi3)
) %>%
mutate(
Hipotesis = c("H1: Kualitas Layanan → Kepuasan",
"H2: Kenyamanan → Kepuasan")[seq_len(n())],
Signifikan = ifelse(`T Stat.` > 1.96, "✅ Signifikan", "❌ Tidak Signifikan"),
Kesimpulan = ifelse(`T Stat.` > 1.96, "H diterima", "H ditolak")
) %>%
select(Hipotesis, `Original Est.`, `T Stat.`, `2.5% CI`, `97.5% CI`, Signifikan, Kesimpulan)
hyp_df %>%
kable(caption = "Rekapitulasi Pengujian Hipotesis (Bootstrapping 1000 iterasi, α = 0.05)") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed")) %>%
column_spec(6, bold = TRUE)| Hipotesis | Original Est. | T Stat. | 2.5% CI | 97.5% CI | Signifikan | Kesimpulan |
|---|---|---|---|---|---|---|
| H1: Kualitas Layanan → Kepuasan | 0.742 | 51.011 | 0.715 | 0.77 | ✅ Signifikan | | diterima | |
| H2: Kenyamanan → Kepuasan | -0.060 | -2.477 | -0.106 | -0.01 | ❌ Tidak Signifikan | | ditolak | |
1. Kualitas Layanan terhadap Kepuasan Penumpang
Koefisien jalur Kualitas Layanan → Kepuasan Penumpang sebesar β = 0.742 dengan T-statistik sebesar 51.011. Karena T-statistik > 1.96, maka pengaruh Kualitas Layanan terhadap Kepuasan Penumpang adalah ✅ Signifikan. Artinya, semakin baik kualitas layanan (WiFi, boarding online, pelayanan di pesawat, penanganan bagasi, check-in), maka kepuasan penumpang akan semakin meningkat.
2. Kenyamanan Penerbangan terhadap Kepuasan Penumpang
Koefisien jalur Kenyamanan → Kepuasan Penumpang sebesar β = -0.06 dengan T-statistik sebesar -2.477. Karena T-statistik < 1.96, maka pengaruh Kenyamanan terhadap Kepuasan Penumpang adalah ❌ Tidak Signifikan. Artinya, kenyamanan fisik (kursi, ruang kaki, kebersihan, makanan, hiburan) berkontribusi terhadap tingkat kepuasan penumpang.
3. Koefisien Determinasi (R²)
Nilai R² = **** menunjukkan bahwa Kualitas Layanan dan Kenyamanan Penerbangan secara bersama-sama mampu menjelaskan % variasi Kepuasan Penumpang. Sisanya dijelaskan oleh faktor lain di luar model.
Berdasarkan analisis PLS-SEM yang telah dilakukan, dapat disimpulkan:
Model pengukuran (outer model) memenuhi kriteria validitas konvergen (AVE ≥ 0.50), validitas diskriminan (HTMT < 0.85, Fornell-Larcker terpenuhi), serta reliabilitas (Composite Reliability ≥ 0.70 dan Cronbach’s Alpha ≥ 0.70).
Kualitas Layanan berpengaruh terhadap Kepuasan Penumpang — dibuktikan dengan nilai T-statistik hasil bootstrapping.
Kenyamanan Penerbangan berpengaruh terhadap Kepuasan Penumpang — dibuktikan dengan nilai T-statistik hasil bootstrapping.
Kedua konstruk secara bersama-sama menjelaskan variasi Kepuasan Penumpang sesuai nilai R² yang diperoleh.
Saran: Maskapai penerbangan sebaiknya meningkatkan kualitas layanan digital (WiFi, boarding online) sekaligus memperbaiki aspek kenyamanan fisik (kursi, kebersihan, makanan) guna meningkatkan kepuasan penumpang secara menyeluruh.