SHELLAPANDIANGANHUNGING commited on
Commit
83e00e2
·
verified ·
1 Parent(s): 0bb8319

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +129 -429
app.py CHANGED
@@ -1659,492 +1659,192 @@ else:
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
- )
 
1659
  st.exception(e) # optionally show full traceback during dev
1660
 
1661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1662
 
1663
+ # =================== OBJECTIVE 6: Automated Insights & AI Recommendations =====================
1664
+ st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
 
1665
 
1666
+ # Membagi tampilan menjadi dua kolom
1667
  col_insights, col_recs = st.columns(2)
1668
 
1669
+ # Kolom kiri: Insights by Advanced Analytics
1670
  with col_insights:
1671
+ st.subheader("Insights by Advanced Analytics")
 
 
 
 
 
 
 
 
 
 
 
1672
 
1673
+ # 1. Critical Hour Analysis (2-5 AM)
1674
+ critical_hours = [2, 3, 4, 5]
1675
  critical_alerts = df[df['hour'].isin(critical_hours)]
1676
  critical_pct = (len(critical_alerts) / len(df)) * 100 if len(df) > 0 else 0
1677
 
1678
+ st.markdown(f"**Critical Hour Risk (3-6 AM)**")
1679
+ # Use conditional formatting for background color
1680
  bg_color = "#ffcccc" if critical_pct > 50 else "#ffebcc" if critical_pct > 25 else "#ffffcc" if critical_pct > 10 else "#e6ffe6"
1681
+ st.markdown(f'<div style="background-color: {bg_color}; padding: 10px; border-radius: 5px;">Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}% of total alerts)</div>', unsafe_allow_html=True)
1682
+ if critical_pct > 10: # If more than 10% of alerts happen in critical hours
1683
+ st.warning(f"High risk: {critical_pct:.1f}% of fatigue alerts occur during critical hours (3-6 AM). This is a known circadian dip period.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1684
  else:
1685
+ st.info(f"{critical_pct:.1f}% of alerts occur during critical hours. This is within acceptable range.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1686
 
1687
+ # 2. High-Speed Fatigue Analysis (Environmental Risk)
1688
+ if col_speed and col_speed in df.columns:
1689
+ high_speed_threshold = df[col_speed].quantile(0.75) if not df[col_speed].dropna().empty else 0 # Handle empty series
1690
+ high_speed_fatigue = df[df[col_speed] >= high_speed_threshold] if high_speed_threshold > 0 else pd.DataFrame()
1691
  high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
1692
+
1693
+ st.markdown(f"**High-Speed Fatigue Risk (Speed > {high_speed_threshold:.0f} km/h)**")
1694
+ st.metric("High-Speed Fatigue Events", f"{len(high_speed_fatigue)}", f"{high_speed_pct:.1f}% of total alerts")
1695
+ if high_speed_pct > 20: # If more than 20% of alerts happen at high speed
1696
+ st.warning(f"High risk: {high_speed_pct:.1f}% of fatigue alerts occur at high speeds. This increases accident severity potential.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1697
  else:
1698
+ st.info(f"{high_speed_pct:.1f}% of alerts occur at high speeds. This is within acceptable range.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1699
  else:
1700
+ st.info("Speed data not available for High-Speed Fatigue Analysis.")
 
 
 
 
 
 
 
 
 
 
 
 
1701
 
1702
  # 3. Shift Pattern Analysis
1703
  if col_shift and col_shift in df.columns:
1704
+ shift_counts = df[col_shift].value_counts()
1705
+ # shift_alerts_by_hour = df.groupby([col_shift, 'hour']).size().reset_index(name='alerts') # Tidak digunakan dalam tampilan ini
1706
+
1707
+ st.markdown(f"**Shift Pattern Risk**")
1708
  for shift_val in shift_counts.index:
1709
+ shift_pct = (shift_counts[shift_val] / len(df)) * 100
1710
+ st.metric(f"Shift {shift_val} Alerts", f"{shift_counts[shift_val]}", f"{shift_pct:.1f}% of total alerts")
1711
+ if shift_pct > 50: # If one shift has more than 50% of alerts
1712
+ st.warning(f"Shift {shift_val} has disproportionately high alerts ({shift_pct:.1f}%). Review shift scheduling and workload.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1713
  else:
1714
+ st.info(f"Shift {shift_val} alert distribution is acceptable ({shift_pct:.1f}%).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
  else:
1716
+ st.info("Shift data not available for Shift Pattern Analysis.")
 
 
 
 
 
 
 
 
 
 
 
 
1717
 
1718
+ # 4. Operator Risk Profiling
1719
  if col_operator and col_operator in df.columns:
1720
  operator_alerts = df[col_operator].value_counts()
1721
+ top_risk_operators = operator_alerts.head(5) # Top 5 operators by alerts
1722
+
1723
+ st.markdown(f"**High-Risk Operator Identification**")
1724
  for op_name, count in top_risk_operators.items():
1725
  op_pct = (count / len(df)) * 100
1726
+ st.metric(f"Operator: {op_name}", f"{count} alerts", f"{op_pct:.1f}% of total alerts")
1727
+ if op_pct > 5: # If an operator has more than 5% of all alerts
1728
+ st.warning(f"Operator {op_name} has high fatigue risk ({op_pct:.1f}% of alerts). Consider coaching or rest plan.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1729
  else:
1730
+ st.info(f"Operator {op_name} fatigue risk is within acceptable range ({op_pct:.1f}%).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1731
  else:
1732
+ st.info("Operator data not available for Operator Risk Profiling.")
 
 
 
 
 
 
 
 
 
 
 
 
1733
 
1734
 
1735
+ # Kolom kanan: AI Recommendations
1736
  with col_recs:
1737
+ st.subheader("Recommendations")
 
 
 
 
 
 
 
 
 
 
 
 
1738
  ai_recs = []
1739
+ insights_found = [] # Untuk menyimpan insight yang ditemukan
1740
+
1741
+ # Peak hour
1742
+ if "hour" in df.columns and not df.empty:
1743
+ peak_hour = df["hour"].value_counts().idxmax()
1744
+ critical_hours = [2, 3, 4, 5]
1745
+ if peak_hour in critical_hours:
1746
+ insights_found.append(f" Most fatigue risk occurs at **{peak_hour}:00** — during critical circadian low period (3-6 AM). Consider enhanced monitoring.")
1747
+ else:
1748
+ insights_found.append(f"Most fatigue risk occurs at **{peak_hour}:00** — likely due to circadian drop.")
1749
 
1750
+ # Risk shift
1751
+ if col_shift and not df.empty:
1752
+ worst_shift = df[col_shift].value_counts().idxmax()
1753
+ insights_found.append(f" Highest fatigue recorded in **Shift {worst_shift}** — review scheduling & workload.")
 
 
 
 
1754
 
1755
+ # Worst operator
1756
+ if col_operator and not df.empty:
1757
+ worst_operator = df[col_operator].value_counts().idxmax()
1758
+ insights_found.append(f" Operator at highest risk: **{worst_operator}** — suggested coaching or rest plan.")
1759
+
1760
+ # Duration risk
1761
+ if "duration_sec" in df.columns and not df.empty:
1762
+ avg_duration = df["duration_sec"].mean()
1763
+ if not pd.isna(avg_duration) and avg_duration > 10:
1764
+ insights_found.append(" Long fatigue event duration suggests slow response — improve alerting training.")
1765
+
1766
+ # Generate recommendations based on found insights
1767
+ if insights_found:
1768
+ # Contoh rekomendasi berdasarkan insight
1769
+ if any("circadian low" in i.lower() for i in insights_found):
1770
  ai_recs.append({
1771
+ "recommendation": "Deploy enhanced fatigue monitoring systems (e.g., EOR) specifically during 3-6 AM shifts.",
1772
+ "data_point": f"Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}% of total alerts)",
1773
+ "reason": "High percentage of alerts occurring during the known circadian low period (3-6 AM) indicates increased risk during these hours."
 
1774
  })
1775
+ if any("shift" in i.lower() for i in insights_found):
 
 
 
 
 
 
1776
  ai_recs.append({
1777
+ "recommendation": "Review shift rotation schedules to minimize consecutive high-risk shifts.",
1778
+ "data_point": f"Shift {worst_shift} Alerts: {df[col_shift].value_counts()[worst_shift]} ({(df[col_shift].value_counts()[worst_shift] / len(df)) * 100:.1f}% of total alerts)",
1779
+ "reason": f"The identified high-risk shift ({worst_shift}) has the highest number of fatigue alerts, suggesting scheduling or workload issues."
 
1780
  })
1781
+ if any("operator" in i.lower() for i in insights_found):
 
 
 
 
 
 
 
1782
  ai_recs.append({
1783
+ "recommendation": "Initiate individual coaching or mandatory rest periods for high-risk operators.",
1784
+ "data_point": f"Operator {worst_operator} Alerts: {df[col_operator].value_counts()[worst_operator]} ({(df[col_operator].value_counts()[worst_operator] / len(df)) * 100:.1f}% of total alerts)",
1785
+ "reason": f"The identified high-risk operator ({worst_operator}) has the highest number of fatigue alerts, indicating a need for targeted intervention."
 
1786
  })
1787
+ if any("duration" in i.lower() for i in insights_found):
 
 
 
 
1788
  ai_recs.append({
1789
+ "recommendation": "Review and improve alert response protocols and training.",
1790
+ "data_point": f"Average Fatigue Event Duration: {avg_duration:.2f} seconds",
1791
+ "reason": "Long average duration suggests potential delays in response time or alert acknowledgment, requiring protocol review."
1792
+ })
1793
+ if any("high-speed" in i.lower() for i in insights_found):
1794
+ ai_recs.append({
1795
+ "recommendation": "Implement speed management strategies in conjunction with fatigue monitoring.",
1796
+ "data_point": f"High-Speed Fatigue Events: {len(high_speed_fatigue)} ({high_speed_pct:.1f}% of total alerts)",
1797
+ "reason": "A significant percentage of alerts occur at high speeds, increasing accident severity risk. Speed control is crucial."
1798
+ })
1799
+ if not ai_recs:
1800
+ ai_recs.append({
1801
+ "recommendation": "Data quality is sufficient. Focus on implementing recommendations from Objectives 1-5.",
1802
+ "data_point": "General Data Quality Check",
1803
+ "reason": "No specific high-impact insights were automatically identified from the aggregated data in this section."
1804
  })
1805
 
1806
+ # Menampilkan rekomendasi dalam format kotak yang sesuai dengan permintaan
1807
+ for rec in ai_recs:
1808
+ # Gunakan div dengan class khusus untuk membuat kotak rekomendasi di kolom kanan
1809
+ # Gaya diambil dari .insight-box untuk konsistensi dan menghindari warna ungu
1810
+ st.markdown(f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1811
  <div style="
1812
+ background: #f8f9fa;
1813
+ border: 1px solid #dee2e6;
1814
  border-radius: 8px;
1815
+ padding: 15px;
1816
+ margin: 10px 0;
1817
  color: #2c3e50;
1818
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1819
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1820
+ display: flex;
1821
+ flex-direction: column;
1822
+ justify-content: space-between;
1823
  ">
1824
  <div style="
1825
  font-weight: bold;
1826
+ color: #2c3e50;
1827
+ margin-bottom: 8px;
1828
+ font-size: 14px;
1829
+ background: #e9ecef;
 
 
 
 
 
 
 
 
 
 
1830
  padding: 8px;
1831
  border-radius: 5px;
1832
+ border-left: 4px solid #495057;
1833
+ ">AI Recommendation</div>
1834
+ <div style="padding-top: 8px; font-size: 14px; margin-bottom: 10px;">
1835
+ <strong>Action:</strong> {rec['recommendation']}
1836
+ </div>
1837
+ <div style="font-size: 12px; padding: 8px; background: #e9ecef; border-radius: 5px; margin-top: 5px;">
1838
  <strong>Data Point:</strong> {rec['data_point']}
1839
  </div>
1840
+ <div style="font-size: 12px; padding: 8px; background: #f1f1f1; border-radius: 5px; margin-top: 5px;">
 
 
 
 
 
 
 
1841
  <strong>AI Reasoning:</strong> {rec['reason']}
1842
  </div>
1843
  </div>
1844
+ """, unsafe_allow_html=True)
1845
+ else:
1846
+ st.info("No specific data points available for AI recommendations. Ensure relevant columns (hour, shift, operator, duration, speed) are present and populated.")
1847
 
1848
  # ================= FOOTER ===========================
1849
  st.markdown("---")
1850
+ st.markdown('<div class="footer">FatigueAnalyzer - Transforming Mining Safety with Intelligent Analytics | Contact: info@bukittechnology.com</div>', unsafe_allow_html=True)