causalis.scenarios.uplift.policy

Interpretable treatment decisions learned from cross-fitted IRM signals.

Module Contents

Classes

UpliftPolicyEvaluation

Independent evaluation of a frozen policy, in outcome units.

UpliftPolicyTree

Learn a shallow, greedy treatment policy from a fitted IRM.

API

class causalis.scenarios.uplift.policy.UpliftPolicyEvaluation

Independent evaluation of a frozen policy, in outcome units.

Let :math:\widehat\pi(Z)\in\{0,1\} be a policy learned on training clients, where :math:Z contains the policy features. On independent evaluation clients, summary() estimates policy gains and rules_summary() estimates effects within each fixed leaf.

The causal policy value is

.. math::

V(\pi) = \mathbb{E}\!\left[
    \pi(Z)Y(1) + (1-\pi(Z))Y(0)\right].

Reports contain differences in this value, not the absolute expected outcome. Higher outcomes are better; no treatment cost is subtracted.

Attributes

diagnostics : dict Training and evaluation overlap settings, clipping counts and fractions, sample sizes, and inference conventions. Clipping can introduce bias. alpha : float Significance level; intervals have nominal coverage :math:1-\alpha.

Notes

Approximate inference assumes unconfoundedness, adequate overlap, suitable nuisance estimation, and independent observations. Rule intervals are pointwise HC3 intervals, not individual-effect or simultaneous intervals. Evaluation conditions on the learned policy; it does not quantify variation from retraining or justify tuning the policy using evaluation results.

diagnostics: dict[str, Any]

None

alpha: float

None

summary() → pandas.DataFrame

Return policy gains, standard errors, intervals, and targeting share.

On the evaluation sample of size :math:n, let :math:a_i=\widehat\pi(Z_i) and let :math:\Gamma_i be the cross-fitted doubly robust signal defined in :class:UpliftPolicyTree. Each comparison uses a paired per-client signal:

.. math::

S_i^{\mathrm{policy\_vs\_none}} &= a_i\Gamma_i, \\
S_i^{\mathrm{policy\_vs\_all}} &= (a_i-1)\Gamma_i, \\
S_i^{\mathrm{all\_vs\_none}} &= \Gamma_i.

For each signal :math:S_i, the reported estimate and standard error are

.. math::

\widehat\Delta = \overline S = \frac{1}{n}\sum_{i=1}^n S_i,
\qquad
\widehat{\mathrm{SE}}(\widehat\Delta)
= \sqrt{\frac{\sum_{i=1}^n(S_i-\overline S)^2}{n(n-1)}}.

The approximate normal interval is

.. math::

\widehat\Delta \;\pm\;
\Phi^{-1}(1-\alpha/2)\widehat{\mathrm{SE}}(\widehat\Delta).

Returns

pandas.DataFrame A copy with one row per comparison. value is the average gain per evaluation client, including clients the policy does not treat; it is not the average among targeted clients alone. std_error, ci_lower, and ci_upper describe that gain. n_obs is the full evaluation size. treatment_fraction is :math:n^{-1}\sum_i a_i, the learned policy’s targeting share in every row, not the historical treatment rate or the baseline share.

Notes

Positive policy_vs_all favors the policy over treating everybody; negative values favor treating everybody. An interval crossing zero does not establish the direction of the gain.

rules_summary() → pandas.DataFrame

Return effects and support for every frozen rule.

A leaf :math:\ell defines a region :math:R_\ell by the conjunction of its predicates. Its evaluation clients are :math:I_\ell=\{i:Z_i\in R_\ell\}, with :math:n_\ell=|I_\ell|. The target is the group average treatment effect

.. math::

\tau_\ell = \mathbb{E}[Y(1)-Y(0)\mid Z\in R_\ell],
\qquad
\widehat\tau_\ell = \frac{1}{n_\ell}
    \sum_{i\in I_\ell}\Gamma_i.

For a supported leaf, HC3 inference for the saturated group regression gives

.. math::

\widehat{\mathrm{SE}}_{\mathrm{HC3}}(\widehat\tau_\ell)
= \sqrt{\frac{\sum_{i\in I_\ell}
        (\Gamma_i-\widehat\tau_\ell)^2}{(n_\ell-1)^2}},
\qquad
\mathrm{CI}_\ell = \widehat\tau_\ell \;\pm\;
    \Phi^{-1}(1-\alpha/2)
    \widehat{\mathrm{SE}}_{\mathrm{HC3}}(\widehat\tau_\ell).

Returns

pandas.DataFrame A copy with one row per training leaf, including leaves with no evaluation clients. Columns are:

* ``rule_id``: stable identifier of the learned leaf.
* ``conditions``: readable conjunction defining its region.
* ``predicates``: exact ``(feature, operator, threshold)`` tuples.
* ``action``: training decision :math:`a_\ell`, either 1 (target)
  or 0 (do not target); evaluation never changes it.
* ``n_eval``: :math:`n_\ell`.
* ``n_treated``: :math:`\sum_{i\in I_\ell}D_i`.
* ``n_control``: :math:`\sum_{i\in I_\ell}(1-D_i)`.
* ``status``: ``ok`` when both historical arms are present,
  ``empty`` when :math:`n_\ell=0`, or
  ``insufficient_arm_support`` when one historical arm is absent.
  ``ok`` is a support check, not proof of reliable causal estimation.
* ``value``: :math:`\widehat\tau_\ell`, irrespective of the action.
* ``std_error``: the HC3 standard error above.
* ``ci_lower``, ``ci_upper``: endpoints of the pointwise interval.

Notes

Unsupported and empty leaves have NaN effect estimates and intervals. Their clients remain in the overall policy evaluation. A positive value means estimated benefit from treatment even if action is 0. An interval entirely above/below zero supplies evidence of positive/negative group uplift; an interval crossing zero leaves its direction unresolved. These intervals describe group means, not effects for individual clients. Disagreement with the training action is evidence to review the policy, not an automatic update to it.

class causalis.scenarios.uplift.policy.UpliftPolicyTree(*, max_depth: int = 2, min_samples_leaf: int = 50, min_samples_per_arm: int = 5)

Bases: sklearn.base.BaseEstimator

Learn a shallow, greedy treatment policy from a fitted IRM.

Let :math:D\in\{0,1\} denote historical treatment, :math:Y the observed outcome, :math:X all pre-treatment adjustment variables, and :math:Z the subset allowed in policy rules. Under consistency, unconfoundedness :math:(Y(0),Y(1))\perp D\mid X, and overlap :math:0<e(X)=P(D=1\mid X)<1, the CATE is

.. math::

\tau(x)=\mathbb{E}[Y(1)-Y(0)\mid X=x]
       =g_1(x)-g_0(x),
\qquad g_d(x)=\mathbb{E}[Y\mid X=x,D=d].

Using out-of-fold nuisance predictions, construct the doubly robust signal

.. math::

\Gamma_i = \widehat g_1^{(-k(i))}(X_i)
           -\widehat g_0^{(-k(i))}(X_i)
  +\frac{D_i\{Y_i-\widehat g_1^{(-k(i))}(X_i)\}}
         {\widetilde e_i}
  -\frac{(1-D_i)\{Y_i-\widehat g_0^{(-k(i))}(X_i)\}}
         {1-\widetilde e_i},

where :math:k(i) is the fold containing observation :math:i and :math:\widetilde e_i=\min(1-\epsilon, \max(\epsilon,\widehat e^{(-k(i))}(X_i))) is IRM’s clipped propensity. Clipping stabilizes denominators but can bias the signal; double robustness does not remove hidden confounding or guarantee validity under clipping.

The empirical policy objective, relative to treating nobody, is

.. math::

\widehat J(\pi)=\frac{1}{n_{\mathrm{train}}}
    \sum_{i\in\mathcal T}\pi(Z_i)\Gamma_i.

The tree greedily improves this objective. It does not solve a global optimization over all possible trees; see :meth:fit for the split reward.

Parameters

max_depth : int, default 2 Maximum number of splits along a path; zero learns a constant policy. min_samples_leaf : int, default 50 Minimum observations in each child of an accepted split. min_samples_per_arm : int, default 5 Minimum treated and control observations in each child.

Notes

A leaf treats when its training mean signal is positive. Greedy search can miss interactions needing a zero-gain first split. If no split is feasible, a constant root policy is retained even when the sample is smaller than the child-size limits.

All adjustment variables belong in IRM; policy_features only limits the rules. Features must be numeric, finite, and measured before intervention. Higher outcomes are better; costs, capacity constraints, weighted samples, and overlap-based sample dropping are not supported. Decisions estimate group benefit, not whether each individual will benefit.

Split clients before fitting either IRM or tuning the policy. Evaluation checks disjoint IDs but cannot verify the provenance of nuisance learners or prevent reuse of evaluation results for tuning.

Initialization

fit(train_irm: causalis.scenarios.unconfoundedness.model.IRM, *, policy_features: Sequence[str] | None = None)

Discover rules using only the training IRM’s cross-fitted signals.

For a node containing training indices :math:A, the optimal constant action and its unnormalized reward are

.. math::

a_A = \mathbf{1}\!\left\{\sum_{i\in A}\Gamma_i>0\right\},
\qquad W(A)=\max\!\left(0,\sum_{i\in A}\Gamma_i\right).

A candidate feature/threshold pair partitions :math:A into :math:L=\{i\in A:Z_{ij}\le t\} and :math:R=\{i\in A:Z_{ij}>t\}. Its gain is

.. math::

G(j,t;A)=W(L)+W(R)-W(A).

Accept the feasible split with greatest positive gain, allowing for floating-point roundoff. Both children must satisfy the total and per-arm sample limits. Recurse until no improvement is available or max_depth is reached. Feature order and then ascending thresholds break ties. A zero leaf signal recommends no treatment.

Parameters

train_irm : IRM Fitted, unweighted IRM with ``overlap_policy=’clip’` and unchanged CausalData containing unique, nonmissing client IDs. policy_features : sequence of str, optional Ordered, nonempty subset of fitted confounders allowed in the rules. Defaults to all confounders. Other confounders still enter the nuisance models and the DR signal.

Returns

UpliftPolicyTree The fitted policy. No IRM models are fitted or modified.

Notes

Signals are computed from fitted nuisances, independently of the last requested IRM estimand (including ATTE) and any lazy CATE scoring models.

rules() → pandas.DataFrame

Return the learned rules and descriptive training statistics.

For training clients :math:\mathcal T_\ell in leaf :math:\ell,

.. math::

\overline\Gamma_{\ell,\mathrm{train}}
  =\frac{1}{|\mathcal T_\ell|}
     \sum_{i\in\mathcal T_\ell}\Gamma_i,
\qquad
a_\ell=\mathbf{1}\{
    \overline\Gamma_{\ell,\mathrm{train}}>0\}.

Returns

pandas.DataFrame A copy containing rule_id, readable conditions, exact predicates tuples (feature, operator, threshold), action (:math:a_\ell), n_train, n_treated_train, n_control_train, and train_mean_uplift_descriptive (:math:\overline\Gamma_{\ell,\mathrm{train}}). All predicates along a path must hold; the leaves form a disjoint partition.

Notes

Training means are selected on the same data used to discover the rules and carry no independent confidence intervals. Use :meth:evaluate and :meth:UpliftPolicyEvaluation.rules_summary for held-out evidence.

assign(clients: pandas.DataFrame, *, user_id: str) → pandas.DataFrame

Assign clients without outcomes or historical treatment labels.

For disjoint learned regions :math:R_\ell and frozen leaf actions :math:a_\ell, the assignment function is

.. math::

\widehat\pi(z)=\sum_{\ell=1}^K
    a_\ell\mathbf{1}\{z\in R_\ell\}.

Each client belongs to exactly one leaf. A threshold test sends :math:z_j\le t left and :math:z_j>t right, including equality on the left. Neither outcomes nor estimated individual effects enter this assignment step.

Parameters

clients : pandas.DataFrame Client IDs and the fitted policy feature columns. Features must be numeric, finite, and nonmissing. Extra columns are ignored. user_id : str Column containing unique, nonmissing IDs. Cannot be rule_id or action, which are reserved output column names.

Returns

pandas.DataFrame Original ID column plus rule_id and integer action: 1 means target, 0 means do not target. Input order, ID dtype, and index are preserved. Empty inputs return the same output schema.

evaluate(eval_irm: causalis.scenarios.unconfoundedness.model.IRM, *, alpha: float = 0.05) → causalis.scenarios.uplift.policy.UpliftPolicyEvaluation

Evaluate frozen decisions on an IRM fitted on disjoint clients.

For an independent evaluation population and a fixed learned policy, the estimands are

.. math::

\Delta_{\pi,0} &= V(\widehat\pi)-V(0)
    =\mathbb{E}[\widehat\pi(Z)\tau(X)], \\
\Delta_{\pi,1} &= V(\widehat\pi)-V(1)
    =\mathbb{E}[(\widehat\pi(Z)-1)\tau(X)], \\
\Delta_{1,0} &= V(1)-V(0)=\mathbb{E}[\tau(X)].

Estimate them with evaluation-only cross-fitted DR signals. See :meth:UpliftPolicyEvaluation.summary for paired-signal standard errors and :meth:UpliftPolicyEvaluation.rules_summary for leaf HC3 intervals.

Parameters

eval_irm : IRM Independently fitted, unweighted IRM with overlap_policy='clip'. Outcome, treatment, and ordered confounder definitions must match training. Its original CausalData and IDs must remain unchanged, and its IDs must be disjoint from training IDs. alpha : float, default 0.05 Significance level in :math:(0,1) for nominal :math:100(1-\alpha)\% intervals.

Returns

UpliftPolicyEvaluation Overall policy gains, per-rule effects and support, and overlap diagnostics. The policy, training summaries, and both IRMs are left unchanged.

Notes

Unsupported leaves have unavailable subgroup estimates; their clients remain in the overall policy evaluation, whose inference relies on population overlap. An interval crossing zero does not establish a zero effect, and evaluation never changes a learned action. If these results are used to revise or select policies, evaluate the selected policy on another untouched sample.