Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- app/forecaster.py +13 -35
- app/main.py +24 -64
app/forecaster.py
CHANGED
|
@@ -1,15 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Future Forecaster
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
skip_n1=True (default — laporan belum masuk):
|
| 6 |
-
N = bulan data terakhir
|
| 7 |
-
N+1 = dilewati
|
| 8 |
-
N+2 = bulan yang diprediksi
|
| 9 |
-
|
| 10 |
-
skip_n1=False (data sudah tersedia):
|
| 11 |
-
N = bulan data terakhir
|
| 12 |
-
N+1 = bulan yang diprediksi langsung
|
| 13 |
"""
|
| 14 |
import numpy as np
|
| 15 |
import pandas as pd
|
|
@@ -42,13 +33,12 @@ def forecast_one_product(
|
|
| 42 |
model,
|
| 43 |
le_cat,
|
| 44 |
le_types,
|
| 45 |
-
country_code: str
|
| 46 |
-
skip_n1 : bool = True,
|
| 47 |
) -> dict:
|
| 48 |
"""
|
| 49 |
-
Prediksi 1 bulan ke depan.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
"""
|
| 53 |
df_prod = df_cont[df_cont["Product_ID"] == product_id].copy()
|
| 54 |
df_prod = df_prod.sort_values("Date").reset_index(drop=True)
|
|
@@ -56,16 +46,8 @@ def forecast_one_product(
|
|
| 56 |
if df_prod.empty:
|
| 57 |
return {"error": f"Produk {product_id} tidak ditemukan."}
|
| 58 |
|
| 59 |
-
last_date
|
| 60 |
-
|
| 61 |
-
if skip_n1:
|
| 62 |
-
skip_date = last_date + relativedelta(months=1)
|
| 63 |
-
target_date = last_date + relativedelta(months=2)
|
| 64 |
-
pola_label = "N+2 (laporan N+1 belum masuk)"
|
| 65 |
-
else:
|
| 66 |
-
skip_date = None
|
| 67 |
-
target_date = last_date + relativedelta(months=1)
|
| 68 |
-
pola_label = "N+1 (data sudah tersedia)"
|
| 69 |
|
| 70 |
qty_history = df_prod["Quantity"].values
|
| 71 |
|
|
@@ -113,14 +95,11 @@ def forecast_one_product(
|
|
| 113 |
"category" : category,
|
| 114 |
"types" : types,
|
| 115 |
"last_data" : last_date.strftime("%b %Y"),
|
| 116 |
-
"skip_month" : skip_date.strftime("%b %Y") if skip_date else None,
|
| 117 |
"target_month": target_date.strftime("%b %Y"),
|
| 118 |
"prediksi" : int(prediksi),
|
| 119 |
-
"skip_n1" : skip_n1,
|
| 120 |
-
"pola_label" : pola_label,
|
| 121 |
"fitur_input" : {
|
| 122 |
-
"qty_lag1": qty_lag1, "qty_lag2": qty_lag2,
|
| 123 |
-
"qty_lag3": qty_lag3, "qty_lag12": qty_lag12,
|
| 124 |
"qty_roll3": round(qty_roll3, 2), "is_holiday": is_holiday,
|
| 125 |
},
|
| 126 |
"hist_labels" : hist_labels,
|
|
@@ -133,9 +112,8 @@ def forecast_all_products(
|
|
| 133 |
model,
|
| 134 |
le_cat,
|
| 135 |
le_types,
|
| 136 |
-
country_code: str
|
| 137 |
-
top_n : int
|
| 138 |
-
skip_n1 : bool = True,
|
| 139 |
) -> list:
|
| 140 |
if top_n:
|
| 141 |
products = (
|
|
@@ -148,7 +126,7 @@ def forecast_all_products(
|
|
| 148 |
|
| 149 |
results = []
|
| 150 |
for pid in products:
|
| 151 |
-
r = forecast_one_product(df_cont, pid, model, le_cat, le_types, country_code
|
| 152 |
if "error" not in r:
|
| 153 |
results.append(r)
|
| 154 |
return results
|
|
|
|
| 1 |
"""
|
| 2 |
+
Future Forecaster — N+1 only
|
| 3 |
+
Prediksi 1 bulan ke depan langsung dari data terakhir.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
import numpy as np
|
| 6 |
import pandas as pd
|
|
|
|
| 33 |
model,
|
| 34 |
le_cat,
|
| 35 |
le_types,
|
| 36 |
+
country_code: str = "ID",
|
|
|
|
| 37 |
) -> dict:
|
| 38 |
"""
|
| 39 |
+
Prediksi stok 1 bulan ke depan (N+1) dari data terakhir.
|
| 40 |
+
N = bulan data terakhir
|
| 41 |
+
N+1 = bulan yang diprediksi
|
| 42 |
"""
|
| 43 |
df_prod = df_cont[df_cont["Product_ID"] == product_id].copy()
|
| 44 |
df_prod = df_prod.sort_values("Date").reset_index(drop=True)
|
|
|
|
| 46 |
if df_prod.empty:
|
| 47 |
return {"error": f"Produk {product_id} tidak ditemukan."}
|
| 48 |
|
| 49 |
+
last_date = df_prod["Date"].max()
|
| 50 |
+
target_date = last_date + relativedelta(months=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
qty_history = df_prod["Quantity"].values
|
| 53 |
|
|
|
|
| 95 |
"category" : category,
|
| 96 |
"types" : types,
|
| 97 |
"last_data" : last_date.strftime("%b %Y"),
|
|
|
|
| 98 |
"target_month": target_date.strftime("%b %Y"),
|
| 99 |
"prediksi" : int(prediksi),
|
|
|
|
|
|
|
| 100 |
"fitur_input" : {
|
| 101 |
+
"qty_lag1" : qty_lag1, "qty_lag2": qty_lag2,
|
| 102 |
+
"qty_lag3" : qty_lag3, "qty_lag12": qty_lag12,
|
| 103 |
"qty_roll3": round(qty_roll3, 2), "is_holiday": is_holiday,
|
| 104 |
},
|
| 105 |
"hist_labels" : hist_labels,
|
|
|
|
| 112 |
model,
|
| 113 |
le_cat,
|
| 114 |
le_types,
|
| 115 |
+
country_code: str = "ID",
|
| 116 |
+
top_n : int = None,
|
|
|
|
| 117 |
) -> list:
|
| 118 |
if top_n:
|
| 119 |
products = (
|
|
|
|
| 126 |
|
| 127 |
results = []
|
| 128 |
for pid in products:
|
| 129 |
+
r = forecast_one_product(df_cont, pid, model, le_cat, le_types, country_code)
|
| 130 |
if "error" not in r:
|
| 131 |
results.append(r)
|
| 132 |
return results
|
app/main.py
CHANGED
|
@@ -323,20 +323,15 @@ def list_products(session_id: str):
|
|
| 323 |
|
| 324 |
|
| 325 |
# ─────────────────────────────────────────
|
| 326 |
-
# FORECAST MASA DEPAN (N+
|
| 327 |
# ─────────────────────────────────────────
|
| 328 |
@app.get("/forecast/{session_id}")
|
| 329 |
def forecast(
|
| 330 |
session_id: str,
|
| 331 |
product_id: Optional[str] = None,
|
| 332 |
-
skip_n1 : bool = True,
|
| 333 |
):
|
| 334 |
"""
|
| 335 |
-
Prediksi stok
|
| 336 |
-
|
| 337 |
-
**skip_n1=true** (default): prediksi N+2 — laporan bulan N+1 belum masuk
|
| 338 |
-
**skip_n1=false**: prediksi N+1 — data sudah tersedia langsung
|
| 339 |
-
|
| 340 |
Jika product_id diisi → prediksi 1 produk.
|
| 341 |
Jika kosong → prediksi top 10 produk terlaris.
|
| 342 |
"""
|
|
@@ -356,37 +351,29 @@ def forecast(
|
|
| 356 |
|
| 357 |
# Prediksi
|
| 358 |
if product_id:
|
| 359 |
-
raw = forecast_one_product(df_cont, product_id, model, le_cat, le_types, country_code
|
| 360 |
if "error" in raw:
|
| 361 |
raise HTTPException(status_code=404, detail=raw["error"])
|
| 362 |
forecasts = [raw]
|
| 363 |
else:
|
| 364 |
forecasts = forecast_all_products(
|
| 365 |
-
df_cont, model, le_cat, le_types, country_code, top_n=10
|
| 366 |
)
|
| 367 |
|
| 368 |
-
# Bangun response
|
| 369 |
output = []
|
| 370 |
for f in forecasts:
|
| 371 |
chart = _build_forecast_chart(
|
| 372 |
hist_labels = f["hist_labels"],
|
| 373 |
hist_qty = f["hist_qty"],
|
| 374 |
-
skip_month = f["skip_month"],
|
| 375 |
target_month = f["target_month"],
|
| 376 |
prediksi = f["prediksi"],
|
| 377 |
product_id = f["product_id"],
|
| 378 |
product_name = f["product_name"],
|
| 379 |
-
skip_n1 = f["skip_n1"],
|
| 380 |
)
|
| 381 |
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
ket = (f"Prediksi stok untuk {f['target_month']} "
|
| 385 |
-
f"(data terakhir: {f['last_data']}, "
|
| 386 |
-
f"bulan {f['skip_month']} dilewati — laporan belum masuk)")
|
| 387 |
-
else:
|
| 388 |
-
ket = (f"Prediksi stok untuk {f['target_month']} "
|
| 389 |
-
f"(data terakhir: {f['last_data']}, prediksi langsung N+1)")
|
| 390 |
|
| 391 |
output.append({
|
| 392 |
"product_id" : f["product_id"],
|
|
@@ -395,39 +382,33 @@ def forecast(
|
|
| 395 |
"types" : f["types"],
|
| 396 |
"forecast" : {
|
| 397 |
"last_data" : f["last_data"],
|
| 398 |
-
"skip_month" : f["skip_month"],
|
| 399 |
"target_month": f["target_month"],
|
| 400 |
"prediksi_pcs": f["prediksi"],
|
| 401 |
-
"pola" : f["pola_label"],
|
| 402 |
"keterangan" : ket,
|
| 403 |
},
|
| 404 |
"chart_json" : json.loads(chart),
|
| 405 |
"fitur_input" : f["fitur_input"],
|
| 406 |
})
|
| 407 |
|
| 408 |
-
pola_str = "N → skip N+1 → prediksi N+2" if skip_n1 else "N → prediksi N+1"
|
| 409 |
return {
|
| 410 |
-
"status"
|
| 411 |
-
"
|
| 412 |
-
"
|
| 413 |
-
"results" : output,
|
| 414 |
}
|
| 415 |
|
| 416 |
|
| 417 |
def _build_forecast_chart(
|
| 418 |
hist_labels : list,
|
| 419 |
hist_qty : list,
|
| 420 |
-
skip_month : str,
|
| 421 |
target_month: str,
|
| 422 |
prediksi : int,
|
| 423 |
product_id : str,
|
| 424 |
product_name: str,
|
| 425 |
-
skip_n1 : bool = True,
|
| 426 |
) -> str:
|
| 427 |
-
"""Buat grafik Plotly gabungan historis + prediksi
|
| 428 |
import plotly.graph_objects as go
|
| 429 |
|
| 430 |
-
fig
|
| 431 |
last_label = hist_labels[-1]
|
| 432 |
last_qty = hist_qty[-1]
|
| 433 |
|
|
@@ -439,39 +420,16 @@ def _build_forecast_chart(
|
|
| 439 |
marker=dict(symbol="circle", size=7),
|
| 440 |
))
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
connectgaps=False,
|
| 453 |
-
))
|
| 454 |
-
# Anotasi skip
|
| 455 |
-
fig.add_annotation(
|
| 456 |
-
x=skip_month, y=max(hist_qty) * 0.1 if hist_qty else 0,
|
| 457 |
-
text="⏭ Laporan<br>belum masuk",
|
| 458 |
-
showarrow=False,
|
| 459 |
-
font=dict(size=10, color="#94a3b8"),
|
| 460 |
-
bgcolor="#f8fafc", bordercolor="#e2e8f0",
|
| 461 |
-
)
|
| 462 |
-
subtitle = f"Historis 12 bulan + Prediksi {target_month} (pola N+2)"
|
| 463 |
-
else:
|
| 464 |
-
# Pola N+1: garis langsung ke prediksi tanpa gap
|
| 465 |
-
fig.add_trace(go.Scatter(
|
| 466 |
-
x=[last_label, target_month],
|
| 467 |
-
y=[last_qty, prediksi],
|
| 468 |
-
mode="lines+markers",
|
| 469 |
-
name=f"Prediksi {target_month}",
|
| 470 |
-
line=dict(color="darkorange", width=2.5, dash="dot"),
|
| 471 |
-
marker=dict(symbol=["circle","star"], size=[0,14],
|
| 472 |
-
color=["darkorange","darkorange"]),
|
| 473 |
-
))
|
| 474 |
-
subtitle = f"Historis 12 bulan + Prediksi {target_month} (pola N+1)"
|
| 475 |
|
| 476 |
# Anotasi nilai prediksi
|
| 477 |
fig.add_annotation(
|
|
@@ -483,6 +441,8 @@ def _build_forecast_chart(
|
|
| 483 |
ay=-40,
|
| 484 |
)
|
| 485 |
|
|
|
|
|
|
|
| 486 |
fig.update_layout(
|
| 487 |
title=dict(
|
| 488 |
text=f"Prediksi Stok — {product_id} ({product_name})<br><sup>{subtitle}</sup>",
|
|
|
|
| 323 |
|
| 324 |
|
| 325 |
# ─────────────────────────────────────────
|
| 326 |
+
# FORECAST MASA DEPAN (N+1)
|
| 327 |
# ─────────────────────────────────────────
|
| 328 |
@app.get("/forecast/{session_id}")
|
| 329 |
def forecast(
|
| 330 |
session_id: str,
|
| 331 |
product_id: Optional[str] = None,
|
|
|
|
| 332 |
):
|
| 333 |
"""
|
| 334 |
+
Prediksi stok bulan depan (N+1) langsung dari data terakhir.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
Jika product_id diisi → prediksi 1 produk.
|
| 336 |
Jika kosong → prediksi top 10 produk terlaris.
|
| 337 |
"""
|
|
|
|
| 351 |
|
| 352 |
# Prediksi
|
| 353 |
if product_id:
|
| 354 |
+
raw = forecast_one_product(df_cont, product_id, model, le_cat, le_types, country_code)
|
| 355 |
if "error" in raw:
|
| 356 |
raise HTTPException(status_code=404, detail=raw["error"])
|
| 357 |
forecasts = [raw]
|
| 358 |
else:
|
| 359 |
forecasts = forecast_all_products(
|
| 360 |
+
df_cont, model, le_cat, le_types, country_code, top_n=10
|
| 361 |
)
|
| 362 |
|
| 363 |
+
# Bangun response
|
| 364 |
output = []
|
| 365 |
for f in forecasts:
|
| 366 |
chart = _build_forecast_chart(
|
| 367 |
hist_labels = f["hist_labels"],
|
| 368 |
hist_qty = f["hist_qty"],
|
|
|
|
| 369 |
target_month = f["target_month"],
|
| 370 |
prediksi = f["prediksi"],
|
| 371 |
product_id = f["product_id"],
|
| 372 |
product_name = f["product_name"],
|
|
|
|
| 373 |
)
|
| 374 |
|
| 375 |
+
ket = (f"Prediksi stok untuk {f['target_month']} "
|
| 376 |
+
f"(data terakhir: {f['last_data']})")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
output.append({
|
| 379 |
"product_id" : f["product_id"],
|
|
|
|
| 382 |
"types" : f["types"],
|
| 383 |
"forecast" : {
|
| 384 |
"last_data" : f["last_data"],
|
|
|
|
| 385 |
"target_month": f["target_month"],
|
| 386 |
"prediksi_pcs": f["prediksi"],
|
|
|
|
| 387 |
"keterangan" : ket,
|
| 388 |
},
|
| 389 |
"chart_json" : json.loads(chart),
|
| 390 |
"fitur_input" : f["fitur_input"],
|
| 391 |
})
|
| 392 |
|
|
|
|
| 393 |
return {
|
| 394 |
+
"status" : "success",
|
| 395 |
+
"pattern": "N → prediksi N+1",
|
| 396 |
+
"results": output,
|
|
|
|
| 397 |
}
|
| 398 |
|
| 399 |
|
| 400 |
def _build_forecast_chart(
|
| 401 |
hist_labels : list,
|
| 402 |
hist_qty : list,
|
|
|
|
| 403 |
target_month: str,
|
| 404 |
prediksi : int,
|
| 405 |
product_id : str,
|
| 406 |
product_name: str,
|
|
|
|
| 407 |
) -> str:
|
| 408 |
+
"""Buat grafik Plotly gabungan historis + prediksi N+1."""
|
| 409 |
import plotly.graph_objects as go
|
| 410 |
|
| 411 |
+
fig = go.Figure()
|
| 412 |
last_label = hist_labels[-1]
|
| 413 |
last_qty = hist_qty[-1]
|
| 414 |
|
|
|
|
| 420 |
marker=dict(symbol="circle", size=7),
|
| 421 |
))
|
| 422 |
|
| 423 |
+
# Garis prediksi N+1 — langsung tanpa gap
|
| 424 |
+
fig.add_trace(go.Scatter(
|
| 425 |
+
x=[last_label, target_month],
|
| 426 |
+
y=[last_qty, prediksi],
|
| 427 |
+
mode="lines+markers",
|
| 428 |
+
name=f"Prediksi {target_month}",
|
| 429 |
+
line=dict(color="darkorange", width=2.5, dash="dot"),
|
| 430 |
+
marker=dict(symbol=["circle","star"], size=[0,14],
|
| 431 |
+
color=["darkorange","darkorange"]),
|
| 432 |
+
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
# Anotasi nilai prediksi
|
| 435 |
fig.add_annotation(
|
|
|
|
| 441 |
ay=-40,
|
| 442 |
)
|
| 443 |
|
| 444 |
+
subtitle = f"Historis 12 bulan + Prediksi {target_month} (N+1)"
|
| 445 |
+
|
| 446 |
fig.update_layout(
|
| 447 |
title=dict(
|
| 448 |
text=f"Prediksi Stok — {product_id} ({product_name})<br><sup>{subtitle}</sup>",
|