Files
skillfactor-pipeline/pipeline/evidence_report.py
skillfactor-pipeline af05383bb1 chore: v1 baseline - full 3,039-package catalog with AI-skill enrichment and O*NET backfill
Snapshot before the quality program (relevance gates, tiered mapping,
QA linter). v1 is the immutable before/after reference; evidence crawl
was at ~175/3039 occupations when tagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDKeXvpT6tENSvyQGLV1Uq
2026-07-09 05:32:49 +02:00

189 lines
7.8 KiB
Python

"""Evidence-Auswertung: coverage report over the job-ad evidence stage.
Reads progress.json + the MSSQL evidence store and writes
docs/evidence-report.md — runnable at any time (interim or final state).
Sections:
- coverage: fetched/extracted/aggregated/failed/thin-market per status
- volume: ads, unique employers, entities by type
- budget: JSearch requests spent vs. lifetime cap
- distributions: seniority, country, top occupations by ads
- extraction engines: Claude (in-session, flagship + QA) vs Ollama (mass run)
Usage: python pipeline/evidence_report.py [--push]
--push additionally commits the report into skills-core/marketplace
as EVIDENCE.md (uses p4_publish push helpers).
"""
import json
import os
import sys
from collections import Counter
from datetime import date
sys.path.insert(0, os.path.dirname(__file__))
import progress
from db import connect
BASE = os.path.join(os.path.dirname(__file__), "..")
OUT = os.path.join(BASE, "docs", "evidence-report.md")
# The recruiter flagship (350 ads) was extracted in-session by Claude before
# the local mass pipeline existed; everything after runs on Ollama gemma3:27b.
CLAUDE_EXTRACTED_SLUGS = {"recruitment-consultant"}
def q(cur, sql, *args):
cur.execute(sql, *args)
return cur.fetchall()
def main():
state = progress.load()
occ = state["occupations"]
ev = Counter(s.get("evidence") for s in occ.values())
ads_per_occ = {slug: s.get("ads", 0) for slug, s in occ.items() if s.get("ads")}
thin = [(slug, s.get("error", "")) for slug, s in occ.items()
if s.get("evidence") == "failed" and "thin market" in (s.get("error") or "")]
other_failed = [(slug, s.get("error", "")) for slug, s in occ.items()
if s.get("evidence") == "failed" and "thin market" not in (s.get("error") or "")]
cn = connect()
cur = cn.cursor()
n_jobs = q(cur, "SELECT COUNT(*) FROM evidence_job")[0][0]
n_occ_jobs = q(cur, "SELECT COUNT(DISTINCT occupation_slug) FROM evidence_job")[0][0]
n_emp = q(cur, "SELECT COUNT(DISTINCT employer) FROM evidence_job WHERE employer IS NOT NULL AND employer<>''")[0][0]
n_ent = q(cur, "SELECT COUNT(*) FROM evidence_entity")[0][0]
ent_types = q(cur, "SELECT entity_type, COUNT(*) FROM evidence_entity GROUP BY entity_type ORDER BY 2 DESC")
seniority = q(cur, "SELECT ISNULL(NULLIF(seniority,''),'n/a'), COUNT(*) FROM evidence_job GROUP BY ISNULL(NULLIF(seniority,''),'n/a') ORDER BY 2 DESC")
country = q(cur, "SELECT ISNULL(NULLIF(country,''),'?'), COUNT(*) FROM evidence_job GROUP BY ISNULL(NULLIF(country,''),'?') ORDER BY 2 DESC")
top_occ = q(cur, "SELECT TOP 20 occupation_slug, COUNT(*) FROM evidence_job GROUP BY occupation_slug ORDER BY 2 DESC")
cn.close()
claude_jobs = sum(n for s, n in
[(slug, ads_per_occ.get(slug, 0)) for slug in CLAUDE_EXTRACTED_SLUGS])
# recruiter ads live in the DB — count them exactly
cn = connect(); cur = cn.cursor()
claude_db = q(cur, "SELECT COUNT(*) FROM evidence_job WHERE occupation_slug IN ('recruitment-consultant')")[0][0]
cn.close()
ollama_db = n_jobs - claude_db
spent = state.get("jsearch_requests_total", 0)
cap = progress.JSEARCH_BUDGET_TOTAL
total_occ = len(occ)
L = []
L.append(f"# Job-ad evidence — coverage report")
L.append("")
L.append(f"_Generated {date.today().isoformat()} by `pipeline/evidence_report.py`. "
f"Re-run any time for the current state._")
L.append("")
L.append("## Coverage")
L.append("")
L.append("| Evidence status | Occupations |")
L.append("|---|---|")
for k in ("done", "fetching", "extracting", "aggregating", "failed", "pending"):
if ev.get(k):
L.append(f"| {k} | {ev[k]} |")
L.append(f"| **total catalog** | **{total_occ}** |")
L.append("")
L.append(f"- Occupations with ads in the evidence store: **{n_occ_jobs}**")
L.append(f"- Thin markets (fewer than 5 usable ads): **{len(thin)}**")
L.append(f"- Failed for other reasons: **{len(other_failed)}**")
L.append("")
L.append("## Volume")
L.append("")
L.append(f"| Metric | Value |")
L.append(f"|---|---|")
L.append(f"| Job ads stored | {n_jobs:,} |")
L.append(f"| Distinct employers | {n_emp:,} |")
L.append(f"| Extracted entities | {n_ent:,} |")
for t, n in ent_types:
L.append(f"| — {t} | {n:,} |")
L.append("")
L.append("## API budget")
L.append("")
L.append(f"- JSearch requests spent: **{spent:,} / {cap:,}** "
f"({round(100.0*spent/cap, 1)} % of the lifetime budget)")
L.append("- Every request is counted BEFORE the HTTP call "
"(`progress.spend_request`, hard cap).")
L.append("")
L.append("## Distributions")
L.append("")
L.append("**Seniority** (per ad):")
L.append("")
L.append("| Seniority | Ads |")
L.append("|---|---|")
for s, n in seniority:
L.append(f"| {s} | {n:,} |")
L.append("")
L.append("**Country**:")
L.append("")
L.append("| Country | Ads |")
L.append("|---|---|")
for c, n in country:
L.append(f"| {c} | {n:,} |")
L.append("")
L.append("**Top 20 occupations by ads:**")
L.append("")
L.append("| Occupation | Ads |")
L.append("|---|---|")
for slug, n in top_occ:
L.append(f"| {slug} | {n:,} |")
L.append("")
L.append("## Extraction engines")
L.append("")
L.append("| Engine | Ads extracted | Share | Role |")
L.append("|---|---|---|---|")
tot = max(1, n_jobs)
L.append(f"| Claude (in-session) | {claude_db:,} | {round(100.0*claude_db/tot,1)} % | "
f"flagship package (recruitment-consultant), prompt/validator design, "
f"2 % QA spot checks on every batch |")
L.append(f"| Ollama gemma3:27b (RTX-3090 box) | {ollama_db:,} | {round(100.0*ollama_db/tot,1)} % | "
f"mass extraction of the full catalog (`extract_local.py`, "
f"strict JSON schema, validator with final word) |")
L.append("")
L.append("_Thin-market occupations (kept for transparency):_")
L.append("")
for slug, err in sorted(thin)[:50]:
L.append(f"- {slug}{err}")
if len(thin) > 50:
L.append(f"- … and {len(thin)-50} more")
L.append("")
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(L))
print(f"report written: {OUT}")
print(f"coverage: {n_occ_jobs}/{total_occ} occupations, {n_jobs} ads, "
f"{n_ent} entities, budget {spent}/{cap}")
if "--push" in sys.argv:
import shutil, tempfile
import p4_publish as p4
tmp = tempfile.mkdtemp(prefix="sfev_")
try:
# marketplace repo: add/refresh EVIDENCE.md next to README
import subprocess
url = p4.GITEA_URL.replace("://", f"://gitadmin:{p4.TOKEN}@") + "/skills-core/marketplace.git"
subprocess.run(["git", "clone", "--depth", "1", url, tmp],
check=True, capture_output=True, text=True)
shutil.copy(OUT, os.path.join(tmp, "EVIDENCE.md"))
def g(*a):
subprocess.run(["git", *a], cwd=tmp, check=True, capture_output=True, text=True)
g("config", "user.name", "skillfactor-pipeline")
g("config", "user.email", "pipeline@noreply.zeiterfassung.cloud")
g("add", "-A")
r = subprocess.run(["git", "commit", "-m", "docs: evidence coverage report"],
cwd=tmp, capture_output=True, text=True)
if r.returncode == 0:
g("push", "origin", "HEAD")
print("EVIDENCE.md pushed to skills-core/marketplace")
else:
print("no changes to push")
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
main()