Families and Dispersion Estimation#
Supported families#
Family |
Variance function |
Default link |
Use case |
|---|---|---|---|
|
V(μ) = 1 |
identity |
Continuous outcomes |
|
V(μ) = μ |
log |
Claim frequency |
|
V(μ) = μ + μ²/θ |
log |
Overdispersed frequency |
|
V(μ) = μ² |
log |
Claim severity |
|
V(μ) = μᵖ |
log |
Pure premium (frequency × severity) |
|
V(μ) = μ(1 − μ) |
logit |
Binary classification |
The native API examples below assume features is an explicit mapping from
input columns to feature specs, such as {"age": Numeric()}. SuperGLM does
not guess an omitted native feature configuration at fit time.
Binomial (binary classification)#
For binary outcomes (y in {0, 1}):
from superglm import SuperGLM
model = SuperGLM(family="binomial", selection_penalty=0, features=features)
model.fit(df, y)
probabilities = model.predict(df) # returns P(Y=1)
The default link is logit. Alternative links can be passed via link=:
from superglm import SuperGLM, ProbitLink, CloglogLink
# Probit link (latent variable interpretation)
model = SuperGLM(
family="binomial",
link=ProbitLink(),
selection_penalty=0,
features=features,
)
# Complementary log-log (asymmetric alternative)
model = SuperGLM(
family="binomial",
link=CloglogLink(),
selection_penalty=0,
features=features,
)
For sklearn-compatible binary classification, use SuperGLMClassifier:
from superglm import SuperGLMClassifier
clf = SuperGLMClassifier(selection_penalty=0, spline_features=["age"])
clf.fit(df, y)
clf.predict(df) # hard labels (0/1)
clf.predict_proba(df) # (n, 2) class probabilities
clf.decision_function(df) # log-odds
Scale is known (phi = 1) for binomial, so no dispersion estimation is needed.
Weight semantics#
What sample_weight says about a row is a declared modelling choice, not a
consequence of the family. Set it with weight_semantics:
features = {"vehicle_age": Spline(n_knots=8), "region": Categorical()}
# The default: sample_weight is a precision, e.g. exposure beside an average.
model = SuperGLM(family="gamma", features=features, weight_semantics="prior")
# The alternative: sample_weight counts identical rows.
model = SuperGLM(family="gamma", features=features, weight_semantics="frequency")
"prior"(default) — an EDM prior weight, a statement of precision:Var(Y_i | x_i) = phi * V(mu_i) / w_i, contributinglog f(y_i; mu_i, phi / w_i). This is what you have when the response is an average:incurred / exposureweighted by exposure, or an average severity weighted by claim count. It is what R’sglmand glum give their single weight argument, and what statsmodels callsvar_weightsand Stata callsaweight."frequency"— a replication count, contributingw_i * log f(y_i; mu_i, phi). Rowistands in forw_iidentical rows, so an integer weight is exactly equivalent to repeating it. This is statsmodels’freq_weights, Stata’sfweight, and SASGENMOD’sFREQ.
The two agree only at w == 1. Integer weights do not make them coincide:
at w == 2 the likelihood size is n under "prior" and 2n under
"frequency", so the residual degrees of freedom, phi, every Wald standard
error and BIC all differ. Fractional prior weights have a precision
interpretation; fractional frequency weights no longer count literal row
replications. Exposure is continuous, which is why "prior" is the default.
For scalar Poisson and negative-binomial models, prior-weighted rates may be
fractional or deliberately adjusted, such as a 20% uplift. No count-integrality
warning is emitted, and fitting and likelihood evaluation do not round them.
If sample_weight * y is fractional, the reported likelihood uses a
gamma-function continuation, not an exact count probability; see the
count-likelihood caveat.
Frequency-replication and prior-weighted binomial warnings are unchanged.
What the choice moves#
With the same family parameters, design and penalty, both contracts give the
same mean score equations. This does not guarantee identical complete fits:
estimated NB theta, REML smoothing parameters, and learned design geometry
can differ and change the fitted means. Other affected quantities include:
quantity |
|
|
|---|---|---|
likelihood size |
rows carrying positive weight |
|
residual d.f. |
|
|
|
from that d.f. |
from that d.f. |
effective |
same size |
same size |
REML criterion → |
prior-weight saturated likelihood |
replicated saturated likelihood |
learned spline knots and bins |
physical rows |
weight mass |
Unweighted fits and fits with w == 1 are identical under both.
A zero weight is admissible under either contract and means the row leaves the
likelihood: under "frequency" it drops out of sum(w) on its own, and under
"prior" it is excluded from the row count, matching R’s rule that
“observations with zero weight [are] not used for calculating dispersion”. The
one exception is a Tweedie fit under "prior", which requires strictly positive
weights — its compound-Poisson normalizer carries log w, so w = 0 is an
unevaluable density rather than an uninformative row.
Replication parity#
The frequency-weight replication statement is conditional on an identical
constructed design. Main-effect spline boundaries and the quantile_rows and
quantile_tempered knot strategies use frequency mass and ignore zero-weight
rows, matching integer row expansion and omission without materializing copies.
Prior weights intentionally leave spline geometry determined by physical rows.
Tensor-interaction marginal centering and interaction-local spline geometry use
that same stream, so a scalar integer-frequency tensor fit matches literal row
expansion through fitting, REML smoothing selection and prediction. Legacy
custom tensor marginals that cannot accept a geometry stream are accepted only
for unit/physical geometry and refuse non-unit replication mass explicitly.
One limitation is declared rather than silent: estimate_p profiles the
Tweedie power against the prior-weight likelihood, so it refuses
weight_semantics="frequency" with non-unit weights rather than answer under
the wrong one. Expand the rows the counts stand for, or profile under
"prior".
sample_weight never enters the linear predictor; use an offset when exposure
should scale the conditional mean.
Negative binomial: estimating theta#
For overdispersed count data where the Poisson variance assumption is too restrictive:
import numpy as np
from superglm import SuperGLM, NegativeBinomial
log_exposure = np.log(exposure)
# Fixed theta
model = SuperGLM(
family=NegativeBinomial(theta=1.0),
selection_penalty=0.01,
features=features,
)
model.fit(df, y, offset=log_exposure)
# Profile estimate theta (alternating GLM fit + safeguarded profile solve)
result = model.estimate_theta(df, y, offset=log_exposure)
print(result.theta_hat) # estimated dispersion
Here y contains raw counts, so exposure enters through the log offset rather
than through sample_weight.
With NegativeBinomial("auto"), fit() and fit_reml() estimate theta
automatically. Under fit_reml, theta is first calibrated at the configured
smoothing and then — since 0.29.0 — re-estimated at the REML fit and
alternated with warm-started refits to a joint fixed point, because a theta
frozen before smoothing selection absorbs lack-of-fit at the calibration
smoothing into spurious overdispersion (biasing theta low and overstating
V(mu) = mu + mu²/theta). The published family.theta,
model._nb_profile_result.theta_hat, and the fit always describe the same
final state.
estimate_theta() uses the classical alternating scheme (Venables & Ripley
2002, ch. 7.4): fit the GLM at the current theta, then update theta by a
bracketed root find on the closed-form NB2 profile score given the fitted
means (Lawless 1987), started from a method-of-moments estimate. Converges
in 2–3 outer iterations. The theta search range defaults to the numerical
guard rails (1e-8, 1e8); an estimate that lands on an active bound is
reported with converged=False plus an NBThetaBoundWarning rather than
published as a converged interior value.
Profile confidence interval#
ci = result.ci(alpha=0.05) # (lower, upper) via profile likelihood ratio
The interval is centred on the best theta for the published mean. That is
theta_hat itself unless the alternation stopped before it settled, or the
published mean was refitted after theta was estimated (for example by
fit_mode="reml"). The interval is still given in both cases. A caution
explains why when the alternation did not settle, or when theta_hat falls
outside the interval.
Profile plot#
result.profile_plot() # likelihood-ratio statistic + CI region
Tweedie: estimating the power parameter#
The examples below assume y is a per-exposure response, such as pure premium
per unit of exposure.
Fit with a fixed Tweedie power:
from superglm import SuperGLM, Tweedie
model = SuperGLM(
family=Tweedie(p=1.5),
selection_penalty=0.01,
features=features,
)
model.fit(df, y, sample_weight=exposure)
Or estimate the power via profile likelihood:
model = SuperGLM(
family=Tweedie(p=1.5),
selection_penalty=0.01,
features=features,
)
result = model.estimate_p(
df,
y,
sample_weight=exposure,
p_bounds=(1.1, 1.9),
ci_alpha=0.05,
)
print(result.p_hat) # estimated Tweedie power
print(model.summary(alpha=0.05))
How the estimate is made (Dunn & Smyth 2005):
p is the power that maximises the profile likelihood. At each candidate power the mean is refitted, so every candidate is a full fit.
φ is the maximum-likelihood dispersion at that candidate’s fitted mean.
A bounded search over
p_boundspicks the power; the model is then refitted at it and returned.The confidence interval inverts the likelihood-ratio test on the same profile.
The result’s evaluations table lists every power the search tried, with its
negative log-likelihood and dispersion. An estimate at an end of p_bounds is
warned and recorded in result.warnings, because the best power may lie beyond it. Near
p=1 such an edge maximum can be an artefact of rounded responses rather than
a property of the data.
sample_weight follows the exponential-dispersion-model prior-weight convention:
Var(Yᵢ | xᵢ) = φ μᵢᵖ / wᵢ (equivalently, observation-specific dispersion
φ / wᵢ). These are prior weights, not replication counts. Zero-weight observations must be removed
consistently from X, y, sample_weight, and offset before profiling; the
profiler rejects non-positive prior weights.
sample_weight does not enter the linear predictor or automatically scale the
conditional mean. Use an explicit offset when exposure should also enter the mean.
With fit_mode="reml", REML selects spline smoothing penalties within each
candidate fit. The p/φ profile is then evaluated conditionally; it does not jointly estimate p and φ
using an mgcv-style REML objective.
Searching and publishing under different regimes#
A REML-mode search runs a full smoothing-parameter selection inside every
candidate p evaluation, so its cost is roughly the number of search steps
times the cost of one fit_reml. search_fit_mode decouples the two: select
p under ordinary ML, then publish one REML fit at the selected p.
result = model.estimate_p(
df,
y,
sample_weight=exposure,
fit_mode="reml", # what gets published
search_fit_mode="fit", # what selects p
)
Whichever coupling you choose, the published fit is publication-grade.
Candidate fits exist only to rank powers, so they run at a loose smoothing
tolerance; the published refit runs the tight publication default and
re-profiles dispersion against its own fitted mean. result.phi_hat, the
coefficients, and their standard errors always describe the model you get
back, never the fits the search discarded.
Upgrading from 0.19.x: a default-tolerance
fit_reml(and therefore every publishedestimate_pfit) now runs the smoothing optimizer to the determined answer (reml_tol1e-6 → 1e-9 on the Newton engines), andphi_hatis re-profiled at the published fit. Standard errors on designs with flat log-lambda directions can move by tens of percent relative to 0.19.x — once, to the values the old tolerance had left undetermined. Predictions are essentially unchanged. This is not a regression to file; passreml_tol=1e-6to reproduce the old numbers.
The two couplings differ in which objective chooses p, and in what can go wrong on the way:
Agreement. Selecting p under ML and publishing under REML is an approximation. On the synthetic benchmark fixture in
benchmarks/tweedie_reml_search_cost.pyit moved p̂ by 1.1e-6 and ran about 1.8× faster, and across a synthetic sweep spanning a 600× range of fitted penalty strength the mode disagreement was uncorrelated with how strongly REML actually shrinks. That is evidence, not a guarantee — compare againstfit_mode="reml"alone on your own data before adopting it.Robustness. A coupled search evaluates REML at every candidate power, and some powers can have no certifiable penalized mode there; the search routes around them and warns when the selected optimum sits against that boundary, since the true optimum may lie beyond it (a censored estimate). A decoupled search never meets that wall — but its single REML publication can then fail at the selected p, with a typed
superglm.PublicationModeErrorthat reports the certifiability score and names the ways out: search underfit_mode="reml", publish the ML fit, or restrictp_bounds.
Likelihood-ratio confidence intervals remain available for either coupling,
eagerly via ci_alpha or lazily via result.ci(). Imperfect convergence is
disclosed, not refused. If the searched winner’s fit did not converge, a
caution naming the cause is warned and recorded in result.warnings when the
search ends. If a fit the interval evaluates did not converge, its interval
says so. Either way the interval is still computed. It inverts the profile that
was searched, around that profile’s own value at p_hat (recorded as
result.search_nll), so it describes the regime named by search_fit_mode;
result.nll describes the published fit’s re-profiled dispersion. If the
interval’s own evaluations find a power below that value by more than the fits’
own tolerances can resolve, p_hat was a local minimum: the interval is
inverted from the lower power, and a caution says so. A smaller difference is
numerical noise from the fits and changes nothing.
profile_plot measures against the same reference. Summaries show a cautioned
interval as “caution”, or “censored with caution” when it is also censored.
Profile confidence interval#
ci = result.ci(alpha=0.05) # (lower, upper) via profile LRT
Note
result.ci() is explicit and potentially expensive: each new boundary probe
can require a full model refit. It updates the detached returned result, not
the model’s independently owned published profile state. Pass ci_alpha=0.05
to estimate_p() when the interval should be computed transactionally and
cached for model.summary(alpha=0.05). Omitting ci_alpha retains the lazy,
no-extra-CI-work path.
An end of the interval that does not reach the likelihood-ratio cutoff stops
where the search had to stop: at the widest range the interval may use, or
next to a power where the fit failed. That end is marked censored in
result.warnings and in the summary, since the interval may extend beyond it,
and result.interval() and result.ci() warn about it; the summary reports it
without a warning.
Search record and profile plot#
result.evaluations # every searched power: p, nll, phi, fit_converged
result.profile_plot() # likelihood-ratio statistic at the evaluated powers
profile_plot() draws only the powers already evaluated, by the search and by
any interval computed so far, so it performs no new fits. It shades the
interval once one has been computed for the same alpha.
How REML treats the dispersion#
For the estimated-scale families — Gaussian, Gamma, and Tweedie — fit_reml
profiles the dispersion out of the Laplace-approximate REML criterion using
the family’s exact saturated log-likelihood (Wood 2011, Eq. 4; Wood, Pya
& Säfken 2016, §3.3). For Tweedie this matters structurally: the compound
Poisson–gamma response has an atom at zero, so a zero row’s saturated
contribution is dispersion-free while positive rows carry the Dunn–Smyth
series normalizer. Since 0.29.0 the criterion evaluates that likelihood
exactly; earlier releases substituted a Gaussian-shaped
0.5·(n − Mp)·log(Dp) term that overweighted the deviance arm in proportion
to the zero fraction, which mis-set smoothing on zero-heavy Tweedie fits and
could run weakly identified variance components to the boundary. Poisson,
negative binomial, and binomial fix phi = 1 and never enter this profile.
A custom, user-supplied distribution with scale_known=False has no exact
profiler; fit_reml falls back to the Gaussian-shaped substitution and warns
that smoothing selection is approximate for such a family.
Which dispersion a Tweedie fit publishes#
Three entry points publish three different (individually standard) Tweedie dispersion estimators. They agree to a few percent on well-behaved data but are not interchangeable:
fit_remlpublishes the Pearson estimatorphi = sum(w·(y−mu)²/V(mu)) / (n − edf)inresult.phi. The REML criterion internally uses the exact profile MLE to choose the smoothing; the published value remains Pearson.estimate_ppublishes the profile MLE of phi, re-profiled at the published fit’s mean (result.phi_hat).The QP monotone passthrough path publishes the deviance-based
Dp / (n − Mp).
This inventory is deliberate documentation of the current state, not an endorsement: a future release may unify them (the exact MLE is already computed internally). Until then, compare dispersions across entry points only with this table in hand. For reference, mgcv’s default reported scale is a Fletcher-(2012)-improved Pearson estimator, which is a fourth convention again.
What AIC/BIC count — and what they do not#
metrics().aic is −2·loglik(mu_hat, phi_hat) + 2·edf with edf = tr(F),
and BIC uses the same likelihood with a log(n)·edf complexity term (n is
the family’s likelihood size: sum(w) for frequency-weighted families,
the row count for Tweedie prior weights). Two deliberate limitations:
No estimated family parameter is counted. An auto-estimated NB2 theta, an estimated Tweedie power
p, and the estimated dispersionphiadd zero to the complexity term. Within one family this matches statsmodels (which never counts the scale) though not R’slogLik.glm(which counts the Gaussian sigma); across families it means aPoissonvsNegativeBinomial("auto")comparison, or a fixed-pvsestimate_pcomparison, gives the model with the extra estimated parameter that parameter for free. Penalize such comparisons by hand (one parameter ≈ 2 AIC points) or compare on held-out deviance.No smoothing-uncertainty correction.
edf = tr(F)is the conditional effective dimension given the selected lambdas; the Wood–Pya–Säfken (2016) corrected AIC, which accounts for smoothing-parameter estimation, is not implemented.
No quasi families#
There is no quasi-Poisson or quasi-binomial path: Poisson and Binomial pin
phi = 1 with no overdispersion escape hatch. For overdispersed counts use
NegativeBinomial("auto") (theta absorbs the overdispersion) or a Tweedie
in 1 < p < 2; for overdispersed binary data no shipped family applies.
Binomial is Bernoulli-only by contract — grouped binomial data must be
expanded to one row per trial (or use frequency weights on 0/1 rows).