|
|
|
from typing import Dict, Any, List, Tuple |
|
import re |
|
from collections import Counter |
|
|
|
class JobMatcher: |
|
"""Utility class for matching LinkedIn profiles with job descriptions""" |
|
|
|
def __init__(self): |
|
self.weight_config = { |
|
'skills': 0.4, |
|
'experience': 0.3, |
|
'keywords': 0.2, |
|
'education': 0.1 |
|
} |
|
|
|
self.skill_synonyms = { |
|
'javascript': ['js', 'ecmascript', 'node.js', 'nodejs'], |
|
'python': ['py', 'django', 'flask', 'fastapi'], |
|
'react': ['reactjs', 'react.js'], |
|
'angular': ['angularjs', 'angular.js'], |
|
'machine learning': ['ml', 'ai', 'artificial intelligence'], |
|
'database': ['db', 'sql', 'mysql', 'postgresql', 'mongodb'] |
|
} |
|
|
|
def calculate_match_score(self, profile_data: Dict[str, Any], job_description: str) -> Dict[str, Any]: |
|
""" |
|
Calculate comprehensive match score between profile and job |
|
|
|
Args: |
|
profile_data (Dict[str, Any]): Cleaned profile data |
|
job_description (str): Job description text |
|
|
|
Returns: |
|
Dict[str, Any]: Match analysis with scores and details |
|
""" |
|
job_requirements = self._parse_job_requirements(job_description) |
|
|
|
|
|
skills_score = self._calculate_skills_match( |
|
profile_data.get('skills', []), |
|
job_requirements['skills'] |
|
) |
|
|
|
experience_score = self._calculate_experience_match( |
|
profile_data.get('experience', []), |
|
job_requirements |
|
) |
|
|
|
keywords_score = self._calculate_keywords_match( |
|
profile_data, |
|
job_requirements['keywords'] |
|
) |
|
|
|
education_score = self._calculate_education_match( |
|
profile_data.get('education', []), |
|
job_requirements |
|
) |
|
|
|
|
|
overall_score = ( |
|
skills_score['score'] * self.weight_config['skills'] + |
|
experience_score['score'] * self.weight_config['experience'] + |
|
keywords_score['score'] * self.weight_config['keywords'] + |
|
education_score['score'] * self.weight_config['education'] |
|
) |
|
|
|
return { |
|
'overall_score': round(overall_score, 2), |
|
'breakdown': { |
|
'skills': skills_score, |
|
'experience': experience_score, |
|
'keywords': keywords_score, |
|
'education': education_score |
|
}, |
|
'recommendations': self._generate_match_recommendations( |
|
skills_score, experience_score, keywords_score, education_score |
|
), |
|
'job_requirements': job_requirements |
|
} |
|
|
|
def find_skill_gaps(self, profile_skills: List[str], job_requirements: List[str]) -> Dict[str, List[str]]: |
|
""" |
|
Identify skill gaps between profile and job requirements |
|
|
|
Args: |
|
profile_skills (List[str]): Current profile skills |
|
job_requirements (List[str]): Required job skills |
|
|
|
Returns: |
|
Dict[str, List[str]]: Missing and matching skills |
|
""" |
|
profile_skills_lower = [skill.lower() for skill in profile_skills] |
|
job_skills_lower = [skill.lower() for skill in job_requirements] |
|
|
|
|
|
matching_skills = [] |
|
missing_skills = [] |
|
|
|
for job_skill in job_skills_lower: |
|
if job_skill in profile_skills_lower: |
|
matching_skills.append(job_skill) |
|
else: |
|
|
|
found_synonym = False |
|
for profile_skill in profile_skills_lower: |
|
if self._are_skills_similar(profile_skill, job_skill): |
|
matching_skills.append(job_skill) |
|
found_synonym = True |
|
break |
|
|
|
if not found_synonym: |
|
missing_skills.append(job_skill) |
|
|
|
return { |
|
'matching_skills': matching_skills, |
|
'missing_skills': missing_skills, |
|
'match_percentage': len(matching_skills) / max(len(job_skills_lower), 1) * 100 |
|
} |
|
|
|
def suggest_profile_improvements(self, match_analysis: Dict[str, Any]) -> List[str]: |
|
""" |
|
Generate specific improvement suggestions based on match analysis |
|
|
|
Args: |
|
match_analysis (Dict[str, Any]): Match analysis results |
|
|
|
Returns: |
|
List[str]: Improvement suggestions |
|
""" |
|
suggestions = [] |
|
breakdown = match_analysis['breakdown'] |
|
|
|
|
|
if breakdown['skills']['score'] < 70: |
|
missing_skills = breakdown['skills']['details']['missing_skills'][:3] |
|
if missing_skills: |
|
suggestions.append( |
|
f"Add these high-priority skills: {', '.join(missing_skills)}" |
|
) |
|
|
|
|
|
if breakdown['experience']['score'] < 60: |
|
suggestions.append( |
|
"Highlight more relevant experience in your current/previous roles" |
|
) |
|
suggestions.append( |
|
"Add quantified achievements that demonstrate impact" |
|
) |
|
|
|
|
|
if breakdown['keywords']['score'] < 50: |
|
suggestions.append( |
|
"Incorporate more industry-specific keywords throughout your profile" |
|
) |
|
|
|
|
|
if breakdown['education']['score'] < 40: |
|
suggestions.append( |
|
"Consider adding relevant certifications or courses" |
|
) |
|
|
|
return suggestions |
|
|
|
def _parse_job_requirements(self, job_description: str) -> Dict[str, Any]: |
|
"""Parse job description to extract requirements""" |
|
requirements = { |
|
'skills': [], |
|
'keywords': [], |
|
'experience_years': 0, |
|
'education_level': '', |
|
'industry': '', |
|
'role_type': '' |
|
} |
|
|
|
|
|
skill_patterns = [ |
|
r'\b(python|javascript|java|react|angular|node\.?js|sql|aws|docker|kubernetes)\b', |
|
r'\b(machine learning|ai|data science|devops|full.?stack)\b', |
|
r'\b(project management|agile|scrum|leadership)\b' |
|
] |
|
|
|
for pattern in skill_patterns: |
|
matches = re.findall(pattern, job_description, re.IGNORECASE) |
|
requirements['skills'].extend([match.lower() for match in matches]) |
|
|
|
|
|
exp_pattern = r'(\d+)\+?\s*years?\s*(?:of\s*)?experience' |
|
exp_matches = re.findall(exp_pattern, job_description, re.IGNORECASE) |
|
if exp_matches: |
|
requirements['experience_years'] = int(exp_matches[0]) |
|
|
|
|
|
keywords = re.findall(r'\b[a-zA-Z]{3,}\b', job_description) |
|
stop_words = {'the', 'and', 'for', 'with', 'you', 'will', 'are', 'have'} |
|
requirements['keywords'] = [ |
|
word.lower() for word in keywords |
|
if word.lower() not in stop_words |
|
] |
|
|
|
|
|
requirements['skills'] = list(set(requirements['skills'])) |
|
requirements['keywords'] = list(set(requirements['keywords'])) |
|
|
|
return requirements |
|
|
|
def _calculate_skills_match(self, profile_skills: List[str], job_skills: List[str]) -> Dict[str, Any]: |
|
"""Calculate skills match score""" |
|
if not job_skills: |
|
return {'score': 100, 'details': {'matching_skills': [], 'missing_skills': []}} |
|
|
|
skill_gap_analysis = self.find_skill_gaps(profile_skills, job_skills) |
|
|
|
return { |
|
'score': skill_gap_analysis['match_percentage'], |
|
'details': skill_gap_analysis |
|
} |
|
|
|
def _calculate_experience_match(self, profile_experience: List[Dict], job_requirements: Dict) -> Dict[str, Any]: |
|
"""Calculate experience match score""" |
|
score = 0 |
|
details = { |
|
'relevant_roles': 0, |
|
'total_experience': 0, |
|
'required_experience': job_requirements.get('experience_years', 0) |
|
} |
|
|
|
|
|
total_years = 0 |
|
relevant_roles = 0 |
|
|
|
for exp in profile_experience: |
|
duration_info = exp.get('duration_info', {}) |
|
if duration_info.get('duration_months'): |
|
total_years += duration_info['duration_months'] / 12 |
|
|
|
|
|
role_text = f"{exp.get('title', '')} {exp.get('description', '')}".lower() |
|
job_keywords = job_requirements.get('keywords', []) |
|
|
|
if any(keyword in role_text for keyword in job_keywords[:10]): |
|
relevant_roles += 1 |
|
|
|
details['total_experience'] = round(total_years, 1) |
|
details['relevant_roles'] = relevant_roles |
|
|
|
|
|
if job_requirements.get('experience_years', 0) > 0: |
|
exp_ratio = min(total_years / job_requirements['experience_years'], 1.0) |
|
score = exp_ratio * 70 + (relevant_roles / max(len(profile_experience), 1)) * 30 |
|
else: |
|
score = 80 |
|
|
|
return { |
|
'score': round(score, 2), |
|
'details': details |
|
} |
|
|
|
def _calculate_keywords_match(self, profile_data: Dict, job_keywords: List[str]) -> Dict[str, Any]: |
|
"""Calculate keywords match score""" |
|
if not job_keywords: |
|
return {'score': 100, 'details': {'matched': 0, 'total': 0}} |
|
|
|
|
|
profile_text = "" |
|
for key, value in profile_data.items(): |
|
if isinstance(value, str): |
|
profile_text += f" {value}" |
|
elif isinstance(value, list): |
|
for item in value: |
|
if isinstance(item, dict): |
|
profile_text += f" {' '.join(str(v) for v in item.values())}" |
|
else: |
|
profile_text += f" {item}" |
|
|
|
profile_text = profile_text.lower() |
|
|
|
|
|
matched_keywords = 0 |
|
for keyword in job_keywords: |
|
if keyword.lower() in profile_text: |
|
matched_keywords += 1 |
|
|
|
score = (matched_keywords / len(job_keywords)) * 100 |
|
|
|
return { |
|
'score': round(score, 2), |
|
'details': { |
|
'matched': matched_keywords, |
|
'total': len(job_keywords), |
|
'percentage': round(score, 2) |
|
} |
|
} |
|
|
|
def _calculate_education_match(self, profile_education: List[Dict], job_requirements: Dict) -> Dict[str, Any]: |
|
"""Calculate education match score""" |
|
score = 70 |
|
details = { |
|
'has_degree': len(profile_education) > 0, |
|
'degree_count': len(profile_education) |
|
} |
|
|
|
if profile_education: |
|
score = 85 |
|
|
|
|
|
job_keywords = job_requirements.get('keywords', []) |
|
for edu in profile_education: |
|
edu_text = f"{edu.get('degree', '')} {edu.get('field', '')}".lower() |
|
if any(keyword in edu_text for keyword in job_keywords[:5]): |
|
score = 95 |
|
break |
|
|
|
return { |
|
'score': score, |
|
'details': details |
|
} |
|
|
|
def _are_skills_similar(self, skill1: str, skill2: str) -> bool: |
|
"""Check if two skills are similar using synonyms""" |
|
skill1_lower = skill1.lower() |
|
skill2_lower = skill2.lower() |
|
|
|
|
|
for main_skill, synonyms in self.skill_synonyms.items(): |
|
if ((skill1_lower == main_skill or skill1_lower in synonyms) and |
|
(skill2_lower == main_skill or skill2_lower in synonyms)): |
|
return True |
|
|
|
|
|
if skill1_lower in skill2_lower or skill2_lower in skill1_lower: |
|
return True |
|
|
|
return False |
|
|
|
def _generate_match_recommendations(self, skills_score: Dict, experience_score: Dict, |
|
keywords_score: Dict, education_score: Dict) -> List[str]: |
|
"""Generate recommendations based on individual scores""" |
|
recommendations = [] |
|
|
|
if skills_score['score'] < 60: |
|
recommendations.append("Focus on developing missing technical skills") |
|
|
|
if experience_score['score'] < 50: |
|
recommendations.append("Highlight more relevant work experience") |
|
|
|
if keywords_score['score'] < 40: |
|
recommendations.append("Optimize profile with job-specific keywords") |
|
|
|
if education_score['score'] < 60: |
|
recommendations.append("Consider additional certifications or training") |
|
|
|
return recommendations |
|
|