skfolio 0.1.2__tar.gz → 0.2.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 (87) hide show
  1. {skfolio-0.1.2/src/skfolio.egg-info → skfolio-0.2.0}/PKG-INFO +20 -21
  2. {skfolio-0.1.2 → skfolio-0.2.0}/README.rst +19 -19
  3. {skfolio-0.1.2 → skfolio-0.2.0}/pyproject.toml +10 -13
  4. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/_base.py +4 -1
  5. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/distance/_distance.py +0 -1
  6. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/measures/_enums.py +3 -1
  7. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/measures/_measures.py +0 -1
  8. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/model_selection/__init__.py +2 -0
  9. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/model_selection/_combinatorial.py +172 -21
  10. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/model_selection/_validation.py +8 -6
  11. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/_base.py +19 -12
  12. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/hierarchical/_base.py +5 -4
  13. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/hierarchical/_herc.py +2 -2
  14. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/hierarchical/_hrp.py +2 -3
  15. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/_base.py +5 -4
  16. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/_distributionally_robust.py +2 -2
  17. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/_maximum_diversification.py +2 -2
  18. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/_mean_risk.py +2 -2
  19. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/_risk_budgeting.py +2 -2
  20. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/ensemble/_base.py +1 -3
  21. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/ensemble/_stacking.py +6 -4
  22. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/naive/_naive.py +6 -6
  23. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/population/_population.py +48 -39
  24. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/portfolio/_base.py +0 -1
  25. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/portfolio/_portfolio.py +13 -12
  26. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/prior/_factor_model.py +8 -5
  27. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/fixes/_dendrogram.py +9 -7
  28. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/tools.py +8 -7
  29. {skfolio-0.1.2 → skfolio-0.2.0/src/skfolio.egg-info}/PKG-INFO +20 -21
  30. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio.egg-info/requires.txt +0 -1
  31. {skfolio-0.1.2 → skfolio-0.2.0}/LICENSE +0 -0
  32. {skfolio-0.1.2 → skfolio-0.2.0}/MANIFEST.in +0 -0
  33. {skfolio-0.1.2 → skfolio-0.2.0}/setup.cfg +0 -0
  34. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/__init__.py +0 -0
  35. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/cluster/__init__.py +0 -0
  36. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/cluster/_hierarchical.py +0 -0
  37. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/__init__.py +0 -0
  38. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/data/__init__.py +0 -0
  39. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  40. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  41. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/datasets/data/sp500_index.csv.gz +0 -0
  42. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/distance/__init__.py +0 -0
  43. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/distance/_base.py +0 -0
  44. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/exceptions.py +0 -0
  45. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/measures/__init__.py +0 -0
  46. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/metrics/__init__.py +0 -0
  47. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/metrics/_scorer.py +0 -0
  48. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/model_selection/_walk_forward.py +0 -0
  49. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/__init__.py +0 -0
  50. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/covariance/__init__.py +0 -0
  51. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/covariance/_base.py +0 -0
  52. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/covariance/_covariance.py +0 -0
  53. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/expected_returns/__init__.py +0 -0
  54. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/expected_returns/_base.py +0 -0
  55. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/moments/expected_returns/_expected_returns.py +0 -0
  56. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/__init__.py +0 -0
  57. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/__init__.py +0 -0
  58. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/_nco.py +0 -0
  59. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/cluster/hierarchical/__init__.py +0 -0
  60. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/convex/__init__.py +0 -0
  61. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/ensemble/__init__.py +0 -0
  62. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/optimization/naive/__init__.py +0 -0
  63. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/population/__init__.py +0 -0
  64. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/portfolio/__init__.py +0 -0
  65. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/portfolio/_multi_period_portfolio.py +0 -0
  66. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/pre_selection/__init__.py +0 -0
  67. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/pre_selection/_pre_selection.py +0 -0
  68. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/preprocessing/__init__.py +0 -0
  69. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/preprocessing/_returns.py +0 -0
  70. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/prior/__init__.py +0 -0
  71. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/prior/_base.py +0 -0
  72. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/prior/_black_litterman.py +0 -0
  73. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/prior/_empirical.py +0 -0
  74. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/typing.py +0 -0
  75. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/uncertainty_set/__init__.py +0 -0
  76. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/uncertainty_set/_base.py +0 -0
  77. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/uncertainty_set/_bootstrap.py +0 -0
  78. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/uncertainty_set/_empirical.py +0 -0
  79. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/__init__.py +0 -0
  80. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/bootstrap.py +0 -0
  81. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/equations.py +0 -0
  82. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/fixes/__init__.py +0 -0
  83. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/sorting.py +0 -0
  84. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio/utils/stats.py +0 -0
  85. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio.egg-info/SOURCES.txt +0 -0
  86. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio.egg-info/dependency_links.txt +0 -0
  87. {skfolio-0.1.2 → skfolio-0.2.0}/src/skfolio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skfolio
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Portfolio optimization built on top of scikit-learn
5
5
  Author-email: Hugo Delatte <delatte.hugo@gmail.com>
6
6
  Maintainer-email: Hugo Delatte <delatte.hugo@gmail.com>
@@ -66,7 +66,6 @@ Requires-Dist: plotly>=5.15.0
66
66
  Provides-Extra: tests
67
67
  Requires-Dist: pytest; extra == "tests"
68
68
  Requires-Dist: pytest-cov; extra == "tests"
69
- Requires-Dist: black; extra == "tests"
70
69
  Requires-Dist: ruff; extra == "tests"
71
70
  Provides-Extra: docs
72
71
  Requires-Dist: Sphinx; extra == "docs"
@@ -86,37 +85,37 @@ Requires-Dist: sphinx-favicon; extra == "docs"
86
85
 
87
86
  .. -*- mode: rst -*-
88
87
 
89
- |Licence|_ |Codecov|_ |Black|_ |PythonVersion|_ |PyPi|_ |CI/CD|_ |Downloads|_ |Ruff|_ |Contribution|_ |Website|_
88
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website|
90
89
 
91
90
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
92
- .. _Licence: https://github.com/skfolio/skfolio/blob/main/LICENSE
91
+ :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
93
92
 
94
93
  .. |Codecov| image:: https://codecov.io/gh/skfolio/skfolio/graph/badge.svg?token=KJ0SE4LHPV
95
- .. _Codecov: https://codecov.io/gh/skfolio/skfolio
94
+ :target: https://codecov.io/gh/skfolio/skfolio
96
95
 
97
- .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue
98
- .. _PythonVersion: https://pypi.org/project/skfolio/
96
+ .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg
97
+ :target: https://pypi.org/project/skfolio/
99
98
 
100
99
  .. |PyPi| image:: https://img.shields.io/pypi/v/skfolio
101
- .. _PyPi: https://pypi.org/project/skfolio
100
+ :target: https://pypi.org/project/skfolio
102
101
 
103
102
  .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
104
- .. _Black: https://github.com/psf/black
103
+ :target: https://github.com/psf/black
105
104
 
106
- .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml?logo=github
107
- .. _CI/CD: https://github.com/skfolio/skfolio/raw/main/LICENSE
105
+ .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml.svg?logo=github
106
+ :target: https://github.com/skfolio/skfolio/raw/main/LICENSE
108
107
 
109
108
  .. |Downloads| image:: https://static.pepy.tech/badge/skfolio
110
- .. _Downloads: https://pepy.tech/project/skfolio
109
+ :target: https://pepy.tech/project/skfolio
111
110
 
112
111
  .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
113
- .. _Ruff: https://github.com/astral-sh/ruff
112
+ :target: https://github.com/astral-sh/ruff
114
113
 
115
114
  .. |Contribution| image:: https://img.shields.io/badge/Contributions-Welcome-blue
116
- .. _Contribution: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
115
+ :target: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
117
116
 
118
- .. |Website| image:: https://img.shields.io/website-up-down-53cc0d-red/http/skfolio.org
119
- .. _Website: https://skfolio.org
117
+ .. |Website| image:: https://img.shields.io/website.svg?down_color=red&down_message=down&up_color=53cc0d&up_message=up&url=https://skfolio.org
118
+ :target: https://skfolio.org
120
119
 
121
120
  .. |PythonMinVersion| replace:: 3.10
122
121
  .. |NumpyMinVersion| replace:: 1.23.4
@@ -646,9 +645,9 @@ If you use `skfolio` in a scientific publication, we would appreciate citations:
646
645
  Bibtex entry::
647
646
 
648
647
  @misc{skfolio,
649
- author = {Hugo Delatte, Carlo Nicolini},
650
- title = {skfolio},
651
- year = {2023},
652
- url = {https://github.com/skfolio/skfolio}
653
-
648
+ author = {Delatte, Hugo and Nicolini, Carlo},
649
+ title = {skfolio},
650
+ year = {2023},
651
+ url = {https://github.com/skfolio/skfolio}
652
+ }
654
653
 
@@ -1,36 +1,36 @@
1
1
  .. -*- mode: rst -*-
2
2
 
3
- |Licence|_ |Codecov|_ |Black|_ |PythonVersion|_ |PyPi|_ |CI/CD|_ |Downloads|_ |Ruff|_ |Contribution|_ |Website|_
3
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website|
4
4
 
5
5
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
6
- .. _Licence: https://github.com/skfolio/skfolio/blob/main/LICENSE
6
+ :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
7
7
 
8
8
  .. |Codecov| image:: https://codecov.io/gh/skfolio/skfolio/graph/badge.svg?token=KJ0SE4LHPV
9
- .. _Codecov: https://codecov.io/gh/skfolio/skfolio
9
+ :target: https://codecov.io/gh/skfolio/skfolio
10
10
 
11
- .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue
12
- .. _PythonVersion: https://pypi.org/project/skfolio/
11
+ .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg
12
+ :target: https://pypi.org/project/skfolio/
13
13
 
14
14
  .. |PyPi| image:: https://img.shields.io/pypi/v/skfolio
15
- .. _PyPi: https://pypi.org/project/skfolio
15
+ :target: https://pypi.org/project/skfolio
16
16
 
17
17
  .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
18
- .. _Black: https://github.com/psf/black
18
+ :target: https://github.com/psf/black
19
19
 
20
- .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml?logo=github
21
- .. _CI/CD: https://github.com/skfolio/skfolio/raw/main/LICENSE
20
+ .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml.svg?logo=github
21
+ :target: https://github.com/skfolio/skfolio/raw/main/LICENSE
22
22
 
23
23
  .. |Downloads| image:: https://static.pepy.tech/badge/skfolio
24
- .. _Downloads: https://pepy.tech/project/skfolio
24
+ :target: https://pepy.tech/project/skfolio
25
25
 
26
26
  .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
27
- .. _Ruff: https://github.com/astral-sh/ruff
27
+ :target: https://github.com/astral-sh/ruff
28
28
 
29
29
  .. |Contribution| image:: https://img.shields.io/badge/Contributions-Welcome-blue
30
- .. _Contribution: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
30
+ :target: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
31
31
 
32
- .. |Website| image:: https://img.shields.io/website-up-down-53cc0d-red/http/skfolio.org
33
- .. _Website: https://skfolio.org
32
+ .. |Website| image:: https://img.shields.io/website.svg?down_color=red&down_message=down&up_color=53cc0d&up_message=up&url=https://skfolio.org
33
+ :target: https://skfolio.org
34
34
 
35
35
  .. |PythonMinVersion| replace:: 3.10
36
36
  .. |NumpyMinVersion| replace:: 1.23.4
@@ -560,9 +560,9 @@ If you use `skfolio` in a scientific publication, we would appreciate citations:
560
560
  Bibtex entry::
561
561
 
562
562
  @misc{skfolio,
563
- author = {Hugo Delatte, Carlo Nicolini},
564
- title = {skfolio},
565
- year = {2023},
566
- url = {https://github.com/skfolio/skfolio}
567
-
563
+ author = {Delatte, Hugo and Nicolini, Carlo},
564
+ title = {skfolio},
565
+ year = {2023},
566
+ url = {https://github.com/skfolio/skfolio}
567
+ }
568
568
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skfolio"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  maintainers = [
9
9
  { name = "Hugo Delatte", email = "delatte.hugo@gmail.com" },
10
10
  ]
@@ -62,7 +62,6 @@ classifiers = [
62
62
  tests = [
63
63
  "pytest",
64
64
  "pytest-cov",
65
- "black",
66
65
  "ruff"
67
66
  ]
68
67
  docs = [
@@ -106,33 +105,31 @@ commit_message = "v{version} [skip ci]\n\nAutomatically generated by python-sema
106
105
  [tool.semantic_release.remote]
107
106
  token = { env = "GH_TOKEN" }
108
107
 
109
- [tool.black]
110
- line-length = 88
111
- target_version = ["py310", "py311"]
112
- preview = true
113
-
114
108
  [tool.ruff]
115
109
  include = ["pyproject.toml", "src/**/*.py"]
110
+ line-length = 88
111
+ src = ["src"]
112
+ target-version = "py310"
113
+
114
+ [tool.ruff.lint]
116
115
  select = [
117
116
  "E", # pycodestyle
118
117
  "F", # pyflakes
119
118
  "I", # isort
120
119
  "A", # prevent using keywords that clobber python builtins
121
120
  "B", # bugbear: security warnings
122
- "E", # pycodestyle
123
121
  "F", # pyflakes
124
122
  "ISC", # implicit string concatenation
125
123
  "UP", # alert you when better syntax is available in your python version
126
124
  "RUF", # the ruff developer's own rules
127
125
  ]
128
- ignore = ["E203", "E501"]
129
- # Same as Black.
130
- line-length = 88
131
- target-version = "py310"
126
+ ignore = ["E203", "ISC001", "ISC002", "E111", "E114", "E117"] # rules redundant with the formatter.
132
127
 
133
- [tool.ruff.isort]
128
+ [tool.ruff.lint.isort]
134
129
  case-sensitive = true
135
130
 
131
+ [tool.ruff.lint.pycodestyle]
132
+ max-line-length = 320
136
133
 
137
134
  [tool.pytest.ini_options]
138
135
  addopts = [
@@ -140,7 +140,10 @@ def download_dataset(
140
140
  DataFrame with each row representing one observation and each column
141
141
  representing the asset price of a given observation.
142
142
  """
143
- url = f"https://github.com/skfolio/skfolio/raw/main/datasets/{data_filename}.csv.gz"
143
+ url = (
144
+ f"https://github.com/skfolio/skfolio-datasets/raw/main/"
145
+ f"datasets/{data_filename}.csv.gz"
146
+ )
144
147
 
145
148
  data_home = get_data_home(data_home=data_home)
146
149
  filepath = os.path.join(data_home, f"{data_filename}.pkz")
@@ -4,7 +4,6 @@
4
4
  # Author: Hugo Delatte <delatte.hugo@gmail.com>
5
5
  # License: BSD 3 clause
6
6
 
7
-
8
7
  import numpy as np
9
8
  import numpy.typing as npt
10
9
  import pandas as pd
@@ -19,7 +19,9 @@ class BaseMeasure(AutoEnum):
19
19
  (
20
20
  word.capitalize()
21
21
  if len(word) > 3
22
- else word.upper() if len(word) != 2 else word.lower()
22
+ else word.upper()
23
+ if len(word) != 2
24
+ else word.lower()
23
25
  )
24
26
  for word in self.value.split("_")
25
27
  ]
@@ -6,7 +6,6 @@
6
6
  # Gini mean difference and OWA GMD weights features are derived
7
7
  # from Riskfolio-Lib, Copyright (c) 2020-2023, Dany Cajas, Licensed under BSD 3 clause.
8
8
 
9
-
10
9
  import numpy as np
11
10
  import scipy.optimize as sco
12
11
 
@@ -6,6 +6,7 @@
6
6
  from skfolio.model_selection._combinatorial import (
7
7
  BaseCombinatorialCV,
8
8
  CombinatorialPurgedCV,
9
+ optimal_folds_number,
9
10
  )
10
11
  from skfolio.model_selection._validation import cross_val_predict
11
12
  from skfolio.model_selection._walk_forward import WalkForward
@@ -15,4 +16,5 @@ __all__ = [
15
16
  "WalkForward",
16
17
  "BaseCombinatorialCV",
17
18
  "CombinatorialPurgedCV",
19
+ "optimal_folds_number",
18
20
  ]
@@ -197,19 +197,13 @@ class CombinatorialPurgedCV(BaseCombinatorialCV):
197
197
  @property
198
198
  def n_splits(self) -> int:
199
199
  """Number of splits"""
200
- return int(
201
- math.factorial(self.n_folds)
202
- / (
203
- math.factorial(self.n_test_folds)
204
- * math.factorial(self.n_folds - self.n_test_folds)
205
- )
206
- )
200
+ return _n_splits(n_folds=self.n_folds, n_test_folds=self.n_test_folds)
207
201
 
208
202
  @property
209
203
  def n_test_paths(self) -> int:
210
204
  """Number of test paths that can be reconstructed from the train/test
211
205
  combinations"""
212
- return self.n_splits * self.n_test_folds // self.n_folds
206
+ return _n_test_paths(n_folds=self.n_folds, n_test_folds=self.n_test_folds)
213
207
 
214
208
  @property
215
209
  def test_set_index(self) -> np.ndarray:
@@ -320,19 +314,24 @@ class CombinatorialPurgedCV(BaseCombinatorialCV):
320
314
  yield train_index, test_index_list
321
315
 
322
316
  def summary(self, X) -> pd.Series:
323
- n_samples = X.shape[0]
324
- return pd.Series({
325
- "Number of Observations": n_samples,
326
- "Total Number of Folds": self.n_folds,
327
- "Number of Test Folds": self.n_test_folds,
328
- "Purge Size": self.purged_size,
329
- "Embargo Size": self.embargo_size,
330
- "Average Training Size": int(
331
- n_samples / self.n_folds * (self.n_folds - self.n_test_folds)
332
- ),
333
- "Number of Test Paths": self.n_test_paths,
334
- "Number of Training Combinations": self.n_splits,
335
- })
317
+ n_observations = X.shape[0]
318
+ avg_train_size = _avg_train_size(
319
+ n_observations=n_observations,
320
+ n_folds=self.n_folds,
321
+ n_test_folds=self.n_test_folds,
322
+ )
323
+ return pd.Series(
324
+ {
325
+ "Number of Observations": n_observations,
326
+ "Total Number of Folds": self.n_folds,
327
+ "Number of Test Folds": self.n_test_folds,
328
+ "Purge Size": self.purged_size,
329
+ "Embargo Size": self.embargo_size,
330
+ "Average Training Size": int(avg_train_size),
331
+ "Number of Test Paths": self.n_test_paths,
332
+ "Number of Training Combinations": self.n_splits,
333
+ }
334
+ )
336
335
 
337
336
  def plot_train_test_folds(self) -> skt.Figure:
338
337
  """Plot the train/test fold locations"""
@@ -408,3 +407,155 @@ class CombinatorialPurgedCV(BaseCombinatorialCV):
408
407
  )
409
408
 
410
409
  return fig
410
+
411
+
412
+ def _n_splits(n_folds: int, n_test_folds: int) -> int:
413
+ """Number of splits.
414
+
415
+ Parameters
416
+ ----------
417
+ n_folds : int
418
+ Number of folds.
419
+
420
+ n_test_folds : int
421
+ Number of test folds.
422
+
423
+ Returns
424
+ -------
425
+ n_splits : int
426
+ Number of splits
427
+ """
428
+ return int(math.comb(n_folds, n_test_folds))
429
+
430
+
431
+ def _n_test_paths(n_folds: int, n_test_folds: int) -> int:
432
+ """Number of test paths that can be reconstructed from the train/test
433
+ combinations
434
+
435
+ Parameters
436
+ ----------
437
+ n_folds : int
438
+ Number of folds.
439
+
440
+ n_test_folds : int
441
+ Number of test folds.
442
+
443
+ Returns
444
+ -------
445
+ n_splits : int
446
+ Number of test paths.
447
+ """
448
+ return (
449
+ _n_splits(n_folds=n_folds, n_test_folds=n_test_folds) * n_test_folds // n_folds
450
+ )
451
+
452
+
453
+ def _avg_train_size(n_observations: int, n_folds: int, n_test_folds: int) -> float:
454
+ """Average number of observations contained in each training set.
455
+
456
+ Parameters
457
+ ----------
458
+ n_observations : int
459
+ Number of observations.
460
+
461
+ n_folds : int
462
+ Number of folds.
463
+
464
+ n_test_folds : int
465
+ Number of test folds.
466
+
467
+ Returns
468
+ -------
469
+ avg_train_size : float
470
+ Average number of observations contained in each training set.
471
+ """
472
+ return n_observations / n_folds * (n_folds - n_test_folds)
473
+
474
+
475
+ def optimal_folds_number(
476
+ n_observations: int,
477
+ target_train_size: int,
478
+ target_n_test_paths: int,
479
+ weight_train_size: float = 1,
480
+ weight_n_test_paths: float = 1,
481
+ ) -> tuple[int, int]:
482
+ r"""Find the optimal number of folds (total folds and test folds) for a target
483
+ training size and a target number of test paths.
484
+
485
+ We find `x = n_folds` and `y = n_test_folds` that minimizes the below
486
+ cost function of the relative distance from the two targets:
487
+
488
+ .. math::
489
+ cost(x,y) = w_{f} \times \lvert\frac{f(x,y)-f_{target}}{f_{target}}\rvert + w_{g} \times \lvert\frac{g(x,y)-g_{target}}{g_{target}}\rvert
490
+
491
+ with :math:`w_{f}` and :math:`w_{g}` the weights assigned to the distance
492
+ from each target and :math:`f(x,y)` and :math:`g(x,y)` the average training size
493
+ and the number of test paths as a function of the number of total folds and test
494
+ folds.
495
+
496
+ This is a combinatorial problem with :math:`\frac{T\times(T-3)}{2}` combinations,
497
+ with :math:`T` the number of observations.
498
+
499
+ We reduce the search space by using the combinatorial symetry
500
+ :math:`{n \choose k}={n \choose n-k}` and skipping cost computation above 1e5.
501
+
502
+ Parameters
503
+ ----------
504
+ n_observations : int
505
+ Number of observations.
506
+
507
+ target_train_size : int
508
+ The target number of observation in the training set.
509
+
510
+ target_n_test_paths : int
511
+ The target number of test paths (that can be reconstructed from the train/test
512
+ combinations).
513
+
514
+ weight_train_size : float, default=1
515
+ The weight assigned to the distance from the target train size.
516
+ The default value is 1.
517
+
518
+ weight_n_test_paths : float, default=1
519
+ The weight assigned to the distance from the target number of test paths.
520
+ The default value is 1.
521
+
522
+ Returns
523
+ -------
524
+ n_folds : int
525
+ Optimal number of total folds.
526
+
527
+ n_test_folds : int
528
+ Optimal number of test folds.
529
+ """
530
+
531
+ def _cost(
532
+ x: int,
533
+ y: int,
534
+ ) -> float:
535
+ n_test_paths = _n_test_paths(n_folds=x, n_test_folds=y)
536
+ avg_train_size = _avg_train_size(
537
+ n_observations=n_observations, n_folds=x, n_test_folds=y
538
+ )
539
+ return (
540
+ weight_n_test_paths
541
+ * abs(n_test_paths - target_n_test_paths)
542
+ / target_n_test_paths
543
+ + weight_train_size
544
+ * abs(avg_train_size - target_train_size)
545
+ / target_train_size
546
+ )
547
+
548
+ costs = []
549
+ res = []
550
+ for n_folds in range(3, n_observations + 1):
551
+ i = None
552
+ for n_test_folds in range(2, n_folds):
553
+ if i is None or n_folds - n_test_folds <= i:
554
+ cost = _cost(x=n_folds, y=n_test_folds)
555
+ costs.append(cost)
556
+ res.append((n_folds, n_test_folds))
557
+ if i is None and cost > 1e5:
558
+ i = n_test_folds
559
+
560
+ j = np.argmin(costs)
561
+ return res[j]
@@ -170,12 +170,14 @@ def cross_val_predict(
170
170
  path_id = path_ids[i, j]
171
171
  portfolios[path_id].append(p)
172
172
  name = portfolio_params.pop("name", "path")
173
- pred = Population([
174
- MultiPeriodPortfolio(
175
- name=f"{name}_{i}", portfolios=portfolios[i], **portfolio_params
176
- )
177
- for i in range(path_nb)
178
- ])
173
+ pred = Population(
174
+ [
175
+ MultiPeriodPortfolio(
176
+ name=f"{name}_{i}", portfolios=portfolios[i], **portfolio_params
177
+ )
178
+ for i in range(path_nb)
179
+ ]
180
+ )
179
181
  else:
180
182
  # We need to re-order the test folds in case they were un-ordered by the
181
183
  # CV generator.
@@ -29,8 +29,8 @@ class BaseOptimization(skb.BaseEstimator, ABC):
29
29
  portfolio_params : dict, optional
30
30
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
31
31
  `score` methods. If not provided, the `name`, `transaction_costs`,
32
- `management_fees` and `previous_weights` are copied from the optimization
33
- model and systematically passed to the portfolio.
32
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
33
+ optimization model and passed to the portfolio.
34
34
 
35
35
  Attributes
36
36
  ----------
@@ -84,7 +84,12 @@ class BaseOptimization(skb.BaseEstimator, ABC):
84
84
  ptf_kwargs = self.portfolio_params.copy()
85
85
 
86
86
  # Set the default portfolio parameters equal to the optimization parameters
87
- for param in ["transaction_costs", "management_fees", "previous_weights"]:
87
+ for param in [
88
+ "transaction_costs",
89
+ "management_fees",
90
+ "previous_weights",
91
+ "risk_free_rate",
92
+ ]:
88
93
  if param not in ptf_kwargs and hasattr(self, param):
89
94
  ptf_kwargs[param] = getattr(self, param)
90
95
 
@@ -97,15 +102,17 @@ class BaseOptimization(skb.BaseEstimator, ABC):
97
102
  # For a 2D array we return a population of portfolios.
98
103
  if self.weights_.ndim == 2:
99
104
  n_portfolios = self.weights_.shape[0]
100
- return Population([
101
- Portfolio(
102
- X=X,
103
- weights=self.weights_[i],
104
- name=f"ptf{i} - {name}",
105
- **ptf_kwargs,
106
- )
107
- for i in range(n_portfolios)
108
- ])
105
+ return Population(
106
+ [
107
+ Portfolio(
108
+ X=X,
109
+ weights=self.weights_[i],
110
+ name=f"ptf{i} - {name}",
111
+ **ptf_kwargs,
112
+ )
113
+ for i in range(n_portfolios)
114
+ ]
115
+ )
109
116
  return Portfolio(X=X, weights=self.weights_, name=name, **ptf_kwargs)
110
117
 
111
118
  def score(self, X: npt.ArrayLike, y: npt.ArrayLike = None) -> float:
@@ -8,6 +8,7 @@
8
8
  # scikit-learn, Copyright (c) 2007-2010 David Cournapeau, Fabian Pedregosa, Olivier
9
9
 
10
10
  from abc import ABC, abstractmethod
11
+ from typing import Any
11
12
 
12
13
  import numpy as np
13
14
  import numpy.typing as npt
@@ -183,8 +184,8 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
183
184
  portfolio_params : dict, optional
184
185
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
185
186
  `score` methods. If not provided, the `name`, `transaction_costs`,
186
- `management_fees` and `previous_weights` are copied from the optimization
187
- model and systematically passed to the portfolio.
187
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
188
+ optimization model and passed to the portfolio.
188
189
 
189
190
  Attributes
190
191
  ----------
@@ -235,7 +236,7 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
235
236
  self,
236
237
  value: float | dict | np.ndarray | list,
237
238
  n_assets: int,
238
- fill_value: any,
239
+ fill_value: Any,
239
240
  name: str,
240
241
  ) -> np.ndarray:
241
242
  """Convert input to cleaned 1D array
@@ -250,7 +251,7 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
250
251
  n_assets : int
251
252
  Number of assets. Used to verify the shape of the converted array.
252
253
 
253
- fill_value : any
254
+ fill_value : Any
254
255
  When `items` is a dictionary, elements that are not in `asset_names` are
255
256
  filled with `fill_value` in the converted array.
256
257
 
@@ -204,8 +204,8 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
204
204
  portfolio_params : dict, optional
205
205
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
206
206
  `score` methods. If not provided, the `name`, `transaction_costs`,
207
- `management_fees` and `previous_weights` are copied from the optimization
208
- model and systematically passed to the portfolio.
207
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
208
+ optimization model and passed to the portfolio.
209
209
 
210
210
  Attributes
211
211
  ----------
@@ -6,7 +6,6 @@
6
6
  # The risk measure generalization and constraint features are derived
7
7
  # from Riskfolio-Lib, Copyright (c) 2020-2023, Dany Cajas, Licensed under BSD 3 clause.
8
8
 
9
-
10
9
  import numpy as np
11
10
  import numpy.typing as npt
12
11
  import pandas as pd
@@ -205,8 +204,8 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
205
204
  portfolio_params : dict, optional
206
205
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
207
206
  `score` methods. If not provided, the `name`, `transaction_costs`,
208
- `management_fees` and `previous_weights` are copied from the optimization
209
- model and systematically passed to the portfolio.
207
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
208
+ optimization model and passed to the portfolio.
210
209
 
211
210
  Attributes
212
211
  ----------
@@ -9,6 +9,7 @@
9
9
  import warnings
10
10
  from abc import ABC, abstractmethod
11
11
  from enum import auto
12
+ from typing import Any
12
13
 
13
14
  import cvxpy as cp
14
15
  import cvxpy.constraints.constraint as cpc
@@ -403,8 +404,8 @@ class ConvexOptimization(BaseOptimization, ABC):
403
404
  portfolio_params : dict, optional
404
405
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
405
406
  `score` methods. If not provided, the `name`, `transaction_costs`,
406
- `management_fees` and `previous_weights` are copied from the optimization
407
- model and systematically passed to the portfolio.
407
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
408
+ optimization model and passed to the portfolio.
408
409
 
409
410
  Attributes
410
411
  ----------
@@ -575,7 +576,7 @@ class ConvexOptimization(BaseOptimization, ABC):
575
576
  self,
576
577
  value: float | dict | npt.ArrayLike | None,
577
578
  n_assets: int,
578
- fill_value: any,
579
+ fill_value: Any,
579
580
  name: str,
580
581
  ) -> float | np.ndarray:
581
582
  """Convert input to cleaned float or ndarray.
@@ -588,7 +589,7 @@ class ConvexOptimization(BaseOptimization, ABC):
588
589
  n_assets : int
589
590
  Number of assets. Used to verify the shape of the converted array.
590
591
 
591
- fill_value : any
592
+ fill_value : Any
592
593
  When `items` is a dictionary, elements that are not in `asset_names` are
593
594
  filled with `fill_value` in the converted array.
594
595