MULTINOMIAL_LOGIT
Overview
The MULTINOMIAL_LOGIT function fits a multinomial logistic regression model, which extends binary logistic regression to handle categorical dependent variables with more than two possible outcomes. This type of model is widely used in discrete choice analysis, where the goal is to predict which category an observation will belong to based on a set of predictor variables.
Multinomial logistic regression models the probability of each outcome category relative to a reference category. For a dependent variable with K possible categories and a set of predictor variables X, the model estimates the probability of observing category k using the softmax function:
P(Y = k \mid X) = \frac{e^{\beta_k \cdot X}}{\sum_{j=1}^{K} e^{\beta_j \cdot X}}
where \beta_k represents the vector of regression coefficients for category k. The lowest category value serves as the reference category (with coefficients set to zero), and the model returns coefficients for all other categories relative to this reference.
This implementation uses the MNLogit class from the statsmodels library. Model parameters are estimated via maximum likelihood estimation, and the function returns coefficients, standard errors, z-statistics, p-values, and confidence intervals for each parameter-category combination. Model fit statistics include pseudo R-squared (McFadden’s), log-likelihood, AIC, and BIC.
Common applications include predicting consumer brand choices, transportation mode selection, voting behavior, and disease classification—any scenario where the outcome falls into one of several unordered categories. The model assumes independence of irrelevant alternatives (IIA), meaning the relative odds between any two choices are independent of other available alternatives. For more theoretical background, see the Wikipedia article on multinomial logistic regression.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=MULTINOMIAL_LOGIT(y, x, fit_intercept, alpha)
y(list[list], required): Categorical dependent variable as a column vector of integer category values (0, 1, 2, …).x(list[list], required): Independent variables (predictors) as a matrix where each column represents a different predictor.fit_intercept(bool, optional, default: true): If True, adds an intercept term to the model.alpha(float, optional, default: 0.05): Significance level for confidence intervals (0 < alpha < 1).
Returns (list[list]): 2D list with multinomial results, or error string.
Examples
Example 1: Basic three categories with intercept
Inputs:
| y | x |
|---|---|
| 0 | 1 |
| 0 | 1.5 |
| 0 | 2 |
| 1 | 2.5 |
| 0 | 3 |
| 1 | 3.5 |
| 1 | 4 |
| 2 | 4.5 |
| 1 | 5 |
| 2 | 5.5 |
| 2 | 6 |
| 0 | 2.2 |
Excel formula:
=MULTINOMIAL_LOGIT({0;0;0;1;0;1;1;2;1;2;2;0}, {1;1.5;2;2.5;3;3.5;4;4.5;5;5.5;6;2.2})
Expected output:
| category | parameter | coefficient | std_error | z_statistic | p_value | ci_lower | ci_upper |
|---|---|---|---|---|---|---|---|
| 1 | const | -7.94 | 5.117 | -1.552 | 0.1207 | -17.97 | 2.089 |
| 1 | x0 | 2.804 | 1.838 | 1.526 | 0.1271 | -0.7986 | 6.407 |
| 2 | const | -19.59 | 10.03 | -1.954 | 0.0507 | -39.24 | 0.0611 |
| 2 | x0 | 5.257 | 2.596 | 2.026 | 0.0428 | 0.1702 | 10.34 |
| pseudo_r_squared | 0.6008 | ||||||
| log_likelihood | -5.162 | ||||||
| aic | 18.32 | ||||||
| bic | 20.26 |
Example 2: Model without intercept
Inputs:
| y | x | fit_intercept |
|---|---|---|
| 0 | 1 | false |
| 0 | 1.5 | |
| 0 | 2 | |
| 1 | 2.5 | |
| 0 | 3 | |
| 1 | 3.5 | |
| 1 | 4 | |
| 2 | 4.5 | |
| 1 | 5 | |
| 2 | 5.5 | |
| 2 | 6 | |
| 0 | 2.2 |
Excel formula:
=MULTINOMIAL_LOGIT({0;0;0;1;0;1;1;2;1;2;2;0}, {1;1.5;2;2.5;3;3.5;4;4.5;5;5.5;6;2.2}, FALSE)
Expected output:
| category | parameter | coefficient | std_error | z_statistic | p_value | ci_lower | ci_upper |
|---|---|---|---|---|---|---|---|
| 1 | x0 | 0.1081 | 0.2062 | 0.5242 | 0.6001 | -0.296 | 0.5121 |
| 2 | x0 | 0.1236 | 0.2035 | 0.6072 | 0.5437 | -0.2753 | 0.5224 |
| pseudo_r_squared | -0.0026 | ||||||
| log_likelihood | -12.96 | ||||||
| aic | 29.93 | ||||||
| bic | 30.9 |
Example 3: Custom confidence level (90%)
Inputs:
| y | x | alpha |
|---|---|---|
| 0 | 1 | 0.1 |
| 0 | 1.5 | |
| 0 | 2 | |
| 1 | 2.5 | |
| 0 | 3 | |
| 1 | 3.5 | |
| 1 | 4 | |
| 2 | 4.5 | |
| 1 | 5 | |
| 2 | 5.5 | |
| 2 | 6 | |
| 0 | 2.2 |
Excel formula:
=MULTINOMIAL_LOGIT({0;0;0;1;0;1;1;2;1;2;2;0}, {1;1.5;2;2.5;3;3.5;4;4.5;5;5.5;6;2.2}, 0.1)
Expected output:
| category | parameter | coefficient | std_error | z_statistic | p_value | ci_lower | ci_upper |
|---|---|---|---|---|---|---|---|
| 1 | const | -7.94 | 5.117 | -1.552 | 0.1207 | -16.36 | 0.4766 |
| 1 | x0 | 2.804 | 1.838 | 1.526 | 0.1271 | -0.2194 | 5.828 |
| 2 | const | -19.59 | 10.03 | -1.954 | 0.0507 | -36.08 | -3.098 |
| 2 | x0 | 5.257 | 2.596 | 2.026 | 0.0428 | 0.9881 | 9.527 |
| pseudo_r_squared | 0.6008 | ||||||
| log_likelihood | -5.162 | ||||||
| aic | 18.32 | ||||||
| bic | 20.26 |
Example 4: All arguments specified (99% CI)
Inputs:
| y | x | fit_intercept | alpha |
|---|---|---|---|
| 0 | 1 | true | 0.01 |
| 0 | 1.5 | ||
| 0 | 2 | ||
| 1 | 2.5 | ||
| 0 | 3 | ||
| 1 | 3.5 | ||
| 1 | 4 | ||
| 2 | 4.5 | ||
| 1 | 5 | ||
| 2 | 5.5 | ||
| 2 | 6 | ||
| 0 | 2.2 |
Excel formula:
=MULTINOMIAL_LOGIT({0;0;0;1;0;1;1;2;1;2;2;0}, {1;1.5;2;2.5;3;3.5;4;4.5;5;5.5;6;2.2}, TRUE, 0.01)
Expected output:
| category | parameter | coefficient | std_error | z_statistic | p_value | ci_lower | ci_upper |
|---|---|---|---|---|---|---|---|
| 1 | const | -7.94 | 5.117 | -1.552 | 0.1207 | -21.12 | 5.24 |
| 1 | x0 | 2.804 | 1.838 | 1.526 | 0.1271 | -1.931 | 7.539 |
| 2 | const | -19.59 | 10.03 | -1.954 | 0.0507 | -45.42 | 6.236 |
| 2 | x0 | 5.257 | 2.596 | 2.026 | 0.0428 | -1.428 | 11.94 |
| pseudo_r_squared | 0.6008 | ||||||
| log_likelihood | -5.162 | ||||||
| aic | 18.32 | ||||||
| bic | 20.26 |
Python Code
import math
from statsmodels.discrete.discrete_model import MNLogit as statsmodels_mnlogit
def multinomial_logit(y, x, fit_intercept=True, alpha=0.05):
"""
Fits a multinomial logistic regression model for multi-category outcomes.
See: https://www.statsmodels.org/stable/generated/statsmodels.discrete.discrete_model.MNLogit.html
This example function is provided as-is without any representation of accuracy.
Args:
y (list[list]): Categorical dependent variable as a column vector of integer category values (0, 1, 2, ...).
x (list[list]): Independent variables (predictors) as a matrix where each column represents a different predictor.
fit_intercept (bool, optional): If True, adds an intercept term to the model. Default is True.
alpha (float, optional): Significance level for confidence intervals (0 < alpha < 1). Default is 0.05.
Returns:
list[list]: 2D list with multinomial results, or error string.
"""
def to2d(val):
return [[val]] if not isinstance(val, list) else val
def validate_numeric(val, name):
if not isinstance(val, (int, float)):
return f"Invalid input: {name} must be a number."
if math.isnan(val) or math.isinf(val):
return f"Invalid input: {name} must be finite."
return None
# Normalize inputs
y = to2d(y)
x = to2d(x)
# Validate y is a column vector
if not isinstance(y, list) or len(y) == 0:
return "Invalid input: y must be a non-empty 2D list."
if not all(isinstance(row, list) and len(row) == 1 for row in y):
return "Invalid input: y must be a column vector (2D list with one column)."
# Validate x is a matrix
if not isinstance(x, list) or len(x) == 0:
return "Invalid input: x must be a non-empty 2D list."
if not all(isinstance(row, list) for row in x):
return "Invalid input: x must be a 2D list."
num_rows_x = len(x)
num_cols_x = len(x[0]) if num_rows_x > 0 else 0
if num_cols_x == 0:
return "Invalid input: x must have at least one column."
if not all(len(row) == num_cols_x for row in x):
return "Invalid input: x must have consistent row lengths."
# Check y and x have same number of rows
if len(y) != num_rows_x:
return "Invalid input: y and x must have the same number of rows."
# Validate fit_intercept
if not isinstance(fit_intercept, bool):
return "Invalid input: fit_intercept must be a boolean."
# Validate alpha
err = validate_numeric(alpha, "alpha")
if err:
return err
if alpha <= 0 or alpha >= 1:
return "Invalid input: alpha must be between 0 and 1."
# Extract y values
y_flat = []
for row in y:
val = row[0]
err = validate_numeric(val, "y value")
if err:
return err
y_flat.append(val)
# Check y values are integers
for val in y_flat:
if val != int(val):
return "Invalid input: y must contain integer category values."
# Extract x values
x_matrix = []
for row in x:
x_row = []
for val in row:
err = validate_numeric(val, "x value")
if err:
return err
x_row.append(float(val))
x_matrix.append(x_row)
# Add intercept if requested
if fit_intercept:
x_matrix = [[1.0] + row for row in x_matrix]
param_names = ["const"] + [f"x{i}" for i in range(num_cols_x)]
else:
param_names = [f"x{i}" for i in range(num_cols_x)]
# Fit the model
try:
model = statsmodels_mnlogit(y_flat, x_matrix)
result = model.fit(disp=0)
except Exception as exc: # noqa: BLE001
return f"Model fitting error: {exc}"
# Extract results
output = [["category", "parameter", "coefficient", "std_error", "z_statistic", "p_value", "ci_lower", "ci_upper"]]
# Get confidence intervals
try:
conf_int = result.conf_int(alpha=alpha)
except Exception as exc: # noqa: BLE001
return f"Confidence interval error: {exc}"
# Get categories (excluding reference category which is the first/lowest category)
categories = sorted(set(y_flat))
non_ref_categories = categories[1:]
# Extract parameters for each non-reference category
# Result arrays are organized as [param_idx, category_idx] or [category_idx, param_idx]
# Need to check the actual structure
params_array = result.params
bse_array = result.bse
tvalues_array = result.tvalues
pvalues_array = result.pvalues
for cat_idx, cat in enumerate(non_ref_categories):
for param_idx, param_name in enumerate(param_names):
# Access values
# params, bse, tvalues, pvalues: [num_params, num_categories-1]
# conf_int: [num_categories-1, num_params, 2]
coef = params_array[param_idx, cat_idx] if len(params_array.shape) > 1 else params_array[param_idx]
std_err = bse_array[param_idx, cat_idx] if len(bse_array.shape) > 1 else bse_array[param_idx]
z_stat = tvalues_array[param_idx, cat_idx] if len(tvalues_array.shape) > 1 else tvalues_array[param_idx]
p_val = pvalues_array[param_idx, cat_idx] if len(pvalues_array.shape) > 1 else pvalues_array[param_idx]
ci_low = conf_int[cat_idx, param_idx, 0] if len(conf_int.shape) > 2 else conf_int[param_idx, 0]
ci_high = conf_int[cat_idx, param_idx, 1] if len(conf_int.shape) > 2 else conf_int[param_idx, 1]
output.append([
float(cat),
param_name,
float(coef),
float(std_err),
float(z_stat),
float(p_val),
float(ci_low),
float(ci_high)
])
# Add model statistics
output.append(["pseudo_r_squared", "", float(result.prsquared), "", "", "", "", ""])
output.append(["log_likelihood", "", float(result.llf), "", "", "", "", ""])
output.append(["aic", "", float(result.aic), "", "", "", "", ""])
output.append(["bic", "", float(result.bic), "", "", "", "", ""])
return output