Skip to content

[Repo Assist] fix(estimators): use treatment/control indicators in PropensityScoreWeightingEstimator - #1778

Closed
github-actions[bot] wants to merge 1 commit into
mainfrom
repo-assist/fix-psw-hardcoded-treatment-values-20260827-c9311cc2921fa4bf
Closed

github-actions[bot] wants to merge 1 commit into
mainfrom
repo-assist/fix-psw-hardcoded-treatment-values-20260827-c9311cc2921fa4bf

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🤖 This PR was created by Repo Assist, an automated AI assistant.

Problem

PropensityScoreWeightingEstimator hardcoded the assumption that treatment is encoded as {0, 1} in every IPW weight formula. The method used data[treatment_col] directly in arithmetic:

# Before — only works when T ∈ {0, 1}
ipst_sum = sum(data[T] / data[ps_col])
ipsc_sum = sum((1 - data[T]) / (1 - data[ps_col]))
data["ips_weight"] = data[T] / data[ps_col] + (1 - data[T]) / (1 - data[ps_col])
# ... and 12 more similar expressions

For any binary treatment encoding other than {0, 1} (e.g. {1, 2}, {-1, 1}, boolean), all weight variants (vanilla IPS, Hajek, stabilized — and their tips/cips ATT/ATC variants) are computed incorrectly. The numerators and denominators of every weight formula include data[T] as if it equals 1 for treated and 0 for control.

Fix

Derive a float binary indicator immediately after propensity-score trimming:

d = (data[self._target_estimand.treatment_variable[0]] == treatment_value).astype(float)

Then replace every data[T] with d and every (1 - data[T]) with (1 - d) throughout all weight formulas. This is correct for any binary encoding.

Relationship to Other PRs

Test Status

✅ New regression test test_psw_non_zero_one_treatment_encoding — treatment {1, 2} (control=1, treated=2) now produces a finite ATE estimate close to the true value of 3.0.

✅ Syntax check passes on both modified files.

✅ black and isort formatting verified (imports sorted correctly, line length ≤ 120).

⚠️ Full parametrised test suite (TestPropensityScoreWeightingEstimator) not run due to environment limitations; the change is a mechanical substitution of data[T] → d throughout weight formulas.

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

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

…eightingEstimator

PSW weight formulas used data[treatment_col] directly in arithmetic (e.g.
data[T] / ps, 1 - data[T]), which only gives correct results when treatment
is encoded as {0, 1}.  For any other binary encoding ({1, 2}, {-1, 1}, etc.)
all IPW weight variants (vanilla, Hajek, stabilized) were computed incorrectly.

Fix: derive a float indicator d = (data[T] == treatment_value).astype(float)
immediately after propensity-score trimming, then use d / (1-d) throughout
every weight formula.  This mirrors the fix applied to PSS in PR #1766 and
to PSM in PR #1762, completing the trilogy for all three propensity-score
estimators.

Adds regression test: treatment encoded as {1, 2} now produces a finite,
sensible ATE estimate (was giving wrong weights before this fix).

Note: the base-class binary check in PropensityScoreEstimator.fit() still
validates that treatment is binary via two distinct values (relaxed in #1766);
this commit focuses only on the weight-calculation fix in PSW.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@emrekiciman
emrekiciman marked this pull request as ready for review August 30, 2026 07:40
@emrekiciman
emrekiciman requested a balanced review from Copilot August 30, 2026 07:40

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes PropensityScoreWeightingEstimator weight computations for non-{0,1} binary treatment encodings by replacing arithmetic on the raw treatment column with an explicit treated/control indicator, and adds a regression test to prevent future regressions.

Changes:

  • Introduce a binary treated indicator (d) after propensity-score trimming and use it across all IPS/Hajek/stabilized weight formulas.
  • Update effect calculation to use the treated indicator instead of the raw treatment column.
  • Add a regression test covering treatment encoded as {1,2}.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
dowhy/causal_estimators/propensity_score_weighting_estimator.py Replaces hardcoded {0,1} treatment arithmetic with a derived treated indicator in all weighting formulas.
tests/causal_estimators/test_propensity_score_weighting_estimator.py Adds a regression test ensuring PSW works with treatment encoded as {1,2}.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +148 to +149
# Binary indicator for treated units: works for any binary encoding (e.g. {1,2}, {-1,1}, {True,False})
d = (data[self._target_estimand.treatment_variable[0]] == treatment_value).astype(float)
num_units = len(data[self._target_estimand.treatment_variable[0]])
num_treatment_units = sum(data[self._target_estimand.treatment_variable[0]])
ipst_sum = sum(d / data[self.propensity_score_column])
ipsc_sum = sum((1 - d) / (1 - data[self.propensity_score_column]))
Comment on lines +162 to +164
data["ips_weight"] = d / data[self.propensity_score_column] + (1 - d) / (
1 - data[self.propensity_score_column]
)
data[self.propensity_score_column] = np.maximum(self.min_ps_score, data[self.propensity_score_column])

# Binary indicator for treated units: works for any binary encoding (e.g. {1,2}, {-1,1}, {True,False})
d = (data[self._target_estimand.treatment_variable[0]] == treatment_value).astype(float)
Comment on lines +105 to +113
rng = np.random.default_rng(42)
n = 2000
X = rng.standard_normal(n)
# Treatment encoded as {1, 2} (control=1, treated=2)
ps = 1 / (1 + np.exp(-X))
T = (rng.random(n) < ps).astype(int) + 1 # 1 = control, 2 = treated
Y = 3.0 * (T == 2) + 0.5 * X + rng.standard_normal(n)

df = pd.DataFrame({"X": X, "T": T, "Y": Y})
@emrekiciman

Copy link
Copy Markdown
Member

superseded by #1795

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