Spaces:
Runtime error
Runtime error
| """ | |
| lib/rrf.py — V6 Adaptive Reciprocal Rank Fusion | |
| Replaces the fixed-weight fusion (0.40/0.35/0.25) with principled RRF. | |
| RRF formula: | |
| score(d) = SUM_r 1 / (k + rank_r(d)) | |
| Where k is typically 60, but we adapt it based on: | |
| - JD complexity (more skills -> higher k to reduce dominance) | |
| - Score distribution (skewed distributions -> lower k to differentiate) | |
| Adaptive K selection: | |
| - Compute score entropy for each ranking | |
| - Higher entropy = more uniform distribution = keep k at default | |
| - Lower entropy = dominated by few candidates = lower k to differentiate | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from collections import Counter | |
| from typing import Sequence | |
| def _score_entropy(scores: list[float]) -> float: | |
| """Compute normalized entropy of a score distribution.""" | |
| if not scores or max(scores) == min(scores): | |
| return 1.0 # uniform | |
| # Normalize to probabilities | |
| total = sum(scores) | |
| if total <= 0: | |
| return 1.0 | |
| probs = [s / total for s in scores] | |
| entropy = -sum(p * math.log2(p) for p in probs if p > 0) | |
| max_entropy = math.log2(len(probs)) | |
| return entropy / max_entropy if max_entropy > 0 else 1.0 | |
| def adaptive_k(sparse_scores: list[float], | |
| skill_scores: list[float], | |
| behaviour_scores: list[float], | |
| base_k: int = 60) -> int: | |
| """ | |
| Adaptively choose k for RRF based on score distributions. | |
| When scores are very concentrated (low entropy), use a lower k | |
| to help differentiate candidates. When uniform (high entropy), | |
| keep default k. | |
| """ | |
| # Compute entropy for each ranking | |
| sparse_ent = _score_entropy(sparse_scores) | |
| skill_ent = _score_entropy(skill_scores) | |
| beh_ent = _score_entropy(behaviour_scores) | |
| avg_ent = (sparse_ent + skill_ent + beh_ent) / 3.0 | |
| # Low entropy -> reduce k to amplify rank differences | |
| # High entropy -> keep k at default | |
| k = int(base_k * (0.5 + 0.5 * avg_ent)) | |
| return max(10, min(100, k)) | |
| def reciprocal_rank_fusion( | |
| rankings: list[Sequence[str]], | |
| k: int = 60, | |
| ) -> dict[str, float]: | |
| """ | |
| Fuse multiple rankings using Reciprocal Rank Fusion. | |
| Args: | |
| rankings: list of ranked candidate ID lists (best first) | |
| k: RRF constant (higher = more forgiving of rank differences) | |
| Returns: | |
| dict mapping candidate_id -> fused RRF score | |
| """ | |
| scores: dict[str, float] = {} | |
| for ranking in rankings: | |
| for rank_pos, cid in enumerate(ranking): | |
| rrf_score = 1.0 / (k + rank_pos + 1) | |
| scores[cid] = scores.get(cid, 0.0) + rrf_score | |
| return scores | |
| def adaptive_rrf( | |
| sparse_ranking: Sequence[tuple[str, float]], | |
| skill_ranking: Sequence[tuple[str, float]], | |
| behaviour_ranking: Sequence[tuple[str, float]], | |
| base_k: int = 60, | |
| ) -> dict[str, float]: | |
| """ | |
| Adaptive RRF that chooses k based on score distributions. | |
| Args: | |
| sparse_ranking: list of (candidate_id, sparse_score), sorted best first | |
| skill_ranking: list of (candidate_id, skill_score), sorted best first | |
| behaviour_ranking: list of (candidate_id, behaviour_score), sorted best first | |
| base_k: base RRF constant | |
| Returns: | |
| dict mapping candidate_id -> adaptive RRF score | |
| """ | |
| # Extract just the IDs in rank order | |
| sparse_ids = [cid for cid, _ in sparse_ranking] | |
| skill_ids = [cid for cid, _ in skill_ranking] | |
| behaviour_ids = [cid for cid, _ in behaviour_ranking] | |
| # Extract scores for entropy computation | |
| sparse_scores = [s for _, s in sparse_ranking] | |
| skill_scores = [s for _, s in skill_ranking] | |
| behaviour_scores = [s for _, s in behaviour_ranking] | |
| # Choose adaptive k | |
| k = adaptive_k(sparse_scores, skill_scores, behaviour_scores, base_k) | |
| # Apply RRF | |
| return reciprocal_rank_fusion([sparse_ids, skill_ids, behaviour_ids], k=k) | |
| def rrf_score_for_candidates( | |
| candidate_ids: list[str], | |
| sparse_scores: dict[str, float], | |
| skill_scores: dict[str, float], | |
| behaviour_scores: dict[str, float], | |
| ) -> dict[str, float]: | |
| """ | |
| Compute adaptive RRF scores for a set of candidates. | |
| Convenience function that handles ranking creation internally. | |
| """ | |
| # Sort by each signal to create rankings | |
| sparse_ranked = sorted( | |
| [(cid, sparse_scores.get(cid, 0)) for cid in candidate_ids], | |
| key=lambda x: x[1], reverse=True, | |
| ) | |
| skill_ranked = sorted( | |
| [(cid, skill_scores.get(cid, 0)) for cid in candidate_ids], | |
| key=lambda x: x[1], reverse=True, | |
| ) | |
| behaviour_ranked = sorted( | |
| [(cid, behaviour_scores.get(cid, 0)) for cid in candidate_ids], | |
| key=lambda x: x[1], reverse=True, | |
| ) | |
| return adaptive_rrf(sparse_ranked, skill_ranked, behaviour_ranked) |