Bounded-Outcome Regression via Kumaraswamy¶
Overview¶
A regression on a \((0, 1)\)-bounded response with the Kumaraswamy distribution as the likelihood. Unlike the Beta distribution, its CDF and quantile are available in elementary power functions, which can make inverse-CDF sampling inexpensive. The covariate enters through the first shape parameter under a log link, so the fitted response stays inside the unit interval for every value of the predictor.
Generative structure:
The Kumaraswamy density is \(f(y; a, b) = a b\, y^{a - 1} (1 - y^a)^{b - 1}\) on \(y \in (0, 1)\).
QVR source¶
# Bounded-Outcome Regression via Kumaraswamy
#
# A regression on a (0, 1)-bounded response. The Kumaraswamy
# distribution is a close cousin of the Beta with a closed-form
# CDF and quantile function, which makes it preferable for models
# that need cheap inverse-CDF sampling or quantile-based
# inference. The first shape parameter carries the linear
# predictor through a log link, so a covariate moves the response
# without ever leaving the unit interval; the second shape
# parameter is global and carries a HalfNormal prior that
# concentrates on near-uniform shapes.
#
# Generative structure:
#
# b ~ HalfNormal(2) global second shape
# gamma_0 ~ Normal(0, 1) intercept
# gamma_1 ~ Normal(0, 1) slope
# a_n = exp(gamma_0 + gamma_1 * x_n) per-row first shape
# y_n ~ Kumaraswamy(a_n, b)
#
# The predictor x is exogenous data supplied at fit time through
# the observations dict. Resp is the plate the responses are
# observed over and Val is the value space of the returned
# per-row shape.
#
# Reference: [Kumaraswamy 1980](https://doi.org/10.1016/0022-1694(80)90036-0).
object Resp : FinSet 64
object Val : Real 1
program kumaraswamy_regression : Resp -> Val
sample b <- HalfNormal(2.0)
sample gamma_0 <- Normal(0.0, 1.0)
sample gamma_1 <- Normal(0.0, 1.0)
let eta = gamma_0 + gamma_1 * x
let a = exp(eta)
observe y : Resp <- Kumaraswamy(a, b)
return a
export kumaraswamy_regression
Walkthrough¶
object Resp : FinSet 64 is the plate the responses are observed over, and object Val : Real 1 is the value space of what the program returns, so the signature reads Resp -> Val. The domain names the plate; the codomain names the space the returned value lives in, which for a real scalar is Real 1.
sample b <- HalfNormal(2.0) gives the global second shape parameter a weakly-informative positive prior. The uniform Kumaraswamy occurs at \(a = b = 1\); a half-Normal is not centred specifically on that value. The two coefficients carry unit-Normal priors, let eta = gamma_0 + gamma_1 * x binds the linear predictor over the exogenous covariate x, and let a = exp(eta) maps it into the positive first shape parameter. return a exposes the per-row fitted shape. The implied mean is \(E[Y_n] = b\,B(1 + 1/a_n, b)\); this parameterization has no standard one-number concentration equal to 1 / (1 + 1 / b).
Try it¶
The short fits below demonstrate the API. Assess convergence with multiple chains and diagnostics before interpreting a posterior.
Generating synthetic data¶
Pick ground truth for all three latent sites, build the per-row first shape from the covariate, and forward-generate the bounded responses from those same values, so the point the harness scores is self-consistent.
import torch
from quivers.dsl import load
torch.manual_seed(0)
prog = load("docs/examples/source/kumaraswamy_bounded_outcome.qvr")
model = prog.morphism
n_resp = 64
true_b = 1.6
true_gamma_0 = 0.3
true_gamma_1 = 0.7
x = torch.randn(n_resp)
a_true = torch.exp(true_gamma_0 + true_gamma_1 * x)
u = torch.rand(n_resp).clamp(1e-6, 1.0 - 1e-6)
y = (1.0 - (1.0 - u) ** (1.0 / true_b)) ** (1.0 / a_true)
observations = {"y": y, "x": x}
x_in = torch.zeros(n_resp, 1)
The response is generated through the Kumaraswamy quantile function \(Q(u) = (1 - (1 - u)^{1/b})^{1/a}\), the closed form that motivates the family in the first place.
SVI fit¶
from quivers.inference import AutoNormalGuide, ELBO, SVI
torch.manual_seed(1)
guide = AutoNormalGuide(model, observed_names={"y", "x"})
optim = torch.optim.Adam(
list(model.parameters()) + list(guide.parameters()), lr=5e-2,
)
svi = SVI(model, guide, optim, ELBO(num_particles=1))
losses = [svi.step(x_in, observations) for _ in range(300)]
print(f"initial loss: {losses[0]:.2f}")
print(f"final loss: {losses[-1]:.2f}")
NUTS posterior¶
from quivers.inference import MCMC, NUTSKernel
torch.manual_seed(2)
kernel = NUTSKernel(step_size=0.05, max_tree_depth=6, target_accept=0.8)
mc = MCMC(kernel, num_warmup=100, num_samples=100, num_chains=1)
result = mc.run(model, x_in, observations)
print(f"acceptance: {float(result.acceptance_rates.mean()):.2f}")
print(f"divergences: {int(result.divergence_counts.sum())}")
Use cases¶
Any bounded-outcome regression where the analyst needs efficient quantile evaluation: financial fraction-of-volume forecasting, biology fraction-of-population modelling, fraction-positive A/B testing where the closed-form quantile speeds Bayesian decision-making. Drop-in alternative to Beta regression when downstream operations are quantile-based rather than moment-based.
References¶
- Ponnambalam Kumaraswamy. 1980. A generalized probability density function for double-bounded random processes. Journal of Hydrology, 46(1-2):79-88.