-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
767 lines (644 loc) · 38.1 KB
/
Copy pathapp.py
File metadata and controls
767 lines (644 loc) · 38.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
import streamlit as st
import pandas as pd
import numpy as np
import os
import random
import sys
import importlib
# Defensive import to handle Streamlit module-reloading KeyError
if 'Attrition_system' in sys.modules:
importlib.reload(sys.modules['Attrition_system'])
import Attrition_system as asy
# Extract functions for easier access
load_model = asy.load_model
sample_employee = asy.sample_employee
classify_employee_value = asy.classify_employee_value
recommend_actions = asy.recommend_actions
preprocess_input = asy.preprocess_input
get_attrition_drivers = asy.get_attrition_drivers
# --- Configuration & Styling ---
st.set_page_config(page_title="HR Attrition Decision Support", page_icon="🛡️", layout="wide")
st.markdown("""
<style>
/* Global Styles */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
html, body, [class*="css"] {
font-family: 'Inter', sans-serif;
}
/* Card Styles - PREMIUM & COMPACT */
.stCard, [data-testid="stVerticalBlock"] > div > div > [data-testid="stVerticalBlock"] {
background-color: white;
padding: 1rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #f1f5f9;
}
.section-header {
background: linear-gradient(90deg, #f8fafc 0%, #eff6ff 100%);
padding: 4px 12px;
border-radius: 6px;
text-align: left;
font-weight: 800;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #475569;
margin: 6px 0 4px 0;
border-left: 3px solid #3b82f6;
}
.risk-score {
font-size: 48px;
font-weight: 900;
text-align: center;
margin: 5px 0;
letter-spacing: -0.02em;
}
</style>
""", unsafe_allow_html=True)
# --- Feature Mapping (Technical -> Human Readable) ---
FEATURE_MAP = {
'DistanceFromHome': 'Commute distance',
'OverTime': 'Overtime frequency',
'YearsSinceLastPromotion': 'Promotion stagnation',
'EnvironmentSatisfaction': 'Environment dissatisfaction',
'JobSatisfaction': 'Low job satisfaction',
'WorkLifeBalance': 'Work-life balance issues',
'NumCompaniesWorked': 'Job hopping history',
'YearsWithCurrManager': 'Manager relationship tenure',
'MonthlyIncome': 'Income level impact',
'Age': 'Demographic age factor',
'YearsAtCompany': 'Company tenure factor',
'MaritalStatus': 'Marital status factor'
}
# --- Resource Loading ---
@st.cache_resource
def get_model():
if os.path.exists("attrition_model.pkl"):
return load_model("attrition_model.pkl")
return None
@st.cache_data
def get_data():
if os.path.exists('WA_Fn-UseC_-HR-Employee-Attrition.csv'):
return pd.read_csv('WA_Fn-UseC_-HR-Employee-Attrition.csv')
return None
model = get_model()
df_raw = get_data()
# --- Logic: Analysis Engine ---
def run_strategic_analysis(model, df, threshold, want_leave, value_type, min_risk, max_risk):
# Call original selector
sample = sample_employee(model, df, threshold, want_leave, value_type, min_risk, max_risk)
if sample is None:
return None
idx = sample["index"]
X_row = sample["X_row"]
pred = sample["pred"]
proba = sample["proba"]
raw_employee = df.loc[idx]
# Ground Truth logic
true_raw = raw_employee['Attrition']
actual = "Leave" if true_raw == "Yes" or true_raw == 1 else "Stay"
# Driver Calculation (Using rule-based system from user)
structured_drivers = get_attrition_drivers(X_row.iloc[0])
# Values & Recommendations (Using the new signature)
score, value_label = classify_employee_value(X_row.iloc[0])
recs = recommend_actions(X_row.iloc[0], proba, value_label, structured_drivers, threshold)
# Derived Insights
work_intensity = "High" if (raw_employee.get('OverTime') == 'Yes' or X_row['OverTime'].values[0] == 1) and raw_employee['WorkLifeBalance'] <= 2 else "Medium" if (raw_employee.get('OverTime') == 'Yes' or X_row['OverTime'].values[0] == 1) else "Low"
# Income level compared to JobLevel average
avg_income = df[df['JobLevel'] == raw_employee['JobLevel']]['MonthlyIncome'].mean()
income_level = "Above Average" if raw_employee['MonthlyIncome'] > avg_income * 1.1 else "Below Average" if raw_employee['MonthlyIncome'] < avg_income * 0.9 else "Standard"
stability = "Stable" if raw_employee['YearsAtCompany'] > 5 and raw_employee['NumCompaniesWorked'] < 3 else "Moderate" if raw_employee['YearsAtCompany'] > 2 else "Low"
return {
"index": idx,
"raw": raw_employee,
"X_row": X_row.iloc[0],
"risk": proba,
"prediction": "Leave" if pred == 1 else "Stay",
"actual": actual,
"value_label": value_label,
"value_score": score,
"problems": recs["Problems"],
"actions": recs["Actions"],
"structured_drivers": structured_drivers,
"insights": {
"Work Intensity": work_intensity,
"Income Alignment": income_level,
"Career Stability": stability
}
}
# --- Sidebar: Redesigned ---
with st.sidebar:
st.title("⚙️ HR Strategy Hub")
st.caption("Nordea Attrition Decision Support")
st.divider()
st.subheader("⚙️ Decision Threshold")
threshold = st.slider("Required confidence to flag risk", 0.3, 0.6, 0.35, 0.05)
st.caption("Standard: 0.35. Increase for tighter intervention budgets.")
st.divider()
st.subheader("📊 Prediction Filter")
pred_choice = st.selectbox("Type of employee to analyze", ["All", "Likely to Leave", "Likely to Stay"])
want_leave_map = {"All": None, "Likely to Leave": True, "Likely to Stay": False}
want_leave = want_leave_map[pred_choice]
st.subheader("⭐ Employee Value")
value_choice = st.selectbox("Priority tier", ["All", "High Value Employee", "Valuable Employee", "Average Employee", "Low Value Employee"])
value_type = None if value_choice == "All" else value_choice
st.divider()
st.subheader("🎯 Risk Range")
risk_range = st.slider("Probability filter (%)", 0, 100, (0, 100))
min_risk, max_risk = risk_range[0] / 100, risk_range[1] / 100
st.caption("Filter specific probability bands.")
# --- Header Section ---
st.title("🛡️ Employee Attrition Decision Support")
st.markdown("##### Strategic Intelligence Unit | Employee Retention Management")
# --- Control Center ---
if st.button("🔄 Generate Employee Scenario", type="primary", use_container_width=True):
with st.spinner("Analyzing workforce data..."):
analysis = run_strategic_analysis(model, df_raw, threshold, want_leave, value_type, min_risk, max_risk)
if analysis:
st.session_state.strategic_analysis = analysis
else:
st.warning("No employees matching your current filters were found in the database.")
# --- Dashboard Structure ---
tab1, tab2 = st.tabs(["📊 Strategic Dashboard", "🧠 Classification Logic"])
with tab1:
if 'strategic_analysis' in st.session_state:
res = st.session_state.strategic_analysis
raw = res['raw']
col1, col2 = st.columns([1, 1], gap="large")
# 1. Persona Profile (Left Card)
ins = res['insights']
# Ultra-compact modern High-fidelity layout
def metric_card(label, value, icon=""):
return f"""
<div style="background-color: white; border: 1px solid #f1f5f9; border-radius: 8px; padding: 6px 10px; margin-bottom: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.02); height: 100%; border-left: 2px solid #3b82f611;">
<div style="font-size: 9px; color: #94a3b8; font-weight: 700; text-transform: uppercase; margin-bottom: 0px; letter-spacing: 0.01em;">{label}</div>
<div style="font-size: 13px; color: #1e3a8a; font-weight: 700; display: flex; align-items: center; gap: 4px;">
<span style="font-size: 14px;">{icon}</span> {value}
</div>
</div>
"""
with col1:
with st.container(border=True):
# Value-based styling
val_meta = {
"High Value Employee": {"color": "#fbbf24", "icon": "⭐", "bg": "#fef3c7"},
"Valuable Employee": {"color": "#60a5fa", "icon": "💎", "bg": "#dbeafe"},
"Average Employee": {"color": "#94a3b8", "icon": "👤", "bg": "#f1f5f9"},
"Low Value Employee": {"color": "#f87171", "icon": "⚠️", "bg": "#fee2e2"}
}.get(res["value_label"], {"color": "#94a3b8", "icon": "👤", "bg": "#f1f5f9"})
st.markdown(f"### {raw['JobRole']}")
st.markdown(f"""
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 5px;">
<span style="color: #64748b; font-weight: 500;">{raw['Department']} Department</span>
<span style="background-color: {val_meta['bg']}; color: {val_meta['color']}; padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 700; border: 1px solid {val_meta['color']}44;">
{val_meta['icon']} {res['value_label'].upper()}
</span>
</div>
""", unsafe_allow_html=True)
st.markdown('<div class="section-header">📋 Demographics & Personal Details</div>', unsafe_allow_html=True)
d_row1_col1, d_row1_col2, d_row1_col3 = st.columns(3)
d_row1_col1.markdown(metric_card("Employee ID", f"#{raw.get('EmployeeNumber', 'N/A')}", "🆔"), unsafe_allow_html=True)
d_row1_col2.markdown(metric_card("Age / Sex", f"{raw['Age']} ({raw.get('Gender', '?')[0]})", "👤"), unsafe_allow_html=True)
d_row1_col3.markdown(metric_card("Marital Status", raw['MaritalStatus'], "💍"), unsafe_allow_html=True)
d_row2_col1, d_row2_col2, d_row2_col3 = st.columns(3)
d_row2_col1.markdown(metric_card("Commute Dist.", f"{raw['DistanceFromHome']} mi", "🏠"), unsafe_allow_html=True)
d_row2_col2.markdown(metric_card("Business Travel", raw['BusinessTravel'].replace('Travel_', '').replace('_', ' '), "✈️"), unsafe_allow_html=True)
d_row2_col3.markdown(metric_card("Stock Options", raw.get('StockOptionLevel', 0), "📊"), unsafe_allow_html=True)
st.markdown('<div class="section-header">🏢 Career & Progress History</div>', unsafe_allow_html=True)
c_row1_col1, c_row1_col2, c_row1_col3 = st.columns(3)
c_row1_col1.markdown(metric_card("Monthly Salary", f"${raw['MonthlyIncome']:,}", "💰"), unsafe_allow_html=True)
c_row1_col2.markdown(metric_card("Career Total", f"{raw.get('TotalWorkingYears', 0)}y", "📈"), unsafe_allow_html=True)
c_row1_col3.markdown(metric_card("Latest Hike", f"{raw.get('PercentSalaryHike', 0)}%", "💹"), unsafe_allow_html=True)
c_row2_col1, c_row2_col2, c_row2_col3 = st.columns(3)
c_row2_col1.markdown(metric_card("Job Level", raw['JobLevel'], "🏗️"), unsafe_allow_html=True)
c_row2_col2.markdown(metric_card("Last Promotion", f"{raw.get('YearsSinceLastPromotion', 0)}y ago", "⌛"), unsafe_allow_html=True)
c_row2_col3.markdown(metric_card("Rating", f"{raw.get('PerformanceRating', 0)}/4", "🏆"), unsafe_allow_html=True)
c_row3_col1, c_row3_col2, c_row3_col3 = st.columns(3)
c_row3_col1.markdown(metric_card("Co. Tenure", f"{raw['YearsAtCompany']}y", "🏢"), unsafe_allow_html=True)
c_row3_col2.markdown(metric_card("Overtime", '🚨 YES' if raw['OverTime'] == 'Yes' else 'No', "⏰"), unsafe_allow_html=True)
c_row3_col3.markdown(metric_card("External Hist.", f"{raw['NumCompaniesWorked']} Cos.", "🏢"), unsafe_allow_html=True)
st.markdown('<div class="section-header">🧠 Sentiment & Satisfaction Audit</div>', unsafe_allow_html=True)
def sat_meta(v):
meta = {
1: ("🔴", "Low"),
2: ("🟠", "Med."),
3: ("🟡", "High"),
4: ("🟢", "V. High")
}.get(v, ("⚪", "N/A"))
return meta
j_ico, j_lbl = sat_meta(raw.get('JobSatisfaction', 0))
e_ico, e_lbl = sat_meta(raw.get('EnvironmentSatisfaction', 0))
r_ico, r_lbl = sat_meta(raw.get('RelationshipSatisfaction', 0))
m_tenure = raw.get('YearsWithCurrManager', 0)
m_ico = "🟢" if m_tenure >= 3 else "🟡" if m_tenure >= 1 else "🔴"
s_col1, s_col2, s_col3, s_col4 = st.columns(4)
s_col1.markdown(metric_card("Job", j_lbl, j_ico), unsafe_allow_html=True)
s_col2.markdown(metric_card("Env.", e_lbl, e_ico), unsafe_allow_html=True)
s_col3.markdown(metric_card("Team", r_lbl, r_ico), unsafe_allow_html=True)
s_col4.markdown(metric_card("Mgr.", f"{m_tenure}y", m_ico), unsafe_allow_html=True)
st.markdown("""
<div style="background-color: #f8fafc; padding: 10px; border-radius: 8px; border: 1px dashed #cbd5e1; margin-top: 5px;">
<p style="font-size: 11px; color: #94a3b8; margin: 0;"><b>Job Sat.:</b> Specific role contentment | <b>Env. Qual.:</b> Workspace culture and safety</p>
<p style="font-size: 11px; color: #94a3b8; margin: 0;"><b>Team Rel.:</b> Peer collaboration quality | <b>Mgr. Tenure:</b> Stability under direct leadership</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height:2px;'></div>", unsafe_allow_html=True)
st.markdown('<div class="section-header">💡 Strategic Perspective Insights</div>', unsafe_allow_html=True)
m_col1, m_col2, m_col3 = st.columns(3)
m_col1.markdown(metric_card("Intensity", ins["Work Intensity"], "🔥"), unsafe_allow_html=True)
m_col2.markdown(metric_card("Income Status", ins["Income Alignment"], "⚖️"), unsafe_allow_html=True)
m_col3.markdown(metric_card("Stability", ins["Career Stability"], "🛡️"), unsafe_allow_html=True)
st.markdown("""
<div style="background-color: #f8fafc; padding: 10px; border-radius: 8px; border: 1px dashed #cbd5e1; margin-top: 5px;">
<p style="font-size: 11px; color: #94a3b8; margin: 0;"><b>Intensity:</b> Workload stress vs life balance | <b>Income Status:</b> Salary vs role expectations</p>
<p style="font-size: 11px; color: #94a3b8; margin: 0;"><b>Stability:</b> Overall career tenure pattern and retention likelihood</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height:10px;'></div>", unsafe_allow_html=True)
# 2. Risk Dynamics (Right)
with col2:
with st.container(border=True):
# Risk Status Banner
is_risky = res['prediction'] == "Leave"
if is_risky:
st.error("🔴 **HIGH RISK — LIKELY TO LEAVE**", icon="🚨")
else:
st.success("🟢 **LOW RISK — LIKELY TO STAY**", icon="✅")
# Risk Distribution
risk_color = "#dc2626" if is_risky else "#059669"
st.markdown(f'<div class="risk-score" style="color:{risk_color};">{res["risk"]:.1%}</div>', unsafe_allow_html=True)
st.markdown(f'<p style="text-align:center; color:#64748b; font-size:14px; margin-top:-10px;">Probability of Attrition</p>', unsafe_allow_html=True)
st.progress(res['risk'])
st.markdown("<br>", unsafe_allow_html=True)
# Precision Audit
audit_col1, audit_col2, audit_col3 = st.columns(3)
audit_col1.markdown(metric_card("Forecast", res['prediction'], "🔮"), unsafe_allow_html=True)
audit_col2.markdown(metric_card("Reality", res['actual'], "👁️"), unsafe_allow_html=True)
is_correct = res['prediction'] == res['actual']
match_label = "Correct" if is_correct else "Incorrect"
match_icon = "✅" if is_correct else "❌"
audit_col3.markdown(metric_card("Audit Result", match_label, match_icon), unsafe_allow_html=True)
st.divider()
# Attrition Drivers (Grouped)
st.markdown('<div class="section-header">🔍 Key Risk Drivers</div>', unsafe_allow_html=True)
s_drivers = res['structured_drivers']
main_pts = s_drivers.get("Main Drivers", ["No strong drivers"])
detailed = s_drivers.get("Detailed Drivers", [])
# Conditional display based on Prediction status
if res['prediction'] == "Stay":
st.info("✅ **This employee is currently likely to stay.**")
if main_pts and main_pts[0] != "No strong drivers":
with st.expander("🔍 Potential Future Risks (Low Emphasis)"):
st.caption("These are secondary signals that do not currently indicate a threat but may warrant long-term monitoring.")
for pt in main_pts:
st.markdown(f"**{pt.upper()}**")
cat_upper = pt.upper()
matches = [d for d in detailed if f"[{cat_upper}]" in d.upper()]
for m in matches:
_, factor = m.split("] ", 1) if "] " in m else ("", m)
st.write(f"• {factor}")
elif main_pts and main_pts[0] != "No strong drivers":
for pt in main_pts:
# Show Category Title (Red Badge)
st.markdown(f"""
<div style="background-color: #fce7e7; border: 1px solid #f87171; padding: 10px; border-radius: 8px 8px 0 0; margin-top: 10px; color: #991b1b; font-weight:700; text-align:center; font-size: 13px;">
🚨 {pt.upper()}
</div>
""", unsafe_allow_html=True)
# Show Matching Detailed Factors (Blue Information Box)
cat_upper = pt.upper()
matches = [d for d in detailed if f"[{cat_upper}]" in d.upper()]
if matches:
match_html = ""
for m in matches:
_, factor = m.split("] ", 1) if "] " in m else ("", m)
match_html += f'<div style="font-size: 12px; color: #1e40af; margin-bottom: 4px;">• {factor}</div>'
st.markdown(f"""
<div style="background-color: #eff6ff; border: 1px solid #bfdbfe; border-top: none; padding: 10px; border-radius: 0 0 8px 8px; margin-bottom: 15px;">
{match_html}
</div>
""", unsafe_allow_html=True)
else:
st.markdown("<div style='height:10px;'></div>", unsafe_allow_html=True)
else:
st.success("No critical risk drivers identified based on current policy.")
# Vertical balancing
st.markdown("<div style='height:10px;'></div>", unsafe_allow_html=True)
st.divider()
# 3. Decision Narrative (Strategic Summary)
st.subheader("💡 Strategic Perspective")
has_drivers = len(main_pts) > 0 and main_pts[0] != "No strong drivers"
risk_summary = " and ".join([p.lower() for p in main_pts[:3]]) if has_drivers else "baseline factors"
# Qualitative Risk intensity
r = res['risk']
if r < 0.10: risk_int = "NEGLIGIBLE"
elif r < 0.25: risk_int = "LOW"
elif r < 0.40: risk_int = "EMERGENT"
elif r < 0.60: risk_int = "SIGNIFICANT"
elif r < 0.85: risk_int = "HIGH RISK"
else: risk_int = "SEVERE"
# Strategic Guidance/Advice
if r > threshold:
# Identify top dynamic mitigations
top_actions = [a.replace("⚠️ ", "").replace("✔️ ", "").replace("PRIORITY: ", "") for a in res['actions'][:2]]
action_summary = ", ".join(top_actions).lower() if top_actions else "engagement and feedback improvements"
if "High Value" in res['value_label']:
advice = f"<b>Mission-Critical Retention.</b> Irreplaceable expertise at risk. <b>Mitigate via:</b> {action_summary}, and personalized leadership support."
elif "Valuable" in res['value_label']:
advice = f"<b>High Retention Priority.</b> Core stability asset at risk. <b>Mitigate via:</b> {action_summary}."
elif "Average" in res['value_label']:
advice = f"<b>General Mitigation.</b> Preventable risk detected. <b>Mitigate via:</b> {action_summary}."
else:
advice = f"<b>Strategic Evaluation.</b> Low-impact profile at high risk. <b>Recommendation:</b> Assess if the cost of retention/intervention exceeds the impact of departure. Focus on standard exit documentation."
else:
if r < 0.10:
advice = "<b>Stable Engagement.</b> No attrition markers detected. Continue standard professional development rituals."
else:
advice = "<b>Stable Retention Outlook.</b> Minimal risk profile. Routine management interactions are sufficient to maintain current state."
# Hardcoded styling for the Executive Summary (Always Red)
summary_bg = "#fef2f2"
summary_border = "#dc2626"
# 3. Decision Narrative (Strategic Summary)
# Narrative scaling based on risk
risk_narrative = "driven primarily by" if r > threshold else "with minor signals observed in"
advice_html = f"<li><b>Strategic Guidance:</b> {advice}</li>" if advice else ""
st.markdown(f"""
<div style="background-color: {summary_bg}; border-left: 5px solid {summary_border}; padding: 20px; border-radius: 8px;">
<strong style="font-size: 18px;">Executive Summary:</strong><br>
<div style="margin-top: 15px; font-size: 15px; color: #1e293b; line-height: 1.8;">
<ul style="padding-left: 20px; margin: 0;">
<li><b>Workforce Classification:</b> Identified as a <b>{res['value_label']}</b> (based on tenure, performance, and role level).</li>
<li><b>Risk Assessment:</b> Detected a <b>{risk_int}</b> profile (<b>{r:.1%}</b> probability), {risk_narrative} <b>{risk_summary}</b>.</li>{advice_html}
</ul>
</div>
</div>
""", unsafe_allow_html=True)
st.divider()
# 4. Challenges vs. Actions (Side-by-Side)
col_problems, col_actions = st.columns(2)
with col_problems:
st.subheader("❗ Identified Challenges")
for p in res['problems']:
st.markdown(f"- {p}")
with col_actions:
st.subheader("🎯 Retention Plan")
for act in res['actions']:
if "PRIORITY" in act:
st.markdown(f"<div style='background-color: #fffbeb; color: #b45309; padding: 6px 12px; border-radius: 6px; border: 1px solid #fde68a; font-weight: 700; font-size: 12px; display: inline-block; margin-bottom: 12px; border-left: 4px solid #f59e0b;'>⚠️ {act}</div>", unsafe_allow_html=True)
else:
st.success(f"✔️ {act}")
else:
# Empty State
st.markdown("""
<div style="text-align: center; padding: 100px; color: #64748b;">
<h2 style="font-weight: 300;">Ready to analyze your workforce?</h2>
<p>Click the button above to generate a strategic employee analysis scenario.</p>
</div>
""", unsafe_allow_html=True)
with tab2:
# --- Dynamic Logic for Current Employee ---
if 'strategic_analysis' in st.session_state:
res = st.session_state.strategic_analysis
row = res['X_row']
# Re-calculate points to identify specific path (Synced with Attrition_system logic)
score_details = []
if row.get('JobLevel', 0) >= 3: score_details.append("JL")
if row.get('YearsAtCompany', 0) >= 5: score_details.append("TC")
if row.get('Income_per_Level', 0) > 3000: score_details.append("IE")
if row.get('YearsWithCurrManager', 0) >= 3: score_details.append("MT")
if row.get('Years_per_Promotion', 0) > 2.5: score_details.append("PV")
if row.get('PerformanceRating', 0) == 3: score_details.append("PR3")
if row.get('PerformanceRating', 0) >= 4: score_details.append("PR4")
if row.get('JobInvolvement', 0) >= 3: score_details.append("JI")
if row.get('YearsAtCompany', 0) < 2: score_details.append("ST")
if row.get('NumCompaniesWorked', 0) >= 5: score_details.append("JH")
if row.get('JobSatisfaction', 0) <= 2: score_details.append("LS")
target_tier = {
"High Value Employee": "HV",
"Valuable Employee": "V",
"Average Employee": "A",
"Low Value Employee": "LV"
}.get(res["value_label"], "A")
# Generate Mermaid with Highlights
hl_style = "stroke:#ef4444,stroke-width:4px"
def get_style(key):
return hl_style if key in score_details else ""
st.markdown(f"""
<div style="background-color: #f8fafc; padding: 2rem; border-radius: 16px; border: 1px solid #e2e8f0; margin-bottom: 2rem;">
<h2 style="color: #1e293b; margin-top: 0;">🧠 Personalized Classification Path</h2>
<p style="color: #64748b; font-size: 1.1rem;">Below is the specific logical sequence used to classify <b>{res['raw']['JobRole']}</b> (Index {res['index']}) into the <b>{res['value_label']}</b> tier.</p>
</div>
""", unsafe_allow_html=True)
# Generate Mermaid with Highlights
hl_style = "stroke:#ef4444,stroke-width:4px"
# Build style lines only if applicable
style_lines = []
for key, node_id in [("JL", "JL"), ("TC", "TC"), ("MT", "MT"), ("PR3", "PR3"), ("PR4", "PR4"), ("JI", "JI"), ("IE", "IE"), ("PV", "PV"), ("ST", "ST"), ("JH", "JH"), ("LS", "LS")]:
if key in score_details:
style_lines.append(f" style {node_id} {hl_style}")
# Style ALL possible end tiers, but highlight chosen one specially
style_lines.append(f" style {target_tier} fill:#fdf2f8,stroke:#ec4899,stroke-width:5px")
style_lines.append(f" style Score fill:#eff6ff,stroke:#3b82f6,stroke-width:3px")
def get_arrow(key):
return "==>" if key in score_details else "-->"
style_section = "\n".join(style_lines)
# Pre-calculate arrows to avoid syntax issues in Mermaid
hv_arrow = '== ">= 5" ==>' if target_tier == "HV" else '-- ">= 5" -->'
v_arrow = '== ">= 2" ==>' if target_tier == "V" else '-- ">= 2" -->'
a_arrow = '== ">= 0" ==>' if target_tier == "A" else '-- ">= 0" -->'
lv_arrow = '== "< 0" ==>' if target_tier == "LV" else '-- "< 0" -->'
def get_edge(key, label):
is_active = key in score_details
return f'== "{label}" ==>' if is_active else f'-- "{label}" -->'
mermaid_code = f"""
graph TD
Start([Start: Base Score = 0]) --> Seniority[Seniority & Role]
Seniority {get_edge('JL', '+2 pts')} JL["Job Level >= 3"]
Seniority {get_edge('TC', '+2 pts')} TC["Tenure >= 5y"]
Seniority {get_edge('MT', '+1 pt')} MT["High Mgr tenure"]
JL {"==>" if 'JL' in score_details else "-->"} Perf{{Performance}}
TC {"==>" if 'TC' in score_details else "-->"} Perf
MT {"==>" if 'MT' in score_details else "-->"} Perf
Seniority --> Perf
Perf {get_edge('PR3', '+1 pt')} PR3["Rating == 3"]
Perf {get_edge('PR4', '+2 pts')} PR4["Rating >= 4"]
Perf {get_edge('JI', '+1 pt')} JI["High Involvement (>= 3)"]
Perf {get_edge('IE', '+1 pt')} IE["Income per Level (> 3000)"]
Perf {get_edge('PV', '+1 pt')} PV["Promotion Velocity (> 2.5)"]
PR3 {"==>" if 'PR3' in score_details else "-->"} Status{{Status Factors}}
PR4 {"==>" if 'PR4' in score_details else "-->"} Status
JI {"==>" if 'JI' in score_details else "-->"} Status
IE {"==>" if 'IE' in score_details else "-->"} Status
PV {"==>" if 'PV' in score_details else "-->"} Status
Perf --> Status
Status {get_edge('ST', '-2 pts')} ST["Short Tenure (< 2y)"]
Status {get_edge('JH', '-1 pt')} JH["Job Hopping (5+)"]
Status {get_edge('LS', '-1 pt')} LS["Low Satisfaction (<= 2)"]
ST {"==>" if 'ST' in score_details else "-->"} Score
JH {"==>" if 'JH' in score_details else "-->"} Score
LS {"==>" if 'LS' in score_details else "-->"} Score
Status --> Score
Score["Final Score = {res['value_score']:.1f}"]
Score {hv_arrow} HV([High Value Employee])
Score {v_arrow} V([Valuable Employee])
Score {a_arrow} A([Average Employee])
Score {lv_arrow} LV([Low Value Employee])
{style_section}
"""
import streamlit.components.v1 as components
html_code = f"""
<div class="mermaid" style="display: flex; justify-content: center;">
{mermaid_code.strip()}
</div>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({{
startOnLoad: true,
securityLevel: 'loose',
theme: 'base',
themeVariables: {{
'fontSize': '16px',
'primaryColor': '#eff6ff',
'primaryTextColor': '#1e3a8a',
'primaryBorderColor': '#3b82f6',
'lineColor': '#64748b',
'secondaryColor': '#f8fafc',
'tertiaryColor': '#ffffff'
}},
flowchart: {{
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}}
}});
</script>
"""
components.html(html_code, height=850, width=1200, scrolling=True)
st.divider()
else:
# Generic Flow when no analysis generated
st.markdown("""
<div style="background-color: #f8fafc; padding: 2rem; border-radius: 16px; border: 1px solid #e2e8f0; margin-bottom: 2rem;">
<h2 style="color: #1e293b; margin-top: 0;">🧠 How We Classify Employee Value</h2>
<p style="color: #64748b; font-size: 1.1rem;">Generate an employee scenario in the first tab to see their personalized classification path here.</p>
</div>
""", unsafe_allow_html=True)
generic_mermaid = """
graph TD
Start([Start: Base Score = 0]) --> Seniority[Seniority & Role]
Seniority --> JL["+2: Job Level >= 3"]
Seniority --> TC["+2: Tenure >= 5y"]
Seniority --> MT["+1: High Mgr Tenure"]
JL --> Perf{Performance}
TC --> Perf
MT --> Perf
Seniority --> Perf
Perf --> PR3["+1: Rating == 3"]
Perf --> PR4["+2: Rating >= 4"]
Perf --> JI["+1: Involvement >= 3"]
Perf --> IE["+1: Income per Level > 3000"]
Perf --> PV["+1: Promotion Velocity > 2.5"]
PR3 --> Status{Status Factors}
PR4 --> Status
JI --> Status
IE --> Status
PV --> Status
Perf --> Status
Status --> ST["-2: Tenure < 2y"]
Status --> JH["-1: Job Hopping (5+)"]
Status --> LS["-1: Low Satisfaction (<= 2)"]
ST --> Score[Final Score Calculation]
JH --> Score
LS --> Score
Status --> Score
Score --> HV([High Value Employee])
Score --> V([Valuable Employee])
Score --> A([Average Employee])
Score --> LV([Low Value Employee])
%% Design Styles
style Start fill:#f8fafc,stroke:#64748b,stroke-width:1px
style Seniority fill:#f8fafc,stroke:#64748b,stroke-width:1px
style Perf fill:#eff6ff,stroke:#3b82f6,stroke-width:2px
style Status fill:#eff6ff,stroke:#3b82f6,stroke-width:2px
style Score fill:#f8fafc,stroke:#64748b,stroke-width:1px
classDef booster fill:#eff6ff,stroke:#3b82f6,stroke-width:1px,color:#1e40af;
class JL,TC,MT,PR3,PR4,JI,IE,PV booster
classDef risk fill:#fff1f2,stroke:#f43f5e,stroke-width:1px,color:#9f1239;
class ST,JH,LS risk
style HV fill:#fef3c7,stroke:#fbbf24,stroke-width:3px
style V fill:#dbeafe,stroke:#60a5fa,stroke-width:3px
style A fill:#f1f5f9,stroke:#94a3b8,stroke-width:3px
style LV fill:#fee2e2,stroke:#f87171,stroke-width:3px
"""
import streamlit.components.v1 as components
html_code_gen = f"""
<div class="mermaid">
{generic_mermaid.strip()}
</div>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({{ startOnLoad: true }});
</script>
"""
components.html(html_code_gen, height=600, scrolling=True)
col_rules1, col_rules2 = st.columns(2)
with col_rules1:
st.markdown("""
<div style="background-color: #ecfdf5; border-left: 5px solid #10b981; padding: 1.5rem; border-radius: 8px; height: 100%;">
<h3 style="color: #065f46; margin-top: 0;">➕ Value Boosters (Positive Points)</h3>
<ul style="color: #065f46; line-height: 1.6;">
<li><b>Job Level (>= 3):</b> +2 pts (Senior Leadership Impact)</li>
<li><b>Tenure (>= 5 years):</b> +2 pts (Deep Institutional Knowledge)</li>
<li><b>Performance (Rating == 3):</b> +1 pt (Excellent Core Output)</li>
<li><b>Performance (Rating >= 4):</b> +2 pts (Outstanding Achievement)</li>
<li><b>Income per Level (> 3000):</b> +1 pt (High salary efficiency | Normalized by Job Level)</li>
<li><b>Promotion Velocity (> 2.5):</b> +1 pt (Loyalty & Growth Momentum)</li>
<li><b>Manager Relationship (>= 3y):</b> +1 pt (Team Stability)</li>
<li><b>Job Involvement (>= 3):</b> +1 pt (Strong Active Engagement)</li>
</ul>
</div>
""", unsafe_allow_html=True)
with col_rules2:
st.markdown("""
<div style="background-color: #fff1f2; border-left: 5px solid #f43f5e; padding: 1.5rem; border-radius: 8px; height: 100%;">
<h3 style="color: #9f1239; margin-top: 0;">⚠️ Risk & Adjustment Factors (Negative Points)</h3>
<ul style="color: #9f1239; line-height: 1.6;">
<li><b>Low Tenure (< 2 years):</b> -2 pts (Higher Initial Attrition Risk)</li>
<li><b>Job Hopping (>= 5 cos):</b> -1 pt (Historical Stability Risk)</li>
<li><b>Low Satisfaction (<= 2):</b> -1 pt (Immediate Disengagement Risk)</li>
</ul>
<p style="color: #9f1239; font-style: italic; margin-top: 1rem;">These factors weigh against the positive scores to provide a balanced view of employee "stickiness" and value.</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown("""
<div style="background-color: #f1f5f9; padding: 2rem; border-radius: 12px; border: 1px solid #e2e8f0;">
<h3 style="text-align: center; color: #334155; margin-bottom: 2rem;">🏆 Final Value Tiers</h3>
<div style="display: flex; justify-content: center; flex-wrap: wrap; gap: 1rem;">
<div style="background-color: #fef3c7; border: 2px solid #fbbf24; padding: 1rem; border-radius: 12px; flex: 1; min-width: 200px; text-align: center;">
<div style="font-size: 2rem;">⭐</div>
<h4 style="color: #92400e; margin: 0.5rem 0;">High Value</h4>
<p style="font-weight: 700; color: #b45309; font-size: 1.2rem;">Score >= 5</p>
<p style="font-size: 0.8rem; color: #d97706;">Mission-critical talent, key leadership.</p>
</div>
<div style="background-color: #dbeafe; border: 2px solid #60a5fa; padding: 1rem; border-radius: 12px; flex: 1; min-width: 200px; text-align: center;">
<div style="font-size: 2rem;">💎</div>
<h4 style="color: #1e40af; margin: 0.5rem 0;">Valuable</h4>
<p style="font-weight: 700; color: #1d4ed8; font-size: 1.2rem;">Score >= 2</p>
<p style="font-size: 0.8rem; color: #2563eb;">Stable core talent, high potential.</p>
</div>
<div style="background-color: #f1f5f9; border: 2px solid #94a3b8; padding: 1rem; border-radius: 12px; flex: 1; min-width: 200px; text-align: center;">
<div style="font-size: 2rem;">👤</div>
<h4 style="color: #475569; margin: 0.5rem 0;">Average</h4>
<p style="font-weight: 700; color: #64748b; font-size: 1.2rem;">Score >= 0</p>
<p style="font-size: 0.8rem; color: #475569;">Solid contributors, standard performance.</p>
</div>
<div style="background-color: #fee2e2; border: 2px solid #f87171; padding: 1rem; border-radius: 12px; flex: 1; min-width: 200px; text-align: center;">
<div style="font-size: 2rem;">⚠️</div>
<h4 style="color: #991b1b; margin: 0.5rem 0;">Low Value</h4>
<p style="font-weight: 700; color: #b91c1c; font-size: 1.2rem;">Score < 0</p>
<p style="font-size: 0.8rem; color: #dc2626;">At-risk performance or low engagement.</p>
</div>
</div>
</div>
""", unsafe_allow_html=True)