SHELLAPANDIANGANHUNGING commited on
Commit
04b05f7
·
verified ·
1 Parent(s): 07bc1d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -211
app.py CHANGED
@@ -1147,54 +1147,76 @@ except Exception as e:
1147
  st.error(f"⚠️ Error Risk Map Objective 4: {e}")
1148
  st.exception(e)
1149
 
1150
- # ... (kode sebelumnya tetap sama) ...
1151
- # ... (kode sebelumnya tetap sama) ...
1152
- # ... (kode sebelumnya tetap sama) ...
1153
 
1154
- # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
1155
-
1156
- # ... (kode sebelumnya tetap sama) ...
1157
 
1158
- st.subheader("OBJECTIVE 5:See your team’s Fatigue Hazard Profile!")
 
 
 
 
1159
 
1160
- # Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
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;
@@ -1207,25 +1229,27 @@ st.markdown("""
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;
@@ -1237,20 +1261,14 @@ st.markdown("""
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);
@@ -1266,18 +1284,34 @@ st.markdown("""
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.")
@@ -1288,7 +1322,12 @@ else:
1288
  st.warning("Required columns (operator, fleet_type, start) are missing.")
1289
  st.stop()
1290
 
 
1291
  df_op = df[[col_operator, col_fleet_type, "start"]].dropna()
 
 
 
 
1292
  if df_op.empty:
1293
  st.info("No operator data after filtering.")
1294
  st.stop()
@@ -1307,6 +1346,8 @@ else:
1307
  ob_data = df_op[df_op["is_ob"]]
1308
  coal_data = df_op[df_op["is_coal"]]
1309
 
 
 
1310
  def get_top10_with_slope(data):
1311
  if data.empty:
1312
  return pd.DataFrame()
@@ -1347,136 +1388,74 @@ else:
1347
  top_ob = get_top10_with_slope(ob_data)
1348
  top_coal = get_top10_with_slope(coal_data)
1349
 
1350
- def get_all_operators_with_slope(data):
1351
- if data.empty:
1352
- return pd.DataFrame()
1353
- if col_operator not in data.columns:
1354
- return pd.DataFrame()
1355
-
1356
- weekly = data.groupby([col_operator, "year_week"]).size().reset_index(name="weekly_sum")
1357
- metrics = []
1358
- for nik, grp in weekly.groupby(col_operator):
1359
- if pd.isna(nik):
1360
- continue
1361
- grp = grp.sort_values("year_week")
1362
- counts = grp["weekly_sum"].values
1363
- weeks = np.arange(len(counts))
1364
- weekly_avg = counts.mean()
1365
- total_events = counts.sum()
1366
- n_weeks = len(counts)
1367
- if n_weeks >= 2:
1368
- slope = np.cov(weeks, counts)[0, 1] / np.var(weeks) if np.var(weeks) != 0 else 0.0
1369
- else:
1370
- slope = 0.0
1371
- metrics.append({
1372
- col_operator: nik,
1373
- "weekly_avg": weekly_avg,
1374
- "slope": slope,
1375
- "total_events": total_events,
1376
- "n_weeks": n_weeks
1377
- })
1378
- return pd.DataFrame(metrics) if metrics else pd.DataFrame()
1379
-
1380
  all_ob = get_all_operators_with_slope(ob_data)
1381
  all_coal = get_all_operators_with_slope(coal_data)
1382
 
1383
  # ===============================================================
1384
- # LEGEND — UPDATED: Stable → One Time Event, Gray → Yellow
1385
  # ===============================================================
1386
- st.subheader("Hazard Gradient Legend")
1387
  st.markdown("""
1388
- <style>
1389
- .legend-container {
1390
- display: flex;
1391
- gap: 20px;
1392
- flex-wrap: wrap;}.legend-box {
1393
- background: #f9f9f9;
1394
- padding: 12px;
1395
- border-radius: 10px;
1396
- border: 1px solid #ddd;
1397
- width: 300px;
1398
- }
1399
- .legend-title {
1400
- font-weight: 700;
1401
- margin-bottom: 10px;
1402
- font-size: 14px;
1403
- }
1404
- .legend-item {
1405
- display: flex;
1406
- align-items: center;
1407
- margin-bottom: 6px;
1408
- }
1409
- .legend-color {
1410
- width: 18px;
1411
- height: 18px;
1412
- border-radius: 4px;
1413
- margin-right: 10px;
1414
- }</style><div class="legend-container"><!-- Worsening Trends --><div class="legend-box">
1415
- <div class="legend-title">Worsening Trends (Positive Slope):</div>
1416
-
1417
- <div class="legend-item">
1418
- <div class="legend-color" style="background-color: #d32f2f;"></div>
1419
- <span>Very High Worsening (≥ 1.5)</span>
1420
- </div>
1421
-
1422
- <div class="legend-item">
1423
- <div class="legend-color" style="background-color: #e57373;"></div>
1424
- <span>High Worsening (1.0 1.5)</span>
1425
- </div>
1426
-
1427
- <div class="legend-item">
1428
- <div class="legend-color" style="background-color: #ef9a9a;"></div>
1429
- <span>Moderate Worsening (0.5 – 1.0)</span>
1430
- </div>
1431
-
1432
- <div class="legend-item">
1433
- <div class="legend-color" style="background-color: #ffcdd2;"></div>
1434
- <span>Slight Worsening (0 – 0.5)</span>
1435
- </div>
1436
-
1437
- <br>
1438
- <i>Note: Worsening trends indicate increasing fatigue frequency over weeks.</i></div><!-- Improving Trends --><div class="legend-box">
1439
- <div class="legend-title">Improving Trends (Negative Slope):</div>
1440
-
1441
- <div class="legend-item">
1442
- <div class="legend-color" style="background-color: #388e3c;"></div>
1443
- <span>Excellent Improvement (≤ -1.5)</span>
1444
- </div>
1445
-
1446
- <div class="legend-item">
1447
- <div class="legend-color" style="background-color: #81c784;"></div>
1448
- <span>Great Improvement (-1.5 to -1.0)</span>
1449
- </div>
1450
-
1451
- <div class="legend-item">
1452
- <div class="legend-color" style="background-color: #a5d6a7;"></div>
1453
- <span>Good Improvement (-1.0 to -0.5)</span>
1454
- </div>
1455
-
1456
- <div class="legend-item">
1457
- <div class="legend-color" style="background-color: #c8e6c9;"></div>
1458
- <span>Slight Improvement (-0.5 to 0)</span>
1459
- </div>
1460
-
1461
- <br>
1462
- <i>Note: Improving trends indicate decreasing fatigue frequency over weeks.</i>
1463
- </div>
1464
- <!-- One-Time Events -->
1465
- <div class="legend-box">
1466
- <div class="legend-title">One-Time Events (Zero Slope):</div>
1467
-
1468
- <div class="legend-item">
1469
- <div class="legend-color" style="background-color: #FFD700;"></div>
1470
- <span>One Time Event (0)</span>
1471
  </div>
1472
-
1473
- <br>
1474
- <i>Note: Applies when an operator has data in only one week (slope = 0).</i>
1475
- </div></div>""", unsafe_allow_html=True)
1476
 
1477
 
1478
  # ===============================================================
1479
- # PLOT FUNCTION — UPDATED: color for slope=0 is now #FFD700
1480
  # ===============================================================
1481
  def plot_chart(data, title):
1482
  if data.empty:
@@ -1485,34 +1464,41 @@ else:
1485
  text="No Data",
1486
  x=0.5, y=0.5,
1487
  showarrow=False,
1488
- font_size=16
 
 
 
 
 
 
 
1489
  )
1490
- fig.update_layout(height=350, title=dict(text=title, x=0.5))
1491
  return fig
1492
 
1493
  data_sorted = data.sort_values('weekly_avg', ascending=False)
1494
 
1495
  def get_color(slope):
 
1496
  if slope == 0:
1497
- return "#FFD700" # ✅ Kuning untuk One Time Event
1498
  elif slope > 0:
1499
- if slope < 0.5:
1500
- return "#ffcdd2"
1501
- elif slope < 1.0:
1502
- return "#ef9a9a"
1503
- elif slope < 1.5:
1504
- return "#e57373"
1505
- else:
1506
  return "#d32f2f"
 
 
 
 
 
 
1507
  else: # slope < 0
1508
- if slope > -0.5:
1509
- return "#c8e6c9"
1510
- elif slope > -1.0:
1511
- return "#a5d6a7"
1512
- elif slope > -1.5:
1513
- return "#81c784"
1514
- else:
1515
  return "#388e3c"
 
 
 
 
 
 
1516
 
1517
  colors = [get_color(s) for s in data_sorted["slope"]]
1518
 
@@ -1521,14 +1507,15 @@ else:
1521
  y=data_sorted["weekly_avg"],
1522
  marker=dict(
1523
  color=colors,
1524
- line=dict(width=2, color="rgba(0,0,0,0.2)")
1525
  ),
1526
  text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1527
  textposition="outside",
 
1528
  hovertemplate=(
1529
  "<b>%{x}</b><br>" +
1530
  "Weekly Avg: %{y:.2f}<br>" +
1531
- "Trend Slope: %{customdata[0]:+.3f}<br>" +
1532
  "Total Events: %{customdata[1]}<br>" +
1533
  "Weeks Active: %{customdata[2]}<br>" +
1534
  "<extra></extra>"
@@ -1538,21 +1525,32 @@ else:
1538
 
1539
  fig = go.Figure(bar_trace)
1540
  fig.update_layout(
1541
- title=dict(text=f"<b>{title}</b>", x=0.5),
 
 
 
 
1542
  height=450,
1543
- margin=dict(l=50, r=20, t=60, b=120),
1544
- xaxis_title="<b>Operator Name</b>",
1545
- yaxis_title="<b>Weekly Avg Events</b>",
1546
  font=dict(family="Segoe UI", size=12),
1547
  bargap=0.3,
1548
  plot_bgcolor="rgba(0,0,0,0)",
1549
  paper_bgcolor="rgba(0,0,0,0)",
1550
- xaxis=dict(tickangle=45)
 
 
 
 
 
 
1551
  )
1552
  return fig
1553
 
 
1554
  # ===============================================================
1555
- # CHARTS
1556
  # ===============================================================
1557
  col1, col2 = st.columns(2)
1558
  with col1:
@@ -1560,33 +1558,37 @@ else:
1560
  with col2:
1561
  st.plotly_chart(plot_chart(top_coal, "HAULING COAL Operators (Hazard Gradient)"), use_container_width=True)
1562
 
 
1563
  # ===============================================================
1564
- # AI INSIGHTS — tetap dalam bahasa Inggris, tanpa emoticon
1565
  # ===============================================================
1566
  col_insight1, col_insight2 = st.columns(2)
1567
 
1568
  with col_insight1:
1569
  if not top_ob.empty:
1570
- st.markdown("### OB HAULER Analysis")
1571
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1572
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1573
  ob_one_time = len(top_ob[top_ob['slope'] == 0])
1574
  ob_avg_risk = top_ob['weekly_avg'].mean()
1575
  ob_max_risk = top_ob['weekly_avg'].max()
1576
- ob_insights = []
 
1577
  if ob_worsening > ob_improving:
1578
- ob_insights.append(f"{ob_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends.")
1579
  else:
1580
- ob_insights.append(f"{ob_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
 
1581
  if ob_one_time > 0:
1582
- ob_insights.append(f"{ob_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1583
- ob_insights.append(f"Average risk: {ob_avg_risk:.2f} events/week (max: {ob_max_risk:.2f}).")
 
1584
 
1585
- for insight in ob_insights:
1586
  st.markdown(f"""
1587
  <div class="ai-insight-box">
1588
  <div class="ai-insight-title">Hazard Summary</div>
1589
- <p>{insight}</p>
1590
  </div>
1591
  """, unsafe_allow_html=True)
1592
  else:
@@ -1594,36 +1596,38 @@ else:
1594
 
1595
  with col_insight2:
1596
  if not top_coal.empty:
1597
- st.markdown("### HAULING COAL Analysis")
1598
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1599
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1600
  coal_one_time = 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.")
1606
  else:
1607
- coal_insights.append(f"{coal_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
 
1608
  if coal_one_time > 0:
1609
- coal_insights.append(f"{coal_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1610
- coal_insights.append(f"Average risk: {coal_avg_risk:.2f} events/week (max: {coal_max_risk:.2f}).")
1611
 
1612
- for insight in coal_insights:
 
 
1613
  st.markdown(f"""
1614
  <div class="ai-insight-box">
1615
  <div class="ai-insight-title">Hazard Summary</div>
1616
- <p>{insight}</p>
1617
  </div>
1618
  """, unsafe_allow_html=True)
1619
  else:
1620
  st.info("No HAULING COAL data for analysis.")
1621
 
 
1622
  # ===============================================================
1623
- # RECOMMENDATIONS
1624
  # ===============================================================
1625
- col_rec1, col_rec2 = st.columns(2)
1626
-
1627
  def generate_recommendations(top_ob, top_coal):
1628
  rec = {}
1629
  if not top_ob.empty:
@@ -1632,7 +1636,7 @@ else:
1632
  avg = top_ob['weekly_avg'].mean()
1633
  if w > 5:
1634
  r = "Prioritize fatigue intervention for operators with worsening trends."
1635
- reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1636
  elif ot > 4:
1637
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1638
  reason = "Operators with single-week data cannot yield reliable trend analysis."
@@ -1651,7 +1655,7 @@ else:
1651
  avg = top_coal['weekly_avg'].mean()
1652
  if w > 5:
1653
  r = "Prioritize fatigue intervention for operators with worsening trends."
1654
- reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1655
  elif ot > 4:
1656
  r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1657
  reason = "Operators with single-week data cannot yield reliable trend analysis."
@@ -1667,9 +1671,10 @@ else:
1667
 
1668
  ai_rec = generate_recommendations(top_ob, top_coal)
1669
 
 
1670
  with col_rec1:
1671
  if 'ob' in ai_rec:
1672
- st.markdown("### OB HAULER Recommendations")
1673
  st.markdown(f"""
1674
  <div class="recommendation-box">
1675
  <div class="recommendation-title">Action Plan</div>
@@ -1682,7 +1687,7 @@ else:
1682
 
1683
  with col_rec2:
1684
  if 'coal' in ai_rec:
1685
- st.markdown("### HAULING COAL Recommendations")
1686
  st.markdown(f"""
1687
  <div class="recommendation-box">
1688
  <div class="recommendation-title">Action Plan</div>
@@ -1695,7 +1700,8 @@ else:
1695
 
1696
  except Exception as e:
1697
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1698
- st.exception(e) # optionally show full traceback during dev
 
1699
 
1700
 
1701
  import re # Tambahkan ini jika belum ada
 
1147
  st.error(f"⚠️ Error Risk Map Objective 4: {e}")
1148
  st.exception(e)
1149
 
 
 
 
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
  .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
  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
  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
  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
  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
  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
  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
  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
 
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
  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
 
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
  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
  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
 
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
 
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
 
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
 
1706
 
1707
  import re # Tambahkan ini jika belum ada