Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -690,7 +690,7 @@ if df_local.empty:
|
|
| 690 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 691 |
|
| 692 |
|
| 693 |
-
# ─── Helper: Hitung rasio per
|
| 694 |
def compute_reporter_ratio_by_nama(df):
|
| 695 |
if 'nama' not in df.columns:
|
| 696 |
return pd.DataFrame()
|
|
@@ -707,18 +707,7 @@ def compute_reporter_ratio_by_nama(df):
|
|
| 707 |
return avg_ratio_per_nama
|
| 708 |
|
| 709 |
|
| 710 |
-
# ─── Helper: Hitung
|
| 711 |
-
def compute_executor_leadtime_by_nama(df):
|
| 712 |
-
if 'nama' not in df.columns or 'days_to_close' not in df.columns:
|
| 713 |
-
return pd.DataFrame()
|
| 714 |
-
|
| 715 |
-
leadtime_by_nama_month = df.groupby(['created_month', 'nama'])['days_to_close'].mean().reset_index(name='avg_leadtime')
|
| 716 |
-
avg_leadtime_nama = leadtime_by_nama_month.groupby('nama')['avg_leadtime'].mean().reset_index(name='avg_monthly_leadtime')
|
| 717 |
-
avg_leadtime_nama = avg_leadtime_nama.dropna(subset=['avg_monthly_leadtime'])
|
| 718 |
-
return avg_leadtime_nama
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
# ─── Helper: Hitung rasio per creator_name ──────────────────────────────────
|
| 722 |
def compute_reporter_rate_by_creator(df):
|
| 723 |
if 'creator_name' not in df.columns:
|
| 724 |
return pd.DataFrame()
|
|
@@ -735,335 +724,261 @@ def compute_reporter_rate_by_creator(df):
|
|
| 735 |
return avg_rate_per_creator
|
| 736 |
|
| 737 |
|
| 738 |
-
# ─── Helper: Hitung lead time per
|
| 739 |
-
def
|
| 740 |
-
if '
|
| 741 |
return pd.DataFrame()
|
| 742 |
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
merged_exec_pic['avg_monthly_leadtime'] = merged_exec_pic['avg_monthly_leadtime'].replace([np.inf, -np.inf], np.nan)
|
| 751 |
-
avg_leadtime_per_executor = merged_exec_pic.dropna(subset=['avg_monthly_leadtime'])
|
| 752 |
-
return avg_leadtime_per_executor
|
| 753 |
|
| 754 |
|
| 755 |
-
# ───
|
| 756 |
-
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
|
| 759 |
-
# ─── Data untuk 3b & 3d ──────────────────────────────────────────────────────
|
| 760 |
-
avg_leadtime_nama = compute_executor_leadtime_by_nama(df_local)
|
| 761 |
-
avg_leadtime_per_executor = compute_executor_leadtime_by_pic(df_local)
|
| 762 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
|
| 764 |
-
|
|
|
|
| 765 |
def add_color_by_global_rank(df, value_col, top_n=5, worst_n=5, high_is_good=True):
|
| 766 |
-
"""
|
| 767 |
-
Menambahkan kolom 'color' berdasarkan ranking global.
|
| 768 |
-
- Jika high_is_good=True (e.g., ratio): top_n → hijau, worst_n → default
|
| 769 |
-
- Jika high_is_good=False (e.g., lead time): worst_n (tertinggi) → merah, top_n (terendah) → default
|
| 770 |
-
"""
|
| 771 |
df = df.copy()
|
| 772 |
-
df = df.sort_values(value_col, ascending=not high_is_good).reset_index(drop=True) # descending if high_is_good
|
| 773 |
df['color'] = '#1f77b4' # default biru
|
| 774 |
|
|
|
|
|
|
|
|
|
|
| 775 |
if high_is_good:
|
| 776 |
-
# Nilai tinggi = baik → top
|
| 777 |
-
|
| 778 |
-
df.loc[
|
| 779 |
else:
|
| 780 |
-
# Nilai tinggi = buruk →
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
df.loc[worst_idx, 'color'] = '#D32F2F'
|
| 785 |
return df
|
| 786 |
|
| 787 |
|
| 788 |
-
# ─── Layout: 2
|
| 789 |
-
# Baris 1: 3a & 3c
|
| 790 |
col_3a, col_3c = st.columns(2)
|
| 791 |
|
|
|
|
| 792 |
with col_3a:
|
| 793 |
-
st.markdown("<h5>3a.
|
| 794 |
if avg_ratio_per_nama.empty:
|
| 795 |
-
st.warning("No data for reporter analysis
|
| 796 |
else:
|
| 797 |
-
|
| 798 |
|
| 799 |
-
|
| 800 |
-
|
| 801 |
|
| 802 |
-
#
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
else: # Bottom 10
|
| 806 |
-
subset_data = sorted_all_3a.tail(10).sort_values('avg_monthly_ratio', ascending=True) # ascending dalam subset
|
| 807 |
|
| 808 |
-
#
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
)
|
| 812 |
-
subset_data = subset_data.merge(
|
| 813 |
-
avg_ratio_per_nama_colored[['nama', 'color']], on='nama', how='left'
|
| 814 |
-
).fillna({'color': '#1f77b4'})
|
| 815 |
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
else:
|
| 820 |
-
subset_data = subset_data # ascending (terendah di atas)
|
| 821 |
-
|
| 822 |
-
fig_rep_nama = px.bar(
|
| 823 |
-
subset_data,
|
| 824 |
-
x='avg_monthly_ratio',
|
| 825 |
-
y='nama',
|
| 826 |
-
orientation='h',
|
| 827 |
-
title=f'Avg Monthly Finding/Person Ratio — {sort_option_3a}',
|
| 828 |
labels={'avg_monthly_ratio': 'Avg Monthly Ratio', 'nama': 'Division'},
|
| 829 |
-
color='color',
|
| 830 |
-
|
| 831 |
-
text=subset_data['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
|
| 832 |
)
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
showlegend=False
|
| 837 |
-
)
|
| 838 |
-
fig_rep_nama.update_traces(textposition='auto')
|
| 839 |
-
st.plotly_chart(fig_rep_nama, use_container_width=True)
|
| 840 |
|
| 841 |
-
#
|
| 842 |
-
if len(
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
median_val = sorted_all_3a['avg_monthly_ratio'].median()
|
| 847 |
-
best_div = sorted_all_3a.iloc[0]['nama']
|
| 848 |
-
worst_div = sorted_all_3a.iloc[-1]['nama']
|
| 849 |
-
|
| 850 |
-
insight_text = (
|
| 851 |
f"<div class='ai-insight'>"
|
| 852 |
-
f"<strong>
|
| 853 |
-
f"
|
| 854 |
-
f"<strong>
|
| 855 |
-
f"
|
| 856 |
-
|
| 857 |
)
|
| 858 |
-
else:
|
| 859 |
-
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 860 |
-
st.markdown(insight_text, unsafe_allow_html=True)
|
| 861 |
|
| 862 |
|
|
|
|
| 863 |
with col_3c:
|
| 864 |
-
st.markdown("<h5>
|
| 865 |
if avg_rate_per_creator.empty:
|
| 866 |
-
st.warning("No data for reporter analysis
|
| 867 |
else:
|
| 868 |
-
|
| 869 |
|
| 870 |
-
|
|
|
|
| 871 |
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
else:
|
| 875 |
-
subset_data = sorted_all_3c.tail(10).sort_values('avg_monthly_rate', ascending=True)
|
| 876 |
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
avg_rate_per_creator, 'avg_monthly_rate', top_n=5, high_is_good=True
|
| 880 |
-
)
|
| 881 |
-
subset_data = subset_data.merge(
|
| 882 |
-
avg_rate_per_creator_colored[['creator_name', 'color']], on='creator_name', how='left'
|
| 883 |
-
).fillna({'color': '#1f77b4'})
|
| 884 |
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
fig_rep_creator = px.bar(
|
| 889 |
-
subset_data,
|
| 890 |
-
x='avg_monthly_rate',
|
| 891 |
-
y='creator_name',
|
| 892 |
-
orientation='h',
|
| 893 |
-
title=f'Avg Monthly Finding Rate — {sort_option_3c}',
|
| 894 |
labels={'avg_monthly_rate': 'Avg Monthly Findings', 'creator_name': 'Reporter'},
|
| 895 |
-
color='color',
|
| 896 |
-
|
| 897 |
-
text=subset_data['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
|
| 898 |
-
)
|
| 899 |
-
fig_rep_creator.update_layout(
|
| 900 |
-
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['creator_name'].tolist()},
|
| 901 |
-
height=500,
|
| 902 |
-
showlegend=False
|
| 903 |
)
|
| 904 |
-
|
| 905 |
-
|
|
|
|
| 906 |
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
mean_val = sorted_all_3c['avg_monthly_rate'].mean()
|
| 912 |
-
median_val = sorted_all_3c['avg_monthly_rate'].median()
|
| 913 |
-
best_reporter = sorted_all_3c.iloc[0]['creator_name']
|
| 914 |
-
worst_reporter = sorted_all_3c.iloc[-1]['creator_name']
|
| 915 |
-
|
| 916 |
-
insight_text = (
|
| 917 |
f"<div class='ai-insight'>"
|
| 918 |
-
f"<strong>
|
| 919 |
-
f"
|
| 920 |
-
f"<strong>
|
| 921 |
-
f"
|
| 922 |
-
|
| 923 |
)
|
| 924 |
-
else:
|
| 925 |
-
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 926 |
-
st.markdown(insight_text, unsafe_allow_html=True)
|
| 927 |
|
| 928 |
|
| 929 |
-
# Baris 2:
|
| 930 |
col_3b, col_3d = st.columns(2)
|
| 931 |
|
|
|
|
| 932 |
with col_3b:
|
| 933 |
-
st.markdown("<h5>
|
| 934 |
if avg_leadtime_nama.empty:
|
| 935 |
-
st.warning("No data for executor analysis
|
| 936 |
else:
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
# Full data: ascending (low = fast = good)
|
| 940 |
-
sorted_all_3b = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
|
| 941 |
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
else:
|
| 946 |
-
|
| 947 |
-
subset_data = sorted_all_3b.tail(10).sort_values('avg_monthly_leadtime', ascending=False) # descending dalam subset
|
| 948 |
|
| 949 |
-
# Warna
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
fig_exec_nama = px.bar(
|
| 962 |
-
subset_data,
|
| 963 |
-
x='avg_monthly_leadtime',
|
| 964 |
-
y='nama',
|
| 965 |
-
orientation='h',
|
| 966 |
-
title=f'Avg Monthly Lead Time (Days) — {sort_option_3b}',
|
| 967 |
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
|
| 968 |
-
color='color',
|
| 969 |
-
|
| 970 |
-
text=subset_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
|
| 971 |
-
)
|
| 972 |
-
fig_exec_nama.update_layout(
|
| 973 |
-
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama'].tolist()},
|
| 974 |
-
height=500,
|
| 975 |
-
showlegend=False
|
| 976 |
)
|
| 977 |
-
|
| 978 |
-
|
|
|
|
| 979 |
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
mean_lt = sorted_all_3b['avg_monthly_leadtime'].mean()
|
| 985 |
-
median_lt = sorted_all_3b['avg_monthly_leadtime'].median()
|
| 986 |
-
fastest_div = sorted_all_3b.iloc[0]['nama']
|
| 987 |
-
slowest_div = sorted_all_3b.iloc[-1]['nama']
|
| 988 |
-
|
| 989 |
-
insight_text = (
|
| 990 |
f"<div class='ai-insight'>"
|
| 991 |
-
f"<strong>
|
| 992 |
-
f"
|
| 993 |
-
f"<strong>
|
| 994 |
-
f"
|
| 995 |
-
|
| 996 |
)
|
| 997 |
-
else:
|
| 998 |
-
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 999 |
-
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1000 |
|
| 1001 |
|
|
|
|
| 1002 |
with col_3d:
|
| 1003 |
-
st.markdown("<h5>3d.
|
| 1004 |
-
if
|
| 1005 |
-
st.warning("No data for executor analysis
|
| 1006 |
else:
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
sorted_all_3d = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
|
| 1010 |
|
| 1011 |
-
|
| 1012 |
-
|
|
|
|
| 1013 |
else:
|
| 1014 |
-
|
| 1015 |
|
| 1016 |
-
# Warna
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
)
|
| 1020 |
-
subset_data = subset_data.merge(
|
| 1021 |
-
avg_leadtime_per_executor_colored[['nama_pic', 'color']], on='nama_pic', how='left'
|
| 1022 |
-
).fillna({'color': '#1f77b4'})
|
| 1023 |
|
| 1024 |
-
if
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama_pic': 'Executor'},
|
| 1034 |
-
color='color',
|
| 1035 |
-
color_discrete_map={c: c for c in subset_data['color'].unique()},
|
| 1036 |
-
text=subset_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
|
| 1037 |
-
)
|
| 1038 |
-
fig_exec_pic.update_layout(
|
| 1039 |
-
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama_pic'].tolist()},
|
| 1040 |
-
height=500,
|
| 1041 |
-
showlegend=False
|
| 1042 |
)
|
| 1043 |
-
|
| 1044 |
-
|
|
|
|
| 1045 |
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
mean_lt = sorted_all_3d['avg_monthly_leadtime'].mean()
|
| 1051 |
-
median_lt = sorted_all_3d['avg_monthly_leadtime'].median()
|
| 1052 |
-
fastest_exec = sorted_all_3d.iloc[0]['nama_pic']
|
| 1053 |
-
slowest_exec = sorted_all_3d.iloc[-1]['nama_pic']
|
| 1054 |
-
|
| 1055 |
-
insight_text = (
|
| 1056 |
f"<div class='ai-insight'>"
|
| 1057 |
-
f"<strong>
|
| 1058 |
-
f"
|
| 1059 |
-
f"<strong>
|
| 1060 |
-
f"
|
| 1061 |
-
|
| 1062 |
)
|
| 1063 |
-
else:
|
| 1064 |
-
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 1065 |
-
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1066 |
|
|
|
|
| 1067 |
try:
|
| 1068 |
from wordcloud import WordCloud
|
| 1069 |
import matplotlib.pyplot as plt
|
|
|
|
| 690 |
df_local['created_month'] = df_local['created_at'].dt.to_period('M')
|
| 691 |
|
| 692 |
|
| 693 |
+
# ─── Helper: Hitung rasio per division (reporter) ─────────────────────────────
|
| 694 |
def compute_reporter_ratio_by_nama(df):
|
| 695 |
if 'nama' not in df.columns:
|
| 696 |
return pd.DataFrame()
|
|
|
|
| 707 |
return avg_ratio_per_nama
|
| 708 |
|
| 709 |
|
| 710 |
+
# ─── Helper: Hitung rata-rata temuan per reporter (individu) ─────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
def compute_reporter_rate_by_creator(df):
|
| 712 |
if 'creator_name' not in df.columns:
|
| 713 |
return pd.DataFrame()
|
|
|
|
| 724 |
return avg_rate_per_creator
|
| 725 |
|
| 726 |
|
| 727 |
+
# ─── Helper: Hitung lead time per division (executor) ───────────────────────
|
| 728 |
+
def compute_executor_leadtime_by_nama(df):
|
| 729 |
+
if 'nama' not in df.columns or 'days_to_close' not in df.columns:
|
| 730 |
return pd.DataFrame()
|
| 731 |
|
| 732 |
+
# Filter hanya data dengan lead time valid
|
| 733 |
+
df_valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 734 |
+
|
| 735 |
+
leadtime_by_nama_month = df_valid.groupby(['created_month', 'nama'])['days_to_close'].mean().reset_index(name='avg_leadtime')
|
| 736 |
+
avg_leadtime_nama = leadtime_by_nama_month.groupby('nama')['avg_leadtime'].mean().reset_index(name='avg_monthly_leadtime')
|
| 737 |
+
avg_leadtime_nama = avg_leadtime_nama.dropna(subset=['avg_monthly_leadtime'])
|
| 738 |
+
return avg_leadtime_nama
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
|
| 741 |
+
# ─── Helper: Hitung lead time per individu executor (deteksi kolom otomatis) ─
|
| 742 |
+
def compute_executor_leadtime_by_individual(df, name_col='creator_name'):
|
| 743 |
+
if name_col not in df.columns or 'days_to_close' not in df.columns:
|
| 744 |
+
return pd.DataFrame()
|
| 745 |
+
|
| 746 |
+
df_valid = df[df['days_to_close'].notna() & (df['days_to_close'] >= 0)]
|
| 747 |
+
|
| 748 |
+
leadtime_by_indiv_month = df_valid.groupby(['created_month', name_col])['days_to_close'].mean().reset_index(name='avg_leadtime')
|
| 749 |
+
avg_leadtime_indiv = leadtime_by_indiv_month.groupby(name_col)['avg_leadtime'].mean().reset_index(name='avg_monthly_leadtime')
|
| 750 |
+
avg_leadtime_indiv = avg_leadtime_indiv.dropna(subset=['avg_monthly_leadtime'])
|
| 751 |
+
return avg_leadtime_indiv
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
# ─── Deteksi kolom executor individu ────────────────────────────────────────
|
| 755 |
+
EXECUTOR_INDIV_COL = None
|
| 756 |
+
candidate_executor_cols = ['pic', 'pic_name', 'responsible', 'responsible_name', 'assigned_to', 'closed_by', 'executor_name', 'executor']
|
| 757 |
+
for col in candidate_executor_cols:
|
| 758 |
+
if col in df_local.columns:
|
| 759 |
+
EXECUTOR_INDIV_COL = col
|
| 760 |
+
break
|
| 761 |
+
|
| 762 |
+
if EXECUTOR_INDIV_COL is None:
|
| 763 |
+
# Fallback — gunakan creator_name (dengan warning transparan)
|
| 764 |
+
EXECUTOR_INDIV_COL = 'creator_name'
|
| 765 |
+
st.warning(
|
| 766 |
+
"⚠️ No dedicated executor column (e.g., 'pic', 'responsible') found. "
|
| 767 |
+
"Using 'creator_name' as proxy for executor — insights may conflate reporters & executors. "
|
| 768 |
+
"Consider adding an executor identifier column for accuracy."
|
| 769 |
+
)
|
| 770 |
|
|
|
|
|
|
|
|
|
|
| 771 |
|
| 772 |
+
# ─── Hitung semua metrik ─────────────────────────────────────────────────────
|
| 773 |
+
avg_ratio_per_nama = compute_reporter_ratio_by_nama(df_local) # 3a
|
| 774 |
+
avg_rate_per_creator = compute_reporter_rate_by_creator(df_local) # 3c
|
| 775 |
+
avg_leadtime_nama = compute_executor_leadtime_by_nama(df_local) # 3b
|
| 776 |
+
avg_leadtime_per_indiv = compute_executor_leadtime_by_individual(df_local, name_col=EXECUTOR_INDIV_COL) # 3d
|
| 777 |
|
| 778 |
+
|
| 779 |
+
# ─── Helper: Warna berdasarkan ranking global ───────────────────────────────
|
| 780 |
def add_color_by_global_rank(df, value_col, top_n=5, worst_n=5, high_is_good=True):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
df = df.copy()
|
|
|
|
| 782 |
df['color'] = '#1f77b4' # default biru
|
| 783 |
|
| 784 |
+
if len(df) == 0:
|
| 785 |
+
return df
|
| 786 |
+
|
| 787 |
if high_is_good:
|
| 788 |
+
# Nilai tinggi = baik → top N → hijau
|
| 789 |
+
top_names = df.nlargest(top_n, value_col)['nama' if 'nama' in df.columns else df.columns[0]]
|
| 790 |
+
df.loc[df[df.columns[0]].isin(top_names), 'color'] = '#4CAF50'
|
| 791 |
else:
|
| 792 |
+
# Nilai tinggi = buruk (e.g., lead time) → worst N (tertinggi) → merah
|
| 793 |
+
worst_names = df.nlargest(worst_n, value_col)['nama' if 'nama' in df.columns else df.columns[0]]
|
| 794 |
+
df.loc[df[df.columns[0]].isin(worst_names), 'color'] = '#D32F2F'
|
| 795 |
+
|
|
|
|
| 796 |
return df
|
| 797 |
|
| 798 |
|
| 799 |
+
# ─── Layout: 2 baris × 2 kolom ───────────────────────────────────────────────
|
|
|
|
| 800 |
col_3a, col_3c = st.columns(2)
|
| 801 |
|
| 802 |
+
# ─── 3a: Reporter by Division (Rasio Temuan/Orang) ───────────────────────────
|
| 803 |
with col_3a:
|
| 804 |
+
st.markdown("<h5 style='text-align:center;'>3a. Avg Finding/Person Ratio by Division (Reporter)</h5>", unsafe_allow_html=True)
|
| 805 |
if avg_ratio_per_nama.empty:
|
| 806 |
+
st.warning("No data for division-level reporter analysis.")
|
| 807 |
else:
|
| 808 |
+
sort_opt = st.selectbox("Show:", ["Top 10", "Bottom 10"], key='sort_3a')
|
| 809 |
|
| 810 |
+
full_sorted = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False)
|
| 811 |
+
subset = full_sorted.head(10) if sort_opt == "Top 10" else full_sorted.tail(10).sort_values('avg_monthly_ratio', ascending=True)
|
| 812 |
|
| 813 |
+
# Tambahkan warna: top 5 → hijau
|
| 814 |
+
colored = add_color_by_global_rank(avg_ratio_per_nama, 'avg_monthly_ratio', top_n=5, high_is_good=True)
|
| 815 |
+
subset = subset.merge(colored[['nama', 'color']], on='nama', how='left').fillna({'color': '#1f77b4'})
|
|
|
|
|
|
|
| 816 |
|
| 817 |
+
# Reverse untuk visual (tertinggi di atas)
|
| 818 |
+
if sort_opt == "Top 10":
|
| 819 |
+
subset = subset.iloc[::-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
|
| 821 |
+
fig = px.bar(
|
| 822 |
+
subset, x='avg_monthly_ratio', y='nama', orientation='h',
|
| 823 |
+
title=f'{sort_opt} Divisions',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
labels={'avg_monthly_ratio': 'Avg Monthly Ratio', 'nama': 'Division'},
|
| 825 |
+
color='color', color_discrete_map={c: c for c in subset['color'].unique()},
|
| 826 |
+
text=subset['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
|
|
|
|
| 827 |
)
|
| 828 |
+
fig.update_layout(height=450, showlegend=False, yaxis={'categoryorder': 'array', 'categoryarray': subset['nama'].tolist()})
|
| 829 |
+
fig.update_traces(textposition='auto')
|
| 830 |
+
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
|
| 832 |
+
# 🔍 Insight (dari full data)
|
| 833 |
+
if len(full_sorted) >= 2:
|
| 834 |
+
min_r, max_r, mean_r = full_sorted['avg_monthly_ratio'].min(), full_sorted['avg_monthly_ratio'].max(), full_sorted['avg_monthly_ratio'].mean()
|
| 835 |
+
best, worst = full_sorted.iloc[0]['nama'], full_sorted.iloc[-1]['nama']
|
| 836 |
+
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
f"<div class='ai-insight'>"
|
| 838 |
+
f"<strong>Insight:</strong> Division reporting efficiency ranges from {min_r:.2f} to {max_r:.2f} (avg: {mean_r:.2f}). "
|
| 839 |
+
f"<strong>{best}</strong> leads; <strong>{worst}</strong> lags. "
|
| 840 |
+
f"<strong>Recommendation:</strong> Benchmark processes from {best}; assess capacity/tooling gaps in {worst}."
|
| 841 |
+
f"</div>",
|
| 842 |
+
unsafe_allow_html=True
|
| 843 |
)
|
|
|
|
|
|
|
|
|
|
| 844 |
|
| 845 |
|
| 846 |
+
# ─── 3c: Reporter by Individual ──────────────────────────────────────────────
|
| 847 |
with col_3c:
|
| 848 |
+
st.markdown("<h5 style='text-align:center;'>3c. Avg Monthly Findings per Reporter (Individual)</h5>", unsafe_allow_html=True)
|
| 849 |
if avg_rate_per_creator.empty:
|
| 850 |
+
st.warning("No data for individual reporter analysis.")
|
| 851 |
else:
|
| 852 |
+
sort_opt = st.selectbox("Show:", ["Top 10", "Bottom 10"], key='sort_3c')
|
| 853 |
|
| 854 |
+
full_sorted = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False)
|
| 855 |
+
subset = full_sorted.head(10) if sort_opt == "Top 10" else full_sorted.tail(10).sort_values('avg_monthly_rate', ascending=True)
|
| 856 |
|
| 857 |
+
colored = add_color_by_global_rank(avg_rate_per_creator, 'avg_monthly_rate', top_n=5, high_is_good=True)
|
| 858 |
+
subset = subset.merge(colored[['creator_name', 'color']], on='creator_name', how='left').fillna({'color': '#1f77b4'})
|
|
|
|
|
|
|
| 859 |
|
| 860 |
+
if sort_opt == "Top 10":
|
| 861 |
+
subset = subset.iloc[::-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
|
| 863 |
+
fig = px.bar(
|
| 864 |
+
subset, x='avg_monthly_rate', y='creator_name', orientation='h',
|
| 865 |
+
title=f'{sort_opt} Reporters',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
labels={'avg_monthly_rate': 'Avg Monthly Findings', 'creator_name': 'Reporter'},
|
| 867 |
+
color='color', color_discrete_map={c: c for c in subset['color'].unique()},
|
| 868 |
+
text=subset['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
)
|
| 870 |
+
fig.update_layout(height=450, showlegend=False, yaxis={'categoryorder': 'array', 'categoryarray': subset['creator_name'].tolist()})
|
| 871 |
+
fig.update_traces(textposition='auto')
|
| 872 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 873 |
|
| 874 |
+
if len(full_sorted) >= 2:
|
| 875 |
+
min_r, max_r, mean_r = full_sorted['avg_monthly_rate'].min(), full_sorted['avg_monthly_rate'].max(), full_sorted['avg_monthly_rate'].mean()
|
| 876 |
+
top_reporter = full_sorted.iloc[0]['creator_name']
|
| 877 |
+
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
f"<div class='ai-insight'>"
|
| 879 |
+
f"<strong>Insight:</strong> Individual reporting ranges from {min_r:.2f} to {max_r:.2f} findings/month (avg: {mean_r:.2f}). "
|
| 880 |
+
f"<strong>{top_reporter}</strong> is the most active contributor. "
|
| 881 |
+
f"<strong>Recommendation:</strong> Recognize top reporters; investigate causes of low activity (<0.5/month) via 1:1 review."
|
| 882 |
+
f"</div>",
|
| 883 |
+
unsafe_allow_html=True
|
| 884 |
)
|
|
|
|
|
|
|
|
|
|
| 885 |
|
| 886 |
|
| 887 |
+
# ─── Baris 2: Executor ─────────���─────────────────────────────────────────────
|
| 888 |
col_3b, col_3d = st.columns(2)
|
| 889 |
|
| 890 |
+
# ─── 3b: Executor by Division (Lead Time) ────────────────────────────────────
|
| 891 |
with col_3b:
|
| 892 |
+
st.markdown("<h5 style='text-align:center;'>3b. Avg Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
|
| 893 |
if avg_leadtime_nama.empty:
|
| 894 |
+
st.warning("No data for division-level executor analysis.")
|
| 895 |
else:
|
| 896 |
+
sort_opt = st.selectbox("Show:", ["Fastest 10", "Slowest 10"], key='sort_3b')
|
|
|
|
|
|
|
|
|
|
| 897 |
|
| 898 |
+
full_sorted = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=True) # cepat → lambat
|
| 899 |
+
if sort_opt == "Fastest 10":
|
| 900 |
+
subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=False) # descending dalam subset (cepat di bawah)
|
| 901 |
else:
|
| 902 |
+
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False) # lambat di atas
|
|
|
|
| 903 |
|
| 904 |
+
# Warna: 5 terlama → merah
|
| 905 |
+
colored = add_color_by_global_rank(avg_leadtime_nama, 'avg_monthly_leadtime', worst_n=5, high_is_good=False)
|
| 906 |
+
subset = subset.merge(colored[['nama', 'color']], on='nama', how='left').fillna({'color': '#1f77b4'})
|
| 907 |
+
|
| 908 |
+
# Reverse agar Slowest 10: tertinggi di atas
|
| 909 |
+
if sort_opt == "Slowest 10":
|
| 910 |
+
subset = subset.iloc[::-1]
|
| 911 |
|
| 912 |
+
fig = px.bar(
|
| 913 |
+
subset, x='avg_monthly_leadtime', y='nama', orientation='h',
|
| 914 |
+
title=f'{sort_opt}',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 915 |
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
|
| 916 |
+
color='color', color_discrete_map={c: c for c in subset['color'].unique()},
|
| 917 |
+
text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 918 |
)
|
| 919 |
+
fig.update_layout(height=450, showlegend=False, yaxis={'categoryorder': 'array', 'categoryarray': subset['nama'].tolist()})
|
| 920 |
+
fig.update_traces(textposition='auto')
|
| 921 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 922 |
|
| 923 |
+
if len(full_sorted) >= 2:
|
| 924 |
+
min_lt, max_lt, mean_lt = full_sorted['avg_monthly_leadtime'].min(), full_sorted['avg_monthly_leadtime'].max(), full_sorted['avg_monthly_leadtime'].mean()
|
| 925 |
+
fastest, slowest = full_sorted.iloc[0]['nama'], full_sorted.iloc[-1]['nama']
|
| 926 |
+
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
f"<div class='ai-insight'>"
|
| 928 |
+
f"<strong>Insight:</strong> Resolution time ranges from {min_lt:.1f} to {max_lt:.1f} days (avg: {mean_lt:.1f}). "
|
| 929 |
+
f"<strong>{slowest}</strong> has highest risk of SLA breach. "
|
| 930 |
+
f"<strong>Recommendation:</strong> Initiate RCA for {slowest}; replicate workflow from {fastest}. Set SLA threshold at 7 days."
|
| 931 |
+
f"</div>",
|
| 932 |
+
unsafe_allow_html=True
|
| 933 |
)
|
|
|
|
|
|
|
|
|
|
| 934 |
|
| 935 |
|
| 936 |
+
# ─── 3d: Executor by Individual ──────────────────────────────────────────────
|
| 937 |
with col_3d:
|
| 938 |
+
st.markdown(f"<h5 style='text-align:center;'>3d. Avg Lead Time per Executor ({EXECUTOR_INDIV_COL})</h5>", unsafe_allow_html=True)
|
| 939 |
+
if avg_leadtime_per_indiv.empty:
|
| 940 |
+
st.warning(f"No data for individual executor analysis (column: '{EXECUTOR_INDIV_COL}').")
|
| 941 |
else:
|
| 942 |
+
sort_opt = st.selectbox("Show:", ["Fastest 10", "Slowest 10"], key='sort_3d')
|
|
|
|
|
|
|
| 943 |
|
| 944 |
+
full_sorted = avg_leadtime_per_indiv.sort_values('avg_monthly_leadtime', ascending=True)
|
| 945 |
+
if sort_opt == "Fastest 10":
|
| 946 |
+
subset = full_sorted.head(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 947 |
else:
|
| 948 |
+
subset = full_sorted.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 949 |
|
| 950 |
+
# Warna: 5 terlama → merah
|
| 951 |
+
colored = add_color_by_global_rank(avg_leadtime_per_indiv, 'avg_monthly_leadtime', worst_n=5, high_is_good=False)
|
| 952 |
+
id_col = EXECUTOR_INDIV_COL
|
| 953 |
+
subset = subset.merge(colored[[id_col, 'color']], on=id_col, how='left').fillna({'color': '#1f77b4'})
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
+
if sort_opt == "Slowest 10":
|
| 956 |
+
subset = subset.iloc[::-1]
|
| 957 |
+
|
| 958 |
+
fig = px.bar(
|
| 959 |
+
subset, x='avg_monthly_leadtime', y=id_col, orientation='h',
|
| 960 |
+
title=f'{sort_opt}',
|
| 961 |
+
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', id_col: 'Executor'},
|
| 962 |
+
color='color', color_discrete_map={c: c for c in subset['color'].unique()},
|
| 963 |
+
text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
)
|
| 965 |
+
fig.update_layout(height=450, showlegend=False, yaxis={'categoryorder': 'array', 'categoryarray': subset[id_col].tolist()})
|
| 966 |
+
fig.update_traces(textposition='auto')
|
| 967 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 968 |
|
| 969 |
+
if len(full_sorted) >= 2:
|
| 970 |
+
min_lt, max_lt, mean_lt = full_sorted['avg_monthly_leadtime'].min(), full_sorted['avg_monthly_leadtime'].max(), full_sorted['avg_monthly_leadtime'].mean()
|
| 971 |
+
slowest_exec = full_sorted.iloc[-1][id_col]
|
| 972 |
+
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
f"<div class='ai-insight'>"
|
| 974 |
+
f"<strong>Insight:</strong> Executor performance ranges from {min_lt:.1f} to {max_lt:.1f} days (avg: {mean_lt:.1f}). "
|
| 975 |
+
f"<strong>{slowest_exec}</strong> requires support to meet SLA. "
|
| 976 |
+
f"<strong>Recommendation:</strong> Assign mentor to executors >7 days; document & share best practices from top performers."
|
| 977 |
+
f"</div>",
|
| 978 |
+
unsafe_allow_html=True
|
| 979 |
)
|
|
|
|
|
|
|
|
|
|
| 980 |
|
| 981 |
+
#Objective 4
|
| 982 |
try:
|
| 983 |
from wordcloud import WordCloud
|
| 984 |
import matplotlib.pyplot as plt
|