In quantile regression the conditional quantile function of a
response \(y\) given covariates \(x\) is \(Q(p \mid
x) = x^\top\beta(p)\), \(0 < p <
1\). Standard quantile regression estimates one quantile at a
time; quantile regression coefficients modeling (QRCM, Frumento and
Bottai, 2016) instead describes the coefficients as parametric functions
of \(p\), \[\beta(p \mid \theta) = \theta\, b(p),\]
where \(b(p) = (b_1(p), \dots,
b_k(p))^\top\) are known functions and \(\theta\) is a \(q
\times k\) matrix of parameters. All quantiles are estimated
simultaneously by minimizing the integrated loss \[L(\theta) = \int_0^1 \sum_{i=1}^n \rho_p\{y_i -
x_i^\top\theta\, b(p)\}\,dp,\] which is a smooth convex function
of \(\theta\) whose gradient and
Hessian are available in closed form. The package ‘qrcm’ implements this
estimator (qrcm::iqr()); ‘qrcmNP’ adds penalized estimation
for covariate selection (piqr()) and nonlinear parametric
models (niqr()).
piqr()Sottile et al. (2020) proposed to select covariates by minimizing \[L(\theta) + \lambda\, \mathrm{pen}(\theta)\] with standardized \(x\) and \(b(p)\). Since \(\beta_j(p \mid \theta) \equiv 0\) if and only if \(\theta_{j1} = \dots = \theta_{jk} = 0\), selection uses the information on all quantiles at once. Two penalties are available:
penalty = "lasso" (default): \(\lambda\sum_{j,h} w_j|\theta_{jh}|\), the
penalty of Sottile et al. (2020, eq. 3.4);penalty = "group": \(\lambda\sum_j
w_j\sqrt{k_j}\,\|\theta_{j\cdot}\|_2\), a group lasso that keeps
or removes each covariate as a whole.The weights \(w_j\) are set by
penalty.factor (default 1; 0 leaves a covariate
unpenalized). The intercept is never penalized.
We simulate data from the model \(\beta_0(p) = 1 + \Phi^{-1}(p)\), \(\beta_1(p) = 1 + 2p\), \(\beta_2(p) = 1\), with three irrelevant covariates.
set.seed(1234)
n <- 400
d <- data.frame(x1 = rexp(n), x2 = runif(n, 0, 5), x3 = rnorm(n),
x4 = runif(n), x5 = rbinom(n, 1, 0.5))
u <- runif(n)
d$y <- 1 + qnorm(u) + (1 + 2 * u) * d$x1 + d$x2The coefficient functions are modelled by shifted Legendre
polynomials of degree 2 (slp(p, 2), plus a constant).
piqr() computes the whole path of solutions, starting from
\(\lambda_{max}\) (only the intercept
in the model):
fit <- piqr(y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~ slp(p, 2), data = d,
nlambda = 40)
fit
#>
#> Call:
#> piqr(formula = y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~slp(p,
#> 2), data = d, nlambda = 40)
#>
#> Penalty: lasso n. of lambda values: 40
#>
#> Df Npar ObjFunction Lambda
#> lambda1 1 3 334.6 80.490000
#> lambda2 3 6 259.0 63.560000
#> lambda3 3 6 225.7 50.190000
#> lambda4 3 7 210.7 39.630000
#> lambda5 3 7 196.1 31.300000
#> lambda6 3 7 188.5 24.710000
#> lambda7 3 8 183.6 19.520000
#> lambda8 3 8 180.1 15.410000
#> lambda9 4 10 177.8 12.170000
#> lambda10 4 10 176.3 9.609000
#> lambda11 4 10 175.4 7.588000
#> lambda12 4 9 174.9 5.992000
#> lambda13 5 10 174.5 4.731000
#> lambda14 5 11 174.3 3.736000
#> lambda15 5 11 174.1 2.950000
#> lambda16 5 11 174.1 2.330000
#> lambda17 5 11 174.0 1.840000
#> lambda18 5 11 174.0 1.453000
#> lambda19 5 11 173.9 1.147000
#> lambda20 6 13 173.9 0.905800
#> lambda21 6 13 173.8 0.715300
#> lambda22 6 15 173.8 0.564800
#> lambda23 6 14 173.8 0.446000
#> lambda24 6 15 173.7 0.352200
#> lambda25 6 17 173.7 0.278100
#> lambda26 6 17 173.7 0.219600
#> lambda27 6 17 173.7 0.173400
#> lambda28 6 17 173.7 0.136900
#> lambda29 6 17 173.7 0.108100
#> lambda30 6 17 173.7 0.085390
#> lambda31 6 17 173.7 0.067430
#> lambda32 6 17 173.7 0.053240
#> lambda33 6 17 173.6 0.042040
#> lambda34 6 17 173.6 0.033200
#> lambda35 6 17 173.6 0.026220
#> lambda36 6 18 173.6 0.020700
#> lambda37 6 18 173.6 0.016350
#> lambda38 6 18 173.6 0.012910
#> lambda39 6 18 173.6 0.010190
#> lambda40 6 18 173.6 0.008049gof.piqr() computes the AIC and BIC of Sottile et
al. (2020, Section 3.1), \(\log
L(\hat\theta_\lambda) + c_n\,df(\lambda)/n\), with \(c_n = 2\) (AIC) or \(c_n = \log n\) (BIC):
best
#>
#> Selection of lambda by BIC
#>
#> Selected lambda: 5.992 (position 12 )
#> Degrees of freedom: 9
#>
#> Coefficients:
#> (Intercept) slp1 slp2
#> (Intercept) -0.36617 1.65368 0.10827
#> x1 1.01840 0.83515 -0.13759
#> x2 0.94377 NA NA
#> x3 NA NA NA
#> x4 NA NA NA
#> x5 NA 0.09813 -0.10019Cross-validation of the integrated loss is also available:
cv <- cv_piqr(y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~ slp(p, 2), data = d,
nfolds = 5, nlambda = 40)
cv
#>
#> Cross-validated integrated loss (5 folds)
#>
#> Lambda Index Measure SE Df
#> min 4.731 13 0.4451 0.01506 5
#> 1se 15.410 8 0.4557 0.01121 3
plot(cv)fitg <- piqr(y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~ slp(p, 2), data = d,
nlambda = 40, penalty = "group")
bestg <- gof.piqr(fitg, plot = FALSE)
bestg
#>
#> Selection of lambda by BIC
#>
#> Selected lambda: 14.37 (position 8 )
#> Degrees of freedom: 9
#>
#> Coefficients:
#> (Intercept) slp1 slp2
#> (Intercept) 1.1980 1.5147 0.9307
#> x1 0.5802 0.7119 -0.4972
#> x2 0.3155 0.1943 -0.2793
#> x3 NA NA NA
#> x4 NA NA NA
#> x5 NA NA NAAs suggested by Sottile et al. (2020, Section 3.2), the penalized
estimator is used to select the covariates, and the selected model is
re-estimated without penalty. refit_piqr() does this with
qrcm::iqr(), which provides standard errors, Wald tests and
goodness-of-fit diagnostics (the standard errors do not account for the
selection step):
m <- refit_piqr(fit, pos.lambda = best$posMinLambda)
summary(m)
#>
#> Call: qrcm::iqr(formula = y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~slp(p,
#> 2), data = d, s = structure(c(1, 1, 1, 0, 0, 1, 1, 1, 1,
#> 0, 0, 1, 1, 1, 1, 0, 0, 1), dim = c(6L, 3L)))
#>
#> converged: TRUE
#> n. of iterations: 10
#> n. of free parameters: 12
#> n. of observations: 400
#>
#> ######################
#> ######################
#>
#> Coefficients:
#> (Intercept) slp1 slp2
#> (Intercept) -0.99415 1.84750 -0.01593
#> x1 1.21977 0.80738 -0.08405
#> x2 1.12352 -0.08471 0.03807
#> x3 NA NA NA
#> x4 NA NA NA
#> x5 0.08917 0.12613 -0.12671
#>
#> Standard errors:
#> (Intercept) slp1 slp2
#> (Intercept) 0.38322 0.21992 0.20119
#> x1 0.17633 0.09931 0.14888
#> x2 0.10656 0.06213 0.05731
#> x3 NA NA NA
#> x4 NA NA NA
#> x5 0.33200 0.19311 0.16572
#>
#> ######################
#> ######################
#>
#> Wald test for x:
#> chi-square df P(> chi)
#> (Intercept) 126.241 3 <2e-16 ***
#> x1 890.682 3 <2e-16 ***
#> x2 432.139 3 <2e-16 ***
#> x3 NA 0 NA
#> x4 NA 0 NA
#> x5 6.509 3 0.0893 .
#>
#>
#> Wald test for b(p):
#> chi-square df P(> chi)
#> (Intercept) 616.893 4 <2e-16 ***
#> slp1 784.854 4 <2e-16 ***
#> slp2 1.582 4 0.812
#>
#> Minimized loss function: 173.9093
summary(m, p = c(0.1, 0.5, 0.9))
#>
#> Call: qrcm::iqr(formula = y ~ x1 + x2 + x3 + x4 + x5, formula.p = ~slp(p,
#> 2), data = d, s = structure(c(1, 1, 1, 0, 0, 1, 1, 1, 1,
#> 0, 0, 1, 1, 1, 1, 0, 0, 1), dim = c(6L, 3L)))
#>
#> p = 0.1
#>
#> Coefficients:
#> Estimate std.err z value p(>|z|))
#> (Intercept) -0.61605 0.28968 -2.127 0.0334 *
#> x1 1.42664 0.12511 11.403 <2e-16 ***
#> x2 1.08602 0.08075 13.449 <2e-16 ***
#> x3 NA NA NA NA
#> x4 NA NA NA NA
#> x5 0.18281 0.25353 0.721 0.4709
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#>
#>
#> p = 0.5
#>
#> Coefficients:
#> Estimate std.err z value p(>|z|))
#> (Intercept) 0.87724 0.22157 3.959 7.52e-05 ***
#> x1 2.15323 0.16310 13.202 < 2e-16 ***
#> x2 0.98170 0.05992 16.385 < 2e-16 ***
#> x3 NA NA NA NA
#> x4 NA NA NA NA
#> x5 0.40536 0.17269 2.347 0.0189 *
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#>
#>
#> p = 0.9
#>
#> Coefficients:
#> Estimate std.err z value p(>|z|))
#> (Intercept) 2.33995 0.21604 10.831 <2e-16 ***
#> x1 2.71845 0.09903 27.451 <2e-16 ***
#> x2 0.95048 0.06296 15.097 <2e-16 ***
#> x3 NA NA NA NA
#> x4 NA NA NA NA
#> x5 0.38462 0.19612 1.961 0.0499 *
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Right-censored and left-truncated responses are specified with
survival::Surv(), as in qrcm::iqr():
cens <- rexp(n, 0.05)
d$time <- pmin(d$y + 5, cens)
d$status <- as.numeric(d$y + 5 <= cens)
fitc <- piqr(survival::Surv(time, status) ~ x1 + x2 + x3 + x4 + x5,
formula.p = ~ slp(p, 2), data = d, nlambda = 20)
gof.piqr(fitc, plot = FALSE)
#>
#> Selection of lambda by BIC
#>
#> Selected lambda: 2.723 (position 7 )
#> Degrees of freedom: 8
#>
#> Coefficients:
#> (Intercept) slp1 slp2
#> (Intercept) 4.223977 1.798755 0.079512
#> x1 1.069666 0.848069 NA
#> x2 1.016954 NA -0.002393
#> x3 NA NA NA
#> x4 NA NA NA
#> x5 NA NA -0.189861For each value of \(\lambda\),
piqr() uses a proximal Newton method: the
integrated loss is replaced by its second-order expansion (its gradient
and Hessian are available in closed form), and the resulting penalized
quadratic problem is solved by coordinate descent (soft-thresholding for
the lasso, group soft-thresholding for the group lasso) followed by an
exact “polishing” step on the selected coefficients; a backtracking line
search guarantees a decrease of the penalized objective. Along the path
the algorithm uses warm starts, the sequential strong rules of
Tibshirani et al. (2012) and a final check of the Karush-Kuhn-Tucker
conditions. The conditional CDF of each observation is found by
bisection on a grid of 1023 values of \(p\), denser in the tails, on which \(b(p)\) and its integrals are pre-computed
(as in ‘qrcm’). The algorithm of ‘qrcmNP’ 0.2.x is available with
method = "legacy".
niqr()Bottai and Cilluffo (2020) considered parametric models \(Q(p \mid x, \theta)\) of any
functional form, estimated by minimizing \[L_n(\theta) = \frac1n\sum_{i=1}^n\int_0^1
\rho_p\{y_i - Q(p \mid x_i, \theta)\}\,dp.\] In
niqr(), the model is specified by two functions:
fun(theta, p) returns the matrix of coefficient
functions \(\beta(p \mid \theta)\) (one
row for each value of p);fun2(beta, X) (optional) combines them with the
covariates, row by row; by default \(Q(p \mid
x) = x^\top\beta(p \mid \theta)\).Following Bottai and Cilluffo (2020, Section 6), the intercept can be modelled as a transformation of a normal quantile function, \(z = \Phi^{-1}(p)\), with polynomial effects of the covariates. Here the data have heavier tails than the normal distribution:
set.seed(1)
n <- 400
x <- runif(n)
z <- qnorm(runif(n))
y <- 2 + 0.5 * z + 0.1 * z^3 + (1 + pnorm(z)) * x
dn <- data.frame(y, x)
# Model 1: normal intercept
m1 <- niqr(y ~ x, data = dn, x0 = c(2, 0, 1, 0),
fun = function(theta, p) cbind(theta[1] + exp(theta[2]) * qnorm(p),
theta[3] + theta[4] * p))
# Model 2: a cubic term accounts for the kurtosis (exp() keeps it positive,
# so that the quantile function is increasing in p)
m2 <- niqr(y ~ x, data = dn, x0 = c(2, 0, -2, 1, 0),
fun = function(theta, p) {
z <- qnorm(p)
cbind(theta[1] + exp(theta[2]) * z + exp(theta[3]) * z^3, theta[4] + theta[5] * p)
})
gof.niqr(m1, m2)
#> npar loss AIC BIC AIC.log BIC.log
#> m1 4 0.2537794 109.5117 125.4776 -1.361290 -1.341333
#> m2 5 0.2528031 111.1212 131.0785 -1.362644 -1.337698
summary(m2)
#>
#> Call: niqr(formula = y ~ x, data = dn, fun = function(theta, p) {
#> z <- qnorm(p)
#> cbind(theta[1] + exp(theta[2]) * z + exp(theta[3]) * z^3,
#> theta[4] + theta[5] * p)
#> }, x0 = c(2, 0, -2, 1, 0))
#>
#> n. of observations: 400 n. of iterations: 4
#> Standard errors: sandwich
#>
#> Estimate Std. Error z value Pr(>|z|)
#> theta0 2.03442 0.07919 25.690 < 2e-16 ***
#> theta1 -0.59066 0.16849 -3.506 0.000456 ***
#> theta2 -2.22999 0.21187 -10.525 < 2e-16 ***
#> theta3 1.23175 0.26375 4.670 3.01e-06 ***
#> theta4 0.55099 0.42081 1.309 0.190417
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Minimized loss function: 0.2528031
#> AIC = 111.121 BIC = 131.079 AIC* = -1.36264 BIC* = -1.3377The log-criteria \(\mathrm{AIC}^* = \log
L_n + k/n\) and \(\mathrm{BIC}^* = \log
L_n + k\log(n)/(2n)\) (Bottai and Cilluffo, 2020, Section 3) are
reported as AIC.log and BIC.log; here AIC*
prefers the second model, while BIC* slightly prefers the simpler one.
The goodness-of-fit test below is more conclusive. The estimated
coefficient functions are plotted with pointwise 95% confidence
bands:
Under a correctly specified model the fitted values of the
conditional CDF are uniformly distributed; test.fit()
computes the Kolmogorov-Smirnov and Cramer-von Mises statistics, with
bootstrap p-values. The normal model is rejected, the model with heavy
tails is not:
The Hill quantile function (Bottai and Cilluffo, 2020, eq. 2) \[Q(p \mid \text{dose}) = \beta_0(p) + \frac{\beta_1(p) - \beta_0(p)}{1 + 10^{\beta_3(p) - \log_{10}\text{dose}}}\] is nonlinear in both the covariate and the parameters. Here the minimal response \(\beta_0(p)\) is normal, and the maximal response and the (log) dose at half the maximal effect are constant:
set.seed(2)
dose <- rep(10^seq(-2, 2, length.out = 20), each = 15)
fun.h <- function(theta, p) cbind(theta[1] + exp(theta[2]) * qnorm(p), theta[3], theta[4])
fun2.h <- function(beta, X) beta[, 1] + (beta[, 2] - beta[, 1]) / (1 + 10^(beta[, 3] - log10(X[, 2])))
yh <- fun2.h(fun.h(c(1, log(0.5), 10, 0), runif(length(dose))), cbind(1, dose))
mh <- niqr(yh ~ dose, fun = fun.h, fun2 = fun2.h, x0 = c(0, 0, 5, 0.5))
summary(mh)
#>
#> Call: niqr(formula = yh ~ dose, fun = fun.h, fun2 = fun2.h, x0 = c(0,
#> 0, 5, 0.5))
#>
#> n. of observations: 300 n. of iterations: 16
#> Standard errors: sandwich
#>
#> Estimate Std. Error z value Pr(>|z|)
#> theta0 1.003353 0.056302 17.821 <2e-16 ***
#> theta1 -0.632827 0.054445 -11.623 <2e-16 ***
#> theta2 10.001469 0.002756 3629.444 <2e-16 ***
#> theta3 0.003041 0.006897 0.441 0.659
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Minimized loss function: 0.073441
#> AIC = 30.0323 BIC = 44.8474 AIC* = -2.59794 BIC* = -2.57325dd <- data.frame(dose = 10^seq(-2, 2, length.out = 100))
qf <- predict(mh, type = "QF", p = c(0.05, 0.5, 0.95), newdata = dd, se = FALSE)
plot(dose, yh, log = "x", pch = 20, col = "gray", xlab = "dose", ylab = "response")
matlines(dd$dose, qf, lty = c(2, 1, 2), col = 1)The covariance matrix of \(\hat\theta\) is estimated by the sandwich
formula \(H^{-1}\Omega H^{-1}/n\)
(Bottai and Cilluffo, 2020, Theorem 3), where \(H\) is the Hessian of the expected loss,
\(H = n^{-1}\sum_i \partial_\theta
Q(p_i^*)\,\partial_\theta Q(p_i^*)^\top/Q'(p_i^*)\), \(p_i^* = F(y_i \mid x_i, \hat\theta)\). With
repeated measures, the argument cluster gives
cluster-robust standard errors; the (cluster) bootstrap used in the
paper is available with
control = niqr.control(se = "boot").