Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,6 +10,7 @@ import os
|
|
| 10 |
import sklearn
|
| 11 |
import kaleido
|
| 12 |
|
|
|
|
| 13 |
# =================== PAGE CONFIG ===================
|
| 14 |
st.set_page_config(
|
| 15 |
page_title="PLN Audit Insight & Intelligence Dashboard",
|
|
@@ -325,27 +326,45 @@ with st.container():
|
|
| 325 |
</div>
|
| 326 |
""", unsafe_allow_html=True)
|
| 327 |
|
| 328 |
-
# RIGHT — LOGO BOX
|
| 329 |
-
with col3:
|
| 330 |
-
logo_path = "pln.png"
|
| 331 |
-
if os.path.exists(logo_path):
|
| 332 |
-
st.image(logo_path, width=200)
|
| 333 |
-
else:
|
| 334 |
-
st.error("Logo not found")
|
| 335 |
|
| 336 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
| 337 |
|
| 338 |
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
-
# Asumsikan df_filtered adalah data utama yang telah difilter
|
| 343 |
df_local = df_filtered.copy()
|
| 344 |
|
| 345 |
# Tambah kolom bulan
|
| 346 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 347 |
|
| 348 |
-
# --- Langsung buat Area_Type PG / UM tanpa filter manual ---
|
| 349 |
if 'temuan_kode_distrik' in df_local.columns:
|
| 350 |
df_local['Area_Type'] = df_local['temuan_kode_distrik'].apply(
|
| 351 |
lambda x: 'PG' if 'PG' in str(x).upper()
|
|
@@ -367,7 +386,6 @@ if 'temuan_kode_distrik' in df_local.columns:
|
|
| 367 |
creators_by_company_month = df_area.groupby(['created_month', 'nama_perusahaan'])['creator_nid'].nunique().reset_index(name='unique_creators')
|
| 368 |
# Gabung
|
| 369 |
merged = findings_by_company_month.merge(creators_by_company_month, on=['created_month', 'nama_perusahaan'], how='outer')
|
| 370 |
-
# Isi NaN dengan 0 untuk kolom yang mungkin hilang dari merge
|
| 371 |
merged = merged.fillna({'findings_count': 0, 'unique_creators': 0})
|
| 372 |
# Filter untuk menghindari pembagian dengan nol
|
| 373 |
merged = merged[merged['unique_creators'] > 0]
|
|
@@ -392,34 +410,102 @@ if 'temuan_kode_distrik' in df_local.columns:
|
|
| 392 |
avg_ratio_pg = calculate_avg_ratio_per_company(df_pg)
|
| 393 |
avg_ratio_um = calculate_avg_ratio_per_company(df_um)
|
| 394 |
|
| 395 |
-
#
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
if 'PLN' in str(company).upper():
|
| 402 |
-
|
| 403 |
else:
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
# Plot
|
| 409 |
col1, col2 = st.columns(2)
|
| 410 |
|
| 411 |
with col1:
|
| 412 |
st.markdown("<h5>Unit Pembangkit: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
fig_pg = px.pie(
|
| 416 |
-
avg_ratio_pg,
|
| 417 |
-
values='avg_monthly_ratio',
|
| 418 |
-
names='nama_perusahaan',
|
| 419 |
-
title='PG Area',
|
| 420 |
-
color='nama_perusahaan',
|
| 421 |
-
color_discrete_map=color_discrete_map_pg
|
| 422 |
-
)
|
| 423 |
st.plotly_chart(fig_pg, use_container_width=True)
|
| 424 |
|
| 425 |
# AI Insight untuk PG
|
|
@@ -443,16 +529,8 @@ if 'temuan_kode_distrik' in df_local.columns:
|
|
| 443 |
|
| 444 |
with col2:
|
| 445 |
st.markdown("<h5>Unit Maintenance: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
fig_um = px.pie(
|
| 449 |
-
avg_ratio_um,
|
| 450 |
-
values='avg_monthly_ratio',
|
| 451 |
-
names='nama_perusahaan',
|
| 452 |
-
title='UM Area',
|
| 453 |
-
color='nama_perusahaan',
|
| 454 |
-
color_discrete_map=color_discrete_map_um
|
| 455 |
-
)
|
| 456 |
st.plotly_chart(fig_um, use_container_width=True)
|
| 457 |
|
| 458 |
# AI Insight untuk UM
|
|
@@ -702,7 +780,7 @@ st.markdown(
|
|
| 702 |
margin-top: 1rem;
|
| 703 |
}
|
| 704 |
</style>
|
| 705 |
-
<h3 class='section-title'>OBJECTIVE
|
| 706 |
""",
|
| 707 |
unsafe_allow_html=True
|
| 708 |
)
|
|
@@ -791,11 +869,11 @@ avg_leadtime_per_executor = compute_executor_leadtime_by_pic(df_local)
|
|
| 791 |
col_3a, col_3c = st.columns(2)
|
| 792 |
|
| 793 |
with col_3a:
|
| 794 |
-
st.markdown("<h5>
|
| 795 |
if avg_ratio_per_nama.empty:
|
| 796 |
st.warning("No data for reporter analysis by division.")
|
| 797 |
else:
|
| 798 |
-
sort_option_3a = st.selectbox("Show
|
| 799 |
|
| 800 |
# Urutkan data dari tertinggi ke terendah
|
| 801 |
sorted_data_all = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False)
|
|
@@ -854,11 +932,11 @@ with col_3a:
|
|
| 854 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 855 |
|
| 856 |
with col_3c:
|
| 857 |
-
st.markdown("<h5>
|
| 858 |
if avg_rate_per_creator.empty:
|
| 859 |
st.warning("No data for reporter analysis by creator_name.")
|
| 860 |
else:
|
| 861 |
-
sort_option_3c = st.selectbox("Show
|
| 862 |
|
| 863 |
# Urutkan data dari tertinggi ke terendah
|
| 864 |
sorted_data_all = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False)
|
|
@@ -921,11 +999,11 @@ with col_3c:
|
|
| 921 |
col_3b, col_3d = st.columns(2)
|
| 922 |
|
| 923 |
with col_3b:
|
| 924 |
-
st.markdown("<h5>
|
| 925 |
if avg_leadtime_nama.empty:
|
| 926 |
st.warning("No data for executor analysis by division.")
|
| 927 |
else:
|
| 928 |
-
sort_option_3b = st.selectbox("Show
|
| 929 |
|
| 930 |
# Urutkan data dari tertinggi ke terendah
|
| 931 |
sorted_data_all = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=False)
|
|
@@ -982,11 +1060,11 @@ with col_3b:
|
|
| 982 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 983 |
|
| 984 |
with col_3d:
|
| 985 |
-
st.markdown("<h5>
|
| 986 |
if avg_leadtime_per_executor.empty:
|
| 987 |
st.warning("No data for executor analysis by nama_pic.")
|
| 988 |
else:
|
| 989 |
-
sort_option_3d = st.selectbox("Show
|
| 990 |
|
| 991 |
# Urutkan data dari tertinggi ke terendah
|
| 992 |
sorted_data_all = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=False)
|
|
@@ -1043,6 +1121,7 @@ with col_3d:
|
|
| 1043 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1044 |
|
| 1045 |
|
|
|
|
| 1046 |
####OBJECTIVE 4
|
| 1047 |
try:
|
| 1048 |
from wordcloud import WordCloud
|
|
|
|
| 10 |
import sklearn
|
| 11 |
import kaleido
|
| 12 |
|
| 13 |
+
|
| 14 |
# =================== PAGE CONFIG ===================
|
| 15 |
st.set_page_config(
|
| 16 |
page_title="PLN Audit Insight & Intelligence Dashboard",
|
|
|
|
| 326 |
</div>
|
| 327 |
""", unsafe_allow_html=True)
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
|
|
|
| 330 |
|
| 331 |
|
| 332 |
+
import streamlit as st
|
| 333 |
+
import plotly.graph_objects as go
|
| 334 |
+
import numpy as np
|
| 335 |
+
import pandas as pd
|
| 336 |
+
|
| 337 |
+
# =================== OBJECTIVE 1 - Company Reporting Activity (Polar Bar Chart) ===================
|
| 338 |
+
st.markdown(
|
| 339 |
+
"""
|
| 340 |
+
<style>
|
| 341 |
+
.section-title {
|
| 342 |
+
text-align: center;
|
| 343 |
+
font-size: 1.5rem;
|
| 344 |
+
font-weight: 600;
|
| 345 |
+
color: #2c3e50;
|
| 346 |
+
margin-bottom: 1.2rem;
|
| 347 |
+
}
|
| 348 |
+
.ai-insight {
|
| 349 |
+
background-color: #f8f9fa;
|
| 350 |
+
padding: 12px;
|
| 351 |
+
border-left: 4px solid #27ae60;
|
| 352 |
+
border-radius: 0 4px 4px 0;
|
| 353 |
+
font-size: 0.95rem;
|
| 354 |
+
line-height: 1.5;
|
| 355 |
+
margin-top: 1rem;
|
| 356 |
+
}
|
| 357 |
+
</style>
|
| 358 |
+
<h3 class='section-title'>OBJECTIVE 1 — Company Reporting Activity: Who Reports the Most?</h3>
|
| 359 |
+
""",
|
| 360 |
+
unsafe_allow_html=True
|
| 361 |
+
)
|
| 362 |
|
|
|
|
| 363 |
df_local = df_filtered.copy()
|
| 364 |
|
| 365 |
# Tambah kolom bulan
|
| 366 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 367 |
|
|
|
|
| 368 |
if 'temuan_kode_distrik' in df_local.columns:
|
| 369 |
df_local['Area_Type'] = df_local['temuan_kode_distrik'].apply(
|
| 370 |
lambda x: 'PG' if 'PG' in str(x).upper()
|
|
|
|
| 386 |
creators_by_company_month = df_area.groupby(['created_month', 'nama_perusahaan'])['creator_nid'].nunique().reset_index(name='unique_creators')
|
| 387 |
# Gabung
|
| 388 |
merged = findings_by_company_month.merge(creators_by_company_month, on=['created_month', 'nama_perusahaan'], how='outer')
|
|
|
|
| 389 |
merged = merged.fillna({'findings_count': 0, 'unique_creators': 0})
|
| 390 |
# Filter untuk menghindari pembagian dengan nol
|
| 391 |
merged = merged[merged['unique_creators'] > 0]
|
|
|
|
| 410 |
avg_ratio_pg = calculate_avg_ratio_per_company(df_pg)
|
| 411 |
avg_ratio_um = calculate_avg_ratio_per_company(df_um)
|
| 412 |
|
| 413 |
+
# Palet biru pastel konsisten (soft, harmonis)
|
| 414 |
+
pastel_blues = [
|
| 415 |
+
"#A8DADC", # light cyan
|
| 416 |
+
"#E2ECE9", # pale mint
|
| 417 |
+
"#CCE4E7", # soft sky
|
| 418 |
+
"#B5D9D9", # muted aqua
|
| 419 |
+
"#98C8D1", # gentle teal
|
| 420 |
+
"#7FB9C1", # calm blue
|
| 421 |
+
"#6BA9B3" # deep pastel teal
|
| 422 |
+
]
|
| 423 |
+
pln_color = "#FFD700" # Kuning PLN
|
| 424 |
+
|
| 425 |
+
def assign_colors(df):
|
| 426 |
+
colors = []
|
| 427 |
+
blue_idx = 0
|
| 428 |
+
for company in df['nama_perusahaan']:
|
| 429 |
if 'PLN' in str(company).upper():
|
| 430 |
+
colors.append(pln_color)
|
| 431 |
else:
|
| 432 |
+
colors.append(pastel_blues[blue_idx % len(pastel_blues)])
|
| 433 |
+
blue_idx += 1
|
| 434 |
+
return colors
|
| 435 |
+
|
| 436 |
+
# Fungsi untuk membuat polar bar chart
|
| 437 |
+
def create_polar_bar_chart(df, area_name):
|
| 438 |
+
if df.empty:
|
| 439 |
+
return None
|
| 440 |
+
|
| 441 |
+
# Urutkan berdasarkan rasio untuk tampilan konsisten
|
| 442 |
+
df = df.sort_values('avg_monthly_ratio', ascending=True)
|
| 443 |
+
companies = df['nama_perusahaan'].tolist()
|
| 444 |
+
ratios = df['avg_monthly_ratio'].tolist()
|
| 445 |
+
colors = assign_colors(df)
|
| 446 |
+
|
| 447 |
+
# Hitung total temuan untuk menghitung sudut (proporsi)
|
| 448 |
+
total_findings = df_local[df_local['Area_Type'] == area_name].groupby('nama_perusahaan').size()
|
| 449 |
+
angles = []
|
| 450 |
+
total_angle = 0
|
| 451 |
+
for comp in companies:
|
| 452 |
+
count = total_findings.get(comp, 0)
|
| 453 |
+
angle = (count / total_findings.sum()) * 360 if total_findings.sum() > 0 else 0
|
| 454 |
+
angles.append(angle)
|
| 455 |
+
|
| 456 |
+
# Hitung sudut tengah untuk setiap bar
|
| 457 |
+
mid_angles = []
|
| 458 |
+
current_angle = 0
|
| 459 |
+
for a in angles:
|
| 460 |
+
mid_angles.append(current_angle + a / 2)
|
| 461 |
+
current_angle += a
|
| 462 |
+
|
| 463 |
+
fig = go.Figure()
|
| 464 |
+
fig.add_trace(go.Barpolar(
|
| 465 |
+
r=ratios,
|
| 466 |
+
theta=mid_angles,
|
| 467 |
+
width=angles,
|
| 468 |
+
marker_color=colors,
|
| 469 |
+
marker_line_color="white",
|
| 470 |
+
marker_line_width=1.2,
|
| 471 |
+
opacity=0.9,
|
| 472 |
+
hovertemplate="<b>%{text}</b><br>Avg Ratio: %{r:.2f}<extra></extra>",
|
| 473 |
+
text=companies,
|
| 474 |
+
))
|
| 475 |
+
|
| 476 |
+
fig.update_layout(
|
| 477 |
+
title=f'{area_name} Area',
|
| 478 |
+
polar=dict(
|
| 479 |
+
bgcolor="white",
|
| 480 |
+
radialaxis=dict(
|
| 481 |
+
visible=True,
|
| 482 |
+
tickfont=dict(size=9),
|
| 483 |
+
gridcolor='lightgray',
|
| 484 |
+
title=dict(text='Avg Finding/Person', font=dict(size=10))
|
| 485 |
+
),
|
| 486 |
+
angularaxis=dict(
|
| 487 |
+
visible=True,
|
| 488 |
+
direction='clockwise',
|
| 489 |
+
tickfont=dict(size=9),
|
| 490 |
+
showline=False
|
| 491 |
+
),
|
| 492 |
+
),
|
| 493 |
+
showlegend=False,
|
| 494 |
+
margin=dict(t=40, b=20, l=20, r=20),
|
| 495 |
+
height=400,
|
| 496 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 497 |
+
plot_bgcolor="rgba(0,0,0,0)"
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
return fig
|
| 501 |
|
| 502 |
# Plot
|
| 503 |
col1, col2 = st.columns(2)
|
| 504 |
|
| 505 |
with col1:
|
| 506 |
st.markdown("<h5>Unit Pembangkit: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 507 |
+
fig_pg = create_polar_bar_chart(avg_ratio_pg, 'PG')
|
| 508 |
+
if fig_pg:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
st.plotly_chart(fig_pg, use_container_width=True)
|
| 510 |
|
| 511 |
# AI Insight untuk PG
|
|
|
|
| 529 |
|
| 530 |
with col2:
|
| 531 |
st.markdown("<h5>Unit Maintenance: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 532 |
+
fig_um = create_polar_bar_chart(avg_ratio_um, 'UM')
|
| 533 |
+
if fig_um:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
st.plotly_chart(fig_um, use_container_width=True)
|
| 535 |
|
| 536 |
# AI Insight untuk UM
|
|
|
|
| 780 |
margin-top: 1rem;
|
| 781 |
}
|
| 782 |
</style>
|
| 783 |
+
<h3 class='section-title'>OBJECTIVE 4 — Frequency & Response Time: Who Reports Well? Who Executes Well?</h3>
|
| 784 |
""",
|
| 785 |
unsafe_allow_html=True
|
| 786 |
)
|
|
|
|
| 869 |
col_3a, col_3c = st.columns(2)
|
| 870 |
|
| 871 |
with col_3a:
|
| 872 |
+
st.markdown("<h5>4a. Average Finding by Division (Reporter)</h5>", unsafe_allow_html=True)
|
| 873 |
if avg_ratio_per_nama.empty:
|
| 874 |
st.warning("No data for reporter analysis by division.")
|
| 875 |
else:
|
| 876 |
+
sort_option_3a = st.selectbox("Show 4a:", ["Top 10", "Bottom 10"], key='sort_3a')
|
| 877 |
|
| 878 |
# Urutkan data dari tertinggi ke terendah
|
| 879 |
sorted_data_all = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False)
|
|
|
|
| 932 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 933 |
|
| 934 |
with col_3c:
|
| 935 |
+
st.markdown("<h5>4b. Average Finding Rate per Reporter (Name)</h5>", unsafe_allow_html=True)
|
| 936 |
if avg_rate_per_creator.empty:
|
| 937 |
st.warning("No data for reporter analysis by creator_name.")
|
| 938 |
else:
|
| 939 |
+
sort_option_3c = st.selectbox("Show 4b:", ["Top 10", "Bottom 10"], key='sort_3c')
|
| 940 |
|
| 941 |
# Urutkan data dari tertinggi ke terendah
|
| 942 |
sorted_data_all = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False)
|
|
|
|
| 999 |
col_3b, col_3d = st.columns(2)
|
| 1000 |
|
| 1001 |
with col_3b:
|
| 1002 |
+
st.markdown("<h5>4c. Average Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
|
| 1003 |
if avg_leadtime_nama.empty:
|
| 1004 |
st.warning("No data for executor analysis by division.")
|
| 1005 |
else:
|
| 1006 |
+
sort_option_3b = st.selectbox("Show 4c:", ["Top 10", "Bottom 10"], key='sort_3b')
|
| 1007 |
|
| 1008 |
# Urutkan data dari tertinggi ke terendah
|
| 1009 |
sorted_data_all = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=False)
|
|
|
|
| 1060 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1061 |
|
| 1062 |
with col_3d:
|
| 1063 |
+
st.markdown("<h5>4d. Average Lead Time by Executor (Name)</h5>", unsafe_allow_html=True)
|
| 1064 |
if avg_leadtime_per_executor.empty:
|
| 1065 |
st.warning("No data for executor analysis by nama_pic.")
|
| 1066 |
else:
|
| 1067 |
+
sort_option_3d = st.selectbox("Show 4d:", ["Top 10", "Bottom 10"], key='sort_3d')
|
| 1068 |
|
| 1069 |
# Urutkan data dari tertinggi ke terendah
|
| 1070 |
sorted_data_all = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=False)
|
|
|
|
| 1121 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1122 |
|
| 1123 |
|
| 1124 |
+
|
| 1125 |
####OBJECTIVE 4
|
| 1126 |
try:
|
| 1127 |
from wordcloud import WordCloud
|