API Reference#

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:

  1. 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).

  2. 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.

  3. 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:

from pair_match import PairedOutcomeTable

table = PairedOutcomeTable.from_outcomes(treated_y, control_y)
print(table)            # the 2x2 table, margins, and success rates
print(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.

Paired Outcome Tables#

Randomization inference for the ATT in matched-pair binary outcomes.

pair_match.net_effects.format_with_min_nonzero_digits(number, min_digits, percentage=True)[source]#

Format with min nonzero digits.

Parameters:
  • number (float) – The number.

  • min_digits (int) – Minimum digits to include.

  • percentage (boolean, optional) – Whether to print as a percentage. Defaults to True.

Returns:

formatted_number – A prettily-formatted number.

Return type:

str

class pair_match.net_effects.EffectSize(*values)[source]#

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).

classmethod get(value)[source]#

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.

Parameters:

value (EffectSize | str)

Return type:

EffectSize

class pair_match.net_effects.PairedOutcomeTable(s00, s01, s10, s11, spend=None)[source]#

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.

Parameters:
s00#

Pairs where both units failed (treated 0, control 0).

Type:

int

s01#

Pairs where the treated unit failed and the control succeeded.

Type:

int

s10#

Pairs where the treated unit succeeded and the control failed.

Type:

int

s11#

Pairs where both units succeeded (treated 1, control 1).

Type:

int

spend#

Total cost of treatment, used by the COST_PER_ISUCCESS effect size. None (the default) when no cost is attached.

Type:

float, optional

property n_pairs: int#

Total number of matched pairs.

property hat_a: int#

Hodges-Lehmann point estimate of the attributable effect.

Equals S_10 - S_01, the McNemar pivot; the same value estimates both A_1 and A_0.

property ate_hat: float#

Point estimate of the average treatment effect.

property treated_success_rate: float#

Fraction of treated units with outcome 1 (S_10 + S_11).

property control_success_rate: float#

Fraction of control units with outcome 1 (S_01 + S_11).

static from_outcomes(treated_outcomes, control_outcomes, *, spend=None)[source]#

Build the table from aligned binary outcome vectors.

Parameters:
  • treated_outcomes (array-like of {0, 1}) – Outcome of the treated unit in each pair.

  • control_outcomes (array-like of {0, 1}) – Outcome of the control unit in each pair, aligned element-wise with treated_outcomes.

  • spend (float, optional) – Total cost of treatment (see spend).

Return type:

PairedOutcomeTable

static from_match_result(result, outcome, *, df_treated, df_control, spend=None)[source]#

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).

Return type:

PairedOutcomeTable

to_dict()[source]#

Return a JSON-serializable representation.

Return type:

dict[str, object]

serialize()[source]#

Serialize to a JSON string.

Return type:

str

static deserialize(s=None, d=None)[source]#

Reconstruct a PairedOutcomeTable from a JSON string or dict.

Tolerates a missing spend key (older payloads) by defaulting it to None.

Parameters:
Return type:

PairedOutcomeTable

analyze(*, alpha=0.1, gamma=1.0, target='ATT', null_value=0, monotonic=False, alternative='two-sided', method='auto', options=None)[source]#

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().

  • options (PairedOutcomeAnalysisOptions, optional) – Display options; defaults to PairedOutcomeAnalysisOptions.

Return type:

PairedOutcomeAnalysis

confidence_interval(*, alpha=0.1, alternative='two-sided', gamma=1.0, monotonic=False, method='exact')[source]#

Confidence interval for the ATT.

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.

Return type:

(float, float)

point_estimate()[source]#

ATE point estimate; the location the sensitivity bands widen around.

Return type:

float

capacity(alpha=0.05)[source]#

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).

Parameters:

alpha (float)

Return type:

float

sensitivity_analysis(gamma=6.0, monotonic=False)[source]#

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 + 2 S01 + S11), S00 + 2 S10 + S11] / (2 S) (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.

Parameters:
Return type:

tuple[float, float]

expanded_confidence_interval(alpha=0.1, gamma=6.0, method='exact', monotonic=False)[source]#

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.

Parameters:
Return type:

tuple[float, float]

gamma_star(null_value=0.0, alpha=0.05, monotonic=False, method='exact')[source]#

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 = 2 S * 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.

Return type:

float

plot_sensitivity(*, target='ATT', null_value=0.0, alpha=0.1, gamma_max=None, num_points=50, method='auto', monotonic=False, legend_loc='lower left', title=None, ax=None)[source]#

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 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).

  • method (str) – Inference method forwarded to expanded_confidence_interval() ('exact' / 'normal' / 'auto').

  • 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. 'lower left', 'lower right'); use it to keep the legend clear of the Γ• annotation (default 'lower left').

  • 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.

Return type:

tuple of (DataFrame, Axes)

pair_match.net_effects.attributable_effect_interval(table, *, target='ATT', confidence=0.9, gamma=1.0, monotonic=False, alternative='two-sided', method='exact')[source]#

Confidence set for an attributable effect, A_1, A_0, or the ATE.

Parameters:
  • table (PairedOutcomeTable) – The matched-pair outcome table.

  • 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.

Return type:

(float, float)

pair_match.net_effects.att_confidence_set(table, *, confidence=0.9, gamma=1.0, monotonic=False, alternative='two-sided')[source]#

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) / (2 S) 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.

Parameters:
  • table (PairedOutcomeTable) – The matched-pair outcome table.

  • confidence (float) – Coverage of the returned ATT confidence set (default 0.90).

  • gamma (float) – Rosenbaum sensitivity parameter (>= 1).

  • 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”.

Return type:

PairedOutcomeAnalysis

pair_match.net_effects.worst_case_pvalue(table, *, target='ATT', null_value=0, gamma=1.0, monotonic=False, alternative='two-sided')[source]#

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 = 2 S * 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.

Parameters:
  • table (PairedOutcomeTable) – The matched-pair outcome table.

  • 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 [(2 hat_a - c0_att - c0_atu) / 2, (2 hat_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.

  • gamma (float) – Rosenbaum sensitivity parameter (>= 1; default 1.0).

  • monotonic (bool) – Assume treatment never hurts (no prevention); yields a smaller p-value. Default False.

  • alternative ({'two-sided', 'less', 'greater'}) – The kind of test (see above). Defaults to “two-sided”.

Return type:

float

pair_match.net_effects.sensitivity_value(table, *, alpha=0.05, target='ATT', null_value=0, monotonic=False, alternative='two-sided')[source]#

Rosenbaum sensitivity value Γ• for a finding.

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.

Parameters:
  • table (PairedOutcomeTable) – The matched-pair outcome table.

  • alpha (float) – Significance level (default 0.05).

  • 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.

Return type:

float

pair_match.net_effects.design_sensitivity_binary(baseline, ate)[source]#

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.

Returns:

Γ̃ > 1, the design sensitivity.

Return type:

float

Notes

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.

References

pair_match.net_effects.mcnemar_ate_interval(table, *, confidence=0.9)[source]#

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]:

\[\hat{\Delta} \pm \frac{z_{1 - \alpha/2}}{S} \sqrt{S_{01} + S_{10} - \frac{(S_{10} - S_{01})^2}{S}},\]

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].

Parameters:
  • table (PairedOutcomeTable) – The matched-pair counts.

  • confidence (float, optional) – Two-sided coverage level. Defaults to 0.90.

Returns:

(lower, upper) on the ATE (rate) scale.

Return type:

tuple of float

Notes

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.

References

class pair_match.net_effects.PairedOutcomeAnalysisOptions(effect_size_columns=(EffectSize.EFFECT, EffectSize.ISUCCESSES), include_p_value=True, include_sensitivity=True, ci_separator=', ', header_overrides=None, format_overrides=None)[source]#

Display options for PairedOutcomeTable.analyze().

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.

Parameters:
effect_size_columns#

The effect-size columns to display, in order.

Type:

sequence of EffectSize or str

include_p_value#

Whether to append the worst-case p-value column.

Type:

bool

include_sensitivity#

Whether to append the Γ• sensitivity-value column.

Type:

bool

ci_separator#

Text placed between a confidence interval’s bounds.

Type:

str

header_overrides, format_overrides

Per-column header-text / format-string overrides.

Type:

dict, optional

class pair_match.net_effects.PairedOutcomeAnalysis(table, alpha, gamma, null_value, alternative, effect, effect_interval, attributable, attributable_interval, p_value, gamma_star, options, monotonic=False, target='ATT', method='auto')[source]#

The full net-effects analysis of a PairedOutcomeTable.

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().

Parameters:
table#

The table analyzed.

Type:

PairedOutcomeTable

alpha#

Significance level; confidence sets have coverage 1 - alpha.

Type:

float

gamma#

Sensitivity parameter entertained for the confidence sets and p-value.

Type:

float

null_value#

The attributable-effect null the p-value and Γ• were tested against.

Type:

int

alternative#

The kind of test the p-value/Γ•/confidence sets reflect.

Type:

{‘two-sided’, ‘less’, ‘greater’}

target#

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).

Type:

{‘ATT’, ‘ATU’, ‘ATE’}

effect#

Point estimate of the scaled effect: ate_hat = hat_a / n_pairs, which equals A_1 / n_pairs (ATT), A_0 / n_pairs (ATU) and the ATE alike.

Type:

float

effect_interval#

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.

Type:

(float, float)

attributable#

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).

Type:

int

attributable_interval#

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.

Type:

(float, float)

p_value#

Worst-case p-value for the tested null (two-sided by default).

Type:

float

gamma_star#

Rosenbaum sensitivity value (a property of the data, independent of the entertained gamma).

Type:

float

options#

Display options.

Type:

PairedOutcomeAnalysisOptions

monotonic#

Whether the sets/tests assumed treatment never hurts (no prevention).

Type:

bool

method#

How the confidence sets were inverted (the p-value and Γ• are always exact). A legacy payload without it deserializes to 'exact'.

Type:

{‘exact’, ‘normal’, ‘auto’}

property point_estimate: float#

Point estimate of the scaled effect (ATT for A_1, ATU for A_0, ATE).

property confidence: float#

Coverage of the confidence sets, 1 - alpha.

to_dict()[source]#

Return a JSON-serializable representation (display options excluded).

Return type:

dict[str, object]

serialize()[source]#

Serialize to a JSON string.

Return type:

str

static deserialize(s=None, d=None)[source]#

Reconstruct from a JSON string or dict (with default display options).

Parameters:
Return type:

PairedOutcomeAnalysis

header_for_column(effect)[source]#

Display header for an effect-size column (honoring overrides).

Parameters:

effect (EffectSize)

Return type:

str

format_for_column(effect)[source]#

Python format string for an effect-size column (honoring overrides).

Parameters:

effect (EffectSize)

Return type:

str

Combinations of Net Effects#

Linear combinations of matched-pair net-effect tables, incl. diff-in-diff.

class pair_match.linear_combination.LinearCombinationTerm(label, coefficient, effect, effect_interval)[source]#

One term’s contribution to a LinearCombinationAnalysis.

Parameters:
label#

Display label for the term.

Type:

str

coefficient#

The term’s coefficient in the combination.

Type:

float

effect#

The component’s scaled point estimate (ate_hat).

Type:

float

effect_interval#

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.

Type:

(float, float)

to_dict()[source]#

Return a JSON-serializable representation.

Return type:

dict[str, object]

static from_dict(d)[source]#

Reconstruct a term from its to_dict() representation.

Parameters:

d (dict[str, object])

Return type:

LinearCombinationTerm

class pair_match.linear_combination.LinearCombinationAnalysis(affine, terms, effect, effect_interval, p_value, n_pairs, alpha, gamma, null_value, alternative, target, monotonic, method, gamma_star)[source]#

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 Γ•.

Parameters:
affine#

The constant offset of the combination.

Type:

float

terms#

Per-term breakdown (coefficient, point estimate, component interval).

Type:

tuple of LinearCombinationTerm

effect#

Combined scaled point estimate, affine + sum_i c_i * effect_i.

Type:

float

effect_interval#

Combined Bonferroni interval at coverage 1 - alpha and sensitivity gamma. One-sided alternative carries +/- math.inf on the uninformative side.

Type:

(float, float)

p_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.

Type:

float

n_pairs#

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.

Type:

int

alpha#

Significance level; the interval has coverage 1 - alpha.

Type:

float

gamma#

Sensitivity parameter entertained for the interval.

Type:

float

null_value#

The null the interval and Γ• were tested against.

Type:

float

alternative#

The kind of interval/test.

Type:

{‘two-sided’, ‘less’, ‘greater’}

target#

The effect each component reports.

Type:

{‘ATT’, ‘ATU’, ‘ATE’}

monotonic#

Whether the component sets assumed treatment never hurts.

Type:

bool

method#

How the component sets were inverted.

Type:

{‘exact’, ‘normal’, ‘auto’}

gamma_star#

Sensitivity value Γ• for H_0: <combination> = null_value.

Type:

float

property point_estimate: float#

Combined scaled point estimate.

property confidence: float#

Coverage of the confidence interval, 1 - alpha.

property significant: bool#

Whether the interval excludes null_value (at the entertained gamma).

to_dict()[source]#

Return a JSON-serializable representation.

Return type:

dict[str, object]

serialize()[source]#

Serialize to a JSON string.

Return type:

str

static deserialize(s=None, d=None)[source]#

Reconstruct from a JSON string or dict.

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.

Parameters:
Return type:

LinearCombinationAnalysis

class pair_match.linear_combination.LinearCombinationEstimator(terms, *, affine=0.0, labels=None)[source]#

Affine combination of matched-pair net effects on shared pairs.

Estimates

\[\theta = \text{affine} + \sum_i c_i\,\theta_i,\]

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 / (2 k) for ATT/ATU and alpha / (4 k) 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 "term 1", "term 2", …

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.

property n_pairs: int#

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.

point_estimate()[source]#

The affine combination of the component point estimates.

Each component contributes its McNemar pivot ate_hat – the same value estimates the ATT, ATU, and ATE – so the point estimate is target-invariant.

Return type:

float

confidence_interval(*, alpha=0.1, alternative='two-sided', target='ATT', monotonic=False, method='auto')[source]#

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'.

Return type:

tuple[float, float]

expanded_confidence_interval(*, alpha=0.1, gamma=6.0, alternative='two-sided', target='ATT', monotonic=False, method='auto')[source]#

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.

Parameters:
  • alpha (float, optional) – Significance level; coverage 1 - alpha (default 0.10).

  • gamma (float, optional) – Rosenbaum sensitivity parameter (finite and >= 1; default 6.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 each component reports; see confidence_interval(). Defaults to 'ATT'.

  • monotonic (bool, optional) – Assume treatment never hurts any unit; narrows every component set. Defaults to False.

  • method ({'exact', 'normal', 'auto'}, optional) – How each component’s worst-case test is inverted; see confidence_interval(). Defaults to 'auto'.

Return type:

tuple[float, float]

gamma_star(*, null_value=0.0, alpha=0.1, alternative='two-sided', target='ATT', monotonic=False, method='auto')[source]#

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'.

Return type:

float

pvalue(*, null_value=0.0, gamma=1.0, alternative='two-sided', target='ATT', monotonic=False, method='auto')[source]#

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'.

Return type:

float

sensitivity_analysis(gamma=6.0, *, target='ATT', monotonic=False)[source]#

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.

Parameters:
  • gamma (float, optional) – Rosenbaum sensitivity parameter (>= 1; default 6.0).

  • target ({'ATT', 'ATU', 'ATE'}, optional) – The effect the band is drawn for. Defaults to 'ATT'.

  • monotonic (bool, optional) – Assume treatment never hurts any unit; narrows the band. Defaults to False.

Return type:

tuple[float, float]

Notes

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.

capacity(alpha=0.05)[source]#

Design-sensitivity ceiling of the combination.

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.

Return type:

float

plot_sensitivity(*, null_value=0.0, alpha=0.1, gamma_max=None, num_points=50, target='ATT', monotonic=False, method='auto', legend_loc='lower left', title=None, ax=None)[source]#

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 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.

Return type:

tuple of (DataFrame, Axes)

analyze(*, alpha=0.1, gamma=1.0, null_value=0.0, alternative='two-sided', target='ATT', monotonic=False, method='auto')[source]#

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'.

Return type:

LinearCombinationAnalysis

class pair_match.linear_combination.DiffInDiff(*, pre_table, post_table, affine=0.0)[source]#

Difference-in-differences of two matched-pair net effects.

Syntactic sugar over LinearCombinationEstimator with the coefficients fixed to \((-1, +1)\), estimating

\[Y = S - P,\]

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.

property pre_table: PairedOutcomeTable#

The placebo (pre-period) table P, the -1 term.

property post_table: PairedOutcomeTable#

The real (post-period) table S, the +1 term.

Matched Pairs#

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().

class pair_match.match_result.Pairing(*args, **kwargs)[source]#

Anything pair_match.PairedOutcomeTable.from_match_result() accepts.

Declared as read-only properties so that a frozen dataclass, a plain class attribute, or a property all satisfy it.

property treated_index: Sequence[object]#

Index labels of the treated units, one per pair.

property control_index: Sequence[object]#

Index labels of the matched controls, aligned with treated_index.

class pair_match.match_result.MatchResult(treated_index, control_index, distances=<factory>)[source]#

One treated unit paired with one control, repeated n_pairs times.

Parameters:
treated_index#

Index labels of the treated units, one per pair.

Type:

list

control_index#

Index labels of the matched controls, aligned element-wise with treated_index: control_index[i] is the control matched to treated_index[i].

Type:

list

distances#

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.

Type:

ndarray, optional

property n_pairs: int#

Number of matched pairs.