Skip to content

[Repo Assist] perf(gcm): vectorise inner loops in marginal_expectation#1676

Merged
bloebp merged 5 commits into
mainfrom
repo-assist/perf-marginal-expectation-vectorize-20260712-d73ffa34c7c24c44
Jul 13, 2026
Merged

[Repo Assist] perf(gcm): vectorise inner loops in marginal_expectation#1676
bloebp merged 5 commits into
mainfrom
repo-assist/perf-marginal-expectation-vectorize-20260712-d73ffa34c7c24c44

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 This is an automated improvement from Repo Assist.

Summary

marginal_expectation() in dowhy/gcm/stats.py is the hot path for Shapley-based feature relevance, arrow strength, and intrinsic causal influence computations — it is evaluated once per coalition subset. This PR eliminates two O(batch_size) Python loops inside the function by replacing them with vectorised NumPy operations.

Changes

1. Input-filling loop → np.repeat

The old code assigned baseline values one baseline-sample at a time in a Python loop:

for index in range(adjusted_batch_size):
    inputs[
        index * n_f : (index + 1) * n_f, baseline_feature_indices
    ] = baseline_samples[offset + index, baseline_feature_indices]

The new code broadcasts all rows in one shot:

if len(baseline_feature_indices) > 0:
    inputs[:, baseline_feature_indices] = np.repeat(
        baseline_samples[offset : offset + adjusted_batch_size, :][:, baseline_feature_indices],
        n_f,
        axis=0,
    )

2. Result-aggregation loop → reshape + mean(axis=1)

The old code called np.mean() and sliced predictions inside a Python loop:

for index in range(adjusted_batch_size):
    if return_averaged_results:
        result[offset + index] = np.mean(predictions[...], axis=0)
    else:
        result[offset + index] = predictions[...]

The new code reshapes into (batch, n_f, ...) and reduces in one NumPy call:

pred_batched = predictions.reshape((adjusted_batch_size, n_f) + extra_dims)
if return_averaged_results:
    result[offset : offset + adjusted_batch_size] = list(pred_batched.mean(axis=1))
else:
    result[offset : offset + adjusted_batch_size] = list(pred_batched)

Impact

With the typical defaults (num_samples_baseline=500, max_batch_size=100), each call to marginal_expectation runs 5 batches of 100. The input-filling loop alone goes from 100 Python iterations × 5 batches = 500 iterations per call to 5 NumPy ops. Since this is evaluated once per coalition (O(2^n) exact or O(permutations × n) approximate), the saving compounds across many Shapley calls.

Correctness

Both changes are semantically equivalent to the original. The reshape handles scalar (1-D) and multi-output (2-D) prediction functions identically to the original per-sample slice/mean.

Test Status

  • ✅ All 19 tests/gcm/test_stats.py tests pass
  • ✅ All 21 tests/gcm/test_feature_relevance.py + test_intrinsic_influence.py + test_arrow_strength.py tests pass
  • black --check and isort --check pass
  • ⚠️ flake8 reports 3 E203 (whitespace before : in slices) and 1 E501 (long docstring line) — all 4 are pre-existing issues present on main before this change (verified with git stash)

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@11c9a2c442e519ff2b427bf58679f5a525353f76

Replace two O(batch_size) Python loops with vectorised NumPy
operations in marginal_expectation():

1. Input-filling loop: previously assigned baseline values one
   row at a time with a Python loop; replaced with a single
   np.repeat() call that broadcasts all baseline rows in one
   shot.

2. Result-aggregation loop: previously called np.mean() and
   sliced predictions inside a Python loop; replaced with a
   reshape + .mean(axis=1) that computes all batch means in a
   single NumPy call, and a list-slice assignment to store results.

Both changes are semantically equivalent to the original code
and handle scalar (1-D) as well as multi-output (2-D) prediction
functions correctly.

marginal_expectation() is the hot path inside Shapley-based
feature relevance, arrow strength, and intrinsic causal influence
computations, where it is evaluated once per coalition subset.
Removing the Python-level loop overhead is most significant when
baseline_samples is large (typical default: 500 samples with
batch_size=100, giving 5 batches × 100 Python iterations → now
5 batches × 1 NumPy op each).

All 19 test_stats tests and all 21 test_feature_relevance /
test_intrinsic_influence / test_arrow_strength tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

Copilot AI 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.

Pull request overview

This PR optimizes dowhy.gcm.stats.marginal_expectation()—a performance-critical routine used in Shapley-based computations in the GCM module—by removing per-sample Python loops and replacing them with NumPy vectorized operations.

Changes:

  • Vectorizes filling of baseline feature columns within each batch (replacing a per-baseline loop).
  • Vectorizes per-baseline aggregation of predictions via reshape(...).mean(axis=1) (replacing a per-baseline slice/mean loop).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dowhy/gcm/stats.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor Author

Commit pushed: b64f187

Generated by 🌈 Repo Assist, see workflow run. Learn more.

@bloebp
bloebp marked this pull request as ready for review July 13, 2026 02:04
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>

@bloebp bloebp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

@bloebp
bloebp merged commit 6c36c9d into main Jul 13, 2026
32 of 37 checks passed
@bloebp
bloebp deleted the repo-assist/perf-marginal-expectation-vectorize-20260712-d73ffa34c7c24c44 branch July 13, 2026 02:12
github-actions Bot added a commit that referenced this pull request Jul 14, 2026
* perf(gcm): vectorise inner loops in marginal_expectation

Replace two O(batch_size) Python loops with vectorised NumPy
operations in marginal_expectation():

1. Input-filling loop: previously assigned baseline values one
   row at a time with a Python loop; replaced with a single
   np.repeat() call that broadcasts all baseline rows in one
   shot.

2. Result-aggregation loop: previously called np.mean() and
   sliced predictions inside a Python loop; replaced with a
   reshape + .mean(axis=1) that computes all batch means in a
   single NumPy call, and a list-slice assignment to store results.

Both changes are semantically equivalent to the original code
and handle scalar (1-D) as well as multi-output (2-D) prediction
functions correctly.

marginal_expectation() is the hot path inside Shapley-based
feature relevance, arrow strength, and intrinsic causal influence
computations, where it is evaluated once per coalition subset.
Removing the Python-level loop overhead is most significant when
baseline_samples is large (typical default: 500 samples with
batch_size=100, giving 5 batches × 100 Python iterations → now
5 batches × 1 NumPy op each).

All 19 test_stats tests and all 21 test_feature_relevance /
test_intrinsic_influence / test_arrow_strength tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* build(deps-dev): bump soupsieve from 2.7 to 2.8.4 (#1670)

* build(deps-dev): bump mistune from 3.2.1 to 3.3.0 (#1671)

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
github-actions Bot added a commit that referenced this pull request Jul 14, 2026
* perf(gcm): vectorise inner loops in marginal_expectation

Replace two O(batch_size) Python loops with vectorised NumPy
operations in marginal_expectation():

1. Input-filling loop: previously assigned baseline values one
   row at a time with a Python loop; replaced with a single
   np.repeat() call that broadcasts all baseline rows in one
   shot.

2. Result-aggregation loop: previously called np.mean() and
   sliced predictions inside a Python loop; replaced with a
   reshape + .mean(axis=1) that computes all batch means in a
   single NumPy call, and a list-slice assignment to store results.

Both changes are semantically equivalent to the original code
and handle scalar (1-D) as well as multi-output (2-D) prediction
functions correctly.

marginal_expectation() is the hot path inside Shapley-based
feature relevance, arrow strength, and intrinsic causal influence
computations, where it is evaluated once per coalition subset.
Removing the Python-level loop overhead is most significant when
baseline_samples is large (typical default: 500 samples with
batch_size=100, giving 5 batches × 100 Python iterations → now
5 batches × 1 NumPy op each).

All 19 test_stats tests and all 21 test_feature_relevance /
test_intrinsic_influence / test_arrow_strength tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* build(deps-dev): bump soupsieve from 2.7 to 2.8.4 (#1670)

* build(deps-dev): bump mistune from 3.2.1 to 3.3.0 (#1671)

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
github-actions Bot added a commit that referenced this pull request Jul 16, 2026
* perf(gcm): vectorise inner loops in marginal_expectation

Replace two O(batch_size) Python loops with vectorised NumPy
operations in marginal_expectation():

1. Input-filling loop: previously assigned baseline values one
   row at a time with a Python loop; replaced with a single
   np.repeat() call that broadcasts all baseline rows in one
   shot.

2. Result-aggregation loop: previously called np.mean() and
   sliced predictions inside a Python loop; replaced with a
   reshape + .mean(axis=1) that computes all batch means in a
   single NumPy call, and a list-slice assignment to store results.

Both changes are semantically equivalent to the original code
and handle scalar (1-D) as well as multi-output (2-D) prediction
functions correctly.

marginal_expectation() is the hot path inside Shapley-based
feature relevance, arrow strength, and intrinsic causal influence
computations, where it is evaluated once per coalition subset.
Removing the Python-level loop overhead is most significant when
baseline_samples is large (typical default: 500 samples with
batch_size=100, giving 5 batches × 100 Python iterations → now
5 batches × 1 NumPy op each).

All 19 test_stats tests and all 21 test_feature_relevance /
test_intrinsic_influence / test_arrow_strength tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* build(deps-dev): bump soupsieve from 2.7 to 2.8.4 (#1670)

* build(deps-dev): bump mistune from 3.2.1 to 3.3.0 (#1671)

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Patrick Blöbaum <51325689+bloebp@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants