pegas/web/flask/app.py

137 lines
6.4 KiB
Python

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)