Author: Dr. Merwan Roudane
Date: 2026-02-01

# Introduction to Quantile-on-Quantile Regression

#### Dr. Merwan Roudane

#### 2026-02-01

## Overview

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.

### Why Quantile-on-Quantile Regression?

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:

1. Modeling the quantile of Y (the dependent variable) as the outcome
2. Examining the effect of different quantiles of X (the independent variable)
3. Producing coefficients indexed by both \(\theta\) (quantile of Y) and \(\tau\) (quantile of X)

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?

### The Original Application: Oil Prices and Stock Returns

Sim and Zhou (2015) applied this methodology to examine the relationship between oil price shocks and US stock returns. They found that:

1. Large negative oil price shocks (low \(\tau\)) can positively affect US equities when the stock market is performing well (high \(\theta\))
2. Positive oil price shocks have weak effects regardless of market conditions
3. The relationship is asymmetric and depends on both the nature of oil shocks and market state

## Installation

    # Install from CRAN (when available)
    install.packages("QuantileOnQuantile")

    # Install from GitHub
    # install.packages("devtools")
    devtools::install_github("merwanroudane/qq")

## Quick Start

### Basic Usage

    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

### Summary Statistics

    # 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.0000

## Visualization

The package provides several interactive visualization options using plotly.

### 3D Surface Plot

The 3D surface plot is the signature visualization of the QQ approach, showing how coefficients vary across both dimensions.

    # Coefficient surface with MATLAB-style Jet colorscale
    plot_qq_3d(result, type = "coefficient", colorscale = "Jet")

    # R-squared surface with Viridis colorscale
    plot_qq_3d(result, type = "rsquared", colorscale = "Viridis")

    # P-value surface
    plot_qq_3d(result, type = "pvalue", colorscale = "Plasma")

### Available Color Scales

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")

- Jet: MATLAB-style rainbow (blue -> cyan -> green -> yellow -> red)
- BlueRed: Diverging scale, useful for coefficients centered around zero
- Viridis: Perceptually uniform, colorblind-friendly
- Plasma: High contrast, perceptually uniform

### Heatmaps

Heatmaps provide a 2D view of the results:

    # Coefficient heatmap
    plot_qq_heatmap(result, type = "coefficient", colorscale = "Viridis")

    # R-squared heatmap
    plot_qq_heatmap(result, type = "rsquared", colorscale = "Plasma")

    # P-value heatmap
    plot_qq_heatmap(result, type = "pvalue", colorscale = "Jet")

### Contour Plot

Contour plots show level curves of the coefficient surface:

    plot_qq_contour(result, colorscale = "Jet", show_labels = TRUE)

### Quantile Correlation

The correlation heatmap shows the relationship between quantiles of both variables:

    plot_qq_correlation(y, x, quantiles = seq(0.1, 0.9, by = 0.1))

## Detailed Example: Simulating Asymmetric Relationships

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 81

## Working with Results

### Extracting Results

The 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.0161

### Exporting Results

    # Export to CSV
    qq_export(result_oil, file.path(tempdir(), "qq_results.csv"))

## Customizing Quantile Grids

You 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: 361

## Methodology Details

### The QQ Model

The 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.

### Estimation

The estimation proceeds by:

1. For each \(\tau\) (quantile of X): subset data where X <= quantile(X, \(\tau\))
2. For each \(\theta\) (quantile of Y): perform quantile regression
3. Extract coefficients and compute pseudo R-squared

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.

## Comparison with Standard Methods

### OLS vs Quantile Regression vs QQ

Method | What it estimates | Captures heterogeneity in…
--- | --- | ---
OLS | E[Y | X]
Quantile Regression | Q_theta[Y | X]
QQ Regression | Q_theta[Y | X_tau]

### When to Use QQ Regression

Use QQ regression when you suspect that:

1. The effect of X on Y varies across the distribution of Y (e.g., bull vs bear markets)
2. The effect differs for large vs small values of X (e.g., large vs small shocks)
3. There is asymmetry (e.g., positive vs negative shocks have different effects)
4. You want to understand the complete dependence structure between two variables

## References

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.

## Session Info

    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