ashokbugude
Cursor
Full HF Space: bundled candidates.jsonl, live rank.py, optional upload
b91193c
Raw History Blame Contribute Delete
6.21 kB
#!/usr/bin/env python3
"""Exploratory data analysis on the candidate dataset."""
from __future__ import annotations
import argparse
import json
import logging
import sys
from collections import Counter
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from app.candidates import count_candidates, load_candidates # noqa: E402
from app.config import get_settings # noqa: E402
from app.constants import KNOWN_TEMPLATE_HASHES, REFERENCE_DATE, career_description_hash # noqa: E402
from app.labels.honeypots import detect_honeypot # noqa: E402
from app.labels.tiers import assign_heuristic_tier # noqa: E402
from app.logging_setup import configure_logging # noqa: E402
logger = logging.getLogger(__name__)
def run_eda(candidates_path: Path, *, limit: int | None = None) -> dict:
"""Compute dataset statistics and trap/honeypot summaries."""
total = count_candidates(candidates_path) if limit is None else min(
count_candidates(candidates_path), limit
)
title_counter: Counter[str] = Counter()
country_counter: Counter[str] = Counter()
industry_counter: Counter[str] = Counter()
yoe_buckets: Counter[str] = Counter()
template_hash_counter: Counter[str] = Counter()
honeypot_rule_counter: Counter[str] = Counter()
tier_counter: Counter[int] = Counter()
skill_count_buckets: Counter[str] = Counter()
processed = 0
honeypot_count = 0
all_template_roles = 0
for candidate in load_candidates(candidates_path, limit=limit):
profile = candidate.get("profile") or {}
title_counter[str(profile.get("current_title") or "unknown").lower()] += 1
country_counter[str(profile.get("country") or "unknown")] += 1
industry_counter[str(profile.get("current_industry") or "unknown").lower()] += 1
yoe = float(profile.get("years_of_experience") or 0)
if yoe < 3:
yoe_buckets["0-3"] += 1
elif yoe <= 5:
yoe_buckets["3-5"] += 1
elif yoe <= 9:
yoe_buckets["5-9"] += 1
elif yoe <= 15:
yoe_buckets["9-15"] += 1
else:
yoe_buckets["15+"] += 1
skills = candidate.get("skills") or []
n_skills = len(skills)
if n_skills <= 5:
skill_count_buckets["0-5"] += 1
elif n_skills <= 10:
skill_count_buckets["6-10"] += 1
elif n_skills <= 15:
skill_count_buckets["11-15"] += 1
else:
skill_count_buckets["16+"] += 1
career = candidate.get("career_history") or []
role_template_hits = 0
for role in career:
desc = str(role.get("description") or "").strip()
if len(desc) < 80:
continue
prefix = career_description_hash(desc)[:12]
template_hash_counter[prefix] += 1
if prefix in KNOWN_TEMPLATE_HASHES:
role_template_hits += 1
if career and role_template_hits == len(career):
all_template_roles += 1
hp = detect_honeypot(candidate, reference_date=REFERENCE_DATE)
if hp.is_honeypot:
honeypot_count += 1
for rule in hp.rules_hit:
honeypot_rule_counter[rule] += 1
tier = assign_heuristic_tier(candidate, hp)
tier_counter[tier.tier] += 1
processed += 1
known_template_role_total = sum(
template_hash_counter[h[:12] if len(h) > 12 else h]
for h in KNOWN_TEMPLATE_HASHES
if h in template_hash_counter
)
report = {
"candidates_path": str(candidates_path),
"candidates_analyzed": processed,
"candidates_total_in_file": total,
"limit_applied": limit,
"profile": {
"top_titles": title_counter.most_common(25),
"top_countries": country_counter.most_common(15),
"top_industries": industry_counter.most_common(15),
"yoe_distribution": dict(yoe_buckets),
"skill_count_distribution": dict(skill_count_buckets),
},
"traps": {
"known_template_hashes": sorted(KNOWN_TEMPLATE_HASHES),
"top_recycled_description_hashes": template_hash_counter.most_common(12),
"candidates_all_roles_template": all_template_roles,
"known_template_role_occurrences": known_template_role_total,
},
"honeypots": {
"detected_count": honeypot_count,
"detection_rate": round(honeypot_count / max(1, processed), 4),
"rule_hit_counts": dict(honeypot_rule_counter.most_common()),
},
"silver_tier_preview": {
str(tier): count for tier, count in sorted(tier_counter.items())
},
}
return report
def main() -> int:
parser = argparse.ArgumentParser(description="Run EDA on candidate dataset.")
parser.add_argument(
"--candidates",
type=Path,
default=None,
help="Candidates file (default: settings.candidates_path).",
)
parser.add_argument(
"--out",
type=Path,
default=None,
help="Output JSON report (default: artifacts/eda_report.json).",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Process only first N candidates (for quick runs).",
)
args = parser.parse_args()
settings = get_settings()
configure_logging(settings.log_level)
candidates_path = (args.candidates or settings.candidates_path).resolve()
out_path = (args.out or settings.artifact_path("eda_report.json")).resolve()
logger.info("Running EDA on %s", candidates_path)
report = run_eda(candidates_path, limit=args.limit)
settings.ensure_artifacts_dir()
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
logger.info(
"EDA complete: %d candidates, %d honeypots (%.1f%%)",
report["candidates_analyzed"],
report["honeypots"]["detected_count"],
report["honeypots"]["detection_rate"] * 100,
)
logger.info("Report written to %s", out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())