```python
%matplotlib inline
import matplotlib.pyplot as plt

# Some nice default configuration for plots
plt.rcParams['figure.figsize'] = 15, 10
plt.rcParams['axes.grid'] = True
plt.gray()

import numpy as np
import pandas as pd
```

# 6. Linear Regression and Its Cousins

We will dicuss several models here, all of which are akin to linear regression in that each can directly or indirectly be written in the form
$$y_i = \beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip} + \epsilon_i,$$
where $y_i$ represents the numeric response for the $i$th sample, $\beta_0$ represents the estimated intercept, $\beta_j$ represents the estimated coefficient for the $j$th predictor, $x_{ij}$ represents the value of the $j$th predictor for the $i$th sample, and $\epsilon_i$ represents random error that cannot be explained by the model. In addition to ordinary linear regression, these types of models include partial least square (PLS) and penalized models such as ridge regression, the lasso, and the elastic net.

Each of these models seek to find estimates of the parameters so that the sum of squared errors or a function of the sum of squared errors is minimized. The objectives of the methods find parameter estimates that fall along the spectrum of the bias-variance trade-off. Ordinary linear regression, at one extreme, finds parameter estimates that have minimum bias, where ridge regression, the lasso, and the elastic net find estimates that have lower variance.

A distinct advantage of linear regression models is that they are highly interpretable. Furthermore, relationships among predictors can be further interpreted through the estimated coefficients. Another advantage is that their mathematrical nature enables us to compute standard errors of the coefficients, provided that we make certain assumptions about the distribution of the model residuals.

While linear regression-type models are highly interpretable, they can be limited in their usefulness. First, these models are appropriate when the relationship between the predictors and response falls along a hyperplane. If there is a curvilinear relationship between the predictors and response, then linear regression models can be augmented with additional predictors that are functions of the original predictors in an attempt to capture these relationships.

## 6.1 Case Study: Quantitative Structure-Acitivity Relationship Modeling

The study investigated a set of compounds with corresponding experimental solubility values using complex sets of descriptors. We will use 1,267 compounds (951 training and 316 test by random sampling) and a set of more understandable descriptors:
- Two hundred and eight binary "fingerprints" that indicate the presence or absence of a particular chemical substructure.
- Sixteen count descriptors, such as the number of bonds or the number of bromine atoms.
- Four continuous descriptors, such as molecular weight or surface area.

The training set will be used to tune and estimate models, as well as to determine initial estimates of performance using repeated 10-fold cross-validation. The test set will be used for a final characterization of the model of interest.

```python
# training
trainX = pd.read_csv("../datasets/solubility/solTrainX.csv").drop("Unnamed: 0", axis=1)
trainY = pd.read_csv("../datasets/solubility/solTrainY.csv").drop("Unnamed: 0", axis=1)

# test
testX = pd.read_csv("../datasets/solubility/solTestX.csv").drop("Unnamed: 0", axis=1)
testY = pd.read_csv("../datasets/solubility/solTestY.csv").drop("Unnamed: 0", axis=1)
```

```python
trainX.head(5)
```

      FP001
      FP002
      FP003
      FP004
      FP005
      FP006
      FP007
      FP008
      FP009
      FP010
      ...
      NumCarbon
      NumNitrogen
      NumOxygen
      NumSulfer
      NumChlorine
      NumHalogen
      NumRings
      HydrophilicFactor
      SurfaceArea1
      SurfaceArea2

      0
      0
      1
      0
      0
      1
      0
      0
      1
      0
      0
      ...
      14
      2
      0
      0
      0
      0
      3
      -0.856
      25.78
      25.78

      1
      0
      1
      0
      1
      1
      1
      1
      1
      0
      0
      ...
      21
      3
      1
      1
      0
      0
      4
      -0.370
      52.19
      80.43

      2
      1
      1
      1
      1
      1
      0
      0
      1
      0
      1
      ...
      13
      0
      2
      0
      0
      0
      1
      -0.330
      37.30
      37.30

      3
      0
      0
      1
      0
      0
      0
      1
      0
      0
      0
      ...
      10
      0
      0
      0
      0
      0
      1
      -0.960
      0.00
      0.00

      4
      0
      0
      1
      1
      1
      1
      0
      0
      1
      0
      ...
      9
      5
      0
      0
      1
      1
      1
      -0.069
      53.94
      53.94

5 rows × 228 columns

```python
trainY.head(5)
```

      0

      0
      -3.97

      1
      -3.98

      2
      -3.99

      3
      -4.00

      4
      -4.06

```python
print("training set size: {0} \ntest set size: {1}".format(trainX.shape, testX.shape))
```

    training set size: (951, 228)
    test set size: (316, 228)

```python
print("The range of the outcome is {0} with an avearge of {1}.".format((round(np.min(trainY)[0], 2),
                                                                        round(np.max(trainY)[0], 2)),
                                                                       round(np.mean(trainY)[0], 2)))
```

    The range of the outcome is (-11.619999999999999, 1.5800000000000001) with an avearge of -2.72.

Relationship between the outcome and two types of descriptors.

```python
fig, (ax1, ax2) = plt.subplots(1, 2)

ax1.scatter(trainX['MolWeight'], trainY)
ax1.set_xlabel("Molecular Weight")
ax1.set_ylabel("Solubility (log)")

trainY_strPresent = trainY.loc[np.where(trainX['FP003'] == 1)[0]]
trainY_strAbsent = trainY.loc[np.where(trainX['FP003'] == 0)[0]]
trainY_str = pd.concat([trainY_strPresent, trainY_strAbsent], ignore_index=True)

ax2.boxplot(np.array(trainY_str))
ax2.set_xticklabels(['structure present', 'structure absent'])
ax2.set_ylabel("Solubility (log)")

```

It is useful to explore the training set to understand the characteristics of the data prior to modeling.

```python
# evaluate the continuous predictors for skewness
trainX_c = trainX[trainX.columns[-20:]] # continuous predictors

from scipy.stats import skew
trainX_skewness = trainX_c.apply(skew)
print("Summary of skewness: range {0}; mean {1}".format((round(np.min(trainX_skewness), 2),
                                                         round(np.max(trainX_skewness), 2)),
                                                        round(np.mean(trainX_skewness), 2)))
```

    Summary of skewness: range (0.67000000000000004, 3.8399999999999999); mean 1.65

It indicates that these predictors have a propensity to be right skewed (skewness = 0 if normally distributed). To correct for this skewness, a Box-Cox transformation was applied to all predictors (add a shift parameter to non-positive predictors).

#### Side note

One-parameter Box-Cox transformations (for x > 0)
\begin{equation}
x^* = \begin{cases} {x^{\lambda}-1 \over \lambda} & \text{if} \ \lambda \neq 0 \\ log(x) & \text{if} \ \lambda = 0 \end{cases}
\end{equation}

Two-parameter Box-Cox transformations (for x > $\lambda_2$)
\begin{equation}
x^* = \begin{cases} {(x+\lambda_2)^{\lambda_1}-1 \over \lambda_1} & \text{if} \ \lambda_1 \neq 0 \\ log(x+\lambda_2) & \text{if} \ \lambda_1 = 0 \end{cases}
\end{equation}

```python
# Box-Cox transformed predictors
trainX_trans = pd.read_csv("../datasets/solubility/solTrainXtrans.csv").drop("Unnamed: 0", axis=1)
testX_trans = pd.read_csv("../datasets/solubility/solTestXtrans.csv").drop("Unnamed: 0", axis=1)
```

###### Q1: Is it safe to assume that the relationship between the predictors and outcome is linear?

```python
trainX_trans_c = trainX_trans[trainX_trans.columns[-20:]] # 20 continuous predictors

# lowess (local smoother)
import statsmodels.api as sm
lowess = sm.nonparametric.lowess
```

```python
def plotContinuousPredictors(X, y, nrows=4, ncols=5):
    '''plot the continous predictors (default 20) versus the outcome'''
    fig, axarr = plt.subplots(nrows, ncols)
    fig.tight_layout()

    for r in range(nrows):
        for c in range(ncols):
            plotOnePredictor(axarr[r, c], X.iloc[:, (r+1)*(c+1)-1], y)

def plotOnePredictor(ax, X, y):
    '''plot one predictor versus the outcome'''
    z = lowess(np.squeeze(y), np.squeeze(X), 0.88) # return sorted X and y_fit
    ax.scatter(X, y, alpha=0.4)
    ax.plot(z[:, 0], z[:, 1], 'r', linewidth=2)
    ax.set_title(X.name)

plotContinuousPredictors(trainX_trans_c, trainY)
```

The smoothed regression lines indicate that there are some linear and non-linear relationship between the predictors and the outcome. Because of this, we might consider augmenting the predictor set with quadratic terms for some variables.

###### Q2: Are there significant between-predictor correlations?

```python
# PCA scree plot
from sklearn.decomposition import PCA
from sklearn.preprocessing import scale

# scale before PCA
trainX_trans_scale = scale(trainX_trans)

pca = PCA()
pca.fit(trainX_trans_scale)

# generate scree plot
plt.plot(pca.explained_variance_ratio_)
plt.xlim((-5, None))
plt.ylim((-0.005, None))
plt.xlabel('Percent of Total Variance')
plt.ylabel('Component')
```

Notice that the amount of variability summarized by components drops sharply, with no one component accounting for more than 13% of the variance. This profile indicates that the structure of the data is contained in a much smaller number of dimensions than the number of dimensions of the original space; this is often due to a large number of collinearities among the predictors.

```python
# correlation matrix for continuous predictors
corr_dataframe_c = trainX_trans_c.corr()

# compute hierarchical cluster on both rows and columns for correlation matrix and plot heatmap
def corr_heatmap(corr_dataframe):
    import scipy.cluster.hierarchy as sch

    corr_matrix = np.array(corr_dataframe)
    col_names = corr_dataframe.columns

    Y = sch.linkage(corr_matrix, 'single', 'correlation')
    Z = sch.dendrogram(Y, color_threshold=0, no_plot=True)['leaves']
    corr_matrix = corr_matrix[Z, :]
    corr_matrix = corr_matrix[:, Z]
    col_names = col_names[Z]
    im = plt.imshow(corr_matrix, interpolation='nearest', aspect='auto', cmap='bwr')
    plt.colorbar()
    plt.xticks(range(corr_matrix.shape[0]), col_names, rotation='vertical', fontsize=12)
    plt.yticks(range(corr_matrix.shape[0]), col_names[::-1], fontsize=12)

# plot
corr_heatmap(corr_dataframe_c)
```

There are many strong positive correlations (dard red). As discussed in Chapter 3, this could create problems in developing some models and appropriate pre-processing steps will need to be taken to account for this problem.

## 6.2 Linear Regression

The objective of ordinary least square is find the plane that minimize the sum-of-squared errors (SSE) between the observed and predicted response: $$\text{SSE} = \sum_{i=1}^n (y_i - \hat{y}_i)^2,$$ where $y_i$ is the outcome and $\hat{y}_i$ is the model prediction of that sample's outcome. Mathematically, the optimal plane can be shown to be $$\hat{\beta} = (\pmb{X}^T \pmb{X})^{-1} \pmb{X}^T y,$$ where $\pmb{X}$ is the matrix of predictors, $y$ is the response vector, and $\hat{\beta}$ is the parameter estimates for each predictor. Making some assumptions about the distribution of the residuals, it is straightforward to show these estimates minimize the bias component of the bias-variance trade-off.

Notice that a unique inverse of $\pmb{X}^T \pmb{X}$ exists when (1) no predictor can be determined from a combination of one or more of the other predictors and (2) the number of samples is greater than the number of predictors. Note that linear regression can still be used for prediction when collinearity exists within the data. But we lose the ability to meaningfully interpret the coefficients. In practice, highly correlated predictors might be managed manually by removing one of the offending predictors. However, if the number of predictors is large, this may be difficult. Also, on many occasions, relationships among predictors can be complex and involve many predictors. In these cases, manual removal of specific predictors may not be possible and models that can tolerate collinearity may be more useful.

When condition (2) is true for a data set, we suggest using pre-processing techniques, such as removing pairwise correlated predictors and checking *variance inflation factor* (VIF). After pre-processing the data, if the number of predictors still outnumbers the number of observations, we need to consider PCA pre-processing, or simultaneous dimension reduction and regression via PLS or parameters shrinking estimates such as ridge regression, the lass or the elastic net.

Another drawback of multiple linear regression is that its solution is linear in the paramters. One visual clue to understanding if the relationship between predictors and the response is not linear is to examine the basic diagnostic plots. Curvature in the predicted-versus-residual plot is a primary indicator that the underlying relationship is not linear. Easily identifiable quadratic, cubic, or interactions between predictors can be accommodated in regression by add quadratic, cubic, and interactions of the original predictors.

A third notable problem with multiple linear regression is that it is prone to chasing observations that are away from the overall trend of the majority of the data. Observations that cause significant changes in the parameter estimates are called *influential*. One common approach to address these problems is to use an alternative metric to SSE that is less sensitive to large outliers, such as absolute errors and Huber function.

There are no tuning parameters for multiple linear regression. This fact, however, dose not impugn the practitioner from using rigorous model validation tools. In fact, we must use the same training and validation techniques to understand the predictive ability of this model on data which the model has not seen.

### Linear Regression for Solubility Data

Recall that we split the solubility data into training and test sets and applied a Box-Cox transformation to the continuous predictors in order to remove skewness. The next step is to identify predictors that have high pairwise correlations and to remove predictors so that no absolute pairwise correlation is greater than some pre-specified level.

In this case, we remove predictors that have pairwise correlations greater than 0.9 using a heuristic approach introduced in Chapter 3.

```python
corr_dataframe = trainX_trans.corr()

def remove_high_corr(corr_dataframe, thresh = 0.9):
    '''remove predictors with high pairwise correlation'''
    abs_corr = np.abs(corr_dataframe).as_matrix() # absolute correlation matrix
    col_names = list(corr_dataframe.columns)

    # set up diagonal to 0
    np.fill_diagonal(abs_corr, 0)

    print "Removed predictors (in order): \n"
    while np.max(abs_corr) >= thresh:
        i, j = np.unravel_index(abs_corr.argmax(), abs_corr.shape) # find maximum element
        # print abs_corr[i, j]
        rdx = which_to_remove(i, j, abs_corr)
        # remove corresponding predictor
        print col_names.pop(rdx)
        abs_corr = np.delete(abs_corr, rdx, 0)
        abs_corr = np.delete(abs_corr, rdx, 1)

    return col_names

def which_to_remove(i, j, abs_corr):
    '''compare two predictors and remove the one with higher abs correlation with other predictors'''
    i_absmean = np.mean(abs_corr[i, np.where(abs_corr[i,:] == 0)])
    j_absmean = np.mean(abs_corr[j, np.where(abs_corr[j,:] == 0)])

    return i if i_absmean > j_absmean else j

# remained predictors
col_remained = remove_high_corr(corr_dataframe)
trainX_trans_filtered = trainX_trans[col_remained]
testX_trans_filtered = testX_trans[col_remained]
```

    Removed predictors (in order):

    NumBonds
    NumSulfer
    FP203
    FP208
    NumNonHBonds
    NumChlorine
    NumHalogen
    FP183
    FP014
    FP194
    NumNitrogen
    NumAromaticBonds
    FP033
    FP166
    FP163
    FP180
    SurfaceArea2
    FP024
    FP175
    FP167
    FP141
    FP144
    FP155
    FP052
    FP056
    FP110
    NumCarbon
    FP005
    FP179
    FP136
    FP162
    FP087
    FP092
    FP112
    FP067
    FP051

Upon removing these predictors, a linear model was fit to the training data. The linear model was resampled using 10-fold cross-validation and the estimated RMSE and $\text{R}^2$ is reported.

```python
from sklearn.linear_model import LinearRegression
from sklearn.cross_validation import cross_val_score, ShuffleSplit

cv = ShuffleSplit(trainX_trans_filtered.shape[0], n_iter=10, random_state=0)

lm = LinearRegression()
train_scores_mse = cross_val_score(lm, trainX_trans_filtered, trainY.values, cv=cv,
                                   scoring = 'mean_squared_error')
train_scores_rmse = np.sqrt(-1.0 * train_scores_mse)

train_scores_r2 = cross_val_score(lm, trainX_trans_filtered, trainY.values, cv=cv,
                                  scoring = 'r2')
print "CV estimated RMSE: {0} \nCV estimated R2: {1}".format(np.mean(train_scores_rmse), np.mean(train_scores_r2))
```

    CV estimated RMSE: 0.707044315368
    CV estimated R2: 0.874723530558

```python
# apply to the test set
lm.fit(trainX_trans_filtered, trainY.values)
testY_pred = lm.predict(testX_trans_filtered)

from sklearn.metrics import r2_score, mean_squared_error
test_score_r2 = r2_score(testY.values, testY_pred)
test_score_rmse = np.sqrt(mean_squared_error(testY.values, testY_pred))

print "Test set RMSE: {0} \nTest set R2: {1}".format(test_score_rmse, test_score_r2)
```

    Test set RMSE: 0.742917835704
    Test set R2: 0.871834926473

```python
# regression diagnostic
fig, (ax1, ax2) = plt.subplots(1, 2)

ax1.scatter(testY_pred, testY.values)
ax1.set(xlim=(-11, 2), ylim=(-11, 2))
ax1.plot(ax1.get_xlim(), ax1.get_ylim(), ls="--", c=".3", linewidth=2)
ax1.set_xlabel("Predicted")
ax1.set_ylabel("Observed")

ax2.scatter(testY_pred, testY.values-testY_pred)
ax2.set(xlim=(-11, 2))
ax2.plot(ax1.get_xlim(), (0, 0), ls="--", c=".3", linewidth=2)
ax2.set_xlabel("Predicted")
ax2.set_ylabel("Residual")
```

There does not appear to be any bias in the prediction, and the distribution between the predicted values and residuals appears to be random about zero.

## 6.3 Partial Least Squares

If the correlation among predictors is high, then the ordinary least squares solution for multiple linear regression will have high variability and will become unstable. One the other hand, if the number of predictors may be greater than the number of observations, ordinary least squares in its usual form will be unable to find a unique set of regression coefficients that minimize the SSE.

A couple of common solutions include pre-processing the predictor by either (1) removal of the highly correlated predictors or (2) conducting PCA on the predictors. Removing highly correlated predictors ensures that pairwise correlations among predictors are below a pre-specified threshold. However, this process does not necessarily ensure that linear combinations of predictors are uncorrelated with each other, i.e., it may not guarantee a stable least squares solution. Alternatively, using PCA for pre-processing guarantees that the resulting predictors, or combinations thereof, will be uncorrelated. The trade-off in using PCA is that the new predictors are linear combinations of the original predictors, and thus, the practical understanding of the new predictors can become murky.

Pre-processing predictors via PCA prior to performing regression is known as principal component regression (PCR). While this two-step regression approach (dimension reduction, then regression) has been widely applied to problems with inherently highly correlated predictors or problems with more predictors than observations, it can easily be misled. Specifically, dimension reduction via PCA does not necessarily produce new predictors that explain the response, since PCA does not consider any aspects of the response when it selects its components. Instead, it simply chases the variability present throughout the predictor space. Because of this inherent problem with PCR, we recommend using PLS when there are correlated predictors and a linear regression-type solution is desired.

PLS find linear combinations of the predictors just like PCA. While the PCA linear combinations are chosen to maximally summarize predictor space variability, the PLS linear combinations of predictors are chosen to maximally summarize covariance with the response. This means that PLS finds components that maximally summarize the variation of the predictors while simultaneously requiring these components to have maximum correlation with the response. It strikes a compromise between the objectives of predictor space dimension reduction and a predictive relationship with the reponse. In other words, PLS is a *supervised* dimension reduction procedure; PCR is an *unsupervised* procedure.

Prior to performing PLS, the predictors should be centered and scaled, especially if the predictors are on scale of differing magnitude. The PLS has one tuning parameter: the number of components to retain. Resampling techniques can be used to determine the optimal number of components.

### PCR and PLSR for Solubility Data

Return to our previous example, 10-fold cross-validation was used to determine the optimal number of components to retain that minimize RMSE in both PCR and PLSR.

```python
# RMSEs for 50 components were examined
pcr_rmse = np.zeros([50, 2])
plsr_rmse = np.zeros([50, 2])

# 10-fold CV
cv = ShuffleSplit(trainX_trans.shape[0], n_iter=10)
```

```python
def optimal_with_osr(rmse):
    '''Return the minimum RMSE and its # of components using "one-standard error" rule'''
    n_best = np.argmin(rmse[:, 0])
    rmse_min = rmse[n_best, 0] + rmse[n_best, 1]
    n_best = np.min(np.where(rmse[:, 0] <= rmse_min))
    return (n_best+1), rmse_min
```

```python
# PCR
from sklearn.decomposition import PCA

for n_compo in range(len(pcr_rmse)):
    pca = PCA(n_components= n_compo+1)
    pca_compo = pca.fit_transform(trainX_trans_scale)
    train_scores_mse = cross_val_score(LinearRegression(), pca_compo, trainY.values, cv=cv, scoring = 'mean_squared_error')
    pcr_rmse[n_compo, 0] = np.mean(np.sqrt(-1.0 * train_scores_mse))
    pcr_rmse[n_compo, 1] = np.std(np.sqrt(-1.0 * train_scores_mse))

n_best, rmse_min = optimal_with_osr(pcr_rmse)
print "With one-standard error rule, the minimum PCR RMSE is {0} with {1} components".format(rmse_min, n_best)
```

    With one-standard error rule, the minimum PCR RMSE is 0.860014338691 with 43 components

```python
# PLSR
from sklearn.cross_decomposition import PLSRegression

for n_compo in range(len(plsr_rmse)):
    plsr= PLSRegression(n_components= n_compo+1, scale=False)
    train_scores_mse = cross_val_score(plsr, trainX_trans_scale, trainY.values, cv=cv, scoring = 'mean_squared_error')
    plsr_rmse[n_compo, 0] = np.mean(np.sqrt(-1.0 * train_scores_mse))
    plsr_rmse[n_compo, 1] = np.std(np.sqrt(-1.0 * train_scores_mse))

n_best, rmse_min = optimal_with_osr(plsr_rmse)
print "With one-standard error rule, the minimum PLSR RMSE is {0} with {1} components".format(rmse_min, n_best)
```

    With one-standard error rule, the minimum PLSR RMSE is 0.726648755663 with 12 components

```python
plt_pcr, = plt.plot(pcr_rmse[:,0], '-x')
plt_plsr, = plt.plot(plsr_rmse[:,0], '-o')
plt.xlim((-1, None))
plt.xlabel("# Components")
plt.ylabel("RMSE (Cross-Validation)")
plt.legend([plt_pcr, plt_plsr], ['PCR', 'PLSR'])
```

With these data, we see the supervised dimension reduction finds a minimum RMSE with significantly fewer components than unsupervised dimension reduction.

```python
from scipy.stats import pearsonr

# compoare the first two components
pca2 = PCA(n_components=2)
pca2.fit(trainX_trans_scale)

plsr2 = PLSRegression(n_components=2, scale=False)
plsr2.fit(trainX_trans_scale, trainY.values)

# visulization
fig, axarr = plt.subplots(2, 2, sharex=True, sharey=True)
#fig.tight_layout()

axarr[0,0].scatter(pca2.transform(trainX_trans_scale)[:, 0], trainY.values)
axarr[0,0].set_title("PCA Component 1")
corr = pearsonr(pca2.transform(trainX_trans_scale)[:, 0], trainY.values[:,0])[0]
axarr[0,0].text(10, -12, 'corr:' + str(round(corr, 2)))

axarr[0,1].scatter(pca2.transform(trainX_trans_scale)[:, 1], trainY.values)
axarr[0,1].set_title("PCA Component 2")
corr = pearsonr(pca2.transform(trainX_trans_scale)[:, 1], trainY.values[:,0])[0]
axarr[0,1].text(10, -12, 'corr:' + str(round(corr, 2)))

axarr[1,0].scatter(plsr2.transform(trainX_trans_scale)[:, 0], trainY.values)
axarr[1,0].set_title("PLS Component 1")
corr = pearsonr(plsr2.transform(trainX_trans_scale)[:, 0], trainY.values[:,0])[0]
axarr[1,0].text(10, -12, 'corr:' + str(round(corr, 2)))

axarr[1,1].scatter(plsr2.transform(trainX_trans_scale)[:, 1], trainY.values)
axarr[1,1].set_title("PLS Component 2")
corr = pearsonr(plsr2.transform(trainX_trans_scale)[:, 0], trainY.values[:,0])[0]
axarr[1,1].text(10, -12, 'corr:' + str(round(corr, 2)))

fig.text(0.5, 0.06, 'Component Score', ha='center', va='center')
fig.text(0.06, 0.5, 'Solubility', va='center', rotation='vertical')
```

It is not surprising that the correlation between these components and the response is greater for PLS than PCR.

Prediction of the test set using the optimal PCR and PLS models is compared.

```python
from sklearn.preprocessing import scale
testX_trans_scale = scale(testX_trans)

# PCR
pca = PCA(n_components=34) # optimal
pca.fit(trainX_trans_scale)
trainX_pca = pca.transform(trainX_trans_scale)
testX_pca = pca.transform(testX_trans_scale)
lm = LinearRegression()
lm.fit(trainX_pca, trainY.values)
testY_pca_pred = lm.predict(testX_pca)

# PLSR
plsr = PLSRegression(n_components=7, scale=False)
plsr.fit(trainX_trans_scale, trainY.values)
testY_plsr_pred = plsr.predict(testX_trans_scale)
```

```python
# regression diagnostic
fig, axarr = plt.subplots(2, 2)

axarr[0, 0].scatter(testY_pca_pred, testY)
axarr[0, 0].set(xlim=(-11, 2), ylim=(-11, 2))
axarr[0, 0].plot(axarr[0,0].get_xlim(), axarr[0,0].get_ylim(), ls="--", c=".3", linewidth=2)
axarr[0, 0].set_xlabel("Predicted")
axarr[0, 0].set_ylabel("Observed")

axarr[0, 1].scatter(testY_pca_pred, testY-testY_pca_pred)
axarr[0, 1].set(xlim=(-11, 2))
axarr[0, 1].plot(axarr[0,1].get_xlim(), (0,0), ls="--", c=".3", linewidth=2)
axarr[0, 1].set_xlabel("Predicted")
axarr[0, 1].set_ylabel("Residual")

axarr[1, 0].scatter(testY_plsr_pred, testY)
axarr[1, 0].set(xlim=(-11, 2), ylim=(-11, 2))
axarr[1, 0].plot(axarr[0,0].get_xlim(), axarr[1,0].get_ylim(), ls="--", c=".3", linewidth=2)
axarr[1, 0].set_xlabel("Predicted")
axarr[1, 0].set_ylabel("Observed")

axarr[1, 1].scatter(testY_plsr_pred, testY-testY_plsr_pred)
axarr[1, 1].set(xlim=(-11, 2))
axarr[1, 1].plot(axarr[1,1].get_xlim(), (0,0), ls="--", c=".3", linewidth=2)
axarr[1, 1].set_xlabel("Predicted")
axarr[1, 1].set_ylabel("Residual")
```

The predictive ability of each method is good, and the residuals appear to be randomly scattered about zero. Although the predictive ability of these models is close, PLS finds a simpler model that uses far fewer components than PCR.

## 6.4 Penalized Models

Under standard assumptions, the coefficients produced by ordinary least squares regression are unbiased and, of all unbiased linear techniques, this model also has the lowest variance. However, given that the MSE is a combination of variane and bias, it is very possible to produce models with smaller MSEs by allowing the parameter estimates to be biased. Combatting collinearity by using biased models may result in regression models where the overall MSE is competitive.

One method of creating biased regression models is to add a penalty to the sum of the squared errors. Recall that original least squares regression found parameter estimates to minimize the sum of the squared errors: $$\text{SSE} = \sum_{i=1}^n (y_i - \hat{y}_i)^2.$$ When the model over-fits the data, or when there are issues with collinearity, the linear regression parameter estimates may become inflated. Controlling (or *regularizing*) the parameter estimates can be accomplished by adding a penalty to the SSE if the estimates become large.

*Ridge regression* adds a second-order penalty on the SSE: $$\text{SSE}_{L_2} = \sum_{i=1}^n (y_i - \hat{y}_i)^2 + \lambda \sum_{j=1}^P \beta_j^2.$$ In effect, this method *shrinks* the estimates towards 0 as the $\lambda$ penalty become large.

By adding the penalty, we are making a trade-off between the model variance and bias. By sacrificing some bias, we can often reduce the variance enough to make the overall MSE lower than unbiased models.

```python
# path of the ridge regression coefficients
from sklearn.linear_model import Ridge

lambda_range = np.linspace(0.0, 0.01, 600)
ridgeModel = Ridge(normalize=True)
coeff_path = []

for a in lambda_range:
    ridgeModel.set_params(alpha = a)
    ridgeModel.fit(trainX_trans, trainY)
    coeff_path.append(ridgeModel.coef_[0])

ax = plt.gca()
ax.plot(lambda_range, coeff_path)
ax.set_xlim((-0.0005, 0.0105))
plt.xlabel("Penalty")
plt.ylabel("Standardized Coefficient")
```

When there is no penalty, many parameters have reasonable values, but some parameter estimates are abnormally large. These large values are indicative of collinearity issues. As the penalty is increased, the paramter estimates move closer to 0 at diferent rate.

Use cross-validation, the penalty value was optimized (over-fit -> under-fit).

```python
# penalty value versus RMSE
lambda_range = np.linspace(0.0, 0.1, 15)
score_rmse = np.zeros(15)

ridgeModel = Ridge(normalize=True)

for adx, a in enumerate(lambda_range):
    temp_scores = cross_val_score(ridgeModel.set_params(alpha = a),
                                  trainX_trans, trainY.values,
                                  cv=10, scoring="mean_squared_error")
    temp_scores = np.sqrt(-1.0 * temp_scores)
    score_rmse[adx] = np.mean(temp_scores)

plt.plot(lambda_range, score_rmse, '-o')
plt.xlabel("Penalty")
plt.ylabel("RMSE (Cross-Validation)")
plt.xlim((-0.005, 0.105))
```

While ridge regression shrinks the parameter estimates towards 0, the model does not set the values to absolute 0 for any value of the penalty. Even though some parameter estimates become negligibly small, this model does not conduct *feature selection*.

A popular alternative to ridge regression is the *least absolute shrinkage and selection operator* model, known as the *lasso*. It uses a similar penalty to ridge regression $$\text{SSE}_{L_1} = \sum_{i=1}^n (y_i - \hat{y}_i)^2 + \lambda \sum_{j=1}^P |\beta_j|.$$ While this may seem like a small modification, the practical implications are significant. While the regression coefficients are still shrunk towards 0, a consequence of penalizing the absolute values is that some parametes are actually set to 0 for some values of $\lambda$. Thus the lasso yields models that simultaneously use regularization to improve the model and to conduct feature selection.

```python
# path of the lasso regression coefficients
from sklearn.linear_model import lars_path

# compute regularization path using the LARS
lambdas, _, coefs = lars_path(trainX_trans.values, trainY.values[:,0], method = 'lasso', verbose=True)

xx = np.sum(np.abs(coefs.T), axis=1)
xx /= xx[-1]

plt.plot(xx, coefs.T)
plt.xlabel('Fraction of Full Solution')
plt.ylabel('Standardized Coefficient')
```

The x-axis is the fraction of the full solution (i.e., ordinary least squares with no penalty). Smaller values on the x-axis indicate that a large penalty has been used. When the penalty is large, many of the regression coefficients are set to 0.

A significant advancement for this model, the least angle regression (LARS), is a broad framework that encompassess the lasso and similar models. The LARS model can be used to fit lasso models more efficiently, especially in high-dimensional problems.

A generalization of the lasso model is the *elastic net*. The model combines te two types of penalties: $$\text{SSE}_{Enet} = \sum_{i=1}^n (y_i - \hat{y}_i)^2 + \lambda_1 \sum_{j=1}^P |\beta_j| + \lambda_2 \sum_{j=1}^P \beta_j^2.$$ The advantage of this model is that it enables effective regularization via the ridge-type penalty with the feature selection quality of the lasso penalty. Both the penalties require tuning to achieve optimal performance.


# Efficient Diversification
## Diversification and Portfolio Risk
Sources of risk
1. general economic conditions: business cycles, inflation, interest rates, exchange rates, ... **None** of these *macroeconomic* factors can be predicted with certainty.
2. firm-specific influences: management style and philosophy, ... those affect the stock without noticeably affecting other firms.
***
- non-diversifiable risk (market risk, systematic risk): Risk factors common to the whole economy.
    - Ex: business cycle, inflation rate, interest rates, and exchange rates
- diversifiable risk (firm-specific risk, nonsystematic risk, Unique risk): Risk that can be eliminated by *diversification*.
    - Ex: Dell’s success in research and development, its management style and philosophy.

##  Asset Allocation with Two Risky Assets
And with a simple scenario analysis we can find that investing two risky assets will have a lower standard deviation of return than investing only one of them.

For risky assets $\newcommand{\ffrac}{\displaystyle \frac}r_1$ and $r_2$, we have:

$$\begin{array}{c:cc} \hline
 & \text{Use future probability distribution} & {\text{Use historical data}\\ \text{(equal probability)}} \\ \hline
{\text{Expected}\\ \text{return}} & E(r) = \bar{r} = \sum p_i \, r_i & E(r) = \bar{r} = \ffrac{1} {n}\sum r_i\\
\text{Variance} & Var(r) = \sum p_i\,\sideset{}{^2}{(r_i - \bar{r})} & Var(r) =\ffrac{1} {n-1} \sum \sideset{}{^2}{(r_i - \bar{r})} \\
{\text{Standard}\\ \text{Deviation}} & \sigma = \sqrt{ Var(r) } & \sigma = \sqrt{ Var(r) } \\
\text{Covariance} & Cov(r_1,r_2) = \sum p_i \, (\sideset{} {_{1,i}}{r} - \sideset{} {_{1}} {\bar{r}}) \, (\sideset{} {_{2,i}}{r} - \sideset{} {_{2}} {\bar{r}}) & Cov(r_1,r_2) = \ffrac{1} {n-1}\sum (\sideset{} {_{1,i}}{r} - \sideset{} {_{1}} {\bar{r}}) \, (\sideset{} {_{2,i}}{r} - \sideset{} {_{2}} {\bar{r}}) \\
\text{Correlation} & \sideset{} {_{1,2}} {\rho} = \ffrac{Cov(r_1,r_2)} {\sigma_1 \sigma_2} & \sideset{} {_{1,2}} {\rho} = \ffrac{Cov(r_1,r_2)} {\sigma_1 \sigma_2} \\ \hline
\end{array}$$

### rules

$$\begin{array}{|c|c:C|} \hline
 & {\text{Two Assets}} & {\text{Three Assets}} \\ \hline
{\text{Rule } 1} & r_p = w_1 r_1 + w_2 r_2 & r_p = w_1 r_1 + w_2 r_2 + w_3 r_3\\[1em] \hline
{\text{Rule } 2} & E(r_p) = w_1 E(r_1) + w_2 E(r_2) & E(r_p) = w_1 E(r_1) + w_2 E(r_2) + w_3 E(r_3) \\[1em] \hline
{\text{Rule } 3} & {\sigma_p^2 = w_1^2\sigma_1^2 + w_2 \sigma_2^2 \\ + 2w_1w_2\sigma_{1,2}} & {\sigma_p^2 = w_1^2\sigma_1^2 + w_2 \sigma_2^2 + w_3 \sigma_3^2 \\ + 2w_1w_2\sigma_{1,2} + 2w_1w_3\sigma_{1,3} + 2w_2w_3\sigma_{2,3}} \\ \hline
\end{array}$$

### Risk-Return Trade-Off
**investment opportunity set**: Set of available portfolio risk-return combinations.

Plot each dots with different $w_1$ and $w_2 = 1-w_1$ we can see the figure.

Here we may let $w_i$ go lower than zero for a little bit, meaning taking the short positions.

### The Mean-Variance Criterion
Portfolio A is said to dominate portfolio B if all investors prefer A over B, meaning that A has a higher mean return and lower variance or standard deviation than B:

$$E(r_A) \geq E(r_B) \text{ AND } \sigma_A \leq \sigma_B$$

1. The investment opportunity set for two assets is a curve or a straight line, depending on correlation.
2. The investment opportunity set for three (or more) assets is a cloud.

## The Optimal Risky Portfolio with a Risk-Free Asset

| Global minimum-variance portfolio (G)| Optimal risky portfolio (P) |
| :----------------------------------: | :-------------------------: |
|     ||

For the global minimum-variance portfolio, let $\displaystyle \sigma_G = \min_{w_1,w_2} \{ \sigma_P \}$ with constraints $w_1 + w_2 = 1$.

$$\begin{align}
\sigma_P^2 =& w_1\sigma_1^2 + (1 - w_1)^2 \sigma_2^2 + 2w_1(1-w_1) \sigma_{1,2} \\
=& \sigma_1^2 w_1^2 + \sigma_2^2 - 2w_1\sigma_2^2 + \sigma_2^2 w_1^2 + 2\sigma_{1,2} - 2w_1^2 \sigma_{1,2} \\
=& (\sigma_1^2 + \sigma_2^2 - 2\sigma_{1,2})w_1^2 - 2(\sigma_2^2 - \sigma_{1,2}) w_1 + \sigma_2^2
\end{align}$$

So we reach the lowest when $w_1 = \ffrac{\sigma_2^2 - \sigma_{1,2}} {\sigma_1^2 + \sigma_2^2 -2\sigma_{1,2}}$.
***
Then for the Optimal risky portfolio, let $\displaystyle \sigma_O = \max_{w_1,w_2}\{\frac{E(r_P) - r_f} {\sigma_P} \}$ with constraints $w_1 + w_2 = 1$.

The result (let the derivative of sharpe ratio with respect to $w_1$ be $0$) is

$$
w_1 = \frac{[E(r_1)-r_f]\sigma_2^2 - [E(r_2)-r_f]\sigma_{1,2}}{[E(r_1)-r_f]\sigma_2^2 + [E(r_2)-r_f]\sigma_1^2 - [E(r_1)-r_f + E(r_2)-r_f]\sigma_{1,2}}, w_2 = 1 - w_1 $$

### Optimization for 3-asset risky portfolio
- Minimum variance frontier: $\displaystyle \min_{w_1,w_2,w_3} \{ SD(r_p)\}$, with constraints
    1. $w_1 + w_2 + w_3 = 1$
    2. $E(r_p) = const$

- global minimum variance portfolio $\displaystyle \min_{w_1,w_2,w_3} \{ SD(r_p)\}$ with constraints
    1. only $w_1 + w_2 + w_3 = 1$

- optimal risky portfolio: $\displaystyle \max_{w_1,w_2,w_3} \left \{ \ffrac{E(r_p) - r_f} {\sigma_p} \right\}$
    1. still $w_1 + w_2 + w_3 = 1$
    2. connect it to the risk-free asset to form the optimal CAL

## Efficient Diversification with Many Risky Assets
1. Efficient frontier
2. Optimal risky portfolio (technical problem)
3. Complete portfolio (personal choice)

### The Efficient Frontier of Risky Assets
| Pairwisely | Tri-wisely |
|:----------:|:----------:|
|||

**efficient frontier**: Graph representing a set of portfolios that maximizes expected return at each level of portfolio risk.

Drawing this line is the first step
### Choosing the Optimal Risky Portfolio
then use the risk free rate to find the CAL, which is the second step.
### The Preferred Complete Portfolio and a Separation Property
Then the final step, each investor chooses the appropriate mix between the optimal risky portfolio (O) and T-bills.

So actually, the portfolio manager will stop here and let you to choose a specific point on that line, which is determined by your risk aversion $\beta$ decide.

**separation property**: The property that implies portfolio choice can be separated into two independent tasks:
1. determination of the optimal risky portfolio, which is a purely technical problem, and
2. the personal choice of the best mix of the risky portfolio and the risk-free asset.

## A Single-Index Stock Market
**Index models**: Model that relates stock returns to returns on both a broad market index and firm-specific factors.

$Def$

**excess return**: for stock $i$, its excess return is denoted as $R_i = r_i - r_f$, rate of return in excess of risk-free return

Then we decompose that into three parts

$$\boxed{ R_i = \alpha + \beta R_m + e_i}, R_m = r_m - r_f$$

1. **alpha**: A stock’s expected return *beyond* that induced by the market index; it's also the expected excess return when the market’s excess return is zero.
2. **beta**: The sensitivity of a security’s excess return to the market index's excess return.
3. **market return**: $r_m$, and **market excess return**: $R_m = r_m - r_f$
4. **Residual (firm-specific or residual risk)**: $e_i$, component of return variance that is independent of the market factor.

$\odot$
So it's assumed that the variance of the firm-specific return, $e_i$ is independent of market performance $r_m$ and $\alpha$, and that $Var(\alpha)$ is $0$.

We can also decompose its variance

$$\begin{align}
Var(R_i) &= Var(\alpha + \beta R_m + e_i) \\
&= Var(\alpha) + Var(\beta R_m) + Var(e_i) + Corr(\dots) \\
&= 0 +  \beta^2 Var(R_m) + Var(e_i) + 0 \\
&= \boxed{ \beta^2 \sigma_m ^2 + \sigma_e^2} \\
&= \boxed{ \text{Systematic risk} + \text{Firm-Specific risk} }
\end{align}$$

### Statistical and Graphical Representation of the Single-Index Model
Use regression model to estimate the parameter $\alpha$ and $\beta$.

Excess return on security, $R_i$, as the dependent variable be explained by the independent (explanatory) variable $R_m$, market excess return.

The formula for $\beta$

$$\boxed{\hat{\beta} = \frac{Cov(R_i,R_m)} {Var(R_m)}}$$

$Proof$

We assume that the firm-specific shocks and market excess return is uncorrelated, $i.e.$,

$$\boxed{Cov(e_i, R_m) = 0}$$

so that $Cov(R_i, R_m) = Cov(\alpha + \beta R_m + e_i, R_m) = Cov(\beta R_m, R_m) = \beta Var(R_m)$, which leads to the final result. **Aggressive** for those with $\beta >1$, and **defensive** for those with $\beta$ under $1$. Some are even lower than $0$, which provides a hedge against systematic risk. Because it profits when market goes down.

And the actual algorithm is to minimize the sum of the squared deviations of each point to the regression line, which is also called the **security characteristic line (SCL)**, the plot of a security’s expected excess return over the risk-free rate as a function of the excess return on the market. Algebraically, it can be represented as

$$\boxed{ E[R_i \mid R_m] = \hat{\alpha}_i + \hat{\beta}_i R_m }$$

which reads: *The expectation of $R_i$ given a value of $R_M$ equals the intercept plus the slope coefficient times the value of $R_m$.* Also, $\beta$ is called the **regression coefficient**.

The regression intercept ($\alpha_D$) also gives us the expected excess return on stock $i$ when market return is *neutral*, that is, equal to the T-bill return.

We also measure the relative importance of systematic risk using $rho$, the correlation coefficient between $R_i$ and $R_m$

$$\begin{align}
\rho^2 &= \frac{\text{Systematic (explained) variance}} {\text{Total variance}} \\
&= \frac{\beta_i^2 \sigma_m^2} {\sigma_i^2} \\
&= \frac{\beta_i^2 \sigma_m^2} {\beta_i^2 \sigma_M^2 + \sigma^2 (e_i)}
\end{align}$$

**perfect correlation**, when $\rho^2 = 1$, no firm-specific effects
### Diversification in a Single-Index Security Market (skipped)
Now consider a portfolio that is equally divided to those whose return follows the single-index model $R_i = \alpha + \beta (r_m - r_f) + e_i$. Now what's the systematic and nonsystematic variances of the portfolio?

The beta of the portfolio is a simple average of the individual security betas, or the sum product of each beta and weight. Anyway that term keep its individual form $\beta R_m$, so that the systematic variance is $\beta^2 \sigma_m^2$.
***
For the firm-specific risk, consider $n$ stocks with weights $w_i$, where $\sum w_i = 1$. So first the nonsystematic portfolio return is $e_p = \sum (w_i e_i)$. And assume they are all uncorrelated, so that the portfolio nonsystematic variance is

$$\sideset{}{_{e_p}^2}{\sigma} = \sum_{i=1} ^n {w_i^2\sideset{}{_{e_i}^2}{\sigma}}$$

$\odot$So actually this term will become negligible as the number of securities grows and the portfolio becomes ever more diversified.$\triangleleft$

### Using Security Analysis with the Index Model
Now that we can predict the market excess return $R_i$, so that we also predict the $\alpha_i = R_i - \beta_i R_m$. Now following certain steps we can find the position of that stock in the optimal risky portfolio.

$$w_i^0 = \frac{\alpha_i / \sigma^2(e_i)} {R_m/\sigma_m^2}$$

Then adjust that value

$$w_i^* = \frac{w_i^0} {1+w_i^0(1-\beta_i)}, w_M^* = 1- w_i^* = \frac{1 - \beta_i w_i^0} {1+w_i^0(1-\beta_i)}$$

Here the $M$ is the passive portfolio, so that the Sharpe ratio of this optimal portfolio exceeds that of the passive portfolio comes with

$$S_O^2 - S_M^2 = \left( \frac{\sigma_i^2} {\sigma(e_i)} \right)^2 := \left(\textbf{information ratio}\right)^2$$

Then comes more stocks to analyse, we shall finanly go to the **active portfolio**:The portfolio formed by optimally combining analyzed stocks.

For stock $i,j$, with already estimated $\alpha_i, \alpha_j$, known $\sigma(e_i), \sigma(e_j)$, how to determine their weights? Anyway, the result is for this active portfolio $A$,

$$w_{i,A} = \frac{\sigma_{i}/\sigma^2(e_i)} {\sigma_{i}/\sigma^2(e_i) + \sigma_{j}/\sigma^2(e_j)},w_{j,A} = \frac{\sigma_{j}/\sigma^2(e_j)} {\sigma_{i}/\sigma^2(e_i) + \sigma_{j}/\sigma^2(e_j)}$$

So that
1. $\alpha_A = w_{i,A}\alpha_i + w_{j,A}\alpha_j$
2. $\beta_A = w_{i,A}\beta_i + w_{j,A}\beta_j$
3. $\sigma^2(e_A) = w_{i,A}^2\sigma^2(e_i) + w_{j,A}^2\sigma^2(e_j)$

## Risk of Long-Term Investments
Now there'll be assumption on duration of the period.
### Risk and Return with Alternative Long-Term Investments
An ***incorrect*** belief, **time diversification**: spreading your risky investments over many time periods offers a similar benefit in risk reduction as spreading an investment budget over many assets in a given period, which lead to the conclusion that stocks become less risky over longer horizons.

> **e.g.**
>
>$$\begin{array}{l:ccc}\hline
\text{Strategy} & {\text{Invest only risky portfolio} \\ 2 \text{ years}} & {\text{One year risky,}\\ \text{one year risk free}} & {\text{Invest half in risky portfolio}\\ 2 \text{ years}} \\ \hline
\text{Risk Premium} & R+R=2R & 0+R=R & 2\cdot 0.5R = R \\
\text{Variance} & \sigma^2+\sigma^2 = 2\sigma^2 & 0 + \sigma^2 = \sigma^2 & 2\cdot (0.5\sigma)^2 = 0.5\sigma^2 \\
\text{Sharpe Ratio} & \frac{2R} {\sqrt{2}\sigma} = \sqrt{2}S_1 & \frac{R} {\sigma} = S_1 & \frac{R} {\sigma \, /\sqrt{2}} = \sqrt{2}S_1 \\
\text{Price of Risk} & \frac{2R} {2\sideset{}{^{^2}}\sigma} = P_1 & \frac{R} {\sideset{}{^{^2}}\sigma} = P_1 & \frac{R} {\sideset{}{^{^2}}\sigma /\, 2} = 2P_1 \\[2em] \hline
\end{array}
$$
>
>Just a friendly reminder:
>1. $\text{Sharpe Ratio} = \ffrac{\text{Risk Premium}} {SD}$
>2. $\text{Price of Risk} = \ffrac{\text{Risk Premium}} {Var}$

So actually the risk grows propotionally over time. The result is $SD \propto \sqrt{t}$, $i.e.$ the standard deviation of the return over $t$ years is $\sigma \sqrt{t}$.

## Summary
- The expected rate of return of a portfolio is the weighted average of the component asset expected returns with the investment proportions as weights.
- The variance of a portfolio is a sum of the contributions of the component-security variances *plus* terms involving the covariance among assets.
- Even if correlations are positive, the portfolio standard deviation will be less than the weighted average of the component standard deviations, as long as the assets are not perfectly positively correlated. Thus, portfolio diversification is of value as long as assets are less than perfectly correlated.
- The contribution of an asset to portfolio variance depends on its correlation with the other assets in the portfolio, as well as on its own variance. An asset that is perfectly negatively correlated with a portfolio can be used to reduce the portfolio variance to zero. Thus, it can serve as a perfect hedge.
- A portfolio manager identifies the efficient frontier by first establishing estimates for the expected returns and standard deviations and determining the correlations among them. The input data are then fed into an optimization program that produces the investment proportions, expected returns, and standard deviations of the portfolios on the efficient frontier.
- In general, portfolio managers will identify different efficient portfolios because of differences in the methods and quality of security analysis. Managers compete on the quality of their security analysis relative to their management fees.
- If a risk-free asset is available and input data are identical, all investors will choose the same portfolio on the efficient frontier, the one that is tangent to the CAL. All investors with identical input data will hold the identical risky portfolio, differing only in how much each allocates to this optimal portfolio and to the risk-free asset. This result is characterized as the separation principle of portfolio selection.
- The single-index model expresses the excess return on a security as a function of the market excess return: $R_i = \alpha_i + \beta_iR_m + e_i$. This equation also can be interpreted as a regression of the security excess return on the market-index excess return. The regression line has intercept $\alpha_i$ and slope $\beta_i$ and is called the security characteristic line (CAL).
- In a single-index model, the variance of the rate of return on a security or portfolio can be decomposed into systematic and firm-specific risk. The systematic component of variance equals $\beta_i^2$ times the variance of the market excess return. The firm-specific component is the variance of the residual term in the index-model equation.
- The beta of a portfolio is the weighted average of the betas of the component securities. A security with negative beta reduces the portfolio beta, thereby reducing exposure to market volatility. The unique risk of a portfolio approaches zero as the portfolio becomes more highly diversified.

## Key Terms
- active portfolio
- alpha
- beta
- diversifiable risk
- efficient frontier
- excess return
- firm-specific risk
- index model
- information ratio
- investment opportunity set
- market risk
- nondiversifiable risk
- nonsystematic risk
- optimal risky portfolio
- residual risk
- security characteristic line
- separation property
- systematic risk
- unique risk

## Exercise
### Q1
An investor can design a risky porgolio based on two stocks, $A$ and $B$. The standard deviation of return on stock $A$ is $24\%$ while the standard deviation on stock $B$ is $14\%$. The correlation coefficient between the return on $A$ and $B$ is $0.35$. The expected return on stock $A$ is $25\%$ while on stock $B$ it is $11\%$. The proportion of the minimum variance portfolio that would be invested in stock $B$ is approximately?

$Answer$

Since $\rho = 0.35$, then we have $Cov(A,B) = \rho \cdot \sigma_A \cdot \sigma_B = 117.6\%\%$. So that to minimum variance, for stock $B$, its weight should be

$$w_B = \frac{\sigma_A ^2 - Cov(A,B)} {\sigma_A^2 + \sigma_B^2 - 2Cov(A,B)} = \frac{24^2-117.6} {24^2 + 14^2 - 2 \times 117.6} \approx 0.85$$

### Q2
Now for stock $A$ and $B$, their expected return are $18\%$ and $14\%$, and standard deviation are $20\%$ and $5\%$. The correlation coefficient is $0.5$. The risk-free rate of return is $10\%$. So in the optimal risky portfolio what proportion of stock $A$ should take?

$Answer$

Just follow the formula before, we can see that

$$w_A = \frac{[E(r_A)-r_f]\sigma_B^2 - [E(r_B)-r_f]\sigma_{A,B}}{[E(r_A)-r_f]\sigma_B^2 + [E(r_B)-r_f]\sigma_A^2 - [E(r_A)-r_f + E(r_B)-r_f]\sigma_{A,B}}$$

Here $\sigma_{A,B} = \sigma_A \cdot \sigma_B \cdot \rho_{A,B}$

## Assignment
1. Asset $A$ has an expected return of $15\%$ and a reward-to-variability ratio of $0.4$. Asset $B$ has an expected return of $20\%$ and a reward-to-variability ratio of $0.3$. A risk-averse investor would prefer a portfolio using the risk-free asset and which one?
    - $A$, with its risk: $14\% \div 0.4 < 20\% \div 0.3$
2. What is equal to the square root of the systematic variance divided by the total variance.
    - ~~covariance~~
    - correlation coefficient, $\rho$
    - ~~standard deviation~~
    - ~~reward-to-variability ratio~~
3. Diversification is most effective when security returns are
    - ~~high~~
    - **negatively correlated**
    - ~~positively correlated~~
    - ~~uncorrelated~~
4. Firm-specific risk is also called what and what?
    - unique risk
    - diversifiable risk
5. Consider two perfectly negatively correlated risky securities, A and B. Security A has an expected rate of return of 16% and a standard deviation of return of 20%. B has an expected rate of return of 10% and a standard deviation of return of 30%. The weight of security B in the minimum-variance portfolio is
$$w_B = \frac{0.2^2 - (-1)\cdot 0.2\cdot 0.3} {0.3^2 + 0.2^2 - 2\cdot (-1)\cdot 0.2\cdot 0.3} = 0.4$$
6. Stock $A$ with standard deviation $20\%$; $15\%$ for $B$. $0$ correlation coefficient. Then the standard deviation of return on the minimum-variance portfolio is
    - Now since $\rho = 0$, the weight would be $w_A = \ffrac{\sigma_B^2} {\sigma_A^2+\sigma_B^2} = 0.36, w_B = 0.64$, so that for the answer
    $$\sigma = \sqrt{0.36^2 \cdot 0.2^2 + 0.64^2 \cdot 0.15^2 + 0} = 0.12 $$

7. To construct a riskless portfolio using two risky stocks, one would need to find two stocks with a correlation coefficient of
    - $\mathbf{-1}$
8. ...
9. ...
10. ...
11. The standard deviation of return on investment $A$ is $0.27$, while the standard deviation of return on investment $B$ is $0.22$. If the *covariance* of returns on $A$ and $B$ is $0.004$, the correlation coefficient between the returns on $A$ and $B$ is
$$\rho = \frac{Cov} {\sigma_A \sigma_B} = \frac{0.004} {0.27 \cdot 0.22} = 0.067$$
12. The expected return of a portfolio is $10.4\%$, and the risk-free rate is $2\%$. If the portfolio standard deviation is $18\%$, what is the reward-to-variability ratio of the portfolio?
$$\text{Reward-to-variability ratio} = \frac{E(r_A) - r_f} {\sigma_A} = \frac{0.104 - 0.02} {0.18} = 0.47$$


# Elastic wave equation implementation on a staggered grid

This is a first attempt at implemenenting the elastic wave equation as described in:

[1] Jean Virieux (1986). ”P-SV wave propagation in heterogeneous media: Velocity‐stress finite‐difference method.” GEOPHYSICS, 51(4), 889-901. https://doi.org/10.1190/1.1442147

The current version actually attempts to mirror the FDELMODC implementation by Jan Thorbecke:

[2] https://janth.home.xs4all.nl/Software/fdelmodcManual.pdf

## Explosive source

We will first attempt to replicate the explosive source test case described in [1], Figure 4. We start by defining the source signature $g(t)$, the derivative of a Gaussian pulse, given by Eq 4:

$$g(t) = -2 \alpha(t - t_0)e^{-\alpha(t-t_0)^2}$$

```python
from devito import *
from examples.seismic.source import WaveletSource, RickerSource, GaborSource, TimeAxis
from examples.seismic import plot_image
import numpy as np

from sympy import init_printing, latex
init_printing(use_latex=True)
```

```python
# Initial grid: 1km x 1km, with spacing 100m
extent = (2000., 2000.)
shape = (201, 201)
x = SpaceDimension(name='x', spacing=Constant(name='h_x', value=extent[0]/(shape[0]-1)))
z = SpaceDimension(name='z', spacing=Constant(name='h_z', value=extent[1]/(shape[1]-1)))
grid = Grid(extent=extent, shape=shape, dimensions=(x, z))
```

```python
class DGaussSource(WaveletSource):

    def wavelet(self, f0, t):
        a = 0.004
        return -2.*a*(t - 1/f0) * np.exp(-a * (t - 1/f0)**2)

# Timestep size from Eq. 7 with V_p=6000. and dx=100
t0, tn = 0., 600.
dt = (10. / np.sqrt(2.)) / 6.
time_range = TimeAxis(start=t0, stop=tn, step=dt)

src = RickerSource(name='src', grid=grid, f0=0.01, time_range=time_range)
src.coordinates.data[:] = [1000., 1000.]
```

```python
#NBVAL_SKIP

src.show()
```

```python
# Now we create the velocity and pressure fields
so = 2
vx= TimeFunction(name='vx', grid=grid, staggered=x, space_order=so)
vz = TimeFunction(name='vz', grid=grid, staggered=z, space_order=so)
txx = TimeFunction(name='txx', grid=grid, staggered=NODE, space_order=so)
tzz = TimeFunction(name='tzz', grid=grid, staggered=NODE, space_order=so)
txz = TimeFunction(name='txz', grid=grid, staggered=(x, z), space_order=so)

```

```python
# Now let's try and create the staggered updates
t = grid.stepping_dim
time = grid.time_dim

# We need some initial conditions
V_p = 4.0
V_s = 1.0
density = 3.

# The source injection term
src_xx = src.inject(field=txx.forward, expr=src)
src_zz = src.inject(field=tzz.forward, expr=src)

#c1 = 9.0/8.0;
#c2 = -1.0/24.0;

# Thorbecke's parameter notation
cp2 = V_p*V_p
cs2 = V_s*V_s
ro = 1/density

mu = cs2*ro
l = (cp2*ro - 2*mu)

# fdelmodc reference implementation
u_vx = Eq(vx.forward, vx - dt*ro*(txx.dx + txz.dz))

u_vz = Eq(vz.forward, vz - ro*dt*(txz.dx + tzz.dz))

u_txx = Eq(txx.forward, txx - (l+2*mu)*dt * vx.forward.dx -  l*dt * vz.forward.dz)
u_tzz = Eq(tzz.forward, tzz - (l+2*mu)*dt * vz.forward.dz -  l*dt * vx.forward.dx)

u_txz = Eq(txz.forward, txz - mu*dt * (vx.forward.dz + vz.forward.dx))
```

```python
op = Operator([u_vx, u_vz, u_txx, u_tzz, u_txz] + src_xx + src_zz)

```

```python
# Reset the fields
vx.data[:] = 0.
vz.data[:] = 0.
txx.data[:] = 0.
tzz.data[:] = 0.
txz.data[:] = 0.
```

```python
#NBVAL_IGNORE_OUTPUT

op()
```

    Operator `Kernel` run in 0.09 s

```python
#NBVAL_SKIP

# Let's see what we got....
plot_image(vx.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(vz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(txx.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(tzz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(txz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
```

```python
#NBVAL_IGNORE_OUTPUT

assert np.isclose(norm(vx), 1.150313, atol=1e-4, rtol=0)
```

```python
# Now that looks pretty! But let's do it again with a higher order...
so = 12
vx= TimeFunction(name='vx', grid=grid, staggered=x, space_order=so)
vz = TimeFunction(name='vz', grid=grid, staggered=z, space_order=so)
txx = TimeFunction(name='txx', grid=grid, staggered=NODE, space_order=so)
tzz = TimeFunction(name='tzz', grid=grid, staggered=NODE, space_order=so)
txz = TimeFunction(name='txz', grid=grid, staggered=(x, z), space_order=so)

# fdelmodc reference implementation
u_vx = Eq(vx.forward, vx - dt*ro*(txx.dx + txz.dz))

u_vz = Eq(vz.forward, vz - ro*dt*(txz.dx + tzz.dz))

u_txx = Eq(txx.forward, txx - (l+2*mu)*dt * vx.forward.dx -  l*dt * vz.forward.dz)
u_tzz = Eq(tzz.forward, tzz - (l+2*mu)*dt * vz.forward.dz -  l*dt * vx.forward.dx)

u_txz = Eq(txz.forward, txz - mu*dt * (vx.forward.dz + vz.forward.dx))

op = Operator([u_vx, u_vz, u_txx, u_tzz, u_txz] + src_xx + src_zz)

# Reset the fields
vx.data[:] = 0.
vz.data[:] = 0.
txx.data[:] = 0.
tzz.data[:] = 0.
txz.data[:] = 0.
```

```python
#NBVAL_IGNORE_OUTPUT

op()
```

    Operator `Kernel` run in 0.10 s

```python
#NBVAL_SKIP

plot_image(vx.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(vz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(txx.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(tzz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
plot_image(txz.data[0], vmin=-.5*1e-2, vmax=.5*1e-2, cmap="seismic")
```

```python
#NBVAL_IGNORE_OUTPUT

assert np.isclose(norm(vx), 1.1253732, atol=1e-4, rtol=0)
```


# import module

```python
import numpy as np
import numba
import xarray as xr
```

# Continuous Equation

$$
    \begin{align}
        \frac{\partial h}{\partial t}+\frac{\partial q_x}{\partial x} +\frac{\partial q_y}{\partial y} = 0
    \end{align}
$$

```python
@numba.jit(nopython=True, parallel=False)
def conEq(dep, qx, qy, zb, dt, dx, dy, hmin, hbuf):
    imax, jmax = len(dep), len(dep[0])
    depn = np.zeros_like(dep, dtype=np.float64)
    fluxx = np.zeros((imax+1, jmax), dtype=np.float64)
    fluxy = np.zeros((imax, jmax+1), dtype=np.float64)
    gravity = float( 9.8 )

    f = lambda Qp, Qm : Qm if Qp >= 0.0 and Qm >= 0.0 else (Qp if Qp <= 0.0 and Qm <= 0.0 else 0.5*Qp+0.5*Qm )

    def flux(Qp, Qm, depp, depm, zbp, zbm) :
        r = f(Qp, Qm)

        if ( (depm + zbm) < zbp) and (depp <= hbuf) : r = 0.0
        if ( (depp + zbp) < zbm) and (depm <= hbuf) : r = 0.0

        return r

    for i in numba.prange( 1, imax ):
        for j in numba.prange( jmax ):
            c, xm = (i,j), (i-1,j)
            fluxx[c] = flux(qx[c], qx[xm], dep[c], dep[xm], zb[c], zb[xm])

    for i in numba.prange( imax ):
        for j in numba.prange( 1, jmax ):
            c, ym = (i,j), (i,j-1)
            fluxy[c] = flux(qy[c], qy[ym], dep[c], dep[ym], zb[c], zb[ym])

# wall boundary
    fluxy[:,0] = 0.0 #fluxy[:,0]
    fluxy[:,-1] = 0.0 #fluxy[:,0]

    n = 0
    for i in numba.prange(1, imax-1):
        for j in numba.prange(jmax):
            c, xp, yp = (i, j), (i+1, j), (i, j+1)
            depn[c] = dep[c] - dt*(fluxx[xp] - fluxx[c])/dx - dt*(fluxy[yp] - fluxy[c])/dy
            if depn[c] < hmin :
                n += 1
                depn[c] = hmin

# upstream boundary
    depn[0][:] = depn[1][:]

# downstream boundary
    depn[-1][:] = depn[-2][:]

    return depn, n
```

# Momentum Equation

$$
    \begin{align}
        \frac{\partial q_x}{\partial t}+\frac{\partial u q_x}{\partial x}+\frac{\partial v q_x}{\partial y}+gh\frac{\partial H}{\partial x}+\frac{\tau_{0x}}{\rho}
        - \nu_t h \left(\frac{\partial^2 u}{\partial x^2}+\frac{\partial^2 u}{\partial y^2} \right)= 0 \\
        \frac{\partial q_y}{\partial t}+\frac{\partial u q_y}{\partial x}+\frac{\partial v q_y}{\partial y}+gh\frac{\partial H}{\partial y}+\frac{\tau_{0y}}{\rho}- \nu_t h \left(\frac{\partial^2 v}{\partial x^2}+\frac{\partial^2 v}{\partial y^2} \right)
        = 0
    \end{align}
$$

```python
@numba.jit(nopython=True, parallel=False)
def momentEq(dep, qx, qy, depn, zb, dt, dx, dy, qup, cManning, hmin, hbuf, direction):
#direction = 1:x, 2:y
    gravity = float( 9.8 )

    q = qx if direction == 1 else qy
    u, v = qx/dep, qy/dep
    Vdir = q/dep

    imax, jmax = len(q), len(q[0])

    qn = np.zeros_like(q, dtype=np.float64)
    fluxx = np.zeros((imax+1, jmax), dtype=np.float64)
    fluxy = np.zeros((imax, jmax+1), dtype=np.float64)

    f = lambda vp,vm,qp,qm : vm*qm if vp >= 0.0 and vm >= 0.0 else \
        (vp*qp if vp <= 0.0 and vm <= 0.0 else (0.5*vp+0.5*vm)*(0.5*qp+0.5*qm) )

    def flux1(vp, vm, qp, qm, depp, depm, zbp, zbm) :
        if ( (depm + zbm) < zbp) and (depp <= hbuf) : r = 0.0
        if ( (depp + zbp) < zbm) and (depm <= hbuf) : r = 0.0

        r = f(vp,vm,qp,qm)
        return r

    for i in numba.prange( 1, imax ):
        for j in numba.prange( jmax ):
            c, xm = (i,j), (i-1,j)
            fluxx[c] = flux1( u[c], u[xm], q[c], q[xm], dep[c], dep[xm], zb[c], zb[xm] )

# boundary : not use
    fluxx[-1,:] = -9999
    fluxx[0,:]  = -9999

    for i in numba.prange( imax ):
        for j in numba.prange( 1, jmax ):
            c, ym = (i,j), (i,j-1)
            fluxy[c] = flux1( v[c], v[ym], q[c], q[ym], dep[c], dep[ym], zb[c], zb[ym] )

# wall boundary
    fluxy[:,0]  = -fluxy[:,1]
    fluxy[:,-1] = -fluxy[:,-2]

    for i in numba.prange(1, imax-1):
        for j in numba.prange(jmax):
            c = (i, j)
            if depn[c] <= hbuf :
                qn[c] = 0.0
            else:
            # pressure & gravity term
                if direction == 2 and ((j == 0) or (j == jmax-1)) :
                    if j == 0 :
                        c, yp = (i, j), (i, j+1)
                        Hc, Hp = depn[c] + zb[c], depn[yp] + zb[yp]

                        if Hc < zb[yp] and depn[yp] <= hbuf :
                            dHdx = 0.0
                        else :
                            dHdx = ( Hp - Hc )/dy
                    elif j == jmax-1 :
                        c, ym = (i, j), (i, j-1)
                        Hc, Hm = depn[c] + zb[c], depn[ym] + zb[ym]

                        if Hc < zb[ym] and depn[ym] <= hbuf :
                            dHdx = 0.0
                        else :
                            dHdx = ( Hc - Hm )/dy

#                 elif direction == 1 and i == imax-1 :
#                     pass
                else :
                    c  = (i, j)
                    if direction == 1 :
                        xp = (i+1, j) #if i == imax-1 else (i+1, j)
                        xm = (i-1, j)
                        dp, dm, delta = xp, xm, dx
                    else :
                        yp = (i, j+1)
                        ym = (i, j-1)
                        dp, dm, delta = yp, ym, dy

                    Vc, Vp, Vm = q[c]/dep[c], q[dp]/dep[dp], q[dm]/dep[dm]
                    Hc, Hp, Hm = depn[c]+zb[c], depn[dp]+zb[dp], depn[dm]+zb[dm]

                    if Hc < zb[dp] and depn[dp] <= hbuf :
                        if Hc < zb[dm] and depn[dm] <= hbuf :
                            dHdx = 0.0
                        else:
                            dHdx = (Hc-Hm)/delta
                    elif Hc < zb[dm] and depn[dm] <= hbuf :
                        dHdx = (Hp-Hc)/delta
                    else :
                        if Vc > 0.0 and Vp > 0.0 and Vm > 0.0:
                            Cr1, Cr2 = 0.5*(abs(Vc)+abs(Vp))*dt/delta, 0.5*(abs(Vc)+abs(Vm))*dt/delta
                            dHdx1, dHdx2 = (Hp-Hc)/delta, (Hc-Hm)/delta
                        elif Vc < 0.0 and Vp < 0.0 and Vm < 0.0:
                            Cr1, Cr2 = 0.5*(abs(Vc)+abs(Vm))*dt/delta, 0.5*(abs(Vc)+abs(Vp))*dt/delta
                            dHdx1, dHdx2 = (Hc-Hm)/delta, (Hp-Hc)/delta
                        else:
                            Cr1 = Cr2 = 0.5*(abs(0.5*(Vc+Vp))+abs(0.5*(Vc+Vm)))*dt/delta
                            dHdx1 = dHdx2 = (0.5*(Hc+Hp) - 0.5*(Hc+Hm)) / delta

                        w1, w2 = 1-Cr1**0.5, Cr2**0.5
                        dHdx = w1 * dHdx1 + w2 * dHdx2

# viscous sublayer
                Cf = gravity*cManning**2.0/dep[c]**(1.0/3.0)
                Vnorm = np.sqrt(u[c]**2.0+v[c]**2.0)
                Vis = Cf * Vnorm * u[c] if direction == 1 else  Cf * Vnorm * v[c]

# turbulence
                nut = 0.4/6.0*dep[c]*np.sqrt(Cf)*np.abs(Vnorm)
                c  = (i, j)
                xp = (i+1, j)
                xm = (i-1, j)
                yp = (i, j+1)
                ym = (i, j-1)

# side boundary : non-slip condition
#                 if i == imax-1:
#                     turb = 0.0
                if j == 0 :
                    if direction == 1:
                        turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
                             + nut * ( Vdir[yp] - 3.0*Vdir[c] )/ dy**2
                    elif direction == 2:
                        turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
                             + nut * ( Vdir[yp] - 3.0*Vdir[c] )/ dy**2
                elif j == jmax-1 :
                    if direction == 1:
                        turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
                             + nut * (  - 3.0*Vdir[c] + Vdir[ym] )/ dy**2
                    elif direction == 2:
                        turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
                             + nut * (  - 3.0*Vdir[c] + Vdir[ym] )/ dy**2
                else :
                    turb = nut * ( Vdir[xp] - 2.0*Vdir[c] + Vdir[xm] )/ dx**2 \
                         + nut * ( Vdir[yp] - 2.0*Vdir[c] + Vdir[ym] )/ dy**2

                c, xp, yp = (i,j), (i+1,j), (i,j+1)
                sourcet = Vis - dep[c] * turb

                qn[c] = q[c] - dt * ( fluxx[xp] - fluxx[c] ) / dx \
                             - dt * ( fluxy[yp] - fluxy[c] ) / dy \
                             - dt * gravity * depn[c] * dHdx \
                             - dt * sourcet

# upstream boundary
    if direction == 2 :
        qn[0,:] = 0.0
    else :
#         updep = depn.copy()
#         for j in range(jmax):
#             updep[0,j] = 0.0 if updep[0,j] <= hmin else updep[0,j]

#         print(np.sum( updep[0,:]**(5/3) ))
#         alpha = Qup / dy / np.sum( updep[0,:]**(5/3) )
        qn[0,:] = qup
#     alpha * updep[0,:]**(5/3)

# downstream boundary
    qn[-1,:] = qn[-2,:]

    return qn
```

```python
@numba.jit(nopython=True, parallel=False)
def simulation(dep, qx, qy, zb, dt, dx, dy, qup, cManning, hmin, hbuf):
    depn, count = conEq(dep, qx, qy, zb, dt, dx, dy, hmin, hbuf)
    qxn = momentEq(dep, qx, qy, depn, zb, dt, dx, dy, qup, cManning, hmin, hbuf, 1)
    qyn = momentEq(dep, qx, qy, depn, zb, dt, dx, dy, qup, cManning, hmin, hbuf, 2)
#     CFL = ((np.abs(qxn/depn) + np.sqrt(9.8*depn))/dx + ( np.abs(qyn/depn) + np.sqrt(9.8*depn) )/dy)*dt
    CFL = ((np.abs(qxn/depn))/dx + ( np.abs(qyn/depn))/dy)*dt
    CFLmax = np.max(CFL)

    return depn, qxn, qyn, CFLmax, count
```

# read zb data

```python
ds = xr.open_dataset('zb.nc')
```

# Main routine

```python
%%time

hmin = float(10.0**(-5))
hbuf = float(10.0**(-2))
dtout= float(300.0)
trunup = float(0.0 * 3600.0)
tmax = float(2.01 * 3600.0)

nout = 0
CFL = float(0.01)
cManning = 0.03

dx, dy = float(2.5), float(2.5)
dtini = float(0.05)

nxmax, nymax = ds.dims['x'], ds.dims['y']
qx = np.zeros((nxmax,nymax), dtype=np.float64)
qy = np.zeros_like(qx, dtype=np.float64)
dep = np.full_like(qx, hmin, dtype=np.float64)
zb = np.zeros_like(qx, dtype=np.float64)
zb[:,:] = ds['elevation'].values[:,:]

# 初期条件の設定方法
# 上流端で等流で10m3/sとなるような水位を与える(下のセルを参照)
# i=0と1に同じ値を設定

ib = 1/60 # condition
zb0 = zb[0,:]
dep0 = - zb0 + 213.96
dep0 = np.where(dep0<0.0, hmin, dep0)
dep[0,:] = dep0
dep[1,:] = dep0

dep0 = - zb0 + 213.96
dep0 = np.where(dep0<0.0, 0.0, dep0)
qup = ib**0.5*dep0**(5.0/3.0)/cManning
qx[0,:] = qup
qx[1,:] = qup

t = trunup
dt = dtini

while tmax >= t :
    t += dt
    dep, qx, qy, CFLmax, count = simulation(dep, qx, qy, zb, dt, dx, dy, qup, cManning, hmin, hbuf)

# update dt
#     dt = np.round( dt * CFL/CFLmax, 5)

    if t >= nout*dtout :
        print(t, dt, CFLmax, count)

        dss = xr.Dataset({'depth': (['x','y'], dep), 'u': (['x','y'], qx/dep), 'v': (['x','y'], qy/dep)
                          , 'elevation': (['x','y'], zb) }
                          , coords={'xc': (('x', 'y'), ds['xc']), 'yc': (('x', 'y'), ds['yc'])}
                          , attrs={'total_second' : round(t, 2)} )

        dss.to_netcdf('out' + str(nout).zfill(8) + '.nc')
        dss.close()
        nout += 1

print(t, dt, CFLmax, count)
```

    0.05 0.05 0.052352445673623564 0
    300.00000000003394 0.05 0.07684804438564026 0
    600.000000000002 0.05 0.10061262671770331 0
    900.0499999997292 0.05 0.09965959753886351 0
    1200.0499999994563 0.05 0.09998808065436166 0
    1500.0499999991835 0.05 0.11842098469155105 0
    1800.0499999989106 0.05 0.11878849459757701 0
    2100.0499999988742 0.05 0.11882649270672492 0
    2400.0499999999656 0.05 0.11881510547164381 0
    2700.000000001057 0.05 0.1187863195476354 0
    3000.000000002148 0.05 0.11877484363740883 0
    3300.0000000032396 0.05 0.1187802128264083 0
    3600.000000004331 0.05 0.11878122614074674 0
    3900.0000000054224 0.05 0.1187887522938012 0
    4200.000000006514 0.05 0.11878038302590968 0
    4500.000000007605 0.05 0.1187968825301164 0
    4800.000000008697 0.05 0.11878014105777002 0
    5100.000000009788 0.05 0.1187920690458197 0
    5400.000000010879 0.05 0.11878160704811534 0
    5700.000000011971 0.05 0.11878628047742552 0
    6000.000000013062 0.05 0.11878461527665465 0
    6300.000000014154 0.05 0.11877934184273109 0
    6600.000000015245 0.05 0.11878427970091268 0
    6900.000000016336 0.05 0.1187877629810914 0
    7200.000000017428 0.05 0.11878506039784553 0
    7236.000000017559 0.05 0.11878299260330763 0
    Wall time: 21min 18s

# ref:setting Qup

```python
ib = 1/60 # condition
zb0 = zb[0,:]
dh = 0.44
H = zb0.min() + dh
dep = - zb0 + H
dep = np.where(dep<0.0,0.0,dep)
print(np.sum( ib**0.5*dep**(5.0/3.0)/cManning )*dy)
print(H)
```


# 最小二乗法に基づいたオドメトリのキャリブレーション

## 条件
対向二輪型ロボット

## 数式

左右車輪の角速度から、ロボット中心の速度と角速度を計算

$$
\begin{equation}
	\left[
		\begin{array}{c}
			\upsilon \\
			\omega
		\end{array}
	\right]
	= \boldsymbol{C}\left[
		\begin{array}{c}
			\omega_{R} \\
			\omega_{L}
		\end{array}
	\right]
\end{equation}
$$

推定したいオドメトリのパラメータ

$$
\begin{equation}
	\small
	\boldsymbol{C}
	= \left[
		\begin{array}{cc}
			\displaystyle \frac{r_{R}}{2} &  \displaystyle \frac{r_{L}}{2} \\
			\displaystyle \frac{r_{R}}{b} & \displaystyle -\frac{r_{L}}{b}
			%r_{R}/2 &  r_{L}/2 \\
			%r_{R}/b & -r_{L}/b
		\end{array}
	\right]
\end{equation}
$$

最小二乗法を用いてパラメータ推定１

\begin{equation}
	\left[
		\begin{array}{c}
			c_{2,1} \\
			c_{2,2}
		\end{array}
	\right]
	= (\boldsymbol{{\it \overline{\Phi}^{\mathrm{T}}_{\theta}}}
	\boldsymbol{{\it \overline{\Phi}_{\theta}}})^{-1}
	\boldsymbol{{\it \overline{\Phi}^{\mathrm{T}}_{\theta}}}
	\left[
		\begin{array}{c}
			\theta_{N,1} - \theta_{0,1} \\
			\vdots \\
			\theta_{N,P} - \theta_{0,P}
		\end{array}
	\right]
\end{equation}

\begin{equation}
	\boldsymbol{{\it \Phi}} = T\left[
		\begin{array}{cc}
			\displaystyle \sum_{i=0}^{N-1}\omega_{R,i} & \displaystyle \sum_{i=0}^{N-1}\omega_{L,i} \\
		\end{array}
	\right]
\end{equation}

\begin{equation}
\boldsymbol{{\it \overline{\Phi}_{\theta}}} = \left[
		\begin{array}{c}
			\boldsymbol{{\it \Phi_{1}}} \\
			\vdots \\
			\boldsymbol{{\it \Phi_{P}}}
		\end{array}
	\right]
\end{equation}

最小二乗法を用いてパラメータ推定２

\begin{equation}
	\left[
		\begin{array}{c}
			c_{1,1} \\
			c_{1,2}
		\end{array}
	\right]
	= (\boldsymbol{{\it \overline{\Phi}^{\mathrm{T}}_{xy}}}
	\boldsymbol{{\it \overline{\Phi}_{xy}}})^{-1}
	\boldsymbol{{\it \overline{\Phi}^{\mathrm{T}}_{xy}}}
	\left[
		\begin{array}{c}
			x_{N,1} - x_{0,1} \\
			\vdots \\
			x_{N,P} - x_{0,P}
		\end{array}
	\right]
\end{equation}

\begin{equation}
	\boldsymbol{{\it \Phi}}
	= T\left[
		\begin{array}{cc}
		\displaystyle \sum_{i=0}^{N-1}\omega_{R,i}\cos({\theta_{i} + T\omega_{i}/2})
		& \displaystyle \sum_{i=0}^{N-1}\omega_{L,i}\cos({\theta_{i} + T\omega_{i}/2}) \\
		\displaystyle \sum_{i=0}^{N-1}\omega_{R,i}\sin({\theta_{i} + T\omega_{i}/2})
		& \displaystyle \sum_{i=0}^{N-1}\omega_{L,i}\sin({\theta_{i} + T\omega_{i}/2})
		\end{array}
	\right]
\end{equation}

\begin{equation}
\boldsymbol{{\it \overline{\Phi}_{xy}}} = \left[
		\begin{array}{c}
			\boldsymbol{{\it \Phi_{1}}} \\
			\vdots \\
			\boldsymbol{{\it \Phi_{P}}}
		\end{array}
	\right]
\end{equation}

```python
# 描画のために必要になるもの
import matplotlib
matplotlib.use('nbagg')
import matplotlib.animation as anm
import matplotlib.pyplot as plt
import matplotlib.patches as patches

# 計算で必要になるもの
import numpy as np
import math
from scipy.stats import expon, norm
```

```python
# 世界座標系におけるオブジェクト（ロボットやランドマーク）の管理を行うクラス
class World:
    def __init__(self, time_span, time_interval, debug=False):
        self.objects = [] # オブジェクトを入れる配列
        self.debug = debug # デバッグ用フラグ
        self.time_span = time_span # シミュレーション時間[s]
        self.time_interval = time_interval # サンプリング間隔

    # 配列にオブジェクトを追加する関数
    def append(self, obj):
        self.objects.append(obj)

    # 図の設定を行い、オブジェクトを描画する関数
    def draw(self):
        fig = plt.figure(figsize=(8,8))
        ax = fig.add_subplot(111)
        ax.set_aspect('equal')
        ax.set_xlim(-5,5)
        ax.set_ylim(-5,5)
        ax.set_xlabel("X", fontsize=20)
        ax.set_ylabel("Y", fontsize=20)

        # オブジェクトリスト
        elems = []

        #debug:Trueならばエラーメッセージが出力される
        if self.debug: # アニメーションなしで処理
            for i in range(1000):
                self.one_step(i, elems, ax)
        else: # アニメーションありで処理, frames:アニメのフレーム数（何回更新するか）、interval:フレーム間の時間
            self.ani = anm.FuncAnimation(fig, self.one_step, fargs=(elems, ax), frames=int(self.time_span/self.time_interval)+1,
                                         interval=int(self.time_interval*1000), repeat=False)
            plt.show()

    # アニメーションの1ステップの処理、iはステップ番号、elemsはオブジェクトリスト、axはサブプロット
    def one_step(self, i, elems, ax):
        # elemsの中身をすべて破棄
        while elems:
            elems.pop().remove()
        # 時間テキストを追加
        time_str = "t = %.2f[s]" % (self.time_interval*i)
        elems.append(ax.text(-4.4, 4.5, time_str, fontsize=10))
        # オブジェクトを描画
        for obj in self.objects:
            obj.draw(ax, elems)
            if hasattr(obj, "one_step"):
                obj.one_step(1.0)
```

```python
# 標準的なロボットクラス
class IdealRobot:
    # 初期化する関数
    def __init__(self, name="Name", wrl=np.array([[0.0,0.0]]).T, pose=np.array([0.0,0.0,0.0]).T, sensor=None, color="black", isBias=False):
        # ロボットの情報を初期化（位置姿勢、半径、色、エージェント、poseリスト）
        self.pose = pose
        self.r = 0.2
        self.color = color
        self.poses = [pose]
        self.sensor = sensor
        # 左右車輪の角速度からロボットの中心の速度と角速度を求める
        self.name = name
        self.rR = 0.5
        self.rL = 0.5
        self.B = 1.0
        self.wrl = wrl
        self.sum_wrl = np.array([0.0, 0.0])
        self.sum_wrl_xy = np.array([[0.0, 0.0], [0.0, 0.0]])
        self.c = np.array([[self.rR/2.0,self.rL/2.0],[self.rR/self.B,-1.0*self.rL/self.B]])
        self.nw = np.array([0.0, 0.0])
        self.isBias = isBias
        # リスト
        self.sum_wrl_s = []
        self.sum_wrl_xy_s = []
        self.theta_s = []
        self.xy_s = []

    def set_wrl(self, wrl):
        self.wrl = wrl

    def init_func(self):
        self.pose = np.array([0.0, -4.0, math.pi/2.0]).T
        self.nw = np.array([0.0, 0.0])
        self.poses = [self.pose]
        self.sum_wrl = np.array([0.0, 0.0])
        self.sum_wrl_xy = np.array([[0.0, 0.0], [0.0, 0.0]])

    # ロボットを描画する関数
    def draw(self, ax, elems):
        # ロボットの情報を用いて円を作成（向きに合わせて直線も引いている）
        x, y, theta = self.pose
        xn = x + self.r * math.cos(theta)
        yn = y + self.r * math.sin(theta)
        elems += ax.plot([x,xn], [y,yn], color=self.color)
        c = patches.Circle(xy=(x,y), radius=self.r, fill=False, color=self.color)
        # 引数のサブプロットへロボットを追加
        elems.append(ax.add_patch(c))

        # ax.plotはリストを返すので+=にする
        self.poses.append(self.pose)
        elems += ax.plot([e[0] for e in self.poses], [e[1] for e in self.poses], linewidth=0.5, color="black")

    # 左右車輪の角速度wrlから速度と角速度nwを計算
    def nu_omega(self):
        self.nw = np.dot(self.c, self.wrl.T)
        nu = self.nw[0]
        omega = self.nw[1]
        if self.isBias:
            nu = nu * 1.1
        return nu, omega

    def one_step(self, time_interval):
        nu, omega = self.nu_omega()
        self.pose = self.state_transition(nu, omega, time_interval, self.pose)
        self.sum_wrl += self.wrl
        self.sum_wrl_xy[0][0] += self.wrl[0] * math.cos(self.pose[2] + 0.1*omega/2.0)
        self.sum_wrl_xy[0][1] += self.wrl[1] * math.cos(self.pose[2] + 0.1*omega/2.0)
        self.sum_wrl_xy[1][0] += self.wrl[0] * math.sin(self.pose[2] + 0.1*omega/2.0)
        self.sum_wrl_xy[1][1] += self.wrl[1] * math.sin(self.pose[2] + 0.1*omega/2.0)

    # @classmethodをつけることで外部から関数だけ呼び出せる
    @classmethod
    def state_transition(cls, nu, omega, time, pose):
        t0 = pose[2]
        if math.fabs(omega) < 1e-10:
            return pose + np.array([nu*math.cos(t0),
                                     nu*math.sin(t0),
                                     omega]) * time
        else:
            return pose + np.array([nu/omega*(math.sin(t0+omega*time) - math.sin(t0)),
                                    nu/omega*(-math.cos(t0+omega*time) + math.cos(t0)),
                                    omega*time])

    def save_data(self):
        self.sum_wrl_s.append(self.sum_wrl*0.1)
        print(self.sum_wrl_s)
        self.sum_wrl_xy_s.append(self.sum_wrl_xy[0]*0.1)
        self.sum_wrl_xy_s.append(self.sum_wrl_xy[1]*0.1)
        self.theta_s.append(np.array([self.poses[-1][2] - self.poses[0][2]]))
        self.xy_s.append(np.array([self.poses[-1][0] - self.poses[0][0]]))
        self.xy_s.append(np.array([self.poses[-1][1] - self.poses[0][1]]))

    def calibration(self):
        Phi_theta = np.array(self.sum_wrl_s)
        a1 = np.dot(Phi_theta.T, Phi_theta)
        a2 = np.linalg.inv(a1)
        a3 = np.dot(a2, Phi_theta.T)
        a4 = np.dot(a3, np.array(self.theta_s))
        self.c[1][0] = a4[0]
        self.c[1][1] = a4[1]

        Phi_xy = np.array(self.sum_wrl_xy_s)
        b1 = np.dot(Phi_xy.T, Phi_xy)
        b2 = np.linalg.inv(b1)
        b3 = np.dot(b2, Phi_xy.T)
        b4 = np.dot(b3, np.array(self.xy_s))
        self.c[0][0] = b4[0][0]
        self.c[0][1] = b4[1][0]

        print(self.c)
```

```python
# time_span:シミュレート総時間、time_interval:サンプリング間隔
world = World(5, 0.1)

# オドメトリと実際の移動経路
odom_wrl = np.array([math.pi/14.9, math.pi/15.0])
act_wrl = np.array([math.pi/15.0, math.pi/17.0])
odometry = IdealRobot(name="odom", wrl=odom_wrl, pose=np.array([0.0, -4.0, math.pi/2.0]).T, sensor=None, color="red", isBias=False)
actual = IdealRobot(name="act", wrl=act_wrl, pose=np.array([0.0, -4.0, math.pi/2.0]).T, sensor=None, color="blue", isBias=True)

world.append(odometry)
world.append(actual)

world.draw()
```

    <IPython.core.display.Javascript object>

```python
actual.save_data()

odometry.init_func()
actual.init_func()

odometry.set_wrl(np.array([math.pi/13.9, math.pi/14.0]))
actual.set_wrl(np.array([math.pi/13.9, math.pi/15.4]))

world.draw()
```

    [array([ 1.08908545,  0.96095775])]

    <IPython.core.display.Javascript object>

```python
actual.save_data()

odometry.init_func()
actual.init_func()

odometry.set_wrl(np.array([math.pi/12.9, math.pi/13.0]))
actual.set_wrl(np.array([math.pi/12.9, math.pi/13.8]))

world.draw()
```

    [array([ 1.08908545,  0.96095775]), array([ 1.17527207,  1.06079752])]

    <IPython.core.display.Javascript object>

```python
actual.save_data()
```

    [array([ 1.08908545,  0.96095775]), array([ 1.17527207,  1.06079752]), array([ 1.26637843,  1.18378854])]

```python
actual.calibration()
```

    [[ 2.72816032  2.66522875]
     [ 4.90384615 -4.90384615]]

```python
odometry.init_func()
actual.init_func()
world.draw()
```

    <IPython.core.display.Javascript object>

```python

```


```python
# from matplotlib import pyplot as plt
from sympy import Symbol, Eq, Function, solve, Rational, lambdify, latex
from IPython.display import display
from typing import List

#initialize some symbols here:

rho1 = Symbol("rho_1")
t = Symbol("t")
R = Function("R")(t)
R_d1 = R.diff()
R_d2 = R.diff().diff()
P0 = Symbol("P_0")
mu = Symbol("mu")
sigma = Symbol("sigma")

variables = {
    rho1: 997,                # Density of water
    P0: -9.81 * 997 * 1000,   # Assume constant throughout process, pressure = density * 9.81 * height
    mu: 0.0013076,
    sigma: 0.072
}

print("Substitution Values")
display(
    Eq(rho1, variables[rho1]),
    Eq(P0, variables[P0]),
    Eq(mu, variables[mu]),
    Eq(sigma, variables[sigma]))

lhs = rho1 * (R * R_d2 + Rational(3 / 2) * R_d1 ** 2)
rhs = - P0 - 4 * mu * (1 / R) * R_d1 - 2 * sigma / R
eqn = Eq(lhs, rhs)

print("\n\nRayleigh-Plesset equation")
display(eqn)

#output should be the initial values that are out
```

    Substitution Values

$\displaystyle \rho_{1} = 997$

$\displaystyle P_{0} = -9780570.0$

$\displaystyle \mu = 0.0013076$

$\displaystyle \sigma = 0.072$

    Rayleigh-Plesset equation

$\displaystyle \rho_{1} \left(R{\left(t \right)} \frac{d^{2}}{d t^{2}} R{\left(t \right)} + \frac{3 \left(\frac{d}{d t} R{\left(t \right)}\right)^{2}}{2}\right) = - P_{0} - \frac{4 \mu \frac{d}{d t} R{\left(t \right)}}{R{\left(t \right)}} - \frac{2 \sigma}{R{\left(t \right)}}$

```python
#Solve the R-P eqn to get the differential terms isolated
dRdt = solve(eqn, R_d1)
d2R_dt2 = solve(eqn, R_d2)

#display([Eq(R_d1, dRdt), Eq(R_d2, d2R_dt2)])
```

```python
#range kutta 4 function
# yk+1 = yk + dt.f_mean
#f_mean = f1/6 +f2/3 + f3\3 + f4/6

def rk4singlestep(func, tk: float, rk: float ,dt: float):
    #runge kutta method below
    f1 = func(rk, tk)
    f2 = func(rk + dt/2*f1, tk + dt/2)
    f3 = func(rk + dt/2*f2, tk + dt/2)
    f4 = func(rk + dt*f3, tk + dt)

    return f1/6 + f2/3 + f3/3 + f4/6

```

```python
#solve the equation, start with 2nd derivative, then to 1st, then repeat for next timestep
#initializing values:

r = 100        # Starting R value
#steps = 1400   # Number of steps to run
dr_dt = -0.001 # Program breaks if dr_dt starts at 0
t = 0          # Starting t value (almost always at 0)
dt=0.01

#loop the program using the inital values to solve for the eqn

for x in range(0, 1400):
    #runs loop 1400
    t = x * dt #should start at 0 for the time step
        #get the second derivative using the rk4

    rk4singlestep(d2R_dt2, t, r, dt)
    #get the first derivative using rk4

    f_mean = rk4singlestep(dR_dt, t, r, dt)

    r.append(r[x] + f_mean/dt)

```

```python

```


```python
# enable equation numbering

%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});
```

define short notation for tex:
$\newcommand{\Embb}{\mathbb{E}}$
$\newcommand{\Fmbb}{\mathbb{F}}$
$\newcommand{\Gmbb}{\mathbb{G}}$

$\newcommand{\Acal}{\mathcal{A}}$
$\newcommand{\Fcal}{\mathcal{F}}$
$\newcommand{\Gcal}{\mathcal{G}}$

$\newcommand{\given}{\:\vert\:}$

The goal is to find a policy for ordering a single product over $T+1$ periods ($t=0,1,\cdots,T$) to minimize the expected total costs, which consists of ordering costs, holding costs, and backordering costs. The inventory level is $x_t$, the demand $d_t$ is uncertain, and the ordered amount is $a_t$. The inventory level evolves according to

$$ x_{t+1} = x_t + a_t - d_t,\quad x_0 = 0 $$

Cost functions are linear: order cost is $k_t a_t$, holding cost is $h_t[x_{t+1}]^+$, and backordering cost is $-p_t [x_{t+1}]^-$. The objective is

\begin{align*}
\inf_{a_0,\cdots,a_T}\;\; & \mathbb{E}\left[\sum_{t=0}^T k_t a_t + \max\{h_t x_{t+1}, -p_t x_{t+1}\}\right] \\
\text{s.t.}\;\; & x_{t+1} = x_t + a_t - d_t \\
& x_0 = 0 \\
& a_t \geq 0, \quad \forall\, t \\
\end{align*}

---

In the framework of information relaxation [BS2022](https://faculty.fuqua.duke.edu/~dbbrown/bio/papers/brown_smith_2021_information_relaxation_review.pdf), we can define a sequence of sets of information $\Fmbb=(\Fcal_0,\cdots,\Fcal_T)$, which is called _filtration_ in the oringinal paper, and $\Fcal_t$ is the set of available information at period $t$ such that $\Fcal_t \subseteq \Fcal_{t+1} \subseteq \Fcal$.

A policy denoted as $\alpha_\Fmbb \in \Acal_\Fmbb$ indicates under the available information $\Fcal_t$ we will select action $a_t \in \Acal_t$. If we define the cumulative cost as

$$r(\alpha) = \sum_{t=0}^T r_t(a_t, d_t) = \sum_{t=0}^T k_t a_t + \max\{h_t(x_t + a_t - d_t), -p_t(x_t + a_t - d_t)\}$$

The objective can simply be

$$\inf_{\alpha_\Fmbb \in \Acal_\Fmbb} \Embb[r(\alpha_\Fmbb)]$$

Confer __Example numerical test__ in Section 6.3 in [BS2022](https://faculty.fuqua.duke.edu/~dbbrown/bio/papers/brown_smith_2021_information_relaxation_review.pdf), we know the cost structures, the demand distribution, we can directly solve a DP problem with backward induction for $T=4$.

We reformulation the objective function as DP

\begin{equation}
V_t(x_t) = \min_{a_t \geq 0} k_t a_t + \Embb[\max\{h_t x_{t+1}, -p_t x_{t+1}\} + V_{t+1}(x_{t+1})] \label{}
\end{equation}

where $x_0=0$ and $V_5(\cdot)=0$.

Notice the complexity is $|S| \times |A| \times T \times |D|$

```python
import numpy as np
from scipy.stats import poisson
```

```python
def opt_DP(t, param, distri=poisson,
           srange=(-100, 100, 1), arange=(0, 100, 1), disp=False):
    """
    Return a table of (inventory level, minimized expected total costs)
    Inputs:
        t      ~ period index, integer
        param  ~ parameters, dict
        distri ~ distribution, see scipy.stats
        srange ~ value range of inventory level, (min, max, step)
        arange ~ value range for action, (min, max, step)
    Output:
        table  ~ hash table (dict),
                    key is the inventory level (integer)
                    value is the min expected total cost
    """

    # parse parameter
    T = param['T']
    k = param['k'][t]
    h = param['h'][t]
    p = param['p'][t]
    Ed = param['Ed'][t]

    if t == T:
        # there is no next value function
        nV = None
    else:
        # fetch next period value function
        nV = opt_DP(t+1, param, distri, srange, arange)

    # solve min problem at the current period

    table = dict()
    for x in range(*srange):
        # action indexed current value
        Va = inner_problem(x, arange, distri, Ed, k, h, p, nV)
        a, Vmin = min(Va.items(), key=lambda x: x[1])
        table[x] = {'V': Vmin, 'a': a}

    # display progress
    if disp: print(f"Period {t} is finished.")

    return table
```

```python
def inner_problem(x, arange, distri, mu, k, h, p, tbl):
    """
    Return (action, value) list when current inventory is x
    Inputs:
        x      ~ current inventory level
        arange ~ set of available actions
        distri ~ distribution function
        mu     ~ parameter of the distribution
        k      ~ unit order cost
        h      ~ unit hold cost
        p      ~ unit backorder cost
        tbl    ~ value function of the next period
    Output:
        value  ~ hash table
    """

    value = dict()
    for a in range(*arange):
        value[a] = k * a
        # calculate expectation
        # todo, make the parameter changeable
        for d in range(70):
            pr = distri.pmf(d, mu)
            nx = x + a - d
            f = max(h*nx, -p*nx)
            nV = {'V': 0, 'a':0} if tbl is None else tbl.get(nx, {'V': np.inf, 'a': np.inf})
            value[a] = value[a] + pr * (f + nV['V'])
    return value
```

```python
# time-varying demand and constant costs

param1 = {
    'T': 4,
    'k': [2, 2, 2, 2, 2],
    'h': [1, 1, 1, 1, -1],
    'p': [9, 9, 9, 9, 11],
    'Ed': [40, 40, 40, 2, 2]
}

V = opt_DP(0, param1)
print(f"The optimal value is: {V[0]['V']:.2f}")
```

    The optimal value is: 293.93

```python
# constant demand and time-varying costs

param2 = {
    'T': 4,
    'k': [7, 8, 3, 4, 1.5],
    'h': [1, 1, 1, 1, -1],
    'p': [9, 9, 9, 9, 11],
    'Ed': [30, 30, 30, 30, 30]
}

V = opt_DP(0, param2)
print(f"The optimal value is: {V[0]['V']:.2f}")
```

    The optimal value is: 752.45

The central idea of __information relaxation__ is to use something not available at the current period with a penalty. Let's consider a perfect information relaxation $\Gmbb=(\Fcal,\cdots, \Fcal)$. That is, we know all the realized demand before making any order decisions. Under this new set of information, we can have $\Acal_\Gmbb = \Acal$.

According to BS2022, Theorem 3.1, with a dual feasible penalty $\pi$, we can have a lower bound (performance bound) for the inventory management problem

$$\Embb[\inf_{\alpha \in \Acal_\Gmbb} r(\alpha) - \pi(\alpha)]$$

We can select $\pi=0$, which provides a looser performance bound, and in most cases such a policy is called (hindsight) clairvoyant policy. The above problem can be reformulated as a dynamic lot-sizing problem and solved as a shortest path problem. For simplicity, define $f_t(x):= \max\{h_t x, -p_t x\}$, and then

$$\inf_{a_0,\cdots,a_T \in \Acal} \sum_{t=0}^T k_t a_t + f_t(x_{t+1})$$

The finite horizon problem can easily be solved by drawing a graph, e.g., see [this paper](https://pubsonline.informs.org/doi/abs/10.1287/opre.1070.0508?casa_token=lR--Ki6hXKgAAAAA:VPlFQ0y7c9tOHj6pPbiylIHyk1x8bmUKg8ywoacSYmwgjsyIis5vdel7hLS1Sd3y52ut0Z_ZiRP6tQ). In the case of constant costs, the optimal decision is $a_t = \Embb[d_t]$ and hence th optimal value equals to
$$\sum_{t=0}^T k_t \Embb[d_t]$$
and in the case of time-varying costs, one has to find the equivalent ordering cost using the graph, and the optimal value is
$$\sum_{t=0}^T \hat{k}_t \Embb[d_t]$$

For example, if $k_{t-1} + h_{t-1} < k_t < k_{t+1} - p_t$, then $\hat{k}_t = k_{t-1} + h_{t-1}$. In this case, $a_t = 0$.

---

The Proposition 3.1 in BS2022 says that a dual feasible penalty function can be in the following form

$$\pi_t(a_t) = w_t(a_t) - \Embb[w_t(a_t) \given \Fcal_t]$$

where $w_t(\cdot)$ is a generating function that depends only on the set of actions until the current period $t$.

Take the generating function to be the period cost, that is,

$$\pi_t(a_t) = r_t(a_t) - \Embb[r_t(a) \given \Fcal_t]$$

The performance bound is equivalent to

$$\Embb[\inf_{\alpha \in \Acal_\Gmbb} \Embb[r(\alpha)\given \Fcal_t]]$$

where $\Fcal_t = (d_0, \cdots, d_{t-1})$. More explicitly, we reformulate the inner problem as

$$
\inf_{a_0\cdots a_T \in \Acal} \sum_{t=0}^T \left\{k_t a_t + \Embb[f_t(x_{t+1}) \given \Fcal_t]\right\}
$$

This is still a dynamic lot-sizing problem with a smoothed function

$$\hat{f}_t(x_{t+1}) = \Embb[f_t(x_{t+1}) \given \Fcal_t]$$

which "takes expectation over the uncertain demand $d_t$ given a particular earlier realized demand sequence $(d_0,\cdots, d_{t-1})$."

```python
def inner_problem_2(dt, x, arange, distri, mu, k, h, p, tbl):
    """
    Return (action, value) list when current inventory is x
    Inputs:
        x      ~ current inventory level
        arange ~ set of available actions
        distri ~ distribution function
        mu     ~ parameter of the distribution
        k      ~ unit order cost
        h      ~ unit hold cost
        p      ~ unit backorder cost
        tbl    ~ value function of the next period
    Output:
        value  ~ hash table
    """

    value = dict()
    for a in range(*arange):
        value[a] = k * a
        # calculate expectation
        d = np.arange(100)
        pr = distri.pmf(d, mu)
        f = np.zeros((100, 2))
        f[:,0] = h * (x + a - d)
        f[:,1] = -p * (x + a - d)
        nx = x + a - dt
        nV = {'V': 0, 'a':0} if tbl is None else tbl.get(nx, {'V': np.inf, 'a': np.inf})
        value[a] = k * a + pr @ np.max(f, axis=1) + nV['V']
    return min(value.items(), key=lambda x: x[1])

def smooth_DP(t, demands, param, distri=poisson,
             srange=(-100, 100, 1), arange=(0, 100, 1), disp=False):
    # parse parameter
    T = param['T']
    k = param['k'][t]
    h = param['h'][t]
    p = param['p'][t]
    Ed = param['Ed'][t]
    dt = demands[t]

    if t == T:
        # there is no next value function
        nV = None
    else:
        # fetch next period value function
        nV = smooth_DP(
            t+1, demands, param, distri=distri
        )

    # solve min problem at the current period

    table = dict()
    for x in range(*srange):
        # action indexed current value
        a, Vmin = inner_problem_2(
            dt, x, arange, distri, Ed, k, h, p, nV
        )
        table[x] = {'V': Vmin, 'a': a}

    # display progress
    if disp: print(f"Period {t} is finished.")

    return table
```

```python
def Monte_Carlo(repeats, param, distri):
    T = param['T'] + 1
    demands = np.zeros((repeats, 5))
    for t in range(T):
        mu = param['Ed'][t]
        demands[:, t] = distri.rvs(mu, size=repeats)

    costs = np.zeros(repeats)
    for i in range(repeats):
        tbl = smooth_DP(0, demands[i,:], param)
        costs[i] = tbl[0]['V']

    return costs
```

```python
param1 = {
    'T': 4,
    'k': [2, 2, 2, 2, 2],
    'h': [1, 1, 1, 1, -1],
    'p': [9, 9, 9, 9, 11],
    'Ed': [40, 40, 40, 2, 2]
}

costs = Monte_Carlo(100, param1, poisson)
```

```python
costs.mean()
```

    293.2754128462732

```python
costs.std()
```

    18.45823672806469