Repository navigation
Expand file tree
/
Copy pathscorer.py
More file actions
418 lines (339 loc) · 15.9 KB
/
Copy pathscorer.py
File metadata and controls
418 lines (339 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
"""
scorer.py — ATS Resume Scorer
==============================
Inputs : parsed_resume (dict from resume_parser.parse_resume)
job_description (str)
Output : dict → total_score, breakdown, matched_skills, missing_skills, recommendations
Scoring breakdown (100 pts total):
40 Skills Match — Jaccard-weighted keyword overlap on shared skill vocabulary
10 Hot Skills Bonus — In-demand skill coverage from JD
20 Experience — Sentence-transformer cosine similarity (resume exp vs JD)
10 Education — Rule-based degree + field matching
10 Projects Relevance — Sentence-transformer cosine similarity (projects vs JD)
10 Completeness — Section presence check
"""
import os
import re
import sys
import math
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity as sk_cosine
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
from skill_loader import load_skill_set
# ── Load skill knowledge base once ───────────────────────────────────────────
SKILLS, HOT_SKILLS = load_skill_set(
os.path.join(_HERE, "skills_dataset", "skills.csv"),
os.path.join(_HERE, "skills_dataset", "Technology Skills.xlsx"),
)
# ── Lazy-load sentence-transformer (heavy, load once) ────────────────────────
_EMBED_MODEL = None
def _get_embed_model():
global _EMBED_MODEL
if _EMBED_MODEL is None:
from sentence_transformers import SentenceTransformer
_EMBED_MODEL = SentenceTransformer("all-MiniLM-L6-v2")
return _EMBED_MODEL
# ─────────────────────────────────────────────────────────────────────────────
# UTILITY FUNCTIONS
# ─────────────────────────────────────────────────────────────────────────────
def _extract_skills_from_text(text: str) -> set:
"""Match all known skills against a block of text using word-boundary regex."""
text_lower = text.lower()
found = set()
for skill in SKILLS:
if re.search(r'\b' + re.escape(skill) + r'\b', text_lower):
found.add(skill)
return found
def _semantic_similarity(text_a: str, text_b: str) -> float:
"""
Cosine similarity between two texts using sentence-transformers.
Returns float in [0, 1]. Returns 0 if either text is empty.
"""
if not text_a.strip() or not text_b.strip():
return 0.0
model = _get_embed_model()
embeddings = model.encode([text_a, text_b], convert_to_numpy=True)
sim = sk_cosine([embeddings[0]], [embeddings[1]])[0][0]
return float(np.clip(sim, 0.0, 1.0))
# Degree keyword buckets for rule-based education scoring
_DEGREE_LEVELS = {
"bachelors": ["b.tech", "b.e", "b.sc", "bachelor", "btech", "be,", "undergraduate"],
"masters": ["m.tech", "m.sc", "m.s", "master", "mtech", "postgraduate"],
"phd": ["phd", "ph.d", "doctorate"],
}
_CS_FIELDS = [
"computer science", "information technology", "software engineering",
"electronics", "electrical", "cse", "ece", "ise", "it,", "cs,",
]
# ─────────────────────────────────────────────────────────────────────────────
# COMPONENT SCORERS
# ─────────────────────────────────────────────────────────────────────────────
def _score_skills(resume: dict, jd_text: str) -> tuple:
"""
Skills match: 40 pts.
Formula:
Let R = resume skills, J = JD skills
matched = R ∩ J
score = sigmoid_scaled(|matched| / |J|) * 40
We use a soft sigmoid scaling instead of raw ratio to:
- reward partial match more fairly
- avoid cliff at 0 when JD has many niche skills
sigmoid_scaled(x) = 2 / (1 + exp(-4x)) - 1 → maps [0,1] → [0,1]
(steeper than standard sigmoid near 0, fair for sparse matches)
"""
resume_skills = set(resume.get("skills", []))
jd_skills = _extract_skills_from_text(jd_text)
if not jd_skills:
return 20.0, resume_skills, set(), jd_skills # neutral
matched = resume_skills & jd_skills
missing = jd_skills - resume_skills
ratio = len(matched) / len(jd_skills)
# Sigmoid scaling: smoother reward curve
scaled = 2.0 / (1.0 + math.exp(-4.0 * ratio)) - 1.0
score = round(min(scaled * 40.0, 40.0), 2)
return score, matched, missing, jd_skills
def _score_hot_skills(resume: dict, jd_skills: set) -> float:
"""
Hot skills bonus: 10 pts.
hot_in_jd = JD skills that are also "hot" (in-demand)
hot_resume = resume hot skills
matched_hot = hot_resume ∩ hot_in_jd
If JD has no hot skills → neutral score 5.
Else: score = (|matched_hot| / |hot_in_jd|) * 10
"""
hot_in_jd = jd_skills & HOT_SKILLS
resume_hot = set(resume.get("hot_skills", []))
hot_matched = resume_hot & hot_in_jd
if not hot_in_jd:
return 5.0 # JD doesn't specify hot skills → neutral
ratio = len(hot_matched) / len(hot_in_jd)
return round(min(ratio * 10.0, 10.0), 2)
def _score_experience(resume: dict, jd_text: str) -> float:
"""
Experience relevance: 20 pts.
Semantic cosine similarity between resume experience/projects text and JD.
For students who lack formal experience, projects are used as fallback.
score = sim * 20
Bonus: +2 if years/duration mentions found in experience (shows depth)
"""
raw = resume.get("raw_sections", {})
exp_text = raw.get("experience", "").strip()
# Student fallback: no experience → use projects
if not exp_text:
exp_text = raw.get("projects", "").strip()
sim = _semantic_similarity(exp_text, jd_text)
score = sim * 20.0
# Small bonus for quantified experience (numbers/years/metrics)
if exp_text and re.search(r'\d+\s*(year|month|%|users|ms|x\b)', exp_text, re.I):
score = min(score + 1.5, 20.0)
return round(score, 2)
def _score_education(resume: dict, jd_text: str) -> tuple:
"""
Education: 10 pts. Rule-based.
Points breakdown:
4 pts — has an education section at all
3 pts — degree level mentioned in resume matches JD expectation
3 pts — CS/Engineering field detected in resume
"""
raw = resume.get("raw_sections", {})
edu_text = raw.get("education", "").lower().strip()
jd_lower = jd_text.lower()
if not edu_text:
return 0.0, "No education section"
score = 4.0 # base for having education section
notes = []
# Degree level match (3 pts)
degree_score = 0
for level, kws in _DEGREE_LEVELS.items():
in_resume = any(kw in edu_text for kw in kws)
in_jd = any(kw in jd_lower for kw in kws)
if in_resume and in_jd:
degree_score = 3
notes.append(f"degree level match ({level})")
break
elif in_resume:
degree_score = 2 # has degree, JD doesn't restrict
notes.append("has degree (JD has no level requirement)")
break
# CS field match (3 pts)
field_score = 0
resume_has_cs = any(kw in edu_text for kw in _CS_FIELDS)
jd_wants_cs = any(kw in jd_lower for kw in _CS_FIELDS)
if resume_has_cs:
field_score = 3
notes.append("CS/engineering field detected")
elif jd_wants_cs and not resume_has_cs:
notes.append("JD prefers CS field — not found in resume")
total = min(score + degree_score + field_score, 10.0)
return round(total, 2), "; ".join(notes) or "education present"
def _score_projects(resume: dict, jd_text: str) -> float:
"""
Projects relevance: 10 pts.
Cosine similarity between projects section and JD.
"""
proj_text = resume.get("raw_sections", {}).get("projects", "").strip()
sim = _semantic_similarity(proj_text, jd_text)
return round(min(sim * 10.0, 10.0), 2)
def _score_completeness(resume: dict) -> tuple:
"""
Completeness: 10 pts. 2 pts per required section.
Required: skills, experience OR projects, education, contact email, summary/objective
"""
sections = set(resume.get("sections_detected", []))
checks = {
"skills": bool(resume.get("skills")),
"experience/projects": ("experience" in sections or "projects" in sections),
"education": "education" in sections,
"contact": bool(resume.get("contact", {}).get("email")),
"summary": ("summary" in sections or "header" in sections),
}
missing = [k for k, ok in checks.items() if not ok]
score = sum(2 for ok in checks.values() if ok)
return float(score), missing
# ─────────────────────────────────────────────────────────────────────────────
# RECOMMENDATIONS ENGINE
# ─────────────────────────────────────────────────────────────────────────────
def _build_recommendations(
skills_score, hot_score, exp_score, edu_score, proj_score, comp_score,
matched_skills, missing_skills, jd_skills, missing_sections,
resume: dict, jd_text: str
) -> list:
recs = []
# Skills
if skills_score < 16: # < 40% of 40
top_missing = sorted(missing_skills)[:8]
recs.append(
f"Critical: Only {len(matched_skills)}/{len(jd_skills)} JD skills matched. "
f"Add these skills to your resume: {', '.join(top_missing)}."
)
elif skills_score < 28: # < 70% of 40
top_missing = sorted(missing_skills)[:5]
if top_missing:
recs.append(
f"Moderate skills match. Consider adding: {', '.join(top_missing)}."
)
# Hot skills
hot_in_jd = jd_skills & HOT_SKILLS
resume_hot = set(resume.get("hot_skills", []))
hot_missing = hot_in_jd - resume_hot
if hot_score < 5 and hot_in_jd:
recs.append(
f"Missing in-demand skills that JD wants: {', '.join(sorted(hot_missing)[:6])}. "
"These carry extra weight in ATS ranking."
)
elif hot_score == 5.0 and not hot_in_jd:
recs.append(
"JD has no specific hot-skill requirement, but adding trending skills "
f"(e.g., {', '.join(sorted(HOT_SKILLS)[:5])}) boosts general ATS rank."
)
# Experience
if exp_score < 8:
recs.append(
"Experience section scores low. Quantify impact: use numbers, %, durations. "
"Mirror JD keywords in your experience bullet points."
)
elif exp_score < 14:
recs.append(
"Experience partially matches JD. Add more role-specific keywords from the JD."
)
# Education
if edu_score < 5:
recs.append(
"Education section is weak or missing. Add your degree, institution, GPA, "
"and relevant coursework."
)
# Projects
if proj_score < 4:
jd_tech = sorted(jd_skills & SKILLS)[:6]
recs.append(
f"Projects don't align with JD. Build or highlight projects that use: "
f"{', '.join(jd_tech if jd_tech else ['technologies mentioned in JD'])}."
)
# Completeness
if comp_score < 8:
recs.append(
f"Resume missing sections: {', '.join(missing_sections)}. "
"Add them — ATS systems penalise incomplete resumes."
)
if not recs:
recs.append(
"Strong match! Tailor your summary/objective directly to this JD for max impact."
)
return recs
# ─────────────────────────────────────────────────────────────────────────────
# MAIN SCORER
# ─────────────────────────────────────────────────────────────────────────────
def score(parsed_resume: dict, job_description: str) -> dict:
"""
ATS scoring entry point.
Parameters
----------
parsed_resume : dict — output of resume_parser.parse_resume()
job_description : str — raw JD text
Returns
-------
dict with keys:
total_score : float (0–100)
breakdown : dict (per-category scores)
matched_skills : list
missing_skills : list (in JD, not in resume)
jd_skills_found : list (all skills extracted from JD)
recommendations : list[str]
"""
jd = job_description.strip()
# Component scores
skills_score, matched, missing, jd_skills = _score_skills(parsed_resume, jd)
hot_score = _score_hot_skills(parsed_resume, jd_skills)
exp_score = _score_experience(parsed_resume, jd)
edu_score, _ = _score_education(parsed_resume, jd)
proj_score = _score_projects(parsed_resume, jd)
comp_score, missing_sections = _score_completeness(parsed_resume)
total = round(
min(skills_score + hot_score + exp_score + edu_score + proj_score + comp_score, 100.0),
1
)
breakdown = {
"skills_match (40)": skills_score,
"hot_skills_bonus (10)": hot_score,
"experience (20)": exp_score,
"education (10)": edu_score,
"projects (10)": proj_score,
"completeness (10)": comp_score,
}
recommendations = _build_recommendations(
skills_score, hot_score, exp_score, edu_score, proj_score, comp_score,
matched, missing, jd_skills, missing_sections,
parsed_resume, jd
)
return {
"total_score": total,
"breakdown": breakdown,
"matched_skills": sorted(matched),
"missing_skills": sorted(missing),
"jd_skills_found": sorted(jd_skills),
"recommendations": recommendations,
}
# ─────────────────────────────────────────────────────────────────────────────
# TEST / DEMO
# ─────────────────────────────────────────────────────────────────────────────
SAMPLE_JD = """
Software Engineering Intern — Backend & ML Platform
We are looking for a motivated software engineering intern to join our backend
and machine learning platform team.
Responsibilities:
- Design and implement RESTful APIs using Python (FastAPI or Django).
- Work with relational databases (PostgreSQL, MySQL) and write optimised SQL queries.
- Contribute to ML pipelines: data preprocessing, model training, evaluation.
- Containerise services using Docker and deploy on Kubernetes or AWS.
- Write unit tests and participate in code reviews via GitHub.
- Collaborate using Git for version control in an Agile environment.
Requirements:
- Pursuing a B.Tech / B.E. / B.Sc. in Computer Science, IT, or related field.
- Strong programming in Python and Java.
- Familiarity with SQL and NoSQL databases (PostgreSQL, MongoDB).
- Understanding of REST APIs and HTTP protocols.
- Experience with Git, GitHub.
- Bonus: Docker, Kubernetes, scikit-learn, PyTorch, NLP, computer vision.
- Good problem-solving and communication skills.
"""