Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -334,9 +334,8 @@ with st.container():
|
|
| 334 |
with col2:
|
| 335 |
st.markdown("""
|
| 336 |
<div class="header-title">
|
| 337 |
-
<h1 style="margin-bottom:6px;">
|
| 338 |
<p style="color:#546e7a; font-size:1.05em; margin-top:-8px;">
|
| 339 |
-
Operational Risk Intelligence for Audit & Compliance
|
| 340 |
</p>
|
| 341 |
</div>
|
| 342 |
""", unsafe_allow_html=True)
|
|
@@ -1264,7 +1263,6 @@ except Exception as e:
|
|
| 1264 |
# ✅ Panel 1: ONLY Coverage < 90% AND Slope < 0
|
| 1265 |
# ✅ Panel 3: ONLY Coverage = 100% AND Slope > 0 → Avg/Month
|
| 1266 |
# ✅ Estetik: Sortable, Hover, Zebra, PLN Blue, No Emoticons
|
| 1267 |
-
|
| 1268 |
import streamlit as st
|
| 1269 |
import plotly.graph_objects as go
|
| 1270 |
import numpy as np
|
|
@@ -1408,7 +1406,7 @@ function makeSortable(tableId) {
|
|
| 1408 |
});
|
| 1409 |
}
|
| 1410 |
setTimeout(() => {
|
| 1411 |
-
makeSortable('tbl-
|
| 1412 |
makeSortable('tbl-issues');
|
| 1413 |
}, 800);
|
| 1414 |
</script>
|
|
@@ -1431,9 +1429,12 @@ def ascii_sparkline_pln(data):
|
|
| 1431 |
except:
|
| 1432 |
return "<span class='spark' style='color:#999;'>▁▁▁</span>"
|
| 1433 |
|
| 1434 |
-
# ——————— 1.
|
| 1435 |
-
def
|
| 1436 |
-
|
|
|
|
|
|
|
|
|
|
| 1437 |
return pd.DataFrame()
|
| 1438 |
|
| 1439 |
start_month = df['created_at'].min().to_period('M')
|
|
@@ -1441,17 +1442,17 @@ def predict_locations(df):
|
|
| 1441 |
all_months = pd.period_range(start=start_month, end=end_month, freq='M')
|
| 1442 |
|
| 1443 |
df_monthly = (
|
| 1444 |
-
df.groupby(['
|
| 1445 |
.size()
|
| 1446 |
.unstack(fill_value=0)
|
| 1447 |
.reindex(columns=all_months, fill_value=0)
|
| 1448 |
.stack()
|
| 1449 |
.reset_index(name='count')
|
| 1450 |
)
|
| 1451 |
-
df_monthly.columns = ['
|
| 1452 |
|
| 1453 |
results = []
|
| 1454 |
-
for
|
| 1455 |
ts = group.set_index('Month')['Count']
|
| 1456 |
total = len(all_months)
|
| 1457 |
active = (ts > 0).sum()
|
|
@@ -1465,7 +1466,7 @@ def predict_locations(df):
|
|
| 1465 |
if slope < 0 and coverage < 0.9:
|
| 1466 |
reason = f"Slope = {slope:.3f}, Coverage = {coverage*100:.1f}%. Avg: {avg_rate:.2f}/mo."
|
| 1467 |
results.append({
|
| 1468 |
-
'
|
| 1469 |
'Avg Reports/Month': round(avg_rate, 2),
|
| 1470 |
'Coverage (%)': round(coverage * 100, 1),
|
| 1471 |
'Trend Slope': round(slope, 3),
|
|
@@ -1535,21 +1536,21 @@ def predict_issues(df):
|
|
| 1535 |
return df_res.reset_index(drop=True)
|
| 1536 |
|
| 1537 |
# ——————— RUN ———————
|
| 1538 |
-
|
| 1539 |
df_issue = predict_issues(df_filtered)
|
| 1540 |
|
| 1541 |
-
# 🎯 PANEL 1:
|
| 1542 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1543 |
-
st.markdown("<div class='predictive-header'>1. Which
|
| 1544 |
-
if not
|
| 1545 |
-
cols = ['
|
| 1546 |
|
| 1547 |
# 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1548 |
-
df_display =
|
| 1549 |
"Reason": "Reason Forecast"
|
| 1550 |
})
|
| 1551 |
|
| 1552 |
-
html = df_display.to_html(escape=False, index=False, table_id="tbl-
|
| 1553 |
st.markdown(f"<div class='predictive-table-wrapper'>{html}</div>", unsafe_allow_html=True)
|
| 1554 |
|
| 1555 |
# st.markdown(
|
|
@@ -1564,65 +1565,20 @@ if not df_loc.empty:
|
|
| 1564 |
# st.markdown(
|
| 1565 |
# "<div class='predictive-table-wrapper'>"
|
| 1566 |
# "<p style='text-align:center; color:#666; padding:24px; font-style:italic;'>"
|
| 1567 |
-
# "No
|
| 1568 |
# "</p>"
|
| 1569 |
# "<div class='warning-box'>"
|
| 1570 |
-
# "💡 Note:
|
| 1571 |
# "</div>"
|
| 1572 |
# "</div>",
|
| 1573 |
# unsafe_allow_html=True
|
| 1574 |
# )
|
| 1575 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 1576 |
|
| 1577 |
-
# 🎯 PANEL 3: Issues (FILTERED: Coverage=100% & Rising) — Hanya Non-Positive
|
| 1578 |
-
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1579 |
-
st.markdown(
|
| 1580 |
-
"<div class='predictive-header'>"
|
| 1581 |
-
"3. Which Issue Categories Are Likely to Appear in the Next 3 Months (Non-Positive Only)"
|
| 1582 |
-
"<span style='font-size:0.75em; font-weight:400; color:#003DA5;'>"
|
| 1583 |
-
" (* Categorization uses NLP — Natural Language Processing from random text)"
|
| 1584 |
-
"</span>"
|
| 1585 |
-
"</div>",
|
| 1586 |
-
unsafe_allow_html=True
|
| 1587 |
-
)
|
| 1588 |
-
|
| 1589 |
-
if not df_issue.empty:
|
| 1590 |
-
cols = ['Category', 'Avg/Month', 'Coverage (%)', 'Trend Slope', 'Status', 'Trend']
|
| 1591 |
-
|
| 1592 |
-
# 🔵 Rename ONLY for display
|
| 1593 |
-
df_display = df_issue[cols].rename(columns={
|
| 1594 |
-
"Status": "Status Issue for Next Month"
|
| 1595 |
-
})
|
| 1596 |
-
|
| 1597 |
-
html = df_display.to_html(escape=False, index=False, table_id="tbl-issues")
|
| 1598 |
-
st.markdown(f"<div class='predictive-table-wrapper'>{html}</div>", unsafe_allow_html=True)
|
| 1599 |
-
|
| 1600 |
-
# st.markdown(
|
| 1601 |
-
# "<div class='predictive-note'>"
|
| 1602 |
-
# "<strong>Filtered:</strong> Reported every month (100% coverage) with increasing trend. "
|
| 1603 |
-
# "<strong>Avg/Month</strong> = total ÷ months. "
|
| 1604 |
-
# "<span class='trend-rising'>High-Risk Rising</span> = slope > 0.2."
|
| 1605 |
-
# "</div>",
|
| 1606 |
-
# unsafe_allow_html=True
|
| 1607 |
-
# )
|
| 1608 |
-
|
| 1609 |
-
# else:
|
| 1610 |
-
# st.markdown(
|
| 1611 |
-
# "<div class='predictive-table-wrapper'>"
|
| 1612 |
-
# "<p style='text-align:center; color:#c62828; padding:24px; font-weight:500;'>"
|
| 1613 |
-
# "⚠️ No rising categories with 100% monthly coverage."
|
| 1614 |
-
# "</p>"
|
| 1615 |
-
# "<p style='text-align:center; color:#666; font-size:0.9em;'>"
|
| 1616 |
-
# "Consider relaxing coverage filter if data is sparse."
|
| 1617 |
-
# "</p></div>",
|
| 1618 |
-
# unsafe_allow_html=True
|
| 1619 |
-
# )
|
| 1620 |
-
|
| 1621 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
| 1622 |
|
| 1623 |
|
| 1624 |
# =================== WHITEBOARD STYLE CHART FOR PANEL 3 ===================
|
| 1625 |
-
st.markdown("<h4 style='text-align: center; color: #2c3e50;'>
|
| 1626 |
|
| 1627 |
# Buat chart scatter dengan gaya whiteboard
|
| 1628 |
if not df_issue.empty:
|
|
@@ -1726,6 +1682,7 @@ if not df_issue.empty:
|
|
| 1726 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1727 |
else:
|
| 1728 |
st.info("No data available for non-positive issues with 100% coverage and positive trend.")
|
|
|
|
| 1729 |
# =================== 6. ✅ AI INSIGHT ENGINE (BARU - BERDASARKAN DATA & RATIO) ===================
|
| 1730 |
|
| 1731 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|
|
|
|
| 334 |
with col2:
|
| 335 |
st.markdown("""
|
| 336 |
<div class="header-title">
|
| 337 |
+
<h1 style="margin-bottom:6px;">Proactive Safety Intelligence & Analytics Dashboard</h1>
|
| 338 |
<p style="color:#546e7a; font-size:1.05em; margin-top:-8px;">
|
|
|
|
| 339 |
</p>
|
| 340 |
</div>
|
| 341 |
""", unsafe_allow_html=True)
|
|
|
|
| 1263 |
# ✅ Panel 1: ONLY Coverage < 90% AND Slope < 0
|
| 1264 |
# ✅ Panel 3: ONLY Coverage = 100% AND Slope > 0 → Avg/Month
|
| 1265 |
# ✅ Estetik: Sortable, Hover, Zebra, PLN Blue, No Emoticons
|
|
|
|
| 1266 |
import streamlit as st
|
| 1267 |
import plotly.graph_objects as go
|
| 1268 |
import numpy as np
|
|
|
|
| 1406 |
});
|
| 1407 |
}
|
| 1408 |
setTimeout(() => {
|
| 1409 |
+
makeSortable('tbl-creators');
|
| 1410 |
makeSortable('tbl-issues');
|
| 1411 |
}, 800);
|
| 1412 |
</script>
|
|
|
|
| 1429 |
except:
|
| 1430 |
return "<span class='spark' style='color:#999;'>▁▁▁</span>"
|
| 1431 |
|
| 1432 |
+
# ——————— 1. Creators: ONLY Coverage < 90% AND Slope < 0 (Non-Positive Only) ———————
|
| 1433 |
+
def predict_creators(df):
|
| 1434 |
+
# 🔥 Filter: Hanya yang bukan 'Positive'
|
| 1435 |
+
df = df[df['temuan_kategori'] != 'Positive'].copy() # ✅ Filter non-Positive
|
| 1436 |
+
|
| 1437 |
+
if 'creator_name' not in df.columns or df.empty:
|
| 1438 |
return pd.DataFrame()
|
| 1439 |
|
| 1440 |
start_month = df['created_at'].min().to_period('M')
|
|
|
|
| 1442 |
all_months = pd.period_range(start=start_month, end=end_month, freq='M')
|
| 1443 |
|
| 1444 |
df_monthly = (
|
| 1445 |
+
df.groupby(['creator_name', df['created_at'].dt.to_period('M')])
|
| 1446 |
.size()
|
| 1447 |
.unstack(fill_value=0)
|
| 1448 |
.reindex(columns=all_months, fill_value=0)
|
| 1449 |
.stack()
|
| 1450 |
.reset_index(name='count')
|
| 1451 |
)
|
| 1452 |
+
df_monthly.columns = ['Creator', 'Month', 'Count']
|
| 1453 |
|
| 1454 |
results = []
|
| 1455 |
+
for creator, group in df_monthly.groupby('Creator'):
|
| 1456 |
ts = group.set_index('Month')['Count']
|
| 1457 |
total = len(all_months)
|
| 1458 |
active = (ts > 0).sum()
|
|
|
|
| 1466 |
if slope < 0 and coverage < 0.9:
|
| 1467 |
reason = f"Slope = {slope:.3f}, Coverage = {coverage*100:.1f}%. Avg: {avg_rate:.2f}/mo."
|
| 1468 |
results.append({
|
| 1469 |
+
'Creator': creator,
|
| 1470 |
'Avg Reports/Month': round(avg_rate, 2),
|
| 1471 |
'Coverage (%)': round(coverage * 100, 1),
|
| 1472 |
'Trend Slope': round(slope, 3),
|
|
|
|
| 1536 |
return df_res.reset_index(drop=True)
|
| 1537 |
|
| 1538 |
# ——————— RUN ———————
|
| 1539 |
+
df_creator = predict_creators(df_filtered)
|
| 1540 |
df_issue = predict_issues(df_filtered)
|
| 1541 |
|
| 1542 |
+
# 🎯 PANEL 1: Creators (FILTERED: Coverage < 90% & Slope < 0) — Non-Positive Only
|
| 1543 |
st.markdown("<div class='predictive-panel'>", unsafe_allow_html=True)
|
| 1544 |
+
st.markdown("<div class='predictive-header'>1. Which Reporters Are Predicted to Have No Future Inspections? (Non-Positive Only)</div>", unsafe_allow_html=True)
|
| 1545 |
+
if not df_creator.empty:
|
| 1546 |
+
cols = ['Creator', 'Avg Reports/Month', 'Coverage (%)', 'Trend Slope', 'Trend', 'Reason']
|
| 1547 |
|
| 1548 |
# 🔥 Rename hanya untuk DISPLAY, bukan data asli
|
| 1549 |
+
df_display = df_creator[cols].rename(columns={
|
| 1550 |
"Reason": "Reason Forecast"
|
| 1551 |
})
|
| 1552 |
|
| 1553 |
+
html = df_display.to_html(escape=False, index=False, table_id="tbl-creators")
|
| 1554 |
st.markdown(f"<div class='predictive-table-wrapper'>{html}</div>", unsafe_allow_html=True)
|
| 1555 |
|
| 1556 |
# st.markdown(
|
|
|
|
| 1565 |
# st.markdown(
|
| 1566 |
# "<div class='predictive-table-wrapper'>"
|
| 1567 |
# "<p style='text-align:center; color:#666; padding:24px; font-style:italic;'>"
|
| 1568 |
+
# "No creators meet criteria: Coverage < 90% and negative trend."
|
| 1569 |
# "</p>"
|
| 1570 |
# "<div class='warning-box'>"
|
| 1571 |
+
# "💡 Note: Creators with Coverage ≥ 90% are excluded — they are considered stable reporters."
|
| 1572 |
# "</div>"
|
| 1573 |
# "</div>",
|
| 1574 |
# unsafe_allow_html=True
|
| 1575 |
# )
|
| 1576 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 1577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1578 |
|
| 1579 |
|
| 1580 |
# =================== WHITEBOARD STYLE CHART FOR PANEL 3 ===================
|
| 1581 |
+
st.markdown("<h4 style='text-align: center; color: #2c3e50;'>2. Which Issue Categories Are Likely to Appear in the Next 3 Months</h4>", unsafe_allow_html=True)
|
| 1582 |
|
| 1583 |
# Buat chart scatter dengan gaya whiteboard
|
| 1584 |
if not df_issue.empty:
|
|
|
|
| 1682 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1683 |
else:
|
| 1684 |
st.info("No data available for non-positive issues with 100% coverage and positive trend.")
|
| 1685 |
+
|
| 1686 |
# =================== 6. ✅ AI INSIGHT ENGINE (BARU - BERDASARKAN DATA & RATIO) ===================
|
| 1687 |
|
| 1688 |
st.markdown("<h3 class='section-title'>OBJECTIVE 7 - Insight and Recommendation</h3>", unsafe_allow_html=True)
|