Step-by-step builds

AI Resume Screening Project: Rank Resumes Against a Job Description in Python

Satvik ParamkushamUpdated October 5, 202613 min read

Quick answer

An AI resume screening project ranks resumes by how well they match a job description. You extract text from PDF resumes, score each one with TF-IDF cosine similarity and with sentence embeddings, check which required skills appear, and combine the scores into a ranked table. A strong version also measures accuracy on labelled resumes and tests for bias.

Recruiters at large companies can receive hundreds of applications for a single opening, and many use software to sort them before a human reads anything. Building a small version of that software is a popular final-year project because it is easy to demo, uses real NLP, and connects directly to the placement process every student is about to go through.

This guide builds a resume screener in Python that reads a folder of PDF resumes and a job description, ranks the candidates with two different text-matching methods, lists matched and missing skills for each, and saves a ranked table. Then it does the part most student projects skip: measuring whether the ranking is any good, and checking whether it treats candidates fairly.

That last part matters. Automated hiring tools have a documented history of bias, and an examiner or interviewer who knows the topic will ask about it.

What is an AI resume screening project?

An AI resume screening project is software that compares resumes with a job description and scores how well each candidate matches. It answers one question for a recruiter: which resumes should a human read first?

The project uses three signals, each catching something the others miss:

  • TF-IDF similarity: rewards resumes that use the same important words as the job description. Fast and explainable, but blind to synonyms.
  • Embedding similarity: uses embeddings to compare meaning, so "built REST APIs in Flask" can match "backend web development in Python" even with few shared words.
  • Skill coverage: checks a curated list of skills and reports exactly which required skills each resume shows and which it lacks.

What do you need to build a resume screener in Python?

You need intermediate Python, a laptop with about 8 GB of RAM, and a set of test resumes. No GPU and no API key are required.

PartWhat we useWhy
PDF readingpypdfPure Python, handles most text-based PDFs
Keyword matchingscikit-learn TfidfVectorizer and cosine_similarityStandard, explainable baseline
Semantic matchingsentence-transformers (all-MiniLM-L6-v2)Free, small, runs on CPU
SkillsA Python dictionary of skills and aliasesTransparent and easy to extend
Outputpandas DataFrame saved as CSVOpens in Excel for recruiters

About test data: never use real people's resumes without consent. Write 15 to 20 sample resumes yourself (with friends, using fake names and contacts), or use public resume datasets on Kaggle, and check each dataset's licence. India's Digital Personal Data Protection Act, 2023 treats resumes as personal data, which is a good point to mention in your report.

How does an AI resume screening system work? (architecture)

The screener runs a simple pipeline once per job opening:

  1. Extract: read text from every PDF resume and the job description.
  2. Score keywords: TF-IDF vectors for the job description and each resume, then cosine similarity.
  3. Score meaning: split the job description into requirement lines and each resume into chunks, embed both, and measure how well each requirement is covered.
  4. Match skills: find skills from your list in the job description and in each resume.
  5. Rank: combine the three scores into one, sort, and save a table with matched and missing skills.
  6. Evaluate: compare the ranking with human shortlist decisions on a labelled set.

Steps 2 to 4 are independent, which makes the comparison clean: you can report how each signal performs alone and combined.

How to build an AI resume screener in Python, step by step

  1. Step 1. Set up the project and sample data

    Create the folder structure and install the libraries.

    mkdir resume-screener && cd resume-screener
    python -m venv .venv
    # Windows: .venv\Scripts\activate    macOS/Linux: source .venv/bin/activate
    pip install pypdf scikit-learn sentence-transformers pandas
    mkdir resumes

    Put your sample PDF resumes in the resumes folder and paste a real-looking job description (for example a data analyst fresher role) into job_description.txt. Make the resumes varied: strong matches, partial matches, clear mismatches, and a couple that use different wording for the same skills.

  2. Step 2. Extract text from the PDF resumes

    pypdf reads the text layer of each PDF. The warning catches scanned resumes, which contain images instead of text.

    # extract.py
    import re
    from pathlib import Path
    from pypdf import PdfReader
    
    def pdf_text(path):
        reader = PdfReader(path)
        text = " ".join((page.extract_text() or "") for page in reader.pages)
        return re.sub(r"\s+", " ", text).strip()
    
    def load_resumes(folder="resumes"):
        resumes = {}
        for pdf in sorted(Path(folder).glob("*.pdf")):
            text = pdf_text(pdf)
            if len(text.split()) < 50:
                print(f"Warning: {pdf.name} has almost no text (scanned PDF?)")
            resumes[pdf.name] = text
        return resumes
    
    def load_jd(path="job_description.txt"):
        return Path(path).read_text(encoding="utf-8")

    Print the extracted text of two or three resumes. Two-column layouts sometimes come out with lines interleaved, which you should note as a limitation.

  3. Step 3. Rank resumes with TF-IDF cosine similarity

    TF-IDF gives each word a weight that is high when it is frequent in one document but rare across all documents. Cosine similarity then measures how closely a resume's word profile matches the job description's.

    # score_tfidf.py
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.metrics.pairwise import cosine_similarity
    
    def tfidf_scores(jd, resumes):
        names = list(resumes)
        vec = TfidfVectorizer(stop_words="english", ngram_range=(1, 2), sublinear_tf=True)
        matrix = vec.fit_transform([jd] + [resumes[n] for n in names])
        sims = cosine_similarity(matrix[0], matrix[1:])[0]
        return dict(zip(names, sims))
    
    if __name__ == "__main__":
        from extract import load_resumes, load_jd
        scores = tfidf_scores(load_jd(), load_resumes())
        for name, s in sorted(scores.items(), key=lambda x: -x[1]):
            print(f"{s:.3f}  {name}")

    Run python score_tfidf.py. Your first ranking is ready. Notice its weakness: a resume that says "Postgres" scores nothing for a job description that says "SQL".

  4. Step 4. Add semantic matching with sentence-transformers

    Embedding models compare meaning rather than exact words. all-MiniLM-L6-v2 reads at most 256 word pieces at a time, so a whole resume would be cut off. Instead, we split the job description into requirement lines, split each resume into chunks, and ask: for each requirement, how well does the best-matching chunk cover it?

    # score_semantic.py
    import re
    from sentence_transformers import SentenceTransformer
    
    model = SentenceTransformer("all-MiniLM-L6-v2")
    
    def chunks(text, size=120):
        words = text.split()
        return [" ".join(words[i:i + size]) for i in range(0, len(words), size)] or [""]
    
    def requirements(jd):
        lines = [l.strip(" -*\t") for l in re.split(r"[\n.;]", jd)]
        return [l for l in lines if len(l.split()) >= 4]
    
    def semantic_scores(jd, resumes):
        reqs = model.encode(requirements(jd), normalize_embeddings=True)
        scores = {}
        for name, text in resumes.items():
            parts = model.encode(chunks(text), normalize_embeddings=True)
            sim = model.similarity(reqs, parts)  # rows: requirements, columns: chunks
            scores[name] = float(sim.max(dim=1).values.mean())
        return scores

    Averaging the best match per requirement means a resume cannot score high by repeating one skill many times. It has to cover most of the requirements.

  5. Step 5. Extract skills with a keyword list

    Scores alone do not tell a recruiter why a candidate ranked high. A skill list does. Each skill has aliases, and the regex checks word boundaries so "excel" does not match "excellent" and "ml" does not match "html".

    # skills.py
    import re
    
    SKILLS = {
        "python": ["python"],
        "sql": ["sql", "mysql", "postgresql", "postgres"],
        "excel": ["excel", "spreadsheets"],
        "power bi": ["power bi", "powerbi"],
        "tableau": ["tableau"],
        "statistics": ["statistics", "statistical analysis"],
        "machine learning": ["machine learning", "ml", "scikit-learn"],
        "deep learning": ["deep learning", "pytorch", "tensorflow"],
        "communication": ["communication", "presentation"],
        "git": ["git", "github"],
    }
    
    def find_skills(text):
        text = text.lower()
        found = set()
        for skill, aliases in SKILLS.items():
            for a in aliases:
                if re.search(rf"(?<![a-z0-9]){re.escape(a)}(?![a-z0-9])", text):
                    found.add(skill)
                    break
        return found

    Extend the dictionary for each role you test. Keeping it as plain data, not a black box, means anyone can audit what the screener looks for.

  6. Step 6. Combine the scores into a ranked table

    TF-IDF and embedding scores sit on different scales, so we rescale each to 0 to 1 across candidates before combining them.

    # rank.py
    import pandas as pd
    from extract import load_resumes, load_jd
    from score_tfidf import tfidf_scores
    from score_semantic import semantic_scores
    from skills import find_skills
    
    jd, resumes = load_jd(), load_resumes()
    jd_skills = find_skills(jd)
    tf, sem = tfidf_scores(jd, resumes), semantic_scores(jd, resumes)
    
    rows = []
    for name, text in resumes.items():
        have = find_skills(text) & jd_skills
        rows.append({"resume": name, "tfidf": tf[name], "semantic": sem[name],
                     "skill_coverage": len(have) / max(len(jd_skills), 1),
                     "missing": ", ".join(sorted(jd_skills - have))})
    
    df = pd.DataFrame(rows)
    norm = lambda s: (s - s.min()) / (s.max() - s.min() + 1e-9)
    df["score"] = 0.4 * norm(df["semantic"]) + 0.3 * norm(df["tfidf"]) + 0.3 * df["skill_coverage"]
    df = df.sort_values("score", ascending=False).round(3)
    print(df.to_string(index=False))
    df.to_csv("ranking.csv", index=False)

    The weights (0.4, 0.3, 0.3) are a starting point, not a truth. Step 7 shows how to check whether they help, and your report should say how you chose them.

  7. Step 7. Evaluate the ranking on a labelled set

    Ask two people (a senior, a placement coordinator or a friend in HR) to read your sample resumes against the job description and mark each one shortlist = 1 or 0. Save their decisions in labels.csv with columns resume and shortlist.

    # evaluate.py
    import pandas as pd
    from sklearn.metrics import roc_auc_score
    
    df = pd.read_csv("ranking.csv").merge(pd.read_csv("labels.csv"), on="resume")
    
    for col in ["tfidf", "semantic", "skill_coverage", "score"]:
        auc = roc_auc_score(df["shortlist"], df[col])
        top5 = int(df.nlargest(5, col)["shortlist"].sum())
        print(f"{col:15s} AUC {auc:.2f}   human-shortlisted in top 5: {top5}/5")

    AUC answers "how often does the method rank a shortlisted resume above a rejected one?" (1.0 is perfect, 0.5 is random). Top-5 hits answer the recruiter's real question. If the two humans disagree on some resumes, report that too: it shows the task itself is partly subjective.

  8. Step 8. Run a simple fairness check

    A counterfactual test changes only a detail that should not matter and checks whether the score moves. Take one resume, create copies that differ only in a gender cue, and score all of them.

    # fairness_check.py
    from extract import load_resumes, load_jd
    from score_tfidf import tfidf_scores
    from score_semantic import semantic_scores
    
    jd, resumes = load_jd(), load_resumes()
    base = resumes[sorted(resumes)[0]]  # pick any resume
    
    variants = {
        "original": base,
        "women's club": base + " Captain of the women's coding club.",
        "men's club": base + " Captain of the men's coding club.",
    }
    for name, scores in [("tfidf", tfidf_scores(jd, variants)),
                         ("semantic", semantic_scores(jd, variants))]:
        print(name, {k: round(v, 4) for k, v in scores.items()})

    The two club variants should score the same. If they do not, your screener reacts to gender cues. Repeat with different names, college names and home towns, and report what you find, including the cases where scores did move.

Is AI resume screening biased?

AI resume screening can be biased, and the risk is real, not theoretical. In 2018 the BBC, citing a Reuters report, described how Amazon abandoned an experimental recruiting tool after it learned to penalise CVs that included the word "women". The tool was trained on a decade of past applications that came mostly from men, so it copied that pattern. Read more on AI bias.

Your project is less risky because it does not learn from past hiring decisions, but bias can still enter:

  • Through the job description: wording like "young and dynamic" or a fixed list of colleges carries bias straight into the scores.
  • Through the embedding model: it learned from web text, which contains social biases.
  • Through formatting: scanned or two-column PDFs extract badly, which quietly penalises candidates for layout, not skill.
  • Through proxies: college name, home town and gaps in employment can stand in for caste, region, gender or disability.

What to do in your project: redact names, photos, emails and phone numbers before scoring; run the Step 8 counterfactual test on several attributes; show the matched and missing skills so humans can see the reason behind each score; and state clearly that the tool ranks, a human decides. Some places now regulate this: New York City's Local Law 144 requires a bias audit before employers use automated employment decision tools.

How can you upgrade an AI resume screening project?

Pick one or two upgrades that suit your branch and time:

  • Section-aware parsing: split resumes into education, experience, projects and skills, then weight experience matches higher.
  • Better skill extraction: use spaCy's PhraseMatcher or a named entity model instead of regex.
  • Re-ranking with an LLM: send the top 10 resumes and the job description to an LLM and ask for a short, cited justification for each. Keep the human in charge.
  • Recruiter dashboard: a Streamlit app with upload, ranking table and a "why" panel per candidate.
  • Candidate-side tool: flip the project so a student uploads their resume and a job description and sees missing skills before applying.

If you add an LLM step, read up on AI hallucination first: a model can invent skills a candidate never listed.

How do you explain a resume screening project in a viva or interview?

Open with: "My system ranks PDF resumes against a job description using TF-IDF similarity, embedding-based requirement coverage and skill matching, evaluated against human shortlist decisions, with a counterfactual fairness check."

Then prepare for:

  • Why use both TF-IDF and embeddings? (Exact terms versus meaning.)
  • Why split the resume into chunks? (The embedding model's input limit.)
  • How did you pick the weights, and what was the AUC of each signal?
  • How do you know the tool is fair? (Show the Step 8 results honestly.)
  • Would you let this reject candidates automatically? (The right answer is no.)

Our data scientist interview questions cover the evaluation metrics behind these answers.

Frequently asked questions

Which algorithm is used in AI resume screening?

Most student and many commercial screeners combine keyword matching with TF-IDF and cosine similarity, semantic matching with sentence embeddings, and rule-based skill extraction. Some add a classifier trained on past hiring decisions, but that approach copies historical bias, so it needs careful auditing before anyone relies on it.

Is an AI resume screening system a good final-year project?

Yes, if it goes beyond a single similarity score. Compare two or three matching methods, evaluate them against human shortlist labels, show matched and missing skills, and include a fairness test. Those parts give you real results to present and plenty to discuss in the viva.

Where can I get a dataset for a resume screening project?

Kaggle hosts several public resume datasets; check each one's licence and description before use. Many students also write 15 to 20 sample resumes with fake names, which gives full control over the test cases. Never collect classmates' real resumes without clear consent.

Can AI resume screening be biased against candidates?

Yes. Tools trained on past hiring data can learn past discrimination, as Amazon's abandoned experimental tool showed. Even similarity-based screeners can be skewed by biased job descriptions, embedding models or poor PDF extraction. Redacting personal details, counterfactual testing and keeping a human decision-maker reduce the risk.

How do I make my resume pass AI resume screening?

Use a text-based PDF, not a scanned image, with a simple single-column layout. Mirror the exact skill names from the job description where they truly apply, such as SQL or Power BI, and describe projects with concrete tools and results. Building this project shows you exactly what screeners look for.

References