fix: implement verified AI/ML audit fixes (15T truck divisor, Other Waste CSV column, daily variance seed, dynamic risk status, fallback event descriptions)
Browse files- app.py +58 -9
- frontend/app.js +1 -1
- frontend/index.html +1 -1
app.py
CHANGED
|
@@ -224,9 +224,10 @@ def parse_flexible_date(date_input: str, default_year: int = 2026) -> pd.Timesta
|
|
| 224 |
|
| 225 |
def get_risk_status(volume: float, location: str) -> str:
|
| 226 |
config = KECAMATAN_DATABASE.get(location, KECAMATAN_DATABASE["Menteng"])
|
| 227 |
-
|
|
|
|
| 228 |
return "CRITICAL"
|
| 229 |
-
elif volume >
|
| 230 |
return "WARNING"
|
| 231 |
return "SAFE"
|
| 232 |
|
|
@@ -742,10 +743,25 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 742 |
event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
|
| 743 |
info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
|
| 744 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
total_day_jiwa = target_pop + event_pop
|
| 746 |
pop_scaling_factor = total_day_jiwa / baseline_pop
|
| 747 |
|
| 748 |
raw_prediction = base * rain_m * pop_scaling_factor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
calibrated_volume = round(float(raw_prediction * calibration_factor), 2)
|
| 750 |
|
| 751 |
total_vol += calibrated_volume
|
|
@@ -761,7 +777,7 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 761 |
paper_waste_ton=round(calibrated_volume*paper_r, 2), metal_waste_ton=round(calibrated_volume*metal_r, 2),
|
| 762 |
glass_waste_ton=round(calibrated_volume*glass_r, 2), textile_waste_ton=round(calibrated_volume*textile_r, 2),
|
| 763 |
other_waste_ton=round(calibrated_volume*other_r, 2),
|
| 764 |
-
recommended_trucks=max(1, int(np.ceil(calibrated_volume/
|
| 765 |
risk_status=risk, event_info=info, hourly_breakdown=hourly
|
| 766 |
))
|
| 767 |
|
|
@@ -790,6 +806,14 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 790 |
event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
|
| 791 |
info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
|
| 792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
total_day_jiwa = target_pop + event_pop
|
| 794 |
has_event = 1 if (event_pop > 0) else 0
|
| 795 |
|
|
@@ -819,6 +843,12 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 819 |
pop_extrapolate_factor = target_pop / baseline_pop
|
| 820 |
raw_pred *= pop_extrapolate_factor
|
| 821 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
calibrated_volume = round(max(0.1, raw_pred), 2)
|
| 823 |
|
| 824 |
total_vol += calibrated_volume
|
|
@@ -834,7 +864,7 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 834 |
paper_waste_ton=round(calibrated_volume*paper_r, 2), metal_waste_ton=round(calibrated_volume*metal_r, 2),
|
| 835 |
glass_waste_ton=round(calibrated_volume*glass_r, 2), textile_waste_ton=round(calibrated_volume*textile_r, 2),
|
| 836 |
other_waste_ton=round(calibrated_volume*other_r, 2),
|
| 837 |
-
recommended_trucks=max(1, int(np.ceil(calibrated_volume/
|
| 838 |
risk_status=risk, event_info=info, hourly_breakdown=hourly
|
| 839 |
))
|
| 840 |
|
|
@@ -855,7 +885,7 @@ async def predict_waste_volume(req: PredictionRequest):
|
|
| 855 |
logistics_plan=LogisticsPlan(
|
| 856 |
trucks_needed=trucks,
|
| 857 |
manpower=trucks*3,
|
| 858 |
-
estimated_duration_hours=round(total_vol/
|
| 859 |
efficiency_rate="85% (Optimal)"
|
| 860 |
)
|
| 861 |
)
|
|
@@ -878,6 +908,7 @@ async def predict_waste_volume_csv(req: PredictionRequest):
|
|
| 878 |
"Organic Waste (Tons)", "Plastic Waste (Tons)",
|
| 879 |
"Paper Waste (Tons)", "Metal Waste (Tons)",
|
| 880 |
"Glass Waste (Tons)", "Textile Waste (Tons)",
|
|
|
|
| 881 |
"Risk Status", "Event Info", "Recommended Trucks (15T)"
|
| 882 |
])
|
| 883 |
|
|
@@ -887,6 +918,7 @@ async def predict_waste_volume_csv(req: PredictionRequest):
|
|
| 887 |
r.organic_waste_ton, r.plastic_waste_ton,
|
| 888 |
r.paper_waste_ton, r.metal_waste_ton,
|
| 889 |
r.glass_waste_ton, r.textile_waste_ton,
|
|
|
|
| 890 |
r.risk_status, r.event_info or "", r.recommended_trucks
|
| 891 |
])
|
| 892 |
|
|
@@ -986,16 +1018,24 @@ async def get_autopilot_data():
|
|
| 986 |
'Event_Crowd_Headcount': float(event_pop)
|
| 987 |
}])
|
| 988 |
|
| 989 |
-
# Predict directly using Spatial GBR model
|
| 990 |
if model_gbr is not None:
|
| 991 |
raw_pred = float(model_gbr.predict(features)[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 992 |
calibrated_volume = round(max(0.1, raw_pred), 2)
|
| 993 |
else:
|
| 994 |
calibrated_volume = round(float(config["normal_avg"]), 2)
|
| 995 |
|
| 996 |
-
trucks = max(1, int(np.ceil(calibrated_volume /
|
| 997 |
|
| 998 |
-
|
|
|
|
| 999 |
|
| 1000 |
total_vol += calibrated_volume
|
| 1001 |
total_trucks += trucks
|
|
@@ -1014,6 +1054,15 @@ async def get_autopilot_data():
|
|
| 1014 |
kecamatan_results.sort(key=lambda x: x["volume_ton"], reverse=True)
|
| 1015 |
top_5 = kecamatan_results[:5]
|
| 1016 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
return {
|
| 1018 |
"status": "success",
|
| 1019 |
"date": d_str,
|
|
@@ -1021,5 +1070,5 @@ async def get_autopilot_data():
|
|
| 1021 |
"total_trucks": total_trucks,
|
| 1022 |
"top_kecamatan": top_5,
|
| 1023 |
"rainy_regions": rainy_count,
|
| 1024 |
-
"event_today":
|
| 1025 |
}
|
|
|
|
| 224 |
|
| 225 |
def get_risk_status(volume: float, location: str) -> str:
|
| 226 |
config = KECAMATAN_DATABASE.get(location, KECAMATAN_DATABASE["Menteng"])
|
| 227 |
+
norm = config["normal_avg"]
|
| 228 |
+
if volume > norm * 1.30:
|
| 229 |
return "CRITICAL"
|
| 230 |
+
elif volume > norm * 1.12:
|
| 231 |
return "WARNING"
|
| 232 |
return "SAFE"
|
| 233 |
|
|
|
|
| 743 |
event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
|
| 744 |
info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
|
| 745 |
|
| 746 |
+
if info is None:
|
| 747 |
+
if rain_val > 15.0:
|
| 748 |
+
info = f"Rain Impact ({rain_val} mm)"
|
| 749 |
+
elif curr_date.weekday() >= 5:
|
| 750 |
+
info = "Weekend Activity"
|
| 751 |
+
else:
|
| 752 |
+
info = "Routine Operations"
|
| 753 |
+
|
| 754 |
total_day_jiwa = target_pop + event_pop
|
| 755 |
pop_scaling_factor = total_day_jiwa / baseline_pop
|
| 756 |
|
| 757 |
raw_prediction = base * rain_m * pop_scaling_factor
|
| 758 |
+
|
| 759 |
+
# Apply stable pseudo-random daily variance (±2.5%) to simulate real human activity fluctuations
|
| 760 |
+
import hashlib
|
| 761 |
+
seed_val = int(hashlib.md5(f"{d_str}_{req.location}".encode()).hexdigest(), 16)
|
| 762 |
+
daily_variance = 1.0 + ((seed_val % 100) - 50) / 2000.0
|
| 763 |
+
raw_prediction *= daily_variance
|
| 764 |
+
|
| 765 |
calibrated_volume = round(float(raw_prediction * calibration_factor), 2)
|
| 766 |
|
| 767 |
total_vol += calibrated_volume
|
|
|
|
| 777 |
paper_waste_ton=round(calibrated_volume*paper_r, 2), metal_waste_ton=round(calibrated_volume*metal_r, 2),
|
| 778 |
glass_waste_ton=round(calibrated_volume*glass_r, 2), textile_waste_ton=round(calibrated_volume*textile_r, 2),
|
| 779 |
other_waste_ton=round(calibrated_volume*other_r, 2),
|
| 780 |
+
recommended_trucks=max(1, int(np.ceil(calibrated_volume/15))),
|
| 781 |
risk_status=risk, event_info=info, hourly_breakdown=hourly
|
| 782 |
))
|
| 783 |
|
|
|
|
| 806 |
event_pop = float(evt.get("jumlah_jiwa", evt.get("crowd_scale", 0.0)))
|
| 807 |
info = f"{evt['event_name']} ({int(event_pop):,} Jiwa) @ {evt['location']}"
|
| 808 |
|
| 809 |
+
if info is None:
|
| 810 |
+
if rain_val > 15.0:
|
| 811 |
+
info = f"Rain Impact ({rain_val} mm)"
|
| 812 |
+
elif curr_date.weekday() >= 5:
|
| 813 |
+
info = "Weekend Activity"
|
| 814 |
+
else:
|
| 815 |
+
info = "Routine Operations"
|
| 816 |
+
|
| 817 |
total_day_jiwa = target_pop + event_pop
|
| 818 |
has_event = 1 if (event_pop > 0) else 0
|
| 819 |
|
|
|
|
| 843 |
pop_extrapolate_factor = target_pop / baseline_pop
|
| 844 |
raw_pred *= pop_extrapolate_factor
|
| 845 |
|
| 846 |
+
# Apply stable pseudo-random daily variance (±2.5%) to simulate real human activity fluctuations
|
| 847 |
+
import hashlib
|
| 848 |
+
seed_val = int(hashlib.md5(f"{d_str}_{req.location}".encode()).hexdigest(), 16)
|
| 849 |
+
daily_variance = 1.0 + ((seed_val % 100) - 50) / 2000.0
|
| 850 |
+
raw_pred *= daily_variance
|
| 851 |
+
|
| 852 |
calibrated_volume = round(max(0.1, raw_pred), 2)
|
| 853 |
|
| 854 |
total_vol += calibrated_volume
|
|
|
|
| 864 |
paper_waste_ton=round(calibrated_volume*paper_r, 2), metal_waste_ton=round(calibrated_volume*metal_r, 2),
|
| 865 |
glass_waste_ton=round(calibrated_volume*glass_r, 2), textile_waste_ton=round(calibrated_volume*textile_r, 2),
|
| 866 |
other_waste_ton=round(calibrated_volume*other_r, 2),
|
| 867 |
+
recommended_trucks=max(1, int(np.ceil(calibrated_volume/15))),
|
| 868 |
risk_status=risk, event_info=info, hourly_breakdown=hourly
|
| 869 |
))
|
| 870 |
|
|
|
|
| 885 |
logistics_plan=LogisticsPlan(
|
| 886 |
trucks_needed=trucks,
|
| 887 |
manpower=trucks*3,
|
| 888 |
+
estimated_duration_hours=round(total_vol/15, 1),
|
| 889 |
efficiency_rate="85% (Optimal)"
|
| 890 |
)
|
| 891 |
)
|
|
|
|
| 908 |
"Organic Waste (Tons)", "Plastic Waste (Tons)",
|
| 909 |
"Paper Waste (Tons)", "Metal Waste (Tons)",
|
| 910 |
"Glass Waste (Tons)", "Textile Waste (Tons)",
|
| 911 |
+
"Other Waste (Tons)",
|
| 912 |
"Risk Status", "Event Info", "Recommended Trucks (15T)"
|
| 913 |
])
|
| 914 |
|
|
|
|
| 918 |
r.organic_waste_ton, r.plastic_waste_ton,
|
| 919 |
r.paper_waste_ton, r.metal_waste_ton,
|
| 920 |
r.glass_waste_ton, r.textile_waste_ton,
|
| 921 |
+
r.other_waste_ton,
|
| 922 |
r.risk_status, r.event_info or "", r.recommended_trucks
|
| 923 |
])
|
| 924 |
|
|
|
|
| 1018 |
'Event_Crowd_Headcount': float(event_pop)
|
| 1019 |
}])
|
| 1020 |
|
| 1021 |
+
# Predict directly using Spatial GBR model with daily variance seed
|
| 1022 |
if model_gbr is not None:
|
| 1023 |
raw_pred = float(model_gbr.predict(features)[0])
|
| 1024 |
+
|
| 1025 |
+
# Apply stable pseudo-random daily variance (±2.5%)
|
| 1026 |
+
import hashlib
|
| 1027 |
+
seed_val = int(hashlib.md5(f"{d_str}_{loc}".encode()).hexdigest(), 16)
|
| 1028 |
+
daily_variance = 1.0 + ((seed_val % 100) - 50) / 2000.0
|
| 1029 |
+
raw_pred *= daily_variance
|
| 1030 |
+
|
| 1031 |
calibrated_volume = round(max(0.1, raw_pred), 2)
|
| 1032 |
else:
|
| 1033 |
calibrated_volume = round(float(config["normal_avg"]), 2)
|
| 1034 |
|
| 1035 |
+
trucks = max(1, int(np.ceil(calibrated_volume / 15)))
|
| 1036 |
|
| 1037 |
+
norm = config["normal_avg"]
|
| 1038 |
+
status = "CRITICAL" if calibrated_volume > norm * 1.30 else "WARNING" if calibrated_volume > norm * 1.12 else "SAFE"
|
| 1039 |
|
| 1040 |
total_vol += calibrated_volume
|
| 1041 |
total_trucks += trucks
|
|
|
|
| 1054 |
kecamatan_results.sort(key=lambda x: x["volume_ton"], reverse=True)
|
| 1055 |
top_5 = kecamatan_results[:5]
|
| 1056 |
|
| 1057 |
+
# Custom event description fallback for Autopilot
|
| 1058 |
+
event_label = "Routine Operations"
|
| 1059 |
+
if evt:
|
| 1060 |
+
event_label = evt["event_name"]
|
| 1061 |
+
elif rainy_count > 10:
|
| 1062 |
+
event_label = "Heavy Rainy Weather"
|
| 1063 |
+
elif today.weekday() >= 5:
|
| 1064 |
+
event_label = "Weekend Activity"
|
| 1065 |
+
|
| 1066 |
return {
|
| 1067 |
"status": "success",
|
| 1068 |
"date": d_str,
|
|
|
|
| 1070 |
"total_trucks": total_trucks,
|
| 1071 |
"top_kecamatan": top_5,
|
| 1072 |
"rainy_regions": rainy_count,
|
| 1073 |
+
"event_today": event_label
|
| 1074 |
}
|
frontend/app.js
CHANGED
|
@@ -486,7 +486,7 @@ function updateDashboardData(data, confScore, message) {
|
|
| 486 |
updateMarkerRisk(selectedLocation, maxRisk);
|
| 487 |
drawTransitRoute(selectedLocation);
|
| 488 |
|
| 489 |
-
if (statTrucks) statTrucks.innerHTML = `${data.logistics_plan.trucks_needed} <span class="unit">Trucks (
|
| 490 |
|
| 491 |
const startDateStr = results[0].date;
|
| 492 |
const endDateStr = results[results.length - 1].date;
|
|
|
|
| 486 |
updateMarkerRisk(selectedLocation, maxRisk);
|
| 487 |
drawTransitRoute(selectedLocation);
|
| 488 |
|
| 489 |
+
if (statTrucks) statTrucks.innerHTML = `${data.logistics_plan.trucks_needed} <span class="unit">Trucks (15T)</span>`;
|
| 490 |
|
| 491 |
const startDateStr = results[0].date;
|
| 492 |
const endDateStr = results[results.length - 1].date;
|
frontend/index.html
CHANGED
|
@@ -405,7 +405,7 @@
|
|
| 405 |
</div>
|
| 406 |
<div class="panel stat-card">
|
| 407 |
<span class="card-label">RECOMMENDED FLEET</span>
|
| 408 |
-
<span id="stat-trucks" class="card-value">0 <span class="unit">Trucks (
|
| 409 |
<span class="card-meta">Logistics Fleet Suggestion</span>
|
| 410 |
</div>
|
| 411 |
</div>
|
|
|
|
| 405 |
</div>
|
| 406 |
<div class="panel stat-card">
|
| 407 |
<span class="card-label">RECOMMENDED FLEET</span>
|
| 408 |
+
<span id="stat-trucks" class="card-value">0 <span class="unit">Trucks (15T)</span></span>
|
| 409 |
<span class="card-meta">Logistics Fleet Suggestion</span>
|
| 410 |
</div>
|
| 411 |
</div>
|