Spaces:
Sleeping
Sleeping
File size: 6,867 Bytes
91e794e | 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 | """
explainability.py
-----------------
Resume matching explainability -- identify key phrases and sections that drove the score.
Features:
- Extract high-impact phrases from resume matching JD
- Section-level importance (experience, skills, education)
- Attention-based highlighting
- SHAP-inspired local explanations (without external models)
Author: SmartHire AI
"""
import logging
import re
from typing import Dict, List, Tuple
logger = logging.getLogger(__name__)
def extract_sections(resume_text: str) -> Dict[str, str]:
"""Parse resume into common sections (Education, Experience, Skills, etc)."""
sections = {
"experience": "",
"education": "",
"skills": "",
"projects": "",
"certifications": "",
"summary": "",
"other": "",
}
# Section headers (flexible matching)
section_patterns = {
"experience": r"(?i)(professional\s+experience|work\s+experience|employment|career)",
"education": r"(?i)(education|academic|degree|university|college)",
"skills": r"(?i)(technical\s+skills|skills|competencies|expertise)",
"projects": r"(?i)(projects?|portfolio|achievements)",
"certifications": r"(?i)(certifications?|licenses|credentials)",
"summary": r"(?i)(professional\s+summary|objective|executive\s+summary|about)",
}
lines = resume_text.split('\n')
current_section = "summary"
for line in lines:
matched = False
for section_key, pattern in section_patterns.items():
if re.search(pattern, line):
current_section = section_key
matched = True
break
if matched or not line.strip():
continue
sections[current_section] += line + "\n"
return {k: v.strip() for k, v in sections.items() if v.strip()}
def extract_key_phrases(text: str, jd_text: str, top_k: int = 5) -> List[Tuple[str, float]]:
"""
Extract phrases from resume that appear in JD (TF-IDF style scoring).
Returns list of (phrase, importance_score) tuples.
"""
# Simple phrase extraction (2-4 word chunks)
phrases = []
words = re.findall(r'\w+', text.lower())
for i in range(len(words) - 1):
for j in range(i + 2, min(i + 5, len(words) + 1)):
phrase = ' '.join(words[i:j])
phrases.append(phrase)
# Score each phrase by frequency in JD
jd_lower = jd_text.lower()
scored_phrases = []
seen = set()
for phrase in set(phrases):
if phrase in seen:
continue
seen.add(phrase)
count_jd = len(re.findall(rf'\b{re.escape(phrase)}\b', jd_lower))
count_resume = len(re.findall(rf'\b{re.escape(phrase)}\b', text.lower()))
if count_jd > 0:
score = count_jd * count_resume
scored_phrases.append((phrase, score))
scored_phrases.sort(key=lambda x: x[1], reverse=True)
return scored_phrases[:top_k]
def compute_section_importance(
resume_sections: Dict[str, str],
jd_text: str,
) -> Dict[str, float]:
"""
Score each section by how much it overlaps with JD.
Returns dict of section -> importance_score (0-100).
"""
scores = {}
jd_lower = jd_text.lower()
for section_name, section_text in resume_sections.items():
if not section_text:
scores[section_name] = 0.0
continue
section_words = set(re.findall(r'\b\w+\b', section_text.lower()))
jd_words = set(re.findall(r'\b\w+\b', jd_lower))
if not section_words:
scores[section_name] = 0.0
continue
overlap = len(section_words & jd_words)
score = min(100.0, (overlap / len(section_words)) * 100)
scores[section_name] = round(score, 2)
return scores
def highlight_matching_phrases(
resume_text: str,
jd_text: str,
top_k: int = 10,
) -> Dict:
"""
Generate a comprehensive explainability report.
Returns:
{
"key_phrases": [(phrase, score), ...],
"sections": { "experience": 75.5, ... },
"highlight_text": "Resume with highlighted phrases",
"summary": "Human-readable explanation"
}
"""
sections = extract_sections(resume_text)
key_phrases = extract_key_phrases(resume_text, jd_text, top_k=top_k)
section_scores = compute_section_importance(sections, jd_text)
# Generate highlight text
highlight_text = resume_text
for phrase, score in key_phrases:
if score > 0:
highlight_text = re.sub(
rf'\b{re.escape(phrase)}\b',
f'**{phrase}**',
highlight_text,
flags=re.IGNORECASE
)
# Generate summary
top_section = max(section_scores.items(), key=lambda x: x[1]) if section_scores else ("", 0)
summary = (
f"Key drivers: {', '.join(p[0] for p in key_phrases[:3])}. "
f"Strongest section: {top_section[0]} ({top_section[1]:.0f}% alignment). "
)
return {
"key_phrases": key_phrases,
"sections": section_scores,
"highlight_text": highlight_text,
"summary": summary,
"top_section": top_section[0],
"top_section_score": top_section[1],
}
def generate_explainability_report(
candidate_name: str,
resume_text: str,
jd_text: str,
match_score: float,
skill_data: Dict,
) -> Dict:
"""
Generate a full explainability report for a candidate match.
Includes:
- Why the score is X% (key drivers)
- Which resume sections matter most
- Top matching phrases
- Recommendations for improvement
"""
explainability = highlight_matching_phrases(resume_text, jd_text, top_k=7)
# Compute improvement recommendations
improvements = []
if skill_data.get("critical_missing"):
improvements.append(
f"Learn/gain experience in: {', '.join(skill_data['critical_missing'][:2])}"
)
if skill_data.get("important_missing"):
improvements.append(
f"Strengthen: {', '.join(skill_data['important_missing'][:2])}"
)
if match_score < 70 and explainability["top_section_score"] < 50:
improvements.append("Restructure resume to emphasize relevant experience")
return {
"candidate": candidate_name,
"match_score": match_score,
"key_drivers": explainability["key_phrases"],
"section_alignment": explainability["sections"],
"strongest_section": explainability["top_section"],
"summary": explainability["summary"],
"improvements": improvements,
"highlight_text": explainability["highlight_text"],
}
|