Skip to main content

Nonparametric, time-varying and quantile causality tests (NPCQ, TVNCQ, TVGC) with journal-quality tables and plots

Project description

nonparmtvcsq

Nonparametric, time-varying and quantile causality tests in Python — with publication-quality tables and visualisations.

One library for three causality toolkits used across the applied-econometrics literature, faithfully reproduced from their R and Stata sources and unified behind one clean API.

Tool What it does Source it reproduces
NPCQ  np_quantile_causality Nonparametric causality-in-quantiles (mean / variance), single sample R package nonParQuantileCausality (Balcilar) — validated to machine precision
TVNCQ tv_np_quantile_causality Time-varying (rolling-window) causality-in-quantiles → time×quantile surface Olasehinde-Williams/Özkan/Adebayo papers (2023–2026)
TVGC  tvgc Time-varying Granger causality (forward / rolling / recursive-evolving Wald) Stata command tvgc (Otero & Baum); Shi–Phillips–Hurn

Author: Dr Merwan Roudane · merwanroudane920@gmail.com · GitHub @merwanroudane · Repo https://github.com/merwanroudane/nonparmtvcsq


Table of contents

  1. Why this package
  2. Installation
  3. 60-second quickstart
  4. The methods & the maths
  5. API reference & full syntax
  6. Mapping from R / Stata to Python
  7. Reproducing the reference papers
  8. Compatibility & validation
  9. Performance notes
  10. References
  11. Citation
  12. License

Why this package

The three reference papers all use the same engine — the Jeong–Härdle–Wang (2012) nonparametric causality-in-quantiles test, extended by Balcilar et al. (2018) to mean and variance, and made time-varying by rolling windows. The R package only ships the single-sample test; the time-varying layer lived in ad-hoc scripts; and tvgc (the parametric time-varying Granger test) lived in Stata. nonparmtvcsq puts all of it in one Python library with:

  • exact numerical compatibility with the R source (see docs/COMPATIBILITY.md);
  • beautiful, journal-ready output — Parula/Jet/Turbo colormaps, 3-D surfaces, heatmaps, contour plots and LaTeX/HTML tables;
  • a single, documented, well-tested API.

Installation

# from source (this folder)
pip install .

# or editable for development
pip install -e ".[test]"

Requirements: Python ≥ 3.9, numpy, scipy, pandas, statsmodels, matplotlib, plotly (all installed automatically).

import nonparmtvcsq as ntc
print(ntc.__version__)   # 0.1.0

60-second quickstart

import numpy as np
import pandas as pd
import nonparmtvcsq as ntc

# bundled daily Gold/Oil prices (3,237 obs) -> log returns
df  = np.log(ntc.load_gold_oil()).diff().dropna()
oil, gold = df["Oil"].values, df["Gold"].values

# 1) NPCQ: does Oil_{t-1} cause Gold_t across quantiles? (mean & variance)
m = ntc.np_quantile_causality(oil, gold, type="mean")
v = ntc.np_quantile_causality(oil, gold, type="variance")
print(m.summary())            # journal-style console table
m.plot(savepath="npcq.png")   # statistic vs quantile + 5% CV band

# 2) TVNCQ: time-varying version -> a (time x quantile) surface
tv = ntc.tv_np_quantile_causality(oil, gold, type="mean",
                                  window=250, step=10,
                                  time_index=df.index.values)
tv.plot_heatmap(savepath="tvncq.png")          # Parula heatmap
tv.plot_surface().write_html("tvncq_3d.html")  # interactive 3-D

# 3) TVGC: time-varying Granger causality (forward/rolling/recursive)
g = ntc.tvgc(pd.DataFrame({"Gold": gold, "Oil": oil}),
             depvar="Gold", causes=["Oil"], p=2, d=1, boot=199, seed=1)
print(g.summary())
g.plot(var="Oil", savepath="tvgc.png")

The methods & the maths

NPCQ — causality in quantiles

For a candidate cause x and effect y, define Q_θ(y_t | ·) the θ-conditional quantile. The null of no causality at quantile θ is

H0:  P{ F_{y_t | x_{t-1}, y_{t-1}}( Q_θ(y_t | y_{t-1}) | x_{t-1}, y_{t-1} ) = θ } = 1

i.e. once you know y_{t-1}, adding x_{t-1} does not change the conditional quantile. The feasible test statistic is a kernel quadratic form in the indicator residuals ε̂ = 1{y_t ≤ Q̂_θ(y_t|y_{t-1})} − θ,

Ĵ_θ  ∝  ε̂ᵀ K ε̂ ,   K = Gaussian product kernel over (y_{t-1}, x_{t-1})

rescaled to an asymptotic N(0, 1) statistic, so |stat| > 1.96 rejects at 5%. With type="variance" the series are squared first (the Balcilar et al. m = 2 extension), giving causality in variance / volatility.

The conditional quantile Q̂_θ is a local-linear quantile regression (quantreg::rq in R; an exact LP here), the bandwidth is the Ruppert–Sheather– Wand plug-in (KernSmooth::dpill, ported exactly), and one lag of each series is used (first-order Granger set-up).

TVNCQ — time-varying causality in quantiles

Structural change breaks the constant-relationship assumption. The hybrid rolling-windows approach computes the NPCQ statistic inside every fixed-width sub-sample, producing a (n_windows × n_quantiles) surface of statistics indexed by (time, quantile) — exactly the 3-D plots in the reference papers.

TVGC — time-varying Granger causality

A parametric counterpart: in a lag-augmented VAR (Toda–Yamamoto, so it is robust to unit roots) it computes three sequences of Wald statistics for H0: x does not Granger-cause y:

  • Forward expanding (FE) — start fixed, end grows;
  • Rolling (RO) — fixed-width window slides;
  • Recursive evolving (RE) — supremum over all start/end pairs (Shi–Phillips– Hurn sup test).

Critical values come from a fixed-regressor wild bootstrap under the null.


API reference & full syntax

1. np_quantile_causality — NPCQ

ntc.np_quantile_causality(
    x, y,                     # 1-D arrays, same length: cause x, effect y
    type="mean",              # "mean" (m=1) or "variance" (m=2)
    q=None,                   # quantile grid; default np.arange(0.05, 0.96, 0.05)
    hm=None,                  # fixed base bandwidth; None -> dpill plug-in
    cv=1.96,                  # reference critical value (5% two-sided N(0,1))
    method="lp",              # "lp" exact (R-compatible) | "sm" statsmodels (fast)
) -> NPCQResult
res = ntc.np_quantile_causality(oil, gold, type="mean",
                                q=np.arange(0.1, 0.91, 0.1))
res.statistic            # np.ndarray of N(0,1) statistics, one per quantile
res.quantiles            # the grid
res.bandwidth            # base bandwidth used
res.significant_quantiles(cv=1.96)   # quantiles where H0 is rejected
res.to_frame()           # tidy DataFrame (quantile, statistic, reject_5pct)
print(res.summary())     # console table with significance stars
res.plot()               # matplotlib figure
res.plot_interactive()   # plotly figure

2. tv_np_quantile_causality — TVNCQ

ntc.tv_np_quantile_causality(
    x, y,
    type="mean",
    q=None,                   # default np.arange(0.05, 0.96, 0.05)
    window=None,              # window width; default floor(0.25*T)
    hm=None,                  # fixed base bandwidth (None -> per-window dpill)
    time_index=None,          # dates/labels aligned to x; surface uses window end
    step=1,                   # step between window starts
    cv=1.96,
    method="sm",              # "sm" fast default | "lp" exact (slower)
    progress=False,           # True -> progress line; or a callable(i, n)
) -> TVNCQResult
tv = ntc.tv_np_quantile_causality(oil, gold, window=250, step=10,
                                  time_index=df.index.values, progress=True)
tv.statistic             # (n_windows, n_quantiles) matrix
tv.reject_share()        # fraction of windows rejecting H0, per quantile
tv.to_frame()            # long tidy DataFrame (time, quantile, statistic, reject)
tv.plot_heatmap()        # static Parula heatmap (matplotlib)
tv.plot_contour()        # filled contour with CV iso-line
tv.plot_surface()        # interactive 3-D Plotly surface

3. tvgc — time-varying Granger causality

ntc.tvgc(
    data,                     # DataFrame (cols = series) or 2-D array
    depvar=None,              # dependent variable name (default: first column)
    causes=None,              # candidate causes (default: all other columns)
    p=2,                      # VAR lag order (the lags actually tested)
    d=1,                      # extra Toda-Yamamoto augmentation lags (untested)
    window=None,              # min / rolling width; default floor(0.2*T)
    trend=False,              # include a linear trend (besides the constant)
    robust=False,             # White heteroskedasticity-robust covariance
    boot=199,                 # bootstrap reps for critical values (>=20)
    seed=None,
    step=1,                   # step in the test sequences
    boot_step=None,           # coarser step inside the bootstrap (auto if None)
    time_index=None,          # labels aligned to data rows
) -> TVGCResult
g = ntc.tvgc(pd.DataFrame({"Gold": gold, "Oil": oil}),
             depvar="Gold", causes=["Oil"], p=2, d=1, boot=199, seed=1)
g.sup                    # (n_causes, 3) max Wald for FE / RO / RE
g.cv90, g.cv95, g.cv99   # bootstrap critical values, same shape
g.to_frame()             # summary DataFrame
g.series_frame("Oil")    # per-window FE/RO/RE series for one cause
print(g.summary())       # console table with bootstrap CVs and significance
g.plot(var="Oil")        # 3-panel FE/RO/RE figure with CV lines

Result objects

Class Key attributes Methods
NPCQResult statistic, quantiles, bandwidth, type, n, cv to_frame, summary, significant_quantiles, plot, plot_interactive
TVNCQResult statistic (T×Q), quantiles, window, time_index to_frame, reject_share, plot_heatmap, plot_contour, plot_surface
TVGCResult sup, cv90/95/99, forward/rolling/recursive, ends to_frame, series_frame, summary, plot

Tables

from nonparmtvcsq import tables
print(tables.npcq_summary_table(res))         # plain text (also res.summary())
print(tables.npcq_to_latex(res, label="t1"))  # LaTeX table environment
print(tables.tvncq_summary_table(tv))
print(tables.tvgc_summary_table(g))
print(tables.tvgc_to_latex(g))
res.to_frame().to_html("npcq.html")           # any pandas exporter works

Plots & colors

Static figures use a serif "journal" theme; surfaces/heatmaps/contours default to Parula. All plot functions accept savepath=... (300 dpi PNG/PDF/SVG).

from nonparmtvcsq import plotting, colors

plotting.plot_npcq(res, cv=1.96, color="#1f4e79", savepath="fig.pdf")
plotting.plot_tvncq_heatmap(tv, colorscale="Parula")
plotting.plot_tvncq_surface(tv, colorscale="Turbo")
plotting.plot_tvgc(g, var="Oil")
plotting.show_colormaps()                      # gradient reference card

colors.parula_colors(64)      # 64 hex stops of MATLAB R2014b Parula
colors.matlab_jet_colors(n)   # jet
colors.turbo_colors(n)        # turbo
colors.bluered_colors(n)      # diverging blue-white-red
colors.sinha_colors(n)        # Sinha et al. (2023) style palette
colors.get_cmap("Parula")     # matplotlib ListedColormap
colors.resolve_colorscale("Parula", 32)        # Plotly colorscale

Datasets

ntc.load_gold_oil()                # DataFrame: Gold, Oil (3237 daily obs)
ntc.simulate_causal(n=400, beta=0.5, type="mean", seed=1)   # (x, y) with x->y

Numerical building blocks

ntc.dpill(x, y, gridsize=None)                 # RSW plug-in bandwidth (= R)
ntc.silverman_bandwidth(x)
ntc.lprq2(x, y, h, tau, x0, method="lp")        # local-linear quantile smoother
ntc.weighted_quantile_regression(X, y, tau, weights=None, method="lp")

Mapping from R / Stata to Python

R nonParQuantileCausality → Python

R Python
np_quantile_causality(x, y, type="mean", q, hm) ntc.np_quantile_causality(x, y, type="mean", q=..., hm=...)
plot(obj) obj.plot()
lprq2_(...) (internal) ntc.lprq2(...)
KernSmooth::dpill(x, y) ntc.dpill(x, y)
data(gold_oil) ntc.load_gold_oil()

Stata tvgc → Python

Stata tvgc y x, ... Python ntc.tvgc(...)
p(2) / d(1) p=2 / d=1
window(k) window=k
trend trend=True
robust robust=True
boot(199) / seed(1) boot=199 / seed=1
r(gcres) (max Wald FE/RO/RE) g.sup
r(gccv95) g.cv95
graph panels g.plot(var=...)

Reproducing the reference papers

The package implements the methodology of:

  1. Olasehinde-Williams, Olanipekun & Özkan (2024), Computational Economics 64:947–977 — Stock Market Response to Quantitative Easing: … Rolling Windows Nonparametric Causality-in-Quantiles. → use tv_np_quantile_causality.
  2. Özkan & Adebayo (2026), Environment, Development and Sustainability…time-varying asymmetric impact of fossil-fuel price volatility on high cleantech investments (TVNCinQ). → tv_np_quantile_causality(type="mean"/"variance").
  3. Olasehinde-Williams, Özkan & Akadiri (2023), Environmental Science and Pollution Research 30:55326–55339 — Effects of climate policy uncertainty on sustainable investment.np_quantile_causality + tv_np_quantile_causality.

A full worked script is in examples/quickstart.py.


Compatibility & validation

Validated against R by sourcing the nonParQuantileCausality tar.gz directly:

  • dpill bandwidth matches KernSmooth::dpill to ~1e-10;
  • lprq2 matches quantreg::rq to ~1e-15;
  • NPCQ statistic matches to machine precision at the majority of quantiles; small deviations only at "tie" quantiles where the local fit interpolates the evaluation point (an instability present in R too).

Details and the reproduction recipe: docs/COMPATIBILITY.md. Enforced by tests/test_r_compat.py.


Performance notes

  • NPCQ with method="lp" solves one small LP per (observation × quantile); for T ≈ 200 and a 0.05 grid that's a few seconds. Use a coarser quantile grid for speed.
  • TVNCQ multiplies that by the number of windows — it defaults to method="sm" (fast statsmodels solver). Use step>1 and a moderate window to keep runs short; pass progress=True to watch it.
  • TVGC's recursive-evolving sequence is O(T²) Wald fits per cause; the bootstrap reuses it boot times with an automatic coarser boot_step. Lower boot (e.g. 99) while exploring, raise it (199–999) for final results.

References

  • Jeong, K., Härdle, W. K., & Wang, S. (2012). Nonparametric quantile causality. Econometric Theory, 28(4), 861–887.
  • Balcilar, M., Gupta, R., & Pierdzioch, C. (2016). Does uncertainty move the gold price? Resources Policy, 49, 74–80. https://doi.org/10.1016/j.resourpol.2016.04.004
  • Balcilar, M., Gupta, R., Kyei, C., & Wohar, M. E. (2016). Open Economies Review, 27(2), 229–250. https://doi.org/10.1007/s11079-016-9388-x
  • Nishiyama, Y., Hitomi, K., Kawasaki, Y., & Jeong, K. (2011). A consistent nonparametric test for nonlinear causality. J. Econometrics, 165(1), 112–127.
  • Shi, S., Phillips, P. C. B., & Hurn, S. (2018). Change detection and the causal impact of the yield curve. J. Time Series Analysis, 39(6), 966–987.
  • Shi, S., Hurn, S., & Phillips, P. C. B. (2020). Causal change detection in possibly integrated systems. Empirical Economics, 59, 595–625.
  • Toda, H. Y., & Yamamoto, T. (1995). Statistical inference in vector autoregressions with possibly integrated processes. J. Econometrics, 66.
  • Ruppert, D., Sheather, S. J., & Wand, M. P. (1995). An effective bandwidth selector for local least squares regression. JASA, 90(432), 1257–1270.
  • Olasehinde-Williams, G., Olanipekun, I., & Özkan, O. (2024). Computational Economics, 64, 947–977.
  • Özkan, O., & Adebayo, T. S. (2026). Environment, Development and Sustainability.
  • Olasehinde-Williams, G., Özkan, O., & Akadiri, S. S. (2023). Environmental Science and Pollution Research, 30, 55326–55339.

Citation

@software{roudane_nonparmtvcsq_2026,
  author  = {Roudane, Merwan},
  title   = {nonparmtvcsq: Nonparametric, time-varying and quantile causality
             tests in Python},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/merwanroudane/nonparmtvcsq}
}

License

MIT © 2026 Merwan Roudane. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nonparmtvcsq-0.1.0.tar.gz (55.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nonparmtvcsq-0.1.0-py3-none-any.whl (48.7 kB view details)

Uploaded Python 3

File details

Details for the file nonparmtvcsq-0.1.0.tar.gz.

File metadata

  • Download URL: nonparmtvcsq-0.1.0.tar.gz
  • Upload date:
  • Size: 55.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for nonparmtvcsq-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c7d35d183aa7a445bba5b71a1db492fa7315a53b921f965490f6eb44fce6ec11
MD5 4927ed64953b0fff60b1b293e17cb045
BLAKE2b-256 00bd4fac3656128f5728ab10cb981cba0d67c023dbcc1f98a8b98517f413c2bf

See more details on using hashes here.

File details

Details for the file nonparmtvcsq-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: nonparmtvcsq-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 48.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for nonparmtvcsq-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 85241d1fcabd4e4ca05a939d5f7069d18d681c0375b2695cd72d7b8ae6734f81
MD5 2d7f9d3e4f9d38c1ad0b185268403281
BLAKE2b-256 fb1736e9dd247834df2ea450c8852f0d5584759228656b9355e4d18f3cf8b79e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page