pycausalarima 0.1.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.
- pycausalarima/__init__.py +68 -0
- pycausalarima/core/__init__.py +13 -0
- pycausalarima/core/arma_utils.py +198 -0
- pycausalarima/core/causal_arima.py +728 -0
- pycausalarima/core/inference.py +373 -0
- pycausalarima/exceptions.py +112 -0
- pycausalarima/py.typed +0 -0
- pycausalarima/reporting/__init__.py +6 -0
- pycausalarima/reporting/summary.py +112 -0
- pycausalarima/reporting/tables.py +406 -0
- pycausalarima/utils/__init__.py +11 -0
- pycausalarima/utils/types.py +192 -0
- pycausalarima/utils/validation.py +169 -0
- pycausalarima/visualization/__init__.py +5 -0
- pycausalarima/visualization/plotting.py +332 -0
- pycausalarima-0.1.0.dist-info/METADATA +368 -0
- pycausalarima-0.1.0.dist-info/RECORD +20 -0
- pycausalarima-0.1.0.dist-info/WHEEL +5 -0
- pycausalarima-0.1.0.dist-info/licenses/LICENSE +674 -0
- pycausalarima-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyCausalArima: Causal Effect Estimation using ARIMA Models
|
|
3
|
+
==========================================================
|
|
4
|
+
|
|
5
|
+
A Python implementation of the C-ARIMA methodology for estimating
|
|
6
|
+
causal effects of interventions on time series data.
|
|
7
|
+
|
|
8
|
+
Main Classes
|
|
9
|
+
------------
|
|
10
|
+
CausalArima : Main class for causal effect estimation
|
|
11
|
+
|
|
12
|
+
Example
|
|
13
|
+
-------
|
|
14
|
+
>>> from pycausalarima import CausalArima
|
|
15
|
+
>>> import pandas as pd
|
|
16
|
+
>>> import numpy as np
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Generate sample data
|
|
19
|
+
>>> n = 100
|
|
20
|
+
>>> np.random.seed(1)
|
|
21
|
+
>>> dates = pd.date_range('2014-01-05', periods=n, freq='D')
|
|
22
|
+
>>> y = np.cumsum(np.random.normal(0, 1, n)) + 100
|
|
23
|
+
>>> intervention_date = pd.Timestamp('2014-03-16')
|
|
24
|
+
>>> y[dates >= intervention_date] += 10 # Add intervention effect
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Fit model and estimate causal effects
|
|
27
|
+
>>> ca = CausalArima(y, dates, intervention_date, n_boot=1000)
|
|
28
|
+
>>> result = ca.fit()
|
|
29
|
+
>>>
|
|
30
|
+
>>> # View summary
|
|
31
|
+
>>> ca.summary()
|
|
32
|
+
|
|
33
|
+
References
|
|
34
|
+
----------
|
|
35
|
+
Menchetti, F., Cipollini, F., & Mealli, F. (2023).
|
|
36
|
+
"Combining counterfactual outcomes and ARIMA models for policy evaluation."
|
|
37
|
+
The Econometrics Journal.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from pycausalarima.core.causal_arima import CausalArima
|
|
41
|
+
from pycausalarima.exceptions import (
|
|
42
|
+
CausalArimaError,
|
|
43
|
+
CoefficientExtractionError,
|
|
44
|
+
InferenceError,
|
|
45
|
+
ModelFittingError,
|
|
46
|
+
SimulationError,
|
|
47
|
+
ValidationError,
|
|
48
|
+
)
|
|
49
|
+
from pycausalarima.utils.types import ARIMAOrder, CausalArimaResult, InferenceResult
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
__author__ = "Robson Tigre"
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# Main class
|
|
56
|
+
"CausalArima",
|
|
57
|
+
# Result types
|
|
58
|
+
"CausalArimaResult",
|
|
59
|
+
"InferenceResult",
|
|
60
|
+
"ARIMAOrder",
|
|
61
|
+
# Exceptions
|
|
62
|
+
"CausalArimaError",
|
|
63
|
+
"ValidationError",
|
|
64
|
+
"ModelFittingError",
|
|
65
|
+
"CoefficientExtractionError",
|
|
66
|
+
"SimulationError",
|
|
67
|
+
"InferenceError",
|
|
68
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Core modules for pycausalarima."""
|
|
2
|
+
|
|
3
|
+
from pycausalarima.core.arma_utils import compute_psi_weights, sarma_to_larma
|
|
4
|
+
from pycausalarima.core.causal_arima import CausalArima
|
|
5
|
+
from pycausalarima.core.inference import bootstrap_inference, normal_inference
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CausalArima",
|
|
9
|
+
"sarma_to_larma",
|
|
10
|
+
"compute_psi_weights",
|
|
11
|
+
"normal_inference",
|
|
12
|
+
"bootstrap_inference",
|
|
13
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""ARMA utility functions for pycausalarima.
|
|
2
|
+
|
|
3
|
+
This module contains functions for converting SARMA models to long-form ARMA
|
|
4
|
+
and computing MA(infinity) psi weights, which are essential for variance
|
|
5
|
+
calculation in causal inference.
|
|
6
|
+
|
|
7
|
+
These are Python translations of the R functions:
|
|
8
|
+
- .sarma2larma() -> sarma_to_larma()
|
|
9
|
+
- .long() -> _merge_polynomials()
|
|
10
|
+
- ARMAtoMA() -> compute_psi_weights()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sarma_to_larma(
|
|
19
|
+
ar: np.ndarray,
|
|
20
|
+
ma: np.ndarray,
|
|
21
|
+
sar: np.ndarray,
|
|
22
|
+
sma: np.ndarray,
|
|
23
|
+
s: int,
|
|
24
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
25
|
+
"""Convert SARMA representation to long-form ARMA.
|
|
26
|
+
|
|
27
|
+
This is a Python translation of the R .sarma2larma() function.
|
|
28
|
+
Merges AR(p) with SAR(P) and MA(q) with SMA(Q).
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
ar : np.ndarray
|
|
33
|
+
AR coefficients (phi). Can be empty array.
|
|
34
|
+
ma : np.ndarray
|
|
35
|
+
MA coefficients (theta). Can be empty array.
|
|
36
|
+
sar : np.ndarray
|
|
37
|
+
Seasonal AR coefficients. Can be empty array.
|
|
38
|
+
sma : np.ndarray
|
|
39
|
+
Seasonal MA coefficients. Can be empty array.
|
|
40
|
+
s : int
|
|
41
|
+
Seasonal period (e.g., 12 for monthly, 7 for daily with weekly pattern).
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
tuple of (ar_long, ma_long)
|
|
46
|
+
Long-form ARMA coefficients after merging seasonal components.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
The R code adjusts signs: ar and sar are negated before merging,
|
|
51
|
+
then the result is negated again. This is because R's ARIMA uses
|
|
52
|
+
the convention: (1 - phi*B) y_t = (1 + theta*B) e_t
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> ar = np.array([0.5])
|
|
57
|
+
>>> ma = np.array([0.3])
|
|
58
|
+
>>> sar = np.array([0.2])
|
|
59
|
+
>>> sma = np.array([0.1])
|
|
60
|
+
>>> ar_long, ma_long = sarma_to_larma(ar, ma, sar, sma, s=12)
|
|
61
|
+
"""
|
|
62
|
+
# Ensure inputs are numpy arrays
|
|
63
|
+
ar = np.atleast_1d(ar) if ar is not None and len(ar) > 0 else np.array([])
|
|
64
|
+
ma = np.atleast_1d(ma) if ma is not None and len(ma) > 0 else np.array([])
|
|
65
|
+
sar = np.atleast_1d(sar) if sar is not None and len(sar) > 0 else np.array([])
|
|
66
|
+
sma = np.atleast_1d(sma) if sma is not None and len(sma) > 0 else np.array([])
|
|
67
|
+
|
|
68
|
+
# Adjust signs (R convention: ar and sar are negated)
|
|
69
|
+
ar_adj = -ar if len(ar) > 0 else ar
|
|
70
|
+
sar_adj = -sar if len(sar) > 0 else sar
|
|
71
|
+
|
|
72
|
+
# Merge and adjust final sign
|
|
73
|
+
ar_long = -_merge_polynomials(ar_adj, sar_adj, s)
|
|
74
|
+
ma_long = _merge_polynomials(ma, sma, s)
|
|
75
|
+
|
|
76
|
+
return ar_long, ma_long
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _merge_polynomials(p: np.ndarray, ps: np.ndarray, s: int) -> np.ndarray:
|
|
80
|
+
"""Merge short and seasonal polynomial components.
|
|
81
|
+
|
|
82
|
+
Python translation of R .long() function.
|
|
83
|
+
Multiplies the short-term polynomial (1 + p1*B + p2*B^2 + ...)
|
|
84
|
+
with the seasonal polynomial (1 + ps1*B^s + ps2*B^{2s} + ...)
|
|
85
|
+
using convolution.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
p : np.ndarray
|
|
90
|
+
Short-term coefficients (without the leading 1).
|
|
91
|
+
ps : np.ndarray
|
|
92
|
+
Seasonal coefficients (without the leading 1).
|
|
93
|
+
s : int
|
|
94
|
+
Seasonal period.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
np.ndarray
|
|
99
|
+
Merged polynomial coefficients (without the leading 1).
|
|
100
|
+
|
|
101
|
+
Notes
|
|
102
|
+
-----
|
|
103
|
+
The function builds coefficient vectors with a leading 1,
|
|
104
|
+
convolves them, and returns the result without the leading 1.
|
|
105
|
+
"""
|
|
106
|
+
# Ensure inputs are numpy arrays
|
|
107
|
+
p = np.atleast_1d(p) if p is not None else np.array([])
|
|
108
|
+
ps = np.atleast_1d(ps) if ps is not None else np.array([])
|
|
109
|
+
|
|
110
|
+
np_len = len(p)
|
|
111
|
+
nps_len = len(ps)
|
|
112
|
+
|
|
113
|
+
# Build coefficient vector for short-term component: [1, p1, p2, ...]
|
|
114
|
+
cp = np.concatenate([[1], p]) if np_len > 0 else np.array([1])
|
|
115
|
+
|
|
116
|
+
# Build coefficient vector for seasonal component
|
|
117
|
+
if nps_len > 0:
|
|
118
|
+
# Create sparse vector with seasonal coefficients at positions s, 2s, 3s, ...
|
|
119
|
+
# e.g., for s=12 and ps=[0.2]: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2]
|
|
120
|
+
cps = np.zeros(s * nps_len + 1)
|
|
121
|
+
cps[0] = 1
|
|
122
|
+
indices = np.arange(s, s * nps_len + 1, s)
|
|
123
|
+
cps[indices] = ps
|
|
124
|
+
else:
|
|
125
|
+
cps = np.array([1])
|
|
126
|
+
|
|
127
|
+
# Convolve polynomials (equivalent to polynomial multiplication)
|
|
128
|
+
# R's convolve with type="open" and rev() is equivalent to np.convolve
|
|
129
|
+
result = np.convolve(cp, cps, mode="full")
|
|
130
|
+
|
|
131
|
+
# Remove leading 1 (return coefficients only)
|
|
132
|
+
return result[1:] if len(result) > 1 else np.array([])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def compute_psi_weights(
|
|
136
|
+
ar: np.ndarray,
|
|
137
|
+
ma: np.ndarray,
|
|
138
|
+
lag_max: int,
|
|
139
|
+
) -> np.ndarray:
|
|
140
|
+
"""Compute MA(infinity) psi weights from ARMA coefficients.
|
|
141
|
+
|
|
142
|
+
Equivalent to R's ARMAtoMA() function.
|
|
143
|
+
Converts ARMA(p,q) representation to MA(infinity) representation
|
|
144
|
+
by computing the impulse response function.
|
|
145
|
+
|
|
146
|
+
The psi weights satisfy:
|
|
147
|
+
psi_0 = 1
|
|
148
|
+
psi_j = theta_j + sum_{i=1}^{min(j,p)} phi_i * psi_{j-i} for j >= 1
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
ar : np.ndarray
|
|
153
|
+
AR coefficients (phi_1, phi_2, ..., phi_p).
|
|
154
|
+
ma : np.ndarray
|
|
155
|
+
MA coefficients (theta_1, theta_2, ..., theta_q).
|
|
156
|
+
lag_max : int
|
|
157
|
+
Maximum lag for psi weights (returns lag_max + 1 weights).
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
np.ndarray
|
|
162
|
+
Psi weights [psi_0=1, psi_1, ..., psi_{lag_max}].
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
>>> # MA(1) model: psi_0=1, psi_1=theta, psi_j=0 for j>1
|
|
167
|
+
>>> ar = np.array([])
|
|
168
|
+
>>> ma = np.array([0.6])
|
|
169
|
+
>>> psi = compute_psi_weights(ar, ma, 5)
|
|
170
|
+
>>> # psi = [1.0, 0.6, 0.0, 0.0, 0.0, 0.0]
|
|
171
|
+
|
|
172
|
+
>>> # AR(1) model: psi_j = phi^j
|
|
173
|
+
>>> ar = np.array([0.8])
|
|
174
|
+
>>> ma = np.array([])
|
|
175
|
+
>>> psi = compute_psi_weights(ar, ma, 5)
|
|
176
|
+
>>> # psi = [1.0, 0.8, 0.64, 0.512, 0.4096, 0.32768]
|
|
177
|
+
"""
|
|
178
|
+
# Ensure inputs are numpy arrays
|
|
179
|
+
ar = np.atleast_1d(ar) if ar is not None and len(ar) > 0 else np.array([])
|
|
180
|
+
ma = np.atleast_1d(ma) if ma is not None and len(ma) > 0 else np.array([])
|
|
181
|
+
|
|
182
|
+
psi = np.zeros(lag_max + 1)
|
|
183
|
+
psi[0] = 1.0
|
|
184
|
+
|
|
185
|
+
p = len(ar)
|
|
186
|
+
q = len(ma)
|
|
187
|
+
|
|
188
|
+
for j in range(1, lag_max + 1):
|
|
189
|
+
# MA contribution: theta_j if j <= q, else 0
|
|
190
|
+
psi[j] = ma[j - 1] if j <= q else 0.0
|
|
191
|
+
|
|
192
|
+
# AR contribution: sum of phi_i * psi_{j-i} for i = 1 to min(j, p)
|
|
193
|
+
for i in range(1, min(j, p) + 1):
|
|
194
|
+
psi[j] += ar[i - 1] * psi[j - i]
|
|
195
|
+
|
|
196
|
+
return psi
|
|
197
|
+
|
|
198
|
+
|