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

Update app.py

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