Credibility And Factor-Varying Smooths#
RandomEffect and FactorSmooth(..., basis="fs") are REML-selected
credibility terms. They are useful when a categorical level has its own
experience, but thinner levels should borrow strength from the portfolio
instead of receiving unrestricted fixed effects.
FactorSmooth(..., basis="sz") is related but has a different interpretation:
it estimates centered level deviations around a required global curve. Its
wiggle is smoothed, but its polynomial null space is not fully shrunk, so its
level table is not labelled as credibility or collapse.
SuperGLM |
mgcv analogue |
What varies by level |
|---|---|---|
|
|
one intercept |
|
|
a fully penalized smooth curve |
|
|
a centered deviation curve |
All three use every fitted level rather than dropping a reference level. REML estimates their penalty strengths. For RE and FS this controls full shrinkage toward the population prediction; for SZ it controls wiggle around a sum-to-zero deviation surface.
Random intercept credibility#
Use a random effect when each level needs one relativity.
import numpy as np
from superglm import Numeric, RandomEffect, SuperGLM
model = SuperGLM(
family="poisson",
features={
"LogDensity": Numeric(),
"VehBrand": RandomEffect(),
},
selection_penalty=0.0,
)
model.fit_reml(
train,
train["ClaimNb"],
offset=np.log(train["Exposure"]),
)
The explicit selection_penalty=0.0 documents the fitting contract; it is not
an additional tuning parameter. fit_reml() owns the smoothing and
variance-component lambdas and therefore accepts only None or 0.0 for
selection. Omitted, None, and zero are equivalent here. Sparse term
selection belongs to fit() or fit_path(), not the REML fit.
The estimated variance component is \(\tau^2 = \phi / \lambda\). At level \(j\), SuperGLM reports scalar credibility
where \(I_j\) is the fitted working information. Thick levels have \(Z_j\) near one and retain more of their own estimate; thin levels have \(Z_j\) near zero and are pulled more strongly toward the population.
brand = model.random_effects(
"VehBrand",
exposure=train["Exposure"].to_numpy(),
)
brand.lambda_value
brand.tau_squared
brand.effective_df
brand.table[
[
"level",
"exposure",
"unpooled_effect",
"effect",
"posterior_se",
"credibility",
]
]
Fully penalized FS credibility#
Use a factor smooth when the deviation itself changes over a continuous variable. For example, the driver-age curve can differ by region:
import numpy as np
from superglm import FactorSmooth, Spline, SuperGLM
model = SuperGLM(
family="poisson",
features={
# The population curve
"DrivAge": Spline(kind="ps", k=8),
},
interactions=[
# Fully penalized regional deviations from that curve
FactorSmooth("DrivAge", group="Region", basis="fs", k=6),
],
selection_penalty=0.0,
)
model.fit_reml(
train,
train["ClaimNb"],
offset=np.log(train["Exposure"]),
)
One shared P-spline basis is repeated implicitly for every factor level. The
term has shared wiggle and null-space smoothing parameters, but each level
gets its own coefficient block. The null-space penalties make the deviations
fully penalized, matching the role of mgcv’s bs="fs" basis.
For level \(j\), the scalar summary generalizes ordinary credibility to the whole coefficient block:
It is the mean fraction of the level’s \(k\) coefficient directions retained after shrinkage. It is a useful compact ranking, not a claim that all parts of the curve receive identical shrinkage.
regional_age = model.factor_smooth(
"DrivAge:Region:fs",
grid=80,
levels=["R11", "R24"],
)
regional_age.lambdas
regional_age.variance_components
regional_age.table[
["level", "fit_weight", "effective_df", "credibility", "sufficient_support"]
]
regional_age.curves[
["level", "DrivAge", "effect", "posterior_se", "lower", "upper"]
]
Add a global Spline("DrivAge") when the scientific question is “what is the
portfolio age curve, and how does each region deviate from it?” A factor
smooth can also be fitted without a global smooth when complete level-specific
curves are the intended model.
The FS null-space smoothing components are REML smoothing/variance
components. They are not sparse selection penalties, and they work with
fit_reml() while selection_penalty remains zero.
Centered SZ deviations#
Use SZ when the global curve should carry the portfolio-wide shape and the level curves should be identifiable deviations from it:
import numpy as np
from superglm import FactorSmooth, Spline, SuperGLM
model = SuperGLM(
family="poisson",
features={"DrivAge": Spline(kind="ps", k=7, m=2)},
interactions=[
FactorSmooth(
"DrivAge",
group="Region",
basis="sz",
kind="ps",
k=6,
m=2,
)
],
selection_penalty=0.0,
).fit_reml(train, train["ClaimNb"], offset=np.log(train["Exposure"]))
regional_deviation = model.factor_smooth("DrivAge:Region:sz", grid=80)
Equivalent marginal coefficients across levels sum to zero, so the deviation
curves also sum to zero pointwise. There is one shared wiggle lambda. The
polynomial null space remains unpenalized: even an extremely large wiggle
lambda can leave finite linear or low-order polynomial deviations.
Consequently regional_deviation.collapsed is None, and its table reports
support, information, EDF, and coefficient norms without credibility or
shrinkage columns.
SZ requires the matching global Spline. It is not a generic
SplineCategorical interaction: SplineCategorical is reference-coded and
unpooled, while SZ uses all levels symmetrically with an exact sum-to-zero
constraint.
Do not add Categorical() or RandomEffect() for the same grouping column as
a FactorSmooth. The factor smooth already contains the lower-order group
geometry, so those terms would duplicate it. SuperGLM rejects that
configuration. Random effects for a different grouping column are supported.
Conditional and population prediction#
Prediction is conditional by default and includes fitted random effects and factor-smooth deviations:
conditional = model.predict(test)
population = model.predict(test, random_effects="population")
random_effects="population" zeros all RandomEffect and FactorSmooth
contributions while retaining fixed effects and global smooths. By default, an
unseen factor level also receives this zero-deviation population prediction.
Use unseen="error" on either term when an unknown level must fail instead.
Missing group values always fail.
Exact, discrete, and structured fitting#
These terms use compact group matrices alongside tabmat-backed narrow design blocks. The structured solver factors the small dense part once. RE and FS use independent scalar or \(k \times k\) local blocks; SZ uses raw all-level blocks plus a small equality-constrained border. It does not form the full \((Kk)^2\) FS or \(((K-1)k)^2\) SZ Hessian.
model = SuperGLM(
family="poisson",
features=features,
interactions=[
FactorSmooth("DrivAge", group="Region", basis="fs", k=6)
],
discrete=True,
n_bins=256,
direct_solve="auto",
selection_penalty=0.0,
)
model.fit_reml(train, y, offset=offset)
direct_solve="auto" retains Gram fitting for small terms and switches to the
structured backend at the measured crossover, which is reached only when the
structured term spans the large majority of the coefficient width: the
factorization saving has to clear the per-iteration moment work both backends
share, so a wide random effect beside an equally wide dense border fits faster
on Gram and stays there. direct_solve="structured"
requires eligible geometry and is useful for reproducible benchmarking.
Locally rank-deficient SZ levels remain eligible because the exact
sum-to-zero constraint can make the full system identifiable. If that global
constrained system is still unidentifiable, "auto" retries on Gram and
records the reason; forced "structured" raises the global identifiability
error.
discrete=True bins the continuous spline support and reuses cached
sufficient statistics across REML iterations; factor identities and the SZ
constraint remain exact.
Tabmat still owns ordinary dense, sparse, and categorical blocks and the
dense-small side of the structured system. Factor smooths retain a more
compact codes + shared basis representation with compiled raw-moment
kernels. Expanding the dominant term into a generic tabmat sparse block would
increase storage and weighted sandwich-product work; the structured solver
combines the two representations at their natural boundary.
Real French motor example#
examples/fremtpl2_credibility.py
uses the real
freMTPL2freq OpenML data set
and follows the caps used by
scikit-learn’s insurance-pricing example.
Each row is a French motor third-party-liability policy with claim count,
exposure, driver, vehicle, and geographic attributes.
Run the deterministic 30,000-policy comparison:
uv run python examples/fremtpl2_credibility.py \
--max-rows 30000 \
--output-dir fremtpl2_credibility_results
The common baseline uses smooth driver age, vehicle age, and Bonus-Malus plus
the remaining tariff controls. All challengers use Poisson claim counts with
log(Exposure) as an offset. The test split contains 7,500 policies and 401
observed claims.
The table below is a 24 July 2026 snapshot generated with seed 20260724
against the OpenML and preprocessing versions available on that date. It is
not a CI-pinned data or performance contract. Regenerate it when the upstream
data set, preprocessing, solver, or material dependencies change.
Challenger |
Credibility addition |
Held-out deviance |
Change vs baseline |
Fit time |
|---|---|---|---|---|
baseline |
none |
0.588680 |
— |
2.50 s |
brand fixed |
unrestricted vehicle-brand factor |
0.590677 |
-0.339% |
2.58 s |
|
vehicle-brand random effect |
0.589453 |
-0.131% |
1.58 s |
|
driver-age deviations by region |
0.587040 |
+0.279% |
3.81 s |
|
both terms |
0.587985 |
+0.118% |
7.00 s |
These are one seeded demonstration, not a universal ranking. They make the modeling distinction visible:
unrestricted brand coefficients overfit this split;
brand credibility partially pools those estimates and removes most of the held-out damage;
regional factor smooths capture useful shape variation rather than only a level shift;
combining plausible terms does not guarantee the best holdout score.
Vehicle-brand credibility ranged from 0.054 to 0.749; the thinnest brand
effects were pulled hardest toward zero. Regional factor-smooth credibility
ranged from 0.0069 to 0.262, reflecting the greater information required
to estimate a complete curve rather than one intercept.
The script writes held-out metrics, the random-effect table, factor-smooth
credibility and curve tables, and credibility_demo.png.
Current scope#
FactorSmoothsupportsbasis="fs"andbasis="sz", currently withkind="ps", and requiresfit_reml().Discrete FS/SZ constructs the normal
m <= 2marginal in bounded QR chunks before evaluating the final support basis. FS declarations with asymmetric null-component policies orm > 2retain a reduced-memory dense compatibility construction so those custom penalty coordinates do not change silently.fit()andfit_path()reject factor smooths.Factor-smooth
kmust be at least 5.Missing numeric or grouping values are rejected.
The mgcv 1.9-4 FS and SZ Gaussian, Poisson, construction, unseen-level, and discrete reference cases are pinned in the test suite.