Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -904,7 +904,7 @@ with col_3b:
|
|
| 904 |
# Ambil subset sesuai pilihan
|
| 905 |
if sort_opt == "Top 10":
|
| 906 |
# 10 tercepat: ascending (kecil → besar), tetap diurut ascending → tercepat di atas
|
| 907 |
-
subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=
|
| 908 |
else: # "Bottom 10 Slowest"
|
| 909 |
# 10 terlambat: descending (besar → kecil), agar terlambat di atas
|
| 910 |
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
|
@@ -971,7 +971,7 @@ with col_3d:
|
|
| 971 |
full_sorted = avg_leadtime_per_indiv.sort_values('avg_monthly_leadtime', ascending=True)
|
| 972 |
|
| 973 |
if sort_opt == "Top 10":
|
| 974 |
-
subset = full_sorted.head(10).subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=
|
| 975 |
else: # "Bottom 10 Slowest"
|
| 976 |
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 977 |
|
|
@@ -1549,7 +1549,7 @@ def predict_creators(df):
|
|
| 1549 |
results.append({
|
| 1550 |
'Creator': creator,
|
| 1551 |
'Reports/Month': round(avg_rate, 2),
|
| 1552 |
-
'
|
| 1553 |
'Trend Slope': round(slope, 3),
|
| 1554 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1555 |
'Reason': reason
|
|
@@ -1597,7 +1597,7 @@ def predict_locations(df):
|
|
| 1597 |
results.append({
|
| 1598 |
'Location': lokasi,
|
| 1599 |
'Reports/Month': round(avg_rate, 2),
|
| 1600 |
-
'
|
| 1601 |
'Trend Slope': round(slope, 3),
|
| 1602 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1603 |
'Reason': reason
|
|
@@ -1645,7 +1645,7 @@ def predict_divisions(df):
|
|
| 1645 |
results.append({
|
| 1646 |
'Division': div,
|
| 1647 |
'Reports/Month': round(avg_rate, 2),
|
| 1648 |
-
'
|
| 1649 |
'Trend Slope': round(slope, 3),
|
| 1650 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1651 |
'Reason': reason
|
|
@@ -1691,7 +1691,7 @@ def predict_categories(df):
|
|
| 1691 |
results.append({
|
| 1692 |
'Category': cat,
|
| 1693 |
'Avg/Month': round(avg_per_month, 2),
|
| 1694 |
-
'
|
| 1695 |
'Trend Slope': round(slope, 3),
|
| 1696 |
'Trend': ascii_sparkline_pln(ts_data.values.tolist())
|
| 1697 |
})
|
|
@@ -1724,7 +1724,7 @@ df_category = predict_categories(df_filtered)
|
|
| 1724 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1725 |
st.markdown("<div class='predictive-header'>1. Which Reporters Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1726 |
if not df_creator.empty:
|
| 1727 |
-
cols = ['Creator', 'Reports/Month', '
|
| 1728 |
|
| 1729 |
# 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1730 |
df_display = df_creator[cols].rename(columns={
|
|
@@ -1760,7 +1760,7 @@ st.markdown("</div>", unsafe_allow_html=True)
|
|
| 1760 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1761 |
st.markdown("<div class='predictive-header'>2. Which Locations Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1762 |
if not df_location.empty:
|
| 1763 |
-
cols = ['Location', 'Reports/Month', '
|
| 1764 |
|
| 1765 |
# # 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1766 |
df_display = df_location[cols].rename(columns={
|
|
@@ -1796,7 +1796,7 @@ st.markdown("</div>", unsafe_allow_html=True)
|
|
| 1796 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1797 |
st.markdown("<div class='predictive-header'>3. Which Divisions Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1798 |
if not df_division.empty:
|
| 1799 |
-
cols = ['Division', 'Reports/Month', '
|
| 1800 |
|
| 1801 |
# # 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1802 |
df_display = df_division[cols].rename(columns={
|
|
@@ -1984,9 +1984,11 @@ else:
|
|
| 1984 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1985 |
|
| 1986 |
|
|
|
|
| 1987 |
# ============================================================== #
|
| 1988 |
-
#
|
| 1989 |
# ============================================================== #
|
|
|
|
| 1990 |
def compute_avg_monthly_ratio_per_location(df: pd.DataFrame) -> pd.DataFrame:
|
| 1991 |
required = ['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']
|
| 1992 |
missing = [col for col in required if col not in df.columns]
|
|
@@ -2024,9 +2026,6 @@ def compute_avg_monthly_ratio_per_location(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 2024 |
|
| 2025 |
return loc_summary
|
| 2026 |
|
| 2027 |
-
# ============================================================== #
|
| 2028 |
-
# Helper 2: Interpretasi Aktivitas Pelaporan secara Adil
|
| 2029 |
-
# ============================================================== #
|
| 2030 |
def interpret_location_safely(df: pd.DataFrame, location_name: str) -> dict:
|
| 2031 |
loc_df = df[df['nama_lokasi_full'] == location_name].copy()
|
| 2032 |
if loc_df.empty:
|
|
@@ -2080,9 +2079,6 @@ def interpret_location_safely(df: pd.DataFrame, location_name: str) -> dict:
|
|
| 2080 |
"positive_rate": perc_positive
|
| 2081 |
}
|
| 2082 |
|
| 2083 |
-
# ============================================================== #
|
| 2084 |
-
# Helper 3: Deteksi Isu Tidak Aman dari Teks
|
| 2085 |
-
# ============================================================== #
|
| 2086 |
def detect_unsafe_terms(df: pd.DataFrame):
|
| 2087 |
text_cols = ['hasil_keyword_dan_kondisi', 'judul_dan_kondisi', 'kondisi', 'judul']
|
| 2088 |
text_col = None
|
|
@@ -2097,127 +2093,144 @@ def detect_unsafe_terms(df: pd.DataFrame):
|
|
| 2097 |
unsafe_terms = [
|
| 2098 |
'terbuka', 'tidak terkunci', 'tanpa izin', 'tanpa pelindung', 'tanpa alat',
|
| 2099 |
'korsleting', 'overload', 'grounding', 'exposed', 'unlocked', 'no ppe',
|
| 2100 |
-
'jatuh', 'slip',
|
| 2101 |
'tidak kompeten', 'untrained', 'prosedur dilanggar', 'bypass'
|
| 2102 |
]
|
| 2103 |
found = [term for term in unsafe_terms if term in all_text]
|
| 2104 |
return list(set(found))
|
| 2105 |
|
| 2106 |
-
|
| 2107 |
-
# Main: Generate Risk Mitigation Insights
|
| 2108 |
-
# ============================================================== #
|
| 2109 |
-
def compute_risk_mitigation_insights(df: pd.DataFrame) -> List[dict]:
|
| 2110 |
-
insights = []
|
| 2111 |
if df.empty:
|
| 2112 |
-
return
|
| 2113 |
|
| 2114 |
-
|
|
|
|
|
|
|
|
|
|
| 2115 |
if {'nama_lokasi_full', 'temuan_kategori', 'creator_nid', 'created_at'}.issubset(df.columns):
|
| 2116 |
top_locs = df['nama_lokasi_full'].value_counts().head(3).index.tolist()
|
| 2117 |
for loc in top_locs:
|
| 2118 |
interp = interpret_location_safely(df, loc)
|
| 2119 |
-
|
| 2120 |
|
| 2121 |
-
|
| 2122 |
-
if signal == "Slight Risk":
|
| 2123 |
-
recommendation = (
|
| 2124 |
-
"Recognize this location as a safety exemplar. Share their positive findings in internal safety communications. "
|
| 2125 |
-
"Facilitate cross-location learning sessions to replicate practices."
|
| 2126 |
-
)
|
| 2127 |
-
elif signal == "Moderate Risk":
|
| 2128 |
-
recommendation = (
|
| 2129 |
-
"Conduct a workshop on positive intervention techniques. Train teams to identify and report good practices. "
|
| 2130 |
-
"Set a target to increase the positive finding rate to above 60 percent within three months."
|
| 2131 |
-
)
|
| 2132 |
-
elif signal == "High Risk":
|
| 2133 |
-
recommendation = (
|
| 2134 |
-
"Assign two additional auditors to rotate into this location for one month. "
|
| 2135 |
-
"Administer an anonymous psychological safety survey to assess reporting barriers."
|
| 2136 |
-
)
|
| 2137 |
-
elif signal == "Very High Risk":
|
| 2138 |
-
recommendation = (
|
| 2139 |
-
"Escalate to area management. Implement daily safety huddles, scheduled supervisor walkarounds, "
|
| 2140 |
-
"and weekly tracking of unsafe finding closure rates."
|
| 2141 |
-
)
|
| 2142 |
-
else:
|
| 2143 |
-
recommendation = (
|
| 2144 |
-
"Validate physical inspection coverage. Ensure field presence aligns with digital reporting records."
|
| 2145 |
-
)
|
| 2146 |
-
|
| 2147 |
-
insights.append({"insight": insight, "recommendation": recommendation})
|
| 2148 |
-
|
| 2149 |
-
# Insight 2: Organizational Agentic Safety Maturity
|
| 2150 |
if 'temuan_kategori' in df.columns:
|
| 2151 |
total = len(df)
|
| 2152 |
n_positive = (df['temuan_kategori'] == 'Positive').sum()
|
| 2153 |
positive_rate = n_positive / total if total > 0 else 0
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
f"
|
| 2157 |
-
f"indicating proactive safety behaviors. The remaining {100 - positive_rate * 100:.1f} percent are reactive, "
|
| 2158 |
-
f"responding to existing hazards."
|
| 2159 |
)
|
| 2160 |
|
| 2161 |
-
|
| 2162 |
-
recommendation = (
|
| 2163 |
-
"Launch an Agentic Safety Program: incentivize near-miss reporting and safety suggestions, "
|
| 2164 |
-
"train designated Safety Coaches per division, and adopt percentage of Positive findings as a leading KPI, "
|
| 2165 |
-
"with a six-month target of 50 percent."
|
| 2166 |
-
)
|
| 2167 |
-
else:
|
| 2168 |
-
recommendation = (
|
| 2169 |
-
"Sustain current momentum. Formalize recognition for divisions with consistently high positive reporting rates."
|
| 2170 |
-
)
|
| 2171 |
-
|
| 2172 |
-
insights.append({"insight": insight, "recommendation": recommendation})
|
| 2173 |
-
|
| 2174 |
-
# Insight 3: Emerging Unsafe Issues from Text Analysis
|
| 2175 |
unsafe_terms = detect_unsafe_terms(df)
|
| 2176 |
if unsafe_terms:
|
| 2177 |
top_terms = ', '.join(sorted(unsafe_terms)[:5])
|
| 2178 |
-
|
| 2179 |
-
recommendation = (
|
| 2180 |
-
"Initiate a targeted two-week Risk Blitz focusing on these conditions. "
|
| 2181 |
-
"Update inspection checklists to include these items as critical control points. "
|
| 2182 |
-
"Require photo documentation for verification of corrective actions."
|
| 2183 |
-
)
|
| 2184 |
-
insights.append({"insight": insight, "recommendation": recommendation})
|
| 2185 |
|
| 2186 |
-
# Insight 4: Low-Activity Locations
|
| 2187 |
if 'nama_lokasi_full' in df.columns:
|
| 2188 |
loc_counts = df['nama_lokasi_full'].value_counts()
|
| 2189 |
low_activity_locs = loc_counts[loc_counts <= 2].index.tolist()
|
| 2190 |
for loc in low_activity_locs[:3]:
|
| 2191 |
interp = interpret_location_safely(df, loc)
|
| 2192 |
if 0 < interp['positive_rate'] < 0.5:
|
| 2193 |
-
|
| 2194 |
f"Location {loc} reports low volume ({loc_counts[loc]} findings) with a positive rate of "
|
| 2195 |
f"{interp['positive_rate']:.0%}, suggesting possible under-reporting or unobserved hazards."
|
| 2196 |
)
|
| 2197 |
-
recommendation = (
|
| 2198 |
-
"Conduct an unannounced observational audit by an independent team to assess true field conditions."
|
| 2199 |
-
)
|
| 2200 |
-
insights.append({"insight": insight, "recommendation": recommendation})
|
| 2201 |
|
| 2202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2203 |
|
| 2204 |
# ============================================================== #
|
| 2205 |
-
#
|
| 2206 |
# ============================================================== #
|
|
|
|
| 2207 |
try:
|
| 2208 |
-
|
| 2209 |
except Exception as e:
|
| 2210 |
-
|
| 2211 |
-
|
|
|
|
| 2212 |
|
| 2213 |
-
|
| 2214 |
-
|
| 2215 |
-
|
| 2216 |
-
|
| 2217 |
-
|
| 2218 |
-
|
| 2219 |
-
|
| 2220 |
-
|
| 2221 |
-
|
| 2222 |
-
|
| 2223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
# Ambil subset sesuai pilihan
|
| 905 |
if sort_opt == "Top 10":
|
| 906 |
# 10 tercepat: ascending (kecil → besar), tetap diurut ascending → tercepat di atas
|
| 907 |
+
subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 908 |
else: # "Bottom 10 Slowest"
|
| 909 |
# 10 terlambat: descending (besar → kecil), agar terlambat di atas
|
| 910 |
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
|
|
|
| 971 |
full_sorted = avg_leadtime_per_indiv.sort_values('avg_monthly_leadtime', ascending=True)
|
| 972 |
|
| 973 |
if sort_opt == "Top 10":
|
| 974 |
+
subset = full_sorted.head(10).subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 975 |
else: # "Bottom 10 Slowest"
|
| 976 |
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 977 |
|
|
|
|
| 1549 |
results.append({
|
| 1550 |
'Creator': creator,
|
| 1551 |
'Reports/Month': round(avg_rate, 2),
|
| 1552 |
+
'Monthly Consistency (%)': round(coverage * 100, 1),
|
| 1553 |
'Trend Slope': round(slope, 3),
|
| 1554 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1555 |
'Reason': reason
|
|
|
|
| 1597 |
results.append({
|
| 1598 |
'Location': lokasi,
|
| 1599 |
'Reports/Month': round(avg_rate, 2),
|
| 1600 |
+
'Monthly Consistency (%)': round(coverage * 100, 1),
|
| 1601 |
'Trend Slope': round(slope, 3),
|
| 1602 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1603 |
'Reason': reason
|
|
|
|
| 1645 |
results.append({
|
| 1646 |
'Division': div,
|
| 1647 |
'Reports/Month': round(avg_rate, 2),
|
| 1648 |
+
'Monthly Consistency (%)': round(coverage * 100, 1),
|
| 1649 |
'Trend Slope': round(slope, 3),
|
| 1650 |
'Trend': ascii_sparkline_pln(ts.values.tolist()),
|
| 1651 |
'Reason': reason
|
|
|
|
| 1691 |
results.append({
|
| 1692 |
'Category': cat,
|
| 1693 |
'Avg/Month': round(avg_per_month, 2),
|
| 1694 |
+
'Monthly Consistency (%)': round(coverage * 100, 1),
|
| 1695 |
'Trend Slope': round(slope, 3),
|
| 1696 |
'Trend': ascii_sparkline_pln(ts_data.values.tolist())
|
| 1697 |
})
|
|
|
|
| 1724 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1725 |
st.markdown("<div class='predictive-header'>1. Which Reporters Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1726 |
if not df_creator.empty:
|
| 1727 |
+
cols = ['Creator', 'Reports/Month', 'Monthly Consistency (%)', 'Trend Slope', 'Trend']
|
| 1728 |
|
| 1729 |
# 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1730 |
df_display = df_creator[cols].rename(columns={
|
|
|
|
| 1760 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1761 |
st.markdown("<div class='predictive-header'>2. Which Locations Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1762 |
if not df_location.empty:
|
| 1763 |
+
cols = ['Location', 'Reports/Month', 'Monthly Consistency (%)', 'Trend Slope', 'Trend']
|
| 1764 |
|
| 1765 |
# # 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1766 |
df_display = df_location[cols].rename(columns={
|
|
|
|
| 1796 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1797 |
st.markdown("<div class='predictive-header'>3. Which Divisions Are Predicted to Have Less Future Inspections? (Top 10 Most Declining)</div>", unsafe_allow_html=True)
|
| 1798 |
if not df_division.empty:
|
| 1799 |
+
cols = ['Division', 'Reports/Month', 'Monthly Consistency (%)', 'Trend Slope', 'Trend']
|
| 1800 |
|
| 1801 |
# # 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1802 |
df_display = df_division[cols].rename(columns={
|
|
|
|
| 1984 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1985 |
|
| 1986 |
|
| 1987 |
+
|
| 1988 |
# ============================================================== #
|
| 1989 |
+
# Fungsi Insight & Rekomendasi (sama seperti sebelumnya, tanpa perubahan logika)
|
| 1990 |
# ============================================================== #
|
| 1991 |
+
|
| 1992 |
def compute_avg_monthly_ratio_per_location(df: pd.DataFrame) -> pd.DataFrame:
|
| 1993 |
required = ['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']
|
| 1994 |
missing = [col for col in required if col not in df.columns]
|
|
|
|
| 2026 |
|
| 2027 |
return loc_summary
|
| 2028 |
|
|
|
|
|
|
|
|
|
|
| 2029 |
def interpret_location_safely(df: pd.DataFrame, location_name: str) -> dict:
|
| 2030 |
loc_df = df[df['nama_lokasi_full'] == location_name].copy()
|
| 2031 |
if loc_df.empty:
|
|
|
|
| 2079 |
"positive_rate": perc_positive
|
| 2080 |
}
|
| 2081 |
|
|
|
|
|
|
|
|
|
|
| 2082 |
def detect_unsafe_terms(df: pd.DataFrame):
|
| 2083 |
text_cols = ['hasil_keyword_dan_kondisi', 'judul_dan_kondisi', 'kondisi', 'judul']
|
| 2084 |
text_col = None
|
|
|
|
| 2093 |
unsafe_terms = [
|
| 2094 |
'terbuka', 'tidak terkunci', 'tanpa izin', 'tanpa pelindung', 'tanpa alat',
|
| 2095 |
'korsleting', 'overload', 'grounding', 'exposed', 'unlocked', 'no ppe',
|
| 2096 |
+
'jatuh', 'slip', 'trip', 'kebakaran', 'fire', 'fall', 'unauthorized',
|
| 2097 |
'tidak kompeten', 'untrained', 'prosedur dilanggar', 'bypass'
|
| 2098 |
]
|
| 2099 |
found = [term for term in unsafe_terms if term in all_text]
|
| 2100 |
return list(set(found))
|
| 2101 |
|
| 2102 |
+
def generate_insight_and_recommendation(df: pd.DataFrame):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2103 |
if df.empty:
|
| 2104 |
+
return "Insufficient data for insight generation.", "Ensure dataset is populated and filtered appropriately."
|
| 2105 |
|
| 2106 |
+
insights_parts = []
|
| 2107 |
+
recommendations_parts = []
|
| 2108 |
+
|
| 2109 |
+
# --- Insight 1: Top Active Locations (Interpreted) ---
|
| 2110 |
if {'nama_lokasi_full', 'temuan_kategori', 'creator_nid', 'created_at'}.issubset(df.columns):
|
| 2111 |
top_locs = df['nama_lokasi_full'].value_counts().head(3).index.tolist()
|
| 2112 |
for loc in top_locs:
|
| 2113 |
interp = interpret_location_safely(df, loc)
|
| 2114 |
+
insights_parts.append(f"Location {loc}: {interp['interpretation']}")
|
| 2115 |
|
| 2116 |
+
# --- Insight 2: Organizational Safety Maturity ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2117 |
if 'temuan_kategori' in df.columns:
|
| 2118 |
total = len(df)
|
| 2119 |
n_positive = (df['temuan_kategori'] == 'Positive').sum()
|
| 2120 |
positive_rate = n_positive / total if total > 0 else 0
|
| 2121 |
+
insights_parts.append(
|
| 2122 |
+
f"Organization-wide, {positive_rate:.1%} of findings are Positive (proactive), "
|
| 2123 |
+
f"while {100 - positive_rate * 100:.1f}% are reactive responses to existing hazards."
|
|
|
|
|
|
|
| 2124 |
)
|
| 2125 |
|
| 2126 |
+
# --- Insight 3: Emerging Unsafe Conditions ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2127 |
unsafe_terms = detect_unsafe_terms(df)
|
| 2128 |
if unsafe_terms:
|
| 2129 |
top_terms = ', '.join(sorted(unsafe_terms)[:5])
|
| 2130 |
+
insights_parts.append(f"Text analysis identifies recurring unsafe conditions related to: {top_terms}.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2131 |
|
| 2132 |
+
# --- Insight 4: Low-Activity Locations ---
|
| 2133 |
if 'nama_lokasi_full' in df.columns:
|
| 2134 |
loc_counts = df['nama_lokasi_full'].value_counts()
|
| 2135 |
low_activity_locs = loc_counts[loc_counts <= 2].index.tolist()
|
| 2136 |
for loc in low_activity_locs[:3]:
|
| 2137 |
interp = interpret_location_safely(df, loc)
|
| 2138 |
if 0 < interp['positive_rate'] < 0.5:
|
| 2139 |
+
insights_parts.append(
|
| 2140 |
f"Location {loc} reports low volume ({loc_counts[loc]} findings) with a positive rate of "
|
| 2141 |
f"{interp['positive_rate']:.0%}, suggesting possible under-reporting or unobserved hazards."
|
| 2142 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2143 |
|
| 2144 |
+
# --- Build Recommendation + Risk Mitigation Strategy ---
|
| 2145 |
+
rec_parts = []
|
| 2146 |
+
mitigation_parts = []
|
| 2147 |
+
|
| 2148 |
+
# Recommendation: Culture & Capability
|
| 2149 |
+
rec_parts.append(
|
| 2150 |
+
"Strengthen agentic safety behaviors by launching an Agentic Safety Program, including incentives for near-miss reporting, "
|
| 2151 |
+
"training of Safety Coaches per division, and adoption of the percentage of Positive findings as a leading performance indicator."
|
| 2152 |
+
)
|
| 2153 |
+
mitigation_parts.append(
|
| 2154 |
+
"Shift from compliance-driven audits to capability-building engagements. Measure success by reduction in repeat unsafe findings and increase in proactive interventions."
|
| 2155 |
+
)
|
| 2156 |
+
|
| 2157 |
+
# Recommendation: Data-Driven Intervention
|
| 2158 |
+
rec_parts.append(
|
| 2159 |
+
"Conduct targeted Risk Blitz campaigns for high-frequency unsafe conditions identified through text analysis, "
|
| 2160 |
+
"supported by updated checklists and photo-based verification of corrective actions."
|
| 2161 |
+
)
|
| 2162 |
+
mitigation_parts.append(
|
| 2163 |
+
"Integrate text analytics into monthly safety reviews to detect emerging risks earlier. Automate alerts when unsafe keywords exceed baseline thresholds."
|
| 2164 |
+
)
|
| 2165 |
+
|
| 2166 |
+
# Recommendation: Coverage & Equity
|
| 2167 |
+
rec_parts.append(
|
| 2168 |
+
"Improve inspection coverage equity through mandatory auditor rotation, geotagged field validation, and deployment of micro-checklists for frontline personnel."
|
| 2169 |
+
)
|
| 2170 |
+
mitigation_parts.append(
|
| 2171 |
+
"Monitor the Gini coefficient of reporter distribution across locations monthly. Set an organizational target of below 0.5 to ensure balanced surveillance."
|
| 2172 |
+
)
|
| 2173 |
+
|
| 2174 |
+
# Recommendation: Psychological Safety
|
| 2175 |
+
rec_parts.append(
|
| 2176 |
+
"Assess and improve psychological safety in high-risk locations using anonymous surveys and leadership listening sessions, "
|
| 2177 |
+
"particularly where reporting relies on very few individuals."
|
| 2178 |
+
)
|
| 2179 |
+
mitigation_parts.append(
|
| 2180 |
+
"Decouple reporting volume from individual performance evaluation. Reward quality, learning, and prevention impact instead."
|
| 2181 |
+
)
|
| 2182 |
+
|
| 2183 |
+
# Combine all
|
| 2184 |
+
insight_text = " ".join(insights_parts) if insights_parts else "No significant patterns detected in current data."
|
| 2185 |
+
recommendation_text = " ".join(rec_parts)
|
| 2186 |
+
mitigation_text = " ".join(mitigation_parts)
|
| 2187 |
+
|
| 2188 |
+
return insight_text, recommendation_text, mitigation_text
|
| 2189 |
|
| 2190 |
# ============================================================== #
|
| 2191 |
+
# Eksekusi & Tampilan — SATU CARD PER BAGIAN
|
| 2192 |
# ============================================================== #
|
| 2193 |
+
|
| 2194 |
try:
|
| 2195 |
+
insight, recommendation, risk_mitigation = generate_insight_and_recommendation(df_filtered)
|
| 2196 |
except Exception as e:
|
| 2197 |
+
insight = "Error during insight generation."
|
| 2198 |
+
recommendation = f"Review data pipeline: {str(e)}"
|
| 2199 |
+
risk_mitigation = "Ensure required columns are present and datetime formats are consistent."
|
| 2200 |
|
| 2201 |
+
# Card Insight
|
| 2202 |
+
st.markdown(
|
| 2203 |
+
f"""
|
| 2204 |
+
<div class="card" style="
|
| 2205 |
+
background-color: #f8f9fa;
|
| 2206 |
+
border-left: 4px solid #2196f3;
|
| 2207 |
+
padding: 16px;
|
| 2208 |
+
margin-bottom: 20px;
|
| 2209 |
+
border-radius: 4px;
|
| 2210 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2211 |
+
">
|
| 2212 |
+
<h4 style="margin-top: 0; color: #1976d2;">Insight Summary</h4>
|
| 2213 |
+
<p style="margin-bottom: 0;">{insight}</p>
|
| 2214 |
+
</div>
|
| 2215 |
+
""",
|
| 2216 |
+
unsafe_allow_html=True
|
| 2217 |
+
)
|
| 2218 |
+
|
| 2219 |
+
# Card Recommendation + Risk Mitigation
|
| 2220 |
+
st.markdown(
|
| 2221 |
+
f"""
|
| 2222 |
+
<div class="card" style="
|
| 2223 |
+
background-color: #f0f7ff;
|
| 2224 |
+
border-left: 4px solid #4caf50;
|
| 2225 |
+
padding: 16px;
|
| 2226 |
+
margin-bottom: 20px;
|
| 2227 |
+
border-radius: 4px;
|
| 2228 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2229 |
+
">
|
| 2230 |
+
<h4 style="margin-top: 0; color: #2e7d32;">Recommended Actions and Risk Mitigation Strategy</h4>
|
| 2231 |
+
<p><strong>Recommended Actions:</strong> {recommendation}</p>
|
| 2232 |
+
<p><strong>Risk Mitigation Strategy:</strong> {risk_mitigation}</p>
|
| 2233 |
+
</div>
|
| 2234 |
+
""",
|
| 2235 |
+
unsafe_allow_html=True
|
| 2236 |
+
)
|