LordofMonarchs commited on
Commit
efa1e0f
·
verified ·
1 Parent(s): 7583033

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +57 -285
README.md CHANGED
@@ -1,131 +1,63 @@
1
  ---
2
- title: Intelligent Candidate Discovery Ranking System
3
- emoji: 🎯
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: streamlit
7
- sdk_version: "1.35.0"
8
- app_file: streamlit_app.py
9
- python_version: "3.11"
10
- pinned: false
11
  ---
12
 
13
- # Redrob Hackathon: Intelligent Candidate Discovery and Ranking System
14
 
15
- **A production grade, deterministic ranking pipeline for the Redrob Intelligent Candidate Discovery and Ranking Challenge.**
16
 
17
- Ranks 100,000 candidates against a structured Job Description in **4 seconds** on CPU, with zero external API calls during inference.
18
 
19
- ![Python](https://img.shields.io/badge/Python-3.10+-blue?logo=python&logoColor=white) ![Runtime](https://img.shields.io/badge/Runtime-4.00s%20%2F%20100K-blue) ![Network](https://img.shields.io/badge/Network-Zero%20Calls-green) ![Model](https://img.shields.io/badge/Ranker-LightGBM%20LambdaRank-orange) ![Labels](https://img.shields.io/badge/Labels-Gemma3%20Pairwise%20(Local)-purple) ![License](https://img.shields.io/badge/License-MIT-yellow)
20
 
21
- [Architecture](#architecture) · [Quick Start](#quick-start) · [Runtime Performance](#runtime-performance) · [Pipeline Internals](#pipeline-internals) · [Model Comparison](#model-comparison-heuristic-vs-gemma-trained) · [Validation](#validation) · [Constraints](#runtime-constraints-all-enforced) · [File Structure](#file-structure)
 
22
 
23
  ---
24
 
25
- ## The Core Problem
26
 
27
- A ranking system built purely on heuristic scoring rules tends to reward whatever pattern the heuristics were designed to detect, which is a closed loop: the model learns to agree with its own assumptions. This pipeline breaks that circularity by training on independent judgments from a local LLM that never sees the engineered features, the BM25 scores, or the penalty weights it is implicitly being checked against. The result is a LightGBM ranker that discovers feature interactions rather than having them hand-coded in, while staying fully deterministic, CPU-only, and network-isolated at inference time.
28
 
29
- ---
30
-
31
- ## Architecture
32
-
33
- The pipeline is split into two phases. The offline phase has no time limit and produces a set of precomputed artifacts. The online phase is what actually runs during the competition's 300 second window, and only touches those artifacts plus the live candidate pool.
34
-
35
- ![App Architecture](model_training_architecture.png)
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
- **Single-command alternative** (handles artifact caching automatically):
74
-
75
- ```bash
76
- python scripts/run_full_pipeline.py --candidates ./candidates.jsonl --out ./CTRL_COFFEE_REPEAT.csv
77
  ```
78
 
79
- Add `--force-precompute` to bypass the cache and rebuild all artifacts from scratch.
80
 
81
  ---
82
 
83
- ## Runtime Performance
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
- The offline phases run once during development with no time or network restrictions. Only Stages 0 through 6 execute during the competition's 5-minute ranking window.
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
- The union of all three passes forms the Stage 1 pool, roughly 8,500 candidates.
 
 
 
 
113
 
114
- ### Stage 2: Feature Engineering
 
115
 
116
- `src/features.py` produces a 22-feature float32 vector per candidate. Every feature maps to a specific field in the candidate schema; nothing is invented or hallucinated.
117
 
118
- **Five adversarial detection functions**, each targeting a pattern identified in the synthetic dataset:
119
 
120
- | Function | Signal |
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
- **Full 22-feature matrix:**
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 to 1.0, 60d to 0.37, 90d to 0.14, 150d to 0.006 |
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 x c2 x c3 x c4 x c5 |
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
- ### Stage 3: Logical Consistency (Honeypot Defenses)
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
- ### Stage 4: LightGBM LambdaRank
172
 
173
  **Model configuration:**
174
  - `objective: lambdarank`
175
- - `eval_at: [5, 10, 50]`, explicitly optimising Precision@5, the spec's primary tiebreak criterion
176
  - Early stopping monitors NDCG@5, patience 30
177
  - 200 boosting rounds
178
 
179
- **Training labels, Gemma3 pairwise annotation (the key differentiator):**
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 Stage 1 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.
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 **Laplace smoothed** win rates:
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-3 relevance labels by quartile, producing a balanced training set with roughly 125 candidates per label.
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 is not possible to compute honestly. What is available, and what is reported here, is a direct head-to-head comparison between the LightGBM model trained on the original heuristic weak label and the LightGBM model trained on the Gemma3 pairwise labels, run on the same Stage 1 candidate pool with the same feature vectors.
218
 
219
- **Method:** both trained models score the full ~8,500-candidate Stage 1 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.
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, statistically independent rankings |
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, 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 Stage 1 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.
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
- ## File Structure
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
- ## Streamlit Sandbox (Section 10.5 Compliance)
328
-
329
- The sandbox runs in lite mode: it accepts a JSONL upload of up to 10,000 candidates, scores uploaded candidates against the real precomputed 100K-corpus BM25 index (falling back to a small inline index only for candidates not present in that corpus), runs the full ranking pipeline, and returns a downloadable `submission.csv`. Peak RAM stays well under 1 GB.
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
- This submission was developed with the assistance of the Antigravity AI coding assistant for code scaffolding, latency diagnostics, and iterative debugging throughout development.
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
+ ![Model Training Architecture](model_training_architecture.png)
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 101300, 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 03 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.