Skip to content

Deprecate positional treatment/y across all learners for sklearn Pipeline compat (#854) - #975

Merged
jeongyoonlee merged 3 commits into
masterfrom
feature/854-fit-arg-order
Jul 31, 2026
Merged

Deprecate positional treatment/y across all learners for sklearn Pipeline compat (#854)#975
jeongyoonlee merged 3 commits into
masterfrom
feature/854-fit-arg-order

Conversation

@jeongyoonlee

@jeongyoonlee jeongyoonlee commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What & why

Kicks off #854 — making CausalML's learners compatible with sklearn.pipeline.Pipeline, the one intentional breaking change on the v1.0 roadmap (M1).

Pipeline calls the final estimator's fit(X, y) positionally, but CausalML learners take fit(X, treatment, y, ...)y is third. In v1.0 the positional order becomes (X, y, treatment, ...).

Reordering two required positional arguments can't be done in one step without risking a silent y/treatment swap for existing positional callers. So this is a two-step deprecation, and this PR is step one (non-breaking):

A deprecation window is one-shot: anything whose positional order changes at v1.0 has to warn in this release, or it needs a second cycle of its own. That is the constraint that sets this PR's scope — the warning has to be complete now, even though the flip is still a PR away.

How

  • causalml/inference/_arg_order.py holds a re-entrancy-guarded decorator that derives each method's v1.0 order from its own signature: X, then y, then treatment, with every other parameter keeping its relative position. Each warning names that method's real target order rather than assuming (X, treatment, y, ...), so BaseDRIVLearner.fit reports (X, y, treatment, assignment, ...) and IVRegressor.fit reports (X, y, treatment, w).
  • Coverage is decided by signature, not by method name. Any method whose positional order would change is shimmed. An earlier draft of this PR used a four-name allowlist (fit / fit_predict / estimate_ate / predict) and silently missed eleven public methods (Extend the #854 deprecation shim to the remaining public treatment-before-y methods #981) — see Scope below.
  • __init_subclass__ lives on SerializableLearner, the shared mixin, so one hook covers the meta-learners, the causal/uplift trees and forests, and the IV learners with no per-method edits. functools.wraps preserves the real signatures for introspection and sklearn metadata routing.
  • Classes outside that hierarchy — TF/JAX DragonNet, Torch/JAX CEVAE, PolicyLearner, TMLELearner, Sensitivity — apply the same decorator at the class level.
  • The guard ensures internal delegation — fit_predict → fit/predict, subclass fit → super().fit(X, treatment, y, *args, **kwargs), estimate_ate → fit_predict → fit — warns at most once per top-level call.
  • No internal call sites needed changing: CausalML's own code (metrics/sensitivity.py, dataset/synthetic.py, …) already calls these methods with keywords.

Scope

Two incidental fixes

Both were required to make the above work, and both are visible in the diff:

  • causalml/inference/tree/utils.py::timeit now uses functools.wraps. Without it, CausalTreeRegressor.bootstrap_pool — its only user — reported (*args, **kw) to inspect.signature and "wrapped" as its __name__, which is exactly what hid it from a signature-based shim. Pre-existing bug; it also means anything introspecting that method, sklearn included, was seeing the wrong signature.
  • v1_order reorders only when both y and treatment are present. Hoisting y on its own reached the vendored sklearn tree builders (build(tree, X, y, ...), where y would land ahead of X) and SemiSynthDataGenerator.fit(X, w, y, ...), whose treatment vector is named w. Both are pinned as regression cases.

Tests

tests/test_fit_arg_order.py (52 cases): positional-use warning across S/T/X/R/DR and the classifier variants, keyword silence, single-warning delegation (fit_predict / estimate_ate / subclass super().fit), signature preservation, positional == keyword equivalence (guards against a silent y/treatment swap), predict warning/silence, bootstrap warning, the v1.0 ordering rule itself (DRIV's assignment, the uplift validation triple, the Sensitivity shape, and the two cases that must not move), and behaviour on CausalTreeRegressor / IVRegressor.

Two completeness guards walk the installed package: one fails if any public method's order would change without the shim, the other fails if coverage regresses to a fixed list of method names. They cover the optional backends only in the lanes where those backends are installed.

Full local run: 418 passed, 10 skipped.

Refs #854. Part of epic #980; closes #981.

Kick off the scikit-learn Pipeline-compatibility change for meta-learners:
the positional fit() argument order will move from (X, treatment, y, ...) to
(X, y, treatment, ...) in v1.0. This is the first, non-breaking step of a
two-step deprecation.

- Add a re-entrancy-guarded shim in BaseLearner that emits a FutureWarning
  when treatment/y are passed positionally to fit/fit_predict/estimate_ate,
  steering callers to order-independent keyword arguments. It is wired in via
  __init_subclass__ so every S/T/X/R/DR learner (and its subclasses) is
  covered with no per-method edits, and the real signatures are preserved via
  functools.wraps. The guard ensures internal delegation (fit_predict -> fit,
  subclass fit -> super().fit, estimate_ate -> fit_predict -> fit) warns at
  most once per top-level call.
- The positional order is UNCHANGED for now, so no existing call silently
  breaks; keyword calls -- fit(X, y=y, treatment=treatment) -- are silent and
  stay correct across the v1.0 flip.
- Add tests/test_fit_arg_order.py covering the warning on positional use,
  keyword silence, single-warning delegation, subclass super().fit,
  signature preservation, and positional==keyword equivalence (guarding
  against a silent y/treatment swap).
Step one of #854 warned only on the BaseLearner meta-learners. A
deprecation window is one-shot: anything whose positional order changes
at v1.0 has to warn in this release or it needs a second cycle. Widen
the shim so v1.0 can flip the whole package at once.

- Move the shim to causalml/inference/_arg_order.py and derive the
  target order from each method's own signature: X, then y, then
  treatment, with every other parameter keeping its relative position.
  Each warning now names that method's real order instead of assuming
  (X, treatment, y, ...). BaseDRIVLearner.fit therefore resolves to
  (X, y, treatment, assignment, ...) and IVRegressor.fit to
  (X, y, treatment, w).

- Host __init_subclass__ on SerializableLearner rather than BaseLearner.
  That one move covers the causal/uplift trees and forests and the IV
  learners as well as the meta-learners. The trees matter most: they
  already inherit sklearn's ForestRegressor/BaseDecisionTree and
  override fit(X, y, ...) with fit(X, treatment, y, ...), so leaving
  them behind would ship v1.0 with the most sklearn-shaped classes
  keeping the least sklearn-shaped signature.

- Decorate the six learners outside that hierarchy: the TF/JAX
  DragonNet, the Torch/JAX CEVAE, PolicyLearner and TMLELearner.

- Shim predict too. Its (X, treatment, y, ...) is unaffected by
  Pipeline, but without this v1.0 would leave fit and predict holding
  the same two arguments in opposite orders on one class. Methods
  without treatment/y, such as BaseRLearner.predict, are returned
  unwrapped.

Tests go 19 -> 43, including a completeness guard that walks the
package and fails if any method taking treatment and y positionally
lacks the shim, so a learner added later cannot miss the window.

Refs #854.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jeongyoonlee jeongyoonlee changed the title Deprecate positional treatment/y in meta-learner fit() for sklearn Pipeline compat (#854) Deprecate positional treatment/y across all learners for sklearn Pipeline compat (#854) Jul 31, 2026
The shim keyed off four method names (fit/fit_predict/estimate_ate/predict).
A scan for public methods taking `treatment` before `y` found eleven more
that got no warning at all: four `bootstrap` variants,
`fit_bootstrap_ensemble`, `bootstrap_pool`, `prune`, `fill`, and the three
`Sensitivity.get_*` helpers. A deprecation window is one-shot, so a method
missing from it needs a second cycle of its own.

Replace the name allowlist with the property we actually care about: shim
any method whose positional order changes at v1.0. That fixes the class of
bug rather than the eleven instances, and the completeness test now scans
every public method instead of the same four names it was blind to.

Resolves both open questions on the epic:

- `UpliftTreeClassifier.fit`'s validation triple is reordered in place, so
  it stays consistent with the main one: (X, y, treatment, X_val, y_val,
  treatment_val, ...) rather than leaving X_val, treatment_val, y_val.
- The `Sensitivity` helpers are in scope. Their (X, p, treatment, y) is a
  third argument shape and becomes (X, y, treatment, p).

Two fixes the work required:

- `timeit` did not use functools.wraps, so `CausalTreeRegressor.bootstrap_pool`
  reported `(*args, **kw)` to `inspect.signature` and `"wrapped"` as its
  `__name__` — which is exactly why a signature-based shim could not see it.
  This also repairs introspection for anything sklearn does with that method.

- `v1_order` now reorders only when both `y` and `treatment` are present.
  Hoisting `y` on its own reached the vendored sklearn tree builders
  (`build(tree, X, y, ...)`, where it would land ahead of `X`) and
  `SemiSynthDataGenerator.fit(X, w, y, ...)`. Both are pinned as tests.

Tests 43 -> 52. Full suite: 418 passed, 10 skipped.

Closes #981. Part of #980.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jeongyoonlee jeongyoonlee added the refactoring Code refactoring label Jul 31, 2026
@jeongyoonlee
jeongyoonlee merged commit 80dbd98 into master Jul 31, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactoring Code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend the #854 deprecation shim to the remaining public treatment-before-y methods

2 participants