dfm-python 0.4.2__py3-none-any.whl → 0.4.3__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.
Files changed (44) hide show
  1. dfm_python/__init__.py +9 -11
  2. dfm_python/config/__init__.py +8 -13
  3. dfm_python/config/adapter.py +12 -210
  4. dfm_python/config/results.py +18 -10
  5. dfm_python/config/schema.py +31 -66
  6. dfm_python/config/utils.py +13 -13
  7. dfm_python/data/__init__.py +5 -29
  8. dfm_python/data/dataloader.py +51 -8
  9. dfm_python/data/utils.py +28 -30
  10. dfm_python/encoder/vae.py +3 -2
  11. dfm_python/lightning/__init__.py +27 -14
  12. dfm_python/lightning/data_module.py +348 -220
  13. dfm_python/lightning/scaling.py +432 -0
  14. dfm_python/models/__init__.py +3 -3
  15. dfm_python/models/base.py +335 -6
  16. dfm_python/models/ddfm.py +1137 -321
  17. dfm_python/models/dfm.py +125 -586
  18. dfm_python/models/estimation.py +13 -14
  19. dfm_python/nowcast/__init__.py +17 -8
  20. dfm_python/nowcast/dataview.py +7 -7
  21. dfm_python/nowcast/helpers.py +6 -6
  22. dfm_python/nowcast/nowcast.py +274 -26
  23. dfm_python/nowcast/splitters.py +431 -0
  24. dfm_python/nowcast/transformers.py +330 -0
  25. dfm_python/nowcast/utils.py +11 -11
  26. dfm_python/ssm/kalman.py +13 -27
  27. dfm_python/ssm/utils.py +3 -1
  28. dfm_python/trainer/__init__.py +22 -22
  29. dfm_python/trainer/ddfm.py +7 -7
  30. dfm_python/trainer/dfm.py +7 -7
  31. dfm_python/utils/__init__.py +15 -15
  32. dfm_python/utils/data.py +17 -20
  33. dfm_python/utils/diagnostics.py +28 -23
  34. dfm_python/utils/helpers.py +7 -7
  35. dfm_python/utils/statespace.py +41 -71
  36. dfm_python/utils/time.py +148 -249
  37. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/METADATA +24 -64
  38. dfm_python-0.4.3.dist-info/RECORD +52 -0
  39. dfm_python/data/transformation.py +0 -313
  40. dfm_python/lightning/ddfm_module.py +0 -580
  41. dfm_python/lightning/dfm_module.py +0 -466
  42. dfm_python-0.4.2.dist-info/RECORD +0 -52
  43. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/WHEEL +0 -0
  44. {dfm_python-0.4.2.dist-info → dfm_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
dfm_python/__init__.py CHANGED
@@ -19,7 +19,7 @@ has a frequency faster than the clock, a ValueError will be raised.
19
19
 
20
20
  Key Features:
21
21
  - Hydra-based configuration (YAML files) - primary configuration method
22
- - Expects preprocessed data from users - package applies internal transformations automatically
22
+ - Expects preprocessed data from users - users handle all preprocessing, package extracts statistics from pipeline
23
23
  - Flexible block structure for factor modeling
24
24
  - Robust handling of missing data (internal spline interpolation)
25
25
  - Automatic standardization and data clipping
@@ -27,11 +27,11 @@ Key Features:
27
27
 
28
28
  Example (Standard Lightning Pattern):
29
29
  >>> from dfm_python import DFM, DFMDataModule, DFMTrainer
30
- >>> import polars as pl
30
+ >>> import pandas as pd
31
31
  >>>
32
32
  >>> # Step 1: Load and preprocess data
33
- >>> df = pl.read_csv('data/sample_data.csv')
34
- >>> df_processed = df.select([col for col in df.columns if col != 'date'])
33
+ >>> df = pd.read_csv('data/sample_data.csv')
34
+ >>> df_processed = df[[col for col in df.columns if col != 'date']]
35
35
  >>>
36
36
  >>> # Step 2: Create DataModule
37
37
  >>> dm = DFMDataModule(config_path='config/default.yaml', data=df_processed)
@@ -67,7 +67,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
67
67
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
68
68
  """
69
69
 
70
- __version__ = "0.4.2"
70
+ __version__ = "0.4.3"
71
71
 
72
72
  # ============================================================================
73
73
  # PUBLIC API DEFINITION
@@ -79,8 +79,8 @@ __version__ = "0.4.2"
79
79
  # Public API categories:
80
80
  # 1. Configuration: DFMConfig, SeriesConfig, config sources
81
81
  # 2. High-level API: DFM, DDFM, module-level convenience functions
82
- # 3. Core utilities: DFMLinear, TimeIndex, diagnostics
83
- # 4. Models: BaseFactorModel, DFMLinear, DDFM (low-level)
82
+ # 3. Core utilities: TimeIndex, diagnostics
83
+ # 4. Models: BaseFactorModel, DDFM (low-level)
84
84
  # 5. Nowcasting: Nowcast, result classes, para_const
85
85
  # 6. Data & Results: DFMResult
86
86
  # ============================================================================
@@ -94,8 +94,6 @@ from .config import (
94
94
  # Internal imports (for backward compatibility, but not recommended)
95
95
  from .config import DFMConfig, SeriesConfig # Internal use only
96
96
 
97
- # Data utilities
98
-
99
97
  # Results
100
98
  from .config.results import DFMResult, DDFMResult, BaseResult
101
99
 
@@ -125,14 +123,14 @@ from .nowcast.helpers import (
125
123
 
126
124
  # Model implementations
127
125
  from .models.base import BaseFactorModel
128
- from .models.dfm import DFMLinear, DFM
126
+ from .models.dfm import DFM
129
127
 
130
128
  # DDFM high-level API and low-level model (PyTorch is mandatory)
131
129
  from .models.ddfm import DDFM, DDFMModel
132
130
 
133
131
  __all__ = [
134
132
  # Core classes
135
- 'DFM', 'DFMLinear', 'Nowcast',
133
+ 'DFM', 'Nowcast',
136
134
  # Model base and implementations
137
135
  'BaseFactorModel',
138
136
  # Nowcast result classes
@@ -18,21 +18,18 @@ from .adapter import (
18
18
  HydraSource,
19
19
  MergedConfigSource,
20
20
  make_config_source,
21
- # Note: from_spec is in models/dfm.py, not in adapter
22
- # Note: Private functions (_load_config_from_dataframe, _write_series_blocks_yaml)
23
- # are available from adapter module but not exported as public API
24
21
  )
25
22
  from .utils import (
26
23
  FREQUENCY_HIERARCHY,
27
24
  PERIODS_PER_YEAR,
28
25
  get_periods_per_year,
29
26
  get_annual_factor,
30
- compute_idio_chain_lengths,
31
- get_tent_weights_for_pair,
27
+ compute_idio_lengths,
28
+ get_tent_weights,
32
29
  generate_tent_weights,
33
30
  generate_R_mat,
34
- get_aggregation_structure,
35
- group_series_by_frequency,
31
+ get_agg_structure,
32
+ group_by_freq,
36
33
  )
37
34
 
38
35
  __all__ = [
@@ -53,13 +50,11 @@ __all__ = [
53
50
  'PERIODS_PER_YEAR',
54
51
  'get_periods_per_year',
55
52
  'get_annual_factor',
56
- 'compute_idio_chain_lengths',
57
- 'get_tent_weights_for_pair',
53
+ 'compute_idio_lengths',
54
+ 'get_tent_weights',
58
55
  'generate_tent_weights',
59
56
  'generate_R_mat',
60
- 'get_aggregation_structure',
61
- 'group_series_by_frequency',
62
- # Note: Private functions (_load_config_from_dataframe, _write_series_blocks_yaml)
63
- # are available from adapter module but not exported as public API
57
+ 'get_agg_structure',
58
+ 'group_by_freq',
64
59
  ]
65
60
 
@@ -3,7 +3,6 @@
3
3
  This module provides adapters for loading DFMConfig from various sources:
4
4
  - YAML files (with Hydra/OmegaConf support)
5
5
  - Dictionary configurations
6
- - Spec CSV files (series definitions)
7
6
  - Hydra DictConfig objects
8
7
  - Merged configurations from multiple sources
9
8
 
@@ -25,12 +24,11 @@ except ImportError:
25
24
 
26
25
  if TYPE_CHECKING:
27
26
  from .schema import DDFMConfig
28
- import polars as pl
29
27
 
30
28
  _logger = get_logger(__name__)
31
29
 
32
30
 
33
- def _load_config_from_defaults(cfg, root_config_dir, config_type: str) -> Optional[dict]:
31
+ def _load_config_defaults(cfg, root_config_dir, config_type: str) -> Optional[dict]:
34
32
  """Load config from defaults or direct path (helper for series/blocks loading).
35
33
 
36
34
  Parameters
@@ -195,7 +193,7 @@ class YamlSource:
195
193
 
196
194
  # Load series from config/series/default.yaml
197
195
  series_list = []
198
- series_dict = _load_config_from_defaults(cfg, root_config_dir, 'series')
196
+ series_dict = _load_config_defaults(cfg, root_config_dir, 'series')
199
197
  series_loaded = series_dict is not None
200
198
 
201
199
  # Convert series dict to SeriesConfig objects
@@ -216,7 +214,8 @@ class YamlSource:
216
214
  frequency=series_data.get('frequency', 'm'),
217
215
  transformation=series_data.get('transformation', 'lin'),
218
216
  blocks=series_data.get('blocks', []),
219
- release_date=release_date
217
+ release_date=release_date,
218
+ scaler=series_data.get('scaler', None) # Optional, for per-series scaling
220
219
  ))
221
220
 
222
221
  # If no series loaded from separate files, try to get from main config
@@ -248,7 +247,7 @@ class YamlSource:
248
247
  blocks_properties_from_config = blocks_data
249
248
 
250
249
  # Also try loading from config/blocks/default.yaml
251
- blocks_dict_raw = _load_config_from_defaults(cfg, root_config_dir, 'blocks')
250
+ blocks_dict_raw = _load_config_defaults(cfg, root_config_dir, 'blocks')
252
251
  if blocks_dict_raw is not None:
253
252
  blocks_properties_from_config.update(blocks_dict_raw)
254
253
 
@@ -403,7 +402,7 @@ class MergedConfigSource:
403
402
  """Merge multiple configuration sources.
404
403
 
405
404
  This allows combining configurations from different sources,
406
- e.g., base YAML config + series from Spec CSV.
405
+ e.g., base YAML config + series from another YAML or dict.
407
406
 
408
407
  The merge strategy:
409
408
  - Base config provides main settings (threshold, max_iter, clock, blocks)
@@ -493,86 +492,6 @@ class MergedConfigSource:
493
492
  )
494
493
 
495
494
 
496
- def _write_series_blocks_yaml(
497
- config: DFMConfig,
498
- output_dir: Path,
499
- series_filename: str,
500
- blocks_filename: str
501
- ) -> Tuple[Path, Path]:
502
- """Write series and block YAML files for a given configuration."""
503
- series_dir = output_dir / 'series'
504
- series_dir.mkdir(parents=True, exist_ok=True)
505
- series_path = series_dir / f'{series_filename}.yaml'
506
-
507
- blocks_dir = output_dir / 'blocks'
508
- blocks_dir.mkdir(parents=True, exist_ok=True)
509
- blocks_path = blocks_dir / f'{blocks_filename}.yaml'
510
-
511
- series_dict = {}
512
- block_names = config.block_names
513
-
514
- for series in config.series:
515
- if isinstance(series.blocks, list) and len(series.blocks) > 0:
516
- if isinstance(series.blocks[0], int):
517
- blocks_names = [block_names[i] for i, val in enumerate(series.blocks) if val != 0]
518
- else:
519
- blocks_names = list(series.blocks)
520
- else:
521
- blocks_names = [DEFAULT_BLOCK_NAME]
522
-
523
- series_entry = {
524
- 'series_name': series.series_name,
525
- 'frequency': series.frequency,
526
- 'transformation': series.transformation,
527
- 'blocks': blocks_names
528
- }
529
- if series.release_date is not None:
530
- series_entry['release'] = series.release_date
531
- series_dict[series.series_id] = series_entry
532
-
533
- blocks_dict = {}
534
- for block_name, block_config in config.blocks.items():
535
- block_entry = {
536
- 'factors': block_config.get('factors', 1),
537
- 'ar_lag': block_config.get('ar_lag', 1),
538
- 'clock': block_config.get('clock', 'm')
539
- }
540
- if block_config.get('notes'):
541
- block_entry['notes'] = block_config['notes']
542
- blocks_dict[block_name] = block_entry
543
-
544
- def _write_yaml(path: Path, payload: Dict[str, Any]) -> None:
545
- try:
546
- import yaml # type: ignore
547
- yaml_kwargs = {'default_flow_style': False, 'sort_keys': False, 'allow_unicode': True}
548
- import io
549
- stream = io.StringIO()
550
- yaml.dump(payload, stream, **yaml_kwargs)
551
- yaml_content = stream.getvalue()
552
- lines = yaml_content.split('\n')
553
- formatted_lines = []
554
- for i, line in enumerate(lines):
555
- if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
556
- if i > 0 and formatted_lines and formatted_lines[-1].strip():
557
- formatted_lines.append('')
558
- formatted_lines.append(line)
559
- with open(path, 'w', encoding='utf-8') as f:
560
- f.write('\n'.join(formatted_lines))
561
- except ImportError:
562
- try:
563
- from omegaconf import OmegaConf # type: ignore
564
- except ImportError as exc: # pragma: no cover
565
- raise ImportError(
566
- "Either PyYAML or omegaconf is required for YAML generation. "
567
- "Install with: pip install pyyaml or pip install omegaconf"
568
- ) from exc
569
- cfg = OmegaConf.create(payload)
570
- OmegaConf.save(cfg, path)
571
-
572
- _write_yaml(series_path, series_dict)
573
- _write_yaml(blocks_path, blocks_dict)
574
-
575
- return series_path, blocks_path
576
495
 
577
496
 
578
497
  def make_config_source(
@@ -580,7 +499,6 @@ def make_config_source(
580
499
  *,
581
500
  yaml: Optional[Union[str, Path]] = None,
582
501
  mapping: Optional[Union[Dict[str, Any], Any]] = None,
583
- spec: Optional[Union[str, Path]] = None,
584
502
  hydra: Optional[Union[Dict[str, Any], 'DictConfig']] = None,
585
503
  ) -> ConfigSource:
586
504
  """Create a ConfigSource adapter from various input formats.
@@ -598,8 +516,6 @@ def make_config_source(
598
516
  Explicit YAML file path
599
517
  mapping : dict, optional
600
518
  Explicit dictionary config
601
- spec : str or Path, optional
602
- Explicit spec CSV file path
603
519
  hydra : DictConfig or dict, optional
604
520
  Explicit Hydra config
605
521
 
@@ -618,18 +534,17 @@ def make_config_source(
618
534
  >>>
619
535
  >>> # Explicit keyword arguments
620
536
  >>> source = make_config_source(yaml='config/default.yaml')
621
- >>> source = make_config_source(spec='data/spec.csv')
622
537
  >>>
623
- >>> # Merge YAML base + Spec CSV series
538
+ >>> # Merge YAML base + dict override
624
539
  >>> base = make_config_source(yaml='config/default.yaml')
625
- >>> override = make_config_source(spec='data/spec.csv')
540
+ >>> override = make_config_source(mapping={'series': [...]})
626
541
  >>> merged = MergedConfigSource(base, override)
627
542
  """
628
543
  # Check for explicit keyword arguments (only one allowed)
629
- explicit_kwargs = [k for k, v in [('yaml', yaml), ('mapping', mapping), ('spec', spec), ('hydra', hydra)] if v is not None]
544
+ explicit_kwargs = [k for k, v in [('yaml', yaml), ('mapping', mapping), ('hydra', hydra)] if v is not None]
630
545
  if len(explicit_kwargs) > 1:
631
546
  raise ValueError(
632
- f"Only one of yaml, mapping, spec, or hydra can be specified. "
547
+ f"Only one of yaml, mapping, or hydra can be specified. "
633
548
  f"Got: {', '.join(explicit_kwargs)}. "
634
549
  f"For merging configs, use MergedConfigSource."
635
550
  )
@@ -655,12 +570,6 @@ def make_config_source(
655
570
  return YamlSource(yaml)
656
571
  if mapping is not None:
657
572
  return DictSource(_coerce_to_mapping(mapping))
658
- if spec is not None:
659
- raise ValueError(
660
- "Direct spec CSV loading has been removed. "
661
- "Use dfm_python.api.from_spec() or from_spec_df() to convert the CSV "
662
- "to YAML files, then load via YAML/Hydra."
663
- )
664
573
  if hydra is not None:
665
574
  return HydraSource(hydra)
666
575
 
@@ -668,7 +577,7 @@ def make_config_source(
668
577
  if source is None:
669
578
  raise ValueError(
670
579
  "No configuration source provided. "
671
- "Specify source, yaml, mapping, spec, or hydra."
580
+ "Specify source, yaml, mapping, or hydra."
672
581
  )
673
582
 
674
583
  # If already a ConfigSource, return as-is
@@ -693,8 +602,7 @@ def make_config_source(
693
602
  elif suffix == '.csv':
694
603
  raise ValueError(
695
604
  "Direct CSV configs are no longer supported. "
696
- "Use dfm_python.api.from_spec() or from_spec_df() to convert the CSV "
697
- "to YAML files, then load the YAML configuration."
605
+ "Please use YAML configuration files instead."
698
606
  )
699
607
  else:
700
608
  # Default to YAML if extension unclear
@@ -715,112 +623,6 @@ def make_config_source(
715
623
  )
716
624
 
717
625
 
718
- # ============================================================================
719
- # Internal helper: Load config from DataFrame
720
- # ============================================================================
721
-
722
- def _load_config_from_dataframe(df: pl.DataFrame) -> DFMConfig:
723
- """Load configuration from DataFrame (internal helper).
724
-
725
- This function converts a DataFrame with series specifications into a DFMConfig.
726
-
727
- Parameters
728
- ----------
729
- df : pl.DataFrame
730
- Polars DataFrame with columns: series_id, series_name, frequency, transformation, blocks
731
- Optional columns: Release (release date)
732
- Block_* columns: Binary indicators for block membership
733
-
734
- Returns
735
- -------
736
- DFMConfig
737
- Model configuration object
738
- """
739
- series_list = []
740
- block_names_set = set()
741
-
742
- # Convert polars DataFrame to dict of rows for easier access
743
- df_dict = df.to_dict(as_series=False)
744
-
745
- # First, try to find Block_* columns (CSV format)
746
- block_cols = [col for col in df.columns
747
- if (col.startswith('Block_') or col.startswith('Block-'))
748
- and col not in ['blocks']]
749
-
750
- # Iterate over rows
751
- num_rows = len(df)
752
- for i in range(num_rows):
753
- # Parse blocks from Block_* columns or 'blocks' column
754
- blocks = []
755
-
756
- if block_cols:
757
- # CSV format: global block, block_1, etc. columns with 1/0 values
758
- for block_col in block_cols:
759
- if block_col in df_dict:
760
- block_value = df_dict[block_col][i]
761
- # Handle various types: int, float, numpy int/float, bool, string "1"/"0"
762
- # Check numpy types first (numpy.int64, numpy.float64, etc.)
763
- if hasattr(block_value, 'item'):
764
- # numpy scalar - convert to Python native type
765
- block_value = block_value.item()
766
-
767
- if isinstance(block_value, (int, float)) and block_value != 0:
768
- blocks.append(block_col)
769
- elif isinstance(block_value, bool) and block_value:
770
- blocks.append(block_col)
771
- elif isinstance(block_value, str) and block_value.strip() in ['1', 'True', 'true']:
772
- blocks.append(block_col)
773
- else:
774
- # Fallback: try 'blocks' column (comma-separated string or list)
775
- if 'blocks' in df_dict:
776
- blocks_str = df_dict['blocks'][i]
777
- if isinstance(blocks_str, str):
778
- blocks = [b.strip() for b in blocks_str.split(',')]
779
- elif isinstance(blocks_str, list):
780
- blocks = blocks_str
781
- else:
782
- blocks = [DEFAULT_BLOCK_NAME]
783
- else:
784
- blocks = [DEFAULT_BLOCK_NAME]
785
-
786
- # Ensure at least one block
787
- if not blocks:
788
- blocks = [DEFAULT_BLOCK_NAME]
789
-
790
- # Track block names
791
- for block in blocks:
792
- block_names_set.add(block)
793
-
794
- # Parse release_date if available
795
- release_date = None
796
- if 'Release' in df_dict:
797
- release_date = df_dict['Release'][i]
798
- if release_date is not None:
799
- try:
800
- release_date = int(release_date)
801
- except (ValueError, TypeError):
802
- release_date = None
803
-
804
- series_id = df_dict.get('series_id', [f"series_{j}" for j in range(num_rows)])[i]
805
- series_name = df_dict.get('series_name', df_dict.get('series_id', [f"Series {j}" for j in range(num_rows)]))[i]
806
-
807
- series_list.append(SeriesConfig(
808
- series_id=series_id,
809
- series_name=series_name,
810
- frequency=df_dict.get('frequency', ['m'] * num_rows)[i],
811
- transformation=df_dict.get('transformation', ['lin'] * num_rows)[i],
812
- blocks=blocks,
813
- release_date=release_date
814
- ))
815
-
816
- # Create default blocks if none specified
817
- block_names = sorted(block_names_set) if block_names_set else [DEFAULT_BLOCK_NAME]
818
- blocks = {}
819
- for block_name in block_names:
820
- blocks[block_name] = {'factors': 1, 'ar_lag': 1, 'clock': 'm'}
821
-
822
- # block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
823
- return DFMConfig(series=series_list, blocks=blocks)
824
626
 
825
627
 
826
628
  # ============================================================================
@@ -4,6 +4,7 @@ This module contains dataclasses for storing DFM estimation results and paramete
4
4
  """
5
5
 
6
6
  import numpy as np
7
+ import warnings
7
8
  from dataclasses import dataclass
8
9
  from typing import Optional, List, Dict, Any
9
10
  from abc import ABC
@@ -118,8 +119,8 @@ class BaseResult(ABC):
118
119
  except Exception:
119
120
  return self.num_state()
120
121
 
121
- def to_polars_factors(self, time_index: Optional[object] = None, factor_names: Optional[List[str]] = None):
122
- """Return factors as polars DataFrame.
122
+ def to_pandas_factors(self, time_index: Optional[object] = None, factor_names: Optional[List[str]] = None):
123
+ """Return factors as pandas DataFrame.
123
124
 
124
125
  Parameters
125
126
  ----------
@@ -129,7 +130,7 @@ class BaseResult(ABC):
129
130
  Column names. Defaults to F1..Fm.
130
131
  """
131
132
  try:
132
- import polars as pl
133
+ import pandas as pd
133
134
  from ..utils.time import TimeIndex
134
135
 
135
136
  cols = factor_names if factor_names is not None else [f"F{i+1}" for i in range(self.num_state())]
@@ -152,17 +153,24 @@ class BaseResult(ABC):
152
153
  if time_list:
153
154
  df_dict['time'] = time_list
154
155
 
155
- return pl.DataFrame(df_dict)
156
+ return pd.DataFrame(df_dict)
156
157
  except (ImportError, ValueError, TypeError):
157
158
  return self.Z
158
159
 
159
- def to_polars_smoothed(self, time_index: Optional[object] = None, series_ids: Optional[List[str]] = None):
160
- """Return smoothed data (original scale) as polars DataFrame."""
160
+
161
+ def to_pandas_smoothed(self, time_index: Optional[object] = None, series_ids: Optional[List[str]] = None):
162
+ """Return smoothed data (original scale) as pandas DataFrame."""
161
163
  try:
162
- import polars as pl
164
+ import pandas as pd
163
165
  from ..utils.time import TimeIndex
164
166
 
165
- cols = series_ids if series_ids is not None else (self.series_ids if self.series_ids is not None else [f"S{i+1}" for i in range(self.num_series())])
167
+ # Get column names: use provided series_ids, fallback to stored IDs, or generate defaults
168
+ if series_ids is not None:
169
+ cols = series_ids
170
+ elif self.series_ids is not None:
171
+ cols = self.series_ids
172
+ else:
173
+ cols = [f"S{i+1}" for i in range(self.num_series())]
166
174
 
167
175
  # Create DataFrame with series as columns
168
176
  df_dict = {col: self.X_sm[:, i] for i, col in enumerate(cols)}
@@ -182,10 +190,10 @@ class BaseResult(ABC):
182
190
  if time_list:
183
191
  df_dict['time'] = time_list
184
192
 
185
- return pl.DataFrame(df_dict)
193
+ return pd.DataFrame(df_dict)
186
194
  except (ImportError, ValueError, TypeError):
187
195
  return self.X_sm
188
-
196
+
189
197
 
190
198
  def save(self, path: str) -> None:
191
199
  """Save result to a pickle file."""