Automated Resume Screening with NLP
Abstract
Section titled “Abstract”Automated Resume Screening with NLP is a Python project that uses NLP to screen and rank resumes. The application features text extraction, candidate ranking, and a CLI interface, demonstrating best practices in HR analytics and text processing.
Prerequisites
Section titled “Prerequisites”- Python 3.8 or above
- A code editor or IDE
- Basic understanding of NLP and HR analytics
- Required libraries:
nltk,scikit-learn,pandas
Before you Start
Section titled “Before you Start”Install Python and the required libraries:
pip install nltk scikit-learn pandasGetting Started
Section titled “Getting Started”Create a Project
Section titled “Create a Project”- Create a folder named
automated-resume-screening-nlp. - Open the folder in your code editor or IDE.
- Create a file named
automated_resume_screening_nlp.py. - Copy the code below into your file.
Write the Code
Section titled “Write the Code”Automated Resume Screening with NLP
pch.viewSource"""Resume screening by keyword, and what that actually selects for.
The version this replaces counted how many keywords from a list appeared in
each file and ranked by the total. That is what most screening tools do, and
this file measures what it costs rather than assuming it works.
Three things are measured against hand-written ground truth: how often the
keyword score agrees with a human judgement, how much of the score is
recoverable by simply writing longer, and how many qualified candidates are
rejected for using a synonym. None of these needs a large model to
demonstrate, and all three are reasons real screening systems get audited.
python automated_resume_screening_nlp.py
"""
import re
from collections import Counter
KEYWORDS = ["python", "sql", "docker", "kubernetes", "aws", "testing",
"ci", "postgres", "api", "linux"]
# Synonyms a person reads as equivalent and a keyword matcher does not.
SYNONYMS = {
"python": {"python3", "cpython"},
"sql": {"postgresql", "mysql", "sqlite", "queries"},
"docker": {"containers", "containerised", "containerized", "podman"},
"kubernetes": {"k8s", "eks", "gke"},
"aws": {"amazon", "ec2", "s3", "lambda"},
"testing": {"pytest", "unittest", "tdd", "test"},
"ci": {"jenkins", "buildkite", "actions", "pipeline"},
"postgres": {"postgresql", "psql"},
"api": {"rest", "endpoint", "endpoints", "graphql"},
"linux": {"ubuntu", "debian", "unix", "bash"},
}
# (name, resume text, what an experienced reviewer said)
CANDIDATES = [
("A. strong, plain words",
"Five years building Python services on AWS. Docker in production, "
"Kubernetes for orchestration, SQL for reporting, testing with CI on "
"every merge. Linux throughout. Designed the API.", 5),
("B. strong, uses synonyms",
"Five years of backend work: python3 microservices on EC2 and S3, "
"shipped in containers orchestrated with k8s, PostgreSQL for storage, "
"pytest and a Buildkite pipeline, Ubuntu servers, REST endpoints.", 5),
("C. keyword stuffed, thin",
"Python SQL Docker Kubernetes AWS testing CI Postgres API Linux. "
"Python SQL Docker Kubernetes AWS. Keen to learn. No commercial "
"experience yet.", 1),
("D. genuinely junior",
"Recent graduate. Coursework in Python and some SQL. Built a small "
"web API for a final project. Familiar with Linux.", 2),
("E. strong, very long",
"Extensive experience across the stack. " + ("Delivered projects using "
"Python and SQL with Docker and testing. " * 8) +
"Led the platform team for three years.", 4),
("F. wrong field, right words",
"Marketing manager. Ran campaigns for a Python conference, an AWS "
"partner and a Docker meetup. Wrote copy about Kubernetes and SQL "
"for our API product blog.", 1),
]
def tokens(text):
return re.findall(r"[a-z0-9]+", text.lower())
def keyword_score(text, keywords=KEYWORDS):
"""What the original did: count keyword occurrences."""
counts = Counter(tokens(text))
return sum(counts[k] for k in keywords)
def coverage_score(text, keywords=KEYWORDS):
"""How many distinct keywords appear, regardless of repetition."""
present = set(tokens(text))
return sum(k in present for k in keywords)
def synonym_coverage(text, keywords=KEYWORDS):
"""Coverage, counting a synonym as the keyword it stands for."""
present = set(tokens(text))
hits = 0
for keyword in keywords:
if keyword in present or (SYNONYMS.get(keyword, set()) & present):
hits += 1
return hits
def density_score(text, keywords=KEYWORDS):
"""Coverage per hundred words -- resistant to padding, not to stuffing."""
words = tokens(text)
return coverage_score(text) / max(len(words), 1) * 100
def spearman(a, b):
"""Rank correlation, written out: n is 6 and scipy is a big dependency."""
def ranks(values):
order = sorted(range(len(values)), key=lambda i: -values[i])
out = [0.0] * len(values)
for position, index in enumerate(order):
out[index] = position + 1
return out
ra, rb = ranks(a), ranks(b)
n = len(a)
d2 = sum((x - y) ** 2 for x, y in zip(ra, rb))
return 1 - 6 * d2 / (n * (n * n - 1))
def main():
print("Automated Resume Screening")
print(f" candidates : {len(CANDIDATES)}")
print(f" keywords : {len(KEYWORDS)}")
truth = [c[2] for c in CANDIDATES]
scorers = (("keyword count", keyword_score),
("distinct coverage", coverage_score),
("coverage + synonyms", synonym_coverage),
("coverage per 100 words", density_score))
print(f"\n{'candidate':>26} {'words':>6} {'reviewer':>9} " +
" ".join(f"{name.split()[0][:9]:>10}" for name, _ in scorers))
print(" " + "-" * 76)
columns = {name: [] for name, _ in scorers}
for name, text, rating in CANDIDATES:
row = []
for label, scorer in scorers:
value = scorer(text)
columns[label].append(value)
row.append(value)
print(f"{name:>26} {len(tokens(text)):>6} {rating:>9} " +
" ".join(f"{v:>10.2f}" for v in row))
print(f"\n agreement with the reviewer (Spearman rank correlation):")
for label, _ in scorers:
print(f" {label:>24} {spearman(truth, columns[label]):>7.3f}")
counts = columns["keyword count"]
stuffed = counts[2]
genuine = counts[1]
print(f"\n Candidate C is keyword-stuffed and unqualified; the reviewer")
print(f" scored it 1 of 5. Its keyword count is {stuffed}, against "
f"{genuine} for")
print(f" candidate B, who is qualified and wrote in synonyms.")
print(f" A keyword counter ranks the worst candidate "
f"{'above' if stuffed > genuine else 'below'} the best one.")
print(f"\n what synonyms cost candidate B:")
print(f" exact keywords matched : {coverage_score(CANDIDATES[1][1])} "
f"of {len(KEYWORDS)}")
print(f" with synonyms accepted : "
f"{synonym_coverage(CANDIDATES[1][1])} of {len(KEYWORDS)}")
print(" Same person, same experience, described in the words their")
print(" last employer used. Every unmatched keyword here is a real")
print(" skill the filter did not see.")
print(f"\n what padding buys candidate E:")
short = "Delivered projects using Python and SQL with Docker and testing."
padded = short + " " + short * 8
print(f" {len(tokens(short)):>3} words: keyword count "
f"{keyword_score(short):>3}, density "
f"{density_score(short):>5.2f}")
print(f" {len(tokens(padded)):>3} words: keyword count "
f"{keyword_score(padded):>3}, density "
f"{density_score(padded):>5.2f}")
print(" Repeating one sentence nine times multiplied the keyword count")
print(" nine-fold and divided the density by nine. The two scores move")
print(" in opposite directions on the same edit, so they cannot both")
print(" be measuring the candidate.")
print(" Neither is safe on its own: counting rewards padding, and")
print(" density rewards a terse resume that lists ten keywords and")
print(" nothing else -- which is candidate C, rated 1 of 5.")
print(f"\n Candidate F is in the wrong field entirely and mentions every")
print(f" keyword in context: keyword count {counts[5]}, reviewer rating "
f"{truth[5]}.")
print(" No bag-of-words method can separate 'wrote copy about Kubernetes'")
print(" from 'ran Kubernetes'. That needs the sentence, not the word,")
print(" and it is the reason keyword screening has to be a filter that")
print(" a person reviews rather than a decision.")
if __name__ == "__main__":
main() Example Usage
Section titled “Example Usage”python automated_resume_screening_nlp.pyWhat it produces
Section titled “What it produces”Running the file exactly as it ships takes 0.1 s and prints:
Automated Resume Screening
candidates : 6
keywords : 10
candidate words reviewer keyword distinct coverage coverage
----------------------------------------------------------------------------
A. strong, plain words 27 5 9.00 9.00 9.00 33.33
B. strong, uses synonyms 29 5 0.00 0.00 10.00 0.00
C. keyword stuffed, thin 22 1 15.00 10.00 10.00 45.45
D. genuinely junior 20 2 4.00 4.00 4.00 20.00
E. strong, very long 92 4 32.00 4.00 4.00 4.35
F. wrong field, right words 26 1 6.00 6.00 6.00 23.08
agreement with the reviewer (Spearman rank correlation):
keyword count -0.086
distinct coverage -0.314
coverage + synonyms 0.200
coverage per 100 words -0.314
Candidate C is keyword-stuffed and unqualified; the reviewer
...The first 20 of 48 lines are shown; the run continues past this point.
How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD RUN(["python automated_resume_screening_nlp.py"]) ResumeParser["ResumeParser
class"] ResumeScreening["ResumeScreening
class"] CLI["CLI
class"] RUN --> ResumeParser CLI --> ResumeScreening ResumeScreening --> ResumeParser
Explanation
Section titled “Explanation”Key Features
Section titled “Key Features”- Text Extraction: Processes and extracts text from resumes.
- Candidate Ranking: Ranks candidates based on skills and experience.
- Error Handling: Validates inputs and manages exceptions.
- CLI Interface: Interactive command-line usage.
Code Breakdown
Section titled “Code Breakdown”- What it imports (lines 16–17)
import re
from collections import Counterkeyword_score— the function (lines 68–71)
def keyword_score(text, keywords=KEYWORDS):
"""What the original did: count keyword occurrences."""
counts = Counter(tokens(text))
return sum(counts[k] for k in keywords)synonym_coverage— the function (lines 80–87)
def synonym_coverage(text, keywords=KEYWORDS):
"""Coverage, counting a synonym as the keyword it stands for."""
present = set(tokens(text))
hits = 0
for keyword in keywords:
if keyword in present or (SYNONYMS.get(keyword, set()) & present):
hits += 1
return hitsspearman— the function (lines 96–108)
def spearman(a, b):
"""Rank correlation, written out: n is 6 and scipy is a big dependency."""
def ranks(values):
order = sorted(range(len(values)), key=lambda i: -values[i])
out = [0.0] * len(values)
for position, index in enumerate(order):
out[index] = position + 1
return out
ra, rb = ranks(a), ranks(b)
n = len(a)
d2 = sum((x - y) ** 2 for x, y in zip(ra, rb))
return 1 - 6 * d2 / (n * (n * n - 1))main— the function (lines 111–181)
def main():
print("Automated Resume Screening")
print(f" candidates : {len(CANDIDATES)}")
print(f" keywords : {len(KEYWORDS)}")
truth = [c[2] for c in CANDIDATES]
scorers = (("keyword count", keyword_score),
("distinct coverage", coverage_score),
("coverage + synonyms", synonym_coverage),
("coverage per 100 words", density_score))
print(f"\n{'candidate':>26} {'words':>6} {'reviewer':>9} " +
" ".join(f"{name.split()[0][:9]:>10}" for name, _ in scorers))
print(" " + "-" * 76)
columns = {name: [] for name, _ in scorers}
for name, text, rating in CANDIDATES:
row = []
for label, scorer in scorers:
# ... 47 more lines in the file ...
print(f" keyword in context: keyword count {counts[5]}, reviewer rating "
f"{truth[5]}.")
print(" No bag-of-words method can separate 'wrote copy about Kubernetes'")
print(" from 'ran Kubernetes'. That needs the sentence, not the word,")
print(" and it is the reason keyword screening has to be a filter that")
print(" a person reviews rather than a decision.")The file defines 7 top-level symbols in all; the whole thing is above under Write the Code.
Features
Section titled “Features”- Automated Resume Screening: Text extraction and candidate ranking
- Modular Design: Separate functions for extraction and ranking
- Error Handling: Manages invalid inputs and exceptions
- Production-Ready: Scalable and maintainable code
Next Steps
Section titled “Next Steps”Enhance the project by:
- Integrating with real-world resume datasets
- Supporting batch screening
- Creating a GUI with Tkinter or a web app with Flask
- Adding evaluation metrics (precision, recall)
- Unit testing for reliability
Educational Value
Section titled “Educational Value”This project teaches:
- HR Analytics: Resume screening and candidate ranking
- Software Design: Modular, maintainable code
- Error Handling: Writing robust Python code
Real-World Applications
Section titled “Real-World Applications”- Recruitment Tools
- HR Analytics
- Educational Tools
Conclusion
Section titled “Conclusion”Automated Resume Screening with NLP demonstrates how to build a scalable and accurate resume screening tool using Python. With modular design and extensibility, this project can be adapted for real-world applications in HR, recruitment, and more. For more advanced projects, visit Python Central Hub.
Pitfalls
Section titled “Pitfalls”- Keyword counting agrees with a human reviewer less than chance does. Measured Spearman rank correlation against hand-written ratings: keyword count -0.086, distinct coverage -0.314, coverage with synonyms 0.200. A negative correlation means the ranking is worse than arbitrary.
- A qualified candidate who uses synonyms scores zero. Candidate B matched 0 of 10 keywords exactly and 10 of 10 once synonyms were accepted — same person, same experience, described in the words their last employer used.
- Counting rewards repetition. Repeating one sentence nine times took the keyword count from 4 to 36 and the density from 40.00 to 4.44. The two scores move in opposite directions on the same edit, so at most one of them is measuring the candidate.
- Density rewards the opposite failure. The keyword-stuffed resume with no experience scores the highest density in the table, 45.45, and was rated 1 of 5 by the reviewer.
- No bag-of-words method can read context. Candidate F is a marketing manager who ran campaigns for a Python conference and an AWS partner: keyword count 6, reviewer rating 1. Separating “wrote copy about Kubernetes” from “ran Kubernetes” needs the sentence.
- A synonym list is a permanent maintenance commitment. Ten keywords needed thirty hand-written synonyms here and the list is still incomplete. Every new tool the industry adopts adds another.
- Measured: 6 candidates, 10 keywords, four scoring methods, all compared against explicit reviewer ratings.
- Best agreement of any method: 0.200 (coverage with synonyms). Every method that ignores synonyms scores negative.
- The keyword-stuffed candidate scores 15 on keyword count against candidate B’s 0 — the worst applicant ranked above the best.
- Screening by keyword is defensible as a filter a person then reads. It is not defensible as a decision, and these numbers are why.
-
Keyword counting scored -0.086 Spearman against reviewer ratings. What does a negative rank correlation mean here?
pch.quizShowAnswer
B — The ranking is slightly worse than arbitrary — the method is not a weak signal, it is anti-correlated with what the reviewer valued
-
Candidate B matched 0 of 10 keywords exactly and 10 of 10 with synonyms. What is the practical consequence?
pch.quizShowAnswer
B — A qualified applicant is filtered out for describing the same work in different words — and neither the applicant nor the employer ever finds out
-
Padding took the keyword count from 4 to 36 and the density from 40.00 to 4.44. What follows?
pch.quizShowAnswer
B — The two disagree completely on the same edit, and density has its own failure — it ranks the experience-free keyword list highest, at 45.45
Try it yourself
Section titled “Try it yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading