Skip to main content

A high-performance Python package for estimating latent-class conditional logit models.

Project description

LCL: Latent-Class Conditional Logit Estimation in Python

PyPI version Python 3.10+ License: MIT

LCL is a Python package for fitting latent-class conditional logit models. It runs the expectation-maximization algorithm on JAX, sharding the per-class M-steps across whatever accelerators it finds, and returns a results object with clustered robust standard errors, counterfactual predictions, and Delta-method willingness-to-pay distributions.

It is written for econometricians who routinely outgrow mlogit, gmnl, or Apollo on large panel datasets. Eighty thousand households with twenty choice occasions apiece is a comfortable working size on a single H200.

What is in the package

  • LatentClassConditionalLogit — finite-mixture conditional logit with a fractional-response multinomial regression for class membership on demographics.
  • ConditionalLogit — a standard conditional logit, useful as both a baseline and the inner kernel of the M-step.
  • cv_optimal_classes — blocked K-fold cross-validation for choosing the number of latent classes; folds are split at the decision-maker level.
  • Counterfactual prediction — out-of-sample choice probabilities, expected consumer surplus, own- and cross-elasticities, and marginal willingness-to-pay broken out by demographic partitions.
  • Inference — clustered sandwich covariance at the panel level and the Delta method for non-linear parameter combinations.

Type contracts are enforced at runtime through jaxtyping and beartype: a wrongly shaped design matrix raises a readable error at the call site rather than a trace through XLA.

Installation

The wheel is published on PyPI as lcl-choice (it imports as lcl):

pip install lcl-choice

If you intend to run on a GPU, install the CUDA-matched JAX build first; see the JAX installation notes.

Quickstart

A two-class model on a small synthetic panel — one class price-sensitive, the other quality-loving.

import numpy as onp
import polars as pl
import lcl
from lcl._struct import EMAlgConfig, MleConfig

rng = onp.random.default_rng(7)

n_panels, n_choices, n_alts = 200, 4, 3
true_class = rng.choice(2, size=n_panels, p=[0.55, 0.45])
beta_price   = onp.array([-1.8, -0.3])
beta_quality = onp.array([ 0.4,  1.6])

rows = []
for panel in range(n_panels):
    income = rng.normal()
    for case in range(n_choices):
        prices  = rng.uniform(0.5, 3.0, size=n_alts)
        quality = rng.uniform(0.0, 5.0, size=n_alts)
        u = (beta_price[true_class[panel]]   * prices
           + beta_quality[true_class[panel]] * quality
           + rng.gumbel(size=n_alts))
        chosen = int(onp.argmax(u))
        for alt in range(n_alts):
            rows.append({
                "panel": panel,
                "case":  panel * n_choices + case,
                "alt":   alt,
                "choice":  alt == chosen,
                "price":   float(prices[alt]),
                "quality": float(quality[alt]),
                "income":  float(income),
            })

df = pl.DataFrame(rows)

model = lcl.LatentClassConditionalLogit(num_classes=2, numeraire="price")
results = model.fit(
    data=df,
    alts_col="alt",
    cases_col="case",
    panels_col="panel",
    choice_col="choice",
    case_varnames=["price", "quality"],
    dem_varnames=["income"],
    em_alg_config=EMAlgConfig(maxiter=50, num_devices=1),
    mle_config=MleConfig(maxiter=40),
)

results.summarize_betas()
print(results)

A representative end-of-run printout:

Estimation time: 15.705 seconds
Information criteria: CAIC=1233.4, BIC=1227.4, adjusted BIC=1197.4

--- Table preview ---

┌──────────┬─────────────┬───────────────────────────┐
│ Variable │ Means (β's) │ Standard deviations (σ's) │
├──────────┼─────────────┼───────────────────────────┤
│ price    │ -1.124      │ 0.723                     │
│          │ (0.114)     │ (0.128)                   │
│ quality  │  0.905      │ 0.611                     │
│          │ (0.097)     │ (0.130)                   │
└──────────┴─────────────┴───────────────────────────┘

<LCLResults: 2 Classes | Converged | Log likelihood: -597.8 |
 CAIC: 1233.4 | BIC: 1227.4 | Adj. BIC: 1197.4>

A complete walkthrough using the Apollo modeChoice data — counterfactual fares, value of time by income quintile, own- and cross-elasticities — is in the estimation tutorial on the docs site.

Roadmap

LCL is under active development. The estimator is stable and the results object covers the cases we encounter in our own work. Active work is on:

  • Model selection. Blocked K-fold cross-validation is included but still labelled experimental; expect refinements on highly unbalanced panels.
  • Documentation. A mathematical appendix and worked examples beyond Apollo's mode-choice data.
  • Companion paper. A working paper covering the econometric framework, hardware benchmarks, and Monte Carlo coverage tests.

Feature requests are welcome on the issue tracker.

Development

The project uses uv for dependency management.

git clone https://github.com/zeyveld/latent-class-conditional-logit.git
cd latent-class-conditional-logit
uv sync --all-extras --dev
uv run pytest tests/

Acknowledgments

LCL is built on JAX, Polars, equinox, jaxopt, jaxtyping, beartype, and formulaic. The differenced-design-matrix kernel at the heart of the conditional logit likelihood evaluation owes a particular debt to the xlogit package by Cristian Arteaga, JeeWoong Park, Prithvi Bhat Beeramoole, and Alexander Paz.

The documentation site is set in Luciole, a typeface designed for visually impaired readers by Laurent Bourcellier and Jonathan Perez in collaboration with the Centre Technique Régional pour la Déficience Visuelle and typographies.fr, released under CC-BY 4.0.

Citation

@software{lcl_2026,
  author = {Jeffries, Anna and Zeyveld, Andrew},
  title  = {LCL: Latent-Class Conditional Logit Estimation in Python},
  year   = {2026},
  url    = {https://github.com/zeyveld/latent-class-conditional-logit}
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lcl_choice-0.1.22.tar.gz (631.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lcl_choice-0.1.22-py3-none-any.whl (49.5 kB view details)

Uploaded Python 3

File details

Details for the file lcl_choice-0.1.22.tar.gz.

File metadata

  • Download URL: lcl_choice-0.1.22.tar.gz
  • Upload date:
  • Size: 631.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for lcl_choice-0.1.22.tar.gz
Algorithm Hash digest
SHA256 f9765e2c59f864d24e63e8647715bb1713af16fec540f664674313b62f70bad5
MD5 28b467672919f3355f377dd4648e4bfb
BLAKE2b-256 67beb092deebb0a616d65007f50fa335c5e3fbfc29567c33881207f34b15a986

See more details on using hashes here.

File details

Details for the file lcl_choice-0.1.22-py3-none-any.whl.

File metadata

  • Download URL: lcl_choice-0.1.22-py3-none-any.whl
  • Upload date:
  • Size: 49.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.9 {"installer":{"name":"uv","version":"0.9.9"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for lcl_choice-0.1.22-py3-none-any.whl
Algorithm Hash digest
SHA256 d511d06f6bc75b962e76bbb3948ee207e0ec1209caf6d3ade1a2177447dbd781
MD5 4e748d42fbb120b2e90919ecff3d393c
BLAKE2b-256 e89ddf8c1afa67aff28bd4dc99e9f4a4e40bc74bd773b59e992a1dfb67b933c3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page