CoolFace
Apppublic

Peter512/developer-salary-predictor

sourceHugging Faceapache-2.0updated 1y agoView on Hugging Face
1likes
app.py712 linesDownload Raw Back to root
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!")