```python
import matplotlib.pyplot as plt
import sympy
import itertools
import numpy as np

import sys
IN_COLAB = 'google.colab' in sys.modules
if IN_COLAB:
  ! git clone https://github.com/OscarJHernandez/qc_portfolio_optimization
  from google.colab import drive
  drive.mount('/content/gdrive')
  %cd qc_portfolio_optimization
  ! pip install cirq
  ! pip install qiskit
  ! git pull

import cirq
from cirq.contrib.svg import SVGCircuit
import src.qc_utils as utils

%load_ext autoreload
%autoreload 2
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
# Define the number of stocks in portfolio to optimize
N_portfolio = 3

# Parameters of the cross-entropy optimizer for the beta, gamma angles
N_trials_ce = 100
N_ce_iterations = 10
N_ce_samples = 50
f_elite = 0.1

# Portfolio parameters
portfolio_parameters = {}

# Daily returns
portfolio_parameters['mu'] = np.array([4.01e-04,  6.10e-05,  9.16e-04, -6.19e-04])

# The covariance matrix
portfolio_parameters['sigma'] = np.array([[9.980e-05, 4.250e-05, 3.720e-05, 4.030e-05],
                                          [4.250e-05, 1.005e-04, 4.110e-05, 1.520e-05],
                                          [3.720e-05, 4.110e-05, 1.813e-04, 1.790e-05],
                                          [4.030e-05, 1.520e-05, 1.790e-05, 2.531e-04]])
# The transaction costs
portfolio_parameters['T'] = 0.0

# The scale of the lagrangian constraint
portfolio_parameters['A'] = 0.03

# The previous portfolio holding vector
portfolio_parameters['y'] = np.array([0.0,0.0,0.0,0.0])

# The risk aversion parameter (lam=0 maximally greedy, lam=1 maximally risk averse)
portfolio_parameters['lam'] = 0.9

# The number of assets to hold in the end (constraint)
portfolio_parameters['D'] = 2
```

# Determine the optimal solution via brute-force search

```python
# Instantiate the portfolio
portfolio = utils.Portfolio(N_portfolio=N_portfolio)

# Get the benchmark parameters
benchmark_parameters = portfolio.benchmark_values()
print(benchmark_parameters)

# Determine the results via a bruteforce search
best_bruteforce_result = portfolio.brute_force_search(portfolio_parameters)

# Provide the optimal energy along with the state
print('='*100)
print('Brute-Force state: ', best_bruteforce_result['minimum_cost_states'])
print('Brute-Force energy: ', best_bruteforce_result['minimum_cost'])
print('='*100)
```

    {'mu': array([4.01e-04, 6.10e-05, 9.16e-04]), 'sigma': array([[9.980e-05, 4.250e-05, 3.720e-05],
           [4.250e-05, 1.005e-04, 4.110e-05],
           [3.720e-05, 4.110e-05, 1.813e-04]]), 'D': 2.0, 'lam': 0.9, 'T': 0, 'A': 0.03, 'y': array([0., 0., 0., 0., 0., 0., 0., 0.])}
    ====================================================================================================
    Brute-Force state:  [[1 0 1]]
    Brute-Force energy:  0.00018825000000000008
    ====================================================================================================

# Construct the QAOA-soft circuit

```python
# Generate a QAOA circuit with p=1 depth
QAOA_soft_circuit = portfolio.QAOA_soft_circuit(p=1)

# plot the circuit
#SVGCircuit(QAOA_soft_circuit)
print(QAOA_soft_circuit)
```

                                                                                ┌────────────────────────────────────────────────────────────┐   ┌───────────────────────────────────────────────────────────────────────────────────────┐   ┌─────────────────────────────────────────────────────────────────────────────────────┐   ┌──────────────────────────────────────────────────────────────┐   ┌────────────────────────────────────────────────────────────┐                                                                                ┌────────────────────────────────────────────────────────────┐   ┌────────────────────────────────────────────────────────┐                                     ┌────────────────────────────────────────────────────────────┐   ┌────────────────────────────────────────────────────────────────────┐   ┌───────────────────────────────────────────────────────────────────┐                                                                                ┌──────────────────────────────────────────┐   ┌─────────────────────────────────────────────────────────────┐   ┌────────────────────────────────────────────────────────────┐   ┌────────────────────────────────────────────┐   ┌──────────────────────────────────────────┐                                                                                ┌──────────────────────────────────────────┐   ┌───────────────────────────────────────┐                            ┌──────────────────────────────────────────┐   ┌────────────────────────────────────────────┐   ┌──────────────────────────────────────────┐
    0: ───H───Rz(-gamma_0*mu_0*(1 - L))───ZZ───────────────────────────────ZZ───────────────────────────────ZZ─────────────────────────────────ZZ──────────────────────────────────────────────────────────────────────────────────────────────ZZ───────────────────────────────────────────────────────────ZZ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ────────────────────────────────────────────────────────────────ZZ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ───────────────────────────────────────────────────────────────ZZ─────────────────────────────────Rz(0.5*T*gamma_0*(-y_0**2 - y_0 + 1))───────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ──────────────────────────────────────Rz(2*A*D*gamma_0)───────────────────────ZZ─────────────────────────────────────ZZ──────────────────────────────────────ZZ──────────────────────────────────────ZZ───────────────────────────────────────────────────────────────────ZZ──────────────────────────────────────────ZZ─────────────────────────────────────────────────────────────────────────────────────ZZ──────────────────────────────────────────────ZZ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ─────────────────────────────────────────────ZZ────────────────────────Rx(2*beta_0)───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────M('m')───
                                          │                                │                                │                                  │                                                                                │                                                            │                                                                                │                                                                 │                                                                                │                                                                │                                                                                │                                                                               │                                      │                                       │                                       │                                                                    │                                           │                                                                                │                                               │                                                                                │                                              │                                                                                │
    1: ───H───Rz(-gamma_0*mu_0*(L - 1))───ZZ^(L*gamma_0*sigma_00/(4*pi))───ZZ^(L*gamma_0*sigma_00/(4*pi))───┼──────────────────────────────────┼─────────────────────────────ZZ─────────────────────────────────ZZ─────────────────────────────┼────────────────────────────────────────────────────────────┼─────────────────────────────ZZ──────────────────────────────────────────────────────────ZZ─────────────────────────────┼───────────────────────────────────ZZ────────────────────────────┼─────────────────────────────────ZZ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────ZZ────────────────────────────┼──────────────────────────────────ZZ───────────────────────────────────────────────────────────────────────Rz(0.5*T*gamma_0*(-y_0**2 + y_0 + 1))──────────────────────────────────ZZ^(0.5*T*gamma_0*(2*y_0**2 - 1)/pi)────Rz(-2*A*D*gamma_0)──────────────────────ZZ^(A*gamma_0/(4*pi))──────────────────ZZ^(A*gamma_0/(4*pi))───────────────────┼───────────────────────────────────────┼────────────────────ZZ────────────────────────ZZ────────────────────┼───────────────────────────────────────────┼────────────────────ZZ──────────────────────────────────────────ZZ────────────────────┼──────────────────────────ZZ───────────────────┼────────────────────────ZZ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────ZZ───────────────────┼─────────────────────────ZZ───────────────────────────────────────────────Rx(2*beta_0)──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────M────────
                                                                                │                                  │                             │                                  │                              │                                                            │                             │                                                           │                              │                                   │                             │                                 │                                                                                │                                  │                             │                                  │                                                                                │                                       │                    │                         │                     │                                           │                    │                                           │                     │                          │                    │                        │                                                                                │                         │                    │                         │                                                                                │
    2: ───H─────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(-L*gamma_0*sigma_10/(4*pi))────┼─────────────────────────────ZZ^(L*gamma_0*sigma_10/(4*pi))─────┼──────────────────────────────┼──────────────────────────────Rz(-gamma_0*mu_1*(1 - L))─────┼─────────────────────────────┼───────────────────────────────────────────────────────────┼──────────────────────────────ZZ^(-L*gamma_0*sigma_01/(4*pi))─────ZZ^(L*gamma_0*sigma_01/(4*pi))┼─────────────────────────────────┼─────────────────────────────────ZZ───────────────────────────────ZZ───────────────────────────────ZZ─────────────────────────────────ZZ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────┼─────────────────────────────┼──────────────────────────────────┼────────────────────────────────────ZZ───────────────────────────────────────────────────────────────────────ZZ────────────────────────────────Rz(0.5*T*gamma_0*(-y_1**2 - y_1 + 1))───────────────────────────────────────────ZZ─────────────────────────────────────────────────────────────────────────────ZZ^(-A*gamma_0/(4*pi))──────────────────┼────────────────────ZZ^(A*gamma_0/(4*pi))─────┼─────────────────────┼─────────────────────Rz(2*A*D*gamma_0)─────┼────────────────────┼───────────────────────────────────────────┼─────────────────────ZZ^(-A*gamma_0/(4*pi))─────ZZ^(A*gamma_0/(4*pi))┼────────────────────────┼────────────────────────ZZ──────────────────────ZZ──────────────────────ZZ────────────────────────ZZ────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────────┼────────────────────┼─────────────────────────┼─────────────────────ZZ──────────────────────────────────────────────ZZ───────────────────────Rx(2*beta_0)────────────────────────────────────────────────────────────────────────────M────────
                                                                                │                                                                │                              │                                                            │                             │                                                           │                                                                                │                                 │                                 │                                │                                │                                  │                                                                                │                                  │                             │                                  │                                    │                                                                        │                                                                                │                                                                                │                                              │                     │                                           │                    │                                           │                                                                     │                        │                        │                       │                       │                         │                                                                                │                         │                    │                         │                     │                                               │                                                                                │
    3: ───H────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(L*gamma_0*sigma_10/(4*pi))───────────────────────────────────ZZ^(-L*gamma_0*sigma_10/(4*pi))┼────────────────────────────────────────────────────────────┼─────────────────────────────┼─────────────────────────────Rz(-gamma_0*mu_1*(L - 1))─────┼────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(L*gamma_0*sigma_01/(4*pi))────ZZ^(-L*gamma_0*sigma_01/(4*pi))───ZZ^(L*gamma_0*sigma_11/(4*pi))───ZZ^(L*gamma_0*sigma_11/(4*pi))───┼──────────────────────────────────┼─────────────────────────────ZZ─────────────────────────────────ZZ──────────────────────────────────────────────────────────┼──────────────────────────────────┼─────────────────────────────┼──────────────────────────────────┼────────────────────────────────────┼───────────────────────────────────ZZ───────────────────────────────────┼─────────────────────────────────ZZ──────────────────────────────────────Rz(0.5*T*gamma_0*(-y_1**2 + y_1 + 1))───ZZ^(0.5*T*gamma_0*(2*y_1**2 - 1)/pi)───────────────────────────────────────────────────────────────────────────────────ZZ^(A*gamma_0/(4*pi))──────────────────────────ZZ^(-A*gamma_0/(4*pi))┼───────────────────────────────────────────┼────────────────────┼────────────────────Rz(-2*A*D*gamma_0)─────┼─────────────────────────────────────────────────────────────────────ZZ^(A*gamma_0/(4*pi))────ZZ^(-A*gamma_0/(4*pi))───ZZ^(A*gamma_0/(4*pi))───ZZ^(A*gamma_0/(4*pi))───┼─────────────────────────┼────────────────────ZZ────────────────────────ZZ─────────────────────────────────────────┼─────────────────────────┼────────────────────┼─────────────────────────┼─────────────────────┼──────────────────────────ZZ───────────────────┼────────────────────────ZZ───────────────────────Rx(2*beta_0)───────────────────────────────────────────────────M────────
                                                                                │                                                            │                             │                                                           │                                                                                │                                  │                             │                                  │                                                           │                                  │                             │                                  │                                    │                                   │                                    │                                 │                                                                                │                                           │                    │                                           │                                                                                │                         │                    │                         │                                          │                         │                    │                         │                     │                          │                    │                        │                                                                                │
    4: ───H────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(-L*gamma_0*sigma_20/(4*pi))──────────────────────────────┼─────────────────────────────ZZ^(L*gamma_0*sigma_20/(4*pi))──────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(-L*gamma_0*sigma_21/(4*pi))────┼─────────────────────────────ZZ^(L*gamma_0*sigma_21/(4*pi))─────┼──────────────────────────────Rz(-gamma_0*mu_2*(1 - L))────ZZ^(-L*gamma_0*sigma_02/(4*pi))────ZZ^(L*gamma_0*sigma_02/(4*pi))┼──────────────────────────────────┼────────────────────────────────────ZZ^(-L*gamma_0*sigma_12/(4*pi))─────ZZ^(L*gamma_0*sigma_12/(4*pi))───────┼─────────────────────────────────┼───────────────────────────────────────ZZ──────────────────────────────────────ZZ─────────────────────────────────────Rz(0.5*T*gamma_0*(-y_2**2 - y_2 + 1))───ZZ───────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(-A*gamma_0/(4*pi))──────────────────────┼────────────────────ZZ^(A*gamma_0/(4*pi))───────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(-A*gamma_0/(4*pi))────┼────────────────────ZZ^(A*gamma_0/(4*pi))─────┼─────────────────────Rz(2*A*D*gamma_0)────ZZ^(-A*gamma_0/(4*pi))────ZZ^(A*gamma_0/(4*pi))┼─────────────────────────┼─────────────────────ZZ^(-A*gamma_0/(4*pi))─────ZZ^(A*gamma_0/(4*pi))┼────────────────────────┼────────────────────────ZZ──────────────────────ZZ──────────────────────Rx(2*beta_0)───M────────
                                                                                │                                                                                │                                                                                │                                                                │                                                                                │                                  │                                                                                │                                 │                                       │                                       │                                                                              │                                                                                │                                                                │                                                                                │                                              │                                                                                │                         │                                                                     │                        │                        │                       │                                      │
    5: ───H─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(L*gamma_0*sigma_20/(4*pi))────────────────────────────────────────────────────────────ZZ^(-L*gamma_0*sigma_20/(4*pi))───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(L*gamma_0*sigma_21/(4*pi))───────────────────────────────────ZZ^(-L*gamma_0*sigma_21/(4*pi))─────────────────────────────Rz(-gamma_0*mu_2*(L - 1))────────────────────────────────────────ZZ^(L*gamma_0*sigma_02/(4*pi))─────ZZ^(-L*gamma_0*sigma_02/(4*pi))───────────────────────────────────────────────────────────────────────────────ZZ^(L*gamma_0*sigma_12/(4*pi))────ZZ^(-L*gamma_0*sigma_12/(4*pi))─────────ZZ^(L*gamma_0*sigma_22/(4*pi))──────────ZZ^(L*gamma_0*sigma_22/(4*pi))─────────Rz(0.5*T*gamma_0*(-y_2**2 + y_2 + 1))───ZZ^(0.5*T*gamma_0*(2*y_2**2 - 1)/pi)─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(A*gamma_0/(4*pi))────────────────────────────────────────────ZZ^(-A*gamma_0/(4*pi))────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────ZZ^(A*gamma_0/(4*pi))──────────────────────────ZZ^(-A*gamma_0/(4*pi))─────────────────────Rz(-2*A*D*gamma_0)─────────────────────────────ZZ^(A*gamma_0/(4*pi))─────ZZ^(-A*gamma_0/(4*pi))────────────────────────────────────────────────ZZ^(A*gamma_0/(4*pi))────ZZ^(-A*gamma_0/(4*pi))───ZZ^(A*gamma_0/(4*pi))───ZZ^(A*gamma_0/(4*pi))───Rx(2*beta_0)───M────────
                                                                                └────────────────────────────────────────────────────────────┘   └───────────────────────────────────────────────────────────────────────────────────────┘   └─────────────────────────────────────────────────────────────────────────────────────┘   └──────────────────────────────────────────────────────────────┘   └────────────────────────────────────────────────────────────┘                                                                                └────────────────────────────────────────────────────────────┘   └────────────────────────────────────────────────────────┘                                     └────────────────────────────────────────────────────────────┘   └────────────────────────────────────────────────────────────────────┘   └───────────────────────────────────────────────────────────────────┘                                                                                └──────────────────────────────────────────┘   └─────────────────────────────────────────────────────────────┘   └────────────────────────────────────────────────────────────┘   └────────────────────────────────────────────┘   └──────────────────────────────────────────┘                                                                                └──────────────────────────────────────────┘   └───────────────────────────────────────┘                            └──────────────────────────────────────────┘   └────────────────────────────────────────────┘   └──────────────────────────────────────────┘

```python
# The results for the QAOA_soft circuit
ce_QAOA_soft_results =portfolio.optimize_circuit_angles_cross_entropy(QAOA_soft_circuit,
                                                            parameters=portfolio_parameters,
                                                            p=1,
                                                            n_trials=N_trials_ce,
                                                            iterations=N_ce_iterations,
                                                            f_elite=f_elite,
                                                            Nce_samples = N_ce_samples)
```

    100%|██████████| 50/50 [00:08<00:00,  6.13it/s]
      2%|▏         | 1/50 [00:00<00:08,  6.11it/s]

    k=0, mean_E=0.108156, std_E=0.024033, gammas[pi]=[0.09731592], betas[pi]=[1.29209572]

    100%|██████████| 50/50 [00:08<00:00,  6.10it/s]
      2%|▏         | 1/50 [00:00<00:08,  6.12it/s]

    k=1, mean_E=0.064138, std_E=0.009880, gammas[pi]=[0.01678452], betas[pi]=[1.46114721]

    100%|██████████| 50/50 [00:08<00:00,  6.04it/s]
      2%|▏         | 1/50 [00:00<00:08,  6.03it/s]

    k=2, mean_E=0.065692, std_E=0.004397, gammas[pi]=[0.58334039], betas[pi]=[1.65899142]

    100%|██████████| 50/50 [00:08<00:00,  6.15it/s]
      2%|▏         | 1/50 [00:00<00:08,  5.76it/s]

    k=3, mean_E=0.050215, std_E=0.001444, gammas[pi]=[0.97275589], betas[pi]=[1.74886255]

    100%|██████████| 50/50 [00:08<00:00,  5.91it/s]
      2%|▏         | 1/50 [00:00<00:08,  6.08it/s]

    k=4, mean_E=0.046393, std_E=0.003081, gammas[pi]=[1.11891424], betas[pi]=[1.76226618]

    100%|██████████| 50/50 [00:08<00:00,  5.80it/s]
      2%|▏         | 1/50 [00:00<00:07,  6.25it/s]

    k=5, mean_E=0.038776, std_E=0.001838, gammas[pi]=[1.22962367], betas[pi]=[1.75369857]

    100%|██████████| 50/50 [00:08<00:00,  6.16it/s]
      2%|▏         | 1/50 [00:00<00:08,  6.06it/s]

    k=6, mean_E=0.032598, std_E=0.001536, gammas[pi]=[1.31320037], betas[pi]=[1.74467055]

    100%|██████████| 50/50 [00:08<00:00,  6.21it/s]
      0%|          | 0/50 [00:00<?, ?it/s]

    k=7, mean_E=0.029361, std_E=0.005100, gammas[pi]=[1.34612923], betas[pi]=[1.74527375]

    100%|██████████| 50/50 [00:08<00:00,  5.95it/s]
      2%|▏         | 1/50 [00:00<00:08,  5.97it/s]

    k=8, mean_E=0.030994, std_E=0.001784, gammas[pi]=[1.44599209], betas[pi]=[1.74645132]

    100%|██████████| 50/50 [00:08<00:00,  6.16it/s]

    k=9, mean_E=0.029603, std_E=0.000987, gammas[pi]=[1.45850832], betas[pi]=[1.73911378]

```python
print('='*100)
print('optimal_gammas: ', ce_QAOA_soft_results['optimal_gammas'])
print('optimals_betas: ', ce_QAOA_soft_results['optimal_betas'])
print('best solutions: ', ce_QAOA_soft_results['best_solutions'])
print('='*100)

bitstrings = portfolio.measure_circuit(QAOA_soft_circuit,parameters=benchmark_parameters,
                                       betas=ce_QAOA_soft_results['optimal_betas'],
                                       gammas=ce_QAOA_soft_results['optimal_gammas'],
                                       n_trials=N_trials_ce)
portfolio_holdings = portfolio.convert_bitstrings_to_portfolio_holdings(bitstrings)

plt.figure(figsize=(15,5))
plt.title("QAOA-soft holdings",size=20)
plt.bar(np.arange(len(portfolio_holdings['labels'])), portfolio_holdings['counts'], tick_label=portfolio_holdings['labels'])
plt.xticks(rotation=90)
plt.xlabel('Portfolio holding states',size=20)
plt.show()
```

# Construct the QAOA-hard circuit

```python
# Generate a QAOA with hard constrains circuit with p=1 depth
QAOA_hard_circuit = portfolio.QAOA_hard_circuit(D=portfolio_parameters['D'],p=1)
```

```python
# The results for the QAOA_hard circuit
ce_QAOA_hard_results =portfolio.optimize_circuit_angles_cross_entropy(QAOA_hard_circuit,
                                                            parameters=portfolio_parameters,
                                                            p=1, # The depth of the circuit
                                                            n_trials=N_trials_ce,
                                                            iterations=N_ce_iterations,
                                                            f_elite=f_elite,
                                                            Nce_samples = N_ce_samples)
```

    100%|██████████| 50/50 [00:05<00:00,  9.74it/s]
      2%|▏         | 1/50 [00:00<00:05,  8.41it/s]

    k=0, mean_E=0.000188, std_E=0.000000, gammas[pi]=[1.49994018], betas[pi]=[1.71512995]

    100%|██████████| 50/50 [00:05<00:00,  9.82it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.60it/s]

    k=1, mean_E=0.000189, std_E=0.000001, gammas[pi]=[1.43688909], betas[pi]=[1.54327342]

    100%|██████████| 50/50 [00:05<00:00,  9.16it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.41it/s]

    k=2, mean_E=0.000189, std_E=0.000001, gammas[pi]=[1.43992351], betas[pi]=[1.54894228]

    100%|██████████| 50/50 [00:05<00:00,  9.63it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.17it/s]

    k=3, mean_E=0.000190, std_E=0.000002, gammas[pi]=[1.25427446], betas[pi]=[1.52444957]

    100%|██████████| 50/50 [00:05<00:00,  9.87it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.63it/s]

    k=4, mean_E=0.000188, std_E=0.000000, gammas[pi]=[1.04687038], betas[pi]=[1.35669905]

    100%|██████████| 50/50 [00:05<00:00,  9.80it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.51it/s]

    k=5, mean_E=0.000188, std_E=0.000000, gammas[pi]=[1.49145303], betas[pi]=[1.64979905]

    100%|██████████| 50/50 [00:05<00:00,  9.33it/s]
      2%|▏         | 1/50 [00:00<00:05,  9.67it/s]

    k=6, mean_E=0.000188, std_E=0.000000, gammas[pi]=[1.72843918], betas[pi]=[1.85248116]

    100%|██████████| 50/50 [00:05<00:00,  9.49it/s]
      2%|▏         | 1/50 [00:00<00:04,  9.81it/s]

    k=7, mean_E=0.000189, std_E=0.000000, gammas[pi]=[1.88245895], betas[pi]=[2.05432078]

    100%|██████████| 50/50 [00:05<00:00,  9.36it/s]
      4%|▍         | 2/50 [00:00<00:04, 10.10it/s]

    k=8, mean_E=0.000189, std_E=0.000000, gammas[pi]=[1.88726506], betas[pi]=[2.06113093]

    100%|██████████| 50/50 [00:05<00:00,  9.85it/s]

    k=9, mean_E=0.000189, std_E=0.000000, gammas[pi]=[2.12001237], betas[pi]=[2.23055624]

```python
print('='*100)
print('optimal_gammas: ', ce_QAOA_hard_results['optimal_gammas'])
print('optimals_betas: ', ce_QAOA_hard_results['optimal_betas'])
print('best solutions: ', ce_QAOA_hard_results['best_solutions'])
print('='*100)

bitstrings = portfolio.measure_circuit(QAOA_hard_circuit,parameters=benchmark_parameters,
                                       betas=ce_QAOA_hard_results['optimal_betas'],
                                       gammas=ce_QAOA_hard_results['optimal_gammas'],
                                       n_trials=N_trials_ce)
portfolio_holdings = portfolio.convert_bitstrings_to_portfolio_holdings(bitstrings)

plt.figure(figsize=(15,5))
plt.title("QAOA-hard holdings",size=20)
plt.bar(np.arange(len(portfolio_holdings['labels'])), portfolio_holdings['counts'], tick_label=portfolio_holdings['labels'])
plt.xticks(rotation=90)
plt.xlabel('Portfolio holding states',size=20)
plt.show()
```

```python

```


# Physics 256
## The Logistic Map and the Route to Chaos

## Last Time
### [Notebook Link: 17_ChaosPendulum](./17_ChaosPendulum.ipynb)
- driven dissipative pendulum
- determinental chaos
- Lyapunov exponents
- phase space trajectories

## Today

- Logistic map
- a simple route to chaotic behavior

### We would like to answer the questions:
* What does it take for a system to be chaotic?
* Can we predict ahead of time if a particular system will display chaotic behavior?

## Setting up the Notebook

```python
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.style.use('../include/notebook.mplstyle');
%config InlineBackend.figure_format = 'svg'
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
```

## Chaos
Aperiodic time-asymptotic behavior in a deterministic system which exhibits sensitive dependence on initial conditions.

1. **Aperiodic time-asymptotic behavior:** the existence of phase-space trajectories which do not settle down to fixed points or periodic orbits. For practical reasons, we insist that these trajectories are not too rare. We also require the trajectories to be bounded: i.e., they should not go off to infinity.
2. **Deterministic:** the equations of motion of the system possess no random inputs. In other words, the irregular behavior of the system arises from non-linear dynamics and not from noisy driving forces.
3. **Sensitive dependence on initial conditions:** nearby trajectories in phase-space separate exponentially fast in time: i.e., the system has a positive Lyapunov exponent.

## The logistic map

$$ x_{n+1} = \mu x_n (1 - x_n) $$

* map $\to$ discrete equation of motion
* A very simple iterative map that has applications in: ecology, statistics, neural networks, medicine.
* Think of $x_n$ as the size of a population in generation $n$, while $\mu$ is the availability of resources.

A study of this simple system will give us a more complete understanding of the origin of period doubling.

 Programming challenge

Complete the function below which takes an array $x$ and paramter $\mu$ and returns the full solution of the Logistic Map.

```python
def logistic_map(x,μ):
    '''Return the iterative logistic map.'''

    # INSERT CODE HERE
    pass

    return x

def μ_lab(μ):
    return f'$\mu =  {μ:G}$'
```

```python
N = 100
μ = [1.5,3.3,3.8]
x = np.random.random(N)
```

```python
fig, axes = plt.subplots(3,1,sharex=True, sharey=True, squeeze=False, figsize=(7,8))
for i, ax in enumerate(axes.flat):

    # iterate the map
    x = logistic_map(x,μ[i])

    # plot the numerical results
    ax.plot(x,linestyle='-',marker='o',linewidth=1.0,markerfacecolor='None',
            color=colors[i], markeredgecolor=colors[i], markersize=3,
            label=μ_lab(μ[i]))
    ax.legend(loc='lower right', frameon=True)
    ax.set_ylabel('x')
    ax.set_ylim(0,1)

axes[-1,0].set_xlabel('n')
```

* For μ=1.5, we see simple convergent behavior.
* For μ=3.3, periodic behavior arises.
* For μ=3.8, non-periodic, chaotic behavior seems to occur.

Can understand the behavior for small $\mu$ by considering the continuous *Logistic function*:
\begin{equation}
f(x,\mu) = \mu x (1-x).
\end{equation}
Then, for $\mu\sim 1$ $\exists$ a **fixed point** value $x^\ast$ such that:
\begin{equation}
f(x^\ast,\mu) = x^\ast.
\end{equation}

### Can we determine $x^\ast$?

\begin{align}
x^\ast &= \mu x^\ast (1-x^\ast) \newline
x^\ast [ \mu x^\ast + 1 - \mu] &= 0 \newline
\Rightarrow x^\ast &= 0, 1-\frac{1}{\mu}.
\end{align}

We thus have two fixed points, corresponding to the two values of x*. Generically we can iteratively or graphically solve.  Let's test our non-zero solution:

### Iterate until we find the fixed point

 Group Programming challenge

Complete the code below which identifies the fixed point via brute force iteration.

```python
x = [np.random.random()]
cμ = 3

# the first iteration
x.append(cμ*x[0]*(1-x[0]))

# continue until we find a fixed point
# INSERT CODE HERE
pass

plt.plot(x, color=colors[0],label=f'x* = {x[-1]:G}', linewidth=1, alpha=0.5)
plt.axhline(y=(1.0-1.0/cμ), color=colors[1], linestyle='--', label=f'1-1/μ = {1-1/cμ:G}')
plt.ylim(0,1)
plt.xlabel('n')
plt.ylabel('x')
plt.legend(frameon=True)
```

### A graphical solution
Plot $x_n$ vs $x_{n+1}$

```python
μ = [1.5,3.3]
x = np.arange(0,1.01,0.01)
fig, axes = plt.subplots(1,2,sharex=True, sharey=True, squeeze=False, figsize=(10,5))
fig.subplots_adjust(wspace=0.3)
for i, ax in enumerate(axes.flat):

    # plot x
    ax.plot(x,x,color='k')

    # plot f(x,μ)
    ax.plot(x,μ[i]*x*(1-x),color=colors[i], label=μ_lab(μ[i]))

    # plot the fixed point solutions
    ax.plot(0,0,'o',markersize=15, markerfacecolor=colors[-3], markeredgecolor='w')
    ax.plot(1-1/μ[i],1-1/μ[i],'o',markersize=10, markerfacecolor=colors[-3], markeredgecolor='w')

    ax.legend(loc='upper left', frameon=True)
    ax.set_xlabel(r'$x_n$')

    ax.set_ylim(0,1)
    ax.set_xlim(0,1)
axes[0,0].set_ylabel(r'$x_{n+1}$')
```

In order to understand the differences between these two maps, let us investigate the logistic function near the fixed points:

\begin{equation}
f(x^\ast + \delta x,\mu) \approx f(x^\ast,\mu) + \frac{d}{d x}f(x^\ast,\mu) \delta x.
\end{equation}

From this expansion, we can see that the **stability** of a fixed point is related to the magnitude of the first derivative of the $f$, i.e.

\begin{align}
|f'(x^\ast,\mu)| < 1 &\Rightarrow \mathrm{ stable} \newline
|f'(x^\ast,\mu)| > 1 &\Rightarrow \mathrm{ unstable} .
\end{align}

We can check this explicitely:

\begin{align}
\frac{d}{dx} f(x,\mu) &= \frac{d}{d x} [ \mu x (1-x) ] \newline
&= \mu(1-x) - \mu x \newline
&= \mu(1-2x) .
\end{align}

Evaluating at the fixed points:

### 1. $x^\ast = 0$
\begin{equation}
f'(x^\ast = 0) = \mu
\end{equation}
which is stable for $\mu < 1$.

### 2. $x^\ast = 1-1/\mu$
\begin{align}
f'(x^\ast = 1-1/\mu) &= \mu(1-2 + 2/\mu) \newline
&= 2 -\mu
\end{align}
which is stable for:
\begin{align}
|2-\mu| &< 1 \newline
-1 &> 2-\mu < 1 \newline
1 &< \mu < 3 .
\end{align}

```python
x = np.random.random(100*N)
cμ = 0.5

# iterate the map
x = logistic_map(x,cμ)

# plot the numerical results
plt.plot(x,linestyle='-',marker='o',linewidth=1.0,markerfacecolor='None',
        color=colors[0], markeredgecolor=colors[0], markersize=4,
        label=μ_lab(cμ))
plt.legend(loc='upper right', frameon=True)
plt.xlabel('n')
plt.ylabel('x')
plt.ylim(0,1)
```

## Phase Space Plots
We can observe these instabilities directly on the map equivalent of a phase space

```python
def logistic(mu,x):
    '''Return the logistic functon'''
    return mu*x*(1.0-x)

μ = [1.5,3.3]
x = np.arange(0,1,0.01)
xp = np.zeros([N,2])
xp[0,0] = np.random.random()
xp[0,1] = 0.0

fig, axes = plt.subplots(1,2,sharex=True, sharey=True, squeeze=False, figsize=(10,5))
fig.subplots_adjust(wspace=0.3)
for i, ax in enumerate(axes.flat):
    # plot x
    ax.plot(x,x,color='k')

    # plot f(x,μ)
    ax.plot(x,μ[i]*x*(1-x),color=colors[i], label=μ_lab(μ[i]))

    # compute the trajectories
    for n in range(1,N-1,2):
        xp[n,0] = xp[n-1,0]
        xp[n,1] = logistic(μ[i],xp[n-1,0])
        xp[n+1,0] = xp[n,1]
        xp[n+1,1] = xp[n,1]

    ax.plot(xp[:,0],xp[:,1],'--',color=colors[-3],linewidth=1)

    # legend and labels
    ax.legend(loc='upper right', frameon=True)
    ax.set_xlabel(r'$x_n$')

    ax.set_ylim(0,1)
    ax.set_xlim(0,1)

axes[0,0].set_ylabel(r'$x_{n+1}$')
```

The unstable fixed point shows evidence of oscillations.  Can understand this and its connection to period doubling by looking at the second iteratue of $f(x,\mu)$, i.e. how does $x_{n+2}$ depend on $x_n$

\begin{equation}
f^{(2)}(x,\mu) = f(f(x,\mu),\mu)
\end{equation}

or in terms of the Logistic map:

\begin{align}
x_{n+2} &= \mu x_{n+1}(1-x_{n+1}) \newline
&= \mu[\mu x_n(1-x_n)][1-\mu x_n(1-x_n)]
\end{align}

A fixed point solution of this equation exists, but is quite complicated.

 Group Programming challenge

Find a graphical solution to the second iterate of the Logistic map and plot for $\mu=1.5,3.3$.

```python
μ = [1.5,3.3]
x = np.arange(0,1.01,0.01)
fig, axes = plt.subplots(1,2,sharex=True, sharey=True, squeeze=False, figsize=(10,5))
fig.subplots_adjust(wspace=0.3)
xp = np.zeros([N,2])
xp[0,0] = np.random.random()
xp[0,1] = 0.0
for i, ax in enumerate(axes.flat):

    # plot x
    ax.plot(x,x,color='k')

    # INSERT CODE HERE
    pass

    # compute the trajectories
    for n in range(1,N-1,2):
        xp[n,0] = xp[n-1,0]
        xp[n,1] = logistic(μ[i],xp[n-1,0])
        xp[n+1,0] = xp[n,1]
        xp[n+1,1] = xp[n,1]

    ax.plot(xp[:,0],xp[:,1],'--',color=colors[-3],linewidth=1)
    ax.legend(loc='upper left', frameon=True)

axes[0,0].set_ylabel(r'$x_{n+2}$')
[axes[0,i].set_xlabel(r'$x_n$') for i in range(2)]
axes[0,0].set_ylim(0,1)
axes[0,0].set_xlim(0,1)
```

## Summary

* We now get a feeling for why period doubling happens, the number of fixed points is increasing with μ!
* Also, we might expect many dynamical systems to have smooth Taylor expansions near their fixed points that have the same fundamental properties as the logistic map.  This is the origin of universality and the Feigenbaum-δ for deterministic chaos via period doubling.

```python

```


```python
import sympy as sy
import fractions as fr
import matplotlib.pyplot as plt
import scipy as sp
import scipy.special
from texttable import Texttable
import scipy.stats
import scipy.special
import numpy as np
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
plt.style.use('fivethirtyeight')
np.set_printoptions(precision=4)
np.set_printoptions(suppress=True) # suppress scientific notation
```

To proceed with inferential statistics, there is no way to circumvent **probability theory** and it's undoubtedly the most important mathematical subject should you ever know in your life. Once probability view deeply implanted in your mind, you would have a revolutionary world view.

Here we will review the probability theory in a textbook style, i.e. start from defining sets.

#  Basic Concepts Refresh

##  Sets

A set is a collection of _distinct elements_, for instance, $(1, 2, 3)$ is a set, but $(2, 2, 3)$ isn't. Here we will demonstrate a bit of **SymPy** library, only scratching the surface. SymPy is an extremely powerful library, there are extensive examples in the **Linear Algebra With Python** training sessions.

We'll start from defining sets then basic operations. ```sy.``` is alias of ```sympy```.

```python
s = sy.FiniteSet(2, 3, 4, 5, 6); s
```

$\displaystyle \left\{2, 3, 4, 5, 6\right\}$

```python
s1 = sy.FiniteSet(2, 1/2, fr.Fraction(1,2), 8); s1 #Fraction subject can keep the fraction format
```

$\displaystyle \left\{\frac{1}{2}, 0.5, 2, 8\right\}$

Check if $2$ is in the set $S_1$

```python
x = 2
if x in s1:
    print('{} is in the set!'.format(x))
```

    2 is in the set!

Convert a list into a set, the list needs unpacking with $*$.

```python
s3_list = [1, 3, 5, 7, 8]
s3 = sy.FiniteSet(*s3_list); s3
```

$\displaystyle \left\{1, 3, 5, 7, 8\right\}$

##  Subsets, Supersets and Power Sets

Now define two sets, $a$ is a **superset** of $b$ ($a\supseteq b$), then $b$ is called a **subset** of $a$ ($b\subseteq a$).

```python
a = sy.FiniteSet(1,2,3,4,5)
b = sy.FiniteSet(2,3,4)
```

In this example, $a$ is a **proper superset** of $b$ denoted as $a\supset b$, $b$ is a **proper subset** of $a$ denoted as $b\subset a$.

The methods ```is_subset``` and ```is_superset``` are doing exactly as their names say.

```python
b.is_subset(a)
```

    True

```python
a.is_superset(b)
```

    True

The **powerset** is set contains all subsets, including the empty set and itself.

```python
b.powerset()
```

$\displaystyle \left\{\emptyset, \left\{2\right\}, \left\{3\right\}, \left\{4\right\}, \left\{2, 3\right\}, \left\{2, 4\right\}, \left\{3, 4\right\}, \left\{2, 3, 4\right\}\right\}$

Any sets are their own subset, but not their proper subset.

```python
b.is_subset(b)
```

    True

```python
b.is_proper_subset(b)
```

    False

##  Set Operations

Define two sets again.

```python
a = sy.FiniteSet(1,2,3)
b = sy.FiniteSet(2,3,4)
display(a)
display(b)
```

$\displaystyle \left\{1, 2, 3\right\}$

$\displaystyle \left\{2, 3, 4\right\}$

**Union** and **intersection** are sympy object methods, the same as we've learned in high school.

```python
display(a.union(b))
display(a.intersect(b))
```

$\displaystyle \left\{1, 2, 3, 4\right\}$

$\displaystyle \left\{2, 3\right\}$

##  Python Built-in Sets And Operations

Sets operations are fairly common in general programming, therefore Python has built-in operations for sets. Here are examples using built-in function.

We define sets in Python with ```{}```, which are the same for the dictionaries.

```python
a = {1, 3, 5, 7, 9}
b = {1, 7, 10}
```

```python
type(a)
```

    set

Union operation.

```python
a | b
```

    {1, 3, 5, 7, 9, 10}

or

```python
a.union(b)
```

    {1, 3, 5, 7, 9, 10}

Intersection.

```python
a & b
```

    {1, 7}

Or

```python
a.intersection(b)
```

    {1, 7}

Set difference.

```python
a - b
```

    {3, 5, 9}

```python
a.difference(b)
```

    {3, 5, 9}

##  Cartesian Product

The famous **Cartesian product** is defined mathematically as below.

Two sets multiply each other, the result presents all the possible ordered paired, the first element from $A$, the second element from $B$.

$$
A\times B=\{(a,b)\mid a\in A\ {\mbox{ and }}\ b\in B\}
$$

A visual example would be helpful. Define two sets, then compute the Cartesian product by multiplication.

```python
x = sy.FiniteSet(*list(range(1, 6)))
y = sy.FiniteSet(*list(range(2, 7)))
z = x*y #Cartesian producet
z
```

$\displaystyle \left\{1, 2, 3, 4, 5\right\} \times \left\{2, 3, 4, 5, 6\right\}$

```python
fig, ax = plt.subplots(figsize = (8, 8))

for i in z:
    ax.scatter(i[0], i[1], s = 100)

ax.grid(True)
ax.set_title('Cartesian Product', size = 15)
plt.show()
```

Here's a more concrete example of Cartesian product, suppose H&M has a type of dress with different parameters, a Cartesian product will show them all combinations they could have. For instance, a dress with parameters $('XL', 'blue', 'polyester')$.

```python
# size = sy.FiniteSet('S', 'M', 'L', 'XL')
colours = sy.FiniteSet('red', 'black', 'blue', 'white')
material = sy.FiniteSet('wool', 'cotton', 'polyester')
colours * material
```

$\displaystyle \left\{black, blue, red, white\right\} \times \left\{cotton, polyester, wool\right\}$

##  Probability and Event

After demonstrating so many sets theory, but how are they used in probability theory?

Dice rolling problem never gets old. To answer the question: _what is the probability of rolling an odd number_? We will show how to answer the question with set operations.

```python
s = sy.FiniteSet(1,2,3,4,5,6)
odd = sy.FiniteSet(1,3,5)
p = len(odd)/len(s)
p
```

This is vastly intuitive, just the proportion of odd sides over all sides.

##  Two Dice Problem

Two dice problem is slightly more complicated. Now we are asking: _what's the probability of getting a $7$ while rolling them together_.

Create a dictionary to hold the Cartesian product of two dice and its sum. The ```.update``` function is for adding elements in the dictionary if the updated key didn't exist.

```python
dice_cartesian = {}
for i in range(1,7):
    for j in range(1,7):
        dice_cartesian.update({(i, j):i + j})
dice_cartesian
```

Python dictionary has a method ```.items()``` that lists all key-value pairs in tuples. We'll make use of this method in the loop below.

```python
dice_cartesian.items()
```

Use ```defaultdict``` from ```collections``` module, it creates a dictionary which doesn't report errors and suitable for counting. We pass ```list``` as the _default factory_, meaning initialising values as lists whenever the key is given.

```python
from collections import defaultdict
dice_count = defaultdict(list)
for i,j in dice_cartesian.items():
    dice_count[j].append(i)
dice_count
```

Create another dictionary holding all sums of dice and corresponding probabilities.

```python
Prob = {i:round(len(j)/6**2, 4) for i,j in dice_count.items()}; Prob
```

The example above actually is more about Python techniques, it also can be conveniently solved without making a fuss, as below:

```python
def dice_prob(number):
    dice1, dice2 = list(range(1, 7)), list(range(1, 7))
    cartesian_dice = [(i, j) for i in dice1 for j in dice2] # list comprehension to create Cartesian product
    ocurrence = 0
    for element in cartesian_dice:
        if np.sum(list(element)) == number:
            ocurrence += 1
    print('The probability of {} while rolling two dice is {:.2f}%'.format(number, ocurrence/6**2*100))
```

```python
dice_prob(7)
```

##  Combination And Permutation

Again, high school skills, two question to help differentiate them.

Pick 3 letters from English alphabet, how many ways to choose? Use combination.

Pick 3 letters from English alphabet to construct a word (doesn't have to be meaningful), how many ways to choose? Use permutation.<\li>

Just remember: whenever needs certain order, use permutation, like the second question above, 'dog' and 'god' are different words though letters are the same. We don't need to memorise formula, but still FYI

$$
_nC_N = \frac{N!}{n!(N-n)!}\\
_nP_N = \frac{N!}{(N-n)!}
$$

```python
scipy.special.comb(26, 3) # combination
```

```python
scipy.special.perm(26, 3) # permutation
```

#  Conditional Probability, Multiplication Law and Independence

All above are just warm-up, from here on comes the real deal of probability theory.

The probability of A given B, so called **conditional probability**, defined as:

\begin{equation}
\frac{P(A\cap B)}{P(B)}=P(A|B)
\end{equation}

It is best to be demonstrated by a joint probability table, which is essentially a discrete numeric form of 3D distribution.

Here I want to introduce a module of draw text-style table, particular useful when you working on shells. Install in jupyter with ```conda install -c conda-forge texttable```.

```python
table = Texttable()
table.set_cols_align(["l", "c", "c", 'c'])
table.set_cols_valign(["m", "m", "m", 'm'])
table.add_rows([["", "Have Ht. Disease", " Not Have ", ' Total '],
                ["Male", 0.45, 0.06, 0.51],
                ["Femal", 0.36, 0.13, 0.49],
                ["Total", 0.81, 0.19, 1]])
print(table.draw())
```

**Marginal probability** is the key concepts, which got its name because located on the margin of a table, it is the sum of all probabilities from the same column or row.

The marginal probability of having heart disease is $81\%$ and marginal probability of not having is $19\%$.

Here is the question: _what is the probability of not having heart disease given the person is a woman_? Because the condition is that _the person must be a woman_. So the first step is to narrow down the table.

```python
table = Texttable()
table.set_cols_align(["l", "c", "c", 'c'])
table.set_cols_valign(["m", "m", "m", 'm'])
table.add_rows([["", "Have Ht. Disease", " Not Have ", ' Total '],
                ["Femal", 0.36, 0.13, 0.49]])
print(table.draw())
```

So the conditional probability of a woman not having heart disease is
$$
\frac{P(A\cap B)}{P(B)} = \frac{.13}{.49}
$$

```python
P_female = 0.49 # the marginal prob of being a women, this is P(B)
P_female_no_disease = 0.13
P_con = P_female_no_disease/P_female
print('The probability of not having heart disease given the person is a woman is {0:.2f}%'.format(P_con*100))
```

From conditional probability it is straightforward to deduct **multiplication law**, simply rearrange the formula
$$
P(A|B)P(B)=P(A\cap B)\\
P(B|A)P(A)=P(B\cap A)
$$

Two events $A$ and $B$ are independent if
$$
P(A|B)=P(A)\\
P(B|A)=P(B)
$$

Otherwise, the events are dependent.

Then the **independent multiplication law** indicates
$$
P(A)P(B)=P(A\cap B)\\
P(B)P(A)=P(B\cap A)
$$

We will not dive deep into probability problems for now, there will be a full-round training session on **Probability Theory**.

#  Distributions That You Should Know

These are most important probability distributions that you should know by heart.

##  Binomial Distribution

The first one is binomial distribution, we will give a it more extensive coverage, other distributions have similar functions in Python.

A binomial experiment has 4 features:
* A sequence of $n$ identical trials, e.g. throwing darts
* Only two outcomes are possible: _success_ or _failure_, e.g. hitting bullseye or not
* The probability of success $p$ does not change from trial to trial
* Trials are independent events, e.g. first throw doesn't affect the second throw

The **probability mass function(PMF)** of binomial distribution is

$$
f(k,n,p)=_nC_k p^kq^{n-k}
$$

    parameters
    meaning

    $n$
    number of trials

    $k$
    number of specific outcome

    $p$
    probability of success

    $q$
    probability of failure

Here's a simple example.

A personal banker might meet 50 people enquiring for loan monthly, empirically 30% of them has bad credit history. So calculate probability from 1 to 50 people has bad credit history, meaning calculate 1 person out of 50 has bad credit, 2 persons out of 50 have bad credit, so on so forth till 50 persons (all of them).

Start from a single number could be more intuitive, what's probability that a personal banker to encounter exact $14$ persons of bad credit history in a month?

```python
n = 50
k = 14 # what is the prob that exact 14 ppl she met had bad credit history?
b = scipy.special.comb(50, 14)
p = .3
f_binomial = b*p**k*(1-p)**(n-k)
print('The prob of meeting {0} persons who has bad credit history is {1:.2f}%.'.format(k, f_binomial * 100))
```

We can use ```scipy.stats.binom.pmf``` to get an array of PMF.

```python
n = 50
p = .3
bad_credit_person = np.arange(1, 51)
prob = sp.stats.binom.pmf(bad_credit_person, n, p)
fig, ax = plt.subplots(figsize = (12, 4))
ax.plot(bad_credit_person, prob, lw = 3, color = 'r', alpha = .5)
ax.set_ylim([0, .13])
ax.set_title('The probability that from 1 to 50 persons who have bad credit history', size = 16, x = .5, y = 1.02)
ax.set_xlabel('Number of Person Who Has Bad Credit History', size = 12)
ax.set_ylabel('Probability', size = 12)
plt.show()
```

We could interpret the plot with straightforward observation:

Most likely the personal banker would encounter $10$ to $20$ persons who have bad credit history.

Encountering less than $5$ or more than $25$ person are less likely.

The highest possibility is to encounter $50\times .03 = 15$ persons with bad credit.

Next example we can formulate a question by using **cumulative probability distribution**, the SciPy function is ```scipy.stats.binom.cdf```.

If a stock trader trades $n$ times a month, he has a $p%$ chance of winning the trade, find out the probability that he can win less than $k$ trades a month.

```python
n = 20
p = .55
k = 12
k1, k2 = 14, 4

win_less = sp.stats.binom.cdf(k, n, p)
win_more = 1- sp.stats.binom.cdf(k, n, p)
win_betw = sp.stats.binom.cdf(14, n, p) - sp.stats.binom.cdf(4, n, p)

print("If a trader's winning rate is {:.0f}%, the probability of winning less than {} times is {:.1f}% if he trades {} per month.".format(p*100, k, win_less*100, n))
print("If a trader's winning rate is {:.0f}%, the probability of winning more than {} times is {:.1f}% if he trades {} per month.".format(p*100, k, win_more*100, n))
print("If a trader's winning rate is {:.0f}%, the probability of winning between {} and {} times is {:.1f}% if he trades {} per month.".format(p*100, k1, k2, win_betw*100, n))
```

Or present in the text table.

```python
table = Texttable()
table.set_cols_align([ "c", "c", 'c'])
table.set_cols_valign([ "m", "m", 'm'])
table.add_rows([["Win Less", " Win More ", ' Win btw 4~14 '],
                [ win_less, win_more, win_betw]])
print(table.draw())
```

What if the probability of wining changing from 0.1 to 0.8, what is the probability that he wins less than 6 trades, assuming every month he trades 20 times.

```python
win_rate = np.arange(.1, .81, .05)
win_less = sp.stats.binom.cdf(6, 20, win_rate)

data_dict = {'win_rate':win_rate, 'win_less':win_less} # I am using annotation in matplotlib, so Pandas is used for easy tracking data points
df = pd.DataFrame(data_dict)
df
```

According to the table above, if the trader has at least $60\%$ winning rate, merely $0.6\%$ probability that he wins less then $6$ trades. Marginally higher winning rates are even important for traders in the short run.

```python
fig, ax = plt.subplots(figsize = (12, 7))
ax.scatter(win_rate, win_less)
txt = 'This point means that if the winning rate is 40%,\n then the probability of winning less\n than 6 trades is 25%.'
ax.annotate(txt, xy = (df.iloc[6][0], df.iloc[6][1]), xytext = (.35, .5), weight = 'bold', color  =  'r', size = 14,
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = 'b'))
ax.set_xlabel('Winning Rate')
ax.set_ylabel('Probabilit of winning Less than 6 trades')
plt.show()
```

###  Binomial Random Variable Generator

In Scipy library, every distribution has a random variable generator named ```.rvs```, we set parameters, the generator will return the randomly generated numbers.

```python
n = 100
p = 0.3
bino = sp.stats.binom.rvs(n, p, size = 10000)
```

The red vertical line in the graph below is the mean, because binomial distribution is a symmetric distribution, thus the mean should theoretically have the highest draw as well.

```python
txt = 'This line is the $y =p \cdot n$ \n and where theoretical highest draw should be.'
fig, ax = plt.subplots(figsize = (12, 8))
h, bins, patches = ax.hist(bino, bins = 160)
ax.axvline(p*n, color = 'r', ls = '--', lw = 3)
ax.annotate(txt, xy = (p*n, h.max()*0.4), xytext = (p*n ,h.max()*0.7), weight = 'bold', color  =  'r', size = 14,
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = 'b'))
ax.set_title('Generated Binomial R.V.s', size = 18)
plt.show()
```

But how to interpret the histogram?

A concrete example: you are trying to shoot basketball into the basket, your chances of success is $30\%$, what each day you can shoot $100$ rounds, but in the long run, you are mostly like to have $30$ successes each day, and it would be extremely unlikely that you would land a $50$-success.

###  Moments of Binomial Distribution

To return moments, use ```.stats``` functions with moments = `msvk`, it is short for _mean_, _skewness_, _variance_ and _kurtosis_.

```python
n = 100
p = 0.3
bino_stats = sp.stats.binom.stats(n, p, moments = 'mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', 'kurtosis '],
                [ bino_stats[0], bino_stats[1], bino_stats[2], bino_stats[3]]])
print(table.draw())
```

##  Poisson Distribution

When $n\rightarrow\infty$ and $p\rightarrow0$，a binomial distribution approaches a **Poisson distribution** asymptotically, i.e. when $n$ is large and $p$ is small, we can use Poisson to approximate Binomial.

Again with trader's example, if a trader has $1/1000$ probability to encounter a 'wiped-out' in each trade (assume each trade is independent, actually not), and trades $20$ times per month, what is the probability that the trader will encounter twice 'wiped-out' within 5 years?

This problem can be solved by Binomial, the formulation as below

$$
\text{trades} = 20\times 12\times 5=1200\\
P(x=2) = \binom{1200}{2}\Big(\frac{1}{1000}\Big)^2\Big(\frac{999}{1000}\Big)^{1198}
$$

```python
sp.special.comb(1200, 2)*1/1000**2*(999/1000)**1198
```

The result tells that if a trader keep a frequency of $20$ trades per month, there $21\%$ possibility that he/she gets wiped out twice in next $5$ years.

As we mentioned, Poisson is the limit version of Binomial, it is a suitable case to use, calculate $\lambda$

\begin{equation}
\lambda = np = 1200 \times \frac{1}{1000} = 1.2
\end{equation}

it means every 5 years, there is in average 1.2 times of chance to get wiped out.

\begin{equation}
P(x=2)=\frac{\lambda^ke^{-\lambda}}{k!}=\frac{1.2^2e^{-1.2}}{2!}
\end{equation}

Formulate in Python

```python
k = 2
n = 20 * 12 * 5 # 20 times per month, and 5 years span
p = 1/1000
lambdaP = p * n # lambda in Poisson
p = sp.stats.poisson.pmf(k, lambdaP)
print('The probability of having {0} wiped-out shock(s) in a span of 5 years is {1:.2f}%.'.format(k, p*100))
```

You probably have notices that Binomial and Poisson provide the same answer.

Similarly what's the probability of having more than $k$ times wiped-out?

```python
k = 2
prob = 1 - sp.stats.poisson.cdf(k, lambdaP)
print('The probability of having more than %1.0f BS shock in 5 years is %3.3f%%.' % (k, prob*100))
```

###  Poisson Random Variable Generator

Use the parameters $\lambda = 1.2$, we generate a frequency distribution with ```sp.stats.poisson.rvs``` function.

```python
lambdaP = 1.2
poisson = sp.stats.poisson.rvs(lambdaP, size = 1000)

fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(poisson, bins = 10)
ax.set_title('Generated Poisson R.V. Distribution', size = 16)
plt.show()
```

###  Moments of Poisson Distribution

```python
lambdaP = 1.2
poiss_stats = sp.stats.poisson.stats(lambdaP, moments = 'mvsk') # mean, variance, skewness, kurtosis
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', 'kurtosis '],
                [ poiss_stats[0], poiss_stats[1], poiss_stats[2], poiss_stats[3]]])
print(table.draw())
```

##  Geometric Distribution

The PMF of **Geometric Distribution** is
$$
f(k)=p(1-p)^k
$$

where  $k \in  \mathbb{Z}^+$ is a number of failures before first success, and $p$ is the probability of success.

Geometric Distribution is to model the solutions to questions: '_How many times you have to fail in order to embrace the initial success?_'

If you are shooting basketball to the basket, your success rate is $30\%$, what's the probability of first success after $k$ times of trials?

```python
k = 5
p = .3

geo_dist = (1 - p)**k * p
print('The probability of observing exact {} times of failures trials before first success is {:.2f}%.'.format(k, geo_dist*100))
```

```sp.stats.geom.pmf``` function will do the same trick.

```python
sp.stats.geom.pmf(k, p)
```

However, you have noticed the same parameters didn't produce the same result. That's because Scipy has a slightly different definition of parameter, $k$ in Scipy means the total number of trials, therefore $k+1$ would work the same.

```python
sp.stats.geom.pmf(k+1, p)
```

Again, CDF could answer the a question: _What's the probability of observing $k$ or less than $k$ times of failure before the first success?_

```python
geom_cdf = sp.stats.geom.cdf(k+1, p)
print('The probability of observing %1.0f or fewer than %1.0f times of failure before a first success is %3.3f%%.'%(k,k,geom_cdf*100))
```

###  Geometric Distribution Moments and Generator

```python
mean, var, skew, kurt = sp.stats.geom.stats(p, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c', 'c', 'c'])
table.set_cols_valign([ "m", "m", 'm','m', 'm', 'm'])
table.add_rows([['p','k',"mean", " variance ", ' skewness ', ' kurtosis '],
                [ p, k, mean, var, skew, kurt]])
print(table.draw())
```

```python
geometric = sp.stats.geom.rvs(p, size = 10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(geometric, bins = 30)
ax.set_title('Generated Geometric R.V. Distribution', size = 16)
plt.show()
```

##  Hypergeometric Distribution

The main difference between hypergeometric and binomial is that the former's sampling is not independent of each other, i.e. the  sampling is **without replacement**.

The PMF of hypergeometric is

$$
f(x) =\frac{{K\choose k} {N-K \choose n-k}}{{N\choose n}}
$$

Read the PMF with this example: $100$ people live in a building, $20$ of them are stashing drugs, $80$ are clean, but we don't have information who is clean or not. In one field operation, police took away $5$ persons form the building, what is the probability of having exact $2$ persons .are drug stasher?

Solution:
$$
\frac{{20\choose2}{80\choose3}}{{100\choose5}}
$$
To solve it:

```python
k = 2
n = 5
K = 20
N = 100
hyper_geo = sp.special.comb(K, k)*sp.special.comb(N-K, n-k) /sp.special.comb(N, n)
print('The probability of getting {} drug stashers by taking {} persons away is {:.2f}%.'.format(k, n, hyper_geo*100))
```

Or use SciPy function

```python
# pmf(x, M, N, n, loc=0)
hgeo = sp.stats.hypergeom.pmf(k, N, K, n, loc = 0) # the argurment order the same as MATLAB, quite confusing
hgeo
```

A histogram would provide some intuitions of geometric distribution.

```python
hgeo_rv = sp.stats.hypergeom.rvs(100, 20, 5, size = 10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(hgeo_rv, density = True)
ax.set_title('Generated Hypergeometric R.V. Distribution', size = 16)
s = '''    It can be interpreted as: if 100 persons in the building,
    20 are drug stasher, take 5 out of 100.

    The probability of getting from 1 to 5 drug stashers,
    is shown in the chart.

    As we can see it is nearly impossible
    to get 4 or 5 drugs stasher.
    But getting one is the most possible outcome.'''

ax.text(1.6, .5, s, fontsize=14, color ='red')
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.hypergeom.stats(N, K, n, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c', 'c', 'c'])
table.set_cols_valign([ "m", "m", 'm','m', 'm', 'm'])
table.add_rows([['N','k',"mean", " variance ", ' skewness ', ' kurtosis '],
                [ N, k, mean, var, skew, kurt]])
print(table.draw())
```

##  Discrete Uniform Distribution

Rolling a die is the simplest **discrete uniform distribution** generator from $1$ to $6$.

```python
unif_d = sp.stats.randint.rvs(low = 0, high = 6, size = 1000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(unif_d, bins = 6, ec = 'k')
ax.set_title('Discrete Uniform Frequency Distribution', size = 16)
plt.show()
```

###  Discrete Probability Mass Function

```python
x = np.arange(0, 8)
l, h = 1, 7
unif_pmf = sp.stats.randint.pmf(x, low = l, high = h)
fig, ax = plt.subplots(figsize = (12, 6))
ax.scatter(x, unif_pmf, s = 100, color = 'green', label =  'Low = {}, High = {}'.format(l, h-1))
ax.plot(x,unif_pmf, lw = 3, color = 'k')
ax.legend(fontsize = 18, loc = 'center')
plt.show()
```

##  Continuous Uniform Distribution

The PDF of *Continuous uniform distribution** is

\begin{equation}
f(x)=\frac{1}{b-a}
\end{equation}

And its r.v. generator is one of the most commonly used function in NumPy.

```python
unif = np.random.rand(10000)
fig, ax = plt.subplots(figsize = (12, 6))
h, bins, patches = ax.hist(unif, density = True, bins = 30)
ax.set_title('Continous Uniform Frequency Distribution', size = 16)
plt.show()
```

###  CDF and PDF of Continuous Uniform Distribution

The CDF and PDF of uniform distribution are ```sp.stats.uniform.cdf``` and ```sp.stats.uniform.pdf``` accordingly.

```python
# pdf(x, loc=0, scale=1)
# cdf(x, loc=0, scale=1)
x = np.linspace(-.2, 1.2, 100)
unif_pdf = sp.stats.uniform.pdf(x)
unif_cdf = sp.stats.uniform.cdf(x)
fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (17, 7))
ax[0].plot(x,unif_pdf, lw = 4, label = 'PDF of Continous Uniform Distribution')
ax[0].set_xlim([-.1, 1.1])
ax[0].set_ylim([0, 2])
ax[0].legend(fontsize = 16)

ax[1].plot(x,unif_cdf, lw  = 4, label = 'CDF of Continous Uniform Distribution')
ax[1].set_xlim([-.2, 1.2])
ax[1].set_ylim([0, 2])
ax[1].legend(fontsize = 16)
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.uniform.stats(moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', ' kurtosis '],
                [ mean, var, skew, kurt]])
print(table.draw())
```

##  Normal Distribution

The **normal distribution** is the king of all distributions, there are extensive discussions in my Linear Algbra Notes. The PDF of Normal distribution is
$$
f(x)=\frac{1}{\sigma \sqrt{2 \pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^{2}}
$$
Below are the plots of CDF and PDF.

```python
mu = 2
sigma = 1
x = np.arange(-2, 6, 0.1)
norm_pdf = sp.stats.norm.pdf(x, mu, sigma)
norm_cdf = sp.stats.norm.cdf(x, mu, sigma)

fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (17, 7))
ax[0].plot(x,norm_pdf, lw = 4, label = 'PDF of Normal Distribution', ls = '--')
ax[0].legend(fontsize = 16, loc = 'lower left', framealpha=0.2)

ax[1].plot(x,norm_cdf, lw  = 4, label = 'CDF of Normal Distribution')
ax[1].legend(fontsize = 16,fancybox=True, framealpha=0.5)
plt.show()
```

###  Inverse Normal CDF

Here is a useful plot that you might have known by heart. The light blue area cover $95\%$ of area under the bell curve, darker blue area on either side covers $2.5\%$ respectively. If we want to search the point where $2.5\%$ area is on its left, we simply need a inverse function of CDF, in SciPy ```.ppf``` (point percentage function) can provide us the location of any percentage that we specify.

```python
norm_95_r = sp.stats.norm.ppf(.975) # ppf mean point percentage function, actually inverse CDF
norm_95_l = sp.stats.norm.ppf(.025)

x = np.linspace(-5, 5, 200)
y = sp.stats.norm.pdf(x)

xl = np.linspace(-5, norm_95_l, 100)
yl = sp.stats.norm.pdf(xl)
xr = np.linspace(norm_95_r, 5, 100)
yr = sp.stats.norm.pdf(xr)

fig, ax = plt.subplots(figsize = (12, 7))
ax.plot(x,y, lw = 4, label = 'PDF of Normal Distribution', ls = '-', color = '#CC4845')
ax.set_ylim([0, .45])

ax.fill_between(x, y, 0, alpha=0.6, color = '#2E86C3')
ax.fill_between(xl,yl, 0, alpha=0.9, color = '#2E86C3')
ax.fill_between(xr,yr, 0, alpha=0.9, color = '#2E86C3')

ax.text(-.2, 0.15, '95%', fontsize = 20)
ax.text(-2.3, 0.015, '2.5%', fontsize = 12, color = 'white')
ax.text(2.01, 0.015, '2.5%', fontsize = 12, color = 'white')

ax.annotate('±%.4f' %norm_95_r, xy = (norm_95_r, 0), xytext = (-.4, .05), weight = 'bold', color  =  '#CC4845',
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = '#757516'), fontsize = 16)
ax.annotate('±%.4f' %norm_95_r, xy = (norm_95_l, 0), xytext = (-.4, .05), weight = 'bold', color  =  '#CC4845',
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = '#757516'), fontsize = 16)

ax.set_title('Normal Distribution And 2.5% Shaded Area', size = 20)
plt.show()
```

###  Normal Random Variable Generator

```python
# rvs(loc=0, scale=1, size=1, random_state=None)
norm_rv = sp.stats.norm.rvs(mu, sigma, size = 5000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(norm_rv, density = True, bins = 50)
ax.set_title('Normal Frequency Distribution', size = 16)
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.norm.stats(mu, sigma, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', ' kurtosis '],
                [ mean, var, skew, kurt]])
print(table.draw())
```

##  Bivariate Normal Distribution

The most prevalent _multivariate normal distribution_ is **bivariate normal distribution**

\begin{equation}
f_\boldsymbol{X}(x_1,...,x_2)=\frac{1}{\sqrt{(2\pi)^k|\Sigma|}}\exp{\Big(-\frac{(x-\mu)^T\Sigma^{-1}(x-\mu)}{2}\Big)}
\end{equation}

There are two ways of formulating a multivariate normal distribution in Python, use any one you see fit.

###  1st Method of Formulation

```python
mu_x = 0
sigma_x = 2

mu_y = 0
sigma_y = 2

#Create grid and multivariate normal
x = np.linspace(-10,10,500)
y = np.linspace(-10,10,500)
X, Y = np.meshgrid(x,y)
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X; pos[:, :, 1] = Y # more technical than next one
norm = sp.stats.multivariate_normal([mu_x, mu_y], [[sigma_x, 0], [0, sigma_y]]) # frozen

#Make a 3D plot
fig = plt.figure(figsize = (12, 12))
ax = fig.gca(projection='3d')
ax.plot_surface(X, Y, norm.pdf(pos),cmap='viridis',linewidth=0)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
ax.set_title('Bivariate Normal Distribution', size = 22, y = 1)

plt.show()
```

###  2st Method of Formulation

```python
#Parameters to set
mu_x = 0
sigma_x = 7

mu_y = 0
sigma_y = 15

x = np.linspace(-10,10,500)
y = np.linspace(-10,10,500)
X,Y = np.meshgrid(x,y)

pos = np.array([X.flatten(),Y.flatten()]).T  # more intuitive than former one

rv = sp.stats.multivariate_normal([mu_x, mu_y], [[sigma_x, 0], [0, sigma_y]])

fig = plt.figure(figsize=(10,10))
ax0 = fig.add_subplot(111)
ax0.contourf(X, Y, rv.pdf(pos).reshape(500,500),cmap='viridis')

plt.show()
```

##  Beta Distribution

The PDF of **Beta distribution** is

\begin{equation}
f(x, a, b)=\frac{\Gamma(a+b) x^{a-1}(1-x)^{b-1}}{\Gamma(a) \Gamma(b)}
\end{equation}

where $0\leq x \leq 1$ and $a>0$, $b>0$, $\Gamma$ is the Gamma function, and $a$ and $b$ decide the shape of Beta PDF.

```python
x = np.linspace(0, 1, 100)

fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(111)

a = [.5, 5, 1, 2, 2, 3, 1] # these are values of parameter a
b = [.5, 1, 5, 2, 3, 2, 1]

for parameter in zip(a, b):
    beta_pdf = sp.stats.beta.pdf(x, parameter[0], parameter[1])
    ax.plot(x, beta_pdf, lw = 3, label = '$a = %.1f, b =  %.1f$' % (parameter[0], parameter[1]))
    ax.legend()

ax.set_title('PDF of Beta Distribution', size = 20)
ax.axis([0, 1, 0, 3])
plt.show()
```

```python
x = np.linspace(0, 1, 100)

fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(111)

a = [.5, 5, 1, 2, 2, 3, 1] # these are values of parameter a
b = [.5, 1, 5, 2, 3, 2, 1]

for parameter in zip(a, b):
    beta_pdf = sp.stats.beta.cdf(x, parameter[0], parameter[1])
    ax.plot(x, beta_pdf, lw = 3, label = '$a = %.1f, b =  %.1f$' % (parameter[0], parameter[1]))
    ax.legend()
ax.set_title('CDF of Beta Distribution', size = 20)
ax.axis([0, 1, 0, 1])
plt.show()
```

Beta distribution is mostly useful as **prior distribution** in Bayesian estimation, because it is bounded in $[0, 1]$, that is perfect for modeling the _probability distribution of probabilities_. In Bayesian estimation, we only care about the proportion between prior and posterior, by adjusting $a$ and $b$ we can use Beta distribution to express any kinds of prior beliefs including normal, uniform, exponential distribution and etc.

\begin{equation}
f(x, a, b)\propto x^{a-1}(1-x)^{b-1}
\end{equation}

##  $\chi^2$ Distribution

$\chi^2$ distribution is closely connected with normal distributions, if $z$ has the standard normal distribution, then $z^2$ has the $\chi^2$ distribution with $d.f.=1$. And further,if

\begin{equation}
z_1, z_2, ..., z_k \sim i.i.d. N(0, 1)
\end{equation}

Then summation has a $\chi^2$ distribution of $d.f. = k$:

\begin{equation}
\sum_{i=0}^k z_i^2 \sim \chi^2(k)
\end{equation}

We will see in later chapters how $\chi^2$ distribution is referred when performing hypothesis testing.

###  $\chi^2$ PDF and CDF

```python
k = 1
x = np.linspace(0, 5, 100)
chi_pdf = sp.stats.chi2.pdf(x, k)

fig, ax = plt.subplots(figsize = (12, 8))
ax.plot(x, chi_pdf, lw = 3, c = 'r', label = '$\chi^2$ distribution with d.f. = 1')
ax.legend(fontsize = 18)
plt.show()
```

```python
fig, ax = plt.subplots(figsize = (12, 8))
for i in range(1, 6):
    x = np.linspace(0, 5, 100)
    chi_pdf = sp.stats.chi2.pdf(x, i)
    ax.plot(x, chi_pdf, lw = 3, label = '$\chi^2$ distribution with d.f. = %.0d'%i)
    ax.legend(fontsize = 12)
    ax.axis([0, 5, 0, 1])
plt.show()
```

##  F Distribution

If $U_1$ has a $\chi^2$ distribution with $\nu_1$ d.f. and $U_2$ has a $\chi^2$ distribution with $\nu_2$ d.f., then

\begin{equation}
\frac{U_1/\nu_1}{U_2/\nu_2}\sim F(\nu_1, \nu_2)
\end{equation}

We are using $F$ distribution for ratios of variances.

```python
x = np.linspace(0, 5, 100)
fig, ax = plt.subplots(figsize = (12, 8))

df1 = [10, 5, 8, 15]
df2 = [5, 10, 15, 8]

for df in zip(df1, df2):
    f_pdf = sp.stats.f.pdf(x, dfn = df[0], dfd = df[0])
    ax.plot(x, f_pdf, lw =3, label = '$df_1 = %.d, df_2 = %.d$' %(df[0], df[1]))
ax.legend(fontsize = 15)
plt.show()
```

$\chi^2$ and $F$ distribution are mostly used for statistical testing, we will elaborate the topic later.

##  Student's t Distribution

The **t-distribution** is used when data are approximately normally distributed, which means the data follow a bell shape but the population variance is unknown. We will come back to this topic in chapter of estimation.

The PDF of t-distribution is derived from normal and $\chi^2$ distribution
$$
f(t)=\frac{\Gamma\left(\frac{\nu+1}{2}\right)}{\sqrt{\nu \pi} \Gamma\left(\frac{\nu}{2}\right)}\left(1+\frac{t^{2}}{\nu}\right)^{-\frac{\nu+1}{2}}
$$
No need to know the meaning of its components. But we do need a visual memory of the comparison of t-distribution and normal distribution.

If a sample has $n$ observations, then the degree of freedom (d.o.f.) of t-distribution is $n-1$, the larger the d.o.f. the closer to normal distribution.

```python
x = np.linspace(-3, 3, 100)
y_norm = sp.stats.norm.pdf(x, loc=0, scale=1)
fig, ax = plt.subplots(figsize = (12, 8))
ax.plot(x, y_norm, label = 'Normal Distribution')
for i in range(1, 11):
    y_t = sp.stats.t.pdf(x, df = i, loc=0, scale=1)
    ax.plot(x, y_t, color = 'tomato', alpha = .1*i, label = 't-Distribution with dof = {}'.format(i))
ax.legend()
plt.show()
```


# Linear Production Model in Pyomo

[Pyomo](http://www.pyomo.org/) is a state-of-the-art language for solving optimization problems embedded within Python. Using Pyomo, a user can describe optimization model by specifying decision **variables**, **constraints**, and an optimization **objective**. Pyomo includes a rich set of features to enable modeling of complex systems, specifying a solver, and displaying the solution.

## Installation

### Anaconda

These instructions assume you have previously installed a suitable Python development environment, in particular the [Anaconda package for Python 3.x](https://www.anaconda.com/download).

The following commands will install Pyomo and extra files plus the glpk (MILP) and ipopt (nonlinear) solvers.  These commands should be executed one at a time from the terminal window on MacOS, or the Anaconda command prompt on Windows 10.

    conda install -c conda-forge pyomo
    conda install -c conda-forge pyomo.extras
    conda install -c conda-forge glpk
    conda install -c conda-forge ipopt

Optionally, you may also wish to install the COIN-OR cbc solvers.

    conda install -c conda-forge coincbc

This installation provides a capable open-source optimization suite with multiple solvers well suited a wide range of process applications. The solvers are

* [glpk](https://en.wikibooks.org/wiki/GLPK). "GNU Linear Programming Kit" is a software package written in highly portable C for the solution of mixed integer linear programming and related problems.
* [ipopt](https://en.wikipedia.org/wiki/IPOPT). "Interior Point Optimizer" for large scale nonlinear optimization of continuous systems.
* [cbc](https://projects.coin-or.org/Cbc). "COIN-OR Branch and Cut" is a mixed integer linear programming solver written in C++. It generally solves the same problems as glpk, but can run multiple threads, and is often faster and more robust.

This suite can be further augmented by installing additional solvers from open-source and commercial sources.

NOTE: If command window procedure fails on your laptop, you might try running these commands in a Jupyter notebook. In that case, each command needs to be prefixed with the shell escape `!`, and followed by the `-y` option to handle the y/n
interaction. For example, the basic pyomo install command would read

    !conda install -c conda-forge pyomo -y

To provide timely feedback, each install command should be executed in a separate cell.

### Google Colab

```python
%%capture
!pip install -q pyomo
!apt-get install -y -qq glpk-utils
```

## Algebraic Modeling Languages

One of the difficult aspects of applying optimization methods to large scale applications is creating and maintaining the underlying model. An application may include tens, hundreds, even thousdands of variables and constraints, and may incorporate data extracted in real time from sensor networks and corporate data bases. The resulting models can be very complex, and essentially impossible to create and maintain without use of software tools.

Algebraic Modeling Languages (AMLs) were developed to assist with the creation and maintainence of optimization models. [GAMS](https://en.wikipedia.org/wiki/General_Algebraic_Modeling_System) (General Algebraic Modeling System, https://www.gams.com/), first proposed in 1976, was among the first and still widely used. Other notable examples include [AIMMS](https://en.wikipedia.org/wiki/AIMMS), [AMPL](https://en.wikipedia.org/wiki/AMPL), and [FICO XPRESS](https://en.wikipedia.org/wiki/FICO_Xpress).

In recent years, modeling for optimization has become more tightly integrated with scripting languages commonly used in science and engineering. Open-source examples include [YALMIP](https://yalmip.github.io/) and [CVX](http://web.cvxr.com/cvx/doc/) which are tightly integrated with Matlab, [JuMP](https://jump.readthedocs.io/en/latest/) which works with Julia, and a variety of systems integrated with Python.

Of the Python options, the open-source [Pyomo](http://www.pyomo.org/) is perhaps the most ambitious and certainly the most aligned with the needs of process systems engineering.

## Key Concepts in Pyomo

A typical Pyomo application involves the creation of at least two Pyomo objects from the following classes:

* **ConcreteModel()**  A python object representing the optimization problem to be solved.
* **SolverFactory()** A python object representing the mathematical progamming software to be used for calculating a solution.

There are a number of of open-source and commercial solvers that can be used with Pyomo. This simple structure allows one to test and find solvers suited to a particular application.

A model, in turn, is composed of additional objects used to specify a problem. A minimal set of classes is needed to create useful models. These classes are:

* **Var()** Objects representing variables determined in the course of solving a particular problem.
* **Objective()** An object denoting the problem objective function that is to be either minimized or maximized.
* **Contraint()** Objects representing problem constraints.

The following cell presents a typical example.

## Example: Linear Production Model with Constraints

The mathematical formulation of a simple linear production model is given by

\begin{align}
\max_{x,y \geq 0} &\ 40\ x + 30\ y  & \mbox{objective}\\
\mbox{subject to:}\qquad \\
x & \leq 40  & \mbox{demand constraint} \\
x + y & \leq 80  & \mbox{labor A constraint} \\
2x + y & \leq 100 & \mbox{labor B constraint}
\end{align}

where $x$ and $y$ are the rates of production (in units per week) for two products.

### Step 1. Import Pyomo.

The first step in any Pyomo project is to import relevant components of the Pyomo library. This can be done with the following python statement.

    from pyomo.environ import *

### Step 2. Create the model object.

Pyomo provides two distinct methods to create models. Here we use `ConcreteModel()` to create a model instance which is appropriate when all of the data needed to complete the model is avaiable at the current time.

    model = ConcreteModel()

The Python variable `model` stores the model instance. This could be any valid Python variable name.

### Step 3. Add the Decision Variables, Objective, and Constraints to the model object.

The first major component of a Pyomo model are decision variables which are added as fields to `model`. In the case we name the fields `model.x` and `model.y` corresponding to $x$ and $y$ in the process model. The Python class `Var()` is used to specify these as real numbers that must be greater than or equal to zero.

    model.x = Var(domain=NonNegativeReals)
    model.y = Var(domain=NonNegativeReals)

As we will see in other examples, the `domain` can specify other types of decision variables including reals, integers, and booleans.

The objective is specified as an algebraic expression involving the decision variables. Here we store the objective in `model.profit`, and use the optional keyword `sense` to specify a maximization problem.

    model.profit = Objective(expr = 40*model.x + 30*model.y, sense=maximize)

Constraints are added as fields to the model, each constraint created using the `Constraint()` class. The constraints are specified using the `expr` keywork in the form of linear algebraic expressions of the decision variables.

    model.demand = Constraint(expr = model.x <= 40)
    model.laborA = Constraint(expr = model.x + model.y <= 80)
    model.laborB = Constraint(expr = 2*model.x + model.y <= 100)

### Step 4.  Create a solver object and solve.

The Pyomo libary includes a `SolverFactory()` class used to specify a solver. In this case, because the problem is a linear programming problem, we use the `glpk` solver.

    results = SolverFactory('glpk').solve(model)
    results.write()
    if results.solver.status == 'ok':
        model.pprint()

The `solve()` method attempts to solve the model using the specified solver, and returns `results` which can be inspected to see if, in fact, a solution was found. If a solution is found, then `model` will have a pretty-print method `pprint()` that creates a summary of the problem solution.

### Step 5. Display the solution.

Most applications will require access to specific components of the model. If a solution is found, the model will include methods with the same name as the fields created above, and which can be used to access solution values.

    print('Profit = ', model.profit())

    print('\nDecision Variables')
    print('x = ', model.x())
    print('y = ', model.y())

    print('\nConstraints')
    print('Demand  = ', model.demand())
    print('Labor A = ', model.laborA())
    print('Labor B = ', model.laborB())

```python
from pyomo.environ import *

# create a model
model = ConcreteModel()

# declare decision variables
model.x = Var(domain=NonNegativeReals)
model.y = Var(domain=NonNegativeReals)

# declare objective
model.profit = Objective(expr = 40*model.x + 30*model.y, sense=maximize)

# declare constraints
model.demand = Constraint(expr = model.x <= 40)
model.laborA = Constraint(expr = model.x + model.y <= 80)
model.laborB = Constraint(expr = 2*model.x + model.y <= 100)

# solve
results = SolverFactory('glpk').solve(model)
results.write()
if results.solver.status:
    model.pprint()

# display solution
print('\nProfit = ', model.profit())

print('\nDecision Variables')
print('x = ', model.x())
print('y = ', model.y())

print('\nConstraints')
print('Demand  = ', model.demand())
print('Labor A = ', model.laborA())
print('Labor B = ', model.laborB())
```

    # ==========================================================
    # = Solver Results                                         =
    # ==========================================================
    # ----------------------------------------------------------
    #   Problem Information
    # ----------------------------------------------------------
    Problem:
    - Name: unknown
      Lower bound: 2600.0
      Upper bound: 2600.0
      Number of objectives: 1
      Number of constraints: 4
      Number of variables: 3
      Number of nonzeros: 6
      Sense: maximize
    # ----------------------------------------------------------
    #   Solver Information
    # ----------------------------------------------------------
    Solver:
    - Status: ok
      Termination condition: optimal
      Statistics:
        Branch and bound:
          Number of bounded subproblems: 0
          Number of created subproblems: 0
      Error rc: 0
      Time: 0.014054059982299805
    # ----------------------------------------------------------
    #   Solution Information
    # ----------------------------------------------------------
    Solution:
    - number of solutions: 0
      number of solutions displayed: 0
    2 Var Declarations
        x : Size=1, Index=None
            Key  : Lower : Value : Upper : Fixed : Stale : Domain
            None :     0 :  20.0 :  None : False : False : NonNegativeReals
        y : Size=1, Index=None
            Key  : Lower : Value : Upper : Fixed : Stale : Domain
            None :     0 :  60.0 :  None : False : False : NonNegativeReals

    1 Objective Declarations
        profit : Size=1, Index=None, Active=True
            Key  : Active : Sense    : Expression
            None :   True : maximize : 40*x + 30*y

    3 Constraint Declarations
        demand : Size=1, Index=None, Active=True
            Key  : Lower : Body : Upper : Active
            None :  -Inf :    x :  40.0 :   True
        laborA : Size=1, Index=None, Active=True
            Key  : Lower : Body  : Upper : Active
            None :  -Inf : x + y :  80.0 :   True
        laborB : Size=1, Index=None, Active=True
            Key  : Lower : Body    : Upper : Active
            None :  -Inf : 2*x + y : 100.0 :   True

    6 Declarations: x y profit demand laborA laborB

    Profit =  2600.0

    Decision Variables
    x =  20.0
    y =  60.0

    Constraints
    Demand  =  20.0
    Labor A =  80.0
    Labor B =  100.0

## Python Lists, Sets, Dictionaries, and Iterators

Pyomo is integrated with the Python language, and inherits significant functionality by leveraging the basic data structures of Python. To make the best use of Pyomo, it is important to understand the basics of Python lists, sets, and dictionaries.

### Data in Matrix/Vector Format

The example above used scalar modeling components, `model.x = Var()` and `model.y = Var()`, and each constraint was added as a separate line in the model.  This is fine for small problems with a just a few unknowns, but becomes impractical for larger problems.

Here we repeat the example above, this time using `numpy` arrays to enter the data. An indexed variable `model.x` represents the unknowns, and the constraints are indexed as well. The indices are represented by the Python `range()` statement.

```python
from pyomo.environ import *
import numpy as np

# enter data as numpy arrays
A = np.array([[1, 0], [1, 1],[2,1]])
b = np.array([40, 80,100])
c = np.array([40,30])

# set of row indices
I = range(len(A))

# set of column indices
J = range(len(A.T))

# create a model instance
model = ConcreteModel()

# create x and y variables in the model
model.x = Var(J)

# add model constraints
model.constraints = ConstraintList()
for i in I:
    model.constraints.add(sum(A[i,j]*model.x[j] for j in J) <= b[i])

# add a model objective
model.objective = Objective(expr = sum(c[j]*model.x[j] for j in J), sense=maximize)

# create a solver
#brew install glpk
solver = SolverFactory('glpk')

# solve
solver.solve(model)

# print solutions
for j in J:
    print("x[", j, "] =", model.x[j].value)

model.constraints.display()
```

    x[ 0 ] = 20.0
    x[ 1 ] = 60.0
    constraints : Size=3
        Key : Lower : Body  : Upper
          1 :  None :  20.0 :  40.0
          2 :  None :  80.0 :  80.0
          3 :  None : 100.0 : 100.0

```python

```

```python

```

```python

```