ALAMDIENG commited on
Commit
0f90396
Β·
1 Parent(s): 7d4bf01

feat: upgrade crowd event modeling to BPS Jumlah Jiwa headcount across 44 Kecamatans

Browse files
Files changed (4) hide show
  1. app.py +81 -60
  2. event_jakarta_2026.txt +32 -32
  3. frontend/app.js +19 -1
  4. frontend/index.html +3 -3
app.py CHANGED
@@ -65,60 +65,60 @@ app.mount("/static", StaticFiles(directory="frontend"), name="static")
65
  # ==========================================
66
  KECAMATAN_DATABASE = {
67
  # 1. JAKARTA PUSAT (8 Kecamatan) - Total: 1150 Ton
68
- "Menteng": {"latitude": -6.1950, "longitude": 106.8322, "normal_avg": 120.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Pusat"},
69
- "Senen": {"latitude": -6.1822, "longitude": 106.8452, "normal_avg": 180.0, "warning_threshold": 220.0, "critical_threshold": 240.0, "city": "Jakarta Pusat"},
70
- "Cempaka Putih": {"latitude": -6.1802, "longitude": 106.8686, "normal_avg": 90.0, "warning_threshold": 120.0, "critical_threshold": 140.0, "city": "Jakarta Pusat"},
71
- "Johar Baru": {"latitude": -6.1866, "longitude": 106.8572, "normal_avg": 70.0, "warning_threshold": 95.0, "critical_threshold": 110.0, "city": "Jakarta Pusat"},
72
- "Kemayoran": {"latitude": -6.1628, "longitude": 106.8438, "normal_avg": 180.0, "warning_threshold": 220.0, "critical_threshold": 240.0, "city": "Jakarta Pusat"},
73
- "Sawah Besar": {"latitude": -6.1554, "longitude": 106.8322, "normal_avg": 110.0, "warning_threshold": 145.0, "critical_threshold": 165.0, "city": "Jakarta Pusat"},
74
- "Tanah Abang": {"latitude": -6.2104, "longitude": 106.8122, "normal_avg": 250.0, "warning_threshold": 320.0, "critical_threshold": 350.0, "city": "Jakarta Pusat"},
75
- "Gambir": {"latitude": -6.1764, "longitude": 106.8190, "normal_avg": 150.0, "warning_threshold": 195.0, "critical_threshold": 215.0, "city": "Jakarta Pusat"},
76
 
77
  # 2. JAKARTA UTARA (6 Kecamatan) - Total: 1350 Ton
78
- "Penjaringan": {"latitude": -6.1264, "longitude": 106.7822, "normal_avg": 280.0, "warning_threshold": 350.0, "critical_threshold": 380.0, "city": "Jakarta Utara"},
79
- "Tanjung Priok": {"latitude": -6.1322, "longitude": 106.8722, "normal_avg": 260.0, "warning_threshold": 320.0, "critical_threshold": 350.0, "city": "Jakarta Utara"},
80
- "Koja": {"latitude": -6.1214, "longitude": 106.9133, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Utara"},
81
- "Cilincing": {"latitude": -6.1288, "longitude": 106.9452, "normal_avg": 290.0, "warning_threshold": 370.0, "critical_threshold": 400.0, "city": "Jakarta Utara"},
82
- "Pademangan": {"latitude": -6.1328, "longitude": 106.8422, "normal_avg": 140.0, "warning_threshold": 180.0, "critical_threshold": 200.0, "city": "Jakarta Utara"},
83
- "Kelapa Gading": {"latitude": -6.1552, "longitude": 106.9022, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Utara"},
84
 
85
  # 3. JAKARTA BARAT (8 Kecamatan) - Total: 1550 Ton
86
- "Cengkareng": {"latitude": -6.1528, "longitude": 106.7322, "normal_avg": 340.0, "warning_threshold": 420.0, "critical_threshold": 460.0, "city": "Jakarta Barat"},
87
- "Grogol Petamburan": {"latitude": -6.1622, "longitude": 106.7882, "normal_avg": 220.0, "warning_threshold": 280.0, "critical_threshold": 310.0, "city": "Jakarta Barat"},
88
- "Kalideres": {"latitude": -6.1428, "longitude": 106.7022, "normal_avg": 260.0, "warning_threshold": 330.0, "critical_threshold": 360.0, "city": "Jakarta Barat"},
89
- "Kebon Jeruk": {"latitude": -6.1922, "longitude": 106.7722, "normal_avg": 210.0, "warning_threshold": 260.0, "critical_threshold": 290.0, "city": "Jakarta Barat"},
90
- "Kembangan": {"latitude": -6.1828, "longitude": 106.7382, "normal_avg": 180.0, "warning_threshold": 230.0, "critical_threshold": 250.0, "city": "Jakarta Barat"},
91
- "Palmerah": {"latitude": -6.2028, "longitude": 106.7882, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Barat"},
92
- "Taman Sari": {"latitude": -6.1454, "longitude": 106.8182, "normal_avg": 100.0, "warning_threshold": 130.0, "critical_threshold": 150.0, "city": "Jakarta Barat"},
93
- "Tambora": {"latitude": -6.1500, "longitude": 106.8000, "normal_avg": 80.0, "warning_threshold": 110.0, "critical_threshold": 125.0, "city": "Jakarta Barat"},
94
 
95
  # 4. JAKARTA SELATAN (10 Kecamatan) - Total: 1850 Ton
96
- "Cilandak": {"latitude": -6.2928, "longitude": 106.7922, "normal_avg": 180.0, "warning_threshold": 230.0, "critical_threshold": 250.0, "city": "Jakarta Selatan"},
97
- "Jagakarsa": {"latitude": -6.3328, "longitude": 106.8222, "normal_avg": 220.0, "warning_threshold": 280.0, "critical_threshold": 310.0, "city": "Jakarta Selatan"},
98
- "Kebayoran Baru": {"latitude": -6.2422, "longitude": 106.7982, "normal_avg": 210.0, "warning_threshold": 260.0, "critical_threshold": 290.0, "city": "Jakarta Selatan"},
99
- "Kebayoran Lama": {"latitude": -6.2488, "longitude": 106.7722, "normal_avg": 230.0, "warning_threshold": 290.0, "critical_threshold": 320.0, "city": "Jakarta Selatan"},
100
- "Mampang Prapatan": {"latitude": -6.2522, "longitude": 106.8182, "normal_avg": 120.0, "warning_threshold": 150.0, "critical_threshold": 170.0, "city": "Jakarta Selatan"},
101
- "Pancoran": {"latitude": -6.2622, "longitude": 106.8382, "normal_avg": 130.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Selatan"},
102
- "Pasar Minggu": {"latitude": -6.2828, "longitude": 106.8438, "normal_avg": 240.0, "warning_threshold": 300.0, "critical_threshold": 330.0, "city": "Jakarta Selatan"},
103
- "Pesanggrahan": {"latitude": -6.2588, "longitude": 106.7588, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Selatan"},
104
- "Setiabudi": {"latitude": -6.2228, "longitude": 106.8282, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Selatan"},
105
- "Tebet": {"latitude": -6.2288, "longitude": 106.8482, "normal_avg": 170.0, "warning_threshold": 210.0, "critical_threshold": 230.0, "city": "Jakarta Selatan"},
106
 
107
  # 5. JAKARTA TIMUR (10 Kecamatan) - Total: 2100 Ton
108
- "Cakung": {"latitude": -6.1828, "longitude": 106.9482, "normal_avg": 350.0, "warning_threshold": 430.0, "critical_threshold": 470.0, "city": "Jakarta Timur"},
109
- "Cipayung": {"latitude": -6.3128, "longitude": 106.9022, "normal_avg": 140.0, "warning_threshold": 180.0, "critical_threshold": 200.0, "city": "Jakarta Timur"},
110
- "Ciracas": {"latitude": -6.3228, "longitude": 106.8782, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Timur"},
111
- "Duren Sawit": {"latitude": -6.2228, "longitude": 106.9282, "normal_avg": 300.0, "warning_threshold": 370.0, "critical_threshold": 410.0, "city": "Jakarta Timur"},
112
- "Jatinegara": {"latitude": -6.2222, "longitude": 106.8682, "normal_avg": 240.0, "warning_threshold": 300.0, "critical_threshold": 330.0, "city": "Jakarta Timur"},
113
- "Kramat Jati": {"latitude": -6.2722, "longitude": 106.8682, "normal_avg": 220.0, "warning_threshold": 270.0, "critical_threshold": 300.0, "city": "Jakarta Timur"},
114
- "Makasar": {"latitude": -6.2622, "longitude": 106.8782, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Timur"},
115
- "Matraman": {"latitude": -6.2022, "longitude": 106.8582, "normal_avg": 130.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Timur"},
116
- "Pasar Rebo": {"latitude": -6.3122, "longitude": 106.8522, "normal_avg": 150.0, "warning_threshold": 190.0, "critical_threshold": 210.0, "city": "Jakarta Timur"},
117
- "Pulo Gadung": {"latitude": -6.1922, "longitude": 106.8922, "normal_avg": 220.0, "warning_threshold": 270.0, "critical_threshold": 300.0, "city": "Jakarta Timur"},
118
 
119
  # 6. KEPULAUAN SERIBU (2 Kecamatan) - Total: 20 Ton
120
- "Kepulauan Seribu Utara": {"latitude": -5.5722, "longitude": 106.5522, "normal_avg": 11.0, "warning_threshold": 15.0, "critical_threshold": 18.0, "city": "Kepulauan Seribu"},
121
- "Kepulauan Seribu Selatan": {"latitude": -5.7722, "longitude": 106.6522, "normal_avg": 9.0, "warning_threshold": 12.0, "critical_threshold": 15.0, "city": "Kepulauan Seribu"}
122
  }
123
 
124
  ALLOWED_LOCATIONS = list(KECAMATAN_DATABASE.keys())
@@ -129,7 +129,8 @@ ALLOWED_LOCATIONS = list(KECAMATAN_DATABASE.keys())
129
  class PredictionRequest(BaseModel):
130
  forecast_days: int = Field(7, ge=1, le=30, description="Forecast horizon in days (1-30)")
131
  rainfall_mm: float = Field(0.0, ge=0, description="Precipitation override. 0.0 means Auto (Open-Meteo)")
132
- event_scale: int = Field(0, ge=0, le=5, description="Manual event crowd scale (0=none, 5=massive)")
 
133
  location: str = Field(..., description="Target sub-district (Kecamatan)")
134
  start_date: Optional[str] = Field(None, description="Start date: YYYY-MM-DD")
135
  granularity: str = Field("daily", pattern="^(daily|hourly)$", description="Granularity")
@@ -305,10 +306,13 @@ async def load_assets():
305
  if str(r.get("ada_event", "1")) == "1":
306
  dk = str(r.get("tanggal", "")).strip()
307
  if dk:
 
 
308
  events_data[dk] = {
309
  "event_name": str(r.get("nama_event", "")),
310
  "location": str(r.get("lokasi", "")),
311
- "crowd_scale": float(r.get("skala_keramaian", 0))
 
312
  }
313
  logger.info(f"βœ… Event calendar loaded: {len(events_data)} entries")
314
  except Exception as e:
@@ -514,7 +518,17 @@ async def predict_waste_volume(req: PredictionRequest):
514
  # Fetch live weather forecast from Open-Meteo API
515
  weather_forecast = await fetch_rainfall_forecast(config["latitude"], config["longitude"], req.forecast_days)
516
 
517
- # Calibrations Setup
 
 
 
 
 
 
 
 
 
 
518
  dataset_mean = df_history["Volume_Total_Ton"].mean()
519
  real_baseline = config["normal_avg"]
520
  calibration_factor = real_baseline / dataset_mean
@@ -548,17 +562,18 @@ async def predict_waste_volume(req: PredictionRequest):
548
  if rain_val > 5.0:
549
  rain_m = 1.0 + min(rain_val * 0.002, 0.20)
550
 
551
- # Events multiplier
552
  evt = events_data.get(d_str)
553
- evt_m = 1.0
554
  info = None
555
- if evt and evt["crowd_scale"] > 0 and (req.location.lower() in evt["location"].lower() or evt["location"].lower() == "jakarta"):
556
- evt_m = 1.0 + 0.10 + min(evt["crowd_scale"] * 0.05, 0.25)
557
- info = f"{evt['event_name']} @ {evt['location']}"
558
- elif req.event_scale > 0:
559
- evt_m = 1.0 + req.event_scale * 0.10
560
 
561
- raw_prediction = base * rain_m * evt_m
 
 
 
562
  calibrated_volume = round(float(raw_prediction * calibration_factor), 2)
563
 
564
  total_vol += calibrated_volume
@@ -591,15 +606,21 @@ async def predict_waste_volume(req: PredictionRequest):
591
  rain_lag1 = req.rainfall_mm if (req.rainfall_mm > 0.0 and i == 1) else weather_forecast.get((curr_date - timedelta(days=1)).strftime("%Y-%m-%d"), 0.0)
592
 
593
  evt = events_data.get(d_str)
594
- has_event = 1 if (evt and (req.location.lower() in evt["location"].lower() or evt["location"].lower() == "jakarta")) else 0
595
- crowd = float(evt["crowd_scale"]) if has_event else (float(req.event_scale) if i == 0 else 0.0)
596
- info = f"{evt['event_name']} @ {evt['location']}" if has_event else None
 
 
 
 
 
 
597
 
598
  # Fitur dataframe construction matching train.py
599
  mrt_riders = 85000 if curr_date.weekday() < 5 else 45000
600
  features = pd.DataFrame([{
601
  'Penumpang_MRT': mrt_riders,
602
- 'Ada_Event': has_event or (1 if (req.event_scale > 0 and i == 0) else 0),
603
  'Curah_Hujan_mm': rain_val,
604
  'Hujan_Kemarin': rain_lag1,
605
  'Hari_Dalam_Minggu': curr_date.weekday(),
@@ -608,7 +629,7 @@ async def predict_waste_volume(req: PredictionRequest):
608
  }])
609
 
610
  raw_pred = float(model_gbr.predict(features)[0])
611
- calibrated_volume = round(float(raw_pred * calibration_factor), 2)
612
 
613
  total_vol += calibrated_volume
614
  risk = get_risk_status(calibrated_volume, req.location)
 
65
  # ==========================================
66
  KECAMATAN_DATABASE = {
67
  # 1. JAKARTA PUSAT (8 Kecamatan) - Total: 1150 Ton
68
+ "Menteng": {"latitude": -6.1950, "longitude": 106.8322, "population_jiwa": 88000, "normal_avg": 120.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Pusat"},
69
+ "Senen": {"latitude": -6.1822, "longitude": 106.8452, "population_jiwa": 128000, "normal_avg": 180.0, "warning_threshold": 220.0, "critical_threshold": 240.0, "city": "Jakarta Pusat"},
70
+ "Cempaka Putih": {"latitude": -6.1802, "longitude": 106.8686, "population_jiwa": 96000, "normal_avg": 90.0, "warning_threshold": 120.0, "critical_threshold": 140.0, "city": "Jakarta Pusat"},
71
+ "Johar Baru": {"latitude": -6.1866, "longitude": 106.8572, "population_jiwa": 130000, "normal_avg": 70.0, "warning_threshold": 95.0, "critical_threshold": 110.0, "city": "Jakarta Pusat"},
72
+ "Kemayoran": {"latitude": -6.1628, "longitude": 106.8438, "population_jiwa": 255000, "normal_avg": 180.0, "warning_threshold": 220.0, "critical_threshold": 240.0, "city": "Jakarta Pusat"},
73
+ "Sawah Besar": {"latitude": -6.1554, "longitude": 106.8322, "population_jiwa": 126000, "normal_avg": 110.0, "warning_threshold": 145.0, "critical_threshold": 165.0, "city": "Jakarta Pusat"},
74
+ "Tanah Abang": {"latitude": -6.2104, "longitude": 106.8122, "population_jiwa": 175000, "normal_avg": 250.0, "warning_threshold": 320.0, "critical_threshold": 350.0, "city": "Jakarta Pusat"},
75
+ "Gambir": {"latitude": -6.1764, "longitude": 106.8190, "population_jiwa": 97000, "normal_avg": 150.0, "warning_threshold": 195.0, "critical_threshold": 215.0, "city": "Jakarta Pusat"},
76
 
77
  # 2. JAKARTA UTARA (6 Kecamatan) - Total: 1350 Ton
78
+ "Penjaringan": {"latitude": -6.1264, "longitude": 106.7822, "population_jiwa": 312000, "normal_avg": 280.0, "warning_threshold": 350.0, "critical_threshold": 380.0, "city": "Jakarta Utara"},
79
+ "Tanjung Priok": {"latitude": -6.1322, "longitude": 106.8722, "population_jiwa": 415000, "normal_avg": 260.0, "warning_threshold": 320.0, "critical_threshold": 350.0, "city": "Jakarta Utara"},
80
+ "Koja": {"latitude": -6.1214, "longitude": 106.9133, "population_jiwa": 330000, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Utara"},
81
+ "Cilincing": {"latitude": -6.1288, "longitude": 106.9452, "population_jiwa": 430000, "normal_avg": 290.0, "warning_threshold": 370.0, "critical_threshold": 400.0, "city": "Jakarta Utara"},
82
+ "Pademangan": {"latitude": -6.1328, "longitude": 106.8422, "population_jiwa": 168000, "normal_avg": 140.0, "warning_threshold": 180.0, "critical_threshold": 200.0, "city": "Jakarta Utara"},
83
+ "Kelapa Gading": {"latitude": -6.1552, "longitude": 106.9022, "population_jiwa": 143000, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Utara"},
84
 
85
  # 3. JAKARTA BARAT (8 Kecamatan) - Total: 1550 Ton
86
+ "Cengkareng": {"latitude": -6.1528, "longitude": 106.7322, "population_jiwa": 592000, "normal_avg": 340.0, "warning_threshold": 420.0, "critical_threshold": 460.0, "city": "Jakarta Barat"},
87
+ "Grogol Petamburan": {"latitude": -6.1622, "longitude": 106.7882, "population_jiwa": 240000, "normal_avg": 220.0, "warning_threshold": 280.0, "critical_threshold": 310.0, "city": "Jakarta Barat"},
88
+ "Kalideres": {"latitude": -6.1428, "longitude": 106.7022, "population_jiwa": 460000, "normal_avg": 260.0, "warning_threshold": 330.0, "critical_threshold": 360.0, "city": "Jakarta Barat"},
89
+ "Kebon Jeruk": {"latitude": -6.1922, "longitude": 106.7722, "population_jiwa": 380000, "normal_avg": 210.0, "warning_threshold": 260.0, "critical_threshold": 290.0, "city": "Jakarta Barat"},
90
+ "Kembangan": {"latitude": -6.1828, "longitude": 106.7382, "population_jiwa": 310000, "normal_avg": 180.0, "warning_threshold": 230.0, "critical_threshold": 250.0, "city": "Jakarta Barat"},
91
+ "Palmerah": {"latitude": -6.2028, "longitude": 106.7882, "population_jiwa": 205000, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Barat"},
92
+ "Taman Sari": {"latitude": -6.1454, "longitude": 106.8182, "population_jiwa": 125000, "normal_avg": 100.0, "warning_threshold": 130.0, "critical_threshold": 150.0, "city": "Jakarta Barat"},
93
+ "Tambora": {"latitude": -6.1500, "longitude": 106.8000, "population_jiwa": 270000, "normal_avg": 80.0, "warning_threshold": 110.0, "critical_threshold": 125.0, "city": "Jakarta Barat"},
94
 
95
  # 4. JAKARTA SELATAN (10 Kecamatan) - Total: 1850 Ton
96
+ "Cilandak": {"latitude": -6.2928, "longitude": 106.7922, "population_jiwa": 215000, "normal_avg": 180.0, "warning_threshold": 230.0, "critical_threshold": 250.0, "city": "Jakarta Selatan"},
97
+ "Jagakarsa": {"latitude": -6.3328, "longitude": 106.8222, "population_jiwa": 390000, "normal_avg": 220.0, "warning_threshold": 280.0, "critical_threshold": 310.0, "city": "Jakarta Selatan"},
98
+ "Kebayoran Baru": {"latitude": -6.2422, "longitude": 106.7982, "population_jiwa": 145000, "normal_avg": 210.0, "warning_threshold": 260.0, "critical_threshold": 290.0, "city": "Jakarta Selatan"},
99
+ "Kebayoran Lama": {"latitude": -6.2488, "longitude": 106.7722, "population_jiwa": 310000, "normal_avg": 230.0, "warning_threshold": 290.0, "critical_threshold": 320.0, "city": "Jakarta Selatan"},
100
+ "Mampang Prapatan": {"latitude": -6.2522, "longitude": 106.8182, "population_jiwa": 150000, "normal_avg": 120.0, "warning_threshold": 150.0, "critical_threshold": 170.0, "city": "Jakarta Selatan"},
101
+ "Pancoran": {"latitude": -6.2622, "longitude": 106.8382, "population_jiwa": 170000, "normal_avg": 130.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Selatan"},
102
+ "Pasar Minggu": {"latitude": -6.2828, "longitude": 106.8438, "population_jiwa": 315000, "normal_avg": 240.0, "warning_threshold": 300.0, "critical_threshold": 330.0, "city": "Jakarta Selatan"},
103
+ "Pesanggrahan": {"latitude": -6.2588, "longitude": 106.7588, "population_jiwa": 250000, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Selatan"},
104
+ "Setiabudi": {"latitude": -6.2228, "longitude": 106.8282, "population_jiwa": 110000, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Selatan"},
105
+ "Tebet": {"latitude": -6.2288, "longitude": 106.8482, "population_jiwa": 220000, "normal_avg": 170.0, "warning_threshold": 210.0, "critical_threshold": 230.0, "city": "Jakarta Selatan"},
106
 
107
  # 5. JAKARTA TIMUR (10 Kecamatan) - Total: 2100 Ton
108
+ "Cakung": {"latitude": -6.1828, "longitude": 106.9482, "population_jiwa": 559000, "normal_avg": 350.0, "warning_threshold": 430.0, "critical_threshold": 470.0, "city": "Jakarta Timur"},
109
+ "Cipayung": {"latitude": -6.3128, "longitude": 106.9022, "population_jiwa": 290000, "normal_avg": 140.0, "warning_threshold": 180.0, "critical_threshold": 200.0, "city": "Jakarta Timur"},
110
+ "Ciracas": {"latitude": -6.3228, "longitude": 106.8782, "population_jiwa": 310000, "normal_avg": 190.0, "warning_threshold": 240.0, "critical_threshold": 270.0, "city": "Jakarta Timur"},
111
+ "Duren Sawit": {"latitude": -6.2228, "longitude": 106.9282, "population_jiwa": 420000, "normal_avg": 300.0, "warning_threshold": 370.0, "critical_threshold": 410.0, "city": "Jakarta Timur"},
112
+ "Jatinegara": {"latitude": -6.2222, "longitude": 106.8682, "population_jiwa": 315000, "normal_avg": 240.0, "warning_threshold": 300.0, "critical_threshold": 330.0, "city": "Jakarta Timur"},
113
+ "Kramat Jati": {"latitude": -6.2722, "longitude": 106.8682, "population_jiwa": 300000, "normal_avg": 220.0, "warning_threshold": 270.0, "critical_threshold": 300.0, "city": "Jakarta Timur"},
114
+ "Makasar": {"latitude": -6.2622, "longitude": 106.8782, "population_jiwa": 210000, "normal_avg": 160.0, "warning_threshold": 200.0, "critical_threshold": 220.0, "city": "Jakarta Timur"},
115
+ "Matraman": {"latitude": -6.2022, "longitude": 106.8582, "population_jiwa": 175000, "normal_avg": 130.0, "warning_threshold": 160.0, "critical_threshold": 180.0, "city": "Jakarta Timur"},
116
+ "Pasar Rebo": {"latitude": -6.3122, "longitude": 106.8522, "population_jiwa": 220000, "normal_avg": 150.0, "warning_threshold": 190.0, "critical_threshold": 210.0, "city": "Jakarta Timur"},
117
+ "Pulo Gadung": {"latitude": -6.1922, "longitude": 106.8922, "population_jiwa": 300000, "normal_avg": 220.0, "warning_threshold": 270.0, "critical_threshold": 300.0, "city": "Jakarta Timur"},
118
 
119
  # 6. KEPULAUAN SERIBU (2 Kecamatan) - Total: 20 Ton
120
+ "Kepulauan Seribu Utara": {"latitude": -5.5722, "longitude": 106.5522, "population_jiwa": 16000, "normal_avg": 11.0, "warning_threshold": 15.0, "critical_threshold": 18.0, "city": "Kepulauan Seribu"},
121
+ "Kepulauan Seribu Selatan": {"latitude": -5.7722, "longitude": 106.6522, "population_jiwa": 13000, "normal_avg": 9.0, "warning_threshold": 12.0, "critical_threshold": 15.0, "city": "Kepulauan Seribu"}
122
  }
123
 
124
  ALLOWED_LOCATIONS = list(KECAMATAN_DATABASE.keys())
 
129
  class PredictionRequest(BaseModel):
130
  forecast_days: int = Field(7, ge=1, le=30, description="Forecast horizon in days (1-30)")
131
  rainfall_mm: float = Field(0.0, ge=0, description="Precipitation override. 0.0 means Auto (Open-Meteo)")
132
+ event_scale: Optional[int] = Field(0, ge=0, description="Legacy crowd scale (optional fallback)")
133
+ jumlah_jiwa: Optional[int] = Field(None, ge=0, description="Target headcount / population override (Jumlah Jiwa)")
134
  location: str = Field(..., description="Target sub-district (Kecamatan)")
135
  start_date: Optional[str] = Field(None, description="Start date: YYYY-MM-DD")
136
  granularity: str = Field("daily", pattern="^(daily|hourly)$", description="Granularity")
 
306
  if str(r.get("ada_event", "1")) == "1":
307
  dk = str(r.get("tanggal", "")).strip()
308
  if dk:
309
+ raw_jiwa = float(r.get("jumlah_jiwa", r.get("skala_keramaian", 0)))
310
+ crowd_jiwa = raw_jiwa * 20000.0 if (0 < raw_jiwa <= 5) else raw_jiwa
311
  events_data[dk] = {
312
  "event_name": str(r.get("nama_event", "")),
313
  "location": str(r.get("lokasi", "")),
314
+ "crowd_scale": crowd_jiwa,
315
+ "jumlah_jiwa": crowd_jiwa
316
  }
317
  logger.info(f"βœ… Event calendar loaded: {len(events_data)} entries")
318
  except Exception as e:
 
518
  # Fetch live weather forecast from Open-Meteo API
519
  weather_forecast = await fetch_rainfall_forecast(config["latitude"], config["longitude"], req.forecast_days)
520
 
521
+ # Calibrations & Headcount Setup
522
+ baseline_pop = float(config.get("population_jiwa", 100000))
523
+
524
+ # User input target headcount / population override (Jumlah Jiwa)
525
+ if req.jumlah_jiwa is not None and req.jumlah_jiwa > 0:
526
+ target_pop = float(req.jumlah_jiwa)
527
+ elif req.event_scale and req.event_scale > 0:
528
+ target_pop = baseline_pop + (req.event_scale * 20000.0 if req.event_scale <= 5 else float(req.event_scale))
529
+ else:
530
+ target_pop = float(baseline_pop)
531
+
532
  dataset_mean = df_history["Volume_Total_Ton"].mean()
533
  real_baseline = config["normal_avg"]
534
  calibration_factor = real_baseline / dataset_mean
 
562
  if rain_val > 5.0:
563
  rain_m = 1.0 + min(rain_val * 0.002, 0.20)
564
 
565
+ # Events multiplier from headcount (Jumlah Jiwa)
566
  evt = events_data.get(d_str)
567
+ event_pop = 0.0
568
  info = None
569
+ if evt and (req.location.lower() in evt["location"].lower() or evt["location"].lower() == "jakarta"):
570
+ event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
571
+ info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
 
 
572
 
573
+ total_day_jiwa = target_pop + event_pop
574
+ pop_scaling_factor = total_day_jiwa / baseline_pop
575
+
576
+ raw_prediction = base * rain_m * pop_scaling_factor
577
  calibrated_volume = round(float(raw_prediction * calibration_factor), 2)
578
 
579
  total_vol += calibrated_volume
 
606
  rain_lag1 = req.rainfall_mm if (req.rainfall_mm > 0.0 and i == 1) else weather_forecast.get((curr_date - timedelta(days=1)).strftime("%Y-%m-%d"), 0.0)
607
 
608
  evt = events_data.get(d_str)
609
+ event_pop = 0.0
610
+ info = None
611
+ if evt and (req.location.lower() in evt["location"].lower() or evt["location"].lower() == "jakarta"):
612
+ event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
613
+ info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
614
+
615
+ total_day_jiwa = target_pop + event_pop
616
+ pop_scaling_factor = total_day_jiwa / baseline_pop
617
+ has_event = 1 if (event_pop > 0 or target_pop > baseline_pop) else 0
618
 
619
  # Fitur dataframe construction matching train.py
620
  mrt_riders = 85000 if curr_date.weekday() < 5 else 45000
621
  features = pd.DataFrame([{
622
  'Penumpang_MRT': mrt_riders,
623
+ 'Ada_Event': has_event,
624
  'Curah_Hujan_mm': rain_val,
625
  'Hujan_Kemarin': rain_lag1,
626
  'Hari_Dalam_Minggu': curr_date.weekday(),
 
629
  }])
630
 
631
  raw_pred = float(model_gbr.predict(features)[0])
632
+ calibrated_volume = round(float(raw_pred * calibration_factor * pop_scaling_factor), 2)
633
 
634
  total_vol += calibrated_volume
635
  risk = get_risk_status(calibrated_volume, req.location)
event_jakarta_2026.txt CHANGED
@@ -1,32 +1,32 @@
1
- tanggal,nama_event,lokasi,skala_keramaian
2
- 2026-01-01,Perayaan Tahun Baru,Monas,4
3
- 2026-02-17,Imlek Festival,Glodok,2
4
- 2026-03-18,H-3 Lebaran,Jakarta,4
5
- 2026-03-22,Idul Fitri,Jakarta,5
6
- 2026-04-10,Jakarta Art Festival,JIExpo,2
7
- 2026-05-01,May Day Rally,Monas,2
8
- 2026-06-11,PRJ Opening,JIExpo,5
9
- 2026-06-13,BTN Marathon 2026,Jalan Protokol,3
10
- 2026-06-14,PRJ Weekend,JIExpo,4
11
- 2026-06-21,PRJ Peak Weekend,JIExpo,5
12
- 2026-06-28,PRJ Mid-Event Weekend,JIExpo,4
13
- 2026-07-01,DWP 2026 Jakarta,Kemayoran,5
14
- 2026-07-02,INAGRITECH 2026,Kemayoran,4
15
- 2026-07-02,"INAMARINE, INAWELDING & RAILWAYTECH INDONESIA 2026",Kemayoran,4
16
- 2026-07-08,BritCham Indonesia's Golf Tournament,Kebayoran Lama,2
17
- 2026-07-08,Canada Cup 2026,Kebayoran Lama,2
18
- 2026-07-11,Uji Coba Sistem AI (Demo Event),Gambir,4
19
- 2026-07-12,Konser Musik Spektakuler,Menteng,3
20
- 2026-07-13,Japan Edu Expo 2026,Setiabudi,4
21
- 2026-07-13,Anime Festival Asia Indonesia 2026,Tanah Abang,5
22
- 2026-07-13,π‘πŽπ’π„π“πŽππˆπ€ π€π’πˆπ€ π“πŽπ”π‘ πŸπŸŽπŸπŸ”,Tanah Abang,5
23
- 2026-07-14,Bangor Run Jakarta 2026,Tanah Abang,3
24
- 2026-07-14,Run For Animals 2026,Cipayung,3
25
- 2026-07-14,Solar Run 2026,Gambir,3
26
- 2026-07-14,Tangy’s Story Adventures | WCIJ Trial Class Experience,Kebayoran Lama,2
27
- 2026-07-20,PRJ Final Weekend,JIExpo,5
28
- 2026-08-17,HUT RI ke-81,Monas,4
29
- 2026-09-15,Food & Culture Expo,Ancol,2
30
- 2026-11-25,Ancol Music Fest,Ancol,3
31
- 2026-12-20,Christmas Market,Bundaran HI,3
32
- 2026-12-31,Countdown Jakarta 2027,Monas,4
 
1
+ tanggal,nama_event,lokasi,jumlah_jiwa
2
+ 2026-01-01,Perayaan Tahun Baru,Monas,100000
3
+ 2026-02-17,Imlek Festival,Glodok,25000
4
+ 2026-03-18,H-3 Lebaran,Jakarta,80000
5
+ 2026-03-22,Idul Fitri,Jakarta,150000
6
+ 2026-04-10,Jakarta Art Festival,JIExpo,20000
7
+ 2026-05-01,May Day Rally,Monas,30000
8
+ 2026-06-11,PRJ Opening,JIExpo,120000
9
+ 2026-06-13,BTN Marathon 2026,Jalan Protokol,40000
10
+ 2026-06-14,PRJ Weekend,JIExpo,80000
11
+ 2026-06-21,PRJ Peak Weekend,JIExpo,150000
12
+ 2026-06-28,PRJ Mid-Event Weekend,JIExpo,75000
13
+ 2026-07-01,DWP 2026 Jakarta,Kemayoran,100000
14
+ 2026-07-02,INAGRITECH 2026,Kemayoran,50000
15
+ 2026-07-02,"INAMARINE, INAWELDING & RAILWAYTECH INDONESIA 2026",Kemayoran,45000
16
+ 2026-07-08,BritCham Indonesia's Golf Tournament,Kebayoran Lama,15000
17
+ 2026-07-08,Canada Cup 2026,Kebayoran Lama,15000
18
+ 2026-07-11,Uji Coba Sistem AI (Demo Event),Gambir,60000
19
+ 2026-07-12,Konser Musik Spektakuler,Menteng,35000
20
+ 2026-07-13,Japan Edu Expo 2026,Setiabudi,50000
21
+ 2026-07-13,Anime Festival Asia Indonesia 2026,Tanah Abang,120000
22
+ 2026-07-13,π‘πŽπ’π„π“πŽππˆπ€ π€π’πˆπ€ π“πŽπ”π‘ πŸπŸŽπŸπŸ”,Tanah Abang,110000
23
+ 2026-07-14,Bangor Run Jakarta 2026,Tanah Abang,30000
24
+ 2026-07-14,Run For Animals 2026,Cipayung,25000
25
+ 2026-07-14,Solar Run 2026,Gambir,30000
26
+ 2026-07-14,Tangy’s Story Adventures | WCIJ Trial Class Experience,Kebayoran Lama,10000
27
+ 2026-07-20,PRJ Final Weekend,JIExpo,140000
28
+ 2026-08-17,HUT RI ke-81,Monas,90000
29
+ 2026-09-15,Food & Culture Expo,Ancol,25000
30
+ 2026-11-25,Ancol Music Fest,Ancol,40000
31
+ 2026-12-20,Christmas Market,Bundaran HI,35000
32
+ 2026-12-31,Countdown Jakarta 2027,Monas,110000
frontend/app.js CHANGED
@@ -72,6 +72,7 @@ const forecastVal = document.getElementById("forecast-val");
72
  const rainOverride = document.getElementById("rain-override");
73
  const rainOverrideVal = document.getElementById("rain-override-val");
74
  const eventOverride = document.getElementById("event-override");
 
75
  const predictBtn = document.getElementById("predict-btn");
76
  const exportBtn = document.getElementById("export-btn");
77
 
@@ -202,9 +203,25 @@ if (rainOverride) {
202
  });
203
  }
204
 
 
 
 
 
 
 
 
 
 
205
  if (locationSelect) {
206
  locationSelect.addEventListener("change", (e) => {
207
  selectedLocation = e.target.value;
 
 
 
 
 
 
 
208
  updateActiveMapMarker(selectedLocation);
209
  panToLocation(selectedLocation);
210
  fetchLiveWeather(selectedLocation);
@@ -418,7 +435,8 @@ async function runPrediction() {
418
  const payload = {
419
  forecast_days: parseInt(forecastSlider.value),
420
  rainfall_mm: parseFloat(rainValue),
421
- event_scale: parseInt(eventOverride.value),
 
422
  location: selectedLocation,
423
  model_type: modelSelect.value,
424
  granularity: forecastSlider.value <= 7 ? "hourly" : "daily"
 
72
  const rainOverride = document.getElementById("rain-override");
73
  const rainOverrideVal = document.getElementById("rain-override-val");
74
  const eventOverride = document.getElementById("event-override");
75
+ const eventOverrideVal = document.getElementById("event-override-val");
76
  const predictBtn = document.getElementById("predict-btn");
77
  const exportBtn = document.getElementById("export-btn");
78
 
 
203
  });
204
  }
205
 
206
+ if (eventOverride) {
207
+ eventOverride.addEventListener("input", (e) => {
208
+ const val = parseInt(e.target.value);
209
+ if (eventOverrideVal) {
210
+ eventOverrideVal.textContent = `${val.toLocaleString()} Jiwa`;
211
+ }
212
+ });
213
+ }
214
+
215
  if (locationSelect) {
216
  locationSelect.addEventListener("change", (e) => {
217
  selectedLocation = e.target.value;
218
+ const pop = KECAMATAN_DATABASE[selectedLocation]?.population_jiwa || 100000;
219
+ if (eventOverride) {
220
+ eventOverride.value = pop;
221
+ }
222
+ if (eventOverrideVal) {
223
+ eventOverrideVal.textContent = `${pop.toLocaleString()} Jiwa (BPS)`;
224
+ }
225
  updateActiveMapMarker(selectedLocation);
226
  panToLocation(selectedLocation);
227
  fetchLiveWeather(selectedLocation);
 
435
  const payload = {
436
  forecast_days: parseInt(forecastSlider.value),
437
  rainfall_mm: parseFloat(rainValue),
438
+ jumlah_jiwa: parseInt(eventOverride.value),
439
+ event_scale: 0,
440
  location: selectedLocation,
441
  model_type: modelSelect.value,
442
  granularity: forecastSlider.value <= 7 ? "hourly" : "daily"
frontend/index.html CHANGED
@@ -214,8 +214,8 @@
214
  </div>
215
 
216
  <div class="control-group">
217
- <label for="event-override">Crowd Event Scale (0 - 5)</label>
218
- <input type="range" id="event-override" min="0" max="5" value="0" class="slider">
219
  </div>
220
 
221
  <div class="button-row">
@@ -423,7 +423,7 @@
423
 
424
  <footer>
425
  <p>&copy; 2026 Aeterna AI - DKI Jakarta Waste Management Intelligence. Calibrated with DLH & Open-Meteo.</p>
426
- <p style="margin-top: 0.6rem; font-size: 0.72rem; opacity: 0.75; letter-spacing: 0.5px; font-family: var(--font-mono); line-height: 1.5;">Developed by: <span style="color:var(--cyan); font-weight:bold;">Faril Putra Pratama</span> (AI Engineer) | <span style="color:var(--cyan); font-weight:bold;">Arga Kurniawan</span> (Front End Developer) | <span style="color:var(--cyan); font-weight:bold;">Bagas Tresna Mustida Sakli</span> (System Architecture)</p>
427
  </footer>
428
 
429
  <!-- Custom Cyber HUD Cursor -->
 
214
  </div>
215
 
216
  <div class="control-group">
217
+ <label for="event-override">Simulasi Populasi (<span id="event-override-val" class="override-display">Auto (BPS Baseline)</span>)</label>
218
+ <input type="range" id="event-override" min="10000" max="700000" step="5000" value="88000" class="slider">
219
  </div>
220
 
221
  <div class="button-row">
 
423
 
424
  <footer>
425
  <p>&copy; 2026 Aeterna AI - DKI Jakarta Waste Management Intelligence. Calibrated with DLH & Open-Meteo.</p>
426
+ <p style="margin-top: 0.6rem; font-size: 0.72rem; opacity: 0.75; letter-spacing: 0.5px; font-family: var(--font-mono); line-height: 1.5;">Developed with ⚑ by: <span style="color:var(--cyan); font-weight:bold;">Faril Putra Pratama</span> (Lead Full-Stack AI Engineer)</p>
427
  </footer>
428
 
429
  <!-- Custom Cyber HUD Cursor -->