skfolio 0.0.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.
- skfolio/__init__.py +29 -0
- skfolio/cluster/__init__.py +8 -0
- skfolio/cluster/_hierarchical.py +387 -0
- skfolio/datasets/__init__.py +20 -0
- skfolio/datasets/_base.py +389 -0
- skfolio/datasets/data/__init__.py +0 -0
- skfolio/datasets/data/factors_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_index.csv.gz +0 -0
- skfolio/distance/__init__.py +26 -0
- skfolio/distance/_base.py +55 -0
- skfolio/distance/_distance.py +574 -0
- skfolio/exceptions.py +30 -0
- skfolio/measures/__init__.py +76 -0
- skfolio/measures/_enums.py +355 -0
- skfolio/measures/_measures.py +607 -0
- skfolio/metrics/__init__.py +3 -0
- skfolio/metrics/_scorer.py +121 -0
- skfolio/model_selection/__init__.py +18 -0
- skfolio/model_selection/_combinatorial.py +407 -0
- skfolio/model_selection/_validation.py +194 -0
- skfolio/model_selection/_walk_forward.py +221 -0
- skfolio/moments/__init__.py +41 -0
- skfolio/moments/covariance/__init__.py +29 -0
- skfolio/moments/covariance/_base.py +101 -0
- skfolio/moments/covariance/_covariance.py +1108 -0
- skfolio/moments/expected_returns/__init__.py +21 -0
- skfolio/moments/expected_returns/_base.py +31 -0
- skfolio/moments/expected_returns/_expected_returns.py +415 -0
- skfolio/optimization/__init__.py +36 -0
- skfolio/optimization/_base.py +147 -0
- skfolio/optimization/cluster/__init__.py +13 -0
- skfolio/optimization/cluster/_nco.py +348 -0
- skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
- skfolio/optimization/cluster/hierarchical/_base.py +440 -0
- skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
- skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
- skfolio/optimization/convex/__init__.py +16 -0
- skfolio/optimization/convex/_base.py +1944 -0
- skfolio/optimization/convex/_distributionally_robust.py +392 -0
- skfolio/optimization/convex/_maximum_diversification.py +417 -0
- skfolio/optimization/convex/_mean_risk.py +974 -0
- skfolio/optimization/convex/_risk_budgeting.py +560 -0
- skfolio/optimization/ensemble/__init__.py +6 -0
- skfolio/optimization/ensemble/_base.py +87 -0
- skfolio/optimization/ensemble/_stacking.py +326 -0
- skfolio/optimization/naive/__init__.py +3 -0
- skfolio/optimization/naive/_naive.py +173 -0
- skfolio/population/__init__.py +3 -0
- skfolio/population/_population.py +883 -0
- skfolio/portfolio/__init__.py +13 -0
- skfolio/portfolio/_base.py +1096 -0
- skfolio/portfolio/_multi_period_portfolio.py +610 -0
- skfolio/portfolio/_portfolio.py +842 -0
- skfolio/pre_selection/__init__.py +7 -0
- skfolio/pre_selection/_pre_selection.py +342 -0
- skfolio/preprocessing/__init__.py +3 -0
- skfolio/preprocessing/_returns.py +114 -0
- skfolio/prior/__init__.py +18 -0
- skfolio/prior/_base.py +63 -0
- skfolio/prior/_black_litterman.py +238 -0
- skfolio/prior/_empirical.py +163 -0
- skfolio/prior/_factor_model.py +268 -0
- skfolio/typing.py +50 -0
- skfolio/uncertainty_set/__init__.py +23 -0
- skfolio/uncertainty_set/_base.py +108 -0
- skfolio/uncertainty_set/_bootstrap.py +281 -0
- skfolio/uncertainty_set/_empirical.py +237 -0
- skfolio/utils/__init__.py +0 -0
- skfolio/utils/bootstrap.py +115 -0
- skfolio/utils/equations.py +350 -0
- skfolio/utils/sorting.py +117 -0
- skfolio/utils/stats.py +466 -0
- skfolio/utils/tools.py +567 -0
- skfolio-0.0.1.dist-info/LICENSE +29 -0
- skfolio-0.0.1.dist-info/METADATA +568 -0
- skfolio-0.0.1.dist-info/RECORD +79 -0
- skfolio-0.0.1.dist-info/WHEEL +5 -0
- skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,883 @@
|
|
1
|
+
""" Population module.
|
2
|
+
A population is a collection of portfolios.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
6
|
+
# License: BSD 3 clause
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
import pandas as pd
|
12
|
+
import plotly.express as px
|
13
|
+
import plotly.graph_objects as go
|
14
|
+
import scipy.interpolate as sci
|
15
|
+
|
16
|
+
import skfolio.typing as skt
|
17
|
+
from skfolio.portfolio import BasePortfolio, MultiPeriodPortfolio, Portfolio
|
18
|
+
from skfolio.utils.sorting import non_denominated_sort
|
19
|
+
from skfolio.utils.tools import deduplicate_names
|
20
|
+
|
21
|
+
pd.options.plotting.backend = "plotly"
|
22
|
+
|
23
|
+
|
24
|
+
class Population(list):
|
25
|
+
"""Population Class.
|
26
|
+
|
27
|
+
A `Population` is a list of :class:`~skfolio.portfolio.Portfolio` or
|
28
|
+
:class:`~skfolio.portfolio.MultiPeriodPortfolio` or both.
|
29
|
+
|
30
|
+
Parameters
|
31
|
+
----------
|
32
|
+
iterable : list[Portfolio | MultiPeriodPortfolio]
|
33
|
+
The list of portfolios. Each item can be of type
|
34
|
+
:class:`~skfolio.portfolio.Portfolio` and/or
|
35
|
+
:class:`~skfolio.portfolio.MultiPeriodPortfolio`.
|
36
|
+
Empty list are accepted.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, iterable: list[Portfolio | MultiPeriodPortfolio]) -> None:
|
40
|
+
super().__init__(self._validate_item(item) for item in iterable)
|
41
|
+
|
42
|
+
def __repr__(self) -> str:
|
43
|
+
return "<Population(" + super().__repr__() + ")>"
|
44
|
+
|
45
|
+
def __getitem__(
|
46
|
+
self, indices: int | list[int] | slice
|
47
|
+
) -> "Portfolio | MultiPeriodPortfolio|Population":
|
48
|
+
item = super().__getitem__(indices)
|
49
|
+
if isinstance(item, list):
|
50
|
+
return self.__class__(item)
|
51
|
+
return item
|
52
|
+
|
53
|
+
def __setitem__(self, index: int, item: Portfolio | MultiPeriodPortfolio) -> None:
|
54
|
+
super().__setitem__(index, self._validate_item(item))
|
55
|
+
|
56
|
+
def __add__(self, other: Portfolio | MultiPeriodPortfolio) -> "Population":
|
57
|
+
if not isinstance(other, Population):
|
58
|
+
raise TypeError(
|
59
|
+
f"Cannot add a Population with an object of type {type(other)}"
|
60
|
+
)
|
61
|
+
return self.__class__(super().__add__(other))
|
62
|
+
|
63
|
+
def insert(self, index, item: Portfolio | MultiPeriodPortfolio) -> None:
|
64
|
+
"""Insert portfolio before index."""
|
65
|
+
super().insert(index, self._validate_item(item))
|
66
|
+
|
67
|
+
def append(self, item: Portfolio | MultiPeriodPortfolio) -> None:
|
68
|
+
"""Append portfolio to the end of the population list."""
|
69
|
+
super().append(self._validate_item(item))
|
70
|
+
|
71
|
+
def extend(self, other: Portfolio | MultiPeriodPortfolio) -> None:
|
72
|
+
"""Extend population list by appending elements from the iterable."""
|
73
|
+
if isinstance(other, type(self)):
|
74
|
+
super().extend(other)
|
75
|
+
else:
|
76
|
+
super().extend(self._validate_item(item) for item in other)
|
77
|
+
|
78
|
+
def set_portfolio_params(self, **params: any) -> "Population":
|
79
|
+
"""Set the parameters of all the portfolios.
|
80
|
+
|
81
|
+
Parameters
|
82
|
+
----------
|
83
|
+
**params : any
|
84
|
+
Portfolio parameters.
|
85
|
+
|
86
|
+
Returns
|
87
|
+
-------
|
88
|
+
self : Population
|
89
|
+
The Population instance.
|
90
|
+
"""
|
91
|
+
if not params:
|
92
|
+
return self
|
93
|
+
init_signature = inspect.signature(BasePortfolio.__init__)
|
94
|
+
# Consider the constructor parameters excluding 'self'
|
95
|
+
valid_params = [
|
96
|
+
p.name
|
97
|
+
for p in init_signature.parameters.values()
|
98
|
+
if p.name != "self" and p.kind != p.VAR_KEYWORD
|
99
|
+
]
|
100
|
+
for key in params:
|
101
|
+
if key not in valid_params:
|
102
|
+
raise ValueError(
|
103
|
+
f"Invalid parameter {key!r} . "
|
104
|
+
f"Valid parameters are: {valid_params!r}."
|
105
|
+
)
|
106
|
+
|
107
|
+
for portfolio in self:
|
108
|
+
for key, value in params.items():
|
109
|
+
setattr(portfolio, key, value)
|
110
|
+
|
111
|
+
@staticmethod
|
112
|
+
def _validate_item(
|
113
|
+
item: Portfolio | MultiPeriodPortfolio,
|
114
|
+
) -> Portfolio | MultiPeriodPortfolio:
|
115
|
+
"""Validate that items are of type Portfolio or MultiPeriodPortfolio."""
|
116
|
+
if isinstance(item, Portfolio | MultiPeriodPortfolio):
|
117
|
+
return item
|
118
|
+
raise TypeError(
|
119
|
+
"Population only accept items of type Portfolio and MultiPeriodPortfolio"
|
120
|
+
f", got {type(item).__name__}"
|
121
|
+
)
|
122
|
+
|
123
|
+
def non_denominated_sort(self, first_front_only: bool = False) -> list[list[int]]:
|
124
|
+
"""Fast non-dominated sorting.
|
125
|
+
Sort the portfolios into different non-domination levels.
|
126
|
+
Complexity O(MN^2) where M is the number of objectives and N the number of
|
127
|
+
portfolios.
|
128
|
+
|
129
|
+
Parameters
|
130
|
+
----------
|
131
|
+
first_front_only : bool, default=False
|
132
|
+
If this is set to True, only the first front is sorted and returned.
|
133
|
+
The default is `False`.
|
134
|
+
|
135
|
+
Returns
|
136
|
+
-------
|
137
|
+
fronts : list[list[int]]
|
138
|
+
A list of Pareto fronts (lists), the first list includes
|
139
|
+
non-dominated portfolios.
|
140
|
+
"""
|
141
|
+
n = len(self)
|
142
|
+
if n > 0 and np.any(
|
143
|
+
[
|
144
|
+
portfolio.fitness_measures != self[0].fitness_measures
|
145
|
+
for portfolio in self
|
146
|
+
]
|
147
|
+
):
|
148
|
+
raise ValueError(
|
149
|
+
"Cannot compute non denominated sorting with Portfolios "
|
150
|
+
"containing mixed `fitness_measures`"
|
151
|
+
)
|
152
|
+
fitnesses = np.array([portfolio.fitness for portfolio in self])
|
153
|
+
fronts = non_denominated_sort(
|
154
|
+
fitnesses=fitnesses, first_front_only=first_front_only
|
155
|
+
)
|
156
|
+
return fronts
|
157
|
+
|
158
|
+
def filter( # noqa: A003
|
159
|
+
self, names: skt.Names | None = None, tags: skt.Tags | None = None
|
160
|
+
) -> "Population":
|
161
|
+
"""Filter the Population of portfolios by names and tags.
|
162
|
+
If both names and tags are provided, the intersection is returned.
|
163
|
+
|
164
|
+
Parameters
|
165
|
+
----------
|
166
|
+
names : str | list[str], optional
|
167
|
+
If provided, the population is filtered by portfolio names.
|
168
|
+
|
169
|
+
tags : str | list[str], optional
|
170
|
+
If provided, the population is filtered by portfolio tags.
|
171
|
+
|
172
|
+
Returns
|
173
|
+
-------
|
174
|
+
population : Population
|
175
|
+
A new population of portfolios filtered by names and tags.
|
176
|
+
"""
|
177
|
+
if tags is None and names is None:
|
178
|
+
return self
|
179
|
+
if isinstance(names, str):
|
180
|
+
names = [names]
|
181
|
+
if isinstance(tags, str):
|
182
|
+
tags = [tags]
|
183
|
+
|
184
|
+
if tags is None:
|
185
|
+
return self.__class__(
|
186
|
+
[portfolio for portfolio in self if portfolio.name in names]
|
187
|
+
)
|
188
|
+
if names is None:
|
189
|
+
return self.__class__(
|
190
|
+
[portfolio for portfolio in self if portfolio.tag in tags]
|
191
|
+
)
|
192
|
+
return self.__class__(
|
193
|
+
[
|
194
|
+
portfolio
|
195
|
+
for portfolio in self
|
196
|
+
if portfolio.name in names and portfolio.tag in tags
|
197
|
+
]
|
198
|
+
)
|
199
|
+
|
200
|
+
def measures(
|
201
|
+
self,
|
202
|
+
measure: skt.Measure,
|
203
|
+
names: skt.Names | None = None,
|
204
|
+
tags: skt.Tags | None = None,
|
205
|
+
) -> np.ndarray:
|
206
|
+
"""Vector of portfolios measures for each portfolio from the
|
207
|
+
population filtered by names and tags.
|
208
|
+
|
209
|
+
Parameters
|
210
|
+
----------
|
211
|
+
measure : Measure
|
212
|
+
The portfolio measure.
|
213
|
+
|
214
|
+
names : str | list[str], optional
|
215
|
+
If provided, the population is filtered by portfolio names.
|
216
|
+
|
217
|
+
tags : str | list[str], optional
|
218
|
+
If provided, the population is filtered by portfolio tags.
|
219
|
+
|
220
|
+
Returns
|
221
|
+
-------
|
222
|
+
values : ndarray
|
223
|
+
The vector of portfolios measures.
|
224
|
+
"""
|
225
|
+
population = self.filter(names=names, tags=tags)
|
226
|
+
return np.array([ptf.__getattribute__(measure.value) for ptf in population])
|
227
|
+
|
228
|
+
def measures_mean(
|
229
|
+
self,
|
230
|
+
measure: skt.Measure,
|
231
|
+
names: skt.Names | None = None,
|
232
|
+
tags: skt.Tags | None = None,
|
233
|
+
) -> float:
|
234
|
+
"""Mean of portfolios measures for each portfolio from the
|
235
|
+
population filtered by names and tags.
|
236
|
+
|
237
|
+
Parameters
|
238
|
+
----------
|
239
|
+
measure : Measure
|
240
|
+
The portfolio measure.
|
241
|
+
|
242
|
+
names : str | list[str], optional
|
243
|
+
If provided, the population is filtered by portfolio names.
|
244
|
+
|
245
|
+
tags : str | list[str], optional
|
246
|
+
If provided, the population is filtered by portfolio tags.
|
247
|
+
|
248
|
+
Returns
|
249
|
+
-------
|
250
|
+
value : float
|
251
|
+
The mean of portfolios measures.
|
252
|
+
"""
|
253
|
+
return self.measures(measure=measure, names=names, tags=tags).mean()
|
254
|
+
|
255
|
+
def measures_std(
|
256
|
+
self,
|
257
|
+
measure: skt.Measure,
|
258
|
+
names: skt.Names | None = None,
|
259
|
+
tags: skt.Tags | None = None,
|
260
|
+
) -> float:
|
261
|
+
"""Standard-deviation of portfolios measures for each portfolio from the
|
262
|
+
population filtered by names and tags.
|
263
|
+
|
264
|
+
Parameters
|
265
|
+
----------
|
266
|
+
measure : Measure
|
267
|
+
The portfolio measure.
|
268
|
+
|
269
|
+
names : str | list[str], optional
|
270
|
+
If provided, the population is filtered by portfolio names.
|
271
|
+
|
272
|
+
tags : str | list[str], optional
|
273
|
+
If provided, the population is filtered by portfolio tags.
|
274
|
+
|
275
|
+
Returns
|
276
|
+
-------
|
277
|
+
value : float
|
278
|
+
The standard-deviation of portfolios measures.
|
279
|
+
"""
|
280
|
+
return self.measures(measure=measure, names=names, tags=tags).std()
|
281
|
+
|
282
|
+
def sort_measure(
|
283
|
+
self,
|
284
|
+
measure: skt.Measure,
|
285
|
+
reverse: bool = False,
|
286
|
+
names: skt.Names | None = None,
|
287
|
+
tags: skt.Tags | None = None,
|
288
|
+
) -> "Population":
|
289
|
+
"""Sort the population by a given portfolio measure and filter the portfolios
|
290
|
+
by names and tags.
|
291
|
+
|
292
|
+
Parameters
|
293
|
+
----------
|
294
|
+
measure : Measure
|
295
|
+
The portfolio measure.
|
296
|
+
|
297
|
+
reverse : bool, default=False
|
298
|
+
If this is set to True, the order is reversed.
|
299
|
+
|
300
|
+
names : str | list[str], optional
|
301
|
+
If provided, the population is filtered by portfolio names.
|
302
|
+
|
303
|
+
tags : str | list[str], optional
|
304
|
+
If provided, the population is filtered by portfolio tags.
|
305
|
+
|
306
|
+
Returns
|
307
|
+
-------
|
308
|
+
values : Populations
|
309
|
+
The sorted population.
|
310
|
+
"""
|
311
|
+
population = self.filter(names=names, tags=tags)
|
312
|
+
return self.__class__(
|
313
|
+
sorted(
|
314
|
+
population,
|
315
|
+
key=lambda x: x.__getattribute__(measure.value),
|
316
|
+
reverse=reverse,
|
317
|
+
)
|
318
|
+
)
|
319
|
+
|
320
|
+
def quantile(
|
321
|
+
self,
|
322
|
+
measure: skt.Measure,
|
323
|
+
q: float,
|
324
|
+
names: skt.Names | None = None,
|
325
|
+
tags: skt.Tags | None = None,
|
326
|
+
) -> Portfolio | MultiPeriodPortfolio:
|
327
|
+
"""Returns the portfolio corresponding to the `q` quantile for a given portfolio
|
328
|
+
measure.
|
329
|
+
|
330
|
+
Parameters
|
331
|
+
----------
|
332
|
+
measure : Measure
|
333
|
+
The portfolio measure.
|
334
|
+
|
335
|
+
q : float
|
336
|
+
The quantile value.
|
337
|
+
|
338
|
+
names : str | list[str], optional
|
339
|
+
If provided, the population is filtered by portfolio names.
|
340
|
+
|
341
|
+
tags : str | list[str], optional
|
342
|
+
If provided, the population is filtered by portfolio tags.
|
343
|
+
|
344
|
+
Returns
|
345
|
+
-------
|
346
|
+
values : Portfolio | MultiPeriodPortfolio
|
347
|
+
Portfolio corresponding to the `q` quantile for the measure.
|
348
|
+
"""
|
349
|
+
if not 0 <= q <= 1:
|
350
|
+
raise ValueError("The quantile`q` must be between 0 and 1")
|
351
|
+
sorted_portfolios = self.sort_measure(
|
352
|
+
measure=measure, reverse=False, names=names, tags=tags
|
353
|
+
)
|
354
|
+
k = max(0, int(np.round(len(sorted_portfolios) * q)) - 1)
|
355
|
+
return sorted_portfolios[k]
|
356
|
+
|
357
|
+
def min_measure(
|
358
|
+
self,
|
359
|
+
measure: skt.Measure,
|
360
|
+
names: skt.Names | None = None,
|
361
|
+
tags: skt.Tags | None = None,
|
362
|
+
) -> Portfolio | MultiPeriodPortfolio:
|
363
|
+
"""Returns the portfolio with the minimum measure.
|
364
|
+
|
365
|
+
Parameters
|
366
|
+
----------
|
367
|
+
measure : Measure
|
368
|
+
The portfolio measure.
|
369
|
+
|
370
|
+
names : str | list[str], optional
|
371
|
+
If provided, the population is filtered by portfolio names.
|
372
|
+
|
373
|
+
tags : str | list[str], optional
|
374
|
+
If provided, the population is filtered by portfolio tags.
|
375
|
+
|
376
|
+
Returns
|
377
|
+
-------
|
378
|
+
values : Portfolio | MultiPeriodPortfolio
|
379
|
+
The portfolio with minimum measure.
|
380
|
+
"""
|
381
|
+
return self.quantile(measure=measure, q=0, names=names, tags=tags)
|
382
|
+
|
383
|
+
def max_measure(
|
384
|
+
self,
|
385
|
+
measure: skt.Measure,
|
386
|
+
names: skt.Names | None = None,
|
387
|
+
tags: skt.Tags | None = None,
|
388
|
+
) -> Portfolio | MultiPeriodPortfolio:
|
389
|
+
"""Returns the portfolio with the maximum measure.
|
390
|
+
|
391
|
+
Parameters
|
392
|
+
----------
|
393
|
+
measure: Measure
|
394
|
+
The portfolio measure.
|
395
|
+
|
396
|
+
names : str | list[str], optional
|
397
|
+
If provided, the population is filtered by portfolio names.
|
398
|
+
|
399
|
+
tags : str | list[str], optional
|
400
|
+
If provided, the population is filtered by portfolio tags.
|
401
|
+
|
402
|
+
Returns
|
403
|
+
-------
|
404
|
+
values : Portfolio | MultiPeriodPortfolio
|
405
|
+
The portfolio with maximum measure.
|
406
|
+
"""
|
407
|
+
return self.quantile(measure=measure, q=1, names=names, tags=tags)
|
408
|
+
|
409
|
+
def summary(
|
410
|
+
self,
|
411
|
+
formatted: bool = True,
|
412
|
+
names: skt.Names | None = None,
|
413
|
+
tags: skt.Tags | None = None,
|
414
|
+
) -> pd.DataFrame:
|
415
|
+
"""Summary of the portfolios in the population
|
416
|
+
|
417
|
+
Parameters
|
418
|
+
----------
|
419
|
+
formatted : bool, default=True
|
420
|
+
If this is set to True, the measures are formatted into rounded string with
|
421
|
+
units.
|
422
|
+
The default is `True`.
|
423
|
+
|
424
|
+
names : str | list[str], optional
|
425
|
+
If provided, the population is filtered by portfolio names.
|
426
|
+
|
427
|
+
tags : str | list[str], optional
|
428
|
+
If provided, the population is filtered by portfolio tags.
|
429
|
+
|
430
|
+
Returns
|
431
|
+
-------
|
432
|
+
summary : pandas DataFrame
|
433
|
+
The population's portfolios summary
|
434
|
+
"""
|
435
|
+
|
436
|
+
portfolios = self.filter(names=names, tags=tags)
|
437
|
+
df = pd.concat(
|
438
|
+
[p.summary(formatted=formatted) for p in portfolios],
|
439
|
+
keys=[p.name for p in portfolios],
|
440
|
+
axis=1,
|
441
|
+
)
|
442
|
+
return df
|
443
|
+
|
444
|
+
def composition(
|
445
|
+
self,
|
446
|
+
names: skt.Names | None = None,
|
447
|
+
tags: skt.Tags | None = None,
|
448
|
+
display_sub_ptf_name: bool = True,
|
449
|
+
) -> pd.DataFrame:
|
450
|
+
"""Composition of the portfolios in the population.
|
451
|
+
|
452
|
+
Parameters
|
453
|
+
----------
|
454
|
+
names : str | list[str], optional
|
455
|
+
If provided, the population is filtered by portfolio names.
|
456
|
+
|
457
|
+
tags : str | list[str], optional
|
458
|
+
If provided, the population is filtered by portfolio tags.
|
459
|
+
|
460
|
+
display_sub_ptf_name : bool, default=True
|
461
|
+
If this is set to True, each sub-portfolio name composing a multi-period
|
462
|
+
portfolio is displayed.
|
463
|
+
|
464
|
+
Returns
|
465
|
+
-------
|
466
|
+
summary : DataFrame
|
467
|
+
Composition of the portfolios in the population.
|
468
|
+
"""
|
469
|
+
portfolios = self.filter(names=names, tags=tags)
|
470
|
+
comp_list = []
|
471
|
+
for p in portfolios:
|
472
|
+
comp = p.composition
|
473
|
+
if display_sub_ptf_name:
|
474
|
+
if isinstance(p, MultiPeriodPortfolio):
|
475
|
+
comp.rename(
|
476
|
+
columns={c: f"{p.name}_{c}" for c in comp.columns}, inplace=True
|
477
|
+
)
|
478
|
+
else:
|
479
|
+
comp.rename(columns={c: p.name for c in comp.columns}, inplace=True)
|
480
|
+
comp_list.append(comp)
|
481
|
+
|
482
|
+
df = pd.concat(comp_list, axis=1)
|
483
|
+
df.columns = deduplicate_names(list(df.columns))
|
484
|
+
df.fillna(0, inplace=True)
|
485
|
+
return df
|
486
|
+
|
487
|
+
def plot_distribution(
|
488
|
+
self,
|
489
|
+
measure_list: list[skt.Measure],
|
490
|
+
tag_list: list[str] | None = None,
|
491
|
+
n_bins: int | None = None,
|
492
|
+
**kwargs,
|
493
|
+
) -> go.Figure:
|
494
|
+
"""Plot the population's distribution for each measure provided in the
|
495
|
+
measure list.
|
496
|
+
|
497
|
+
Parameters
|
498
|
+
----------
|
499
|
+
measure_list : list[Measure]
|
500
|
+
The list of portfolio measures. A different distribution is plotted per
|
501
|
+
measure.
|
502
|
+
|
503
|
+
tag_list : list[str], optional
|
504
|
+
If this is provided, an additional distribution is plotted per measure
|
505
|
+
for each tag provided.
|
506
|
+
|
507
|
+
n_bins : int, optional
|
508
|
+
Sets the number of bins.
|
509
|
+
|
510
|
+
Returns
|
511
|
+
-------
|
512
|
+
plot : Figure
|
513
|
+
Returns the plotly Figure object.
|
514
|
+
"""
|
515
|
+
values = []
|
516
|
+
labels = []
|
517
|
+
for measure in measure_list:
|
518
|
+
if tag_list is not None:
|
519
|
+
for tag in tag_list:
|
520
|
+
values.append(self.measures(measure=measure, tags=tag))
|
521
|
+
labels.append(f"{measure} - {tag}")
|
522
|
+
else:
|
523
|
+
values.append(self.measures(measure=measure))
|
524
|
+
labels.append(measure.value)
|
525
|
+
|
526
|
+
df = pd.DataFrame(np.array(values).T, columns=labels).melt(
|
527
|
+
var_name="Population"
|
528
|
+
)
|
529
|
+
fig = px.histogram(
|
530
|
+
df,
|
531
|
+
color="Population",
|
532
|
+
barmode="overlay",
|
533
|
+
marginal="box",
|
534
|
+
nbins=n_bins,
|
535
|
+
**kwargs,
|
536
|
+
)
|
537
|
+
fig.update_layout(title_text="Measures Distribution", xaxis_title="measures")
|
538
|
+
return fig
|
539
|
+
|
540
|
+
def plot_cumulative_returns(
|
541
|
+
self,
|
542
|
+
log_scale: bool = False,
|
543
|
+
idx: slice | np.ndarray | None = None,
|
544
|
+
names: skt.Names | None = None,
|
545
|
+
tags: skt.Tags | None = None,
|
546
|
+
) -> go.Figure:
|
547
|
+
"""Plot the population's portfolios cumulative returns.
|
548
|
+
Non-compounded cumulative returns start at 0.
|
549
|
+
Compounded cumulative returns are rescaled to start at 1000.
|
550
|
+
|
551
|
+
Parameters
|
552
|
+
----------
|
553
|
+
log_scale : bool, default=False
|
554
|
+
If this is set to True, the cumulative returns are displayed with a
|
555
|
+
logarithm scale on the y-axis and rebased at 1000. The cumulative returns
|
556
|
+
must be compounded otherwise an exception is raise.
|
557
|
+
|
558
|
+
idx : slice | array, optional
|
559
|
+
Indexes or slice of the observations to plot.
|
560
|
+
The default (`None`) is to take all observations.
|
561
|
+
|
562
|
+
names : str | list[str], optional
|
563
|
+
If provided, the population is filtered by portfolio names.
|
564
|
+
|
565
|
+
tags : str | list[str], optional
|
566
|
+
If provided, the population is filtered by portfolio tags.
|
567
|
+
|
568
|
+
Returns
|
569
|
+
-------
|
570
|
+
plot : Figure
|
571
|
+
Returns the plot Figure object.
|
572
|
+
"""
|
573
|
+
if idx is None:
|
574
|
+
idx = slice(None)
|
575
|
+
portfolios = self.filter(names=names, tags=tags)
|
576
|
+
if not portfolios:
|
577
|
+
raise ValueError("No portfolio found")
|
578
|
+
|
579
|
+
cumulative_returns = []
|
580
|
+
names = []
|
581
|
+
compounded = []
|
582
|
+
for ptf in portfolios:
|
583
|
+
cumulative_returns.append(ptf.cumulative_returns_df)
|
584
|
+
names.append(f"{ptf.name}_{ptf.tag}" if ptf.tag is not None else ptf.name)
|
585
|
+
compounded.append(ptf.compounded)
|
586
|
+
compounded = set(compounded)
|
587
|
+
|
588
|
+
if len(compounded) == 2:
|
589
|
+
raise ValueError(
|
590
|
+
"Some portfolios cumulative returns are compounded while some "
|
591
|
+
"are non-compounded. You can change the compounded with"
|
592
|
+
"`population.set_portfolio_params(compounded=False)`",
|
593
|
+
)
|
594
|
+
title = "Cumulative Returns"
|
595
|
+
compounded = compounded.pop()
|
596
|
+
if compounded:
|
597
|
+
yaxis_title = f"{title} (rebased at 1000)"
|
598
|
+
if log_scale:
|
599
|
+
title = f"{title} (compounded & log scaled)"
|
600
|
+
else:
|
601
|
+
title = f"{title} (compounded)"
|
602
|
+
else:
|
603
|
+
if log_scale:
|
604
|
+
raise ValueError(
|
605
|
+
"Plotting with logarithm scaling must be done on cumulative "
|
606
|
+
"returns that are compounded as opposed to non-compounded."
|
607
|
+
"You can change to compounded with "
|
608
|
+
"`set_portfolio_params(compounded=True)`"
|
609
|
+
)
|
610
|
+
yaxis_title = title
|
611
|
+
title = f"{title} (non-compounded)"
|
612
|
+
|
613
|
+
df = pd.concat(cumulative_returns, axis=1).iloc[:, idx]
|
614
|
+
df.columns = deduplicate_names(names)
|
615
|
+
|
616
|
+
fig = df.plot()
|
617
|
+
fig.update_layout(
|
618
|
+
title=title,
|
619
|
+
xaxis_title="Observations",
|
620
|
+
yaxis_title=yaxis_title,
|
621
|
+
legend_title_text="Portfolios",
|
622
|
+
)
|
623
|
+
if compounded:
|
624
|
+
fig.update_yaxes(tickformat=".0f")
|
625
|
+
else:
|
626
|
+
fig.update_yaxes(tickformat=".2%")
|
627
|
+
if log_scale:
|
628
|
+
fig.update_yaxes(type="log")
|
629
|
+
return fig
|
630
|
+
|
631
|
+
def plot_composition(
|
632
|
+
self,
|
633
|
+
names: skt.Names | None = None,
|
634
|
+
tags: skt.Tags | None = None,
|
635
|
+
display_sub_ptf_name: bool = True,
|
636
|
+
) -> go.Figure:
|
637
|
+
"""Plot the compositions of the portfolios in the population.
|
638
|
+
|
639
|
+
Parameters
|
640
|
+
----------
|
641
|
+
names : str | list[str], optional
|
642
|
+
If provided, the population is filtered by portfolio names.
|
643
|
+
|
644
|
+
tags : str | list[str], optional
|
645
|
+
If provided, the population is filtered by portfolio tags.
|
646
|
+
|
647
|
+
display_sub_ptf_name : bool, default=True
|
648
|
+
If this is set to True, each sub-portfolio name composing a multi-period
|
649
|
+
portfolio is displayed.
|
650
|
+
|
651
|
+
Returns
|
652
|
+
-------
|
653
|
+
plot : Figure
|
654
|
+
Returns the plotly Figure object.
|
655
|
+
"""
|
656
|
+
df = self.composition(
|
657
|
+
names=names, tags=tags, display_sub_ptf_name=display_sub_ptf_name
|
658
|
+
).T
|
659
|
+
fig = px.bar(df, x=df.index, y=df.columns)
|
660
|
+
fig.update_layout(
|
661
|
+
title="Portfolios Composition",
|
662
|
+
xaxis={
|
663
|
+
"title": "Portfolios",
|
664
|
+
},
|
665
|
+
yaxis={
|
666
|
+
"title": "Weight",
|
667
|
+
"tickformat": ",.0%",
|
668
|
+
},
|
669
|
+
legend=dict(yanchor="top", y=0.99, xanchor="left", x=1.15),
|
670
|
+
)
|
671
|
+
return fig
|
672
|
+
|
673
|
+
def plot_measures(
|
674
|
+
self,
|
675
|
+
x: skt.Measure,
|
676
|
+
y: skt.Measure,
|
677
|
+
z: skt.Measure = None,
|
678
|
+
to_surface: bool = False,
|
679
|
+
hover_measures: list[skt.Measure] | None = None,
|
680
|
+
show_fronts: bool = False,
|
681
|
+
color_scale: skt.Measure | str | None = None,
|
682
|
+
names: skt.Names | None = None,
|
683
|
+
tags: skt.Tags | None = None,
|
684
|
+
title="Portfolios",
|
685
|
+
) -> go.Figure:
|
686
|
+
"""Plot the 2D (or 3D) scatter points (or surface) of a given set of
|
687
|
+
measures for each portfolio in the population.
|
688
|
+
|
689
|
+
Parameters
|
690
|
+
----------
|
691
|
+
x : Measure
|
692
|
+
The x-axis measure.
|
693
|
+
|
694
|
+
y : Measure
|
695
|
+
The y-axis measure.
|
696
|
+
|
697
|
+
z : Measure, optional
|
698
|
+
The z-axis measure.
|
699
|
+
|
700
|
+
to_surface : bool, default=False
|
701
|
+
If this is set to True, a surface is estimated.
|
702
|
+
|
703
|
+
hover_measures : list[Measure], optional
|
704
|
+
The list of measure to show on point hover.
|
705
|
+
|
706
|
+
show_fronts : bool, default=False
|
707
|
+
If this is set to True, the pareto fronts are highlighted.
|
708
|
+
The default is `False`.
|
709
|
+
|
710
|
+
color_scale : Measure | str, optional
|
711
|
+
If this is provided, a color scale is displayed.
|
712
|
+
|
713
|
+
title : str, default="Portfolios"
|
714
|
+
The graph title. The default value is "Portfolios".
|
715
|
+
|
716
|
+
names : str | list[str], optional
|
717
|
+
If provided, the population is filtered by portfolio names.
|
718
|
+
|
719
|
+
tags : str | list[str], optional
|
720
|
+
If provided, the population is filtered by portfolio tags.
|
721
|
+
|
722
|
+
Returns
|
723
|
+
-------
|
724
|
+
plot : Figure
|
725
|
+
Returns the plotly Figure object.
|
726
|
+
"""
|
727
|
+
portfolios = self.filter(names=names, tags=tags)
|
728
|
+
num_fmt = ":.3f"
|
729
|
+
hover_data = {x: num_fmt, y: num_fmt, "tag": True}
|
730
|
+
|
731
|
+
if z is not None:
|
732
|
+
hover_data[z] = num_fmt
|
733
|
+
|
734
|
+
if hover_measures is not None:
|
735
|
+
for measure in hover_measures:
|
736
|
+
hover_data[measure] = num_fmt
|
737
|
+
|
738
|
+
columns = list(hover_data)
|
739
|
+
columns.append("name")
|
740
|
+
if isinstance(color_scale, skt.Measure):
|
741
|
+
hover_data[color_scale] = num_fmt
|
742
|
+
|
743
|
+
if color_scale is not None and color_scale not in columns:
|
744
|
+
columns.append(color_scale)
|
745
|
+
|
746
|
+
col_values = [e.value if isinstance(e, skt.Measure) else e for e in columns]
|
747
|
+
res = [
|
748
|
+
[portfolio.__getattribute__(attr) for attr in col_values]
|
749
|
+
for portfolio in portfolios
|
750
|
+
]
|
751
|
+
# Improved formatting
|
752
|
+
columns = [str(e) for e in columns]
|
753
|
+
hover_data = {str(k): v for k, v in hover_data.items()}
|
754
|
+
|
755
|
+
df = pd.DataFrame(res, columns=columns)
|
756
|
+
df["tag"] = df["tag"].astype(str).replace("None", "")
|
757
|
+
|
758
|
+
if show_fronts:
|
759
|
+
fronts = self.non_denominated_sort(first_front_only=False)
|
760
|
+
if tags is not None:
|
761
|
+
ValueError("Cannot plot front with tags selected")
|
762
|
+
df["front"] = -1
|
763
|
+
for i, front in enumerate(fronts):
|
764
|
+
for idx in front:
|
765
|
+
df.iloc[idx, -1] = str(i)
|
766
|
+
color = df.columns[-1]
|
767
|
+
elif color_scale is not None:
|
768
|
+
color = str(color_scale)
|
769
|
+
else:
|
770
|
+
color = "tag"
|
771
|
+
|
772
|
+
if z is not None:
|
773
|
+
if to_surface:
|
774
|
+
# estimate the surface
|
775
|
+
x_arr = np.array(df[str(x)])
|
776
|
+
y_arr = np.array(df[str(y)])
|
777
|
+
z_arr = np.array(df[str(z)])
|
778
|
+
|
779
|
+
xi = np.linspace(start=min(x_arr), stop=max(x_arr), num=100)
|
780
|
+
yi = np.linspace(start=min(y_arr), stop=max(y_arr), num=100)
|
781
|
+
|
782
|
+
X, Y = np.meshgrid(xi, yi)
|
783
|
+
Z = sci.griddata(
|
784
|
+
points=(x_arr, y_arr), values=z_arr, xi=(X, Y), method="cubic"
|
785
|
+
)
|
786
|
+
fig = go.Figure(
|
787
|
+
go.Surface(
|
788
|
+
x=xi,
|
789
|
+
y=yi,
|
790
|
+
z=Z,
|
791
|
+
hovertemplate="<br>".join(
|
792
|
+
[
|
793
|
+
str(e)
|
794
|
+
+ ": %{"
|
795
|
+
+ v
|
796
|
+
+ ":"
|
797
|
+
+ (",.3%" if not e.is_ratio else None)
|
798
|
+
+ "}"
|
799
|
+
for e, v in [(x, "x"), (y, "y"), (z, "z")]
|
800
|
+
]
|
801
|
+
)
|
802
|
+
+ "<extra></extra>",
|
803
|
+
colorbar=dict(
|
804
|
+
title=str(z),
|
805
|
+
titleside="top",
|
806
|
+
tickformat=",.2%" if not z.is_ratio else None,
|
807
|
+
),
|
808
|
+
)
|
809
|
+
)
|
810
|
+
|
811
|
+
fig.update_layout(
|
812
|
+
title=title,
|
813
|
+
scene=dict(
|
814
|
+
xaxis={
|
815
|
+
"title": str(x),
|
816
|
+
"tickformat": ",.1%" if not x.is_ratio else None,
|
817
|
+
},
|
818
|
+
yaxis={
|
819
|
+
"title": str(y),
|
820
|
+
"tickformat": ",.1%" if not y.is_ratio else None,
|
821
|
+
},
|
822
|
+
zaxis={
|
823
|
+
"title": str(z),
|
824
|
+
"tickformat": ",.1%" if not z.is_ratio else None,
|
825
|
+
},
|
826
|
+
),
|
827
|
+
)
|
828
|
+
else:
|
829
|
+
# plot the points
|
830
|
+
fig = px.scatter_3d(
|
831
|
+
df,
|
832
|
+
x=str(x),
|
833
|
+
y=str(y),
|
834
|
+
z=str(z),
|
835
|
+
hover_name="name",
|
836
|
+
hover_data=hover_data,
|
837
|
+
color=color,
|
838
|
+
symbol="tag",
|
839
|
+
)
|
840
|
+
fig.update_traces(marker_size=8)
|
841
|
+
fig.update_layout(
|
842
|
+
title=title,
|
843
|
+
scene=dict(
|
844
|
+
xaxis={
|
845
|
+
"title": str(x),
|
846
|
+
"tickformat": ",.1%" if not x.is_ratio else None,
|
847
|
+
},
|
848
|
+
yaxis={
|
849
|
+
"title": str(y),
|
850
|
+
"tickformat": ",.1%" if not y.is_ratio else None,
|
851
|
+
},
|
852
|
+
zaxis={
|
853
|
+
"title": str(z),
|
854
|
+
"tickformat": ",.1%" if not z.is_ratio else None,
|
855
|
+
},
|
856
|
+
),
|
857
|
+
legend=dict(yanchor="top", y=0.99, xanchor="left", x=1.15),
|
858
|
+
)
|
859
|
+
|
860
|
+
else:
|
861
|
+
fig = px.scatter(
|
862
|
+
df,
|
863
|
+
x=str(x),
|
864
|
+
y=str(y),
|
865
|
+
hover_name="name",
|
866
|
+
hover_data=hover_data,
|
867
|
+
color=color,
|
868
|
+
symbol="tag",
|
869
|
+
)
|
870
|
+
fig.update_traces(marker_size=10)
|
871
|
+
fig.update_layout(
|
872
|
+
title=title,
|
873
|
+
xaxis={
|
874
|
+
"title": str(x),
|
875
|
+
"tickformat": ",.1%" if not x.is_ratio else None,
|
876
|
+
},
|
877
|
+
yaxis={
|
878
|
+
"title": str(y),
|
879
|
+
"tickformat": ",.1%" if not y.is_ratio else None,
|
880
|
+
},
|
881
|
+
legend=dict(yanchor="top", y=0.96, xanchor="left", x=1.25),
|
882
|
+
)
|
883
|
+
return fig
|