Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1999,7 +1999,7 @@ def extract_agentic_insights_v5(df: pd.DataFrame):
|
|
| 1999 |
"obj6_top2_categories": [],
|
| 2000 |
}
|
| 2001 |
|
| 2002 |
-
# === 1. 9
|
| 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:
|
| 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:
|
| 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:
|
| 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:
|
| 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.
|
| 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.
|
| 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
|
| 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
|
| 2105 |
insight_lines = []
|
| 2106 |
|
| 2107 |
-
# 1. 9
|
| 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.
|
| 2111 |
|
| 2112 |
-
# 2.
|
| 2113 |
parts = []
|
| 2114 |
if dev["obj3a_lowest_div"]:
|
| 2115 |
-
parts.append(f"
|
| 2116 |
if dev["obj3c_lowest_reporter"]:
|
| 2117 |
-
parts.append(f"reporter <strong>{dev['obj3c_lowest_reporter'][0]}</strong> ({dev['obj3c_lowest_reporter'][1]}/
|
| 2118 |
if dev["obj3d_slowest_div"]:
|
| 2119 |
-
parts.append(f"
|
| 2120 |
if dev["obj3b_slowest_executor"]:
|
| 2121 |
-
parts.append(f"
|
| 2122 |
|
| 2123 |
if parts:
|
| 2124 |
joined = "; ".join(parts)
|
| 2125 |
insight_lines.append(
|
| 2126 |
-
f"2.
|
| 2127 |
-
"
|
| 2128 |
)
|
| 2129 |
|
| 2130 |
-
# 3.
|
| 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.
|
| 2135 |
-
"Proporsi Near Miss masih rendah — indikasi <em>under-reporting of close calls</em>."
|
| 2136 |
)
|
| 2137 |
|
| 2138 |
-
# 4.
|
| 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"][:
|
| 2141 |
-
q2 = ", ".join([f"<strong>{d}</strong>" for d in dev["obj5_q2_divs"][:
|
| 2142 |
q1_str = q1 if q1 else "—"
|
| 2143 |
q2_str = q2 if q2 else "—"
|
| 2144 |
-
insight_lines.append(f"4.
|
| 2145 |
|
| 2146 |
-
# 5. Top
|
| 2147 |
if dev["obj6_top2_categories"]:
|
| 2148 |
c1, c2 = dev["obj6_top2_categories"]
|
| 2149 |
-
insight_lines.append(
|
|
|
|
|
|
|
| 2150 |
|
| 2151 |
insight_text = "<br>".join(insight_lines)
|
| 2152 |
|
| 2153 |
-
# ===
|
| 2154 |
recs = []
|
| 2155 |
|
| 2156 |
-
# 1
|
| 2157 |
if dev["lowest_ratio_9_locs"]:
|
| 2158 |
recs.append({
|
| 2159 |
"point": "1",
|
| 2160 |
-
"rec": "
|
| 2161 |
-
"mit": "
|
| 2162 |
})
|
| 2163 |
|
| 2164 |
-
# 2
|
| 2165 |
if parts:
|
| 2166 |
recs.append({
|
| 2167 |
"point": "2",
|
| 2168 |
-
"rec": "
|
| 2169 |
-
"mit": "
|
| 2170 |
})
|
| 2171 |
|
| 2172 |
-
# 3
|
| 2173 |
if uc + ua + nm > 0:
|
| 2174 |
recs.append({
|
| 2175 |
"point": "3",
|
| 2176 |
-
"rec": "
|
| 2177 |
-
"mit": "
|
| 2178 |
})
|
| 2179 |
|
| 2180 |
-
# 4
|
| 2181 |
if dev["obj5_q1_divs"] or dev["obj5_q2_divs"]:
|
| 2182 |
recs.append({
|
| 2183 |
"point": "4",
|
| 2184 |
-
"rec": "
|
| 2185 |
-
"mit": "
|
| 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"
|
| 2194 |
-
"mit": "
|
| 2195 |
})
|
| 2196 |
|
| 2197 |
-
# ===
|
| 2198 |
st.markdown(
|
| 2199 |
f"""
|
| 2200 |
<div class="card" style="
|
|
@@ -2212,7 +2213,7 @@ st.markdown(
|
|
| 2212 |
unsafe_allow_html=True
|
| 2213 |
)
|
| 2214 |
|
| 2215 |
-
#
|
| 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
|
| 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: Division — lowest 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: Executor — longest 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: Reporter — lowest 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: Division — longest 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>
|