Spaces:
Runtime error
Runtime error
| """ | |
| Feature engineering β all ~48 features for a candidate profile. | |
| Interface contracts (frozen after Sync 2): | |
| FEATURE_NAMES : list[str] module-level, column order is the contract | |
| compute_features(candidate, precomputed, violation_count) -> np.ndarray | |
| compute_features_dict(candidate, precomputed) -> dict[str, float] | |
| load_precomputed(artifacts_dir) -> dict | |
| Semantic features (groups 6.1, 6.2) return 0.0 stubs until Sync 1 delivers | |
| artifacts. All other feature groups are fully operational from day 1. | |
| """ | |
| import math | |
| import pickle | |
| from datetime import date | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| from src.utils import ( | |
| REFERENCE_DATE, | |
| IT_SERVICES, | |
| JD_CORE_SKILLS, | |
| JD_NICE_SKILLS, | |
| PREFERRED_CITIES, | |
| PRODUCTION_KEYWORDS, | |
| EDU_TIER_SCORES, | |
| STEM_FIELDS, | |
| COMPANY_SIZE_ORDINAL, | |
| career_text, | |
| company_size_ordinal, | |
| days_since, | |
| is_it_services_company, | |
| notice_penalty, | |
| yoe_fit_score, | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # Feature name list β column order is frozen after Sync 2 | |
| # Append-only from that point; never reorder. | |
| # --------------------------------------------------------------------------- # | |
| FEATURE_NAMES: list[str] = [ | |
| # 6.1 Semantic (dense) β stubs until Sync 1 | |
| "sim_ideal_max", # 0 | |
| "sim_ideal_mean", # 1 | |
| "sim_anti_max", # 2 | |
| "semantic_contrastive", # 3 | |
| "sim_jd_summary", # 4 | |
| "per_req_coverage_mean", # 5 | |
| "per_req_coverage_min", # 6 | |
| "per_req_coverage_std", # 7 | |
| # 6.2 Lexical (BM25) β stubs until Sync 1 | |
| "bm25_score", # 8 | |
| "bm25_rank_pct", # 9 | |
| # 6.3 Evidence-gated skill match | |
| "core_skill_count_raw", # 10 | |
| "core_skill_count_evidenced", # 11 | |
| "skill_evidence_ratio", # 12 | |
| "nice_skill_count_evidenced", # 13 | |
| "max_assessment_score_jd", # 14 | |
| "assessment_coverage", # 15 | |
| "github_activity_score", # 16 | |
| # 6.4 Career quality | |
| "years_of_experience", # 17 | |
| "yoe_fit_score", # 18 | |
| "product_company_months", # 19 | |
| "product_company_ratio", # 20 | |
| "current_role_relevance", # 21 | |
| "ever_at_it_services_only", # 22 | |
| "longest_tenure_months", # 23 | |
| "avg_tenure_months", # 24 | |
| "title_chaser_flag", # 25 | |
| "max_company_size", # 26 | |
| "startup_experience", # 27 | |
| "production_signal_count", # 28 | |
| # 6.5 Education | |
| "best_edu_tier", # 29 | |
| "stem_degree", # 30 | |
| # 6.6 Logistics | |
| "india_location", # 31 | |
| "preferred_city_match", # 32 | |
| "willing_to_relocate", # 33 | |
| "notice_penalty", # 34 | |
| "salary_in_range", # 35 | |
| "work_mode_match", # 36 | |
| # 6.7 Behavioral signals | |
| "recency_score", # 37 | |
| "open_to_work", # 38 | |
| "recruiter_response_rate", # 39 | |
| "interview_completion_rate",# 40 | |
| "applications_30d", # 41 | |
| "profile_completeness", # 42 | |
| "saved_by_recruiters_30d", # 43 | |
| "verified_contact", # 44 | |
| "behavioral_composite", # 45 | |
| # 6.8 Consistency | |
| "consistency_violation_count", # 46 | |
| "is_honeypot", # 47 | |
| ] | |
| assert len(FEATURE_NAMES) == 48 | |
| # --------------------------------------------------------------------------- # | |
| # Artifact loader | |
| # --------------------------------------------------------------------------- # | |
| def load_precomputed(artifacts_dir: Path, load_candidate_artifacts: bool = True) -> dict: | |
| """ | |
| Load precomputed artifacts from artifacts/. | |
| Missing files are silently skipped β stubs kick in for those features. | |
| load_candidate_artifacts: | |
| True β load the by-candidate_id dense embeddings + BM25 index (used by | |
| offline training, which scores the original dataset). | |
| False β skip them. rank.py uses this so it can build the dense + BM25 | |
| indexes at runtime from the *uploaded* candidates instead | |
| (see src.runtime_index). Only JD-side, dataset-independent | |
| artifacts (jd_query_vectors, hypothetical_resumes) are loaded. | |
| """ | |
| pre: dict[str, Any] = {"feature_names": FEATURE_NAMES} | |
| emb_path = artifacts_dir / "candidate_embeddings.npy" | |
| ids_path = artifacts_dir / "candidate_ids.json" | |
| jd_vec_path = artifacts_dir / "jd_query_vectors.npy" | |
| hyp_path = artifacts_dir / "hypothetical_resumes.json" | |
| bm25_path = artifacts_dir / "bm25_index.pkl" | |
| if load_candidate_artifacts and emb_path.exists() and ids_path.exists(): | |
| import json | |
| from collections import defaultdict | |
| pre["candidate_embeddings"] = np.load(emb_path) | |
| with open(ids_path) as f: | |
| pre["candidate_ids"] = json.load(f) | |
| # Fast lookups: cid β first index (existence check) and cid β all chunk indices | |
| cid_to_idx: dict[str, int] = {} | |
| cid_to_rows: dict[str, list[int]] = defaultdict(list) | |
| for i, cid in enumerate(pre["candidate_ids"]): | |
| if cid not in cid_to_idx: | |
| cid_to_idx[cid] = i | |
| cid_to_rows[cid].append(i) | |
| pre["cid_to_idx"] = cid_to_idx | |
| pre["cid_to_rows"] = dict(cid_to_rows) | |
| if jd_vec_path.exists(): | |
| pre["jd_query_vectors"] = np.load(jd_vec_path) # shape (n_queries, dim) | |
| if hyp_path.exists(): | |
| import json | |
| with open(hyp_path) as f: | |
| pre["hypothetical_resumes"] = json.load(f) | |
| if load_candidate_artifacts and bm25_path.exists(): | |
| with open(bm25_path, "rb") as f: | |
| bm25_data = pickle.load(f) | |
| pre["bm25_index"] = bm25_data["bm25"] | |
| pre["bm25_cid_to_idx"] = { | |
| cid: i for i, cid in enumerate(bm25_data["candidate_ids"]) | |
| } | |
| return pre | |
| # --------------------------------------------------------------------------- # | |
| # Feature group implementations | |
| # --------------------------------------------------------------------------- # | |
| def _semantic_features(candidate: dict, precomputed: dict) -> list[float]: | |
| """Group 6.1 β returns stubs (0.0) until Sync 1 artifacts arrive.""" | |
| cid = candidate["candidate_id"] | |
| cid_to_idx = precomputed.get("cid_to_idx", {}) | |
| embeddings = precomputed.get("candidate_embeddings") | |
| jd_vecs = precomputed.get("jd_query_vectors") | |
| if embeddings is None or jd_vecs is None or cid not in cid_to_idx: | |
| return [0.0] * 8 | |
| # candidate embedding row(s) β O(1) lookup via prebuilt cid_to_rows index | |
| rows = precomputed.get("cid_to_rows", {}).get(cid, []) | |
| if not rows: | |
| return [0.0] * 8 | |
| cand_vecs = embeddings[rows].astype(np.float32) # (n_chunks, dim) | |
| hyp_data = precomputed.get("hypothetical_resumes", {}) | |
| hyp = hyp_data.get("resumes", []) if isinstance(hyp_data, dict) else [] | |
| ideal_idxs = [i for i, h in enumerate(hyp) if h.get("is_positive", False)] | |
| anti_idxs = [i for i, h in enumerate(hyp) if not h.get("is_positive", True)] | |
| ideal_vecs = jd_vecs[ideal_idxs] if ideal_idxs else np.zeros((1, jd_vecs.shape[1])) | |
| anti_vecs = jd_vecs[anti_idxs] if anti_idxs else np.zeros((1, jd_vecs.shape[1])) | |
| jd_summary_vec = jd_vecs[0:1] # first vector as JD proxy | |
| # Per-requirement query vectors (last 6 in jd_query_vectors by convention) | |
| req_vecs = jd_vecs[-6:] if len(jd_vecs) >= 6 else jd_vecs | |
| def cosine_matrix(a: np.ndarray, b: np.ndarray) -> np.ndarray: | |
| a_n = a / (np.linalg.norm(a, axis=1, keepdims=True) + 1e-9) | |
| b_n = b / (np.linalg.norm(b, axis=1, keepdims=True) + 1e-9) | |
| return a_n @ b_n.T | |
| sim_ideal = cosine_matrix(cand_vecs, ideal_vecs) if len(ideal_vecs) else np.zeros((len(cand_vecs), 1)) | |
| sim_anti = cosine_matrix(cand_vecs, anti_vecs) if len(anti_vecs) else np.zeros((len(cand_vecs), 1)) | |
| sim_jd = cosine_matrix(cand_vecs, jd_summary_vec) | |
| sim_ideal_max = float(sim_ideal.max()) | |
| sim_ideal_mean = float(sim_ideal.mean()) | |
| sim_anti_max = float(sim_anti.max()) | |
| semantic_contrastive = sim_ideal_max - 0.4 * sim_anti_max | |
| sim_jd_summary = float(sim_jd.max()) | |
| # Per-requirement coverage | |
| sim_req = cosine_matrix(cand_vecs, req_vecs) # (n_chunks, 6) | |
| per_req_max = sim_req.max(axis=0) # (6,) best chunk per requirement | |
| per_req_coverage_mean = float(per_req_max.mean()) | |
| per_req_coverage_min = float(per_req_max.min()) | |
| per_req_coverage_std = float(per_req_max.std()) | |
| return [ | |
| sim_ideal_max, sim_ideal_mean, sim_anti_max, semantic_contrastive, | |
| sim_jd_summary, per_req_coverage_mean, per_req_coverage_min, per_req_coverage_std, | |
| ] | |
| def _bm25_features(candidate: dict, precomputed: dict) -> list[float]: | |
| """Group 6.2 β stubs until Sync 1 delivers bm25_index.""" | |
| bm25 = precomputed.get("bm25_index") | |
| bm25_cid_to_idx = precomputed.get("bm25_cid_to_idx", {}) | |
| if bm25 is None or candidate["candidate_id"] not in bm25_cid_to_idx: | |
| return [0.0, 0.0] | |
| jd_query_tokens = ( | |
| "production embeddings retrieval vector database hybrid search " | |
| "NDCG evaluation LLM fine-tuning LoRA Python senior AI engineer " | |
| "ranking recommendation NLP information retrieval startup" | |
| ).lower().split() | |
| all_scores = precomputed.get("bm25_all_scores") | |
| if all_scores is None: | |
| all_scores = bm25.get_scores(jd_query_tokens) | |
| precomputed["bm25_all_scores"] = all_scores # cache for reuse | |
| idx = bm25_cid_to_idx[candidate["candidate_id"]] | |
| score = float(all_scores[idx]) | |
| rank_pct = float(np.mean(all_scores <= score)) | |
| return [score, rank_pct] | |
| def _skill_features(candidate: dict) -> list[float]: | |
| """Group 6.3 β evidence-gated skill match.""" | |
| skills: list[dict] = candidate.get("skills", []) | |
| career: list[dict] = candidate.get("career_history", []) | |
| sig: dict = candidate.get("redrob_signals", {}) | |
| assessment_scores: dict = sig.get("skill_assessment_scores", {}) | |
| all_career_text = " ".join( | |
| j.get("description", "") for j in career | |
| ).lower() | |
| def skill_name_lower(s: dict) -> str: | |
| return s.get("name", "").lower() | |
| def is_corroborated(skill: dict) -> bool: | |
| name = skill_name_lower(skill) | |
| in_career = name in all_career_text | |
| long_duration = skill.get("duration_months", 0) > 6 | |
| many_endorsements = skill.get("endorsements", 0) > 5 | |
| has_assessment = skill.get("name", "") in assessment_scores and \ | |
| assessment_scores[skill["name"]] > 60 | |
| senior_proficiency = skill.get("proficiency") in ("advanced", "expert") | |
| return sum([in_career, long_duration, many_endorsements, | |
| has_assessment, senior_proficiency]) >= 1 | |
| def matches_jd_core(skill: dict) -> bool: | |
| name = skill_name_lower(skill) | |
| return any(kw.lower() in name or name in kw.lower() | |
| for kw in JD_CORE_SKILLS) | |
| def matches_jd_nice(skill: dict) -> bool: | |
| name = skill_name_lower(skill) | |
| return any(kw.lower() in name or name in kw.lower() | |
| for kw in JD_NICE_SKILLS) | |
| core_raw = sum(1 for s in skills if matches_jd_core(s)) | |
| core_evidenced = sum(1 for s in skills if matches_jd_core(s) and is_corroborated(s)) | |
| skill_evidence_ratio = core_evidenced / core_raw if core_raw > 0 else 0.0 | |
| nice_evidenced = sum(1 for s in skills if matches_jd_nice(s) and is_corroborated(s)) | |
| # Best assessment score on any JD-relevant skill | |
| jd_assessment_scores = [ | |
| v for k, v in assessment_scores.items() | |
| if any(kw.lower() in k.lower() for kw in JD_CORE_SKILLS + JD_NICE_SKILLS) | |
| ] | |
| max_assessment = max(jd_assessment_scores) / 100.0 if jd_assessment_scores else 0.0 | |
| # Fraction of JD core skills that have any assessment score | |
| core_skill_names = {s.get("name", "") for s in skills if matches_jd_core(s)} | |
| assessed_core = sum(1 for n in core_skill_names if n in assessment_scores) | |
| assessment_coverage = assessed_core / len(JD_CORE_SKILLS) | |
| github = sig.get("github_activity_score", -1) | |
| github_score = max(0.0, float(github)) / 100.0 if github >= 0 else 0.0 | |
| return [ | |
| float(core_raw), | |
| float(core_evidenced), | |
| skill_evidence_ratio, | |
| float(nice_evidenced), | |
| max_assessment, | |
| assessment_coverage, | |
| github_score, | |
| ] | |
| def _career_features(candidate: dict) -> list[float]: | |
| """Group 6.4 β career quality.""" | |
| profile: dict = candidate.get("profile", {}) | |
| career: list[dict] = candidate.get("career_history", []) | |
| yoe = float(profile.get("years_of_experience", 0.0)) | |
| # Product company months / ratio | |
| total_months = sum(j.get("duration_months", 0) for j in career) | |
| product_months = sum( | |
| j.get("duration_months", 0) for j in career | |
| if not is_it_services_company(j.get("company", "")) | |
| ) | |
| product_ratio = product_months / total_months if total_months > 0 else 0.0 | |
| # Ever exclusively at IT services | |
| ever_at_it_only = ( | |
| len(career) > 0 | |
| and all(is_it_services_company(j.get("company", "")) for j in career) | |
| ) | |
| # Tenure stats | |
| durations = [j.get("duration_months", 0) for j in career if j.get("duration_months", 0) > 0] | |
| longest_tenure = max(durations) if durations else 0 | |
| avg_tenure = sum(durations) / len(durations) if durations else 0.0 | |
| # Title chaser: avg tenure < 18mo across 3+ consecutive jobs | |
| title_chaser = (len(durations) >= 3 and avg_tenure < 18.0) | |
| # Max company size (ordinal) | |
| max_size = max( | |
| (company_size_ordinal(j.get("company_size")) for j in career), | |
| default=0, | |
| ) | |
| # Startup experience: any role at small company | |
| startup_exp = any( | |
| j.get("company_size") in ("1-10", "11-50", "51-200") for j in career | |
| ) | |
| # Production signal count: action words in all descriptions | |
| all_desc = " ".join(j.get("description", "") for j in career).lower() | |
| production_signals = sum(kw in all_desc for kw in PRODUCTION_KEYWORDS) | |
| # Current role relevance β keyword proxy until Sync 1 delivers embeddings | |
| AI_ENG_KEYWORDS = { | |
| "ml", "machine learning", "ai", "artificial intelligence", "nlp", | |
| "deep learning", "llm", "retrieval", "ranking", "recommendation", | |
| "embeddings", "vector", "search", "data scientist", "research", | |
| "engineer", "senior", | |
| } | |
| current_title_lower = profile.get("current_title", "").lower() | |
| matched = sum(1 for kw in AI_ENG_KEYWORDS if kw in current_title_lower) | |
| current_role_relevance = min(1.0, matched / 3.0) | |
| return [ | |
| yoe, | |
| yoe_fit_score(yoe), | |
| float(product_months), | |
| product_ratio, | |
| current_role_relevance, | |
| float(ever_at_it_only), | |
| float(longest_tenure), | |
| avg_tenure, | |
| float(title_chaser), | |
| float(max_size), | |
| float(startup_exp), | |
| float(production_signals), | |
| ] | |
| def _education_features(candidate: dict) -> list[float]: | |
| """Group 6.5 β education.""" | |
| edu: list[dict] = candidate.get("education", []) | |
| best_tier = max( | |
| (EDU_TIER_SCORES.get(e.get("tier", ""), 1) for e in edu), | |
| default=1, | |
| ) | |
| stem = any( | |
| any(sf in (e.get("field_of_study", "") + " " + e.get("degree", "")).lower() | |
| for sf in STEM_FIELDS) | |
| for e in edu | |
| ) | |
| return [float(best_tier), float(stem)] | |
| def _logistics_features(candidate: dict) -> list[float]: | |
| """Group 6.6 β location / logistics.""" | |
| profile: dict = candidate.get("profile", {}) | |
| sig: dict = candidate.get("redrob_signals", {}) | |
| country = profile.get("country", "") | |
| location = (profile.get("location", "") + " " + country).lower() | |
| india = country == "India" | |
| city_match = any(city in location for city in PREFERRED_CITIES) | |
| relocate = bool(sig.get("willing_to_relocate", False)) | |
| np_days = int(sig.get("notice_period_days", 90)) | |
| np_penalty = notice_penalty(np_days) | |
| salary_range = sig.get("expected_salary_range_inr_lpa", {}) | |
| sal_min = salary_range.get("min", 0) | |
| sal_max = salary_range.get("max", 0) | |
| # JD budget estimate: 25β60 LPA | |
| salary_ok = not (sal_max < 25 or sal_min > 60) | |
| mode = sig.get("preferred_work_mode", "").lower() | |
| work_mode = 1.0 if mode in ("hybrid", "flexible", "onsite") else 0.6 | |
| return [ | |
| float(india), | |
| float(city_match), | |
| float(relocate), | |
| np_penalty, | |
| float(salary_ok), | |
| work_mode, | |
| ] | |
| def _behavioral_features(candidate: dict) -> list[float]: | |
| """Group 6.7 β Redrob behavioral signals.""" | |
| sig: dict = candidate.get("redrob_signals", {}) | |
| last_active = sig.get("last_active_date", "") | |
| if last_active: | |
| inactive_days = days_since(last_active) | |
| recency = max(0.0, 1.0 - inactive_days / 180.0) | |
| else: | |
| recency = 0.0 | |
| open_flag = float(sig.get("open_to_work_flag", False)) | |
| response_rate = float(sig.get("recruiter_response_rate", 0.0)) | |
| interview_rate = float(sig.get("interview_completion_rate", 0.0)) | |
| apps_30d = math.log1p(float(sig.get("applications_submitted_30d", 0))) | |
| completeness = float(sig.get("profile_completeness_score", 0.0)) / 100.0 | |
| saved = math.log1p(float(sig.get("saved_by_recruiters_30d", 0))) | |
| verified = int(sig.get("verified_email", False)) + int(sig.get("verified_phone", False)) | |
| # Multiplicative composite β all three must be good for a high score | |
| open_multiplier = 1.1 if sig.get("open_to_work_flag") else 0.9 | |
| composite = ( | |
| (recency ** 0.5) | |
| * (response_rate ** 0.3) | |
| * (interview_rate ** 0.2) | |
| * open_multiplier | |
| ) | |
| composite = float(np.clip(composite, 0.0, 1.1)) | |
| return [ | |
| recency, | |
| open_flag, | |
| response_rate, | |
| interview_rate, | |
| apps_30d, | |
| completeness, | |
| saved, | |
| float(verified), | |
| composite, | |
| ] | |
| def _consistency_features(violation_count: int, is_honeypot: bool) -> list[float]: | |
| """Group 6.8 β passed in from Stage A results.""" | |
| return [float(violation_count), float(is_honeypot)] | |
| # --------------------------------------------------------------------------- # | |
| # Public API | |
| # --------------------------------------------------------------------------- # | |
| def compute_features( | |
| candidate: dict, | |
| precomputed: dict, | |
| violation_count: int = 0, | |
| is_honeypot: bool = False, | |
| ) -> np.ndarray: | |
| """ | |
| Returns a float32 vector of length len(FEATURE_NAMES) == 48. | |
| Column order matches FEATURE_NAMES β never reorder after Sync 2. | |
| """ | |
| feats: list[float] = [] | |
| feats.extend(_semantic_features(candidate, precomputed)) # 8 | |
| feats.extend(_bm25_features(candidate, precomputed)) # 2 | |
| feats.extend(_skill_features(candidate)) # 7 | |
| feats.extend(_career_features(candidate)) # 12 | |
| feats.extend(_education_features(candidate)) # 2 | |
| feats.extend(_logistics_features(candidate)) # 6 | |
| feats.extend(_behavioral_features(candidate)) # 9 | |
| feats.extend(_consistency_features(violation_count, is_honeypot)) # 2 | |
| assert len(feats) == 48, f"Feature count mismatch: {len(feats)}" | |
| return np.array(feats, dtype=np.float32) | |
| def compute_features_dict( | |
| candidate: dict, | |
| precomputed: dict, | |
| violation_count: int = 0, | |
| is_honeypot: bool = False, | |
| ) -> dict[str, float]: | |
| """Dict form of compute_features β used by Stage F disqualifier checks.""" | |
| vec = compute_features(candidate, precomputed, violation_count, is_honeypot) | |
| return dict(zip(FEATURE_NAMES, vec.tolist())) | |