skfolio 0.2.3__py3-none-any.whl → 0.3.1__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 +462 -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.3.dist-info → skfolio-0.3.1.dist-info}/METADATA +3 -2
  47. skfolio-0.3.1.dist-info/RECORD +91 -0
  48. {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/WHEEL +1 -1
  49. skfolio/moments/covariance/_covariance.py +0 -1114
  50. skfolio-0.2.3.dist-info/RECORD +0 -79
  51. {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/LICENSE +0 -0
  52. {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/top_level.txt +0 -0
@@ -11,6 +11,7 @@ import numpy as np
11
11
  import numpy.typing as npt
12
12
  import pandas as pd
13
13
  import sklearn as sk
14
+ import sklearn.utils.metadata_routing as skm
14
15
 
15
16
  import skfolio.typing as skt
16
17
  from skfolio.measures import RiskMeasure
@@ -681,7 +682,25 @@ class MeanRisk(ConvexOptimization):
681
682
  "`objective_function = ObjectiveFunction.MINIMIZE_RISK`"
682
683
  )
683
684
 
684
- def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None) -> "MeanRisk":
685
+ def get_metadata_routing(self):
686
+ # noinspection PyTypeChecker
687
+ router = (
688
+ super()
689
+ .get_metadata_routing()
690
+ .add(
691
+ mu_uncertainty_set_estimator=self.mu_uncertainty_set_estimator,
692
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
693
+ )
694
+ .add(
695
+ covariance_uncertainty_set_estimator=self.covariance_uncertainty_set_estimator,
696
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
697
+ )
698
+ )
699
+ return router
700
+
701
+ def fit(
702
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
703
+ ) -> "MeanRisk":
685
704
  """Fit the Mean-Risk Optimization estimator.
686
705
 
687
706
  Parameters
@@ -698,6 +717,8 @@ class MeanRisk(ConvexOptimization):
698
717
  self : MeanRisk
699
718
  Fitted estimator.
700
719
  """
720
+ routed_params = skm.process_routing(self, "fit", **fit_params)
721
+
701
722
  self._check_feature_names(X, reset=True)
702
723
  # Validate
703
724
  self._validation()
@@ -708,7 +729,7 @@ class MeanRisk(ConvexOptimization):
708
729
  default=EmpiricalPrior(),
709
730
  check_type=BasePrior,
710
731
  )
711
- self.prior_estimator_.fit(X, y)
732
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
712
733
  prior_model = self.prior_estimator_.prior_model_
713
734
  n_observations, n_assets = prior_model.returns.shape
714
735
 
@@ -766,7 +787,9 @@ class MeanRisk(ConvexOptimization):
766
787
  self.mu_uncertainty_set_estimator_ = sk.clone(
767
788
  self.mu_uncertainty_set_estimator
768
789
  )
769
- self.mu_uncertainty_set_estimator_.fit(X, y)
790
+ self.mu_uncertainty_set_estimator_.fit(
791
+ X, y, **routed_params.mu_uncertainty_set_estimator.fit
792
+ )
770
793
  mu_uncertainty_set = self._cvx_mu_uncertainty_set(
771
794
  mu_uncertainty_set=self.mu_uncertainty_set_estimator_.uncertainty_set_,
772
795
  w=w,
@@ -831,11 +854,11 @@ class MeanRisk(ConvexOptimization):
831
854
  efficient_frontier_size=None,
832
855
  portfolio_params=dict(annualized_factor=1),
833
856
  )
834
- model.fit(X)
857
+ model.fit(X, y, **fit_params)
835
858
  min_return = model.problem_values_["expected_return"]
836
859
  # noinspection PyTypeChecker
837
860
  model.set_params(objective_function=ObjectiveFunction.MAXIMIZE_RETURN)
838
- model.fit(X)
861
+ model.fit(X, y, **fit_params)
839
862
  max_return = model.problem_values_["expected_return"]
840
863
  if max_return <= 0:
841
864
  raise ValueError(
@@ -888,7 +911,11 @@ class MeanRisk(ConvexOptimization):
888
911
  self.covariance_uncertainty_set_estimator_ = sk.clone(
889
912
  self.covariance_uncertainty_set_estimator
890
913
  )
891
- self.covariance_uncertainty_set_estimator_.fit(X, y)
914
+ self.covariance_uncertainty_set_estimator_.fit(
915
+ X,
916
+ y,
917
+ **routed_params.covariance_uncertainty_set_estimator.fit,
918
+ )
892
919
  args[arg_name] = (
893
920
  self.covariance_uncertainty_set_estimator_.uncertainty_set_
894
921
  )
@@ -9,6 +9,7 @@
9
9
  import cvxpy as cp
10
10
  import numpy as np
11
11
  import numpy.typing as npt
12
+ import sklearn.utils.metadata_routing as skm
12
13
 
13
14
  import skfolio.typing as skt
14
15
  from skfolio.measures import RiskMeasure
@@ -440,7 +441,7 @@ class RiskBudgeting(ConvexOptimization):
440
441
  " otherwise the problem becomes non-convex."
441
442
  )
442
443
 
443
- def fit(self, X: npt.ArrayLike, y=None) -> "RiskBudgeting":
444
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "RiskBudgeting":
444
445
  """Fit the Risk Budgeting Optimization estimator.
445
446
 
446
447
  Parameters
@@ -458,6 +459,8 @@ class RiskBudgeting(ConvexOptimization):
458
459
  self : RiskBudgeting
459
460
  Fitted estimator.
460
461
  """
462
+ routed_params = skm.process_routing(self, "fit", **fit_params)
463
+
461
464
  self._check_feature_names(X, reset=True)
462
465
  # Validate
463
466
  self._validation()
@@ -468,7 +471,7 @@ class RiskBudgeting(ConvexOptimization):
468
471
  default=EmpiricalPrior(),
469
472
  check_type=BasePrior,
470
473
  )
471
- self.prior_estimator_.fit(X, y)
474
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
472
475
  prior_model = self.prior_estimator_.prior_model_
473
476
  n_observations, n_assets = prior_model.returns.shape
474
477
 
@@ -12,8 +12,9 @@ from copy import deepcopy
12
12
  import numpy as np
13
13
  import numpy.typing as npt
14
14
  import sklearn as sk
15
- import sklearn.model_selection as skm
15
+ import sklearn.model_selection as sks
16
16
  import sklearn.utils as sku
17
+ import sklearn.utils.metadata_routing as skm
17
18
  import sklearn.utils.parallel as skp
18
19
  import sklearn.utils.validation as skv
19
20
 
@@ -138,7 +139,7 @@ class StackingOptimization(BaseOptimization, BaseComposition):
138
139
  self,
139
140
  estimators: list[tuple[str, BaseOptimization]],
140
141
  final_estimator: BaseOptimization | None = None,
141
- cv: skm.BaseCrossValidator | BaseCombinatorialCV | str | int | None = None,
142
+ cv: sks.BaseCrossValidator | BaseCombinatorialCV | str | int | None = None,
142
143
  quantile: float = 0.5,
143
144
  quantile_measure: skt.Measure = RatioMeasure.SHARPE_RATIO,
144
145
  n_jobs: int | None = None,
@@ -229,8 +230,18 @@ class StackingOptimization(BaseOptimization, BaseComposition):
229
230
  """
230
231
  return super()._get_params("estimators", deep=deep)
231
232
 
233
+ def get_metadata_routing(self):
234
+ # noinspection PyTypeChecker
235
+ router = skm.MetadataRouter(owner=self.__class__.__name__)
236
+ for name, estimator in self.estimators:
237
+ router.add(
238
+ **{name: estimator},
239
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
240
+ )
241
+ return router
242
+
232
243
  def fit(
233
- self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
244
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
234
245
  ) -> "StackingOptimization":
235
246
  """Fit the Stacking Optimization estimator.
236
247
 
@@ -243,11 +254,20 @@ class StackingOptimization(BaseOptimization, BaseComposition):
243
254
  Price returns of factors or a target benchmark.
244
255
  The default is `None`.
245
256
 
257
+ **fit_params : dict
258
+ Parameters to pass to the underlying estimators.
259
+ Only available if `enable_metadata_routing=True`, which can be
260
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
261
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
262
+ more details.
263
+
246
264
  Returns
247
265
  -------
248
266
  self : StackingOptimization
249
267
  Fitted estimator.
250
268
  """
269
+ routed_params = skm.process_routing(self, "fit", **fit_params)
270
+
251
271
  names, all_estimators = self._validate_estimators()
252
272
  self.final_estimator_ = check_estimator(
253
273
  self.final_estimator,
@@ -266,8 +286,10 @@ class StackingOptimization(BaseOptimization, BaseComposition):
266
286
  # They are exposed publicly.
267
287
  # noinspection PyCallingNonCallable
268
288
  self.estimators_ = skp.Parallel(n_jobs=self.n_jobs)(
269
- skp.delayed(fit_single_estimator)(sk.clone(est), X, y)
270
- for est in all_estimators
289
+ skp.delayed(fit_single_estimator)(
290
+ sk.clone(est), X, y, routed_params[name]["fit"]
291
+ )
292
+ for name, est in zip(names, all_estimators, strict=True)
271
293
  )
272
294
 
273
295
  self.named_estimators_ = {
@@ -287,21 +309,22 @@ class StackingOptimization(BaseOptimization, BaseComposition):
287
309
  [estimator.predict(X) for estimator in self.estimators_]
288
310
  ).T
289
311
  else:
290
- cv = skm.check_cv(self.cv)
312
+ cv = sks.check_cv(self.cv)
291
313
  if hasattr(cv, "random_state") and cv.random_state is None:
292
314
  cv.random_state = np.random.RandomState()
293
315
  # noinspection PyCallingNonCallable
294
316
  cv_predictions = skp.Parallel(n_jobs=self.n_jobs)(
295
317
  skp.delayed(cross_val_predict)(
296
- sk.clone(estimator),
318
+ sk.clone(est),
297
319
  X,
298
320
  y,
299
321
  cv=deepcopy(cv),
300
322
  method="predict",
301
323
  n_jobs=self.n_jobs,
324
+ params=routed_params[name]["fit"],
302
325
  verbose=self.verbose,
303
326
  )
304
- for estimator in all_estimators
327
+ for name, est in zip(names, all_estimators, strict=True)
305
328
  )
306
329
 
307
330
  # We validate and convert to numpy array only after base-estimator fitting
@@ -326,7 +349,7 @@ class StackingOptimization(BaseOptimization, BaseComposition):
326
349
  )
327
350
  y = y[test_indices]
328
351
 
329
- fit_single_estimator(self.final_estimator_, X_pred, y)
352
+ fit_single_estimator(self.final_estimator_, X_pred, y, {})
330
353
  outer_weights = self.final_estimator_.weights_
331
354
  self.weights_ = outer_weights @ inner_weights
332
355
  return self
@@ -5,6 +5,7 @@
5
5
 
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
+ import sklearn.utils.metadata_routing as skm
8
9
 
9
10
  from skfolio.optimization._base import BaseOptimization
10
11
  from skfolio.prior import BasePrior, EmpiricalPrior
@@ -53,8 +54,16 @@ class InverseVolatility(BaseOptimization):
53
54
  super().__init__(portfolio_params=portfolio_params)
54
55
  self.prior_estimator = prior_estimator
55
56
 
57
+ def get_metadata_routing(self):
58
+ # noinspection PyTypeChecker
59
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
60
+ prior_estimator=self.prior_estimator,
61
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
62
+ )
63
+ return router
64
+
56
65
  def fit(
57
- self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
66
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
58
67
  ) -> "InverseVolatility":
59
68
  """Fit the Inverse Volatility estimator.
60
69
 
@@ -67,18 +76,27 @@ class InverseVolatility(BaseOptimization):
67
76
  Price returns of factors or a target benchmark.
68
77
  The default is `None`.
69
78
 
79
+ **fit_params : dict
80
+ Parameters to pass to the underlying estimators.
81
+ Only available if `enable_metadata_routing=True`, which can be
82
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
83
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
84
+ more details.
85
+
70
86
  Returns
71
87
  -------
72
88
  self : InverseVolatility
73
89
  Fitted estimator.
74
90
  """
91
+ routed_params = skm.process_routing(self, "fit", **fit_params)
92
+
75
93
  # fitting prior estimator
76
94
  self.prior_estimator_ = check_estimator(
77
95
  self.prior_estimator,
78
96
  default=EmpiricalPrior(),
79
97
  check_type=BasePrior,
80
98
  )
81
- self.prior_estimator_.fit(X, y)
99
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
82
100
  covariance = self.prior_estimator_.prior_model_.covariance
83
101
  w = 1 / np.sqrt(np.diag(covariance))
84
102
  self.weights_ = w / sum(w)
@@ -612,6 +612,8 @@ class Population(list):
612
612
  title = f"{title} (non-compounded)"
613
613
 
614
614
  df = pd.concat(cumulative_returns, axis=1).iloc[:, idx]
615
+ # Sort index because pd.concat unsort NaNs at the end
616
+ df.sort_index(inplace=True)
615
617
  df.columns = deduplicate_names(names)
616
618
 
617
619
  fig = df.plot(backend="plotly")
skfolio/prior/_base.py CHANGED
@@ -61,5 +61,5 @@ class BasePrior(skb.BaseEstimator, ABC):
61
61
  pass
62
62
 
63
63
  @abstractmethod
64
- def fit(self, X: npt.ArrayLike, y=None):
64
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params):
65
65
  pass
@@ -9,6 +9,7 @@
9
9
 
10
10
  import numpy as np
11
11
  import numpy.typing as npt
12
+ import sklearn.utils.metadata_routing as skm
12
13
 
13
14
  from skfolio.moments import EquilibriumMu
14
15
  from skfolio.prior._base import BasePrior, PriorModel
@@ -134,7 +135,15 @@ class BlackLitterman(BasePrior):
134
135
  self.view_confidences = view_confidences
135
136
  self.risk_free_rate = risk_free_rate
136
137
 
137
- def fit(self, X: npt.ArrayLike, y=None) -> "BlackLitterman":
138
+ def get_metadata_routing(self):
139
+ # noinspection PyTypeChecker
140
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
141
+ prior_estimator=self.prior_estimator,
142
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
143
+ )
144
+ return router
145
+
146
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "BlackLitterman":
138
147
  """Fit the Black & Litterman estimator.
139
148
 
140
149
  Parameters
@@ -145,18 +154,27 @@ class BlackLitterman(BasePrior):
145
154
  y : Ignored
146
155
  Not used, present for API consistency by convention.
147
156
 
157
+ **fit_params : dict
158
+ Parameters to pass to the underlying estimators.
159
+ Only available if `enable_metadata_routing=True`, which can be
160
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
161
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
162
+ more details.
163
+
148
164
  Returns
149
165
  -------
150
166
  self : BlackLitterman
151
167
  Fitted estimator.
152
168
  """
169
+ routed_params = skm.process_routing(self, "fit", **fit_params)
170
+
153
171
  self.prior_estimator_ = check_estimator(
154
172
  self.prior_estimator,
155
173
  default=EmpiricalPrior(mu_estimator=EquilibriumMu()),
156
174
  check_type=BasePrior,
157
175
  )
158
176
  # fitting prior estimator
159
- self.prior_estimator_.fit(X, y)
177
+ self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
160
178
 
161
179
  prior_mu = self.prior_estimator_.prior_model_.mu
162
180
  prior_covariance = self.prior_estimator_.prior_model_.covariance
@@ -6,6 +6,7 @@
6
6
 
7
7
  import numpy as np
8
8
  import numpy.typing as npt
9
+ import sklearn.utils.metadata_routing as skm
9
10
 
10
11
  from skfolio.moments import BaseCovariance, BaseMu, EmpiricalCovariance, EmpiricalMu
11
12
  from skfolio.prior._base import BasePrior, PriorModel
@@ -86,7 +87,22 @@ class EmpiricalPrior(BasePrior):
86
87
  self.is_log_normal = is_log_normal
87
88
  self.investment_horizon = investment_horizon
88
89
 
89
- def fit(self, X: npt.ArrayLike, y=None) -> "EmpiricalPrior":
90
+ def get_metadata_routing(self):
91
+ # noinspection PyTypeChecker
92
+ router = (
93
+ skm.MetadataRouter(owner=self.__class__.__name__)
94
+ .add(
95
+ mu_estimator=self.mu_estimator,
96
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
97
+ )
98
+ .add(
99
+ covariance_estimator=self.covariance_estimator,
100
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
101
+ )
102
+ )
103
+ return router
104
+
105
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "EmpiricalPrior":
90
106
  """Fit the Empirical Prior estimator.
91
107
 
92
108
  Parameters
@@ -97,11 +113,20 @@ class EmpiricalPrior(BasePrior):
97
113
  y : Ignored
98
114
  Not used, present for API consistency by convention.
99
115
 
116
+ **fit_params : dict
117
+ Parameters to pass to the underlying estimators.
118
+ Only available if `enable_metadata_routing=True`, which can be
119
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
120
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
121
+ more details.
122
+
100
123
  Returns
101
124
  -------
102
125
  self : EmpiricalPrior
103
126
  Fitted estimator.
104
127
  """
128
+ routed_params = skm.process_routing(self, "fit", **fit_params)
129
+
105
130
  self.mu_estimator_ = check_estimator(
106
131
  self.mu_estimator,
107
132
  default=EmpiricalMu(),
@@ -120,11 +145,15 @@ class EmpiricalPrior(BasePrior):
120
145
  "`is_log_normal` is `False`"
121
146
  )
122
147
  # Expected returns
123
- self.mu_estimator_.fit(X, y)
148
+ # noinspection PyArgumentList
149
+ self.mu_estimator_.fit(X, y, **routed_params.mu_estimator.fit)
124
150
  mu = self.mu_estimator_.mu_
125
151
 
126
152
  # Covariance
127
- self.covariance_estimator_.fit(X, y)
153
+ # noinspection PyArgumentList
154
+ self.covariance_estimator_.fit(
155
+ X, y, **routed_params.covariance_estimator.fit
156
+ )
128
157
  covariance = self.covariance_estimator_.covariance_
129
158
  else:
130
159
  if self.investment_horizon is None:
@@ -138,11 +167,15 @@ class EmpiricalPrior(BasePrior):
138
167
 
139
168
  # Estimates the moments on the log returns
140
169
  # Expected returns
141
- self.mu_estimator_.fit(X_log, y_log)
170
+ # noinspection PyArgumentList
171
+ self.mu_estimator_.fit(X_log, y_log, **routed_params.mu_estimator.fit)
142
172
  mu = self.mu_estimator_.mu_
143
173
 
144
174
  # Covariance
145
- self.covariance_estimator_.fit(X_log, y_log)
175
+ # noinspection PyArgumentList
176
+ self.covariance_estimator_.fit(
177
+ X_log, y_log, **routed_params.covariance_estimator.fit
178
+ )
146
179
  covariance = self.covariance_estimator_.covariance_
147
180
 
148
181
  # Using the property of aggregation across time we scale this distribution
@@ -15,7 +15,8 @@ import numpy as np
15
15
  import numpy.typing as npt
16
16
  import sklearn.base as skb
17
17
  import sklearn.linear_model as skl
18
- import sklearn.multioutput as skm
18
+ import sklearn.multioutput as skmo
19
+ import sklearn.utils.metadata_routing as skm
19
20
 
20
21
  from skfolio.prior._base import BasePrior, PriorModel
21
22
  from skfolio.prior._empirical import EmpiricalPrior
@@ -74,7 +75,7 @@ class LoadingMatrixRegression(BaseLoadingMatrix):
74
75
  Fitted `sklearn.multioutput.MultiOutputRegressor`
75
76
  """
76
77
 
77
- multi_output_regressor_: skm.MultiOutputRegressor
78
+ multi_output_regressor_: skmo.MultiOutputRegressor
78
79
 
79
80
  def __init__(
80
81
  self,
@@ -84,7 +85,15 @@ class LoadingMatrixRegression(BaseLoadingMatrix):
84
85
  self.linear_regressor = linear_regressor
85
86
  self.n_jobs = n_jobs
86
87
 
87
- def fit(self, X: npt.ArrayLike, y: npt.ArrayLike):
88
+ def get_metadata_routing(self):
89
+ # noinspection PyTypeChecker
90
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
91
+ linear_regressor=self.linear_regressor,
92
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
93
+ )
94
+ return router
95
+
96
+ def fit(self, X: npt.ArrayLike, y: npt.ArrayLike, **fit_params):
88
97
  """Fit the Loading Matrix Regression Estimator.
89
98
 
90
99
  Parameters
@@ -95,21 +104,30 @@ class LoadingMatrixRegression(BaseLoadingMatrix):
95
104
  y : array-like of shape (n_observations, n_factors)
96
105
  Price returns of the factors.
97
106
 
107
+ **fit_params : dict
108
+ Parameters to pass to the underlying estimators.
109
+ Only available if `enable_metadata_routing=True`, which can be
110
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
111
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
112
+ more details.
113
+
98
114
  Returns
99
115
  -------
100
116
  self : LoadingMatrixRegression
101
117
  Fitted estimator.
102
118
  """
119
+ routed_params = skm.process_routing(self, "fit", **fit_params)
120
+
103
121
  _linear_regressor = check_estimator(
104
122
  self.linear_regressor,
105
123
  default=skl.LassoCV(fit_intercept=False),
106
124
  check_type=skb.BaseEstimator,
107
125
  )
108
126
 
109
- self.multi_output_regressor_ = skm.MultiOutputRegressor(
127
+ self.multi_output_regressor_ = skmo.MultiOutputRegressor(
110
128
  _linear_regressor, n_jobs=self.n_jobs
111
129
  )
112
- self.multi_output_regressor_.fit(X=y, y=X)
130
+ self.multi_output_regressor_.fit(X=y, y=X, **routed_params.linear_regressor.fit)
113
131
  # noinspection PyUnresolvedReferences
114
132
  n_assets = X.shape[1]
115
133
  self.loading_matrix_ = np.array(
@@ -196,8 +214,16 @@ class FactorModel(BasePrior):
196
214
  self.higham = higham
197
215
  self.max_iteration = max_iteration
198
216
 
217
+ def get_metadata_routing(self):
218
+ # noinspection PyTypeChecker
219
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
220
+ factor_prior_estimator=self.factor_prior_estimator,
221
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
222
+ )
223
+ return router
224
+
199
225
  # noinspection PyMethodOverriding, PyPep8Naming
200
- def fit(self, X: npt.ArrayLike, y: Any):
226
+ def fit(self, X: npt.ArrayLike, y: Any, **fit_params):
201
227
  """Fit the Factor Model estimator.
202
228
 
203
229
  Parameters
@@ -208,11 +234,20 @@ class FactorModel(BasePrior):
208
234
  y : array-like of shape (n_observations, n_factors)
209
235
  Factors' returns.
210
236
 
237
+ **fit_params : dict
238
+ Parameters to pass to the underlying estimators.
239
+ Only available if `enable_metadata_routing=True`, which can be
240
+ set by using ``sklearn.set_config(enable_metadata_routing=True)``.
241
+ See :ref:`Metadata Routing User Guide <metadata_routing>` for
242
+ more details.
243
+
211
244
  Returns
212
245
  -------
213
246
  self : FactorModel
214
247
  Fitted estimator.
215
248
  """
249
+ routed_params = skm.process_routing(self, "fit", **fit_params)
250
+
216
251
  self.factor_prior_estimator_ = check_estimator(
217
252
  self.factor_prior_estimator,
218
253
  default=EmpiricalPrior(),
@@ -225,7 +260,9 @@ class FactorModel(BasePrior):
225
260
  )
226
261
 
227
262
  # Fitting prior estimator
228
- self.factor_prior_estimator_.fit(y)
263
+ self.factor_prior_estimator_.fit(
264
+ X=y, **routed_params.factor_prior_estimator.fit
265
+ )
229
266
  factor_mu = self.factor_prior_estimator_.prior_model_.mu
230
267
  factor_covariance = self.factor_prior_estimator_.prior_model_.covariance
231
268
 
@@ -10,6 +10,9 @@ from dataclasses import dataclass
10
10
  import numpy as np
11
11
  import numpy.typing as npt
12
12
  import sklearn.base as skb
13
+ import sklearn.utils.metadata_routing as skm
14
+
15
+ from skfolio.prior import BasePrior
13
16
 
14
17
 
15
18
  # frozen=True with eq=False will lead to an id-based hashing which is needed for
@@ -54,13 +57,22 @@ class BaseMuUncertaintySet(skb.BaseEstimator, ABC):
54
57
  """
55
58
 
56
59
  uncertainty_set_: UncertaintySet
60
+ prior_estimator_: BasePrior
57
61
 
58
62
  @abstractmethod
59
- def __init__(self):
60
- pass
63
+ def __init__(self, prior_estimator: BasePrior | None = None):
64
+ self.prior_estimator = prior_estimator
65
+
66
+ def get_metadata_routing(self):
67
+ # noinspection PyTypeChecker
68
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
69
+ prior_estimator=self.prior_estimator,
70
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
71
+ )
72
+ return router
61
73
 
62
74
  @abstractmethod
63
- def fit(self, X: npt.ArrayLike, y=None):
75
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params):
64
76
  pass
65
77
 
66
78
 
@@ -75,14 +87,11 @@ class BaseCovarianceUncertaintySet(skb.BaseEstimator, ABC):
75
87
  """
76
88
 
77
89
  uncertainty_set_: UncertaintySet
90
+ prior_estimator_: BasePrior
78
91
 
79
92
  @abstractmethod
80
- def __init__(self):
81
- pass
82
-
83
- @abstractmethod
84
- def fit(self, X: npt.ArrayLike, y=None):
85
- pass
93
+ def __init__(self, prior_estimator: BasePrior | None = None):
94
+ self.prior_estimator = prior_estimator
86
95
 
87
96
  def _validate_X_y(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
88
97
  """Validate X and y if provided.
@@ -108,3 +117,15 @@ class BaseCovarianceUncertaintySet(skb.BaseEstimator, ABC):
108
117
  else:
109
118
  X, y = self._validate_data(X, y, multi_output=True)
110
119
  return X, y
120
+
121
+ def get_metadata_routing(self):
122
+ # noinspection PyTypeChecker
123
+ router = skm.MetadataRouter(owner=self.__class__.__name__).add(
124
+ prior_estimator=self.prior_estimator,
125
+ method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
126
+ )
127
+ return router
128
+
129
+ @abstractmethod
130
+ def fit(self, X: npt.ArrayLike, y=None, **fit_params):
131
+ pass