Spaces:
Running
Running
| """Decision engine: run every analysis and synthesize one verdict. | |
| Combines the frequentist test, bootstrap CI, power analysis, CUPED, mSPRT, | |
| Bayesian posterior, and per-cluster correction into an ``AnalysisReport`` that | |
| the Streamlit UI and the PDF report both render. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| import pandas as pd | |
| from core.data import JobCorpus | |
| from core.types import ScoringResult, TestResult | |
| from stats.bayesian import BayesResult, beta_binomial | |
| from stats.cuped import CupedResult, build_job_covariates, cuped_adjust | |
| from stats.frequentist import ( | |
| BootstrapCI, | |
| bootstrap_ci, | |
| choose_and_test, | |
| cohens_d, | |
| paired_t_test, | |
| ) | |
| from stats.multiple_comparisons import per_cluster_analysis | |
| from stats.power import achieved_power, mde_table, required_sample_size | |
| from stats.sequential import SequentialResult, msprt | |
| class Verdict: | |
| winner: str # "A" | "B" | "tie" | |
| mean_delta: float | |
| ci_low: float | |
| ci_high: float | |
| p_value: float | |
| significant: bool | |
| cohens_d: float | |
| confidence: str # "high" | "moderate" | "low" | |
| headline: str | |
| class AnalysisReport: | |
| n_jobs: int | |
| primary_test: TestResult | |
| normality: TestResult | |
| bootstrap: BootstrapCI | |
| cohens_d: float | |
| required_n_80: float | |
| achieved_power: float | |
| mde: pd.DataFrame | |
| cuped: CupedResult | |
| cuped_test: TestResult | |
| sequential: SequentialResult | |
| bayes: BayesResult | |
| per_cluster: pd.DataFrame | |
| gaps: dict | |
| differentiators: dict | |
| verdict: Verdict | |
| scores_summary: dict | |
| def _confidence(p: float, prob_b: float) -> str: | |
| extreme = max(prob_b, 1 - prob_b) | |
| if p < 0.01 and extreme > 0.99: | |
| return "high" | |
| if p < 0.05 and extreme > 0.95: | |
| return "moderate" | |
| return "low" | |
| def build_verdict( | |
| deltas: np.ndarray, | |
| primary: TestResult, | |
| bootstrap: BootstrapCI, | |
| bayes: BayesResult, | |
| ) -> Verdict: | |
| mean = float(np.mean(deltas)) | |
| p = primary.pvalue | |
| ci_excludes_zero = not (bootstrap.bca_low <= 0.0 <= bootstrap.bca_high) | |
| significant = (p < 0.05) and ci_excludes_zero | |
| winner = "tie" | |
| if significant and mean > 0: | |
| winner = "B" | |
| elif significant and mean < 0: | |
| winner = "A" | |
| d = cohens_d(deltas) | |
| conf = _confidence(p, bayes.prob_b_beats_a) | |
| lo, hi = bootstrap.bca_low * 100, bootstrap.bca_high * 100 # cosine pts -> "points" | |
| if winner == "tie": | |
| headline = ( | |
| f"No decisive winner: B differs from A by {mean * 100:+.2f} points " | |
| f"(95% CI [{lo:.2f}, {hi:.2f}], p={p:.3g}) — the interval includes zero." | |
| ) | |
| else: | |
| headline = ( | |
| f"Resume {winner} wins by {abs(mean) * 100:.2f} points " | |
| f"(95% CI [{lo:.2f}, {hi:.2f}], p={p:.3g})." | |
| ) | |
| return Verdict( | |
| winner, mean, bootstrap.bca_low, bootstrap.bca_high, p, significant, d, conf, headline | |
| ) | |
| def analyze( | |
| scoring: ScoringResult, | |
| corpus: JobCorpus, | |
| resume_a_text: str = "", | |
| resume_b_text: str = "", | |
| ) -> AnalysisReport: | |
| """Full pipeline on one A-vs-B scoring result.""" | |
| deltas = scoring.deltas | |
| n = scoring.n_jobs | |
| primary, normality = choose_and_test(deltas) | |
| boot = bootstrap_ci(deltas) | |
| d = cohens_d(deltas) | |
| req_n = required_sample_size(d) | |
| power = achieved_power(n, d) | |
| mde = mde_table(n) | |
| desc_len = corpus.jobs["description"].fillna("").str.len().to_numpy(dtype=float) | |
| covariates = build_job_covariates(scoring.cluster_ids, desc_len) | |
| cuped = cuped_adjust(deltas, covariates) | |
| cuped_test = paired_t_test(cuped.adjusted) | |
| sequential = msprt(deltas) | |
| bayes = beta_binomial(deltas) | |
| per_cluster = per_cluster_analysis(deltas, scoring.cluster_ids, corpus.cluster_names) | |
| from core.gaps import cluster_differentiators, cluster_gaps | |
| gaps = cluster_gaps(corpus, per_cluster, resume_a_text, resume_b_text) | |
| differentiators = cluster_differentiators(corpus, per_cluster, resume_a_text, resume_b_text) | |
| verdict = build_verdict(deltas, primary, boot, bayes) | |
| scores_summary = { | |
| "mean_a": float(scoring.scores_a.mean()), | |
| "mean_b": float(scoring.scores_b.mean()), | |
| "mean_delta": float(deltas.mean()), | |
| "std_delta": float(deltas.std(ddof=1)), | |
| "pct_jobs_b_wins": float(np.mean(deltas > 0) * 100), | |
| } | |
| return AnalysisReport( | |
| n_jobs=n, | |
| primary_test=primary, | |
| normality=normality, | |
| bootstrap=boot, | |
| cohens_d=d, | |
| required_n_80=req_n, | |
| achieved_power=power, | |
| mde=mde, | |
| cuped=cuped, | |
| cuped_test=cuped_test, | |
| sequential=sequential, | |
| bayes=bayes, | |
| per_cluster=per_cluster, | |
| gaps=gaps, | |
| differentiators=differentiators, | |
| verdict=verdict, | |
| scores_summary=scores_summary, | |
| ) | |