Discrete choice models in Python
Latent classes, clear inference
LCL estimates conditional-logit and latent-class conditional-logit models with JAX. It combines a declarative model specification, safeguarded Newton updates, panel-clustered inference, and practical tools for counterfactual analysis.
Specify once
Define identifiers, utility and membership formulas, constraints, and
publication labels in one LCLSpec.
Estimate efficiently
JAX compiles the numerical kernels and shards independent class updates across available accelerators.
Analyze behavior
Update forecasts with consumer demographics and choice histories. Compute welfare changes, market elasticities, and demographic WTP with parameter uncertainty.
Boundary prices in 0.1.42
Retain valid price-insensitive classes in predictive models and quantify
uncertainty in coefficient means and standard deviations with
InferenceOptions(boundary="projected"). The
boundary-price tutorial includes a reproducible
example and links to the underlying papers.
Class-specific binding-price SEs are suppressed; other class and prediction SEs
remain conditional. The default boundary="strict" is unchanged.
Install
The prediction and welfare guide covers mixed new/returning populations, WTP with attribute interactions, different choice sets, and readable coefficient tables through 64 classes.
The distribution is named lcl-choice; the import is lcl. See
API contracts and compatibility for option precedence,
input alignment, return types, and supported compatibility aliases.
For GPU use, install the JAX build that matches your CUDA environment before installing LCL. See the JAX installation guide.
A compact model
This example estimates two latent classes and assigns readable labels to raw
variables. Labels affect presentation only: formulas, constraints, prediction
inputs, and returned variable columns continue to use the original names.
import lcl
from lcl import (
ChoiceIds,
FitOptions,
LCLSpec,
NegativeCoefficient,
OptimizationOptions,
)
spec = LCLSpec(
ids=ChoiceIds(
alt="alt",
case="case",
panel="panel",
choice="choice",
),
utility_formula="choice ~ price + quality",
membership_formula="~ income",
classes=2,
constraints={"price": NegativeCoefficient()},
variable_labels={
"price": "Price",
"quality": "Product quality",
"income": "Household income",
},
)
results = lcl.fit(
data,
spec,
fit_options=FitOptions(seed=7, starts=3, max_em_iter=50, num_devices=1),
optimization_options=OptimizationOptions(
maxiter=40,
newton_decrement_tol=1e-5,
),
)
coefficient_table = results.summarize_betas()
print(coefficient_table.select("variable", "label", "mean", "mean_se"))
The printed coefficient table uses the labels:
┌─────────────────┬───────────────┬─────────────────────────────┐
│ Variable │ Means (β's) │ Standard deviations (σ's) │
├─────────────────┼───────────────┼─────────────────────────────┤
│ Price │ -1.124 │ 0.723 │
│ │ (0.114) │ (0.128) │
│ Product quality │ 0.906 │ 0.612 │
│ │ (0.097) │ (0.131) │
└─────────────────┴───────────────┴─────────────────────────────┘
The returned frame keeps both identities:
┌──────────┬─────────────────┬────────┬─────────┐
│ variable ┆ label ┆ mean ┆ mean_se │
╞══════════╪═════════════════╪════════╪═════════╡
│ price ┆ Price ┆ -1.124 ┆ 0.114 │
│ quality ┆ Product quality ┆ 0.906 ┆ 0.097 │
└──────────┴─────────────────┴────────┴─────────┘
Use results.summarize_betas(show=False) when you want the returned frame
without the LaTeX and terminal preview.
The same fitted encoder scores held-out choices and powers panel-blocked model selection:
test_log_likelihood = results.loglik(test_data)
panel_scores = results.loglik(test_data, per_panel=True)
cv_results = lcl.cv_optimal_classes(
data,
spec=spec,
num_classes_list=[2, 3, 4],
fit_options=FitOptions(seed=7, starts=3),
)
Cross-validation reports pooled mean held-out log likelihood per panel, fold standard errors, best and one-SE selections, convergence, panel counts, and explicit failure diagnostics. User-defined panel folds are supported.
What the package covers
- Latent-class conditional logit. Estimate a finite mixture of conditional logits, with optional demographic predictors of class membership.
- Standard conditional logit. Fit a homogeneous benchmark with the same data-ingestion and inference conventions.
- Inference and diagnostics. Request panel-clustered sandwich covariance, class/membership and nonlinear ratio standard errors, classification metrics, convergence summaries, and structured audit output.
- Counterfactuals. Reuse the fitted encoder for new choice sets, weighted market shares, aggregate elasticities, and explicitly unit-labeled welfare changes; optionally update class probabilities with observed choice histories.
- Model selection. Compare class counts using blocked cross-validation that keeps each decision-maker entirely within one fold.
Who it is for
LCL is intended for researchers who work with repeated-choice data, including transportation, marketing, operations research, political science, economics, and public policy. Runtime shape checking catches many specification errors before they become opaque compiled-kernel failures.
Next steps
- Follow the estimation and counterfactual tutorial.
- Compare class counts with panel-blocked cross-validation.
- Review the
LCLSpecand options API.
The project is open source under the MIT license. Bug reports, focused feature requests, and reproducible examples are welcome on GitHub.