SHELLAPANDIANGANHUNGING commited on
Commit
5d8e657
·
verified ·
1 Parent(s): 859f248

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +240 -175
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 - 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')
@@ -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().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')
@@ -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["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')
@@ -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["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))
@@ -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["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="
2229
  background-color: #f8f9fa;
2230
- border-left: 4px solid #2196f3;
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: #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;
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: #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>
@@ -2277,10 +2348,4 @@ if rec_mitigation:
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
- )
 
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.")