Ai-Resume-Ranking / core /scoring.py
yhng2525's picture
Upload 15 files
1595f22 verified
# %%writefile core/scoring.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Any, List
def _safe_len(x) -> int:
return len(x) if isinstance(x, list) else 0
def normalize_weights(weights: Dict[str, Any]) -> Dict[str, float]:
w_must = float(weights.get("must_have", 0))
w_nice = float(weights.get("nice_to_have", 0))
w_exp = float(weights.get("experience", 0))
w_soft = float(weights.get("soft_skills", 0))
total = w_must + w_nice + w_exp + w_soft
if total <= 0:
return {"must_have": 0.6, "nice_to_have": 0.25, "experience": 0.15, "soft_skills": 0.0}
return {
"must_have": w_must / total,
"nice_to_have": w_nice / total,
"experience": w_exp / total,
"soft_skills": w_soft / total
}
def compute_coverage(matched: List[Any], missing: List[Any], partial: List[Any] | None = None) -> float:
m = _safe_len(matched)
miss = _safe_len(missing)
p = _safe_len(partial) if partial is not None else 0
denom = m + miss + p
if denom == 0:
return 0.0
return (m + 0.5 * p) / denom
def experience_score(assessment: str) -> float:
assessment = (assessment or "").strip().lower()
if assessment == "below":
return 0.0
if assessment in ("meets", "exceeds"):
return 1.0
return 0.0
def clamp(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x))
def score_candidate(jd_rubric: Dict[str, Any], match_summary: Dict[str, Any]) -> Dict[str, Any]:
weights = normalize_weights(jd_rubric.get("recommended_weights", {}))
mh = match_summary.get("must_have_match", {}) or {}
nh = match_summary.get("nice_to_have_match", {}) or {}
exp = match_summary.get("experience_analysis", {}) or {}
must_cov = compute_coverage(
matched=mh.get("matched", []),
missing=mh.get("missing", []),
partial=mh.get("partial", [])
)
nice_cov = compute_coverage(
matched=nh.get("matched", []),
missing=nh.get("missing", []),
partial=None
)
exp_sc = experience_score(exp.get("assessment", ""))
# Soft skills scoring (optional). Keep 0 for student MVP unless you add logic later.
soft_sc = 0.0
base_total = (
weights["must_have"] * must_cov +
weights["nice_to_have"] * nice_cov +
weights["experience"] * exp_sc +
weights["soft_skills"] * soft_sc
) * 100.0
# ✅ Bonus/Penalty based on Step-4 indicators (transparent & capped)
positives = match_summary.get("positive_indicators", []) or []
negatives = match_summary.get("negative_indicators", []) or []
bonus_per_positive = 1.0
penalty_per_negative = 1.5
raw_adjustment = (len(positives) * bonus_per_positive) - (len(negatives) * penalty_per_negative)
# Cap adjustment so it doesn't dominate scoring
adjustment_cap = 8.0
adjustment = clamp(raw_adjustment, -adjustment_cap, adjustment_cap)
final_total = clamp(base_total + adjustment, 0.0, 100.0)
return {
"candidate_name": match_summary.get("candidate_name", ""),
"total_score": round(final_total, 2),
"breakdown": {
"base_score": round(base_total, 2),
"bonus_penalty_adjustment": round(adjustment, 2),
"positive_count": len(positives),
"negative_count": len(negatives),
"must_have_coverage": round(must_cov, 3),
"nice_to_have_coverage": round(nice_cov, 3),
"experience_score": round(exp_sc, 3),
"weights_normalized": weights
}
}