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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -27
app.py CHANGED
@@ -1985,63 +1985,205 @@ 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
  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:
@@ -2083,4 +2225,4 @@ if entries:
2083
  """
2084
  st.markdown(table_html, unsafe_allow_html=True)
2085
  else:
2086
- st.info("ℹ️ No actionable insights generated. Ensure required columns exist.")
 
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
+
1989
+ # =================== OBJECTIVE 7 — Insight and Recommendation ===================
1990
  st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
1991
 
1992
+ # Pastikan df_filtered tersedia
1993
+ if 'df_filtered' not in st.session_state:
1994
+ st.error("⚠️ `df_filtered` not found in session state. Please ensure filters are applied.")
1995
+ st.stop()
1996
+ df_filtered = st.session_state.df_filtered
1997
+
1998
+ # ✅ Definisi fungsi — dipastikan di global scope
1999
+ def extract_agentic_insights_v5(df: pd.DataFrame):
2000
+ dev = {
2001
+ "lowest_ratio_9_locs": [],
2002
+ "obj3a_lowest_div": None,
2003
+ "obj3b_slowest_executor": None,
2004
+ "obj3c_lowest_reporter": None,
2005
+ "obj3d_slowest_div": None,
2006
+ "obj4_unsafe_condition_pct": 0.0,
2007
+ "obj4_unsafe_action_pct": 0.0,
2008
+ "obj4_near_miss_pct": 0.0,
2009
+ "obj5_q1_divs": [],
2010
+ "obj5_q2_divs": [],
2011
+ "obj6_top2_categories": [],
2012
+ }
2013
+
2014
+ # === 1. 9 locations with lowest finding-to-reporter ratio ===
2015
+ if {'nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2016
+ calc = df[['nama_lokasi_full', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2017
+ calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2018
+ calc = calc.dropna(subset=['created_at', 'nama_lokasi_full', 'creator_nid'])
2019
+ calc['bulan'] = calc['created_at'].dt.to_period('M')
2020
+ monthly = calc.groupby(['nama_lokasi_full', 'bulan']).agg(
2021
+ findings=('kode_temuan', 'size'),
2022
+ reporters=('creator_nid', 'nunique')
2023
+ ).reset_index()
2024
+ monthly = monthly[monthly['reporters'] > 0]
2025
+ monthly['ratio'] = monthly['findings'] / monthly['reporters']
2026
+ loc_avg = monthly.groupby('nama_lokasi_full')['ratio'].mean()
2027
+ lowest_9 = loc_avg.nsmallest(9)
2028
+ dev["lowest_ratio_9_locs"] = [(loc, round(ratio, 2)) for loc, ratio in lowest_9.items()]
2029
+
2030
+ # === 2a: Division — lowest ratio ===
2031
+ if {'nama', 'creator_nid', 'created_at', 'kode_temuan'}.issubset(df.columns):
2032
+ calc = df[['nama', 'creator_nid', 'created_at', 'kode_temuan']].copy()
2033
+ calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2034
+ agg = calc.groupby(['nama', 'bulan']).agg(
2035
+ findings=('kode_temuan', 'size'),
2036
+ reporters=('creator_nid', 'nunique')
2037
+ )
2038
+ agg = agg[agg['reporters'] > 0].reset_index()
2039
+ agg['ratio'] = agg['findings'] / agg['reporters']
2040
+ div_ratio = agg.groupby('nama')['ratio'].mean()
2041
+ if not div_ratio.empty:
2042
+ name = div_ratio.idxmin()
2043
+ val = round(div_ratio.min(), 2)
2044
+ dev["obj3a_lowest_div"] = (name, val)
2045
+
2046
+ # === 2b: Executor — slowest resolution ===
2047
+ if 'days_to_close' in df.columns:
2048
+ valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2049
+ exec_col = 'nama_pic' if 'nama_pic' in valid.columns else 'creator_name'
2050
+ if exec_col in valid.columns:
2051
+ lead = valid.groupby(exec_col)['days_to_close'].mean()
2052
+ if not lead.empty:
2053
+ name = lead.idxmax()
2054
+ val = round(lead.max(), 2)
2055
+ dev["obj3b_slowest_executor"] = (name, val)
2056
+
2057
+ # === 2c: Reporter — lowest frequency ===
2058
+ if {'creator_name', 'created_at'}.issubset(df.columns):
2059
+ calc = df[['creator_name', 'created_at']].copy()
2060
+ calc['bulan'] = pd.to_datetime(calc['created_at']).dt.to_period('M')
2061
+ monthly = calc.groupby(['creator_name', 'bulan']).size().reset_index(name='count')
2062
+ avg = monthly.groupby('creator_name')['count'].mean()
2063
+ avg = avg[avg > 0]
2064
+ if not avg.empty:
2065
+ name = avg.idxmin()
2066
+ val = round(avg.min(), 2)
2067
+ dev["obj3c_lowest_reporter"] = (name, val)
2068
+
2069
+ # === 2d: Division — slowest resolution ===
2070
+ if 'days_to_close' in df.columns and 'nama' in df.columns:
2071
+ valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
2072
+ if not valid.empty:
2073
+ lead = valid.groupby('nama')['days_to_close'].mean()
2074
+ if not lead.empty:
2075
+ name = lead.idxmax()
2076
+ val = round(lead.max(), 2)
2077
+ dev["obj3d_slowest_div"] = (name, val)
2078
+
2079
+ # === 3. Non-Positive composition ===
2080
+ if 'temuan_kategori' in df.columns:
2081
+ cnt = df['temuan_kategori'].value_counts(normalize=True) * 100
2082
+ dev["obj4_unsafe_condition_pct"] = round(cnt.get("Unsafe Condition", 0), 2)
2083
+ dev["obj4_unsafe_action_pct"] = round(cnt.get("Unsafe Action", 0), 2)
2084
+ dev["obj4_near_miss_pct"] = round(cnt.get("Near Miss", 0), 2)
2085
+
2086
+ # === 4. Risk Quadrants ===
2087
+ X_LIMIT, Y_LIMIT = 20, 3
2088
+ if {'nama', 'created_at', 'days_to_close', 'kode_temuan'}.issubset(df.columns):
2089
+ calc = df.copy()
2090
+ calc['created_at'] = pd.to_datetime(calc['created_at'], errors='coerce')
2091
+ calc = calc.assign(month=calc['created_at'].dt.to_period('M').astype(str))
2092
+ monthly_counts = calc.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
2093
+ avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
2094
+ leadtime = calc.groupby('nama')['days_to_close'].mean().reset_index(name='Avg Lead Time')
2095
+ mat = avg_count.merge(leadtime, on='nama', how='left').fillna(0)
2096
+ for _, r in mat.iterrows():
2097
+ if r['Finding Count'] >= X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2098
+ dev["obj5_q1_divs"].append(r['nama'])
2099
+ elif r['Finding Count'] < X_LIMIT and r['Avg Lead Time'] >= Y_LIMIT:
2100
+ dev["obj5_q2_divs"].append(r['nama'])
2101
+
2102
+ # === 5. Top 2 non-Positive categories ===
2103
+ if {'kategori', 'temuan_kategori', 'created_at'}.issubset(df.columns):
2104
+ nonpos = df[df['temuan_kategori'] != 'Positive']
2105
+ if not nonpos.empty:
2106
+ start = nonpos['created_at'].min().to_period('M')
2107
+ end = nonpos['created_at'].max().to_period('M')
2108
+ n_months = len(pd.period_range(start=start, end=end, freq='M'))
2109
+ cat_avg = (nonpos.groupby('kategori').size() / n_months).sort_values(ascending=False).head(2)
2110
+ dev["obj6_top2_categories"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
2111
+
2112
+ return dev
2113
+
2114
+ # === Jalankan ekstraksi ===
2115
  dev = extract_agentic_insights_v5(df_filtered)
2116
 
2117
+ # === Siapkan entri tabel ===
2118
  entries = []
2119
 
2120
  # 1. Low-ratio locations
2121
  if dev["lowest_ratio_9_locs"]:
2122
+ loc_list = ", ".join([f"{loc} ({ratio:.2f})" for loc, ratio in dev["lowest_ratio_9_locs"]])
2123
+ entries.append({
2124
+ "Risk Category": "Reporting Coverage Risk",
2125
+ "Insight": f"Nine locations with the lowest finding-to-reporter ratio: {loc_list}.",
2126
+ "Recommendation": "Launch <em>Agency Activation Sprint</em>: assign Safety Champions to conduct ≥1 spot inspection/week per site.",
2127
+ "Mitigation": "Deploy QR-code checklists + automated WhatsApp reminders. Target: ratio ≥0.5 within 45 days."
2128
+ })
2129
 
2130
  # 2. Capacity imbalance
2131
  parts = []
2132
  if dev["obj3a_lowest_div"]:
2133
+ name, val = dev["obj3a_lowest_div"]
2134
+ parts.append(f"division <strong>{name}</strong> (ratio: {val:.2f})")
2135
  if dev["obj3c_lowest_reporter"]:
2136
+ name, val = dev["obj3c_lowest_reporter"]
2137
+ parts.append(f"reporter <strong>{name}</strong> ({val:.2f} findings/month)")
2138
  if dev["obj3d_slowest_div"]:
2139
+ name, val = dev["obj3d_slowest_div"]
2140
+ parts.append(f"division <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2141
  if dev["obj3b_slowest_executor"]:
2142
+ name, val = dev["obj3b_slowest_executor"]
2143
+ parts.append(f"executor <strong>{name}</strong> (avg. resolution: {val:.2f} days)")
2144
 
2145
  if parts:
2146
+ insight = f"Uneven operational capacity detected: {'; '.join(parts)}."
2147
+ entries.append({
2148
+ "Risk Category": "Capacity Imbalance Risk",
2149
+ "Insight": insight,
2150
+ "Recommendation": "Activate <em>Agentic Capacity Dashboard</em> for real-time monitoring of reporting & resolution KPIs.",
2151
+ "Mitigation": "Auto-trigger coaching alerts to Area PICs if deviation >20% from baseline, with peer benchmarking."
2152
+ })
2153
 
2154
  # 3. Non-Positive composition
2155
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2156
  if uc + ua + nm > 0:
2157
  insight = f"Non-Positive finding composition: Unsafe Condition ({uc:.2f}%), Unsafe Action ({ua:.2f}%), Near Miss ({nm:.2f}%)."
2158
+ entries.append({
2159
+ "Risk Category": "Data Quality & Categorization Risk",
2160
+ "Insight": insight,
2161
+ "Recommendation": "Enforce photo-based validation for all Unsafe Condition/Action/Near Miss submissions.",
2162
+ "Mitigation": "System blocks submission if photo evidence or justification is missing."
2163
+ })
2164
 
2165
  # 4. Risk Quadrants
2166
  if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
2167
+ q1 = ", ".join([f"{d}" for d in dev["obj5_q1_divs"][:3]]) or "—"
2168
+ q2 = ", ".join([f"{d}" for d in dev["obj5_q2_divs"][:3]]) or "—"
2169
  insight = f"High-risk divisions (Q1): {q1}; Hidden-risk divisions (Q2): {q2}."
2170
+ entries.append({
2171
+ "Risk Category": "SLA & Backlog Risk",
2172
+ "Insight": insight,
2173
+ "Recommendation": "Assign dedicated safety crews to QI divisions; enforce <em>One Finding, One Day</em> closure for QII.",
2174
+ "Mitigation": "Auto-generate executive escalation reports to VP Ops if any division remains in QI/QII ≥2 months."
2175
+ })
2176
 
2177
  # 5. Top categories
2178
  if dev["obj6_top2_categories"]:
2179
  c1, c2 = dev["obj6_top2_categories"]
2180
  insight = f"Top recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]:.2f}/month) and <strong>{c2[0]}</strong> ({c2[1]:.2f}/month)."
2181
+ entries.append({
2182
+ "Risk Category": "Recurring Hazard Risk",
2183
+ "Insight": insight,
2184
+ "Recommendation": f"Form cross-functional <em>RCA Task Force</em> (Civil, Electrical, HSE, Contractors) for <strong>{c1[0]}</strong> and <strong>{c2[0]}</strong>.",
2185
+ "Mitigation": "Update tender templates: all bids must include historical mitigations for these categories."
2186
+ })
2187
 
2188
  # === RENDER TABEL TERPADU ===
2189
  if entries:
 
2225
  """
2226
  st.markdown(table_html, unsafe_allow_html=True)
2227
  else:
2228
+ st.info("ℹ️ No actionable insights generated. Ensure required columns exist.")