# ===== UBAH BAGIAN INI =====
file_data   <- "shared_Dataset_Simulasi_Data_Cleaning_1200x15_-_Copy.xlsx"  # data MENTAH (kotor)
file_bersih <- "Dataset_bersih_mahassiwa.xlsx"                              # data BERSIH hasil Excel (untuk pembanding)
# Letakkan kedua file sefolder dengan file .Rmd
# ===========================

# Bila nama file tidak ditemukan, cari otomatis berdasarkan kata kunci pada nama file
if (!file.exists(file_data)) {
  kandidat <- list.files(pattern = "Simulasi_Data_Cleaning.*\\.(xlsx|csv)$", ignore.case = TRUE)
  if (length(kandidat)) file_data <- kandidat[1]
}
if (!file.exists(file_bersih)) {
  kandidat <- list.files(pattern = "bersih.*\\.xlsx$", ignore.case = TRUE)
  if (length(kandidat)) file_bersih <- kandidat[1]
}

baca_file <- function(f) {
  if (!file.exists(f)) return(NULL)
  if (grepl("\\.csv$", f, ignore.case = TRUE)) {
    if (grepl(";", readLines(f, n = 1, warn = FALSE))) read_csv2(f, show_col_types = FALSE) else read_csv(f, show_col_types = FALSE)
  } else read_excel(f)
}

# Samakan nama kolom dengan penulisan baku dan pastikan tipe datanya benar
kanon <- c("ID_Mahasiswa", "Nama", "Gender", "Umur", "Semester", "Program_Studi", "Kota_Asal",
           "Jam_Belajar_Per_Minggu", "Kehadiran_Persen", "Nilai_Tugas", "Nilai_Ujian", "Nilai_Akhir",
           "Jam_Internet_Per_Hari", "Jam_Tidur_Per_Hari", "Kepuasan_Kuliah")
kolom_num <- setdiff(kanon, c("ID_Mahasiswa", "Nama", "Gender", "Program_Studi", "Kota_Asal"))

rapikan <- function(d) {
  if (is.null(d)) return(NULL)
  nama_rapi <- gsub("\\s+", "_", trimws(names(d)))
  cocok <- match(tolower(nama_rapi), tolower(kanon))
  names(d) <- ifelse(is.na(cocok), nama_rapi, kanon[cocok])
  d %>% mutate(across(any_of(kolom_num), ~ suppressWarnings(as.numeric(.x))),
               ID_Mahasiswa = as.character(ID_Mahasiswa))
}

df_mentah <- rapikan(baca_file(file_data))
df_excel  <- rapikan(baca_file(file_bersih))   # boleh NULL; hanya dipakai untuk perbandingan

pakai_simulasi <- is.null(df_mentah)
if (pakai_simulasi) { df_mentah <- buat_data_simulasi(); df_excel <- NULL }
Mode demonstrasi. File data asli tidak ditemukan, jadi laporan ini memakai data simulasi yang meniru masalah pada dataset mahasiswa. Angka di bawah belum mewakili data sebenarnya. Letakkan file data di folder yang sama dengan file .Rmd lalu ubah nilai file_data pada chunk muat-data.

Sebelum dianalisis, data harus dipercaya dulu. Laporan ini memeriksa dataset mahasiswa yang penuh masalah, membersihkannya langkah demi langkah, lalu memakainya untuk menjawab satu pertanyaan: apakah jam belajar dan jam internet berkaitan dengan lama tidur mahasiswa?

Data cleaningData wranglingIntegrasi dan encodingRegresi linear bergandaUji asumsi klasik

1.215
baris data mentah dengan 15 variabel
46
sel kosong (missing value)
15
ID mahasiswa yang muncul lebih dari sekali
14
nilai di luar batas wajar (noisy)
1.200
baris tersisa setelah duplikat dibuang
EksplorasiMengenal isi dan struktur dataset
›
IdentifikasiMencari kosong, kembar, tidak konsisten, dan tidak wajar
›
WranglingMembersihkan dan merapikan data
›
Integrasi dan encodingMenggabungkan tabel dan mengubah kategori jadi angka
›
RegresiMembangun model dan menguji asumsinya

Pendahuluan

Latar belakang

Data mahasiswa menyimpan banyak cerita: siapa mereka, bagaimana kondisi akademiknya, seberapa lama mereka belajar dan berselancar di internet, berapa jam mereka tidur, sampai seberapa puas mereka dengan perkuliahan. Cerita itu hanya berguna kalau datanya bersih.

Karena itu, sebelum analisis lanjutan dijalankan, kualitas data perlu diperiksa lebih dulu. Ada empat hal yang dicari: nilai yang kosong, penulisan yang tidak seragam, baris yang tercatat dua kali, dan angka yang mustahil terjadi. Laporan ini merekam seluruh proses itu pada sebuah dataset mahasiswa.

Tujuan

  1. Memahami kondisi dan gambaran umum dataset mahasiswa.
  2. Menemukan data yang kosong, kembar, tidak konsisten, atau tidak wajar.
  3. Membersihkan dan merapikan dataset sebelum dipakai untuk analisis.
  4. Menguji apakah jam belajar per minggu dan jam internet per hari berkaitan dengan jam tidur per hari.

Mengenal dataset

1.215
baris (objek: mahasiswa)
15
kolom atau variabel
Terstruktur
berbentuk tabel baris dan kolom
kamus <- tribble(
  ~Variabel,                 ~Jenis,       ~Arti,
  "ID_Mahasiswa",            "Teks",       "Kode unik tiap mahasiswa",
  "Nama",                    "Teks",       "Nama mahasiswa",
  "Gender",                  "Kategori",   "Jenis kelamin",
  "Umur",                    "Numerik",    "Usia dalam tahun",
  "Semester",                "Numerik",    "Semester yang sedang ditempuh",
  "Program_Studi",           "Kategori",   "Program studi",
  "Kota_Asal",               "Kategori",   "Kota asal mahasiswa",
  "Jam_Belajar_Per_Minggu",  "Numerik",    "Total jam belajar dalam seminggu",
  "Kehadiran_Persen",        "Numerik",    "Persentase kehadiran kuliah (0 sampai 100)",
  "Nilai_Tugas",             "Numerik",    "Nilai tugas (skala 0 sampai 100)",
  "Nilai_Ujian",             "Numerik",    "Nilai ujian (skala 0 sampai 100)",
  "Nilai_Akhir",             "Numerik",    "Nilai akhir (skala 0 sampai 100)",
  "Jam_Internet_Per_Hari",   "Numerik",    "Lama memakai internet dalam sehari",
  "Jam_Tidur_Per_Hari",      "Numerik",    "Lama tidur dalam sehari",
  "Kepuasan_Kuliah",         "Ordinal",    "Tingkat kepuasan kuliah (skala 1 sampai 5)"
)
kable(kamus)
Variabel Jenis Arti
ID_Mahasiswa Teks Kode unik tiap mahasiswa
Nama Teks Nama mahasiswa
Gender Kategori Jenis kelamin
Umur Numerik Usia dalam tahun
Semester Numerik Semester yang sedang ditempuh
Program_Studi Kategori Program studi
Kota_Asal Kategori Kota asal mahasiswa
Jam_Belajar_Per_Minggu Numerik Total jam belajar dalam seminggu
Kehadiran_Persen Numerik Persentase kehadiran kuliah (0 sampai 100)
Nilai_Tugas Numerik Nilai tugas (skala 0 sampai 100)
Nilai_Ujian Numerik Nilai ujian (skala 0 sampai 100)
Nilai_Akhir Numerik Nilai akhir (skala 0 sampai 100)
Jam_Internet_Per_Hari Numerik Lama memakai internet dalam sehari
Jam_Tidur_Per_Hari Numerik Lama tidur dalam sehari
Kepuasan_Kuliah Ordinal Tingkat kepuasan kuliah (skala 1 sampai 5)
datatable(df_mentah, rownames = FALSE, class = "compact stripe",
          options = list(pageLength = 6, scrollX = TRUE, dom = "tip"),
          caption = "Data mentah sebelum dibersihkan. Gulir ke kanan untuk melihat semua kolom.")

Asal dan jenis data

Data eksternal dan sekunder. Asal-usul pembuat dataset tidak diketahui, sehingga sumbernya belum terverifikasi. Data ini memang disiapkan sebagai bahan latihan pembersihan (dirty data) dan tidak diambil langsung dari para mahasiswanya, jadi statusnya data sekunder.
Data terstruktur. Datanya rapi dalam baris dan kolom, punya skema variabel yang jelas, dan mudah dibuka di Excel maupun R.

Menemukan masalah di dalam data

Ada empat kelompok masalah. Kita telusuri satu per satu.

Data yang kosong

na_tbl <- df_mentah %>%
  summarise(across(everything(), ~ sum(is.na(.x)))) %>%
  pivot_longer(everything(), names_to = "Variabel", values_to = "Kosong") %>%
  filter(Kosong > 0) %>%
  arrange(desc(Kosong))

ggplot(na_tbl, aes(reorder(Variabel, Kosong), Kosong)) +
  geom_col(fill = pal$steel, width = .7) +
  geom_text(aes(label = Kosong), hjust = -.3, color = pal$navy, fontface = "bold") +
  coord_flip() + scale_y_continuous(expand = expansion(mult = c(0, .15))) +
  labs(title = "Sel kosong per variabel",
       subtitle = paste("Total", n_kosong, "sel kosong pada data mentah"),
       x = NULL, y = "Jumlah data kosong")

Sel kosong tersebar di beberapa variabel, terutama jam belajar dan kehadiran. Jika dibiarkan, perhitungan rata-rata dan model statistik bisa terganggu atau bahkan gagal jalan.

Penulisan yang tidak konsisten

Satu kategori bisa ditulis dengan banyak cara. Bagi manusia artinya sama, tetapi R membacanya sebagai kategori yang berbeda.

kelompok_gender <- function(x) case_when(
  str_to_lower(str_trim(x)) %in% c("l", "laki-laki", "male", "pria") ~ "Laki-laki",
  str_to_lower(str_trim(x)) %in% c("p", "perempuan", "female", "wanita") ~ "Perempuan",
  TRUE ~ "Lainnya")

g1 <- df_mentah %>% count(Gender) %>% filter(!is.na(Gender)) %>%
  mutate(Kelompok = kelompok_gender(Gender)) %>%
  ggplot(aes(reorder(Gender, n), n, fill = Kelompok)) +
  geom_col(width = .7) + coord_flip() +
  scale_fill_manual(values = c("Laki-laki" = pal$steel, "Perempuan" = pal$orange, "Lainnya" = "grey60")) +
  labs(title = "Gender: satu makna, banyak tulisan", x = NULL, y = "Jumlah baris", fill = NULL)

k1 <- df_mentah %>% count(Kota_Asal) %>% filter(!is.na(Kota_Asal)) %>%
  mutate(Kota_Baku = str_to_title(str_trim(Kota_Asal))) %>%
  ggplot(aes(reorder(Kota_Asal, n), n, fill = Kota_Baku)) +
  geom_col(width = .7) + coord_flip() +
  scale_fill_viridis_d(option = "mako", end = .85) +
  labs(title = "Kota_Asal: beda huruf besar dan kecil", x = NULL, y = "Jumlah baris", fill = NULL) +
  theme(legend.position = "none")

g1 + k1

Pada kolom Gender, 544 baris ditulis dengan varian selain “Laki-laki” dan “Perempuan” (misalnya Male, Pria, L, Female, Wanita, P). Pada kolom Kota_Asal, 373 baris memakai huruf besar atau kecil yang tidak baku (misalnya “pekanbaru” atau “PADANG”). Warna yang sama pada grafik kota menandakan kota yang sebenarnya satu.

Baris yang kembar

tibble(Tahap = factor(c("Data mentah", "Setelah duplikat dibuang"),
                      levels = c("Data mentah", "Setelah duplikat dibuang")),
       Baris = c(n_awal, n_setelah_dup)) %>%
  ggplot(aes(Tahap, Baris, fill = Tahap)) +
  geom_col(width = .55) +
  geom_text(aes(label = fmt_n(Baris)), vjust = -.5, fontface = "bold", color = pal$navy, size = 5) +
  scale_fill_manual(values = c(pal$red, pal$green), guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .12))) +
  labs(title = "Jumlah baris sebelum dan sesudah menghapus duplikat",
       subtitle = paste0(n_id_kembar, " ID muncul lebih dari sekali; ", n_baris_sama, " baris identik persis"),
       x = NULL, y = NULL)

Mahasiswa yang tercatat dua kali akan terhitung dua kali dan bisa menggeser hasil analisis. Berikut ID yang bermasalah:

df_mentah %>%
  filter(ID_Mahasiswa %in% id_kembar$ID_Mahasiswa) %>%
  arrange(ID_Mahasiswa) %>%
  select(ID_Mahasiswa, Nama, Gender, Semester, Program_Studi) %>%
  datatable(rownames = FALSE, class = "compact stripe", options = list(pageLength = 6, dom = "tip"))

Angka yang mustahil (noisy data)

Setiap variabel punya batas kewajaran: umur tidak mungkin negatif, kehadiran tidak boleh lebih dari 100 persen, satu hari hanya 24 jam, dan skala kepuasan hanya 1 sampai 5.

rules %>%
  mutate(`Nilai tidak valid` = n_noisy_var,
         `Rentang wajar` = paste(min, "sampai", max)) %>%
  select(Variabel = var, `Rentang wajar`, `Nilai tidak valid`) %>%
  filter(`Nilai tidak valid` > 0) %>%
  arrange(desc(`Nilai tidak valid`)) %>%
  kable()
Variabel Rentang wajar Nilai tidak valid
Jam_Internet_Per_Hari 0 sampai 24 3
Umur 15 sampai 60 2
Kehadiran_Persen 0 sampai 100 2
Nilai_Tugas 0 sampai 100 2
Nilai_Ujian 0 sampai 100 2
Kepuasan_Kuliah 1 sampai 5 2
Nilai_Akhir 0 sampai 100 1

Membersihkan data (data wrangling)

Pembersihan dilakukan dalam lima langkah berurutan. Urutannya penting: nilai mustahil diubah menjadi kosong terlebih dahulu, baru semua sel kosong diisi bersama-sama.

# Langkah 1. Buang baris kembar (identik, lalu ID kembar: ambil kemunculan pertama)
df_1 <- df_mentah %>% distinct() %>% distinct(ID_Mahasiswa, .keep_all = TRUE)

# Langkah 2 dan 3. Seragamkan huruf kota dan tulisan gender
df_2 <- df_1 %>%
  mutate(
    Kota_Asal = str_to_title(str_trim(Kota_Asal)),
    Gender = case_when(
      str_to_lower(str_trim(Gender)) %in% c("l", "laki-laki", "male", "pria") ~ "Laki-laki",
      str_to_lower(str_trim(Gender)) %in% c("p", "perempuan", "female", "wanita") ~ "Perempuan",
      TRUE ~ NA_character_))

# Langkah 4. Ubah nilai di luar batas wajar menjadi kosong (NA)
di_luar <- function(x, lo, hi) ifelse(x < lo | x > hi, NA_real_, x)
df_3 <- df_2
for (i in seq_len(nrow(rules))) {
  v <- rules$var[i]
  df_3[[v]] <- di_luar(df_3[[v]], rules$min[i], rules$max[i])
}

# Langkah 5. Isi semua sel kosong pada kolom numerik dengan median kolomnya
df_bersih <- df_3
kolom_isi <- df_bersih %>% select(where(is.numeric)) %>% names() %>% setdiff("ID_Mahasiswa")
for (k in kolom_isi) {
  df_bersih[[k]][is.na(df_bersih[[k]])] <- median(df_bersih[[k]], na.rm = TRUE)
}

Kenapa median, bukan rata-rata? Median tidak ikut tertarik oleh nilai ekstrem, jadi hasil pengisiannya lebih stabil pada data yang sempat berantakan.

Hati-hati saat merapikan teks. Di Excel, fitur Replace All tanpa opsi “Match entire cell contents” ikut mengganti potongan kata: mengganti “Male” menjadi “Laki-laki” akan mengubah “Female” menjadi “FeLaki-laki”. Kode R di atas mencocokkan seluruh isi sel, sehingga jebakan ini tidak terjadi.

tabel_hasil <- tibble(
  Pemeriksaan = c("Baris data", "Sel kosong", "Varian Gender tidak baku", "Kota_Asal tidak baku"),
  Sebelum = c(n_awal, n_kosong, n_gender_var, n_kota_var),
  Sesudah = c(nrow(df_bersih), sum(is.na(df_bersih[kolom_num])),
              sum(!df_bersih$Gender %in% c("Laki-laki", "Perempuan"), na.rm = TRUE),
              sum(df_bersih$Kota_Asal != str_to_title(str_trim(df_bersih$Kota_Asal)), na.rm = TRUE))
)
kable(tabel_hasil, format.args = list(big.mark = "."))
Pemeriksaan Sebelum Sesudah
Baris data 1.215 1.200
Sel kosong 46 0
Varian Gender tidak baku 544 0
Kota_Asal tidak baku 373 0
g2 <- df_bersih %>% count(Gender) %>%
  ggplot(aes(Gender, n, fill = Gender)) + geom_col(width = .6) +
  geom_text(aes(label = n), vjust = -.4, fontface = "bold", color = pal$navy) +
  scale_fill_manual(values = c("Laki-laki" = pal$steel, "Perempuan" = pal$orange), na.value = "grey60", guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .15))) +
  labs(title = "Gender setelah dirapikan", x = NULL, y = "Jumlah mahasiswa")

k2 <- df_bersih %>% count(Kota_Asal) %>%
  ggplot(aes(reorder(Kota_Asal, n), n, fill = Kota_Asal)) + geom_col(width = .6) +
  geom_text(aes(label = n), hjust = -.3, fontface = "bold", color = pal$navy) +
  coord_flip() + scale_fill_viridis_d(option = "mako", end = .85, guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .15))) +
  labs(title = "Kota asal setelah dirapikan", x = NULL, y = "Jumlah mahasiswa")

g2 + k2

Setelah semua langkah, dataset berisi 1.200 baris, tanpa sel kosong, tanpa ID kembar, dengan penulisan kategori yang seragam dan semua angka berada dalam batas wajar.

Analisis: apakah belajar dan internet berkaitan dengan tidur?

Pertanyaan dan validasi data

Pertanyaan analisis: apakah durasi memakai internet per hari dan waktu belajar per minggu berkaitan dengan lama tidur mahasiswa per hari?

Sebelum modelnya dibuat, data dipastikan siap lewat lima pemeriksaan:

  1. Variabel yang dipakai sesuai dengan tujuan analisis.
  2. Semua variabel model berbentuk numerik agar bisa diproses R.
  3. Tidak ada sel kosong atau nilai yang tidak bisa dipakai.
  4. Satuan sesuai: jam per hari untuk internet dan tidur, jam per minggu untuk belajar.
  5. Data yang lolos dipakai untuk membentuk model regresi.
variabel_model <- c("Jam_Tidur_Per_Hari", "Jam_Belajar_Per_Minggu", "Jam_Internet_Per_Hari")
tibble(Variabel = variabel_model,
       Numerik = map_lgl(variabel_model, ~ is.numeric(df_bersih[[.x]])),
       `Sel kosong` = map_int(variabel_model, ~ sum(is.na(df_bersih[[.x]]))),
       Minimum = map_dbl(variabel_model, ~ min(df_bersih[[.x]])),
       Maksimum = map_dbl(variabel_model, ~ max(df_bersih[[.x]]))) %>%
  kable()
Variabel Numerik Sel kosong Minimum Maksimum
Jam_Tidur_Per_Hari TRUE 0 3.5 9.1
Jam_Belajar_Per_Minggu TRUE 0 1.0 30.1
Jam_Internet_Per_Hari TRUE 0 0.5 9.2

Integrasi data: menggabungkan dua tabel

Untuk melatih integrasi, informasi dipisah menjadi dua tabel lalu digabung kembali. Kuncinya adalah ID_Mahasiswa, supaya setiap kategori belajar menempel pada mahasiswa yang benar.

Data 1ID, jam tidur, jam belajar per minggu, jam internet per hari
+
Data 2ID dan hasil encoding jam belajar (kategori dan kode)
=
Data gabunganSemua informasi dalam satu tabel, cocok lewat ID

Jam belajar dikelompokkan menjadi tiga tingkat, lalu tiap tingkat diberi kode angka (encoding) agar bisa dipakai dalam hitungan statistik:

Jam belajar per minggu Kategori Kode
kurang dari 10 jam Rendah 1
10 sampai 20 jam Sedang 2
lebih dari 20 jam Tinggi 3
data1 <- df_bersih %>%
  select(ID_Mahasiswa, Jam_Tidur_Per_Hari, Jam_Belajar_Per_Minggu, Jam_Internet_Per_Hari)

data2 <- df_bersih %>%
  transmute(
    ID_Mahasiswa,
    Jam_Belajar_Factor = factor(
      ifelse(Jam_Belajar_Per_Minggu < 10, "Rendah",
             ifelse(Jam_Belajar_Per_Minggu <= 20, "Sedang", "Tinggi")),
      levels = c("Rendah", "Sedang", "Tinggi"), ordered = TRUE),
    Jam_Belajar_Code = as.numeric(Jam_Belajar_Factor))

df_final <- data1 %>% left_join(data2, by = "ID_Mahasiswa")

# Pemeriksaan integritas sebelum dan sesudah penggabungan
tibble(
  Pemeriksaan = c("ID pada Data 1 unik", "ID pada Data 2 unik",
                  "ID Data 1 tanpa pasangan di Data 2", "Jumlah baris data gabungan"),
  Hasil = c(ifelse(anyDuplicated(data1$ID_Mahasiswa) == 0, "Lolos", "Gagal"),
            ifelse(anyDuplicated(data2$ID_Mahasiswa) == 0, "Lolos", "Gagal"),
            as.character(nrow(anti_join(data1, data2, by = "ID_Mahasiswa"))),
            fmt_n(nrow(df_final)))
) %>% kable()
Pemeriksaan Hasil
ID pada Data 1 unik Lolos
ID pada Data 2 unik Lolos
ID Data 1 tanpa pasangan di Data 2 0
Jumlah baris data gabungan 1.200
datatable(df_final, rownames = FALSE, class = "compact stripe",
          options = list(pageLength = 6, scrollX = TRUE, dom = "tip"),
          caption = "Data gabungan hasil integrasi dan encoding.") %>%
  formatRound(c("Jam_Tidur_Per_Hari", "Jam_Belajar_Per_Minggu", "Jam_Internet_Per_Hari"), 1)

Melihat pola sebelum memodelkan

warna_kat <- c("Rendah" = pal$orange, "Sedang" = pal$steel, "Tinggi" = pal$navy)

p_kat <- df_final %>% count(Jam_Belajar_Factor) %>%
  mutate(persen = n / sum(n)) %>%
  ggplot(aes(Jam_Belajar_Factor, n, fill = Jam_Belajar_Factor)) +
  geom_col(width = .6) +
  geom_text(aes(label = paste0(n, " (", percent(persen, accuracy = .1), ")")), vjust = -.4, color = pal$navy, fontface = "bold") +
  scale_fill_manual(values = warna_kat, guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .15))) +
  labs(title = "Sebaran tingkat jam belajar", x = NULL, y = "Jumlah mahasiswa")

p_vio <- ggplot(df_final, aes(Jam_Belajar_Factor, Jam_Tidur_Per_Hari, fill = Jam_Belajar_Factor)) +
  geom_violin(alpha = .75, color = NA) +
  geom_boxplot(width = .13, fill = "white", outlier.alpha = .3) +
  scale_fill_manual(values = warna_kat, guide = "none") +
  labs(title = "Jam tidur menurut tingkat belajar", x = NULL, y = "Jam tidur per hari")

p_kat + p_vio

df_final %>%
  select(`Jam tidur per hari` = Jam_Tidur_Per_Hari,
         `Jam belajar per minggu` = Jam_Belajar_Per_Minggu,
         `Jam internet per hari` = Jam_Internet_Per_Hari) %>%
  pivot_longer(everything()) %>%
  ggplot(aes(value)) +
  geom_histogram(aes(y = after_stat(density)), bins = 30, fill = pal$mist, color = "white") +
  geom_density(color = pal$navy, linewidth = 1) +
  facet_wrap(~ name, scales = "free") +
  labs(title = "Sebaran tiga variabel utama", x = NULL, y = "Kepadatan")

Model regresi linear berganda

Model yang dibangun menempatkan jam tidur sebagai variabel yang dijelaskan, dan jam belajar serta jam internet sebagai penjelasnya.

model <- lm(Jam_Tidur_Per_Hari ~ Jam_Belajar_Per_Minggu + Jam_Internet_Per_Hari, data = df_final)

hasil <- tidy(model, conf.int = TRUE) %>%
  mutate(Variabel = recode(term, `(Intercept)` = "Konstanta",
                           Jam_Belajar_Per_Minggu = "Jam belajar per minggu",
                           Jam_Internet_Per_Hari = "Jam internet per hari"),
         Signifikan = ifelse(p.value < .05, "Ya", "Tidak")) %>%
  transmute(Variabel, Koefisien = estimate, `Galat baku` = std.error, `Nilai t` = statistic,
            `Nilai p` = p.value, `Batas bawah` = conf.low, `Batas atas` = conf.high, Signifikan)
kable(hasil, digits = 4)
Variabel Koefisien Galat baku Nilai t Nilai p Batas bawah Batas atas Signifikan
Konstanta 8.1731 0.0894 91.3801 0 7.9977 8.3486 Ya
Jam belajar per minggu -0.0507 0.0042 -12.1153 0 -0.0589 -0.0425 Ya
Jam internet per hari -0.2501 0.0133 -18.7777 0 -0.2762 -0.2239 Ya
ringkas <- glance(model)
b_bel <- coef(model)["Jam_Belajar_Per_Minggu"]; p_bel <- summary(model)$coefficients["Jam_Belajar_Per_Minggu", 4]
b_int <- coef(model)["Jam_Internet_Per_Hari"];  p_int <- summary(model)$coefficients["Jam_Internet_Per_Hari", 4]
arah  <- function(b) ifelse(b < 0, "menurunkan", "menaikkan")
sig   <- function(p) ifelse(p < .05, "signifikan", "tidak signifikan")
hasil %>% filter(Variabel != "Konstanta") %>%
  ggplot(aes(Koefisien, Variabel)) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "grey50") +
  geom_errorbarh(aes(xmin = `Batas bawah`, xmax = `Batas atas`), height = .15, color = pal$steel, linewidth = 1) +
  geom_point(size = 4.5, color = pal$orange) +
  labs(title = "Koefisien regresi dan selang kepercayaan 95%",
       subtitle = "Selang yang tidak menyentuh garis nol berarti pengaruhnya signifikan",
       x = "Perubahan jam tidur per satu satuan prediktor", y = NULL)

Cara membaca hasil. Setiap tambahan satu jam internet per hari menurunkan jam tidur sekitar 0.25 jam (signifikan, nilai p = 0,0000), dengan jam belajar dianggap tetap. Setiap tambahan satu jam belajar per minggu menurunkan jam tidur sekitar 0.051 jam (signifikan, nilai p = 0,0000). Model menjelaskan sekitar 28.9% keragaman jam tidur (R2 terkoreksi = 0.288).

Menguji asumsi model

Model regresi hanya layak dipercaya bila asumsinya terpenuhi. Dua asumsi diuji di sini: residual berdistribusi normal, dan ragam residual sama besar (homogen).

Uji normalitas residual

  • H0: residual berdistribusi normal
  • H1: residual tidak berdistribusi normal
residu <- resid(model)
uji_ks <- ks.test(residu, "pnorm", mean(residu), sd(residu))
uji_ks
## 
##  Asymptotic one-sample Kolmogorov-Smirnov test
## 
## data:  residu
## D = 0.016722, p-value = 0.8905
## alternative hypothesis: two-sided
p_h <- ggplot(tibble(residu), aes(residu)) +
  geom_histogram(aes(y = after_stat(density)), bins = 30, fill = pal$mist, color = "white") +
  stat_function(fun = dnorm, args = list(mean = mean(residu), sd = sd(residu)), color = pal$red, linewidth = 1.1) +
  labs(title = "Histogram residual", subtitle = "Garis merah: kurva normal pembanding", x = "Residual", y = "Kepadatan")

p_q <- ggplot(tibble(residu), aes(sample = residu)) +
  stat_qq(color = pal$steel, alpha = .55) +
  stat_qq_line(color = pal$red, linewidth = 1) +
  labs(title = "Q-Q plot residual", subtitle = "Titik yang menempel pada garis menandakan normal",
       x = "Kuantil teoretis", y = "Kuantil residual")

p_h + p_q

Normalitas terpenuhi. Uji Kolmogorov-Smirnov menghasilkan nilai p = 0,8905, lebih besar dari 0,05, sehingga H0 tidak ditolak. Residual model dapat dianggap berdistribusi normal.

Uji heteroskedastisitas

  • H0: tidak terdapat heteroskedastisitas (ragam residual homogen)
  • H1: terdapat heteroskedastisitas
uji_bp <- bptest(model)
uji_bp
## 
##  studentized Breusch-Pagan test
## 
## data:  model
## BP = 0.082133, df = 2, p-value = 0.9598
tibble(fitted = fitted(model), residu = residu) %>%
  ggplot(aes(fitted, residu)) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
  geom_point(color = pal$steel, alpha = .45) +
  geom_smooth(method = "loess", se = FALSE, color = pal$orange, linewidth = 1.2) +
  labs(title = "Residual terhadap nilai prediksi",
       subtitle = "Sebaran yang merata tanpa pola corong menandakan ragam homogen",
       x = "Nilai prediksi jam tidur", y = "Residual")

Homogenitas ragam terpenuhi. Uji Breusch-Pagan menghasilkan BP = 0,08213 dan nilai p = 0,9598, lebih besar dari 0,05, sehingga H0 tidak ditolak. Tidak ada gejala heteroskedastisitas.

Kesimpulan

Kualitas data. Dataset mentah berisi 1.215 baris dan mengandung keempat jenis masalah: 46 sel kosong, penulisan gender dan kota yang tidak seragam, 15 ID mahasiswa yang kembar, dan 14 nilai yang berada di luar batas wajar. Setelah lima langkah pembersihan, tersisa 1.200 baris yang lengkap, seragam, dan masuk akal.

Integrasi dan encoding. Dua tabel berhasil digabung lewat ID_Mahasiswa tanpa kehilangan baris, dan jam belajar berhasil diubah menjadi kategori Rendah, Sedang, dan Tinggi berkode 1, 2, dan 3.

Hasil analisis. Pada model regresi, jam internet per hari signifikan dalam memengaruhi jam tidur, sedangkan jam belajar per minggu signifikan. Asumsi normalitas residual terpenuhi (nilai p = 0,8905) dan asumsi homogenitas ragam terpenuhi (nilai p = 0,9598).

Pesan utamanya: data yang bersih membuat kesimpulan statistik bisa dipercaya. Tanpa pembersihan, ID kembar dan angka mustahil seperti umur 150 tahun atau internet 30 jam sehari akan diam-diam merusak hasil.


Disusun oleh Riswan Nurintan bersama Kelompok 1, Universitas Riau. Dosen pengampu: Anisa Nurizki, M.Si dan Rezi Wahyuni, M.Si. Dibuat dengan R Markdown.

Informasi sesi R
## R version 4.6.1 (2026-06-24 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_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: Asia/Bangkok
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] knitr_1.52      broom_1.0.13    patchwork_1.3.2 DT_0.34.0      
##  [5] scales_1.4.0    lmtest_0.9-40   zoo_1.9-1       readxl_1.5.0.1 
##  [9] lubridate_1.9.5 forcats_1.0.1   stringr_1.6.0   dplyr_1.2.1    
## [13] purrr_1.2.2     readr_2.2.0     tidyr_1.3.2     tibble_3.3.1   
## [17] ggplot2_4.0.3   tidyverse_2.0.0
## 
## loaded via a namespace (and not attached):
##  [1] sass_0.4.10        generics_0.1.4     stringi_1.8.9      lattice_0.22-9    
##  [5] hms_1.1.4          digest_0.6.39      magrittr_2.0.5     evaluate_1.0.5    
##  [9] grid_4.6.1         timechange_0.4.0   RColorBrewer_1.1-3 fastmap_1.2.0     
## [13] Matrix_1.7-5       cellranger_1.1.0   jsonlite_2.0.0     backports_1.5.1   
## [17] mgcv_1.9-4         viridisLite_0.4.3  crosstalk_1.2.2    jquerylib_0.1.4   
## [21] cli_3.6.6          rlang_1.3.0        splines_4.6.1      withr_3.0.3       
## [25] cachem_1.1.0       yaml_2.3.12        otel_0.2.0         tools_4.6.1       
## [29] tzdb_0.5.0         vctrs_0.7.3        R6_2.6.1           stats4_4.6.1      
## [33] lifecycle_1.0.5    htmlwidgets_1.6.4  pkgconfig_2.0.3    pillar_1.11.1     
## [37] bslib_0.12.0       gtable_0.3.6       glue_1.8.1         xfun_0.61         
## [41] tidyselect_1.2.1   rstudioapi_0.19.0  farver_2.1.2       nlme_3.1-169      
## [45] htmltools_0.5.9    labeling_0.4.3     rmarkdown_2.32     compiler_4.6.1    
## [49] S7_0.2.2