Resume Wizard
2024 — A web app that ranks a pile of resumes by the skills a job needs
01 Problem
In July 2023, a data scientist opening on LinkedIn had more than 250 applicants. Someone on the HR team has to read all of them to find the few worth a test and an interview, and that is one opening among many.
The goal is a tool that ranks a pile of resumes against the skills a role needs, so the reading starts with the best matches.
02 The original app
Resume Wizard was a Shiny web app in R. The first version was built in September 2023 for an event at a data science school, and Resume Wizard 2.0 replaced it on shinyapps.io in February 2024. It is offline now.
An HR user uploaded PDF resumes, typed the skills the role needs, either technical skills separated by commas or a written description with one skill per line, chose up to four clusters, and pressed Bewitch.
Back came a bar chart of the top 15 and a table of every resume's matched skills and percentage, a viewer with any one resume's match, word cloud, and PDF, and a word cloud of the matched skills in each cluster.

03 Ranking
Every resume is turned into text, lowercased, its punctuation replaced by spaces, and its runs of whitespace collapsed into one. The skills are split on commas and cleaned the same way, and the resume is cut into every run of 1 up to n words, where n is the number of words in the longest skill.
A skill matches when it equals one of those runs, and the score is the share of the skills a resume matches. Resumes are sorted by that score, and ties keep their order.
Rebuilding it fixed four things. A skill is now cleaned like the text, so one typed as "Node.js" or "Rhinoceros (Rhino)" can match at all. Punctuation becomes a space instead of disappearing, so "Photoshop/Illustrator" stays two words. The longest run is now the longest skill, not the number of skills, which with 24 skills had cut every resume into runs of up to 24 words for the same matches. And resumes are sorted by the exact share, not one rounded to two decimals that made different scores tie.

skills <- unlist(str_split(desired_skills, ",")) %>%
clean_text(as.corpus = F, lower = T, rm.number = F,
rm.stopwords_english = F, rm.stopwords_bahasa = F,
rm.punctuation = T, stem = F, rm.whitespace = T)
tokenized_desired_skills <- unique(skills[skills != ""])
max_n_gram <- tokenized_desired_skills %>% str_split(" ") %>% lengths() %>% max()
ranking_df <- resumes_df %>%
rowwise() %>%
mutate(Matching_Skills = intersect(tokenized_desired_skills,
tokenize_ngrams_by_range(PDF_Text, max_n_gram)) %>%
paste(collapse = ", "),
Matching_Percentage = ifelse(Matching_Skills == "", 0,
length(unlist(str_split(Matching_Skills, ", "))) /
length(tokenized_desired_skills))) %>%
arrange(desc(Matching_Percentage))04 Judging
The original was only ever tried on designer resumes, where every resume is a right answer, so it never showed whether the right ones rise. Here the same ranking runs on the Kaggle resume dataset, where each resume is labeled with its category.
It holds 2,484 resumes in 24 categories, 107 of them designers, so a resume picked at random is a designer 4.3% of the time. The skills are the two example lists the app offered for a designer, unchanged, and a good ranking puts designers first. The resumes are shuffled once, so resumes with the same score sit in random order, as an upload would.
A recruiter reads from the top, so the first measure is the share of designers among the first 10, 25, 50, and 100 resumes. AUC judges the whole order. It is the chance that a random designer scores above a random non-designer, with ties counted as half, and it is 0.783 for the technical skills.
set.seed(1)
pool_df <- pool_df[sample(nrow(pool_df)), ]
precision_at <- function(ranking_df, k) {
mean(ranking_df$Category[1:k] == "DESIGNER")
}
get_roc <- function(ranking_df) {
pROC::roc(response = ranking_df$Category == "DESIGNER",
predictor = ranking_df$Matching_Percentage,
levels = c(FALSE, TRUE),
direction = "<",
quiet = TRUE)
}05 Technical or descriptive
The 24 technical skills put 7 designers in the top 10, 24 in the top 50, and 36 in the top 100.
The written description, 7 lines about image editing and prototyping, puts 4 in the top 10 and 7 in the top 50, with an AUC of 0.603. It is cleaned harder, without stopwords and with every word stemmed, and cut into terms of one and two words, each kept within its line and counted once. So generic words like "creating", "managing", and "tools" become skills of their own and match almost any resume.
skill_lines <- unlist(str_split(desired_skills, "\n"))
skill_lines <- skill_lines %>%
clean_text(as.corpus = F, lower = T, rm.number = F,
rm.stopwords_english = T, rm.stopwords_bahasa = T,
rm.punctuation = T, stem = T, rm.whitespace = T)
tokenized_desired_skills <- skill_lines[skill_lines != ""] %>%
lapply(tokenize_ngrams_by_range, 2) %>%
unlist() %>%
unique()06 Who names what
Photoshop appears in 41% of designer resumes and 7% of the rest, Illustrator in 36% against 4%, AutoCAD in 25% against 3%, and InDesign in 23% against 2%. Each is far more common among designers, which is why the ranking works at all.
Yet even Photoshop, the commonest, is missing from most designer resumes, and 6 of the 24 skills, Lumion, Rhinoceros, Figma, Procreate, Inkscape, and Unity3D, appear in no resume at all.
technical_matches <- str_split(technical_pool_ranking$Matching_Skills, ", ")
is_designer <- technical_pool_ranking$Category == "DESIGNER"
skill_shares <- tibble(Skill = unique(unlist(technical_matches))) %>%
filter(Skill != "") %>%
rowwise() %>%
mutate(Designers = mean(sapply(technical_matches[is_designer], function(matched) Skill %in% matched)),
Others = mean(sapply(technical_matches[!is_designer], function(matched) Skill %in% matched))) %>%
ungroup() %>%
arrange(desc(Designers))07 Clustering
After ranking, the matched skills become features, one column per skill, 1 if a resume names it and 0 if not. K-means splits the resumes into the chosen number of clusters, and each cluster shows the skills its resumes share.
On the top 50 of the technical ranking, two clusters explain who rose. In one, all 12 resumes name AutoCAD and 7 name Revit, which makes it a CAD and architecture group of 7 designers and 5 from the construction, agriculture, engineering, and teacher categories.
In the other, all 38 name Photoshop, and most also name Illustrator, InDesign, and Dreamweaver. It is the graphic and web design cluster, where 17 designers sit next to digital media, arts, and public relations resumes, among others.
Two fixes make the clusters trustworthy. Each matched skill is now one feature, where the old document-term matrix split "Premiere Pro" and "Cinema 4D" into separate words and dropped any word shorter than three letters, so XD and 4D never counted. And k-means now runs from a fixed seed with 25 starts, so the same resumes always give the same clusters.

cluster_resumes <- function(ranking_df, k){
resume_skills <- str_split(ranking_df$Matching_Skills, ", ")
skills <- unique(unlist(resume_skills))
skills <- skills[skills != ""]
skill_matrix <- t(sapply(resume_skills, function(matched) as.integer(skills %in% matched)))
colnames(skill_matrix) <- skills
rownames(skill_matrix) <- ranking_df$Resume
set.seed(1)
clusters <- kmeans(x = skill_matrix,
centers = k,
nstart = 25)
ranking_df %>% mutate(Cluster = clusters$cluster)
}08 In the browser
The app on this page runs the same ranking in the browser on 240 resumes of the dataset, 10 from each of the 24 categories, so designers are about as rare as in the whole. There is no upload, because their text was extracted from the PDFs and cleaned in R exactly as above, and the browser cuts the same runs of words, matches them the same way, and keeps tied resumes in pool order.
K-means moves one resume at a time to the cluster that lowers the spread most, the method behind R's default, from its own fixed seed with 25 starts, so on a near tie it can split the resumes differently from R.
It takes technical skills separated by commas only. The descriptive mode is judged above but not offered there.
09 Limits
A skill counts only when its exact words appear. 35% of designers name none of the 24 skills, against 88% of everyone else, so a keyword ranker never sees those designers at all, since "photo editing" never matches "Photoshop".
It only reads text. A scanned PDF with no text layer scores zero, as one resume in the dataset does.
The example list mixes CAD tools like AutoCAD, Revit, and SolidWorks into design, so engineering and construction resumes rise with the designers. Behind the designers, related fields crowd in. Of the top 50, 24 are designers, followed by 8 digital media, 3 arts, and 3 public relations.
The score is a share of the whole list, so a long wish list keeps every score low, and even the best resume names only 6 of the 24 skills, or 25%.
Resume Wizard
7 of the top 10 are designers, against a 4.3% base rate — AUC 0.78