The QuantileOnQuantile package implements the Quantile-on-Quantile (QQ) regression methodology developed by Sim and Zhou (2015). This approach estimates the effect that quantiles of one variable have on quantiles of another, capturing the dependence between their distributions.
Traditional regression methods like OLS estimate the effect of independent variables on the conditional mean of the dependent variable. Quantile regression extends this by estimating effects on conditional quantiles. However, both approaches treat the independent variable as a single entity, ignoring the possibility that the relationship may vary depending on whether the independent variable takes extreme or moderate values.
The QQ approach addresses this limitation by:
This allows researchers to ask questions like: - Do large positive shocks in X affect Y differently than large negative shocks? - Does the relationship between X and Y depend on market conditions (bull vs bear)? - Is the dependence between variables stronger in the tails of their distributions?
Sim and Zhou (2015) applied this methodology to examine the relationship between oil price shocks and US stock returns. They found that:
library(QuantileOnQuantile)
#> QuantileOnQuantile v1.0.0
#> Based on: Sim & Zhou (2015) doi:10.1016/j.jbankfin.2015.01.013
#> Type ?QuantileOnQuantile for help, or vignette('introduction') for tutorial.
# Generate example data
set.seed(42)
n <- 300
x <- rnorm(n)
y <- 0.5 * x + 0.3 * x * (x < 0) + rnorm(n, sd = 0.5) # Asymmetric relationship
# Run QQ regression
result <- qq_regression(y, x, verbose = FALSE)
# Print summary
print(result)
#>
#> Quantile-on-Quantile Regression Results
#> ========================================
#>
#> Call:
#> qq_regression(y = y, x = x, verbose = FALSE)
#>
#> Number of observations: 300
#> Y quantiles: 19 levels
#> X quantiles: 19 levels
#> Total combinations: 361
#>
#> Coefficient Summary:
#> Mean: 0.8204
#> Min: 0.6102
#> Max: 1.4603
#>
#> Significant at 0.05: 360 of 361# Get detailed summary
summary(result)
#>
#> Quantile-on-Quantile Regression Summary
#> =======================================
#>
#> Data:
#> Observations: 300
#> Y quantiles: 19 levels
#> X quantiles: 19 levels
#> Total combinations: 361
#> Complete results: 361
#>
#> Coefficient Statistics:
#> Mean: 0.8204
#> Median: 0.7868
#> Min: 0.6102
#> Max: 1.4603
#> SD: 0.1375
#>
#> R-squared Statistics:
#> Mean: 0.3289
#> Median: 0.3232
#> Min: 0.2083
#> Max: 0.527
#>
#> Significance:
#> p < 0.05: 360 of 361
#> p < 0.01: 356 of 361
# Get statistics as data frame
stats <- qq_statistics(result)
print(stats)
#> Statistic Value
#> 1 Mean Coefficient 0.8204
#> 2 Median Coefficient 0.7868
#> 3 Min Coefficient 0.6102
#> 4 Max Coefficient 1.4603
#> 5 SD Coefficient 0.1375
#> 6 Mean R-squared 0.3289
#> 7 Median R-squared 0.3232
#> 8 Min R-squared 0.2083
#> 9 Max R-squared 0.5270
#> 10 Significant (p < 0.05) 360.0000
#> 11 Total Results 361.0000The package provides several interactive visualization options using plotly.
The 3D surface plot is the signature visualization of the QQ approach, showing how coefficients vary across both dimensions.
The package supports several color scales:
qq_colorscales()
#>
#> Available Color Scales for QQ Regression Plots
#> ===============================================
#>
#> Jet : MATLAB-style rainbow (blue -> cyan -> green -> yellow -> red)
#> BlueRed : Diverging scale (blue = low, red = high)
#> Viridis : Perceptually uniform, colorblind-friendly
#> Plasma : Perceptually uniform, high contrast
#>
#> Usage:
#> plot_qq_3d(result, colorscale = "Jet")
#> plot_qq_heatmap(result, colorscale = "Viridis")Heatmaps provide a 2D view of the results:
Contour plots show level curves of the coefficient surface:
Let’s create data that mimics the oil-stock relationship from the original paper:
set.seed(2015)
n <- 500
# Generate "oil shocks"
oil_shock <- rnorm(n)
# Generate "stock returns" with asymmetric response
stock_return <- numeric(n)
for (i in 1:n) {
# Base return
base_return <- 0.01
# Asymmetric effect
if (oil_shock[i] < quantile(oil_shock, 0.3)) {
# Large negative oil shocks have positive effect
effect <- -0.02 * oil_shock[i]
} else if (oil_shock[i] > quantile(oil_shock, 0.7)) {
# Large positive oil shocks have weak effect
effect <- -0.005 * oil_shock[i]
} else {
# Moderate shocks have little effect
effect <- -0.001 * oil_shock[i]
}
stock_return[i] <- base_return + effect + rnorm(1, sd = 0.04)
}
# Run QQ regression with finer grid
result_oil <- qq_regression(
y = stock_return,
x = oil_shock,
y_quantiles = seq(0.1, 0.9, by = 0.1),
x_quantiles = seq(0.1, 0.9, by = 0.1),
verbose = FALSE
)
# Print summary
print(result_oil)
#>
#> Quantile-on-Quantile Regression Results
#> ========================================
#>
#> Call:
#> qq_regression(y = stock_return, x = oil_shock, y_quantiles = seq(0.1,
#> 0.9, by = 0.1), x_quantiles = seq(0.1, 0.9, by = 0.1), verbose = FALSE)
#>
#> Number of observations: 500
#> Y quantiles: 9 levels
#> X quantiles: 9 levels
#> Total combinations: 81
#>
#> Coefficient Summary:
#> Mean: -0.0234
#> Min: -0.046
#> Max: -0.0031
#>
#> Significant at 0.05: 69 of 81The results are stored in a data frame:
# Access raw results
head(result_oil$results)
#> y_quantile x_quantile coefficient std_error t_value p_value
#> 1 0.1 0.1 -0.003145838 0.02413509 -0.1303429 0.89683985
#> 2 0.2 0.1 -0.031636163 0.02586211 -1.2232631 0.22720195
#> 3 0.3 0.1 -0.031180344 0.02435317 -1.2803401 0.20657833
#> 4 0.4 0.1 -0.023552074 0.02156900 -1.0919408 0.28030970
#> 5 0.5 0.1 -0.042512061 0.02129035 -1.9967764 0.05153707
#> 6 0.6 0.1 -0.037150726 0.01716210 -2.1646958 0.03541426
#> r_squared method
#> 1 0.00796267 quantile_regression
#> 2 0.02912363 quantile_regression
#> 3 0.03286816 quantile_regression
#> 4 0.04825456 quantile_regression
#> 5 0.05179744 quantile_regression
#> 6 0.07560317 quantile_regression
# Convert to matrix format
coef_matrix <- qq_to_matrix(result_oil, type = "coefficient")
print(round(coef_matrix, 4))
#> 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9
#> 0.1 -0.0031 -0.0303 -0.0194 -0.0300 -0.0194 -0.0154 -0.0131 -0.0190 -0.0177
#> 0.2 -0.0316 -0.0373 -0.0283 -0.0265 -0.0178 -0.0127 -0.0127 -0.0168 -0.0131
#> 0.3 -0.0312 -0.0460 -0.0367 -0.0230 -0.0146 -0.0110 -0.0098 -0.0110 -0.0098
#> 0.4 -0.0236 -0.0422 -0.0326 -0.0303 -0.0168 -0.0168 -0.0144 -0.0144 -0.0128
#> 0.5 -0.0425 -0.0428 -0.0261 -0.0280 -0.0204 -0.0188 -0.0175 -0.0168 -0.0145
#> 0.6 -0.0372 -0.0372 -0.0320 -0.0308 -0.0199 -0.0193 -0.0153 -0.0152 -0.0132
#> 0.7 -0.0308 -0.0382 -0.0339 -0.0305 -0.0245 -0.0229 -0.0165 -0.0164 -0.0126
#> 0.8 -0.0307 -0.0357 -0.0341 -0.0342 -0.0252 -0.0247 -0.0221 -0.0177 -0.0147
#> 0.9 -0.0179 -0.0261 -0.0244 -0.0368 -0.0263 -0.0266 -0.0257 -0.0188 -0.0161You can customize the quantile grid for more or less granularity:
# Coarse grid (faster computation)
result_coarse <- qq_regression(
y = stock_return,
x = oil_shock,
y_quantiles = seq(0.2, 0.8, by = 0.2),
x_quantiles = seq(0.2, 0.8, by = 0.2),
verbose = FALSE
)
# Fine grid (more detail, slower)
result_fine <- qq_regression(
y = stock_return,
x = oil_shock,
y_quantiles = seq(0.05, 0.95, by = 0.05),
x_quantiles = seq(0.05, 0.95, by = 0.05),
verbose = FALSE
)
cat("Coarse grid combinations:", nrow(result_coarse$results), "\n")
#> Coarse grid combinations: 16
cat("Fine grid combinations:", nrow(result_fine$results), "\n")
#> Fine grid combinations: 361The QQ approach is based on the following model:
\[r_t^\theta = \beta^\theta(Oil_t) + \alpha^\theta r_{t-1} + v_t^\theta\]
where \(r_t^\theta\) is the \(\theta\)-quantile of the return and \(\beta^\theta(\cdot)\) is an unknown function.
Taking a Taylor expansion around the \(\tau\)-quantile of oil shocks (\(Oil^\tau\)):
\[\beta^\theta(Oil_t) \approx \beta_0(\theta, \tau) + \beta_1(\theta, \tau)(Oil_t - Oil^\tau)\]
The key insight is that \(\beta_0(\theta, \tau)\) and \(\beta_1(\theta, \tau)\) are doubly indexed by \(\theta\) and \(\tau\), capturing the dependence between both distributions.
The estimation proceeds by:
The pseudo R-squared is computed as:
\[R^2 = 1 - \frac{\sum \rho_\theta(y - \hat{y})}{\sum \rho_\theta(y - Q_\theta(y))}\]
where \(\rho_\theta(u) = u(\theta - I(u < 0))\) is the check function.
| Method | What it estimates | Captures heterogeneity in… |
|---|---|---|
| OLS | E[Y | X] |
| Quantile Regression | Q_theta[Y | X] |
| QQ Regression | Q_theta[Y | X_tau] |
Use QQ regression when you suspect that:
Sim, N. and Zhou, H. (2015). Oil Prices, US Stock Return, and the Dependence Between Their Quantiles. Journal of Banking & Finance, 55, 1-12. doi:10.1016/j.jbankfin.2015.01.013
Koenker, R. (2005). Quantile Regression. Cambridge University Press.
Koenker, R. and Xiao, Z. (2006). Quantile Autoregression. Journal of the American Statistical Association, 101, 980-990.
sessionInfo()
#> R version 4.5.2 (2025-10-31 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26100)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=C LC_CTYPE=French_France.utf8
#> [3] LC_MONETARY=French_France.utf8 LC_NUMERIC=C
#> [5] LC_TIME=French_France.utf8
#>
#> time zone: Europe/Paris
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] QuantileOnQuantile_1.0.3
#>
#> loaded via a namespace (and not attached):
#> [1] Matrix_1.7-4 gtable_0.3.6 jsonlite_2.0.0 dplyr_1.1.4
#> [5] compiler_4.5.2 tidyselect_1.2.1 MatrixModels_0.5-4 tidyr_1.3.2
#> [9] jquerylib_0.1.4 splines_4.5.2 scales_1.4.0 yaml_2.3.12
#> [13] fastmap_1.2.0 lattice_0.22-7 ggplot2_4.0.1 R6_2.6.1
#> [17] generics_0.1.4 knitr_1.51 htmlwidgets_1.6.4 MASS_7.3-65
#> [21] tibble_3.3.1 bslib_0.9.0 pillar_1.11.1 RColorBrewer_1.1-3
#> [25] rlang_1.1.7 cachem_1.1.0 xfun_0.56 sass_0.4.10
#> [29] S7_0.2.1 lazyeval_0.2.2 otel_0.2.0 viridisLite_0.4.2
#> [33] plotly_4.12.0 cli_3.6.5 magrittr_2.0.4 digest_0.6.39
#> [37] grid_4.5.2 rstudioapi_0.18.0 quantreg_6.1 lifecycle_1.0.5
#> [41] vctrs_0.7.0 data.table_1.18.0 SparseM_1.84-2 evaluate_1.0.5
#> [45] glue_1.8.0 farver_2.1.2 survival_3.8-3 purrr_1.2.1
#> [49] httr_1.4.7 rmarkdown_2.30 pkgconfig_2.0.3 tools_4.5.2
#> [53] htmltools_0.5.9