Randomization inference for matched pairs with binary outcomes.
Given matched pairs in which each unit’s outcome is 0 or 1, this package answers three
questions without assuming monotonicity of the treatment effect and without any
distributional assumption beyond the within-pair coin flip:
How large is the effect?PairedOutcomeTable gives exact
randomization confidence sets for the attributable effect and for the
average treatment effect (ATT, ATU, or ATE).
How fragile is it?PairedOutcomeTable.sensitivity_analysis()
reports how much unmeasured confounding – Rosenbaum’s Gamma – the
finding withstands before it can no longer be distinguished from bias.
How do effects combine?LinearCombinationEstimator and
DiffInDiff carry that inference through a weighted sum of net
effects measured on the same pairs, such as a post-period effect minus
a pre-period placebo.
A one-minute start, from two aligned 0/1 outcome vectors:
frompair_matchimportPairedOutcomeTabletable=PairedOutcomeTable.from_outcomes(treated_y,control_y)print(table)# the 2x2 table, margins, and success ratesprint(table.analyze())# point estimate, confidence set, sensitivity
See USAGE.md (also available at runtime via pair_match.usage()) for the full
guide.
Note
alpha is the total error rate of an interval, so alpha=0.10
produces a 90% two-sided interval (5% in each tail). Functions
that take confidence instead use confidence=1-alpha.
An effect-size column available in a PairedOutcomeTable analysis.
EFFECT is the scaled treatment effect (a proportion) for the analysis
target: the average effect on the treated (ATT) for A_1, on the untreated
(ATU) for A_0, or the average treatment effect (ATE). It is the attributable
effect divided by n_pairs, so for A_1 / A_0 it is an effect on the
matched units (for whom the matched counterparts stand in as counterfactuals), not a
population ATE. ISUCCESSES is the attributable effect – the net count of
successes among treated units caused by treatment (S_10-S_01, an integer);
COST_PER_ISUCCESS is the treatment cost per incremental success (needs
PairedOutcomeTable.spend).
Coerce a member or a case-insensitive string to an EffectSize.
The legacy name "att" – the pre-target-generic label for the scaled column
– is accepted as an alias for EFFECT, so options and payloads written
before the rename still resolve.
The 2x2 table of outcome patterns among matched pairs.
Each pair contributes to exactly one cell according to the observed binary outcomes
of its treated and control units. The first subscript is the treated unit’s outcome,
the second is the control unit’s.
Build the table from a matching and an outcome column.
Reads the binary outcome for each matched unit by index label from the frames
the matching was built from – typically the same df_treated /
df_control passed to the matcher, which already carry the outcome as a
column.
Parameters:
result (Pairing) – A MatchResult, or any object carrying
aligned treated_index and control_index sequences.
outcome (str) – Name of the binary outcome column, present in both df_treated and
df_control.
df_treated (DataFrame) – The treated and control frames the matching was built from. The outcome is
gathered from df_treated at result.treated_index and from
df_control at result.control_index.
df_control (DataFrame) – The treated and control frames the matching was built from. The outcome is
gathered from df_treated at result.treated_index and from
df_control at result.control_index.
spend (float, optional) – Total cost of treatment (see spend).
Summarize the matched-pair effect: estimates, CIs, p-value, Γ•.
Bundles the net-effects analysis of this table into a single displayable result:
the scaled effect – the ATT for the default target='ATT', the ATU for
target='ATU' – and the attributable effect (ISUCCESSES) with (1-alpha)-coverage confidence sets at sensitivity gamma, the worst-case
p-value for H_0:<target>=null_value at gamma, and the Rosenbaum
sensitivity value Γ• at level alpha.
Parameters:
alpha (float) – Significance level: the confidence sets have coverage 1-alpha and the
p-value is flagged significant below it (default 0.10, the RL MDS
convention).
gamma (float) – Sensitivity parameter entertained for the confidence sets and the p-value
(1 = randomized; larger widens the sets). Distinct from Γ•, which is
a property of the data, not a value we choose.
target ({'ATT', 'ATU', 'ATE'}) – The effect the p-value and Γ• concern, and the one reported as
ISUCCESSES and (scaled) in the leading column (default 'ATT').
A_1 is the effect on the treated (scaled column ATT), A_0 the
effect on the untreated (ATU), and ATE the average effect (ATE),
whose ISUCCESSES is the average attributable effect (A_1+A_0)/2
and whose test combines both effects under the Rigdon-Hudgens constraint
(see _ate_worst_case_pvalue()).
null_value (int) – Null value tested by the p-value and Γ• (default 0), on the
attributable-effect (count) scale (for ATE, the average-effect count
ATE*n_pairs).
monotonic (bool) – Assume treatment never hurts any unit (no prevention). The net- effects
procedure’s main contribution is not to require this; the default
(False) makes no such assumption. Opting in is valid when monotonicity
is substantively defensible and narrows every set, sharpens the p-value, and
raises Γ•.
alternative ({'two-sided', 'less', 'greater'}) –
The kind of test/interval:
”two-sided”: H_0:<target>=null_value; both the p-value and
the confidence sets are two-sided (each bound at alpha/2).
”greater”: H_0:<target><=null_value; the p-value is the
right-tail worst-case tail, and every confidence set becomes
one-sided [lb,+inf) (lower bound finite, upper +inf).
”less”: H_0:<target>>=null_value; the p-value is the
left-tail worst-case tail, and every confidence set becomes
one-sided (-inf,ub] (upper bound finite, lower -inf).
Defaults to “two-sided”.
method ({'exact', 'normal', 'auto'}) – How the confidence sets are inverted, forwarded to
attributable_effect_interval(): 'exact' by binary search over
exact binomial tails, 'normal' by the closed-form large- sample
approximation, or 'auto' (default) to choose from the sample size. Only
the confidence sets honor this – the p-value and Γ• are always exact
(they need no inversion, so exactness is free). Defaults to 'auto',
matching expanded_confidence_interval() and gamma_star().
The ATT is the treated-side attributable effect A_1 scaled by 1/n_pairs: this returns attributable_effect_interval() for A_1 at
coverage 1-alpha under a hidden bias of odds ratio gamma, divided by
the pair count. It is the matched-pair binary analog of
TreatmentEffectEstimator.confidence_interval and agrees with
analyze()’s effect_interval when analyze targets the treated side
(its default); other targets scale a different attributable set, so their scaled
interval differs.
Parameters:
alpha (float) – Significance level; the interval has coverage 1-alpha (default 0.10, a
90% interval).
alternative ({'two-sided', 'less', 'greater'}) –
The kind of interval:
”two-sided”: both bounds finite, each side at alpha/2.
”greater”: [lb,+inf) – lb finite (right-tail test
inverted at full alpha), ub=+inf.
”less”: (-inf,ub] – ub finite (left-tail test inverted
at full alpha), lb=-inf.
Defaults to “two-sided”.
gamma (float) – Rosenbaum sensitivity parameter (>=1; default 1.0). 1 corresponds
to a randomized experiment; larger widens the interval.
monotonic (bool) – Assume treatment never hurts (no prevention); narrows the interval. Default
False.
method ({'exact', 'normal', 'auto'}) – How the worst-case test is inverted (see
attributable_effect_interval()). 'exact' (default) uses binary
search over exact binomial tails; 'normal' the closed-form large-sample
approximation; 'auto' chooses from the sample size.
Returns:
Lower and upper bounds on the ATT. For one-sided intervals only one bound is
finite (the other is +/-math.inf), per alternative.
Design-sensitivity ceiling over the discordant pairs.
Beyond this Gamma no discordancy pattern is significant at level alpha.
Only the m=S_01+S_10 discordant pairs carry sign information, so the
ceiling ((1/alpha)^(1/m)-1)^{-1} uses m rather than the full pair
count – the McNemar analog of PairedEstimator.capacity(). Returns 1.0
when there are no discordant pairs (the study is uninformative).
Confounding-only ATE interval: the estimate range implied by bias Gamma.
The range the ATE estimate could take under a hidden bias of odds ratio
gamma from confounding alone (no sampling uncertainty) – the binary
analog of PairedEstimator.sensitivity_analysis(). Collapses to ate_hat
at gamma==1 and opens with gamma, saturating at the ATE’s a-priori
range [-(S00+2S01+S11),S00+2S10+S11]/(2S) (and so never
leaving [-1,1]) rather than diverging. See _ate_sensitivity_band().
With monotonic (treatment never hurts, so the box ceilings shrink; see
_ceilings()) the band narrows, and its lower endpoints saturate at the
a-priori minimum 0 once gamma>=S10/S01.
Sensitivity/confidence ATE set: sampling and confounding uncertainty.
The Rigdon-Hudgens confidence set for the ATE at coverage 1-alpha under a
hidden bias of odds ratio gamma – the binary analog of
PairedEstimator.expanded_confidence_interval(). At gamma==1 it is
the randomized 1-alpha set; larger gamma widens it.
method selects how the worst-case tests are inverted: 'exact' (default)
by binary search over exact binomial tails, 'normal' by the closed-form
large-sample approximation (O(1) per endpoint), or 'auto' chosen from the
sample size (see attributable_effect_interval()).
With monotonic (treatment never hurts) the box ceilings shrink (see
_ceilings()), narrowing the set.
Rosenbaum sensitivity value Γ• for the ATE finding.
The largest Gamma at which the expanded (Rigdon-Hudgens)
sensitivity/confidence interval for the ATE at level alpha still excludes
null_value – the point on a plot_sensitivity() sweep where the wider
band first touches the null and the finding stops being significant. It
summarizes exactly the set expanded_confidence_interval() returns – both
attributable effects A_1 and A_0 combined through A_1+A_0=2S*ATE under the Bonferroni budget – so the dotted Γ• line and the band
cross the null together. This is a strictly smaller (more conservative) value
than inverting the A_1-only worst-case McNemar p-value alone
(sensitivity_value()), which tests a narrower hypothesis than the interval
it accompanies.
null_value is on the ATE scale – the reference line the band is tested
against (default 0, the no-effect null). A value near 1 means the
finding is fragile; a large value means it is robust to substantial hidden bias.
With monotonic (treatment never hurts) the box ceilings shrink (see
_ceilings()), which narrows the band and raises Γ•.
Parameters:
null_value (float) – ATE null the expanded band is tested against (default 0.0).
alpha (float) – Significance level; the band has coverage 1-alpha (default 0.05).
monotonic (bool) – Assume treatment never hurts (no prevention); raises Γ•. Default
False.
method ({'exact', 'normal', 'auto'}) – How the worst-case tests behind the band are inverted, forwarded to
expanded_confidence_interval() (default 'exact').
Returns:
Γ•>=1. Returns 1.0 when the band already contains null_value
in the randomized case, and math.inf when it excludes null_value for
arbitrarily large Gamma.
Sweep the sensitivity parameter and plot how the finding degrades.
The headline inference-stage diagnostic for the binary (McNemar / net-effects)
path: as the hidden-bias odds ratio Gamma grows from 1 (a randomized
experiment) upward, two intervals widen around the (bias-independent) point
estimate for the chosen target –
the sensitivity interval, the range of the effect from confounding
alone (sensitivity_analysis() for the ATE, the A_1 / A_0
analog otherwise); and
The left axis is the scaled effect (the ATT for A_1, the ATU for A_0,
the ATE for ATE); a secondary right axis rescales it to the matching count
of induced successes (iSuccesses=effect*n_pairs), so both the rate and
the count can be read off the same curves.
The study’s sensitivity value Γ• – where the wider interval first touches
null_value and the finding stops being significant – inverts the plotted
confidence band for target and is marked when it falls within the swept
range.
Parameters:
target ({'ATT', 'ATU', 'ATE'}) – Which effect to sweep: A_1 (net effect on the treated, plotted as the
ATT), A_0 (net effect on the control, the ATU), or ATE (the
Rigdon-Hudgens average effect). Defaults to 'ATT' for consistency with
analyze().
null_value (float) – The null the wider band is tested against, on the left-axis (scaled-effect)
scale; Γ• is computed against it and marked with a vertical line when it
falls within the swept range.
alpha (float) – Significance level; the wider band has coverage 1-alpha.
gamma_max (float, optional) – Largest Gamma to sweep to. Defaults to min(6,0.95*capacity) (6 is
the smoking / lung-cancer benchmark; the cap keeps the intervals finite
below the study’s capacity).
num_points (int) – Number of Gamma values swept (>=2).
monotonic (bool) – When True (treatment never hurts), both bands and Γ• use the
shrunken monotonic box ceilings, narrowing the plotted intervals.
legend_loc (str) – Matplotlib legend location (e.g. 'lowerleft', 'lowerright'); use
it to keep the legend clear of the Γ• annotation (default 'lowerleft').
title (str, optional) – Plot title; no title is drawn when omitted.
ax (Axes, optional) – Axes to draw on; a new figure and axes are created when omitted.
Returns:
The swept data (columns gamma, point, sens_lower,
sens_upper, ci_lower, ci_upper) and the axes drawn on.
target ({'ATT', 'ATU', 'ATE'}) – Which effect to bound. A_1 is the net effect among treated units; A_0
the net effect among control units. ATE returns the Rigdon-Hudgens
average-effect set on the count scale – the average attributable effect
(A_1+A_0)/2=ATE*n_pairs, the _ate_confidence_core() ATE set
scaled back up by n_pairs (so dividing it by n_pairs recovers the ATE
set exactly).
confidence (float) – Coverage of the returned set (default 0.90). Two-sided, each side is tested at
level (1-confidence)/2; one-sided, the single informative bound is
tested at the full 1-confidence.
gamma (float) – Rosenbaum sensitivity parameter (>=1). 1 corresponds to a randomized
experiment.
monotonic (bool) – Assume treatment never hurts (no prevention). Narrows the set. The default
(False) makes no monotonicity assumption – the main point of the
net-effects procedure.
alternative ({'two-sided', 'less', 'greater'}) – The kind of confidence set. “two-sided” returns two finite integer endpoints;
“greater” returns [lb,+inf) and “less” returns (-inf,ub], with the
finite bound an integer. Defaults to “two-sided”.
method ({'exact', 'normal', 'auto'}) – How the worst-case test is inverted. 'exact' (default) uses binary search
over exact binomial tails. 'normal' uses the closed-form large-sample
Gaussian approximation (O(1) per endpoint), preferable at scale. 'auto'
chooses from the sample size.
Returns:
Inclusive lower and upper endpoints of the confidence set. For the single-effect
targets (ATT, ATU) two-sided endpoints are integer counts of induced successes;
for the ATE target they are the averaged set (A_1+A_0)/2 and so may be
half-integers (the ISUCCESSES column format rounds them for display).
One-sided sets carry +/-math.inf on the uninformative side.
Confidence set for the ATT (the treated-side attributable effect A_1).
A convenience wrapper over PairedOutcomeTable.analyze(): it returns a full
PairedOutcomeAnalysis (with the default target='ATT' and
null_value=0) at significance alpha=1-confidence. The ATT set is on the
returned object as effect_interval – the A_1 attributable- effect set
(attributable_interval) scaled by 1/n_pairs, not the Rigdon-Hudgens (A_1+A_0)/(2S) ATE combination.
Because it runs the full analysis, the returned object also carries the worst-case
p-value and the sensitivity value Γ•; computing the latter is a search over
gamma, so callers that need only the interval pay for it.
monotonic (bool) – Assume treatment never hurts (no prevention); narrows the sets. Default
False.
alternative ({'two-sided', 'less', 'greater'}) – The kind of confidence set. One-sided sets carry +/-math.inf on the
uninformative side (see PairedOutcomeTable.analyze()). Defaults to
“two-sided”.
Worst-case p-value for a hypothesis about <target>.
The largest p-value over the sharp nulls consistent with the observed discordancy,
under Rosenbaum sensitivity parameter gamma (1 = randomized). The worst case
sits at a boundary corner of the consistency rectangle. At gamma==1 this
reduces to the exact McNemar p-value.
The two worst-case one-sided McNemar tails are combined per alternative:
“two-sided” (H_0:<target>=null_value): twice the smaller tail,
capped at 1.
“greater” (H_0:<target><=null_value): the right tail (evidence
that <target> exceeds null_value); no factor of two.
“less” (H_0:<target>>=null_value): the left tail.
Each tail is 1.0 when the estimate is within one discordant pair of
null_value on that side (nothing to reject).
For target='ATE' there is no single McNemar pivot, so the test combines the
A_1 and A_0 tails under A_1+A_0=2S*ATE – an intersection-union
test over the split of the sum budget, with a Bonferroni price for combining the two
effects (see _ate_worst_case_pvalue() for the full logic). null_value is
then the ATE “iSuccesses” count (A_1+A_0)/2=ATE*n_pairs.
target ({'ATT', 'ATU', 'ATE'}) – Which effect the null concerns. A_1 / A_0 are the treated- and
control-side attributable effects; ATE is the average effect.
null_value (int) – The null value tested against (default 0), on the attributable-effect count
scale (for ATE, the average-effect count ATE*n_pairs). A single-effect
null outside the reachable range [hat_a-c0,hat_a+c1] raises
ValueError. The ATE path raises likewise when the average-effect null
lies outside its reachable band [(2hat_a-c0_att-c0_atu)/2,(2hat_a+c1_att+c1_atu)/2] – a mis-scaled ATE null (e.g. passed on the rate scale
rather than the count scale) is surfaced as an error rather than a spurious
0.0. Because null_value is an integer count, the ATE band is rounded
inward to integers – the lower edge is ceil’d, the upper edge floor’d – so
when the raw halves are non-integer the enforced bound (and the range quoted in
the ValueError) can sit up to half a unit inside the exact endpoints above.
Returns the largest Gamma at which the worst-case test of H_0:<target>=null_value (with the given alternative) can still be rejected at level
alpha. A value near 1 means the finding is fragile; a large value means it is
robust to substantial hidden bias. Γ• inverts the same test as
worst_case_pvalue(), so alternative is threaded through unchanged.
target ({'ATT', 'ATU', 'ATE'}) – Which effect the null concerns. For ATE this inverts the combined
(Rigdon-Hudgens) worst-case test of _ate_worst_case_pvalue(), so at the
no-effect null the two-sided value agrees with
PairedOutcomeTable.gamma_star(). The scales differ for a non-zero null:
null_value here is on the count scale (ATE*n_pairs) while
gamma_star takes its null on the rate (ATE) scale, so convert with
null_value=ate_null*n_pairs before comparing the two.
null_value (int) – The null value being tested against (default 0); for ATE the average-effect
count ATE*n_pairs.
monotonic (bool) – Assume treatment never hurts (no prevention); yields a larger Γ•. Default
False.
alternative ({'two-sided', 'less', 'greater'}) – The kind of test whose worst-case rejection is inverted; must match the p-value
being reported. Defaults to “two-sided”.
Returns:
Γ•>=1. Returns 1.0 when the null cannot be rejected even in the
randomized case.
Design sensitivity Γ̃ for a matched-pair binary net effect.
The Rosenbaum sensitivity value Γ• mixes two ingredients: robustness to hidden
bias and ordinary stochastic noise. The design sensitivity Γ̃[1] is the limit
of Γ• as the sample size grows without bound at the favorable situation,
isolating the bias component. If the true (unknown) hidden bias Γ exceeds
Γ̃, no amount of data will let us reject the null of zero net effect; robustness
is a matter of effect size, not sample size. Like a power analysis, it is most
useful before a study is run: computing it post hoc from an observed table
carries the same caveats as post-hoc power.
The closed form is Γ̃=1+ate/baseline in both the general and the monotonic
regime; only the meaning of baseline changes:
General (no monotonicity assumed). Pass the control success rate
p_{+1}. Then Γ̃=p_{1+}/p_{+1}=1+τ/p_{+1}, the ratio
of the treated to the control success rate, where τ=ate is the
rate difference. This uses only the outcome marginals.
Monotonic (treatment never hurts). Pass the harmful-discordance
rate p_{01} (the pair-type probability of a control success paired
with a treated failure). Then Γ̃=1+τ/p_{01}. Because
p_{01}<=p_{+1}, assuming monotonicity yields a larger, more
favorable design sensitivity, but p_{01} is a joint quantity that
cannot be recovered from the marginals alone.
Parameters:
baseline (float) – The denominator of the relative effect size, in (0,1). Pass the control
success rate p_{+1} for the general design sensitivity, or the
harmful-discordance rate p_{01} if assuming monotonicity (see the discussion
above).
ate (float) – The net-effect rate difference τ=p_{1+}-p_{+1} to be detected; must be
positive.
The clean form Γ̃=1+τ/p_{+1} for the general case assumes the worst-case
(least-favorable) bias configuration sits at the top of the 2x2 contingency box –
equivalently, that failures are more common than successes (S_{11}<=S_{00} at
the boundary). This is documented, not enforced; outside that regime the general
closed form is an approximation (Wilson, 2026, section 7, “Design sensitivity”)
[2].
The baseline+ate<1.0 check is the general-regime constraint: there
baseline=p_{+1} and baseline+ate=p_{1+} is the treated success
marginal, which must be a valid rate below 1. In the monotonic regime baseline=p_{01} is a joint pair-type rate and the sum is not a marginal, so the bound is
not strictly required; it is nonetheless enforced uniformly for both regimes. This
is conservative in the monotonic case – it can reject inputs that are formally
legitimate there – but the excluded region (p_{01}+τ>=1) requires an
implausibly large harmful-discordance rate, so the over-restriction is mild and buys
a single, simple validation path.
Textbook McNemar (Wald) ATE interval, for comparison only.
The conventional large-sample interval for the difference of marginal success
probabilities Delta=p_{1+}-p_{+1}, which models the four counts as a
multinomial sample (Fleiss, Levin and Paik, 2013) [3]:
with \hat{\Delta}=(S_{10}-S_{01})/S, the same point estimate as
PairedOutcomeTable.ate_hat. It is narrower than
att_confidence_set() because it rests on multinomial sampling rather than
on randomization, and implicitly assumes monotonicity; it is provided as a
baseline, not as the recommended estimator (Wilson, 2026, section 6,
“Comparison with the textbook McNemar interval”) [4].
The variance is the Wald (plug-in) variance of \hat{\Delta}, not the variance
under the null Delta=0: the latter drops the (S_{10}-S_{01})^2/S
term, as McNemar’s test does, and so is wider whenever the effect is nonzero.
Which effect-size columns appear is controlled by membership in
effect_size_columns (each shows its estimate and a confidence interval),
mirroring SpockTestSummaryOptions. include_p_value and
include_sensitivity toggle the trailing p-value and Γ• columns.
header_overrides / format_overrides replace a column’s header text or its
Python format string; keys are EffectSize members (or the equivalent strings)
for the effect columns. header_overrides additionally accepts a "pval" key
to rename the p-value column header; the p-value column’s numeric format is fixed
and is not overridable.
The single result object for the estimation module: it carries the scaled effect
(the ATT or the ATU, depending on target), the attributable effects, the
worst-case p-value and the sensitivity value, and knows how to display itself (via
PairedOutcomeAnalysisOptions) and serialize itself. Produced by
PairedOutcomeTable.analyze() and by att_confidence_set().
Which effect the analysis concerns. A_1 is the effect on the treated (scaled
column shown as ATT); A_0 the effect on the untreated (ATU); ATE
the average effect (ATE).
Confidence set for the scaled target effect: the attributable_interval
divided by n_pairs (the ATT when target=='ATT', the ATU when target=='ATU', the ATE set when target=='ATE'). For one-sided alternative
one endpoint is +/-math.inf.
Attributable-effect point estimate (S_10-S_01); for ATE this is also
the average-effect point estimate (A_1+A_0)/2 (both effects share the
S_10-S_01 pivot).
The confidence set for the target effect (the one shown as ISUCCESS):
the attributable-effect set for A_1 / A_0, or the average-effect set
(A_1+A_0)/2=ATE*n_pairs for ATE. Two-sided, an A_1 / A_0
set is tested at alpha/2 per side with integer endpoints (the ATE set
carries the extra Bonferroni split); one-sided it carries +/-math.inf on
the uninformative side. effect_interval is exactly this set divided by
n_pairs.
The component’s net-effect interval at the Bonferroni share alpha/k (k
= number of nonzero terms) – the level at which it actually enters the combined
interval, and on the side at which it enters. Under a one-sided combination that
is one-sided too, flipped for a negative coefficient, so the term rows reproduce
the combined bound rather than quoting a bound the combination never used.
Two-sided when the combination is, or when the coefficient is zero.
The displayable, serializable result of analyzing a linear combination.
Produced by LinearCombinationEstimator.analyze(). Carries the combined scaled
effect and its Bonferroni interval at the entertained gamma, the Bonferroni
p-value for the tested null, the matched-pair count the effects are rates over (so
the table can also report them as iSuccesses counts), the per-term breakdown,
and the sensitivity value Γ•.
Worst-case Bonferroni p-value for H_0:<combination>=null_value at
sensitivity gamma – the p-value dual of effect_interval, below
alpha when the interval excludes the null. See
LinearCombinationEstimator.pvalue() for the numerical tolerance on that
correspondence; significant reads the interval itself.
The shared matched-pair count of the component tables (0 only when the
combination has no terms at all), used to render the iSuccesses (count)
columns as effect*n_pairs.
Every key is read strictly, with no defaults. That is deliberate: this class has
never landed, so no payload written by an earlier version of it exists anywhere
to be compatible with, and a missing key means a corrupt or hand-edited payload
– better a KeyError here than a silently defaulted field in a reported
estimate.
where each \(\theta_i\) is the scaled net effect (a proportion) of one
PairedOutcomeTable for the target the analysis
asks for – the ATT for target='ATT', the ATU for target='ATU', or the ATE
for target='ATE'.
The estimator holds only the combination – which tables, which coefficients, what
offset. target, monotonic and method describe an analysis of it and are
passed to the method that performs one, exactly as
PairedOutcomeTable takes them. One estimator can
therefore report the same combination as an ATT and an ATE without being rebuilt.
Inference is Bonferroni, not variance-propagated. Every component is computed on
the same matched pairs, so the component effects are dependent (a pair that
discordantly favors treatment on one outcome tends to on another). We split the
level – alpha/k across the k terms with nonzero coefficient – take each
component’s exact net-effects interval at its share, and combine the endpoints
sign-aware (a union bound, valid under arbitrary dependence). The cost is
conservatism: the interval ignores the positive correlation between components. The
D_pair follow-up (pair-level differencing + Pagano-Tritchler) reclaims that
correlation with a single signed-score inference and no Bonferroni penalty.
alpha/k is the level handed to each component, not the level reaching each
quantile. A two-sided component splits its share again across the two tails, and
target='ATE' splits once more across A_1 and A_0 (see
_ate_confidence_core()), so the smallest level inverted
anywhere is alpha/(2k) for ATT/ATU and alpha/(4k) for ATE. Coverage is
still at least 1-alpha – the extra splits only make the interval wider – but
an ATE combination is materially more conservative than the same combination on ATT.
Parameters:
terms (sequence of (float, PairedOutcomeTable)) – Coefficients paired with the tables they scale. All tables must share the same
n_pairs (they describe the same matched pairs under different outcomes); a
mismatch raises ValueError. Every coefficient must be finite. One entry per
outcome: two entries on the same outcome each pay a Bonferroni share, so sum
their coefficients into a single term rather than listing the table twice.
affine (float, optional) – Constant offset added to the combination (finite). Defaults to 0.0.
labels (sequence of str, optional) – Display labels for the terms, one per entry in terms, used by
analyze()’s summary. Defaults to "term1", "term2", …
Notes
The constructor’s arguments are validated once and nothing re-validates them
afterwards, so treat the attributes it sets as read-only: reassigning est.affine
is not prevented, it simply skips that validation and will produce invalid or NaN
bounds. terms and labels are the exceptions – they are frozen into tuples,
because a shared list can be mutated without any assignment to the estimator (the
caller need only keep the list they passed in), which is the subtler footgun of the
two and the only one a tuple can close. Build a new estimator rather than editing
one in place. (The analysis arguments carry no such caveat: each method validates
the ones it is handed, every time.)
Nothing ties one call’s target to another’s, which is the price of taking them
per call: a Γ• computed for the ATE and an interval computed for the ATT are not
a matched pair, and neither reports the mismatch. Prefer analyze(), which runs
one target across all of them and records which, when the numbers are going to be
read together.
The matched-pair count shared by every component table.
The constructor requires the tables to agree, so any of them reports the shared
count. A combination with no terms – a bare offset with no table behind it –
has no pairs and reports 0.
Note this is not the “all-constant” condition pvalue() and
gamma_star() answer exactly, which is the weaker “no term has a nonzero
coefficient”. A term carries its table’s pair set whatever its coefficient, so
[(0.0,table)] is all-constant for inference – nothing in it moves with
alpha or gamma – and still reports table.n_pairs here. That is
deliberate: the pair count describes the design the combination is stated
over, and the offset is a per-pair rate on that design, so affine*n_pairs
is a real count of successes. Zeroing it would silently blank a column that has
a faithful rendering.
Randomized (gamma=1) Bonferroni interval for the combination.
The union-bound interval at coverage 1-alpha assuming no hidden bias. Use
expanded_confidence_interval() to entertain a sensitivity parameter.
Parameters:
alpha (float, optional) – Significance level; the interval has coverage 1-alpha (default 0.10,
the RL MDS convention).
alternative ({'two-sided', 'less', 'greater'}, optional) – The kind of interval. 'two-sided' returns two finite bounds;
'greater' returns [lb,+inf) and 'less'(-inf,ub]. Defaults
to 'two-sided'.
target ({'ATT', 'ATU', 'ATE'}, optional) – The effect each component reports, and so the scale of the result. Defaults
to 'ATT'.
monotonic (bool, optional) – Assume treatment never hurts any unit; narrows every component set. Defaults
to False (the net-effects default – no such assumption).
method ({'exact', 'normal', 'auto'}, optional) – How each component’s worst-case test is inverted, forwarded to
attributable_effect_interval(). Defaults to
'auto'.
Sensitivity-expanded Bonferroni interval at hidden bias gamma.
Widens confidence_interval() to allow a hidden bias of odds ratio
gamma in the pair assignment. At gamma==1 it equals the randomized
interval; larger gamma widens each component set (and so the combination).
The union bound holds at every gamma.
Rosenbaum sensitivity value Γ• for the combined finding.
The largest hidden bias gamma at which the expanded (Bonferroni) interval at
level alpha still excludes null_value – the point where the widening
interval first admits the null and the finding stops being significant. Returns
1.0 when the randomized interval already contains null_value, and
math.inf when the interval excludes it for arbitrarily large gamma (e.g.
an all-constant combination whose offset alone clears the null).
Parameters:
null_value (float, optional) – The value the interval is tested against (default 0.0).
alpha (float, optional) – Significance level; the interval has coverage 1-alpha (default 0.10).
alternative ({'two-sided', 'less', 'greater'}, optional) – The kind of interval inverted; see confidence_interval(). Defaults to
'two-sided'.
target ({'ATT', 'ATU', 'ATE'}, optional) – The effect the sensitivity value concerns; see confidence_interval().
Defaults to 'ATT'.
monotonic (bool, optional) – Assume treatment never hurts any unit; raises Γ•. Defaults to False.
method ({'exact', 'normal', 'auto'}, optional) – How the inverted intervals are computed; see confidence_interval().
Defaults to 'auto'.
Bonferroni p-value for H_0:<combination>=null_value.
The smallest level alpha at which the gamma-expanded Bonferroni interval
excludes null_value – the p-value dual of
expanded_confidence_interval(), obtained by inverting it. Valid
(conservative) under arbitrary dependence via the same union bound. Lacking a
closed form for the combined test, it is found by bisection: the interval
narrows as alpha grows, so exclusion is monotone and the threshold is the
p-value. The bracket is [1e-12,1-1e-12]; see _PVALUE_EPS for why
that floor is where it is. Returns 1.0 when even the narrowest
(near-zero-coverage) interval contains the null, and the floor when even the
widest interval excludes it.
An all-constant combination (no term with a nonzero coefficient) is answered
exactly instead: it carries no sampling uncertainty, so the interval does not
move with alpha and there is no threshold to bisect for. The offset either
clears the null – 0.0 – or it does not – 1.0. This mirrors
gamma_star(), which reports an exact math.inf for the same
combination.
The bracket deliberately runs past 0.5, which _validate_alpha()
refuses for a one-sided alternative, and the two are not in conflict: that
guard is about what a caller may ask for. A one-sided interval at alpha>=0.5 lands on the far side of the point estimate and has no coverage reading,
so nobody should be handed one. Here the levels are not coverage claims but the
search variable of an inversion, and the answer is a p-value: a one-sided test
of a null the data point away from has a p-value above 0.5, and it is exactly
the levels above 0.5 that measure how far above. Truncating the bracket at 0.5
would report every such null as p=0.5, collapsing the whole uninformative
half onto one number.
The value returned is the upper end of the final bracket, so it is an
over-estimate of the true threshold – the conservative direction, since a
p-value rounded up never overstates the evidence. The over-estimate is bounded
by whichever of the two stopping rules binds first: the relative break gives
1e-9*p, and the 40-iteration cap gives 2**-40 (~9.1e-13) absolute. The
relative rule is the binding one down to p~9e-4; below that the bracket
simply runs out of iterations and the absolute bound governs, which is the
tighter guarantee anyway.
The consequence is that agreement with
LinearCombinationAnalysis.significant is exact only outside a band of
that width around alpha: if the threshold falls inside it, the reported
p-value can sit a hair above alpha while the interval genuinely excludes the
null. significant reads the interval directly and is authoritative there;
the p-value is the numerically-inverted summary of the same fact.
Parameters:
null_value (float, optional) – The value tested against (default 0.0).
gamma (float, optional) – Rosenbaum sensitivity parameter entertained for the test (>=1; default
1.0, the randomized case).
alternative ({'two-sided', 'less', 'greater'}, optional) – The kind of test inverted; see confidence_interval(). Defaults to
'two-sided'.
target ({'ATT', 'ATU', 'ATE'}, optional) – The effect tested; see confidence_interval(). Defaults to 'ATT'.
monotonic (bool, optional) – Assume treatment never hurts any unit; sharpens the p-value. Defaults to
False.
method ({'exact', 'normal', 'auto'}, optional) – How the inverted intervals are computed; see confidence_interval().
Defaults to 'auto'.
Confounding-only band for the combination at hidden bias gamma.
The range the combined estimate could take under a hidden bias of odds ratio
gamma from confounding alone – no sampling uncertainty – mirroring
PairedOutcomeTable.sensitivity_analysis(). Each component’s
confounding-only band is combined sign-aware (a positive coefficient contributes
its lower bound to the combined lower bound, a negative coefficient its upper
bound, and vice versa). Collapses to point_estimate() at gamma==1
and opens with gamma, saturating at each component’s a-priori range rather
than diverging.
Like the Bonferroni interval this is conservative: the components share matched
pairs, so the true joint worst case is a subset of the independent per-component
worst cases combined here. No level is split, though – a confounding-only band
carries no sampling error, so there is no Bonferroni penalty.
There is no method here, unlike the interval methods: a confounding-only
band inverts nothing, so there is no test to choose an exact or normal form for.
gamma stays positional to mirror
PairedOutcomeTable.sensitivity_analysis(), which users move between. The
mirror stops there: the sibling’s second positional is monotonic, and this
one has a target the single table does not, so a positional second argument
would mean different things in the two classes. Keyword-only from target on,
which is also the convention every other analysis method here follows.
The smallest PairedOutcomeTable.capacity() over the components with a
nonzero coefficient – the most binding one, since the combination is
uninformative once any contributing component is. Each component’s level is its
Bonferroni share alpha/k, the share the union bound spends on it. Returns
math.inf when no coefficient is nonzero (no component can degrade).
For target='ATE' the share is not the last split: the ATE interval halves
its level once more across A_1 and A_0, so the level a component is
really inverted at is alpha/(2k), and a capacity falls as its level does.
The share is quoted at alpha/k anyway, because
PairedOutcomeTable.capacity() quotes an ATE capacity the same way and a
capacity is only worth reading against another quoted on the same convention.
The cost is that the ceiling reported for an ATE combination sits slightly above
the level-consistent one, so the gamma_max default plot_sensitivity()
derives from it can sweep a little past the point where the wider band has
already saturated – cosmetic, and in the conservative direction for a sweep
range.
Parameters:
alpha (float, optional) – Level split across the components (default 0.05). Note this is
PairedOutcomeTable.capacity()’s default, not the 0.10 the inference
methods on this class use: a capacity is a property of the design that is
quoted against the conventional level, and the two capacities have to be
comparable to be worth comparing.
Sweep Gamma and plot how the combined finding degrades.
The combination’s analog of PairedOutcomeTable.plot_sensitivity(), and its
visual companion: as the hidden-bias odds ratio Gamma grows from 1 (a
randomized experiment) upward, two bands widen around the (bias-independent)
combined point estimate –
the sensitivity interval, the range of the combination from
confounding alone (sensitivity_analysis()); and
the sensitivity/confidence interval, which adds sampling
uncertainty (expanded_confidence_interval(), Bonferroni across
the terms).
The left axis is the scaled combined effect; a secondary right axis rescales it
to the matching count (iSuccesses=effect*n_pairs), matching
analyze()’s columns. A combination with no terms has no pairs to count
over, so it is drawn without that second axis, just as analyze() leaves
its count columns blank. The sensitivity value Γ• – where the wider band
first touches null_value – inverts the plotted band, so the dotted line and
the band cross the null together.
Both bands and Γ• use the target and monotonic given here, so the
whole figure is one coherent analysis. method reaches the wider band alone
– it selects the test inverted for the sampling component, and the
confounding-only band has none to invert, so it is unaffected. The plot is
always two-sided (a one-sided band has an infinite edge and cannot be drawn),
independent of the alternative used elsewhere.
Parameters:
null_value (float, optional) – The null the wider band is tested against, on the scaled-effect axis; Γ•
is computed against it (default 0.0).
alpha (float, optional) – Significance level; the wider band has coverage 1-alpha (default
0.10).
gamma_max (float, optional) – Largest Gamma swept. Defaults to min(6,0.95*capacity) (6 is the
smoking / lung-cancer benchmark; the cap keeps the bands finite below the
combination’s capacity()).
num_points (int, optional) – Number of Gamma values swept (>=2; default 50).
target ({'ATT', 'ATU', 'ATE'}, optional) – The effect plotted, and the left axis’s label. Defaults to 'ATT'.
monotonic (bool, optional) – Assume treatment never hurts any unit; narrows both bands and raises Γ•.
Defaults to False.
method ({'exact', 'normal', 'auto'}, optional) – How the wider band’s tests are inverted; see confidence_interval().
Defaults to 'auto'.
legend_loc (str, optional) – Matplotlib legend location, forwarded to ax.legend; use it to keep the
legend clear of the Γ• annotation.
title (str, optional) – Plot title; no title is drawn when omitted.
ax (Axes, optional) – Axes to draw on; a new figure and axes are created when omitted.
Returns:
The swept data (columns gamma, point, sens_lower,
sens_upper, ci_lower, ci_upper) and the axes drawn on.
Bundle the combination into a displayable, serializable result.
Computes the combined point estimate and Bonferroni interval at sensitivity
gamma, a per-term breakdown, the worst-case p-value, and the sensitivity
value Γ• for H_0:<combination>=null_value. See
LinearCombinationAnalysis.
Parameters:
alpha (float, optional) – Significance level; the interval has coverage 1-alpha (default 0.10).
The level is Bonferroni-split across the nonzero terms.
gamma (float, optional) – Sensitivity parameter entertained for the interval (>=1; default 1.0,
the randomized case). Distinct from Γ•.
null_value (float, optional) – The null the interval and Γ• are tested against (default 0.0).
alternative ({'two-sided', 'less', 'greater'}, optional) – The kind of interval; see confidence_interval(). Defaults to
'two-sided'.
target ({'ATT', 'ATU', 'ATE'}, optional) – The effect reported throughout the summary; see confidence_interval().
Defaults to 'ATT'.
monotonic (bool, optional) – Assume treatment never hurts any unit; narrows every set. Defaults to
False.
method ({'exact', 'normal', 'auto'}, optional) – How the sets are inverted; see confidence_interval(). Defaults to
'auto'.
the post-period net effect S minus the pre-period (placebo) net effect P on
the same matched pairs. Under parallel trends the hidden bias on the real
outcome is approximated by the placebo net effect, so subtracting P removes it;
a placebo net effect far from zero is itself evidence of bias. DiD trades the
ignorability premise for parallel trends and earns its keep when P cannot simply
be balanced away in the design (no overlap / selection-on-trend).
Parameters:
pre_table (PairedOutcomeTable) – The placebo (pre-period) outcome table, P.
post_table (PairedOutcomeTable) – The real (post-period) outcome table, S. Must share pre_table’s
n_pairs.
affine (float, optional) – Forwarded to LinearCombinationEstimator; shifts the estimand to Y=affine+S-P. It is only fixing the coefficients that makes this class
sugar, so a known constant offset stays available rather than forcing a caller
who needs one back to the general constructor and a hand-written (-1,+1).
Defaults to 0.0. target, monotonic and method are not
construction arguments here either – pass them to
analyze() and the other inference methods.
Notes
Both tables are keyword-only. They have the same type and the same shape, so a
positional call offers nothing to catch a swap: transposing them estimates P-S
instead of S-P and every diagnostic still looks healthy – the sign of the
effect simply flips. The chronological (pre,post) order is also the reverse of
the S-P the estimand is written as above, which is precisely the sort of thing
a reader supplies from memory. Naming them at the call site costs one word and
removes the failure mode.
The matched pairs a net-effects analysis is computed on.
This package does the estimation half of a matched study – it takes pairs as given
and draws inference from their binary outcomes. How the pairs were formed is out of
scope: propensity-score matching, exact matching on a few keys, or a pairing that
already exists in the data all work equally well.
MatchResult is the hand-off point. It is a plain record of which treated unit
was paired with which control, by index label, and is consumed by
pair_match.PairedOutcomeTable.from_match_result(). Anything else carrying
treated_index and control_index attributes of equal length works there too –
the method reads only those two.
If you are starting from two aligned 0/1 outcome vectors rather than from index labels,
skip this module entirely and use pair_match.PairedOutcomeTable.from_outcomes().
Covariate distance within each pair, in the same order. Carried for reporting
only – nothing in the net-effects inference reads it – so it defaults to an
empty array when the pairing came from somewhere that does not compute
distances.