dima806/developer_salary_prediction
Developer Salary Prediction
A minimal, local-first ML application that predicts developer salaries using Stack Overflow Developer Survey data. Built with Python, XGBoost, Pydantic, and Streamlit.
Features
- ๐ฏ XGBoost (gradient boosting) model for salary prediction
- โ Input validation with Pydantic (schema) and runtime guardrails (valid categories)
- ๐ Interactive web UI with Streamlit
- ๐ Trained on Stack Overflow 2025 Developer Survey data
- ๐ง Easy setup with
uvpackage manager
Quick Start
1. Install Dependencies
uv sync2. Download Data
Download the Stack Overflow Developer Survey CSV file:
- Visit: https://insights.stackoverflow.com/survey
- Download the latest survey results (2025)
- Extract the
survey_results_public.csvfile - Place it in the
data/directory:
data/survey_results_public.csvRequired columns: Country, YearsCode, WorkExp, EdLevel, DevType, Industry, Age, ICorPM, OrgSize, Employment, ConvertedCompYearly
3. Train the Model
uv run python -m src.trainThis will:
- Load configuration from
config/model_parameters.yaml - Filter salaries and reduce cardinality of categorical features
- Run 5-fold cross-validation and report mean MAPE per fold
- Train a final XGBoost model on the full dataset with early stopping
- Save the model artifact to
models/model.pkl - Generate
config/valid_categories.yamlโ valid input values for runtime guardrails - Generate
config/currency_rates.yamlโ per-country median currency conversion rates
4. Run the Streamlit App
uv run streamlit run app.pyThe app will open in your browser at http://localhost:8501
Development Cycle
The full development workflow from data to deployment:
data/ โโโบ (optional) tune โโโบ train โโโบ test โโโบ commit โโโบ CI passes โโโบ deployStep-by-step
1. (Optional) Tune hyperparameters
Run Optuna to search for optimal XGBoost hyperparameters. The search space is defined in config/optuna_config.yaml. Best parameters are written directly back into config/model_parameters.yaml.
make tune
# or with a custom number of trials:
uv run python -m src.tune --n-trials 502. Train the model
uv run python -m src.train3. Check code quality (lint + test + complexity + security)
make checkThis runs all quality gates in sequence:
make check runs lint, test, complexity, maintainability, audit, and security together. make all is an alias for make check.
4. Run all pre-commit checks manually
make pre-commitUsage
Web Interface
Launch the Streamlit app and enter:
- Country: Developer's country
- Years of Coding (Total): Total years coding including education
- Years of Professional Work Experience: Years of professional work experience
- Education Level: Highest degree completed
- Developer Type: Primary developer role
- Industry: Industry the developer works in
- Age: Developer's age range
- IC or PM: Individual contributor or people manager
- Organization Size: Approximate number of employees at the developer's company
- Employment Status: Current employment status
Click "Predict Salary" to see the estimated annual salary in USD plus a local currency equivalent where available.
Programmatic Usage
from src.schema import SalaryInput
from src.infer import predict_salary
input_data = SalaryInput(
country="United States of America",
years_code=5.0,
work_exp=3.0,
education_level="Bachelor's degree (B.A., B.S., B.Eng., etc.)",
dev_type="Developer, full-stack",
industry="Software Development",
age="25-34 years old",
ic_or_pm="Individual contributor",
org_size="20 to 99 employees",
employment="Employed",
)
salary = predict_salary(input_data)
print(f"Estimated salary: ${salary:,.0f}")Run the example script:
uv run python example_inference.pyInput Validation and Guardrails
Validation is enforced at two layers:
Layer 1 โ Pydantic schema (src/schema.py)
Checked at object construction time:
- All 10 fields are required
years_codemust be>= 0work_expmust be>= 0
Layer 2 โ Runtime category guardrails (src/infer.py)
Checked at inference time against config/valid_categories.yaml, which is generated during training to reflect only categories that appeared frequently enough in the training data (controlled by features.cardinality.min_frequency in config/model_parameters.yaml):
- Valid Countries (~21) โ low-frequency countries collapsed to
Other, which is then dropped - Valid Education Levels (~9)
- Valid Developer Types (~20) โ
Otherdropped - Valid Industries (~15) โ
Otherdropped - Valid Age Ranges (~7) โ
Otherdropped - Valid IC/PM Values (~3) โ
Otherdropped - Valid Organization Sizes (~8) โ
Otherdropped - Valid Employment Statuses (~5)
Passing an invalid value raises a ValueError with a message pointing to config/valid_categories.yaml.
Example:
from src.infer import predict_salary
from src.schema import SalaryInput
# Raises ValueError: "Invalid country: 'Japan'. Check config/valid_categories.yaml"
predict_salary(SalaryInput(country="Japan", ...))View valid categories:
cat config/valid_categories.yamlModel guardrails (config/model_parameters.yaml)
The guardrails section defines thresholds used by make guardrails and the pre-tune check in make tune:
guardrails:
max_abs_pct_diff: 100 # max acceptable absolute % difference per categoryTesting
Tests live in tests/ and cover all major modules:
Run all tests:
make testRun with coverage:
make coverageConfiguration
All runtime parameters are centralised in two YAML files:
config/model_parameters.yaml
Controls data processing, feature engineering, model hyperparameters, training settings, and guardrail thresholds. You can customise:
- Data Processing: Salary thresholds, percentile bounds, train/test split ratio
- Feature Engineering: Cardinality reduction settings (max categories, min frequency)
- Model Hyperparameters: Learning rate, tree depth, early stopping, etc.
- Training Settings: Verbosity, model save path
- Guardrails: MAPE thresholds for model evaluation
Example parameter changes:
# Increase model complexity
model:
max_depth: 8 # Default: 3
n_estimators: 10000 # Default: 5000
# Keep more categories
features:
cardinality:
max_categories: 30 # Default: 30
min_frequency: 50 # Default: 50config/optuna_config.yaml
Controls the Optuna hyperparameter search โ search space (type, bounds, log scale), number of trials, CV folds, and fixed parameters that are not tuned (e.g. n_estimators, random_state).
Project Structure
.
โโโ .github/
โ โโโ workflows/
โ โโโ ci.yml # GitHub Actions CI (lint + test)
โโโ config/
โ โโโ model_parameters.yaml # Model configuration and guardrails
โ โโโ optuna_config.yaml # Optuna hyperparameter search space
โ โโโ valid_categories.yaml # Valid input categories (generated by training)
โ โโโ currency_rates.yaml # Per-country currency rates (generated by training)
โโโ data/
โ โโโ survey_results_public.csv # Stack Overflow survey data (download required)
โโโ models/
โ โโโ model.pkl # Trained model artifact (generated by training)
โโโ src/
โ โโโ __init__.py
โ โโโ schema.py # Pydantic input model
โ โโโ preprocessing.py # Feature engineering (one-hot encoding, scaling)
โ โโโ train.py # Training pipeline
โ โโโ tune.py # Optuna hyperparameter optimisation
โ โโโ infer.py # Inference with runtime guardrails
โโโ tests/
โ โโโ conftest.py # Shared pytest fixtures
โ โโโ test_schema.py
โ โโโ test_infer.py
โ โโโ test_train.py
โ โโโ test_preprocessing.py
โ โโโ test_tune.py
โ โโโ test_feature_impact.py
โโโ app.py # Streamlit web app
โโโ example_inference.py # Inference usage examples
โโโ Makefile # Developer workflow commands
โโโ .pre-commit-config.yaml # Pre-commit hooks
โโโ pyproject.toml # Project dependencies
โโโ README.md # This file (also Hugging Face Space config)Tech Stack
- Python 3.12+
- uv โ Package manager
- pandas โ Data manipulation
- xgboost โ Gradient boosting model
- scikit-learn โ Cross-validation and train/test split
- optuna โ Hyperparameter optimisation
- pydantic โ Input schema validation
- streamlit โ Web UI
- ruff โ Linting and formatting
- radon โ Complexity and maintainability metrics
- bandit โ Static security analysis
- pip-audit โ Dependency vulnerability scanning
Development
For detailed development information, see Claude.md.
Code Quality
Pre-commit hooks
The project uses prek to enforce code quality checks before each commit. Hooks are defined in .pre-commit-config.yaml and run:
- ruff format โ auto-formats Python files (
make format) - ruff lint โ checks for linting errors (
make lint) - Standard checks โ trailing whitespace, end-of-file newline, LF line endings, valid YAML/TOML/JSON, large files, merge conflict markers, stray debug statements
Install hooks (once, after cloning):
uv tool install prek && prek installHooks will then run automatically on every git commit. To run them manually against all files:
make pre-commitGitHub Actions CI
A CI workflow (.github/workflows/ci.yml) runs automatically on every push to any branch. It:
- Sets up Python 3.12 and installs
uv - Installs all dependencies (
uv sync --all-extras) - Runs
make lintโ ruff linting - Runs
make testโ full pytest suite
The workflow must pass before merging changes.
Re-training the Model
If you want to use a different survey year or update the model:
# 1. Place new CSV in data/
# 2. (Optional) tune first
make tune
# 3. Retrain
uv run python -m src.trainRunning Tests
# Run all tests
make test
# Run with coverage report
make coverage
# Run a specific test file
uv run pytest tests/test_infer.py -vVersioning
This project follows Semantic Versioning (MAJOR.MINOR.PATCH):
Pre-release suffixes (for work in progress):
v1.0.0-alpha.1 # early development, unstable
v1.0.0-beta.1 # feature-complete, under testing
v1.0.0-rc.1 # release candidate, final validationTags are applied on main after a successful CI run:
git tag v2.0.0
git push origin v2.0.0Branching Strategy
The project uses a GitFlow-inspired branching model:
main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ hotfix/v2.0.1
โฒ โ
โ merge + tag โ
โ (urgent fix)
develop โโโโโ feature/add-currency-display
โโโโโ feature/new-dev-types
โโโโโ fix/invalid-category-message
โ
โโโโบ release/v2.1.0 โโโบ (final testing) โโโบ main + tag v2.1.0Branches
Rules
- `main` is protected โ no direct pushes; merge only via PR after CI passes
- `develop` is the default branch for day-to-day work
- Branch names use lowercase kebab-case:
feature/optuna-cv-splits - Every merge to
mainis tagged with a semver version - Hotfixes branch off
maindirectly and merge back to bothmainanddevelop
Typical workflow
# Start a new feature
git checkout develop
git pull origin develop
git checkout -b feature/add-local-currency
# ... work, commit, push ...
git push -u origin feature/add-local-currency
# Open a PR into develop, CI must pass before merging
# Prepare a release
git checkout -b release/v2.1.0 develop
# bump version in pyproject.toml, update changelog
git push -u origin release/v2.1.0
# Open PR into main, merge, tag
git tag v2.1.0
git push origin v2.1.0Deployment
Hugging Face Spaces
The app is deployed on Hugging Face Spaces using the Docker SDK. The Space configuration is embedded in the frontmatter at the top of this README, which Hugging Face reads automatically:
- SDK: Docker (runs the
Dockerfilein the repo root) - Port: 8501 (Streamlit default)
- License: Apache 2.0
To deploy your own copy:
- Create a new Space on Hugging Face and select "Docker" as the SDK
- Push this repository to your Space:
git remote add space https://huggingface.co/spaces/<your-username>/<your-space-name>
git push space mainNote: The pre-trained model (models/model.pkl) and configuration (config/valid_categories.yaml, config/currency_rates.yaml) must be present before building the Docker image. Train locally first if needed.
Local Docker
Build and run:
docker build -t developer-salary-predictor .
docker run -p 8501:8501 developer-salary-predictorThen visit http://localhost:8501
Local (without Docker)
Using uv (recommended for development):
uv run streamlit run app.pyUsing pip:
pip install -r requirements.txt
streamlit run app.pyTroubleshooting
"Model file not found"
- Run
uv run python -m src.trainfirst to generate the model
"Valid categories file not found"
- Run
uv run python -m src.trainโ training generates bothmodels/model.pklandconfig/valid_categories.yaml
"Data file not found"
- Download the Stack Overflow survey CSV and place it in
data/
"Configuration file not found"
- The
config/model_parameters.yamlfile should exist in the project root - Check that you're running commands from the project root directory
Dependencies issues
- Run
uv syncto ensure all packages are installed
Design Principles
- Simplicity: Minimal codebase, easy to read and modify
- Separation of concerns: Schema validation, preprocessing, training, and inference are distinct modules
- Config-driven: All tunable parameters in YAML โ no magic numbers in code
- Local-first: No cloud dependencies for training or inference
- Testable: Every public function has unit tests; model sanity covered by feature-impact tests
License
Apache 2.0 License - see LICENSE file
Acknowledgments
Data from Stack Overflow Developer Survey
