Peter512/developer-salary-predictor
1
1import streamlit as st2import pandas as pd3import numpy as np4import pickle5import shap6import matplotlib.pyplot as plt7import seaborn as sns8from streamlit_shap import st_shap9 10# Page configuration11st.set_page_config(12 page_title="EU Developer Salary Predictor",13 page_icon="๐ฐ",14 layout="wide",15 initial_sidebar_state="expanded"16)17 18# Custom CSS for better styling19st.markdown("""20 <style>21 .main-header {22 font-size: 2.8rem;23 font-weight: bold;24 color: #1f77b4;25 text-align: center;26 margin-bottom: 1rem;27 }28 .sub-header {29 font-size: 1.2rem;30 color: #666;31 text-align: center;32 margin-bottom: 2rem;33 }34 .prediction-box {35 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);36 padding: 2rem 2.5rem;37 border-radius: 15px;38 text-align: center;39 margin: 1.5rem 0;40 box-shadow: 0 10px 30px rgba(0,0,0,0.2);41 }42 .prediction-value {43 font-size: 3.5rem;44 font-weight: bold;45 color: white;46 text-shadow: 2px 2px 4px rgba(0,0,0,0.3);47 margin: 0.5rem 0;48 }49 .prediction-label {50 font-size: 1.1rem;51 color: rgba(255,255,255,0.95);52 margin-bottom: 0.5rem;53 font-weight: 500;54 }55 .metric-card {56 background-color: #f8f9fa;57 padding: 1rem;58 border-radius: 10px;59 border-left: 4px solid #1f77b4;60 margin: 0.5rem 0;61 }62 .sidebar .sidebar-content {63 background-color: #f8f9fa;64 }65 /* Ensure tab text is always visible */66 .stTabs [data-baseweb="tab-list"] button {67 color: #262730 !important;68 }69 .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {70 background-color: #1f77b4 !important;71 color: white !important;72 }73 .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] p {74 color: white !important;75 }76 .stTabs [data-baseweb="tab-list"] {77 gap: 1rem;78 background-color: #f8f9fa;79 padding: 0.5rem;80 border-radius: 10px;81 }82 .stTabs [data-baseweb="tab"] {83 height: 50px;84 padding: 0 20px;85 border-radius: 8px;86 color: #262730;87 font-weight: 500;88 }89 .stTabs [data-baseweb="tab"]:hover {90 background-color: #e9ecef;91 }92 .stTabs [aria-selected="true"] {93 background-color: #1f77b4;94 color: white !important;95 }96 </style>97""", unsafe_allow_html=True)98 99# Load model and info100@st.cache_resource101def load_model():102 with open('salary_model.pkl', 'rb') as f:103 model = pickle.load(f)104 with open('model_info.pkl', 'rb') as f:105 info = pickle.load(f)106 return model, info107 108try:109 model_pipeline, model_info = load_model()110except Exception as e:111 st.error(f"โ Error loading model: {e}")112 st.stop()113 114# Feature options - Only countries using EUR or commonly reporting in EUR115COUNTRY_OPTIONS = [116 'Austria',117 'Belgium', 118 'France',119 'Germany',120 'Ireland',121 'Italy',122 'Netherlands',123 'Portugal',124 'Spain'125]126 127ED_LEVEL_OPTIONS = [128 "Primary/elementary school",129 "Secondary school (e.g. American high school, German Realschule or Gymnasium, etc.)",130 "Some college/university study without earning a degree",131 "Associate degree (A.A., A.S., etc.)",132 "Bachelor's degree (B.A., B.S., B.Eng., etc.)",133 "Master's degree (M.A., M.S., M.Eng., MBA, etc.)",134 "Professional degree (JD, MD, Ph.D, Ed.D, etc.)",135 "Something else"136]137 138DEV_TYPE_OPTIONS = [139 'Developer, back-end', 'Developer, full-stack', 'Developer, front-end',140 'Engineering manager', 'Developer, desktop or enterprise applications',141 'Developer, mobile', 'DevOps specialist', 'Data scientist or machine learning specialist',142 'Data or business analyst', 'System administrator', 'Developer, QA or test',143 'Product manager', 'Other'144]145 146ORG_SIZE_OPTIONS = [147 '2 to 9 employees',148 '10 to 19 employees',149 '20 to 99 employees',150 '100 to 499 employees',151 '500 to 999 employees',152 '1,000 to 4,999 employees',153 '5,000 to 9,999 employees',154 '10,000 or more employees'155]156 157REMOTE_WORK_OPTIONS = [158 'Hybrid (some remote, some in-person)', 'Fully remote', 'In-person'159]160 161# ============================================================================162# SIDEBAR - INPUT FORM163# ============================================================================164with st.sidebar:165 st.title("๐ฏ Developer Profile")166 st.markdown("---")167 168 # Personal Information169 st.subheader("๐ค Personal")170 age_group = st.selectbox(171 "Age Group",172 options=[1, 2, 3, 4, 5],173 format_func=lambda x: {174 1: "18-24 years", 2: "25-34 years", 3: "35-44 years",175 4: "45-54 years", 5: "55+ years"176 }[x],177 key="age"178 )179 180 years_code_pro = st.slider(181 "Years of Experience",182 min_value=0, max_value=40, value=5,183 key="years"184 )185 186 country = st.selectbox(187 "Country",188 options=COUNTRY_OPTIONS,189 key="country"190 )191 192 st.markdown("---")193 194 # Professional Information195 st.subheader("๐ผ Professional")196 197 dev_type = st.selectbox(198 "Developer Type",199 options=DEV_TYPE_OPTIONS,200 key="dev_type"201 )202 203 ed_level = st.selectbox(204 "Education Level",205 options=ED_LEVEL_OPTIONS,206 key="ed_level"207 )208 209 org_size = st.selectbox(210 "Organization Size",211 options=ORG_SIZE_OPTIONS,212 key="org_size"213 )214 215 remote_work = st.selectbox(216 "Work Arrangement",217 options=REMOTE_WORK_OPTIONS,218 key="remote"219 )220 221 st.markdown("---")222 223 # Additional Information224 st.subheader("โ๏ธ Additional")225 226 so_account = st.checkbox(227 "Stack Overflow Account",228 value=True,229 key="so"230 )231 232 ai_select = st.checkbox(233 "Uses AI Tools",234 value=True,235 key="ai"236 )237 238 st.markdown("---")239 240 # Predict button in sidebar241 predict_button = st.button(242 "๐ฎ Predict Salary",243 type="primary",244 use_container_width=True,245 key="predict_btn"246 )247 248 st.markdown("---")249 250 # MODEL DETAILS - Moved to bottom of sidebar251 st.subheader("๐ Model Details")252 st.markdown("""253 - **Data Source**: Stack Overflow 2024 Survey254 - **Sample**: 7,000+ European developers255 - **Algorithm**: Optimized Random Forest256 - **Accuracy**: RMSE ~โฌ18,600257 - **Last Updated**: 2025258 """)259 260# ============================================================================261# MAIN CONTENT AREA262# ============================================================================263 264# Header265st.markdown('<div class="main-header">๐ฐ European Developer Salary Predictor</div>', unsafe_allow_html=True)266st.markdown('<div class="sub-header">Salary estimation for European software developers</div>', unsafe_allow_html=True)267 268# Handle prediction269if predict_button:270 # Create input dataframe271 input_data = pd.DataFrame({272 'age_group': [age_group],273 'years_code_pro': [years_code_pro],274 'remote_work': [remote_work],275 'ed_level': [ed_level],276 'dev_type': [dev_type],277 'org_size': [org_size],278 'country': [country],279 'so_account': [so_account],280 'ai_select': [ai_select]281 })282 283 # Make prediction284 prediction = model_pipeline.predict(input_data)[0]285 286 # Store in session state287 st.session_state['current_input'] = input_data288 st.session_state['current_prediction'] = prediction289 st.session_state['has_prediction'] = True290 291# Show results if prediction exists292if st.session_state.get('has_prediction', False):293 prediction = st.session_state['current_prediction']294 295 # Display main prediction296 st.markdown("""297 <div class="prediction-box">298 <div class="prediction-label">Predicted Annual Salary</div>299 <div class="prediction-value">โฌ{:,.0f}</div>300 </div>301 """.format(prediction), unsafe_allow_html=True)302 303 # Breakdown metrics304 col1, col2, col3, col4 = st.columns(4)305 with col1:306 st.metric("๐ฐ Annual", f"โฌ{prediction:,.0f}")307 with col2:308 st.metric("๐
Monthly", f"โฌ{prediction/12:,.0f}")309 with col3:310 st.metric("๐ Weekly", f"โฌ{prediction/52:,.0f}")311 with col4:312 st.metric("โฐ Hourly", f"โฌ{prediction/2080:,.0f}")313 314 st.markdown("---")315 316 # Tabs for detailed analysis317 tab1, tab2, tab3 = st.tabs(["๐ Model Insights", "๐ What-If Analysis", "โน๏ธ About Prediction"])318 319 # TAB 1: MODEL INSIGHTS320 with tab1:321 st.header("๐ง Understanding Your Prediction")322 st.write("See which factors had the biggest impact on your predicted salary.")323 324 input_data = st.session_state['current_input']325 326 # Transform input327 preprocessor = model_pipeline.named_steps['preprocessor']328 model = model_pipeline.named_steps['regressor']329 330 X_transformed = preprocessor.transform(input_data)331 feature_names = list(preprocessor.get_feature_names_out())332 333 # Create SHAP explainer334 with st.spinner("Calculating feature impacts..."):335 explainer = shap.TreeExplainer(model)336 shap_values = explainer.shap_values(X_transformed)337 338 if isinstance(explainer.expected_value, np.ndarray):339 expected_value = float(explainer.expected_value[0])340 else:341 expected_value = float(explainer.expected_value)342 343 st.subheader("๐ฏ Feature Impact Visualization")344 st.write(f"**Base salary** (average): โฌ{expected_value:,.0f}")345 st.write("Features in **red** increase your salary. Features in **blue** decrease it.")346 347 # Force plot348 st_shap(shap.force_plot(349 expected_value,350 shap_values[0],351 X_transformed[0],352 feature_names=feature_names353 ))354 355 st.markdown("---")356 357 # Feature contribution table358 col1, col2 = st.columns([2, 1])359 360 with col1:361 st.subheader("๐ Top Contributing Factors")362 363 # Create feature mapping WITHOUT emojis for chart364 def clean_feature_name(feature):365 """Convert technical feature names to user-friendly labels WITHOUT emojis"""366 # Remove prefixes367 feature = feature.replace('cat__', '').replace('num__', '').replace('remainder__', '')368 369 # Simple mappings370 simple_map = {371 'years_code_pro': 'Years of Experience',372 'age_group': 'Age Group',373 'so_account': 'Stack Overflow Account',374 'ai_select': 'Uses AI Tools'375 }376 377 if feature in simple_map:378 return simple_map[feature]379 380 # Handle categorical variables381 replacements = {382 'country_': 'Country: ',383 'remote_work_': 'Work: ',384 'dev_type_': 'Role: ',385 'org_size_': 'Company Size: ',386 'ed_level_': 'Education: '387 }388 389 for prefix, label in replacements.items():390 if prefix in feature:391 return label + feature.replace(prefix, '').replace('_', ' ')392 393 # Fallback394 return feature.replace('_', ' ').title()395 396 # Create emoji version for the table only397 def clean_feature_name_with_emoji(feature):398 """Convert technical feature names to user-friendly labels WITH emojis"""399 base_name = clean_feature_name(feature)400 401 # Add emojis based on content402 if 'Years of Experience' in base_name:403 return 'โฑ๏ธ ' + base_name404 elif 'Age Group' in base_name:405 return '๐ค ' + base_name406 elif 'Country:' in base_name:407 return '๐ ' + base_name408 elif 'Work:' in base_name:409 return '๐ ' + base_name410 elif 'Role:' in base_name:411 return '๐ป ' + base_name412 elif 'Company Size:' in base_name:413 return '๐ข ' + base_name414 elif 'Education:' in base_name:415 return '๐ ' + base_name416 elif 'Stack Overflow' in base_name:417 return '๐ ' + base_name418 elif 'AI Tools' in base_name:419 return '๐ค ' + base_name420 421 return base_name422 423 shap_df = pd.DataFrame({424 'Feature': feature_names,425 'SHAP Value': shap_values[0],426 'Impact': ['โฌ๏ธ Increases' if x > 0 else 'โฌ๏ธ Decreases' for x in shap_values[0]]427 })428 shap_df['Abs SHAP'] = shap_df['SHAP Value'].abs()429 shap_df = shap_df.sort_values('Abs SHAP', ascending=False).head(10)430 431 # Clean feature names - NO emojis for chart, WITH emojis for table432 shap_df['Feature_Clean'] = shap_df['Feature'].apply(clean_feature_name)433 shap_df['Feature_Clean_Emoji'] = shap_df['Feature'].apply(clean_feature_name_with_emoji)434 435 # Create visualization with improved styling436 fig, ax = plt.subplots(figsize=(10, 6))437 438 # Modern color scheme439 colors = ['#10b981' if x > 0 else '#ef4444' for x in shap_df['SHAP Value']]440 441 # Create bars442 bars = ax.barh(range(len(shap_df)), shap_df['SHAP Value'], color=colors, alpha=0.85, height=0.7)443 444 # Add value labels on bars - improved positioning445 max_abs_value = shap_df['Abs SHAP'].max()446 447 for i, (bar, value) in enumerate(zip(bars, shap_df['SHAP Value'])):448 abs_value = abs(value)449 450 # For large bars (>30% of max), place label inside451 # For small bars, place label outside452 if abs_value > max_abs_value * 0.3:453 # Inside the bar454 x_pos = value / 2455 color = 'white'456 ha = 'center'457 else:458 # Outside the bar459 offset = max_abs_value * 0.05 # 5% of max value as offset460 x_pos = value + (offset if value > 0 else -offset)461 color = '#10b981' if value > 0 else '#ef4444'462 ha = 'left' if value > 0 else 'right'463 464 ax.text(x_pos, i, f'โฌ{abs_value:,.0f}',465 ha=ha, va='center', 466 fontweight='bold', fontsize=10,467 color=color)468 469 # Set labels with cleaned names WITHOUT EMOJIS470 ax.set_yticks(range(len(shap_df)))471 ax.set_yticklabels(shap_df['Feature_Clean'], fontsize=10)472 ax.set_xlabel('Impact on Salary (EUR)', fontsize=11, fontweight='bold')473 ax.set_title('How Different Factors Affect Your Salary', 474 fontsize=13, fontweight='bold', pad=20)475 476 # Add zero line477 ax.axvline(x=0, color='#64748b', linestyle='-', linewidth=2, alpha=0.5)478 479 # Add legend480 from matplotlib.patches import Patch481 legend_elements = [482 Patch(facecolor='#10b981', alpha=0.85, label='Increases Salary'),483 Patch(facecolor='#ef4444', alpha=0.85, label='Decreases Salary')484 ]485 ax.legend(handles=legend_elements, loc='upper right', frameon=True, 486 fancybox=True, shadow=True, fontsize=10)487 488 # Styling489 ax.grid(axis='x', alpha=0.2, linestyle='--')490 ax.set_facecolor('#f8fafc')491 fig.patch.set_facecolor('white')492 ax.spines['top'].set_visible(False)493 ax.spines['right'].set_visible(False)494 495 plt.tight_layout()496 st.pyplot(fig)497 498 # Add explanation box with improved text499 st.info("""500 **๐ก How to read this chart:**501 - **Green bars** pointing right โ These factors *increase* your salary502 - **Red bars** pointing left โ These factors *decrease* your salary 503 - **Longer bars** = Bigger impact on your predicted salary504 - **Why do I see other countries/categories I didn't select?** The chart shows the top 10 most impactful features for your prediction. When you see a **red bar** for a category you *didn't* select (like other countries), it means "not having this characteristic lowers your salary compared to having it." For example, if you see "Country: Germany" with a **red bar showing โฌ1,059**, it means being from Germany would have added โฌ1,059 to your salary compared to your current country.505 """)506 507 with col2:508 st.subheader("๐ Impact Details")509 510 # Format the table with cleaned names (WITH emojis for table)511 display_df = shap_df[['Feature_Clean_Emoji', 'SHAP Value']].copy()512 display_df.columns = ['Factor', 'Impact Amount']513 display_df['Impact Amount'] = display_df['Impact Amount'].apply(514 lambda x: f"+โฌ{x:,.0f}" if x > 0 else f"-โฌ{abs(x):,.0f}"515 )516 display_df = display_df.reset_index(drop=True)517 518 st.dataframe(519 display_df,520 use_container_width=True,521 hide_index=True522 )523 524 # TAB 2: WHAT-IF ANALYSIS525 with tab2:526 st.header("๐ What-If Scenario Analysis")527 st.write("Explore how changing different factors affects your predicted salary.")528 529 original_input = st.session_state['current_input'].copy()530 original_prediction = st.session_state['current_prediction']531 532 col1, col2 = st.columns([1, 2])533 534 with col1:535 st.subheader("๐๏ธ Modify Factor")536 537 feature_to_change = st.selectbox(538 "Select factor to modify",539 options=['years_code_pro', 'country', 'remote_work', 'dev_type', 'org_size', 'ed_level'],540 format_func=lambda x: {541 'years_code_pro': 'โฑ๏ธ Years of Experience',542 'country': '๐ Country',543 'remote_work': '๐ Work Arrangement',544 'dev_type': '๐ป Developer Type',545 'org_size': '๐ข Organization Size',546 'ed_level': '๐ Education Level'547 }[x]548 )549 550 modified_input = original_input.copy()551 552 if feature_to_change == 'years_code_pro':553 new_value = st.slider(554 "New years of experience",555 min_value=0, max_value=40,556 value=int(original_input[feature_to_change].values[0]),557 key="what_if_years"558 )559 modified_input[feature_to_change] = new_value560 561 elif feature_to_change == 'country':562 new_value = st.selectbox("New country", COUNTRY_OPTIONS, key="what_if_country")563 modified_input[feature_to_change] = new_value564 565 elif feature_to_change == 'remote_work':566 new_value = st.selectbox("New work arrangement", REMOTE_WORK_OPTIONS, key="what_if_remote")567 modified_input[feature_to_change] = new_value568 569 elif feature_to_change == 'dev_type':570 new_value = st.selectbox("New developer type", DEV_TYPE_OPTIONS, key="what_if_dev")571 modified_input[feature_to_change] = new_value572 573 elif feature_to_change == 'org_size':574 new_value = st.selectbox("New org size", ORG_SIZE_OPTIONS, key="what_if_org")575 modified_input[feature_to_change] = new_value576 577 elif feature_to_change == 'ed_level':578 new_value = st.selectbox("New education level", ED_LEVEL_OPTIONS, key="what_if_ed")579 modified_input[feature_to_change] = new_value580 581 # Calculate comparison button582 if st.button("๐ Compare Scenarios", use_container_width=True):583 st.session_state['comparison_active'] = True584 st.session_state['modified_input'] = modified_input585 586 with col2:587 if st.session_state.get('comparison_active', False):588 modified_input = st.session_state['modified_input']589 modified_prediction = model_pipeline.predict(modified_input)[0]590 difference = modified_prediction - original_prediction591 percent_change = (difference / original_prediction) * 100592 593 st.subheader("๐ Comparison Results")594 595 # Visual comparison596 fig, ax = plt.subplots(figsize=(10, 5))597 scenarios = ['Current\nProfile', 'Modified\nProfile']598 salaries = [original_prediction, modified_prediction]599 colors = ['#3498db', '#e74c3c' if difference < 0 else '#2ecc71']600 601 bars = ax.bar(scenarios, salaries, color=colors, alpha=0.7, width=0.6)602 603 # Add value labels on bars604 for bar, salary in zip(bars, salaries):605 height = bar.get_height()606 ax.text(bar.get_x() + bar.get_width()/2., height,607 f'โฌ{salary:,.0f}',608 ha='center', va='bottom', fontweight='bold', fontsize=12)609 610 ax.set_ylabel('Annual Salary (EUR)', fontweight='bold', fontsize=11)611 ax.set_title('Salary Comparison', fontweight='bold', fontsize=13, pad=20)612 ax.grid(axis='y', alpha=0.3)613 614 plt.tight_layout()615 st.pyplot(fig)616 617 # Summary metrics618 col_a, col_b, col_c = st.columns(3)619 with col_a:620 st.metric("Current Salary", f"โฌ{original_prediction:,.0f}")621 with col_b:622 st.metric("Modified Salary", f"โฌ{modified_prediction:,.0f}")623 with col_c:624 st.metric("Difference", f"โฌ{abs(difference):,.0f}", 625 f"{percent_change:+.1f}%")626 627 # Interpretation628 if difference > 0:629 st.success(f"โ
This change would **increase** your salary by โฌ{difference:,.0f} ({percent_change:.1f}%)")630 elif difference < 0:631 st.error(f"โ ๏ธ This change would **decrease** your salary by โฌ{abs(difference):,.0f} ({percent_change:.1f}%)")632 else:633 st.info("โก๏ธ This change has **no significant impact** on salary")634 else:635 st.info("๐ Select a factor to modify and click 'Compare Scenarios' to see the impact")636 637 # TAB 3: ABOUT PREDICTION638 with tab3:639 st.header("โน๏ธ About This Prediction")640 641 col1, col2 = st.columns(2)642 643 with col1:644 st.subheader("๐ Model Information")645 st.markdown("""646 - **Algorithm**: Random Forest Regressor (Optimized)647 - **Training Data**: Stack Overflow 2024 Developer Survey648 - **Sample Size**: 7,000+ European developers649 - **Model Accuracy**: RMSE โ โฌ18,600650 - **Features Used**: 9 key factors651 """)652 653 st.subheader("๐ฏ Prediction Confidence")654 st.info("This model performs best for developers with 0-20 years of experience in Eurozone countries. Average prediction error: ยฑโฌ18,600")655 656 with col2:657 st.subheader("๐ Your Profile Summary")658 659 profile_data = {660 'Factor': ['Age Group', 'Experience', 'Country', 'Developer Type', 661 'Education', 'Org Size', 'Work Arrangement', 'SO Account', 'Uses AI'],662 'Value': [663 {1: "18-24", 2: "25-34", 3: "35-44", 4: "45-54", 5: "55+"}[age_group],664 f"{years_code_pro} years",665 country,666 dev_type,667 ed_level[:30] + "..." if len(ed_level) > 30 else ed_level,668 org_size,669 remote_work,670 "Yes" if so_account else "No",671 "Yes" if ai_select else "No"672 ]673 }674 675 st.dataframe(676 pd.DataFrame(profile_data),677 use_container_width=True,678 hide_index=True679 )680 681 st.markdown("---")682 st.warning("""683 **โ ๏ธ Important Disclaimer**: This prediction is an **estimate** based on historical survey data. 684 Actual salaries can vary significantly based on:685 - Specific technical skills and expertise686 - Company size, stage, and funding687 - Individual negotiation and performance688 - Local market conditions and demand689 - Benefits, equity, and other compensation690 691 Use this tool as a **reference point**, not a definitive salary expectation.692 """)693 694else:695 # Only show the instructions when no prediction has been made696 if not st.session_state.get("has_prediction", False):697 st.markdown("---")698 st.info("๐ **Get Started**: Fill in your profile in the sidebar and click **'Predict Salary'** to see your results!")699 700 col1, col2, col3 = st.columns(3)701 702 with col1:703 st.markdown("### ๐ฏ Step 1")704 st.write("Enter personal information (age, experience, country)")705 706 with col2:707 st.markdown("### ๐ผ Step 2")708 st.write("Add professional details (role, education, company)")709 710 with col3:711 st.markdown("### ๐ฎ Step 3")712 st.write("Click **'Predict Salary'** to see your estimate!")