Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1246ea5
add semi-synthetic causal simulator with observed treatment
kirilklein Apr 3, 2026
bcbbd41
merge baseline/longitudinal coefficients into single dict
kirilklein Apr 3, 2026
b42dca6
fix edge cases, add azure components and docs
kirilklein Apr 3, 2026
187bda6
vectorize feature functions and add public calibration API
kirilklein Apr 3, 2026
515d8df
fix per-shard standardization: use two-pass global z-scoring
kirilklein Apr 3, 2026
50dd4f5
fix counterfactual AUC: score against matching labels
kirilklein Apr 3, 2026
d7bb391
revert counterfactual AUC change: score against factual outcome
kirilklein Apr 3, 2026
351c22b
add NaN fallback for event recency when no events exist
kirilklein Apr 3, 2026
e962f8b
fix ruff format for oracle_features.py
kirilklein Apr 3, 2026
86d83dc
add semi-synthetic multi-run study (N outer x K inner) for azure
kirilklein Jun 26, 2026
5d187a5
fail fast on unknown feature names in simulation config
kirilklein Jun 26, 2026
3ec9ca0
make coverage badge step non-blocking in CI
kirilklein Jun 26, 2026
6b79409
read index dates from a cohort artifact instead of raw MEDS
kirilklein Jun 27, 2026
b358ef6
add study summarizer (bias / SD / SE-calibration / coverage)
kirilklein Jun 27, 2026
55470e8
restructure study to fixed-cohort flow with bootstrap refits + baseline
kirilklein Jun 27, 2026
7500fe4
point study cohort at adult_diab2/v01
kirilklein Jun 27, 2026
1cc61c5
fix cohort path segment: cohorts/adult_diab2/v01
kirilklein Jun 27, 2026
1369342
take observed treatment from the cohort, not a MEDS exposure code
kirilklein Jun 28, 2026
1edab37
fix semisynthetic bootstrap uncertainty workflow
kirilklein Jul 21, 2026
18e2fa5
add training-fold bootstrap for k-refit uncertainty
kirilklein Jul 21, 2026
0444221
format with ruff to match ci
kirilklein Aug 5, 2026
3fffbec
register semisynthetic simulate and calibrate azure jobs
kirilklein Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/docstring_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:

- name: Create Dynamic Coverage Badge
if: always() # Always try to create badge
continue-on-error: true # Badge update (gist auth) must not fail the check
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }} # Your secret token with the gist scope
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/utest_coverage_badge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:

- name: Create Dynamic Coverage Badge
if: always() # Always try to create badge
continue-on-error: true # Badge update (gist auth) must not fail the check
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
Expand Down
20 changes: 20 additions & 0 deletions corebehrt/azure/components/calibrate_semisynthetic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from corebehrt.azure.util import job

INPUTS = {
"data": {"type": "uri_folder"},
}

OUTPUTS = {
"outcomes": {"type": "uri_folder"},
}


if __name__ == "__main__":
from corebehrt.main_causal import calibrate_semisynthetic

job.run_main(
"calibrate_semisynthetic",
calibrate_semisynthetic.main_calibrate,
INPUTS,
OUTPUTS,
)
51 changes: 51 additions & 0 deletions corebehrt/azure/components/run_semisynthetic_study.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Azure component for the semi-synthetic simulation study (one outer run per job)."""

import shlex

from corebehrt.azure.util import job

INPUTS = {
"meds": {"type": "uri_folder"},
"features": {"type": "uri_folder"},
"tokenized": {"type": "uri_folder"},
"pretrain_model": {"type": "uri_folder"},
"cohort": {"type": "uri_folder"},
}

OUTPUTS = {
"results": {"type": "uri_folder"}, # Output dir for this outer run
}


def main_run_study(config_path):
"""Translate the job config into run_study CLI arguments and run it."""
from corebehrt.modules.setup.config import load_config

cfg = load_config(config_path)

args = [
"--meds",
cfg.paths.meds,
"--features",
cfg.paths.features,
"--tokenized",
cfg.paths.tokenized,
"--pretrain-model",
cfg.paths.pretrain_model,
"--cohort",
cfg.paths.cohort,
"--experiment-dir",
cfg.paths.results,
]

# Run-specific flags (run-id, inner-runs, sample-fraction, ...) come via --bash-args.
if hasattr(cfg, "bash_args") and cfg.bash_args:
args.extend(shlex.split(cfg.bash_args))

from experiments.semisynthetic_simulation.python_scripts.run_study import main

main(args)


if __name__ == "__main__":
job.run_main("run_semisynthetic_study", main_run_study, INPUTS, OUTPUTS)
20 changes: 20 additions & 0 deletions corebehrt/azure/components/simulate_semisynthetic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from corebehrt.azure.util import job

INPUTS = {
"data": {"type": "uri_folder"},
}

OUTPUTS = {
"outcomes": {"type": "uri_folder"},
}


if __name__ == "__main__":
from corebehrt.main_causal import simulate_semisynthetic

job.run_main(
"simulate_semisynthetic",
simulate_semisynthetic.main_simulate,
INPUTS,
OUTPUTS,
)
3 changes: 3 additions & 0 deletions corebehrt/azure/main/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def add_parser(subparsers) -> None:
"evaluate_xgboost",
"get_pat_counts_by_code",
"run_batch_experiments",
"simulate_semisynthetic",
"calibrate_semisynthetic",
"run_semisynthetic_study",
},
help="Job to run.",
)
Expand Down
72 changes: 72 additions & 0 deletions corebehrt/configs/causal/simulate_semisynthetic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# --------------------------------------------------------------------------
# Semi-Synthetic Simulation: Observed Treatment, Simulated Outcome
# --------------------------------------------------------------------------
# Treatment assignment (A_i) and index dates are taken from the real data.
# Only the outcome is simulated from hand-crafted oracle features.

logging:
level: INFO
path: ./outputs/logs/causal

paths:
data: ./example_data/synthea_meds_causal
splits: ["tuning"]
outcomes: ./outputs/causal/semisynthetic_outcomes

seed: 42
debug: false
min_num_codes: 3
exposure_code: "EXPOSURE"

# --------------------------------------------------------------------------
# Oracle Feature Extraction
# --------------------------------------------------------------------------
features:
code_prefixes:
diagnosis: "D/"
medication: "M/"
procedure: "P/"
admission: "ADM/"
lookback_days: 365
recent_window_days: 90
burst_window_days: 30
motif_window_days: 30

# --------------------------------------------------------------------------
# Outcomes
# --------------------------------------------------------------------------
outcomes:
OUTCOME:
outcome_model:
run_in_days: 1
beta_0: -2.0
coefficients:
recent_event_count: 0.3
disease_burden: 0.2
medication_count: 0.15
utilization_intensity: 0.1
age: 0.4
chronic_disease_count: 0.25
code_diversity: 0.1
event_recency: -0.15
recent_burst_ratio: 0.2
sequence_motif_count: 0.1
interactions:
- features: [disease_burden, age]
coefficient: 0.1
noise_scale: 0.1
treatment_effect:
mode: constant
delta: 1.0

OUTCOME_NULL:
outcome_model:
run_in_days: 1
beta_0: -2.0
coefficients:
disease_burden: 0.2
age: 0.4
noise_scale: 0.1
treatment_effect:
mode: constant
delta: 0.0
1 change: 1 addition & 0 deletions corebehrt/constants/causal/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@

class EffectColumns:
method = "method"
effect_type = "effect_type"
effect = "effect"
true_effect = "true_effect"
ps_bias = "ps_bias"
Expand Down
1 change: 1 addition & 0 deletions corebehrt/constants/causal/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
EXPERIMENT_DATA_FILE = "experiment_data.parquet"
EXPERIMENT_STATS_FILE = "experiment_stats.csv"
ESTIMATE_RESULTS_FILE = "estimate_results.csv"
BOOTSTRAP_RESULTS_FILE = "bootstrap_results.csv"

SKS_DUMP_DIR = "corebehrt/main_causal/helper/data/sks_dumps"
SKS_TREES_DIR = "corebehrt/main_causal/helper/data/sks_trees"
Expand Down
4 changes: 2 additions & 2 deletions corebehrt/functional/causal/effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def compute_effect_from_counterfactuals(df: pd.DataFrame, effect_type: str) -> f
y1_mean = df[SIMULATED_PROBAS_EXPOSED].mean()
y0_mean = df[SIMULATED_PROBAS_CONTROL].mean()

if effect_type == "ATE":
if effect_type in {"ATE", "ARR"}:
effect = y1_mean - y0_mean
elif effect_type in ["ATT", "ATC"]:
treated_flag = 1 if effect_type == "ATT" else 0
Expand All @@ -62,7 +62,7 @@ def compute_effect_from_counterfactuals(df: pd.DataFrame, effect_type: str) -> f
- subset[SIMULATED_PROBAS_CONTROL].mean()
)
elif effect_type == "RR":
effect = (y1_mean + 1) / (y0_mean + 1)
effect = y1_mean / y0_mean
elif effect_type == "OR":
effect = (y1_mean / (1 - y1_mean)) / (y0_mean / (1 - y0_mean))
else:
Expand Down
19 changes: 15 additions & 4 deletions corebehrt/functional/estimate/benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
EffectColumns,
)
from corebehrt.constants.data import PID_COL
from corebehrt.functional.causal.effect import compute_effect_from_ite
from corebehrt.functional.causal.effect import (
compute_effect_from_counterfactuals,
compute_effect_from_ite,
)
from corebehrt.functional.causal.estimate import (
calculate_risk_difference,
calculate_risk_ratio,
Expand Down Expand Up @@ -42,6 +45,8 @@ def append_true_effect(
ite_df: pd.DataFrame,
outcome_name: str,
analysis_pids: np.ndarray,
effect_type: str = "ATE",
counterfactual_df: pd.DataFrame | None = None,
) -> pd.DataFrame:
"""
Add ground truth effect estimates from simulated counterfactual outcomes.
Expand All @@ -51,9 +56,15 @@ def append_true_effect(

Adds the true effect to the effect_df. (TRUE_EFFECT_COL)
"""
effect_df[EffectColumns.true_effect] = compute_effect_from_ite(
ite_df, analysis_pids, outcome_name
)
if counterfactual_df is not None:
counterfactuals = prepare_counterfactual_data_for_outcome(
counterfactual_df, outcome_name
)
counterfactuals = counterfactuals[counterfactuals[PID_COL].isin(analysis_pids)]
true_effect = compute_effect_from_counterfactuals(counterfactuals, effect_type)
else:
true_effect = compute_effect_from_ite(ite_df, analysis_pids, outcome_name)
effect_df[EffectColumns.true_effect] = true_effect
return effect_df


Expand Down
21 changes: 21 additions & 0 deletions corebehrt/functional/features/split.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,24 @@ def create_folds(
folds[i][VAL_KEY] = val_pids

return folds


def bootstrap_training_folds(
folds: List[Dict[str, list]], seed: int = 42
) -> List[Dict[str, list]]:
"""Resample each training fold with replacement, keeping validation fixed."""
rng = np.random.default_rng(seed)
bootstrapped = []
for fold in folds:
train_pids = fold[TRAIN_KEY]
if not train_pids:
raise ValueError("Cannot bootstrap an empty training fold")
bootstrapped.append(
{
TRAIN_KEY: rng.choice(
train_pids, size=len(train_pids), replace=True
).tolist(),
VAL_KEY: list(fold[VAL_KEY]),
}
)
return bootstrapped
Loading
Loading