# This cell is added by sphinx-gallery
# It can be customized to whatever you like
%matplotlib inline

3-qubit Ising model in PyTorch

The interacting spins with variable coupling strengths of an Ising model can be used to simulate various machine learning concepts like Hopfield networks and Boltzmann machines (Schuld & Petruccione (2018)). They also closely imitate the underlying mathematics of a subclass of computational problems called Quadratic Unconstrained Binary Optimization (QUBO) problems.

Ising models are commonly encountered in the subject area of adiabatic quantum computing. Quantum annealing algorithms (for example, as performed on a D-wave system) are often used to find low-energy configurations of Ising problems. The optimization landscape of the Ising model is non-convex, which can make finding global minima challenging. In this tutorial, we get a closer look at this phenomenon by applying gradient descent techniques to a toy Ising model.

PennyLane implementation

This basic tutorial optimizes a 3-qubit Ising model using the PennyLane default.qubit device with PyTorch. In the absence of external fields, the Hamiltonian for this system is given by:

\[H=-\sum_{<i,j>} J_{ij} \sigma_i \sigma_{j},\]

where each spin can be in the +1 or -1 spin state and \(J_{ij}\) are the nearest-neighbour coupling strengths.

For simplicity, the first spin can be assumed to be in the “up” state (+1 eigenstate of Pauli-Z operator) and the coupling matrix can be set to \(J = [1,-1]\). The rotation angles for the other two spins are then optimized so that the energy of the system is minimized for the given couplings.

import torch
from torch.autograd import Variable
import pennylane as qml
from pennylane import numpy as np

A three-qubit quantum circuit is initialized to represent the three spins:

dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev, interface="torch") 
def circuit(p1, p2):
    # We use the general Rot(phi,theta,omega,wires) single-qubit operation
    qml.Rot(p1[0], p1[1], p1[2], wires=1)
    qml.Rot(p2[0], p2[1], p2[2], wires=2)
    return [qml.expval(qml.PauliZ(i)) for i in range(3)]

The cost function to be minimized is defined as the energy of the spin configuration:

def cost(var1, var2):
    # the circuit function returns a numpy array of Pauli-Z expectation values
    spins = circuit(var1, var2)

    # the expectation value of Pauli-Z is +1 for spin up and -1 for spin down
    energy = -(1 * spins[0] * spins[1]) - (-1 * spins[1] * spins[2])
    return energy

Sanity check

Let’s test the functions above using the \([s_1, s_2, s_3] = [1, -1, -1]\) spin configuration and the given coupling matrix. The total energy for this Ising model should be:

\[H = -1(J_1 s_1 \otimes s_2 + J_2 s_2 \otimes s3) = 2\]
test1 = torch.tensor([0, np.pi, 0])
test2 = torch.tensor([0, np.pi, 0])

cost_check = cost(test1, test2)
print("Energy for [1, -1, -1] spin configuration:", cost_check)
Energy for [1, -1, -1] spin configuration: tensor(2.0000, dtype=torch.float64)

Random initialization

torch.manual_seed(56)
p1 = Variable((np.pi * torch.rand(3, dtype=torch.float64)), requires_grad=True)
p2 = Variable((np.pi * torch.rand(3, dtype=torch.float64)), requires_grad=True)

var_init = [p1, p2]
cost_init = cost(p1, p2)

print("Randomly initialized angles:")
print(p1)
print(p2)
print("Corresponding cost before optimization:")
print(cost_init)
Randomly initialized angles:
tensor([1.9632, 2.6022, 2.3277], dtype=torch.float64, requires_grad=True)
tensor([0.6521, 2.8474, 2.4300], dtype=torch.float64, requires_grad=True)
Corresponding cost before optimization:
tensor(1.6792, dtype=torch.float64, grad_fn=<SubBackward0>)

Optimization

Now we use the PyTorch gradient descent optimizer to minimize the cost:

opt = torch.optim.SGD(var_init, lr=0.1)

def closure():
    opt.zero_grad()
    loss = cost(p1, p2)
    loss.backward()
    return loss

var_pt = [var_init]
cost_pt = [cost_init]
x = [0]

for i in range(100):
    opt.step(closure)
    if (i + 1) % 5 == 0:
        x.append(i)
        p1n, p2n = opt.param_groups[0]["params"]
        costn = cost(p1n, p2n)
        var_pt.append([p1n, p2n])
        cost_pt.append(costn)

        # for clarity, the angles are printed as numpy arrays
        print("Energy after step {:5d}: {: .7f} | Angles: {}".format(
            i+1, costn, [p1n.detach().numpy(), p2n.detach().numpy()]),"\n"
        )
Energy after step     5:  0.6846474 | Angles: [array([1.96323939, 1.93604492, 2.32767565]), array([0.65212549, 2.73080219, 2.4299563 ])] 

Energy after step    10: -1.0138530 | Angles: [array([1.96323939, 1.0136468 , 2.32767565]), array([0.65212549, 2.73225282, 2.4299563 ])] 

Energy after step    15: -1.8171995 | Angles: [array([1.96323939, 0.38483073, 2.32767565]), array([0.65212549, 2.85992571, 2.4299563 ])] 

Energy after step    20: -1.9686584 | Angles: [array([1.96323939, 0.13026452, 2.32767565]), array([0.65212549, 2.97097572, 2.4299563 ])] 

Energy after step    25: -1.9930403 | Angles: [array([1.96323939, 0.04302756, 2.32767565]), array([0.65212549, 3.04042222, 2.4299563 ])] 

Energy after step    30: -1.9980133 | Angles: [array([1.96323939, 0.01413292, 2.32767565]), array([0.65212549, 3.08179844, 2.4299563 ])] 

Energy after step    35: -1.9993550 | Angles: [array([1.96323939, 0.00463472, 2.32767565]), array([0.65212549, 3.10627578, 2.4299563 ])] 

Energy after step    40: -1.9997802 | Angles: [array([1.96323939e+00, 1.51911413e-03, 2.32767565e+00]), array([0.65212549, 3.12073668, 2.4299563 ])] 

Energy after step    45: -1.9999239 | Angles: [array([1.96323939e+00, 4.97829828e-04, 2.32767565e+00]), array([0.65212549, 3.12927707, 2.4299563 ])] 
Energy after step    50: -1.9999735 | Angles: [array([1.96323939e+00, 1.63134183e-04, 2.32767565e+00]), array([0.65212549, 3.13432035, 2.4299563 ])] 
Energy after step    55: -1.9999908 | Angles: [array([1.96323939e+00, 5.34564150e-05, 2.32767565e+00]), array([0.65212549, 3.13729842, 2.4299563 ])] 
Energy after step    60: -1.9999968 | Angles: [array([1.96323939e+00, 1.75166673e-05, 2.32767565e+00]), array([0.65212549, 3.13905695, 2.4299563 ])] 

Energy after step    65: -1.9999989 | Angles: [array([1.96323939e+00, 5.73986944e-06, 2.32767565e+00]), array([0.65212549, 3.14009534, 2.4299563 ])] 

Energy after step    70: -1.9999996 | Angles: [array([1.96323939e+00, 1.88084132e-06, 2.32767565e+00]), array([0.65212549, 3.14070851, 2.4299563 ])] 

Energy after step    75: -1.9999999 | Angles: [array([1.96323939e+00, 6.16314188e-07, 2.32767565e+00]), array([0.65212549, 3.14107057, 2.4299563 ])] 

Energy after step    80: -2.0000000 | Angles: [array([1.96323939e+00, 2.01953845e-07, 2.32767565e+00]), array([0.65212549, 3.14128437, 2.4299563 ])] 

Energy after step    85: -2.0000000 | Angles: [array([1.96323939e+00, 6.61762372e-08, 2.32767565e+00]), array([0.65212549, 3.14141062, 2.4299563 ])] 

Energy after step    90: -2.0000000 | Angles: [array([1.96323939e+00, 2.16846296e-08, 2.32767565e+00]), array([0.65212549, 3.14148516, 2.4299563 ])] 

Energy after step    95: -2.0000000 | Angles: [array([1.96323939e+00, 7.10561943e-09, 2.32767565e+00]), array([0.65212549, 3.14152918, 2.4299563 ])] 

Energy after step   100: -2.0000000 | Angles: [array([1.96323939e+00, 2.32836938e-09, 2.32767565e+00]), array([0.65212549, 3.14155517, 2.4299563 ])] 

Note

When using the PyTorch optimizer, keep in mind that:

  1. loss.backward() computes the gradient of the cost function with respect to all parameters with requires_grad=True.

  2. opt.step() performs the parameter update based on this current gradient and the learning rate.

  3. opt.zero_grad() sets all the gradients back to zero. It’s important to call this before loss.backward() to avoid the accumulation of gradients from multiple passes.

Hence, its standard practice to define the closure() function that clears up the old gradient, evaluates the new gradient and passes it onto the optimizer in each step.

The minimum energy is -2 for the spin configuration \([s_1, s_2, s_3] = [1, 1, -1]\) which corresponds to \((\phi, \theta, \omega) = (0, 0, 0)\) for the second spin and \((\phi, \theta, \omega) = (0, \pi, 0)\) for the third spin. Note that gradient descent optimization might not find this global minimum due to the non-convex cost function, as is shown in the next section.

p1_final, p2_final = opt.param_groups[0]["params"]
print("Optimized angles:")
print(p1_final)
print(p2_final)
print("Final cost after optimization:")
print(cost(p1_final, p2_final))
Optimized angles:
tensor([1.9632e+00, 2.3284e-09, 2.3277e+00], dtype=torch.float64,
       requires_grad=True)
tensor([0.6521, 3.1416, 2.4300], dtype=torch.float64, requires_grad=True)
Final cost after optimization:
tensor(-2.0000, dtype=torch.float64, grad_fn=<SubBackward0>)
import matplotlib
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(6, 4))

# Enable processing the Torch trainable tensors
with torch.no_grad():
    plt.plot(x, cost_pt, label = 'global minimum')
    plt.xlabel("Optimization steps")
    plt.ylabel("Cost / Energy")
    plt.legend()
    plt.show()
../../../../_images/813d145c39e1d786ce8f7be9386fac61e42c95a64eab1b18bf012a03bdaf43d5.png

Local minimum

If the spins are initialized close to the local minimum of zero energy, the optimizer is likely to get stuck here and never find the global minimum at -2.

torch.manual_seed(9)
p3 = Variable((np.pi*torch.rand(3, dtype = torch.float64)), requires_grad = True)
p4 = Variable((np.pi*torch.rand(3, dtype = torch.float64)), requires_grad = True)

var_init_loc = [p3, p4]
cost_init_loc = cost(p3, p4)

print("Corresponding cost before optimization:")
print(cost_init_loc)
Corresponding cost before optimization:
tensor(0.0082, dtype=torch.float64, grad_fn=<SubBackward0>)
opt = torch.optim.SGD(var_init_loc, lr = 0.1)

def closure():
    opt.zero_grad()
    loss = cost(p3, p4)
    loss.backward()
    return loss

var_pt_loc = [var_init_loc]
cost_pt_loc = [cost_init_loc]

for j in range(100):
    opt.step(closure)
    if (j + 1) % 5 == 0:
        p3n, p4n = opt.param_groups[0]['params']
        costn = cost(p3n, p4n)
        var_pt_loc.append([p3n, p4n])
        cost_pt_loc.append(costn)

        # for clarity, the angles are printed as numpy arrays
        print('Energy after step {:5d}: {: .7f} | Angles: {}'.format(
            j+1, costn, [p3n.detach().numpy(), p4n.detach().numpy()]),"\n"
        )
Energy after step     5:  0.0032761 | Angles: [array([0.77369911, 2.63471297, 1.07981163]), array([0.26038622, 0.08659858, 1.91060734])] 

Energy after step    10:  0.0013137 | Angles: [array([0.77369911, 2.63406019, 1.07981163]), array([0.26038622, 0.05483683, 1.91060734])] 

Energy after step    15:  0.0005266 | Angles: [array([0.77369911, 2.63379816, 1.07981163]), array([0.26038622, 0.03471974, 1.91060734])] 

Energy after step    20:  0.0002111 | Angles: [array([0.77369911, 2.63369307, 1.07981163]), array([0.26038622, 0.02198151, 1.91060734])] 

Energy after step    25:  0.0000846 | Angles: [array([0.77369911, 2.63365094, 1.07981163]), array([0.26038622, 0.01391648, 1.91060734])] 
Energy after step    30:  0.0000339 | Angles: [array([0.77369911, 2.63363405, 1.07981163]), array([0.26038622, 0.00881044, 1.91060734])] 

Energy after step    35:  0.0000136 | Angles: [array([0.77369911, 2.63362729, 1.07981163]), array([0.26038622, 0.00557782, 1.91060734])] 

Energy after step    40:  0.0000054 | Angles: [array([0.77369911, 2.63362457, 1.07981163]), array([0.26038622, 0.00353126, 1.91060734])] 

Energy after step    45:  0.0000022 | Angles: [array([0.77369911, 2.63362348, 1.07981163]), array([0.26038622, 0.00223561, 1.91060734])] 

Energy after step    50:  0.0000009 | Angles: [array([0.77369911, 2.63362305, 1.07981163]), array([2.60386222e-01, 1.41534339e-03, 1.91060734e+00])] 
Energy after step    55:  0.0000004 | Angles: [array([0.77369911, 2.63362287, 1.07981163]), array([2.60386222e-01, 8.96040252e-04, 1.91060734e+00])] 
Energy after step    60:  0.0000001 | Angles: [array([0.77369911, 2.6336228 , 1.07981163]), array([2.60386222e-01, 5.67274421e-04, 1.91060734e+00])] 

Energy after step    65:  0.0000001 | Angles: [array([0.77369911, 2.63362278, 1.07981163]), array([2.60386222e-01, 3.59135947e-04, 1.91060734e+00])] 

Energy after step    70:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 2.27365491e-04, 1.91060734e+00])] 

Energy after step    75:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 1.43942891e-04, 1.91060734e+00])] 

Energy after step    80:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 9.11288509e-05, 1.91060734e+00])] 
Energy after step    85:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 5.76927932e-05, 1.91060734e+00])] 

Energy after step    90:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 3.65247488e-05, 1.91060734e+00])] 

Energy after step    95:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 2.31234648e-05, 1.91060734e+00])] 

Energy after step   100:  0.0000000 | Angles: [array([0.77369911, 2.63362276, 1.07981163]), array([2.60386222e-01, 1.46392417e-05, 1.91060734e+00])] 
fig = plt.figure(figsize=(6, 4))

# Enable processing the Torch trainable tensors
with torch.no_grad():
    plt.plot(x, cost_pt_loc, 'r', label = 'local minimum')
    plt.xlabel("Optimization steps")
    plt.ylabel("Cost / Energy")
    plt.legend()
    plt.show()
../../../../_images/fbaf564ef75943463e102b7afb28255703668d3f495417fd0661b00843eca61d.png

Send it after class |

Try with different initialization parameters and see how the results change.

Two Types of Quantum Computers

It is not well-known what problems quantum computers can solve. We can classify quantum computers into “gate model” and “quantum annealing”. They have different use cases and architecture.

Gate Model

The quantum gate model computers are designed for universal purposes by applying gates to build complex algorithms. They can be considered the upward compatible machines of classical computers but exponentially fast if appropriate quantum algorithms like Shor and Grover are used. The applications include prime factorisation, which becomes a threat to the existing secure data transmission methods over the internet using RSA (Rivest–Shamir–Adleman) keys, quantum simulations which have the potential for finding new medicines, and machine learning. However, the gate model requires high-quality qubit processors; the number of qubits is limited to two digits (except for IBM’s exploratory 127-qubit Eagle system.)

Quantum Annealing

On the other hand, quantum annealers shine when they are used to search for the best from a vast number of possible combinations. Such challenges are called combinatorial optimisation problems. Many applications of quantum annealing have been reported recently. There are also researches to develop novel machine learning algorithms using quantum annealers. Amin et al. showed that there were possibilities to use quantum annealing hardware as a sampler for Boltzmann Machine by exploiting its quantum nature.

In physics, the minimum energy principle states that the internal energy decreases and approaches the minimum values. If we can formulate our problems in the form of an energy minimisation equation, the quantum annealer will be able to search for the best possible answer.

Quantum annealer is relatively noise-tolerant compared to the gate model. At the time of this writing, the most considerable number of qubits on a quantum processing unit (QPU) is at least five thousand provided by D-Wave’s Advantage solver. It is expected to increase more in the coming years. Five thousand qubits is a large number compared to what is achieved by the gate model. However, what we obtain from the annealer is as limited as a set of 5k-bit values per sampling. Furthermore, not all qubits are connected. Therefore, a hybrid system of classical computing and the quantum annealer is being developed to complement the capabilities of the two systems.

Annealing is a technique used to make quality iron products by heating the material to a certain degree of temperature and cooling it down slowly. The iron’s mechanical characteristics are altered through the heat treatment, which reduces the hardness and increases the elasticity.

Simulated annealing (SA) is a probabilistic technique whose name and idea are derived from annealing in material science. SA is a meta-heuristic in which algorithms approximate the global minimum or maximum of a given function. It uses a parameter called temperature that is initialised with a high value and decreases as the algorithm proceeds, like the physical annealing. High-temperature value promotes the algorithm to look for the various parts of the problem space, which helps to avoid being stuck in local minima or maxima. When the temperature goes down to zero, the state reaches the optimal point if successful. Going through the SA algorithm is outside the scope of this article, and the details on how it works can be found, for example, on the MathWorks website.

Quantum annealing (QA) is an algorithm to solve combinatorial optimisation problems mainly. Instead of using temperature to explore the problem space, QA uses the laws of quantum mechanics to measure the energy state. Starting from the qubits’ dual state, where all the possible combinations of the values are equally likely to happen, we can gradually reduce such quantum mechanical effects by the process called quantum fluctuation to reach the most optimal point where the lowest energy state is realised.

Kadowaki and Nishimori proved that QA converges to the optimal state with a much larger probability than SA in almost all cases on classical computers. The actual quantum annealing machines were developed by D-Wave and built on the ground of their theoretical framework.

Formulating Problem for QA

QA machines are specialised hardware to solve combinatorial optimisation problems. These problems can be found in many places in our life. For example, an Amazon delivery person needs to find the most optimal route to deliver all parcels given for the day. The objective is to find a route with the shortest distance to visit all the delivery addresses. We can treat the distance as the energy in the QA context and find the path that reaches the ground state. We can also think of a futuristic example where multiple autonomous cars are navigated to optimise the traffic in a specific city area with heavy traffic jams. The objective is to reduce the traffic by minimising the number of overlaps of cars taking the same route simultaneously while each car aims to optimise the travel time to a destination.

Quantum annealers require an objective function to be defined in the Ising model or Quadratic Unconstrained Binary Optimisation (QUBO). Ising model is a mathematical model in statistical mechanics. QUBO is mathematically equivalent to the Ising model and is much simpler to formulate a problem.

QUBO is a minimisation objective function expressed as follows:

\[ H_{QUBO}(x)=\sum_{i=1}^N \sum_{j=1}^N Q_{ij}x_ix_j \]

where \(x_i\) and \(x_j\_ {i,j =0,1,…,N}\) takes 0 or 1. \(Q_{ij}\) is a coefficient matrix of size \(n \times n\), which we need to prepare for a QA machine. In case of a problem to search for a global maximum, we can flip the sign to minus to convert the problem into a minimisation problem.

In reality, a problem must be satisfied with one or more constraints. For example, a knapsack problem has a constraint of how many weights you can pack things in the bag. Such constraints are multiplied by weight parameters and are added to the objective function as penalty terms.

Implementation Using Quantum Annealer

In this section, we would like to code and solve a simple combinatorial optimisation problem in Python and the real Quantum Annealer by D-Wave.

Preparation

Before starting, we need to sign up for a cloud computing service. D-wave offers trial access to QA solvers with a sixty-second computational time allowance for one month. Please go to D-Wave Leap to find out more. After successful registration, the API token is available on the dashboard.

Problem Setting

We would like to challenge a simplified portfolio optimisation problem. Suppose there are twenty candidate stocks from FTSE100 to invest in. Our objective is to maximise an expected return rate at the minimum risk while keeping more or less ten stocks in our portfolio.

We will take the following six steps to solve the problem:

  1. Import historical stock price data from the internet

  2. Prepare data by cleansing and feature engineering

  3. Formulate the cost functions and constraints to obtain a QUBO matrix

  4. Sample optimisation results from a D-Wave solver

  5. Sample optimisation results from a simulated quantum annealing solver

  6. Evaluate the outcomes

Each of the above steps is demonstrated in a Colab notebook found in Python Coding in Colab Notebook section.

Dependencies

Some notes on the Python requirements:

  • yfinance is used to retrieve stock price data from Yahoo! Finance.

  • Prophet is a python package used to make time-series forecasting. It is possible to do multivariate time series forecasting and add holiday effects easily.

  • D-Wave Ocean SDK needs to be installed. The SDK includes packages that are necessary to communicate with D-Wave resources.

  • PyQUBO is a library that can construct QUBO from cost and constraints functions.

  • OpenJIJ is an open-source meta-heuristics library for the Ising model and QUBO. It simulates the quantum annealers and can be used to experiment without the D-Wave computers. It accepts QUBOs stored in a Python dictionary type.

Cost and Constraint Function

Our objective in this example is to minimise the sum of two cost functions and one constraint term, namely:

  1. the maximisation of return rates

  2. the minimisation of risks by lowering portfolio variance

  3. the minimisation of the penalty term

  4. The maximisation of the return rates can be formulated as a minimisation cost function:

(1)\[ H_{cost1}=-\sum_{i=1}^N R_ix_i \]

where \(R_i\) is the return rate of the \(i\)th stock \(x_i\). Because \(x_i\) only takes a binary value \(0\) or \(1\), \(x_i\) is equivalent with the quadratic term \(x_i \times x_i\). Therefore, \(H_{cost1}\) function can be equivalent to the following QUBO:

(2)\[ H_{cost1_{QUBO}} = -\sum_{i=j\, i,j=1}^NR_{ij}x_ix_j \]
  1. The minimisation of portfolio variance is represented as :

(3)\[ H_{cost2}=\sum_{i=1}^N\sum_{j=1}^NW_{ij}x_ix_j \]

where W denotes a covariance matrix.

  1. The minimisation of the penalty can be formed in mean squared error (MSE) as a constraint term:

(4)\[ H_{const}=(\sum_{i=1}^N x_i- K)^2 \]

where K is the number of stocks to be selected for the portfolio.

\(H_{const}\) can also be transformed to the equivalent QUBOs by expanding the formula:

\[\begin{split} H_{const}=(\sum_{i=1}^N x_i- K)^2\\ =(\sum_{i=1}^N x_i- K)(\sum_{j=1}^N x_j- K)\\=\sum_{i,j=1}^N x_ix_j-2K\sum_{i=1}^Nx_i + K^2 \end{split}\]

(Constraint term expanded for QUBO formulation)

\(-2K\sum_{i=1}^Nx_i\) part can also be equivalent to the quadratic form, as seen in the above \(H_{cost1}\) function. \(K^2\) is a constant value called offset which is not included in the QUBO matrix.

We will use the PyQUBO library in Python code to generate the QUBO matrix from functions for simplicity.

Thus, we can solve the problem by finding such x that minimises the function H(x) = (H_cost1 + H_cost2 + lambda*H_const), where lambda is a weight parameter for the constraint

1. Import historical stock price data from the internet

First of all, Install Yahoo finance library to retrieve stock data. We will import randomly selected 20 stocks from FTSE100 and extract the historical closing prices for the past 2 years from July 2020.

!pip install yfinance
/bin/bash: /home/obm/Prog/miniconda3/envs/qml/lib/libtinfo.so.6: no version information available (required by /bin/bash)
Requirement already satisfied: yfinance in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (0.2.3)
Requirement already satisfied: numpy>=1.16.5 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (1.23.4)
Requirement already satisfied: lxml>=4.9.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (4.9.2)
Requirement already satisfied: frozendict>=2.3.4 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (2.3.4)
Requirement already satisfied: beautifulsoup4>=4.11.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (4.11.1)
Requirement already satisfied: pandas>=1.3.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (1.5.1)
Requirement already satisfied: pytz>=2022.5 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (2022.7)
Requirement already satisfied: appdirs>=1.4.4 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (1.4.4)
Requirement already satisfied: multitasking>=0.0.7 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (0.0.11)
Requirement already satisfied: html5lib>=1.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (1.1)
Requirement already satisfied: cryptography>=3.3.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (37.0.1)
Requirement already satisfied: requests>=2.26 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from yfinance) (2.28.1)
Requirement already satisfied: soupsieve>1.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.3.1)
Requirement already satisfied: cffi>=1.12 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from cryptography>=3.3.2->yfinance) (1.15.1)
Requirement already satisfied: webencodings in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from html5lib>=1.1->yfinance) (0.5.1)
Requirement already satisfied: six>=1.9 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from html5lib>=1.1->yfinance) (1.16.0)
Requirement already satisfied: python-dateutil>=2.8.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pandas>=1.3.0->yfinance) (2.8.2)
Requirement already satisfied: idna<4,>=2.5 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests>=2.26->yfinance) (3.4)
Requirement already satisfied: certifi>=2017.4.17 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests>=2.26->yfinance) (2022.9.24)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests>=2.26->yfinance) (1.26.12)
Requirement already satisfied: charset-normalizer<3,>=2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests>=2.26->yfinance) (2.1.1)
Requirement already satisfied: pycparser in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from cffi>=1.12->cryptography>=3.3.2->yfinance) (2.21)
[notice] A new release of pip available: 22.3 -> 22.3.1
[notice] To update, run: pip install --upgrade pip
import yfinance as yf  
import numpy as np
import pandas as pd
# list of FTSE100 stocks available at 
# https://www.londonstockexchange.com/indices/ftse-100/constituents/table
# below are randomly selected stocks

stocks_dict = {
    'FLTR.L':'FLUTTER ENTERTAINMENT PLC',
    'SVT.L':'SEVERN TRENT PLC', 
    'LSEG.L':'LONDON STOCK EXCHANGE GROUP PLC', 
    'BNZL.L':'BUNZL PLC', 
    'CPG.L':'COMPASS GROUP PLC', 
    'NXT.L':'NEXT PLC', 
    'ULVR.L':'UNILEVER PLC', 
    'SHEL.L':'SHELL PLC', 
    'CCH.L':'COCA-COLA HBC A', 
    'BRBY.L':'BURBERRY GROUP PLC', 
    'SSE.L':'SSE PLC', 
    'REL.L':'RELX PLC', 
    'EXPN.L':'EXPERIAN PLC', 
    'DCC.L':'DCC PLC ORD', 
    'AZN.L':'ASTRAZENECA PLC', 
    'GSK.L':'GSK PLC ORD', 
    'ADM.L':'ADMIRAL GROUP PLC', 
    'SDR.L':'SCHRODERS PLC', 
    'BATS.L':'BRITISH AMERICAN TOBACCO PLC', 
    'IHG.L':'INTERCONTINENTAL HOTELS GROUP PLC', 
    }
    
stocks = list(stocks_dict.keys()) # list of candidate stocks

data = yf.download(stocks,'2020-07-01','2022-04-30')['Close'] # main data
data_test = yf.download(stocks,'2022-05-01','2022-07-24')['Close'] # testing data
[                       0%                       ]
[*****                 10%                       ]  2 of 20 completed
[*******               15%                       ]  3 of 20 completed
[*******               15%                       ]  3 of 20 completed
[*******               15%                       ]  3 of 20 completed
[*******               15%                       ]  3 of 20 completed
[*****************     35%                       ]  7 of 20 completed
[*****************     35%                       ]  7 of 20 completed
[*****************     35%                       ]  7 of 20 completed
[**********************50%                       ]  10 of 20 completed
[**********************50%                       ]  10 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************75%***********            ]  15 of 20 completed
[**********************75%***********            ]  15 of 20 completed
[**********************85%****************       ]  17 of 20 completed
[**********************90%******************     ]  18 of 20 completed
[**********************95%*********************  ]  19 of 20 completed
[*********************100%***********************]  20 of 20 completed

[                       0%                       ]
[*****                 10%                       ]  2 of 20 completed
[*****                 10%                       ]  2 of 20 completed
[**********            20%                       ]  4 of 20 completed
[**********            20%                       ]  4 of 20 completed
[**********            20%                       ]  4 of 20 completed
[**********            20%                       ]  4 of 20 completed
[*******************   40%                       ]  8 of 20 completed
[**********************45%                       ]  9 of 20 completed
[**********************45%                       ]  9 of 20 completed
[**********************45%                       ]  9 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************60%****                   ]  12 of 20 completed
[**********************85%****************       ]  17 of 20 completed
[**********************90%******************     ]  18 of 20 completed
[**********************95%*********************  ]  19 of 20 completed
[*********************100%***********************]  20 of 20 completed

data.describe()
ADM.L AZN.L BATS.L BNZL.L BRBY.L CCH.L CPG.L DCC.L EXPN.L FLTR.L GSK.L IHG.L LSEG.L NXT.L REL.L SDR.L SHEL.L SSE.L SVT.L ULVR.L
count 464.000000 464.000000 463.000000 464.000000 464.000000 464.000000 463.000000 463.000000 463.000000 464.000000 464.000000 463.000000 464.000000 464.000000 464.000000 464.000000 463.000000 464.000000 464.000000 464.000000
mean 2949.351293 8366.258621 2787.561555 2537.961207 1804.203375 2294.758621 1466.997840 6042.596112 2933.794816 12754.031149 1475.618385 4722.868251 7975.392241 7204.778017 1997.713362 568.022027 1489.860042 1503.168103 2601.586207 4142.657884
std 298.590229 753.326915 223.859266 228.969271 228.739954 314.297710 176.851449 420.743959 288.931273 2003.651264 128.447452 399.235132 794.814741 952.486937 248.250476 52.854523 299.833511 144.853407 241.375319 357.982299
min 2240.000000 6794.000000 2448.000000 2131.000000 1252.500000 1460.500000 1050.500000 5006.000000 2273.000000 7922.000000 1199.561523 3516.000000 6370.000000 4673.000000 1527.500000 442.510010 900.000000 1171.000000 2168.000000 3328.000000
25% 2756.750000 7931.500000 2643.750000 2363.000000 1630.000000 2073.000000 1393.250000 5785.000000 2738.000000 11400.000000 1385.821991 4549.000000 7371.500000 6282.000000 1788.625000 519.009995 1333.299988 1384.875000 2413.250000 3911.125000
50% 2962.000000 8413.000000 2734.000000 2479.500000 1820.250000 2372.500000 1497.500000 6030.000000 2893.000000 12890.000000 1446.565674 4812.000000 7939.000000 7660.000000 1918.250000 587.179993 1440.400024 1524.250000 2511.000000 4124.500000
75% 3116.250000 8678.250000 2825.750000 2712.750000 1965.250000 2532.250000 1586.250000 6268.000000 3109.000000 14282.500000 1579.234680 5026.000000 8596.000000 7980.500000 2227.250000 606.942520 1660.300049 1612.000000 2823.000000 4358.500000
max 3688.000000 10930.000000 3439.000000 3163.000000 2264.000000 2784.000000 1820.500000 7204.000000 3667.000000 16915.000000 1823.720459 5338.000000 9910.000000 8426.000000 2449.000000 658.070007 2228.000000 1868.500000 3211.000000 4892.000000
data_test.describe()
ADM.L AZN.L BATS.L BNZL.L BRBY.L CCH.L CPG.L DCC.L EXPN.L FLTR.L GSK.L IHG.L LSEG.L NXT.L REL.L SDR.L SHEL.L SSE.L SVT.L ULVR.L
count 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000 57.000000
mean 2185.973684 10508.228070 3466.447368 2814.035088 1629.964912 1777.026316 1710.345614 5494.614035 2555.052632 8403.048070 1758.225729 4650.421053 7403.684211 6209.087719 2244.596491 471.872281 2205.994736 1753.149123 2853.773158 3699.675439
std 173.503948 403.405858 81.319788 146.057225 60.288581 101.134432 237.407090 409.703802 152.041397 1243.157730 34.193845 256.656103 264.537545 239.741846 75.721260 16.497939 148.555222 89.980502 401.790333 129.866329
min 1729.000000 9750.000000 3300.000000 2575.000000 1482.000000 1548.000000 16.700001 4889.000000 2285.000000 81.739998 1684.302124 4193.000000 6738.000000 5764.000000 2081.000000 438.260010 1936.400024 1587.500000 28.070000 3451.500000
25% 2157.000000 10244.000000 3426.000000 2671.000000 1586.000000 1712.000000 1691.500000 5162.000000 2424.000000 8200.000000 1732.252319 4404.000000 7220.000000 5986.000000 2210.000000 458.320007 2044.000000 1677.500000 2794.000000 3609.500000
50% 2228.000000 10506.000000 3478.000000 2837.000000 1636.500000 1775.500000 1730.000000 5308.000000 2578.000000 8468.000000 1758.242188 4695.000000 7390.000000 6148.000000 2265.000000 469.200012 2225.000000 1761.000000 2872.000000 3690.500000
75% 2267.000000 10800.000000 3528.500000 2919.000000 1678.500000 1837.500000 1796.500000 5710.000000 2661.000000 8944.000000 1783.400024 4841.000000 7632.000000 6428.000000 2299.000000 486.200012 2338.000000 1821.000000 2996.000000 3802.500000
max 2561.000000 11232.000000 3628.000000 3108.000000 1743.000000 1945.500000 1854.000000 6268.000000 2835.000000 9760.000000 1815.863037 5150.000000 7894.000000 6686.000000 2375.000000 505.239990 2440.000000 1920.000000 3148.000000 3943.000000

2. Prepare data by cleansing and feature engineering

Check null values. We will drop a row with null in this example for simplicity but please cosnider replacing the values if possible.

print(data.isna().sum()) # null row for each stock
ADM.L     0
AZN.L     0
BATS.L    1
BNZL.L    0
BRBY.L    0
CCH.L     0
CPG.L     1
DCC.L     1
EXPN.L    1
FLTR.L    0
GSK.L     0
IHG.L     1
LSEG.L    0
NXT.L     0
REL.L     0
SDR.L     0
SHEL.L    1
SSE.L     0
SVT.L     0
ULVR.L    0
dtype: int64
data_test.isna().sum() # check for test data
ADM.L     0
AZN.L     0
BATS.L    0
BNZL.L    0
BRBY.L    0
CCH.L     0
CPG.L     0
DCC.L     0
EXPN.L    0
FLTR.L    0
GSK.L     0
IHG.L     0
LSEG.L    0
NXT.L     0
REL.L     0
SDR.L     0
SHEL.L    0
SSE.L     0
SVT.L     0
ULVR.L    0
dtype: int64
data.dropna(inplace=True) # drop the rows
data.shape
(459, 20)
data_test.shape
(57, 20)

Feature Engineering

To make prediction, we use Prophet. See more at https://facebook.github.io/prophet/

We make time series prediction based on the historical data between July 2020 and April 2022. The period to predict is May 2022 till late July 2022, which is the same period of the data in test set.

The predicted values are transformed to be the average return rate for each stock. We can use it for one of the objectives to maximise the sum of return rates.

!pip install prophet
/bin/bash: /home/obm/Prog/miniconda3/envs/qml/lib/libtinfo.so.6: no version information available (required by /bin/bash)
Requirement already satisfied: prophet in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (1.1.1)
Requirement already satisfied: pandas>=1.0.4 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (1.5.1)
Requirement already satisfied: python-dateutil>=2.8.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (2.8.2)
Requirement already satisfied: cmdstanpy>=1.0.4 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (1.0.8)
Requirement already satisfied: LunarCalendar>=0.0.9 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (0.0.9)
Requirement already satisfied: wheel>=0.37.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (0.37.1)
Requirement already satisfied: holidays>=0.14.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (0.17.2)
Requirement already satisfied: tqdm>=4.36.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (4.64.1)
Requirement already satisfied: numpy>=1.15.4 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (1.23.4)
Requirement already satisfied: matplotlib>=2.0.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (3.6.0)
Requirement already satisfied: setuptools-git>=1.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (1.2)
Requirement already satisfied: setuptools>=42 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (63.4.1)
Requirement already satisfied: convertdate>=2.1.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from prophet) (2.4.0)
Requirement already satisfied: pymeeus<=1,>=0.3.13 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from convertdate>=2.1.2->prophet) (0.5.12)
Requirement already satisfied: korean-lunar-calendar in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from holidays>=0.14.2->prophet) (0.3.1)
Requirement already satisfied: hijri-converter in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from holidays>=0.14.2->prophet) (2.2.4)
Requirement already satisfied: pytz in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from LunarCalendar>=0.0.9->prophet) (2022.7)
Requirement already satisfied: ephem>=3.7.5.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from LunarCalendar>=0.0.9->prophet) (4.1.4)
Requirement already satisfied: pillow>=6.2.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (9.2.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (1.4.4)
Requirement already satisfied: pyparsing>=2.2.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (3.0.9)
Requirement already satisfied: packaging>=20.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (21.3)
Requirement already satisfied: fonttools>=4.22.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (4.37.4)
Requirement already satisfied: cycler>=0.10 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (0.11.0)
Requirement already satisfied: contourpy>=1.0.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from matplotlib>=2.0.0->prophet) (1.0.5)
Requirement already satisfied: six>=1.5 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from python-dateutil>=2.8.0->prophet) (1.16.0)
[notice] A new release of pip available: 22.3 -> 22.3.1
[notice] To update, run: pip install --upgrade pip
from prophet import Prophet
ERROR:prophet.plot:Importing plotly failed. Interactive plots will not work.
pred_return_rate = []
pred_return_rate_mean = []
i = 0 # current index of stock
N = 20 # number of candidate stocks
periods = data_test.shape[0] # number of days to forecast, same as data_test length

for i in range(N): # for each stock
  model = Prophet()
  data['ds'] = data.index # index values (dates) as ds 
  data['y'] = data.iloc[:,i] # stock close price as y
  stock_data = data[['ds','y']] # training data
  model.fit(stock_data)
  future = model.make_future_dataframe(periods=periods) # predict 
  forecast = model.predict(future)['yhat'] # predicted values stored in yhat column
  return_rate = np.zeros(len(forecast)) # return rate to be stored in numpy array
  for j in range(len(forecast)-1): 
    return_rate[j+1] = (forecast[j+1] - forecast[j])/forecast[j] # calculate the return rate per day
  pred_return_rate.append(return_rate)
  pred_return_rate_mean.append(np.mean(return_rate)) # predicted mean return rate for the stock
  data.drop(columns=['ds','y'], inplace=True)
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/skex3r5n.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/8eglu_aj.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=23638', 'data', 'file=/tmp/tmpe07d9yq5/skex3r5n.json', 'init=/tmp/tmpe07d9yq5/8eglu_aj.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelg0zqix38/prophet_model-20221222121631.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:31 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:31 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/052ut3s8.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/swzcijw4.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=17686', 'data', 'file=/tmp/tmpe07d9yq5/052ut3s8.json', 'init=/tmp/tmpe07d9yq5/swzcijw4.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modely5g46xyq/prophet_model-20221222121632.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:32 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:32 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/es1uny16.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/63bplu6q.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=91517', 'data', 'file=/tmp/tmpe07d9yq5/es1uny16.json', 'init=/tmp/tmpe07d9yq5/63bplu6q.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelwnhpvk3_/prophet_model-20221222121632.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:32 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:32 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/cylm8696.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/kk03bnzs.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=81409', 'data', 'file=/tmp/tmpe07d9yq5/cylm8696.json', 'init=/tmp/tmpe07d9yq5/kk03bnzs.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelexnjnl_m/prophet_model-20221222121632.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:32 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:32 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/vd8s_xnk.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/n8j7f_dd.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=10998', 'data', 'file=/tmp/tmpe07d9yq5/vd8s_xnk.json', 'init=/tmp/tmpe07d9yq5/n8j7f_dd.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modeld0xvxd8y/prophet_model-20221222121633.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:33 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:33 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ry7mprqi.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ybvjqwrf.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=61378', 'data', 'file=/tmp/tmpe07d9yq5/ry7mprqi.json', 'init=/tmp/tmpe07d9yq5/ybvjqwrf.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelrg_q5vmx/prophet_model-20221222121633.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:33 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:33 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ig23xk22.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/qdw_gm1u.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=88980', 'data', 'file=/tmp/tmpe07d9yq5/ig23xk22.json', 'init=/tmp/tmpe07d9yq5/qdw_gm1u.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelnvhx7wuy/prophet_model-20221222121633.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:33 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:33 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ekpsf0b4.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ksxnhchk.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=60911', 'data', 'file=/tmp/tmpe07d9yq5/ekpsf0b4.json', 'init=/tmp/tmpe07d9yq5/ksxnhchk.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modely39vk739/prophet_model-20221222121634.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:34 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:34 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/0qmul04z.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/sdeq5y5b.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=55767', 'data', 'file=/tmp/tmpe07d9yq5/0qmul04z.json', 'init=/tmp/tmpe07d9yq5/sdeq5y5b.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modeldixoqs6p/prophet_model-20221222121634.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:34 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:34 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/bzppo40u.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/nii8lkpy.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=55520', 'data', 'file=/tmp/tmpe07d9yq5/bzppo40u.json', 'init=/tmp/tmpe07d9yq5/nii8lkpy.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelu5mxui7g/prophet_model-20221222121634.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:34 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:35 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ol16hhgg.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/pb5krrj0.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=79316', 'data', 'file=/tmp/tmpe07d9yq5/ol16hhgg.json', 'init=/tmp/tmpe07d9yq5/pb5krrj0.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelahv6j1e7/prophet_model-20221222121635.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:35 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:35 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/y16zipx0.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/f9n1f9ih.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=6867', 'data', 'file=/tmp/tmpe07d9yq5/y16zipx0.json', 'init=/tmp/tmpe07d9yq5/f9n1f9ih.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modela9i6besj/prophet_model-20221222121635.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:35 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:35 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/6fk9spa1.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/17u09qm5.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=53221', 'data', 'file=/tmp/tmpe07d9yq5/6fk9spa1.json', 'init=/tmp/tmpe07d9yq5/17u09qm5.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_model4kbc_lsk/prophet_model-20221222121636.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:36 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:36 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/60_ptvjz.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/rno5elri.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=34168', 'data', 'file=/tmp/tmpe07d9yq5/60_ptvjz.json', 'init=/tmp/tmpe07d9yq5/rno5elri.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelf5gv4e98/prophet_model-20221222121636.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:36 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:36 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/qpnqpham.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/ghjrsror.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=45374', 'data', 'file=/tmp/tmpe07d9yq5/qpnqpham.json', 'init=/tmp/tmpe07d9yq5/ghjrsror.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelktys17lh/prophet_model-20221222121636.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:36 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:36 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/brmnv4l1.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/68kjgs0t.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=51331', 'data', 'file=/tmp/tmpe07d9yq5/brmnv4l1.json', 'init=/tmp/tmpe07d9yq5/68kjgs0t.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelss6xg87a/prophet_model-20221222121637.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:37 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:37 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/xpzy2i_b.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/bo_t_qas.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=39935', 'data', 'file=/tmp/tmpe07d9yq5/xpzy2i_b.json', 'init=/tmp/tmpe07d9yq5/bo_t_qas.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelfn0km4gm/prophet_model-20221222121637.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:37 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:37 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/smatzxr6.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/lklye49q.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=63219', 'data', 'file=/tmp/tmpe07d9yq5/smatzxr6.json', 'init=/tmp/tmpe07d9yq5/lklye49q.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_model3vq_aokf/prophet_model-20221222121637.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:37 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:37 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/7ah9njlh.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/6zojv1br.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=2477', 'data', 'file=/tmp/tmpe07d9yq5/7ah9njlh.json', 'init=/tmp/tmpe07d9yq5/6zojv1br.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modelvwnz2hcl/prophet_model-20221222121638.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:38 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:38 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
INFO:prophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
INFO:prophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/to_oa922.json
DEBUG:cmdstanpy:input tempfile: /tmp/tmpe07d9yq5/2hnnidxp.json
DEBUG:cmdstanpy:idx 0
DEBUG:cmdstanpy:running CmdStan, num_threads: None
DEBUG:cmdstanpy:CmdStan args: ['/home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/prophet/stan_model/prophet_model.bin', 'random', 'seed=42296', 'data', 'file=/tmp/tmpe07d9yq5/to_oa922.json', 'init=/tmp/tmpe07d9yq5/2hnnidxp.json', 'output', 'file=/tmp/tmpe07d9yq5/prophet_modele4gx5s0x/prophet_model-20221222121638.csv', 'method=optimize', 'algorithm=lbfgs', 'iter=10000']
12:16:38 - cmdstanpy - INFO - Chain [1] start processing
INFO:cmdstanpy:Chain [1] start processing
12:16:38 - cmdstanpy - INFO - Chain [1] done processing
INFO:cmdstanpy:Chain [1] done processing
print(pred_return_rate_mean)
[-3.295120211693522e-05, 0.0004212352164733634, 0.0006443323333763341, 0.0006180876152479546, 0.0005054489754747284, -0.0011332381597290778, 0.0009594389542370872, -0.0004696525374264205, -0.0002568726047600979, -0.0010414517228963223, 0.00016392271944240828, 0.0006151834219843348, -9.028002715718702e-05, -6.64373367560175e-05, 0.0005966905208675229, -0.00019014641894963337, 0.0011606326569417041, 0.0005490521838582731, 0.0004985535894805963, -0.0005879920495425485]
print(np.mean(pred_return_rate_mean))
0.00014317780640250334

We also calculate actual return rates from the historical stock price data. We will use the results for the calculation of covariance that will be used for the second objective function to reduce the risk by diversify the portfolio items to avoid losing it altogether.

actual_return_rate = []
actual_return_rate_mean = []
N = 20 # number of candidate stocks
for i in range(N): # for each stock
  stock_data = data.iloc[:,i]
  return_rate = np.zeros(len(stock_data)) # return rate to be stored in numpy array
  for j in range(len(stock_data)-1): 
    return_rate[j+1] = (stock_data[j+1] - stock_data[j])/stock_data[j] # calculate the return rate per day
  actual_return_rate.append(return_rate)
  actual_return_rate_mean.append(np.mean(return_rate)) # predicted mean return rate for the stock
actual_return_rate_mean_mean = np.mean(actual_return_rate_mean)
actual_return_rate_mean_mean
0.00039797518633514584

Finally, we calculate the actual return rates on the test set for the final evaluation.

actual_return_rate_test = []
actual_return_rate_mean_test = []
N = 20 # number of candidate stocks
for i in range(N): # for each stock
  stock_data = data_test.iloc[:,i]
  return_rate = np.zeros(len(stock_data)) # return rate to be stored in numpy array
  for j in range(len(stock_data)-1): 
    return_rate[j+1] = (stock_data[j+1] - stock_data[j])/stock_data[j] # calculate the return rate per day
  actual_return_rate_test.append(return_rate)
  actual_return_rate_mean_test.append(np.mean(return_rate)) # predicted mean return rate for the stock
actual_return_rate_mean_mean_test = np.mean(actual_return_rate_mean_test)
actual_return_rate_mean_mean_test
0.26481830738480705

#3. Formulate the cost functions and constraints to obtain a QUBO matrix

Constraint Terms

In our portfolio optimisation example, let’s assume 1 constraint: the number of stock to be included in the portfolio.

In this example, we will use PyQubo, a python library that that can conver the cost function to a quadratic unconstraintbinary optimisation matrix that can be sent to D-wave qunatum annealer and JIJ simulated quantum annealer.

!pip install pyqubo # https://pyqubo.readthedocs.io/en/latest/index.html
/bin/bash: /home/obm/Prog/miniconda3/envs/qml/lib/libtinfo.so.6: no version information available (required by /bin/bash)
Requirement already satisfied: pyqubo in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (1.3.1)
Requirement already satisfied: dimod<0.13,>=0.9.14 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo) (0.12.3)
Requirement already satisfied: Deprecated>=1.2.12 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo) (1.2.13)
Requirement already satisfied: six>=1.15.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo) (1.16.0)
Requirement already satisfied: dwave-neal>=0.5.7 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo) (0.6.0)
Requirement already satisfied: numpy>=1.17.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo) (1.23.4)
Requirement already satisfied: wrapt<2,>=1.10 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Deprecated>=1.2.12->pyqubo) (1.14.1)
Requirement already satisfied: dwave-samplers<2.0.0,>=1.0.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-neal>=0.5.7->pyqubo) (1.0.0)
Requirement already satisfied: networkx>=2.4.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-samplers<2.0.0,>=1.0.0->dwave-neal>=0.5.7->pyqubo) (2.8.7)
[notice] A new release of pip available: 22.3 -> 22.3.1
[notice] To update, run: pip install --upgrade pip
from pyqubo import Array, Constraint, Placeholder
x = Array.create('x',shape=N, vartype='BINARY') # array takes binary 0 or 1 indicate to exclude or include a stock in the portfolio, which we need to optimise
# number of stocks constraint
num_stocks = 10 # specify the desired number of stocks
h_const1 = (num_stocks - np.dot(x,x))**2 # MSE from the budget. This lead to x values that makes the portfolio as close as the budget

Cost Functions

We have set two cost functions to optimise our portfolio. One is to maximise the sum of predicted growth rates that we predicted in the feature engineering section. Another is to minimise the covariance between each of the stock items to be selected in the portfolio. We then add up two cost functions for QUBO.

# cost function 1 to aim the highest return
h_cost1 = 0
h_cost1 -= np.dot(x,pred_return_rate_mean) # maximisation problem. negative to make it minimisation problem
# cost function 2 to balance the portfolio not to put all eggs in one basket
# minimising the sum of covariance leads to the safer choice
h_cost2 = 0
for i in range(N):
  for j in range(N):
    h_cost2 += x[i]*x[j]*np.sum((actual_return_rate[i]-actual_return_rate_mean[i])*(actual_return_rate[j]-actual_return_rate_mean[j]))/len(actual_return_rate[i])
h_cost = h_cost1 + h_cost2

Prepare QUBO

The problme formulation and representation in QUBO format are habled by PyQubo applications. In the lines below, we compile the model using the objective function that needs to be minimised, then define the constraint coefficient. to_qubo function generates a QUBO matrix in the dictionary type and an offset value which is an costanct value not required for the search for the minimum.

h = h_cost + Placeholder('l1')*Constraint(h_const1,label='num_stocks')
model = h.compile()
feed_dict = {'l1': 2}
qubo, offset = model.to_qubo(feed_dict=feed_dict)
offset
200.0

4. Sample optimisation results from a D-Wave solver

Install D-Wave’s Ocean SDK to request the computation to the quantum annealer. EmbeddingComposite is used to embed the QUBO to the physical quantum anneler in D-Wave.

!pip install dwave-ocean-sdk # https://docs.ocean.dwavesys.com/en/stable/
/bin/bash: /home/obm/Prog/miniconda3/envs/qml/lib/libtinfo.so.6: no version information available (required by /bin/bash)
Requirement already satisfied: dwave-ocean-sdk in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (6.1.1)
Requirement already satisfied: minorminer==0.2.9 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.2.9)
Requirement already satisfied: dwave-tabu==0.5.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.5.0)
Requirement already satisfied: dwave-samplers==1.0.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (1.0.0)
Requirement already satisfied: dwave-preprocessing==0.5.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.5.3)
Requirement already satisfied: dimod==0.12.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.12.3)
Requirement already satisfied: dwavebinarycsp==0.2.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.2.0)
Requirement already satisfied: dwave-inspector==0.3.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.3.0)
Requirement already satisfied: penaltymodel==1.0.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (1.0.2)
Requirement already satisfied: dwave-system==1.18.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (1.18.0)
Requirement already satisfied: pyqubo==1.3.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (1.3.1)
Requirement already satisfied: dwave-greedy==0.3.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.3.0)
Requirement already satisfied: dwave-hybrid==0.6.9 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.6.9)
Requirement already satisfied: dwave-neal==0.6.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.6.0)
Requirement already satisfied: dwave-cloud-client==0.10.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.10.3)
Requirement already satisfied: dwave-networkx==0.8.12 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-ocean-sdk) (0.8.12)
Requirement already satisfied: numpy<2.0.0,>=1.17.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dimod==0.12.3->dwave-ocean-sdk) (1.23.4)
Requirement already satisfied: homebase>=1.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (1.0.1)
Requirement already satisfied: pydantic>=1.7.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (1.10.2)
Requirement already satisfied: diskcache>=5.2.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (5.4.0)
Requirement already satisfied: requests[socks]>=2.18 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (2.28.1)
Requirement already satisfied: python-dateutil>=2.7 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (2.8.2)
Requirement already satisfied: plucky>=0.4.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (0.4.3)
Requirement already satisfied: click>=7.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-cloud-client==0.10.3->dwave-ocean-sdk) (8.1.3)
Requirement already satisfied: networkx in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-hybrid==0.6.9->dwave-ocean-sdk) (2.8.7)
Requirement already satisfied: importlib-resources>=3.2.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-inspector==0.3.0->dwave-ocean-sdk) (5.2.0)
Requirement already satisfied: Flask>=1.1.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-inspector==0.3.0->dwave-ocean-sdk) (2.2.2)
Requirement already satisfied: scipy>=1.7.3 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from dwave-system==1.18.0->dwave-ocean-sdk) (1.9.3)
Requirement already satisfied: fasteners in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from minorminer==0.2.9->dwave-ocean-sdk) (0.18)
Requirement already satisfied: rectangle-packer>=2.0.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from minorminer==0.2.9->dwave-ocean-sdk) (2.0.1)
Requirement already satisfied: Deprecated>=1.2.12 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo==1.3.1->dwave-ocean-sdk) (1.2.13)
Requirement already satisfied: six>=1.15.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pyqubo==1.3.1->dwave-ocean-sdk) (1.16.0)
Requirement already satisfied: wrapt<2,>=1.10 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Deprecated>=1.2.12->pyqubo==1.3.1->dwave-ocean-sdk) (1.14.1)
Requirement already satisfied: importlib-metadata>=3.6.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Flask>=1.1.1->dwave-inspector==0.3.0->dwave-ocean-sdk) (5.0.0)
Requirement already satisfied: Werkzeug>=2.2.2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Flask>=1.1.1->dwave-inspector==0.3.0->dwave-ocean-sdk) (2.2.2)
Requirement already satisfied: itsdangerous>=2.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Flask>=1.1.1->dwave-inspector==0.3.0->dwave-ocean-sdk) (2.1.2)
Requirement already satisfied: Jinja2>=3.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Flask>=1.1.1->dwave-inspector==0.3.0->dwave-ocean-sdk) (3.0.3)
Requirement already satisfied: zipp>=3.1.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from importlib-resources>=3.2.0->dwave-inspector==0.3.0->dwave-ocean-sdk) (3.9.0)
Requirement already satisfied: typing-extensions>=4.1.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from pydantic>=1.7.3->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (4.4.0)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests[socks]>=2.18->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (1.26.12)
Requirement already satisfied: charset-normalizer<3,>=2 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests[socks]>=2.18->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (2.1.1)
Requirement already satisfied: certifi>=2017.4.17 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests[socks]>=2.18->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (2022.9.24)
Requirement already satisfied: idna<4,>=2.5 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests[socks]>=2.18->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (3.4)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from requests[socks]>=2.18->dwave-cloud-client==0.10.3->dwave-ocean-sdk) (1.7.1)
Requirement already satisfied: MarkupSafe>=2.0 in /home/obm/Prog/miniconda3/envs/qml/lib/python3.8/site-packages (from Jinja2>=3.0->Flask>=1.1.1->dwave-inspector==0.3.0->dwave-ocean-sdk) (2.1.1)
[notice] A new release of pip available: 22.3 -> 22.3.1
[notice] To update, run: pip install --upgrade pip
from dwave.system import DWaveSampler, EmbeddingComposite
endpoint = 'https://cloud.dwavesys.com/sapi'
token = '***' # Please relace with your own token

Available annelers depend on the geographic location and machine’s online status. The list of solvers can be found on the D-Wave Leap dashboard.

sampler_dw = DWaveSampler(solver='Advantage_system4.1',token=token)
sampler_qa = EmbeddingComposite(sampler_dw)
---------------------------------------------------------------------------
SolverAuthenticationError                 Traceback (most recent call last)
Input In [43], in <cell line: 1>()
----> 1 sampler_dw = DWaveSampler(solver='Advantage_system4.1',token=token)
      2 sampler_qa = EmbeddingComposite(sampler_dw)

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/system/samplers/dwave_sampler.py:185, in DWaveSampler.__init__(self, failover, retry_interval, **config)
    182 self._solver_penalty = defaultdict(int)
    184 self.client = Client.from_config(**config)
--> 185 self.solver = self._get_solver(penalty=self._solver_penalty)

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/system/samplers/dwave_sampler.py:200, in DWaveSampler._get_solver(self, refresh, penalty)
    198 filters = copy.deepcopy(self.client.default_solver)
    199 order_by = filters.pop('order_by', 'avg_load')
--> 200 solvers = self.client.get_solvers(refresh=refresh, order_by=order_by, **filters)
    202 # we now just need to de-prioritize penalized solvers
    203 solvers.sort(key=lambda solver: penalty.get(solver.id, 0))

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/cloud/events.py:105, in dispatches_events.__call__.<locals>.wrapped(*pargs, **kwargs)
    103 dispatch_event(self.before_eventname, obj=obj, args=args)
    104 try:
--> 105     rval = fn(*pargs, **kwargs)
    106 except Exception as exc:
    107     dispatch_event(self.after_eventname, obj=obj, args=args, exception=exc)

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/cloud/client/base.py:1173, in Client.get_solvers(self, refresh, order_by, **filters)
   1170     query['name'] = filters['name__eq']
   1172 # filter
-> 1173 solvers = self._fetch_solvers(**query)
   1174 solvers = [s for s in solvers if all(p(s) for p in predicates)]
   1176 # sort: undefined (None) key values go last

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/cloud/utils.py:489, in cached.__call__.<locals>.wrapper(*args, **kwargs)
    487     val = data.get('val')
    488 else:
--> 489     val = fn(*args, **kwargs)
    490     self.cache[key] = dict(expires=now+self.maxage, val=val)
    492 return val

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/cloud/client/base.py:807, in Client._fetch_solvers(self, name)
    804     url = 'solvers/remote/'
    806 try:
--> 807     data = Client._sapi_request(self.session.get, url)
    808 except SAPIError as exc:
    809     if name is not None and exc.error_code == 404:

File ~/Prog/miniconda3/envs/qml/lib/python3.8/site-packages/dwave/cloud/client/base.py:1798, in Client._sapi_request(meth, *args, **kwargs)
   1796 else:
   1797     if response.status_code == 401:
-> 1798         raise SolverAuthenticationError(error_code=401)
   1800     try:
   1801         msg = response.json()

SolverAuthenticationError: Invalid token or access denied
sampleset_qa = sampler_qa.sample_qubo(qubo,num_reads=10)
records_qa = sampleset_qa.record
print(records_qa)

Above is the result of 10 samples from the solver.

First list items are the optimised combination of the stock items, 1 indicates the stock is included in the portfolio.

The second value is the energy state where the lowest number is the best solution. The number shows close to minus two hundred, because the offset value is not included.

Third number is the number of occurance of the solution. We have 10 reads and each read has unique stock selections.

The last number indicate “chain break” that the connection between qubits are broken then fixed by the software. Ideal soliution should have 0. chain break ratio.

Using records returned from the D-Wave solver, we can validate if the constraint terms are complied.

portfolio_candidates_qa = []
num_stock_counts_qa = []
record_len = len(records_qa)
for i in range(record_len):
  portfolio = []
  num_stock_count = 0
  for j in range(len(records_qa[i][0])):
    if records_qa[i][0][j] == 1:
      portfolio.append(stocks[j])
      num_stock_count += 1
  portfolio_candidates_qa.append(portfolio)
  num_stock_counts_qa.append(num_stock_count)
num_stock_counts_qa # number of stocks selected for the portfolio

In our example, 4 of the 10 solutions fit the requirement. We may adjust the coeffient value by increasing the penalty.

Finally, we can select one best solution and display the sotck codes. We will use 7th record where there was no chain break.

best_portfolio_qa = portfolio_candidates_qa[7]
print(best_portfolio_qa)

D-Wave has hybrid solver that uses classical computer and quantum annealer. The solver ‘hybrid_binary_quadratic_model_version2’ can have up to 1,000,000 variables.

To try the hybrid solver, we need to import LeapHybridSampler library. The difference from the quantum solver are:

  • it does not require embed composite

  • only one sample record is returned (no num_reads parameter)

  • it consumes more QPU time (different conversion rate applies QPU: Hybrid 1:20)

from dwave.system import LeapHybridSampler
sampler_dw_hybrid = LeapHybridSampler(solver='hybrid_binary_quadratic_model_version2',token=token)
sampleset_qa_hybrid = sampler_dw_hybrid.sample_qubo(qubo)
records_qa_hybrid = sampleset_qa_hybrid.record
print(records_qa_hybrid)

5. Sample optimisation results from a simulated quantum annealing solver

There is publicly available open source QA simulators provided by OpenJIJ. It can be used as an alternative the real quantum annerler. We can use the same QUBO prepared by PyQubo.

!pip install openjij
from openjij import SQASampler
sampler_jij = SQASampler()
sampleset_sqa = sampler_jij.sample_qubo(qubo,num_reads=10)
records_sqa = sampleset_sqa.record
print(records_sqa)
portfolio_candidates_sqa = []
num_stock_counts_sqa = []
record_len = len(records_sqa)
for i in range(record_len):
  portfolio = []
  num_stock_count = 0
  for j in range(len(records_sqa[i][0])):
    if records_sqa[i][0][j] == 1:
      portfolio.append(stocks[j])
      num_stock_count += 1
  portfolio_candidates_sqa.append(portfolio)
  num_stock_counts_sqa.append(num_stock_count)
num_stock_counts_sqa
best_portfolio_sqa = portfolio_candidates_sqa[-1]
print(best_portfolio_sqa)

#6. Evaluate the outcomes

We can select the best portfolio selection from QA and SQA samples.

portfolio_qa = records_qa[7][0] # best combination from QA hybdrid solver
print(portfolio_qa)
portfolio_sqa = records_sqa[-1][0] # best combination from SQA hybdrid solver
print(portfolio_sqa)

We compare the cost 1: increases the return rate. The more return is achieved if the number is smaller than the other.

h_cost1_qa = 0
h_cost1_qa -= np.dot(portfolio_qa,actual_return_rate_mean_test) # maximisation problem. negative to make it minimisation problem
print(h_cost1_qa) # return rate factor by QA
h_cost1_sqa = 0
h_cost1_sqa -= np.dot(portfolio_sqa,actual_return_rate_mean_test) # maximisation problem. negative to make it minimisation problem
print(h_cost1_sqa) # return rate cost by SQA

We also compare the cost 2: decrease the risk. The more return is achieved if the number is smaller than the other.

N= 20
h_cost2_qa = 0
for i in range(N):
  for j in range(N):
    h_cost2_qa += portfolio_qa[i]*portfolio_qa[j]*np.sum((actual_return_rate_test[i]-actual_return_rate_mean_test[i])*(actual_return_rate_test[j]-actual_return_rate_mean_test[j]))/len(actual_return_rate_test[i])
print(h_cost2_qa)
N= 20
h_cost2_sqa = 0
for i in range(N):
  for j in range(N):
    h_cost2_sqa += portfolio_sqa[i]*portfolio_sqa[j]*np.sum((actual_return_rate_test[i]-actual_return_rate_mean_test[i])*(actual_return_rate_test[j]-actual_return_rate_mean_test[j]))/len(actual_return_rate_test[i])
print(h_cost2_sqa)

We can check the costs in case we selected all 20 stocks as benchmark. The numbers will be divided by two for comparison.

portfolio_all = np.ones(20)
h_cost1_all = 0
h_cost1_all -= np.dot(portfolio_all,actual_return_rate_mean_test) # maximisation problem. negative to make it minimisation problem
print(h_cost1_all/2)
N= 20
h_cost2_all = 0
for i in range(N):
  for j in range(N):
    h_cost2_all += portfolio_all[i]*portfolio_all[j]*np.sum((actual_return_rate_test[i]-actual_return_rate_mean_test[i])*(actual_return_rate_test[j]-actual_return_rate_mean_test[j]))/len(actual_return_rate_test[i])
print(h_cost2_all/2)

Finally, the costs from Hybrid solver as follows:

portfolio_qa_hybrid = records_qa_hybrid[0][0] # best combination from QA hybdrid solver
print(portfolio_qa_hybrid)
h_cost1_qa_hybrid = 0
h_cost1_qa_hybrid -= np.dot(portfolio_qa_hybrid,actual_return_rate_mean_test) # maximisation problem. negative to make it minimisation problem
print(h_cost1_qa_hybrid) # return rate factor by QA Hybrid Solver
N= 20
h_cost2_qa_hybrid = 0
for i in range(N):
  for j in range(N):
    h_cost2_qa_hybrid += portfolio_qa_hybrid[i]*portfolio_qa_hybrid[j]*np.sum((actual_return_rate_test[i]-actual_return_rate_mean_test[i])*(actual_return_rate_test[j]-actual_return_rate_mean_test[j]))/len(actual_return_rate_test[i])
print(h_cost2_qa_hybrid)

With compariso to the benchmark, we can observe sample reocrds from QA/SQA/Hybdrid solvers as:

  • QA solver showed high-return but high-risks.

  • SQA solver recommended slightly lower return but minimum risk

  • Hybrid solver showed very similar costs for both return and risk as SQA

Above is just a comparison of 1 sample record. We could analyse more sample records and visualsing the outcome. We could also tune hyper-parameters, like constraint coefficient.