Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1982,6 +1982,7 @@ 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 |
|
| 1986 |
|
| 1987 |
def extract_critical_deviations(df: pd.DataFrame):
|
|
@@ -2010,7 +2011,6 @@ def extract_critical_deviations(df: pd.DataFrame):
|
|
| 2010 |
monthly_agg = monthly_agg[monthly_agg['reporters'] > 0]
|
| 2011 |
monthly_agg['ratio'] = monthly_agg['findings'] / monthly_agg['reporters']
|
| 2012 |
loc_avg = monthly_agg.groupby('nama_lokasi_full')['ratio'].mean().reset_index()
|
| 2013 |
-
# Ambil yang 0.95 ≤ ratio ≤ 1.05
|
| 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 |
|
|
@@ -2066,14 +2066,12 @@ def extract_critical_deviations(df: pd.DataFrame):
|
|
| 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 ===
|
| 2070 |
-
# Gunakan logika yang sama seperti Objective 5 (X_LIMIT=20, Y_LIMIT=3)
|
| 2071 |
X_LIMIT, Y_LIMIT = 20, 3
|
| 2072 |
if 'nama' in df.columns and 'days_to_close' in df.columns:
|
| 2073 |
df_risk = df.copy()
|
| 2074 |
df_risk['created_at'] = pd.to_datetime(df_risk['created_at'], errors='coerce')
|
| 2075 |
df_risk = df_risk.assign(month=df_risk['created_at'].dt.to_period('M').astype(str))
|
| 2076 |
-
# Avg bulanan per divisi
|
| 2077 |
monthly_counts = df_risk.groupby(['nama', 'month'])['kode_temuan'].nunique().reset_index()
|
| 2078 |
avg_count = monthly_counts.groupby('nama')['kode_temuan'].mean().reset_index(name='Finding Count')
|
| 2079 |
leadtime = df_risk.groupby('nama')['days_to_close'].mean().reset_index(name='Average Lead Time')
|
|
@@ -2089,36 +2087,33 @@ def extract_critical_deviations(df: pd.DataFrame):
|
|
| 2089 |
elif cnt < X_LIMIT and lt >= Y_LIMIT:
|
| 2090 |
dev["obj5_quadrant_II"].append(div)
|
| 2091 |
|
| 2092 |
-
# === OBJ 6: Whiteboard — 2 bubble terbesar (Avg/Month
|
| 2093 |
if 'kategori' in df.columns and 'temuan_kategori' in df.columns:
|
| 2094 |
-
df_nonpos = df[df['temuan_kategori'] != 'Positive']
|
| 2095 |
if not df_nonpos.empty:
|
| 2096 |
-
|
| 2097 |
-
|
| 2098 |
-
n_months = len(pd.period_range(start=
|
| 2099 |
-
cat_avg = (
|
| 2100 |
-
|
| 2101 |
-
).sort_values(ascending=False).head(2)
|
| 2102 |
-
dev["obj6_top2_bubbles"] = [(cat, round(val, 2)) for cat, val in cat_avg.items()]
|
| 2103 |
|
| 2104 |
return dev
|
| 2105 |
|
| 2106 |
-
# Jalankan ekstraksi
|
| 2107 |
deviations = extract_critical_deviations(df_filtered)
|
| 2108 |
|
| 2109 |
-
# Bangun
|
| 2110 |
insight_parts = []
|
| 2111 |
-
rec_parts = []
|
| 2112 |
|
| 2113 |
-
#
|
| 2114 |
if deviations["obj2_locations_ratio_1"]:
|
| 2115 |
-
locs = ", ".join(deviations["obj2_locations_ratio_1"][:5])
|
| 2116 |
insight_parts.append(
|
| 2117 |
f"Nine locations show near-optimal finding-to-reporter ratio (~1.0), indicating balanced workload: "
|
| 2118 |
f"{locs}, and others."
|
| 2119 |
)
|
| 2120 |
|
| 2121 |
-
#
|
| 2122 |
if deviations["obj3a_lowest_div_ratio"]:
|
| 2123 |
div, ratio = deviations["obj3a_lowest_div_ratio"]
|
| 2124 |
insight_parts.append(f"Division {div} has the lowest reporting ratio ({ratio}), suggesting potential under-utilization or resource gaps.")
|
|
@@ -2132,13 +2127,13 @@ if deviations["obj3d_slowest_executor"]:
|
|
| 2132 |
name, lt = deviations["obj3d_slowest_executor"]
|
| 2133 |
insight_parts.append(f"Executor {name} has the longest lead time ({lt} days), requiring workflow review.")
|
| 2134 |
|
| 2135 |
-
#
|
| 2136 |
if deviations["obj4_unsafe_share"]:
|
| 2137 |
unsafe_list = [f"{cat} ({pct}%)" for cat, pct in deviations["obj4_unsafe_share"].items()]
|
| 2138 |
unsafe_str = "; ".join(unsafe_list)
|
| 2139 |
insight_parts.append(f"Unsafe issues dominate: {unsafe_str} of all findings.")
|
| 2140 |
|
| 2141 |
-
#
|
| 2142 |
if deviations["obj5_quadrant_I"]:
|
| 2143 |
q1 = ", ".join(deviations["obj5_quadrant_I"][:3])
|
| 2144 |
insight_parts.append(f"High-risk divisions (high volume + slow resolution): {q1}.")
|
|
@@ -2146,7 +2141,7 @@ if deviations["obj5_quadrant_II"]:
|
|
| 2146 |
q2 = ", ".join(deviations["obj5_quadrant_II"][:3])
|
| 2147 |
insight_parts.append(f"Hidden-risk divisions (low volume but very slow): {q2} — may indicate capacity or priority issues.")
|
| 2148 |
|
| 2149 |
-
#
|
| 2150 |
if deviations["obj6_top2_bubbles"]:
|
| 2151 |
bub1, bub2 = deviations["obj6_top2_bubbles"]
|
| 2152 |
insight_parts.append(
|
|
@@ -2154,19 +2149,14 @@ if deviations["obj6_top2_bubbles"]:
|
|
| 2154 |
f"and {bub2[0]} ({bub2[1]}/month), indicating systemic root causes."
|
| 2155 |
)
|
| 2156 |
|
| 2157 |
-
# Combine insight
|
| 2158 |
insight_text = " ".join(insight_parts) if insight_parts else "No significant deviations detected based on current filters."
|
| 2159 |
|
| 2160 |
-
# Rekomendasi & Risk Mitigation
|
| 2161 |
-
rec_parts
|
| 2162 |
-
"Prioritize capacity assessment and coaching for divisions and individuals with lowest activity or longest resolution times."
|
| 2163 |
-
|
| 2164 |
-
rec_parts.append(
|
| 2165 |
-
"Initiate root-cause analysis on top two high-frequency unsafe categories to prevent recurrence."
|
| 2166 |
-
)
|
| 2167 |
-
rec_parts.append(
|
| 2168 |
"Review workload distribution for locations with ratio ≈1.0 — they represent a benchmark for sustainable inspection load."
|
| 2169 |
-
|
| 2170 |
|
| 2171 |
mitigation_parts = [
|
| 2172 |
"Establish SLA thresholds: max 7 days lead time, min 0.5 findings/reporter/month for active status.",
|
|
@@ -2177,7 +2167,7 @@ mitigation_parts = [
|
|
| 2177 |
recommendation_text = " ".join(rec_parts)
|
| 2178 |
mitigation_text = " ".join(mitigation_parts)
|
| 2179 |
|
| 2180 |
-
# Tampilkan —
|
| 2181 |
st.markdown(
|
| 2182 |
f"""
|
| 2183 |
<div class="card" style="
|
|
|
|
| 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):
|
|
|
|
| 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 |
|
|
|
|
| 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')
|
|
|
|
| 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.")
|
|
|
|
| 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}.")
|
|
|
|
| 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(
|
|
|
|
| 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.",
|
|
|
|
| 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="
|