Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1984,221 +1984,200 @@ else:
|
|
| 1984 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1985 |
|
| 1986 |
|
| 1987 |
-
|
| 1988 |
-
|
| 1989 |
-
|
| 1990 |
-
|
| 1991 |
-
|
| 1992 |
-
|
| 1993 |
-
|
| 1994 |
-
|
| 1995 |
-
|
| 1996 |
-
|
| 1997 |
-
|
| 1998 |
-
df_calc = df[required].copy()
|
| 1999 |
-
df_calc['created_at'] = pd.to_datetime(df_calc['created_at'], errors='coerce')
|
| 2000 |
-
df_calc = df_calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
|
| 2001 |
-
|
| 2002 |
-
if df_calc.empty:
|
| 2003 |
-
return pd.DataFrame(columns=[
|
| 2004 |
-
'nama_lokasi_full', 'avg_monthly_ratio', 'total_months_active',
|
| 2005 |
-
'total_findings', 'avg_unique_reporters_per_month'
|
| 2006 |
-
])
|
| 2007 |
-
|
| 2008 |
-
df_calc['bulan'] = df_calc['created_at'].dt.to_period('M')
|
| 2009 |
-
monthly_agg = df_calc.groupby(['nama_lokasi_full', 'bulan']).agg(
|
| 2010 |
-
findings_count=('kode_temuan', 'size'),
|
| 2011 |
-
unique_reporters=('creator_nid', 'nunique')
|
| 2012 |
-
).reset_index()
|
| 2013 |
-
|
| 2014 |
-
monthly_agg = monthly_agg[monthly_agg['unique_reporters'] > 0]
|
| 2015 |
-
monthly_agg['monthly_ratio'] = monthly_agg['findings_count'] / monthly_agg['unique_reporters']
|
| 2016 |
-
|
| 2017 |
-
loc_summary = monthly_agg.groupby('nama_lokasi_full').agg(
|
| 2018 |
-
avg_monthly_ratio=('monthly_ratio', 'mean'),
|
| 2019 |
-
total_months_active=('bulan', 'nunique'),
|
| 2020 |
-
total_findings=('findings_count', 'sum'),
|
| 2021 |
-
avg_unique_reporters_per_month=('unique_reporters', 'mean')
|
| 2022 |
-
).reset_index()
|
| 2023 |
-
|
| 2024 |
-
loc_summary['avg_monthly_ratio'] = loc_summary['avg_monthly_ratio'].round(2)
|
| 2025 |
-
loc_summary['avg_unique_reporters_per_month'] = loc_summary['avg_unique_reporters_per_month'].round(1)
|
| 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:
|
| 2032 |
-
return {
|
| 2033 |
-
"interpretation": "No findings reported. Validation of coverage or actual safety status is required.",
|
| 2034 |
-
"risk_signal": "Slight Risk",
|
| 2035 |
-
"positive_rate": 0.0
|
| 2036 |
-
}
|
| 2037 |
-
|
| 2038 |
-
total = len(loc_df)
|
| 2039 |
-
n_positive = (loc_df['temuan_kategori'] == 'Positive').sum()
|
| 2040 |
-
n_unsafe = total - n_positive
|
| 2041 |
-
perc_positive = n_positive / total if total > 0 else 0
|
| 2042 |
-
unique_reporters = loc_df['creator_nid'].nunique()
|
| 2043 |
-
months_active = loc_df['created_at'].dt.to_period('M').nunique() if 'created_at' in loc_df.columns else 1
|
| 2044 |
-
|
| 2045 |
-
if total == 0:
|
| 2046 |
-
signal = "Slight Risk"
|
| 2047 |
-
interp = "No findings reported. Validation of coverage or actual safety status is required."
|
| 2048 |
-
elif perc_positive >= 0.6:
|
| 2049 |
-
signal = "Slight Risk"
|
| 2050 |
-
interp = (
|
| 2051 |
-
f"High reporting engagement with {total} findings and {perc_positive:.0%} positive category, "
|
| 2052 |
-
f"contributed by {unique_reporters} unique reporter(s) over {months_active} month(s). "
|
| 2053 |
-
f"This indicates a proactive safety culture."
|
| 2054 |
-
)
|
| 2055 |
-
elif perc_positive >= 0.3:
|
| 2056 |
-
signal = "Moderate Risk"
|
| 2057 |
-
interp = (
|
| 2058 |
-
f"Balanced reporting with {n_unsafe} unsafe findings versus {n_positive} positive. "
|
| 2059 |
-
f"Active monitoring is present, with opportunity to increase preventive behaviors."
|
| 2060 |
-
)
|
| 2061 |
-
else:
|
| 2062 |
-
if unique_reporters == 1:
|
| 2063 |
-
signal = "High Risk"
|
| 2064 |
-
interp = (
|
| 2065 |
-
f"High volume of unsafe findings with low positivity ({perc_positive:.0%}) "
|
| 2066 |
-
f"and reliance on only one reporter. This may indicate observer fatigue, bias, "
|
| 2067 |
-
f"or psychological barriers to broader reporting."
|
| 2068 |
-
)
|
| 2069 |
-
else:
|
| 2070 |
-
signal = "Very High Risk"
|
| 2071 |
-
interp = (
|
| 2072 |
-
f"Predominantly unsafe findings ({n_unsafe} out of {total}) reported by multiple individuals, "
|
| 2073 |
-
f"suggesting genuine and systemic safety hazards requiring urgent management attention."
|
| 2074 |
-
)
|
| 2075 |
-
|
| 2076 |
-
return {
|
| 2077 |
-
"interpretation": interp,
|
| 2078 |
-
"risk_signal": signal,
|
| 2079 |
-
"positive_rate": perc_positive
|
| 2080 |
}
|
| 2081 |
|
| 2082 |
-
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
|
| 2086 |
-
|
| 2087 |
-
|
| 2088 |
-
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
|
| 2096 |
-
|
| 2097 |
-
|
| 2098 |
-
|
| 2099 |
-
|
| 2100 |
-
|
| 2101 |
-
|
| 2102 |
-
|
| 2103 |
-
|
| 2104 |
-
|
| 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 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
| 2132 |
-
#
|
| 2133 |
-
if '
|
| 2134 |
-
|
| 2135 |
-
|
| 2136 |
-
|
| 2137 |
-
|
| 2138 |
-
|
| 2139 |
-
|
| 2140 |
-
|
| 2141 |
-
|
| 2142 |
-
|
| 2143 |
-
|
| 2144 |
-
|
| 2145 |
-
|
| 2146 |
-
|
| 2147 |
-
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
|
| 2167 |
-
|
| 2168 |
-
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2172 |
)
|
| 2173 |
|
| 2174 |
-
|
| 2175 |
-
|
| 2176 |
-
|
| 2177 |
-
|
| 2178 |
-
|
| 2179 |
-
|
| 2180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2181 |
)
|
| 2182 |
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
recommendation_text = " ".join(rec_parts)
|
| 2186 |
-
mitigation_text = " ".join(mitigation_parts)
|
| 2187 |
|
| 2188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2189 |
|
| 2190 |
-
|
| 2191 |
-
|
| 2192 |
-
|
|
|
|
|
|
|
| 2193 |
|
| 2194 |
-
|
| 2195 |
-
|
| 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 |
-
#
|
| 2202 |
st.markdown(
|
| 2203 |
f"""
|
| 2204 |
<div class="card" style="
|
|
@@ -2210,13 +2189,12 @@ st.markdown(
|
|
| 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;">{
|
| 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="
|
|
@@ -2228,8 +2206,8 @@ st.markdown(
|
|
| 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> {
|
| 2232 |
-
<p><strong>Risk Mitigation Strategy:</strong> {
|
| 2233 |
</div>
|
| 2234 |
""",
|
| 2235 |
unsafe_allow_html=True
|
|
|
|
| 1984 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1985 |
|
| 1986 |
|
| 1987 |
+
def extract_critical_deviations(df: pd.DataFrame):
|
| 1988 |
+
dev = {
|
| 1989 |
+
"obj2_locations_ratio_1": [],
|
| 1990 |
+
"obj3a_lowest_div_ratio": None,
|
| 1991 |
+
"obj3b_lowest_reporter": None,
|
| 1992 |
+
"obj3c_slowest_div_leadtime": None,
|
| 1993 |
+
"obj3d_slowest_executor": None,
|
| 1994 |
+
"obj4_unsafe_share": {},
|
| 1995 |
+
"obj5_quadrant_I": [],
|
| 1996 |
+
"obj5_quadrant_II": [],
|
| 1997 |
+
"obj6_top2_bubbles": []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1998 |
}
|
| 1999 |
|
| 2000 |
+
# === OBJ 2: 9 lokasi dengan finding ratio ≈ 1.0 (rentang 0.95–1.05) ===
|
| 2001 |
+
if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2002 |
+
df_calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2003 |
+
df_calc['created_at'] = pd.to_datetime(df_calc['created_at'], errors='coerce')
|
| 2004 |
+
df_calc = df_calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
|
| 2005 |
+
df_calc['bulan'] = df_calc['created_at'].dt.to_period('M')
|
| 2006 |
+
monthly_agg = df_calc.groupby(['nama_lokasi_full', 'bulan']).agg(
|
| 2007 |
+
findings=('kode_temuan', 'size'),
|
| 2008 |
+
reporters=('creator_nid', 'nunique')
|
| 2009 |
+
).reset_index()
|
| 2010 |
+
monthly_agg = monthly_agg[monthly_agg['reporters'] > 0]
|
| 2011 |
+
monthly_agg['ratio'] = monthly_agg['findings'] / monthly_agg['reporters']
|
| 2012 |
+
loc_avg = monthly_agg.groupby('nama_lokasi_full')['ratio'].mean().reset_index()
|
| 2013 |
+
# Ambil yang 0.95 ≤ ratio ≤ 1.05
|
| 2014 |
+
near_1 = loc_avg[(loc_avg['ratio'] >= 0.95) & (loc_avg['ratio'] <= 1.05)]
|
| 2015 |
+
dev["obj2_locations_ratio_1"] = near_1.nlargest(9, 'ratio')['nama_lokasi_full'].tolist()
|
| 2016 |
+
|
| 2017 |
+
# === OBJ 3a: Divisi dengan rasio temuan/orang terendah ===
|
| 2018 |
+
if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2019 |
+
df_ratio = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2020 |
+
df_ratio['bulan'] = pd.to_datetime(df_ratio['created_at']).dt.to_period('M')
|
| 2021 |
+
agg = df_ratio.groupby(['nama', 'bulan']).agg(
|
| 2022 |
+
findings=('kode_temuan', 'size'),
|
| 2023 |
+
reporters=('creator_nid', 'nunique')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2024 |
)
|
| 2025 |
+
agg = agg[agg['reporters'] > 0].reset_index()
|
| 2026 |
+
agg['ratio'] = agg['findings'] / agg['reporters']
|
| 2027 |
+
div_ratio = agg.groupby('nama')['ratio'].mean()
|
| 2028 |
+
if not div_ratio.empty:
|
| 2029 |
+
lowest = div_ratio.idxmin()
|
| 2030 |
+
dev["obj3a_lowest_div_ratio"] = (lowest, round(div_ratio.min(), 2))
|
| 2031 |
+
|
| 2032 |
+
# === OBJ 3b: Reporter dengan frekuensi terendah (>0) ===
|
| 2033 |
+
if {'creator_name', 'created_at'}.issubset(df.columns):
|
| 2034 |
+
df_rep = df[['creator_name', 'created_at']].copy()
|
| 2035 |
+
df_rep['bulan'] = pd.to_datetime(df_rep['created_at']).dt.to_period('M')
|
| 2036 |
+
rep_monthly = df_rep.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
|
| 2037 |
+
rep_avg = rep_monthly.groupby('creator_name')['count'].mean()
|
| 2038 |
+
if not rep_avg.empty and rep_avg.min() > 0:
|
| 2039 |
+
lowest = rep_avg.idxmin()
|
| 2040 |
+
dev["obj3b_lowest_reporter"] = (lowest, round(rep_avg.min(), 2))
|
| 2041 |
+
|
| 2042 |
+
# === OBJ 3c & 3d: Lead time terpanjang (divisi & individu) ===
|
| 2043 |
+
if 'days_to_close' in df.columns:
|
| 2044 |
+
valid_df = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 2045 |
+
|
| 2046 |
+
# 3c: divisi
|
| 2047 |
+
if 'nama' in valid_df.columns:
|
| 2048 |
+
div_lead = valid_df.groupby('nama')['days_to_close'].mean()
|
| 2049 |
+
if not div_lead.empty:
|
| 2050 |
+
slowest = div_lead.idxmax()
|
| 2051 |
+
dev["obj3c_slowest_div_leadtime"] = (slowest, round(div_lead.max(), 1))
|
| 2052 |
+
|
| 2053 |
+
# 3d: executor (prioritas: nama_pic → creator_name)
|
| 2054 |
+
executor_col = 'nama_pic' if 'nama_pic' in valid_df.columns else 'creator_name'
|
| 2055 |
+
if executor_col in valid_df.columns:
|
| 2056 |
+
exec_lead = valid_df.groupby(executor_col)['days_to_close'].mean()
|
| 2057 |
+
if not exec_lead.empty:
|
| 2058 |
+
slowest = exec_lead.idxmax()
|
| 2059 |
+
dev["obj3d_slowest_executor"] = (slowest, round(exec_lead.max(), 1))
|
| 2060 |
+
|
| 2061 |
+
# === OBJ 4: Pie chart — unsafe share ===
|
| 2062 |
+
if 'temuan_kategori' in df.columns:
|
| 2063 |
+
cat_counts = df['temuan_kategori'].value_counts(normalize=True) * 100
|
| 2064 |
+
unsafe_cats = ['Unsafe Condition', 'Unsafe Action', 'Near Miss']
|
| 2065 |
+
for cat in unsafe_cats:
|
| 2066 |
+
if cat in cat_counts.index:
|
| 2067 |
+
dev["obj4_unsafe_share"][cat] = round(cat_counts[cat], 1)
|
| 2068 |
+
|
| 2069 |
+
# === OBJ 5: Risk Matrix kuadran ===
|
| 2070 |
+
# Gunakan logika yang sama seperti Objective 5 (X_LIMIT=20, Y_LIMIT=3)
|
| 2071 |
+
X_LIMIT, Y_LIMIT = 20, 3
|
| 2072 |
+
if 'nama' in df.columns and 'days_to_close' in df.columns:
|
| 2073 |
+
df_risk = df.copy()
|
| 2074 |
+
df_risk['created_at'] = pd.to_datetime(df_risk['created_at'], errors='coerce')
|
| 2075 |
+
df_risk = df_risk.assign(month=df_risk['created_at'].dt.to_period('M').astype(str))
|
| 2076 |
+
# Avg bulanan per divisi
|
| 2077 |
+
monthly_counts = df_risk.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
|
| 2078 |
+
avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
|
| 2079 |
+
leadtime = df_risk.groupby('nama')['days_to_close'].mean().reset_index(name='Average Lead Time')
|
| 2080 |
+
risk_mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
|
| 2081 |
+
risk_mat['Average Lead Time'] = risk_mat['Average Lead Time'].clip(lower=0)
|
| 2082 |
+
|
| 2083 |
+
for _, row in risk_mat.iterrows():
|
| 2084 |
+
div = row['nama']
|
| 2085 |
+
cnt = row['Finding Count']
|
| 2086 |
+
lt = row['Average Lead Time']
|
| 2087 |
+
if cnt >= X_LIMIT and lt >= Y_LIMIT:
|
| 2088 |
+
dev["obj5_quadrant_I"].append(div)
|
| 2089 |
+
elif cnt < X_LIMIT and lt >= Y_LIMIT:
|
| 2090 |
+
dev["obj5_quadrant_II"].append(div)
|
| 2091 |
+
|
| 2092 |
+
# === OBJ 6: Whiteboard — 2 bubble terbesar (Avg/Month tertinggi) ===
|
| 2093 |
+
if 'kategori' in df.columns and 'temuan_kategori' in df.columns:
|
| 2094 |
+
df_nonpos = df[df['temuan_kategori'] != 'Positive']
|
| 2095 |
+
if not df_nonpos.empty:
|
| 2096 |
+
start_month = df['created_at'].min().to_period('M')
|
| 2097 |
+
end_month = df['created_at'].max().to_period('M')
|
| 2098 |
+
n_months = len(pd.period_range(start=start_month, end=end_month, freq='M'))
|
| 2099 |
+
cat_avg = (
|
| 2100 |
+
df_nonpos.groupby('kategori').size() / n_months
|
| 2101 |
+
).sort_values(ascending=False).head(2)
|
| 2102 |
+
dev["obj6_top2_bubbles"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
|
| 2103 |
+
|
| 2104 |
+
return dev
|
| 2105 |
+
|
| 2106 |
+
# Jalankan ekstraksi
|
| 2107 |
+
deviations = extract_critical_deviations(df_filtered)
|
| 2108 |
+
|
| 2109 |
+
# Bangun insight berbasis temuan nyata
|
| 2110 |
+
insight_parts = []
|
| 2111 |
+
rec_parts = []
|
| 2112 |
+
|
| 2113 |
+
# Objective 2
|
| 2114 |
+
if deviations["obj2_locations_ratio_1"]:
|
| 2115 |
+
locs = ", ".join(deviations["obj2_locations_ratio_1"][:5]) # Tampilkan 5 saja di teks
|
| 2116 |
+
insight_parts.append(
|
| 2117 |
+
f"Nine locations show near-optimal finding-to-reporter ratio (~1.0), indicating balanced workload: "
|
| 2118 |
+
f"{locs}, and others."
|
| 2119 |
)
|
| 2120 |
|
| 2121 |
+
# Objective 3
|
| 2122 |
+
if deviations["obj3a_lowest_div_ratio"]:
|
| 2123 |
+
div, ratio = deviations["obj3a_lowest_div_ratio"]
|
| 2124 |
+
insight_parts.append(f"Division {div} has the lowest reporting ratio ({ratio}), suggesting potential under-utilization or resource gaps.")
|
| 2125 |
+
if deviations["obj3b_lowest_reporter"]:
|
| 2126 |
+
name, rate = deviations["obj3b_lowest_reporter"]
|
| 2127 |
+
insight_parts.append(f"Reporter {name} averages only {rate} finding(s) per month — the lowest among active staff.")
|
| 2128 |
+
if deviations["obj3c_slowest_div_leadtime"]:
|
| 2129 |
+
div, lt = deviations["obj3c_slowest_div_leadtime"]
|
| 2130 |
+
insight_parts.append(f"Division {div} takes longest to resolve findings (avg {lt} days), risking SLA breach.")
|
| 2131 |
+
if deviations["obj3d_slowest_executor"]:
|
| 2132 |
+
name, lt = deviations["obj3d_slowest_executor"]
|
| 2133 |
+
insight_parts.append(f"Executor {name} has the longest lead time ({lt} days), requiring workflow review.")
|
| 2134 |
+
|
| 2135 |
+
# Objective 4
|
| 2136 |
+
if deviations["obj4_unsafe_share"]:
|
| 2137 |
+
unsafe_list = [f"{cat} ({pct}%)" for cat, pct in deviations["obj4_unsafe_share"].items()]
|
| 2138 |
+
unsafe_str = "; ".join(unsafe_list)
|
| 2139 |
+
insight_parts.append(f"Unsafe issues dominate: {unsafe_str} of all findings.")
|
| 2140 |
+
|
| 2141 |
+
# Objective 5
|
| 2142 |
+
if deviations["obj5_quadrant_I"]:
|
| 2143 |
+
q1 = ", ".join(deviations["obj5_quadrant_I"][:3])
|
| 2144 |
+
insight_parts.append(f"High-risk divisions (high volume + slow resolution): {q1}.")
|
| 2145 |
+
if deviations["obj5_quadrant_II"]:
|
| 2146 |
+
q2 = ", ".join(deviations["obj5_quadrant_II"][:3])
|
| 2147 |
+
insight_parts.append(f"Hidden-risk divisions (low volume but very slow): {q2} — may indicate capacity or priority issues.")
|
| 2148 |
+
|
| 2149 |
+
# Objective 6
|
| 2150 |
+
if deviations["obj6_top2_bubbles"]:
|
| 2151 |
+
bub1, bub2 = deviations["obj6_top2_bubbles"]
|
| 2152 |
+
insight_parts.append(
|
| 2153 |
+
f"The two most frequently recurring unsafe issues are {bub1[0]} ({bub1[1]}/month) "
|
| 2154 |
+
f"and {bub2[0]} ({bub2[1]}/month), indicating systemic root causes."
|
| 2155 |
)
|
| 2156 |
|
| 2157 |
+
# Combine insight
|
| 2158 |
+
insight_text = " ".join(insight_parts) if insight_parts else "No significant deviations detected based on current filters."
|
|
|
|
|
|
|
| 2159 |
|
| 2160 |
+
# Rekomendasi & Risk Mitigation
|
| 2161 |
+
rec_parts.append(
|
| 2162 |
+
"Prioritize capacity assessment and coaching for divisions and individuals with lowest activity or longest resolution times."
|
| 2163 |
+
)
|
| 2164 |
+
rec_parts.append(
|
| 2165 |
+
"Initiate root-cause analysis on top two high-frequency unsafe categories to prevent recurrence."
|
| 2166 |
+
)
|
| 2167 |
+
rec_parts.append(
|
| 2168 |
+
"Review workload distribution for locations with ratio ≈1.0 — they represent a benchmark for sustainable inspection load."
|
| 2169 |
+
)
|
| 2170 |
|
| 2171 |
+
mitigation_parts = [
|
| 2172 |
+
"Establish SLA thresholds: max 7 days lead time, min 0.5 findings/reporter/month for active status.",
|
| 2173 |
+
"Deploy predictive alerts when a division enters Quadrant I or II in the risk matrix.",
|
| 2174 |
+
"Integrate category-level trend monitoring into monthly safety meetings to catch emerging risks early."
|
| 2175 |
+
]
|
| 2176 |
|
| 2177 |
+
recommendation_text = " ".join(rec_parts)
|
| 2178 |
+
mitigation_text = " ".join(mitigation_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2179 |
|
| 2180 |
+
# Tampilkan — SATU CARD INSIGHT, SATU CARD REKOMENDASI + MITIGASI
|
| 2181 |
st.markdown(
|
| 2182 |
f"""
|
| 2183 |
<div class="card" style="
|
|
|
|
| 2189 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2190 |
">
|
| 2191 |
<h4 style="margin-top: 0; color: #1976d2;">Insight Summary</h4>
|
| 2192 |
+
<p style="margin-bottom: 0;">{insight_text}</p>
|
| 2193 |
</div>
|
| 2194 |
""",
|
| 2195 |
unsafe_allow_html=True
|
| 2196 |
)
|
| 2197 |
|
|
|
|
| 2198 |
st.markdown(
|
| 2199 |
f"""
|
| 2200 |
<div class="card" style="
|
|
|
|
| 2206 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2207 |
">
|
| 2208 |
<h4 style="margin-top: 0; color: #2e7d32;">Recommended Actions and Risk Mitigation Strategy</h4>
|
| 2209 |
+
<p><strong>Recommended Actions:</strong> {recommendation_text}</p>
|
| 2210 |
+
<p><strong>Risk Mitigation Strategy:</strong> {mitigation_text}</p>
|
| 2211 |
</div>
|
| 2212 |
""",
|
| 2213 |
unsafe_allow_html=True
|