Skip to content
Table of contents

Salary

Salary is an AlgoÉTS data project about pay in Canada. It turns recruitment-firm salary guides (PDF) and federal wage tables (CSV) into data frames, then compares jobs across provinces, regions and experience levels. It is useful to anyone who wants to see how a job such as cloud architect is paid from one region to another, and to readers interested in extracting tables from PDF reports.

Data analysisAPIDocker
PythonpandasPolarsPyPDF2JupyterFastAPIBeautifulSouppre-commit
  • Updated

1. Overview

The PDF path is handled by one script per guide edition. src/pdf_salary_scraper_2023_1.py and src/pdf_salary_scraper_2024_1.py read a guide in data/pdf/ with PyPDF2, track the current province and region from fixed name lists, split each line into a job title and three salary ranges (entry, mid and senior) and save the result as a pickle in data/pickle/. src/pdf.py is an earlier PyMuPDF experiment that tags text by font size to find headings.

src/wages_in_time.py loads the Job Bank wage files data/csv/wage_2012.csv to wage_2022.csv with Polars, harmonizes column names across years, caches the combined table in data/pickle/wages.pkl and prints the best-paid jobs per year and the best-paid engineering jobs per province. src/stackoverflow_scraper.py scrapes the salary table of the 2022 Stack Overflow survey.

Analysis happens in sandbox/salary_exploration_2023.ipynb and sandbox/salary_exploration_2024.ipynb, with helper versions of the same functions in src/analyse.py: split the ranges, compute means per level, rank jobs, filter engineering and Québec jobs, compare one job across regions and provinces, and find the jobs with the widest regional spread. api/app.py exposes the 2023 and 2024 pickles as JSON through FastAPI, and web/ holds two static HTML pages.

2. Use cases

Python scripts and notebooks that extract Canadian salary guides and wage tables to compare jobs by region.

Use-case diagram for Salary: Extract salary guide PDFs; Load 2012–2022 wage tables; Compare jobs by region; Scrape Stack Overflow pay; Query salary data over HTTPSalaryExtract salary guide PDFsLoad 2012–2022 wage tablesCompare jobs by regionScrape Stack Overflow payQuery salary data over HTTPSalary analystAPI client«system»Stack Overflow survey
People on the left, external systems on the right; each oval is a use case backed by the files cited below.
Extract salary guide PDFs
Salary analyst

src/pdf_salary_scraper_2023_1.py and src/pdf_salary_scraper_2024_1.py read a guide in data/pdf with PyPDF2, track province and region, parse entry, mid and senior ranges per job and save a pickle in data/pickle.

Load 2012–2022 wage tables
Salary analyst

src/wages_in_time.py reads data/csv/wage_2012.csv to wage_2022.csv with Polars, harmonizes column names, caches data/pickle/wages.pkl and prints the top-paid jobs per year and engineering jobs per province.

Compare jobs by region
Salary analyst

sandbox/salary_exploration_2023.ipynb and salary_exploration_2024.ipynb rank jobs by maximum salary, filter engineering and Québec jobs, compare one job across regions and provinces, and find the widest gaps.

Scrape Stack Overflow pay
Salary analystStack Overflow survey

src/stackoverflow_scraper.py fetches the 2022 Stack Overflow survey salary page with httpx, parses the table with BeautifulSoup and pickles it to data/pickle/stackoverflow_salary_data.pkl.

Query salary data over HTTP
API client

api/app.py is a FastAPI app whose /load_pickle/2023 and /salary/2024 routes return the pickled guides as JSON; interactive documentation is at /docs.

2.1. Scenario: Compare cloud architect pay across Québec regions

  1. Run pip install -r requirements.txt from the repository root.
  2. Place 2023_salary_guide.pdf in data/pdf and run python src/pdf_salary_scraper_2023_1.py.
  3. Check that data/pickle/salary_guide_2023.pkl was written.
  4. Start jupyter notebook and open sandbox/salary_exploration_2023.ipynb, which splits each range and computes entry, mid and senior means.
  5. Call compare_job_province('cloud architect', 'québec') to rank Québec regions by mean mid-level salary.

Files readREADME.mdrequirements.txtsetup.pyvercel.jsonapi/app.pyapi/Dockerfileapi/docker-compose.ymlapi/setup.mdsrc/pdf.pysrc/pdf_salary_scraper_2023_1.pysrc/pdf_salary_scraper_2024_1.pysrc/stackoverflow_scraper.pysrc/wages_in_time.pysrc/analyse.pysrc/test.pydata/csv/README.mddata/pdf/README.mdsandbox/salary_exploration_2023.ipynbsandbox/salary_exploration_2024.ipynbweb/index.htmlweb/job_search.html

3. Results from the 2023 guide

The repository keeps the raw text of the 2023 guide (the README names Randstad as the source) in sandbox/output/output.txt: one line per table cell, tagged with the heading level it came from. Re-reading that file with the table headers kept, instead of assuming every row is entry / mid / senior, gives the numbers below. All amounts are the guide’s own, in thousands of dollars per year, and each figure is the median of range midpoints (for “75.0 - 90.0”, the midpoint is 82.5). The companion article, What a Canadian salary guide says once the tables are read correctly, tells the same story with more context.

What is in the file.

CountRows
Rows with salary ranges19,387
In entry / mid / senior tables17,915
In tables by years of experience (1, 3, 5, 10 years)760
In tables by company revenue (10 M$, 100 M$, 1,000 M$)677
Exact duplicates (same job, region and province)3,607
Broken ranges (high above three times low, or below low)3
Clean job-and-region rows left14,305

The 1,437 rows from the experience and revenue tables are finance and HR roles. The extraction scripts read them as entry, mid and senior, which is how a chief financial officer at a company with 1,000 M$ in revenue (594.1k at the top of the range) ends up as a “senior” salary in the notebooks. Of the clean rows, the 1,242 in the industrial support family are hourly rates ($19.8, $22.3 and $24.9 an hour at the three levels), so they are kept apart from the 13,063 annual rows, which cover 371 jobs in 57 regions.

Levels. The median midpoint is 66.2k at entry level, 75.9k at mid level and 86.8k at senior level. The step between levels is very regular: senior pay is 1.30 times entry pay for the median job, and 80% of rows fall between 1.22 and 1.36.

Horizontal bar chart of median mid-level pay by job family: technologies 97.2k, engineering 81.8k, marketing 81.7k, human resources 76.5k, manufacturing 75.3k, sales 64.7k, healthcare 57.0k, business support 54.8k, customer experience 54.8k
Median mid-level pay by job family in the 2023 guide. Technology roles sit about 15k above engineering and marketing.

Provinces. Alberta has the highest median mid-level midpoint (85.2k), then British Columbia (81.8k) and Saskatchewan (78.4k); Québec (71.4k) and Prince Edward Island (70.3k) are lowest. These medians mix whatever jobs each region lists, and Québec alone has 16 regions, most of them outside Montréal, so the gap is partly a difference in which jobs and places are counted, not only in pay for the same job.

Horizontal bar chart of median mid-level pay by province, from Alberta at 85.2k to Prince Edward Island at 70.3k
Median mid-level pay by province. The job mix differs between provinces, so compare the same job across cities before drawing conclusions.

Same job, four cities. Comparing one job across regions removes the job-mix effect. For developer and architect roles the four metro areas are within about 10% of each other: a mid-level full stack developer is at 112.4k in Montréal, 120.2k in Greater Toronto, 115.1k in Greater Vancouver and 111.7k in Calgary, and DevOps (development) is highest in Montréal at 110.0k. The exception is support work: a mid-level help desk analyst is at 61.6k in Montréal against 78.7k in Greater Toronto.

Grouped bar chart of mid-level pay for seven tech roles in Montréal, Greater Toronto, Greater Vancouver and Calgary
Mid-level midpoints for seven tech roles in four metro areas. Developer pay is close across cities; help desk pay is not.

Best-paid jobs. Among jobs listed in at least 25 regions, the highest median mid-level midpoints are director of operations (190.4k), e-commerce director (159.7k), construction director (153.6k), manufacturing director (153.3k) and procurement director (152.3k). The best-paid technology roles are IT director (151.8k), technical lead (143.1k) and application architect (132.2k).

What the comparison notebook shows instead. sandbox/compare_2023_to_2024.ipynb compares the 2023 and 2024 pickles directly. Its maximum-salary chart puts “receptionist (bilingual)” at 713k, the broken Simcoe County row, and shows the same 713k for 2024 because the 2024 minimum and maximum lists read their senior value from df_sorted_2023. In the 2024 pickle, entry_max has exactly the same mean (94.67) and standard deviation as mid_max, which points to an entry range assembled from the wrong numbers. The 2024 guide is also in French (ingénieur·e chimiste) while 2023 is in English, so jobs cannot be matched across the two years by title.

Notebook bar chart of maximum salaries in 2023 and 2024, with 713k for both years at senior level
The saved notebook chart: both senior bars show the 713k outlier, the 2024 one because of a copy-paste of the 2023 frame.

4. Getting started

The README lists these steps:

pip install -r requirements.txt
python src/pdf.py
jupyter notebook

Run the extraction scripts from the repository root, since they use paths such as data/pdf/2023_salary_guide.pdf:

python src/pdf_salary_scraper_2023_1.py
python src/wages_in_time.py

api/setup.md describes Docker and Vercel deployment of the API; locally, it can be started with uvicorn from the api/ directory:

cd api
uvicorn app:app --reload

5. Repository guide

  • src/pdf_salary_scraper_2023_1.py and src/pdf_salary_scraper_2024_1.py extract the 2023 and 2024 guides (the README names Randstad as the source); the _2 and generic_wip scripts are function-based variants.
  • src/wages_in_time.py combines the 2012–2022 Job Bank wage tables.
  • src/stackoverflow_scraper.py scrapes the Stack Overflow survey salary table.
  • src/analyse.py and src/utils.py hold analysis and pickle helpers; src/__init__.py lists provinces and regions.
  • sandbox/ contains the 2023 and 2024 exploration notebooks and a 2023-to-2024 comparison.
  • data/pdf/, data/csv/, data/pickle/ and data/geo/ hold source guides from several recruitment firms, wage tables, extracted pickles and province shapefiles; data/csv/README.md documents the wage columns.
  • api/ contains the FastAPI app, Dockerfile, Compose file and deployment notes.
  • web/index.html shows 2023 results as static text; web/job_search.html is a search form mock-up.

6. Features

  • extract job, province, region and entry, mid and senior ranges from salary guide PDFs;
  • combine eleven years of Job Bank wage tables into one Polars data frame;
  • rank jobs by maximum salary, overall, per year, per province and for engineering roles;
  • compare one job across regions and provinces and find the widest regional gaps;
  • scrape Stack Overflow survey salaries;
  • serve the extracted 2023 and 2024 guides as JSON through FastAPI.

7. Limits

  • The README states the goal of a salary prediction model by region, experience and education; the repository contains extraction and exploration code, but no model.
  • The README commands name src/pdf_salary_scraper.py and a root stackoverflow_scraper.py, which do not exist under those names.
  • requirements.txt misses PyMuPDF, Polars and BeautifulSoup, which the scripts import; src/wages_in_time.py uses an older Polars API.
  • Province and region come from the first name in a fixed list found anywhere on the page, the entry range joins the low of the first pair with the high of the second, and four-range lines are skipped. The README and web/index.html list a bilingual receptionist at $713,000 as the top 2023 salary, which points to extraction errors.
  • src/wages_in_time.py annualizes hourly wages with 40 × 45 hours in one function and 45 × 45 in another, and flags the 2017 and 2018 files as badly formatted.
  • Deployment files disagree: vercel.json builds a root app.py while the app is api/app.py, the Dockerfile runs unicorn on port 8000 while Compose maps 5000, and the API reads pickles through ../data relative paths.
  • src/test.py imports functions that do not exist, and setup.py still points to a code package directory and a template description.

8. Sources

9. CI/CD pipeline

No CI/CD pipeline in the repository. The code is built and run by hand, as described above.

Generated from the source at commit 40b10f4.

10. Deployment diagram

Mermaid Deployment diagram
flowchart LR
  repo[("GitHub repository")]
  image["Docker image<br/>FROM python:3.12.0rc3-slim"]
  host_Vercel("Vercel<br/>vercel.json")
  repo -->|"docker build"| image
  repo -.->|"configured"| host_Vercel

11. External dependencies

12 declared packages (12 runtime, 0 development) in requirements.txt, api/requirements.txt.

Package table 12
External dependencies
PackageVersionScopePurposeRegistry
python-dotenv==0.21.1runtimeConfigurationpypi.org
backtesting==0.3.3runtimeTestingpypi.org
pandas==1.4.0runtimeData analysispypi.org
numpy==1.24.2runtimeData analysispypi.org
matplotlib==3.5.1runtimeChartspypi.org
seaborn==0.11.2runtimeChartspypi.org
PyPDF2==3.0.1runtimeA pure-python PDF library capable of splitting, merging, cropping, and transforming PDF filespypi.org
dash==2.8.1runtimeUser interfacepypi.org
fastapi—runtimeWeb frameworkpypi.org
uvicorn—runtimeApplication serverpypi.org
jinja2—runtimeTemplatespypi.org
pydantic—runtimeData validationpypi.org

12. Glossary

Repository

The versioned source files, configuration, and history of a project.

README

The repository’s entry document, usually covering its purpose and initial setup.

Dependency

A library, tool, or service that another component needs to build or run.

API

An interface through which software components exchange requests and results. An API does not have to use HTTP.

HTTP

A request-and-response protocol used by browsers and web services. A method, path, headers, and optional body describe a request.

Container

An isolated process environment created from an image. Persistent data needs storage outside the container’s disposable filesystem.

Docker Compose

A configuration format and tool for starting related containers, networks, and volumes. Startup order alone does not guarantee service readiness.

Volume and bind mount

A volume is storage managed by the container platform; a bind mount exposes a chosen host path inside a container.

Published port

A host port mapped to a service’s container port. Publishing a port can expose that service beyond its container network.

CLI

A text interface in which commands and arguments select operations. Exit codes let scripts detect success or failure.

Build

The steps that turn source files and dependencies into a runnable program or distributable artifact.

Backend

An implementation behind a shared interface. Depending on context, it can mean a server-side service or a graphics, input, or storage implementation.

13. References

  1. AlgoETS. (2026, November 5). Salary. [Software repository] github.com.
  2. AlgoETS. (2026, November 5). README. [Source file or documentation] github.com.
  3. AlgoETS. (2026, November 5). src/pdf_salary_scraper_2024_1.py. [Source file or documentation] github.com.
  4. AlgoETS. (2026, November 5). src/wages_in_time.py. [Source file or documentation] github.com.
  5. AlgoETS. (2026, November 5). sandbox/salary_exploration_2023.ipynb. [Source file or documentation] github.com.
  6. AlgoETS. (2026, November 5). sandbox/compare_2023_to_2024.ipynb. [Source file or documentation] github.com.
  7. AlgoETS. (2026, November 5). sandbox/output/output.txt. [Source file or documentation] github.com.
  8. AlgoETS. (2026, November 5). api/app.py. [Source file or documentation] github.com.
  9. AlgoETS. (2026, November 5). data/csv/README.md. [Source file or documentation] github.com.

Sources without a publication date show the date they were last checked, November 5, 2026. File links retain the inspected revision where available.