SHELLAPANDIANGANHUNGING commited on
Commit
56b6a03
·
verified ·
1 Parent(s): 551db22

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -180
app.py CHANGED
@@ -1150,73 +1150,46 @@ except Exception as e:
1150
 
1151
 
1152
  # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
1153
- # ✅ OBJECTIVE 5 HEADER centered, white background, elegant
1154
- st.markdown("""
1155
- <h3 class="objective-header">OBJECTIVE 5: See your team’s Fatigue Hazard Profile!</h3>
1156
- """, unsafe_allow_html=True)
1157
 
1158
- # CUSTOM CSS — SEMUA STRUCTURE DIPERBAIKI SESUAI PREFERENSI ANDA
1159
  st.markdown("""
1160
  <style>
1161
- /* === MAIN OBJECTIVE HEADER === */
1162
- .objective-header {
1163
- font-size: 24px;
1164
- font-weight: bold;
1165
- color: #2c3e50;
1166
- text-align: center;
1167
- margin: 10px 0 20px 0;
1168
- background: white; /* ✅ white header background */
1169
- padding: 12px;
1170
- border-radius: 8px;
1171
- box-shadow: 0 3px 10px rgba(0,0,0,0.08);
1172
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1173
- }
1174
-
1175
- /* === BIG TITLE (e.g., for main section titles) === */
1176
  .big-title {
1177
- font-size: 26px;
1178
  font-weight: bold;
1179
- color: #2c3e50; /* ✅ not white on white */
1180
  text-align: center;
1181
- margin: 25px 0 15px 0;
1182
- background: white; /* ✅ white, not dark gradient */
1183
- padding: 14px;
1184
  border-radius: 10px;
1185
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
1186
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1187
  }
1188
-
1189
  .subnote {
1190
  font-size: 16px;
1191
  color: #7f8c8d;
1192
  text-align: center;
1193
  margin-bottom: 20px;
1194
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1195
  }
1196
-
1197
  .section-divider {
1198
  height: 2px;
1199
  background: linear-gradient(to right, #3498db, #2ecc71, #f1c40f, #e74c3c);
1200
- margin: 25px 0;
1201
  }
1202
-
1203
- /* === LEGEND === */
1204
  .legend-container {
1205
  display: flex;
1206
- gap: 20px;
1207
- flex-wrap: wrap;
1208
- justify-content: center;
1209
- margin: 20px 0;
1210
  }
1211
  .legend-box {
1212
- background: #f9f9f9;
1213
  border: 1px solid #ddd;
1214
- border-radius: 10px;
1215
- padding: 14px;
1216
- min-width: 280px;
1217
- max-width: 320px;
1218
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1219
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1220
  }
1221
  .legend-title {
1222
  font-weight: bold;
@@ -1229,27 +1202,25 @@ st.markdown("""
1229
  .legend-item {
1230
  display: flex;
1231
  align-items: center;
1232
- margin: 6px 0;
1233
- font-size: 13px;
1234
  }
1235
  .legend-color {
1236
  width: 18px;
1237
  height: 18px;
1238
  border-radius: 3px;
1239
- margin-right: 10px;
1240
  border: 1px solid #ccc;
1241
  }
1242
-
1243
- /* === AI INSIGHTS === */
1244
  .ai-insight-box {
1245
  background: #f8f9fa;
1246
  border: 1px solid #dee2e6;
1247
  border-radius: 8px;
1248
- padding: 16px;
1249
- margin: 12px 0;
1250
  color: #2c3e50;
1251
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1252
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
1253
  }
1254
  .ai-insight-title {
1255
  font-weight: bold;
@@ -1261,14 +1232,20 @@ st.markdown("""
1261
  border-radius: 5px;
1262
  border-left: 4px solid #495057;
1263
  }
1264
-
1265
- /* === RECOMMENDATIONS === */
 
 
 
 
 
 
1266
  .recommendation-box {
1267
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1268
  border: 1px solid #4a5568;
1269
  border-radius: 8px;
1270
- padding: 16px;
1271
- margin: 12px 0;
1272
  color: white;
1273
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1274
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
@@ -1284,34 +1261,18 @@ st.markdown("""
1284
  border-left: 4px solid white;
1285
  }
1286
  .recommendation-reason {
1287
- font-size: 13px;
1288
  margin-top: 10px;
1289
  padding: 8px;
1290
  background: rgba(255,255,255,0.1);
1291
  border-radius: 5px;
1292
  border-left: 3px solid rgba(255,255,255,0.3);
1293
  }
1294
-
1295
- /* === TRENDS === */
1296
- .trend-up {
1297
- color: #e74c3c;
1298
- font-weight: bold;
1299
- }
1300
- .trend-down {
1301
- color: #27ae60;
1302
- font-weight: bold;
1303
- }
1304
-
1305
- /* === PLOTLY AXIS LABELS — ensure English & readability */
1306
- .plotly-title {
1307
- font-family: 'Segoe UI', sans-serif;
1308
- }
1309
  </style>
1310
  """, unsafe_allow_html=True)
1311
 
1312
-
1313
  # ===============================================================
1314
- # LOGIC UTAMA — DIPERBAIKI: PENYINGKATAN NAMA OPERATOR
1315
  # ===============================================================
1316
  if df.empty:
1317
  st.info("No data available after applying filters.")
@@ -1322,12 +1283,7 @@ else:
1322
  st.warning("Required columns (operator, fleet_type, start) are missing.")
1323
  st.stop()
1324
 
1325
- # ✅ Shorten operator names: "John Doe" → "John"
1326
  df_op = df[[col_operator, col_fleet_type, "start"]].dropna()
1327
- if col_operator in df_op.columns:
1328
- df_op[col_operator] = df_op[col_operator].astype(str).str.strip()
1329
- df_op[col_operator] = df_op[col_operator].str.split().str[0] # ✅ only first name
1330
-
1331
  if df_op.empty:
1332
  st.info("No operator data after filtering.")
1333
  st.stop()
@@ -1346,8 +1302,6 @@ else:
1346
  ob_data = df_op[df_op["is_ob"]]
1347
  coal_data = df_op[df_op["is_coal"]]
1348
 
1349
- # [Fungsi get_top10_with_slope & get_all_operators_with_slope tetap sama — tidak berubah]
1350
-
1351
  def get_top10_with_slope(data):
1352
  if data.empty:
1353
  return pd.DataFrame()
@@ -1388,74 +1342,101 @@ else:
1388
  top_ob = get_top10_with_slope(ob_data)
1389
  top_coal = get_top10_with_slope(coal_data)
1390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1391
  all_ob = get_all_operators_with_slope(ob_data)
1392
  all_coal = get_all_operators_with_slope(coal_data)
1393
 
1394
  # ===============================================================
1395
- # LEGEND — UPDATED: Stable → One Time Event, Gray → Yellow (#FFD700)
1396
  # ===============================================================
1397
- st.markdown('<h3 class="big-title">Hazard Gradient Legend</h3>', unsafe_allow_html=True)
1398
  st.markdown("""
1399
  <div class="legend-container">
1400
- <!-- Worsening Trends -->
1401
  <div class="legend-box">
1402
  <div class="legend-title">Worsening Trends (Positive Slope):</div>
1403
  <div class="legend-item">
1404
  <div class="legend-color" style="background-color: #d32f2f;"></div>
1405
- <span>Very High Risk (≥ 1.5)</span>
1406
  </div>
1407
  <div class="legend-item">
1408
  <div class="legend-color" style="background-color: #e57373;"></div>
1409
- <span>High Risk (1.0 1.5)</span>
1410
  </div>
1411
  <div class="legend-item">
1412
  <div class="legend-color" style="background-color: #ef9a9a;"></div>
1413
- <span>Moderate Risk (0.5 1.0)</span>
1414
  </div>
1415
  <div class="legend-item">
1416
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1417
- <span>Slight Risk (0 0.5)</span>
1418
  </div>
1419
- <p style="font-size:12px; margin-top:10px; color:#666;"><i>Note: Increasing fatigue frequency over weeks.</i></p>
 
1420
  </div>
1421
-
1422
- <!-- Improving Trends -->
1423
  <div class="legend-box">
1424
  <div class="legend-title">Improving Trends (Negative Slope):</div>
1425
  <div class="legend-item">
1426
  <div class="legend-color" style="background-color: #388e3c;"></div>
1427
- <span>Excellent Improvement (≤ -1.5)</span>
1428
  </div>
1429
  <div class="legend-item">
1430
  <div class="legend-color" style="background-color: #81c784;"></div>
1431
- <span>Great Improvement (-1.5 to -1.0)</span>
1432
  </div>
1433
  <div class="legend-item">
1434
  <div class="legend-color" style="background-color: #a5d6a7;"></div>
1435
- <span>Good Improvement (-1.0 to -0.5)</span>
1436
  </div>
1437
  <div class="legend-item">
1438
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1439
- <span>Slight Improvement (-0.5 to 0)</span>
1440
  </div>
1441
- <p style="font-size:12px; margin-top:10px; color:#666;"><i>Note: Decreasing fatigue frequency over weeks.</i></p>
 
1442
  </div>
1443
-
1444
- <!-- One Time Events -->
1445
  <div class="legend-box">
1446
- <div class="legend-title">One Time Events (Zero Slope):</div>
1447
  <div class="legend-item">
1448
  <div class="legend-color" style="background-color: #FFD700;"></div>
1449
  <span>One Time Event (0)</span>
1450
  </div>
1451
- <p style="font-size:12px; margin-top:10px; color:#666;"><i>Note: Single-week activity (insufficient data for trend).</i></p>
 
1452
  </div>
1453
  </div>
1454
  """, unsafe_allow_html=True)
1455
 
1456
-
1457
  # ===============================================================
1458
- # PLOT FUNCTION — UPDATED: color for slope=0 is #FFD700 (yellow)
1459
  # ===============================================================
1460
  def plot_chart(data, title):
1461
  if data.empty:
@@ -1464,41 +1445,34 @@ else:
1464
  text="No Data",
1465
  x=0.5, y=0.5,
1466
  showarrow=False,
1467
- font_size=16,
1468
- font_color="#666"
1469
- )
1470
- fig.update_layout(
1471
- height=350,
1472
- title=dict(text=title, x=0.5, font=dict(size=18, family="Segoe UI")),
1473
- plot_bgcolor="rgba(0,0,0,0)",
1474
- paper_bgcolor="rgba(0,0,0,0)"
1475
  )
 
1476
  return fig
1477
 
1478
  data_sorted = data.sort_values('weekly_avg', ascending=False)
1479
 
1480
  def get_color(slope):
1481
- # ✅ One Time Event → Yellow (#FFD700)
1482
  if slope == 0:
1483
- return "#FFD700"
1484
  elif slope > 0:
1485
- if slope >= 1.5:
1486
- return "#d32f2f"
1487
- elif slope >= 1.0:
1488
- return "#e57373"
1489
- elif slope >= 0.5:
1490
- return "#ef9a9a"
1491
- else: # 0 < slope < 0.5
1492
  return "#ffcdd2"
 
 
 
 
 
 
1493
  else: # slope < 0
1494
- if slope <= -1.5:
1495
- return "#388e3c"
1496
- elif slope <= -1.0:
1497
- return "#81c784"
1498
- elif slope <= -0.5:
1499
- return "#a5d6a7"
1500
- else: # -0.5 < slope < 0
1501
  return "#c8e6c9"
 
 
 
 
 
 
1502
 
1503
  colors = [get_color(s) for s in data_sorted["slope"]]
1504
 
@@ -1507,15 +1481,14 @@ else:
1507
  y=data_sorted["weekly_avg"],
1508
  marker=dict(
1509
  color=colors,
1510
- line=dict(width=1.5, color="rgba(0,0,0,0.2)")
1511
  ),
1512
  text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1513
  textposition="outside",
1514
- textfont=dict(size=11, family="Segoe UI"),
1515
  hovertemplate=(
1516
  "<b>%{x}</b><br>" +
1517
  "Weekly Avg: %{y:.2f}<br>" +
1518
- "Trend Slope: %{customdata[0]:+.2f}<br>" +
1519
  "Total Events: %{customdata[1]}<br>" +
1520
  "Weeks Active: %{customdata[2]}<br>" +
1521
  "<extra></extra>"
@@ -1525,32 +1498,21 @@ else:
1525
 
1526
  fig = go.Figure(bar_trace)
1527
  fig.update_layout(
1528
- title=dict(
1529
- text=f"<b>{title}</b>",
1530
- x=0.5,
1531
- font=dict(size=18, family="Segoe UI", color="#2c3e50")
1532
- ),
1533
  height=450,
1534
- margin=dict(l=50, r=20, t=70, b=130),
1535
- xaxis_title=dict(text="<b>Operator</b>", font=dict(family="Segoe UI")),
1536
- yaxis_title=dict(text="<b>Weekly Avg Events</b>", font=dict(family="Segoe UI")),
1537
  font=dict(family="Segoe UI", size=12),
1538
  bargap=0.3,
1539
  plot_bgcolor="rgba(0,0,0,0)",
1540
  paper_bgcolor="rgba(0,0,0,0)",
1541
- xaxis=dict(
1542
- tickangle=45,
1543
- tickfont=dict(family="Segoe UI")
1544
- ),
1545
- yaxis=dict(
1546
- gridcolor="#eee"
1547
- )
1548
  )
1549
  return fig
1550
 
1551
-
1552
  # ===============================================================
1553
- # CHARTS — TITLES CENTERED VIA PLOTLY (already handled in plot_chart)
1554
  # ===============================================================
1555
  col1, col2 = st.columns(2)
1556
  with col1:
@@ -1558,37 +1520,33 @@ else:
1558
  with col2:
1559
  st.plotly_chart(plot_chart(top_coal, "HAULING COAL Operators (Hazard Gradient)"), use_container_width=True)
1560
 
1561
-
1562
  # ===============================================================
1563
- # AI INSIGHTS — English, no emoticon, shortened names already applied
1564
  # ===============================================================
1565
  col_insight1, col_insight2 = st.columns(2)
1566
 
1567
  with col_insight1:
1568
  if not top_ob.empty:
1569
- st.markdown('<h4 class="big-title">OB HAULER Analysis</h4>', unsafe_allow_html=True)
1570
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1571
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1572
  ob_one_time = len(top_ob[top_ob['slope'] == 0])
1573
  ob_avg_risk = top_ob['weekly_avg'].mean()
1574
  ob_max_risk = top_ob['weekly_avg'].max()
1575
-
1576
- insights = []
1577
  if ob_worsening > ob_improving:
1578
- insights.append(f"{ob_worsening} out of 10 top-risk operators show <span class='trend-up'>worsening</span> trends.")
1579
  else:
1580
- insights.append(f"{ob_improving} out of 10 top-risk operators show <span class='trend-down'>improvement</span>.")
1581
-
1582
  if ob_one_time > 0:
1583
- insights.append(f"{ob_one_time} operator(s) classified as <b>One Time Event</b>.")
1584
-
1585
- insights.append(f"Average risk: {ob_avg_risk:.2f} events/week (max: {ob_max_risk:.2f}).")
1586
 
1587
- for txt in insights:
1588
  st.markdown(f"""
1589
  <div class="ai-insight-box">
1590
- <div class="ai-insight-title">Hazard Summary</div>
1591
- <p>{txt}</p>
1592
  </div>
1593
  """, unsafe_allow_html=True)
1594
  else:
@@ -1596,38 +1554,36 @@ else:
1596
 
1597
  with col_insight2:
1598
  if not top_coal.empty:
1599
- st.markdown('<h4 class="big-title">HAULING COAL Analysis</h4>', unsafe_allow_html=True)
1600
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1601
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1602
  coal_one_time = len(top_coal[top_coal['slope'] == 0])
1603
  coal_avg_risk = top_coal['weekly_avg'].mean()
1604
  coal_max_risk = top_coal['weekly_avg'].max()
1605
-
1606
- insights = []
1607
  if coal_worsening > coal_improving:
1608
- insights.append(f"{coal_worsening} out of 10 top-risk operators show <span class='trend-up'>worsening</span> trends.")
1609
  else:
1610
- insights.append(f"{coal_improving} out of 10 top-risk operators show <span class='trend-down'>improvement</span>.")
1611
-
1612
  if coal_one_time > 0:
1613
- insights.append(f"{coal_one_time} operator(s) classified as <b>One Time Event</b>.")
1614
-
1615
- insights.append(f"Average risk: {coal_avg_risk:.2f} events/week (max: {coal_max_risk:.2f}).")
1616
 
1617
- for txt in insights:
1618
  st.markdown(f"""
1619
  <div class="ai-insight-box">
1620
- <div class="ai-insight-title">Hazard Summary</div>
1621
- <p>{txt}</p>
1622
  </div>
1623
  """, unsafe_allow_html=True)
1624
  else:
1625
  st.info("No HAULING COAL data for analysis.")
1626
 
1627
-
1628
  # ===============================================================
1629
- # RECOMMENDATIONS — unchanged logic, improved styling
1630
  # ===============================================================
 
 
1631
  def generate_recommendations(top_ob, top_coal):
1632
  rec = {}
1633
  if not top_ob.empty:
@@ -1636,7 +1592,7 @@ else:
1636
  avg = top_ob['weekly_avg'].mean()
1637
  if w > 5:
1638
  r = "Prioritize fatigue intervention for operators with worsening trends."
1639
- reason = "A high proportion of deteriorating operators signals emerging fatigue risks."
1640
  elif ot > 4:
1641
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1642
  reason = "Operators with single-week data cannot yield reliable trend analysis."
@@ -1655,7 +1611,7 @@ else:
1655
  avg = top_coal['weekly_avg'].mean()
1656
  if w > 5:
1657
  r = "Prioritize fatigue intervention for operators with worsening trends."
1658
- reason = "A high proportion of deteriorating operators signals emerging fatigue risks."
1659
  elif ot > 4:
1660
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1661
  reason = "Operators with single-week data cannot yield reliable trend analysis."
@@ -1671,10 +1627,9 @@ else:
1671
 
1672
  ai_rec = generate_recommendations(top_ob, top_coal)
1673
 
1674
- col_rec1, col_rec2 = st.columns(2)
1675
  with col_rec1:
1676
  if 'ob' in ai_rec:
1677
- st.markdown('<h4 class="big-title">OB HAULER Recommendations</h4>', unsafe_allow_html=True)
1678
  st.markdown(f"""
1679
  <div class="recommendation-box">
1680
  <div class="recommendation-title">Action Plan</div>
@@ -1687,7 +1642,7 @@ else:
1687
 
1688
  with col_rec2:
1689
  if 'coal' in ai_rec:
1690
- st.markdown('<h4 class="big-title">HAULING COAL Recommendations</h4>', unsafe_allow_html=True)
1691
  st.markdown(f"""
1692
  <div class="recommendation-box">
1693
  <div class="recommendation-title">Action Plan</div>
@@ -1700,7 +1655,7 @@ else:
1700
 
1701
  except Exception as e:
1702
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1703
- # st.exception(e) # uncomment for debugging
1704
 
1705
  import re # Tambahkan ini jika belum ada
1706
 
 
1150
 
1151
 
1152
  # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
1153
+ st.subheader("OBJECTIVE 5:See your team’s fatigue Fatigue Hazard Profile!")
 
 
 
1154
 
1155
+ # Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
1156
  st.markdown("""
1157
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1158
  .big-title {
1159
+ font-size: 28px;
1160
  font-weight: bold;
1161
+ color: #ffffff;
1162
  text-align: center;
1163
+ margin-bottom: 10px;
1164
+ background: linear-gradient(135deg, #2c3e50, #1a252c);
1165
+ padding: 15px;
1166
  border-radius: 10px;
1167
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
 
1168
  }
 
1169
  .subnote {
1170
  font-size: 16px;
1171
  color: #7f8c8d;
1172
  text-align: center;
1173
  margin-bottom: 20px;
 
1174
  }
 
1175
  .section-divider {
1176
  height: 2px;
1177
  background: linear-gradient(to right, #3498db, #2ecc71, #f1c40f, #e74c3c);
1178
+ margin: 20px 0;
1179
  }
 
 
1180
  .legend-container {
1181
  display: flex;
1182
+ gap: 15px;
1183
+ margin: 15px 0;
 
 
1184
  }
1185
  .legend-box {
1186
+ background: white;
1187
  border: 1px solid #ddd;
1188
+ border-radius: 8px;
1189
+ padding: 15px;
1190
+ flex: 1;
1191
+ min-width: 300px;
1192
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
 
1193
  }
1194
  .legend-title {
1195
  font-weight: bold;
 
1202
  .legend-item {
1203
  display: flex;
1204
  align-items: center;
1205
+ margin: 5px 0;
1206
+ font-size: 12px;
1207
  }
1208
  .legend-color {
1209
  width: 18px;
1210
  height: 18px;
1211
  border-radius: 3px;
1212
+ margin-right: 8px;
1213
  border: 1px solid #ccc;
1214
  }
 
 
1215
  .ai-insight-box {
1216
  background: #f8f9fa;
1217
  border: 1px solid #dee2e6;
1218
  border-radius: 8px;
1219
+ padding: 15px;
1220
+ margin: 10px 0;
1221
  color: #2c3e50;
1222
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1223
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1224
  }
1225
  .ai-insight-title {
1226
  font-weight: bold;
 
1232
  border-radius: 5px;
1233
  border-left: 4px solid #495057;
1234
  }
1235
+ .trend-up {
1236
+ color: #e74c3c;
1237
+ font-weight: bold;
1238
+ }
1239
+ .trend-down {
1240
+ color: #27ae60;
1241
+ font-weight: bold;
1242
+ }
1243
  .recommendation-box {
1244
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1245
  border: 1px solid #4a5568;
1246
  border-radius: 8px;
1247
+ padding: 15px;
1248
+ margin: 10px 0;
1249
  color: white;
1250
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1251
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
 
1261
  border-left: 4px solid white;
1262
  }
1263
  .recommendation-reason {
1264
+ font-size: 12px;
1265
  margin-top: 10px;
1266
  padding: 8px;
1267
  background: rgba(255,255,255,0.1);
1268
  border-radius: 5px;
1269
  border-left: 3px solid rgba(255,255,255,0.3);
1270
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1271
  </style>
1272
  """, unsafe_allow_html=True)
1273
 
 
1274
  # ===============================================================
1275
+ # LOGIC UTAMA
1276
  # ===============================================================
1277
  if df.empty:
1278
  st.info("No data available after applying filters.")
 
1283
  st.warning("Required columns (operator, fleet_type, start) are missing.")
1284
  st.stop()
1285
 
 
1286
  df_op = df[[col_operator, col_fleet_type, "start"]].dropna()
 
 
 
 
1287
  if df_op.empty:
1288
  st.info("No operator data after filtering.")
1289
  st.stop()
 
1302
  ob_data = df_op[df_op["is_ob"]]
1303
  coal_data = df_op[df_op["is_coal"]]
1304
 
 
 
1305
  def get_top10_with_slope(data):
1306
  if data.empty:
1307
  return pd.DataFrame()
 
1342
  top_ob = get_top10_with_slope(ob_data)
1343
  top_coal = get_top10_with_slope(coal_data)
1344
 
1345
+ def get_all_operators_with_slope(data):
1346
+ if data.empty:
1347
+ return pd.DataFrame()
1348
+ if col_operator not in data.columns:
1349
+ return pd.DataFrame()
1350
+
1351
+ weekly = data.groupby([col_operator, "year_week"]).size().reset_index(name="weekly_sum")
1352
+ metrics = []
1353
+ for nik, grp in weekly.groupby(col_operator):
1354
+ if pd.isna(nik):
1355
+ continue
1356
+ grp = grp.sort_values("year_week")
1357
+ counts = grp["weekly_sum"].values
1358
+ weeks = np.arange(len(counts))
1359
+ weekly_avg = counts.mean()
1360
+ total_events = counts.sum()
1361
+ n_weeks = len(counts)
1362
+ if n_weeks >= 2:
1363
+ slope = np.cov(weeks, counts)[0, 1] / np.var(weeks) if np.var(weeks) != 0 else 0.0
1364
+ else:
1365
+ slope = 0.0
1366
+ metrics.append({
1367
+ col_operator: nik,
1368
+ "weekly_avg": weekly_avg,
1369
+ "slope": slope,
1370
+ "total_events": total_events,
1371
+ "n_weeks": n_weeks
1372
+ })
1373
+ return pd.DataFrame(metrics) if metrics else pd.DataFrame()
1374
+
1375
  all_ob = get_all_operators_with_slope(ob_data)
1376
  all_coal = get_all_operators_with_slope(coal_data)
1377
 
1378
  # ===============================================================
1379
+ # LEGEND — UPDATED: Stable → One Time Event, Gray → Yellow
1380
  # ===============================================================
1381
+ st.subheader("Hazard Gradient Legend")
1382
  st.markdown("""
1383
  <div class="legend-container">
 
1384
  <div class="legend-box">
1385
  <div class="legend-title">Worsening Trends (Positive Slope):</div>
1386
  <div class="legend-item">
1387
  <div class="legend-color" style="background-color: #d32f2f;"></div>
1388
+ <span>Very High Worsening (≥1.5)</span>
1389
  </div>
1390
  <div class="legend-item">
1391
  <div class="legend-color" style="background-color: #e57373;"></div>
1392
+ <span>High Worsening (1.0–1.5)</span>
1393
  </div>
1394
  <div class="legend-item">
1395
  <div class="legend-color" style="background-color: #ef9a9a;"></div>
1396
+ <span>Moderate Worsening (0.5–1.0)</span>
1397
  </div>
1398
  <div class="legend-item">
1399
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1400
+ <span>Slight Worsening (0–0.5)</span>
1401
  </div>
1402
+ <br>
1403
+ <i>Note: Worsening trends indicate increasing fatigue frequency over weeks.</i>
1404
  </div>
 
 
1405
  <div class="legend-box">
1406
  <div class="legend-title">Improving Trends (Negative Slope):</div>
1407
  <div class="legend-item">
1408
  <div class="legend-color" style="background-color: #388e3c;"></div>
1409
+ <span>Excellent Improvement (≤−1.5)</span>
1410
  </div>
1411
  <div class="legend-item">
1412
  <div class="legend-color" style="background-color: #81c784;"></div>
1413
+ <span>Great Improvement (1.5 to 1.0)</span>
1414
  </div>
1415
  <div class="legend-item">
1416
  <div class="legend-color" style="background-color: #a5d6a7;"></div>
1417
+ <span>Good Improvement (1.0 to 0.5)</span>
1418
  </div>
1419
  <div class="legend-item">
1420
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1421
+ <span>Slight Improvement (0.5 to 0)</span>
1422
  </div>
1423
+ <br>
1424
+ <i>Note: Improving trends indicate decreasing fatigue frequency over weeks.</i>
1425
  </div>
 
 
1426
  <div class="legend-box">
1427
+ <div class="legend-title">One-Time Events (Zero Slope):</div>
1428
  <div class="legend-item">
1429
  <div class="legend-color" style="background-color: #FFD700;"></div>
1430
  <span>One Time Event (0)</span>
1431
  </div>
1432
+ <br>
1433
+ <i>Note: Applies when an operator has data in only one week — slope is set to 0 by definition.</i>
1434
  </div>
1435
  </div>
1436
  """, unsafe_allow_html=True)
1437
 
 
1438
  # ===============================================================
1439
+ # PLOT FUNCTION — UPDATED: color for slope=0 is now #FFD700
1440
  # ===============================================================
1441
  def plot_chart(data, title):
1442
  if data.empty:
 
1445
  text="No Data",
1446
  x=0.5, y=0.5,
1447
  showarrow=False,
1448
+ font_size=16
 
 
 
 
 
 
 
1449
  )
1450
+ fig.update_layout(height=350, title=dict(text=title, x=0.5))
1451
  return fig
1452
 
1453
  data_sorted = data.sort_values('weekly_avg', ascending=False)
1454
 
1455
  def get_color(slope):
 
1456
  if slope == 0:
1457
+ return "#FFD700" # ✅ Kuning untuk One Time Event
1458
  elif slope > 0:
1459
+ if slope < 0.5:
 
 
 
 
 
 
1460
  return "#ffcdd2"
1461
+ elif slope < 1.0:
1462
+ return "#ef9a9a"
1463
+ elif slope < 1.5:
1464
+ return "#e57373"
1465
+ else:
1466
+ return "#d32f2f"
1467
  else: # slope < 0
1468
+ if slope > -0.5:
 
 
 
 
 
 
1469
  return "#c8e6c9"
1470
+ elif slope > -1.0:
1471
+ return "#a5d6a7"
1472
+ elif slope > -1.5:
1473
+ return "#81c784"
1474
+ else:
1475
+ return "#388e3c"
1476
 
1477
  colors = [get_color(s) for s in data_sorted["slope"]]
1478
 
 
1481
  y=data_sorted["weekly_avg"],
1482
  marker=dict(
1483
  color=colors,
1484
+ line=dict(width=2, color="rgba(0,0,0,0.2)")
1485
  ),
1486
  text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1487
  textposition="outside",
 
1488
  hovertemplate=(
1489
  "<b>%{x}</b><br>" +
1490
  "Weekly Avg: %{y:.2f}<br>" +
1491
+ "Trend Slope: %{customdata[0]:+.3f}<br>" +
1492
  "Total Events: %{customdata[1]}<br>" +
1493
  "Weeks Active: %{customdata[2]}<br>" +
1494
  "<extra></extra>"
 
1498
 
1499
  fig = go.Figure(bar_trace)
1500
  fig.update_layout(
1501
+ title=dict(text=f"<b>{title}</b>", x=0.5),
 
 
 
 
1502
  height=450,
1503
+ margin=dict(l=50, r=20, t=60, b=120),
1504
+ xaxis_title="<b>Operator Name</b>",
1505
+ yaxis_title="<b>Weekly Avg Events</b>",
1506
  font=dict(family="Segoe UI", size=12),
1507
  bargap=0.3,
1508
  plot_bgcolor="rgba(0,0,0,0)",
1509
  paper_bgcolor="rgba(0,0,0,0)",
1510
+ xaxis=dict(tickangle=45)
 
 
 
 
 
 
1511
  )
1512
  return fig
1513
 
 
1514
  # ===============================================================
1515
+ # CHARTS
1516
  # ===============================================================
1517
  col1, col2 = st.columns(2)
1518
  with col1:
 
1520
  with col2:
1521
  st.plotly_chart(plot_chart(top_coal, "HAULING COAL Operators (Hazard Gradient)"), use_container_width=True)
1522
 
 
1523
  # ===============================================================
1524
+ # AI INSIGHTS — tetap dalam bahasa Inggris, tanpa emoticon
1525
  # ===============================================================
1526
  col_insight1, col_insight2 = st.columns(2)
1527
 
1528
  with col_insight1:
1529
  if not top_ob.empty:
1530
+ st.markdown("### OB HAULER Analysis")
1531
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1532
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1533
  ob_one_time = len(top_ob[top_ob['slope'] == 0])
1534
  ob_avg_risk = top_ob['weekly_avg'].mean()
1535
  ob_max_risk = top_ob['weekly_avg'].max()
1536
+ ob_insights = []
 
1537
  if ob_worsening > ob_improving:
1538
+ ob_insights.append(f"{ob_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends.")
1539
  else:
1540
+ ob_insights.append(f"{ob_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
 
1541
  if ob_one_time > 0:
1542
+ ob_insights.append(f"{ob_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1543
+ ob_insights.append(f"Average risk: {ob_avg_risk:.2f} events/week (max: {ob_max_risk:.2f}).")
 
1544
 
1545
+ for insight in ob_insights:
1546
  st.markdown(f"""
1547
  <div class="ai-insight-box">
1548
+ <div class="ai-insight-title">Risk Summary</div>
1549
+ <p>{insight}</p>
1550
  </div>
1551
  """, unsafe_allow_html=True)
1552
  else:
 
1554
 
1555
  with col_insight2:
1556
  if not top_coal.empty:
1557
+ st.markdown("### HAULING COAL Analysis")
1558
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1559
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1560
  coal_one_time = len(top_coal[top_coal['slope'] == 0])
1561
  coal_avg_risk = top_coal['weekly_avg'].mean()
1562
  coal_max_risk = top_coal['weekly_avg'].max()
1563
+ coal_insights = []
 
1564
  if coal_worsening > coal_improving:
1565
+ coal_insights.append(f"{coal_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends.")
1566
  else:
1567
+ coal_insights.append(f"{coal_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
 
1568
  if coal_one_time > 0:
1569
+ coal_insights.append(f"{coal_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1570
+ coal_insights.append(f"Average risk: {coal_avg_risk:.2f} events/week (max: {coal_max_risk:.2f}).")
 
1571
 
1572
+ for insight in coal_insights:
1573
  st.markdown(f"""
1574
  <div class="ai-insight-box">
1575
+ <div class="ai-insight-title">Risk Summary</div>
1576
+ <p>{insight}</p>
1577
  </div>
1578
  """, unsafe_allow_html=True)
1579
  else:
1580
  st.info("No HAULING COAL data for analysis.")
1581
 
 
1582
  # ===============================================================
1583
+ # RECOMMENDATIONS
1584
  # ===============================================================
1585
+ col_rec1, col_rec2 = st.columns(2)
1586
+
1587
  def generate_recommendations(top_ob, top_coal):
1588
  rec = {}
1589
  if not top_ob.empty:
 
1592
  avg = top_ob['weekly_avg'].mean()
1593
  if w > 5:
1594
  r = "Prioritize fatigue intervention for operators with worsening trends."
1595
+ reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1596
  elif ot > 4:
1597
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1598
  reason = "Operators with single-week data cannot yield reliable trend analysis."
 
1611
  avg = top_coal['weekly_avg'].mean()
1612
  if w > 5:
1613
  r = "Prioritize fatigue intervention for operators with worsening trends."
1614
+ reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1615
  elif ot > 4:
1616
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1617
  reason = "Operators with single-week data cannot yield reliable trend analysis."
 
1627
 
1628
  ai_rec = generate_recommendations(top_ob, top_coal)
1629
 
 
1630
  with col_rec1:
1631
  if 'ob' in ai_rec:
1632
+ st.markdown("### OB HAULER Recommendations")
1633
  st.markdown(f"""
1634
  <div class="recommendation-box">
1635
  <div class="recommendation-title">Action Plan</div>
 
1642
 
1643
  with col_rec2:
1644
  if 'coal' in ai_rec:
1645
+ st.markdown("### HAULING COAL Recommendations")
1646
  st.markdown(f"""
1647
  <div class="recommendation-box">
1648
  <div class="recommendation-title">Action Plan</div>
 
1655
 
1656
  except Exception as e:
1657
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1658
+ st.exception(e) # optionally show full traceback during dev
1659
 
1660
  import re # Tambahkan ini jika belum ada
1661