Seluruh bagian dokumen (teori, metodologi, dan kode) menggunakan notasi yang sama berikut.
| Simbol | Makna |
|---|---|
| \(n\) | jumlah observasi pada data yang sedang dimodelkan (data latih saat pemodelan) |
| \(p=4\) | jumlah prediktor: AT, V, AP, RH |
| \(y_i\) | nilai respons PE pada observasi ke-\(i\) |
| \(\mathbf{x}_i=(x_{i1},x_{i2},x_{i3},x_{i4})^\top\) | vektor prediktor (AT, V, AP, RH) observasi ke-\(i\) |
| \(\mathbf{y}\), \(\mathbf{X}\) | vektor respons dan matriks desain |
| \(\boldsymbol\beta\) | koefisien model yang linear terhadap parameter (OLS, polynomial, basis GAM) |
| \(\boldsymbol\theta\) | parameter model nonlinear terhadap parameter (NLS) |
| \(z_{ij}\) | prediktor terstandarisasi |
| \(\hat y_i\), \(e_i=y_i-\hat y_i\) | nilai prediksi dan residual |
| \(\mathrm{RSS}\) | jumlah kuadrat residual \(\sum_i e_i^2\) |
| \(k\) | jumlah parameter model pada kriteria AICc (untuk GAM: \(\mathrm{edf}\)) |
| \(d\) | derajat polynomial |
| \(q\) | jumlah fungsi basis per fungsi smooth GAM (\(q=20\)) |
| \(\lambda\) | parameter penghalus (smoothing parameter) GAM |
| \(\mu\) | parameter peredam (damping) pada algoritma Levenberg–Marquardt |
| \(K=10\) | jumlah fold pada Cross-Validation |
| \(\mathcal{V}_m\), \(n_m\) | himpunan indeks validasi dan ukurannya pada fold ke-\(m\) |
| \(G\), \(N\) | jumlah kelompok (sheet) dan total pengamatan pada uji ANOVA/Kruskal–Wallis |
| \(M=3\) | jumlah model yang dibandingkan (Polynomial, NLS, GAM) |
Model regresi menyatakan respons sebagai fungsi prediktor ditambah galat acak (James et al., 2021; Hansen, 2022):
\[ y_i=f(\mathbf{x}_i)+\varepsilon_i,\qquad i=1,\dots,n, \]
dengan \(\varepsilon_i\) galat dengan \(E(\varepsilon_i)=0\) dan variansi \(\sigma^2\). Pada regresi linear, \(f(\mathbf{x}_i)=\beta_0+\sum_{j=1}^{p}\beta_j x_{ij}\), atau dalam bentuk matriks \(\mathbf{y}=\mathbf{X}\boldsymbol\beta+\boldsymbol\varepsilon\).
Penaksir Ordinary Least Squares (OLS) meminimumkan jumlah kuadrat residual:
\[ \mathrm{RSS}(\boldsymbol\beta)=\sum_{i=1}^{n}(y_i-\mathbf{x}_i^\top\boldsymbol\beta)^2 =\lVert \mathbf{y}-\mathbf{X}\boldsymbol\beta\rVert_2^2 , \qquad \hat{\boldsymbol\beta}=(\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}. \]
Jika \(\mathbf{X}^\top\mathbf{X}\) singular atau berkondisi buruk, solusi kuadrat terkecil dapat dihitung melalui pseudo-inverse Moore–Penrose, \(\hat{\boldsymbol\beta}=\mathbf{X}^{+}\mathbf{y}\), yang dihitung dengan dekomposisi nilai singular (SVD): jika \(\mathbf{X}=\mathbf{U}\mathbf{D}\mathbf{V}^\top\), maka \(\mathbf{X}^{+}=\mathbf{V}\mathbf{D}^{+}\mathbf{U}^\top\), dengan \(\mathbf{D}^{+}\) membalik hanya nilai singular yang melebihi toleransi (Hansen, 2022).
Jika galat berdistribusi normal, \(\varepsilon_i\sim N(0,\sigma^2)\), penaksir Maximum Likelihood Estimator (MLE) bagi \(\boldsymbol\beta\) sama dengan penaksir OLS, dan MLE bagi variansi adalah \(\hat\sigma^2=\mathrm{RSS}/n\) (Hansen, 2022).
Ukuran kecocokan yang digunakan pada penelitian ini adalah Root Mean Squared Error dan koefisien determinasi (James et al., 2021):
\[ \mathrm{RMSE}=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat y_i)^2},\qquad R^2=1-\frac{\sum_{i=1}^{n}(y_i-\hat y_i)^2}{\sum_{i=1}^{n}(y_i-\bar y)^2}. \]
Perlu dicatat bahwa bila \(R^2\) dihitung pada data uji dengan \(\bar y\) dari data uji, nilainya tidak dijamin berada pada selang \([0,1]\); nilai negatif dapat terjadi jika model memprediksi lebih buruk daripada rata-rata data uji.
Regresi polinomial memperluas model linear dengan menambahkan pangkat prediktor sebagai variabel baru; model tetap linear terhadap parameter sehingga dapat diestimasi dengan OLS (James et al., 2021; Hastie et al., 2009). Pada \(p\) prediktor, polinomial berderajat total \(d\) yang memuat seluruh suku pangkat dan suku interaksi adalah
\[ f(\mathbf{x};\boldsymbol\beta)=\beta_0+\sum_{\boldsymbol\alpha\in\mathcal{A}_d}\beta_{\boldsymbol\alpha}\prod_{j=1}^{p}x_j^{\alpha_j}, \qquad \mathcal{A}_d=\Big\{\boldsymbol\alpha\in\mathbb{N}_0^{p}:\;1\le \textstyle\sum_{j}\alpha_j\le d\Big\}. \]
Banyaknya suku tanpa intercept adalah \(|\mathcal{A}_d|=\binom{p+d}{d}-1\). Untuk \(p=4\) diperoleh 4 suku (\(d=1\)), 14 suku (\(d=2\)), dan 34 suku (\(d=3\)), sehingga jumlah parameter termasuk intercept adalah \(k=5,\,15,\,35\). Derajat \(d\) merupakan hyperparameter yang menentukan fleksibilitas model: derajat yang lebih tinggi menurunkan bias tetapi menaikkan variansi (Hastie et al., 2009).
Pada regresi nonlinear, fungsi rata-rata nonlinear terhadap parameter (Fox & Weisberg, 2019):
\[ y_i=f(\mathbf{x}_i;\boldsymbol\theta)+\varepsilon_i,\qquad \boldsymbol\theta\in\mathbb{R}^{k}. \]
Perbedaannya dengan regresi polinomial: polinomial nonlinear terhadap \(\mathbf{x}\) tetapi linear terhadap \(\boldsymbol\beta\), sedangkan pada NLS turunan \(f\) terhadap sebagian parameter masih bergantung pada parameter itu sendiri (contoh: \(\theta_1 e^{\theta_2 z}\)). Penaksir NLS meminimumkan
\[ S(\boldsymbol\theta)=\sum_{i=1}^{n}\big(y_i-f(\mathbf{x}_i;\boldsymbol\theta)\big)^2 . \]
Dengan \(\mathbf{J}(\boldsymbol\theta)\) matriks Jacobian berukuran \(n\times k\) dengan elemen \(J_{ij}=\partial f(\mathbf{x}_i;\boldsymbol\theta)/\partial\theta_j\) dan \(\mathbf{r}(\boldsymbol\theta)=\mathbf{y}-f(\mathbf{X};\boldsymbol\theta)\), persamaan normal NLS adalah \(\mathbf{J}(\hat{\boldsymbol\theta})^\top\mathbf{r}(\hat{\boldsymbol\theta})=\mathbf{0}\). Persamaan ini umumnya tidak mempunyai solusi tertutup sehingga diselesaikan secara iteratif (Fox & Weisberg, 2019; Hansen, 2022). Bila galat \(\varepsilon_i\sim N(0,\sigma^2)\) saling bebas, penaksir NLS identik dengan MLE bagi \(\boldsymbol\theta\) (Hansen, 2022).
Metode Gauss–Newton memperbarui parameter dengan aproksimasi linear \(f(\boldsymbol\theta+\boldsymbol\delta)\approx f(\boldsymbol\theta)+\mathbf{J}\boldsymbol\delta\):
\[ \boldsymbol\theta^{(t+1)}=\boldsymbol\theta^{(t)}+\big(\mathbf{J}^\top\mathbf{J}\big)^{-1}\mathbf{J}^\top\mathbf{r}. \]
Algoritma Levenberg–Marquardt menstabilkan langkah tersebut dengan menambahkan peredam \(\mu\ge 0\) pada diagonal (Levenberg, 1944):
\[ \big(\mathbf{J}^\top\mathbf{J}+\mu\,\mathbf{D}\big)\boldsymbol\delta=\mathbf{J}^\top\mathbf{r},\qquad \boldsymbol\theta^{(t+1)}=\boldsymbol\theta^{(t)}+\boldsymbol\delta , \]
dengan \(\mathbf{D}\) matriks
diagonal positif (matriks identitas pada formulasi Levenberg). Nilai
\(\mu\) kecil membuat langkah mendekati
Gauss–Newton, sedangkan \(\mu\) besar
membuat langkah mendekati arah penurunan tercuram. Implementasi yang
digunakan pada dokumen ini adalah subrutin MINPACK (Moré et al., 1980)
melalui paket minpack.lm (Elzhov et al., 2023), yaitu
algoritma yang sama dengan yang dipakai
scipy.optimize.curve_fit pada notebook Python.
Algoritma iteratif NLS memerlukan nilai awal parameter, dan hasil
dapat bergantung pada nilai awal tersebut (Fox & Weisberg, 2019).
Karena itu pada penelitian ini nilai awal ditetapkan secara tetap dan
sama untuk setiap fit. Bila algoritma tidak mencapai kriteria
konvergensi, fit dinyatakan gagal dan nilai kriterianya dicatat sebagai
NA.
GAM mengganti suku linear pada regresi dengan fungsi smooth tak-parametrik yang dijumlahkan (Hastie & Tibshirani, 1986; James et al., 2021):
\[ y_i=\beta_0+\sum_{j=1}^{p}f_j(x_{ij})+\varepsilon_i . \]
Setiap fungsi smooth dinyatakan sebagai kombinasi linear fungsi basis, \(f_j(x)=\sum_{m=1}^{q}\beta_{jm}b_{jm}(x)\), sehingga model menjadi linear terhadap koefisien basis. Agar fungsi tidak terlalu bergelombang, estimasi dilakukan dengan penalized least squares:
\[ \hat{\boldsymbol\beta}=\arg\min_{\boldsymbol\beta}\;\lVert\mathbf{y}-\mathbf{X}\boldsymbol\beta\rVert_2^2+\sum_{j=1}^{p}\lambda_j\,\boldsymbol\beta_j^\top\mathbf{S}_j\boldsymbol\beta_j , \]
dengan \(\mathbf{S}_j\) matriks penalti dan \(\lambda_j\ge 0\) parameter penghalus: \(\lambda_j\) besar menghasilkan fungsi yang lebih halus (mendekati linear), \(\lambda_j\) kecil menghasilkan fungsi yang lebih fleksibel (Hastie et al., 2009; Wood, 2024).
Basis P-spline : Pada penelitian ini basis yang
dipakai adalah P-spline (Eilers & Marx, 1996), yaitu basis
B-spline kubik dengan penalti selisih orde dua pada koefisien basis yang
berdekatan, \(\lambda\sum_m(\Delta^2\beta_m)^2\). Pilihan
ini sesuai dengan basis LinearGAM pada pustaka pyGAM yang
dipakai notebook (Servén & Brummitt, 2018) dan dinyatakan di
mgcv dengan bs = "ps" (Wood, 2024).
Derajat kebebasan efektif dan GCV : Nilai prediksi GAM berbentuk \(\hat{\mathbf{y}}=\mathbf{A}_\lambda\mathbf{y}\) dengan matriks pengaruh \(\mathbf{A}_\lambda=\mathbf{X}(\mathbf{X}^\top\mathbf{X}+\sum_j\lambda_j\mathbf{S}_j)^{-1}\mathbf{X}^\top\). Effective degrees of freedom didefinisikan sebagai \(\mathrm{edf}=\operatorname{tr}(\mathbf{A}_\lambda)\) (Hastie et al., 2009). Generalized Cross-Validation untuk pemilih parameter penghalus adalah
\[ \mathrm{GCV}(\lambda)=\frac{\mathrm{RSS}/n}{\big(1-\mathrm{edf}/n\big)^{2}} , \]
yang merupakan aproksimasi leave-one-out CV untuk penghalus
linear (Hastie et al., 2009). Pada mgcv,
method = "GCV.Cp" memilih \(\lambda\) dengan meminimumkan skor GCV bila
skala galat tidak diketahui (Wood, 2024).
Pada galat normal, log-likelihood maksimum model regresi adalah \(\log L(\hat{\boldsymbol\theta},\hat\sigma^2)=-\tfrac{n}{2}\big[\log(2\pi)+\log(\mathrm{RSS}/n)+1\big]\). Karena \(\mathrm{AIC}=-2\log L+2k\), diperoleh bentuk yang berbeda dari AIC lengkap hanya oleh konstanta yang sama untuk semua model pada data yang sama (Hansen, 2022; Portet, 2020):
\[ \mathrm{AIC}=n\log\!\left(\frac{\mathrm{RSS}}{n}\right)+2k . \]
Untuk sampel berhingga digunakan koreksi (AICc):
\[ \mathrm{AICc}=\mathrm{AIC}+\frac{2k(k+1)}{n-k-1},\qquad n-k-1>0 . \]
Model dengan AICc terkecil dipilih (Portet, 2020). Catatan konvensi: pada implementasi penelitian ini \(k\) adalah jumlah parameter fungsi rata-rata (intercept + koefisien untuk Polynomial; \(\dim\boldsymbol\theta\) untuk NLS; \(\mathrm{edf}\) untuk GAM), tanpa menghitung \(\sigma^2\), mengikuti kode pada notebook Python. Bila \(n-k-1\le 0\), koreksi tidak ditambahkan.
Pada \(K\)-fold CV, data latih dibagi menjadi \(K\) lipatan yang saling lepas; secara bergantian satu lipatan menjadi data validasi dan sisanya menjadi data pelatihan (James et al., 2021; Arlot & Celisse, 2010). Untuk lipatan ke-\(m\) dengan himpunan indeks validasi \(\mathcal{V}_m\) berukuran \(n_m\):
\[ \mathrm{RMSE}_m=\sqrt{\frac{1}{n_m}\sum_{i\in\mathcal{V}_m}\big(y_i-\hat y_i^{(-m)}\big)^2}, \]
dengan \(\hat y_i^{(-m)}\) prediksi dari model yang dilatih tanpa lipatan ke-\(m\). Ringkasan lintas fold yang digunakan pada penelitian ini ada dua, sesuai notebook:
\[ \mathrm{CV\text{-}RMSE}^{\text{rms}}=\sqrt{\frac{1}{K}\sum_{m=1}^{K}\mathrm{RMSE}_m^{2}} \quad(\text{Polynomial dan NLS}),\qquad \mathrm{CV\text{-}RMSE}^{\text{mean}}=\frac{1}{K}\sum_{m=1}^{K}\mathrm{RMSE}_m \quad(\text{GAM}). \]
Kedua ringkasan sama bila seluruh \(\mathrm{RMSE}_m\) identik, dan keduanya berskala sama dengan RMSE. Model dengan CV-RMSE lebih kecil dinilai memiliki galat prediksi lebih rendah.
Statistik deskriptif Kuartil dihitung dengan interpolasi linear antar-statistik terurut. Skewness dan kurtosis yang dilaporkan adalah penaksir terkoreksi bias (pandas development team, 2024). Dengan \(m_r=\frac1n\sum_i(x_i-\bar x)^r\), \(g_1=m_3/m_2^{3/2}\), dan \(g_2=m_4/m_2^{2}-3\) (NIST/SEMATECH, 2012):
\[ G_1=\frac{\sqrt{n(n-1)}}{n-2}\,g_1,\qquad G_2=\frac{n-1}{(n-2)(n-3)}\Big[(n+1)\,g_2+6\Big]. \]
Nilai \(G_1\) mendekati 0 menunjukkan distribusi simetris, dan \(G_2\) (kurtosis berlebih) mendekati 0 menunjukkan ekor setara distribusi normal.
Outlier aturan IQR Dengan \(\mathrm{IQR}=Q_3-Q_1\), pengamatan dinyatakan outlier bila berada di luar \([\,Q_1-1.5\,\mathrm{IQR},\;Q_3+1.5\,\mathrm{IQR}\,]\) (NIST/SEMATECH, 2012).
Multikolinearitas (VIF) Untuk prediktor ke-\(j\) (James et al., 2021):
\[ \mathrm{VIF}_j=\frac{1}{1-R_j^2}, \]
dengan \(R_j^2\) koefisien determinasi regresi prediktor ke-\(j\) terhadap prediktor lain. Nilai VIF yang melebihi 5 atau 10 umumnya dianggap mengindikasikan kolinearitas yang bermasalah (James et al., 2021).
Normalitas residual (Shapiro–Wilk) Statistik uji adalah
\[ W=\frac{\big(\sum_{i=1}^{n}a_i\,e_{(i)}\big)^2}{\sum_{i=1}^{n}(e_i-\bar e)^2}, \]
dengan \(e_{(i)}\) residual terurut
dan \(a_i\) koefisien yang bergantung
pada nilai harapan dan kovariansi statistik terurut normal baku
(Navarro, 2015). \(H_0\): residual
berdistribusi normal. Fungsi shapiro.test di R, seperti
padanannya di Python, mensyaratkan \(n\le
5000\), sehingga pada data lebih besar dilakukan pengambilan
sampel acak 5000 residual.
Heteroskedastisitas (Breusch–Pagan) Residual kuadrat \(e_i^2\) diregresikan terhadap prediktor; dengan \(R^2_{\text{aux}}\) koefisien determinasi regresi bantu tersebut, statistik
\[ \mathrm{LM}=n\,R^2_{\text{aux}} \]
berdistribusi asimtotik \(\chi^2_{p}\) di bawah \(H_0\) (variansi galat konstan). Bentuk
studentized inilah yang menjadi bawaan
lmtest::bptest (Zeileis & Hothorn, 2002) dan
statsmodels.stats.diagnostic.het_breuschpagan.
Korelasi peringkat Spearman Untuk dua kriteria yang menilai \(M\) model, dengan \(d_i\) selisih peringkat model ke-\(i\) pada kedua kriteria dan tanpa nilai kembar (Navarro, 2015):
\[ \rho=1-\frac{6\sum_{i=1}^{M}d_i^{2}}{M(M^{2}-1)} . \]
Nilai \(\rho=1\) berarti urutan kedua kriteria identik. Dengan \(M=3\) hanya terdapat \(3!=6\) kemungkinan urutan sehingga p-value eksak terkecil yang mungkin adalah \(2/6\approx0{,}3333\) (dua sisi); karena itu hubungan sempurna sekalipun tidak dapat signifikan pada taraf 5%.
ANOVA satu arah Membandingkan rata-rata \(G\) kelompok (Navarro, 2015):
\[ F=\frac{\mathrm{SSB}/(G-1)}{\mathrm{SSW}/(N-G)},\quad \mathrm{SSB}=\sum_{g=1}^{G}n_g(\bar y_g-\bar y)^2,\quad \mathrm{SSW}=\sum_{g=1}^{G}\sum_{i=1}^{n_g}(y_{gi}-\bar y_g)^2 , \]
dengan \(F\sim F_{G-1,\,N-G}\) di bawah \(H_0\): seluruh rata-rata kelompok sama. Asumsinya meliputi normalitas dalam kelompok dan kesamaan variansi.
Kruskal–Wallis Alternatif nonparametrik berbasis peringkat gabungan; dengan \(R_g\) jumlah peringkat kelompok ke-\(g\) (Navarro, 2015):
\[ H=\frac{12}{N(N+1)}\sum_{g=1}^{G}\frac{R_g^{2}}{n_g}-3(N+1), \]
dan bila terdapat nilai kembar, \(H\) dibagi faktor koreksi \(1-\sum_t(t^3-t)/(N^3-N)\) dengan \(t\) ukuran tiap kelompok nilai kembar
(koreksi ini diterapkan oleh kruskal.test di R; R Core
Team, 2024). Di bawah \(H_0\), \(H\) mengikuti distribusi \(\chi^2_{G-1}\) secara aproksimasi.
Penelitian ini menggunakan data sekunder Combined Cycle Power Plant (CCPP) yang tersedia publik di UCI Machine Learning Repository (Tüfekci & Kaya, 2014). Dataset didonasikan pada 25 Maret 2014 dan pertama kali digunakan oleh Tüfekci (2014) untuk memprediksi daya keluaran listrik pembangkit siklus gabungan. Dataset asli memuat 9.568 titik data yang dikumpulkan selama 6 tahun (2006–2011) ketika pembangkit beroperasi pada beban penuh (full load).
Tabel 3.1. Variabel penelitian
| Variabel | Keterangan | Skala Data |
|---|---|---|
| AT | Suhu lingkungan | Interval |
| V | Vakum buang | Interval |
| AP | Tekanan udara ambien | Interval |
| RH | Kelembapan relatif | Interval |
| PE | Daya keluaran listrik | Interval |
Variabel PE berperan sebagai respons (\(y\)), sedangkan AT, V, AP, dan RH sebagai prediktor (\(x_1,\dots,x_4\)). Berkas data terdiri atas beberapa sheet dengan struktur variabel yang sama. ## 2. Metode Analisis
Penelitian ini menggunakan pendekatan kuantitatif untuk memodelkan PE berdasarkan AT, V, AP, dan RH. Pada setiap sheet dijalankan tiga metode: Polynomial Regression, Nonlinear Least Squares (NLS), dan Generalized Additive Model (GAM). Setiap metode memiliki satu hyperparameter yang dipilih melalui dua rute, yaitu rute AICc dan rute 10-fold Cross-Validation. Kinerja akhir dievaluasi pada data uji.
Eksplorasi meliputi statistik deskriptif (count, mean, std, min,
kuartil, median, modus, maks, IQR, skewness, kurtosis), pemeriksaan
missing value dan duplikat, outlier aturan IQR, korelasi
antarvariabel, VIF, serta pemeriksaan residual model linear awal
(baseline) \(y=\beta_0+\sum_j\beta_jx_j+\varepsilon\)
dengan uji Shapiro–Wilk dan Breusch–Pagan. Nilai VIF dihitung seperti
pada statsmodels: untuk setiap kolom matriks desain
(termasuk kolom konstanta) dilakukan regresi kolom tersebut terhadap
kolom lainnya. Hanya VIF keempat prediktor yang dilaporkan dan
diinterpretasikan; nilai untuk kolom konstanta (intercept) tidak
bermakna dan bergantung pada versi statsmodels.
Pada setiap sheet, data dibagi 80% data latih dan 20% data uji dengan
indeks yang sama untuk semua sheet. Prosedurnya adalah permutasi acak
penuh indeks \(1,\dots,n\) menggunakan
set.seed(42); sebanyak \(n_{\text{test}}\) indeks pertama menjadi
data uji dan sisanya data latih, dengan
\[ n_{\text{test}}=\lceil 0{,}20\,n\rceil,\qquad n_{\text{train}}=n-n_{\text{test}} . \]
Pada data latih, 10 fold dibentuk dengan permutasi acak penuh
(set.seed(42)) lalu penempelan nomor fold secara
round-robin (\(1,2,\dots,10,1,2,\dots\)), bukan blok
berurutan. Setiap fold bergantian menjadi data validasi, dan RMSE tiap
fold dihitung.
Catatan kesetaraan: generator bilangan acak R dan NumPy berbeda sehingga indeks acak yang persis sama secara numerik tidak dapat dicapai lintas bahasa yang disamakan adalah prosedur (bentuk permutasi, cara membagi, cara membentuk fold, rumus AICc, grid hyperparameter, dan cara agregasi RMSE).
Model yang dipakai adalah polynomial multivariat berderajat total
\(d\in\{1,2,3\}\) dengan suku
interaksi, sehingga \(k=5,15,35\).
Estimasi memakai OLS dengan solusi kuadrat terkecil berbasis SVD (setara
LinearRegression pada scikit-learn yang memusatkan data
lalu menyelesaikan kuadrat terkecil), karena kolom polynomial pada skala
asli berkondisi buruk. Prediktor tidak distandarisasi pada Polynomial.
Untuk setiap \(d\) dihitung \(\mathrm{AICc}\) pada seluruh data latih dan
\(\mathrm{CV\text{-}RMSE}^{\text{rms}}\) dari
10 fold. Derajat terpilih rute AICc adalah \(d\) dengan AICc minimum; derajat terpilih
rute CV adalah \(d\) dengan CV-RMSE
minimum.
Prediktor distandarisasi, \(z_{ij}=(x_{ij}-\bar x_j)/s_j\) dengan \(\bar x_j\) dan \(s_j\) (simpangan baku sampel, pembagi \(n-1\)) dihitung hanya dari data yang dipakai pada fit tersebut sehingga tidak ada kebocoran data (bila \(s_j=0\) atau tak terdefinisi digunakan 1). Standardisasi ini dimaksudkan agar skala prediktor setara bagi optimisasi numerik. Tiga bentuk fungsi nonlinear terhadap parameter dibandingkan, dengan indeks \(z_1,\dots,z_4\) berturut-turut untuk AT, V, AP, RH:
\[ \begin{aligned} \text{exp(AT)}:\quad & f=\theta_0+\theta_1e^{\theta_2 z_{1}}+\theta_3z_{2}+\theta_4z_{3}+\theta_5z_{4} && (k=6)\\ \text{exp(AT)+exp(V)}:\quad & f=\theta_0+\theta_1e^{\theta_2 z_{1}}+\theta_3e^{\theta_4 z_{2}}+\theta_5z_{3}+\theta_6z_{4} && (k=7)\\ \text{exp(linear)}:\quad & f=\exp\!\big(\theta_0+\theta_1z_{1}+\theta_2z_{2}+\theta_3z_{3}+\theta_4z_{4}\big) && (k=5) \end{aligned} \]
Bentuk fungsi eksponensial dipilih karena eksplorasi data menunjukkan hubungan AT dan V terhadap PE menurun dengan kemiringan yang melandai (pola peluruhan eksponensial), seperti dirancang pada notebook. Nilai awal ditetapkan tetap dan sama pada semua fit: koefisien linear \(b_j\) dari OLS pada \(\mathbf{Z}\), parameter pengali eksponen \(=1\), dan eksponen \(=-0{,}5\):
\[ \begin{aligned} \text{exp(AT)}:\quad & \boldsymbol\theta^{(0)}=(\bar y,\;1,\;-0{,}5,\;b_{2},\;b_{3},\;b_{4})\\ \text{exp(AT)+exp(V)}:\quad & \boldsymbol\theta^{(0)}=(\bar y,\;1,\;-0{,}5,\;1,\;-0{,}5,\;b_{3},\;b_{4})\\ \text{exp(linear)}:\quad & \boldsymbol\theta^{(0)}=(\log\bar y,\;0,\;0,\;0,\;0) \end{aligned} \]
Estimasi dengan Levenberg–Marquardt (batas evaluasi fungsi 20.000,
toleransi relatif \(\sqrt{\epsilon_{\text{mesin}}}\approx1{,}49\times10^{-8}\)).
Fit yang tidak mencapai kriteria konvergensi dicatat sebagai gagal
(NA) dan tidak dipilih. Bentuk fungsi berperan sebagai
hyperparameter yang dipilih lewat rute AICc dan rute CV,
seperti derajat pada Polynomial; \(\mathrm{CV\text{-}RMSE}^{\text{rms}}\)
dipakai seperti pada Polynomial.
Model yang dipakai:
\[ y\sim s(\text{AT},q{=}20)+s(\text{V},q{=}20)+s(\text{AP},q{=}20)+s(\text{RH},q{=}20), \]
dengan basis P-spline. Dua rute pemilihan \(\lambda\):
method = "GCV.Cp"). Nilai \(\mathrm{edf}\) diambil dari jumlah elemen
diagonal matriks pengaruh, dan \(\mathrm{AICc}\) dihitung dengan \(k=\mathrm{edf}\). Konfigurasi dilaporkan
sebagai rata-rata \(\lambda\)
terpilih.Catatan kesetaraan: skala dan parameterisasi \(\lambda\) pada mgcv dan pyGAM
tidak identik secara numerik; yang disamakan adalah desain model (4
smooth, \(q=20\), basis P-spline),
desain grid, dan prosedur pemilihan.
Untuk tiap sheet, “model terbaik rute AICc” adalah model dengan AICc
minimum di antara Polynomial (derajat terpilih AICc), NLS (bentuk
terpilih AICc), dan GAM (rute GCV); “model terbaik rute CV” adalah model
dengan CV-RMSE minimum di antara ketiganya (konfigurasi terpilih rute
CV). Kesesuaian kedua rute dicatat pada kolom Sama.
Model akhir per sheet ditentukan dari tiga kandidat (satu per metode)
dengan AICc terkecil tiap metode, CV-RMSE pada konfigurasi terpilih rute
CV, serta metrik data uji dari rute CV. Kandidat bertanda AICc
terbaik atau CV terbaik bila nilainya sama dengan minimum
antar-kandidat (isclose, toleransi relatif \(10^{-5}\) dan mutlak \(10^{-8}\)). Jika terdapat kandidat yang
terbaik pada kedua kriteria, dipilih yang ber-AICc terkecil di
antaranya; jika tidak, dipilih kandidat ber-CV-RMSE terkecil.
Model terpilih dilatih ulang pada seluruh data latih dan dievaluasi pada data uji dengan RMSE dan \(R^2\). Kesesuaian peringkat AICc dan CV-RMSE antar tiga model diuji dengan korelasi Spearman per sheet (\(M=3\)). Perbedaan kinerja antar-sheet diuji dengan ANOVA satu arah dan Kruskal–Wallis pada 10 nilai RMSE fold dari konfigurasi terpilih rute CV, untuk masing-masing metode (\(G\) = jumlah sheet).
Seluruh angka pada narasi di bawah dihasilkan langsung dari keluaran kode (inline R), sehingga narasi otomatis konsisten dengan data yang digunakan saat dokumen di-knit.
Pemeriksaan dilakukan secara berurutan: pembacaan dan validasi
seluruh sheet, pemeriksaan kesamaan antar-sheet, dan statistik
deskriptif. Ubah fname sesuai lokasi berkas data pada
komputer Anda.
fname <- "D:/FASTTRACK S2/REGRESI/PROJECT/Folds5x2_pp.xlsx"
read_ccpp_file <- function(path) {
sheets <- readxl::excel_sheets(path)
out <- lapply(sheets, function(sh) {
d <- readxl::read_excel(path, sheet = sh)
colnames(d) <- trimws(as.character(colnames(d)))
as.data.frame(d)
})
names(out) <- sheets
out
}
target <- "PE"
features <- c("AT", "V", "AP", "RH")
data_sheets <- read_ccpp_file(fname)
sheet_names <- names(data_sheets)
validate_ccpp_data <- function(data_sheets, target, features) {
if (length(data_sheets) == 0) stop("Tidak ada sheet yang terbaca.")
need <- c(features, target)
problems <- c()
for (sh in names(data_sheets)) {
missing <- setdiff(need, colnames(data_sheets[[sh]]))
if (length(missing) > 0)
problems <- c(problems, paste0(sh, ": ", paste(missing, collapse = ", ")))
}
if (length(problems) > 0)
stop(paste("Variabel tidak ditemukan ->", paste(problems, collapse = " | ")))
}
validate_ccpp_data(data_sheets, target, features)
cat(sprintf("Jumlah sheet terbaca: %d -> %s\n", length(sheet_names), paste(sheet_names, collapse = ", ")))
## Jumlah sheet terbaca: 5 -> Sheet1, Sheet2, Sheet3, Sheet4, Sheet5
for (sh in sheet_names) cat(sprintf(" - %s: %d baris x %d kolom\n", sh, nrow(data_sheets[[sh]]), ncol(data_sheets[[sh]])))
## - Sheet1: 9568 baris x 5 kolom
## - Sheet2: 9568 baris x 5 kolom
## - Sheet3: 9568 baris x 5 kolom
## - Sheet4: 9568 baris x 5 kolom
## - Sheet5: 9568 baris x 5 kolom
# df dipakai untuk EDA -> sheet pertama sebagai representasi
df <- data_sheets[[sheet_names[1]]]
cat(sprintf("\nEDA memakai sheet pertama ('%s') sebagai representasi.\n", sheet_names[1]))
##
## EDA memakai sheet pertama ('Sheet1') sebagai representasi.
head(df)
## AT V AP RH PE
## 1 14.96 41.76 1024.07 73.17 463.26
## 2 25.18 62.96 1020.04 59.08 444.37
## 3 5.11 39.40 1012.16 92.14 488.56
## 4 20.86 57.32 1010.24 76.64 446.48
## 5 10.82 37.50 1009.23 96.62 473.90
## 6 26.27 59.44 1012.23 58.77 443.67
verify_identical_sheets <- function(data_sheets, features, target) {
cols <- c(features, target)
sheet_names <- names(data_sheets)
sort_df <- function(d) { d <- d[, cols]; d <- d[do.call(order, unname(as.list(d))), ]; rownames(d) <- NULL; d }
ref <- sort_df(data_sheets[[sheet_names[1]]])
rows <- lapply(sheet_names, function(sh) {
data.frame(Sheet = sh,
n_baris = nrow(data_sheets[[sh]]),
identik_dgn_sheet1_setelah_sort = isTRUE(all.equal(ref, sort_df(data_sheets[[sh]]),
check.attributes = FALSE)))
})
do.call(rbind, rows)
}
verify_df <- verify_identical_sheets(data_sheets, features, target)
tbl(verify_df)
| Sheet | n_baris | identik_dgn_sheet1_setelah_sort |
|---|---|---|
| Sheet1 | 9568 | TRUE |
| Sheet2 | 9568 | TRUE |
| Sheet3 | 9568 | TRUE |
| Sheet4 | 9568 | TRUE |
| Sheet5 | 9568 | TRUE |
str(df)
## 'data.frame': 9568 obs. of 5 variables:
## $ AT: num 14.96 25.18 5.11 20.86 10.82 ...
## $ V : num 41.8 63 39.4 57.3 37.5 ...
## $ AP: num 1024 1020 1012 1010 1009 ...
## $ RH: num 73.2 59.1 92.1 76.6 96.6 ...
## $ PE: num 463 444 489 446 474 ...
# Ukuran pusat & sebaran lengkap (definisi sama dengan pandas pada notebook)
calc_mode <- function(x) { # modus; bila kembar, nilai terkecil (seperti pandas .mode().iloc[0])
ux <- sort(unique(x))
ux[which.max(tabulate(match(x, ux)))]
}
calc_skewness <- function(x) { # G1 (terkoreksi bias), setara pandas .skew()
n <- length(x); m <- mean(x); m2 <- mean((x - m)^2); m3 <- mean((x - m)^3)
sqrt(n * (n - 1)) / (n - 2) * m3 / m2^1.5
}
calc_kurtosis <- function(x) { # G2 (kurtosis berlebih terkoreksi bias), setara pandas .kurt()
n <- length(x); m <- mean(x); m2 <- mean((x - m)^2); m4 <- mean((x - m)^4)
g2 <- m4 / m2^2 - 3
(n - 1) / ((n - 2) * (n - 3)) * ((n + 1) * g2 + 6)
}
desc_stats <- data.frame(
Count = sapply(df, function(x) sum(!is.na(x))),
Mean = colMeans(df),
Std = apply(df, 2, sd),
Min = apply(df, 2, min),
Q25 = apply(df, 2, quantile, probs = 0.25),
Median = apply(df, 2, median),
Q75 = apply(df, 2, quantile, probs = 0.75),
Max = apply(df, 2, max),
Mode = apply(df, 2, calc_mode),
IQR = apply(df, 2, IQR),
Skewness = apply(df, 2, calc_skewness),
Kurtosis = apply(df, 2, calc_kurtosis)
)
knitr::kable(desc_stats[, c("Count","Mean","Std","Min","Q25","Median","Q75","Max")], digits = 3, booktabs = TRUE)
| Count | Mean | Std | Min | Q25 | Median | Q75 | Max | |
|---|---|---|---|---|---|---|---|---|
| AT | 9568 | 19.651 | 7.452 | 1.81 | 13.510 | 20.345 | 25.72 | 37.11 |
| V | 9568 | 54.306 | 12.708 | 25.36 | 41.740 | 52.080 | 66.54 | 81.56 |
| AP | 9568 | 1013.259 | 5.939 | 992.89 | 1009.100 | 1012.940 | 1017.26 | 1033.30 |
| RH | 9568 | 73.309 | 14.600 | 25.56 | 63.328 | 74.975 | 84.83 | 100.16 |
| PE | 9568 | 454.365 | 17.067 | 420.26 | 439.750 | 451.550 | 468.43 | 495.76 |
knitr::kable(desc_stats[, c("Mode","IQR","Skewness","Kurtosis")], digits = 3, booktabs = TRUE)
| Mode | IQR | Skewness | Kurtosis | |
|---|---|---|---|---|
| AT | 25.21 | 12.210 | -0.136 | -1.038 |
| V | 41.17 | 24.800 | 0.199 | -1.444 |
| AP | 1013.88 | 8.160 | 0.265 | 0.094 |
| RH | 100.09 | 21.502 | -0.432 | -0.445 |
| PE | 468.80 | 28.680 | 0.307 | -1.049 |
Interpretasi : Seluruh sheet memiliki struktur variabel yang sama; hasil pemeriksaan kesamaan data setelah pengurutan: 5 dari 5 sheet identik dengan sheet pertama, masing-masing dengan 9568 baris. Pada sheet pertama, PE memiliki rata-rata 454,365 dengan simpangan baku 17,067. Nilai skewness berkisar antara -0,432 hingga 0,307 dan kurtosis berlebih antara -1,444 hingga 0,094.
colSums(is.na(df))
## AT V AP RH PE
## 0 0 0 0 0
n_dup <- sum(duplicated(df))
cat("\nJumlah baris duplikat:", n_dup, "\n")
##
## Jumlah baris duplikat: 41
p_list <- lapply(colnames(df), function(col) {
ggplot(df, aes(x = .data[[col]])) +
geom_histogram(aes(y = after_stat(density)), fill = "steelblue", bins = 30, alpha = 0.7) +
geom_density(color = "darkblue", linewidth = 0.8) +
labs(title = paste("Distribusi", col))
})
grid.arrange(grobs = p_list, ncol = 3)
p_box <- lapply(colnames(df), function(col) {
ggplot(df, aes(y = .data[[col]])) + geom_boxplot(fill = "lightcoral") + labs(title = col)
})
grid.arrange(grobs = p_box, ncol = 5)
iqr_outlier_count <- function(s) {
q1 <- quantile(s, 0.25); q3 <- quantile(s, 0.75); iqr <- q3 - q1
sum(s < q1 - 1.5 * iqr | s > q3 + 1.5 * iqr)
}
out_counts <- sapply(df, iqr_outlier_count)
cat("Jumlah outlier (aturan IQR 1.5x) per kolom:\n"); print(out_counts)
## Jumlah outlier (aturan IQR 1.5x) per kolom:
## AT V AP RH PE
## 0 0 88 12 0
Interpretasi : Hasil pemeriksaan menunjukkan 0 missing value dan 41 baris duplikat. Berdasarkan aturan IQR, outlier teridentifikasi pada AP (88 observasi), RH (12 observasi), sedangkan AT, V, PE tidak menunjukkan outlier. Histogram dan boxplot menunjukkan bentuk sebaran serta nilai ekstrem tiap variabel secara visual.
cor_matrix <- cor(df)
corrplot(cor_matrix, method = "color", addCoef.col = "black",
tl.col = "black", number.digits = 2, col.lim = c(-1, 1),
title = "Matriks Korelasi", mar = c(0, 0, 2, 0))
p_reg <- lapply(features, function(col) {
ggplot(df, aes(x = .data[[col]], y = .data[[target]])) +
geom_point(alpha = 0.15, size = 1) +
geom_smooth(method = "lm", color = "red", se = TRUE) +
labs(title = paste(col, "vs", target))
})
grid.arrange(grobs = p_reg, ncol = 4)
# VIF seperti statsmodels: tiap kolom (termasuk konstanta) diregresikan pada kolom lain.
# R^2 dipusatkan bila regresor memuat konstanta, tak dipusatkan bila tidak (aturan statsmodels).
vif_statsmodels <- function(M) {
sapply(seq_len(ncol(M)), function(i) {
y <- M[, i]; Z <- M[, -i, drop = FALSE]
b <- qr.solve(Z, y)
res <- y - Z %*% b
has_const <- any(apply(Z, 2, function(c) all(c == c[1]) && c[1] != 0))
tss <- if (has_const) sum((y - mean(y))^2) else sum(y^2)
1 / (1 - (1 - sum(res^2) / tss))
})
}
X_check <- cbind(const = 1, as.matrix(df[, features]))
vif_all <- data.frame(variabel = colnames(X_check), VIF = vif_statsmodels(X_check))
vif_df <- vif_all[vif_all$variabel %in% features, ] # kolom konstanta tidak diinterpretasikan
tbl(vif_df)
| variabel | VIF |
|---|---|
| AT | 5.9776 |
| V | 3.9430 |
| AP | 1.4526 |
| RH | 1.7053 |
model0 <- lm(as.formula(paste(target, "~", paste(features, collapse = " + "))), data = df)
resid0 <- residuals(model0)
# Normalitas residual (sampel maks. 5000)
set.seed(RANDOM_STATE)
sample_resid <- sample(resid0, min(5000, length(resid0)))
shapiro_res <- shapiro.test(sample_resid)
cat(sprintf("Shapiro-Wilk: statistic=%.4f, p-value=%.4g\n", shapiro_res$statistic, shapiro_res$p.value))
## Shapiro-Wilk: statistic=0.9795, p-value=2.728e-26
p1 <- ggplot(data.frame(resid = resid0), aes(x = resid)) +
geom_histogram(aes(y = after_stat(density)), bins = 30, fill = "steelblue", alpha = 0.7) +
geom_density(color = "darkblue") + labs(title = "Distribusi Residual")
p2 <- ggplot(data.frame(resid = resid0), aes(sample = resid)) +
stat_qq() + stat_qq_line(color = "red") + labs(title = "QQ-Plot Residual")
grid.arrange(p1, p2, ncol = 2)
# Homoskedastisitas (Breusch-Pagan, studentized)
bp_res <- lmtest::bptest(model0)
cat(sprintf("Breusch-Pagan: statistic=%.4f, p-value=%.4g\n", bp_res$statistic, bp_res$p.value))
## Breusch-Pagan: statistic=33.9302, p-value=7.702e-07
cat("(p-value kecil -> indikasi heteroskedastisitas, perlu dicatat sebagai catatan interpretasi, bukan alasan transformasi otomatis)\n")
## (p-value kecil -> indikasi heteroskedastisitas, perlu dicatat sebagai catatan interpretasi, bukan alasan transformasi otomatis)
Interpretasi : Korelasi terhadap PE: AT (r = -0,95), V (r = -0,87), AP (r = 0,52), RH (r = 0,39); korelasi antara AT dan V adalah 0,84. Nilai VIF: AT = 5,978, V = 3,943, AP = 1,453, RH = 1,705; AT memiliki VIF tertinggi, yaitu melebihi ambang 5 (James et al., 2021). Pada model linear awal, uji Shapiro–Wilk menghasilkan \(W=\) 0,9795 dengan p-value 2,73e-26 (residual menyimpang dari normalitas pada taraf 5%), dan uji Breusch–Pagan menghasilkan \(\mathrm{LM}=\) 33,9302 dengan p-value 7,7e-07 (terdapat indikasi heteroskedastisitas). Temuan ini dicatat sebagai bahan interpretasi dan menjadi pertimbangan untuk memakai metode yang mampu menangkap hubungan nonlinier.
Fungsi-fungsi berikut menyalin fungsi bantu pada notebook: kriteria informasi, metrik, pembagian data, pembentukan fold, standardisasi, dan pemasangan OLS berbasis SVD.
compute_aic <- function(n, rss, k) n * log(rss / n) + 2 * k
compute_aicc <- function(n, rss, k) {
aic <- compute_aic(n, rss, k)
if (n - k - 1 > 0) aic <- aic + (2 * k * (k + 1)) / (n - k - 1)
aic
}
rmse <- function(y_true, y_pred) sqrt(mean((y_true - y_pred)^2))
r2_score <- function(y_true, y_pred) 1 - sum((y_true - y_pred)^2) / sum((y_true - mean(y_true))^2)
# Permutasi penuh; n_test indeks pertama = uji, sisanya = latih
make_train_test_split <- function(n, test_size = 0.20, seed = 42) {
set.seed(seed)
shuffled <- sample(n)
n_test <- ceiling(n * test_size)
list(train = shuffled[(n_test + 1):n], test = shuffled[1:n_test])
}
# Permutasi penuh; nomor fold ditempel ROUND-ROBIN (rep(1:K, length.out = n))
make_folds <- function(n, K = 10, seed = 42) {
set.seed(seed)
shuffled <- sample(n)
fold_id <- rep(1:K, length.out = n)
lapply(1:K, function(m) sort(shuffled[fold_id == m]))
}
# Standardisasi: sd sampel (pembagi n-1); scale = 1 bila sd = 0 / NA
fit_scaler <- function(X) {
X <- as.matrix(X)
center <- colMeans(X)
scale <- apply(X, 2, sd)
scale[scale == 0 | is.na(scale)] <- 1.0
list(center = center, scale = scale)
}
apply_scaler <- function(X, center, scale) {
sweep(sweep(as.matrix(X), 2, center, "-"), 2, scale, "/")
}
# OLS dengan intercept: pusatkan data lalu kuadrat terkecil via SVD (pseudo-inverse),
# setara sklearn.LinearRegression. Aman terhadap kolom polynomial yang berkondisi buruk.
ols_fit <- function(Xm, y) {
Xm <- as.matrix(Xm)
xm <- colMeans(Xm); ym <- mean(y)
Xc <- sweep(Xm, 2, xm, "-")
sv <- svd(Xc)
tol <- .Machine$double.eps * max(dim(Xc)) * sv$d[1]
pos <- sv$d > tol
b <- sv$v[, pos, drop = FALSE] %*% ((t(sv$u[, pos, drop = FALSE]) %*% (y - ym)) / sv$d[pos])
list(coef = as.numeric(b), intercept = ym - sum(xm * b))
}
ols_predict <- function(fit, Xm) as.numeric(as.matrix(Xm) %*% fit$coef + fit$intercept)
# Fitur polynomial derajat total d dengan suku interaksi, tanpa kolom konstanta
poly_design <- function(X, d) {
as.matrix(do.call(polym, c(as.list(as.data.frame(X)), list(degree = d, raw = TRUE))))
}
Tiap metode dijalankan pada setiap sheet dengan pembagian data dan skema 10-fold CV yang sama, sehingga hasil antar-metode dapat dibandingkan.
run_polynomial <- function(Xtr, ytr, Xte, yte, folds) {
n <- length(ytr)
rows <- list(); fold_scores <- list()
Xp_all <- lapply(1:3, function(d) list(tr = poly_design(Xtr, d), te = poly_design(Xte, d)))
for (degree in 1:3) {
Xp <- Xp_all[[degree]]$tr
fit <- ols_fit(Xp, ytr)
rss <- sum((ytr - ols_predict(fit, Xp))^2)
k <- ncol(Xp) + 1 # +1 intercept
aicc <- compute_aicc(n, rss, k)
fold_rmse <- numeric(length(folds))
for (i in seq_along(folds)) {
val_idx <- folds[[i]]
tr_idx <- setdiff(1:n, val_idx)
f <- ols_fit(Xp[tr_idx, , drop = FALSE], ytr[tr_idx])
fold_rmse[i] <- rmse(ytr[val_idx], ols_predict(f, Xp[val_idx, , drop = FALSE]))
}
fold_scores[[as.character(degree)]] <- fold_rmse
rows[[degree]] <- data.frame(degree = degree, k = k, AICc = aicc,
CV_RMSE = sqrt(mean(fold_rmse^2)))
}
res <- do.call(rbind, rows)
degree_aic <- res$degree[which.min(res$AICc)]
degree_cv <- res$degree[which.min(res$CV_RMSE)]
evaluate_route <- function(route, degree) {
fit <- ols_fit(Xp_all[[degree]]$tr, ytr)
pred <- ols_predict(fit, Xp_all[[degree]]$te)
row <- res[res$degree == degree, ]
list(config = paste0("degree=", degree),
AICc = if (route == "AIC") row$AICc else NA,
CV_RMSE = if (route == "CV") row$CV_RMSE else NA,
test_RMSE = rmse(yte, pred), test_R2 = r2_score(yte, pred))
}
list(table = res,
final = list(AIC = evaluate_route("AIC", degree_aic),
CV = evaluate_route("CV", degree_cv)),
cv_fold_rmse = fold_scores)
}
NLS_FORMS <- c("exp(AT)", "exp(AT)+exp(V)", "exp(linear)")
# Fungsi rata-rata f(Z; theta) pada fitur terstandarisasi Z (kolom bernama AT, V, AP, RH)
nls_function <- function(name) {
if (name == "exp(AT)")
return(function(Z, b) b[1] + b[2] * exp(b[3] * Z[, "AT"]) + b[4] * Z[, "V"] +
b[5] * Z[, "AP"] + b[6] * Z[, "RH"])
if (name == "exp(AT)+exp(V)")
return(function(Z, b) b[1] + b[2] * exp(b[3] * Z[, "AT"]) + b[4] * exp(b[5] * Z[, "V"]) +
b[6] * Z[, "AP"] + b[7] * Z[, "RH"])
function(Z, b) exp(b[1] + b[2] * Z[, "AT"] + b[3] * Z[, "V"] + b[4] * Z[, "AP"] + b[5] * Z[, "RH"])
}
# Nilai awal tetap: koefisien linear dari OLS, pengali eksponen = 1, eksponen = -0.5
nls_start_values <- function(name, Z, y) {
b <- ols_fit(Z, y)$coef
names(b) <- colnames(Z)
if (name == "exp(AT)")
return(c(mean(y), 1.0, -0.5, b[["V"]], b[["AP"]], b[["RH"]]))
if (name == "exp(AT)+exp(V)")
return(c(mean(y), 1.0, -0.5, 1.0, -0.5, b[["AP"]], b[["RH"]]))
c(log(mean(y)), 0, 0, 0, 0)
}
# Estimasi NLS: Levenberg-Marquardt (MINPACK, seperti scipy curve_fit). Standardisasi hanya dari data fit.
# Gagal konvergen -> stop() (ditangkap pemanggil, setara RuntimeError pada Python).
nls_fit <- function(name, X_raw, y_raw) {
y_raw <- as.numeric(y_raw)
sc <- fit_scaler(X_raw)
Z <- apply_scaler(X_raw, sc$center, sc$scale)
f <- nls_function(name)
fit <- minpack.lm::nls.lm(
par = unname(nls_start_values(name, Z, y_raw)),
fn = function(p) y_raw - f(Z, p),
control = minpack.lm::nls.lm.control(maxfev = 20000, maxiter = 1024)
)
if (!(fit$info %in% 1:4)) stop("NLS tidak konvergen") # info 1-4 = konvergen
theta <- fit$par
list(theta = theta,
predict = function(X_new) f(apply_scaler(X_new, sc$center, sc$scale), theta))
}
run_nls <- function(Xtr, ytr, Xte, yte, folds) {
ytr <- as.numeric(ytr); yte <- as.numeric(yte)
n <- length(ytr)
rows <- list(); fold_scores <- list()
for (name in NLS_FORMS) {
full <- tryCatch({
m <- nls_fit(name, Xtr, ytr)
rss <- sum((ytr - m$predict(Xtr))^2)
list(k = length(m$theta), aicc = compute_aicc(n, rss, length(m$theta)))
}, error = function(e) {
cat(sprintf(" [peringatan] NLS '%s' tidak konvergen pada data latih penuh\n", name))
list(k = NA, aicc = NA)
})
fold_rmse <- numeric(length(folds))
for (i in seq_along(folds)) {
val_idx <- folds[[i]]
tr_idx <- setdiff(1:n, val_idx)
fold_rmse[i] <- tryCatch({
m <- nls_fit(name, Xtr[tr_idx, , drop = FALSE], ytr[tr_idx])
rmse(ytr[val_idx], m$predict(Xtr[val_idx, , drop = FALSE]))
}, error = function(e) {
cat(sprintf(" [peringatan] NLS '%s' tidak konvergen pada fold %d\n", name, i))
NA_real_
})
}
fold_scores[[name]] <- fold_rmse
rows[[name]] <- data.frame(form = name, k = full$k, AICc = full$aicc,
CV_RMSE = sqrt(mean(fold_rmse^2))) # sama seperti Polynomial
}
res <- do.call(rbind, rows); rownames(res) <- NULL
form_aic <- res$form[which.min(res$AICc)]
form_cv <- res$form[which.min(res$CV_RMSE)]
evaluate_route <- function(route, name) {
m <- nls_fit(name, Xtr, ytr)
pred <- m$predict(Xte)
row <- res[res$form == name, ]
list(config = paste0("form=", name),
AICc = if (route == "AIC") row$AICc else NA,
CV_RMSE = if (route == "CV") row$CV_RMSE else NA,
test_RMSE = rmse(yte, pred), test_R2 = r2_score(yte, pred))
}
list(table = res,
final = list(AIC = evaluate_route("AIC", form_aic),
CV = evaluate_route("CV", form_cv)),
cv_fold_rmse = fold_scores[[form_cv]])
}
run_gam <- function(Xtr, ytr, Xte, yte, folds, lambda_grid = NULL, k_spline = 20) {
if (is.null(lambda_grid)) lambda_grid <- 10^seq(-3, 3, length.out = 10)
df_tr <- data.frame(y = as.numeric(ytr), Xtr)
df_te <- data.frame(y = as.numeric(yte), Xte)
n_tr <- nrow(df_tr)
gam_formula <- as.formula(paste("y ~", paste(
sprintf("s(%s, bs = 'ps', k = %d)", colnames(Xtr), k_spline), collapse = " + ")))
# Rute AICc: lambda dipilih GCV; AICc dengan k = edf
gam_gcv <- gam(gam_formula, data = df_tr, method = "GCV.Cp")
rss <- sum(residuals(gam_gcv)^2)
edof <- sum(gam_gcv$hat)
aicc_gcv <- compute_aicc(n_tr, rss, edof)
lambda_gcv <- mean(gam_gcv$sp)
# Rute CV: lambda sama untuk semua smooth, grid 10^seq(-3,3,length=10)
cv_rows <- list(); fold_scores <- list()
for (j in seq_along(lambda_grid)) {
lam <- lambda_grid[j]
sp_vec <- rep(lam, ncol(Xtr))
fold_rmse <- numeric(length(folds))
for (i in seq_along(folds)) {
val_idx <- folds[[i]]
tr_idx <- setdiff(1:n_tr, val_idx)
fit <- gam(gam_formula, data = df_tr[tr_idx, ], sp = sp_vec)
pred <- as.numeric(predict(fit, newdata = df_tr[val_idx, ]))
fold_rmse[i] <- rmse(df_tr$y[val_idx], pred)
}
fold_scores[[j]] <- fold_rmse
cv_rows[[j]] <- data.frame(lambda = lam, CV_RMSE = mean(fold_rmse)) # rata-rata RMSE fold
}
cv_df <- do.call(rbind, cv_rows)
best_j <- which.min(cv_df$CV_RMSE)
best_cv <- cv_df[best_j, ]
pred_test_gcv <- as.numeric(predict(gam_gcv, newdata = df_te))
gam_cv <- gam(gam_formula, data = df_tr, sp = rep(best_cv$lambda, ncol(Xtr)))
pred_test_cv <- as.numeric(predict(gam_cv, newdata = df_te))
list(table = cv_df,
final = list(
AIC = list(config = sprintf("lambda(GCV)~=%.4g", lambda_gcv), AICc = aicc_gcv, CV_RMSE = NA,
test_RMSE = rmse(yte, pred_test_gcv), test_R2 = r2_score(yte, pred_test_gcv)),
CV = list(config = sprintf("lambda=%.4g", best_cv$lambda), AICc = NA, CV_RMSE = best_cv$CV_RMSE,
test_RMSE = rmse(yte, pred_test_cv), test_R2 = r2_score(yte, pred_test_cv))),
cv_fold_rmse = fold_scores[[best_j]],
edof = edof, lambda_gcv = lambda_gcv)
}
run_full_analysis <- function(data_sheets, target = "PE", features = c("AT", "V", "AP", "RH"),
K = 10, seed = 42) {
sheet_names <- names(data_sheets)
n <- nrow(data_sheets[[sheet_names[1]]])
for (sh in sheet_names)
if (nrow(data_sheets[[sh]]) != n) stop("Semua sheet harus memiliki jumlah baris yang sama untuk eksperimen ini.")
split_res <- make_train_test_split(n, test_size = 0.20, seed = seed)
train_idx <- split_res$train
test_idx <- split_res$test
folds <- make_folds(length(train_idx), K = K, seed = seed)
splits <- list()
for (sh in sheet_names) {
d <- data_sheets[[sh]]
splits[[sh]] <- list(X_train = d[train_idx, features, drop = FALSE],
X_test = d[test_idx, features, drop = FALSE],
y_train = d[train_idx, target],
y_test = d[test_idx, target])
}
poly_all <- list(); nls_all <- list(); gam_all <- list()
for (sh in sheet_names) {
sp <- splits[[sh]]
cat(sprintf("[%s] menjalankan Polynomial...\n", sh))
poly_all[[sh]] <- run_polynomial(sp$X_train, sp$y_train, sp$X_test, sp$y_test, folds)
cat(sprintf("[%s] menjalankan NLS...\n", sh))
nls_all[[sh]] <- run_nls(sp$X_train, sp$y_train, sp$X_test, sp$y_test, folds)
cat(sprintf("[%s] menjalankan GAM...\n", sh))
gam_all[[sh]] <- run_gam(sp$X_train, sp$y_train, sp$X_test, sp$y_test, folds)
}
list(sheet_names = sheet_names, splits = splits, folds = folds,
poly_all = poly_all, nls_all = nls_all, gam_all = gam_all)
}
result <- run_full_analysis(data_sheets, target = target, features = features, K = 10, seed = RANDOM_STATE)
## [Sheet1] menjalankan Polynomial...
## [Sheet1] menjalankan NLS...
## [Sheet1] menjalankan GAM...
## [Sheet2] menjalankan Polynomial...
## [Sheet2] menjalankan NLS...
## [Sheet2] menjalankan GAM...
## [Sheet3] menjalankan Polynomial...
## [Sheet3] menjalankan NLS...
## [Sheet3] menjalankan GAM...
## [Sheet4] menjalankan Polynomial...
## [Sheet4] menjalankan NLS...
## [Sheet4] menjalankan GAM...
## [Sheet5] menjalankan Polynomial...
## [Sheet5] menjalankan NLS...
## [Sheet5] menjalankan GAM...
cat("\nSelesai menjalankan Polynomial/NLS/GAM untuk seluruh sheet:", paste(result$sheet_names, collapse = ", "), "\n")
##
## Selesai menjalankan Polynomial/NLS/GAM untuk seluruh sheet: Sheet1, Sheet2, Sheet3, Sheet4, Sheet5
n_train <- length(result$splits[[1]]$y_train); n_test <- length(result$splits[[1]]$y_test)
Data pada tiap sheet dibagi menjadi 7654 observasi latih dan 1914 observasi uji, dengan 10 fold pada data latih.
sh1 <- result$sheet_names[1]
cat("Polynomial -", sh1, "\n"); tbl(result$poly_all[[sh1]]$table)
## Polynomial - Sheet1
| degree | k | AICc | CV_RMSE |
|---|---|---|---|
| 1 | 5 | 23318.05 | 4.5880 |
| 2 | 15 | 22316.47 | 4.2963 |
| 3 | 35 | 21967.29 | 4.2016 |
cat("NLS -", sh1, "\n"); tbl(result$nls_all[[sh1]]$table)
## NLS - Sheet1
| form | k | AICc | CV_RMSE |
|---|---|---|---|
| exp(AT) | 6 | 22597.75 | 4.3776 |
| exp(AT)+exp(V) | 7 | 22595.91 | 4.3769 |
| exp(linear) | 5 | 23105.92 | 4.5249 |
cat("GAM (rute CV) -", sh1, "\n"); tbl(result$gam_all[[sh1]]$table)
## GAM (rute CV) - Sheet1
| lambda | CV_RMSE |
|---|---|
| 0.0010 | 4.1783 |
| 0.0046 | 4.1780 |
| 0.0215 | 4.1764 |
| 0.1000 | 4.1721 |
| 0.4642 | 4.1658 |
| 2.1544 | 4.1625 |
| 10.0000 | 4.1620 |
| 46.4159 | 4.1631 |
| 215.4435 | 4.1693 |
| 1000.0000 | 4.1832 |
get_config_degree <- function(config) as.integer(sub(".*=", "", config))
build_best_model_df <- function(result) {
rows <- list()
for (sh in result$sheet_names) {
aicc_vals <- c(Polynomial = result$poly_all[[sh]]$final$AIC$AICc,
NLS = result$nls_all[[sh]]$final$AIC$AICc,
GAM = result$gam_all[[sh]]$final$AIC$AICc)
cv_vals <- c(Polynomial = result$poly_all[[sh]]$final$CV$CV_RMSE,
NLS = result$nls_all[[sh]]$final$CV$CV_RMSE,
GAM = result$gam_all[[sh]]$final$CV$CV_RMSE)
best_aic <- names(aicc_vals)[which.min(aicc_vals)]
best_cv <- names(cv_vals)[which.min(cv_vals)]
rows[[sh]] <- data.frame(
Sheet = sh,
AICc_Polynomial = aicc_vals[["Polynomial"]], AICc_NLS = aicc_vals[["NLS"]], AICc_GAM = aicc_vals[["GAM"]],
CV_RMSE_Polynomial = cv_vals[["Polynomial"]], CV_RMSE_NLS = cv_vals[["NLS"]], CV_RMSE_GAM = cv_vals[["GAM"]],
Model_Terbaik_AIC = best_aic, Model_Terbaik_CV = best_cv, Sama = (best_aic == best_cv))
}
out <- do.call(rbind, rows); rownames(out) <- NULL
out
}
best_model_df <- build_best_model_df(result)
agreement_rate <- mean(best_model_df$Sama)
cat(sprintf("Tingkat kesepakatan model terbaik (AICc vs CV) antar sheet: %.2f%%\n", agreement_rate * 100))
## Tingkat kesepakatan model terbaik (AICc vs CV) antar sheet: 100.00%
tbl(best_model_df[, c("Sheet", "AICc_Polynomial", "AICc_NLS", "AICc_GAM")])
| Sheet | AICc_Polynomial | AICc_NLS | AICc_GAM |
|---|---|---|---|
| Sheet1 | 21967.29 | 22595.91 | 21878.05 |
| Sheet2 | 21947.70 | 22558.62 | 21866.38 |
| Sheet3 | 21944.92 | 22577.70 | 21888.98 |
| Sheet4 | 21786.73 | 22469.04 | 21694.37 |
| Sheet5 | 21927.22 | 22548.09 | 21830.49 |
tbl(best_model_df[, c("Sheet", "CV_RMSE_Polynomial", "CV_RMSE_NLS", "CV_RMSE_GAM")])
| Sheet | CV_RMSE_Polynomial | CV_RMSE_NLS | CV_RMSE_GAM |
|---|---|---|---|
| Sheet1 | 4.2016 | 4.3769 | 4.1620 |
| Sheet2 | 4.1910 | 4.3643 | 4.1657 |
| Sheet3 | 4.1922 | 4.3707 | 4.1727 |
| Sheet4 | 4.1515 | 4.3408 | 4.1275 |
| Sheet5 | 4.1886 | 4.3633 | 4.1547 |
tbl(best_model_df[, c("Sheet", "Model_Terbaik_AIC", "Model_Terbaik_CV", "Sama")])
| Sheet | Model_Terbaik_AIC | Model_Terbaik_CV | Sama |
|---|---|---|---|
| Sheet1 | GAM | GAM | TRUE |
| Sheet2 | GAM | GAM | TRUE |
| Sheet3 | GAM | GAM | TRUE |
| Sheet4 | GAM | GAM | TRUE |
| Sheet5 | GAM | GAM | TRUE |
build_rank_corr_df <- function(best_model_df) {
rows <- list()
for (i in 1:nrow(best_model_df)) {
row <- best_model_df[i, ]
aicc <- c(row$AICc_Polynomial, row$AICc_NLS, row$AICc_GAM)
cv <- c(row$CV_RMSE_Polynomial, row$CV_RMSE_NLS, row$CV_RMSE_GAM)
res <- tryCatch(cor.test(aicc, cv, method = "spearman"), error = function(e) NULL)
rows[[i]] <- data.frame(Sheet = row$Sheet,
Spearman_rho = if (is.null(res)) NA_real_ else unname(res$estimate),
p_value = if (is.null(res)) NA_real_ else res$p.value)
}
do.call(rbind, rows)
}
rank_corr_df <- build_rank_corr_df(best_model_df)
tbl(rank_corr_df)
| Sheet | Spearman_rho | p_value |
|---|---|---|
| Sheet1 | 1 | 0.3333 |
| Sheet2 | 1 | 0.3333 |
| Sheet3 | 1 | 0.3333 |
| Sheet4 | 1 | 0.3333 |
| Sheet5 | 1 | 0.3333 |
Interpretasi : Berdasarkan AICc, model terbaik adalah GAM (5 dari 5 sheet). Berdasarkan CV-RMSE, model terbaik adalah GAM (5 dari 5 sheet). Kisaran CV-RMSE antar-sheet: Polynomial 4,151-4,202, NLS 4,341-4,377, dan GAM 4,127-4,173. Kedua rute memilih model yang sama pada 5 dari 5 sheet (100,0%).
Koefisien Spearman antara peringkat AICc dan CV-RMSE ketiga model
berkisar 1,000 dengan p-value 0,333. Dengan hanya \(M=3\) model, p-value terkecil yang mungkin
bagi uji Spearman eksak adalah sekitar 0,3333, sehingga p-value besar
pada uji ini terutama mencerminkan sedikitnya jumlah model yang
diperingkat, bukan lemahnya kesesuaian urutan. Perlu dicatat pula bahwa
scipy.stats.spearmanr pada notebook menghitung p-value
dengan aproksimasi distribusi-\(t\),
sedangkan cor.test di R memakai distribusi eksak untuk
sampel kecil, sehingga p-value kedua bahasa dapat berbeda untuk \(\rho\) yang sama.
isclose <- function(a, b, rtol = 1e-5, atol = 1e-8) abs(a - b) <= (atol + rtol * abs(b))
build_overall_best <- function(result) {
rows <- list()
for (sh in result$sheet_names) {
pt <- result$poly_all[[sh]]$table; nt <- result$nls_all[[sh]]$table
fp <- result$poly_all[[sh]]$final; fn <- result$nls_all[[sh]]$final; fg <- result$gam_all[[sh]]$final
deg_c <- get_config_degree(fp$CV$config)
form_c <- sub("^form=", "", fn$CV$config)
cand <- data.frame(
Model = c("Polynomial", "NLS", "GAM"),
Parameter = c(fp$CV$config, fn$CV$config, fg$CV$config),
AICc = c(min(pt$AICc, na.rm = TRUE), min(nt$AICc, na.rm = TRUE), fg$AIC$AICc),
CV_RMSE = c(fp$CV$CV_RMSE, fn$CV$CV_RMSE, fg$CV$CV_RMSE),
Test_RMSE = c(fp$CV$test_RMSE, fn$CV$test_RMSE, fg$CV$test_RMSE),
Test_R2 = c(fp$CV$test_R2, fn$CV$test_R2, fg$CV$test_R2),
stringsAsFactors = FALSE)
cand$AICc_Best <- isclose(cand$AICc, min(cand$AICc, na.rm = TRUE))
cand$CV_Best <- isclose(cand$CV_RMSE, min(cand$CV_RMSE, na.rm = TRUE))
cand$Both_Best <- cand$AICc_Best & cand$CV_Best
if (any(cand$Both_Best, na.rm = TRUE)) {
b <- which(cand$Both_Best); best_idx <- b[which.min(cand$AICc[b])]
status <- "Terbaik pada AICc dan CV_RMSE"
} else {
best_idx <- which.min(cand$CV_RMSE)
status <- "AICc dan CV_RMSE tidak sepakat; mengikuti minimum CV_RMSE"
}
rows[[sh]] <- data.frame(Sheet = sh, Model_Akhir = cand$Model[best_idx], Konfigurasi = cand$Parameter[best_idx],
AICc = cand$AICc[best_idx], CV_RMSE = cand$CV_RMSE[best_idx],
Test_RMSE = cand$Test_RMSE[best_idx], Test_R2 = cand$Test_R2[best_idx],
Status = status, stringsAsFactors = FALSE)
}
out <- do.call(rbind, rows); rownames(out) <- NULL
out
}
overall_df <- build_overall_best(result)
tbl(overall_df[, c("Sheet", "Model_Akhir", "Konfigurasi", "Status")])
| Sheet | Model_Akhir | Konfigurasi | Status |
|---|---|---|---|
| Sheet1 | GAM | lambda=10 | Terbaik pada AICc dan CV_RMSE |
| Sheet2 | GAM | lambda=10 | Terbaik pada AICc dan CV_RMSE |
| Sheet3 | GAM | lambda=10 | Terbaik pada AICc dan CV_RMSE |
| Sheet4 | GAM | lambda=10 | Terbaik pada AICc dan CV_RMSE |
| Sheet5 | GAM | lambda=10 | Terbaik pada AICc dan CV_RMSE |
tbl(overall_df[, c("Sheet", "AICc", "CV_RMSE", "Test_RMSE", "Test_R2")])
| Sheet | AICc | CV_RMSE | Test_RMSE | Test_R2 |
|---|---|---|---|---|
| Sheet1 | 21878.05 | 4.1620 | 4.0119 | 0.9443 |
| Sheet2 | 21866.38 | 4.1657 | 4.0238 | 0.9462 |
| Sheet3 | 21888.98 | 4.1727 | 3.9975 | 0.9453 |
| Sheet4 | 21694.37 | 4.1275 | 4.2185 | 0.9372 |
| Sheet5 | 21830.49 | 4.1547 | 4.0634 | 0.9435 |
Evaluasi pada data uji menilai kemampuan model memprediksi observasi yang tidak dipakai saat pelatihan, menggunakan RMSE dan \(R^2\).
detail_rows <- list()
for (sh in result$sheet_names) {
for (model_name in c("Polynomial", "NLS", "GAM")) {
all_dict <- switch(model_name, "Polynomial" = result$poly_all, "NLS" = result$nls_all, "GAM" = result$gam_all)
for (route in c("AIC", "CV")) {
f <- all_dict[[sh]]$final[[route]]
detail_rows[[length(detail_rows) + 1]] <- data.frame(
Sheet = sh, Model = model_name, Rute = route, Konfigurasi = f$config,
AICc = f$AICc, CV_RMSE = f$CV_RMSE, Test_RMSE = f$test_RMSE, Test_R2 = f$test_R2,
stringsAsFactors = FALSE)
}
}
}
detail_df <- do.call(rbind, detail_rows)
tbl(detail_df[, c("Sheet", "Model", "Rute", "Konfigurasi", "Test_RMSE", "Test_R2")])
| Sheet | Model | Rute | Konfigurasi | Test_RMSE | Test_R2 |
|---|---|---|---|---|---|
| Sheet1 | Polynomial | AIC | degree=3 | 4.0311 | 0.9438 |
| Sheet1 | Polynomial | CV | degree=3 | 4.0311 | 0.9438 |
| Sheet1 | NLS | AIC | form=exp(AT)+exp(V) | 4.2073 | 0.9388 |
| Sheet1 | NLS | CV | form=exp(AT)+exp(V) | 4.2073 | 0.9388 |
| Sheet1 | GAM | AIC | lambda(GCV)~=151.8 | 4.0064 | 0.9445 |
| Sheet1 | GAM | CV | lambda=10 | 4.0119 | 0.9443 |
| Sheet2 | Polynomial | AIC | degree=3 | 4.0556 | 0.9454 |
| Sheet2 | Polynomial | CV | degree=3 | 4.0556 | 0.9454 |
| Sheet2 | NLS | AIC | form=exp(AT)+exp(V) | 4.2515 | 0.9400 |
| Sheet2 | NLS | CV | form=exp(AT)+exp(V) | 4.2515 | 0.9400 |
| Sheet2 | GAM | AIC | lambda(GCV)~=152.9 | 4.0230 | 0.9463 |
| Sheet2 | GAM | CV | lambda=10 | 4.0238 | 0.9462 |
| Sheet3 | Polynomial | AIC | degree=3 | 4.0585 | 0.9437 |
| Sheet3 | Polynomial | CV | degree=3 | 4.0585 | 0.9437 |
| Sheet3 | NLS | AIC | form=exp(AT)+exp(V) | 4.2281 | 0.9388 |
| Sheet3 | NLS | CV | form=exp(AT)+exp(V) | 4.2281 | 0.9388 |
| Sheet3 | GAM | AIC | lambda(GCV)~=310.1 | 3.9977 | 0.9453 |
| Sheet3 | GAM | CV | lambda=10 | 3.9975 | 0.9453 |
| Sheet4 | Polynomial | AIC | degree=3 | 4.2316 | 0.9368 |
| Sheet4 | Polynomial | CV | degree=3 | 4.2316 | 0.9368 |
| Sheet4 | NLS | AIC | form=exp(AT)+exp(V) | 4.3534 | 0.9331 |
| Sheet4 | NLS | CV | form=exp(AT)+exp(V) | 4.3534 | 0.9331 |
| Sheet4 | GAM | AIC | lambda(GCV)~=167 | 4.2118 | 0.9374 |
| Sheet4 | GAM | CV | lambda=10 | 4.2185 | 0.9372 |
| Sheet5 | Polynomial | AIC | degree=3 | 4.0747 | 0.9432 |
| Sheet5 | Polynomial | CV | degree=3 | 4.0747 | 0.9432 |
| Sheet5 | NLS | AIC | form=exp(AT)+exp(V) | 4.2637 | 0.9378 |
| Sheet5 | NLS | CV | form=exp(AT)+exp(V) | 4.2637 | 0.9378 |
| Sheet5 | GAM | AIC | lambda(GCV)~=104.3 | 4.0578 | 0.9437 |
| Sheet5 | GAM | CV | lambda=10 | 4.0634 | 0.9435 |
tbl(detail_df[, c("Sheet", "Model", "Rute", "AICc", "CV_RMSE")])
| Sheet | Model | Rute | AICc | CV_RMSE |
|---|---|---|---|---|
| Sheet1 | Polynomial | AIC | 21967.29 | - |
| Sheet1 | Polynomial | CV | - | 4.2016 |
| Sheet1 | NLS | AIC | 22595.91 | - |
| Sheet1 | NLS | CV | - | 4.3769 |
| Sheet1 | GAM | AIC | 21878.05 | - |
| Sheet1 | GAM | CV | - | 4.1620 |
| Sheet2 | Polynomial | AIC | 21947.70 | - |
| Sheet2 | Polynomial | CV | - | 4.1910 |
| Sheet2 | NLS | AIC | 22558.62 | - |
| Sheet2 | NLS | CV | - | 4.3643 |
| Sheet2 | GAM | AIC | 21866.38 | - |
| Sheet2 | GAM | CV | - | 4.1657 |
| Sheet3 | Polynomial | AIC | 21944.92 | - |
| Sheet3 | Polynomial | CV | - | 4.1922 |
| Sheet3 | NLS | AIC | 22577.70 | - |
| Sheet3 | NLS | CV | - | 4.3707 |
| Sheet3 | GAM | AIC | 21888.98 | - |
| Sheet3 | GAM | CV | - | 4.1727 |
| Sheet4 | Polynomial | AIC | 21786.73 | - |
| Sheet4 | Polynomial | CV | - | 4.1515 |
| Sheet4 | NLS | AIC | 22469.04 | - |
| Sheet4 | NLS | CV | - | 4.3408 |
| Sheet4 | GAM | AIC | 21694.37 | - |
| Sheet4 | GAM | CV | - | 4.1275 |
| Sheet5 | Polynomial | AIC | 21927.22 | - |
| Sheet5 | Polynomial | CV | - | 4.1886 |
| Sheet5 | NLS | AIC | 22548.09 | - |
| Sheet5 | NLS | CV | - | 4.3633 |
| Sheet5 | GAM | AIC | 21830.49 | - |
| Sheet5 | GAM | CV | - | 4.1547 |
Interpretasi : Ringkasan rata-rata antar-sheet pada rute CV: GAM: RMSE rata-rata 4,063, \(R^2\) rata-rata 0,9433; NLS: RMSE rata-rata 4,261, \(R^2\) rata-rata 0,9377; Polynomial: RMSE rata-rata 4,090, \(R^2\) rata-rata 0,9426. Untuk model akhir tiap sheet, RMSE data uji berkisar 3,997-4,218 dan \(R^2\) berkisar 0,937-0,946; \(R^2\) menyatakan proporsi variasi PE pada data uji yang dijelaskan model, sedangkan RMSE menyatakan besarnya galat prediksi dalam satuan PE.
Uji ANOVA satu arah dan Kruskal–Wallis diterapkan pada 10 nilai RMSE fold dari konfigurasi terpilih rute CV untuk menguji apakah kinerja berbeda antar-sheet.
cv_fold_store <- list(Polynomial = list(), NLS = list(), GAM = list())
for (sh in result$sheet_names) {
deg <- get_config_degree(result$poly_all[[sh]]$final$CV$config)
cv_fold_store$Polynomial[[sh]] <- result$poly_all[[sh]]$cv_fold_rmse[[as.character(deg)]]
cv_fold_store$NLS[[sh]] <- result$nls_all[[sh]]$cv_fold_rmse
cv_fold_store$GAM[[sh]] <- result$gam_all[[sh]]$cv_fold_rmse
}
anova_rows <- list()
for (model_name in names(cv_fold_store)) {
per_sheet <- cv_fold_store[[model_name]]
df_anova <- do.call(rbind, lapply(names(per_sheet), function(sh)
data.frame(Sheet = sh, RMSE = per_sheet[[sh]])))
df_anova$Sheet <- factor(df_anova$Sheet)
aov_res <- summary(aov(RMSE ~ Sheet, data = df_anova))[[1]]
kw_res <- kruskal.test(RMSE ~ Sheet, data = df_anova)
anova_rows[[model_name]] <- data.frame(
Model = model_name,
ANOVA_F = aov_res["Sheet", "F value"], ANOVA_p = aov_res["Sheet", "Pr(>F)"],
KruskalWallis_H = unname(kw_res$statistic), KruskalWallis_p = kw_res$p.value,
Signifikan_p_lt_0_05 = (aov_res["Sheet", "Pr(>F)"] < 0.05))
}
anova_df <- do.call(rbind, anova_rows); rownames(anova_df) <- NULL
tbl(anova_df)
| Model | ANOVA_F | ANOVA_p | KruskalWallis_H | KruskalWallis_p | Signifikan_p_lt_0_05 |
|---|---|---|---|---|---|
| Polynomial | 0.0347 | 0.9976 | 0.3624 | 0.9854 | FALSE |
| NLS | 0.0137 | 0.9996 | 1.0071 | 0.9087 | FALSE |
| GAM | 0.0408 | 0.9967 | 0.8546 | 0.9310 | FALSE |
plot_df <- do.call(rbind, lapply(names(cv_fold_store), function(model_name)
do.call(rbind, lapply(names(cv_fold_store[[model_name]]), function(sh)
data.frame(Model = model_name, Sheet = sh, CV_Fold_RMSE = cv_fold_store[[model_name]][[sh]])))))
ggplot(plot_df, aes(x = Sheet, y = CV_Fold_RMSE)) +
geom_boxplot(fill = "lightsteelblue") +
facet_wrap(~ Model, scales = "free_y") +
labs(title = "Sebaran CV-Fold RMSE per Sheet (dasar uji ANOVA/Kruskal-Wallis)") +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
Interpretasi : p-value ANOVA: Polynomial = 0,9976, NLS = 0,9996, GAM = 0,9967. p-value Kruskal–Wallis: Polynomial = 0,9854, NLS = 0,9087, GAM = 0,9310. Seluruh p-value lebih besar dari 0,05, sehingga tidak terdapat bukti statistik adanya perbedaan kinerja antar-sheet berdasarkan CV-fold RMSE, baik menurut ANOVA maupun Kruskal-Wallis. Boxplot memperlihatkan sebaran CV-fold RMSE tiap sheet.
Hasil eksplorasi : Korelasi antara prediktor dan PE serta diagnostik residual model linear awal menjadi dasar mempertimbangkan metode yang dapat menangkap hubungan nonlinier. Pemeriksaan VIF menunjukkan bahwa prediktor dengan VIF tertinggi adalah AT (5,978). Uji Shapiro–Wilk dan Breusch–Pagan pada model linear awal dilaporkan sebagai catatan interpretasi, bukan sebagai alasan transformasi data.
Perbandingan metode : Rata-rata CV-RMSE antar-sheet pada konfigurasi terpilih rute CV adalah Polynomial 4,185, NLS 4,363, GAM 4,157; metode yang paling sering terbaik menurut CV-RMSE adalah GAM (5 dari 5 sheet). Secara teoretis, ketiga metode berbeda dalam cara menangkap nonlinearitas. Polynomial Regression membatasi bentuk hubungan pada polinomial berderajat \(d\) dengan suku interaksi, dan jumlah parameternya tumbuh cepat terhadap \(d\) (\(k=5,15,35\)). NLS menuntut bentuk fungsi parametrik yang ditentukan sebelumnya, di sini berbentuk eksponensial, sehingga lebih hemat parameter (\(k=5\) sampai \(7\)) tetapi hanya sesuai bila bentuk tersebut memadai bagi data dan bergantung pada nilai awal serta konvergensi optimisasi. GAM tidak memaksakan bentuk parametrik karena tiap prediktor dimodelkan dengan fungsi smooth yang kehalusannya diatur oleh \(\lambda\) (Hastie & Tibshirani, 1986), dengan konsekuensi struktur aditif tanpa interaksi antarprediktor.
Konsistensi kriteria : Kesesuaian pemilihan model antara rute AICc dan rute CV adalah 100,0% antar-sheet. Kedua kriteria berbeda secara prinsip: AICc menghukum kompleksitas secara analitik berdasarkan RSS pada data latih, sedangkan CV mengestimasi galat prediksi pada data yang tidak dipakai melatih model (Arlot & Celisse, 2010; Portet, 2020). Oleh karena itu, kesepakatan keduanya memperkuat keyakinan pada pemilihan model, sedangkan ketidaksepakatan menunjukkan bahwa keputusan bergantung pada kriteria yang dipakai.
Antar-sheet : Hasil ANOVA dan Kruskal–Wallis menunjukkan bahwa tidak ada bukti perbedaan kinerja antar-sheet. Karena pemeriksaan kesamaan data menunjukkan 5 dari 5 sheet identik dengan sheet pertama setelah diurutkan, perbedaan antar-sheet bersumber dari urutan baris (randomisasi), sehingga kestabilan hasil antar-sheet mencerminkan kestabilan terhadap pengacakan.
Evaluasi data uji : Model akhir mencapai RMSE data uji 3,997-4,218 dan \(R^2\) 0,937-0,946.
Berdasarkan seluruh analisis diperoleh kesimpulan berikut.
Data memuat 9568 observasi per sheet dengan 0 missing value dan 41 baris duplikat. Korelasi terhadap PE: AT -0,95, V -0,87, AP 0,52, RH 0,39. Model linear awal menunjukkan penyimpangan normalitas residual dan indikasi heteroskedastisitas pada taraf 5%. Sheet yang berbeda pada dataset adalah pengacakan berulang pada data, sehingga setiap sheet memiliki data yang sama hanya saja berbeda urutan dengan tujuan untuk mengetahui apakah pengacakan berpengaruh pada performa model dalam menjelaskan pengaruh variabel prediktor terhadap variabel dependen.
Polynomial Regression, Nonlinear Least Squares (NLS), dan Generalized Additive Model (GAM) dapat digunakan untuk memodelkan hubungan AT, V, AP, RH terhadap PE. Rata-rata CV-RMSE antar-sheet: Polynomial 4,185, NLS 4,363, GAM 4,157.
Pemilihan model berdasarkan AICc dan CV-RMSE sepakat pada 5 dari 5 sheet. Model terbaik menurut AICc: GAM (5 dari 5 sheet); menurut CV-RMSE: GAM (5 dari 5 sheet).
Model akhir pada seluruh sheet: GAM (5 dari 5 sheet), dengan RMSE data uji 3,997-4,218 dan \(R^2\) 0,937-0,946.
Uji ANOVA dan Kruskal–Wallis tidak menunjukkan perbedaan kinerja yang signifikan antar-sheet berdasarkan CV-fold RMSE pada taraf 5%.
DataFrame.skew and DataFrame.kurt. https://pandas.pydata.org/docs/gam, s, dan
smooth.construct.ps.smooth.spec). CRAN. https://CRAN.R-project.org/package=mgcv