# Solution Methods for Microeconomic Dynamic Stochastic Optimization Problems (MicroDSOP)

- Author: Tao Wang and Chris Carroll (as of Oct 2021)

This notebook reproduces all the figures generated by Mathematica (except for those associated with the "method of moderation") in Chris's Carroll's [SolvingMicroDSOP lecture notes](https://llorracc.github.io/SolvingMicroDSOPs/) using Python.

The purpose here is to mirror the results from the original lecture notes as closely as possible. Therefore, the notebook contains a stand-alone chunk of code specific to the content of each section in the paper.

<!-- Tao:  I don't see the notebook mentioned below.  -->

<!-- A bundled SolvingMicroDSOP class that could attain the solution in different approaches discussed in the lecture note can be found in the notebook [SolvingMicroDSOPs_ExtendedVersion](SolvingMicroDSOPS_ExtendedVersion.ipynb). -->

### Some Python-related specifics: import necessary classes

The method takes advantage of Python's ability to bundle parameters with functionality via its object-oriented framework. We will import classes for CRRA utility, a class that discretes approximation to continuous distributions, and the "gothic" class that encapsulates functions $\mathfrak{v}$, $\mathfrak{v}'$, $\mathfrak{c}$, which all involve expectations.

- The "utility" function class allows us to create an instance of a utility function, setting the risk aversion parameter once, and has a convenient "prime" method which executes the first derivative.

- "DiscreteApproximation" fully implements an instance of the discrete approximation to a continuous distribution, taking advantage of SciPy's "frozen distribution" framework. It has a convenient way of calculating the expected value of any defined function of the random variable.

- The "Gothic" class is bundled with methods that execute $\mathfrak{v}$, $\mathfrak{v}'$, $\mathfrak{c}$, and interpolations of each. They all involve calculating expectations of utility/marginal utility/value/marginal values, which will loop over some discretized values of the income shock.

```python
# First import all necessary libraries.
import numpy as np               # Import Python's numeric library as an easier-to-use abbreviation, "np"
import pylab as plt              # Import Python's plotting library as the abbreviation "plt"
import scipy.stats as stats      # Import Scientific Python's statistics library
from scipy.optimize import brentq, minimize_scalar # Import the brentq root-finder
from scipy.optimize import minimize
from resources import Utility, DiscreteApproximation,DiscreteApproximationToMeanOneLogNormal,DiscreteApproximationTwoIndependentDistribs
from scipy.interpolate import InterpolatedUnivariateSpline
from numpy import log, exp    # for cleaner-looking code below
from copy import copy
from time import time
import warnings

### for comparison purposes
from HARK.ConsumptionSaving.ConsPortfolioModel import PortfolioConsumerType, init_portfolio
from HARK.ConsumptionSaving.ConsIndShockModel import init_lifecycle
from HARK.utilities import plot_funcs
```

## 0. Define Parameters, Grids, Utility Function
Set up general parameters, as well as the first two major class instances: the utility function and the discrete approximation.

```python
# Set up general parameters:

rho = 2.0    ### CRRA coefficient
beta = 0.96  ### discount factor
gamma = np.array([1.0]) # permanent income growth factor
                        # A one-element "time series" array
                        # (the array structure needed for gothic class below)
R = 1.02    ## Risk free interest factor

# Define utility:
u = Utility(gamma=rho)
```

## 1. Discretization of the Income Shock Distribution

- See detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Discretizing-the-Distribution)

```python
# Create a discrete approximation instance:

theta_sigma = 0.5
theta_mu = -0.5*(theta_sigma**2)
theta_z = stats.lognorm(theta_sigma, 0, np.exp(theta_mu))    # Create "frozen" distribution instance
theta_grid_N = 7  ### how many grid points to approximate this continuous distribution

theta = DiscreteApproximation(N = theta_grid_N, cdf=theta_z.cdf, pdf=theta_z.pdf, invcdf=theta_z.ppf)

#theta2 = DiscreteApproximationToMeanOneLogNormal(N=theta_grid_N,sigma=sigma)
## (both are correct)
```

```python
#############################
## Figure 1
############################

x_min = theta_mu
x0 = 0.0
x1 = 4.0

theta.plot(x0,x1)

print('The distance between two adjacent horizontal dash lines represent the equiprobable bin.')
```

```python
## Other parameters

# Self-imposed lower bounds(for period T)
# the minimum asset (the maximum debt) is the discounted value of lowest possible transitory income shock
## the so called "natural borrowing constraint"

self_a_min = -min(theta.X) *gamma[0]/ R     # self-imposed minimum a

```

## A Gothic Class

The main code used to implement the solution can be found in the "Gothic" class definition, which contains methods implementing the $\mathfrak{v}$, $\grave{\mathfrak{v}}$  (linear interpolation of $\mathfrak{v}$), $\mathfrak{v}'$, $\grave{\mathfrak{v}}'$, and $\mathfrak{c}$, $\grave{\mathfrak{c}}'$ functions. These are essentially expectated value, expected marginal value, and expected marginal utility, respectively, as the functions of next-period value and consumption policy functions.

Since these functions all involve computing expectations, we bundle them together as a Gothic class and use an instance of the class below to solve a consumption function; this mirrors Sections 5.5 - 5.9 in the [lecture note](https://llorracc.github.io/SolvingMicroDSOPs/).

#### Import and create instance of the "Gothic" class

Create a particular instance of the "Gothic" class, using $u$, $\beta$, $\rho$, $\gamma$, and the $\theta$-approximation.

```python
from gothic_class import Gothic
gothic = Gothic(u, beta, rho, gamma, R, theta)
```

## 2. Solving the Model by Value Function Maximization

- See detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#x1-70005.2)

First, we numerically maximize the value functions over a very fine set of grid points of the market resource (m) to get the benchmark solution to consumption policy and value function. We use it to approximate the "accurate" solution in this notebook.

This approach will be improved in different ways in the sections below.

```python
m_min, m_max, m_size = -0.1, 4.0, 5

# include some negative values
```

```python
### solve the model with very fine mVec_fine

cVec = []   # Start with an empty list. Lists can hold any Python object.
vVec = []   # Empty list for values

## fine grid of m
mVec_fine = np.linspace(m_min, m_max, 100)

for m in mVec_fine:
    c_max= m + gamma[0]*theta.X[0]/R  ## minimum total wealth

    nvalue = lambda c: -(u(c) + gothic.V(m - c))## Define the a one-line (negative) value function of the T-1 period
                                                ## negative value to be minimized
                                                ## notice here V only takes saving at T-1 as input since this is the T-1 period
                                                ## the consumption policy at T is trivial
    ## for this m, find the c that maximizes the value from T-1 and T
    res = minimize(nvalue,
                   np.array([0.3*m]),  ## an initial guess of the c
                   method='trust-constr', ## trust-constr turns out a good constrained optimization algorithm
                   bounds =((1e-12,0.999*c_max),) , ## consumption has to be between 0.0 and c_max
                   options={'gtol': 1e-12,
                           'disp':False}
                  )
    c = res['x'][0]            # maximizer of the value
    v = -res['fun']            ## value
    cVec.append(c)
    vVec.append(v)

```

    /Volumes/Sync/Sys/OSX/linked/root/usr/local/bin/anaconda/lib/python3.8/site-packages/scipy/optimize/_hessian_update_strategy.py:182: UserWarning: delta_grad == 0.0. Check if the approximated function is linear. If the function is linear better results can be obtained by defining the Hessian as zero instead of using quasi-Newton approximations.
      warn('delta_grad == 0.0. Check if the approximated '

```python
# Look at the solution

plt.title(r'$c_{T −1}(m_{T-1})$ (solid)')
plt.plot(mVec_fine,cVec,'k-')   ## c(m)
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$c(m_{T-1})$')
```

Then, solve the model with only a small number of grid points of m.

```python
# very sparse m_grid

mVec = np.linspace(m_min, m_max, m_size)
```

```python
cVec0 = []   # Start with empty list. Lists can hold any Python object.
vVec0 = []

for m in mVec:
    c_max= m + gamma[0]*theta.X[0]/R

    nvalue = lambda c: -(u(c) + gothic.V(m - c))  # Define the a one-line value function of the T-1 period

    res = minimize(nvalue,
                   np.array([0.2*m]),
                   method='trust-constr',
                   bounds =((1e-12,
                             0.999*c_max),) ,
                   options={'gtol': 1e-12,
                           'disp':False}
                  )
    c = res['x'][0]            # maximizer of the value
    v = -res['fun']            ## value
    cVec0.append(c)
    vVec0.append(v)

print(cVec0)  # Look at the consumption from the list.
```

    [0.21504381726759095, 0.8376488053983221, 1.3951569910040684, 1.9353194102565596, 2.4682182754077435]

    /Volumes/Sync/Sys/OSX/linked/root/usr/local/bin/anaconda/lib/python3.8/site-packages/scipy/optimize/_hessian_update_strategy.py:182: UserWarning: delta_grad == 0.0. Check if the approximated function is linear. If the function is linear better results can be obtained by defining the Hessian as zero instead of using quasi-Newton approximations.
      warn('delta_grad == 0.0. Check if the approximated '

## 3. An Interpolated Consumption Function

We solve optimal c above for a finite set of predetermined grids of m, how do we know the consumption value at different values of m not in these grid points? We need interpolation.

- See detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#x1-80005.3)

The interpolated consumption function is not very different from the true consumption function. (See Figure 2)

```python
## interpolated c func based on the solution from a small number of m grids

cFunc0 = InterpolatedUnivariateSpline(mVec,cVec0,k=1)
mVec_int = np.linspace(0.0,4.0,50)
cVec_int = cFunc0(mVec_int)

######################################
### Figure 2
######################################
plt.plot(mVec_fine,cVec,'k-')   ## c(m)
plt.plot(mVec_int,cVec_int,'--') ## 'c(m)
plt.xlim(self_a_min, 4.0)
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$c(m_{T-1})$')
plt.title(r'$c_{T −1}(m_{T-1})$ (solid) versus $\grave c_{T-1}(m_{T-1})$(dashed)')
```

Does the interpolation of value function work as well as that for consumption function?

It turns out that the interpolated value function only poorly approximates its true counterpart.(See Figure 3) The reason for this is that value function is highly concave.

```python
## interpolated v func
vFunc0 = InterpolatedUnivariateSpline(mVec,vVec0,k= 1)

vVec_int = vFunc0(mVec_int)

#########################################
### Figure 3
########################################

plt.plot(mVec_fine,vVec,'k-')   ## v(m)
plt.plot(mVec_int,vVec_int,'--') ## 'v(m)
plt.xlim(self_a_min, 4.0)
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$V_{T-1}$')
plt.title(r'$v_{T −1}(m_{T −1})$ (solid) versus $\grave v_{T-1}(m_{T-1})$ (dashed)')
```

## 4. Interpolating Expectations

- See detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Interpolating-Expectations)

The program above turns out to be __inefficient__. For every value of $m_{T −1}$ the program must calculate the utility consequences of various possible choices of $c_{T−1}$ as it searches for the best choice. But for any given value of $m_{T-1}-c_{T-1}=a_{T−1}$, there is a good chance that the program may end up calculating the corresponding v many times while maximizing utility from different $m_{T −1}$’s.

An improvement can be made: we can construct a direct numerical approximation to the value function based on a vector of predefined $a=m-c$ grid and use the interpolated function to calculate $\mathfrak{v}_{T-1}$ for a given $a$.

```python
# A grid:
a_min, a_max, a_size = 0.0, 4.0, 5
aVec = np.linspace(a_min, a_max, a_size)

## gothiv v values for a vector of a
## get the values from a finely constructed a
gothicvVec = np.array([gothic.V(a) for a in aVec])
## then create an interpolation of that to use in solving the models
gothicvFunc0 = InterpolatedUnivariateSpline(aVec,gothicvVec,k = 1) ## this is the interpolated gothic v func

## solve the model again

cVec1 = []   # Start with empty list. Lists can hold any Python object.
vVec1 = []

for m in mVec:
    c_max= m + gamma[0]*theta.X[0]/R

    ## Below, instead of using gothic.V func, we use the interpolated gothicvFun
    nvalue = lambda c: -(u(c) + gothicvFunc0(m - c))
    ##################################################################################################
    #### Notice here, instead of drawing Gothic.V function, we use the interpolation of it gothicvFunc0
    ###################################################################################################

    res = minimize(nvalue,
                   np.array([0.3*m]),
                   method='trust-constr',
                   bounds =((1e-12,0.999*c_max),),
                   options={'gtol': 1e-12,
                           'disp':False}
                  )
    c = res['x'][0]            # maximizer of the value
    v = -res['fun']          ## value
    cVec1.append(c)
    vVec1.append(v)

print(cVec1)
```

    /Volumes/Sync/Sys/OSX/linked/root/usr/local/bin/anaconda/lib/python3.8/site-packages/scipy/optimize/_hessian_update_strategy.py:182: UserWarning: delta_grad == 0.0. Check if the approximated function is linear. If the function is linear better results can be obtained by defining the Hessian as zero instead of using quasi-Newton approximations.
      warn('delta_grad == 0.0. Check if the approximated '

    [0.301102241520467, 1.1880191548134826, 1.188019151792427, 1.9749999951053032, 2.372419510096964]

The interpolated functions are of course identical at the gridpoints chosen for $a_{T− 1}$   and they appear reasonably close except in the region below $m_{T −1} = 1.0$. (See Figure 4)

```python
#################
### Figure 4
#################

### get real gothic v
aVec_fine = np.linspace(a_min, a_max, 100)
gothicvVec_fine  = np.array([gothic.V(a) for a in aVec_fine])

#plt.plot(mVec,vVec1,'-.')
plt.plot(aVec_fine,gothicvVec_fine,'k-')
plt.plot(aVec,gothicvVec,'--')
plt.xlabel(r'$a_{T-1}$')  ### this is different from lecture note
plt.ylabel(r'$\mathfrak{v}_{T-1}$')
plt.title(r'End of period value $\mathfrak{v}_{T-1}(a_{T-1})$ (solid) versus $\grave \mathfrak{v}_{T-1}(a_{T-1})$ (dashed)')
```

Nevertheless, the resulting consumption rule obtained when $\grave{\mathfrak{v}}_{T-1}(a_{T-1})$ is used instead of $\mathfrak{v}_{T −1}(a_{T−1})$  is surprisingly bad. (See Figure 5)

```python
####################
### Figure 5
####################

plt.plot(mVec_fine,cVec,'k-')
plt.plot(mVec,cVec1,'--')
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$c_{T-1}$')
plt.title(r'$c_{T-1}(m_{T-1})$ (solid) versus $\grave c_{T-1}(m_{T-1})$ (dashed)')
```

## 5. Value Function versus the First Order Condition

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Value-Function-versus-First-Order-Condition)

Our difficulty is caused by the fact that the consumption choice is governed by the marginal value function, not by the level of the value function (which is what we tried to approximate).

This leads us to an improved approach to solving consumption policy by working with marginal utility/values via the  first-order conditions(FOC). For every exogenously set m grid, we could find the root of the FOC.

\begin{equation}
u'(c_{T-1}(m_{T-1})) = \mathfrak{v}'(m_{T-1}-c_{T-1}(m_{T-1}))
\end{equation}

```python
##########################
### Figure 6 (omitted here)
##########################

#u′(c) versus v′_{T −1}(3 − c), v′_{T −1}(4 − c), v`′_{T −1}(3 − c), v`′_{T −1}(4 − c)
```

Now solve the consumption using FOCs instead of value function maximization.

```python
cVec2 = []   # Start with empty list.
for m in mVec:
    mintotwealth= m + gamma[0]*theta.X[0]/R
    foc_condition = lambda c: u.prime(c) - gothic.VP_Tminus1(m - c)  # Define the a one-line function for the FOC
    c = brentq(foc_condition, 1e-12, 0.999*mintotwealth)              # Zero-find on the FOC
    cVec2.append(c)

print(cVec2)  # Look at the consumption from the list.
```

    [0.21504382477195785, 0.8376488103781283, 1.3951570021328097, 1.9353194204290078, 2.4682183015706145]

The linear interpolating approximation looks roughly as good (or bad) for the marginal value function as it was for the level of the value function.

```python
########################
## Figure 7
########################

#get the VP for fine grids of a

aVec_fine = np.linspace(0.0001, 4, 1000)
vpVec_fine = [gothic.VP_Tminus1(a) for a in aVec_fine]

# Examine the interpolated GothicVP function:
vpVec = [gothic.VP_Tminus1(a) for a in aVec]

## this is the interpolated gothic v func
gothicvpFunc = InterpolatedUnivariateSpline(aVec,vpVec,k = 1)

plt.plot(aVec_fine, vpVec_fine, 'k-') ## gothic v'(a)
plt.plot(aVec, gothicvpFunc(aVec), '--')   ## 'gothic v'(a)
plt.ylim(0.0, 1.0)
plt.xlabel(r'$a_{T-1}$')
plt.ylabel(r'$\mathfrak{v}^{\prime}_{T-1}$')
plt.title(r'$\mathfrak{v}^{\prime}(a_{T-1})$ (solid) and $\grave \mathfrak{v}^{\prime}(a_{T-1})$ (dashed)')
```

The new consumption function (long dashes) is a considerably better approximation of the true consumption function (solid) than was the consumption function obtained by approximating the level of the value function (short dashes). (See Figure 8).

```python
#########################
## Figure 8
########################

plt.plot(mVec_fine,cVec,'k-',label=r'$c_{T-1}(m_{T-1})$')  ## real c func
plt.plot(mVec,cVec1,'-.',label=r'$\grave c_{T-1}(m_{T-1})$ via $\mathfrak{v}$')      ## interpolated c func based on interpolated level of value
plt.plot(mVec,cVec2,'r--',label=r'$\grave \grave c_{T-1}(m_{T-1})$ via $\mathfrak{v}^{\prime}$')      ## interpolated c func based on interpolated marginal value
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$c_{T-1}$')
plt.title(r'$c_{T-1}(m_{T-1})$ (solid) versus two methods of constructing $\grave c_{T-1}(m_{T-1})$')
plt.legend(loc=4)
```

## 6. Transformation

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Transformation)

However, even the new-and-improved consumption function diverges from the true solution, especially at lower values of m. That is because the linear interpolation does an increasingly poor job of capturing the nonlinearity of $v′_{T −1}(a_{T −1})$ at lower and lower levels of a.

```python
cVec3 = []
cVec4 = []

for a in aVec:
    c = gothic.C_Tminus1(a)
    cVec3.append(c)

for a in aVec_fine:
    c = gothic.VP_Tminus1(a)**(-1/rho)
    cVec4.append(c)
```

## 7. The Self-Imposed ‘Natural’ Borrowing Constraint and the $a_{T−1}$ Lower Bound

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#The-Self-Imposed-Natural-Borrowing-Constraint-and-the-a-Lower-Bound)

As the marginal utility from zero consumption goes to infinity, the agents will try to have at least a little of consumption in the "worst" case. This cutting-edge case happens when the $a_{T-1}$ is exactly equal to the present value of the worst possible transitory shock. As a result, there is a self-imposed borrowing constraint, i.e. a lower bound for the value of $a_{T-1}$.

In general, preset grids of a may not necessarily include this point. We hence insert this self-imposed bound to the beginning of the list of grid to make sure the consumption policy close to this bound is correctly interpolated.

```python
## Augment the mVec and cVec with one point with bottom

aVecBot = np.insert(aVec,0,self_a_min)
cVec3Bot  = np.insert(cVec3,0,0.0)

aVecBot_fine = np.insert(aVec_fine,0,self_a_min)
cVec4Bot  = np.insert(cVec4,0,0.0)
```

This ﬁgure well illustrates the value of the transformation (Section 6): The true function is close to linear, and so the linear approximation is almost indistinguishable from the true function except at the very lowest values of $a_{T−1}$. (See Figure 9)

```python
##############
## Figure 9
##############

plt.plot(aVecBot_fine,cVec4Bot,'k-',label=r'$(\mathfrak{v}^{\prime}_{T-1}(a_{T-1}))^{-1/\rho}$')
plt.plot(aVecBot,cVec3Bot,'--',label=r'$\grave c_{T-1}$')
plt.xlabel(r'$a_{T-1}$')
plt.ylabel(r'$\mathfrak{c}_{T-1}$')
plt.ylim(0.0,5.0)
plt.vlines(0.0,0.0,5.0,'k','-.')
plt.title(r'$\mathfrak{c}_{T-1}(a_{T-1})$ (solid) versus $\grave \mathfrak{c}_{T-1}(a_{T-1})$')
plt.legend(loc=4)
```

```python
## compute gothic_v' as c^(-rho)

vpVec_fromc = lambda a: InterpolatedUnivariateSpline(aVecBot,cVec3Bot,k=1)(a)**(-rho)
```

And when we calculate $\grave{\grave{\mathfrak{v}^{\prime}_{T-1}}}(a_{T−1})$ as $[\grave{\mathfrak{c}_{T-1}}(a_{T-1})]^{-\rho}$  (dashed line) we obtain a much closer approximation of $\mathfrak{v}^{\prime}_{T-1}(a_{T-1})$. (See Figure 10)

```python
#############################
## Figure 10
############################

plt.plot(aVec_fine, vpVec_fine, 'k-')
plt.plot(aVec_fine,vpVec_fromc(aVec_fine),'--')
plt.xlabel(r'$a_{T-1}$')
plt.ylabel(r'$\mathfrak{v}^{\prime}_{T-1},\quad \grave \grave \mathfrak{v}^{\prime}_{T-1}$')
plt.title(r'$\mathfrak{v}^{\prime}_{T-1}(a_{T-1})$ (solid) versus $\grave \grave \mathfrak{v}^{\prime}(a_{T-1})$' +
          '\n constructed using $\grave \mathfrak{c}_{T-1}$ (dashed)')
```

## 8. Endogenous Gridpoints: Use Algebra to Find $c_{T-1}(m_{T-1})$

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#The-Method-of-Endogenous-Gridpoints)

We now take advantage of the fact that

$$m_{T−1,i} = c_{T−1,i} + a_{T−1,i}\;\;\forall a_{i} \in \mathbb{A}_{grid},$$

to find optimal consumption a'
s a function of $m$, once we have the optimizing choice and $a$ in hand.

```python
# Create the first point in the consumption function:
mVec_egm= [self_a_min] ## consumption is zero therefore a = m here
cVec_egm = [0.0]

for a in aVec:
    c = gothic.C_Tminus1(a)
    m = c + a
    cVec_egm.append(c)
    mVec_egm.append(m)

# Set up the interpolation:
cFunc_egm = InterpolatedUnivariateSpline(mVec_egm, cVec_egm, k=1)
```

Compared to the approximate consumption functions illustrated in Figure 8 $\grave c_{T-1}$ is quite close to the actual consumption function. (See Figure 11)

```python
####################
 ## Figure 11 ####
####################

# Plot the first consumption function (c_{T-1}). We will plot the rest in the loop below.
#plt.plot(mVec_egm, mVec_egm, color='0.7')    # Gray 45 deg line
#plot_m_max = 5.0                               # Max plotting point.
#plot_c_max = cFunc_egm(plot_m_max)
temp_c_values = cFunc_egm(mVec_egm)

plt.plot(mVec_fine,cVec,'k-')
plt.plot(mVec_egm, temp_c_values,'--')
plt.xlim(self_a_min,4.0)
plt.ylim(0.0,3.0)
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'${c}_{T-1}(m_{T-1})$')
plt.title(r'$c_{T-1}(m_{T-1})$ (solid) versus $\grave c_{T-1}(m_{T-1})$ using EGM')
```

## 9. Improve the $\mathbb{A}_{grid}$

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Improving-the-a-Grid)

We will improve our $\mathbb{A}_{grid}.$

We use a multi-exponential growth rate (that is $e^{e^{e^{...}}}$ for some number of exponentiations n) from each point to the next point is constant (instead of, as previously, imposing constancy of the absolute gap between points).

```python
### This function creates multiple-exp a_grid

def setup_grids_expMult(minval, maxval, size, timestonest=20):
    i=1
    gMaxNested = maxval;
    while i <= timestonest:
        gMaxNested = np.log(gMaxNested+1);
        i += 1

    index = gMaxNested/float(size)

    point = gMaxNested
    points = np.empty(size)
    for j in range(1, size+1):
            points[size-j] = np.exp(point) - 1
            point = point-index
            for i in range(2,timestonest+1):
                points[size-j] = np.exp(points[size-j]) - 1
    a_grid = points
    return a_grid
```

```python
def set_up_improved_EEE_a_grid(minval, maxval, size):
    gMinMin = 0.01*minval;
    gMaxMax = 10*maxval;
    gridMin = log(1+log(1+log(1+gMaxMax)))
    gridMax = (log(1+log(1+log(1+gMaxMax)))-log(1+log(1+log(1+gMinMin))))/size
    index = log(1+log(1+log(1+gMinMin))) + (log(1+log(1+log(1+gMaxMax)))-log(1+log(1+log(1+gMinMin))))/size
    i = 1
    point = 0
    points = []
    while point < gridMin:
        point = point + index
        points.append(point)
        i += 1

    new_a_grid = exp(exp(exp(points)-1)-1)-1
    return new_a_grid
```

```python
### create the new grid with multiple exponential approach

a_size_splus = 20 ## just need a little more than 5 to cover the whole range of a well

aVec_eee = setup_grids_expMult(a_min, a_max, a_size_splus)
print(aVec_eee)
```

    [0.00544505 0.01151539 0.01832306 0.02600821 0.03474848 0.04477235
     0.05637847 0.06996458 0.08607189 0.10545534 0.12919912 0.15891542
     0.19710444 0.24785046 0.31827969 0.42194498 0.58784314 0.88957933
     1.57247134 4.        ]

Find the consumption function using the improved grid and exogenous gridpoints, and plot against the earlier versions.

```python
cVecBot = [0.0]
mVecBot = [self_a_min]  # Use the self-imposed a-min value.

for a in aVec_eee:
    c = gothic.C_Tminus1(a)
    m = c + a
    cVecBot.append(c)
    mVecBot.append(m)

print('a grid:',aVec_eee)
```

    a grid: [0.00544505 0.01151539 0.01832306 0.02600821 0.03474848 0.04477235
     0.05637847 0.06996458 0.08607189 0.10545534 0.12919912 0.15891542
     0.19710444 0.24785046 0.31827969 0.42194498 0.58784314 0.88957933
     1.57247134 4.        ]

We can see that the endogenous gridpoints of $m$ naturally "bunch" near the area with the most curvature.

It allows a better characterization of the consumption and marginal values of at small values of $a$ (See Figure 12 and 13).

```python
###################
## Figure 12
###################

plt.plot(mVec_fine,cVec,'k-')
plt.plot(mVecBot, cVecBot,'*')
plt.xlim(2*self_a_min,4.0)
plt.ylim(-0.1,3.0)
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'${c}_{T-1}(m_{T-1})$')
plt.title(r'$c_{T-1}(m_{T-1})$ (solid) versus $\grave c_{T-1}(m_{T-1})$' +
          '\n using EGM and EEE grid of $a$ (star)')
```

```python
##################################
## Figure 13
#################################

vpVec_eee = [gothic.VP_Tminus1(a) for a in aVec_eee]

plt.plot(aVec_fine, vpVec_fine, 'k-')
plt.plot(aVec_eee, vpVec_eee,'*')
plt.xlabel(r'$a_{T-1}$')
plt.ylabel(r'$\mathfrak{v}^{\prime}_{T-1}$')
plt.title(r'$\mathfrak{v}^{\prime}_{T-1}(a_{T-1})$ (solid) versus $\grave \grave \mathfrak{v}^{\prime}(a_{T-1})$'+
          '\n using EGM with EEE grid of a (star)')
```

## 10. Artifical Borrowing Constraint

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Imposing-Artificial-Borrowing-Constraints)

Some applications assume an externally imposed borrowing constraint. For instance, when the external borrowing constraint is exactly zero, it is binding before the self-imposed borrowing constraint takes effect.

This can be easily taken care of by replacing the first point in the m grid with 0 instead of a self-imposed borrowing constraint.

```python
## set the bool for constrained to be TRUE

constrained = True

# Create initial consumption function:
cVec_const = [0.0]
mVec_const= [self_a_min]

## now the a_min depends on if artifical borrowing constraint is tighter than the natural one
if constrained and self_a_min<0:
    mVec_const=[0.0]
for a in aVec_fine:
    c = gothic.C_Tminus1(a)
    m = c + a
    if constrained:
        c = np.minimum(c, m)
    cVec_const.append(c)
    mVec_const.append(m)

```

```python
## set the bool for constrained to be FALSE

constrained = False

# Create initial consumption function:
cVec_uconst = [0.0]
mVec_uconst= [self_a_min]

## now the a_min depends on if artifical borrowing constraint is tighter than the natural one
if constrained and self_a_min<0:
    mVec_const=[0.0]
for a in aVec_fine:
    c = gothic.C_Tminus1(a)
    m = c + a
    if constrained:
        c = np.minimum(c, m)
    cVec_uconst.append(c)
    mVec_uconst.append(m)
```

Not surprisingly, the main difference between the two c functions lies in the area of negative wealth. (See Figure 18)

```python
#####################
### Figure 18
#####################

plt.plot(mVec_const, cVec_const, 'k-')
plt.plot(mVec_uconst, cVec_uconst, '--')
plt.xlabel(r'$m_{T-1}$')
plt.ylabel(r'$c_{T-1}(m_{T-1})')
plt.ylim(0.0,5.0)
plt.vlines(0.0,0.0,5.0,'k','-.')
plt.title('Constrained (solid) and Unconstrained Consumption (dashed)')
```

## 11. Solving for $c_t(m_t)$ in Multiple Periods

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#x1-210006)

We now employ the recursive nature of the problem to solve all periods. Recall that in general,

$$\mathfrak{v}'(a_{t}) = \mathbb{E}_{t}[\beta \mathrm{R} \Gamma^{-\rho} _{t+1} \mathrm{u}' (c _{t+1} (\mathcal{R} _{t+1}a _{t}+\theta _{t+1}))]$$

That is, once we have $c _{t+1} (\mathcal{R} _{t+1}a _{t}+\theta _{t+1})$ in hand, we can solve backwards for the next period, and so on back to the first period.

As with $c_{T-1}$, we will employ the first-order condition

$$u'(c_{t}) = \mathfrak{v}'(m_{t}-c_{t}) = \mathfrak{v}'(a_{t})$$

to obtain our consumption function from $\mathfrak{v}^{'}_{t}(a_t)$.

To get smoothness, we will make a very large "EEE" grid.

We will also use Python's "time" module to time the whole procedure as it executes.

```python
from time import time

T = 60                              # How many periods/consumption functions?
aVec_eee = setup_grids_expMult(a_min, a_max, 40)    # Create a bigger grid, for smoother curve.

###########################################################
self_a_min_life = T*[self_a_min]  ### need to correct!!!! every period has different value
###########################################################
```

```python
## to make the solution simpler for life cycle, i.e. no need to update self_a_min every period
constrained = True

##########################################################
# Create initial consumption function for the second to the last period
#########################################################

cVec = [0.0]
mVec = [self_a_min]
if constrained and self_a_min<0:
    mVec=[0.0]

for a in aVec_eee:
    c = gothic.C_Tminus1(a)
    m = c + a
    if constrained:
        c = np.minimum(c, m)
    cVec.append(c)
    mVec.append(m)

# Set up the interpolation:
cFunc = InterpolatedUnivariateSpline(mVec, cVec, k=1)

## save it in a dictionary
cFunc_life ={T-1: cFunc}

########################################
## backward iteration over life cycle
########################################

# Loop for all consumption functions in our range:
for t in range(T-2, -1, -1):
    cVec = [0.0]
    mVec = [self_a_min]
    if constrained and self_a_min<0:
        mVec=[0.0]
    for a in aVec_eee:
        c = gothic.C_t(a, cFunc) ## notice here the c func from previous period is the input !!!
        m = c + a
        if constrained:
            c = np.minimum(c, m)
        cVec.append(c)
        mVec.append(m)

    # Update the consumption function
    cFunc = InterpolatedUnivariateSpline(mVec, cVec, k=1)

    # Save updated function:
    cFunc_life[t] = cFunc

```

    Time taken, in seconds: 0.23591923713684082

```python
##############################
#### Figure 19
##############################

for t in range(T-1, T-10, -1):
    cFunc = cFunc_life[t]
    cVec_fine = cFunc(mVec_fine)
    plt.plot(mVec_fine,cVec_fine,'k-')

plt.xlabel(r'$m$')
plt.ylabel(r'$\grave c_{T-n}(m)$')
plt.title(r'Convergence of $\grave c_{T-n}(m)$ Functions as $n$ Increases')
```

The consumption functions converge as the horizon extends.

## 12. Multiple Control Variables (MC)

- See more detailed discussion [here](https://llorracc.github.io/SolvingMicroDSOPs/#Multiple-Control-Variables)

Besides consumption, the new control variable that the consumer can now choose is the portion of the portfolio $\varsigma_t$ to invest in risky assets with a return factor $\mathbf{R}_{t+1}$.  The overall return on the consumer’s portfolio between $t$ and $t + 1$, $\pmb{\mathfrak{R}}_t$, is equal to the following.

\begin{equation}
\pmb{\mathfrak{R}}_t = R + (\mathbf{R}_{t+1}-R) \varsigma_t
\end{equation}

Now, $\mathfrak{v}_t$ is a function of both $a_{t}$ and the risky asset share $\varsigma_t$.
We also need to define $\mathfrak{v}^{a}$ and $\mathfrak{v}^{\varsigma}$, the expected marginal value from saving and risky share, respectively.

We can solve the problem sequentially in two separate stages.

 - At the first stage, solve the optimizing share $\varsigma^*$ for a vector of predetermined $a$ relying on the FOC condition associated with $\mathfrak{v}^{\varsigma}$.
 - At the second stage, use optimal $\varsigma^*$ to construct $\pmb{\mathfrak{R}}$ and solve the consumption exactly in a similar way as before with only one single choice variable.

Now we need to redefine and add additional Gothic functions for the portfolio choice problem. In the meantime, some elements of the class remain the same. One easy and consistent way of achiving this end is to ''inherit'' the existing Gothic class and superimpose some of the functions with modified ones.

```python
## MC stands for multiple controls

## Now the GothicMC takes Distribution as a new input, which
## is a class of two discretized log normal distributions, for income and risky asset return

class GothicMC(Gothic):  ## inheriting from Gothic class
    ## inheritance from Gothic and adds additional functions/methods for portfolio choice
    def __init__(self, u, beta, rho, Gamma, R, Distribution, Income = None, Return = None,variable_variance=False):
        super().__init__(u, beta, rho, Gamma, R, Income, variable_variance=False)
        ## super(). here initialize the GothicMC class as Gothic does
        self.Return = Return
        self.Distribution = Distribution ## a class instance of two log normal distributions
        ## x1 is income and x2 is asset return
        ### additional discretized risky asset returns
        self.varsigma_grids = np.linspace(0.0,1.0,300)

    def varsigma_Tminus1(self,a):
        ## for a given a, compute the optimal risky share

        if a < 0:
            varsigma_opt = 0.0
        else:
            foc_condition = lambda varsigma: self.Vsigma_Tminus1(a,varsigma)

            # lower and upper bound
            lb = 0.0
            ub = 1.0

            if foc_condition(ub)>0:
                #print('constrained from the top')
                varsigma_opt = 1.0
            elif foc_condition(lb)<0:
                #print('constrained from the bottom')
                varsigma_opt = 0.0
            else:
                #print('root-finding')
                varsigma_opt = brentq(foc_condition,0.0, 1.0)
                varsigma_opt = min(varsigma_opt,1.0)
                varsigma_opt = max(varsigma_opt,0.0)

        return varsigma_opt

    def varsigma_t(self,a,c_prime):

        if a < 0:
            varsigma_opt = 0.0
        else:
            foc_condition = lambda varsigma: self.Vsigma_t(a,varsigma,c_prime)
            # lower and upper bound
            lb = 0.0
            ub = 1.0

            if foc_condition(ub)> 0:
                #print('constrained from the top')
                varsigma_opt = 1.0
            elif foc_condition(lb)<0:
                #print('constrained from the bottom')
                varsigma_opt = 0.0
            else:
                #print('root-finding')
                varsigma_opt = brentq(foc_condition,0.0, 1.0)
                #print(varsigma_opt)
                varsigma_opt = min(varsigma_opt,1.0)
                varsigma_opt = max(varsigma_opt,0.0)

        return varsigma_opt

    def C_Tminus1(self,a):
        return self.Va_Tminus1(a)**(-1.0/self.rho)

    def C_t(self,a,c_prime):
        return self.Va_t(a,c_prime)**(-1.0/self.rho)

    def Va_Tminus1(self,a):

        """
        gothic v's first derivative with respect to the a given optimal share varsigma_opt at a
        """
        varsigma_opt = self.varsigma_Tminus1(a)
        ## the g function below computes the gothic v' for a particular realization of risky return and income shock
        g = lambda inc_shk,rreturn: (self.R+(rreturn-self.R)*varsigma_opt)*self.u.prime((self.R+(rreturn-self.R)*varsigma_opt)* a/self.Gamma[-1]  + inc_shk)
        ## the Distribution.E() calculates the expectation of g values over the joint distribution of return and income shock
        GVTm1Pa = self.beta * self.R* self.Gamma_to_minusRho[-1] * self.Distribution.E(g)
        return GVTm1Pa

    def Va_t(self,a,c_prime):
        varsigma_opt = self.varsigma_t(a,c_prime)
        g = lambda inc_shk,rreturn: (self.R+(rreturn-self.R)*varsigma_opt)*self.u.prime(c_prime((self.R+(rreturn-self.R)*varsigma_opt)*a/self.Gamma[-1] + inc_shk))
        GVtPa = self.beta * self.R *self.Gamma_to_minusRho[-1] * self.Distribution.E(g)
        return GVtPa

    def Vsigma_Tminus1(self,a,varsigma):
        """
        gothic v's first derivative with respect to the portfolio share varsigma in the last period
        """
        gg = lambda inc_shk, rreturn: (rreturn-self.R)*self.u.prime((self.R+(rreturn-self.R)*varsigma)* a/self.Gamma[-1]  + inc_shk)
        GVTm1Psigma = self.beta *self.Distribution.E(gg)
        return GVTm1Psigma

    def Vsigma_t(self,a,varsigma,c_prime):
        """
        gothic v's first derivative with respect to the portfolio share varsigma in all earlier periods
        """
        gg = lambda inc_shk, rreturn: (rreturn-self.R)*self.u.prime(c_prime((self.R+(rreturn-self.R)*varsigma)*a/self.Gamma[-1] + inc_shk))
        GVtPsigma =  self.beta*self.Distribution.E(gg)
        return GVtPsigma
```

```python
## set parameters for risky asset returns, i,e, make sure there is positive excessive return
## otherwise, the problem will be trivial
## discretized log-normal asset returns

theta_sigma_port = 0.15
theta_mu_port = -0.5*(theta_sigma_port**2)
theta_z_port = stats.lognorm(theta_sigma_port, 0, np.exp(theta_mu_port))    # Create "frozen" distribution instance

RiskyR_grid_N = 7
RiskyR_sigma = 0.15 ## standard deviation of risky asset return
r = np.log(R)
phi = 0.02 ## excess return from risky asset
RiskyR_mu = phi+r -0.5*(RiskyR_sigma**2)
RiskyR_z = stats.lognorm(RiskyR_sigma, 0, np.exp(RiskyR_mu))    # Create "frozen" distribution instance

Distribution = DiscreteApproximationTwoIndependentDistribs(theta_grid_N,
                                                           theta_z_port.cdf,
                                                           theta_z_port.pdf,
                                                           theta_z_port.ppf,
                                                           RiskyR_grid_N,
                                                           RiskyR_z.cdf,
                                                           RiskyR_z.pdf,
                                                           RiskyR_z.ppf
                                                          )

```

```python
## Solve the portfolio choice problem in the second-to-the-last period

t_start = time()

#######################################################
## new parameterization for the portfolio problems
#######################################################

## higher risk aversion coefficient
rho_port = 6.0
u_port = Utility(gamma=rho_port)

### create a GothicMC instance
## notice we use a bigger coefficient of risk aversion
gothicMC = GothicMC(u_port, beta, rho_port, gamma, R, Distribution = Distribution)

### set the a grid

aVec_eee_big = setup_grids_expMult(a_min, 100.0, 800)

#################################
### the last period
###############################

cVec_port = [0.0]
mVec_port = [0.0]
varsigmaVec_port = [0.0]

#print('period'+str(T))
for a in aVec_eee_big:
    varsigma = gothicMC.varsigma_Tminus1(a) ## optimal share for a
    #print(varsigma)
    c = gothicMC.C_Tminus1(a)     ## optimal c given varsigma being optimal
    m = c + a
    c = np.minimum(c, m)
    cVec_port.append(c)
    mVec_port.append(m)
    varsigmaVec_port.append(varsigma)

# Set up the interpolation:
cFunc = InterpolatedUnivariateSpline(mVec_port, cVec_port, k=1)
varsigmaFunc = InterpolatedUnivariateSpline(mVec_port[1:], varsigmaVec_port[1:], k=1)

## save the grid in a dictionary
mGridPort_life = {T-1:mVec_port}
cGridPort_life = {T-1:cVec_port}
varsigmaGrid_life = {T-1:varsigmaVec_port}

## save the interpolated function in a dictionary
cFuncPort_life = {T-1: cFunc}
varsigma_life = {T-1:varsigmaFunc}

#################################
### backward to earlier periods
###############################

for t in range(T-2, 0, -1):
    #print('period'+str(t))
    cVec_port = [0.0]
    mVec_port = [0.0]
    varsigmaVec_port = [0.0]
    for a in aVec_eee_big:
        #print(a)
        varsigma = gothicMC.varsigma_t(a,cFunc) ## optimal share for a
        #print(varsigma)
        c = gothicMC.C_t(a,cFunc)     ## optimal c given varsigma being optimal
        m = c + a
        c = np.minimum(c, m)
        cVec_port.append(c)
        mVec_port.append(m)
        varsigmaVec_port.append(varsigma)

    # Update the consumption function and share function
    cFunc = InterpolatedUnivariateSpline(mVec_port, cVec_port, k=1)
    varsigmaFunc = InterpolatedUnivariateSpline(mVec_port[1:], varsigmaVec_port[1:], k=1)

    ## save the policy grid in a dictionary
    mGridPort_life[t] = mVec_port
    cGridPort_life[t] = cVec_port
    varsigmaGrid_life[t] = varsigmaVec_port

    # save interpolated function:
    cFuncPort_life[t] = cFunc
    varsigma_life[t]= varsigmaFunc

t_finish = time()

print("Time taken, in seconds: "+ str(t_finish - t_start))
```

    Time taken, in seconds: 6.693940877914429

Figure 20 plots the ﬁrst-period consumption function generated by the program; qualitatively it does not look much diﬀerent from the consumption functions generated by the program without portfolio choice.

```python
############################
## Figure 20
#############################
cFunc = cFuncPort_life[1]
plt.plot(mVec, cFunc(mVec), 'k-')
plt.xlabel(r'$m$')
plt.ylabel(r'$c_{1}(m)$')
plt.title(r'$c_{1}(m)$ with Portfolio Choice')
```

Figure 21 plots $\varsigma_1(a_{1})$, the optimal portfolio share as a function of the end-of-period asset $a$ in the first period.

- First, even with a coeﬃcient of relative risk aversion of 6, an equity premium of only 4 percent, and an annual standard deviation in equity returns of 15 percent, the optimal choice is for the agent to invest a proportion 1 (100 percent) of the portfolio in stocks (instead of the safe bank account with riskless return R  ) is at values of at  less than about 2.

- Second, the proportion of the portfolio kept in stocks is declining in the level of wealth - i.e., the poor should hold all of their meager assets in stocks, while the rich should be cautious, holding more of their wealth in safe bank deposits and less in stocks. This seemingly bizarre (and highly counterfactual) prediction reﬂects the nature of the risks the consumer faces. Those consumers who are poor in measured ﬁnancial wealth are likely to derive a high proportion of future consumption from their labor income. Since by assumption labor income risk is uncorrelated with rate-of-return risk, the covariance between their future consumption and future stock returns is relatively low. By contrast, persons with relatively large wealth will be paying for a large proportion of future consumption out of that wealth, and hence if they invest too much of it in stocks their consumption will have a high covariance with stock returns. Consequently, they reduce that correlation by holding some of their wealth in the riskless form.

```python
#####################################
## Figure 21
####################################
varsigmaGrid = varsigmaGrid_life[1][1:]
## drop the first point (share for zero asset corner solution)
plt.plot(aVec_eee_big,varsigmaGrid, 'k-')
plt.xlabel(r'$a$')
plt.ylabel(r'$\varsigma_{1}(a)$')
plt.title(r'Portfolio Share in Risky Assets in the First Period $\varsigma(a)$')
```

### Cross-validation of the results

We compare the MicroDSOP solution with solutions using the HARK toolkit's PortfolioConsumer class. We configure the HARK class with exactly the same parameters used above.

```python
# Solve the agent over a standard life-cycle

init_life_cycle_new = copy(init_lifecycle)
init_life_cycle_new['T_cycle'] = T-1   ## minus 1 because T_cycle is nb periods in a life cycle - 1 in HARK
init_life_cycle_new['CRRA'] = rho_port
init_life_cycle_new['Rfree'] = R
init_life_cycle_new['LivPrb'] = [1.0]*init_life_cycle_new['T_cycle']
init_life_cycle_new['PermGroFac'] = [1.0]*init_life_cycle_new['T_cycle']
init_life_cycle_new['PermShkStd'] = [0.0]*init_life_cycle_new['T_cycle']
init_life_cycle_new['TranShkStd'] = [theta_sigma]*init_life_cycle_new['T_cycle']
init_life_cycle_new['UnempPrb'] = 0.0
init_life_cycle_new['RiskyAvg'] = [R+phi]*init_life_cycle_new['T_cycle'] ## phi is risk premium
init_life_cycle_new['RiskyStd'] = [RiskyR_sigma]*init_life_cycle_new['T_cycle']
init_life_cycle_new['RiskyAvgTrue'] = R+phi
init_life_cycle_new['RiskyStdTrue'] = RiskyR_sigma
init_life_cycle_new['DiscFac'] = beta
init_life_cycle_new['PermGroFacAgg'] = 1.0

LifeCycleType =  PortfolioConsumerType(**init_life_cycle_new)

LifeCycleType.cycles = 1 ## life cycle problem instead of infinite horizon
LifeCycleType.vFuncBool = False  ## no need to calculate the value for the purpose here
```

```python
## solving the model
t0 = time()
LifeCycleType.solve()
LifeCycleType.cFunc = [LifeCycleType.solution[t].cFuncAdj for t in range(LifeCycleType.T_cycle)]
LifeCycleType.ShareFunc = [LifeCycleType.solution[t].ShareFuncAdj for t in range(LifeCycleType.T_cycle)]
t1 = time()
print('Solving a ' + str(LifeCycleType.T_cycle) + ' period portfolio choice problem takes ' + str(t1-t0) + ' seconds.')
```

    Solving a 59 period portfolio choice problem takes 0.44767093658447266 seconds.

```python
## compare the consumption function for the first period
which_period = T-1
mGrid = mGridPort_life[which_period]
plt.plot(mGrid,LifeCycleType.cFunc[which_period-1](mGrid),'r--',label='HARK')
plt.plot(mGrid, cFuncPort_life[which_period](mGrid), 'k-',label='MicroDSOP')
plt.legend(loc=0)
plt.title('consumption function solved by MicroDSOP and HARK')
plt.xlabel('m')
plt.ylabel(r'$c_{T-1}(m)$')
```

```python
## Compare the solutions

which_period = 1
mGrid = mGrid
plt.plot(mGrid,LifeCycleType.ShareFunc[which_period-1](mGrid),'r--',label='HARK solution')
plt.plot(mGrid,varsigma_life[which_period](mGrid),'k-',label='MicroDSOP solution')
plt.legend(loc=3)
plt.title('share function solved by MicroDSOP and HARK')
plt.xlabel(r'$m$')
plt.ylabel(r'$\varsigma_{T-1}(m)$')
plt.legend(loc=1)
```


# Flipping USB Connectors

*If you like this article, you might also like the second edition of [*Think Bayes*](https://greenteapress.com/wp/think-bayes/).*

[Click here to run this article on Colab](https://colab.research.google.com/github/AllenDowney/ThinkBayes2/blob/master/examples/usb.ipynb)

I am not the first person to observe that it sometimes takes several tries to plug in a USB connector (specifically the rectangular [Type A connector](https://en.wikipedia.org/wiki/USB), which is not reversible).
There are [memes about it](https://knowyourmeme.com/memes/usb-insertion-issues), there are [cartoons about it](https://www.smbc-comics.com/?id=2388), and on Quora alone,
[people](https://www.quora.com/Why-do-you-always-have-to-flip-a-USB-twice-when-trying-to-plug-it-in-Shouldn-t-it-have-gone-in-the-first-time-when-you-had-it-the-right-way)
[have](https://www.quora.com/There-is-a-50-chance-of-plugging-in-a-USB-cable-the-right-way-on-the-first-try-Why-does-it-only-happen-20-of-the-time-when-I-do-it)
[asked](https://www.quora.com/Why-doesnt-the-USB-go-in-the-first-time-but-does-after-youve-turn-it-a-few-times)
[about](https://www.quora.com/People-that-can-plug-the-USB-in-on-their-first-try-How)
[it](https://www.quora.com/Why-cant-we-plug-in-USB-devices-correctly-the-first-time) more than once.

But I might be the first to use Bayesian decision analysis to figure out the optimal strategy for plugging in a USB connector. Specifically, I have worked out how long you should try on the first side before flipping, how long you should try on the second side before flipping again, how long you should try on the third side, and so on.

Of course, my analysis is based on some modeling assumptions:

1. Initially, the probability is 0.5 that the connector is in the right orientation.

2. If it is, the time it takes to succeed follows an exponential distribution with a mean of 1.1 seconds.

3. Flipping the connector takes 0.1 seconds.

With that, we are ready to get started.

## Continuous Updates

The first step is to figure out the probability that the connector is in the right orientation as a function of how long you have been trying.
For that, we can use a Bayes table, which is a form of Bayes's Theorem I use in [Chapter 2 of *Think Bayes*](https://allendowney.github.io/ThinkBayes2/chap02.html#bayes-tables).

The following function takes a sequence of hypotheses, prior probabilities, and likelihoods, and returns a pandas `DataFrame` that represents a Bayes table.

```python
import pandas as pd

def bayes_table(hypos, prior, likelihood):
    """Make a table showing a Bayesian update."""
    table = pd.DataFrame(dict(prior=prior, likelihood=likelihood), index=hypos)
    table['unnorm'] = table['prior'] * table['likelihood']
    prob_data = table['unnorm'].sum()
    table['posterior'] = table['unnorm'] / prob_data
    return table
```

Now suppose that the prior probability is 0.5 that the orientation of the connector is correct, and you have been trying for 0.9 seconds.
What is the likelihood that you would have to try so long?

* If you are on the wrong side, it is 100%.

* If you are on the right side, it's given by the survival function (complementary CDF) of the exponential distribution, which is $\exp(-\lambda t)$, where $\lambda$ is the rate parameter and $t$ is time.

The following function computes this likelihood:

```python
import numpy as np

def expo_sf(t, lam):
    """Survival function of the exponential distribution."""
    return np.exp(-lam * t)
```

We can use this function to compute the likelihood of trying for 0.9 seconds or more, given an exponential distribution with mean 1.1.

```python
t = 0.9
mu = 1.1
lam = 1/mu

expo_sf(t, lam)
```

    0.441233167759984

The result is the likelihood of the data, given that the orientation of the connector is correct.

Now let's make a Bayes table with two hypotheses -- the connector is either the right way or the wrong way -- with equal prior probabilities.

```python
hypos = ['Right way', 'Wrong way']
prior = [1/2, 1/2]
```

And here is the likelihood of the data for each hypothesis:

```python
likelihood = [expo_sf(t, lam), 1]
```

Putting it together, here's the Bayes table.

```python
bayes_table(hypos, prior, likelihood)
```

      prior
      likelihood
      unnorm
      posterior

      Right way
      0.5
      0.441233
      0.220617
      0.30615

      Wrong way
      0.5
      1.000000
      0.500000
      0.69385

After 0.9 seconds, the probability is about 69% that the orientation of the connector is wrong, so you might want to think about trying the other side.

But if it takes 0.1 seconds to flip, maybe you should keep trying a little longer. To figure out when to flip, let's do the same analysis again for general values of $\lambda$ and $t$.

## Generalization

To minimize human error, I'll use Sympy to do the algebra. Here are the symbols I'll use.

```python
from sympy import symbols, exp

t, lam, p, q, r = symbols('t lam p q r')
```

Here's the likelihood again, using the symbols.

```python
likelihood = [exp(-lam * t), 1]
likelihood
```

    [exp(-lam*t), 1]

And here's the Bayes table, using $p$ and $q$ for the prior probabilities of the hypotheses.

```python
prior = [p, q]
table = bayes_table(hypos, prior, likelihood)
table
```

      prior
      likelihood
      unnorm
      posterior

      Right way
      p
      exp(-lam*t)
      p*exp(-lam*t)
      p*exp(-lam*t)/(p*exp(-lam*t) + q)

      Wrong way
      q
      1
      q
      q/(p*exp(-lam*t) + q)

From the table I'll select the posterior probability that the orientation is correct.

```python
expr = table.loc['Right way', 'posterior']
expr.simplify()
```

$\displaystyle \frac{p}{p + q e^{lam t}}$

You might recognize this as a form of the [logistic function](https://en.wikipedia.org/wiki/Logistic_function); we can compute it like this:

```python
def logistic(p, lam, t):
    q = 1-p
    return p / (p + q * np.exp(lam * t))
```

Let's see what that looks like for a range of values of `t`, assuming that the prior probability is `p=0.5`.

```python
import matplotlib.pyplot as plt

ts = np.linspace(0, 4)
ps = logistic(p=0.5, lam=1/mu, t=ts)

plt.plot(ts, ps)
plt.xlabel("How long you've been trying (seconds)")
plt.ylabel("Probability the orientation is right");
```

After a few seconds of fiddling, you should be reasonably convinced that the orientation is wrong.

## Strategy

Now, let's think about turning belief into action. Let me start with a conjecture: I suspect that the best strategy is to try on the first side until the probability of correct orientation drops below some threshold (to be determined), then try on the second side until the probability drops below that threshold again, and repeat until success.

To test this strategy, we will have to figure out how long to try as a function of the prior probability, `p`, and the threshold probability, `r`. Again, I'll make Sympy do the work.

Here's the equation that sets the posterior probability, which we computed in the previous section, to `r`.

```python
from sympy import Eq, solve

eqn = Eq(expr, r)
eqn
```

$\displaystyle \frac{p e^{- lam t}}{p e^{- lam t} + q} = r$

And here's the solution for `t` in terms of `p`, `q`, `r`, and `lam`.

```python
solve(eqn, t)[0]
```

$\displaystyle \frac{\log{\left(\frac{p \left(1 - r\right)}{q r} \right)}}{lam}$

And here's how we can express this solution in terms of the prior and posterior odds.

```python
def wait_time(p, lam, r):
    q = 1-p
    prior_odds = p / q
    posterior_odds = r / (1-r)
    return np.log(prior_odds / posterior_odds) / lam
```

Let's see what that looks like for a range of values of `r`, assuming that the prior probability is `p=0.5`.

```python
rs = np.linspace(0.05, 0.5)
ts = wait_time(p=0.5, lam=1/mu, r=rs)

plt.plot(rs, ts, color='C2')
plt.xlabel("Probability the orientation is right")
plt.ylabel("How long to keep trying (seconds)");
```

When the threshold is low, we have to wait a few seconds to reach it. As the threshold increases, the time to reach it decreases.
We'll use this function in the next section to simulate the strategy.

## Simulation

As a step toward optimization, let's run a simulation. The following function takes as parameters:

* `correct`: A Boolean indicating if the orientation is correct.
* `p`: The prior probability that the orientation is correct.
* `lam`: The rate parameter for the distribution of time until success.
* `r`: The threshold for the posterior probability.
* `flip`: The time it takes to flip the connector, in seconds.
* `trace`: A list that indicates how much time we have spent, so far, trying and flipping.

It runs the simulation and returns a sequence of waiting and flipping times. The sum of this sequence is the total time it took to connect. And we can use the length of the sequence to figure out how many times we had to flip.

```python
def simulate(correct, p, lam, r, flip, trace):
    # figure out the maximum time we should try before flipping
    wait = wait_time(p, lam, r)

    # if we're on the correct side, see if we succeed before time's up
    if correct:
        t = np.random.exponential(1/lam)
        if t < wait:
            # if so, update and return the trace
            return trace + [t]

    # if time expired, add the wait time and flip time to the trace
    # and make a recursive call to continue the simulation
    return simulate(not correct, 1-r, lam, r, flip, trace + [wait, flip])
```

Here's a test run, starting on the correct side.

```python
simulate(correct=True, p=0.5, lam=1/mu, r=0.2, flip=0.1, trace=[])
```

    [1.5249237972318797, 0.1, 3.0498475944637597, 0.1, 0.22920261920526902]

And here's a run where we start on the wrong side.

```python
simulate(correct=False, p=0.5, lam=1/mu, r=0.2, flip=0.1, trace=[])
```

    [1.5249237972318797, 0.1, 0.8744612650971085]

The following function runs the simulation many times with initial probability `p=0.5`, starting in the right orientation half the time.

It returns two arrays, containing the length of the trace and the total duration for each simulation.

```python
def run_simulations(lam, r, flip, iters=20000, flag=None):
    res = []
    for i in range(iters):
        correct = i%2 if flag is None else flag
        trace = simulate(correct, 0.5, lam, r, flip, [])
        res.append((len(trace), sum(trace)))

    return np.transpose(res)
```

Here's the average total duration with threshold probability `r=0.25`.

```python
lengths, totals = run_simulations(lam=1/mu, r=0.25, flip=0.1)
totals.mean()
```

    2.403304555631497

With this threshold, it takes about 2 seconds to connect, on average.

## Optimization

Now let's see how the average duration varies as we sweep through a range of values for the threshold probability, `r`:

```python
rs = np.linspace(0.15, 0.4, 21)
rs
```

    array([0.15  , 0.1625, 0.175 , 0.1875, 0.2   , 0.2125, 0.225 , 0.2375,
           0.25  , 0.2625, 0.275 , 0.2875, 0.3   , 0.3125, 0.325 , 0.3375,
           0.35  , 0.3625, 0.375 , 0.3875, 0.4   ])

```python
np.random.seed(17)

res = []
for r in rs:
    lengths, totals = run_simulations(lam=1/mu, r=r, flip=0.1)
    res.append((r, totals.mean()))
```

```python
from statsmodels.nonparametric.smoothers_lowess import lowess

def make_lowess(series):
    """Use LOWESS to compute a smooth line.

    series: pd.Series

    returns: pd.Series
    """
    endog = series.values
    exog = series.index.values

    smooth = lowess(endog, exog)
    index, data = np.transpose(smooth)

    return pd.Series(data, index=index)
```

```python
def plot_series_lowess(series, color):
    """Plots a series of data points and a smooth line.

    series: pd.Series
    color: string or tuple
    """
    series.plot(lw=0, marker='o', color=color, alpha=0.5)
    smooth = make_lowess(series)
    smooth.plot(label='_', color=color)
```

Here's what the results look like.

```python
rs, ts = np.transpose(res)
series = pd.Series(ts, rs)

plot_series_lowess(series, 'C1')

plt.xlabel("Threshold probability where you flip (r)")
plt.ylabel("Average total duration (seconds)");
```

The optimal value of `r` is close to `0.3`. With that threshold we can see how long we should try on the first side, starting with prior probability `p=0.5`.

```python
r_opt = 0.3
wait_time(p=0.5, lam=1/mu, r=r_opt)
```

    0.9320276464259238

With the given values of `lam` and `flip`, it turns out the optimal time to wait is about `0.9` seconds.

If we have to flip, the prior probability for the second side is `p=1-r`, so we have to wait twice as long for the posterior probability to get down to `r`.

```python
wait_time(p=1-r_opt, lam=1/mu, r=r_opt)
```

    1.864055292851848

## How many flips?

Now let's run the simulations with the optimal value of `r` and see what the distributions look like for the total time and the number of flips.

```python
lengths1, totals1 = run_simulations(lam=1/mu, r=r_opt, flip=0.1, flag=True)
lengths2, totals2 = run_simulations(lam=1/mu, r=r_opt, flip=0.1, flag=False)
```

Here's the distribution of total time, represented as a CDF.

```python
try:
    import empiricaldist
except ImportError:
    !pip install empiricaldist
```

```python
from empiricaldist import Cdf

Cdf.from_seq(totals1).plot(lw=2, label='Right the first time')
Cdf.from_seq(totals2).plot(lw=2, label='Wrong the first time')

plt.xlabel('Total time to connect (seconds)')
plt.ylabel('CDF')
plt.title('Distribution of total time to connect')
plt.legend();
```

```python
totals1.mean(), totals2.mean()
```

    (2.2006236558767154, 2.616228241925388)

```python
np.append(totals1, totals2).mean()
```

    2.4084259489010518

The average is about 2.4 seconds, but occasionally it takes much longer!

And here's the distribution for the total number of flips.

```python
from empiricaldist import Pmf

flips1 = (lengths1-1) // 2
pmf1 = Pmf.from_seq(flips1) / 2
pmf1.bar(alpha=0.7, label='Right the first time')

flips2 = (lengths2-1) // 2
pmf2 = Pmf.from_seq(flips2) / 2
pmf2.bar(alpha=0.7, label='Right the second time')

plt.xlabel('How many times you have to flip')
plt.ylabel('PMF')
plt.title('Distribution of number of flips')
plt.legend();
```

```python
lengths = np.append(lengths1, lengths2)
flips = (lengths-1) // 2
Pmf.from_seq(flips).head(5)
```

      probs

      0.0
      0.282925

      1.0
      0.407050

      2.0
      0.177200

      3.0
      0.075200

      4.0
      0.032575

The probability of getting it right on the first try is only about 28%. That might seem low, because the chance of starting in the right orientation is 50%, but remember that we have a substantial chance of flipping even if we start in the right orientation (and in that case, we have to flip at least twice).

The most common outcome is that we have to flip once, about 40% of the time. And the probability of the notorious double flip is about 18%.

Fortunately, it is rare to flip three or more times.

## Summary

With that, I think we have solved the USB connector problem.

1. For given parameters `lam` and `flip`, we can find the threshold probability, `r`, that minimizes the average time to connect.

2. Given this optimal value, we can estimate the distribution of total time and the number of times we have to flip.

Sadly, all of this fun is gradually being spoiled by the encroachment of the [USB-C](https://en.wikipedia.org/wiki/USB-C) connector, which is reversible.

*If you like this article, you might also like the second edition of [*Think Bayes*](https://greenteapress.com/wp/think-bayes/).*

Copyright 2021 Allen Downey

Code: [MIT License](https://opensource.org/licenses/MIT)

Text: [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/)

```python

```


```python
from IPython.display import HTML

hide_me = ''
HTML('''
<form action="javascript:code_toggle()"><input style="opacity:0" type="submit" value="Click here to toggle on/off the raw code."></form>''')
```

## Exercise

Solve the following equation using addition.

$$x + (-6) = 5$$

```python
from sympy import symbols, Eq, simplify

def ex():
    x = symbols('x')

    eq1 = Eq(x + (-6), 5)

    # YOUR CODE HERE
    raise NotImplementedError()

    return(eq1)

ex()  # Test out results
```

```python
# hide_me
# Unit tests
import inspect

assert 'ex' in locals(), 'Please keep the function name as `ex`'
assert (simplify(ex()) == Eq(symbols('x'), 11)), 'The final answer for x is incorrect'
assert '.lhs' in inspect.getsource(ex), 'You should use the lhs method to modify the left of the equation'
assert '.lhs' in inspect.getsource(ex), 'You should use the rhs method to modify the right of the equation'
assert 'solve' not in inspect.getsource(ex), 'Do not use the `solve()` function to get the answer'
assert type(ex()) == Eq, 'Your function should return an SymPy equation'

print("Great job!")
```

```python

```


# Regressão Logística

A regressão logística é uma técnica estatística que tem como objetivo produzir, a partir de um conjunto de observações, um modelo que permita a predição de valores tomados por uma variável categórica, frequentemente binária, a partir de uma série de variáveis explicativas contínuas e/ou binárias[1][2]

A regressão logística é amplamente usada em ciências médicas e sociais, e tem outras denominações, como modelo logístico, modelo logit, e classificador de máxima entropia. A regressão logística é utilizada em áreas como as seguintes:

- Em medicina, permite por exemplo determinar os factores que caracterizam um grupo de indivíduos doentes em relação a indivíduos sãos.
- No domínio dos seguros, permite encontrar fracções da clientela que sejam sensíveis a determinada política securitária em relação a um dado risco particular.
- Em instituições financeiras, pode detectar os grupos de risco para a subscrição de um crédito.
- Em econometria, permite explicar uma variável discreta, como por exemplo as intenções de voto em atos eleitorais.

O êxito da regressão logística assenta sobretudo nas numerosas ferramentas que permitem interpretar de modo aprofundado os resultados obtidos.

Em comparação com as técnicas conhecidas em regressão, em especial a regressão linear, a regressão logística distingue-se essencialmente pelo facto de a variável resposta ser categórica.

Enquanto método de predição para variáveis categóricas, a regressão logística é comparável às técnicas supervisionadas propostas em aprendizagem automática (árvores de decisão, redes neurais, etc.), ou ainda a análise discriminante preditiva em estatística exploratória. É possível de as colocar em concorrência para escolha do modelo mais adaptado para um certo problema preditivo a resolver.

Trata-se de um modelo de regressão para variáveis dependentes ou de resposta binomialmente distribuídas. É útil para modelar a probabilidade de um evento ocorrer como função de outros factores. É um modelo linear generalizado que usa como função de ligação a função logit.

Calculando a probabilidade de um certo evento booleano ser verdadeiro:

\begin{equation*}
P(y=1|X)= \dfrac{1}{e^{-z}} = P_i
\end{equation*}

Da mesma forma, o mesmo evento tem a seguinte probabilidade de ser falso:

\begin{equation*}
P(y=0|X)= \dfrac{1}{e^{z}} = 1 - P_i
\end{equation*}

Dividindo-se uma equação pela outra:

\begin{equation*}
\dfrac{P_i}{1-P_i} = \dfrac{1}{e^{-z}} * \dfrac{e^{z}}{1}
\end{equation*}

Aplicando o Logatítmo Neperiano (Log de base _e_) dos dois lados da equação:

\begin{equation*}
z = ln(\dfrac{P_i}{1-P_i})
\end{equation*}

# Exemplo de Regressão Logistica

Vamos estudar como analisar as correlações parciais e os impactos causais em variaveis dependentes binárias.

Cerca de um em cada sete adultos dos EUA tem diabetes agora, de acordo com os Centros para Controle e Prevenção de Doenças. Mas até 2050, essa taxa pode disparar para até um em três. Com isto em mente, isso é o que vamos fazer hoje: Aprender a usar o Aprendizado de Máquina para nos ajudar a prever o Diabetes. Vamos começar!

Dataset1: Diabetes
    1. Number of times pregnant
    2. Plasma glucose concentration a 2 hours in an oral glucose tolerance test
    3. Diastolic blood pressure (mm Hg)
    4. Triceps skin fold thickness (mm)
    5. 2-Hour serum insulin (mu U/ml)
    6. Body mass index (weight in kg/(height in m)^2)
    7. Diabetes pedigree function
    8. Age (years)
    9. Class variable (0 or 1)

### Importando as bibliotecas e os dados

```python
import matplotlib.pyplot as plt
import pandas as pd
import statsmodels.api as sm
import numpy as np
from bokeh.plotting import figure, show, output_file
from bokeh.io import output_notebook, push_notebook, show
from ipywidgets import interact
import scipy.special
import statsmodels.formula.api as smf
from scipy import stats

output_notebook()

#read the data in
df = pd.read_csv('../../99 Datasets/diabetes.csv.zip')
```

### Lendo e analisando os dados

```python
df.shape
```

```python
df.head()
```

```python
df.describe()
```

```python
df.corr().round(2)
```

```python
df['class'].value_counts()
```

```python
colormap = {'tested_positive': 'red', 'tested_negative': 'blue'}
colors = [colormap[x] for x in df['class']]

colors[:5]
```

# DICA DO PROFESSOR

Uma opção de biblioteca de vizualização me Python, com a geração de gráficos interativos é o BOKEH.

# BOKEH: Biblioteca para plotagem de gráficos em Python

O Bokeh é uma biblioteca de visualização interativa voltada para os navegadores da Web modernos para apresentação. Seu objetivo é fornecer uma construção elegante e concisa de gráficos versáteis e estender esse recurso com interatividade de alto desempenho em conjuntos de dados muito grandes ou de fluxo contínuo. O Bokeh pode ajudar qualquer pessoa que queira criar de forma rápida e fácil plotagens interativas, painéis e aplicativos de dados.

O Bokeh é um projeto patrocinado fiscalmente do NumFOCUS, uma organização sem fins lucrativos dedicada a apoiar a comunidade de computação científica de código aberto. Se você gosta do Bokeh e quer apoiar nossa missão, por favor, considere fazer uma doação para apoiar nossos esforços.

Para documentação oficial [clique aqui](https://bokeh.pydata.org/en/latest/)

# Exemplos do quanto o bokeh é flexivel e poderoso

- http://bokeh.pydata.org/en/0.10.0/docs/gallery/periodic.html
- https://bokeh.pydata.org/en/latest/docs/gallery.html

```python
import bokeh
bokeh.sampledata.download()
```

```python
x = np.linspace(0,2*np.pi, 2000)
y = np.sin(x)

p = figure(title='teste seno', plot_height=300, plot_width=600, y_range=(-5,5))
r = p.line(x, y, color='#2222aa', line_width=3)

def update(f, w=1, A=1, phi=0):
    if f == 'sin': func = np.sin
    elif f == 'cos': func = np.cos
    elif f == 'tan': func = np.tan
    r.data_source.data['y'] = A * func(w * x + phi)
    push_notebook()
```

```python
show(p, notebook_handle=True)
```

```python
interact(update, f=['sin', 'cos', 'tan'], w=(0,100), A=(1,5), phi=(0,20, 0.1))
```

```python
import pandas as pd

from bokeh.palettes import Spectral4
from bokeh.plotting import figure, output_file, show
from bokeh.sampledata.stocks import AAPL, IBM, MSFT, GOOG

p = figure(plot_width=800, plot_height=250, x_axis_type="datetime")
p.title.text = 'Click on legend entries to mute the corresponding lines'

for data, name, color in zip([AAPL, IBM, MSFT, GOOG], ["AAPL", "IBM", "MSFT", "GOOG"], Spectral4):
    temp = pd.DataFrame(data)
    temp['date'] = pd.to_datetime(temp['date'])
    p.line(temp['date'], temp['close'], line_width=2, color=color, alpha=0.8,
           muted_color=color, muted_alpha=0.2, legend=name)

p.legend.location = "top_left"
p.legend.click_policy="mute"

show(p)
```

```python
import numpy as np
import scipy.special

from bokeh.layouts import gridplot
from bokeh.plotting import figure, show, output_file

def make_plot(title, hist, edges, x, pdf, cdf):
    p = figure(title=title, tools='', background_fill_color="#fafafa")
    p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
           fill_color="navy", line_color="white", alpha=0.5)
    p.line(x, pdf, line_color="#ff8888", line_width=4, alpha=0.7, legend="PDF")
    p.line(x, cdf, line_color="orange", line_width=2, alpha=0.7, legend="CDF")

    p.y_range.start = 0
    p.legend.location = "center_right"
    p.legend.background_fill_color = "#fefefe"
    p.xaxis.axis_label = 'x'
    p.yaxis.axis_label = 'Pr(x)'
    p.grid.grid_line_color="white"
    return p

# Normal Distribution

mu, sigma = 0, 0.5

measured = np.random.normal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(-2, 2, 1000)
pdf = 1/(sigma * np.sqrt(2*np.pi)) * np.exp(-(x-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((x-mu)/np.sqrt(2*sigma**2)))/2

p1 = make_plot("Normal Distribution (μ=0, σ=0.5)", hist, edges, x, pdf, cdf)

# Log-Normal Distribution

mu, sigma = 0, 0.5

measured = np.random.lognormal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 8.0, 1000)
pdf = 1/(x* sigma * np.sqrt(2*np.pi)) * np.exp(-(np.log(x)-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((np.log(x)-mu)/(np.sqrt(2)*sigma)))/2

p2 = make_plot("Log Normal Distribution (μ=0, σ=0.5)", hist, edges, x, pdf, cdf)

# Gamma Distribution

k, theta = 7.5, 1.0

measured = np.random.gamma(k, theta, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 20.0, 1000)
pdf = x**(k-1) * np.exp(-x/theta) / (theta**k * scipy.special.gamma(k))
cdf = scipy.special.gammainc(k, x/theta)

p3 = make_plot("Gamma Distribution (k=7.5, θ=1)", hist, edges, x, pdf, cdf)

# Weibull Distribution

lam, k = 1, 1.25
measured = lam*(-np.log(np.random.uniform(0, 1, 1000)))**(1/k)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 8, 1000)
pdf = (k/lam)*(x/lam)**(k-1) * np.exp(-(x/lam)**k)
cdf = 1 - np.exp(-(x/lam)**k)

p4 = make_plot("Weibull Distribution (λ=1, k=1.25)", hist, edges, x, pdf, cdf)

# output_file('histogram.html', title="histogram.py example")

show(gridplot([p1,p2,p3,p4], ncols=2, plot_width=400, plot_height=400, toolbar_location=None))
```

```python
from numpy import linspace
from scipy.stats.kde import gaussian_kde

from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, FixedTicker, PrintfTickFormatter
from bokeh.plotting import figure
from bokeh.sampledata.perceptions import probly

import colorcet as cc

# output_file("ridgeplot.html")

def ridge(category, data, scale=20):
    return list(zip([category]*len(data), scale*data))

cats = list(reversed(probly.keys()))

palette = [cc.rainbow[i*15] for i in range(17)]

x = linspace(-20,110, 500)

source = ColumnDataSource(data=dict(x=x))

p = figure(y_range=cats, plot_width=900, x_range=(-5, 105), toolbar_location=None)

for i, cat in enumerate(reversed(cats)):
    pdf = gaussian_kde(probly[cat])
    y = ridge(cat, pdf(x))
    source.add(y, cat)
    p.patch('x', cat, color=palette[i], alpha=0.6, line_color="black", source=source)

p.outline_line_color = None
p.background_fill_color = "#efefef"

p.xaxis.ticker = FixedTicker(ticks=list(range(0, 101, 10)))
p.xaxis.formatter = PrintfTickFormatter(format="%d%%")

p.ygrid.grid_line_color = None
p.xgrid.grid_line_color = "#dddddd"
p.xgrid.ticker = p.xaxis[0].ticker

p.axis.minor_tick_line_color = None
p.axis.major_tick_line_color = None
p.axis.axis_line_color = None

p.y_range.range_padding = 0.12

show(p)
```

# Voltando para os dados

Estabelecendo cores para os resultados dos testes de diabetes (Positivo=Vermelho, Negativo=Azul), podemos plotar uma despersão do índice de massa corpórea por idade, para tentar identificar alguma tendência nos dados

```python
df['class'].value_counts()
```

```python
colormap = {'tested_positive': 'red', 'tested_negative': 'blue'}
colors = [colormap[x] for x in df['class']]

colors[:5]
```

```python
# Plotando a de

p = figure(title = "Diabeyes")
p.xaxis.axis_label = 'Age'
p.yaxis.axis_label = 'IMC'

p.circle(df["age"], df["mass"],
         color=colors, fill_alpha=0.3, size=7)

show(p)
```

## Plotando uma dispersão com reta de tendência (best fit line) do nível de gordura da pele pela insulina

```python
import numpy as np
from bokeh.plotting import figure
from bokeh.io import show

#the data
x=df['skin']
y=df['insu']

# determine best fit line
par = np.polyfit(x, y, 1, full=True)
slope=par[0][0]
intercept=par[0][1]
y_predicted = [slope*i + intercept  for i in x]

# plot it
fig=figure()
fig.circle(x,y)
fig.line(x,y_predicted,color='red',legend='y='+str(round(slope,2))+'x+'+str(round(intercept,2)))
show(fig)
```

## Identificando o nível de correlação das pessoas com diabetes (tested_positive) com todas as variáveis do DataSet

```python
df['class'].unique()
```

```python
df['y'] = df['class'] == 'tested_positive'
```

```python
df.head()
```

```python
df.corr().round(2)['y']
```

## Plotando a dispersão da concentração de glicose

Quando plotamos a dispersão da concentração de glicose pelos casosde teste positivo em diabetes, não parece algo que possa ser representado por uma reta de tendência para representação dos dados

```python
x=df['plas']
y=df['y']

fig=figure()
fig.circle(x,y)
show(fig)
```

# Quando usamos OLS para estimar esse efeito, temos um modelo linear de probabilidade

- pode ser usado porque a categórica tem distribuição de Bernouli e sua média é a própria probabilidade
- Pode ser maior que 1 e menor que zero (que não faz muito sentido)

```python
# determine best fit line
par = np.polyfit(x, y, 1, full=True)
slope=par[0][0]
intercept=par[0][1]
y_predicted = [slope*i + intercept  for i in x]

fig=figure()
fig.circle(x,y)
fig.line(x,y_predicted,color='red',legend='y='+str(round(slope,2))+'x+'+str(round(intercept,2)))
show(fig)
```

## Rodando uma Regressão Linear dos testes positivos de diabetes pela concentração de glicose

```python
df['y'] = df['y'].astype(int)
```

```python
function1 = 'y ~ plas'

model1 = smf.ols(function1, df).fit()
print(model1.summary2())
```

## Rodando a Regressão Linear com todas as variáveis do DataSet

```python
list(df)
```

```python
function1 = 'y ~ preg + plas + pres + skin + insu + mass + pedi + age'

model1 = smf.ols(function1, df).fit()
print(model1.summary2())
```

# Rodando a Regressão Logística

```python
logit = smf.logit(function1, df).fit()
```

```python
print(logit.summary2())
```

## Cálculo do valor predito para que o indivíduo tenha diabetes de acordo com a Regressão Logistica

Este cálculo deve trazer o valor da __PROBABILIDADE__ do indivíduo ter diabetes, de acordo com a fórmula da regressão (que segue o mesmo princípio da regrassão linear), trazendo a composição dos efeitos de cada variável de aacordo com seus __Betas.__

\begin{equation}
  (\text{Diabete} = 1 \mid \text{Id, Imc, Ins}) = \frac{\text{exp}(\beta_0 + \beta_1 \text{Idade} + \beta_2 \text{IMC} + \beta_{3} \text{Insulina)} }{1 + \text{exp}(\beta_0 + \beta_1 \text{Idade} + \beta_2 \text{IMC} +  \beta_{3} \text{Insulina})} \label{eq:glm1}
  \end{equation}

```python
# verificando os valores para a primeira observação

df.head(1)
```

```python
# definindo o valor do intercepto

logit.conf_int().mean(axis=1)[0]
```

```python
# verificando o código para cada posição dos valores da primeira observação

df.iloc[0, 0]
```

```python
# calculando a soma dos valores para a primeira observação

soma = 0

for index, beta in enumerate(list(logit.conf_int().mean(axis=1))):
    if index == 0:
        soma = soma + beta
    else:
        betax = beta * df.iloc[0, index-1]
        print(df.iloc[0, index-1], beta, betax)
        soma += betax

print(soma)
```

```python
1/(1+np.exp(-soma))
```

```python
# Comparando com o valor nominal temos o mesmo resultado

logit.predict()[0]
```

## Plotando a dsitribuição dos valores preditos pelo modelo de Regressão Logística

```python
df['yhat'] = logit.predict()
```

```python
df['yhat'].plot.hist()
```

```python
df['yhat'].plot.kde()
```

```python
df['class'].value_counts()
```

# Analisando probabilidade condicional com pandas

```python
df.head()
```

```python
df['yresult'] = 0

threshold = 0.5

for (i, p) in enumerate(df['yhat']):
    if p > threshold:
        df['yresult'][i] = 1

```

```python
df.head()
```

```python
df['yresult'].value_counts()
```

## Matriz de Confusão

No campo de Machine Learning e especificamente o problema da classificação estatística, uma matriz de confusão, também conhecida como matriz de erro, é um layout de tabela específico que permite a visualização do desempenho de um algoritmo, tipicamente um aprendizado supervisionado (em aprendizagem não supervisionada é geralmente chamado de matriz de correspondência). Cada linha da matriz representa as instâncias em uma classe prevista, enquanto cada coluna representa as instâncias em uma classe real (ou vice-versa). O nome deriva do fato de que fica mais fácil ver se o sistema está confundindo duas classes (ou seja, comumente errando um nome como outro).

É um tipo especial de tabela de contingência, com duas dimensões ("atual" e "previsto") e conjuntos idênticos de "classes" em ambas as dimensões (cada combinação de dimensão e classe é uma variável na tabela de contingência).

```python
from sklearn.metrics import confusion_matrix

import warnings
warnings.filterwarnings("ignore")

result_matrix = confusion_matrix(df.y, df.yresult)
result_matrix
```

```python
tn, fp, fn, tp = confusion_matrix(df.y, df.yresult).ravel()
```

```python
# Cálculo da Precisão

precision = tp / (tp+fp)
precision
```

```python
# Cálculo da Sensiblidade

recall = tp / (tp+fn)
recall
```

## Alterando o limite da condição

```python
df['yresult'] = 0

threshold = 0.7

for (i, p) in enumerate(df['yhat']):
    if p > threshold:
        df['yresult'][i] = 1
```

```python
df['yresult'].value_counts()
```

```python
result_matrix = confusion_matrix(df.y, df.yresult)
result_matrix
```

```python
tn, fp, fn, tp = confusion_matrix(df.y, df.yresult).ravel()
```

```python
# Cálculo da Precisão

precision = tp / (tp+fp)
precision
```

```python
# Cálculo da Sensiblidade

recall = tp / (tp+fn)
recall
```

### Com o aumento do limite de corte da probabilidade, ganhamos em precisão mas perdemos em sensibilidade

# Logit x Probit (Poisson x OLS)

## Modelo Probit

_fonte: Wikipedia_

Na estatística, um modelo de __probit__ é um tipo de regressão onde a variável dependente pode levar apenas dois valores, por exemplo, casado ou não. A palavra é uma mala, vinda da probabilidade + unidade. O objetivo do modelo é estimar a probabilidade de que uma observação com características particulares caia em uma das categorias específicas. Além disso, classificar as observações com base em suas probabilidades previstas é um tipo de modelo de classificação binária.

Um modelo de probit é uma especificação popular para um modelo de resposta ordinal ou binária. Como tal, trata o mesmo conjunto de problemas que a regressão logística usando técnicas semelhantes. O modelo __probit__, que emprega uma função probit link, é mais frequentemente estimado usando o procedimento padrão de máxima verossimilhança, sendo essa estimativa chamada de regressão probit.

Os modelos probit foram introduzidos por Chester Bliss em 1934 como um método rápido para calcular as estimativas de máxima verossimilhança; para eles foi proposto por Ronald Fisher como um apêndice ao trabalho de Bliss em 1935.

```python
fig = plt.figure(figsize=(12,8))
ax = fig.add_subplot(111)
support = np.linspace(-6, 6, 1000)
ax.plot(support, stats.logistic.cdf(support), 'r-', label='Logistic')
ax.plot(support, stats.norm.cdf(support), label='Probit')
ax.legend();
```

```python
fig = plt.figure(figsize=(12,8))
ax = fig.add_subplot(111)
support = np.linspace(-6, 6, 1000)
ax.plot(support, stats.logistic.pdf(support), 'r-', label='Logistic')
ax.plot(support, stats.norm.pdf(support), label='Probit')
ax.legend();

# Multiplicando-se a Legistica pelo desvio padrão da logistica, obtém-se a probit
```

02-stat-multivariada/05 Variavel Dependente Binaria 1/aula_logit.ipynb


---
# Section 2.3: Perturbing $A$

---

In this section, we will solve

$$
Ax = b
$$

where $A$ is nonsingular, but we obtain a solution $\hat{x}$ that satisfies

$$
(A + \delta A) \hat{x} = b.
$$

That is, we have a solution to a system where the matrix $A$ has been perturbed by $\delta A$.

---

## When is $A + \delta A$ nonsingular?

The first question is whether or not the perturbed matrix $A + \delta A$ is nonsingular.

Let's see what happens if $A + \delta A$ is singular.

In that case, there is a vector $y \neq 0$ such that

$$
(A + \delta A)y = 0.
$$

Since $A$ is nonsingular, we have

$$
y = -A^{-1} \delta A y.
$$

Taking norms of both sides, we have

$$
\lVert y \rVert = \lVert A^{-1} \delta A y \rVert \leq \lVert A^{-1} \rVert \lVert \delta A \rVert \lVert y \rVert.
$$

Since $y \neq 0$, we know that $\lVert y \rVert \neq 0$, so we can divide both sides of this inequality by $\lVert y \rVert$ to obtain

$$
1 \leq \lVert A^{-1} \rVert \lVert \delta A \rVert.
$$

Now we divide both sides by the condition number of $A$, $\kappa(A) = \lVert A \rVert \lVert A^{-1} \rVert$, to get

$$
\frac{1}{\kappa(A)} \leq \frac{\lVert \delta A \rVert}{\lVert A \rVert}.
$$

Thus, we have the following theorem.

> ### Theorem: (Nonsingular $A + \delta A$)
> Let $A$ be nonsingular. If
>
> $$
\frac{\lVert \delta A \rVert}{\lVert A \rVert} < \frac{1}{\kappa(A)},
$$
>
> then $A + \delta A$ is nonsingular.

---

## Bounding the relative error $\frac{\lVert \delta x \rVert}{\lVert x \rVert}$

Suppose that $A$ is nonsingular, $b \neq 0$, $Ax = b$, and

$$
(A + \delta A)(x + \delta x) = b.
$$

We would also like to know that $A + \delta A$ is nonsingular, so we will assume that

$$
\frac{\lVert \delta A \rVert}{\lVert A \rVert} < \frac{1}{\kappa(A)}.
$$

Partially expanding $(A + \delta A)(x + \delta x) = b$, we have

$$
Ax + A \delta x + \delta A (x + \delta x) = b.
$$

Since $Ax = b$, we can simplify this to

$$
A \delta x + \delta A (x + \delta x) = 0.
$$

Using the fact that $A$ is nonsingular, we have

$$
\delta x = - A^{-1} \delta A (x + \delta x).
$$

Taking norms of both sides and applying norm inequalities, we obtain

$$
\begin{align}
\lVert \delta x \rVert
&\leq \lVert A^{-1} \rVert \lVert \delta A \rVert \lVert x + \delta x \rVert \\
&\leq \lVert A^{-1} \rVert \lVert \delta A \rVert \big( \lVert x \rVert + \lVert \delta x \rVert \big) \qquad \text{(triangle inequality)}\\
&= \lVert A^{-1} \rVert \lVert A \rVert \frac{\lVert \delta A \rVert}{\lVert A \rVert} \big( \lVert x \rVert + \lVert \delta x \rVert \big) \\
&= \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} \big( \lVert x \rVert + \lVert \delta x \rVert \big). \\
\end{align}
$$

Therefore,

$$
\lVert \delta x \rVert - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} \lVert \delta x \rVert
\leq \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} \lVert x \rVert.
$$

Since $b \neq 0$, we must have $x \neq 0$, so we can divide both sides of this inequality by $\lVert x \rVert \neq 0$ to obtain

$$
\left( 1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} \right) \frac{\lVert \delta x \rVert}{\lVert x \rVert}
\leq \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} .
$$

Note that our assumption that

$$\frac{\lVert \delta A \rVert}{\lVert A \rVert} < \frac{1}{\kappa(A)}$$

implies that

$$
1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} > 0.
$$

Therefore,

$$
\frac{\lVert \delta x \rVert}{\lVert x \rVert}
\leq \frac{\kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert}}{1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert}},
$$

giving us a bound on the relative error $\frac{\lVert \delta x \rVert}{\lVert x \rVert}$.

Thus we have proved the following theorem.

> ### Theorem: (Relative error bound II)
> Let $A$ be nonsingular, $b \neq 0$, and $Ax = b$. If
>
> $$\frac{\lVert \delta A \rVert}{\lVert A \rVert} < \frac{1}{\kappa(A)} \qquad \text{and} \qquad (A + \delta A)(x + \delta x) = b,$$
>
> then
>
> $$
\frac{\lVert \delta x \rVert}{\lVert x \rVert}
\leq \frac{\kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert}}{1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert}}.
$$

**Note:**

If $A$ is well-conditioned (i.e., $\kappa(A)$ is not large), and the relative error of $A$ is much less than $\kappa(A)^{-1}$, then

$$
1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert} \approx 1.
$$

---

## Perturbing $A$ and $b$

> ### Theorem: (Relative error bound III)
> Let $A$ be nonsingular, $b \neq 0$, and $Ax = b$. If
>
> $$\frac{\lVert \delta A \rVert}{\lVert A \rVert} < \frac{1}{\kappa(A)} \qquad \text{and} \qquad (A + \delta A)(x + \delta x) = (b + \delta b),$$
>
> then
>
> $$
\frac{\lVert \delta x \rVert}{\lVert x \rVert}
\leq
\frac{\kappa(A) \left( \frac{\lVert \delta A \rVert}{\lVert A \rVert} + \frac{\lVert \delta b \rVert}{\lVert b \rVert} \right)}
     {1 - \kappa(A) \frac{\lVert \delta A \rVert}{\lVert A \rVert}}.
$$

---


```python
from simba.graph import nodes_from_dofs
from simba.core import SLH
from sympy import Matrix
```

```python
help(nodes_from_dofs)
```

    Help on function nodes_from_dofs in module simba.graph:

    nodes_from_dofs(gs, h_d) -> simba.graph.Nodes
        Construct the Node graph for an n degree-of-freedom generalised open oscillator
        :param gs: list of n 1-dof generalised open oscillators
        :param h_d: the direct interaction Hamiltonian matrix
        :return: a `Nodes` instance

```python
def graph_slh(slh):
    nodes = nodes_from_dofs([slh], Matrix.zeros(2))
    g = nodes.as_graphviz_agraph()
    g.layout(prog='neato')
    return g

def remove_all_but_node(g):
    g.remove_node("1'")
    g.remove_node('input')
    g.remove_node('output')
```

```python
rs = [
    (Matrix([[0, 0], [0, 0]]), 'tuned.pdf'),
    (Matrix([[1, 0], [0, 1]]), 'detuned.pdf'),
    (Matrix([[0, 1], [1, 0]]), 'dpa.pdf'),
    (Matrix([[1, 1], [1, 1]]), 'detuned-dpa.pdf')
]

for r, filename in rs:
    slh = SLH(Matrix.eye(2), Matrix.eye(2), r)
    g = graph_slh(slh)
    remove_all_but_node(g)
    g.draw(filename)
```

```python

```