screen_interactions#
- SuperGLM.screen_interactions(
- X: object,
- y: NDArray,
- sample_weight: NDArray | None = None,
- *,
- offset: NDArray | None = None,
- candidates: list[tuple[str, str]] | None = None,
- edf0: float | tuple[float, ...] = (2.0, 4.0, 8.0, 16.0),
- max_cells: int = 5000000,
- screen_bins: int = 256,
- phi: float | None = None,
Rank candidate pair interactions by PSST.
PSST (Penalized Smooth Score Test) asks, for each candidate pair of fitted features, how much of the model’s leftover working signal the interaction the pair would actually refit as could absorb at a fixed screening complexity, after profiling out what the pair’s own main effects already explain. The
kindcolumn names that refit target —tifor spline x spline,spline_catfor a spline crossed with a factor,cat_catfor two factors,numeric_catfor a per-level numeric slope andnumeric_numericfor a product of two numerics — andznormalizes each kind against its own noise floor, so the one sorted table ranks them together, though not on equal terms and not simply by df: the measured null maxima span 3.98 to 7.53 across kinds. The heaviest tails sit at low probe df, yet “neither kind is monotone in df”, and the headline maxima read as “Gaussian-driven rather than as something every family reproduces”. Compare like with like before spending a refit (see the measured floors in the screening guide). A spline-modeOrderedCategoricalmargin screens as a spline on its mapped level scores, so its pairs carry the spline kinds. A kind whose block carries no penalty (cat_cat,numeric_cat,numeric_numeric) has no bandwidth to scan and is evaluated at a single rung:edf0then reports the block’s achieved rank withlambda00.edf0is a probe bandwidth; the default ladder evaluates each pair at several budgets and ranks by the best noise-normalized scorez(the statistic is scaled by the fit’s Pearson dispersion estimate first, so the noise floor stays honest beyond unit-dispersion families), so smooth and high-frequency interactions are both visible. One O(n) cell pass per pair; no refits.offsetandsample_weightboth default to the values the model was fitted with (weights only when the fit’s were non-unit); inheriting requiresX/yto be the training data — pass both arrays explicitly to screen a holdout, subsample, or reordered frame. The dispersion used is attached astable.attrs["phi"]and can be overridden viaphi=.Fitted mains with no screenable margin —
Polynomial,RandomEffect, step-modeOrderedCategoricaland anyOrderedCategoricalcarryingspecials=— are excluded from the sweep and reported intable.attrs["deferred_features"], a{feature: reason}mapping that is empty when everything fitted was screened. Naming one of them incandidatesraises with the same reason.The returned frame is sorted by
zdescending; rank byz—statistic/edf0/lambda0describe each pair’s winning rung, sostatisticis not comparable across rows. Pairs whose unique-value grid exceedsmax_cellsare quantile-binned toscreen_binssupport points per margin and flaggedapprox=True; pairs within budget are always computed exactly.max_cellsbounds allocation AND time: the probe block’s dimensionkenters every rung as a(k, k)decomposition — a pivoted QR of the pair’s design factor with one SVD beside it — so per-pair time grows ask^3where the allocations grow ask^2. A probe column collinear with the overlap span is routine rather than exceptional (one emptycat_catcell or one singleton level does it) and is settled by a rank cut on that factor; the Cholesky that used to run here, and the pseudo-inverse it used to fall back to, went with the assembled Grams in issue #257, which changed neither the cost class nor any ceiling below. A cubic-work gate capskat the FLOOR of(1000 * max_cells)^(1/3)for an unpenalized block and of(500 * max_cells)^(1/3)for a penalized one, whose ladder can bisect: 1709 and 1357 at the default. Take the floor — atmax_cells1e4 the penalized root is 170.998 and the cap is 170, not 171. That cap is NOT a constant —max_cellssets it and nothing else does. Penalized ceilings: 1357 at 5e6, 793 at 1e6, 368 at 1e5, 292 at 5e4, 170 at 1e4, with an allocation gatek <= 2 * sqrt(max_cells)binding instead belowmax_cells~3906 (the crossover is ~15625 for an unpenalized block, so a widecat_catreaches the allocation gate at amax_cellsfour times larger). Family, link,phi, penalty order, basis kind, prior weights and theedf0ladder move none of it; only the block’s own width does, and forspline_catthat width isk_splinetimes the factor’s CONTRAST count — the levels that carry a column, not the declared universe, since a level with no effective training rows is pinned to base and contributes none. (Per-pair times are deliberately not quoted here; the module’s own comments carry the measured ones.)Which kernel scores a pair follows from that. The dense path gets first refusal and keeps every pair it can score exactly. A
spline_catpair it cannot is RETRIED through the arrow kernel — linear in the level count, with no block-dimension ceiling — rather than refused, at any of three exits. First, the block over the cap above: at the default a library-defaultSpline()margin crosses it at 105 contrasts, so fromL = 106UNPINNED levels (ps(15)from 77,ps(20)from 61) — a declaredlevels=universe can be far larger and still route dense, since only the unpinned levels widen the block. Second, the pair’s dense support intermediate over budget where the arrow kernel’s transposed one still fits, which fires with the dense block far BELOW the cap (measured atk = 231: a 22-level factor against aps(8)margin with 46,119 distinct values). Third, the exhaustion of the binning fallback. Onlyspline_cathas that retry:tiandcat_catare gridded and dense-only, so for them the cap IS the refusal. The two numeric kinds are neither — they enter through the z-moment kernels rather than the gridded one, andnumeric_numericcontracts to a five-column design whatever the supports, so it never meets the cap at all. Every kind is reduced to a triangular FACTOR of its own weighted design rather than to a curvature matrix (issue #257); the caps above are dimensional and are unchanged by that.A NaN row is not by itself a routing signal. A
spline_catpair reaches one either because the arrow gates or its ladder refused after a handoff, or because the ladder that ran — on EITHER path — resolved no direction at any rung, which is reported as no test rather than as an infinitez.A categorical margin never bins — its support is the fitted level set — and a numeric margin never grids at all: it enters its probe linearly, so a
numeric_cat/numeric_numericrow that carries a statistic is exact. Those kinds have no binning fallback, so their only degradation is refusal: anumeric_catpair whose factor is too wide for the pair’s blocks to fitmax_cells— or to be solved inside it, which binds first and admits 1710 UNPINNED levels (1709 contrasts) at the default, counted the same way as thespline_catthreshold above — gets a NaN row, and raisingmax_cellscomputes it exactly. Screening always probes exact bases, so a pair whose confirmatory refit would discretize LOSSILY is flaggedapprox=Trueto make that support-discretization gap — measured at ~3.5% on signal pairs, the same class as the quantile fallback — visible in the output. The flag applies the refit’s own gate, which differs by kind: atirefit bins its marginals only when BOTH parents resolve to fit-time discretization, aspline_catrefit bins its spline margin whenever that ONE parent does, and thecat_cat/numeric_cat/numeric_numericrefits bin nothing. Lossless binning returns the exact support and staysapprox=False. Pairs already fitted as an interaction of any class are excluded from the sweep. The statistic is a ranking device, not a calibrated p-value: confirm the top-ranked pairs by refitting them as the interaction theirkindnames.