SHELLAPANDIANGANHUNGING commited on
Commit
3f86577
·
verified ·
1 Parent(s): 5d8e657

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -224
app.py CHANGED
@@ -1980,36 +1980,30 @@ if not df_category.empty:
1980
  # st.markdown(insight_text, unsafe_allow_html=True)
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 (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')
@@ -2022,11 +2016,10 @@ def extract_deviations_v2(df: pd.DataFrame):
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. TERENDAHRasio 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,9 +2033,20 @@ def extract_deviations_v2(df: pd.DataFrame):
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: individuavg 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,47 +2056,26 @@ def extract_deviations_v2(df: pd.DataFrame):
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. TERPANJANGLead 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()
@@ -2108,192 +2091,155 @@ def extract_deviations_v2(df: pd.DataFrame):
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
- # 1atingkatkan 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
- # 1bperbaiki 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
- # 4integrasi 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="
@@ -2304,17 +2250,17 @@ st.markdown(
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>"
@@ -2331,7 +2277,7 @@ if recoms:
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;">
 
1980
  # st.markdown(insight_text, unsafe_allow_html=True)
1981
  else:
1982
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1983
+ # =================== OBJECTIVE 7 — Insight and Recommendation (Agentic AI LLM Style — Final) ===================
 
 
1984
  st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
1985
 
1986
+ def extract_agentic_insights_v4(df: pd.DataFrame):
1987
  dev = {
1988
+ # 1. 9 lokasi rasio TERENDAH
1989
+ "lowest_ratio_9_locs": [],
1990
+ # 2a–2d: dari Obj 3a–3d
1991
+ "obj3a_lowest_div": None,
1992
+ "obj3b_slowest_executor": None,
1993
+ "obj3c_lowest_reporter": None,
1994
+ "obj3d_slowest_div": None,
1995
+ # 3. Non-Positive composition
1996
+ "obj4_unsafe_condition_pct": 0.0,
1997
+ "obj4_unsafe_action_pct": 0.0,
1998
+ "obj4_near_miss_pct": 0.0,
1999
+ # 4. Quadrant I & II
 
 
 
2000
  "obj5_q1_divs": [],
2001
  "obj5_q2_divs": [],
2002
+ # 5. Top 2 non-Positive categories
2003
+ "obj6_top2_categories": [],
 
2004
  }
2005
 
2006
+ # === 1. 9 lokasi rasio terendah ===
2007
  if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2008
  calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2009
  calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
 
2016
  monthly = monthly[monthly['reporters'] > 0]
2017
  monthly['ratio'] = monthly['findings'] / monthly['reporters']
2018
  loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2019
+ lowest_9 = loc_avg.nsmallest(9)
2020
+ dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 3)) for loc, ratio in lowest_9.items()]
2021
 
2022
+ # === 2a: divisirasio temuan/orang terendah (Obj 3a) ===
 
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
  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
+ # === 2b: eksekutorlead time terpanjang (Obj 3b) ===
2039
+ if 'days_to_close' in df.columns:
2040
+ valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2041
+ exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
2042
+ if exec_col in valid.columns:
2043
+ lead = valid.groupby(exec_col)['days_to_close'].mean()
2044
+ if not lead.empty:
2045
+ name = lead.idxmax()
2046
+ val = round(lead.max(), 1)
2047
+ dev["obj3b_slowest_executor"] = (name, val)
2048
+
2049
+ # === 2c: reporter — frekuensi terendah (Obj 3c) ===
2050
  if {'creator_name', 'created_at'}.issubset(df.columns):
2051
  calc = df[['creator_name', 'created_at']].copy()
2052
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
 
2056
  if not avg.empty:
2057
  name = avg.idxmin()
2058
  val = round(avg.min(), 2)
2059
+ dev["obj3c_lowest_reporter"] = (name, val)
2060
 
2061
+ # === 2d: divisilead time terpanjang (Obj 3d) ===
2062
+ if 'days_to_close' in df.columns and 'nama' in df.columns:
2063
  valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2064
+ if not valid.empty:
 
2065
  lead = valid.groupby('nama')['days_to_close'].mean()
2066
  if not lead.empty:
2067
  name = lead.idxmax()
2068
  val = round(lead.max(), 1)
2069
+ dev["obj3d_slowest_div"] = (name, val)
 
 
 
 
 
 
 
 
2070
 
2071
+ # === 3. Komposisi non-Positive ===
2072
  if 'temuan_kategori' in df.columns:
2073
+ cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2074
+ dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 1)
2075
+ dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 1)
2076
+ dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 1)
 
 
 
 
 
 
 
 
 
 
 
 
2077
 
2078
+ # === 4. Kuadran Risiko (X=20, Y=3) ===
2079
  X_LIMIT, Y_LIMIT = 20, 3
2080
  if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2081
  calc = df.copy()
 
2091
  elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2092
  dev["obj5_q2_divs"].append(r['nama'])
2093
 
2094
+ # === 5. Top 2 non-Positive category (Obj 6) ===
2095
+ if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2096
+ nonpos = df[df['temuan_kategori'] != 'Positive']
2097
+ if not nonpos.empty:
2098
+ start = nonpos['created_at'].min().to_period('M')
2099
+ end = nonpos['created_at'].max().to_period('M')
2100
+ n_months = len(pd.period_range(start=start, end=end, freq='M'))
2101
+ cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
2102
+ dev["obj6_top2_categories"] = [(cat, round(val, 1)) for cat, val in cat_avg.items()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2103
 
2104
  return dev
2105
 
2106
  # Ekstrak
2107
+ dev = extract_agentic_insights_v4(df_filtered)
2108
+
2109
+ # 📝 INSIGHT TEXT STRUKTUR FINAL
2110
+ insight_lines = []
2111
+
2112
+ # 1. 9 lokasi rasio terendah (semua ditampilkan)
2113
+ if dev["lowest_ratio_9_locs"]:
2114
+ loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2115
+ insight_lines.append(f"1. Sembilan lokasi dengan rasio temuan/orang *terendah* (<0.5): {loc_list}.")
2116
+
2117
+ # 2. AGENTIC AI SUMMARY dari 2a–2d (BUKAN insight umum biasa)
2118
+ # Ini adalah kunci perbaikan: summary berbasis *capacity-agency-risk mismatch*
2119
+ summary_parts = []
2120
+ if dev["obj3a_lowest_div"]:
2121
+ summary_parts.append(f"divisi {dev['obj3a_lowest_div'][0]} (rasio {dev['obj3a_lowest_div'][1]})")
2122
+ if dev["obj3c_lowest_reporter"]:
2123
+ summary_parts.append(f"reporter {dev['obj3c_lowest_reporter'][0]} ({dev['obj3c_lowest_reporter'][1]}/bln)")
2124
+ if dev["obj3d_slowest_div"]:
2125
+ summary_parts.append(f"divisi {dev['obj3d_slowest_div'][0]} ({dev['obj3d_slowest_div'][1]} hari)")
2126
+ if dev["obj3b_slowest_executor"]:
2127
+ summary_parts.append(f"eksekutor {dev['obj3b_slowest_executor'][0]} ({dev['obj3b_slowest_executor'][1]} hari)")
2128
+
2129
+ if summary_parts:
2130
+ joined = "; ".join(summary_parts)
2131
+ insight_lines.append(
2132
+ "2. Sistem mendeteksi *agency-capacity mismatch*: "
2133
+ f"<strong>{joined}</strong>. "
2134
+ "Ini menunjukkan ketidakseimbangan antara kapasitas pelaporan dan kapasitas resolusi — dua dimensi kritis dalam ekosistem *agentic safety*."
2135
  )
2136
 
2137
+ # 2a–2d: detail
2138
+ if dev["obj3a_lowest_div"]:
2139
+ name, val = dev["obj3a_lowest_div"]
2140
+ insight_lines.append(f" 2a. Divisi <strong>{name}</strong> memiliki rasio temuan/orang terendah ({val}), indikasi *low agency* atau *high reporting barrier*.")
 
 
 
 
2141
 
2142
+ if dev["obj3b_slowest_executor"]:
2143
+ name, days = dev["obj3b_slowest_executor"]
2144
+ insight_lines.append(f" 2b. Eksekutor <strong>{name}</strong> memiliki lead time resolusi tertinggi ({days} hari), berisiko menjadi *bottleneck agent* dalam alur penutupan.")
 
 
 
2145
 
2146
+ if dev["obj3c_lowest_reporter"]:
2147
+ name, rate = dev["obj3c_lowest_reporter"]
2148
+ insight_lines.append(f" 2c. Reporter <strong>{name}</strong> memiliki frekuensi pelaporan terendah ({rate}/bulan), perlu *capacity uplift* melalui *nudge* atau *coaching*.")
2149
+
2150
+ if dev["obj3d_slowest_div"]:
2151
+ name, days = dev["obj3d_slowest_div"]
2152
+ insight_lines.append(f" 2d. Divisi <strong>{name}</strong> memiliki lead time rata-rata tertinggi ({days} hari), menunjukkan *systemic delay* dalam *execution loop*.")
 
 
 
2153
 
2154
+ # 3. Komposisi non-Positive
2155
+ uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2156
+ if uc + ua + nm > 0:
2157
+ insight_lines.append(f"3. Komposisi non-Positive: Unsafe Condition ({uc}%), Unsafe Action ({ua}%), Near Miss ({nm}%). Proporsi Near Miss masih rendah — indikasi *under-reporting of close calls*.")
2158
 
2159
+ # 4. Kuadran risiko
2160
+ if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2161
+ q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:3]])
2162
+ q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:3]])
2163
+ insight_lines.append(f"4. Divisi risiko tinggi (Kuadran I): {q1 if q1 else '—'}. Divisi risiko tersembunyi (Kuadran II): {q2 if q2 else '—'}.")
 
 
2164
 
2165
+ # 5. Top 2 kategori
2166
+ if dev["obj6_top2_categories"]:
2167
+ c1, c2 = dev["obj6_top2_categories"]
2168
+ insight_lines.append(f"5. Dua kategori non-Positive paling sering: <strong>{c1[0]}</strong> ({c1[1]}/bulan) dan <strong>{c2[0]}</strong> ({c2[1]}/bulan).")
2169
 
2170
+ insight_text = "<br>".join(insight_lines)
2171
 
2172
+ # 🔔 REKOMENDASI AGENTIC AI STYLE (per poin, actionable, closed-loop)
2173
+ recs = []
2174
 
2175
+ # 1 → 9 lokasi terendah
2176
+ if dev["lowest_ratio_9_locs"]:
2177
+ recs.append({
2178
  "point": "1",
2179
+ "rec": "Luncurkan *Agency Activation Sprint* di 9 lokasi terendah: PIC Area wajib lakukan 1 inspeksi spot/minggu dan catat dalam sistem.",
2180
+ "mit": "Aktifkan *QR code checklist* 3-menit + notifikasi WhatsApp otomatis setiap Senin. Target: rasio ≥0.5 dalam 45 hari."
2181
  })
2182
 
2183
+ # 2summary agentic
2184
+ if summary_parts:
2185
+ recs.append({
2186
+ "point": "2",
2187
+ "rec": "Integrasikan *Agentic Capacity Dashboard*: real-time monitoring rasio & lead time per individu/divisi. Jika deviasi >20% dari baseline picu *auto-coaching alert*.",
2188
+ "mit": "Sistem akan mengirim notifikasi ke PIC Area & Safety Manager, disertai rekomendasi tindakan berbasis best practice dari division benchmark (misal: divisi dengan rasio ~1.0)."
2189
  })
2190
 
2191
+ # 2a–2drekomendasi individual
2192
+ if dev["obj3a_lowest_div"]:
2193
+ recs.append({
2194
+ "point": "2a",
2195
+ "rec": "Assign *Safety Buddy* dari divisi berkinerja tinggi ke divisi terendah untuk 2 minggu *shadowing & transfer knowledge*.",
2196
+ "mit": "Integrasikan *micro-reporting goal*: 1 temuan/minggu/orang. Sistem otomatis lacak progres & kirim apresiasi digital tiap goal tercapai."
2197
  })
2198
+ if dev["obj3b_slowest_executor"]:
2199
+ recs.append({
2200
+ "point": "2b",
2201
+ "rec": "Aktifkan *Rapid Closure Protocol* untuk eksekutor terlambat: verifikasi via foto + approval satu tingkat → target ≤3 hari.",
2202
+ "mit": "Jika >5 hari, sistem auto-escalate ke Daily Safety Huddle & PIC Area via Telegram."
 
 
2203
  })
2204
+ if dev["obj3c_lowest_reporter"]:
2205
+ recs.append({
2206
+ "point": "2c",
2207
+ "rec": "Terapkan *Nudge Agent Program*: PIC lokasi bertanggung jawab memastikan semua anggota tim minimal 1x inspeksi/minggu.",
2208
+ "mit": "Reward: point redeemable di kantin. Sistem kirim reminder 2x/minggu + contoh temuan sederhana (e.g., APD tidak lengkap)."
 
 
2209
  })
2210
+ if dev["obj3d_slowest_div"]:
2211
+ recs.append({
2212
+ "point": "2d",
2213
+ "rec": "Lakukan *Process Mining*: petakan alur temuan dari buka → tutup, identifikasi *waste* (waiting, rework, approval bottleneck).",
2214
+ "mit": "Terapkan SLA berlapis: High Risk ≤3 hari, Medium/Low ≤7 hari. Jika breach ≥2x/bulan → picu *capacity intervention* otomatis."
 
 
 
2215
  })
2216
 
2217
+ # 3 → Near Miss
2218
+ if uc + ua + nm > 0:
2219
+ recs.append({
2220
  "point": "3",
2221
+ "rec": "Luncurkan *Near Miss Amplifier*: setiap laporan Unsafe Condition wajib diikuti minimal 1 Near Miss terkait.",
2222
+ "mit": "Sistem blokir submit jika tidak memenuhi. Tampilkan *Near Miss Leaderboard* di dashboard tim."
2223
  })
2224
+
2225
+ # 4 → Kuadran
2226
+ if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2227
+ recs.append({
2228
+ "point": "4",
2229
+ "rec": "Alokasikan *dedicated safety crew* untuk Kuadran I; terapkan *One Finding, One Day* untuk Kuadran II.",
2230
+ "mit": "Jika masuk Kuadran I/II ≥2 bulan berturut-turut → sistem auto-generate *executive escalation report* ke VP Operasi."
2231
  })
2232
 
2233
+ # 5Top 2 kategori
2234
+ if dev["obj6_top2_categories"]:
2235
+ c1, c2 = dev["obj6_top2_categories"]
2236
+ recs.append({
2237
+ "point": "5",
2238
+ "rec": f"Bentuk *RCA Task Force* lintas fungsi (SIPIL, ELEKTRIKAL, K3, Kontraktor) untuk {c1[0]} & {c2[0]}.",
2239
+ "mit": "Revisi spesifikasi teknis & template tender: semua penawaran wajib menyertakan mitigasi berbasis temuan historis."
2240
+ })
2241
 
2242
+ # 🖼️ TAMPILKAN
 
2243
  st.markdown(
2244
  f"""
2245
  <div class="card" style="
 
2250
  border-radius: 4px;
2251
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
2252
  ">
2253
+ <h4 style="margin-top: 0; color: #003DA5;">Insight Summary (Agentic AI LLM Style)</h4>
2254
+ <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{insight_text}</p>
2255
  </div>
2256
  """,
2257
  unsafe_allow_html=True
2258
  )
2259
 
2260
+ # Tabel rekomendasi
2261
+ if recs:
2262
  rows = []
2263
+ for r in recs:
2264
  rows.append(
2265
  f"<tr>"
2266
  f"<td style='text-align:center; font-weight:bold; width:5%;'>{r['point']}</td>"
 
2277
  border-radius: 4px;
2278
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
2279
  ">
2280
+ <h4 style="margin-top: 0; color: #2E7D32;">Recommended Actions & Agentic Risk Mitigation</h4>
2281
  <table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:12px;">
2282
  <thead>
2283
  <tr style="background-color:#e8f5ee;">