OrderedCategorical#

class superglm.OrderedCategorical(
values: dict[str, float] | None = None,
order: list[str] | None = None,
basis: _SplineBase | None = None,
base: str = 'most_exposed',
grouping: Any = None,
specials: list[str] | None = None,
)#

Bases: object

Ordered categorical feature smoothed by a spline over its level values.

Designed for continuous variables that arrive pre-binned into ordered categories (e.g. age bands, mileage bands). Maps category labels to numeric values and fits a smooth function through them, borrowing strength between adjacent levels. With fit_reml(), REML selects the smoothing parameter automatically — the effective degrees of freedom will typically be much less than the number of levels.

The smooth is configured by passing a Spline() specification as basis:

OrderedCategorical(
    order=["low", "medium", "high"],
    basis=Spline(kind="ps", k=6),
)
Parameters:
valuesdict[str, float] or None

Explicit mapping from category labels to numeric values (e.g. midpoints: {"18-25": 21.5, "26-35": 30.5, ...}). Mutually exclusive with order.

orderlist[str] or None

Ordered list of category labels. Numeric values are generated as linspace(0, 1, len(order)). Mutually exclusive with values.

basisSpline, Piecewise, Polynomial object, or None

The shape fitted over the ordered levels. Deep-copied at construction, so mutating the passed object afterwards changes nothing here.

Spline(...) gives the penalized smooth:

OrderedCategorical(order=[...], basis=Spline(kind="cr", k=6))

Omitting basis retains the historical default P-spline, Spline(kind="ps", n_knots=5, degree=3, penalty="ssp", select=False). In either form n_knots is clamped to n_levels - 1 with a warning. Spline(knots=[...]) may state knots as BAND NAMES (knots=["Mi060", "Mi066"]): each name resolves to that level’s VALUE on the smooth’s axis at construction – smooth-at-stated-breaks needs no new device, because a spline IS the C1 piecewise polynomial. Numeric entries stay axis values, so both spellings live on one scale.

Piecewise(breaks=[...]) gives stated kinks with NO smoothing penalty – an unpenalized main block, exactly like the specials= block. The inner basis evaluates on level positions 0..L-1 (declared order; values= still sets the order but not the spacing). Breaks are stated as band names, with integer positions as the escape hatch; degrees=[...] states one polynomial degree per segment (0 = flat/grouped tail), value-continuous at every seam by construction. Rating-table export stays one row per band at any degree, which is why per-segment degrees are legal here and only here. Piecewise’s extrapolation parameter is inert on the level axis – every level lies inside [0, L-1] by construction, so no policy ever binds – and is deliberately ignored rather than refused.

Polynomial(powers=[...]) gives classical orthogonal ordinal contrasts (the contr.poly device) built on the level positions and orthonormalized against the training exposure – SAS ORPOL’s weighted construction inside a modeling term. Classical trend practice keeps lower-order contrasts under a significant higher-order one (the hierarchical convention); powers= deliberately allows non-contiguous subsets, each orthogonal component individually in or out. Each stated power reports its own clean-z summary row – a main-effect property the segmented Piecewise deliberately does not claim.

The legacy strings "spline" and "step", and the scalar shortcut parameters kind, n_knots, degree, select and penalty, were removed in 0.24.0.

basestr

Reporting reference level. "most_exposed" (default), "first", or a specific level name. This changes only the reported relativities and reference-adjusted intercept, not the fitted smooth.

specialslist[str] or None

Level labels held out of the smooth and fitted as free, unpenalized level effects — one indicator column and one coefficient each. Use for levels that are structurally different rather than merely sparse (a MISSING band, a structural zero); the penalty already handles sparse bands better than free levels do. A label listed here is removed from order/values if also present there, and never receives a numeric position on the smooth’s axis.

Examples

Using ordered level names (auto-spaced 0 to 1) with an explicit smooth:

OrderedCategorical(
    order=["18-25", "26-35", "36-45", "46-55", "56+"],
    basis=Spline(kind="ps", k=6),
)

Using explicit midpoints:

OrderedCategorical(
    values={"18-25": 21.5, "26-35": 30.5, "36-45": 40.5},
    basis=Spline(kind="cr", k=4),
)

Independent, unsmoothed level effects should use Categorical:

Categorical(base="most_exposed")
property kind: str#

Public factory kind of the inner spline ("ps", "cr", …).

property n_knots: int#

Knot count of the inner spline, after the n_levels - 1 clamp.

property degree: int#

B-spline degree of the inner spline.

property select: bool#

Whether the inner spline carries double-penalty selection.

property penalty: str#

Penalty type of the inner spline.

property constraint_kind: str | None#

Shape token declared on the inner basis ("increasing", …).

The constraint binds on the WHOLE LEVEL AXIS, not only on the L fitted level values – a stated contract, and deliberately conservative. Every published method for a monotone ordinal effect constrains the level effects directly (Rufibach, Comput. Statist. Data Anal. 54(6):1442-1456, 2010, Sec. 5; Barlow, Bartholomew, Bremner & Brunk, 1972); none constrains a curve between category positions, and the interval version is strictly stronger, since coefficient sign conditions are “sufficient but not necessary” for a monotone effect (Hofner, Kneib & Hothorn, Statist. Comput. 26:1-14, 2016, Sec. 3.3) and no proper linear-inequality basis exists for monotone CUBICS at all (Meyer, Ann. Appl. Statist. 2(3):1013-1033, 2008, Sec. 2). See docs/how-to/constrain-a-smooth.md for the measured cost.

property constraint_mode: str#

Whether the declared constraint binds at fit time or post-fit.

shape_axis(
x: NDArray,
) → tuple[NDArray, NDArray[bool]]#

The inner basis’ numeric axis, and the rows that sit on it.

A shape constraint on an ordered term is stated over the inner spline’s level-score axis, not over the label column the frame carries. Engines that read a raw numeric column off the frame therefore cannot read this term’s; they have to resolve the axis through the wrapper, exactly as transform does.

The returned mask is not decoration: a specials= level is fitted as a free effect with no coordinate on the smooth’s axis, so its rows are absent from the first return value and a caller carrying per-row weights must drop the same rows to keep them aligned.

property has_specials: bool#

True when one or more levels are fitted as free effects.

property basis_kind: str#

Kind of the inner basis: "spline", "piecewise" or "polynomial".

build(
x: NDArray,
sample_weight: NDArray[floating] | None = None,
) → GroupInfo | list[GroupInfo]#

Build design columns from ordered categorical data.

With specials=, returns two GroupInfos in a fixed order: the penalized spline block first, the unpenalized special-indicator block second. Downstream metadata readers select by subgroup_type, but the order is part of the contract — _split_beta and transform both assume it. The free block covers the specials that have effective rows in THIS fit; when every declared special is pinned there is no second block and one GroupInfo comes back.

transform(
x: NDArray,
) → NDArray#

Build design matrix for new data using learned parameters.

score(
x: NDArray,
beta: NDArray[floating],
) → NDArray[floating]#

Score the fitted ordered-categorical contribution directly on new data.

reconstruct(
beta: NDArray[floating],
) → dict[str, Any]#

Convert fitted coefficients to interpretable output.