SHELLAPANDIANGANHUNGING commited on
Commit
0bb8319
·
verified ·
1 Parent(s): 51366a9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +402 -136
app.py CHANGED
@@ -1659,226 +1659,492 @@ else:
1659
  st.exception(e) # optionally show full traceback during dev
1660
 
1661
 
1662
-
1663
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1664
- st.markdown('<div style="text-align: center; font-size: 1.5em; font-weight: bold; margin-bottom: 20px;">OBJECTIVE 6: Instant Insights & Recommendations</div>', unsafe_allow_html=True)
1665
-
1666
- # Membagi tampilan menjadi dua kolom
1667
- col_insights, col_recs = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
1668
 
1669
- # Helper function for red % formatting
1670
  def format_red_pct(value):
1671
  return f'<span style="color: #d32f2f; font-weight: bold;">{value:.1f}%</span>'
1672
 
1673
- # Kolom kiri: Insights by Advanced Analytics
 
 
 
1674
  with col_insights:
1675
- st.markdown('<div style="text-align: center; font-size: 1.3em; font-weight: bold; margin-bottom: 15px;">Insights by Advanced Analytics</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
1676
 
1677
- # 1. Critical Hour Analysis (2-5 AM)
1678
- critical_hours = [2, 3, 4, 5]
1679
  critical_alerts = df[df['hour'].isin(critical_hours)]
1680
  critical_pct = (len(critical_alerts) / len(df)) * 100 if len(df) > 0 else 0
1681
 
1682
- st.markdown(f"**Critical Hour Risk (3-6 AM)**", unsafe_allow_html=True)
1683
- # Use conditional formatting for background color
1684
  bg_color = "#ffcccc" if critical_pct > 50 else "#ffebcc" if critical_pct > 25 else "#ffffcc" if critical_pct > 10 else "#e6ffe6"
1685
  st.markdown(
1686
- f'<div style="background-color: {bg_color}; padding: 10px; border-radius: 5px; margin-bottom: 10px;">'
1687
- f'Critical Hour Alerts: <b>{len(critical_alerts)}</b> ({format_red_pct(critical_pct)} of total alerts)'
1688
- f'</div>',
 
 
 
 
 
 
 
 
1689
  unsafe_allow_html=True
1690
  )
 
1691
  if critical_pct > 10:
1692
- st.warning(f"High risk: {format_red_pct(critical_pct)} of fatigue alerts occur during critical hours (3-6 AM). This is a known circadian dip period.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1693
  else:
1694
- st.info(f"{format_red_pct(critical_pct)} of alerts occur during critical hours. This is within acceptable range.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1695
 
1696
- # 2. High-Speed Fatigue Analysis (Environmental Risk)
1697
- if col_speed and col_speed in df.columns:
1698
- high_speed_threshold = df[col_speed].quantile(0.75) if not df[col_speed].dropna().empty else 0
1699
- high_speed_fatigue = df[df[col_speed] >= high_speed_threshold] if high_speed_threshold > 0 else pd.DataFrame()
1700
  high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
1701
 
1702
  st.markdown(f"**High-Speed Fatigue Risk (Speed > {high_speed_threshold:.0f} km/h)**", unsafe_allow_html=True)
1703
  st.markdown(
1704
- f'<div style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; border: 1px solid #dee2e6; margin-bottom: 10px;">'
1705
- f'<b>High-Speed Fatigue Events:</b> <span style="color: #1a73e8; font-weight: bold;">{len(high_speed_fatigue)}</span>'
1706
- f' ({format_red_pct(high_speed_pct)} of total alerts)'
1707
- f'</div>',
 
 
 
 
 
 
 
 
 
1708
  unsafe_allow_html=True
1709
  )
 
1710
  if high_speed_pct > 20:
1711
- st.warning(f"High risk: {format_red_pct(high_speed_pct)} of fatigue alerts occur at high speeds. This increases accident severity potential.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1712
  else:
1713
- st.info(f"{format_red_pct(high_speed_pct)} of alerts occur at high speeds. This is within acceptable range.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1714
  else:
1715
- st.info("Speed data not available for High-Speed Fatigue Analysis.")
 
 
 
 
 
 
 
 
 
 
 
 
1716
 
1717
  # 3. Shift Pattern Analysis
1718
  if col_shift and col_shift in df.columns:
1719
- shift_counts = df[col_shift].value_counts()
1720
  st.markdown(f"**Shift Pattern Risk**", unsafe_allow_html=True)
 
1721
  for shift_val in shift_counts.index:
1722
  count = shift_counts[shift_val]
1723
  shift_pct = (count / len(df)) * 100
1724
  st.markdown(
1725
- f'<div style="background-color: #f8f9fa; padding: 8px; border-radius: 5px; border: 1px solid #e9ecef; margin: 5px 0;">'
1726
- f'<b>Shift {shift_val} Alerts:</b> <span style="color: #1a73e8; font-weight: bold;">{count}</span>'
1727
- f' ({format_red_pct(shift_pct)} of total alerts)'
1728
- f'</div>',
 
 
 
 
 
 
 
 
 
1729
  unsafe_allow_html=True
1730
  )
 
1731
  if shift_pct > 50:
1732
- st.warning(f"Shift {shift_val} has disproportionately high alerts ({format_red_pct(shift_pct)}). Review shift scheduling and workload.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1733
  else:
1734
- st.info(f"Shift {shift_val} alert distribution is acceptable ({format_red_pct(shift_pct)}).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1735
  else:
1736
- st.info("Shift data not available for Shift Pattern Analysis.")
 
 
 
 
 
 
 
 
 
 
 
 
1737
 
1738
- # 4. Operator Risk Profiling
1739
  if col_operator and col_operator in df.columns:
1740
  operator_alerts = df[col_operator].value_counts()
1741
  top_risk_operators = operator_alerts.head(5)
1742
-
1743
  st.markdown(f"**High-Risk Operator Identification**", unsafe_allow_html=True)
 
1744
  for op_name, count in top_risk_operators.items():
1745
  op_pct = (count / len(df)) * 100
1746
- # Anonymize name: first part only
1747
- display_name = op_name.split()[0] if isinstance(op_name, str) else str(op_name)
1748
  st.markdown(
1749
- f'<div style="background-color: #f8f9fa; padding: 8px; border-radius: 5px; border: 1px solid #e9ecef; margin: 5px 0;">'
1750
- f'<b>Operator: {display_name}</b> — <span style="color: #1a73e8; font-weight: bold;">{count}</span> alerts'
1751
- f' ({format_red_pct(op_pct)} of total alerts)'
1752
- f'</div>',
 
 
 
 
 
 
 
 
 
1753
  unsafe_allow_html=True
1754
  )
 
1755
  if op_pct > 5:
1756
- st.warning(f"Operator {display_name} has high fatigue risk ({format_red_pct(op_pct)} of alerts). Consider coaching or rest plan.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1757
  else:
1758
- st.info(f"Operator {display_name} fatigue risk is within acceptable range ({format_red_pct(op_pct)}).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1759
  else:
1760
- st.info("Operator data not available for Operator Risk Profiling.")
 
 
 
 
 
 
 
 
 
 
 
 
1761
 
1762
 
1763
- # Kolom kanan: AI Recommendations
1764
  with col_recs:
1765
- st.markdown('<div style="text-align: center; font-size: 1.3em; font-weight: bold; margin-bottom: 15px;">Recommendations</div>', unsafe_allow_html=True)
1766
- ai_recs = []
1767
- insights_found = []
1768
-
1769
- # Peak hour
1770
- if "hour" in df.columns and not df.empty:
1771
- peak_hour = df["hour"].value_counts().idxmax()
1772
- critical_hours = [2, 3, 4, 5]
1773
- if peak_hour in critical_hours:
1774
- insights_found.append(f"Most fatigue risk occurs at **{peak_hour}:00** — during critical circadian low period (3-6 AM). Consider enhanced monitoring.")
1775
- else:
1776
- insights_found.append(f"Most fatigue risk occurs at **{peak_hour}:00** — likely due to circadian drop.")
1777
-
1778
- # Risk shift
1779
- if col_shift and not df.empty:
1780
- worst_shift = df[col_shift].value_counts().idxmax()
1781
- insights_found.append(f"Highest fatigue recorded in **Shift {worst_shift}** — review scheduling & workload.")
1782
-
1783
- # Worst operator
1784
- if col_operator and not df.empty:
1785
- worst_operator = df[col_operator].value_counts().idxmax()
1786
- display_name = worst_operator.split()[0] if isinstance(worst_operator, str) else str(worst_operator)
1787
- insights_found.append(f"Operator at highest risk: **{display_name}** — suggested coaching or rest plan.")
1788
 
1789
- # Duration risk
1790
- if "duration_sec" in df.columns and not df.empty:
1791
- avg_duration = df["duration_sec"].mean()
1792
- if not pd.isna(avg_duration) and avg_duration > 10:
1793
- insights_found.append("Long fatigue event duration suggests slow response — improve alerting training.")
1794
 
1795
- # High-speed insight (add conditionally)
1796
- if col_speed and col_speed in df.columns and high_speed_pct > 20:
1797
- insights_found.append("High-speed fatigue events exceed threshold — integrate with speed management.")
 
 
 
 
 
1798
 
1799
- # Generate recommendations based on found insights
1800
- if insights_found:
1801
- if any("circadian low" in i.lower() for i in insights_found):
1802
- ai_recs.append({
1803
- "recommendation": "Deploy enhanced fatigue monitoring systems (e.g., EOR) specifically during 3-6 AM shifts.",
1804
- "data_point": f"Critical Hour Alerts: {len(critical_alerts)} ({format_red_pct(critical_pct)} of total alerts)",
1805
- "reason": "High percentage of alerts occurring during the known circadian low period (3-6 AM) indicates increased risk during these hours."
1806
- })
1807
- if any("shift" in i.lower() for i in insights_found):
1808
- shift_pct_val = (df[col_shift].value_counts()[worst_shift] / len(df)) * 100
1809
- ai_recs.append({
1810
- "recommendation": "Review shift rotation schedules to minimize consecutive high-risk shifts.",
1811
- "data_point": f"Shift {worst_shift} Alerts: {df[col_shift].value_counts()[worst_shift]} ({format_red_pct(shift_pct_val)} of total alerts)",
1812
- "reason": f"The identified high-risk shift ({worst_shift}) has the highest number of fatigue alerts, suggesting scheduling or workload issues."
1813
- })
1814
- if any("operator" in i.lower() for i in insights_found):
1815
- op_pct_val = (df[col_operator].value_counts()[worst_operator] / len(df)) * 100
1816
  ai_recs.append({
1817
- "recommendation": "Initiate individual coaching or mandatory rest periods for high-risk operators.",
1818
- "data_point": f"Operator {display_name} Alerts: {df[col_operator].value_counts()[worst_operator]} ({format_red_pct(op_pct_val)} of total alerts)",
1819
- "reason": f"The identified high-risk operator ({display_name}) has the highest number of fatigue alerts, indicating a need for targeted intervention."
 
1820
  })
1821
- if any("duration" in i.lower() for i in insights_found):
 
 
 
 
 
 
1822
  ai_recs.append({
1823
- "recommendation": "Review and improve alert response protocols and training.",
1824
- "data_point": f"Average Fatigue Event Duration: {avg_duration:.2f} seconds",
1825
- "reason": "Long average duration suggests potential delays in response time or alert acknowledgment, requiring protocol review."
 
1826
  })
1827
- if any("high-speed" in i.lower() for i in insights_found):
 
 
 
 
 
 
 
1828
  ai_recs.append({
1829
- "recommendation": "Implement speed management strategies in conjunction with fatigue monitoring.",
1830
- "data_point": f"High-Speed Fatigue Events: {len(high_speed_fatigue)} ({format_red_pct(high_speed_pct)} of total alerts)",
1831
- "reason": "A significant percentage of alerts occur at high speeds, increasing accident severity risk. Speed control is crucial."
 
1832
  })
1833
- if not ai_recs:
 
 
 
 
1834
  ai_recs.append({
1835
- "recommendation": "Data quality is sufficient. Focus on implementing recommendations from Objectives 1-5.",
1836
- "data_point": "General Data Quality Check",
1837
- "reason": "No specific high-impact insights were automatically identified from the aggregated data in this section."
 
1838
  })
1839
 
1840
- # Display recommendations with consistent styling
1841
- for rec in ai_recs:
1842
- st.markdown(f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1843
  <div style="
1844
- background: #f8f9fa;
1845
- border: 1px solid #dee2e6;
1846
  border-radius: 8px;
1847
- padding: 15px;
1848
- margin: 10px 0;
1849
  color: #2c3e50;
1850
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1851
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1852
  ">
1853
  <div style="
1854
  font-weight: bold;
1855
- color: #2c3e50;
1856
- margin-bottom: 8px;
1857
- font-size: 14px;
1858
- background: #e9ecef;
 
 
 
 
 
 
 
 
 
 
1859
  padding: 8px;
1860
  border-radius: 5px;
1861
- border-left: 4px solid #495057;
1862
- ">AI Recommendation</div>
1863
- <div style="padding-top: 8px; font-size: 14px; margin-bottom: 10px;">
1864
- <strong>Action:</strong> {rec['recommendation']}
1865
- </div>
1866
- <div style="font-size: 12px; padding: 8px; background: #e9ecef; border-radius: 5px; margin-top: 5px;">
1867
  <strong>Data Point:</strong> {rec['data_point']}
1868
  </div>
1869
- <div style="font-size: 12px; padding: 8px; background: #f1f1f1; border-radius: 5px; margin-top: 5px;">
 
 
 
 
 
 
 
1870
  <strong>AI Reasoning:</strong> {rec['reason']}
1871
  </div>
1872
  </div>
1873
- """, unsafe_allow_html=True)
1874
- else:
1875
- st.info("No specific data points available for AI recommendations. Ensure relevant columns (hour, shift, operator, duration, speed) are present and populated.")
1876
 
1877
  # ================= FOOTER ===========================
1878
  st.markdown("---")
1879
  st.markdown(
1880
- '<div style="text-align: center; color: #6c757d; font-size: 0.9em; margin-top: 20px;">'
1881
- 'FatigueAnalyzer - Transforming Mining Safety with Intelligent Analytics | Contact: info@bukittechnology.com'
1882
- '</div>',
 
 
 
 
 
 
 
 
 
1883
  unsafe_allow_html=True
1884
  )
 
1659
  st.exception(e) # optionally show full traceback during dev
1660
 
1661
 
 
1662
  # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1663
+ st.markdown(
1664
+ '''
1665
+ <div style="
1666
+ text-align: center;
1667
+ font-size: 1.5em;
1668
+ font-weight: bold;
1669
+ margin-bottom: 20px;
1670
+ color: #2c3e50;
1671
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1672
+ ">
1673
+ OBJECTIVE 6: Instant Insights & Recommendations
1674
+ </div>
1675
+ ''',
1676
+ unsafe_allow_html=True
1677
+ )
1678
 
1679
+ # Helper: format % in red (hazard indicator)
1680
  def format_red_pct(value):
1681
  return f'<span style="color: #d32f2f; font-weight: bold;">{value:.1f}%</span>'
1682
 
1683
+ # Split layout
1684
+ col_insights, col_recs = st.columns(2)
1685
+
1686
+ # ================= LEFT COLUMN: Insights =================
1687
  with col_insights:
1688
+ st.markdown(
1689
+ '''
1690
+ <div style="
1691
+ text-align: center;
1692
+ font-size: 1.3em;
1693
+ font-weight: bold;
1694
+ margin-bottom: 15px;
1695
+ color: #2c3e50;
1696
+ ">Insights by Advanced Analytics</div>
1697
+ ''',
1698
+ unsafe_allow_html=True
1699
+ )
1700
 
1701
+ # 1. Critical Hour Analysis (3–6 AM)
1702
+ critical_hours = [3, 4, 5, 6] # sesuai label "3-6 AM"
1703
  critical_alerts = df[df['hour'].isin(critical_hours)]
1704
  critical_pct = (len(critical_alerts) / len(df)) * 100 if len(df) > 0 else 0
1705
 
1706
+ st.markdown(f"**Critical Hour Risk (36 AM)**", unsafe_allow_html=True)
 
1707
  bg_color = "#ffcccc" if critical_pct > 50 else "#ffebcc" if critical_pct > 25 else "#ffffcc" if critical_pct > 10 else "#e6ffe6"
1708
  st.markdown(
1709
+ f'''
1710
+ <div style="
1711
+ background-color: {bg_color};
1712
+ padding: 10px;
1713
+ border-radius: 5px;
1714
+ margin-bottom: 12px;
1715
+ font-family: 'Segoe UI', sans-serif;
1716
+ ">
1717
+ Critical Hour Alerts: <b>{len(critical_alerts)}</b> ({format_red_pct(critical_pct)} of total alerts)
1718
+ </div>
1719
+ ''',
1720
  unsafe_allow_html=True
1721
  )
1722
+
1723
  if critical_pct > 10:
1724
+ st.markdown(
1725
+ f'''
1726
+ <div style="
1727
+ background-color: #fff8e1;
1728
+ color: #5d4037;
1729
+ padding: 10px;
1730
+ border-radius: 5px;
1731
+ border-left: 4px solid #ff9800;
1732
+ margin-bottom: 15px;
1733
+ font-family: 'Segoe UI', sans-serif;
1734
+ ">
1735
+ ⚠️ <strong>High risk:</strong> {format_red_pct(critical_pct)} of fatigue alerts occur during critical hours (3–6 AM).
1736
+ This is a known circadian dip period.
1737
+ </div>
1738
+ ''',
1739
+ unsafe_allow_html=True
1740
+ )
1741
  else:
1742
+ st.markdown(
1743
+ f'''
1744
+ <div style="
1745
+ background-color: #e8f5e8;
1746
+ color: #2e7d32;
1747
+ padding: 10px;
1748
+ border-radius: 5px;
1749
+ border-left: 4px solid #4caf50;
1750
+ margin-bottom: 15px;
1751
+ font-family: 'Segoe UI', sans-serif;
1752
+ ">
1753
+ ✓ <strong>Acceptable:</strong> {format_red_pct(critical_pct)} of alerts occur during critical hours.
1754
+ This is within acceptable range.
1755
+ </div>
1756
+ ''',
1757
+ unsafe_allow_html=True
1758
+ )
1759
 
1760
+ # 2. High-Speed Fatigue Analysis
1761
+ if col_speed and col_speed in df.columns and not df[col_speed].dropna().empty:
1762
+ high_speed_threshold = df[col_speed].quantile(0.75)
1763
+ high_speed_fatigue = df[df[col_speed] >= high_speed_threshold]
1764
  high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
1765
 
1766
  st.markdown(f"**High-Speed Fatigue Risk (Speed > {high_speed_threshold:.0f} km/h)**", unsafe_allow_html=True)
1767
  st.markdown(
1768
+ f'''
1769
+ <div style="
1770
+ background-color: #f9f9f9;
1771
+ padding: 10px;
1772
+ border-radius: 5px;
1773
+ border: 1px solid #e0e0e0;
1774
+ margin-bottom: 12px;
1775
+ font-family: 'Segoe UI', sans-serif;
1776
+ ">
1777
+ High-Speed Fatigue Events: <span style="color: #1a73e8; font-weight: bold;">{len(high_speed_fatigue)}</span>
1778
+ ({format_red_pct(high_speed_pct)} of total alerts)
1779
+ </div>
1780
+ ''',
1781
  unsafe_allow_html=True
1782
  )
1783
+
1784
  if high_speed_pct > 20:
1785
+ st.markdown(
1786
+ f'''
1787
+ <div style="
1788
+ background-color: #fff8e1;
1789
+ color: #5d4037;
1790
+ padding: 10px;
1791
+ border-radius: 5px;
1792
+ border-left: 4px solid #ff9800;
1793
+ margin-bottom: 15px;
1794
+ font-family: 'Segoe UI', sans-serif;
1795
+ ">
1796
+ ⚠️ <strong>High risk:</strong> {format_red_pct(high_speed_pct)} of fatigue alerts occur at high speeds.
1797
+ This increases accident severity potential.
1798
+ </div>
1799
+ ''',
1800
+ unsafe_allow_html=True
1801
+ )
1802
  else:
1803
+ st.markdown(
1804
+ f'''
1805
+ <div style="
1806
+ background-color: #e8f5e8;
1807
+ color: #2e7d32;
1808
+ padding: 10px;
1809
+ border-radius: 5px;
1810
+ border-left: 4px solid #4caf50;
1811
+ margin-bottom: 15px;
1812
+ font-family: 'Segoe UI', sans-serif;
1813
+ ">
1814
+ ✓ <strong>Acceptable:</strong> {format_red_pct(high_speed_pct)} of alerts occur at high speeds.
1815
+ This is within acceptable range.
1816
+ </div>
1817
+ ''',
1818
+ unsafe_allow_html=True
1819
+ )
1820
  else:
1821
+ st.markdown(
1822
+ '''
1823
+ <div style="
1824
+ background-color: #f1f3f4;
1825
+ padding: 10px;
1826
+ border-radius: 5px;
1827
+ font-style: italic;
1828
+ color: #607d8b;
1829
+ margin-bottom: 15px;
1830
+ ">Speed data not available for High-Speed Fatigue Analysis.</div>
1831
+ ''',
1832
+ unsafe_allow_html=True
1833
+ )
1834
 
1835
  # 3. Shift Pattern Analysis
1836
  if col_shift and col_shift in df.columns:
1837
+ shift_counts = df[col_shift].value_counts().sort_index()
1838
  st.markdown(f"**Shift Pattern Risk**", unsafe_allow_html=True)
1839
+
1840
  for shift_val in shift_counts.index:
1841
  count = shift_counts[shift_val]
1842
  shift_pct = (count / len(df)) * 100
1843
  st.markdown(
1844
+ f'''
1845
+ <div style="
1846
+ background-color: #f9f9f9;
1847
+ padding: 8px;
1848
+ border-radius: 5px;
1849
+ border: 1px solid #e0e0e0;
1850
+ margin: 6px 0;
1851
+ font-family: 'Segoe UI', sans-serif;
1852
+ ">
1853
+ Shift {shift_val} Alerts: <span style="color: #1a73e8; font-weight: bold;">{count}</span>
1854
+ ({format_red_pct(shift_pct)} of total alerts)
1855
+ </div>
1856
+ ''',
1857
  unsafe_allow_html=True
1858
  )
1859
+
1860
  if shift_pct > 50:
1861
+ st.markdown(
1862
+ f'''
1863
+ <div style="
1864
+ background-color: #fff8e1;
1865
+ color: #5d4037;
1866
+ padding: 8px;
1867
+ border-radius: 5px;
1868
+ border-left: 4px solid #ff9800;
1869
+ margin: 6px 0 12px 0;
1870
+ font-family: 'Segoe UI', sans-serif;
1871
+ ">
1872
+ ⚠️ <strong>Disproportionate risk:</strong> Shift {shift_val} has {format_red_pct(shift_pct)} alerts.
1873
+ Review shift scheduling and workload.
1874
+ </div>
1875
+ ''',
1876
+ unsafe_allow_html=True
1877
+ )
1878
  else:
1879
+ st.markdown(
1880
+ f'''
1881
+ <div style="
1882
+ background-color: #e8f5e8;
1883
+ color: #2e7d32;
1884
+ padding: 8px;
1885
+ border-radius: 5px;
1886
+ border-left: 4px solid #4caf50;
1887
+ margin: 6px 0 12px 0;
1888
+ font-family: 'Segoe UI', sans-serif;
1889
+ ">
1890
+ ✓ <strong>Acceptable:</strong> Shift {shift_val} alert distribution is acceptable ({format_red_pct(shift_pct)}).
1891
+ </div>
1892
+ ''',
1893
+ unsafe_allow_html=True
1894
+ )
1895
  else:
1896
+ st.markdown(
1897
+ '''
1898
+ <div style="
1899
+ background-color: #f1f3f4;
1900
+ padding: 10px;
1901
+ border-radius: 5px;
1902
+ font-style: italic;
1903
+ color: #607d8b;
1904
+ margin-bottom: 15px;
1905
+ ">Shift data not available for Shift Pattern Analysis.</div>
1906
+ ''',
1907
+ unsafe_allow_html=True
1908
+ )
1909
 
1910
+ # 4. Operator Risk Profiling (anonymized)
1911
  if col_operator and col_operator in df.columns:
1912
  operator_alerts = df[col_operator].value_counts()
1913
  top_risk_operators = operator_alerts.head(5)
 
1914
  st.markdown(f"**High-Risk Operator Identification**", unsafe_allow_html=True)
1915
+
1916
  for op_name, count in top_risk_operators.items():
1917
  op_pct = (count / len(df)) * 100
1918
+ display_name = str(op_name).split()[0] if pd.notna(op_name) else "Unknown"
 
1919
  st.markdown(
1920
+ f'''
1921
+ <div style="
1922
+ background-color: #f9f9f9;
1923
+ padding: 8px;
1924
+ border-radius: 5px;
1925
+ border: 1px solid #e0e0e0;
1926
+ margin: 6px 0;
1927
+ font-family: 'Segoe UI', sans-serif;
1928
+ ">
1929
+ Operator: <b>{display_name}</b> — <span style="color: #1a73e8; font-weight: bold;">{count}</span> alerts
1930
+ ({format_red_pct(op_pct)} of total alerts)
1931
+ </div>
1932
+ ''',
1933
  unsafe_allow_html=True
1934
  )
1935
+
1936
  if op_pct > 5:
1937
+ st.markdown(
1938
+ f'''
1939
+ <div style="
1940
+ background-color: #fff8e1;
1941
+ color: #5d4037;
1942
+ padding: 8px;
1943
+ border-radius: 5px;
1944
+ border-left: 4px solid #ff9800;
1945
+ margin: 6px 0 12px 0;
1946
+ font-family: 'Segoe UI', sans-serif;
1947
+ ">
1948
+ ⚠️ <strong>High risk:</strong> Operator {display_name} has {format_red_pct(op_pct)} of alerts.
1949
+ Consider coaching or rest plan.
1950
+ </div>
1951
+ ''',
1952
+ unsafe_allow_html=True
1953
+ )
1954
  else:
1955
+ st.markdown(
1956
+ f'''
1957
+ <div style="
1958
+ background-color: #e8f5e8;
1959
+ color: #2e7d32;
1960
+ padding: 8px;
1961
+ border-radius: 5px;
1962
+ border-left: 4px solid #4caf50;
1963
+ margin: 6px 0 12px 0;
1964
+ font-family: 'Segoe UI', sans-serif;
1965
+ ">
1966
+ ✓ <strong>Acceptable:</strong> Operator {display_name} fatigue risk is within acceptable range ({format_red_pct(op_pct)}).
1967
+ </div>
1968
+ ''',
1969
+ unsafe_allow_html=True
1970
+ )
1971
  else:
1972
+ st.markdown(
1973
+ '''
1974
+ <div style="
1975
+ background-color: #f1f3f4;
1976
+ padding: 10px;
1977
+ border-radius: 5px;
1978
+ font-style: italic;
1979
+ color: #607d8b;
1980
+ margin-bottom: 15px;
1981
+ ">Operator data not available for Operator Risk Profiling.</div>
1982
+ ''',
1983
+ unsafe_allow_html=True
1984
+ )
1985
 
1986
 
1987
+ # ================= RIGHT COLUMN: AI Recommendations =================
1988
  with col_recs:
1989
+ st.markdown(
1990
+ '''
1991
+ <div style="
1992
+ text-align: center;
1993
+ font-size: 1.3em;
1994
+ font-weight: bold;
1995
+ margin-bottom: 15px;
1996
+ color: #2c3e50;
1997
+ ">Recommendations</div>
1998
+ ''',
1999
+ unsafe_allow_html=True
2000
+ )
 
 
 
 
 
 
 
 
 
 
 
2001
 
2002
+ ai_recs = []
 
 
 
 
2003
 
2004
+ # 1. Critical Hour Insight → Recommendation
2005
+ if critical_pct > 10:
2006
+ ai_recs.append({
2007
+ "title": "Enhance Monitoring During Circadian Low",
2008
+ "action": "Deploy enhanced fatigue monitoring systems (e.g., EOR or real-time biometrics) specifically during 3–6 AM shifts.",
2009
+ "data_point": f"Critical Hour Alerts: {len(critical_alerts)} ({format_red_pct(critical_pct)} of total alerts)",
2010
+ "reason": "High alert concentration during the circadian trough (3–6 AM) significantly increases operational risk."
2011
+ })
2012
 
2013
+ # 2. High-Speed Insight Recommendation
2014
+ if col_speed in df.columns and not df[col_speed].empty:
2015
+ high_speed_threshold = df[col_speed].quantile(0.75)
2016
+ high_speed_fatigue = df[df[col_speed] >= high_speed_threshold]
2017
+ high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
2018
+ if high_speed_pct > 20:
 
 
 
 
 
 
 
 
 
 
 
2019
  ai_recs.append({
2020
+ "title": "Integrate Speed & Fatigue Controls",
2021
+ "action": "Implement speed advisory or automatic speed reduction triggers when fatigue alerts occur at high speeds.",
2022
+ "data_point": f"High-Speed Fatigue Events: {len(high_speed_fatigue)} ({format_red_pct(high_speed_pct)} of total alerts)",
2023
+ "reason": "Fatigue at high speed multiplies collision severity; proactive speed management reduces fatality risk."
2024
  })
2025
+
2026
+ # 3. Shift Imbalance → Recommendation
2027
+ if col_shift in df.columns:
2028
+ shift_counts = df[col_shift].value_counts()
2029
+ worst_shift = shift_counts.idxmax()
2030
+ worst_pct = (shift_counts[worst_shift] / len(df)) * 100
2031
+ if worst_pct > 50:
2032
  ai_recs.append({
2033
+ "title": "Rebalance Shift Workload",
2034
+ "action": "Review shift rotation policies, rest intervals, and task allocation for Shift {worst_shift}.",
2035
+ "data_point": f"Shift {worst_shift} Alerts: {shift_counts[worst_shift]} ({format_red_pct(worst_pct)} of total alerts)",
2036
+ "reason": f"Overconcentration of fatigue events in one shift suggests systemic scheduling or ergonomic issues."
2037
  })
2038
+
2039
+ # 4. High-Risk Operator → Recommendation
2040
+ if col_operator in df.columns:
2041
+ operator_counts = df[col_operator].value_counts()
2042
+ worst_op = operator_counts.idxmax()
2043
+ worst_op_pct = (operator_counts[worst_op] / len(df)) * 100
2044
+ display_name = str(worst_op).split()[0] if pd.notna(worst_op) else "Unknown"
2045
+ if worst_op_pct > 5:
2046
  ai_recs.append({
2047
+ "title": "Individual Risk Intervention",
2048
+ "action": "Initiate targeted coaching, health screening, or adjusted rest plan for Operator {display_name}.",
2049
+ "data_point": f"Operator {display_name} Alerts: {operator_counts[worst_op]} ({format_red_pct(worst_op_pct)} of total alerts)",
2050
+ "reason": "Persistent high alerts for one individual may indicate medical, behavioral, or training factors requiring support."
2051
  })
2052
+
2053
+ # 5. Long Duration → Recommendation
2054
+ if "duration_sec" in df.columns and not df["duration_sec"].dropna().empty:
2055
+ avg_duration = df["duration_sec"].mean()
2056
+ if pd.notna(avg_duration) and avg_duration > 10:
2057
  ai_recs.append({
2058
+ "title": "Improve Alert Response Protocol",
2059
+ "action": "Review and retrain on fatigue alert acknowledgment procedures; consider haptic or multi-sensory alerts.",
2060
+ "data_point": f"Average Fatigue Event Duration: {avg_duration:.1f} seconds",
2061
+ "reason": "Long event duration (>10s) suggests delayed recognition or response—increasing near-miss potential."
2062
  })
2063
 
2064
+ # Final fallback
2065
+ if not ai_recs:
2066
+ ai_recs.append({
2067
+ "title": "Baseline Monitoring",
2068
+ "action": "Continue routine monitoring and periodic review of fatigue trends.",
2069
+ "data_point": "No high-risk patterns detected in current dataset.",
2070
+ "reason": "Data indicates balanced risk distribution—maintain current controls and re-evaluate monthly."
2071
+ })
2072
+
2073
+ # Render recommendations
2074
+ for rec in ai_recs:
2075
+ # Replace placeholder {worst_shift}/{display_name} if needed
2076
+ action = rec["action"]
2077
+ if "{worst_shift}" in action and col_shift in df.columns:
2078
+ worst_shift = df[col_shift].value_counts().idxmax()
2079
+ action = action.replace("{worst_shift}", str(worst_shift))
2080
+ if "{display_name}" in action and col_operator in df.columns:
2081
+ worst_op = df[col_operator].value_counts().idxmax()
2082
+ display_name = str(worst_op).split()[0] if pd.notna(worst_op) else "Unknown"
2083
+ action = action.replace("{display_name}", display_name)
2084
+
2085
+ st.markdown(
2086
+ f'''
2087
  <div style="
2088
+ background: #ffffff;
2089
+ border: 1px solid #e0e0e0;
2090
  border-radius: 8px;
2091
+ padding: 16px;
2092
+ margin: 12px 0;
2093
  color: #2c3e50;
2094
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
2095
+ box-shadow: 0 2px 6px rgba(0,0,0,0.05);
2096
  ">
2097
  <div style="
2098
  font-weight: bold;
2099
+ color: #1a237e;
2100
+ font-size: 15px;
2101
+ margin-bottom: 10px;
2102
+ padding-bottom: 6px;
2103
+ border-bottom: 1px solid #e0e0e0;
2104
+ ">📌 {rec['title']}</div>
2105
+
2106
+ <div style="font-size: 14px; margin-bottom: 12px;">
2107
+ <strong>Action:</strong> {action}
2108
+ </div>
2109
+
2110
+ <div style="
2111
+ font-size: 12px;
2112
+ background: #f5f9ff;
2113
  padding: 8px;
2114
  border-radius: 5px;
2115
+ margin-top: 8px;
2116
+ ">
 
 
 
 
2117
  <strong>Data Point:</strong> {rec['data_point']}
2118
  </div>
2119
+
2120
+ <div style="
2121
+ font-size: 12px;
2122
+ background: #f8f9fa;
2123
+ padding: 8px;
2124
+ border-radius: 5px;
2125
+ margin-top: 6px;
2126
+ ">
2127
  <strong>AI Reasoning:</strong> {rec['reason']}
2128
  </div>
2129
  </div>
2130
+ ''',
2131
+ unsafe_allow_html=True
2132
+ )
2133
 
2134
  # ================= FOOTER ===========================
2135
  st.markdown("---")
2136
  st.markdown(
2137
+ '''
2138
+ <div style="
2139
+ text-align: center;
2140
+ color: #6c757d;
2141
+ font-size: 0.9em;
2142
+ font-family: 'Segoe UI', sans-serif;
2143
+ margin-top: 20px;
2144
+ padding: 10px;
2145
+ ">
2146
+ FatigueAnalyzer — Transforming Mining Safety with Intelligent Analytics | Contact: info@bukittechnology.com
2147
+ </div>
2148
+ ''',
2149
  unsafe_allow_html=True
2150
  )