[Repo Assist] perf(gcm): vectorise inner loops in marginal_expectation#1676
Merged
bloebp merged 5 commits intoJul 13, 2026
Conversation
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>
56 tasks
Contributor
There was a problem hiding this comment.
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.
Contributor
Author
|
Commit pushed:
|
…ize-20260712-d73ffa34c7c24c44
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 This is an automated improvement from Repo Assist.
Summary
marginal_expectation()indowhy/gcm/stats.pyis 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.repeatThe old code assigned baseline values one baseline-sample at a time in a Python loop:
The new code broadcasts all rows in one shot:
2. Result-aggregation loop → reshape +
mean(axis=1)The old code called
np.mean()and sliced predictions inside a Python loop:The new code reshapes into
(batch, n_f, ...)and reduces in one NumPy call:Impact
With the typical defaults (
num_samples_baseline=500,max_batch_size=100), each call tomarginal_expectationruns 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
reshapehandles scalar (1-D) and multi-output (2-D) prediction functions identically to the original per-sample slice/mean.Test Status
tests/gcm/test_stats.pytests passtests/gcm/test_feature_relevance.py+test_intrinsic_influence.py+test_arrow_strength.pytests passblack --checkandisort --checkpassflake8reports 3 E203 (whitespace before:in slices) and 1 E501 (long docstring line) — all 4 are pre-existing issues present onmainbefore this change (verified withgit stash)