137 lines
6.4 KiB
Python
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) |