redesigned plot API (can pass ax)

pull/279/head
robertmartin8 2021-02-10 15:32:05 +08:00
parent 6bb799d823
commit 3e2308ed27
5 changed files with 155 additions and 23 deletions

View File

@ -22,18 +22,60 @@ and add constraints like you normally would, but *before* calling an optimisatio
ef.add_constraint(lambda w: w[2] == 0.15)
ef.add_constraint(lambda w: w[3] + w[4] <= 0.10)
# 100 portfolios with risks between 0.10 and 0.30
risk_range = np.linspace(0.10, 0.40, 100)
ax = plotting.plot_efficient_frontier(ef, ef_param="risk", ef_param_range=risk_range,
show_assets=True, showfig=True)
fig, ax = plt.subplots()
plotting.plot_efficient_frontier(ef, ax=ax, show_assets=True)
plt.show()
This produces the following plot -- you can set attributes using the returned ``ax`` object:
This produces the following plot:
.. image:: ../media/ef_plot.png
:width: 80%
:align: center
:alt: the Efficient Frontier
You can explicitly pass a range of parameters (risk, utility, or returns) to generate a frontier::
# 100 portfolios with risks between 0.10 and 0.30
risk_range = np.linspace(0.10, 0.40, 100)
plotting.plot_efficient_frontier(ef, ef_param="risk", ef_param_range=risk_range,
show_assets=True, showfig=True)
We can easily generate more complex plots. The following script plots both the efficient frontier and
randomly generated (suboptimal) portfolios, coloured by the Sharpe ratio::
fig, ax = plt.subplots()
plotting.plot_efficient_frontier(ef, ax=ax, show_assets=False)
# Find the tangency portfolio
ef.max_sharpe()
ret_tangent, std_tangent, _ = ef.portfolio_performance()
ax.scatter(std_tangent, ret_tangent, marker="*", s=100, c="r", label="Max Sharpe")
# Generate random portfolios
n_samples = 10000
w = np.random.dirichlet(np.ones(len(mu)), n_samples)
rets = w.dot(mu)
stds = np.sqrt(np.diag(w @ S @ w.T))
sharpes = rets / stds
ax.scatter(stds, rets, marker=".", c=sharpes, cmap="viridis_r")
# Output
ax.set_title("Efficient Frontier with random portfolios")
ax.legend()
plt.tight_layout()
plt.savefig("ef_scatter.png", dpi=200)
plt.show()
This is the result:
.. image:: ../media/ef_scatter.png
:width: 80%
:align: center
:alt: the Efficient Frontier with random portfolios
Documentation reference
=======================
.. automodule:: pypfopt.plotting

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@ -30,11 +30,11 @@ def _plot_io(**kwargs):
:type filename: str, optional
:param dpi: dpi of figure to save or plot, defaults to 300
:type dpi: int (between 50-500)
:param showfig: whether to plt.show() the figure, defaults to True
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
"""
filename = kwargs.get("filename", None)
showfig = kwargs.get("showfig", True)
showfig = kwargs.get("showfig", False)
dpi = kwargs.get("dpi", 300)
plt.tight_layout()
@ -81,7 +81,7 @@ def plot_covariance(cov_matrix, plot_correlation=False, show_tickers=True, **kwa
return ax
def plot_dendrogram(hrp, show_tickers=True, **kwargs):
def plot_dendrogram(hrp, ax=None, show_tickers=True, **kwargs):
"""
Plot the clusters in the form of a dendrogram.
@ -92,18 +92,19 @@ def plot_dendrogram(hrp, show_tickers=True, **kwargs):
:type show_tickers: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to True
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
ax = ax or plt.gca()
if hrp.clusters is None:
hrp.optimize()
fig, ax = plt.subplots()
if show_tickers:
sch.dendrogram(hrp.clusters, labels=hrp.tickers, ax=ax, orientation="top")
plt.xticks(rotation=90)
ax.tick_params(axis="x", rotation=90)
plt.tight_layout()
else:
sch.dendrogram(hrp.clusters, no_labels=True, ax=ax)
@ -113,7 +114,7 @@ def plot_dendrogram(hrp, show_tickers=True, **kwargs):
return ax
def _plot_cla(cla, points, show_assets, **kwargs):
def _plot_cla(cla, points, ax, show_assets):
"""
Helper function to plot the efficient frontier from a CLA object
"""
@ -126,7 +127,6 @@ def _plot_cla(cla, points, show_assets, **kwargs):
mus, sigmas, _ = cla.frontier_values
fig, ax = plt.subplots()
ax.plot(sigmas, mus, label="Efficient frontier")
ax.scatter(optimal_risk, optimal_ret, marker="x", s=100, color="r", label="optimal")
@ -155,7 +155,7 @@ def _ef_default_returns_range(ef, points):
return np.linspace(min_ret, max_ret, points)
def _plot_ef(ef, ef_param, ef_param_range, show_assets):
def _plot_ef(ef, ef_param, ef_param_range, ax, show_assets):
"""
Helper function to plot the efficient frontier from an EfficientFrontier object
"""
@ -183,7 +183,6 @@ def _plot_ef(ef, ef_param, ef_param_range, show_assets):
mus.append(ret)
sigmas.append(sigma)
fig, ax = plt.subplots()
ax.plot(sigmas, mus, label="Efficient frontier")
if show_assets:
@ -198,7 +197,13 @@ def _plot_ef(ef, ef_param, ef_param_range, show_assets):
def plot_efficient_frontier(
opt, ef_param="return", ef_param_range=None, points=100, show_assets=True, **kwargs
opt,
ef_param="return",
ef_param_range=None,
points=100,
ax=None,
show_assets=True,
**kwargs
):
"""
Plot the efficient frontier based on either a CLA or EfficientFrontier object.
@ -218,18 +223,20 @@ def plot_efficient_frontier(
:type show_assets: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to True
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
ax = ax or plt.gca()
if isinstance(opt, CLA):
ax = _plot_cla(opt, points, show_assets=show_assets)
ax = _plot_cla(opt, points, ax=ax, show_assets=show_assets)
elif isinstance(opt, EfficientFrontier):
if ef_param_range is None:
ef_param_range = _ef_default_returns_range(opt, points)
ax = _plot_ef(opt, ef_param, ef_param_range, show_assets=show_assets)
ax = _plot_ef(opt, ef_param, ef_param_range, ax=ax, show_assets=show_assets)
else:
raise NotImplementedError("Please pass EfficientFrontier or CLA object")
@ -241,22 +248,25 @@ def plot_efficient_frontier(
return ax
def plot_weights(weights, **kwargs):
def plot_weights(weights, ax=None, **kwargs):
"""
Plot the portfolio weights as a horizontal bar chart
:param weights: the weights outputted by any PyPortfolioOpt optimiser
:type weights: {ticker: weight} dict
:param ax: ax to plot to, optional
:type ax: matplotlib.axes
:return: matplotlib axis
:rtype: matplotlib.axes object
:rtype: matplotlib.axes
"""
ax = ax or plt.gca()
desc = sorted(weights.items(), key=lambda x: x[1], reverse=True)
labels = [i[0] for i in desc]
vals = [i[1] for i in desc]
y_pos = np.arange(len(labels))
fig, ax = plt.subplots()
ax.barh(y_pos, vals)
ax.set_xlabel("Weight")
ax.set_yticks(y_pos)

View File

@ -1,21 +1,27 @@
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from tests.utilities_for_tests import get_data, setup_efficient_frontier
from pypfopt import plotting, risk_models, expected_returns
from pypfopt import HRPOpt, CLA, EfficientFrontier
def test_correlation_plot():
plt.figure()
df = get_data()
S = risk_models.CovarianceShrinkage(df).ledoit_wolf()
ax = plotting.plot_covariance(S, showfig=False)
assert len(ax.findobj()) == 256
plt.clf()
ax = plotting.plot_covariance(S, show_tickers=False, showfig=False)
assert len(ax.findobj()) == 136
plt.clf()
plt.close()
def test_dendrogram_plot():
plt.figure()
df = get_data()
returns = df.pct_change().dropna(how="all")
hrp = HRPOpt(returns)
@ -24,13 +30,17 @@ def test_dendrogram_plot():
ax = plotting.plot_dendrogram(hrp, showfig=False)
assert len(ax.findobj()) == 185
assert type(ax.findobj()[0]) == matplotlib.collections.LineCollection
plt.clf()
ax = plotting.plot_dendrogram(hrp, show_tickers=False, showfig=False)
assert len(ax.findobj()) == 65
assert type(ax.findobj()[0]) == matplotlib.collections.LineCollection
plt.clf()
plt.close()
def test_cla_plot():
plt.figure()
df = get_data()
rets = expected_returns.mean_historical_return(df)
S = risk_models.exp_cov(df)
@ -38,14 +48,34 @@ def test_cla_plot():
ax = plotting.plot_efficient_frontier(cla, showfig=False)
assert len(ax.findobj()) == 143
plt.clf()
ax = plotting.plot_efficient_frontier(cla, show_assets=False, showfig=False)
assert len(ax.findobj()) == 161
plt.clf()
plt.close()
def test_cla_plot_ax():
plt.figure()
df = get_data()
rets = expected_returns.mean_historical_return(df)
S = risk_models.exp_cov(df)
cla = CLA(rets, S)
fig, ax = plt.subplots(figsize=(12, 10))
plotting.plot_efficient_frontier(cla, ax=ax)
assert len(ax.findobj()) == 143
plt.close()
plt.close()
def test_default_ef_plot():
plt.figure()
ef = setup_efficient_frontier()
ax = plotting.plot_efficient_frontier(ef)
ax = plotting.plot_efficient_frontier(ef, show_assets=True)
assert len(ax.findobj()) == 125
plt.clf()
# with constraints
ef = setup_efficient_frontier()
@ -53,18 +83,24 @@ def test_default_ef_plot():
ef.add_constraint(lambda x: x[0] == 0.05)
ax = plotting.plot_efficient_frontier(ef)
assert len(ax.findobj()) == 125
plt.clf()
plt.close()
def test_ef_plot_utility():
plt.figure()
ef = setup_efficient_frontier()
delta_range = np.arange(0.001, 100, 1)
ax = plotting.plot_efficient_frontier(
ef, ef_param="utility", ef_param_range=delta_range, showfig=False
)
assert len(ax.findobj()) == 125
plt.clf()
plt.close()
def test_ef_plot_risk():
plt.figure()
ef = setup_efficient_frontier()
ef.min_volatility()
min_risk = ef.portfolio_performance()[1]
@ -75,18 +111,24 @@ def test_ef_plot_risk():
ef, ef_param="risk", ef_param_range=risk_range, showfig=False
)
assert len(ax.findobj()) == 125
plt.clf()
plt.close()
def ef_plot_return():
plt.figure()
ef = setup_efficient_frontier()
return_range = np.linspace(0, ef.expected_returns.max(), 50)
ax = plotting.plot_efficient_frontier(
ef, ef_param="return", ef_param_range=return_range, showfig=False
)
assert len(ax.findobj()) == 125
plt.clf()
plt.close()
def test_ef_plot_utility_short():
plt.figure()
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(None, None)
)
@ -95,9 +137,12 @@ def test_ef_plot_utility_short():
ef, ef_param="utility", ef_param_range=delta_range, showfig=False
)
assert len(ax.findobj()) == 161
plt.clf()
plt.close()
def test_constrained_ef_plot_utility():
plt.figure()
ef = setup_efficient_frontier()
ef.add_constraint(lambda w: w[0] >= 0.2)
ef.add_constraint(lambda w: w[2] == 0.15)
@ -108,9 +153,12 @@ def test_constrained_ef_plot_utility():
ef, ef_param="utility", ef_param_range=delta_range, showfig=False
)
assert len(ax.findobj()) == 125
plt.clf()
plt.close()
def test_constrained_ef_plot_risk():
plt.figure()
ef = EfficientFrontier(
*setup_efficient_frontier(data_only=True), weight_bounds=(None, None)
)
@ -125,9 +173,12 @@ def test_constrained_ef_plot_risk():
ef, ef_param="risk", ef_param_range=risk_range, show_assets=True, showfig=False
)
assert len(ax.findobj()) == 137
plt.clf()
plt.close()
def test_weight_plot():
plt.figure()
df = get_data()
returns = df.pct_change().dropna(how="all")
hrp = HRPOpt(returns)
@ -135,3 +186,32 @@ def test_weight_plot():
ax = plotting.plot_weights(w, showfig=False)
assert len(ax.findobj()) == 197
plt.clf()
plt.close()
def test_weight_plot_multi():
ef = setup_efficient_frontier()
w1 = ef.min_volatility()
ef = setup_efficient_frontier()
w2 = ef.max_sharpe()
fig, (ax1, ax2) = plt.subplots(2)
plotting.plot_weights(w1, ax1, showfig=False)
plotting.plot_weights(w2, ax2, showfig=False)
assert len(fig.axes) == 2
assert len(fig.axes[0].findobj()) == 209
assert len(fig.axes[1].findobj()) == 209
plt.close()
def test_weight_plot_add_attribute():
plt.figure()
ef = setup_efficient_frontier()
w = ef.min_volatility()
ax = plotting.plot_weights(w)
ax.set_title("Test")
assert len(ax.findobj()) == 209
plt.close()