SHELLAPANDIANGANHUNGING commited on
Commit
af7b1cd
·
verified ·
1 Parent(s): 17f07e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -180
app.py CHANGED
@@ -1155,9 +1155,9 @@ except Exception as e:
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 {
@@ -1283,7 +1283,6 @@ if df.empty:
1283
  st.info("No data available after applying filters.")
1284
  else:
1285
  try:
1286
- # Validasi kolom
1287
  required = [col_operator, col_fleet_type, "start"]
1288
  if not all(c in df.columns for c in required if c is not None):
1289
  st.warning("Required columns (operator, fleet_type, start) are missing.")
@@ -1294,9 +1293,8 @@ else:
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")
@@ -1309,107 +1307,81 @@ else:
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,50 +1394,50 @@ else:
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">
1456
- <div class="legend-title">Stable Trend (Zero Slope):</div>
1457
  <div class="legend-item">
1458
- <div class="legend-color" style="background-color: #95a5a6;"></div>
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:
@@ -1476,41 +1448,35 @@ else:
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"],
@@ -1531,13 +1497,9 @@ else:
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>",
@@ -1545,12 +1507,13 @@ else:
1545
  font=dict(family="Segoe UI", size=12),
1546
  bargap=0.3,
1547
  plot_bgcolor="rgba(0,0,0,0)",
1548
- paper_bgcolor="rgba(0,0,0,0)"
 
1549
  )
1550
  return fig
1551
 
1552
  # ===============================================================
1553
- # TAMPILKAN BAR CHART
1554
  # ===============================================================
1555
  col1, col2 = st.columns(2)
1556
  with col1:
@@ -1559,58 +1522,58 @@ else:
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"""
1587
  <div class="ai-insight-box">
1588
- <div class="ai-insight-title">Risk Analysis</div>
1589
  <p>{insight}</p>
1590
  </div>
1591
  """, unsafe_allow_html=True)
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"""
1612
  <div class="ai-insight-box">
1613
- <div class="ai-insight-title">Risk Analysis</div>
1614
  <p>{insight}</p>
1615
  </div>
1616
  """, unsafe_allow_html=True)
@@ -1618,83 +1581,82 @@ else:
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")
1669
  st.markdown(f"""
1670
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: 1px solid #4a5568; border-radius: 8px; padding: 15px; margin: 10px 0; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; box-shadow: 0 4px 15px rgba(0,0,0,0.1); height: 180px; display: flex; flex-direction: column; justify-content: space-between;">
1671
- <div style="font-weight: bold; font-size: 14px; background: rgba(255,255,255,0.2); padding: 8px; border-radius: 5px; border-left: 4px solid white;">Recommendation</div>
1672
- <div style="padding-top: 8px; font-size: 14px;">{ai_recommendations['ob']}</div>
1673
- <div style="font-size: 12px; margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.1); border-radius: 5px; border-left: 3px solid rgba(255,255,255,0.3);">AI Reasoning: {ai_recommendations['ob_reason']}</div>
1674
  </div>
1675
  """, unsafe_allow_html=True)
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")
1683
  st.markdown(f"""
1684
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: 1px solid #4a5568; border-radius: 8px; padding: 15px; margin: 10px 0; color: white; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; box-shadow: 0 4px 15px rgba(0,0,0,0.1); height: 180px; display: flex; flex-direction: column; justify-content: space-between;">
1685
- <div style="font-weight: bold; font-size: 14px; background: rgba(255,255,255,0.2); padding: 8px; border-radius: 5px; border-left: 4px solid white;">Recommendation</div>
1686
- <div style="padding-top: 8px; font-size: 14px;">{ai_recommendations['coal']}</div>
1687
- <div style="font-size: 12px; margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.1); border-radius: 5px; border-left: 3px solid rgba(255,255,255,0.3);">AI Reasoning: {ai_recommendations['coal_reason']}</div>
1688
  </div>
1689
  """, unsafe_allow_html=True)
1690
  else:
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 =====================
 
1155
 
1156
  # ... (kode sebelumnya tetap sama) ...
1157
 
 
1158
  st.subheader("OBJECTIVE 5: See your team’s fatigue risk gradient at a glance!")
1159
+
1160
+ # Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
1161
  st.markdown("""
1162
  <style>
1163
  .big-title {
 
1283
  st.info("No data available after applying filters.")
1284
  else:
1285
  try:
 
1286
  required = [col_operator, col_fleet_type, "start"]
1287
  if not all(c in df.columns for c in required if c is not None):
1288
  st.warning("Required columns (operator, fleet_type, start) are missing.")
 
1293
  st.info("No operator data after filtering.")
1294
  st.stop()
1295
 
 
1296
  if col_operator is None:
1297
+ st.error("Operator column could not be auto-detected. Please check your data.")
1298
  st.stop()
1299
 
1300
  df_op["year_week"] = df_op["start"].dt.strftime("%Y-W%U")
 
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()
1313
+ if col_operator not in data.columns:
1314
+ st.error(f"Operator column '{col_operator}' not found in data subset.")
 
1315
  return pd.DataFrame()
1316
 
1317
  weekly = data.groupby([col_operator, "year_week"]).size().reset_index(name="weekly_sum")
1318
  metrics = []
1319
+ for nik, grp in weekly.groupby(col_operator):
1320
+ if pd.isna(nik):
1321
+ continue
1322
+ grp = grp.sort_values("year_week")
1323
+ counts = grp["weekly_sum"].values
1324
+ weeks = np.arange(len(counts))
1325
+ weekly_avg = counts.mean()
1326
+ total_events = counts.sum()
1327
+ n_weeks = len(counts)
1328
+ if n_weeks >= 2:
1329
+ x_mean = weeks.mean()
1330
+ y_mean = counts.mean()
1331
+ numerator = np.sum((weeks - x_mean) * (counts - y_mean))
1332
+ denominator = np.sum((weeks - x_mean) ** 2)
1333
+ slope = numerator / denominator if denominator != 0 else 0.0
1334
+ else:
1335
+ slope = 0.0 # One Time Event
1336
+ metrics.append({
1337
+ col_operator: nik,
1338
+ "weekly_avg": weekly_avg,
1339
+ "slope": slope,
1340
+ "total_events": total_events,
1341
+ "n_weeks": n_weeks
1342
+ })
 
 
 
 
 
 
1343
  if not metrics:
 
1344
  return pd.DataFrame()
1345
  return pd.DataFrame(metrics).nlargest(10, "weekly_avg")
1346
 
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("Risk Gradient Legend")
1387
  st.markdown("""
 
1394
  </div>
1395
  <div class="legend-item">
1396
  <div class="legend-color" style="background-color: #e57373;"></div>
1397
+ <span>High Risk (1.01.5)</span>
1398
  </div>
1399
  <div class="legend-item">
1400
  <div class="legend-color" style="background-color: #ef9a9a;"></div>
1401
+ <span>Moderate Risk (0.51.0)</span>
1402
  </div>
1403
  <div class="legend-item">
1404
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1405
+ <span>Slight Risk (00.5)</span>
1406
  </div>
1407
  </div>
1408
  <div class="legend-box">
1409
  <div class="legend-title">Improving Trends (Negative Slope):</div>
1410
  <div class="legend-item">
1411
  <div class="legend-color" style="background-color: #388e3c;"></div>
1412
+ <span>Excellent Improvement (≤−1.5)</span>
1413
  </div>
1414
  <div class="legend-item">
1415
  <div class="legend-color" style="background-color: #81c784;"></div>
1416
+ <span>Great Improvement (1.5 to 1.0)</span>
1417
  </div>
1418
  <div class="legend-item">
1419
  <div class="legend-color" style="background-color: #a5d6a7;"></div>
1420
+ <span>Good Improvement (1.0 to 0.5)</span>
1421
  </div>
1422
  <div class="legend-item">
1423
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1424
+ <span>Slight Improvement (0.5 to 0)</span>
1425
  </div>
1426
  </div>
1427
  <div class="legend-box">
1428
+ <div class="legend-title">One-Time Events (Zero Slope):</div>
1429
  <div class="legend-item">
1430
+ <div class="legend-color" style="background-color: #FFD700;"></div>
1431
+ <span>One Time Event (0)</span>
1432
  </div>
1433
  <br>
1434
+ <i>Note: Applies when an operator has data in only one week slope is set to 0 by definition.</i>
1435
  </div>
1436
  </div>
1437
  """, unsafe_allow_html=True)
1438
 
1439
  # ===============================================================
1440
+ # PLOT FUNCTION UPDATED: color for slope=0 is now #FFD700
1441
  # ===============================================================
1442
  def plot_chart(data, title):
1443
  if data.empty:
 
1448
  showarrow=False,
1449
  font_size=16
1450
  )
1451
+ fig.update_layout(height=350, title=dict(text=title, x=0.5))
 
1452
  return fig
1453
 
 
1454
  data_sorted = data.sort_values('weekly_avg', ascending=False)
1455
 
 
1456
  def get_color(slope):
1457
  if slope == 0:
1458
+ return "#FFD700" # Kuning untuk One Time Event
1459
  elif slope > 0:
 
1460
  if slope < 0.5:
1461
+ return "#ffcdd2"
1462
  elif slope < 1.0:
1463
+ return "#ef9a9a"
1464
  elif slope < 1.5:
1465
+ return "#e57373"
1466
  else:
1467
+ return "#d32f2f"
1468
  else: # slope < 0
 
1469
  if slope > -0.5:
1470
+ return "#c8e6c9"
1471
  elif slope > -1.0:
1472
+ return "#a5d6a7"
1473
  elif slope > -1.5:
1474
+ return "#81c784"
1475
  else:
1476
+ return "#388e3c"
1477
 
1478
  colors = [get_color(s) for s in data_sorted["slope"]]
1479
 
 
1480
  bar_trace = go.Bar(
1481
  x=data_sorted[col_operator].astype(str),
1482
  y=data_sorted["weekly_avg"],
 
1497
  customdata=np.stack([data_sorted["slope"], data_sorted["total_events"], data_sorted["n_weeks"]], axis=-1)
1498
  )
1499
 
 
1500
  fig = go.Figure(bar_trace)
 
 
1501
  fig.update_layout(
1502
+ title=dict(text=f"<b>{title}</b>", x=0.5),
 
1503
  height=450,
1504
  margin=dict(l=50, r=20, t=60, b=120),
1505
  xaxis_title="<b>Operator ID</b>",
 
1507
  font=dict(family="Segoe UI", size=12),
1508
  bargap=0.3,
1509
  plot_bgcolor="rgba(0,0,0,0)",
1510
+ paper_bgcolor="rgba(0,0,0,0)",
1511
+ xaxis=dict(tickangle=45)
1512
  )
1513
  return fig
1514
 
1515
  # ===============================================================
1516
+ # CHARTS
1517
  # ===============================================================
1518
  col1, col2 = st.columns(2)
1519
  with col1:
 
1522
  st.plotly_chart(plot_chart(top_coal, "HAULING COAL Operators (Risk Gradient)"), use_container_width=True)
1523
 
1524
  # ===============================================================
1525
+ # AI INSIGHTS tetap dalam bahasa Inggris, tanpa emoticon
1526
  # ===============================================================
 
 
 
 
1527
  col_insight1, col_insight2 = st.columns(2)
1528
 
 
1529
  with col_insight1:
1530
  if not top_ob.empty:
1531
  st.markdown("### OB HAULER Analysis")
1532
  ob_worsening = len(top_ob[top_ob['slope'] > 0])
1533
  ob_improving = len(top_ob[top_ob['slope'] < 0])
1534
+ ob_one_time = len(top_ob[top_ob['slope'] == 0])
1535
  ob_avg_risk = top_ob['weekly_avg'].mean()
1536
  ob_max_risk = top_ob['weekly_avg'].max()
1537
  ob_insights = []
1538
  if ob_worsening > ob_improving:
1539
+ ob_insights.append(f"{ob_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends.")
1540
  else:
1541
+ ob_insights.append(f"{ob_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
1542
+ if ob_one_time > 0:
1543
+ ob_insights.append(f"{ob_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1544
+ ob_insights.append(f"Average risk: {ob_avg_risk:.2f} events/week (max: {ob_max_risk:.2f}).")
1545
 
1546
  for insight in ob_insights:
1547
  st.markdown(f"""
1548
  <div class="ai-insight-box">
1549
+ <div class="ai-insight-title">Risk Summary</div>
1550
  <p>{insight}</p>
1551
  </div>
1552
  """, unsafe_allow_html=True)
1553
  else:
1554
  st.info("No OB HAULER data for analysis.")
1555
 
 
1556
  with col_insight2:
1557
  if not top_coal.empty:
1558
  st.markdown("### HAULING COAL Analysis")
1559
  coal_worsening = len(top_coal[top_coal['slope'] > 0])
1560
  coal_improving = len(top_coal[top_coal['slope'] < 0])
1561
+ coal_one_time = len(top_coal[top_coal['slope'] == 0])
1562
  coal_avg_risk = top_coal['weekly_avg'].mean()
1563
  coal_max_risk = top_coal['weekly_avg'].max()
1564
  coal_insights = []
1565
  if coal_worsening > coal_improving:
1566
+ coal_insights.append(f"{coal_worsening} out of 10 top risk operators are showing <span class='trend-up'>worsening</span> trends.")
1567
  else:
1568
+ coal_insights.append(f"{coal_improving} out of 10 top risk operators are showing <span class='trend-down'>improvement</span>.")
1569
+ if coal_one_time > 0:
1570
+ coal_insights.append(f"{coal_one_time} operators are classified as <b>One Time Event</b> (single-week activity).")
1571
+ coal_insights.append(f"Average risk: {coal_avg_risk:.2f} events/week (max: {coal_max_risk:.2f}).")
1572
 
1573
  for insight in coal_insights:
1574
  st.markdown(f"""
1575
  <div class="ai-insight-box">
1576
+ <div class="ai-insight-title">Risk Summary</div>
1577
  <p>{insight}</p>
1578
  </div>
1579
  """, unsafe_allow_html=True)
 
1581
  st.info("No HAULING COAL data for analysis.")
1582
 
1583
  # ===============================================================
1584
+ # RECOMMENDATIONS
1585
  # ===============================================================
 
 
 
 
 
 
1586
  col_rec1, col_rec2 = st.columns(2)
1587
 
1588
  def generate_recommendations(top_ob, top_coal):
1589
+ rec = {}
1590
  if not top_ob.empty:
1591
+ w = len(top_ob[top_ob['slope'] > 0])
1592
+ ot = len(top_ob[top_ob['slope'] == 0])
1593
+ avg = top_ob['weekly_avg'].mean()
1594
+ if w > 5:
1595
+ r = "Prioritize fatigue intervention for operators with worsening trends."
1596
+ reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1597
+ elif ot > 4:
1598
+ r = "Validate data completeness high One Time Event count may indicate reporting gaps."
1599
+ reason = "Operators with single-week data cannot yield reliable trend analysis."
1600
+ elif avg > 8:
1601
+ r = "Review scheduling and rest protocols to reduce event frequency."
1602
+ reason = "Elevated average event rate increases cumulative fatigue exposure."
1603
  else:
1604
+ r = "Maintain current protocols with targeted monitoring."
1605
+ reason = "Risk profile is stable; focus on sustaining safe practices."
1606
+ rec['ob'] = r
1607
+ rec['ob_reason'] = reason
1608
 
1609
  if not top_coal.empty:
1610
+ w = len(top_coal[top_coal['slope'] > 0])
1611
+ ot = len(top_coal[top_coal['slope'] == 0])
1612
+ avg = top_coal['weekly_avg'].mean()
1613
+ if w > 5:
1614
+ r = "Prioritize fatigue intervention for operators with worsening trends."
1615
+ reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1616
+ elif ot > 4:
1617
+ r = "Validate data completeness high One Time Event count may indicate reporting gaps."
1618
+ reason = "Operators with single-week data cannot yield reliable trend analysis."
1619
+ elif avg > 8:
1620
+ r = "Review scheduling and rest protocols to reduce event frequency."
1621
+ reason = "Elevated average event rate increases cumulative fatigue exposure."
1622
  else:
1623
+ r = "Maintain current protocols with targeted monitoring."
1624
+ reason = "Risk profile is stable; focus on sustaining safe practices."
1625
+ rec['coal'] = r
1626
+ rec['coal_reason'] = reason
1627
+ return rec
1628
 
1629
+ ai_rec = generate_recommendations(top_ob, top_coal)
1630
 
 
 
 
1631
  with col_rec1:
1632
+ if 'ob' in ai_rec:
1633
  st.markdown("### OB HAULER Recommendations")
1634
  st.markdown(f"""
1635
+ <div class="recommendation-box">
1636
+ <div class="recommendation-title">Action Plan</div>
1637
+ <div>{ai_rec['ob']}</div>
1638
+ <div class="recommendation-reason">AI Reasoning: {ai_rec['ob_reason']}</div>
1639
  </div>
1640
  """, unsafe_allow_html=True)
1641
  else:
1642
+ st.info("No OB HAULER recommendations.")
1643
 
 
1644
  with col_rec2:
1645
+ if 'coal' in ai_rec:
1646
  st.markdown("### HAULING COAL Recommendations")
1647
  st.markdown(f"""
1648
+ <div class="recommendation-box">
1649
+ <div class="recommendation-title">Action Plan</div>
1650
+ <div>{ai_rec['coal']}</div>
1651
+ <div class="recommendation-reason">AI Reasoning: {ai_rec['coal_reason']}</div>
1652
  </div>
1653
  """, unsafe_allow_html=True)
1654
  else:
1655
+ st.info("No HAULING COAL recommendations.")
1656
 
1657
  except Exception as e:
1658
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1659
+ st.exception(e) # optionally show full traceback during dev
 
 
1660
 
1661
 
1662
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================