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>
<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>
<!-- 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>

View File

@ -682,6 +682,9 @@ class _PredictionScreenState extends State<PredictionScreen> {
label: 'Permintaan',
value:
'${_controller.predictedDemand ?? 0}',
infoTitle: 'Permintaan (Prediksi)',
infoText:
'Perkiraan jumlah produk yang akan dibeli pelanggan berdasarkan riwayat penjualan toko.',
),
),
const SizedBox(width: 10),
@ -690,6 +693,9 @@ class _PredictionScreenState extends State<PredictionScreen> {
label: 'Produksi',
value:
'${_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
.predictionR2!
.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),
@ -720,10 +735,63 @@ class _PredictionScreenState extends State<PredictionScreen> {
: _controller
.predictionMae!
.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 {
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 value;
final String? infoTitle;
final String? infoText;
@override
Widget build(BuildContext context) {
@ -1465,9 +1540,117 @@ class _PredictionMetric extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 11, color: Color(0xFF6B7280)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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),
Text(

View File

@ -4,9 +4,9 @@
Informasi
Keterangan
Jumlah data
6.742 transaksi
6.742 transaksi (6.741 data bersih)
Pembagian data
80% training dan 20% testing
80% training dan 20% testing secara kronologis
Target
jumlah_permintaan_bahan
Model yang dibandingkan
@ -16,7 +16,7 @@ Tujuan dokumen
Disusun berdasarkan hasil pengujian model pada proyek tugas akhir
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
R² Testing
MAE
@ -72,15 +72,15 @@ Membandingkan serta mengevaluasi validitas hasil.
Komponen
Nilai
Total data
6.742 transaksi
6.742 transaksi (6.741 data bersih)
Training set
5.393 transaksi atau 80%
5.392 transaksi atau 80% (periode 2021-01-01 s/d 2024-12-28)
Testing set
1.349 transaksi atau 20%
1.349 transaksi atau 20% (periode 2024-12-28 s/d 2025-12-31)
Target
jumlah_permintaan_bahan
Jumlah fitur model tersimpan
10 fitur
9 fitur
2.2 Fitur Model
No.
@ -88,7 +88,7 @@ Jumlah fitur model tersimpan
Makna
1
produk_encoded
Kode angka produk
Kode angka produk (diubah menggunakan LabelEncoder)
2
tahun
Tahun transaksi
@ -100,31 +100,31 @@ Jumlah fitur model tersimpan
Tanggal dalam bulan
5
hari_dalam_minggu
Kode hari dalam minggu
Kode hari dalam minggu transaksi
6
harga_satuan_update
Harga satuan bahan
7
total_harga_update
Total harga transaksi
8
hari_minggu
Duplikasi informasi hari dalam minggu
9
Kode hari minggu (dayofweek)
8
nama_produk_encoded
Nama produk yang telah diubah menjadi angka
10
Nama produk yang telah diubah menggunakan LabelEncoder
9
kategori_produk_encoded
Kategori produk yang telah diubah menjadi angka
Kategori produk yang telah diubah menggunakan LabelEncoder
2.3 Pembagian Training dan Testing
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Urutkan data secara kronologis
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.
Catatan untuk Data Berdasarkan Waktu
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.
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.
Pemberitahuan Metodologi
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
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
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
Komponen
Nilai
Jumlah absolute error atau SAE
848,4592
3.357,4719
Jumlah squared error atau SSE
1.122,3551
11.007,5413
Jumlah data testing
1.349
Total variasi aktual atau SST
5.133,0719
10.964,3617
MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63
RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91
R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813
Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set dan rata-rata prediksinya meleset sekitar 0,63 unit.
MAE = 3.357,4719 / 1.349 = 2,4889 ≈ 2,49
RMSE = √(11.007,5413 / 1.349) = 2,8565 ≈ 2,86
R² = 1 - (11.007,5413 / 10.964,3617) = -0,0039
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
Komponen
Nilai
Jumlah absolute error atau SAE
39,86
3.494,5478
Jumlah squared error atau SSE
18,7142
12.431,3712
Jumlah data testing
1.349
Total variasi aktual atau SST
5.133,0719
10.964,3617
MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03
RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12
R² = 1 - (18,7142 / 5.133,0719) = 0,9964
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.
MAE = 3.494,5478 / 1.349 = 2,5905 ≈ 2,59
RMSE = √(12.431,3712 / 1.349) = 3,0357 ≈ 3,04
R² = 1 - (12.431,3712 / 10.964,3617) = -0,1338
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
Aktual
Prediksi Linear Regression
Prediksi Random Forest
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,2195
7,00
0,00
5,4795
3,71
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
Aspek
Linear Regression
Random Forest
R² testing
0,7813
0,9964
-0,0039
-0,1338
MAE testing
0,63 unit
0,03 unit
2,49 unit
2,59 unit
RMSE testing
0,91 unit
0,12 unit
2,86 unit
3,04 unit
Kemampuan pola non-linear
Terbatas
Baik
Baik (pada data training)
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)
7.1 Perbandingan MAE
Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3%
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.
Peningkatan MAE = [(2,5905 - 2,4889) / 2,4889] x 100% = 4,08%
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
Model
R² Training
R² Testing
Selisih
Linear Regression
0,7902
0,7813
0,0089
0,0013
-0,0039
0,0052
Random Forest
0,9995
0,9964
0,0031
0,6913
-0,1338
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
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
@ -344,26 +344,16 @@ Untuk mengukur kemampuan prediksi yang lebih realistis, total_harga_update dihap
MAE
RMSE
Random split tanpa total harga
Chronological split tanpa total harga (backend Railway)
Linear Regression
-0,0045
1,53
1,96
Random split tanpa total harga
-0,0039
2,49
2,86
Chronological split tanpa total harga (backend Railway)
Random Forest
-0,1124
1,64
2,06
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
-0,1338
2,59
3,04
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.
@ -385,19 +375,19 @@ Bandingkan model dengan baseline sederhana seperti rata-rata atau nilai periode
Lakukan tuning parameter Random Forest hanya menggunakan data training.
Laporkan R², MAE, dan RMSE secara bersamaan.
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.1 Paragraf Metode Pengujian
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
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
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
Contoh Penulisan
@ -405,15 +395,15 @@ Pengembangan berikutnya perlu menghapus fitur total harga dan menggunakan fitur
12. Contoh Jawaban Saat Sidang
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?
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?
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.
12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%?
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.
12.5 Apakah Model Mengalami Overfitting?
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.
12.3 Mengapa Hasil Evaluasi R² Bernilai Negatif?
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 Bagaimana Hasil Model yang Di-deploy di Railway?
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 Setelah Leakage Dihapus?
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?
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

View File

@ -56,6 +56,13 @@ logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# Initialize global variables for ML model components to satisfy IDE static analysis
model = None
encoders = {}
feature_columns = []
metadata = {}
# ============================================================================
# DATABASE CONFIGURATION
# ============================================================================
@ -852,13 +859,14 @@ def normalize_prediction_payload(data: dict) -> dict:
# LOAD MODELS AT STARTUP
# ============================================================================
try:
model = joblib.load('model_prediksi.pkl')
encoders = joblib.load('encoders.pkl')
feature_columns = joblib.load('feature_columns.pkl')
metadata = joblib.load('model_metadata.pkl')
_model_dir = os.path.dirname(__file__)
model = joblib.load(os.path.join(_model_dir, 'model_prediksi.pkl'))
encoders = joblib.load(os.path.join(_model_dir, 'encoders.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(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:
logger.error(f"Failed to load models: {e}")
raise
@ -2809,7 +2817,7 @@ if __name__ == '__main__':
logger.info("Starting Prediksi Stok API")
logger.info("=" * 80)
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("Endpoints: /health, /metadata, /info, /prediksi, /batch-prediksi, /products, /transactions, /predictions, /recipes")
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.ns import qn
from docx.shared import Inches, Pt, RGBColor
import os
OUTPUT = "ml_model/Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx"
script_dir = os.path.dirname(os.path.abspath(__file__))
NAVY = "17365D"
BLUE = "2E74B5"
@ -301,8 +301,8 @@ def add_cover(doc):
doc,
["Informasi", "Keterangan"],
[
["Jumlah data", "6.742 transaksi"],
["Pembagian data", "80% training dan 20% testing"],
["Jumlah data", "6.742 transaksi (6.741 data bersih)"],
["Pembagian data", "80% training dan 20% testing secara kronologis"],
["Target", "jumlah_permintaan_bahan"],
["Model yang dibandingkan", "Linear Regression dan Random Forest Regressor"],
["Tujuan dokumen", "Bahan belajar, pembahasan laporan, dan persiapan sidang"],
@ -329,8 +329,9 @@ def build_document():
add_body(
doc,
"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."
"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."
)
add_table(
doc,
@ -409,11 +410,11 @@ def build_document():
doc,
["Komponen", "Nilai"],
[
["Total data", "6.742 transaksi"],
["Training set", "5.393 transaksi atau 80%"],
["Testing set", "1.349 transaksi atau 20%"],
["Total data", "6.742 transaksi (6.741 data bersih)"],
["Training set", "5.392 transaksi atau 80% (periode 2021-01-01 s/d 2024-12-28)"],
["Testing set", "1.349 transaksi atau 20% (periode 2024-12-28 s/d 2025-12-31)"],
["Target", "jumlah_permintaan_bahan"],
["Jumlah fitur model tersimpan", "10 fitur"],
["Jumlah fitur model tersimpan", "9 fitur"],
],
widths=[2.3, 4.0],
)
@ -422,37 +423,40 @@ def build_document():
doc,
["No.", "Fitur", "Makna"],
[
["1", "produk_encoded", "Kode angka produk"],
["1", "produk_encoded", "Kode angka produk (diubah menggunakan LabelEncoder)"],
["2", "tahun", "Tahun transaksi"],
["3", "bulan", "Bulan transaksi"],
["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"],
["7", "total_harga_update", "Total harga transaksi"],
["8", "hari_minggu", "Duplikasi informasi hari dalam minggu"],
["9", "nama_produk_encoded", "Nama produk yang telah diubah menjadi angka"],
["10", "kategori_produk_encoded", "Kategori produk yang telah diubah menjadi angka"],
["7", "hari_minggu", "Kode hari minggu (dayofweek)"],
["8", "nama_produk_encoded", "Nama produk yang telah diubah menggunakan LabelEncoder"],
["9", "kategori_produk_encoded", "Kategori produk yang telah diubah menggunakan LabelEncoder"],
],
widths=[0.6, 2.3, 3.4],
)
add_heading(doc, "2.3 Pembagian Training dan Testing", 2)
add_code(
doc,
"X_train, X_test, y_train, y_test = train_test_split(\n"
" X, y, test_size=0.2, random_state=42\n"
")"
"# Urutkan data secara kronologis\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(
doc,
"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."
)
add_callout(
doc,
"Catatan untuk Data Berdasarkan Waktu",
"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.",
"Pemberitahuan Metodologi",
"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.",
fill=LIGHT_GOLD,
title_color=GOLD,
)
@ -590,60 +594,60 @@ def build_document():
add_heading(doc, "6. Perhitungan Hasil Testing Proyek", 1)
add_body(
doc,
"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."
)
add_heading(doc, "6.1 Linear Regression", 2)
add_table(
doc,
["Komponen", "Nilai"],
[
["Jumlah absolute error atau SAE", "848,4592"],
["Jumlah squared error atau SSE", "1.122,3551"],
["Jumlah absolute error atau SAE", "3.357,4719"],
["Jumlah squared error atau SSE", "11.007,5413"],
["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],
)
add_formula(doc, "MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63")
add_formula(doc, "RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91")
add_formula(doc, "R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813")
add_formula(doc, "MAE = 3.357,4719 / 1.349 = 2,4889 ≈ 2,49")
add_formula(doc, "RMSE = √(11.007,5413 / 1.349) = 2,8565 ≈ 2,86")
add_formula(doc, "R² = 1 - (11.007,5413 / 10.964,3617) = -0,0039")
add_body(
doc,
"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."
)
add_heading(doc, "6.2 Random Forest", 2)
add_table(
doc,
["Komponen", "Nilai"],
[
["Jumlah absolute error atau SAE", "39,86"],
["Jumlah squared error atau SSE", "18,7142"],
["Jumlah absolute error atau SAE", "3.494,5478"],
["Jumlah squared error atau SSE", "12.431,3712"],
["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],
)
add_formula(doc, "MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03")
add_formula(doc, "RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12")
add_formula(doc, "R² = 1 - (18,7142 / 5.133,0719) = 0,9964")
add_formula(doc, "MAE = 3.494,5478 / 1.349 = 2,5905 ≈ 2,59")
add_formula(doc, "RMSE = √(12.431,3712 / 1.349) = 3,0357 ≈ 3,04")
add_formula(doc, "R² = 1 - (12.431,3712 / 10.964,3617) = -0,1338")
add_body(
doc,
"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."
)
add_heading(doc, "6.3 Contoh Hasil Prediksi Individual", 2)
add_table(
doc,
["Aktual", "Prediksi Linear Regression", "Prediksi Random Forest", "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,2195", "7,00", "0,00"],
["7", "5,4795", "3,71", "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"],
],
widths=[0.9, 2.0, 2.0, 1.4],
font_size=9,
@ -654,38 +658,39 @@ def build_document():
doc,
["Aspek", "Linear Regression", "Random Forest"],
[
["R² testing", "0,7813", "0,9964"],
["MAE testing", "0,63 unit", "0,03 unit"],
["RMSE testing", "0,91 unit", "0,12 unit"],
["Kemampuan pola non-linear", "Terbatas", "Baik"],
["R² testing", "-0,0039", "-0,1338"],
["MAE testing", "2,49 unit", "2,59 unit"],
["RMSE testing", "2,86 unit", "3,04 unit"],
["Kemampuan pola non-linear", "Terbatas", "Baik (pada data training)"],
["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],
)
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(
doc,
"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."
)
add_heading(doc, "7.2 Indikasi Overfitting", 2)
add_table(
doc,
["Model", "R² Training", "R² Testing", "Selisih"],
[
["Linear Regression", "0,7902", "0,7813", "0,0089"],
["Random Forest", "0,9995", "0,9964", "0,0031"],
["Linear Regression", "0,0013", "-0,0039", "0,0052"],
["Random Forest", "0,6913", "-0,1338", "0,8251"],
],
widths=[2.0, 1.4, 1.4, 1.4],
)
add_body(
doc,
"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."
)
add_heading(doc, "8. Analisis Data Leakage", 1)
@ -742,10 +747,8 @@ def build_document():
doc,
["Skenario", "Model", "", "MAE", "RMSE"],
[
["Random split tanpa total harga", "Linear Regression", "-0,0045", "1,53", "1,96"],
["Random split tanpa total harga", "Random Forest", "-0,1124", "1,64", "2,06"],
["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"],
["Chronological split tanpa total harga (backend Railway)", "Linear Regression", "-0,0039", "2,49", "2,86"],
["Chronological split tanpa total harga (backend Railway)", "Random Forest", "-0,1338", "2,59", "3,04"],
],
widths=[2.3, 1.6, 0.8, 0.8, 0.8],
font_size=8.8,
@ -790,9 +793,9 @@ def build_document():
add_heading(doc, "10.3 Konsistensi Aplikasi Flutter", 2)
add_body(
doc,
"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."
)
add_heading(doc, "11. Contoh Penjelasan untuk Laporan", 1)
@ -800,31 +803,31 @@ def build_document():
add_callout(
doc,
"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.",
)
add_heading(doc, "11.2 Paragraf Hasil Perbandingan", 2)
add_callout(
doc,
"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.",
)
add_heading(doc, "11.3 Paragraf Keterbatasan", 2)
add_callout(
doc,
"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.",
fill=LIGHT_GOLD,
title_color=GOLD,
)
@ -844,10 +847,11 @@ def build_document():
add_heading(doc, "12.1 Bagaimana Proses Testing Dilakukan?", 2)
add_body(
doc,
"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."
)
add_heading(doc, "12.2 Mengapa Menggunakan Tiga Metrik?", 2)
add_body(
@ -856,26 +860,28 @@ def build_document():
"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."
)
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(
doc,
"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."
)
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(
doc,
"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."
)
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(
doc,
"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."
)
add_heading(doc, "12.6 Apa Perbaikan yang Harus Dilakukan?", 2)
add_body(
@ -916,8 +922,22 @@ def build_document():
title_color=GREEN,
)
doc.save(OUTPUT)
print(OUTPUT)
path1 = os.path.join(script_dir, 'Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx')
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__":

View File

@ -19,14 +19,15 @@ DB_CONFIG = {
# Products list
PRODUCTS = [
{'name': 'Tepung Terigu 1kg', 'category': 'Tepung'},
{'name': 'Telur 1kg', 'category': 'Telur'},
{'name': 'Gula Pasir 1kg', 'category': 'Gula'},
{'name': 'Susu Bubuk', 'category': 'Susu'},
{'name': 'Cokelat Bubuk 250gr', 'category': 'Cokelat'},
{'name': 'Mentega 500gr', 'category': 'Mentega'},
{'name': 'Keju Parut 250gr', 'category': 'Keju'},
{'name': 'Baking Powder', 'category': 'Bahan Tambahan'},
{'name': 'Tepung Terigu 1kg', 'category': 'Tepung', 'price': 12000, 'current_stock': 4.000, 'min_stock': 10.000},
{'name': 'Telur 1kg', 'category': 'Telur', 'price': 25000, 'current_stock': 21.200, 'min_stock': 5.000},
{'name': 'Gula Pasir 1kg', 'category': 'Gula', 'price': 14000, 'current_stock': 18.200, 'min_stock': 8.000},
{'name': 'Mentega 500gr', 'category': 'Mentega', 'price': 10000, 'current_stock': 10.600, 'min_stock': 5.000},
{'name': 'Susu Bubuk', 'category': 'Susu', 'price': 20000, 'current_stock': 14.000, 'min_stock': 4.000},
{'name': 'Ragi', 'category': 'Bahan Tambahan', 'price': 5000, 'current_stock': 10.000, 'min_stock': 0.000},
{'name': 'Baking Powder', 'category': 'Bahan Tambahan', 'price': 5000, 'current_stock': 9.960, 'min_stock': 2.000},
{'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():
@ -39,12 +40,12 @@ def create_database():
)
cursor = connection.cursor()
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()
connection.close()
return True
except Error as err:
print(f" Error creating database: {err}")
print(f"[ERROR] Error creating database: {err}")
return False
def create_tables():
@ -56,69 +57,88 @@ def create_tables():
# Products table
cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
category VARCHAR(50) NOT NULL,
price DECIMAL(10, 2) DEFAULT 0,
stock INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL,
product_type VARCHAR(20) DEFAULT 'Bahan',
unit VARCHAR(20) DEFAULT 'kg',
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
cursor.execute("""
CREATE TABLE IF NOT EXISTS transactions (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL,
id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
total_price DECIMAL(10, 2) NOT NULL,
transaction_date DATETIME NOT NULL,
unit_price INT NOT NULL,
total_price INT NOT NULL,
transaction_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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("""
CREATE TABLE IF NOT EXISTS predictions (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL,
unit_price DECIMAL(10, 2),
prediction_date DATETIME NOT NULL,
predicted_quantity DECIMAL(10, 2),
raw_value DECIMAL(10, 2),
estimated_total_price DECIMAL(10, 2),
id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
unit_price INT NOT NULL,
prediction_date DATE NOT NULL,
predicted_quantity INT NOT NULL,
raw_value FLOAT,
estimated_total_price INT,
estimated_needs TEXT,
accuracy_r2 DECIMAL(5, 4),
error_mae DECIMAL(5, 4),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_name) REFERENCES products(name)
)
accuracy_r2 FLOAT,
error_mae FLOAT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info(" Predictions table created successfully")
logger.info("[OK] Predictions table created successfully")
# Login table
cursor.execute("""
CREATE TABLE IF NOT EXISTS login (
id INT PRIMARY KEY AUTO_INCREMENT,
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
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
cursor.execute("""
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,
email VARCHAR(100) NOT NULL,
otp_code VARCHAR(10) NOT NULL,
@ -126,18 +146,43 @@ def create_tables():
is_used TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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()
cursor.close()
connection.close()
return True
except Error as err:
print(f"❌ Error creating tables: {err}")
print(f"[ERROR] Error creating tables: {err}")
return False
def insert_default_products():
"""Insert default products"""
try:
@ -147,8 +192,8 @@ def insert_default_products():
for product in PRODUCTS:
try:
cursor.execute(
"INSERT INTO products (name, category) VALUES (%s, %s)",
(product['name'], product['category'])
"INSERT INTO products (name, category, price, current_stock, min_stock) VALUES (%s, %s, %s, %s, %s)",
(product['name'], product['category'], product['price'], product.get('current_stock', 0), product.get('min_stock', 0))
)
except:
pass
@ -156,12 +201,12 @@ def insert_default_products():
connection.commit()
cursor.execute("SELECT COUNT(*) FROM products")
count = cursor.fetchone()[0]
logger.info(f" Inserted {count} default products")
logger.info(f"[OK] Inserted {count} default products")
cursor.close()
connection.close()
return True
except Error as err:
print(f" Error inserting products: {err}")
print(f"[ERROR] Error inserting products: {err}")
return False
def insert_default_login():
@ -180,12 +225,12 @@ def insert_default_login():
""", ('Ibu Sulastri', 'sulastri.aritanto10@gmail.com', 'admin', 'password'))
connection.commit()
logger.info("Default login account ready")
logger.info("[OK] Default login account ready")
cursor.close()
connection.close()
return True
except Error as err:
print(f"Error inserting login account: {err}")
print(f"[ERROR] Error inserting login account: {err}")
return False
def verify_connection():
@ -195,12 +240,12 @@ def verify_connection():
cursor = connection.cursor()
cursor.execute("SELECT COUNT(*) FROM products")
count = cursor.fetchone()[0]
logger.info(f" Database connected! Found {count} products")
logger.info(f"[OK] Database connected! Found {count} products")
cursor.close()
connection.close()
return True
except Error as err:
print(f" Connection error: {err}")
print(f"[ERROR] Connection error: {err}")
return False
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
import joblib
import warnings
import os
warnings.filterwarnings('ignore')
script_dir = os.path.dirname(os.path.abspath(__file__))
# ============================================================================
# LOAD AND PREPARE DATA
# ============================================================================
@ -15,7 +18,7 @@ print("=" * 80)
print("MODEL TESTING - LINEAR REGRESSION vs RANDOM FOREST")
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:
df = pd.read_excel(file_path)
@ -261,7 +264,7 @@ print("[INFO] Linear Regression hanya dipakai sebagai pembanding evaluasi.")
# ============================================================================
# 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("HASIL TESTING MODEL - PREDIKSI PERMINTAAN STOK BAHAN KUE\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()
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
@ -299,15 +302,15 @@ print("\n" + "=" * 80)
print("MENYIMPAN MODEL TERBAIK")
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")
# Save encoders
joblib.dump(encoders, 'encoders.pkl')
joblib.dump(encoders, os.path.join(script_dir, 'encoders.pkl'))
print("[OK] Encoders disimpan: encoders.pkl")
# 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")
# Save metadata
@ -324,7 +327,7 @@ metadata = {
'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("\nFile yang telah disimpan:")

View File

@ -7,3 +7,8 @@ scikit-learn==1.8.0
numpy==2.4.4
scipy==1.17.1
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 os
import mysql.connector
from cleanup_dummy_data import db_config
@ -155,7 +155,9 @@ def cleanup_to_backend_recipe_data(cursor):
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()
connection = mysql.connector.connect(**db_config())

View File

@ -1,50 +1,86 @@
#!/usr/bin/env python3
"""
Simple backend connectivity test
Simple backend connectivity test using built-in urllib (dependency-free)
Run: python test_backend.py
"""
import requests
import json
from datetime import datetime
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
# 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():
"""Test if backend is running"""
try:
print("\n[TEST 1] Health Check")
print(f"URL: {BACKEND_URL}/health")
response = requests.get(f"{BACKEND_URL}/health", timeout=5)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
return response.status_code == 200
url = f"{BACKEND_URL}/health"
print(f"URL: {url}")
status, data = make_request(url)
print(f"Status: {status}")
print(f"Response: {data}")
return status == 200
except Exception as e:
print(f"❌ Error: {e}")
print(f"[ERROR] Error: {e}")
return False
def test_get_products():
"""Test get products endpoint"""
try:
print("\n[TEST 2] Get Products")
print(f"URL: {BACKEND_URL}/products")
response = requests.get(f"{BACKEND_URL}/products", timeout=5)
print(f"Status: {response.status_code}")
data = response.json()
print(f"Total products: {data.get('total', 0)}")
if data.get('products'):
print(f"First product: {data['products'][0]}")
return response.status_code == 200
url = f"{BACKEND_URL}/products"
print(f"URL: {url}")
status, data = make_request(url)
print(f"Status: {status}")
if isinstance(data, dict):
print(f"Total products: {data.get('total', 0)}")
if data.get('products'):
print(f"First product: {data['products'][0]}")
else:
print(f"Response: {data}")
return status == 200
except Exception as e:
print(f"❌ Error: {e}")
print(f"[ERROR] Error: {e}")
return False
def test_create_product():
"""Test create product endpoint"""
try:
print("\n[TEST 3] Create Product")
print(f"URL: {BACKEND_URL}/products")
url = f"{BACKEND_URL}/products"
print(f"URL: {url}")
payload = {
"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)}")
response = requests.post(
f"{BACKEND_URL}/products",
json=payload,
headers={"Content-Type": "application/json"},
timeout=5
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
return response.status_code in [201, 409]
status, data = make_request(url, method="POST", data=payload)
print(f"Status: {status}")
print(f"Response: {data}")
return status in [201, 409]
except Exception as e:
print(f" Error: {e}")
print(f"[ERROR] Error: {e}")
return False
if __name__ == "__main__":
@ -81,13 +112,13 @@ if __name__ == "__main__":
print("\n" + "="*80)
print("SUMMARY:")
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("="*80)
all_passed = all(result for _, result in tests)
if all_passed:
print("\n Backend is working correctly!")
print("\n[OK] Backend is working correctly!")
else:
print("\n Some tests failed. Check your backend connection.")
print("\n[X] Some tests failed. Check your backend connection.")