Abstrak

Perkembangan metode pembayaran modern telah mengubah perilaku transaksi konsumen muda dan berpotensi meningkatkan risiko terjadinya overspending. Penelitian ini menganalisis faktor-faktor yang memengaruhi perilaku overspending pada konsumen muda menggunakan regresi logistik biner, dengan data sekunder dari Survey Data on Payment Methods and Overspending Among Young Consumers (Mendeley Data). Setelah pembersihan data, diperoleh 674 observasi yang dianalisis. Seleksi variabel dilakukan melalui uji bivariat (Chi-Square dan Wilcoxon), kemudian model dibentuk menggunakan backward stepwise selection berbasis AIC. Model terbaik terdiri atas variabel prepaid card usage, daily cash usage, daily credit card usage, daily prepaid usage, dan employed. Variabel daily credit card usage memiliki pengaruh terbesar terhadap peluang overspending dengan odds ratio = 13,42. Model memiliki kecocokan baik (Hosmer–Lemeshow, p = 0,2752), signifikan secara keseluruhan (Likelihood Ratio Test, p < 0,001), dan kemampuan diskriminasi cukup baik (AUC = 0,7114). Secara umum, overspending lebih banyak dipengaruhi oleh pola penggunaan metode pembayaran dibandingkan karakteristik demografis maupun literasi keuangan.

Kata kunci: regresi logistik biner; overspending; metode pembayaran; literasi keuangan; konsumen muda; odds ratio.

1 Pendahuluan

Perkembangan teknologi keuangan mengubah pola transaksi masyarakat, terutama pada konsumen muda. Kemudahan metode pembayaran modern—kartu kredit, kartu prabayar, hingga pembayaran digital—di satu sisi memberi fleksibilitas, namun di sisi lain berpotensi mendorong perilaku konsumsi berlebihan (overspending), yaitu kondisi pengeluaran yang melampaui kemampuan atau perencanaan keuangan individu. Pada konsumen muda, risiko ini penting diperhatikan karena mereka berada pada fase pembentukan kebiasaan keuangan jangka panjang.

Literatur menunjukkan bahwa instrumen pembayaran non-tunai dapat mengurangi persepsi terhadap pengeluaran aktual sehingga mendorong konsumsi, sementara literasi keuangan yang baik cenderung meningkatkan kemampuan individu mengendalikan pengeluaran. Karakteristik demografis (gender, status kerja, pendapatan, dsb.) juga diduga berperan, meski temuan riset sebelumnya masih beragam.

Karena variabel respons bersifat dikotomi (mengalami overspending atau tidak), regresi logistik biner dipilih sebagai metode analisis. Tujuan penelitian ini adalah mengidentifikasi faktor-faktor yang berpengaruh terhadap peluang overspending sekaligus mengukur besarnya pengaruh masing-masing faktor melalui odds ratio.

2 Metode Penelitian

2.1 Sumber Data

Data merupakan data sekunder dari repositori publik Mendeley Data, berjudul Survey Data on Payment Methods and Overspending Among Young Consumers. Dataset awal memuat 718 responden dengan 17 variabel. Missing value ditangani dengan listwise deletion (na.omit/drop_na), menyisakan 674 observasi yang digunakan dalam analisis dengan 13 variabel independen dan 1 varibael dependen yang digunakan.

2.2 Model Regresi Logistik Biner

Bentuk umum model regresi logistik biner yang digunakan:

\[\ln\left(\frac{P(Y=1)}{1-P(Y=1)}\right) = \beta_0 + \sum_{k=1}^{13}\beta_k X_k\]

dengan \(P(Y=1)\) adalah probabilitas responden mengalami overspending, \(\beta_0\) adalah intersep, dan \(\beta_k\) adalah koefisien regresi untuk variabel prediktor \(X_k\). Parameter diestimasi dengan Maximum Likelihood Estimation (MLE), dan interpretasi pengaruh setiap prediktor dilakukan melalui odds ratio (eksponensial koefisien): OR > 1 menaikkan peluang overspending, OR < 1 menurunkannya.

2.3 Prosedur Analisis

Tahapan analisis dilakukan secara berurutan sebagai berikut:

  1. Uji bivariat — Chi-Square untuk variabel kategorik, Wilcoxon Rank-Sum untuk variabel numerik (literasi keuangan), sebagai penyaringan awal kandidat variabel.
  2. Pembentukan model — model penuh (seluruh prediktor), kemudian seleksi variabel dengan stepwise selection (backward, forward, both) berbasis AIC.
  3. Diagnostik model — uji multikolinearitas (VIF), Hosmer–Lemeshow Goodness of Fit Test, Likelihood Ratio Test, dan Pseudo R² (Cox & Snell, Nagelkerke, McFadden).
  4. Evaluasi performa — confusion matrix, ROC Curve & AUC, penentuan threshold optimal (Indeks Youden), serta Precision-Recall Curve (karena distribusi kelas tidak seimbang).
  5. Diagnostik residual — deviance residuals, Q-Q plot, Cook’s Distance, dan leverage untuk mendeteksi observasi berpengaruh.

Tujuan bagian selanjutnya. Mulai dari sini, dokumen berpindah dari kerangka teori ke praktik: data disiapkan, dieksplorasi, dimodelkan, diuji asumsinya, dievaluasi performanya, lalu didiagnosis residualnya — seluruhnya dijalankan langsung melalui kode R di bawah.


3 Persiapan Data

3.1 Instalasi dan Pemanggilan Package

packages <- c("readxl", "dplyr", "ggplot2", "tidyr", "caret", "pROC",
              "lmtest", "ResourceSelection", "car", "DescTools",
              "knitr", "kableExtra", "corrplot", "gridExtra", "PRROC", "broom")

install_if_missing <- function(pkg) {
  if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg)
}

invisible(lapply(packages, install_if_missing))
invisible(lapply(packages, library, character.only = TRUE))

3.2 Variabel Penelitian

style_table <- function(df, caption = NULL, full_width = FALSE) {
  knitr::kable(df, caption = caption, align = "c", row.names = FALSE) %>%
    kableExtra::kable_styling(
      bootstrap_options = c("striped", "hover", "condensed"),
      full_width = full_width,
      position = "center"
    ) %>%
    kableExtra::row_spec(0, bold = TRUE, color = "#FDF6E3", background = "#117864") %>%
    kableExtra::column_spec(1, bold = TRUE, color = "#0B5345")
}

style_table(tabel_variabel, caption = "Tabel 1. Variabel Penelitian")
Tabel 1. Variabel Penelitian
Variabel Skala Keterangan
Overspending Nominal (0/1) Variabel respons biner (pengeluaran berlebihan)
Cash Payment Usage Nominal (0/1) Penggunaan uang tunai sebagai metode pembayaran
Daily Cash Usage Nominal (0/1) Penggunaan uang tunai harian
Credit Card Usage Nominal (0/1) Penggunaan kartu kredit sebagai metode pembayaran
Daily Credit Card Usage Nominal (0/1) Penggunaan kartu kredit harian
Prepaid Card Usage Nominal (0/1) Penggunaan kartu prabayar sebagai metode pembayaran
Daily Prepaid Card Usage Nominal (0/1) Penggunaan kartu prabayar harian
Total Financial Literacy Score Rasio (0-6) Skor total literasi keuangan
Gender Nominal (0/1) 0=Female, 1=Male
Academic Year Ordinal (1-4) 1=Freshmen … 4=Senior
Department Nominal (1-3) 1=Other, 2=Accounting, 3=Management
Employed Nominal (0/1) 0=Not working, 1=Employed
Income Ordinal (1-3) 1=<1jt, 2=1-3jt, 3=3-5jt
Received Financial Education Nominal (0/1) Pernah/tidak menerima edukasi keuangan

3.3 Impor Data

# Ganti path di bawah ini sesuai lokasi file dataset Anda.
# Jika file tidak ditemukan pada path tersebut, jendela pemilihan file akan terbuka.
data_path <- "Survey Data on Payment Methods and Overspending.xlsx"

if (file.exists(data_path)) {
  df_raw <- read_excel(data_path)
} else {
  df_raw <- read_excel(file.choose())
}

cat("=== DIMENSI DATA ===\n")
## === DIMENSI DATA ===
cat("Jumlah baris :", nrow(df_raw), "\n")
## Jumlah baris : 718
cat("Jumlah kolom :", ncol(df_raw), "\n\n")
## Jumlah kolom : 18
cat("=== NAMA KOLOM ASLI ===\n")
## === NAMA KOLOM ASLI ===
print(names(df_raw))
##  [1] "Respondent's identifier"         "Overspending"                   
##  [3] "Cash payment usage"              "Cash payment frequency"         
##  [5] "Daily cash usage"                "Credit card usage"              
##  [7] "Credit card payment frequency"   "Daily credit card usage"        
##  [9] "Prepaid card usage"              "Prepaid card payment frequency" 
## [11] "Daily prepaid card usage"        "Total financial literacy scores"
## [13] "Male"                            "Acedemic year"                  
## [15] "Department"                      "Employed"                       
## [17] "Income"                          "Received financial education"

Dataset awal berisi 718 responden dengan 17 variabel. Tahap berikutnya melakukan penyesuaian nama kolom, ekstraksi nilai numerik, dan penghapusan missing value.

3.4 Pembersihan dan Pra-pemrosesan Data

Rename kolom agar lebih mudah dipakai dalam analisis:

df <- df_raw %>%
  dplyr::rename(
    id                    = `Respondent's identifier`,
    overspending          = `Overspending`,
    cash_usage            = `Cash payment usage`,
    cash_freq             = `Cash payment frequency`,
    daily_cash            = `Daily cash usage`,
    cc_usage              = `Credit card usage`,
    cc_freq               = `Credit card payment frequency`,
    daily_cc              = `Daily credit card usage`,
    prepaid_usage         = `Prepaid card usage`,
    prepaid_freq          = `Prepaid card payment frequency`,
    daily_prepaid         = `Daily prepaid card usage`,
    fin_literacy          = `Total financial literacy scores`,
    gender                = `Male`,
    academic_year         = `Acedemic year`,
    department            = `Department`,
    employed              = `Employed`,
    income                = `Income`,
    fin_education         = `Received financial education`
  )

Ekstraksi nilai numerik dari kolom berformat teks (misalnya "0=no", "1=yes"):

extract_num <- function(x) {
  as.numeric(sub("^([0-9]+).*", "\\1", as.character(x)))
}

df <- df %>%
  mutate(
    # Variabel dependen: Overspending (0 = no, 1 = yes)
    overspending   = extract_num(overspending),

    # Payment usage (0/1)
    cash_usage     = extract_num(cash_usage),
    cc_usage       = extract_num(cc_usage),
    prepaid_usage  = extract_num(prepaid_usage),

    # Payment frequency (1=never, 2=once a month, 3=once a week, 4=daily)
    cash_freq      = extract_num(cash_freq),
    cc_freq        = extract_num(cc_freq),
    prepaid_freq   = extract_num(prepaid_freq),

    # Daily usage (0/1)
    daily_cash     = extract_num(daily_cash),
    daily_cc       = extract_num(daily_cc),
    daily_prepaid  = extract_num(daily_prepaid),

    # Financial literacy (numerik, sudah langsung)
    fin_literacy   = as.numeric(fin_literacy),

    # Demografis
    gender         = extract_num(gender),        # 0 = female, 1 = male
    academic_year  = extract_num(academic_year),  # 1-4
    department     = extract_num(department),     # 1=other, 2=accounting, 3=management
    employed       = extract_num(employed),
    income         = extract_num(income),         # 1, 2, 3
    fin_education  = extract_num(fin_education)
  )

# Hapus kolom ID dan variabel frekuensi (tumpang tindih dengan usage & daily)
df <- df %>% dplyr::select(-id, -cash_freq, -cc_freq, -prepaid_freq)

Pemeriksaan missing value dan penghapusan baris dengan NA:

cat("=== MISSING VALUE PER KOLOM ===\n")
## === MISSING VALUE PER KOLOM ===
print(colSums(is.na(df)))
##  overspending    cash_usage    daily_cash      cc_usage      daily_cc 
##             3             3             3            10            10 
## prepaid_usage daily_prepaid  fin_literacy        gender academic_year 
##            13            13             0             0             0 
##    department      employed        income fin_education 
##             0             2            26             2
df_clean <- df %>% drop_na()
cat("\nJumlah observasi setelah hapus NA:", nrow(df_clean), "\n")
## 
## Jumlah observasi setelah hapus NA: 674

Konversi ke factor untuk seluruh variabel kategorik:

df_clean <- df_clean %>%
  mutate(
    overspending  = factor(overspending,  levels = c(0, 1), labels = c("No", "Yes")),
    cash_usage    = factor(cash_usage,    levels = c(0, 1), labels = c("Non-user", "Cash user")),
    cc_usage      = factor(cc_usage,      levels = c(0, 1), labels = c("Non-user", "CC user")),
    prepaid_usage = factor(prepaid_usage, levels = c(0, 1), labels = c("Non-user", "Prepaid user")),
    daily_cash    = factor(daily_cash,    levels = c(0, 1)),
    daily_cc      = factor(daily_cc,      levels = c(0, 1)),
    daily_prepaid = factor(daily_prepaid, levels = c(0, 1)),
    gender        = factor(gender,        levels = c(0, 1), labels = c("Female", "Male")),
    academic_year = factor(academic_year, levels = 1:4,
                           labels = c("Freshmen", "Sophomore", "Junior", "Senior")),
    department    = factor(department,    levels = 1:3,
                           labels = c("Other", "Accounting", "Management")),
    employed      = factor(employed,      levels = c(0, 1), labels = c("Not working", "Employed")),
    income        = factor(income,        levels = 1:3,
                           labels = c("<1Jt", "1-3Jt", "3-5Jt")),
    fin_education = factor(fin_education, levels = c(0, 1), labels = c("No", "Yes")),
  )

Setelah pembersihan data, jumlah observasi yang digunakan dalam analisis adalah 674 responden, dengan 51 responden (7,57%) mengalami overspending dan 623 responden (92,43%) tidak mengalami overspending — menunjukkan distribusi kelas yang tidak seimbang (imbalanced).


4 Analisis Deskriptif (Exploratory Data Analysis)

4.1 Distribusi Variabel Respon

cat("--- Distribusi Variabel Dependen ---\n")
## --- Distribusi Variabel Dependen ---
print(table(df_clean$overspending))
## 
##  No Yes 
## 623  51
print(prop.table(table(df_clean$overspending)) * 100)
## 
##        No       Yes 
## 92.433234  7.566766
p1 <- ggplot(df_clean, aes(x = overspending, fill = overspending)) +
  geom_bar(color = "white", width = 0.6) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 4) +
  scale_fill_manual(values = c("No" = EMERALD, "Yes" = GOLD)) +
  labs(title = "Distribusi Overspending", x = "Overspending", y = "Frekuensi") +
  theme_minimal() + theme(legend.position = "none")

p1

Interpretasi. Sebagian besar responden tidak mengalami overspending (623 orang / 92,43%), sementara hanya 51 orang (7,57%) yang mengalami overspending. Ketidakseimbangan ini menjadi pertimbangan penting pada tahap evaluasi model (perlu metrik selain accuracy, seperti AUC dan AUPRC).

4.2 Karakteristik Sosiodemografis vs Overspending

p2 <- ggplot(df_clean, aes(x = gender, fill = overspending)) +
  geom_bar(position = "fill", color = "white") +
  scale_fill_manual(values = c("No" = EMERALD, "Yes" = GOLD)) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Overspending by Gender", x = "Gender", y = "Proporsi", fill = "Overspending") +
  theme_minimal()

p5 <- ggplot(
  df_clean %>% filter(!is.na(income)),
  aes(x = income, fill = overspending)
) +
  geom_bar(position = "fill", color = "white") +
  scale_fill_manual(values = c("No" = EMERALD,
                               "Yes" = GOLD)) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Overspending by Income",
       x = "Income Level",
       y = "Proporsi",
       fill = "Overspending") +
  theme_minimal()

p6 <- ggplot(df_clean, aes(x = academic_year, fill = overspending)) +
  geom_bar(position = "fill", color = "white") +
  scale_fill_manual(values = c("No" = EMERALD, "Yes" = GOLD)) +
  scale_y_continuous(labels = scales::percent) +
  labs(title = "Overspending by Academic Year", x = "Academic Year", y = "Proporsi", fill = "Overspending") +
  theme_minimal()

grid.arrange(p2, p5, p6, ncol = 2)

Interpretasi. Proporsi overspending pada responden laki-laki sedikit lebih tinggi dibandingkan perempuan, walau sebagian besar responden pada kedua kelompok tetap berada pada kategori tidak overspending. Berdasarkan tingkat pendapatan dan tahun akademik, perilaku overspending tersebar relatif merata pada seluruh kategori — mengindikasikan karakteristik demografis ini cenderung tidak membedakan kelompok overspending secara tajam.

4.3 Penggunaan Metode Pembayaran vs Overspending

df_payment <- df_clean %>%
  dplyr::select(overspending, cash_usage, cc_usage, prepaid_usage) %>%
  pivot_longer(-overspending, names_to = "method", values_to = "usage") %>%
  mutate(method = case_match(method,
    "cash_usage"    ~ "Cash",
    "cc_usage"      ~ "Credit Card",
    "prepaid_usage" ~ "Prepaid"))

p3 <- ggplot(df_payment, aes(x = usage, fill = overspending)) +
  geom_bar(position = "fill", color = "white") +
  scale_fill_manual(values = c("No" = EMERALD, "Yes" = GOLD)) +
  scale_y_continuous(labels = scales::percent) +
  facet_wrap(~method) +
  labs(title = "Overspending by Payment Method Usage", x = "Usage", y = "Proporsi", fill = "Overspending") +
  theme_minimal()

p3

Interpretasi. Pengguna kartu kredit dan kartu prabayar menunjukkan proporsi overspending yang lebih tinggi dibandingkan kelompok non-pengguna. Sebaliknya, pada penggunaan uang tunai, perbedaan proporsi overspending antara pengguna dan non-pengguna relatif lebih kecil dibandingkan dua metode pembayaran lainnya.

4.4 Financial Literacy vs Overspending

cat("--- Summary Financial Literacy (Numerik) ---\n")
## --- Summary Financial Literacy (Numerik) ---
print(summary(df_clean$fin_literacy))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.000   2.000   3.000   3.047   4.000   6.000
p4 <- ggplot(df_clean, aes(x = overspending, y = fin_literacy, fill = overspending)) +
  geom_boxplot(alpha = 0.8, outlier.color = GOLD_ACCENT) +
  scale_fill_manual(values = c("No" = EMERALD, "Yes" = GOLD)) +
  labs(title = "Financial Literacy vs Overspending", x = "Overspending", y = "Skor Financial Literacy") +
  theme_minimal() + theme(legend.position = "none")

p4

Interpretasi. Distribusi skor literasi keuangan pada kelompok overspending dan non-overspending memiliki rentang yang relatif serupa, namun terdapat perbedaan pola distribusi yang perlu diuji lebih lanjut secara statistik (lihat Uji Wilcoxon pada bagian berikutnya).


5 Uji Bivariat: Seleksi Variabel Awal

5.1 Uji Chi-Square (Variabel Kategorik)

cat_vars <- c("cash_usage", "cc_usage", "prepaid_usage",
              "daily_cash", "daily_cc", "daily_prepaid",
              "gender", "academic_year", "department",
              "employed", "income", "fin_education")

cat("--- Uji Chi-Square (p < 0.05 = signifikan) ---\n")
## --- Uji Chi-Square (p < 0.05 = signifikan) ---
for (v in cat_vars) {
  tbl <- table(df_clean[[v]], df_clean$overspending)
  test <- chisq.test(tbl, simulate.p.value = TRUE)
  cat(sprintf("%-25s : chi2 = %6.3f, p = %.4f %s\n",
              v, test$statistic, test$p.value,
              ifelse(test$p.value < 0.05, "***", "")))
}
## cash_usage                : chi2 =  1.113, p = 0.3428 
## cc_usage                  : chi2 =  8.028, p = 0.0060 ***
## prepaid_usage             : chi2 =  6.565, p = 0.0150 ***
## daily_cash                : chi2 =  3.306, p = 0.0875 
## daily_cc                  : chi2 = 35.960, p = 0.0005 ***
## daily_prepaid             : chi2 =  0.439, p = 0.6422 
## gender                    : chi2 =  3.243, p = 0.0750 
## academic_year             : chi2 =  4.249, p = 0.2379 
## department                : chi2 =  2.924, p = 0.2404 
## employed                  : chi2 =  3.945, p = 0.0535 
## income                    : chi2 =  0.458, p = 0.8356 
## fin_education             : chi2 =  0.314, p = 0.6702

Interpretasi. Variabel credit card usage, prepaid usage, daily credit card usage, dan employed menunjukkan hubungan signifikan dengan overspending pada taraf 5%. Variabel daily cash usage dan gender signifikan pada taraf 10% (p < 0,10), sehingga masih dipertimbangkan sebagai kandidat meskipun signifikansinya lebih lemah.

5.2 Uji Wilcoxon (Variabel Numerik)

wt <- wilcox.test(fin_literacy ~ overspending, data = df_clean)
print(wt)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  fin_literacy by overspending
## W = 19031, p-value = 0.01609
## alternative hypothesis: true location shift is not equal to 0

Interpretasi. Nilai p-value sebesar 0,0161 (< 0,05) menunjukkan terdapat perbedaan skor literasi keuangan yang signifikan antara kelompok overspending dan non-overspending pada taraf deskriptif/bivariat.


6 Pembentukan Model Regresi Logistik Biner

6.1 Model Penuh (Full Model)

df_model <- df_clean %>%
  mutate(overspending_bin = ifelse(overspending == "Yes", 1, 0)) %>%
  drop_na()

model_full <- glm(overspending_bin ~
                    cash_usage + cc_usage + prepaid_usage +
                    daily_cash + daily_cc + daily_prepaid +
                    fin_literacy + gender + academic_year +
                    department + employed + income + fin_education,
                  data = df_model,
                  family = binomial(link = "logit"),
                  na.action = na.omit)

summary(model_full)
## 
## Call:
## glm(formula = overspending_bin ~ cash_usage + cc_usage + prepaid_usage + 
##     daily_cash + daily_cc + daily_prepaid + fin_literacy + gender + 
##     academic_year + department + employed + income + fin_education, 
##     family = binomial(link = "logit"), data = df_model, na.action = na.omit)
## 
## Coefficients:
##                            Estimate Std. Error z value Pr(>|z|)    
## (Intercept)               -15.40875  714.58790  -0.022 0.982796    
## cash_usageCash user        12.69712  714.58809   0.018 0.985824    
## cc_usageCC user            -0.26962    0.56646  -0.476 0.634092    
## prepaid_usagePrepaid user   1.13261    0.54713   2.070 0.038444 *  
## daily_cash1                 0.89254    0.42753   2.088 0.036829 *  
## daily_cc1                   3.01401    0.82182   3.667 0.000245 ***
## daily_prepaid1             -1.73762    1.13480  -1.531 0.125715    
## fin_literacy               -0.14458    0.12279  -1.178 0.238984    
## genderMale                  0.24529    0.33794   0.726 0.467935    
## academic_yearSophomore      0.24548    0.46295   0.530 0.595944    
## academic_yearJunior        -0.57123    0.50394  -1.134 0.256989    
## academic_yearSenior         0.34127    0.46215   0.738 0.460251    
## departmentAccounting       -0.30207    0.65901  -0.458 0.646691    
## departmentManagement       -0.01819    0.59121  -0.031 0.975450    
## employedEmployed           -0.96663    0.57424  -1.683 0.092314 .  
## income1-3Jt                -0.05752    0.36436  -0.158 0.874559    
## income3-5Jt                -0.62453    0.60491  -1.032 0.301869    
## fin_educationYes           -0.24387    0.33767  -0.722 0.470175    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 329.93  on 633  degrees of freedom
## Residual deviance: 289.63  on 616  degrees of freedom
## AIC: 325.63
## 
## Number of Fisher Scoring iterations: 14

Model penuh memuat seluruh 16 prediktor dan menghasilkan AIC = 325,63. Model ini menjadi titik awal untuk proses seleksi variabel pada tahap stepwise berikutnya.

6.2 Seleksi Variabel: Stepwise AIC

model_null <- glm(overspending_bin ~ 1, data = df_model, family = binomial)

# Backward
cat("--- Stepwise Backward ---\n")
## --- Stepwise Backward ---
model_step_back <- step(model_full, direction = "backward", trace = FALSE)
summary(model_step_back)
## 
## Call:
## glm(formula = overspending_bin ~ prepaid_usage + daily_cash + 
##     daily_cc + daily_prepaid + employed, family = binomial(link = "logit"), 
##     data = df_model, na.action = na.omit)
## 
## Coefficients:
##                           Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                -3.3472     0.3856  -8.681  < 2e-16 ***
## prepaid_usagePrepaid user   1.0437     0.4854   2.150   0.0316 *  
## daily_cash1                 0.9137     0.4135   2.209   0.0271 *  
## daily_cc1                   2.5971     0.6157   4.218 2.46e-05 ***
## daily_prepaid1             -1.4446     1.0473  -1.379   0.1678    
## employedEmployed           -0.9690     0.5465  -1.773   0.0762 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 329.93  on 633  degrees of freedom
## Residual deviance: 298.68  on 628  degrees of freedom
## AIC: 310.68
## 
## Number of Fisher Scoring iterations: 6
# Forward
cat("\n--- Stepwise Forward ---\n")
## 
## --- Stepwise Forward ---
model_step_fwd <- step(model_null, direction = "forward",
                       scope = formula(model_full), trace = FALSE)
summary(model_step_fwd)
## 
## Call:
## glm(formula = overspending_bin ~ daily_cc + daily_cash + employed + 
##     prepaid_usage + daily_prepaid, family = binomial, data = df_model)
## 
## Coefficients:
##                           Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                -3.3472     0.3856  -8.681  < 2e-16 ***
## daily_cc1                   2.5971     0.6157   4.218 2.46e-05 ***
## daily_cash1                 0.9137     0.4135   2.209   0.0271 *  
## employedEmployed           -0.9690     0.5465  -1.773   0.0762 .  
## prepaid_usagePrepaid user   1.0437     0.4854   2.150   0.0316 *  
## daily_prepaid1             -1.4446     1.0473  -1.379   0.1678    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 329.93  on 633  degrees of freedom
## Residual deviance: 298.68  on 628  degrees of freedom
## AIC: 310.68
## 
## Number of Fisher Scoring iterations: 6
# Both
cat("\n--- Stepwise Both (Forward + Backward) ---\n")
## 
## --- Stepwise Both (Forward + Backward) ---
model_step_both <- step(model_null, direction = "both",
                        scope = formula(model_full), trace = FALSE)
summary(model_step_both)
## 
## Call:
## glm(formula = overspending_bin ~ daily_cc + daily_cash + employed + 
##     prepaid_usage + daily_prepaid, family = binomial, data = df_model)
## 
## Coefficients:
##                           Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                -3.3472     0.3856  -8.681  < 2e-16 ***
## daily_cc1                   2.5971     0.6157   4.218 2.46e-05 ***
## daily_cash1                 0.9137     0.4135   2.209   0.0271 *  
## employedEmployed           -0.9690     0.5465  -1.773   0.0762 .  
## prepaid_usagePrepaid user   1.0437     0.4854   2.150   0.0316 *  
## daily_prepaid1             -1.4446     1.0473  -1.379   0.1678    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 329.93  on 633  degrees of freedom
## Residual deviance: 298.68  on 628  degrees of freedom
## AIC: 310.68
## 
## Number of Fisher Scoring iterations: 6

Perbandingan AIC antar metode stepwise:

aic_step <- AIC(model_full, model_step_back, model_step_fwd, model_step_both)

aic_display <- data.frame(
  Model = c("Full", "Backward", "Forward", "Both"),
  df    = aic_step$df,
  AIC   = round(aic_step$AIC, 4)
)
style_table(aic_display, caption = "Tabel: Perbandingan AIC Antar Model")
Tabel: Perbandingan AIC Antar Model
Model df AIC
Full 18 325.6266
Backward 6 310.6772
Forward 6 310.6772
Both 6 310.6772
aic_vals    <- aic_step$AIC
model_list  <- list(model_full, model_step_back, model_step_fwd, model_step_both)
model_names <- c("Full", "Backward", "Forward", "Both")
best_idx    <- which.min(aic_vals)
model_final <- model_list[[best_idx]]

cat(sprintf("-> Model terpilih: %s (AIC = %.4f)\n", model_names[best_idx], aic_vals[best_idx]))
## -> Model terpilih: Backward (AIC = 310.6772)

Ketiga metode stepwise (backward, forward, both) konvergen pada model yang sama dengan AIC = 310,68, lebih kecil dibandingkan model penuh (AIC = 325,63). Model inilah yang ditetapkan sebagai model final.

6.3 Model Final

cat("=================== SUMMARY MODEL FINAL ===================\n")
## =================== SUMMARY MODEL FINAL ===================
summary(model_final)
## 
## Call:
## glm(formula = overspending_bin ~ prepaid_usage + daily_cash + 
##     daily_cc + daily_prepaid + employed, family = binomial(link = "logit"), 
##     data = df_model, na.action = na.omit)
## 
## Coefficients:
##                           Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                -3.3472     0.3856  -8.681  < 2e-16 ***
## prepaid_usagePrepaid user   1.0437     0.4854   2.150   0.0316 *  
## daily_cash1                 0.9137     0.4135   2.209   0.0271 *  
## daily_cc1                   2.5971     0.6157   4.218 2.46e-05 ***
## daily_prepaid1             -1.4446     1.0473  -1.379   0.1678    
## employedEmployed           -0.9690     0.5465  -1.773   0.0762 .  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 329.93  on 633  degrees of freedom
## Residual deviance: 298.68  on 628  degrees of freedom
## AIC: 310.68
## 
## Number of Fisher Scoring iterations: 6

Model regresi logistik biner final yang diperoleh:

\[\ln\left(\frac{\hat{\pi}(x)}{1-\hat{\pi}(x)}\right) = -3,3472 + 1,0437\,X_1 + 0,9137\,X_2 + 2,5971\,X_3 - 1,4446\,X_4 - 0,9690\,X_5\]

dengan \(X_1\) = Prepaid Usage, \(X_2\) = Daily Cash Usage, \(X_3\) = Daily Credit Card Usage, \(X_4\) = Daily Prepaid Card Usage, dan \(X_5\) = Employed.

Interpretasi. Variabel prepaid usage, daily cash usage, dan daily credit card usage berpengaruh signifikan pada taraf 5%, sementara employed signifikan pada taraf 10%. Variabel daily prepaid usage tidak signifikan secara statistik namun tetap dipertahankan dalam model karena kontribusinya terhadap AIC minimum.


7 Interpretasi Odds Ratio

OR <- exp(coef(model_final))
CI <- exp(confint(model_final))

OR_table <- data.frame(
  OR = round(OR, 4),
  CI_lower = round(CI[, 1], 4),
  CI_upper = round(CI[, 2], 4),
  p_value  = round(summary(model_final)$coefficients[, 4], 4)
)

OR_display <- data.frame(Variabel = rownames(OR_table), OR_table, row.names = NULL)
style_table(OR_display, caption = "Tabel: Odds Ratio dan 95% Confidence Interval - Model Final")
Tabel: Odds Ratio dan 95% Confidence Interval - Model Final
Variabel OR CI_lower CI_upper p_value
(Intercept) 0.0352 0.0152 0.0699 0.0000
prepaid_usagePrepaid user 2.8396 1.0143 6.9755 0.0316
daily_cash1 2.4935 1.1673 6.0254 0.0271
daily_cc1 13.4248 3.9520 46.0066 0.0000
daily_prepaid1 0.2358 0.0236 1.5648 0.1678
employedEmployed 0.3795 0.1100 0.9891 0.0762

Forest Plot Odds Ratio:

or_df <- OR_table[-1, ]  # Hapus intercept
or_df$variable <- rownames(or_df)
or_df$signif <- ifelse(or_df$p_value < 0.05, "Signifikan", "Tidak Signifikan")

p_or <- ggplot(or_df, aes(x = reorder(variable, OR), y = OR, color = signif)) +
  geom_point(size = 3) +
  geom_errorbar(aes(ymin = CI_lower, ymax = CI_upper), width = 0.25) +
  geom_hline(yintercept = 1, linetype = "dashed", color = "gray40") +
  scale_color_manual(values = c("Signifikan" = GOLD_ACCENT, "Tidak Signifikan" = GRAY_MUTED)) +
  coord_flip() +
  labs(title = "Forest Plot: Odds Ratio", subtitle = "dengan 95% Confidence Interval",
       x = "Variabel", y = "Odds Ratio", color = "Status") +
  theme_minimal(base_size = 11)

p_or

Daily credit card usage memiliki OR terbesar = 13,42 — responden yang menggunakan kartu kredit setiap hari memiliki peluang overspending 13,42 kali lebih besar dibanding kelompok referensi. Variabel prepaid usage (OR = 2,84) dan daily cash usage (OR = 2,49) juga meningkatkan peluang overspending, sementara employed (OR = 0,38) menurunkan peluang overspending — responden yang bekerja cenderung lebih kecil risikonya mengalami overspending.


8 Uji Asumsi dan Diagnostik Model

8.1 Uji Multikolinearitas (VIF)

cat("Nilai VIF > 10 mengindikasikan multikolinieritas serius\n\n")
## Nilai VIF > 10 mengindikasikan multikolinieritas serius
vif_values <- vif(model_final)

vif_df <- data.frame(Variabel = names(vif_values), VIF = round(as.numeric(vif_values), 3))
style_table(vif_df, caption = "Tabel: Variance Inflation Factor (VIF)")
Tabel: Variance Inflation Factor (VIF)
Variabel VIF
prepaid_usage 1.232
daily_cash 1.017
daily_cc 1.202
daily_prepaid 1.422
employed 1.004

Interpretasi. Seluruh nilai VIF berada jauh di bawah ambang batas 10 (bahkan di bawah 2), sehingga tidak terdapat indikasi multikolinearitas antarprediktor dalam model final.

8.2 Hosmer-Lemeshow Goodness of Fit Test

cat("H0: Model fit (tidak ada perbedaan signifikan observasi vs prediksi)\n")
## H0: Model fit (tidak ada perbedaan signifikan observasi vs prediksi)
cat("Jika p > 0.05 -> model fit baik\n\n")
## Jika p > 0.05 -> model fit baik
hl_test <- hoslem.test(df_model$overspending_bin, fitted(model_final), g = 10)
print(hl_test)
## 
##  Hosmer and Lemeshow goodness of fit (GOF) test
## 
## data:  df_model$overspending_bin, fitted(model_final)
## X-squared = 2.5802, df = 2, p-value = 0.2752

Interpretasi. Nilai p-value = 0,2752 (> 0,05) menunjukkan tidak ada perbedaan signifikan antara frekuensi observasi dan prediksi model, sehingga model dinyatakan memiliki kecocokan (goodness of fit) yang baik.

8.3 Likelihood Ratio Test

model_null <- glm(overspending_bin ~ 1, data = df_model, family = binomial)
lr_test <- lrtest(model_null, model_final)
print(lr_test)
## Likelihood ratio test
## 
## Model 1: overspending_bin ~ 1
## Model 2: overspending_bin ~ prepaid_usage + daily_cash + daily_cc + daily_prepaid + 
##     employed
##   #Df  LogLik Df  Chisq Pr(>Chisq)    
## 1   1 -164.97                         
## 2   6 -149.34  5 31.255  8.341e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Interpretasi. Hasil LRT signifikan (p < 0,001), artinya model final secara keseluruhan signifikan dibandingkan model tanpa prediktor (null model).

8.4 Pseudo R-Square

L0 <- as.numeric(logLik(model_null))
Lm <- as.numeric(logLik(model_final))
n  <- nrow(df_model)

cox_snell  <- 1 - exp((2 / n) * (L0 - Lm))
nagelkerke <- cox_snell / (1 - exp((2 / n) * L0))
mcfadden   <- 1 - (logLik(model_final) / logLik(model_null))

pseudo_r2_df <- data.frame(
  Ukuran = c("Cox & Snell R²", "Nagelkerke R²", "McFadden R²"),
  Nilai  = round(c(as.numeric(cox_snell), nagelkerke, as.numeric(mcfadden)), 4)
)
style_table(pseudo_r2_df, caption = "Tabel: Pseudo R-Square")
Tabel: Pseudo R-Square
Ukuran Nilai
Cox & Snell R² 0.0481
Nagelkerke R² 0.1186
McFadden R² 0.0947

Interpretasi. Nilai Nagelkerke R² = 0,1186 menunjukkan model mampu menjelaskan sekitar 11,86% variasi perilaku overspending — nilai yang wajar untuk model dengan respons biner yang sangat imbalanced, namun mengindikasikan masih ada faktor lain di luar model yang berkontribusi.


9 Evaluasi Performa Model

9.1 Confusion Matrix (Threshold 0.5)

prob_pred <- predict(model_final, type = "response")
class_pred <- ifelse(prob_pred >= 0.5, "Yes", "No")
class_pred <- factor(class_pred, levels = c("No", "Yes"))

cm <- confusionMatrix(class_pred, factor(df_model$overspending, levels = c("No", "Yes")), positive = "Yes")
print(cm)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  No Yes
##        No  584  44
##        Yes   4   2
##                                           
##                Accuracy : 0.9243          
##                  95% CI : (0.9009, 0.9437)
##     No Information Rate : 0.9274          
##     P-Value [Acc > NIR] : 0.6559          
##                                           
##                   Kappa : 0.0612          
##                                           
##  Mcnemar's Test P-Value : 1.811e-08       
##                                           
##             Sensitivity : 0.043478        
##             Specificity : 0.993197        
##          Pos Pred Value : 0.333333        
##          Neg Pred Value : 0.929936        
##              Prevalence : 0.072555        
##          Detection Rate : 0.003155        
##    Detection Prevalence : 0.009464        
##       Balanced Accuracy : 0.518338        
##                                           
##        'Positive' Class : Yes             
## 
cm_table <- as.data.frame(cm$table)
names(cm_table) <- c("Predicted", "Actual", "Freq")

p_cm <- ggplot(cm_table, aes(x = Actual, y = Predicted, fill = Freq)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Freq,
                color = ifelse(Freq > max(Freq) * 0.5, "white", EMERALD_DARK)),
            size = 6, fontface = "bold") +
  scale_color_identity() +
  scale_fill_gradient(low = "#FDF6E3", high = EMERALD_DARK) +
  labs(title = "Confusion Matrix", x = "Actual Class", y = "Predicted Class") +
  theme_minimal()

p_cm

Interpretasi. Pada threshold default 0,5 diperoleh accuracy = 92,43%, namun sensitivity hanya 4,35% (specificity = 99,32%, balanced accuracy = 51,83%). Accuracy yang tinggi ini menyesatkan karena data sangat imbalanced — model hampir selalu memprediksi “tidak overspending”.

9.2 ROC Curve dan AUC

roc_obj <- roc(df_model$overspending_bin, prob_pred)
auc_val <- auc(roc_obj)
cat(sprintf("AUC = %.4f\n", auc_val))
## AUC = 0.7114
plot(roc_obj,
     main = paste("ROC Curve - AUC =", round(auc_val, 4)),
     col = EMERALD_DARK, lwd = 2,
     xlab = "1 - Specificity (False Positive Rate)",
     ylab = "Sensitivity (True Positive Rate)")
abline(a = 0, b = 1, lty = 2, col = "gray60")
legend("bottomright", legend = paste("AUC =", round(auc_val, 4)), col = EMERALD_DARK, lwd = 2)

Interpretasi. Nilai AUC = 0,7114 menunjukkan model memiliki kemampuan diskriminasi yang cukup baik dalam membedakan responden yang mengalami overspending dan yang tidak.

9.3 Penentuan Threshold Optimal (Youden Index)

best_thresh <- coords(roc_obj, "best", ret = "threshold", best.method = "youden")
cat(sprintf("Optimal Threshold (Youden): %.4f\n", best_thresh))
## Optimal Threshold (Youden): 0.0353
class_pred_opt <- ifelse(prob_pred >= as.numeric(best_thresh), "Yes", "No")
class_pred_opt <- factor(class_pred_opt, levels = c("No", "Yes"))

cm_opt <- confusionMatrix(class_pred_opt, factor(df_model$overspending, levels = c("No", "Yes")), positive = "Yes")
print(cm_opt)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  No Yes
##        No  258   5
##        Yes 330  41
##                                           
##                Accuracy : 0.4716          
##                  95% CI : (0.4322, 0.5113)
##     No Information Rate : 0.9274          
##     P-Value [Acc > NIR] : 1               
##                                           
##                   Kappa : 0.0776          
##                                           
##  Mcnemar's Test P-Value : <2e-16          
##                                           
##             Sensitivity : 0.89130         
##             Specificity : 0.43878         
##          Pos Pred Value : 0.11051         
##          Neg Pred Value : 0.98099         
##              Prevalence : 0.07256         
##          Detection Rate : 0.06467         
##    Detection Prevalence : 0.58517         
##       Balanced Accuracy : 0.66504         
##                                           
##        'Positive' Class : Yes             
## 

Interpretasi. Pada threshold optimal (≈ 0,0353), diperoleh sensitivity = 89,13% (naik tajam dari 4,35%), dengan trade-off specificity = 43,88% dan balanced accuracy = 66,50%. Threshold yang lebih rendah lebih sesuai untuk konteks deteksi kasus minoritas (overspending) dibandingkan threshold default 0,5.

9.4 Trade-off Sensitivity vs Specificity

thresholds <- seq(0.1, 0.9, by = 0.05)
thresh_results <- data.frame(Threshold = thresholds)

thresh_results$Sensitivity <- sapply(thresholds, function(t) {
  pred_t <- ifelse(prob_pred >= t, 1, 0)
  cm_t   <- table(Pred = pred_t, Obs = df_model$overspending_bin)
  TP <- ifelse("1" %in% rownames(cm_t) & "1" %in% colnames(cm_t), cm_t["1", "1"], 0)
  FN <- ifelse("0" %in% rownames(cm_t) & "1" %in% colnames(cm_t), cm_t["0", "1"], 0)
  if ((TP + FN) == 0) return(NA)
  TP / (TP + FN)
})

thresh_results$Specificity <- sapply(thresholds, function(t) {
  pred_t <- ifelse(prob_pred >= t, 1, 0)
  cm_t   <- table(Pred = pred_t, Obs = df_model$overspending_bin)
  TN <- ifelse("0" %in% rownames(cm_t) & "0" %in% colnames(cm_t), cm_t["0", "0"], 0)
  FP <- ifelse("1" %in% rownames(cm_t) & "0" %in% colnames(cm_t), cm_t["1", "0"], 0)
  if ((TN + FP) == 0) return(NA)
  TN / (TN + FP)
})

thresh_results$Youden_J <- thresh_results$Sensitivity + thresh_results$Specificity - 1

thresh_display <- data.frame(
  Threshold   = thresh_results$Threshold,
  Sensitivity = round(thresh_results$Sensitivity, 4),
  Specificity = round(thresh_results$Specificity, 4),
  Youden_J    = round(thresh_results$Youden_J, 4)
)
style_table(thresh_display, caption = "Tabel: Sensitivity-Specificity pada Berbagai Threshold")
Tabel: Sensitivity-Specificity pada Berbagai Threshold
Threshold Sensitivity Specificity Youden_J
0.10 0.2174 0.9541 0.1715
0.15 0.2174 0.9541 0.1715
0.20 0.1522 0.9864 0.1386
0.25 0.1304 0.9864 0.1168
0.30 0.1304 0.9864 0.1168
0.35 0.0652 0.9898 0.0550
0.40 0.0652 0.9898 0.0550
0.45 0.0435 0.9932 0.0367
0.50 0.0435 0.9932 0.0367
0.55 0.0435 1.0000 0.0435
0.60 0.0435 1.0000 0.0435
0.65 0.0435 1.0000 0.0435
0.70 0.0435 1.0000 0.0435
0.75 0.0435 1.0000 0.0435
0.80 0.0000 1.0000 0.0000
0.85 0.0000 1.0000 0.0000
0.90 0.0000 1.0000 0.0000
best_row <- which.max(thresh_results$Youden_J)
cat(sprintf("Threshold optimal (Youden J max): %.2f\n", thresh_results$Threshold[best_row]))
## Threshold optimal (Youden J max): 0.10
cat(sprintf("  Sensitivity : %.4f\n", thresh_results$Sensitivity[best_row]))
##   Sensitivity : 0.2174
cat(sprintf("  Specificity : %.4f\n", thresh_results$Specificity[best_row]))
##   Specificity : 0.9541
cat(sprintf("  Youden J    : %.4f\n", thresh_results$Youden_J[best_row]))
##   Youden J    : 0.1715
thresh_long <- thresh_results %>%
  pivot_longer(cols = c(Sensitivity, Specificity), names_to = "Metrik", values_to = "Nilai")

p_thresh <- ggplot(thresh_long, aes(x = Threshold, y = Nilai, color = Metrik)) +
  geom_line(size = 1.2) +
  geom_point(size = 2) +
  geom_vline(xintercept = thresh_results$Threshold[best_row], linetype = "dashed", color = "gray50") +
  annotate("text", x = thresh_results$Threshold[best_row] + 0.03, y = 0.5,
           label = paste("Optimal\n=", thresh_results$Threshold[best_row]),
           size = 3.5, color = "gray30") +
  scale_color_manual(values = c("Sensitivity" = GOLD, "Specificity" = EMERALD)) +
  labs(title = "Trade-off Sensitivity vs Specificity", subtitle = "pada Berbagai Threshold Klasifikasi",
       x = "Threshold", y = "Nilai", color = "Metrik") +
  theme_minimal()

p_thresh

Interpretasi. Semakin tinggi threshold, sensitivity menurun sementara specificity meningkat — menggambarkan trade-off klasik antara mendeteksi kasus positif (overspending) dan menghindari false alarm. Threshold sebesar 0,10 memberikan keseimbangan: sensitivity = 0,217, specificity = 0,954, Youden Index = 0,172.

9.5 Precision-Recall Curve

prevalensi <- mean(df_model$overspending_bin)
cat(sprintf("Prevalensi kelas positif (Overspending = Yes): %.4f (%.2f%%)\n", prevalensi, prevalensi * 100))
## Prevalensi kelas positif (Overspending = Yes): 0.0726 (7.26%)
cat("Jika AUPRC > prevalensi -> model lebih baik dari random guess.\n\n")
## Jika AUPRC > prevalensi -> model lebih baik dari random guess.
pr_obj <- pr.curve(
  scores.class0 = prob_pred[df_model$overspending_bin == 1],
  scores.class1 = prob_pred[df_model$overspending_bin == 0],
  curve = TRUE
)

cat(sprintf("AUPRC (Area Under Precision-Recall Curve) = %.4f\n", pr_obj$auc.integral))
## AUPRC (Area Under Precision-Recall Curve) = 0.2097
plot(pr_obj,
     main = paste("Precision-Recall Curve\nAUPRC =", round(pr_obj$auc.integral, 4)),
     color = GOLD_ACCENT, auc.main = FALSE)
abline(h = prevalensi, lty = 2, col = EMERALD)
legend("topright",
       legend = c(paste("AUPRC =", round(pr_obj$auc.integral, 4)),
                  paste("Baseline =", round(prevalensi, 4))),
       col = c(GOLD_ACCENT, EMERALD), lty = c(1, 2), lwd = 2)

Interpretasi. Karena prevalensi overspending hanya 7,57%, ROC-AUC saja kurang informatif. AUPRC = 0,2097 jauh lebih tinggi dibanding baseline (0,0726), menunjukkan model lebih baik dari tebakan acak dalam mengidentifikasi kelas minoritas — meski nilainya masih relatif rendah, menyiratkan ruang perbaikan model pada penelitian lanjutan.


10 Diagnostik Residual dan Observasi Berpengaruh

dev_resid <- residuals(model_final, type = "deviance")
pearson_resid <- residuals(model_final, type = "pearson")
fitted_vals <- fitted(model_final)

EMERALD_TRANS <- rgb(17 / 255, 120 / 255, 100 / 255, 0.45)

par(mfrow = c(2, 2))

# Plot 1: Deviance Residuals vs Fitted Values
plot(fitted_vals, dev_resid,
     main = "Deviance Residuals vs Fitted",
     xlab = "Fitted Values (Probabilitas)", ylab = "Deviance Residuals",
     pch = 16, col = EMERALD_TRANS)
abline(h = 0, col = "#B8860B", lty = 2)
lines(lowess(fitted_vals, dev_resid), col = EMERALD_DARK, lwd = 2)

# Plot 2: Q-Q Plot Deviance Residuals
qqnorm(dev_resid, main = "Q-Q Plot Deviance Residuals", pch = 16, col = EMERALD_TRANS)
qqline(dev_resid, col = "#B8860B", lwd = 2)

# Plot 3: Cook's Distance
cooksd <- cooks.distance(model_final)
plot(cooksd, type = "h",
     main = "Cook's Distance", xlab = "Observasi", ylab = "Cook's Distance",
     col = ifelse(cooksd > 4 / nrow(df_model), GOLD, EMERALD))
abline(h = 4 / nrow(df_model), col = "#B8860B", lty = 2)

# Plot 4: Leverage vs Standardized Pearson Residuals
leverage <- hatvalues(model_final)
std_pearson <- rstandard(model_final, type = "pearson")
plot(leverage, std_pearson,
     main = "Leverage vs Std. Pearson Residuals",
     xlab = "Leverage", ylab = "Std. Pearson Residuals",
     pch = 16, col = EMERALD_TRANS)
abline(h = c(-2, 2), col = "#B8860B", lty = 2)
abline(v = 2 * mean(leverage), col = "#8C6A00", lty = 2)

par(mfrow = c(1, 1))
influential <- which(cooksd > 4 / nrow(df_model))
cat("Jumlah observasi berpengaruh (Cook's D > 4/n):", length(influential), "\n")
## Jumlah observasi berpengaruh (Cook's D > 4/n): 23

Interpretasi. Residual deviance tersebar di sekitar garis nol tanpa pola sistematis, dan Q-Q plot menunjukkan kesesuaian yang baik dengan distribusi normal (sedikit penyimpangan di ekor). Ditemukan 23 observasi dengan Cook’s Distance melebihi batas 4/n, namun tidak ada nilai yang ekstrem — sehingga observasi tersebut tetap dipertahankan dan tidak dianggap mendistorsi hasil model secara serius. Plot leverage juga tidak menunjukkan kombinasi leverage-residual yang ekstrem, mengonfirmasi tidak ada observasi yang mendominasi pembentukan model.


Kesimpulan

Berdasarkan analisis terhadap 674 responden, model regresi logistik biner final terdiri atas variabel prepaid card usage, daily cash usage, daily credit card usage, daily prepaid usage, dan employed. Di antara variabel tersebut, daily credit card usage memiliki pengaruh terbesar terhadap peluang overspending (OR = 13,42), diikuti oleh prepaid usage (OR = 2,84) dan daily cash usage (OR = 2,49); sementara status bekerja (employed) cenderung menurunkan risiko overspending (OR = 0,38).

Model memiliki kecocokan baik (Hosmer–Lemeshow p = 0,2752), signifikan secara keseluruhan (LRT p < 0,001), mampu menjelaskan 11,86% variasi overspending (Nagelkerke R²), dan memiliki kemampuan diskriminasi cukup baik (AUC = 0,7114; AUPRC = 0,2097 jauh di atas baseline 0,0726).

Kesimpulan utama: perilaku overspending pada konsumen muda lebih banyak dipengaruhi oleh pola penggunaan metode pembayaran — khususnya kartu kredit — dibandingkan karakteristik demografis maupun tingkat literasi keuangan semata. Temuan ini menegaskan pentingnya pengelolaan penggunaan instrumen pembayaran non-tunai sebagai upaya mitigasi risiko pengeluaran berlebih pada konsumen muda.


sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_Indonesia.utf8  LC_CTYPE=English_Indonesia.utf8   
## [3] LC_MONETARY=English_Indonesia.utf8 LC_NUMERIC=C                      
## [5] LC_TIME=English_Indonesia.utf8    
## 
## time zone: Asia/Jakarta
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] broom_1.0.9             PRROC_1.4               rlang_1.2.0            
##  [4] gridExtra_2.3           corrplot_0.95           kableExtra_1.4.0       
##  [7] knitr_1.50              DescTools_0.99.60       car_3.1-3              
## [10] carData_3.0-5           ResourceSelection_0.3-6 lmtest_0.9-40          
## [13] zoo_1.8-14              pROC_1.19.0.1           caret_7.0-1            
## [16] lattice_0.22-7          tidyr_1.3.1             ggplot2_4.0.3          
## [19] dplyr_1.1.4             readxl_1.4.5           
## 
## loaded via a namespace (and not attached):
##  [1] gld_2.6.8            magrittr_2.0.3       e1071_1.7-16        
##  [4] compiler_4.5.1       systemfonts_1.3.1    vctrs_0.6.5         
##  [7] reshape2_1.4.4       stringr_1.5.2        pkgconfig_2.0.3     
## [10] fastmap_1.2.0        backports_1.5.0      labeling_0.4.3      
## [13] rmarkdown_2.29       prodlim_2025.04.28   tzdb_0.5.0          
## [16] haven_2.5.5          purrr_1.1.0          xfun_0.53           
## [19] cachem_1.1.0         jsonlite_2.0.0       recipes_1.3.1       
## [22] parallel_4.5.1       R6_2.6.1             bslib_0.9.0         
## [25] stringi_1.8.7        RColorBrewer_1.1-3   parallelly_1.45.1   
## [28] boot_1.3-31          rpart_4.1.24         lubridate_1.9.4     
## [31] jquerylib_0.1.4      cellranger_1.1.0     Rcpp_1.1.0          
## [34] iterators_1.0.14     future.apply_1.20.0  readr_2.1.5         
## [37] Matrix_1.7-3         splines_4.5.1        nnet_7.3-20         
## [40] timechange_0.3.0     tidyselect_1.2.1     rstudioapi_0.17.1   
## [43] abind_1.4-8          yaml_2.3.10          timeDate_4051.111   
## [46] codetools_0.2-20     listenv_0.9.1        tibble_3.3.0        
## [49] plyr_1.8.9           withr_3.0.2          S7_0.2.0            
## [52] evaluate_1.0.5       future_1.67.0        survival_3.8-3      
## [55] proxy_0.4-27         xml2_1.5.1           pillar_1.11.0       
## [58] foreach_1.5.2        stats4_4.5.1         generics_0.1.4      
## [61] hms_1.1.3            scales_1.4.0         rootSolve_1.8.2.4   
## [64] globals_0.18.0       class_7.3-23         glue_1.8.0          
## [67] lmom_3.2             tools_4.5.1          data.table_1.17.8   
## [70] ModelMetrics_1.2.2.2 gower_1.0.2          forcats_1.0.0       
## [73] Exact_3.3            fs_1.6.6             mvtnorm_1.3-3       
## [76] grid_4.5.1           ipred_0.9-15         nlme_3.1-168        
## [79] Formula_1.2-5        cli_3.6.5            textshaping_1.0.4   
## [82] expm_1.0-0           viridisLite_0.4.2    svglite_2.2.2       
## [85] lava_1.8.1           gtable_0.3.6         sass_0.4.10         
## [88] digest_0.6.37        farver_2.1.2         htmltools_0.5.8.1   
## [91] lifecycle_1.0.4      hardhat_1.4.2        httr_1.4.7          
## [94] MASS_7.3-65