from flask import Flask, jsonify, request import pandas as pd import numpy as np from prophet import Prophet app = Flask(__name__) # Nilai scale hasil pengujian grid search pada data pangkalan. # Scale 0.4 dipilih karena menghasilkan nilai MAPE terkecil dibanding nilai scale lainnya. # Nilai ini merepresentasikan bahwa setelah regulasi Oktober 2025, rata-rata penjualan # pangkalan menjadi 40% dari kondisi sebelum regulasi. PENGETATAN_SCALE = 0.4 # Threshold penurunan untuk mendeteksi apakah pangkalan terdampak pengetatan. # Nilai 0.4 (40%) diperoleh dari data aktual pangkalan Eka yang menunjukkan # penurunan sebesar 40% dari September 2025 (358 tabung) ke Oktober 2025 (215 tabung) # akibat regulasi pengetatan distribusi LPG dari agen. PENGETATAN_THRESHOLD = 0.4 @app.route("/") def home(): return jsonify({"status": "API PeGas Aktif", "mode": "Adaptive Local Scaling"}) @app.route("/predict", methods=['POST']) def predict(): try: data = request.json # 1. AMBIL PARAMETER DARI LARAVEL days = int(data.get('days', 30)) # jumlah hari prediksi (default 30) user_data_raw = data.get('user_data', []) # data dari Laravel # 2. PREPROCESSING DATA df_user = pd.DataFrame(user_data_raw) df_user['ds'] = pd.to_datetime(df_user['tanggal']) # ubah ke datetime df_user['y'] = df_user['jumlah'] # target df_user = df_user[['ds', 'y']].sort_values('ds') # urutkan berdasarkan tanggal # 3. DETEKSI APAKAH PANGKALAN TERDAMPAK PENGETATAN # Hitung rata-rata penjualan sebelum dan sesudah Oktober 2025 before = df_user[df_user['ds'] < '2025-10-01']['y'].mean() # rata-rata sebelum Oktober 2025 after = df_user[df_user['ds'] >= '2025-10-01']['y'].mean() # rata-rata setelah Oktober 2025 # Cek apakah data lengkap dan terjadi penurunan signifikan # Pangkalan dianggap terdampak jika penurunan setelah Oktober 2025 # mencapai 40% atau lebih dari kondisi sebelumnya, sesuai data aktual pangkalan. # Jika data sebelum atau sesudah Oktober 2025 tidak tersedia (NaN), # atau before bernilai 0 (menghindari pembagian nol), maka dianggap tidak terdampak. if pd.isna(before) or pd.isna(after) or before == 0: terdampak = False # data tidak lengkap, anggap kondisi normal else: penurunan = (before - after) / before # hitung persentase penurunan terdampak = penurunan >= PENGETATAN_THRESHOLD # terdampak jika turun >= 40% print(f">>> Penurunan: {round(penurunan * 100, 1) if not pd.isna(before) and before != 0 else 'N/A'}%") print(f">>> Terdampak pengetatan: {terdampak}") # 4. BUAT REGRESSOR PENGETATAN # Penambahan variabel pengetatan_level sebagai regressor tambahan untuk # memberitahu model bahwa terdapat perubahan kondisi distribusi sejak Oktober 2025. # Nilai 1.0 merepresentasikan kondisi normal sebelum Oktober 2025. # Nilai PENGETATAN_SCALE (0.4) hanya diterapkan jika pangkalan terdeteksi terdampak, # konsisten dengan nilai yang digunakan pada saat pelatihan dan evaluasi model. df_user['pengetatan_level'] = 1.0 # default kondisi normal if terdampak: df_user.loc[df_user['ds'] >= '2025-10-01', 'pengetatan_level'] = PENGETATAN_SCALE print(f">>> Scale pengetatan diterapkan: {PENGETATAN_SCALE}") else: print(f">>> Pangkalan tidak terdampak pengetatan, scale tidak diterapkan") # 5. MODEL PROPHET # Konfigurasi seasonality disesuaikan dengan karakteristik data penjualan LPG: # - weekly_seasonality diaktifkan karena pola penjualan dipengaruhi jadwal pengiriman mingguan model = Prophet( yearly_seasonality=False, weekly_seasonality=True, daily_seasonality=False ) # Tambahkan regressor pengetatan agar model mengetahui adanya perubahan kondisi eksternal. # Regressor harus ditambahkan sebelum model.fit(). model.add_regressor('pengetatan_level') # 6. TRAIN MODEL model.fit(df_user) # latih model menggunakan seluruh data historis pengguna # 7. FUTURE DATAFRAME future = model.make_future_dataframe(periods=days) # buat daftar tanggal yang akan diprediksi # Isi nilai regressor untuk tanggal masa depan. # Nilai pengetatan_level harus diketahui untuk seluruh tanggal prediksi, # sesuai dengan ketentuan penggunaan additional regressor pada Prophet. future['pengetatan_level'] = 1.0 # default kondisi normal if terdampak: future.loc[future['ds'] >= '2025-10-01', 'pengetatan_level'] = PENGETATAN_SCALE # 8. PREDIKSI forecast = model.predict(future) # prediksi masa depan dengan mempertimbangkan kondisi pengetatan # 9. HARI MINGGU = 0 (ATURAN BISNIS NYATA / agen libur pengiriman) # Setelah prediksi dihasilkan, dilakukan koreksi bahwa hari Minggu bernilai 0 # karena agen tidak melakukan pengiriman pada hari tersebut. forecast['ds_dt'] = pd.to_datetime(forecast['ds']) is_sunday = forecast['ds_dt'].dt.dayofweek == 6 # 6 = hari Minggu forecast.loc[is_sunday, ['yhat', 'yhat_upper', 'yhat_lower']] = 0 # 10. AMBIL HASIL AKHIR result = forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(days).copy() # 11. POST-PROCESSING sebelum dikirim ke Laravel # Tidak boleh negatif karena penjualan LPG tidak mungkin bernilai minus result['yhat'] = result['yhat'].clip(lower=0) result['yhat_lower'] = result['yhat_lower'].clip(lower=0) result['yhat_upper'] = result['yhat_upper'].clip(lower=0) # Pembulatan karena satuan adalah tabung LPG yang tidak mungkin bernilai desimal result['yhat'] = result['yhat'].round(0).astype(int) result['yhat_lower'] = result['yhat_lower'].round(0).astype(int) result['yhat_upper'] = result['yhat_upper'].round(0).astype(int) # Format tanggal ke string agar bisa dikirim sebagai JSON ke Laravel result['ds'] = result['ds'].dt.strftime('%Y-%m-%d') # Kirim hasil prediksi ke Laravel dalam format JSON return jsonify(result.to_dict(orient="records")) except Exception as e: print(f"❌ Error pada server Flask: {str(e)}") return jsonify({"error": str(e)}), 500 if __name__ == "__main__": app.run(debug=True, port=5000)