OrderedCategorical

Ordered categorical (proportional odds / cumulative logit) family.

Usage

Source

OrderedCategorical(n_categories)

OrderedCategorical models an ordinal response — one with a small number of categories that have a natural order but no meaningful numeric spacing, such as a Likert scale (“disagree” / “neutral” / “agree”) or a severity grade — using the proportional-odds cumulative logit model. A single set of K - 1 ordered cutpoints (thresholds) alpha_1 < alpha_2 < ... < alpha_{K-1} is estimated jointly with the smooth/linear predictor eta, and each cutpoint defines a binary split between “category k or below” versus “above category k”. Because the same eta (and hence the same covariate effects) is shared across all thresholds, covariate effects are assumed to shift the log-odds of being in a higher category by the same amount at every threshold — the proportional-odds assumption. Use this family for ordinal outcomes with three or more ordered levels; for a two-level (binary) response, use Binomial instead, and for unordered categorical responses, use Multinomial.

Parameters

n_categories: int
Number of ordered response categories K (must be >= 2). Responses passed to GAM.fit() should be integer-coded 1, 2, ..., K.

Notes

The response, without an intercept in the design matrix (since the cutpoints absorb it), is modeled through cumulative probabilities:

P(Y \le k \mid \eta) = \operatorname{expit}(\alpha_k - \eta), \qquad k = 1, \dots, K-1,

which is equivalent to a logit link on each cumulative probability, g(P(Y \le k)) = \alpha_k - \eta. Category probabilities follow by differencing:

P(Y = 1) = \operatorname{expit}(\alpha_1 - \eta), \qquad P(Y = K) = 1 - \operatorname{expit}(\alpha_{K-1} - \eta),

and for interior categories 1 < k < K,

P(Y = k) = \operatorname{expit}(\alpha_k - \eta) - \operatorname{expit}(\alpha_{k-1} - \eta).

Because this loss does not fit the standard GLM deviance framework, link and link_inverse are the identity on eta, and fitting instead uses a custom irls_update together with an inner maximum-likelihood step (_update_cutpoints) that re-estimates the cutpoints alpha at each P-IRLS iteration. The deviance reported is -2 times the multinomial log-likelihood of the observed categories under the fitted probabilities.

Examples

Fit a GAM to a four-level ordinal response with a smooth covariate effect:

import numpy as np
import whittaker as wk
from scipy.special import expit

rng = np.random.default_rng(0)
n = 300
x = np.linspace(-3, 3, n)
eta = np.sin(x)
cutpoints = np.array([-1.5, 0.0, 1.5])

y = np.empty(n)
for i in range(n):
    probs = np.diff(
        np.concatenate([[0.0], expit(cutpoints - eta[i]), [1.0]])
    )
    y[i] = rng.choice([1, 2, 3, 4], p=probs)

data = {"x": x, "y": y}

model = wk.GAM("y ~ s(x)", family=wk.OrderedCategorical(n_categories=4))
model.fit(data, method="REML")
print(model.summary())
GAM fit summary
============================================================
Formula:    y ~ s(x)
Family:     OrderedCategorical(K=4)
Inference:  REML
Observations: 300
Coefficients: 10

Parametric coefficients:
  Term                       Estimate    Std.Err    z value    p-value
  ------------------------ ---------- ---------- ---------- ----------
  (Intercept)                  0.0143     0.0887      0.161     0.8722

Approximate significance of smooth terms:
  Term                        EDF Ref.df     Chi.sq    p-value
  ------------------------ ------ ------ ---------- ----------
  s(x)                       5.02      6     51.396  2.466e-09

Total EDF:  6.02
Scale est:  1.000000
Deviance:   785.2424
Null dev:   1365.4479
Dev. expl:  42.5%
GCV score:  2.725803
AIC:        797.29
BIC:        819.59

Attributes

Name Description
cutpoints Fitted cutpoints (thresholds) alpha_1 < alpha_2 < ... < alpha_{K-1}.
n_categories Number of ordered response categories K.
scale_known Whether the dispersion (scale) parameter is fixed rather than estimated.

cutpoints

Fitted cutpoints (thresholds) alpha_1 < alpha_2 < ... < alpha_{K-1}.

cutpoints: NDArray | None

Each cutpoint alpha_k defines the boundary of the cumulative logit P(Y \le k \mid \eta) = \operatorname{expit}(\alpha_k - \eta) separating category k or below from categories above k. The cutpoints are estimated jointly with eta and are updated at every P-IRLS iteration by _update_cutpoints.


n_categories

Number of ordered response categories K.

n_categories: int

This is the value supplied to the constructor and equals 1 + the number of cutpoints (thresholds) fitted by the model.


scale_known

Whether the dispersion (scale) parameter is fixed rather than estimated.

scale_known: bool

Overrides the base Family.scale_known (which returns False) because the multinomial log-likelihood underlying the proportional-odds model has no free dispersion parameter — the scale is always 1 and is never estimated during fitting.

Methods

Name Description
deviance() Total deviance, -2 times the multinomial log-likelihood of the observed categories.
initialize() Initialize cutpoints and the starting linear predictor before P-IRLS.
irls_update() Working response and weights for the cumulative-logit log-likelihood.
link() Identity link, g(mu) = mu.
link_derivative() Derivative of the identity link, g'(mu) = 1.
link_inverse() Identity inverse link, g^{-1}(eta) = eta.
log_likelihood() Log-likelihood of the observed categories, -0.5 * deviance(y, mu).
simulate() Simulate ordinal category labels from the fitted cumulative-logit model.
unit_deviance() Per-observation deviance contributions, overriding the base (y - mu)^2 default.
variance() Placeholder variance function, V(mu) = 1.

deviance()

Total deviance, -2 times the multinomial log-likelihood of the observed categories.

Usage

Source

deviance(
    y,
    mu,
    *,
    weights=None,
)

Implements the family-specific version of the abstract Family.deviance. Because the proportional-odds model has a saturated log-likelihood of 0 (each observation can be assigned probability 1 to its own category), the deviance reduces to

D(y, \eta) = -2 \sum_i \log P(Y_i = y_i \mid \eta_i),

with the cumulative category probabilities P(Y_i = k \mid \eta_i) computed by _category_probs from the current cutpoints. The weights argument is accepted for interface compatibility but is not applied.

Parameters

y: NDArray

Observed category labels, coded 1, ..., K, shape (n,).

mu: NDArray

Linear predictor values eta (see link), shape (n,).

weights: NDArray | None = None
Unused.

Returns

float
The total deviance, or len(y) as a placeholder if the model has not yet been fitted (cutpoints not initialized).

initialize()

Initialize cutpoints and the starting linear predictor before P-IRLS.

Usage

Source

initialize(y)

Overrides the base Family.initialize (which returns y unchanged). Sets the initial cutpoints via _init_cutpoints, which derives each threshold alpha_k from the empirical logit of P(Y <= k + 1), and starts the linear predictor eta at zero for every observation (i.e. an intercept-only model at the first iteration).

Parameters

y: NDArray
Observed category labels, coded 1, ..., K, shape (n,).

Returns

NDArray
Starting linear predictor values, an array of zeros with shape (n,).

irls_update()

Working response and weights for the cumulative-logit log-likelihood.

Usage

Source

irls_update(
    y,
    mu,
    eta,
)

Implements the family-specific override of the base Family.irls_update (whose default returns None and falls back to the standard GLM formula). On the first call the cutpoints are initialized via _init_cutpoints; on every call the cutpoints are then re-estimated by maximum likelihood via _update_cutpoints given the current eta, implementing the two-block (cutpoints, then smooth) coordinate ascent that fits the proportional-odds model. Category probabilities are computed by _category_probs, and for each observation i in its observed category k = y_int[i] - 1 the first and second derivatives of the log-likelihood

\ell_i(\eta_i) = \log\bigl[P(Y_i = k+1 \mid \eta_i)\bigr]

with respect to eta_i are computed in closed form (differing at the boundary categories k = 0 and k = K - 1 versus interior categories). The working weight is the (clipped, non-negative) negative second derivative, and the working response is the Newton step z_i = \eta_i + (d\ell_i/d\eta_i) / W_i.

Parameters

y: NDArray

Observed category labels, coded 1, ..., K, shape (n,).

mu: NDArray

Unused; OrderedCategorical uses eta directly (see link).

eta: NDArray
Current linear predictor values, shape (n,).

Returns

tuple of NDArray, NDArray
(z, W), the working response and working weights, each shape (n,).




log_likelihood()

Log-likelihood of the observed categories, -0.5 * deviance(y, mu).

Usage

Source

log_likelihood(
    y,
    mu,
    scale,
    *,
    weights=None,
)

Implements the family-specific version of the abstract Family.log_likelihood. Since deviance is already defined as -2 times the multinomial log-likelihood, this simply inverts that relationship; the scale parameter is ignored because OrderedCategorical has no free dispersion parameter (see scale_known).

Parameters

y: NDArray

Observed category labels, coded 1, ..., K, shape (n,).

mu: NDArray

Linear predictor values eta (see link), shape (n,).

scale: float

Unused; the scale is fixed at 1 for this family.

weights: NDArray | None = None
Optional prior weights, forwarded to deviance.

Returns

float
The total multinomial log-likelihood.

simulate()

Simulate ordinal category labels from the fitted cumulative-logit model.

Usage

Source

simulate(
    mu,
    scale,
    rng,
)

Implements the family-specific version of the abstract Family.simulate. For each observation, category probabilities are computed from the current cutpoints and eta via _category_probs, and a category in 1, ..., K is drawn from the resulting categorical distribution using rng.choice.

Parameters

mu: NDArray

Linear predictor values eta (see link), shape (n,).

scale: float

Unused; this family has no free dispersion parameter (see scale_known).

rng: np.random.Generator
A numpy.random.Generator instance used to draw the simulated categories.

Returns

NDArray
Simulated category labels, coded 1, ..., K, shape (n,).

Notes

Raises RuntimeError if called before the model has been fitted (i.e. before initialize has set cutpoints).


unit_deviance()

Per-observation deviance contributions, overriding the base (y - mu)^2 default.

Usage

Source

unit_deviance(
    y,
    mu,
)

Implements the family-specific per-observation deviance so that the total returned by deviance decomposes as sum(unit_deviance(y, mu)). For observation i in category y_i, the contribution is

d_i = -2 \log P(Y_i = y_i \mid \eta_i),

using the cumulative category probabilities from _category_probs.

Parameters

y: NDArray

Observed category labels, coded 1, ..., K, shape (n,).

mu: NDArray
Linear predictor values eta (see link), shape (n,).

Returns

NDArray
Per-observation deviance contributions d_i, shape (n,), or an array of ones if the model has not yet been fitted (cutpoints not initialized).

variance()

Placeholder variance function, V(mu) = 1.

Usage

Source

variance(mu)

OrderedCategorical does not fit the standard GLM variance-function framework: its working weights are instead derived from the second derivative of the multinomial log-likelihood with respect to eta inside irls_update. This method returns a constant and is not used by the fitting loop.

Parameters

mu: NDArray
Values used only to determine the output shape, shape (n,).

Returns

NDArray
Array of ones, shape (n,).