Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -284,15 +284,26 @@ st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
|
| 284 |
|
| 285 |
|
| 286 |
# =================== HEADER ===================
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
""
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
# =================== 1. Pie Charts: Temuan/Person by Company (PG & UM) - PERBAIKAN ===================
|
| 298 |
st.markdown("<h3 class='section-title'>OBJECTIVE 1 - Company Reporting Activity: Who Reports the Most?</h3>", unsafe_allow_html=True)
|
|
@@ -303,28 +314,21 @@ df_local = df_filtered.copy()
|
|
| 303 |
# Tambah kolom bulan
|
| 304 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 305 |
|
| 306 |
-
# --- Langsung buat Area_Type PG / UM tanpa filter ---
|
| 307 |
-
|
| 308 |
if 'temuan_kode_distrik' in df_local.columns:
|
| 309 |
-
|
| 310 |
df_local['Area_Type'] = df_local['temuan_kode_distrik'].apply(
|
| 311 |
lambda x: 'PG' if 'PG' in str(x).upper()
|
| 312 |
else 'UM' if 'UM' in str(x).upper()
|
| 313 |
else 'Other'
|
| 314 |
)
|
| 315 |
|
| 316 |
-
# Otomatis bagi dataset
|
| 317 |
df_pg = df_local[df_local['Area_Type'] == 'PG'].copy()
|
| 318 |
df_um = df_local[df_local['Area_Type'] == 'UM'].copy()
|
| 319 |
|
| 320 |
-
else:
|
| 321 |
-
df_pg = pd.DataFrame()
|
| 322 |
-
df_um = pd.DataFrame()
|
| 323 |
-
|
| 324 |
# --- Fungsi untuk menghitung rasio perusahaan ---
|
| 325 |
def calculate_avg_ratio_per_company(df_area):
|
| 326 |
if df_area.empty:
|
| 327 |
-
# Jika area tidak dipilih atau data kosong setelah filter
|
| 328 |
return pd.DataFrame()
|
| 329 |
# Hitung temuan per bulan per perusahaan
|
| 330 |
findings_by_company_month = df_area.groupby(['created_month', 'nama_perusahaan']).size().reset_index(name='findings_count')
|
|
@@ -335,10 +339,8 @@ else:
|
|
| 335 |
# Isi NaN dengan 0 untuk kolom yang mungkin hilang dari merge
|
| 336 |
merged = merged.fillna({'findings_count': 0, 'unique_creators': 0})
|
| 337 |
# Filter untuk menghindari pembagian dengan nol
|
| 338 |
-
# Kita hanya ingin menghitung rasio jika jumlah pelapor > 0
|
| 339 |
merged = merged[merged['unique_creators'] > 0]
|
| 340 |
# Hitung rasio (ignore NaN)
|
| 341 |
-
# Pembagian oleh 0 akan menghasilkan inf, jadi kita ganti inf dengan NaN
|
| 342 |
merged['ratio'] = merged['findings_count'] / merged['unique_creators']
|
| 343 |
merged['ratio'] = merged['ratio'].replace([np.inf, -np.inf], np.nan)
|
| 344 |
|
|
@@ -347,11 +349,9 @@ else:
|
|
| 347 |
return pd.DataFrame()
|
| 348 |
|
| 349 |
# Rata-rata bulanan per perusahaan
|
| 350 |
-
# Group by nama_perusahaan dan ambil mean dari rasio
|
| 351 |
-
# mean() akan mengabaikan NaN secara default
|
| 352 |
avg_ratio = merged.groupby('nama_perusahaan')['ratio'].mean().reset_index(name='avg_monthly_ratio')
|
| 353 |
|
| 354 |
-
# Jika hasil akhirnya hanya NaN
|
| 355 |
if avg_ratio['avg_monthly_ratio'].isna().all():
|
| 356 |
return pd.DataFrame()
|
| 357 |
|
|
@@ -364,14 +364,12 @@ else:
|
|
| 364 |
# Fungsi untuk menentukan warna
|
| 365 |
def get_color_map(company_series):
|
| 366 |
pln_color = "#FFD700" # Kuning untuk PLN
|
| 367 |
-
# Daftar warna biru (dari gelap ke terang)
|
| 368 |
blue_colors = ["#1E90FF", "#87CEEB", "#B0E0E6", "#ADD8E6", "#E0F6FF"]
|
| 369 |
color_map = {}
|
| 370 |
for company in company_series:
|
| 371 |
if 'PLN' in str(company).upper():
|
| 372 |
color_map[company] = pln_color
|
| 373 |
else:
|
| 374 |
-
# Pilih warna biru berdasarkan indeks, ulangi jika perlu
|
| 375 |
idx = len([c for c in color_map.values() if c != pln_color]) % len(blue_colors)
|
| 376 |
color_map[company] = blue_colors[idx]
|
| 377 |
return color_map
|
|
@@ -380,14 +378,14 @@ else:
|
|
| 380 |
col1, col2 = st.columns(2)
|
| 381 |
|
| 382 |
with col1:
|
| 383 |
-
st.markdown("<h5>Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 384 |
if not avg_ratio_pg.empty:
|
| 385 |
color_discrete_map_pg = get_color_map(avg_ratio_pg['nama_perusahaan'])
|
| 386 |
fig_pg = px.pie(
|
| 387 |
avg_ratio_pg,
|
| 388 |
values='avg_monthly_ratio',
|
| 389 |
names='nama_perusahaan',
|
| 390 |
-
title='
|
| 391 |
color='nama_perusahaan',
|
| 392 |
color_discrete_map=color_discrete_map_pg
|
| 393 |
)
|
|
@@ -395,11 +393,10 @@ else:
|
|
| 395 |
|
| 396 |
# AI Insight untuk PG
|
| 397 |
if not avg_ratio_pg.empty:
|
| 398 |
-
# Temukan perusahaan dengan rasio tertinggi dan terendah di PG
|
| 399 |
top_company_pg = avg_ratio_pg.loc[avg_ratio_pg['avg_monthly_ratio'].idxmax()]
|
| 400 |
low_company_pg = avg_ratio_pg.loc[avg_ratio_pg['avg_monthly_ratio'].idxmin()]
|
| 401 |
|
| 402 |
-
st.markdown("### Insight")
|
| 403 |
insight_text = (
|
| 404 |
f"<div class='ai-insight'>"
|
| 405 |
f"In PG Area, <strong>{top_company_pg['nama_perusahaan']}</strong> has the highest average finding-to-person ratio "
|
|
@@ -414,14 +411,14 @@ else:
|
|
| 414 |
st.warning("No data for PG area or all ratios are NaN.")
|
| 415 |
|
| 416 |
with col2:
|
| 417 |
-
st.markdown("<h5>Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
|
| 418 |
if not avg_ratio_um.empty:
|
| 419 |
color_discrete_map_um = get_color_map(avg_ratio_um['nama_perusahaan'])
|
| 420 |
fig_um = px.pie(
|
| 421 |
avg_ratio_um,
|
| 422 |
values='avg_monthly_ratio',
|
| 423 |
names='nama_perusahaan',
|
| 424 |
-
title='
|
| 425 |
color='nama_perusahaan',
|
| 426 |
color_discrete_map=color_discrete_map_um
|
| 427 |
)
|
|
@@ -429,11 +426,10 @@ else:
|
|
| 429 |
|
| 430 |
# AI Insight untuk UM
|
| 431 |
if not avg_ratio_um.empty:
|
| 432 |
-
# Temukan perusahaan dengan rasio tertinggi dan terendah di UM
|
| 433 |
top_company_um = avg_ratio_um.loc[avg_ratio_um['avg_monthly_ratio'].idxmax()]
|
| 434 |
low_company_um = avg_ratio_um.loc[avg_ratio_um['avg_monthly_ratio'].idxmin()]
|
| 435 |
|
| 436 |
-
st.markdown("### Insight")
|
| 437 |
insight_text = (
|
| 438 |
f"<div class='ai-insight'>"
|
| 439 |
f"In UM Area, <strong>{top_company_um['nama_perusahaan']}</strong> exhibits the highest average finding-to-person ratio "
|
|
@@ -445,7 +441,9 @@ else:
|
|
| 445 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 446 |
else:
|
| 447 |
st.warning("No data for UM area or all ratios are NaN.")
|
| 448 |
-
|
|
|
|
|
|
|
| 449 |
|
| 450 |
# =================== 2. Treemap: Distribusi Temuan per Area (nama_lokasi_full) - PERBAIKAN ===================
|
| 451 |
st.markdown("<h3 class='section-title'>OBJECTIVE 2 - Active vs Inactive Locations: Who Leads?</h3>", unsafe_allow_html=True)
|
|
|
|
| 284 |
|
| 285 |
|
| 286 |
# =================== HEADER ===================
|
| 287 |
+
# Gunakan kolom untuk menyusun logo dan teks secara horizontal
|
| 288 |
+
header_cols = st.columns([1, 3]) # Rasio lebar kolom: Logo (kecil), Teks (lebih besar)
|
| 289 |
+
|
| 290 |
+
with header_cols[0]:
|
| 291 |
+
# Cek apakah file logo ada, lalu tampilkan
|
| 292 |
+
logo_path = "pln.png" # Sesuaikan nama file jika berbeda
|
| 293 |
+
if os.path.exists(logo_path):
|
| 294 |
+
st.image(logo_path, caption="", width=100) # Atur width sesuai kebutuhan
|
| 295 |
+
else:
|
| 296 |
+
st.warning(f"Logo `{logo_path}` not found. Please place it in the same directory as this script.")
|
| 297 |
+
|
| 298 |
+
with header_cols[1]:
|
| 299 |
+
st.markdown("""
|
| 300 |
+
<div class="main-header">
|
| 301 |
+
<h1>PLN Audit Insight & Intelligence Dashboard</h1>
|
| 302 |
+
<p style="text-align:center; color:#546e7a; font-size:1.1em; margin-top:8px;">
|
| 303 |
+
Operational Risk Intelligence for Audit & Compliance
|
| 304 |
+
</p>
|
| 305 |
+
</div>
|
| 306 |
+
""", unsafe_allow_html=True)
|
| 307 |
|
| 308 |
# =================== 1. Pie Charts: Temuan/Person by Company (PG & UM) - PERBAIKAN ===================
|
| 309 |
st.markdown("<h3 class='section-title'>OBJECTIVE 1 - Company Reporting Activity: Who Reports the Most?</h3>", unsafe_allow_html=True)
|
|
|
|
| 314 |
# Tambah kolom bulan
|
| 315 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 316 |
|
| 317 |
+
# --- Langsung buat Area_Type PG / UM tanpa filter manual ---
|
|
|
|
| 318 |
if 'temuan_kode_distrik' in df_local.columns:
|
|
|
|
| 319 |
df_local['Area_Type'] = df_local['temuan_kode_distrik'].apply(
|
| 320 |
lambda x: 'PG' if 'PG' in str(x).upper()
|
| 321 |
else 'UM' if 'UM' in str(x).upper()
|
| 322 |
else 'Other'
|
| 323 |
)
|
| 324 |
|
| 325 |
+
# Otomatis bagi dataset berdasarkan Area_Type
|
| 326 |
df_pg = df_local[df_local['Area_Type'] == 'PG'].copy()
|
| 327 |
df_um = df_local[df_local['Area_Type'] == 'UM'].copy()
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
# --- Fungsi untuk menghitung rasio perusahaan ---
|
| 330 |
def calculate_avg_ratio_per_company(df_area):
|
| 331 |
if df_area.empty:
|
|
|
|
| 332 |
return pd.DataFrame()
|
| 333 |
# Hitung temuan per bulan per perusahaan
|
| 334 |
findings_by_company_month = df_area.groupby(['created_month', 'nama_perusahaan']).size().reset_index(name='findings_count')
|
|
|
|
| 339 |
# Isi NaN dengan 0 untuk kolom yang mungkin hilang dari merge
|
| 340 |
merged = merged.fillna({'findings_count': 0, 'unique_creators': 0})
|
| 341 |
# Filter untuk menghindari pembagian dengan nol
|
|
|
|
| 342 |
merged = merged[merged['unique_creators'] > 0]
|
| 343 |
# Hitung rasio (ignore NaN)
|
|
|
|
| 344 |
merged['ratio'] = merged['findings_count'] / merged['unique_creators']
|
| 345 |
merged['ratio'] = merged['ratio'].replace([np.inf, -np.inf], np.nan)
|
| 346 |
|
|
|
|
| 349 |
return pd.DataFrame()
|
| 350 |
|
| 351 |
# Rata-rata bulanan per perusahaan
|
|
|
|
|
|
|
| 352 |
avg_ratio = merged.groupby('nama_perusahaan')['ratio'].mean().reset_index(name='avg_monthly_ratio')
|
| 353 |
|
| 354 |
+
# Jika hasil akhirnya hanya NaN, kembalikan DataFrame kosong
|
| 355 |
if avg_ratio['avg_monthly_ratio'].isna().all():
|
| 356 |
return pd.DataFrame()
|
| 357 |
|
|
|
|
| 364 |
# Fungsi untuk menentukan warna
|
| 365 |
def get_color_map(company_series):
|
| 366 |
pln_color = "#FFD700" # Kuning untuk PLN
|
|
|
|
| 367 |
blue_colors = ["#1E90FF", "#87CEEB", "#B0E0E6", "#ADD8E6", "#E0F6FF"]
|
| 368 |
color_map = {}
|
| 369 |
for company in company_series:
|
| 370 |
if 'PLN' in str(company).upper():
|
| 371 |
color_map[company] = pln_color
|
| 372 |
else:
|
|
|
|
| 373 |
idx = len([c for c in color_map.values() if c != pln_color]) % len(blue_colors)
|
| 374 |
color_map[company] = blue_colors[idx]
|
| 375 |
return color_map
|
|
|
|
| 378 |
col1, col2 = st.columns(2)
|
| 379 |
|
| 380 |
with col1:
|
| 381 |
+
st.markdown("<h5>Unit Pembangkit: Avg Monthly Finding/Person by Company</h5>", unsafe_allow_html=True)
|
| 382 |
if not avg_ratio_pg.empty:
|
| 383 |
color_discrete_map_pg = get_color_map(avg_ratio_pg['nama_perusahaan'])
|
| 384 |
fig_pg = px.pie(
|
| 385 |
avg_ratio_pg,
|
| 386 |
values='avg_monthly_ratio',
|
| 387 |
names='nama_perusahaan',
|
| 388 |
+
title='PG Area',
|
| 389 |
color='nama_perusahaan',
|
| 390 |
color_discrete_map=color_discrete_map_pg
|
| 391 |
)
|
|
|
|
| 393 |
|
| 394 |
# AI Insight untuk PG
|
| 395 |
if not avg_ratio_pg.empty:
|
|
|
|
| 396 |
top_company_pg = avg_ratio_pg.loc[avg_ratio_pg['avg_monthly_ratio'].idxmax()]
|
| 397 |
low_company_pg = avg_ratio_pg.loc[avg_ratio_pg['avg_monthly_ratio'].idxmin()]
|
| 398 |
|
| 399 |
+
st.markdown("### Insight (PG)")
|
| 400 |
insight_text = (
|
| 401 |
f"<div class='ai-insight'>"
|
| 402 |
f"In PG Area, <strong>{top_company_pg['nama_perusahaan']}</strong> has the highest average finding-to-person ratio "
|
|
|
|
| 411 |
st.warning("No data for PG area or all ratios are NaN.")
|
| 412 |
|
| 413 |
with col2:
|
| 414 |
+
st.markdown("<h5>Unit Maintenance: Avg Monthly Finding/Person by Company</h5>", unsafe_allow_html=True)
|
| 415 |
if not avg_ratio_um.empty:
|
| 416 |
color_discrete_map_um = get_color_map(avg_ratio_um['nama_perusahaan'])
|
| 417 |
fig_um = px.pie(
|
| 418 |
avg_ratio_um,
|
| 419 |
values='avg_monthly_ratio',
|
| 420 |
names='nama_perusahaan',
|
| 421 |
+
title='UM Area',
|
| 422 |
color='nama_perusahaan',
|
| 423 |
color_discrete_map=color_discrete_map_um
|
| 424 |
)
|
|
|
|
| 426 |
|
| 427 |
# AI Insight untuk UM
|
| 428 |
if not avg_ratio_um.empty:
|
|
|
|
| 429 |
top_company_um = avg_ratio_um.loc[avg_ratio_um['avg_monthly_ratio'].idxmax()]
|
| 430 |
low_company_um = avg_ratio_um.loc[avg_ratio_um['avg_monthly_ratio'].idxmin()]
|
| 431 |
|
| 432 |
+
st.markdown("### Insight (UM)")
|
| 433 |
insight_text = (
|
| 434 |
f"<div class='ai-insight'>"
|
| 435 |
f"In UM Area, <strong>{top_company_um['nama_perusahaan']}</strong> exhibits the highest average finding-to-person ratio "
|
|
|
|
| 441 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 442 |
else:
|
| 443 |
st.warning("No data for UM area or all ratios are NaN.")
|
| 444 |
+
else:
|
| 445 |
+
st.error("Column 'temuan_kode_distrik' not found in the data. Cannot determine PG/UM areas.")
|
| 446 |
+
st.stop()
|
| 447 |
|
| 448 |
# =================== 2. Treemap: Distribusi Temuan per Area (nama_lokasi_full) - PERBAIKAN ===================
|
| 449 |
st.markdown("<h3 class='section-title'>OBJECTIVE 2 - Active vs Inactive Locations: Who Leads?</h3>", unsafe_allow_html=True)
|