SHELLAPANDIANGANHUNGING commited on
Commit
7a6d211
·
verified ·
1 Parent(s): 538cc62

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -46
app.py CHANGED
@@ -1999,7 +1999,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
1999
  "obj6_top2_categories": [],
2000
  }
2001
 
2002
- # === 1. 9 lokasi rasio terendah ===
2003
  if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2004
  calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2005
  calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
@@ -2015,7 +2015,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2015
  lowest_9 = loc_avg.nsmallest(9)
2016
  dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 3)) for loc, ratio in lowest_9.items()]
2017
 
2018
- # === 2a: divisirasio temuan/orang terendah (Obj 3a) ===
2019
  if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2020
  calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2021
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
@@ -2031,7 +2031,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2031
  val = round(div_ratio.min(), 2)
2032
  dev["obj3a_lowest_div"] = (name, val)
2033
 
2034
- # === 2b: eksekutorlead time terpanjang (Obj 3b) ===
2035
  if 'days_to_close' in df.columns:
2036
  valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2037
  exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
@@ -2042,7 +2042,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2042
  val = round(lead.max(), 1)
2043
  dev["obj3b_slowest_executor"] = (name, val)
2044
 
2045
- # === 2c: reporterfrekuensi terendah (Obj 3c) ===
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')
@@ -2054,7 +2054,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2054
  val = round(avg.min(), 2)
2055
  dev["obj3c_lowest_reporter"] = (name, val)
2056
 
2057
- # === 2d: divisilead time terpanjang (Obj 3d) ===
2058
  if 'days_to_close' in df.columns and 'nama' in df.columns:
2059
  valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2060
  if not valid.empty:
@@ -2064,14 +2064,14 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2064
  val = round(lead.max(), 1)
2065
  dev["obj3d_slowest_div"] = (name, val)
2066
 
2067
- # === 3. Komposisi non-Positive ===
2068
  if 'temuan_kategori' in df.columns:
2069
  cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2070
  dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 1)
2071
  dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 1)
2072
  dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 1)
2073
 
2074
- # === 4. Kuadran Risiko (X=20, Y=3) ===
2075
  X_LIMIT, Y_LIMIT = 20, 3
2076
  if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2077
  calc = df.copy()
@@ -2087,7 +2087,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2087
  elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2088
  dev["obj5_q2_divs"].append(r['nama'])
2089
 
2090
- # === 5. Top 2 non-Positive category ===
2091
  if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2092
  nonpos = df[df['temuan_kategori'] != 'Positive']
2093
  if not nonpos.empty:
@@ -2101,100 +2101,101 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
2101
 
2102
  dev = extract_agentic_insights_v5(df_filtered)
2103
 
2104
- # === INSIGHT BUILD ===
2105
  insight_lines = []
2106
 
2107
- # 1. 9 lokasi terendah (semua)
2108
  if dev["lowest_ratio_9_locs"]:
2109
  loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2110
- insight_lines.append(f"1. Sembilan lokasi dengan rasio temuan/orang <em>terendah</em> (<0.5): {loc_list}.")
2111
 
2112
- # 2. Gabungan 2a–2d satu insight agentic AI
2113
  parts = []
2114
  if dev["obj3a_lowest_div"]:
2115
- parts.append(f"divisi <strong>{dev['obj3a_lowest_div'][0]}</strong> (rasio {dev['obj3a_lowest_div'][1]})")
2116
  if dev["obj3c_lowest_reporter"]:
2117
- parts.append(f"reporter <strong>{dev['obj3c_lowest_reporter'][0]}</strong> ({dev['obj3c_lowest_reporter'][1]}/bln)")
2118
  if dev["obj3d_slowest_div"]:
2119
- parts.append(f"divisi <strong>{dev['obj3d_slowest_div'][0]}</strong> ({dev['obj3d_slowest_div'][1]} hari)")
2120
  if dev["obj3b_slowest_executor"]:
2121
- parts.append(f"eksekutor <strong>{dev['obj3b_slowest_executor'][0]}</strong> ({dev['obj3b_slowest_executor'][1]} hari)")
2122
 
2123
  if parts:
2124
  joined = "; ".join(parts)
2125
  insight_lines.append(
2126
- f"2. Sistem mendeteksi <em>agency-capacity mismatch</em>: {joined}. "
2127
- "Ini menunjukkan ketidakseimbangan antara kapasitas pelaporan dan kapasitas resolusi dua pilar utama ekosistem <em>agentic safety</em>."
2128
  )
2129
 
2130
- # 3. Komposisi non-Positive
2131
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2132
  if uc + ua + nm > 0:
2133
  insight_lines.append(
2134
- f"3. Komposisi non-Positive: Unsafe Condition ({uc}%), Unsafe Action ({ua}%), Near Miss ({nm}%). "
2135
- "Proporsi Near Miss masih rendah — indikasi <em>under-reporting of close calls</em>."
2136
  )
2137
 
2138
- # 4. Kuadran risiko
2139
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2140
- q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:3]])
2141
- q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:3]])
2142
  q1_str = q1 if q1 else "—"
2143
  q2_str = q2 if q2 else "—"
2144
- insight_lines.append(f"4. Divisi risiko tinggi (Kuadran I): {q1_str}. Divisi risiko tersembunyi (Kuadran II): {q2_str}.")
2145
 
2146
- # 5. Top 2 kategori
2147
  if dev["obj6_top2_categories"]:
2148
  c1, c2 = dev["obj6_top2_categories"]
2149
- 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).")
 
 
2150
 
2151
  insight_text = "<br>".join(insight_lines)
2152
 
2153
- # === REKOMENDASI & MITIGASI (Agentic AI Style) ===
2154
  recs = []
2155
 
2156
- # 1
2157
  if dev["lowest_ratio_9_locs"]:
2158
  recs.append({
2159
  "point": "1",
2160
- "rec": "Luncurkan <em>Agency Activation Sprint</em> di 9 lokasi terendah: PIC Area wajib lakukan 1 inspeksi spot/minggu.",
2161
- "mit": "Aktifkan QR code checklist 3-menit + notifikasi WhatsApp otomatis. Target: rasio ≥0.5 dalam 45 hari."
2162
  })
2163
 
2164
- # 2 gabungan 2a–2d
2165
  if parts:
2166
  recs.append({
2167
  "point": "2",
2168
- "rec": "Aktifkan <em>Agentic Capacity Dashboard</em>: monitor rasio & lead time per individu/divisi secara real-time.",
2169
- "mit": "Jika deviasi >20% dari baseline → picu <em>auto-coaching alert</em> ke PIC Area & Safety Manager, disertai best practice dari division benchmark."
2170
  })
2171
 
2172
- # 3
2173
  if uc + ua + nm > 0:
2174
  recs.append({
2175
  "point": "3",
2176
- "rec": "Luncurkan <em>Near Miss Amplifier</em>: setiap laporan Unsafe Condition wajib diikuti ≥1 Near Miss terkait.",
2177
- "mit": "Sistem blokir submit jika tidak memenuhi. Tampilkan <em>Near Miss Leaderboard</em> di dashboard tim."
2178
  })
2179
 
2180
- # 4
2181
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2182
  recs.append({
2183
  "point": "4",
2184
- "rec": "Alokasikan <em>dedicated safety crew</em> untuk Kuadran I; terapkan <em>One Finding, One Day</em> untuk Kuadran II.",
2185
- "mit": "Jika masuk Kuadran I/II ≥2 bulan berturut-turut sistem auto-generate <em>executive escalation report</em> ke VP Operasi."
2186
  })
2187
 
2188
- # 5
2189
  if dev["obj6_top2_categories"]:
2190
  c1, c2 = dev["obj6_top2_categories"]
2191
  recs.append({
2192
  "point": "5",
2193
- "rec": f"Bentuk <em>RCA Task Force</em> lintas fungsi (SIPIL, ELEKTRIKAL, K3, Kontraktor) untuk {c1[0]} & {c2[0]}.",
2194
- "mit": "Revisi spesifikasi teknis & template tender: semua penawaran wajib menyertakan mitigasi berbasis temuan historis."
2195
  })
2196
 
2197
- # === TAMPILKAN ===
2198
  st.markdown(
2199
  f"""
2200
  <div class="card" style="
@@ -2212,7 +2213,7 @@ st.markdown(
2212
  unsafe_allow_html=True
2213
  )
2214
 
2215
- # Tabel rekomendasi
2216
  if recs:
2217
  rows = []
2218
  for r in recs:
@@ -2237,7 +2238,7 @@ if recs:
2237
  <thead>
2238
  <tr style="background-color:#e8f5ee;">
2239
  <th style="padding:10px; text-align:center; border:1px solid #ccc;">#</th>
2240
- <th style="padding:10px; text-align:left; border:1px solid #ccc;">Recommended Actions</th>
2241
  <th style="padding:10px; text-align:left; border:1px solid #ccc;">Risk Mitigation Strategy</th>
2242
  </tr>
2243
  </thead>
 
1999
  "obj6_top2_categories": [],
2000
  }
2001
 
2002
+ # === 1. 9 locations with lowest finding-to-reporter ratio ===
2003
  if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2004
  calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2005
  calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
 
2015
  lowest_9 = loc_avg.nsmallest(9)
2016
  dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 3)) for loc, ratio in lowest_9.items()]
2017
 
2018
+ # === 2a: Divisionlowest finding-to-reporter ratio (Obj 3a) ===
2019
  if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2020
  calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2021
  calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
 
2031
  val = round(div_ratio.min(), 2)
2032
  dev["obj3a_lowest_div"] = (name, val)
2033
 
2034
+ # === 2b: Executorlongest average resolution time (Obj 3b) ===
2035
  if 'days_to_close' in df.columns:
2036
  valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2037
  exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
 
2042
  val = round(lead.max(), 1)
2043
  dev["obj3b_slowest_executor"] = (name, val)
2044
 
2045
+ # === 2c: Reporterlowest reporting frequency (Obj 3c) ===
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')
 
2054
  val = round(avg.min(), 2)
2055
  dev["obj3c_lowest_reporter"] = (name, val)
2056
 
2057
+ # === 2d: Divisionlongest average resolution time (Obj 3d) ===
2058
  if 'days_to_close' in df.columns and 'nama' in df.columns:
2059
  valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2060
  if not valid.empty:
 
2064
  val = round(lead.max(), 1)
2065
  dev["obj3d_slowest_div"] = (name, val)
2066
 
2067
+ # === 3. Composition of non-Positive findings ===
2068
  if 'temuan_kategori' in df.columns:
2069
  cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2070
  dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 1)
2071
  dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 1)
2072
  dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 1)
2073
 
2074
+ # === 4. Risk Quadrants (X=20 findings/month, Y=3 days avg lead time) ===
2075
  X_LIMIT, Y_LIMIT = 20, 3
2076
  if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2077
  calc = df.copy()
 
2087
  elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2088
  dev["obj5_q2_divs"].append(r['nama'])
2089
 
2090
+ # === 5. Top 2 non-Positive categories (avg per month) ===
2091
  if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2092
  nonpos = df[df['temuan_kategori'] != 'Positive']
2093
  if not nonpos.empty:
 
2101
 
2102
  dev = extract_agentic_insights_v5(df_filtered)
2103
 
2104
+ # === INSIGHT SUMMARY ===
2105
  insight_lines = []
2106
 
2107
+ # 1. 9 lowest-ratio locations
2108
  if dev["lowest_ratio_9_locs"]:
2109
  loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2110
+ insight_lines.append(f"1. Nine locations with the <em>lowest</em> finding-to-reporter ratio: {loc_list}.")
2111
 
2112
+ # 2. Combined lowest-performer insights (3a–3d)
2113
  parts = []
2114
  if dev["obj3a_lowest_div"]:
2115
+ parts.append(f"division <strong>{dev['obj3a_lowest_div'][0]}</strong> (ratio: {dev['obj3a_lowest_div'][1]})")
2116
  if dev["obj3c_lowest_reporter"]:
2117
+ parts.append(f"reporter <strong>{dev['obj3c_lowest_reporter'][0]}</strong> ({dev['obj3c_lowest_reporter'][1]} findings/month)")
2118
  if dev["obj3d_slowest_div"]:
2119
+ parts.append(f"division <strong>{dev['obj3d_slowest_div'][0]}</strong> (avg. resolution: {dev['obj3d_slowest_div'][1]} days)")
2120
  if dev["obj3b_slowest_executor"]:
2121
+ parts.append(f"executor <strong>{dev['obj3b_slowest_executor'][0]}</strong> (avg. resolution: {dev['obj3b_slowest_executor'][1]} days)")
2122
 
2123
  if parts:
2124
  joined = "; ".join(parts)
2125
  insight_lines.append(
2126
+ f"2. Agentic AI detection flags uneven operational capacity: {joined}. "
2127
+ "This suggests imbalance in reporting engagement and resolution efficiency across divisions and individuals."
2128
  )
2129
 
2130
+ # 3. Non-Positive composition
2131
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2132
  if uc + ua + nm > 0:
2133
  insight_lines.append(
2134
+ f"3. Non-Positive finding composition: Unsafe Condition ({uc}%), Unsafe Action ({ua}%), Near Miss ({nm}%)."
 
2135
  )
2136
 
2137
+ # 4. Risk Quadrants
2138
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2139
+ q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:5]]) # cap for readability
2140
+ q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:5]])
2141
  q1_str = q1 if q1 else "—"
2142
  q2_str = q2 if q2 else "—"
2143
+ insight_lines.append(f"4. High-risk divisions (Quadrant I): {q1_str}. Hidden-risk divisions (Quadrant II): {q2_str}.")
2144
 
2145
+ # 5. Top non-Positive categories
2146
  if dev["obj6_top2_categories"]:
2147
  c1, c2 = dev["obj6_top2_categories"]
2148
+ insight_lines.append(
2149
+ f"5. Top two recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]}/month) and <strong>{c2[0]}</strong> ({c2[1]}/month)."
2150
+ )
2151
 
2152
  insight_text = "<br>".join(insight_lines)
2153
 
2154
+ # === RECOMMENDATIONS & MITIGATION (Agentic AI Style) ===
2155
  recs = []
2156
 
2157
+ # Rec 1: Low-ratio locations
2158
  if dev["lowest_ratio_9_locs"]:
2159
  recs.append({
2160
  "point": "1",
2161
+ "rec": "Launch <em>Agency Activation Sprint</em> across the 9 lowest-ratio locations: Area PICs to conduct ≥1 spot inspection/week.",
2162
+ "mit": "Deploy 3-minute QR-code checklists + automated WhatsApp reminders. Target: ratio 0.5 within 45 days."
2163
  })
2164
 
2165
+ # Rec 2: Capacity imbalance
2166
  if parts:
2167
  recs.append({
2168
  "point": "2",
2169
+ "rec": "Activate <em>Agentic Capacity Dashboard</em>: real-time monitoring of finding/reporter ratios and resolution lead times per division/individual.",
2170
+ "mit": "Auto-trigger <em>coaching alerts</em> to Area PICs & Safety Managers if deviation >20% from divisional baseline, with peer benchmarking insights."
2171
  })
2172
 
2173
+ # Rec 3: Category validation
2174
  if uc + ua + nm > 0:
2175
  recs.append({
2176
  "point": "3",
2177
+ "rec": "Enforce photo-based validation for Unsafe Condition/Action/Near Miss submissions to ensure accurate categorization.",
2178
+ "mit": "System blocks submission if photo evidence or category justification is missing."
2179
  })
2180
 
2181
+ # Rec 4: Quadrant-based intervention
2182
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2183
  recs.append({
2184
  "point": "4",
2185
+ "rec": "Assign <em>dedicated safety crews</em> to Quadrant I divisions; enforce <em>One Finding, One Day</em> closure policy for Quadrant II.",
2186
+ "mit": "Auto-generate executive escalation reports to VP Operations if any division remains in QI/QII for ≥2 consecutive months."
2187
  })
2188
 
2189
+ # Rec 5: Top categories RCA
2190
  if dev["obj6_top2_categories"]:
2191
  c1, c2 = dev["obj6_top2_categories"]
2192
  recs.append({
2193
  "point": "5",
2194
+ "rec": f"Form cross-functional <em>RCA Task Force</em> (Civil, Electrical, HSE, Contractors) for <strong>{c1[0]}</strong> and <strong>{c2[0]}</strong>.",
2195
+ "mit": "Update technical specifications & tender templates: all bids must include mitigations based on historical findings for these categories."
2196
  })
2197
 
2198
+ # === RENDER OUTPUT ===
2199
  st.markdown(
2200
  f"""
2201
  <div class="card" style="
 
2213
  unsafe_allow_html=True
2214
  )
2215
 
2216
+ # Recommendations table
2217
  if recs:
2218
  rows = []
2219
  for r in recs:
 
2238
  <thead>
2239
  <tr style="background-color:#e8f5ee;">
2240
  <th style="padding:10px; text-align:center; border:1px solid #ccc;">#</th>
2241
+ <th style="padding:10px; text-align:left; border:1px solid #ccc;">Recommended Action</th>
2242
  <th style="padding:10px; text-align:left; border:1px solid #ccc;">Risk Mitigation Strategy</th>
2243
  </tr>
2244
  </thead>