Skip to content

How to Build and Plot an Efficient Frontier in Python

Original question

How do I construct an efficient frontier in Python and visualise the trade-off between portfolio risk and expected return?
Solve a minimum-variance problem for each target return, then plot the resulting expected returns against portfolio volatility. With long-only weights that sum to one, the efficient targets run from the global minimum-variance portfolio's return to the highest expected asset return. A constrained optimiser handles the change in weights when an asset reaches zero allocation.

Find the part of the frontier that is efficient

Let μ\mu be the vector of expected returns and Σ\Sigma the covariance matrix for the same horizon. Use w⊤Σww^\top\Sigma w as the objective, impose ∑iwi=1\sum_i w_i=1 and wi≥0w_i\ge0, and add μ⊤w=m\mu^\top w=m at each target mm. Start by solving without the target equality to get the global minimum-variance point. For a long-only portfolio with only a budget constraint, the upper return endpoint is max⁡iμi\max_i\mu_i because a weighted average cannot exceed its largest component.

Plot w⊤Σw\sqrt{w^\top\Sigma w} horizontally and μ⊤w\mu^\top w vertically. Store the weights with each point so you can inspect whether the higher return relies on concentrated holdings or a small change in several allocations.

Three sleeves and two constraints to check

Use synthetic annual inputs for an equity sleeve, a bond sleeve and short bills. Their expected returns are 8%, 4% and 2.5%, and their volatilities are 18%, 8% and 2.5%. Assume a 0.20 correlation between equities and bonds and low positive correlations with bills. A 6% target requires at least 50% in equities because the other sleeves return at most 4%. Pairing 50% equities with 50% bonds achieves exactly 6% at about 10.55% volatility. The optimiser assigns bills zero weight at this target. The extra equities needed to fund bills would increase variance here. At the minimum-variance end, bills receive most of the weight, so the solution changes as the return constraint tightens.
When sweeping target returns, keep solver status with every point. Recompute w⊤μw^\top\mu and w⊤Σw\sqrt{w^\top\Sigma w} from the returned weights and check that they respect the requested target and constraints within tolerance. Connecting the dots from unsuccessful solves can make an attractive curve with infeasible points. A minimum-variance solution already dominates any target with less expected return and more risk. That is why the frontier for the long-only sweep starts at the global minimum-variance return.

Constrained optimisation in Python

import matplotlib.pyplot as plt import numpy as np from scipy.optimize import LinearConstraint, minimize mu = np.array([0.08, 0.04, 0.025]) vol = np.array([0.18, 0.08, 0.025]) corr = np.array([[1.0, 0.20, 0.05], [0.20, 1.0, 0.10], [0.05, 0.10, 1.0]]) cov = corr * np.outer(vol, vol) ones = np.ones(mu.size) def variance(weights): return weights @ cov @ weights def minimise(constraints, starting_weights): result = minimize( variance, starting_weights, jac=lambda w: 2 * cov @ w, method="SLSQP", bounds=[(0, 1)] * mu.size, constraints=constraints, options={"ftol": 1e-12}, ) if not result.success: raise RuntimeError(result.message) return result.x minimum = minimise(LinearConstraint(ones, 1, 1), ones / mu.size) minimum_return = mu @ minimum highest = np.eye(mu.size)[mu.argmax()] points = [] for fraction in np.linspace(0, 1, 41): start = (1 - fraction) * minimum + fraction * highest target = (1 - fraction) * minimum_return + fraction * mu.max() limits = LinearConstraint(np.vstack([ones, mu]), [1, target], [1, target]) weights = minimise(limits, start) points.append((np.sqrt(variance(weights)), mu @ weights)) risk, expected_return = 100 * np.asarray(points).T plt.plot(risk, expected_return) plt.scatter(100 * vol, 100 * mu, label="Individual sleeves") plt.xlabel("Annual volatility (%)") plt.ylabel("Expected annual return (%)") plt.legend() plt.show()

Interactive example

See how correlation changes two-asset portfolio risk

Asset A has 15% annual volatility and asset B has 25%. Move the correlation or the long-only weight in A.

Selected portfolio volatility 15.03% · minimum-risk weight in A 80.19%
Portfolio volatility as the weight in asset A changes1520250255075100Weight in asset A (%)Annual volatility (%)
  • Portfolio volatility
  • Selected weight

References and next steps