Building a Salary Predictor with Scikit-Learn and Node.js
I wanted a project that demonstrates end-to-end ML: data preprocessing, model training, evaluation, and deployment. All running live on a 2GB VPS with zero Python dependencies at runtime.
The Problem
Salary data is noisy. The same "Data Scientist" title can pay anywhere from $60K to $250K depending on experience, location, company, and negotiation. Can we build a model that captures the broad trends?
Dataset
I used a Kaggle salary dataset with 6,700 records containing age, gender, education level, job title, and years of experience.
Preprocessing
- Filtered to tech/data roles using keyword matching (data, ML, engineer, developer, etc.)
- Stripped seniority prefixes (Senior, Junior, Lead) from titles since seniority is better captured by years of experience
- Standardized education levels (merged "Bachelor's Degree" into "Bachelor's", "phD" into "PhD")
- Kept titles with 20+ entries to avoid sparse categories
This reduced 52 raw titles down to 16 clean ones.
Model Comparison
I trained three models and compared them:
| Model | R² | MAE |
|---|---|---|
| Linear Regression | 0.65 | $23,449 |
| Random Forest | 0.96 | $4,375 |
| Gradient Boosting | 0.96 | $4,576 |
Gradient Boosting won by a small margin. The ensemble models massively outperform linear regression because salary relationships aren't linear. A PhD with 10 years earns disproportionately more than a linear model would predict.
Deployment Strategy
The challenge: deploy an ML model on a 2GB VPS running Node.js with no Python, no pickle files, and no heavy dependencies.
Solution: Pre-computed predictions exported as JSON.
During training, I iterate over every valid combination of (job title x education x years of experience) and store the prediction. The resulting JSON is ~80KB, essentially a lookup table.
At runtime, Node.js loads the JSON once, and predictions are a simple key lookup. Zero computation, instant response.
function predict(jobTitle, education, yearsExp) {
const key = `${jobTitle}|${education}|${yearsExp}`;
return model.predictions[key];
}
Limitations
This model doesn't account for:
- Geographic location
- Company size or industry
- Equity, bonus, or total compensation
- Negotiation skills
It captures broad trends, useful for ballpark estimates but not offer negotiations.