skfolio 0.1.3__py3-none-any.whl → 0.2.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.
@@ -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,17 +314,20 @@ 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]
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
+ )
324
323
  return pd.Series(
325
324
  {
326
- "Number of Observations": n_samples,
325
+ "Number of Observations": n_observations,
327
326
  "Total Number of Folds": self.n_folds,
328
327
  "Number of Test Folds": self.n_test_folds,
329
328
  "Purge Size": self.purged_size,
330
329
  "Embargo Size": self.embargo_size,
331
- "Average Training Size": int(
332
- n_samples / self.n_folds * (self.n_folds - self.n_test_folds)
333
- ),
330
+ "Average Training Size": int(avg_train_size),
334
331
  "Number of Test Paths": self.n_test_paths,
335
332
  "Number of Training Combinations": self.n_splits,
336
333
  }
@@ -410,3 +407,155 @@ class CombinatorialPurgedCV(BaseCombinatorialCV):
410
407
  )
411
408
 
412
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]
@@ -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
 
@@ -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
  ----------
@@ -204,8 +204,8 @@ class HierarchicalRiskParity(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
  ----------
@@ -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
 
@@ -208,8 +208,8 @@ class DistributionallyRobustCVaR(ConvexOptimization):
208
208
  portfolio_params : dict, optional
209
209
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
210
210
  `score` methods. If not provided, the `name`, `transaction_costs`,
211
- `management_fees` and `previous_weights` are copied from the optimization
212
- model and systematically passed to the portfolio.
211
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
212
+ optimization model and passed to the portfolio.
213
213
 
214
214
  Attributes
215
215
  ----------
@@ -303,8 +303,8 @@ class MaximumDiversification(MeanRisk):
303
303
  portfolio_params : dict, optional
304
304
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
305
305
  `score` methods. If not provided, the `name`, `transaction_costs`,
306
- `management_fees` and `previous_weights` are copied from the optimization
307
- model and systematically passed to the portfolio.
306
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
307
+ optimization model and passed to the portfolio.
308
308
 
309
309
  Attributes
310
310
  ----------
@@ -510,8 +510,8 @@ class MeanRisk(ConvexOptimization):
510
510
  portfolio_params : dict, optional
511
511
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
512
512
  `score` methods. If not provided, the `name`, `transaction_costs`,
513
- `management_fees` and `previous_weights` are copied from the optimization
514
- model and systematically passed to the portfolio.
513
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
514
+ optimization model and passed to the portfolio.
515
515
 
516
516
  Attributes
517
517
  ----------
@@ -340,8 +340,8 @@ class RiskBudgeting(ConvexOptimization):
340
340
  portfolio_params : dict, optional
341
341
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
342
342
  `score` methods. If not provided, the `name`, `transaction_costs`,
343
- `management_fees` and `previous_weights` are copied from the optimization
344
- model and systematically passed to the portfolio.
343
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
344
+ optimization model and passed to the portfolio.
345
345
 
346
346
  Attributes
347
347
  ----------
@@ -31,8 +31,8 @@ class InverseVolatility(BaseOptimization):
31
31
  portfolio_params : dict, optional
32
32
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
33
33
  `score` methods. If not provided, the `name`, `transaction_costs`,
34
- `management_fees` and `previous_weights` are copied from the optimization
35
- model and systematically passed to the portfolio.
34
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
35
+ optimization model and passed to the portfolio.
36
36
 
37
37
  Attributes
38
38
  ----------
@@ -95,8 +95,8 @@ class EqualWeighted(BaseOptimization):
95
95
  portfolio_params : dict, optional
96
96
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
97
97
  `score` methods. If not provided, the `name`, `transaction_costs`,
98
- `management_fees` and `previous_weights` are copied from the optimization
99
- model and systematically passed to the portfolio.
98
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
99
+ optimization model and passed to the portfolio.
100
100
 
101
101
  Attributes
102
102
  ----------
@@ -139,8 +139,8 @@ class Random(BaseOptimization):
139
139
  portfolio_params : dict, optional
140
140
  Portfolio parameters passed to the portfolio evaluated by the `predict` and
141
141
  `score` methods. If not provided, the `name`, `transaction_costs`,
142
- `management_fees` and `previous_weights` are copied from the optimization
143
- model and systematically passed to the portfolio.
142
+ `management_fees`, `previous_weights` and `risk_free_rate` are copied from the
143
+ optimization model and passed to the portfolio.
144
144
 
145
145
  Attributes
146
146
  ----------
@@ -7,6 +7,7 @@ A population is a collection of portfolios.
7
7
  # License: BSD 3 clause
8
8
 
9
9
  import inspect
10
+ from typing import Any
10
11
 
11
12
  import numpy as np
12
13
  import pandas as pd
@@ -15,7 +16,7 @@ import plotly.graph_objects as go
15
16
  import scipy.interpolate as sci
16
17
 
17
18
  import skfolio.typing as skt
18
- from skfolio.portfolio import BasePortfolio, MultiPeriodPortfolio, Portfolio
19
+ from skfolio.portfolio import BasePortfolio, MultiPeriodPortfolio
19
20
  from skfolio.utils.sorting import non_denominated_sort
20
21
  from skfolio.utils.tools import deduplicate_names
21
22
 
@@ -30,14 +31,14 @@ class Population(list):
30
31
 
31
32
  Parameters
32
33
  ----------
33
- iterable : list[Portfolio | MultiPeriodPortfolio]
34
+ iterable : list[BasePortfolio]
34
35
  The list of portfolios. Each item can be of type
35
36
  :class:`~skfolio.portfolio.Portfolio` and/or
36
37
  :class:`~skfolio.portfolio.MultiPeriodPortfolio`.
37
38
  Empty list are accepted.
38
39
  """
39
40
 
40
- def __init__(self, iterable: list[Portfolio | MultiPeriodPortfolio]) -> None:
41
+ def __init__(self, iterable: list[BasePortfolio]) -> None:
41
42
  super().__init__(self._validate_item(item) for item in iterable)
42
43
 
43
44
  def __repr__(self) -> str:
@@ -45,43 +46,43 @@ class Population(list):
45
46
 
46
47
  def __getitem__(
47
48
  self, indices: int | list[int] | slice
48
- ) -> "Portfolio | MultiPeriodPortfolio|Population":
49
+ ) -> "BasePortfolio | Population":
49
50
  item = super().__getitem__(indices)
50
51
  if isinstance(item, list):
51
52
  return self.__class__(item)
52
53
  return item
53
54
 
54
- def __setitem__(self, index: int, item: Portfolio | MultiPeriodPortfolio) -> None:
55
+ def __setitem__(self, index: int, item: BasePortfolio) -> None:
55
56
  super().__setitem__(index, self._validate_item(item))
56
57
 
57
- def __add__(self, other: Portfolio | MultiPeriodPortfolio) -> "Population":
58
+ def __add__(self, other: BasePortfolio) -> "Population":
58
59
  if not isinstance(other, Population):
59
60
  raise TypeError(
60
61
  f"Cannot add a Population with an object of type {type(other)}"
61
62
  )
62
63
  return self.__class__(super().__add__(other))
63
64
 
64
- def insert(self, index, item: Portfolio | MultiPeriodPortfolio) -> None:
65
+ def insert(self, index, item: BasePortfolio) -> None:
65
66
  """Insert portfolio before index."""
66
67
  super().insert(index, self._validate_item(item))
67
68
 
68
- def append(self, item: Portfolio | MultiPeriodPortfolio) -> None:
69
+ def append(self, item: BasePortfolio) -> None:
69
70
  """Append portfolio to the end of the population list."""
70
71
  super().append(self._validate_item(item))
71
72
 
72
- def extend(self, other: Portfolio | MultiPeriodPortfolio) -> None:
73
+ def extend(self, other: BasePortfolio) -> None:
73
74
  """Extend population list by appending elements from the iterable."""
74
75
  if isinstance(other, type(self)):
75
76
  super().extend(other)
76
77
  else:
77
78
  super().extend(self._validate_item(item) for item in other)
78
79
 
79
- def set_portfolio_params(self, **params: any) -> "Population":
80
+ def set_portfolio_params(self, **params: Any) -> "Population":
80
81
  """Set the parameters of all the portfolios.
81
82
 
82
83
  Parameters
83
84
  ----------
84
- **params : any
85
+ **params : Any
85
86
  Portfolio parameters.
86
87
 
87
88
  Returns
@@ -111,13 +112,14 @@ class Population(list):
111
112
 
112
113
  @staticmethod
113
114
  def _validate_item(
114
- item: Portfolio | MultiPeriodPortfolio,
115
- ) -> Portfolio | MultiPeriodPortfolio:
115
+ item: BasePortfolio,
116
+ ) -> BasePortfolio:
116
117
  """Validate that items are of type Portfolio or MultiPeriodPortfolio."""
117
- if isinstance(item, Portfolio | MultiPeriodPortfolio):
118
+ if isinstance(item, BasePortfolio):
118
119
  return item
119
120
  raise TypeError(
120
- "Population only accept items of type Portfolio and MultiPeriodPortfolio"
121
+ "Population only accept items that inherit from BasePortfolio such as "
122
+ "Portfolio or MultiPeriodPortfolio"
121
123
  f", got {type(item).__name__}"
122
124
  )
123
125
 
@@ -324,7 +326,7 @@ class Population(list):
324
326
  q: float,
325
327
  names: skt.Names | None = None,
326
328
  tags: skt.Tags | None = None,
327
- ) -> Portfolio | MultiPeriodPortfolio:
329
+ ) -> BasePortfolio:
328
330
  """Returns the portfolio corresponding to the `q` quantile for a given portfolio
329
331
  measure.
330
332
 
@@ -344,7 +346,7 @@ class Population(list):
344
346
 
345
347
  Returns
346
348
  -------
347
- values : Portfolio | MultiPeriodPortfolio
349
+ values : BasePortfolio
348
350
  Portfolio corresponding to the `q` quantile for the measure.
349
351
  """
350
352
  if not 0 <= q <= 1:
@@ -360,7 +362,7 @@ class Population(list):
360
362
  measure: skt.Measure,
361
363
  names: skt.Names | None = None,
362
364
  tags: skt.Tags | None = None,
363
- ) -> Portfolio | MultiPeriodPortfolio:
365
+ ) -> BasePortfolio:
364
366
  """Returns the portfolio with the minimum measure.
365
367
 
366
368
  Parameters
@@ -376,7 +378,7 @@ class Population(list):
376
378
 
377
379
  Returns
378
380
  -------
379
- values : Portfolio | MultiPeriodPortfolio
381
+ values : BasePortfolio
380
382
  The portfolio with minimum measure.
381
383
  """
382
384
  return self.quantile(measure=measure, q=0, names=names, tags=tags)
@@ -386,7 +388,7 @@ class Population(list):
386
388
  measure: skt.Measure,
387
389
  names: skt.Names | None = None,
388
390
  tags: skt.Tags | None = None,
389
- ) -> Portfolio | MultiPeriodPortfolio:
391
+ ) -> BasePortfolio:
390
392
  """Returns the portfolio with the maximum measure.
391
393
 
392
394
  Parameters
@@ -402,7 +404,7 @@ class Population(list):
402
404
 
403
405
  Returns
404
406
  -------
405
- values : Portfolio | MultiPeriodPortfolio
407
+ values : BasePortfolio
406
408
  The portfolio with maximum measure.
407
409
  """
408
410
  return self.quantile(measure=measure, q=1, names=names, tags=tags)
@@ -156,7 +156,7 @@ class BlackLitterman(BasePrior):
156
156
  check_type=BasePrior,
157
157
  )
158
158
  # fitting prior estimator
159
- self.prior_estimator_.fit(X)
159
+ self.prior_estimator_.fit(X, y)
160
160
 
161
161
  prior_mu = self.prior_estimator_.prior_model_.mu
162
162
  prior_covariance = self.prior_estimator_.prior_model_.covariance
@@ -120,11 +120,11 @@ class EmpiricalPrior(BasePrior):
120
120
  "`is_log_normal` is `False`"
121
121
  )
122
122
  # Expected returns
123
- self.mu_estimator_.fit(X)
123
+ self.mu_estimator_.fit(X, y)
124
124
  mu = self.mu_estimator_.mu_
125
125
 
126
126
  # Covariance
127
- self.covariance_estimator_.fit(X)
127
+ self.covariance_estimator_.fit(X, y)
128
128
  covariance = self.covariance_estimator_.covariance_
129
129
  else:
130
130
  if self.investment_horizon is None:
@@ -134,14 +134,15 @@ class EmpiricalPrior(BasePrior):
134
134
  )
135
135
  # Convert linear returns to log returns
136
136
  X_log = np.log(1 + X)
137
+ y_log = np.log(1 + y) if y is not None else None
137
138
 
138
139
  # Estimates the moments on the log returns
139
140
  # Expected returns
140
- self.mu_estimator_.fit(X_log)
141
+ self.mu_estimator_.fit(X_log, y_log)
141
142
  mu = self.mu_estimator_.mu_
142
143
 
143
144
  # Covariance
144
- self.covariance_estimator_.fit(X_log)
145
+ self.covariance_estimator_.fit(X_log, y_log)
145
146
  covariance = self.covariance_estimator_.covariance_
146
147
 
147
148
  # Using the property of aggregation across time we scale this distribution
@@ -9,6 +9,7 @@
9
9
  # Grisel Licensed under BSD 3 clause.
10
10
 
11
11
  from abc import ABC, abstractmethod
12
+ from typing import Any
12
13
 
13
14
  import numpy as np
14
15
  import numpy.typing as npt
@@ -196,7 +197,7 @@ class FactorModel(BasePrior):
196
197
  self.max_iteration = max_iteration
197
198
 
198
199
  # noinspection PyMethodOverriding, PyPep8Naming
199
- def fit(self, X: npt.ArrayLike, y: any):
200
+ def fit(self, X: npt.ArrayLike, y: Any):
200
201
  """Fit the Factor Model estimator.
201
202
 
202
203
  Parameters
skfolio/utils/tools.py CHANGED
@@ -10,6 +10,7 @@
10
10
  from collections.abc import Callable, Iterator
11
11
  from enum import Enum
12
12
  from functools import wraps
13
+ from typing import Any
13
14
 
14
15
  import numpy as np
15
16
  import numpy.typing as npt
@@ -41,7 +42,7 @@ class AutoEnum(str, Enum):
41
42
 
42
43
  @staticmethod
43
44
  def _generate_next_value_(
44
- name: str, start: int, count: int, last_values: any
45
+ name: str, start: int, count: int, last_values: Any
45
46
  ) -> str:
46
47
  """Overriding `auto()`"""
47
48
  return name.lower()
@@ -179,7 +180,7 @@ def args_names(func: object) -> list[str]:
179
180
 
180
181
 
181
182
  def check_estimator(
182
- estimator: skb.BaseEstimator | None, default: skb.BaseEstimator, check_type: any
183
+ estimator: skb.BaseEstimator | None, default: skb.BaseEstimator, check_type: Any
183
184
  ):
184
185
  """Check the estimator type and returns its cloned version it provided, otherwise
185
186
  return the default estimator.
@@ -192,7 +193,7 @@ def check_estimator(
192
193
  default : BaseEstimator
193
194
  Default estimator to return when `estimator` is `None`.
194
195
 
195
- check_type : any
196
+ check_type : Any
196
197
  Expected type of the estimator to check against.
197
198
 
198
199
  Returns
@@ -211,7 +212,7 @@ def check_estimator(
211
212
  def input_to_array(
212
213
  items: dict | npt.ArrayLike,
213
214
  n_assets: int,
214
- fill_value: any,
215
+ fill_value: Any,
215
216
  dim: int,
216
217
  assets_names: np.ndarray | None,
217
218
  name: str,
@@ -228,7 +229,7 @@ def input_to_array(
228
229
  Expected number of assets.
229
230
  Used to verify the shape of the converted array.
230
231
 
231
- fill_value : any
232
+ fill_value : Any
232
233
  When `items` is a dictionary, elements that are not in `asset_names` are filled
233
234
  with `fill_value` in the converted array.
234
235
 
@@ -424,7 +425,7 @@ def safe_split(
424
425
 
425
426
 
426
427
  def fit_single_estimator(
427
- estimator: any,
428
+ estimator: Any,
428
429
  X: npt.ArrayLike,
429
430
  y: npt.ArrayLike | None = None,
430
431
  indices: np.ndarray | None = None,
@@ -463,7 +464,7 @@ def fit_single_estimator(
463
464
 
464
465
 
465
466
  def fit_and_predict(
466
- estimator: any,
467
+ estimator: Any,
467
468
  X: npt.ArrayLike,
468
469
  y: npt.ArrayLike | None,
469
470
  train: np.ndarray,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skfolio
3
- Version: 0.1.3
3
+ Version: 0.2.1
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>
@@ -84,37 +84,37 @@ Requires-Dist: ruff ; extra == 'tests'
84
84
 
85
85
  .. -*- mode: rst -*-
86
86
 
87
- |Licence|_ |Codecov|_ |Black|_ |PythonVersion|_ |PyPi|_ |CI/CD|_ |Downloads|_ |Ruff|_ |Contribution|_ |Website|_
87
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website|
88
88
 
89
89
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
90
- .. _Licence: https://github.com/skfolio/skfolio/blob/main/LICENSE
90
+ :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
91
91
 
92
92
  .. |Codecov| image:: https://codecov.io/gh/skfolio/skfolio/graph/badge.svg?token=KJ0SE4LHPV
93
- .. _Codecov: https://codecov.io/gh/skfolio/skfolio
93
+ :target: https://codecov.io/gh/skfolio/skfolio
94
94
 
95
- .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue
96
- .. _PythonVersion: https://pypi.org/project/skfolio/
95
+ .. |PythonVersion| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg
96
+ :target: https://pypi.org/project/skfolio/
97
97
 
98
98
  .. |PyPi| image:: https://img.shields.io/pypi/v/skfolio
99
- .. _PyPi: https://pypi.org/project/skfolio
99
+ :target: https://pypi.org/project/skfolio
100
100
 
101
101
  .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
102
- .. _Black: https://github.com/psf/black
102
+ :target: https://github.com/psf/black
103
103
 
104
- .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml?logo=github
105
- .. _CI/CD: https://github.com/skfolio/skfolio/raw/main/LICENSE
104
+ .. |CI/CD| image:: https://img.shields.io/github/actions/workflow/status/skfolio/skfolio/release.yml.svg?logo=github
105
+ :target: https://github.com/skfolio/skfolio/raw/main/LICENSE
106
106
 
107
107
  .. |Downloads| image:: https://static.pepy.tech/badge/skfolio
108
- .. _Downloads: https://pepy.tech/project/skfolio
108
+ :target: https://pepy.tech/project/skfolio
109
109
 
110
110
  .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
111
- .. _Ruff: https://github.com/astral-sh/ruff
111
+ :target: https://github.com/astral-sh/ruff
112
112
 
113
113
  .. |Contribution| image:: https://img.shields.io/badge/Contributions-Welcome-blue
114
- .. _Contribution: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
114
+ :target: https://github.com/skfolio/skfolio/blob/main/CONTRIBUTING.md
115
115
 
116
- .. |Website| image:: https://img.shields.io/website-up-down-53cc0d-red/http/skfolio.org
117
- .. _Website: https://skfolio.org
116
+ .. |Website| image:: https://img.shields.io/website.svg?down_color=red&down_message=down&up_color=53cc0d&up_message=up&url=https://skfolio.org
117
+ :target: https://skfolio.org
118
118
 
119
119
  .. |PythonMinVersion| replace:: 3.10
120
120
  .. |NumpyMinVersion| replace:: 1.23.4
@@ -563,7 +563,7 @@ Black & Litterman Factor Model
563
563
  ------------------------------
564
564
  .. code-block:: python
565
565
 
566
- factor_views = ["MTUM - QUAL == 0.03 ", "SIZE - TLT == 0.04", "VLUE == 0.06"]
566
+ factor_views = ["MTUM - QUAL == 0.03 ", "VLUE == 0.06"]
567
567
  model = MeanRisk(
568
568
  objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
569
569
  prior_estimator=FactorModel(
@@ -17,8 +17,8 @@ skfolio/measures/_enums.py,sha256=NJcngwg9b2JMMiekwkWU9POfnDvgfUgtYtyV2VSFDVM,89
17
17
  skfolio/measures/_measures.py,sha256=VU9tahJKJvt8PaJZNxWkmFkFU4PsN0FfYO32Je6D53E,16829
18
18
  skfolio/metrics/__init__.py,sha256=MomHJ5_bgjq4qUwGS2bfhNmG_ld0oQ4wK6y0Yy_Eonc,75
19
19
  skfolio/metrics/_scorer.py,sha256=h1VuZk-zzn4rIChHl9FvM7RxqVT3b-jR1CEB-cr9F2s,4306
20
- skfolio/model_selection/__init__.py,sha256=QYYm5lYyioZuPnsTu-b3lz-tCcl3Gwrx-y9IIvep13A,453
21
- skfolio/model_selection/_combinatorial.py,sha256=Dts2eic69ZTa8HOLcf3n1MyfakA_HxRT7JWn5WTXGlQ,14782
20
+ skfolio/model_selection/__init__.py,sha256=8j9Z5tpbgBScjFbn8ZsCm_6rZO7RkPQ1QIF8BqYMVA8,507
21
+ skfolio/model_selection/_combinatorial.py,sha256=DHfbnjqaOGV5bT_9pGuoR23JN6iEp8Tn0Kr6sLq1Xmg,19045
22
22
  skfolio/model_selection/_validation.py,sha256=Rs3j6LFfm8HJCnaoUXlPnNsqxIyjRmMhmGWvvJ0olXA,7686
23
23
  skfolio/model_selection/_walk_forward.py,sha256=pbMD8VM1OoKiEtrlPeCLG29xVijsq1RArSjTvGO1FnU,7539
24
24
  skfolio/moments/__init__.py,sha256=BaP6FjU-CEsV_mcC2AOKXNFy-_wXZsk-jqzJG6mTpsM,746
@@ -29,26 +29,26 @@ skfolio/moments/expected_returns/__init__.py,sha256=NETEcKKYKsQvzUU4ZIq6mgT14zdr
29
29
  skfolio/moments/expected_returns/_base.py,sha256=xk9mzi48uCOHaMTGQBMr3FU7Ai_shxYhmGeOsVwjv9Q,871
30
30
  skfolio/moments/expected_returns/_expected_returns.py,sha256=T_tt_dsYgXrjIM2REUii7_4_R8IEc3xdtuv45yYM-5o,13388
31
31
  skfolio/optimization/__init__.py,sha256=vXIbwWJL48zJ1jy7ZB2PPBVx7rZo0vVA8QQQuD-L6ts,1021
32
- skfolio/optimization/_base.py,sha256=O8jYu9Tv1IHIHvvcRXBCa-rjPnhKYvVn_Up6QI2ymig,5668
32
+ skfolio/optimization/_base.py,sha256=LoRONJP70AwbFpdgqVS_g145pCx0JGkazjWvkQzT_iM,5748
33
33
  skfolio/optimization/cluster/__init__.py,sha256=M3xVdYhNKp4e9CB7hzb4yjTxkkNCHh7Mt_KGFFrkOgs,388
34
34
  skfolio/optimization/cluster/_nco.py,sha256=MKzNvDIAbqS_D41Vp7tNa7OrXYq_bYVSQEVbVCHOEWA,14834
35
35
  skfolio/optimization/cluster/hierarchical/__init__.py,sha256=YnfcPHvjwB6kcG4hoQqc0NqIJKaG7OjBtmXNbOxCq08,405
36
- skfolio/optimization/cluster/hierarchical/_base.py,sha256=0FtVcrMOFsQIJVaShf_qoHBRSuVmR4qzmJdvJc214HI,17255
37
- skfolio/optimization/cluster/hierarchical/_herc.py,sha256=JyXYX4PE17alCzIv6piF-G7_nprgzdzfYp35kjAeWMo,17235
38
- skfolio/optimization/cluster/hierarchical/_hrp.py,sha256=CSBfOk47uv_9tLdWTI2dnh2UXDqy8iXdBbc4rK7G7-0,16120
36
+ skfolio/optimization/cluster/hierarchical/_base.py,sha256=BVJ8Yq2Yg3Z0iqgx0HwS2OxPLVruoSzrelyYQmt_AiQ,17281
37
+ skfolio/optimization/cluster/hierarchical/_herc.py,sha256=p7VdEeJOD5ykA1H9FOLaEqIL6D_YYF8VXXfAqTSRdjs,17238
38
+ skfolio/optimization/cluster/hierarchical/_hrp.py,sha256=i3bliOjWxDX3V3vekhl_Gf_MZ4HroU4r_3rAR-QnyK0,16123
39
39
  skfolio/optimization/convex/__init__.py,sha256=F6BPFikTo0B-7JCKazqLGEwM3RkgTNbFm5GAGkaq9Uo,570
40
- skfolio/optimization/convex/_base.py,sha256=CyEP2JXP0ISnEZRsV-tumH9wwPGBbyM6MDf1B-jCxyw,75413
41
- skfolio/optimization/convex/_distributionally_robust.py,sha256=nQW_MD0wlVY_tvLvRq_CvezOn4q92YC7wHfzFE9k9tY,17308
42
- skfolio/optimization/convex/_maximum_diversification.py,sha256=8wQn5eFEat5wixmu0cfaswAee8D5C-hzV4s5ntUoAFM,19137
43
- skfolio/optimization/convex/_mean_risk.py,sha256=ivAjLXis4fUZ9XOvh_iOemfnvgDAzrK3LDvrlaa2TpA,43130
44
- skfolio/optimization/convex/_risk_budgeting.py,sha256=wXCgn64w2alAGG-dGJ8IwFrhYVtybM-O2hXiFaXietg,23618
40
+ skfolio/optimization/convex/_base.py,sha256=xvhgSQ5X7Lqz6kzGtldlDLIMV61gwrphW9_qMQSG498,75439
41
+ skfolio/optimization/convex/_distributionally_robust.py,sha256=PHAZvXb_zRXpeDD90fgxVEQAuYH5EY8fTpYQ8jO9shA,17311
42
+ skfolio/optimization/convex/_maximum_diversification.py,sha256=vqUJZdR28X75LQ18XXWLGd7yX9-f5wvOByN0UVSyd_Y,19140
43
+ skfolio/optimization/convex/_mean_risk.py,sha256=q9aH_thsmKeK_3-8jQOLN1S9z9DXvZmYGgj0a2ezAl0,43134
44
+ skfolio/optimization/convex/_risk_budgeting.py,sha256=pRMTA-CCTsV-yysADA3b9RDjLGPk4Jw0eP5ZVQ5W_sc,23622
45
45
  skfolio/optimization/ensemble/__init__.py,sha256=8TXxcxH2_gG3C1xtgQj9OHHr0Le8lhdejtlURL6T3ZY,158
46
46
  skfolio/optimization/ensemble/_base.py,sha256=GaNDQu6ivosYuwMrb-b0PhToCsNrmhSYyXkxeM8W4rU,3399
47
47
  skfolio/optimization/ensemble/_stacking.py,sha256=2PodXf_JahToShSAbwQRXosEer3ESOAhpsnB6SCwztk,13148
48
48
  skfolio/optimization/naive/__init__.py,sha256=Dkr55R48urC-jfYN007NTbei16N91Na_EDYLVqzhGgQ,147
49
- skfolio/optimization/naive/_naive.py,sha256=sadsU8TnNRf28EDLg-KZ4yJm2FvV204vdsb1MhVJyMQ,5561
49
+ skfolio/optimization/naive/_naive.py,sha256=lXBUK0-JZd_30xUqnwv3IN1-yvKNVrub2A4ZVdfjmnY,5570
50
50
  skfolio/population/__init__.py,sha256=rsPPMUv95aTK7vmpPeQwF8NzFuBwk6RDo5g4HNaPzNM,80
51
- skfolio/population/_population.py,sha256=O5o8OmR7szAA6NRKvELyq2P6B14jHMUaB8LMbrS3b7E,29411
51
+ skfolio/population/_population.py,sha256=DNBjR2Ob_Rz3B70mwLIrWd_ElOvHxDWjN3EQs-rGHCk,29148
52
52
  skfolio/portfolio/__init__.py,sha256=YYtcAPmA2zeCxFGTXegg2FXcA7py6CxOX7IMTdYuXl0,586
53
53
  skfolio/portfolio/_base.py,sha256=jO6YKXu8oxE-uldl2CayLvbA40yfeCA1vNa_xm-WcWo,38313
54
54
  skfolio/portfolio/_multi_period_portfolio.py,sha256=63RwL0oTn-3L5LON6f2e9J58zw9q8S2gQ5c8JfSnN28,22759
@@ -59,9 +59,9 @@ skfolio/preprocessing/__init__.py,sha256=15A1bzfPsbfxxXgGP1gstf4R0E_347Wn18z5W5j
59
59
  skfolio/preprocessing/_returns.py,sha256=_7UtXugQPWitNrrZ3M2dUOAun8aVr0lI45Ms6KFiS94,3826
60
60
  skfolio/prior/__init__.py,sha256=jql8NTiWlykPKJUXTOPdqm531mP8Pul1QAR6hXTXA6c,446
61
61
  skfolio/prior/_base.py,sha256=Dx6rX0X6ymDiieFOI-ik3xMNNFhYEtwLSXOdajf5wZY,1927
62
- skfolio/prior/_black_litterman.py,sha256=JuwVXLXY5qHETBZDg0wmBGMSQehJp_1t7c1-kanKaiU,9397
63
- skfolio/prior/_empirical.py,sha256=YhCYW8R3vqs3ntF4WSokuwLW6Gd98df7f8LL8zCS-D0,5740
64
- skfolio/prior/_factor_model.py,sha256=feQzb04-BjBN-ryba5niAhI3RRuRAcUxD8yuiNOIWsI,9642
62
+ skfolio/prior/_black_litterman.py,sha256=sVx8113xeP4B6LA4rICKp0cgw7w3F46aQzIQY_34QwQ,9400
63
+ skfolio/prior/_empirical.py,sha256=eHBSXVzIEvA2oBAnv6HW9D8iy3xKDxpCN_xsg8j8Ye8,5821
64
+ skfolio/prior/_factor_model.py,sha256=xC-womM7oC91jRaIQP7MSiRPkwm5b7pkWHi-5mOb2jY,9665
65
65
  skfolio/uncertainty_set/__init__.py,sha256=LlMHtYv9G9fgtM7m4sCSToS9et57Pm2Q2gGchTVrj6c,617
66
66
  skfolio/uncertainty_set/_base.py,sha256=rZ3g2AhDKFQTPajgh6Fz5S5TTf0qM4Ie6RGxPhp32D8,3301
67
67
  skfolio/uncertainty_set/_bootstrap.py,sha256=Rig8ZGc8rEOnYygPyTaDSu5lp3wGuZrQVRoJTm3UuAA,10301
@@ -71,11 +71,11 @@ skfolio/utils/bootstrap.py,sha256=3zY2kO_GQURKEcQMCasJOSByde9Mt2IAi3KJH0_a4mk,35
71
71
  skfolio/utils/equations.py,sha256=w0HsYjA7cS0mHYsI9MpixHLkof3HN26nc14ZfqFrHlE,11047
72
72
  skfolio/utils/sorting.py,sha256=lSjMvH2L-sSj-06B3MlwBrH1rtjCeGEe4hG894W7TE0,3504
73
73
  skfolio/utils/stats.py,sha256=IP36nMc5j5Hcqjbg7lvDIsGp1GWRdOh5jU3W6Z8nkYs,13132
74
- skfolio/utils/tools.py,sha256=roj4zTwmfunPb8HtxcOPAsCKpk0ZPEPnQztSglE9t4o,15351
74
+ skfolio/utils/tools.py,sha256=xa42f7U3Ki8-CJS6g8w7bKCLI_QMJ8D6LxLBjlEM7Ok,15374
75
75
  skfolio/utils/fixes/__init__.py,sha256=knHau8PRZP07XDHR59CW8VWxkpTP0gdr6RAHJrO-zaA,95
76
76
  skfolio/utils/fixes/_dendrogram.py,sha256=9aIhSnMwpQHJhQx7IpXC3jlw6YJ3H4XQnnx_d4nMllQ,13551
77
- skfolio-0.1.3.dist-info/LICENSE,sha256=F6Gi-ZJX5BlVzYK8R9NcvAkAsKa7KO29xB1OScbrH6Q,1526
78
- skfolio-0.1.3.dist-info/METADATA,sha256=JXNH0_kBjWWR-hp4XQfTtLWP5KIQW5i4pN7SK7lFpYY,19567
79
- skfolio-0.1.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
80
- skfolio-0.1.3.dist-info/top_level.txt,sha256=NXEaoS9Ms7t32gxkb867nV0OKlU0KmssL7IJBVo0fJs,8
81
- skfolio-0.1.3.dist-info/RECORD,,
77
+ skfolio-0.2.1.dist-info/LICENSE,sha256=F6Gi-ZJX5BlVzYK8R9NcvAkAsKa7KO29xB1OScbrH6Q,1526
78
+ skfolio-0.2.1.dist-info/METADATA,sha256=OzW5QNHb_EG_jHtP5lPqWas8EAMyl6FPgS3XP6CXulQ,19585
79
+ skfolio-0.2.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
80
+ skfolio-0.2.1.dist-info/top_level.txt,sha256=NXEaoS9Ms7t32gxkb867nV0OKlU0KmssL7IJBVo0fJs,8
81
+ skfolio-0.2.1.dist-info/RECORD,,