<a id="sphx-glr-auto-examples-pre-selection-plot-2-select-best-performers-py"></a>

<a id="select-best-performers"></a>

# Select Best Performers

This tutorial introduces the [pre-selection transformers](https://skfolio.org/user_guide/pre_selection.html.md#pre-selection)
[`SelectKExtremes`](https://skfolio.org/generated/skfolio.pre_selection.SelectKExtremes.html.md#skfolio.pre_selection.SelectKExtremes) to select the `k` best or the `k` worst
assets according to a given measure before the optimization.

In this example, we will use a `Pipeline` to assemble the pre-selection step with a
minimum variance optimization. Then, we will use cross-validation to find the optimal
number of pre-selected assets to maximize the mean out-of-sample Sharpe Ratio.

<a id="data"></a>

## Data

We load the FTSE 100 [dataset](https://skfolio.org/user_guide/datasets.html.md#datasets) composed of the daily prices of 64
assets from the FTSE 100 Index starting from 2000-01-04 up to 2023-05-31:

```Python
import plotly.graph_objs as go
from plotly.io import show
from sklearn import set_config
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline

from skfolio import Population, RatioMeasure
from skfolio.datasets import load_ftse100_dataset
from skfolio.metrics import make_scorer
from skfolio.model_selection import (
    WalkForward,
    cross_val_predict,
)
from skfolio.optimization import MeanRisk
from skfolio.pre_selection import SelectKExtremes
from skfolio.preprocessing import prices_to_returns

prices = load_ftse100_dataset()
X = prices_to_returns(prices)
X_train, X_test = train_test_split(X, test_size=0.33, shuffle=False)
```

<a id="model"></a>

## Model

First, we create a Minimum Variance model without pre-selection:

```Python
benchmark = MeanRisk()
```

#### NOTE
A covariance matrix that is not positive definite often occurs in high
dimensional problems. It can be due to multicollinearity, floating-point
inaccuracies, or when the number of observations is smaller than the number of
assets. By default, the parameter named `nearest` from the covariance estimator is
set to `True`: if the covariance is not positive definite (PD), it is replaced by
the nearest covariance that is PD without changing the variance.
For more details, see [`cov_nearest`](https://skfolio.org/generated/skfolio.utils.stats.cov_nearest.html.md#skfolio.utils.stats.cov_nearest).

<a id="pipeline"></a>

## Pipeline

Then, we create a Minimum Variance model with pre-selection using `Pipeline`:

```Python
set_config(transform_output="pandas")

model = Pipeline([("pre_selection", SelectKExtremes()), ("optimization", benchmark)])
```

<a id="parameter-tuning"></a>

## Parameter Tuning

To demonstrate how parameter tuning works in a Pipeline model, we find the number of
pre-selected assets `k` that maximizes the out-of-sample Sharpe Ratio using
`GridSearchCV` with `WalkForward` cross-validation on the training set. The
`WalkForward` is chosen to simulate a three months (60 business days) rolling
portfolio fitted on the previous year (252 business days):

```Python
cv = WalkForward(train_size=252, test_size=60)

scorer = make_scorer(RatioMeasure.ANNUALIZED_SHARPE_RATIO)
```

Note that we can also create a custom scorer this way:
`scorer=make_scorer(lambda pred: pred.mean - 0.5 * pred.variance)`

```Python
grid_search = GridSearchCV(
    estimator=model,
    cv=cv,
    n_jobs=-1,
    param_grid={"pre_selection__k": list(range(5, 66, 3))},
    scoring=scorer,
    return_train_score=True,
)
grid_search.fit(X_train)
model = grid_search.best_estimator_
print(model)
```

```none
Pipeline(steps=[('pre_selection', SelectKExtremes(k=53)),
                ('optimization', MeanRisk())])
```

Let’s plot the train and test scores as a function of the number of pre-selected
assets. The vertical line represents the best test score and the selected model:

```Python
cv_results = grid_search.cv_results_
fig = go.Figure(
    [
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_train_score"],
            name="Train",
            mode="lines",
            line=dict(color="rgb(31, 119, 180)"),
        ),
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_train_score"] + cv_results["std_train_score"],
            mode="lines",
            line=dict(width=0),
            showlegend=False,
        ),
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_train_score"] - cv_results["std_train_score"],
            mode="lines",
            line=dict(width=0),
            showlegend=False,
            fillcolor="rgba(31, 119, 180,0.15)",
            fill="tonexty",
        ),
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_test_score"],
            name="Test",
            mode="lines",
            line=dict(color="rgb(255,165,0)"),
        ),
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_test_score"] + cv_results["std_test_score"],
            mode="lines",
            line=dict(width=0),
            showlegend=False,
        ),
        go.Scatter(
            x=cv_results["param_pre_selection__k"],
            y=cv_results["mean_test_score"] - cv_results["std_test_score"],
            line=dict(width=0),
            mode="lines",
            fillcolor="rgba(255,165,0, 0.15)",
            fill="tonexty",
            showlegend=False,
        ),
    ]
)
fig.add_vline(
    x=grid_search.best_params_["pre_selection__k"],
    line_width=2,
    line_dash="dash",
    line_color="green",
)
fig.update_layout(
    title="Train/Test score",
    xaxis_title="Number of pre-selected best performers",
    yaxis_title="Annualized Sharpe Ratio",
)
fig.update_yaxes(tickformat=".2f")
show(fig)
```

[plotly figure stripped from llms output]<style>html[data-theme="dark"] div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}@media (prefers-color-scheme: dark){html:not([data-theme="light"]) div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}}</style><script>if (!window.plotlySphinxGalleryResize) {window.plotlySphinxGalleryResize = true;window.addEventListener("load", function () {document.querySelectorAll(".plotly-graph-div").forEach(function (gd) { Plotly.Plots.resize(gd); });});}</script>

<br/>

The mean test Sharpe Ratio increases from 1.17 (for k=5) to its maximum 1.91
(for k=50) then decreases to 1.81 (for k=65).
The selected model is a pre-selection of the top 50 performers based on their Sharpe
Ratio, followed by a Minimum Variance optimization.

<a id="prediction"></a>

## Prediction

Now we evaluate the two models using the same `WalkForward` object on the test set:

```Python
pred_bench = cross_val_predict(
    benchmark,
    X_test,
    cv=cv,
    portfolio_params=dict(name="Benchmark"),
)

pred_model = cross_val_predict(
    model,
    X_test,
    cv=cv,
    n_jobs=-1,
    portfolio_params=dict(name="Pre-selection"),
)
```

Each predicted object is a `MultiPeriodPortfolio`.
For improved analysis, we can add them to a `Population`:

```Python
population = Population([pred_bench, pred_model])
```

Let’s plot the rolling portfolios cumulative returns on the test set:

```Python
population.plot_cumulative_returns()
```

<style>html[data-theme="dark"] div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}@media (prefers-color-scheme: dark){html:not([data-theme="light"]) div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}}</style><script>if (!window.plotlySphinxGalleryResize) {window.plotlySphinxGalleryResize = true;window.addEventListener("load", function () {document.querySelectorAll(".plotly-graph-div").forEach(function (gd) { Plotly.Plots.resize(gd); });});}</script>[plotly figure stripped from llms output]
<br />
<br />

Let’s plot the rolling portfolios compositions:

```Python
population.plot_composition(display_sub_ptf_name=False)
```

<style>html[data-theme="dark"] div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}@media (prefers-color-scheme: dark){html:not([data-theme="light"]) div.output_subarea:has(.plotly-graph-div){background:#fff;border-radius:0.25rem;padding:0.5rem}}</style><script>if (!window.plotlySphinxGalleryResize) {window.plotlySphinxGalleryResize = true;window.addEventListener("load", function () {document.querySelectorAll(".plotly-graph-div").forEach(function (gd) { Plotly.Plots.resize(gd); });});}</script>[plotly figure stripped from llms output]
<br />
<br />

Let’s display the full summary:

```Python
population.summary()
```

<div class="output_subarea output_html rendered_html output_result">
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Benchmark</th>
      <th>Pre-selection</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>Mean</th>
      <td>0.029%</td>
      <td>0.032%</td>
    </tr>
    <tr>
      <th>Annualized Mean</th>
      <td>7.28%</td>
      <td>8.16%</td>
    </tr>
    <tr>
      <th>Variance</th>
      <td>0.000074</td>
      <td>0.000075</td>
    </tr>
    <tr>
      <th>Annualized Variance</th>
      <td>1.85%</td>
      <td>1.88%</td>
    </tr>
    <tr>
      <th>Semi-Variance</th>
      <td>0.000040</td>
      <td>0.000041</td>
    </tr>
    <tr>
      <th>Annualized Semi-Variance</th>
      <td>1.02%</td>
      <td>1.03%</td>
    </tr>
    <tr>
      <th>Standard Deviation</th>
      <td>0.86%</td>
      <td>0.86%</td>
    </tr>
    <tr>
      <th>Annualized Standard Deviation</th>
      <td>13.61%</td>
      <td>13.71%</td>
    </tr>
    <tr>
      <th>Semi-Deviation</th>
      <td>0.64%</td>
      <td>0.64%</td>
    </tr>
    <tr>
      <th>Annualized Semi-Deviation</th>
      <td>10.09%</td>
      <td>10.15%</td>
    </tr>
    <tr>
      <th>Mean Absolute Deviation</th>
      <td>0.60%</td>
      <td>0.60%</td>
    </tr>
    <tr>
      <th>CVaR at 95%</th>
      <td>2.03%</td>
      <td>2.02%</td>
    </tr>
    <tr>
      <th>EVaR at 95%</th>
      <td>4.38%</td>
      <td>4.44%</td>
    </tr>
    <tr>
      <th>Worst Realization</th>
      <td>8.39%</td>
      <td>8.49%</td>
    </tr>
    <tr>
      <th>CDaR at 95%</th>
      <td>18.24%</td>
      <td>17.78%</td>
    </tr>
    <tr>
      <th>MAX Drawdown</th>
      <td>29.72%</td>
      <td>29.26%</td>
    </tr>
    <tr>
      <th>Average Drawdown</th>
      <td>4.59%</td>
      <td>4.61%</td>
    </tr>
    <tr>
      <th>EDaR at 95%</th>
      <td>21.50%</td>
      <td>21.31%</td>
    </tr>
    <tr>
      <th>First Lower Partial Moment</th>
      <td>0.30%</td>
      <td>0.30%</td>
    </tr>
    <tr>
      <th>Ulcer Index</th>
      <td>0.068</td>
      <td>0.067</td>
    </tr>
    <tr>
      <th>Gini Mean Difference</th>
      <td>0.88%</td>
      <td>0.89%</td>
    </tr>
    <tr>
      <th>Value at Risk at 95%</th>
      <td>1.26%</td>
      <td>1.25%</td>
    </tr>
    <tr>
      <th>Drawdown at Risk at 95%</th>
      <td>14.54%</td>
      <td>14.08%</td>
    </tr>
    <tr>
      <th>Entropic Risk Measure at 95%</th>
      <td>3.00</td>
      <td>3.00</td>
    </tr>
    <tr>
      <th>Fourth Central Moment</th>
      <td>0.000007%</td>
      <td>0.000007%</td>
    </tr>
    <tr>
      <th>Fourth Lower Partial Moment</th>
      <td>0.000005%</td>
      <td>0.000006%</td>
    </tr>
    <tr>
      <th>Skew</th>
      <td>-72.68%</td>
      <td>-74.62%</td>
    </tr>
    <tr>
      <th>Kurtosis</th>
      <td>1334.40%</td>
      <td>1347.93%</td>
    </tr>
    <tr>
      <th>Sharpe Ratio</th>
      <td>0.034</td>
      <td>0.038</td>
    </tr>
    <tr>
      <th>Annualized Sharpe Ratio</th>
      <td>0.54</td>
      <td>0.60</td>
    </tr>
    <tr>
      <th>Sortino Ratio</th>
      <td>0.045</td>
      <td>0.051</td>
    </tr>
    <tr>
      <th>Annualized Sortino Ratio</th>
      <td>0.72</td>
      <td>0.80</td>
    </tr>
    <tr>
      <th>Mean Absolute Deviation Ratio</th>
      <td>0.048</td>
      <td>0.054</td>
    </tr>
    <tr>
      <th>First Lower Partial Moment Ratio</th>
      <td>0.097</td>
      <td>0.11</td>
    </tr>
    <tr>
      <th>Value at Risk Ratio at 95%</th>
      <td>0.023</td>
      <td>0.026</td>
    </tr>
    <tr>
      <th>CVaR Ratio at 95%</th>
      <td>0.014</td>
      <td>0.016</td>
    </tr>
    <tr>
      <th>Entropic Risk Measure Ratio at 95%</th>
      <td>0.000096</td>
      <td>0.00011</td>
    </tr>
    <tr>
      <th>EVaR Ratio at 95%</th>
      <td>0.0066</td>
      <td>0.0073</td>
    </tr>
    <tr>
      <th>Worst Realization Ratio</th>
      <td>0.0034</td>
      <td>0.0038</td>
    </tr>
    <tr>
      <th>Drawdown at Risk Ratio at 95%</th>
      <td>0.0020</td>
      <td>0.0023</td>
    </tr>
    <tr>
      <th>CDaR Ratio at 95%</th>
      <td>0.0016</td>
      <td>0.0018</td>
    </tr>
    <tr>
      <th>Calmar Ratio</th>
      <td>0.00097</td>
      <td>0.0011</td>
    </tr>
    <tr>
      <th>Average Drawdown Ratio</th>
      <td>0.0063</td>
      <td>0.0070</td>
    </tr>
    <tr>
      <th>EDaR Ratio at 95%</th>
      <td>0.0013</td>
      <td>0.0015</td>
    </tr>
    <tr>
      <th>Ulcer Index Ratio</th>
      <td>0.0042</td>
      <td>0.0048</td>
    </tr>
    <tr>
      <th>Gini Mean Difference Ratio</th>
      <td>0.033</td>
      <td>0.037</td>
    </tr>
    <tr>
      <th>Avg nb of Assets per Portfolio</th>
      <td>64.0</td>
      <td>53.0</td>
    </tr>
    <tr>
      <th>Number of Portfolios</th>
      <td>28</td>
      <td>28</td>
    </tr>
    <tr>
      <th>Number of Failed Portfolios</th>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>Number of Fallback Portfolios</th>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>
</div>
</div>
<br />
<br />

**Total running time of the script:** (0 minutes 23.269 seconds)

<a id="sphx-glr-download-auto-examples-pre-selection-plot-2-select-best-performers-py"></a>
