| from rapidfuzz import fuzz |
|
|
| def fuzzy_match_skill(cand_skill_name: str, jd_skills: list[str]) -> str | None: |
| """Finds the best matching JD skill for a candidate's skill using a threshold of 85. |
| |
| Returns the JD skill name if matched, else None. |
| """ |
| if not cand_skill_name or not jd_skills: |
| return None |
| best_ratio = 0 |
| best_skill = None |
| for jd_s in jd_skills: |
| ratio = fuzz.token_sort_ratio(cand_skill_name.lower().strip(), jd_s.lower().strip()) |
| if ratio > best_ratio: |
| best_ratio = ratio |
| best_skill = jd_s |
| if best_ratio >= 85: |
| return best_skill |
| return None |
|
|
| def skill_trust(skill_obj, assess_scores: dict, jd_skill: str) -> float: |
| """Calculates the trust score for a matched skill. |
| |
| Formula: trust = prof_weight * (0.35 + 0.25 * endorse_w + 0.25 * assess_w + 0.15 * duration_w) |
| """ |
| if isinstance(skill_obj, str): |
| name = skill_obj |
| proficiency = "intermediate" |
| endorsements = 0 |
| duration_months = 0 |
| elif isinstance(skill_obj, dict): |
| name = skill_obj.get("name") or "" |
| proficiency = skill_obj.get("proficiency") or "intermediate" |
| endorsements = skill_obj.get("endorsements") or 0 |
| duration_months = skill_obj.get("duration_months") or 0 |
| else: |
| return 0.0 |
| |
| |
| prof = str(proficiency).lower().strip() |
| if prof == "beginner": |
| prof_weight = 0.40 |
| elif prof == "intermediate": |
| prof_weight = 0.70 |
| elif prof == "advanced": |
| prof_weight = 0.90 |
| elif prof in ["expert", "master"]: |
| prof_weight = 1.00 |
| else: |
| prof_weight = 0.70 |
| |
| |
| endorse_w = min(float(endorsements) / 20.0, 1.0) |
| |
| |
| assess_val = 0.0 |
| if isinstance(assess_scores, dict): |
| |
| |
| assess_val = assess_scores.get(jd_skill) or assess_scores.get(name) |
| if assess_val is None: |
| |
| lower_scores = {k.lower(): v for k, v in assess_scores.items()} |
| assess_val = lower_scores.get(jd_skill.lower()) or lower_scores.get(name.lower()) or 0.0 |
| assess_w = float(assess_val) / 100.0 |
| |
| |
| duration_w = min(float(duration_months) / 24.0, 1.0) |
| |
| trust = prof_weight * (0.35 + 0.25 * endorse_w + 0.25 * assess_w + 0.15 * duration_w) |
| return float(trust) |
|
|
| def compute_B(candidate: dict, jd: dict) -> dict: |
| """Computes the skill trust score (B) for a candidate, supporting nested and flat structures.""" |
| must_have_skills = jd.get("must_have_skills") or [] |
| nice_to_have_skills = jd.get("nice_to_have_skills") or [] |
| |
| must_trust = {s: 0.0 for s in must_have_skills} |
| nice_trust = {s: 0.0 for s in nice_to_have_skills} |
| |
| cand_skills = candidate.get("skills") or [] |
| |
| |
| signals = candidate.get("redrob_signals") or {} |
| assess_scores = signals.get("skill_assessment_scores") |
| if assess_scores is None: |
| assess_scores = candidate.get("skill_assessment_scores") or {} |
| |
| |
| for s_obj in cand_skills: |
| s_name = s_obj if isinstance(s_obj, str) else s_obj.get("name", "") |
| if not s_name: |
| continue |
| |
| matched_must = fuzzy_match_skill(s_name, must_have_skills) |
| if matched_must: |
| t = skill_trust(s_obj, assess_scores, matched_must) |
| must_trust[matched_must] = max(must_trust[matched_must], t) |
| |
| matched_nice = fuzzy_match_skill(s_name, nice_to_have_skills) |
| if matched_nice: |
| t = skill_trust(s_obj, assess_scores, matched_nice) |
| nice_trust[matched_nice] = max(nice_trust[matched_nice], t) |
| |
| must_cov = sum(must_trust.values()) / len(must_have_skills) if must_have_skills else 0.0 |
| nice_cov = sum(nice_trust.values()) / len(nice_to_have_skills) if nice_to_have_skills else 0.0 |
| |
| |
| certs = candidate.get("certifications") or candidate.get("certs") or [] |
| all_jd_skills = must_have_skills + nice_to_have_skills |
| cert_matches = 0 |
| for cert in certs: |
| cert_name = cert if isinstance(cert, str) else cert.get("name", "") |
| if not cert_name: |
| continue |
| |
| |
| matched = False |
| for jd_s in all_jd_skills: |
| if fuzz.token_sort_ratio(cert_name.lower().strip(), jd_s.lower().strip()) >= 85: |
| matched = True |
| break |
| if jd_s.lower().strip() in cert_name.lower(): |
| matched = True |
| break |
| if matched: |
| cert_matches += 1 |
| |
| cert_bonus = min(cert_matches * 0.05, 0.15) |
| |
| B = min(0.75 * must_cov + 0.25 * nice_cov + cert_bonus, 1.0) |
| |
| return { |
| "B": round(B, 4), |
| "must_have_coverage": round(must_cov, 4), |
| "nice_coverage": round(nice_cov, 4), |
| "cert_bonus": round(cert_bonus, 4) |
| } |
|
|