dfm-python 0.1.3__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 CHANGED
@@ -30,13 +30,13 @@ Example:
30
30
  For detailed documentation, see the README.md file.
31
31
  """
32
32
 
33
- __version__ = "0.1.3"
33
+ __version__ = "0.1.4"
34
34
 
35
35
  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
- # Extract estimation parameters if provided
297
- return cls(
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, index_col=0, parse_dates=True)
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
- Z = df.values.astype(float)
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