Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1981,44 +1981,49 @@ if not df_category.empty:
|
|
| 1981 |
else:
|
| 1982 |
st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
|
| 1983 |
|
| 1984 |
-
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1985 |
-
# =================== OBJECTIVE 7 - Insight and Recommendation (Revised per Deviasi Aktual) ===================
|
| 1986 |
|
|
|
|
|
|
|
|
|
|
| 1987 |
|
| 1988 |
-
def
|
|
|
|
| 1989 |
dev = {
|
| 1990 |
-
"
|
| 1991 |
-
"
|
| 1992 |
"obj3b_lowest_reporter": None,
|
| 1993 |
-
"
|
| 1994 |
"obj3d_slowest_executor": None,
|
| 1995 |
-
"
|
|
|
|
|
|
|
| 1996 |
"obj5_quadrant_I": [],
|
| 1997 |
"obj5_quadrant_II": [],
|
| 1998 |
-
"
|
| 1999 |
}
|
| 2000 |
|
| 2001 |
-
# === OBJ 2: 9 lokasi dengan
|
| 2002 |
if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2003 |
-
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
| 2007 |
-
|
| 2008 |
findings=('kode_temuan', 'size'),
|
| 2009 |
reporters=('creator_nid', 'nunique')
|
| 2010 |
).reset_index()
|
| 2011 |
-
|
| 2012 |
-
|
| 2013 |
-
loc_avg =
|
| 2014 |
near_1 = loc_avg[(loc_avg['ratio'] >= 0.95) & (loc_avg['ratio'] <= 1.05)]
|
| 2015 |
-
dev["
|
| 2016 |
|
| 2017 |
-
# === OBJ 3a:
|
|
|
|
| 2018 |
if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2019 |
-
|
| 2020 |
-
|
| 2021 |
-
agg =
|
| 2022 |
findings=('kode_temuan', 'size'),
|
| 2023 |
reporters=('creator_nid', 'nunique')
|
| 2024 |
)
|
|
@@ -2026,148 +2031,198 @@ def extract_critical_deviations(df: pd.DataFrame):
|
|
| 2026 |
agg['ratio'] = agg['findings'] / agg['reporters']
|
| 2027 |
div_ratio = agg.groupby('nama')['ratio'].mean()
|
| 2028 |
if not div_ratio.empty:
|
| 2029 |
-
|
| 2030 |
-
|
|
|
|
| 2031 |
|
| 2032 |
-
#
|
| 2033 |
if {'creator_name', 'created_at'}.issubset(df.columns):
|
| 2034 |
-
|
| 2035 |
-
|
| 2036 |
-
|
| 2037 |
-
|
| 2038 |
-
|
| 2039 |
-
|
| 2040 |
-
|
| 2041 |
-
|
| 2042 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2043 |
if 'days_to_close' in df.columns:
|
| 2044 |
-
|
| 2045 |
-
|
| 2046 |
-
|
| 2047 |
-
|
| 2048 |
-
|
| 2049 |
-
|
| 2050 |
-
|
| 2051 |
-
dev["
|
| 2052 |
-
|
| 2053 |
-
|
| 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 |
-
|
| 2064 |
-
|
| 2065 |
-
|
| 2066 |
-
|
| 2067 |
-
|
| 2068 |
-
|
| 2069 |
-
|
|
|
|
| 2070 |
X_LIMIT, Y_LIMIT = 20, 3
|
| 2071 |
-
if 'nama'
|
| 2072 |
-
|
| 2073 |
-
|
| 2074 |
-
|
| 2075 |
-
monthly_counts =
|
| 2076 |
avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
|
| 2077 |
-
leadtime =
|
| 2078 |
-
|
| 2079 |
-
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
|
| 2083 |
-
|
| 2084 |
-
|
| 2085 |
-
|
| 2086 |
-
|
| 2087 |
-
|
| 2088 |
-
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
if 'kategori' in df.columns and 'temuan_kategori' in df.columns:
|
| 2092 |
-
df_nonpos = df[df['temuan_kategori'] != 'Positive'].copy()
|
| 2093 |
-
if not df_nonpos.empty:
|
| 2094 |
-
start = df['created_at'].min().to_period('M')
|
| 2095 |
-
end = df['created_at'].max().to_period('M')
|
| 2096 |
n_months = len(pd.period_range(start=start, end=end, freq='M'))
|
| 2097 |
-
cat_avg = (
|
| 2098 |
-
dev["
|
| 2099 |
|
| 2100 |
return dev
|
| 2101 |
|
| 2102 |
-
#
|
| 2103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2104 |
|
| 2105 |
-
#
|
| 2106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2107 |
|
| 2108 |
-
#
|
| 2109 |
-
if
|
| 2110 |
-
|
| 2111 |
-
|
| 2112 |
-
|
| 2113 |
-
|
| 2114 |
-
)
|
| 2115 |
|
| 2116 |
-
#
|
| 2117 |
-
if
|
| 2118 |
-
|
| 2119 |
-
|
| 2120 |
-
|
| 2121 |
-
|
| 2122 |
-
|
| 2123 |
-
if deviations["obj3c_slowest_div_leadtime"]:
|
| 2124 |
-
div, lt = deviations["obj3c_slowest_div_leadtime"]
|
| 2125 |
-
insight_parts.append(f"Division {div} takes longest to resolve findings (avg {lt} days), risking SLA breach.")
|
| 2126 |
-
if deviations["obj3d_slowest_executor"]:
|
| 2127 |
-
name, lt = deviations["obj3d_slowest_executor"]
|
| 2128 |
-
insight_parts.append(f"Executor {name} has the longest lead time ({lt} days), requiring workflow review.")
|
| 2129 |
-
|
| 2130 |
-
# Obj 4: Unsafe share
|
| 2131 |
-
if deviations["obj4_unsafe_share"]:
|
| 2132 |
-
unsafe_list = [f"{cat} ({pct}%)" for cat, pct in deviations["obj4_unsafe_share"].items()]
|
| 2133 |
-
unsafe_str = "; ".join(unsafe_list)
|
| 2134 |
-
insight_parts.append(f"Unsafe issues dominate: {unsafe_str} of all findings.")
|
| 2135 |
-
|
| 2136 |
-
# Obj 5: Kuadran risiko
|
| 2137 |
-
if deviations["obj5_quadrant_I"]:
|
| 2138 |
-
q1 = ", ".join(deviations["obj5_quadrant_I"][:3])
|
| 2139 |
-
insight_parts.append(f"High-risk divisions (high volume + slow resolution): {q1}.")
|
| 2140 |
-
if deviations["obj5_quadrant_II"]:
|
| 2141 |
-
q2 = ", ".join(deviations["obj5_quadrant_II"][:3])
|
| 2142 |
-
insight_parts.append(f"Hidden-risk divisions (low volume but very slow): {q2} — may indicate capacity or priority issues.")
|
| 2143 |
-
|
| 2144 |
-
# Obj 6: Top 2 bubble
|
| 2145 |
-
if deviations["obj6_top2_bubbles"]:
|
| 2146 |
-
bub1, bub2 = deviations["obj6_top2_bubbles"]
|
| 2147 |
-
insight_parts.append(
|
| 2148 |
-
f"The two most frequently recurring unsafe issues are {bub1[0]} ({bub1[1]}/month) "
|
| 2149 |
-
f"and {bub2[0]} ({bub2[1]}/month), indicating systemic root causes."
|
| 2150 |
-
)
|
| 2151 |
|
| 2152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2153 |
|
| 2154 |
-
#
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
|
|
|
| 2160 |
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
|
| 2164 |
-
|
| 2165 |
-
|
|
|
|
|
|
|
| 2166 |
|
| 2167 |
-
|
| 2168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2169 |
|
| 2170 |
-
# Tampilkan —
|
| 2171 |
st.markdown(
|
| 2172 |
f"""
|
| 2173 |
<div class="card" style="
|
|
@@ -2179,14 +2234,24 @@ st.markdown(
|
|
| 2179 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2180 |
">
|
| 2181 |
<h4 style="margin-top: 0; color: #1976d2;">Insight Summary</h4>
|
| 2182 |
-
<p style="margin-bottom: 0;">{insight_text}</p>
|
| 2183 |
</div>
|
| 2184 |
""",
|
| 2185 |
unsafe_allow_html=True
|
| 2186 |
)
|
| 2187 |
|
| 2188 |
-
|
| 2189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2190 |
<div class="card" style="
|
| 2191 |
background-color: #f0f7ff;
|
| 2192 |
border-left: 4px solid #4caf50;
|
|
@@ -2196,9 +2261,26 @@ st.markdown(
|
|
| 2196 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2197 |
">
|
| 2198 |
<h4 style="margin-top: 0; color: #2e7d32;">Recommended Actions and Risk Mitigation Strategy</h4>
|
| 2199 |
-
<
|
| 2200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2201 |
</div>
|
| 2202 |
-
"""
|
| 2203 |
-
unsafe_allow_html=True
|
| 2204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1981 |
else:
|
| 1982 |
st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
|
| 1983 |
|
|
|
|
|
|
|
| 1984 |
|
| 1985 |
+
# =================== OBJECTIVE 7 - Insight and Recommendation (Revised per Deviasi Aktual) ===================
|
| 1986 |
+
# =================== OBJECTIVE 7 - Insight and Recommendation ===================
|
| 1987 |
+
st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1988 |
|
| 1989 |
+
def extract_deviations_for_insight(df: pd.DataFrame):
|
| 1990 |
+
"""Ekstrak nilai aktual dari Obj 2–6 sesuai permintaan: 9 lokasi ratio=1.0, dll."""
|
| 1991 |
dev = {
|
| 1992 |
+
"obj2_ratio_1_locs": [],
|
| 1993 |
+
"obj3a_lowest_div": None,
|
| 1994 |
"obj3b_lowest_reporter": None,
|
| 1995 |
+
"obj3c_slowest_div": None,
|
| 1996 |
"obj3d_slowest_executor": None,
|
| 1997 |
+
"obj4_unsafe_condition": 0.0,
|
| 1998 |
+
"obj4_unsafe_action": 0.0,
|
| 1999 |
+
"obj4_near_miss": 0.0,
|
| 2000 |
"obj5_quadrant_I": [],
|
| 2001 |
"obj5_quadrant_II": [],
|
| 2002 |
+
"obj6_top2_categories": []
|
| 2003 |
}
|
| 2004 |
|
| 2005 |
+
# === OBJ 2: 9 lokasi dengan ratio ≈ 1.0 ===
|
| 2006 |
if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2007 |
+
calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2008 |
+
calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
|
| 2009 |
+
calc = calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
|
| 2010 |
+
calc['bulan'] = calc['created_at'].dt.to_period('M')
|
| 2011 |
+
monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
|
| 2012 |
findings=('kode_temuan', 'size'),
|
| 2013 |
reporters=('creator_nid', 'nunique')
|
| 2014 |
).reset_index()
|
| 2015 |
+
monthly = monthly[monthly['reporters'] > 0]
|
| 2016 |
+
monthly['ratio'] = monthly['findings'] / monthly['reporters']
|
| 2017 |
+
loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean().reset_index()
|
| 2018 |
near_1 = loc_avg[(loc_avg['ratio'] >= 0.95) & (loc_avg['ratio'] <= 1.05)]
|
| 2019 |
+
dev["obj2_ratio_1_locs"] = near_1.nlargest(9, 'ratio')['nama_lokasi_full'].tolist()
|
| 2020 |
|
| 2021 |
+
# === OBJ 3a–3d: ekstrem aktual ===
|
| 2022 |
+
# 3a: divisi rasio terendah
|
| 2023 |
if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2024 |
+
calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2025 |
+
calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
|
| 2026 |
+
agg = calc.groupby(['nama', 'bulan']).agg(
|
| 2027 |
findings=('kode_temuan', 'size'),
|
| 2028 |
reporters=('creator_nid', 'nunique')
|
| 2029 |
)
|
|
|
|
| 2031 |
agg['ratio'] = agg['findings'] / agg['reporters']
|
| 2032 |
div_ratio = agg.groupby('nama')['ratio'].mean()
|
| 2033 |
if not div_ratio.empty:
|
| 2034 |
+
name = div_ratio.idxmin()
|
| 2035 |
+
val = round(div_ratio.min(), 2)
|
| 2036 |
+
dev["obj3a_lowest_div"] = (name, val)
|
| 2037 |
|
| 2038 |
+
# 3b: reporter frekuensi terendah
|
| 2039 |
if {'creator_name', 'created_at'}.issubset(df.columns):
|
| 2040 |
+
calc = df[['creator_name', 'created_at']].copy()
|
| 2041 |
+
calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
|
| 2042 |
+
monthly = calc.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
|
| 2043 |
+
avg = monthly.groupby('creator_name')['count'].mean()
|
| 2044 |
+
avg = avg[avg > 0]
|
| 2045 |
+
if not avg.empty:
|
| 2046 |
+
name = avg.idxmin()
|
| 2047 |
+
val = round(avg.min(), 2)
|
| 2048 |
+
dev["obj3b_lowest_reporter"] = (name, val)
|
| 2049 |
+
|
| 2050 |
+
# 3c: divisi lead time terpanjang
|
| 2051 |
+
if 'days_to_close' in df.columns and 'nama' in df.columns:
|
| 2052 |
+
valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 2053 |
+
if not valid.empty:
|
| 2054 |
+
lead = valid.groupby('nama')['days_to_close'].mean()
|
| 2055 |
+
if not lead.empty:
|
| 2056 |
+
name = lead.idxmax()
|
| 2057 |
+
val = round(lead.max(), 1)
|
| 2058 |
+
dev["obj3c_slowest_div"] = (name, val)
|
| 2059 |
+
|
| 2060 |
+
# 3d: eksekutor lead time terpanjang
|
| 2061 |
if 'days_to_close' in df.columns:
|
| 2062 |
+
valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 2063 |
+
exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
|
| 2064 |
+
if exec_col in valid.columns:
|
| 2065 |
+
lead = valid.groupby(exec_col)['days_to_close'].mean()
|
| 2066 |
+
if not lead.empty:
|
| 2067 |
+
name = lead.idxmax()
|
| 2068 |
+
val = round(lead.max(), 1)
|
| 2069 |
+
dev["obj3d_slowest_executor"] = (name, val)
|
| 2070 |
+
|
| 2071 |
+
# === OBJ 4: unsafe share ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2072 |
if 'temuan_kategori' in df.columns:
|
| 2073 |
+
total = len(df)
|
| 2074 |
+
if total > 0:
|
| 2075 |
+
cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
|
| 2076 |
+
dev["obj4_unsafe_condition"] = round(cnt.get("Unsafe Condition", 0), 1)
|
| 2077 |
+
dev["obj4_unsafe_action"] = round(cnt.get("Unsafe Action", 0), 1)
|
| 2078 |
+
dev["obj4_near_miss"] = round(cnt.get("Near Miss", 0), 1)
|
| 2079 |
+
|
| 2080 |
+
# === OBJ 5: kuadran risiko (X=20, Y=3) ===
|
| 2081 |
X_LIMIT, Y_LIMIT = 20, 3
|
| 2082 |
+
if {'nama', 'created_at', 'days_to_close'}.issubset(df.columns):
|
| 2083 |
+
calc = df.copy()
|
| 2084 |
+
calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
|
| 2085 |
+
calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
|
| 2086 |
+
monthly_counts = calc.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
|
| 2087 |
avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
|
| 2088 |
+
leadtime = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
|
| 2089 |
+
mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
|
| 2090 |
+
for _, r in mat.iterrows():
|
| 2091 |
+
if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
|
| 2092 |
+
dev["obj5_quadrant_I"].append(r['nama'])
|
| 2093 |
+
elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
|
| 2094 |
+
dev["obj5_quadrant_II"].append(r['nama'])
|
| 2095 |
+
|
| 2096 |
+
# === OBJ 6: top 2 bubble (non-Positive, Avg/Month tertinggi) ===
|
| 2097 |
+
if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
|
| 2098 |
+
nonpos = df[df['temuan_kategori'] != 'Positive']
|
| 2099 |
+
if not nonpos.empty:
|
| 2100 |
+
start = nonpos['created_at'].min().to_period('M')
|
| 2101 |
+
end = nonpos['created_at'].max().to_period('M')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2102 |
n_months = len(pd.period_range(start=start, end=end, freq='M'))
|
| 2103 |
+
cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
|
| 2104 |
+
dev["obj6_top2_categories"] = [(cat, round(val, 1)) for cat, val in cat_avg.items()]
|
| 2105 |
|
| 2106 |
return dev
|
| 2107 |
|
| 2108 |
+
# Ekstrak deviasi aktual
|
| 2109 |
+
dev = extract_deviations_for_insight(df_filtered)
|
| 2110 |
+
|
| 2111 |
+
# Bangun Insight Summary — sesuai struktur: 1, 2 → 2a–2d, 3, 4a, 4b, 5
|
| 2112 |
+
insight_lines = []
|
| 2113 |
+
|
| 2114 |
+
# 1. Pola umum
|
| 2115 |
+
insight_lines.append("1. Analisis deviasi menunjukkan ketidakseimbangan antara aktivitas inspeksi dan kapasitas resolusi.")
|
| 2116 |
+
|
| 2117 |
+
# 2. Temuan utama (dipecah)
|
| 2118 |
+
if dev["obj2_ratio_1_locs"]:
|
| 2119 |
+
locs = ", ".join(dev["obj2_ratio_1_locs"][:5])
|
| 2120 |
+
insight_lines.append(f"2. Lokasi dengan rasio optimal (~1,0): {locs}, dan 4 lainnya.")
|
| 2121 |
+
insight_lines.append(f" 2a. Sembilan lokasi tersebut menunjukkan beban kerja seimbang dan pelaporan konsisten.")
|
| 2122 |
+
if dev["obj3a_lowest_div"] and dev["obj3b_lowest_reporter"]:
|
| 2123 |
+
div, r1 = dev["obj3a_lowest_div"]
|
| 2124 |
+
rep, r2 = dev["obj3b_lowest_reporter"]
|
| 2125 |
+
insight_lines.append(" 2b. Aktivitas pelaporan terendah terjadi di:")
|
| 2126 |
+
insight_lines.append(f" • Divisi: {div} (rasio {r1})")
|
| 2127 |
+
insight_lines.append(f" • Reporter: {rep} ({r2} temuan/bulan)")
|
| 2128 |
+
if dev["obj3c_slowest_div"] and dev["obj3d_slowest_executor"]:
|
| 2129 |
+
div, lt1 = dev["obj3c_slowest_div"]
|
| 2130 |
+
exe, lt2 = dev["obj3d_slowest_executor"]
|
| 2131 |
+
insight_lines.append(" 2c. Lead time penyelesaian terpanjang terjadi di:")
|
| 2132 |
+
insight_lines.append(f" • Divisi: {div} ({lt1} hari)")
|
| 2133 |
+
insight_lines.append(f" • Eksekutor: {exe} ({lt2} hari)")
|
| 2134 |
+
if dev["obj4_unsafe_condition"] or dev["obj4_unsafe_action"] or dev["obj4_near_miss"]:
|
| 2135 |
+
uc, ua, nm = dev["obj4_unsafe_condition"], dev["obj4_unsafe_action"], dev["obj4_near_miss"]
|
| 2136 |
+
insight_lines.append(" 2d. Komposisi temuan:")
|
| 2137 |
+
insight_lines.append(f" • Unsafe Condition: {uc}%")
|
| 2138 |
+
insight_lines.append(f" • Unsafe Action: {ua}%")
|
| 2139 |
+
insight_lines.append(f" • Near Miss: {nm}%")
|
| 2140 |
+
|
| 2141 |
+
# 3. Objective 6 — top 2 bubble
|
| 2142 |
+
if dev["obj6_top2_categories"]:
|
| 2143 |
+
c1, c2 = dev["obj6_top2_categories"]
|
| 2144 |
+
insight_lines.append(f"3. Dua kategori temuan paling sering muncul (non-Positive): {c1[0]} ({c1[1]}/bulan) dan {c2[0]} ({c2[1]}/bulan).")
|
| 2145 |
+
|
| 2146 |
+
# 4a & 4b — Kuadran risiko
|
| 2147 |
+
if dev["obj5_quadrant_I"]:
|
| 2148 |
+
q1 = ", ".join(dev["obj5_quadrant_I"][:3])
|
| 2149 |
+
insight_lines.append(f"4a. Divisi risiko tinggi (volume tinggi + lead time tinggi): {q1}.")
|
| 2150 |
+
if dev["obj5_quadrant_II"]:
|
| 2151 |
+
q2 = ", ".join(dev["obj5_quadrant_II"][:3])
|
| 2152 |
+
insight_lines.append(f"4b. Divisi risiko tersembunyi (volume rendah + lead time tinggi): {q2}.")
|
| 2153 |
+
|
| 2154 |
+
# 5. Sintesis
|
| 2155 |
+
insight_lines.append("5. Pola utama: aktivitas inspeksi tidak diimbangi kapasitas resolusi, sehingga hazard tetap terbuka lama.")
|
| 2156 |
+
|
| 2157 |
+
insight_text = "<br>".join(insight_lines)
|
| 2158 |
+
|
| 2159 |
+
# Rekomendasi & Risk Mitigation — per poin insight
|
| 2160 |
+
rec_mitigation = []
|
| 2161 |
+
|
| 2162 |
+
# Untuk 2a
|
| 2163 |
+
if dev["obj2_ratio_1_locs"]:
|
| 2164 |
+
rec_mitigation.append({
|
| 2165 |
+
"point": "2a",
|
| 2166 |
+
"recommendation": "Jadikan 9 lokasi ber-rasio ~1,0 sebagai benchmark operasional: dokumentasikan praktik pelaporan, rotasi, dan validasi lapangan.",
|
| 2167 |
+
"mitigation": "Tetapkan standar beban kerja: 0,8–1,2 temuan/pelapor/bulan. Jika rasio <0,5 atau >1,5 selama 2 bulan → picu capacity review."
|
| 2168 |
+
})
|
| 2169 |
|
| 2170 |
+
# Untuk 2b
|
| 2171 |
+
if dev["obj3a_lowest_div"] and dev["obj3b_lowest_reporter"]:
|
| 2172 |
+
rec_mitigation.append({
|
| 2173 |
+
"point": "2b",
|
| 2174 |
+
"recommendation": "Lakukan 1:1 coaching dengan reporter terendah dan silent walkdown di divisi terendah untuk identifikasi hambatan.",
|
| 2175 |
+
"mitigation": "Aktifkan micro-checklist digital (QR code → form 3 pertanyaan). Target: kenaikan temuan/bulan ≥0,5 dalam 30 hari."
|
| 2176 |
+
})
|
| 2177 |
|
| 2178 |
+
# Untuk 2c
|
| 2179 |
+
if dev["obj3c_slowest_div"] and dev["obj3d_slowest_executor"]:
|
| 2180 |
+
rec_mitigation.append({
|
| 2181 |
+
"point": "2c",
|
| 2182 |
+
"recommendation": "Bentuk Rapid Response Task Force untuk divisi dan eksekutor dengan lead time terpanjang: lakukan workflow mapping & integrasi verifikasi foto.",
|
| 2183 |
+
"mitigation": "Terapkan SLA dua tingkat: High Risk ≤3 hari, Medium/Low ≤7 hari. Jika >7 hari → eskalasi otomatis ke Daily Safety Huddle."
|
| 2184 |
+
})
|
| 2185 |
|
| 2186 |
+
# Untuk 2d
|
| 2187 |
+
if dev["obj4_unsafe_condition"] or dev["obj4_unsafe_action"] or dev["obj4_near_miss"]:
|
| 2188 |
+
rec_mitigation.append({
|
| 2189 |
+
"point": "2d",
|
| 2190 |
+
"recommendation": "Luncurkan Positive Hunt Sprint selama 1 bulan: wajibkan 1 temuan Positive/reporter/minggu dan berikan reward.",
|
| 2191 |
+
"mitigation": "Ubah default form digital dari Unsafe Condition → Positive. Target: kenaikan Near Miss & Unsafe Action ≥3× dalam 60 hari."
|
| 2192 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2193 |
|
| 2194 |
+
# Untuk 3
|
| 2195 |
+
if dev["obj6_top2_categories"]:
|
| 2196 |
+
rec_mitigation.append({
|
| 2197 |
+
"point": "3",
|
| 2198 |
+
"recommendation": "Bentuk Cross-Functional RCA Team (SIPIL, ELEKTRIKAL, Kontraktor, Safety) untuk analisis akar masalah desain/spesifikasi.",
|
| 2199 |
+
"mitigation": "Perbarui spesifikasi tender: wajibkan mitigasi berbasis temuan historis (misal: double insulation untuk kabel outdoor)."
|
| 2200 |
+
})
|
| 2201 |
|
| 2202 |
+
# Untuk 4a
|
| 2203 |
+
if dev["obj5_quadrant_I"]:
|
| 2204 |
+
rec_mitigation.append({
|
| 2205 |
+
"point": "4a",
|
| 2206 |
+
"recommendation": "Alokasikan dedicated safety crew (2 orang) hanya untuk menutup temuan High Risk dari divisi kuadran I.",
|
| 2207 |
+
"mitigation": "Aktifkan predictive alert: jika divisi masuk Kuadran I ≥2 bulan berturut-turut → notifikasi otomatis ke Manajer Area."
|
| 2208 |
+
})
|
| 2209 |
|
| 2210 |
+
# Untuk 4b
|
| 2211 |
+
if dev["obj5_quadrant_II"]:
|
| 2212 |
+
rec_mitigation.append({
|
| 2213 |
+
"point": "4b",
|
| 2214 |
+
"recommendation": "Terapkan One Finding, One Day untuk divisi kuadran II: jika ≤3 temuan/bulan, wajib selesai dalam 24 jam.",
|
| 2215 |
+
"mitigation": "Jadikan KPI supervisor: 100% closure <24 jam. Luncurkan Zero Backlog Challenge tiap bulan."
|
| 2216 |
+
})
|
| 2217 |
|
| 2218 |
+
# Untuk 5 (sintesis)
|
| 2219 |
+
rec_mitigation.append({
|
| 2220 |
+
"point": "5",
|
| 2221 |
+
"recommendation": "Integrasikan modul kapasitas resolusi ke dalam perencanaan inspeksi: verifikasi lead time historis sebelum jadwalkan inspeksi.",
|
| 2222 |
+
"mitigation": "Bangun closed-loop system: setiap temuan baru harus diverifikasi capacity to close terlebih dahulu."
|
| 2223 |
+
})
|
| 2224 |
|
| 2225 |
+
# Tampilkan — Insight (1 card), Rekomendasi (1 card, tabel rapi)
|
| 2226 |
st.markdown(
|
| 2227 |
f"""
|
| 2228 |
<div class="card" style="
|
|
|
|
| 2234 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2235 |
">
|
| 2236 |
<h4 style="margin-top: 0; color: #1976d2;">Insight Summary</h4>
|
| 2237 |
+
<p style="margin-bottom: 0; line-height: 1.6;">{insight_text}</p>
|
| 2238 |
</div>
|
| 2239 |
""",
|
| 2240 |
unsafe_allow_html=True
|
| 2241 |
)
|
| 2242 |
|
| 2243 |
+
# Rekomendasi dalam tabel rapi
|
| 2244 |
+
if rec_mitigation:
|
| 2245 |
+
rows = []
|
| 2246 |
+
for item in rec_mitigation:
|
| 2247 |
+
rows.append(
|
| 2248 |
+
f"<tr>"
|
| 2249 |
+
f"<td style='text-align:center; font-weight:bold;'>{item['point']}</td>"
|
| 2250 |
+
f"<td>{item['recommendation']}</td>"
|
| 2251 |
+
f"<td>{item['mitigation']}</td>"
|
| 2252 |
+
f"</tr>"
|
| 2253 |
+
)
|
| 2254 |
+
table_html = f"""
|
| 2255 |
<div class="card" style="
|
| 2256 |
background-color: #f0f7ff;
|
| 2257 |
border-left: 4px solid #4caf50;
|
|
|
|
| 2261 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2262 |
">
|
| 2263 |
<h4 style="margin-top: 0; color: #2e7d32;">Recommended Actions and Risk Mitigation Strategy</h4>
|
| 2264 |
+
<table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:12px;">
|
| 2265 |
+
<thead>
|
| 2266 |
+
<tr style="background-color:#e3f2fd;">
|
| 2267 |
+
<th style="padding:10px; text-align:center; border:1px solid #ccc;">Point</th>
|
| 2268 |
+
<th style="padding:10px; text-align:left; border:1px solid #ccc;">Recommended Actions</th>
|
| 2269 |
+
<th style="padding:10px; text-align:left; border:1px solid #ccc;">Risk Mitigation Strategy</th>
|
| 2270 |
+
</tr>
|
| 2271 |
+
</thead>
|
| 2272 |
+
<tbody>
|
| 2273 |
+
{"".join(rows)}
|
| 2274 |
+
</tbody>
|
| 2275 |
+
</table>
|
| 2276 |
</div>
|
| 2277 |
+
"""
|
| 2278 |
+
st.markdown(table_html, unsafe_allow_html=True)
|
| 2279 |
+
else:
|
| 2280 |
+
st.markdown(
|
| 2281 |
+
"<div class='card' style='background-color:#f0f7ff; border-left:4px solid #4caf50; padding:16px; margin-bottom:20px;'>"
|
| 2282 |
+
"<h4 style='margin-top:0; color:#2e7d32;'>Recommended Actions and Risk Mitigation Strategy</h4>"
|
| 2283 |
+
"<p>No actionable insights generated. Ensure data contains required columns.</p>"
|
| 2284 |
+
"</div>",
|
| 2285 |
+
unsafe_allow_html=True
|
| 2286 |
+
)
|