tempdisagg 0.2.2__tar.gz → 0.2.4__tar.gz

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.
Files changed (40) hide show
  1. {tempdisagg-0.2.2/tempdisagg.egg-info → tempdisagg-0.2.4}/PKG-INFO +1 -1
  2. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/pyproject.toml +5 -5
  3. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tempdisagg/__init__.py +0 -6
  4. tempdisagg-0.2.4/tempdisagg/core/__init__.py +9 -0
  5. tempdisagg-0.2.4/tempdisagg/core/numeric_utils.py +151 -0
  6. tempdisagg-0.2.4/tempdisagg/core/rho_optimizer.py +212 -0
  7. tempdisagg-0.2.4/tempdisagg/core/temporal_aggregator.py +82 -0
  8. tempdisagg-0.2.4/tempdisagg/model/__init__.py +4 -0
  9. tempdisagg-0.2.4/tempdisagg/model/models_handler.py +515 -0
  10. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_adjuster.py +115 -0
  11. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_base.py +120 -0
  12. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_core.py +292 -0
  13. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_ensemble.py +247 -0
  14. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_fitter.py +190 -0
  15. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_model.py +299 -0
  16. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_summary.py +212 -0
  17. tempdisagg-0.2.4/tempdisagg/model/tempdisagg_visualizer.py +69 -0
  18. tempdisagg-0.2.4/tempdisagg/postprocessing/__init__.py +7 -0
  19. tempdisagg-0.2.4/tempdisagg/postprocessing/ensemble_prediction.py +298 -0
  20. tempdisagg-0.2.4/tempdisagg/postprocessing/post_estimation.py +245 -0
  21. tempdisagg-0.2.4/tempdisagg/preprocessing/__init__.py +11 -0
  22. tempdisagg-0.2.4/tempdisagg/preprocessing/conversion_matrix_builder.py +150 -0
  23. tempdisagg-0.2.4/tempdisagg/preprocessing/disagg_input_preparer.py +134 -0
  24. tempdisagg-0.2.4/tempdisagg/preprocessing/input_preprocessor.py +115 -0
  25. tempdisagg-0.2.4/tempdisagg/preprocessing/timeseries_completer.py +268 -0
  26. tempdisagg-0.2.4/tempdisagg/utils/__init__.py +6 -0
  27. tempdisagg-0.2.4/tempdisagg/utils/logging_utils.py +68 -0
  28. {tempdisagg-0.2.2 → tempdisagg-0.2.4/tempdisagg.egg-info}/PKG-INFO +1 -1
  29. tempdisagg-0.2.4/tempdisagg.egg-info/SOURCES.txt +37 -0
  30. tempdisagg-0.2.2/tempdisagg.egg-info/SOURCES.txt +0 -13
  31. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/LICENSE +0 -0
  32. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/README.md +0 -0
  33. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/setup.cfg +0 -0
  34. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tempdisagg.egg-info/dependency_links.txt +0 -0
  35. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tempdisagg.egg-info/requires.txt +0 -0
  36. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tempdisagg.egg-info/top_level.txt +0 -0
  37. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tests/test_numeric_utils.py +0 -0
  38. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tests/test_prediction.py +0 -0
  39. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tests/test_rho_optimizer.py +0 -0
  40. {tempdisagg-0.2.2 → tempdisagg-0.2.4}/tests/test_summary_visualize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tempdisagg
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Temporal disaggregation models in Python
5
5
  Author-email: Jaime Vera-Jaramillo <jaimevera1107@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tempdisagg"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Temporal disaggregation models in Python"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -28,14 +28,14 @@ dependencies = [
28
28
  "matplotlib>=3.4"
29
29
  ]
30
30
 
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["tempdisagg", "tempdisagg.*"]
34
+
31
35
  [project.urls]
32
36
  Homepage = "https://github.com/jaimevera1107/tempdisagg"
33
37
 
34
38
  [tool.setuptools]
35
- packages = ["tempdisagg"]
36
39
 
37
40
  [tool.setuptools.package-data]
38
41
  tempdisagg = ["py.typed"]
39
-
40
- [tool.setuptools.exclude-package-data]
41
- "*" = ["notebooks*", "example*", "tests*"]
@@ -26,9 +26,3 @@ __all__ = [
26
26
  "EnsemblePrediction",
27
27
  "TemporalAggregator",
28
28
  ]
29
-
30
-
31
- # Versioning and metadata
32
- __version__ = "0.2.2"
33
- __author__ = "Jaime Vera-Jaramillo"
34
- __license__ = "MIT"
@@ -0,0 +1,9 @@
1
+ from .rho_optimizer import RhoOptimizer
2
+ from .numeric_utils import NumericUtils
3
+ from .temporal_aggregator import TemporalAggregator
4
+
5
+ __all__ = [
6
+ "RhoOptimizer",
7
+ "NumericUtils",
8
+ "TemporalAggregator"
9
+ ]
@@ -0,0 +1,151 @@
1
+ import numpy as np
2
+ from scipy.linalg import inv, pinv
3
+ import warnings
4
+
5
+ class NumericUtils:
6
+ """
7
+ Utility class for numerical operations used in temporal disaggregation models.
8
+ Includes methods for constructing power matrices and Q matrices with autocorrelation structure.
9
+ """
10
+
11
+ @staticmethod
12
+ def power_matrix(n):
13
+ """
14
+ Create a matrix where each element (i, j) is |i - j|.
15
+
16
+ Parameters
17
+ ----------
18
+ n : int
19
+ Size of the square matrix to generate.
20
+
21
+ Returns
22
+ -------
23
+ numpy.ndarray
24
+ A square matrix of shape (n, n) with absolute differences of indices.
25
+ """
26
+ # Validate that n is a positive integer
27
+ if not isinstance(n, int) or n <= 0:
28
+ raise ValueError("Parameter 'n' must be a positive integer.")
29
+
30
+ # Construct the power matrix using broadcasting
31
+ return np.abs(np.subtract.outer(np.arange(n), np.arange(n)))
32
+
33
+ @staticmethod
34
+ def q_matrix(rho, power_matrix, epsilon=1e-6):
35
+ """
36
+ Construct a Q matrix for Chow-Lin type models using a power matrix.
37
+
38
+ Parameters
39
+ ----------
40
+ rho : float
41
+ Autocorrelation parameter, expected to be between -1 and 1.
42
+ power_matrix : numpy.ndarray
43
+ Matrix of absolute differences in time indices.
44
+ epsilon : float, optional
45
+ Small constant added for numerical stability (default is 1e-6).
46
+
47
+ Returns
48
+ -------
49
+ numpy.ndarray
50
+ Q matrix capturing autocorrelation structure.
51
+ """
52
+ # Clip rho to avoid instability
53
+ rho = np.clip(rho, -0.99, 0.99)
54
+
55
+ # Validate input power matrix
56
+ if not isinstance(power_matrix, np.ndarray):
57
+ raise TypeError("Parameter 'power_matrix' must be a NumPy ndarray.")
58
+
59
+ if power_matrix.shape[0] != power_matrix.shape[1]:
60
+ raise ValueError("Parameter 'power_matrix' must be a square matrix.")
61
+
62
+ # Compute the Q matrix based on rho and power_matrix
63
+ return (1 / (1 - rho ** 2 + epsilon)) * (rho ** power_matrix)
64
+
65
+ @staticmethod
66
+ def q_litterman(X, rho=0):
67
+ """
68
+ Construct the Q matrix for Litterman's method.
69
+
70
+ Parameters
71
+ ----------
72
+ X : numpy.ndarray
73
+ Regressor matrix with shape (n, k).
74
+ rho : float, optional
75
+ Autocorrelation parameter (default is 0).
76
+
77
+ Returns
78
+ -------
79
+ numpy.ndarray
80
+ The inverted Q matrix used for GLS estimation.
81
+ """
82
+ # Validate input matrix
83
+ if not isinstance(X, np.ndarray):
84
+ raise TypeError("Parameter 'X' must be a NumPy ndarray.")
85
+
86
+ if X.ndim != 2:
87
+ raise ValueError("Parameter 'X' must be a 2D array.")
88
+
89
+ # Get the number of observations
90
+ n = X.shape[0]
91
+
92
+ # Construct H and D matrices for differencing
93
+ H = np.eye(n) - np.diag(np.ones(n - 1), -1) * rho
94
+ D = np.eye(n) - np.diag(np.ones(n - 1), -1)
95
+
96
+ # Compute Q_Litterman
97
+ Q_lit = D.T @ H.T @ H @ D
98
+
99
+ try:
100
+ # Attempt to invert Q_Lit with a small ridge for stability
101
+ return inv(Q_lit + np.eye(n) * 1e-8)
102
+ except np.linalg.LinAlgError:
103
+ # Fallback to pseudoinverse if singular
104
+ warnings.warn("Q_Lit matrix is singular. Using pseudo-inverse instead.", RuntimeWarning)
105
+ return pinv(Q_lit)
106
+
107
+
108
+ @staticmethod
109
+ def validate_symmetric_positive_definite(matrix, tol=1e-8):
110
+ """
111
+ Validate whether a matrix is symmetric and positive definite.
112
+
113
+ Parameters
114
+ ----------
115
+ matrix : numpy.ndarray
116
+ The matrix to validate.
117
+ tol : float
118
+ Tolerance for symmetry check (default is 1e-8).
119
+
120
+ Returns
121
+ -------
122
+ bool
123
+ True if the matrix is symmetric and positive definite.
124
+
125
+ Raises
126
+ ------
127
+ TypeError
128
+ If input is not a NumPy array.
129
+ ValueError
130
+ If the matrix is not square or not symmetric within tolerance.
131
+ np.linalg.LinAlgError
132
+ If the matrix is not positive definite (Cholesky fails).
133
+ """
134
+ # Ensure matrix is a NumPy array
135
+ if not isinstance(matrix, np.ndarray):
136
+ raise TypeError("Input must be a NumPy ndarray.")
137
+
138
+ # Ensure matrix is square
139
+ if matrix.shape[0] != matrix.shape[1]:
140
+ raise ValueError("Matrix must be square.")
141
+
142
+ # Check symmetry within tolerance
143
+ if not np.allclose(matrix, matrix.T, atol=tol):
144
+ raise ValueError("Matrix is not symmetric within the given tolerance.")
145
+
146
+ try:
147
+ # Try Cholesky decomposition to verify positive definiteness
148
+ _ = np.linalg.cholesky(matrix)
149
+ return True
150
+ except np.linalg.LinAlgError as e:
151
+ raise np.linalg.LinAlgError("Matrix is not positive definite.") from e
@@ -0,0 +1,212 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import warnings
4
+
5
+ from scipy.linalg import pinv
6
+ from scipy.optimize import minimize_scalar
7
+ from scipy.stats import norm
8
+
9
+ from tempdisagg.utils.logging_utils import VerboseLogger
10
+
11
+
12
+
13
+ class RhoOptimizer:
14
+ """
15
+ Optimizes the autocorrelation parameter (rho) for temporal disaggregation models.
16
+
17
+ Supports:
18
+ - 'maxlog': Maximum likelihood estimation.
19
+ - 'minrss': Minimum residual sum of squares.
20
+
21
+ Returns full statistical outputs: rho, beta, residuals, Q matrix, variance-covariance matrix, and summary.
22
+ """
23
+
24
+ def __init__(self, rho_min=-0.9, rho_max=0.99, verbose=False):
25
+ """
26
+ Initialize the optimizer with bounds and verbosity.
27
+
28
+ Parameters
29
+ ----------
30
+ rho_min : float
31
+ Lower bound for rho (default = -0.9).
32
+ rho_max : float
33
+ Upper bound for rho (default = 0.99).
34
+ verbose : bool
35
+ Whether to enable informative logging.
36
+ """
37
+ self.rho_min = rho_min
38
+ self.rho_max = rho_max
39
+ self.verbose = verbose
40
+
41
+ # Create logger with verbosity control
42
+ self.logger = VerboseLogger(f"{__name__}.{id(self)}", verbose=self.verbose).get_logger()
43
+
44
+ def power_matrix(self, n):
45
+ """
46
+ Construct a power matrix of absolute differences |i - j|.
47
+
48
+ Parameters
49
+ ----------
50
+ n : int
51
+ Dimension of the square matrix.
52
+
53
+ Returns
54
+ -------
55
+ numpy.ndarray
56
+ Power matrix of shape (n, n).
57
+ """
58
+ return np.abs(np.subtract.outer(np.arange(n), np.arange(n)))
59
+
60
+ def q_matrix(self, rho, power_matrix, epsilon=1e-6):
61
+ """
62
+ Generate Q matrix using autocorrelation parameter rho.
63
+
64
+ Parameters
65
+ ----------
66
+ rho : float
67
+ Autocorrelation coefficient.
68
+ power_matrix : numpy.ndarray
69
+ Power matrix from self.power_matrix().
70
+ epsilon : float
71
+ Stability constant to avoid division by zero.
72
+
73
+ Returns
74
+ -------
75
+ numpy.ndarray
76
+ Q matrix used for GLS estimation.
77
+ """
78
+ rho = np.clip(rho, -0.99, 0.99)
79
+ return (1 / (1 - rho**2 + epsilon)) * (rho ** power_matrix)
80
+
81
+ def optimize(self, y_l, X, C, method="maxlog"):
82
+ """
83
+ Optimize rho and estimate model parameters.
84
+
85
+ Parameters
86
+ ----------
87
+ y_l : array-like
88
+ Low-frequency response variable (n_low x 1).
89
+ X : array-like
90
+ High-frequency explanatory variable (n_high x 1).
91
+ C : array-like
92
+ Conversion matrix (n_low x n_high).
93
+ method : str
94
+ Optimization method: 'maxlog' or 'minrss'.
95
+
96
+ Returns
97
+ -------
98
+ dict
99
+ Dictionary with rho, Q, beta, residuals, std_errors, t_stats, p_values, and summary.
100
+ """
101
+ try:
102
+ # Reshape and validate inputs
103
+ y_l = np.atleast_2d(y_l).reshape(-1, 1)
104
+ X = np.atleast_2d(X).reshape(-1, 1)
105
+ C = np.atleast_2d(C)
106
+
107
+ if C.shape[1] != X.shape[0]:
108
+ raise ValueError("C columns must match X rows.")
109
+ if C.shape[0] != y_l.shape[0]:
110
+ raise ValueError("C rows must match y_l rows.")
111
+
112
+ # Compute low-frequency regressor
113
+ X_l = C @ X
114
+
115
+ # Precompute power matrix
116
+ power_mat = self.power_matrix(X.shape[0])
117
+
118
+ # Store best result during optimization
119
+ results = {}
120
+
121
+ def objective(rho):
122
+ """
123
+ Objective function to minimize (depends on method).
124
+ """
125
+ if not (self.rho_min < rho < self.rho_max):
126
+ return np.inf
127
+
128
+ try:
129
+ # Compute Q matrix and covariance
130
+ Q = self.q_matrix(rho, power_mat)
131
+ vcov = C @ Q @ C.T + np.eye(C.shape[0]) * 1e-8
132
+ inv_vcov = pinv(vcov)
133
+
134
+ # Estimate beta
135
+ XTX = X_l.T @ inv_vcov @ X_l
136
+ if XTX.shape[0] != XTX.shape[1]:
137
+ return np.inf
138
+
139
+ beta = pinv(XTX) @ X_l.T @ inv_vcov @ y_l
140
+ residuals = y_l - X_l @ beta
141
+
142
+ # Evaluate objective
143
+ if method == "maxlog":
144
+ det = np.linalg.det(vcov)
145
+ if det <= 0:
146
+ return np.inf
147
+ loglike = -0.5 * (np.log(det + 1e-8) + residuals.T @ inv_vcov @ residuals)
148
+ value = -loglike.item()
149
+ elif method == "minrss":
150
+ value = (residuals.T @ inv_vcov @ residuals).item()
151
+ else:
152
+ raise ValueError("Invalid method: use 'maxlog' or 'minrss'.")
153
+
154
+ # Update best result
155
+ if "best_val" not in results or value < results["best_val"]:
156
+ results.update({
157
+ "rho": rho,
158
+ "Q": Q,
159
+ "vcov": vcov,
160
+ "beta": beta,
161
+ "residuals": residuals,
162
+ "XTX": XTX,
163
+ "inv_vcov": inv_vcov,
164
+ "X_l": X_l,
165
+ "best_val": value
166
+ })
167
+
168
+ return value
169
+
170
+ except Exception as e:
171
+ warnings.warn(f"Optimization failed at rho={rho:.4f}: {e}", RuntimeWarning)
172
+ return np.inf
173
+
174
+ # Minimize scalar function within bounds
175
+ result = minimize_scalar(objective, bounds=(self.rho_min, self.rho_max), method="bounded")
176
+
177
+ if not result.success:
178
+ raise RuntimeError(f"Rho optimization failed: {result.message}")
179
+
180
+ if self.verbose:
181
+ self.logger.info(f"Optimal rho found: {results['rho']:.6f}")
182
+
183
+ # Compute inference statistics
184
+ XTX_inv = pinv(results["XTX"])
185
+ std_errors = np.sqrt(np.diag(XTX_inv))
186
+ t_stats = results["beta"].flatten() / std_errors
187
+ p_values = 2 * (1 - norm.cdf(np.abs(t_stats)))
188
+ significance = ["***" if p < 0.01 else "**" if p < 0.05 else "*" if p < 0.1 else "" for p in p_values]
189
+
190
+ summary_df = pd.DataFrame({
191
+ "Coef.": results["beta"].flatten(),
192
+ "Std.Err.": std_errors,
193
+ "t-stat": t_stats,
194
+ "P>|t|": p_values,
195
+ "Signif.": significance
196
+ })
197
+
198
+ return {
199
+ "rho": results["rho"],
200
+ "Q": results["Q"],
201
+ "vcov": results["vcov"],
202
+ "beta": results["beta"],
203
+ "residuals": results["residuals"],
204
+ "std_errors": std_errors,
205
+ "t_stats": t_stats,
206
+ "p_values": p_values,
207
+ "significance": significance,
208
+ "summary": summary_df
209
+ }
210
+
211
+ except Exception as e:
212
+ raise RuntimeError(f"Optimization process failed: {e}")
@@ -0,0 +1,82 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import logging
4
+ from tempdisagg.utils.logging_utils import VerboseLogger
5
+
6
+ class TemporalAggregator:
7
+ """
8
+ Aggregates a high-frequency time series into a low-frequency series
9
+ using one of four supported rules: sum, average, first, or last.
10
+ """
11
+
12
+ def __init__(self, conversion="sum", freq_ratio=None, grain_col="Grain", index_col="Index", verbose=False):
13
+ """
14
+ Parameters
15
+ ----------
16
+ conversion : str
17
+ Aggregation rule: 'sum', 'average', 'first', or 'last'.
18
+ freq_ratio : int
19
+ Number of high-frequency units per low-frequency unit.
20
+ grain_col : str
21
+ Column name for high-frequency grain.
22
+ index_col : str
23
+ Column name for low-frequency index.
24
+ verbose : bool
25
+ Enable logging messages.
26
+ """
27
+ self.conversion = conversion
28
+ self.freq_ratio = freq_ratio
29
+ self.grain_col = grain_col
30
+ self.index_col = index_col
31
+ self.verbose = verbose
32
+ self.logger = VerboseLogger(f"{__name__}.{id(self)}", verbose=self.verbose).get_logger()
33
+
34
+ valid = ["sum", "average", "first", "last"]
35
+ if self.conversion not in valid:
36
+ raise ValueError(f"Invalid conversion: {self.conversion}. Must be one of {valid}.")
37
+
38
+ def aggregate(self, df, value_col):
39
+ """
40
+ Aggregate the high-frequency series to low-frequency.
41
+
42
+ Parameters
43
+ ----------
44
+ df : pd.DataFrame
45
+ Input DataFrame containing high-frequency data.
46
+ value_col : str
47
+ Name of the value column to aggregate.
48
+
49
+ Returns
50
+ -------
51
+ pd.DataFrame
52
+ Low-frequency aggregated series.
53
+ """
54
+ if not isinstance(df, pd.DataFrame):
55
+ raise TypeError("Input must be a pandas DataFrame.")
56
+ if value_col not in df.columns:
57
+ raise ValueError(f"Column '{value_col}' not found in input DataFrame.")
58
+
59
+ if self.index_col not in df.columns or self.grain_col not in df.columns:
60
+ raise ValueError("Both 'index_col' and 'grain_col' must exist in the DataFrame.")
61
+
62
+ df = df.copy()
63
+ df = df.sort_values([self.index_col, self.grain_col])
64
+
65
+ if self.verbose:
66
+ self.logger.info(f"Aggregating using rule: {self.conversion}")
67
+
68
+ if self.conversion == "sum":
69
+ agg_func = "sum"
70
+ elif self.conversion == "average":
71
+ agg_func = "mean"
72
+ elif self.conversion == "first":
73
+ agg_func = "first"
74
+ elif self.conversion == "last":
75
+ agg_func = "last"
76
+ else:
77
+ raise ValueError(f"Unsupported conversion: {self.conversion}")
78
+
79
+ result = df.groupby(self.index_col, as_index=False)[value_col].agg(agg_func)
80
+ result.rename(columns={value_col: f"{value_col}_agg"}, inplace=True)
81
+
82
+ return result
@@ -0,0 +1,4 @@
1
+ from .tempdisagg_model import TempDisaggModel
2
+ from .models_handler import ModelsHandler
3
+
4
+ __all__ = ["TempDisaggModel", "ModelsHandler"]