SHELLAPANDIANGANHUNGING commited on
Commit
b1cd953
·
verified ·
1 Parent(s): 4b7bc59

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -269
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 nama (Reporter) ────────────────────────────────
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 lead time per nama (Executor) ───────────────────────────
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 nama_pic ──────────────────────────────────
739
- def compute_executor_leadtime_by_pic(df):
740
- if 'nama_pic' not in df.columns or 'days_to_close' not in df.columns:
741
  return pd.DataFrame()
742
 
743
- leadtime_by_executor_month = df.groupby(['created_month', 'nama_pic'])['days_to_close'].mean().reset_index(name='avg_leadtime')
744
- active_months_by_executor = leadtime_by_executor_month.groupby('nama_pic')['created_month'].nunique().reset_index(name='active_months')
745
- total_leadtime_by_executor = leadtime_by_executor_month.groupby('nama_pic')['avg_leadtime'].sum().reset_index()
746
- merged_exec_pic = total_leadtime_by_executor.merge(active_months_by_executor, on='nama_pic', how='outer')
747
- merged_exec_pic = merged_exec_pic.fillna({'avg_leadtime': 0, 'active_months': 0})
748
- merged_exec_pic = merged_exec_pic[merged_exec_pic['active_months'] > 0]
749
- merged_exec_pic['avg_monthly_leadtime'] = merged_exec_pic['avg_leadtime'] / merged_exec_pic['active_months']
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
- # ─── Data untuk 3a & 3c ──────────────────────────────────────────────────────
756
- avg_ratio_per_nama = compute_reporter_ratio_by_nama(df_local)
757
- avg_rate_per_creator = compute_reporter_rate_by_creator(df_local)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Helper: Dapatkan warna berdasarkan ranking global (bukan lokal subset)
 
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 5 hijau
777
- top_idx = df.head(top_n).index
778
- df.loc[top_idx, 'color'] = '#4CAF50'
779
  else:
780
- # Nilai tinggi = buruk → top 5 (tertinggi) = merah
781
- # Urut ascending, ambil tail 5 (tertinggi)
782
- df_asc = df.sort_values(value_col, ascending=True)
783
- worst_idx = df_asc.tail(worst_n).index
784
- df.loc[worst_idx, 'color'] = '#D32F2F'
785
  return df
786
 
787
 
788
- # ─── Layout: 2 Baris — 3a & 3c di baris pertama, 3b & 3d di baris kedua ─────
789
- # Baris 1: 3a & 3c
790
  col_3a, col_3c = st.columns(2)
791
 
 
792
  with col_3a:
793
- st.markdown("<h5>3a. Average Finding by Division (Reporter)</h5>", unsafe_allow_html=True)
794
  if avg_ratio_per_nama.empty:
795
- st.warning("No data for reporter analysis by division.")
796
  else:
797
- sort_option_3a = st.selectbox("Show 3a:", ["Top 10", "Bottom 10"], key='sort_3a')
798
 
799
- # Full data sorted descending (high ratio = good)
800
- sorted_all_3a = avg_ratio_per_nama.sort_values('avg_monthly_ratio', ascending=False).reset_index(drop=True)
801
 
802
- # Subset sesuai pilihan user
803
- if sort_option_3a == "Top 10":
804
- subset_data = sorted_all_3a.head(10)
805
- else: # Bottom 10
806
- subset_data = sorted_all_3a.tail(10).sort_values('avg_monthly_ratio', ascending=True) # ascending dalam subset
807
 
808
- # Tambahkan warna berdasarkan rank global
809
- avg_ratio_per_nama_colored = add_color_by_global_rank(
810
- avg_ratio_per_nama, 'avg_monthly_ratio', top_n=5, high_is_good=True
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
- # Reverse order for better visual (low at bottom)
817
- if sort_option_3a == "Top 10":
818
- subset_data = subset_data.iloc[::-1] # descending (tertinggi di atas)
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
- color_discrete_map={c: c for c in subset_data['color'].unique()},
831
- text=subset_data['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
832
  )
833
- fig_rep_nama.update_layout(
834
- yaxis={'categoryorder': 'array', 'categoryarray': subset_data['nama'].tolist()},
835
- height=500,
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
- # 🔥 INSIGHT: SELALU dari FULL DATA, stabil
842
- if len(sorted_all_3a) >= 2:
843
- min_val = sorted_all_3a['avg_monthly_ratio'].min()
844
- max_val = sorted_all_3a['avg_monthly_ratio'].max()
845
- mean_val = sorted_all_3a['avg_monthly_ratio'].mean()
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>Overall (all {len(sorted_all_3a)} divisions)</strong>: Ratio ranges from <strong>{min_val:.2f}</strong> "
853
- f"to <strong>{max_val:.2f}</strong> (mean: <strong>{mean_val:.2f}</strong>, median: <strong>{median_val:.2f}</strong>). "
854
- f"<strong>{best_div}</strong> is the most active division (highest ratio), while <strong>{worst_div}</strong> is the least. "
855
- 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}."
856
- f"</div>"
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>3b. Average Finding Rate per Reporter (Name)</h5>", unsafe_allow_html=True)
865
  if avg_rate_per_creator.empty:
866
- st.warning("No data for reporter analysis by creator_name.")
867
  else:
868
- sort_option_3c = st.selectbox("Show 3c:", ["Top 10", "Bottom 10"], key='sort_3c')
869
 
870
- sorted_all_3c = avg_rate_per_creator.sort_values('avg_monthly_rate', ascending=False).reset_index(drop=True)
 
871
 
872
- if sort_option_3c == "Top 10":
873
- subset_data = sorted_all_3c.head(10)
874
- else:
875
- subset_data = sorted_all_3c.tail(10).sort_values('avg_monthly_rate', ascending=True)
876
 
877
- # Warna global: top 5 tertinggi → hijau
878
- avg_rate_per_creator_colored = add_color_by_global_rank(
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
- if sort_option_3c == "Top 10":
886
- subset_data = subset_data.iloc[::-1]
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
- color_discrete_map={c: c for c in subset_data['color'].unique()},
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
- fig_rep_creator.update_traces(textposition='auto')
905
- st.plotly_chart(fig_rep_creator, use_container_width=True)
 
906
 
907
- # 🔥 INSIGHT: dari FULL DATA
908
- if len(sorted_all_3c) >= 2:
909
- min_val = sorted_all_3c['avg_monthly_rate'].min()
910
- max_val = sorted_all_3c['avg_monthly_rate'].max()
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>Overall (all {len(sorted_all_3c)} reporters)</strong>: Monthly rate ranges from <strong>{min_val:.2f}</strong> "
919
- f"to <strong>{max_val:.2f}</strong> (mean: <strong>{mean_val:.2f}</strong>). "
920
- f"<strong>{best_reporter}</strong> is the top reporter; <strong>{worst_reporter}</strong> reports least frequently. "
921
- f"<strong>Recommendation:</strong> Conduct 1:1 coaching for reporters with <0.5 findings/month; recognize top contributors publicly to motivate peers."
922
- f"</div>"
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: 3b & 3d
930
  col_3b, col_3d = st.columns(2)
931
 
 
932
  with col_3b:
933
- st.markdown("<h5>3c. Average Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
934
  if avg_leadtime_nama.empty:
935
- st.warning("No data for executor analysis by division.")
936
  else:
937
- sort_option_3b = st.selectbox("Show 3b:", ["Top 10", "Bottom 10"], key='sort_3b')
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
- if sort_option_3b == "Top 10":
943
- # Top 10 tercepat (terendah)
944
- subset_data = sorted_all_3b.head(10).sort_values('avg_monthly_leadtime', ascending=False)
945
  else:
946
- # Bottom 10 = terlama (tertinggi)
947
- subset_data = sorted_all_3b.tail(10).sort_values('avg_monthly_leadtime', ascending=False) # descending dalam subset
948
 
949
- # Warna global: 5 division dengan lead time TERPANJANG → merah
950
- avg_leadtime_nama_colored = add_color_by_global_rank(
951
- avg_leadtime_nama, 'avg_monthly_leadtime', worst_n=5, high_is_good=False
952
- )
953
- subset_data = subset_data.merge(
954
- avg_leadtime_nama_colored[['nama', 'color']], on='nama', how='left'
955
- ).fillna({'color': '#1f77b4'})
956
 
957
- # Reverse untuk visual yang intuitif (lama di atas)
958
- if sort_option_3b == "Bottom 10":
959
- subset_data = subset_data.iloc[::-1] # biar tertinggi di atas
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
- color_discrete_map={c: c for c in subset_data['color'].unique()},
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
- fig_exec_nama.update_traces(textposition='auto')
978
- st.plotly_chart(fig_exec_nama, use_container_width=True)
 
979
 
980
- # 🔥 INSIGHT: dari FULL DATA
981
- if len(sorted_all_3b) >= 2:
982
- min_lt = sorted_all_3b['avg_monthly_leadtime'].min()
983
- max_lt = sorted_all_3b['avg_monthly_leadtime'].max()
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>Overall (all {len(sorted_all_3b)} divisions)</strong>: Resolution time ranges from <strong>{min_lt:.1f}</strong> "
992
- f"to <strong>{max_lt:.1f}</strong> days (mean: <strong>{mean_lt:.1f}</strong>, median: <strong>{median_lt:.1f}</strong>). "
993
- f"<strong>{slowest_div}</strong> has the longest lead time; <strong>{fastest_div}</strong> resolves fastest. "
994
- 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}."
995
- f"</div>"
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. Average Lead Time by Executor (Name)</h5>", unsafe_allow_html=True)
1004
- if avg_leadtime_per_executor.empty:
1005
- st.warning("No data for executor analysis by nama_pic.")
1006
  else:
1007
- sort_option_3d = st.selectbox("Show 3d:", ["Top 10", "Bottom 10"], key='sort_3d')
1008
-
1009
- sorted_all_3d = avg_leadtime_per_executor.sort_values('avg_monthly_leadtime', ascending=True).reset_index(drop=True)
1010
 
1011
- if sort_option_3d == "Top 10":
1012
- subset_data = sorted_all_3b.head(10).sort_values('avg_monthly_leadtime', ascending=False)
 
1013
  else:
1014
- subset_data = sorted_all_3d.tail(10).sort_values('avg_monthly_leadtime', ascending=False)
1015
 
1016
- # Warna global: 5 eksekutor TERLAMBAT → merah
1017
- avg_leadtime_per_executor_colored = add_color_by_global_rank(
1018
- avg_leadtime_per_executor, 'avg_monthly_leadtime', worst_n=5, high_is_good=False
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 sort_option_3d == "Bottom 10":
1025
- subset_data = subset_data.iloc[::-1]
1026
-
1027
- fig_exec_pic = px.bar(
1028
- subset_data,
1029
- x='avg_monthly_leadtime',
1030
- y='nama_pic',
1031
- orientation='h',
1032
- title=f'Avg Monthly Lead Time (Days) {sort_option_3d}',
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
- fig_exec_pic.update_traces(textposition='auto')
1044
- st.plotly_chart(fig_exec_pic, use_container_width=True)
 
1045
 
1046
- # 🔥 INSIGHT: dari FULL DATA
1047
- if len(sorted_all_3d) >= 2:
1048
- min_lt = sorted_all_3d['avg_monthly_leadtime'].min()
1049
- max_lt = sorted_all_3d['avg_monthly_leadtime'].max()
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>Overall (all {len(sorted_all_3d)} executors)</strong>: Lead time ranges from <strong>{min_lt:.1f}</strong> "
1058
- f"to <strong>{max_lt:.1f}</strong> days (mean: <strong>{mean_lt:.1f}</strong>). "
1059
- f"<strong>{slowest_exec}</strong> has the longest resolution time; <strong>{fastest_exec}</strong> is most efficient. "
1060
- f"<strong>Recommendation:</strong> Assign mentor to executors with >7-day average; document and share best practices from {fastest_exec} across the team."
1061
- f"</div>"
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