Skip to content

add functionality for continuous fine-tuning on subpopulation - #172

Open
kirilklein wants to merge 12 commits into
mainfrom
feat/subpop-finetune
Open

add functionality for continuous fine-tuning on subpopulation#172
kirilklein wants to merge 12 commits into
mainfrom
feat/subpop-finetune

Conversation

@kirilklein

@kirilklein kirilklein commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Subpopulation-specific causal finetuning with bootstrap fold creation and continuation support
    • Azure ML pipeline and component to run subpopulation finetune → calibrate → estimate (handles optional inputs)
    • Runtime entrypoint script and YAML config for subpopulation finetuning
    • Trainer option to freeze the encoder at initialization
    • Dataset utility to drop constant outcome labels
  • Tests

    • Tests for fold creation/validation and encoder-freeze-at-init behavior
  • Chores

    • Optional pretrain check in setup and minor CI workflow formatting tweaks

@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a subpopulation fine-tuning flow: new Azure component and pipeline, CLI entrypoint and config, dataset filtering and fold creation, trainer freeze-on-init option, directory setup flag, dataset utility to drop constant outcomes, and tests for folds and freezing behavior.

Changes

Cohort / File(s) Summary
Azure component & pipeline
corebehrt/azure/components/finetune_subpop.py, corebehrt/azure/pipelines/SUBPOP_FINETUNE_CALIBRATE_ESTIMATE.py, corebehrt/azure/pipelines/__init__.py
Adds finetune_subpop Azure component and a new SUBPOP_FINETUNE_CALIBRATE_ESTIMATE pipeline with four DSL variants selected at runtime based on optional counterfactual_outcomes and secondary_cohort_config; registers the pipeline.
Causal finetune entry & config
corebehrt/main_causal/finetune_subpop.py, corebehrt/configs/causal/finetune/ft_subpop.yaml
New main_finetune_subpop CLI that loads config, filters dataset by subpopulation PIDs, drops constant outcomes, creates/validates folds (bootstrap support), runs CV loop, accumulates/saves predictions and metrics; adds YAML config for subpopulation finetune.
Trainer logic
corebehrt/modules/trainer/causal/trainer.py
CausalEHRTrainer.__init__ optionally calls _freeze_encoder() when freeze_encoder_at_init is true to disable grads for encoder params (excluding names with pooler, cls, or head).
Dataset mutation
corebehrt/modules/preparation/causal/dataset.py
Adds CausalPatientDataset.drop_constant_outcomes() to remove outcomes with no variability across patients; returns dropped outcome names and raises if no varying outcomes remain.
Directory setup API
corebehrt/modules/setup/directory.py
DirectoryPreparer.setup_finetune now accepts check_pretrain: bool = True to optionally skip pretrain directory validation and PRETRAIN_CFG copy.
Tests
tests/test_main_causal/test_finetune_subpop.py
Adds tests for fold creation/validation (bootstrap and non-bootstrap) and for trainer freeze-on-init behavior (patching to assert _freeze_encoder invocation).
CI formatting
.github/workflows/format.yml
Minor YAML formatting/indentation changes only (no behavioral changes).

Sequence Diagram

sequenceDiagram
    participant Client
    participant Pipeline as SUBPOP_FINETUNE_CALIBRATE_ESTIMATE<br/>Pipeline
    participant Finetune as finetune_subpop<br/>Component
    participant Calibrate as calibrate_exp_y<br/>Component
    participant Estimate as estimate<br/>Component
    participant GetStats as get_stats<br/>Component

    Client->>Pipeline: trigger(prepared_data, finetune_model, subpopulation_pids, optional: counterfactual_outcomes, secondary_cohort_config)
    
    rect rgba(100,150,200,0.5)
    Note over Pipeline: select DSL variant based on optional inputs
    end

    Pipeline->>Finetune: run(prepared_data, restart_model=finetune_model, subpopulation_pids)
    Finetune->>Finetune: create folds, drop constant outcomes, cv_loop, accumulate predictions
    Finetune-->>Pipeline: model

    Pipeline->>Calibrate: run(model, predictions)
    Calibrate-->>Pipeline: calibrated_predictions

    alt counterfactual_outcomes present
        Pipeline->>Estimate: run(calibrated_predictions, counterfactual_outcomes)
    else
        Pipeline->>Estimate: run(calibrated_predictions)
    end
    Estimate-->>Pipeline: ps_calibrated_predictions

    alt secondary_cohort_config present
        Pipeline->>GetStats: run(ps_calibrated_predictions, secondary_cohort_config)
    else
        Pipeline->>GetStats: run(ps_calibrated_predictions)
    end
    GetStats-->>Pipeline: statistics

    Pipeline-->>Client: results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through folds and tensors bright,
I dropped constants in the night,
I froze the encoder, then took flight,
Pipelines branch and models grow—
carrots, code, and stats in tow.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main purpose of the PR: adding functionality to support continuous fine-tuning on subpopulation data. Multiple new components, pipelines, and scripts directly implement this core functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/subpop-finetune

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
corebehrt/main_causal/finetune_subpop.py (3)

67-68: Empty test_data is intentional but worth documenting.

An empty CausalPatientDataset is passed as test_data to cv_loop. Per finetune_fold (context snippet 2), this is handled correctly via if test_data and len(test_data) > 0. However, a brief comment would clarify the intent.

📝 Proposed clarification
-    # Run CV loop (loads per-fold checkpoints via restart_model)
-    test_data = CausalPatientDataset([], vocab)
+    # Run CV loop (loads per-fold checkpoints via restart_model)
+    # Note: No separate test set for subpop fine-tuning - only train/val splits
+    test_data = CausalPatientDataset([], vocab)  # Empty placeholder
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 67 - 68, Document that
passing an empty CausalPatientDataset to cv_loop is intentional: add a brief
inline comment where test_data = CausalPatientDataset([], vocab) is created
explaining that test_data is intentionally empty because finetune_fold checks
"if test_data and len(test_data) > 0" before using it, and cv_loop relies on
that behavior; reference CausalPatientDataset, cv_loop, and finetune_fold in the
comment so future readers understand the defensive check and intent.

54-58: Inconsistent config access: bootstrap at root vs n_folds/seed under data.

n_folds and seed are accessed from cfg.data, but bootstrap is accessed from the root cfg. Looking at the YAML config (ft_subpop.yaml), bootstrap: true is indeed at the root level while n_folds/seed are under data:. This works but is inconsistent.

Consider moving bootstrap under the data: section in the YAML for consistency, then access via data_cfg.get("bootstrap", True).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 54 - 58, The code
accesses n_folds and seed from data_cfg but reads bootstrap from the root cfg,
which is inconsistent; change the access to read bootstrap from the same
data_cfg (use data_cfg.get("bootstrap", True)) so all data-related options use
the same source; update any references to the bootstrap variable in this module
(e.g., where bootstrap is used after it's defined) to rely on the new
data_cfg-derived value and ensure the YAML moves bootstrap under data if you
also update configuration files.

43-47: Add weights_only=True to torch.load calls for security.

PyTorch 2.5.1 (your current version) emits a FutureWarning when torch.load is called without weights_only, as arbitrary pickle loading is a security risk—malicious pickles can execute arbitrary code during unpickling. Since these files contain only tensors/dicts, explicitly set weights_only=True.

🛡️ Proposed fix
-    loaded_data = torch.load(join(cfg.paths.prepared_data, PREPARED_ALL_PATIENTS))
+    loaded_data = torch.load(join(cfg.paths.prepared_data, PREPARED_ALL_PATIENTS), weights_only=True)
     vocab = load_vocabulary(cfg.paths.prepared_data)
     data = CausalPatientDataset(loaded_data, vocab)

-    subpop_pids = torch.load(cfg.paths.subpopulation_pids)
+    subpop_pids = torch.load(cfg.paths.subpopulation_pids, weights_only=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 43 - 47, The two
torch.load calls that populate loaded_data and subpop_pids should pass
weights_only=True to avoid unsafe pickle deserialization; update the calls that
set loaded_data = torch.load(join(cfg.paths.prepared_data,
PREPARED_ALL_PATIENTS)) and subpop_pids =
torch.load(cfg.paths.subpopulation_pids) to include the weights_only=True kwarg
so only tensor weights are loaded (no arbitrary pickles) while keeping the rest
of the logic (vocab = load_vocabulary(...) and data = CausalPatientDataset(...))
unchanged.
corebehrt/modules/trainer/causal/trainer.py (1)

63-64: Frozen parameters remain registered with optimizer.

When freeze_encoder_at_init is enabled, _freeze_encoder() sets requires_grad=False on encoder parameters but doesn't remove them from the optimizer's param_groups. This means the optimizer still tracks these parameters, wasting memory and potentially causing confusion during debugging.

Consider either:

  1. Recreating the optimizer with only unfrozen parameters, or
  2. Documenting this as intentional (if you want to support unfreezing later)
♻️ Option: Filter frozen params from optimizer after freezing
         if self.args.get("freeze_encoder_at_init", False):
             self._freeze_encoder()
+            # Optionally rebuild optimizer with only trainable params
+            # self._rebuild_optimizer_for_frozen_params()

Alternatively, add a comment explaining the design choice:

         if self.args.get("freeze_encoder_at_init", False):
+            # Note: Frozen params remain in optimizer to support potential unfreezing
             self._freeze_encoder()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/trainer/causal/trainer.py` around lines 63 - 64, The
freeze_encoder_at_init path calls _freeze_encoder() which sets encoder
parameters to requires_grad=False but leaves them in the optimizer.param_groups;
update the code immediately after _freeze_encoder() to either recreate the
optimizer with only parameters where p.requires_grad is True (e.g., rebuild
optimizer from model.parameters() filtered by p.requires_grad) or explicitly
remove frozen params from existing optimizer.param_groups by filtering each
group to keep only params with p.requires_grad==True; reference the symbols
freeze_encoder_at_init, _freeze_encoder, and optimizer when making this change
so the optimizer no longer tracks frozen encoder parameters.
tests/test_main_causal/test_finetune_subpop.py (1)

46-90: Tests manually replicate __init__ logic instead of exercising it.

These tests patch EHRTrainer.__init__ to return None, then manually recreate the freeze logic at lines 73-74 and 87-88. This doesn't verify that the actual __init__ calls _freeze_encoder — it only tests the conditional expression in isolation.

If someone refactors __init__ (e.g., moves the freeze check), these tests would still pass while the real behavior breaks.

Consider testing the actual initialization path, perhaps by mocking the dependencies that __init__ requires rather than bypassing __init__ entirely.

♻️ Alternative test approach
class TestFreezeEncoderAtInit(unittest.TestCase):
    `@patch`("corebehrt.modules.trainer.causal.trainer.CausalEHRTrainer._freeze_encoder")
    def test_freeze_called_when_flag_set(self, mock_freeze):
        """Verify _freeze_encoder is called when freeze_encoder_at_init=True."""
        # Create minimal mocks for actual __init__ dependencies
        mock_model = MagicMock()
        mock_model.config.outcome_names = ["outcome_1"]
        
        # Actually instantiate and let __init__ run
        trainer = CausalEHRTrainer(
            model=mock_model,
            optimizer=MagicMock(),
            train_dataset=MagicMock(),
            val_dataset=None,
            test_dataset=None,
            args={"freeze_encoder_at_init": True, "use_pcgrad": False, ...},
            # ... other required args
        )
        
        mock_freeze.assert_called_once()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_main_causal/test_finetune_subpop.py` around lines 46 - 90, The
tests patch out EHRTrainer.__init__ and then duplicate the freeze logic, so they
don't verify that CausalEHRTrainer.__init__ actually calls _freeze_encoder;
update the tests (TestFreezeEncoderAtInit.test_freeze_called_when_flag_set and
test_freeze_not_called_when_flag_not_set) to let CausalEHRTrainer.__init__ run
instead of stubbing it: remove the `@patch` of EHRTrainer.__init__, supply minimal
mocked dependencies (e.g., a MagicMock model with config.outcome_names, mock
optimizer/datasets, and args containing or omitting "freeze_encoder_at_init")
when instantiating CausalEHRTrainer so the real __init__ executes, and delete
the manual if trainer.args... trainer._freeze_encoder() lines; keep patching
CausalEHRTrainer._freeze_encoder to assert it was (or was not) called.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@corebehrt/azure/components/finetune_subpop.py`:
- Line 1: The file fails formatting (ruff) — reformat
corebehrt/azure/components/finetune_subpop.py so it passes ruff format checks;
specifically ensure the import line "from corebehrt.azure.util import job" and
the file overall follow project style (run `ruff format` on the file or apply
your formatter) and then re-run linting to confirm the file is no longer
reported as "would be reformatted".

In `@corebehrt/configs/causal/finetune/ft_subpop.yaml`:
- Line 34: The config has mismatched epoch counts: the top-level key epochs
(currently 3) does not match num_training_epochs (currently 5) used by the
scheduler; update one so both match (either set epochs to 5 to match
num_training_epochs or change num_training_epochs to 3) — edit the keys "epochs"
and/or "num_training_epochs" in ft_subpop.yaml so they are identical and
consistent with the intended training length.

In `@corebehrt/main_causal/finetune_subpop.py`:
- Line 1: The file failed ruff formatting; run the code formatter on
corebehrt/main_causal/finetune_subpop.py (e.g., run `ruff format` or your repo's
formatter) and commit the reformat; ensure any top-level docstring/quotes and
surrounding imports/whitespace are normalized so `ruff format --check` passes;
no logic changes needed—only apply the automatic formatting to
finetune_subpop.py.

In `@tests/test_main_causal/test_finetune_subpop.py`:
- Line 1: The file contains an unformatted import statement ("import unittest")
causing ruff format --check to fail; fix by running ruff format on that test
file (or applying ruff's suggested changes) so whitespace, import formatting and
trailing newline are corrected and the file passes ruff format checks.

---

Nitpick comments:
In `@corebehrt/main_causal/finetune_subpop.py`:
- Around line 67-68: Document that passing an empty CausalPatientDataset to
cv_loop is intentional: add a brief inline comment where test_data =
CausalPatientDataset([], vocab) is created explaining that test_data is
intentionally empty because finetune_fold checks "if test_data and
len(test_data) > 0" before using it, and cv_loop relies on that behavior;
reference CausalPatientDataset, cv_loop, and finetune_fold in the comment so
future readers understand the defensive check and intent.
- Around line 54-58: The code accesses n_folds and seed from data_cfg but reads
bootstrap from the root cfg, which is inconsistent; change the access to read
bootstrap from the same data_cfg (use data_cfg.get("bootstrap", True)) so all
data-related options use the same source; update any references to the bootstrap
variable in this module (e.g., where bootstrap is used after it's defined) to
rely on the new data_cfg-derived value and ensure the YAML moves bootstrap under
data if you also update configuration files.
- Around line 43-47: The two torch.load calls that populate loaded_data and
subpop_pids should pass weights_only=True to avoid unsafe pickle
deserialization; update the calls that set loaded_data =
torch.load(join(cfg.paths.prepared_data, PREPARED_ALL_PATIENTS)) and subpop_pids
= torch.load(cfg.paths.subpopulation_pids) to include the weights_only=True
kwarg so only tensor weights are loaded (no arbitrary pickles) while keeping the
rest of the logic (vocab = load_vocabulary(...) and data =
CausalPatientDataset(...)) unchanged.

In `@corebehrt/modules/trainer/causal/trainer.py`:
- Around line 63-64: The freeze_encoder_at_init path calls _freeze_encoder()
which sets encoder parameters to requires_grad=False but leaves them in the
optimizer.param_groups; update the code immediately after _freeze_encoder() to
either recreate the optimizer with only parameters where p.requires_grad is True
(e.g., rebuild optimizer from model.parameters() filtered by p.requires_grad) or
explicitly remove frozen params from existing optimizer.param_groups by
filtering each group to keep only params with p.requires_grad==True; reference
the symbols freeze_encoder_at_init, _freeze_encoder, and optimizer when making
this change so the optimizer no longer tracks frozen encoder parameters.

In `@tests/test_main_causal/test_finetune_subpop.py`:
- Around line 46-90: The tests patch out EHRTrainer.__init__ and then duplicate
the freeze logic, so they don't verify that CausalEHRTrainer.__init__ actually
calls _freeze_encoder; update the tests
(TestFreezeEncoderAtInit.test_freeze_called_when_flag_set and
test_freeze_not_called_when_flag_not_set) to let CausalEHRTrainer.__init__ run
instead of stubbing it: remove the `@patch` of EHRTrainer.__init__, supply minimal
mocked dependencies (e.g., a MagicMock model with config.outcome_names, mock
optimizer/datasets, and args containing or omitting "freeze_encoder_at_init")
when instantiating CausalEHRTrainer so the real __init__ executes, and delete
the manual if trainer.args... trainer._freeze_encoder() lines; keep patching
CausalEHRTrainer._freeze_encoder to assert it was (or was not) called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 012652ca-45b9-4ca7-a9da-072ca6916aaa

📥 Commits

Reviewing files that changed from the base of the PR and between 545e880 and 0c154db.

📒 Files selected for processing (7)
  • corebehrt/azure/components/finetune_subpop.py
  • corebehrt/azure/pipelines/SUBPOP_FINETUNE_CALIBRATE_ESTIMATE.py
  • corebehrt/azure/pipelines/__init__.py
  • corebehrt/configs/causal/finetune/ft_subpop.yaml
  • corebehrt/main_causal/finetune_subpop.py
  • corebehrt/modules/trainer/causal/trainer.py
  • tests/test_main_causal/test_finetune_subpop.py

Comment thread corebehrt/azure/components/finetune_subpop.py
batch_size: 128
val_batch_size: 256
effective_batch_size: 128
epochs: 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mismatch: epochs: 3 but num_training_epochs: 5.

The trainer will run for 3 epochs (line 34), but the scheduler is configured for 5 epochs (line 59). This means the learning rate schedule won't complete — training ends before reaching the final scheduled LR.

🐛 Proposed fix: Align epoch counts
 trainer_args:
   # ...
-  epochs: 3
+  epochs: 5

Or alternatively:

 scheduler:
   _target_: transformers.get_linear_schedule_with_warmup
-  num_training_epochs: 5
+  num_training_epochs: 3
   num_warmup_epochs: 1

Also applies to: 57-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/configs/causal/finetune/ft_subpop.yaml` at line 34, The config has
mismatched epoch counts: the top-level key epochs (currently 3) does not match
num_training_epochs (currently 5) used by the scheduler; update one so both
match (either set epochs to 5 to match num_training_epochs or change
num_training_epochs to 3) — edit the keys "epochs" and/or "num_training_epochs"
in ft_subpop.yaml so they are identical and consistent with the intended
training length.

Comment thread corebehrt/main_causal/finetune_subpop.py
Comment thread tests/test_main_causal/test_finetune_subpop.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/format.yml:
- Around line 15-16: The workflow uses outdated action versions; update the
actions referenced in the workflow by replacing the usages of
actions/checkout@v3 and actions/setup-python@v4 with the current stable releases
(e.g., actions/checkout@v6.0.2 and actions/setup-python@v6.2.0) so the job
definitions that call these actions use the newer tags; ensure any inputs or
behavior differences are accounted for by verifying the checkout and
setup-python steps still pass after updating.

In `@corebehrt/main_causal/finetune_subpop.py`:
- Around line 55-60: The call to create_folds ignores data.val_ratio in
single-fold mode, so when n_folds == 1 the configured validation split isn't
passed through; update the code around data_cfg, n_folds, seed, bootstrap to
read val_ratio = data_cfg.get("val_ratio", <default>) and pass
val_ratio=val_ratio into create_folds (i.e., create_folds(train_val_pids,
n_folds, seed, bootstrap=bootstrap, val_ratio=val_ratio)) so create_folds
receives the intended validation proportion in single-fold mode.
- Around line 50-61: Before calling create_folds/validate_folds add explicit
precondition checks on the filtered subpopulation: after computing
train_val_pids (from data.filter_by_pids) check that len(train_val_pids) > 0 and
that len(train_val_pids) >= n_folds (n_folds comes from cfg.get("data") as used
above); if not, raise a clear ValueError or log an error via logger with a
descriptive message including len(train_val_pids) and n_folds (and mention
bootstrap mode if cfg.get("bootstrap") affects behavior), so create_folds and
validate_folds are only called when the subpopulation size meets fold
requirements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3333f4a-22e0-4f47-adbf-387be7d22bf9

📥 Commits

Reviewing files that changed from the base of the PR and between 0c154db and aff936a.

📒 Files selected for processing (4)
  • .github/workflows/format.yml
  • corebehrt/azure/components/finetune_subpop.py
  • corebehrt/main_causal/finetune_subpop.py
  • tests/test_main_causal/test_finetune_subpop.py
✅ Files skipped from review due to trivial changes (1)
  • corebehrt/azure/components/finetune_subpop.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_main_causal/test_finetune_subpop.py

Comment on lines +15 to +16
- uses: actions/checkout@v3
- uses: actions/setup-python@v4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the latest releases for GitHub Actions used in this workflow

echo "Checking latest release for actions/checkout:"
gh api repos/actions/checkout/releases/latest --jq '.tag_name'

echo -e "\nChecking latest release for actions/setup-python:"
gh api repos/actions/setup-python/releases/latest --jq '.tag_name'

Repository: kirilklein/PHAIR_EHR

Length of output: 464


🌐 Web query:

actions/checkout latest version 2025 2026

💡 Result:

The latest version of the GitHub Actions actions/checkout is v6.0.2, released on 2026-01-09. This is the current version as of 2026-04-01. Key recent releases: - v6.0.2 (2026-01-09): Fix tag handling: preserve annotations and explicit fetch-tags. - v6.0.1 (late 2025): Minor updates including worktree support and README clarifications. - v6.0.0 (late 2025): Improved credential security (persist-credentials to separate file), Node.js 24 support. - v5.0.0 (2025-08): Updated to Node.js 24 runtime. Usage example: - uses: actions/checkout@v6 Official sources confirm v6.0.2 as latest, with last repo push on 2026-02-03 but no newer release by April 2026.

Citations:


🌐 Web query:

actions/setup-python latest version 2025 2026

💡 Result:

The latest version of the actions/setup-python GitHub Action is v6.2.0, released on January 22, 2026. This is confirmed directly from the official repository homepage, which states "v6.2.0 Latest Jan 22, 2026". Usage examples in the docs and marketplace also reference uses: actions/setup-python@v6. Recent activity includes commits up to January 22, 2026, such as dependency updates. Earlier 2025 releases include v6.0.0 in September 2025 with breaking changes (Node 20 to Node 24 upgrade, requiring runner v2.327.1+), and various enhancements like pip-version support.

Citations:


Update GitHub Actions to current stable versions.

The workflow uses outdated action versions. actions/checkout@v3 (released in 2022) and actions/setup-python@v4 (released in 2023) are significantly behind current stable versions and may cause compatibility issues with modern GitHub Actions runners.

Current stable versions as of April 2026:

  • actions/checkout: v6.0.2 (latest)
  • actions/setup-python: v6.2.0 (latest)

Update to the current stable versions:

Proposed fix
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v6
+      - uses: actions/setup-python@v6
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
🧰 Tools
🪛 actionlint (1.7.11)

[error] 15-15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


[error] 16-16: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/format.yml around lines 15 - 16, The workflow uses
outdated action versions; update the actions referenced in the workflow by
replacing the usages of actions/checkout@v3 and actions/setup-python@v4 with the
current stable releases (e.g., actions/checkout@v6.0.2 and
actions/setup-python@v6.2.0) so the job definitions that call these actions use
the newer tags; ensure any inputs or behavior differences are accounted for by
verifying the checkout and setup-python steps still pass after updating.

Comment on lines +50 to +61
data = data.filter_by_pids(subpop_pids)
train_val_pids = data.get_pids()
logger.info(f"Filtered to {len(train_val_pids)} patients in prepared data")

# Create fresh bootstrap folds from subpopulation
data_cfg = cfg.get("data", {})
n_folds = data_cfg.get("n_folds", 5)
seed = data_cfg.get("seed", 42)
bootstrap = cfg.get("bootstrap", True)

folds = create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap)
validate_folds(folds, set(train_val_pids), logger, bootstrap=bootstrap)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add explicit subpopulation/fold precondition checks before creating folds.

If filtering leaves too few patients, fold creation can fail at runtime (e.g., KFold with n_folds > len(train_val_pids)), and the failure message will be less actionable than a direct validation error.

Proposed fix
     data = data.filter_by_pids(subpop_pids)
     train_val_pids = data.get_pids()
     logger.info(f"Filtered to {len(train_val_pids)} patients in prepared data")

     # Create fresh bootstrap folds from subpopulation
     data_cfg = cfg.get("data", {})
     n_folds = data_cfg.get("n_folds", 5)
     seed = data_cfg.get("seed", 42)
     bootstrap = cfg.get("bootstrap", True)
+
+    if not train_val_pids:
+        raise ValueError(
+            "No matching subpopulation patients found in prepared data. "
+            "Check cfg.paths.subpopulation_pids and prepared dataset alignment."
+        )
+    if n_folds < 1:
+        raise ValueError(f"n_folds must be >= 1, got {n_folds}")
+    if n_folds > len(train_val_pids):
+        raise ValueError(
+            f"n_folds ({n_folds}) cannot exceed number of filtered patients "
+            f"({len(train_val_pids)})."
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data = data.filter_by_pids(subpop_pids)
train_val_pids = data.get_pids()
logger.info(f"Filtered to {len(train_val_pids)} patients in prepared data")
# Create fresh bootstrap folds from subpopulation
data_cfg = cfg.get("data", {})
n_folds = data_cfg.get("n_folds", 5)
seed = data_cfg.get("seed", 42)
bootstrap = cfg.get("bootstrap", True)
folds = create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap)
validate_folds(folds, set(train_val_pids), logger, bootstrap=bootstrap)
data = data.filter_by_pids(subpop_pids)
train_val_pids = data.get_pids()
logger.info(f"Filtered to {len(train_val_pids)} patients in prepared data")
# Create fresh bootstrap folds from subpopulation
data_cfg = cfg.get("data", {})
n_folds = data_cfg.get("n_folds", 5)
seed = data_cfg.get("seed", 42)
bootstrap = cfg.get("bootstrap", True)
if not train_val_pids:
raise ValueError(
"No matching subpopulation patients found in prepared data. "
"Check cfg.paths.subpopulation_pids and prepared dataset alignment."
)
if n_folds < 1:
raise ValueError(f"n_folds must be >= 1, got {n_folds}")
if n_folds > len(train_val_pids):
raise ValueError(
f"n_folds ({n_folds}) cannot exceed number of filtered patients "
f"({len(train_val_pids)})."
)
folds = create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap)
validate_folds(folds, set(train_val_pids), logger, bootstrap=bootstrap)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 50 - 61, Before
calling create_folds/validate_folds add explicit precondition checks on the
filtered subpopulation: after computing train_val_pids (from
data.filter_by_pids) check that len(train_val_pids) > 0 and that
len(train_val_pids) >= n_folds (n_folds comes from cfg.get("data") as used
above); if not, raise a clear ValueError or log an error via logger with a
descriptive message including len(train_val_pids) and n_folds (and mention
bootstrap mode if cfg.get("bootstrap") affects behavior), so create_folds and
validate_folds are only called when the subpopulation size meets fold
requirements.

Comment thread corebehrt/main_causal/finetune_subpop.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
corebehrt/modules/setup/directory.py (1)

391-397: ⚠️ Potential issue | 🟠 Major

Inconsistency: write_config still references pretrain_model when directory check is disabled.

When check_pretrain=False, line 396 still attempts to copy the config from the pretrain_model directory via write_config("model", source="pretrain_model", ...). The check_directory call at line 391 is skipped, but write_config will subsequently call copyfile() on a source that may not exist, causing a runtime failure.

Make the write_config call conditional on the same flag:

Proposed fix
         if check_pretrain:
             self.check_directory("pretrain_model")
         self.create_run_directory("model", base="runs")

         # Write config in output directory.
-        self.write_config("model", source="pretrain_model", name=PRETRAIN_CFG)
+        if check_pretrain:
+            self.write_config("model", source="pretrain_model", name=PRETRAIN_CFG)
         self.write_config("model", name=FINETUNE_CFG)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/setup/directory.py` around lines 391 - 397, The
write_config call currently always tries to copy PRETRAIN_CFG from the
"pretrain_model" source even when check_pretrain is False; update the logic so
that after calling create_run_directory("model", base="runs") you only call
write_config("model", source="pretrain_model", name=PRETRAIN_CFG) when
check_pretrain is True (the same condition used for calling
check_directory("pretrain_model")), otherwise skip that source-based write and
only call write_config("model", name=FINETUNE_CFG); this ensures check_pretrain,
check_directory, write_config, PRETRAIN_CFG and "pretrain_model" are consistent
and avoids copyfile on a missing source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@corebehrt/modules/setup/directory.py`:
- Around line 391-397: The write_config call currently always tries to copy
PRETRAIN_CFG from the "pretrain_model" source even when check_pretrain is False;
update the logic so that after calling create_run_directory("model",
base="runs") you only call write_config("model", source="pretrain_model",
name=PRETRAIN_CFG) when check_pretrain is True (the same condition used for
calling check_directory("pretrain_model")), otherwise skip that source-based
write and only call write_config("model", name=FINETUNE_CFG); this ensures
check_pretrain, check_directory, write_config, PRETRAIN_CFG and "pretrain_model"
are consistent and avoids copyfile on a missing source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1d175be-6bce-43c7-92e2-d1354d42e13d

📥 Commits

Reviewing files that changed from the base of the PR and between aff936a and c721ac4.

📒 Files selected for processing (1)
  • corebehrt/modules/setup/directory.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
corebehrt/main_causal/finetune_subpop.py (2)

62-67: ⚠️ Potential issue | 🟡 Minor

Pass data.val_ratio through to create_folds to preserve configured split behavior.

On Line 67, create_folds(...) currently ignores data.val_ratio, which can diverge from config when n_folds == 1.

Proposed fix
     data_cfg = cfg.get("data", {})
     n_folds = data_cfg.get("n_folds", 5)
     seed = data_cfg.get("seed", 42)
+    val_ratio = data_cfg.get("val_ratio", 0.8)
     bootstrap = cfg.get("bootstrap", True)

-    folds = create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap)
+    folds = create_folds(
+        train_val_pids,
+        n_folds,
+        seed,
+        val_ratio=val_ratio,
+        bootstrap=bootstrap,
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 62 - 67, The call to
create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap) is ignoring the
configured validation split; read data_cfg.get("val_ratio", <default>) from the
same cfg block (data_cfg variable) and pass it into create_folds (e.g., include
val_ratio=val_ratio) so that when n_folds == 1 the configured validation
proportion is preserved; update the invocation near create_folds and ensure the
val_ratio symbol is defined from data_cfg before the call.

58-68: ⚠️ Potential issue | 🟠 Major

Add explicit filtered-subpopulation/fold precondition checks before fold creation.

On Line 67, fold creation can fail with unclear errors when train_val_pids is empty or n_folds is invalid relative to filtered size. Add explicit checks right before create_folds(...) for clearer failures.

Proposed fix
     train_val_pids = data.get_pids()
     logger.info(f"Filtered to {len(train_val_pids)} patients in prepared data")

     # Create fresh bootstrap folds from subpopulation
     data_cfg = cfg.get("data", {})
     n_folds = data_cfg.get("n_folds", 5)
     seed = data_cfg.get("seed", 42)
     bootstrap = cfg.get("bootstrap", True)
+
+    if not train_val_pids:
+        raise ValueError(
+            "No matching subpopulation patients found in prepared data. "
+            "Check cfg.paths.subpopulation_pids and prepared dataset alignment."
+        )
+    if n_folds < 1:
+        raise ValueError(f"n_folds must be >= 1, got {n_folds}")
+    if n_folds > len(train_val_pids):
+        raise ValueError(
+            f"n_folds ({n_folds}) cannot exceed number of filtered patients "
+            f"({len(train_val_pids)})."
+        )

     folds = create_folds(train_val_pids, n_folds, seed, bootstrap=bootstrap)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/main_causal/finetune_subpop.py` around lines 58 - 68, The code may
call create_folds with an empty or inappropriate fold count causing unclear
errors; before calling create_folds(validate_folds), check that train_val_pids
(from data.get_pids()) is not empty and that n_folds is a positive integer and
reasonably <= len(train_val_pids) (or if cfg.get("bootstrap") is True,
document/validate the allowed relation) — if these preconditions fail, log a
clear error via logger.error (including values of len(train_val_pids), n_folds,
and bootstrap) and raise a ValueError with a descriptive message so callers see
an explicit failure instead of a downstream cryptic one; update the block that
sets data_cfg, n_folds, seed, bootstrap and calls create_folds/validate_folds to
perform these checks first.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@corebehrt/modules/preparation/causal/dataset.py`:
- Around line 87-111: The method drop_constant_outcomes has formatting issues
causing ruff format --check to fail; run an auto-formatter (e.g., ruff format)
on the file or at least reformat this method so spacing and line wrapping meet
project style (ensure consistent indentation and wrapping around the list
comprehension for varying, the f-string in the ValueError, and the for-loop that
rebuilds p.outcomes for patients), then re-run ruff format --check to confirm;
key symbols to touch: drop_constant_outcomes, get_outcomes, outcomes, varying,
dropped, and p.outcomes.

---

Duplicate comments:
In `@corebehrt/main_causal/finetune_subpop.py`:
- Around line 62-67: The call to create_folds(train_val_pids, n_folds, seed,
bootstrap=bootstrap) is ignoring the configured validation split; read
data_cfg.get("val_ratio", <default>) from the same cfg block (data_cfg variable)
and pass it into create_folds (e.g., include val_ratio=val_ratio) so that when
n_folds == 1 the configured validation proportion is preserved; update the
invocation near create_folds and ensure the val_ratio symbol is defined from
data_cfg before the call.
- Around line 58-68: The code may call create_folds with an empty or
inappropriate fold count causing unclear errors; before calling
create_folds(validate_folds), check that train_val_pids (from data.get_pids())
is not empty and that n_folds is a positive integer and reasonably <=
len(train_val_pids) (or if cfg.get("bootstrap") is True, document/validate the
allowed relation) — if these preconditions fail, log a clear error via
logger.error (including values of len(train_val_pids), n_folds, and bootstrap)
and raise a ValueError with a descriptive message so callers see an explicit
failure instead of a downstream cryptic one; update the block that sets
data_cfg, n_folds, seed, bootstrap and calls create_folds/validate_folds to
perform these checks first.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c70c83fb-2869-460b-a84c-a5abb9c82d37

📥 Commits

Reviewing files that changed from the base of the PR and between c721ac4 and 27ce020.

📒 Files selected for processing (2)
  • corebehrt/main_causal/finetune_subpop.py
  • corebehrt/modules/preparation/causal/dataset.py

Comment on lines +87 to +111
def drop_constant_outcomes(self) -> List[str]:
"""Remove outcome columns that take only one value across all patients.

Returns:
Names of outcomes that were dropped (empty if none).

Raises:
ValueError: If every outcome would be dropped (no varying labels left).
"""
if not self.patients:
return []
outcomes = self.get_outcomes()
varying = [
name for name, vals in outcomes.items() if len(set(vals)) > 1
]
dropped = [name for name in outcomes if name not in varying]
if not varying:
raise ValueError(
"After filtering, no outcome has both classes (0 and 1). "
f"All outcomes are constant: dropped candidates {dropped}"
)
if dropped:
for p in self.patients:
p.outcomes = {k: p.outcomes[k] for k in varying}
return dropped

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix formatting in this changed block to unblock CI.

ruff format --check is failing for this file, so this method block should be auto-formatted before merge (e.g., run ruff format corebehrt/modules/preparation/causal/dataset.py).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@corebehrt/modules/preparation/causal/dataset.py` around lines 87 - 111, The
method drop_constant_outcomes has formatting issues causing ruff format --check
to fail; run an auto-formatter (e.g., ruff format) on the file or at least
reformat this method so spacing and line wrapping meet project style (ensure
consistent indentation and wrapping around the list comprehension for varying,
the f-string in the ValueError, and the for-loop that rebuilds p.outcomes for
patients), then re-run ruff format --check to confirm; key symbols to touch:
drop_constant_outcomes, get_outcomes, outcomes, varying, dropped, and
p.outcomes.

@kirilklein

Copy link
Copy Markdown
Owner Author

@Montgomeryyyy we can skip calibration for outcomes entirely, only ps needs to be calibrated. I can implement this quickly. Depending how many jobs you have run already, it could just be a separate script

@Montgomeryyyy

Copy link
Copy Markdown
Collaborator

@Montgomeryyyy we can skip calibration for outcomes entirely, only ps needs to be calibrated. I can implement this quickly. Depending how many jobs you have run already, it could just be a separate script

I have mostly run the cvd and diab sub cohorts, and this does not take long. So if you can implement if quickly that would be nice:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants