SHELLAPANDIANGANHUNGING commited on
Commit
52d12ba
·
verified ·
1 Parent(s): d390ba1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -144
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
- # Urutkan data dari tertinggi ke terendah
805
- sorted_data_all = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False)
806
 
807
- # Ambil Top 10 atau Bottom 10
808
  if sort_option_3a == "Top 10":
809
- sorted_data = sorted_data_all.head(10)
810
- else:
811
- sorted_data = sorted_data_all.tail(10)
812
 
813
- # 🔥 Urutkan data yang ditampilkan dari besar ke kecil (jika Bottom 10, tetap besar ke kecil)
814
- sorted_data = sorted_data.sort_values('avg_monthly_ratio', ascending=False).reset_index(drop=True)
815
- sorted_data = sorted_data.iloc[::-1] # ← Balik posisi data
816
-
817
-
818
- # Tambahkan warna untuk top 5 dari data yang ditampilkan
819
- sorted_data['color'] = '#1f77b4'
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
- sorted_data,
825
  x='avg_monthly_ratio',
826
  y='nama',
827
  orientation='h',
828
- title='Avg Monthly Finding by Division',
829
- labels={'avg_monthly_ratio': 'Avg Monthly Finding/Person Ratio', 'nama': 'Division'},
830
  color='color',
831
- color_discrete_map={c: c for c in sorted_data['color'].unique()},
832
- text=sorted_data['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
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
- # Insight
847
- top = sorted_data.iloc[0] if sort_option_3a == "Top 10" else sorted_data_all.iloc[-1]
848
- low = sorted_data.iloc[-1] if sort_option_3a == "Top 10" else sorted_data_all.iloc[0]
849
- insight_text = (
850
- f"<div class='ai-insight'>"
851
- f"The division <strong>{top['nama']}</strong> has the lowest average finding-to-person ratio "
852
- f"(<strong>{top['avg_monthly_ratio']:.2f}</strong>). "
853
- f"<strong>{low['nama']}</strong> has the highest ratio "
854
- f"(<strong>{low['avg_monthly_ratio']:.2f}</strong>). "
855
- f"Monitor low-ratio divisions for potential systemic issues and verify reporting completeness."
856
- f"</div>"
857
- )
 
 
 
 
 
 
 
858
  st.markdown(insight_text, unsafe_allow_html=True)
859
 
 
860
  with col_3c:
861
- st.markdown("<h5>3b. Average Finding Rate per Reporter (Name)</h5>", unsafe_allow_html=True)
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 3b:", ["Top 10", "Bottom 10"], key='sort_3c')
866
 
867
- # Urutkan data dari tertinggi ke terendah
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
- sorted_data = sorted_data_all.head(10)
873
  else:
874
- sorted_data = sorted_data_all.tail(10)
875
 
876
- # 🔥 Urutkan data yang ditampilkan dari besar ke kecil (jika Bottom 10, tetap besar ke kecil)
877
- sorted_data = sorted_data.sort_values('avg_monthly_rate', ascending=False).reset_index(drop=True)
878
- sorted_data = sorted_data.iloc[::-1] # ← Balik posisi data
879
-
880
-
881
- # Tambahkan warna untuk top 5 dari data yang ditampilkan
882
- sorted_data['color'] = '#1f77b4'
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
- sorted_data,
888
  x='avg_monthly_rate',
889
  y='creator_name',
890
  orientation='h',
891
- title='Avg Monthly Finding by Creator Name',
892
- labels={'avg_monthly_rate': 'Avg Monthly Finding Rate', 'creator_name': 'Creator Name'},
893
  color='color',
894
- color_discrete_map={c: c for c in sorted_data['color'].unique()},
895
- text=sorted_data['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
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
- # Insight
910
- top = sorted_data.iloc[0] if sort_option_3c == "Top 10" else sorted_data_all.iloc[-1]
911
- low = sorted_data.iloc[-1] if sort_option_3c == "Top 10" else sorted_data_all.iloc[0]
912
- insight_text = (
913
- f"<div class='ai-insight'>"
914
- f"The reporter <strong>{top['creator_name']}</strong> has the lowest average monthly finding rate "
915
- f"(<strong>{top['avg_monthly_rate']:.2f}</strong>). "
916
- f"<strong>{low['creator_name']}</strong> has the highest rate "
917
- f"(<strong>{low['avg_monthly_rate']:.2f}</strong>). "
918
- f"Recognize high performers and investigate low performers."
919
- f"</div>"
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>3c. Average Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
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 3c:", ["Top 10", "Bottom 10"], key='sort_3b')
933
 
934
- # Urutkan data dari tertinggi ke terendah
935
- sorted_data_all = avg_leadtime_nama.sort_values('avg_monthly_leadtime', ascending=True)
936
 
937
- # Ambil Top 10 atau Bottom 10
938
  if sort_option_3b == "Top 10":
939
- sorted_data = sorted_data_all.head(10)
 
940
  else:
941
- sorted_data = sorted_data_all.tail(10)
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
- # Tambahkan warna untuk top 5 dari data yang ditampilkan
947
- sorted_data['color'] = '#1f77b4'
948
- top_5_indices = sorted_data.head(5).index
949
- sorted_data.loc[top_5_indices, 'color'] = '#D32F2F'
 
 
 
950
 
 
 
 
 
951
  fig_exec_nama = px.bar(
952
- sorted_data,
953
  x='avg_monthly_leadtime',
954
  y='nama',
955
  orientation='h',
956
- title='Avg Monthly Lead Time by Division',
957
  labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
958
  color='color',
959
- color_discrete_map={c: c for c in sorted_data['color'].unique()},
960
- text=sorted_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
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
- # Insight
975
- top = sorted_data.iloc[0] if sort_option_3b == "Top 10" else sorted_data_all.iloc[-1]
976
- low = sorted_data.iloc[-1] if sort_option_3b == "Top 10" else sorted_data_all.iloc[0]
977
- insight_text = (
978
- f"<div class='ai-insight'>"
979
- f"The division <strong>{top['nama']}</strong> has the highest average lead time "
980
- f"(<strong>{top['avg_monthly_leadtime']:.2f} days</strong>). "
981
- f"<strong>{low['nama']}</strong> has the fastest average resolution "
982
- f"(<strong>{low['avg_monthly_leadtime']:.2f} days</strong>). "
983
- f"Focus on improving SLA compliance in divisions with longer lead times."
984
- f"</div>"
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
- # Urutkan data dari tertinggi ke terendah
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
- sorted_data = sorted_data_all.head(10)
1001
  else:
1002
- sorted_data = sorted_data_all.tail(10)
1003
 
1004
- # 🔥 Urutkan data yang ditampilkan dari besar ke kecil (jika Bottom 10, tetap besar ke kecil)
1005
- sorted_data = sorted_data.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
1006
-
1007
- # Tambahkan warna untuk top 5 dari data yang ditampilkan
1008
- sorted_data['color'] = '#1f77b4'
1009
- top_5_indices = sorted_data.head(5).index
1010
- sorted_data.loc[top_5_indices, 'color'] = '#D32F2F'
1011
 
 
 
 
1012
  fig_exec_pic = px.bar(
1013
- sorted_data,
1014
  x='avg_monthly_leadtime',
1015
  y='nama_pic',
1016
  orientation='h',
1017
- title='Avg Monthly Lead Time by Executor (Name)',
1018
- labels={'avg_monthly_leadtime': 'Avg Monthly Lead Time (Days)', 'nama_pic': 'Executor Name'},
1019
  color='color',
1020
- color_discrete_map={c: c for c in sorted_data['color'].unique()},
1021
- text=sorted_data['avg_monthly_leadtime'].apply(lambda x: f'{x:.2f}')
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
- # Insight
1036
- top = sorted_data.iloc[0] if sort_option_3d == "Top 10" else sorted_data_all.iloc[-1]
1037
- low = sorted_data.iloc[-1] if sort_option_3d == "Top 10" else sorted_data_all.iloc[0]
1038
- insight_text = (
1039
- f"<div class='ai-insight'>"
1040
- f"The executor <strong>{top['nama_pic']}</strong> has the highest average monthly lead time "
1041
- f"(<strong>{top['avg_monthly_leadtime']:.2f} days</strong>). "
1042
- f"<strong>{low['nama_pic']}</strong> resolves tasks fastest on average "
1043
- f"(<strong>{low['avg_monthly_leadtime']:.2f} days</strong>). "
1044
- f"Focus on improving SLA compliance for executors with longer lead times."
1045
- f"</div>"
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