cleverly.sensitivity.PositivityReport#

class cleverly.sensitivity.PositivityReport(propensity_quantiles: dict[str, dict[float, float]], tail_mass: dict[float, dict[str, float]], effective_sample_size: dict[str, dict[str, float]], weight_share: dict[str, dict[str, float]], truncated: dict[str, float], clever_covariate_max: dict[str, float], bounds: tuple[float, float], n: int, mechanisms: dict[str, dict[str, float]] = <factory>, composed_excluded: tuple[str, ...] = (), nuisance_bound: float = 0.0, simplex_deviation: float = 0.0, n_repeats: int = 1, backend: str | None = None, group_score_load: dict[str, ~cleverly.sensitivity.positivity.GroupScoreLoadRow] = <factory>, group_score_load_omissions: dict[str, str] = <factory>, treatment_mechanism: str = 'estimated')[source]#

Overlap diagnostics for a fitted TMLE.

Parameters:
propensity_quantilesdict of str to dict of float to float

Quantiles of g(W), overall and within each treatment arm.

tail_massdict of float to dict of str to float

Fraction of units with g(W) below each threshold, and above its mirror.

effective_sample_sizedict of str to dict of str to float

Kish ESS of the inverse-probability weights per arm, with the nominal arm size alongside for comparison.

weight_sharedict of str to dict of str to float

Share of the total weight held by the largest 1% and 5% of weights.

truncateddict of str to float

Count and fraction of propensity scores clipped by the truncation bounds, and the most extreme untruncated value.

clever_covariate_maxdict of str to float

Largest reconstructed absolute clever-covariate value per targeted estimand family. This is a covariate-scale diagnostic, not a score contribution or an observation’s influence on the estimate.

group_score_loaddict of str to GroupScoreLoadRow

Absolute residual-multiplier concentration for each targeted family. A row holds fourteen keys: equation, n_total, n_targeted, effective, targeted_ratio, total_ratio, top_1pct, top_5pct, max_load, zero_load, lower_bound, upper_bound, clipped_count and clipped_fraction. It carries no draw count, which PositivityReport.n_repeats records once for the whole fit. The fitted fluctuation retains abs(w_i * h_ij) on the exact rows and with the exact weights used to evaluate each score equation. This report computes one Kish concentration ratio per column, then retains the most concentrated column and names it under equation. Every metric in a group row comes from that one column. It never sums columns with different units.

effective is the Kish-equivalent row count of that absolute load. It is a descriptive concentration index. It is not the estimator’s information, actual influence, or the sample size used for its standard error. targeted_ratio divides it by the rows in the fitted score mask. total_ratio divides it by the full data row count, so outcome missingness and intermediate selection remain visible. The two top shares, max_load, and zero_load describe the same selected column.

n_targeted counts rows in the fitted score mask, including a row whose selected equation has a structural zero. It does not count nonzero residual contributions.

lower_bound, upper_bound, clipped_count, and clipped_fraction describe the treatment-mechanism bound used by that group. ATT and ATC therefore report g_bounds_conditional rather than the marginal bound.

The groups that reach this table through support() are mean, att, atc and msm. A regime, shift or incremental fit gets its own support report from that method instead and so gets no row here; regime and ipsi appear only when positivity_report() is called directly. mtp never appears at all, because policies= needs a continuous treatment and this report refuses one.

group_score_load_omissionsdict of str to str

Machine-readable reasons an exact group diagnostic or one of its components could not be reported. Exact score-weight loads are never reconstructed from a generic submodel, and clipping for a targeted treatment mechanism is unavailable unless its fitted clipping mask was retained.

boundstuple of float

Truncation bounds the fit applied to the treatment mechanism.

nint

Number of observations the diagnostics were computed over.

mechanismsdict of str to dict of str to float

Overlap for the other denominators in the clever covariate: P(Delta = 1 | A, W) when outcomes are missing, and P(Z = z | A, W) for a controlled direct effect. Every row carries the same nine keys: min, q01, q05 and median of the mechanism as fitted; clipped and clipped_fraction for the cells the bound moved; and ess_ratio, top_1pct and top_5pct for the load the weights leave behind. Empty when neither applies.

The three load measures are taken over the weights the estimating equation actually forms: the bounded mechanism at each unit’s realised arm, times that unit’s observation weight, over the rows whose residual it multiplies. They are on the same scale as the propensity’s effective_sample_size and weight_share, and fold in the design for the same reason those do, so the two tables can be read side by side.

These deserve reporting for exactly the reason g does: they enter the estimating equation as a denominator, so a value near zero can give one observation a large residual multiplier. Unlike g they are one-sided – only the approach to zero matters – and they are easy to overlook, because a fit can have perfectly healthy propensity overlap and still be resting on a handful of rows that were very unlikely to be observed at all.

A derived row reports the complete denominator for a missing-outcome fit (P(A=a,Delta=1|W)), a controlled direct effect (P(A=a,Z=z|W)), or both at once (P(A=a,Delta=1,Z=z|W)). The z in those names is the level the fit targets, which the factor row beside them states outright. It is not a further fitted mechanism and it was never held to a bound of its own: the estimator truncates each factor separately and multiplies. So its min and quantiles stay on the untruncated product, as the fitted rows’ do, while its clipped counts the cells where any factor moved – which is the truncation this denominator underwent, and which a comparison of the two products would miss wherever a zero in one factor hides a clip in another. A per-arm outcome-adaptive fit regresses each factor on Qbar(a, W), so its row names that design as the condition: P(A=a,Delta=1|Qbar(a,W)), with ,S added under baseline strata.

The derived row is the reason this table exists. Each factor can look comfortable on its own while their product concentrates the weight, because the denominator factors multiply. That does not imply their Kish ratios multiply. Only the product is what the clever covariate divides by. It describes the fit’s marginal-mean estimands – the ey family, its contrasts, a regime, and a marginal structural model. A fit that targets none of those gets its factor rows and no product row, because an ATT or ATC covariate divides by P(A = a) rather than g_a(W) and an incremental one drops the propensity entirely.

composed_excludedtuple of str

Targeted groups the derived row does not describe, on a fit that has the factors to build one. Empty when every targeted group forms the product, and empty when no factor stands beside g at all, because then there is no derived row for any group to be outside of.

This is here so the omission explains itself. A silently absent row reads exactly like a fit with nothing to report, and those are the two readings that must never be confused: one says the denominator is healthy, the other says this diagnostic does not cover the estimand asked for. summary() names these groups and what each divides by instead.

nuisance_boundfloat

The lower bound applied to the fitted mechanisms above. A derived row has no single such bound – see _bound_label().

simplex_deviationfloat

Largest |sum_a g(a | W) - 1| across rows after truncation, and 0 for a two-armed fit, where the complement form preserves the sum exactly.

Non-zero is expected rather than alarming: with more than two arms the bounds are applied arm by arm and deliberately not renormalised, because rescaling a row back onto the simplex can push a column below the floor and so undo the only thing truncation is for. The number is reported because it is the size of that deliberate inconsistency, and a large value says the bounds are binding hard – which is a positivity finding, not a bookkeeping one. It does not bias the plug-in: the plug-in averages targeted predictions and contains no mechanism at all.

n_repeatsint

Cross-fitting draws the fit combined. Everything else here describes the first of them, because overlap is a property of one fitted mechanism and a combination of draws would describe one no estimate came from.

backendstr or None

Dataframe backend to_frame() returns when data is omitted.

treatment_mechanism{“estimated”, “known”}

Whether g(W) is an estimate or the mechanism the data declares known. A known mechanism never moves under the fitted bounds, because the fit refuses a bound pair that would move it.

Methods#

PositivityReport.summary() → str[source]#

A printable overlap report.

Returns:
str

A printable report, one line per reported quantity.

PositivityReport.to_frame(data: Any = None) → Any[source]#

Propensity quantiles as a tidy frame.

Parameters:
dataAny

A dataframe or fitted container whose backend to match. None uses backend.

Returns:
dataframe

One row per (arm, quantile) of the treatment mechanism.

PositivityReport.verdict() → str[source]#

A one-line reading of the diagnostics.

Returns:
str

One line saying whether the overlap supports the reported estimate.

Attributes#

PositivityReport.backend: str | None = None#

Name of the dataframe backend the fit’s data arrived in, so that to_frame() honours “results come back in the backend you passed in” without a caller having to thread the container back in by hand.

PositivityReport.composed_excluded: tuple[str, ...] = ()#
PositivityReport.n_repeats: int = 1#

How many cross-fitting draws the fit combined. Everything above describes the first of them, and this is here so a reader knows that. Overlap is a property of one fitted mechanism, and combining R propensity vectors would produce a perfectly good estimate of g that is nonetheless not the object any reported psi was computed from – a different aggregation from the one the estimates use, under the same heading. The draws share the data and differ only in the split, so their overlap is near identical in practice; when it is not, that is itself worth seeing rather than averaging away.

PositivityReport.nuisance_bound: float = 0.0#
PositivityReport.severity#

Which tier this report’s own thresholds place the fit in.

Returns:
{“adequate”, “strain”, “serious”}

The tier behind verdict(), for a caller that must branch on it.

PositivityReport.simplex_deviation: float = 0.0#
PositivityReport.treatment_mechanism: str = 'estimated'#
PositivityReport.propensity_quantiles: dict[str, dict[float, float]]#
PositivityReport.tail_mass: dict[float, dict[str, float]]#
PositivityReport.effective_sample_size: dict[str, dict[str, float]]#
PositivityReport.weight_share: dict[str, dict[str, float]]#
PositivityReport.truncated: dict[str, float]#
PositivityReport.clever_covariate_max: dict[str, float]#
PositivityReport.bounds: tuple[float, float]#
PositivityReport.n: int#
PositivityReport.mechanisms: dict[str, dict[str, float]]#
PositivityReport.group_score_load: dict[str, GroupScoreLoadRow]#
PositivityReport.group_score_load_omissions: dict[str, str]#

A fluctuation without exact per-column absolute score weights is omitted rather than reconstructed from a submodel that might not be the one the fit used (for example under fold-specific targeting).