Skip to content

Specification & options

The high-level entry point. Describe the model declaratively with an LCLSpec, then estimate it with lcl.fit. Behaviour is tuned through four grouped options objects rather than a long keyword list: FitOptions (the EM loop), OptimizationOptions (the M-step optimizer), InferenceOptions (covariance and standard errors), and DiagnosticsOptions (the post-fit health checks).

Fitting

lcl.fit(data, spec, *, fit_options=None, optimization_options=None, inference=None, diagnostics=None)

Fit a latent-class conditional-logit model from an :class:LCLSpec.

Parameters:

Name Type Description Default
data object

Long-format choice data.

required
spec LCLSpec

Declarative model specification.

required
fit_options FitOptions | None

EM algorithm options.

None
optimization_options OptimizationOptions | None

M-step optimizer options.

None
inference InferenceOptions | None

Covariance and standard-error options.

None
diagnostics DiagnosticsOptions | None

Diagnostic thresholds and switches.

None

Returns:

Type Description
LCLResults

Fitted latent-class results.

Source code in src/lcl/__init__.py
def fit(
    data: object,
    spec: LCLSpec,
    *,
    fit_options: FitOptions | None = None,
    optimization_options: OptimizationOptions | None = None,
    inference: InferenceOptions | None = None,
    diagnostics: DiagnosticsOptions | None = None,
) -> LCLResults:
    """Fit a latent-class conditional-logit model from an :class:`LCLSpec`.

    Parameters
    ----------
    data : object
        Long-format choice data.
    spec : LCLSpec
        Declarative model specification.
    fit_options : FitOptions | None, optional
        EM algorithm options.
    optimization_options : OptimizationOptions | None, optional
        M-step optimizer options.
    inference : InferenceOptions | None, optional
        Covariance and standard-error options.
    diagnostics : DiagnosticsOptions | None, optional
        Diagnostic thresholds and switches.

    Returns
    -------
    LCLResults
        Fitted latent-class results.
    """
    model = LatentClassConditionalLogit(spec)
    return model.fit(
        data=data,
        fit_options=fit_options,
        optimization_options=optimization_options,
        inference=inference,
        diagnostics=diagnostics,
    )

Model specification

lcl.spec.LCLSpec(ids, utility=None, membership=None, classes=2, constraints=None, formula=None, utility_formula=None, membership_formula=None) dataclass

Declarative latent-class conditional-logit specification.

Parameters:

Name Type Description Default
ids ChoiceIds

Identifier and choice columns for the long-format dataset.

required
utility Sequence[str] | None

Alternative-specific variables in the utility specification. Omit when utility_formula or legacy formula provides the utility design.

None
membership Sequence[str] | None

Panel-level variables for class-membership probabilities. Omit when membership_formula or legacy formula provides the demographic design.

None
classes int

Number of latent classes.

2
constraints mapping or sequence

Coefficient constraints. The current estimation engine supports one negative coefficient, typically a price, cost, or travel-time numeraire.

None
formula str | None

Backward-compatible combined Formulaic string such as "choice ~ cost + time | income + C(segment)". Prefer utility_formula and membership_formula in new code.

None
utility_formula str | None

Formulaic string for the alternative-specific utility specification, such as "choice ~ cost + C(mode)". A right-hand-side-only utility formula is permitted when the choice column is supplied by :class:ChoiceIds.

None
membership_formula str | None

Right-hand-side Formulaic string for the class-membership demographic regression, such as "~ income + C(segment)".

None

negative_constraint property

Return the single negative constraint, if present.

negative_constraints property

Return normalized negative-coefficient constraints.

numeraire property

Return the constrained variable used as the numeraire.

numeraire_min_abs property

Return the numeraire floor implied by the specification.

__post_init__()

Validate internal consistency.

Source code in src/lcl/spec.py
def __post_init__(self) -> None:
    """Validate internal consistency."""
    if self.classes < 2:
        raise ValueError("LCLSpec.classes must be at least 2.")
    if self.formula is not None and (
        self.utility_formula is not None or self.membership_formula is not None
    ):
        raise ValueError(
            "Use either legacy formula=... or utility_formula=.../"
            "membership_formula=..., not both."
        )
    if self.formula is None and self.utility_formula is None and not self.utility:
        raise ValueError("LCLSpec requires either utility variables or a formula.")
    if len(self.negative_constraints) > 1:
        raise NotImplementedError(
            "The current latent-class estimator supports one negative "
            "coefficient constraint. Multiple constraints can be added once "
            "the optimizer is generalized beyond a single numeraire row."
        )

constraint_rows()

Return serializable constraint metadata.

Source code in src/lcl/spec.py
def constraint_rows(self) -> list[dict[str, object]]:
    """Return serializable constraint metadata."""
    return constraint_summary_rows(self.negative_constraints)

summary_lines()

Return a compact, human-readable specification summary.

Source code in src/lcl/spec.py
def summary_lines(self) -> list[str]:
    """Return a compact, human-readable specification summary."""
    lines = [
        "Latent-class conditional logit",
        f"Classes: {self.classes}",
        f"Panel id: {self.ids.panel}",
        f"Case id: {self.ids.case}",
        f"Alternative id: {self.ids.alt}",
        f"Choice column: {self.ids.choice}",
        "",
        "Utility variables:",
    ]
    if self.formula is not None:
        lines.append(f"  formula: {self.formula}")
    elif self.utility_formula is not None:
        lines.append(f"  formula: {self.utility_formula}")
    else:
        for variable in self.utility or []:
            suffix = ""
            for constraint in self.negative_constraints:
                if constraint.variable == variable:
                    suffix = f" [negative, min_abs={constraint.min_abs:g}]"
            lines.append(f"  {variable}{suffix}")
    lines.append("")
    lines.append("Class-membership variables:")
    if self.membership:
        lines.extend(f"  {variable}" for variable in self.membership)
    elif self.membership_formula is not None:
        lines.append(f"  formula: {self.membership_formula}")
    elif self.formula is not None:
        lines.append("  from formula")
    else:
        lines.append("  none")
    return lines

lcl.spec.ChoiceIds(alt, case, panel, choice) dataclass

Column names identifying a long-format choice dataset.

Parameters:

Name Type Description Default
alt str

Alternative identifier column.

required
case str

Choice-situation identifier column.

required
panel str

Decision-maker or panel identifier column.

required
choice str

Boolean or binary chosen-alternative indicator column.

required

lcl.constraints.NegativeCoefficient(variable=None, min_abs=DEFAULT_NEGATIVE_MIN_ABS, units=None, warn_below=None) dataclass

Constrain a coefficient to be strictly negative.

Parameters:

Name Type Description Default
variable str | None

Name of the variable being constrained. It may be omitted when the object is supplied in a mapping keyed by variable name.

None
min_abs float

Minimum absolute magnitude of the structural coefficient. The forward transform is -(softplus(raw) + min_abs).

1e-5
units str | None

Optional human-readable units for summaries and audit reports.

None
warn_below float | None

Optional threshold used by diagnostics to flag weakly identified numeraires.

None

__post_init__()

Validate constraint settings.

Source code in src/lcl/constraints.py
def __post_init__(self) -> None:
    """Validate constraint settings."""
    if self.min_abs <= 0:
        raise ValueError("NegativeCoefficient.min_abs must be positive.")
    if self.warn_below is not None and self.warn_below <= 0:
        raise ValueError("NegativeCoefficient.warn_below must be positive.")

bind(variable)

Return a copy tied to variable.

Parameters:

Name Type Description Default
variable str

Variable name from a specification mapping.

required

Returns:

Type Description
NegativeCoefficient

A constraint with a concrete variable name.

Source code in src/lcl/constraints.py
def bind(self, variable: str) -> "NegativeCoefficient":
    """Return a copy tied to ``variable``.

    Parameters
    ----------
    variable : str
        Variable name from a specification mapping.

    Returns
    -------
    NegativeCoefficient
        A constraint with a concrete variable name.
    """
    if self.variable is not None and self.variable != variable:
        raise ValueError(
            "NegativeCoefficient variable mismatch: "
            f"{self.variable!r} != {variable!r}."
        )
    return replace(self, variable=variable)

forward(raw)

Map unconstrained parameters to negative structural coefficients.

Source code in src/lcl/constraints.py
def forward(self, raw: Float64[Array, "..."]) -> Float64[Array, "..."]:
    """Map unconstrained parameters to negative structural coefficients."""
    return -(softplus(raw) + self.min_abs)

hessian_diag(raw)

Return the diagonal second derivative of :meth:forward.

Source code in src/lcl/constraints.py
def hessian_diag(self, raw: Float64[Array, "..."]) -> Float64[Array, "..."]:
    """Return the diagonal second derivative of :meth:`forward`."""
    d1 = self.jacobian_diag(raw)
    return d1 * (1.0 + d1)

jacobian_diag(raw)

Return the diagonal Jacobian element of :meth:forward.

Source code in src/lcl/constraints.py
def jacobian_diag(self, raw: Float64[Array, "..."]) -> Float64[Array, "..."]:
    """Return the diagonal Jacobian element of :meth:`forward`."""
    return -sigmoid(raw)

Options

lcl._struct.FitOptions(seed=0, max_em_iter=2000, em_tol=1e-06, num_devices=device_count(), check_interval=10, starts=1, start_method='panel_partition', refit_best_start=True) dataclass

User-facing EM fit options.

Parameters:

Name Type Description Default
seed int

Random seed used for panel-partition starts.

0
max_em_iter int

Maximum number of EM recursions.

2000
em_tol float

Relative log-likelihood tolerance checked over the EM history.

1e-6
num_devices int

Number of local JAX devices used for class-wise beta updates.

device_count()
check_interval int

Frequency of convergence checks.

10
starts int

Reserved for future multi-start orchestration.

1
start_method str

Initialization method label.

"panel_partition"
refit_best_start bool

Reserved for future multi-start orchestration.

True

to_em_config()

Convert to the internal EM configuration dataclass.

Source code in src/lcl/_struct.py
def to_em_config(self) -> EMAlgConfig:
    """Convert to the internal EM configuration dataclass."""
    return EMAlgConfig(
        jax_prng_seed=self.seed,
        loglik_tol=self.em_tol,
        maxiter=self.max_em_iter,
        num_devices=self.num_devices,
        check_interval=self.check_interval,
    )

lcl._struct.OptimizationOptions(maxiter=75, ftol=1e-05, method='newton', gradient_tol=None, hessian_damping=1e-06, max_step_norm=25.0, line_search='armijo', fallback='gradient') dataclass

Bases: MleConfig

User-facing optimizer settings.

Parameters:

Name Type Description Default
maxiter int

Maximum Newton/BFGS iterations used inside each M-step.

75
ftol float

Gradient tolerance. Kept for backward compatibility with :class:MleConfig.

1e-5
method str

Optimizer family requested by the user. The latent-class M-step currently uses exact Newton updates.

"newton"
gradient_tol float | None

More descriptive alias for ftol. When supplied, it overrides ftol.

None
hessian_damping float

Reserved for explicit optimizer configuration in future releases.

1e-6
max_step_norm float

Reserved for explicit optimizer configuration in future releases.

25.0
line_search str

Name of the line-search strategy.

"armijo"
fallback str

Fallback direction when the Newton direction is not a descent direction.

"gradient"

__post_init__()

Normalize aliases to the legacy fields consumed by internals.

Source code in src/lcl/_struct.py
def __post_init__(self) -> None:
    """Normalize aliases to the legacy fields consumed by internals."""
    if self.gradient_tol is not None:
        self.ftol = self.gradient_tol

lcl._struct.InferenceOptions(robust=True, skip_std_errs=False, covariance='clustered', cluster='panel', finite_sample_correction=True, skip=False) dataclass

Bases: ErrorConfig

User-facing inference and covariance settings.

Parameters:

Name Type Description Default
robust bool

Legacy flag controlling robust covariance calculation.

True
skip_std_errs bool

Legacy flag to skip standard-error calculations.

False
covariance str

Covariance estimator label. "none"/"unadjusted" disable the sandwich correction; "clustered" and "robust" enable it.

"clustered"
cluster str | None

Cluster level label for reports. The current latent-class estimator clusters at panel level.

"panel"
finite_sample_correction bool

Whether reports should describe the finite-sample correction.

True
skip bool

Descriptive alias for skip_std_errs.

False

__post_init__()

Normalize user-facing covariance labels.

Source code in src/lcl/_struct.py
def __post_init__(self) -> None:
    """Normalize user-facing covariance labels."""
    covariance = self.covariance.lower()
    if covariance in {"none", "unadjusted", "hessian"}:
        self.robust = False
    elif covariance in {"clustered", "robust", "sandwich"}:
        self.robust = True
    else:
        raise ValueError(
            "InferenceOptions.covariance must be one of 'clustered', "
            "'robust', 'sandwich', 'unadjusted', or 'none'."
        )
    if self.skip:
        self.skip_std_errs = True

lcl._struct.DiagnosticsOptions(check_separation=True, check_collinearity=True, warn_near_zero_numeraire=True, warn_large_coefficients=True, near_zero_numeraire_threshold=0.001, large_coefficient_threshold=25.0) dataclass

Options controlling public diagnostic summaries.