Bayesian binary logit via bayesm (R) — independence Metropolis¶
Companion to binary_logit_python.ipynb¶
Reference: Rossi, Allenby & McCulloch (2005), Bayesian Statistics and Marketing (bayesm).
| Section | Content |
|---|---|
| Model | Logit as a 2-alternative MNL; why bayesm uses Metropolis (not Gibbs); rmnlIndepMetrop; refs |
| Section 1 | Synthetic data (shared binary_logit_synth.csv) — posterior vs. truth, MLE, and the Python samplers |
| Section 2 | Real data — Mroz (1987) women’s labor-force participation |
Fits the same synthetic data as the Python notebook (binary_logit_synth.csv) → a direct cross-language check. bayesm estimates a binary logit as a multinomial logit with 2 alternatives by independence Metropolis.
Model — binary logit as MNL(2), independence Metropolis¶
$$y_i \sim \text{Bernoulli}(\sigma(x_i'\beta))$$
bayesm has no binary-logit Gibbs sampler — because, unlike probit's Albert–Chib, logit has no conjugate data augmentation built in — so it fits logit as a multinomial logit with 2 alternatives via independence Metropolis (rmnlIndepMetrop):
- alt 1 = "success" ($y=1$), alt 2 = base ($y=0$); utilities $u_1 = x'\beta$, $u_2 = 0 \Rightarrow \Pr(y=1)=\sigma(x'\beta)$.
createX(p=2, nd=3, Xd=[x1,x2,x3], INT=TRUE, base=2)builds the stacked $(2n)\times k$ design.- Independence Metropolis: proposes $\beta$ from a multivariate-$t$ centred at the MLE with the MLE covariance — so proposals already resemble the posterior and acceptance is high (~0.8), unlike the random-walk Metropolis in the Python notebook (acceptance ~0.3).
The symmetry in bayesm: probit → rbprobitGibbs (Albert–Chib Gibbs); logit → rmnlIndepMetrop (Metropolis). The Pólya–Gamma trick (Section 2 of the Python notebook) is exactly what restores Gibbs for logit.
References: Rossi, Allenby & McCulloch (2005); McCulloch & Rossi (1994).
.libPaths(c("C:/Users/user/R/win-library/4.6", .libPaths()))
suppressMessages(library(bayesm))
d <- read.csv("C:/Users/user/project/binary_logit_synth.csv")
cat(sprintf("n=%d mean(y)=%.3f\n", nrow(d), mean(d$y)))
Xd <- as.matrix(d[, c("x1", "x2", "x3")])
ymnl <- ifelse(d$y == 1, 1L, 2L) # alt 1 = success, alt 2 = base
X <- createX(p = 2, na = NULL, nd = 3, Xa = NULL, Xd = Xd, INT = TRUE, base = 2)
cat("createX -> design dim:", dim(X), " (expect", nrow(d) * 2, "x 4)\n")
n=2000 mean(y)=0.569 createX -> design dim: 4000 4 (expect 4000 x 4)
set.seed(0)
out <- rmnlIndepMetrop(Data = list(p = 2, y = ymnl, X = X),
Mcmc = list(R = 20000, keep = 5, nprint = 0))
bd <- out$betadraw
burn <- nrow(bd) %/% 4
bd <- bd[(burn + 1):nrow(bd), ]
truth <- c(0.5, -1.0, 1.5, -0.75)
mle <- c(0.445, -1.005, 1.535, -0.619) # Python notebook, identical data
nm <- c("const", "x1", "x2", "x3")
pm <- colMeans(bd); ps <- apply(bd, 2, sd)
cat(sprintf("\nindependence-Metropolis acceptance = %.3f\n\n", mean(out$acceptr)))
cat(sprintf("%-7s %7s %9s %12s %10s\n", "param", "truth", "MLE", "bayesm mean", "bayesm sd"))
for (j in 1:4)
cat(sprintf("%-7s %7.2f %9.3f %12.3f %10.3f\n", nm[j], truth[j], mle[j], pm[j], ps[j]))
table of y values
y
1 2
1138 862
Starting Independence Metropolis Sampler for Multinomial Logit Model
2000 obs with 2 alternatives
Table of y Values
y
1 2
1138 862
Prior Parms:
betabar
[1] 0 0 0 0
A
[,1] [,2] [,3] [,4]
[1,] 0.01 0.00 0.00 0.00
[2,] 0.00 0.01 0.00 0.00
[3,] 0.00 0.00 0.01 0.00
[4,] 0.00 0.00 0.00 0.01
MCMC parms:
R= 20000 keep= 5 nprint= 0 nu (df for st candidates) = 6
independence-Metropolis acceptance = 0.818
param truth MLE bayesm mean bayesm sd
const 0.50 0.445 0.444 0.058
x1 -1.00 -1.005 -1.008 0.066
x2 1.50 1.535 1.538 0.080
x3 -0.75 -0.619 -0.619 0.060
options(repr.plot.width = 12, repr.plot.height = 3.4)
truth <- c(0.5, -1.0, 1.5, -0.75); mle <- c(0.445, -1.005, 1.535, -0.619)
nm <- c("const", "x1", "x2", "x3")
par(mfrow = c(1, 4), mar = c(4, 3, 2, 1))
for (j in 1:4) {
hist(bd[, j], breaks = 40, freq = FALSE, col = "lightsteelblue", border = "white",
main = nm[j], xlab = "beta")
abline(v = truth[j], col = "black", lty = 2, lwd = 2)
abline(v = mle[j], col = "firebrick", lwd = 2)
}
legend("topright", c("truth", "MLE"), col = c("black", "firebrick"),
lty = c(2, 1), lwd = 2, bty = "n", cex = 0.8)
Results — bayesm logit vs. the Python samplers¶
| param | truth | MLE | bayesm (R) | Python RWM | Python PG | bambi | PyMC |
|---|---|---|---|---|---|---|---|
| const | 0.50 | 0.445 | 0.444 | 0.446 | 0.449 | 0.444 | 0.444 |
| x1 | −1.00 | −1.005 | −1.008 | −1.008 | −1.016 | −1.000 | −1.005 |
| x2 | 1.50 | 1.535 | 1.538 | 1.542 | 1.552 | 1.527 | 1.539 |
| x3 | −0.75 | −0.619 | −0.619 | −0.623 | −0.625 | −0.616 | −0.620 |
(bayesm posterior sd ≈ [0.058, 0.066, 0.080, 0.060], matching the MLE se.)
Six methods agree across two languages — the from-scratch RW-Metropolis and Pólya–Gamma Gibbs, bambi, PyMC (Python), the bayesm independence Metropolis (R), and the MLE baseline. Independence-Metropolis acceptance is ~0.82, far above the random walk's ~0.30, because the proposal is the MLE-centred multivariate-$t$ — proposals already look like the posterior.
The probit/logit symmetry, made concrete: bayesm does probit by Gibbs (rbprobitGibbs, Albert–Chib) but logit by Metropolis (rmnlIndepMetrop), precisely because logit lacks a built-in conjugate augmentation. The Pólya–Gamma sampler in Section 2 of the Python notebook is the missing piece that restores Gibbs for logit.
This completes the binary-logit cross-validation. Next: choose a real-data application.
2. Real data: Mroz (1987) women's labor-force participation¶
The same Mroz LFP model as binary_logit_python.ipynb Section 4 (mroz_lfp.csv, $n=753$):
$$\text{inlf} \sim \text{nwifeinc} + \text{educ} + \text{exper} + \text{exper}^2 + \text{age} + \text{kidslt6} + \text{kidsge6}$$
fit by bayesm's independence Metropolis — a cross-language check against the Python samplers and Wooldridge's published estimates.
Variables (Mroz 1987; married white women aged 30–60, $n=753$; shared mroz_lfp.csv):
| Variable | Definition | Units | mean | range | exp. sign |
|---|---|---|---|---|---|
inlf (outcome) |
=1 if in the labor force in 1975 | 0/1 | 0.568 | {0,1} | — |
nwifeinc |
Non-wife income = (family income − wife's earnings) | $1000s | 20.13 | −0.03…96 | − (income effect) |
educ |
Years of schooling | years | 12.29 | 5…17 | + |
exper |
Actual labor-market experience | years | 10.63 | 0…45 | + |
expersq |
exper² |
years² | 178.0 | 0…2025 | − (concave) |
age |
Age | years | 42.54 | 30…60 | − |
kidslt6 |
Number of children < 6 | count | 0.238 | 0…3 | − (childcare) |
kidsge6 |
Number of children 6–18 | count | 1.353 | 0…8 | ≈ 0 |
In the MNL(2) coding, alt 1 = "in labor force" with utility $x'\beta$ and alt 2 (out) as base, so $\Pr(\text{inlf})=\Lambda(x'\beta)$ and the coefficients are read exactly as a binary logit.
dm <- read.csv("C:/Users/user/project/mroz_lfp.csv")
cols <- c("nwifeinc", "educ", "exper", "expersq", "age", "kidslt6", "kidsge6")
Xdm <- as.matrix(dm[, cols])
ymnl2 <- ifelse(dm$inlf == 1, 1L, 2L)
Xm <- createX(p = 2, na = NULL, nd = length(cols), Xa = NULL, Xd = Xdm, INT = TRUE, base = 2)
set.seed(0)
outm <- rmnlIndepMetrop(Data = list(p = 2, y = ymnl2, X = Xm),
Mcmc = list(R = 20000, keep = 5, nprint = 0))
bdm <- outm$betadraw; bdm <- bdm[(nrow(bdm) %/% 4 + 1):nrow(bdm), ]
nm <- c("const", cols)
wool <- c(0.425, -0.021, 0.221, 0.206, -0.003, -0.088, -1.443, 0.060)
pm <- colMeans(bdm); ps <- apply(bdm, 2, sd)
cat(sprintf("independence-Metropolis acceptance = %.3f\n\n", mean(outm$acceptr)))
cat(sprintf("%-9s %11s %12s %10s\n", "param", "Wooldridge", "bayesm mean", "bayesm sd"))
for (j in 1:8) cat(sprintf("%-9s %11.3f %12.3f %10.3f\n", nm[j], wool[j], pm[j], ps[j]))
table of y values
y
1 2
428 325
Starting Independence Metropolis Sampler for Multinomial Logit Model
753 obs with 2 alternatives
Table of y Values
y
1 2
428 325
Prior Parms:
betabar
[1] 0 0 0 0 0 0 0 0
A
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 0.01 0.00 0.00 0.00 0.00 0.00 0.00 0.00
[2,] 0.00 0.01 0.00 0.00 0.00 0.00 0.00 0.00
[3,] 0.00 0.00 0.01 0.00 0.00 0.00 0.00 0.00
[4,] 0.00 0.00 0.00 0.01 0.00 0.00 0.00 0.00
[5,] 0.00 0.00 0.00 0.00 0.01 0.00 0.00 0.00
[6,] 0.00 0.00 0.00 0.00 0.00 0.01 0.00 0.00
[7,] 0.00 0.00 0.00 0.00 0.00 0.00 0.01 0.00
[8,] 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.01
MCMC parms:
R= 20000 keep= 5 nprint= 0 nu (df for st candidates) = 6
independence-Metropolis acceptance = 0.492
param Wooldridge bayesm mean bayesm sd
const 0.425 0.437 0.855
nwifeinc -0.021 -0.022 0.008
educ 0.221 0.224 0.043
exper 0.206 0.208 0.033
expersq -0.003 -0.003 0.001
age -0.088 -0.089 0.015
kidslt6 -1.443 -1.467 0.204
kidsge6 0.060 0.062 0.075
2. Results — Mroz LFP (R bayesm vs. Python & Wooldridge)¶
| param | Wooldridge | bayesm (R) | Python PG | odds ratio |
|---|---|---|---|---|
| const | 0.425 | 0.437 | 0.441 | — |
| nwifeinc | −0.021 | −0.022 | −0.022 | 0.98 / $1k |
| educ | 0.221 | 0.224 | 0.225 | 1.25 / yr |
| exper | 0.206 | 0.208 | 0.208 | 1.23 |
| expersq | −0.003 | −0.003 | −0.003 | concave |
| age | −0.088 | −0.089 | −0.090 | 0.91 / yr |
| kidslt6 | −1.443 | −1.467 | −1.467 | 0.23 / child < 6 |
| kidsge6 | 0.060 | 0.062 | 0.059 | 1.06 (n.s.) |
bayesm's independence Metropolis (acceptance ≈ 0.49) reproduces Wooldridge's logit estimates and the Python from-scratch / bambi / PyMC results — the same economic story (a young child is the dominant drag on participation; education and experience raise it). This closes the binary-logit project: two languages, six samplers, synthetic + the canonical Mroz dataset, all in agreement.
# --- Analysis of findings: odds ratios, predicted probabilities, average partial effects ---
B <- bdm
lo <- apply(B, 2, quantile, 0.025); hi <- apply(B, 2, quantile, 0.975); mn <- colMeans(B)
cat(sprintf("%-9s %8s %-20s %4s %8s\n", "coef", "mean", "95% CI", "sig", "OR"))
for (j in 1:8)
cat(sprintf("%-9s %8.3f [%7.3f, %7.3f] %4s %8.3f\n", nm[j], mn[j], lo[j], hi[j],
ifelse(lo[j] > 0 | hi[j] < 0, "yes", "no"), exp(mn[j])))
Xf <- cbind(1, Xdm); xbar <- colMeans(Xf)
Pavg <- function(x) mean(plogis(as.numeric(B %*% x)))
prof <- function(k, v) { x <- xbar; x[which(nm == k)] <- v; x }
cat(sprintf("\nP(inlf) at sample means = %.3f\n", Pavg(xbar)))
cat(sprintf(" educ 12 -> 16: %.3f -> %.3f\n", Pavg(prof("educ", 12)), Pavg(prof("educ", 16))))
cat(sprintf(" kids<6 0 -> 1: %.3f -> %.3f\n", Pavg(prof("kidslt6", 0)), Pavg(prof("kidslt6", 1))))
base <- plogis(Xf %*% t(B))
ame <- function(k, d) { X2 <- Xf; X2[, which(nm == k)] <- X2[, which(nm == k)] + d
e <- colMeans(plogis(X2 %*% t(B)) - base); c(mean(e), quantile(e, 0.025), quantile(e, 0.975)) }
ak <- ame("kidslt6", 1); ae <- ame("educ", 1)
cat(sprintf(" AME +1 child<6 = %+.3f [%+.3f, %+.3f]\n", ak[1], ak[2], ak[3]))
cat(sprintf(" AME +1 yr educ = %+.3f [%+.3f, %+.3f]\n", ae[1], ae[2], ae[3]))
cat(sprintf("\nin-sample accuracy (0.5 cutoff) = %.3f (base rate %.3f)\n",
mean((plogis(Xf %*% mn) > 0.5) == dm$inlf), max(mean(dm$inlf), 1 - mean(dm$inlf))))
coef mean 95% CI sig OR const 0.437 [ -1.212, 2.106] no 1.548 nwifeinc -0.022 [ -0.038, -0.005] yes 0.979 educ 0.224 [ 0.142, 0.309] yes 1.251 exper 0.208 [ 0.144, 0.271] yes 1.232 expersq -0.003 [ -0.005, -0.001] yes 0.997 age -0.089 [ -0.118, -0.062] yes 0.915 kidslt6 -1.467 [ -1.876, -1.082] yes 0.231 kidsge6 0.062 [ -0.082, 0.208] no 1.063 P(inlf) at sample means = 0.583 educ 12 -> 16: 0.567 -> 0.761 kids<6 0 -> 1: 0.664 -> 0.315 AME +1 child<6 = -0.258 [-0.314, -0.199] AME +1 yr educ = +0.039 [+0.025, +0.052] in-sample accuracy (0.5 cutoff) = 0.736 (base rate 0.568)
2. Analysis of findings — Mroz LFP (bayesm posterior)¶
Computed from the bayesm independence-Metropolis draws — and matching the Python Section 4 results (same data, same model).
- Significance: six of seven predictors are credibly nonzero (
nwifeinc,educ,exper,expersq,age,kidslt6); onlykidsge6is not (95% CI ≈ [−0.08, 0.21]) — school-age children don't measurably affect participation once the rest is controlled. - Odds ratios:
kidslt60.23 (each child under 6 multiplies the odds of working by ~0.23 — the dominant effect),educ1.25/yr,exper1.23 (concave viaexpersq),age0.91/yr,nwifeinc0.98/$1k. - Predicted probabilities (others at means): ≈ 0.58 at the average profile; HS→college 0.57 → 0.76; first child under 6 0.66 → 0.32.
- Average partial effects: +1 child < 6 ≈ −0.26 probability, +1 year educ ≈ +0.039, each with a credible interval.
- Fit: ~73.6% in-sample accuracy vs. a 56.8% base rate.
Cross-language agreement: these match the Python from-scratch / bambi / PyMC findings to ~0.01 — the bayesm posterior, the two from-scratch Python samplers, and the production tools all tell the identical economic story. The Bayesian framing delivers every quantity (odds ratios, predicted probabilities, partial effects) with a credible interval directly from the posterior draws.