Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1982,27 +1982,34 @@ else:
|
|
| 1982 |
st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
|
| 1983 |
|
| 1984 |
|
| 1985 |
-
# =================== OBJECTIVE 7
|
| 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
|
| 1990 |
-
"""Ekstrak nilai aktual dari Obj 2–6 sesuai permintaan: 9 lokasi ratio=1.0, dll."""
|
| 1991 |
dev = {
|
| 1992 |
-
|
| 1993 |
-
"
|
| 1994 |
-
|
| 1995 |
-
"
|
| 1996 |
-
"
|
| 1997 |
-
|
| 1998 |
-
"
|
| 1999 |
-
"
|
| 2000 |
-
|
| 2001 |
-
"
|
| 2002 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2003 |
}
|
| 2004 |
|
| 2005 |
-
#
|
| 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')
|
|
@@ -2014,12 +2021,12 @@ def extract_deviations_for_insight(df: pd.DataFrame):
|
|
| 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()
|
| 2018 |
-
near_1 = loc_avg[(loc_avg
|
| 2019 |
-
dev["
|
| 2020 |
|
| 2021 |
-
#
|
| 2022 |
-
# 3a: divisi rasio
|
| 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')
|
|
@@ -2033,9 +2040,9 @@ def extract_deviations_for_insight(df: pd.DataFrame):
|
|
| 2033 |
if not div_ratio.empty:
|
| 2034 |
name = div_ratio.idxmin()
|
| 2035 |
val = round(div_ratio.min(), 2)
|
| 2036 |
-
dev["
|
| 2037 |
|
| 2038 |
-
#
|
| 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')
|
|
@@ -2045,41 +2052,49 @@ def extract_deviations_for_insight(df: pd.DataFrame):
|
|
| 2045 |
if not avg.empty:
|
| 2046 |
name = avg.idxmin()
|
| 2047 |
val = round(avg.min(), 2)
|
| 2048 |
-
dev["
|
| 2049 |
|
| 2050 |
-
#
|
| 2051 |
-
if 'days_to_close' in df.columns
|
| 2052 |
valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 2053 |
-
|
|
|
|
| 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["
|
| 2059 |
-
|
| 2060 |
-
|
| 2061 |
-
|
| 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["
|
| 2070 |
|
| 2071 |
-
#
|
| 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["
|
| 2077 |
-
dev["
|
| 2078 |
-
dev["
|
| 2079 |
|
| 2080 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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))
|
|
@@ -2089,182 +2104,238 @@ def extract_deviations_for_insight(df: pd.DataFrame):
|
|
| 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["
|
| 2093 |
elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
|
| 2094 |
-
dev["
|
| 2095 |
-
|
| 2096 |
-
#
|
| 2097 |
-
|
| 2098 |
-
|
| 2099 |
-
|
| 2100 |
-
|
| 2101 |
-
|
| 2102 |
-
|
| 2103 |
-
|
| 2104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2105 |
|
| 2106 |
return dev
|
| 2107 |
|
| 2108 |
-
# Ekstrak
|
| 2109 |
-
dev =
|
| 2110 |
-
|
| 2111 |
-
#
|
| 2112 |
-
|
| 2113 |
-
|
| 2114 |
-
# 1
|
| 2115 |
-
|
| 2116 |
-
|
| 2117 |
-
|
| 2118 |
-
|
| 2119 |
-
|
| 2120 |
-
|
| 2121 |
-
|
| 2122 |
-
if
|
| 2123 |
-
|
| 2124 |
-
|
| 2125 |
-
|
| 2126 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
| 2132 |
-
|
| 2133 |
-
|
| 2134 |
-
|
| 2135 |
-
|
| 2136 |
-
|
| 2137 |
-
|
| 2138 |
-
|
| 2139 |
-
|
| 2140 |
-
|
| 2141 |
-
|
| 2142 |
-
|
| 2143 |
-
|
| 2144 |
-
|
| 2145 |
-
|
| 2146 |
-
|
| 2147 |
-
if
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
| 2151 |
-
|
| 2152 |
-
|
| 2153 |
-
|
| 2154 |
-
|
| 2155 |
-
|
| 2156 |
-
|
| 2157 |
-
|
| 2158 |
-
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
-
|
| 2162 |
-
|
| 2163 |
-
if
|
| 2164 |
-
|
| 2165 |
-
|
| 2166 |
-
"
|
| 2167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2168 |
})
|
| 2169 |
|
| 2170 |
-
#
|
| 2171 |
-
if
|
| 2172 |
-
|
| 2173 |
-
"point": "
|
| 2174 |
-
"
|
| 2175 |
-
"
|
| 2176 |
})
|
| 2177 |
|
| 2178 |
-
#
|
| 2179 |
-
if
|
| 2180 |
-
|
| 2181 |
-
"point": "
|
| 2182 |
-
"
|
| 2183 |
-
"
|
| 2184 |
})
|
| 2185 |
|
| 2186 |
-
#
|
| 2187 |
-
if
|
| 2188 |
-
|
| 2189 |
-
"point": "
|
| 2190 |
-
"
|
| 2191 |
-
"
|
| 2192 |
})
|
| 2193 |
|
| 2194 |
-
#
|
| 2195 |
-
if
|
| 2196 |
-
|
| 2197 |
-
"point": "
|
| 2198 |
-
"
|
| 2199 |
-
"
|
| 2200 |
})
|
| 2201 |
|
| 2202 |
-
#
|
| 2203 |
-
if dev["
|
| 2204 |
-
|
| 2205 |
-
|
| 2206 |
-
"
|
| 2207 |
-
"
|
|
|
|
| 2208 |
})
|
| 2209 |
|
| 2210 |
-
#
|
| 2211 |
-
if
|
| 2212 |
-
|
| 2213 |
-
"point": "
|
| 2214 |
-
"
|
| 2215 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2216 |
})
|
| 2217 |
|
| 2218 |
-
#
|
| 2219 |
-
|
| 2220 |
-
"point": "
|
| 2221 |
-
"
|
| 2222 |
-
"
|
| 2223 |
})
|
| 2224 |
|
| 2225 |
-
#
|
|
|
|
| 2226 |
st.markdown(
|
| 2227 |
f"""
|
| 2228 |
<div class="card" style="
|
| 2229 |
background-color: #f8f9fa;
|
| 2230 |
-
border-left: 4px solid #
|
| 2231 |
padding: 16px;
|
| 2232 |
margin-bottom: 20px;
|
| 2233 |
border-radius: 4px;
|
| 2234 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2235 |
">
|
| 2236 |
-
<h4 style="margin-top: 0; color: #
|
| 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
|
| 2244 |
-
if
|
| 2245 |
rows = []
|
| 2246 |
-
for
|
| 2247 |
rows.append(
|
| 2248 |
f"<tr>"
|
| 2249 |
-
f"<td style='text-align:center; font-weight:bold;'>{
|
| 2250 |
-
f"<td>{
|
| 2251 |
-
f"<td>{
|
| 2252 |
f"</tr>"
|
| 2253 |
)
|
| 2254 |
table_html = f"""
|
| 2255 |
<div class="card" style="
|
| 2256 |
-
background-color: #
|
| 2257 |
-
border-left: 4px solid #
|
| 2258 |
padding: 16px;
|
| 2259 |
margin-bottom: 20px;
|
| 2260 |
border-radius: 4px;
|
| 2261 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2262 |
">
|
| 2263 |
-
<h4 style="margin-top: 0; color: #
|
| 2264 |
<table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:12px;">
|
| 2265 |
<thead>
|
| 2266 |
-
<tr style="background-color:#
|
| 2267 |
-
<th style="padding:10px; text-align:center; border:1px solid #ccc;"
|
| 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>
|
|
@@ -2277,10 +2348,4 @@ if rec_mitigation:
|
|
| 2277 |
"""
|
| 2278 |
st.markdown(table_html, unsafe_allow_html=True)
|
| 2279 |
else:
|
| 2280 |
-
st.
|
| 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 |
-
)
|
|
|
|
| 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 (FINAL v2 — Sesuai Permintaan) ===================
|
|
|
|
| 1986 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
|
| 1987 |
|
| 1988 |
+
def extract_deviations_v2(df: pd.DataFrame):
|
|
|
|
| 1989 |
dev = {
|
| 1990 |
+
# Obj 2: lokasi dengan avg ratio ≈ 1.0
|
| 1991 |
+
"obj2_optimal_locs": [],
|
| 1992 |
+
# Obj 3a & 3c: rasio temuan/orang TERENDAH (divisi & reporter)
|
| 1993 |
+
"obj3a_lowest_div_ratio": None, # ('Div A', 0.12)
|
| 1994 |
+
"obj3c_lowest_reporter_ratio": None, # ('Ali', 0.05)
|
| 1995 |
+
# Obj 3b & 3d: lead time TERPANJANG (divisi & eksekutor)
|
| 1996 |
+
"obj3b_slowest_div_lead": None, # ('Div B', 12.4)
|
| 1997 |
+
"obj3d_slowest_executor_lead": None, # ('Budi', 15.2)
|
| 1998 |
+
# Obj 4: unsafe share
|
| 1999 |
+
"obj4_uc": 0.0,
|
| 2000 |
+
"obj4_ua": 0.0,
|
| 2001 |
+
"obj4_nm": 0.0,
|
| 2002 |
+
# Obj 6 (Panel 4): top 2 non-Positive categories (avg/month tertinggi)
|
| 2003 |
+
"obj6_top2_cat": [],
|
| 2004 |
+
# Obj 5: Quadrant I & II
|
| 2005 |
+
"obj5_q1_divs": [],
|
| 2006 |
+
"obj5_q2_divs": [],
|
| 2007 |
+
# 🔥 BARU: Obj 3 (predictive panel 1–3): trend slope TERENDAH (bukan avg ratio!)
|
| 2008 |
+
"obj3_most_declining_locs": [], # 5 lokasi dengan slope paling negatif
|
| 2009 |
+
"obj3_most_declining_divs": [], # 5 divisi dengan slope paling negatif
|
| 2010 |
}
|
| 2011 |
|
| 2012 |
+
# ========= 1. Optimal Ratio (~1.0) dari Objective 2 =========
|
| 2013 |
if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2014 |
calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2015 |
calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
|
|
|
|
| 2021 |
).reset_index()
|
| 2022 |
monthly = monthly[monthly['reporters'] > 0]
|
| 2023 |
monthly['ratio'] = monthly['findings'] / monthly['reporters']
|
| 2024 |
+
loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
|
| 2025 |
+
near_1 = loc_avg[(loc_avg >= 0.95) & (loc_avg <= 1.05)].nlargest(9)
|
| 2026 |
+
dev["obj2_optimal_locs"] = near_1.index.tolist()
|
| 2027 |
|
| 2028 |
+
# ========= 2. TERENDAH — Rasio Temuan/Orang (Objective 3a & 3c) =========
|
| 2029 |
+
# 3a: divisi — rasio temuan/orang
|
| 2030 |
if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
|
| 2031 |
calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
|
| 2032 |
calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
|
|
|
|
| 2040 |
if not div_ratio.empty:
|
| 2041 |
name = div_ratio.idxmin()
|
| 2042 |
val = round(div_ratio.min(), 2)
|
| 2043 |
+
dev["obj3a_lowest_div_ratio"] = (name, val)
|
| 2044 |
|
| 2045 |
+
# 3c: individu — avg temuan/bulan
|
| 2046 |
if {'creator_name', 'created_at'}.issubset(df.columns):
|
| 2047 |
calc = df[['creator_name', 'created_at']].copy()
|
| 2048 |
calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
|
|
|
|
| 2052 |
if not avg.empty:
|
| 2053 |
name = avg.idxmin()
|
| 2054 |
val = round(avg.min(), 2)
|
| 2055 |
+
dev["obj3c_lowest_reporter_ratio"] = (name, val)
|
| 2056 |
|
| 2057 |
+
# ========= 3. TERPANJANG — Lead Time (Objective 3b & 3d) =========
|
| 2058 |
+
if 'days_to_close' in df.columns:
|
| 2059 |
valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 2060 |
+
# 3b: divisi
|
| 2061 |
+
if 'nama' in valid.columns:
|
| 2062 |
lead = valid.groupby('nama')['days_to_close'].mean()
|
| 2063 |
if not lead.empty:
|
| 2064 |
name = lead.idxmax()
|
| 2065 |
val = round(lead.max(), 1)
|
| 2066 |
+
dev["obj3b_slowest_div_lead"] = (name, val)
|
| 2067 |
+
# 3d: eksekutor (deteksi otomatis kolom)
|
| 2068 |
+
exec_col = next((c for c in ['nama_pic', 'pic', 'responsible', 'creator_name'] if c in valid.columns), None)
|
| 2069 |
+
if exec_col:
|
|
|
|
|
|
|
|
|
|
| 2070 |
lead = valid.groupby(exec_col)['days_to_close'].mean()
|
| 2071 |
if not lead.empty:
|
| 2072 |
name = lead.idxmax()
|
| 2073 |
val = round(lead.max(), 1)
|
| 2074 |
+
dev["obj3d_slowest_executor_lead"] = (name, val)
|
| 2075 |
|
| 2076 |
+
# ========= 4. Unsafe Share (Objective 4) =========
|
| 2077 |
if 'temuan_kategori' in df.columns:
|
| 2078 |
total = len(df)
|
| 2079 |
if total > 0:
|
| 2080 |
cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
|
| 2081 |
+
dev["obj4_uc"] = round(cnt.get("Unsafe Condition", 0), 1)
|
| 2082 |
+
dev["obj4_ua"] = round(cnt.get("Unsafe Action", 0), 1)
|
| 2083 |
+
dev["obj4_nm"] = round(cnt.get("Near Miss", 0), 1)
|
| 2084 |
|
| 2085 |
+
# ========= 5. Top 2 Bubble: Category Non-Positive (Objective 6 Panel 4) =========
|
| 2086 |
+
if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
|
| 2087 |
+
nonpos = df[df['temuan_kategori'] != 'Positive']
|
| 2088 |
+
if not nonpos.empty:
|
| 2089 |
+
start = nonpos['created_at'].min().to_period('M')
|
| 2090 |
+
end = nonpos['created_at'].max().to_period('M')
|
| 2091 |
+
n_months = len(pd.period_range(start=start, end=end, freq='M'))
|
| 2092 |
+
cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
|
| 2093 |
+
dev["obj6_top2_cat"] = [(cat, round(val, 1)) for cat, val in cat_avg.items()]
|
| 2094 |
+
|
| 2095 |
+
# ========= 6. Quadrant I & II (Objective 5) =========
|
| 2096 |
X_LIMIT, Y_LIMIT = 20, 3
|
| 2097 |
+
if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
|
| 2098 |
calc = df.copy()
|
| 2099 |
calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
|
| 2100 |
calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
|
|
|
|
| 2104 |
mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
|
| 2105 |
for _, r in mat.iterrows():
|
| 2106 |
if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
|
| 2107 |
+
dev["obj5_q1_divs"].append(r['nama'])
|
| 2108 |
elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
|
| 2109 |
+
dev["obj5_q2_divs"].append(r['nama'])
|
| 2110 |
+
|
| 2111 |
+
# ========= 🔥 7. TREND TERENDAH — Slope paling negatif (Objective 6 Panels 1–3) =========
|
| 2112 |
+
# Lokasi (Panel 2)
|
| 2113 |
+
if {'nama_lokasi_full', 'created_at'}.issubset(df.columns):
|
| 2114 |
+
start = df['created_at'].min().to_period('M')
|
| 2115 |
+
end = df['created_at'].max().to_period('M')
|
| 2116 |
+
all_months = pd.period_range(start=start, end=end, freq='M')
|
| 2117 |
+
monthly = (
|
| 2118 |
+
df.groupby(['nama_lokasi_full', df['created_at'].dt.to_period('M')])
|
| 2119 |
+
.size()
|
| 2120 |
+
.unstack(fill_value=0)
|
| 2121 |
+
.reindex(columns=all_months, fill_value=0)
|
| 2122 |
+
)
|
| 2123 |
+
slopes = {}
|
| 2124 |
+
for loc in monthly.index:
|
| 2125 |
+
ts = monthly.loc[loc].values
|
| 2126 |
+
if len(ts) >= 2:
|
| 2127 |
+
try:
|
| 2128 |
+
slope = np.polyfit(range(len(ts)), ts, 1)[0]
|
| 2129 |
+
slopes[loc] = slope
|
| 2130 |
+
except:
|
| 2131 |
+
continue
|
| 2132 |
+
top5 = sorted(slopes.items(), key=lambda x: x[1])[:5] # paling negatif
|
| 2133 |
+
dev["obj3_most_declining_locs"] = [loc for loc, _ in top5]
|
| 2134 |
+
|
| 2135 |
+
# Divisi (Panel 3)
|
| 2136 |
+
if {'nama', 'created_at'}.issubset(df.columns):
|
| 2137 |
+
monthly = (
|
| 2138 |
+
df.groupby(['nama', df['created_at'].dt.to_period('M')])
|
| 2139 |
+
.size()
|
| 2140 |
+
.unstack(fill_value=0)
|
| 2141 |
+
.reindex(columns=all_months, fill_value=0)
|
| 2142 |
+
)
|
| 2143 |
+
slopes = {}
|
| 2144 |
+
for div in monthly.index:
|
| 2145 |
+
ts = monthly.loc[div].values
|
| 2146 |
+
if len(ts) >= 2:
|
| 2147 |
+
try:
|
| 2148 |
+
slope = np.polyfit(range(len(ts)), ts, 1)[0]
|
| 2149 |
+
slopes[div] = slope
|
| 2150 |
+
except:
|
| 2151 |
+
continue
|
| 2152 |
+
top5 = sorted(slopes.items(), key=lambda x: x[1])[:5]
|
| 2153 |
+
dev["obj3_most_declining_divs"] = [div for div, _ in top5]
|
| 2154 |
|
| 2155 |
return dev
|
| 2156 |
|
| 2157 |
+
# Ekstrak
|
| 2158 |
+
dev = extract_deviations_v2(df_filtered)
|
| 2159 |
+
|
| 2160 |
+
# ======== INSIGHT SUMMARY (Revised Penomoran) ========
|
| 2161 |
+
insight_parts = []
|
| 2162 |
+
|
| 2163 |
+
# 1 → dari Obj 2
|
| 2164 |
+
if dev["obj2_optimal_locs"]:
|
| 2165 |
+
locs = ", ".join(dev["obj2_optimal_locs"][:5])
|
| 2166 |
+
insight_parts.append("1. Sembilan lokasi menunjukkan rasio temuan/orang optimal (~1,0), indikasi beban kerja seimbang dan pelaporan konsisten: {} dan 4 lainnya.".format(locs))
|
| 2167 |
+
|
| 2168 |
+
# 1a → reporter/Divisi TERENDAH (Obj 3a & 3c)
|
| 2169 |
+
low_div = dev["obj3a_lowest_div_ratio"]
|
| 2170 |
+
low_rep = dev["obj3c_lowest_reporter_ratio"]
|
| 2171 |
+
if low_div and low_rep:
|
| 2172 |
+
insight_parts.append(
|
| 2173 |
+
f" 1a. Namun, aktivitas inspeksi terendah terjadi di: • Divisi <strong>{low_div[0]}</strong> (rasio {low_div[1]}), "
|
| 2174 |
+
f"• Reporter <strong>{low_rep[0]}</strong> ({low_rep[1]} temuan/bulan), menunjukkan kemungkinan under-reporting atau hambatan akses."
|
| 2175 |
+
)
|
| 2176 |
+
|
| 2177 |
+
# 1b → lead time TERPANJANG (Obj 3b & 3d)
|
| 2178 |
+
slow_div = dev["obj3b_slowest_div_lead"]
|
| 2179 |
+
slow_exe = dev["obj3d_slowest_executor_lead"]
|
| 2180 |
+
if slow_div and slow_exe:
|
| 2181 |
+
insight_parts.append(
|
| 2182 |
+
f" 1b. Kapasitas resolusi terendah di: • Divisi <strong>{slow_div[0]}</strong> ({slow_div[1]} hari), "
|
| 2183 |
+
f"• Eksekutor <strong>{slow_exe[0]}</strong> ({slow_exe[1]} hari), berisiko SLA breach."
|
| 2184 |
+
)
|
| 2185 |
+
|
| 2186 |
+
# 1c → unsafe share (Obj 4)
|
| 2187 |
+
uc, ua, nm = dev["obj4_uc"], dev["obj4_ua"], dev["obj4_nm"]
|
| 2188 |
+
if uc > 0 or ua > 0 or nm > 0:
|
| 2189 |
+
insight_parts.append(
|
| 2190 |
+
f" 1c. Komposisi temuan non-Positive: Unsafe Condition ({uc}%), Unsafe Action ({ua}%), Near Miss ({nm}%)."
|
| 2191 |
+
)
|
| 2192 |
+
|
| 2193 |
+
# 1d → trend paling menurun (NEW!)
|
| 2194 |
+
decl_locs = dev["obj3_most_declining_locs"]
|
| 2195 |
+
decl_divs = dev["obj3_most_declining_divs"]
|
| 2196 |
+
if decl_locs and decl_divs:
|
| 2197 |
+
top_loc = decl_locs[0]
|
| 2198 |
+
top_div = decl_divs[0]
|
| 2199 |
+
insight_parts.append(
|
| 2200 |
+
f" 1d. Lokasi <strong>{top_loc}</strong> dan Divisi <strong>{top_div}</strong> memiliki tren aktivitas inspeksi paling menurun (slope negatif), "
|
| 2201 |
+
f"mengindikasikan potensi penurunan komitmen atau pergantian pelapor kunci."
|
| 2202 |
+
)
|
| 2203 |
+
|
| 2204 |
+
# 2 → top 2 bubble (Obj 6)
|
| 2205 |
+
if dev["obj6_top2_cat"]:
|
| 2206 |
+
c1, c2 = dev["obj6_top2_cat"]
|
| 2207 |
+
insight_parts.append(f"2. Dua kategori paling sering muncul (non-Positive): <strong>{c1[0]}</strong> ({c1[1]}/bulan) dan <strong>{c2[0]}</strong> ({c2[1]}/bulan).")
|
| 2208 |
+
|
| 2209 |
+
# 3 → quadrant I & II (Obj 5)
|
| 2210 |
+
q1 = dev["obj5_q1_divs"]
|
| 2211 |
+
q2 = dev["obj5_q2_divs"]
|
| 2212 |
+
if q1:
|
| 2213 |
+
insight_parts.append(f"3. Divisi risiko tinggi (volume tinggi + lead time tinggi / Quadrant I): {', '.join(q1[:3])}.")
|
| 2214 |
+
if q2:
|
| 2215 |
+
insight_parts.append(f" 3a. Divisi risiko tersembunyi (volume rendah + lead time tinggi / Quadrant II): {', '.join(q2[:3])}.")
|
| 2216 |
+
|
| 2217 |
+
# 4 → sintesis
|
| 2218 |
+
insight_parts.append("4. Pola utama: terjadi *mismatch* antara kapasitas inspeksi (menurun di beberapa area) dan kapasitas resolusi (terlalu lambat), menyebabkan hazard tetap terbuka dalam jangka waktu lama.")
|
| 2219 |
+
|
| 2220 |
+
insight_text = "<br>".join(insight_parts)
|
| 2221 |
+
|
| 2222 |
+
# ======== REKOMENDASI & MITIGASI ========
|
| 2223 |
+
recoms = []
|
| 2224 |
+
|
| 2225 |
+
# 1 → benchmark lokasi optimal
|
| 2226 |
+
if dev["obj2_optimal_locs"]:
|
| 2227 |
+
recoms.append({
|
| 2228 |
+
"point": "1",
|
| 2229 |
+
"rec": "Jadikan 9 lokasi ber-rasio ~1,0 sebagai benchmark operasional: dokumentasikan rotasi pelapor, checklist, dan frekuensi.",
|
| 2230 |
+
"mit": "Tetapkan *tolerance band* rasio 0,8–1,2. Jika keluar band ≥2 bulan → picu *capacity review* otomatis."
|
| 2231 |
})
|
| 2232 |
|
| 2233 |
+
# 1a → tingkatkan inspeksi di area rendah
|
| 2234 |
+
if low_div and low_rep:
|
| 2235 |
+
recoms.append({
|
| 2236 |
+
"point": "1a",
|
| 2237 |
+
"rec": "Luncurkan *micro-inspection sprint* di divisi & reporter terendah: target minimal 0,5 temuan/bulan/orang dalam 30 hari.",
|
| 2238 |
+
"mit": "Aktifkan *QR code checklist* 3-menit di lokasi kritis. Integrasi reward points di aplikasi mobile."
|
| 2239 |
})
|
| 2240 |
|
| 2241 |
+
# 1b → perbaiki lead time
|
| 2242 |
+
if slow_div and slow_exe:
|
| 2243 |
+
recoms.append({
|
| 2244 |
+
"point": "1b",
|
| 2245 |
+
"rec": "Bentuk *Rapid Closure Team* khusus untuk divisi & eksekutor terlambat. Terapkan *photo verification* & *auto-escalation*.",
|
| 2246 |
+
"mit": "Terapkan SLA berlapis: High Risk ≤3 hari, Medium/Low ≤7 hari. >7 hari → notifikasi ke Daily Huddle & PIC Area."
|
| 2247 |
})
|
| 2248 |
|
| 2249 |
+
# 1c → tingkatkan Near Miss & Positive
|
| 2250 |
+
if uc > 0 or ua > 0 or nm > 0:
|
| 2251 |
+
recoms.append({
|
| 2252 |
+
"point": "1c",
|
| 2253 |
+
"rec": "Ubah alur pelaporan default: wajibkan 1 temuan Near Miss atau Positive per Unsafe Condition.",
|
| 2254 |
+
"mit": "Aktifkan *Positive Hunt Challenge* bulanan: reward terbaik berdasarkan kualitas & frekuensi temuan positif."
|
| 2255 |
})
|
| 2256 |
|
| 2257 |
+
# 1d → mitigasi penurunan tren
|
| 2258 |
+
if decl_locs and decl_divs:
|
| 2259 |
+
recoms.append({
|
| 2260 |
+
"point": "1d",
|
| 2261 |
+
"rec": "Identifikasi *root cause* penurunan tren: apakah perubahan PIC, rotasi, atau kelelahan? Lakukan *exit interview safety* bagi pelapor yang keluar.",
|
| 2262 |
+
"mit": "Terapkan *trend alert*: jika slope < -0.1 selama 2 bulan → notifikasi ke Safety Manager & PIC Area."
|
| 2263 |
})
|
| 2264 |
|
| 2265 |
+
# 2 → top category RCA
|
| 2266 |
+
if dev["obj6_top2_cat"]:
|
| 2267 |
+
c1, c2 = dev["obj6_top2_cat"]
|
| 2268 |
+
recoms.append({
|
| 2269 |
+
"point": "2",
|
| 2270 |
+
"rec": "Lakukan *Cross-Functional RCA* untuk <strong>{}</strong> & <strong>{}</strong>, libatkan desain, kontraktor, dan operasi.".format(c1[0], c2[0]),
|
| 2271 |
+
"mit": "Perbarui spesifikasi teknis: wajibkan mitigasi berbasis temuan historis sebelum tender dimulai."
|
| 2272 |
})
|
| 2273 |
|
| 2274 |
+
# 3 → Quadrant I & II
|
| 2275 |
+
if q1:
|
| 2276 |
+
recoms.append({
|
| 2277 |
+
"point": "3",
|
| 2278 |
+
"rec": "Alokasikan *dedicated crew* (2 orang) hanya untuk menutup temuan Quadrant I.",
|
| 2279 |
+
"mit": "Jika divisi masuk Quadrant I ≥2 bulan → eskalasi ke VP Operasi & PIC Area."
|
| 2280 |
+
})
|
| 2281 |
+
if q2:
|
| 2282 |
+
recoms.append({
|
| 2283 |
+
"point": "3a",
|
| 2284 |
+
"rec": "Terapkan *One Finding, One Day* untuk Quadrant II: semua temuan wajib selesai ≤24 jam.",
|
| 2285 |
+
"mit": "Jadikan *100% closure <24 jam* sebagai KPI supervisor. Reward: zero-backlog selama 1 bulan."
|
| 2286 |
})
|
| 2287 |
|
| 2288 |
+
# 4 → integrasi sistem
|
| 2289 |
+
recoms.append({
|
| 2290 |
+
"point": "4",
|
| 2291 |
+
"rec": "Integrasikan modul *capacity validation* ke dalam penjadwalan inspeksi: verifikasi lead time historis sebelum jadwal dibuat.",
|
| 2292 |
+
"mit": "Bangun *closed-loop safety system*: setiap temuan baru harus diverifikasi *capacity to close* terlebih dahulu."
|
| 2293 |
})
|
| 2294 |
|
| 2295 |
+
# ======== TAMPILKAN ========
|
| 2296 |
+
# Insight Card
|
| 2297 |
st.markdown(
|
| 2298 |
f"""
|
| 2299 |
<div class="card" style="
|
| 2300 |
background-color: #f8f9fa;
|
| 2301 |
+
border-left: 4px solid #003DA5;
|
| 2302 |
padding: 16px;
|
| 2303 |
margin-bottom: 20px;
|
| 2304 |
border-radius: 4px;
|
| 2305 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2306 |
">
|
| 2307 |
+
<h4 style="margin-top: 0; color: #003DA5;">Insight Summary</h4>
|
| 2308 |
<p style="margin-bottom: 0; line-height: 1.6;">{insight_text}</p>
|
| 2309 |
</div>
|
| 2310 |
""",
|
| 2311 |
unsafe_allow_html=True
|
| 2312 |
)
|
| 2313 |
|
| 2314 |
+
# Rekomendasi Table
|
| 2315 |
+
if recoms:
|
| 2316 |
rows = []
|
| 2317 |
+
for r in recoms:
|
| 2318 |
rows.append(
|
| 2319 |
f"<tr>"
|
| 2320 |
+
f"<td style='text-align:center; font-weight:bold; width:5%;'>{r['point']}</td>"
|
| 2321 |
+
f"<td style='padding:8px;'>{r['rec']}</td>"
|
| 2322 |
+
f"<td style='padding:8px;'>{r['mit']}</td>"
|
| 2323 |
f"</tr>"
|
| 2324 |
)
|
| 2325 |
table_html = f"""
|
| 2326 |
<div class="card" style="
|
| 2327 |
+
background-color: #e8f5e9;
|
| 2328 |
+
border-left: 4px solid #4CAF50;
|
| 2329 |
padding: 16px;
|
| 2330 |
margin-bottom: 20px;
|
| 2331 |
border-radius: 4px;
|
| 2332 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 2333 |
">
|
| 2334 |
+
<h4 style="margin-top: 0; color: #2E7D32;">Recommended Actions and Risk Mitigation Strategy</h4>
|
| 2335 |
<table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:12px;">
|
| 2336 |
<thead>
|
| 2337 |
+
<tr style="background-color:#e8f5ee;">
|
| 2338 |
+
<th style="padding:10px; text-align:center; border:1px solid #ccc;">#</th>
|
| 2339 |
<th style="padding:10px; text-align:left; border:1px solid #ccc;">Recommended Actions</th>
|
| 2340 |
<th style="padding:10px; text-align:left; border:1px solid #ccc;">Risk Mitigation Strategy</th>
|
| 2341 |
</tr>
|
|
|
|
| 2348 |
"""
|
| 2349 |
st.markdown(table_html, unsafe_allow_html=True)
|
| 2350 |
else:
|
| 2351 |
+
st.info("No actionable insights generated. Ensure data contains required columns.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|