<a id="sphx-glr-auto-examples-mean-risk-plot-15-mip-cardinality-constraints-py"></a>

<a id="cardinality-constraints"></a>

# Cardinality Constraints

This tutorial shows how to use cardinality constraints with the
[`MeanRisk`](https://skfolio.org/generated/skfolio.optimization.MeanRisk.html.md#skfolio.optimization.MeanRisk) optimization.

Cardinality constraint controls the total number of invested assets (non-zero weights)
in the portfolio. Cardinality constraints can also be specified for asset groups
(e.g., tech, healthcare).

In a previous tutorial, we showed how to reduce the number of assets using L1
regularization. However, asset managers sometimes require more granularity and
precision regarding the exact number of assets allowed, both in total and per group.

Cardinality constraints require a mixed-integer solver. For an open-source option,
we recommend using SCIP by setting `solver="SCIP"`. To install it, use:
`pip install cvxpy[SCIP]`. For commercial solvers, supported options include
MOSEK, GUROBI, or CPLEX.

Mixed-Integer Programming (MIP) involves optimization with both continuous and integer
variables and is inherently non-convex due to the discrete nature of integer variables.
Over recent decades, MIP solvers have significantly advanced, utilizing methods
like Branch and Bound and cutting planes to improve efficiency.

By leveraging specialized techniques such as homogenization and the Big M method,
combined with problem-specific calibration, Skfolio can reformulate these complex
problems into a Mixed-Integer Program that can be efficiently solved using these
solvers.

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

## Data

We load the S&P 500 [dataset](https://skfolio.org/user_guide/datasets.html.md#datasets) composed of the daily prices of 20
assets from the S&P 500 Index composition starting from 2018-01-02 up to 2022-12-28.

```Python
from plotly.io import show
from sklearn.model_selection import train_test_split

from skfolio import PerfMeasure, RatioMeasure, RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import MeanRisk
from skfolio.preprocessing import prices_to_returns

prices = load_sp500_dataset()
prices = prices["2018":]
X = prices_to_returns(prices)
```

<a id="cardinality-constraint"></a>

## Cardinality Constraint

let’s use a Minimum CVaR model and limit the total number of assets to 5:

```Python
model = MeanRisk(risk_measure=RiskMeasure.CVAR, cardinality=5, solver="SCIP")
model.fit(X)
model.weights_
```

```none
array([0.        , 0.        , 0.        , 0.        , 0.        ,
       0.        , 0.        , 0.        , 0.        , 0.18903764,
       0.        , 0.28559484, 0.        , 0.        , 0.12448701,
       0.1392389 , 0.        , 0.        , 0.26164161, 0.        ])
```

We notice that the number of non-zero weights is indeed equal to 5.
You can change the default solver parameters using `solver_params`.
For more details about solver arguments, check the CVXPY
documentation: [https://www.cvxpy.org/tutorial/solvers](https://www.cvxpy.org/tutorial/solvers)

<a id="cardinality-constraint-per-group"></a>

## Cardinality Constraint per Group

First, let’s assign two groups to each asset: sector and capitalization.

```Python
groups = {
    "AAPL": ["Technology", "Mega Cap"],
    "AMD": ["Technology", "Large Cap"],
    "BAC": ["Financials", "Mega Cap"],
    "BBY": ["Consumer", "Large Cap"],
    "CVX": ["Energy", "Mega Cap"],
    "GE": ["Industrials", "Large Cap"],
    "HD": ["Consumer", "Mega Cap"],
    "JNJ": ["Healthcare", "Mega Cap"],
    "JPM": ["Financials", "Mega Cap"],
    "KO": ["Consumer", "Mega Cap"],
    "LLY": ["Healthcare", "Mega Cap"],
    "MRK": ["Healthcare", "Mega Cap"],
    "MSFT": ["Technology", "Mega Cap"],
    "PEP": ["Consumer", "Mega Cap"],
    "PFE": ["Healthcare", "Mega Cap"],
    "PG": ["Consumer", "Mega Cap"],
    "RRC": ["Energy", "Small Cap"],
    "UNH": ["Healthcare", "Mega Cap"],
    "WMT": ["Consumer", "Mega Cap"],
    "XOM": ["Energy", "Mega Cap"],
}
```

Let’s restrict the maximum number of assets in the following groups:

> * Healthcare: 2
> * Small Cap: 1
> * Mega Cap: 4

```Python
model = MeanRisk(
    risk_measure=RiskMeasure.CVAR,
    groups=groups,
    group_cardinalities={"Healthcare": 2, "Small Cap": 1, "Mega Cap": 4},
    solver="SCIP",
)
model.fit(X)
model.weights_
```

```none
array([0.        , 0.        , 0.        , 0.        , 0.        ,
       0.        , 0.        , 0.        , 0.        , 0.22548433,
       0.        , 0.30773101, 0.        , 0.        , 0.1394078 ,
       0.        , 0.01095357, 0.        , 0.31642328, 0.        ])
```

We can see that the maximum number of assets per group has been respected:

```Python
portfolio = model.predict(X)
print({k: groups[k] for k in portfolio.composition.index})
```

```none
{'WMT': ['Consumer', 'Mega Cap'], 'MRK': ['Healthcare', 'Mega Cap'], 'KO': ['Consumer', 'Mega Cap'], 'PFE': ['Healthcare', 'Mega Cap'], 'RRC': ['Energy', 'Small Cap']}
```

<a id="efficient-frontier"></a>

## Efficient Frontier

Let’s plot the efficient frontiers of cardinality-constrained and unconstrained
mean-CVaR portfolios on the training set and analyze the results on the test set.
We will focus only on the portfolios on the frontier that have a CVaR at 95% below 5%:

```Python
X_train, X_test = train_test_split(X, test_size=0.33, shuffle=False)

model = MeanRisk(
    risk_measure=RiskMeasure.CVAR,
    efficient_frontier_size=20,
    max_cvar=0.05,
    # Name and tag are used to improve plot readability
    portfolio_params=dict(name="Unconstrained", tag="Unconstrained"),
)
model.fit(X_train)

model_constrained = MeanRisk(
    risk_measure=RiskMeasure.CVAR,
    efficient_frontier_size=20,
    max_cvar=0.05,
    cardinality=3,
    solver="SCIP",
    portfolio_params=dict(name="Constrained", tag="Constrained"),
)
model_constrained.fit(X_train)

population_train = model.predict(X_train) + model_constrained.predict(X_train)
population_test = model.predict(X_test) + model_constrained.predict(X_test)

fig = population_train.plot_measures(
    x=RiskMeasure.CVAR,
    y=PerfMeasure.ANNUALIZED_MEAN,
    color_scale=RatioMeasure.CVAR_RATIO,
    hover_measures=[RatioMeasure.ANNUALIZED_SHARPE_RATIO],
)
show(fig)
```

<!doctype html>
[plotly figure stripped from llms output]

<br/>

Let’s plot the compositions:

```Python
population_train.plot_composition()
```

[plotly figure stripped from llms output]
<br />
<br />

Finally, we can analyse the test population using methods such as:

> * `population_test.summary()`
> * `population_test.plot_cumulative_returns()`
> * `population_test.plot_distribution(measure_list=[RatioMeasure.CVAR_RATIO, RatioMeasure.ANNUALIZED_SHARPE_RATIO])`
> * `population_test.plot_rolling_measure(measure=RatioMeasure.CVAR_RATIO)`

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

<a id="sphx-glr-download-auto-examples-mean-risk-plot-15-mip-cardinality-constraints-py"></a>
