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,607 @@
1
+ """Module that includes all Measures functions used across `skfolio`."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+
7
+ import numpy as np
8
+ import scipy.optimize as sco
9
+
10
+
11
+ def mean(returns: np.ndarray) -> float:
12
+ """Compute the mean.
13
+
14
+ Parameters
15
+ ----------
16
+ returns : ndarray of shape (n_observations,)
17
+ Vector of returns.
18
+
19
+ Returns
20
+ -------
21
+ value : float
22
+ Mean
23
+ """
24
+ return returns.mean()
25
+
26
+
27
+ def mean_absolute_deviation(
28
+ returns: np.ndarray, min_acceptable_return: float | None = None
29
+ ) -> float:
30
+ """Compute the mean absolute deviation (MAD).
31
+
32
+ Parameters
33
+ ----------
34
+ returns : ndarray of shape (n_observations,)
35
+ Vector of returns.
36
+
37
+ min_acceptable_return : float, optional
38
+ Minimum acceptable return. It is the return target to distinguish "downside" and
39
+ "upside" returns.
40
+ The default (`None`) is to use the returns' mean.
41
+
42
+ Returns
43
+ -------
44
+ value : float
45
+ Mean absolute deviation.
46
+ """
47
+ if min_acceptable_return is None:
48
+ min_acceptable_return = np.mean(returns, axis=0)
49
+ return float(np.mean(np.abs(returns - min_acceptable_return)))
50
+
51
+
52
+ def first_lower_partial_moment(
53
+ returns: np.ndarray, min_acceptable_return: float | None = None
54
+ ) -> float:
55
+ """Compute the first lower partial moment.
56
+
57
+ The first lower partial moment is the mean of the returns below a minimum
58
+ acceptable return.
59
+
60
+ Parameters
61
+ ----------
62
+ returns : ndarray of shape (n_observations,)
63
+ Vector of returns
64
+
65
+ min_acceptable_return : float, optional
66
+ Minimum acceptable return. It is the return target to distinguish "downside" and
67
+ "upside" returns.
68
+ The default (`None`) is to use the mean.
69
+
70
+ Returns
71
+ -------
72
+ value : float
73
+ First lower partial moment.
74
+ """
75
+ if min_acceptable_return is None:
76
+ min_acceptable_return = np.mean(returns, axis=0)
77
+ return -np.sum(np.minimum(0, returns - min_acceptable_return)) / len(returns)
78
+
79
+
80
+ def variance(returns: np.ndarray) -> float:
81
+ """Compute the variance (second moment).
82
+
83
+ Parameters
84
+ ----------
85
+ returns : ndarray of shape (n_observations,)
86
+ Vector of returns.
87
+
88
+ Returns
89
+ -------
90
+ value : float
91
+ Variance.
92
+ """
93
+ return returns.var(ddof=1)
94
+
95
+
96
+ def semi_variance(
97
+ returns: np.ndarray, min_acceptable_return: float | None = None
98
+ ) -> float:
99
+ """Compute the semi-variance (second lower partial moment).
100
+
101
+ The semi-variance is the variance of the returns below a minimum acceptable return.
102
+
103
+ Parameters
104
+ ----------
105
+ returns : ndarray of shape (n_observations,)
106
+ Vector of returns
107
+
108
+ min_acceptable_return : float, optional
109
+ Minimum acceptable return. It is the return target to distinguish "downside" and
110
+ "upside" returns.
111
+ The default (`None`) is to use the mean.
112
+
113
+ Returns
114
+ -------
115
+ value : float
116
+ Semi-variance.
117
+ """
118
+ if min_acceptable_return is None:
119
+ min_acceptable_return = np.mean(returns, axis=0)
120
+ return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 2)) / (
121
+ len(returns) - 1
122
+ )
123
+
124
+
125
+ def standard_deviation(returns: np.ndarray) -> float:
126
+ """Compute the standard-deviation (square root of the second moment).
127
+
128
+ Parameters
129
+ ----------
130
+ returns : ndarray of shape (n_observations,)
131
+ Vector of returns.
132
+
133
+ Returns
134
+ -------
135
+ value : float
136
+ Standard-deviation.
137
+ """
138
+ return np.sqrt(variance(returns=returns))
139
+
140
+
141
+ def semi_deviation(
142
+ returns: np.ndarray, min_acceptable_return: float | None = None
143
+ ) -> float:
144
+ """Compute the semi standard-deviation (semi-deviation) (square root of the second lower
145
+ partial moment).
146
+
147
+ Parameters
148
+ ----------
149
+ returns : ndarray of shape (n_observations,)
150
+ Vector of returns.
151
+
152
+ min_acceptable_return : float, optional
153
+ Minimum acceptable return. It is the return target to distinguish "downside" and
154
+ "upside" returns.
155
+ The default (`None`) is to use the returns mean.
156
+
157
+ Returns
158
+ -------
159
+ value : float
160
+ Semi-standard-deviation.
161
+ """
162
+ return np.sqrt(
163
+ semi_variance(returns=returns, min_acceptable_return=min_acceptable_return)
164
+ )
165
+
166
+
167
+ def third_central_moment(returns: np.ndarray) -> float:
168
+ """Compute the third central moment.
169
+
170
+ Parameters
171
+ ----------
172
+ returns : ndarray of shape (n_observations,)
173
+ Vector of returns.
174
+
175
+ Returns
176
+ -------
177
+ value : float
178
+ Third central moment.
179
+ """
180
+
181
+ return np.sum(np.power(returns - np.mean(returns, axis=0), 3)) / len(returns)
182
+
183
+
184
+ def skew(returns: np.ndarray) -> float:
185
+ """Compute the Skew.
186
+
187
+ The Skew is a measure of the lopsidedness of the distribution.
188
+ A symmetric distribution have a Skew of zero.
189
+ Higher Skew corresponds to longer right tail.
190
+
191
+ Parameters
192
+ ----------
193
+ returns : ndarray of shape (n_observations,)
194
+ Vector of returns.
195
+
196
+ Returns
197
+ -------
198
+ value : float
199
+ Skew.
200
+ """
201
+
202
+ return third_central_moment(returns) / standard_deviation(returns) ** 3
203
+
204
+
205
+ def fourth_central_moment(returns: np.ndarray) -> float:
206
+ """Compute the Fourth central moment.
207
+
208
+ Parameters
209
+ ----------
210
+ returns : ndarray of shape (n_observations,)
211
+ Vector of returns.
212
+
213
+ Returns
214
+ -------
215
+ value : float
216
+ Fourth central moment.
217
+ """
218
+ return np.sum(np.power(returns - np.mean(returns, axis=0), 4)) / len(returns)
219
+
220
+
221
+ def kurtosis(returns: np.ndarray) -> float:
222
+ """Compute the Kurtosis.
223
+
224
+ The Kurtosis is a measure of the heaviness of the tail of the distribution.
225
+ Higher Kurtosis corresponds to greater extremity of deviations (fat tails).
226
+
227
+ Parameters
228
+ ----------
229
+ returns : ndarray of shape (n_observations,)
230
+ Vector of returns.
231
+
232
+ Returns
233
+ -------
234
+ value : float
235
+ Kurtosis.
236
+ """
237
+
238
+ return fourth_central_moment(returns) / standard_deviation(returns) ** 4
239
+
240
+
241
+ def fourth_lower_partial_moment(
242
+ returns: np.ndarray, min_acceptable_return: float | None = None
243
+ ) -> float:
244
+ """Compute the fourth lower partial moment.
245
+
246
+ The Fourth Lower Partial Moment is a measure of the heaviness of the downside tail
247
+ of the returns below a minimum acceptable return.
248
+ Higher Fourth Lower Partial Moment corresponds to greater extremity of downside
249
+ deviations (downside fat tail).
250
+
251
+ Parameters
252
+ ----------
253
+ returns : ndarray of shape (n_observations,)
254
+ Vector of returns
255
+
256
+ min_acceptable_return : float, optional
257
+ Minimum acceptable return. It is the return target to distinguish "downside" and
258
+ "upside" returns.
259
+ The default (`None`) is to use the returns mean.
260
+
261
+ Returns
262
+ -------
263
+ value : float
264
+ Fourth lower partial moment.
265
+ """
266
+ if min_acceptable_return is None:
267
+ min_acceptable_return = np.mean(returns, axis=0)
268
+ return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 4)) / len(
269
+ returns
270
+ )
271
+
272
+
273
+ def worst_realization(returns: np.ndarray) -> float:
274
+ """Compute the worst realization (worst return).
275
+
276
+ Parameters
277
+ ----------
278
+ returns : ndarray of shape (n_observations,)
279
+ Vector of returns.
280
+
281
+ Returns
282
+ -------
283
+ value : float
284
+ Worst realization.
285
+ """
286
+ return -min(returns)
287
+
288
+
289
+ def value_at_risk(returns: np.ndarray, beta: float = 0.95) -> float:
290
+ """Compute the historical value at risk (VaR).
291
+
292
+ The VaR is the maximum loss at a given confidence level (beta).
293
+
294
+ Parameters
295
+ ----------
296
+ returns : ndarray of shape (n_observations,)
297
+ Vector of returns.
298
+
299
+ beta : float, default=0.95
300
+ The VaR confidence level (return on the worst (1-beta)% observation).
301
+
302
+ Returns
303
+ -------
304
+ value : float
305
+ Value at Risk.
306
+ """
307
+ k = (1 - beta) * len(returns)
308
+ ik = max(0, int(np.ceil(k) - 1))
309
+ # We only need the first k elements so using `partition` O(n log(n)) is faster
310
+ # than `sort` O(n).
311
+ ret = np.partition(returns, ik)
312
+ return -ret[ik]
313
+
314
+
315
+ def cvar(returns: np.ndarray, beta: float = 0.95) -> float:
316
+ """Compute the historical CVaR (conditional value at risk).
317
+
318
+ The CVaR (or Tail VaR) represents the mean shortfall at a specified confidence
319
+ level (beta).
320
+
321
+ Parameters
322
+ ----------
323
+ returns : ndarray of shape (n_observations,)
324
+ Vector of returns.
325
+
326
+ beta : float, default=0.95
327
+ The CVaR confidence level (expected VaR on the worst (1-beta)% observations).
328
+
329
+ Returns
330
+ -------
331
+ value : float
332
+ CVaR.
333
+ """
334
+ k = (1 - beta) * len(returns)
335
+ ik = max(0, int(np.ceil(k) - 1))
336
+ # We only need the first k elements so using `partition` O(n log(n)) is faster
337
+ # than `sort` O(n).
338
+ ret = np.partition(returns, ik)
339
+ return -np.sum(ret[:ik]) / k + ret[ik] * (ik / k - 1)
340
+
341
+
342
+ def entropic_risk_measure(
343
+ returns: np.ndarray, theta: float = 1, beta: float = 0.95
344
+ ) -> float:
345
+ """Compute the entropic risk measure.
346
+
347
+ The entropic risk measure is a risk measure which depends on the risk aversion
348
+ defined by the investor (theat) through the exponential utility function at a given
349
+ confidence level (beta).
350
+
351
+ Parameters
352
+ ----------
353
+ returns : ndarray of shape (n_observations,)
354
+ Vector of returns.
355
+
356
+ theta : float, default=1.0
357
+ Risk aversion.
358
+
359
+ beta : float, default=0.95
360
+ Confidence level.
361
+
362
+ Returns
363
+ -------
364
+ value : float
365
+ Entropic risk measure.
366
+ """
367
+ return theta * np.log(np.mean(np.exp(-returns / theta)) / (1 - beta))
368
+
369
+
370
+ def evar(returns: np.ndarray, beta: float = 0.95) -> float:
371
+ """Compute the EVaR (entropic value at risk) and its associated risk aversion.
372
+
373
+ The EVaR is a coherent risk measure which is an upper bound for the VaR and the
374
+ CVaR, obtained from the Chernoff inequality. The EVaR can be represented by using
375
+ the concept of relative entropy.
376
+
377
+ Parameters
378
+ ----------
379
+ returns : ndarray of shape (n_observations,)
380
+ Vector of returns.
381
+
382
+ beta : float, default=0.95
383
+ The EVaR confidence level.
384
+
385
+ Returns
386
+ -------
387
+ value : float
388
+ EVaR.
389
+ """
390
+
391
+ def func(x: float) -> float:
392
+ return entropic_risk_measure(returns=returns, theta=x, beta=beta)
393
+
394
+ # The lower bound is chosen to avoid exp overflow
395
+ lower_bound = np.max(-returns) / 100
396
+ result = sco.minimize(
397
+ func,
398
+ x0=np.array([lower_bound * 2]),
399
+ method="SLSQP",
400
+ bounds=[(lower_bound, np.inf)],
401
+ tol=1e-10,
402
+ )
403
+ return result.fun
404
+
405
+
406
+ def get_cumulative_returns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
407
+ """Compute the cumulative returns from the returns.
408
+ Non-compounded cumulative returns start at 0.
409
+ Compounded cumulative returns are rescaled to start at 1000.
410
+
411
+ Parameters
412
+ ----------
413
+ returns : ndarray of shape (n_observations,)
414
+ Vector of returns.
415
+
416
+ compounded : bool, default=False
417
+ If this is set to True, the cumulative returns are compounded otherwise they
418
+ are uncompounded.
419
+
420
+ Returns
421
+ -------
422
+ values: ndarray of shape (n_observations,)
423
+ Cumulative returns.
424
+ """
425
+ if compounded:
426
+ cumulative_returns = 1000 * np.cumprod(1 + returns) # Rescaled to start at 1000
427
+ else:
428
+ cumulative_returns = np.cumsum(returns)
429
+ return cumulative_returns
430
+
431
+
432
+ def get_drawdowns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
433
+ """Compute the drawdowns' series from the returns.
434
+
435
+ Parameters
436
+ ----------
437
+ returns : ndarray of shape (n_observations,)
438
+ Vector of returns.
439
+
440
+ compounded : bool, default=False
441
+ If this is set to True, the cumulative returns are compounded otherwise they
442
+ are uncompounded.
443
+
444
+ Returns
445
+ -------
446
+ values: ndarray of shape (n_observations,)
447
+ Drawdowns.
448
+ """
449
+ cumulative_returns = get_cumulative_returns(returns=returns, compounded=compounded)
450
+ if compounded:
451
+ drawdowns = cumulative_returns / np.maximum.accumulate(cumulative_returns) - 1
452
+ else:
453
+ drawdowns = cumulative_returns - np.maximum.accumulate(cumulative_returns)
454
+ return drawdowns
455
+
456
+
457
+ def drawdown_at_risk(drawdowns: np.ndarray, beta: float = 0.95) -> float:
458
+ """Compute the Drawdown at risk.
459
+
460
+ The Drawdown at risk is the maximum drawdown at a given confidence level (beta).
461
+
462
+ Parameters
463
+ ----------
464
+ drawdowns : ndarray of shape (n_observations,)
465
+ Vector of drawdowns.
466
+
467
+ beta : float, default = 0.95
468
+ The DaR confidence level (drawdown on the worst (1-beta)% observations).
469
+
470
+ Returns
471
+ -------
472
+ value : float
473
+ Drawdown at risk.
474
+ """
475
+ return value_at_risk(returns=drawdowns, beta=beta)
476
+
477
+
478
+ def max_drawdown(drawdowns: np.ndarray) -> float:
479
+ """Compute the maximum drawdown.
480
+
481
+ Parameters
482
+ ----------
483
+ drawdowns : ndarray of shape (n_observations,)
484
+ Vector of drawdowns.
485
+
486
+ Returns
487
+ -------
488
+ value : float
489
+ Maximum drawdown.
490
+ """
491
+ return drawdown_at_risk(drawdowns=drawdowns, beta=1)
492
+
493
+
494
+ def average_drawdown(drawdowns: np.ndarray) -> float:
495
+ """Compute the average drawdown.
496
+
497
+ Parameters
498
+ ----------
499
+ drawdowns : ndarray of shape (n_observations,)
500
+ Vector of drawdowns.
501
+
502
+ Returns
503
+ -------
504
+ value : float
505
+ Average drawdown.
506
+ """
507
+ return cdar(drawdowns=drawdowns, beta=0)
508
+
509
+
510
+ def cdar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
511
+ """Compute the historical CDaR (conditional drawdown at risk).
512
+
513
+ Parameters
514
+ ----------
515
+ drawdowns : ndarray of shape (n_observations,)
516
+ Vector of drawdowns.
517
+
518
+ beta : float, default = 0.95
519
+ The CDaR confidence level (expected drawdown on the worst
520
+ (1-beta)% observations).
521
+
522
+ Returns
523
+ -------
524
+ value : float
525
+ CDaR.
526
+ """
527
+ return cvar(returns=drawdowns, beta=beta)
528
+
529
+
530
+ def edar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
531
+ """Compute the EDaR (entropic drawdown at risk).
532
+
533
+ The EDaR is a coherent risk measure which is an upper bound for the DaR and the
534
+ CDaR, obtained from the Chernoff inequality. The EDaR can be represented by using
535
+ the concept of relative entropy.
536
+
537
+ Parameters
538
+ ----------
539
+ drawdowns : ndarray of shape (n_observations,)
540
+ Vector of drawdowns.
541
+
542
+ beta : float, default=0.95
543
+ The EDaR confidence level.
544
+
545
+ Returns
546
+ -------
547
+ value : float
548
+ EDaR.
549
+ """
550
+ return evar(returns=drawdowns, beta=beta)
551
+
552
+
553
+ def ulcer_index(drawdowns: np.ndarray) -> float:
554
+ """Compute the Ulcer index.
555
+
556
+ Parameters
557
+ ----------
558
+ drawdowns : ndarray of shape (n_observations,)
559
+ Vector of drawdowns.
560
+
561
+ Returns
562
+ -------
563
+ value : float
564
+ Ulcer index.
565
+ """
566
+ return np.sqrt(np.sum(np.power(drawdowns, 2)) / len(drawdowns))
567
+
568
+
569
+ def owa_gmd_weights(n_observations: int) -> np.ndarray:
570
+ """Compute the OWA weights used for the Gini mean difference (GMD) computation.
571
+
572
+ Parameters
573
+ ----------
574
+ n_observations : int
575
+ Number of observations.
576
+
577
+ Returns
578
+ -------
579
+ value : float
580
+ OWA GMD weights.
581
+ """
582
+ return (4 * np.arange(1, n_observations + 1) - 2 * (n_observations + 1)) / (
583
+ n_observations * (n_observations - 1)
584
+ )
585
+
586
+
587
+ def gini_mean_difference(returns: np.ndarray) -> float:
588
+ """Compute the Gini mean difference (GMD).
589
+
590
+ The GMD is the expected absolute difference between two realisations.
591
+ The GMD is a superior measure of variability for non-normal distribution than the
592
+ variance.
593
+ It can be used to form necessary conditions for second-degree stochastic dominance,
594
+ while the variance cannot.
595
+
596
+ Parameters
597
+ ----------
598
+ returns : ndarray of shape (n_observations,)
599
+ Vector of returns.
600
+
601
+ Returns
602
+ -------
603
+ value : float
604
+ Gini mean difference.
605
+ """
606
+ w = owa_gmd_weights(len(returns))
607
+ return float(w @ np.sort(returns, axis=0))
@@ -0,0 +1,3 @@
1
+ from skfolio.metrics._scorer import make_scorer
2
+
3
+ __all__ = ["make_scorer"]