Data & AI Compensation Benchmark
A compensation benchmark for data and AI professionals built on 85,088 real records (2020–2025), with a dual business focus: an HR team assessing salary competitiveness, and a data/AI professional deciding what to specialize in. This isn't a descriptive EDA — every business claim is tied to a test that it isn't sampling noise: non-parametric hypothesis tests, bootstrap confidence intervals, and an explicit minimum sample-size floor before comparing countries. An ETL pipeline with a declarative data contract (Pandera), purchasing-power adjustment using World Bank data, business SQL queries over SQLite, and an interactive Power BI dashboard — reproducible with a single command (uv sync), no external database servers, with lint/type-checking (Ruff, MyPy), automated tests and CI on GitHub Actions.
Comparing salaries across roles, experience and countries is easy to get wrong: averages over tiny samples, comparisons in nominal USD that ignore cost of living, and conclusions that mistake a visual difference for a real one. The project starts from an aggregated data/AI compensation survey (85,088 rows, 2020–2025, no respondent ID) with a dual objective:
- Identify which factors — role, experience, country, remote arrangement, company size — explain compensation, quantifying how defensible those differences are given each sample's actual size.
- Serve two different audiences with the same dataset: an HR team assessing salary competitiveness, and a data/AI professional deciding what to specialize in.
The project started in April 2026 as a descriptive analysis of 607 rows (2020–2022); the data refresh and reframing into a statistically rigorous business benchmark is what turned it into the current project.
The pipeline is organized into layers with a single-direction dependency (Extraction → Transformation → PPP Enrichment → Validation → Loading → SQL/Notebook/Dashboard), each decoupled from the next — the Power BI dashboard doesn't depend on the notebook having run, and both read from the same SQLite database generated by the ETL. Each module has a single responsibility:
etl.py— the main pipeline.reference_data.py— reference data.schema.py— the data contract.
Every non-obvious tooling or architecture decision was documented as an ADR — 11 decisions logged to date, from dependency management to renaming the project.
The original dataset has 151,445 rows of tech jobs in general; filtering to data/AI/ML/BI roles by title keyword leaves 85,088 — almost 44% of the dataset didn't belong to the project's domain and was explicitly excluded rather than analyzed mixed in. Before loading, every run passes through a declarative Pandera schema contract (types, ranges, valid categorical values): if a row doesn't meet the contract, the ETL fails with an explicit error instead of loading suspicious data.
A non-trivial data-quality decision: drop_duplicates() is not applied, because this is an aggregated survey with no respondent ID — identical rows represent different people who reported the same role/salary/country, not capture errors.
Four variables are derived from the raw columns:
salary_category— a range-based heuristic.job_category— 9 role families, by title keyword.continent— a mapping of 90 country codes.salary_usd_ppp— purchasing-power adjustment.
The ETL explicitly logs what it discards or leaves incomplete — for example, which country codes have no price-index data available — instead of failing silently or forcing a default value.
The salary distribution is right-skewed (skew ≈ 1.47), which rules out ANOVA or a t-test as valid tests: their non-parametric equivalents are used instead — Kruskal-Wallis to compare all 4 experience levels at once (p ≈ 0) and Mann-Whitney U for the pointed Junior vs. Senior contrast (median $85,000 → $156,400, p ≈ 0) — the difference visible on a boxplot is not sampling noise.
Comparing countries is where rigor matters most: even with a minimum sample-size floor (HAVING COUNT(*) >= 5 in SQL), a 1,000-resample bootstrap per country shows that Israel (n=12) or Ukraine (n=13) have confidence intervals over $60,000 wide, while the United States (n=75,409) has one under $2,000 — a ranking that only looks at the average would treat both as equally reliable.
Comparing salaries in nominal USD alone ignores how much cost of living varies across countries. Enriching the dataset with salary_usd_ppp (World Bank GDP price-level index) changes the ranking: a nominal salary of $48,553 in India equals, in real purchasing power, $211,065 — above Switzerland's adjusted $112,950 (from a nominal $126,453). That's why the project's business conclusions use salary_usd_ppp, not nominal salary, to compare countries.
sql/queries.sql translates the same findings into business queries over SQLite, with the same sample-size floor as the notebook:
- Average salary by company size — large/medium companies pay $149,000–$157,000, small ones $86,000.
- Average salary by remote arrangement — fully remote doesn't pay less than on-site: $145,250 vs. $151,010.
- PPP-adjusted country ranking.
Hypothesis testing and the bootstrap were deliberately left out of SQL: they have no natural expression there, and forcing them in would have been complexity without a purpose — those live in the notebook.
The dashboard exposes global average salary, comparison by role and experience, country analysis, salary distribution and the impact of employment type, with dynamic filters, consuming the SQLite database the ETL generates directly. It's pending an update to reflect the August 2026 data refresh (85,088 rows, 2020–2025, not the original 607 rows from 2020–2022) — an explicit checklist documents the pending work instead of leaving it implicit:
- Minimum sample-size filter per country.
- Outlier footnote.
- Bar-chart categories.
31 automated tests cover the hand-written business logic, deliberately without infrastructure mocks or coverage targets:
- Salary categorization.
- Data/AI role filtering.
- Role classification.
- PPP adjustment.
- That the Pandera data contract rejects invalid rows.
Ruff (lint + format), MyPy (strict type-checking over src/) and pre-commit run before every commit; a GitHub Actions workflow repeats lint, format check, mypy, pytest and the full ETL (with its Pandera validation) on every push/PR to main. Dependency management uses uv with a deterministic lockfile — no external database servers: SQLite replaced a local SQL Server so the ETL could run end-to-end in CI.
The project documented three non-trivial technical decisions, not just the final result:
- A corrupted
requirements.txtwith no lockfile (the original April 2026 version) was completely replaced withuv+pyproject.toml, instead of trying to repair the file. - The temptation to blindly apply
drop_duplicates()— the almost-automatic step in any data cleaning — was evaluated and explicitly rejected, because it would have artificially shrunk the sample size that later supports the hypothesis tests. - When the repository and project folder were renamed,
uvleft stale absolute paths baked into the virtual environment and the lockfile — resolved by recreating the environment withuv syncinstead of hand-editing those derived artifacts.
The result isn't just a salary ranking by country or role — it's an analysis where every business claim is tied to a test that it isn't sampling noise:
- Experience level is the most determinant and statistically significant factor.
- ML/AI Engineer is the highest-paid specialization.
- The leadership track pays less than staying a senior specialist.
- A lower nominal salary in a cheaper country can, adjusted for purchasing power, beat a higher nominal salary in an expensive one.
The project shows that the hardest part of a data benchmark isn't computing an average — it's deciding when that average is trustworthy, documenting it, and building the pipeline so those decisions still hold when the dataset gets refreshed again.