SHELLAPANDIANGANHUNGING commited on
Commit
a3e1c8a
·
verified ·
1 Parent(s): 0cf89fe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -301
app.py CHANGED
@@ -1985,337 +1985,102 @@ if not df_category.empty:
1985
  # st.markdown(insight_text, unsafe_allow_html=True)
1986
  else:
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
1988
- # =================== OBJECTIVE 7 — Insight and Recommendation (Agentic AI LLM Style — Final) ===================
1989
- # =================== OBJECTIVE 7 — Insight and Recommendation (Final — Agentic AI, No markdown bold) ===================
1990
- # =================== OBJECTIVE 7 — Insight and Recommendation (FINAL — 3 Cards + Phi-3-mini) ===================
1991
- import streamlit as st
1992
- import pandas as pd
1993
- import re
1994
- import os
1995
-
1996
- # ✅ SIMPAN df_filtered KE SESSION STATE (harus dilakukan SEBELUM Objective 7)
1997
- # Letakkan ini tepat setelah filtering di sidebar (setelah `submit_clicked = ...`)
1998
- st.session_state.df_filtered = df_filtered # <-- BARIS INI WAJIB ADA!
1999
-
2000
- # ==============================
2001
- # 1. IMPORT & LLM LOADING (cached)
2002
- # ==============================
2003
- try:
2004
- from transformers import pipeline
2005
- except ImportError:
2006
- st.error("❌ `transformers` not installed. Run: `pip install transformers torch accelerate sentencepiece einops`")
2007
- pipe = None
2008
- else:
2009
- @st.cache_resource
2010
- def load_llm():
2011
- try:
2012
- st.info("🧠 Loading Phi-3-mini-4k-instruct (optimized for safety recommendations)...")
2013
- pipe = pipeline(
2014
- "text-generation",
2015
- model="microsoft/Phi-3-mini-4k-instruct",
2016
- device_map="auto",
2017
- torch_dtype="auto",
2018
- trust_remote_code=True,
2019
- max_new_tokens=256
2020
- )
2021
- st.success("✅ Phi-3-mini loaded!")
2022
- return pipe
2023
- except Exception as e:
2024
- st.error(f"❌ Failed to load model: {e}")
2025
- return None
2026
- pipe = load_llm()
2027
-
2028
- # ==============================
2029
- # 2. INSIGHT EXTRACTION (sama seperti kode Anda — diperbaiki ke 2 desimal)
2030
- # ==============================
2031
- def extract_agentic_insights_v5(df: pd.DataFrame):
2032
- dev = {
2033
- "lowest_ratio_9_locs": [],
2034
- "obj3a_lowest_div": None,
2035
- "obj3b_slowest_executor": None,
2036
- "obj3c_lowest_reporter": None,
2037
- "obj3d_slowest_div": None,
2038
- "obj4_unsafe_condition_pct": 0.0,
2039
- "obj4_unsafe_action_pct": 0.0,
2040
- "obj4_near_miss_pct": 0.0,
2041
- "obj5_q1_divs": [],
2042
- "obj5_q2_divs": [],
2043
- "obj6_top2_categories": [],
2044
- }
2045
-
2046
- # 1. 9 locations with lowest finding-to-reporter ratio
2047
- if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2048
- calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2049
- calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2050
- calc = calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
2051
- calc['bulan'] = calc['created_at'].dt.to_period('M')
2052
- monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
2053
- findings=('kode_temuan', 'size'),
2054
- reporters=('creator_nid', 'nunique')
2055
- ).reset_index()
2056
- monthly = monthly[monthly['reporters'] > 0]
2057
- monthly['ratio'] = monthly['findings'] / monthly['reporters']
2058
- loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2059
- lowest_9 = loc_avg.nsmallest(9)
2060
- dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 2)) for loc, ratio in lowest_9.items()]
2061
-
2062
- # 2a: Division — lowest ratio
2063
- if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2064
- calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2065
- calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2066
- agg = calc.groupby(['nama', 'bulan']).agg(
2067
- findings=('kode_temuan', 'size'),
2068
- reporters=('creator_nid', 'nunique')
2069
- )
2070
- agg = agg[agg['reporters'] > 0].reset_index()
2071
- agg['ratio'] = agg['findings'] / agg['reporters']
2072
- div_ratio = agg.groupby('nama')['ratio'].mean()
2073
- if not div_ratio.empty:
2074
- name = div_ratio.idxmin()
2075
- val = round(div_ratio.min(), 2)
2076
- dev["obj3a_lowest_div"] = (name, val)
2077
-
2078
- # 2b: Executor — slowest resolution
2079
- if 'days_to_close' in df.columns:
2080
- valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2081
- exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
2082
- if exec_col in valid.columns:
2083
- lead = valid.groupby(exec_col)['days_to_close'].mean()
2084
- if not lead.empty:
2085
- name = lead.idxmax()
2086
- val = round(lead.max(), 2)
2087
- dev["obj3b_slowest_executor"] = (name, val)
2088
-
2089
- # 2c: Reporter — lowest frequency
2090
- if {'creator_name', 'created_at'}.issubset(df.columns):
2091
- calc = df[['creator_name', 'created_at']].copy()
2092
- calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2093
- monthly = calc.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
2094
- avg = monthly.groupby('creator_name')['count'].mean()
2095
- avg = avg[avg > 0]
2096
- if not avg.empty:
2097
- name = avg.idxmin()
2098
- val = round(avg.min(), 2)
2099
- dev["obj3c_lowest_reporter"] = (name, val)
2100
-
2101
- # 2d: Division — slowest resolution
2102
- if 'days_to_close' in df.columns and 'nama' in df.columns:
2103
- valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2104
- if not valid.empty:
2105
- lead = valid.groupby('nama')['days_to_close'].mean()
2106
- if not lead.empty:
2107
- name = lead.idxmax()
2108
- val = round(lead.max(), 2)
2109
- dev["obj3d_slowest_div"] = (name, val)
2110
-
2111
- # 3. Non-Positive composition
2112
- if 'temuan_kategori' in df.columns:
2113
- cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2114
- dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 2)
2115
- dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 2)
2116
- dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 2)
2117
-
2118
- # 4. Risk Quadrants
2119
- X_LIMIT, Y_LIMIT = 20, 3
2120
- if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2121
- calc = df.copy()
2122
- calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2123
- calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
2124
- monthly_counts = calc.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
2125
- avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
2126
- leadtime = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
2127
- mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
2128
- for _, r in mat.iterrows():
2129
- if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2130
- dev["obj5_q1_divs"].append(r['nama'])
2131
- elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2132
- dev["obj5_q2_divs"].append(r['nama'])
2133
-
2134
- # 5. Top 2 non-Positive categories
2135
- if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2136
- nonpos = df[df['temuan_kategori'] != 'Positive']
2137
- if not nonpos.empty:
2138
- start = nonpos['created_at'].min().to_period('M')
2139
- end = nonpos['created_at'].max().to_period('M')
2140
- n_months = len(pd.period_range(start=start, end=end, freq='M'))
2141
- cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
2142
- dev["obj6_top2_categories"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
2143
-
2144
- return dev
2145
-
2146
- # ==============================
2147
- # 3. LLM UTILS (aman, fallback-ready)
2148
- # ==============================
2149
- def generate_llm_text(insight: str, mode: str = "rec") -> str:
2150
- if pipe is None:
2151
- mode_map = {"rec": "Recommend action", "mit": "Mitigation strategy"}
2152
- return f"[LLM disabled] {mode_map[mode]} for: {insight[:50]}..."
2153
-
2154
- suffix = "Recommend a single high-leverage action." if mode == "rec" else "Propose one automated/systemic risk control."
2155
- messages = [
2156
- {"role": "system", "content": "You are PLN's Lead Safety AI. Output ONLY a short, professional sentence. Be directive. No markdown, no emoticons."},
2157
- {"role": "user", "content": f"Insight: {insight}\n\n{suffix}"}
2158
- ]
2159
- try:
2160
- out = pipe(
2161
- messages,
2162
- do_sample=False,
2163
- temperature=0.1,
2164
- return_full_text=False
2165
- )
2166
- text = out[0]["generated_text"].strip()
2167
- text = re.sub(r"^(Recommendation|Mitigation|Action|Control):\s*", "", text, flags=re.IGNORECASE)
2168
- text = re.sub(r"[\n\"`*]", " ", text).strip(". ")
2169
- return text[:250]
2170
- except Exception as e:
2171
- # Fallback aman (tetap sesuai gaya Anda)
2172
- fallbacks = {
2173
- ("1", "rec"): "Launch Agency Activation Sprint: ≥1 spot inspection/week per low-ratio location.",
2174
- ("1", "mit"): "Deploy QR-code checklists + automated reminders; target ratio ≥0.5 in 45 days.",
2175
- ("2", "rec"): "Activate Agentic Capacity Dashboard for real-time monitoring of reporter engagement and resolution efficiency.",
2176
- ("2", "mit"): "Auto-trigger coaching alerts if performance deviates >20% from divisional baseline.",
2177
- ("3", "rec"): "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions.",
2178
- ("3", "mit"): "System blocks submission if photo evidence or justification is missing.",
2179
- ("4", "rec"): "Assign dedicated safety crews to Quadrant I; enforce ‘One Finding, One Day’ closure for Quadrant II.",
2180
- ("4", "mit"): "Auto-generate VP escalation reports if division remains in risk quadrant ≥2 months.",
2181
- ("5", "rec"): "Form cross-functional RCA Task Force (Civil, Electrical, HSE, Contractors) for top recurring categories.",
2182
- ("5", "mit"): "Update tender templates: all bids must include mitigations for these historical findings.",
2183
- }
2184
- idx = str(len(insight_list) + 1) if 'insight_list' in locals() else "1"
2185
- return fallbacks.get((idx, mode), f"Review insight and implement targeted action for: {insight[:30]}...")
2186
-
2187
- # ==============================
2188
- # 4. RUN & RENDER
2189
- # ==============================
2190
  st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
2191
 
2192
- # Ambil df_filtered dari session state
2193
- if 'df_filtered' not in st.session_state:
2194
- st.error("⚠️ `df_filtered` not found in session state. Please apply filters first.")
2195
- st.stop()
2196
-
2197
- df_filtered = st.session_state.df_filtered
2198
  dev = extract_agentic_insights_v5(df_filtered)
2199
 
2200
- # === BUILD INSIGHT LINES ===
2201
- insight_lines = []
2202
 
 
2203
  if dev["lowest_ratio_9_locs"]:
2204
  loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio:.2f})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2205
- insight_lines.append(f"1. Nine locations with the <em>lowest</em> finding-to-reporter ratio: {loc_list}.")
 
 
 
2206
 
 
2207
  parts = []
2208
  if dev["obj3a_lowest_div"]:
2209
- name, val = dev["obj3a_lowest_div"]
2210
- parts.append(f"division <strong>{name}</strong> (ratio: {val:.2f})")
2211
  if dev["obj3c_lowest_reporter"]:
2212
- name, val = dev["obj3c_lowest_reporter"]
2213
- parts.append(f"reporter <strong>{name}</strong> ({val:.2f} findings/month)")
2214
  if dev["obj3d_slowest_div"]:
2215
- name, val = dev["obj3d_slowest_div"]
2216
- parts.append(f"division <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2217
  if dev["obj3b_slowest_executor"]:
2218
- name, val = dev["obj3b_slowest_executor"]
2219
- parts.append(f"executor <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2220
 
2221
  if parts:
2222
- insight_lines.append(
2223
- f"2. Uneven operational capacity detected: {'; '.join(parts)}. "
2224
- "This indicates systemic imbalance in reporting engagement and resolution efficiency."
2225
- )
2226
 
 
2227
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2228
  if uc + ua + nm > 0:
2229
- insight_lines.append(
2230
- f"3. Non-Positive finding composition: Unsafe Condition ({uc:.2f}%), Unsafe Action ({ua:.2f}%), Near Miss ({nm:.2f}%)."
2231
- )
 
2232
 
 
2233
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2234
  q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:3]]) or "—"
2235
  q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:3]]) or "—"
2236
- insight_lines.append(f"4. High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}.")
 
 
 
2237
 
 
2238
  if dev["obj6_top2_categories"]:
2239
  c1, c2 = dev["obj6_top2_categories"]
2240
- insight_lines.append(
2241
- f"5. Top two recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]:.2f}/month) and <strong>{c2[0]}</strong> ({c2[1]:.2f}/month)."
2242
- )
2243
-
2244
- insight_text = "<br>".join(insight_lines)
2245
-
2246
- # === RENDER 3 CARDS ===
2247
- # Card 1: Insight
2248
- st.markdown(
2249
- f"""
 
 
 
 
 
 
 
 
 
2250
  <div class="card" style="
2251
- background-color: #f8f9fa;
2252
  border-left: 5px solid #003DA5;
2253
  padding: 20px;
2254
  margin-bottom: 24px;
2255
  border-radius: 6px;
2256
- box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2257
  ">
2258
- <h4 style="margin-top: 0; color: #003DA5; text-align: center;">🔍 Insight Summary</h4>
2259
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{insight_text}</p>
 
 
 
 
 
 
 
 
 
 
 
 
2260
  </div>
2261
- """,
2262
- unsafe_allow_html=True
2263
- )
2264
-
2265
- # Card 2 & 3: Recommendation + Mitigation (only if insights exist)
2266
- if insight_lines:
2267
- rec_list, mit_list = [], []
2268
- with st.spinner("🧠 Generating Recommendation & Risk Mitigation with Phi-3-mini..."):
2269
- for i, ins in enumerate(insight_lines, 1):
2270
- clean_ins = re.sub(r"<[^>]+>", "", ins)
2271
- # Hapus nomor urut depan (misal "1. ", "2. ")
2272
- for prefix in ["1. ", "2. ", "3. ", "4. ", "5. "]:
2273
- if clean_ins.startswith(prefix):
2274
- clean_ins = clean_ins[len(prefix):]
2275
- break
2276
- clean_ins = clean_ins.strip()
2277
- rec = generate_llm_text(clean_ins, "rec")
2278
- mit = generate_llm_text(clean_ins, "mit")
2279
- rec_list.append(f"{i}. {rec}")
2280
- mit_list.append(f"{i}. {mit}")
2281
-
2282
- rec_text = "<br>".join(rec_list)
2283
- mit_text = "<br>".join(mit_list)
2284
-
2285
- # Card 2: Recommendation
2286
- st.markdown(
2287
- f"""
2288
- <div class="card" style="
2289
- background-color: #e8f5e9;
2290
- border-left: 5px solid #4CAF50;
2291
- padding: 20px;
2292
- margin-bottom: 24px;
2293
- border-radius: 6px;
2294
- box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2295
- ">
2296
- <h4 style="margin-top: 0; color: #2E7D32; text-align: center;">✅ Recommendation</h4>
2297
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{rec_text}</p>
2298
- </div>
2299
- """,
2300
- unsafe_allow_html=True
2301
- )
2302
-
2303
- # Card 3: Risk Mitigation
2304
- st.markdown(
2305
- f"""
2306
- <div class="card" style="
2307
- background-color: #e3f2fd;
2308
- border-left: 5px solid #1976D2;
2309
- padding: 20px;
2310
- margin-bottom: 24px;
2311
- border-radius: 6px;
2312
- box-shadow: 0 3px 6px rgba(0,0,0,0.06);
2313
- ">
2314
- <h4 style="margin-top: 0; color: #0D47A1; text-align: center;">🛡️ Risk Mitigation Strategy</h4>
2315
- <p style="margin-bottom: 0; line-height: 1.6; font-size: 0.98em;">{mit_text}</p>
2316
- </div>
2317
- """,
2318
- unsafe_allow_html=True
2319
- )
2320
  else:
2321
- st.info("ℹ️ No insights generated. Ensure required columns are present in the dataset.")
 
1985
  # st.markdown(insight_text, unsafe_allow_html=True)
1986
  else:
1987
  st.info("No data available for non-positive issue categories with 100% coverage and positive trend.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1988
  st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
1989
 
1990
+ # === Ekstraksi Insight (sama seperti Anda) ===
 
 
 
 
 
1991
  dev = extract_agentic_insights_v5(df_filtered)
1992
 
1993
+ # === Buat List Insight & Rekomendasi Spesifik (perbaiki duplikasi) ===
1994
+ entries = []
1995
 
1996
+ # 1. Low-ratio locations
1997
  if dev["lowest_ratio_9_locs"]:
1998
  loc_list = ", ".join([f"<strong>{loc}</strong> ({ratio:.2f})" for loc, ratio in dev["lowest_ratio_9_locs"]])
1999
+ insight = f"Nine locations with the <em>lowest</em> finding-to-reporter ratio: {loc_list}."
2000
+ rec = "Launch <em>Agency Activation Sprint</em>: assign Safety Champions to conduct ≥1 spot inspection/week per site."
2001
+ mit = "Deploy QR-code checklists + automated WhatsApp reminders. Target: ratio ≥0.5 within 45 days."
2002
+ entries.append({"Risk Category": "Reporting Coverage Risk", "Insight": insight, "Recommendation": rec, "Mitigation": mit})
2003
 
2004
+ # 2. Capacity imbalance
2005
  parts = []
2006
  if dev["obj3a_lowest_div"]:
2007
+ parts.append(f"division <strong>{dev['obj3a_lowest_div'][0]}</strong> (ratio: {dev['obj3a_lowest_div'][1]:.2f})")
 
2008
  if dev["obj3c_lowest_reporter"]:
2009
+ parts.append(f"reporter <strong>{dev['obj3c_lowest_reporter'][0]}</strong> ({dev['obj3c_lowest_reporter'][1]:.2f}/month)")
 
2010
  if dev["obj3d_slowest_div"]:
2011
+ parts.append(f"division <strong>{dev['obj3d_slowest_div'][0]}</strong> (avg. resolution: {dev['obj3d_slowest_div'][1]:.2f} days)")
 
2012
  if dev["obj3b_slowest_executor"]:
2013
+ parts.append(f"executor <strong>{dev['obj3b_slowest_executor'][0]}</strong> (avg. resolution: {dev['obj3b_slowest_executor'][1]:.2f} days)")
 
2014
 
2015
  if parts:
2016
+ insight = f"Uneven operational capacity: {'; '.join(parts)}."
2017
+ rec = "Activate <em>Agentic Capacity Dashboard</em> for real-time monitoring of reporting & resolution KPIs."
2018
+ mit = "Auto-trigger coaching alerts to Area PICs if deviation >20% from baseline, with peer benchmarking."
2019
+ entries.append({"Risk Category": "Capacity Imbalance Risk", "Insight": insight, "Recommendation": rec, "Mitigation": mit})
2020
 
2021
+ # 3. Non-Positive composition
2022
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2023
  if uc + ua + nm > 0:
2024
+ insight = f"Non-Positive finding composition: Unsafe Condition ({uc:.2f}%), Unsafe Action ({ua:.2f}%), Near Miss ({nm:.2f}%)."
2025
+ rec = "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions."
2026
+ mit = "System blocks submission if photo evidence or justification is missing."
2027
+ entries.append({"Risk Category": "Data Quality & Categorization Risk", "Insight": insight, "Recommendation": rec, "Mitigation": mit})
2028
 
2029
+ # 4. Risk Quadrants
2030
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2031
  q1 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q1_divs"][:3]]) or "—"
2032
  q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:3]]) or "—"
2033
+ insight = f"High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}."
2034
+ rec = "Assign dedicated safety crews to QI divisions; enforce <em>One Finding, One Day</em> closure for QII."
2035
+ mit = "Auto-generate executive escalation reports to VP Ops if any division remains in QI/QII ≥2 months."
2036
+ entries.append({"Risk Category": "SLA & Backlog Risk", "Insight": insight, "Recommendation": rec, "Mitigation": mit})
2037
 
2038
+ # 5. Top categories
2039
  if dev["obj6_top2_categories"]:
2040
  c1, c2 = dev["obj6_top2_categories"]
2041
+ insight = f"Top recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]:.2f}/month) and <strong>{c2[0]}</strong> ({c2[1]:.2f}/month)."
2042
+ rec = f"Form cross-functional <em>RCA Task Force</em> (Civil, Electrical, HSE, Contractors) for <strong>{c1[0]}</strong> and <strong>{c2[0]}</strong>."
2043
+ mit = "Update tender templates: all bids must include mitigations for these historical finding categories."
2044
+ entries.append({"Risk Category": "Recurring Hazard Risk", "Insight": insight, "Recommendation": rec, "Mitigation": mit})
2045
+
2046
+ # === RENDER TABEL TERPADU ===
2047
+ if entries:
2048
+ rows = []
2049
+ for e in entries:
2050
+ rows.append(f"""
2051
+ <tr>
2052
+ <td style="padding:12px; font-weight:bold; vertical-align:top; background-color:#f8f9fa;">{e['Risk Category']}</td>
2053
+ <td style="padding:12px; vertical-align:top; line-height:1.5;">{e['Insight']}</td>
2054
+ <td style="padding:12px; vertical-align:top; line-height:1.5; color:#2E7D32;"><strong>▶</strong> {e['Recommendation']}</td>
2055
+ <td style="padding:12px; vertical-align:top; line-height:1.5; color:#1976D2;"><strong>✓</strong> {e['Mitigation']}</td>
2056
+ </tr>
2057
+ """)
2058
+
2059
+ table_html = f"""
2060
  <div class="card" style="
2061
+ background-color: white;
2062
  border-left: 5px solid #003DA5;
2063
  padding: 20px;
2064
  margin-bottom: 24px;
2065
  border-radius: 6px;
2066
+ box-shadow: 0 3px 8px rgba(0,0,0,0.08);
2067
  ">
2068
+ <h4 style="margin-top: 0; color: #003DA5; text-align: center;">🔍 Objective 7 — Risk-Based Insight, Recommendation & Mitigation</h4>
2069
+ <table style="width:100%; border-collapse:collapse; font-size:0.95em; margin-top:16px;">
2070
+ <thead>
2071
+ <tr style="background-color:#e3f2fd;">
2072
+ <th style="padding:12px; text-align:center; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Risk Category</th>
2073
+ <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Insight</th>
2074
+ <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#2E7D32;">Recommendation</th>
2075
+ <th style="padding:12px; text-align:left; border:1px solid #ccc; font-weight:600; color:#0D47A1;">Risk Mitigation Strategy</th>
2076
+ </tr>
2077
+ </thead>
2078
+ <tbody>
2079
+ {"".join(rows)}
2080
+ </tbody>
2081
+ </table>
2082
  </div>
2083
+ """
2084
+ st.markdown(table_html, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2085
  else:
2086
+ st.info("ℹ️ No actionable insights generated. Ensure required columns exist.")