skfolio 0.9.1__py3-none-any.whl → 0.10.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 (41) hide show
  1. skfolio/distribution/multivariate/_vine_copula.py +35 -34
  2. skfolio/distribution/univariate/_base.py +20 -15
  3. skfolio/exceptions.py +5 -0
  4. skfolio/measures/__init__.py +2 -0
  5. skfolio/measures/_measures.py +392 -155
  6. skfolio/optimization/_base.py +21 -4
  7. skfolio/optimization/cluster/hierarchical/_base.py +16 -13
  8. skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
  9. skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
  10. skfolio/optimization/convex/_base.py +238 -144
  11. skfolio/optimization/convex/_distributionally_robust.py +32 -20
  12. skfolio/optimization/convex/_maximum_diversification.py +15 -15
  13. skfolio/optimization/convex/_mean_risk.py +26 -24
  14. skfolio/optimization/convex/_risk_budgeting.py +23 -21
  15. skfolio/optimization/ensemble/__init__.py +2 -4
  16. skfolio/optimization/ensemble/_stacking.py +1 -1
  17. skfolio/optimization/naive/_naive.py +2 -2
  18. skfolio/population/_population.py +30 -9
  19. skfolio/portfolio/_base.py +68 -26
  20. skfolio/portfolio/_multi_period_portfolio.py +5 -0
  21. skfolio/portfolio/_portfolio.py +5 -0
  22. skfolio/prior/__init__.py +6 -2
  23. skfolio/prior/_base.py +7 -3
  24. skfolio/prior/_black_litterman.py +14 -12
  25. skfolio/prior/_empirical.py +8 -7
  26. skfolio/prior/_entropy_pooling.py +1493 -0
  27. skfolio/prior/_factor_model.py +39 -22
  28. skfolio/prior/_opinion_pooling.py +475 -0
  29. skfolio/prior/_synthetic_data.py +10 -8
  30. skfolio/uncertainty_set/_bootstrap.py +4 -4
  31. skfolio/uncertainty_set/_empirical.py +6 -6
  32. skfolio/utils/equations.py +10 -4
  33. skfolio/utils/figure.py +185 -0
  34. skfolio/utils/tools.py +4 -2
  35. {skfolio-0.9.1.dist-info → skfolio-0.10.1.dist-info}/METADATA +105 -14
  36. {skfolio-0.9.1.dist-info → skfolio-0.10.1.dist-info}/RECORD +40 -38
  37. {skfolio-0.9.1.dist-info → skfolio-0.10.1.dist-info}/WHEEL +1 -1
  38. skfolio/synthetic_returns/__init__.py +0 -1
  39. /skfolio/{optimization/ensemble/_base.py → utils/composition.py} +0 -0
  40. {skfolio-0.9.1.dist-info → skfolio-0.10.1.dist-info}/licenses/LICENSE +0 -0
  41. {skfolio-0.9.1.dist-info → skfolio-0.10.1.dist-info}/top_level.txt +0 -0
@@ -39,6 +39,7 @@
39
39
 
40
40
  import warnings
41
41
  from abc import abstractmethod
42
+ from collections.abc import Callable
42
43
  from typing import ClassVar
43
44
 
44
45
  import numpy as np
@@ -113,6 +114,10 @@ class BasePortfolio:
113
114
  If this is set to True, cumulative returns are compounded.
114
115
  The default is `False`.
115
116
 
117
+ sample_weight : ndarray of shape (n_observations,), optional
118
+ Sample weights for each observation. The weights must sum to one.
119
+ If None, equal weights are assumed.
120
+
116
121
  min_acceptable_return : float, optional
117
122
  The minimum acceptable return used to distinguish "downside" and "upside"
118
123
  returns for the computation of lower partial moments:
@@ -376,6 +381,7 @@ class BasePortfolio:
376
381
  "min_acceptable_return",
377
382
  "compounded",
378
383
  "risk_free_rate",
384
+ "sample_weight",
379
385
  }
380
386
 
381
387
  # Arguments locally used in measures computation
@@ -403,6 +409,7 @@ class BasePortfolio:
403
409
  # custom getter and setter
404
410
  "_fitness_measures",
405
411
  "_annualized_factor",
412
+ "_sample_weight",
406
413
  # custom getter (read-only and cached)
407
414
  "_fitness",
408
415
  "_cumulative_returns",
@@ -485,6 +492,7 @@ class BasePortfolio:
485
492
  fitness_measures: list[skt.Measure] | None = None,
486
493
  risk_free_rate: float = 0.0,
487
494
  compounded: bool = False,
495
+ sample_weight: np.ndarray | None = None,
488
496
  min_acceptable_return: float | None = None,
489
497
  value_at_risk_beta: float = 0.95,
490
498
  entropic_risk_measure_theta: float = 1.0,
@@ -497,6 +505,7 @@ class BasePortfolio:
497
505
  ):
498
506
  self._loaded = False
499
507
  self._annualized_factor = annualized_factor
508
+ self._sample_weight = sample_weight
500
509
  self.returns = np.asarray(returns)
501
510
  self.observations = np.asarray(observations)
502
511
  self.risk_free_rate = risk_free_rate
@@ -652,6 +661,25 @@ class BasePortfolio:
652
661
  self._annualized_factor = value
653
662
  self.clear()
654
663
 
664
+ @property
665
+ def sample_weight(self) -> float:
666
+ """Observations sample weights."""
667
+ return self._sample_weight
668
+
669
+ @sample_weight.setter
670
+ def sample_weight(self, value: np.ndarray | None) -> None:
671
+ if value is not None:
672
+ value = np.asarray(value)
673
+ if value.ndim != 1:
674
+ raise ValueError("sample_weight must be a 1D array.")
675
+ if len(value) != self.n_observations:
676
+ raise ValueError(
677
+ "sample_weight must have the same length as the number of observations."
678
+ )
679
+ if not np.isclose(value.sum(), 1):
680
+ raise ValueError("sample_weight must sum to one.")
681
+ self._sample_weight = value
682
+
655
683
  # Custom attribute getter (read-only and cached)
656
684
  @cached_property_slots
657
685
  def fitness(self) -> np.ndarray:
@@ -738,19 +766,9 @@ class BasePortfolio:
738
766
  # Local measures function arguments need to be defined in the class
739
767
  # attributes with the argument name preceded by the measure name and
740
768
  # separated by "_".
741
- if measure.is_annualized:
742
- func = getattr(mt, str(measure.non_annualized_measure.value))
743
- else:
744
- func = getattr(mt, str(measure.value))
745
769
 
746
- args = {
747
- arg: (
748
- getattr(self, arg)
749
- if arg in self._measure_global_args
750
- else getattr(self, f"{measure.value}_{arg}")
751
- )
752
- for arg in args_names(func)
753
- }
770
+ func, args = self._get_measure_func(measure=measure)
771
+
754
772
  try:
755
773
  value = func(**args)
756
774
  if measure in [
@@ -844,15 +862,7 @@ class BasePortfolio:
844
862
  risk_measure = non_annualized_measure
845
863
 
846
864
  if risk_measure is not None:
847
- risk_func = getattr(mt, str(risk_measure.value))
848
- risk_func_args = {
849
- arg: (
850
- getattr(self, arg)
851
- if arg in self._measure_global_args
852
- else getattr(self, f"{risk_measure.value}_{arg}")
853
- )
854
- for arg in args_names(risk_func)
855
- }
865
+ risk_func, risk_func_args = self._get_measure_func(measure=risk_measure)
856
866
 
857
867
  if "drawdowns" in risk_func_args:
858
868
  del risk_func_args["drawdowns"]
@@ -1032,18 +1042,33 @@ class BasePortfolio:
1032
1042
  )
1033
1043
  return fig
1034
1044
 
1035
- def plot_returns_distribution(self) -> go.Figure:
1045
+ def plot_returns_distribution(
1046
+ self, percentile_cutoff: float | None = None
1047
+ ) -> go.Figure:
1036
1048
  """Plot the Portfolio returns distribution using Gaussian KDE.
1037
1049
 
1050
+ Parameters
1051
+ ----------
1052
+ percentile_cutoff : float, default=None
1053
+ Percentile cutoff for tail truncation (percentile), in percent.
1054
+ If a float p is provided, the distribution support is truncated at the p-th
1055
+ and (100 - p)-th percentiles.
1056
+ If None, no truncation is applied (uses full min/max of returns).
1057
+
1038
1058
  Returns
1039
1059
  -------
1040
1060
  plot : Figure
1041
1061
  Returns the plot Figure object
1042
1062
  """
1043
- lower = np.percentile(self.returns, 1e-1)
1044
- upper = np.percentile(self.returns, 100 - 1e-1)
1063
+ returns = self.returns
1064
+ if percentile_cutoff is None:
1065
+ lower, upper = returns.min(), returns.max()
1066
+ else:
1067
+ lower = np.percentile(returns, percentile_cutoff)
1068
+ upper = np.percentile(returns, 100.0 - percentile_cutoff)
1069
+
1045
1070
  x = np.linspace(lower, upper, 500)
1046
- y = st.gaussian_kde(self.returns)(x)
1071
+ y = st.gaussian_kde(self.returns, weights=self.sample_weight)(x)
1047
1072
 
1048
1073
  fig = go.Figure(
1049
1074
  go.Scatter(
@@ -1090,7 +1115,7 @@ class BasePortfolio:
1090
1115
  fig = rolling.plot(backend="plotly")
1091
1116
  fig.add_hline(
1092
1117
  y=getattr(self, measure.value),
1093
- line_width=1,
1118
+ line_width=1.5,
1094
1119
  line_dash="dash",
1095
1120
  line_color="blue",
1096
1121
  )
@@ -1163,3 +1188,20 @@ class BasePortfolio:
1163
1188
  legend_title_text="Assets",
1164
1189
  )
1165
1190
  return fig
1191
+
1192
+ def _get_measure_func(self, measure: skt.Measure) -> tuple[Callable, dict]:
1193
+ """Return the function and arguments of a given measure."""
1194
+ if measure.is_annualized:
1195
+ func = getattr(mt, str(measure.non_annualized_measure.value))
1196
+ else:
1197
+ func = getattr(mt, str(measure.value))
1198
+
1199
+ args = {}
1200
+ for arg in args_names(func):
1201
+ if arg in self._measure_global_args:
1202
+ args[arg] = getattr(self, arg)
1203
+ elif arg == "biased":
1204
+ args[arg] = False
1205
+ else:
1206
+ args[arg] = getattr(self, f"{measure.value}_{arg}")
1207
+ return func, args
@@ -63,6 +63,9 @@ class MultiPeriodPortfolio(BasePortfolio):
63
63
  If this is set to True, cumulative returns are compounded.
64
64
  The default is `False`.
65
65
 
66
+ sample_weight : ndarray of shape (n_observations,), optional
67
+ Sample weights for each observation. If None, equal weights are assumed.
68
+
66
69
  min_acceptable_return : float, optional
67
70
  The minimum acceptable return used to distinguish "downside" and "upside"
68
71
  returns for the computation of lower partial moments:
@@ -334,6 +337,7 @@ class MultiPeriodPortfolio(BasePortfolio):
334
337
  annualized_factor: float = 252.0,
335
338
  fitness_measures: list[skt.Measure] | None = None,
336
339
  compounded: bool = False,
340
+ sample_weight: np.ndarray | None = None,
337
341
  min_acceptable_return: float | None = None,
338
342
  value_at_risk_beta: float = 0.95,
339
343
  entropic_risk_measure_theta: float = 1,
@@ -354,6 +358,7 @@ class MultiPeriodPortfolio(BasePortfolio):
354
358
  annualized_factor=annualized_factor,
355
359
  fitness_measures=fitness_measures,
356
360
  compounded=compounded,
361
+ sample_weight=sample_weight,
357
362
  min_acceptable_return=min_acceptable_return,
358
363
  value_at_risk_beta=value_at_risk_beta,
359
364
  cvar_beta=cvar_beta,
@@ -147,6 +147,9 @@ class Portfolio(BasePortfolio):
147
147
  If this is set to True, cumulative returns are compounded.
148
148
  The default is `False`.
149
149
 
150
+ sample_weight : ndarray of shape (n_observations,), optional
151
+ Sample weights for each observation. If None, equal weights are assumed.
152
+
150
153
  min_acceptable_return : float, optional
151
154
  The minimum acceptable return used to distinguish "downside" and "upside"
152
155
  returns for the computation of lower partial moments:
@@ -442,6 +445,7 @@ class Portfolio(BasePortfolio):
442
445
  annualized_factor: float = 252,
443
446
  fitness_measures: list[skt.Measure] | None = None,
444
447
  compounded: bool = False,
448
+ sample_weight: np.ndarray | None = None,
445
449
  min_acceptable_return: float | None = None,
446
450
  value_at_risk_beta: float = 0.95,
447
451
  entropic_risk_measure_theta: float = 1,
@@ -541,6 +545,7 @@ class Portfolio(BasePortfolio):
541
545
  tag=tag,
542
546
  fitness_measures=fitness_measures,
543
547
  compounded=compounded,
548
+ sample_weight=sample_weight,
544
549
  risk_free_rate=risk_free_rate,
545
550
  annualized_factor=annualized_factor,
546
551
  min_acceptable_return=min_acceptable_return,
skfolio/prior/__init__.py CHANGED
@@ -1,13 +1,15 @@
1
1
  """Prior module."""
2
2
 
3
- from skfolio.prior._base import BasePrior, PriorModel
3
+ from skfolio.prior._base import BasePrior, ReturnDistribution
4
4
  from skfolio.prior._black_litterman import BlackLitterman
5
5
  from skfolio.prior._empirical import EmpiricalPrior
6
+ from skfolio.prior._entropy_pooling import EntropyPooling
6
7
  from skfolio.prior._factor_model import (
7
8
  BaseLoadingMatrix,
8
9
  FactorModel,
9
10
  LoadingMatrixRegression,
10
11
  )
12
+ from skfolio.prior._opinion_pooling import OpinionPooling
11
13
  from skfolio.prior._synthetic_data import SyntheticData
12
14
 
13
15
  __all__ = [
@@ -15,8 +17,10 @@ __all__ = [
15
17
  "BasePrior",
16
18
  "BlackLitterman",
17
19
  "EmpiricalPrior",
20
+ "EntropyPooling",
18
21
  "FactorModel",
19
22
  "LoadingMatrixRegression",
20
- "PriorModel",
23
+ "OpinionPooling",
24
+ "ReturnDistribution",
21
25
  "SyntheticData",
22
26
  ]
skfolio/prior/_base.py CHANGED
@@ -15,8 +15,8 @@ import sklearn.base as skb
15
15
  # frozen=True with eq=False will lead to an id-based hashing which is needed for
16
16
  # caching CVX models in Optimization without impacting performance
17
17
  @dataclass(frozen=True, eq=False)
18
- class PriorModel:
19
- """Prior model dataclass.
18
+ class ReturnDistribution:
19
+ """Return Distribution dataclass used by the optimization estimators.
20
20
 
21
21
  Attributes
22
22
  ----------
@@ -36,12 +36,16 @@ class PriorModel:
36
36
  (for example in Factor Models). When provided, this cholesky factor is use in
37
37
  some optimizations (for example in mean-variance) to improve performance and
38
38
  convergence. The default is `None`.
39
+
40
+ sample_weight : ndarray of shape (n_observations,), optional
41
+ Sample weights for each observation. If None, equal weights are assumed.
39
42
  """
40
43
 
41
44
  mu: np.ndarray
42
45
  covariance: np.ndarray
43
46
  returns: np.ndarray
44
47
  cholesky: np.ndarray | None = None
48
+ sample_weight: np.ndarray | None = None
45
49
 
46
50
 
47
51
  class BasePrior(skb.BaseEstimator, ABC):
@@ -54,7 +58,7 @@ class BasePrior(skb.BaseEstimator, ABC):
54
58
  arguments (no ``*args`` or ``**kwargs``).
55
59
  """
56
60
 
57
- prior_model_: PriorModel
61
+ return_distribution_: ReturnDistribution
58
62
 
59
63
  @abstractmethod
60
64
  def __init__(self):
@@ -1,4 +1,4 @@
1
- """Black & Litterman Prior Model estimator."""
1
+ """Black & Litterman estimator."""
2
2
 
3
3
  # Copyright (c) 2023
4
4
  # Author: Hugo Delatte <delatte.hugo@gmail.com>
@@ -13,14 +13,14 @@ import sklearn.utils.metadata_routing as skm
13
13
  import sklearn.utils.validation as skv
14
14
 
15
15
  from skfolio.moments import EquilibriumMu
16
- from skfolio.prior._base import BasePrior, PriorModel
16
+ from skfolio.prior._base import BasePrior, ReturnDistribution
17
17
  from skfolio.prior._empirical import EmpiricalPrior
18
18
  from skfolio.utils.equations import equations_to_matrix
19
19
  from skfolio.utils.tools import check_estimator, input_to_array
20
20
 
21
21
 
22
22
  class BlackLitterman(BasePrior):
23
- """Black & Litterman Prior Model estimator.
23
+ """Black & Litterman estimator.
24
24
 
25
25
  The Black & Litterman model [1]_ takes a Bayesian approach by using a prior estimate
26
26
  of the assets expected returns and covariance matrix, which are updated using the
@@ -59,9 +59,9 @@ class BlackLitterman(BasePrior):
59
59
  * groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
60
60
 
61
61
  prior_estimator : BasePrior, optional
62
- The assets' :ref:`prior model estimator <prior>`. It is used to estimate
63
- the :class:`~skfolio.prior.PriorModel` containing the estimation of the assets
64
- expected returns, covariance matrix, returns and Cholesky decomposition.
62
+ The assets' :ref:`prior estimator <prior>`. It is used to estimate
63
+ the :class:`~skfolio.prior.ReturnDistribution` containing the estimation of the
64
+ assets expected returns, covariance matrix, returns and Cholesky decomposition.
65
65
  The default (`None`) is to use `EmpiricalPrior(mu_estimator=EquilibriumMu())`.
66
66
 
67
67
  tau : float, default=0.05
@@ -81,8 +81,10 @@ class BlackLitterman(BasePrior):
81
81
 
82
82
  Attributes
83
83
  ----------
84
- prior_model_ : PriorModel
85
- The :class:`~skfolio.prior.PriorModel`.
84
+ return_distribution_ : ReturnDistribution
85
+ Fitted :class:`~skfolio.prior.ReturnDistribution` to be used by the optimization
86
+ estimators, containing the asset returns distribution and posterior Black &
87
+ Litterman moments estimation.
86
88
 
87
89
  groups_ : ndarray of shape(n_groups, n_assets)
88
90
  Assets names and groups converted to an 2D array.
@@ -179,9 +181,9 @@ class BlackLitterman(BasePrior):
179
181
  # fitting prior estimator
180
182
  self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
181
183
 
182
- prior_mu = self.prior_estimator_.prior_model_.mu
183
- prior_covariance = self.prior_estimator_.prior_model_.covariance
184
- prior_returns = self.prior_estimator_.prior_model_.returns
184
+ prior_mu = self.prior_estimator_.return_distribution_.mu
185
+ prior_covariance = self.prior_estimator_.return_distribution_.covariance
186
+ prior_returns = self.prior_estimator_.return_distribution_.returns
185
187
 
186
188
  # we validate after all models have been fitted to keep features names
187
189
  # information.
@@ -260,7 +262,7 @@ class BlackLitterman(BasePrior):
260
262
  + self.tau * prior_covariance
261
263
  - _v @ np.linalg.solve(_a, _v.T)
262
264
  )
263
- self.prior_model_ = PriorModel(
265
+ self.return_distribution_ = ReturnDistribution(
264
266
  mu=posterior_mu, covariance=posterior_covariance, returns=prior_returns
265
267
  )
266
268
  return self
@@ -1,4 +1,4 @@
1
- """Empirical Prior Model estimator."""
1
+ """Empirical Prior estimator."""
2
2
 
3
3
  # Copyright (c) 2023
4
4
  # Author: Hugo Delatte <delatte.hugo@gmail.com>
@@ -10,15 +10,15 @@ import sklearn.utils.metadata_routing as skm
10
10
  import sklearn.utils.validation as skv
11
11
 
12
12
  from skfolio.moments import BaseCovariance, BaseMu, EmpiricalCovariance, EmpiricalMu
13
- from skfolio.prior._base import BasePrior, PriorModel
13
+ from skfolio.prior._base import BasePrior, ReturnDistribution
14
14
  from skfolio.utils.tools import check_estimator
15
15
 
16
16
 
17
17
  class EmpiricalPrior(BasePrior):
18
18
  """Empirical Prior estimator.
19
19
 
20
- The Empirical Prior estimates the :class:`~skfolio.prior.PriorModel` by fitting a
21
- `mu_estimator` and a `covariance_estimator` separately.
20
+ The Empirical Prior estimates the :class:`~skfolio.prior.ReturnDistribution` by
21
+ fitting a `mu_estimator` and a `covariance_estimator` separately.
22
22
 
23
23
  Parameters
24
24
  ----------
@@ -50,8 +50,9 @@ class EmpiricalPrior(BasePrior):
50
50
 
51
51
  Attributes
52
52
  ----------
53
- prior_model_ : PriorModel
54
- The assets :class:`~skfolio.prior.PriorModel`.
53
+ return_distribution_ : ReturnDistribution
54
+ Fitted :class:`~skfolio.prior.ReturnDistribution` to be used by the optimization
55
+ estimators, containing the asset returns distribution and moments estimation.
55
56
 
56
57
  mu_estimator_ : BaseMu
57
58
  Fitted `mu_estimator`.
@@ -194,7 +195,7 @@ class EmpiricalPrior(BasePrior):
194
195
  # we validate and convert to numpy after all models have been fitted to keep
195
196
  # features names information.
196
197
  X = skv.validate_data(self, X)
197
- self.prior_model_ = PriorModel(
198
+ self.return_distribution_ = ReturnDistribution(
198
199
  mu=mu,
199
200
  covariance=covariance,
200
201
  returns=X,