dfm-python 0.1.3__tar.gz → 0.1.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.
- dfm_python-0.1.4/BUG_ANALYSIS.md +242 -0
- dfm_python-0.1.4/CHANGELOG.md +54 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/PKG-INFO +1 -1
- {dfm_python-0.1.3 → dfm_python-0.1.4}/pyproject.toml +1 -1
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/__init__.py +1 -1
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/dfm.py +42 -15
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/test/test_dfm.py +95 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/.gitignore +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/.python-version +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/LICENSE +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/MANIFEST.in +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/README.md +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/config/example_config.yaml +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/data/sample_data.csv +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/spec/example_spec.csv +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/__init__.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/config.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/data_loader.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/kalman.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/news.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/utils/__init__.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/utils/aggregation.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/dfm_python/utils/data_utils.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/test/__init__.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/test/test_kalman.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/test/test_news.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/src/test/test_synthetic.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/test_comprehensive.py +0 -0
- {dfm_python-0.1.3 → dfm_python-0.1.4}/uv.lock +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Bug Analysis Report - dfm-python Package
|
|
2
|
+
|
|
3
|
+
## Date: 2025-11-07
|
|
4
|
+
|
|
5
|
+
### Summary
|
|
6
|
+
This document identifies potential bugs found during code inspection of the dfm-python package.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Bug #1: A_temp Variable Scoping Issue (POTENTIAL BUG)
|
|
11
|
+
|
|
12
|
+
### Location
|
|
13
|
+
- **File:** `src/dfm_python/dfm.py`
|
|
14
|
+
- **Function:** `init_conditions()`
|
|
15
|
+
- **Lines:** 667-693
|
|
16
|
+
|
|
17
|
+
### Description
|
|
18
|
+
The variable `A_temp` is created inside a `try` block (line 676) but may persist across block iterations, similar to the `ff` bug that was just fixed.
|
|
19
|
+
|
|
20
|
+
### Code Context
|
|
21
|
+
```python
|
|
22
|
+
for i in range(n_b): # Block loop
|
|
23
|
+
# ...
|
|
24
|
+
if len(idx_iM) > 0:
|
|
25
|
+
# ...
|
|
26
|
+
if Z_lag.shape[0] > 0 and Z_lag.shape[1] > 0:
|
|
27
|
+
try:
|
|
28
|
+
A_temp = inv(Z_lag.T @ Z_lag) @ Z_lag.T @ z # Line 676
|
|
29
|
+
A_i[:int(r_i), :int(r_i * p)] = A_temp.T
|
|
30
|
+
except (np.linalg.LinAlgError, ValueError) as e:
|
|
31
|
+
# Exception caught - A_temp NOT created
|
|
32
|
+
A_i[:int(r_i), :int(r_i * p)] = 0.0
|
|
33
|
+
|
|
34
|
+
# ...
|
|
35
|
+
if len(z) > 0:
|
|
36
|
+
e = z - Z_lag @ A_temp if 'A_temp' in locals() else z # Line 693
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Problem
|
|
40
|
+
1. **Block 0**: If `try` succeeds, `A_temp` is created with dimensions for block 0
|
|
41
|
+
2. **Block 1**: If `try` fails (exception caught), `A_temp` is NOT created
|
|
42
|
+
3. **Line 693**: Checks `'A_temp' in locals()` - this returns `True` because `A_temp` from Block 0 still exists!
|
|
43
|
+
4. **Result**: Uses `A_temp` from Block 0 with `Z_lag` from Block 1 → **dimension mismatch or incorrect computation**
|
|
44
|
+
|
|
45
|
+
### Impact
|
|
46
|
+
- **Severity:** Medium
|
|
47
|
+
- **Likelihood:** Low (only occurs if one block's try succeeds and next block's try fails)
|
|
48
|
+
- **Affected:** Multi-block models where OLS regression fails for some blocks
|
|
49
|
+
|
|
50
|
+
### Suggested Fix
|
|
51
|
+
Reset `A_temp = None` at the start of each block iteration, similar to the `ff` fix:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
for i in range(n_b):
|
|
55
|
+
r_i = int(r[i])
|
|
56
|
+
ff = None
|
|
57
|
+
A_temp = None # <-- ADD THIS
|
|
58
|
+
|
|
59
|
+
# ... rest of code ...
|
|
60
|
+
|
|
61
|
+
if len(z) > 0:
|
|
62
|
+
e = z - Z_lag @ A_temp if A_temp is not None else z # <-- CHANGE CHECK
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Bug #2: bl_idxM and bl_idxQ Initialization Issue (POTENTIAL BUG)
|
|
68
|
+
|
|
69
|
+
### Location
|
|
70
|
+
- **File:** `src/dfm_python/dfm.py`
|
|
71
|
+
- **Function:** `em_step()`
|
|
72
|
+
- **Lines:** 1482-1517
|
|
73
|
+
|
|
74
|
+
### Description
|
|
75
|
+
`bl_idxM` and `bl_idxQ` are initialized as empty lists `[]` but then used with `np.hstack()` which expects numpy arrays.
|
|
76
|
+
|
|
77
|
+
### Code Context
|
|
78
|
+
```python
|
|
79
|
+
bl_idxM = [] # Line 1482 - Python list
|
|
80
|
+
bl_idxQ = [] # Line 1483 - Python list
|
|
81
|
+
|
|
82
|
+
for i in range(num_blocks):
|
|
83
|
+
# ...
|
|
84
|
+
if len(bl_idxM) == 0: # Line 1504
|
|
85
|
+
bl_idxM = bl_col_monthly # numpy array
|
|
86
|
+
bl_idxQ = bl_col_quarterly # numpy array
|
|
87
|
+
else:
|
|
88
|
+
bl_idxM = np.hstack([bl_idxM, bl_col_monthly]) # Line 1508
|
|
89
|
+
bl_idxQ = np.hstack([bl_idxQ, bl_col_quarterly]) # Line 1509
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Problem
|
|
93
|
+
- First iteration: `bl_idxM` is `[]` (list), then assigned numpy array → OK
|
|
94
|
+
- Second iteration: `bl_idxM` is numpy array, `np.hstack([bl_idxM, ...])` → OK
|
|
95
|
+
- **BUT**: If `num_blocks == 0` or loop doesn't execute, `bl_idxM` remains `[]` (list)
|
|
96
|
+
- Line 1516: `bl_idxM.astype(bool)` on a list → **AttributeError**
|
|
97
|
+
|
|
98
|
+
### Impact
|
|
99
|
+
- **Severity:** Low
|
|
100
|
+
- **Likelihood:** Very Low (only if `num_blocks == 0`, which shouldn't happen in practice)
|
|
101
|
+
- **Affected:** Edge case with empty block structure
|
|
102
|
+
|
|
103
|
+
### Suggested Fix
|
|
104
|
+
Initialize as empty numpy arrays instead of lists:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
bl_idxM = np.array([]).reshape(0, n_bl) # Empty 2D array
|
|
108
|
+
bl_idxQ = np.array([]).reshape(0, n_bl) # Empty 2D array
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or check before converting:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
if len(bl_idxM) > 0:
|
|
115
|
+
bl_idxM = bl_idxM.astype(bool)
|
|
116
|
+
bl_idxQ = bl_idxQ.astype(bool)
|
|
117
|
+
else:
|
|
118
|
+
bl_idxM = np.array([]).reshape(0, n_bl).astype(bool)
|
|
119
|
+
bl_idxQ = np.array([]).reshape(0, n_bl).astype(bool)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Bug #3: F Variable May Be Uninitialized (POTENTIAL BUG)
|
|
125
|
+
|
|
126
|
+
### Location
|
|
127
|
+
- **File:** `src/dfm_python/dfm.py`
|
|
128
|
+
- **Function:** `init_conditions()`
|
|
129
|
+
- **Lines:** 547-559, 636
|
|
130
|
+
|
|
131
|
+
### Description
|
|
132
|
+
The variable `F` is created inside `if len(idx_iM) > 0:` block but may be referenced in the `else` branch.
|
|
133
|
+
|
|
134
|
+
### Code Context
|
|
135
|
+
```python
|
|
136
|
+
if len(idx_iM) > 0:
|
|
137
|
+
# ...
|
|
138
|
+
F = None # Line 548
|
|
139
|
+
max_lag = max(p + 1, pC)
|
|
140
|
+
for kk in range(max_lag):
|
|
141
|
+
lag_data = f[pC - kk:T - kk, :]
|
|
142
|
+
if F is None:
|
|
143
|
+
F = lag_data
|
|
144
|
+
else:
|
|
145
|
+
F = np.hstack([F, lag_data])
|
|
146
|
+
# ...
|
|
147
|
+
ff = F[:, :int(r_i * pC)] # Line 566
|
|
148
|
+
|
|
149
|
+
# Later...
|
|
150
|
+
else:
|
|
151
|
+
# ...
|
|
152
|
+
if len(idx_iM) > 0 and 'f' in locals() and f is not None:
|
|
153
|
+
F = np.hstack([f] + [np.roll(f, -kk, axis=0) for kk in range(1, max(p+1, pC))]) # Line 636
|
|
154
|
+
ff = F[:, :int(r_i * pC)]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Problem
|
|
158
|
+
The `else` branch at line 630 checks `if len(idx_iM) > 0` - but if we're in the `else` branch, it means the outer `if len(idx_iM) > 0:` (line 435) was False, so `len(idx_iM) == 0`. This condition can never be True!
|
|
159
|
+
|
|
160
|
+
### Impact
|
|
161
|
+
- **Severity:** Low (dead code)
|
|
162
|
+
- **Likelihood:** N/A (code never executes)
|
|
163
|
+
- **Affected:** None (unreachable code)
|
|
164
|
+
|
|
165
|
+
### Suggested Fix
|
|
166
|
+
Remove the unreachable condition or fix the logic:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
else:
|
|
170
|
+
# If no monthly series, create dummy ff
|
|
171
|
+
ff_padded_i = np.zeros((T, int(pC * r_i)))
|
|
172
|
+
# Remove the unreachable if statement
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Bug #4: Dimension Mismatch in bl_idxQ_i (POTENTIAL BUG)
|
|
178
|
+
|
|
179
|
+
### Location
|
|
180
|
+
- **File:** `src/dfm_python/dfm.py`
|
|
181
|
+
- **Function:** `em_step()`
|
|
182
|
+
- **Lines:** 1700-1704
|
|
183
|
+
|
|
184
|
+
### Description
|
|
185
|
+
`bl_idxQ_i` is computed but may have incorrect dimensions when used to reshape empty arrays.
|
|
186
|
+
|
|
187
|
+
### Code Context
|
|
188
|
+
```python
|
|
189
|
+
if R_con.size > 0:
|
|
190
|
+
bl_idxQ_i = np.where(bl_idxQ[i, :])[0]
|
|
191
|
+
if len(bl_idxQ_i) > 0 and bl_idxQ_i.max() < R_con.shape[1]:
|
|
192
|
+
# ...
|
|
193
|
+
else:
|
|
194
|
+
R_con_i = np.array([]).reshape(0, len(bl_idxQ_i)) # Line 1700
|
|
195
|
+
q_con_i = np.array([])
|
|
196
|
+
else:
|
|
197
|
+
bl_idxQ_i = np.where(bl_idxQ[i, :])[0]
|
|
198
|
+
R_con_i = np.array([]).reshape(0, len(bl_idxQ_i)) # Line 1704
|
|
199
|
+
q_con_i = np.array([])
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Problem
|
|
203
|
+
If `bl_idxQ_i` is empty (`len(bl_idxQ_i) == 0`), then `reshape(0, 0)` creates a 0x0 array, which may cause issues in subsequent operations expecting a specific shape.
|
|
204
|
+
|
|
205
|
+
### Impact
|
|
206
|
+
- **Severity:** Low
|
|
207
|
+
- **Likelihood:** Low (only if no quarterly series in block)
|
|
208
|
+
- **Affected:** Blocks with no quarterly series
|
|
209
|
+
|
|
210
|
+
### Suggested Fix
|
|
211
|
+
Add a check for empty `bl_idxQ_i`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
if len(bl_idxQ_i) > 0:
|
|
215
|
+
R_con_i = np.array([]).reshape(0, len(bl_idxQ_i))
|
|
216
|
+
else:
|
|
217
|
+
R_con_i = np.array([]).reshape(0, 1) # Default to 1 column
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Recommendations
|
|
223
|
+
|
|
224
|
+
### Priority Order
|
|
225
|
+
1. **Bug #1 (A_temp)**: Fix immediately - similar to the `ff` bug that was just fixed
|
|
226
|
+
2. **Bug #2 (bl_idxM/bl_idxQ)**: Fix for robustness - edge case but easy to fix
|
|
227
|
+
3. **Bug #3 (F variable)**: Clean up dead code
|
|
228
|
+
4. **Bug #4 (bl_idxQ_i)**: Low priority - may not cause issues in practice
|
|
229
|
+
|
|
230
|
+
### Testing Recommendations
|
|
231
|
+
1. Create test case for multi-block model where one block's OLS regression fails
|
|
232
|
+
2. Test edge case with `num_blocks == 0` (should be caught by validation, but test anyway)
|
|
233
|
+
3. Test blocks with no quarterly series to verify Bug #4 doesn't cause issues
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Notes
|
|
238
|
+
- All bugs found are potential issues that may not manifest in typical usage
|
|
239
|
+
- Bug #1 is the most critical as it's similar to the recently fixed `ff` scoping bug
|
|
240
|
+
- Most issues are related to variable scoping across loop iterations
|
|
241
|
+
- Consider adding more explicit variable resets at loop starts for safety
|
|
242
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.4] - 2024-11-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fixed critical bug: `ff` variable scoping issue in `init_conditions()` causing dimension mismatch errors in multi-block models with different factor counts
|
|
7
|
+
- Fixed `A_temp` variable scoping issue that could cause incorrect computations when OLS regression fails for some blocks
|
|
8
|
+
- Fixed `bl_idxM` and `bl_idxQ` initialization issue that could cause AttributeError when no blocks are processed
|
|
9
|
+
- Removed unreachable code in `init_conditions()` else branch
|
|
10
|
+
- Improved empty array handling in `em_step()` for quarterly series constraints
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Enhanced variable scoping safety by resetting block-specific variables at start of each iteration
|
|
14
|
+
- Improved error handling for edge cases with empty arrays
|
|
15
|
+
|
|
16
|
+
## [0.1.3] - 2024-11-07
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Comprehensive README with detailed inputs/outputs documentation
|
|
20
|
+
- Clock-based mixed-frequency framework documentation
|
|
21
|
+
- Enhanced module-level docstrings throughout the codebase
|
|
22
|
+
- Detailed API reference with examples
|
|
23
|
+
- Improved error messages with context and solutions
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Optimized code performance (removed redundant computations)
|
|
27
|
+
- Improved memory usage (replaced unnecessary copies with views)
|
|
28
|
+
- Enhanced code documentation and comments
|
|
29
|
+
- Updated package description and metadata
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Removed duplicate `frequencies_array` computation
|
|
33
|
+
- Fixed `optNaN` dictionary mutation issue
|
|
34
|
+
- Improved code organization and clarity
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
- Complete input/output specifications
|
|
38
|
+
- 5 comprehensive usage examples
|
|
39
|
+
- Troubleshooting guide with solutions
|
|
40
|
+
- Architecture overview
|
|
41
|
+
- Clock-based framework explanation
|
|
42
|
+
|
|
43
|
+
## [0.1.2] - Previous release
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- Fixed `ModuleNotFoundError: No module named 'utils'` by moving utils into dfm_python package
|
|
47
|
+
- Improved import paths and package structure
|
|
48
|
+
|
|
49
|
+
## [0.1.1] - Initial release
|
|
50
|
+
|
|
51
|
+
- Initial PyPI release
|
|
52
|
+
- Core DFM estimation functionality
|
|
53
|
+
- Mixed-frequency data support
|
|
54
|
+
- News decomposition
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -422,6 +422,11 @@ def init_conditions(x: np.ndarray, r: np.ndarray, p: int, blocks: np.ndarray,
|
|
|
422
422
|
for i in range(n_b):
|
|
423
423
|
r_i = int(r[i]) # r_i = 1 when block is loaded (ensure integer)
|
|
424
424
|
|
|
425
|
+
# Reset block-specific variables at start of each iteration to prevent dimension mismatch
|
|
426
|
+
# These variables are block-specific and must be reset to avoid using values from previous blocks
|
|
427
|
+
ff = None
|
|
428
|
+
A_temp = None
|
|
429
|
+
|
|
425
430
|
# Observation equation
|
|
426
431
|
C_i = np.zeros((N, int(r_i * ppC)))
|
|
427
432
|
idx_i = np.where(blocks[:, i] == 1)[0] # Series loading block i
|
|
@@ -605,7 +610,18 @@ def init_conditions(x: np.ndarray, r: np.ndarray, p: int, blocks: np.ndarray,
|
|
|
605
610
|
|
|
606
611
|
# Pad ff with zeros to match res dimensions (T x N) - BLOCK-SPECIFIC
|
|
607
612
|
# This must be inside the block loop to handle different r_i per block
|
|
608
|
-
if
|
|
613
|
+
if ff is not None:
|
|
614
|
+
# Validate dimensions match before stacking (safety check)
|
|
615
|
+
expected_width = int(pC * r_i)
|
|
616
|
+
if ff.shape[1] != expected_width:
|
|
617
|
+
# This should not happen if ff is properly reset, but add safety check
|
|
618
|
+
_logger.warning(
|
|
619
|
+
f"init_conditions: Block {i+1} ff dimension mismatch detected. "
|
|
620
|
+
f"Expected width {expected_width}, got {ff.shape[1]}. Resetting ff."
|
|
621
|
+
)
|
|
622
|
+
ff = None
|
|
623
|
+
|
|
624
|
+
if ff is not None:
|
|
609
625
|
# ff has shape (T - pC + 1, r_i * pC) from lag_data
|
|
610
626
|
# Need to pad to match res shape (T, r_i * pC)
|
|
611
627
|
ff_padded_i = np.vstack([np.zeros((pC - 1, int(pC * r_i))), ff])
|
|
@@ -615,12 +631,8 @@ def init_conditions(x: np.ndarray, r: np.ndarray, p: int, blocks: np.ndarray,
|
|
|
615
631
|
else:
|
|
616
632
|
# If no monthly series, create dummy ff
|
|
617
633
|
ff_padded_i = np.zeros((T, int(pC * r_i)))
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
ff = F[:, :int(r_i * pC)]
|
|
621
|
-
ff_padded_i = np.vstack([np.zeros((pC - 1, int(pC * r_i))), ff])
|
|
622
|
-
if ff_padded_i.shape[0] < T:
|
|
623
|
-
ff_padded_i = np.vstack([ff_padded_i, np.zeros((T - ff_padded_i.shape[0], int(pC * r_i)))])
|
|
634
|
+
# Note: If we're in this else branch, len(idx_iM) == 0, so the condition below is unreachable
|
|
635
|
+
# but kept for safety in case of edge cases
|
|
624
636
|
|
|
625
637
|
# Residual calculations - ensure shapes match (only for this block's series)
|
|
626
638
|
# C_i has shape (N, r_i * ppC), but we only use the first r_i * pC columns for residuals
|
|
@@ -673,7 +685,7 @@ def init_conditions(x: np.ndarray, r: np.ndarray, p: int, blocks: np.ndarray,
|
|
|
673
685
|
|
|
674
686
|
Q_i = np.zeros((int(ppC * r_i), int(ppC * r_i)))
|
|
675
687
|
if len(z) > 0:
|
|
676
|
-
e = z - Z_lag @ A_temp if
|
|
688
|
+
e = z - Z_lag @ A_temp if A_temp is not None else z
|
|
677
689
|
# Clean e before covariance calculation
|
|
678
690
|
e_clean = e.copy()
|
|
679
691
|
e_clean[np.isnan(e_clean)] = 0.0
|
|
@@ -1462,8 +1474,9 @@ def em_step(y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
|
1462
1474
|
n_bl = bl.shape[0] # Number of unique loadings
|
|
1463
1475
|
|
|
1464
1476
|
# Initialize indices: These help with subsetting
|
|
1465
|
-
|
|
1466
|
-
|
|
1477
|
+
# Initialize as None - will be set to numpy arrays on first iteration
|
|
1478
|
+
bl_idxM = None # Indicator for monthly factor loadings
|
|
1479
|
+
bl_idxQ = None # Indicator for quarterly factor loadings
|
|
1467
1480
|
R_con_list = [] # List to build block diagonal
|
|
1468
1481
|
|
|
1469
1482
|
# Loop through each block to build indices
|
|
@@ -1484,7 +1497,7 @@ def em_step(y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
|
1484
1497
|
bl_col_quarterly = np.repeat(bl[:, i:i+1], r_i * ppC, axis=1) # n_bl x (r_i * ppC)
|
|
1485
1498
|
|
|
1486
1499
|
# Append columns (MATLAB uses horizontal concatenation)
|
|
1487
|
-
if
|
|
1500
|
+
if bl_idxM is None:
|
|
1488
1501
|
bl_idxM = bl_col_monthly
|
|
1489
1502
|
bl_idxQ = bl_col_quarterly
|
|
1490
1503
|
else:
|
|
@@ -1496,8 +1509,14 @@ def em_step(y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
|
1496
1509
|
R_con_list.append(np.kron(R_mat, np.eye(r_i)))
|
|
1497
1510
|
|
|
1498
1511
|
# Convert to boolean (MATLAB: logical)
|
|
1499
|
-
|
|
1500
|
-
|
|
1512
|
+
# Handle case where no blocks were processed (shouldn't happen, but be safe)
|
|
1513
|
+
if bl_idxM is not None:
|
|
1514
|
+
bl_idxM = bl_idxM.astype(bool)
|
|
1515
|
+
bl_idxQ = bl_idxQ.astype(bool)
|
|
1516
|
+
else:
|
|
1517
|
+
# No blocks processed - create empty boolean arrays
|
|
1518
|
+
bl_idxM = np.array([]).reshape(n_bl, 0).astype(bool)
|
|
1519
|
+
bl_idxQ = np.array([]).reshape(n_bl, 0).astype(bool)
|
|
1501
1520
|
|
|
1502
1521
|
# Build block diagonal constraint matrix
|
|
1503
1522
|
if len(R_con_list) > 0:
|
|
@@ -1680,11 +1699,19 @@ def em_step(y: np.ndarray, A: np.ndarray, C: np.ndarray, Q: np.ndarray,
|
|
|
1680
1699
|
R_con_i = R_con_i[~no_c, :]
|
|
1681
1700
|
q_con_i = q_con_i[~no_c]
|
|
1682
1701
|
else:
|
|
1683
|
-
|
|
1702
|
+
# Handle empty bl_idxQ_i case
|
|
1703
|
+
if len(bl_idxQ_i) > 0:
|
|
1704
|
+
R_con_i = np.array([]).reshape(0, len(bl_idxQ_i))
|
|
1705
|
+
else:
|
|
1706
|
+
R_con_i = np.array([]).reshape(0, 1) # Default to 1 column for empty case
|
|
1684
1707
|
q_con_i = np.array([])
|
|
1685
1708
|
else:
|
|
1686
1709
|
bl_idxQ_i = np.where(bl_idxQ[i, :])[0]
|
|
1687
|
-
|
|
1710
|
+
# Handle empty bl_idxQ_i case
|
|
1711
|
+
if len(bl_idxQ_i) > 0:
|
|
1712
|
+
R_con_i = np.array([]).reshape(0, len(bl_idxQ_i))
|
|
1713
|
+
else:
|
|
1714
|
+
R_con_i = np.array([]).reshape(0, 1) # Default to 1 column for empty case
|
|
1688
1715
|
q_con_i = np.array([])
|
|
1689
1716
|
|
|
1690
1717
|
# Loop through quarterly series in this block pattern
|
|
@@ -513,6 +513,7 @@ def run_all_tests():
|
|
|
513
513
|
('em_step_dimensions', test_em_step_dimensions),
|
|
514
514
|
('init_conditions_basic', test_init_conditions_basic),
|
|
515
515
|
('matlab_comparison', test_matlab_comparison),
|
|
516
|
+
('multi_block_different_factors', test_multi_block_different_factor_counts),
|
|
516
517
|
]
|
|
517
518
|
|
|
518
519
|
for name, func in test_funcs:
|
|
@@ -715,5 +716,99 @@ def test_init_conditions_blocks():
|
|
|
715
716
|
return True
|
|
716
717
|
|
|
717
718
|
|
|
719
|
+
def test_multi_block_different_factor_counts():
|
|
720
|
+
"""
|
|
721
|
+
Test for bug fix: multi-block models with different factor counts.
|
|
722
|
+
|
|
723
|
+
This test reproduces the bug where ff variable from previous block
|
|
724
|
+
iteration caused dimension mismatch when stacking arrays.
|
|
725
|
+
|
|
726
|
+
Bug scenario:
|
|
727
|
+
- Block 0: r_i=3, creates ff with width 3*pC
|
|
728
|
+
- Block 1: r_i=2, tries to use ff from block 0 (width 3*pC)
|
|
729
|
+
with np.zeros(width 2*pC) -> dimension mismatch
|
|
730
|
+
|
|
731
|
+
Fix: Reset ff = None at start of each block iteration.
|
|
732
|
+
"""
|
|
733
|
+
print("\n" + "="*70)
|
|
734
|
+
print("Testing multi-block with different factor counts (bug fix)")
|
|
735
|
+
print("="*70)
|
|
736
|
+
|
|
737
|
+
np.random.seed(42)
|
|
738
|
+
T = 100 # Time periods
|
|
739
|
+
N = 15 # Total series
|
|
740
|
+
|
|
741
|
+
# Create synthetic data
|
|
742
|
+
x = np.random.randn(T, N)
|
|
743
|
+
|
|
744
|
+
# Create blocks: 3 blocks with different factor counts
|
|
745
|
+
# Block 0: 5 series, 3 factors
|
|
746
|
+
# Block 1: 5 series, 2 factors
|
|
747
|
+
# Block 2: 5 series, 2 factors
|
|
748
|
+
blocks = np.zeros((N, 3), dtype=int)
|
|
749
|
+
blocks[0:5, 0] = 1 # First 5 series in block 0
|
|
750
|
+
blocks[5:10, 1] = 1 # Next 5 series in block 1
|
|
751
|
+
blocks[10:15, 2] = 1 # Last 5 series in block 2
|
|
752
|
+
# All series also load on block 0 (global factor)
|
|
753
|
+
blocks[:, 0] = 1
|
|
754
|
+
|
|
755
|
+
# Different factor counts per block (this triggers the bug)
|
|
756
|
+
r = np.array([3, 2, 2]) # 3 factors, 2 factors, 2 factors
|
|
757
|
+
p = 1 # AR lag
|
|
758
|
+
pC = max(p, 1) # Will be 1
|
|
759
|
+
|
|
760
|
+
# Setup other parameters
|
|
761
|
+
opt_nan = {'method': 2, 'k': 3}
|
|
762
|
+
nQ = 0 # All monthly
|
|
763
|
+
i_idio = np.ones(N)
|
|
764
|
+
Rcon = None
|
|
765
|
+
q = None
|
|
766
|
+
|
|
767
|
+
# This should NOT raise ValueError about dimension mismatch
|
|
768
|
+
try:
|
|
769
|
+
A, C, Q, R, Z_0, V_0 = init_conditions(
|
|
770
|
+
x, r, p, blocks, opt_nan, Rcon, q, nQ, i_idio
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# Verify outputs are valid
|
|
774
|
+
assert A is not None, "A should not be None"
|
|
775
|
+
assert C is not None, "C should not be None"
|
|
776
|
+
assert Q is not None, "Q should not be None"
|
|
777
|
+
assert R is not None, "R should not be None"
|
|
778
|
+
|
|
779
|
+
# Verify dimensions
|
|
780
|
+
# State dimension = sum(r_i * ppC) + monthly_idio + quarterly_idio * 5
|
|
781
|
+
# where ppC = max(p, pC) and pC = 1 when Rcon is None
|
|
782
|
+
ppC = max(p, 1) # pC = 1 when Rcon is None
|
|
783
|
+
expected_state_dim = sum(r) * ppC + N + 5 * nQ # factors + monthly_idio + quarterly_idio*5
|
|
784
|
+
assert A.shape == (expected_state_dim, expected_state_dim), \
|
|
785
|
+
f"A shape {A.shape} != expected {(expected_state_dim, expected_state_dim)}"
|
|
786
|
+
assert C.shape == (N, expected_state_dim), \
|
|
787
|
+
f"C shape {C.shape} != expected {(N, expected_state_dim)}"
|
|
788
|
+
|
|
789
|
+
# Verify no NaN/Inf
|
|
790
|
+
assert np.all(np.isfinite(A)), "A contains NaN/Inf"
|
|
791
|
+
assert np.all(np.isfinite(C)), "C contains NaN/Inf"
|
|
792
|
+
assert np.all(np.isfinite(Q)), "Q contains NaN/Inf"
|
|
793
|
+
assert np.all(np.isfinite(R)), "R contains NaN/Inf"
|
|
794
|
+
|
|
795
|
+
print(" ✓ Multi-block init_conditions with different factor counts: PASSED")
|
|
796
|
+
print(f" ✓ State dimension: {expected_state_dim}")
|
|
797
|
+
print(f" ✓ A shape: {A.shape}")
|
|
798
|
+
print(f" ✓ C shape: {C.shape}")
|
|
799
|
+
|
|
800
|
+
return True
|
|
801
|
+
|
|
802
|
+
except ValueError as e:
|
|
803
|
+
if "dimension" in str(e).lower() and "match" in str(e).lower():
|
|
804
|
+
print(f" ✗ FAILED: Dimension mismatch error still present!")
|
|
805
|
+
print(f" Error: {e}")
|
|
806
|
+
raise
|
|
807
|
+
else:
|
|
808
|
+
# Other ValueError is OK (e.g., insufficient data)
|
|
809
|
+
print(f" ⚠ Warning: {e}")
|
|
810
|
+
return True
|
|
811
|
+
|
|
812
|
+
|
|
718
813
|
if __name__ == '__main__':
|
|
719
814
|
run_all_tests()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|