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.
@@ -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
+