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.
Files changed (79) hide show
  1. skfolio/__init__.py +29 -0
  2. skfolio/cluster/__init__.py +8 -0
  3. skfolio/cluster/_hierarchical.py +387 -0
  4. skfolio/datasets/__init__.py +20 -0
  5. skfolio/datasets/_base.py +389 -0
  6. skfolio/datasets/data/__init__.py +0 -0
  7. skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  8. skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  9. skfolio/datasets/data/sp500_index.csv.gz +0 -0
  10. skfolio/distance/__init__.py +26 -0
  11. skfolio/distance/_base.py +55 -0
  12. skfolio/distance/_distance.py +574 -0
  13. skfolio/exceptions.py +30 -0
  14. skfolio/measures/__init__.py +76 -0
  15. skfolio/measures/_enums.py +355 -0
  16. skfolio/measures/_measures.py +607 -0
  17. skfolio/metrics/__init__.py +3 -0
  18. skfolio/metrics/_scorer.py +121 -0
  19. skfolio/model_selection/__init__.py +18 -0
  20. skfolio/model_selection/_combinatorial.py +407 -0
  21. skfolio/model_selection/_validation.py +194 -0
  22. skfolio/model_selection/_walk_forward.py +221 -0
  23. skfolio/moments/__init__.py +41 -0
  24. skfolio/moments/covariance/__init__.py +29 -0
  25. skfolio/moments/covariance/_base.py +101 -0
  26. skfolio/moments/covariance/_covariance.py +1108 -0
  27. skfolio/moments/expected_returns/__init__.py +21 -0
  28. skfolio/moments/expected_returns/_base.py +31 -0
  29. skfolio/moments/expected_returns/_expected_returns.py +415 -0
  30. skfolio/optimization/__init__.py +36 -0
  31. skfolio/optimization/_base.py +147 -0
  32. skfolio/optimization/cluster/__init__.py +13 -0
  33. skfolio/optimization/cluster/_nco.py +348 -0
  34. skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
  35. skfolio/optimization/cluster/hierarchical/_base.py +440 -0
  36. skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
  37. skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
  38. skfolio/optimization/convex/__init__.py +16 -0
  39. skfolio/optimization/convex/_base.py +1944 -0
  40. skfolio/optimization/convex/_distributionally_robust.py +392 -0
  41. skfolio/optimization/convex/_maximum_diversification.py +417 -0
  42. skfolio/optimization/convex/_mean_risk.py +974 -0
  43. skfolio/optimization/convex/_risk_budgeting.py +560 -0
  44. skfolio/optimization/ensemble/__init__.py +6 -0
  45. skfolio/optimization/ensemble/_base.py +87 -0
  46. skfolio/optimization/ensemble/_stacking.py +326 -0
  47. skfolio/optimization/naive/__init__.py +3 -0
  48. skfolio/optimization/naive/_naive.py +173 -0
  49. skfolio/population/__init__.py +3 -0
  50. skfolio/population/_population.py +883 -0
  51. skfolio/portfolio/__init__.py +13 -0
  52. skfolio/portfolio/_base.py +1096 -0
  53. skfolio/portfolio/_multi_period_portfolio.py +610 -0
  54. skfolio/portfolio/_portfolio.py +842 -0
  55. skfolio/pre_selection/__init__.py +7 -0
  56. skfolio/pre_selection/_pre_selection.py +342 -0
  57. skfolio/preprocessing/__init__.py +3 -0
  58. skfolio/preprocessing/_returns.py +114 -0
  59. skfolio/prior/__init__.py +18 -0
  60. skfolio/prior/_base.py +63 -0
  61. skfolio/prior/_black_litterman.py +238 -0
  62. skfolio/prior/_empirical.py +163 -0
  63. skfolio/prior/_factor_model.py +268 -0
  64. skfolio/typing.py +50 -0
  65. skfolio/uncertainty_set/__init__.py +23 -0
  66. skfolio/uncertainty_set/_base.py +108 -0
  67. skfolio/uncertainty_set/_bootstrap.py +281 -0
  68. skfolio/uncertainty_set/_empirical.py +237 -0
  69. skfolio/utils/__init__.py +0 -0
  70. skfolio/utils/bootstrap.py +115 -0
  71. skfolio/utils/equations.py +350 -0
  72. skfolio/utils/sorting.py +117 -0
  73. skfolio/utils/stats.py +466 -0
  74. skfolio/utils/tools.py +567 -0
  75. skfolio-0.0.1.dist-info/LICENSE +29 -0
  76. skfolio-0.0.1.dist-info/METADATA +568 -0
  77. skfolio-0.0.1.dist-info/RECORD +79 -0
  78. skfolio-0.0.1.dist-info/WHEEL +5 -0
  79. skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,115 @@
1
+ """Bootstrap module."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ import numpy as np
7
+
8
+ __all__ = ["stationary_bootstrap"]
9
+
10
+
11
+ def optimal_block_size(x: np.ndarray) -> float:
12
+ """Compute the optimal block size for a single series using Politis & White
13
+ algorithm [1]_.
14
+
15
+ Parameters
16
+ ----------
17
+ x : ndarray
18
+ The input 1D-array.
19
+
20
+ Returns
21
+ -------
22
+ value : float
23
+ The optimal block size.
24
+
25
+ References
26
+ ----------
27
+ .. [1] "Automatic Block-Length Selection for the Dependent Bootstrap".
28
+ Politis & White (2004).
29
+
30
+ .. [2] "Correction to Automatic Block-Length Selection for the Dependent Bootstrap".
31
+ Patton, Politis & White (2009).
32
+ """
33
+ n = x.shape[0]
34
+ eps = x - x.mean(0)
35
+ b_max = np.ceil(min(3 * np.sqrt(n), n / 3))
36
+ kn = max(5, int(np.log10(n)))
37
+ m_max = int(np.ceil(np.sqrt(n))) + kn
38
+ cv = 2 * np.sqrt(np.log10(n) / n)
39
+ acv = np.zeros(m_max + 1)
40
+ abs_acorr = np.zeros(m_max + 1)
41
+ opt_m = None
42
+ for i in range(m_max + 1):
43
+ v1 = eps[i + 1 :] @ eps[i + 1 :]
44
+ v2 = eps[: -(i + 1)] @ eps[: -(i + 1)]
45
+ cross_prod = eps[i:] @ eps[: n - i]
46
+ acv[i] = cross_prod / n
47
+ abs_acorr[i] = np.abs(cross_prod) / np.sqrt(v1 * v2)
48
+ if i >= kn:
49
+ if np.all(abs_acorr[i - kn : i] < cv) and opt_m is None:
50
+ opt_m = i - kn
51
+ m = 2 * max(opt_m, 1) if opt_m is not None else m_max
52
+ m = min(m, m_max)
53
+ g = 0.0
54
+ lr_acv = acv[0]
55
+ for k in range(1, m + 1):
56
+ lam = 1 if k / m <= 1 / 2 else 2 * (1 - k / m)
57
+ g += 2 * lam * k * acv[k]
58
+ lr_acv += 2 * lam * acv[k]
59
+ d = 2 * lr_acv**2
60
+ b = ((2 * g**2) / d) ** (1 / 3) * n ** (1 / 3)
61
+ b = min(b, b_max)
62
+ return b
63
+
64
+
65
+ def stationary_bootstrap(
66
+ returns: np.ndarray,
67
+ n_bootstrap_samples: int,
68
+ block_size: float | None = None,
69
+ seed: int | None = None,
70
+ ) -> np.ndarray:
71
+ """Creates `n_bootstrap_samples` samples from a multivariate return series via
72
+ stationary bootstrapping.
73
+
74
+ Parameters
75
+ ----------
76
+ returns: ndarray of shape (n_observations, n_assets)
77
+ The returns array.
78
+
79
+ n_bootstrap_samples: int
80
+ The number of bootstrap samples to generate.
81
+
82
+ block_size: float, optional
83
+ The block size.
84
+ If this is set to None, we estimate the optimal block size using Politis &
85
+ White algorithm for all individual asset and the median.
86
+
87
+ seed: int, optional
88
+ Random seed used to initialize the pseudo-random number generator
89
+
90
+ Returns
91
+ -------
92
+ value: ndarray
93
+ The sample returns of shape (reps, nb observations, nb assets)
94
+
95
+ """
96
+ np.random.seed(seed=seed)
97
+ n_observations, n_assets = returns.shape
98
+ x = np.vstack((returns, returns))
99
+ # Loop over reps bootstraps
100
+ if block_size is None:
101
+ block_size = np.median(
102
+ [optimal_block_size(returns[:, i]) for i in range(n_assets)]
103
+ )
104
+
105
+ indices = np.random.randint(
106
+ n_observations, size=(n_bootstrap_samples, n_observations)
107
+ )
108
+ cond = np.random.rand(n_bootstrap_samples, n_observations) >= 1.0 / block_size
109
+ # TODO: don't use loop
110
+ for i in range(n_bootstrap_samples):
111
+ for j in range(1, n_observations):
112
+ if cond[i, j]:
113
+ indices[i, j] = indices[i, j - 1] + 1
114
+ indices[indices > 2 * n_observations] = 0
115
+ return x[indices, :]
@@ -0,0 +1,350 @@
1
+ """Equation module"""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ import re
7
+ import warnings
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+
12
+ from skfolio.exceptions import EquationToMatrixError, GroupNotFoundError
13
+
14
+ __all__ = ["equations_to_matrix"]
15
+
16
+
17
+ def equations_to_matrix(
18
+ groups: npt.ArrayLike,
19
+ equations: npt.ArrayLike,
20
+ sum_to_one: bool = False,
21
+ raise_if_group_missing: bool = False,
22
+ names: tuple[str, str] = ("groups", "equations"),
23
+ ) -> tuple[np.ndarray, np.ndarray]:
24
+ """Convert a list of linear equations into the left and right matrices of the
25
+ inequality A <= B.
26
+
27
+ Parameters
28
+ ----------
29
+ groups : array-like of shape (n_groups, n_assets)
30
+ 2D array of assets groups.
31
+
32
+ Examples:
33
+ groups = np.array(
34
+ [
35
+ ["SPX", "SX5E", "NKY", "TLT"],
36
+ ["Equity", "Equity", "Equity", "Bond"],
37
+ ["US", "Europe", "Japan", "US"],
38
+ ]
39
+ )
40
+
41
+ equations : array-like of shape (n_equations,)
42
+ 1D array of equations.
43
+
44
+ Example of valid equation patterns:
45
+ * "number_1 * group_1 + number_3 <= number_4 * group_3 + number_5"
46
+ * "group_1 >= number * group_2"
47
+ * "group_1 <= number"
48
+ * "group_1 >= number"
49
+
50
+ "group_1" and "group_2" are the group names defined in `groups`.
51
+ The second expression means that the sum of all assets in "group_1" should be
52
+ less or equal to "number" times the sum of all assets in "group_2".
53
+
54
+ Examples:
55
+ equations = [
56
+ "Equity <= 3 * Bond",
57
+ "US >= 1.5",
58
+ "Europe >= 0.5 * Japan",
59
+ "Japan <= 1",
60
+ "3*SPX + 5*SX5E <= 2*TLT + 3",
61
+ ]
62
+
63
+ sum_to_one : bool
64
+ If this is set to True, all elements in a group sum to one (used in the `views`
65
+ of the Black-Litterman model).
66
+
67
+ raise_if_group_missing : bool, default=False
68
+ If this is set to True, an error is raised when a group is not found in the
69
+ groups, otherwise only a warning is shown.
70
+ The default is False.
71
+
72
+ names : tuple[str, str], default=('groups', 'equations')
73
+ The group and equation names used in error messages.
74
+ The default is `('groups', 'equations')`.
75
+
76
+ Returns
77
+ -------
78
+ left: ndarray of shape (n_equations, n_assets)
79
+ right: ndarray of shape (n_equations,)
80
+ The left and right matrices of the inequality A <= B.
81
+ If none of the group inside the equations are part of the groups, `None` is
82
+ returned.
83
+ """
84
+ groups = np.asarray(groups)
85
+ equations = np.asarray(equations)
86
+ if groups.ndim != 2:
87
+ raise ValueError(
88
+ f"`{names[0]}` must be a 2D array, got {groups.ndim}D array instead."
89
+ )
90
+ if equations.ndim != 1:
91
+ raise ValueError(
92
+ f"`{names[1]}` must be a 1D array, got {equations.ndim}D array instead."
93
+ )
94
+
95
+ n_equations = len(equations)
96
+ n_assets = groups.shape[1]
97
+ a = np.zeros((n_equations, n_assets))
98
+ b = np.zeros(n_equations)
99
+ for i, string in enumerate(equations):
100
+ try:
101
+ left, right = _string_to_equation(
102
+ groups=groups,
103
+ string=string,
104
+ sum_to_one=sum_to_one,
105
+ )
106
+ a[i] = left
107
+ b[i] = right
108
+ except GroupNotFoundError as e:
109
+ if raise_if_group_missing:
110
+ raise
111
+ warnings.warn(str(e), stacklevel=2)
112
+ return a, b
113
+
114
+
115
+ def _matching_array(values: np.ndarray, key: str, sum_to_one: bool) -> np.ndarray:
116
+ """Takes in a 2D array of strings, a key string, and a boolean flag.
117
+ It returns a 1D array where the value is 1 if there is a match between the key and
118
+ any value in the 2D array, and 0 otherwise. The returned array can be scaled to
119
+ have a sum of one if the flag is set to True.
120
+
121
+ Parameters
122
+ ----------
123
+ values : ndarray of shape (n, m)
124
+ 2D-array of strings.
125
+
126
+ key : str
127
+ String to match in the values.
128
+
129
+ sum_to_one : bool
130
+ If this is set to True, the matching 1D-array is scaled to have a sum of one.
131
+
132
+ Returns
133
+ -------
134
+ matching_array : ndarray of shape (n, )
135
+ Matching 1D-array.
136
+ """
137
+ arr = np.any(values == key, axis=0)
138
+ if not arr.any():
139
+ raise EquationToMatrixError(f"Unable to find '{key}' in '{values}'")
140
+ if sum_to_one:
141
+ s = np.sum(arr)
142
+ else:
143
+ s = 1
144
+ return arr / s
145
+
146
+
147
+ _operator_mapping = {">=": -1, "<=": 1, "==": 1, "=": 1}
148
+ _operator_signs = {"+": 1, "-": -1}
149
+
150
+
151
+ def _inequality_operator_sign(operator: str) -> int:
152
+ """Convert the operators '>=', "==" and '<=' into the corresponding integer
153
+ values -1, 1 and 1, respectively.
154
+
155
+ Parameters
156
+ ----------
157
+ operator : str
158
+ Operator: '>=' or '<='.
159
+
160
+ Returns
161
+ -------
162
+ value : int
163
+ Operator sign: 1 or -1.
164
+ """
165
+ try:
166
+ return _operator_mapping[operator]
167
+ except KeyError:
168
+ raise EquationToMatrixError(
169
+ f"operator '{operator}' is not valid. It should be '<=' or '>='"
170
+ ) from None
171
+
172
+
173
+ def _operator_sign(operator: str) -> int:
174
+ """Convert the operators '+' and '-' into 1 or -1
175
+
176
+ Parameters
177
+ ----------
178
+ operator : str
179
+ Operator: '+' and '-'.
180
+
181
+ Returns
182
+ -------
183
+ value : int
184
+ Operator sign: 1 or -1.
185
+ """
186
+ try:
187
+ return _operator_signs[operator]
188
+ except KeyError:
189
+ raise EquationToMatrixError(
190
+ f"operator '{operator}' is not valid. It should be be '+' or '-'"
191
+ ) from None
192
+
193
+
194
+ def _string_to_float(string: str) -> float:
195
+ """Convert the factor string into a float.
196
+
197
+ Parameters
198
+ ----------
199
+ string : str
200
+ The factor string.
201
+
202
+ Returns
203
+ -------
204
+ value : int
205
+ The factor string converted to float.
206
+ """
207
+ try:
208
+ return float(string)
209
+ except ValueError:
210
+ raise EquationToMatrixError(f"Unable to convert {string} into float") from None
211
+
212
+
213
+ def _string_to_equation(
214
+ groups: np.ndarray,
215
+ string: str,
216
+ sum_to_one: bool,
217
+ ) -> tuple[np.ndarray, float]:
218
+ """Convert a string to a left 1D-array and right float of the form:
219
+ `groups @ left <= right`.
220
+
221
+ Parameters
222
+ ----------
223
+ groups : ndarray of shape (n_groups, n_assets)
224
+ Groups 2D-array
225
+
226
+ string : str
227
+ String to convert
228
+
229
+ sum_to_one : bool
230
+ If this is set to True, the 1D-array is scaled to have a sum of one.
231
+
232
+ Returns
233
+ -------
234
+ left: 1D-array of shape (n_assets,)
235
+ right: float
236
+ """
237
+ n = groups.shape[1]
238
+ operators = ["-", "+", "*", ">=", "<=", "==", "="]
239
+ invalid_operators = [">", "<"]
240
+ pattern = re.compile(r"((?:" + "|\\".join(operators) + r"))")
241
+ invalid_pattern = re.compile(r"((?:" + "|\\".join(invalid_operators) + r"))")
242
+ err_msg = f"Wrong pattern encountered while converting the string '{string}'"
243
+
244
+ res = re.split(pattern, string)
245
+ res = [x.strip() for x in res]
246
+ res = [x for x in res if x != ""]
247
+ iterator = iter(res)
248
+ group_names = set(groups.flatten())
249
+
250
+ def is_group(name: str) -> bool:
251
+ return name in group_names
252
+
253
+ left = np.zeros(n)
254
+ right = 0
255
+ main_sign = 1
256
+ inequality_sign = None
257
+ e = next(iterator, None)
258
+ i = 0
259
+ while True:
260
+ i += 1
261
+ if i > 1e6:
262
+ raise RecursionError(err_msg)
263
+ if e is None:
264
+ break
265
+ sign = 1
266
+ if e in [">=", "<=", "==", "="]:
267
+ main_sign = -1
268
+ inequality_sign = _inequality_operator_sign(e)
269
+ e = next(iterator, None)
270
+ if e in ["-", "+"]:
271
+ sign *= _operator_sign(e)
272
+ e = next(iterator, None)
273
+ elif e in ["-", "+"]:
274
+ sign *= _operator_sign(e)
275
+ e = next(iterator, None)
276
+ elif e == "*":
277
+ raise EquationToMatrixError(
278
+ f"{err_msg}: the character '{e}' is wrongly positioned"
279
+ )
280
+ sign *= main_sign
281
+ # next can only be a number or a group
282
+ if e is None or e in operators:
283
+ raise EquationToMatrixError(
284
+ f"{err_msg}: the character '{e}' is wrongly positioned"
285
+ )
286
+ if is_group(e):
287
+ arr = _matching_array(values=groups, key=e, sum_to_one=sum_to_one)
288
+ # next can only be a '*' or an ['-', '+', '>=', '<=', '==', '='] or None
289
+ e = next(iterator, None)
290
+ if e is None or e in ["-", "+", ">=", "<=", "==", "="]:
291
+ left += sign * arr
292
+ elif e == "*":
293
+ # next can only a number
294
+ e = next(iterator, None)
295
+ try:
296
+ number = float(e)
297
+ except ValueError:
298
+ invalid_ops = invalid_pattern.findall(e)
299
+ if len(invalid_ops) > 0:
300
+ raise EquationToMatrixError(
301
+ f"{invalid_ops[0]} is an invalid operator. Valid operators"
302
+ f" are: {operators}"
303
+ ) from None
304
+ raise GroupNotFoundError(
305
+ f"{err_msg}: the group '{e}' is missing from the groups"
306
+ f" {groups}"
307
+ ) from None
308
+
309
+ left += number * sign * arr
310
+ e = next(iterator, None)
311
+ else:
312
+ raise EquationToMatrixError(
313
+ f"{err_msg}: the character '{e}' is wrongly positioned"
314
+ )
315
+ else:
316
+ try:
317
+ number = float(e)
318
+ except ValueError:
319
+ invalid_ops = invalid_pattern.findall(e)
320
+ if len(invalid_ops) > 0:
321
+ raise EquationToMatrixError(
322
+ f"{invalid_ops[0]} is an invalid operator. Valid operators are:"
323
+ f" {operators}"
324
+ ) from None
325
+ raise GroupNotFoundError(
326
+ f"{err_msg}: the group '{e}' is missing from the groups {groups}"
327
+ ) from None
328
+ # next can only be a '*' or an operator or None
329
+ e = next(iterator, None)
330
+ if e == "*":
331
+ # next can only a group
332
+ e = next(iterator, None)
333
+ if not is_group(e):
334
+ raise EquationToMatrixError(
335
+ f"{err_msg}: the character '{e}' is wrongly positioned"
336
+ )
337
+ arr = _matching_array(values=groups, key=e, sum_to_one=sum_to_one)
338
+ left += number * sign * arr
339
+ e = next(iterator, None)
340
+ elif e is None or e in ["-", "+", ">=", "<=", "==", "="]:
341
+ right += number * sign
342
+ else:
343
+ raise EquationToMatrixError(
344
+ f"{err_msg}: the character '{e}' is wrongly positioned"
345
+ )
346
+
347
+ left *= inequality_sign
348
+ right *= -inequality_sign
349
+
350
+ return left, right
@@ -0,0 +1,117 @@
1
+ """Fast non-dominated sorting module"""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ import numpy as np
7
+
8
+ __all__ = ["dominate", "non_denominated_sort"]
9
+
10
+
11
+ def dominate(fitness_1: np.ndarray, fitness_2: np.ndarray) -> bool:
12
+ """Compute the domination of two fitness arrays.
13
+
14
+ Domination of `fitness_1` over `fitness_2` means that each objective (value) of
15
+ `fitness_1` is not strictly worse than the corresponding objective of `fitness_2`
16
+ and at least one objective is strictly better.
17
+
18
+ Parameters
19
+ ----------
20
+ fitness_1 : ndarray of floats of shape (n_objectives,)
21
+ Fitness array 1.
22
+
23
+ fitness_2 : ndarray of floats of shape (n_objectives,)
24
+ Fitness array 2.
25
+
26
+ Returns
27
+ -------
28
+ is_dominated : bool
29
+ Ture if `fitness_1` dominates `fitness_2`, False otherwise.
30
+ """
31
+ if fitness_1.ndim != fitness_2.ndim != 1:
32
+ raise ValueError("fitness_1 and fitness_2 must be 1D array")
33
+ not_equal = False
34
+ for self_value, other_value in zip(fitness_1, fitness_2, strict=True):
35
+ if self_value > other_value:
36
+ not_equal = True
37
+ elif self_value < other_value:
38
+ return False
39
+ return not_equal
40
+
41
+
42
+ def non_denominated_sort(
43
+ fitnesses: np.ndarray, first_front_only: bool
44
+ ) -> list[list[int]]:
45
+ """Fast non-dominated sorting.
46
+
47
+ Sort the fitnesses into different non-domination levels.
48
+ Complexity O(MN^2) where M is the number of objectives and N the number of
49
+ portfolios.
50
+
51
+ Parameters
52
+ ----------
53
+ fitnesses: ndarray of shape(n, n_fitness)
54
+ Fitnesses array.
55
+
56
+ first_front_only : bool
57
+ If this is set to True, only the first front is computed and returned.
58
+
59
+ Returns
60
+ -------
61
+ fronts: list[list[int]]
62
+ A list of Pareto fronts (lists), the first list includes non-dominated fitnesses.
63
+ """
64
+ n = len(fitnesses)
65
+ fronts = []
66
+ if n == 0:
67
+ return fronts
68
+
69
+ # final rank that will be returned
70
+ n_ranked = 0
71
+ ranked = np.array([0 for _ in range(n)])
72
+
73
+ # for each portfolio a list of all portfolios that are dominated by this one
74
+ is_dominating = [[x for x in range(0)] for _ in range(n)]
75
+
76
+ # storage for the number of solutions dominated this one
77
+ n_dominated = [0 for _ in range(n)]
78
+
79
+ current_front = [x for x in range(0)]
80
+
81
+ for i in range(n):
82
+ for j in range(i + 1, n):
83
+ if dominate(fitnesses[i], fitnesses[j]):
84
+ is_dominating[i].append(j)
85
+ n_dominated[j] += 1
86
+ elif dominate(fitnesses[j], fitnesses[i]):
87
+ is_dominating[j].append(i)
88
+ n_dominated[i] += 1
89
+
90
+ if n_dominated[i] == 0:
91
+ current_front.append(i)
92
+ ranked[i] = 1.0
93
+ n_ranked += 1
94
+
95
+ # append the first front to the current front
96
+ fronts.append(current_front)
97
+
98
+ if first_front_only:
99
+ return fronts
100
+
101
+ # while not all solutions are assigned to a pareto front
102
+ while n_ranked < n:
103
+ next_front = []
104
+ # for each portfolio in the current front
105
+ for i in current_front:
106
+ # all solutions that are dominated by this portfolio
107
+ for j in is_dominating[i]:
108
+ n_dominated[j] -= 1
109
+ if n_dominated[j] == 0:
110
+ next_front.append(j)
111
+ ranked[j] = 1.0
112
+ n_ranked += 1
113
+
114
+ fronts.append(next_front)
115
+ current_front = next_front
116
+
117
+ return fronts