Update README.md
Browse files
README.md
CHANGED
|
@@ -1,131 +1,63 @@
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
#
|
| 14 |
|
| 15 |
-
**
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-

|
| 36 |
-
|
| 37 |
-
---
|
| 38 |
-
|
| 39 |
-
## Quick Start
|
| 40 |
-
|
| 41 |
-
### Docker (recommended, matches the Stage 3 reproduction environment exactly)
|
| 42 |
-
|
| 43 |
-
```bash
|
| 44 |
-
docker build -t redrob-ranker .
|
| 45 |
-
docker run --rm --network none \
|
| 46 |
-
-v $(pwd)/candidates.jsonl:/app/candidates.jsonl \
|
| 47 |
-
-v $(pwd)/out:/app/out \
|
| 48 |
-
redrob-ranker
|
| 49 |
-
```
|
| 50 |
-
|
| 51 |
-
Output: `./out/CTRL_COFFEE_REPEAT.csv`, 100 ranked candidates, validated and ready to submit.
|
| 52 |
-
|
| 53 |
-
### Without Docker
|
| 54 |
-
|
| 55 |
-
```bash
|
| 56 |
-
# 1. Create and activate a virtualenv
|
| 57 |
-
python -m venv .venv
|
| 58 |
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 59 |
-
|
| 60 |
-
# 2. Install pinned dependencies
|
| 61 |
-
pip install -r requirements.txt
|
| 62 |
-
|
| 63 |
-
# 3. Run precomputation (one-time, roughly 7 minutes on 100K candidates)
|
| 64 |
-
python scripts/precompute.py --candidates ./candidates.jsonl --base-dir .
|
| 65 |
-
|
| 66 |
-
# 4. Run ranking (roughly 4 seconds)
|
| 67 |
-
python src/rank.py --candidates ./candidates.jsonl --out ./CTRL_COFFEE_REPEAT.csv
|
| 68 |
-
|
| 69 |
-
# 5. Validate output format
|
| 70 |
-
python scripts/validate_submission.py --submission ./CTRL_COFFEE_REPEAT.csv
|
| 71 |
```
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
```bash
|
| 76 |
-
python scripts/run_full_pipeline.py --candidates ./candidates.jsonl --out ./CTRL_COFFEE_REPEAT.csv
|
| 77 |
```
|
| 78 |
|
| 79 |
-
|
| 80 |
|
| 81 |
---
|
| 82 |
|
| 83 |
-
##
|
| 84 |
-
|
| 85 |
-
| Phase | Module | Operation | Time |
|
| 86 |
-
|---|---|---|---|
|
| 87 |
-
| Offline | `experiments/pairwise_llm_check/` | Gemma3 pairwise annotation (2,500 pairs, local Ollama) | ~45 min |
|
| 88 |
-
| Offline | `scripts/precompute.py` | BM25 indexing, static feature precomputation, LightGBM training | ~7 min |
|
| 89 |
-
| Stage 0 | `src/rank.py` | Load precomputed artifacts (BM25, LightGBM, static features) | 1.10s |
|
| 90 |
-
| Stage 1 | `src/retrieval.py` | Dual-pass BM25 retrieval (top 5,000 + rare-term safety net) | 0.05s |
|
| 91 |
-
| Stage 2 | `src/rank.py` | Load Stage 1 candidate records via byte-offset index | 0.45s |
|
| 92 |
-
| Stage 2b | `src/features.py` | Live feature extraction (22-feature matrix) | 0.45s |
|
| 93 |
-
| Stage 4 | `src/rank.py` | LightGBM LambdaRank inference, consistency multiplier | 0.02s |
|
| 94 |
-
| Stage 5 | `src/reasoning.py` | Deterministic reasoning compiler (top 100) | 1.93s |
|
| 95 |
-
| Stage 6 | `src/rank.py` | Monotonicity assertion, honeypot and diversity audits, CSV write | <0.01s |
|
| 96 |
-
| **Total** | | **End-to-end wall-clock** | **4.00s** |
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
## Pipeline Internals
|
| 103 |
-
|
| 104 |
-
### Stage 1: Dual-Pass BM25 Retrieval
|
| 105 |
-
|
| 106 |
-
Two independent BM25 queries run against a vectorised NumPy CSR matrix that is pre-built offline.
|
| 107 |
-
|
| 108 |
-
- **Pass A**: JD skill terms expanded via `data/skill_aliases.json`, queried against each candidate's `skills[].name` array. Skill names are structured, unique, and immune to the templated noise found in summary or description fields.
|
| 109 |
-
- **Pass B**: production signal keywords (`deployed`, `serving`, `latency`, `scale`, `inference`) queried against `career_history[].description`, catching candidates with production scaling experience who do not surface on skill keywords alone.
|
| 110 |
-
- **Rare-term safety net**: niche terms such as `pinecone`, `lambdarank`, `qdrant`, and `bm25` explicitly retrieve sparse but highly relevant profiles that might not rank in the top 5,000 by aggregate score.
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
|
|
|
| 115 |
|
| 116 |
-
|
| 117 |
|
| 118 |
-
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
| `detect_description_title_mismatch` | Domain-category mismatch between job title and role description, for example a "Marketing Manager" title paired with a mechanical engineering design description |
|
| 123 |
-
| `detect_template_description` | Career description matching one of 12 known synthetic templates identified by manual inspection of the dataset |
|
| 124 |
-
| `extract_production_ml_signal` | `log(1 + prod_kw_count)`, returns -1.0 (an explicit JD disqualifier) when only academic keywords are present with no production signal |
|
| 125 |
-
| `score_langchain_dabbler` | LLM-era skill months greater than 12 with zero pre-LLM IR or ML foundational skills |
|
| 126 |
-
| `score_cv_speech_specialist` | CV or speech skill months greater than 24 with zero NLP or IR skill months |
|
| 127 |
|
| 128 |
-
|
| 129 |
|
| 130 |
| # | Feature | Formula / Source |
|
| 131 |
|---|---|---|
|
|
@@ -134,14 +66,14 @@ The union of all three passes forms the Stage 1 pool, roughly 8,500 candidates.
|
|
| 134 |
| 3 | `Param_A_Systems_Depth` | Fraction of career months in roles whose descriptions contain retrieval, search, or ranking keywords |
|
| 135 |
| 4 | `Param_B_Availability` | `(recruiter_response_rate + exp(-days_inactive / 90)) / 2` |
|
| 136 |
| 5 | `Param_C_Tenure` | `min(avg_tenure_months, 48) / 48`, rewards 3+ year tenures |
|
| 137 |
-
| 6 | `Param_D_Notice_Exp` | `exp(-max(0, days-30) / 30)`: 30d
|
| 138 |
| 7 | `Param_E_Credibility` | `advanced_claimed_count / max(1, assessed_count)`, higher means less credible |
|
| 139 |
| 8 | `Param_F_Consulting` | Fraction of career at IT-services consulting firms (`industry == "IT Services" AND size == "10001+"`) |
|
| 140 |
| 9 | `Param_G_Location` | Noida/Pune = 1.0, other India = 0.7, outside and willing to relocate = 0.3, outside and unwilling = 0.0 |
|
| 141 |
| 10 | `Param_H_GitHub` | `github_activity_score / 100`; 0.3 imputed when the field equals -1 (absent) |
|
| 142 |
| 11 | `title_ai_fraction` | Career-weighted fraction in AI, ML, or data roles via a static title taxonomy |
|
| 143 |
| 12 | `prod_signal_log` | Log-compressed production keyword count, -1.0 if academic-only |
|
| 144 |
-
| 13 | `consistency_score` | Multiplicative honeypot penalty, c1
|
| 145 |
| 14 | `hard_req_coverage` | Fraction of JD hard requirements satisfied by the candidate's skill list |
|
| 146 |
| 15 | `flag_consulting_only` | `consulting_fraction > 0.95` |
|
| 147 |
| 16 | `flag_title_chaser` | `avg_tenure < 18 months` across 3+ jobs |
|
|
@@ -152,221 +84,61 @@ The union of all three passes forms the Stage 1 pool, roughly 8,500 candidates.
|
|
| 152 |
| 21 | `interaction_req_x_consistency` | `hard_req_coverage * consistency_score` |
|
| 153 |
| 22 | `interaction_yoe_x_prod` | `yoe * prod_signal_log` |
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
```
|
| 158 |
-
consistency_score = c1 * c2 * c3 * c4 * c5
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
A single logical impossibility reduces the composite to near-zero, suppressing that candidate regardless of skill profile quality.
|
| 162 |
-
|
| 163 |
-
| Check | Condition | Effect |
|
| 164 |
-
|---|---|---|
|
| 165 |
-
| c1, timeline impossibility | `skill.duration_months > total_experience_months` | Hard zero |
|
| 166 |
-
| c2, signup anomaly | `signup_date > last_active_date` | Hard zero |
|
| 167 |
-
| c3, salary inversion | `expected_salary.min > max` | 0.1 (heavy penalty) |
|
| 168 |
-
| c4, assessment contradiction | Claims "advanced" and an assessment score exists and is below 50 | Compounding 0.4x per violation |
|
| 169 |
-
| c5, engagement mismatch | High BM25 score with `connections <= 60`, `search_appearances <= 15`, `endorsements <= 4` | Hard zero |
|
| 170 |
|
| 171 |
-
##
|
| 172 |
|
| 173 |
**Model configuration:**
|
| 174 |
- `objective: lambdarank`
|
| 175 |
-
- `eval_at: [5, 10, 50]`, explicitly optimising Precision@5
|
| 176 |
- Early stopping monitors NDCG@5, patience 30
|
| 177 |
- 200 boosting rounds
|
| 178 |
|
| 179 |
-
**Training labels
|
| 180 |
|
| 181 |
-
Rather than a pure heuristic label, training labels are generated via 2,500 pairwise LLM comparisons using Gemma3:4b-it-q4_K_M running locally on Ollama, with zero external API calls and full reproducibility. A stratified sample of 500
|
| 182 |
|
| 183 |
-
For each pair, Gemma3 reads both candidates' full structured profiles alongside the JD requirements and disqualifiers, then produces a single verdict: `CANDIDATE_A`, `CANDIDATE_B`, or `TIE`. Win and loss tallies convert to Elo ratings via
|
| 184 |
|
| 185 |
```python
|
| 186 |
-
win_rate = (wins + 0.5) / (total + 1)
|
| 187 |
elo = 400 * log10(win_rate / (1 - win_rate)) + 1500
|
| 188 |
```
|
| 189 |
|
| 190 |
-
Elo ratings are thresholded to 0
|
| 191 |
|
| 192 |
**Why this breaks circularity:** Gemma had no knowledge of the 22 engineered features, the BM25 scores, or the penalty weights. It learned independently that IR-specific skills (FAISS, BM25, Qdrant, Sentence Transformers) outrank generic ML skills, and that production-company backgrounds outrank consulting-only careers. LightGBM then learns how the 22 features correlate with these independent judgments, surfacing interactions that were never explicitly encoded.
|
| 193 |
|
| 194 |
-
**Post-inference consistency multiplier:**
|
| 195 |
-
|
| 196 |
-
```python
|
| 197 |
-
final_score = lgbm_raw_score * consistency_score
|
| 198 |
-
```
|
| 199 |
-
|
| 200 |
-
This ensures candidates with data integrity violations (c1 through c5) are suppressed to near-zero regardless of model prediction, giving a clean separation of concerns: LightGBM handles fit, the consistency checks handle data integrity.
|
| 201 |
-
|
| 202 |
-
### Stage 5: Reasoning Compiler
|
| 203 |
-
|
| 204 |
-
`src/reasoning.py` generates a one to two sentence reasoning string per candidate using a deterministic grammar engine with the following properties:
|
| 205 |
-
|
| 206 |
-
- **Four structural templates** rotated via `abs(hash(candidate_id)) % 4`, so no two consecutive strings share the same sentence skeleton, which eliminates template monotony across the top 100.
|
| 207 |
-
- **Priority-ranked concern surfacing**: a notice period over 90 days surfaces before location preference, which surfaces before skill credibility concerns. Concerns are never presented as a generic checklist.
|
| 208 |
-
- **JD-specific skill phrases**: named skill combinations such as FAISS, Sentence Transformers, and BM25 are surfaced directly instead of generic category labels.
|
| 209 |
-
- **Numeric regex audit**: every number in the output string is asserted to exist in the candidate's raw JSON before writing, guaranteeing zero numeric hallucination.
|
| 210 |
-
- **N-gram collision check**: `difflib.SequenceMatcher` runs across all 100 outputs, and strings with more than 85 percent structural similarity are flagged before submission.
|
| 211 |
-
- **Decision audit trail**: `reasoning_trace.jsonl` logs the exact features, tone percentile, and concern selected for each of the top 30 candidates, enabling direct answers during a Stage 5 interview.
|
| 212 |
-
|
| 213 |
---
|
| 214 |
|
| 215 |
-
## Model Comparison: Heuristic vs Gemma-Trained
|
| 216 |
|
| 217 |
-
The competition provides no ground-truth relevance labels, so a standard NDCG@10 ablation against a labeled holdout set
|
| 218 |
|
| 219 |
-
**Method:** both trained models score the full ~8,500-candidate
|
| 220 |
|
| 221 |
| Metric | Result |
|
| 222 |
|---|---|
|
| 223 |
| Top-10 overlap between the two models | 0 of 10 candidates in common |
|
| 224 |
-
| Spearman rank correlation (top-100) | 0.001
|
| 225 |
| Honeypot leakage, heuristic-trained model | Required a hand-coded post-processing suppression list to keep keyword-stuffed non-technical profiles out of the top 100 |
|
| 226 |
-
| Honeypot leakage, Gemma-trained model | 0 of 100 candidates with `consistency_score < 0.25`, achieved with no post-processing suppression list |
|
| 227 |
|
| 228 |
-
**Qualitative before/after:** prior to the Gemma retrain, the heuristic-trained model's unsuppressed top-10 surfaced profiles such as Content Writer, Project Manager, and Sales Executive
|
| 229 |
|
| 230 |
The two models disagreeing almost completely (Spearman 0.001) is itself evidence of non-circularity: a model trained on labels derived from the same 22 features it predicts on would be expected to correlate strongly with a heuristic built from those same features, not diverge from it entirely.
|
| 231 |
|
| 232 |
-
This comparison, not a fabricated NDCG number, is the evidence offered for why the pairwise-LLM-label approach was chosen over a simpler heuristic scorer.
|
| 233 |
-
|
| 234 |
-
---
|
| 235 |
-
|
| 236 |
-
## Validation
|
| 237 |
-
|
| 238 |
-
### Full validation suite
|
| 239 |
-
|
| 240 |
-
```bash
|
| 241 |
-
python scripts/run_full_validation.py
|
| 242 |
-
```
|
| 243 |
-
|
| 244 |
-
Runs four checks in sequence:
|
| 245 |
-
|
| 246 |
-
1. **Honeypot injection test**: injects all 7 synthetic violation types into a cloned top-ranked candidate and asserts zero leakage into the top-100 output.
|
| 247 |
-
2. **Diversity audit**: asserts employer concentration at or below 30 percent and archetype signature concentration at or below 25 percent via `validate_pipeline.check_top100_diversity`.
|
| 248 |
-
3. **c5 boundary test**: validates the engagement mismatch threshold fires correctly at the boundary values (connections=60, appearances=15, endorsements=4).
|
| 249 |
-
4. **NDCG probe**: computes NDCG@10 against hand-labeled reference points where available in the Stage 1 pool.
|
| 250 |
-
|
| 251 |
-
### Blocking audits in rank.py
|
| 252 |
-
|
| 253 |
-
Two hard-blocking assertions run before any CSV write. If either fails, `rank.py` exits non-zero with a descriptive error; there are no silent failures.
|
| 254 |
-
|
| 255 |
-
```python
|
| 256 |
-
# Honeypot audit (Section 8.1)
|
| 257 |
-
assert count(consistency_score < 0.25 in top_100) < 10
|
| 258 |
-
|
| 259 |
-
# Diversity audit (Section 8.2)
|
| 260 |
-
assert max_company_concentration <= 0.30
|
| 261 |
-
assert max_signature_concentration <= 0.25
|
| 262 |
-
```
|
| 263 |
-
|
| 264 |
-
---
|
| 265 |
-
|
| 266 |
-
## Runtime Constraints (All Enforced)
|
| 267 |
-
|
| 268 |
-
| Constraint | Limit | Enforcement |
|
| 269 |
-
|---|---|---|
|
| 270 |
-
| Wall-clock | <= 300s | `assert elapsed < 300` plus `sys.exit(4)` if exceeded |
|
| 271 |
-
| RAM | <= 16 GB | BM25 Stage 1 pool capped at 5,000 candidates |
|
| 272 |
-
| Network | Zero | `--network none` Docker flag; no runtime import makes a network call |
|
| 273 |
-
| Disk | <= 5 GB | Total precomputed artifacts: ~216 MB |
|
| 274 |
-
| Output rows | Exactly 100 | `assert len(df) == 100` before CSV write |
|
| 275 |
-
| Score monotonicity | Non-increasing | `assert_monotonicity()` before CSV write |
|
| 276 |
-
| Tiebreaking | Ascending `candidate_id` | `sorted(key=lambda x: (-x[1], x[0]))` |
|
| 277 |
-
| Determinism | Byte-identical across runs | `REFERENCE_DATE = date(2026, 1, 1)` constant, never `datetime.now()` |
|
| 278 |
-
|
| 279 |
---
|
| 280 |
|
| 281 |
-
##
|
| 282 |
-
|
| 283 |
-
```
|
| 284 |
-
├── data/
|
| 285 |
-
│ └── skill_aliases.json JD taxonomy: skill aliases for BM25 query expansion
|
| 286 |
-
├── precomputed/ Artifacts generated by precompute.py
|
| 287 |
-
│ ├── vocab.pkl BM25 vocabulary: term to column index (19.5 KB)
|
| 288 |
-
│ ├── bm25_matrix.npz Vectorised Scipy BM25 CSR matrix (39.6 MB)
|
| 289 |
-
│ ├── candidate_offsets.pkl Byte-offset index for O(1) JSONL lookup (2.0 MB)
|
| 290 |
-
│ ├── lgbm_model.txt Trained LightGBM booster, native text format (1.3 MB)
|
| 291 |
-
│ ├── lgbm_model.pkl LightGBM booster, pickle fallback (1.4 MB)
|
| 292 |
-
│ ├── static_features.pkl 18 JD-independent features precomputed offline (21.7 MB)
|
| 293 |
-
│ ├── candidate_ids.pkl BM25 row to candidate_id mapping (1.5 MB)
|
| 294 |
-
│ └── weak_labels.pkl Training labels log from offline precomputation (2.4 MB)
|
| 295 |
-
├── src/
|
| 296 |
-
│ ├── jd_parser.py JD requirement extraction from skill_aliases.json
|
| 297 |
-
│ ├── retrieval.py Dual-pass BM25 retrieval, rare-term safety net
|
| 298 |
-
│ ├── features.py 22-feature matrix, 5 adversarial detection functions
|
| 299 |
-
│ ├── reasoning.py Deterministic reasoning compiler
|
| 300 |
-
│ └── rank.py Main entry point
|
| 301 |
-
├── scripts/
|
| 302 |
-
│ ├── precompute.py Offline: BM25 indexing, LightGBM training
|
| 303 |
-
│ ├── app.py Streamlit sandbox (lite mode, <= 1 GB RAM)
|
| 304 |
-
│ ├── validate_submission.py Output format validator
|
| 305 |
-
│ ├── validate_pipeline.py Competition-provided validation module (unmodified)
|
| 306 |
-
│ ├── run_full_pipeline.py End-to-end orchestration with artifact caching
|
| 307 |
-
│ ├── run_full_validation.py Full validation suite
|
| 308 |
-
│ └── rebuild_fast_artifacts.py Utility: rebuild NumPy BM25 artifacts from scratch
|
| 309 |
-
├── experiments/
|
| 310 |
-
│ └── pairwise_llm_check/ Offline annotation experiment, isolated from inference
|
| 311 |
-
│ ├── annotate_and_retrain.py Gemma3 pairwise annotation, LightGBM retraining
|
| 312 |
-
│ ├── annotations.jsonl 2,500 pairwise judgments (Gemma3:4b-it-q4_K_M, local)
|
| 313 |
-
│ └── README.md Experiment methodology and budget exemption statement
|
| 314 |
-
├── diagnostics/
|
| 315 |
-
│ ├── diag_profile_live_features.py Live feature extraction latency profiler
|
| 316 |
-
│ └── verify_c5_thresholds.py c5 boundary condition verification
|
| 317 |
-
├── logs/ Runtime logs generated by rank.py (gitignored)
|
| 318 |
-
├── requirements.txt All dependencies pinned to exact versions
|
| 319 |
-
├── Dockerfile CPU-only, --network none compatible
|
| 320 |
-
├── docker-entrypoint.sh Pipeline mode selector
|
| 321 |
-
├── submission_metadata.yaml Competition portal metadata
|
| 322 |
-
└── README.md This file
|
| 323 |
-
```
|
| 324 |
-
|
| 325 |
-
---
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
On small uploaded batches, the trained model places very low weight on `bm25_score` relative to JD-fit features (a direct consequence of training on Gemma labels, which never see retrieval scores), so multiple candidates can legitimately receive identical model scores. When this happens, the sandbox display applies a transparent, display-only secondary sort by `hard_req_coverage` and `bm25_score` so the ranking order remains legible; the underlying score values and the production `rank.py` pipeline are unaffected.
|
| 332 |
-
|
| 333 |
-
**Local:**
|
| 334 |
-
|
| 335 |
-
```bash
|
| 336 |
-
streamlit run scripts/app.py
|
| 337 |
-
```
|
| 338 |
-
|
| 339 |
-
---
|
| 340 |
-
|
| 341 |
-
## Troubleshooting
|
| 342 |
-
|
| 343 |
-
**`precompute.py` raises a memory error**
|
| 344 |
-
Ensure at least 16 GB RAM is available. The full 100K JSONL requires approximately 4 to 6 GB peak during BM25 index construction.
|
| 345 |
-
|
| 346 |
-
**`rank.py` fails the diversity audit (exit code 3)**
|
| 347 |
-
Not encountered during testing; every run, including the most recent full pipeline run after the Streamlit sandbox fixes, produced 93 distinct archetype signatures with max employer concentration of 14 percent and max signature concentration of 3 percent, both comfortably under the 30/25 percent thresholds. This entry documents the expected resolution path if a future model retrain or feature change causes a regression: check LightGBM feature importances via `precomputed/lgbm_model.txt` and verify the training label distribution in `scripts/precompute.py` is balanced across all four quartiles.
|
| 348 |
-
|
| 349 |
-
**`rank.py` exits with code 2 (honeypot audit failed)**
|
| 350 |
-
More than 10 candidates with `consistency_score < 0.25` reached the top-100. Verify that `consistency_score` is computed correctly in `src/features.py` and that the post-inference multiplier (`final_score = lgbm_score * consistency_score`) is active in `src/rank.py`.
|
| 351 |
-
|
| 352 |
-
**Docker build fails on arm64 Mac**
|
| 353 |
-
Use `--platform linux/amd64` if cross-building for a cloud runner. LightGBM provides native arm64 wheels for local builds.
|
| 354 |
|
| 355 |
---
|
| 356 |
|
| 357 |
## AI Tool Disclosure
|
| 358 |
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
Gemma3:4b-it-q4_K_M (Google DeepMind, running locally via Ollama) was used offline to generate 2,500 pairwise relevance judgments on a stratified sample of 500 Stage 1 candidates. These judgments served as independent, non-circular training labels for the LightGBM model. No candidate data was transmitted to any external service at any point. All ranking inference is CPU-only with zero network calls.
|
| 362 |
-
|
| 363 |
-
Key milestones directed and verified by the human team at every stage:
|
| 364 |
-
|
| 365 |
-
- Identified and fixed the weak label circularity bug where heuristic labels were rewarding keyword-stuffed trap candidates.
|
| 366 |
-
- Designed the stratified pairwise sampling strategy with guaranteed low-consistency candidate coverage.
|
| 367 |
-
- Diagnosed and resolved the score compression issue via a normalization scope fix in output assembly.
|
| 368 |
-
- Approved the Elo to quartile label conversion thresholds and the post-inference consistency multiplier.
|
| 369 |
-
- Verified all Stage 4 and Stage 5 compliance criteria against actual pipeline output before submission.
|
| 370 |
-
- Diagnosed and fixed the Streamlit sandbox's BM25 scoping bug, where an inline index built on small upload batches produced unreliable term statistics; the sandbox now queries the real 100K-corpus index directly.
|
| 371 |
-
- Ran the heuristic-vs-Gemma model comparison reported above and verified its numbers directly against pipeline output before including them in this document.
|
| 372 |
-
Done
|
|
|
|
| 1 |
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: lightgbm
|
| 4 |
+
tags:
|
| 5 |
+
- learning-to-rank
|
| 6 |
+
- lightgbm
|
| 7 |
+
- lambdarank
|
| 8 |
+
- recruitment
|
| 9 |
+
- candidate-ranking
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Intelligent Candidate Ranker (LightGBM LambdaRank)
|
| 13 |
|
| 14 |
+
**The ranking model from the Redrob Intelligent Candidate Discovery and Ranking Challenge submission.**
|
| 15 |
|
| 16 |
+
Given a 22-feature vector describing a candidate's fit against a job description, this model outputs a relevance score. It is trained on labels generated by 2,500 pairwise judgments from a local LLM (Gemma3) rather than hand-coded heuristics, specifically to avoid label circularity.
|
| 17 |
|
| 18 |
+

|
| 19 |
|
| 20 |
+
Full pipeline (retrieval, feature engineering, consistency scoring, reasoning generation) lives in the GitHub repo:
|
| 21 |
+
https://github.com/Pranjal1342/Intelligent-Candidate-Discovery-Ranking-System
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
+
## This model's role in the pipeline
|
| 26 |
|
| 27 |
+
This model is one stage inside a larger offline candidate-ranking pipeline. It does not do retrieval, does not compute the input features itself, and does not produce the final rank on its own.
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
```
|
| 30 |
+
raw_score = this_model.predict(feature_vector)
|
| 31 |
+
final_score = raw_score * consistency_score # applied by the host pipeline, not this model
|
|
|
|
|
|
|
|
|
|
| 32 |
```
|
| 33 |
|
| 34 |
+
`consistency_score` is a separate, multiplicative honeypot/data-integrity check computed by the host application — it is not part of this model's output.
|
| 35 |
|
| 36 |
---
|
| 37 |
|
| 38 |
+
## How to load
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
```python
|
| 41 |
+
from huggingface_hub import hf_hub_download
|
| 42 |
+
import lightgbm as lgb
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
model_path = hf_hub_download(
|
| 45 |
+
repo_id="<your-username>/intelligent-candidate-ranker",
|
| 46 |
+
filename="lgbm_model.txt"
|
| 47 |
+
)
|
| 48 |
+
model = lgb.Booster(model_file=model_path)
|
| 49 |
|
| 50 |
+
raw_score = model.predict(feature_vector) # feature_vector: 22-dim float32
|
| 51 |
+
```
|
| 52 |
|
| 53 |
+
---
|
| 54 |
|
| 55 |
+
## Input / Output
|
| 56 |
|
| 57 |
+
- **Input:** a 22-feature float32 vector per candidate (exact feature order below)
|
| 58 |
+
- **Output:** a single raw relevance score (higher = more relevant). Not yet penalized for data-integrity issues — combine with a `consistency_score` downstream before final ranking.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
### Feature vector (in order)
|
| 61 |
|
| 62 |
| # | Feature | Formula / Source |
|
| 63 |
|---|---|---|
|
|
|
|
| 66 |
| 3 | `Param_A_Systems_Depth` | Fraction of career months in roles whose descriptions contain retrieval, search, or ranking keywords |
|
| 67 |
| 4 | `Param_B_Availability` | `(recruiter_response_rate + exp(-days_inactive / 90)) / 2` |
|
| 68 |
| 5 | `Param_C_Tenure` | `min(avg_tenure_months, 48) / 48`, rewards 3+ year tenures |
|
| 69 |
+
| 6 | `Param_D_Notice_Exp` | `exp(-max(0, days-30) / 30)`: 30d → 1.0, 60d → 0.37, 90d → 0.14, 150d → 0.006 |
|
| 70 |
| 7 | `Param_E_Credibility` | `advanced_claimed_count / max(1, assessed_count)`, higher means less credible |
|
| 71 |
| 8 | `Param_F_Consulting` | Fraction of career at IT-services consulting firms (`industry == "IT Services" AND size == "10001+"`) |
|
| 72 |
| 9 | `Param_G_Location` | Noida/Pune = 1.0, other India = 0.7, outside and willing to relocate = 0.3, outside and unwilling = 0.0 |
|
| 73 |
| 10 | `Param_H_GitHub` | `github_activity_score / 100`; 0.3 imputed when the field equals -1 (absent) |
|
| 74 |
| 11 | `title_ai_fraction` | Career-weighted fraction in AI, ML, or data roles via a static title taxonomy |
|
| 75 |
| 12 | `prod_signal_log` | Log-compressed production keyword count, -1.0 if academic-only |
|
| 76 |
+
| 13 | `consistency_score` | Multiplicative honeypot penalty, c1 × c2 × c3 × c4 × c5 (included as a training feature; also reapplied post-inference — see below) |
|
| 77 |
| 14 | `hard_req_coverage` | Fraction of JD hard requirements satisfied by the candidate's skill list |
|
| 78 |
| 15 | `flag_consulting_only` | `consulting_fraction > 0.95` |
|
| 79 |
| 16 | `flag_title_chaser` | `avg_tenure < 18 months` across 3+ jobs |
|
|
|
|
| 84 |
| 21 | `interaction_req_x_consistency` | `hard_req_coverage * consistency_score` |
|
| 85 |
| 22 | `interaction_yoe_x_prod` | `yoe * prod_signal_log` |
|
| 86 |
|
| 87 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
## Training
|
| 90 |
|
| 91 |
**Model configuration:**
|
| 92 |
- `objective: lambdarank`
|
| 93 |
+
- `eval_at: [5, 10, 50]`, explicitly optimising Precision@5
|
| 94 |
- Early stopping monitors NDCG@5, patience 30
|
| 95 |
- 200 boosting rounds
|
| 96 |
|
| 97 |
+
**Training labels — Gemma3 pairwise annotation (the key differentiator):**
|
| 98 |
|
| 99 |
+
Rather than a pure heuristic label, training labels are generated via 2,500 pairwise LLM comparisons using Gemma3:4b-it-q4_K_M running locally on Ollama, with zero external API calls and full reproducibility. A stratified sample of 500 candidates is drawn across three strata (top-100, boundary 101–300, and a broader pool with guaranteed low-consistency coverage), and each candidate receives roughly five matchups against random opponents.
|
| 100 |
|
| 101 |
+
For each pair, Gemma3 reads both candidates' full structured profiles alongside the JD requirements and disqualifiers, then produces a single verdict: `CANDIDATE_A`, `CANDIDATE_B`, or `TIE`. Win and loss tallies convert to Elo ratings via Laplace-smoothed win rates:
|
| 102 |
|
| 103 |
```python
|
| 104 |
+
win_rate = (wins + 0.5) / (total + 1)
|
| 105 |
elo = 400 * log10(win_rate / (1 - win_rate)) + 1500
|
| 106 |
```
|
| 107 |
|
| 108 |
+
Elo ratings are thresholded to 0–3 relevance labels by quartile, producing a balanced training set with roughly 125 candidates per label.
|
| 109 |
|
| 110 |
**Why this breaks circularity:** Gemma had no knowledge of the 22 engineered features, the BM25 scores, or the penalty weights. It learned independently that IR-specific skills (FAISS, BM25, Qdrant, Sentence Transformers) outrank generic ML skills, and that production-company backgrounds outrank consulting-only careers. LightGBM then learns how the 22 features correlate with these independent judgments, surfacing interactions that were never explicitly encoded.
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
---
|
| 113 |
|
| 114 |
+
## Model Comparison: Heuristic vs. Gemma-Trained
|
| 115 |
|
| 116 |
+
The competition provides no ground-truth relevance labels, so a standard NDCG@10 ablation against a labeled holdout set isn't possible to compute honestly. What is available, and what is reported here, is a direct head-to-head comparison between a LightGBM model trained on the original heuristic weak label and this model (trained on Gemma3 pairwise labels), run on the same candidate pool with the same feature vectors.
|
| 117 |
|
| 118 |
+
**Method:** both trained models score the full ~8,500-candidate retrieval pool. The same post-inference consistency multiplier is applied to both before ranking, so the comparison isolates the effect of the training label, not the honeypot suppression layer.
|
| 119 |
|
| 120 |
| Metric | Result |
|
| 121 |
|---|---|
|
| 122 |
| Top-10 overlap between the two models | 0 of 10 candidates in common |
|
| 123 |
+
| Spearman rank correlation (top-100) | 0.001 — statistically independent rankings |
|
| 124 |
| Honeypot leakage, heuristic-trained model | Required a hand-coded post-processing suppression list to keep keyword-stuffed non-technical profiles out of the top 100 |
|
| 125 |
+
| Honeypot leakage, Gemma-trained model (this model) | 0 of 100 candidates with `consistency_score < 0.25`, achieved with no post-processing suppression list |
|
| 126 |
|
| 127 |
+
**Qualitative before/after:** prior to the Gemma retrain, the heuristic-trained model's unsuppressed top-10 surfaced profiles such as Content Writer, Project Manager, and Sales Executive — each with AI-sounding skills listed but no underlying technical career history, because the heuristic label rewarded keyword coverage directly. After the Gemma retrain, the same pool's top-10 surfaced candidates with FAISS, BM25, Qdrant, Sentence Transformers, and Hugging Face Transformers in their skill history — sourced from a model that never saw `bm25_score` or `hard_req_coverage` during label generation and discovered the IR-relevance ordering independently from reading full candidate profiles.
|
| 128 |
|
| 129 |
The two models disagreeing almost completely (Spearman 0.001) is itself evidence of non-circularity: a model trained on labels derived from the same 22 features it predicts on would be expected to correlate strongly with a heuristic built from those same features, not diverge from it entirely.
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
---
|
| 132 |
|
| 133 |
+
## Intended Use & Limitations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
- Built for a hackathon submission (Redrob Intelligent Candidate Discovery and Ranking Challenge); not validated for production hiring decisions.
|
| 136 |
+
- Expects the exact 22-feature schema above, computed by the host pipeline's `src/features.py`. Feeding hand-built or differently-ordered features will produce meaningless scores.
|
| 137 |
+
- Raw model output is **not** the final ranking score — it must be multiplied by a separately computed `consistency_score` before use.
|
| 138 |
+
- Trained on a synthetic/competition candidate dataset; label distribution and feature semantics may not generalize to other candidate pools without retraining.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
---
|
| 141 |
|
| 142 |
## AI Tool Disclosure
|
| 143 |
|
| 144 |
+
Gemma3:4b-it-q4_K_M (Google DeepMind, running locally via Ollama) was used offline to generate 2,500 pairwise relevance judgments on a stratified sample of 500 candidates. These judgments served as independent, non-circular training labels for this model. No candidate data was transmitted to any external service at any point.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|