dfm-python 0.2.8__py3-none-any.whl → 0.2.9__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
@@ -50,22 +50,17 @@ Example (Low-level API - For advanced usage):
50
50
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
51
51
  """
52
52
 
53
- __version__ = "0.2.8"
53
+ __version__ = "0.2.9"
54
54
 
55
- from .config import DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME
56
55
  from .config import (
57
- ConfigSource,
58
- YamlSource,
59
- DictSource,
60
- SpecCSVSource,
61
- HydraSource,
62
- MergedConfigSource,
63
- make_config_source,
56
+ DFMConfig, SeriesConfig, BlockConfig, Params, DEFAULT_GLOBAL_BLOCK_NAME,
57
+ ConfigSource, YamlSource, DictSource, SpecCSVSource, HydraSource,
58
+ MergedConfigSource, make_config_source,
64
59
  )
65
60
  from .data import transform_data
66
61
  from .dfm import DFMResult
67
62
  from .core import calculate_rmse, diagnose_series, print_series_diagnosis
68
- from .dfm import DFM as _DFMCore # Core DFM class from dfm.py
63
+ from .dfm import DFMCore # Core DFM class from dfm.py
69
64
  from .kalman import run_kf, skf, fis, miss_data
70
65
  from .news import update_nowcast, news_dfm, para_const
71
66
 
@@ -100,27 +95,9 @@ def get_original_data():
100
95
  """Get original (untransformed) data matrix."""
101
96
  return _dfm_instance.original_data
102
97
 
103
- # For direct attribute access, we'll use __getattr__ but handle config module conflict
104
-
105
- def __getattr__(name):
106
- """Allow access to DFM instance properties at module level."""
107
- # Handle special cases that conflict with submodules
108
- if name == 'config' and _dfm_instance.config is not None:
109
- return _dfm_instance.config
110
- elif name == 'data':
111
- return _dfm_instance.data
112
- elif name == 'time':
113
- return _dfm_instance.time
114
- elif name == 'result':
115
- return _dfm_instance.result
116
- elif name == 'original_data':
117
- return _dfm_instance.original_data
118
- # For other attributes, try normal import
119
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
120
-
121
98
  __all__ = [
122
99
  # Core classes
123
- 'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM',
100
+ 'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params', 'DFM', 'DFMCore',
124
101
  # Constants
125
102
  'DEFAULT_GLOBAL_BLOCK_NAME',
126
103
  # Config sources
dfm_python/api.py CHANGED
@@ -29,11 +29,11 @@ from .config import (
29
29
  ConfigSource,
30
30
  MergedConfigSource,
31
31
  )
32
- from .dfm import DFM as _DFMCore, DFMResult
32
+ from .dfm import DFMCore, DFMResult
33
33
  from .core.helpers import safe_get_method, safe_get_attr
34
34
 
35
35
 
36
- class DFM(_DFMCore):
36
+ class DFM(DFMCore):
37
37
  """High-level API for Dynamic Factor Model estimation.
38
38
 
39
39
  This class provides a simple, object-oriented interface for DFM operations.
@@ -368,29 +368,48 @@ def from_yaml(yaml_path: Union[str, Path]) -> DFM:
368
368
  return _dfm_instance.load_config(yaml=yaml_path)
369
369
 
370
370
 
371
- def from_spec(spec_path: Union[str, Path], params: Optional[Params] = None) -> DFM:
372
- """Load configuration from spec CSV file (convenience constructor)."""
373
- from .config import SpecCSVSource
371
+ def from_spec(
372
+ csv_path: Union[str, Path],
373
+ output_dir: Optional[Union[str, Path]] = None,
374
+ series_filename: Optional[str] = None,
375
+ blocks_filename: Optional[str] = None
376
+ ) -> Tuple[Path, Path]:
377
+ """Convert spec CSV file to YAML configuration files.
374
378
 
375
- if params is None:
376
- params = Params()
379
+ This function reads a spec CSV file and generates two YAML files:
380
+ - config/series/{basename}.yaml - series definitions
381
+ - config/blocks/{basename}.yaml - block definitions
377
382
 
378
- # Load config from spec CSV using ConfigSource pattern
379
- spec_source = SpecCSVSource(spec_path)
380
- config = spec_source.load()
381
-
382
- # Override estimation parameters from params
383
- for key, value in params.__dict__.items():
384
- if hasattr(config, key):
385
- setattr(config, key, value)
386
-
387
- _dfm_instance._config = config
388
- return _dfm_instance
383
+ Parameters
384
+ ----------
385
+ csv_path : str or Path
386
+ Path to the spec CSV file
387
+ output_dir : str or Path, optional
388
+ Output directory for YAML files. Defaults to config/ directory relative to CSV.
389
+ series_filename : str, optional
390
+ Custom filename for series YAML (without .yaml extension).
391
+ Defaults to CSV basename.
392
+ blocks_filename : str, optional
393
+ Custom filename for blocks YAML (without .yaml extension).
394
+ Defaults to CSV basename.
395
+
396
+ Returns
397
+ -------
398
+ Tuple[Path, Path]
399
+ Paths to generated series YAML and blocks YAML files
400
+
401
+ Examples
402
+ --------
403
+ >>> series_path, blocks_path = from_spec('data/sample_spec.csv')
404
+ >>> # Creates config/series/sample_spec.yaml and config/blocks/sample_spec.yaml
405
+ """
406
+ from .config_sources import from_spec as _from_spec
407
+ return _from_spec(csv_path, output_dir, series_filename, blocks_filename)
389
408
 
390
409
 
391
410
  def from_spec_df(spec_df: pd.DataFrame, params: Optional[Params] = None) -> DFM:
392
411
  """Load configuration from spec DataFrame (convenience constructor)."""
393
- from .data import _load_config_from_dataframe
412
+ from .config_sources import _load_config_from_dataframe
394
413
 
395
414
  if params is None:
396
415
  params = Params()
dfm_python/config.py CHANGED
@@ -21,6 +21,7 @@ from config_sources for backward compatibility.
21
21
  """
22
22
 
23
23
  import numpy as np
24
+ import warnings
24
25
  from typing import List, Optional, Dict, Any, Union
25
26
  from dataclasses import dataclass, field
26
27
 
@@ -102,10 +103,14 @@ class SeriesConfig:
102
103
  Unique identifier (auto-generated if None)
103
104
  series_name : str, optional
104
105
  Human-readable name (defaults to series_id if None)
105
- units : str
106
- Units of measurement
107
- category : str
108
- Category/group name
106
+ units : str, optional
107
+ Units of measurement (optional metadata for display purposes only).
108
+ Used in news decomposition output for readability. Not used in model estimation.
109
+ release_date : int, optional
110
+ Release date information for pseudo real-time nowcasting.
111
+ - Positive value (1-31): Day of month when data is released
112
+ - Negative value: Days before end of previous month when data is released
113
+ Example: 25 = released on 25th of each month, -5 = released 5 days before end of previous month
109
114
  """
110
115
  # Required fields (no defaults)
111
116
  frequency: str
@@ -114,9 +119,8 @@ class SeriesConfig:
114
119
  # Optional fields (with defaults - must come after required fields)
115
120
  series_id: Optional[str] = None # Auto-generated if None: "series_0", "series_1", etc.
116
121
  series_name: Optional[str] = None # Optional metadata for display
117
- units: str = "" # Optional metadata
118
- category: str = "" # Optional metadata
119
- aggregate: Optional[str] = None # Aggregation method (deprecated: higher frequencies than clock are not supported)
122
+ units: Optional[str] = None # Optional metadata for display only (used in news.py output)
123
+ release_date: Optional[int] = None # Release date for pseudo real-time nowcasting
120
124
 
121
125
  def __post_init__(self):
122
126
  """Validate fields after initialization."""
@@ -716,9 +720,7 @@ class DFMConfig:
716
720
  blocks=series_blocks,
717
721
  series_id=get_list_value('SeriesID', i, None),
718
722
  series_name=get_list_value('SeriesName', i, None),
719
- units=str(get_list_value('Units', i, '')),
720
- category=str(get_list_value('Category', i, '')),
721
- aggregate=get_list_value('Aggregate', i, None)
723
+ units=get_list_value('Units', i, None) # Optional, for display only
722
724
  ))
723
725
 
724
726
  # Convert block_names to blocks dict
@@ -805,9 +807,7 @@ class DFMConfig:
805
807
  frequency=series_cfg.get('frequency', 'm'),
806
808
  transformation=series_cfg.get('transformation', 'lin'),
807
809
  blocks=series_blocks,
808
- units=series_cfg.get('units', ''),
809
- category=series_cfg.get('category', ''),
810
- aggregate=series_cfg.get('aggregate', None) # Deprecated
810
+ units=series_cfg.get('units', None) # Optional, for display only
811
811
  ))
812
812
 
813
813
  # Convert blocks_dict to BlockConfig dict
@@ -1031,7 +1031,6 @@ def validate_transformation(transformation: str) -> str:
1031
1031
  >>> validate_transformation('unknown') # Issues warning but returns value
1032
1032
  'unknown'
1033
1033
  """
1034
- import warnings
1035
1034
  if transformation not in _VALID_TRANSFORMATIONS:
1036
1035
  warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
1037
1036
  return transformation
@@ -10,11 +10,62 @@ This module provides adapters for loading DFMConfig from various sources:
10
10
  All adapters implement the ConfigSource protocol and return DFMConfig objects.
11
11
  """
12
12
 
13
- from typing import Protocol, Optional, Dict, Any, Union
13
+ import warnings
14
+ from typing import Protocol, Optional, Dict, Any, Union, Tuple
14
15
  from pathlib import Path
15
16
  from dataclasses import is_dataclass, asdict
16
17
 
17
18
  from .config import DFMConfig, SeriesConfig, BlockConfig, DEFAULT_GLOBAL_BLOCK_NAME
19
+ import pandas as pd
20
+
21
+
22
+ def _load_config_from_defaults(cfg, root_config_dir, config_type: str) -> Optional[dict]:
23
+ """Load config from defaults or direct path (helper for series/blocks loading).
24
+
25
+ Parameters
26
+ ----------
27
+ cfg : OmegaConf.DictConfig
28
+ Main config object
29
+ root_config_dir : Path
30
+ Root config directory (contains series/ and blocks/ subdirectories)
31
+ config_type : str
32
+ Type of config to load: 'series' or 'blocks'
33
+
34
+ Returns
35
+ -------
36
+ Optional[dict]
37
+ Loaded config dict or None if not found
38
+ """
39
+ from omegaconf import OmegaConf
40
+
41
+ config_dict = None
42
+ config_loaded = False
43
+
44
+ # Try loading from defaults
45
+ if 'defaults' in cfg:
46
+ defaults_list = cfg.defaults
47
+ for default_item in defaults_list:
48
+ default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
49
+
50
+ # Handle dict format: {'series': 'default'} or {'blocks': 'default'}
51
+ if isinstance(default_dict, dict) and config_type in default_dict:
52
+ config_name = default_dict[config_type]
53
+ config_path = root_config_dir / config_type / f'{config_name}.yaml'
54
+ if config_path.exists():
55
+ config_cfg = OmegaConf.load(config_path)
56
+ config_dict = OmegaConf.to_container(config_cfg, resolve=True)
57
+ config_loaded = True
58
+ break
59
+
60
+ # If not loaded from defaults, try direct path
61
+ if not config_loaded:
62
+ config_path = root_config_dir / config_type / 'default.yaml'
63
+ if config_path.exists():
64
+ config_cfg = OmegaConf.load(config_path)
65
+ config_dict = OmegaConf.to_container(config_cfg, resolve=True)
66
+ config_loaded = True
67
+
68
+ return config_dict if config_loaded else None
18
69
 
19
70
 
20
71
  class ConfigSource(Protocol):
@@ -55,6 +106,14 @@ class YamlSource:
55
106
  raise FileNotFoundError(f"Configuration file not found: {configfile}")
56
107
 
57
108
  config_dir = configfile.parent
109
+ # If config file is in a subdirectory (e.g., experiment/), find the root config directory
110
+ # Look for series/ or blocks/ directories to identify config root
111
+ root_config_dir = config_dir
112
+ while root_config_dir.parent != root_config_dir: # Not at filesystem root
113
+ if (root_config_dir / 'series').exists() or (root_config_dir / 'blocks').exists():
114
+ break
115
+ root_config_dir = root_config_dir.parent
116
+
58
117
  cfg = OmegaConf.load(configfile)
59
118
  cfg_dict = OmegaConf.to_container(cfg, resolve=True)
60
119
 
@@ -64,44 +123,28 @@ class YamlSource:
64
123
 
65
124
  # Load series from config/series/default.yaml
66
125
  series_list = []
67
- series_loaded = False
68
- series_dict = None
69
-
70
- if 'defaults' in cfg:
71
- defaults_list = cfg.defaults
72
- for default_item in defaults_list:
73
- default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
74
-
75
- # Handle dict format: {'series': 'default'}
76
- if isinstance(default_dict, dict) and 'series' in default_dict:
77
- series_config_name = default_dict['series']
78
- series_config_path = config_dir / 'series' / f'{series_config_name}.yaml'
79
- if series_config_path.exists():
80
- series_cfg = OmegaConf.load(series_config_path)
81
- series_dict = OmegaConf.to_container(series_cfg, resolve=True)
82
- series_loaded = True
83
- break
84
-
85
- # If not loaded from defaults, try direct path
86
- if not series_loaded:
87
- series_config_path = config_dir / 'series' / 'default.yaml'
88
- if series_config_path.exists():
89
- series_cfg = OmegaConf.load(series_config_path)
90
- series_dict = OmegaConf.to_container(series_cfg, resolve=True)
91
- series_loaded = True
126
+ series_dict = _load_config_from_defaults(cfg, root_config_dir, 'series')
127
+ series_loaded = series_dict is not None
92
128
 
93
129
  # Convert series dict to SeriesConfig objects
94
130
  if series_loaded and series_dict is not None:
95
131
  for series_id, series_data in series_dict.items():
96
132
  if isinstance(series_data, dict):
133
+ # Parse release_date if available
134
+ release_date = series_data.get('release', series_data.get('release_date', None))
135
+ if release_date is not None:
136
+ try:
137
+ release_date = int(release_date)
138
+ except (ValueError, TypeError):
139
+ release_date = None
140
+
97
141
  series_list.append(SeriesConfig(
98
142
  series_id=series_id,
99
143
  series_name=series_data.get('series_name', series_id),
100
144
  frequency=series_data.get('frequency', 'm'),
101
145
  transformation=series_data.get('transformation', 'lin'),
102
- category=series_data.get('category', ''),
103
- units=series_data.get('units', ''),
104
- blocks=series_data.get('blocks', [])
146
+ blocks=series_data.get('blocks', []),
147
+ release_date=release_date
105
148
  ))
106
149
 
107
150
  # If no series loaded from separate files, try to get from main config
@@ -118,31 +161,8 @@ class YamlSource:
118
161
 
119
162
  # Load blocks from config/blocks/default.yaml
120
163
  blocks_dict = {}
121
- blocks_loaded = False
122
- blocks_dict_raw = None
123
-
124
- if 'defaults' in cfg:
125
- defaults_list = cfg.defaults
126
- for default_item in defaults_list:
127
- default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
128
-
129
- # Handle dict format: {'blocks': 'default'}
130
- if isinstance(default_dict, dict) and 'blocks' in default_dict:
131
- blocks_config_name = default_dict['blocks']
132
- blocks_config_path = config_dir / 'blocks' / f'{blocks_config_name}.yaml'
133
- if blocks_config_path.exists():
134
- blocks_cfg = OmegaConf.load(blocks_config_path)
135
- blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
136
- blocks_loaded = True
137
- break
138
-
139
- # If not loaded from defaults, try direct path
140
- if not blocks_loaded:
141
- blocks_config_path = config_dir / 'blocks' / 'default.yaml'
142
- if blocks_config_path.exists():
143
- blocks_cfg = OmegaConf.load(blocks_config_path)
144
- blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
145
- blocks_loaded = True
164
+ blocks_dict_raw = _load_config_from_defaults(cfg, root_config_dir, 'blocks')
165
+ blocks_loaded = blocks_dict_raw is not None
146
166
 
147
167
  # Convert blocks dict to BlockConfig objects
148
168
  if blocks_loaded and blocks_dict_raw is not None:
@@ -241,9 +261,6 @@ class SpecCSVSource:
241
261
 
242
262
  def load(self) -> DFMConfig:
243
263
  """Load series definitions from spec CSV."""
244
- import pandas as pd
245
- from .data import _load_config_from_dataframe
246
-
247
264
  specfile = Path(self.spec_path)
248
265
  if not specfile.exists():
249
266
  raise FileNotFoundError(f"Spec file not found: {specfile}")
@@ -371,6 +388,190 @@ class MergedConfigSource:
371
388
  )
372
389
 
373
390
 
391
+ def from_spec(
392
+ csv_path: Union[str, Path],
393
+ output_dir: Optional[Union[str, Path]] = None,
394
+ series_filename: Optional[str] = None,
395
+ blocks_filename: Optional[str] = None
396
+ ) -> Tuple[Path, Path]:
397
+ """Convert spec CSV file to YAML configuration files.
398
+
399
+ This function reads a spec CSV file and generates two YAML files:
400
+ - config/series/{basename}.yaml - series definitions
401
+ - config/blocks/{basename}.yaml - block definitions
402
+
403
+ Parameters
404
+ ----------
405
+ csv_path : str or Path
406
+ Path to the spec CSV file
407
+ output_dir : str or Path, optional
408
+ Output directory for YAML files. Defaults to config/ directory relative to CSV.
409
+ series_filename : str, optional
410
+ Custom filename for series YAML (without .yaml extension).
411
+ Defaults to CSV basename.
412
+ blocks_filename : str, optional
413
+ Custom filename for blocks YAML (without .yaml extension).
414
+ Defaults to CSV basename.
415
+
416
+ Returns
417
+ -------
418
+ Tuple[Path, Path]
419
+ Paths to generated series YAML and blocks YAML files
420
+
421
+ Examples
422
+ --------
423
+ >>> series_path, blocks_path = from_spec('data/sample_spec.csv')
424
+ >>> # Creates config/series/sample_spec.yaml and config/blocks/sample_spec.yaml
425
+ """
426
+
427
+ csv_path = Path(csv_path)
428
+ if not csv_path.exists():
429
+ raise FileNotFoundError(f"CSV file not found: {csv_path}")
430
+
431
+ # Determine output directory
432
+ if output_dir is None:
433
+ # Default: config/ directory relative to CSV file's parent
434
+ output_dir = csv_path.parent.parent / 'config'
435
+ else:
436
+ output_dir = Path(output_dir)
437
+
438
+ output_dir.mkdir(parents=True, exist_ok=True)
439
+
440
+ # Determine filenames
441
+ csv_basename = csv_path.stem
442
+ series_filename = series_filename or csv_basename
443
+ blocks_filename = blocks_filename or csv_basename
444
+
445
+ # Read CSV and convert to DFMConfig
446
+ try:
447
+ df = pd.read_csv(csv_path)
448
+ except Exception as e:
449
+ raise ValueError(f"Failed to read CSV file {csv_path}: {e}")
450
+
451
+ config = _load_config_from_dataframe(df)
452
+
453
+ # Prepare series YAML content
454
+ series_dir = output_dir / 'series'
455
+ series_dir.mkdir(parents=True, exist_ok=True)
456
+ series_path = series_dir / f'{series_filename}.yaml'
457
+
458
+ series_dict = {}
459
+ # Get block names in order (from config.block_names)
460
+ block_names = config.block_names
461
+
462
+ for series in config.series:
463
+ # Convert blocks from indices to block names if needed
464
+ if isinstance(series.blocks, list) and len(series.blocks) > 0:
465
+ if isinstance(series.blocks[0], int):
466
+ # Blocks are indices - convert to block names
467
+ blocks_names = [block_names[i] for i, val in enumerate(series.blocks) if val != 0]
468
+ else:
469
+ # Blocks are already names
470
+ blocks_names = list(series.blocks)
471
+ else:
472
+ blocks_names = ['Block_Global'] # Default
473
+
474
+ series_entry = {
475
+ 'series_name': series.series_name,
476
+ 'frequency': series.frequency,
477
+ 'transformation': series.transformation,
478
+ 'blocks': blocks_names
479
+ }
480
+ if series.release_date is not None:
481
+ series_entry['release'] = series.release_date
482
+ series_dict[series.series_id] = series_entry
483
+
484
+ # Prepare blocks YAML content
485
+ blocks_dir = output_dir / 'blocks'
486
+ blocks_dir.mkdir(parents=True, exist_ok=True)
487
+ blocks_path = blocks_dir / f'{blocks_filename}.yaml'
488
+
489
+ blocks_dict = {}
490
+ for block_name, block_config in config.blocks.items():
491
+ block_entry = {
492
+ 'factors': block_config.factors,
493
+ 'ar_lag': block_config.ar_lag,
494
+ 'clock': block_config.clock
495
+ }
496
+ if block_config.notes:
497
+ block_entry['notes'] = block_config.notes
498
+ blocks_dict[block_name] = block_entry
499
+
500
+ # Write YAML files
501
+ try:
502
+ try:
503
+ import yaml
504
+ yaml_lib = yaml
505
+ yaml_kwargs = {'default_flow_style': False, 'sort_keys': False, 'allow_unicode': True}
506
+ except ImportError:
507
+ try:
508
+ from omegaconf import OmegaConf
509
+ yaml_lib = OmegaConf
510
+ yaml_kwargs = {}
511
+ except ImportError:
512
+ raise ImportError(
513
+ "Either PyYAML or omegaconf is required for YAML generation. "
514
+ "Install with: pip install pyyaml or pip install omegaconf"
515
+ )
516
+
517
+ # Write series YAML
518
+ if hasattr(yaml_lib, 'dump'):
519
+ # PyYAML - add blank lines between series for better readability
520
+ import io
521
+ stream = io.StringIO()
522
+ yaml_lib.dump(series_dict, stream, **yaml_kwargs)
523
+ yaml_content = stream.getvalue()
524
+
525
+ # Add blank line before each series key (except the first one)
526
+ lines = yaml_content.split('\n')
527
+ formatted_lines = []
528
+ for i, line in enumerate(lines):
529
+ # If line starts a new series (starts with a key and colon, not indented)
530
+ if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
531
+ # Add blank line before this line (except for the first series)
532
+ if i > 0 and formatted_lines and formatted_lines[-1].strip():
533
+ formatted_lines.append('')
534
+ formatted_lines.append(line)
535
+
536
+ with open(series_path, 'w', encoding='utf-8') as f:
537
+ f.write('\n'.join(formatted_lines))
538
+ else:
539
+ # OmegaConf
540
+ series_cfg = OmegaConf.create(series_dict)
541
+ OmegaConf.save(series_cfg, series_path)
542
+
543
+ # Write blocks YAML
544
+ if hasattr(yaml_lib, 'dump'):
545
+ # PyYAML - add blank lines between blocks for better readability
546
+ import io
547
+ stream = io.StringIO()
548
+ yaml_lib.dump(blocks_dict, stream, **yaml_kwargs)
549
+ yaml_content = stream.getvalue()
550
+
551
+ # Add blank line before each block key (except the first one)
552
+ lines = yaml_content.split('\n')
553
+ formatted_lines = []
554
+ for i, line in enumerate(lines):
555
+ # If line starts a new block (starts with a key and colon, not indented)
556
+ if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
557
+ # Add blank line before this line (except for the first block)
558
+ if i > 0 and formatted_lines and formatted_lines[-1].strip():
559
+ formatted_lines.append('')
560
+ formatted_lines.append(line)
561
+
562
+ with open(blocks_path, 'w', encoding='utf-8') as f:
563
+ f.write('\n'.join(formatted_lines))
564
+ else:
565
+ # OmegaConf
566
+ blocks_cfg = OmegaConf.create(blocks_dict)
567
+ OmegaConf.save(blocks_cfg, blocks_path)
568
+
569
+ except Exception as e:
570
+ raise RuntimeError(f"Failed to write YAML files: {e}")
571
+
572
+ return series_path, blocks_path
573
+
574
+
374
575
  def make_config_source(
375
576
  source: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
376
577
  *,
@@ -503,6 +704,101 @@ def make_config_source(
503
704
  )
504
705
 
505
706
 
707
+ # ============================================================================
708
+ # Internal helper: Load config from DataFrame
709
+ # ============================================================================
710
+
711
+ def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
712
+ """Load configuration from DataFrame (internal helper).
713
+
714
+ This function converts a DataFrame with series specifications into a DFMConfig.
715
+ It's used internally by SpecCSVSource and other config loaders.
716
+
717
+ Parameters
718
+ ----------
719
+ df : pd.DataFrame
720
+ DataFrame with columns: series_id, series_name, frequency, transformation, blocks
721
+ Optional columns: Release (release date)
722
+ Block_* columns: Binary indicators for block membership
723
+
724
+ Returns
725
+ -------
726
+ DFMConfig
727
+ Model configuration object
728
+ """
729
+ series_list = []
730
+ block_names_set = set()
731
+
732
+ for _, row in df.iterrows():
733
+ # Parse blocks from Block_* columns or 'blocks' column
734
+ blocks = []
735
+
736
+ # First, try to find Block_* columns (CSV format)
737
+ block_cols = [col for col in df.columns
738
+ if (col.startswith('Block_') or col.startswith('Block-'))
739
+ and col not in ['blocks']]
740
+
741
+ if block_cols:
742
+ # CSV format: Block_Global, Block_Consumption, etc. columns with 1/0 values
743
+ for block_col in block_cols:
744
+ block_value = row.get(block_col, 0)
745
+ # Handle various types: int, float, numpy int/float, bool, string "1"/"0"
746
+ # Check numpy types first (numpy.int64, numpy.float64, etc.)
747
+ if hasattr(block_value, 'item'):
748
+ # numpy scalar - convert to Python native type
749
+ block_value = block_value.item()
750
+
751
+ if isinstance(block_value, (int, float)) and block_value != 0:
752
+ blocks.append(block_col)
753
+ elif isinstance(block_value, bool) and block_value:
754
+ blocks.append(block_col)
755
+ elif isinstance(block_value, str) and block_value.strip() in ['1', 'True', 'true']:
756
+ blocks.append(block_col)
757
+ else:
758
+ # Fallback: try 'blocks' column (comma-separated string or list)
759
+ blocks_str = row.get('blocks', 'Block_Global')
760
+ if isinstance(blocks_str, str):
761
+ blocks = [b.strip() for b in blocks_str.split(',')]
762
+ elif isinstance(blocks_str, list):
763
+ blocks = blocks_str
764
+ else:
765
+ blocks = ['Block_Global']
766
+
767
+ # Ensure at least Block_Global
768
+ if not blocks:
769
+ blocks = ['Block_Global']
770
+
771
+ # Track block names
772
+ for block in blocks:
773
+ block_names_set.add(block)
774
+
775
+ # Parse release_date if available
776
+ release_date = row.get('Release', None)
777
+ if release_date is not None:
778
+ try:
779
+ release_date = int(release_date)
780
+ except (ValueError, TypeError):
781
+ release_date = None
782
+
783
+ series_list.append(SeriesConfig(
784
+ series_id=row.get('series_id', f"series_{len(series_list)}"),
785
+ series_name=row.get('series_name', row.get('series_id', f"Series {len(series_list)}")),
786
+ frequency=row.get('frequency', 'm'),
787
+ transformation=row.get('transformation', 'lin'),
788
+ blocks=blocks,
789
+ release_date=release_date
790
+ ))
791
+
792
+ # Create default blocks if none specified
793
+ block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
794
+ blocks = {}
795
+ for block_name in block_names:
796
+ blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
797
+
798
+ # block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
799
+ return DFMConfig(series=series_list, blocks=blocks)
800
+
801
+
506
802
  # ============================================================================
507
803
  # Hydra ConfigStore Registration (optional - only if Hydra is available)
508
804
  # ============================================================================
@@ -527,10 +823,9 @@ if HYDRA_AVAILABLE and ConfigStore is not None:
527
823
  series_id: str
528
824
  series_name: str
529
825
  frequency: str
530
- units: str
531
826
  transformation: str
532
- category: str
533
827
  blocks: ListType[int]
828
+ units: Optional[str] = None # Optional, for display only
534
829
 
535
830
  @schema_dataclass
536
831
  class DFMConfigSchema:
@@ -996,3 +996,79 @@ def validate_params(
996
996
  V_0 = np.eye(V_0.shape[0]) * default_idio_init_covariance
997
997
 
998
998
  return A, Q, R, C, Z_0, V_0
999
+
1000
+
1001
+ # ============================================================================
1002
+ # Legacy config compatibility helpers
1003
+ # ============================================================================
1004
+
1005
+ def _get_frequencies_from_config(config: Any) -> list:
1006
+ """Get frequencies from config, supporting both new and legacy formats.
1007
+
1008
+ This function provides a unified interface for accessing frequencies
1009
+ from DFMConfig objects, handling both:
1010
+ - New format: config.get_frequencies() method
1011
+ - Legacy format: config.Frequency attribute
1012
+
1013
+ Parameters
1014
+ ----------
1015
+ config : Any
1016
+ Configuration object (DFMConfig or legacy format)
1017
+
1018
+ Returns
1019
+ -------
1020
+ list
1021
+ List of frequency codes for each series
1022
+
1023
+ Notes
1024
+ -----
1025
+ - Maintains backward compatibility with legacy configuration formats
1026
+ - Returns empty list if neither format is available
1027
+ """
1028
+ if hasattr(config, 'get_frequencies'):
1029
+ return config.get_frequencies()
1030
+ elif hasattr(config, 'Frequency'):
1031
+ # Legacy format support: Frequency attribute (for backward compatibility)
1032
+ freq_attr = config.Frequency
1033
+ if hasattr(freq_attr, '__iter__') and not isinstance(freq_attr, str):
1034
+ return list(freq_attr)
1035
+ else:
1036
+ return [freq_attr] if freq_attr is not None else []
1037
+ else:
1038
+ return []
1039
+
1040
+
1041
+ def _get_units_from_config(config: Any, series_index: int) -> Optional[str]:
1042
+ """Get units for a series from config, supporting both new and legacy formats.
1043
+
1044
+ This function provides a unified interface for accessing units from config,
1045
+ handling both:
1046
+ - New format: config.series[series_index].units
1047
+ - Legacy format: config.UnitsTransformed[series_index]
1048
+
1049
+ Parameters
1050
+ ----------
1051
+ config : Any
1052
+ Configuration object (DFMConfig or legacy format)
1053
+ series_index : int
1054
+ Index of the series (0-based)
1055
+
1056
+ Returns
1057
+ -------
1058
+ Optional[str]
1059
+ Units string or None if not available
1060
+
1061
+ Notes
1062
+ -----
1063
+ - Maintains backward compatibility with legacy configuration formats
1064
+ - Returns None if units are not available for the series
1065
+ """
1066
+ # New format: SeriesConfig.units
1067
+ if hasattr(config, 'series') and series_index < len(config.series):
1068
+ return config.series[series_index].units
1069
+
1070
+ # Legacy format: UnitsTransformed attribute
1071
+ if hasattr(config, 'UnitsTransformed') and series_index < len(config.UnitsTransformed):
1072
+ return config.UnitsTransformed[series_index]
1073
+
1074
+ return None
dfm_python/data.py CHANGED
@@ -5,6 +5,7 @@ for Dynamic Factor Model estimation.
5
5
  """
6
6
 
7
7
  import logging
8
+ import warnings
8
9
  from pathlib import Path
9
10
  from typing import List, Optional, Tuple, Union
10
11
 
@@ -408,7 +409,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
408
409
  if len(warnings_list) > 5:
409
410
  logger.warning(f"... and {len(warnings_list) - 5} more series with T < N")
410
411
 
411
- import warnings
412
412
  warnings.warn(
413
413
  f"Insufficient data: {len(warnings_list)} series have T < N (time periods < number of series). "
414
414
  f"This may cause numerical issues. Suggested fix: increase sample size or reduce number of series. "
@@ -434,7 +434,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
434
434
  if len(extreme_missing_series) > 5:
435
435
  logger.warning(f"... and {len(extreme_missing_series) - 5} more series with >90% missing data")
436
436
 
437
- import warnings
438
437
  warnings.warn(
439
438
  f"Extreme missing data detected: {len(extreme_missing_series)} series have >90% missing values. "
440
439
  f"Estimation may be unreliable. Consider removing these series or increasing data coverage. "
@@ -446,55 +445,6 @@ def load_data(datafile: Union[str, Path], config: DFMConfig,
446
445
  return X, Time, Z
447
446
 
448
447
 
449
- def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
450
- """Load configuration from DataFrame (internal helper).
451
-
452
- This function converts a DataFrame with series specifications into a DFMConfig.
453
- It's used internally by SpecCSVSource and other config loaders.
454
-
455
- Parameters
456
- ----------
457
- df : pd.DataFrame
458
- DataFrame with columns: series_id, series_name, frequency, transformation, blocks
459
-
460
- Returns
461
- -------
462
- DFMConfig
463
- Model configuration object
464
- """
465
- series_list = []
466
- block_names_set = set()
467
-
468
- for _, row in df.iterrows():
469
- # Parse blocks (can be comma-separated string or list)
470
- blocks_str = row.get('blocks', 'Block_Global')
471
- if isinstance(blocks_str, str):
472
- blocks = [b.strip() for b in blocks_str.split(',')]
473
- else:
474
- blocks = blocks_str if isinstance(blocks_str, list) else ['Block_Global']
475
-
476
- # Track block names
477
- for block in blocks:
478
- block_names_set.add(block)
479
-
480
- series_list.append(SeriesConfig(
481
- series_id=row.get('series_id', f"series_{len(series_list)}"),
482
- series_name=row.get('series_name', row.get('series_id', f"Series {len(series_list)}")),
483
- frequency=row.get('frequency', 'm'),
484
- transformation=row.get('transformation', 'lin'),
485
- blocks=blocks
486
- ))
487
-
488
- # Create default blocks if none specified
489
- block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
490
- blocks = {}
491
- for block_name in block_names:
492
- blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
493
-
494
- # block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
495
- return DFMConfig(series=series_list, blocks=blocks)
496
-
497
-
498
448
  def rem_nans_spline(X: np.ndarray, method: int = 2, k: int = 3) -> Tuple[np.ndarray, np.ndarray]:
499
449
  """Treat NaNs in dataset for DFM estimation using standard interpolation methods.
500
450
 
dfm_python/dfm.py CHANGED
@@ -301,11 +301,14 @@ class EMAlgorithmParams:
301
301
  # Core functions are imported directly from core modules - no proxy functions needed
302
302
 
303
303
 
304
- class DFM:
304
+ class DFMCore:
305
305
  """Core Dynamic Factor Model class.
306
306
 
307
307
  This class provides the main DFM estimation functionality. The core algorithm
308
308
  is implemented in the `fit()` method, which performs EM estimation.
309
+
310
+ Note: This is the core implementation class. For high-level API with
311
+ convenience methods, use the `DFM` class from `api.py` which extends this class.
309
312
  """
310
313
 
311
314
  def __init__(self):
dfm_python/news.py CHANGED
@@ -18,6 +18,7 @@ import numpy as np
18
18
  from scipy.linalg import pinv, inv
19
19
  from typing import Tuple, Optional, Dict, Union, List, Any, Callable
20
20
  import pandas as pd
21
+ import warnings
21
22
  import logging
22
23
 
23
24
  from .kalman import skf, fis, miss_data
@@ -61,7 +62,6 @@ def _check_config_consistency(saved_config: Any, current_config: DFMConfig) -> N
61
62
  else:
62
63
  current_ids = []
63
64
  if saved_ids != current_ids:
64
- import warnings
65
65
  warnings.warn(
66
66
  "Config used in estimation differs from current config. "
67
67
  "Results may be inconsistent. Consider re-estimating.",
@@ -552,12 +552,9 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
552
552
  # Validate period format
553
553
  try:
554
554
  i_series = series_ids.index(series)
555
- if hasattr(config, 'get_frequencies'):
556
- frequencies = config.get_frequencies()
557
- elif hasattr(config, 'Frequency'):
558
- frequencies = config.Frequency
559
- else:
560
- frequencies = []
555
+ # Get frequencies using unified helper function (supports both new and legacy formats)
556
+ from .core.helpers import _get_frequencies_from_config
557
+ frequencies = _get_frequencies_from_config(config)
561
558
  freq = frequencies[i_series] if i_series < len(frequencies) else 'm'
562
559
  if freq == 'm':
563
560
  # Format: YYYYmMM (e.g., "2016m12")
@@ -650,12 +647,9 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
650
647
  else:
651
648
  series_names = []
652
649
 
653
- if hasattr(config, 'get_frequencies'):
654
- frequencies = config.get_frequencies()
655
- elif hasattr(config, 'Frequency'):
656
- frequencies = config.Frequency
657
- else:
658
- frequencies = []
650
+ # Get frequencies using unified helper function (supports both new and legacy formats)
651
+ from .core.helpers import _get_frequencies_from_config
652
+ frequencies = _get_frequencies_from_config(config)
659
653
  series_name = series_names[i_series] if i_series < len(series_names) else series
660
654
  freq = frequencies[i_series] if i_series < len(frequencies) else 'm'
661
655
 
@@ -705,14 +699,13 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
705
699
  else:
706
700
  period_str = pd.Timestamp(Time[t_nowcast]).strftime('%Y-%m')
707
701
 
708
- # Get units
709
- if hasattr(config, 'series') and i_series < len(config.series):
710
- units = config.series[i_series].units
711
- elif hasattr(config, 'UnitsTransformed') and i_series < len(config.UnitsTransformed):
712
- units = config.UnitsTransformed[i_series]
713
- else:
714
- units = ""
715
- print(f'Nowcast for {series_name} ({units}), {period_str}')
702
+ # Get units using unified helper function (supports both new and legacy formats)
703
+ from .core.helpers import _get_units_from_config
704
+ units = _get_units_from_config(config, i_series)
705
+
706
+ # Format units for display
707
+ units_str = f" ({units})" if units else ""
708
+ print(f'Nowcast for {series_name}{units_str}, {period_str}')
716
709
 
717
710
  if len(forecast) == 0 or np.all(np.isnan(forecast)):
718
711
  print('\n No forecast was made.\n')
@@ -774,7 +767,6 @@ def update_nowcast(X_old: np.ndarray, X_new: np.ndarray, Time: pd.DatetimeIndex,
774
767
  Res=Res
775
768
  )
776
769
  except Exception as e:
777
- import warnings
778
770
  warnings.warn(
779
771
  f"save_callback failed: {e}. Continuing without saving.",
780
772
  UserWarning
dfm_python/utils.py CHANGED
@@ -224,13 +224,10 @@ def get_aggregation_structure(
224
224
  >>> print(agg_info['tent_weights'])
225
225
  {'q': array([1, 2, 3, 2, 1]), 'sa': array([1, 2, 3, 4, 3, 2, 1])}
226
226
  """
227
- # Get frequencies using new API with fallback for legacy configs
228
- if hasattr(config, 'get_frequencies'):
229
- frequencies = set(config.get_frequencies())
230
- elif hasattr(config, 'Frequency'):
231
- frequencies = set(config.Frequency)
232
- else:
233
- frequencies = set()
227
+ # Get frequencies using unified helper function (supports both new and legacy formats)
228
+ from .core.helpers import _get_frequencies_from_config
229
+ frequencies_list = _get_frequencies_from_config(config)
230
+ frequencies = set(frequencies_list)
234
231
  structures = {}
235
232
  tent_weights = {}
236
233
  n_periods_map = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -25,18 +25,12 @@ Requires-Dist: numpy>=1.24.0
25
25
  Requires-Dist: pandas>=2.0.0
26
26
  Requires-Dist: scipy>=1.10.0
27
27
  Provides-Extra: all
28
- Requires-Dist: httpx>=0.24.0; extra == 'all'
29
28
  Requires-Dist: hydra-core>=1.3.0; extra == 'all'
30
29
  Requires-Dist: omegaconf>=2.3.0; extra == 'all'
31
- Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
32
- Requires-Dist: pydantic-settings>=2.11.0; extra == 'all'
33
30
  Requires-Dist: pytest>=7.0.0; extra == 'all'
34
- Requires-Dist: supabase>=2.0.0; extra == 'all'
35
- Provides-Extra: database
36
- Requires-Dist: httpx>=0.24.0; extra == 'database'
37
- Requires-Dist: psycopg2-binary>=2.9.0; extra == 'database'
38
- Requires-Dist: pydantic-settings>=2.11.0; extra == 'database'
39
- Requires-Dist: supabase>=2.0.0; extra == 'database'
31
+ Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
32
+ Provides-Extra: db
33
+ Requires-Dist: sqlalchemy>=2.0.0; extra == 'db'
40
34
  Provides-Extra: dev
41
35
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
42
36
  Provides-Extra: hydra
@@ -0,0 +1,18 @@
1
+ dfm_python/__init__.py,sha256=ygVrK-7Q8VPyOIPMib15hVu5AFQO-ILSCgFR8fsXcyU,4844
2
+ dfm_python/api.py,sha256=d18qOn1XG5F7gTuoFWxQ_6xhGff9AjGbym5Ji3m6Qfw,16989
3
+ dfm_python/config.py,sha256=-SkouUFq38-rQKfyFcg1nI4_YkNbwGCwogNQiyOOT8A,47193
4
+ dfm_python/config_sources.py,sha256=0e5_yOWUlMZA7bEgSAJF_ZFqr-vKTlhsqpl84GB2ySY,33193
5
+ dfm_python/data.py,sha256=22M9zn2fS1yaZvJe94k2BgOkH9LFwVKw_WltnVu-qFs,20380
6
+ dfm_python/dfm.py,sha256=ANG_-7qCizp8VS5tdR_aJoBtPhvRR4QGJjvntEpA6KY,35109
7
+ dfm_python/kalman.py,sha256=EXhloLbpzy7GKG9KFrSZt9pRp0y3CZyvycPVbvUWxq8,18074
8
+ dfm_python/news.py,sha256=abt6X7q-9b6TL0aRcCjhLUGuBOEnjo-pfK4kXgkLo8A,32503
9
+ dfm_python/utils.py,sha256=cRb3afQzpbh_RqoScaq25jH08bn1qImz_MADdFWRteY,15674
10
+ dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
11
+ dfm_python/core/diagnostics.py,sha256=1ninWcQlWhJmfUZq9kky7IQoRf_6fI_IlVzzSb7apRQ,28085
12
+ dfm_python/core/em.py,sha256=jbtsFHynMDtblZTtlzgodX04e86BBB9hPn20MMnV3w0,72705
13
+ dfm_python/core/helpers.py,sha256=NMM04OdSbrG73QDciuue8x0XvxleLYWr-OBzvG1wQ8c,34109
14
+ dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
15
+ dfm_python-0.2.9.dist-info/METADATA,sha256=0sOxEYOl7hJhj3vGe9CTLoj8D59Q1bbuqvkOjnresfQ,28264
16
+ dfm_python-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ dfm_python-0.2.9.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
18
+ dfm_python-0.2.9.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- dfm_python/__init__.py,sha256=oz0p9JreUg6D9SfU2vUAFMRwf7JwHaa3dQDy87YHtIw,5589
2
- dfm_python/api.py,sha256=-mmV7aaXOAqVW9HPhTEut7T6wN1EyW0rS3J0y-z-MRs,16221
3
- dfm_python/config.py,sha256=bBiKfoETohNVJ3brRbY1330C1nQROybCR3co9iagIKs,46978
4
- dfm_python/config_sources.py,sha256=6LTK7k5WJpnayMmtxtYwZ9SQi_hGXoz-Vg4sRGTabI0,21798
5
- dfm_python/data.py,sha256=GpvI4dZ48-uUI_rbsQhCq2WcbhyHJ-foER7-baTyHo8,22223
6
- dfm_python/dfm.py,sha256=9k_0RyxtsbZZrfqhAKmxX4CQy41CCoPolbe_oi8a_K8,34942
7
- dfm_python/kalman.py,sha256=EXhloLbpzy7GKG9KFrSZt9pRp0y3CZyvycPVbvUWxq8,18074
8
- dfm_python/news.py,sha256=iXbJnsvH5FgKToPzKnqIpTosrZu-QHXBNHjsqwMYrHo,32583
9
- dfm_python/utils.py,sha256=qOAJm2KG71yP2a29iJVDGOC-ZUS_HPL5oAo16EgNXA4,15709
10
- dfm_python/core/__init__.py,sha256=oDrhAoc1W_ODohgufQ-gN0uri8-vZI0EXsF0dysth_A,331
11
- dfm_python/core/diagnostics.py,sha256=1ninWcQlWhJmfUZq9kky7IQoRf_6fI_IlVzzSb7apRQ,28085
12
- dfm_python/core/em.py,sha256=jbtsFHynMDtblZTtlzgodX04e86BBB9hPn20MMnV3w0,72705
13
- dfm_python/core/helpers.py,sha256=XJvWWEZlUqozBfFNcENKgbOOFkwlm-tGwO-u0LnAf-E,31541
14
- dfm_python/core/numeric.py,sha256=IcIwcKtMhIoJ5-7Mz6UNYwTUtEa5W_ZMnQtF87v0SSc,26431
15
- dfm_python-0.2.8.dist-info/METADATA,sha256=wY69TZa4fK9I4zY4t7hQUJksWuMCyxtGUS0Nr_C3SPk,28599
16
- dfm_python-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- dfm_python-0.2.8.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
18
- dfm_python-0.2.8.dist-info/RECORD,,