asycaus 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
asycaus/__init__.py ADDED
@@ -0,0 +1,64 @@
1
+ """
2
+ asycaus
3
+ =======
4
+
5
+ Asymmetric Granger-causality suite for Python. Python mirror of the Stata
6
+ package `asycaus` by the same author.
7
+
8
+ Quick start
9
+ -----------
10
+
11
+ >>> import numpy as np, asycaus
12
+ >>> rng = np.random.default_rng(0)
13
+ >>> x = np.cumsum(rng.standard_normal(300))
14
+ >>> y = np.r_[0, 0.5*x[:-1] + rng.standard_normal(299)]
15
+ >>> r = asycaus.static(y, x, shock="both", boot=200, plot=False)
16
+
17
+ Available tests
18
+ ---------------
19
+
20
+ asycaus.static Hatemi-J (2012) static asymmetric (leverage bootstrap)
21
+ asycaus.dynamic Hatemi-J (2021) rolling / recursive
22
+ asycaus.fourier Nazlioglu et al. (2016) Fourier-augmented TY
23
+ asycaus.spectral Bahmani-Oskooee et al. (2016) BC frequency-domain
24
+ asycaus.quantile Fang et al. (2026) quantile asymmetric
25
+ asycaus.efficient Hatemi-J (2024) SUR (Pos / Neg / Joint / Pos=Neg)
26
+ asycaus.all_tests Run every test, return unified summary
27
+
28
+ Author : Dr Merwan Roudane <merwanroudane920@gmail.com>
29
+ GitHub : https://github.com/merwanroudane/asycaus
30
+ License: MIT
31
+ """
32
+
33
+ from importlib.metadata import version, PackageNotFoundError
34
+
35
+ try:
36
+ __version__ = version("asycaus")
37
+ except PackageNotFoundError:
38
+ __version__ = "1.0.0"
39
+
40
+ __author__ = "Dr Merwan Roudane"
41
+ __email__ = "merwanroudane920@gmail.com"
42
+ __license__ = "MIT"
43
+ __url__ = "https://github.com/merwanroudane/asycaus"
44
+
45
+ from . import engine
46
+ from . import tables
47
+ from . import plots
48
+ from .static import static, StaticResult
49
+ from .dynamic import dynamic, DynamicResult
50
+ from .fourier import fourier, FourierResult
51
+ from .spectral import spectral, SpectralResult
52
+ from .quantile import quantile, QuantileResult
53
+ from .efficient import efficient, EfficientResult
54
+ from .all_tests import all_tests, AllResult
55
+ from .engine import pos_neg_components
56
+
57
+ __all__ = [
58
+ "static", "dynamic", "fourier", "spectral", "quantile",
59
+ "efficient", "all_tests", "pos_neg_components",
60
+ "StaticResult", "DynamicResult", "FourierResult", "SpectralResult",
61
+ "QuantileResult", "EfficientResult", "AllResult",
62
+ "engine", "tables", "plots",
63
+ "__version__", "__author__", "__email__", "__license__", "__url__",
64
+ ]
asycaus/all_tests.py ADDED
@@ -0,0 +1,160 @@
1
+ """asycaus.all_tests — run the full battery and print a unified summary."""
2
+
3
+ from __future__ import annotations
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+ import pandas as pd
7
+
8
+ from .static import static, StaticResult
9
+ from .dynamic import dynamic, DynamicResult
10
+ from .fourier import fourier, FourierResult
11
+ from .spectral import spectral, SpectralResult
12
+ from .quantile import quantile, QuantileResult
13
+ from .efficient import efficient, EfficientResult
14
+
15
+
16
+ @dataclass
17
+ class AllResult:
18
+ static_: StaticResult | None = None
19
+ fourier_: FourierResult | None = None
20
+ efficient_: EfficientResult | None = None
21
+ spectral_: SpectralResult | None = None
22
+ quantile_: QuantileResult | None = None
23
+ dynamic_: DynamicResult | None = None
24
+ summary: pd.DataFrame = field(default_factory=pd.DataFrame)
25
+ depvar: str = "y"
26
+ causvar: str = "x"
27
+
28
+ def print(self):
29
+ from .tables import print_all_summary
30
+ print_all_summary(self)
31
+ return self
32
+
33
+ def plot(self, *, save=None):
34
+ from .plots import plot_dashboard
35
+ return plot_dashboard(self, save=save)
36
+
37
+
38
+ def all_tests(
39
+ y, x,
40
+ max_lag: int = 4,
41
+ ic: str = "hjc",
42
+ intorder: int = 1,
43
+ boot: int = 500,
44
+ seed: int | None = 12345,
45
+ kmax: int = 5,
46
+ nfreq: int = 50,
47
+ quantiles=(0.1, 0.25, 0.5, 0.75, 0.9),
48
+ window: int | None = None,
49
+ lnform: bool = False,
50
+ skip_dynamic: bool = False,
51
+ skip_spectral: bool = False,
52
+ skip_quantile: bool = False,
53
+ show: bool = True,
54
+ plot: bool = False,
55
+ ) -> AllResult:
56
+ """Run every asymmetric-causality test on the same (y, x) pair and print a
57
+ unified summary table at the end. Dashboard plot optional via `plot=True`.
58
+ """
59
+ print("\n" + "=" * 78)
60
+ print(f"{'ASYMMETRIC CAUSALITY BATTERY':^78}")
61
+ print(f"{'Author: Dr Merwan Roudane':^78}")
62
+ print("=" * 78)
63
+ print(f" Direction tested: causvar -> depvar")
64
+
65
+ common = dict(max_lag=max_lag, ic=ic, intorder=intorder, lnform=lnform,
66
+ show=False, plot=False)
67
+
68
+ print("\n[1/6] Static Asymmetric Causality (Hatemi-J 2012)...")
69
+ sr = static(y, x, shock="both", boot=boot, seed=seed, **common)
70
+
71
+ print("\n[2/6] Fourier Asymmetric TY (Nazlioglu et al. 2016)...")
72
+ fr = fourier(y, x, shock="both", kmax=kmax, form="single", **common)
73
+
74
+ print("\n[3/6] Efficient Asymmetric (Hatemi-J 2024)...")
75
+ er = efficient(y, x, max_lag=max_lag, ic=ic, intorder=intorder,
76
+ lnform=lnform, show=False, plot=False)
77
+
78
+ sp = None
79
+ if not skip_spectral:
80
+ print("\n[4/6] Spectral Asymmetric (Bahmani-Oskooee et al. 2016)...")
81
+ sp = spectral(y, x, shock="both", nfreq=nfreq, max_lag=max_lag, ic=ic,
82
+ lnform=lnform, show=False, plot=False)
83
+
84
+ qr = None
85
+ if not skip_quantile:
86
+ print("\n[5/6] Quantile Asymmetric (Fang et al. 2026)...")
87
+ qr = quantile(y, x, shock="both", quantiles=quantiles,
88
+ max_lag=max_lag, ic=ic, intorder=intorder,
89
+ lnform=lnform, show=False, plot=False)
90
+
91
+ dy = None
92
+ if not skip_dynamic:
93
+ print("\n[*] Dynamic Asymmetric (Hatemi-J 2021, Pos shocks)...")
94
+ try:
95
+ dy = dynamic(y, x, shock="pos", mode="rolling",
96
+ window=window, max_lag=max_lag, ic=ic,
97
+ intorder=intorder, boot=min(boot, 200), seed=seed,
98
+ lnform=lnform, show=False, plot=False, progress=False)
99
+ except Exception as ex:
100
+ print(f" (dynamic skipped: {ex})")
101
+ dy = None
102
+
103
+ # Build unified summary -------------------------------------------------
104
+ rows = []
105
+ for s in ["Positive", "Negative"]:
106
+ if s in sr.table.index:
107
+ row = sr.table.loc[s]
108
+ rows.append({"Test": "Static (Hatemi-J 2012)",
109
+ "Shock": s[:3], "Statistic": row["Wald"],
110
+ "p-value": row["asy_p"],
111
+ "Decision": row["decision_5pct"]})
112
+ for s in ["Positive", "Negative"]:
113
+ if s in fr.table.index:
114
+ row = fr.table.loc[s]
115
+ rows.append({"Test": "Fourier (Nazlioglu 2016)",
116
+ "Shock": s[:3], "Statistic": row["Wald"],
117
+ "p-value": row["asy_p"],
118
+ "Decision": row["decision_5pct"]})
119
+ eff = er.raw
120
+ rows.extend([
121
+ {"Test": "Efficient Pos only (HJ 2024)", "Shock": "Pos",
122
+ "Statistic": eff["W_pos"], "p-value": eff["p_pos"],
123
+ "Decision": "Reject" if eff["p_pos"] < 0.05 else "Fail to reject"},
124
+ {"Test": "Efficient Neg only (HJ 2024)", "Shock": "Neg",
125
+ "Statistic": eff["W_neg"], "p-value": eff["p_neg"],
126
+ "Decision": "Reject" if eff["p_neg"] < 0.05 else "Fail to reject"},
127
+ {"Test": "Efficient Joint (HJ 2024)", "Shock": "both",
128
+ "Statistic": eff["W_joint"], "p-value": eff["p_joint"],
129
+ "Decision": "Reject" if eff["p_joint"] < 0.05 else "Fail to reject"},
130
+ {"Test": "Efficient Pos=Neg (HJ 2024)", "Shock": "diff",
131
+ "Statistic": eff["W_diff"], "p-value": eff["p_diff"],
132
+ "Decision": "Reject" if eff["p_diff"] < 0.05 else "Fail to reject"},
133
+ ])
134
+ if sp is not None:
135
+ for s in sp.summary.index:
136
+ pct = sp.summary.loc[s, "pct_reject_5pct"]
137
+ rows.append({"Test": "Spectral (BCRanjbar 2016)",
138
+ "Shock": s[:3], "Statistic": f"{pct:.2%} freq.",
139
+ "p-value": pct,
140
+ "Decision": "Reject at some" if pct > 0 else "Fail to reject"})
141
+ if qr is not None:
142
+ for s in ["Positive", "Negative"]:
143
+ sub = qr.table[qr.table["shock"] == s]
144
+ if len(sub):
145
+ nrej = (sub["asy_p"] < 0.05).sum()
146
+ pct = nrej / len(sub)
147
+ rows.append({"Test": "Quantile (Fang et al. 2026)",
148
+ "Shock": s[:3], "Statistic": f"{pct:.2%} quant.",
149
+ "p-value": pct,
150
+ "Decision": "Reject at some" if nrej > 0 else "Fail to reject"})
151
+
152
+ summary = pd.DataFrame(rows)
153
+ out = AllResult(static_=sr, fourier_=fr, efficient_=er, spectral_=sp,
154
+ quantile_=qr, dynamic_=dy, summary=summary)
155
+ if show:
156
+ out.print()
157
+ if plot:
158
+ try: out.plot()
159
+ except Exception: pass
160
+ return out
asycaus/dynamic.py ADDED
@@ -0,0 +1,131 @@
1
+ """asycaus.dynamic — Hatemi-J (2021) rolling / recursive asymmetric causality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import math
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from .engine import (pos_neg_components, select_lag, wald_test,
11
+ bootstrap_critical_values, ic_label)
12
+
13
+
14
+ @dataclass
15
+ class DynamicResult:
16
+ table: pd.DataFrame
17
+ depvar: str = "y"
18
+ causvar: str = "x"
19
+ shock: str = "pos"
20
+ mode: str = "Rolling window"
21
+ window: int = 0
22
+ smin: int = 0
23
+ nsub: int = 0
24
+ ic: str = "HJC"
25
+ boot: int = 200
26
+ intorder: int = 1
27
+
28
+ def print(self):
29
+ from .tables import print_dynamic_table
30
+ print_dynamic_table(self)
31
+ return self
32
+
33
+ def plot(self, *, ax=None, save=None):
34
+ from .plots import plot_dynamic
35
+ return plot_dynamic(self, ax=ax, save=save)
36
+
37
+
38
+ def dynamic(
39
+ y, x,
40
+ shock: str = "pos",
41
+ mode: str = "rolling",
42
+ window: int | None = None,
43
+ max_lag: int = 4,
44
+ ic: str = "hjc",
45
+ intorder: int = 1,
46
+ boot: int = 200,
47
+ seed: int | None = 12345,
48
+ lnform: bool = False,
49
+ show: bool = True,
50
+ plot: bool = True,
51
+ progress: bool = True,
52
+ ) -> DynamicResult:
53
+ """Hatemi-J (2021) dynamic asymmetric Granger-causality.
54
+
55
+ Parameters
56
+ ----------
57
+ mode : {'rolling','recursive'}, default 'rolling'.
58
+ window : int or None
59
+ Window length S. Defaults to Phillips-Shi-Yu (2015) lower bound
60
+ S = ceil(T*(0.01 + 1.8/sqrt(T))).
61
+ progress : bool
62
+ Print "subsample k/N" every 10 windows.
63
+ """
64
+ y = np.asarray(y, dtype=float).ravel()
65
+ x = np.asarray(x, dtype=float).ravel()
66
+ if lnform:
67
+ y = np.log(y); x = np.log(x)
68
+ Y = np.column_stack([y, x])
69
+
70
+ if shock.lower() in {"pos", "positive"}:
71
+ Zfull = pos_neg_components(Y, positive=True); s_lbl = "Positive components"
72
+ s_short = "pos"
73
+ elif shock.lower() in {"neg", "negative"}:
74
+ Zfull = pos_neg_components(Y, positive=False); s_lbl = "Negative components"
75
+ s_short = "neg"
76
+ else:
77
+ raise ValueError("shock must be 'pos' or 'neg'")
78
+
79
+ Tcomp = Zfull.shape[0]
80
+ if Tcomp < 10:
81
+ raise ValueError("Too few observations after differencing.")
82
+
83
+ smin = math.ceil(Tcomp * (0.01 + 1.8 / math.sqrt(Tcomp)))
84
+ if window is None:
85
+ window = smin
86
+ min_window = max_lag + intorder + 3
87
+ if window < min_window:
88
+ raise ValueError(f"window must be at least {min_window}.")
89
+
90
+ nsub = Tcomp - window + 1
91
+ if nsub < 1:
92
+ raise ValueError("window too large for the sample.")
93
+
94
+ mode = mode.lower()
95
+ if mode not in {"rolling", "recursive"}:
96
+ raise ValueError("mode must be 'rolling' or 'recursive'.")
97
+ mode_lbl = "Rolling window" if mode == "rolling" else "Recursive"
98
+
99
+ rows = []
100
+ for k in range(1, nsub + 1):
101
+ if progress and (k % 10 == 0 or k == nsub):
102
+ print(f" subsample {k}/{nsub}")
103
+ if mode == "rolling":
104
+ s_idx, e_idx = k - 1, k - 1 + window
105
+ else:
106
+ s_idx, e_idx = 0, window + k - 1
107
+ Zsub = Zfull[s_idx:e_idx, :]
108
+ p = select_lag(Zsub, 1, max_lag, ic)
109
+ W, _ = wald_test(Zsub, p, intorder, dep_idx=0, cause_idx=1)
110
+ cv = bootstrap_critical_values(Zsub, p, intorder, 0, 1, B=boot,
111
+ seed=(seed or 0) + k)
112
+ rows.append({
113
+ "sub_start": s_idx + 1, "sub_end": e_idx, "lag": p,
114
+ "Wald": W, "cv10": cv["cv10"], "cv5": cv["cv5"], "cv1": cv["cv1"],
115
+ "ratio_5pct": W / cv["cv5"] if cv["cv5"] else np.nan,
116
+ })
117
+
118
+ table = pd.DataFrame(rows)
119
+ res = DynamicResult(
120
+ table=table, depvar="y", causvar="x", shock=s_short,
121
+ mode=mode_lbl, window=window, smin=smin, nsub=nsub,
122
+ ic=ic_label(ic), boot=boot, intorder=intorder,
123
+ )
124
+ if show:
125
+ res.print()
126
+ if plot:
127
+ try:
128
+ res.plot()
129
+ except Exception:
130
+ pass
131
+ return res
asycaus/efficient.py ADDED
@@ -0,0 +1,86 @@
1
+ """asycaus.efficient — Hatemi-J (2024) efficient SUR asymmetric causality."""
2
+
3
+ from __future__ import annotations
4
+ from dataclasses import dataclass
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from .engine import pos_neg_components, select_lag, efficient_sur, ic_label
9
+
10
+
11
+ @dataclass
12
+ class EfficientResult:
13
+ table: pd.DataFrame
14
+ raw: dict
15
+ depvar: str = "y"
16
+ causvar: str = "x"
17
+ ic: str = "HJC"
18
+ intorder: int = 1
19
+ lag: int = 1
20
+
21
+ def print(self):
22
+ from .tables import print_efficient_table
23
+ print_efficient_table(self)
24
+ return self
25
+
26
+ def plot(self, *, ax=None, save=None):
27
+ from .plots import plot_efficient
28
+ return plot_efficient(self, ax=ax, save=save)
29
+
30
+
31
+ def efficient(
32
+ y, x,
33
+ max_lag: int = 8,
34
+ ic: str = "hjc",
35
+ intorder: int = 1,
36
+ lnform: bool = False,
37
+ show: bool = True,
38
+ plot: bool = True,
39
+ ) -> EfficientResult:
40
+ """Hatemi-J (2024) efficient asymmetric causality test (SUR).
41
+
42
+ Tests four hypotheses jointly within one SUR system:
43
+ H1: no causality via positive shocks
44
+ H2: no causality via negative shocks
45
+ H3: joint no causality (H1 AND H2)
46
+ H4: Pos = Neg causal coefficients -- the formal asymmetry test.
47
+ """
48
+ y = np.asarray(y, dtype=float).ravel()
49
+ x = np.asarray(x, dtype=float).ravel()
50
+ if lnform:
51
+ y = np.log(y); x = np.log(x)
52
+ Y = np.column_stack([y, x])
53
+
54
+ Zpos = pos_neg_components(Y, positive=True)
55
+ Zneg = pos_neg_components(Y, positive=False)
56
+ p_pos = select_lag(Zpos, 1, max_lag, ic)
57
+ p_neg = select_lag(Zneg, 1, max_lag, ic)
58
+ p = max(p_pos, p_neg)
59
+
60
+ out = efficient_sur(Zpos, Zneg, p, intorder, 0, 1)
61
+
62
+ rows = [
63
+ {"hypothesis": "No causality via POS shocks", "Wald": out["W_pos"],
64
+ "df": p, "asy_p": out["p_pos"],
65
+ "decision_5pct": "Reject" if out["p_pos"] < 0.05 else "Fail to reject"},
66
+ {"hypothesis": "No causality via NEG shocks", "Wald": out["W_neg"],
67
+ "df": p, "asy_p": out["p_neg"],
68
+ "decision_5pct": "Reject" if out["p_neg"] < 0.05 else "Fail to reject"},
69
+ {"hypothesis": "Joint no causality", "Wald": out["W_joint"],
70
+ "df": 2 * p, "asy_p": out["p_joint"],
71
+ "decision_5pct": "Reject" if out["p_joint"] < 0.05 else "Fail to reject"},
72
+ {"hypothesis": "POS = NEG causal effects", "Wald": out["W_diff"],
73
+ "df": p, "asy_p": out["p_diff"],
74
+ "decision_5pct": "Reject" if out["p_diff"] < 0.05 else "Fail to reject"},
75
+ ]
76
+ table = pd.DataFrame(rows).set_index("hypothesis")
77
+ res = EfficientResult(table=table, raw=out, ic=ic_label(ic),
78
+ intorder=intorder, lag=p)
79
+ if show:
80
+ res.print()
81
+ if plot:
82
+ try:
83
+ res.plot()
84
+ except Exception:
85
+ pass
86
+ return res