Bayesian Priors & Data Augmentation — the R engine¶
Statistical Distributions — the machinery of Bayesian computation, validated in R¶
The independent R counterpart to bayespriors_python.ipynb. Each law's PDF, CDF, RNG, moments is built from scratch in base R, using dnorm/pnorm/qnorm, dcauchy, dt and besselK as mathematical primitives (the same role scipy.special plays in Python). Validation uses:
- the CDF recovered by integrating our own PDF (cumulative trapezoid) against the closed form;
- closed-form moments matched to large samples;
- for the Pólya-Gamma (no elementary density), the $b=1$ series density, the mean $\tfrac{b}{2c}\tanh\tfrac c2$, and the Laplace transform;
- for the Dirichlet, the Beta marginals and the density.
RNG algorithms mirror the Python notebook: inverse-CDF (Truncated Normal), folding (Half-*), Michael–Schucany–Haas (Inverse-Gaussian), numerical inverse-transform (GIG), the Gamma-sum representation (Pólya-Gamma) and normalised Gammas (Dirichlet). Self-contained; base R only.
options(repr.plot.width=13, repr.plot.height=3.6)
BLUE<-"#2b6cb0"; RED<-"#c53030"; GREEN<-"#2f855a"; ORANGE<-"#dd6b20"; GREY<-"#718096"; PURP<-"#6b46c1"
# ---- RNG primitives ----
norm_rng<-function(mu,s,n){ m<-ceiling(n/2); u1<-runif(m); u2<-runif(m); r<-sqrt(-2*log(u1)); z<-c(r*cos(2*pi*u2), r*sin(2*pi*u2)); mu+s*z[1:n] }
gam_shape<-function(k,n){ if(k<1){ g<-gam_shape(k+1,n); u<-runif(n); return(g*u^(1/k)) }
d<-k-1/3; c<-1/sqrt(9*d); res<-numeric(n); todo<-rep(TRUE,n)
while(any(todo)){ m<-sum(todo); x<-norm_rng(0,1,m); v<-(1+c*x)^3; u<-runif(m)
ok<- v>0 & log(u) < 0.5*x^2 + d - d*v + d*log(ifelse(v>0,v,1)); idx<-which(todo); acc<-idx[ok]; res[acc]<-d*v[ok]; todo[acc]<-FALSE }
res }
# ---- truncated & half ----
tn_pdf<-function(x,mu,s,a,b){ al<-(a-mu)/s; be<-(b-mu)/s; Z<-pnorm(be)-pnorm(al); ifelse(x>=a & x<=b, dnorm((x-mu)/s)/(s*Z), 0) }
tn_cdf<-function(x,mu,s,a,b){ al<-(a-mu)/s; be<-(b-mu)/s; Z<-pnorm(be)-pnorm(al); ifelse(x<a,0,ifelse(x>b,1,(pnorm((pmin(pmax(x,a),b)-mu)/s)-pnorm(al))/Z)) }
tn_rng<-function(mu,s,a,b,n){ al<-(a-mu)/s; be<-(b-mu)/s; Pa<-pnorm(al); Pb<-pnorm(be); mu+s*qnorm(Pa+runif(n)*(Pb-Pa)) }
tn_mom<-function(mu,s,a,b){ al<-(a-mu)/s; be<-(b-mu)/s; Z<-pnorm(be)-pnorm(al); mean<-mu+s*(dnorm(al)-dnorm(be))/Z
v<-s^2*(1+(al*dnorm(al)-be*dnorm(be))/Z-((dnorm(al)-dnorm(be))/Z)^2); c(mean=mean, var=v, skew=NA, exkurt=NA) }
hn_pdf<-function(x,s) ifelse(x>=0, sqrt(2/pi)/s*exp(-x^2/(2*s^2)), 0)
hn_cdf<-function(x,s) ifelse(x>=0, 2*pnorm(x/s)-1, 0)
hn_rng<-function(s,n) abs(s*norm_rng(0,1,n))
hn_mom<-function(s) c(mean=s*sqrt(2/pi), var=s^2*(1-2/pi), skew=sqrt(2)*(4-pi)/(pi-2)^1.5, exkurt=8*(pi-3)/(pi-2)^2)
hc_pdf<-function(x,s) ifelse(x>=0, 2/(pi*s*(1+(x/s)^2)), 0)
hc_cdf<-function(x,s) ifelse(x>=0, 2/pi*atan(x/s), 0)
hc_rng<-function(s,n) abs(s*tan(pi*(runif(n)-0.5)))
ht_pdf<-function(x,s,nu) ifelse(x>=0, 2/s*dt(x/s,nu), 0)
ht_cdf<-function(x,s,nu) ifelse(x>=0, 2*pt(pmax(x,0)/s,nu)-1, 0)
ht_rng<-function(s,nu,n){ z<-norm_rng(0,1,n); v<-2*gam_shape(nu/2,n); abs(s*z/sqrt(v/nu)) }
# ---- mixing ----
ig_pdf<-function(x,mu,lam) ifelse(x>0, sqrt(lam/(2*pi*pmax(x,1e-300)^3))*exp(-lam*(x-mu)^2/(2*mu^2*pmax(x,1e-300))), 0)
ig_cdf<-function(x,mu,lam){ xp<-pmax(x,1e-300); r<-sqrt(lam/xp); ifelse(x>0, pnorm(r*(xp/mu-1))+exp(2*lam/mu)*pnorm(-r*(xp/mu+1)), 0) }
ig_rng<-function(mu,lam,n){ y<-norm_rng(0,1,n)^2; x<-mu+mu^2*y/(2*lam)-(mu/(2*lam))*sqrt(4*mu*lam*y+mu^2*y^2); ifelse(runif(n)<=mu/(mu+x), x, mu^2/x) }
ig_mom<-function(mu,lam) c(mean=mu, var=mu^3/lam, skew=3*sqrt(mu/lam), exkurt=15*mu/lam)
gig_pdf<-function(x,p,a,b){ w<-sqrt(a*b); k<-(a/b)^(p/2)/(2*besselK(w,p)); ifelse(x>0, k*pmax(x,1e-300)^(p-1)*exp(-(a*x+b/pmax(x,1e-300))/2), 0) }
gig_mom<-function(p,a,b){ w<-sqrt(a*b); r<-sqrt(b/a); raw<-function(k) r^k*besselK(w,p+k)/besselK(w,p); m<-sapply(1:4,raw)
v<-m[2]-m[1]^2; c(mean=m[1], var=v, skew=(m[3]-3*m[1]*m[2]+2*m[1]^3)/v^1.5, exkurt=(m[4]-4*m[1]*m[3]+6*m[1]^2*m[2]-3*m[1]^4)/v^2-3) }
cumtrapz<-function(x,y){ n<-length(x); c(0, cumsum((y[-1]+y[-n])/2*diff(x))) }
inv_tf<-function(pdf,lo,hi,n,npts=2e5){ g<-seq(lo,hi,length=npts); cdf<-cumtrapz(g,pdf(g)); cdf<-cdf/cdf[length(cdf)]
keep<-!duplicated(cdf) # a saturated CDF grid has ties; drop them so approx() is well defined
approx(cdf[keep],g[keep],runif(n),rule=2)$y }
gig_rng<-function(p,a,b,n){ hi<-gig_mom(p,a,b)["mean"]*30+20; inv_tf(function(t) gig_pdf(t,p,a,b), 1e-6, hi, n) }
# ---- Polya-Gamma ----
pg_rng<-function(b,c,n,K=200){ k<-1:K; denom<-(k-0.5)^2+c^2/(4*pi^2); G<-matrix(0,n,K); for(j in 1:K) G[,j]<-gam_shape(b,n); rowSums(sweep(G,2,denom,"/"))/(2*pi^2) }
pg1_pdf<-function(x,c,nt=200){ xp<-pmax(x,1e-300); S<-rep(0,length(xp)); for(nc in seq(1,2*nt-1,by=2)){ S<-S+((-1)^((nc-1)/2))*nc/sqrt(2*pi*xp^3)*exp(-nc^2/(8*xp)) }; cosh(c/2)*exp(-c^2*x/2)*ifelse(x>0,S,0) }
pg_lt<-function(t,b,c) cosh(c/2)^b/cosh(sqrt((t+c^2/2)/2))^b
pg_mean<-function(b,c) if(abs(c)<1e-9) b/4 else b/(2*c)*tanh(c/2)
# ---- Dirichlet ----
dir_rng<-function(al,n){ K<-length(al); G<-matrix(0,n,K); for(i in 1:K) G[,i]<-gam_shape(al[i],n); G/rowSums(G) }
dir_pdf<-function(x,al){ logB<-sum(lgamma(al))-lgamma(sum(al)); exp(colSums((al-1)*log(t(x)))-logB) }
emp_mom<-function(x){ m<-mean(x); v<-mean((x-m)^2); c(mean=m, var=v, skew=mean((x-m)^3)/v^1.5, exkurt=mean((x-m)^4)/v^2-3) }
panel <- function(name, xs, pdf, cdf_closed, samp, m_an, show_mom=TRUE){
par(mfrow=c(1,3), mar=c(4,4,3,1))
plot(xs, pdf, type="l", lwd=2.2, col=BLUE, main=paste0(name," | PDF"), xlab="x", ylab="f(x)")
cdf_num <- cumtrapz(xs, pdf)
plot(xs, cdf_closed, type="l", lwd=2.2, col=BLUE, main="CDF (line = closed form, dashes = integral of PDF)", xlab="x", ylab="F(x)", ylim=c(0,1.02)); lines(xs, cdf_num, col=RED, lty=2, lwd=1.4)
lo<-min(xs); hi<-max(xs); edges<-seq(lo,hi,length=61); bw<-edges[2]-edges[1]
h<-hist(samp[samp>=lo & samp<=hi], breaks=edges, plot=FALSE)
plot(edges[-1]-bw/2, h$counts/(length(samp)*bw), type="h", lwd=6, col="#cfe3f6", main=sprintf("RNG vs PDF (N=%s)", format(length(samp),big.mark=",")), xlab="x", ylab="density"); lines(xs, pdf, col=BLUE, lwd=2)
par(mfrow=c(1,1))
cat(sprintf(" max|closed-form CDF - integrated CDF| = %.2e\n", max(abs(cdf_closed-cdf_num))))
if(show_mom){ em<-emp_mom(samp)
cat(sprintf(" moments mean %.3f (emp %.3f) | var %.3f (emp %.3f) | skew %s (emp %.3f) | ex.kurt %s (emp %.3f)\n",
m_an["mean"],em["mean"],m_an["var"],em["var"], ifelse(is.na(m_an["skew"]),"NA",sprintf("%.3f",m_an["skew"])),em["skew"],
ifelse(is.na(m_an["exkurt"]),"NA",sprintf("%.3f",m_an["exkurt"])),em["exkurt"])) }
}
cat("from-scratch R Bayesian-prior distributions + panel() ready\n")
from-scratch R Bayesian-prior distributions + panel() ready
1. Truncated & half priors¶
Truncated Normal (probit augmentation; inverse-CDF), Half-Normal, Half-Cauchy (Gelman scale prior), Half-t.
set.seed(1)
mu<-0.5; s<-1; a<--1; b<-3; xs<-seq(-1.5,3.5,length=500)
panel("Truncated Normal(0.5,1;[-1,3])", xs, tn_pdf(xs,mu,s,a,b), tn_cdf(xs,mu,s,a,b), tn_rng(mu,s,a,b,1.5e5), tn_mom(mu,s,a,b))
s<-1.5; xs<-seq(0,6,length=500)
panel("Half-Normal(1.5)", xs, hn_pdf(xs,s), hn_cdf(xs,s), hn_rng(s,1.5e5), hn_mom(s))
s<-1.2; xs<-seq(0,14,length=500)
panel("Half-Cauchy(1.2)", xs, hc_pdf(xs,s), hc_cdf(xs,s), hc_rng(s,1.5e5), c(mean=NA,var=NA,skew=NA,exkurt=NA), show_mom=FALSE)
cat(" (Half-Cauchy: no moments; its heavy tail is what makes it a good weakly-informative scale prior.)\n")
# scale-prior comparison
xs<-seq(0,8,length=400)
plot(xs, hn_pdf(xs,1), type="l", lwd=2.2, col=BLUE, xlab="scale", ylab="prior density", main="Weakly-informative scale priors: tails set how large sigma may go")
lines(xs, ht_pdf(xs,1,3), lwd=2.2, col=GREEN); lines(xs, hc_pdf(xs,1), lwd=2.2, col=RED)
legend("topright", c("Half-Normal(1)","Half-t(1,nu=3)","Half-Cauchy(1)"), col=c(BLUE,GREEN,RED), lwd=2, bty="n", cex=.8)
max|closed-form CDF - integrated CDF| = 6.35e-04 moments mean 0.621 (emp 0.619) | var 0.729 (emp 0.730) | skew NA (emp 0.274) | ex.kurt NA (emp -0.524)
max|closed-form CDF - integrated CDF| = 2.59e-06 moments mean 1.197 (emp 1.193) | var 0.818 (emp 0.814) | skew 0.995 (emp 1.000) | ex.kurt 0.869 (emp 0.886)
max|closed-form CDF - integrated CDF| = 1.88e-05
(Half-Cauchy: no moments; its heavy tail is what makes it a good weakly-informative scale prior.)
2. Positive mixing distributions — Inverse-Gaussian & GIG¶
Inverse-Gaussian (Michael–Schucany–Haas sampler); GIG (Bessel-K density via base besselK; numerical inverse-transform).
mu<-1.5; lam<-2; xs<-seq(0.02,8,length=500)
panel("Inverse-Gaussian(mu=1.5, lam=2)", xs, ig_pdf(xs,mu,lam), ig_cdf(xs,mu,lam), ig_rng(mu,lam,1.5e5), ig_mom(mu,lam))
p<-0.5; a<-1.5; b<-2; xs<-seq(0.03,9,length=500)
panel("GIG(p=0.5, a=1.5, b=2)", xs, gig_pdf(xs,p,a,b), cumtrapz(xs,gig_pdf(xs,p,a,b)), gig_rng(p,a,b,1.5e5), gig_mom(p,a,b))
# GIG special cases: Gamma (b->0) and Inverse-Gaussian (p=-1/2)
xs<-seq(0.02,8,length=400)
cat(sprintf("\nGIG special cases:\n max|GIG(p=2,a=2,b->0) - Gamma(2,1)| = %.2e\n max|GIG(p=-1/2,a=2,b=2) - InvGaussian(1,2)| = %.2e\n",
max(abs(gig_pdf(xs,2,2,1e-6)-dgamma(xs,2,scale=1))), max(abs(gig_pdf(xs,-0.5,2,2)-ig_pdf(xs,1,2)))))
plot(xs, gig_pdf(xs,2,2,1e-6), type="l", lwd=3, col=adjustcolor(BLUE,0.5), ylim=c(0,1), xlab="x", ylab="f(x)", main="GIG umbrella: Gamma & Inverse-Gaussian limits")
lines(xs, dgamma(xs,2,scale=1), col=RED, lwd=1.5, lty=2); lines(xs, gig_pdf(xs,-0.5,2,2), lwd=3, col=adjustcolor(GREEN,0.5)); lines(xs, ig_pdf(xs,1,2), col=ORANGE, lwd=1.5, lty=2)
legend("topright", c("GIG(b->0)","= Gamma","GIG(p=-1/2)","= Inv-Gaussian"), col=c(BLUE,RED,GREEN,ORANGE), lwd=2, lty=c(1,2,1,2), bty="n", cex=.75)
max|closed-form CDF - integrated CDF| = 5.76e-05 moments mean 1.500 (emp 1.500) | var 1.688 (emp 1.697) | skew 2.598 (emp 2.638) | ex.kurt 11.250 (emp 12.127)
max|closed-form CDF - integrated CDF| = 0.00e+00 moments mean 1.821 (emp 1.822) | var 1.659 (emp 1.647) | skew 1.830 (emp 1.790) | ex.kurt 5.312 (emp 4.954)
GIG special cases: max|GIG(p=2,a=2,b->0) - Gamma(2,1)| = 4.80e-07 max|GIG(p=-1/2,a=2,b=2) - InvGaussian(1,2)| = 4.44e-16
3. The augmentation centrepiece — Pólya-Gamma¶
$\mathrm{PG}(b,c)$ makes logistic likelihoods conditionally Gaussian. Sampled by the Gamma-sum representation; validated by the $b=1$ series density, the mean, and the Laplace transform.
par(mfrow=c(1,2), mar=c(4,4,3,1))
xx<-seq(1e-3,2.2,length=400)
plot(xx, pg1_pdf(xx,0), type="l", lwd=2.2, col=BLUE, xlim=c(0,2.2), xlab="omega", ylab="density", main="PG(1,c): Gamma-sum sampler vs series density")
for(cc in c(0,2)){ om<-pg_rng(1,cc,2e5); h<-hist(om[om<2.2], breaks=seq(0,2.2,length=70), plot=FALSE); col<-if(cc==0) BLUE else RED
points(h$mids, h$counts/(length(om)*(2.2/69)), pch=20, col=adjustcolor(col,0.4), cex=.5); lines(xx, pg1_pdf(xx,cc), col=col, lwd=2.2); abline(v=pg_mean(1,cc), col=col, lty=3) }
legend("topright", c("PG(1,0)","PG(1,2)"), col=c(BLUE,RED), lwd=2, bty="n", cex=.8)
t<-seq(0,8,length=100); om<-pg_rng(2,1.5,3e5); elt<-sapply(t, function(tt) mean(exp(-tt*om))); tlt<-pg_lt(t,2,1.5)
plot(t, tlt, type="l", lwd=2, col=RED, xlab="t", ylab="E[exp(-t*omega)]", main="PG(2,1.5) Laplace transform"); points(t, elt, pch=20, col=BLUE, cex=.5)
par(mfrow=c(1,1))
xg<-seq(1e-3,8,length=2e5)
cat(sprintf("PG(1,0) series density integral = %.4f; PG mean(1,2) = %.4f (emp %.4f); max|emp-theory Laplace| = %.2e\n",
cumtrapz(xg,pg1_pdf(xg,0))[length(xg)], pg_mean(1,2), mean(pg_rng(1,2,2e5)), max(abs(elt-tlt))))
PG(1,0) series density integral = 1.0000; PG mean(1,2) = 0.1904 (emp 0.1899); max|emp-theory Laplace| = 4.19e-05
4. The simplex prior — Dirichlet¶
The conjugate prior on probability vectors; sampled by normalising Gammas; its marginals are Beta and the concentration controls sparsity.
to2d<-function(P) list(x=P[,2]+0.5*P[,3], y=(sqrt(3)/2)*P[,3])
par(mfrow=c(1,3), mar=c(2,2,3,1))
cfg<-list(list(c(0.4,0.4,0.4),"alpha=(0.4,0.4,0.4) sparse -> corners",BLUE), list(c(1,1,1),"alpha=(1,1,1) uniform",GREEN), list(c(6,6,6),"alpha=(6,6,6) concentrated",RED))
for(cf in cfg){ P<-dir_rng(cf[[1]],4000); xy<-to2d(P); plot(xy$x, xy$y, pch=20, col=adjustcolor(cf[[3]],0.35), cex=.4, axes=FALSE, xlab="", ylab="", main=cf[[2]], asp=1); lines(c(0,1,0.5,0),c(0,0,sqrt(3)/2,0), lwd=1) }
par(mfrow=c(1,1))
al<-c(2,3,5); P<-dir_rng(al,2e5); a0<-sum(al)
cat(sprintf("Marginal x1 ~ Beta(%d, %d): empirical mean %.4f vs analytical %.4f; var %.5f vs %.5f\n", al[1], a0-al[1], mean(P[,1]), al[1]/a0, var(P[,1]), al[1]*(a0-al[1])/(a0^2*(a0+1))))
pts<-rbind(c(0.2,0.3,0.5), c(0.1,0.6,0.3))
cat(sprintf("pdf at two simplex points: %.4f, %.4f\n", dir_pdf(pts,al)[1], dir_pdf(pts,al)[2]))
Marginal x1 ~ Beta(2, 8): empirical mean 0.2006 vs analytical 0.2000; var 0.01469 vs 0.01455
pdf at two simplex points: 8.5050, 2.2045
5. Augmentation in action¶
(a) The Laplace (Bayesian LASSO) prior as a Normal with Exponentially-mixed variance; (b) the Pólya-Gamma logistic identity.
par(mfrow=c(1,2), mar=c(4,4,3,1))
lam<-1; tau<--log(runif(4e5))*(2/lam^2); beta<-sqrt(tau)*norm_rng(0,1,4e5)
edges<-seq(-6,6,length=120); bw<-edges[2]-edges[1]; h<-hist(beta[abs(beta)<6], breaks=edges, plot=FALSE)
plot(h$mids, h$counts/(length(beta)*bw), type="h", lwd=3, col="#cfe3f6", xlab="beta", ylab="density", main="Bayesian LASSO: N(0,tau), tau~Exp = Laplace")
xx<-seq(-6,6,length=300); lines(xx, 0.5*lam*exp(-lam*abs(xx)), col=RED, lwd=2.2)
legend("topright", c("N(0,tau) mixture","Laplace(0,1/lam)"), col=c("#cfe3f6",RED), lwd=c(4,2), bty="n", cex=.75)
psi<-seq(-6,6,length=240); om<-pg_rng(1,0,6e4)
mix<-0.5*exp(psi/2)*sapply(psi, function(p) mean(exp(-om*p^2/2))); sig<-exp(psi)/(1+exp(psi))
plot(psi, sig, type="l", lwd=3, col=adjustcolor(BLUE,0.6), xlab="psi", ylab="value", main="Polya-Gamma identity: logistic = Gaussian mixture")
lines(psi, mix, col=RED, lwd=1.8, lty=2)
legend("topleft", c("logistic sigma(psi)","1/2 e^(psi/2) E_PG[e^(-omega psi^2/2)]"), col=c(BLUE,RED), lwd=2, lty=c(1,2), bty="n", cex=.7)
par(mfrow=c(1,1))
cat(sprintf("max|logistic - PG mixture| = %.2e (Monte-Carlo error). Conditional on omega, the logistic is Gaussian in psi.\n", max(abs(sig-mix))))
max|logistic - PG mixture| = 1.04e-02 (Monte-Carlo error). Conditional on omega, the logistic is Gaussian in psi.
6. Timing — exact transforms vs numerical samplers¶
From-scratch R vs base R. Inverse-CDF, folding, MSH, Gamma-sum and normalised-Gammas are all fast; only the GIG needs a numerical CDF grid.
best <- function(f, min_t=0.05){ reps<-0L; s<-proc.time()[3]; repeat{ force(f()); reps<-reps+1L; el<-proc.time()[3]-s; if(el>=min_t && reps>=2L) break }; el/reps }
N<-2e5
b<-list(
list("TruncNormal", function() tn_rng(0.5,1,-1,3,N), function() { al<-pnorm(-1.5); be<-pnorm(2.5); 0.5+qnorm(al+runif(N)*(be-al)) }, "inverse-CDF"),
list("HalfNormal", function() hn_rng(1.5,N), function() abs(rnorm(N,0,1.5)), "|Normal|"),
list("HalfCauchy", function() hc_rng(1.2,N), function() abs(rcauchy(N,0,1.2)), "|Cauchy|"),
list("InvGaussian", function() ig_rng(1.5,2,N), function() NA, "Michael-Schucany-Haas"),
list("GIG", function() gig_rng(0.5,1.5,2,N), function() NA, "numerical inverse-CDF"),
list("PolyaGamma", function() pg_rng(1,1.5,N), function() NA, "Gamma-sum (200 terms)"),
list("Dirichlet3", function() dir_rng(c(2,3,5),N), function() NA, "normalised Gammas"))
res<-data.frame()
for(x in b){ tf<-best(x[[2]]); has_ref<-length(suppressWarnings(tryCatch(x[[3]](),error=function(e) NA)))>=N
tr<-if(has_ref) best(x[[3]]) else NA; res<-rbind(res, data.frame(dist=x[[1]], scratch_Mps=N/tf/1e6, R_Mps=if(has_ref) N/tr/1e6 else NA, method=x[[4]])) }
print(format(res, digits=3))
cols_bar<-ifelse(is.na(res$R_Mps), ORANGE, BLUE)
barplot(res$scratch_Mps, names.arg=res$dist, las=2, log="y", col=cols_bar, main="From-scratch RNG throughput (orange = no base-R equivalent), log scale", ylab="million samples / sec")
legend("topright", c("has base-R","no base-R equivalent"), fill=c(BLUE,ORANGE), bty="n", cex=.8)
# summary text computed from the measured table -- never hardcode a timing claim
cat(sprintf("\nThe closed-form transforms (inverse-CDF, folding, MSH) run at %.0f-%.0f M draws/s. The GIG must build\n",
min(res$scratch_Mps[1:3]), max(res$scratch_Mps[1:3])))
cat(sprintf("a CDF grid, and the Polya-Gamma is the slowest in the set at %.3f M draws/s -- its Gamma-sum needs\n",
min(res$scratch_Mps)))
cat("200 Gamma draws per variate, which is the standing cost of the augmentation that makes a logistic\nlikelihood conjugate.\n")
dist scratch_Mps R_Mps method elapsed TruncNormal 30.000 32 inverse-CDF elapsed1 HalfNormal 13.333 28 |Normal| elapsed2 HalfCauchy 26.667 36 |Cauchy| elapsed3 InvGaussian 10.000 NA Michael-Schucany-Haas elapsed4 GIG 2.500 NA numerical inverse-CDF elapsed5 PolyaGamma 0.022 NA Gamma-sum (200 terms) elapsed6 Dirichlet3 1.379 NA normalised Gammas
The closed-form transforms (inverse-CDF, folding, MSH) run at 13-30 M draws/s. The GIG must build
a CDF grid, and the Polya-Gamma is the slowest in the set at 0.022 M draws/s -- its Gamma-sum needs
200 Gamma draws per variate, which is the standing cost of the augmentation that makes a logistic likelihood conjugate.
7. Summary¶
Every Bayesian-prior and augmentation law's from-scratch R implementation recovers its CDF by integrating its own PDF, matches its closed-form moments against large samples, and — for the density-less Pólya-Gamma — is validated by the $b=1$ series density, the mean, and the Laplace transform; the Dirichlet by its Beta marginals. GIG additionally reduces to the Gamma and Inverse-Gaussian in its limiting cases. A cross-language corroboration of the Python results.
These are the distributions that make Bayesian computation work: constrained and weakly-informative priors, the positive mixing laws behind scale-mixture shrinkage, the Pólya-Gamma that linearises logistic likelihoods, and the Dirichlet on the simplex. Next in the catalog: multivariate distributions.