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 +1 -10
- dfm_python/utils/__init__.py +12 -0
- dfm_python/utils/data_utils.py +155 -0
- {dfm_python-0.1.1.dist-info → dfm_python-0.1.2.dist-info}/METADATA +1 -1
- dfm_python-0.1.2.dist-info/RECORD +12 -0
- dfm_python-0.1.1.dist-info/RECORD +0 -10
- {dfm_python-0.1.1.dist-info → dfm_python-0.1.2.dist-info}/WHEEL +0 -0
- {dfm_python-0.1.1.dist-info → dfm_python-0.1.2.dist-info}/licenses/LICENSE +0 -0
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
|
|
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,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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|