evalrx 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Steps 5-7 starting template: fit a justified model for a binary outcome, test
|
|
3
|
+
per-variable significance, run goodness-of-fit diagnostics, and visualize results.
|
|
4
|
+
|
|
5
|
+
Given a dataframe, a binary outcome column, a list of predictor columns, and an
|
|
6
|
+
optional cluster column (for repeated/clustered observations):
|
|
7
|
+
- fits logistic regression (statsmodels Logit) when there's no clustering
|
|
8
|
+
- for clustered data, python's mixed-effects binary GLM support is thin -- this script
|
|
9
|
+
fits a cluster-robust-SE logistic regression as an approximation and prints a note
|
|
10
|
+
recommending scripts/fit_outcome_model.R (lme4::glmer / glmmTMB) for a true GLMM
|
|
11
|
+
- reports per-variable Wald and likelihood-ratio significance with odds ratios
|
|
12
|
+
- runs Hosmer-Lemeshow, VIF, ROC/AUC, and a calibration check
|
|
13
|
+
- produces a coefficient/odds-ratio plot, a predicted-probability curve, an ROC curve,
|
|
14
|
+
and a calibration plot
|
|
15
|
+
|
|
16
|
+
This is a template to adapt to the real column names and data -- not a black box.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python fit_outcome_model.py data.csv --outcome success --vars age,region,score \
|
|
20
|
+
[--cluster cohort] --out output/
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import statsmodels.api as sm
|
|
30
|
+
import statsmodels.formula.api as smf
|
|
31
|
+
from scipy.stats import chi2
|
|
32
|
+
from sklearn.metrics import roc_auc_score, roc_curve
|
|
33
|
+
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fit_model(df: pd.DataFrame, outcome: str, predictors: list[str], cluster: str | None):
|
|
37
|
+
formula = f"{outcome} ~ " + " + ".join(predictors)
|
|
38
|
+
if cluster is None:
|
|
39
|
+
model = smf.logit(formula, data=df).fit(disp=0)
|
|
40
|
+
print("Fit plain logistic regression (observations treated as independent).")
|
|
41
|
+
else:
|
|
42
|
+
model = smf.logit(formula, data=df).fit(
|
|
43
|
+
disp=0, cov_type="cluster", cov_kwds={"groups": df[cluster]}
|
|
44
|
+
)
|
|
45
|
+
print(f"Observations are clustered by '{cluster}': fit logistic regression with "
|
|
46
|
+
f"cluster-robust standard errors as an approximation.")
|
|
47
|
+
print("For a full mixed-effects logistic regression (random intercept/slope for "
|
|
48
|
+
f"'{cluster}'), use fit_outcome_model.R instead (lme4::glmer / glmmTMB) -- "
|
|
49
|
+
"python's support for mixed binary GLMs is limited.")
|
|
50
|
+
return model
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def per_variable_significance(df: pd.DataFrame, outcome: str, predictors: list[str],
|
|
54
|
+
full_model) -> pd.DataFrame:
|
|
55
|
+
"""Wald p-value/odds ratio per model coefficient (one row per dummy level for a
|
|
56
|
+
categorical predictor), plus a likelihood-ratio test dropping each *variable* (all
|
|
57
|
+
of its levels at once) so marginal-screening signal (steps 3-4) can be compared
|
|
58
|
+
against adjusted significance."""
|
|
59
|
+
conf_int = full_model.conf_int()
|
|
60
|
+
rows = []
|
|
61
|
+
for v in predictors:
|
|
62
|
+
# a categorical predictor expands to one coefficient per non-reference level,
|
|
63
|
+
# named like "region[T.south]" by patsy -- match all of them, not just "v" itself
|
|
64
|
+
coef_names = [c for c in full_model.params.index if c == v or c.startswith(f"{v}[")]
|
|
65
|
+
|
|
66
|
+
reduced_predictors = [p for p in predictors if p != v]
|
|
67
|
+
reduced_formula = f"{outcome} ~ " + (" + ".join(reduced_predictors) if reduced_predictors else "1")
|
|
68
|
+
reduced_model = smf.logit(reduced_formula, data=df).fit(disp=0)
|
|
69
|
+
lrt_stat = 2 * (full_model.llf - reduced_model.llf)
|
|
70
|
+
lrt_p = chi2.sf(lrt_stat, df=len(coef_names))
|
|
71
|
+
|
|
72
|
+
for coef_name in coef_names:
|
|
73
|
+
rows.append({
|
|
74
|
+
"variable": coef_name,
|
|
75
|
+
"odds_ratio": np.exp(full_model.params[coef_name]),
|
|
76
|
+
"ci_low": np.exp(conf_int.loc[coef_name, 0]),
|
|
77
|
+
"ci_high": np.exp(conf_int.loc[coef_name, 1]),
|
|
78
|
+
"wald_p": full_model.pvalues[coef_name],
|
|
79
|
+
"lrt_p": lrt_p, # shared across all levels of this variable
|
|
80
|
+
})
|
|
81
|
+
return pd.DataFrame(rows)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def hosmer_lemeshow(y_true, y_pred, n_bins: int = 10) -> tuple[float, float]:
|
|
85
|
+
df = pd.DataFrame({"y": y_true, "p": y_pred})
|
|
86
|
+
df["bin"] = pd.qcut(df["p"], n_bins, duplicates="drop")
|
|
87
|
+
grouped = df.groupby("bin")
|
|
88
|
+
obs = grouped["y"].sum()
|
|
89
|
+
exp = grouped["p"].sum()
|
|
90
|
+
n = grouped["y"].count()
|
|
91
|
+
stat = (((obs - exp) ** 2) / (exp * (1 - exp / n))).sum()
|
|
92
|
+
p_value = chi2.sf(stat, df=n_bins - 2)
|
|
93
|
+
return float(stat), float(p_value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def vif_table(df: pd.DataFrame, predictors: list[str]) -> pd.DataFrame:
|
|
97
|
+
numeric_predictors = [p for p in predictors if pd.api.types.is_numeric_dtype(df[p])]
|
|
98
|
+
X = sm.add_constant(df[numeric_predictors].dropna())
|
|
99
|
+
return pd.DataFrame({
|
|
100
|
+
"variable": X.columns,
|
|
101
|
+
"vif": [variance_inflation_factor(X.values, i) for i in range(X.shape[1])],
|
|
102
|
+
}).query("variable != 'const'")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def plot_coefficients(sig_table: pd.DataFrame, out_dir: str) -> None:
|
|
106
|
+
fig, ax = plt.subplots(figsize=(5, 0.5 * len(sig_table) + 1))
|
|
107
|
+
y = np.arange(len(sig_table))
|
|
108
|
+
ax.errorbar(sig_table["odds_ratio"], y,
|
|
109
|
+
xerr=[sig_table["odds_ratio"] - sig_table["ci_low"], sig_table["ci_high"] - sig_table["odds_ratio"]],
|
|
110
|
+
fmt="o", color="steelblue")
|
|
111
|
+
ax.axvline(1, color="gray", linestyle="--", linewidth=1)
|
|
112
|
+
ax.set_yticks(y)
|
|
113
|
+
ax.set_yticklabels(sig_table["variable"])
|
|
114
|
+
ax.set_xlabel("Odds ratio (95% CI)")
|
|
115
|
+
ax.spines[["top", "right"]].set_visible(False)
|
|
116
|
+
fig.tight_layout()
|
|
117
|
+
fig.savefig(os.path.join(out_dir, "coefficient_plot.pdf"))
|
|
118
|
+
plt.close(fig)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def plot_roc(y_true, y_pred, out_dir: str) -> float:
|
|
122
|
+
fpr, tpr, _ = roc_curve(y_true, y_pred)
|
|
123
|
+
auc = roc_auc_score(y_true, y_pred)
|
|
124
|
+
fig, ax = plt.subplots(figsize=(4, 4))
|
|
125
|
+
ax.plot(fpr, tpr, color="steelblue", label=f"AUC = {auc:.3f}")
|
|
126
|
+
ax.plot([0, 1], [0, 1], color="gray", linestyle="--")
|
|
127
|
+
ax.set_xlabel("False positive rate")
|
|
128
|
+
ax.set_ylabel("True positive rate")
|
|
129
|
+
ax.legend()
|
|
130
|
+
ax.spines[["top", "right"]].set_visible(False)
|
|
131
|
+
fig.tight_layout()
|
|
132
|
+
fig.savefig(os.path.join(out_dir, "roc_curve.pdf"))
|
|
133
|
+
plt.close(fig)
|
|
134
|
+
return float(auc)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def plot_calibration(y_true, y_pred, out_dir: str, n_bins: int = 10) -> None:
|
|
138
|
+
df = pd.DataFrame({"y": y_true, "p": y_pred})
|
|
139
|
+
df["bin"] = pd.qcut(df["p"], n_bins, duplicates="drop")
|
|
140
|
+
grouped = df.groupby("bin").agg(observed=("y", "mean"), predicted=("p", "mean"))
|
|
141
|
+
fig, ax = plt.subplots(figsize=(4, 4))
|
|
142
|
+
ax.plot(grouped["predicted"], grouped["observed"], "o-", color="steelblue")
|
|
143
|
+
ax.plot([0, 1], [0, 1], color="gray", linestyle="--")
|
|
144
|
+
ax.set_xlabel("Mean predicted probability")
|
|
145
|
+
ax.set_ylabel("Observed proportion")
|
|
146
|
+
ax.spines[["top", "right"]].set_visible(False)
|
|
147
|
+
fig.tight_layout()
|
|
148
|
+
fig.savefig(os.path.join(out_dir, "calibration_plot.pdf"))
|
|
149
|
+
plt.close(fig)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def run(csv_path: str, outcome: str, predictors: list[str], out_dir: str, cluster: str | None) -> None:
|
|
153
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
154
|
+
df = pd.read_csv(csv_path).dropna(subset=[outcome] + predictors)
|
|
155
|
+
|
|
156
|
+
model = fit_model(df, outcome, predictors, cluster)
|
|
157
|
+
print(model.summary())
|
|
158
|
+
|
|
159
|
+
sig_table = per_variable_significance(df, outcome, predictors, model)
|
|
160
|
+
sig_table.to_csv(os.path.join(out_dir, "per_variable_significance.csv"), index=False)
|
|
161
|
+
print("\nPer-variable significance (adjusted for other predictors):\n", sig_table)
|
|
162
|
+
|
|
163
|
+
y_pred = model.predict(df)
|
|
164
|
+
hl_stat, hl_p = hosmer_lemeshow(df[outcome], y_pred)
|
|
165
|
+
print(f"\nHosmer-Lemeshow: chi2={hl_stat:.3f}, p={hl_p:.4g}")
|
|
166
|
+
|
|
167
|
+
vif = vif_table(df, predictors)
|
|
168
|
+
vif.to_csv(os.path.join(out_dir, "vif.csv"), index=False)
|
|
169
|
+
print("\nVIF:\n", vif)
|
|
170
|
+
|
|
171
|
+
auc = plot_roc(df[outcome], y_pred, out_dir)
|
|
172
|
+
plot_calibration(df[outcome], y_pred, out_dir)
|
|
173
|
+
plot_coefficients(sig_table, out_dir)
|
|
174
|
+
print(f"\nAUC: {auc:.3f}")
|
|
175
|
+
print(f"Plots written to {out_dir}: coefficient_plot.pdf, roc_curve.pdf, calibration_plot.pdf")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
180
|
+
parser.add_argument("csv_path")
|
|
181
|
+
parser.add_argument("--outcome", required=True, help="binary outcome column name")
|
|
182
|
+
parser.add_argument("--vars", required=True, help="comma-separated predictor column names")
|
|
183
|
+
parser.add_argument("--cluster", default=None, help="optional clustering/grouping column")
|
|
184
|
+
parser.add_argument("--out", default="output")
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
run(args.csv_path, args.outcome, args.vars.split(","), args.out, args.cluster)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Steps 3-4 starting template: per-variable exploration vs. outcome, plus marginal
|
|
2
|
+
# variable screening.
|
|
3
|
+
#
|
|
4
|
+
# Given a dataframe, a binary outcome column, and a list of explanatory variables:
|
|
5
|
+
# - categorical variable -> contingency table vs. outcome + chi-square/Fisher's exact
|
|
6
|
+
# - continuous variable -> side-by-side boxplot vs. outcome + Welch's t-test / Mann-Whitney
|
|
7
|
+
# - optional conditioning variable -> repeat the above within each stratum
|
|
8
|
+
# - marginal screening -> univariate logistic regression per variable, ranked by p-value
|
|
9
|
+
#
|
|
10
|
+
# This is a template to adapt to the real column names and data -- not a black box.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# Rscript univariate_eda.R data.csv outcome "age,region,score" output/ cohort
|
|
14
|
+
|
|
15
|
+
suppressMessages({
|
|
16
|
+
library(dplyr)
|
|
17
|
+
library(ggplot2)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
infer_variable_types <- function(df, variables) {
|
|
21
|
+
sapply(variables, function(v) {
|
|
22
|
+
if (is.numeric(df[[v]]) && length(unique(df[[v]])) > 10) "continuous" else "categorical"
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
categorical_vs_outcome <- function(df, var, outcome) {
|
|
27
|
+
ct <- table(df[[var]], df[[outcome]])
|
|
28
|
+
expected <- suppressWarnings(chisq.test(ct)$expected)
|
|
29
|
+
if (any(expected < 5) && all(dim(ct) == 2)) {
|
|
30
|
+
test_used <- "fisher_exact"
|
|
31
|
+
p_value <- fisher.test(ct)$p.value
|
|
32
|
+
} else {
|
|
33
|
+
test_used <- "chi_square"
|
|
34
|
+
p_value <- suppressWarnings(chisq.test(ct)$p.value)
|
|
35
|
+
}
|
|
36
|
+
chi2 <- suppressWarnings(chisq.test(ct)$statistic)
|
|
37
|
+
n <- sum(ct)
|
|
38
|
+
r <- nrow(ct); k <- ncol(ct)
|
|
39
|
+
cramers_v <- sqrt((chi2 / n) / min(k - 1, r - 1))
|
|
40
|
+
list(variable = var, test = test_used, p_value = p_value, cramers_v = as.numeric(cramers_v),
|
|
41
|
+
contingency_table = ct)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
continuous_vs_outcome <- function(df, var, outcome) {
|
|
45
|
+
groups <- split(df[[var]], df[[outcome]])
|
|
46
|
+
a <- na.omit(groups[[1]]); b <- na.omit(groups[[2]])
|
|
47
|
+
normal_a <- shapiro.test(sample(a, min(length(a), 5000)))$p.value > 0.05
|
|
48
|
+
normal_b <- shapiro.test(sample(b, min(length(b), 5000)))$p.value > 0.05
|
|
49
|
+
if (normal_a && normal_b) {
|
|
50
|
+
test_used <- "welch_t"
|
|
51
|
+
test_result <- t.test(a, b)
|
|
52
|
+
} else {
|
|
53
|
+
test_used <- "mann_whitney_u"
|
|
54
|
+
test_result <- wilcox.test(a, b)
|
|
55
|
+
}
|
|
56
|
+
pooled_sd <- sqrt((sd(a)^2 + sd(b)^2) / 2)
|
|
57
|
+
cohens_d <- (mean(a) - mean(b)) / pooled_sd
|
|
58
|
+
list(variable = var, test = test_used, statistic = unname(test_result$statistic),
|
|
59
|
+
p_value = test_result$p.value, cohens_d = cohens_d)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
boxplot_vs_outcome <- function(df, var, outcome, out_dir, facet_on = NULL) {
|
|
63
|
+
p <- ggplot(df, aes(.data[[outcome]], .data[[var]])) + geom_boxplot(fill = "steelblue", alpha = 0.6) +
|
|
64
|
+
labs(title = var) + theme_minimal()
|
|
65
|
+
fname <- paste0("box_", var, "_vs_", outcome)
|
|
66
|
+
if (!is.null(facet_on)) {
|
|
67
|
+
p <- p + facet_wrap(vars(.data[[facet_on]]))
|
|
68
|
+
fname <- paste0(fname, "_by_", facet_on)
|
|
69
|
+
}
|
|
70
|
+
ggsave(file.path(out_dir, paste0(fname, ".pdf")), p, width = 5, height = 3.5)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
conditioned_analysis <- function(df, var, outcome, var_type, condition_on, out_dir) {
|
|
74
|
+
strata <- unique(na.omit(df[[condition_on]]))
|
|
75
|
+
rows <- lapply(strata, function(s) {
|
|
76
|
+
sub <- df[df[[condition_on]] == s, ]
|
|
77
|
+
if (var_type == "categorical") {
|
|
78
|
+
r <- categorical_vs_outcome(sub, var, outcome)
|
|
79
|
+
data.frame(stratum = s, test = r$test, p_value = r$p_value, effect_size = r$cramers_v)
|
|
80
|
+
} else {
|
|
81
|
+
r <- continuous_vs_outcome(sub, var, outcome)
|
|
82
|
+
data.frame(stratum = s, test = r$test, p_value = r$p_value, effect_size = r$cohens_d)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
if (var_type == "continuous") boxplot_vs_outcome(df, var, outcome, out_dir, facet_on = condition_on)
|
|
86
|
+
do.call(rbind, rows)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
marginal_screen <- function(df, outcome, variables) {
|
|
90
|
+
# the null model is refit on each variable's own non-missing subset below, since
|
|
91
|
+
# comparing a null model fit on all rows against a univariate model that drops rows
|
|
92
|
+
# with a missing predictor (mismatched sample sizes) invalidates the LRT
|
|
93
|
+
rows <- lapply(variables, function(v) {
|
|
94
|
+
tryCatch({
|
|
95
|
+
sub <- df[stats::complete.cases(df[, c(outcome, v)]), ]
|
|
96
|
+
null_model <- glm(as.formula(paste(outcome, "~ 1")), data = sub, family = binomial)
|
|
97
|
+
model <- glm(as.formula(paste(outcome, "~", v)), data = sub, family = binomial)
|
|
98
|
+
# likelihood-ratio test: a joint test across all dummy levels for a categorical
|
|
99
|
+
# variable, not just one coefficient's Wald p-value
|
|
100
|
+
lrt <- anova(null_model, model, test = "Chisq")
|
|
101
|
+
p_value <- lrt[["Pr(>Chi)"]][2]
|
|
102
|
+
aic_improvement <- AIC(null_model) - AIC(model)
|
|
103
|
+
data.frame(variable = v, p_value = p_value, aic_improvement = aic_improvement)
|
|
104
|
+
}, error = function(e) data.frame(variable = v, p_value = NA, aic_improvement = NA))
|
|
105
|
+
})
|
|
106
|
+
do.call(rbind, rows) %>% arrange(p_value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
run <- function(csv_path, outcome, variables, out_dir, condition_on = NULL) {
|
|
110
|
+
dir.create(out_dir, showWarnings = FALSE, recursive = TRUE)
|
|
111
|
+
df <- read.csv(csv_path)
|
|
112
|
+
var_types <- infer_variable_types(df, variables)
|
|
113
|
+
|
|
114
|
+
univariate_results <- list()
|
|
115
|
+
for (v in variables) {
|
|
116
|
+
if (var_types[[v]] == "categorical") {
|
|
117
|
+
result <- categorical_vs_outcome(df, v, outcome)
|
|
118
|
+
cat("\n", v, "(categorical) vs", outcome, ":\n")
|
|
119
|
+
print(result$contingency_table)
|
|
120
|
+
cat(sprintf(" %s: p=%.4g, Cramer's V=%.3f\n", result$test, result$p_value, result$cramers_v))
|
|
121
|
+
univariate_results[[v]] <- result[c("variable", "test", "p_value", "cramers_v")]
|
|
122
|
+
} else {
|
|
123
|
+
boxplot_vs_outcome(df, v, outcome, out_dir)
|
|
124
|
+
result <- continuous_vs_outcome(df, v, outcome)
|
|
125
|
+
cat("\n", v, "(continuous) vs", outcome, ":\n")
|
|
126
|
+
cat(sprintf(" %s: p=%.4g, Cohen's d=%.3f\n", result$test, result$p_value, result$cohens_d))
|
|
127
|
+
univariate_results[[v]] <- result
|
|
128
|
+
}
|
|
129
|
+
if (!is.null(condition_on)) {
|
|
130
|
+
cond <- conditioned_analysis(df, v, outcome, var_types[[v]], condition_on, out_dir)
|
|
131
|
+
cat(" conditioned on", condition_on, ":\n"); print(cond)
|
|
132
|
+
write.csv(cond, file.path(out_dir, paste0("conditioned_", v, "_on_", condition_on, ".csv")), row.names = FALSE)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
screen <- marginal_screen(df, outcome, variables)
|
|
137
|
+
write.csv(screen, file.path(out_dir, "marginal_screening.csv"), row.names = FALSE)
|
|
138
|
+
cat("\nMarginal screening (ranked by p-value):\n"); print(screen)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (sys.nframe() == 0) {
|
|
142
|
+
args <- commandArgs(trailingOnly = TRUE)
|
|
143
|
+
csv_path <- args[1]
|
|
144
|
+
outcome <- args[2]
|
|
145
|
+
variables <- strsplit(args[3], ",")[[1]]
|
|
146
|
+
out_dir <- ifelse(length(args) >= 4, args[4], "output")
|
|
147
|
+
condition_on <- if (length(args) >= 5) args[5] else NULL
|
|
148
|
+
run(csv_path, outcome, variables, out_dir, condition_on)
|
|
149
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Steps 3-4 starting template: per-variable exploration vs. outcome, plus marginal
|
|
3
|
+
variable screening.
|
|
4
|
+
|
|
5
|
+
Given a dataframe, a binary outcome column, and a list of explanatory variables:
|
|
6
|
+
- categorical variable -> contingency table vs. outcome + chi-square/Fisher's exact
|
|
7
|
+
- continuous variable -> side-by-side boxplot vs. outcome + Welch's t-test / Mann-Whitney
|
|
8
|
+
- optional conditioning variable -> repeat the above within each stratum
|
|
9
|
+
- marginal screening -> univariate logistic regression per variable, ranked by p-value
|
|
10
|
+
|
|
11
|
+
This is a template to adapt to the real column names and data -- not a black box.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python univariate_eda.py data.csv --outcome success --vars age,region,score \
|
|
15
|
+
--condition_on cohort --out output/
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
import statsmodels.formula.api as smf
|
|
25
|
+
from scipy import stats
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def infer_variable_types(df: pd.DataFrame, variables: list[str]) -> dict[str, str]:
|
|
29
|
+
types = {}
|
|
30
|
+
for v in variables:
|
|
31
|
+
if pd.api.types.is_numeric_dtype(df[v]) and df[v].nunique() > 10:
|
|
32
|
+
types[v] = "continuous"
|
|
33
|
+
else:
|
|
34
|
+
types[v] = "categorical"
|
|
35
|
+
return types
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def categorical_vs_outcome(df: pd.DataFrame, var: str, outcome: str) -> dict:
|
|
39
|
+
ct = pd.crosstab(df[var], df[outcome])
|
|
40
|
+
expected = stats.contingency.expected_freq(ct)
|
|
41
|
+
if (expected < 5).any():
|
|
42
|
+
_, p, _ = stats.fisher_exact(ct) if ct.shape == (2, 2) else (None, np.nan, None)
|
|
43
|
+
test_used = "fisher_exact"
|
|
44
|
+
else:
|
|
45
|
+
chi2, p, _, _ = stats.chi2_contingency(ct)
|
|
46
|
+
test_used = "chi_square"
|
|
47
|
+
n = ct.sum().sum()
|
|
48
|
+
phi2 = stats.chi2_contingency(ct)[0] / n
|
|
49
|
+
r, k = ct.shape
|
|
50
|
+
cramers_v = float(np.sqrt(phi2 / min(k - 1, r - 1))) if min(k - 1, r - 1) > 0 else np.nan
|
|
51
|
+
return {"variable": var, "test": test_used, "p_value": p, "cramers_v": cramers_v,
|
|
52
|
+
"contingency_table": ct}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def continuous_vs_outcome(df: pd.DataFrame, var: str, outcome: str) -> dict:
|
|
56
|
+
groups = df.groupby(outcome)[var]
|
|
57
|
+
labels = list(groups.groups.keys())
|
|
58
|
+
a, b = groups.get_group(labels[0]).dropna(), groups.get_group(labels[1]).dropna()
|
|
59
|
+
normal_a = stats.shapiro(a.sample(min(len(a), 5000)))[1] > 0.05
|
|
60
|
+
normal_b = stats.shapiro(b.sample(min(len(b), 5000)))[1] > 0.05
|
|
61
|
+
if normal_a and normal_b:
|
|
62
|
+
stat, p = stats.ttest_ind(a, b, equal_var=False)
|
|
63
|
+
test_used = "welch_t"
|
|
64
|
+
else:
|
|
65
|
+
stat, p = stats.mannwhitneyu(a, b)
|
|
66
|
+
test_used = "mann_whitney_u"
|
|
67
|
+
pooled_sd = np.sqrt(((a.std() ** 2) + (b.std() ** 2)) / 2)
|
|
68
|
+
cohens_d = (a.mean() - b.mean()) / pooled_sd if pooled_sd > 0 else np.nan
|
|
69
|
+
return {"variable": var, "test": test_used, "statistic": stat, "p_value": p, "cohens_d": cohens_d}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def boxplot_vs_outcome(df: pd.DataFrame, var: str, outcome: str, out_dir: str, facet_on: str | None = None) -> None:
|
|
73
|
+
if facet_on is None:
|
|
74
|
+
fig, ax = plt.subplots(figsize=(4, 3.5))
|
|
75
|
+
df.boxplot(column=var, by=outcome, ax=ax)
|
|
76
|
+
ax.set_title(var)
|
|
77
|
+
plt.suptitle("")
|
|
78
|
+
fig.tight_layout()
|
|
79
|
+
fig.savefig(os.path.join(out_dir, f"box_{var}_vs_{outcome}.pdf"))
|
|
80
|
+
plt.close(fig)
|
|
81
|
+
else:
|
|
82
|
+
strata = df[facet_on].dropna().unique()
|
|
83
|
+
fig, axes = plt.subplots(1, len(strata), figsize=(4 * len(strata), 3.5), sharey=True)
|
|
84
|
+
axes = np.atleast_1d(axes)
|
|
85
|
+
for ax, s in zip(axes, strata):
|
|
86
|
+
sub = df[df[facet_on] == s]
|
|
87
|
+
sub.boxplot(column=var, by=outcome, ax=ax)
|
|
88
|
+
ax.set_title(f"{facet_on} = {s}")
|
|
89
|
+
plt.suptitle(var)
|
|
90
|
+
fig.tight_layout()
|
|
91
|
+
fig.savefig(os.path.join(out_dir, f"box_{var}_vs_{outcome}_by_{facet_on}.pdf"))
|
|
92
|
+
plt.close(fig)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def conditioned_analysis(df: pd.DataFrame, var: str, outcome: str, var_type: str,
|
|
96
|
+
condition_on: str, out_dir: str) -> pd.DataFrame:
|
|
97
|
+
"""Repeat the marginal test within each stratum of `condition_on` to check for
|
|
98
|
+
confounding/interaction (does the relationship appear, vanish, or reverse?)."""
|
|
99
|
+
rows = []
|
|
100
|
+
for stratum, sub in df.groupby(condition_on):
|
|
101
|
+
if var_type == "categorical":
|
|
102
|
+
result = categorical_vs_outcome(sub, var, outcome)
|
|
103
|
+
rows.append({"stratum": stratum, "test": result["test"],
|
|
104
|
+
"p_value": result["p_value"], "effect_size": result["cramers_v"]})
|
|
105
|
+
else:
|
|
106
|
+
result = continuous_vs_outcome(sub, var, outcome)
|
|
107
|
+
rows.append({"stratum": stratum, "test": result["test"],
|
|
108
|
+
"p_value": result["p_value"], "effect_size": result["cohens_d"]})
|
|
109
|
+
if var_type == "continuous":
|
|
110
|
+
boxplot_vs_outcome(df, var, outcome, out_dir, facet_on=condition_on)
|
|
111
|
+
return pd.DataFrame(rows)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def marginal_screen(df: pd.DataFrame, outcome: str, variables: list[str]) -> pd.DataFrame:
|
|
115
|
+
"""Univariate logistic regression per variable, ranked by likelihood-ratio p-value
|
|
116
|
+
(a joint test across all dummy levels for a categorical variable, not just one
|
|
117
|
+
coefficient) and AIC improvement over the null model.
|
|
118
|
+
|
|
119
|
+
The null model is refit on each variable's own non-missing subset, since comparing
|
|
120
|
+
a null model fit on all rows against a univariate model that silently drops rows
|
|
121
|
+
with a missing predictor (mismatched sample sizes) invalidates the LRT."""
|
|
122
|
+
rows = []
|
|
123
|
+
for v in variables:
|
|
124
|
+
try:
|
|
125
|
+
sub = df[[outcome, v]].dropna()
|
|
126
|
+
null_model = smf.logit(f"{outcome} ~ 1", data=sub).fit(disp=0)
|
|
127
|
+
model = smf.logit(f"{outcome} ~ Q('{v}')", data=sub).fit(disp=0)
|
|
128
|
+
df_diff = model.df_model - null_model.df_model
|
|
129
|
+
lrt_stat = 2 * (model.llf - null_model.llf)
|
|
130
|
+
p_value = stats.chi2.sf(lrt_stat, df=df_diff) if df_diff > 0 else np.nan
|
|
131
|
+
aic_improvement = null_model.aic - model.aic
|
|
132
|
+
except Exception:
|
|
133
|
+
p_value, aic_improvement = np.nan, np.nan
|
|
134
|
+
rows.append({"variable": v, "p_value": p_value, "aic_improvement": aic_improvement})
|
|
135
|
+
return pd.DataFrame(rows).sort_values("p_value")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run(csv_path: str, outcome: str, variables: list[str], out_dir: str, condition_on: str | None) -> None:
|
|
139
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
140
|
+
df = pd.read_csv(csv_path)
|
|
141
|
+
var_types = infer_variable_types(df, variables)
|
|
142
|
+
|
|
143
|
+
univariate_results = []
|
|
144
|
+
for v in variables:
|
|
145
|
+
if var_types[v] == "categorical":
|
|
146
|
+
result = categorical_vs_outcome(df, v, outcome)
|
|
147
|
+
print(f"\n{v} (categorical) vs {outcome}:\n{result['contingency_table']}")
|
|
148
|
+
print(f" {result['test']}: p={result['p_value']:.4g}, Cramer's V={result['cramers_v']:.3f}")
|
|
149
|
+
univariate_results.append({k: v2 for k, v2 in result.items() if k != "contingency_table"})
|
|
150
|
+
else:
|
|
151
|
+
boxplot_vs_outcome(df, v, outcome, out_dir)
|
|
152
|
+
result = continuous_vs_outcome(df, v, outcome)
|
|
153
|
+
print(f"\n{v} (continuous) vs {outcome}:")
|
|
154
|
+
print(f" {result['test']}: p={result['p_value']:.4g}, Cohen's d={result['cohens_d']:.3f}")
|
|
155
|
+
univariate_results.append(result)
|
|
156
|
+
|
|
157
|
+
if condition_on:
|
|
158
|
+
cond = conditioned_analysis(df, v, outcome, var_types[v], condition_on, out_dir)
|
|
159
|
+
print(f" conditioned on {condition_on}:\n{cond}")
|
|
160
|
+
cond.to_csv(os.path.join(out_dir, f"conditioned_{v}_on_{condition_on}.csv"), index=False)
|
|
161
|
+
|
|
162
|
+
pd.DataFrame(univariate_results).to_csv(os.path.join(out_dir, "univariate_vs_outcome.csv"), index=False)
|
|
163
|
+
|
|
164
|
+
screen = marginal_screen(df, outcome, variables)
|
|
165
|
+
screen.to_csv(os.path.join(out_dir, "marginal_screening.csv"), index=False)
|
|
166
|
+
print("\nMarginal screening (ranked by p-value):\n", screen)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
171
|
+
parser.add_argument("csv_path")
|
|
172
|
+
parser.add_argument("--outcome", required=True, help="binary outcome column name")
|
|
173
|
+
parser.add_argument("--vars", required=True, help="comma-separated explanatory variable column names")
|
|
174
|
+
parser.add_argument("--condition_on", default=None, help="optional column to stratify/facet on")
|
|
175
|
+
parser.add_argument("--out", default="output")
|
|
176
|
+
args = parser.parse_args()
|
|
177
|
+
run(args.csv_path, args.outcome, args.vars.split(","), args.out, args.condition_on)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Bundled Agent Skills shipped with the package.
|
|
2
|
+
|
|
3
|
+
Bundled skills (eval-chart-style, nature-figure, evalrx-report-ui) are
|
|
4
|
+
applied BY DEFAULT on every skill-capable coding backend: claude/agy discover
|
|
5
|
+
them via the vendored ``<workdir>/.claude/skills/`` + the ``Skill`` tool, and
|
|
6
|
+
codex is pointed at the same vendored ``SKILL.md`` files through the workdir's
|
|
7
|
+
``AGENTS.md`` (it has no Skill tool). Pass ``--no-skills`` / empty ``skills``
|
|
8
|
+
to opt out.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
BUNDLED_SKILLS_DIR = Path(__file__).resolve().parent / "skills"
|
|
16
|
+
SKILL_BACKENDS = {"claude_code", "antigravity", "codex"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bundled_skill_paths() -> list[str]:
|
|
20
|
+
"""Absolute paths of bundled Agent-Skill directories containing ``SKILL.md``."""
|
|
21
|
+
if not BUNDLED_SKILLS_DIR.is_dir():
|
|
22
|
+
return []
|
|
23
|
+
return [
|
|
24
|
+
str(path)
|
|
25
|
+
for path in sorted(BUNDLED_SKILLS_DIR.iterdir())
|
|
26
|
+
if (path / "SKILL.md").is_file()
|
|
27
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CLI-agent runtime: sandboxing, code generation, providers, judge models, and skills.
|
|
2
|
+
|
|
3
|
+
Shared by ``evalrx.analysis`` and ``evalrx.eval_agent`` — this package
|
|
4
|
+
imports nothing from either, so it is safe for both to depend on it.
|
|
5
|
+
|
|
6
|
+
Submodules:
|
|
7
|
+
cli_types CliAgentConfig / CliAgentResult and BINARY_DEFAULTS
|
|
8
|
+
cli_runtime SubprocessRunner, ProcessRun, collect_py_files
|
|
9
|
+
cli_transcript CLI stdout post-processing (e.g. Claude stream-json rendering)
|
|
10
|
+
sandbox ExperimentSandbox — safe subprocess execution of generated code
|
|
11
|
+
factory create_sandbox() — subprocess/Docker sandbox backend selection
|
|
12
|
+
codegen CodegenRunner — the single boundary stages use to invoke a CLI
|
|
13
|
+
coding agent and harvest generated files
|
|
14
|
+
providers CLI coding-provider adapters (claude_code, codex, antigravity, ...)
|
|
15
|
+
judges CLI-backed judge model wrappers (ClaudeModel, AgyModel) +
|
|
16
|
+
liveness-probe autodetection (resolve_cli_judge)
|
|
17
|
+
skills Agent Skills resolution, sandbox installation, and prompt policy
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from evalrx.agent_runtime.cli_types import (
|
|
23
|
+
BINARY_DEFAULTS,
|
|
24
|
+
CliAgentConfig,
|
|
25
|
+
CliAgentResult,
|
|
26
|
+
)
|
|
27
|
+
from evalrx.agent_runtime.codegen import CodegenCodeResult, CodegenRunner
|
|
28
|
+
from evalrx.agent_runtime.factory import (
|
|
29
|
+
SandboxConfig,
|
|
30
|
+
SandboxFactoryConfig,
|
|
31
|
+
create_sandbox,
|
|
32
|
+
)
|
|
33
|
+
from evalrx.agent_runtime.json_shape import validate_json_shape
|
|
34
|
+
from evalrx.agent_runtime.judges import (
|
|
35
|
+
AgyModel,
|
|
36
|
+
ClaudeModel,
|
|
37
|
+
CodexModel,
|
|
38
|
+
ResolvedJudge,
|
|
39
|
+
resolve_cli_judge,
|
|
40
|
+
)
|
|
41
|
+
from evalrx.agent_runtime.providers import create_cli_agent
|
|
42
|
+
from evalrx.agent_runtime.sandbox import (
|
|
43
|
+
ExperimentSandbox,
|
|
44
|
+
SandboxProtocol,
|
|
45
|
+
SandboxResult,
|
|
46
|
+
parse_metrics,
|
|
47
|
+
validate_entry_point,
|
|
48
|
+
validate_entry_point_resolved,
|
|
49
|
+
)
|
|
50
|
+
from evalrx.agent_runtime.skills.prompt_policy import fences_hint, skills_hint
|
|
51
|
+
from evalrx.agent_runtime.skills.resolver import resolve_skill_paths
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"BINARY_DEFAULTS",
|
|
55
|
+
"CliAgentConfig",
|
|
56
|
+
"CliAgentResult",
|
|
57
|
+
"CodegenCodeResult",
|
|
58
|
+
"CodegenRunner",
|
|
59
|
+
"SandboxConfig",
|
|
60
|
+
"SandboxFactoryConfig",
|
|
61
|
+
"create_sandbox",
|
|
62
|
+
"AgyModel",
|
|
63
|
+
"ClaudeModel",
|
|
64
|
+
"CodexModel",
|
|
65
|
+
"ResolvedJudge",
|
|
66
|
+
"resolve_cli_judge",
|
|
67
|
+
"create_cli_agent",
|
|
68
|
+
"ExperimentSandbox",
|
|
69
|
+
"SandboxProtocol",
|
|
70
|
+
"SandboxResult",
|
|
71
|
+
"parse_metrics",
|
|
72
|
+
"validate_entry_point",
|
|
73
|
+
"validate_entry_point_resolved",
|
|
74
|
+
"resolve_skill_paths",
|
|
75
|
+
"skills_hint",
|
|
76
|
+
"fences_hint",
|
|
77
|
+
"validate_json_shape",
|
|
78
|
+
]
|