SHELLAPANDIANGANHUNGING commited on
Commit
d361fe3
·
verified ·
1 Parent(s): 3af60d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +315 -120
app.py CHANGED
@@ -1155,18 +1155,129 @@ except Exception as e:
1155
 
1156
  # ... (kode sebelumnya tetap sama) ...
1157
 
1158
- # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard (DAILY-BASED) =====================
1159
  st.subheader("OBJECTIVE 5: See your team’s fatigue risk gradient at a glance!")
1160
-
1161
- # Custom CSS (tetap sama — tidak berubah)
1162
  st.markdown("""
1163
  <style>
1164
- /* ... CSS tetap persis seperti sebelumnya ... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
  </style>
1166
  """, unsafe_allow_html=True)
1167
 
1168
  # ===============================================================
1169
- # LOGIC UTAMA — DIPERBAIKI: DAILY-BASED SLOPE
1170
  # ===============================================================
1171
  if df.empty:
1172
  st.info("No data available after applying filters.")
@@ -1183,14 +1294,14 @@ else:
1183
  st.info("No operator data after filtering.")
1184
  st.stop()
1185
 
 
1186
  if col_operator is None:
1187
- st.error("Operator column could not be auto-detected. Please check your data.")
1188
  st.stop()
1189
 
1190
- # ✅ PERBAIKAN 1: Ganti dari year_week date (harus datetime.date agar bisa diurutkan)
1191
- df_op["date"] = pd.to_datetime(df_op["start"]).dt.date
1192
 
1193
- # Fuzzy match fleet names (tetap)
1194
  fleet_clean = df_op[col_fleet_type].str.strip().str.upper()
1195
  df_op["is_ob"] = fleet_clean.str.contains(r"OB HAULLER", na=False)
1196
  df_op["is_coal"] = fleet_clean.str.contains(r"HAULING COAL", na=False)
@@ -1198,60 +1309,107 @@ else:
1198
  ob_data = df_op[df_op["is_ob"]]
1199
  coal_data = df_op[df_op["is_coal"]]
1200
 
1201
- # PERBAIKAN 2: FUNGSI BARU Hitung slope berdasarkan DAILY rata-rata
1202
- def get_top10_daily_slope(data, top_n=10):
1203
- if data.empty or col_operator is None or col_operator not in data.columns:
 
 
 
 
 
1204
  return pd.DataFrame()
1205
 
1206
- # Hitung jumlah event per operator per hari
1207
- daily = data.groupby([col_operator, "date"]).size().reset_index(name="daily_count")
1208
-
1209
  metrics = []
1210
- for nik, grp in daily.groupby(col_operator):
1211
- if pd.isna(nik):
1212
- continue
1213
- # Urutkan berdasarkan tanggal (pastikan sorted)
1214
- grp = grp.sort_values("date")
1215
- # Ambil hanya tanggal & count
1216
- dates = pd.to_datetime(grp["date"]).dt.date
1217
- counts = grp["daily_count"].values
1218
- n_days = len(counts)
1219
-
1220
- # Rata-rata harian → untuk ranking & Y-axis
1221
- daily_avg = np.mean(counts)
1222
- total_events = int(np.sum(counts))
1223
-
1224
- # Slope: regresi linier terhadap urutan hari (0, 1, 2, ...)
1225
- if n_days >= 2:
1226
- x = np.arange(n_days) # [0, 1, 2, ..., n-1]
1227
- y = counts
1228
- slope, intercept = np.polyfit(x, y, 1) # Lebih stabil daripada manual
1229
- else:
1230
- slope = 0.0
1231
-
1232
- metrics.append({
1233
- col_operator: nik,
1234
- "daily_avg": daily_avg,
1235
- "slope": slope,
1236
- "total_events": total_events,
1237
- "n_days": n_days
1238
- })
1239
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1240
  if not metrics:
 
1241
  return pd.DataFrame()
1242
-
1243
- result_df = pd.DataFrame(metrics)
1244
- # ✅ Ambil top N berdasarkan daily_avg (descending)
1245
- return result_df.nlargest(top_n, "daily_avg")
1246
 
1247
- # Panggil fungsi baru
1248
- top_ob = get_top10_daily_slope(ob_data, top_n=10)
1249
- top_coal = get_top10_daily_slope(coal_data, top_n=10)
1250
- all_ob = get_top10_daily_slope(ob_data, top_n=1000) # semua, tanpa batas
1251
- all_coal = get_top10_daily_slope(coal_data, top_n=1000)
1252
 
1253
  # ===============================================================
1254
- # LEGEND (tetap sama sudah sesuai preferensi Anda)
1255
  # ===============================================================
1256
  st.subheader("Risk Gradient Legend")
1257
  st.markdown("""
@@ -1264,34 +1422,34 @@ else:
1264
  </div>
1265
  <div class="legend-item">
1266
  <div class="legend-color" style="background-color: #e57373;"></div>
1267
- <span>High Risk (1.01.5)</span>
1268
  </div>
1269
  <div class="legend-item">
1270
  <div class="legend-color" style="background-color: #ef9a9a;"></div>
1271
- <span>Moderate Risk (0.51.0)</span>
1272
  </div>
1273
  <div class="legend-item">
1274
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1275
- <span>Slight Risk (00.5)</span>
1276
  </div>
1277
  </div>
1278
  <div class="legend-box">
1279
  <div class="legend-title">Improving Trends (Negative Slope):</div>
1280
  <div class="legend-item">
1281
  <div class="legend-color" style="background-color: #388e3c;"></div>
1282
- <span>Excellent Improvement (≤−1.5)</span>
1283
  </div>
1284
  <div class="legend-item">
1285
  <div class="legend-color" style="background-color: #81c784;"></div>
1286
- <span>Great Improvement (1.5 to 1.0)</span>
1287
  </div>
1288
  <div class="legend-item">
1289
  <div class="legend-color" style="background-color: #a5d6a7;"></div>
1290
- <span>Good Improvement (1.0 to 0.5)</span>
1291
  </div>
1292
  <div class="legend-item">
1293
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1294
- <span>Slight Improvement (0.5 to 0)</span>
1295
  </div>
1296
  </div>
1297
  <div class="legend-box">
@@ -1301,66 +1459,89 @@ else:
1301
  <span>Stable (0)</span>
1302
  </div>
1303
  <br>
1304
- <i>Note: Appears when only one day observed or flat trend.</i>
1305
  </div>
1306
  </div>
1307
  """, unsafe_allow_html=True)
1308
 
1309
  # ===============================================================
1310
- # PLOT FUNCTION DIPERBAIKI: daily_avg + tooltip harian
1311
  # ===============================================================
1312
- def plot_daily_risk_chart(data, title):
1313
  if data.empty:
1314
  fig = go.Figure()
1315
- fig.add_annotation(text="No Data", x=0.5, y=0.5, showarrow=False, font_size=16)
1316
- fig.update_layout(height=350, title=title, title_x=0.5)
 
 
 
 
 
 
1317
  return fig
1318
 
1319
- # Urutkan berdasarkan daily_avg tertinggi
1320
- data_sorted = data.sort_values('daily_avg', ascending=False)
1321
 
1322
- # Warna tetap sesuai preferensi Anda (4-level merah/hijau/abu-abu)
1323
  def get_color(slope):
1324
  if slope == 0:
1325
- return "#95a5a6"
1326
  elif slope > 0:
1327
- if slope < 0.5: return "#ffcdd2"
1328
- elif slope < 1.0: return "#ef9a9a"
1329
- elif slope < 1.5: return "#e57373"
1330
- else: return "#d32f2f"
 
 
 
 
 
1331
  else: # slope < 0
1332
- if slope > -0.5: return "#c8e6c9"
1333
- elif slope > -1.0: return "#a5d6a7"
1334
- elif slope > -1.5: return "#81c784"
1335
- else: return "#388e3c"
 
 
 
 
 
1336
 
1337
  colors = [get_color(s) for s in data_sorted["slope"]]
1338
 
 
1339
  bar_trace = go.Bar(
1340
  x=data_sorted[col_operator].astype(str),
1341
- y=data_sorted["daily_avg"],
1342
- marker=dict(color=colors, line=dict(width=2, color="rgba(0,0,0,0.2)")),
1343
- text=[f"{v:.1f}" for v in data_sorted["daily_avg"]],
 
 
 
1344
  textposition="outside",
1345
  hovertemplate=(
1346
  "<b>%{x}</b><br>" +
1347
- "Daily Avg: %{y:.2f}<br>" + # ✅ Weekly → Daily
1348
  "Trend Slope: %{customdata[0]:+.3f}<br>" +
1349
  "Total Events: %{customdata[1]}<br>" +
1350
- "Days Active: %{customdata[2]}<br>" + # ✅ Weeks → Days
1351
  "<extra></extra>"
1352
  ),
1353
- customdata=np.stack([data_sorted["slope"], data_sorted["total_events"], data_sorted["n_days"]], axis=-1)
1354
  )
1355
 
 
1356
  fig = go.Figure(bar_trace)
 
 
1357
  fig.update_layout(
1358
  title=f"<b>{title}</b>",
1359
- title_x=0.5,
1360
  height=450,
1361
  margin=dict(l=50, r=20, t=60, b=120),
1362
  xaxis_title="<b>Operator ID</b>",
1363
- yaxis_title="<b>Daily Avg Events</b>", # ✅ Weekly → Daily
1364
  font=dict(family="Segoe UI", size=12),
1365
  bargap=0.3,
1366
  plot_bgcolor="rgba(0,0,0,0)",
@@ -1369,32 +1550,37 @@ else:
1369
  return fig
1370
 
1371
  # ===============================================================
1372
- # TAMPILKAN BAR CHART — SUDAH HARIAN
1373
  # ===============================================================
1374
  col1, col2 = st.columns(2)
1375
  with col1:
1376
- st.plotly_chart(plot_daily_risk_chart(top_ob, "OB HAULER Operators (Risk Gradient)"), use_container_width=True)
1377
  with col2:
1378
- st.plotly_chart(plot_daily_risk_chart(top_coal, "HAULING COAL Operators (Risk Gradient)"), use_container_width=True)
1379
 
1380
  # ===============================================================
1381
- # AI INSIGHTS DIPERBAIKI: Weekly Daily
1382
  # ===============================================================
 
 
 
 
1383
  col_insight1, col_insight2 = st.columns(2)
1384
 
 
1385
  with col_insight1:
1386
  if not top_ob.empty:
1387
  st.markdown("### OB HAULER Analysis")
1388
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1389
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1390
- ob_avg_risk = top_ob['daily_avg'].mean() # ✅ weekly_avg → daily_avg
1391
- ob_max_risk = top_ob['daily_avg'].max() # ✅ weekly_avg → daily_avg
1392
  ob_insights = []
1393
  if ob_worsening > ob_improving:
1394
  ob_insights.append(f"{ob_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends, indicating potential fatigue issues in this fleet type.")
1395
  else:
1396
  ob_insights.append(f"{ob_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>, suggesting effective fatigue management strategies.")
1397
- ob_insights.append(f"Average risk level among top 10 operators is {ob_avg_risk:.2f} events per day with maximum {ob_max_risk:.2f}.")
1398
 
1399
  for insight in ob_insights:
1400
  st.markdown(f"""
@@ -1406,19 +1592,20 @@ else:
1406
  else:
1407
  st.info("No OB HAULER data for analysis.")
1408
 
 
1409
  with col_insight2:
1410
  if not top_coal.empty:
1411
  st.markdown("### HAULING COAL Analysis")
1412
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1413
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1414
- coal_avg_risk = top_coal['daily_avg'].mean() # ✅ weekly_avg → daily_avg
1415
- coal_max_risk = top_coal['daily_avg'].max() # ✅ weekly_avg → daily_avg
1416
  coal_insights = []
1417
  if coal_worsening > coal_improving:
1418
  coal_insights.append(f"{coal_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends, requiring immediate attention.")
1419
  else:
1420
  coal_insights.append(f"{coal_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>, indicating positive trends in safety management.")
1421
- coal_insights.append(f"Average risk level among top 10 operators is {coal_avg_risk:.2f} events per day with maximum {coal_max_risk:.2f}.")
1422
 
1423
  for insight in coal_insights:
1424
  st.markdown(f"""
@@ -1431,45 +1618,51 @@ else:
1431
  st.info("No HAULING COAL data for analysis.")
1432
 
1433
  # ===============================================================
1434
- # AI RECOMMENDATIONS DIPERBAIKI: threshold berdasarkan daily_avg
1435
  # ===============================================================
 
 
 
 
 
 
1436
  col_rec1, col_rec2 = st.columns(2)
1437
 
1438
- def generate_recommendations_daily(top_ob, top_coal):
1439
  recommendations = {}
1440
  if not top_ob.empty:
1441
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1442
- ob_avg_risk = top_ob['daily_avg'].mean()
1443
- # threshold disesuaikan: daily avg > 2 = high (misal: >2 event/hari = perlu perhatian)
1444
- if ob_worsening > 5:
1445
- recommendations['ob'] = "Implement immediate fatigue monitoring protocols for operators showing worsening daily trends."
1446
- reason_ob = "High percentage of operators with increasing daily fatigue events suggests emerging risk patterns."
1447
- elif ob_avg_risk > 2.0:
1448
- recommendations['ob'] = "Consider workload redistribution to reduce daily fatigue exposure."
1449
- reason_ob = f"Average daily fatigue events ({ob_avg_risk:.2f}) exceeds safe threshold (2.0 events/day)."
1450
  else:
1451
- recommendations['ob'] = "Continue current safety protocols with enhanced daily monitoring."
1452
- reason_ob = "Stable daily fatigue profile maintain vigilance for early trend shifts."
1453
  recommendations['ob_reason'] = reason_ob
1454
 
1455
  if not top_coal.empty:
1456
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1457
- coal_avg_risk = top_coal['daily_avg'].mean()
1458
- if coal_worsening > 5:
1459
- recommendations['coal'] = "Implement immediate fatigue monitoring protocols for operators showing worsening daily trends."
1460
- reason_coal = "High percentage of operators with increasing daily fatigue events requires urgent intervention."
1461
- elif coal_avg_risk > 2.0:
1462
- recommendations['coal'] = "Review task allocation to reduce daily fatigue burden."
1463
- reason_coal = f"Average daily fatigue events ({coal_avg_risk:.2f}) exceeds safe threshold (2.0 events/day)."
1464
  else:
1465
- recommendations['coal'] = "Continue current safety protocols with enhanced daily monitoring."
1466
- reason_coal = "Stable daily fatigue profile maintain proactive monitoring."
1467
  recommendations['coal_reason'] = reason_coal
1468
 
1469
  return recommendations
1470
 
1471
- ai_recommendations = generate_recommendations_daily(top_ob, top_coal)
1472
 
 
1473
  with col_rec1:
1474
  if 'ob' in ai_recommendations:
1475
  st.markdown("### OB HAULER Recommendations")
@@ -1483,6 +1676,7 @@ else:
1483
  else:
1484
  st.info("No OB HAULER recommendations generated.")
1485
 
 
1486
  with col_rec2:
1487
  if 'coal' in ai_recommendations:
1488
  st.markdown("### HAULING COAL Recommendations")
@@ -1497,11 +1691,12 @@ else:
1497
  st.info("No HAULING COAL recommendations generated.")
1498
 
1499
  except Exception as e:
1500
- st.error(f"Error in Daily Operator Fatigue Analysis: {str(e)}")
1501
  st.code(f"Error: {e}", language="python")
1502
 
1503
 
1504
 
 
1505
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1506
  st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
1507
 
 
1155
 
1156
  # ... (kode sebelumnya tetap sama) ...
1157
 
1158
+ # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
1159
  st.subheader("OBJECTIVE 5: See your team’s fatigue risk gradient at a glance!")
1160
+ # Custom CSS untuk tampilan ala market saham yang sangat fancy dan profesional
 
1161
  st.markdown("""
1162
  <style>
1163
+ .big-title {
1164
+ font-size: 28px;
1165
+ font-weight: bold;
1166
+ color: #ffffff;
1167
+ text-align: center;
1168
+ margin-bottom: 10px;
1169
+ background: linear-gradient(135deg, #2c3e50, #1a252c);
1170
+ padding: 15px;
1171
+ border-radius: 10px;
1172
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
1173
+ }
1174
+ .subnote {
1175
+ font-size: 16px;
1176
+ color: #7f8c8d;
1177
+ text-align: center;
1178
+ margin-bottom: 20px;
1179
+ }
1180
+ .section-divider {
1181
+ height: 2px;
1182
+ background: linear-gradient(to right, #3498db, #2ecc71, #f1c40f, #e74c3c);
1183
+ margin: 20px 0;
1184
+ }
1185
+ .legend-container {
1186
+ display: flex;
1187
+ gap: 15px;
1188
+ margin: 15px 0;
1189
+ }
1190
+ .legend-box {
1191
+ background: white;
1192
+ border: 1px solid #ddd;
1193
+ border-radius: 8px;
1194
+ padding: 15px;
1195
+ flex: 1;
1196
+ min-width: 300px;
1197
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
1198
+ }
1199
+ .legend-title {
1200
+ font-weight: bold;
1201
+ color: #2c3e50;
1202
+ margin-bottom: 10px;
1203
+ font-size: 14px;
1204
+ border-bottom: 1px solid #eee;
1205
+ padding-bottom: 5px;
1206
+ }
1207
+ .legend-item {
1208
+ display: flex;
1209
+ align-items: center;
1210
+ margin: 5px 0;
1211
+ font-size: 12px;
1212
+ }
1213
+ .legend-color {
1214
+ width: 18px;
1215
+ height: 18px;
1216
+ border-radius: 3px;
1217
+ margin-right: 8px;
1218
+ border: 1px solid #ccc;
1219
+ }
1220
+ .ai-insight-box {
1221
+ background: #f8f9fa;
1222
+ border: 1px solid #dee2e6;
1223
+ border-radius: 8px;
1224
+ padding: 15px;
1225
+ margin: 10px 0;
1226
+ color: #2c3e50;
1227
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1228
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1229
+ }
1230
+ .ai-insight-title {
1231
+ font-weight: bold;
1232
+ color: #2c3e50;
1233
+ margin-bottom: 8px;
1234
+ font-size: 14px;
1235
+ background: #e9ecef;
1236
+ padding: 8px;
1237
+ border-radius: 5px;
1238
+ border-left: 4px solid #495057;
1239
+ }
1240
+ .trend-up {
1241
+ color: #e74c3c;
1242
+ font-weight: bold;
1243
+ }
1244
+ .trend-down {
1245
+ color: #27ae60;
1246
+ font-weight: bold;
1247
+ }
1248
+ .recommendation-box {
1249
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1250
+ border: 1px solid #4a5568;
1251
+ border-radius: 8px;
1252
+ padding: 15px;
1253
+ margin: 10px 0;
1254
+ color: white;
1255
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1256
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1257
+ }
1258
+ .recommendation-title {
1259
+ font-weight: bold;
1260
+ color: white;
1261
+ margin-bottom: 8px;
1262
+ font-size: 14px;
1263
+ background: rgba(255,255,255,0.2);
1264
+ padding: 8px;
1265
+ border-radius: 5px;
1266
+ border-left: 4px solid white;
1267
+ }
1268
+ .recommendation-reason {
1269
+ font-size: 12px;
1270
+ margin-top: 10px;
1271
+ padding: 8px;
1272
+ background: rgba(255,255,255,0.1);
1273
+ border-radius: 5px;
1274
+ border-left: 3px solid rgba(255,255,255,0.3);
1275
+ }
1276
  </style>
1277
  """, unsafe_allow_html=True)
1278
 
1279
  # ===============================================================
1280
+ # LOGIC UTAMA
1281
  # ===============================================================
1282
  if df.empty:
1283
  st.info("No data available after applying filters.")
 
1294
  st.info("No operator data after filtering.")
1295
  st.stop()
1296
 
1297
+ # Pastikan col_operator bukan None sebelum digunakan
1298
  if col_operator is None:
1299
+ st.error(f"Operator column could not be auto-detected. Please check your data.")
1300
  st.stop()
1301
 
1302
+ df_op["year_week"] = df_op["start"].dt.strftime("%Y-W%U")
 
1303
 
1304
+ # Fuzzy match fleet names
1305
  fleet_clean = df_op[col_fleet_type].str.strip().str.upper()
1306
  df_op["is_ob"] = fleet_clean.str.contains(r"OB HAULLER", na=False)
1307
  df_op["is_coal"] = fleet_clean.str.contains(r"HAULING COAL", na=False)
 
1309
  ob_data = df_op[df_op["is_ob"]]
1310
  coal_data = df_op[df_op["is_coal"]]
1311
 
1312
+ # Fungsi hitung top 10 (untuk bar chart) - berdasarkan weekly avg events tertinggi
1313
+ def get_top10_with_slope(data):
1314
+ if data.empty:
1315
+ st.warning("Data is empty in get_top10_with_slope.")
1316
+ return pd.DataFrame()
1317
+ # Pastikan col_operator tidak None dan ada di data
1318
+ if col_operator is None or col_operator not in data.columns:
1319
+ st.error(f"Operator column '{col_operator}' not found in data subset for get_top10.")
1320
  return pd.DataFrame()
1321
 
1322
+ weekly = data.groupby([col_operator, "year_week"]).size().reset_index(name="weekly_sum")
 
 
1323
  metrics = []
1324
+ try:
1325
+ for nik, grp in weekly.groupby(col_operator):
1326
+ # Lewati jika nik adalah None
1327
+ if pd.isna(nik):
1328
+ continue
1329
+ grp = grp.sort_values("year_week")
1330
+ counts = grp["weekly_sum"].values
1331
+ weeks = np.arange(len(counts))
1332
+ weekly_avg = counts.mean()
1333
+ total_events = counts.sum()
1334
+ n_weeks = len(counts)
1335
+ if n_weeks >= 2:
1336
+ x_mean = weeks.mean()
1337
+ y_mean = counts.mean()
1338
+ numerator = np.sum((weeks - x_mean) * (counts - y_mean))
1339
+ denominator = np.sum((weeks - x_mean) ** 2)
1340
+ slope = numerator / denominator if denominator != 0 else 0.0
1341
+ else:
1342
+ slope = 0.0
1343
+ metrics.append({
1344
+ col_operator: nik,
1345
+ "weekly_avg": weekly_avg,
1346
+ "slope": slope,
1347
+ "total_events": total_events,
1348
+ "n_weeks": n_weeks
1349
+ })
1350
+ except KeyError as e:
1351
+ st.error(f"KeyError in get_top10_with_slope: {e}. This might happen if the operator column contains invalid data types or unexpected values.")
1352
+ return pd.DataFrame()
1353
+ # Ambil top 10 berdasarkan weekly_avg (descending order)
1354
+ if not metrics:
1355
+ st.warning("No valid operator data found for slope calculation in get_top10.")
1356
+ return pd.DataFrame()
1357
+ return pd.DataFrame(metrics).nlargest(10, "weekly_avg")
1358
+
1359
+ top_ob = get_top10_with_slope(ob_data)
1360
+ top_coal = get_top10_with_slope(coal_data)
1361
+
1362
+ # Fungsi hitung semua operator (untuk summary)
1363
+ def get_all_operators_with_slope(data):
1364
+ if data.empty:
1365
+ st.warning("Data is empty in get_all_operators_with_slope.")
1366
+ return pd.DataFrame()
1367
+ # Pastikan col_operator tidak None dan ada di data
1368
+ if col_operator is None or col_operator not in data.columns:
1369
+ st.error(f"Operator column '{col_operator}' not found in data subset for get_all.")
1370
+ return pd.DataFrame()
1371
+
1372
+ weekly = data.groupby([col_operator, "year_week"]).size().reset_index(name="weekly_sum")
1373
+ metrics = []
1374
+ try:
1375
+ for nik, grp in weekly.groupby(col_operator):
1376
+ # Lewati jika nik adalah None
1377
+ if pd.isna(nik):
1378
+ continue
1379
+ grp = grp.sort_values("year_week")
1380
+ counts = grp["weekly_sum"].values
1381
+ weeks = np.arange(len(counts))
1382
+ weekly_avg = counts.mean()
1383
+ total_events = counts.sum()
1384
+ n_weeks = len(counts)
1385
+ if n_weeks >= 2:
1386
+ x_mean = weeks.mean()
1387
+ y_mean = counts.mean()
1388
+ numerator = np.sum((weeks - x_mean) * (counts - y_mean))
1389
+ denominator = np.sum((weeks - x_mean) ** 2)
1390
+ slope = numerator / denominator if denominator != 0 else 0.0
1391
+ else:
1392
+ slope = 0.0
1393
+ metrics.append({
1394
+ col_operator: nik,
1395
+ "weekly_avg": weekly_avg,
1396
+ "slope": slope,
1397
+ "total_events": total_events,
1398
+ "n_weeks": n_weeks
1399
+ })
1400
+ except KeyError as e:
1401
+ st.error(f"KeyError in get_all_operators_with_slope: {e}. This might happen if the operator column contains invalid data types or unexpected values.")
1402
+ return pd.DataFrame()
1403
  if not metrics:
1404
+ st.warning("No valid operator data found for slope calculation in get_all.")
1405
  return pd.DataFrame()
1406
+ return pd.DataFrame(metrics)
 
 
 
1407
 
1408
+ all_ob = get_all_operators_with_slope(ob_data)
1409
+ all_coal = get_all_operators_with_slope(coal_data)
 
 
 
1410
 
1411
  # ===============================================================
1412
+ # LEGEND DI LUAR CHART - 3 KOTAK DENGAN UKURAN SAMA
1413
  # ===============================================================
1414
  st.subheader("Risk Gradient Legend")
1415
  st.markdown("""
 
1422
  </div>
1423
  <div class="legend-item">
1424
  <div class="legend-color" style="background-color: #e57373;"></div>
1425
+ <span>High Risk (1.0-1.5)</span>
1426
  </div>
1427
  <div class="legend-item">
1428
  <div class="legend-color" style="background-color: #ef9a9a;"></div>
1429
+ <span>Moderate Risk (0.5-1.0)</span>
1430
  </div>
1431
  <div class="legend-item">
1432
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1433
+ <span>Slight Risk (0-0.5)</span>
1434
  </div>
1435
  </div>
1436
  <div class="legend-box">
1437
  <div class="legend-title">Improving Trends (Negative Slope):</div>
1438
  <div class="legend-item">
1439
  <div class="legend-color" style="background-color: #388e3c;"></div>
1440
+ <span>Excellent Improvement (≤-1.5)</span>
1441
  </div>
1442
  <div class="legend-item">
1443
  <div class="legend-color" style="background-color: #81c784;"></div>
1444
+ <span>Great Improvement (-1.5 to -1.0)</span>
1445
  </div>
1446
  <div class="legend-item">
1447
  <div class="legend-color" style="background-color: #a5d6a7;"></div>
1448
+ <span>Good Improvement (-1.0 to -0.5)</span>
1449
  </div>
1450
  <div class="legend-item">
1451
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1452
+ <span>Slight Improvement (-0.5-0)</span>
1453
  </div>
1454
  </div>
1455
  <div class="legend-box">
 
1459
  <span>Stable (0)</span>
1460
  </div>
1461
  <br>
1462
+ <i>Note: Only appears when operator data shows consistent behavior within a single week observation period.</i>
1463
  </div>
1464
  </div>
1465
  """, unsafe_allow_html=True)
1466
 
1467
  # ===============================================================
1468
+ # PLOT FUNCTION (Bar Chart with Risk Gradient Colors) - PERBAIKAN DI SINI
1469
  # ===============================================================
1470
+ def plot_chart(data, title):
1471
  if data.empty:
1472
  fig = go.Figure()
1473
+ fig.add_annotation(
1474
+ text="No Data",
1475
+ x=0.5, y=0.5,
1476
+ showarrow=False,
1477
+ font_size=16
1478
+ )
1479
+ # Gunakan update_layout untuk menetapkan judul
1480
+ fig.update_layout(height=350, title=title)
1481
  return fig
1482
 
1483
+ # Urutkan data berdasarkan weekly_avg dari besar ke kecil
1484
+ data_sorted = data.sort_values('weekly_avg', ascending=False)
1485
 
1486
+ # Kategorisasi warna berdasarkan slope dengan gradasi yang berbeda
1487
  def get_color(slope):
1488
  if slope == 0:
1489
+ return "#95a5a6" # Abu-abu (Stabil)
1490
  elif slope > 0:
1491
+ # Gradasi merah untuk slope positif
1492
+ if slope < 0.5:
1493
+ return "#ffcdd2" # Merah sangat muda
1494
+ elif slope < 1.0:
1495
+ return "#ef9a9a" # Merah muda
1496
+ elif slope < 1.5:
1497
+ return "#e57373" # Merah sedang
1498
+ else:
1499
+ return "#d32f2f" # Merah gelap
1500
  else: # slope < 0
1501
+ # Gradasi hijau untuk slope negatif
1502
+ if slope > -0.5:
1503
+ return "#c8e6c9" # Hijau sangat muda
1504
+ elif slope > -1.0:
1505
+ return "#a5d6a7" # Hijau muda
1506
+ elif slope > -1.5:
1507
+ return "#81c784" # Hijau sedang
1508
+ else:
1509
+ return "#388e3c" # Hijau gelap
1510
 
1511
  colors = [get_color(s) for s in data_sorted["slope"]]
1512
 
1513
+ # Buat trace bar, TANPA argumen 'title'
1514
  bar_trace = go.Bar(
1515
  x=data_sorted[col_operator].astype(str),
1516
+ y=data_sorted["weekly_avg"],
1517
+ marker=dict(
1518
+ color=colors,
1519
+ line=dict(width=2, color="rgba(0,0,0,0.2)")
1520
+ ),
1521
+ text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1522
  textposition="outside",
1523
  hovertemplate=(
1524
  "<b>%{x}</b><br>" +
1525
+ "Weekly Avg: %{y:.2f}<br>" +
1526
  "Trend Slope: %{customdata[0]:+.3f}<br>" +
1527
  "Total Events: %{customdata[1]}<br>" +
1528
+ "Weeks Active: %{customdata[2]}<br>" +
1529
  "<extra></extra>"
1530
  ),
1531
+ customdata=np.stack([data_sorted["slope"], data_sorted["total_events"], data_sorted["n_weeks"]], axis=-1)
1532
  )
1533
 
1534
+ # Buat figure dan tambahkan trace
1535
  fig = go.Figure(bar_trace)
1536
+
1537
+ # Gunakan update_layout untuk menetapkan judul dan layout lainnya
1538
  fig.update_layout(
1539
  title=f"<b>{title}</b>",
1540
+ title_x=0.5, # Pusatkan judul
1541
  height=450,
1542
  margin=dict(l=50, r=20, t=60, b=120),
1543
  xaxis_title="<b>Operator ID</b>",
1544
+ yaxis_title="<b>Weekly Avg Events</b>",
1545
  font=dict(family="Segoe UI", size=12),
1546
  bargap=0.3,
1547
  plot_bgcolor="rgba(0,0,0,0)",
 
1550
  return fig
1551
 
1552
  # ===============================================================
1553
+ # TAMPILKAN BAR CHART
1554
  # ===============================================================
1555
  col1, col2 = st.columns(2)
1556
  with col1:
1557
+ st.plotly_chart(plot_chart(top_ob, "OB HAULER Operators (Risk Gradient)"), use_container_width=True)
1558
  with col2:
1559
+ st.plotly_chart(plot_chart(top_coal, "HAULING COAL Operators (Risk Gradient)"), use_container_width=True)
1560
 
1561
  # ===============================================================
1562
+ # AI INSIGHTS - DIBEDAKAN UNTUK OB HAULER DAN COAL HAULING - SEKARANG BERSEBELAHAN
1563
  # ===============================================================
1564
+ # st.markdown("---")
1565
+ # st.subheader("Data Insight Automation")
1566
+
1567
+ # Gunakan kolom untuk menampilkan analisis secara bersebelahan
1568
  col_insight1, col_insight2 = st.columns(2)
1569
 
1570
+ # Insight untuk OB HAULER - Ditampilkan di kolom kiri
1571
  with col_insight1:
1572
  if not top_ob.empty:
1573
  st.markdown("### OB HAULER Analysis")
1574
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1575
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1576
+ ob_avg_risk = top_ob['weekly_avg'].mean()
1577
+ ob_max_risk = top_ob['weekly_avg'].max()
1578
  ob_insights = []
1579
  if ob_worsening > ob_improving:
1580
  ob_insights.append(f"{ob_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends, indicating potential fatigue issues in this fleet type.")
1581
  else:
1582
  ob_insights.append(f"{ob_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>, suggesting effective fatigue management strategies.")
1583
+ ob_insights.append(f"Average risk level among top 10 operators is {ob_avg_risk:.2f} events per week with maximum {ob_max_risk:.2f}.")
1584
 
1585
  for insight in ob_insights:
1586
  st.markdown(f"""
 
1592
  else:
1593
  st.info("No OB HAULER data for analysis.")
1594
 
1595
+ # Insight untuk HAULING COAL - Ditampilkan di kolom kanan
1596
  with col_insight2:
1597
  if not top_coal.empty:
1598
  st.markdown("### HAULING COAL Analysis")
1599
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1600
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1601
+ coal_avg_risk = top_coal['weekly_avg'].mean()
1602
+ coal_max_risk = top_coal['weekly_avg'].max()
1603
  coal_insights = []
1604
  if coal_worsening > coal_improving:
1605
  coal_insights.append(f"{coal_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends, requiring immediate attention.")
1606
  else:
1607
  coal_insights.append(f"{coal_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>, indicating positive trends in safety management.")
1608
+ coal_insights.append(f"Average risk level among top 10 operators is {coal_avg_risk:.2f} events per week with maximum {coal_max_risk:.2f}.")
1609
 
1610
  for insight in coal_insights:
1611
  st.markdown(f"""
 
1618
  st.info("No HAULING COAL data for analysis.")
1619
 
1620
  # ===============================================================
1621
+ # AI RECOMMENDATIONS - JUGA BERSEBELAHAN
1622
  # ===============================================================
1623
+ # st.markdown("---")
1624
+
1625
+
1626
+ # st.subheader("Recommendations for Objective 5")
1627
+
1628
+ # Gunakan kolom untuk menampilkan rekomendasi secara bersebelahan
1629
  col_rec1, col_rec2 = st.columns(2)
1630
 
1631
+ def generate_recommendations(top_ob, top_coal):
1632
  recommendations = {}
1633
  if not top_ob.empty:
1634
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1635
+ ob_avg_risk = top_ob['weekly_avg'].mean()
1636
+ if ob_worsening > 5: # Lebih dari setengah
1637
+ recommendations['ob'] = "Implement immediate fatigue monitoring protocols for operators showing worsening trends."
1638
+ reason_ob = "High percentage of operators showing increasing risk trends indicates potential systemic fatigue issues requiring immediate intervention."
1639
+ elif ob_avg_risk > 10: # High average risk
1640
+ recommendations['ob'] = "Consider workload redistribution to reduce average risk levels."
1641
+ reason_ob = "High average risk levels suggest operational adjustments are needed to maintain optimal safety standards."
 
1642
  else:
1643
+ recommendations['ob'] = "Continue current safety protocols with enhanced monitoring for early detection."
1644
+ reason_ob = "Stable risk profile indicates current protocols are effective, but continuous monitoring ensures early detection of potential issues."
1645
  recommendations['ob_reason'] = reason_ob
1646
 
1647
  if not top_coal.empty:
1648
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1649
+ coal_avg_risk = top_coal['weekly_avg'].mean()
1650
+ if coal_worsening > 5: # Lebih dari setengah
1651
+ recommendations['coal'] = "Implement immediate fatigue monitoring protocols for operators showing worsening trends."
1652
+ reason_coal = "High percentage of operators showing increasing risk trends indicates potential systemic fatigue issues requiring immediate intervention."
1653
+ elif coal_avg_risk > 10: # High average risk
1654
+ recommendations['coal'] = "Consider workload redistribution to reduce average risk levels."
1655
+ reason_coal = "High average risk levels suggest operational adjustments are needed to maintain optimal safety standards."
1656
  else:
1657
+ recommendations['coal'] = "Continue current safety protocols with enhanced monitoring for early detection."
1658
+ reason_coal = "Stable risk profile indicates current protocols are effective, but continuous monitoring ensures early detection of potential issues."
1659
  recommendations['coal_reason'] = reason_coal
1660
 
1661
  return recommendations
1662
 
1663
+ ai_recommendations = generate_recommendations(top_ob, top_coal)
1664
 
1665
+ # Recommendation untuk OB HAULER - Ditampilkan di kolom kiri
1666
  with col_rec1:
1667
  if 'ob' in ai_recommendations:
1668
  st.markdown("### OB HAULER Recommendations")
 
1676
  else:
1677
  st.info("No OB HAULER recommendations generated.")
1678
 
1679
+ # Recommendation untuk HAULING COAL - Ditampilkan di kolom kanan
1680
  with col_rec2:
1681
  if 'coal' in ai_recommendations:
1682
  st.markdown("### HAULING COAL Recommendations")
 
1691
  st.info("No HAULING COAL recommendations generated.")
1692
 
1693
  except Exception as e:
1694
+ st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1695
  st.code(f"Error: {e}", language="python")
1696
 
1697
 
1698
 
1699
+
1700
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1701
  st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
1702