Skip to main content

tvccointreg

PyPI version Python versions License: MIT CI

Time-Varying-Coefficient regression and Generalized Cointegration in Python.

📦 PyPI: https://pypi.org/project/tvccointreg/ — install with pip install tvccointreg 🌐 Documentation site: https://merwanroudane.github.io/tvccointreg/

tvccointreg implements the generalized cointegration framework of

Hall, S. G., Swamy, P. A. V. B., & Tavlas, G. S. (2015). A Note on Generalizing the Concept of Cointegration.

together with the surrounding Swamy time-varying-coefficient (TVC) literature (Swamy & Mehta 1975; Swamy, Tavlas, Hall & co-authors 2003–2014; Granger 2008).

Conventional cointegration (Engle–Granger 1987) is an inherently linear concept: it can only recover a structural relationship if that relationship happens to be linear in unit-root variables. Most economic theory, however, implies nonlinear relationships among variables that are nonstationary but not necessarily unit-root. tvccointreg lets you:

  • estimate a model that is linear in variables but with time-varying coefficients — the Swamy–Mehta / Granger representation of any nonlinear relationship (eq. 7);
  • decompose each coefficient into a bias-free structural component plus omitted-variable bias and measurement-error bias using coefficient drivers (eq. 8);
  • test for generalized cointegration — i.e. whether the bias-free structural partial derivative is nonzero — with standard χ²/normal inference (no Dickey–Fuller tables);
  • produce journal-quality tables (text / LaTeX booktabs / HTML) and publication-quality plots with the MATLAB Parula colormap by default.

Table of contents


Installation

From PyPI:

pip install tvccointreg

# with the optional ADF stationarity diagnostic
pip install "tvccointreg[adf]"

Or from source:

git clone https://github.com/merwanroudane/tvccointreg.git
cd tvccointreg
pip install -e ".[dev]"      # editable install with test extras

Requirements: numpy, scipy, pandas, matplotlib. statsmodels is optional (used only for the ADF stationarity diagnostic and some examples).


The 60-second tour

from tvccointreg import TVCModel, DriverSpec
from tvccointreg.datasets import simulate_nonlinear_cointegration

# 1) A nonlinear relationship between nonstationary variables.
sim = simulate_nonlinear_cointegration(T=300, seed=1, nonlinearity=0.4)

# 2) Partition the drivers into the three sets (Assumption 1 / eq. 8).
spec = DriverSpec(
    names=list(sim.drivers.columns),
    bias_free=["x_lag", "y_lag"],   # true coefficient variation
    omitted=["w"],                  # omitted-variable bias
    measurement=[],                 # measurement-error bias
)

# 3) Fit by iteratively rescaled GLS.
res = TVCModel(sim.y, sim.X, sim.drivers, driver_spec=spec).fit()

# 4) Journal table + generalized cointegration test.
print(res.summary())
res.coint_test()
==============================================================================
      Time-Varying-Coefficient Regression  /  Generalized Cointegration
==============================================================================
Dep. variable: y                       No. observations: 300
No. coefficients: 2                    No. drivers: 3
Estimator: Iteratively rescaled GLS    Covariance: GLS
R-squared: 0.8311                      Log-likelihood: -78.07
Converged: True                        Resid ADF p-value: 0.0000
==============================================================================
Variable    Coef (mean)  Bias-free    Std.Err          t    p-value    G-Coint
------------------------------------------------------------------------------
x                0.3283     0.3392     0.0698     4.8614     0.0000 ***     Yes
==============================================================================
                   Signif.: *** p<0.01  ** p<0.05  * p<0.10
       Bias-free = structural derivative; G-Coint via Wald test on it.
==============================================================================

On this DGP the recovered bias-free coefficient correlates 0.99 with the true time-varying derivative, and the residuals are stationary (ADF p ≈ 0) — exactly the standard-inference result the paper proves.


Concepts: how the paper maps to the code

Paper (Hall–Swamy–Tavlas 2015) Symbol In tvccointreg
TVC model, eq. (7) y_t = γ_0t + γ_1t x_1t + … + γ_{K-1,t} x_{K-1,t} TVCModel(y, X, drivers)
Coefficient-driver eq., eq. (8) γ_jt = π_j0 + Σ_d π_jd z_dt + ε_jt DriverSpec(...) + the drivers matrix
Three components of a coefficient bias-free / omitted-variable / measurement-error DriverSpec(bias_free=, omitted=, measurement=)
Concentrated model y_t = w_t'π + u_t, u_t = Σ_j x_jt ε_jt estimation.build_design, estimation.irgls
Variance structure Var(u_t) = Σ_j σ_j² x_jt² estimation.variance_components (Hildreth–Houck–Swamy)
Consistent estimator (§3.3) iteratively rescaled GLS TVCModel.fit(method="irgls")
Bias-free component γ_jt^BF = π_j0 + Σ_{d∈BF} π_jd z_dt res.bias_free_coefficients()
Generalized cointegration, eqs. (5)–(6) ∂y/∂x ≠ 0 ⇔ bias-free component ≠ 0 res.coint_test()
Standard inference (§3.3) χ² Wald / normal, not Dickey–Fuller Wald + delta-method t in cointegration.py
Drivers vs. instruments (Table 1) drivers should be correlated with the misspecification the omitted / measurement sets

The definition, in one line

y and x are generalized-cointegrated if, holding all other relevant preexisting conditions w constant, the bias-free part of ∂y/∂x is nonzero.

Cointegration is thus a property of the real-world structural relationship, not of any particular statistical model — and it survives nonlinearity and omitted regressors.


The estimator

Substituting the driver equation (8) into the TVC model (7) gives a model that is linear in the stacked parameter vector π:

y_t = w_t' π + u_t ,   w_t = ( x_0t·z_t , x_1t·z_t , … ) ,   u_t = Σ_j x_jt ε_jt
Var(u_t | x_t) = Σ_j σ_j² x_jt²          (Hildreth–Houck–Swamy structure)

tvccointreg then:

  1. OLS start on the concentrated design W.
  2. Variance components σ_j² from a non-negative regression of squared residuals on squared regressors (scipy.optimize.nnls).
  3. GLS with weights 1/h_t, h_t = Σ_j σ_j² x_jt²; iterate to convergence (iteratively rescaled GLS).
  4. Recover the time-varying coefficients γ_jt = z_t'π_j + ε̂_jt, where the random part is the best linear predictor ε̂_jt = σ_j² x_jt u_t / h_t.
  5. Decompose γ_jt into bias-free / omitted / measurement parts using the driver-set masks, and test the bias-free block.

Inference uses the GLS covariance (W'Ω⁻¹W)⁻¹ (the paper's standard-inference result) or a heteroskedasticity-robust sandwich (cov_type="robust").

Note / honest caveat. As in the paper, validity hinges on Assumption 1 — that the chosen drivers genuinely span the bias components. This is an identifying assumption, not something the data can confirm; choose drivers that are plausibly correlated with the suspected misspecification (Table 1).


API reference

TVCModel(y, X, drivers, driver_spec=None, driver_sets=None, add_const=True, index=None)

Build a model. y (T,), X (T, K−1), drivers (T, m). A constant regressor and a constant driver are added automatically. Accepts NumPy arrays or pandas objects (column names are picked up automatically).

  • .fit(method="irgls", max_iter=100, tol=1e-8, cov_type="gls", verbose=False) → TVCResults.

DriverSpec(names, bias_free=[], omitted=[], measurement=[])

Three-set partition of the drivers. Any driver not listed defaults to bias_free. .describe() prints the partition.

TVCResults

Method Returns
`.summary(fmt="text" "latex"
.coef_table() per-regressor average coefficient frame
.coint_test(alpha=0.05, skip_const=True) tidy DataFrame of test results (and .coint_results_)
.tv_coefficients(include_random=True) T×K time-varying coefficients
.bias_free_coefficients() T×K bias-free (structural) coefficients
.components(regressor) bias-free / omitted / measurement / random / total
.coefficient_se() pointwise standard errors
.diagnostics() R², log-lik, convergence, residual ADF
.plot_coefficients(...) TVC paths with CI bands
.plot_decomposition(regressor) the three-component decomposition
.plot_fit() actual vs fitted + residuals
.plot_coint_heatmap() Parula heatmap of bias-free paths

Datasets

  • simulate_nonlinear_cointegration(T, seed, nonlinearity, omitted, measurement_error)
  • simulate_spurious(T, seed)

Colormaps

parula_colors(n), matlab_jet_colors(n), turbo_colors(n), bluered_colors(n), sinha_colors(n), resolve_colorscale("Parula", n), get_cmap("parula").


Detailed syntax

Using raw NumPy arrays and reading off a specific test:

import numpy as np
from tvccointreg import TVCModel, DriverSpec

res = TVCModel(y, X, Z,                       # y:(T,), X:(T,K-1), Z:(T,m)
               driver_sets={"bias_free": ["z1"],
                            "omitted":   ["z2"],
                            "measurement": ["z3"]}).fit()

ct = res.coint_test()
print(ct.loc["x1", "cointegrated"], ct.loc["x1", "p_value"])

Robust covariance and OLS (homoskedastic) comparison:

res_robust = TVCModel(y, X, Z, driver_spec=spec).fit(cov_type="robust")
res_ols    = TVCModel(y, X, Z, driver_spec=spec).fit(method="ols")

Exporting a LaTeX table for a paper:

with open("table1.tex", "w") as f:
    f.write(res.summary(fmt="latex"))

Pulling the time-varying coefficient path of one regressor:

gamma_x = res.tv_coefficients()["x"]          # pandas Series indexed by t
bf_x    = res.bias_free_coefficients()["x"]

Visualizations

All plots default to the MATLAB Parula colormap and return a matplotlib Figure.

plot_coefficients() plot_decomposition("x")
coefficients decomposition
plot_fit() plot_coint_heatmap()
fit heatmap

Worked example: spurious vs. real

examples/spurious_vs_real.py reproduces the central point of the paper:

================ SPURIOUS (independent random walks) ================
Naive OLS slope = 1.125  (p = 5.97e-46)   <- spuriously 'significant'
Generalized cointegration: NO (p = 0.708)

================ GENUINE generalized cointegration ================
Generalized cointegration: YES (p = 1.34e-35)

A naive OLS regression of one random walk on another is wildly "significant", yet the generalized-cointegration test on the bias-free component correctly finds no structural relationship.


Empirical application: the US consumption function (real data)

examples/real_data_consumption.py applies the method to real US quarterly macro data (statsmodels macrodata, 1959Q1–2009Q3, 202 obs after lagging). We model the long-run relationship between real personal consumption and real disposable income (both in logs, both strongly trending / I(1)):

import numpy as np, pandas as pd
from statsmodels.datasets import macrodata
from tvccointreg import TVCModel, DriverSpec

d = macrodata.load_pandas().data
d.index = pd.period_range("1959Q1", periods=len(d), freq="Q")
zscore = lambda s: (s - s.mean()) / s.std(ddof=0)

y = np.log(d["realcons"]).rename("log_cons")
X = np.log(d[["realdpi"]]).rename(columns={"realdpi": "log_inc"})
drivers = pd.DataFrame({
    "inc_lag":  zscore(np.log(d["realdpi"]).shift(1)),
    "trend":    zscore(pd.Series(np.arange(len(d)), index=d.index)),
    "realint":  zscore(d["realint"]),
    "unemp":    zscore(d["unemp"]),
    "cons_lag": zscore(np.log(d["realcons"]).shift(1)),
})
df = pd.concat([y, X, drivers], axis=1).dropna()

spec = DriverSpec(
    names=["inc_lag", "trend", "realint", "unemp", "cons_lag"],
    bias_free=["inc_lag", "trend"],   # true elasticity variation
    omitted=["realint", "unemp"],     # interest-rate / labour-market channels
    measurement=["cons_lag"],         # dynamics / measurement
)
res = TVCModel(df["log_cons"], df[["log_inc"]], df[spec.names],
               driver_spec=spec, index=df.index.astype(str)).fit()
print(res.summary())
print(res.coint_test())

Result (verbatim output):

==============================================================================
      Time-Varying-Coefficient Regression  /  Generalized Cointegration
==============================================================================
Dep. variable: log_cons                No. observations: 202
No. coefficients: 2                    No. drivers: 5
Estimator: Iteratively rescaled GLS    Covariance: GLS
R-squared: 0.9999                      Log-likelihood: 752.32
Converged: True                        Resid ADF p-value: 0.0000
==============================================================================
Variable    Coef (mean)  Bias-free    Std.Err          t    p-value    G-Coint
------------------------------------------------------------------------------
log_inc          0.3886     0.3886     0.0528     7.3626     0.0000 ***     Yes
==============================================================================
regressor avg_bias_free std_err t_stat wald df p_value cointegrated
log_inc 0.3886 0.0528 7.3626 56.9504 3 0.0000 True

Reading the result.

  • Generalized cointegration is confirmed between consumption and income (Wald = 56.95, 3 df, p ≈ 0): the bias-free structural elasticity is firmly nonzero, so the long-run relationship is genuine, not spurious.
  • Standard inference is valid here: the composite residuals are stationary (ADF p = 1.9 × 10⁻⁷), exactly the condition under which Hall–Swamy–Tavlas show the TVC test uses ordinary χ²/normal critical values rather than Dickey–Fuller ones.
  • The bias-free income elasticity drifts down over the sample, from 0.43 (1959Q2) to 0.34 (2009Q3) — the direct income channel after the dynamics (cons_lag) and omitted business-cycle conditions (realint, unemp) are stripped out into the bias terms. A declining direct sensitivity of consumption to current income over five decades is consistent with greater consumption smoothing / financial deepening.
Bias-free income elasticity over time Coefficient decomposition
mpc decomp

Run it yourself:

python examples/real_data_consumption.py

Multiple regressors

The framework handles several regressors at once — each gets its own time-varying coefficient and its own cointegration verdict. examples/consumption_multivariate.py estimates a two-regressor consumption function (disposable income and the real interest rate) on the same data:

Regressor Bias-free coef t Wald p-value Cointegrated
log_realDPI (income) 0.6666 *** 10.13 111.40 <0.0001 Yes
realint (real rate) −0.0007 ** −2.26 8.37 0.039 Yes

Positive income elasticity (~0.67) and a negative real-rate effect (intertemporal substitution); R² = 0.9998, residuals stationary (ADF p = 3.5×10⁻⁹). See the rendered results on the documentation site.


Testing

pip install -e ".[dev]"
pytest -q

The suite checks coefficient recovery, that the three components sum exactly to the total coefficient, detection of genuine generalized cointegration, rejection of spurious relationships, and the Parula palette/colorscale helpers.


Publishing (maintainer notes)

The package is PyPI-ready (python -m build + twine check both pass).

First release — manual upload with an API token

  1. Create accounts on TestPyPI and PyPI, and generate an API token for each (Account settings → API tokens).
  2. Build and check:
    python -m build
    python -m twine check dist/*
    
  3. Dry-run on TestPyPI first, then install from there to confirm:
    python -m twine upload --repository testpypi dist/*
    pip install --index-url https://test.pypi.org/simple/ \
        --extra-index-url https://pypi.org/simple/ tvccointreg
    
  4. Upload to the real PyPI:
    python -m twine upload dist/*
    
    When prompted, use __token__ as the username and the API token (starting with pypi-) as the password.

Subsequent releases — automated via Trusted Publishing (recommended)

.github/workflows/publish.yml publishes automatically when you publish a GitHub Release. One-time setup: on PyPI add a Trusted Publisher (Project → Publishing) for repository merwanroudane/tvccointreg, workflow publish.yml, environment pypi. After that, no tokens or secrets are needed — just bump version in pyproject.toml and __version__, tag, and publish a Release.

Remember: a version number can only be uploaded to PyPI once. Bump the version for every release.


Citing

If you use this package, please cite both the package and the underlying paper.

@software{roudane_tvccointreg,
  author  = {Merwan Roudane},
  title   = {tvccointreg: Time-Varying-Coefficient Regression and
             Generalized Cointegration in Python},
  year    = {2026},
  url     = {https://github.com/merwanroudane/tvccointreg}
}

@incollection{hall_swamy_tavlas_2015,
  author  = {Hall, Stephen G. and Swamy, P. A. V. B. and Tavlas, George S.},
  title   = {A Note on Generalizing the Concept of Cointegration},
  year    = {2015}
}

References

  • Hall, S. G., Swamy, P. A. V. B., & Tavlas, G. S. (2015). A Note on Generalizing the Concept of Cointegration.
  • Hall, S. G., Swamy, P. A. V. B., & Tavlas, G. S. (2014). Time Varying Coefficient Models; A Proposal for Selecting the Coefficient Driver Sets. University of Leicester WP 14/18.
  • Swamy, P. A. V. B., & Mehta, J. S. (1975). Bayesian and non-Bayesian Analysis of Switching Regressions and of Random Coefficient Regression Models. JASA.
  • Swamy, P. A. V. B., Tavlas, G. S., Hall, S. G., et al. (2010). Nonparametric Nonstationary Regression.
  • Granger, C. W. J. (2008). Non-linear Models: Where Do We Go Next — Time-Varying Parameter Models? Studies in Nonlinear Dynamics & Econometrics.
  • Hildreth, C., & Houck, J. P. (1968). Some Estimators for a Linear Model with Random Coefficients. JASA.
  • Engle, R. F., & Granger, C. W. J. (1987). Co-integration and Error Correction. Econometrica.
  • Cramér, H. (1946). Mathematical Methods of Statistics.

Author

Dr Merwan Roudane · merwanroudane920@gmail.com · github.com/merwanroudane

Released under the MIT License.

Metadata

Release files for tvccointreg 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tvccointreg 0.1.1
File Size Uploaded Provenance
tvccointreg-0.1.1.tar.gz 38.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for tvccointreg 0.1.1
File Interpreter ABI Platform
tvccointreg-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.0 kB

Release files / tvccointreg-0.1.1.tar.gz

Download URL tvccointreg-0.1.1.tar.gz
Size 38.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d303abbbdd7dc265788b9cc704e32e109fb89799e1243b598440de753935a314
BLAKE2b-256 checksum
How to use checksums
af9eeafe9ca0148acd27d710b5cf1411bd512663fa2400a87993ee4607d9ebc9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.0

Release files / tvccointreg-0.1.1-py3-none-any.whl

Download URL tvccointreg-0.1.1-py3-none-any.whl
Size 30.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5c15862ad8c05b4ac725b8af82bbdcfa5dc6508a99069d66491112da878bf454
BLAKE2b-256 checksum
How to use checksums
c2f923722d6c349704f7699bd19113f897f37d4dc0415268c34e7c6a74f95a17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.0

Security for tvccointreg 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page