CoolFace
Apppublic

dima806/developer_salary_prediction

sourceHugging Faceapache-2.0updated 7mo agoView on Hugging Face
2likes
App README

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 uv package manager

Quick Start

1. Install Dependencies

bash
uv sync

2. Download Data

Download the Stack Overflow Developer Survey CSV file:

  1. 1.Visit: https://insights.stackoverflow.com/survey
  2. 2.Download the latest survey results (2025)
  3. 3.Extract the survey_results_public.csv file
  4. 4.Place it in the data/ directory:
text
   data/survey_results_public.csv

Required columns: Country, YearsCode, WorkExp, EdLevel, DevType, Industry, Age, ICorPM, OrgSize, Employment, ConvertedCompYearly

3. Train the Model

bash
uv run python -m src.train

This 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

bash
uv run streamlit run app.py

The app will open in your browser at http://localhost:8501

Development Cycle

The full development workflow from data to deployment:

text
data/ โ”€โ”€โ–บ (optional) tune โ”€โ”€โ–บ train โ”€โ”€โ–บ test โ”€โ”€โ–บ commit โ”€โ”€โ–บ CI passes โ”€โ”€โ–บ deploy

Step-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.

bash
make tune
# or with a custom number of trials:
uv run python -m src.tune --n-trials 50
2. Train the model
bash
uv run python -m src.train
3. Check code quality (lint + test + complexity + security)
bash
make check

This runs all quality gates in sequence:

TargetToolWhat it checks
make ciruff + pytestMirrors GitHub Actions CI (lint + test)
make pre-commitprekAll hooks from .pre-commit-config.yaml against every file
make lintruffStyle and linting errors
make formatruffAuto-formats code
make testpytestUnit and integration tests
make coveragepytest-covTest coverage report
make complexityradon CCCyclomatic complexity
make maintainabilityradon MIMaintainability index
make auditpip-auditDependency vulnerability scan
make securitybanditStatic security analysis

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
bash
make pre-commit

Usage

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

python
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:

bash
uv run python example_inference.py

Input 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_code must be >= 0
  • โ€”work_exp must 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) โ€” Other dropped
  • โ€”Valid Industries (~15) โ€” Other dropped
  • โ€”Valid Age Ranges (~7) โ€” Other dropped
  • โ€”Valid IC/PM Values (~3) โ€” Other dropped
  • โ€”Valid Organization Sizes (~8) โ€” Other dropped
  • โ€”Valid Employment Statuses (~5)

Passing an invalid value raises a ValueError with a message pointing to config/valid_categories.yaml.

Example:

python
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:

bash
cat config/valid_categories.yaml

Model guardrails (config/model_parameters.yaml)

The guardrails section defines thresholds used by make guardrails and the pre-tune check in make tune:

yaml
guardrails:
  max_abs_pct_diff: 100        # max acceptable absolute % difference per category

Testing

Tests live in tests/ and cover all major modules:

FileWhat it tests
test_schema.pyPydantic validation โ€” required fields, ge=0 constraints
test_infer.pyInference pipeline โ€” valid predictions, ValueError on invalid categories, currency lookup
test_train.pyTraining helpers โ€” salary filtering, cardinality reduction, valid category extraction, currency rate computation
test_preprocessing.pyFeature engineering โ€” one-hot encoding, numeric transforms
test_tune.pyOptuna helpers โ€” parameter sampling, objective function construction, best-param saving
test_feature_impact.pyModel sanity โ€” changing each input feature (country, education, dev type, etc.) produces a distinct prediction

Run all tests:

bash
make test

Run with coverage:

bash
make coverage

Configuration

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:

yaml
# 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: 50

config/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

text
.
โ”œโ”€โ”€ .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):

bash
uv tool install prek && prek install

Hooks will then run automatically on every git commit. To run them manually against all files:

bash
make pre-commit
GitHub Actions CI

A CI workflow (.github/workflows/ci.yml) runs automatically on every push to any branch. It:

  1. 1.Sets up Python 3.12 and installs uv
  2. 2.Installs all dependencies (uv sync --all-extras)
  3. 3.Runs make lint โ€” ruff linting
  4. 4.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:

bash
# 1. Place new CSV in data/
# 2. (Optional) tune first
make tune
# 3. Retrain
uv run python -m src.train

Running Tests

bash
# Run all tests
make test

# Run with coverage report
make coverage

# Run a specific test file
uv run pytest tests/test_infer.py -v

Versioning

This project follows Semantic Versioning (MAJOR.MINOR.PATCH):

Version bumpWhen to useExamples
MAJORBreaking changes to the public interfaceNew required input field, incompatible model artifact format, renamed API
MINORBackward-compatible new featuresNew optional input field, new supported country, new Makefile target, UI addition
PATCHBackward-compatible fixes and improvementsBug fixes, model retrain with same schema, config tuning, dependency updates

Pre-release suffixes (for work in progress):

text
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 validation

Tags are applied on main after a successful CI run:

bash
git tag v2.0.0
git push origin v2.0.0

Branching Strategy

The project uses a GitFlow-inspired branching model:

text
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.0

Branches

BranchPurposeMerges into
mainProduction-ready code, always deployable. Tagged on every release.โ€”
developIntegration branch for completed features. Base for new work.main via release branch
feature/<name>New features or improvements (e.g. feature/add-local-currency)develop
fix/<name>Non-urgent bug fixes (e.g. fix/guardrail-error-message)develop
release/v<semver>Release preparation โ€” version bump, changelog, final QAmain and back to develop
hotfix/v<semver>Urgent production fixes (e.g. hotfix/v2.0.1)main and back to develop

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 main is tagged with a semver version
  • โ€”Hotfixes branch off main directly and merge back to both main and develop

Typical workflow

bash
# 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.0

Deployment

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 Dockerfile in the repo root)
  • โ€”Port: 8501 (Streamlit default)
  • โ€”License: Apache 2.0

To deploy your own copy:

  1. 1.Create a new Space on Hugging Face and select "Docker" as the SDK
  2. 2.Push this repository to your Space:
bash
   git remote add space https://huggingface.co/spaces/<your-username>/<your-space-name>
   git push space main

Note: 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:

bash
docker build -t developer-salary-predictor .
docker run -p 8501:8501 developer-salary-predictor

Then visit http://localhost:8501

Local (without Docker)

Using uv (recommended for development):

bash
uv run streamlit run app.py

Using pip:

bash
pip install -r requirements.txt
streamlit run app.py

Troubleshooting

"Model file not found"

  • โ€”Run uv run python -m src.train first to generate the model

"Valid categories file not found"

  • โ€”Run uv run python -m src.train โ€” training generates both models/model.pkl and config/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.yaml file should exist in the project root
  • โ€”Check that you're running commands from the project root directory

Dependencies issues

  • โ€”Run uv sync to 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