skfolio 0.2.2__py3-none-any.whl → 0.3.0__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 (52) hide show
  1. skfolio/datasets/__init__.py +2 -0
  2. skfolio/datasets/_base.py +51 -0
  3. skfolio/distance/_distance.py +15 -4
  4. skfolio/model_selection/_combinatorial.py +2 -2
  5. skfolio/model_selection/_validation.py +70 -15
  6. skfolio/model_selection/_walk_forward.py +3 -3
  7. skfolio/moments/__init__.py +2 -0
  8. skfolio/moments/covariance/__init__.py +11 -11
  9. skfolio/moments/covariance/_base.py +10 -9
  10. skfolio/moments/covariance/_denoise_covariance.py +181 -0
  11. skfolio/moments/covariance/_detone_covariance.py +158 -0
  12. skfolio/moments/covariance/_empirical_covariance.py +100 -0
  13. skfolio/moments/covariance/_ew_covariance.py +109 -0
  14. skfolio/moments/covariance/_gerber_covariance.py +157 -0
  15. skfolio/moments/covariance/_graphical_lasso_cv.py +194 -0
  16. skfolio/moments/covariance/_implied_covariance.py +454 -0
  17. skfolio/moments/covariance/_ledoit_wolf.py +140 -0
  18. skfolio/moments/covariance/_oas.py +115 -0
  19. skfolio/moments/covariance/_shrunk_covariance.py +104 -0
  20. skfolio/moments/expected_returns/__init__.py +4 -7
  21. skfolio/moments/expected_returns/_empirical_mu.py +63 -0
  22. skfolio/moments/expected_returns/_equilibrium_mu.py +124 -0
  23. skfolio/moments/expected_returns/_ew_mu.py +69 -0
  24. skfolio/moments/expected_returns/{_expected_returns.py → _shrunk_mu.py} +22 -200
  25. skfolio/optimization/cluster/_nco.py +46 -8
  26. skfolio/optimization/cluster/hierarchical/_base.py +21 -1
  27. skfolio/optimization/cluster/hierarchical/_herc.py +18 -4
  28. skfolio/optimization/cluster/hierarchical/_hrp.py +13 -4
  29. skfolio/optimization/convex/_base.py +10 -1
  30. skfolio/optimization/convex/_distributionally_robust.py +12 -2
  31. skfolio/optimization/convex/_maximum_diversification.py +9 -2
  32. skfolio/optimization/convex/_mean_risk.py +33 -6
  33. skfolio/optimization/convex/_risk_budgeting.py +5 -2
  34. skfolio/optimization/ensemble/_stacking.py +32 -9
  35. skfolio/optimization/naive/_naive.py +20 -2
  36. skfolio/population/_population.py +2 -0
  37. skfolio/prior/_base.py +1 -1
  38. skfolio/prior/_black_litterman.py +20 -2
  39. skfolio/prior/_empirical.py +38 -5
  40. skfolio/prior/_factor_model.py +44 -7
  41. skfolio/uncertainty_set/_base.py +30 -9
  42. skfolio/uncertainty_set/_bootstrap.py +26 -10
  43. skfolio/uncertainty_set/_empirical.py +25 -10
  44. skfolio/utils/stats.py +24 -3
  45. skfolio/utils/tools.py +213 -79
  46. {skfolio-0.2.2.dist-info → skfolio-0.3.0.dist-info}/METADATA +4 -3
  47. skfolio-0.3.0.dist-info/RECORD +91 -0
  48. {skfolio-0.2.2.dist-info → skfolio-0.3.0.dist-info}/WHEEL +1 -1
  49. skfolio/moments/covariance/_covariance.py +0 -1114
  50. skfolio-0.2.2.dist-info/RECORD +0 -79
  51. {skfolio-0.2.2.dist-info → skfolio-0.3.0.dist-info}/LICENSE +0 -0
  52. {skfolio-0.2.2.dist-info → skfolio-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Expected returns estimators."""
1
+ """Shrinkage Expected Returns (Mu) Estimators."""
2
2
 
3
3
  # Copyright (c) 2023
4
4
  # Author: Hugo Delatte <delatte.hugo@gmail.com>
@@ -12,209 +12,13 @@ from enum import auto
12
12
 
13
13
  import numpy as np
14
14
  import numpy.typing as npt
15
- import pandas as pd
15
+ import sklearn.utils.metadata_routing as skm
16
16
 
17
17
  from skfolio.moments.covariance import BaseCovariance, EmpiricalCovariance
18
18
  from skfolio.moments.expected_returns._base import BaseMu
19
19
  from skfolio.utils.tools import AutoEnum, check_estimator
20
20
 
21
21
 
22
- class EmpiricalMu(BaseMu):
23
- """Empirical Expected Returns (Mu) estimator.
24
-
25
- Estimates the expected returns with the historical mean.
26
-
27
- Parameters
28
- ----------
29
- window_size : int, optional
30
- Window size. The model is fitted on the last `window_size` observations.
31
- The default (`None`) is to use all the data.
32
-
33
- Attributes
34
- ----------
35
- mu_ : ndarray of shape (n_assets,)
36
- Estimated expected returns of the assets.
37
-
38
- n_features_in_ : int
39
- Number of assets seen during `fit`.
40
-
41
- feature_names_in_ : ndarray of shape (`n_features_in_`,)
42
- Names of assets seen during `fit`. Defined only when `X`
43
- has assets names that are all strings.
44
- """
45
-
46
- def __init__(self, window_size: int | None = None):
47
- self.window_size = window_size
48
-
49
- def fit(self, X: npt.ArrayLike, y=None) -> "EmpiricalMu":
50
- """Fit the Mu Empirical estimator model.
51
-
52
- Parameters
53
- ----------
54
- X : array-like of shape (n_observations, n_assets)
55
- Price returns of the assets.
56
-
57
- y : Ignored
58
- Not used, present for API consistency by convention.
59
-
60
- Returns
61
- -------
62
- self : EmpiricalMu
63
- Fitted estimator.
64
- """
65
- X = self._validate_data(X)
66
- if self.window_size is not None:
67
- X = X[-self.window_size :]
68
- self.mu_ = np.mean(X, axis=0)
69
- return self
70
-
71
-
72
- class EWMu(BaseMu):
73
- r"""Exponentially Weighted Expected Returns (Mu) estimator.
74
-
75
- Estimates the expected returns with the exponentially weighted mean (EWM).
76
-
77
- Parameters
78
- ----------
79
- window_size : int, optional
80
- Window size. The model is fitted on the last `window_size` observations.
81
- The default (`None`) is to use all the data.
82
-
83
- alpha : float, default=0.2
84
- Exponential smoothing factor. The default value is `0.2`.
85
-
86
- :math:`0 < \alpha \leq 1`.
87
-
88
- Attributes
89
- ----------
90
- mu_ : ndarray of shape (n_assets,)
91
- Estimated expected returns of the assets.
92
-
93
- n_features_in_ : int
94
- Number of assets seen during `fit`.
95
-
96
- feature_names_in_ : ndarray of shape (`n_features_in_`,)
97
- Names of assets seen during `fit`. Defined only when `X`
98
- has assets names that are all strings.
99
- """
100
-
101
- def __init__(self, window_size: int | None = None, alpha: float = 0.2):
102
- self.window_size = window_size
103
- self.alpha = alpha
104
-
105
- def fit(self, X: npt.ArrayLike, y=None) -> "EWMu":
106
- """Fit the EWMu estimator model.
107
-
108
- Parameters
109
- ----------
110
- X : array-like of shape (n_observations, n_assets)
111
- Price returns of the assets.
112
-
113
- y : Ignored
114
- Not used, present for API consistency by convention.
115
-
116
- Returns
117
- -------
118
- self : EWMu
119
- Fitted estimator.
120
- """
121
- X = self._validate_data(X)
122
- if self.window_size is not None:
123
- X = X[-self.window_size :]
124
- self.mu_ = pd.DataFrame(X).ewm(alpha=self.alpha).mean().iloc[-1, :].to_numpy()
125
- return self
126
-
127
-
128
- class EquilibriumMu(BaseMu):
129
- r"""Equilibrium Expected Returns (Mu) estimator.
130
-
131
- The Equilibrium is defined as:
132
-
133
- .. math:: risk\_aversion \times \Sigma \cdot w^T
134
-
135
- For Market Cap Equilibrium, the weights are the assets Market Caps.
136
- For Equal-weighted Equilibrium, the weights are equal-weighted (1/N).
137
-
138
- Parameters
139
- ----------
140
- risk_aversion : float, default=1.0
141
- Risk aversion factor.
142
- The default value is `1.0`.
143
-
144
- weights : array-like of shape (n_assets,), optional
145
- Asset weights used to compute the Expected Return Equilibrium.
146
- The default is to use the equal-weighted equilibrium (1/N).
147
- For a Market Cap weighted equilibrium, you must provide the asset Market Caps.
148
-
149
- covariance_estimator : BaseCovariance, optional
150
- :ref:`Covariance estimator <covariance_estimator>` used to estimate the
151
- covariance in the equilibrium formula.
152
- The default (`None`) is to use :class:`~skfolio.moments.EmpiricalCovariance`.
153
-
154
- Attributes
155
- ----------
156
- mu_ : ndarray of shape (n_assets,)
157
- Estimated expected returns of the assets.
158
-
159
- covariance_estimator_ : BaseCovariance
160
- Fitted `covariance_estimator`.
161
-
162
- n_features_in_ : int
163
- Number of assets seen during `fit`.
164
-
165
- feature_names_in_ : ndarray of shape (`n_features_in_`,)
166
- Names of assets seen during `fit`. Defined only when `X`
167
- has assets names that are all strings.
168
- """
169
-
170
- covariance_estimator_: BaseCovariance
171
-
172
- def __init__(
173
- self,
174
- risk_aversion: float = 1,
175
- weights: np.ndarray | None = None,
176
- covariance_estimator: BaseCovariance | None = None,
177
- ):
178
- self.risk_aversion = risk_aversion
179
- self.weights = weights
180
- self.covariance_estimator = covariance_estimator
181
-
182
- def fit(self, X: npt.ArrayLike, y=None) -> "EquilibriumMu":
183
- """Fit the EquilibriumMu estimator model.
184
-
185
- Parameters
186
- ----------
187
- X : array-like of shape (n_observations, n_assets)
188
- Price returns of the assets.
189
-
190
- y : Ignored
191
- Not used, present for API consistency by convention.
192
-
193
- Returns
194
- -------
195
- self : EquilibriumMu
196
- Fitted estimator.
197
- """
198
- # fitting estimators
199
- self.covariance_estimator_ = check_estimator(
200
- self.covariance_estimator,
201
- default=EmpiricalCovariance(),
202
- check_type=BaseCovariance,
203
- )
204
- self.covariance_estimator_.fit(X)
205
-
206
- # we validate and convert to numpy after all models have been fitted to keep
207
- # features names information.
208
- X = self._validate_data(X)
209
- n_assets = X.shape[1]
210
- if self.weights is None:
211
- weights = np.ones(n_assets) / n_assets
212
- else:
213
- weights = np.asarray(self.weights)
214
- self.mu_ = self.risk_aversion * self.covariance_estimator_.covariance_ @ weights
215
- return self
216
-
217
-
218
22
  class ShrunkMuMethods(AutoEnum):
219
23
  """Shrinkage methods for the ShrunkMu estimator
220
24
 
@@ -336,7 +140,15 @@ class ShrunkMu(BaseMu):
336
140
  self.vol_weighted_target = vol_weighted_target
337
141
  self.method = method
338
142
 
339
- def fit(self, X: npt.ArrayLike, y=None) -> "ShrunkMu":
143
+ def get_metadata_routing(self):
144
+ # noinspection PyTypeChecker
145
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
146
+ covariance_estimator=self.covariance_estimator,
147
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
148
+ )
149
+ return router
150
+
151
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "ShrunkMu":
340
152
  """Fit the ShrunkMu estimator model.
341
153
 
342
154
  Parameters
@@ -347,11 +159,20 @@ class ShrunkMu(BaseMu):
347
159
  y : Ignored
348
160
  Not used, present for API consistency by convention.
349
161
 
162
+ **fit_params : dict
163
+ Parameters to pass to the underlying estimators.
164
+ Only available if `enable_metadata_routing=True`, which can be
165
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
166
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
167
+ more details.
168
+
350
169
  Returns
351
170
  -------
352
171
  self : ShrunkMu
353
172
  Fitted estimator.
354
173
  """
174
+ routed_params = skm.process_routing(self, "fit", **fit_params)
175
+
355
176
  if not isinstance(self.method, ShrunkMuMethods):
356
177
  raise ValueError(
357
178
  "`method` must be of type ShrunkMuMethods, got"
@@ -363,7 +184,8 @@ class ShrunkMu(BaseMu):
363
184
  default=EmpiricalCovariance(),
364
185
  check_type=BaseCovariance,
365
186
  )
366
- self.covariance_estimator_.fit(X)
187
+ # noinspection PyArgumentList
188
+ self.covariance_estimator_.fit(X, y, **routed_params.covariance_estimator.fit)
367
189
 
368
190
  # we validate and convert to numpy after all models have been fitted to keep
369
191
  # features names information.
@@ -15,7 +15,8 @@ import numpy.typing as npt
15
15
  import pandas as pd
16
16
  import sklearn as sk
17
17
  import sklearn.base as skb
18
- import sklearn.model_selection as skm
18
+ import sklearn.model_selection as sks
19
+ import sklearn.utils.metadata_routing as skm
19
20
  import sklearn.utils.parallel as skp
20
21
 
21
22
  import skfolio.typing as skt
@@ -174,7 +175,7 @@ class NestedClustersOptimization(BaseOptimization):
174
175
  outer_estimator: BaseOptimization | None = None,
175
176
  distance_estimator: BaseDistance | None = None,
176
177
  clustering_estimator: skb.BaseEstimator | None = None,
177
- cv: skm.BaseCrossValidator | BaseCombinatorialCV | str | int | None = None,
178
+ cv: sks.BaseCrossValidator | BaseCombinatorialCV | str | int | None = None,
178
179
  quantile: float = 0.5,
179
180
  quantile_measure: skt.Measure = RatioMeasure.SHARPE_RATIO,
180
181
  n_jobs: int | None = None,
@@ -192,8 +193,27 @@ class NestedClustersOptimization(BaseOptimization):
192
193
  self.n_jobs = n_jobs
193
194
  self.verbose = verbose
194
195
 
196
+ def get_metadata_routing(self):
197
+ # noinspection PyTypeChecker
198
+ router = (
199
+ skm.MetadataRouter(owner=self.__class__.__name__)
200
+ .add(
201
+ distance_estimator=self.distance_estimator,
202
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
203
+ )
204
+ .add(
205
+ clustering_estimator=self.clustering_estimator,
206
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
207
+ )
208
+ .add(
209
+ inner_estimator=self.inner_estimator,
210
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
211
+ )
212
+ )
213
+ return router
214
+
195
215
  def fit(
196
- self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
216
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
197
217
  ) -> "NestedClustersOptimization":
198
218
  """Fit the Nested Clusters Optimization estimator.
199
219
 
@@ -206,11 +226,20 @@ class NestedClustersOptimization(BaseOptimization):
206
226
  Price returns of factors or a target benchmark.
207
227
  The default is `None`.
208
228
 
229
+ **fit_params : dict
230
+ Parameters to pass to the underlying estimators.
231
+ Only available if `enable_metadata_routing=True`, which can be
232
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
233
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
234
+ more details.
235
+
209
236
  Returns
210
237
  -------
211
238
  self : NestedClustersOptimization
212
239
  Fitted estimator.
213
240
  """
241
+ routed_params = skm.process_routing(self, "fit", **fit_params)
242
+
214
243
  self.distance_estimator_ = check_estimator(
215
244
  self.distance_estimator,
216
245
  default=PearsonDistance(),
@@ -232,7 +261,8 @@ class NestedClustersOptimization(BaseOptimization):
232
261
  check_type=BaseOptimization,
233
262
  )
234
263
 
235
- self.distance_estimator_.fit(X)
264
+ # noinspection PyArgumentList
265
+ self.distance_estimator_.fit(X, y, **routed_params.distance_estimator.fit)
236
266
  distance = self.distance_estimator_.distance_
237
267
  n_assets = distance.shape[0]
238
268
 
@@ -241,7 +271,9 @@ class NestedClustersOptimization(BaseOptimization):
241
271
  distance = pd.DataFrame(distance, columns=X.columns)
242
272
 
243
273
  # noinspection PyUnresolvedReferences
244
- self.clustering_estimator_.fit(distance)
274
+ self.clustering_estimator_.fit(
275
+ X=distance, y=None, **routed_params.clustering_estimator.fit
276
+ )
245
277
  # noinspection PyUnresolvedReferences
246
278
  labels = self.clustering_estimator_.labels_
247
279
  n_clusters = max(labels) + 1
@@ -254,7 +286,12 @@ class NestedClustersOptimization(BaseOptimization):
254
286
  # noinspection PyCallingNonCallable
255
287
  fitted_inner_estimators = skp.Parallel(n_jobs=self.n_jobs)(
256
288
  skp.delayed(fit_single_estimator)(
257
- sk.clone(_inner_estimator), X, y, indices=cluster_ids, axis=1
289
+ sk.clone(_inner_estimator),
290
+ X,
291
+ y,
292
+ routed_params.inner_estimator.fit,
293
+ indices=cluster_ids,
294
+ axis=1,
258
295
  )
259
296
  for cluster_ids in clusters
260
297
  if len(cluster_ids) != 1
@@ -288,7 +325,7 @@ class NestedClustersOptimization(BaseOptimization):
288
325
  cv_predictions = None
289
326
  test_indices = slice(None)
290
327
  else:
291
- cv = skm.check_cv(self.cv)
328
+ cv = sks.check_cv(self.cv)
292
329
  if hasattr(cv, "random_state") and cv.random_state is None:
293
330
  cv.random_state = np.random.RandomState()
294
331
  # noinspection PyCallingNonCallable
@@ -302,6 +339,7 @@ class NestedClustersOptimization(BaseOptimization):
302
339
  verbose=self.verbose,
303
340
  column_indices=cluster_ids,
304
341
  method="predict",
342
+ params=routed_params.inner_estimator.fit,
305
343
  )
306
344
  for cluster_ids in clusters
307
345
  if len(cluster_ids) != 1
@@ -347,7 +385,7 @@ class NestedClustersOptimization(BaseOptimization):
347
385
  else:
348
386
  assert not any(cv_predictions), "cv_predictions iterator must be empty"
349
387
 
350
- fit_single_estimator(self.outer_estimator_, X=X_pred, y=y_pred)
388
+ fit_single_estimator(self.outer_estimator_, X_pred, y_pred, fit_params={})
351
389
  outer_weights = self.outer_estimator_.weights_
352
390
  self.weights_ = outer_weights @ inner_weights
353
391
  return self
@@ -12,6 +12,7 @@ from typing import Any
12
12
 
13
13
  import numpy as np
14
14
  import numpy.typing as npt
15
+ import sklearn.utils.metadata_routing as skm
15
16
 
16
17
  import skfolio.typing as skt
17
18
  from skfolio.cluster import HierarchicalClustering
@@ -438,6 +439,25 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
438
439
  )
439
440
  return alpha
440
441
 
442
+ def get_metadata_routing(self):
443
+ # noinspection PyTypeChecker
444
+ router = (
445
+ skm.MetadataRouter(owner=self.__class__.__name__)
446
+ .add(
447
+ prior_estimator=self.prior_estimator,
448
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
449
+ )
450
+ .add(
451
+ distance_estimator=self.distance_estimator,
452
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
453
+ )
454
+ .add(
455
+ hierarchical_clustering_estimator=self.hierarchical_clustering_estimator,
456
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
457
+ )
458
+ )
459
+ return router
460
+
441
461
  @abstractmethod
442
- def fit(self, X: npt.ArrayLike, y: None = None):
462
+ def fit(self, X: npt.ArrayLike, y: None = None, **fit_params):
443
463
  pass
@@ -10,6 +10,7 @@ import numpy as np
10
10
  import numpy.typing as npt
11
11
  import pandas as pd
12
12
  import scipy.cluster.hierarchy as sch
13
+ import sklearn.utils.metadata_routing as skm
13
14
 
14
15
  import skfolio.typing as skt
15
16
  from skfolio.cluster import HierarchicalClustering
@@ -269,7 +270,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
269
270
  )
270
271
 
271
272
  def fit(
272
- self, X: npt.ArrayLike, y: None = None
273
+ self, X: npt.ArrayLike, y: None = None, **fit_params
273
274
  ) -> "HierarchicalEqualRiskContribution":
274
275
  """Fit the Hierarchical Equal Risk Contribution estimator.
275
276
 
@@ -281,11 +282,20 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
281
282
  y : Ignored
282
283
  Not used, present for API consistency by convention.
283
284
 
285
+ **fit_params : dict
286
+ Parameters to pass to the underlying estimators.
287
+ Only available if `enable_metadata_routing=True`, which can be
288
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
289
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
290
+ more details.
291
+
284
292
  Returns
285
293
  -------
286
294
  self : HierarchicalEqualRiskContribution
287
295
  Fitted estimator.
288
296
  """
297
+ routed_params = skm.process_routing(self, "fit", **fit_params)
298
+
289
299
  # Validate
290
300
  if not isinstance(self.risk_measure, RiskMeasure | ExtraRiskMeasure):
291
301
  raise TypeError(
@@ -308,7 +318,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
308
318
  )
309
319
 
310
320
  # Fit the estimators
311
- self.prior_estimator_.fit(X, y)
321
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
312
322
  prior_model = self.prior_estimator_.prior_model_
313
323
  returns = prior_model.returns
314
324
 
@@ -316,14 +326,18 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
316
326
  if isinstance(X, pd.DataFrame):
317
327
  returns = pd.DataFrame(returns, columns=X.columns)
318
328
 
319
- self.distance_estimator_.fit(returns)
329
+ # noinspection PyArgumentList
330
+ self.distance_estimator_.fit(returns, y, **routed_params.distance_estimator.fit)
320
331
  distance = self.distance_estimator_.distance_
321
332
 
322
333
  # To keep the asset_names
323
334
  if isinstance(X, pd.DataFrame):
324
335
  distance = pd.DataFrame(distance, columns=X.columns)
325
336
 
326
- self.hierarchical_clustering_estimator_.fit(distance)
337
+ # noinspection PyArgumentList
338
+ self.hierarchical_clustering_estimator_.fit(
339
+ X=distance, y=None, **routed_params.hierarchical_clustering_estimator.fit
340
+ )
327
341
 
328
342
  n_clusters = self.hierarchical_clustering_estimator_.n_clusters_
329
343
  labels = self.hierarchical_clustering_estimator_.labels_
@@ -10,6 +10,7 @@ import numpy as np
10
10
  import numpy.typing as npt
11
11
  import pandas as pd
12
12
  import scipy.cluster.hierarchy as sch
13
+ import sklearn.utils.metadata_routing as skm
13
14
 
14
15
  import skfolio.typing as skt
15
16
  from skfolio.cluster import HierarchicalClustering
@@ -270,7 +271,9 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
270
271
  portfolio_params=portfolio_params,
271
272
  )
272
273
 
273
- def fit(self, X: npt.ArrayLike, y: None = None) -> "HierarchicalRiskParity":
274
+ def fit(
275
+ self, X: npt.ArrayLike, y: None = None, **fit_params
276
+ ) -> "HierarchicalRiskParity":
274
277
  """Fit the Hierarchical Risk Parity Optimization estimator.
275
278
 
276
279
  Parameters
@@ -286,6 +289,8 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
286
289
  self : HierarchicalRiskParity
287
290
  Fitted estimator.
288
291
  """
292
+ routed_params = skm.process_routing(self, "fit", **fit_params)
293
+
289
294
  # Validate
290
295
  if not isinstance(self.risk_measure, RiskMeasure | ExtraRiskMeasure):
291
296
  raise TypeError(
@@ -308,7 +313,7 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
308
313
  )
309
314
 
310
315
  # Fit the estimators
311
- self.prior_estimator_.fit(X, y)
316
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
312
317
  prior_model = self.prior_estimator_.prior_model_
313
318
  returns = prior_model.returns
314
319
 
@@ -316,14 +321,18 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
316
321
  if isinstance(X, pd.DataFrame):
317
322
  returns = pd.DataFrame(returns, columns=X.columns)
318
323
 
319
- self.distance_estimator_.fit(returns)
324
+ # noinspection PyArgumentList
325
+ self.distance_estimator_.fit(returns, y, **routed_params.distance_estimator.fit)
320
326
  distance = self.distance_estimator_.distance_
321
327
 
322
328
  # To keep the asset_names
323
329
  if isinstance(X, pd.DataFrame):
324
330
  distance = pd.DataFrame(distance, columns=X.columns)
325
331
 
326
- self.hierarchical_clustering_estimator_.fit(distance)
332
+ # noinspection PyArgumentList
333
+ self.hierarchical_clustering_estimator_.fit(
334
+ X=distance, y=None, **routed_params.hierarchical_clustering_estimator.fit
335
+ )
327
336
 
328
337
  X = self._validate_data(X)
329
338
  n_assets = X.shape[1]
@@ -17,6 +17,7 @@ import numpy as np
17
17
  import numpy.typing as npt
18
18
  import scipy as sc
19
19
  import scipy.sparse.linalg as scl
20
+ import sklearn.utils.metadata_routing as skm
20
21
 
21
22
  import skfolio.typing as skt
22
23
  from skfolio.measures import RiskMeasure, owa_gmd_weights
@@ -1959,6 +1960,14 @@ class ConvexOptimization(BaseOptimization, ABC):
1959
1960
  ]
1960
1961
  return risk, constraints
1961
1962
 
1963
+ def get_metadata_routing(self):
1964
+ # noinspection PyTypeChecker
1965
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
1966
+ prior_estimator=self.prior_estimator,
1967
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
1968
+ )
1969
+ return router
1970
+
1962
1971
  @abstractmethod
1963
- def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
1972
+ def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params):
1964
1973
  pass
@@ -7,6 +7,7 @@
7
7
  import cvxpy as cp
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
+ import sklearn.utils.metadata_routing as skm
10
11
 
11
12
  import skfolio.typing as skt
12
13
  from skfolio.measures import RiskMeasure
@@ -303,7 +304,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
303
304
  self.wasserstein_ball_radius = wasserstein_ball_radius
304
305
 
305
306
  def fit(
306
- self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
307
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
307
308
  ) -> "DistributionallyRobustCVaR":
308
309
  """Fit the Distributionally Robust CVaR Optimization estimator.
309
310
 
@@ -316,11 +317,20 @@ class DistributionallyRobustCVaR(ConvexOptimization):
316
317
  Price returns of factors.
317
318
  The default is `None`.
318
319
 
320
+ **fit_params : dict
321
+ Parameters to pass to the underlying estimators.
322
+ Only available if `enable_metadata_routing=True`, which can be
323
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
324
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
325
+ more details.
326
+
319
327
  Returns
320
328
  -------
321
329
  self : DistributionallyRobustCVaR
322
330
  Fitted estimator.
323
331
  """
332
+ routed_params = skm.process_routing(self, "fit", **fit_params)
333
+
324
334
  self._check_feature_names(X, reset=True)
325
335
  # Used to avoid adding multiple times similar constrains linked to identical
326
336
  # risk models
@@ -329,7 +339,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
329
339
  default=EmpiricalPrior(),
330
340
  check_type=BasePrior,
331
341
  )
332
- self.prior_estimator_.fit(X, y)
342
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
333
343
  prior_model = self.prior_estimator_.prior_model_
334
344
  n_observations, n_assets = prior_model.returns.shape
335
345
 
@@ -398,7 +398,7 @@ class MaximumDiversification(MeanRisk):
398
398
  )
399
399
 
400
400
  def fit(
401
- self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
401
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
402
402
  ) -> "MaximumDiversification":
403
403
  """Fit the Maximum Diversification Optimization estimator.
404
404
 
@@ -411,6 +411,13 @@ class MaximumDiversification(MeanRisk):
411
411
  Price returns of factors or a target benchmark.
412
412
  The default is `None`.
413
413
 
414
+ **fit_params : dict
415
+ Parameters to pass to the underlying estimators.
416
+ Only available if `enable_metadata_routing=True`, which can be
417
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
418
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
419
+ more details.
420
+
414
421
  Returns
415
422
  -------
416
423
  self : MaximumDiversification
@@ -424,5 +431,5 @@ class MaximumDiversification(MeanRisk):
424
431
  return np.sqrt(np.diag(covariance)) @ w
425
432
 
426
433
  self.overwrite_expected_return = func
427
- super().fit(X, y)
434
+ super().fit(X, y, **fit_params)
428
435
  return self