Make the conditional baseline XGBoost over a full feature set#68
Conversation
The conditional baseline was a logistic regression over two cosine-similarity features. That understated how well a cheap model predicts individuals: it ignored panel covariates entirely, so on a Pew climate question it trailed the LLM twin and read as "the twin adds value" when a stronger cheap model would not. Replace it with an XGBoost classifier over the full available feature set: - raw question and option embeddings (so the model generalizes across questions), - one-hot respondent covariates from panel metadata (party, region, age — the demographic signal cosine similarity alone can't recover), - embedding-similarity scalars vs the leave-one-question-out respondent profile, - the option index. Training stays leave-one-question-out, so the baseline never sees the target question's labels and remains deployable, not an oracle. On Pew w158 the new baseline reaches 0.802 accuracy / 0.738 p(actual), beating the GPT-5.5 twin (0.735 / 0.658) on both — the twin only edges it on NLL. That is the point of a strong baseline: it makes "the twin adds individual signal" a claim you have to earn. Also adds a local sentence-transformers embedder so the baseline runs with no API key (`--embedder sentence-transformers`), and pins the deps behind extras: `zwill[local-embeddings]` (embeddings) and `zwill[conditional-baseline]` (xgboost + embeddings). Both are lazy-imported with actionable install hints. Docs (module docstring, report appendix, interpreting-results and agent-workflow guides) updated to describe the XGBoost + covariate design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR replaces the conditional-baseline logistic regression (two cosine-similarity features) with an XGBoost classifier trained over the full feature set — raw question/option embeddings, one-hot respondent covariates, and embedding-similarity scalars — while adding a local sentence-transformers embedder so the baseline can run without any API key.
Confidence Score: 4/5Safe to merge. The core training and prediction logic is well-structured, leave-one-question-out isolation is maintained, and the new covariate/embedder paths are covered by tests. The XGBoost replacement is substantive but the design is correct: feature construction is consistent across training and inference, binary labels flow correctly through predict_proba, and the feature version bump invalidates prior cached results. The stale --l2 help text and the missing covariate fingerprint in baseline_job_id are real but non-blocking. zwill/twin_baseline.py (baseline_job_id hash, n_jobs value) and zwill/cli_parser.py (--l2 help text) deserve a second look before merging. Important Files Changed
|
| reg_lambda=max(1.0, float(l2)), | ||
| tree_method="hist", | ||
| eval_metric="logloss", | ||
| n_jobs=0, |
There was a problem hiding this comment.
n_jobs=0 is XGBoost-specific: it maps to "use all available threads", which is correct, but most readers familiar with the scikit-learn API expect n_jobs=-1 for that behaviour (n_jobs=0 would raise a ValueError in sklearn). Using -1 is the idiomatic cross-library convention and avoids the ambiguity.
| n_jobs=0, | |
| n_jobs=-1, |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What
Replaces the conditional-baseline model (logistic regression over two cosine-similarity features) with an XGBoost classifier over the full available feature set, and adds a local, API-key-free embedding backend so the baseline can run anywhere.
The feature set (per candidate option)
Training stays leave-one-question-out: the model never sees the target question's labels, so the baseline is deployable, not an oracle.
Why
The old baseline ignored covariates entirely, so it understated how well a cheap model predicts individuals — making the LLM twin look better than it is. A good baseline should be the honest bar.
On Pew w158 the new baseline beats the GPT-5.5 twin on two of three metrics:
The twin now has to earn the claim that it adds individual-level signal.
Also in this PR
--embedder sentence-transformers) — the conditional baseline runs with no API key.autofalls back to it when no OpenAI/Expected Parrot key is present.zwill[local-embeddings](embeddings) andzwill[conditional-baseline](xgboost + embeddings). Both lazy-imported with actionable install hints.Testing
pytest— 242 passed (addstest_baseline_uses_respondent_covariates, which proves a covariate-only signal lifts the baseline well above chance, and the ST-embedder resolver test).ruff check zwill/ tests/clean.🤖 Generated with Claude Code