Skip to content

Fix order-dependent interaction-term lookup in DiD effect_summary (port of #1063) - #1153

Merged
drbenvincent merged 1 commit into
pymc6_and_pymcmarketing1_migrationfrom
migration/1063-did-interaction-lookup
Aug 8, 2026
Merged

Fix order-dependent interaction-term lookup in DiD effect_summary (port of #1063)#1153
drbenvincent merged 1 commit into
pymc6_and_pymcmarketing1_migrationfrom
migration/1063-did-interaction-lookup

Conversation

@derwells

@derwells derwells commented Aug 7, 2026

Copy link
Copy Markdown

Summary

Port of #1063 by @Mari1988 to the migration branch.

effect_summary() on an OLS DifferenceInDifferences result crashed with ValueError: Could not find interaction term group:post_treatment in model when the formula was written as y ~ 1 + post_treatment*group instead of y ~ 1 + group*post_treatment. Both formulas fit the same model. The point estimate was already order-independent (#994 fixed that in algorithm()); only this reporting step crashed.

Cause

_compute_statistics_did_ols needs the design-matrix column index of the interaction term to compute the SE, CI, and p-value. It found that column by checking whether the concatenated string "group:post_treatment" appears in each patsy column name. patsy names interaction columns in formula order, so the reversed formula produces the label post_treatment[T.True]:group and the check never matches. The categorical spelling C(group)*post_treatment fails the same way: its label is C(group)[T.1]:post_treatment[T.True], which also does not contain the literal string.

Fix

Use _is_treatment_interaction() for the lookup instead. That helper was introduced by #994 and algorithm() already uses it to locate the causal_impact coefficient. It parses each label into its factor set, strips [T.…] suffixes, accepts the C(group) spelling, and requires an exact match on {group, post_treatment}.

This deviates from the diff in #1063, which matched the two variable names as independent substrings. That fixes the crash, but a label like subgroup:post_treatment would also match, and since the loop takes the first hit the SE could silently come from the wrong column while the mean stays correct. The reviewer on #1063 suggested the helper for exactly this reason, and this PR adopts that suggestion. The improved error message is taken from #1063 unchanged.

Part of the "stop string-matching patsy display labels" family (#993 to #1000).

Testing

  • test_effect_summary_ols_did_order_independent, ported verbatim from Fix order-dependent interaction-term lookup in DiD effect_summary #1063: both formula orders must produce identical effect_summary statistics.
  • test_effect_summary_ols_did_categorical_group_spelling, new: the C(group) spelling must match the plain spelling. The old lookup failed on it too.
  • Full causalpy/tests/test_reporting.py: 166 passed. Full suite: 2283 passed; the only 3 failures (test_doctest_sampling.py) also fail on the untouched branch head in my local environment, so they are pre-existing and unrelated.
  • Local patch-coverage gate (diff-cover vs this base branch, 96% threshold): 100% on all 22 changed lines.
  • Manual check on the banks dataset: all three spellings return identical output (effect 20.12, 95% CI [19.12, 21.12]). Two of the three raised ValueError before the fix.

Notes

  • The diagnosis, the error message, and the order-independence test are @Mari1988's work (co-authored on the commit). Maintainers may want to close Fix order-dependent interaction-term lookup in DiD effect_summary #1063 once the migration branch lands, since this supersedes it there.
  • Same bug family, out of scope here: PrePostNEGD._get_treatment_effect_coeff in causalpy/experiments/prepostnegd.py still substring-matches the group name and could misfire on a column like subgroup. Worth a follow-up issue.

Checklist

  • Regression tests added
  • prek run --all-files passes
  • Local patch-coverage gate passes (DIFF_COVER_COMPARE_BRANCH=origin/pymc6_and_pymcmarketing1_migration)
  • No ARCHITECTURE.md change needed (internal lookup and error message only)

_compute_statistics_did_ols located the DiD interaction coefficient by substring-matching the concatenated string "group:post_treatment" against the patsy design-matrix column names. patsy names interaction columns by formula order, so a formula written as "y ~ 1 + post_treatment*group" produces the label "post_treatment[T.True]:group" and effect_summary() raised "ValueError: Could not find interaction term ..." even though the fitted model is identical. The same lookup also failed for the categorical spelling "C(group)*post_treatment", whose label is "C(group)[T.1]:post_treatment[T.True]".

Replace the substring match with the experiment's structural _is_treatment_interaction() helper (introduced in #994), which DifferenceInDifferences.algorithm() already uses to locate the causal_impact coefficient and which handles reversed factor order and the C(group) spelling. Also make the no-match error message name both variables instead of echoing the concatenated string.

Regression tests assert that both formula orders and the C(group) spelling produce identical effect_summary statistics.

Co-authored-by: Mari1988 <[email protected]>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

👋 Welcome to CausalPy, @derwells!

Thank you for opening your first pull request! We're excited to have you contribute to the project. 🎉

Here are a few tips to help your PR get merged smoothly:

  • ✅ Make sure all CI checks pass (tests, linting, type checking)
  • 📝 Run prek run --all-files locally before pushing
  • 📖 Check our Contributing Guide for more details

A maintainer will review your changes soon. Thanks for helping make CausalPy better! 🚀


💼 LinkedIn Shoutout: Once your PR is merged, we'd love to give you a shoutout on LinkedIn to thank you for your contribution! If you're interested, just drop your LinkedIn profile URL in a comment below.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.11%. Comparing base (194343c) to head (b56e2d9).

Additional details and impacted files
@@                         Coverage Diff                         @@
##           pymc6_and_pymcmarketing1_migration    #1153   +/-   ##
===================================================================
  Coverage                               97.11%   97.11%           
===================================================================
  Files                                     122      122           
  Lines                                   21634    21649   +15     
  Branches                                 1175     1175           
===================================================================
+ Hits                                    21009    21024   +15     
  Misses                                    414      414           
  Partials                                  211      211           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 causalpy | 🛠️ Build #33960196 | 📁 Comparing b56e2d9 against latest (57cd9e0)

  🔍 Preview build  

249 files changed · + 30 added · ± 197 modified · - 22 deleted

+ Added

± Modified

- Deleted

@drbenvincent
drbenvincent merged commit c5b3d4f into pymc6_and_pymcmarketing1_migration Aug 8, 2026
16 checks passed
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