```python
import numpy as np
import scipy.misc
from scipy.fftpack import dct, idct
import sys
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
import random
from tqdm._tqdm_notebook import tqdm_notebook
from scipy.fftpack import dct, idct
import seaborn as sns
from skimage.metrics import structural_similarity as ssim
import pandas as pd
import sympy

%matplotlib inline

class ImageLoader:
    def __init__(self, FILE_PATH):
        self.img = np.array(Image.open(FILE_PATH))
        # 行数
        self.row_blocks_count = self.img.shape[0] // 8
        # 列数
        self.col_blocks_count = self.img.shape[1] // 8

    def get_points(self, POINT):
        Row = random.randint(0, len(self.img) - POINT - 1)
        Col = random.randint(0, len(self.img) - 1)
        return self.img[Row : Row + POINT, Col]

    def get_block(self, col, row):
        return self.img[col * 8 : (col + 1) * 8, row * 8 : (row + 1) * 8]

# plt.rcParams['font.family'] ='sans-serif'#使用するフォント
# plt.rcParams["font.sans-serif"] = "Source Han Sans"
plt.rcParams["font.family"] = "Source Han Sans JP"  # 使用するフォント
plt.rcParams["xtick.direction"] = "in"  # x軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["ytick.direction"] = "in"  # y軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["xtick.major.width"] = 1.0  # x軸主目盛り線の線幅
plt.rcParams["ytick.major.width"] = 1.0  # y軸主目盛り線の線幅
plt.rcParams["font.size"] = 12  # フォントの大きさ
plt.rcParams["axes.linewidth"] = 1.0  # 軸の線幅edge linewidth。囲みの太さ
matplotlib.font_manager._rebuild()

MONO_DIR_PATH = "../../Mono/"
AIRPLANE = ImageLoader(MONO_DIR_PATH + "airplane512.bmp")
BARBARA = ImageLoader(MONO_DIR_PATH + "barbara512.bmp")
BOAT = ImageLoader(MONO_DIR_PATH + "boat512.bmp")
GOLDHILL = ImageLoader(MONO_DIR_PATH + "goldhill512.bmp")
LENNA = ImageLoader(MONO_DIR_PATH + "lenna512.bmp")
MANDRILL = ImageLoader(MONO_DIR_PATH + "mandrill512.bmp")
MILKDROP = ImageLoader(MONO_DIR_PATH + "milkdrop512.bmp")
SAILBOAT = ImageLoader(MONO_DIR_PATH + "sailboat512.bmp")
```

```python
n_bar = 4
N = 8
```

# MSDS

```python
def msds(N,arr):
    w_e = 0
    e_e = 0
    n_e = 0
    s_e = 0
    nw_e = 0
    ne_e = 0
    sw_e = 0
    se_e = 0
    for row in range(arr.shape[0] // N):
        for col in range(arr.shape[1] // N):
            f_block = arr[row * N : (row + 1) * N, col * N : (col + 1) * N]
            # w
            if col == 0:
                w_block = np.fliplr(f_block)
            else:
                w_block = arr[row * N : (row + 1) * N, (col - 1) * N : col * N]
            # e
            if col == arr.shape[1] // N - 1:
                e_block = np.fliplr(f_block)
            else:
                e_block = arr[row * N : (row + 1) * N, (col + 1) * N : (col + 2) * N]
            # n
            if row == 0:
                n_block = np.flipud(f_block)
            else:
                n_block = arr[(row - 1) * N : row * N, col * N : (col + 1) * N]
            # s
            if row == arr.shape[0] // N - 1:
                s_block = np.flipud(f_block)
            else:
                s_block = arr[(row + 1) * N : (row + 2) * N, col * N : (col + 1) * N]

            w_d1 = f_block[:, 0] - w_block[:, N-1]
            e_d1 = f_block[:, N-1] - e_block[:, 0]
            n_d1 = f_block[0, :] - n_block[N-1, :]
            s_d1 = f_block[N-1, :] - s_block[0, :]

            w_d2 = (w_block[:, N-1] - w_block[:, N-2] + f_block[:, 1] - f_block[:, 0]) / 2
            e_d2 = (e_block[:, 1] - e_block[:, 0] + f_block[:, N-1] - f_block[:, N-2]) / 2
            n_d2 = (n_block[N-1, :] - n_block[N-2, :] + f_block[1, :] - f_block[0, :]) / 2
            s_d2 = (s_block[1, :] - s_block[0, :] + f_block[N-1, :] - f_block[N-2, :]) / 2

            w_e += np.sum((w_d1 - w_d2) ** 2 )
            e_e += np.sum((e_d1 - e_d2) ** 2 )
            n_e += np.sum((n_d1 - n_d2) ** 2)
            s_e += np.sum((s_d1 - s_d2) ** 2)

            # nw
            if row == 0 or col == 0:
                nw_block = np.flipud(np.fliplr(f_block))
            else:
                nw_block = arr[(row - 1) * N : row * N, (col - 1) * N : col * N]
            # ne
            if row == 0 or col == arr.shape[1] // N - 1:
                ne_block = np.flipud(np.fliplr(f_block))
            else:
                ne_block = arr[(row-1) * N : row * N, (col + 1) * N : (col + 2) * N]
            # sw
            if row == arr.shape[0] // N -1 or col == 0:
                sw_block = np.flipud(np.fliplr(f_block))
            else:
                sw_block = arr[row * N : (row+1) * N, (col-1) * N : col * N]
            # se
            if  row == arr.shape[0]//N-1 or col == arr.shape[0] // N -1:
                se_block = np.flipud(np.fliplr(f_block))
            else:
                se_block = arr[(row + 1) * N : (row + 2) * N, (col+1) * N : (col + 2) * N]

            nw_g1 = f_block[0, 0] - nw_block[N-1, N-1]
            ne_g1 = f_block[0, N-1] - ne_block[N-1, 0]
            sw_g1 = f_block[N-1, 0] - sw_block[0, N-1]
            se_g1 = f_block[N-1, N-1] - se_block[0, 0]

            nw_g2 = (nw_block[N-1,N-1] - nw_block[N-2,N-2] + f_block[1,1] - f_block[0,0])/2
            ne_g2 = (ne_block[N-1,0] - ne_block[N-2,1] + f_block[1,N-2] - f_block[0,N-1])/2
            sw_g2 = (sw_block[0,N-1] - nw_block[1,N-2] + f_block[N-2,1] - f_block[N-1,0])/2
            se_g2 = (nw_block[0,0] - nw_block[1,1] + f_block[N-2,N-2] - f_block[N-1,N-1])/2

            nw_e += (nw_g1 - nw_g2) ** 2
            ne_e += (ne_g1 - ne_g2) ** 2
            sw_e += (sw_g1 - sw_g2) ** 2
            se_e += (se_g1 - se_g2) ** 2

    MSDSt = (w_e + e_e + n_e + s_e + nw_e + ne_e + sw_e + se_e)/ ((arr.shape[0]/N)**2)
    MSDS1 = (w_e + e_e + n_e + s_e)/ ((arr.shape[0]/N)**2)
    MSDS2 = (nw_e + ne_e + sw_e + se_e)/ ((arr.shape[0]/N)**2)
    return MSDSt, MSDS1, MSDS2
```

```python
class DMLCT:
    def __init__(self, n_bar, N):
        self.n_bar = n_bar
        self.N = N

        self.x_l = (2 * np.arange(N) + 1) / (2 * N)
        self.s_l = np.arange(n_bar) / (n_bar - 1)
        self.xi = (np.arange(n_bar + 1) - 0.5) / (n_bar - 1)

        self.lambda_kh = self.get_lambda_kh(self.n_bar)

        self.w_k_j = self.get_w_k_j(self.n_bar, self.N)
        self.W_L_k_kh = self.get_W_L_k_kh(self.n_bar, self.N)
        self.W_k_kh = self.get_W_k_kh(self.n_bar, self.N)
        self.W_R_k_kh = self.get_W_R_k_kh(self.n_bar, self.N)

    def Lagrange_j(self, j):
        x = sympy.Symbol("x")
        L_x = 1.0
        for l in range(self.n_bar):
            if l != j:
                L_x *= (x - self.s_l[l]) / (self.s_l[j] - self.s_l[l])
        return sympy.integrate(L_x)

    def get_lambda_kh(self, n_bar):
        lambda_kh = np.ones(n_bar)
        lambda_kh[0] = np.sqrt(1 / 2)
        return lambda_kh

    def get_w_k_j(self, n_bar, N):
        L_j = np.zeros((n_bar, N))
        x = sympy.Symbol("x")
        for j in range(n_bar):
            temp = []
            Lj = self.Lagrange_j(j)
            for k in range(N):
                temp.append(Lj.subs(x, self.x_l[k]))
            L_j[j] = np.array(temp)

        w_k_j = np.zeros((n_bar, N))
        for j in range(n_bar):
            w_k_j[j] = scipy.fftpack.dct(L_j[j], norm="ortho")
        return w_k_j

    def get_W_L_k_kh(self, n_bar, N):
        W_L_k_kh = np.zeros((n_bar - 1, N))
        lambda_kh = self.get_lambda_kh(n_bar)

        for kh in range(n_bar - 1):
            W_L_k_kh[kh] = (
                (1 - n_bar)
                * np.sqrt(2 / N)
                * lambda_kh[kh]
                * np.cos(np.pi * kh * (self.xi[0] + 1))
                * self.w_k_j[0]
            )
        return W_L_k_kh

    def get_W_k_kh(self, n_bar, N):
        W_k_kh = np.zeros((n_bar - 1, N))
        for kh in range(n_bar - 1):
            sum_sin = np.zeros(N)
            for j in range(1, n_bar - 2 + 1):
                sum_sin += np.sin(np.pi * kh * self.s_l[j]) * self.w_k_j[j]

            W_k_kh[kh] = (
                (n_bar - 1)
                * np.sqrt(2 / N)
                * self.lambda_kh[kh]
                * (
                    np.cos(np.pi * kh * self.xi[1])
                    * (self.w_k_j[0] - (-1) ** (kh) * self.w_k_j[n_bar - 1])
                    - 2 * np.sin((np.pi * kh) / (2 * (n_bar - 1))) * sum_sin
                )
            )
        return W_k_kh

    def get_W_R_k_kh(self, n_bar, N):
        W_R_k_kh = np.zeros((n_bar - 1, N))
        for kh in range(n_bar - 1):
            W_R_k_kh[kh] = (
                (n_bar - 1)
                * np.sqrt(2 / N)
                * self.lambda_kh[kh]
                * np.cos(np.pi * kh * (self.xi[n_bar] - 1))
                * self.w_k_j[n_bar - 1]
            )
        return W_R_k_kh
```

```python
def get_F_L_k_horizontal(arr, N, row, col):
    # w
    if col == 0:
#         w_block = np.zeros(N)
        w_block = arr[row, col * N : (col + 1) * N]
    else:
        w_block = arr[row, (col - 1) * N : col * N]

    return w_block
```

```python
def get_F_R_k_horizontal(arr, N, row, col):
    # e
    if col == arr.shape[1] // N - 1:
#         e_block = np.zeros(N)
        e_block = arr[row, col * N : (col + 1) * N]
    else:
        e_block = arr[row, (col + 1) * N : (col + 2) * N]

    return e_block
```

```python
def get_F_L_k_vertical(arr, N, row, col):
    # n
    if row == 0:
#         n_block = np.zeros(N)
        n_block = arr[row * N : (row + 1) * N, col]
    else:
        n_block = arr[(row - 1) * N : row * N, col]

    return n_block
```

```python
def get_F_R_k_vertical(arr, N, row, col):
    # s
    if row == arr.shape[0] // N - 1:
#         s_block = np.zeros(N)
        s_block = arr[row * N : (row + 1) * N, col]
    else:
        s_block = arr[(row + 1) * N : (row + 2) * N, col]

    return s_block
```

```python
# dmlct = DMLCT(n_bar, N)
```

```python
IMG = AIRPLANE
# IMG = ImageLoader(MONO_DIR_PATH + "LENNA.bmp")
```

```python
Fk = np.zeros(IMG.img.shape)
```

# 順変換

## 縦方向

### DCT

```python
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1]):
        eight_points = IMG.img[N * row : N * (row + 1), col]
        c = scipy.fftpack.dct(eight_points, norm="ortho")
        Fk[N * row : N * (row + 1), col] = c
```

### 残差

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1]):
        # ビューなら直接いじっちゃう
        F = Fk[N * row : N * (row + 1), col]
        F_L = get_F_L_k_vertical(Fk, N, row, col)
        F_R = get_F_R_k_vertical(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] -= U_k_n_bar[n_bar - 2 + 1 :]
```

```python
# 0を残す
for k in reversed(range(1, n_bar - 2 + 1)):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1]):
            # ビューなら直接いじっちゃう
            F = Fk[N * row : N * (row + 1), col]
            F_L = get_F_L_k_vertical(Fk, N, row, col)
            F_R = get_F_R_k_vertical(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)

            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] -= U_k_n_bar[k]
```

## 横方向

### DCT

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // N):
        eight_points = Fk[row, N * col : N * (col + 1)]
        c = scipy.fftpack.dct(eight_points, norm="ortho")
        Fk[row, N * col : N * (col + 1)] = c
```

### 残差

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0]):
    for col in range(IMG.img.shape[1] // N):
        F = Fk[row, N * col : N * (col + 1)]
        F_L = get_F_L_k_horizontal(Fk, N, row, col)
        F_R = get_F_R_k_horizontal(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] -= U_k_n_bar[n_bar - 2 + 1 :]
```

```python
# 0を残す
for k in reversed(range(1, n_bar - 2 + 1)):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0]):
        for col in range(IMG.img.shape[1] // N):
            F = Fk[row, N * col : N * (col + 1)]
            F_L = get_F_L_k_horizontal(Fk, N, row, col)
            F_R = get_F_R_k_horizontal(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)

            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] -= U_k_n_bar[k]
```

```python
plt.imshow(Fk)
pd.DataFrame(Fk).to_csv("DMLCT_lenna_hiroya_coef.csv",header=False,index=False)
```

# 係数の確保

```python
Fk_Ori = np.copy(Fk)
```

# 逆変換

```python
recover = np.zeros(IMG.img.shape)
```

## 横方向

### 残差

```python
for k in range(1, n_bar - 2 + 1):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0]):
        for col in range(IMG.img.shape[1] // N):
            F = Fk[row, N * col : N * (col + 1)]
            F_L = get_F_L_k_horizontal(Fk, N, row, col)
            F_R = get_F_R_k_horizontal(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)
            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] += U_k_n_bar[k]
```

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0]):
    for col in range(IMG.img.shape[1] // N):
        F = Fk[row, N * col : N * (col + 1)]
        F_L = get_F_L_k_horizontal(Fk, N, row, col)
        F_R = get_F_R_k_horizontal(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] += U_k_n_bar[n_bar - 2 + 1 :]
```

### IDCT

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // N):
        F = Fk[row, N * col : N * col + N]
        data = scipy.fftpack.idct(F, norm="ortho")
        # Fkに代入した後、縦方向に対して処理
        Fk[row, N * col : N * col + N] = data
```

## 縦方向

### 残差

```python
for k in range(1, n_bar - 2 + 1):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1]):
            # ビューなら直接いじっちゃう
            F = Fk[N * row : N * (row + 1), col]
            F_L = get_F_L_k_vertical(Fk, N, row, col)
            F_R = get_F_R_k_vertical(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)

            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] += U_k_n_bar[k]
```

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1]):
        # ビューなら直接いじっちゃう
        F = Fk[N * row : N * (row + 1), col]
        F_L = get_F_L_k_vertical(Fk, N, row, col)
        F_R = get_F_R_k_vertical(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] += U_k_n_bar[n_bar - 2 + 1 :]
```

### IDCT

```python
for row in range(Fk.shape[0] // N):
    for col in range(Fk.shape[1]):
        F = Fk[N * row : N * (row + 1), col]
        data = scipy.fftpack.idct(F, norm="ortho")
        # 復元画像
        recover[N * row : N * (row + 1), col] = data
```

```python
plt.imshow(recover.astype("u8"), cmap="gray")
```

もどった...！

# 量子化テーブル

```python
Q50_Luminance = np.array(
    [
        [16, 11, 10, 16, 24, 40, 51, 61],
        [12, 12, 14, 19, 26, 58, 60, 55],
        [14, 13, 16, 24, 40, 57, 69, 56],
        [14, 17, 22, 29, 51, 87, 80, 62],
        [18, 22, 37, 56, 68, 109, 103, 77],
        [24, 35, 55, 64, 81, 104, 113, 92],
        [49, 64, 78, 87, 103, 121, 120, 101],
        [72, 92, 95, 98, 112, 100, 103, 99],
    ]
)
```

```python
for i in tqdm_notebook(np.arange(1,10000,1)):
    Q = i / 100
    Q_Luminance = np.copy(Q50_Luminance)
    if Q < 50:
        Q_Luminance = Q_Luminance * 50 / Q
        Q_Luminance = np.where(Q_Luminance > 255, 255, Q_Luminance)
    else:
        Q_Luminance = (100 - Q) * Q50_Luminance / 50
        Q_Luminance = np.where(Q_Luminance < 1, 1, Q_Luminance)
```

# 量子化

```python
Fk = np.copy(Fk_Ori)
Q_Fk = np.zeros(Fk.shape)
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1] // N):
        block = Fk[row * N : (row + 1) * N, col * N : (col + 1) * N]
        # 量子化
        block = np.round(block / Q_Luminance)
        # 逆量子化
        block = block * Q_Luminance
        Q_Fk[row * N : (row+1)*N, col * N : (col+1)*N] = block
```

```python
Fk = np.copy(Q_Fk)
Q_recover = np.zeros(Q_Fk.shape)
```

## 横方向

### 残差

```python
for k in range(1, n_bar - 2 + 1):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0]):
        for col in range(IMG.img.shape[1] // N):
            F = Fk[row, N * col : N * (col + 1)]
            F_L = get_F_L_k_horizontal(Fk, N, row, col)
            F_R = get_F_R_k_horizontal(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)
            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] += U_k_n_bar[k]
```

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0]):
    for col in range(IMG.img.shape[1] // N):
        F = Fk[row, N * col : N * (col + 1)]
        F_L = get_F_L_k_horizontal(Fk, N, row, col)
        F_R = get_F_R_k_horizontal(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] += U_k_n_bar[n_bar - 2 + 1 :]
```

### IDCT

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // N):
        F = Fk[row, N * col : N * col + N]
        data = scipy.fftpack.idct(F, norm="ortho")
        # Fkに代入した後、縦方向に対して処理
        Fk[row, N * col : N * col + N] = data
```

## 縦方向

### 残差

```python
for k in range(1, n_bar - 2 + 1):
    dmlct = DMLCT(k+1, N)
    for row in range(IMG.img.shape[0] // N):
        for col in range(IMG.img.shape[1]):
            # ビューなら直接いじっちゃう
            F = Fk[N * row : N * (row + 1), col]
            F_L = get_F_L_k_vertical(Fk, N, row, col)
            F_R = get_F_R_k_vertical(Fk, N, row, col)

            U_k_n_bar = np.zeros(N)

            for kh in range((k + 1) - 2 + 1):
                U_k_n_bar += (
                    F_L[kh] * dmlct.W_L_k_kh[kh]
                    + F[kh] * dmlct.W_k_kh[kh]
                    + F_R[kh] * dmlct.W_R_k_kh[kh]
                )

            F[k] += U_k_n_bar[k]
```

```python
dmlct = DMLCT(n_bar, N)
for row in range(IMG.img.shape[0] // N):
    for col in range(IMG.img.shape[1]):
        # ビューなら直接いじっちゃう
        F = Fk[N * row : N * (row + 1), col]
        F_L = get_F_L_k_vertical(Fk, N, row, col)
        F_R = get_F_R_k_vertical(Fk, N, row, col)

        U_k_n_bar = np.zeros(N)
        for kh in range(n_bar - 2 + 1):
            U_k_n_bar += (
                F_L[kh] * dmlct.W_L_k_kh[kh]
                + F[kh] * dmlct.W_k_kh[kh]
                + F_R[kh] * dmlct.W_R_k_kh[kh]
            )

        # n_bar = 4 なら 0,1,2は残す 3,4,5,6,7を書き換える
        F[n_bar - 2 + 1 :] += U_k_n_bar[n_bar - 2 + 1 :]
```

### IDCT

```python
for row in range(Fk.shape[0] // N):
    for col in range(Fk.shape[1]):
        F = Fk[N * row : N * (row + 1), col]
        data = scipy.fftpack.idct(F, norm="ortho")
        # 復元画像
        Q_recover[N * row : N * (row + 1), col] = data
```

```python
Q_recover = np.round(Q_recover)
```

```python
plt.imshow(Q_recover, cmap="gray")
plt.imsave("DMLCT_8x8_n"+str(n_bar)+ "_LENNA.png",Q_recover,cmap="gray")
```

# 情報量

```python
qfk = pd.Series(Q_Fk.flatten())
pro = qfk.value_counts() / qfk.value_counts().sum()
pro.head()
```

     0.0     0.961128
    -50.0    0.002392
     50.0    0.002209
    -46.0    0.002056
     46.0    0.001724
    dtype: float64

```python
S = 0
for pi in pro:
    S -= pi * np.log2(pi)
S
```

    0.45178635772331066

# PSNR

```python
MSE = np.sum(np.sum(np.power((IMG.img - Q_recover),2)))/(Q_recover.shape[0] * Q_recover.shape[1])
PSNR = 10 * np.log10(255 * 255 / MSE)
PSNR
```

    31.395338973745677

# MSSIM

```python
MSSIM = ssim(IMG.img,Q_recover.astype(IMG.img.dtype),gaussian_weights=True,sigma=1.5,K1=0.01,K2=0.03)
MSSIM
```

    0.8489789251506287

```python
dmlct = DMLCT(n_bar, N)
```

# MSDS

```python
MSDSt, MSDS1, MSDS2 = msds(N,Q_recover)
```

```python
MSDS1
```

    5847.0750732421875

```python
MSDS2
```

    2063.2566528320312

```python

```


# 状態空間モデルとはなにか

- 「様々な分析手法を統合したフレームワーク」
- 「Box-Jenkins法と比べて属人性が高い」
- 「目に見えない【状態】があることを過程した時系列モデルの1種」
- 「状態」と「観測値」を切り分けることによって人間が理解しやすい形でデータを表現する

## 状態空間モデルのデータの表現

状態空間モデルは**状態方程式** と **観測方程式** でデータを表現する。

- 状態方程式: 状態の変化のプロセスを記述
- 観測方程式: 状態から観測地が得られるプロセスを記述

日本語で表現すると次のようになる。

$
\begin{align}
    状態 &= 前時点の状態を用いた予測値 + 過程誤差　\\
    観測地 &= 状態 + 観測誤差
\end{align}
$

## パラメタ推定

データの表現方法とパラメタ推定を分けて考えること。
パラメタ推定方法は大きく2つある。
- 〇〇フィルタ(フィルタリング系)
    - カルマンフィルタ+最尤法、散漫カルマンフィルタ
* MCMC
    - ベイズ推論 + HMC法

## パラメタ推定: カルマンフィルタと最尤法

<デメリット>
- **正規分布に従い、かつ線形なデータにしか適用できない**
    - **線形ガウス状態空間モデル** または **動的線形モデル(Dynamic Linear Models : DLM)** と呼ばれる

<メリット>
- MCMCと比べると計算は比較的容易
- パラメタ推定と状態の推定を分けることができる。
    - 一度パラメタさえ決まれば、データが手にはいるたび、逐次的に状態の推定や予測を行うことができる。

## パラメタ推定: ベイズ推論とHMC法

<デメリット>
- 計算不可、属人性が高く分析に手間と時間がかかる。
- 状態の推定とパラメタの推定を同時に行うため、逐次的な処理は苦手。
- Stanというソフトを習熟する必要がある。

<メリット>
- 非正規分布、非線形のデータにも適用可能
- Stanを使ってデータ生成過程を直接コードに書き下すことができる


# **Introduction**

In this tutorial we will implement an agent in a sequential decision making task using model-free and model-based approaches. To begin we will introduce the notion of contextual-bandits, which provide an intermediate step between the bandit problem we discussed in the previous tutorial and the full reinforcement learning problem.

# **Part I: Introduction**

**Contextual bandits:**

- Suppose there are several different k-armed bandit tasks, and that on each step you confront one of these chosen at random. Thus, the bandit task changes randomly from step to step. This would appear to you as a single, nonstationary k-armed bandit task whose true action values change randomly from step to step.
- Now suppose, that when a bandit task is selected, you are given some distinctive clue about its identity (but not its action values). Maybe you are facing an actual slot machine that changes the color of its display as it changes its action values. Now you can learn a policy associating each context with the best action to take when facing that context.
- This is an example of an associative search task and is often called contextual bandits.
- Learning value functions in these task requires the agent to maintain a representation of both the contexts (which we will refer to as a state) and the actions associated with them.
- State-action value functions for contextual bandits are defined and updated in the same way as those in the bandit task.

**Definitions:**

State-action value function:

\begin{align}
q (s, a) = \mathbb{E} [r_{t} | s_{t} = s,a_{t} = a]
\end{align}

State-action value updates:

\begin{align}
q(s_{t}, a_{t}) \leftarrow q(s_{t}, a_{t}) + \alpha (r_{t} - q(s_{t}, a_{t}))
\end{align}

**Please run the following code for your implementation:**

```
% matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from pylab import *
import copy
```

```
class drifting_probabilitic_bandit():
    """
    World: 2-Armed bandit.
    Each arm returns reward with a different probability.
    The probability of returning rewards for all arms follow Gaussian random walks.
    """

    def __init__(self, arm_number, drift):
        self.name = "n_armed_bandit"
        self.n_states = 1
        self.n_actions = arm_number
        self.dim_x = 1
        self.dim_y = 1

        self.mu_min = 0.25
        self.mu_max = 0.75
        self.drift = drift

        self.reward_mag = 1

        self.mu = [np.random.uniform(self.mu_min, self.mu_max) for a in range(self.n_actions)]

    def update_mu(self):
        self.mu += np.random.normal(0, self.drift, self.n_actions)
        self.mu[self.mu > self.mu_max] = self.mu_max
        self.mu[self.mu < self.mu_min] = self.mu_min

    def get_outcome(self, state, action):

        self.update_mu()
        self.rewards = [self.reward_mag if np.random.uniform(0,1) < self.mu[a] else 0 for a in range(self.n_actions) ]
        next_state = None

        reward = self.rewards[action]
        return int(next_state) if next_state is not None else None, reward
```

```
class contextual_bandits(drifting_probabilitic_bandit):

    def __init__(self):

        self.name = "contextual_bandit"
        self.n_states = 3
        self.n_actions = 2
        self.dim_x = 1
        self.dim_y = 1

        self.n_arms = self.n_actions
        self.n_of_bandits = self.n_states
        self.drift = 0.02
        self.bandits = [drifting_probabilitic_bandit(self.n_arms, self.drift) for n in range(self.n_of_bandits)]

    def get_outcome(self, state, action):

        _, reward = self.bandits[state].get_outcome(0,action)
        available_states = [s for s in range(self.n_of_bandits) if s != state]
        next_state = np.random.choice(available_states)

        return int(next_state) if next_state is not None else None, reward
```

```
def e_greedy_policy(q, epsilon):
    if np.random.uniform(0,1) > epsilon:
        return np.argmax(q)
    else:
        # choose randomly from all but the highest q value action
        a_list = np.arange(len(q))
        arg_max = np.argmax(q)
        choices = np.delete(a_list, arg_max)
        return np.random.choice(np.delete(a_list, arg_max))
```

# **Exercise 1:**

Train an agent to estimate the state-action value function for the contextual bandit task. The task has three contexts or states, each allows a two-arm bandit choice. On each step, the environment will provide a context and the agent is required to select an action. Select actions using an $\epsilon$-greedy policy.

Plot performance curves for each of the bandits in each of the contexts.

```

```

# **Part II: Introduction**

We will now extend the contextual bandit task to a sequential decision making task which provides a simple, yet very interesting environment that captures many aspects of the full reinforcement learning problem. The state-action value function in this case not only accounts for immediate rewards, but also future rewards. Also, actions in this environment allow the agent to influence the next state it encounters.

We will train an agent to solve this task using, both, model-free and model based strategies. For the model-free case, at each step the agent updates its estimate of its state-action value function using TD-learning.

For the model-based case, on each step, we update a state transition matrix using a state-prediction error and an estimate of expected reward from each end state using the same update rule for the SARSA implementation. Finally, we explicitly compute the model-based state-action value function $Q_{MB}$ using the state transition matrix and expected rewards from the end states.

**Daw two-step task description:**
- The task is a two-stage Markov decision task in which, on each trial, an initial choice between two options probabilistically leads to either of two, second-stage ‘‘states’’.
- In turn, these second stage states demand another two-option choice, each of which is associated with a different chance of delivering a reward.
- The choice of one first-stage option leads predominantly (70% of the time) to one of the two second-stage states, and this relationship is fixed.
- The chances of payoff associated with the four second-stage options, however, change slowly and independently, according to Gaussian random walks.

**Definitions:**

For the general case, we define a state-action value function as:

\begin{align}
Q (s, a) = \mathbb{E} [r_{t} + \gamma r_{t+1} + \gamma^2 r_{t+2} + ... | s_{t} = s,a_{t} = a]
\end{align}

\begin{align}
= \mathbb{E} [r_{t} + \gamma Q(s', a') | s_{t} = s,a_{t} = a]
\end{align}

State-action value updates (SARSA):

\begin{align}
Q(s_{t}, a_{t}) \leftarrow Q(s_{t}, a_{t}) + \alpha (r_{t} + \gamma Q(s_{t+1},a_{t+1}) - Q(s_{t}, a_{t}))
\end{align}

Soft-max policy:

\begin{align}
P(a_{t}|s_{t}) = \frac{ e^{\beta Q(s_{t}, a_{t})} }{ \sum_{b_{t}} e^{\beta Q(s_{t}, b_{t})} }
\end{align}

State transition matrix:

\begin{align}
T(s,a,s') = P(s_{t+1} = s' | s_{t} = s, a_{t} = a)
\end{align}

Model-based state-action value function:

\begin{align}
Q_{MB}(s,a) =  r_{t} + \sum_{s'} T(s,a,s') \: \text{max}_{a'} Q(s',a')
\end{align}

**Please run the following code for your implementation:**

```
class Daw_two_step_task(drifting_probabilitic_bandit):

    def __init__(self):

        self.name = "Daw_two_step_task"
        self.n_states = 3
        self.n_actions = 2
        self.dim_x = 1
        self.dim_y = 1

        self.n_arms = self.n_actions
        self.n_of_bandits = 2
        self.drift = 0.02

        self.context_transition_prob = 0.7
        self.bandits = [drifting_probabilitic_bandit(self.n_arms, self.drift) for n in range(self.n_of_bandits)]

    def get_outcome(self, state, action):

        if state == 0:
            reward = 0
            if action == 0:
                if np.random.uniform(0,1) < self.context_transition_prob:
                    next_state = 1
                else:
                    next_state = 2
            elif action == 1:
                if np.random.uniform(0,1) < self.context_transition_prob:
                    next_state = 2
                else:
                    next_state = 1
            else:
                print('No valid action specified')

        if state == 1:
            _, reward = self.bandits[0].get_outcome(0, action)
            next_state = 0

        if state == 2:
            _, reward = self.bandits[1].get_outcome(0, action)
            next_state = 0

        return int(next_state) if next_state is not None else None, reward
```

```
def softmax(state, q, beta):
    """
    Softmax policy: selects action probabilistically depending on the value.
    Args:
        state: an integer corresponding to the current state.
        q: a matrix indexed by state and action.
        params: a dictionary containing the default parameters.
    Returns:
        an integer corresponding to the action chosen according to the policy.
    """

    value = q[state,:]
    prob = exp(value * beta) # beta is the inverse temperature parameter
    prob = prob / sum(prob)  # normalize
    cum_prob = cumsum(prob)  # cummulation summation
    action = where(cum_prob > rand())[0][0]
    return action
```

# **Exercise 2:**

Implement the two-step task using a model-free strategy. To do so, train an agent on the above two-step task using SARSA updates. Run 3000 trials. Use a constant learning rate $\alpha = 0.2$ and a soft-max policy using an inverse temperature parameter of $\beta = 4$.

1. For a single run, plot the reward probability associated with each of the second stage choices on each trial, the q-values learnt by the agent for all second state actions and the q-values of the two first stage actions.
2. Vary parameters $\alpha$ and $\beta$. How do these parameters influence the agent's estimates?
3. Each trial can be characterised by whether or not reward was received and whether the second-stage presented was common or rare given the first-stage choice. For each of these trial types, plot the probability of repeating the first-stage choice on the next trial.
4. How do the parameters $\alpha$ and $\beta$ affect the probabilities in (3)?

```

```

# **Exercise 3:**

We will now implement the two-step task using a model-based strategy. To do so, estimate a state transision matrix $T(s,a,s')$ and expected reward from each end state-action pair $Q(s,a)$. Initialise the transition matrix to have uniform probabilities of transitioning to all possible next states. Run 3000 trials. Use constant learning rates $\alpha = 0.2$ and a soft-max policy using an inverse temperature parameter of $\beta = 4$.

1. Plot the expected reward from each of the second stage choices on each trial and the actual reward obtained by the agent.
2. Vary parameters $\alpha$ and $\beta$. How do these parameters influence the agent's performance?
3. For each of the four trial types discussed in the previous exercise, plot the probability of repeating the first-stage choice on the next trial when using a model-based strategy.
4. How do the parameters $\alpha$ and $\beta$ affect the probabilities in (3)?
5. How does the agent's performance using the model-based strategy compare with that using a model-free strategy? How do the stay probabilities differ when using a model-based vs model-free strategy?

```

```

# **Exercise 4:**

We will now implement an agent that is a hybrid version of the model-free and model-based agents. Simultaneously train a model based and model free agent (as above). Run 5000 trials. Choose actions using a soft-max policy that uses a weighted combination of the model-based and model-free Q-values. Test for $w_{1} \in [0.3, 0.6]$ and $w_{2} = 1- w_{1}$.

1. Plot the expected reward from each of the second stage choices on each trial and the actual reward obtained by the agent.
2. For each of the four trial types discussed in the previous exercise, plot the probability of repeating the first-stage choice on the next trial when using the hybrid strategy.
3. Often, subjects are tested on this task and their stay probabilities are fit using such a hybrid model. The relative magnitude of the weights $w(1)$ and $w(2)$ is often taken to indicate how much they rely on model-based vs model-free strategies for behavioural control. Under what assumptions does this task allow us to do so? Hint: what components in your model simulation were required for you to be able to reproduce the profiles of stay-probabilities associated with model-based and model-free control?

```

```


## The Bias Variance Trade-off

### Packages Versions

```python
import sys
import sklearn
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt

%matplotlib inline

from sklearn import metrics
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold
from sklearn.preprocessing import MinMaxScaler, PolynomialFeatures
from sklearn.metrics import roc_auc_score, roc_curve

print('The Python version is {}.\n'.format(sys.version))
print('The Numpy version is {}.\n'.format(np.__version__))
print('The Pandas version is {}.\n'.format(pd.__version__))
print('The Matplotlib version is {}.\n'.format(mpl.__version__))
print('The Scikit-Learn version is {}.\n'.format(sklearn.__version__))
```

    The Python version is 3.8.5 (default, Jul 28 2020, 12:59:40)
    [GCC 9.3.0].

    The Numpy version is 1.19.1.

    The Pandas version is 1.1.0.

    The Matplotlib version is 3.3.3.

    The Scikit-Learn version is 0.24.0.

### Logistic Regression Coefficients and Intercept Estimation

The **parameters** of a Logistic Regression model reflect the best approximation to the data. The process of finding this best approximation is called **estimation**. To estimate the best parameters the model uses a concept of **cost function**, which is a way of telling how far the model prediction are from perfectly describing the data.

In classification models there are different cost functions, being their particularities dependent on the model itself. Specifically in fitting logistic regression models, a typical cost function is the **log-loss** function (also called **cross entropy loss**):

>$ \text{LL} = \frac{1}{n}\sum_{i=1}^{n} - (y_{i}\log(p_{i}) + (1 - y_{i})\log(1 - p_{i}))$

Where $n$ is the training samples number, $y_{i}$ is the ture label (0 or 1) of the *i-th* sample, $p_{i}$ is the predicted probability $\frac{1}{1 + e^{-X}}$ that the *i-th* samples equals 1, and $\log$ is the natural logarithm.

The summation notation over all the training samples and the division by $n$ serve to take the average of this cost function over all training samples.

Let's examine the log-loss function arguments. First, the natural logarithm distribution:

```python
X_log = np.linspace(0.01, 0.99, 99)
y_log = np.log(X_log)

print(X_log[:5], '...', X_log[-4:])
print(y_log[:2], '...', y_log[-2:])

plt.plot(X_log, y_log)
plt.title('Natural Logarithm')
```

So, for a sample with positive class (in other words, with *y = 1*) the $(1 - y_{i})$ term equals zero and the cost function is reduced to:

> $ \text{LL} = \frac{1}{n}\sum_{i=1}^{n} - (\log(p_{i}))$

And the cost function is the negative of the natural logarithm of the predicted probability $p_{i}$. If this sample is correctly classified and the probability to be a positive class be close to 1, the $\log(p_{i})$  term will be close to 0. If, on the other hand, this sample is incorreclty predicted with a small $p_{i}$ value, the log loss will be high and therefore increase the overall sum.

For a sample with negative class (in other words, with *y = 0*) the $y_{i}\log(p_{i})$ term equals zeor and the cost function is reduced to:

> $ \text{LL} = \frac{1}{n}\sum_{i=1}^{n} - (\log(1 - p_{i}))$

And therefore, a similar behaviour occurs for the log loss value when the model predict a negative sample incorrectly. The $p_{i}$ will be high and the term $(1 - p_{i})$ will be low;

#### Gradient Descent

To find the best parameters, the a problem of **optimization** is set using the cost function. The **gradient descent** is the method used to make the **estimations**. It is an iterative algorithm that randomly attributes initial parameters values and iterates over the *error surface* - that is, the distribution of the **log loss** for different samples parameters - to find the least virtually cost. The gradient descent is calculated as:

> $x_{new} = x_{old} - f'(x_{old}) * \alpha$

Where $\alpha$ is the learning rate.

The new parameters values is based in the current guess $x_{old}$ and the derivative of it, $f'(x_{old})$, times a learning rate, being the learning rate a *step* to a *descent* - towards the local minimum - process.

So, for the Logistic Regression using the log loss cost function with just one feature, the first derivative result in (after some math that is omitted here):

> $\text{LL}' = \frac{1}{n} (y_{i} - p_{i}) * X_{j}$

Where $X_{j}$ is the is the input samples for the j-th input feature.

Let's simulate the process of finding the best parameters using synthetic data. The data will have only one feature, called **feat_1**, that is in fact a third degree polynomial:

```python
np.random.seed(seed=1)

X_poly = np.linspace(-4, 3, 101)

X_rand = np.random.uniform(0, 20, (101,))
Y_poly = X_poly ** 3 + X_poly ** 2 + X_poly + 3
Y_dist = X_poly ** 3 + X_poly ** 2 + X_poly + 3 + X_rand

y_true = X_poly >= 0

plt.plot(X_poly, Y_poly, 'r--')
plt.scatter(X_poly, Y_dist, s=10)
plt.ylabel('y')
plt.xlabel('x')
plt.title('Third degree polynomial with Normal noise')
plt.legend(['3rd degree poly', '3rd degree poly w/ noise'])
```

Let's create a DataFrame object:

```python
df = pd.DataFrame({'feat_1': Y_dist, 'response': y_true})
```

And create train and tests sets:

```python
X_train, X_test, y_train, y_test = train_test_split(
    df.feat_1, df.response, test_size=0.2, random_state=24)
print(X_train.shape)
print(X_test.shape)
```

    (80,)
    (21,)

Now, let's declare the logistic regression model with the training data:

```python
model = LogisticRegression()
model.fit(np.array(X_train).reshape(-1, 1), np.array(y_train))
```

    LogisticRegression()

Let's map the true values for the train data that will be used in the log loss functions and gradient functions:

```python
y_train_true = np.array(y_train.map({True:1, False:0}))
```

Let's see the coefficients and the intercept paramenters of the model:

```python
model.coef_, model.intercept_
```

    (array([[0.22758664]]), array([-3.6139925]))

```python
intercept_and_coefs = np.concatenate([model.intercept_.reshape(1,1), model.coef_], axis=1)
intercept_and_coefs
```

    array([[-3.6139925 ,  0.22758664]])

Now, let's create some functions that will allow the parameter estimation manually:

```python
def sigmoid(X):
    """
    Calculates the Sigmoid values of an array input.

    Arguments:
        X: Numpy array or DataFrame object
    Returns:
       Numpy array
    """
    return 1/(1 + np.exp(-X))

def linear_comb(X, Y):
    """
    Calculates the Linear Combination of two arrays (or pandas objects)

    Arguments:
        X, Y: Numpy arrays or DataFrame objects
    Returns:
        A Numpy array
    """
    assert X.shape[1] == Y.shape[0]
    return np.dot(X, Y)

def feature_array(X):
    """
    Make an array of features, with a column of 1s added, using horizontal stack

    Arguments:
        X: Numpy array or Dataframe object
    Returns:
        Numpy array
    """
    if len(X.shape) == 1:
        n = X.shape[0]
    else:
        n = X.shape[1]

    return np.hstack([np.ones((n, 1)),
                      np.array(X).reshape(-1,1)])
```

Let's create also a dataframe with the coefficients for each epoch. This will allow to track each iteration of the estimation process:

```python
coefs = pd.DataFrame({'epochs': [0], 'theta_0': [0], 'theta_1': [0], 'cost': [np.inf]})
coefs
```

      epochs
      theta_0
      theta_1
      cost

      0
      0
      0
      0
      inf

Let's define the log loss function, the log loss gradient and the probability function:

```python
def probabilities(X, Y):
    """
    Calculates the Logistic Regression positive class probabilities

    Arguments:
        X, Y: Numpy array
    Returns:
        Numpy array
    """
    return sigmoid(linear_comb(X, Y.T))

def log_loss(Y, T, X):
    """
    Estimates the log loss cost function.

    Arguments:
       Y, T, X: Numpy Array
    Returns:
       Numpy Array
    """
    n = len(Y)
    p = probabilities(X, T)

    total_cost = -(Y * np.log(p) + (1 - Y) * np.log(1 - p))

    return np.sum(total_cost) / n

def log_loss_gradient(Y, T, X):
    """
    Calculates the log loss gradient.

    Arguments:
        Y, T, X: Numpy Array
    Returns:
        Numpy Array
    """
    p = -(Y - probabilities(X, T))

    return (1/len(X)) * linear_comb(p.T, X)
```

For each epoch, we will save the values obtained in the estimation process in the coefs dataframe:

```python
coefs = pd.DataFrame({'epochs': [0], 'theta_0': [0], 'theta_1': [0], 'cost': [np.nan]})
learning_rate = .05

for i in range(1, 1000):
    old = np.array(coefs[['theta_0', 'theta_1']].loc[i-1]).reshape(1,2)
    gradient = log_loss_gradient(y_train_true.reshape(-1, 1),
                                 old,
                                 feature_array(X_train))
    new = old - (gradient * learning_rate)
    cost = log_loss(y_train_true.reshape(-1, 1),
                    new,
                    feature_array(X_train))

    coefs = coefs.append({'epochs': i,
                          'theta_0': new[0][0],
                          'theta_1': new[0][1],
                          'cost': cost}, ignore_index=True)
```

```python
coefs
```

      epochs
      theta_0
      theta_1
      cost

      0
      0.0
      0.000000
      0.000000
      NaN

      1
      1.0
      -0.005625
      0.286111
      1.121069

      2
      2.0
      -0.022261
      0.119162
      0.623601

      3
      3.0
      -0.036138
      0.011686
      0.630216

      4
      4.0
      -0.042604
      0.227193
      0.917138

      ...
      ...
      ...
      ...
      ...

      995
      995.0
      -2.709599
      0.176171
      0.312043

      996
      996.0
      -2.710446
      0.176218
      0.312029

      997
      997.0
      -2.711293
      0.176265
      0.312015

      998
      998.0
      -2.712138
      0.176312
      0.312000

      999
      999.0
      -2.712982
      0.176359
      0.311986

1000 rows × 4 columns

Lets plot the log loss cost function for this synthetic data:

```python
plt.plot(coefs.epochs, coefs.cost)
plt.ylabel('Cost Function')
plt.xlabel('Epochs')
plt.title('Logistic Regression Training')
```

It is possible to see from the above figure that the log loss function stoped at a minimum. In the beggining, the log loss values were somewhat erratic, but then it smoothed after 200 iterations.

Lets verify the log loss for the result parameters and compare with the LogisticRegression model:

```python
mask = coefs.cost == coefs.cost.min()
coefs[mask]
```

      epochs
      theta_0
      theta_1
      cost

      999
      999.0
      -2.712982
      0.176359
      0.311986

```python
print('Logistic Regression model intercept and coefficient: {}'.format(intercept_and_coefs))
print('Manual Regression model intercept and coefficient: {}'.format(coefs[['theta_0', 'theta_1']][mask].values))
```

    Logistic Regression model intercept and coefficient: [[-3.6139925   0.22758664]]
    Manual Regression model intercept and coefficient: [[-2.71298235  0.17635905]]

Not bat, pretty close. Let's compare the log loss cost function:

```python
model_cost = log_loss(y_train_true.reshape(-1, 1),
                      intercept_and_coefs,
                      feature_array(X_train))

print('Logistic Regression model cost: {}'.format(model_cost))
print('Manual Regression model cost: {}'.format(coefs.cost[mask].values[0]))
```

    Logistic Regression model cost: 0.304990771795166
    Manual Regression model cost: 0.31198596238577425

As it is possible to see, the manual model cost for the final estimation parameters is pretty close to the scikit-learn model.

```python
X_man = linear_comb(feature_array(df.feat_1.sort_values()),
                    np.array(coefs[['theta_0', 'theta_1']].loc[999]).reshape(1,2).T)
Y_man = sigmoid(X_man)

X = linear_comb(feature_array(df.feat_1.sort_values()),
                intercept_and_coefs.T)
Y = sigmoid(X)

plt.scatter(df.feat_1, df.response, s=10, c='y', marker='x')
plt.plot(X, Y, 'r--')
plt.plot(X, Y_man,)
plt.ylabel('response variable')
plt.xlabel('feat_1')
plt.legend(['Manual Model', 'Scikit-learn Model', 'feat_1'])
```

### Assumptions of Logistic Regression

Since it is a classical statistical model, similar to F-test and Pearson correlation, logistic regression makes certain assumptions about the data. It is good to be aware of them, though in practice a model could be applied in less stricty situations.

**Features are linear in the log odds**

As Logistic Regression is a linear model (see details-of-logistic-regression-and-feature-exploration file), it is expected that the features describe a linear trend in the log odds. If not, as is the case with **pay_1** feature, the algorithm would not perform well.

**No Multicollinearity of Features**

Multicollinearity means that features are correlated with each other. The worst violation of this assumption is when features are perfectly correlated with each other (e.g. they are identical or multiplied by a scalar). In the study case, the **bill_amt_i** features are highly correlated between themselves, though they are not perfectly correlated. So, while these features may not contribute much independent information, let's keep them.

**The Independence of Observations**

This is a common assumption in classical statistical models, including linear regression. In this case study the samples are asssumed to be independent as represent accounts that are individual and unique (there are no accounts for the same person). So there is independence of observations in this respect.

**No outliers**

Outliers are observations where the value of the feature(s) or response are very far from most of the data, or are different in some other way. They are capable of affect linear models in general, because weights to much a parameter for both too high or too low. There were no outliers observed in this case study.

**Feature Numbers**

This is not an assumption, as it is more of a guidance on model building. Too many features can "confuse" the model. There is a common rule of thumb known as "rule of 10" which states that for every 10 occurrences of the rarest outcome class, 1 feature may be added to the model. For example, in a binary logistic regression problem with 100 samples, if the class balance is 20% positive, then are only 20 positive outcomes in total, and so only 2 features should be used in the model. There is also a "rule of 20" which is more stringent than the "rule of 10".

### Motivation for Regulatization

Regularization is a technique that contrains (**regularize**) or **shrinks** the model coefficients, balancing the model between complexity or flexibility on the feature relationships. Common terminology in machine-learning extends the regularization techniques into the following concepts:

- Overfitting
- Underfitting
- Bias-Variance trade-off

A model is said to have high **variance** when it captures very well the existing variability in the training set, showing high training scores. However, when such a model is then used to make predictions on unseen data, the performance is substantially lower compared to the training scores.

### Lasso (L1) and Rigde (L2) Regularization

The two ways of regularizing logistic regression models in scikit-learn are called lasso (also known as L1 regularization) and ridge (also known as L2 regularization). When instantiating the model object from the scikit-learn class, you can choose either penalty = 'l1' or 'l2'.

Lasso and ridge regularization use different mathematical formulations to accomplish this goal. These methods work by making changes to the cost function that is used for model fitting, which we introduced previously as the log-loss function. Lasso regularization uses what is called the 1-norm (hence the term L1):

$$
\begin{equation}
\begin{split}
LL_{lasso} = \sum_{j=1}^{m} |\sigma_{j}| + \frac{C}{n} \sum_{i=1}^{n} - (y_{i}log(p_{i}) + (1-y_{i})log(1 - p_{i}))
\end{split}
\end{equation}
$$

The one norm, $\sum_{j=1}^{m} |\sigma_{j}|$ is just the sum of the absolute values of all the coefficients of the $m$ different features. The absolute value is used because having a coefficient be large in either the positive or negative directions can contribute to overfitting. $C$ is a factor that is multiplied by the numerator of the fraction in front of the sum of the log-loss function over all the n training samples. This is the "inverse of regularization strength".

In short, *larger values of C lead to less regularization in the scikit-learn implementation.*

L2, or ridge regularization, is similar to L1, except that instead of the sum of absolute values of coefficients, ridge uses the sum of their squares, called the 2-norm:

$$
\begin{equation}
\begin{split}
LL_{rigde} = \sum_{j=1}^{m} \sigma_{j}^2 + \frac{C}{n} \sum_{i=1}^{n} - (y_{i}log(p_{i}) + (1-y_{i})log(1 - p_{i}))
\end{split}
\end{equation}
$$

There is also an additional regularization method called **elastic net**, which is a combination of both the lasso and ridge.

#### Their differences

The L1 penalty also performs feature selection, in addition to regularization. It does this by setting some coefficient values to exactly zero during the regularization process, effectively removing features from the model. L2 regularization makes the coefficient values smaller but does not completely eliminate them.

#### Scaling and Regularization

As noted previously, it is best practice to scale the data so that all the features have roughly the same range of values before using regularization. This is because the coefficients are all going to be subject to the same penalty in the cost function. Normalizing all the features before using regularization avoids complications like this that arise simply from differences in scale.

Lets apply the concepts learned in the study case dataset:

### Loading the Data

Load the data and subset columns list to features:

```python
df = pd.read_csv('data/default_of_credit_card_clients_cleaned.csv')

features_response = df.columns.tolist()
to_remove = ['id', 'sex'] + ['pay_{}'.format(i) for i in range(2,7)] + features_response[-6:]

features_response = [feature for feature in features_response if feature not in to_remove]
features_response
```

    ['limit_bal',
     'education',
     'marriage',
     'age',
     'pay_1',
     'bill_amt1',
     'bill_amt2',
     'bill_amt3',
     'bill_amt4',
     'bill_amt5',
     'bill_amt6',
     'pay_amt1',
     'pay_amt2',
     'pay_amt3',
     'pay_amt4',
     'pay_amt5',
     'pay_amt6']

```python
X = df[features_response].values
```

```python
len(features_response)
```

    17

And create train and test sets:

```python
X_train, X_test, y_train, y_test = train_test_split(X,
                                                    df['default_payment_next_month'].values,
                                                    test_size=0.2,
                                                    random_state=24)
```

### Pipelines

Before using any of the regularization described above, create a pipeline that will abstract all the desired transformations to the data. First, let us declare the scaler object:

```python
min_max_sc = MinMaxScaler()
```

And the model object:

```python
lr = LogisticRegression(solver='saga', penalty='l1', max_iter=1000)
```

Now, let us encapsulate into the Pipeline:

```python
scale_lr_pipeline = Pipeline(steps=[
    ('scaler', min_max_sc),
    ('model', lr),
])

scale_lr_pipeline.get_params()
```

    {'memory': None,
     'steps': [('scaler', MinMaxScaler()),
      ('model', LogisticRegression(max_iter=1000, penalty='l1', solver='saga'))],
     'verbose': False,
     'scaler': MinMaxScaler(),
     'model': LogisticRegression(max_iter=1000, penalty='l1', solver='saga'),
     'scaler__clip': False,
     'scaler__copy': True,
     'scaler__feature_range': (0, 1),
     'model__C': 1.0,
     'model__class_weight': None,
     'model__dual': False,
     'model__fit_intercept': True,
     'model__intercept_scaling': 1,
     'model__l1_ratio': None,
     'model__max_iter': 1000,
     'model__multi_class': 'auto',
     'model__n_jobs': None,
     'model__penalty': 'l1',
     'model__random_state': None,
     'model__solver': 'saga',
     'model__tol': 0.0001,
     'model__verbose': 0,
     'model__warm_start': False}

Set the Regularization coefficient to 2:

```python
scale_lr_pipeline.set_params(model__C=2.0)
```

    Pipeline(steps=[('scaler', MinMaxScaler()),
                    ('model',
                     LogisticRegression(C=2.0, max_iter=1000, penalty='l1',
                                        solver='saga'))])

Let's verify the performance of the model for different regularization values. The performance will be applied to different folds created in the train set. The following function abstracts the interaction for each fold and C value:

```python
C_val_exponents = np.linspace(2, -3, 6)
C_vals = np.float(10) ** C_val_exponents

def cross_val_C_search(k_folds, C_vals, pipeline, X, Y):

    n_folds = k_folds.n_splits
    cv_train_roc_auc = np.empty((n_folds, len(C_vals)))
    cv_test_roc_auc = np.empty((n_folds, len(C_vals)))
    cv_test_roc = [[]]*len(C_vals)

    for c_val_counter in range(len(C_vals)):

        # Set the C value for the model object in the Pipeline
        pipeline.set_params(model__C = C_vals[c_val_counter])

        # Count folds for each value of C
        fold_counter = 0

        # Get training and testing indices for each fold
        for train_index, test_index in k_folds.split(X, Y):

            # Subset the features and response, for training and testing data for
            # this fold
            X_cv_train, X_cv_test = X[train_index], X[test_index]
            y_cv_train, y_cv_test = Y[train_index], Y[test_index]

            # Fit the model on the training data
            pipeline.fit(X_cv_train, y_cv_train)

            # Get the training ROC AUC
            y_cv_train_predict_proba = pipeline.predict_proba(X_cv_train)
            cv_train_roc_auc[fold_counter, c_val_counter] = \
            roc_auc_score(y_cv_train, y_cv_train_predict_proba[:, 1])

            # Get the testing ROC AUC
            y_cv_test_predict_proba = pipeline.predict_proba(X_cv_test)
            cv_test_roc_auc[fold_counter, c_val_counter] = \
            roc_auc_score(y_cv_test, y_cv_test_predict_proba[:, 1])

            # Testing ROC curves for each fold
            this_fold_roc = roc_curve(y_cv_test, y_cv_test_predict_proba[:, 1])
            cv_test_roc[c_val_counter].append(this_fold_roc)

            # Increment the fold counter
            fold_counter += 1

        # Indicate progress
        print('Done with C = {}'.format(pipeline.get_params()['model__C']))

    return cv_train_roc_auc, cv_test_roc_auc, cv_test_roc
```

### Cross-Validation

Let us define a function to see how the folds are divided within the training data:

```python
cmap = mpl.cm.get_cmap('tab10')

def plot_kfolds(k_folds_iterator, X_train, y_train):

    fold_counter = 0

    for train_index, test_index in k_folds_iterator.split(X_train, y_train):

        # Axis to hold the plot of this fold
        ax = plt.subplot(n_folds, 1, fold_counter+1)

        # Background rectangle representing all samples
        n_train_samples = len(y_train)
        rect = mpl.patches.Rectangle(xy=(0,0), width=n_train_samples, height=1)
        ax.add_patch(rect)

        # Plot each testing sample from this fold as a vertical line
        for this_text_ix in test_index:
            ax.plot([this_text_ix, this_text_ix], [0, 1], color=cmap(1),
                    linewidth=0.75)

        # Plot formatting
        ax.set_xticks([])
        ax.set_yticks([])
        ax.set_xlim(0, n_train_samples)
        ax.set_ylim(0,1)

        # Subplot titles
        if fold_counter == 0:
            ax.text(0.01, 1.2, 'Training data,',
                    transform=ax.transAxes, backgroundcolor = cmap(0))
            ax.text(0.20, 1.2, 'testing data:',
                transform=ax.transAxes, backgroundcolor = cmap(1))
            ax.text(0.45, 1.2, 'Fold {}'.format(fold_counter+1), transform=ax.transAxes)
        else:
            ax.text(0.45, 1.2, 'Fold {}'.format(fold_counter+1), transform=ax.transAxes)

        fold_counter += 1

    plt.tight_layout()
```

```python
n_folds = 4
k_folds = KFold(n_splits=n_folds)
```

And now the result:

```python
plot_kfolds(k_folds, X_train, y_train)
```

If a Stratified sampling is needed, use the StratifiedKfold class:

```python
k_folds_stratified = StratifiedKFold(n_splits=n_folds)
```

```python
plot_kfolds(k_folds_stratified, X_train, y_train)
```

To "mix up" a particlar dataset and the sampling process, use the shuffle argument.

Now, let's run the cross_val_C_search function with the k_folds:

```python
cv_train_roc_auc, cv_test_roc_auc, cv_test_roc = \
cross_val_C_search(k_folds_stratified, C_vals, scale_lr_pipeline, X_train, y_train)
```

    Done with C = 100.0
    Done with C = 10.0
    Done with C = 1.0
    Done with C = 0.1
    Done with C = 0.01
    Done with C = 0.001

Let's see the results for each fold and the mean value of all folds:

```python
for this_fold in range(k_folds.n_splits):
    plt.plot(C_val_exponents, cv_train_roc_auc[this_fold], '-o',
             color=cmap(this_fold), label='Training fold {}'.format(this_fold+1))
    plt.plot(C_val_exponents, cv_test_roc_auc[this_fold], '-x',
             color=cmap(this_fold), label='Testing fold {}'.format(this_fold+1))

plt.ylabel('ROC AUC')
plt.xlabel('log$_{10}$(C)')
plt.legend(loc = [1.1, 0.2])
plt.title('Cross validation scores for each fold')
```

```python
plt.plot(C_val_exponents, np.mean(cv_train_roc_auc, axis=0), '-o',
        label='Average training score')
plt.plot(C_val_exponents, np.mean(cv_test_roc_auc, axis=0), '-x',
        label='Average testing score')

plt.ylabel('ROC AUC')
plt.xlabel('log$_{10}$(C)')
plt.legend()
plt.title('Cross validation on Case Study problem')
```

The Regularization did not impart much benefit here, as was be expected (there are few features, only two had powerful predictive capacity, and lots of samples). While the model performance increased over previous efforts by using all the features available, it appears there was no overfitting going on. Instead, the training and testing scores were about the same. Instead of overfitting, it's possible that the model was underfitting. Let's try engineering some interaction features to see if they can improve performance.

### Feature Engineer

Considering the study case dataset, a total of 17 features exist and a total of 26,664 samples were found. Let us feature engineer new variables that may capture the default behaviour besides the one we already saw:

```python
make_interactions = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)

X_interact = make_interactions.fit_transform(X)
X_interact.shape
```

    (26664, 153)

From this it is possible to see that the new number of features is 153. which is 17 + "17 choose 2" = 17 + 136 = 153. The "17 choose 2" part comes from choosing all possible combinations of 2 features to interact from the possible 17.

```python
X_train, X_test, y_train, y_test = train_test_split(X_interact,
                                                    df['default_payment_next_month'].values,
                                                    test_size=0.2,
                                                    random_state=24)
```

The following might take a while:

```python
cv_train_roc_auc, cv_test_roc_auc, cv_test_roc = \
    cross_val_C_search(k_folds, C_vals, scale_lr_pipeline, X_train, y_train)
```

    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "

    Done with C = 100.0

    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "

    Done with C = 10.0

    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "
    /home/jeancarlo/venv/lib/python3.8/site-packages/sklearn/linear_model/_sag.py:328: ConvergenceWarning: The max_iter was reached which means the coef_ did not converge
      warnings.warn("The max_iter was reached which means "

    Done with C = 1.0
    Done with C = 0.1
    Done with C = 0.01
    Done with C = 0.001

```python
for this_fold in range(k_folds.n_splits):
    plt.plot(C_val_exponents, cv_train_roc_auc[this_fold], '-o',
             color=cmap(this_fold), label='Training fold {}'.format(this_fold+1))
    plt.plot(C_val_exponents, cv_test_roc_auc[this_fold], '-x',
             color=cmap(this_fold), label='Testing fold {}'.format(this_fold+1))

plt.ylabel('ROC AUC')
plt.xlabel('log$_{10}$(C)')
plt.legend(loc = [1.1, 0.2])
plt.title('Cross validation scores for each fold')
```

```python
plt.plot(C_val_exponents, np.mean(cv_train_roc_auc, axis=0), '-o',
        label='Average training score')
plt.plot(C_val_exponents, np.mean(cv_test_roc_auc, axis=0), '-x',
        label='Average testing score')

plt.ylabel('ROC AUC')
plt.xlabel('log$_{10}$(C)')
plt.legend()
plt.title('Cross validation on Case Study problem')
```

Engineering the interaction features increased the best model testing score to about ROC AUC = 0.74 on average across the folds, from about 0.72 without including interactions. These scores happened at C = 100, that is, with negligible regularization.

On the plot of training versus testing scores for the model with interactions, it is possible to see that the training score was a bit higher than the testing score, so it could be said that some amount of overfitting was going on. However, it could not be possible to increase the testing score through regularization here, so this may
not be a problematic instance of overfitting. In most cases, whatever strategy yields the highest testing score is the best strategy.


```python
%matplotlib inline
```

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sympy as sym

import solowpy
```

```python
# define model parameters
ces_params = {'A0': 1.0, 'L0': 1.0, 'g': 0.02, 'n': 0.03, 's': 0.15,
              'delta': 0.05, 'alpha': 0.33, 'sigma': 1.01}

# create an instance of the solow.Model class
ces_model = solowpy.CESModel(params=ces_params)
```

# 5. Impulse response functions

Impulse response functions (IRFs) are a standard tool for analyzing the short run dynamics of dynamic macroeconomic models, such as the Solow growth model, in response to an exogenous shock. The `solow.impulse_response.ImpulseResponse` class has several attributes and methods for generating and analyzing impulse response functions.

```python
# use tab completion to see the available attributes and methods...
solowpy.impulse_response.ImpulseResponse.
```

The `solow.Model` class provides access to all of the functionality of the `solow.impulse_response.ImpulseResponse` class through its `irf` attribute.

```python
# use tab completion to see the available attributes and methods...
ces_model.irf.
```

### Example: Impact of a change in the savings rate
One can analyze the impact of a doubling of the savings rate on model variables as follows.

```python
# 100% increase in the current savings rate...
ces_model.irf.impulse = {'s': 2.0 * ces_model.params['s']}

# in efficiency units...
ces_model.irf.kind = 'efficiency_units'
```

Take a look at the IRF for the savings rate shock. Note that while capital and output are unaffected at the `t=0`, both consumption and investment jump (in opposite directions!) in response to the change in the savings rate.

```python
# ordering of variables is t, k, y, c, i!
print(ces_model.irf.impulse_response[:25,])
```

    [[-10.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -9.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -8.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -7.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -6.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -5.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -4.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -3.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -2.           1.83266593   1.22177729   1.03851069   0.18326659]
     [ -1.           1.83266593   1.22177729   1.03851069   0.18326659]
     [  0.           1.83266593   1.22177729   0.8552441    0.36653319]
     [  1.           2.01275138   1.26032782   0.88222947   0.37809834]
     [  2.           2.18618199   1.29535125   0.90674587   0.38860537]
     [  3.           2.35264866   1.32727865   0.92909505   0.39818359]
     [  4.           2.51198357   1.35646703   0.94952692   0.40694011]
     [  5.           2.66412817   1.38321633   0.96825143   0.4149649 ]
     [  6.           2.80910835   1.40778168   0.98544717   0.4223345 ]
     [  7.           2.94701497   1.43038227   1.00126759   0.42911468]
     [  8.           3.07798847   1.45120817   1.01584572   0.43536245]
     [  9.           3.20220659   1.47042544   1.02929781   0.44112763]
     [ 10.           3.31987457   1.48818014   1.0417261    0.44645404]
     [ 11.           3.43121733   1.50460152   1.05322106   0.45138045]
     [ 12.           3.53647313   1.5198045    1.06386315   0.45594135]
     [ 13.           3.63588855   1.53389178   1.07372425   0.46016754]
     [ 14.           3.72971448   1.54695551   1.08286886   0.46408665]]

### Example: Plotting an impulse response function
One can use a convenience method to to plot the impulse response functions for a particular variable.

```python
# check the docstring to see the call signature
ces_model.irf.plot_impulse_response?
```

```python
fig, ax = plt.subplots(1, 1, figsize=(8,6))
ces_model.irf.plot_impulse_response(ax, variable='output')
plt.show()
```

### Example: More complicated impulse responses are possible
Note that by defining impulses as dictionaries, one can analyze extremely general shocks. For example, suppose that an exogenous 5% increase in the growth rate of technology was accompanied by a simultaneous 10% fall in the savings rate.

```python
# more complicate shocks are possible
ces_model.irf.impulse = {'s': 0.9 * ces_model.params['s'], 'g': 1.05 * ces_model.params['g']}

# in efficiency units...
ces_model.irf.kind = 'per_capita'
```

```python
fig, ax = plt.subplots(1, 1, figsize=(8,6))
ces_model.irf.plot_impulse_response(ax, variable='output', log=True)
plt.show()
```

### Example: Interactive impulse reponse functions
Using IPython widgets makes it extremely easy to analyze the various impulse response functions.

```python
from IPython.html.widgets import fixed, interact, FloatSliderWidget
```

```python
def interactive_impulse_response(model, shock, param, variable, kind, log_scale):
    """Interactive impulse response plotting tool."""
    # specify the impulse response
    model.irf.impulse = {param: shock * model.params[param]}
    model.irf.kind = kind

    # create the plot
    fig, ax = plt.subplots(1, 1, figsize=(8,6))
    model.irf.plot_impulse_response(ax, variable=variable, log=log_scale)

irf_widget = interact(interactive_impulse_response,
                      model=fixed(ces_model),
                      shock = FloatSliderWidget(min=0.1, max=5.0, step=0.1, value=0.5),
                      param = ces_model.params.keys(),
                      variable=['capital', 'output', 'consumption', 'investment'],
                      kind=['efficiency_units', 'per_capita', 'levels'],
                      log_scale=False,
                      )
```

```python

```

examples/5 Impulse response functions.ipynb


Universidade Federal do Rio Grande do Sul (UFRGS)
Programa de Pós-Graduação em Engenharia Civil (PPGEC)

# PEC00144: Métodos experimentais na engenharia civil

### Trabalho final
_Adriano Rodrigues da Silva_

_Daniel Barbosa Mapurunga Matos_

```python
#======================================================#
# 1. IMPORTING NECESSARY CLASSES AND DIMENSIONAL MATRIX#
#======================================================#

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.linalg as sc
import scipy.stats as st
from MRPy import *

DimData = pd.read_excel('Resources/DimData.xlsx')

```

## 1. Introdução

 Este trabalho consiste no projeto e ensaio de um modelo reduzido e está dividido como apresentado abaixo:

 * Projeto de modelo reduzido;
 * Construção do modelo reduzido;
 * Análise da propagação de erro;
 * Análise do sinal;
 * Instrumentação;
 * Análise do sinal;
 * Conclusão.

O modelo real consiste em um _Shear building_ de 3 pavimentos, com suas características expostas abaixo:

* Dimensões dos pilares:

  $B\times H = 0.7 \times 0.2{\rm m}$

  Propriedades da seção:

  $A = 0.14{\rm m}^2$ e $I = 0.000466 {\rm m}^4$

* Massa de um pavimento:

  $m = 38400 {\rm kg}$

*  Altura dos pilares:

  $h =3{\rm m}$

* Módulo de elasticidade do concreto:

  $E_{\rm c} = 28 \times 10^{9} {\rm N/m}^2$

* Rigidez à flexão:

  $ E_cI = 13.04 * 10^6 Nm^2 $

* Rigidez do pilar:

  $ K = \frac{12 E_cI}{h^3} = 57.99*10^6 N/m$

O objetivo desta análise é a determinação da frequência fundamental de vibração da estrutura por meio de ensaios e verificar se estes resultados convergem para os valores calculados por meio das expressões disponíveis na literatura.

## 2. Projeto de modelo reduzido

Para a realização do projeto de modelo reduzido, é necessário definir as 3 grandezas que formarão a nova base da matriz dimensional. Para isto, como o modelo visa a análise das frequências naturais, utiliza-se como nova base as grandezas de comprimento, aceleração e rigidez à flexão.

```python
#======================================================#
# 2. MAKING DIMENSIONAL MATRIX                         #
#======================================================#
ABC    = ['L', 'a', 'EI']
LMT    = ['L', 'M', 'T']
base   =  DimData.loc[ABC, LMT]
i_base =  np.linalg.inv(base)

print(base)
```

        L  M  T
    L   1  0  0
    a   1  0 -2
    EI  3  1 -2

Para definir o fator de escala da rigidez à flexão, é necessário impor as dimensões da seção transversal utilizada no modelo reduzido e o material que será utilizado. No presente estudo, será utilizada uma chapa de dimensões $ 23 \times 0.5 mm$. Abaixo está apresentado o cálculo da rigidez à flexão do modelo reduzido.
* Módulo de elasticidade do alumínio
$$ E = 71 GPa $$
* Momento de inércia da seção
$$ I = \frac{0.023 * 0.00045^3}{12} = 1.746*10^{-13} m^4$$
* Rigidez à flexão
$$ EI = 0.012401 Nm^2 $$

As escalas adotadas no modelo são:

```python
#======================================================#
# 3. DEFINING NEW SCALES                               #
#======================================================#
λ_L    =  1/30                         # Escala de comprimento do modelo reduzido
λ_a    =  1                            # Escala de acelração
λ_EI    = 0.012401/(13.04*10**6)       # Modelo construído em alumínio com seção(0.5X23mm)

```

Agora, é possível calcular o valor das gradezas desejadas em função da nova base dimensional.

```python
#======================================================#
# 4. MAKING NEW DIMENSIONAL BASE MATRIX                #
#======================================================#
par    = ['f', 'EI', 'm']
npar   =  len(par)
DimMat =  DimData.loc[par, LMT]

print(DimMat)
```

        L  M  T
    f   0  0 -1
    EI  3  1 -2
    m   0  1  0

```python
scales =  np.tile([λ_L, λ_a, λ_EI],(npar,1))

NewMat = pd.DataFrame(data    = np.matmul(DimMat, i_base),
                      index   = DimMat.index,
                      columns = ABC)
print(NewMat)
```

                   L    a   EI
    f  -5.000000e-01  0.5  0.0
    EI  4.440892e-16  0.0  1.0
    m  -2.000000e+00 -1.0  1.0

por fim, calculam-se as escalas:

```python
#======================================================#
# 5. PRINTING NECESSARY SCALES                         #
#======================================================#
[λ_f, λ_EI, λ_m]  = np.prod(scales**NewMat, axis=1);

print('Frequência:             λ_f  = 1:{0:4.5f}'.format(1/λ_f),  '\n'
      'Rigidez à flexão:       λ_EI = 1:{0:4.0f}'.format(1/λ_EI), '\n'
      'Massa:                  λ_m  = 1:{0:4.1f}'.format(1/λ_m))
```

    Frequência:             λ_f  = 1:0.18257
    Rigidez à flexão:       λ_EI = 1:1051528103
    Massa:                  λ_m  = 1:1168364.6

Sendo assim, o valor da massa por pavimento será:

```python
print('Massa do pavimento:     {0:5.3f}g'.format(38450*λ_m*1000))
```

    Massa do pavimento:     32.909g

## 3. Construção do modelo reduzido
Para a construção do modelo, utilizou-se tiras de chapa de alumínio, de seção $ 23 \times 0.5 mm $ conforme especificado no projeto. O comprimento destas tiras é 35 cm, sendo 30 cm o valor calculado em projeto e os outros 5 cm para realizar o engaste na base. Para representar as massas, serão utilizadas cantoneiras de alumínio, de massa por comprimento linear igual a $1.82g/cm$. Para representar os engastes, serão utilizadas 12 presilhas( 4 em cada pavimento) de papel junto a 6 suportes de alumínio(2 por pavimento), que serão tratadas como variáveis aleatórias. A rotina a seguir calcula a média e o desvio padrão destas variáveis, assim como o tamanho da cantoneira que deverá ser utilizada no modelo.

```python
#======================================================#
# 6. CALCULATING THE NECESSARY LENGHT OF THE BAR       #
#======================================================#
m_p = [2.18,2.16,2.18,2.21,2.12,2.19,2.18,2.17,2.20,2.16,2.18,2.20] #massa das presilhas em [g]
m_s = [3.61,3.59,3.62,3.60,3.60,3.59]                               #massa dos suportes em  [g]

mi_p = np.mean(m_p)
sigma_p = np.std(m_p)

mi_s = np.mean(m_s)
sigma_s = np.std(m_s)

lc = (38450*λ_m*1000 - 4*mi_p - 2*mi_s)/1.82                    # Comprimento da cantoneira

print('--------------------------------------------','\n',
      'Massa média da presilha:           {0:5.2f}g'.format(mi_p),'\n',
      'Desvio padrão da presilha:         {0:5.2f}g'.format(sigma_p),'\n',
      'Massa média do suporte:            {0:5.2f}g'.format(mi_s),'\n',
      'Desvio padrão do suporte:          {0:5.2f}g'.format(sigma_s),'\n',
      'Comprimento da cantoneira:         {0:5.2f}cm'.format(lc),'\n',
      '--------------------------------------------')

```

    --------------------------------------------
     Massa média da presilha:            2.18g
     Desvio padrão da presilha:          0.02g
     Massa média do suporte:             3.60g
     Desvio padrão do suporte:           0.01g
     Comprimento da cantoneira:          9.34cm
     --------------------------------------------

Para calcular o valor massa por pavimento, considera-se ela uma função de variáveis aleatórias, sendo expressa por:
\begin{equation}
M = 4m_p + 2m_s + 1.82l_c
\end{equation}
Em que $m_p$ e $m_s$ são, respectivamente, as massas da presilha e do suporte que serão consideradas variáveis aleatórias. Já que a função mostrada acima é linear, sendo as varáveis consideradas não correlacionadas, é possível utilizar a seguinte expressão para calcular o desvio padrão da massa do pavimento:
\begin{equation}
\sigma_M^2 = \sum_{i=1}^{n} a_i^2 \sigma_X^2
\end{equation}
Sendo $a_i$ coeficientes que multiplicam as variáveis e  $\sigma_X$ o desvio padrão de cada variável.

```python
#======================================================#
# 7.ERROR PROPAGATION IN FLOOR MASS                    #
#======================================================#
mi_M = 4* mi_p + 2* mi_s+ lc*1.82
sigma_M = np.sqrt((4*sigma_p)**2+(2*sigma_s)**2)
print('--------------------------------------------','\n',
      'Massa média de um pavimento:          {0:5.2f}g'.format(mi_M),'\n',
      'Desvio Padrão:                        {0:5.2f}g'.format(sigma_M),'\n',
      '--------------------------------------------')

```

    --------------------------------------------
     Massa média de um pavimento:          32.91g
     Desvio Padrão:                         0.09g
     --------------------------------------------

## 4. Análise da propagação de erro

Para definir o erro da frequência natural, utiliza-se uma simulação de Monte Carlo, considerando como variáveis aleatórias gaussianas o momento de inércia de área,onde admite-se um erro de 5% devido a imprecisão no corte da chapa, e a massa dos pavimentos, conforme foi calculado no terceiro tópico deste trabalho. Os valores médios e seus respectivos desvios padrões estão apresentados abaixo:

$$ I = 1.746 \pm 0.0873 *10^{-13} m^4$$
$$ M = 32.91 \pm 0.09 g$$

Devido a imprecisão na ligação dos engastes, adiciona-se um erro de 0.5 cm no comprimento das chapas de alumínio.
$$ L = 0.10 \pm 0.005 m $$

```python
#======================================================#
# 8. ERROR PROPAGATION IN NATURAL FREQUENCYS           #
#======================================================#
E = 71e9
I = st.norm(1.746e-13,0.0873e-13)
M = st.norm(32.91/1000,0.09/1000)
L = st.norm(0.10,0.005)
#----------------------------------#
def autoval(E,I,L,M):

    K = 12*E*I/(L*L*L)
    M_K = np.array([[4*K,-2*K,0],
                   [-2*K, 4*K,-2*K],
                   [0,-2*K,2*K]])

    M_M = np.identity(3)*M

    w21,Phi1 = sc.eig(M_K,M_M)

    iw = w21.argsort()
    w21 = w21[iw]
    Phi1 = Phi1[:,iw]

    wr = np.real(w21)
    wk = np.sqrt(w21)
    fk = wk/(2*np.pi)

    return fk[0]
#------------------------------------#
n = 50000
ii = I.rvs(n)
m = M.rvs(n)
l = L.rvs(n)
fi = []
for i in range(n):

    f = autoval(E,ii[i],l[i],m[i])
    fi += [np.real(f)]

mi_f = np.mean(fi)
sigma_f = np.std(fi)
print('---------------------------------------','\n',
      'Valor médio da frequência:    {0:5.2f}hz'.format((mi_f)),'\n',
      'Valor do erro:                {0:5.2f}hz' .format(sigma_f),'\n',
      'Coeficiente de variação:      {0:5.2f}%'.format(sigma_f/np.real(mi_f)*100),'\n',
      '--------------------------------------')

```

    ---------------------------------------
     Valor médio da frequência:     6.77hz
     Valor do erro:                 0.54hz
     Coeficiente de variação:       8.02%
     --------------------------------------

Afim de verificar o tipo de distribuição da frequência, plota-se a função de probabilidade acumulada e o histograma da resposta:

```python
#======================================================#
# 9. PLOTING FREQUENCY PROBABILITY FUNCTIONS           #
#======================================================#
fx = sorted(fi)               #Colocando em ordem as frequências
fp = st.norm(mi_f,sigma_f)    # Criando uma distribuição gaussiana
p = np.arange(0,n)/(n-1)      # Cálculo da função de probabilidade acumulada

plt.figure(1,figsize=(12,5))

plt.subplot(1,2,1)
plt.title('Função de probabilidade acumulada',size = 12)
plt.plot(fx,p,'black',drawstyle = 'steps')

plt.axis([5, 8.5, 0, 1.1])
plt.grid(True)
plt.subplot(1,2,2)

plt.title('Histograma')
plt.hist(fx,bins= 64, range =(5,8),density= True,color = 'black',histtype = 'step')
plt.plot(fx,fp.pdf(fx),'r:')
plt.xlim(5,8.5)
plt.legend(('Função de probabilidade Gaussiana','Distribuição de probabilidades da resposta'))
plt.grid(True)
```

A partir do exposto, é notável que a distribuição de frequências também é do tipo gaussiana.

## 5. Instrumentação
Para a realização do ensaio, utiliza-se o acelerômetro triaxial MPU6050 fixado no pavimento superior do modelo reduzido. O sinal deste acelerômetro será lido com auxílio do microcomputador Raspberry Pi 3 model B, a partir de ligações do tipo $I^2C$. A ligação entre os dois é feita a partir de fios de cobre esmaltado, afim de que estes não colaborem para o amortecimento do modelo. A Figura 2 ilustra o esquema de ligação entre o acelerômetro e o Raspberry.

O cabo vermelho corresponde ao VCC, que produz uma voltagem de 3,3 V, enquanto o preto simboliza o terra(GND). Já os cabos verde e amarelo representam, respectivamente, as ligações SDA e SCL, que serão responsáveis pela transmissão dos dados.

A Figura 3 apresenta o modelo instrumentado.

## 6. Análise do sinal
Para a determinação da frenquência fundamental de vibração, submete-se a estrutura a um deslocamento inicial, fazendo-a descrever um movimento de vibração livre amortecida. A partir da série temporal, utiliza-se o artifício do periodograma para observar qual frequência possui a maior energia armazenada, sendo esta a frequência fundamental desejada.

A partir de uma rotina em Python utilizada no Raspberry, é possível obtenção dos dados fornecidos pelo MPU6050. Com o auxílio da biblioteca ``MRPY``, é possível a realização da leitura das acelerações e da plotagem dos seus respectivos periodogramas. Este procedimento foi realizado em um Jupyter Notebook auxiliar ( ``import data``) e importado a partir da biblioteca pickle, como apresentado a seguir:

```python
#======================================================#
# 10. IMPORTING AND PLOTING SIGNALS                    #
#======================================================#
import pickle as pk
with open('sample_freq.pk', 'rb') as KM:
    az, t, Saz, f,az2, t2, Saz2, f2,az3, t3, Saz3, f3 = pk.load(KM)

plt.figure(2,figsize=(15,12))
plt.subplot(3,2,1)
plt.title('Aceleração 1')
plt.plot(t, az[0],)
plt.xlim(0,17.5)
plt.ylabel('a_x (m/s^2)')
plt.grid(True)

plt.subplot(3,2,2)
plt.title('Periodograma 1')
plt.plot(f, Saz[0])
plt.ylabel('Densidade espectral')
plt.xlim(0,10)
plt.grid(True)

plt.subplot(3,2,3)
plt.title('Aceleração 2')
plt.plot(t2, az2[0])
plt.xlim(0,17.5)
plt.ylabel('a_x (m/s^2)')
plt.grid(True)

plt.subplot(3,2,4)
plt.title('Periodograma 2')
plt.plot(f2, Saz2[0])
plt.ylabel('Densidade espectral')
plt.xlim(0,10)
plt.grid(True)

plt.subplot(3,2,5)
plt.title('Aceleração 3')
plt.plot(t3, az3[0])
plt.xlim(0,17.5)
plt.xlabel('tempo (s)')
plt.ylabel('a_x (m/s^2)')
plt.grid(True)

plt.subplot(3,2,6)
plt.title('Periodograma 3')
plt.plot(f3, Saz3[0])
plt.ylabel('Densidade espectral')
plt.xlim(0,10)
plt.xlabel('Fequência(hz)')
plt.grid(True)
```

Para encontrar o valor exato da frequência que possui maior energia em cada espectro, utiliza-se a rotina a seguir:

```python
#======================================================#
# 11. PRINTING NATURAL FREQUENCYS                      #
#======================================================#
f_max1 =f[Saz[0].argmax()]
f_max2 =f2[Saz2[0].argmax()]
f_max3 =f3[Saz3[0].argmax()]

print(' As frequências naturais em cada ensaio são:','\n',
     '  Ensaio 1:               {0:5.2f}hz'.format(f_max1),'\n',
     '  Ensaio 2:               {0:5.2f}hz'.format(f_max2),'\n',
     '  Ensaio 3:               {0:5.2f}hz'.format(f_max3),'\n',)

```

     As frequências naturais em cada ensaio são:
       Ensaio 1:                6.73hz
       Ensaio 2:                7.08hz
       Ensaio 3:                6.73hz

## 7. Resultados e considerações finais

A partir dos valores de frequência obtidos com o uso dos espectros, é possível concluir que os valores encontrados estão dentro da margem  de erro estipulada no Capítulo 4 deste trabalho, concluindo, assim, que o modelo experimental valida a metodologia encontrada na literatura.
Para conferir se a escala de frequência adotada neste trabalho está correta, calcula-se o valor da frequência fundamental do modelo real a partir da função autoval, definidada no Capítulo 4.

```python
#======================================================#
# 12. PRINTING WORKS RESULTS                           #
#======================================================#
F_real = np.real(autoval(28e9,0.000466,3,38400))
print('-------------------------------------------------------','\n',
     'Frequência fundamental do modelo real:       {0:5.2f}hz'.format(F_real),'\n',
     'Escala de frequência:                         1:{0:5.2f}'.format(1/λ_f),'\n',
     'Frequência fundamental do modelo reduzido:   {0:5.2f}hz'.format(mi_f),'\n',
     'Frequência fundamental escalada:             {0:5.2f}hz'.format(mi_f/λ_f),'\n',
     '-------------------------------------------------------',)
```

    -------------------------------------------------------
     Frequência fundamental do modelo real:        1.23hz
     Escala de frequência:                         1: 0.18
     Frequência fundamental do modelo reduzido:    6.77hz
     Frequência fundamental escalada:              1.24hz
     -------------------------------------------------------

Com isso, é possível afirmar, também, que a escala obtida a partir da análise dimensional está correta, visto que o erro é desprezível.

Portanto, a partir do exposto neste trabalho, é possível concluir que a utilização do modelo reduzido como ferramenta de projeto é bastante proveitoso para problemas em que a formulação teórica  é de caráter complexo. A análise de propagação de erro, considerando como variáveis aleatórias a massa, o comprimento e a inércia possibilitou definir o erro admissível para a medição experimental, sendo fundamental para a a validação dos dados encontrados.

A utilização do MPU6050 em conjunto com o Raspberry Pi se provou bastante eficiente para a medição da frequência fundamental de vibração, sendo possível realizar até mesmo a medição das outras frequências naturais de vibração da estrutura, apesar de possuirem uma menor energia associada.