RedrobJobRanker / redrob /features /skill_features.py
optimus
Deploy Redrob Streamlit sandbox
1aebf33 verified
Raw History Blame Contribute Delete
9.51 kB
"""Skill-related features: must-have coverage, assessments, durations."""
from __future__ import annotations
import numpy as np
import pandas as pd
from .. import config
_CORE = {s.lower() for s in config.CORE_COMPETENCIES}
_ADJ = {s.lower() for s in config.ADJACENT_COMPETENCIES}
_NEG = {s.lower() for s in config.NEGATIVE_COMPETENCIES}
_CRIT = {s.lower() for s in config.JD_CRITICAL}
_NICE = {s.lower() for s in config.JD_NICE_TO_HAVE}
_SKILL_WEIGHT = {s: 2.0 for s in _CRIT}
_SKILL_WEIGHT.update({s: 1.0 for s in _NICE})
for s in _CORE - _CRIT - _NICE:
_SKILL_WEIGHT.setdefault(s, 0.5)
# Max possible weighted score (6 critical + 4 nice-to-have)
_MAX_WEIGHT = sum(sorted(_SKILL_WEIGHT.values(), reverse=True)[:10])
# ----------------------------------------------------------------------
# Skill alias / canonicalization map (added by agent review, Exp #8)
# ----------------------------------------------------------------------
# Maps variant skill names (as they appear in candidates.jsonl and
# Redrob assessments) to their canonical JD-core counterparts.
# This boosts recall for candidates who list the same skill under a
# slightly different name (e.g. "Fine-tuning LLMs" → "Fine-tuning",
# "pgvector" → vector DB family, "Search Relevance" → BM25 family).
_SKILL_ALIAS = {
# Retrieval / vector DB family
"pgvector": "vector database",
"pgvector (postgres vector)": "vector database",
"vector database": "vector database",
"vector databases": "vector database",
"vector db": "vector database",
"dense retrieval": "dense retrieval",
"dense passage retrieval": "dense retrieval",
"dense passage retrieval (dpr)": "dense retrieval",
"approximate nearest neighbor": "vector database",
"faiss": "faiss",
"ann": "faiss",
"hnsw": "faiss",
"scaNN": "faiss",
"qdrant": "qdrant",
# Search ranking family
"search relevance": "bm25",
"search ranking": "bm25",
"relevance ranking": "learning to rank",
"relevance": "bm25",
"search engineer": "bm25",
"lexical search": "bm25",
"tf-idf": "bm25",
"tfidf": "bm25",
# LLM / fine-tuning family
"fine-tuning llms": "fine-tuning",
"llm fine-tuning": "fine-tuning",
"llm finetuning": "fine-tuning",
"fine-tuning": "fine-tuning",
"finetuning": "fine-tuning",
"instruction tuning": "fine-tuning",
"rlhf": "fine-tuning",
"rlhf (reinforcement learning from human feedback)": "fine-tuning",
"prompt tuning": "prompt engineering",
"prompt engineering": "prompt engineering",
"prompt design": "prompt engineering",
"prompt crafting": "prompt engineering",
"rag (retrieval-augmented generation)": "rag",
"retrieval augmented generation (rag)": "rag",
"retrieval-augmented generation (rag)": "rag",
"retrieval augmented generation": "rag",
"rag": "rag",
# Evaluation family
"learning-to-rank": "learning to rank",
"learning to rank (ltr)": "learning to rank",
"ltr (learning to rank)": "learning to rank",
"learning2rank": "learning to rank",
"lambdarank": "lambdarank",
"lambda rank": "lambdarank",
"lambdamart": "lambdarank",
"ndcg (normalised discounted cumulative gain)": "ndcg",
"mrr (mean reciprocal rank)": "mrr",
"map (mean average precision)": "map",
"mean reciprocal rank": "mrr",
"mean average precision": "map",
"normalised discounted cumulative gain": "ndcg",
# Embeddings / encoders
"sentence-transformer": "sentence transformers",
"sentence transformer": "sentence transformers",
"sentence-transformers": "sentence transformers",
"sbert": "sentence transformers",
"use (universal sentence encoder)": "sentence transformers",
"bge (bge embedding)": "bge",
"e5 (text embeddings)": "e5",
# Hybrid search
"hybrid retrieval": "hybrid search",
"hybrid ranking": "hybrid search",
"hybrid information retrieval": "hybrid search",
# OpenSearch / Elasticsearch
"elastic search": "elasticsearch",
"opensearch (managed elasticsearch)": "opensearch",
# Pinecone / Weaviate
"pinecone (vector database)": "pinecone",
"weaviate (vector database)": "weaviate",
# LLM
"large language models": "llm",
"large language model (llm)": "llm",
"llm (large language models)": "llm",
"llms": "llm",
# NLP
"natural language processing": "nlp",
"nlp (natural language processing)": "nlp",
# Vector Search alternative
"vector search (semantic search)": "embeddings",
"vector search": "embeddings",
"semantic search": "embeddings",
"approximate nearest neighbours": "vector database",
}
def _canon_skill(name: str) -> str:
"""Return the canonical form of a skill name. Falls back to lower(name)."""
n = (name or "").strip().lower()
if not n:
return ""
return _SKILL_ALIAS.get(n, n)
def _canon_skill_set(names: set) -> set:
"""Apply canonicalization to a set of skill names."""
return {_canon_skill(n) for n in names if n}
def skill_features(df: pd.DataFrame) -> pd.DataFrame:
n = len(df)
def _as_list(v):
if v is None:
return []
if hasattr(v, "tolist"):
return v.tolist()
return v
skills_full_col = [_as_list(x) for x in df["skills"].tolist()]
sigs_col = [_as_list(x) for x in df["signals"].tolist()]
n_core_hits = np.zeros(n, dtype=np.int32)
n_adj_hits = np.zeros(n, dtype=np.int32)
n_neg_hits = np.zeros(n, dtype=np.int32)
n_advanced = np.zeros(n, dtype=np.int32)
n_expert = np.zeros(n, dtype=np.int32)
assessment_max = np.zeros(n, dtype=np.float32)
assessment_mean = np.zeros(n, dtype=np.float32)
endorsement_log_mean = np.zeros(n, dtype=np.float32)
duration_log_mean = np.zeros(n, dtype=np.float32)
skill_count = np.zeros(n, dtype=np.int32)
jaccard_core = np.zeros(n, dtype=np.float32)
jd_crit_score = np.zeros(n, dtype=np.float32)
# Alias-canonicalized sets (for matching against core/jd-critical)
names_canon_lc_col = []
for i in range(n):
skills_full = skills_full_col[i]
if skills_full is None:
skills_full = []
names = [s.get("name", "").strip() for s in skills_full if s.get("name")]
names_lc = {n_.lower() for n_ in names if n_}
names_canon_lc = _canon_skill_set(names_lc)
names_canon_lc_col.append(names_canon_lc)
skill_count[i] = len(names)
n_core_hits[i] = len(names_canon_lc & _CORE)
n_adj_hits[i] = len(names_canon_lc & _ADJ)
n_neg_hits[i] = len(names_canon_lc & _NEG)
n_advanced[i] = sum(1 for s in skills_full if s.get("proficiency") in ("advanced",))
n_expert[i] = sum(1 for s in skills_full if s.get("proficiency") == "expert")
# JD-criticality weighted score (uses canonical names)
w_sum = sum(_SKILL_WEIGHT.get(n, 0.0) for n in names_canon_lc)
jd_crit_score[i] = min(w_sum / max(_MAX_WEIGHT, 1e-9), 1.0)
# Assessments — canonicalize keys to merge aliases (e.g. "pgvector" and
# "vector database" both map to the same canonical key for aggregation)
sig = sigs_col[i]
if sig is None:
sig = {}
if isinstance(sig, dict):
sas = sig.get("skill_assessment_scores") or {}
# Canonicalize keys and merge values (take max for same canonical)
canon_sas: dict = {}
for k, v in sas.items():
if v is None or not isinstance(v, (int, float)):
continue
ck = _canon_skill(k)
if ck in canon_sas:
if v > canon_sas[ck]:
canon_sas[ck] = float(v)
else:
canon_sas[ck] = float(v)
if canon_sas:
vals = list(canon_sas.values())
assessment_max[i] = max(vals)
assessment_mean[i] = sum(vals) / len(vals)
# Endorsement / duration log means
e_vals = [int(s.get("endorsements") or 0) for s in skills_full]
d_vals = [int(s.get("duration_months") or 0) for s in skills_full]
if e_vals:
endorsement_log_mean[i] = float(np.mean(np.log1p(e_vals)))
if d_vals:
duration_log_mean[i] = float(np.mean(np.log1p(d_vals)))
# Jaccard of candidate's skills with core (canonicalized)
if names_canon_lc and _CORE:
inter = len(names_canon_lc & _CORE)
union = len(names_canon_lc | _CORE)
jaccard_core[i] = inter / max(union, 1)
must_have_coverage = np.minimum(n_core_hits / 8.0, 1.0) # saturates at 8 core skills
adj_coverage = np.minimum(n_adj_hits / 5.0, 1.0)
ai_signal_strength = (n_core_hits + 0.5 * n_adj_hits) / np.maximum(1, skill_count)
return pd.DataFrame({
"skill_count": skill_count,
"n_core_skills": n_core_hits,
"n_adj_skills": n_adj_hits,
"n_neg_skills": n_neg_hits,
"n_advanced_skills": n_advanced,
"n_expert_skills": n_expert,
"must_have_coverage": must_have_coverage,
"adjacent_coverage": adj_coverage,
"ai_signal_strength": ai_signal_strength,
"assessment_max": assessment_max,
"assessment_mean": assessment_mean,
"endorsement_log_mean": endorsement_log_mean,
"duration_log_mean": duration_log_mean,
"jaccard_core": jaccard_core,
"jd_criticality_score": jd_crit_score,
})