Perbandingan Polynomial Regression, Nonlinear Least Squares (NLS), dan Generalized Additive Model (GAM) pada Data Combined Cycle Power Plant

Yuda Taufiqurahman Wenske - 140720260017 ; Rasendriya Nandana Kurniawan - 140720260011

06 October 2026

Notasi dan Simbol

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)

1. Model Regresi dan Ordinary Least Squares

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.

2. Regresi Polinomial Multivariat

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).

3. Nonlinear Least Squares (NLS)

3.1 Model dan penaksir

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).

3.2 Algoritma iteratif: Gauss–Newton dan Levenberg–Marquardt

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.

3.3 Nilai awal dan konvergensi

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.

4. Generalized Additive Model (GAM)

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).

5. Pemilihan Model

5.1 AIC dan AICc

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.

5.2 K-fold Cross-Validation

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.

6. Diagnostik Data dan Asumsi

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.

7. Uji Kesesuaian Peringkat dan Perbedaan Antar-Kelompok

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.

Metodologi Penelitian

1. Data Penelitian

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.

2.1 Eksplorasi Data dan Pemeriksaan Awal

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.

2.2 Pembagian Data dan 10-Fold Cross-Validation

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).

2.3 Polynomial Regression

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.

2.4 Nonlinear Least Squares (NLS)

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.

2.5 Generalized Additive Model (GAM)

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\):

  • Rute AICc/GCV: \(\lambda\) tiap smooth dipilih otomatis dengan GCV (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.
  • Rute CV: \(\lambda\) yang sama diberlakukan pada keempat smooth dan dipilih dari grid \(\lambda\in 10^{\,\text{seq}(-3,3,\text{length}=10)}\); untuk tiap \(\lambda\) dihitung \(\mathrm{CV\text{-}RMSE}^{\text{mean}}\) dari 10 fold, lalu \(\lambda\) dengan nilai minimum dipilih.

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.

2.6 Pemilihan Model per Sheet dan Model Akhir

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.

2.7 Evaluasi pada Data Uji, Spearman, ANOVA, dan Kruskal–Wallis

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).

2.8 Tahapan Penelitian

  1. membaca dan memvalidasi data seluruh sheet,
  2. memeriksa kesamaan data antar-sheet,
  3. melakukan eksplorasi data,
  4. membagi data menjadi data latih dan data uji,
  5. membentuk 10 fold pada data latih,
  6. membangun Polynomial Regression, NLS, dan GAM pada setiap sheet,
  7. memilih model berdasarkan AICc dan CV-RMSE,
  8. mengevaluasi model pada data uji (RMSE dan \(R^2\)),
  9. menganalisis kesesuaian peringkat (Spearman) dan perbedaan kinerja antar-sheet (ANOVA dan Kruskal–Wallis).

Hasil Penelitian

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.

1. Hasil Eksplorasi dan Pemeriksaan Data

1.1 Struktur dan Karakteristik Data

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.

1.2 Pemeriksaan Kualitas dan Distribusi Data

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.

1.3 Hubungan Antarvariabel dan Pemeriksaan Asumsi Awal

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.

1.4 Fungsi Bantu Pemodelan

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))))
}

2. Hasil Pemodelan

Tiap metode dijalankan pada setiap sheet dengan pembagian data dan skema 10-fold CV yang sama, sehingga hasil antar-metode dapat dibandingkan.

2.1 Polynomial Regression

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)
}

2.2 Nonlinear Least Squares (NLS)

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]])
}

2.3 Generalized Additive Model (GAM)

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)
}

2.4 Menjalankan Analisis pada Seluruh Sheet

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.

2.5 Tabel Hasil Pemilihan Hyperparameter (Sheet Pertama)

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

3. Perbandingan dan Pemilihan Model

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.

3.1 Penentuan Model Akhir per Sheet

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

4. Evaluasi Model pada Data Uji

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.

5. Perbedaan Kinerja Antar Randomisasi (Antar-Sheet)

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.

6. Pembahasan

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.

Kesimpulan

Berdasarkan seluruh analisis diperoleh kesimpulan berikut.

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. Uji ANOVA dan Kruskal–Wallis tidak menunjukkan perbedaan kinerja yang signifikan antar-sheet berdasarkan CV-fold RMSE pada taraf 5%.

Daftar Pustaka