Live · de25-hemtenta-christofer.onrender.com

MYH Data Pipeline

by Christofer Lindholm · Data Engineering

8 years · 8 Excel files · 9,983 records → PostgreSQL + FastAPI. Swedish vocational education (YH) application rounds, harmonized from inconsistent source data into one clean, queryable dataset.

Python 3.11pandasPostgreSQL 16FastAPIpsycopg2DockerNeon.techRender.com
Years covered
2018–25
Source files
8
Records
9,983
API endpoints
17
02 / Architecture

Pipeline overview

01
Raw MYH files

8 yearly Excel files from 2018-2025. Sheet names, header rows, and columns differ across years.

02
Harmonize

One column map turns the yearly files into one canonical applications schema with traceable source columns.

03
Clean & derive

Normalize decisions, fix Landsting to Region, coerce numeric values, and derive distance/multi-municipality flags.

04
Validate

Notebook and API checks verify row counts, year coverage, required values, and normalized decision values.

05
PostgreSQL

9,983 rows loaded into one typed applications table with indexes for common filters and stats.

06
FastAPI

17 endpoints expose records, stats, CSV export, refresh, prediction, and SCB municipality enrichment.

07
ML model

Random Forest model estimates approval probability from historical MYH application patterns.

08
SCB enrichment

Separate enrichment notebook joins population, employment, and unemployment context from Statistics Sweden.

09
Dashboard

React, TanStack Query, and Recharts present the API, notebooks, data explorer, ML model, and enrichment layer.

03 / Walkthrough

Project story

Each step below covers the decisions made at that stage, from the source files to the live API.

04 / Notebooks

Jupyter notebooks

Full notebooks including all code and cell outputs. Click any tab to switch notebook.

Excel ingestion, harmonization, cleaning, enrichment, validation, and CSV export.

data_preparation.ipynbopen full screen
05 / Statistics

Live dashboard

All charts hit the FastAPI directly, no mock data, no caching layer. If the API is cold-starting on Render's free tier, the data may take up to 60 seconds to appear. Charts will populate automatically.

Total applications
-
Curated rows in PostgreSQL
Overall approval
-
Highest volume
-
Lowest approval
-

Applications over time

Volume trend with highlighted peak and latest year

Approval rate trend

Approved applications divided by total

Decision comparison

Approved, rejected, and withdrawn by year

County comparison

Top counties by application volume

Education areas

Volume and approval rate by subject area

Study form comparison

On-site, distance, and other study forms

Top providers

Largest providers by application count

06 / Records

Data explorer

Query GET /applications with combinable filters. 50 rows per page, server-side pagination.

Filters
CSV

All filters except Area query the full dataset server-side. Area filters only the rows on the current page.

YearProgramAreaPointsProviderCityDecisionFormExamDetails
Page 1 · loaded 0 rows

External enrichment: Statistics Sweden municipality context

MYH application activity joined with SCB population and BAS labour-market statistics. The charts use municipality/kommun, not informal city labels.

MYH2018–2025
SCB
Time window
MYH 2018–2025SCB

All MYH years 2018–2025 are aggregated. Each municipality is joined with its most recent SCB snapshot (population stops at 2024, labour-market data at 2024).

Normalization
per 100k residents

Raw totals favor large municipalities. Per-capita values make smaller municipalities comparable.

Interpretation
context, not cause

Employment and unemployment describe the local labour market. They do not prove why MYH approved an application.

07 / Reference

API endpoints

MethodPathDescription
GET/pipeline/statusLive data quality checks against the applications table.
GET/healthDatabase connectivity and row-count check.
GET/applicationsList applications with year, decision, region, municipality, provider, and study-form filters.
GET/applications/{diarienummer}All rows for one MYH case number.
GET/providersDistinct education provider names.
GET/providers/{name}/applicationsProvider search using case-insensitive contains matching.
GET/stats/by-yearYearly totals and approval rates.
GET/stats/by-regionCounty statistics; excludes Flera kommuner by default.
GET/stats/by-education-areaVolume and approval rate grouped by education area.
GET/stats/by-study-formComparison by Bunden, Distans, and other study forms.
GET/stats/by-providerTop providers by application count.
GET/stats/multi-municipalitySeparate summary for rows spanning several municipalities.
GET/stats/municipality-enrichmentMYH municipality summaries joined with SCB population and labour-market context.
GET/export/applicationsFiltered applications as CSV.
GET/export/stats/by-yearYearly stats as CSV.
POST/predictML model: approval probability estimate for a new application.
POST/refreshReload the database table from the curated CSV.
08 / Implementation

Technical details

Tools & stack

ToolLayerRole
Python 3.11RuntimeCore language for the data pipeline and API
pandasDataExcel ingestion, harmonization, cleaning, and enrichment
openpyxlDataEngine for reading MYH .xlsx files
SCB PxWeb APIExternal dataPopulation and BAS labour-market enrichment snapshot
JupyterNotebookDocuments the raw-data journey and transformation decisions
PostgreSQL 16StorageStores the curated applications table
psycopg2-binaryDatabasePostgreSQL adapter — direct connection pooling and parameterized SQL queries
FastAPIAPIRead API with filters, statistics, CSV export, and Swagger docs
PydanticAPIResponse models for application rows and yearly stats
UvicornAPIASGI server for running the FastAPI app
python-dotenvConfigLoads local PG_* settings from .env
DockerLocal/DeployContainerized API image based on python:3.11-slim
Docker ComposeLocalStarts PostgreSQL and API together with seeded data
Neon.techCloud DBHosted PostgreSQL for the deployed API
Render.comHostingHosts the deployed FastAPI service
RuffQualityPython formatter and linter configured in the backend repo
LovableFrontendCreated and deploys this React dashboard project
React 19FrontendUI framework for the dashboard and explorer
TanStack QueryFrontendLive API fetching, retry, and loading state
RechartsFrontendGradient trend charts and comparison charts
Tailwind CSS v4FrontendVisual styling and responsive layout
Claude CodeAssistantCode assistant and project assistant during development
CodexAssistantReviewer of the project plus API/frontend polish and release prep

Harmonization decisions

  • beslut
    Mapped to approved / rejected / withdrawn. Handles 'Beviljad', 'Avslag', 'Ej beviljad', 'Återkallad' and per-year variants.
  • Header rows
    2018–2022 header on row 0; 2023–2024 on row 5; 2025 on row 6. Detected per-file before parsing.
  • Column names
    Unified across all 8 files via a single COLUMN_MAP. Diacritics preserved (YH-poäng, Utbildningsområde, Sökta omgångar).
  • Landsting → Region
    2018 file uses old administrative term. Corrected in harmonization so huvudmannatyp is consistent 2018–2025.
  • SUN5 / SeQF
    Columns introduced in 2023. Kept as null for 2018-2022, not imputed or dropped.
  • typ_av_examen
    Missing in 2018 and 2020, expected. Documented in the schema.
  • yh_poang
    Coerced to int via pd.to_numeric(errors='coerce'). Non-numeric or empty → null.
  • is_distance
    Derived: True when studieform contains 'Distans'. Added as a boolean column for easier filtering.

applications schema

27 columns (26 data + id). ? = nullable.

ColumnTypeNotes
idintSurrogate primary key (SERIAL)
source_yearintMYH application round year
source_filetextOriginal .xlsx filename
source_sheettextSheet within the workbook
diarienummertextMYH case number (not unique pre-2019)
utbildningsnamntextProgram name
utbildningsomradetext?Education area
besluttext?Raw Swedish decision value
beslut_normalizedtext?approved / rejected / withdrawn
lantext?County (län)
kommuntext?Municipality
yh_poangint?YH credits
studieformtext?Form of study
studietakt_procentint?Pace of study (%)
typ_av_examentext?Exam type, null for 2018 and 2020
utbildningsanordnaretext?Education provider
huvudmannatyptext?Provider type (Privat/Kommun/Region/Statlig)
flera_kommunertext?Multi-municipality raw value, null for 2018
antal_kommunerint?Municipality count, null for 2018
sokta_omgangarint?Rounds applied for, null for 2018-2019
beviljade_omgangarint?Rounds granted, null for 2018-2019
sun5_inriktningtext?SUN5 subject code, 2023+ only
sun5_inriktning_namntext?SUN5 subject name, 2023+ only
seqf_nivatext?SeQF qualification level, 2023+ only
smalt_yrkesomradetext?Narrow occupational area, 2023+ only
is_distancebool?Derived: studieform contains 'Distans'
has_multiple_municipalitiesbool?Derived: more than one kommun
09 / ML Model

Approval Predictor

Random Forest · 9 983 training records

Fill in the details of a YH application. The model estimates the probability it would be approved based on 8 years of MYH decisions (2018–2025).

Application details
Values must match the training data exactly.
Result

Fill in the details and click Predict.

How it works: A Random Forest model trained on 9,983 MYH applications (2018–2025) predicts based on region, education area, study form, operator type, program size, and year.

Limitation: The model reflects historical patterns only. MYH's actual decisions also weigh program quality, employer demand, and regional need, none of which are in the dataset. Historical average approval rate: ~36%.

10 / Verification

Data quality report

Live checks against the applications table, checking row count, year coverage, null constraints, and value integrity. Run against the production database on every page load. The four notebooks also include their own end logs or output summaries for the recorded walkthrough.