Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -790,6 +790,30 @@ avg_leadtime_nama = compute_executor_leadtime_by_nama(df_local)
|
|
| 790 |
avg_leadtime_per_executor = compute_executor_leadtime_by_pic(df_local)
|
| 791 |
|
| 792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
# ─── Layout: 2 Baris — 3a & 3c di baris pertama, 3b & 3d di baris kedua ─────
|
| 794 |
# Baris 1: 3a & 3c
|
| 795 |
col_3a, col_3c = st.columns(2)
|
|
@@ -801,123 +825,133 @@ with col_3a:
|
|
| 801 |
else:
|
| 802 |
sort_option_3a = st.selectbox("Show 3a:", ["Top 10", "Bottom 10"], key='sort_3a')
|
| 803 |
|
| 804 |
-
#
|
| 805 |
-
|
| 806 |
|
| 807 |
-
#
|
| 808 |
if sort_option_3a == "Top 10":
|
| 809 |
-
|
| 810 |
-
else:
|
| 811 |
-
|
| 812 |
|
| 813 |
-
#
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
top_5_indices = sorted_data.head(5).index
|
| 821 |
-
sorted_data.loc[top_5_indices, 'color'] = '#4CAF50'
|
| 822 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 823 |
fig_rep_nama = px.bar(
|
| 824 |
-
|
| 825 |
x='avg_monthly_ratio',
|
| 826 |
y='nama',
|
| 827 |
orientation='h',
|
| 828 |
-
title='Avg Monthly Finding
|
| 829 |
-
labels={'avg_monthly_ratio': 'Avg Monthly
|
| 830 |
color='color',
|
| 831 |
-
color_discrete_map={c: c for c in
|
| 832 |
-
text=
|
| 833 |
)
|
| 834 |
-
# 🔥 Atur urutan Y-axis sesuai data yang ditampilkan
|
| 835 |
fig_rep_nama.update_layout(
|
| 836 |
-
yaxis={
|
| 837 |
-
'categoryorder': 'array',
|
| 838 |
-
'categoryarray': sorted_data['nama'].tolist()
|
| 839 |
-
},
|
| 840 |
height=500,
|
| 841 |
showlegend=False
|
| 842 |
)
|
| 843 |
fig_rep_nama.update_traces(textposition='auto')
|
| 844 |
st.plotly_chart(fig_rep_nama, use_container_width=True)
|
| 845 |
|
| 846 |
-
#
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 859 |
|
|
|
|
| 860 |
with col_3c:
|
| 861 |
-
st.markdown("<h5>
|
| 862 |
if avg_rate_per_creator.empty:
|
| 863 |
st.warning("No data for reporter analysis by creator_name.")
|
| 864 |
else:
|
| 865 |
-
sort_option_3c = st.selectbox("Show
|
| 866 |
|
| 867 |
-
|
| 868 |
-
sorted_data_all = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False)
|
| 869 |
|
| 870 |
-
# Ambil Top 10 atau Bottom 10
|
| 871 |
if sort_option_3c == "Top 10":
|
| 872 |
-
|
| 873 |
else:
|
| 874 |
-
|
| 875 |
|
| 876 |
-
#
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
top_5_indices = sorted_data.head(5).index
|
| 884 |
-
sorted_data.loc[top_5_indices, 'color'] = '#4CAF50'
|
| 885 |
|
|
|
|
|
|
|
|
|
|
| 886 |
fig_rep_creator = px.bar(
|
| 887 |
-
|
| 888 |
x='avg_monthly_rate',
|
| 889 |
y='creator_name',
|
| 890 |
orientation='h',
|
| 891 |
-
title='Avg Monthly Finding
|
| 892 |
-
labels={'avg_monthly_rate': 'Avg Monthly
|
| 893 |
color='color',
|
| 894 |
-
color_discrete_map={c: c for c in
|
| 895 |
-
text=
|
| 896 |
)
|
| 897 |
-
# 🔥 Atur urutan Y-axis sesuai data yang ditampilkan
|
| 898 |
fig_rep_creator.update_layout(
|
| 899 |
-
yaxis={
|
| 900 |
-
'categoryorder': 'array',
|
| 901 |
-
'categoryarray': sorted_data['creator_name'].tolist()
|
| 902 |
-
},
|
| 903 |
height=500,
|
| 904 |
showlegend=False
|
| 905 |
)
|
| 906 |
fig_rep_creator.update_traces(textposition='auto')
|
| 907 |
st.plotly_chart(fig_rep_creator, use_container_width=True)
|
| 908 |
|
| 909 |
-
#
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 922 |
|
| 923 |
|
|
@@ -925,66 +959,75 @@ with col_3c:
|
|
| 925 |
col_3b, col_3d = st.columns(2)
|
| 926 |
|
| 927 |
with col_3b:
|
| 928 |
-
st.markdown("<h5>
|
| 929 |
if avg_leadtime_nama.empty:
|
| 930 |
st.warning("No data for executor analysis by division.")
|
| 931 |
else:
|
| 932 |
-
sort_option_3b = st.selectbox("Show
|
| 933 |
|
| 934 |
-
#
|
| 935 |
-
|
| 936 |
|
| 937 |
-
# Ambil Top 10 atau Bottom 10
|
| 938 |
if sort_option_3b == "Top 10":
|
| 939 |
-
|
|
|
|
| 940 |
else:
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
# 🔥 Urutkan data yang ditampilkan dari besar ke kecil (jika Bottom 10, tetap besar ke kecil)
|
| 944 |
-
sorted_data = sorted_data.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
|
| 945 |
|
| 946 |
-
#
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
|
|
|
|
|
|
|
|
|
| 950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
fig_exec_nama = px.bar(
|
| 952 |
-
|
| 953 |
x='avg_monthly_leadtime',
|
| 954 |
y='nama',
|
| 955 |
orientation='h',
|
| 956 |
-
title='Avg Monthly Lead Time
|
| 957 |
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
|
| 958 |
color='color',
|
| 959 |
-
color_discrete_map={c: c for c in
|
| 960 |
-
text=
|
| 961 |
)
|
| 962 |
-
# 🔥 Atur urutan Y-axis sesuai data yang ditampilkan
|
| 963 |
fig_exec_nama.update_layout(
|
| 964 |
-
yaxis={
|
| 965 |
-
'categoryorder': 'array',
|
| 966 |
-
'categoryarray': sorted_data['nama'].tolist()
|
| 967 |
-
},
|
| 968 |
height=500,
|
| 969 |
showlegend=False
|
| 970 |
)
|
| 971 |
fig_exec_nama.update_traces(textposition='auto')
|
| 972 |
st.plotly_chart(fig_exec_nama, use_container_width=True)
|
| 973 |
|
| 974 |
-
#
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 987 |
|
|
|
|
| 988 |
with col_3d:
|
| 989 |
st.markdown("<h5>3d. Average Lead Time by Executor (Name)</h5>", unsafe_allow_html=True)
|
| 990 |
if avg_leadtime_per_executor.empty:
|
|
@@ -992,60 +1035,66 @@ with col_3d:
|
|
| 992 |
else:
|
| 993 |
sort_option_3d = st.selectbox("Show 3d:", ["Top 10", "Bottom 10"], key='sort_3d')
|
| 994 |
|
| 995 |
-
|
| 996 |
-
sorted_data_all = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=True)
|
| 997 |
|
| 998 |
-
# Ambil Top 10 atau Bottom 10
|
| 999 |
if sort_option_3d == "Top 10":
|
| 1000 |
-
|
| 1001 |
else:
|
| 1002 |
-
|
| 1003 |
|
| 1004 |
-
#
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
|
|
|
|
|
|
|
|
|
|
| 1012 |
fig_exec_pic = px.bar(
|
| 1013 |
-
|
| 1014 |
x='avg_monthly_leadtime',
|
| 1015 |
y='nama_pic',
|
| 1016 |
orientation='h',
|
| 1017 |
-
title='Avg Monthly Lead Time
|
| 1018 |
-
labels={'avg_monthly_leadtime': 'Avg
|
| 1019 |
color='color',
|
| 1020 |
-
color_discrete_map={c: c for c in
|
| 1021 |
-
text=
|
| 1022 |
)
|
| 1023 |
-
# 🔥 Atur urutan Y-axis sesuai data yang ditampilkan
|
| 1024 |
fig_exec_pic.update_layout(
|
| 1025 |
-
yaxis={
|
| 1026 |
-
'categoryorder': 'array',
|
| 1027 |
-
'categoryarray': sorted_data['nama_pic'].tolist()
|
| 1028 |
-
},
|
| 1029 |
height=500,
|
| 1030 |
showlegend=False
|
| 1031 |
)
|
| 1032 |
fig_exec_pic.update_traces(textposition='auto')
|
| 1033 |
st.plotly_chart(fig_exec_pic, use_container_width=True)
|
| 1034 |
|
| 1035 |
-
#
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1048 |
|
|
|
|
|
|
|
| 1049 |
try:
|
| 1050 |
from wordcloud import WordCloud
|
| 1051 |
import matplotlib.pyplot as plt
|
|
|
|
| 790 |
avg_leadtime_per_executor = compute_executor_leadtime_by_pic(df_local)
|
| 791 |
|
| 792 |
|
| 793 |
+
# Helper: Dapatkan warna berdasarkan ranking global (bukan lokal subset)
|
| 794 |
+
def add_color_by_global_rank(df, value_col, top_n=5, worst_n=5, high_is_good=True):
|
| 795 |
+
"""
|
| 796 |
+
Menambahkan kolom 'color' berdasarkan ranking global.
|
| 797 |
+
- Jika high_is_good=True (e.g., ratio): top_n → hijau, worst_n → default
|
| 798 |
+
- Jika high_is_good=False (e.g., lead time): worst_n (tertinggi) → merah, top_n (terendah) → default
|
| 799 |
+
"""
|
| 800 |
+
df = df.copy()
|
| 801 |
+
df = df.sort_values(value_col, ascending=not high_is_good).reset_index(drop=True) # descending if high_is_good
|
| 802 |
+
df['color'] = '#1f77b4' # default biru
|
| 803 |
+
|
| 804 |
+
if high_is_good:
|
| 805 |
+
# Nilai tinggi = baik → top 5 hijau
|
| 806 |
+
top_idx = df.head(top_n).index
|
| 807 |
+
df.loc[top_idx, 'color'] = '#4CAF50'
|
| 808 |
+
else:
|
| 809 |
+
# Nilai tinggi = buruk → top 5 (tertinggi) = merah
|
| 810 |
+
# → Urut ascending, ambil tail 5 (tertinggi)
|
| 811 |
+
df_asc = df.sort_values(value_col, ascending=True)
|
| 812 |
+
worst_idx = df_asc.tail(worst_n).index
|
| 813 |
+
df.loc[worst_idx, 'color'] = '#D32F2F'
|
| 814 |
+
return df
|
| 815 |
+
|
| 816 |
+
|
| 817 |
# ─── Layout: 2 Baris — 3a & 3c di baris pertama, 3b & 3d di baris kedua ─────
|
| 818 |
# Baris 1: 3a & 3c
|
| 819 |
col_3a, col_3c = st.columns(2)
|
|
|
|
| 825 |
else:
|
| 826 |
sort_option_3a = st.selectbox("Show 3a:", ["Top 10", "Bottom 10"], key='sort_3a')
|
| 827 |
|
| 828 |
+
# Full data sorted descending (high ratio = good)
|
| 829 |
+
sorted_all_3a = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False).reset_index(drop=True)
|
| 830 |
|
| 831 |
+
# Subset sesuai pilihan user
|
| 832 |
if sort_option_3a == "Top 10":
|
| 833 |
+
subset_data = sorted_all_3a.head(10)
|
| 834 |
+
else: # Bottom 10
|
| 835 |
+
subset_data = sorted_all_3a.tail(10).sort_values('avg_monthly_ratio', ascending=True) # ascending dalam subset
|
| 836 |
|
| 837 |
+
# Tambahkan warna berdasarkan rank global
|
| 838 |
+
avg_ratio_per_nama_colored = add_color_by_global_rank(
|
| 839 |
+
avg_ratio_per_nama, 'avg_monthly_ratio', top_n=5, high_is_good=True
|
| 840 |
+
)
|
| 841 |
+
subset_data = subset_data.merge(
|
| 842 |
+
avg_ratio_per_nama_colored[['nama', 'color']], on='nama', how='left'
|
| 843 |
+
).fillna({'color': '#1f77b4'})
|
|
|
|
|
|
|
| 844 |
|
| 845 |
+
# Reverse order for better visual (low at bottom)
|
| 846 |
+
if sort_option_3a == "Top 10":
|
| 847 |
+
subset_data = subset_data.iloc[::-1] # descending (tertinggi di atas)
|
| 848 |
+
else:
|
| 849 |
+
subset_data = subset_data # ascending (terendah di atas)
|
| 850 |
+
|
| 851 |
fig_rep_nama = px.bar(
|
| 852 |
+
subset_data,
|
| 853 |
x='avg_monthly_ratio',
|
| 854 |
y='nama',
|
| 855 |
orientation='h',
|
| 856 |
+
title=f'Avg Monthly Finding/Person Ratio — {sort_option_3a}',
|
| 857 |
+
labels={'avg_monthly_ratio': 'Avg Monthly Ratio', 'nama': 'Division'},
|
| 858 |
color='color',
|
| 859 |
+
color_discrete_map={c: c for c in subset_data['color'].unique()},
|
| 860 |
+
text=subset_data['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
|
| 861 |
)
|
|
|
|
| 862 |
fig_rep_nama.update_layout(
|
| 863 |
+
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama'].tolist()},
|
|
|
|
|
|
|
|
|
|
| 864 |
height=500,
|
| 865 |
showlegend=False
|
| 866 |
)
|
| 867 |
fig_rep_nama.update_traces(textposition='auto')
|
| 868 |
st.plotly_chart(fig_rep_nama, use_container_width=True)
|
| 869 |
|
| 870 |
+
# 🔥 INSIGHT: SELALU dari FULL DATA, stabil
|
| 871 |
+
if len(sorted_all_3a) >= 2:
|
| 872 |
+
min_val = sorted_all_3a['avg_monthly_ratio'].min()
|
| 873 |
+
max_val = sorted_all_3a['avg_monthly_ratio'].max()
|
| 874 |
+
mean_val = sorted_all_3a['avg_monthly_ratio'].mean()
|
| 875 |
+
median_val = sorted_all_3a['avg_monthly_ratio'].median()
|
| 876 |
+
best_div = sorted_all_3a.iloc[0]['nama']
|
| 877 |
+
worst_div = sorted_all_3a.iloc[-1]['nama']
|
| 878 |
+
|
| 879 |
+
insight_text = (
|
| 880 |
+
f"<div class='ai-insight'>"
|
| 881 |
+
f"<strong>Overall (all {len(sorted_all_3a)} divisions)</strong>: Ratio ranges from <strong>{min_val:.2f}</strong> "
|
| 882 |
+
f"to <strong>{max_val:.2f}</strong> (mean: <strong>{mean_val:.2f}</strong>, median: <strong>{median_val:.2f}</strong>). "
|
| 883 |
+
f"<strong>{best_div}</strong> is the most active division (highest ratio), while <strong>{worst_div}</strong> is the least. "
|
| 884 |
+
f"<strong>Recommendation:</strong> Investigate root causes in low-activity divisions (e.g., training, tool access, workload); replicate workflows from top performers like {best_div}."
|
| 885 |
+
f"</div>"
|
| 886 |
+
)
|
| 887 |
+
else:
|
| 888 |
+
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 889 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 890 |
|
| 891 |
+
|
| 892 |
with col_3c:
|
| 893 |
+
st.markdown("<h5>3c. Average Finding Rate per Reporter (Name)</h5>", unsafe_allow_html=True)
|
| 894 |
if avg_rate_per_creator.empty:
|
| 895 |
st.warning("No data for reporter analysis by creator_name.")
|
| 896 |
else:
|
| 897 |
+
sort_option_3c = st.selectbox("Show 3c:", ["Top 10", "Bottom 10"], key='sort_3c')
|
| 898 |
|
| 899 |
+
sorted_all_3c = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False).reset_index(drop=True)
|
|
|
|
| 900 |
|
|
|
|
| 901 |
if sort_option_3c == "Top 10":
|
| 902 |
+
subset_data = sorted_all_3c.head(10)
|
| 903 |
else:
|
| 904 |
+
subset_data = sorted_all_3c.tail(10).sort_values('avg_monthly_rate', ascending=True)
|
| 905 |
|
| 906 |
+
# Warna global: top 5 tertinggi → hijau
|
| 907 |
+
avg_rate_per_creator_colored = add_color_by_global_rank(
|
| 908 |
+
avg_rate_per_creator, 'avg_monthly_rate', top_n=5, high_is_good=True
|
| 909 |
+
)
|
| 910 |
+
subset_data = subset_data.merge(
|
| 911 |
+
avg_rate_per_creator_colored[['creator_name', 'color']], on='creator_name', how='left'
|
| 912 |
+
).fillna({'color': '#1f77b4'})
|
|
|
|
|
|
|
| 913 |
|
| 914 |
+
if sort_option_3c == "Top 10":
|
| 915 |
+
subset_data = subset_data.iloc[::-1]
|
| 916 |
+
|
| 917 |
fig_rep_creator = px.bar(
|
| 918 |
+
subset_data,
|
| 919 |
x='avg_monthly_rate',
|
| 920 |
y='creator_name',
|
| 921 |
orientation='h',
|
| 922 |
+
title=f'Avg Monthly Finding Rate — {sort_option_3c}',
|
| 923 |
+
labels={'avg_monthly_rate': 'Avg Monthly Findings', 'creator_name': 'Reporter'},
|
| 924 |
color='color',
|
| 925 |
+
color_discrete_map={c: c for c in subset_data['color'].unique()},
|
| 926 |
+
text=subset_data['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
|
| 927 |
)
|
|
|
|
| 928 |
fig_rep_creator.update_layout(
|
| 929 |
+
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['creator_name'].tolist()},
|
|
|
|
|
|
|
|
|
|
| 930 |
height=500,
|
| 931 |
showlegend=False
|
| 932 |
)
|
| 933 |
fig_rep_creator.update_traces(textposition='auto')
|
| 934 |
st.plotly_chart(fig_rep_creator, use_container_width=True)
|
| 935 |
|
| 936 |
+
# 🔥 INSIGHT: dari FULL DATA
|
| 937 |
+
if len(sorted_all_3c) >= 2:
|
| 938 |
+
min_val = sorted_all_3c['avg_monthly_rate'].min()
|
| 939 |
+
max_val = sorted_all_3c['avg_monthly_rate'].max()
|
| 940 |
+
mean_val = sorted_all_3c['avg_monthly_rate'].mean()
|
| 941 |
+
median_val = sorted_all_3c['avg_monthly_rate'].median()
|
| 942 |
+
best_reporter = sorted_all_3c.iloc[0]['creator_name']
|
| 943 |
+
worst_reporter = sorted_all_3c.iloc[-1]['creator_name']
|
| 944 |
+
|
| 945 |
+
insight_text = (
|
| 946 |
+
f"<div class='ai-insight'>"
|
| 947 |
+
f"<strong>Overall (all {len(sorted_all_3c)} reporters)</strong>: Monthly rate ranges from <strong>{min_val:.2f}</strong> "
|
| 948 |
+
f"to <strong>{max_val:.2f}</strong> (mean: <strong>{mean_val:.2f}</strong>). "
|
| 949 |
+
f"<strong>{best_reporter}</strong> is the top reporter; <strong>{worst_reporter}</strong> reports least frequently. "
|
| 950 |
+
f"<strong>Recommendation:</strong> Conduct 1:1 coaching for reporters with <0.5 findings/month; recognize top contributors publicly to motivate peers."
|
| 951 |
+
f"</div>"
|
| 952 |
+
)
|
| 953 |
+
else:
|
| 954 |
+
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 955 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 956 |
|
| 957 |
|
|
|
|
| 959 |
col_3b, col_3d = st.columns(2)
|
| 960 |
|
| 961 |
with col_3b:
|
| 962 |
+
st.markdown("<h5>3b. Average Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
|
| 963 |
if avg_leadtime_nama.empty:
|
| 964 |
st.warning("No data for executor analysis by division.")
|
| 965 |
else:
|
| 966 |
+
sort_option_3b = st.selectbox("Show 3b:", ["Top 10", "Bottom 10"], key='sort_3b')
|
| 967 |
|
| 968 |
+
# Full data: ascending (low = fast = good)
|
| 969 |
+
sorted_all_3b = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
|
| 970 |
|
|
|
|
| 971 |
if sort_option_3b == "Top 10":
|
| 972 |
+
# Top 10 tercepat (terendah)
|
| 973 |
+
subset_data = sorted_all_3b.head(10)
|
| 974 |
else:
|
| 975 |
+
# Bottom 10 = terlama (tertinggi)
|
| 976 |
+
subset_data = sorted_all_3b.tail(10).sort_values('avg_monthly_leadtime', ascending=False) # descending dalam subset
|
|
|
|
|
|
|
| 977 |
|
| 978 |
+
# Warna global: 5 division dengan lead time TERPANJANG → merah
|
| 979 |
+
avg_leadtime_nama_colored = add_color_by_global_rank(
|
| 980 |
+
avg_leadtime_nama, 'avg_monthly_leadtime', worst_n=5, high_is_good=False
|
| 981 |
+
)
|
| 982 |
+
subset_data = subset_data.merge(
|
| 983 |
+
avg_leadtime_nama_colored[['nama', 'color']], on='nama', how='left'
|
| 984 |
+
).fillna({'color': '#1f77b4'})
|
| 985 |
|
| 986 |
+
# Reverse untuk visual yang intuitif (lama di atas)
|
| 987 |
+
if sort_option_3b == "Bottom 10":
|
| 988 |
+
subset_data = subset_data.iloc[::-1] # biar tertinggi di atas
|
| 989 |
+
|
| 990 |
fig_exec_nama = px.bar(
|
| 991 |
+
subset_data,
|
| 992 |
x='avg_monthly_leadtime',
|
| 993 |
y='nama',
|
| 994 |
orientation='h',
|
| 995 |
+
title=f'Avg Monthly Lead Time (Days) — {sort_option_3b}',
|
| 996 |
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
|
| 997 |
color='color',
|
| 998 |
+
color_discrete_map={c: c for c in subset_data['color'].unique()},
|
| 999 |
+
text=subset_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
|
| 1000 |
)
|
|
|
|
| 1001 |
fig_exec_nama.update_layout(
|
| 1002 |
+
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama'].tolist()},
|
|
|
|
|
|
|
|
|
|
| 1003 |
height=500,
|
| 1004 |
showlegend=False
|
| 1005 |
)
|
| 1006 |
fig_exec_nama.update_traces(textposition='auto')
|
| 1007 |
st.plotly_chart(fig_exec_nama, use_container_width=True)
|
| 1008 |
|
| 1009 |
+
# 🔥 INSIGHT: dari FULL DATA
|
| 1010 |
+
if len(sorted_all_3b) >= 2:
|
| 1011 |
+
min_lt = sorted_all_3b['avg_monthly_leadtime'].min()
|
| 1012 |
+
max_lt = sorted_all_3b['avg_monthly_leadtime'].max()
|
| 1013 |
+
mean_lt = sorted_all_3b['avg_monthly_leadtime'].mean()
|
| 1014 |
+
median_lt = sorted_all_3b['avg_monthly_leadtime'].median()
|
| 1015 |
+
fastest_div = sorted_all_3b.iloc[0]['nama']
|
| 1016 |
+
slowest_div = sorted_all_3b.iloc[-1]['nama']
|
| 1017 |
+
|
| 1018 |
+
insight_text = (
|
| 1019 |
+
f"<div class='ai-insight'>"
|
| 1020 |
+
f"<strong>Overall (all {len(sorted_all_3b)} divisions)</strong>: Resolution time ranges from <strong>{min_lt:.1f}</strong> "
|
| 1021 |
+
f"to <strong>{max_lt:.1f}</strong> days (mean: <strong>{mean_lt:.1f}</strong>, median: <strong>{median_lt:.1f}</strong>). "
|
| 1022 |
+
f"<strong>{slowest_div}</strong> has the longest lead time; <strong>{fastest_div}</strong> resolves fastest. "
|
| 1023 |
+
f"<strong>Recommendation:</strong> Escalate SLA breach risk for divisions >7 days; initiate root-cause analysis for {slowest_div} and replicate efficiency from {fastest_div}."
|
| 1024 |
+
f"</div>"
|
| 1025 |
+
)
|
| 1026 |
+
else:
|
| 1027 |
+
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 1028 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1029 |
|
| 1030 |
+
|
| 1031 |
with col_3d:
|
| 1032 |
st.markdown("<h5>3d. Average Lead Time by Executor (Name)</h5>", unsafe_allow_html=True)
|
| 1033 |
if avg_leadtime_per_executor.empty:
|
|
|
|
| 1035 |
else:
|
| 1036 |
sort_option_3d = st.selectbox("Show 3d:", ["Top 10", "Bottom 10"], key='sort_3d')
|
| 1037 |
|
| 1038 |
+
sorted_all_3d = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
|
|
|
|
| 1039 |
|
|
|
|
| 1040 |
if sort_option_3d == "Top 10":
|
| 1041 |
+
subset_data = sorted_all_3d.head(10)
|
| 1042 |
else:
|
| 1043 |
+
subset_data = sorted_all_3d.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
|
| 1044 |
|
| 1045 |
+
# Warna global: 5 eksekutor TERLAMBAT → merah
|
| 1046 |
+
avg_leadtime_per_executor_colored = add_color_by_global_rank(
|
| 1047 |
+
avg_leadtime_per_executor, 'avg_monthly_leadtime', worst_n=5, high_is_good=False
|
| 1048 |
+
)
|
| 1049 |
+
subset_data = subset_data.merge(
|
| 1050 |
+
avg_leadtime_per_executor_colored[['nama_pic', 'color']], on='nama_pic', how='left'
|
| 1051 |
+
).fillna({'color': '#1f77b4'})
|
| 1052 |
|
| 1053 |
+
if sort_option_3d == "Bottom 10":
|
| 1054 |
+
subset_data = subset_data.iloc[::-1]
|
| 1055 |
+
|
| 1056 |
fig_exec_pic = px.bar(
|
| 1057 |
+
subset_data,
|
| 1058 |
x='avg_monthly_leadtime',
|
| 1059 |
y='nama_pic',
|
| 1060 |
orientation='h',
|
| 1061 |
+
title=f'Avg Monthly Lead Time (Days) — {sort_option_3d}',
|
| 1062 |
+
labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama_pic': 'Executor'},
|
| 1063 |
color='color',
|
| 1064 |
+
color_discrete_map={c: c for c in subset_data['color'].unique()},
|
| 1065 |
+
text=subset_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
|
| 1066 |
)
|
|
|
|
| 1067 |
fig_exec_pic.update_layout(
|
| 1068 |
+
yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama_pic'].tolist()},
|
|
|
|
|
|
|
|
|
|
| 1069 |
height=500,
|
| 1070 |
showlegend=False
|
| 1071 |
)
|
| 1072 |
fig_exec_pic.update_traces(textposition='auto')
|
| 1073 |
st.plotly_chart(fig_exec_pic, use_container_width=True)
|
| 1074 |
|
| 1075 |
+
# 🔥 INSIGHT: dari FULL DATA
|
| 1076 |
+
if len(sorted_all_3d) >= 2:
|
| 1077 |
+
min_lt = sorted_all_3d['avg_monthly_leadtime'].min()
|
| 1078 |
+
max_lt = sorted_all_3d['avg_monthly_leadtime'].max()
|
| 1079 |
+
mean_lt = sorted_all_3d['avg_monthly_leadtime'].mean()
|
| 1080 |
+
median_lt = sorted_all_3d['avg_monthly_leadtime'].median()
|
| 1081 |
+
fastest_exec = sorted_all_3d.iloc[0]['nama_pic']
|
| 1082 |
+
slowest_exec = sorted_all_3d.iloc[-1]['nama_pic']
|
| 1083 |
+
|
| 1084 |
+
insight_text = (
|
| 1085 |
+
f"<div class='ai-insight'>"
|
| 1086 |
+
f"<strong>Overall (all {len(sorted_all_3d)} executors)</strong>: Lead time ranges from <strong>{min_lt:.1f}</strong> "
|
| 1087 |
+
f"to <strong>{max_lt:.1f}</strong> days (mean: <strong>{mean_lt:.1f}</strong>). "
|
| 1088 |
+
f"<strong>{slowest_exec}</strong> has the longest resolution time; <strong>{fastest_exec}</strong> is most efficient. "
|
| 1089 |
+
f"<strong>Recommendation:</strong> Assign mentor to executors with >7-day average; document and share best practices from {fastest_exec} across the team."
|
| 1090 |
+
f"</div>"
|
| 1091 |
+
)
|
| 1092 |
+
else:
|
| 1093 |
+
insight_text = "<div class='ai-insight'>Insufficient data for insight.</div>"
|
| 1094 |
st.markdown(insight_text, unsafe_allow_html=True)
|
| 1095 |
|
| 1096 |
+
|
| 1097 |
+
|
| 1098 |
try:
|
| 1099 |
from wordcloud import WordCloud
|
| 1100 |
import matplotlib.pyplot as plt
|