Merge branch 'flutter-update'

This commit is contained in:
rhanarmt 2026-06-29 09:19:10 +07:00
commit 4df1fb966e
14 changed files with 520 additions and 211 deletions

File diff suppressed because one or more lines are too long

View File

@ -212,4 +212,12 @@
<text x="1180" y="3210" text-anchor="middle" class="small">Ya</text> <text x="1180" y="3210" text-anchor="middle" class="small">Ya</text>
<path d="M965,3152.5 L830,3152.5 L970,3162.5" class="line" marker-end="url(#arrow)"/> <path d="M965,3152.5 L830,3152.5 L970,3162.5" class="line" marker-end="url(#arrow)"/>
<text x="870" y="3135" text-anchor="middle" class="small">Tidak</text> <text x="870" y="3135" text-anchor="middle" class="small">Tidak</text>
<!-- Loopback: login_retry to input_login -->
<path d="M90,1312.5 L50,1312.5 L50,775 L306,775" class="line" marker-end="url(#arrow)"/>
<!-- Loopback: account_retry to input_email -->
<path d="M2090,1207.5 L2130,1207.5 L2130,775 L1720,775" class="line" marker-end="url(#arrow)"/>
<!-- Loopback: otp_retry to input_otp -->
<path d="M2090,1992.5 L2130,1992.5 L2130,1560 L1715,1560" class="line" marker-end="url(#arrow)"/>
<!-- Connection: reset_ok to login_page -->
<path d="M1570,2245 L1570,2270 L1100,2270 L1100,555" class="line" marker-end="url(#arrow)"/>
</svg> </svg>

View File

@ -682,6 +682,9 @@ class _PredictionScreenState extends State<PredictionScreen> {
label: 'Permintaan', label: 'Permintaan',
value: value:
'${_controller.predictedDemand ?? 0}', '${_controller.predictedDemand ?? 0}',
infoTitle: 'Permintaan (Prediksi)',
infoText:
'Perkiraan jumlah produk yang akan dibeli pelanggan berdasarkan riwayat penjualan toko.',
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@ -690,6 +693,9 @@ class _PredictionScreenState extends State<PredictionScreen> {
label: 'Produksi', label: 'Produksi',
value: value:
'${_controller.productionQuantity} pcs', '${_controller.productionQuantity} pcs',
infoTitle: 'Jumlah Produksi',
infoText:
'Banyaknya produk yang akan Anda buat. Jumlah ini bisa disesuaikan secara manual di bagian atas.',
), ),
), ),
], ],
@ -707,6 +713,15 @@ class _PredictionScreenState extends State<PredictionScreen> {
: _controller : _controller
.predictionR2! .predictionR2!
.toStringAsFixed(4), .toStringAsFixed(4),
infoTitle: 'Akurasi Model (R²)',
infoText:
'Mengukur seberapa pintar model membaca pola penjualan Anda.\n\n'
'• Semakin mendekati 1, tebakan model semakin tepat.\n'
'• Nilai rendah/negatif berarti pola penjualan sangat fluktuatif.\n\n'
'Standar Acuan Prediksi Baik:\n'
'• Sangat Baik: 0.70 s/d 1.00\n'
'• Cukup Baik: 0.50 s/d 0.69\n'
'• Kurang Baik: Di bawah 0.50',
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@ -720,10 +735,63 @@ class _PredictionScreenState extends State<PredictionScreen> {
: _controller : _controller
.predictionMae! .predictionMae!
.toStringAsFixed(2), .toStringAsFixed(2),
infoTitle: 'Potensi Meleset (MAE)',
infoText:
'Rata-rata selisih antara hasil prediksi dengan kenyataan penjualan.\n\n'
'• Menunjukkan rata-rata seberapa jauh tebakan model bisa meleset.\n'
'• Semakin kecil nilai MAE, tebakan model semakin akurat.\n\n'
'Standar Acuan Prediksi Baik:\n'
'• Semakin dekat ke 0 semakin akurat.\n'
'• Baik/Akurat jika MAE ≤ 2.0 unit (atau di bawah 20% dari rata-rata penjualan).',
), ),
), ),
], ],
), ),
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.65),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFA89080).withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.info_outline_rounded,
size: 14,
color: Color(0xFFA89080),
),
const SizedBox(width: 6),
Text(
'Rangkuman Prediksi & Acuan Dosen:',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: const Color(0xFFA89080).withOpacity(0.9),
),
),
],
),
const SizedBox(height: 6),
Text(
'• Akurasi Model (R²): ${_controller.predictionR2 == null ? '-' : _controller.predictionR2!.toStringAsFixed(4)} (${_controller.predictionR2 == null ? 'Belum dihitung' : _controller.predictionR2! >= 0.7 ? 'Sangat Baik, target ≥ 0.50' : _controller.predictionR2! >= 0.5 ? 'Cukup Baik, target ≥ 0.50' : 'Kurang Baik, target ≥ 0.50'})\n'
'• Potensi Meleset (MAE): ${_controller.predictionMae == null ? '-' : '±${_controller.predictionMae!.toStringAsFixed(1)} unit'} (${_controller.predictionMae == null ? 'Belum dihitung' : _controller.predictionMae! <= 2.0 ? 'Sangat Akurat, target ≤ 2.0' : 'Kurang Akurat, target ≤ 2.0'})',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF4B5563),
height: 1.4,
),
),
],
),
),
], ],
), ),
), ),
@ -1448,10 +1516,17 @@ class _PredictionScreenState extends State<PredictionScreen> {
} }
class _PredictionMetric extends StatelessWidget { class _PredictionMetric extends StatelessWidget {
const _PredictionMetric({required this.label, required this.value}); const _PredictionMetric({
required this.label,
required this.value,
this.infoTitle,
this.infoText,
});
final String label; final String label;
final String value; final String value;
final String? infoTitle;
final String? infoText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -1465,9 +1540,117 @@ class _PredictionMetric extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
label, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: const TextStyle(fontSize: 11, color: Color(0xFF6B7280)), children: [
Text(
label,
style: const TextStyle(fontSize: 11, color: Color(0xFF6B7280)),
),
if (infoText != null)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
showDialog<void>(
context: context,
builder: (context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 24),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: const Color(0xFFA89080).withOpacity(0.15),
shape: BoxShape.circle,
),
child: const Icon(
Icons.info_outline_rounded,
color: Color(0xFFA89080),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
infoTitle ?? label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF1F2937),
),
),
),
],
),
const SizedBox(height: 16),
Text(
infoText!,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF4B5563),
height: 1.5,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFA89080),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'Mengerti',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
),
],
),
),
);
},
);
},
child: const Padding(
padding: EdgeInsets.only(left: 6, bottom: 4, top: 4),
child: Icon(
Icons.help_outline_rounded,
size: 13,
color: Color(0xFFA89080),
),
),
),
],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(

View File

@ -4,9 +4,9 @@
Informasi Informasi
Keterangan Keterangan
Jumlah data Jumlah data
6.742 transaksi 6.742 transaksi (6.741 data bersih)
Pembagian data Pembagian data
80% training dan 20% testing 80% training dan 20% testing secara kronologis
Target Target
jumlah_permintaan_bahan jumlah_permintaan_bahan
Model yang dibandingkan Model yang dibandingkan
@ -16,7 +16,7 @@ Tujuan dokumen
Disusun berdasarkan hasil pengujian model pada proyek tugas akhir Disusun berdasarkan hasil pengujian model pada proyek tugas akhir
Ringkasan Eksekutif Ringkasan Eksekutif
Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi dan dibagi menjadi 5.393 data training serta 1.349 data testing menggunakan rasio 80:20. Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi, dengan 1 baris memiliki missing value pada produk_encoded sehingga menghasilkan 6.741 data bersih. Data tersebut dibagi secara kronologis menjadi 5.392 data training serta 1.349 data testing menggunakan rasio 80:20.
Model Model
R² Testing R² Testing
MAE MAE
@ -72,15 +72,15 @@ Membandingkan serta mengevaluasi validitas hasil.
Komponen Komponen
Nilai Nilai
Total data Total data
6.742 transaksi 6.742 transaksi (6.741 data bersih)
Training set Training set
5.393 transaksi atau 80% 5.392 transaksi atau 80% (periode 2021-01-01 s/d 2024-12-28)
Testing set Testing set
1.349 transaksi atau 20% 1.349 transaksi atau 20% (periode 2024-12-28 s/d 2025-12-31)
Target Target
jumlah_permintaan_bahan jumlah_permintaan_bahan
Jumlah fitur model tersimpan Jumlah fitur model tersimpan
10 fitur 9 fitur
2.2 Fitur Model 2.2 Fitur Model
No. No.
@ -88,7 +88,7 @@ Jumlah fitur model tersimpan
Makna Makna
1 1
produk_encoded produk_encoded
Kode angka produk Kode angka produk (diubah menggunakan LabelEncoder)
2 2
tahun tahun
Tahun transaksi Tahun transaksi
@ -100,31 +100,31 @@ Jumlah fitur model tersimpan
Tanggal dalam bulan Tanggal dalam bulan
5 5
hari_dalam_minggu hari_dalam_minggu
Kode hari dalam minggu Kode hari dalam minggu transaksi
6 6
harga_satuan_update harga_satuan_update
Harga satuan bahan Harga satuan bahan
7 7
total_harga_update
Total harga transaksi
8
hari_minggu hari_minggu
Duplikasi informasi hari dalam minggu Kode hari minggu (dayofweek)
9 8
nama_produk_encoded nama_produk_encoded
Nama produk yang telah diubah menjadi angka Nama produk yang telah diubah menggunakan LabelEncoder
10 9
kategori_produk_encoded kategori_produk_encoded
Kategori produk yang telah diubah menjadi angka Kategori produk yang telah diubah menggunakan LabelEncoder
2.3 Pembagian Training dan Testing 2.3 Pembagian Training dan Testing
X_train, X_test, y_train, y_test = train_test_split( # Urutkan data secara kronologis
X, y, test_size=0.2, random_state=42 df = df.sort_values(by='tanggal_transaksi')
) # Bagi data dengan rasio 80:20 secara manual
split_index = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]
y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]
Parameter test_size=0.2 berarti 20% data digunakan untuk testing. Parameter random_state=42 membuat pembagian acak selalu sama ketika program dijalankan ulang, sehingga hasil pengujian dapat direproduksi. Pembagian data dilakukan secara kronologis berdasarkan urutan tanggal transaksi. Parameter split_index diambil dari 80% panjang dataset bersih (6.741 data), yaitu int(6741 * 0.8) = 5.392 data training, dan sisa 1.349 data digunakan sebagai testing. Metode ini memastikan bahwa model diuji untuk memprediksi permintaan di masa depan, bukan sekadar memprediksi data acak di masa lalu.
Catatan untuk Data Berdasarkan Waktu Pemberitahuan Metodologi
Karena tujuan sistem adalah memprediksi masa depan, pembagian data secara kronologis lebih kuat daripada pembagian acak. Data lama sebaiknya digunakan sebagai training dan data terbaru sebagai testing. Pengujian model menggunakan pembagian kronologis (chronological split) sangat penting untuk data deret waktu (time series) guna menghindari data leakage temporal, di mana model tidak sengaja mempelajari data masa depan.
3. Cara Kerja Linear Regression 3. Cara Kerja Linear Regression
Linear Regression mencari hubungan berbentuk garis lurus antara fitur masukan dan target. Setiap fitur memperoleh koefisien yang menunjukkan arah serta besarnya pengaruh terhadap hasil prediksi. Linear Regression mencari hubungan berbentuk garis lurus antara fitur masukan dan target. Setiap fitur memperoleh koefisien yang menunjukkan arah serta besarnya pengaruh terhadap hasil prediksi.
@ -212,106 +212,106 @@ RMSE
Kesalahan dengan penalti lebih besar untuk error besar Kesalahan dengan penalti lebih besar untuk error besar
6. Perhitungan Hasil Testing Proyek 6. Perhitungan Hasil Testing Proyek
Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 4,9622 unit, sedangkan total variasi aktual atau SST adalah 5.133,0719. Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 5,4188 unit, sedangkan total variasi aktual atau SST adalah 10.964,3617.
6.1 Linear Regression 6.1 Linear Regression
Komponen Komponen
Nilai Nilai
Jumlah absolute error atau SAE Jumlah absolute error atau SAE
848,4592 3.357,4719
Jumlah squared error atau SSE Jumlah squared error atau SSE
1.122,3551 11.007,5413
Jumlah data testing Jumlah data testing
1.349 1.349
Total variasi aktual atau SST Total variasi aktual atau SST
5.133,0719 10.964,3617
MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63 MAE = 3.357,4719 / 1.349 = 2,4889 ≈ 2,49
RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91 RMSE = √(11.007,5413 / 1.349) = 2,8565 ≈ 2,86
R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813 R² = 1 - (11.007,5413 / 10.964,3617) = -0,0039
Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set dan rata-rata prediksinya meleset sekitar 0,63 unit. Interpretasinya, Linear Regression tidak mampu menjelaskan variasi target pada testing set secara optimal (R² bernilai negatif sebesar -0,0039 atau -0,39%) dan rata-rata prediksinya meleset sekitar 2,49 unit.
6.2 Random Forest 6.2 Random Forest
Komponen Komponen
Nilai Nilai
Jumlah absolute error atau SAE Jumlah absolute error atau SAE
39,86 3.494,5478
Jumlah squared error atau SSE Jumlah squared error atau SSE
18,7142 12.431,3712
Jumlah data testing Jumlah data testing
1.349 1.349
Total variasi aktual atau SST Total variasi aktual atau SST
5.133,0719 10.964,3617
MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03 MAE = 3.494,5478 / 1.349 = 2,5905 ≈ 2,59
RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12 RMSE = √(12.431,3712 / 1.349) = 3,0357 ≈ 3,04
R² = 1 - (18,7142 / 5.133,0719) = 0,9964 R² = 1 - (12.431,3712 / 10.964,3617) = -0,1338
Interpretasinya, Random Forest menjelaskan sekitar 99,64% variasi target pada testing set dan rata-rata prediksinya meleset sekitar 0,03 unit. Angka ini harus dibaca bersama analisis data leakage pada bagian berikutnya. Interpretasinya, Random Forest tidak mampu menjelaskan variasi target pada testing set secara optimal (R² bernilai negatif sebesar -0,1338 atau -13,38%) dan rata-rata prediksinya meleset sekitar 2,59 unit setelah fitur total_harga_update yang menimbulkan data leakage dihapus.
6.3 Contoh Hasil Prediksi Individual 6.3 Contoh Hasil Prediksi Individual
Aktual Aktual
Prediksi Linear Regression Prediksi Linear Regression
Prediksi Random Forest Prediksi Random Forest
Error Absolut RF Error Absolut RF
4
3,5472
4,01
0,01
3
2,5369
3,00
0,00
4
4,4879
4,00
0,00
2
0,7336
2,00
0,00
7 7
7,2195 5,4795
7,00 3,71
0,00 3,29
5
5,5121
4,99
0,01
10
5,5587
5,04
4,96
1
5,4686
5,67
4,67
2
5,4883
5,64
3,64
7. Perbandingan dan Interpretasi Model 7. Perbandingan dan Interpretasi Model
Aspek Aspek
Linear Regression Linear Regression
Random Forest Random Forest
R² testing R² testing
0,7813 -0,0039
0,9964 -0,1338
MAE testing MAE testing
0,63 unit 2,49 unit
0,03 unit 2,59 unit
RMSE testing RMSE testing
0,91 unit 2,86 unit
0,12 unit 3,04 unit
Kemampuan pola non-linear Kemampuan pola non-linear
Terbatas Terbatas
Baik Baik (pada data training)
Kemudahan interpretasi Kemudahan interpretasi
Sangat mudah Sangat mudah
Lebih kompleks Lebih kompleks
Risiko pada hasil proyek Ketergantungan data leakage
Terpengaruh leakage Bebas leakage (total harga dihapus)
Sangat terpengaruh leakage Bebas leakage (total harga dihapus)
7.1 Perbandingan MAE 7.1 Perbandingan MAE
Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3% Peningkatan MAE = [(2,5905 - 2,4889) / 2,4889] x 100% = 4,08%
Pernyataan yang tepat adalah Random Forest mengurangi MAE sekitar 95,3% dibandingkan Linear Regression pada test set saat ini. Pernyataan tersebut tidak sama dengan mengatakan model 95,3% lebih akurat. Pernyataan yang tepat adalah model Random Forest menghasilkan kesalahan rata-rata yang sedikit lebih tinggi (+4,08%) dibandingkan Linear Regression pada test set aktual tanpa leakage.
7.2 Indikasi Overfitting 7.2 Indikasi Overfitting
Model Model
R² Training R² Training
R² Testing R² Testing
Selisih Selisih
Linear Regression Linear Regression
0,7902 0,0013
0,7813 -0,0039
0,0089 0,0052
Random Forest Random Forest
0,9995 0,6913
0,9964 -0,1338
0,0031 0,8251
Selisih training dan testing terlihat kecil sehingga secara angka awal model tampak stabil. Namun, kestabilan ini tidak membatalkan masalah data leakage karena informasi target tersedia pada kedua kelompok data melalui total harga. Selisih training dan testing pada Linear Regression terlihat sangat kecil, namun kedua nilainya mendekati nol, menunjukkan underfitting. Sedangkan pada Random Forest, terdapat selisih R² yang sangat besar (0,8251) antara training (0,6913) dan testing (-0,1338). Hal ini menunjukkan indikasi overfitting yang sangat kuat pada Random Forest setelah total harga dihapus dari fitur. Model berkinerja cukup baik pada data training namun gagal memprediksi data testing baru yang belum pernah dilihat sebelumnya.
8. Analisis Data Leakage 8. Analisis Data Leakage
Data leakage adalah kondisi ketika fitur model mengandung informasi yang seharusnya belum tersedia pada saat prediksi dilakukan atau secara langsung dibentuk dari target. Leakage membuat hasil testing terlihat sangat baik tetapi performa tersebut sulit dipertahankan pada penggunaan nyata. Data leakage adalah kondisi ketika fitur model mengandung informasi yang seharusnya belum tersedia pada saat prediksi dilakukan atau secara langsung dibentuk dari target. Leakage membuat hasil testing terlihat sangat baik tetapi performa tersebut sulit dipertahankan pada penggunaan nyata.
total_harga_update = harga_satuan_update x jumlah_permintaan_bahan total_harga_update = harga_satuan_update x jumlah_permintaan_bahan
@ -344,26 +344,16 @@ Untuk mengukur kemampuan prediksi yang lebih realistis, total_harga_update dihap
MAE MAE
RMSE RMSE
Random split tanpa total harga Chronological split tanpa total harga (backend Railway)
Linear Regression Linear Regression
-0,0045 -0,0039
1,53 2,49
1,96 2,86
Random split tanpa total harga Chronological split tanpa total harga (backend Railway)
Random Forest Random Forest
-0,1124 -0,1338
1,64 2,59
2,06 3,04
Chronological split tanpa total harga
Linear Regression
-0,0025
1,57
1,99
Chronological split tanpa total harga
Random Forest
-0,0831
1,65
2,06
Makna R² Negatif Makna R² Negatif
R² negatif berarti model lebih buruk dibandingkan metode sederhana yang selalu memprediksi nilai rata-rata permintaan. Hasil ini menunjukkan fitur yang tersedia belum mengandung pola permintaan yang cukup kuat setelah total harga dihapus. R² negatif berarti model lebih buruk dibandingkan metode sederhana yang selalu memprediksi nilai rata-rata permintaan. Hasil ini menunjukkan fitur yang tersedia belum mengandung pola permintaan yang cukup kuat setelah total harga dihapus.
@ -385,19 +375,19 @@ Bandingkan model dengan baseline sederhana seperti rata-rata atau nilai periode
Lakukan tuning parameter Random Forest hanya menggunakan data training. Lakukan tuning parameter Random Forest hanya menggunakan data training.
Laporkan R², MAE, dan RMSE secara bersamaan. Laporkan R², MAE, dan RMSE secara bersamaan.
10.3 Konsistensi Aplikasi Flutter 10.3 Konsistensi Aplikasi Flutter
Python menggunakan kode hari 0 sampai 6, sedangkan DateTime.weekday pada Flutter menggunakan 1 sampai 7. Nilai harus disamakan sebelum dikirim ke model. Selain itu, pemetaan encoded produk dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training. Python menggunakan kode hari 0 sampai 6 melalui dt.dayofweek (Senin=0), sedangkan DateTime.weekday pada Flutter menggunakan 1 sampai 7 (Senin=1). Nilai harus dipetakan secara konsisten sebelum dikirim ke backend Railway. Selain itu, pemetaan encoded produk dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training di backend Flask.
11. Contoh Penjelasan untuk Laporan 11. Contoh Penjelasan untuk Laporan
11.1 Paragraf Metode Pengujian 11.1 Paragraf Metode Pengujian
Contoh Penulisan Contoh Penulisan
Pengujian model dilakukan menggunakan metode hold-out dengan membagi dataset menjadi 80% data training dan 20% data testing. Dari total 6.742 data transaksi, sebanyak 5.393 data digunakan untuk melatih model dan 1.349 data digunakan untuk menguji performa model. Pembagian data menggunakan random_state sebesar 42 agar hasil pembagian dapat direproduksi. Evaluasi dilakukan menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error. Pengujian model dilakukan menggunakan metode chronological split dengan membagi dataset bersih menjadi 80% data training dan 20% data testing. Dari total 6.742 data transaksi (6.741 data bersih setelah pembersihan 1 missing value), sebanyak 5.392 data digunakan sebagai training set (periode awal 2021-01-01 s/d 2024-12-28) dan 1.349 data digunakan sebagai testing set (periode akhir 2024-12-28 s/d 2025-12-31). Pembagian data diurutkan secara kronologis berdasarkan tanggal transaksi untuk mengevaluasi kemampuan peramalan model secara realistis. Evaluasi dilakukan menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.
11.2 Paragraf Hasil Perbandingan 11.2 Paragraf Hasil Perbandingan
Contoh Penulisan Contoh Penulisan
Berdasarkan pengujian awal, Linear Regression menghasilkan R² sebesar 0,7813, MAE sebesar 0,63, dan RMSE sebesar 0,91. Random Forest menghasilkan R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE sebesar 0,12. Secara numerik, Random Forest memberikan hasil prediksi yang lebih dekat terhadap nilai aktual pada testing set dibandingkan Linear Regression. Berdasarkan hasil pengujian model tanpa data leakage, Linear Regression menghasilkan R² sebesar -0,0039, MAE sebesar 2,49, dan RMSE sebesar 2,86. Random Forest menghasilkan R² sebesar -0,1338, MAE sebesar 2,59, dan RMSE sebesar 3,04. Secara numerik, model Linear Regression memberikan kesalahan rata-rata (MAE) yang sedikit lebih rendah dibandingkan Random Forest pada data testing.
11.3 Paragraf Keterbatasan 11.3 Paragraf Keterbatasan
Contoh Penulisan Contoh Penulisan
Evaluasi lanjutan menunjukkan bahwa nilai Random Forest yang sangat tinggi dipengaruhi oleh penggunaan fitur total_harga_update. Fitur tersebut dihitung dari harga_satuan_update dikalikan jumlah_permintaan_bahan yang menjadi target model. Kondisi ini menimbulkan data leakage karena fitur mengandung informasi langsung mengenai target. Oleh karena itu, hasil R² sebesar 0,9964 harus dipahami sebagai performa pada konfigurasi dataset awal dan belum sepenuhnya menggambarkan kemampuan prediksi permintaan masa depan. Evaluasi model ini dilakukan setelah mengeluarkan fitur total_harga_update untuk menghindari data leakage. Pada pengujian awal sebelum fitur tersebut dikeluarkan, model Random Forest sempat memperoleh R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE sebesar 0,12. Namun, nilai tersebut tidak valid untuk menggambarkan kemampuan prediksi masa depan karena fitur total harga dihitung langsung dari target (jumlah permintaan). Setelah perbaikan metodologi dengan menghapus fitur total harga, diperoleh hasil pengujian aktual yang lebih realistis dan aman dari data leakage.
11.4 Paragraf Rekomendasi Pengembangan 11.4 Paragraf Rekomendasi Pengembangan
Contoh Penulisan Contoh Penulisan
@ -405,15 +395,15 @@ Pengembangan berikutnya perlu menghapus fitur total harga dan menggunakan fitur
12. Contoh Jawaban Saat Sidang 12. Contoh Jawaban Saat Sidang
12.1 Bagaimana Proses Testing Dilakukan? 12.1 Bagaimana Proses Testing Dilakukan?
Jawaban: Proses testing dilakukan dengan membagi 6.742 data menjadi 80% data training dan 20% data testing. Model mempelajari pola dari 5.393 data training, kemudian menghasilkan prediksi untuk 1.349 data testing yang tidak digunakan saat pelatihan. Hasil prediksi dibandingkan dengan nilai aktual menggunakan R², MAE, dan RMSE. Jawaban: Proses testing dilakukan dengan membagi 6.742 data transaksi (dengan 1 baris dieliminasi karena mengandung missing value sehingga tersisa 6.741 data bersih) menjadi 80% data training (5.392 data, periode awal) dan 20% data testing (1.349 data, periode akhir) secara kronologis berdasarkan urutan tanggal. Model mempelajari pola dari data training untuk kemudian memprediksi data testing. Hasil prediksi dievaluasi menggunakan R², MAE, dan RMSE.
12.2 Mengapa Menggunakan Tiga Metrik? 12.2 Mengapa Menggunakan Tiga Metrik?
Jawaban: R² digunakan untuk mengukur seberapa besar variasi target dapat dijelaskan model. MAE menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap. Jawaban: R² digunakan untuk mengukur seberapa besar variasi target dapat dijelaskan model. MAE menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap.
12.3 Mengapa Random Forest Lebih Baik? 12.3 Mengapa Hasil Evaluasi R² Bernilai Negatif?
Jawaban: Pada konfigurasi testing awal, Random Forest lebih baik karena mampu menangkap hubungan non-linear dan menggabungkan prediksi dari 100 Decision Tree. Namun, performa yang sangat tinggi juga disebabkan total harga yang memiliki hubungan langsung dengan target. Jawaban: Nilai R² negatif pada data testing (Linear Regression: -0,0039, Random Forest: -0,1338) menunjukkan bahwa model kesulitan untuk memprediksi data masa depan berdasarkan urutan waktu (chronological split) setelah fitur data leakage (total_harga_update) dihapus. Hal ini mengonfirmasi bahwa fitur waktu kalender saja belum cukup kuat untuk menangkap fluktuasi pola permintaan rill, sehingga performanya di bawah tebakan rata-rata.
12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%? 12.4 Bagaimana Hasil Model yang Di-deploy di Railway?
Jawaban: Tidak sepenuhnya. R² sebesar 0,9964 berarti model menjelaskan sekitar 99,64% variasi target pada testing set. R² bukan akurasi klasifikasi. Selain itu, nilai tersebut dipengaruhi data leakage sehingga belum mewakili kemampuan prediksi masa depan secara valid. Jawaban: Model backend Flask yang di-deploy di Railway adalah model Random Forest yang dilatih tanpa menggunakan fitur total_harga_update untuk memastikan kebebasan dari data leakage. Model ini diakses oleh aplikasi Flutter sebagai frontend melalui API. Model ini memiliki metrik evaluasi R² sebesar -0,1338, MAE sebesar 2,59, dan RMSE sebesar 3,04 pada data testing. Walaupun secara angka lebih rendah dibandingkan model awal dengan leakage, performa ini jauh lebih valid dan dapat diandalkan secara akademis.
12.5 Apakah Model Mengalami Overfitting? 12.5 Apakah Model Mengalami Overfitting Setelah Leakage Dihapus?
Jawaban: Selisih R² training dan testing kecil, sehingga secara angka awal tidak menunjukkan overfitting yang besar. Akan tetapi, terdapat masalah yang lebih penting, yaitu data leakage. Leakage membuat training dan testing sama-sama mudah diprediksi karena keduanya mengandung informasi target. Jawaban: Ya. Setelah fitur total_harga_update dihapus, model Random Forest menunjukkan indikasi overfitting yang sangat kuat di mana R² training bernilai cukup baik (0,6913) tetapi R² testing anjlok menjadi negatif (-0,1338). Ini membuktikan model menghafal pola data training tetapi gagal dalam melakukan generalisasi pada data pengujian baru.
12.6 Apa Perbaikan yang Harus Dilakukan? 12.6 Apa Perbaikan yang Harus Dilakukan?
Jawaban: Total harga harus dihapus dari fitur, kemudian model dilatih menggunakan fitur yang benar-benar tersedia sebelum transaksi terjadi. Testing sebaiknya menggunakan pembagian berdasarkan waktu dan dibandingkan dengan baseline. Fitur histori seperti lag dan moving average juga perlu ditambahkan. Jawaban: Total harga harus dihapus dari fitur, kemudian model dilatih menggunakan fitur yang benar-benar tersedia sebelum transaksi terjadi. Testing sebaiknya menggunakan pembagian berdasarkan waktu dan dibandingkan dengan baseline. Fitur histori seperti lag dan moving average juga perlu ditambahkan.
Lampiran: Ringkasan Rumus dan Istilah Lampiran: Ringkasan Rumus dan Istilah

View File

@ -56,6 +56,13 @@ logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# Initialize global variables for ML model components to satisfy IDE static analysis
model = None
encoders = {}
feature_columns = []
metadata = {}
# ============================================================================ # ============================================================================
# DATABASE CONFIGURATION # DATABASE CONFIGURATION
# ============================================================================ # ============================================================================
@ -852,13 +859,14 @@ def normalize_prediction_payload(data: dict) -> dict:
# LOAD MODELS AT STARTUP # LOAD MODELS AT STARTUP
# ============================================================================ # ============================================================================
try: try:
model = joblib.load('model_prediksi.pkl') _model_dir = os.path.dirname(__file__)
encoders = joblib.load('encoders.pkl') model = joblib.load(os.path.join(_model_dir, 'model_prediksi.pkl'))
feature_columns = joblib.load('feature_columns.pkl') encoders = joblib.load(os.path.join(_model_dir, 'encoders.pkl'))
metadata = joblib.load('model_metadata.pkl') feature_columns = joblib.load(os.path.join(_model_dir, 'feature_columns.pkl'))
metadata = joblib.load(os.path.join(_model_dir, 'model_metadata.pkl'))
logger.info("Models loaded successfully") logger.info("Models loaded successfully")
logger.info(f"Model Type: {metadata['model_type']}") logger.info(f"Model Type: {metadata['model_type']}")
logger.info(f"R² Score: {metadata['r2_score']:.4f}") logger.info(f"R2 Score: {metadata['r2_score']:.4f}")
except Exception as e: except Exception as e:
logger.error(f"Failed to load models: {e}") logger.error(f"Failed to load models: {e}")
raise raise
@ -2809,7 +2817,7 @@ if __name__ == '__main__':
logger.info("Starting Prediksi Stok API") logger.info("Starting Prediksi Stok API")
logger.info("=" * 80) logger.info("=" * 80)
logger.info(f"Model: {metadata['model_type']}") logger.info(f"Model: {metadata['model_type']}")
logger.info(f"Accuracy (R²): {metadata['r2_score']:.4f}") logger.info(f"Accuracy (R2): {metadata['r2_score']:.4f}")
logger.info(f"Features: {len(feature_columns)}") logger.info(f"Features: {len(feature_columns)}")
logger.info("Endpoints: /health, /metadata, /info, /prediksi, /batch-prediksi, /products, /transactions, /predictions, /recipes") logger.info("Endpoints: /health, /metadata, /info, /prediksi, /batch-prediksi, /products, /transactions, /predictions, /recipes")
logger.info("Access API at: http://localhost:5000") logger.info("Access API at: http://localhost:5000")

View File

@ -5,9 +5,9 @@ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
from docx.oxml import OxmlElement from docx.oxml import OxmlElement
from docx.oxml.ns import qn from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor from docx.shared import Inches, Pt, RGBColor
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
OUTPUT = "ml_model/Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx"
NAVY = "17365D" NAVY = "17365D"
BLUE = "2E74B5" BLUE = "2E74B5"
@ -301,8 +301,8 @@ def add_cover(doc):
doc, doc,
["Informasi", "Keterangan"], ["Informasi", "Keterangan"],
[ [
["Jumlah data", "6.742 transaksi"], ["Jumlah data", "6.742 transaksi (6.741 data bersih)"],
["Pembagian data", "80% training dan 20% testing"], ["Pembagian data", "80% training dan 20% testing secara kronologis"],
["Target", "jumlah_permintaan_bahan"], ["Target", "jumlah_permintaan_bahan"],
["Model yang dibandingkan", "Linear Regression dan Random Forest Regressor"], ["Model yang dibandingkan", "Linear Regression dan Random Forest Regressor"],
["Tujuan dokumen", "Bahan belajar, pembahasan laporan, dan persiapan sidang"], ["Tujuan dokumen", "Bahan belajar, pembahasan laporan, dan persiapan sidang"],
@ -329,8 +329,9 @@ def build_document():
add_body( add_body(
doc, doc,
"Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest " "Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest "
"dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi dan dibagi " "dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi, dengan 1 baris "
"menjadi 5.393 data training serta 1.349 data testing menggunakan rasio 80:20." "memiliki missing value pada produk_encoded sehingga menghasilkan 6.741 data bersih. Data tersebut "
"dibagi secara kronologis menjadi 5.392 data training serta 1.349 data testing menggunakan rasio 80:20."
) )
add_table( add_table(
doc, doc,
@ -409,11 +410,11 @@ def build_document():
doc, doc,
["Komponen", "Nilai"], ["Komponen", "Nilai"],
[ [
["Total data", "6.742 transaksi"], ["Total data", "6.742 transaksi (6.741 data bersih)"],
["Training set", "5.393 transaksi atau 80%"], ["Training set", "5.392 transaksi atau 80% (periode 2021-01-01 s/d 2024-12-28)"],
["Testing set", "1.349 transaksi atau 20%"], ["Testing set", "1.349 transaksi atau 20% (periode 2024-12-28 s/d 2025-12-31)"],
["Target", "jumlah_permintaan_bahan"], ["Target", "jumlah_permintaan_bahan"],
["Jumlah fitur model tersimpan", "10 fitur"], ["Jumlah fitur model tersimpan", "9 fitur"],
], ],
widths=[2.3, 4.0], widths=[2.3, 4.0],
) )
@ -422,37 +423,40 @@ def build_document():
doc, doc,
["No.", "Fitur", "Makna"], ["No.", "Fitur", "Makna"],
[ [
["1", "produk_encoded", "Kode angka produk"], ["1", "produk_encoded", "Kode angka produk (diubah menggunakan LabelEncoder)"],
["2", "tahun", "Tahun transaksi"], ["2", "tahun", "Tahun transaksi"],
["3", "bulan", "Bulan transaksi"], ["3", "bulan", "Bulan transaksi"],
["4", "hari", "Tanggal dalam bulan"], ["4", "hari", "Tanggal dalam bulan"],
["5", "hari_dalam_minggu", "Kode hari dalam minggu"], ["5", "hari_dalam_minggu", "Kode hari dalam minggu transaksi"],
["6", "harga_satuan_update", "Harga satuan bahan"], ["6", "harga_satuan_update", "Harga satuan bahan"],
["7", "total_harga_update", "Total harga transaksi"], ["7", "hari_minggu", "Kode hari minggu (dayofweek)"],
["8", "hari_minggu", "Duplikasi informasi hari dalam minggu"], ["8", "nama_produk_encoded", "Nama produk yang telah diubah menggunakan LabelEncoder"],
["9", "nama_produk_encoded", "Nama produk yang telah diubah menjadi angka"], ["9", "kategori_produk_encoded", "Kategori produk yang telah diubah menggunakan LabelEncoder"],
["10", "kategori_produk_encoded", "Kategori produk yang telah diubah menjadi angka"],
], ],
widths=[0.6, 2.3, 3.4], widths=[0.6, 2.3, 3.4],
) )
add_heading(doc, "2.3 Pembagian Training dan Testing", 2) add_heading(doc, "2.3 Pembagian Training dan Testing", 2)
add_code( add_code(
doc, doc,
"X_train, X_test, y_train, y_test = train_test_split(\n" "# Urutkan data secara kronologis\n"
" X, y, test_size=0.2, random_state=42\n" "df = df.sort_values(by='tanggal_transaksi')\n"
")" "# Bagi data dengan rasio 80:20 secara manual\n"
"split_index = int(len(X) * 0.8)\n"
"X_train, X_test = X.iloc[:split_index], X.iloc[split_index:]\n"
"y_train, y_test = y.iloc[:split_index], y.iloc[split_index:]"
) )
add_body( add_body(
doc, doc,
"Parameter test_size=0.2 berarti 20% data digunakan untuk testing. Parameter random_state=42 " "Pembagian data dilakukan secara kronologis berdasarkan urutan tanggal transaksi. Parameter split_index "
"membuat pembagian acak selalu sama ketika program dijalankan ulang, sehingga hasil pengujian " "diambil dari 80% panjang dataset bersih (6.741 data), yaitu int(6741 * 0.8) = 5.392 data training, "
"dapat direproduksi." "dan sisa 1.349 data digunakan sebagai testing. Metode ini memastikan bahwa model diuji untuk memprediksi "
"permintaan di masa depan, bukan sekadar memprediksi data acak di masa lalu."
) )
add_callout( add_callout(
doc, doc,
"Catatan untuk Data Berdasarkan Waktu", "Pemberitahuan Metodologi",
"Karena tujuan sistem adalah memprediksi masa depan, pembagian data secara kronologis lebih kuat " "Pengujian model menggunakan pembagian kronologis (chronological split) sangat penting untuk data deret "
"daripada pembagian acak. Data lama sebaiknya digunakan sebagai training dan data terbaru sebagai testing.", "waktu (time series) guna menghindari data leakage temporal, di mana model tidak sengaja mempelajari data masa depan.",
fill=LIGHT_GOLD, fill=LIGHT_GOLD,
title_color=GOLD, title_color=GOLD,
) )
@ -590,60 +594,60 @@ def build_document():
add_heading(doc, "6. Perhitungan Hasil Testing Proyek", 1) add_heading(doc, "6. Perhitungan Hasil Testing Proyek", 1)
add_body( add_body(
doc, doc,
"Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 4,9622 unit, " "Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 5,4188 unit, "
"sedangkan total variasi aktual atau SST adalah 5.133,0719." "sedangkan total variasi aktual atau SST adalah 10.964,3617."
) )
add_heading(doc, "6.1 Linear Regression", 2) add_heading(doc, "6.1 Linear Regression", 2)
add_table( add_table(
doc, doc,
["Komponen", "Nilai"], ["Komponen", "Nilai"],
[ [
["Jumlah absolute error atau SAE", "848,4592"], ["Jumlah absolute error atau SAE", "3.357,4719"],
["Jumlah squared error atau SSE", "1.122,3551"], ["Jumlah squared error atau SSE", "11.007,5413"],
["Jumlah data testing", "1.349"], ["Jumlah data testing", "1.349"],
["Total variasi aktual atau SST", "5.133,0719"], ["Total variasi aktual atau SST", "10.964,3617"],
], ],
widths=[3.4, 2.9], widths=[3.4, 2.9],
) )
add_formula(doc, "MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63") add_formula(doc, "MAE = 3.357,4719 / 1.349 = 2,4889 ≈ 2,49")
add_formula(doc, "RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91") add_formula(doc, "RMSE = √(11.007,5413 / 1.349) = 2,8565 ≈ 2,86")
add_formula(doc, "R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813") add_formula(doc, "R² = 1 - (11.007,5413 / 10.964,3617) = -0,0039")
add_body( add_body(
doc, doc,
"Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set " "Interpretasinya, Linear Regression tidak mampu menjelaskan variasi target pada testing set secara optimal "
"dan rata-rata prediksinya meleset sekitar 0,63 unit." "(R² bernilai negatif sebesar -0,0039 atau -0,39%) dan rata-rata prediksinya meleset sekitar 2,49 unit."
) )
add_heading(doc, "6.2 Random Forest", 2) add_heading(doc, "6.2 Random Forest", 2)
add_table( add_table(
doc, doc,
["Komponen", "Nilai"], ["Komponen", "Nilai"],
[ [
["Jumlah absolute error atau SAE", "39,86"], ["Jumlah absolute error atau SAE", "3.494,5478"],
["Jumlah squared error atau SSE", "18,7142"], ["Jumlah squared error atau SSE", "12.431,3712"],
["Jumlah data testing", "1.349"], ["Jumlah data testing", "1.349"],
["Total variasi aktual atau SST", "5.133,0719"], ["Total variasi aktual atau SST", "10.964,3617"],
], ],
widths=[3.4, 2.9], widths=[3.4, 2.9],
) )
add_formula(doc, "MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03") add_formula(doc, "MAE = 3.494,5478 / 1.349 = 2,5905 ≈ 2,59")
add_formula(doc, "RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12") add_formula(doc, "RMSE = √(12.431,3712 / 1.349) = 3,0357 ≈ 3,04")
add_formula(doc, "R² = 1 - (18,7142 / 5.133,0719) = 0,9964") add_formula(doc, "R² = 1 - (12.431,3712 / 10.964,3617) = -0,1338")
add_body( add_body(
doc, doc,
"Interpretasinya, Random Forest menjelaskan sekitar 99,64% variasi target pada testing set " "Interpretasinya, Random Forest tidak mampu menjelaskan variasi target pada testing set secara optimal "
"dan rata-rata prediksinya meleset sekitar 0,03 unit. Angka ini harus dibaca bersama analisis " "(R² bernilai negatif sebesar -0,1338 atau -13,38%) dan rata-rata prediksinya meleset sekitar 2,59 unit "
"data leakage pada bagian berikutnya." "setelah fitur total_harga_update yang menimbulkan data leakage dihapus."
) )
add_heading(doc, "6.3 Contoh Hasil Prediksi Individual", 2) add_heading(doc, "6.3 Contoh Hasil Prediksi Individual", 2)
add_table( add_table(
doc, doc,
["Aktual", "Prediksi Linear Regression", "Prediksi Random Forest", "Error Absolut RF"], ["Aktual", "Prediksi Linear Regression", "Prediksi Random Forest", "Error Absolut RF"],
[ [
["4", "3,5472", "4,01", "0,01"], ["7", "5,4795", "3,71", "3,29"],
["3", "2,5369", "3,00", "0,00"], ["5", "5,5121", "4,99", "0,01"],
["4", "4,4879", "4,00", "0,00"], ["10", "5,5587", "5,04", "4,96"],
["2", "0,7336", "2,00", "0,00"], ["1", "5,4686", "5,67", "4,67"],
["7", "7,2195", "7,00", "0,00"], ["2", "5,4883", "5,64", "3,64"],
], ],
widths=[0.9, 2.0, 2.0, 1.4], widths=[0.9, 2.0, 2.0, 1.4],
font_size=9, font_size=9,
@ -654,38 +658,39 @@ def build_document():
doc, doc,
["Aspek", "Linear Regression", "Random Forest"], ["Aspek", "Linear Regression", "Random Forest"],
[ [
["R² testing", "0,7813", "0,9964"], ["R² testing", "-0,0039", "-0,1338"],
["MAE testing", "0,63 unit", "0,03 unit"], ["MAE testing", "2,49 unit", "2,59 unit"],
["RMSE testing", "0,91 unit", "0,12 unit"], ["RMSE testing", "2,86 unit", "3,04 unit"],
["Kemampuan pola non-linear", "Terbatas", "Baik"], ["Kemampuan pola non-linear", "Terbatas", "Baik (pada data training)"],
["Kemudahan interpretasi", "Sangat mudah", "Lebih kompleks"], ["Kemudahan interpretasi", "Sangat mudah", "Lebih kompleks"],
["Risiko pada hasil proyek", "Terpengaruh leakage", "Sangat terpengaruh leakage"], ["Ketergantungan data leakage", "Bebas leakage (total harga dihapus)", "Bebas leakage (total harga dihapus)"],
], ],
widths=[2.2, 2.05, 2.05], widths=[2.2, 2.05, 2.05],
) )
add_heading(doc, "7.1 Perbandingan MAE", 2) add_heading(doc, "7.1 Perbandingan MAE", 2)
add_formula(doc, "Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3%") add_formula(doc, "Peningkatan MAE = [(2,5905 - 2,4889) / 2,4889] x 100% = 4,08%")
add_body( add_body(
doc, doc,
"Pernyataan yang tepat adalah Random Forest mengurangi MAE sekitar 95,3% dibandingkan Linear " "Pernyataan yang tepat adalah model Random Forest menghasilkan kesalahan rata-rata yang sedikit "
"Regression pada test set saat ini. Pernyataan tersebut tidak sama dengan mengatakan model " "lebih tinggi (+4,08%) dibandingkan Linear Regression pada test set aktual tanpa leakage."
"95,3% lebih akurat."
) )
add_heading(doc, "7.2 Indikasi Overfitting", 2) add_heading(doc, "7.2 Indikasi Overfitting", 2)
add_table( add_table(
doc, doc,
["Model", "R² Training", "R² Testing", "Selisih"], ["Model", "R² Training", "R² Testing", "Selisih"],
[ [
["Linear Regression", "0,7902", "0,7813", "0,0089"], ["Linear Regression", "0,0013", "-0,0039", "0,0052"],
["Random Forest", "0,9995", "0,9964", "0,0031"], ["Random Forest", "0,6913", "-0,1338", "0,8251"],
], ],
widths=[2.0, 1.4, 1.4, 1.4], widths=[2.0, 1.4, 1.4, 1.4],
) )
add_body( add_body(
doc, doc,
"Selisih training dan testing terlihat kecil sehingga secara angka awal model tampak stabil. " "Selisih training dan testing pada Linear Regression terlihat sangat kecil, namun kedua nilainya mendekati nol, "
"Namun, kestabilan ini tidak membatalkan masalah data leakage karena informasi target tersedia " "menunjukkan underfitting. Sedangkan pada Random Forest, terdapat selisih R² yang sangat besar (0,8251) "
"pada kedua kelompok data melalui total harga." "antara training (0,6913) dan testing (-0,1338). Hal ini menunjukkan indikasi overfitting yang sangat kuat "
"pada Random Forest setelah total harga dihapus dari fitur. Model berkinerja cukup baik pada data training "
"namun gagal memprediksi data testing baru yang belum pernah dilihat sebelumnya."
) )
add_heading(doc, "8. Analisis Data Leakage", 1) add_heading(doc, "8. Analisis Data Leakage", 1)
@ -742,10 +747,8 @@ def build_document():
doc, doc,
["Skenario", "Model", "", "MAE", "RMSE"], ["Skenario", "Model", "", "MAE", "RMSE"],
[ [
["Random split tanpa total harga", "Linear Regression", "-0,0045", "1,53", "1,96"], ["Chronological split tanpa total harga (backend Railway)", "Linear Regression", "-0,0039", "2,49", "2,86"],
["Random split tanpa total harga", "Random Forest", "-0,1124", "1,64", "2,06"], ["Chronological split tanpa total harga (backend Railway)", "Random Forest", "-0,1338", "2,59", "3,04"],
["Chronological split tanpa total harga", "Linear Regression", "-0,0025", "1,57", "1,99"],
["Chronological split tanpa total harga", "Random Forest", "-0,0831", "1,65", "2,06"],
], ],
widths=[2.3, 1.6, 0.8, 0.8, 0.8], widths=[2.3, 1.6, 0.8, 0.8, 0.8],
font_size=8.8, font_size=8.8,
@ -790,9 +793,9 @@ def build_document():
add_heading(doc, "10.3 Konsistensi Aplikasi Flutter", 2) add_heading(doc, "10.3 Konsistensi Aplikasi Flutter", 2)
add_body( add_body(
doc, doc,
"Python menggunakan kode hari 0 sampai 6, sedangkan DateTime.weekday pada Flutter menggunakan " "Python menggunakan kode hari 0 sampai 6 melalui dt.dayofweek (Senin=0), sedangkan DateTime.weekday pada Flutter menggunakan "
"1 sampai 7. Nilai harus disamakan sebelum dikirim ke model. Selain itu, pemetaan encoded produk " "1 sampai 7 (Senin=1). Nilai harus dipetakan secara konsisten sebelum dikirim ke backend Railway. Selain itu, pemetaan encoded produk "
"dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training." "dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training di backend Flask."
) )
add_heading(doc, "11. Contoh Penjelasan untuk Laporan", 1) add_heading(doc, "11. Contoh Penjelasan untuk Laporan", 1)
@ -800,31 +803,31 @@ def build_document():
add_callout( add_callout(
doc, doc,
"Contoh Penulisan", "Contoh Penulisan",
"Pengujian model dilakukan menggunakan metode hold-out dengan membagi dataset menjadi 80% data " "Pengujian model dilakukan menggunakan metode chronological split dengan membagi dataset bersih "
"training dan 20% data testing. Dari total 6.742 data transaksi, sebanyak 5.393 data digunakan " "menjadi 80% data training dan 20% data testing. Dari total 6.742 data transaksi (6.741 data bersih setelah pembersihan 1 missing value), sebanyak 5.392 data digunakan "
"untuk melatih model dan 1.349 data digunakan untuk menguji performa model. Pembagian data " "sebagai training set (periode awal 2021-01-01 s/d 2024-12-28) dan 1.349 data digunakan sebagai testing set "
"menggunakan random_state sebesar 42 agar hasil pembagian dapat direproduksi. Evaluasi dilakukan " "(periode akhir 2024-12-28 s/d 2025-12-31). Pembagian data diurutkan secara kronologis berdasarkan tanggal transaksi "
"menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.", "untuk mengevaluasi kemampuan peramalan model secara realistis. Evaluasi dilakukan menggunakan "
"koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.",
) )
add_heading(doc, "11.2 Paragraf Hasil Perbandingan", 2) add_heading(doc, "11.2 Paragraf Hasil Perbandingan", 2)
add_callout( add_callout(
doc, doc,
"Contoh Penulisan", "Contoh Penulisan",
"Berdasarkan pengujian awal, Linear Regression menghasilkan R² sebesar 0,7813, MAE sebesar 0,63, " "Berdasarkan hasil pengujian model tanpa data leakage, Linear Regression menghasilkan R² sebesar -0,0039, "
"dan RMSE sebesar 0,91. Random Forest menghasilkan R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE " "MAE sebesar 2,49, dan RMSE sebesar 2,86. Random Forest menghasilkan R² sebesar -0,1338, MAE sebesar 2,59, "
"sebesar 0,12. Secara numerik, Random Forest memberikan hasil prediksi yang lebih dekat terhadap " "dan RMSE sebesar 3,04. Secara numerik, model Linear Regression memberikan kesalahan rata-rata (MAE) "
"nilai aktual pada testing set dibandingkan Linear Regression.", "yang sedikit lebih rendah dibandingkan Random Forest pada data testing.",
) )
add_heading(doc, "11.3 Paragraf Keterbatasan", 2) add_heading(doc, "11.3 Paragraf Keterbatasan", 2)
add_callout( add_callout(
doc, doc,
"Contoh Penulisan", "Contoh Penulisan",
"Evaluasi lanjutan menunjukkan bahwa nilai Random Forest yang sangat tinggi dipengaruhi oleh " "Evaluasi model ini dilakukan setelah mengeluarkan fitur total_harga_update untuk menghindari data leakage. "
"penggunaan fitur total_harga_update. Fitur tersebut dihitung dari harga_satuan_update dikalikan " "Pada pengujian awal sebelum fitur tersebut dikeluarkan, model Random Forest sempat memperoleh R² sebesar 0,9964, "
"jumlah_permintaan_bahan yang menjadi target model. Kondisi ini menimbulkan data leakage karena " "MAE sebesar 0,03, dan RMSE sebesar 0,12. Namun, nilai tersebut tidak valid untuk menggambarkan kemampuan "
"fitur mengandung informasi langsung mengenai target. Oleh karena itu, hasil R² sebesar 0,9964 " "prediksi masa depan karena fitur total harga dihitung langsung dari target (jumlah permintaan). Setelah perbaikan "
"harus dipahami sebagai performa pada konfigurasi dataset awal dan belum sepenuhnya menggambarkan " "metodologi dengan menghapus fitur total harga, diperoleh hasil pengujian aktual yang lebih realistis dan aman dari data leakage.",
"kemampuan prediksi permintaan masa depan.",
fill=LIGHT_GOLD, fill=LIGHT_GOLD,
title_color=GOLD, title_color=GOLD,
) )
@ -844,10 +847,11 @@ def build_document():
add_heading(doc, "12.1 Bagaimana Proses Testing Dilakukan?", 2) add_heading(doc, "12.1 Bagaimana Proses Testing Dilakukan?", 2)
add_body( add_body(
doc, doc,
"Jawaban: Proses testing dilakukan dengan membagi 6.742 data menjadi 80% data training dan 20% " "Jawaban: Proses testing dilakukan dengan membagi 6.742 data transaksi (dengan 1 baris dieliminasi karena "
"data testing. Model mempelajari pola dari 5.393 data training, kemudian menghasilkan prediksi " "mengandung missing value sehingga tersisa 6.741 data bersih) menjadi 80% data training (5.392 data, periode "
"untuk 1.349 data testing yang tidak digunakan saat pelatihan. Hasil prediksi dibandingkan dengan " "awal) dan 20% data testing (1.349 data, periode akhir) secara kronologis berdasarkan urutan tanggal. Model "
"nilai aktual menggunakan R², MAE, dan RMSE." "mempelajari pola dari data training untuk kemudian memprediksi data testing. Hasil prediksi "
"dievaluasi menggunakan R², MAE, dan RMSE."
) )
add_heading(doc, "12.2 Mengapa Menggunakan Tiga Metrik?", 2) add_heading(doc, "12.2 Mengapa Menggunakan Tiga Metrik?", 2)
add_body( add_body(
@ -856,26 +860,28 @@ def build_document():
"menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan " "menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan "
"penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap." "penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap."
) )
add_heading(doc, "12.3 Mengapa Random Forest Lebih Baik?", 2) add_heading(doc, "12.3 Mengapa Hasil Evaluasi R² Bernilai Negatif?", 2)
add_body( add_body(
doc, doc,
"Jawaban: Pada konfigurasi testing awal, Random Forest lebih baik karena mampu menangkap hubungan " "Jawaban: Nilai R² negatif pada data testing (Linear Regression: -0,0039, Random Forest: -0,1338) menunjukkan "
"non-linear dan menggabungkan prediksi dari 100 Decision Tree. Namun, performa yang sangat tinggi " "bahwa model kesulitan untuk memprediksi data masa depan berdasarkan urutan waktu (chronological split) setelah "
"juga disebabkan total harga yang memiliki hubungan langsung dengan target." "fitur data leakage (total_harga_update) dihapus. Hal ini mengonfirmasi bahwa fitur waktu kalender saja belum cukup "
"kuat untuk menangkap fluktuasi pola permintaan rill, sehingga performanya di bawah tebakan rata-rata."
) )
add_heading(doc, "12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%?", 2) add_heading(doc, "12.4 Bagaimana Hasil Model yang Di-deploy di Railway?", 2)
add_body( add_body(
doc, doc,
"Jawaban: Tidak sepenuhnya. R² sebesar 0,9964 berarti model menjelaskan sekitar 99,64% variasi " "Jawaban: Model backend Flask yang di-deploy di Railway adalah model Random Forest yang dilatih tanpa menggunakan fitur total_harga_update "
"target pada testing set. R² bukan akurasi klasifikasi. Selain itu, nilai tersebut dipengaruhi " "untuk memastikan kebebasan dari data leakage. Model ini diakses oleh aplikasi Flutter sebagai frontend melalui API. Model ini memiliki metrik evaluasi R² sebesar -0,1338, MAE sebesar 2,59, "
"data leakage sehingga belum mewakili kemampuan prediksi masa depan secara valid." "dan RMSE sebesar 3,04 pada data testing. Walaupun secara angka lebih rendah dibandingkan model awal dengan leakage, "
"performa ini jauh lebih valid dan dapat diandalkan secara akademis."
) )
add_heading(doc, "12.5 Apakah Model Mengalami Overfitting?", 2) add_heading(doc, "12.5 Apakah Model Mengalami Overfitting Setelah Leakage Dihapus?", 2)
add_body( add_body(
doc, doc,
"Jawaban: Selisih R² training dan testing kecil, sehingga secara angka awal tidak menunjukkan " "Jawaban: Ya. Setelah fitur total_harga_update dihapus, model Random Forest menunjukkan indikasi overfitting yang sangat kuat "
"overfitting yang besar. Akan tetapi, terdapat masalah yang lebih penting, yaitu data leakage. " "di mana R² training bernilai cukup baik (0,6913) tetapi R² testing anjlok menjadi negatif (-0,1338). Ini membuktikan model "
"Leakage membuat training dan testing sama-sama mudah diprediksi karena keduanya mengandung informasi target." "menghafal pola data training tetapi gagal dalam melakukan generalisasi pada data pengujian baru."
) )
add_heading(doc, "12.6 Apa Perbaikan yang Harus Dilakukan?", 2) add_heading(doc, "12.6 Apa Perbaikan yang Harus Dilakukan?", 2)
add_body( add_body(
@ -916,8 +922,22 @@ def build_document():
title_color=GREEN, title_color=GREEN,
) )
doc.save(OUTPUT) path1 = os.path.join(script_dir, 'Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx')
print(OUTPUT) path2 = os.path.join(os.path.dirname(script_dir), 'Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx')
try:
doc.save(path1)
print(f"Saved: {path1}")
except Exception as e:
print(f"Error saving to {path1}: {e}")
try:
doc.save(path2)
print(f"Saved: {path2}")
except Exception as e:
backup_path = os.path.join(os.path.dirname(script_dir), 'Panduan_Pengujian_Linear_Regression_dan_Random_Forest_Project.docx')
doc.save(backup_path)
print(f"Warning: File {path2} is locked. Saved as backup: {backup_path}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -19,14 +19,15 @@ DB_CONFIG = {
# Products list # Products list
PRODUCTS = [ PRODUCTS = [
{'name': 'Tepung Terigu 1kg', 'category': 'Tepung'}, {'name': 'Tepung Terigu 1kg', 'category': 'Tepung', 'price': 12000, 'current_stock': 4.000, 'min_stock': 10.000},
{'name': 'Telur 1kg', 'category': 'Telur'}, {'name': 'Telur 1kg', 'category': 'Telur', 'price': 25000, 'current_stock': 21.200, 'min_stock': 5.000},
{'name': 'Gula Pasir 1kg', 'category': 'Gula'}, {'name': 'Gula Pasir 1kg', 'category': 'Gula', 'price': 14000, 'current_stock': 18.200, 'min_stock': 8.000},
{'name': 'Susu Bubuk', 'category': 'Susu'}, {'name': 'Mentega 500gr', 'category': 'Mentega', 'price': 10000, 'current_stock': 10.600, 'min_stock': 5.000},
{'name': 'Cokelat Bubuk 250gr', 'category': 'Cokelat'}, {'name': 'Susu Bubuk', 'category': 'Susu', 'price': 20000, 'current_stock': 14.000, 'min_stock': 4.000},
{'name': 'Mentega 500gr', 'category': 'Mentega'}, {'name': 'Ragi', 'category': 'Bahan Tambahan', 'price': 5000, 'current_stock': 10.000, 'min_stock': 0.000},
{'name': 'Keju Parut 250gr', 'category': 'Keju'}, {'name': 'Baking Powder', 'category': 'Bahan Tambahan', 'price': 5000, 'current_stock': 9.960, 'min_stock': 2.000},
{'name': 'Baking Powder', 'category': 'Bahan Tambahan'}, {'name': 'Cokelat Bubuk 250gr', 'category': 'Cokelat', 'price': 15000, 'current_stock': 9.000, 'min_stock': 3.000},
{'name': 'Keju Parut 250gr', 'category': 'Keju', 'price': 20000, 'current_stock': 9.000, 'min_stock': 2.000},
] ]
def create_database(): def create_database():
@ -39,12 +40,12 @@ def create_database():
) )
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']}") cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']}")
logger.info(f" Database '{DB_CONFIG['database']}' created successfully") logger.info(f"[OK] Database '{DB_CONFIG['database']}' created successfully")
cursor.close() cursor.close()
connection.close() connection.close()
return True return True
except Error as err: except Error as err:
print(f" Error creating database: {err}") print(f"[ERROR] Error creating database: {err}")
return False return False
def create_tables(): def create_tables():
@ -56,69 +57,88 @@ def create_tables():
# Products table # Products table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS products ( CREATE TABLE IF NOT EXISTS products (
id INT PRIMARY KEY AUTO_INCREMENT, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(50) NOT NULL, category VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) DEFAULT 0, product_type VARCHAR(20) DEFAULT 'Bahan',
stock INT DEFAULT 0, unit VARCHAR(20) DEFAULT 'kg',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP price INT NOT NULL,
) current_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
min_stock DECIMAL(10,3) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""") """)
logger.info("✅ Products table created successfully") logger.info("[OK] Products table created successfully")
# Transactions table # Transactions table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS transactions ( CREATE TABLE IF NOT EXISTS transactions (
id INT PRIMARY KEY AUTO_INCREMENT, id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL, product_name VARCHAR(255) NOT NULL,
category VARCHAR(50) NOT NULL, category VARCHAR(100) NOT NULL,
quantity INT NOT NULL, quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL, unit_price INT NOT NULL,
total_price DECIMAL(10, 2) NOT NULL, total_price INT NOT NULL,
transaction_date DATETIME NOT NULL, transaction_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_name) REFERENCES products(name) FOREIGN KEY (product_name) REFERENCES products(name)
) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""") """)
logger.info(" Transactions table created successfully") logger.info("[OK] Transactions table created successfully")
# Predictions table (FIXED SCHEMA) # Stock Usage History Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS stock_usage_history (
id INT AUTO_INCREMENT PRIMARY KEY,
recipe_name VARCHAR(255),
production_quantity INT,
product_id INT NOT NULL,
product_name VARCHAR(255) NOT NULL,
quantity_used FLOAT NOT NULL,
unit VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info("[OK] Stock Usage History table created successfully")
# Predictions table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS predictions ( CREATE TABLE IF NOT EXISTS predictions (
id INT PRIMARY KEY AUTO_INCREMENT, id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL, product_name VARCHAR(255) NOT NULL,
category VARCHAR(50) NOT NULL, category VARCHAR(100) NOT NULL,
unit_price DECIMAL(10, 2), unit_price INT NOT NULL,
prediction_date DATETIME NOT NULL, prediction_date DATE NOT NULL,
predicted_quantity DECIMAL(10, 2), predicted_quantity INT NOT NULL,
raw_value DECIMAL(10, 2), raw_value FLOAT,
estimated_total_price DECIMAL(10, 2), estimated_total_price INT,
estimated_needs TEXT, estimated_needs TEXT,
accuracy_r2 DECIMAL(5, 4), accuracy_r2 FLOAT,
error_mae DECIMAL(5, 4), error_mae FLOAT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (product_name) REFERENCES products(name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
)
""") """)
logger.info(" Predictions table created successfully") logger.info("[OK] Predictions table created successfully")
# Login table # Login table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS login ( CREATE TABLE IF NOT EXISTS login (
id INT PRIMARY KEY AUTO_INCREMENT, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""") """)
logger.info("Login table created successfully") logger.info("[OK] Login table created successfully")
# Password reset OTP table # Password reset OTP table
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS password_reset_otps ( CREATE TABLE IF NOT EXISTS password_reset_otps (
id INT PRIMARY KEY AUTO_INCREMENT, id INT AUTO_INCREMENT PRIMARY KEY,
login_id INT NOT NULL, login_id INT NOT NULL,
email VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL,
otp_code VARCHAR(10) NOT NULL, otp_code VARCHAR(10) NOT NULL,
@ -126,18 +146,43 @@ def create_tables():
is_used TINYINT(1) DEFAULT 0, is_used TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (login_id) REFERENCES login(id) ON DELETE CASCADE FOREIGN KEY (login_id) REFERENCES login(id) ON DELETE CASCADE
) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""") """)
logger.info("Password reset OTP table created successfully") logger.info("[OK] Password reset OTP table created successfully")
# Recipes table
cursor.execute("""
CREATE TABLE IF NOT EXISTS recipes (
id INT AUTO_INCREMENT PRIMARY KEY,
recipe_name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info("[OK] Recipes table created successfully")
# Recipe Ingredients table
cursor.execute("""
CREATE TABLE IF NOT EXISTS recipe_ingredients (
id INT AUTO_INCREMENT PRIMARY KEY,
recipe_id INT NOT NULL,
product_name VARCHAR(255) NOT NULL,
quantity_needed FLOAT NOT NULL,
unit VARCHAR(50) NOT NULL,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info("[OK] Recipe Ingredients table created successfully")
connection.commit() connection.commit()
cursor.close() cursor.close()
connection.close() connection.close()
return True return True
except Error as err: except Error as err:
print(f"❌ Error creating tables: {err}") print(f"[ERROR] Error creating tables: {err}")
return False return False
def insert_default_products(): def insert_default_products():
"""Insert default products""" """Insert default products"""
try: try:
@ -147,8 +192,8 @@ def insert_default_products():
for product in PRODUCTS: for product in PRODUCTS:
try: try:
cursor.execute( cursor.execute(
"INSERT INTO products (name, category) VALUES (%s, %s)", "INSERT INTO products (name, category, price, current_stock, min_stock) VALUES (%s, %s, %s, %s, %s)",
(product['name'], product['category']) (product['name'], product['category'], product['price'], product.get('current_stock', 0), product.get('min_stock', 0))
) )
except: except:
pass pass
@ -156,12 +201,12 @@ def insert_default_products():
connection.commit() connection.commit()
cursor.execute("SELECT COUNT(*) FROM products") cursor.execute("SELECT COUNT(*) FROM products")
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
logger.info(f" Inserted {count} default products") logger.info(f"[OK] Inserted {count} default products")
cursor.close() cursor.close()
connection.close() connection.close()
return True return True
except Error as err: except Error as err:
print(f" Error inserting products: {err}") print(f"[ERROR] Error inserting products: {err}")
return False return False
def insert_default_login(): def insert_default_login():
@ -180,12 +225,12 @@ def insert_default_login():
""", ('Ibu Sulastri', 'sulastri.aritanto10@gmail.com', 'admin', 'password')) """, ('Ibu Sulastri', 'sulastri.aritanto10@gmail.com', 'admin', 'password'))
connection.commit() connection.commit()
logger.info("Default login account ready") logger.info("[OK] Default login account ready")
cursor.close() cursor.close()
connection.close() connection.close()
return True return True
except Error as err: except Error as err:
print(f"Error inserting login account: {err}") print(f"[ERROR] Error inserting login account: {err}")
return False return False
def verify_connection(): def verify_connection():
@ -195,12 +240,12 @@ def verify_connection():
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("SELECT COUNT(*) FROM products") cursor.execute("SELECT COUNT(*) FROM products")
count = cursor.fetchone()[0] count = cursor.fetchone()[0]
logger.info(f" Database connected! Found {count} products") logger.info(f"[OK] Database connected! Found {count} products")
cursor.close() cursor.close()
connection.close() connection.close()
return True return True
except Error as err: except Error as err:
print(f" Connection error: {err}") print(f"[ERROR] Connection error: {err}")
return False return False
def main(): def main():

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,8 +6,11 @@ from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import LabelEncoder
import joblib import joblib
import warnings import warnings
import os
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
script_dir = os.path.dirname(os.path.abspath(__file__))
# ============================================================================ # ============================================================================
# LOAD AND PREPARE DATA # LOAD AND PREPARE DATA
# ============================================================================ # ============================================================================
@ -15,7 +18,7 @@ print("=" * 80)
print("MODEL TESTING - LINEAR REGRESSION vs RANDOM FOREST") print("MODEL TESTING - LINEAR REGRESSION vs RANDOM FOREST")
print("=" * 80) print("=" * 80)
file_path = 'dataset_prediksi_permintaan_bahan_2021_2025_update_tanggal.xlsx' file_path = os.path.join(script_dir, 'dataset_prediksi_permintaan_bahan_2021_2025_update_tanggal.xlsx')
try: try:
df = pd.read_excel(file_path) df = pd.read_excel(file_path)
@ -261,7 +264,7 @@ print("[INFO] Linear Regression hanya dipakai sebagai pembanding evaluasi.")
# ============================================================================ # ============================================================================
# SAVE RESULTS # SAVE RESULTS
# ============================================================================ # ============================================================================
results_file = open('model_testing_results.txt', 'w', encoding='utf-8') results_file = open(os.path.join(script_dir, 'model_testing_results.txt'), 'w', encoding='utf-8')
results_file.write("=" * 80 + "\n") results_file.write("=" * 80 + "\n")
results_file.write("HASIL TESTING MODEL - PREDIKSI PERMINTAAN STOK BAHAN KUE\n") results_file.write("HASIL TESTING MODEL - PREDIKSI PERMINTAAN STOK BAHAN KUE\n")
results_file.write("=" * 80 + "\n\n") results_file.write("=" * 80 + "\n\n")
@ -290,7 +293,7 @@ results_file.write(f" RMSE: {test_rmse_rf:.2f}\n\n")
results_file.close() results_file.close()
print("\n[OK] Hasil disimpan ke: model_testing_results.txt") print(f"\n[OK] Hasil disimpan ke: {os.path.join(script_dir, 'model_testing_results.txt')}")
# ============================================================================ # ============================================================================
# SAVE BEST MODEL # SAVE BEST MODEL
@ -299,15 +302,15 @@ print("\n" + "=" * 80)
print("MENYIMPAN MODEL TERBAIK") print("MENYIMPAN MODEL TERBAIK")
print("=" * 80) print("=" * 80)
joblib.dump(rf_model, 'model_prediksi.pkl') joblib.dump(rf_model, os.path.join(script_dir, 'model_prediksi.pkl'))
print("\n[OK] Model Random Forest disimpan: model_prediksi.pkl") print("\n[OK] Model Random Forest disimpan: model_prediksi.pkl")
# Save encoders # Save encoders
joblib.dump(encoders, 'encoders.pkl') joblib.dump(encoders, os.path.join(script_dir, 'encoders.pkl'))
print("[OK] Encoders disimpan: encoders.pkl") print("[OK] Encoders disimpan: encoders.pkl")
# Save feature columns # Save feature columns
joblib.dump(feature_cols, 'feature_columns.pkl') joblib.dump(feature_cols, os.path.join(script_dir, 'feature_columns.pkl'))
print("[OK] Feature columns disimpan: feature_columns.pkl") print("[OK] Feature columns disimpan: feature_columns.pkl")
# Save metadata # Save metadata
@ -324,7 +327,7 @@ metadata = {
'total_samples': len(df) 'total_samples': len(df)
} }
joblib.dump(metadata, 'model_metadata.pkl') joblib.dump(metadata, os.path.join(script_dir, 'model_metadata.pkl'))
print("[OK] Metadata disimpan: model_metadata.pkl") print("[OK] Metadata disimpan: model_metadata.pkl")
print("\nFile yang telah disimpan:") print("\nFile yang telah disimpan:")

View File

@ -7,3 +7,8 @@ scikit-learn==1.8.0
numpy==2.4.4 numpy==2.4.4
scipy==1.17.1 scipy==1.17.1
gunicorn==21.2.0 gunicorn==21.2.0
openpyxl==3.1.5
python-docx==1.2.0
python-dotenv==1.2.2

View File

@ -1,5 +1,5 @@
import re import re
import os
import mysql.connector import mysql.connector
from cleanup_dummy_data import db_config from cleanup_dummy_data import db_config
@ -155,7 +155,9 @@ def cleanup_to_backend_recipe_data(cursor):
def main(): def main():
with open('prediksi_stok_db.sql', 'r', encoding='utf-8') as sql_file: script_dir = os.path.dirname(os.path.abspath(__file__))
sql_path = os.path.join(script_dir, 'prediksi_stok_db.sql')
with open(sql_path, 'r', encoding='utf-8') as sql_file:
sql_text = sql_file.read() sql_text = sql_file.read()
connection = mysql.connector.connect(**db_config()) connection = mysql.connector.connect(**db_config())

View File

@ -1,50 +1,86 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Simple backend connectivity test Simple backend connectivity test using built-in urllib (dependency-free)
Run: python test_backend.py Run: python test_backend.py
""" """
import requests
import json import json
from datetime import datetime from datetime import datetime
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
# Change this to your backend URL # Change this to your backend URL
BACKEND_URL = "http://192.168.1.2:5000" BACKEND_URL = "http://127.0.0.1:5000" # Default to localhost for easier local checks
def make_request(url, method="GET", data=None, headers=None, timeout=5):
if headers is None:
headers = {}
req_data = None
if data is not None:
req_data = json.dumps(data).encode('utf-8')
if 'Content-Type' not in headers:
headers['Content-Type'] = 'application/json'
req = Request(url, data=req_data, headers=headers, method=method)
try:
with urlopen(req, timeout=timeout) as response:
status_code = response.status
body = response.read().decode('utf-8')
try:
json_data = json.loads(body)
except json.JSONDecodeError:
json_data = body
return status_code, json_data
except HTTPError as e:
body = e.read().decode('utf-8', errors='replace')
try:
json_data = json.loads(body)
except json.JSONDecodeError:
json_data = body
return e.code, json_data
except URLError as e:
raise RuntimeError(f"Connection failed: {e.reason}")
def test_health(): def test_health():
"""Test if backend is running""" """Test if backend is running"""
try: try:
print("\n[TEST 1] Health Check") print("\n[TEST 1] Health Check")
print(f"URL: {BACKEND_URL}/health") url = f"{BACKEND_URL}/health"
response = requests.get(f"{BACKEND_URL}/health", timeout=5) print(f"URL: {url}")
print(f"Status: {response.status_code}") status, data = make_request(url)
print(f"Response: {response.json()}") print(f"Status: {status}")
return response.status_code == 200 print(f"Response: {data}")
return status == 200
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"[ERROR] Error: {e}")
return False return False
def test_get_products(): def test_get_products():
"""Test get products endpoint""" """Test get products endpoint"""
try: try:
print("\n[TEST 2] Get Products") print("\n[TEST 2] Get Products")
print(f"URL: {BACKEND_URL}/products") url = f"{BACKEND_URL}/products"
response = requests.get(f"{BACKEND_URL}/products", timeout=5) print(f"URL: {url}")
print(f"Status: {response.status_code}") status, data = make_request(url)
data = response.json() print(f"Status: {status}")
print(f"Total products: {data.get('total', 0)}") if isinstance(data, dict):
if data.get('products'): print(f"Total products: {data.get('total', 0)}")
print(f"First product: {data['products'][0]}") if data.get('products'):
return response.status_code == 200 print(f"First product: {data['products'][0]}")
else:
print(f"Response: {data}")
return status == 200
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"[ERROR] Error: {e}")
return False return False
def test_create_product(): def test_create_product():
"""Test create product endpoint""" """Test create product endpoint"""
try: try:
print("\n[TEST 3] Create Product") print("\n[TEST 3] Create Product")
print(f"URL: {BACKEND_URL}/products") url = f"{BACKEND_URL}/products"
print(f"URL: {url}")
payload = { payload = {
"name": f"Test Produk {datetime.now().strftime('%H%M%S')}", "name": f"Test Produk {datetime.now().strftime('%H%M%S')}",
@ -54,17 +90,12 @@ def test_create_product():
} }
print(f"Payload: {json.dumps(payload, indent=2)}") print(f"Payload: {json.dumps(payload, indent=2)}")
response = requests.post( status, data = make_request(url, method="POST", data=payload)
f"{BACKEND_URL}/products", print(f"Status: {status}")
json=payload, print(f"Response: {data}")
headers={"Content-Type": "application/json"}, return status in [201, 409]
timeout=5
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
return response.status_code in [201, 409]
except Exception as e: except Exception as e:
print(f" Error: {e}") print(f"[ERROR] Error: {e}")
return False return False
if __name__ == "__main__": if __name__ == "__main__":
@ -81,13 +112,13 @@ if __name__ == "__main__":
print("\n" + "="*80) print("\n" + "="*80)
print("SUMMARY:") print("SUMMARY:")
for test_name, result in tests: for test_name, result in tests:
status = "✅ PASS" if result else " FAIL" status = "[OK] PASS" if result else "[X] FAIL"
print(f" {status}: {test_name}") print(f" {status}: {test_name}")
print("="*80) print("="*80)
all_passed = all(result for _, result in tests) all_passed = all(result for _, result in tests)
if all_passed: if all_passed:
print("\n Backend is working correctly!") print("\n[OK] Backend is working correctly!")
else: else:
print("\n Some tests failed. Check your backend connection.") print("\n[X] Some tests failed. Check your backend connection.")