Skill Tracer
Skills › Scientific Agent Skills › statsmodels › discrete_choice.md

discrete_choice.md

660 lines · skills/statsmodels/references/discrete_choice.md at 5f2bc03

Open on GitHub
1# Discrete Choice Models Reference
2
3This document provides comprehensive guidance on discrete choice models in statsmodels, including binary, multinomial, count, and ordinal models.
4

Discrete Choice Models Reference

This document provides comprehensive guidance on discrete choice models in statsmodels, including binary, multinomial, count, and ordinal models.

Raw lines 1 to 4
5## Overview
6
7Discrete choice models handle outcomes that are:
8- **Binary**: 0/1, success/failure
9- **Multinomial**: Multiple unordered categories
10- **Ordinal**: Ordered categories
11- **Count**: Non-negative integers
13These models use likelihood-based estimation. Independence and sampling assumptions are model-specific: conditional models condition out group intercepts, while clustered sampling needs an appropriate covariance/design. Check finite estimates, design rank, convergence and separation; convergence alone does not establish identifiability.

Overview

Discrete choice models handle outcomes that are:

  • Binary: 0/1, success/failure
  • Multinomial: Multiple unordered categories
  • Ordinal: Ordered categories
  • Count: Non-negative integers

These models use likelihood-based estimation. Independence and sampling assumptions are model-specific: conditional models condition out group intercepts, while clustered sampling needs an appropriate covariance/design. Check finite estimates, design rank, convergence and separation; convergence alone does not establish identifiability.

Raw lines 5 to 14
15## Binary Models

Binary Models

Raw lines 15 to 16
17### Logit (Logistic Regression)
19Uses logistic distribution for binary outcomes.
21**When to use:**
22- Binary classification (yes/no, success/failure)
23- Probability estimation for binary outcomes
24- Interpretable odds ratios
26**Model**: P(Y=1|X) = 1 / (1 + exp(-Xβ))
28```python
29import statsmodels.api as sm
30from statsmodels.discrete.discrete_model import Logit
32# Prepare data
33X = sm.add_constant(X_data)
35# Fit model
36model = Logit(y, X)
37results = model.fit()
39print(results.summary())
40```
42**Interpretation:**
43```python
44import numpy as np
46# Odds ratios
47odds_ratios = np.exp(results.params)
48print("Odds ratios:", odds_ratios)
50# For 1-unit increase in X, odds multiply by exp(β)
51# OR > 1: increases odds of success
52# OR < 1: decreases odds of success
53# OR = 1: no effect
55# Confidence intervals for odds ratios
56odds_ci = np.exp(results.conf_int())
57print("Odds ratio 95% CI:")
58print(odds_ci)
59```
61**Marginal effects:**
62```python
63# Average marginal effects (AME)
64marginal_effects = results.get_margeff(at='overall')
65print(marginal_effects.summary())
67# Marginal effects at means (MEM)
68marginal_effects_mem = results.get_margeff(at='mean', method='dydx')
70# Marginal effects at representative values
71marginal_effects_custom = results.get_margeff(at='mean',
72 atexog={1: 1, 2: 5}) # Zero-based design-column indices
73```
75**Predictions:**
76```python
77# Predicted probabilities
78probs = results.predict(X)
80# Binary predictions (0.5 threshold)
81predictions = (probs > 0.5).astype(int)
83# Custom threshold
84threshold = 0.3
85predictions_custom = (probs > threshold).astype(int)
87# For new data
88X_new = sm.add_constant(X_new_data, has_constant="add")
89new_probs = results.predict(X_new)
90```
92**Model evaluation:**
93```python
94from sklearn.metrics import (classification_report, confusion_matrix,
95 roc_auc_score, roc_curve)
97# Classification report
98print(classification_report(y, predictions))
100# Confusion matrix
101print(confusion_matrix(y, predictions))
103# AUC-ROC
104auc = roc_auc_score(y, probs)
105print(f"AUC: {auc:.4f}")
107# Pseudo R-squared
108print(f"McFadden's Pseudo R²: {results.prsquared:.4f}")
109```

Logit (Logistic Regression)

Uses logistic distribution for binary outcomes.

When to use:

  • Binary classification (yes/no, success/failure)
  • Probability estimation for binary outcomes
  • Interpretable odds ratios

Model: P(Y=1|X) = 1 / (1 + exp(-Xβ))

import statsmodels.api as sm
from statsmodels.discrete.discrete_model import Logit

# Prepare data
X = sm.add_constant(X_data)

# Fit model
model = Logit(y, X)
results = model.fit()

print(results.summary())

Interpretation:

import numpy as np

# Odds ratios
odds_ratios = np.exp(results.params)
print("Odds ratios:", odds_ratios)

# For 1-unit increase in X, odds multiply by exp(β)
# OR > 1: increases odds of success
# OR < 1: decreases odds of success
# OR = 1: no effect

# Confidence intervals for odds ratios
odds_ci = np.exp(results.conf_int())
print("Odds ratio 95% CI:")
print(odds_ci)

Marginal effects:

# Average marginal effects (AME)
marginal_effects = results.get_margeff(at='overall')
print(marginal_effects.summary())

# Marginal effects at means (MEM)
marginal_effects_mem = results.get_margeff(at='mean', method='dydx')

# Marginal effects at representative values
marginal_effects_custom = results.get_margeff(at='mean',
                                              atexog={1: 1, 2: 5})  # Zero-based design-column indices

Predictions:

# Predicted probabilities
probs = results.predict(X)

# Binary predictions (0.5 threshold)
predictions = (probs > 0.5).astype(int)

# Custom threshold
threshold = 0.3
predictions_custom = (probs > threshold).astype(int)

# For new data
X_new = sm.add_constant(X_new_data, has_constant="add")
new_probs = results.predict(X_new)

Model evaluation:

from sklearn.metrics import (classification_report, confusion_matrix,
                             roc_auc_score, roc_curve)

# Classification report
print(classification_report(y, predictions))

# Confusion matrix
print(confusion_matrix(y, predictions))

# AUC-ROC
auc = roc_auc_score(y, probs)
print(f"AUC: {auc:.4f}")

# Pseudo R-squared
print(f"McFadden's Pseudo R²: {results.prsquared:.4f}")
Raw lines 17 to 110
111### Probit
113Uses normal distribution for binary outcomes.
115**When to use:**
116- Binary outcomes
117- Prefer normal distribution assumption
118- Field convention (econometrics often uses probit)
120**Model**: P(Y=1|X) = Φ(Xβ), where Φ is standard normal CDF
122```python
123from statsmodels.discrete.discrete_model import Probit
125model = Probit(y, X)
126results = model.fit()
128print(results.summary())
129```
131**Comparison with Logit:**
132- Probit and Logit usually give similar results
133- Probit: symmetric, based on normal distribution
134- Logit: slightly heavier tails, easier interpretation (odds ratios)
135- Coefficients not directly comparable (scale difference)
137```python
138# Marginal effects are comparable
139logit_me = logit_results.get_margeff().margeff
140probit_me = probit_results.get_margeff().margeff
142print("Logit marginal effects:", logit_me)
143print("Probit marginal effects:", probit_me)
144```

Probit

Uses normal distribution for binary outcomes.

When to use:

  • Binary outcomes
  • Prefer normal distribution assumption
  • Field convention (econometrics often uses probit)

Model: P(Y=1|X) = Φ(Xβ), where Φ is standard normal CDF

from statsmodels.discrete.discrete_model import Probit

model = Probit(y, X)
results = model.fit()

print(results.summary())

Comparison with Logit:

  • Probit and Logit usually give similar results
  • Probit: symmetric, based on normal distribution
  • Logit: slightly heavier tails, easier interpretation (odds ratios)
  • Coefficients not directly comparable (scale difference)
# Marginal effects are comparable
logit_me = logit_results.get_margeff().margeff
probit_me = probit_results.get_margeff().margeff

print("Logit marginal effects:", logit_me)
print("Probit marginal effects:", probit_me)
Raw lines 111 to 145
146## Multinomial Models

Multinomial Models

Raw lines 146 to 147
148### MNLogit (Multinomial Logit)
150For unordered categorical outcomes with 3+ categories.
152**When to use:**
153- Multiple unordered categories (e.g., transportation mode, brand choice)
154- No natural ordering among categories
155- Need probabilities for each category
157**Model**: P(Y=j|X) = exp(Xβⱼ) / Σₖ exp(Xβₖ)
159```python
160from statsmodels.discrete.discrete_model import MNLogit
162# y should be integers 0, 1, 2, ... for categories
163model = MNLogit(y, X)
164results = model.fit()
166print(results.summary())
167```
169**Interpretation:**
170```python
171# One category is reference (usually category 0)
172# Coefficients represent log-odds relative to reference
174# For category j vs reference:
175# exp(β_j) = odds ratio of category j vs reference
177# Predicted probabilities for each category
178probs = results.predict(X) # Shape: (n_samples, n_categories)
180# Most likely category
181predicted_categories = np.asarray(probs).argmax(axis=1)
182```
184**Relative risk ratios:**
185```python
186# Exponentiate coefficients for relative risk ratios
187import numpy as np
188import pandas as pd
190# Get parameter names and values
191params_df = pd.concat({
192 'coef': pd.DataFrame(results.params),
193 'RRR': pd.DataFrame(np.exp(results.params))
194}, axis=1)
195print(params_df)
196```

MNLogit (Multinomial Logit)

For unordered categorical outcomes with 3+ categories.

When to use:

  • Multiple unordered categories (e.g., transportation mode, brand choice)
  • No natural ordering among categories
  • Need probabilities for each category

Model: P(Y=j|X) = exp(Xβⱼ) / Σₖ exp(Xβₖ)

from statsmodels.discrete.discrete_model import MNLogit

# y should be integers 0, 1, 2, ... for categories
model = MNLogit(y, X)
results = model.fit()

print(results.summary())

Interpretation:

# One category is reference (usually category 0)
# Coefficients represent log-odds relative to reference

# For category j vs reference:
# exp(β_j) = odds ratio of category j vs reference

# Predicted probabilities for each category
probs = results.predict(X)  # Shape: (n_samples, n_categories)

# Most likely category
predicted_categories = np.asarray(probs).argmax(axis=1)

Relative risk ratios:

# Exponentiate coefficients for relative risk ratios
import numpy as np
import pandas as pd

# Get parameter names and values
params_df = pd.concat({
    'coef': pd.DataFrame(results.params),
    'RRR': pd.DataFrame(np.exp(results.params))
}, axis=1)
print(params_df)
Raw lines 148 to 197
198### Conditional Logit
200For choice models where alternatives have characteristics.
202**When to use:**
203- Alternative-specific regressors (vary across choices)
204- Panel data with choices
205- Discrete choice experiments
207```python
208from statsmodels.discrete.conditional_models import ConditionalLogit
210# Long format, binary choice indicator, no constant in X_alternatives.
211# One group per choice occasion (not one person across multiple occasions).
212model = ConditionalLogit(y_choice, X_alternatives, groups=choice_set_id)
213results = model.fit()
214```

Conditional Logit

For choice models where alternatives have characteristics.

When to use:

  • Alternative-specific regressors (vary across choices)
  • Panel data with choices
  • Discrete choice experiments
from statsmodels.discrete.conditional_models import ConditionalLogit

# Long format, binary choice indicator, no constant in X_alternatives.
# One group per choice occasion (not one person across multiple occasions).
model = ConditionalLogit(y_choice, X_alternatives, groups=choice_set_id)
results = model.fit()
Raw lines 198 to 215
216## Count Models
218### Poisson
220Standard model for count data.
222**When to use:**
223- Count outcomes (events, occurrences)
224- Rare events
225- Mean ≈ variance
227**Model**: P(Y=k|X) = exp(-λ) λᵏ / k!, where log(λ) = Xβ
229```python
230from statsmodels.discrete.discrete_model import Poisson
232model = Poisson(y_counts, X)
233results = model.fit()
235print(results.summary())
236```
238**Interpretation:**
239```python
240# Rate ratios (incident rate ratios)
241rate_ratios = np.exp(results.params)
242print("Rate ratios:", rate_ratios)
244# For 1-unit increase in X, expected count multiplies by exp(β)
245```
247**Check overdispersion:**
248```python
249# Dispersion concerns the conditional variance, not raw variance/mean.
250mu = results.predict(X)
251pearson_dispersion = np.sum((y_counts - mu)**2 / mu) / results.df_resid
252print(f"Pearson dispersion: {pearson_dispersion:.2f}")
253# A screening statistic: inspect mean misspecification, exposure and dependence.
254```
256**With offset (for rates):**
257```python
258# When modeling rates with varying exposure
259# log(λ) = log(exposure) + Xβ
261model = Poisson(y_counts, X, offset=np.log(exposure))
262results = model.fit()
263```

Poisson

Standard model for count data.

When to use:

  • Count outcomes (events, occurrences)
  • Rare events
  • Mean ≈ variance

Model: P(Y=k|X) = exp(-λ) λᵏ / k!, where log(λ) = Xβ

from statsmodels.discrete.discrete_model import Poisson

model = Poisson(y_counts, X)
results = model.fit()

print(results.summary())

Interpretation:

# Rate ratios (incident rate ratios)
rate_ratios = np.exp(results.params)
print("Rate ratios:", rate_ratios)

# For 1-unit increase in X, expected count multiplies by exp(β)

Check overdispersion:

# Dispersion concerns the conditional variance, not raw variance/mean.
mu = results.predict(X)
pearson_dispersion = np.sum((y_counts - mu)**2 / mu) / results.df_resid
print(f"Pearson dispersion: {pearson_dispersion:.2f}")
# A screening statistic: inspect mean misspecification, exposure and dependence.

With offset (for rates):

# When modeling rates with varying exposure
# log(λ) = log(exposure) + Xβ

model = Poisson(y_counts, X, offset=np.log(exposure))
results = model.fit()
Raw lines 218 to 264
265### Negative Binomial
267For overdispersed count data (variance > mean).
269**When to use:**
270- Count data with overdispersion
271- Excess variance not explained by Poisson
272- Heterogeneity in counts
274**Model**: Adds dispersion parameter α to account for overdispersion
276```python
277from statsmodels.discrete.discrete_model import NegativeBinomial
279model = NegativeBinomial(y_counts, X)
280results = model.fit()
282print(results.summary())
283print(f"Dispersion parameter alpha: {np.asarray(results.params)[-1]:.4f}")
284```
286**Compare with Poisson:**
287```python
288# Fit both models
289poisson_results = Poisson(y_counts, X).fit()
290nb_results = NegativeBinomial(y_counts, X).fit()
292# AIC comparison (lower is better)
293print(f"Poisson AIC: {poisson_results.aic:.2f}")
294print(f"Negative Binomial AIC: {nb_results.aic:.2f}")
296# Alpha=0 is a boundary under the Poisson null. A naive chi-square(1)
297# likelihood-ratio p-value is not the ordinary regular nested-model test.
298# Use a justified boundary reference distribution or parametric bootstrap.
299lr_stat = 2 * (nb_results.llf - poisson_results.llf)
300print("Boundary LR statistic (no naive p-value):", lr_stat)
301```

Negative Binomial

For overdispersed count data (variance > mean).

When to use:

  • Count data with overdispersion
  • Excess variance not explained by Poisson
  • Heterogeneity in counts

Model: Adds dispersion parameter α to account for overdispersion

from statsmodels.discrete.discrete_model import NegativeBinomial

model = NegativeBinomial(y_counts, X)
results = model.fit()

print(results.summary())
print(f"Dispersion parameter alpha: {np.asarray(results.params)[-1]:.4f}")

Compare with Poisson:

# Fit both models
poisson_results = Poisson(y_counts, X).fit()
nb_results = NegativeBinomial(y_counts, X).fit()

# AIC comparison (lower is better)
print(f"Poisson AIC: {poisson_results.aic:.2f}")
print(f"Negative Binomial AIC: {nb_results.aic:.2f}")

# Alpha=0 is a boundary under the Poisson null. A naive chi-square(1)
# likelihood-ratio p-value is not the ordinary regular nested-model test.
# Use a justified boundary reference distribution or parametric bootstrap.
lr_stat = 2 * (nb_results.llf - poisson_results.llf)
print("Boundary LR statistic (no naive p-value):", lr_stat)
Raw lines 265 to 302
303### Zero-Inflated Models
305For count data with excess zeros.
307**When to use:**
308- More zeros than expected from Poisson/NB
309- Two processes: one for zeros, one for counts
310- Examples: number of doctor visits, insurance claims
312**Models:**
313- ZeroInflatedPoisson (ZIP)
314- ZeroInflatedNegativeBinomialP (ZINB)
316```python
317from statsmodels.discrete.count_model import (ZeroInflatedPoisson,
318 ZeroInflatedNegativeBinomialP)
320# ZIP model
321zip_model = ZeroInflatedPoisson(y_counts, X, exog_infl=X_inflation)
322zip_results = zip_model.fit()
324# ZINB model (for overdispersion + excess zeros)
325zinb_model = ZeroInflatedNegativeBinomialP(y_counts, X, exog_infl=X_inflation)
326zinb_results = zinb_model.fit()
328print(zip_results.summary())
329```
331**Two parts of the model:**
332```python
333# 1. Inflation model: P(Y=0 due to inflation)
334# 2. Count model: distribution of counts
336# Predicted probabilities of inflation
337inflation_probs = 1 - zip_results.predict(
338 X, exog_infl=X_inflation, which='prob-main')
339zero_probs = zip_results.predict(X, exog_infl=X_inflation, which='prob-zero')
340pmf = zip_results.predict(X, exog_infl=X_inflation, which='prob')
342# Predicted counts
343predicted_counts = zip_results.predict(X, exog_infl=X_inflation, which='mean')
344```

Zero-Inflated Models

For count data with excess zeros.

When to use:

  • More zeros than expected from Poisson/NB
  • Two processes: one for zeros, one for counts
  • Examples: number of doctor visits, insurance claims

Models:

  • ZeroInflatedPoisson (ZIP)
  • ZeroInflatedNegativeBinomialP (ZINB)
from statsmodels.discrete.count_model import (ZeroInflatedPoisson,
                                               ZeroInflatedNegativeBinomialP)

# ZIP model
zip_model = ZeroInflatedPoisson(y_counts, X, exog_infl=X_inflation)
zip_results = zip_model.fit()

# ZINB model (for overdispersion + excess zeros)
zinb_model = ZeroInflatedNegativeBinomialP(y_counts, X, exog_infl=X_inflation)
zinb_results = zinb_model.fit()

print(zip_results.summary())

Two parts of the model:

# 1. Inflation model: P(Y=0 due to inflation)
# 2. Count model: distribution of counts

# Predicted probabilities of inflation
inflation_probs = 1 - zip_results.predict(
    X, exog_infl=X_inflation, which='prob-main')
zero_probs = zip_results.predict(X, exog_infl=X_inflation, which='prob-zero')
pmf = zip_results.predict(X, exog_infl=X_inflation, which='prob')

# Predicted counts
predicted_counts = zip_results.predict(X, exog_infl=X_inflation, which='mean')
Raw lines 303 to 345
346### Hurdle Models
348Two-stage model: whether any counts, then how many.
350**When to use:**
351- Excess zeros
352- Different processes for zero vs positive counts
353- Zeros structurally different from positive values
355```python
356from statsmodels.discrete.truncated_model import HurdleCountModel
358# Poisson-Poisson hurdle model; use `zerodist` for the zero hurdle process
359model = HurdleCountModel(y_counts, X,
360 dist='poisson',
361 zerodist='poisson')
362results = model.fit()
364print(results.summary())
365```

Hurdle Models

Two-stage model: whether any counts, then how many.

When to use:

  • Excess zeros
  • Different processes for zero vs positive counts
  • Zeros structurally different from positive values
from statsmodels.discrete.truncated_model import HurdleCountModel

# Poisson-Poisson hurdle model; use `zerodist` for the zero hurdle process
model = HurdleCountModel(y_counts, X,
                         dist='poisson',
                         zerodist='poisson')
results = model.fit()

print(results.summary())
Raw lines 346 to 366
367## Ordinal Models

Ordinal Models

Raw lines 367 to 368
369### Ordered Logit/Probit
371For ordered categorical outcomes.
373**When to use:**
374- Ordered categories (e.g., low/medium/high, ratings 1-5)
375- Natural ordering matters
376- Want to respect ordinal structure
378**Model**: Cumulative probability model with cutpoints
380```python
381from statsmodels.miscmodels.ordinal_model import OrderedModel
383# Ordered integers or an ordered pandas categorical; X_data has NO constant
384model = OrderedModel(y_ordered, X_data, distr='logit') # or 'probit'
385results = model.fit(method='bfgs')
387print(results.summary())
388```
390**Interpretation:**
391```python
392# Cutpoints (thresholds between categories)
393cutpoints = results.model.transform_threshold_params(results.params)[1:-1]
394print("Cutpoints:", cutpoints)
396# Coefficients
397coefficients = np.asarray(results.params)[:results.model.k_vars]
398print("Coefficients:", coefficients)
400# Predicted probabilities for each category
401probs = results.predict(X_data) # Shape: (n_samples, n_categories)
403# Most likely category
404predicted_categories = np.asarray(probs).argmax(axis=1)
405```
407**Proportional odds assumption:**
408```python
409# Separate binary fits can be exploratory, but comparing coefficients visually
410# is not a calibrated Brant test. A formal proportional-odds assessment needs
411# the covariance between cutpoint-specific estimates.
412```
414The hurdle model uses the same exog for both processes; `zerodist` is a censored count distribution, not a logit keyword. Check identification of NB zero-process parameters. OrderedModel reports transformed increments after its first threshold; only `transform_threshold_params` gives actual cutpoints.

Ordered Logit/Probit

For ordered categorical outcomes.

When to use:

  • Ordered categories (e.g., low/medium/high, ratings 1-5)
  • Natural ordering matters
  • Want to respect ordinal structure

Model: Cumulative probability model with cutpoints

from statsmodels.miscmodels.ordinal_model import OrderedModel

# Ordered integers or an ordered pandas categorical; X_data has NO constant
model = OrderedModel(y_ordered, X_data, distr='logit')  # or 'probit'
results = model.fit(method='bfgs')

print(results.summary())

Interpretation:

# Cutpoints (thresholds between categories)
cutpoints = results.model.transform_threshold_params(results.params)[1:-1]
print("Cutpoints:", cutpoints)

# Coefficients
coefficients = np.asarray(results.params)[:results.model.k_vars]
print("Coefficients:", coefficients)

# Predicted probabilities for each category
probs = results.predict(X_data)  # Shape: (n_samples, n_categories)

# Most likely category
predicted_categories = np.asarray(probs).argmax(axis=1)

Proportional odds assumption:

# Separate binary fits can be exploratory, but comparing coefficients visually
# is not a calibrated Brant test. A formal proportional-odds assessment needs
# the covariance between cutpoint-specific estimates.

The hurdle model uses the same exog for both processes; zerodist is a censored count distribution, not a logit keyword. Check identification of NB zero-process parameters. OrderedModel reports transformed increments after its first threshold; only transform_threshold_params gives actual cutpoints.

Raw lines 369 to 415
416## Model Diagnostics
418Select the matching result type for each fragment; pseudo-R²/null-likelihood and marginal-effect helpers are not universal across every count/ordinal result. Binary metrics below are in-sample diagnostics unless X/y are replaced by held-out data. Thresholds and tuning must be chosen without the test set.

Model Diagnostics

Select the matching result type for each fragment; pseudo-R²/null-likelihood and marginal-effect helpers are not universal across every count/ordinal result. Binary metrics below are in-sample diagnostics unless X/y are replaced by held-out data. Thresholds and tuning must be chosen without the test set.

Raw lines 416 to 419
420### Goodness of Fit
422```python
423# Pseudo R-squared (McFadden)
424print(f"Pseudo R²: {results.prsquared:.4f}")
426# AIC/BIC for model comparison
427print(f"AIC: {results.aic:.2f}")
428print(f"BIC: {results.bic:.2f}")
430# Log-likelihood
431print(f"Log-likelihood: {results.llf:.2f}")
433# Likelihood ratio test vs null model
434lr_stat = 2 * (results.llf - results.llnull)
435from scipy import stats
436lr_pval = stats.chi2.sf(lr_stat, results.df_model)
437print(f"LR test p-value: {lr_pval}")
438```

Goodness of Fit

# Pseudo R-squared (McFadden)
print(f"Pseudo R²: {results.prsquared:.4f}")

# AIC/BIC for model comparison
print(f"AIC: {results.aic:.2f}")
print(f"BIC: {results.bic:.2f}")

# Log-likelihood
print(f"Log-likelihood: {results.llf:.2f}")

# Likelihood ratio test vs null model
lr_stat = 2 * (results.llf - results.llnull)
from scipy import stats
lr_pval = stats.chi2.sf(lr_stat, results.df_model)
print(f"LR test p-value: {lr_pval}")
Raw lines 420 to 439
440### Classification Metrics (Binary)
442```python
443from sklearn.metrics import (accuracy_score, precision_score, recall_score,
444 f1_score, roc_auc_score)
446# Predictions
447probs = results.predict(X)
448predictions = (probs > 0.5).astype(int)
450# Metrics
451print(f"Accuracy: {accuracy_score(y, predictions):.4f}")
452print(f"Precision: {precision_score(y, predictions):.4f}")
453print(f"Recall: {recall_score(y, predictions):.4f}")
454print(f"F1: {f1_score(y, predictions):.4f}")
455print(f"AUC: {roc_auc_score(y, probs):.4f}")
456```

Classification Metrics (Binary)

from sklearn.metrics import (accuracy_score, precision_score, recall_score,
                             f1_score, roc_auc_score)

# Predictions
probs = results.predict(X)
predictions = (probs > 0.5).astype(int)

# Metrics
print(f"Accuracy: {accuracy_score(y, predictions):.4f}")
print(f"Precision: {precision_score(y, predictions):.4f}")
print(f"Recall: {recall_score(y, predictions):.4f}")
print(f"F1: {f1_score(y, predictions):.4f}")
print(f"AUC: {roc_auc_score(y, probs):.4f}")
Raw lines 440 to 457
458### Classification Metrics (Multinomial)
460```python
461from sklearn.metrics import accuracy_score, classification_report, log_loss
463# Predicted categories
464probs = results.predict(X)
465predictions = np.asarray(probs).argmax(axis=1)
467# Accuracy
468accuracy = accuracy_score(y, predictions)
469print(f"Accuracy: {accuracy:.4f}")
471# Classification report
472print(classification_report(y, predictions))
474# Log loss
475logloss = log_loss(y, probs)
476print(f"Log Loss: {logloss:.4f}")
477```

Classification Metrics (Multinomial)

from sklearn.metrics import accuracy_score, classification_report, log_loss

# Predicted categories
probs = results.predict(X)
predictions = np.asarray(probs).argmax(axis=1)

# Accuracy
accuracy = accuracy_score(y, predictions)
print(f"Accuracy: {accuracy:.4f}")

# Classification report
print(classification_report(y, predictions))

# Log loss
logloss = log_loss(y, probs)
print(f"Log Loss: {logloss:.4f}")
Raw lines 458 to 478
479### Count Model Diagnostics
481```python
482# Expected frequencies require probabilities, not rounded conditional means.
483# This example is specifically a fitted ordinary Poisson result.
484values = np.arange(int(np.max(y_counts)) + 1)
485pmf = results.predict(X, which="prob", y_values=values)
486expected = pmf.sum(axis=0)
487observed = np.bincount(np.asarray(y_counts, dtype=int), minlength=len(values))
488import matplotlib.pyplot as plt
489plt.plot(values, observed, label="Observed")
490plt.plot(values, expected, label="Model expected")
491plt.legend()
492plt.show()
493# Include/report the model tail beyond values[-1] when assessing total mass.
494```

Count Model Diagnostics

# Expected frequencies require probabilities, not rounded conditional means.
# This example is specifically a fitted ordinary Poisson result.
values = np.arange(int(np.max(y_counts)) + 1)
pmf = results.predict(X, which="prob", y_values=values)
expected = pmf.sum(axis=0)
observed = np.bincount(np.asarray(y_counts, dtype=int), minlength=len(values))
import matplotlib.pyplot as plt
plt.plot(values, observed, label="Observed")
plt.plot(values, expected, label="Model expected")
plt.legend()
plt.show()
# Include/report the model tail beyond values[-1] when assessing total mass.
Raw lines 479 to 495
496### Influence and Outliers
498```python
499# Pearson residuals for an ordinary Poisson model only.
500mu = results.predict(X)
501std_resid = (y_counts - mu) / np.sqrt(mu)
503# Check for outliers (|std_resid| > 2)
504outliers = np.where(np.abs(std_resid) > 2)[0]
505print(f"Number of outliers: {len(outliers)}")
507# Leverage (hat values) - for logit/probit
508# from statsmodels.stats.outliers_influence
509```

Influence and Outliers

# Pearson residuals for an ordinary Poisson model only.
mu = results.predict(X)
std_resid = (y_counts - mu) / np.sqrt(mu)

# Check for outliers (|std_resid| > 2)
outliers = np.where(np.abs(std_resid) > 2)[0]
print(f"Number of outliers: {len(outliers)}")

# Leverage (hat values) - for logit/probit
# from statsmodels.stats.outliers_influence
Raw lines 496 to 510
511## Hypothesis Testing
513```python
514# Single parameter test (automatic in summary)
516# Multiple parameters: Wald test
517# Test H0: β₁ = β₂ = 0
518R = [[0, 1, 0, 0], [0, 0, 1, 0]]
519wald_test = results.wald_test(R)
520print(wald_test)
522# Likelihood ratio test for nested models
523model_reduced = Logit(y, X_reduced).fit()
524model_full = Logit(y, X_full).fit()
526lr_stat = 2 * (model_full.llf - model_reduced.llf)
527df_diff = model_full.df_model - model_reduced.df_model
528from scipy import stats
529lr_pval = stats.chi2.sf(lr_stat, df_diff)
530print(f"LR test p-value: {lr_pval:.4f}")
531```

Hypothesis Testing

# Single parameter test (automatic in summary)

# Multiple parameters: Wald test
# Test H0: β₁ = β₂ = 0
R = [[0, 1, 0, 0], [0, 0, 1, 0]]
wald_test = results.wald_test(R)
print(wald_test)

# Likelihood ratio test for nested models
model_reduced = Logit(y, X_reduced).fit()
model_full = Logit(y, X_full).fit()

lr_stat = 2 * (model_full.llf - model_reduced.llf)
df_diff = model_full.df_model - model_reduced.df_model
from scipy import stats
lr_pval = stats.chi2.sf(lr_stat, df_diff)
print(f"LR test p-value: {lr_pval:.4f}")
Raw lines 511 to 532
533## Model Selection and Comparison
535```python
536# Fit multiple models
537models = {
538 'Logit': Logit(y, X).fit(),
539 'Probit': Probit(y, X).fit(),
540 # Add more models
543# Compare AIC/BIC
544comparison = pd.DataFrame({
545 'AIC': {name: model.aic for name, model in models.items()},
546 'BIC': {name: model.bic for name, model in models.items()},
547 'Pseudo R²': {name: model.prsquared for name, model in models.items()}
548})
549print(comparison.sort_values('AIC'))
551# Cross-validation for predictive performance
552from sklearn.model_selection import cross_val_score
553from sklearn.linear_model import LogisticRegression
555# Use sklearn wrapper or manual CV
556```

Model Selection and Comparison

# Fit multiple models
models = {
    'Logit': Logit(y, X).fit(),
    'Probit': Probit(y, X).fit(),
    # Add more models
}

# Compare AIC/BIC
comparison = pd.DataFrame({
    'AIC': {name: model.aic for name, model in models.items()},
    'BIC': {name: model.bic for name, model in models.items()},
    'Pseudo R²': {name: model.prsquared for name, model in models.items()}
})
print(comparison.sort_values('AIC'))

# Cross-validation for predictive performance
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression

# Use sklearn wrapper or manual CV
Raw lines 533 to 557
558## Formula API
560Use R-style formulas for easier specification.
562```python
563import statsmodels.formula.api as smf
565# Logit with formula
566formula = 'y ~ x1 + x2 + C(category) + x1:x2'
567results = smf.logit(formula, data=df).fit()
569# MNLogit with formula
570results = smf.mnlogit('choice ~ x1 + x2', data=df).fit() # Numeric category codes
572# Poisson with formula
573results = smf.poisson('count ~ x1 + x2', data=df).fit()
575# Negative Binomial with formula
576results = smf.negativebinomial('count ~ x1 + x2', data=df).fit()
577```

Formula API

Use R-style formulas for easier specification.

import statsmodels.formula.api as smf

# Logit with formula
formula = 'y ~ x1 + x2 + C(category) + x1:x2'
results = smf.logit(formula, data=df).fit()

# MNLogit with formula
results = smf.mnlogit('choice ~ x1 + x2', data=df).fit()  # Numeric category codes

# Poisson with formula
results = smf.poisson('count ~ x1 + x2', data=df).fit()

# Negative Binomial with formula
results = smf.negativebinomial('count ~ x1 + x2', data=df).fit()
Raw lines 558 to 578
579## Common Applications

Common Applications

Raw lines 579 to 580
581### Binary Classification (Marketing Response)
583```python
584# Predict customer purchase probability
585X = sm.add_constant(customer_features)
586model = Logit(purchased, X)
587results = model.fit()
589# Targeting: select top 20% likely to purchase
590probs = results.predict(X)
591top_20_pct_idx = np.argsort(probs)[-int(0.2*len(probs)):]
592```

Binary Classification (Marketing Response)

# Predict customer purchase probability
X = sm.add_constant(customer_features)
model = Logit(purchased, X)
results = model.fit()

# Targeting: select top 20% likely to purchase
probs = results.predict(X)
top_20_pct_idx = np.argsort(probs)[-int(0.2*len(probs)):]
Raw lines 581 to 593
594### Multinomial Choice (Transportation Mode)
596```python
597# Predict transportation mode choice
598model = MNLogit(mode_choice, X)
599results = model.fit()
601# Predicted mode for new commuter
602new_commuter = sm.add_constant(new_features, has_constant="add")
603mode_probs = results.predict(new_commuter)
604predicted_mode = np.asarray(mode_probs).argmax(axis=1)
605```

Multinomial Choice (Transportation Mode)

# Predict transportation mode choice
model = MNLogit(mode_choice, X)
results = model.fit()

# Predicted mode for new commuter
new_commuter = sm.add_constant(new_features, has_constant="add")
mode_probs = results.predict(new_commuter)
predicted_mode = np.asarray(mode_probs).argmax(axis=1)
Raw lines 594 to 606
607### Count Data (Number of Doctor Visits)
609```python
610# Model healthcare utilization
611model = NegativeBinomial(num_visits, X)
612results = model.fit()
614# Expected visits for new patient
615expected_visits = results.predict(new_patient_X)
616```

Count Data (Number of Doctor Visits)

# Model healthcare utilization
model = NegativeBinomial(num_visits, X)
results = model.fit()

# Expected visits for new patient
expected_visits = results.predict(new_patient_X)
Raw lines 607 to 617
618### Zero-Inflated (Insurance Claims)
620```python
621# Many people have zero claims
622# Zero-inflation: some never claim
623# Count process: those who might claim
625zip_model = ZeroInflatedPoisson(claims, X_count, exog_infl=X_inflation)
626results = zip_model.fit()
628# Fitted structural-zero mixture probability; not a permanent individual trait
629structural_zero_prob = 1 - results.predict(
630 X_count, exog_infl=X_inflation, which='prob-main')
632# Expected claims
633expected_claims = results.predict(X_count, exog_infl=X_inflation, which='mean')
634```

Zero-Inflated (Insurance Claims)

# Many people have zero claims
# Zero-inflation: some never claim
# Count process: those who might claim

zip_model = ZeroInflatedPoisson(claims, X_count, exog_infl=X_inflation)
results = zip_model.fit()

# Fitted structural-zero mixture probability; not a permanent individual trait
structural_zero_prob = 1 - results.predict(
    X_count, exog_infl=X_inflation, which='prob-main')

# Expected claims
expected_claims = results.predict(X_count, exog_infl=X_inflation, which='mean')
Raw lines 618 to 635
636## Best Practices
6381. **Check data type**: Ensure response matches model (binary, counts, categories)
6392. **Specify intercept correctly**: Array Logit/Poisson usually need a constant; OrderedModel and ConditionalLogit forbid one
6403. **Scale continuous predictors**: For better convergence and interpretation
6414. **Check convergence**: Look for convergence warnings
6425. **Use formula API**: For categorical variables and interactions
6436. **Marginal effects**: Report marginal effects, not just coefficients
6447. **Model comparison**: Use AIC/BIC and cross-validation
6458. **Validate**: Holdout set or cross-validation for predictive models
6469. **Check overdispersion**: For count models, test Poisson assumption
64710. **Consider alternatives**: Zero-inflation, hurdle models for excess zeros

Best Practices

  1. Check data type: Ensure response matches model (binary, counts, categories)
  2. Specify intercept correctly: Array Logit/Poisson usually need a constant; OrderedModel and ConditionalLogit forbid one
  3. Scale continuous predictors: For better convergence and interpretation
  4. Check convergence: Look for convergence warnings
  5. Use formula API: For categorical variables and interactions
  6. Marginal effects: Report marginal effects, not just coefficients
  7. Model comparison: Use AIC/BIC and cross-validation
  8. Validate: Holdout set or cross-validation for predictive models
  9. Check overdispersion: For count models, test Poisson assumption
  10. Consider alternatives: Zero-inflation, hurdle models for excess zeros
Raw lines 636 to 648
649## Common Pitfalls
6511. **Forgetting constant**: No intercept term
6522. **Perfect separation**: Logit/probit may not converge
6533. **Using Poisson with overdispersion**: Check and use Negative Binomial
6544. **Misinterpreting coefficients**: Remember they're on log-odds/log scale
6555. **Not checking convergence**: Optimization may fail silently
6566. **Wrong distribution**: Match model to data type (binary/count/categorical)
6577. **Ignoring excess zeros**: Use ZIP/ZINB when appropriate
6588. **Not validating predictions**: Always check out-of-sample performance
6599. **Comparing non-nested models**: Use AIC/BIC, not likelihood ratio test
66010. **Ordinal as nominal**: Use OrderedModel for ordered categories

Common Pitfalls

  1. Forgetting constant: No intercept term
  2. Perfect separation: Logit/probit may not converge
  3. Using Poisson with overdispersion: Check and use Negative Binomial
  4. Misinterpreting coefficients: Remember they're on log-odds/log scale
  5. Not checking convergence: Optimization may fail silently
  6. Wrong distribution: Match model to data type (binary/count/categorical)
  7. Ignoring excess zeros: Use ZIP/ZINB when appropriate
  8. Not validating predictions: Always check out-of-sample performance
  9. Comparing non-nested models: Use AIC/BIC, not likelihood ratio test
  10. Ordinal as nominal: Use OrderedModel for ordered categories
Raw lines 649 to 660