ArizalMuluk commited on
Commit
222ee23
·
verified ·
1 Parent(s): b0e8ab4

Upload 12 files

Browse files
Files changed (2) hide show
  1. app/forecaster.py +13 -35
  2. app/main.py +24 -64
app/forecaster.py CHANGED
@@ -1,15 +1,6 @@
1
  """
2
- Future Forecaster
3
- Mendukung dua pola prediksi:
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 = "ID",
46
- skip_n1 : bool = True,
47
  ) -> dict:
48
  """
49
- Prediksi 1 bulan ke depan.
50
- skip_n1=True → prediksi N+2 (laporan N+1 belum masuk)
51
- skip_n1=False → prediksi N+1 (data sudah tersedia)
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 = df_prod["Date"].max()
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 = "ID",
137
- top_n : int = None,
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, skip_n1)
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+2)
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 masa depan.
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, skip_n1)
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, skip_n1=skip_n1
366
  )
367
 
368
- # Bangun response dengan chart gabungan
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
- # Keterangan dinamis
383
- if f["skip_n1"]:
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" : "success",
411
- "skip_n1" : skip_n1,
412
- "pattern" : pola_str,
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 future."""
428
  import plotly.graph_objects as go
429
 
430
- fig = go.Figure()
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
- if skip_n1:
443
- # Pola N+2: garis putus melewati skip_month (gap)
444
- fig.add_trace(go.Scatter(
445
- x=[last_label, skip_month, target_month],
446
- y=[last_qty, None, prediksi],
447
- mode="lines+markers",
448
- name=f"Prediksi {target_month}",
449
- line=dict(color="darkorange", width=2.5, dash="dot"),
450
- marker=dict(symbol=["circle","x","star"], size=[0,10,14],
451
- color=["darkorange","gray","darkorange"]),
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>",