dfm-python 0.1.1__py3-none-any.whl → 0.1.2__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.
dfm_python/dfm.py CHANGED
@@ -21,16 +21,7 @@ except ImportError:
21
21
  SCIPY_AVAILABLE = False
22
22
 
23
23
  # Import rem_nans_spline from utils to avoid duplication
24
- import sys
25
- from pathlib import Path
26
- try:
27
- from ..utils.data_utils import rem_nans_spline
28
- except (ImportError, ValueError):
29
- # Fallback: add parent directory to path if relative import fails
30
- utils_path = Path(__file__).parent.parent.parent / 'utils'
31
- if str(utils_path) not in sys.path:
32
- sys.path.insert(0, str(utils_path.parent))
33
- from utils.data_utils import rem_nans_spline
24
+ from .utils.data_utils import rem_nans_spline
34
25
 
35
26
  def _rem_nans_spline(X: np.ndarray, method: int = 2, k: int = 3):
36
27
  """Treat NaNs in dataset for DFM estimation (wrapper for rem_nans_spline).
@@ -0,0 +1,12 @@
1
+ """Utility functions for data preprocessing and summary statistics."""
2
+
3
+ from .data_utils import rem_nans_spline, summarize
4
+
5
+ __all__ = [
6
+ 'rem_nans_spline',
7
+ 'summarize',
8
+ ]
9
+
10
+ # For backward compatibility
11
+ from . import data_utils
12
+
@@ -0,0 +1,155 @@
1
+ """Utility functions for data preprocessing and summary statistics."""
2
+
3
+ import numpy as np
4
+ from scipy.interpolate import CubicSpline
5
+ from scipy.signal import lfilter
6
+ from typing import Tuple, Optional
7
+ import logging
8
+
9
+ # Set up logger
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ def rem_nans_spline(X: np.ndarray, method: int = 2, k: int = 3) -> Tuple[np.ndarray, np.ndarray]:
14
+ """Treat NaNs in dataset for DFM estimation."""
15
+ T, N = X.shape
16
+ indNaN = np.isnan(X)
17
+
18
+ def _remove_leading_trailing(threshold: float):
19
+ """Remove rows with NaN count above threshold."""
20
+ rem = np.sum(indNaN, axis=1) > (N * threshold if threshold < 1 else threshold)
21
+ nan_lead = np.cumsum(rem) == np.arange(1, T + 1)
22
+ nan_end = np.cumsum(rem[::-1]) == np.arange(1, T + 1)[::-1]
23
+ return ~(nan_lead | nan_end)
24
+
25
+ def _fill_missing(x: np.ndarray, mask: np.ndarray):
26
+ """Fill missing values using spline interpolation and moving average."""
27
+ # Ensure mask matches x length
28
+ if len(mask) != len(x):
29
+ mask = mask[:len(x)]
30
+
31
+ non_nan = np.where(~mask)[0]
32
+ if len(non_nan) < 2:
33
+ return x
34
+
35
+ x_filled = x.copy()
36
+ if non_nan[-1] >= len(x):
37
+ non_nan = non_nan[non_nan < len(x)]
38
+ if len(non_nan) < 2:
39
+ return x
40
+
41
+ x_filled[non_nan[0]:non_nan[-1]+1] = CubicSpline(non_nan, x[non_nan])(np.arange(non_nan[0], min(non_nan[-1]+1, len(x))))
42
+ x_filled[mask[:len(x_filled)]] = np.nanmedian(x_filled)
43
+
44
+ # Moving average filter
45
+ pad = np.concatenate([np.full(k, x_filled[0]), x_filled, np.full(k, x_filled[-1])])
46
+ ma = lfilter(np.ones(2*k+1)/(2*k+1), 1, pad)[2*k+1:]
47
+ if len(ma) == len(x_filled):
48
+ x_filled[mask[:len(x_filled)]] = ma[mask[:len(x_filled)]]
49
+ return x_filled
50
+
51
+ if method == 1:
52
+ # Replace all missing values
53
+ for i in range(N):
54
+ mask = indNaN[:, i]
55
+ x = X[:, i].copy()
56
+ x[mask] = np.nanmedian(x)
57
+ pad = np.concatenate([np.full(k, x[0]), x, np.full(k, x[-1])])
58
+ ma = lfilter(np.ones(2*k+1)/(2*k+1), 1, pad)[2*k+1:]
59
+ x[mask] = ma[mask]
60
+ X[:, i] = x
61
+
62
+ elif method == 2:
63
+ # Remove >80% NaN rows, then fill
64
+ mask = _remove_leading_trailing(0.8)
65
+ X = X[mask]
66
+ indNaN = np.isnan(X)
67
+ for i in range(N):
68
+ X[:, i] = _fill_missing(X[:, i], indNaN[:, i])
69
+
70
+ elif method == 3:
71
+ # Only remove all-NaN rows
72
+ mask = _remove_leading_trailing(N)
73
+ X = X[mask]
74
+ indNaN = np.isnan(X)
75
+
76
+ elif method == 4:
77
+ # Remove all-NaN rows, then fill
78
+ mask = _remove_leading_trailing(N)
79
+ X = X[mask]
80
+ indNaN = np.isnan(X)
81
+ for i in range(N):
82
+ X[:, i] = _fill_missing(X[:, i], indNaN[:, i])
83
+
84
+ elif method == 5:
85
+ # Fill missing values
86
+ for i in range(N):
87
+ X[:, i] = _fill_missing(X[:, i], indNaN[:, i])
88
+
89
+ return X, indNaN
90
+
91
+
92
+ def summarize(X: np.ndarray, Time, config, vintage: Optional[str] = None) -> None:
93
+ """Display data summary table."""
94
+ T, N = X.shape
95
+
96
+ print("\n\n\nTable 2: Data Summary")
97
+ print(f"N = {N:4d} data series")
98
+
99
+ try:
100
+ time_start = Time[0].strftime('%Y-%m-%d') if hasattr(Time[0], 'strftime') else str(Time[0])
101
+ time_end = Time[-1].strftime('%Y-%m-%d') if hasattr(Time[-1], 'strftime') else str(Time[-1])
102
+ except (AttributeError, ValueError, TypeError) as e:
103
+ _logger.debug(f"summarize: Failed to format time range: {type(e).__name__}: {str(e)}")
104
+ time_start = time_end = "N/A"
105
+
106
+ print(f"T = {T:4d} observations from {time_start:10s} to {time_end:10s}\n")
107
+ print(f"{'Data Series':30s} | {'Observations':17s} {'Units':12s} "
108
+ f"{'Frequency':10s} {'Mean':8s} {'Std. Dev.':8s} {'Min':8s} {'Max':8s}")
109
+ print("-" * 130)
110
+
111
+ for i in range(N):
112
+ t_obs = ~np.isnan(X[:, i])
113
+ obs_idx = np.where(t_obs)[0]
114
+
115
+ name = config.SeriesName[i][:27] + "..." if len(config.SeriesName[i]) > 30 else config.SeriesName[i]
116
+ sid = f"[{config.SeriesID[i][:25]}...]" if len(config.SeriesID[i]) > 28 else f"[{config.SeriesID[i]}]"
117
+
118
+ freq = 'Monthly' if config.Frequency[i] == 'm' else 'Quarterly'
119
+ trans = config.Transformation[i]
120
+ units_t = 'MoM%' if trans == 'pch' and config.Frequency[i] == 'm' else \
121
+ 'QoQ% AR' if trans == 'pca' and config.Frequency[i] == 'q' else \
122
+ config.UnitsTransformed[i]
123
+
124
+ date_range = "N/A"
125
+ if len(obs_idx) > 0:
126
+ try:
127
+ fmt = '%b %Y' if config.Frequency[i] == 'm' else '%Y-%m'
128
+ # Use iloc for positional indexing
129
+ if hasattr(Time, 'iloc'):
130
+ date_range = f"{Time.iloc[obs_idx[0]].strftime(fmt)}-{Time.iloc[obs_idx[-1]].strftime(fmt)}"
131
+ else:
132
+ date_range = f"{Time[obs_idx[0]].strftime(fmt)}-{Time[obs_idx[-1]].strftime(fmt)}"
133
+ except (AttributeError, ValueError, TypeError) as e:
134
+ # Fallback to positional indexing
135
+ _logger.debug(f"summarize: Failed to format date range with strftime: {type(e).__name__}: {str(e)}")
136
+ try:
137
+ if hasattr(Time, 'iloc'):
138
+ date_range = f"{Time.iloc[obs_idx[0]]}-{Time.iloc[obs_idx[-1]]}"
139
+ else:
140
+ date_range = f"{Time[obs_idx[0]]}-{Time[obs_idx[-1]]}"
141
+ except (AttributeError, ValueError, TypeError, IndexError) as e2:
142
+ _logger.debug(f"summarize: Failed to format date range fallback: {type(e2).__name__}: {str(e2)}")
143
+ date_range = "N/A"
144
+
145
+ y = X[t_obs, i]
146
+ if len(y) == 0:
147
+ stats = (np.nan, np.nan, np.nan, np.nan)
148
+ else:
149
+ stats = np.nanmean(y), np.nanstd(y), np.nanmin(y) if len(y) > 0 else np.nan, np.nanmax(y) if len(y) > 0 else np.nan
150
+
151
+ print(f"{name:30s} | {len(obs_idx):17d} {units_t:12s} {freq:10s} "
152
+ f"{stats[0]:8.1f} {stats[1]:8.1f} {stats[2]:8.1f} {stats[3]:8.1f}")
153
+ print(f"{sid:30s} | {date_range:17s}\n")
154
+
155
+ print("\n\n\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Dynamic Factor Model (DFM) estimation and nowcasting in Python
5
5
  Project-URL: Homepage, https://github.com/yourusername/dfm-python
6
6
  Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
@@ -0,0 +1,12 @@
1
+ dfm_python/__init__.py,sha256=_74eT10hQse0xb22IsiTNKLe7daY_0YbKHjMkdhO3U4,961
2
+ dfm_python/config.py,sha256=dBhBO-ckKlzm0DPImOhdP9eDo1m0Nfh2vnanPUGMVfs,19005
3
+ dfm_python/data_loader.py,sha256=v81w3V_IkV9i6V5WYZOlky8mRTyPTaTRfhDKQY2bBLQ,14507
4
+ dfm_python/dfm.py,sha256=UDMXEMjVY3_d5hd9_AgIWHrTdDDZaQU05HH_Y7erwPI,107948
5
+ dfm_python/kalman.py,sha256=RS9obW1eT7Q6L5NeXk9vB4ZYZbg96d0yYm4gk6NaO1M,14712
6
+ dfm_python/news.py,sha256=wLT3ppauByLZpDRhN3VMhCk6rIgxFJcFjJJG5GwFrdI,30150
7
+ dfm_python/utils/__init__.py,sha256=y6GDS1VXVcsjnKmlhEaTGyElwm65DW6Z8HyzOByPwI0,234
8
+ dfm_python/utils/data_utils.py,sha256=q7ugdz5Jr0wNhY3tY_TJiLHs5jzu0s4br4tm4ivI8Cg,6369
9
+ dfm_python-0.1.2.dist-info/METADATA,sha256=CpWELdzB5-uD2xelNnpfMS0hZKUmO4co5hLMl2ZbcPY,11035
10
+ dfm_python-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ dfm_python-0.1.2.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
12
+ dfm_python-0.1.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- dfm_python/__init__.py,sha256=_74eT10hQse0xb22IsiTNKLe7daY_0YbKHjMkdhO3U4,961
2
- dfm_python/config.py,sha256=dBhBO-ckKlzm0DPImOhdP9eDo1m0Nfh2vnanPUGMVfs,19005
3
- dfm_python/data_loader.py,sha256=v81w3V_IkV9i6V5WYZOlky8mRTyPTaTRfhDKQY2bBLQ,14507
4
- dfm_python/dfm.py,sha256=MMIOrkWKrN5u8eXl3526JLHb8wVDMyc8hH3Uak7wbVg,108301
5
- dfm_python/kalman.py,sha256=RS9obW1eT7Q6L5NeXk9vB4ZYZbg96d0yYm4gk6NaO1M,14712
6
- dfm_python/news.py,sha256=wLT3ppauByLZpDRhN3VMhCk6rIgxFJcFjJJG5GwFrdI,30150
7
- dfm_python-0.1.1.dist-info/METADATA,sha256=IeXFro6fYWKjajX6xYb-y6SjVBMEuCYDXm-_3tTjNf0,11035
8
- dfm_python-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- dfm_python-0.1.1.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
10
- dfm_python-0.1.1.dist-info/RECORD,,