|
|
import pandas as pd |
|
|
import streamlit as st |
|
|
import joblib |
|
|
import time |
|
|
from datetime import datetime |
|
|
from sklearn.pipeline import Pipeline |
|
|
from sklearn.compose import ColumnTransformer |
|
|
from sklearn.preprocessing import StandardScaler, OneHotEncoder |
|
|
from PIL import Image, ImageFile |
|
|
ImageFile.LOAD_TRUNCATED_IMAGES = True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_page_config(): |
|
|
st.set_page_config( |
|
|
page_title="WageWise", |
|
|
page_icon="š¼", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
:root { |
|
|
--primary: #012326; |
|
|
--secondary: #011a1c; |
|
|
--accent: #014d4f; |
|
|
--success: #017a7c; |
|
|
--text: #e0f2f3; |
|
|
--light: #f8f9fa; |
|
|
--gray: #adb5bd; |
|
|
--dark: #010f10; |
|
|
} |
|
|
|
|
|
/* Main container */ |
|
|
.main { |
|
|
background-color: #f9fafc; |
|
|
} |
|
|
|
|
|
/* Sidebar styling */ |
|
|
[data-testid="stSidebar"] { |
|
|
background: linear-gradient(180deg, #012326 0%, #011a1c 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
/* Input fields */ |
|
|
.stTextInput, .stNumberInput, .stSelectbox { |
|
|
border-radius: 8px !important; |
|
|
border: 1px solid #e2e8f0 !important; |
|
|
} |
|
|
|
|
|
/* Input labels - make them visible on dark background */ |
|
|
.stSelectbox > label, .stNumberInput > label, .stTextInput > label { |
|
|
color: white !important; |
|
|
font-weight: 500 !important; |
|
|
} |
|
|
|
|
|
/* Buttons */ |
|
|
.stButton>button { |
|
|
background: linear-gradient(90deg, #012326 0%, #014d4f 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
padding: 12px 24px; |
|
|
font-weight: 500; |
|
|
transition: all 0.3s ease; |
|
|
box-shadow: 0 2px 10px rgba(1, 35, 38, 0.3); |
|
|
} |
|
|
|
|
|
.stButton>button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 15px rgba(1, 35, 38, 0.4); |
|
|
} |
|
|
|
|
|
/* Cards */ |
|
|
.custom-card { |
|
|
background: white; |
|
|
border-radius: 12px; |
|
|
padding: 24px; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.05); |
|
|
border: 1px solid #e2e8f0; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
/* Typography */ |
|
|
h1, h2, h3 { |
|
|
color: var(--dark) !important; |
|
|
} |
|
|
|
|
|
.sidebar-title { |
|
|
color: white !important; |
|
|
font-weight: 700 !important; |
|
|
} |
|
|
|
|
|
/* Custom elements */ |
|
|
.divider { |
|
|
height: 1px; |
|
|
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0) 100%); |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.metric-card { |
|
|
background: white; |
|
|
border-radius: 10px; |
|
|
padding: 16px; |
|
|
box-shadow: 0 2px 15px rgba(0,0,0,0.03); |
|
|
} |
|
|
|
|
|
/* Animations */ |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
|
|
|
.fade-in { |
|
|
animation: fadeIn 0.5s ease-out forwards; |
|
|
} |
|
|
|
|
|
.job-title-info { |
|
|
color: #64748b; |
|
|
font-size: 18px; |
|
|
margin-top: 8px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
/* Update accent colors in prediction card */ |
|
|
.custom-card h1 { |
|
|
color: #014d4f !important; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_data |
|
|
def load_data(): |
|
|
df = pd.read_csv('cleaned_job_salaries.csv') |
|
|
if 'Posting Date' in df.columns: |
|
|
df['Posting Day'] = pd.to_datetime(df['Posting Date']).dt.day |
|
|
df['Posting Month'] = pd.to_datetime(df['Posting Date']).dt.month |
|
|
return df |
|
|
|
|
|
@st.cache_resource |
|
|
def load_model(): |
|
|
model = joblib.load('best_decision_tree_model2.pkl') |
|
|
|
|
|
preprocessor = ColumnTransformer( |
|
|
transformers=[ |
|
|
('num', StandardScaler(), ['# Of Positions', 'min_experience', 'license_required','bar_admission','driver_license_required', 'Posting Day','Posting Month']), |
|
|
('cat', OneHotEncoder(), ['Agency', 'Posting Type', 'Business Title', |
|
|
'Title Classification', 'Job Category', 'Full-Time/Part-Time indicator', |
|
|
'Career Level', 'Salary Frequency', 'Level Description', |
|
|
'required_degree', 'has_communication_skills']) |
|
|
] |
|
|
) |
|
|
|
|
|
pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('regressor', model)]) |
|
|
df = load_data() |
|
|
pipeline.fit(df[['Agency', 'Posting Type', '# Of Positions', 'Business Title', |
|
|
'Title Classification', 'Job Category', 'Full-Time/Part-Time indicator', |
|
|
'Career Level', 'Salary Frequency', 'Level Description', |
|
|
'required_degree', 'min_experience', 'license_required', |
|
|
'bar_admission', 'driver_license_required', 'Posting Day', |
|
|
'Posting Month', 'has_communication_skills']], df[['Salary']]) |
|
|
return pipeline |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prediction_card(title, value, job_title, career_level, icon="šµ"): |
|
|
st.markdown(f""" |
|
|
<div class="custom-card fade-in"> |
|
|
<div style="display: flex; align-items: center; margin-bottom: 16px;"> |
|
|
<div style="background: linear-gradient(135deg, #012326 0%, #014d4f 100%); width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-right: 16px;"> |
|
|
<span style="font-size: 24px;">{icon}</span> |
|
|
</div> |
|
|
<h3 style="margin: 0;">{title}</h3> |
|
|
</div> |
|
|
<h1 style="color: #014d4f; margin: 0;">${value:,.2f}</h1> |
|
|
<p class="job-title-info">Monthly: ${value/12:,.2f}</p> |
|
|
<p class="job-title-info">{job_title} | {career_level}</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
def company_metric(label, value, change=None): |
|
|
st.markdown(f""" |
|
|
<div class="metric-card fade-in"> |
|
|
<p style="color: #64748b; font-size: 14px; margin-bottom: 8px;">{label}</p> |
|
|
<h3 style="margin: 0;">{value}</h3> |
|
|
{f'<p style="color: {"#10b981" if change >=0 else "#ef4444"}; font-size: 14px; margin: 4px 0 0;">{"+" if change >=0 else ""}{change if change is not None else ""}</p>' if change is not None else ''} |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
set_page_config() |
|
|
|
|
|
|
|
|
col1, col2 = st.columns([1, 5]) |
|
|
with col1: |
|
|
st.image("LOGO3.jpg", width=125) |
|
|
with col2: |
|
|
st.markdown(""" |
|
|
<div style="display: flex; flex-direction: column; justify-content: center; height: 100%; margin-top: 13px;"> |
|
|
<h1 style="font-size: 80px; margin: 0; padding: 0; line-height: 1;">WageWise</h1> |
|
|
<p style="color: #64748b; font-size: 16px; margin: 5px 0 0 0;"> |
|
|
AI-powered salary predictions with market intelligence |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown('<div class="divider"></div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.markdown('<h2 class="sidebar-title">Job Details</h2>', unsafe_allow_html=True) |
|
|
|
|
|
df = load_data() |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Business Title</p>', unsafe_allow_html=True) |
|
|
BusinessTitle = st.selectbox('Business Title', df['Business Title'].unique(), label_visibility="collapsed") |
|
|
|
|
|
Agency = df[df['Business Title'] == BusinessTitle]['Agency'].mode().iloc[0] |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Posting Type</p>', unsafe_allow_html=True) |
|
|
PostingType = st.selectbox('Posting Type', df['Posting Type'].unique(), label_visibility="collapsed") |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Experience (years)</p>', unsafe_allow_html=True) |
|
|
MinExperience = st.number_input('Experience (years)', 0, 50, 3, label_visibility="collapsed") |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Title Classification</p>', unsafe_allow_html=True) |
|
|
TitleClassification = st.selectbox('Title Classification', df['Title Classification'].unique(), label_visibility="collapsed") |
|
|
|
|
|
JobCategory = df[df['Business Title'] == BusinessTitle]['Job Category'].mode().iloc[0] |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Employment Type</p>', unsafe_allow_html=True) |
|
|
FullOrPartTime = st.selectbox('Employment Type', df['Full-Time/Part-Time indicator'].unique(), label_visibility="collapsed") |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Career Level</p>', unsafe_allow_html=True) |
|
|
CareerLevel = st.selectbox('Career Level', df['Career Level'].unique(), label_visibility="collapsed") |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Required Degree</p>', unsafe_allow_html=True) |
|
|
RequiredDegree = st.selectbox('Required Degree', df['required_degree'].unique(), label_visibility="collapsed") |
|
|
|
|
|
st.markdown('<div class="divider"></div>', unsafe_allow_html=True) |
|
|
st.markdown('<h3 class="sidebar-title">Additional Requirements</h3>', unsafe_allow_html=True) |
|
|
|
|
|
options = {'Yes': 1, 'No': 0} |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">License Required</p>', unsafe_allow_html=True) |
|
|
LicenseRequired_option = st.selectbox('License Required', list(options.keys()), label_visibility="collapsed") |
|
|
LicenseRequired = options[LicenseRequired_option] |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Bar Admission</p>', unsafe_allow_html=True) |
|
|
BarAdmission_option = st.selectbox('Bar Admission', list(options.keys()), label_visibility="collapsed") |
|
|
BarAdmission = options[BarAdmission_option] |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Driver License</p>', unsafe_allow_html=True) |
|
|
DriverLicenseRequired_option = st.selectbox('Driver License', list(options.keys()), label_visibility="collapsed") |
|
|
DriverLicenseRequired = options[DriverLicenseRequired_option] |
|
|
|
|
|
communication_options = {'Yes': True, 'No': False} |
|
|
|
|
|
|
|
|
st.markdown('<p style="color: white; font-weight: 500; margin-bottom: 0.5rem;">Communication Skills</p>', unsafe_allow_html=True) |
|
|
hasCommunicationSkills = communication_options[st.selectbox('Communication Skills', list(communication_options.keys()), label_visibility="collapsed")] |
|
|
|
|
|
career_to_level_description = { |
|
|
'Intern / Student Role': ['Intern/Trainee'], |
|
|
'Entry-Level Professional': ['Junior-Level', 'Entry Specialist A', 'Entry Specialist B'], |
|
|
'Experienced Professional': ['Mid-Level', 'Specialist Level A', 'Specialist Level B', 'Advanced Tech Level'], |
|
|
'Mid-Level Manager': ['Manager Level 1', 'Manager Level 2', 'Manager Level 3', 'Manager Level 4', 'Manager Level 5'], |
|
|
'Executive / Senior Leadership': ['Executive Manager 1', 'Executive Manager 2', 'Executive Manager 3', 'Lead-Level', 'Mayoral Appointee'] |
|
|
} |
|
|
LevelDescription = career_to_level_description.get(CareerLevel, [None])[0] |
|
|
|
|
|
df_forecast = pd.read_excel('forecast.xlsx') |
|
|
if BusinessTitle in df_forecast["Business Titles"].values: |
|
|
current_date = datetime.now() |
|
|
date = f'{current_date.month:02d}.2025' |
|
|
NumberOfPositions = df_forecast.loc[df_forecast["Business Titles"] == BusinessTitle, date].values[0] |
|
|
else: |
|
|
df = pd.read_csv('cleaned_job_salaries.csv') |
|
|
NumberOfPositions = df.loc[df["Business Title"] == BusinessTitle]['# Of Positions'].mean() |
|
|
|
|
|
if st.button('Predict Salary', use_container_width=True): |
|
|
st.session_state.predict_clicked = True |
|
|
|
|
|
|
|
|
if 'predict_clicked' in st.session_state and st.session_state.predict_clicked: |
|
|
pipeline = load_model() |
|
|
|
|
|
current_date = datetime.today().date() |
|
|
|
|
|
input_data = pd.DataFrame({ |
|
|
'Agency': [Agency], |
|
|
'Posting Type': [PostingType], |
|
|
'# Of Positions': [NumberOfPositions], |
|
|
'Business Title': [BusinessTitle], |
|
|
'Title Classification': [TitleClassification], |
|
|
'Job Category': [JobCategory], |
|
|
'Full-Time/Part-Time indicator': [FullOrPartTime], |
|
|
'Career Level': [CareerLevel], |
|
|
'Salary Frequency': ["Annual"], |
|
|
'Level Description': [LevelDescription], |
|
|
'required_degree': [RequiredDegree], |
|
|
'min_experience': [MinExperience], |
|
|
'license_required': [LicenseRequired], |
|
|
'bar_admission': [BarAdmission], |
|
|
'driver_license_required': [DriverLicenseRequired], |
|
|
'Posting Day': [current_date.day], |
|
|
'Posting Month': [current_date.month], |
|
|
'has_communication_skills': [hasCommunicationSkills], |
|
|
}) |
|
|
|
|
|
with st.spinner('Analyzing job details with our AI model...'): |
|
|
time.sleep(1.5) |
|
|
predicted_salary = pipeline.predict(input_data)[0] |
|
|
|
|
|
|
|
|
st.markdown("## Prediction Results") |
|
|
|
|
|
|
|
|
show_positions = (BusinessTitle in df_forecast["Business Titles"].values) and (NumberOfPositions != 0) |
|
|
|
|
|
if show_positions: |
|
|
prediction_card( |
|
|
"Annual Salary", |
|
|
predicted_salary, |
|
|
f"{BusinessTitle} | {NumberOfPositions:.0f} positions available", |
|
|
CareerLevel, |
|
|
"šµ" |
|
|
) |
|
|
else: |
|
|
prediction_card( |
|
|
"Annual Salary", |
|
|
predicted_salary, |
|
|
BusinessTitle, |
|
|
CareerLevel, |
|
|
"šµ" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("### Job Summary") |
|
|
col1, col2, col3 = st.columns(3) |
|
|
with col1: |
|
|
company_metric("Position Type", FullOrPartTime) |
|
|
with col2: |
|
|
company_metric("Experience Required", f"{MinExperience} years") |
|
|
with col3: |
|
|
company_metric("Education Level", RequiredDegree) |
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.footer { |
|
|
position: fixed; |
|
|
bottom: 0; |
|
|
width: 100%; |
|
|
text-align: center; |
|
|
padding: 20px 0; |
|
|
margin-top: 50px; |
|
|
background-color: #f9fafc; |
|
|
color: #64748b; |
|
|
} |
|
|
</style> |
|
|
<div class="footer"> |
|
|
<hr style="border: 0.5px solid #e2e8f0;"> |
|
|
Created with ā¤ļø by Senasu & Sude |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
main() |