Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1156,7 +1156,7 @@ import streamlit as st
|
|
| 1156 |
import pandas as pd
|
| 1157 |
import numpy as np
|
| 1158 |
import plotly.graph_objects as go
|
| 1159 |
-
st.subheader("OBJECTIVE 5: See your team’s
|
| 1160 |
|
| 1161 |
# Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
|
| 1162 |
st.markdown("""
|
|
@@ -1384,7 +1384,7 @@ else:
|
|
| 1384 |
# ===============================================================
|
| 1385 |
# LEGEND — UPDATED: Stable → One Time Event, Gray → Yellow
|
| 1386 |
# ===============================================================
|
| 1387 |
-
st.subheader("
|
| 1388 |
|
| 1389 |
st.markdown("""
|
| 1390 |
<div class="legend-container">
|
|
@@ -1519,15 +1519,23 @@ else:
|
|
| 1519 |
)
|
| 1520 |
return fig
|
| 1521 |
|
|
|
|
|
|
|
|
|
|
| 1522 |
# ===============================================================
|
| 1523 |
# CHARTS
|
| 1524 |
# ===============================================================
|
| 1525 |
col1, col2 = st.columns(2)
|
| 1526 |
with col1:
|
| 1527 |
-
|
|
|
|
|
|
|
|
|
|
| 1528 |
with col2:
|
| 1529 |
-
|
| 1530 |
-
|
|
|
|
|
|
|
| 1531 |
# ===============================================================
|
| 1532 |
# AI INSIGHTS — DIPERBAIKI: Risk Summary jadi 1 box + 3 list
|
| 1533 |
# ===============================================================
|
|
@@ -1752,7 +1760,6 @@ with col_insights:
|
|
| 1752 |
|
| 1753 |
# ===================== 2. High-Speed Fatigue Analysis =====================
|
| 1754 |
if col_speed and col_speed in df.columns:
|
| 1755 |
-
|
| 1756 |
high_speed_threshold = 20
|
| 1757 |
high_speed_fatigue = df[df[col_speed] >= high_speed_threshold]
|
| 1758 |
high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
|
|
@@ -1775,10 +1782,221 @@ with col_insights:
|
|
| 1775 |
st.info(
|
| 1776 |
f"{high_speed_pct:.1f}% of alerts occur at high speeds. This is within acceptable range."
|
| 1777 |
)
|
| 1778 |
-
|
| 1779 |
else:
|
| 1780 |
st.info("Speed data not available for High-Speed Fatigue Analysis.")
|
| 1781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1782 |
# ===================== 3. Shift Pattern Analysis =====================
|
| 1783 |
if col_shift and col_shift in df.columns:
|
| 1784 |
|
|
|
|
| 1156 |
import pandas as pd
|
| 1157 |
import numpy as np
|
| 1158 |
import plotly.graph_objects as go
|
| 1159 |
+
st.subheader("OBJECTIVE 5: See your team’s Fatigue Hazard Profile!")
|
| 1160 |
|
| 1161 |
# Custom CSS — tetap seperti sebelumnya (sudah sesuai preferensi)
|
| 1162 |
st.markdown("""
|
|
|
|
| 1384 |
# ===============================================================
|
| 1385 |
# LEGEND — UPDATED: Stable → One Time Event, Gray → Yellow
|
| 1386 |
# ===============================================================
|
| 1387 |
+
st.subheader("Legend of Frequency Trends")
|
| 1388 |
|
| 1389 |
st.markdown("""
|
| 1390 |
<div class="legend-container">
|
|
|
|
| 1519 |
)
|
| 1520 |
return fig
|
| 1521 |
|
| 1522 |
+
# ===============================================================
|
| 1523 |
+
# CHARTS
|
| 1524 |
+
# ===============================================================
|
| 1525 |
# ===============================================================
|
| 1526 |
# CHARTS
|
| 1527 |
# ===============================================================
|
| 1528 |
col1, col2 = st.columns(2)
|
| 1529 |
with col1:
|
| 1530 |
+
# Center-align chart
|
| 1531 |
+
st.markdown("<div style='display: flex; justify-content: center;'>", unsafe_allow_html=True)
|
| 1532 |
+
st.plotly_chart(plot_chart(top_ob, "OB HAULER Operator Hazard Profile"), use_container_width=False, config={'displayModeBar': False})
|
| 1533 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 1534 |
with col2:
|
| 1535 |
+
# Center-align chart
|
| 1536 |
+
st.markdown("<div style='display: flex; justify-content: center;'>", unsafe_allow_html=True)
|
| 1537 |
+
st.plotly_chart(plot_chart(top_coal, "COAL HAULING Operator Hazard Profile"), use_container_width=False, config={'displayModeBar': False})
|
| 1538 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 1539 |
# ===============================================================
|
| 1540 |
# AI INSIGHTS — DIPERBAIKI: Risk Summary jadi 1 box + 3 list
|
| 1541 |
# ===============================================================
|
|
|
|
| 1760 |
|
| 1761 |
# ===================== 2. High-Speed Fatigue Analysis =====================
|
| 1762 |
if col_speed and col_speed in df.columns:
|
|
|
|
| 1763 |
high_speed_threshold = 20
|
| 1764 |
high_speed_fatigue = df[df[col_speed] >= high_speed_threshold]
|
| 1765 |
high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
|
|
|
|
| 1782 |
st.info(
|
| 1783 |
f"{high_speed_pct:.1f}% of alerts occur at high speeds. This is within acceptable range."
|
| 1784 |
)
|
|
|
|
| 1785 |
else:
|
| 1786 |
st.info("Speed data not available for High-Speed Fatigue Analysis.")
|
| 1787 |
|
| 1788 |
+
# ===================== 3. Weekend vs Weekday Risk =====================
|
| 1789 |
+
df['is_weekend'] = df['start'].dt.weekday >= 5
|
| 1790 |
+
weekend_alerts = len(df[df['is_weekend']])
|
| 1791 |
+
weekday_alerts = len(df[~df['is_weekend']])
|
| 1792 |
+
weekend_pct = (weekend_alerts / len(df)) * 100 if len(df) > 0 else 0
|
| 1793 |
+
|
| 1794 |
+
st.markdown(f"**Weekend vs Weekday Risk Distribution**")
|
| 1795 |
+
st.markdown(
|
| 1796 |
+
f"""
|
| 1797 |
+
<div style="display: flex; gap: 10px; justify-content: space-between;">
|
| 1798 |
+
<div style="text-align: center; flex: 1;">
|
| 1799 |
+
<div style="font-size: 18px; font-weight: bold;">{weekend_alerts}</div>
|
| 1800 |
+
<div style="color: #555;">Weekend</div>
|
| 1801 |
+
<div style="color: red; font-size: 12px;">{weekend_pct:.1f}%</div>
|
| 1802 |
+
</div>
|
| 1803 |
+
<div style="text-align: center; flex: 1;">
|
| 1804 |
+
<div style="font-size: 18px; font-weight: bold;">{weekday_alerts}</div>
|
| 1805 |
+
<div style="color: #555;">Weekday</div>
|
| 1806 |
+
<div style="color: green; font-size: 12px;">{100-weekend_pct:.1f}%</div>
|
| 1807 |
+
</div>
|
| 1808 |
+
</div>
|
| 1809 |
+
""",
|
| 1810 |
+
unsafe_allow_html=True
|
| 1811 |
+
)
|
| 1812 |
+
if weekend_pct > 60:
|
| 1813 |
+
st.warning(f"High weekend fatigue risk ({weekend_pct:.1f}%). Consider weekend rest protocols.")
|
| 1814 |
+
else:
|
| 1815 |
+
st.info(f"Weekend fatigue is {weekend_pct:.1f}% — balanced with weekday activity.")
|
| 1816 |
+
|
| 1817 |
+
# ===================== 4. Shift-Based Risk Analysis =====================
|
| 1818 |
+
if col_shift and col_shift in df.columns:
|
| 1819 |
+
shift_counts = df[col_shift].value_counts()
|
| 1820 |
+
st.markdown(f"**Shift-Based Risk Distribution**")
|
| 1821 |
+
for shift, count in shift_counts.items():
|
| 1822 |
+
shift_pct = (count / len(df)) * 100
|
| 1823 |
+
st.markdown(f"- **Shift {shift}**: {count} alerts ({shift_pct:.1f}%)")
|
| 1824 |
+
max_shift = shift_counts.idxmax() if not shift_counts.empty else "N/A"
|
| 1825 |
+
if max_shift != "N/A":
|
| 1826 |
+
st.warning(f"Shift {max_shift} has the highest fatigue alert count. Investigate scheduling or rest breaks.")
|
| 1827 |
+
else:
|
| 1828 |
+
st.info("Shift data not available for analysis.")
|
| 1829 |
+
|
| 1830 |
+
# ===================== 5. Fleet Type Risk Distribution =====================
|
| 1831 |
+
if col_fleet_type and col_fleet_type in df.columns:
|
| 1832 |
+
fleet_counts = df[col_fleet_type].value_counts()
|
| 1833 |
+
st.markdown(f"**Fleet Type Risk Distribution**")
|
| 1834 |
+
for fleet, count in fleet_counts.items():
|
| 1835 |
+
fleet_pct = (count / len(df)) * 100
|
| 1836 |
+
st.markdown(f"- **{fleet}**: {count} alerts ({fleet_pct:.1f}%)")
|
| 1837 |
+
max_fleet = fleet_counts.idxmax() if not fleet_counts.empty else "N/A"
|
| 1838 |
+
if max_fleet != "N/A":
|
| 1839 |
+
st.warning(f"Fleet type '{max_fleet}' has the highest fatigue alert count. Investigate vehicle-specific factors.")
|
| 1840 |
+
else:
|
| 1841 |
+
st.info("Fleet type data not available for analysis.")
|
| 1842 |
+
|
| 1843 |
+
# ===================== 6. Top 5 Operators by Alert Count =====================
|
| 1844 |
+
if col_operator and col_operator in df.columns:
|
| 1845 |
+
top5_operators = df[col_operator].value_counts().head(5)
|
| 1846 |
+
st.markdown(f"**Top 5 Operators by Fatigue Alerts**")
|
| 1847 |
+
for op, count in top5_operators.items():
|
| 1848 |
+
op_pct = (count / len(df)) * 100
|
| 1849 |
+
st.markdown(f"- **{op}**: {count} alerts ({op_pct:.1f}%)")
|
| 1850 |
+
if not top5_operators.empty:
|
| 1851 |
+
top_op = top5_operators.index[0]
|
| 1852 |
+
st.warning(f"Operator {top_op} has the highest alert count. Prioritize fatigue intervention.")
|
| 1853 |
+
else:
|
| 1854 |
+
st.info("Operator data not available for analysis.")
|
| 1855 |
+
|
| 1856 |
+
# ===================== 7. Fatigue Alert Trend Over Time (Weekly) =====================
|
| 1857 |
+
df_weekly = df.set_index('start').resample('W').size().reset_index(name='count')
|
| 1858 |
+
if len(df_weekly) > 0:
|
| 1859 |
+
trend_slope = np.polyfit(range(len(df_weekly)), df_weekly['count'], 1)[0]
|
| 1860 |
+
st.markdown(f"**Fatigue Alert Trend (Weekly)**")
|
| 1861 |
+
if trend_slope > 0.5:
|
| 1862 |
+
st.warning(f"Trend is increasing (slope: {trend_slope:.2f}). Fatigue risk is rising.")
|
| 1863 |
+
elif trend_slope < -0.5:
|
| 1864 |
+
st.info(f"Trend is decreasing (slope: {trend_slope:.2f}). Fatigue risk is improving.")
|
| 1865 |
+
else:
|
| 1866 |
+
st.info(f"Trend is stable (slope: {trend_slope:.2f}).")
|
| 1867 |
+
else:
|
| 1868 |
+
st.info("Insufficient data for weekly trend analysis.")
|
| 1869 |
+
|
| 1870 |
+
# ===================== 8. Geographic Risk Hotspots =====================
|
| 1871 |
+
if 'location' in df.columns or ('lat' in df.columns and 'lon' in df.columns):
|
| 1872 |
+
st.markdown(f"**Geographic Risk Hotspots**")
|
| 1873 |
+
if 'location' in df.columns:
|
| 1874 |
+
loc_counts = df['location'].value_counts().head(3)
|
| 1875 |
+
for loc, count in loc_counts.items():
|
| 1876 |
+
loc_pct = (count / len(df)) * 100
|
| 1877 |
+
st.markdown(f"- **{loc}**: {count} alerts ({loc_pct:.1f}%)")
|
| 1878 |
+
else:
|
| 1879 |
+
st.info("Geographic data not available for hotspot analysis.")
|
| 1880 |
+
else:
|
| 1881 |
+
st.info("Geographic data not available for hotspot analysis.")
|
| 1882 |
+
|
| 1883 |
+
# ===================== 9. Correlation: Speed vs Hour =====================
|
| 1884 |
+
if col_speed and col_speed in df.columns and 'hour' in df.columns:
|
| 1885 |
+
corr = df[col_speed].corr(df['hour'])
|
| 1886 |
+
st.markdown(f"**Correlation: Speed vs Hour**")
|
| 1887 |
+
if abs(corr) > 0.3:
|
| 1888 |
+
st.warning(f"Moderate correlation detected (r = {corr:.2f}). Speed patterns may vary by hour.")
|
| 1889 |
+
else:
|
| 1890 |
+
st.info(f"No strong correlation (r = {corr:.2f}). Speed is consistent across hours.")
|
| 1891 |
+
else:
|
| 1892 |
+
st.info("Insufficient data for speed vs hour correlation.")
|
| 1893 |
+
|
| 1894 |
+
# =====================================================================
|
| 1895 |
+
# 🔹 KOLOM KANAN — AI RECOMMENDATIONS
|
| 1896 |
+
# =====================================================================
|
| 1897 |
+
with col_recs:
|
| 1898 |
+
st.subheader("AI Recommendations")
|
| 1899 |
+
|
| 1900 |
+
# Generate 9 recommendations based on the 9 insights above
|
| 1901 |
+
recs = []
|
| 1902 |
+
|
| 1903 |
+
# 1. Critical Hour
|
| 1904 |
+
if critical_pct > 10:
|
| 1905 |
+
recs.append("Implement mandatory 15-min break protocol during 3-6 AM shifts.")
|
| 1906 |
+
else:
|
| 1907 |
+
recs.append("Maintain current scheduling for early shifts — risk is low.")
|
| 1908 |
+
|
| 1909 |
+
# 2. High-Speed Fatigue
|
| 1910 |
+
if 'col_speed' in locals() and col_speed in df.columns:
|
| 1911 |
+
if high_speed_pct > 20:
|
| 1912 |
+
recs.append("Install speed monitoring systems to alert drivers during high-speed fatigue alerts.")
|
| 1913 |
+
else:
|
| 1914 |
+
recs.append("Continue monitoring speed-related fatigue — current levels are acceptable.")
|
| 1915 |
+
else:
|
| 1916 |
+
recs.append("Enable speed data collection to assess high-speed fatigue risk.")
|
| 1917 |
+
|
| 1918 |
+
# 3. Weekend Risk
|
| 1919 |
+
if weekend_pct > 60:
|
| 1920 |
+
recs.append("Schedule mandatory rest periods after weekend shifts to reduce fatigue accumulation.")
|
| 1921 |
+
else:
|
| 1922 |
+
recs.append("Maintain current weekend scheduling protocols — risk is balanced.")
|
| 1923 |
+
|
| 1924 |
+
# 4. Shift Risk
|
| 1925 |
+
if col_shift and col_shift in df.columns:
|
| 1926 |
+
max_shift = df[col_shift].value_counts().idxmax() if not df[col_shift].value_counts().empty else "N/A"
|
| 1927 |
+
if max_shift != "N/A":
|
| 1928 |
+
recs.append(f"Review shift {max_shift} scheduling and rest-break frequency for operators.")
|
| 1929 |
+
else:
|
| 1930 |
+
recs.append("No dominant shift risk — continue monitoring all shifts equally.")
|
| 1931 |
+
else:
|
| 1932 |
+
recs.append("Enable shift data collection to assess shift-based fatigue risk.")
|
| 1933 |
+
|
| 1934 |
+
# 5. Fleet Risk
|
| 1935 |
+
if col_fleet_type and col_fleet_type in df.columns:
|
| 1936 |
+
max_fleet = df[col_fleet_type].value_counts().idxmax() if not df[col_fleet_type].value_counts().empty else "N/A"
|
| 1937 |
+
if max_fleet != "N/A":
|
| 1938 |
+
recs.append(f"Investigate fatigue factors specific to fleet type '{max_fleet}' (e.g., ergonomics, route, etc.).")
|
| 1939 |
+
else:
|
| 1940 |
+
recs.append("No dominant fleet risk — continue monitoring all fleet types equally.")
|
| 1941 |
+
else:
|
| 1942 |
+
recs.append("Enable fleet type data collection to assess vehicle-specific fatigue risk.")
|
| 1943 |
+
|
| 1944 |
+
# 6. Top Operator Risk
|
| 1945 |
+
if col_operator and col_operator in df.columns:
|
| 1946 |
+
if not df[col_operator].value_counts().empty:
|
| 1947 |
+
top_op = df[col_operator].value_counts().index[0]
|
| 1948 |
+
recs.append(f"Scheduled one-on-one fatigue risk assessment for operator {top_op}.")
|
| 1949 |
+
else:
|
| 1950 |
+
recs.append("No top operator identified — continue general monitoring.")
|
| 1951 |
+
else:
|
| 1952 |
+
recs.append("Enable operator data collection to identify high-risk drivers.")
|
| 1953 |
+
|
| 1954 |
+
# 7. Trend Risk
|
| 1955 |
+
if len(df_weekly) > 0:
|
| 1956 |
+
trend_slope = np.polyfit(range(len(df_weekly)), df_weekly['count'], 1)[0]
|
| 1957 |
+
if trend_slope > 0.5:
|
| 1958 |
+
recs.append("Initiate immediate fatigue risk review across all shifts and operators.")
|
| 1959 |
+
elif trend_slope < -0.5:
|
| 1960 |
+
recs.append("Recognize current safety protocols — continue to monitor trend reversal.")
|
| 1961 |
+
else:
|
| 1962 |
+
recs.append("Maintain current protocols — fatigue trend is stable.")
|
| 1963 |
+
else:
|
| 1964 |
+
recs.append("Insufficient data to assess fatigue trend.")
|
| 1965 |
+
|
| 1966 |
+
# 8. Geographic Risk
|
| 1967 |
+
if 'location' in df.columns or ('lat' in df.columns and 'lon' in df.columns):
|
| 1968 |
+
if 'location' in df.columns:
|
| 1969 |
+
top_loc = df['location'].value_counts().index[0] if not df['location'].value_counts().empty else "N/A"
|
| 1970 |
+
if top_loc != "N/A":
|
| 1971 |
+
recs.append(f"Review route planning and rest stops for high-risk location: {top_loc}.")
|
| 1972 |
+
else:
|
| 1973 |
+
recs.append("No geographic hotspots identified.")
|
| 1974 |
+
else:
|
| 1975 |
+
recs.append("Enable geographic data collection for hotspot analysis.")
|
| 1976 |
+
else:
|
| 1977 |
+
recs.append("Enable geographic data collection for hotspot analysis.")
|
| 1978 |
+
|
| 1979 |
+
# 9. Correlation Risk
|
| 1980 |
+
if col_speed and col_speed in df.columns and 'hour' in df.columns:
|
| 1981 |
+
corr = df[col_speed].corr(df['hour'])
|
| 1982 |
+
if abs(corr) > 0.3:
|
| 1983 |
+
recs.append("Implement time-of-day speed limits to reduce hour-speed correlation risk.")
|
| 1984 |
+
else:
|
| 1985 |
+
recs.append("Speed patterns are consistent — no immediate action required.")
|
| 1986 |
+
else:
|
| 1987 |
+
recs.append("Enable speed and hour data for correlation analysis.")
|
| 1988 |
+
|
| 1989 |
+
# Display all 9 recommendations in one box with 9 list items
|
| 1990 |
+
st.markdown(f"""
|
| 1991 |
+
<div class="ai-insight-box">
|
| 1992 |
+
<div class="ai-insight-title">AI Action Plan</div>
|
| 1993 |
+
<ul style="padding-left: 20px; margin: 8px 0; line-height: 1.5;">
|
| 1994 |
+
""", unsafe_allow_html=True)
|
| 1995 |
+
|
| 1996 |
+
for i, rec in enumerate(recs, 1):
|
| 1997 |
+
st.markdown(f"<li>{rec}</li>", unsafe_allow_html=True)
|
| 1998 |
+
|
| 1999 |
+
st.markdown("</ul></div>", unsafe_allow_html=True)
|
| 2000 |
# ===================== 3. Shift Pattern Analysis =====================
|
| 2001 |
if col_shift and col_shift in df.columns:
|
| 2002 |
|