Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@ pip install -e .
## Quick Start

```python
from multe import MultichoiceLogit, simulate_data
from multe import MultichoiceLogit, parse_choices, simulate_choices, simulate_data

# Generate synthetic data
# Matrix-first workflow
X, y_single, y_dual, true_beta = simulate_data(N=1000, J=4, K=3, seed=42)

# Fit model
model = MultichoiceLogit(num_alternatives=4, num_covariates=3)
model.fit(X, y_single, y_dual)
print(model.get_result().summary())

# Choices-first workflow (easiest entry point)
X2, choices, _ = simulate_choices(N=1000, J=4, K=3, seed=123)
model.fit_choices(X2, choices)

# Access fitted coefficients
print(model.coef_) # Shape: (J-1, K) = (3, 3)
# Access matrices if you need them
y_single2, y_dual2 = parse_choices(choices, J=4)
```

See `examples/simple_fit_example.py` for a complete example, or `examples/basic_example.py` for advanced usage.
See `examples/quickstart.py` for a ready-to-run script, `examples/simple_fit_example.py` for a complete example, or `examples/basic_example.py` for advanced usage.

## Model

Expand Down Expand Up @@ -91,8 +94,9 @@ The model fixes beta_0 = 0 for identification, so it estimates (J-1) × K parame
## Data Format

- **X**: Covariates (N, K)
- **Choices** (recommended): Length-N list of either `int` (single choice) or `(s, t)` tuples (dual choice). Convert to model-ready matrices with `parse_choices(choices, J)` or pass directly to `fit_choices`/`fit(..., choices=...)`.
- **y_single**: Binary matrix (N, J) where `y_single[i,j]=1` if agent i chose alternative j
- **y_dual**: Binary tensor (N, J, J) where `y_dual[i,s,t]=1` if agent i chose pair {s,t} with s<t
- **y_dual**: Binary tensor (N, J, J) where `y_dual[i,s,t]=1` if agent i chose pair {s,t} with s<t. Sparse CSR (shape N × J², row-major flattening) and tuple index inputs `(rows, s, t)` are also supported if you build them yourself.

Each agent must have exactly one choice (one entry in either y_single or y_dual).

Expand All @@ -112,18 +116,34 @@ Run benchmarks: `python examples/benchmark.py`

### MultichoiceLogit(num_alternatives, num_covariates)
Model class with methods:
- **`fit(X, y_single, y_dual, method='L-BFGS-B')`** - Fit model using MLE (recommended)
- Returns `self` with fitted `coef_` attribute
- Stores optimization details in `optimization_result_`
- `compute_standard_errors(X, y_single, y_dual, flat_beta=None)` - Standard errors (uses fitted params by default)
- `predict_proba(X, flat_beta=None)` - Single/dual choice probabilities
- `log_likelihood_contributions(X, y_single, y_dual, flat_beta=None)` - Per-observation log-likelihoods

### simulate_data(N, J, K, true_beta=None, mix_ratio=0.5, seed=42)
- **`fit(X, y_single=None, y_dual=None, choices=None, init_beta=None, method='L-BFGS-B', options=None, bounds=None, constraints=None, num_restarts=0, restart_scale=0.5, rng=None)`** – Fit via MLE. Provide either `choices` (ints/tuples) or both `y_single` and `y_dual`. Returns `self`, stores result in `optimization_result_`.
- `fit_choices(X, choices, **kwargs)` – Convenience wrapper for the choices-first workflow.
- `get_result(standard_errors=None)` – Returns a `ModelResult` snapshot with `summary()` for quick inspection.
- **`gradient(flat_beta, X, y_single, y_dual)`** – Public analytical gradient; accepts dense, sparse, or tuple dual inputs.
- `compute_standard_errors(X, y_single, y_dual, flat_beta=None, epsilon=None)` – Numerical Hessian SEs (uses fitted params by default).
- `predict_proba(X, flat_beta=None)` – Single/dual choice probabilities.
- `log_likelihood_contributions(X, y_single, y_dual, flat_beta=None)` – Per-observation log-likelihoods.

### parse_choices(choices, J)
Convert a list of choices (ints or `(s, t)` tuples) into `y_single, y_dual`.

### simulate_choices(N, J, K, true_beta=None, mix_ratio=0.5, seed=42, rng=None, dtype=np.float64)
Generate synthetic data and return `(X, choices, true_beta)` in the choices-first format.

### simulate_data(N, J, K, true_beta=None, mix_ratio=0.5, seed=42, rng=None, dtype=np.float64)
Generate synthetic data following the RUM framework.

Returns: `X, y_single, y_dual, true_beta`

## Interpreting the output

The inference table shows one row per alternative (`alt`) and covariate (`k`): the estimated coefficient, its standard error, z-score, and p-value.

Example (immigration attitudes):
- `alt1` = “less immigration”, `alt2` = “stay the same”, `alt3` = “more immigration”.
- `k0` = non-EU migrant share, `k1` = unemployment rate.
- A row `alt=1, k=0, coef=-0.26` means higher non-EU share is associated with lower likelihood of choosing “less immigration”. Each row reads the same way for every attitude option and predictor.

## Acknowledgements

Many thanks to [Alan Manning](https://www.alan-manning.com/) for his guidance and support with this project.
2 changes: 1 addition & 1 deletion examples/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def benchmark_estimation(N, J, K, mix_ratio=0.5, method="BFGS", seed=42):
# Compute standard errors
t0 = time.time()
# Compute standard errors (runtime tracked, values unused in benchmark output)
_ = model.compute_standard_errors(result.x, X, y_single, y_dual)
_ = model.compute_standard_errors(X, y_single, y_dual, flat_beta=result.x)
se_time = time.time() - t0

return {
Expand Down
218 changes: 0 additions & 218 deletions examples/csv_example.py

This file was deleted.

29 changes: 29 additions & 0 deletions examples/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Minimal quickstart for Multe using the choices-first workflow."""

from __future__ import annotations

import numpy as np

from multe import MultichoiceLogit, parse_choices, simulate_choices


def main() -> None:
# Simulate data and get choices list directly
X, choices, true_beta = simulate_choices(N=10000, J=4, K=2, seed=42)

# Fit with the convenience wrapper
model = MultichoiceLogit(num_alternatives=4, num_covariates=2)
model.fit_choices(X, choices)

# Optionally compute standard errors
y_single, y_dual = parse_choices(choices, J=4)
std_errs = model.compute_standard_errors(X, y_single, y_dual)

# Report
result = model.get_result(standard_errors=std_errs)
result.summary(verbose=True)
print("\nTrue parameters (free):\n", np.array2string(true_beta, precision=4))


if __name__ == "__main__":
main()
4 changes: 3 additions & 1 deletion multe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"""

from .model import MultichoiceLogit
from .simulate import simulate_data
from .simulate import parse_choices, simulate_choices, simulate_data

__all__ = [
"MultichoiceLogit",
"parse_choices",
"simulate_choices",
"simulate_data",
]
Loading