dfm-python 0.1.4__py3-none-any.whl → 0.1.5__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/__init__.py +2 -2
- dfm_python/config.py +79 -5
- dfm_python/data_loader.py +94 -3
- dfm_python/dfm.py +1029 -275
- dfm_python/news.py +11 -1
- dfm_python-0.1.5.dist-info/METADATA +572 -0
- dfm_python-0.1.5.dist-info/RECORD +13 -0
- dfm_python-0.1.4.dist-info/METADATA +0 -1010
- dfm_python-0.1.4.dist-info/RECORD +0 -13
- {dfm_python-0.1.4.dist-info → dfm_python-0.1.5.dist-info}/WHEEL +0 -0
- {dfm_python-0.1.4.dist-info → dfm_python-0.1.5.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py
CHANGED
|
@@ -36,7 +36,7 @@ from .config import DFMConfig
|
|
|
36
36
|
from .data_loader import (
|
|
37
37
|
load_config, load_config_from_yaml, load_config_from_csv, load_data, transform_data
|
|
38
38
|
)
|
|
39
|
-
from .dfm import DFMResult, dfm
|
|
39
|
+
from .dfm import DFMResult, dfm, calculate_rmse
|
|
40
40
|
from .kalman import run_kf, skf, fis, miss_data
|
|
41
41
|
from .news import update_nowcast, news_dfm, para_const
|
|
42
42
|
|
|
@@ -51,7 +51,7 @@ __all__ = [
|
|
|
51
51
|
'ModelConfig', # Alias for DFMConfig
|
|
52
52
|
'load_config', 'load_config_from_yaml', 'load_config_from_csv',
|
|
53
53
|
'load_data', 'transform_data',
|
|
54
|
-
'DFMResult', 'dfm',
|
|
54
|
+
'DFMResult', 'dfm', 'calculate_rmse',
|
|
55
55
|
'run_kf', 'skf', 'fis', 'miss_data',
|
|
56
56
|
'update_nowcast', 'news_dfm', 'para_const',
|
|
57
57
|
# Deprecated aliases
|
dfm_python/config.py
CHANGED
|
@@ -119,6 +119,32 @@ class DFMConfig:
|
|
|
119
119
|
nan_k: int = 3 # Spline parameter for NaN interpolation
|
|
120
120
|
clock: str = 'm' # Base frequency for nowcasting (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
|
|
121
121
|
|
|
122
|
+
# ========================================================================
|
|
123
|
+
# Numerical Stability Parameters (transparent and configurable)
|
|
124
|
+
# ========================================================================
|
|
125
|
+
# AR Coefficient Clipping
|
|
126
|
+
clip_ar_coefficients: bool = True # Enable AR coefficient clipping for stationarity
|
|
127
|
+
ar_clip_min: float = -0.99 # Minimum AR coefficient (must be > -1 for stationarity)
|
|
128
|
+
ar_clip_max: float = 0.99 # Maximum AR coefficient (must be < 1 for stationarity)
|
|
129
|
+
warn_on_ar_clip: bool = True # Warn when AR coefficients are clipped (indicates near-unit root)
|
|
130
|
+
|
|
131
|
+
# Data Value Clipping
|
|
132
|
+
clip_data_values: bool = True # Enable clipping of extreme data values
|
|
133
|
+
data_clip_threshold: float = 100.0 # Clip values beyond this many standard deviations
|
|
134
|
+
warn_on_data_clip: bool = True # Warn when data values are clipped (indicates outliers)
|
|
135
|
+
|
|
136
|
+
# Regularization
|
|
137
|
+
use_regularization: bool = True # Enable regularization for numerical stability
|
|
138
|
+
regularization_scale: float = 1e-6 # Scale factor for ridge regularization (relative to trace)
|
|
139
|
+
min_eigenvalue: float = 1e-8 # Minimum eigenvalue for positive definite matrices
|
|
140
|
+
max_eigenvalue: float = 1e6 # Maximum eigenvalue cap to prevent explosion
|
|
141
|
+
warn_on_regularization: bool = True # Warn when regularization is applied
|
|
142
|
+
|
|
143
|
+
# Damped Updates
|
|
144
|
+
use_damped_updates: bool = True # Enable damped updates when likelihood decreases
|
|
145
|
+
damping_factor: float = 0.8 # Damping factor (0.8 = 80% new, 20% old)
|
|
146
|
+
warn_on_damped_update: bool = True # Warn when damped updates are used
|
|
147
|
+
|
|
122
148
|
# ========================================================================
|
|
123
149
|
# Internal cache (not user-configurable)
|
|
124
150
|
# ========================================================================
|
|
@@ -293,8 +319,8 @@ class DFMConfig:
|
|
|
293
319
|
category=str(cat_val) if cat_val is not None else ''
|
|
294
320
|
))
|
|
295
321
|
|
|
296
|
-
|
|
297
|
-
|
|
322
|
+
# Extract estimation parameters if provided
|
|
323
|
+
return cls(
|
|
298
324
|
series=series_list,
|
|
299
325
|
block_names=data.get('BlockNames', data.get('block_names', [])),
|
|
300
326
|
factors_per_block=data.get('factors_per_block', None),
|
|
@@ -304,7 +330,23 @@ class DFMConfig:
|
|
|
304
330
|
max_iter=data.get('max_iter', 5000),
|
|
305
331
|
nan_method=data.get('nan_method', 2),
|
|
306
332
|
nan_k=data.get('nan_k', 3),
|
|
307
|
-
clock=data.get('clock', 'm')
|
|
333
|
+
clock=data.get('clock', 'm'),
|
|
334
|
+
# Numerical stability parameters
|
|
335
|
+
clip_ar_coefficients=data.get('clip_ar_coefficients', True),
|
|
336
|
+
ar_clip_min=data.get('ar_clip_min', -0.99),
|
|
337
|
+
ar_clip_max=data.get('ar_clip_max', 0.99),
|
|
338
|
+
warn_on_ar_clip=data.get('warn_on_ar_clip', True),
|
|
339
|
+
clip_data_values=data.get('clip_data_values', True),
|
|
340
|
+
data_clip_threshold=data.get('data_clip_threshold', 100.0),
|
|
341
|
+
warn_on_data_clip=data.get('warn_on_data_clip', True),
|
|
342
|
+
use_regularization=data.get('use_regularization', True),
|
|
343
|
+
regularization_scale=data.get('regularization_scale', 1e-6),
|
|
344
|
+
min_eigenvalue=data.get('min_eigenvalue', 1e-8),
|
|
345
|
+
max_eigenvalue=data.get('max_eigenvalue', 1e6),
|
|
346
|
+
warn_on_regularization=data.get('warn_on_regularization', True),
|
|
347
|
+
use_damped_updates=data.get('use_damped_updates', True),
|
|
348
|
+
damping_factor=data.get('damping_factor', 0.8),
|
|
349
|
+
warn_on_damped_update=data.get('warn_on_damped_update', True)
|
|
308
350
|
)
|
|
309
351
|
|
|
310
352
|
@classmethod
|
|
@@ -376,7 +418,23 @@ class DFMConfig:
|
|
|
376
418
|
max_iter=data.get('max_iter', 5000),
|
|
377
419
|
nan_method=data.get('nan_method', 2),
|
|
378
420
|
nan_k=data.get('nan_k', 3),
|
|
379
|
-
clock=data.get('clock', 'm')
|
|
421
|
+
clock=data.get('clock', 'm'),
|
|
422
|
+
# Numerical stability parameters
|
|
423
|
+
clip_ar_coefficients=data.get('clip_ar_coefficients', True),
|
|
424
|
+
ar_clip_min=data.get('ar_clip_min', -0.99),
|
|
425
|
+
ar_clip_max=data.get('ar_clip_max', 0.99),
|
|
426
|
+
warn_on_ar_clip=data.get('warn_on_ar_clip', True),
|
|
427
|
+
clip_data_values=data.get('clip_data_values', True),
|
|
428
|
+
data_clip_threshold=data.get('data_clip_threshold', 100.0),
|
|
429
|
+
warn_on_data_clip=data.get('warn_on_data_clip', True),
|
|
430
|
+
use_regularization=data.get('use_regularization', True),
|
|
431
|
+
regularization_scale=data.get('regularization_scale', 1e-6),
|
|
432
|
+
min_eigenvalue=data.get('min_eigenvalue', 1e-8),
|
|
433
|
+
max_eigenvalue=data.get('max_eigenvalue', 1e6),
|
|
434
|
+
warn_on_regularization=data.get('warn_on_regularization', True),
|
|
435
|
+
use_damped_updates=data.get('use_damped_updates', True),
|
|
436
|
+
damping_factor=data.get('damping_factor', 0.8),
|
|
437
|
+
warn_on_damped_update=data.get('warn_on_damped_update', True)
|
|
380
438
|
)
|
|
381
439
|
|
|
382
440
|
# New format with series list
|
|
@@ -396,7 +454,23 @@ class DFMConfig:
|
|
|
396
454
|
max_iter=data.get('max_iter', 5000),
|
|
397
455
|
nan_method=data.get('nan_method', 2),
|
|
398
456
|
nan_k=data.get('nan_k', 3),
|
|
399
|
-
clock=data.get('clock', 'm')
|
|
457
|
+
clock=data.get('clock', 'm'),
|
|
458
|
+
# Numerical stability parameters
|
|
459
|
+
clip_ar_coefficients=data.get('clip_ar_coefficients', True),
|
|
460
|
+
ar_clip_min=data.get('ar_clip_min', -0.99),
|
|
461
|
+
ar_clip_max=data.get('ar_clip_max', 0.99),
|
|
462
|
+
warn_on_ar_clip=data.get('warn_on_ar_clip', True),
|
|
463
|
+
clip_data_values=data.get('clip_data_values', True),
|
|
464
|
+
data_clip_threshold=data.get('data_clip_threshold', 100.0),
|
|
465
|
+
warn_on_data_clip=data.get('warn_on_data_clip', True),
|
|
466
|
+
use_regularization=data.get('use_regularization', True),
|
|
467
|
+
regularization_scale=data.get('regularization_scale', 1e-6),
|
|
468
|
+
min_eigenvalue=data.get('min_eigenvalue', 1e-8),
|
|
469
|
+
max_eigenvalue=data.get('max_eigenvalue', 1e6),
|
|
470
|
+
warn_on_regularization=data.get('warn_on_regularization', True),
|
|
471
|
+
use_damped_updates=data.get('use_damped_updates', True),
|
|
472
|
+
damping_factor=data.get('damping_factor', 0.8),
|
|
473
|
+
warn_on_damped_update=data.get('warn_on_damped_update', True)
|
|
400
474
|
)
|
|
401
475
|
|
|
402
476
|
# Direct instantiation (shouldn't happen often, but handle it)
|
dfm_python/data_loader.py
CHANGED
|
@@ -74,7 +74,13 @@ def load_config_from_yaml(configfile: Union[str, Path]) -> DFMConfig:
|
|
|
74
74
|
model_dict = cfg_dict
|
|
75
75
|
elif 'model' in cfg_dict:
|
|
76
76
|
# Nested under 'model' key (from @package model:)
|
|
77
|
-
model_dict = cfg_dict['model']
|
|
77
|
+
model_dict = cfg_dict['model'].copy() if isinstance(cfg_dict['model'], dict) else {}
|
|
78
|
+
# If series is at top level, merge it into model_dict
|
|
79
|
+
if 'series' in cfg_dict and 'series' not in model_dict:
|
|
80
|
+
model_dict['series'] = cfg_dict['series']
|
|
81
|
+
elif 'series' in cfg_dict:
|
|
82
|
+
# Series at top level, construct model_dict from top-level keys
|
|
83
|
+
model_dict = {k: v for k, v in cfg_dict.items() if k not in ['dfm', 'data', 'experiment_name', 'output_dir', 'defaults']}
|
|
78
84
|
else:
|
|
79
85
|
# Try to construct from top-level keys
|
|
80
86
|
model_dict = cfg_dict
|
|
@@ -362,6 +368,10 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, pd.DatetimeIndex,
|
|
|
362
368
|
- First column: Date (YYYY-MM-DD format or pandas parseable)
|
|
363
369
|
- Subsequent columns: Series data (one column per series)
|
|
364
370
|
- Header row: Series IDs (matching config.SeriesID)
|
|
371
|
+
|
|
372
|
+
Alternatively, CSV can have metadata columns first:
|
|
373
|
+
- Metadata columns: series_id, series_name, frequency, transformation, category, units
|
|
374
|
+
- Then date columns starting from the first date column
|
|
365
375
|
"""
|
|
366
376
|
datafile = Path(datafile)
|
|
367
377
|
if not datafile.exists():
|
|
@@ -369,13 +379,94 @@ def read_data(datafile: Union[str, Path]) -> Tuple[np.ndarray, pd.DatetimeIndex,
|
|
|
369
379
|
|
|
370
380
|
# Read CSV file
|
|
371
381
|
try:
|
|
372
|
-
df = pd.read_csv(datafile
|
|
382
|
+
df = pd.read_csv(datafile)
|
|
373
383
|
except Exception as e:
|
|
374
384
|
raise ValueError(f"Failed to read CSV file {datafile}: {e}")
|
|
375
385
|
|
|
386
|
+
# Check if first column is a date column or metadata
|
|
387
|
+
first_col = df.columns[0]
|
|
388
|
+
|
|
389
|
+
# Try to parse first column as date
|
|
390
|
+
try:
|
|
391
|
+
pd.to_datetime(df[first_col].iloc[0])
|
|
392
|
+
is_date_first = True
|
|
393
|
+
except (ValueError, TypeError):
|
|
394
|
+
is_date_first = False
|
|
395
|
+
|
|
396
|
+
# If first column is not a date, check if CSV is in "long" format (one row per series)
|
|
397
|
+
if not is_date_first:
|
|
398
|
+
# Check if first column contains series IDs (string values)
|
|
399
|
+
first_col_values = df[first_col].astype(str)
|
|
400
|
+
# If first column looks like series IDs and we have date columns, transpose
|
|
401
|
+
if 'series_id' in first_col.lower() or first_col_values.str.match(r'^[A-Z0-9_]+$').any():
|
|
402
|
+
# Long format: transpose so series are columns and dates are rows
|
|
403
|
+
# Find first date column
|
|
404
|
+
date_col_idx = None
|
|
405
|
+
for i, col in enumerate(df.columns):
|
|
406
|
+
try:
|
|
407
|
+
# Try to parse first value as date
|
|
408
|
+
pd.to_datetime(df[col].iloc[0])
|
|
409
|
+
date_col_idx = i
|
|
410
|
+
break
|
|
411
|
+
except (ValueError, TypeError):
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
if date_col_idx is None:
|
|
415
|
+
raise ValueError(f"Could not find date column in CSV file {datafile}")
|
|
416
|
+
|
|
417
|
+
# Set series_id as index, then transpose
|
|
418
|
+
series_id_col = df.columns[0]
|
|
419
|
+
df = df.set_index(series_id_col)
|
|
420
|
+
|
|
421
|
+
# Get date columns (from date_col_idx onwards)
|
|
422
|
+
date_cols = df.columns[date_col_idx:]
|
|
423
|
+
df_data = df[date_cols].T # Transpose: dates become rows, series become columns
|
|
424
|
+
|
|
425
|
+
# Convert date column names to datetime index
|
|
426
|
+
df_data.index = pd.to_datetime(df_data.index)
|
|
427
|
+
|
|
428
|
+
mnemonics = df_data.columns.tolist()
|
|
429
|
+
Time = df_data.index
|
|
430
|
+
Z = df_data.apply(pd.to_numeric, errors='coerce').values.astype(float)
|
|
431
|
+
|
|
432
|
+
return Z, Time, mnemonics
|
|
433
|
+
else:
|
|
434
|
+
# Find first column that looks like a date
|
|
435
|
+
date_col_idx = None
|
|
436
|
+
for i, col in enumerate(df.columns):
|
|
437
|
+
try:
|
|
438
|
+
# Try to parse first value as date
|
|
439
|
+
pd.to_datetime(df[col].iloc[0])
|
|
440
|
+
date_col_idx = i
|
|
441
|
+
break
|
|
442
|
+
except (ValueError, TypeError):
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
if date_col_idx is None:
|
|
446
|
+
raise ValueError(f"Could not find date column in CSV file {datafile}")
|
|
447
|
+
|
|
448
|
+
# Use first date column as index, drop metadata columns
|
|
449
|
+
date_col = df.columns[date_col_idx]
|
|
450
|
+
df = df.set_index(date_col)
|
|
451
|
+
df.index = pd.to_datetime(df.index)
|
|
452
|
+
# Drop any remaining non-numeric columns (metadata)
|
|
453
|
+
numeric_cols = []
|
|
454
|
+
for col in df.columns:
|
|
455
|
+
try:
|
|
456
|
+
pd.to_numeric(df[col], errors='coerce')
|
|
457
|
+
numeric_cols.append(col)
|
|
458
|
+
except (ValueError, TypeError):
|
|
459
|
+
continue
|
|
460
|
+
df = df[numeric_cols]
|
|
461
|
+
else:
|
|
462
|
+
# Standard format: first column is date
|
|
463
|
+
df = df.set_index(first_col)
|
|
464
|
+
df.index = pd.to_datetime(df.index)
|
|
465
|
+
|
|
376
466
|
mnemonics = df.columns.tolist()
|
|
377
467
|
Time = df.index
|
|
378
|
-
|
|
468
|
+
# Convert to float, handling any remaining non-numeric values
|
|
469
|
+
Z = df.apply(pd.to_numeric, errors='coerce').values.astype(float)
|
|
379
470
|
|
|
380
471
|
return Z, Time, mnemonics
|
|
381
472
|
|