SHELLAPANDIANGANHUNGING commited on
Commit
faab708
·
verified ·
1 Parent(s): cd900f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -128
app.py CHANGED
@@ -1158,12 +1158,11 @@ import numpy as np
1158
  import plotly.graph_objects as go
1159
 
1160
  # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
1161
-
1162
  # ... (kode sebelumnya tetap sama) ...
1163
 
1164
- st.subheader("OBJECTIVE 5:See your team’s fatigue Fatigue Hazard Profile!")
1165
 
1166
- # Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
1167
  st.markdown("""
1168
  <style>
1169
  .big-title {
@@ -1264,20 +1263,30 @@ st.markdown("""
1264
  .recommendation-title {
1265
  font-weight: bold;
1266
  color: white;
1267
- margin-bottom: 8px;
1268
  font-size: 14px;
1269
  background: rgba(255,255,255,0.2);
1270
  padding: 8px;
1271
  border-radius: 5px;
1272
  border-left: 4px solid white;
1273
  }
 
 
 
 
 
 
 
 
 
1274
  .recommendation-reason {
1275
  font-size: 12px;
1276
- margin-top: 10px;
1277
  padding: 8px;
1278
- background: rgba(255,255,255,0.1);
1279
  border-radius: 5px;
1280
- border-left: 3px solid rgba(255,255,255,0.3);
 
1281
  }
1282
  </style>
1283
  """, unsafe_allow_html=True)
@@ -1411,7 +1420,7 @@ else:
1411
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1412
  <span>Slight Worsening (0–0.5)</span>
1413
  </div>
1414
- <i style="display: block; margin-top: 12px; font-size: 12px; color: #666; font-style: italic;">
1415
  Note: Positive slope indicates increasing fatigue event frequency over weeks.
1416
  </i>
1417
  </div>
@@ -1432,12 +1441,11 @@ else:
1432
  <div class="legend-item">
1433
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1434
  <span>Slight Improvement (−0.5 to 0)</span>
1435
- </div>
1436
- <i style="display: block; margin-top: 12px; font-size: 12px; color: #666; font-style: italic;">
1437
  Note: Negative slope reflects a consistent decline in fatigue events.
1438
  </i>
1439
- </div>
1440
- <!-- One-Time Events -->
1441
  <div class="legend-box">
1442
  <div class="legend-title">One-Time Events (Zero Slope):</div>
1443
  <div class="legend-item">
@@ -1451,81 +1459,81 @@ else:
1451
  </div>
1452
  """, unsafe_allow_html=True)
1453
 
1454
- # ===============================================================
1455
- # PLOT FUNCTION — UPDATED: color for slope=0 is now #FFD700
1456
- # ===============================================================
1457
- def plot_chart(data, title):
1458
- if data.empty:
1459
- fig = go.Figure()
1460
- fig.add_annotation(
1461
- text="No Data",
1462
- x=0.5, y=0.5,
1463
- showarrow=False,
1464
- font_size=16
1465
- )
1466
- fig.update_layout(height=350, title=dict(text=title, x=0.5))
1467
- return fig
1468
-
1469
- data_sorted = data.sort_values('weekly_avg', ascending=False)
1470
-
1471
- def get_color(slope):
1472
- if slope == 0:
1473
- return "#FFD700" # ✅ Kuning untuk One Time Event
1474
- elif slope > 0:
1475
- if slope < 0.5:
1476
- return "#ffcdd2"
1477
- elif slope < 1.0:
1478
- return "#ef9a9a"
1479
- elif slope < 1.5:
1480
- return "#e57373"
1481
- else:
1482
- return "#d32f2f"
1483
- else: # slope < 0
1484
- if slope > -0.5:
1485
- return "#c8e6c9"
1486
- elif slope > -1.0:
1487
- return "#a5d6a7"
1488
- elif slope > -1.5:
1489
- return "#81c784"
1490
- else:
1491
- return "#388e3c"
1492
-
1493
- colors = [get_color(s) for s in data_sorted["slope"]]
1494
-
1495
- bar_trace = go.Bar(
1496
- x=data_sorted[col_operator].astype(str),
1497
- y=data_sorted["weekly_avg"],
1498
- marker=dict(
1499
- color=colors,
1500
- line=dict(width=2, color="rgba(0,0,0,0.2)")
1501
- ),
1502
- text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1503
- textposition="outside",
1504
- hovertemplate=(
1505
- "<b>%{x}</b><br>" +
1506
- "Weekly Avg: %{y:.2f}<br>" +
1507
- "Trend Slope: %{customdata[0]:+.3f}<br>" +
1508
- "Total Events: %{customdata[1]}<br>" +
1509
- "Weeks Active: %{customdata[2]}<br>" +
1510
- "<extra></extra>"
1511
- ),
1512
- customdata=np.stack([data_sorted["slope"], data_sorted["total_events"], data_sorted["n_weeks"]], axis=-1)
1513
- )
1514
 
1515
- fig = go.Figure(bar_trace)
1516
- fig.update_layout(
1517
- title=dict(text=f"<b>{title}</b>", x=0.5),
1518
- height=450,
1519
- margin=dict(l=50, r=20, t=60, b=120),
1520
- xaxis_title="<b>Operator Name</b>",
1521
- yaxis_title="<b>Weekly Avg Events</b>",
1522
- font=dict(family="Segoe UI", size=12),
1523
- bargap=0.3,
1524
- plot_bgcolor="rgba(0,0,0,0)",
1525
- paper_bgcolor="rgba(0,0,0,0)",
1526
- xaxis=dict(tickangle=45)
1527
- )
1528
- return fig
1529
 
1530
  # ===============================================================
1531
  # CHARTS
@@ -1596,74 +1604,103 @@ def plot_chart(data, title):
1596
  st.info("No HAULING COAL data for analysis.")
1597
 
1598
  # ===============================================================
1599
- # RECOMMENDATIONS
1600
  # ===============================================================
1601
  col_rec1, col_rec2 = st.columns(2)
1602
 
1603
- def generate_recommendations(top_ob, top_coal):
1604
- rec = {}
1605
  if not top_ob.empty:
1606
  w = len(top_ob[top_ob['slope'] > 0])
1607
  ot = len(top_ob[top_ob['slope'] == 0])
1608
  avg = top_ob['weekly_avg'].mean()
 
 
 
 
1609
  if w > 5:
1610
- r = "Prioritize fatigue intervention for operators with worsening trends."
1611
- reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1612
  elif ot > 4:
1613
- r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1614
- reason = "Operators with single-week data cannot yield reliable trend analysis."
1615
  elif avg > 8:
1616
- r = "Review scheduling and rest protocols to reduce event frequency."
1617
- reason = "Elevated average event rate increases cumulative fatigue exposure."
1618
  else:
1619
- r = "Maintain current protocols with targeted monitoring."
1620
- reason = "Risk profile is stable; focus on sustaining safe practices."
1621
- rec['ob'] = r
1622
- rec['ob_reason'] = reason
1623
 
1624
- if not top_coal.empty:
1625
- w = len(top_coal[top_coal['slope'] > 0])
1626
- ot = len(top_coal[top_coal['slope'] == 0])
1627
- avg = top_coal['weekly_avg'].mean()
1628
- if w > 5:
1629
- r = "Prioritize fatigue intervention for operators with worsening trends."
1630
- reason = "High proportion of deteriorating operators signals emerging fatigue risks."
1631
- elif ot > 4:
1632
- r = "Validate data completeness — high One Time Event count may indicate reporting gaps."
1633
- reason = "Operators with single-week data cannot yield reliable trend analysis."
1634
- elif avg > 8:
1635
- r = "Review scheduling and rest protocols to reduce event frequency."
1636
- reason = "Elevated average event rate increases cumulative fatigue exposure."
1637
  else:
1638
- r = "Maintain current protocols with targeted monitoring."
1639
- reason = "Risk profile is stable; focus on sustaining safe practices."
1640
- rec['coal'] = r
1641
- rec['coal_reason'] = reason
1642
- return rec
1643
 
1644
- ai_rec = generate_recommendations(top_ob, top_coal)
 
 
 
 
 
1645
 
1646
- with col_rec1:
1647
- if 'ob' in ai_rec:
1648
  st.markdown("### OB HAULER Recommendations")
1649
  st.markdown(f"""
1650
  <div class="recommendation-box">
1651
  <div class="recommendation-title">Action Plan</div>
1652
- <div>{ai_rec['ob']}</div>
1653
- <div class="recommendation-reason">AI Reasoning: {ai_rec['ob_reason']}</div>
 
 
 
 
 
 
1654
  </div>
1655
  """, unsafe_allow_html=True)
1656
  else:
1657
  st.info("No OB HAULER recommendations.")
1658
 
 
1659
  with col_rec2:
1660
- if 'coal' in ai_rec:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
  st.markdown("### HAULING COAL Recommendations")
1662
  st.markdown(f"""
1663
  <div class="recommendation-box">
1664
  <div class="recommendation-title">Action Plan</div>
1665
- <div>{ai_rec['coal']}</div>
1666
- <div class="recommendation-reason">AI Reasoning: {ai_rec['coal_reason']}</div>
 
 
 
 
 
 
1667
  </div>
1668
  """, unsafe_allow_html=True)
1669
  else:
@@ -1671,9 +1708,7 @@ def plot_chart(data, title):
1671
 
1672
  except Exception as e:
1673
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1674
- st.exception(e) # optionally show full traceback during dev
1675
-
1676
-
1677
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1678
  st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
1679
 
 
1158
  import plotly.graph_objects as go
1159
 
1160
  # =================== OBJECTIVE 5: Operator Fatigue Risk Gradient Dashboard =====================
 
1161
  # ... (kode sebelumnya tetap sama) ...
1162
 
1163
+ st.subheader("OBJECTIVE 5: See your team’s fatigue Fatigue Hazard Profile!")
1164
 
1165
+ # Custom CSS — diperbarui dengan styling list untuk rekomendasi
1166
  st.markdown("""
1167
  <style>
1168
  .big-title {
 
1263
  .recommendation-title {
1264
  font-weight: bold;
1265
  color: white;
1266
+ margin-bottom: 12px;
1267
  font-size: 14px;
1268
  background: rgba(255,255,255,0.2);
1269
  padding: 8px;
1270
  border-radius: 5px;
1271
  border-left: 4px solid white;
1272
  }
1273
+ .recommendation-box ul {
1274
+ padding-left: 24px;
1275
+ margin: 12px 0;
1276
+ line-height: 1.6;
1277
+ font-size: 14px;
1278
+ }
1279
+ .recommendation-box ul li {
1280
+ margin-bottom: 8px;
1281
+ }
1282
  .recommendation-reason {
1283
  font-size: 12px;
1284
+ margin-top: 12px;
1285
  padding: 8px;
1286
+ background: rgba(255,255,255,0.15);
1287
  border-radius: 5px;
1288
+ border-left: 3px solid rgba(255,255,255,0.4);
1289
+ font-style: italic;
1290
  }
1291
  </style>
1292
  """, unsafe_allow_html=True)
 
1420
  <div class="legend-color" style="background-color: #ffcdd2;"></div>
1421
  <span>Slight Worsening (0–0.5)</span>
1422
  </div>
1423
+ <i style="display: block; margin-top: 12px; font-size: 12px; color: #666; font-style: italic;">
1424
  Note: Positive slope indicates increasing fatigue event frequency over weeks.
1425
  </i>
1426
  </div>
 
1441
  <div class="legend-item">
1442
  <div class="legend-color" style="background-color: #c8e6c9;"></div>
1443
  <span>Slight Improvement (−0.5 to 0)</span>
1444
+ </div>
1445
+ <i style="display: block; margin-top: 12px; font-size: 12px; color: #666; font-style: italic;">
1446
  Note: Negative slope reflects a consistent decline in fatigue events.
1447
  </i>
1448
+ </div>
 
1449
  <div class="legend-box">
1450
  <div class="legend-title">One-Time Events (Zero Slope):</div>
1451
  <div class="legend-item">
 
1459
  </div>
1460
  """, unsafe_allow_html=True)
1461
 
1462
+ # ===============================================================
1463
+ # PLOT FUNCTION — UPDATED: color for slope=0 is now #FFD700
1464
+ # ===============================================================
1465
+ def plot_chart(data, title):
1466
+ if data.empty:
1467
+ fig = go.Figure()
1468
+ fig.add_annotation(
1469
+ text="No Data",
1470
+ x=0.5, y=0.5,
1471
+ showarrow=False,
1472
+ font_size=16
1473
+ )
1474
+ fig.update_layout(height=350, title=dict(text=title, x=0.5))
1475
+ return fig
1476
+
1477
+ data_sorted = data.sort_values('weekly_avg', ascending=False)
1478
+
1479
+ def get_color(slope):
1480
+ if slope == 0:
1481
+ return "#FFD700" # ✅ Yellow for One Time Event
1482
+ elif slope > 0:
1483
+ if slope < 0.5:
1484
+ return "#ffcdd2"
1485
+ elif slope < 1.0:
1486
+ return "#ef9a9a"
1487
+ elif slope < 1.5:
1488
+ return "#e57373"
1489
+ else:
1490
+ return "#d32f2f"
1491
+ else: # slope < 0
1492
+ if slope > -0.5:
1493
+ return "#c8e6c9"
1494
+ elif slope > -1.0:
1495
+ return "#a5d6a7"
1496
+ elif slope > -1.5:
1497
+ return "#81c784"
1498
+ else:
1499
+ return "#388e3c"
1500
+
1501
+ colors = [get_color(s) for s in data_sorted["slope"]]
1502
+
1503
+ bar_trace = go.Bar(
1504
+ x=data_sorted[col_operator].astype(str),
1505
+ y=data_sorted["weekly_avg"],
1506
+ marker=dict(
1507
+ color=colors,
1508
+ line=dict(width=2, color="rgba(0,0,0,0.2)")
1509
+ ),
1510
+ text=[f"{v:.1f}" for v in data_sorted["weekly_avg"]],
1511
+ textposition="outside",
1512
+ hovertemplate=(
1513
+ "<b>%{x}</b><br>" +
1514
+ "Weekly Avg: %{y:.2f}<br>" +
1515
+ "Trend Slope: %{customdata[0]:+.3f}<br>" +
1516
+ "Total Events: %{customdata[1]}<br>" +
1517
+ "Weeks Active: %{customdata[2]}<br>" +
1518
+ "<extra></extra>"
1519
+ ),
1520
+ customdata=np.stack([data_sorted["slope"], data_sorted["total_events"], data_sorted["n_weeks"]], axis=-1)
1521
+ )
1522
 
1523
+ fig = go.Figure(bar_trace)
1524
+ fig.update_layout(
1525
+ title=dict(text=f"<b>{title}</b>", x=0.5),
1526
+ height=450,
1527
+ margin=dict(l=50, r=20, t=60, b=120),
1528
+ xaxis_title="<b>Operator Name</b>",
1529
+ yaxis_title="<b>Weekly Avg Events</b>",
1530
+ font=dict(family="Segoe UI", size=12),
1531
+ bargap=0.3,
1532
+ plot_bgcolor="rgba(0,0,0,0)",
1533
+ paper_bgcolor="rgba(0,0,0,0)",
1534
+ xaxis=dict(tickangle=45)
1535
+ )
1536
+ return fig
1537
 
1538
  # ===============================================================
1539
  # CHARTS
 
1604
  st.info("No HAULING COAL data for analysis.")
1605
 
1606
  # ===============================================================
1607
+ # RECOMMENDATIONS — FORMAT BARU: Judul + 3 List Langsung
1608
  # ===============================================================
1609
  col_rec1, col_rec2 = st.columns(2)
1610
 
1611
+ # === OB HAULER RECOMMENDATIONS (3-point list) ===
1612
+ with col_rec1:
1613
  if not top_ob.empty:
1614
  w = len(top_ob[top_ob['slope'] > 0])
1615
  ot = len(top_ob[top_ob['slope'] == 0])
1616
  avg = top_ob['weekly_avg'].mean()
1617
+
1618
+ rec_list_ob = []
1619
+
1620
+ # Point 1: Trend-driven action
1621
  if w > 5:
1622
+ rec_list_ob.append("Conduct targeted fatigue risk assessments for operators with worsening trends (slope > 0).")
 
1623
  elif ot > 4:
1624
+ rec_list_ob.append("Investigate data completeness — high count of One Time Events may reflect inconsistent reporting.")
 
1625
  elif avg > 8:
1626
+ rec_list_ob.append("Review shift scheduling and rest-break compliance to reduce event frequency.")
 
1627
  else:
1628
+ rec_list_ob.append("Continue current protocols with routine monitoring of top-risk operators.")
 
 
 
1629
 
1630
+ # Point 2: One-Time Event follow-up
1631
+ if ot > 0:
1632
+ rec_list_ob.append(f"Re-engage {ot} operators flagged as <b>One Time Event</b> to verify activity continuity.")
 
 
 
 
 
 
 
 
 
 
1633
  else:
1634
+ rec_list_ob.append("No One Time Event operators identified trend analysis remains reliable.")
 
 
 
 
1635
 
1636
+ # Point 3: Benchmarking
1637
+ rec_list_ob.append(f"Use cohort average ({avg:.2f} events/week) as baseline for monthly fatigue KPI reviews.")
1638
+
1639
+ # Ensure exactly 3 items
1640
+ while len(rec_list_ob) < 3:
1641
+ rec_list_ob.append("—")
1642
 
 
 
1643
  st.markdown("### OB HAULER Recommendations")
1644
  st.markdown(f"""
1645
  <div class="recommendation-box">
1646
  <div class="recommendation-title">Action Plan</div>
1647
+ <ul>
1648
+ <li>{rec_list_ob[0]}</li>
1649
+ <li>{rec_list_ob[1]}</li>
1650
+ <li>{rec_list_ob[2]}</li>
1651
+ </ul>
1652
+ <div class="recommendation-reason">
1653
+ Based on trend slope, activity duration, and cohort event frequency.
1654
+ </div>
1655
  </div>
1656
  """, unsafe_allow_html=True)
1657
  else:
1658
  st.info("No OB HAULER recommendations.")
1659
 
1660
+ # === HAULING COAL RECOMMENDATIONS (3-point list) ===
1661
  with col_rec2:
1662
+ if not top_coal.empty:
1663
+ w = len(top_coal[top_coal['slope'] > 0])
1664
+ ot = len(top_coal[top_coal['slope'] == 0])
1665
+ avg = top_coal['weekly_avg'].mean()
1666
+
1667
+ rec_list_coal = []
1668
+
1669
+ # Point 1: Trend-driven action
1670
+ if w > 5:
1671
+ rec_list_coal.append("Conduct targeted fatigue risk assessments for operators with worsening trends (slope > 0).")
1672
+ elif ot > 4:
1673
+ rec_list_coal.append("Investigate data completeness — high count of One Time Events may reflect inconsistent reporting.")
1674
+ elif avg > 8:
1675
+ rec_list_coal.append("Review shift scheduling and rest-break compliance to reduce event frequency.")
1676
+ else:
1677
+ rec_list_coal.append("Continue current protocols with routine monitoring of top-risk operators.")
1678
+
1679
+ # Point 2: One-Time Event follow-up
1680
+ if ot > 0:
1681
+ rec_list_coal.append(f"Re-engage {ot} operators flagged as <b>One Time Event</b> to verify activity continuity.")
1682
+ else:
1683
+ rec_list_coal.append("No One Time Event operators identified — trend analysis remains reliable.")
1684
+
1685
+ # Point 3: Benchmarking
1686
+ rec_list_coal.append(f"Use cohort average ({avg:.2f} events/week) as baseline for monthly fatigue KPI reviews.")
1687
+
1688
+ # Ensure exactly 3 items
1689
+ while len(rec_list_coal) < 3:
1690
+ rec_list_coal.append("—")
1691
+
1692
  st.markdown("### HAULING COAL Recommendations")
1693
  st.markdown(f"""
1694
  <div class="recommendation-box">
1695
  <div class="recommendation-title">Action Plan</div>
1696
+ <ul>
1697
+ <li>{rec_list_coal[0]}</li>
1698
+ <li>{rec_list_coal[1]}</li>
1699
+ <li>{rec_list_coal[2]}</li>
1700
+ </ul>
1701
+ <div class="recommendation-reason">
1702
+ Based on trend slope, activity duration, and cohort event frequency.
1703
+ </div>
1704
  </div>
1705
  """, unsafe_allow_html=True)
1706
  else:
 
1708
 
1709
  except Exception as e:
1710
  st.error(f"Error in Top 10 Operator analysis: {str(e)}")
1711
+ # st.exception(e) # Uncomment during development only
 
 
1712
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1713
  st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
1714