1 Load Library

library(DiagrammeR)
library(seminr)      
library(tidyverse)   
library(skimr)
library(corrplot)    
library(kableExtra) 

1.1 Load Data

if (!file.exists('Combined_Datalist_v1.1.csv')){
  
  print("Dataset tidak ditemukan...")
  
  print("Memulai Download Dataset...")
  download.file('https://drive.google.com/uc?id=1QEaHEoS_Od4N56U_JTlduEKRaPSXvxi8', 'Combined_Datalist_v1.1.csv', mode = 'wb')
}

# Load dataset
df_raw <- read.csv("Combined_Datalist_v1.1.csv", sep= ";", stringsAsFactors = TRUE)
print("Dataset selesai di Donwload")
## [1] "Dataset selesai di Donwload"
# Cek dimensi dan nama kolom
cat("Dimensi data:", nrow(df_raw), "baris x", ncol(df_raw), "kolom\n")
## Dimensi data: 8980 baris x 18 kolom
colnames(df_raw)
##  [1] "Kecamatan"           "Price"               "Kamar.Tidur"        
##  [4] "Kamar.Mandi"         "Luas.Tanah"          "Luas.Bangunan"      
##  [7] "Sertifikat"          "Daya.Listrik"        "Ruang.Makan"        
## [10] "Ruang.Tamu"          "Kondisi.Perabotan"   "Jumlah.Lantai"      
## [13] "Hadap"               "Terjangkau.Internet" "Lebar.Jalan"        
## [16] "Sumber.Air"          "Hook"                "Kondisi.Properti"

2 Preprocessing

2.1 Cleaning Kolom

# Bersihkan nama kolom (hapus spasi)
df <- df_raw
colnames(df) <- trimws(colnames(df))

# Bersihkan dan konversi kolom Price
# Format asli: " 600.000.000 " -> 600000000
df$Price <- df$Price %>%
  trimws() %>%
  gsub("\\.", "", .) %>%   # hapus titik ribuan
  as.numeric()

# Cek distribusi Price (dalam juta rupiah untuk readability)
cat("Price (juta Rp) - Min:", min(df$Price, na.rm=TRUE)/1e6,
    "| Max:", max(df$Price, na.rm=TRUE)/1e6,
    "| Median:", median(df$Price, na.rm=TRUE)/1e6, "\n")
## Price (juta Rp) - Min: 600 | Max: 85000 | Median: 2200

2.2 Encoding Variabel Kategorikal

#Variabel Biner (Ya=1, Tidak=0)
binary_vars <- c("Ruang.Makan", "Ruang.Tamu", "Terjangkau.Internet", "Hook")

# Cek nama kolom aktual 
colnames(df) <- gsub(" ", ".", colnames(df))

df <- df %>%
  mutate(
    # Biner
    Ruang.Makan        = ifelse(Ruang.Makan == "Ya", 1, 0),
    Ruang.Tamu         = ifelse(Ruang.Tamu == "Ya", 1, 0),
    Terjangkau.Internet = ifelse(Terjangkau.Internet == "Ya", 1, 0),
    Hook               = ifelse(Hook == "Ya", 1, 0),

    # Kondisi Perabotan (ordinal: Unfurnished=0, Semi Furnished=1)
    Kondisi.Perabotan  = case_when(
      Kondisi.Perabotan == "Unfurnished"    ~ 0,
      Kondisi.Perabotan == "Semi Furnished" ~ 1,
      TRUE ~ NA_real_
    ),

    # Kondisi Properti (ordinal: semakin baik = nilai lebih tinggi)
    Kondisi.Properti   = case_when(
      Kondisi.Properti == "Butuh Renovasi"  ~ 1,
      Kondisi.Properti == "Sudah Renovasi"  ~ 2,
      Kondisi.Properti == "Bagus"           ~ 3,
      Kondisi.Properti == "Bagus Sekali"    ~ 4,
      Kondisi.Properti == "Baru"            ~ 5,
      TRUE ~ NA_real_
    ),

    # Sertifikat (ordinal berdasarkan kekuatan hukum)
    Sertifikat = case_when(
      grepl("Lainnya|PPJB|Girik|Adat", Sertifikat) ~ 1,
      grepl("HP|Hak Pakai", Sertifikat)             ~ 2,
      grepl("HGB|Hak Guna", Sertifikat)             ~ 3,
      grepl("SHM|Hak Milik", Sertifikat)            ~ 4,
      TRUE ~ NA_real_
    )
  )

cat("Encoding selesai.\n")
## Encoding selesai.

2.3 Seleksi Kolom Final & Handle Missing Values

# Kolom yang masuk di pake
cols_model <- c(
  # Ukuran Fisik
  "Luas.Tanah", "Luas.Bangunan", "Kamar.Tidur", "Kamar.Mandi", "Jumlah.Lantai",
  # Fasilitas Interior
  "Ruang.Makan", "Ruang.Tamu", "Daya.Listrik",
  # Kualitas & Legalitas
  "Kondisi.Properti", "Sertifikat",
  # Outcome
  "Price"
)

df_model <- df %>%
  select(all_of(cols_model)) %>%
  drop_na()

cat("Jumlah observasi setelah drop NA:", nrow(df_model), "\n")
## Jumlah observasi setelah drop NA: 6444
cat("Kolom yang digunakan:", ncol(df_model), "\n")
## Kolom yang digunakan: 11

3 Exploratory Data Analysis (EDA)

3.1 Statistik Deskriptif

df_model %>%
  skim() %>%
  kable(caption = "Statistik Deskriptif Variabel Model") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Statistik Deskriptif Variabel Model
skim_type skim_variable n_missing complete_rate numeric.mean numeric.sd numeric.p0 numeric.p25 numeric.p50 numeric.p75 numeric.p100 numeric.hist
numeric Luas.Tanah 0 1 1.780357e+02 1.315130e+02 4.5e+01 9.6e+01 1.33e+02 2.1e+02 1.00e+03 ▇▂▁▁▁
numeric Luas.Bangunan 0 1 1.969064e+02 1.455973e+02 5.0e+01 1.1e+02 1.55e+02 2.3e+02 3.30e+03 ▇▁▁▁▁
numeric Kamar.Tidur 0 1 3.539882e+00 1.042385e+00 1.0e+00 3.0e+00 3.00e+00 4.0e+00 7.00e+00 ▂▇▆▂▁
numeric Kamar.Mandi 0 1 2.670081e+00 1.018119e+00 1.0e+00 2.0e+00 3.00e+00 3.0e+00 7.00e+00 ▇▇▂▁▁
numeric Jumlah.Lantai 0 1 1.670081e+00 5.298174e-01 1.0e+00 1.0e+00 2.00e+00 2.0e+00 4.00e+00 ▅▇▁▁▁
numeric Ruang.Makan 0 1 4.442893e-01 4.969252e-01 0.0e+00 0.0e+00 0.00e+00 1.0e+00 1.00e+00 ▇▁▁▁▆
numeric Ruang.Tamu 0 1 5.681254e-01 4.953756e-01 0.0e+00 0.0e+00 1.00e+00 1.0e+00 1.00e+00 ▆▁▁▁▇
numeric Daya.Listrik 0 1 2.991589e+03 1.747596e+03 9.0e+02 2.2e+03 2.20e+03 3.5e+03 1.76e+04 ▇▂▁▁▁
numeric Kondisi.Properti 0 1 3.573246e+00 1.149025e+00 1.0e+00 3.0e+00 3.00e+00 5.0e+00 5.00e+00 ▁▁▇▁▅
numeric Sertifikat 0 1 3.719429e+00 6.698222e-01 1.0e+00 4.0e+00 4.00e+00 4.0e+00 4.00e+00 ▁▁▁▂▇
numeric Price 0 1 3.186012e+09 3.637670e+09 6.0e+08 1.5e+09 2.10e+09 3.5e+09 8.50e+10 ▇▁▁▁▁

3.2 Distribusi Harga Properti

df_model %>%
  mutate(Price_juta = Price / 1e6) %>%
  ggplot(aes(x = Price_juta)) +
  geom_histogram(bins = 50, fill = "#2E86AB", color = "white", alpha = 0.8) +
  scale_x_continuous(labels = scales::comma) +
  labs(
    title = "Distribusi Harga Properti di Surabaya",
    x = "Harga (Juta Rupiah)",
    y = "Frekuensi"
  ) +
  theme_minimal()

3.3 Korelasi Antar Indikator

# Korelasi numerik
cor_matrix <- df_model %>%
  select(-Price) %>%
  cor(use = "complete.obs")

corrplot(cor_matrix,
         method = "color",
         type = "upper",
         tl.cex = 0.8,
         addCoef.col = "black",
         number.cex = 0.6,
         title = "Matriks Korelasi Antar Indikator",
         mar = c(0,0,1,0))


4 Model PLS-SEM

4.1 Definisi Measurement Model (Outer Model)

Di sini kita mendefinisikan konstruk laten beserta indikatornya. Karena data bersifat formatif (indikator membentuk konstruk), kita gunakan composite("nama", all_items("..."), weights = mode_B).

Catatan: mode_B adalah mode PLS untuk model formatif, sedangkan mode_A untuk reflektif.

measurements <- constructs(

  composite("Ukuran_Fisik",
            multi_items("", c("Luas.Tanah", "Luas.Bangunan",
                              "Kamar.Tidur", "Kamar.Mandi",
                              "Jumlah.Lantai")),
            weights = mode_B),

  composite("Fasilitas_Interior",
            multi_items("", c("Ruang.Makan", "Ruang.Tamu",
                              "Daya.Listrik")),
            weights = mode_B),

  composite("Kualitas_Legalitas",
            multi_items("", c("Kondisi.Properti", "Sertifikat")),
            weights = mode_B),

  composite("Harga",
            single_item("Price"))
)

4.2 Definisi Structural Model (Inner Model)

Di sini kita mendefinisikan path (jalur) antar konstruk laten — mana yang mempengaruhi mana.

# Definisi structural model (inner model)
# Sintaks: paths(from = c("eksogen"), to = "endogen")
structure <- relationships(
  paths(from = c("Ukuran_Fisik",
                 "Fasilitas_Interior",
                 "Kualitas_Legalitas"),
        to = "Harga")
)

4.3 Estimasi Model

# Estimasi PLS-SEM
pls_model <- estimate_pls(
  data = as.data.frame(df_model),
  measurement_model = measurements,
  structural_model  = structure,
  inner_weights     = path_weighting  # default: path weighting algorithm
)

# Ringkasan model
summary_pls <- summary(pls_model)
cat("Model berhasil diestimasi.\n")
## Model berhasil diestimasi.

5 Evaluasi Model

5.1 Outer Model (Measurement Model)

Untuk model formatif, evaluasi fokus pada:

  1. Outer Weights — kontribusi masing-masing indikator ke konstruk
  2. VIF (Variance Inflation Factor) — cek multikolinearitas antar indikator (VIF < 5 = aman, VIF < 3.3 = ideal)
# Outer weights (kontribusi indikator)
cat("OUTER WEIGHTS")
## OUTER WEIGHTS
print(summary_pls$weights)
##                  Ukuran_Fisik Fasilitas_Interior Kualitas_Legalitas Harga
## Luas.Tanah              0.840              0.000              0.000 0.000
## Luas.Bangunan           0.219              0.000              0.000 0.000
## Kamar.Tidur            -0.088              0.000              0.000 0.000
## Kamar.Mandi             0.083              0.000              0.000 0.000
## Jumlah.Lantai           0.108              0.000              0.000 0.000
## Ruang.Makan             0.000              0.016              0.000 0.000
## Ruang.Tamu              0.000             -0.013              0.000 0.000
## Daya.Listrik            0.000              1.000              0.000 0.000
## Kondisi.Properti        0.000              0.000              0.992 0.000
## Sertifikat              0.000              0.000              0.205 0.000
## Price                   0.000              0.000              0.000 1.000
# VIF untuk cek multikolinearitas
cat("VIF INDIKATOR (Formatif) ")
## VIF INDIKATOR (Formatif)
print(summary_pls$validity$vif_items)
## Ukuran_Fisik :
##    Luas.Tanah Luas.Bangunan   Kamar.Tidur   Kamar.Mandi Jumlah.Lantai 
##         2.387         2.640         1.688         1.854         1.295 
## 
## Fasilitas_Interior :
##  Ruang.Makan   Ruang.Tamu Daya.Listrik 
##        2.330        2.326        1.003 
## 
## Kualitas_Legalitas :
## Kondisi.Properti       Sertifikat 
##            1.004            1.004 
## 
## Harga :
## Price 
##     1
cat("\nCatatan: VIF < 5 = tidak ada masalah multikolinearitas serius\n")
## 
## Catatan: VIF < 5 = tidak ada masalah multikolinearitas serius
cat("         VIF < 3.3 = ideal untuk model formatif\n")
##          VIF < 3.3 = ideal untuk model formatif

5.2 Inner Model (Structural Model)

Evaluasi inner model fokus pada:

  1. R² (R-squared) — proporsi varians Harga yang dijelaskan oleh konstruk
  2. Path Coefficients — besaran dan arah pengaruh
  3. f² (Effect Size) — seberapa besar kontribusi masing-masing prediktor
cat("R-SQUARED")
## R-SQUARED
print(summary_pls$paths)
##                    Harga
## R^2                0.567
## AdjR^2             0.567
## Ukuran_Fisik       0.695
## Fasilitas_Interior 0.116
## Kualitas_Legalitas 0.059
cat("Interpretasi R²")
## Interpretasi R²
cat("0.19 = lemah | 0.33 = moderat | 0.67 = kuat (Hair et al., 2019)")
## 0.19 = lemah | 0.33 = moderat | 0.67 = kuat (Hair et al., 2019)
# Path coefficients (koefisien jalur)
cat("PATH COEFFICIENTS")
## PATH COEFFICIENTS
print(summary_pls$paths)
##                    Harga
## R^2                0.567
## AdjR^2             0.567
## Ukuran_Fisik       0.695
## Fasilitas_Interior 0.116
## Kualitas_Legalitas 0.059

5.3 Bootstrapping untuk Signifikansi

Bootstrapping digunakan karena PLS-SEM tidak mengasumsikan distribusi normal — kita tidak bisa pakai p-value dari distribusi t biasa.

# Bootstrapping (1000 resample) untuk inferensi statistik
set.seed(123)
boot_model <- bootstrap_model(
  seminr_model = pls_model,
  nboot        = 1000,
  cores        = 1 
)

summary_boot <- summary(boot_model, alpha = 0.05)

cat("HASIL BOOTSTRAPPING: PATH COEFFICIENTS ")
## HASIL BOOTSTRAPPING: PATH COEFFICIENTS
print(summary_boot$bootstrapped_paths)
##                               Original Est. Bootstrap Mean Bootstrap SD T Stat.
## Ukuran_Fisik  ->  Harga               0.695          0.696        0.019  36.926
## Fasilitas_Interior  ->  Harga         0.116          0.116        0.020   5.735
## Kualitas_Legalitas  ->  Harga         0.059          0.058        0.009   6.376
##                               2.5% CI 97.5% CI Bootstrap P Val
## Ukuran_Fisik  ->  Harga         0.661    0.732           0.000
## Fasilitas_Interior  ->  Harga   0.076    0.155           0.000
## Kualitas_Legalitas  ->  Harga   0.039    0.076           0.000
cat("\nKolom penting:\n")
## 
## Kolom penting:
cat("- 'Original Est.' : koefisien jalur dari model asli\n")
## - 'Original Est.' : koefisien jalur dari model asli
cat("- 'Bootstrap Mean': rata-rata dari 1000 bootstrap sample\n")
## - 'Bootstrap Mean': rata-rata dari 1000 bootstrap sample
cat("- 'T Stat.'       : t-statistic (|T| > 1.96 = signifikan pada α=0.05)\n")
## - 'T Stat.'       : t-statistic (|T| > 1.96 = signifikan pada α=0.05)
cat("- '2.5% CI' & '97.5% CI': confidence interval\n")
## - '2.5% CI' & '97.5% CI': confidence interval

6 Interpretasi Hasil

6.1 Ringkasan Pengujian Hipotesis

# Buat tabel ringkasan hipotesis manual berdasarkan hasil bootstrap
hipotesis <- data.frame(
  Hipotesis = c("H1", "H2", "H3"),
  Path = c(
    "Ukuran Fisik → Harga",
    "Fasilitas Interior → Harga",
    "Kualitas & Legalitas → Harga"
  ),
  `Path Coefficient` = c(
    round(summary_pls$paths["Ukuran_Fisik", "Harga"], 3),
    round(summary_pls$paths["Fasilitas_Interior", "Harga"], 3),
    round(summary_pls$paths["Kualitas_Legalitas", "Harga"], 3)
  ),
  check.names = FALSE
)

hipotesis %>%
  kable(caption = "Ringkasan Pengujian Hipotesis") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE) %>%
  column_spec(1, bold = TRUE)
Ringkasan Pengujian Hipotesis
Hipotesis Path Path Coefficient
H1 Ukuran Fisik → Harga 0.695
H2 Fasilitas Interior → Harga 0.116
H3 Kualitas & Legalitas → Harga 0.059

6.2 Plot Path Coefficients

# Visualisasi koefisien jalur
path_df <- data.frame(
  Konstruk = c("Ukuran Fisik", "Fasilitas Interior", "Kualitas & Legalitas"),
  Koefisien = c(
    summary_pls$paths["Ukuran_Fisik", "Harga"],
    summary_pls$paths["Fasilitas_Interior", "Harga"],
    summary_pls$paths["Kualitas_Legalitas", "Harga"]
  )
)

ggplot(path_df, aes(x = reorder(Konstruk, Koefisien), y = Koefisien, fill = Koefisien > 0)) +
  geom_col(width = 0.5, show.legend = FALSE) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
  scale_fill_manual(values = c("TRUE" = "#2E86AB", "FALSE" = "#E84855")) +
  coord_flip() +
  labs(
    title = "Path Coefficients: Pengaruh terhadap Harga Properti",
    x = NULL,
    y = "Path Coefficient"
  ) +
  theme_minimal(base_size = 12)

grViz("
digraph PLS_SEM {
  graph [rankdir = LR, fontname = 'Helvetica']
  node  [fontname = 'Helvetica', fontsize = 10, shape = rectangle, style = filled]
  edge  [fontname = 'Helvetica', fontsize = 9]

  # Indikator - Ukuran Fisik
  node [fillcolor = '#9FE1CB', color = '#0F6E56']
  LT  [label = 'Luas Tanah']
  LB  [label = 'Luas Bangunan']
  KT  [label = 'Kamar Tidur']
  KM  [label = 'Kamar Mandi']
  JL  [label = 'Jumlah Lantai']

  # Indikator - Fasilitas Interior
  RM  [label = 'Ruang Makan']
  RTa [label = 'Ruang Tamu']
  DL  [label = 'Daya Listrik']

  # Indikator - Kualitas & Legalitas
  KP  [label = 'Kondisi Properti']
  S   [label = 'Sertifikat']

  # Indikator - Harga
  node [shape = rectangle]
  PR  [label = 'Price (Rp)']

  # Konstruk Laten
  node [fillcolor = '#CECBF6', color = '#3C3489', shape = ellipse, fontsize = 11]
  UF  [label = 'Ukuran\\nFisik']
  FI  [label = 'Fasilitas\\nInterior']
  KL  [label = 'Kualitas &\\nLegalitas']

  node [fillcolor = '#F5C4B3', color = '#993C1D']
  H   [label = 'Harga\\n(R² = 0.567)']

  # Outer model: indikator → konstruk (formatif)
  LT -> UF; LB -> UF; KT -> UF; KM -> UF; JL -> UF
  RM -> FI; RTa -> FI; DL -> FI
  KP -> KL; S  -> KL
  PR -> H

  # Inner model: konstruk → Harga
  UF -> H [label = 'β = 0.695', penwidth = 4,   color = '#534AB7']
  FI -> H [label = 'β = 0.116', penwidth = 1.8,  color = '#534AB7']
  KL -> H [label = 'β = 0.059', penwidth = 1,    color = '#534AB7']

  # Ranking node (kiri ke kanan)
  { rank = same; LT; LB; KT; KM; JL }
  { rank = same; RM; RTa; DL }
  { rank = same; KP; S }
}
")

7 Kesimpulan

Berdasarkan hasil analisis PLS-SEM dengan menggunakan data properti Surabaya (n = 6444 observasi), diperoleh temuan sebagai berikut:

  1. Ukuran Fisik (luas tanah, luas bangunan, kamar tidur, kamar mandi, jumlah lantai) terbukti berpengaruh positif dan signifikan terhadap harga properti (β = 0.695, t = 36.926, p < 0.05). H1 diterima.

  2. Fasilitas Interior (ruang makan, ruang tamu, daya listrik) terbukti berpengaruh positif dan signifikan terhadap harga properti (β = 0.116, t = 5.735, p < 0.05). H2 diterima.

  3. Kualitas & Legalitas (kondisi properti, sertifikat) terbukti berpengaruh positif dan signifikan terhadap harga properti (β = 0.059, t = 6.376, p < 0.05). H3 diterima.

Nilai R² = 0.567 menunjukkan bahwa model mampu menjelaskan 56.7% variasi harga properti di Surabaya, yang tergolong moderat-kuat menurut kriteria Hair et al. (2022) dengan threshold 0.33 (moderat) dan 0.67 (kuat).

7.1 Limitasi

  • Data bersifat atribut objektif, bukan persepsi — tidak ideal untuk SEM yang dirancang untuk data survei.
  • Beberapa variabel dengan missing value tinggi (Lebar Jalan, Hadap, Sumber Air) tidak diikutsertakan dalam model.
  • Model ini bersifat formatif, bukan reflektif, sehingga evaluasi reliabilitas konvensional (Cronbach Alpha, AVE) tidak relevan.

8 Referensi

  • Hair, J.F., Risher, J.J., Sarstedt, M., & Ringle, C.M. (2019). When to use and how to report the results of PLS-SEM. European Business Review, 31(1), 2–24.
  • Freeman, J. & Zhao, X. (2019). An SEM approach to modeling housing values. In Data Analysis and Applications 1 (pp. 125–135). Wiley.
  • Ray, S. & Danks, N. (2020). seminr: Domain-Specific Language for Building and Estimating PLS-SEM Models. R package.