dfm-python 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. dfm_python-0.4.0/.git +1 -0
  2. {dfm_python-0.3.1 → dfm_python-0.4.0}/PKG-INFO +11 -9
  3. {dfm_python-0.3.1 → dfm_python-0.4.0}/README.md +6 -6
  4. dfm_python-0.4.0/SKTIME_INTEGRATION_PLAN.md +562 -0
  5. {dfm_python-0.3.1 → dfm_python-0.4.0}/pyproject.toml +8 -3
  6. {dfm_python-0.3.1 → dfm_python-0.4.0}/src/dfm_python/__init__.py +103 -60
  7. dfm_python-0.4.0/src/dfm_python/config/__init__.py +65 -0
  8. dfm_python-0.4.0/src/dfm_python/config/params.py +202 -0
  9. {dfm_python-0.3.1/src/dfm_python/core → dfm_python-0.4.0/src/dfm_python/config}/results.py +96 -71
  10. {dfm_python-0.3.1 → dfm_python-0.4.0}/src/dfm_python/config/schema.py +260 -331
  11. {dfm_python-0.3.1/src/dfm_python/core → dfm_python-0.4.0/src/dfm_python/config}/structure.py +1 -1
  12. dfm_python-0.4.0/src/dfm_python/config/utils.py +93 -0
  13. dfm_python-0.4.0/src/dfm_python/encoder/__init__.py +33 -0
  14. dfm_python-0.4.0/src/dfm_python/encoder/base.py +0 -0
  15. dfm_python-0.4.0/src/dfm_python/encoder/pca.py +157 -0
  16. dfm_python-0.4.0/src/dfm_python/encoder/vae.py +291 -0
  17. dfm_python-0.4.0/src/dfm_python/lightning/__init__.py +48 -0
  18. dfm_python-0.4.0/src/dfm_python/lightning/callbacks.py +0 -0
  19. dfm_python-0.4.0/src/dfm_python/lightning/data_module.py +364 -0
  20. dfm_python-0.4.0/src/dfm_python/lightning/ddfm_module.py +553 -0
  21. dfm_python-0.4.0/src/dfm_python/lightning/dfm_module.py +466 -0
  22. dfm_python-0.4.0/src/dfm_python/lightning/utils.py +0 -0
  23. dfm_python-0.4.0/src/dfm_python/logger/__init__.py +51 -0
  24. dfm_python-0.4.0/src/dfm_python/logger/inference_logger.py +270 -0
  25. dfm_python-0.4.0/src/dfm_python/logger/logger.py +110 -0
  26. dfm_python-0.4.0/src/dfm_python/logger/train_logger.py +309 -0
  27. dfm_python-0.4.0/src/dfm_python/models/__init__.py +36 -0
  28. {dfm_python-0.3.1 → dfm_python-0.4.0}/src/dfm_python/models/base.py +24 -28
  29. dfm_python-0.4.0/src/dfm_python/models/ddfm.py +1120 -0
  30. dfm_python-0.4.0/src/dfm_python/models/dfm.py +810 -0
  31. dfm_python-0.4.0/src/dfm_python/models/estimation.py +352 -0
  32. dfm_python-0.4.0/src/dfm_python/nowcast/__init__.py +28 -0
  33. dfm_python-0.4.0/src/dfm_python/nowcast/dataview.py +143 -0
  34. dfm_python-0.4.0/src/dfm_python/nowcast/helpers.py +554 -0
  35. {dfm_python-0.3.1/src/dfm_python/nowcasting → dfm_python-0.4.0/src/dfm_python/nowcast}/nowcast.py +57 -536
  36. dfm_python-0.4.0/src/dfm_python/nowcast/utils.py +290 -0
  37. dfm_python-0.4.0/src/dfm_python/ssm/__init__.py +47 -0
  38. dfm_python-0.4.0/src/dfm_python/ssm/em.py +488 -0
  39. dfm_python-0.4.0/src/dfm_python/ssm/kalman.py +604 -0
  40. dfm_python-0.4.0/src/dfm_python/ssm/utils.py +379 -0
  41. dfm_python-0.4.0/src/dfm_python/trainer/__init__.py +20 -0
  42. dfm_python-0.4.0/src/dfm_python/trainer/ddfm.py +196 -0
  43. dfm_python-0.4.0/src/dfm_python/trainer/dfm.py +153 -0
  44. dfm_python-0.4.0/src/dfm_python/transformations/__init__.py +35 -0
  45. dfm_python-0.4.0/src/dfm_python/transformations/sktime.py +40 -0
  46. dfm_python-0.4.0/src/dfm_python/transformations/transformers.py +320 -0
  47. dfm_python-0.4.0/src/dfm_python/transformations/utils.py +339 -0
  48. dfm_python-0.4.0/src/dfm_python/utils/__init__.py +201 -0
  49. dfm_python-0.4.0/src/dfm_python/utils/data.py +706 -0
  50. {dfm_python-0.3.1/src/dfm_python/core → dfm_python-0.4.0/src/dfm_python/utils}/diagnostics.py +11 -87
  51. dfm_python-0.4.0/src/dfm_python/utils/helpers.py +533 -0
  52. dfm_python-0.4.0/src/dfm_python/utils/statespace.py +1326 -0
  53. {dfm_python-0.3.1/src/dfm_python/core → dfm_python-0.4.0/src/dfm_python/utils}/time.py +373 -0
  54. dfm_python-0.4.0/src/test/__init__.py +226 -0
  55. dfm_python-0.4.0/src/test/test_ddfm.py +681 -0
  56. dfm_python-0.4.0/src/test/test_dfm.py +450 -0
  57. dfm_python-0.4.0/src/test/test_lightning_module.py +678 -0
  58. dfm_python-0.4.0/src/test/test_nowcast.py +516 -0
  59. dfm_python-0.4.0/src/test/test_trainer.py +54 -0
  60. dfm_python-0.4.0/src/test/test_transformations.py +1151 -0
  61. dfm_python-0.4.0/test_comprehensive.py +1 -0
  62. {dfm_python-0.3.1 → dfm_python-0.4.0}/uv.lock +1 -1
  63. dfm_python-0.3.1/.git +0 -1
  64. dfm_python-0.3.1/config/generated/blocks/spec_20251118_194140_025b98_blocks.yaml +0 -29
  65. dfm_python-0.3.1/config/generated/blocks/spec_20251118_194248_82a2a3_blocks.yaml +0 -29
  66. dfm_python-0.3.1/config/generated/blocks/spec_20251118_194423_013beb_blocks.yaml +0 -29
  67. dfm_python-0.3.1/config/generated/blocks/spec_20251118_194532_190712_blocks.yaml +0 -29
  68. dfm_python-0.3.1/config/generated/blocks/spec_20251118_195001_ef69d1_blocks.yaml +0 -29
  69. dfm_python-0.3.1/config/generated/blocks/spec_20251118_195110_eb12bc_blocks.yaml +0 -29
  70. dfm_python-0.3.1/config/generated/blocks/spec_20251118_195439_12eea1_blocks.yaml +0 -29
  71. dfm_python-0.3.1/config/generated/blocks/spec_20251118_195547_0ecebd_blocks.yaml +0 -29
  72. dfm_python-0.3.1/config/generated/blocks/spec_20251118_200209_8b6463_blocks.yaml +0 -29
  73. dfm_python-0.3.1/config/generated/blocks/spec_20251118_200319_c62fea_blocks.yaml +0 -29
  74. dfm_python-0.3.1/config/generated/blocks/spec_20251118_200722_7d6a26_blocks.yaml +0 -29
  75. dfm_python-0.3.1/config/generated/blocks/spec_20251118_200838_320d08_blocks.yaml +0 -29
  76. dfm_python-0.3.1/config/generated/blocks/spec_20251118_200934_65c460_blocks.yaml +0 -29
  77. dfm_python-0.3.1/config/generated/blocks/spec_20251118_201046_8e7482_blocks.yaml +0 -29
  78. dfm_python-0.3.1/config/generated/series/spec_20251118_194140_025b98_series.yaml +0 -749
  79. dfm_python-0.3.1/config/generated/series/spec_20251118_194248_82a2a3_series.yaml +0 -749
  80. dfm_python-0.3.1/config/generated/series/spec_20251118_194423_013beb_series.yaml +0 -749
  81. dfm_python-0.3.1/config/generated/series/spec_20251118_194532_190712_series.yaml +0 -749
  82. dfm_python-0.3.1/config/generated/series/spec_20251118_195001_ef69d1_series.yaml +0 -749
  83. dfm_python-0.3.1/config/generated/series/spec_20251118_195110_eb12bc_series.yaml +0 -749
  84. dfm_python-0.3.1/config/generated/series/spec_20251118_195439_12eea1_series.yaml +0 -749
  85. dfm_python-0.3.1/config/generated/series/spec_20251118_195547_0ecebd_series.yaml +0 -749
  86. dfm_python-0.3.1/config/generated/series/spec_20251118_200209_8b6463_series.yaml +0 -749
  87. dfm_python-0.3.1/config/generated/series/spec_20251118_200319_c62fea_series.yaml +0 -749
  88. dfm_python-0.3.1/config/generated/series/spec_20251118_200722_7d6a26_series.yaml +0 -749
  89. dfm_python-0.3.1/config/generated/series/spec_20251118_200838_320d08_series.yaml +0 -749
  90. dfm_python-0.3.1/config/generated/series/spec_20251118_200934_65c460_series.yaml +0 -749
  91. dfm_python-0.3.1/config/generated/series/spec_20251118_201046_8e7482_series.yaml +0 -749
  92. dfm_python-0.3.1/config/generated/spec_20251118_194140_025b98.yaml +0 -29
  93. dfm_python-0.3.1/config/generated/spec_20251118_194248_82a2a3.yaml +0 -29
  94. dfm_python-0.3.1/config/generated/spec_20251118_194423_013beb.yaml +0 -29
  95. dfm_python-0.3.1/config/generated/spec_20251118_194532_190712.yaml +0 -29
  96. dfm_python-0.3.1/config/generated/spec_20251118_195001_ef69d1.yaml +0 -29
  97. dfm_python-0.3.1/config/generated/spec_20251118_195110_eb12bc.yaml +0 -29
  98. dfm_python-0.3.1/config/generated/spec_20251118_195439_12eea1.yaml +0 -29
  99. dfm_python-0.3.1/config/generated/spec_20251118_195547_0ecebd.yaml +0 -29
  100. dfm_python-0.3.1/config/generated/spec_20251118_200209_8b6463.yaml +0 -29
  101. dfm_python-0.3.1/config/generated/spec_20251118_200319_c62fea.yaml +0 -29
  102. dfm_python-0.3.1/config/generated/spec_20251118_200722_7d6a26.yaml +0 -29
  103. dfm_python-0.3.1/config/generated/spec_20251118_200838_320d08.yaml +0 -29
  104. dfm_python-0.3.1/config/generated/spec_20251118_200934_65c460.yaml +0 -29
  105. dfm_python-0.3.1/config/generated/spec_20251118_201046_8e7482.yaml +0 -29
  106. dfm_python-0.3.1/src/adapters/__init__.py +0 -52
  107. dfm_python-0.3.1/src/adapters/data_view.py +0 -230
  108. dfm_python-0.3.1/src/adapters/database.py +0 -161
  109. dfm_python-0.3.1/src/adapters/model_result.py +0 -200
  110. dfm_python-0.3.1/src/dfm_python/api/__init__.py +0 -44
  111. dfm_python-0.3.1/src/dfm_python/api/model_api.py +0 -1148
  112. dfm_python-0.3.1/src/dfm_python/api/session_api.py +0 -402
  113. dfm_python-0.3.1/src/dfm_python/config/__init__.py +0 -35
  114. dfm_python-0.3.1/src/dfm_python/core/__init__.py +0 -139
  115. dfm_python-0.3.1/src/dfm_python/core/em.py +0 -1385
  116. dfm_python-0.3.1/src/dfm_python/core/estimation.py +0 -553
  117. dfm_python-0.3.1/src/dfm_python/core/helpers.py +0 -1338
  118. dfm_python-0.3.1/src/dfm_python/core/state_space.py +0 -993
  119. dfm_python-0.3.1/src/dfm_python/dataloader/__init__.py +0 -30
  120. dfm_python-0.3.1/src/dfm_python/dataloader/loader.py +0 -763
  121. dfm_python-0.3.1/src/dfm_python/dataloader/preprocess.py +0 -11
  122. dfm_python-0.3.1/src/dfm_python/models/__init__.py +0 -19
  123. dfm_python-0.3.1/src/dfm_python/models/ddfm.py +0 -978
  124. dfm_python-0.3.1/src/dfm_python/models/dfm.py +0 -213
  125. dfm_python-0.3.1/src/dfm_python/nowcasting/__init__.py +0 -13
  126. dfm_python-0.3.1/src/dfm_python/utils/__init__.py +0 -38
  127. dfm_python-0.3.1/src/dfm_python/utils/convert.py +0 -342
  128. dfm_python-0.3.1/src/dfm_python/utils/loss.py +0 -184
  129. dfm_python-0.3.1/src/test/test_api.py +0 -466
  130. dfm_python-0.3.1/src/test/test_core.py +0 -339
  131. dfm_python-0.3.1/src/test/test_ddfm.py +0 -645
  132. dfm_python-0.3.1/src/test/test_dfm_regression.py +0 -172
  133. dfm_python-0.3.1/src/test/test_integration.py +0 -183
  134. dfm_python-0.3.1/src/test/test_kalman.py +0 -205
  135. dfm_python-0.3.1/src/test/test_nowcasting.py +0 -1280
  136. dfm_python-0.3.1/tutorial/basic_tutorial.py +0 -1118
  137. {dfm_python-0.3.1 → dfm_python-0.4.0}/.gitignore +0 -0
  138. {dfm_python-0.3.1 → dfm_python-0.4.0}/.python-version +0 -0
  139. {dfm_python-0.3.1 → dfm_python-0.4.0}/LICENSE +0 -0
  140. {dfm_python-0.3.1 → dfm_python-0.4.0}/MANIFEST.in +0 -0
  141. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/blocks/default.yaml +0 -0
  142. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/blocks/sample_spec.yaml +0 -0
  143. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/blocks/sample_spec2.yaml +0 -0
  144. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/default.yaml +0 -0
  145. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/experiment/ddfm_example.yaml +0 -0
  146. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/experiment/exp1.yaml +0 -0
  147. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/experiment/exp2.yaml +0 -0
  148. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/experiment/exp3.yaml +0 -0
  149. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/series/default.yaml +0 -0
  150. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/series/sample_spec.yaml +0 -0
  151. {dfm_python-0.3.1 → dfm_python-0.4.0}/config/series/sample_spec2.yaml +0 -0
  152. {dfm_python-0.3.1 → dfm_python-0.4.0}/data/mdfred_snapshot_monthly.csv +0 -0
  153. {dfm_python-0.3.1 → dfm_python-0.4.0}/data/mdfred_snapshot_quarterly.csv +0 -0
  154. {dfm_python-0.3.1 → dfm_python-0.4.0}/data/sample_data.csv +0 -0
  155. {dfm_python-0.3.1 → dfm_python-0.4.0}/data/sample_spec.csv +0 -0
  156. {dfm_python-0.3.1 → dfm_python-0.4.0}/full_tutorial_example.ipynb +0 -0
  157. {dfm_python-0.3.1 → dfm_python-0.4.0}/src/dfm_python/config/io.py +0 -0
dfm_python-0.4.0/.git ADDED
@@ -0,0 +1 @@
1
+ gitdir: ../.git/modules/dfm-python
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.3.1
3
+ Version: 0.4.0
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
@@ -24,24 +24,26 @@ Requires-Dist: matplotlib>=3.5.0
24
24
  Requires-Dist: numpy>=1.24.0
25
25
  Requires-Dist: polars>=0.20.0
26
26
  Requires-Dist: scipy>=1.10.0
27
+ Requires-Dist: torch>=2.0.0
27
28
  Requires-Dist: twine>=6.2.0
28
29
  Provides-Extra: all
29
30
  Requires-Dist: hydra-core>=1.3.0; extra == 'all'
30
31
  Requires-Dist: omegaconf>=2.3.0; extra == 'all'
31
32
  Requires-Dist: pytest>=7.0.0; extra == 'all'
32
33
  Requires-Dist: scikit-learn>=1.0.0; extra == 'all'
34
+ Requires-Dist: sktime>=0.27.0; extra == 'all'
33
35
  Requires-Dist: sqlalchemy>=2.0.0; extra == 'all'
34
- Requires-Dist: torch>=2.0.0; extra == 'all'
35
36
  Provides-Extra: db
36
37
  Requires-Dist: sqlalchemy>=2.0.0; extra == 'db'
37
38
  Provides-Extra: deep
38
- Requires-Dist: torch>=2.0.0; extra == 'deep'
39
39
  Provides-Extra: dev
40
40
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
41
41
  Requires-Dist: scikit-learn>=1.0.0; extra == 'dev'
42
42
  Provides-Extra: hydra
43
43
  Requires-Dist: hydra-core>=1.3.0; extra == 'hydra'
44
44
  Requires-Dist: omegaconf>=2.3.0; extra == 'hydra'
45
+ Provides-Extra: transform
46
+ Requires-Dist: sktime>=0.27.0; extra == 'transform'
45
47
  Description-Content-Type: text/markdown
46
48
 
47
49
  # dfm-python: Dynamic Factor Models for Python
@@ -735,12 +737,12 @@ SQLite data view storage is planned for future releases. Currently, `BasicDataVi
735
737
  # X_view, Time_view, config = adapter.load_data_view(view_id)
736
738
  ```
737
739
 
738
- ### News Decomposition (Legacy API)
740
+ ### Advanced News Decomposition
739
741
 
740
- For advanced users, the low-level `news_dfm` function is still available:
742
+ For advanced users, low-level functions are available in the `nowcasting` module:
741
743
 
742
744
  ```python
743
- from dfm_python.nowcast import para_const
745
+ from dfm_python.nowcasting import para_const
744
746
 
745
747
  # Before new data release
746
748
  result_old = model.fit(X_old, config)
@@ -752,7 +754,7 @@ X_new = ... # Updated data
752
754
  Res_old = para_const(X_old, result_old, lag=1)
753
755
  # ... (see nowcast.py for full implementation)
754
756
 
755
- # Recommended: Use Nowcast class instead (see Nowcasting API section above)
757
+ # Recommended: Use Nowcast class for most use cases (see Nowcasting API section above)
756
758
  ```
757
759
 
758
760
  ### Custom Block Structure
@@ -1208,7 +1210,7 @@ DATA_VIEW_SOURCE=file # or database
1208
1210
 
1209
1211
  ## Project Status
1210
1212
 
1211
- **Version**: 0.3.0
1213
+ **Version**: 0.3.1
1212
1214
  **Status**: Stable and production-ready
1213
1215
  **Python**: 3.10+
1214
1216
  **PyPI**: https://pypi.org/project/dfm-python/
@@ -1297,7 +1299,7 @@ If you use `dfm-python` in your research, please cite:
1297
1299
  author = {DFM Python Contributors},
1298
1300
  year = {2025},
1299
1301
  url = {https://pypi.org/project/dfm-python/},
1300
- version = {0.2.7}
1302
+ version = {0.3.1}
1301
1303
  }
1302
1304
  ```
1303
1305
 
@@ -689,12 +689,12 @@ SQLite data view storage is planned for future releases. Currently, `BasicDataVi
689
689
  # X_view, Time_view, config = adapter.load_data_view(view_id)
690
690
  ```
691
691
 
692
- ### News Decomposition (Legacy API)
692
+ ### Advanced News Decomposition
693
693
 
694
- For advanced users, the low-level `news_dfm` function is still available:
694
+ For advanced users, low-level functions are available in the `nowcasting` module:
695
695
 
696
696
  ```python
697
- from dfm_python.nowcast import para_const
697
+ from dfm_python.nowcasting import para_const
698
698
 
699
699
  # Before new data release
700
700
  result_old = model.fit(X_old, config)
@@ -706,7 +706,7 @@ X_new = ... # Updated data
706
706
  Res_old = para_const(X_old, result_old, lag=1)
707
707
  # ... (see nowcast.py for full implementation)
708
708
 
709
- # Recommended: Use Nowcast class instead (see Nowcasting API section above)
709
+ # Recommended: Use Nowcast class for most use cases (see Nowcasting API section above)
710
710
  ```
711
711
 
712
712
  ### Custom Block Structure
@@ -1162,7 +1162,7 @@ DATA_VIEW_SOURCE=file # or database
1162
1162
 
1163
1163
  ## Project Status
1164
1164
 
1165
- **Version**: 0.3.0
1165
+ **Version**: 0.3.1
1166
1166
  **Status**: Stable and production-ready
1167
1167
  **Python**: 3.10+
1168
1168
  **PyPI**: https://pypi.org/project/dfm-python/
@@ -1251,7 +1251,7 @@ If you use `dfm-python` in your research, please cite:
1251
1251
  author = {DFM Python Contributors},
1252
1252
  year = {2025},
1253
1253
  url = {https://pypi.org/project/dfm-python/},
1254
- version = {0.2.7}
1254
+ version = {0.3.1}
1255
1255
  }
1256
1256
  ```
1257
1257
 
@@ -0,0 +1,562 @@
1
+ # sktime (Polars 지원) Transformations 통합 계획
2
+
3
+ ## 목표
4
+
5
+ sktime의 `ColumnTransformer` (Polars 지원)를 활용하여 dfm-python의 transformation과 standardization을 통합하고, polars DataFrame을 직접 사용할 수 있도록 구조 개선.
6
+
7
+ ## 현재 구조 분석
8
+
9
+ ### 현재 전처리 위치
10
+ 1. **`dataloader/loader.py`**: `transform_data()` - transformation만 적용 (config 기반)
11
+ 2. **`core/estimation.py`**: `standardize_data()` - standardization만 적용
12
+ 3. **`models/dfm.py`**: `DFMLinear.fit()` - standardization 적용
13
+ 4. **`models/ddfm.py`**: `DDFM.fit()` - standardization 적용
14
+
15
+ ### 문제점
16
+ - Transformation과 standardization이 분리되어 있음
17
+ - `DFMLinear.fit()`에서는 transformation이 적용되지 않음 (config의 transformation 무시)
18
+ - Polars DataFrame → numpy array 변환 필요
19
+ - 중복 코드 (여러 곳에서 standardization)
20
+
21
+ ## 제안 구조
22
+
23
+ ### 1. 새로운 모듈 구조
24
+
25
+ ```
26
+ dfm-python/src/dfm_python/
27
+ ├── transformations/
28
+ │ ├── __init__.py # Public API export
29
+ │ ├── scaler.py # DFMScaler 클래스 (핵심)
30
+ │ ├── transformers.py # Custom transformation 함수들
31
+ │ └── sktime_wrappers.py # sktime optional dependency 처리
32
+ ```
33
+
34
+ ### 2. DFMScaler 클래스 설계
35
+
36
+ ```python
37
+ # transformations/scaler.py
38
+
39
+ class DFMScaler:
40
+ """DFM Scaler using sktime transformers with Polars support.
41
+
42
+ This class handles:
43
+ 1. Series-specific transformations (lin, log, chg, ch1, pch, pc1, pca, cch, cca)
44
+ 2. Frequency-aware transformations (based on series frequency)
45
+ 3. Global standardization (mean=0, std=1 for entire dataset)
46
+
47
+ Uses sktime's ColumnTransformer with Polars support for efficient processing.
48
+ """
49
+
50
+ def __init__(self, config: DFMConfig):
51
+ """Initialize scaler from DFMConfig.
52
+
53
+ Parameters
54
+ ----------
55
+ config : DFMConfig
56
+ DFM configuration containing series transformations and frequencies
57
+ """
58
+ pass
59
+
60
+ def fit(self, X: pl.DataFrame, y=None) -> 'DFMScaler':
61
+ """Fit the scaler to data.
62
+
63
+ Parameters
64
+ ----------
65
+ X : pl.DataFrame
66
+ Input data (T x N), where T is time periods and N is number of series.
67
+ Column names should match series_ids from config.
68
+ y : optional
69
+ Not used, present for sklearn compatibility
70
+
71
+ Returns
72
+ -------
73
+ self : DFMScaler
74
+ Fitted scaler instance
75
+ """
76
+ pass
77
+
78
+ def transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
79
+ """Transform data using fitted scaler.
80
+
81
+ Parameters
82
+ ----------
83
+ X : pl.DataFrame
84
+ Input data (T x N)
85
+
86
+ Returns
87
+ -------
88
+ X_transformed : pl.DataFrame
89
+ Transformed and standardized data
90
+ """
91
+ pass
92
+
93
+ def fit_transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
94
+ """Fit and transform in one step."""
95
+ pass
96
+
97
+ def inverse_transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
98
+ """Inverse transform (for forecasting/backtesting).
99
+
100
+ Note: Some transformations (pch, pc1, etc.) are not fully invertible
101
+ due to division by zero handling. Returns best-effort reconstruction.
102
+ """
103
+ pass
104
+
105
+ @property
106
+ def Mx(self) -> np.ndarray:
107
+ """Mean values used for standardization (N,)"""
108
+ pass
109
+
110
+ @property
111
+ def Wx(self) -> np.ndarray:
112
+ """Standard deviation values used for standardization (N,)"""
113
+ pass
114
+ ```
115
+
116
+ ### 3. Optional Dependency 처리
117
+
118
+ ```python
119
+ # transformations/sktime_wrappers.py
120
+
121
+ try:
122
+ from sktime.transformations.compose import (
123
+ ColumnTransformer,
124
+ TransformerPipeline
125
+ )
126
+ from sktime.transformations.series.log import LogTransformer
127
+ from sktime.transformations.series.difference import Differencer
128
+ from sktime.transformations.series.func_transform import FunctionTransformer
129
+ from sklearn.preprocessing import StandardScaler
130
+ HAS_SKTIME = True
131
+ except ImportError:
132
+ HAS_SKTIME = False
133
+ ColumnTransformer = None
134
+ TransformerPipeline = None
135
+ LogTransformer = None
136
+ Differencer = None
137
+ FunctionTransformer = None
138
+ StandardScaler = None
139
+
140
+ def check_sktime_available():
141
+ """Check if sktime is available."""
142
+ if not HAS_SKTIME:
143
+ raise ImportError(
144
+ "sktime is required for DFMScaler. "
145
+ "Install it with: pip install dfm-python[transform]"
146
+ )
147
+ ```
148
+
149
+ ### 4. 통합 전략
150
+
151
+ #### 전략 A: DFMScaler를 필수로 사용 (권장)
152
+
153
+ **장점**:
154
+ - 일관된 transformation/standardization 처리
155
+ - Polars 직접 지원
156
+ - 코드 중복 제거
157
+
158
+ **구현**:
159
+ 1. `DFMLinear.fit()`에서 `DFMScaler` 사용
160
+ 2. `core/estimation.py`의 `standardize_data()` 제거 (또는 deprecated)
161
+ 3. `dataloader/loader.py`의 `transform_data()` 제거 (또는 deprecated)
162
+
163
+ **단점**:
164
+ - sktime이 optional dependency이므로, 없으면 fallback 필요
165
+
166
+ #### 전략 B: DFMScaler를 선택적으로 사용
167
+
168
+ **장점**:
169
+ - Backward compatibility 유지
170
+ - sktime 없이도 동작 가능
171
+
172
+ **구현**:
173
+ 1. `DFMLinear.fit()`에서 `use_sktime` 플래그로 선택
174
+ 2. sktime 없으면 기존 방식 사용
175
+
176
+ **단점**:
177
+ - 두 가지 코드 경로 유지 필요
178
+ - 복잡도 증가
179
+
180
+ ## 권장 구현: 전략 A (DFMScaler 필수)
181
+
182
+ ### 단계별 구현
183
+
184
+ #### Step 1: transformations 모듈 생성
185
+
186
+ ```python
187
+ # transformations/__init__.py
188
+ from .scaler import DFMScaler
189
+ from .transformers import (
190
+ make_pch_transformer,
191
+ make_pc1_transformer,
192
+ make_pca_transformer,
193
+ make_cch_transformer,
194
+ make_cca_transformer,
195
+ )
196
+
197
+ __all__ = [
198
+ 'DFMScaler',
199
+ 'make_pch_transformer',
200
+ 'make_pc1_transformer',
201
+ 'make_pca_transformer',
202
+ 'make_cch_transformer',
203
+ 'make_cca_transformer',
204
+ ]
205
+ ```
206
+
207
+ #### Step 2: DFMScaler 구현
208
+
209
+ ```python
210
+ # transformations/scaler.py
211
+
212
+ import numpy as np
213
+ import polars as pl
214
+ from typing import Optional
215
+ from ..config import DFMConfig
216
+ from .sktime_wrappers import (
217
+ check_sktime_available,
218
+ ColumnTransformer,
219
+ TransformerPipeline,
220
+ LogTransformer,
221
+ Differencer,
222
+ FunctionTransformer,
223
+ StandardScaler,
224
+ )
225
+ from .transformers import (
226
+ make_pch_transformer,
227
+ make_pc1_transformer,
228
+ make_pca_transformer,
229
+ make_cch_transformer,
230
+ make_cca_transformer,
231
+ log_transform,
232
+ )
233
+
234
+ # Frequency → lag 매핑
235
+ FREQ_TO_LAG_YOY = {'m': 12, 'q': 4, 'sa': 2, 'a': 1}
236
+ FREQ_TO_LAG_STEP = {'m': 1, 'q': 3, 'sa': 6, 'a': 12}
237
+
238
+ def get_periods_per_year(freq: str) -> int:
239
+ """Get number of periods per year for a frequency"""
240
+ return FREQ_TO_LAG_YOY.get(freq, 12)
241
+
242
+ def get_annual_factor(freq: str, step: int) -> float:
243
+ """Get annualization factor for a frequency and step"""
244
+ periods_per_year = get_periods_per_year(freq)
245
+ return periods_per_year / step
246
+
247
+ class DFMScaler:
248
+ """DFM Scaler using sktime transformers with Polars support."""
249
+
250
+ def __init__(self, config: DFMConfig):
251
+ check_sktime_available()
252
+
253
+ self.config = config
254
+ self.transformations = [s.transformation for s in config.series]
255
+ self.frequencies = [s.frequency for s in config.series]
256
+ self.series_ids = config.get_series_ids()
257
+
258
+ # 각 series별로 TransformerPipeline 구성
259
+ transformers = []
260
+ for i, (trans, freq) in enumerate(zip(self.transformations, self.frequencies)):
261
+ trafo_pipeline = self._create_transformation_pipeline(trans, freq)
262
+ transformers.append((f'series_{i}', trafo_pipeline, i))
263
+
264
+ # ColumnTransformer 구성 (Polars 지원!)
265
+ self.column_transformer = ColumnTransformer(transformers)
266
+
267
+ # 전체 pipeline: ColumnTransformer → StandardScaler
268
+ self.pipeline = TransformerPipeline([
269
+ ("transform", self.column_transformer),
270
+ ("scaler", StandardScaler())
271
+ ])
272
+
273
+ # Polars 출력 설정
274
+ self.pipeline.set_output(transform="polars")
275
+
276
+ # Standardization 파라미터 (fit 후 설정됨)
277
+ self._Mx: Optional[np.ndarray] = None
278
+ self._Wx: Optional[np.ndarray] = None
279
+
280
+ def _create_transformation_pipeline(self, trans: str, freq: str) -> TransformerPipeline:
281
+ """Create transformation pipeline for a single series."""
282
+ if trans == 'lin':
283
+ return TransformerPipeline([
284
+ FunctionTransformer(func=lambda x: x)
285
+ ])
286
+ elif trans == 'log':
287
+ return TransformerPipeline([
288
+ FunctionTransformer(func=log_transform)
289
+ ])
290
+ elif trans == 'chg':
291
+ lag = FREQ_TO_LAG_STEP.get(freq, 1)
292
+ return TransformerPipeline([
293
+ Differencer(lags=[lag])
294
+ ])
295
+ elif trans == 'ch1':
296
+ lag = FREQ_TO_LAG_YOY.get(freq, 1)
297
+ return TransformerPipeline([
298
+ Differencer(lags=[lag])
299
+ ])
300
+ elif trans == 'pch':
301
+ step = FREQ_TO_LAG_STEP.get(freq, 1)
302
+ return TransformerPipeline([
303
+ make_pch_transformer(step)
304
+ ])
305
+ elif trans == 'pc1':
306
+ year_step = FREQ_TO_LAG_YOY.get(freq, 12)
307
+ return TransformerPipeline([
308
+ make_pc1_transformer(year_step)
309
+ ])
310
+ elif trans == 'pca':
311
+ step = FREQ_TO_LAG_STEP.get(freq, 1)
312
+ annual_factor = get_annual_factor(freq, step)
313
+ return TransformerPipeline([
314
+ make_pca_transformer(step, annual_factor)
315
+ ])
316
+ elif trans == 'cch':
317
+ step = FREQ_TO_LAG_STEP.get(freq, 1)
318
+ return TransformerPipeline([
319
+ make_cch_transformer(step)
320
+ ])
321
+ elif trans == 'cca':
322
+ step = FREQ_TO_LAG_STEP.get(freq, 1)
323
+ annual_factor = get_annual_factor(freq, step)
324
+ return TransformerPipeline([
325
+ make_cca_transformer(step, annual_factor)
326
+ ])
327
+ else:
328
+ # Default: Identity
329
+ return TransformerPipeline([
330
+ FunctionTransformer(func=lambda x: x)
331
+ ])
332
+
333
+ def fit(self, X: pl.DataFrame, y=None) -> 'DFMScaler':
334
+ """Fit the scaler to data."""
335
+ # Ensure column order matches series_ids
336
+ if list(X.columns) != self.series_ids:
337
+ X = X.select(self.series_ids)
338
+
339
+ # Fit and transform
340
+ X_transformed = self.pipeline.fit_transform(X, y)
341
+
342
+ # Extract StandardScaler parameters
343
+ self.scaler = self.pipeline.steps[1][1] # StandardScaler
344
+ self._Mx = self.scaler.mean_
345
+ self._Wx = self.scaler.scale_
346
+
347
+ return self
348
+
349
+ def transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
350
+ """Transform data using fitted scaler."""
351
+ # Ensure column order matches series_ids
352
+ if list(X.columns) != self.series_ids:
353
+ X = X.select(self.series_ids)
354
+
355
+ return self.pipeline.transform(X, y)
356
+
357
+ def fit_transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
358
+ """Fit and transform in one step."""
359
+ return self.fit(X, y).transform(X, y)
360
+
361
+ def inverse_transform(self, X: pl.DataFrame, y=None) -> pl.DataFrame:
362
+ """Inverse transform (for forecasting/backtesting)."""
363
+ # Note: Some transformations are not fully invertible
364
+ # This is a best-effort implementation
365
+ return self.pipeline.inverse_transform(X, y)
366
+
367
+ @property
368
+ def Mx(self) -> np.ndarray:
369
+ """Mean values used for standardization (N,)"""
370
+ if self._Mx is None:
371
+ raise ValueError("Scaler has not been fitted yet.")
372
+ return self._Mx
373
+
374
+ @property
375
+ def Wx(self) -> np.ndarray:
376
+ """Standard deviation values used for standardization (N,)"""
377
+ if self._Wx is None:
378
+ raise ValueError("Scaler has not been fitted yet.")
379
+ return self._Wx
380
+ ```
381
+
382
+ #### Step 3: DFMLinear.fit() 수정
383
+
384
+ ```python
385
+ # models/dfm.py
386
+
387
+ from ..transformations import DFMScaler
388
+
389
+ class DFMLinear(BaseFactorModel):
390
+ def fit(self, X: Union[np.ndarray, pl.DataFrame], config: Optional[DFMConfig] = None, ...):
391
+ # ... (기존 초기화 코드) ...
392
+
393
+ # Convert to polars DataFrame if needed
394
+ if isinstance(X, np.ndarray):
395
+ X = pl.DataFrame(X, schema=series_ids)
396
+ elif not isinstance(X, pl.DataFrame):
397
+ raise TypeError(f"X must be np.ndarray or pl.DataFrame, got {type(X)}")
398
+
399
+ # Create and fit scaler
400
+ scaler = DFMScaler(config)
401
+ X_transformed = scaler.fit_transform(X)
402
+
403
+ # Convert to numpy for estimation (필요한 경우)
404
+ X_numpy = X_transformed.to_numpy()
405
+
406
+ # Extract standardization parameters
407
+ Mx = scaler.Mx
408
+ Wx = scaler.Wx
409
+
410
+ # ... (기존 estimation 코드) ...
411
+
412
+ # 결과에 scaler 저장 (inverse_transform용)
413
+ result.scaler = scaler
414
+ result.Mx = Mx
415
+ result.Wx = Wx
416
+
417
+ return result
418
+ ```
419
+
420
+ #### Step 4: core/estimation.py 수정
421
+
422
+ ```python
423
+ # core/estimation.py
424
+
425
+ def _dfm_core(X: np.ndarray, config: DFMConfig, ...):
426
+ """DFM estimation core function.
427
+
428
+ Note: This function now expects pre-transformed and standardized data.
429
+ Use DFMScaler before calling this function.
430
+ """
431
+ # ... (기존 코드, standardization 제거) ...
432
+
433
+ # X는 이미 transformed + standardized 되어 있다고 가정
434
+ # standardization 코드 제거
435
+ ```
436
+
437
+ ### 5. Backward Compatibility
438
+
439
+ #### Option 1: Deprecation Warnings
440
+
441
+ ```python
442
+ # dataloader/loader.py
443
+
444
+ def transform_data(...):
445
+ """Transform data using config transformations.
446
+
447
+ .. deprecated:: 0.2.0
448
+ Use DFMScaler instead. This function will be removed in 0.3.0.
449
+ """
450
+ warnings.warn(
451
+ "transform_data() is deprecated. Use DFMScaler instead.",
452
+ DeprecationWarning,
453
+ stacklevel=2
454
+ )
455
+ # ... (기존 코드) ...
456
+ ```
457
+
458
+ #### Option 2: Fallback 구현
459
+
460
+ ```python
461
+ # transformations/scaler.py
462
+
463
+ class DFMScaler:
464
+ def __init__(self, config: DFMConfig, use_sktime: bool = True):
465
+ if use_sktime:
466
+ try:
467
+ check_sktime_available()
468
+ self._use_sktime = True
469
+ except ImportError:
470
+ if use_sktime:
471
+ raise
472
+ self._use_sktime = False
473
+ else:
474
+ self._use_sktime = False
475
+
476
+ if not self._use_sktime:
477
+ # Fallback to native implementation
478
+ self._init_native(config)
479
+ else:
480
+ self._init_sktime(config)
481
+ ```
482
+
483
+ ## 마이그레이션 가이드
484
+
485
+ ### 사용자 관점
486
+
487
+ #### Before (현재)
488
+ ```python
489
+ from dfm_python import DFM
490
+ from dfm_python.dataloader import load_data
491
+
492
+ model = DFM()
493
+ X, Time, Z = load_data('data.csv', config)
494
+ # load_data 내부에서 transformation 적용
495
+ result = model.fit(X, config)
496
+ # fit 내부에서 standardization 적용
497
+ ```
498
+
499
+ #### After (새로운 구조)
500
+ ```python
501
+ from dfm_python import DFM
502
+ from dfm_python.transformations import DFMScaler
503
+ import polars as pl
504
+
505
+ model = DFM()
506
+ X = pl.read_csv('data.csv') # Polars 직접 사용
507
+ scaler = DFMScaler(config)
508
+ X_transformed = scaler.fit_transform(X)
509
+ result = model.fit(X_transformed, config)
510
+ # standardization은 이미 scaler에서 적용됨
511
+ ```
512
+
513
+ ## 테스트 전략
514
+
515
+ 1. **Unit Tests**: DFMScaler의 각 transformation 테스트
516
+ 2. **Integration Tests**: DFMLinear.fit()과의 통합 테스트
517
+ 3. **Backward Compatibility Tests**: 기존 코드와의 호환성 테스트
518
+ 4. **Performance Tests**: Polars vs numpy 변환 성능 비교
519
+
520
+ ## 의존성 관리
521
+
522
+ ### pyproject.toml
523
+
524
+ ```toml
525
+ [project.optional-dependencies]
526
+ transform = [
527
+ "sktime>=0.27.0",
528
+ ]
529
+ all = [
530
+ "dfm-python[hydra,db,dev,deep,transform]",
531
+ ]
532
+ ```
533
+
534
+ ### 설치 방법
535
+
536
+ ```bash
537
+ # sktime 포함 설치
538
+ pip install dfm-python[transform]
539
+
540
+ # 또는 전체 설치
541
+ pip install dfm-python[all]
542
+ ```
543
+
544
+ ## 장점 요약
545
+
546
+ 1. **Polars 직접 지원**: pandas 변환 불필요
547
+ 2. **일관된 처리**: transformation + standardization 통합
548
+ 3. **코드 중복 제거**: 여러 곳에서 standardization 하지 않음
549
+ 4. **확장성**: 새로운 transformation 쉽게 추가 가능
550
+ 5. **표준 인터페이스**: sklearn/sktime 호환
551
+ 6. **성능**: Polars의 빠른 처리 속도 활용
552
+
553
+ ## 다음 단계
554
+
555
+ 1. ✅ 구조 설계 (현재)
556
+ 2. ⏳ transformations 모듈 구현
557
+ 3. ⏳ DFMScaler 구현
558
+ 4. ⏳ DFMLinear.fit() 통합
559
+ 5. ⏳ 테스트 작성
560
+ 6. ⏳ 문서화
561
+ 7. ⏳ 마이그레이션 가이드 작성
562
+