Spaces:
Running
Running
| """ | |
| ranking.py | |
| ---------- | |
| Production candidate ranking engine. | |
| Improvements over v1: | |
| - Calibrated similarity scores (no score clustering at 70-80%) | |
| - Weighted skill scoring (critical skills count 3x) | |
| - Confidence intervals per candidate | |
| - Percentile rank across the pool | |
| - AI-generated insight text per candidate | |
| - important_missing field in CandidateResult | |
| - Full structured CandidateResult dataclass | |
| Author: SmartHire AI | |
| """ | |
| import logging | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional, Tuple | |
| import pandas as pd | |
| from src.similarity import ( | |
| calibrate_score, | |
| compute_confidence, | |
| compute_percentile_ranks, | |
| get_recommendation, | |
| similarity_to_percentage, | |
| ) | |
| from src.skills import full_skill_analysis, get_skill_weight | |
| logger = logging.getLogger(__name__) | |
| # -- CandidateResult Dataclass ---------------------------------------- | |
| class CandidateResult: | |
| """ | |
| Full result record for one candidate against one job description. | |
| """ | |
| name : str | |
| resume_text : str | |
| similarity_score : float | |
| calibrated_sim_pct : float | |
| score_pct : float | |
| recommendation : str | |
| recommendation_color : str | |
| matching_skills : List[str] = field(default_factory=list) | |
| missing_skills : List[str] = field(default_factory=list) | |
| critical_missing : List[str] = field(default_factory=list) | |
| important_missing : List[str] = field(default_factory=list) | |
| resume_only_skills : List[str] = field(default_factory=list) | |
| skill_coverage_pct : float = 0.0 | |
| weighted_coverage_pct: float = 0.0 | |
| skills_by_category : Dict[str, List[str]] = field(default_factory=dict) | |
| confidence : str = "Moderate" | |
| percentile_rank : float = 0.0 | |
| rank : int = 0 | |
| ai_insight : str = "" | |
| def to_dict(self) -> Dict: | |
| """Serialize to flat dictionary for DataFrame/CSV export.""" | |
| return { | |
| "Candidate" : self.name, | |
| "Rank" : self.rank, | |
| "Match Score (%)" : self.score_pct, | |
| "Semantic Similarity (%)" : self.calibrated_sim_pct, | |
| "Skill Coverage (%)" : self.skill_coverage_pct, | |
| "Weighted Coverage (%)" : self.weighted_coverage_pct, | |
| "Recommendation" : self.recommendation, | |
| "Confidence" : self.confidence, | |
| "Percentile Rank" : self.percentile_rank, | |
| "Matched Skills" : ", ".join(self.matching_skills) or "--", | |
| "Missing Skills" : ", ".join(self.missing_skills) or "--", | |
| "Critical Missing" : ", ".join(self.critical_missing) or "None", | |
| "AI Insight" : self.ai_insight, | |
| } | |
| # -- AI Insight Generator --------------------------------------------- | |
| def generate_insight(result_data: dict, jd_skill_count: int) -> str: | |
| """Generate a concise human-readable insight. Rule-based, no API needed.""" | |
| sim = result_data["calibrated_sim_pct"] | |
| coverage = result_data["skill_coverage_pct"] | |
| critical_miss= result_data["critical_missing"] | |
| important_miss=result_data["important_missing"] | |
| resume_extra = result_data["resume_only"] | |
| parts = [] | |
| if sim >= 80: | |
| parts.append(f"Strong contextual alignment with the JD (semantic similarity {sim:.0f}%).") | |
| elif sim >= 60: | |
| parts.append(f"Moderate contextual alignment with the JD (semantic similarity {sim:.0f}%).") | |
| else: | |
| parts.append(f"Limited contextual alignment with the JD (semantic similarity {sim:.0f}%).") | |
| if coverage >= 80: | |
| parts.append(f"Covers {coverage:.0f}% of required skills -- excellent match.") | |
| elif coverage >= 60: | |
| parts.append(f"Covers {coverage:.0f}% of required skills -- solid foundation.") | |
| elif coverage >= 40: | |
| parts.append(f"Covers {coverage:.0f}% of required skills -- some gaps present.") | |
| else: | |
| parts.append(f"Covers only {coverage:.0f}% of required skills -- significant gaps.") | |
| if critical_miss: | |
| top = ", ".join(critical_miss[:3]) | |
| parts.append(f"Missing critical skills: {top}{'...' if len(critical_miss) > 3 else ''}.") | |
| elif important_miss: | |
| top = ", ".join(important_miss[:2]) | |
| parts.append(f"Gaps in important skills: {top}.") | |
| else: | |
| parts.append("No critical skill gaps detected.") | |
| if len(resume_extra) >= 5: | |
| parts.append(f"Brings {len(resume_extra)} additional skills beyond JD requirements.") | |
| return " ".join(parts) | |
| # -- Core Ranking Function -------------------------------------------- | |
| def rank_candidates( | |
| candidates: List[Dict], | |
| jd_text: str, | |
| similarity_weight: float = 0.7, | |
| skill_weight: float = 0.3, | |
| ) -> List["CandidateResult"]: | |
| """ | |
| Rank candidates using weighted composite of calibrated semantic similarity | |
| and weighted skill coverage. | |
| Composite = (similarity_weight * calibrated_semantic_pct) | |
| + (skill_weight * weighted_skill_coverage_pct) | |
| Args: | |
| candidates : List of dicts with 'name', 'text', 'score'. | |
| jd_text : Preprocessed JD text. | |
| similarity_weight : Weight for semantic score (default 0.7). | |
| skill_weight : Weight for skill coverage (default 0.3). | |
| Returns: | |
| List of CandidateResult sorted by composite score (descending). | |
| """ | |
| if not candidates: | |
| raise ValueError("Candidates list is empty.") | |
| if abs(similarity_weight + skill_weight - 1.0) > 0.01: | |
| raise ValueError(f"Weights must sum to 1.0. Got {similarity_weight + skill_weight:.2f}") | |
| logger.info(f"Ranking {len(candidates)} candidates | sim={similarity_weight} skill={skill_weight}") | |
| raw_results = [] | |
| for candidate in candidates: | |
| name = candidate["name"] | |
| raw_cosine = candidate["score"] | |
| calibrated_sim = calibrate_score(raw_cosine) | |
| skill_data = full_skill_analysis(candidate["text"], jd_text) | |
| weighted_cov = skill_data["weighted_coverage_pct"] | |
| simple_cov = skill_data["skill_coverage_pct"] | |
| composite = round(min(100.0, max(0.0, | |
| calibrated_sim * similarity_weight + weighted_cov * skill_weight | |
| )), 2) | |
| recommendation, color = get_recommendation(composite) | |
| raw_results.append({ | |
| "name" : name, | |
| "resume_text" : candidate["text"], | |
| "similarity_score" : round(raw_cosine, 4), | |
| "calibrated_sim_pct": calibrated_sim, | |
| "score_pct" : composite, | |
| "recommendation" : recommendation, | |
| "color" : color, | |
| "matching" : skill_data["matching"], | |
| "missing" : skill_data["missing"], | |
| "critical_missing" : skill_data["critical_missing"], | |
| "important_missing" : skill_data["important_missing"], | |
| "resume_only" : skill_data["resume_only"], | |
| "skill_coverage_pct" : simple_cov, | |
| "weighted_coverage_pct" : weighted_cov, | |
| "skills_by_category" : skill_data["skills_by_category"], | |
| "jd_skill_count" : len(skill_data["jd_skills"]), | |
| }) | |
| raw_results.sort(key=lambda x: x["score_pct"], reverse=True) | |
| scores = [r["score_pct"] for r in raw_results] | |
| percentiles = compute_percentile_ranks(scores) | |
| n = len(raw_results) | |
| results: List[CandidateResult] = [] | |
| for rank_idx, (r, pct) in enumerate(zip(raw_results, percentiles), start=1): | |
| confidence = compute_confidence(r["score_pct"], n) | |
| ai_insight = generate_insight(r, r["jd_skill_count"]) | |
| result = CandidateResult( | |
| name = r["name"], | |
| resume_text = r["resume_text"], | |
| similarity_score = r["similarity_score"], | |
| calibrated_sim_pct = r["calibrated_sim_pct"], | |
| score_pct = r["score_pct"], | |
| recommendation = r["recommendation"], | |
| recommendation_color = r["color"], | |
| matching_skills = r["matching"], | |
| missing_skills = r["missing"], | |
| critical_missing = r["critical_missing"], | |
| important_missing = r["important_missing"], | |
| resume_only_skills = r["resume_only"], | |
| skill_coverage_pct = r["skill_coverage_pct"], | |
| weighted_coverage_pct = r["weighted_coverage_pct"], | |
| skills_by_category = r["skills_by_category"], | |
| confidence = confidence, | |
| percentile_rank = pct, | |
| rank = rank_idx, | |
| ai_insight = ai_insight, | |
| ) | |
| results.append(result) | |
| logger.debug( | |
| f" #{rank_idx} {r['name']}: composite={r['score_pct']:.1f}% " | |
| f"sim={r['calibrated_sim_pct']:.1f}% skill={r['weighted_coverage_pct']:.1f}%" | |
| ) | |
| logger.info(f"Top candidate: {results[0].name} ({results[0].score_pct:.1f}%)") | |
| return results | |
| # -- Export Helpers --------------------------------------------------- | |
| def results_to_dataframe(results: List[CandidateResult]) -> pd.DataFrame: | |
| """Convert ranked results to a Pandas DataFrame indexed by Rank.""" | |
| rows = [r.to_dict() for r in results] | |
| df = pd.DataFrame(rows) | |
| df = df.set_index("Rank") | |
| return df | |
| def export_to_csv(results: List[CandidateResult], filepath: str) -> str: | |
| """Export results to CSV and return filepath.""" | |
| df = results_to_dataframe(results) | |
| df.to_csv(filepath) | |
| logger.info(f"Exported to: {filepath}") | |
| return filepath | |
| def summarize_rankings(results: List[CandidateResult]) -> Dict: | |
| """Compute aggregate summary statistics.""" | |
| if not results: | |
| return {} | |
| scores = [r.score_pct for r in results] | |
| return { | |
| "total_candidates" : len(results), | |
| "average_score" : round(sum(scores) / len(scores), 2), | |
| "highest_score" : round(max(scores), 2), | |
| "lowest_score" : round(min(scores), 2), | |
| "score_std" : round(pd.Series(scores).std(), 2) if len(scores) > 1 else 0.0, | |
| "highly_recommended": sum(1 for r in results if r.recommendation == "Highly Recommended"), | |
| "recommended" : sum(1 for r in results if r.recommendation == "Recommended"), | |
| "consider" : sum(1 for r in results if r.recommendation == "Consider"), | |
| "not_recommended" : sum(1 for r in results if r.recommendation == "Not Recommended"), | |
| } | |